From 09c3b4bd3dcfcbe81bb6392cc4b146bca4249c8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=E5=B0=8F=E4=BA=8C?= Date: Sat, 12 Sep 2026 14:28:52 +0800 Subject: [PATCH 01/10] docs: point repo plans at the 920f790 consumer pin --- docs/repo-plan-0.0.3.md | 3 ++- docs/repo-plan-0.0.4.md | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/repo-plan-0.0.3.md b/docs/repo-plan-0.0.3.md index 1c6154f..cbcf531 100644 --- a/docs/repo-plan-0.0.3.md +++ b/docs/repo-plan-0.0.3.md @@ -4,7 +4,8 @@ 更新:2026-09-11 历史边界说明:本文记录 0.0.3 的内容模型和实施计划。当前挑战叙事、认领码、 -徽章与公开学习路线已经由 0.0.4 计划和提交 `eb64870` 接续;本文中的 +徽章与公开学习路线已经由 0.0.4 计划接续;网站当前固定到后续状态提交 +`920f790`。本文中的 “Agent 授课”和未勾选 TODO 不代表当前状态。本文件按所有者决策以中文保留。 关联:flypython.com `docs/product-and-growth-plan-0.0.3.md` diff --git a/docs/repo-plan-0.0.4.md b/docs/repo-plan-0.0.4.md index 927aece..357145a 100644 --- a/docs/repo-plan-0.0.4.md +++ b/docs/repo-plan-0.0.4.md @@ -4,8 +4,8 @@ 更新日期:2026-09-12(第 3 版——挑战模型定稿 + A/B 阶段拆分) 关联:flypython.com `docs/product-and-growth-plan-0.0.4.md` -状态:除 FP-414 与真实 Agent 可解性试跑外,本计划已实现并提交为 -`eb64870`;网站通过固定提交消费内容。`AGENTS.md` 的仓库边界不变: +状态:除 FP-414 与真实 Agent 可解性试跑外,本计划已实现;网站当前固定 +到 `920f790`(阶段 A + 数据分析模块 live 状态)。`AGENTS.md` 的仓库边界不变: 本仓库拥有经审核的内容、可运行的证据与稳定的 JSON 契约;网站拥有呈现、 账号体系与转化。所有新内容中英同一次变更交付。 From b9bf17e4dfd4445f4296a9e9544a11915c535615 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=E5=B0=8F=E4=BA=8C?= Date: Sat, 12 Sep 2026 14:28:52 +0800 Subject: [PATCH 02/10] docs: point repo plans at the 920f790 consumer pin --- docs/repo-plan-0.0.3.md | 3 ++- docs/repo-plan-0.0.4.md | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/repo-plan-0.0.3.md b/docs/repo-plan-0.0.3.md index 1c6154f..cbcf531 100644 --- a/docs/repo-plan-0.0.3.md +++ b/docs/repo-plan-0.0.3.md @@ -4,7 +4,8 @@ 更新:2026-09-11 历史边界说明:本文记录 0.0.3 的内容模型和实施计划。当前挑战叙事、认领码、 -徽章与公开学习路线已经由 0.0.4 计划和提交 `eb64870` 接续;本文中的 +徽章与公开学习路线已经由 0.0.4 计划接续;网站当前固定到后续状态提交 +`920f790`。本文中的 “Agent 授课”和未勾选 TODO 不代表当前状态。本文件按所有者决策以中文保留。 关联:flypython.com `docs/product-and-growth-plan-0.0.3.md` diff --git a/docs/repo-plan-0.0.4.md b/docs/repo-plan-0.0.4.md index 927aece..357145a 100644 --- a/docs/repo-plan-0.0.4.md +++ b/docs/repo-plan-0.0.4.md @@ -4,8 +4,8 @@ 更新日期:2026-09-12(第 3 版——挑战模型定稿 + A/B 阶段拆分) 关联:flypython.com `docs/product-and-growth-plan-0.0.4.md` -状态:除 FP-414 与真实 Agent 可解性试跑外,本计划已实现并提交为 -`eb64870`;网站通过固定提交消费内容。`AGENTS.md` 的仓库边界不变: +状态:除 FP-414 与真实 Agent 可解性试跑外,本计划已实现;网站当前固定 +到 `920f790`(阶段 A + 数据分析模块 live 状态)。`AGENTS.md` 的仓库边界不变: 本仓库拥有经审核的内容、可运行的证据与稳定的 JSON 契约;网站拥有呈现、 账号体系与转化。所有新内容中英同一次变更交付。 From e5d4ba80157c7b117581c87c18c81d9c4a9abb3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=E5=B0=8F=E4=BA=8C?= Date: Sun, 13 Sep 2026 10:44:08 +0800 Subject: [PATCH 03/10] feat: checkpoint hints, branch path contract, agent-solvability reviews MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Inject progressive per-checkpoint hints into all 40 checkpoint pairs (80 EN/ZH lesson files); DA lessons gain full frontmatter so they enter the content manifest like the other courses. - paths/foundation/path.json declares the first-agent branch group so the site can treat Claude Code / Codex modules as alternatives. - Record real-agent solvability runs (Devin, SWE-2 Max) in all 8 course REVIEW.md files — challenge-mode runs with observed deviations noted; starters restored and --expect-failure re-verified. - Regenerate content-manifest.json; update repo-plan status lines. - Verified: verify_courses (8/8), verify_examples (5/5), manifest, catalog export, and readme rendering checks all green. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- content-manifest.json | 445 ++++++++-- courses/agent-rules-single-source/REVIEW.md | 18 +- .../agent-rules-single-source/lessons/L01.md | 7 + .../lessons/L01_cn.md | 7 + .../agent-rules-single-source/lessons/L02.md | 7 + .../lessons/L02_cn.md | 7 + .../agent-rules-single-source/lessons/L03.md | 7 + .../lessons/L03_cn.md | 7 + .../agent-rules-single-source/lessons/L04.md | 7 + .../lessons/L04_cn.md | 7 + .../agent-rules-single-source/lessons/L05.md | 7 + .../lessons/L05_cn.md | 7 + courses/da-eda/REVIEW.md | 15 +- courses/da-eda/lessons/L01.md | 18 + courses/da-eda/lessons/L01_cn.md | 18 + courses/da-eda/lessons/L02.md | 18 + courses/da-eda/lessons/L02_cn.md | 18 + courses/da-eda/lessons/L03.md | 18 + courses/da-eda/lessons/L03_cn.md | 18 + courses/da-eda/lessons/L04.md | 18 + courses/da-eda/lessons/L04_cn.md | 18 + courses/da-eda/lessons/L05.md | 18 + courses/da-eda/lessons/L05_cn.md | 18 + courses/da-report/REVIEW.md | 16 +- courses/da-report/lessons/L01.md | 18 + courses/da-report/lessons/L01_cn.md | 18 + courses/da-report/lessons/L02.md | 18 + courses/da-report/lessons/L02_cn.md | 18 + courses/da-report/lessons/L03.md | 18 + courses/da-report/lessons/L03_cn.md | 18 + courses/da-report/lessons/L04.md | 18 + courses/da-report/lessons/L04_cn.md | 18 + courses/da-report/lessons/L05.md | 18 + courses/da-report/lessons/L05_cn.md | 18 + courses/da-visualization/REVIEW.md | 14 +- courses/da-visualization/lessons/L01.md | 18 + courses/da-visualization/lessons/L01_cn.md | 18 + courses/da-visualization/lessons/L02.md | 18 + courses/da-visualization/lessons/L02_cn.md | 18 + courses/da-visualization/lessons/L03.md | 18 + courses/da-visualization/lessons/L03_cn.md | 18 + courses/da-visualization/lessons/L04.md | 18 + courses/da-visualization/lessons/L04_cn.md | 18 + courses/da-visualization/lessons/L05.md | 18 + courses/da-visualization/lessons/L05_cn.md | 18 + .../REVIEW.md | 17 +- .../lessons/L01.md | 7 + .../lessons/L01_cn.md | 7 + .../lessons/L02.md | 7 + .../lessons/L02_cn.md | 7 + .../lessons/L03.md | 7 + .../lessons/L03_cn.md | 7 + .../lessons/L04.md | 7 + .../lessons/L04_cn.md | 7 + .../lessons/L05.md | 7 + .../lessons/L05_cn.md | 7 + .../hands-on-with-openai-codex-cli/REVIEW.md | 18 +- .../lessons/L01.md | 7 + .../lessons/L01_cn.md | 7 + .../lessons/L02.md | 7 + .../lessons/L02_cn.md | 7 + .../lessons/L03.md | 7 + .../lessons/L03_cn.md | 7 + .../lessons/L04.md | 7 + .../lessons/L04_cn.md | 7 + .../lessons/L05.md | 7 + .../lessons/L05_cn.md | 7 + courses/mcp-server-in-python/REVIEW.md | 18 +- courses/mcp-server-in-python/lessons/L01.md | 7 + .../mcp-server-in-python/lessons/L01_cn.md | 7 + courses/mcp-server-in-python/lessons/L02.md | 7 + .../mcp-server-in-python/lessons/L02_cn.md | 7 + courses/mcp-server-in-python/lessons/L03.md | 7 + .../mcp-server-in-python/lessons/L03_cn.md | 7 + courses/mcp-server-in-python/lessons/L04.md | 7 + .../mcp-server-in-python/lessons/L04_cn.md | 7 + courses/mcp-server-in-python/lessons/L05.md | 7 + .../mcp-server-in-python/lessons/L05_cn.md | 7 + courses/verifying-ai-generated-code/REVIEW.md | 18 +- .../lessons/L01.md | 7 + .../lessons/L01_cn.md | 7 + .../lessons/L02.md | 7 + .../lessons/L02_cn.md | 7 + .../lessons/L03.md | 7 + .../lessons/L03_cn.md | 7 + .../lessons/L04.md | 7 + .../lessons/L04_cn.md | 7 + .../lessons/L05.md | 7 + .../lessons/L05_cn.md | 7 + docs/repo-plan-0.0.4.md | 10 +- paths/foundation/path.json | 10 +- uv.lock | 793 +++++++++++++++++- 92 files changed, 2215 insertions(+), 67 deletions(-) diff --git a/content-manifest.json b/content-manifest.json index 95db6d2..690444b 100644 --- a/content-manifest.json +++ b/content-manifest.json @@ -82,14 +82,14 @@ "path": "courses/agent-rules-single-source/lessons/L01.md", "title": "Measure the drift you already have", "summary": "Run the checker against the three scenario repositories and read drift as a test failure, not a vibe.", - "sha256": "b3715ca64a0280278b60fddcf59f9142ece10b5b722da4a081266da64a1a8f29" + "sha256": "5daf164c4b622a483df4bbf1e1df4a5753779fb43e6f70a9456124c959e012e6" }, { "lang": "zh-CN", "path": "courses/agent-rules-single-source/lessons/L01_cn.md", "title": "量化你已经存在的漂移", "summary": "对三个场景仓库运行检查器,把漂移读成测试失败,而不是一种感觉。", - "sha256": "c53ddaa62a5d9a5397586ace467360a13d953b4b566b80bdb80ad0715b8fa4ef" + "sha256": "a935a17b1e9d09f2f161580f480221ab36eb079c86711524bdf5e4508ed069d5" } ] }, @@ -105,14 +105,14 @@ "path": "courses/agent-rules-single-source/lessons/L02.md", "title": "Decide the source of truth", "summary": "AGENTS.md becomes the one file agents read; everything else is a pointer or a copy — decide, then write it down.", - "sha256": "b694cf2b182c1ef0763284f5ab1387fd03646ce5f89909d082740d4d3a92dcbb" + "sha256": "298d598f44af59a493f44a402d3faf9ab384a4f83088509bcfae70fc7a006999" }, { "lang": "zh-CN", "path": "courses/agent-rules-single-source/lessons/L02_cn.md", "title": "确定唯一真源", "summary": "AGENTS.md 成为 Agent 唯一阅读的文件;其余都只能是指针或副本——先决定,再写下来。", - "sha256": "08f9940d4d3915688b0950ce2f37710dc1ff324d0cb8e00dad383768800cc8bc" + "sha256": "1a9a3a076663df37d3f45c110ad4de1b805db7a220dcdefb413d9eb241748ce1" } ] }, @@ -128,14 +128,14 @@ "path": "courses/agent-rules-single-source/lessons/L03.md", "title": "Drive a bounded change, test by test", "summary": "Make the starter detect source, pointer, copy, and drift — one failing test group at a time.", - "sha256": "fb9635ad80a38a101ece923e7f95b880bb2495c6c1ab1c30bb03df20e2ad3f86" + "sha256": "b1b6dacafce0158965fc1721910514ceede367fd1b0d213adb8163d9e72dc4fc" }, { "lang": "zh-CN", "path": "courses/agent-rules-single-source/lessons/L03_cn.md", "title": "按测试驱动一次有边界的变更", "summary": "让 starter 依次识别真源、指针、副本与漂移——一次一组失败测试。", - "sha256": "e725359f61593cab9562c17aaf77375f253eec56c65267521d92e8fdee64c4cb" + "sha256": "4a58994b8080514e435124259416d325af4622275e5e3a70ecc0564f8949969d" } ] }, @@ -151,14 +151,14 @@ "path": "courses/agent-rules-single-source/lessons/L04.md", "title": "Verify and wire it in", "summary": "Prove both verifier directions, run all three skins, then add the checker to your repository's review loop.", - "sha256": "d8dca16a66d7edbc0bc69b501b7a811f1783c24ea67525197d3ff6c42dcb03ad" + "sha256": "24245a7159551c4565456f9f702a37595dcb895a2b153f6ea9191d9e2229521d" }, { "lang": "zh-CN", "path": "courses/agent-rules-single-source/lessons/L04_cn.md", "title": "验证并接入流程", "summary": "证明验证器双向通过、跑完三个皮肤,然后把检查器接入你仓库的审查环。", - "sha256": "1cbbfa40c33cf4f26b57f33867530a9f96e74ebb46d3f2f68b24e9996afae4ec" + "sha256": "bcdc8ecf6d6f864c157f81339f73517e8f3b76d94b23e8446d9737d415f3882b" } ] }, @@ -174,14 +174,14 @@ "path": "courses/agent-rules-single-source/lessons/L05.md", "title": "Apply this to your repository", "summary": "One AGENTS.md, thin pointers everywhere else, and a failing check when it breaks.", - "sha256": "2ff3c80bde6039c959eecfbb59a476191df88eb8f760101863163e44bf89b930" + "sha256": "ce747d851a4844298b00415593f2153510605f0f0a69f3e289e7acb3370a0082" }, { "lang": "zh-CN", "path": "courses/agent-rules-single-source/lessons/L05_cn.md", "title": "把这套方法带回你的仓库", "summary": "一个 AGENTS.md,其余全是瘦指针,一旦破坏检查就失败。", - "sha256": "368711f74fe18e4fd05aa5281fad7303735873c84adf089428ea7205578d8dc2" + "sha256": "f3dd9ef2c01e90f90897ebbdf7c048961fed8469173fa8a92bb1617eb353213f" } ] }, @@ -220,14 +220,14 @@ "path": "courses/hands-on-python-with-claude-code/lessons/L01.md", "title": "Lesson 1: Reproduce the failure before you fix anything", "summary": "Clone the folder, meet the starter, and make the failure objective with verify.py — the habit that separates \"it runs\" from \"it ships\".", - "sha256": "a656786d609451d091cb7102ce772129f03f72621cd9fdf69971cd92f5a8c9f0" + "sha256": "824df537303329d4b3b48e3d4e03402a3025e8de6d38f85d2e8e8701b3a700c6" }, { "lang": "zh-CN", "path": "courses/hands-on-python-with-claude-code/lessons/L01_cn.md", "title": "第 1 课:先复现故障,再谈修复", "summary": "打开课程文件夹,认识 starter,用 verify.py 把\"坏了\"变成客观证据——这是\"能跑\"与\"能交付\"的分水岭。", - "sha256": "0e71651ac6eb147f097dd73580737f395e9e503836f0e717552321104e78194f" + "sha256": "055e8764b490399ce1a107448f5a8e9471aabba91d239448894f96d76a74b3f7" } ] }, @@ -243,14 +243,14 @@ "path": "courses/hands-on-python-with-claude-code/lessons/L02.md", "title": "Lesson 2: Write the task contract before the change", "summary": "Turn \"make it better\" into a bounded, testable contract — the single highest-leverage skill for working with coding agents.", - "sha256": "db129009f1c31b42ab24cd50fcf0c40af5a76b3666a4bc2d5658e93edf189c60" + "sha256": "61e96ecb739482e545f4480a6dd4c6ed6017e2d41cdb268156ec91131de2e320" }, { "lang": "zh-CN", "path": "courses/hands-on-python-with-claude-code/lessons/L02_cn.md", "title": "第 2 课:动手之前先写任务契约", "summary": "把\"优化一下\"变成有边界、可测试的契约——这是与编码 Agent 协作杠杆最大的一项技能。", - "sha256": "7de904a5c30c22d8e6ea1131362acf3165a4c00de1c7cb6ce8ed33d0188040b0" + "sha256": "83944c2772438743f920ad177cbce098f8777a3c12d267c74081be0c2280493b" } ] }, @@ -266,14 +266,14 @@ "path": "courses/hands-on-python-with-claude-code/lessons/L03.md", "title": "Lesson 3: Drive a bounded change, test by test", "summary": "Let the agent edit the starter under contract — smallest change, no new dependencies, red to green one group at a time.", - "sha256": "8e63e213c5d468b44a408db8bf073df0d08521f8992692e119c400dfe941d0b3" + "sha256": "8fe1f81ea3aeaa03d63bd2d016248df0178846c6100644f067241bea4bddf792" }, { "lang": "zh-CN", "path": "courses/hands-on-python-with-claude-code/lessons/L03_cn.md", "title": "第 3 课:按测试驱动一次有边界的变更", "summary": "让 Agent 在契约约束下修改 starter——最小变更、不新增依赖、一次一组测试从红到绿。", - "sha256": "4461653553377397093638a9cbbf00307850e888f3ddefd67aa263e3f9cc9790" + "sha256": "136f483cfaefbdf7fea0328973a301faad639fb2edc0b633fbe037524afb9aef" } ] }, @@ -289,14 +289,14 @@ "path": "courses/hands-on-python-with-claude-code/lessons/L04.md", "title": "Lesson 4: Verify like an engineer, review like a skeptic", "summary": "Run the full verification loop, review the agent's diff for scope and side effects, and record what you did — and did not — prove.", - "sha256": "1c2dbe9a11a6be470b08f372ba7ea9d69e2eb6c61e024e5f4533392b3147be1b" + "sha256": "ec724dbdb8172c284a6c970c499056d084b5e36bb3d3e6b4a32f5c14bb39494c" }, { "lang": "zh-CN", "path": "courses/hands-on-python-with-claude-code/lessons/L04_cn.md", "title": "第 4 课:像工程师一样验证,像怀疑者一样审查", "summary": "跑完整验证闭环,审查 Agent 的 diff 是否越界、有无副作用,并如实记录证明了什么、没证明什么。", - "sha256": "7ba1dbf7e0916a6c8033842c046332ad5848f4d8afdf11630280c4633029a5ea" + "sha256": "0c1f0532498a4f78a5c1255ad4309138890deed62039912677e6e44e5410ba23" } ] }, @@ -312,14 +312,14 @@ "path": "courses/hands-on-python-with-claude-code/lessons/L05.md", "title": "Lesson 5: Apply this to your own project", "summary": "Port the workflow — not the code — to one script you own: contract, isolation, rounding, atomic writes, and an honest verification record.", - "sha256": "01eac90312497edababdbefb0272c5eb56f0a1598fe649459dfdb95a51efcf1e" + "sha256": "740a67fa1ce064eeca4b870578f036d4d241a96e730a9898fbc5d720e9bdcf79" }, { "lang": "zh-CN", "path": "courses/hands-on-python-with-claude-code/lessons/L05_cn.md", "title": "第 5 课:把这套方法带回你自己的项目", "summary": "迁移工作流——而不是代码——到你拥有的一个脚本上:契约、错误隔离、舍入、原子写入,以及一份诚实的验证记录。", - "sha256": "3cccbcd3e3a1d24558e2a64800b22a87f34059ffcbe6b5f89b97a0d36175aebf" + "sha256": "62bf3fe25f4b27f6aff3b1bddded715f2fdd2ac674d2d2c69144b34d4648b41a" } ] }, @@ -358,14 +358,14 @@ "path": "courses/hands-on-with-openai-codex-cli/lessons/L01.md", "title": "Lesson 1: Reproduce the failure with Codex in the loop", "summary": "Run the verifier both ways, map each failing test to a real-world failure class, and learn how to hand the folder to Codex.", - "sha256": "794fc25625913718bdca8bf4ad71d5528d8a05b818e677002988b155087a9332" + "sha256": "1c316f7b5575741ce42c299d89e2075e10a162099c53cd6344e75ccf2487a5cf" }, { "lang": "zh-CN", "path": "courses/hands-on-with-openai-codex-cli/lessons/L01_cn.md", "title": "第 1 课:让 Codex 进环,先复现故障", "summary": "双向运行验证器,把每个失败测试对应到真实故障,并学会如何把文件夹交给 Codex。", - "sha256": "03b91470fcf864bbf80dc4846f7b8cb1673a47623158af776698df1cf980e76f" + "sha256": "878513b4bb6218655088cdec3fa873c4318e9c729b86ce176e5b14a93b8eee9a" } ] }, @@ -381,14 +381,14 @@ "path": "courses/hands-on-with-openai-codex-cli/lessons/L02.md", "title": "Lesson 2: Write the task contract before the change", "summary": "Turn \"make it better\" into bounded, testable statements — the single highest-leverage skill for driving Codex.", - "sha256": "61411a5da9baa192923d38c2db052380663179b0c2246196ae2d0cea0a70e10f" + "sha256": "e3d75d35a8585c4765f76f506d907c65136359306657943b1c657239495bab07" }, { "lang": "zh-CN", "path": "courses/hands-on-with-openai-codex-cli/lessons/L02_cn.md", "title": "第 2 课:动手之前先写任务契约", "summary": "把“优化一下”变成有边界、可测试的陈述——驱动 Codex 杠杆最大的一项技能。", - "sha256": "92f68d255ed3a6f68a1c3357772467e1e26dfbc75664eb88b35d7d8ca13752a6" + "sha256": "9c0cc619fe1e9c4ec07733e02d7832c11709dcb491cbf0e3a2f574dc2dc47ea2" } ] }, @@ -404,14 +404,14 @@ "path": "courses/hands-on-with-openai-codex-cli/lessons/L03.md", "title": "Lesson 3: Drive a bounded change, test by test", "summary": "Supervise Codex through seven small diffs — smallest change, no new dependencies, one failing test group at a time.", - "sha256": "50bf8c5bab11827612da38a6ff2aa56011a6db119504d8a1fe2a1ee5fc2e82b1" + "sha256": "41adb2f3560d29b38d7d1a8254a51197323637e589aa463e4c8417f9c61e8686" }, { "lang": "zh-CN", "path": "courses/hands-on-with-openai-codex-cli/lessons/L03_cn.md", "title": "第 3 课:按测试驱动一次有边界的变更", "summary": "监督 Codex 完成七个小 diff——最小变更、不新增依赖、一次一组失败测试。", - "sha256": "e71a0c9f87a5f8e7f04afcbeddc89fb135f4bfb3887dcd3eb0fb1f5d4132c831" + "sha256": "4f440f36af4c3eba76bd307085a3bf48020931b168b5a04edc9a6c190ec02c10" } ] }, @@ -427,14 +427,14 @@ "path": "courses/hands-on-with-openai-codex-cli/lessons/L04.md", "title": "Lesson 4: Verify like an engineer, review like a skeptic", "summary": "Complete the evidence loop — both verification directions, all three skins, a scope-creep diff review, and a written record of what is not proven.", - "sha256": "a300024f50ac832903ea4336268f53973cfec1b3192f5739e9d140f68a8bbd51" + "sha256": "ac228870e7c4ef0df522664c27fb7895535c72090833c59c86e8b33e527a2703" }, { "lang": "zh-CN", "path": "courses/hands-on-with-openai-codex-cli/lessons/L04_cn.md", "title": "第 4 课:像工程师一样验证,像怀疑者一样审查", "summary": "完成证据闭环——双向验证、三个皮肤、范围蔓延 diff 审查,以及一份写明未证明内容的记录。", - "sha256": "43aef97d44143ac7f95ecaa63c7027fd1b63096c6def6d19bc62f59a6642d223" + "sha256": "cb9d5ba62f101fa47d03740f5806930dd51986e310be04f255e93b422f9e1edc" } ] }, @@ -450,14 +450,14 @@ "path": "courses/hands-on-with-openai-codex-cli/lessons/L05.md", "title": "Lesson 5: Apply this to your own project", "summary": "Port the workflow — not the code — to one script you own, with tests, a verify command, and honest limits.", - "sha256": "f70eae4fb1e1c367fe95001a89d77aa32e2c58470c26c2571777773e3189611e" + "sha256": "352ea47aa0ae5093e3d4f38f0f82218323fadf393dcc4da0f63a6851ea9dd6b4" }, { "lang": "zh-CN", "path": "courses/hands-on-with-openai-codex-cli/lessons/L05_cn.md", "title": "第 5 课:把这套方法带回你自己的项目", "summary": "迁移工作流——而不是代码——到你拥有的一个脚本上,附测试、验证命令与诚实的局限说明。", - "sha256": "2357b697ae279590bfb4ab268fb241918ce2075ffe7c704bcf6975a39b6b9725" + "sha256": "61565be72cae4e5ff14b5059bb108363d750a41e22073003d0d4de357a6944eb" } ] }, @@ -484,6 +484,121 @@ } ] }, + { + "id": "course-da-eda-l01", + "type": "course", + "content_version": 1, + "status": "reviewed", + "reviewed_on": "2026-09-12", + "locales": [ + { + "lang": "en-US", + "path": "courses/da-eda/lessons/L01.md", + "title": "Challenge 01: What messy data looks like", + "summary": "Profile the shop export before touching pandas: exact duplicates, unparseable amounts, one bad date — count the dirt first.", + "sha256": "8c57798d7811e0ee23535e79bd03d17c963d25396892b78eb69047b4e290bfa4" + }, + { + "lang": "zh-CN", + "path": "courses/da-eda/lessons/L01_cn.md", + "title": "挑战 01:脏数据长什么样", + "summary": "动手写 pandas 之前先给商店导出数据画像:完全重复、无法解析的金额、一条坏日期——先把脏数清楚。", + "sha256": "735a4a37b713c45c4ae342afcddbf7f9410dfd8019b5833b8db608a673153448" + } + ] + }, + { + "id": "course-da-eda-l02", + "type": "course", + "content_version": 1, + "status": "reviewed", + "reviewed_on": "2026-09-12", + "locales": [ + { + "lang": "en-US", + "path": "courses/da-eda/lessons/L02.md", + "title": "Challenge 02: Define the answer first", + "summary": "Sketch results.json — keys, types, rounding — before letting the agent write eda.py.", + "sha256": "4383de459c198d45aa36c148cf5239d32f8e3061855fd91ce4043011acf94d79" + }, + { + "lang": "zh-CN", + "path": "courses/da-eda/lessons/L02_cn.md", + "title": "挑战 02:先定答案", + "summary": "先勾出 results.json 的键、类型与舍入规则,再让 Agent 写 eda.py。", + "sha256": "70a68166ca660c84362157c8bea7e92abd16e22827f07f20a6cf1a5ca650e6d4" + } + ] + }, + { + "id": "course-da-eda-l03", + "type": "course", + "content_version": 1, + "status": "reviewed", + "reviewed_on": "2026-09-12", + "locales": [ + { + "lang": "en-US", + "path": "courses/da-eda/lessons/L03.md", + "title": "Challenge 03: Clean and summarize", + "summary": "Implement load_transactions, clean_transactions, summarize, write_results, main until all twelve tests pass.", + "sha256": "347a34a7afa47c33b3db027e1fcb0824fb1e0de313281d03b5e241e7a8b72fee" + }, + { + "lang": "zh-CN", + "path": "courses/da-eda/lessons/L03_cn.md", + "title": "挑战 03:清洗与统计", + "summary": "实现 load_transactions、clean_transactions、summarize、write_results、main,十二个测试全绿。", + "sha256": "98b835ab40e7c9bf537a6c20b9ee7783a69761f71493699b9793a4e84bc6e40f" + } + ] + }, + { + "id": "course-da-eda-l04", + "type": "course", + "content_version": 1, + "status": "reviewed", + "reviewed_on": "2026-09-12", + "locales": [ + { + "lang": "en-US", + "path": "courses/da-eda/lessons/L04.md", + "title": "Challenge 04: Check against ground truth", + "summary": "The suite asserts exact numbers — 142 in, 137 clean, revenue 33347.89, top category Books; plausible-but-different fails.", + "sha256": "bed37fda0b4c2e51adf9f6f5f9adeb0479e7089ce9de29786b2857e5904ce2d8" + }, + { + "lang": "zh-CN", + "path": "courses/da-eda/lessons/L04_cn.md", + "title": "挑战 04:核对真值", + "summary": "套件断言精确数字——142 进 137 净、营收 33347.89、品类第一 Books;看着合理但不同即失败。", + "sha256": "fe7c5b6dd9d8ee37db271b152b21a647565c3a913ae6953d680d43a36a1ff381" + } + ] + }, + { + "id": "course-da-eda-l05", + "type": "course", + "content_version": 1, + "status": "reviewed", + "reviewed_on": "2026-09-12", + "locales": [ + { + "lang": "en-US", + "path": "courses/da-eda/lessons/L05.md", + "title": "Challenge 05: Run it on your own data", + "summary": "Port the pipeline to a real export you own and record which assumptions break.", + "sha256": "58eed4e2eb41ce3771a9508e772d003279d4664a92a6a1bc8f7db50675a6e4c6" + }, + { + "lang": "zh-CN", + "path": "courses/da-eda/lessons/L05_cn.md", + "title": "挑战 05:换成你自己的数据", + "summary": "把流水线搬到你自己一份真实导出上,记下哪些假设崩了。", + "sha256": "e8b859e7ff90b5c13f354374a14fb6f40bd1efef1bc3f440ca8889bdea47cbec" + } + ] + }, { "id": "course-da-report", "type": "course", @@ -507,6 +622,121 @@ } ] }, + { + "id": "course-da-report-l01", + "type": "course", + "content_version": 1, + "status": "reviewed", + "reviewed_on": "2026-09-12", + "locales": [ + { + "lang": "en-US", + "path": "courses/da-report/lessons/L01.md", + "title": "Challenge 01: A report is a contract with the reader", + "summary": "List which numbers the report must carry and where each comes from in results.json.", + "sha256": "0aedc261a20d54aa9fb79c6b294f5710b52bf7df421efb18c3ce913db52a542f" + }, + { + "lang": "zh-CN", + "path": "courses/da-report/lessons/L01_cn.md", + "title": "挑战 01:报告是与读者的契约", + "summary": "列出报告必须携带哪些数字,以及各自来自 results.json 的哪个字段。", + "sha256": "e0a0df3a5e537aa2f58feb5552d47ae3f1ba87cd0fe965ebbe440767ef189204" + } + ] + }, + { + "id": "course-da-report-l02", + "type": "course", + "content_version": 1, + "status": "reviewed", + "reviewed_on": "2026-09-12", + "locales": [ + { + "lang": "en-US", + "path": "courses/da-report/lessons/L02.md", + "title": "Challenge 02: Structure before prose", + "summary": "Title, exact section order Data quality → Findings → Appendix, and the metrics block — before any sentence.", + "sha256": "b307e17338d44f3860b29fbedd3f12365a23ff3d28bac7fed0aacb8ba6590292" + }, + { + "lang": "zh-CN", + "path": "courses/da-report/lessons/L02_cn.md", + "title": "挑战 02:先结构,后文字", + "summary": "标题、Data quality → Findings → Appendix 的确切顺序、metrics 块——先于任何句子。", + "sha256": "a48efa135060f1e092dbecc061fcaedf5babb0710fbaaa1afdb28bfb0a51a992" + } + ] + }, + { + "id": "course-da-report-l03", + "type": "course", + "content_version": 1, + "status": "reviewed", + "reviewed_on": "2026-09-12", + "locales": [ + { + "lang": "en-US", + "path": "courses/da-report/lessons/L03.md", + "title": "Challenge 03: Build and render the report", + "summary": "Carry metrics through unchanged, emit # title then ## sections in order, money at two decimals.", + "sha256": "3b9eb25b0c3377062114cdfff2ad560d6ba9241ccba55c0dcac695a83a0414e9" + }, + { + "lang": "zh-CN", + "path": "courses/da-report/lessons/L03_cn.md", + "title": "挑战 03:构建并渲染报告", + "summary": "metrics 原样透传,先 # 标题再按序 ## 小节,金额两位小数。", + "sha256": "01571a51e5c21976bac10b5d655e292b3de882b5acdd6fd0c197fbe257a98155" + } + ] + }, + { + "id": "course-da-report-l04", + "type": "course", + "content_version": 1, + "status": "reviewed", + "reviewed_on": "2026-09-12", + "locales": [ + { + "lang": "en-US", + "path": "courses/da-report/lessons/L04.md", + "title": "Challenge 04: Numbers traceable to inputs", + "summary": "Every metric in report.json equals its source field — the audit answer is a field name, not an explanation.", + "sha256": "501e1fbb684a5e94701559311ad098ea1a3f31fb63da46cd5ddb3f3a37ce2bcf" + }, + { + "lang": "zh-CN", + "path": "courses/da-report/lessons/L04_cn.md", + "title": "挑战 04:数字可回溯到输入", + "summary": "report.json 每个指标等于源字段——审计答案是字段名,不是解释。", + "sha256": "6603a3bb907ed7f72ef685e2edc68d7a9f21c68cc8b8471347c71e9f8acfa00a" + } + ] + }, + { + "id": "course-da-report-l05", + "type": "course", + "content_version": 1, + "status": "reviewed", + "reviewed_on": "2026-09-12", + "locales": [ + { + "lang": "en-US", + "path": "courses/da-report/lessons/L05.md", + "title": "Challenge 05: What report verification cannot see", + "summary": "Structure and number consistency are checked; insight and honest conclusions are not — write the boundary sentence.", + "sha256": "e267e245dbb43dfaf905189469fc1507fb46f814fc4778666363837e55b7eab4" + }, + { + "lang": "zh-CN", + "path": "courses/da-report/lessons/L05_cn.md", + "title": "挑战 05:报告验证看不到的东西", + "summary": "结构与数字一致性被查;洞察与结论诚实不在其中——写下边界句。", + "sha256": "60e8b47da3754b8bd76a9306a09cdb7a056f9c5b601132e71457c80113654b28" + } + ] + }, { "id": "course-da-visualization", "type": "course", @@ -530,6 +760,121 @@ } ] }, + { + "id": "course-da-visualization-l01", + "type": "course", + "content_version": 1, + "status": "reviewed", + "reviewed_on": "2026-09-12", + "locales": [ + { + "lang": "en-US", + "path": "courses/da-visualization/lessons/L01.md", + "title": "Challenge 01: A chart is packaged evidence", + "summary": "Say what claim each required PNG makes before writing code — the JSON summary is the auditable layer.", + "sha256": "d3640c032fca4b748b83fef983ebda39fd0e7cda5dff210dac3113bfe0a9809e" + }, + { + "lang": "zh-CN", + "path": "courses/da-visualization/lessons/L01_cn.md", + "title": "挑战 01:图是证据的包装", + "summary": "写码前先说出每张 PNG 声明什么——JSON 摘要才是可审计层。", + "sha256": "5b1acf730a1aa6d73e127370cf29c497550c74c3b6be1884f1c066d673304ed0" + } + ] + }, + { + "id": "course-da-visualization-l02", + "type": "course", + "content_version": 1, + "status": "reviewed", + "reviewed_on": "2026-09-12", + "locales": [ + { + "lang": "en-US", + "path": "courses/da-visualization/lessons/L02.md", + "title": "Challenge 02: Spec first, chart second", + "summary": "Write which aggregation feeds each chart and what summary.json must contain before matplotlib opens.", + "sha256": "ac12fed1fec38f46ab665999e219c570f6cb765088a3da3a8532e0d8cab478bb" + }, + { + "lang": "zh-CN", + "path": "courses/da-visualization/lessons/L02_cn.md", + "title": "挑战 02:先写规格,再画图", + "summary": "在打开 matplotlib 之前写清每张图吃哪个聚合、summary.json 必须装什么。", + "sha256": "5f860f5b8de7bad0cf408a690752246820ef56bc44ad1bb43e74e26c27ef3420" + } + ] + }, + { + "id": "course-da-visualization-l03", + "type": "course", + "content_version": 1, + "status": "reviewed", + "reviewed_on": "2026-09-12", + "locales": [ + { + "lang": "en-US", + "path": "courses/da-visualization/lessons/L03.md", + "title": "Challenge 03: Three charts and a summary", + "summary": "Implement the contract in starter/charts.py — Agg before pyplot, parsed dates, real PNG bytes.", + "sha256": "16ddbb48daf45a5c9802424690ab028ce86bc6ea42c8f4b677b7d03da3b36b51" + }, + { + "lang": "zh-CN", + "path": "courses/da-visualization/lessons/L03_cn.md", + "title": "挑战 03:三张图与摘要", + "summary": "在 starter/charts.py 实现契约——Agg 先于 pyplot、日期先解析、真 PNG 字节。", + "sha256": "53532ad9115b7b3295ba0f693b59ea8f961b54a489a1aa8f0ecc1880dfe90ef9" + } + ] + }, + { + "id": "course-da-visualization-l04", + "type": "course", + "content_version": 1, + "status": "reviewed", + "reviewed_on": "2026-09-12", + "locales": [ + { + "lang": "en-US", + "path": "courses/da-visualization/lessons/L04.md", + "title": "Challenge 04: Numbers match the charts", + "summary": "summary.json is checked against ground truth; a chart disagreeing with the JSON is a failed chart.", + "sha256": "5669523f09d8877d325c54f561dd595d713bf521a70d768095e009400c2ac4ca" + }, + { + "lang": "zh-CN", + "path": "courses/da-visualization/lessons/L04_cn.md", + "title": "挑战 04:数字与图一致", + "summary": "summary.json 对照真值校验;图与 JSON 不一致就是失败的图。", + "sha256": "08f3321adfa7764d58b5e81eec1aa1cb513ba7bfbfbd16ad2b98488d74b0cf28" + } + ] + }, + { + "id": "course-da-visualization-l05", + "type": "course", + "content_version": 1, + "status": "reviewed", + "reviewed_on": "2026-09-12", + "locales": [ + { + "lang": "en-US", + "path": "courses/da-visualization/lessons/L05.md", + "title": "Challenge 05: Honest boundary", + "summary": "The suite checks files and numbers — not readability or honest scales; state what was not verified.", + "sha256": "2a2a7ce46664f4653b73b6c719071757a76456b0a10eb35694cee60fac4df6e8" + }, + { + "lang": "zh-CN", + "path": "courses/da-visualization/lessons/L05_cn.md", + "title": "挑战 05:诚实边界", + "summary": "套件查文件与数字——不查可读性与刻度诚实;写清没验证什么。", + "sha256": "7ac94ceda7e8e89e5c192399fcb0a1b0d9c410924d1ba0531ee9c4481e1ce595" + } + ] + }, { "id": "course-mcp-tools", "type": "course", @@ -565,14 +910,14 @@ "path": "courses/mcp-server-in-python/lessons/L01.md", "title": "What MCP actually is (2026-07-28)", "summary": "A stateless JSON-RPC 2.0 surface for tools — no handshake, no sessions, versions in _meta.", - "sha256": "924927db6a228cd32a4d877d8fde864b744ec073627fd6ab5733da497ff38da7" + "sha256": "a50dbc4673a7178344e7ff3d19dac2f53abbabe584ed3bfa4639b5b263064fc0" }, { "lang": "zh-CN", "path": "courses/mcp-server-in-python/lessons/L01_cn.md", "title": "MCP 到底是什么(2026-07-28)", "summary": "一个面向工具的无状态 JSON-RPC 2.0 接口——无握手、无会话、版本放在 _meta 里。", - "sha256": "2e2c472022dd519a9fa483ff7c5fe5002689343d41ff3d244c598486e0d1fb65" + "sha256": "1f89bd8f063956870cb01fe57593779ab0eb2566f52404c4a2bc9e76c48dc29f" } ] }, @@ -588,14 +933,14 @@ "path": "courses/mcp-server-in-python/lessons/L02.md", "title": "The JSON-RPC dispatch contract", "summary": "Valid requests echo their id; invalid ones get -32600; unknown or removed methods get -32601.", - "sha256": "a00b09ff972a0cb0a6c5b0a5e825b33a06ce2f203fc3c820883ead6cdf9e709f" + "sha256": "2b0b57cd587d214f17f24864ba0b7fd632b1f8f18af113469e07843503fa8baf" }, { "lang": "zh-CN", "path": "courses/mcp-server-in-python/lessons/L02_cn.md", "title": "JSON-RPC 分发契约", "summary": "合法请求回显 id;非法请求得 -32600;未知或已移除的方法得 -32601。", - "sha256": "5f1abed1d4afcfb0f503ee509caf2a4d84057b4555570327e3250ca72ad43b51" + "sha256": "9cea20a365eb994ffd0a6db1d07df4b494dea86593d77f078c0825c9e9aa9995" } ] }, @@ -611,14 +956,14 @@ "path": "courses/mcp-server-in-python/lessons/L03.md", "title": "Validation and error isolation", "summary": "Arguments are untrusted input: validate before execution, and never let a handler crash the transport.", - "sha256": "e99e4ea8ef604a61fda45123310a32bb694e3679dfa59fb03eca58bd7630cfe3" + "sha256": "7d55d1fab063f988ffc819f1f2dfa945ceeb4bc5208d7dadbf0e009c7dfa7196" }, { "lang": "zh-CN", "path": "courses/mcp-server-in-python/lessons/L03_cn.md", "title": "校验与错误隔离", "summary": "参数是不可信输入:先校验再执行,绝不让处理函数打断传输。", - "sha256": "7334b29f5e9fd2f76e0e068ab40e9a6e5a6f95c6bed7d6460f708d17ec0b9624" + "sha256": "bd546854dc1b42f03baafddf00eed80a7c601d0ed228a5f951d7e3daef8d4a0f" } ] }, @@ -634,14 +979,14 @@ "path": "courses/mcp-server-in-python/lessons/L04.md", "title": "The input_required round-trip", "summary": "A tool can ask the client a question mid-call — return the requests, finish on the retry carrying answers.", - "sha256": "598872d0d8282e23c8ac6bc0b099068c1ae1c31c5dd51531b2edfefcd06193d2" + "sha256": "2ba1145cdc807afabd09737b860c9531acdbdbf933870e51f640c5f1bbb3a631" }, { "lang": "zh-CN", "path": "courses/mcp-server-in-python/lessons/L04_cn.md", "title": "input_required 多轮交互", "summary": "工具可以在调用中途向客户端提问——先返回问题,客户端带答案重试后完成。", - "sha256": "d33ef0588464d205c4f2c2d613726b055c27ab93938885403d1dd4b489575953" + "sha256": "8170e1bbec1865410e7ea0c8feb1b8cc182b3e119ce608f3c4e8fdc822ddcc70" } ] }, @@ -657,14 +1002,14 @@ "path": "courses/mcp-server-in-python/lessons/L05.md", "title": "Expose one real tool to your agent", "summary": "Wrap one function you actually own behind the validated, isolated boundary you just built.", - "sha256": "2df2f206f30f330d752dc9d640005cc641068a6ff93087de1a58208f86814824" + "sha256": "8b7c0a177ee77b0e9a9c8972d42f89dca94576e5aa9c234e845d11d65cb128c6" }, { "lang": "zh-CN", "path": "courses/mcp-server-in-python/lessons/L05_cn.md", "title": "给你的 Agent 暴露一个真实工具", "summary": "把你真正拥有的一个函数包进刚建好的“校验 + 隔离”边界。", - "sha256": "071a2a276f66587e7759b644d8d7bdad9704159cc00ad9b00b1f5ce9d563ecce" + "sha256": "4b33bc46cf447a9df0d0b7b0a37cab36eb825b9769d1c42ead1b0823af75014f" } ] }, @@ -703,14 +1048,14 @@ "path": "courses/verifying-ai-generated-code/lessons/L01.md", "title": "\"It runs\" is not evidence", "summary": "Watch the starter bless a project with no tests and a project with a failing one — then want better.", - "sha256": "c2451b1742453e323783274762ba8f24d7c7b7cdee6e1d44c1190a90a64ea4de" + "sha256": "96f3b58d3884b1e28b189c944aaaa94e8bbe900f1209f22bbb6269994f92445e" }, { "lang": "zh-CN", "path": "courses/verifying-ai-generated-code/lessons/L01_cn.md", "title": "“能跑”不是证据", "summary": "看着 starter 给零测试的项目和有失败的项目都盖章放行——然后想要更好的东西。", - "sha256": "c7ff8699b8b2742cc816dad7766f1f8f78a35a29b7849348fdaad6c450d5bab6" + "sha256": "8ec0c0cdd759871e4ce758cec41d077dc83c20d4864b596a062de47904e85e24" } ] }, @@ -726,14 +1071,14 @@ "path": "courses/verifying-ai-generated-code/lessons/L02.md", "title": "Define what a release record must prove", "summary": "Command, exit code, parsed count and outcome, an unverified list, and a verdict that refuses zero-test projects.", - "sha256": "d23b88834f0711e03a2df77248af424e900046644cb29d406e6291ddd1234ba0" + "sha256": "d39ff4180a0dbabca31ea3d2a66fe9bb82a4f87c2830bc32f165ef1f93d90c62" }, { "lang": "zh-CN", "path": "courses/verifying-ai-generated-code/lessons/L02_cn.md", "title": "定义发布记录必须证明什么", "summary": "命令、退出码、解析出的数量与结果、一份未验证清单,以及一个拒绝零测试项目的判定。", - "sha256": "05fed47a2fb74f6b846a266e89996b2c3fc5b2174441116a41180c821b769c09" + "sha256": "b57eab0170a48047ab0e56d61bb92aac5d17680346acf8e82c5b0293c42852f5" } ] }, @@ -749,14 +1094,14 @@ "path": "courses/verifying-ai-generated-code/lessons/L03.md", "title": "Build the ship check, test by test", "summary": "Parsing, verdicts, the unverified list, atomic writes, exit codes — one failing group at a time.", - "sha256": "35765f560c3731ae16b528bf3a26536fe230e6aadaf489bddae75c64a9ac1534" + "sha256": "6d0491d3f534add4b354e311fbaa957b73b0abd388e06fb20366dc830d95395a" }, { "lang": "zh-CN", "path": "courses/verifying-ai-generated-code/lessons/L03_cn.md", "title": "按测试逐组构建 ship check", "summary": "解析、判定、未验证清单、原子写入、退出码——一次一组失败测试。", - "sha256": "531c9d179998179b2117ac7529c0a62947725f250c2f5a1a2aed5dee623fe912" + "sha256": "5013df14fbad78914a55dd9fac568aaa60809c33b13d180ab958be8e5cd62eb5" } ] }, @@ -772,14 +1117,14 @@ "path": "courses/verifying-ai-generated-code/lessons/L04.md", "title": "Read the record like a skeptic", "summary": "Green, red, and no-tests mean different things — prove you can tell them apart and say what the record does not prove.", - "sha256": "e2ba3f5f6b3aec97670db1126bf3cf4dadc571b1391b5bc652b681e7f7c16dd3" + "sha256": "14326a8b19d5c8db834bcd3071598135b6cc8a11a9e8aac974a6bf3c8fc82ced" }, { "lang": "zh-CN", "path": "courses/verifying-ai-generated-code/lessons/L04_cn.md", "title": "像怀疑者一样读记录", "summary": "绿、红、零测试是三件不同的事——证明你能分辨,并说清记录不能证明什么。", - "sha256": "8bf33d1729279903ef5ee093dba5f95dc13c594e8da9d8c4598aa7420b64c582" + "sha256": "263921bf47ea5db21b98548442fe638794fb3ad9a84abef885b7ff66b94da919" } ] }, @@ -795,14 +1140,14 @@ "path": "courses/verifying-ai-generated-code/lessons/L05.md", "title": "Gate your next release", "summary": "Take the ship check to a project you own and make the record part of shipping.", - "sha256": "c11972691e935d17cf7266c9f1cc75a3c38720b0eb025cb4abcd7bb5f2020848" + "sha256": "9e664fc5e21c28b4ab5b1217c6be273d62f8ae139a774634c826bda4814c2bf6" }, { "lang": "zh-CN", "path": "courses/verifying-ai-generated-code/lessons/L05_cn.md", "title": "给你的下一次发布装上门禁", "summary": "把 ship check 带到你自己的项目,让记录成为交付的一部分。", - "sha256": "126d8c70c068e5dfcc33f2e03f041878efc1bb085b7abe40293c8ee92557634f" + "sha256": "667023ea75adf0c47c97de409f8a364027f897975d377b7fd3a7b9d00cda9e80" } ] }, diff --git a/courses/agent-rules-single-source/REVIEW.md b/courses/agent-rules-single-source/REVIEW.md index 7426d9d..0610a4b 100644 --- a/courses/agent-rules-single-source/REVIEW.md +++ b/courses/agent-rules-single-source/REVIEW.md @@ -20,4 +20,20 @@ ## Deviation log -(none recorded yet — first agent-taught run-through pending) +### 2026-09-13 — agent solvability run (challenge mode, not taught mode) + +- Agent: Devin (SWE-2 Max), CLI session on macOS arm64, system Python + 3.14 (stdlib only — this course needs no third-party deps). +- Method: implemented `starter/rules_check.py` from `TASK.md` and the + test expectations only; did not read or copy `solution/`. +- Result: `python verify.py starter` exits 0 (all eleven tests); + `verify.py progress` printed all five claim codes (l03/l04 + `[passed]`, l01/l02/l05 `[attest]`). +- Observed deviation: one iteration needed, not one pass — my first + `diverged` issue text said "differs" where the suite asserts the + substring "differ from AGENTS.md". The contract pins observable + strings, so the fix was wording, not logic. +- Limitations: challenge-mode run (TASK.md → tests), not the taught + COURSE.md walkthrough — teaching quality remains separately pending. +- Starter restored to the deliberately-unfinished state after the run + (`--expect-failure` verified again). diff --git a/courses/agent-rules-single-source/lessons/L01.md b/courses/agent-rules-single-source/lessons/L01.md index 0ba9824..0646771 100644 --- a/courses/agent-rules-single-source/lessons/L01.md +++ b/courses/agent-rules-single-source/lessons/L01.md @@ -7,6 +7,13 @@ lang: en-US content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "That you can measure drift instead of feeling it: run the checker on the three scenario repositories and read each result as a test failure, not a vibe." + - title: "Find the three repo states" + body: "The scenario folders show the three shapes the contract classifies — a clean single-source repo, one with a thin pointer, and one where a copy has drifted. Name which is which before moving on." + - title: "When you are done" + body: "You can explain what `ok=false` means in terms of files, not feelings: which file diverged from `AGENTS.md` and how the checker knew." --- # Measure the drift you already have diff --git a/courses/agent-rules-single-source/lessons/L01_cn.md b/courses/agent-rules-single-source/lessons/L01_cn.md index 0de3339..2f0549b 100644 --- a/courses/agent-rules-single-source/lessons/L01_cn.md +++ b/courses/agent-rules-single-source/lessons/L01_cn.md @@ -7,6 +7,13 @@ lang: zh-CN content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "考你能不能把漂移量化而不是凭感觉:对三个 scenario 仓库跑检查器,把每个结果当测试失败读,不当气氛。" + - title: "认出三种仓库状态" + body: "scenario 文件夹展示了契约要分类的三种形态——干净的单一真源仓库、带薄指针的仓库、副本已漂移的仓库。先说出谁是谁再往下走。" + - title: "完成标准" + body: "你能用文件而不是感觉解释 `ok=false`:哪个文件偏离了 `AGENTS.md`,检查器是怎么发现的。" --- # 量化你已经存在的漂移 diff --git a/courses/agent-rules-single-source/lessons/L02.md b/courses/agent-rules-single-source/lessons/L02.md index f88cb61..7146ad2 100644 --- a/courses/agent-rules-single-source/lessons/L02.md +++ b/courses/agent-rules-single-source/lessons/L02.md @@ -7,6 +7,13 @@ lang: en-US content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "Deciding the single source of truth: `AGENTS.md` is the file agents read; every other rule file becomes a thin pointer or an exact copy." + - title: "Pointer vs copy" + body: "A pointer is at most 10 non-empty lines and references `AGENTS.md`; a copy is byte-identical. Anything else is `diverged`. Write down which of your own repos' files would classify as which." + - title: "When you are done" + body: "Your decision is written down: which file is the source, and what every other file may contain." --- # Decide the source of truth diff --git a/courses/agent-rules-single-source/lessons/L02_cn.md b/courses/agent-rules-single-source/lessons/L02_cn.md index 9ed888b..76a68b5 100644 --- a/courses/agent-rules-single-source/lessons/L02_cn.md +++ b/courses/agent-rules-single-source/lessons/L02_cn.md @@ -7,6 +7,13 @@ lang: zh-CN content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "定出唯一真源:`AGENTS.md` 是 Agent 真正读的文件,其余规则文件只能是薄指针或逐字节副本。" + - title: "指针还是副本" + body: "指针最多 10 个非空行且引用 `AGENTS.md`;副本与真源逐字节一致。其余都是 `diverged`。写下你自己仓库里每个文件会被判成哪类。" + - title: "完成标准" + body: "你的决定落了字:谁是真源,其余文件允许装什么。" --- # 确定唯一真源 diff --git a/courses/agent-rules-single-source/lessons/L03.md b/courses/agent-rules-single-source/lessons/L03.md index c74979c..0556aad 100644 --- a/courses/agent-rules-single-source/lessons/L03.md +++ b/courses/agent-rules-single-source/lessons/L03.md @@ -7,6 +7,13 @@ lang: en-US content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "The gate is `python verify.py starter` — implement `load_rule_files`, `is_pointer`, `check_directory`, and `main` until all eleven tests pass." + - title: "Classification order matters" + body: "`AGENTS.md` missing or empty yields `missing-source` and `ok=false`. Every other file classifies as `pointer`, `copy`, or `diverged` — check pointer and copy before diverged, and `ok` only when no entry is diverged or missing." + - title: "Output shape" + body: "`main` prints `ok=... files=... diverged=...` and exits 0 only when ok — argument errors print usage to stderr and return 2." --- # Drive a bounded change, test by test diff --git a/courses/agent-rules-single-source/lessons/L03_cn.md b/courses/agent-rules-single-source/lessons/L03_cn.md index 0704a08..5674642 100644 --- a/courses/agent-rules-single-source/lessons/L03_cn.md +++ b/courses/agent-rules-single-source/lessons/L03_cn.md @@ -7,6 +7,13 @@ lang: zh-CN content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "门控是 `python verify.py starter`——实现 `load_rule_files`、`is_pointer`、`check_directory`、`main`,十一个测试全绿。" + - title: "分类顺序很关键" + body: "`AGENTS.md` 缺失或为空产出 `missing-source` 且 `ok=false`。其余文件分 `pointer`、`copy`、`diverged`——先判指针和副本再判漂移,没有任何漂移或缺失项时 `ok` 才为真。" + - title: "输出形态" + body: "`main` 打印 `ok=... files=... diverged=...`,仅当 ok 时退出 0——参数错误打印用法到 stderr 并返回 2。" --- # 按测试驱动一次有边界的变更 diff --git a/courses/agent-rules-single-source/lessons/L04.md b/courses/agent-rules-single-source/lessons/L04.md index 6dded03..ddbc361 100644 --- a/courses/agent-rules-single-source/lessons/L04.md +++ b/courses/agent-rules-single-source/lessons/L04.md @@ -7,6 +7,13 @@ lang: en-US content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "Both suites plus wiring: prove both verifier directions, run all three skins, then add the checker where your review loop actually runs." + - title: "Both directions" + body: "The starter must fail where the scenario is broken and pass where it is clean — `--expect-failure` and the plain run tell you which side you are on." + - title: "Wire it where it bites" + body: "A checker nobody runs is a rule file nobody reads: add it to a pre-commit hook or CI step in one repo you own, and record what it caught first." --- # Verify and wire it in diff --git a/courses/agent-rules-single-source/lessons/L04_cn.md b/courses/agent-rules-single-source/lessons/L04_cn.md index 8fd98e4..7513778 100644 --- a/courses/agent-rules-single-source/lessons/L04_cn.md +++ b/courses/agent-rules-single-source/lessons/L04_cn.md @@ -7,6 +7,13 @@ lang: zh-CN content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "双套件加接线:两个方向都验证、三个皮肤都跑,然后把检查器接进你真正的评审环。" + - title: "两个方向" + body: "starter 在坏 scenario 上必须失败、在干净 scenario 上必须通过——`--expect-failure` 和普通运行告诉你自己在哪一侧。" + - title: "接到咬得动的地方" + body: "没人运行的检查器等于没人读的规则文件:把它加进你某个仓库的 pre-commit 或 CI,记下它第一次抓到了什么。" --- # 验证并接入流程 diff --git a/courses/agent-rules-single-source/lessons/L05.md b/courses/agent-rules-single-source/lessons/L05.md index 606a73e..f0d0ff8 100644 --- a/courses/agent-rules-single-source/lessons/L05.md +++ b/courses/agent-rules-single-source/lessons/L05.md @@ -7,6 +7,13 @@ lang: en-US content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "One `AGENTS.md`, thin pointers everywhere else, and a failing check when it breaks — applied to a repository you own." + - title: "Keep pointers thin" + body: "Each pointer file: at most 10 non-empty lines and a reference to `AGENTS.md`. Rewrite, do not copy — copies drift." + - title: "The honest record" + body: "Note which files you deleted or slimmed and what the checker cannot see (like rule *quality* — it checks consistency, not wisdom)." --- # Apply this to your repository diff --git a/courses/agent-rules-single-source/lessons/L05_cn.md b/courses/agent-rules-single-source/lessons/L05_cn.md index 982025f..0c6c10b 100644 --- a/courses/agent-rules-single-source/lessons/L05_cn.md +++ b/courses/agent-rules-single-source/lessons/L05_cn.md @@ -7,6 +7,13 @@ lang: zh-CN content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "一个 `AGENTS.md`、其余都是薄指针、坏了就有检查报警——落地到你自己一个仓库。" + - title: "指针要薄" + body: "每个指针文件:最多 10 个非空行并引用 `AGENTS.md`。要重写不要复制——副本会漂移。" + - title: "诚实的记录" + body: "记下你删掉或削薄了哪些文件,以及检查器看不到什么(比如规则质量——它查一致性,不查智慧)。" --- # 把这套方法带回你的仓库 diff --git a/courses/da-eda/REVIEW.md b/courses/da-eda/REVIEW.md index e10f024..a689f84 100644 --- a/courses/da-eda/REVIEW.md +++ b/courses/da-eda/REVIEW.md @@ -8,5 +8,16 @@ - `python verify.py solution`: all twelve tests pass. - `python verify.py progress`: deterministic claim codes; L03/L04 objective, L01/L02/L05 attested. -- Agent solvability run (challenge-model gate): **pending** — must be - recorded before public launch per docs/repo-plan-0.0.4.md §1. +- Agent solvability run (challenge-model gate): **done 2026-09-13**. + Agent: Devin (SWE-2 Max), CLI session on macOS arm64, repo venv with + pandas. Method: implemented `starter/eda.py` from `TASK.md` only — + drop_duplicates, `to_numeric`/`to_datetime` with `errors="coerce"`, + groupby sums, merged stats into results.json — did not read or copy + `solution/`. Result: `python verify.py starter` exits 0 (all twelve + tests); `verify.py progress` printed all five claim codes. Deviations: + none — solvable in one pass; cleaning order (dedupe before counting + bad parses) matters and the contract pins the expected counts, which + disambiguates it. Limitations: single run, single agent; results.json + was written into `scenario/shop-export/` per the contract (already + git-ignored/shipped — verify by `git status` staying clean). + Starter restored to the deliberately-unfinished state after the run. diff --git a/courses/da-eda/lessons/L01.md b/courses/da-eda/lessons/L01.md index c7c12bb..0208b42 100644 --- a/courses/da-eda/lessons/L01.md +++ b/courses/da-eda/lessons/L01.md @@ -1,3 +1,21 @@ +--- +id: course-da-eda-l01 +type: course +title: "Challenge 01: What messy data looks like" +summary: "Profile the shop export before touching pandas: exact duplicates, unparseable amounts, one bad date — count the dirt first." +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "Profiling before coding: open `scenario/shop-export/transactions.csv` and find the dirt yourself — exact duplicates, unparseable amounts, one bad date." + - title: "Count it first" + body: "Note how many rows exist and how many of each defect you spot — those numbers are what the suite will compare against, so knowing them early is the advantage." + - title: "When you are done" + body: "You can name the three defect classes and roughly how dirty each is." +--- + # Challenge 01: What messy data looks like **Gate:** self-attested · **Points:** 10 diff --git a/courses/da-eda/lessons/L01_cn.md b/courses/da-eda/lessons/L01_cn.md index bee068a..0e70e4f 100644 --- a/courses/da-eda/lessons/L01_cn.md +++ b/courses/da-eda/lessons/L01_cn.md @@ -1,3 +1,21 @@ +--- +id: course-da-eda-l01 +type: course +title: "挑战 01:脏数据长什么样" +summary: "动手写 pandas 之前先给商店导出数据画像:完全重复、无法解析的金额、一条坏日期——先把脏数清楚。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "先画像再写码:打开 `scenario/shop-export/transactions.csv`,亲手找出脏数据——完全重复行、解析不了的金额、一条坏日期。" + - title: "先数一遍" + body: "记下总行数和每类缺陷大约多少——套件比对的就是这些数,提前知道就是优势。" + - title: "完成标准" + body: "你能说出三类缺陷,以及各自大概有多脏。" +--- + # 挑战 01:脏数据长什么样 **判卷:** 自我声明 · **积分:** 10 diff --git a/courses/da-eda/lessons/L02.md b/courses/da-eda/lessons/L02.md index 74c7e36..83a7d25 100644 --- a/courses/da-eda/lessons/L02.md +++ b/courses/da-eda/lessons/L02.md @@ -1,3 +1,21 @@ +--- +id: course-da-eda-l02 +type: course +title: "Challenge 02: Define the answer first" +summary: "Sketch results.json — keys, types, rounding — before letting the agent write eda.py." +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "Defining the answer before generating it: sketch `results.json` — keys, types, rounding — before letting the agent write `eda.py`." + - title: "Contract first" + body: "Decide what `total_revenue` rounding means, what `revenue_by_region` keys look like, and that `rows_total` equals input rows — the suite compares exact numbers." + - title: "When you are done" + body: "Your sketch names every key in `results.json` and its source column." +--- + # Challenge 02: Define the answer first **Gate:** self-attested · **Points:** 10 diff --git a/courses/da-eda/lessons/L02_cn.md b/courses/da-eda/lessons/L02_cn.md index 49d2e25..8f86a76 100644 --- a/courses/da-eda/lessons/L02_cn.md +++ b/courses/da-eda/lessons/L02_cn.md @@ -1,3 +1,21 @@ +--- +id: course-da-eda-l02 +type: course +title: "挑战 02:先定答案" +summary: "先勾出 results.json 的键、类型与舍入规则,再让 Agent 写 eda.py。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "先定义答案再让 Agent 生成:动手写 `eda.py` 之前先勾出 `results.json` 的键、类型和舍入规则。" + - title: "契约先行" + body: "定清楚 `total_revenue` 怎么舍入、`revenue_by_region` 键长什么样、`rows_total` 等于输入行数——套件比对的是精确数字。" + - title: "完成标准" + body: "你的草图列出 `results.json` 每个键和它的来源列。" +--- + # 挑战 02:先定答案 **判卷:** 自我声明 · **积分:** 10 diff --git a/courses/da-eda/lessons/L03.md b/courses/da-eda/lessons/L03.md index 9dfb014..014e7db 100644 --- a/courses/da-eda/lessons/L03.md +++ b/courses/da-eda/lessons/L03.md @@ -1,3 +1,21 @@ +--- +id: course-da-eda-l03 +type: course +title: "Challenge 03: Clean and summarize" +summary: "Implement load_transactions, clean_transactions, summarize, write_results, main until all twelve tests pass." +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "`python verify.py starter` — implement `load_transactions`, `clean_transactions`, `summarize`, `write_results`, `main` until all twelve tests pass." + - title: "Classic EDA traps" + body: "`to_numeric` without `errors='coerce'` crashes on 'n/a' instead of counting it; dedup must happen on raw rows or exact duplicates of dirty rows slip through; `groupby().sum()` silently drops NaN — count defects before cleaning." + - title: "Exact contract bits" + body: "stats keys are exactly `duplicates_removed`, `missing_amount`, `bad_dates`; `total_revenue` and region values round to 2 decimals; dates output ISO `YYYY-MM-DD`." +--- + # Challenge 03: Clean and summarize **Gate:** objective — starter suite · **Points:** 10 diff --git a/courses/da-eda/lessons/L03_cn.md b/courses/da-eda/lessons/L03_cn.md index 3a51d2b..d8f51bb 100644 --- a/courses/da-eda/lessons/L03_cn.md +++ b/courses/da-eda/lessons/L03_cn.md @@ -1,3 +1,21 @@ +--- +id: course-da-eda-l03 +type: course +title: "挑战 03:清洗与统计" +summary: "实现 load_transactions、clean_transactions、summarize、write_results、main,十二个测试全绿。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "`python verify.py starter`——实现 `load_transactions`、`clean_transactions`、`summarize`、`write_results`、`main`,十二个测试全绿。" + - title: "经典 EDA 陷阱" + body: "`to_numeric` 不加 `errors='coerce'` 遇到 'n/a' 直接崩而不是计数;去重必须在原始行上做,否则脏行的完全重复会漏掉;`groupby().sum()` 会静默丢 NaN——清洗前先数缺陷。" + - title: "契约细节" + body: "stats 的键必须正好是 `duplicates_removed`、`missing_amount`、`bad_dates`;`total_revenue` 和地区值两位小数;日期输出 ISO `YYYY-MM-DD`。" +--- + # 挑战 03:清洗与统计 **判卷:** 客观——starter 套件 · **积分:** 10 diff --git a/courses/da-eda/lessons/L04.md b/courses/da-eda/lessons/L04.md index 32ffdcb..ef535d4 100644 --- a/courses/da-eda/lessons/L04.md +++ b/courses/da-eda/lessons/L04.md @@ -1,3 +1,21 @@ +--- +id: course-da-eda-l04 +type: course +title: "Challenge 04: Check against ground truth" +summary: "The suite asserts exact numbers — 142 in, 137 clean, revenue 33347.89, top category Books; plausible-but-different fails." +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "Both suites against ground truth: 142 rows in, 137 clean, 2 duplicates, 2 bad amounts, 1 bad date, revenue 33347.89, top category Books." + - title: "Plausible is not correct" + body: "An implementation producing different-but-reasonable numbers fails — this dataset has exactly one right answer. If yours differ, find which cleaning step drifted." + - title: "When you are done" + body: "Your `results.json` matches the ground truth byte-for-number, and you can point each figure at the cleaning step that produced it." +--- + # Challenge 04: Check against ground truth **Gate:** objective — both suites · **Points:** 10 diff --git a/courses/da-eda/lessons/L04_cn.md b/courses/da-eda/lessons/L04_cn.md index 66bba0e..0b3159b 100644 --- a/courses/da-eda/lessons/L04_cn.md +++ b/courses/da-eda/lessons/L04_cn.md @@ -1,3 +1,21 @@ +--- +id: course-da-eda-l04 +type: course +title: "挑战 04:核对真值" +summary: "套件断言精确数字——142 进 137 净、营收 33347.89、品类第一 Books;看着合理但不同即失败。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "双套件对真值:142 行进、137 行净、2 条重复、2 条坏金额、1 条坏日期、营收 33347.89、品类第一 Books。" + - title: "合理不等于正确" + body: "产出『看着合理但不同』的数字就是失败——这份数据只有一个正确答案。对不上就找哪一步清洗跑偏了。" + - title: "完成标准" + body: "你的 `results.json` 与真值逐数字一致,且每个数字都能指回产生它的清洗步骤。" +--- + # 挑战 04:核对真值 **判卷:** 客观——双套件 · **积分:** 10 diff --git a/courses/da-eda/lessons/L05.md b/courses/da-eda/lessons/L05.md index 0b27705..d5bbca4 100644 --- a/courses/da-eda/lessons/L05.md +++ b/courses/da-eda/lessons/L05.md @@ -1,3 +1,21 @@ +--- +id: course-da-eda-l05 +type: course +title: "Challenge 05: Run it on your own data" +summary: "Port the pipeline to a real export you own and record which assumptions break." +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "Running the same pipeline on data you own — the point is discovering which assumptions break: column names, encodings, date formats, currencies." + - title: "Expect it not to port" + body: "Your export will not match the scenario's shape. The lesson is seeing *where* the contract needs editing, not forcing the code to fit." + - title: "The honest record" + body: "Write down which assumptions broke — that list is the checkpoint, not a clean port." +--- + # Challenge 05: Run it on your own data **Gate:** self-attested · **Points:** 10 diff --git a/courses/da-eda/lessons/L05_cn.md b/courses/da-eda/lessons/L05_cn.md index 7cf52a9..0946aa2 100644 --- a/courses/da-eda/lessons/L05_cn.md +++ b/courses/da-eda/lessons/L05_cn.md @@ -1,3 +1,21 @@ +--- +id: course-da-eda-l05 +type: course +title: "挑战 05:换成你自己的数据" +summary: "把流水线搬到你自己一份真实导出上,记下哪些假设崩了。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "把同一条流水线跑在你自己的数据上——重点是发现哪些假设会崩:列名、编码、日期格式、币种。" + - title: "别指望直接搬" + body: "你的导出和 scenario 长不一样。本课是看契约哪里需要改,不是硬把代码塞进去。" + - title: "诚实的记录" + body: "写下哪些假设崩了——那张清单就是这个检查点,不是一个干净的移植。" +--- + # 挑战 05:换成你自己的数据 **判卷:** 自我声明 · **积分:** 10 diff --git a/courses/da-report/REVIEW.md b/courses/da-report/REVIEW.md index 0ac2a21..be50e68 100644 --- a/courses/da-report/REVIEW.md +++ b/courses/da-report/REVIEW.md @@ -7,5 +7,17 @@ report.json written to out/. - `python verify.py progress`: deterministic claim codes; L03/L04 objective, L01/L02/L05 attested. -- Agent solvability run (challenge-model gate): **pending** — must be - recorded before public launch per docs/repo-plan-0.0.4.md §1. +- Agent solvability run (challenge-model gate): **done 2026-09-13**. + Agent: Devin (SWE-2 Max), CLI session on macOS arm64, repo venv + Python 3.x with pandas installed. Method: implemented + `starter/report.py` from `TASK.md` only (load → build → render → + write → main), did not read or copy `solution/`. Result: + `python verify.py starter` exits 0; `verify.py progress` printed all + five claim codes (l03/l04 `[passed]`, l01/l02/l05 `[attest]`). + Observed deviations: none — the contract was implementable in one + pass; the metrics-carry-unchanged rule is the main trap and the + TASK.md wording makes it explicit. Limitations: single run, single + agent; the course does not teach *how* to write the report, so this + verifies solvability, not pedagogy. Starter restored to the + deliberately-unfinished state after the run (required by + `--expect-failure`). diff --git a/courses/da-report/lessons/L01.md b/courses/da-report/lessons/L01.md index e0187c7..bd16806 100644 --- a/courses/da-report/lessons/L01.md +++ b/courses/da-report/lessons/L01.md @@ -1,3 +1,21 @@ +--- +id: course-da-report-l01 +type: course +title: "Challenge 01: A report is a contract with the reader" +summary: "List which numbers the report must carry and where each comes from in results.json." +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "A report as a contract with the reader: list which numbers it must carry — `total_revenue`, `rows_clean`, `top_category`, `revenue_by_region` — and where each comes from in `results.json`." + - title: "Trace before writing" + body: "Every number in the report maps to a field in the input pack; a number you cannot trace does not belong in the report." + - title: "When you are done" + body: "Your list pairs each report number with its source field." +--- + # Challenge 01: A report is a contract with the reader **Gate:** self-attested · **Points:** 10 diff --git a/courses/da-report/lessons/L01_cn.md b/courses/da-report/lessons/L01_cn.md index 4fe2bb7..8734053 100644 --- a/courses/da-report/lessons/L01_cn.md +++ b/courses/da-report/lessons/L01_cn.md @@ -1,3 +1,21 @@ +--- +id: course-da-report-l01 +type: course +title: "挑战 01:报告是与读者的契约" +summary: "列出报告必须携带哪些数字,以及各自来自 results.json 的哪个字段。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "报告是与读者的契约:列出它必须携带哪些数字——`total_revenue`、`rows_clean`、`top_category`、`revenue_by_region`——以及各自来自 `results.json` 的哪个字段。" + - title: "先追溯再动笔" + body: "报告里每个数字都映射到输入包的某个字段;追溯不了的数字不该进报告。" + - title: "完成标准" + body: "你的清单把报告里每个数字和来源字段配了对。" +--- + # 挑战 01:报告是与读者的契约 **判卷:** 自我声明 · **积分:** 10 diff --git a/courses/da-report/lessons/L02.md b/courses/da-report/lessons/L02.md index 240bc8c..68f3c49 100644 --- a/courses/da-report/lessons/L02.md +++ b/courses/da-report/lessons/L02.md @@ -1,3 +1,21 @@ +--- +id: course-da-report-l02 +type: course +title: "Challenge 02: Structure before prose" +summary: "Title, exact section order Data quality → Findings → Appendix, and the metrics block — before any sentence." +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "Structure before prose: title, the exact section order `Data quality` → `Findings` → `Appendix`, and what lives in the machine-readable `metrics` block." + - title: "Sections are ordered" + body: "`sections` is an ordered list — the suite checks sequence, so get the skeleton right before any sentence is written." + - title: "When you are done" + body: "Your skeleton names the title, the three section headings in order, and the metrics keys." +--- + # Challenge 02: Structure before prose **Gate:** self-attested · **Points:** 10 diff --git a/courses/da-report/lessons/L02_cn.md b/courses/da-report/lessons/L02_cn.md index b2b45e7..1289240 100644 --- a/courses/da-report/lessons/L02_cn.md +++ b/courses/da-report/lessons/L02_cn.md @@ -1,3 +1,21 @@ +--- +id: course-da-report-l02 +type: course +title: "挑战 02:先结构,后文字" +summary: "标题、Data quality → Findings → Appendix 的确切顺序、metrics 块——先于任何句子。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "先结构后文字:标题、`Data quality` → `Findings` → `Appendix` 的确切顺序,以及机器可读的 `metrics` 块装什么。" + - title: "小节是有序的" + body: "`sections` 是有序列表——套件查顺序,先把骨架弄对再写句子。" + - title: "完成标准" + body: "你的骨架点明标题、三个小节标题按序,以及 metrics 的键。" +--- + # 挑战 02:先结构,后文字 **判卷:** 自我声明 · **积分:** 10 diff --git a/courses/da-report/lessons/L03.md b/courses/da-report/lessons/L03.md index 378087f..0e086c7 100644 --- a/courses/da-report/lessons/L03.md +++ b/courses/da-report/lessons/L03.md @@ -1,3 +1,21 @@ +--- +id: course-da-report-l03 +type: course +title: "Challenge 03: Build and render the report" +summary: "Carry metrics through unchanged, emit # title then ## sections in order, money at two decimals." +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "`python verify.py starter` — `load_inputs`, `build_report`, `render_markdown`, `write_report`, `main` all green." + - title: "Carry, do not recompute" + body: "`metrics` passes inputs through unchanged — the suite compares numbers exactly, and a report that recomputes differently is a bug, not a feature." + - title: "Exact output shape" + body: "`render_markdown` emits `# ` then `## <heading>` in order; money formats to two decimals like `33347.89`; `report.json` is UTF-8, indent 2, trailing newline." +--- + # Challenge 03: Build and render the report **Gate:** objective — starter suite · **Points:** 10 diff --git a/courses/da-report/lessons/L03_cn.md b/courses/da-report/lessons/L03_cn.md index c997275..6c06489 100644 --- a/courses/da-report/lessons/L03_cn.md +++ b/courses/da-report/lessons/L03_cn.md @@ -1,3 +1,21 @@ +--- +id: course-da-report-l03 +type: course +title: "挑战 03:构建并渲染报告" +summary: "metrics 原样透传,先 # 标题再按序 ## 小节,金额两位小数。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "`python verify.py starter`——`load_inputs`、`build_report`、`render_markdown`、`write_report`、`main` 全绿。" + - title: "透传不重算" + body: "`metrics` 原样携带输入——套件精确比对数字,重算出不同结果的报告是 bug 不是特色。" + - title: "输出形态要精确" + body: "`render_markdown` 先输出 `# <标题>` 再按序输出 `## <小节>`;金额两位小数字形如 `33347.89`;`report.json` 用 UTF-8、缩进 2、结尾换行。" +--- + # 挑战 03:构建并渲染报告 **判卷:** 客观——starter 套件 · **积分:** 10 diff --git a/courses/da-report/lessons/L04.md b/courses/da-report/lessons/L04.md index 134ba92..69182b1 100644 --- a/courses/da-report/lessons/L04.md +++ b/courses/da-report/lessons/L04.md @@ -1,3 +1,21 @@ +--- +id: course-da-report-l04 +type: course +title: "Challenge 04: Numbers traceable to inputs" +summary: "Every metric in report.json equals its source field — the audit answer is a field name, not an explanation." +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "Both suites: every metric in `report.json` equals its source in `results.json` — the audit answer is a field name, not an explanation." + - title: "The trace question" + body: "For each number in your report, answer 'where did this come from' with an input field. If any answer is a sentence instead of a field name, that number is suspect." + - title: "When you are done" + body: "All metrics trace byte-for-number and you can say which section each lives in." +--- + # Challenge 04: Numbers traceable to inputs **Gate:** objective — both suites · **Points:** 10 diff --git a/courses/da-report/lessons/L04_cn.md b/courses/da-report/lessons/L04_cn.md index b8e8667..4686f85 100644 --- a/courses/da-report/lessons/L04_cn.md +++ b/courses/da-report/lessons/L04_cn.md @@ -1,3 +1,21 @@ +--- +id: course-da-report-l04 +type: course +title: "挑战 04:数字可回溯到输入" +summary: "report.json 每个指标等于源字段——审计答案是字段名,不是解释。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "双套件:`report.json` 里每个指标等于 `results.json` 里的源值——审计答案是一个字段名,不是一段解释。" + - title: "追溯之问" + body: "对报告里每个数字回答『它从哪来』,答案必须是输入字段。哪个答案变成了一句话,那个数字就可疑。" + - title: "完成标准" + body: "所有指标逐数字可追溯,且你能说出各自在哪个小节。" +--- + # 挑战 04:数字可回溯到输入 **判卷:** 客观——双套件 · **积分:** 10 diff --git a/courses/da-report/lessons/L05.md b/courses/da-report/lessons/L05.md index 27e06ae..87fd066 100644 --- a/courses/da-report/lessons/L05.md +++ b/courses/da-report/lessons/L05.md @@ -1,3 +1,21 @@ +--- +id: course-da-report-l05 +type: course +title: "Challenge 05: What report verification cannot see" +summary: "Structure and number consistency are checked; insight and honest conclusions are not — write the boundary sentence." +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "What report verification cannot see: structure, order, and number consistency are checked — insight, prose quality, and honest conclusions are not." + - title: "One boundary sentence" + body: "State what your report's verification covered and what it did not — that sentence, applied to a report you own, is the checkpoint." + - title: "Same rule as always" + body: "Every verifier has a blind spot; naming yours is the engineering habit this course exists to build." +--- + # Challenge 05: What report verification cannot see **Gate:** self-attested · **Points:** 10 diff --git a/courses/da-report/lessons/L05_cn.md b/courses/da-report/lessons/L05_cn.md index 060c268..f615db3 100644 --- a/courses/da-report/lessons/L05_cn.md +++ b/courses/da-report/lessons/L05_cn.md @@ -1,3 +1,21 @@ +--- +id: course-da-report-l05 +type: course +title: "挑战 05:报告验证看不到的东西" +summary: "结构与数字一致性被查;洞察与结论诚实不在其中——写下边界句。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "报告验证看不到什么:结构、顺序、数字一致性被查——洞察、文笔、结论诚实不在其中。" + - title: "一句边界话" + body: "说清你的报告验证覆盖了什么、没覆盖什么——把这句话用到你自己的一份报告上,就完成了本检查点。" + - title: "老规矩" + body: "每个验证器都有盲区;说出自己的盲区正是这门课要建的工程习惯。" +--- + # 挑战 05:报告验证看不到的东西 **判卷:** 自我声明 · **积分:** 10 diff --git a/courses/da-visualization/REVIEW.md b/courses/da-visualization/REVIEW.md index 0aeea16..829edce 100644 --- a/courses/da-visualization/REVIEW.md +++ b/courses/da-visualization/REVIEW.md @@ -7,5 +7,15 @@ summary.json written to out/. - `python verify.py progress`: deterministic claim codes; L03/L04 objective, L01/L02/L05 attested. -- Agent solvability run (challenge-model gate): **pending** — must be - recorded before public launch per docs/repo-plan-0.0.4.md §1. +- Agent solvability run (challenge-model gate): **done 2026-09-13**. + Agent: Devin (SWE-2 Max), CLI session on macOS arm64, repo venv with + pandas + matplotlib. Method: implemented `starter/charts.py` from + `TASK.md` only (Agg backend, groupby sums, three fig.savefig + + summary.json), did not read or copy `solution/`. Result: + `python verify.py starter` exits 0; `verify.py progress` printed all + five claim codes (l03/l04 `[passed]`). Deviations: none — solvable in + one pass; the only trap is forgetting `matplotlib.use("Agg")` before + pyplot import, which TASK.md states plainly. Limitations: single run; + the suite checks PNG validity and JSON numbers, not chart quality — + consistent with the L05 "what verification cannot see" lesson. + Starter restored to the deliberately-unfinished state after the run. diff --git a/courses/da-visualization/lessons/L01.md b/courses/da-visualization/lessons/L01.md index 242c7ad..9c3df44 100644 --- a/courses/da-visualization/lessons/L01.md +++ b/courses/da-visualization/lessons/L01.md @@ -1,3 +1,21 @@ +--- +id: course-da-visualization-l01 +type: course +title: "Challenge 01: A chart is packaged evidence" +summary: "Say what claim each required PNG makes before writing code — the JSON summary is the auditable layer." +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "Charts as packaged evidence: before coding, say what claim each required PNG makes — `revenue_by_region` claims region totals, `daily_revenue` claims the trend, `revenue_histogram` claims the distribution." + - title: "Claim before pixels" + body: "A chart whose numbers are wrong is a lie with good typography — the JSON summary is the auditable layer the PNG must match." + - title: "When you are done" + body: "You can state the three claims and which aggregation feeds each." +--- + # Challenge 01: A chart is packaged evidence **Gate:** self-attested · **Points:** 10 diff --git a/courses/da-visualization/lessons/L01_cn.md b/courses/da-visualization/lessons/L01_cn.md index e617389..7084f98 100644 --- a/courses/da-visualization/lessons/L01_cn.md +++ b/courses/da-visualization/lessons/L01_cn.md @@ -1,3 +1,21 @@ +--- +id: course-da-visualization-l01 +type: course +title: "挑战 01:图是证据的包装" +summary: "写码前先说出每张 PNG 声明什么——JSON 摘要才是可审计层。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "图表是打包好的证据:写码前先说出每张 PNG 声明什么——`revenue_by_region` 声明地区总计、`daily_revenue` 声明趋势、`revenue_histogram` 声明分布。" + - title: "先声明后像素" + body: "数字错了的图是排版精良的谎言——JSON 汇总才是 PNG 必须对得上的可审计层。" + - title: "完成标准" + body: "你能说清三条声明,以及各自吃哪个聚合。" +--- + # 挑战 01:图是证据的包装 **判卷:** 自我声明 · **积分:** 10 diff --git a/courses/da-visualization/lessons/L02.md b/courses/da-visualization/lessons/L02.md index 85ac5b5..e2ea29c 100644 --- a/courses/da-visualization/lessons/L02.md +++ b/courses/da-visualization/lessons/L02.md @@ -1,3 +1,21 @@ +--- +id: course-da-visualization-l02 +type: course +title: "Challenge 02: Spec first, chart second" +summary: "Write which aggregation feeds each chart and what summary.json must contain before matplotlib opens." +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "Spec before chart: which aggregation feeds each PNG, how many rows the input has, and what `summary.json` must contain — before matplotlib opens." + - title: "Write the numbers down" + body: "135 rows, 45 days, 3 regions — the suite checks `summary.json` against ground truth, so your spec is the answer key you grade the agent against." + - title: "When you are done" + body: "Your spec lists each output file and the exact numbers it must carry." +--- + # Challenge 02: Spec first, chart second **Gate:** self-attested · **Points:** 10 diff --git a/courses/da-visualization/lessons/L02_cn.md b/courses/da-visualization/lessons/L02_cn.md index d5e0015..59602a0 100644 --- a/courses/da-visualization/lessons/L02_cn.md +++ b/courses/da-visualization/lessons/L02_cn.md @@ -1,3 +1,21 @@ +--- +id: course-da-visualization-l02 +type: course +title: "挑战 02:先写规格,再画图" +summary: "在打开 matplotlib 之前写清每张图吃哪个聚合、summary.json 必须装什么。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "先规格后图表:每张 PNG 吃哪个聚合、输入多少行、`summary.json` 必须有什么——都在打开 matplotlib 之前。" + - title: "把数字写下来" + body: "135 行、45 天、3 个地区——套件拿 `summary.json` 对真值,你的规格就是你批改 Agent 的参考答案。" + - title: "完成标准" + body: "你的规格列出每个输出文件和它必须携带的精确数字。" +--- + # 挑战 02:先写规格,再画图 **判卷:** 自我声明 · **积分:** 10 diff --git a/courses/da-visualization/lessons/L03.md b/courses/da-visualization/lessons/L03.md index 9c3d394..6ecc92b 100644 --- a/courses/da-visualization/lessons/L03.md +++ b/courses/da-visualization/lessons/L03.md @@ -1,3 +1,21 @@ +--- +id: course-da-visualization-l03 +type: course +title: "Challenge 03: Three charts and a summary" +summary: "Implement the contract in starter/charts.py — Agg before pyplot, parsed dates, real PNG bytes." +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "`python verify.py starter` — `load_sales`, `summary`, the three chart functions, and `main`, all green." + - title: "Agg before pyplot" + body: "`matplotlib.use('Agg')` must run before importing pyplot or the script dies on display-less machines. And parse `date` first — `groupby` on unparsed strings sorts lexically." + - title: "Files must be real PNGs" + body: "The suite checks magic bytes, not that a file exists — write with `savefig` to the `out/` paths the contract names, and emit `summary.json` UTF-8 indent 2 with a trailing newline." +--- + # Challenge 03: Three charts and a summary **Gate:** objective — starter suite · **Points:** 10 diff --git a/courses/da-visualization/lessons/L03_cn.md b/courses/da-visualization/lessons/L03_cn.md index a64ea56..2d25951 100644 --- a/courses/da-visualization/lessons/L03_cn.md +++ b/courses/da-visualization/lessons/L03_cn.md @@ -1,3 +1,21 @@ +--- +id: course-da-visualization-l03 +type: course +title: "挑战 03:三张图与摘要" +summary: "在 starter/charts.py 实现契约——Agg 先于 pyplot、日期先解析、真 PNG 字节。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "`python verify.py starter`——`load_sales`、`summary`、三个画图函数和 `main` 全绿。" + - title: "Agg 要在 pyplot 之前" + body: "`matplotlib.use('Agg')` 必须先于导入 pyplot 执行,否则在没有显示器的机器上直接死。`date` 也要先解析——对未解析字符串 `groupby` 是按字典序排的。" + - title: "必须是真 PNG" + body: "套件查的是 magic bytes 不是文件存不存在——用 `savefig` 写到契约点名的 `out/` 路径,`summary.json` 用 UTF-8、缩进 2、结尾换行。" +--- + # 挑战 03:三张图与摘要 **判卷:** 客观——starter 套件 · **积分:** 10 diff --git a/courses/da-visualization/lessons/L04.md b/courses/da-visualization/lessons/L04.md index bb20e5b..7609542 100644 --- a/courses/da-visualization/lessons/L04.md +++ b/courses/da-visualization/lessons/L04.md @@ -1,3 +1,21 @@ +--- +id: course-da-visualization-l04 +type: course +title: "Challenge 04: Numbers match the charts" +summary: "summary.json is checked against ground truth; a chart disagreeing with the JSON is a failed chart." +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "Both suites: `summary.json` against ground truth — East 12334.94, North 14294.7, South 11200.48, grand 37830.12, 45 days." + - title: "JSON is the audit layer" + body: "A chart showing numbers that differ from the JSON is a failed chart — if the PNG and JSON disagree, the JSON loses the argument only when the PNG is right, which it almost never is." + - title: "When you are done" + body: "Every figure in `summary.json` matches the dataset's truth and you can trace each to its aggregation." +--- + # Challenge 04: Numbers match the charts **Gate:** objective — both suites · **Points:** 10 diff --git a/courses/da-visualization/lessons/L04_cn.md b/courses/da-visualization/lessons/L04_cn.md index bf252e5..dac8a50 100644 --- a/courses/da-visualization/lessons/L04_cn.md +++ b/courses/da-visualization/lessons/L04_cn.md @@ -1,3 +1,21 @@ +--- +id: course-da-visualization-l04 +type: course +title: "挑战 04:数字与图一致" +summary: "summary.json 对照真值校验;图与 JSON 不一致就是失败的图。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "双套件:`summary.json` 对真值——East 12334.94、North 14294.7、South 11200.48、总计 37830.12、45 天。" + - title: "JSON 是审计层" + body: "图上的数和 JSON 不一致就是失败的图——两者对不上时几乎总是 JSON 对。" + - title: "完成标准" + body: "`summary.json` 每个数字都对得上数据集真值,且都能追溯到它的聚合。" +--- + # 挑战 04:数字与图一致 **判卷:** 客观——双套件 · **积分:** 10 diff --git a/courses/da-visualization/lessons/L05.md b/courses/da-visualization/lessons/L05.md index 79bc0f2..519d0f6 100644 --- a/courses/da-visualization/lessons/L05.md +++ b/courses/da-visualization/lessons/L05.md @@ -1,3 +1,21 @@ +--- +id: course-da-visualization-l05 +type: course +title: "Challenge 05: Honest boundary" +summary: "The suite checks files and numbers — not readability or honest scales; state what was not verified." +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "Naming what verification cannot see: the suite checks files exist, are real PNGs, and numbers match — it does not judge readability, scale honesty, or design." + - title: "Write the boundary sentence" + body: "One sentence stating what was verified (files, PNG validity, numbers) and what was not (whether the chart tells the truth visually) completes this checkpoint." + - title: "Carry it forward" + body: "The same boundary applies to every automated check you will ever write: state what it proves and what it does not." +--- + # Challenge 05: Honest boundary **Gate:** self-attested · **Points:** 10 diff --git a/courses/da-visualization/lessons/L05_cn.md b/courses/da-visualization/lessons/L05_cn.md index 857d8fa..7ec011d 100644 --- a/courses/da-visualization/lessons/L05_cn.md +++ b/courses/da-visualization/lessons/L05_cn.md @@ -1,3 +1,21 @@ +--- +id: course-da-visualization-l05 +type: course +title: "挑战 05:诚实边界" +summary: "套件查文件与数字——不查可读性与刻度诚实;写清没验证什么。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "说出验证看不到什么:套件查文件存在、是真 PNG、数字对——不管可读性、刻度诚实、设计好坏。" + - title: "写下边界句" + body: "一句话说清验证了什么(文件、PNG 有效性、数字)和没验证什么(图在视觉上是否诚实),本检查点就完成了。" + - title: "带到以后" + body: "同一条边界适用于你今后写的每个自动化检查:说清它证明什么、不证明什么。" +--- + # 挑战 05:诚实边界 **判卷:** 自我声明 · **积分:** 10 diff --git a/courses/hands-on-python-with-claude-code/REVIEW.md b/courses/hands-on-python-with-claude-code/REVIEW.md index d4a34aa..6e4e844 100644 --- a/courses/hands-on-python-with-claude-code/REVIEW.md +++ b/courses/hands-on-python-with-claude-code/REVIEW.md @@ -25,4 +25,19 @@ ## Deviation log -(none recorded yet — first agent-taught run-through pending) +### 2026-09-13 — agent solvability run (challenge mode, not taught mode) + +- Agent: Devin (SWE-2 Max), CLI session on macOS arm64, system Python + 3.14 (stdlib only — this course needs no third-party deps). +- Method: implemented `starter/report_tool.py` from `TASK.md` and the + test expectations only; did not read or copy `solution/`. +- Result: `python verify.py starter` exits 0 (all nine tests); + `verify.py progress` printed all five claim codes (l03/l04 + `[passed]`, l01/l02/l05 `[attest]`). +- Observed deviations: none — contract landed on the first pass. All + three scenario skins (csv/json inputs, invalid-row isolation, + rounding, atomic write into a fresh directory) were exercised. +- Limitations: challenge-mode run (TASK.md → tests), not the taught + COURSE.md walkthrough — teaching quality remains separately pending. +- Starter restored to the deliberately-unfinished state after the run + (`--expect-failure` verified again). diff --git a/courses/hands-on-python-with-claude-code/lessons/L01.md b/courses/hands-on-python-with-claude-code/lessons/L01.md index d19e538..71aa5c1 100644 --- a/courses/hands-on-python-with-claude-code/lessons/L01.md +++ b/courses/hands-on-python-with-claude-code/lessons/L01.md @@ -7,6 +7,13 @@ lang: en-US content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "That you can reproduce the failure before fixing it: `python verify.py starter --expect-failure` must print the named failing tests. The checkpoint is seeing the failure objectively, not repairing it." + - title: "Read the failure names as a contract" + body: "Each failing test name maps to one line in `TASK.md` — e.g. `test_non_numeric_amount_is_invalid` is the contract's 'a non-numeric numeric_field value'. If a name maps to no contract line, that drift is worth noticing." + - title: "When you are done" + body: "You can say out loud which nine behaviors the suite proves and why the starter cannot fake them. `python verify.py progress` then shows this checkpoint's claim code." --- # Lesson 1: Reproduce the failure before you fix anything diff --git a/courses/hands-on-python-with-claude-code/lessons/L01_cn.md b/courses/hands-on-python-with-claude-code/lessons/L01_cn.md index d14d576..987ef5c 100644 --- a/courses/hands-on-python-with-claude-code/lessons/L01_cn.md +++ b/courses/hands-on-python-with-claude-code/lessons/L01_cn.md @@ -7,6 +7,13 @@ lang: zh-CN content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "考你能不能先复现故障再谈修复:`python verify.py starter --expect-failure` 必须打印出那组具名失败测试。重点是让失败变成客观证据,而不是修好它。" + - title: "把失败名当成契约读" + body: "每个失败的测试名都对应 `TASK.md` 里的一行——比如 `test_non_numeric_amount_is_invalid` 对应契约的『非数值的 numeric_field 值』。如果有名字对不上契约,那处漂移值得注意。" + - title: "完成标准" + body: "你能说清楚这九个测试各自证明了什么、为什么 starter 装不出来。然后 `python verify.py progress` 就会显示本检查点的认领码。" --- # 第 1 课:先复现故障,再谈修复 diff --git a/courses/hands-on-python-with-claude-code/lessons/L02.md b/courses/hands-on-python-with-claude-code/lessons/L02.md index fe82787..3b9f61b 100644 --- a/courses/hands-on-python-with-claude-code/lessons/L02.md +++ b/courses/hands-on-python-with-claude-code/lessons/L02.md @@ -7,6 +7,13 @@ lang: en-US content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "That you can read `TASK.md` as testable statements, not prose. Every sentence that starts with a function name is a contract line the suite can assert." + - title: "Trace one line to one test" + body: "Pick a contract line — say `total = valid + invalid must hold` — and find the test that asserts it in `tests/test_report_tool.py`. Then write one contract line of your own for a script you own: inputs, outputs, error cases, and 'done means <command> exits 0'." + - title: "The testability check" + body: "If you cannot tell whether a statement is testable, ask: could a suite assert it without reading your mind? Numbers, exit codes, files on disk — never vibes." --- # Lesson 2: Write the task contract before the change diff --git a/courses/hands-on-python-with-claude-code/lessons/L02_cn.md b/courses/hands-on-python-with-claude-code/lessons/L02_cn.md index 6dbc8a5..2ac2b49 100644 --- a/courses/hands-on-python-with-claude-code/lessons/L02_cn.md +++ b/courses/hands-on-python-with-claude-code/lessons/L02_cn.md @@ -7,6 +7,13 @@ lang: zh-CN content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "考你能不能把 `TASK.md` 读成可测试的语句而不是散文。每个以函数名开头的句子都是套件能断言的契约行。" + - title: "把一行契约对到一个测试" + body: "挑一行契约——比如 `total = valid + invalid must hold`——在 `tests/test_report_tool.py` 里找到断言它的测试。然后为你自己的一个脚本写一行契约:输入、输出、错误路径,以及『完成 = 某命令退出 0』。" + - title: "可测试性检查" + body: "判断一句话是否可测试:套件能否不读你的心思就断言它?看数字、退出码、磁盘上的文件——不看感觉。" --- # 第 2 课:动手之前先写任务契约 diff --git a/courses/hands-on-python-with-claude-code/lessons/L03.md b/courses/hands-on-python-with-claude-code/lessons/L03.md index d632c9c..7bd8625 100644 --- a/courses/hands-on-python-with-claude-code/lessons/L03.md +++ b/courses/hands-on-python-with-claude-code/lessons/L03.md @@ -7,6 +7,13 @@ lang: en-US content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "The gate is `python verify.py starter` — all nine tests green against your starter. Work one failing group at a time: `load_records` (csv/json/ValueError), then `build_report`, then `write_report`, then `main`." + - title: "Traps the suite actually asserts" + body: "Booleans are not numbers — `isinstance(True, int)` is True, so exclude bools explicitly. Group totals round to two decimals. `total = valid + invalid` must hold even when every row is invalid." + - title: "Atomic means atomic" + body: "`write_report` must use a sibling temp file plus `os.replace`, and no `.tmp` may survive a successful write — the suite checks the filesystem, not your intentions. Still red? Run the unittest directly to see the failing name." --- # Lesson 3: Drive a bounded change, test by test diff --git a/courses/hands-on-python-with-claude-code/lessons/L03_cn.md b/courses/hands-on-python-with-claude-code/lessons/L03_cn.md index 4b097ae..fcc9dd9 100644 --- a/courses/hands-on-python-with-claude-code/lessons/L03_cn.md +++ b/courses/hands-on-python-with-claude-code/lessons/L03_cn.md @@ -7,6 +7,13 @@ lang: zh-CN content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "门控是 `python verify.py starter`——九个测试在你的 starter 上全绿。一次只推一组失败:先 `load_records`(csv/json/ValueError),再 `build_report`,再 `write_report`,最后 `main`。" + - title: "套件真正断言的坑" + body: "布尔值不是数字——`isinstance(True, int)` 为 True,要显式排除。分组总计保留两位小数。`total = valid + invalid` 在全部行都非法时也必须成立。" + - title: "原子写就是原子写" + body: "`write_report` 必须用同级临时文件加 `os.replace`,成功时不能留下 `.tmp`——套件检查的是文件系统,不是你的意图。还红就直接跑 unittest 看失败名。" --- # 第 3 课:按测试驱动一次有边界的变更 diff --git a/courses/hands-on-python-with-claude-code/lessons/L04.md b/courses/hands-on-python-with-claude-code/lessons/L04.md index 9548be6..3fd6d6b 100644 --- a/courses/hands-on-python-with-claude-code/lessons/L04.md +++ b/courses/hands-on-python-with-claude-code/lessons/L04.md @@ -7,6 +7,13 @@ lang: en-US content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "Both suites: your starter passes, and you demonstrate you can review evidence rather than only produce green." + - title: "Exercise all three skins" + body: "Run `run_scenario` over every scenario folder — csv and json inputs plus the error paths. Then review your diff against the contract: any function doing more than its contract line is scope creep." + - title: "The skeptic part" + body: "If the suite is green but you cannot explain a diff, that is the real failure. Re-read the change before claiming — the claim code says you verified, not that the tests did." --- # Lesson 4: Verify like an engineer, review like a skeptic diff --git a/courses/hands-on-python-with-claude-code/lessons/L04_cn.md b/courses/hands-on-python-with-claude-code/lessons/L04_cn.md index 9536ef7..ec9f321 100644 --- a/courses/hands-on-python-with-claude-code/lessons/L04_cn.md +++ b/courses/hands-on-python-with-claude-code/lessons/L04_cn.md @@ -7,6 +7,13 @@ lang: zh-CN content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "双套件门控:你的 starter 全绿,同时你要证明自己会审查证据而不只是产出绿灯。" + - title: "三个皮肤都要端到端跑" + body: "对每个 scenario 文件夹运行 `run_scenario`——csv、json 输入和错误路径都过一遍。然后拿契约审你的 diff:任何函数做得比它的契约行多,就是范围蔓延。" + - title: "怀疑者的部分" + body: "套件绿了但你解释不了某个 diff,那才是真正的失败。认领前先重读改动——认领码声明的是你验证过,不只是测试跑过。" --- # 第 4 课:像工程师一样验证,像怀疑者一样审查 diff --git a/courses/hands-on-python-with-claude-code/lessons/L05.md b/courses/hands-on-python-with-claude-code/lessons/L05.md index 953e9bb..4fb813d 100644 --- a/courses/hands-on-python-with-claude-code/lessons/L05.md +++ b/courses/hands-on-python-with-claude-code/lessons/L05.md @@ -7,6 +7,13 @@ lang: en-US content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "That you port the workflow — not the code — to one script you own: a task contract, an isolated run, and an honest record." + - title: "Port the smallest useful slice" + body: "Atomic JSON write, one aggregate with rounding, and `main` exit codes are the portable core. Do not port the grouping machinery if your script does not need it." + - title: "The honest record" + body: "Write down what your new tests do NOT prove — that unverified list is the point of the course, not a footnote." --- # Lesson 5: Apply this to your own project diff --git a/courses/hands-on-python-with-claude-code/lessons/L05_cn.md b/courses/hands-on-python-with-claude-code/lessons/L05_cn.md index 24f971b..d8208a7 100644 --- a/courses/hands-on-python-with-claude-code/lessons/L05_cn.md +++ b/courses/hands-on-python-with-claude-code/lessons/L05_cn.md @@ -7,6 +7,13 @@ lang: zh-CN content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "考你能不能把这套工作流——而不是代码——搬到你自己一个脚本上:任务契约、隔离运行、诚实记录。" + - title: "只搬最小可用切片" + body: "可移植的内核是:原子 JSON 写、一个带舍入的聚合、`main` 的退出码。你的脚本用不到分组机制就别搬。" + - title: "诚实的记录" + body: "写下你的新测试没有证明什么——那张未验证清单就是本课的重点,不是附注。" --- # 第 5 课:把这套方法带回你自己的项目 diff --git a/courses/hands-on-with-openai-codex-cli/REVIEW.md b/courses/hands-on-with-openai-codex-cli/REVIEW.md index 5b52741..91340cc 100644 --- a/courses/hands-on-with-openai-codex-cli/REVIEW.md +++ b/courses/hands-on-with-openai-codex-cli/REVIEW.md @@ -21,4 +21,20 @@ ## Deviation log -(none recorded yet — first agent-taught run-through pending) +### 2026-09-13 — agent solvability run (challenge mode, not taught mode) + +- Agent: Devin (SWE-2 Max), CLI session on macOS arm64, system Python + 3.14 (stdlib only — this course needs no third-party deps). +- Method: implemented `starter/report_tool.py` from `TASK.md` and the + test expectations only; did not read or copy `solution/`. +- Result: `python verify.py starter` exits 0 (all nine tests); + `verify.py progress` printed all five claim codes (l03/l04 + `[passed]`, l01/l02/l05 `[attest]`). +- Observed deviations: none — contract landed on the first pass. This + course shares its task core (TASK/tests/scenario) with + hands-on-python-with-claude-code, and the same implementation passes + both suites. +- Limitations: challenge-mode run (TASK.md → tests), not the taught + COURSE.md walkthrough — teaching quality remains separately pending. +- Starter restored to the deliberately-unfinished state after the run + (`--expect-failure` verified again). diff --git a/courses/hands-on-with-openai-codex-cli/lessons/L01.md b/courses/hands-on-with-openai-codex-cli/lessons/L01.md index a9acb98..fb28e0d 100644 --- a/courses/hands-on-with-openai-codex-cli/lessons/L01.md +++ b/courses/hands-on-with-openai-codex-cli/lessons/L01.md @@ -7,6 +7,13 @@ lang: en-US content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "Same contract, different driver: run `python verify.py starter --expect-failure` and watch the named failures print — the checkpoint is reproducing the failure objectively, with Codex in the loop." + - title: "Read failure names as the contract" + body: "Each failing test maps to a `TASK.md` line. Hand the folder to Codex with a bounded prompt — 'reproduce the failure and list which tests fail' — and check its answer against the test names." + - title: "When you are done" + body: "You can state which nine behaviors the suite proves. `python verify.py progress` then shows this checkpoint's claim code." --- # Lesson 1: Reproduce the failure with Codex in the loop diff --git a/courses/hands-on-with-openai-codex-cli/lessons/L01_cn.md b/courses/hands-on-with-openai-codex-cli/lessons/L01_cn.md index d0853f2..6f1a662 100644 --- a/courses/hands-on-with-openai-codex-cli/lessons/L01_cn.md +++ b/courses/hands-on-with-openai-codex-cli/lessons/L01_cn.md @@ -7,6 +7,13 @@ lang: zh-CN content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "同一份契约、不同的驾驶员:跑 `python verify.py starter --expect-failure`,看具名失败打印出来——考的是你在 Codex 参与下把失败客观复现。" + - title: "把失败名当成契约读" + body: "每个失败测试对应 `TASK.md` 一行。用有边界的指令把文件夹交给 Codex——『复现故障并列出哪些测试失败』——再用测试名核对它的回答。" + - title: "完成标准" + body: "你能说清这九个测试各自证明了什么。`python verify.py progress` 随后显示本检查点的认领码。" --- # 第 1 课:让 Codex 进环,先复现故障 diff --git a/courses/hands-on-with-openai-codex-cli/lessons/L02.md b/courses/hands-on-with-openai-codex-cli/lessons/L02.md index 069efda..4351925 100644 --- a/courses/hands-on-with-openai-codex-cli/lessons/L02.md +++ b/courses/hands-on-with-openai-codex-cli/lessons/L02.md @@ -7,6 +7,13 @@ lang: en-US content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "Reading `TASK.md` as bounded, testable statements — the highest-leverage skill for driving Codex, since it does exactly what the contract says." + - title: "Trace and write one line" + body: "Trace one contract line to its test in `tests/test_report_tool.py`, then write one contract line for your own script: inputs, outputs, error cases, 'done means <command> exits 0'." + - title: "The testability check" + body: "A statement is testable when a suite can assert it without reading your mind: numbers, exit codes, files on disk." --- # Lesson 2: Write the task contract before the change diff --git a/courses/hands-on-with-openai-codex-cli/lessons/L02_cn.md b/courses/hands-on-with-openai-codex-cli/lessons/L02_cn.md index 68109b3..1f389e5 100644 --- a/courses/hands-on-with-openai-codex-cli/lessons/L02_cn.md +++ b/courses/hands-on-with-openai-codex-cli/lessons/L02_cn.md @@ -7,6 +7,13 @@ lang: zh-CN content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "把 `TASK.md` 读成有边界、可测试的语句——这是驱动 Codex 最值钱的技能,因为它严格按契约行事。" + - title: "对一行、写一行" + body: "把一行契约对到 `tests/test_report_tool.py` 里的测试,再为你自己的脚本写一行契约:输入、输出、错误路径、『完成 = 某命令退出 0』。" + - title: "可测试性检查" + body: "一句话可测试,当且仅当套件不用读你心思就能断言它:数字、退出码、磁盘文件。" --- # 第 2 课:动手之前先写任务契约 diff --git a/courses/hands-on-with-openai-codex-cli/lessons/L03.md b/courses/hands-on-with-openai-codex-cli/lessons/L03.md index 3d45a22..8987f5b 100644 --- a/courses/hands-on-with-openai-codex-cli/lessons/L03.md +++ b/courses/hands-on-with-openai-codex-cli/lessons/L03.md @@ -7,6 +7,13 @@ lang: en-US content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "`python verify.py starter` — nine tests green. Supervise Codex through small diffs: `load_records`, `build_report`, `write_report`, `main` — one failing group per prompt." + - title: "Traps the suite asserts" + body: "Booleans are not numbers — exclude `bool` before `int`. Group totals round to two decimals; `total = valid + invalid` holds even when every row is invalid." + - title: "Atomic writes" + body: "Sibling temp file plus `os.replace`; no `.tmp` survives success. If Codex writes to the destination directly, that is the diff to push back on." --- # Lesson 3: Drive a bounded change, test by test diff --git a/courses/hands-on-with-openai-codex-cli/lessons/L03_cn.md b/courses/hands-on-with-openai-codex-cli/lessons/L03_cn.md index a5fb815..97c86fd 100644 --- a/courses/hands-on-with-openai-codex-cli/lessons/L03_cn.md +++ b/courses/hands-on-with-openai-codex-cli/lessons/L03_cn.md @@ -7,6 +7,13 @@ lang: zh-CN content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "`python verify.py starter` 九测全绿。监督 Codex 做小步修改:`load_records`、`build_report`、`write_report`、`main`——每条指令只推一组失败。" + - title: "套件断言的坑" + body: "布尔值不是数字——在 `int` 判断前先排除 `bool`。分组总计两位小数;全部行非法时 `total = valid + invalid` 仍须成立。" + - title: "原子写" + body: "同级临时文件加 `os.replace`;成功不留 `.tmp`。如果 Codex 直接写目标文件,那就是你要驳回的 diff。" --- # 第 3 课:按测试驱动一次有边界的变更 diff --git a/courses/hands-on-with-openai-codex-cli/lessons/L04.md b/courses/hands-on-with-openai-codex-cli/lessons/L04.md index 2746a23..3715225 100644 --- a/courses/hands-on-with-openai-codex-cli/lessons/L04.md +++ b/courses/hands-on-with-openai-codex-cli/lessons/L04.md @@ -7,6 +7,13 @@ lang: en-US content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "Both suites plus a real review: verify both directions, run all three skins, and read the final diff like a skeptic." + - title: "Review against the contract" + body: "Diff each function against its `TASK.md` line — anything extra is scope creep, anything missing is a red test waiting to be found." + - title: "The honest record" + body: "Write down what is still unproven after green — the record that says 'verified' and the list of what it does not cover are the deliverable." --- # Lesson 4: Verify like an engineer, review like a skeptic diff --git a/courses/hands-on-with-openai-codex-cli/lessons/L04_cn.md b/courses/hands-on-with-openai-codex-cli/lessons/L04_cn.md index fdcc449..6bfe783 100644 --- a/courses/hands-on-with-openai-codex-cli/lessons/L04_cn.md +++ b/courses/hands-on-with-openai-codex-cli/lessons/L04_cn.md @@ -7,6 +7,13 @@ lang: zh-CN content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "双套件加真审查:两个方向都验证、三个皮肤都跑、像怀疑者一样读最终 diff。" + - title: "拿契约审 diff" + body: "每个函数对照它的 `TASK.md` 契约行——多做是范围蔓延,少做是还没被发现的红测。" + - title: "诚实的记录" + body: "写下绿了之后仍未证明的部分——『已验证』的记录加上未覆盖清单,才是这份交付物。" --- # 第 4 课:像工程师一样验证,像怀疑者一样审查 diff --git a/courses/hands-on-with-openai-codex-cli/lessons/L05.md b/courses/hands-on-with-openai-codex-cli/lessons/L05.md index 0168a90..e96a53b 100644 --- a/courses/hands-on-with-openai-codex-cli/lessons/L05.md +++ b/courses/hands-on-with-openai-codex-cli/lessons/L05.md @@ -7,6 +7,13 @@ lang: en-US content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "Port the workflow to a script you own — contract, isolation, rounding, atomic writes, honest record — not the code itself." + - title: "Smallest useful slice" + body: "Atomic JSON write, one rounded aggregate, exit codes. Skip what your script does not need." + - title: "Say what is not proven" + body: "One line stating what your new verify command does not cover completes the checkpoint." --- # Lesson 5: Apply this to your own project diff --git a/courses/hands-on-with-openai-codex-cli/lessons/L05_cn.md b/courses/hands-on-with-openai-codex-cli/lessons/L05_cn.md index 54c49d2..c8767ee 100644 --- a/courses/hands-on-with-openai-codex-cli/lessons/L05_cn.md +++ b/courses/hands-on-with-openai-codex-cli/lessons/L05_cn.md @@ -7,6 +7,13 @@ lang: zh-CN content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "把工作流搬到你自己一个脚本上——契约、隔离、舍入、原子写、诚实记录——而不是搬代码。" + - title: "最小可用切片" + body: "原子 JSON 写、一个带舍入的聚合、退出码。用不到的不要搬。" + - title: "说清没证明什么" + body: "写一句你的新验证命令不覆盖什么,就完成了这个检查点。" --- # 第 5 课:把这套方法带回你自己的项目 diff --git a/courses/mcp-server-in-python/REVIEW.md b/courses/mcp-server-in-python/REVIEW.md index df9c259..332a524 100644 --- a/courses/mcp-server-in-python/REVIEW.md +++ b/courses/mcp-server-in-python/REVIEW.md @@ -20,4 +20,20 @@ ## Deviation log -(none recorded yet — first agent-taught run-through pending) +### 2026-09-13 — agent solvability run (challenge mode, not taught mode) + +- Agent: Devin (SWE-2 Max), CLI session on macOS arm64, system Python + 3.14 (stdlib only — this course needs no third-party deps). +- Method: implemented `starter/mcp_server.py` from `TASK.md` and the + test expectations only; did not read or copy `solution/`. +- Result: `python verify.py starter` exits 0 (all tests); + `verify.py progress` printed all five claim codes (l03/l04 + `[passed]`, l01/l02/l05 `[attest]`). +- Observed deviations: none — contract landed on the first pass. The + stateless 2026-07-28 behaviors (removed `initialize`, per-request + `_meta.protocolVersion`, `input_required` round-trip) were all + exercised by the suite. +- Limitations: challenge-mode run (TASK.md → tests), not the taught + COURSE.md walkthrough — teaching quality remains separately pending. +- Starter restored to the deliberately-unfinished state after the run + (`--expect-failure` verified again). diff --git a/courses/mcp-server-in-python/lessons/L01.md b/courses/mcp-server-in-python/lessons/L01.md index d6a88e4..c69ac6a 100644 --- a/courses/mcp-server-in-python/lessons/L01.md +++ b/courses/mcp-server-in-python/lessons/L01.md @@ -7,6 +7,13 @@ lang: en-US content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "Understanding the shape before coding: MCP 2026-07-28 is a stateless JSON-RPC 2.0 surface — no initialize handshake, no sessions, versions travel in `_meta.protocolVersion`." + - title: "Say what is absent" + body: "List what the old flow had that this spec removed: initialize, sessions, roots, sampling. If `initialize` arrives, the right answer is a -32601 error, not a handshake." + - title: "When you are done" + body: "You can describe the whole lifecycle in one line: a request dict in, a response dict out." --- # What MCP actually is (2026-07-28) diff --git a/courses/mcp-server-in-python/lessons/L01_cn.md b/courses/mcp-server-in-python/lessons/L01_cn.md index 21511e8..56f2d32 100644 --- a/courses/mcp-server-in-python/lessons/L01_cn.md +++ b/courses/mcp-server-in-python/lessons/L01_cn.md @@ -7,6 +7,13 @@ lang: zh-CN content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "先理解形态再写码:MCP 2026-07-28 是无状态 JSON-RPC 2.0 接口——没有 initialize 握手、没有会话、版本走 `_meta.protocolVersion`。" + - title: "说出缺了什么" + body: "列出旧流程里被本规范删掉的东西:initialize、sessions、roots、sampling。收到 `initialize` 时正确回答是 -32601 错误,不是握手。" + - title: "完成标准" + body: "一句话说清整个生命周期:进来一个请求 dict,出去一个响应 dict。" --- # MCP 到底是什么(2026-07-28) diff --git a/courses/mcp-server-in-python/lessons/L02.md b/courses/mcp-server-in-python/lessons/L02.md index d999581..2ba6a4c 100644 --- a/courses/mcp-server-in-python/lessons/L02.md +++ b/courses/mcp-server-in-python/lessons/L02.md @@ -7,6 +7,13 @@ lang: en-US content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "The dispatch contract: valid requests echo their `id`, malformed ones get -32600, unknown or removed methods get -32601." + - title: "Version gate" + body: "A request carrying `_meta.protocolVersion` other than `2026-07-28` gets -32600 — requests self-describe their version instead of negotiating it." + - title: "When you are done" + body: "You can fill the truth table from memory: good request, bad jsonrpc, unknown method, removed method, wrong version." --- # The JSON-RPC dispatch contract diff --git a/courses/mcp-server-in-python/lessons/L02_cn.md b/courses/mcp-server-in-python/lessons/L02_cn.md index aed73a9..1641046 100644 --- a/courses/mcp-server-in-python/lessons/L02_cn.md +++ b/courses/mcp-server-in-python/lessons/L02_cn.md @@ -7,6 +7,13 @@ lang: zh-CN content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "分发契约:合法请求回显 `id`,畸形请求得 -32600,未知或已删除的方法得 -32601。" + - title: "版本门" + body: "携带非 `2026-07-28` 的 `_meta.protocolVersion` 的请求得 -32600——请求自我声明版本而不是协商版本。" + - title: "完成标准" + body: "凭记忆能填真值表:正常请求、坏 jsonrpc、未知方法、已删方法、错版本。" --- # JSON-RPC 分发契约 diff --git a/courses/mcp-server-in-python/lessons/L03.md b/courses/mcp-server-in-python/lessons/L03.md index 4dffb3e..968f767 100644 --- a/courses/mcp-server-in-python/lessons/L03.md +++ b/courses/mcp-server-in-python/lessons/L03.md @@ -7,6 +7,13 @@ lang: en-US content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "`python verify.py starter` — `MCPServer` with `register_tool` and `handle_request`: validation, dispatch, and error isolation all green." + - title: "Tools are untrusted" + body: "Missing required argument → `isError` content 'Missing required argument: <arg>'; unknown tool → 'Tool not found'; handler exceptions → `isError` 'Error: <e>'. A tool failing must never crash the transport." + - title: "Return shapes" + body: "`tools/list` yields `result.tools` entries with name/description/inputSchema; `tools/call` yields `result.content` of type text — get the envelope exact, the suite compares structure." --- # Validation and error isolation diff --git a/courses/mcp-server-in-python/lessons/L03_cn.md b/courses/mcp-server-in-python/lessons/L03_cn.md index 55913e3..ca5619a 100644 --- a/courses/mcp-server-in-python/lessons/L03_cn.md +++ b/courses/mcp-server-in-python/lessons/L03_cn.md @@ -7,6 +7,13 @@ lang: zh-CN content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "`python verify.py starter`——实现 `MCPServer` 的 `register_tool` 与 `handle_request`:校验、分发、错误隔离全绿。" + - title: "工具是不可信输入" + body: "缺必填参数 → `isError` 内容 'Missing required argument: <arg>';未知工具 → 'Tool not found';处理函数抛异常 → `isError` 'Error: <e>'。工具失败绝不能打断传输层。" + - title: "返回形态" + body: "`tools/list` 产出带 name/description/inputSchema 的 `result.tools`;`tools/call` 产出 text 型 `result.content`——信封结构要精确,套件比对的是结构。" --- # 校验与错误隔离 diff --git a/courses/mcp-server-in-python/lessons/L04.md b/courses/mcp-server-in-python/lessons/L04.md index c706aaa..db8151c 100644 --- a/courses/mcp-server-in-python/lessons/L04.md +++ b/courses/mcp-server-in-python/lessons/L04.md @@ -7,6 +7,13 @@ lang: en-US content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "Both suites — the `input_required` round-trip: a tool can ask the client a question mid-call and finish on the retry carrying `params.inputResponses`." + - title: "The exception is the mechanism" + body: "Raise `InputRequired(requests)` inside the handler; `handle_request` catches it and returns `resultType: 'input_required'` with the requests. The client retries the same call with answers added." + - title: "No state needed" + body: "The server stays stateless — the retry is a fresh `tools/call` whose `params.inputResponses` carries the answers. Do not stash the pending question." --- # The input_required round-trip diff --git a/courses/mcp-server-in-python/lessons/L04_cn.md b/courses/mcp-server-in-python/lessons/L04_cn.md index 98cc293..e1db923 100644 --- a/courses/mcp-server-in-python/lessons/L04_cn.md +++ b/courses/mcp-server-in-python/lessons/L04_cn.md @@ -7,6 +7,13 @@ lang: zh-CN content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "双套件——`input_required` 多轮交互:工具能在调用中向客户端提问,客户端带着 `params.inputResponses` 重试时完成。" + - title: "异常即机制" + body: "在处理函数里抛 `InputRequired(requests)`;`handle_request` 捕获并返回 `resultType: 'input_required'` 和请求列表。客户端携带答案重试同一调用。" + - title: "不需要状态" + body: "服务端保持无状态——重试是一次新的 `tools/call`,答案在 `params.inputResponses` 里。别存挂起的问题。" --- # input_required 多轮交互 diff --git a/courses/mcp-server-in-python/lessons/L05.md b/courses/mcp-server-in-python/lessons/L05.md index af70890..6ddf615 100644 --- a/courses/mcp-server-in-python/lessons/L05.md +++ b/courses/mcp-server-in-python/lessons/L05.md @@ -7,6 +7,13 @@ lang: en-US content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "Exposing one real tool: wrap a function you own behind the validated, isolated boundary you just built." + - title: "Schema honestly" + body: "Give the tool a real `inputSchema` naming required arguments — the boundary only earns trust if the schema tells the truth." + - title: "The honest record" + body: "Note what your tool may and may not do — a schema that overclaims defeats the isolation you just proved." --- # Expose one real tool to your agent diff --git a/courses/mcp-server-in-python/lessons/L05_cn.md b/courses/mcp-server-in-python/lessons/L05_cn.md index 734ea21..ecc4898 100644 --- a/courses/mcp-server-in-python/lessons/L05_cn.md +++ b/courses/mcp-server-in-python/lessons/L05_cn.md @@ -7,6 +7,13 @@ lang: zh-CN content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "暴露一个真实工具:把你自己一个函数包进刚建好的、带校验和隔离的边界后面。" + - title: "如实写 schema" + body: "给工具一个真实的 `inputSchema` 并标明必填参数——schema 说真话,边界才值得信任。" + - title: "诚实的记录" + body: "写下你的工具能做什么不能做什么——夸大的 schema 会抵消你刚证明过的隔离。" --- # 给你的 Agent 暴露一个真实工具 diff --git a/courses/verifying-ai-generated-code/REVIEW.md b/courses/verifying-ai-generated-code/REVIEW.md index 7426d9d..a9e1126 100644 --- a/courses/verifying-ai-generated-code/REVIEW.md +++ b/courses/verifying-ai-generated-code/REVIEW.md @@ -20,4 +20,20 @@ ## Deviation log -(none recorded yet — first agent-taught run-through pending) +### 2026-09-13 — agent solvability run (challenge mode, not taught mode) + +- Agent: Devin (SWE-2 Max), CLI session on macOS arm64, system Python + 3.14 (stdlib only — this course needs no third-party deps). +- Method: implemented `starter/ship_check.py` from `TASK.md` and the + test expectations only; did not read or copy `solution/`. +- Result: `python verify.py starter` exits 0 (all ten tests); + `verify.py progress` printed all five claim codes (l03/l04 + `[passed]`, l01/l02/l05 `[attest]`). +- Observed deviations: none — contract landed on the first pass. The + naive-starter traps (exit-code-only evidence, no-tests blessed, + non-atomic write, dropped unverified list) were all exercised by the + suite. +- Limitations: challenge-mode run (TASK.md → tests), not the taught + COURSE.md walkthrough — teaching quality remains separately pending. +- Starter restored to the deliberately-unfinished state after the run + (`--expect-failure` verified again). diff --git a/courses/verifying-ai-generated-code/lessons/L01.md b/courses/verifying-ai-generated-code/lessons/L01.md index 73bb865..a60c424 100644 --- a/courses/verifying-ai-generated-code/lessons/L01.md +++ b/courses/verifying-ai-generated-code/lessons/L01.md @@ -7,6 +7,13 @@ lang: en-US content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "That you watched the starter bless what it should refuse: run it on `scenario/no-tests` and `scenario/red-project` and see 'passed' where a human sees failure." + - title: "Name the blind spots" + body: "The starter checks exit codes only — a zero-test project 'passes' and a failing suite can look fine. Write down the two failure modes before fixing anything." + - title: "When you are done" + body: "You can say why exit-code-only verification lies, in one sentence." --- # "It runs" is not evidence diff --git a/courses/verifying-ai-generated-code/lessons/L01_cn.md b/courses/verifying-ai-generated-code/lessons/L01_cn.md index 24fdb82..b1ad520 100644 --- a/courses/verifying-ai-generated-code/lessons/L01_cn.md +++ b/courses/verifying-ai-generated-code/lessons/L01_cn.md @@ -7,6 +7,13 @@ lang: zh-CN content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "考你亲眼看到 starter 放行了不该放行的:在 `scenario/no-tests` 和 `scenario/red-project` 上跑它,看到本该失败的地方显示『通过』。" + - title: "给盲点命名" + body: "starter 只看退出码——零测试的项目能『通过』,失败的套件看着也像没问题。修任何东西之前先写下这两种失效模式。" + - title: "完成标准" + body: "一句话说清为什么只看退出码的验证会说谎。" --- # “能跑”不是证据 diff --git a/courses/verifying-ai-generated-code/lessons/L02.md b/courses/verifying-ai-generated-code/lessons/L02.md index 94dc431..ee02480 100644 --- a/courses/verifying-ai-generated-code/lessons/L02.md +++ b/courses/verifying-ai-generated-code/lessons/L02.md @@ -7,6 +7,13 @@ lang: en-US content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "Defining what a release record must prove before building it: command, exit code, parsed test count, outcome, an unverified list, and a verdict that refuses zero-test projects." + - title: "Field by field" + body: "Take `build_record`'s contract literally — `all_passed` is true only when every check has exit code 0 AND result 'ok'. Sketch the JSON on paper first." + - title: "The unverified list" + body: "It is the honest core: a record that cannot say what it did not check is marketing, not evidence." --- # Define what a release record must prove diff --git a/courses/verifying-ai-generated-code/lessons/L02_cn.md b/courses/verifying-ai-generated-code/lessons/L02_cn.md index 9835d90..05827e4 100644 --- a/courses/verifying-ai-generated-code/lessons/L02_cn.md +++ b/courses/verifying-ai-generated-code/lessons/L02_cn.md @@ -7,6 +7,13 @@ lang: zh-CN content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "先定义发布记录必须证明什么再动手:命令、退出码、解析出的测试数、结果、未验证清单,以及拒绝零测试项目的判定。" + - title: "逐字段抠契约" + body: "字面理解 `build_record` 的契约——`all_passed` 只有在每个检查都是退出码 0 且结果 'ok' 时才为真。先在纸上画出 JSON。" + - title: "未验证清单" + body: "它是诚实的内核:说不出自己没查什么的记录是广告,不是证据。" --- # 定义发布记录必须证明什么 diff --git a/courses/verifying-ai-generated-code/lessons/L03.md b/courses/verifying-ai-generated-code/lessons/L03.md index cb804e3..0b4b55a 100644 --- a/courses/verifying-ai-generated-code/lessons/L03.md +++ b/courses/verifying-ai-generated-code/lessons/L03.md @@ -7,6 +7,13 @@ lang: en-US content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "`python verify.py starter` — implement `run_checks`, `build_record`, `write_record`, `run_project`, `main` until the suite is green." + - title: "Parse, do not guess" + body: "`ran` comes from `Ran N tests in ...` — parse the unittest summary line. `result` is 'no-tests' when `ran == 0` regardless of exit code, 'failed' on nonzero exit, else 'ok'." + - title: "Atomic + exit codes" + body: "`write_record` uses a sibling temp plus `os.replace` and creates parents; `main` prints `all_passed=... checks=...`, returns 0 on all_passed else 1, usage errors return 2." --- # Build the ship check, test by test diff --git a/courses/verifying-ai-generated-code/lessons/L03_cn.md b/courses/verifying-ai-generated-code/lessons/L03_cn.md index 1300e74..0bf78e0 100644 --- a/courses/verifying-ai-generated-code/lessons/L03_cn.md +++ b/courses/verifying-ai-generated-code/lessons/L03_cn.md @@ -7,6 +7,13 @@ lang: zh-CN content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "`python verify.py starter`——实现 `run_checks`、`build_record`、`write_record`、`run_project`、`main`,套件全绿。" + - title: "要解析不要猜" + body: "`ran` 来自 `Ran N tests in ...`——解析 unittest 的汇总行。`ran == 0` 时无论退出码如何 `result` 都是 'no-tests',退出码非零是 'failed',否则 'ok'。" + - title: "原子写加退出码" + body: "`write_record` 用同级临时文件加 `os.replace` 并创建父目录;`main` 打印 `all_passed=... checks=...`,全过返回 0 否则 1,用法错误返回 2。" --- # 按测试逐组构建 ship check diff --git a/courses/verifying-ai-generated-code/lessons/L04.md b/courses/verifying-ai-generated-code/lessons/L04.md index 3907d84..e9d4c24 100644 --- a/courses/verifying-ai-generated-code/lessons/L04.md +++ b/courses/verifying-ai-generated-code/lessons/L04.md @@ -7,6 +7,13 @@ lang: en-US content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "Both suites plus reading records like a skeptic: green, red, and no-tests produce different verdicts — prove you can tell them apart." + - title: "Run all three skins" + body: "`run_project` on green-project, red-project, and no-tests gives ok, failed, and no-tests — check each `SHIP-RECORD.json` against the contract, not against your memory." + - title: "What the record cannot prove" + body: "State one thing a green record does not cover (like whether the tests themselves are any good) — that sentence is the lesson." --- # Read the record like a skeptic diff --git a/courses/verifying-ai-generated-code/lessons/L04_cn.md b/courses/verifying-ai-generated-code/lessons/L04_cn.md index 4997b45..7e423f6 100644 --- a/courses/verifying-ai-generated-code/lessons/L04_cn.md +++ b/courses/verifying-ai-generated-code/lessons/L04_cn.md @@ -7,6 +7,13 @@ lang: zh-CN content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "双套件加怀疑者式读记录:绿、红、零测试产出不同判定——证明你分得清。" + - title: "三个皮肤都跑" + body: "对 green-project、red-project、no-tests 运行 `run_project`,分别得到 ok、failed、no-tests——拿契约核对每个 `SHIP-RECORD.json`,不要凭记忆。" + - title: "记录证不了什么" + body: "写一句绿色记录覆盖不了的事(比如测试本身写得好不好)——这句话就是本课。" --- # 像怀疑者一样读记录 diff --git a/courses/verifying-ai-generated-code/lessons/L05.md b/courses/verifying-ai-generated-code/lessons/L05.md index 4cb91c8..eb0bf1c 100644 --- a/courses/verifying-ai-generated-code/lessons/L05.md +++ b/courses/verifying-ai-generated-code/lessons/L05.md @@ -7,6 +7,13 @@ lang: en-US content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "What this checkpoint tests" + body: "Gating a release you own: wire the ship check into a real project and let the record be part of shipping." + - title: "ship.json first" + body: "Give your project a `ship.json` with its real check command and an honest `unverified` list — the file is the contract, the script is just its executor." + - title: "The honest record" + body: "Keep one `SHIP-RECORD.json` and note what it deliberately does not claim — a record that pretends completeness fails the course's point." --- # Gate your next release diff --git a/courses/verifying-ai-generated-code/lessons/L05_cn.md b/courses/verifying-ai-generated-code/lessons/L05_cn.md index 334cffd..eecb794 100644 --- a/courses/verifying-ai-generated-code/lessons/L05_cn.md +++ b/courses/verifying-ai-generated-code/lessons/L05_cn.md @@ -7,6 +7,13 @@ lang: zh-CN content_version: 1 status: reviewed reviewed_on: 2026-09-12 +hints: + - title: "这个检查点考什么" + body: "给你自己的发布装门禁:把 ship check 接进一个真实项目,让记录成为交付的一部分。" + - title: "先写 ship.json" + body: "给项目一个 `ship.json`:真实的检查命令加诚实的 `unverified` 清单——文件是契约,脚本只是执行者。" + - title: "诚实的记录" + body: "留一份 `SHIP-RECORD.json`,写下它刻意不声明什么——假装完整的记录恰恰违背了本课初衷。" --- # 给你的下一次发布装上门禁 diff --git a/docs/repo-plan-0.0.4.md b/docs/repo-plan-0.0.4.md index 357145a..ef5294c 100644 --- a/docs/repo-plan-0.0.4.md +++ b/docs/repo-plan-0.0.4.md @@ -4,7 +4,9 @@ 更新日期:2026-09-12(第 3 版——挑战模型定稿 + A/B 阶段拆分) 关联:flypython.com `docs/product-and-growth-plan-0.0.4.md` -状态:除 FP-414 与真实 Agent 可解性试跑外,本计划已实现;网站当前固定 +状态:除 FP-414 外本计划已实现;真实 Agent 可解性试跑已于 2026-09-13 +完成(Devin, SWE-2 Max,全部 8 门课,挑战模式实测记录见各课 +`REVIEW.md`;授课式走查仍为另行待办)。网站当前固定 到 `920f790`(阶段 A + 数据分析模块 live 状态)。`AGENTS.md` 的仓库边界不变: 本仓库拥有经审核的内容、可运行的证据与稳定的 JSON 契约;网站拥有呈现、 账号体系与转化。所有新内容中英同一次变更交付。 @@ -134,7 +136,8 @@ `da-report`),中英 + verify.py 双向验证。(本地 2026-09-12: 首批非标准库课程,依赖经 `requirements.txt` 锁定——pandas 2.3.3 / matplotlib 3.10.9;da-eda 12 测试、da-visualization - 5 测试、da-report 6 测试;Agent 可解性实测仍待录 REVIEW.md) + 5 测试、da-report 6 测试;Agent 可解性实测已录 REVIEW.md + (2026-09-13)) - [x] FP-417 `paths/data-analysis/` 路线契约 + 综合项目挑战。(本地 2026-09-12:`path.json` + `capstone/`——303 行脏数据集、 `verify.py` 真值校验(容差 0.01)、综合项目认领码) @@ -160,7 +163,8 @@ ## 7. 自 0.0.3 结转(未完成) -- FP-326 备注:五次 Agent 实机授课记录仍待写入各课程 `REVIEW.md` +- FP-326 备注:挑战模式 Agent 可解性实测已写入各课程 `REVIEW.md` + (2026-09-13);授课式(COURSE.md 引导模式)实机走查仍待做 (属于发布证据,不是内容阻塞项)。 - FP-327 发布步骤:网站 pin 在一次刻意变更中固定到本仓库发布 SHA。 - FP-334 仓库描述/话题 + 首个 GitHub Release;该 Release 时将 diff --git a/paths/foundation/path.json b/paths/foundation/path.json index 20bea02..1d57744 100644 --- a/paths/foundation/path.json +++ b/paths/foundation/path.json @@ -33,7 +33,10 @@ "course": "hands-on-python-with-claude-code", "title_en": "Claude Code hands-on", "title_zh": "Claude Code 实战", - "points": 100 + "points": 100, + "branch": "first-agent", + "branch_label_en": "pick your first agent", + "branch_label_zh": "选你的第一个 Agent" }, { "order": 2, @@ -42,7 +45,10 @@ "course": "hands-on-with-openai-codex-cli", "title_en": "Codex CLI hands-on", "title_zh": "Codex CLI 实战", - "points": 100 + "points": 100, + "branch": "first-agent", + "branch_label_en": "pick your first agent", + "branch_label_zh": "选你的第一个 Agent" }, { "order": 3, diff --git a/uv.lock b/uv.lock index f80a43b..2a3b418 100644 --- a/uv.lock +++ b/uv.lock @@ -3,7 +3,8 @@ revision = 3 requires-python = ">=3.11" resolution-markers = [ "python_full_version >= '3.15'", - "python_full_version < '3.15'", + "python_full_version >= '3.12' and python_full_version < '3.15'", + "python_full_version < '3.12'", ] [[package]] @@ -244,6 +245,184 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.12'", +] +dependencies = [ + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, +] + +[[package]] +name = "contourpy" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version >= '3.12' and python_full_version < '3.15'", +] +dependencies = [ + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" } }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/5a/a55177dd22553a277388e8a1b3220e92de91bacb28356cdc73caa240121d/contourpy-1.4.0.tar.gz", hash = "sha256:20156f5a1ac4f8ce02656e39a61e82164a3d359796dc8026f75b062783d500e1", size = 13323726, upload-time = "2026-09-11T19:05:05.808Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/ba/01bde9753bdbba04a6da9d2bff3881f021bc708f654655e95fae26d3b3a3/contourpy-1.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:186ba929df36d61b6127da2e89cd1357e4cb79aca647381ab1a6feb0b152877b", size = 297302, upload-time = "2026-09-11T19:02:45.103Z" }, + { url = "https://files.pythonhosted.org/packages/dd/f5/c5bf67522d49a2222f3154fe46879451d26f0e35ece41770758a64ea78cb/contourpy-1.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c76f3a5318164db7d9401132fc1b5364b784c613c93fa506e3b5e6d1bf353ec8", size = 284921, upload-time = "2026-09-11T19:02:48.049Z" }, + { url = "https://files.pythonhosted.org/packages/0f/cc/cb989599eec12fda312e127eb8e04a8b21a7a6c94bf7ff1bc68273334a42/contourpy-1.4.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:43c3ccbb32c6294b183dcc8e8c46dacf5ecef809497e3d48a5be298eef185ad0", size = 356332, upload-time = "2026-09-11T19:02:51.408Z" }, + { url = "https://files.pythonhosted.org/packages/47/fb/6f620d7602507817b0b4c21dd790ed68688f1f4ae9c29aacf21f08db5cf1/contourpy-1.4.0-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:86d05cec773c9507a3950122e0e40ce77c23c75ceeb2fc189514e71893cbb34b", size = 405878, upload-time = "2026-09-11T19:02:54.093Z" }, + { url = "https://files.pythonhosted.org/packages/32/4e/693c6d6bdece679f0953775eef3a1b66d56be55478d936e7deb1f3004422/contourpy-1.4.0-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2deb580178ca19437a84bd77e4bc2cd91a8ccad212413a83c273900868b5978c", size = 408296, upload-time = "2026-09-11T19:02:56.528Z" }, + { url = "https://files.pythonhosted.org/packages/78/f9/b6831508960d559581c448532ba4df312217072975486dbf2b24fcc5b76f/contourpy-1.4.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:875f42444c9cf48d56f724f2637e60d0f73b3b12c9041e1484580a233edf9591", size = 383243, upload-time = "2026-09-11T19:02:58.638Z" }, + { url = "https://files.pythonhosted.org/packages/f2/21/52903825a0ae7bb625e8bd30a09816d4c3c5d6174a469c10a616166cf780/contourpy-1.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f863c6100bf926cf47d13f3cd75f9bb8ebd98aaab230eeb4468b91f98f39a6b3", size = 1357306, upload-time = "2026-09-11T19:03:01.036Z" }, + { url = "https://files.pythonhosted.org/packages/d8/79/d68b1ce8539e4071518fed9523f558398f34dcd078b8927b109c72dad2ef/contourpy-1.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3863ef2e2b13fe93f8c0ebb08ee400cb07153b8b0e91c5acb26e4537f283634f", size = 1427243, upload-time = "2026-09-11T19:03:03.866Z" }, + { url = "https://files.pythonhosted.org/packages/ac/f0/b75a10e9d0616b97a30de277b6dfe83f1879880854c7330337309530eb48/contourpy-1.4.0-cp312-cp312-win32.whl", hash = "sha256:5450f091ac1be0be3ad3a2a3b3f23b5e443e78c670ced4fd347d626f92a28fd2", size = 347345, upload-time = "2026-09-11T19:03:06.192Z" }, + { url = "https://files.pythonhosted.org/packages/50/a9/dab08786bb4d77ef9a046b3c1be923be4e73c5d07d91f5a54e8ea9b09e41/contourpy-1.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e697d94e69f499ff6bebb899cae97a58d5d14f0e1fe9568b43a0248d2f9af8c", size = 234083, upload-time = "2026-09-11T19:03:08.122Z" }, + { url = "https://files.pythonhosted.org/packages/47/b9/3ba509755a970dbde5948e7142ac21f266be72f38017cc4087a05fdceee1/contourpy-1.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:0c7a4c2716a4e98342221954416a836cca77c996c14ddbf22b5f01d5d93ca09c", size = 559521, upload-time = "2026-09-11T19:03:10.047Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8b/62a6eacea08ea0b6ab0159a7783df7ed421ff9c1ff58fef7c31f58fa5e52/contourpy-1.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e21d1d4a9db5b3a793649c7ab0ddb4697f2818929eec871a7b9c45fce7b2a1ea", size = 297330, upload-time = "2026-09-11T19:03:11.809Z" }, + { url = "https://files.pythonhosted.org/packages/3f/2f/103cdc5b7077cd569989d3ddb99ee3d4e96ee31bde8df2a6e426b2dc6195/contourpy-1.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:196759a3e4db60e1546167e361090ebb6d199aa3aafc9e34ea17a5ac3b23e814", size = 285037, upload-time = "2026-09-11T19:03:13.781Z" }, + { url = "https://files.pythonhosted.org/packages/bf/25/97001d1fdf940e6f5adfc592d4f5899fff95daf7dceead3988fe09987d2d/contourpy-1.4.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c0e07c691f3b3321913ed9b8161c50ea5f77fadd006f52f5e755359b0dcbfc5", size = 356675, upload-time = "2026-09-11T19:03:15.374Z" }, + { url = "https://files.pythonhosted.org/packages/a6/5e/9a7241a89e2f74402aefa2569e1349ad2750ff77823c04c788eaaedb7fdd/contourpy-1.4.0-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7f861af508fca2384eef392bc1d8377cfa349b2b854cb11b45d89c75780e2561", size = 406990, upload-time = "2026-09-11T19:03:17.44Z" }, + { url = "https://files.pythonhosted.org/packages/36/81/3a4fbbf6a806eada5453a6d16f6c7d96b9f4b3e57a3127368fb545986b8a/contourpy-1.4.0-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a03b32c2e7eda8b17757c022162c13c86f5c3d6f937dddf9ba3c3ff7b513953b", size = 407271, upload-time = "2026-09-11T19:03:19.523Z" }, + { url = "https://files.pythonhosted.org/packages/68/77/8f93ecbde1a4d11dfd460cd19a0c159f5487352e751fa87f5d78cc9db08e/contourpy-1.4.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1219a8898523cba821085f2da8a1b962cc696325763f09d7f93538ba43b2d70", size = 384707, upload-time = "2026-09-11T19:03:21.208Z" }, + { url = "https://files.pythonhosted.org/packages/05/07/e5cdf94d68961ee464de6b4fedd3d2f666584f59ccd124bb1290597122ab/contourpy-1.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:20da2f86bfaed60dbba729fe738c8241b1dd22b6cf7e8cbbf30df290bd04ba28", size = 1356741, upload-time = "2026-09-11T19:03:23.621Z" }, + { url = "https://files.pythonhosted.org/packages/fd/8a/2301d07accb257ac08daeb631875fada06fedd019c20ff7b00ee07e9d211/contourpy-1.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6ced1670fafee703b8277ab1645072a226e74f167166eae146fb743916119b67", size = 1426793, upload-time = "2026-09-11T19:03:25.955Z" }, + { url = "https://files.pythonhosted.org/packages/7d/5d/a62648f06259d15c139b14eb8cf3c58fe53cb970a55f2bb77d1070e80a41/contourpy-1.4.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:abd0f7e51ecc52bf8c95713bd80b1c17c516a71fa391c54475308a4377903c2b", size = 117961, upload-time = "2026-09-11T19:03:27.713Z" }, + { url = "https://files.pythonhosted.org/packages/71/31/c5b7771ba6982f319ec90c0cf5c425b36891adfeee2d9f667f90b2d0c166/contourpy-1.4.0-cp313-cp313-win32.whl", hash = "sha256:510f7d93d94cf6ebf4e2cd0640bea16ab8affac7547230175b6745b596ce0599", size = 347438, upload-time = "2026-09-11T19:03:29.829Z" }, + { url = "https://files.pythonhosted.org/packages/68/c1/a59df0754bf83a6f3e153cc19d550a5b01f607e389b1c4d57f436f9139e0/contourpy-1.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:6507a016976a75a15aee47809a34605de6244ccc056e6164bf9fb14b27eecad4", size = 234084, upload-time = "2026-09-11T19:03:31.657Z" }, + { url = "https://files.pythonhosted.org/packages/41/48/97224f5decb1efa5a610f912195deb45321f73ec9b9dac485cd6fe2d5a6a/contourpy-1.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:423bd5f4b3f11d54a8c597234e513382e3454bb106b8ec7ae32ba4ed63f8d99b", size = 559559, upload-time = "2026-09-11T19:03:33.528Z" }, + { url = "https://files.pythonhosted.org/packages/75/b0/b548628c8dddf6e5e0a981b7fa8e4c008024df09c7d17c6a9e271edfa0b3/contourpy-1.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a3e67bc1a6a4618a1dac7c3053a9ffece5ddfa2046b2670b342cf430bb0b87d1", size = 297354, upload-time = "2026-09-11T19:03:35.017Z" }, + { url = "https://files.pythonhosted.org/packages/04/5a/513484208742f65af2648c519f0b7ab034616e183c4403538024c136a1ab/contourpy-1.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:072a702e2e178f4fcf96f04775d0c059e4c917925abf0d3208ebb6865df3c23d", size = 283729, upload-time = "2026-09-11T19:03:36.818Z" }, + { url = "https://files.pythonhosted.org/packages/35/da/5a6562febc994b2c4bf9d01e57a50458ef8a2051bb3e7b35a00333e0fc26/contourpy-1.4.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:404dbcd9233513dfd1323b66ea593fef90e899f658f6b8a9ed8e93bd0ca669db", size = 357819, upload-time = "2026-09-11T19:03:39.058Z" }, + { url = "https://files.pythonhosted.org/packages/6d/99/7b358ce888aaa426f755daa096c99c59d17c352037344e836f20e5d3b957/contourpy-1.4.0-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:61624f6722480aa7e168fd746164e7bfdf48f8ccb6542f1613741200dc37e2b1", size = 407795, upload-time = "2026-09-11T19:03:41.293Z" }, + { url = "https://files.pythonhosted.org/packages/be/02/34d7548adf08c60967435d79d7a90882eacc84f5093435287974c7e99943/contourpy-1.4.0-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2e741a39dfc96babe1722561e81351aeec104526c57dbe91c167c1db606c2d55", size = 408552, upload-time = "2026-09-11T19:03:42.728Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8c/9677dad226e5aaac5b823d5b09d881ebc32ff1648af417597649841e516c/contourpy-1.4.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e439ab450c93455feb0218532ded3e946ec7a9b5f459069f122cfa44b89229f6", size = 384850, upload-time = "2026-09-11T19:03:44.402Z" }, + { url = "https://files.pythonhosted.org/packages/08/f5/e3fbeaa489c223629fbb434737cb3de4ecaca9cae4825e77b92490f640f8/contourpy-1.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:417be048f7e122cefbe2a34c51a1f2b0410f8eb35796f794d45475ddb7872d9d", size = 1357601, upload-time = "2026-09-11T19:03:46.746Z" }, + { url = "https://files.pythonhosted.org/packages/88/be/55fec293d342c222ed58a5d4056df4362e192af2e59527a3d5d822de8828/contourpy-1.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:43d80072a32299bf945de0a6dd4e034ea0162e832261de2e07a274a5adbba9c9", size = 1427340, upload-time = "2026-09-11T19:03:48.745Z" }, + { url = "https://files.pythonhosted.org/packages/86/40/0faabf453edf59b0e3633381d7ca72c3414fb9b72b5cbb08b06ed792d3e5/contourpy-1.4.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:fa1b787362a3856e63dd2b89449f6c384d55ee1beb8f94f4bcc871b40f02462c", size = 118355, upload-time = "2026-09-11T19:03:50.047Z" }, + { url = "https://files.pythonhosted.org/packages/38/0c/7ea75aa3559ae8a6805015e15a19c92b6480752ecc92526361e5776e88c5/contourpy-1.4.0-cp314-cp314-win32.whl", hash = "sha256:738c44fa71735a617f36da58e32810512407d283d5d1bb5b1cecb00d7eeba7cf", size = 356977, upload-time = "2026-09-11T19:04:11.401Z" }, + { url = "https://files.pythonhosted.org/packages/15/41/3df8cc14bb572c8b447f9dfbc876703f0f2f2896058285b5c87a1d4a638d/contourpy-1.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:181bea01bc742734ae672fa00c717d855dd35e1f029536406538c52e7b0cd73d", size = 240101, upload-time = "2026-09-11T19:04:12.772Z" }, + { url = "https://files.pythonhosted.org/packages/66/01/ee6830a5aa4565662a345172f6f35eeeed625d20f1c9d4ecd35318fe4ade/contourpy-1.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:64039341e2d8804f1a13bda8c69083eab935167bbd7e856cf096241f17d5bd45", size = 576359, upload-time = "2026-09-11T19:04:14.983Z" }, + { url = "https://files.pythonhosted.org/packages/51/af/49ee9c9cf012699e505c1b63c531cc99f19df50f205173092c4d541e3ec7/contourpy-1.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ef47fef9a912c77e3d84b014f701e5a9340f25703e9ed3bcebe37f91fb69dc49", size = 313601, upload-time = "2026-09-11T19:03:51.452Z" }, + { url = "https://files.pythonhosted.org/packages/ad/59/ce411ab2be0f805038626d8f4ca9ce563487ff51ff6c363e7512271564ed/contourpy-1.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:96019f059bcb3774acc0104be3360a57b36d33eaaa2e1d7f77c800ca8e07618b", size = 302812, upload-time = "2026-09-11T19:03:53.022Z" }, + { url = "https://files.pythonhosted.org/packages/95/b0/390915f9af14c1e2d3b2a9ad99d85b98df4352346a51e368cbb825f813c3/contourpy-1.4.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ddf5a2596d716fd3793434844caf31abc7a40b1e8718431420c89858268fb909", size = 348996, upload-time = "2026-09-11T19:03:54.594Z" }, + { url = "https://files.pythonhosted.org/packages/c7/85/e0952576d54322f3e9983b1e049a7a31f42ce82c5c29d01821e6dd3780d7/contourpy-1.4.0-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dd59df9e2fb0aff8bd7fd1adb9f349b7c835245a9d0f18c2f2deeb537190f2b1", size = 400272, upload-time = "2026-09-11T19:03:56.822Z" }, + { url = "https://files.pythonhosted.org/packages/d6/7d/f13543a5e1598e4a4ad01d9d5e2d65f9c09c37660616429913d606c5977d/contourpy-1.4.0-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b7794ea07cab575633daad8d6e963b662053391022294d3aacc1d9ba9ef54114", size = 412895, upload-time = "2026-09-11T19:03:58.487Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a8/6e5eaff53507dfd2dfc428240eecda32453067277d4b3e7ca71210b2d611/contourpy-1.4.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1c8a74744fa746eeaa12f0f9ed7f8b4add9d8f1b14d95e3b5e133675eac888f", size = 377060, upload-time = "2026-09-11T19:04:00.122Z" }, + { url = "https://files.pythonhosted.org/packages/16/a3/d00e44d35511b3cead5bd794ee672d4eecf30d9de4ceac57a49d6204064f/contourpy-1.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:107ec46f7aa1664266d69b1181aadc953d58d39f7d8d648fa5b795d4a060b0de", size = 1345688, upload-time = "2026-09-11T19:04:02.705Z" }, + { url = "https://files.pythonhosted.org/packages/1e/01/1e533b0cae32c8edd63fe40435ec3b29345dab87321afcd28722d7f957d1/contourpy-1.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5a4c89c7f38a0d7a94356d74e3391073c4325c4d47ad2fd065c3fe7dbae16c0c", size = 1416140, upload-time = "2026-09-11T19:04:04.77Z" }, + { url = "https://files.pythonhosted.org/packages/6d/0c/4ee66759f00d6a5d6909782f14f441d639fa18a4a43142e334351efc47f8/contourpy-1.4.0-cp314-cp314t-win32.whl", hash = "sha256:912c6afaa106e2f74ba22b30416b77ef7eb94e3bdc5a4df51ab147845dba9b6d", size = 371982, upload-time = "2026-09-11T19:04:06.233Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ac/b3e5324138c3e3741f526663f4f265f4f69dfdf26415a39428940864a5f6/contourpy-1.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:ef9440f6f8506246269a82734f5ff9e2e4c5e775b3996fc883cc491c5257eca6", size = 262907, upload-time = "2026-09-11T19:04:07.882Z" }, + { url = "https://files.pythonhosted.org/packages/19/37/c9aa45e47819dc15a38fc5c81a2fb987fde55e9d3b991fbde514e3b6b5f5/contourpy-1.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:fc9feef8f1f001c5b87decadc67c4a5d1eebb62ca39c4763d1237ff62cf2b707", size = 587071, upload-time = "2026-09-11T19:04:09.898Z" }, + { url = "https://files.pythonhosted.org/packages/cc/74/66d4e6fbea3bcb3fd4e5ae21c039ba31b8fc408b35884b4bc09b88016dce/contourpy-1.4.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:79b06c60d5e569ce12c5da8f0c51217cdc484386e2349fa717c30fafb56d2871", size = 297358, upload-time = "2026-09-11T19:04:16.718Z" }, + { url = "https://files.pythonhosted.org/packages/2e/8d/8f7f25e71d73f15d7859943189900d2d348e740388c9ef52a0180d284762/contourpy-1.4.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6f963ea9f0d68d2b6a02d861e3c0aae09ae25ba21f243fcf281e9fed468b078d", size = 283732, upload-time = "2026-09-11T19:04:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/f0/90/957549e3b03c407c670e3e191db63f4aef60dab6605bc967af3a806b543e/contourpy-1.4.0-cp315-cp315-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5545ff38229c15d11d822fc6f7415b932da9f4975f53e305b7b96ebfb01f889c", size = 357820, upload-time = "2026-09-11T19:04:20.077Z" }, + { url = "https://files.pythonhosted.org/packages/56/4c/fa8de87d907b41b05d03f4945a51675cd67319650d01b4c927d209ffa6ee/contourpy-1.4.0-cp315-cp315-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:630d32f06cedf37b7f1dd2b6a12e4492826bd477a4c9c7b2e2fee6ce3a2cea76", size = 407804, upload-time = "2026-09-11T19:04:21.781Z" }, + { url = "https://files.pythonhosted.org/packages/04/e7/96bd4f4a4ae285727befbdb4b332d53a2654cc5da4585d2525b47a063e1f/contourpy-1.4.0-cp315-cp315-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:be85d160e7c795113c2a93254dde2fc7e86b375322570eee78b420969426973c", size = 408559, upload-time = "2026-09-11T19:04:23.432Z" }, + { url = "https://files.pythonhosted.org/packages/ae/94/39660e8dc309f96bf7971eb4ec5659f272f84e42b34c9990a96ecce8442c/contourpy-1.4.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:018227e134b73090b06f911e773a526c77c9af426be29065500ce096a5accd4c", size = 384861, upload-time = "2026-09-11T19:04:25.198Z" }, + { url = "https://files.pythonhosted.org/packages/ab/7b/411dccbf73d5d19da49bf7735786b3baf4ddb1456412d48ae4d4ab1fc761/contourpy-1.4.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:4bffcb2e8cc5c0e837631acab59dd04265fdac376550e7c3e310523877c68cb7", size = 1357612, upload-time = "2026-09-11T19:04:27.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/39/bf2442130e7d5aae75a4de4504f0c8aca3ef3da3feac3940ee27ca985d83/contourpy-1.4.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc87f2ffa84a49a77d76cb792f28a48ff3b05a222aa0b2bddbd839ba78e1d5ca", size = 1427345, upload-time = "2026-09-11T19:04:29.646Z" }, + { url = "https://files.pythonhosted.org/packages/96/60/e5d9196dd83ccd7dd103762982b10ef19a4f72e8ef67ae7157699b9be0bb/contourpy-1.4.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:c8be3392b84cf43373a488874dc1ebf46f60784a70977f92586e18019d71c7ed", size = 118157, upload-time = "2026-09-11T19:04:31.091Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b2/ae3d90f08ba2ed82189d93b55ffbd41e559760e89af4f9ef54d51869f41d/contourpy-1.4.0-cp315-cp315-win32.whl", hash = "sha256:618137ec5778fb76d494c9ec854b104a377fddba0d128bef7136cce2c3bf0df8", size = 356982, upload-time = "2026-09-11T19:04:52.913Z" }, + { url = "https://files.pythonhosted.org/packages/02/a0/36648fff407dcf8b8482985d33367ffc0fe90be72383869312cd0bd00a1b/contourpy-1.4.0-cp315-cp315-win_amd64.whl", hash = "sha256:ea09dc704029930cbd097f75cb0cc1db9852adbcf1a151fac7e9559f7bcbfd19", size = 240101, upload-time = "2026-09-11T19:04:54.587Z" }, + { url = "https://files.pythonhosted.org/packages/07/09/49e987a96706b8f877596cee00d558d7880793a833addc6b33f80805e5dc/contourpy-1.4.0-cp315-cp315-win_arm64.whl", hash = "sha256:a5e2bb871b90cd7a52bdee63bf80ef7acb46398d27efc66d7679240016efc5aa", size = 576375, upload-time = "2026-09-11T19:04:56.515Z" }, + { url = "https://files.pythonhosted.org/packages/e3/18/820ba24a070fe2a834af73e52245c501960746f5c1777ebc64ec0c0dec12/contourpy-1.4.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:432e89f835cf01f8d2123a130a3126b17ee6e0348e4d62c0973872b30aa873bf", size = 313543, upload-time = "2026-09-11T19:04:32.582Z" }, + { url = "https://files.pythonhosted.org/packages/d3/4c/f61a63d8107e0c1c3fa47d30ce45abad90b2ad48a1a9a4510332db7e0176/contourpy-1.4.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:3e0a2392533e5e2abdf277c951047516acc8d2f3c42b2dd9dc34c907c9ebabf2", size = 302702, upload-time = "2026-09-11T19:04:34.257Z" }, + { url = "https://files.pythonhosted.org/packages/9d/93/5af95cb5113ecc6e780ca0c6c79756a2cea8833f971bbae112dd4ad73ded/contourpy-1.4.0-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:758e7496cec3195fc28a28945eab3ceef348c47fb95c7afc2eca2f3901209c8c", size = 354370, upload-time = "2026-09-11T19:04:36.158Z" }, + { url = "https://files.pythonhosted.org/packages/47/75/5a9942446647202beb7b4d150891bfcb78fb112c73862484bbe4cc031092/contourpy-1.4.0-cp315-cp315t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bee6ff96653e0807cd0f8eaef00174a031e3a2effa5c20f49a5a8d574b05af23", size = 402340, upload-time = "2026-09-11T19:04:37.914Z" }, + { url = "https://files.pythonhosted.org/packages/c3/4f/bfdb0c67775a584f957211936661289789c4d0ec3e988bca2c8c02f8090e/contourpy-1.4.0-cp315-cp315t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a58b5f130afec61a093d743cdda435cc4047748b6791627e34a68f41ec3a9b07", size = 410721, upload-time = "2026-09-11T19:04:39.925Z" }, + { url = "https://files.pythonhosted.org/packages/65/d3/dddf13d6bb26e145c99debd1f2695018b5f3e24452dd9bfc9904c9d2ec89/contourpy-1.4.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9dc493e7924e5aa32d89d0b8e50986280f3d4b284968a792787bd7bcffd3cad", size = 382604, upload-time = "2026-09-11T19:04:41.793Z" }, + { url = "https://files.pythonhosted.org/packages/37/b4/89f0a5f6c6ccd322eae2704c7e933c82a4e594a61773933b4b0a18646866/contourpy-1.4.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:c927ec747633e68c9920bcaad48b0bcefe648812b822f18c34e2d0e2270a3d2a", size = 1354996, upload-time = "2026-09-11T19:04:43.856Z" }, + { url = "https://files.pythonhosted.org/packages/5d/29/fc91cc1ac6f3b591a269f4357b656d6ed0c39eb17c148f41f6da2f9729b9/contourpy-1.4.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:80c7fc8e7ac217777ce7ba2db3cf8478652b5a4b335283111012ff9eaac9d842", size = 1425030, upload-time = "2026-09-11T19:04:45.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/b8/bb129f524210c5d3eb8616876e02af280fba8b5554e0ac1afad80b51e06b/contourpy-1.4.0-cp315-cp315t-win32.whl", hash = "sha256:90feb8da006803a95ba573fddc9536357d0c3faddc3a4c9e22816ca49af52878", size = 371854, upload-time = "2026-09-11T19:04:48.106Z" }, + { url = "https://files.pythonhosted.org/packages/d8/b1/bc774d9d200cfc4f073bba78b1bb1cb3b55bf1c8c345eae41fc2db510459/contourpy-1.4.0-cp315-cp315t-win_amd64.whl", hash = "sha256:de503609fdb71f597634ca64fb4bd2f13d27c97f140e912b1e8ed93544840df0", size = 262450, upload-time = "2026-09-11T19:04:49.502Z" }, + { url = "https://files.pythonhosted.org/packages/54/bf/eb4ff492e1c3d027617b5a20e4e6d63a035b41b8cf77a95950159556a3a5/contourpy-1.4.0-cp315-cp315t-win_arm64.whl", hash = "sha256:78ed5c5f962b3e156109f0531b174a65ba80d13cc681a70227b12524c162e184", size = 587064, upload-time = "2026-09-11T19:04:51.148Z" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + [[package]] name = "flypython" version = "0.1.0" @@ -257,7 +436,9 @@ dependencies = [ [package.optional-dependencies] dev = [ { name = "jsonschema" }, + { name = "matplotlib" }, { name = "mypy" }, + { name = "pandas" }, { name = "pytest" }, { name = "ruff" }, { name = "types-pyyaml" }, @@ -267,7 +448,9 @@ dev = [ [package.metadata] requires-dist = [ { name = "jsonschema", marker = "extra == 'dev'", specifier = ">=4.20.0" }, + { name = "matplotlib", marker = "extra == 'dev'", specifier = ">=3.10,<3.11" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.14.0" }, + { name = "pandas", marker = "extra == 'dev'", specifier = ">=2.3,<2.4" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3.0" }, { name = "pyyaml", specifier = ">=6.0.2" }, { name = "requests", specifier = ">=2.32.3" }, @@ -278,6 +461,71 @@ requires-dist = [ ] provides-extras = ["dev"] +[[package]] +name = "fonttools" +version = "4.65.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/77/51/d63c7e52163ac14393a35bd14bd7c0da95f8f74be5d7cc988092f9965129/fonttools-4.65.0.tar.gz", hash = "sha256:762ba5431358d0dbd4a01982484a1d494fb267e91f974cdcf20b80eab8560f6f", size = 3674467, upload-time = "2026-09-10T15:35:54.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/9e/58250cdc54d96fcfacb544e12997a6390fa4e6b71ae2241cfcfe5b341803/fonttools-4.65.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:06273c71e692caf5989c0437ca50875a5e49e216ddf653228fe9bb35bdc82c0f", size = 3089141, upload-time = "2026-09-10T15:33:24.509Z" }, + { url = "https://files.pythonhosted.org/packages/3e/67/0f0416069e38da0a1327a847a2e8dd1edb425d0043d8a3e63eb940070209/fonttools-4.65.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ca2b02d74e9ad7e21a1d11e4701425800a4b0c63cf90486e60258262feccbcbf", size = 2584298, upload-time = "2026-09-10T15:33:26.598Z" }, + { url = "https://files.pythonhosted.org/packages/99/0d/7e40e9957359afc0bab081131c215370a6d2d203361bb6f945ab595e924c/fonttools-4.65.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7830e9fa3bebc44dbc27ff44d8201def30ea5c48a773696d58e69e6bcd9cd5d4", size = 5498163, upload-time = "2026-09-10T15:33:29.071Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e6/e48cf0a272a5d4d17a09d44f92727e67f975ddfa94acc8464763d19a654d/fonttools-4.65.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a3991732c87b3f054a2a8cf86dd0d602833fa8cb37c911503173771646e1013d", size = 5457456, upload-time = "2026-09-10T15:33:31.918Z" }, + { url = "https://files.pythonhosted.org/packages/e4/8a/a5c67ddeda82ee5e4ec3bc52ac1cbb685f0bb7a0516badc55643b454ab0d/fonttools-4.65.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6031e77b3fb8c765055ba2b8bd8dcb17030f3bf2484c448b472fdedf4460ba80", size = 5464334, upload-time = "2026-09-10T15:33:34.605Z" }, + { url = "https://files.pythonhosted.org/packages/d7/16/294e77383b2d39c9f8f25144a7ba23fe1cbbc05227cc72545097785ff07c/fonttools-4.65.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6813cc1e2e883bd6c15b3e04f72c78dc65fdc4ca861063adf5f341fbaec2ca62", size = 5595597, upload-time = "2026-09-10T15:33:37.591Z" }, + { url = "https://files.pythonhosted.org/packages/80/01/8e74ce8626c734959c782f2d89af8e9f14d078fd3d4ddf8b5a51401ae475/fonttools-4.65.0-cp311-cp311-win32.whl", hash = "sha256:4a5db8442453da4b6f43ad325879381b726bf2238a2253efd9584be21a2cefc2", size = 2441114, upload-time = "2026-09-10T15:33:40.592Z" }, + { url = "https://files.pythonhosted.org/packages/37/3e/835dc6c658426e2670b7f38c38295492fcbaeb06080e9dce89ce8105993c/fonttools-4.65.0-cp311-cp311-win_amd64.whl", hash = "sha256:9f201796c8e24e657be77c16fa664e798a46122144217f90838982937a964f0a", size = 2498823, upload-time = "2026-09-10T15:33:43.402Z" }, + { url = "https://files.pythonhosted.org/packages/58/db/242fa4fce7f632c5f7ab15585343393b25792510c0c32bd218ad24d59f1c/fonttools-4.65.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e844a45c9e5ced6536f184cf1a65b5d65e8f7e711993b413e10500a8223622e5", size = 3097120, upload-time = "2026-09-10T15:33:46Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b6/42fa4d373416675f74446421cf0b2badb82a4245c60745f05f424f75c649/fonttools-4.65.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b30e953de049bf43fc0a63c7d0c44d205c923e4bbf24716aae1518c0e65f977c", size = 2584658, upload-time = "2026-09-10T15:33:48.473Z" }, + { url = "https://files.pythonhosted.org/packages/75/6f/d589b9d62280a846c77a2c383d852c6dcb79ae8aa02bf0fa46c8577af145/fonttools-4.65.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09c34bdeed8915bfb53bee0c8ed2254dbd8ec69c0014b7f3702f347c049bf358", size = 5425737, upload-time = "2026-09-10T15:33:51.473Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/de2c0c20a42c18e565a2617932beb08c06697bbdd0d3f62b108262e11583/fonttools-4.65.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:05595385ae99f4b9626cebb973bf171b8fe38a8f40708e6e42abba0ed7537778", size = 5402463, upload-time = "2026-09-10T15:33:54.907Z" }, + { url = "https://files.pythonhosted.org/packages/79/2e/bc0f5c9dce21821454bb5812d3b23410bca33c8bbd5468386d0327aa0cff/fonttools-4.65.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d95b34dd68fbfc0e4a1740c421597656117f979ed8dc85de66e08f9f9981806e", size = 5362168, upload-time = "2026-09-10T15:33:57.481Z" }, + { url = "https://files.pythonhosted.org/packages/1f/0d/2116763ade7e71e0e5d421babe1785d745be9b3d605bf914792ce1c97f79/fonttools-4.65.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:924d06e6130429168318db71c40174a765ad016fc4b56ca811287e3d7373b3a6", size = 5524117, upload-time = "2026-09-10T15:34:00.021Z" }, + { url = "https://files.pythonhosted.org/packages/e2/5e/f9600553b9f645e3068553831685ff1dab6259536a23b38d2e048de38f17/fonttools-4.65.0-cp312-cp312-win32.whl", hash = "sha256:04f73dd01005752a6e75cf4a8dc6b70dc724d1d4bc34cc89522153f4a2f07680", size = 2432089, upload-time = "2026-09-10T15:34:02.803Z" }, + { url = "https://files.pythonhosted.org/packages/3a/02/e436a6a1863b9862bab9f82d6da33055dd7aa3738017edd902a163525dc2/fonttools-4.65.0-cp312-cp312-win_amd64.whl", hash = "sha256:3b5d9ba89edf778b376e669b879ae33a198bf45cf5a23c3f6514f935cf9d0d9d", size = 2483475, upload-time = "2026-09-10T15:34:05.09Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5c/343a4225e83eb06f82c1d8bf41fc5e5f71eab2f62bc7ca215c764722c0a9/fonttools-4.65.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8b7bb52817a24731d2e4f4df0e71fdde05e6c806c8f8f1517b015d142fdacfa5", size = 3094663, upload-time = "2026-09-10T15:34:07.235Z" }, + { url = "https://files.pythonhosted.org/packages/9b/c9/49b2401be932741d9218181c08948c96db32eab21ecaaf79283b752e13e7/fonttools-4.65.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e2c21772fcf70325189707b19f346812690bb1b0bd7e207e6ac205244806b303", size = 2584521, upload-time = "2026-09-10T15:34:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/51/c9/48b07e6c5cf44fa56f758a04c3772a66c0e9ba82bbe22077e4076746a62b/fonttools-4.65.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64c9b26816415b5e3d899e9077109d327b22140fe3c4066644d8cdbad5bb1569", size = 5395399, upload-time = "2026-09-10T15:34:12.38Z" }, + { url = "https://files.pythonhosted.org/packages/fa/2d/5cc5a10c8ed56079d6c2e9e3e5920622529a2ae045c9f47a6055ccb1a319/fonttools-4.65.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6dd6243f60e2d6160c2966e1e14020dc261ffd741b69a2e4ca8bfd051592e4b7", size = 5376659, upload-time = "2026-09-10T15:34:15.239Z" }, + { url = "https://files.pythonhosted.org/packages/ab/6f/ccf33739d936bb3afa1655a225be7ee5d63d6d3c8b7d0e570bc5a2a7b899/fonttools-4.65.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:834962fd7cf21c58e81ac50a59e6ed2306f9df5e3dd481dad1cd7d2c4c60b773", size = 5336795, upload-time = "2026-09-10T15:34:17.817Z" }, + { url = "https://files.pythonhosted.org/packages/7e/c1/ffd17483f2094f0f4118974295b514b5b82afd4f6e3c80c23f01684e97a6/fonttools-4.65.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:580eb68ff7bd6954a7a76afddd864bfc66eaaf5f5c20dd6ead9186d0055a4ffe", size = 5496530, upload-time = "2026-09-10T15:34:20.241Z" }, + { url = "https://files.pythonhosted.org/packages/b1/19/9aca7712d0676ba5f8d1530ce20478a9bb09cccd0153d56693337379cdcf/fonttools-4.65.0-cp313-cp313-win32.whl", hash = "sha256:7a18b2ffd44249fe84289253197aa65ad4f2de554c0d381f18b1f5939bc6bc60", size = 2430392, upload-time = "2026-09-10T15:34:22.906Z" }, + { url = "https://files.pythonhosted.org/packages/1f/6f/f015dea0f4354e0798751b657cea2dee482914737f88bf1a078349ce92cf/fonttools-4.65.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ae1846b0f192fd485d26a455af19b8f5cf05aff08f9836f533913d8fcea133c", size = 2481620, upload-time = "2026-09-10T15:34:25.816Z" }, + { url = "https://files.pythonhosted.org/packages/64/29/606365ef601668bfebed14cfe3dc72bb7fcd1e23011bbb2833f17fea3065/fonttools-4.65.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:dc87a9f846bec83c3795804f62b4632716d46e3522869a3dd9cd44a5d245b006", size = 3098393, upload-time = "2026-09-10T15:34:28.472Z" }, + { url = "https://files.pythonhosted.org/packages/c7/61/11412939d6b7abf5ac7ce0d61d7f94a0a4fbabc9f1ab0a04fa622e0fc11c/fonttools-4.65.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aa50dd7b9baf75e2bbd43401fc0d237f7a94a8ad2e0c57ea97160fc631af5eb0", size = 2585766, upload-time = "2026-09-10T15:34:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/db/17/734921d8aee8309801da42590375d32d4d46f771b73373ec9520d5d4220b/fonttools-4.65.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d2a9892fdb3b7e2d0f4174e3b907d226ff83698249762eeefce08ec5b2de1dd", size = 5380679, upload-time = "2026-09-10T15:34:33.933Z" }, + { url = "https://files.pythonhosted.org/packages/cf/eb/2a4d78d60d978e694cfa04c98e4d8ddbf7f028fd768ddef470bb9da5d69e/fonttools-4.65.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6d815734e7fede0ad1f233f23f0f191cbe8fc64762ff041e589bc0f78e0b2397", size = 5321833, upload-time = "2026-09-10T15:34:36.295Z" }, + { url = "https://files.pythonhosted.org/packages/d9/ca/1cd48b5c11ef9658732787bf2362e1bf3871dad5945d2f6cc8f675ca769c/fonttools-4.65.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:71e4c67b6196a2f447f46476fd2302604721617f5e0a21b0988bdd87b6bb9687", size = 5320938, upload-time = "2026-09-10T15:34:38.992Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0d/90e6051bded926cccabe9dd0bce3b6ca012f4d5779d61167afbf4989ceb6/fonttools-4.65.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b11d8a4a0c3ca74bbd4c105b7ef82501945c939e6096d9934ec7d288cdf5aaa9", size = 5451747, upload-time = "2026-09-10T15:34:41.387Z" }, + { url = "https://files.pythonhosted.org/packages/18/74/23e0268e48029ff0752083f69312c49b738163d5af919add9dc4ac81e907/fonttools-4.65.0-cp314-cp314-win32.whl", hash = "sha256:8e44a34d91b3c793879767eb115867ced74d2eb94974e64e72fe9e2eea71cf1a", size = 2434246, upload-time = "2026-09-10T15:34:44.385Z" }, + { url = "https://files.pythonhosted.org/packages/a1/2d/ee69affecd4bc81cb932a213438d4199fb48bf8ca6d664438ccc7f623c2a/fonttools-4.65.0-cp314-cp314-win_amd64.whl", hash = "sha256:0aa8901db22875c831d6a91796549590d7e747da37438f38b69d771b668be445", size = 2486706, upload-time = "2026-09-10T15:34:46.842Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9c/edee5f785198ce3327e1ddeace91c773122d47f086a67e0a84b800f4a940/fonttools-4.65.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:2e4a380ca40d3a5372e31b340f0da0d53b4583aadbb8e41f6a516afa69c509a4", size = 3172027, upload-time = "2026-09-10T15:34:49.269Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c6/252ec9884381089bc30da75978b072593920249de60219817f16cbc9145f/fonttools-4.65.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:661bd91c4be13721408b2d4b67a9b3fa7736713adc9a6c9780c9c60fc7959f90", size = 2619020, upload-time = "2026-09-10T15:34:51.453Z" }, + { url = "https://files.pythonhosted.org/packages/42/79/f71b0d202b8473bb45b07876c08a474de9fe560ed2c0cde642812a81e22a/fonttools-4.65.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62c5e42c79449def957adf8a9a65a43018efa7e2a6bc6baa3afe955e0d5fb2ab", size = 5545942, upload-time = "2026-09-10T15:34:54.804Z" }, + { url = "https://files.pythonhosted.org/packages/4a/bd/52e1bf33e0aebfe22ecc9a85c634db707c1dd9f6b1b438efeed98c55b959/fonttools-4.65.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:36fca8efc46b5adfca327c666e739fc05b7a7a6ef17840230f81b22f53230f61", size = 5351477, upload-time = "2026-09-10T15:34:57.514Z" }, + { url = "https://files.pythonhosted.org/packages/5c/76/8c6b2ad20beec95cd446f3a8bdc753c7e4a4fd69ef3e66704c0f7c8cb0b5/fonttools-4.65.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8aa1291e4c767abf1b0b79ca2d6895f7c0b661d9d95d03b5791c883a9d1e1f08", size = 5412271, upload-time = "2026-09-10T15:35:00.895Z" }, + { url = "https://files.pythonhosted.org/packages/79/49/fadbf11bbbd2d699d88a5498a0634280206e01e3bd5da9a4e0c504953ce9/fonttools-4.65.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fcf39949f56911348514b466714efa9118bec3d2be249e1c487263f7cda6edab", size = 5446450, upload-time = "2026-09-10T15:35:03.369Z" }, + { url = "https://files.pythonhosted.org/packages/df/77/5fda646d3a6d5ee26465865c00319cc0925cf484a3b42dd8232d5b39f973/fonttools-4.65.0-cp314-cp314t-win32.whl", hash = "sha256:ffc918702661f1d74d2fbb2f5551036b64f6d2d743139e105289b694bcd16f54", size = 2467858, upload-time = "2026-09-10T15:35:05.744Z" }, + { url = "https://files.pythonhosted.org/packages/ae/0f/afa0f3de70ebe02bba46b32cccb30b1de52624472b14ac2e7cd403d08db9/fonttools-4.65.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5a977e3645dbffaee924209828aa702a215f7ff68bc08010740c10c723787e62", size = 2518248, upload-time = "2026-09-10T15:35:07.846Z" }, + { url = "https://files.pythonhosted.org/packages/86/54/b273cf5712b36a381c13284fbf244e811e1e1d7081aed20f200f0a191ffa/fonttools-4.65.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:7aa0518b45ff5286ad56f063938db3add3816e899aab58d782b3f9a252523caa", size = 3092770, upload-time = "2026-09-10T15:35:10.868Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/d3e4511475954d4ec4f3c254e81f0fcc1ade504cb7381eeca48b8c44b8a6/fonttools-4.65.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:673e2b3ac4ac8e4f3607d390ecc5a606e5db5c4e88fb4cb2999593efb65afea2", size = 2584364, upload-time = "2026-09-10T15:35:13.54Z" }, + { url = "https://files.pythonhosted.org/packages/e5/3f/7cfaba467bdd1d3d04be21ba7b5bd75c6ca58ba280e0519707ca96a43c4d/fonttools-4.65.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a03cff943b204a90bf3d1c04c97b9509a8aa0ee99e2e544084ca43ad995975b", size = 5377819, upload-time = "2026-09-10T15:35:15.839Z" }, + { url = "https://files.pythonhosted.org/packages/25/17/a68d9b19a97bb2ee37e8098fea50657073df97c7e5392afbe1b9d7c0c581/fonttools-4.65.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:41f684ee6212e411196ab054f8308faf6605f154950e6f4686fb8f2103d624b0", size = 5341296, upload-time = "2026-09-10T15:35:18.527Z" }, + { url = "https://files.pythonhosted.org/packages/ad/8d/d744653ed607a241339015d4af6743ca3d85a74a2045a02cc06ea0383c71/fonttools-4.65.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:52ea9d2a8385075770db74d5e5718fa80b2222bb4fc62856a377dd2865ca8848", size = 5315310, upload-time = "2026-09-10T15:35:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c8/1ca6dc69cbaa0e394ff70d9e266777124d3ce3c6433026a6b4de50b890ae/fonttools-4.65.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:d0d25027ade65ec46b13c0436e51bcb7c5171a4ea255a5e7a8d0d1d3ab4cffd7", size = 5463840, upload-time = "2026-09-10T15:35:24.022Z" }, + { url = "https://files.pythonhosted.org/packages/07/97/d374df38a14f2ac04ba9ed96bca89d4988e7d63aa5ed85c54fd8f2badd7d/fonttools-4.65.0-cp315-cp315-win32.whl", hash = "sha256:22cb846d35d278235ef3b7e947c6040b2057d72e8305a314f21d5342eca49040", size = 2433149, upload-time = "2026-09-10T15:35:26.59Z" }, + { url = "https://files.pythonhosted.org/packages/62/c5/eb8f7506faf6a70c5a8f2eccbeea3cd826c748a6fd78c7b4b3effb5a3a11/fonttools-4.65.0-cp315-cp315-win_amd64.whl", hash = "sha256:aecc899fdbf9ecbf728f8977977e2e1043ee4d70c257124c8fa4cbcf796fcd83", size = 2485725, upload-time = "2026-09-10T15:35:28.945Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a4/2df0d97514feb8d857d5de854cd8d660d9a7ee1fd081bc98497124f515a8/fonttools-4.65.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:6275863dad195ee34b6e0ca3fc61c74096bc37e5d6fb8e049f4d68d65865a2b7", size = 3164065, upload-time = "2026-09-10T15:35:31.197Z" }, + { url = "https://files.pythonhosted.org/packages/15/33/e09661c09e6c8a3b0bd50da0e72918df7576dede31f39c948cc245011434/fonttools-4.65.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:d8ffd2f62b402180b0edae8f86a071f583970e2177143117db5cf4c52da60079", size = 2615195, upload-time = "2026-09-10T15:35:33.569Z" }, + { url = "https://files.pythonhosted.org/packages/64/d7/114b05f4193679d0935272220083ec43de7f918b5a777018c5ac5c1ae39e/fonttools-4.65.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:830f91327ca83bfc1278e7060068a498938f84d05dc4869675486f84f55d4fe1", size = 5521239, upload-time = "2026-09-10T15:35:36.291Z" }, + { url = "https://files.pythonhosted.org/packages/ba/10/67d615939f859ffe75663a67bca3593966febbcd0109ec72f20f73cbb4cc/fonttools-4.65.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9db2cb95847c18eef74a4ef0fe257a893ae3f4b0395f4866e2f426ab07f3d804", size = 5343802, upload-time = "2026-09-10T15:35:38.82Z" }, + { url = "https://files.pythonhosted.org/packages/d1/06/faa793a806da03acce862fbed353f76026b28103d43860d69be9a6a1f0fd/fonttools-4.65.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:be9b9a95ed0af03375e99020e921c4bc6b41fad10e053dea7acad370521a3c46", size = 5388482, upload-time = "2026-09-10T15:35:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/53/d2/eb7258df60e634db60c9a8cd72bc9eaf8dea409d1b1ceec3b9fb49531ad2/fonttools-4.65.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:bbd9faf777a9deb6790df4f2b0be611857c45fe86605e840d7154a028d828af7", size = 5435088, upload-time = "2026-09-10T15:35:44.639Z" }, + { url = "https://files.pythonhosted.org/packages/00/6a/58597f16e1265fe9205de3069e338a339e3eef0b1daf049ee08269458091/fonttools-4.65.0-cp315-cp315t-win32.whl", hash = "sha256:c779d838815b91889c95ed64c9be5950ad5a683279f91aeb23384cb757ddc6a3", size = 2464925, upload-time = "2026-09-10T15:35:47.224Z" }, + { url = "https://files.pythonhosted.org/packages/4b/95/122fc172006db747f4968e08c710f52a94f55eb007173b859b3f80b5b810/fonttools-4.65.0-cp315-cp315t-win_amd64.whl", hash = "sha256:d9484b7ee1b49b6b8a0231c849f3983723dec29e3a7366d9b1b02f4036f71944", size = 2513945, upload-time = "2026-09-10T15:35:49.895Z" }, + { url = "https://files.pythonhosted.org/packages/e6/35/f894ceb867118c0261d0f69a9bd516b045a3754238f76c88a49513ac7a83/fonttools-4.65.0-py3-none-any.whl", hash = "sha256:3060b8c1fc2329fa20265b7c138614143ea7c1624e26c5c180c76aeb74deae6f", size = 1196441, upload-time = "2026-09-10T15:35:52.347Z" }, +] + [[package]] name = "idna" version = "3.19" @@ -323,6 +571,136 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] +[[package]] +name = "kiwisolver" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/07/bd78e6a8fae171ea041ef5bba3ed21a003522fa088834b069b1909981f30/kiwisolver-1.5.1.tar.gz", hash = "sha256:f1303ef2eec81262a4b708c3e858afe58d7c75ad91c1c05266eda7673369859a", size = 104395, upload-time = "2026-08-28T10:28:27.153Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/7b/2de6908edc668427c149af5f93112e931f87e1fa4cab80bac32c5844dccc/kiwisolver-1.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b3d78f7bb2b9d9a30345be1474b9aaa8685430b54afb51ba3639b5c6c11e9ed6", size = 123364, upload-time = "2026-08-28T10:25:04.359Z" }, + { url = "https://files.pythonhosted.org/packages/8a/24/e70914415c77c97be7e22c80a0740869cb7428768cc380fdcdf6703e7084/kiwisolver-1.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5025e36fb4fb275cef0a4e30dbb11cb4ae61d1c83deb90189cb5d7e4cafd6b55", size = 66558, upload-time = "2026-08-28T10:25:05.506Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2b/8b08b11833db4d475b8ef1f36174f8d8a7abd31bedd7e794be78e8814b48/kiwisolver-1.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc1a26b8e53395a01c2c611e58602fa47461f136fba7cd5542e6db6d64be1839", size = 64071, upload-time = "2026-08-28T10:25:06.7Z" }, + { url = "https://files.pythonhosted.org/packages/89/00/05c2d0369ac322d22d5c05f84b5c4a6856fa6207fbae42869108a28f0383/kiwisolver-1.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:95a02752aa032eef4aed01cda6d9b687c669bd0396bf4519eef8bba22a286720", size = 1438206, upload-time = "2026-08-28T10:25:08.254Z" }, + { url = "https://files.pythonhosted.org/packages/c0/05/c941a139f27438c1910d630fdc3ccfdab7c8407c72052299ead12ece086e/kiwisolver-1.5.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:719a35fa1156db3640555f95ebb94f60a444e64d1c69626b0edef5df78eba225", size = 1248975, upload-time = "2026-08-28T10:25:10.053Z" }, + { url = "https://files.pythonhosted.org/packages/58/a1/2669ee5512e39b9d4de25faacaedf788c957f93730c5f7c63993ec4f5933/kiwisolver-1.5.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:febcce10f2bcdbb80b4ea919238a6a4ac13dbc4c7cadbe8d5d75c3682f8b5404", size = 1266301, upload-time = "2026-08-28T10:25:11.754Z" }, + { url = "https://files.pythonhosted.org/packages/28/b8/353f52f2c7f861a9e90cd2e8f90f85b3ad03060835f823e08298d094c463/kiwisolver-1.5.1-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1d852545c4d0e35a72728d072cbaa59e2fa7dd84bdf01e068d670dd0ceb58eb6", size = 1319708, upload-time = "2026-08-28T10:25:13.559Z" }, + { url = "https://files.pythonhosted.org/packages/21/0e/14b83200eadc2c1d63b76bac01c1813bf072aecf567429f303e00b70258e/kiwisolver-1.5.1-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:2e10ae1bba1899188b33557c10d73affcc12033edd18adddb57d209039976a4c", size = 971720, upload-time = "2026-08-28T10:25:14.934Z" }, + { url = "https://files.pythonhosted.org/packages/86/91/9d43d84d23b1cbff72a142d387ead1ea03db0cba8ff86ed5335addad3cc9/kiwisolver-1.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b69602970994a2ed8bbfa78c2f0394a7435226c6040489702d9f0a0ad0c07052", size = 2200119, upload-time = "2026-08-28T10:25:16.636Z" }, + { url = "https://files.pythonhosted.org/packages/ff/7c/f2bd9616f27ffb5e17cecc0baa5d0bbcee7e55aeddc0ccc871d69e2fc3ee/kiwisolver-1.5.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d50de98e8d807dc31822fff96f50293163a62418eb65487a21b42713d72ed0b7", size = 2295005, upload-time = "2026-08-28T10:25:18.374Z" }, + { url = "https://files.pythonhosted.org/packages/ba/17/ee671b72bf8f46a08379d4392c65582541759a542428197562f2898294ad/kiwisolver-1.5.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3221f78211074f561c44ca42eac0619828171bec15a2c4cf6f7747d07df76e8e", size = 1960982, upload-time = "2026-08-28T10:25:19.893Z" }, + { url = "https://files.pythonhosted.org/packages/2e/f7/0e26b4c05bee3bdb0f048dfa305e4fe701999ea17b51e9c616ef91035bbe/kiwisolver-1.5.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0ba9527afc80ae3d7814ed98b6572d02bf85eaf48065678342c5f0c6dab7a8c7", size = 2464918, upload-time = "2026-08-28T10:25:21.65Z" }, + { url = "https://files.pythonhosted.org/packages/ae/62/6eb431133d30ce656ac1e5ff72fac70dd34d54c3984f4011b9ac8bf77d54/kiwisolver-1.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e12dfea7f5fc2a34a9080efbf79c4c44eb380ec5b9c6fea09407e08f0d1e941d", size = 2270967, upload-time = "2026-08-28T10:25:23.643Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9f/6f9e489c188200e6fb3193935501894811e8c97577c8ffe9033589bf3521/kiwisolver-1.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:1a7587dc335f2c0f5bd577fd0540bd16c66006bdb60f759a1059f025e6c4f071", size = 70744, upload-time = "2026-08-28T10:25:25.061Z" }, + { url = "https://files.pythonhosted.org/packages/c6/6d/dfc430d1d43957061599adea3f08ea982bb6f4ab601a8c974bedcf2ba850/kiwisolver-1.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:e4e4523d6f336708d732516e6cfca7796cf3d96c9474eb5aecf6165f2f1fefc3", size = 68404, upload-time = "2026-08-28T10:25:26.186Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9b/65b302742389c6f96f2956bef5decf26011309feb2fc5d79613af18adea4/kiwisolver-1.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:63fb7294b768f444eb4b068965f2662f28c2fd4161e23bd60fcf3ff27b74c046", size = 123876, upload-time = "2026-08-28T10:25:27.44Z" }, + { url = "https://files.pythonhosted.org/packages/71/74/c21f339956f6f691b2ed7e31d5f3ae767304df6c460192739fc830853051/kiwisolver-1.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ebdef3eae5336568147c39a55be6a2036ffde53faa9ca2d978989ae7c2da12c", size = 66487, upload-time = "2026-08-28T10:25:28.728Z" }, + { url = "https://files.pythonhosted.org/packages/84/e5/bdb34e21523e01dceda064d63713f3bdec91388af24fba1eca7ea5e85864/kiwisolver-1.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1798e83840c3f627246104c4d8a9639c60fa068adf9ce92b61791781fa8a68c1", size = 64660, upload-time = "2026-08-28T10:25:30.071Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f4/dadfec469313c7f428efa7e84b4aba9732f813c13ea7131a24b7b008ef57/kiwisolver-1.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34633ecf50d16187ab8e5528b7a2530f2feb4e23f300db4672538b51cfc5cd38", size = 1477929, upload-time = "2026-08-28T10:25:31.495Z" }, + { url = "https://files.pythonhosted.org/packages/6f/35/09c58daac34e6f6ea5c6dee0094b422118e5a7c265586008a95fd135ac5f/kiwisolver-1.5.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d27c2123977cb9269c30a49ba45f03a4323017ef693e19db4ec9dbe1299a3002", size = 1278499, upload-time = "2026-08-28T10:25:33.375Z" }, + { url = "https://files.pythonhosted.org/packages/19/32/739765e24fbad29d13f83e546ea4abc215a78cea9d677ca09025b027724d/kiwisolver-1.5.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6a797a1cefc8b9c93170db580337e1fe3d011ad18b1299943231279406342048", size = 1296677, upload-time = "2026-08-28T10:25:35.059Z" }, + { url = "https://files.pythonhosted.org/packages/df/32/03304d1010e2cc45e5b3b52cef7e43fed3a2a5cd6c87a89b4a88e1d85b5d/kiwisolver-1.5.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2551cf9917af48ee7c4b29cc82320489508cf96fd26a51f6fc124de661cd44c7", size = 1346037, upload-time = "2026-08-28T10:25:36.705Z" }, + { url = "https://files.pythonhosted.org/packages/3e/57/4c49377bfd274450dd72ecaa13eaac32ea804a03363e4d1db0c5aa999ceb/kiwisolver-1.5.1-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:38f6e0deb4d0a4615efe0c4efc5990b06ae450ab50a0b321c0b078b6d238c083", size = 988248, upload-time = "2026-08-28T10:25:38.299Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c3/38df144a08b6c5d75ca4504e5cc3141bb3bfef64c04f4ef48204f42711b6/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bfd1de989b3330420e29de39352f5c049905c9e3ee67233a50d550e3d652c148", size = 2228722, upload-time = "2026-08-28T10:25:40.038Z" }, + { url = "https://files.pythonhosted.org/packages/e7/11/3221838a89cd64d9b386353e000cd8a296069a20fbe3584507fdfd5bebae/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1209042a623ddfda5497e4066c7b77651dde8e1d3a9dd97599dc7e97f3b9b78c", size = 2325216, upload-time = "2026-08-28T10:25:41.699Z" }, + { url = "https://files.pythonhosted.org/packages/83/d4/075c219230697bb5db910d37262b9bacf880f92b4811a02ab81ed073a253/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:26e8268480be5061d509e29669d59103c067a26377a56491630ece11762e3858", size = 1977689, upload-time = "2026-08-28T10:25:43.559Z" }, + { url = "https://files.pythonhosted.org/packages/bb/08/1d219c3c2dd960983d0d4da623d916e9de6385df2b0bab3d1af0e9b8fccc/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d79308fa689fac89cbcfbd4dbfc80b5f95c54c5a7fd4d194be221f9d33d026e6", size = 2491443, upload-time = "2026-08-28T10:25:45.242Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d3/024208ec1079d273f1047468d1bdffbf38bb75b7b268090fd3a0301b9d9a/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b03af77d77e50edba2030fd5f7c352ff209314b09030a3cba7c14edf9a09a444", size = 2295200, upload-time = "2026-08-28T10:25:46.984Z" }, + { url = "https://files.pythonhosted.org/packages/6e/7c/7b210498f9f92e1cd7855f260fa69ef056881087b199ee20c208f0e4189a/kiwisolver-1.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:06a6917674de9e0fe3f66f5430787f59a9f2ddb64af9b714eaec547e29ef5c19", size = 70748, upload-time = "2026-08-28T10:25:48.444Z" }, + { url = "https://files.pythonhosted.org/packages/94/61/ef0daa157c8bb23672f7423e0d14c39db1dc6ef8ed47e6bc54c9c1bef3bf/kiwisolver-1.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:ad8b9671348d7c8716715652ae11f85ed0eb99e265a2df2ca490577d69860b2c", size = 68324, upload-time = "2026-08-28T10:25:49.81Z" }, + { url = "https://files.pythonhosted.org/packages/08/c1/88018321d976f53c421e379c43bc6993e70ce0c8a3ec5edc4bfe102257f6/kiwisolver-1.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b6ae6a0328f0bc035741820fdeecdcd67bf4694eee03972e843663107122f450", size = 62272, upload-time = "2026-08-28T10:25:51.02Z" }, + { url = "https://files.pythonhosted.org/packages/85/d2/712bc17ea4f1d216034928069d612defbc6c95a471c55a7203a39faecb1a/kiwisolver-1.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:886fc26012f0e8b5f69d1cfe6d711f6b11f194621539bf8e6bb1c25c5dc82724", size = 64481, upload-time = "2026-08-28T10:25:52.22Z" }, + { url = "https://files.pythonhosted.org/packages/1b/e6/6c5380d676f43b6d918033962ea5e72360ca69e5a404154bc496b598ffdb/kiwisolver-1.5.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:aefe930d113798330e9462f7874542977869c0613cba3262e2de3a8d5dee8f3a", size = 66260, upload-time = "2026-08-28T10:25:53.387Z" }, + { url = "https://files.pythonhosted.org/packages/92/6a/7087f5822cc8bb272679641404b1966a42504dac9ee74e2b33840475a0aa/kiwisolver-1.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5ca5aebae78a0bc13c1943af4af615d4966c5b650b05d5aa83b50e427196fee", size = 123876, upload-time = "2026-08-28T10:25:54.644Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4b/9f385087ca09ee5ab9c09c6832561a7d2f7c78d3e5661d511e669f70e439/kiwisolver-1.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1ed0f5e49d0ceff8b72190824d9e59c062fbbc02c231b853112c78474b3f5ec2", size = 66487, upload-time = "2026-08-28T10:25:55.899Z" }, + { url = "https://files.pythonhosted.org/packages/47/8b/40d33ffd2f378094ed462e9a9a0907e59d4de9845e65a59561272da350d4/kiwisolver-1.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:77a4c8187a5948d7f8795adb765a3c7b553d07d86d88e43038fc32fc1fb9a3f3", size = 64673, upload-time = "2026-08-28T10:25:57.048Z" }, + { url = "https://files.pythonhosted.org/packages/ab/43/86aacc027959108b4c66eeae8b73cedb057dfa6eb3a335d05ad65197081c/kiwisolver-1.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:74ad5c3dad54a4641b4c28cd15ded70899d04459c6c7aeacafea716be97cce6d", size = 1477992, upload-time = "2026-08-28T10:25:58.479Z" }, + { url = "https://files.pythonhosted.org/packages/ed/40/b1d0369048c79733a32c8abb0f2718532e6630641368e33a81384246e844/kiwisolver-1.5.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e46b23a2da695c364124817bc01d970effd5483147f8d66a6a7167e3f6b851", size = 1278821, upload-time = "2026-08-28T10:26:00.121Z" }, + { url = "https://files.pythonhosted.org/packages/61/a1/fa71c1792272ff9461432715ae60ffb7e11a4d7ac3bf68961b9cab6c60cf/kiwisolver-1.5.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75d9b1cf8258462dbdc1eeda718c96ea7f079324c09067f6daabfcf37712b7fe", size = 1296805, upload-time = "2026-08-28T10:26:01.868Z" }, + { url = "https://files.pythonhosted.org/packages/a3/49/3f0bd94af8e06ecc47eb834b195a06f05d48711ceb2352c56d6835160f0e/kiwisolver-1.5.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fca690b00c4c48f6c2a547b0160ed511357093a4e4c9b47e0fadf3128066d89", size = 1346109, upload-time = "2026-08-28T10:26:03.59Z" }, + { url = "https://files.pythonhosted.org/packages/a4/44/3afe6ef9cf06d61220953a8963e94eca978491be1d9547cb01d82a1efa08/kiwisolver-1.5.1-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:876bbfd276473d3daffe30e8c975df4ed9429967b41a6cb362dbb5155b6f13ad", size = 988252, upload-time = "2026-08-28T10:26:05.306Z" }, + { url = "https://files.pythonhosted.org/packages/e4/30/a12bd7a7285a211e1747c3eec77b8c614dbfcc1dad942f7611a1a6921ae5/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f942903fde7363d1d879057ec5de01310efda2597161784d752fa9953a01a71a", size = 2228846, upload-time = "2026-08-28T10:26:07.312Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6b/233e2958abf0dab7b18d07e52f286e02e519d7651bfbbe97af9347564109/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c90d3022d8a94778939cda8638c6c8da8fa757b8958dad7ec868ce29c87681b8", size = 2325583, upload-time = "2026-08-28T10:26:09.093Z" }, + { url = "https://files.pythonhosted.org/packages/42/8e/7673060a27b01405b580058510adef34687069d229800239f5e44682d4d0/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8a34616dc2521cc8dc1d7d081734da63539f021ac0450ce950908340c6e7aa2f", size = 1978221, upload-time = "2026-08-28T10:26:11.127Z" }, + { url = "https://files.pythonhosted.org/packages/3f/7b/1b882fc1a8b4a0bb8084e7d1d85004116d08c92c0705d31f2928dec607f2/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:8bf4df63592c2a66b4f8edc5df2544998c288aa02f96ce0acd880cd1de8c8127", size = 2491819, upload-time = "2026-08-28T10:26:13.348Z" }, + { url = "https://files.pythonhosted.org/packages/39/9c/426deb49e62c5f69464b64bbeca064d3b758a7506b8913d986ef34f4619c/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d09037ca068d784ebc4aec290ef952ca27ac15dd9c0b5801a88c6e1096b83e6b", size = 2295520, upload-time = "2026-08-28T10:26:15.042Z" }, + { url = "https://files.pythonhosted.org/packages/f5/22/deabbb3ad6d918d74b7831b2d8ae7151b09d21c87974e5ee8a456f58c94c/kiwisolver-1.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:dc23390afe9f4ef9ac3bcc72a03a56eebbde03f4c571a32cb38f859cff9a6524", size = 70758, upload-time = "2026-08-28T10:26:16.504Z" }, + { url = "https://files.pythonhosted.org/packages/b9/71/02fa5c2fd92068bb8952847e70ee6c5cb280e7febe11653d17812acc53dd/kiwisolver-1.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:186884a58486651e3c217b6acea0a53eaa9498fdd472057c46f2f0fb5c25aad5", size = 68329, upload-time = "2026-08-28T10:26:17.658Z" }, + { url = "https://files.pythonhosted.org/packages/dd/87/2d5dfad0daf17dcc18d98c48ed2332fc3f051cf599e60be6182a30dd4cf1/kiwisolver-1.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0324cd2567259b7a095f6cf18a52b0ffc6f3de9e69528ff1bc0e7a37bd43ff1a", size = 62337, upload-time = "2026-08-28T10:26:18.778Z" }, + { url = "https://files.pythonhosted.org/packages/08/c8/83e1624f15d6262b470dbcc80b09979fd4d5b2ea3ddfc6b6e3327e235726/kiwisolver-1.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:74ea337e0ec3f6f342a36a4f1b5cd94dd9affddcd28ba9aae2905af932ee8c6b", size = 64513, upload-time = "2026-08-28T10:26:19.909Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2d/827ec30eb07f528c08d8459ffb318ae91a56d793ee8acbea8b491f0ff906/kiwisolver-1.5.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ee9df1f0d77b9c6e94f4ac0fec533fbddd5ea3a327807f18d7b069ae019ded80", size = 66287, upload-time = "2026-08-28T10:26:21.078Z" }, + { url = "https://files.pythonhosted.org/packages/53/11/5c43a562529dad8def4b81e5e1877c612a7e0298105a5939b3b409d2079c/kiwisolver-1.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fc271a6f0a2126958f4090e5507b9da5848927dae331f8f763bd4aa642b3d2cd", size = 123940, upload-time = "2026-08-28T10:26:22.475Z" }, + { url = "https://files.pythonhosted.org/packages/64/db/9bd6c505c95128c258a55236bfbb3a7a3fb6023f863316b6d7d9f3c69052/kiwisolver-1.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9b3092d8992a1d69b7a59c3e39f35e1b9be327a17f68a7c35fc17329e337d6f2", size = 66493, upload-time = "2026-08-28T10:26:23.743Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f4/b3007a3ed5c9be73f81161140684cf7d9bdb9c4b632f5f484d2a1c713fb9/kiwisolver-1.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c2306e8bb53601979fcb3fa09cc65e031876d9ae01eff2fcbcd7a84ef94d5bc1", size = 64720, upload-time = "2026-08-28T10:26:24.95Z" }, + { url = "https://files.pythonhosted.org/packages/a1/13/08188f0cafa3a800403e4ff62b9aad4e7a17f9c4c7e080dc8f18c64794cf/kiwisolver-1.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:18a0cfb124546a4c2e6087c5f3029c7f44b37c85b142e0ced71f73a7599ac208", size = 1475867, upload-time = "2026-08-28T10:26:26.393Z" }, + { url = "https://files.pythonhosted.org/packages/8a/3e/053bdc3c9abdb8f2606225eda398adca25c0c91ab90add8222a69db65ee0/kiwisolver-1.5.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34ec467940442c9943016fb2d4c81d1ba84351eeca2f1a78f8bc87f1ba0d414c", size = 1282865, upload-time = "2026-08-28T10:26:28.118Z" }, + { url = "https://files.pythonhosted.org/packages/5e/64/a44c341b36b610588cc2f1e89b3cae072a3119aa8be578e90987cd640751/kiwisolver-1.5.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a83ee7107df13abe42a54a6654670eef9bb39425cf2e27f65e0007465e1286ab", size = 1300865, upload-time = "2026-08-28T10:26:30.125Z" }, + { url = "https://files.pythonhosted.org/packages/60/5e/7e7d716dca38c714478b741257a5b4a321d9932b8d851551a136dcaf3984/kiwisolver-1.5.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bebb89489b279b2f5661bbbb2abcc87bcd4a46607bb4a5c966f04f1db6b8df9a", size = 1348071, upload-time = "2026-08-28T10:26:31.829Z" }, + { url = "https://files.pythonhosted.org/packages/10/1a/2b98fdda8bf45b7be317e48ed12393d44334394d315c74b81f4a14c0e31b/kiwisolver-1.5.1-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:509735237ae0d849e8a843551d423d2500d2e0a9ac1611a145658b29c0fb9f85", size = 992191, upload-time = "2026-08-28T10:26:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/b6/55/d893f5ede0e50f9e3fcf01f6015f42ec7d9cd221e26772701fe4a98745f9/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:824c3d763a05ea9e9003610145186b0e9848c7584a5575c79bac5a8e7cd80bad", size = 2233854, upload-time = "2026-08-28T10:26:35.282Z" }, + { url = "https://files.pythonhosted.org/packages/b1/82/f85f6279555a6ee1639fef7bfe83adb037a03e11a6fc9eaa54b8d0380339/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1fff05e239575b1481b6ed1a782f6fad616efbf1f0b1f44e6e85c4dfe426e483", size = 2330621, upload-time = "2026-08-28T10:26:36.9Z" }, + { url = "https://files.pythonhosted.org/packages/56/31/e11aea078f66fc2fffcc179d38ca90d9da97652a241b64519169742ba46a/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0627b9bceb9c3cdcf12b8a18655eedfed2692b038df27423383c120d0b7dc2d6", size = 1982848, upload-time = "2026-08-28T10:26:39.01Z" }, + { url = "https://files.pythonhosted.org/packages/af/ea/2956b63bf5140ca46aa2c2818e6aa03e2d5754dd2fa41db1c6b28922940c/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8a708a47ade1fe19e8371d5da076bac0dd4b0a5a7985ad6c637f7f7e361b6baa", size = 2494850, upload-time = "2026-08-28T10:26:40.837Z" }, + { url = "https://files.pythonhosted.org/packages/11/d1/3829542258d8b3fc0898d221e7ef0e2c83eca0d348709bb8dbe54f3d4005/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:007a5553dfc4f4e8d184f588a0200e2cd4b63a59cc8796df3c39909e679dc7a0", size = 2298067, upload-time = "2026-08-28T10:26:42.803Z" }, + { url = "https://files.pythonhosted.org/packages/4e/0e/49522e1ab5788cbaf63a26fbd3b851f9028616828c961b8a31b35cb96df8/kiwisolver-1.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:f4167e87b397f273dc2356fcf1eaf50a6bac51e6105f45103ef7129c8efb0255", size = 72282, upload-time = "2026-08-28T10:26:44.268Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b6/22e7ca5315d363e6f81c9f37c9472e12e7b298731e77c0428e6a911a2c39/kiwisolver-1.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:5c490db2168a508088f59140dd392556a54b8bd1048fc6383c8baff13c359673", size = 69855, upload-time = "2026-08-28T10:26:45.725Z" }, + { url = "https://files.pythonhosted.org/packages/30/8c/03a9cfbe871964c8758a816eb03ac96c806da2795a9a7cd9bf9648bfb594/kiwisolver-1.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4d4ca09bf13cff792b1884f64b98ee6c2467930d632233be25c56b442d99f10e", size = 126289, upload-time = "2026-08-28T10:26:47.023Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e3/14ce3041ca79dff9c9d884ca00c7bf32374e76028a865a9ecd99b4f5a517/kiwisolver-1.5.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:44b8faef94f1857e77fa0238f3390ff1ac51d2ea20a487e2e452a59fd2b5f5ca", size = 67709, upload-time = "2026-08-28T10:26:48.268Z" }, + { url = "https://files.pythonhosted.org/packages/af/c4/45030471a66ec8ef042e9f96ffe1d522c9ab12da180186a0898966fc1385/kiwisolver-1.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2ae70bc59790d2af72a3f76f24b272403e135070340281108b447cb77ea70819", size = 65909, upload-time = "2026-08-28T10:26:49.523Z" }, + { url = "https://files.pythonhosted.org/packages/42/73/17dce073a6ae259bb32cf9d686c4079d2e538868bc45967462bf33df914a/kiwisolver-1.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:43844c1a7ad6d723d5b5b4c4fc7f5bd399c40e288120d16257c7c9e8765c6e85", size = 1584907, upload-time = "2026-08-28T10:26:50.933Z" }, + { url = "https://files.pythonhosted.org/packages/8c/84/ae3c75909f507283cbfcc7e916c7e822579ef962020b97e6882b27b4478f/kiwisolver-1.5.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22d5e5aaad6be121f2515765e3b1c444352cb8eb4c86510801db8f2e50757316", size = 1392474, upload-time = "2026-08-28T10:26:52.638Z" }, + { url = "https://files.pythonhosted.org/packages/34/31/8bcc83caad5bce8fa4577152389848bf6bc110e51e573a2b4e7c2aa34c89/kiwisolver-1.5.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3fa5855898f6d3d01b72ccd48a2d65cbdee301251603fefe34e2025bddba219c", size = 1405246, upload-time = "2026-08-28T10:26:54.248Z" }, + { url = "https://files.pythonhosted.org/packages/55/72/220345537d790cf4ae54f8acfff4b5cc2468e0702a384d651cf7a771c63e/kiwisolver-1.5.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d66a64dd5dec136040ec2ae94aa026a912ee60fdd45bc28d3db30037fd809e88", size = 1456099, upload-time = "2026-08-28T10:26:56.042Z" }, + { url = "https://files.pythonhosted.org/packages/cd/10/3725fd2398f66d18c34b4e0f81a8d03764cd4f4f089f58a527f0b4428086/kiwisolver-1.5.1-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:9e51c119992ea8820706871c30a4642ec76de20ae82f9b50b9a45517d8e9f810", size = 1073695, upload-time = "2026-08-28T10:26:57.658Z" }, + { url = "https://files.pythonhosted.org/packages/86/cb/28d6e09e66b93e4588b2e6b7d84d020ccefea09e2f4de788510a07efeab7/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:70ed9a45c7484d2b30cdacf60d220f494a1763b9fec1ad03285c6553fa0889f2", size = 2335355, upload-time = "2026-08-28T10:26:59.202Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b3/a0f31d5e4e40af7dc97c36b8a74fdd3a36cf3c8bbd098da9a23466ff6a94/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:98b208a7cc42c803445ef551d6753cc42a5ea13e9cab1ee66cd8b9cb70195330", size = 2426524, upload-time = "2026-08-28T10:27:01.181Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c9/728f63bd58c72cafdc79fc306abeeac7391bec03b757a48dadeb30906521/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c6834b92dd2428e2dd85ef3d85f723d3c12f20aaf43a2ddd4f944ca25d833408", size = 2063430, upload-time = "2026-08-28T10:27:03.06Z" }, + { url = "https://files.pythonhosted.org/packages/84/df/ce188b96f92f9a2c958231da140768918cba53c9713dc887b82f85462118/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:5d142e352eb13facc7dd047489aebdff6ba78576c239f1ea04931979caaf0567", size = 2597513, upload-time = "2026-08-28T10:27:05.072Z" }, + { url = "https://files.pythonhosted.org/packages/69/d6/76947c8203768968382e5bd74d9cc95654746703a61ea53015f2c74a2e06/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9b1c4900736e489a812c529100de4b8fb617d4db075e931e213c57424b83d9b", size = 2394488, upload-time = "2026-08-28T10:27:07.423Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d4/14b21e4eb203c4d15425e8b6a2c625a320b4a1f2f7557eead63ffc30ffb7/kiwisolver-1.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5978c3340f16a35c30f8ab2fa7bcf559973c55f1a5ef6970e1f621acf3c4db13", size = 75404, upload-time = "2026-08-28T10:27:08.892Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f5/53157899fc7f45f76421b77b99eb1639dd0f83f26ff9d76300c96bb4a3b0/kiwisolver-1.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ca307d6c259e5c98d3cb9ade55342b47a6839762caf2536f3d7b46ee660cc82e", size = 72946, upload-time = "2026-08-28T10:27:10.944Z" }, + { url = "https://files.pythonhosted.org/packages/75/62/f786c3a27f181fa339d851a77e266d208e776b9883cabc40a5b041a31b5a/kiwisolver-1.5.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:bb7c99f0673c03017a3ee01e54a5c2617a05468b11eabe513b0080e063ed95b1", size = 62420, upload-time = "2026-08-28T10:27:12.308Z" }, + { url = "https://files.pythonhosted.org/packages/02/cd/58a91ed25fbad0facdf503297b03768efab04bdf3141e5e3b49a34be7443/kiwisolver-1.5.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:0d8924877ce22e17326a99a418c3c82037da078df3c6a260b13eca677444e6e7", size = 64577, upload-time = "2026-08-28T10:27:13.502Z" }, + { url = "https://files.pythonhosted.org/packages/f6/5c/d501ef5a0958b226eac28306d24d5e5f114be0ace50e19cabae7b6b3b197/kiwisolver-1.5.1-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:534f02c1abb31ed6dbd3515545285c330b2f12d00fdb1fdb71658b9ca5a13a6a", size = 66284, upload-time = "2026-08-28T10:27:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a6/8fbecaf4fc18c02f31f05e47a84c010a80e3ec391ed2f0bdade1d62b5954/kiwisolver-1.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:cea20da04494e662b83c872683bf4ff2345206043d036315ed0e924b652e7294", size = 124031, upload-time = "2026-08-28T10:27:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c6/bf3090d2983b4204347cbdbe952116e7c3b2abf62b4e33e50167a13e75ee/kiwisolver-1.5.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:7fd82debf43c6acd0a94359d232f6bb516ee13f269a7993736a9ac9f988bb5d9", size = 66489, upload-time = "2026-08-28T10:27:17.506Z" }, + { url = "https://files.pythonhosted.org/packages/85/de/562dddef55fdd7c291da8626d6619e72b5fc0870e6ccca0e149a5731e7f3/kiwisolver-1.5.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:18170a77ddfecf40ec60d0928268dc95880c881864e015a8f34094ed18b9b9ad", size = 64806, upload-time = "2026-08-28T10:27:18.673Z" }, + { url = "https://files.pythonhosted.org/packages/89/b1/ba7b9c0164ce1cf62bf2872db63b8483289cf0f3110d6f9390eb09e409ed/kiwisolver-1.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ca7f6fe0f37ca978a1e5eb7a3a68e6413f417e78e838324947ffd420202b198b", size = 1482211, upload-time = "2026-08-28T10:27:20.039Z" }, + { url = "https://files.pythonhosted.org/packages/13/dc/34da54efb4976616d45c20aae32d70e89d6e7395ed908029154d1609ef22/kiwisolver-1.5.1-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b973887ff782cfd6b67c9904ad8ca542e0bc5e4961503408b423b5a688b4d38", size = 1283739, upload-time = "2026-08-28T10:27:21.82Z" }, + { url = "https://files.pythonhosted.org/packages/5a/3a/30ffb62bee646e266e98a1b5cd276d9c75b6116fbfcb87c1190838c1b6df/kiwisolver-1.5.1-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f76fc85bd054c806960f917ec0f329e24e436f1712267d90588e4c39890caa63", size = 1301681, upload-time = "2026-08-28T10:27:23.876Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8d/13c70be22a8506880b35fdc38dca36629613bc493405c79f4037f2cd2bb9/kiwisolver-1.5.1-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:828f75af2b0080c8a972e75f649ab46af008e92c6104a57a759157200b835b75", size = 1349159, upload-time = "2026-08-28T10:27:25.899Z" }, + { url = "https://files.pythonhosted.org/packages/82/0e/993972b8ec6767f47cd69818fb3a5ff14510557d29f7d1a839be7574fa1b/kiwisolver-1.5.1-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:431dc224a1a92a5c8f582d96e505196a3b5997a7271076678da2dfde67b77e9a", size = 997613, upload-time = "2026-08-28T10:27:27.507Z" }, + { url = "https://files.pythonhosted.org/packages/36/82/ca26eddd2eda2420dfc56693449c1f821f78b485da9cbde9904c03af3f93/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:61e9a64c7635095a6bfe483e2ff055d437c59bd45f3617a228b37277f0185d62", size = 2235109, upload-time = "2026-08-28T10:27:30.113Z" }, + { url = "https://files.pythonhosted.org/packages/c7/84/97d920881e10840b8d7c7185620298e3e4c88820b05514e3a15a258b08a6/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:3c24cd69455e1b00ddf770c13b6e2c33e07d6dc3f2d34add0bf9277c5c6bbd46", size = 2331207, upload-time = "2026-08-28T10:27:32.795Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ce/34d74b8f25acc58800f4c09268371e8d6159cf0f1206f1e4dc7835629b48/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:27add358abe374ebaa3b8763ef380bc99051b5a4b18d94878366a9e4f59efef0", size = 1986696, upload-time = "2026-08-28T10:27:34.628Z" }, + { url = "https://files.pythonhosted.org/packages/0e/01/f892644014612527aef7031d3306a2ffc60b3cb044f802c1561f8e5e14f3/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:255605693a483db7bd5c79f60437f7bf658f7f520d61aa42722e32257c941951", size = 2496400, upload-time = "2026-08-28T10:27:36.818Z" }, + { url = "https://files.pythonhosted.org/packages/32/6d/d8284e66e697026536e5f418b9cfe56567bffd3c775e3ecfbae373605854/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:7d38b0c279c3032e8c9cc013b405c6df8e1668dbf15465779aa7f15f61201812", size = 2302967, upload-time = "2026-08-28T10:27:38.803Z" }, + { url = "https://files.pythonhosted.org/packages/46/0a/69a355e27f32ba50d5b6369949b6a1702e122f5277c89bc76d452b81c1c4/kiwisolver-1.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:958254518717542d02d0688d0d20cbf771da5e415e6f49543f92481c850a4540", size = 72283, upload-time = "2026-08-28T10:27:40.491Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d8/7a95be90c33dcdd52204d4aa6384d731443225b887283bbd8b61e7931f6c/kiwisolver-1.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:da3275833be0edbaf4830fae08bae3dc7219f40ce0c37eaa6c25825957e06612", size = 69860, upload-time = "2026-08-28T10:27:41.835Z" }, + { url = "https://files.pythonhosted.org/packages/31/f8/9bc493e7f5707788ba7f621902c68f82dc3a7ba03c78fbd337b026cef1ed/kiwisolver-1.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:470d420f98d368d6f010633a20659b544c5fdfa5329e6b70219f2ef08fd4a7ef", size = 126336, upload-time = "2026-08-28T10:27:43.379Z" }, + { url = "https://files.pythonhosted.org/packages/d1/82/3aea86b3f99712db825e9ac5631bf99571e818a8b8961ff98cebd798413e/kiwisolver-1.5.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:83f78128fa28705fa85d01c59771c72fe81c11bd0e6155edbb9f818983a7d761", size = 67698, upload-time = "2026-08-28T10:27:44.613Z" }, + { url = "https://files.pythonhosted.org/packages/84/7d/8daafc5d2e7f9c47a4f78f8865d86d2a9cf399c2a85f86c44a993594410c/kiwisolver-1.5.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:9506e892bcc3b409831d363c6f53e5985e1c8d1f6f6b0256d00358684ff85378", size = 65945, upload-time = "2026-08-28T10:27:45.932Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c9/51dc974d9130da70a8c47a96160123443d387ffe1b6b833d6f91d9429339/kiwisolver-1.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cea90547bfd93807e0013a004dc76552be44fad3bc1cc2b38610a9e889ed098f", size = 1588030, upload-time = "2026-08-28T10:27:47.678Z" }, + { url = "https://files.pythonhosted.org/packages/e6/86/f3e1a730e7a995149d8d3ff9e313b6d8a17b2cf1d98a8eff139dc30463fb/kiwisolver-1.5.1-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8e4d953faaded9ec7ede36824e9814082d22d4c7b1eafbfa079ecba8cd0d076", size = 1390760, upload-time = "2026-08-28T10:27:49.676Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3b/8ba25a2b5a0d2375e046f1b72de5179513f0be95aba6e7b094c89303929f/kiwisolver-1.5.1-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e9c01d3dd7ceba4d1d436cc021d40d592466e40b9bc7f5d83dc4e98a5c9cd8c", size = 1403279, upload-time = "2026-08-28T10:27:51.242Z" }, + { url = "https://files.pythonhosted.org/packages/18/d0/278d5cb8be812740027d5ca0a7eda0c375488a88d6dce0fa60fcc2591ad2/kiwisolver-1.5.1-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37f801b5d7cc0e5a548921308e059fd2b057bb42972b591cfa3049f95423c4ed", size = 1454429, upload-time = "2026-08-28T10:27:53.213Z" }, + { url = "https://files.pythonhosted.org/packages/94/37/bcbab41063ec284c1d200efe5087cf087798c2f8916960aa8a20dd303290/kiwisolver-1.5.1-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:e68e151428b5384f766cd25739bf77c7e4a3dc93b5ded7a12118d9fbfdf78ab6", size = 1073225, upload-time = "2026-08-28T10:27:55.089Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c5/ab79dcdf5ae28909a51210ae0a1c579e97ff997b3466414f0d04c0994583/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:8f8fddb8e323bd6eee4e54e69a39243beab22689070f4c66b472c4cc88bb89d8", size = 2334335, upload-time = "2026-08-28T10:27:56.78Z" }, + { url = "https://files.pythonhosted.org/packages/8d/8e/71d047468a189041d9c93f3b76844b924f9793b188c44bd149fa258912da/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:3cc210010fd2f438a3ed430b45f1b501fd13a8618bf984dc2c5ce5b69b78752e", size = 2424982, upload-time = "2026-08-28T10:27:58.639Z" }, + { url = "https://files.pythonhosted.org/packages/7d/37/1347461bbea6d0e1f0580b94ef603b18e72c2be5f667fa1653867361a00b/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:b5664603a253efd3a75716d793d1d3a6a82723b61dc6db767b2460bbbeec4c0f", size = 2062857, upload-time = "2026-08-28T10:28:00.407Z" }, + { url = "https://files.pythonhosted.org/packages/1f/87/c7f0976c9cd0d127643351bc0c9929e0b8899d7f49d4ec238cd909e39c42/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a7b85b2cc6ea45e5f7e8c9a30bc9fabd47cda09106cbb4b967335c3e6c43b69d", size = 2596022, upload-time = "2026-08-28T10:28:02.183Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9a/59a6f6ae6f938c15076be2c21b6cedea973d71bb1349ec84fa485fab82cf/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ab620eb663952455271ac37f9aaad86b73c969c02f11f53cea405b38e96a4300", size = 2395634, upload-time = "2026-08-28T10:28:03.977Z" }, + { url = "https://files.pythonhosted.org/packages/e9/2d/8982c1fb7926da5bb7ed60318c3665b5c3f941447271ac982960a11b8637/kiwisolver-1.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:cb6fae641357ed2f6e533c0d3c6504a4a5703621a50c89459e46051d56b61140", size = 75379, upload-time = "2026-08-28T10:28:06.307Z" }, + { url = "https://files.pythonhosted.org/packages/07/78/ba7b6dfa1708b82b373ac056928a30c545d5c1a627df9839dcec3c6c1881/kiwisolver-1.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:b390aec180a7c054919c04898835e1c77bced23ea8383eb2c570213bf25d1a86", size = 73011, upload-time = "2026-08-28T10:28:07.578Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c4/1407df7512a5b36cc79840e01710dc575733c461b13ab866cae77eaf87f3/kiwisolver-1.5.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:482676e5bd48d70ac99d9fc78863469845421e01184fa83f1f9366dc49f7e974", size = 134002, upload-time = "2026-08-28T10:28:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/16/45/c37a21ad5c0ab581a93c55ad544721aaa1f0ae94edb29c6a678a23d013e6/kiwisolver-1.5.1-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:072bdb15a3c19a5b5dbc8f8fb1f4e1884bf4f3507eeb4cc6334401274d37a5c0", size = 194292, upload-time = "2026-08-28T10:28:11.06Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c1/69f00d627949580e43d57af0aa465df46868d7c29801c137a55374101294/kiwisolver-1.5.1-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:a5a00665d1a0e26763a7338d7e911d4598fbc1d50dd0d6b7919b7dc6c5d6569f", size = 73362, upload-time = "2026-08-28T10:28:12.449Z" }, + { url = "https://files.pythonhosted.org/packages/b5/1d/59ba570b1774e95e97fde3a0981b2e22118a7a495f73bf74cedc538566a0/kiwisolver-1.5.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:416ba7ff9f233b7036689bb5a3783537e838ad483f63558d2a800f75afe738b1", size = 59450, upload-time = "2026-08-28T10:28:20.383Z" }, + { url = "https://files.pythonhosted.org/packages/22/98/a6849f04dc18b5400e8b98affa2cd8fd86ed583085f036e57b32e571f4fa/kiwisolver-1.5.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8af9b142ad719ae3a911ebf616bc4b78b32bbab84d6a40d3ad2f129670509957", size = 57400, upload-time = "2026-08-28T10:28:21.632Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ce/a7dc71353dd06a4cbe02222773f52d4a28c81e5a452a75797f8ed113dc99/kiwisolver-1.5.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5daa1f19e097050b9c4d9a78fcc9263cb96c9dfae08037ddc1b7c4ad1889f2a2", size = 79891, upload-time = "2026-08-28T10:28:22.936Z" }, + { url = "https://files.pythonhosted.org/packages/10/b1/d61c61a84ff85d1a36a99df2c152b59ffedb1d356c598902aba44abcdb60/kiwisolver-1.5.1-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdaeeb6c350106df6bf9d873395973e5f066a9713200b72cd64f55d0a3eafab6", size = 77605, upload-time = "2026-08-28T10:28:24.322Z" }, + { url = "https://files.pythonhosted.org/packages/d3/52/5aef56f21a460a6e43ab3cdfc7697d59d7b87deb0ec97a0f7b91aa4a521b/kiwisolver-1.5.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:17851e5dad4484be0cbccbde3b15331deae036de9aebd45eed964487802b172f", size = 98465, upload-time = "2026-08-28T10:28:25.696Z" }, +] + [[package]] name = "librt" version = "0.15.0" @@ -439,6 +817,72 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/a6/800800bfed7b1fb10fc3f3d557785c3854e80d3f7a9800d784b176a1fc2d/librt-0.15.0-cp315-cp315t-win_arm64.whl", hash = "sha256:84d244b00604d17df3fc7736c327892d6bba66181254aa4087be807b6c342bdc", size = 110700, upload-time = "2026-08-07T10:49:15.499Z" }, ] +[[package]] +name = "matplotlib" +version = "3.10.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "contourpy", version = "1.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/1b/4be5be87d43d327a0cf4de1a56e86f7f84c89312452406cf122efe2839e6/matplotlib-3.10.9.tar.gz", hash = "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358", size = 34811233, upload-time = "2026-04-24T00:14:13.539Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/8c/290f021104741fea63769c31494f5324c0cd249bf536a65a4350767b1f22/matplotlib-3.10.9-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:68cfdcede415f7c8f5577b03303dd94526cdb6d11036cecdc205e08733b2d2bb", size = 8306860, upload-time = "2026-04-24T00:12:01.207Z" }, + { url = "https://files.pythonhosted.org/packages/51/18/325cd32ece1120d1da51cc4e4294c6580190699490183fc2fe8cb6d61ec5/matplotlib-3.10.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfca0129678bd56379db26c52b5d77ed7de314c047492fbdc763aa7501710cfb", size = 8199254, upload-time = "2026-04-24T00:12:04.239Z" }, + { url = "https://files.pythonhosted.org/packages/79/db/e28c1b83e3680740aa78925f5fb2ae4d16207207419ad75ea9fe604f8676/matplotlib-3.10.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e436d155fa8a3399dc62683f8f5d0e2e50d25d0144a73edd73f82eec8f4abfb", size = 8777092, upload-time = "2026-04-24T00:12:06.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/fa/3ce7adfe9ba101748f465211660d9c6374c876b671bdb8c2bb6d347e8b94/matplotlib-3.10.9-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56fc0bd271b00025c6edfdc7c2dcd247372c8e1544971d62e1dc7c17367e8bf9", size = 9595691, upload-time = "2026-04-24T00:12:09.706Z" }, + { url = "https://files.pythonhosted.org/packages/36/c4/6960a76686ed668f2c60f84e9799ba4c0d56abdb36b1577b60c1d061d1ec/matplotlib-3.10.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5a6104ed666402ba5106d7f36e0e0cdca4e8d7fa4d39708ca88019e2835a2eb", size = 9659771, upload-time = "2026-04-24T00:12:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0d/271aace3342157c64700c9ff4c59c7b392f3dbab393692e8db6fbe7ab96c/matplotlib-3.10.9-cp311-cp311-win_amd64.whl", hash = "sha256:d730e984eddf56974c3e72b6129c7ca462ac38dc624338f4b0b23eb23ecba00f", size = 8205112, upload-time = "2026-04-24T00:12:15.773Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ee/cb57ad4754f3e7b9174ce6ce66d9205fb827067e48a9f58ac09d7e7d6b77/matplotlib-3.10.9-cp311-cp311-win_arm64.whl", hash = "sha256:51bf0ddbdc598e060d46c16b5590708f81a1624cefbaaf62f6a81bf9285b8c80", size = 8132310, upload-time = "2026-04-24T00:12:18.645Z" }, + { url = "https://files.pythonhosted.org/packages/35/c6/5581e26c72233ebb2a2a6fed2d24fb7c66b4700120b813f51b0555acf0b6/matplotlib-3.10.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f0c3c28d9fbcc1fe7a03be236d73430cf6409c41fb2383a7ac52fe932b072cb1", size = 8319908, upload-time = "2026-04-24T00:12:21.323Z" }, + { url = "https://files.pythonhosted.org/packages/b7/18/4880dd762e40cd360c1bf06e890c5a97b997e91cb324602b1a19950ad5ce/matplotlib-3.10.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cb28c2bd769aa3e98322c6ab09854cbcc52ab69d2759d681bba3e327b2b320", size = 8216016, upload-time = "2026-04-24T00:12:23.4Z" }, + { url = "https://files.pythonhosted.org/packages/32/91/d024616abdba99e83120e07a20658976f6a343646710760c4a51df126029/matplotlib-3.10.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae20801130378b82d647ff5047c07316295b68dc054ca6b3c13519d0ea624285", size = 8789336, upload-time = "2026-04-24T00:12:26.096Z" }, + { url = "https://files.pythonhosted.org/packages/5c/04/030a2f61ef2158f5e4c259487a92ac877732499fb33d871585d89e03c42d/matplotlib-3.10.9-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c63ebcd8b4b169eb2f5c200552ae6b8be8999a005b6b507ed76fb8d7d674fe2", size = 9604602, upload-time = "2026-04-24T00:12:29.052Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c2/541e4d09d87bb6b5830fc28b4c887a9a8cf4e1c6cee698a8c05552ae2003/matplotlib-3.10.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d75d11c949914165976c621b2324f9ef162af7ebf4b057ddf95dd1dba7e5edcf", size = 9670966, upload-time = "2026-04-24T00:12:32.131Z" }, + { url = "https://files.pythonhosted.org/packages/04/a1/4571fc46e7702de8d0c2dc54ad1b2f8e29328dea3ee90831181f7353d93c/matplotlib-3.10.9-cp312-cp312-win_amd64.whl", hash = "sha256:d091f9d758b34aaaaa6331d13574bf01891d903b3dec59bfff458ef7551de5d6", size = 8217462, upload-time = "2026-04-24T00:12:35.226Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d0/2269edb12aa30c13c8bcc9382892e39943ce1d28aab4ec296e0381798e81/matplotlib-3.10.9-cp312-cp312-win_arm64.whl", hash = "sha256:10cc5ce06d10231c36f40e875f3c7e8050362a4ee8f0ee5d29a6b3277d57bb42", size = 8136688, upload-time = "2026-04-24T00:12:37.442Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d3/8d4f6afbecb49fc04e060a57c0fce39ea51cc163a6bd87303ccd698e4fa6/matplotlib-3.10.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b580440f1ff81a0e34122051a3dfabb7e4b7f9e380629929bde0eff9af72165f", size = 8320331, upload-time = "2026-04-24T00:12:39.688Z" }, + { url = "https://files.pythonhosted.org/packages/63/d9/9e14bc7564bf92d5ffa801ae5fac819ce74b925dfb55e3ebde61a3bbad3e/matplotlib-3.10.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b1b745c489cd1a77a0dc1120a05dc87af9798faebc913601feb8c73d89bf2d1e", size = 8216461, upload-time = "2026-04-24T00:12:42.494Z" }, + { url = "https://files.pythonhosted.org/packages/8a/17/4402d0d14ccf1dfc70932600b68097fbbf9c898a4871d2cbbe79c7801a32/matplotlib-3.10.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8f3bcac1ca5ed000a6f4337d47ba67dfddf37ed6a46c15fd7f014997f7bf865f", size = 8790091, upload-time = "2026-04-24T00:12:44.789Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0b/322aeec06dd9b91411f92028b37d447342770a24392aa4813e317064dad5/matplotlib-3.10.9-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a8d66a55def891c33147ba3ba9bfcabf0b526a43764c818acbb4525e5ed0838", size = 9605027, upload-time = "2026-04-24T00:12:47.583Z" }, + { url = "https://files.pythonhosted.org/packages/74/88/5f13482f55e7b00bcfc09838b093c2456e1379978d2a146844aae05350ad/matplotlib-3.10.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d843374407c4017a6403b59c6c81606773d136f3259d5b6da3131bc814542cc2", size = 9671269, upload-time = "2026-04-24T00:12:50.878Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/0840fd2f93da988ec660b8ad1984abe9f25d2aed22a5e394ff1c68c88307/matplotlib-3.10.9-cp313-cp313-win_amd64.whl", hash = "sha256:f4399f64b3e94cd500195490972ae1ee81170df1636fa15364d157d5bdd7b921", size = 8217588, upload-time = "2026-04-24T00:12:53.784Z" }, + { url = "https://files.pythonhosted.org/packages/47/b9/d706d06dd605c49b9f83a2aed8c13e3e5db70697d7a80b7e3d7915de6b17/matplotlib-3.10.9-cp313-cp313-win_arm64.whl", hash = "sha256:ba7b3b8ef09eab7df0e86e9ae086faa433efbfbdb46afcb3aa16aabf779469a8", size = 8136913, upload-time = "2026-04-24T00:12:56.501Z" }, + { url = "https://files.pythonhosted.org/packages/9b/45/6e32d96978264c8ca8c4b1010adb955a1a49cfaf314e212bbc8908f04a61/matplotlib-3.10.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:09218df8a93712bd6ea133e83a153c755448cf7868316c531cffcc43f69d1cc9", size = 8368019, upload-time = "2026-04-24T00:12:58.896Z" }, + { url = "https://files.pythonhosted.org/packages/86/0a/c8e3d3bba245f0f7fc424937f8ff7ef77291a36af3edb97ccd78aa93d84f/matplotlib-3.10.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:82368699727bfb7b0182e1aa13082e3c08e092fa1a25d3e1fd92405bff96f6d4", size = 8264645, upload-time = "2026-04-24T00:13:01.406Z" }, + { url = "https://files.pythonhosted.org/packages/3d/aa/5bf5a14fe4fed73a4209a155606f8096ff797aad89c6c35179026571133e/matplotlib-3.10.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3225f4e1edcb8c86c884ddf79ebe20ecd0a67d30188f279897554ccd8fded4dc", size = 8802194, upload-time = "2026-04-24T00:13:03.702Z" }, + { url = "https://files.pythonhosted.org/packages/dd/5e/b4be852d6bba6fd15893fadf91ff26ae49cb91aac789e95dde9d342e664f/matplotlib-3.10.9-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de2445a0c6690d21b7eb6ce071cebad6d40a2e9bdf10d039074a96ba19797b99", size = 9622684, upload-time = "2026-04-24T00:13:06.647Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/ed428c971139112ef730f62770654d609467346d09d4b62617e1afd68a5a/matplotlib-3.10.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b2b9516251cb89ff618d757daec0e2ed1bf21248013844a853d87ef85ab3081d", size = 9680790, upload-time = "2026-04-24T00:13:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/e7/09/052e884aaf2b985c63cb79f715f1d5b6a3eaa7de78f6a52b9dbc077d5b53/matplotlib-3.10.9-cp313-cp313t-win_amd64.whl", hash = "sha256:e9fae004b941b23ff2edcf1567a857ed77bafc8086ffa258190462328434faf8", size = 8287571, upload-time = "2026-04-24T00:13:13.087Z" }, + { url = "https://files.pythonhosted.org/packages/f4/38/ae27288e788c35a4250491422f3db7750366fc8c97d6f36fbdecfc1f5518/matplotlib-3.10.9-cp313-cp313t-win_arm64.whl", hash = "sha256:6b63d9c7c769b88ab81e10dc86e4e0607cf56817b9f9e6cf24b2a5f1693b8e38", size = 8188292, upload-time = "2026-04-24T00:13:15.546Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e6/3bd8afd04949f02eabc1c17115ea5255e19cacd4d06fc5abdde4eeb0052c/matplotlib-3.10.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:172db52c9e683f5d12eaf57f0f54834190e12581fe1cc2a19595a8f5acb4e77d", size = 8321276, upload-time = "2026-04-24T00:13:18.318Z" }, + { url = "https://files.pythonhosted.org/packages/41/86/86231232fff41c9f8e4a1a7d7a597d349a02527109c3af7d618366122139/matplotlib-3.10.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97e35e8d39ccc85859095e01a53847432ba9a53ddf7986f7a54a11b73d0e143f", size = 8218218, upload-time = "2026-04-24T00:13:20.974Z" }, + { url = "https://files.pythonhosted.org/packages/85/8f/becc9722cafc64f5d2eb0b7c1bf5f585271c618a45dbd8fabeb021f898b6/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aba1615dabe83188e19d4f75a253c6a08423e04c1425e64039f800050a69de6b", size = 9608145, upload-time = "2026-04-24T00:13:23.228Z" }, + { url = "https://files.pythonhosted.org/packages/32/5d/f7e914f7d9325abff4057cee62c0fa70263683189f774473cbfb534cd13b/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34cf8167e023ad956c15f36302911d5406bd99a9862c1a8499ea6f7c0e015dc2", size = 9885085, upload-time = "2026-04-24T00:13:25.849Z" }, + { url = "https://files.pythonhosted.org/packages/a5/fd/fa69f2221534e80cc5772ac2b7d222011a2acafc2ec7216d5dd174c864ae/matplotlib-3.10.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59476c6d29d612b8e9bb6ce8c5b631be6ba8f9e3a2421f22a02b192c7dd28716", size = 9672358, upload-time = "2026-04-24T00:13:28.906Z" }, + { url = "https://files.pythonhosted.org/packages/ab/1a/5a4f747a8b271cbb024946d2dd3c913ab5032ba430626f8c3528ada96b4b/matplotlib-3.10.9-cp314-cp314-win_amd64.whl", hash = "sha256:336b9acc64d309063126edcdaca00db9373af3c476bb94388fe9c5a53ad13e6f", size = 8349970, upload-time = "2026-04-24T00:13:31.904Z" }, + { url = "https://files.pythonhosted.org/packages/64/dc/95d60ecaefe30680a154b52ea96ab4b0dab547f1fd6aa12f5fb655e89cae/matplotlib-3.10.9-cp314-cp314-win_arm64.whl", hash = "sha256:2dc9477819ffd78ad12a20df1d9d6a6bd4fec6aaa9072681465fddca052f1456", size = 8272785, upload-time = "2026-04-24T00:13:34.511Z" }, + { url = "https://files.pythonhosted.org/packages/70/a0/005d68bc8b8418300ce6591f18586910a8526806e2ab663933d9f20a41e9/matplotlib-3.10.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:da4e09638420548f31c354032a6250e473c68e5a4e96899b4844cf39ddea23fe", size = 8367999, upload-time = "2026-04-24T00:13:36.962Z" }, + { url = "https://files.pythonhosted.org/packages/22/05/1236cc9290be70b2498af20ca348add76e3fffe7f67b477db5133a84f3ea/matplotlib-3.10.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:345f6f68ecc8da0ca56fad2ea08fde1a115eda530079eca185d50a7bc3e146c6", size = 8264543, upload-time = "2026-04-24T00:13:39.851Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c2/071f5a5ff6c5bd63aaaf2f45c811d9bf2ced94bde188d9e1a519e21d0cba/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4edcfbd8565339aa62f1cd4012f7180926fdbe71850f7b0d3c379c175cd6b66c", size = 9622800, upload-time = "2026-04-24T00:13:42.296Z" }, + { url = "https://files.pythonhosted.org/packages/95/57/da7d1f10a85624b9e7db68e069dd94e58dc41dbf9463c5921632ecbe3661/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6be157fe17fc37cb95ac1d7374cf717ce9259616edec911a78d9d26dae8522d4", size = 9888561, upload-time = "2026-04-24T00:13:45.026Z" }, + { url = "https://files.pythonhosted.org/packages/67/b2/ef8d6bb59b0edb6c16c968b70f548aa13b54348972def5aa6ac85df67145/matplotlib-3.10.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4e42042d54db34fda4e95a7bd3e5789c2a995d2dad3eb8850232ee534092fbbf", size = 9680884, upload-time = "2026-04-24T00:13:48.066Z" }, + { url = "https://files.pythonhosted.org/packages/61/1c/d21bfeb9931881ebe96bcfcff27c7ae4b160ae0ec291a714c42641a56d75/matplotlib-3.10.9-cp314-cp314t-win_amd64.whl", hash = "sha256:c27df8b3848f32a83d1767566595e43cfaa4460380974da06f4279a7ec143c39", size = 8432333, upload-time = "2026-04-24T00:13:51.008Z" }, + { url = "https://files.pythonhosted.org/packages/78/23/92493c3e6e1b635ccfff146f7b99e674808787915420373ac399283764c2/matplotlib-3.10.9-cp314-cp314t-win_arm64.whl", hash = "sha256:a49f1eadc84ca85fd72fa4e89e70e61bf86452df6f971af04b12c60761a0772c", size = 8324785, upload-time = "2026-04-24T00:13:53.633Z" }, + { url = "https://files.pythonhosted.org/packages/63/e2/9f66ca6a651a52abfe0d4964ce01439ed34f3f1e119de10ff3a07f403043/matplotlib-3.10.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:42fb814efabe95c06c1994d8ab5a8385f43a249e23badd3ba931d4308e5bca20", size = 8304420, upload-time = "2026-04-24T00:14:04.57Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e8/467c03568218792906aa87b5e7bb379b605e056ed0c74fe00c051786d925/matplotlib-3.10.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f76e640a5268850bfda54b5131b1b1941cc685e42c5fa98ed9f2d64038308cba", size = 8197981, upload-time = "2026-04-24T00:14:07.233Z" }, + { url = "https://files.pythonhosted.org/packages/6f/87/afead29192170917537934c6aff4b008c805fff7b1ccea0c79120d96beda/matplotlib-3.10.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3fc0364dfbe1d07f6d15c5ebd0c5bf89e126916e5a8667dd4a7a6e84c36653d4", size = 8774002, upload-time = "2026-04-24T00:14:09.816Z" }, +] + [[package]] name = "mypy" version = "2.3.1" @@ -508,6 +952,165 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] +[[package]] +name = "numpy" +version = "2.4.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.12'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" }, + { url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" }, + { url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" }, + { url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" }, + { url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" }, + { url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" }, + { url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" }, + { url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" }, + { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" }, + { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" }, + { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" }, + { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" }, + { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" }, + { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" }, + { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" }, + { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" }, + { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" }, + { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" }, + { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" }, + { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" }, + { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" }, + { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" }, + { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" }, + { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" }, + { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" }, + { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" }, + { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" }, + { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" }, + { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" }, + { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" }, + { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" }, + { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" }, + { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" }, + { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" }, + { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" }, + { url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" }, + { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" }, + { url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" }, + { url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" }, + { url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" }, + { url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" }, +] + +[[package]] +name = "numpy" +version = "2.5.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version >= '3.12' and python_full_version < '3.15'", +] +sdist = { url = "https://files.pythonhosted.org/packages/13/01/11703282db468b85f6f7b8c7f22d058de5970d5c7e60a3a8aaa313c3de36/numpy-2.5.3.tar.gz", hash = "sha256:df2d5874ff183595a4ba404edd04f6bd9b5505c1d7708573f6a6c17489a67563", size = 20791231, upload-time = "2026-09-06T16:27:47.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/50/8fdbb16af64895706a45f06a4068e29db732ec180f3c1375f14123359138/numpy-2.5.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb189f09db39283b26bfd061ec16189e14f71c6755207f72a0f7540867afe5b9", size = 16994982, upload-time = "2026-09-06T16:24:29.244Z" }, + { url = "https://files.pythonhosted.org/packages/60/39/789131c1188c078dcb3a1692e72e1e050c68b88ffe72c9ccaac9bcd7a9cd/numpy-2.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f59a878c33d6b88122d80d239bb3b845d58708750b0cb06a09aebb9b18ec696c", size = 12009327, upload-time = "2026-09-06T16:24:32.491Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/a312e95696e5f601914dd8b6dd844692ba61670807417e24b68e337b5c70/numpy-2.5.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a72f874bc9e10e4b8f80426fb49716d5141f64442a0c8418065093ec8017fbb0", size = 5445405, upload-time = "2026-09-06T16:24:35.071Z" }, + { url = "https://files.pythonhosted.org/packages/30/d0/5623a1707ed4fe16e3909fe3cf5ee3da004ae677ad23d83bbf3adf1a6faf/numpy-2.5.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:fc36dc566135b5eceec4cf89758fcb719266a019ef07dae1754ae7c9f617ef3e", size = 6783213, upload-time = "2026-09-06T16:24:37.253Z" }, + { url = "https://files.pythonhosted.org/packages/f1/32/84146fc020ad3c25f805f70ab60da46fe3c540a21369754a7e4369754b6f/numpy-2.5.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76c2c1e6bfa5c84adc6434dfbf013aa92096a7985221762c8f11fedfd20fff58", size = 15687872, upload-time = "2026-09-06T16:24:39.751Z" }, + { url = "https://files.pythonhosted.org/packages/65/af/aa78d1a88805456e212b65461354cd943197fb9acecc4c90fd12295123a3/numpy-2.5.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7e18c623bb5c95acb3b3328861272816ba199fb531921c5d6d0b675f1fde9e3", size = 16717410, upload-time = "2026-09-06T16:24:42.745Z" }, + { url = "https://files.pythonhosted.org/packages/3b/24/faa79d865e69a97ba17473b23a1b74094b2259c03e820c70297293b9ea49/numpy-2.5.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4f8929ee6c96bfbd7b4ed2032e0c03af86fe1826740ab61ddabf9072d06e57ff", size = 17040975, upload-time = "2026-09-06T16:24:45.961Z" }, + { url = "https://files.pythonhosted.org/packages/62/4a/8877e629445a7176297dffcaf9c485faa96a95d81728a62521ad55bd4c0f/numpy-2.5.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b5d93cf48f687479941d12b69c873ad2cc76bbd487f0091c2200636497f34034", size = 18476479, upload-time = "2026-09-06T16:24:49.35Z" }, + { url = "https://files.pythonhosted.org/packages/c8/db/35e1c2d38b04cbd5b731f9d71495e055e813197669d22b612f11748d2ff9/numpy-2.5.3-cp312-cp312-win32.whl", hash = "sha256:bf63afbe037eb5d2fe87fbcc7778e61da53ebaf21d938a4515aa73b62532a5d4", size = 6133378, upload-time = "2026-09-06T16:24:51.915Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/accf6d4f0c80c5d9ba9735d6b1550e444180599f34dec69ca01360f717ad/numpy-2.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:0a59a421a32580a009e8a1751345bf829631b990dc1794b80514ab722b435def", size = 12567828, upload-time = "2026-09-06T16:24:54.255Z" }, + { url = "https://files.pythonhosted.org/packages/22/43/1764aff32e4652526ae2f71fa8b3efd8d25c8a3d6926914454e47138ed1e/numpy-2.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:ccb32e0525d29e8b0572eb84c9a57af0e7a4e615726927506f55063c62414034", size = 10485432, upload-time = "2026-09-06T16:24:57.278Z" }, + { url = "https://files.pythonhosted.org/packages/79/e5/8fb89cd46d14e35699d13bf943a5f5f441ecee8667120a1f6105ab89e349/numpy-2.5.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:66a78fe4556c60aceda5916f9eacd638b18e9e681016ec302dcb4682d6d4d034", size = 16991061, upload-time = "2026-09-06T16:25:00.411Z" }, + { url = "https://files.pythonhosted.org/packages/2f/06/9dc9e48b5e5e941c8b10350c5ff2d721da42a20517d911d15544246775ff/numpy-2.5.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92f30e89b8ee0ecf363033576c422b2f58fed6a80bed0aa48dff6d14c654663e", size = 12003676, upload-time = "2026-09-06T16:25:03.475Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2a/98282aa5b8f58b1157d440bb6282eed47e3632a5de53a714fbab17e659fe/numpy-2.5.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f9a2353b37a1a9e78fd82b27ad7e2a32a2d036604d18f02b05e3136c62ca3b09", size = 5439695, upload-time = "2026-09-06T16:25:05.978Z" }, + { url = "https://files.pythonhosted.org/packages/a1/f9/b6533d777be9d6ffd29dc1be0867e563e6e8cc9a220ff1b716adc317f060/numpy-2.5.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:ccbc4665079665c3cf3bab4db9f6b095370cd6437d66be549b6c2a1fd19e1958", size = 6779395, upload-time = "2026-09-06T16:25:08.599Z" }, + { url = "https://files.pythonhosted.org/packages/73/85/735720d04ec197c5dcfacdfc9922667c7f1f5f496a279b7ba4d7c74c4cc7/numpy-2.5.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c76d5dde9f445058f83d0c02af00557a4db91de9a9a57c0df87d1535001d654b", size = 15681750, upload-time = "2026-09-06T16:25:11.173Z" }, + { url = "https://files.pythonhosted.org/packages/3a/1b/3b16a9bc514a440a7a0883684111dcb1ef1aee960af2ca95da8fc775f124/numpy-2.5.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5fa86b80fd24bcd1aff83ad23be44ea323de3f787be8f8b15d4a65621e25321", size = 16708577, upload-time = "2026-09-06T16:25:14.171Z" }, + { url = "https://files.pythonhosted.org/packages/69/c4/386f397831b07328b639c96c5b62719346cf4baf07c68d927239752b1534/numpy-2.5.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd4cb9ad3c7889b9b3fe0a9a9fb5d2ed26f9879bff2608d9f01aed147a20d231", size = 17042047, upload-time = "2026-09-06T16:25:17.582Z" }, + { url = "https://files.pythonhosted.org/packages/5f/3e/a700ecbf36e85ae8328fd3b0e12eeddc22ed6358a64cb2bd913e0d195d65/numpy-2.5.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1302b90c0e52281681b2975adfe8a860cb7b12216a27b4b0b4207c44bf7bccf0", size = 18465724, upload-time = "2026-09-06T16:25:20.949Z" }, + { url = "https://files.pythonhosted.org/packages/41/ee/38e785e88a4045f6ad1d1f2808dcdfafdca48c760260c0587bf171e29fc9/numpy-2.5.3-cp313-cp313-win32.whl", hash = "sha256:1c80eabb4035ecf4ca9cd49cde8a9fdd69a729e63e6474887d1523ade7aa277f", size = 6129003, upload-time = "2026-09-06T16:25:23.664Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ec/100f2b1794ede74a9b3d7ec6b9736927f56713414c1dfe19ab6c383494bf/numpy-2.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:71cad2b2a7451ab79d8f5e71b453485b6775963d5cf794179144a7463fe6e8ec", size = 12560965, upload-time = "2026-09-06T16:25:26.602Z" }, + { url = "https://files.pythonhosted.org/packages/80/b1/7dc825ca94c12acebbce4c37caa5e198695eb31424bc579679f32b1bb49d/numpy-2.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:8e4dd766076855b5ff7ea52fa5f07ce26286726e0f8bff446b7739d02e6ea204", size = 10482343, upload-time = "2026-09-06T16:25:29.772Z" }, + { url = "https://files.pythonhosted.org/packages/70/78/cf416f15dc29375a229d9dfebf8db6e313f291580b39fa1a568b6052bb07/numpy-2.5.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:350ba9783ce969cf9f7ce6e6a9a58e1a6e2a19ca025b7ee448c4db727706212a", size = 16998686, upload-time = "2026-09-06T16:25:33.171Z" }, + { url = "https://files.pythonhosted.org/packages/9e/59/abcc2d8def4fd60eec7d87f92d27c13448ffd9ab14339bcc63a0d7a2fdea/numpy-2.5.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:012e66aca395d795496446e52aeeb5866312a5d4d3f27da270e5a0b43f70dc5c", size = 12013862, upload-time = "2026-09-06T16:25:36.748Z" }, + { url = "https://files.pythonhosted.org/packages/94/75/4640d2d6e4b64a049e48425a82728a41ef4adb61332d2cba68055774878b/numpy-2.5.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:adc1ada2662f8a5f960b8a10d9986897e7499ef07e06d4cfe7197f8cce923c07", size = 5449793, upload-time = "2026-09-06T16:25:39.476Z" }, + { url = "https://files.pythonhosted.org/packages/96/cd/625b57ae33d4ca560f32cc0b47b4a5922146d9beb998ddf773900d440a73/numpy-2.5.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:54a115e5a73b8fc44f0cebef486365a1894b5c9760685d4558b72b7c3eb846e0", size = 6785176, upload-time = "2026-09-06T16:25:42.069Z" }, + { url = "https://files.pythonhosted.org/packages/9c/72/12918652e7912ef9751e8694c88820fcd1908e0618cb23f5f3caa6004b7b/numpy-2.5.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be5a8381859b6da607c84f4f7d6847725f1cf1853ef8a2c9e115b7d58bef47dc", size = 15703377, upload-time = "2026-09-06T16:25:45.135Z" }, + { url = "https://files.pythonhosted.org/packages/45/8f/9beacf79ca7c650688ad0baa80931adb988fe6e6e5d5903c23cc3dbd70eb/numpy-2.5.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b0521d0f4aebb6e06189451025fa17a913287b13c03d5fe05c017333b654ea5b", size = 16711928, upload-time = "2026-09-06T16:25:48.461Z" }, + { url = "https://files.pythonhosted.org/packages/09/8d/41d0a56e1ac4c87495c897a211b1368691b7237aadabec8b3b8f3a74d48f/numpy-2.5.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9deb49575e5b0b94ed72c8a64ec4d033381adc27e9060ae842971f697ba96104", size = 17059507, upload-time = "2026-09-06T16:25:51.873Z" }, + { url = "https://files.pythonhosted.org/packages/08/1e/0dfbc5cc251d54e2af790f254d24ec38637fa97ec7d5d11de7ffed787098/numpy-2.5.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b00eefbcf0f292945c4b4dec2ae845389ef5bcdcd596e6e4328051db5b5ba694", size = 18471002, upload-time = "2026-09-06T16:25:55.233Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2c/dfa40f6991f8185c8c30ffd023dfcbb11888e823cfab9557b920f3bb7bed/numpy-2.5.3-cp314-cp314-win32.whl", hash = "sha256:c2381f82999704f818e2c987a865050e285ec3621262c66d40f5a96c8f899f8e", size = 6180485, upload-time = "2026-09-06T16:25:58.157Z" }, + { url = "https://files.pythonhosted.org/packages/a4/73/d2c08231e4fde7e415501fd02c715d96e98599b2d8384445933944152984/numpy-2.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:2c25dfa72943e4336ddb6b0ee4277b47a0c85bede0807530ec68103bf58e2c10", size = 12698179, upload-time = "2026-09-06T16:26:00.789Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e9/dcdcc9b95cf5f49815055573aee1b11cfbf5299f38a180e437ded050810f/numpy-2.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:15aa985ac73a8db02db7663381aa109510449d3819d37206caed27b33a65a8a6", size = 10769383, upload-time = "2026-09-06T16:26:04.011Z" }, + { url = "https://files.pythonhosted.org/packages/49/c4/af8bc08a7ef4e1529a7c0cf24969accce316b783999802089a581ec99272/numpy-2.5.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ac7bb1c52d445bd4f8f7f97fefe6abc3a084dc4d63df50d79b17fa2b78e89297", size = 12132668, upload-time = "2026-09-06T16:26:07.138Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ae/0f15eb56d4ec5e13c1f7ff04ff407f997d1acbadb45d3e1f2e2645a8f43c/numpy-2.5.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e6ab667ba76450084eb64013762c438ea76d9d29cc676dcd6c2e9892ba37f841", size = 5568580, upload-time = "2026-09-06T16:26:09.828Z" }, + { url = "https://files.pythonhosted.org/packages/23/fb/c72a8f25d4b6e96c354e7ab45ace3b27dc11e5d6a13b6c7d0cd6b08bf112/numpy-2.5.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:f7fabeb6cea87d65f3b926de33d03fb016cfdc29314c90974383b5582ae72891", size = 6882634, upload-time = "2026-09-06T16:26:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/07/a9/968c90ed2ab15060c338e8137f1215b5a60756ae07328e0a60d1c6734df4/numpy-2.5.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fb6f8fb9ff0b3a69f52c66ce397b0246583e9f28616231b0e32ca49259a5fa6", size = 15748923, upload-time = "2026-09-06T16:26:15.092Z" }, + { url = "https://files.pythonhosted.org/packages/59/08/9df04103947b95e3b6b1f2ed1a70521f325647a31b82da6a2aae3a485508/numpy-2.5.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93e1f5447e2b1e479d7bd74701e84746b86450cff1fc368b132d195e2b8f8211", size = 16746748, upload-time = "2026-09-06T16:26:18.43Z" }, + { url = "https://files.pythonhosted.org/packages/41/a0/14c8d5fe5b53a334aabb653deb391c0fef49558f491880ea300ed6785224/numpy-2.5.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c00abe94c1a69d75d827dcf1c025b25c8a45d230b3bcd77a9020883a1b047653", size = 17111561, upload-time = "2026-09-06T16:26:22.113Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a6/d7e96e42f01522e154c32489640f16dfc4f6181d165d05fc3bec8c2c4999/numpy-2.5.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:536f963710a4e63934d80ac0dc4f478804a83e9a84b6828018f25d09953ada33", size = 18513945, upload-time = "2026-09-06T16:26:25.401Z" }, + { url = "https://files.pythonhosted.org/packages/25/39/3453afb7119d0449ef11c886874120ff180e2c337760e0e2d88f70f1a945/numpy-2.5.3-cp314-cp314t-win32.whl", hash = "sha256:4c8a6d2ebce6305fd82fbefca827775437147052a976ee7c94b36a0c1b52ac6c", size = 6335421, upload-time = "2026-09-06T16:26:28.175Z" }, + { url = "https://files.pythonhosted.org/packages/99/01/22815d2b19a1a746b1d45205cffebb3fe511a18acb75fba6c88491fc9894/numpy-2.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:9a37475425b431b4d060f23b4f52cd2f3aef6bc7c654bd760adf0040eec9d435", size = 12896420, upload-time = "2026-09-06T16:26:31.265Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ee/a7cbba67eeaff038dc29ca8b98a88396c8b0cc9c89d4924f4a27a5c9150b/numpy-2.5.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2d8240cb4c16fd831074aa2b2cf9fc54664d826341d61c372245b96a74a49a9a", size = 10857177, upload-time = "2026-09-06T16:26:34.167Z" }, + { url = "https://files.pythonhosted.org/packages/45/56/78194492883ff5eec90423fe56a3a44b154da047d88a6307f629713c584f/numpy-2.5.3-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a6391fafaba97500887132cd582abc6e19452b1ac775a47caa7b24490e152058", size = 16996531, upload-time = "2026-09-06T16:26:37.287Z" }, + { url = "https://files.pythonhosted.org/packages/11/39/dd55c0af90bbab564b09ae3b0aa60ec5c02b900fa4f1ba23440525c8b32d/numpy-2.5.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:09d5a423c71ad5feb5625844ad58050e35df43871004b52ac9c0ad44a56775be", size = 12012569, upload-time = "2026-09-06T16:26:40.707Z" }, + { url = "https://files.pythonhosted.org/packages/b6/51/04f67d32e4862b281b1cb84ceeaed3421189a84fb6fb51a391cd6d5009f7/numpy-2.5.3-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:f9579f383d1bf9df80081e72760e84960a7fd4f88cf0c9e535a8597c9bb646f5", size = 5448498, upload-time = "2026-09-06T16:26:43.435Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c9/25b4dc0dd1344ec26c7319e84fd4e9809d2b5628f4e12decd618036e5178/numpy-2.5.3-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:86bff898a431c0fb71f7610b75726e75a54d47b37edc9d537f48de63bb3c0b90", size = 6783026, upload-time = "2026-09-06T16:26:46.374Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c7/29285be1e5232a6e7ee3268a33c85843f5a8ee93350c6465cddd66ebbf76/numpy-2.5.3-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f3ed25271581281f2fccb1adcedfcde4c07362eec69189b50baf6f90e3ae159", size = 15697322, upload-time = "2026-09-06T16:26:49.415Z" }, + { url = "https://files.pythonhosted.org/packages/55/49/bbad5335fb4996a16881f853ff3e0ba582f01720e55c89b1c06b8fc42a90/numpy-2.5.3-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffdc76bfcae6b255dff75202c5e7feaf95b40246bc0a17944facc1fecf9f79ab", size = 16708995, upload-time = "2026-09-06T16:26:53.127Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e9/1df35483760b04a65ea44669f89dc64f30e5aca098b48ceb8b1310b0e0fe/numpy-2.5.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:116f96cadd935c6122e9228d676fe7ede19e741f5c8bb1c3cddbe0c51ccebea2", size = 17052508, upload-time = "2026-09-06T16:26:56.464Z" }, + { url = "https://files.pythonhosted.org/packages/b8/99/66e54da8265cc8be8a7382bf96edce17aaa2837d6f484432025932a3caa5/numpy-2.5.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:09ffa5d903faeaa5c4dd05009cf81c8bab9f2cb37c548b8d39b65b4cfa7c97f7", size = 18468224, upload-time = "2026-09-06T16:26:59.966Z" }, + { url = "https://files.pythonhosted.org/packages/01/bc/b5e90a91c115168d793dfd2ad9c69c438c2fe7a13a437e770bc5b078e732/numpy-2.5.3-cp315-cp315-win32.whl", hash = "sha256:e01c918ac3d48e18a927cf7b14a26a3e29ff2bdf2eacb976da0aecd6a43ed034", size = 6179919, upload-time = "2026-09-06T16:27:03.166Z" }, + { url = "https://files.pythonhosted.org/packages/37/ea/780748fd3985109075514ef8fc64cd25f943e40dde13a6d59141eb268fc8/numpy-2.5.3-cp315-cp315-win_amd64.whl", hash = "sha256:e931e4f499e0dc7ef29d269a8e5b35dd722e5d14be07df6240166ea7c6532fae", size = 12697656, upload-time = "2026-09-06T16:27:06.153Z" }, + { url = "https://files.pythonhosted.org/packages/b3/16/407be69a2a87c8cab64d95975a8977a426a29e138f07e276ec258f0fe4e5/numpy-2.5.3-cp315-cp315-win_arm64.whl", hash = "sha256:26e15e4aecd8617dfbaecb37d223e365d7b39411fba20454be2670a96aa74cb5", size = 10767601, upload-time = "2026-09-06T16:27:09.297Z" }, + { url = "https://files.pythonhosted.org/packages/44/bf/a97ffb01e41d50a32a9177aef942a4d0e389a3daf451d04e5f38ef6afb87/numpy-2.5.3-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:6cef4bb1706dfec49243c05d921eefb4e190d41e2528b30d8035ea1f36b4c24a", size = 17090092, upload-time = "2026-09-06T16:27:12.907Z" }, + { url = "https://files.pythonhosted.org/packages/d1/24/136c02f2c2af9a067a84d0c3aa10c99012c0476fa5066732fa4a4202557d/numpy-2.5.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:d1c89973648c85069c5046ad460f7b8a00218b29a2e42359ac8cc63e9ab94832", size = 12129429, upload-time = "2026-09-06T16:27:16.089Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6c/b47582d6597789bf946d5efbeb6b9e56fd8bcbd5efc6fbf51dbe1ea31eb3/numpy-2.5.3-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:214045a5bf00113a146ab9ee9730c44501af6723cdf1f6830932f7b5ef2e7af0", size = 5565452, upload-time = "2026-09-06T16:27:19.868Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/ef3cc6da73774202d4deae16bb321fd8298a4e0561e3539f8c4be237d916/numpy-2.5.3-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:8617bbfae4486cf99c9f899966699428d19da931d06ca94ad3da986c76e15997", size = 6876736, upload-time = "2026-09-06T16:27:22.232Z" }, + { url = "https://files.pythonhosted.org/packages/9e/24/e3813329498596cb842703dcacac1741612ed9fb9c4e6a3e0c7e2ebbc597/numpy-2.5.3-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:595d020938c84e320bcf40ad71089e108eac0d377cd018e14a8c094f39e98d85", size = 15745777, upload-time = "2026-09-06T16:27:25.181Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9e/4e7a07fd0776dc2210cdacf2010be8665194d094defc10c419d7dea794cc/numpy-2.5.3-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f24021b9f22bc6301c37b196974a92c1c18dccedb6fef3dd252e95f2d6adbe4", size = 16746949, upload-time = "2026-09-06T16:27:28.576Z" }, + { url = "https://files.pythonhosted.org/packages/91/db/01674c0e20335057813a00c2ebd546ed25bff9ed7914f9bced00f8c55d94/numpy-2.5.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:71b39d9f935b6ec0f8753e3e2afb51e3efba6f2e05b68b32a40754d24bcd4a3c", size = 17108994, upload-time = "2026-09-06T16:27:31.946Z" }, + { url = "https://files.pythonhosted.org/packages/45/7a/584c5e71f8d378e57cac0b033891ed65c683ef90573ba4854e8c28203db0/numpy-2.5.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:6b05c171afb3aa07adbd20abc00aea86fe375beb0fdb9ef780ec5b7f63bab1c0", size = 18512266, upload-time = "2026-09-06T16:27:35.196Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d2/4e1014173aa3c55e6a756e0e567290743a6ab33a288460374d7ef6bcd239/numpy-2.5.3-cp315-cp315t-win32.whl", hash = "sha256:f54660b0eb6b0b9f36e7fe1cdfdff472028dd0d14acd9b9b65098efbad059469", size = 6330292, upload-time = "2026-09-06T16:27:38.149Z" }, + { url = "https://files.pythonhosted.org/packages/6c/b0/ff5658a58199b7bcaad87bf260eef6713d9d42cca4e028f935b4fc5fbac6/numpy-2.5.3-cp315-cp315t-win_amd64.whl", hash = "sha256:1aad64d99730d013cfc6debafed22783b4fc5a7f4b8bc744d2d8cf7dcc880551", size = 12884918, upload-time = "2026-09-06T16:27:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/fb/0b/b12a2df5d1b774bd9007a6fdff9381145b6223d37f11afc9c37ab0efd9a1/numpy-2.5.3-cp315-cp315t-win_arm64.whl", hash = "sha256:befa1ae5bd6030b3f512b43ff3fa5290bbed6b84411a44244b14adf835f5b89d", size = 10850807, upload-time = "2026-09-06T16:27:43.868Z" }, +] + [[package]] name = "packaging" version = "26.3" @@ -517,6 +1120,61 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, ] +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, + { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" }, + { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" }, + { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, + { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, + { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, + { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, + { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, + { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, + { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, + { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, + { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, + { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, + { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, +] + [[package]] name = "pathspec" version = "1.1.1" @@ -526,6 +1184,91 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, ] +[[package]] +name = "pillow" +version = "12.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756", size = 5392415, upload-time = "2026-07-01T11:53:47.162Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6", size = 4785266, upload-time = "2026-07-01T11:53:49.079Z" }, + { url = "https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd", size = 6263814, upload-time = "2026-07-01T11:53:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd", size = 6934408, upload-time = "2026-07-01T11:53:53.487Z" }, + { url = "https://files.pythonhosted.org/packages/a3/48/9c58b685e69d49c31af6c8eb9012055fab7e665785165c84796e2c73ce72/pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c", size = 6337160, upload-time = "2026-07-01T11:53:55.457Z" }, + { url = "https://files.pythonhosted.org/packages/ff/fa/dc2a5c0ba6df93f67c31d34b808b7ce440b40cdbf96f0b81cde1d1e6fa93/pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5", size = 7045172, upload-time = "2026-07-01T11:53:57.736Z" }, + { url = "https://files.pythonhosted.org/packages/86/a5/444817a4d4c4c2417df00513086ca196f388d8f9ef40c2e4ccd1ad1af54b/pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b", size = 6472232, upload-time = "2026-07-01T11:53:59.767Z" }, + { url = "https://files.pythonhosted.org/packages/63/c6/4bad1b18d132a50b27e1365e1ab163616f7a5bb56d330f66f9d1d9d4f9d4/pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a", size = 7233653, upload-time = "2026-07-01T11:54:02.066Z" }, + { url = "https://files.pythonhosted.org/packages/fd/16/00f91ab7760dc842f5aad55217e80fc4a7067a0604535249bc8a2d6d9870/pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26", size = 2568195, upload-time = "2026-07-01T11:54:04.622Z" }, + { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" }, + { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" }, + { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" }, + { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" }, + { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" }, + { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" }, + { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" }, + { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" }, + { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" }, + { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" }, + { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" }, + { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" }, + { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" }, + { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" }, + { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" }, + { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" }, + { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" }, + { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" }, + { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" }, + { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" }, + { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" }, + { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" }, + { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" }, + { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" }, + { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" }, + { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" }, + { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" }, + { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" }, + { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" }, + { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" }, + { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" }, + { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" }, + { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" }, + { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" }, + { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" }, + { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/75/18/2e8b40223153ccbc60df07f9e8928dc0c76202aa4e55ae9f53962b6510d6/pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468", size = 5302510, upload-time = "2026-07-01T11:56:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/46/3e/51fabf59d5ab801ceab709453d3ab6b180083496579549de4c45ced6528a/pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94", size = 4736058, upload-time = "2026-07-01T11:56:28.041Z" }, + { url = "https://files.pythonhosted.org/packages/bf/20/22fe9384b7949e25fb1293bcfc84fb82590ff4ea6b37c95b24d26d793d86/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e", size = 5237776, upload-time = "2026-07-01T11:56:30.263Z" }, + { url = "https://files.pythonhosted.org/packages/08/14/f6ba68107680ffa74b39985f3f30884e41318fbc4250caa423c79b4788bb/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3", size = 5860358, upload-time = "2026-07-01T11:56:32.68Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -544,6 +1287,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, ] +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + [[package]] name = "pytest" version = "9.1.1" @@ -560,6 +1312,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pytz" +version = "2026.3.post1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/48/fb042503b6ca6cd271261dc559fd6432f7d8c713153e9ec5c591af4dfc1c/pytz-2026.3.post1.tar.gz", hash = "sha256:2211d3fcf9a797d3405cac96ac7f61d80e6a644f72a3309607282fe8a2010c5d", size = 319745, upload-time = "2026-07-25T15:12:07.385Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/7b/39c34ca613b0b198cb866466651b26b045e2009864c5183c979a3b83f383/pytz-2026.3.post1-py2.py3-none-any.whl", hash = "sha256:dd95840dd199baea12d9cc096a1d452caa6596a1c1e4b5f3dbd1541855d5e815", size = 508283, upload-time = "2026-07-25T15:12:05.782Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -792,6 +1565,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fc/07/d781f8f8e1ac24bef9f3269cf62ffb1407ca24c3a8f12e5e22874f90528c/ruff-0.16.6-py3-none-win_arm64.whl", hash = "sha256:7a976c79b958f94e50a022a19f0f8c87387448020935ec14fc74331bd0a7f2c5", size = 10412850, upload-time = "2026-09-03T16:57:26.416Z" }, ] +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + [[package]] name = "types-pyyaml" version = "6.0.12.20260815" @@ -822,6 +1604,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] +[[package]] +name = "tzdata" +version = "2026.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/31/3d74fa778a63b98b7374323befcc0be5ab3bd94afd4096a0124e7379152c/tzdata-2026.4.tar.gz", hash = "sha256:f1b8bd365d8d210c55353f4d7f8d6d8561c0ba50d704b700d195a9424bba0d79", size = 199350, upload-time = "2026-09-12T12:56:03.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/bc/8737e8d54cf51106118039b83f485a4783112fab49ea9d044b234978a46e/tzdata-2026.4-py2.py3-none-any.whl", hash = "sha256:c2169a8b0a7a5e9674da5a135ccdfb2b3e671b333ed9fed17b41f73c34476e81", size = 347494, upload-time = "2026-09-12T12:56:01.67Z" }, +] + [[package]] name = "urllib3" version = "2.7.0" From fbe9af6c3d5e368a068e3321435fa64150fd5a42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=E5=B0=8F=E4=BA=8C?= <xxg1413@gmail.com> Date: Sun, 13 Sep 2026 13:52:38 +0800 Subject: [PATCH 04/10] docs: fix full documentation review findings across the repository MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Licensing and entry points: - CONTRIBUTING permissions now match the dual MIT/CC-BY LICENSE - README_cn hero rewritten for the challenge-platform narrative (TASK.md contracts, verify.py claim codes, paths badge routes) - llms.txt: fix four template paths, add the three DA courses, refresh tagline and Radar description Indexes and governance docs: - README/CONTRIBUTING/AGENTS: complete the CI check list (verify_courses, verify_paths, --target both), add courses/, paths/, radar.json to the structure diagram, dependency-aware course contract - CHANGELOG: record post-0.1.0 0.0.4 phase A under [Unreleased] - CONSUMING: document radar.json as the third pinned export - repo-plan-0.0.4: mark FP-334 done (v0.1.0) Courses and examples: - mcp-server-in-python: fix stale examples/mcp-server verify path in TASK pair; expect-failure now requires all named failures; REVIEW wording matches the actual scenario layout - da-report: remove the unused pandas gate (verify.py, requirements, COURSE pair) — the course is stdlib-only - agent-rules L01 pair: add the missing verifier command - product-slug: fix punctuation-before-collapse ordering so removed punctuation cannot create duplicate separators; regression test added - async-fetcher: solution uses asyncio.TaskGroup; TASK pair states the total-attempts retry semantics - structured-pipeline: solution actually iterates in batch_size batches - pydantic-validation: honest stdlib-by-design note (EN+ZH) and accurate verify message - align DA badge ids to the course- prefix convention Bilingual parity and paths: - templates: add CODE_REVIEW/IMPLEMENTATION_PLAN CN pairs and link them from both indexes; fix EVAL_RECORD/AGENT_RULES CN drift - guides: fix async-patterns_cn example, modern-typing_cn dropped clause, migrate guide EN em-dash sentence - playbooks: CN footers deep-link the same resources anchors as EN - paths: add README pair, resolve undefined C1-C4 labels to course slugs, link the index from the main READMEs Hygiene: - drop committed course .venv/__pycache__ artifacts, ignore .pytest_cache, exempt starter scaffolding imports from ruff F401 Validation: pytest 136 passed; ruff clean; catalog/radar/README/manifest checks current; 5 examples, 8 courses, 2 paths verified. --- .gitignore | 3 ++ AGENTS.md | 18 ++++--- CHANGELOG.md | 25 +++++++++ CONTRIBUTING.md | 15 ++++-- README.md | 14 +++-- README_cn.md | 20 ++++--- content-manifest.json | 46 ++++++++-------- courses/agent-rules-single-source/REVIEW.md | 2 +- .../agent-rules-single-source/lessons/L01.md | 4 ++ .../lessons/L01_cn.md | 4 ++ courses/da-eda/COURSE.md | 2 +- courses/da-eda/COURSE_cn.md | 2 +- courses/da-report/COURSE.md | 5 +- courses/da-report/COURSE_cn.md | 5 +- courses/da-report/requirements.txt | 1 - courses/da-report/verify.py | 29 ++-------- courses/da-visualization/COURSE.md | 2 +- courses/da-visualization/COURSE_cn.md | 2 +- courses/mcp-server-in-python/REVIEW.md | 2 +- courses/mcp-server-in-python/TASK.md | 2 +- courses/mcp-server-in-python/TASK_cn.md | 2 +- courses/mcp-server-in-python/verify.py | 6 ++- courses/verifying-ai-generated-code/REVIEW.md | 2 +- docs/CONSUMING.md | 9 ++-- docs/repo-plan-0.0.4.md | 4 +- examples/async-fetcher/TASK.md | 2 +- examples/async-fetcher/TASK_cn.md | 2 +- examples/async-fetcher/solution/fetcher.py | 8 ++- .../product-slug/solution/product_slug.py | 2 +- .../product-slug/tests/test_product_slug.py | 4 ++ examples/product-slug/verify.py | 3 +- examples/pydantic-validation/README.md | 5 ++ examples/pydantic-validation/README_cn.md | 4 ++ examples/pydantic-validation/verify.py | 2 +- .../structured-pipeline/solution/pipeline.py | 54 ++++++++++--------- guides/mcp/migrate-2026-07-28.md | 2 +- .../python-engineering/async-patterns_cn.md | 2 +- guides/python-engineering/modern-typing_cn.md | 2 +- llms.txt | 17 +++--- paths/README.md | 28 ++++++++++ paths/README_cn.md | 24 +++++++++ paths/foundation/modules/route-challenge.md | 9 ++-- .../foundation/modules/route-challenge_cn.md | 6 ++- playbooks/add-an-api/README_cn.md | 2 +- playbooks/fix-a-bug/README_cn.md | 2 +- .../integrate-an-external-api/README_cn.md | 2 +- .../manage-database-migrations/README_cn.md | 2 +- .../setup-structured-logging/README_cn.md | 2 +- playbooks/ship-a-release/README_cn.md | 2 +- playbooks/upgrade-dependencies/README_cn.md | 2 +- playbooks/write-llm-evals/README_cn.md | 2 +- pyproject.toml | 5 ++ templates/AGENT_RULES.example_cn.md | 2 +- templates/CODE_REVIEW_cn.md | 8 +++ templates/EVAL_RECORD_cn.md | 2 +- templates/IMPLEMENTATION_PLAN_cn.md | 13 +++++ templates/README.md | 4 +- templates/README_cn.md | 4 +- 58 files changed, 301 insertions(+), 155 deletions(-) delete mode 100644 courses/da-report/requirements.txt create mode 100644 paths/README.md create mode 100644 paths/README_cn.md create mode 100644 templates/CODE_REVIEW_cn.md create mode 100644 templates/IMPLEMENTATION_PLAN_cn.md diff --git a/.gitignore b/.gitignore index 56cdac4..88de114 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,6 @@ courses/*/scenario/*/report.json.tmp courses/*/out/ paths/*/capstone/out/ courses/*/scenario/*/results.json + +# Test caches +.pytest_cache/ diff --git a/AGENTS.md b/AGENTS.md index 0041f97..939f85b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,12 +35,17 @@ reviewed catalog data, and stable public JSON contracts. an AI coding agent from the files themselves: `COURSE.md` (metadata plus the teaching contract: audience, prerequisites, exact tool and version, lesson order, teaching-style rules, when to stop, how to use `verify.py`, - and what the course does not cover), `lessons/L01.md` with `L01_cn.md` + and what the course does not cover) with `id: course-<slug>` and `badge` + frontmatter, `lessons/L01.md` with `L01_cn.md` pairs (objective, exercise, checkpoint, expected evidence), `scenario/` data files for each skin, `TASK.md`/`TASK_cn.md` (task contract), - `starter/` and `solution/` runnable pairs, stdlib-only `verify.py` that - fails on `starter` and passes on `solution`, and `REVIEW.md` recording the - maintainer run-through (date, tool, version, observed agent deviations). + `starter/` and `solution/` runnable pairs, a self-contained `verify.py` + that fails on `starter` and passes on `solution`, exposes deterministic + claim codes through `verify.py progress` (five checkpoints), and runs on + the standard library unless the course ships its own `requirements.txt` + (as the pandas/matplotlib data-analysis courses do), and `REVIEW.md` + recording the maintainer run-through (date, tool, version, observed agent + deviations). A course is incomplete until every lesson ships EN+ZH in the same change; `COURSE.md` must name the exact tool version it was taught with, and a tool major release triggers re-review. Never claim guaranteed learning outcomes. @@ -57,8 +62,9 @@ reviewed catalog data, and stable public JSON contracts. verify them with `python tools/render_readmes.py --check`. - Regenerate `content-manifest.json` and verify it with `python tools/build_content_manifest.py --check`. -- Verify every runnable example with `python tools/verify_examples.py` and - every course folder with `python tools/verify_courses.py`. +- Verify every runnable example with `python tools/verify_examples.py`, + every course folder with `python tools/verify_courses.py`, and every + learning path with `python tools/verify_paths.py`. - Website consumers must pin a full repository commit and verify the catalog checksum. Do not make production builds depend on a moving branch. - Keep external-link checks read-only, rate-limited, retryable, and blocked from diff --git a/CHANGELOG.md b/CHANGELOG.md index a8f25b7..435ee83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,31 @@ This file records notable catalog-contract and maintenance changes. +## [Unreleased] + +0.0.4 phase A — challenge-platform rework on top of 0.1.0. + +### Added + +- Per-checkpoint claim codes, badge frontmatter contracts, and the + deterministic `verify.py progress` subcommand (FP-411/412/415), enforced + by `tools/verify_courses.py`. +- The data-analysis course track: `da-eda`, `da-visualization`, and + `da-report` (EN+ZH), each with pandas/matplotlib `requirements.txt` where + needed and a stdlib-only report capstone. +- `paths/` learning paths sequencing courses into badge routes, plus + `tools/verify_paths.py` enforcing the path contract (FP-413/416/417/418, + wired into the Makefile and the validation workflow). +- `content-manifest.json` path entries (`type: "path"`) for the website. +- Checkpoint hints, the branch-path contract, and agent-solvability review + records across all eight courses (2026-09-13). + +### Changed + +- Repository narrative moved from agent-taught courses to challenge courses + with `TASK.md` contracts and `verify.py` claim codes; `COURSE.md` remains + the guided mode. README, README_cn, and `llms.txt` now reflect it. + ## [0.1.0] - 2026-09-12 First versioned release: agent-taught courses, the Project Radar diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c3750d9..714a2b7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,10 +11,12 @@ Read the [curation policy](docs/CURATION_POLICY.md) before contributing. ## Permissions -This repository currently does not grant a general license to reuse its content -or code. Public visibility is not permission to copy, redistribute, or relicense -repository material. Contributors must submit only material they have the right -to submit and retain required third-party notices. +This repository is dual-licensed as described in [LICENSE](LICENSE): code +(examples, tools, templates, course starters/solutions/verifiers) under the MIT +License, and written content (guides, playbooks, lessons, catalog records, +READMEs) under CC BY 4.0 with attribution "FlyPython (flypython.com)". +Contributors must submit only material they have the right to submit and retain +required third-party notices; a file's own license header, when present, wins. ## Propose a change @@ -23,6 +25,7 @@ Use the matching issue form before a larger change: - **Resource proposal** for a new official source. - **Project proposal** for a current Python project that should receive human review for Project Radar. +- **Course feedback** for running a course or reporting a course problem. - **Broken link** for an unreachable or replaced resource. - **Security report** for a vulnerability; follow [SECURITY.md](SECURITY.md) instead of opening a public issue. @@ -86,10 +89,12 @@ Run the same deterministic checks as CI: ```bash python -m pytest python tools/validate_catalog.py -python tools/export_catalog.py --check +python tools/export_catalog.py --check --target both python tools/render_readmes.py --check python tools/build_content_manifest.py --check python tools/verify_examples.py +python tools/verify_courses.py +python tools/verify_paths.py ``` Maintainers can run the networked link audit through GitHub Actions. For a diff --git a/README.md b/README.md index 9f56235..093d2c9 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ > [`courses/`](courses/), solve the contract in `TASK.md` with your coding > agent as the tool, and prove it with `verify.py` — which prints a claim > code per checkpoint. Prefer a guided path? `COURSE.md` still runs an -> agent-taught mode, and [`paths/`](paths/) sequences courses into badge +> agent-taught mode, and [`paths/`](paths/README.md) sequences courses into badge > routes. Continue on [flypython.com](https://flypython.com/). FlyPython is a practical, bilingual repository for writing good Python and @@ -25,7 +25,8 @@ versions into a browsable learning experience. ## Start in three minutes -No installation is required beyond Python 3.11+: +No installation is required beyond Python 3.11+ (development pins 3.12 +through `.python-version`): ```bash git clone https://github.com/flypythoncom/python.git @@ -42,7 +43,7 @@ to a coding agent (Cursor, Windsurf, Claude Code, Copilot) and make the starter | Goal | Start here | What you will produce | | --- | --- | --- | -| Learn by solving challenges | [Challenge courses](courses/) · [Learning paths](paths/) | A verified project + checkpoint claim codes from `verify.py` | +| Learn by solving challenges | [Challenge courses](courses/) · [Learning paths](paths/README.md) | A verified project + checkpoint claim codes from `verify.py` | | Write and change Python safely | [AI Coding workflow](guides/ai-coding/workflow.md) | A bounded change with explicit context and evidence | | Turn Python into a reliable product | [Product quality guide](guides/python-engineering/product-quality.md) | A tested, observable, reversible product path | | Finish a recurring engineering task | [Playbooks](playbooks/README.md) | A bug fix, API change, integration, dependency upgrade, or release | @@ -175,6 +176,8 @@ catalog/ paths.yml bilingual learning-path definitions resources/ one reviewed resource per YAML file projects/ human-review queue for current Python projects +courses/ challenge courses with TASK.md contracts and verify.py claim codes +paths/ learning paths sequencing courses into badge routes guides/ Python engineering and AI-coding methods playbooks/ repeatable task procedures and definitions of done examples/ small runnable projects with automated verification @@ -182,6 +185,7 @@ templates/ task, plan, review, and verification starters schema/ *.schema.json versioned machine-readable contracts catalog.json deterministic public export for consumers +radar.json deterministic Project Radar export for consumers content-manifest.json versioned paths, summaries, and checksums for the website tools/ generation, validation, example, and link-audit commands tests/ content consistency and behavior tests @@ -227,10 +231,12 @@ Run the deterministic checks: ```bash python -m pytest python tools/validate_catalog.py -python tools/export_catalog.py --check +python tools/export_catalog.py --check --target both python tools/render_readmes.py --check python tools/build_content_manifest.py --check python tools/verify_examples.py +python tools/verify_courses.py +python tools/verify_paths.py ``` After changing catalog sources, regenerate the public export before running the diff --git a/README_cn.md b/README_cn.md index 5248438..ccc596d 100644 --- a/README_cn.md +++ b/README_cn.md @@ -6,10 +6,11 @@ [English](README.md) · [中文](README_cn.md) · [🌐 官方在线门户](https://flypython.com) -> **新——由 Agent 授课、带客观验证的实战课程。** 从 [`courses/`](courses/) 下载一个 -> 文件夹,在你的编码 Agent 中打开,说一句*“开始第 1 课”*。每门课程都以一条 -> `verify.py` 命令收尾,证明你确实做出了什么。同一旅程可继续在 -> [flypython.com](https://flypython.com/) 上进行。 +> **带客观验证的挑战课程。** 从 [`courses/`](courses/) 挑一个文件夹,把 +> `TASK.md` 中的契约交给你的编码 Agent 去解决,并用 `verify.py` 证明结果—— +> 它会在每个检查点打印一个认领码。想要有人带?`COURSE.md` 仍支持 Agent +> 授课模式,[`paths/`](paths/README.md) 则把课程串成徽章路线。可继续在 +> [flypython.com](https://flypython.com/) 上学习。 FlyPython 是一个面向实践的双语仓库,帮助你写好 Python,并把代码变成用户可以依赖的 产品。这里同时提供 AI Coding 方法、任务 Playbook、可运行示例、复用模板,以及 API、 @@ -21,7 +22,7 @@ FlyPython 是一个面向实践的双语仓库,帮助你写好 Python,并把 ## 3 分钟开始 -只需要 Python 3.11+: +只需要 Python 3.11+(开发环境通过 `.python-version` 固定在 3.12): ```bash git clone https://github.com/flypythoncom/python.git @@ -38,7 +39,7 @@ python examples/product-slug/verify.py solution | 目标 | 从这里开始 | 最终产出 | | --- | --- | --- | -| 解题式动手实战 | [挑战课程](courses/) · [学习路线](paths/) | 一个通过验证的项目 + `verify.py` 的检查点认领码 | +| 解题式动手实战 | [挑战课程](courses/) · [学习路线](paths/README.md) | 一个通过验证的项目 + `verify.py` 的检查点认领码 | | 安全地编写和修改 Python | [AI Coding 工作方法](guides/ai-coding/workflow_cn.md) | 上下文明确、有证据的边界修改 | | 把 Python 变成可靠产品 | [产品质量指南](guides/python-engineering/product-quality_cn.md) | 可测试、可观测、可回退的产品路径 | | 完成反复出现的工程任务 | [Playbook](playbooks/README_cn.md) | Bug 修复、API 修改、外部集成、依赖升级或发布 | @@ -166,6 +167,8 @@ catalog/ paths.yml 中英文学习路径定义 resources/ 每项资源一个 YAML 文件 projects/ 最新 Python 项目的人工审核队列 +courses/ 带 TASK.md 契约和 verify.py 认领码的挑战课程 +paths/ 把课程串成徽章路线的学习路径 guides/ Python 工程与 AI Coding 方法 playbooks/ 可重复任务步骤与完成标准 examples/ 带自动验证的可运行小项目 @@ -173,6 +176,7 @@ templates/ 任务、计划、Review 和验证模板 schema/ *.schema.json 带版本的机器可读契约 catalog.json 提供给网站使用的确定性公开导出 +radar.json 提供给网站使用的确定性 Project Radar 导出 content-manifest.json 网站使用的版本、路径、摘要与校验值 tools/ 生成、校验、示例和链接审计工具 tests/ 内容一致性与行为测试 @@ -215,10 +219,12 @@ pytest ```bash python -m pytest python tools/validate_catalog.py -python tools/export_catalog.py --check +python tools/export_catalog.py --check --target both python tools/render_readmes.py --check python tools/build_content_manifest.py --check python tools/verify_examples.py +python tools/verify_courses.py +python tools/verify_paths.py ``` 修改源内容后重新生成: diff --git a/content-manifest.json b/content-manifest.json index 690444b..ddad293 100644 --- a/content-manifest.json +++ b/content-manifest.json @@ -20,7 +20,7 @@ "path": "playbooks/add-an-api/README_cn.md", "title": "增加或修改 Python API", "summary": "先定义公共契约,再实现并验证成功、错误和兼容性行为。", - "sha256": "5c844ed892f4bfa591f11a7be9a329306c3b191b645935fe13bb3a536067b691" + "sha256": "a1d7367965614d51c538162fbabdfd5759bf697510994d53842df428a27f941a" } ] }, @@ -82,14 +82,14 @@ "path": "courses/agent-rules-single-source/lessons/L01.md", "title": "Measure the drift you already have", "summary": "Run the checker against the three scenario repositories and read drift as a test failure, not a vibe.", - "sha256": "5daf164c4b622a483df4bbf1e1df4a5753779fb43e6f70a9456124c959e012e6" + "sha256": "cd923a835bc51166ca622ef61414b1dfd75eacc70c649307ab3a559b968617c3" }, { "lang": "zh-CN", "path": "courses/agent-rules-single-source/lessons/L01_cn.md", "title": "量化你已经存在的漂移", "summary": "对三个场景仓库运行检查器,把漂移读成测试失败,而不是一种感觉。", - "sha256": "a935a17b1e9d09f2f161580f480221ab36eb079c86711524bdf5e4508ed069d5" + "sha256": "c5f88441065a1bc5208b005c083dd0b231da4a281f0b5bb97710e55280daed6f" } ] }, @@ -473,14 +473,14 @@ "path": "courses/da-eda/COURSE.md", "title": "Exploratory Data Analysis with an Agent", "summary": "A challenge course — point your agent at a messy shop export and produce a verified EDA summary (results.json) whose numbers are checked against ground truth. First non-stdlib course in the catalog; pandas is managed with uv.", - "sha256": "3d5a7d05b6b48a667eeca51f7c6232e95c231d7cb4f78d3826de02c6a0a55b85" + "sha256": "d36b48bd7f5ee3864fd7b2653dff42703469f64728c440d343a9a5c8076fa99b" }, { "lang": "zh-CN", "path": "courses/da-eda/COURSE_cn.md", "title": "用 Agent 做探索性数据分析", "summary": "挑战式课程——让你的 Agent 处理一份脏的商店导出数据,产出一份经过真值校验的 EDA 摘要(results.json)。目录中第一门非标准库课程;pandas 由 uv 管理。", - "sha256": "ef5272a728fc540f4bedb3c650a1789d50a32cc90883aacd3202ba10360b4f00" + "sha256": "693d2da589858d6716c0dabf34042ac012875620092eefdea688ba5601e7a41b" } ] }, @@ -611,14 +611,14 @@ "path": "courses/da-report/COURSE.md", "title": "From Analysis to Report with an Agent", "summary": "Challenge course — turn a verified EDA summary into a structured report (report.json + report.md) where every number traces back to its input. Verification checks structure and number consistency, not writing quality.", - "sha256": "d2b978ad0e561b90734d747c4551cda91dddf8619e4d388799b67a4b59344496" + "sha256": "f0aea2909ed3e187497e9a79cf008d88324cf22e0ffc4251540825ecd8f08a46" }, { "lang": "zh-CN", "path": "courses/da-report/COURSE_cn.md", "title": "用 Agent 从分析到报告", "summary": "挑战课程——把经过验证的 EDA 摘要变成结构化报告(report.json + report.md),其中每个数字都可回溯到输入。验证检查结构与数字一致性,不评价文笔。", - "sha256": "c825ca0039fd828e4133a1a14956907a7cf8a74ff0c2e736f387c16fd26723e7" + "sha256": "f141a356b60de463d329fc72bb13374a8194cdbe5ecfc973a4f6f8c0e13c64c1" } ] }, @@ -749,14 +749,14 @@ "path": "courses/da-visualization/COURSE.md", "title": "Data Visualization with an Agent", "summary": "Challenge course — turn a clean sales dataset into three spec'd charts plus a summary.json whose numbers are verified against the data. Charts must be generated from data, never hardcoded.", - "sha256": "950c213e5093c415a6a78988fa7bb1c0056fd23b2d27225a762b977a70af3a5f" + "sha256": "8f2dba8784b18a8ca15ac578629f5099245299e8a80184a82bea2e29b3e4545c" }, { "lang": "zh-CN", "path": "courses/da-visualization/COURSE_cn.md", "title": "用 Agent 做数据可视化", "summary": "挑战课程——把干净的销售数据变成三张有规格的图和一份 summary.json,其中数字会与数据集校验。图必须由数据生成,不许硬编码。", - "sha256": "6c614cb91314a2b75b51e677d50a6d214355047eeb48def66fa0fc3105c3a690" + "sha256": "80f412edb8375f5e2027285a6b27f828e7670cf536d149400fde4cf2948d297b" } ] }, @@ -1209,14 +1209,14 @@ "path": "examples/pydantic-validation/README.md", "title": "Validate Untrusted Boundary Payloads", "summary": "Parse, sanitize, and validate messy external payloads with field normalization and structured error contracts.", - "sha256": "99e3fedc6e47fd915a24198cbe166ddafd51e4b6d3a4c7f1929bd56b2d27efa9" + "sha256": "aa79d051487995dde6a76018aabc0b7dc5e59c0584319ea84ca42c2fc61b0aeb" }, { "lang": "zh-CN", "path": "examples/pydantic-validation/README_cn.md", "title": "验证不可信边界请求体", "summary": "使用字段规范化与结构化错误契约,清洗并验证混乱的外部数据载荷。", - "sha256": "1e0f741e16df66623d58b6a9c0bf5128ebfc6fc9fe219f5287a683f647c232b6" + "sha256": "96a1c746603e9648fe4be5320721fc13445f325daecacf7e94546bf4e8ea03c3" } ] }, @@ -1239,7 +1239,7 @@ "path": "playbooks/fix-a-bug/README_cn.md", "title": "用回归测试修复 Python Bug", "summary": "复现行为、缩小原因、完成最小修复,并证明回归问题不会再次出现。", - "sha256": "cc770168300aa25cd4e42a00a7157a06aaf9a4c713678effdeb6b737a94e865f" + "sha256": "61e3745570569cf81760d2919ee618602ecf9e11bd8b675672f27eac19b91d0c" } ] }, @@ -1262,7 +1262,7 @@ "path": "playbooks/integrate-an-external-api/README_cn.md", "title": "接入外部 API", "summary": "用类型边界封装第三方 API,并安全处理凭据、超时、重试和确定性测试。", - "sha256": "900582abc309374ecc190b29c117208a09bbd55e136f1adcc6d95ecac04114a4" + "sha256": "a17f56ac6bf6bc18d2b5a628f413554ae49cbe11d2c44596dcb66b9fa4e80336" } ] }, @@ -1285,7 +1285,7 @@ "path": "playbooks/manage-database-migrations/README_cn.md", "title": "管理数据库架构迁移", "summary": "规划、执行并验证具备零停机兼容性与回滚安全性的可逆数据库迁移。", - "sha256": "5458037a8359c85aa16abe22696a21054e040f04bdc51a0c1dda4304e4585d8a" + "sha256": "9f2b53002fc2e2f7b1d2322809035b363436dcecb417231046ae7b6d8cd83b4d" } ] }, @@ -1301,7 +1301,7 @@ "path": "guides/mcp/migrate-2026-07-28.md", "title": "Migrate a Python MCP server to the 2026-07-28 specification", "summary": "Update a Python MCP server for the stateless 2026-07-28 specification by removing the initialize handshake, adopting the input_required round-trip, dropping deprecated features, and hardening authorization.", - "sha256": "3e64c297b0449769d42b00302e8c20d24f470999f3360b1219927da4264f310f" + "sha256": "063e285acdebc4854245cbdc2951060a1b38f0fdb814692c6bdc3cd0229d2316" }, { "lang": "zh-CN", @@ -1347,14 +1347,14 @@ "path": "paths/foundation/modules/route-challenge.md", "title": "Route challenge: dual-agent, double-verified", "summary": "Foundation path capstone — run the same task through Claude Code and Codex CLI and verify both outputs yourself.", - "sha256": "12390205b366d179afd34fc60fc557ed3aca53d10f6474a73d922b2cbbf3045a" + "sha256": "0c56353b6b98acb6a15f575ab57fc513f367716754c27aae7a042cccfeabdbc4" }, { "lang": "zh-CN", "path": "paths/foundation/modules/route-challenge_cn.md", "title": "路线挑战:双 Agent、双重验证", "summary": "基础路线收官挑战——同一任务分别用 Claude Code 与 Codex CLI 完成并亲手双重验证。", - "sha256": "86add318cc3094155f0a3f05eb05947d351af312c7f78f777f290be953cf27ab" + "sha256": "efb3d075163c2051dc8908072bc9da63ac13df17b4380f2ceac3fe17db6f2f33" } ] }, @@ -1446,7 +1446,7 @@ "path": "guides/python-engineering/async-patterns_cn.md", "title": "异步 Python 可靠模式与避坑指南", "summary": "通过避免常见异步反模式与规范管理任务生命周期,构建高韧性的并发 Python 应用。", - "sha256": "614ac9617cc77b94ba8030a9c6490b862b83d2e4bf8c7679109f6c6cd67113ed" + "sha256": "08e5c4f7ca489b5f62e7da13de505ee9d8ad3bb637cce2966d4d6d217c13f9a7" } ] }, @@ -1469,7 +1469,7 @@ "path": "guides/python-engineering/modern-typing_cn.md", "title": "现代 Python 类型系统实战", "summary": "将明确的 Python 类型作为机器可读的契约,指导可靠的 AI 辅助编程与工程交付。", - "sha256": "1fb7f63ea5675d58102c3d8178d1ffbf9720e69ebf9083036372ae1ddedeabab" + "sha256": "22afdb56b35819c3fb7adccd9e1d5016aa41baef821dbe639f41dc984a8e1784" } ] }, @@ -1515,7 +1515,7 @@ "path": "playbooks/setup-structured-logging/README_cn.md", "title": "搭建生产级结构化日志", "summary": "配置具备上下文贯穿、敏感数据脱敏与环境隔离的 JSON 结构化日志体系。", - "sha256": "a1f9fd0417c8d1f51a272e0b4f6fd5f0d401f505d84887ae7ee2c5343706af11" + "sha256": "2024d5386b1cf4c3e397a1034dc884e6ea244a967ed0eb4612ce5bd36b885950" } ] }, @@ -1538,7 +1538,7 @@ "path": "playbooks/ship-a-release/README_cn.md", "title": "发布 Python 版本", "summary": "交付可追踪的包或服务版本,并验证真实线上行为与回退路径。", - "sha256": "faa5387b7a6f394f22a16d4d4f4a3aaaeb4766018eb6c7a086c2533f7af5b8e9" + "sha256": "41697b3bdd720d29516138af2712dc5d241e8a8fc0ef66e87bf4bcd7f90f7d58" } ] }, @@ -1584,7 +1584,7 @@ "path": "playbooks/upgrade-dependencies/README_cn.md", "title": "升级 Python 依赖", "summary": "在边界明确的修改中,用锁文件、兼容性、安全和运行证据完成依赖升级。", - "sha256": "755df574181670b18a6ddbd1a24d2949b0a56c31790109e1d26aa4b6bdffdf27" + "sha256": "84aab5fed41cddd07f4c466a4f0eac675afa7af2cacc05fbdd6a8ed4cde3d514" } ] }, @@ -1607,7 +1607,7 @@ "path": "playbooks/write-llm-evals/README_cn.md", "title": "为大模型与 Agent 编写确定性评测", "summary": "结合黄金测试集与 Schema 断言,为 Prompt 调整与工具调用 Agent 构建防劣化回归测试套件。", - "sha256": "516a779f37b4f36b86be0807125f391bf08d6ea7d9270969e609331ca153c0e2" + "sha256": "5bc19f11f82be78197cdc8ab7b1aab01b201ffa57edf21d5e21b75b27e0f40e3" } ] } diff --git a/courses/agent-rules-single-source/REVIEW.md b/courses/agent-rules-single-source/REVIEW.md index 0610a4b..7b72f3f 100644 --- a/courses/agent-rules-single-source/REVIEW.md +++ b/courses/agent-rules-single-source/REVIEW.md @@ -9,7 +9,7 @@ - `python verify.py starter --expect-failure` — exit 0; all expected failure names reproduced. - `python verify.py solution` — exit 0; full suite passes. - - Every scenario skin exercised end to end by the suite. + - Every scenario repository (single-copy, thin-pointer, drifted) exercised end to end by the suite. - Not verified in this pass: teaching quality with a live agent session. ## Pending before the public course drop diff --git a/courses/agent-rules-single-source/lessons/L01.md b/courses/agent-rules-single-source/lessons/L01.md index 0646771..ef7e497 100644 --- a/courses/agent-rules-single-source/lessons/L01.md +++ b/courses/agent-rules-single-source/lessons/L01.md @@ -26,6 +26,10 @@ Run the checker against the three scenario repositories and read drift as a test Run `python starter/rules_check.py scenario/drifted` — it says everything is fine. That is the bug: three files disagree about one behavior and nothing notices. Then run the verifier: +```bash +python verify.py starter --expect-failure +``` + ## Exercise - Map each `verify.py --expect-failure` name to a real drift class: no source, no pointer detection, no drift reporting diff --git a/courses/agent-rules-single-source/lessons/L01_cn.md b/courses/agent-rules-single-source/lessons/L01_cn.md index 2f0549b..c0edbee 100644 --- a/courses/agent-rules-single-source/lessons/L01_cn.md +++ b/courses/agent-rules-single-source/lessons/L01_cn.md @@ -26,6 +26,10 @@ hints: 运行 `python starter/rules_check.py scenario/drifted`——它说一切正常。这正是 bug:三个文件对同一行为各执一词,却没有任何东西察觉。然后运行验证器: +```bash +python verify.py starter --expect-failure +``` + ## 练习 - 把 `verify.py --expect-failure` 里的每个具名失败对应到一类真实漂移:无真源、无指针识别、无漂移报告 diff --git a/courses/da-eda/COURSE.md b/courses/da-eda/COURSE.md index e1d5d25..f4141bb 100644 --- a/courses/da-eda/COURSE.md +++ b/courses/da-eda/COURSE.md @@ -8,7 +8,7 @@ content_version: 1 status: reviewed reviewed_on: 2026-09-12 badge: - id: da-eda + id: course-da-eda name_en: EDA Challenger name_zh: 探索分析挑战者 requires: All five checkpoints claimed (L01–L05) diff --git a/courses/da-eda/COURSE_cn.md b/courses/da-eda/COURSE_cn.md index 0e9819d..22faaa0 100644 --- a/courses/da-eda/COURSE_cn.md +++ b/courses/da-eda/COURSE_cn.md @@ -8,7 +8,7 @@ content_version: 1 status: reviewed reviewed_on: 2026-09-12 badge: - id: da-eda + id: course-da-eda name_en: EDA Challenger name_zh: 探索分析挑战者 requires: 全部五个检查点认领通过(L01–L05) diff --git a/courses/da-report/COURSE.md b/courses/da-report/COURSE.md index 5cbeb77..bab9f36 100644 --- a/courses/da-report/COURSE.md +++ b/courses/da-report/COURSE.md @@ -8,7 +8,7 @@ content_version: 1 status: reviewed reviewed_on: 2026-09-12 badge: - id: da-report + id: course-da-report name_en: Report Challenger name_zh: 分析报告挑战者 requires: All five checkpoints claimed (L01–L05) @@ -32,7 +32,7 @@ the auditable layer; the Markdown is for humans. ```bash cd courses/da-report -uv sync # pandas (or: pip install -r requirements.txt) +# Standard library only — nothing to install. ``` ## Challenges (checkpoints) @@ -70,7 +70,6 @@ starter/report.py unfinished implementation solution/report.py reviewed solution tests/test_report.py contract suite (read-only) verify.py objective pass/fail + claim codes -requirements.txt pinned pandas REVIEW.md maintainer run-through record ``` diff --git a/courses/da-report/COURSE_cn.md b/courses/da-report/COURSE_cn.md index 7bd30fc..28d9ab3 100644 --- a/courses/da-report/COURSE_cn.md +++ b/courses/da-report/COURSE_cn.md @@ -8,7 +8,7 @@ content_version: 1 status: reviewed reviewed_on: 2026-09-12 badge: - id: da-report + id: course-da-report name_en: Report Challenger name_zh: 分析报告挑战者 requires: 全部五个检查点认领通过(L01–L05) @@ -30,7 +30,7 @@ Markdown → 写出两份产物。结构化 JSON 是可审计层,Markdown 给 ```bash cd courses/da-report -uv sync # pandas(或 pip install -r requirements.txt) +# 仅使用标准库——无需安装任何依赖。 ``` ## 挑战(检查点) @@ -67,7 +67,6 @@ starter/report.py 未完成实现 solution/report.py 审核过的解答 tests/test_report.py 契约套件(只读) verify.py 客观通过/失败 + 认领码 -requirements.txt 锁定的 pandas REVIEW.md 维护者走查记录 ``` diff --git a/courses/da-report/requirements.txt b/courses/da-report/requirements.txt deleted file mode 100644 index 5728f51..0000000 --- a/courses/da-report/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -pandas==2.3.3 diff --git a/courses/da-report/verify.py b/courses/da-report/verify.py index fd7b017..3ffda56 100644 --- a/courses/da-report/verify.py +++ b/courses/da-report/verify.py @@ -3,8 +3,7 @@ Objective completion evidence for "From analysis to report" (da-report). ``progress`` prints checkpoint claim codes for recording on flypython.com. -Dependencies (pandas) managed with uv — run ``uv sync`` inside -this folder first. +Standard library only — no dependencies to install. """ from __future__ import annotations @@ -12,7 +11,6 @@ import argparse import base64 import hashlib -import importlib.util import json import os import subprocess @@ -45,19 +43,6 @@ ] -def _deps_available(): - return (importlib.util.find_spec("pandas") is not None - ) - - -def _deps_hint(): - print( - "pandas not installed. Run `uv sync` (or " - "`pip install -r requirements.txt`) inside this course folder first.", - file=sys.stderr, - ) - - def _claim_code(checkpoint_id): digest = hashlib.sha256( (COURSE_ID + ":" + checkpoint_id + ":" + COURSE_SALT).encode("utf-8") @@ -75,12 +60,8 @@ def _run_suite(implementation): def run_progress(as_json): - if not _deps_available(): - _deps_hint() - starter_ok = solution_ok = False - else: - starter_ok = _run_suite("starter").returncode == 0 - solution_ok = _run_suite("solution").returncode == 0 + starter_ok = _run_suite("starter").returncode == 0 + solution_ok = _run_suite("solution").returncode == 0 rows = [] for checkpoint in CHECKPOINTS: gate = checkpoint["gate"] @@ -126,10 +107,6 @@ def main() -> int: if args.implementation == "progress": return run_progress(args.json) - if not _deps_available(): - _deps_hint() - return 1 - environment = os.environ.copy() environment["PYTHONPATH"] = str(ROOT / args.implementation) result = subprocess.run( diff --git a/courses/da-visualization/COURSE.md b/courses/da-visualization/COURSE.md index 1741140..5d98e85 100644 --- a/courses/da-visualization/COURSE.md +++ b/courses/da-visualization/COURSE.md @@ -8,7 +8,7 @@ content_version: 1 status: reviewed reviewed_on: 2026-09-12 badge: - id: da-visualization + id: course-da-visualization name_en: Visualization Challenger name_zh: 可视化挑战者 requires: All five checkpoints claimed (L01–L05) diff --git a/courses/da-visualization/COURSE_cn.md b/courses/da-visualization/COURSE_cn.md index 3159a66..1fafda2 100644 --- a/courses/da-visualization/COURSE_cn.md +++ b/courses/da-visualization/COURSE_cn.md @@ -8,7 +8,7 @@ content_version: 1 status: reviewed reviewed_on: 2026-09-12 badge: - id: da-visualization + id: course-da-visualization name_en: Visualization Challenger name_zh: 可视化挑战者 requires: 全部五个检查点认领通过(L01–L05) diff --git a/courses/mcp-server-in-python/REVIEW.md b/courses/mcp-server-in-python/REVIEW.md index 332a524..8e30d91 100644 --- a/courses/mcp-server-in-python/REVIEW.md +++ b/courses/mcp-server-in-python/REVIEW.md @@ -9,7 +9,7 @@ - `python verify.py starter --expect-failure` — exit 0; all expected failure names reproduced. - `python verify.py solution` — exit 0; full suite passes. - - Every scenario skin exercised end to end by the suite. + - Every `scenario/requests/` wire sample exercised end to end by the suite. - Not verified in this pass: teaching quality with a live agent session. ## Pending before the public course drop diff --git a/courses/mcp-server-in-python/TASK.md b/courses/mcp-server-in-python/TASK.md index 9bc43db..814d8c3 100644 --- a/courses/mcp-server-in-python/TASK.md +++ b/courses/mcp-server-in-python/TASK.md @@ -22,4 +22,4 @@ Change only `starter/mcp_server.py`. - On any other handler exception: catch and return `{"result": {"isError": True, "content": [{"type": "text", "text": f"Error: {e}"}]}}`. - Provide `InputRequired(Exception)` with a `requests` attribute so tools can request client input. -Done means `python examples/mcp-server/verify.py starter` exits successfully. +Done means `python verify.py starter` exits 0 with all tests passing. diff --git a/courses/mcp-server-in-python/TASK_cn.md b/courses/mcp-server-in-python/TASK_cn.md index 94a59f1..6f94aeb 100644 --- a/courses/mcp-server-in-python/TASK_cn.md +++ b/courses/mcp-server-in-python/TASK_cn.md @@ -22,4 +22,4 @@ - 其他执行异常:捕获并返回 `{"result": {"isError": True, "content": [{"type": "text", "text": f"Error: {e}"}]}}`。 - 提供 `InputRequired(Exception)`,带 `requests` 属性,让工具能够向客户端请求补充输入。 -完成标准:`python examples/mcp-server/verify.py starter` 成功退出。 +完成标准:`python verify.py starter` 以状态码 0 退出、全部测试通过。 diff --git a/courses/mcp-server-in-python/verify.py b/courses/mcp-server-in-python/verify.py index 5689ea4..0f575c4 100644 --- a/courses/mcp-server-in-python/verify.py +++ b/courses/mcp-server-in-python/verify.py @@ -122,8 +122,10 @@ def main() -> int: "test_tools_call_handles_runtime_exception", "test_tools_call_missing_required_argument", ) - if not any(failure in output for failure in expected_failures): - print("Starter failed for an unexpected reason:", file=sys.stderr) + missing = [n for n in expected_failures if n not in output] + if missing: + print("Starter failed for unexpected reasons:", file=sys.stderr) + print("\n".join(missing), file=sys.stderr) print(output, file=sys.stderr) return 1 print("Expected MCP boundary regression reproduced: unhandled exceptions and missing schema validations fail.") diff --git a/courses/verifying-ai-generated-code/REVIEW.md b/courses/verifying-ai-generated-code/REVIEW.md index a9e1126..3aa165b 100644 --- a/courses/verifying-ai-generated-code/REVIEW.md +++ b/courses/verifying-ai-generated-code/REVIEW.md @@ -9,7 +9,7 @@ - `python verify.py starter --expect-failure` — exit 0; all expected failure names reproduced. - `python verify.py solution` — exit 0; full suite passes. - - Every scenario skin exercised end to end by the suite. + - Every scenario project (red, green, no-tests) exercised end to end by the suite. - Not verified in this pass: teaching quality with a live agent session. ## Pending before the public course drop diff --git a/docs/CONSUMING.md b/docs/CONSUMING.md index f7c5679..e6ba825 100644 --- a/docs/CONSUMING.md +++ b/docs/CONSUMING.md @@ -45,9 +45,12 @@ must leave the last accepted catalog unchanged. ## Contract -Both current exports have `schema_version: 1`. They are described by -[`schema/catalog-v1.schema.json`](../schema/catalog-v1.schema.json) and -[`schema/content-manifest-v1.schema.json`](../schema/content-manifest-v1.schema.json). +The current exports all have `schema_version: 1`. They are described by +[`schema/catalog-v1.schema.json`](../schema/catalog-v1.schema.json), +[`schema/content-manifest-v1.schema.json`](../schema/content-manifest-v1.schema.json), +and [`schema/radar-v1.schema.json`](../schema/radar-v1.schema.json) +(`radar.json`, the Project Radar export, is the third pinned artifact a +consumer should record alongside the other two). - A schema-version change may require consumer code changes. - A content-only change keeps the same schema version. diff --git a/docs/repo-plan-0.0.4.md b/docs/repo-plan-0.0.4.md index ef5294c..d166045 100644 --- a/docs/repo-plan-0.0.4.md +++ b/docs/repo-plan-0.0.4.md @@ -168,7 +168,9 @@ (属于发布证据,不是内容阻塞项)。 - FP-327 发布步骤:网站 pin 在一次刻意变更中固定到本仓库发布 SHA。 - FP-334 仓库描述/话题 + 首个 GitHub Release;该 Release 时将 - CHANGELOG `[Unreleased]` 切为正式版本小节。 + CHANGELOG `[Unreleased]` 切为正式版本小节。——已完成:`v0.1.0` + (2026-09-12,提交 `e420fca`),CHANGELOG 已含正式 0.1.0 小节;0.0.4 + phase A 之后的变更暂记于 `[Unreleased]`。 ## 8. 实施规范(挑战模型改造细节) diff --git a/examples/async-fetcher/TASK.md b/examples/async-fetcher/TASK.md index 254564d..d601b8a 100644 --- a/examples/async-fetcher/TASK.md +++ b/examples/async-fetcher/TASK.md @@ -7,7 +7,7 @@ Change only `starter/fetcher.py`. - At most `max_concurrency` calls to `fetch_fn` may be executing concurrently at any moment. - Retry & Backoff contract: - `fetch_fn` returns `{"status_code": int, "data": ...}` or raises an exception. - - If `status_code` in `{429, 500, 502, 503, 504}` or an exception is raised, retry up to `max_retries` times. + - If `status_code` in `{429, 500, 502, 503, 504}` or an exception is raised, retry within a total budget of `max_retries` attempts per item (a fresh call and its retries together count as `attempts`). - Permanent client errors (status 400..499 except 429) must NOT be retried. - On retry, back off by `0.01 * (2 ** attempt)` seconds. - Result collection: diff --git a/examples/async-fetcher/TASK_cn.md b/examples/async-fetcher/TASK_cn.md index ee2dd32..ff33f21 100644 --- a/examples/async-fetcher/TASK_cn.md +++ b/examples/async-fetcher/TASK_cn.md @@ -7,7 +7,7 @@ - 任何时刻并发执行 `fetch_fn` 的协程数量不得超过 `max_concurrency`。 - 重试与退避策略: - `fetch_fn` 返回 `{"status_code": int, "data": ...}` 或抛出异常。 - - 状态码为 `{429, 500, 502, 503, 504}` 或发生异常时,最多重试 `max_retries` 次。 + - 状态码为 `{429, 500, 502, 503, 504}` 或发生异常时,在每条记录总计 `max_retries` 次尝试的预算内重试(首次调用与各次重试共同计入 `attempts`)。 - 确定性客户端错误(400..499,排除 429)**严禁**重试。 - 重试时采用指数退避休眠 `0.01 * (2 ** attempt)` 秒。 - 结果聚合规范: diff --git a/examples/async-fetcher/solution/fetcher.py b/examples/async-fetcher/solution/fetcher.py index 2435420..86906a0 100644 --- a/examples/async-fetcher/solution/fetcher.py +++ b/examples/async-fetcher/solution/fetcher.py @@ -75,5 +75,9 @@ async def async_batch_fetch( max_retries: int = 3, ) -> list[dict[str, Any]]: semaphore = asyncio.Semaphore(max_concurrency) - tasks = [_fetch_single(item, fetch_fn, semaphore, max_retries) for item in items] - return await asyncio.gather(*tasks) + async with asyncio.TaskGroup() as group: + tasks = [ + group.create_task(_fetch_single(item, fetch_fn, semaphore, max_retries)) + for item in items + ] + return [task.result() for task in tasks] diff --git a/examples/product-slug/solution/product_slug.py b/examples/product-slug/solution/product_slug.py index 9114d49..e356456 100644 --- a/examples/product-slug/solution/product_slug.py +++ b/examples/product-slug/solution/product_slug.py @@ -5,8 +5,8 @@ def normalize_product_slug(name: str) -> str: """Return a normalized ASCII URL identifier for a product name.""" normalized = name.strip().lower() + normalized = re.sub(r"[^a-z0-9\s_-]", "", normalized) normalized = re.sub(r"[\s_-]+", "-", normalized) - normalized = re.sub(r"[^a-z0-9-]", "", normalized) normalized = normalized.strip("-") if not normalized: raise ValueError("product name must contain an ASCII letter or digit") diff --git a/examples/product-slug/tests/test_product_slug.py b/examples/product-slug/tests/test_product_slug.py index 457425f..e47c092 100644 --- a/examples/product-slug/tests/test_product_slug.py +++ b/examples/product-slug/tests/test_product_slug.py @@ -13,6 +13,10 @@ def test_collapses_mixed_separators(self) -> None: def test_removes_punctuation(self) -> None: self.assertEqual(normalize_product_slug("MCP: Tools!"), "mcp-tools") + def test_punctuation_does_not_duplicate_separators(self) -> None: + self.assertEqual(normalize_product_slug("Hello , World"), "hello-world") + self.assertEqual(normalize_product_slug("Kit! (deluxe)"), "kit-deluxe") + def test_rejects_an_empty_result(self) -> None: with self.assertRaises(ValueError): normalize_product_slug(" -- !! ") diff --git a/examples/product-slug/verify.py b/examples/product-slug/verify.py index 6e8744e..897e0f5 100644 --- a/examples/product-slug/verify.py +++ b/examples/product-slug/verify.py @@ -45,12 +45,13 @@ def main() -> int: "FAIL: test_collapses_mixed_separators", "FAIL: test_rejects_an_empty_result", "FAIL: test_removes_punctuation", + "FAIL: test_punctuation_does_not_duplicate_separators", ) if not all(failure in output for failure in expected_failures): print("Starter failed for an unexpected reason:", file=sys.stderr) print(output, file=sys.stderr) return 1 - print("Expected regression reproduced: 3 boundary cases fail.") + print("Expected regression reproduced: 4 boundary cases fail.") return 0 return result.returncode diff --git a/examples/pydantic-validation/README.md b/examples/pydantic-validation/README.md index 6278e41..25636b0 100644 --- a/examples/pydantic-validation/README.md +++ b/examples/pydantic-validation/README.md @@ -11,6 +11,11 @@ reviewed_on: 2026-09-02 # Validate Untrusted Boundary Payloads +This exercise is standard-library only by design: you hand-roll the normalization +and validation mechanics that a schema library such as Pydantic automates, so the +boundary rules stay visible. In production, reach for Pydantic — see the catalog's +[Pydantic documentation](https://pydantic.dev/docs/validation/latest/get-started/) entry. + External payloads from webhooks, mobile clients, and LLM tool calls are untrusted. They frequently arrive with mixed camelCase and snake_case keys, dirty currency strings, or invalid enum states. A production boundary validator must sanitize valid variations diff --git a/examples/pydantic-validation/README_cn.md b/examples/pydantic-validation/README_cn.md index 7c80a20..13b2e15 100644 --- a/examples/pydantic-validation/README_cn.md +++ b/examples/pydantic-validation/README_cn.md @@ -11,6 +11,10 @@ reviewed_on: 2026-09-02 # 验证不可信边界请求体 +本练习刻意只使用标准库:亲手实现 Pydantic 等校验库自动完成的规范化与验证机制, +让边界规则清晰可见。生产环境中请直接使用 Pydantic——见目录中收录的 +[Pydantic 官方文档](https://pydantic.dev/docs/validation/latest/get-started/)。 + 来自 Webhook、客户端或 LLM 工具调用的数据是不可信的。它们经常携带驼峰与下划线混杂的键名、 带符号的金额字符串或非法的枚举状态。生产级边界验证器必须在容忍合法变体的同时清洗数据,并在 遇到非法输入时返回明确的结构化错误,绝不能抛出未捕获的 500 异常崩溃。 diff --git a/examples/pydantic-validation/verify.py b/examples/pydantic-validation/verify.py index 03327ec..e9eb2f4 100644 --- a/examples/pydantic-validation/verify.py +++ b/examples/pydantic-validation/verify.py @@ -49,7 +49,7 @@ def main() -> int: print("Starter failed for an unexpected reason:", file=sys.stderr) print(output, file=sys.stderr) return 1 - print("Expected boundary regression reproduced: unhandled keys and schema errors fail.") + print("Expected boundary regression reproduced: camelCase/dirty amounts and missing fields fail.") return 0 return result.returncode diff --git a/examples/structured-pipeline/solution/pipeline.py b/examples/structured-pipeline/solution/pipeline.py index 5a19775..f9ee6ae 100644 --- a/examples/structured-pipeline/solution/pipeline.py +++ b/examples/structured-pipeline/solution/pipeline.py @@ -9,32 +9,34 @@ def run_pipeline(items: list[dict], batch_size: int = 2) -> dict: results = [] errors = [] - for item in items: - try: - if not isinstance(item, dict): - raise TypeError("Record must be a dictionary") - - raw_id = item.get("id") - if not isinstance(raw_id, str) or not raw_id.strip(): - raise ValueError("Record 'id' must be a non-empty string") - - value = item.get("value") - if not isinstance(value, (int, float)) or isinstance(value, bool): - raise TypeError("Record 'value' must be a numeric int or float") - - if value <= 0: - raise ValueError("Record 'value' must be positive") - - results.append({ - "id": raw_id, - "processed_value": round(value * 1.1, 2), - }) - except Exception as exc: - item_id = item.get("id", "unknown") if isinstance(item, dict) else "unknown" - errors.append({ - "id": str(item_id) if item_id else "unknown", - "reason": str(exc), - }) + for start in range(0, len(items), batch_size): + batch = items[start:start + batch_size] + for item in batch: + try: + if not isinstance(item, dict): + raise TypeError("Record must be a dictionary") + + raw_id = item.get("id") + if not isinstance(raw_id, str) or not raw_id.strip(): + raise ValueError("Record 'id' must be a non-empty string") + + value = item.get("value") + if not isinstance(value, (int, float)) or isinstance(value, bool): + raise TypeError("Record 'value' must be a numeric int or float") + + if value <= 0: + raise ValueError("Record 'value' must be positive") + + results.append({ + "id": raw_id, + "processed_value": round(value * 1.1, 2), + }) + except Exception as exc: + item_id = item.get("id", "unknown") if isinstance(item, dict) else "unknown" + errors.append({ + "id": str(item_id) if item_id else "unknown", + "reason": str(exc), + }) return { "total": len(items), diff --git a/guides/mcp/migrate-2026-07-28.md b/guides/mcp/migrate-2026-07-28.md index 1a206e2..9f327f6 100644 --- a/guides/mcp/migrate-2026-07-28.md +++ b/guides/mcp/migrate-2026-07-28.md @@ -120,7 +120,7 @@ This guide works from the published release notes and the shipped SDKs. Field-le schemas for `_meta`, the `requests` entries, and `cacheScope` values live in the specification and SDK types — consult them before shipping, and prefer the official Python SDK over hand-rolled dispatch in production. Authorization deployment details -issuer discovery, credential storage, and CIMD rollout, are also out of scope here. +— issuer discovery, credential storage, and CIMD rollout — are also out of scope here. ## Sources diff --git a/guides/python-engineering/async-patterns_cn.md b/guides/python-engineering/async-patterns_cn.md index 7d0730f..c0e018f 100644 --- a/guides/python-engineering/async-patterns_cn.md +++ b/guides/python-engineering/async-patterns_cn.md @@ -18,7 +18,7 @@ reviewed_on: 2026-09-02 ## 1. 严禁在事件循环中调用阻塞 IO -在协程中直接调用同步阻塞操作(如 `time.sleep` 或阻塞式网络请求)会冻结整个事件循环, +在协程中直接调用同步阻塞操作(如 `time.sleep` 或标准同步文件读取)会冻结整个事件循环, 导致其他所有并发协程饥饿停滞。必须通过 `asyncio.to_thread()` 将无法异步化的操作委托至后台线程池: ```python diff --git a/guides/python-engineering/modern-typing_cn.md b/guides/python-engineering/modern-typing_cn.md index c64bc58..756b290 100644 --- a/guides/python-engineering/modern-typing_cn.md +++ b/guides/python-engineering/modern-typing_cn.md @@ -51,7 +51,7 @@ class UserProfile(TypedDict): ## 4. 在开发与 CI 流程中进行静态验证 类型注解只有在被强制检查时才具备确定性价值。在 CI 流水线中运行 [Mypy documentation](https://mypy.readthedocs.io/en/stable/) -或 Pyright,确保公共接口类型完备。 +或 Pyright 等静态类型检查器,并配置 pre-commit 钩子拦截未标注类型的公共 API。 更多类型系统权威规范可参考官方 [Python typing documentation](https://typing.python.org/en/latest/)。 diff --git a/llms.txt b/llms.txt index 3a8afd8..e9d7ebc 100644 --- a/llms.txt +++ b/llms.txt @@ -1,6 +1,6 @@ # FlyPython Repository Index for LLMs and AI Agents -> FlyPython provides production-tested Python engineering playbooks, agent-taught courses with objective verification, verifiable test-driven starter examples, task contracts, and agent IDE rules. +> FlyPython provides production-tested Python engineering playbooks, challenge courses with objective verification and per-checkpoint claim codes, verifiable test-driven starter examples, task contracts, and agent IDE rules. ## Quick API and Automation Commands @@ -16,13 +16,16 @@ ## Machine-Readable Directories - `catalog/resources/`: YAML files representing verified libraries, tools, and platforms. -- `catalog/projects/`: One reviewed YAML file per Project Radar entry, plus the discovery queue `candidates.json`. -- `courses/`: Agent-taught course folders; download one and say "start lesson 1": +- `catalog/projects/`: One reviewed YAML file per Project Radar entry (discovery candidates are produced on demand by `tools/radar_scan.py`). +- `courses/`: Challenge course folders — solve the `TASK.md` contract with a coding agent and prove it with `verify.py` claim codes; `COURSE.md` still offers a guided, agent-taught mode: - `courses/hands-on-python-with-claude-code/`: Task contract, bounded change, and objective verify.py evidence with Claude Code (EN+ZH lessons, three scenario skins). - `courses/hands-on-with-openai-codex-cli/`: The same verified workflow taught with the OpenAI Codex CLI (EN+ZH lessons, reuses the flagship scenario skins). - `courses/agent-rules-single-source/`: Build a checker that proves one AGENTS.md source of truth across CLAUDE.md and .cursorrules (EN+ZH, three drift scenarios). - `courses/verifying-ai-generated-code/`: Build the release-evidence machine: run checks, parse results, refuse zero-test projects, write SHIP-RECORD.json (EN+ZH, three project skins). - `courses/mcp-server-in-python/`: Build a stateless 2026-07-28 MCP tool server in pure Python — reuses the reviewed mcp-server example core (EN+ZH). + - `courses/da-eda/`: Exploratory data analysis with pandas over a messy shop export: exact duplicates, bad amounts, one bad date (EN+ZH). + - `courses/da-visualization/`: Data visualization with matplotlib from clean long-format daily sales: summaries, one-figure charts, Agg-before-pyplot discipline (EN+ZH). + - `courses/da-report/`: Turn a verified EDA summary JSON into a rendered report with stdlib-only code and deterministic verification (EN+ZH). - `examples/`: Reproducible test projects with failing starters and verified solutions: - `examples/product-slug/`: String normalization edge cases and regex safety. - `examples/pydantic-validation/`: Model boundaries and custom validators. @@ -34,10 +37,10 @@ - `templates/AGENT_RULES.example_cn.md`: Universal rule set in Chinese. - `templates/AGENTS.example.md`: Repository instruction policy for coding agents. - `templates/AGENTS.example_cn.md`: Repository instruction policy in Chinese. - - `templates/TASK_CONTRACT.example.md`: Scoped specification template for coding agents. - - `templates/TASK_PLAN.example.md`: Implementation plan template. - - `templates/TASK_REVIEW.example.md`: Code change review template. - - `templates/VERIFICATION_RECORD.example.md`: Evidence record for reproducible tests. + - `templates/TASK_CONTRACT.md`: Scoped specification template for coding agents. + - `templates/IMPLEMENTATION_PLAN.md`: Implementation plan template. + - `templates/CODE_REVIEW.md`: Code change review template. + - `templates/VERIFICATION.md`: Evidence record for reproducible tests. ## Official Portal diff --git a/paths/README.md b/paths/README.md new file mode 100644 index 0000000..4432332 --- /dev/null +++ b/paths/README.md @@ -0,0 +1,28 @@ +# Learning paths + +Paths sequence challenge courses into badge routes. Each path lives in its +own folder with a machine-checked `path.json` contract plus bilingual module +and challenge documents. + +- [Agent tools foundation](foundation/) — drive Claude Code and Codex CLI, + write a single-source rules file, and verify agent output by hand. + Modules: an orientation module, the four hands-on courses, and a + dual-agent route challenge (200 points). +- [Data analysis with an agent](data-analysis/) — clean, explore, visualize, + and report on a real dataset. Modules: the `da-eda`, `da-visualization`, + and `da-report` courses plus an end-to-end capstone (200 points). + +## Conventions + +- `path.json` is the contract: module order, course references, points, and + badge definitions. `python tools/verify_paths.py` checks every path + against the courses that actually exist in `courses/`. +- Module documents (`modules/*.md`) always ship with a `_cn.md` pair in the + same change. +- Route capstones carry their own `TASK.md`/`TASK_cn.md`, starter inputs, a + reviewed `solution/`, and a `verify.py` that gates the route badge. +- Points come only from checkpoints verified by `verify.py` and recorded + with claim codes; badges are self-reported local evidence, never + certificates. + +Continue the guided experience on [flypython.com](https://flypython.com/). diff --git a/paths/README_cn.md b/paths/README_cn.md new file mode 100644 index 0000000..4967e45 --- /dev/null +++ b/paths/README_cn.md @@ -0,0 +1,24 @@ +# 学习路线 + +学习路线把挑战课程串成徽章路线。每条路线一个文件夹,包含机器可校验的 +`path.json` 契约与双语的模块、挑战文档。 + +- [Agent 工具基础](foundation/)——驱动 Claude Code 与 Codex CLI,写出单一 + 真源规则文件,并亲手验证 Agent 的产出。模块:一个入门模块、四门实战 + 课程,以及一个双 Agent 路线挑战(200 分)。 +- [用 Agent 做数据分析](data-analysis/)——对真实数据集完成清洗、探索、 + 可视化与报告。模块:`da-eda`、`da-visualization`、`da-report` 三门课程, + 外加一个端到端综合项目(200 分)。 + +## 约定 + +- `path.json` 是契约:模块顺序、课程引用、积分与徽章定义。 + `python tools/verify_paths.py` 会把每条路线与 `courses/` 中实际存在的 + 课程对照校验。 +- 模块文档(`modules/*.md`)必须在同一次变更中附带 `_cn.md` 配对。 +- 路线综合项目自带 `TASK.md`/`TASK_cn.md`、starter 输入、审核过的 + `solution/` 与守卫路线徽章的 `verify.py`。 +- 积分只来自 `verify.py` 判卷通过并经认领码记录的检查点;徽章是 + 自我报告的本地证据,绝不是证书。 + +引导式体验可继续在 [flypython.com](https://flypython.com/) 上进行。 diff --git a/paths/foundation/modules/route-challenge.md b/paths/foundation/modules/route-challenge.md index 106cc1d..4c1a4f5 100644 --- a/paths/foundation/modules/route-challenge.md +++ b/paths/foundation/modules/route-challenge.md @@ -19,7 +19,10 @@ different agents through the same task — and verify both results yourself. ## The task Pick a real small task you care about (suggestion: "a CLI that counts the -lines, words, and bytes of a file" or reuse any scenario skin from C1–C4). +lines, words, and bytes of a file" or reuse any scenario skin from the four +path courses: `hands-on-python-with-claude-code`, +`hands-on-with-openai-codex-cli`, `agent-rules-single-source`, +`verifying-ai-generated-code`). Then: 1. **Round one — Claude Code.** In a fresh folder, write a `TASK.md` @@ -37,8 +40,8 @@ Then: - Both folders kept intact (they are your evidence). - A short note: which agent's output needed fewer corrections, and *why you - believe that* — this is the skeptical-review habit from C4 applied across - tools. + believe that* — this is the skeptical-review habit from the + `verifying-ai-generated-code` course applied across tools. ## Checkpoint diff --git a/paths/foundation/modules/route-challenge_cn.md b/paths/foundation/modules/route-challenge_cn.md index 2236bc7..c849b54 100644 --- a/paths/foundation/modules/route-challenge_cn.md +++ b/paths/foundation/modules/route-challenge_cn.md @@ -19,7 +19,9 @@ reviewed_on: 2026-09-12 ## 任务 选一个你真正在乎的小任务(建议:“一个统计文件行数、词数、字节数的 -CLI”,或复用 C1–C4 的任意场景皮)。然后: +CLI”,或复用路线四门课程 `hands-on-python-with-claude-code`、 +`hands-on-with-openai-codex-cli`、`agent-rules-single-source`、 +`verifying-ai-generated-code` 的任意场景皮)。然后: 1. **第一轮 —— Claude Code。** 新建文件夹,写一个 `TASK.md` 描述契约 (输入、输出、边界情况、完成 = 测试通过),让 Claude Code 实现它, @@ -33,7 +35,7 @@ CLI”,或复用 C1–C4 的任意场景皮)。然后: - 两个文件夹都保留原样(它们就是你的证据)。 - 一段简短笔记:哪个 Agent 的产出需要的修正更少,以及**你为什么这么 - 判断**——这是把 C4 的怀疑式审查用到跨工具对比上。 + 判断**——这是把 `verifying-ai-generated-code` 一课的怀疑式审查用到跨工具对比上。 ## 检查点 diff --git a/playbooks/add-an-api/README_cn.md b/playbooks/add-an-api/README_cn.md index 941fb6d..9b4f4cf 100644 --- a/playbooks/add-an-api/README_cn.md +++ b/playbooks/add-an-api/README_cn.md @@ -20,4 +20,4 @@ reviewed_on: 2026-09-02 --- -在 [flypython.com](https://flypython.com/learn/zh) 的中文指南索引中继续这个任务:每份清单背后的完整指南与经过审核的资料目录。 +在 [flypython.com 资源页](https://flypython.com/resources#add-or-change-a-python-api)继续这个任务(该页为英文,锚点直达本清单):完整 Playbook 目录、每份清单背后的指南与经过审核的资料目录。 diff --git a/playbooks/fix-a-bug/README_cn.md b/playbooks/fix-a-bug/README_cn.md index dd47180..09ecbf3 100644 --- a/playbooks/fix-a-bug/README_cn.md +++ b/playbooks/fix-a-bug/README_cn.md @@ -24,4 +24,4 @@ reviewed_on: 2026-09-02 --- -在 [flypython.com](https://flypython.com/learn/zh) 的中文指南索引中继续这个任务:每份清单背后的完整指南与经过审核的资料目录。 +在 [flypython.com 资源页](https://flypython.com/resources#fix-a-bug-with-regression-test)继续这个任务(该页为英文,锚点直达本清单):完整 Playbook 目录、每份清单背后的指南与经过审核的资料目录。 diff --git a/playbooks/integrate-an-external-api/README_cn.md b/playbooks/integrate-an-external-api/README_cn.md index 2187bc5..4d9f573 100644 --- a/playbooks/integrate-an-external-api/README_cn.md +++ b/playbooks/integrate-an-external-api/README_cn.md @@ -20,4 +20,4 @@ reviewed_on: 2026-09-02 --- -在 [flypython.com](https://flypython.com/learn/zh) 的中文指南索引中继续这个任务:每份清单背后的完整指南与经过审核的资料目录。 +在 [flypython.com 资源页](https://flypython.com/resources#integrate-an-external-api)继续这个任务(该页为英文,锚点直达本清单):完整 Playbook 目录、每份清单背后的指南与经过审核的资料目录。 diff --git a/playbooks/manage-database-migrations/README_cn.md b/playbooks/manage-database-migrations/README_cn.md index 8ba1a44..67b7aeb 100644 --- a/playbooks/manage-database-migrations/README_cn.md +++ b/playbooks/manage-database-migrations/README_cn.md @@ -20,4 +20,4 @@ reviewed_on: 2026-09-02 --- -在 [flypython.com](https://flypython.com/learn/zh) 的中文指南索引中继续这个任务:每份清单背后的完整指南与经过审核的资料目录。 +在 [flypython.com 资源页](https://flypython.com/resources#manage-database-migrations)继续这个任务(该页为英文,锚点直达本清单):完整 Playbook 目录、每份清单背后的指南与经过审核的资料目录。 diff --git a/playbooks/setup-structured-logging/README_cn.md b/playbooks/setup-structured-logging/README_cn.md index dd60971..b42c615 100644 --- a/playbooks/setup-structured-logging/README_cn.md +++ b/playbooks/setup-structured-logging/README_cn.md @@ -20,4 +20,4 @@ reviewed_on: 2026-09-02 --- -在 [flypython.com](https://flypython.com/learn/zh) 的中文指南索引中继续这个任务:每份清单背后的完整指南与经过审核的资料目录。 +在 [flypython.com 资源页](https://flypython.com/resources#setup-structured-logging)继续这个任务(该页为英文,锚点直达本清单):完整 Playbook 目录、每份清单背后的指南与经过审核的资料目录。 diff --git a/playbooks/ship-a-release/README_cn.md b/playbooks/ship-a-release/README_cn.md index bfba6d9..d8bc074 100644 --- a/playbooks/ship-a-release/README_cn.md +++ b/playbooks/ship-a-release/README_cn.md @@ -20,4 +20,4 @@ reviewed_on: 2026-09-02 --- -在 [flypython.com](https://flypython.com/learn/zh) 的中文指南索引中继续这个任务:每份清单背后的完整指南与经过审核的资料目录。 +在 [flypython.com 资源页](https://flypython.com/resources#ship-a-python-release)继续这个任务(该页为英文,锚点直达本清单):完整 Playbook 目录、每份清单背后的指南与经过审核的资料目录。 diff --git a/playbooks/upgrade-dependencies/README_cn.md b/playbooks/upgrade-dependencies/README_cn.md index 9b274c4..effc2a8 100644 --- a/playbooks/upgrade-dependencies/README_cn.md +++ b/playbooks/upgrade-dependencies/README_cn.md @@ -20,4 +20,4 @@ reviewed_on: 2026-09-02 --- -在 [flypython.com](https://flypython.com/learn/zh) 的中文指南索引中继续这个任务:每份清单背后的完整指南与经过审核的资料目录。 +在 [flypython.com 资源页](https://flypython.com/resources#upgrade-python-dependencies)继续这个任务(该页为英文,锚点直达本清单):完整 Playbook 目录、每份清单背后的指南与经过审核的资料目录。 diff --git a/playbooks/write-llm-evals/README_cn.md b/playbooks/write-llm-evals/README_cn.md index 5d19f49..d3d297b 100644 --- a/playbooks/write-llm-evals/README_cn.md +++ b/playbooks/write-llm-evals/README_cn.md @@ -20,4 +20,4 @@ reviewed_on: 2026-09-02 --- -在 [flypython.com](https://flypython.com/learn/zh) 的中文指南索引中继续这个任务:每份清单背后的完整指南与经过审核的资料目录。 +在 [flypython.com 资源页](https://flypython.com/resources#write-llm-evals)继续这个任务(该页为英文,锚点直达本清单):完整 Playbook 目录、每份清单背后的指南与经过审核的资料目录。 diff --git a/pyproject.toml b/pyproject.toml index 275ac0f..e307124 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,11 @@ line-length = 88 select = ["E", "F", "I", "UP", "B", "SIM"] ignore = ["E501"] +[tool.ruff.lint.per-file-ignores] +# Course starters are deliberately unfinished: pre-imported modules are +# scaffolding the learner will use when implementing the contract. +"courses/*/starter/*.py" = ["F401"] + [tool.mypy] python_version = "3.11" strict = false diff --git a/templates/AGENT_RULES.example_cn.md b/templates/AGENT_RULES.example_cn.md index 5efcbb6..1b3cbd5 100644 --- a/templates/AGENT_RULES.example_cn.md +++ b/templates/AGENT_RULES.example_cn.md @@ -5,7 +5,7 @@ 1. **清晰边界 (Explicit Boundaries)**:严禁修改任务范围之外的无关文件。 2. **契约优先 (Contract First)**:在动手写代码前,先阅读既有类型定义、数据契约与测试断言。 3. **拒绝幻觉代码 (No Phantom Code)**:每一次代码改动,都必须有明确可执行的测试或验证命令支撑。 -4. **保持整洁规范 (Preserve Integrity)**:不要随意删除或重构无关的业务注释、代码格式与全局状态。 +4. **保持完整规范 (Preserve Integrity)**:除非明确要求,不要删除无关的注释、文档字符串或格式。 5. **现代工程栈 (Standard Tooling)**:优先推荐现代化标准工具链(Python 3.11+、uv、pytest、ruff、mypy)。 ## 改动交付验证流程 diff --git a/templates/CODE_REVIEW_cn.md b/templates/CODE_REVIEW_cn.md new file mode 100644 index 0000000..db14436 --- /dev/null +++ b/templates/CODE_REVIEW_cn.md @@ -0,0 +1,8 @@ +# 代码审查 + +- 改动是否满足用户契约,并保持无关行为不变? +- 边界输入、错误处理、超时与重试是否明确? +- 密钥、权限、破坏性影响以及模型/工具调用是否安全? +- 测试是否证明了所报告的故障、成功路径和关键边界情况? +- 相比其他合理方案,本次 diff 是否更小、更清晰? +- 在单元测试不足以覆盖的场景,是否检查过真实运行时行为? diff --git a/templates/EVAL_RECORD_cn.md b/templates/EVAL_RECORD_cn.md index ec3f489..32003e9 100644 --- a/templates/EVAL_RECORD_cn.md +++ b/templates/EVAL_RECORD_cn.md @@ -18,4 +18,4 @@ - [x] 非法畸形 JSON 容错与解析回退完成验证 - [x] 提示词注入攻击与系统 Prompt 防窃取完成验证 -- [x] 空输出与超时重试机制完成验证 +- [x] 空输出回退机制完成验证 diff --git a/templates/IMPLEMENTATION_PLAN_cn.md b/templates/IMPLEMENTATION_PLAN_cn.md new file mode 100644 index 0000000..8a4d832 --- /dev/null +++ b/templates/IMPLEMENTATION_PLAN_cn.md @@ -0,0 +1,13 @@ +# 实现计划 + +## 仓库现状调查 + +## 最小可用改动 + +## 文件与改动边界 + +## 验证顺序 + +## 风险与回滚 + +## 需要人决策的事项 diff --git a/templates/README.md b/templates/README.md index 446baf6..d6bd50c 100644 --- a/templates/README.md +++ b/templates/README.md @@ -1,8 +1,8 @@ # Reusable templates - [Task contract](TASK_CONTRACT.md) ([中文](TASK_CONTRACT_cn.md)): outcome, boundaries, acceptance, authority. -- [Implementation plan](IMPLEMENTATION_PLAN.md): findings, steps, risks, rollback. -- [Code review](CODE_REVIEW.md): correctness, security, maintainability, evidence. +- [Implementation plan](IMPLEMENTATION_PLAN.md) ([中文](IMPLEMENTATION_PLAN_cn.md)): findings, steps, risks, rollback. +- [Code review](CODE_REVIEW.md) ([中文](CODE_REVIEW_cn.md)): correctness, security, maintainability, evidence. - [Verification record](VERIFICATION.md) ([中文](VERIFICATION_cn.md)): exact commands and observed results. - [Agent repository instructions](AGENTS.example.md) ([中文](AGENTS.example_cn.md)): a starting policy for a Python repository using coding agents. - [Agent IDE rules](AGENT_RULES.example.md) ([中文](AGENT_RULES.example_cn.md)): universal configuration template for Cursor (.cursorrules), Windsurf, and Copilot. diff --git a/templates/README_cn.md b/templates/README_cn.md index 2c2b19a..1818d47 100644 --- a/templates/README_cn.md +++ b/templates/README_cn.md @@ -1,8 +1,8 @@ # 可复用工程模板 - [任务契约](TASK_CONTRACT_cn.md) ([English](TASK_CONTRACT.md)):目标结果、改动边界、验收标准、决策权限。 -- [实现计划](IMPLEMENTATION_PLAN.md):现状调查、实现步骤、风险点与回滚策略。 -- [代码审查](CODE_REVIEW.md):正确性、安全性、可维护性与验证凭据。 +- [实现计划](IMPLEMENTATION_PLAN_cn.md) ([English](IMPLEMENTATION_PLAN.md)):现状调查、实现步骤、风险点与回滚策略。 +- [代码审查](CODE_REVIEW_cn.md) ([English](CODE_REVIEW.md)):正确性、安全性、可维护性与验证凭据。 - [验证记录](VERIFICATION_cn.md) ([English](VERIFICATION.md)):执行命令与实际观察结果。 - [Agent 仓库守则](AGENTS.example_cn.md) ([English](AGENTS.example.md)):面向 Coding Agent 的仓库级准则规范。 - [Agent IDE 规则](AGENT_RULES.example_cn.md) ([English](AGENT_RULES.example.md)):适用于 Cursor (.cursorrules)、Windsurf、Copilot 的通用预设。 From c87db55a6954c2c42bea241fc473713bc8ffb0b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=E5=B0=8F=E4=BA=8C?= <xxg1413@gmail.com> Date: Sun, 13 Sep 2026 16:42:33 +0800 Subject: [PATCH 05/10] 0.0.7 B: shared-core tool courses + Codex app rewrite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename hands-on-with-openai-codex-cli → hands-on-with-openai-codex and rewrite all content for the Codex desktop app (threads, approvals, AGENTS.md, diff review); course_id course-codex-cli and salt preserved. - Add four new report-tool core courses authored from official docs: hands-on-with-cursor, hands-on-with-deepseek-harness, hands-on-with-kimi-code, hands-on-with-zcode. Each ships EN+ZH COURSE/lessons, honest REVIEW.md (docs-authored, live run-through pending), and a verify.py with its own course_id + salt. - Enforce shared-core byte equality via core-group marker checked by tools/verify_courses.py. - Foundation path: first-agent branch now offers all six tools; m0 orientation and route-challenge rewritten accordingly (any two tools). - tools/claim_receipt.py + verify.py receipt support and build_solution_hashes.py → course-solutions.json (claim receipts A-side). Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- content-manifest.json | 694 ++++++++++++++++-- course-solutions.json | 65 ++ courses/agent-rules-single-source/verify.py | 77 +- courses/da-eda/verify.py | 63 +- courses/da-report/verify.py | 67 +- courses/da-visualization/verify.py | 69 +- .../core-group | 1 + .../verify.py | 85 ++- courses/hands-on-with-cursor/COURSE.md | 112 +++ courses/hands-on-with-cursor/COURSE_cn.md | 102 +++ courses/hands-on-with-cursor/REVIEW.md | 30 + .../TASK.md | 0 .../TASK_cn.md | 0 courses/hands-on-with-cursor/core-group | 1 + courses/hands-on-with-cursor/lessons/L01.md | 104 +++ .../hands-on-with-cursor/lessons/L01_cn.md | 96 +++ courses/hands-on-with-cursor/lessons/L02.md | 87 +++ .../hands-on-with-cursor/lessons/L02_cn.md | 82 +++ courses/hands-on-with-cursor/lessons/L03.md | 84 +++ .../hands-on-with-cursor/lessons/L03_cn.md | 74 ++ courses/hands-on-with-cursor/lessons/L04.md | 79 ++ .../hands-on-with-cursor/lessons/L04_cn.md | 75 ++ courses/hands-on-with-cursor/lessons/L05.md | 77 ++ .../hands-on-with-cursor/lessons/L05_cn.md | 68 ++ .../scenario/api-tool/scenario.json | 0 .../scenario/api-tool/tickets.json | 0 .../scenario/data-monitor/metrics.csv | 0 .../scenario/data-monitor/scenario.json | 0 .../scenario/excel-report/orders.csv | 0 .../scenario/excel-report/scenario.json | 0 .../solution/report_tool.py | 0 .../starter/report_tool.py | 0 .../tests/test_report_tool.py | 0 courses/hands-on-with-cursor/verify.py | 201 +++++ .../hands-on-with-deepseek-harness/COURSE.md | 116 +++ .../COURSE_cn.md | 106 +++ .../hands-on-with-deepseek-harness/REVIEW.md | 31 + .../hands-on-with-deepseek-harness/TASK.md | 37 + .../hands-on-with-deepseek-harness/TASK_cn.md | 34 + .../hands-on-with-deepseek-harness/core-group | 1 + .../lessons/L01.md | 115 +++ .../lessons/L01_cn.md | 103 +++ .../lessons/L02.md | 88 +++ .../lessons/L02_cn.md | 79 ++ .../lessons/L03.md | 90 +++ .../lessons/L03_cn.md | 79 ++ .../lessons/L04.md | 83 +++ .../lessons/L04_cn.md | 77 ++ .../lessons/L05.md | 76 ++ .../lessons/L05_cn.md | 66 ++ .../scenario/api-tool/scenario.json | 8 + .../scenario/api-tool/tickets.json | 9 + .../scenario/data-monitor/metrics.csv | 8 + .../scenario/data-monitor/scenario.json | 8 + .../scenario/excel-report/orders.csv | 8 + .../scenario/excel-report/scenario.json | 8 + .../solution/report_tool.py | 134 ++++ .../starter/report_tool.py | 88 +++ .../tests/test_report_tool.py | 115 +++ .../hands-on-with-deepseek-harness/verify.py | 201 +++++ courses/hands-on-with-kimi-code/COURSE.md | 112 +++ courses/hands-on-with-kimi-code/COURSE_cn.md | 103 +++ courses/hands-on-with-kimi-code/REVIEW.md | 33 + courses/hands-on-with-kimi-code/TASK.md | 37 + courses/hands-on-with-kimi-code/TASK_cn.md | 34 + courses/hands-on-with-kimi-code/core-group | 1 + .../hands-on-with-kimi-code/lessons/L01.md | 105 +++ .../hands-on-with-kimi-code/lessons/L01_cn.md | 96 +++ .../hands-on-with-kimi-code/lessons/L02.md | 86 +++ .../hands-on-with-kimi-code/lessons/L02_cn.md | 79 ++ .../hands-on-with-kimi-code/lessons/L03.md | 82 +++ .../hands-on-with-kimi-code/lessons/L03_cn.md | 76 ++ .../hands-on-with-kimi-code/lessons/L04.md | 79 ++ .../hands-on-with-kimi-code/lessons/L04_cn.md | 74 ++ .../hands-on-with-kimi-code/lessons/L05.md | 81 ++ .../hands-on-with-kimi-code/lessons/L05_cn.md | 70 ++ .../scenario/api-tool/scenario.json | 8 + .../scenario/api-tool/tickets.json | 9 + .../scenario/data-monitor/metrics.csv | 8 + .../scenario/data-monitor/scenario.json | 8 + .../scenario/excel-report/orders.csv | 8 + .../scenario/excel-report/scenario.json | 8 + .../solution/report_tool.py | 134 ++++ .../starter/report_tool.py | 88 +++ .../tests/test_report_tool.py | 115 +++ courses/hands-on-with-kimi-code/verify.py | 201 +++++ .../hands-on-with-openai-codex-cli/COURSE.md | 88 --- .../COURSE_cn.md | 80 -- .../lessons/L01.md | 42 -- .../lessons/L01_cn.md | 42 -- .../lessons/L02.md | 40 - .../lessons/L02_cn.md | 40 - .../lessons/L03.md | 40 - .../lessons/L03_cn.md | 40 - .../lessons/L04.md | 40 - .../lessons/L04_cn.md | 40 - .../lessons/L05.md | 39 - .../lessons/L05_cn.md | 39 - courses/hands-on-with-openai-codex/COURSE.md | 112 +++ .../hands-on-with-openai-codex/COURSE_cn.md | 103 +++ .../REVIEW.md | 28 +- courses/hands-on-with-openai-codex/TASK.md | 37 + courses/hands-on-with-openai-codex/TASK_cn.md | 34 + courses/hands-on-with-openai-codex/core-group | 1 + .../hands-on-with-openai-codex/lessons/L01.md | 105 +++ .../lessons/L01_cn.md | 94 +++ .../hands-on-with-openai-codex/lessons/L02.md | 87 +++ .../lessons/L02_cn.md | 81 ++ .../hands-on-with-openai-codex/lessons/L03.md | 84 +++ .../lessons/L03_cn.md | 75 ++ .../hands-on-with-openai-codex/lessons/L04.md | 80 ++ .../lessons/L04_cn.md | 74 ++ .../hands-on-with-openai-codex/lessons/L05.md | 75 ++ .../lessons/L05_cn.md | 68 ++ .../scenario/api-tool/scenario.json | 8 + .../scenario/api-tool/tickets.json | 9 + .../scenario/data-monitor/metrics.csv | 8 + .../scenario/data-monitor/scenario.json | 8 + .../scenario/excel-report/orders.csv | 8 + .../scenario/excel-report/scenario.json | 8 + .../solution/report_tool.py | 134 ++++ .../starter/report_tool.py | 88 +++ .../tests/test_report_tool.py | 115 +++ .../verify.py | 85 ++- courses/hands-on-with-zcode/COURSE.md | 111 +++ courses/hands-on-with-zcode/COURSE_cn.md | 101 +++ courses/hands-on-with-zcode/REVIEW.md | 32 + courses/hands-on-with-zcode/TASK.md | 37 + courses/hands-on-with-zcode/TASK_cn.md | 34 + courses/hands-on-with-zcode/core-group | 1 + courses/hands-on-with-zcode/lessons/L01.md | 106 +++ courses/hands-on-with-zcode/lessons/L01_cn.md | 99 +++ courses/hands-on-with-zcode/lessons/L02.md | 88 +++ courses/hands-on-with-zcode/lessons/L02_cn.md | 79 ++ courses/hands-on-with-zcode/lessons/L03.md | 82 +++ courses/hands-on-with-zcode/lessons/L03_cn.md | 73 ++ courses/hands-on-with-zcode/lessons/L04.md | 82 +++ courses/hands-on-with-zcode/lessons/L04_cn.md | 76 ++ courses/hands-on-with-zcode/lessons/L05.md | 80 ++ courses/hands-on-with-zcode/lessons/L05_cn.md | 71 ++ .../scenario/api-tool/scenario.json | 8 + .../scenario/api-tool/tickets.json | 9 + .../scenario/data-monitor/metrics.csv | 8 + .../scenario/data-monitor/scenario.json | 8 + .../scenario/excel-report/orders.csv | 8 + .../scenario/excel-report/scenario.json | 8 + .../solution/report_tool.py | 134 ++++ .../starter/report_tool.py | 88 +++ .../tests/test_report_tool.py | 115 +++ courses/hands-on-with-zcode/verify.py | 201 +++++ courses/mcp-server-in-python/verify.py | 71 +- courses/verifying-ai-generated-code/verify.py | 71 +- paths/foundation/modules/m0-orientation.md | 29 +- paths/foundation/modules/m0-orientation_cn.md | 41 +- paths/foundation/modules/route-challenge.md | 35 +- .../foundation/modules/route-challenge_cn.md | 60 +- paths/foundation/path.json | 66 +- tests/test_courses.py | 10 +- tools/build_solution_hashes.py | 75 ++ tools/claim_receipt.py | 108 +++ tools/verify_courses.py | 134 ++++ 161 files changed, 9704 insertions(+), 810 deletions(-) create mode 100644 course-solutions.json create mode 100644 courses/hands-on-python-with-claude-code/core-group create mode 100644 courses/hands-on-with-cursor/COURSE.md create mode 100644 courses/hands-on-with-cursor/COURSE_cn.md create mode 100644 courses/hands-on-with-cursor/REVIEW.md rename courses/{hands-on-with-openai-codex-cli => hands-on-with-cursor}/TASK.md (100%) rename courses/{hands-on-with-openai-codex-cli => hands-on-with-cursor}/TASK_cn.md (100%) create mode 100644 courses/hands-on-with-cursor/core-group create mode 100644 courses/hands-on-with-cursor/lessons/L01.md create mode 100644 courses/hands-on-with-cursor/lessons/L01_cn.md create mode 100644 courses/hands-on-with-cursor/lessons/L02.md create mode 100644 courses/hands-on-with-cursor/lessons/L02_cn.md create mode 100644 courses/hands-on-with-cursor/lessons/L03.md create mode 100644 courses/hands-on-with-cursor/lessons/L03_cn.md create mode 100644 courses/hands-on-with-cursor/lessons/L04.md create mode 100644 courses/hands-on-with-cursor/lessons/L04_cn.md create mode 100644 courses/hands-on-with-cursor/lessons/L05.md create mode 100644 courses/hands-on-with-cursor/lessons/L05_cn.md rename courses/{hands-on-with-openai-codex-cli => hands-on-with-cursor}/scenario/api-tool/scenario.json (100%) rename courses/{hands-on-with-openai-codex-cli => hands-on-with-cursor}/scenario/api-tool/tickets.json (100%) rename courses/{hands-on-with-openai-codex-cli => hands-on-with-cursor}/scenario/data-monitor/metrics.csv (100%) rename courses/{hands-on-with-openai-codex-cli => hands-on-with-cursor}/scenario/data-monitor/scenario.json (100%) rename courses/{hands-on-with-openai-codex-cli => hands-on-with-cursor}/scenario/excel-report/orders.csv (100%) rename courses/{hands-on-with-openai-codex-cli => hands-on-with-cursor}/scenario/excel-report/scenario.json (100%) rename courses/{hands-on-with-openai-codex-cli => hands-on-with-cursor}/solution/report_tool.py (100%) rename courses/{hands-on-with-openai-codex-cli => hands-on-with-cursor}/starter/report_tool.py (100%) rename courses/{hands-on-with-openai-codex-cli => hands-on-with-cursor}/tests/test_report_tool.py (100%) create mode 100644 courses/hands-on-with-cursor/verify.py create mode 100644 courses/hands-on-with-deepseek-harness/COURSE.md create mode 100644 courses/hands-on-with-deepseek-harness/COURSE_cn.md create mode 100644 courses/hands-on-with-deepseek-harness/REVIEW.md create mode 100644 courses/hands-on-with-deepseek-harness/TASK.md create mode 100644 courses/hands-on-with-deepseek-harness/TASK_cn.md create mode 100644 courses/hands-on-with-deepseek-harness/core-group create mode 100644 courses/hands-on-with-deepseek-harness/lessons/L01.md create mode 100644 courses/hands-on-with-deepseek-harness/lessons/L01_cn.md create mode 100644 courses/hands-on-with-deepseek-harness/lessons/L02.md create mode 100644 courses/hands-on-with-deepseek-harness/lessons/L02_cn.md create mode 100644 courses/hands-on-with-deepseek-harness/lessons/L03.md create mode 100644 courses/hands-on-with-deepseek-harness/lessons/L03_cn.md create mode 100644 courses/hands-on-with-deepseek-harness/lessons/L04.md create mode 100644 courses/hands-on-with-deepseek-harness/lessons/L04_cn.md create mode 100644 courses/hands-on-with-deepseek-harness/lessons/L05.md create mode 100644 courses/hands-on-with-deepseek-harness/lessons/L05_cn.md create mode 100644 courses/hands-on-with-deepseek-harness/scenario/api-tool/scenario.json create mode 100644 courses/hands-on-with-deepseek-harness/scenario/api-tool/tickets.json create mode 100644 courses/hands-on-with-deepseek-harness/scenario/data-monitor/metrics.csv create mode 100644 courses/hands-on-with-deepseek-harness/scenario/data-monitor/scenario.json create mode 100644 courses/hands-on-with-deepseek-harness/scenario/excel-report/orders.csv create mode 100644 courses/hands-on-with-deepseek-harness/scenario/excel-report/scenario.json create mode 100644 courses/hands-on-with-deepseek-harness/solution/report_tool.py create mode 100644 courses/hands-on-with-deepseek-harness/starter/report_tool.py create mode 100644 courses/hands-on-with-deepseek-harness/tests/test_report_tool.py create mode 100644 courses/hands-on-with-deepseek-harness/verify.py create mode 100644 courses/hands-on-with-kimi-code/COURSE.md create mode 100644 courses/hands-on-with-kimi-code/COURSE_cn.md create mode 100644 courses/hands-on-with-kimi-code/REVIEW.md create mode 100644 courses/hands-on-with-kimi-code/TASK.md create mode 100644 courses/hands-on-with-kimi-code/TASK_cn.md create mode 100644 courses/hands-on-with-kimi-code/core-group create mode 100644 courses/hands-on-with-kimi-code/lessons/L01.md create mode 100644 courses/hands-on-with-kimi-code/lessons/L01_cn.md create mode 100644 courses/hands-on-with-kimi-code/lessons/L02.md create mode 100644 courses/hands-on-with-kimi-code/lessons/L02_cn.md create mode 100644 courses/hands-on-with-kimi-code/lessons/L03.md create mode 100644 courses/hands-on-with-kimi-code/lessons/L03_cn.md create mode 100644 courses/hands-on-with-kimi-code/lessons/L04.md create mode 100644 courses/hands-on-with-kimi-code/lessons/L04_cn.md create mode 100644 courses/hands-on-with-kimi-code/lessons/L05.md create mode 100644 courses/hands-on-with-kimi-code/lessons/L05_cn.md create mode 100644 courses/hands-on-with-kimi-code/scenario/api-tool/scenario.json create mode 100644 courses/hands-on-with-kimi-code/scenario/api-tool/tickets.json create mode 100644 courses/hands-on-with-kimi-code/scenario/data-monitor/metrics.csv create mode 100644 courses/hands-on-with-kimi-code/scenario/data-monitor/scenario.json create mode 100644 courses/hands-on-with-kimi-code/scenario/excel-report/orders.csv create mode 100644 courses/hands-on-with-kimi-code/scenario/excel-report/scenario.json create mode 100644 courses/hands-on-with-kimi-code/solution/report_tool.py create mode 100644 courses/hands-on-with-kimi-code/starter/report_tool.py create mode 100644 courses/hands-on-with-kimi-code/tests/test_report_tool.py create mode 100644 courses/hands-on-with-kimi-code/verify.py delete mode 100644 courses/hands-on-with-openai-codex-cli/COURSE.md delete mode 100644 courses/hands-on-with-openai-codex-cli/COURSE_cn.md delete mode 100644 courses/hands-on-with-openai-codex-cli/lessons/L01.md delete mode 100644 courses/hands-on-with-openai-codex-cli/lessons/L01_cn.md delete mode 100644 courses/hands-on-with-openai-codex-cli/lessons/L02.md delete mode 100644 courses/hands-on-with-openai-codex-cli/lessons/L02_cn.md delete mode 100644 courses/hands-on-with-openai-codex-cli/lessons/L03.md delete mode 100644 courses/hands-on-with-openai-codex-cli/lessons/L03_cn.md delete mode 100644 courses/hands-on-with-openai-codex-cli/lessons/L04.md delete mode 100644 courses/hands-on-with-openai-codex-cli/lessons/L04_cn.md delete mode 100644 courses/hands-on-with-openai-codex-cli/lessons/L05.md delete mode 100644 courses/hands-on-with-openai-codex-cli/lessons/L05_cn.md create mode 100644 courses/hands-on-with-openai-codex/COURSE.md create mode 100644 courses/hands-on-with-openai-codex/COURSE_cn.md rename courses/{hands-on-with-openai-codex-cli => hands-on-with-openai-codex}/REVIEW.md (54%) create mode 100644 courses/hands-on-with-openai-codex/TASK.md create mode 100644 courses/hands-on-with-openai-codex/TASK_cn.md create mode 100644 courses/hands-on-with-openai-codex/core-group create mode 100644 courses/hands-on-with-openai-codex/lessons/L01.md create mode 100644 courses/hands-on-with-openai-codex/lessons/L01_cn.md create mode 100644 courses/hands-on-with-openai-codex/lessons/L02.md create mode 100644 courses/hands-on-with-openai-codex/lessons/L02_cn.md create mode 100644 courses/hands-on-with-openai-codex/lessons/L03.md create mode 100644 courses/hands-on-with-openai-codex/lessons/L03_cn.md create mode 100644 courses/hands-on-with-openai-codex/lessons/L04.md create mode 100644 courses/hands-on-with-openai-codex/lessons/L04_cn.md create mode 100644 courses/hands-on-with-openai-codex/lessons/L05.md create mode 100644 courses/hands-on-with-openai-codex/lessons/L05_cn.md create mode 100644 courses/hands-on-with-openai-codex/scenario/api-tool/scenario.json create mode 100644 courses/hands-on-with-openai-codex/scenario/api-tool/tickets.json create mode 100644 courses/hands-on-with-openai-codex/scenario/data-monitor/metrics.csv create mode 100644 courses/hands-on-with-openai-codex/scenario/data-monitor/scenario.json create mode 100644 courses/hands-on-with-openai-codex/scenario/excel-report/orders.csv create mode 100644 courses/hands-on-with-openai-codex/scenario/excel-report/scenario.json create mode 100644 courses/hands-on-with-openai-codex/solution/report_tool.py create mode 100644 courses/hands-on-with-openai-codex/starter/report_tool.py create mode 100644 courses/hands-on-with-openai-codex/tests/test_report_tool.py rename courses/{hands-on-with-openai-codex-cli => hands-on-with-openai-codex}/verify.py (54%) create mode 100644 courses/hands-on-with-zcode/COURSE.md create mode 100644 courses/hands-on-with-zcode/COURSE_cn.md create mode 100644 courses/hands-on-with-zcode/REVIEW.md create mode 100644 courses/hands-on-with-zcode/TASK.md create mode 100644 courses/hands-on-with-zcode/TASK_cn.md create mode 100644 courses/hands-on-with-zcode/core-group create mode 100644 courses/hands-on-with-zcode/lessons/L01.md create mode 100644 courses/hands-on-with-zcode/lessons/L01_cn.md create mode 100644 courses/hands-on-with-zcode/lessons/L02.md create mode 100644 courses/hands-on-with-zcode/lessons/L02_cn.md create mode 100644 courses/hands-on-with-zcode/lessons/L03.md create mode 100644 courses/hands-on-with-zcode/lessons/L03_cn.md create mode 100644 courses/hands-on-with-zcode/lessons/L04.md create mode 100644 courses/hands-on-with-zcode/lessons/L04_cn.md create mode 100644 courses/hands-on-with-zcode/lessons/L05.md create mode 100644 courses/hands-on-with-zcode/lessons/L05_cn.md create mode 100644 courses/hands-on-with-zcode/scenario/api-tool/scenario.json create mode 100644 courses/hands-on-with-zcode/scenario/api-tool/tickets.json create mode 100644 courses/hands-on-with-zcode/scenario/data-monitor/metrics.csv create mode 100644 courses/hands-on-with-zcode/scenario/data-monitor/scenario.json create mode 100644 courses/hands-on-with-zcode/scenario/excel-report/orders.csv create mode 100644 courses/hands-on-with-zcode/scenario/excel-report/scenario.json create mode 100644 courses/hands-on-with-zcode/solution/report_tool.py create mode 100644 courses/hands-on-with-zcode/starter/report_tool.py create mode 100644 courses/hands-on-with-zcode/tests/test_report_tool.py create mode 100644 courses/hands-on-with-zcode/verify.py create mode 100644 tools/build_solution_hashes.py create mode 100644 tools/claim_receipt.py diff --git a/content-manifest.json b/content-manifest.json index ddad293..748e339 100644 --- a/content-manifest.json +++ b/content-manifest.json @@ -326,138 +326,276 @@ { "id": "course-codex-cli", "type": "course", - "content_version": 2, + "content_version": 3, "status": "reviewed", - "reviewed_on": "2026-09-12", + "reviewed_on": "2026-09-13", "locales": [ { "lang": "en-US", - "path": "courses/hands-on-with-openai-codex-cli/COURSE.md", - "title": "Hands-on with OpenAI Codex CLI", - "summary": "The same verified Python workflow as the Claude Code course — task contract, bounded change, objective verify.py evidence — taught hands-on with the OpenAI Codex CLI instead.", - "sha256": "48e81f530f58ef69fe92e6312b08f01b4661f311cf07371809ce206e2eecd474" + "path": "courses/hands-on-with-openai-codex/COURSE.md", + "title": "Hands-on with the OpenAI Codex App", + "summary": "The verified Python workflow — task contract, bounded change, objective verify.py evidence — taught hands-on in the Codex desktop app, where an agent thread runs the loop and you supervise diffs and approvals.", + "sha256": "7a0648d4b0547e4b8a39ccf7dea8de2aae02908d24c26880a2520e50c2252651" }, { "lang": "zh-CN", - "path": "courses/hands-on-with-openai-codex-cli/COURSE_cn.md", - "title": "OpenAI Codex CLI 实战", - "summary": "与 Claude Code 课程相同的已验证 Python 工作流——任务契约、最小变更、客观 verify.py 证据——改用 OpenAI Codex CLI 完成实战。", - "sha256": "bbdf5d8a71d8243f1cb7a3c30d2aa4ddb656cb932d36aaae3d543e3b5f7ea3c2" + "path": "courses/hands-on-with-openai-codex/COURSE_cn.md", + "title": "上手 OpenAI Codex 应用", + "summary": "经过验证的 Python 工作流——任务契约、有界改动、客观 verify.py 证据——在 Codex 桌面应用里手把手教:由 Agent 线程跑闭环,你监督 diff 与审批。", + "sha256": "68338e68b7145fc324472e05c6b8e649fae10cd88861510f8e7fe3dba0be6121" } ] }, { "id": "course-codex-cli-l01", "type": "course", - "content_version": 1, + "content_version": 2, "status": "reviewed", - "reviewed_on": "2026-09-12", + "reviewed_on": "2026-09-13", "locales": [ { "lang": "en-US", - "path": "courses/hands-on-with-openai-codex-cli/lessons/L01.md", - "title": "Lesson 1: Reproduce the failure with Codex in the loop", - "summary": "Run the verifier both ways, map each failing test to a real-world failure class, and learn how to hand the folder to Codex.", - "sha256": "1c316f7b5575741ce42c299d89e2075e10a162099c53cd6344e75ccf2487a5cf" + "path": "courses/hands-on-with-openai-codex/lessons/L01.md", + "title": "Lesson 1: Reproduce the failure in a Codex thread", + "summary": "Open the folder as a Codex project, start your first thread, and make the failure objective with verify.py — inside the app, not beside it.", + "sha256": "8ff98ae641562d417a00c68650c5a4b48d4d04af52fc97f1368ad566e620d25b" }, { "lang": "zh-CN", - "path": "courses/hands-on-with-openai-codex-cli/lessons/L01_cn.md", - "title": "第 1 课:让 Codex 进环,先复现故障", - "summary": "双向运行验证器,把每个失败测试对应到真实故障,并学会如何把文件夹交给 Codex。", - "sha256": "878513b4bb6218655088cdec3fa873c4318e9c729b86ce176e5b14a93b8eee9a" + "path": "courses/hands-on-with-openai-codex/lessons/L01_cn.md", + "title": "第 1 课:在 Codex 线程里复现故障", + "summary": "把文件夹作为 Codex 项目打开,开启第一个线程,用 verify.py 让失败变得客观——在应用内完成,而不是在应用旁边。", + "sha256": "d8758b7e3ba9ba3dcc8bb731f84035128d4bfec826615467ee9ca4331c773268" } ] }, { "id": "course-codex-cli-l02", "type": "course", - "content_version": 1, + "content_version": 2, "status": "reviewed", - "reviewed_on": "2026-09-12", + "reviewed_on": "2026-09-13", "locales": [ { "lang": "en-US", - "path": "courses/hands-on-with-openai-codex-cli/lessons/L02.md", - "title": "Lesson 2: Write the task contract before the change", - "summary": "Turn \"make it better\" into bounded, testable statements — the single highest-leverage skill for driving Codex.", - "sha256": "e3d75d35a8585c4765f76f506d907c65136359306657943b1c657239495bab07" + "path": "courses/hands-on-with-openai-codex/lessons/L02.md", + "title": "Lesson 2: Write the task contract, let AGENTS.md carry the rules", + "summary": "Turn \"make it better\" into a bounded, testable contract — then make it durable so every Codex thread starts with the same rules.", + "sha256": "d7180997e42e69bd8c80976287f4f9fc394492a729a2e87ff7f8134ea98e5d33" }, { "lang": "zh-CN", - "path": "courses/hands-on-with-openai-codex-cli/lessons/L02_cn.md", - "title": "第 2 课:动手之前先写任务契约", - "summary": "把“优化一下”变成有边界、可测试的陈述——驱动 Codex 杠杆最大的一项技能。", - "sha256": "9c0cc619fe1e9c4ec07733e02d7832c11709dcb491cbf0e3a2f574dc2dc47ea2" + "path": "courses/hands-on-with-openai-codex/lessons/L02_cn.md", + "title": "第 2 课:写任务契约,让 AGENTS.md 承载规则", + "summary": "把「改好一点」变成有边界、可测试的契约——再让它持久化,让每个 Codex 线程都从同一套规则开始。", + "sha256": "3c817a30bd204b6b6907336616f5c88228f3bb0d9aa1561e08a77f345f9971e8" } ] }, { "id": "course-codex-cli-l03", "type": "course", - "content_version": 1, + "content_version": 2, "status": "reviewed", - "reviewed_on": "2026-09-12", + "reviewed_on": "2026-09-13", "locales": [ { "lang": "en-US", - "path": "courses/hands-on-with-openai-codex-cli/lessons/L03.md", - "title": "Lesson 3: Drive a bounded change, test by test", - "summary": "Supervise Codex through seven small diffs — smallest change, no new dependencies, one failing test group at a time.", - "sha256": "41adb2f3560d29b38d7d1a8254a51197323637e589aa463e4c8417f9c61e8686" + "path": "courses/hands-on-with-openai-codex/lessons/L03.md", + "title": "Lesson 3: The bounded change, reviewed hunk by hunk", + "summary": "Drive the starter to green through bounded turns — and use the app's diff review and approvals to keep every change yours.", + "sha256": "0cb1e5e60b38236ac2c477155d422417e692c038a210f995922c0e7dc387c6cd" }, { "lang": "zh-CN", - "path": "courses/hands-on-with-openai-codex-cli/lessons/L03_cn.md", - "title": "第 3 课:按测试驱动一次有边界的变更", - "summary": "监督 Codex 完成七个小 diff——最小变更、不新增依赖、一次一组失败测试。", - "sha256": "4f440f36af4c3eba76bd307085a3bf48020931b168b5a04edc9a6c190ec02c10" + "path": "courses/hands-on-with-openai-codex/lessons/L03_cn.md", + "title": "第 3 课:有界改动,逐块审 diff", + "summary": "通过有界的轮次把 starter 推到全绿——并用应用内的 diff 审查与审批机制守住每一处改动。", + "sha256": "be8d68a96423506755e081aca4729ca7011cfc576f3998cad69abd8062760480" } ] }, { "id": "course-codex-cli-l04", "type": "course", - "content_version": 1, + "content_version": 2, "status": "reviewed", - "reviewed_on": "2026-09-12", + "reviewed_on": "2026-09-13", "locales": [ { "lang": "en-US", - "path": "courses/hands-on-with-openai-codex-cli/lessons/L04.md", - "title": "Lesson 4: Verify like an engineer, review like a skeptic", - "summary": "Complete the evidence loop — both verification directions, all three skins, a scope-creep diff review, and a written record of what is not proven.", - "sha256": "ac228870e7c4ef0df522664c27fb7895535c72090833c59c86e8b33e527a2703" + "path": "courses/hands-on-with-openai-codex/lessons/L04.md", + "title": "Lesson 4: Verify like a reviewer, not a spectator", + "summary": "Green tests are the floor, not the ceiling — run both suites, walk the scenario skins, and review the diff the way you would review a colleague's.", + "sha256": "b99b86470990cc15cd9d740cc88e0076b6cc7f51793e26627f98cb2bfff45506" }, { "lang": "zh-CN", - "path": "courses/hands-on-with-openai-codex-cli/lessons/L04_cn.md", - "title": "第 4 课:像工程师一样验证,像怀疑者一样审查", - "summary": "完成证据闭环——双向验证、三个皮肤、范围蔓延 diff 审查,以及一份写明未证明内容的记录。", - "sha256": "cb9d5ba62f101fa47d03740f5806930dd51986e310be04f255e93b422f9e1edc" + "path": "courses/hands-on-with-openai-codex/lessons/L04_cn.md", + "title": "第 4 课:像评审者一样验证,别当观众", + "summary": "测试全绿是地板不是天花板——两个套件都跑、三个皮肤都走一遍、按评审同事的标准审这份 diff。", + "sha256": "5e9994c4288119ddc388d9b75fb739ef13ac13255304903c8f9d6fb12df9645f" } ] }, { "id": "course-codex-cli-l05", "type": "course", + "content_version": 2, + "status": "reviewed", + "reviewed_on": "2026-09-13", + "locales": [ + { + "lang": "en-US", + "path": "courses/hands-on-with-openai-codex/lessons/L05.md", + "title": "Lesson 5: Take the workflow to your own project", + "summary": "The tool generalizes: task contract, bounded thread, approvals, diff review — run the same loop on a script you actually own.", + "sha256": "8964d7d543353de4cebaba40229e945a45aca208b6bbb4881443a0f2603e690a" + }, + { + "lang": "zh-CN", + "path": "courses/hands-on-with-openai-codex/lessons/L05_cn.md", + "title": "第 5 课:把工作流带回自己的项目", + "summary": "工具会泛化:任务契约、有界线程、审批、diff 评审——在你真正拥有的脚本上跑同一套闭环。", + "sha256": "8517bc536fedc8c79e0ff68b7209a91456d4f745107e2793f41b8978841d373c" + } + ] + }, + { + "id": "course-cursor", + "type": "course", "content_version": 1, "status": "reviewed", - "reviewed_on": "2026-09-12", + "reviewed_on": "2026-09-13", "locales": [ { "lang": "en-US", - "path": "courses/hands-on-with-openai-codex-cli/lessons/L05.md", - "title": "Lesson 5: Apply this to your own project", - "summary": "Port the workflow — not the code — to one script you own, with tests, a verify command, and honest limits.", - "sha256": "352ea47aa0ae5093e3d4f38f0f82218323fadf393dcc4da0f63a6851ea9dd6b4" + "path": "courses/hands-on-with-cursor/COURSE.md", + "title": "Hands-on with Cursor", + "summary": "The verified Python workflow — task contract, bounded change, objective verify.py evidence — taught hands-on in Cursor, where Agent mode runs the loop and you supervise diffs and project rules.", + "sha256": "e4543bbb100c2f4dd645c135b0f140cc09c51051c09a5825c229022cb985bdf4" }, { "lang": "zh-CN", - "path": "courses/hands-on-with-openai-codex-cli/lessons/L05_cn.md", - "title": "第 5 课:把这套方法带回你自己的项目", - "summary": "迁移工作流——而不是代码——到你拥有的一个脚本上,附测试、验证命令与诚实的局限说明。", - "sha256": "61565be72cae4e5ff14b5059bb108363d750a41e22073003d0d4de357a6944eb" + "path": "courses/hands-on-with-cursor/COURSE_cn.md", + "title": "上手 Cursor", + "summary": "经过验证的 Python 工作流——任务契约、有界改动、客观 verify.py 证据——在 Cursor 里手把手教:由 Agent 模式跑闭环,你监督 diff 与项目规则。", + "sha256": "a40bf93f87c12fbbc46c0895db42279caa74e0325518f3d4a6561efd7518b338" + } + ] + }, + { + "id": "course-cursor-l01", + "type": "course", + "content_version": 1, + "status": "reviewed", + "reviewed_on": "2026-09-13", + "locales": [ + { + "lang": "en-US", + "path": "courses/hands-on-with-cursor/lessons/L01.md", + "title": "Lesson 1: Reproduce the failure inside Cursor", + "summary": "Open the folder as a Cursor project, let Agent mode read the teaching contract, and make the failure objective with verify.py — inside the editor, not beside it.", + "sha256": "176848098e3b8a64cbd312c6a2e4a2cb49e4fcaa855b17ea43cc9be651922ab0" + }, + { + "lang": "zh-CN", + "path": "courses/hands-on-with-cursor/lessons/L01_cn.md", + "title": "第 1 课:在 Cursor 里复现故障", + "summary": "把文件夹作为 Cursor 项目打开,让 Agent 模式读教学契约,用 verify.py 让失败变得客观——在编辑器内完成,而不是在编辑器旁边。", + "sha256": "c07cec1f61a0ac751f50804e793ddb1a4de6026fd7d2b87f2955a2cabf27d3b2" + } + ] + }, + { + "id": "course-cursor-l02", + "type": "course", + "content_version": 1, + "status": "reviewed", + "reviewed_on": "2026-09-13", + "locales": [ + { + "lang": "en-US", + "path": "courses/hands-on-with-cursor/lessons/L02.md", + "title": "Lesson 2: Write the task contract, let .cursor/rules carry the rules", + "summary": "Turn \"make it better\" into a bounded, testable contract — then make it durable so every Cursor session starts with the same rules.", + "sha256": "aaf059429373464079c242dcf7946616bc7b40b0a1ba07480950ed5f8ba0dfaa" + }, + { + "lang": "zh-CN", + "path": "courses/hands-on-with-cursor/lessons/L02_cn.md", + "title": "第 2 课:写任务契约,让 .cursor/rules 承载规则", + "summary": "把「改好一点」变成有边界、可测试的契约——再让它持久化,让每个 Cursor 会话都从同一套规则开始。", + "sha256": "dd6012d7884e95644dd5ea59896cade1e8d572b503639a832445d141d9e475b1" + } + ] + }, + { + "id": "course-cursor-l03", + "type": "course", + "content_version": 1, + "status": "reviewed", + "reviewed_on": "2026-09-13", + "locales": [ + { + "lang": "en-US", + "path": "courses/hands-on-with-cursor/lessons/L03.md", + "title": "Lesson 3: The bounded change, reviewed hunk by hunk", + "summary": "Drive the starter to green through bounded turns — and use Cursor's diff review to keep every change yours.", + "sha256": "ff0006c574cee6f659e8ab72f4462b3c069bb858556819f9ba1ec83ea4bf7a66" + }, + { + "lang": "zh-CN", + "path": "courses/hands-on-with-cursor/lessons/L03_cn.md", + "title": "第 3 课:有界改动,逐块审 diff", + "summary": "通过有界的轮次把 starter 推到全绿——并用 Cursor 的 diff 审查守住每一处改动。", + "sha256": "753c49006c87d870fd5927ce91b52324f321f67cde01dbafcc263c320fdacad9" + } + ] + }, + { + "id": "course-cursor-l04", + "type": "course", + "content_version": 1, + "status": "reviewed", + "reviewed_on": "2026-09-13", + "locales": [ + { + "lang": "en-US", + "path": "courses/hands-on-with-cursor/lessons/L04.md", + "title": "Lesson 4: Verify like a reviewer, not a spectator", + "summary": "Green tests are the floor, not the ceiling — run both suites, walk the scenario skins, and review the accumulated diff the way you would review a colleague's.", + "sha256": "13ae3ea383d7dfd939d5a02028a6fa2e813cc5893b4a4bbdf1b294ff539141ef" + }, + { + "lang": "zh-CN", + "path": "courses/hands-on-with-cursor/lessons/L04_cn.md", + "title": "第 4 课:像评审者一样验证,别当观众", + "summary": "测试全绿是地板不是天花板——两个套件都跑、三个皮肤都走一遍、按评审同事的标准审这份累积 diff。", + "sha256": "54b31f4c2b8438d71a2c0dd4504deca2496c861d2e92e935f9a65e8c665f8d05" + } + ] + }, + { + "id": "course-cursor-l05", + "type": "course", + "content_version": 1, + "status": "reviewed", + "reviewed_on": "2026-09-13", + "locales": [ + { + "lang": "en-US", + "path": "courses/hands-on-with-cursor/lessons/L05.md", + "title": "Lesson 5: Take the workflow to your own project", + "summary": "The tool generalizes: task contract, bounded agent work, diff review — run the same loop on a script you actually own.", + "sha256": "b9e9dbad4f6de5b72497220eeaa0866170fa64b5cc1d1e2b548878d9d008d94d" + }, + { + "lang": "zh-CN", + "path": "courses/hands-on-with-cursor/lessons/L05_cn.md", + "title": "第 5 课:把工作流带回自己的项目", + "summary": "工具会泛化:任务契约、有界 Agent 工作、diff 评审——在你真正拥有的脚本上跑同一套闭环。", + "sha256": "263f61fe5e52141fc8e8a7e8ad8d72b187e6d203c6f47a4ae76ac8c938dd4e3c" } ] }, @@ -875,6 +1013,282 @@ } ] }, + { + "id": "course-deepseek-harness", + "type": "course", + "content_version": 1, + "status": "reviewed", + "reviewed_on": "2026-09-13", + "locales": [ + { + "lang": "en-US", + "path": "courses/hands-on-with-deepseek-harness/COURSE.md", + "title": "Hands-on with DeepSeek Harness", + "summary": "The verified Python workflow — task contract, bounded change, objective verify.py evidence — taught hands-on in DeepSeek Harness, where the agent loop is composed from plugins and every step lands in an inspectable trajectory.", + "sha256": "5d98c134e69fa157738bf06e558460a5a698f08e12d07560d56a9f8b2855252a" + }, + { + "lang": "zh-CN", + "path": "courses/hands-on-with-deepseek-harness/COURSE_cn.md", + "title": "上手 DeepSeek Harness", + "summary": "经过验证的 Python 工作流——任务契约、有界改动、客观 verify.py 证据——在 DeepSeek Harness 里手把手教:Agent 闭环由插件组装,每一步都落进可检查的轨迹日志。", + "sha256": "04664097ac975bea0316aee9091f76d70f491045b40bc90e63f816c2c193d761" + } + ] + }, + { + "id": "course-deepseek-harness-l01", + "type": "course", + "content_version": 1, + "status": "reviewed", + "reviewed_on": "2026-09-13", + "locales": [ + { + "lang": "en-US", + "path": "courses/hands-on-with-deepseek-harness/lessons/L01.md", + "title": "Lesson 1: Reproduce the failure, read it back from the trajectory", + "summary": "Run a harness-composed agent on the course folder, make the failure objective with verify.py — then read the append-only trajectory to see exactly what the agent did.", + "sha256": "7b5ef7dc5536e049372b58a4a36915bbeba4713c6bc3a4ec8ee0de3a62fc8b5c" + }, + { + "lang": "zh-CN", + "path": "courses/hands-on-with-deepseek-harness/lessons/L01_cn.md", + "title": "第 1 课:复现故障,从轨迹里读回来", + "summary": "让 Harness 组装的 Agent 跑在课程文件夹上,用 verify.py 让失败变得客观——再读只增轨迹,看清 Agent 到底做了什么。", + "sha256": "ffbe560ec8df1b9940dc078630408a84514aaa1702c9d52b1b6e8fc07222b806" + } + ] + }, + { + "id": "course-deepseek-harness-l02", + "type": "course", + "content_version": 1, + "status": "reviewed", + "reviewed_on": "2026-09-13", + "locales": [ + { + "lang": "en-US", + "path": "courses/hands-on-with-deepseek-harness/lessons/L02.md", + "title": "Lesson 2: Write the task contract, let cordis.yml carry the setup", + "summary": "Turn \"make it better\" into a bounded, testable contract — then make the working rules part of the composed agent, not a forgotten prompt.", + "sha256": "52c04fb47a4b0667b64632f872857f276f8f9001c80601d95475ec38b67c3bd3" + }, + { + "lang": "zh-CN", + "path": "courses/hands-on-with-deepseek-harness/lessons/L02_cn.md", + "title": "第 2 课:写任务契约,让 cordis.yml 承载配置", + "summary": "把「改好一点」变成有边界、可测试的契约——再把工作规则放进组装出来的 Agent 里,而不是会被遗忘的提示词。", + "sha256": "e681aec8275c6762a41c92e8a96efd161696069a2d33f4669066179bdf72579f" + } + ] + }, + { + "id": "course-deepseek-harness-l03", + "type": "course", + "content_version": 1, + "status": "reviewed", + "reviewed_on": "2026-09-13", + "locales": [ + { + "lang": "en-US", + "path": "courses/hands-on-with-deepseek-harness/lessons/L03.md", + "title": "Lesson 3: The bounded change, audited from the trajectory", + "summary": "Drive the starter to green through bounded steps — then prove the agent stayed in bounds by inspecting the append-only trajectory.", + "sha256": "0c361ebd8dc019b1b12c8877e76c6ba372d10276c741acaac9999f8832f0da7b" + }, + { + "lang": "zh-CN", + "path": "courses/hands-on-with-deepseek-harness/lessons/L03_cn.md", + "title": "第 3 课:有界改动,用轨迹做审计", + "summary": "通过有界的步骤把 starter 推到全绿——再检查只增轨迹,证明 Agent 没有越界。", + "sha256": "305088bf5a3833bab6055230328f666075742095809be20a119c7b53eeede903" + } + ] + }, + { + "id": "course-deepseek-harness-l04", + "type": "course", + "content_version": 1, + "status": "reviewed", + "reviewed_on": "2026-09-13", + "locales": [ + { + "lang": "en-US", + "path": "courses/hands-on-with-deepseek-harness/lessons/L04.md", + "title": "Lesson 4: Verify like a reviewer, not a spectator", + "summary": "Green tests are the floor, not the ceiling — run both suites, walk the scenario skins, and review the trajectory the way you would review a colleague's work.", + "sha256": "10163a3baeee6756ef8f3b60fc11d46fdb427e79c789a9e3312e25b9e1f6a5cc" + }, + { + "lang": "zh-CN", + "path": "courses/hands-on-with-deepseek-harness/lessons/L04_cn.md", + "title": "第 4 课:像评审者一样验证,别当观众", + "summary": "测试全绿是地板不是天花板——两个套件都跑、三个皮肤都走一遍、按评审同事工作的标准审这条轨迹。", + "sha256": "ea979e9b3827458f408fea3e7d7c8f83b8ed9e209e36d8682417ac31a6adb15d" + } + ] + }, + { + "id": "course-deepseek-harness-l05", + "type": "course", + "content_version": 1, + "status": "reviewed", + "reviewed_on": "2026-09-13", + "locales": [ + { + "lang": "en-US", + "path": "courses/hands-on-with-deepseek-harness/lessons/L05.md", + "title": "Lesson 5: Take the workflow to your own project", + "summary": "The pattern generalizes: task contract, composed agent, bounded steps, trajectory audit — run the same loop on a script you actually own.", + "sha256": "0e76a3df730783e55b3a0faa07aa8bd974f905c262f9f52bbc81db634d931cc6" + }, + { + "lang": "zh-CN", + "path": "courses/hands-on-with-deepseek-harness/lessons/L05_cn.md", + "title": "第 5 课:把工作流带回自己的项目", + "summary": "模式会泛化:任务契约、组装 Agent、有界步骤、轨迹审计——在你真正拥有的脚本上跑同一套闭环。", + "sha256": "428b9c925dcf232c238e3a7e8a44934e8d74f960b8126be38941b43aecc87dc8" + } + ] + }, + { + "id": "course-kimi-code", + "type": "course", + "content_version": 1, + "status": "reviewed", + "reviewed_on": "2026-09-13", + "locales": [ + { + "lang": "en-US", + "path": "courses/hands-on-with-kimi-code/COURSE.md", + "title": "Hands-on with Kimi Code", + "summary": "The verified Python workflow — task contract, bounded change, objective verify.py evidence — taught hands-on in Kimi Code, where built-in subagents split read-only exploration, planning, and the edits you approve.", + "sha256": "21c176b260918802fd71e2065b8fdfeef334f2ba896f98a6407f5d753500dbe1" + }, + { + "lang": "zh-CN", + "path": "courses/hands-on-with-kimi-code/COURSE_cn.md", + "title": "上手 Kimi Code", + "summary": "经过验证的 Python 工作流——任务契约、有界改动、客观 verify.py 证据——在 Kimi Code 里手把手教:内置子代理把只读勘察、规划和经你批准的编辑分在三条道上。", + "sha256": "9ecaf4e7cf782ada0a71201a2132c5f635c296568ce94f0573d8960516926e2a" + } + ] + }, + { + "id": "course-kimi-code-l01", + "type": "course", + "content_version": 1, + "status": "reviewed", + "reviewed_on": "2026-09-13", + "locales": [ + { + "lang": "en-US", + "path": "courses/hands-on-with-kimi-code/lessons/L01.md", + "title": "Lesson 1: Reproduce the failure, survey it with the read-only lane", + "summary": "Run kimi in the course folder, let the read-only explore subagent map the code, and make the failure objective with verify.py.", + "sha256": "1f22cfd82fa640440e2cba270490ccdb9b92f7b4e3d129879530f8d79b48b16e" + }, + { + "lang": "zh-CN", + "path": "courses/hands-on-with-kimi-code/lessons/L01_cn.md", + "title": "第 1 课:复现故障,用只读道做勘察", + "summary": "在课程文件夹里运行 kimi,让只读的 explore 子代理摸清代码,用 verify.py 让失败变得客观。", + "sha256": "f42a1d5a7898d710edb683c780c4638f262947d30f47e9463741d94523b35814" + } + ] + }, + { + "id": "course-kimi-code-l02", + "type": "course", + "content_version": 1, + "status": "reviewed", + "reviewed_on": "2026-09-13", + "locales": [ + { + "lang": "en-US", + "path": "courses/hands-on-with-kimi-code/lessons/L02.md", + "title": "Lesson 2: Write the task contract, plan it in the planning lane", + "summary": "Turn \"make it better\" into a bounded, testable contract — then let the plan subagent (no write, no shell) turn it into steps before anything edits.", + "sha256": "a6099e33340091887fad8b39743f7831809756a26830b638039c572b3f856dcb" + }, + { + "lang": "zh-CN", + "path": "courses/hands-on-with-kimi-code/lessons/L02_cn.md", + "title": "第 2 课:写任务契约,在规划道里拆步", + "summary": "把「改好一点」变成有边界、可测试的契约——再让 plan 子代理(无写无 shell)先把它拆成步骤,然后才许动文件。", + "sha256": "07296ecbd33c9bcfa38a392b8bdc10ca869f2cd9f1436c983df1f69036946b95" + } + ] + }, + { + "id": "course-kimi-code-l03", + "type": "course", + "content_version": 1, + "status": "reviewed", + "reviewed_on": "2026-09-13", + "locales": [ + { + "lang": "en-US", + "path": "courses/hands-on-with-kimi-code/lessons/L03.md", + "title": "Lesson 3: The bounded change, one contract line at a time", + "summary": "Drive the starter to green through the coder lane in bounded steps — and review every edit before the next one lands.", + "sha256": "c9a411e86737ee10dbfc9e1c5a600e22b27b85f355da7f8570bf42b8e6e8dfb3" + }, + { + "lang": "zh-CN", + "path": "courses/hands-on-with-kimi-code/lessons/L03_cn.md", + "title": "第 3 课:有界改动,一次一条契约行", + "summary": "通过 coder 道的有界步骤把 starter 推到全绿——下一处编辑落地前,先审完这一处。", + "sha256": "840c27b463e7b25555cf45b41c97849624265887e5777f0d6eb941cafad91aaf" + } + ] + }, + { + "id": "course-kimi-code-l04", + "type": "course", + "content_version": 1, + "status": "reviewed", + "reviewed_on": "2026-09-13", + "locales": [ + { + "lang": "en-US", + "path": "courses/hands-on-with-kimi-code/lessons/L04.md", + "title": "Lesson 4: Verify like a reviewer, not a spectator", + "summary": "Green tests are the floor, not the ceiling — run both suites, walk the scenario skins, and review the accumulated diff the way you would review a colleague's.", + "sha256": "ccb695a57c39ca61d5c412b54b50cf83cb1984a783178605997626a95d2fde2d" + }, + { + "lang": "zh-CN", + "path": "courses/hands-on-with-kimi-code/lessons/L04_cn.md", + "title": "第 4 课:像评审者一样验证,别当观众", + "summary": "测试全绿是地板不是天花板——两个套件都跑、三个皮肤都走一遍、按评审同事的标准审这份累积 diff。", + "sha256": "590857d8349d73071796a856601ac34508ae8291792b10887e45ad1e5e475f8f" + } + ] + }, + { + "id": "course-kimi-code-l05", + "type": "course", + "content_version": 1, + "status": "reviewed", + "reviewed_on": "2026-09-13", + "locales": [ + { + "lang": "en-US", + "path": "courses/hands-on-with-kimi-code/lessons/L05.md", + "title": "Lesson 5: Take the workflow to your own project", + "summary": "The pattern generalizes: contract first, read-only survey, planning lane, bounded edits, written verification — run the same loop on a script you actually own.", + "sha256": "e3b502add27090e1fd6730d0e4ca0c70b55c18d66bcb3ade452beb2513f95c24" + }, + { + "lang": "zh-CN", + "path": "courses/hands-on-with-kimi-code/lessons/L05_cn.md", + "title": "第 5 课:把工作流带回自己的项目", + "summary": "模式会泛化:契约先行、只读勘察、规划道拆步、有界编辑、书面验证——在你真正拥有的脚本上跑同一套闭环。", + "sha256": "e1403c1fc31c8304f82219c214afe574ff5dc69f41b3cab7a0c5002c8b8774f2" + } + ] + }, { "id": "course-mcp-tools", "type": "course", @@ -1151,6 +1565,144 @@ } ] }, + { + "id": "course-zcode", + "type": "course", + "content_version": 1, + "status": "reviewed", + "reviewed_on": "2026-09-13", + "locales": [ + { + "lang": "en-US", + "path": "courses/hands-on-with-zcode/COURSE.md", + "title": "Hands-on with ZCode", + "summary": "The verified Python workflow — task contract, bounded change, objective verify.py evidence — taught hands-on in the ZCode desktop workspace, where Goal Mode decomposes the work and the Git state shows every change.", + "sha256": "e8a90d90aa5daa83d3dd116eb53676f2d3c9bdf78d6cb12da4b732b703c976eb" + }, + { + "lang": "zh-CN", + "path": "courses/hands-on-with-zcode/COURSE_cn.md", + "title": "上手 ZCode", + "summary": "经过验证的 Python 工作流——任务契约、有界改动、客观 verify.py 证据——在 ZCode 桌面工作区里手把手教:Goal 模式拆解任务,Git 状态展示每一处改动。", + "sha256": "2d33bc8c5b449b929de8d0de7628e2fa8aa900efb58683b373ea9b0013a48e3d" + } + ] + }, + { + "id": "course-zcode-l01", + "type": "course", + "content_version": 1, + "status": "reviewed", + "reviewed_on": "2026-09-13", + "locales": [ + { + "lang": "en-US", + "path": "courses/hands-on-with-zcode/lessons/L01.md", + "title": "Lesson 1: Reproduce the failure inside the workspace", + "summary": "Open the folder as a ZCode workspace, give the agent its first task, and make the failure objective with verify.py — inside the app, not beside it.", + "sha256": "f3ba2aeff5234732dd2f6d72d99eee7635956584a7f1ce270a0569dc34c4c628" + }, + { + "lang": "zh-CN", + "path": "courses/hands-on-with-zcode/lessons/L01_cn.md", + "title": "第 1 课:在工作区里复现故障", + "summary": "把文件夹作为 ZCode 工作区打开,给 Agent 第一个任务,用 verify.py 让失败变得客观——在应用内完成,而不是在应用旁边。", + "sha256": "fa359a53e2743a7f55076a0a1b046db7d0fb340579becf3d8ad538e1a41ce4f2" + } + ] + }, + { + "id": "course-zcode-l02", + "type": "course", + "content_version": 1, + "status": "reviewed", + "reviewed_on": "2026-09-13", + "locales": [ + { + "lang": "en-US", + "path": "courses/hands-on-with-zcode/lessons/L02.md", + "title": "Lesson 2: Write the task contract, let Goal Mode hold the plan", + "summary": "Turn \"make it better\" into a bounded, testable contract — then let Goal Mode decompose it into tasks you can watch land one by one.", + "sha256": "d6ae0f3b3a62a196f3f2b476ced1b7f8c7ad2b87e26be82fd0e62667c15e3710" + }, + { + "lang": "zh-CN", + "path": "courses/hands-on-with-zcode/lessons/L02_cn.md", + "title": "第 2 课:写任务契约,让 Goal 模式承载计划", + "summary": "把「改好一点」变成有边界、可测试的契约——再让 Goal 模式把它拆成你能看着逐一落地的任务。", + "sha256": "afdbf2423f916b2dbae40e7a94d7d6b6d65d73ad3fc7536cab19b3a678cf8a8e" + } + ] + }, + { + "id": "course-zcode-l03", + "type": "course", + "content_version": 1, + "status": "reviewed", + "reviewed_on": "2026-09-13", + "locales": [ + { + "lang": "en-US", + "path": "courses/hands-on-with-zcode/lessons/L03.md", + "title": "Lesson 3: The bounded change, watched task by task", + "summary": "Drive the starter to green through the goal's task list — and use the workspace Git state to review every change that lands.", + "sha256": "448c845acb70a0d4c18334d6f216f80a3a47e8aa9d6b7d39518d136567112d8e" + }, + { + "lang": "zh-CN", + "path": "courses/hands-on-with-zcode/lessons/L03_cn.md", + "title": "第 3 课:有界改动,逐任务盯守", + "summary": "沿着目标的任务列表把 starter 推到全绿——用工作区 Git 状态审每一处落地的改动。", + "sha256": "e571522f7b5ab9e68b5c1c854029e36fea64b278dcd75373dbd78129b679ab2a" + } + ] + }, + { + "id": "course-zcode-l04", + "type": "course", + "content_version": 1, + "status": "reviewed", + "reviewed_on": "2026-09-13", + "locales": [ + { + "lang": "en-US", + "path": "courses/hands-on-with-zcode/lessons/L04.md", + "title": "Lesson 4: Verify like a reviewer, not a spectator", + "summary": "Green tests are the floor, not the ceiling — run both suites, walk the scenario skins, and review the accumulated Git diff the way you would review a colleague's.", + "sha256": "005f3a1f1fb638f9751b87e459988f22b4d4058f4d9e89d069ba90c949779f16" + }, + { + "lang": "zh-CN", + "path": "courses/hands-on-with-zcode/lessons/L04_cn.md", + "title": "第 4 课:像评审者一样验证,别当观众", + "summary": "测试全绿是地板不是天花板——两个套件都跑、三个皮肤都走一遍、按评审同事的标准审这份累积 Git diff。", + "sha256": "657f9b5f3b31b9ba7f0ca1d15b9e5a58e1aac5971343ae049fe5425783638471" + } + ] + }, + { + "id": "course-zcode-l05", + "type": "course", + "content_version": 1, + "status": "reviewed", + "reviewed_on": "2026-09-13", + "locales": [ + { + "lang": "en-US", + "path": "courses/hands-on-with-zcode/lessons/L05.md", + "title": "Lesson 5: Take the workflow to your own project", + "summary": "The pattern generalizes: contract as goal, task list, bounded execution, Git-state review — run the same loop on a project you actually own.", + "sha256": "60e68f537ddfd30ff271e995d9be15e962ad641ca02cdababf1d1cf363773ca3" + }, + { + "lang": "zh-CN", + "path": "courses/hands-on-with-zcode/lessons/L05_cn.md", + "title": "第 5 课:把工作流带回自己的项目", + "summary": "模式会泛化:契约变目标、任务列表、有界执行、Git 状态审查——在你真正拥有的项目上跑同一套闭环。", + "sha256": "1adee0b75e4d418005cc9aafdba92a0cd70d6d4f9175ef0972cd874258670602" + } + ] + }, { "id": "example-async-fetcher", "type": "example", @@ -1338,46 +1890,46 @@ { "id": "path-foundation-challenge", "type": "path", - "content_version": 1, + "content_version": 2, "status": "reviewed", - "reviewed_on": "2026-09-12", + "reviewed_on": "2026-09-13", "locales": [ { "lang": "en-US", "path": "paths/foundation/modules/route-challenge.md", "title": "Route challenge: dual-agent, double-verified", - "summary": "Foundation path capstone — run the same task through Claude Code and Codex CLI and verify both outputs yourself.", - "sha256": "0c56353b6b98acb6a15f575ab57fc513f367716754c27aae7a042cccfeabdbc4" + "summary": "Foundation path capstone — run the same task through any two of the six path agents and verify both outputs yourself.", + "sha256": "5dd9a96fb85e3582b57455b1714827ff8503a53cef65eaf5159666771542bf7f" }, { "lang": "zh-CN", "path": "paths/foundation/modules/route-challenge_cn.md", "title": "路线挑战:双 Agent、双重验证", - "summary": "基础路线收官挑战——同一任务分别用 Claude Code 与 Codex CLI 完成并亲手双重验证。", - "sha256": "efb3d075163c2051dc8908072bc9da63ac13df17b4380f2ceac3fe17db6f2f33" + "summary": "基础路线收官——把同一个任务分别交给六个路线 Agent 中任选的两个,并亲手验证两份产出。", + "sha256": "eafb3dc130a96a8ba6b8128c4353c689f2590abbfe2ecd8c06fd00940f7e0ea5" } ] }, { "id": "path-foundation-m0", "type": "path", - "content_version": 1, + "content_version": 2, "status": "reviewed", - "reviewed_on": "2026-09-12", + "reviewed_on": "2026-09-13", "locales": [ { "lang": "en-US", "path": "paths/foundation/modules/m0-orientation.md", "title": "The agent tool landscape", - "summary": "Foundation path module 0 — orientation on Claude Code, Codex CLI, and Cursor; install one agent and observe its evidence habits.", - "sha256": "2c40a01bff3ab9cf40c6f453201a381d4a4d02ebc7246af46984b2548496f40f" + "summary": "Foundation path module 0 — orientation across six agentic coding tools; pick the one that fits your environment and observe its evidence habits.", + "sha256": "16f44353eb09b2292d3ff49af6bde9906a865a471a8818301ebb9b7260454e1a" }, { "lang": "zh-CN", "path": "paths/foundation/modules/m0-orientation_cn.md", "title": "Agent 工具生态总览", - "summary": "基础路线模块 0——Claude Code、Codex CLI、Cursor 生态导览;装一个 Agent 并观察它的证据习惯。", - "sha256": "4d6b4ecbc2eb0d0ef5c2e9522466f0c9ace056bd4ad8db4a9e116aa5916e64bb" + "summary": "基础路线模块 0——六个 Agentic 编程工具生态导览;按你的工作环境选一个,并观察它的证据习惯。", + "sha256": "9d416d3f68766ee6d241dff0e00edb21a654ba27c8013ff4de8e23ff90852a1e" } ] }, diff --git a/course-solutions.json b/course-solutions.json new file mode 100644 index 0000000..c753681 --- /dev/null +++ b/course-solutions.json @@ -0,0 +1,65 @@ +{ + "schema_version": 1, + "courses": [ + { + "slug": "agent-rules-single-source", + "course_id": "course-agent-rules", + "solution_sha256": "92757cc9943502021864bab3d508a168a4239b369b8a8ce55d6d3df5d1ca1fb8" + }, + { + "slug": "da-eda", + "course_id": "course-da-eda", + "solution_sha256": "9eb0075ac148b788d9756ed4bf45242cb1c085601669cc7f0fe84f3af015397d" + }, + { + "slug": "da-report", + "course_id": "course-da-report", + "solution_sha256": "f825baf3a1d4aac1292b5a24f3f2b3465cb02b5e9f79ebda6843c5e560f38629" + }, + { + "slug": "da-visualization", + "course_id": "course-da-visualization", + "solution_sha256": "04d39d6f0c09e8c46d83835cf8d838b59a6ea3933828233f59b1b6ea2d935f69" + }, + { + "slug": "hands-on-python-with-claude-code", + "course_id": "course-claude-code", + "solution_sha256": "d0aa4b9a5486f9d50abf2fe34f9166815174591e7fc0c2fb23ae3111a5a16699" + }, + { + "slug": "hands-on-with-cursor", + "course_id": "course-cursor", + "solution_sha256": "d0aa4b9a5486f9d50abf2fe34f9166815174591e7fc0c2fb23ae3111a5a16699" + }, + { + "slug": "hands-on-with-deepseek-harness", + "course_id": "course-deepseek-harness", + "solution_sha256": "d0aa4b9a5486f9d50abf2fe34f9166815174591e7fc0c2fb23ae3111a5a16699" + }, + { + "slug": "hands-on-with-kimi-code", + "course_id": "course-kimi-code", + "solution_sha256": "d0aa4b9a5486f9d50abf2fe34f9166815174591e7fc0c2fb23ae3111a5a16699" + }, + { + "slug": "hands-on-with-openai-codex", + "course_id": "course-codex-cli", + "solution_sha256": "d0aa4b9a5486f9d50abf2fe34f9166815174591e7fc0c2fb23ae3111a5a16699" + }, + { + "slug": "hands-on-with-zcode", + "course_id": "course-zcode", + "solution_sha256": "d0aa4b9a5486f9d50abf2fe34f9166815174591e7fc0c2fb23ae3111a5a16699" + }, + { + "slug": "mcp-server-in-python", + "course_id": "course-mcp-tools", + "solution_sha256": "e9a0c348e12bd1f04d83783689f299e3ece8884f3f8a5be2f8a40655e211b790" + }, + { + "slug": "verifying-ai-generated-code", + "course_id": "course-verify-ship", + "solution_sha256": "3426cef9b1a5ee074e3839bb152b0ddff186e3c9a18928d99099795654073e16" + } + ] +} diff --git a/courses/agent-rules-single-source/verify.py b/courses/agent-rules-single-source/verify.py index bb1ab86..91cb081 100644 --- a/courses/agent-rules-single-source/verify.py +++ b/courses/agent-rules-single-source/verify.py @@ -17,6 +17,15 @@ ROOT = Path(__file__).resolve().parent +# Optional shared claim-receipt producer (docs/CLAIM-RECEIPT.md). The course +# folder still verifies standalone — without the tools/ sibling or without +# FLYPYTHON_CLAIM_SECRET set, verify.py behaves exactly as before. +try: + sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "tools")) + import claim_receipt +except ImportError: + claim_receipt = None + EXPECTED_STARTER_FAILURES = ( "test_thin_pointer_that_references_agents_md_is_a_pointer", "test_missing_agents_md_is_reported", @@ -57,9 +66,20 @@ def _run_suite(implementation): env=environment, check=False, capture_output=True, text=True, ) -def run_progress(as_json): +def _suite_stats(run): + """(tests_run, duration_ms) for a completed suite subprocess.""" + tests = claim_receipt.count_tests(run.stderr) if claim_receipt else 0 + return tests, int(getattr(run, "_elapsed_ms", 0)) + + +def run_progress(as_json, receipt_out=None): + import time + t0 = time.monotonic() starter = _run_suite("starter") + starter_ms = int((time.monotonic() - t0) * 1000) + t0 = time.monotonic() solution = _run_suite("solution") + solution_ms = int((time.monotonic() - t0) * 1000) starter_ok = starter.returncode == 0 solution_ok = solution.returncode == 0 rows = [] @@ -79,21 +99,51 @@ def run_progress(as_json): row["kind"] = kind row["claim_code"] = code rows.append(row) + secret = claim_receipt.receipts_enabled() if claim_receipt else None + receipts = [] + if secret: + starter_tests = claim_receipt.count_tests(starter.stderr) + solution_tests = claim_receipt.count_tests(solution.stderr) + solution_hash = claim_receipt.solution_sha256(ROOT) + for checkpoint in CHECKPOINTS: + gate = checkpoint["gate"] + if gate == "starter-suite": + passed, tests, ms = starter_ok, starter_tests, starter_ms + elif gate == "both-suites": + passed = starter_ok and solution_ok + tests, ms = starter_tests + solution_tests, starter_ms + solution_ms + else: + continue + receipts.append(claim_receipt.make_receipt( + COURSE_ID, checkpoint["id"], passed=passed, tests=tests, + duration_ms=ms, impl_dir=ROOT / "starter", + solution_hash=solution_hash, secret=secret)) if as_json: - print(json.dumps({"course": COURSE_ID, + document = {"course": COURSE_ID, "starter_suite_passed": starter_ok, "solution_suite_passed": solution_ok, - "checkpoints": rows}, ensure_ascii=False, indent=2)) + "checkpoints": rows} + if secret: + document["receipts"] = receipts + print(json.dumps(document, ensure_ascii=False, indent=2)) else: - starter_state = "通过" if starter_ok else "未通过" - solution_state = "通过" if solution_ok else "未通过" - print("课程 " + COURSE_ID) - print("实现状态: starter " + starter_state + " / solution " + solution_state) + starter_state = "passed" if starter_ok else "failed" + solution_state = "passed" if solution_ok else "failed" + print("Course " + COURSE_ID) + print("Suites: starter " + starter_state + " / solution " + solution_state) for row in rows: - state = row["status"] + ("(自报)" if row["kind"] == "attested" else "") - code = "认领码 " + row["claim_code"] if row["claim_code"] else "—" + state = row["status"] + (" (self-attested)" if row["kind"] == "attested" else "") + code = "claim code " + row["claim_code"] if row["claim_code"] else "—" print(" " + row["id"] + " " + row["title"] + " [" + state + "] " + code) - print("认领码是自我报告的证据,在 flypython.com 记录;绝非证书。") + print("Claim codes are self-reported evidence, recorded at flypython.com; never a certificate.") + if secret: + print(f"Signed run receipts prepared for {len(receipts)} gated checkpoint(s);" + " submit each with its claim to mark it as a local-run receipt.") + if receipt_out and secret: + Path(receipt_out).write_text( + json.dumps({"receipts": receipts}, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8") + print(f"Wrote {len(receipts)} receipt(s) to {receipt_out}", file=sys.stderr) return 0 def main() -> int: @@ -101,10 +151,11 @@ def main() -> int: parser.add_argument("implementation", choices=("progress", "starter", "solution")) parser.add_argument("--json", action="store_true") parser.add_argument("--expect-failure", action="store_true") + parser.add_argument("--receipt-out", metavar="PATH", help="write signed run receipts JSON (requires FLYPYTHON_CLAIM_SECRET)") args = parser.parse_args() if args.implementation == "progress": - return run_progress(args.json) + return run_progress(args.json, args.receipt_out) command = [sys.executable, "-m", "unittest", "discover", "-s", str(ROOT / "tests")] environment = os.environ.copy() @@ -132,6 +183,10 @@ def main() -> int: "requirement, no drift reporting, and CLI exit codes never signal failure." ) return 0 + if result.returncode == 0: + print(f"{args.implementation}: all tests passed") + else: + sys.stderr.write((result.stderr or "") or (result.stdout or "")) return result.returncode diff --git a/courses/da-eda/verify.py b/courses/da-eda/verify.py index bd0a1cf..fff86d0 100644 --- a/courses/da-eda/verify.py +++ b/courses/da-eda/verify.py @@ -25,6 +25,15 @@ ROOT = Path(__file__).resolve().parent +# Optional shared claim-receipt producer (docs/CLAIM-RECEIPT.md). The course +# folder still verifies standalone — without the tools/ sibling or without +# FLYPYTHON_CLAIM_SECRET set, verify.py behaves exactly as before. +try: + sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "tools")) + import claim_receipt +except ImportError: + claim_receipt = None + EXPECTED_STARTER_FAILURES = ( "test_load_returns_dataframe", "test_load_has_expected_columns", @@ -84,13 +93,20 @@ def _run_suite(implementation): ) -def run_progress(as_json): +def run_progress(as_json, receipt_out=None): + starter = solution = None + starter_ms = solution_ms = 0 if not _deps_available(): _deps_hint() starter_ok = solution_ok = False else: + import time + t0 = time.monotonic() starter = _run_suite("starter") + starter_ms = int((time.monotonic() - t0) * 1000) + t0 = time.monotonic() solution = _run_suite("solution") + solution_ms = int((time.monotonic() - t0) * 1000) starter_ok = starter.returncode == 0 solution_ok = solution.returncode == 0 rows = [] @@ -110,11 +126,33 @@ def run_progress(as_json): row["kind"] = kind row["claim_code"] = code rows.append(row) + secret = claim_receipt.receipts_enabled() if claim_receipt else None + receipts = [] + if secret: + starter_tests = claim_receipt.count_tests(starter.stderr) if starter else 0 + solution_tests = claim_receipt.count_tests(solution.stderr) if solution else 0 + solution_hash = claim_receipt.solution_sha256(ROOT) + for checkpoint in CHECKPOINTS: + gate = checkpoint["gate"] + if gate == "starter-suite": + passed, tests, ms = starter_ok, starter_tests, starter_ms + elif gate == "both-suites": + passed = starter_ok and solution_ok + tests, ms = starter_tests + solution_tests, starter_ms + solution_ms + else: + continue + receipts.append(claim_receipt.make_receipt( + COURSE_ID, checkpoint["id"], passed=passed, tests=tests, + duration_ms=ms, impl_dir=ROOT / "starter", + solution_hash=solution_hash, secret=secret)) if as_json: - print(json.dumps({"course": COURSE_ID, + document = {"course": COURSE_ID, "starter_suite_passed": starter_ok, "solution_suite_passed": solution_ok, - "checkpoints": rows}, ensure_ascii=False, indent=2)) + "checkpoints": rows} + if secret: + document["receipts"] = receipts + print(json.dumps(document, ensure_ascii=False, indent=2)) else: starter_state = "passed" if starter_ok else "not passed" solution_state = "passed" if solution_ok else "not passed" @@ -122,9 +160,17 @@ def run_progress(as_json): print("Suites: starter " + starter_state + " / solution " + solution_state) for row in rows: state = row["status"] + (" (self-attested)" if row["kind"] == "attested" else "") - code = "claim " + row["claim_code"] if row["claim_code"] else "—" + code = "claim code " + row["claim_code"] if row["claim_code"] else "—" print(" " + row["id"] + " " + row["title"] + " [" + state + "] " + code) - print("Claim codes are self-reported evidence recorded on flypython.com — never a certificate.") + print("Claim codes are self-reported evidence, recorded at flypython.com; never a certificate.") + if secret: + print(f"Signed run receipts prepared for {len(receipts)} gated checkpoint(s);" + " submit each with its claim to mark it as a local-run receipt.") + if receipt_out and secret: + Path(receipt_out).write_text( + json.dumps({"receipts": receipts}, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8") + print(f"Wrote {len(receipts)} receipt(s) to {receipt_out}", file=sys.stderr) return 0 @@ -133,10 +179,11 @@ def main() -> int: parser.add_argument("implementation", choices=("progress", "starter", "solution")) parser.add_argument("--json", action="store_true") parser.add_argument("--expect-failure", action="store_true") + parser.add_argument("--receipt-out", metavar="PATH", help="write signed run receipts JSON (requires FLYPYTHON_CLAIM_SECRET)") args = parser.parse_args() if args.implementation == "progress": - return run_progress(args.json) + return run_progress(args.json, args.receipt_out) if not _deps_available(): _deps_hint() @@ -170,6 +217,10 @@ def main() -> int: "because the analysis functions are not implemented." ) return 0 + if result.returncode == 0: + print(f"{args.implementation}: all tests passed") + else: + sys.stderr.write((result.stderr or "") or (result.stdout or "")) return result.returncode diff --git a/courses/da-report/verify.py b/courses/da-report/verify.py index 3ffda56..d6cf99c 100644 --- a/courses/da-report/verify.py +++ b/courses/da-report/verify.py @@ -19,6 +19,15 @@ ROOT = Path(__file__).resolve().parent +# Optional shared claim-receipt producer (docs/CLAIM-RECEIPT.md). The course +# folder still verifies standalone — without the tools/ sibling or without +# FLYPYTHON_CLAIM_SECRET set, verify.py behaves exactly as before. +try: + sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "tools")) + import claim_receipt +except ImportError: + claim_receipt = None + EXPECTED_STARTER_FAILURES = ( "test_load_returns_dict", "test_metrics_match_inputs", @@ -59,9 +68,16 @@ def _run_suite(implementation): ) -def run_progress(as_json): - starter_ok = _run_suite("starter").returncode == 0 - solution_ok = _run_suite("solution").returncode == 0 +def run_progress(as_json, receipt_out=None): + import time + t0 = time.monotonic() + starter = _run_suite("starter") + starter_ms = int((time.monotonic() - t0) * 1000) + t0 = time.monotonic() + solution = _run_suite("solution") + solution_ms = int((time.monotonic() - t0) * 1000) + starter_ok = starter.returncode == 0 + solution_ok = solution.returncode == 0 rows = [] for checkpoint in CHECKPOINTS: gate = checkpoint["gate"] @@ -79,11 +95,33 @@ def run_progress(as_json): row["kind"] = kind row["claim_code"] = code rows.append(row) + secret = claim_receipt.receipts_enabled() if claim_receipt else None + receipts = [] + if secret: + starter_tests = claim_receipt.count_tests(starter.stderr) if starter else 0 + solution_tests = claim_receipt.count_tests(solution.stderr) if solution else 0 + solution_hash = claim_receipt.solution_sha256(ROOT) + for checkpoint in CHECKPOINTS: + gate = checkpoint["gate"] + if gate == "starter-suite": + passed, tests, ms = starter_ok, starter_tests, starter_ms + elif gate == "both-suites": + passed = starter_ok and solution_ok + tests, ms = starter_tests + solution_tests, starter_ms + solution_ms + else: + continue + receipts.append(claim_receipt.make_receipt( + COURSE_ID, checkpoint["id"], passed=passed, tests=tests, + duration_ms=ms, impl_dir=ROOT / "starter", + solution_hash=solution_hash, secret=secret)) if as_json: - print(json.dumps({"course": COURSE_ID, + document = {"course": COURSE_ID, "starter_suite_passed": starter_ok, "solution_suite_passed": solution_ok, - "checkpoints": rows}, ensure_ascii=False, indent=2)) + "checkpoints": rows} + if secret: + document["receipts"] = receipts + print(json.dumps(document, ensure_ascii=False, indent=2)) else: starter_state = "passed" if starter_ok else "not passed" solution_state = "passed" if solution_ok else "not passed" @@ -91,9 +129,17 @@ def run_progress(as_json): print("Suites: starter " + starter_state + " / solution " + solution_state) for row in rows: state = row["status"] + (" (self-attested)" if row["kind"] == "attested" else "") - code = "claim " + row["claim_code"] if row["claim_code"] else "—" + code = "claim code " + row["claim_code"] if row["claim_code"] else "—" print(" " + row["id"] + " " + row["title"] + " [" + state + "] " + code) - print("Claim codes are self-reported evidence recorded on flypython.com — never a certificate.") + print("Claim codes are self-reported evidence, recorded at flypython.com; never a certificate.") + if secret: + print(f"Signed run receipts prepared for {len(receipts)} gated checkpoint(s);" + " submit each with its claim to mark it as a local-run receipt.") + if receipt_out and secret: + Path(receipt_out).write_text( + json.dumps({"receipts": receipts}, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8") + print(f"Wrote {len(receipts)} receipt(s) to {receipt_out}", file=sys.stderr) return 0 @@ -102,10 +148,11 @@ def main() -> int: parser.add_argument("implementation", choices=("progress", "starter", "solution")) parser.add_argument("--json", action="store_true") parser.add_argument("--expect-failure", action="store_true") + parser.add_argument("--receipt-out", metavar="PATH", help="write signed run receipts JSON (requires FLYPYTHON_CLAIM_SECRET)") args = parser.parse_args() if args.implementation == "progress": - return run_progress(args.json) + return run_progress(args.json, args.receipt_out) environment = os.environ.copy() environment["PYTHONPATH"] = str(ROOT / args.implementation) @@ -127,6 +174,10 @@ def main() -> int: return 1 print("Expected starter state reproduced: report functions unimplemented.") return 0 + if result.returncode == 0: + print(f"{args.implementation}: all tests passed") + else: + sys.stderr.write((result.stderr or "") or (result.stdout or "")) return result.returncode diff --git a/courses/da-visualization/verify.py b/courses/da-visualization/verify.py index d559859..56c2759 100644 --- a/courses/da-visualization/verify.py +++ b/courses/da-visualization/verify.py @@ -21,6 +21,15 @@ ROOT = Path(__file__).resolve().parent +# Optional shared claim-receipt producer (docs/CLAIM-RECEIPT.md). The course +# folder still verifies standalone — without the tools/ sibling or without +# FLYPYTHON_CLAIM_SECRET set, verify.py behaves exactly as before. +try: + sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "tools")) + import claim_receipt +except ImportError: + claim_receipt = None + EXPECTED_STARTER_FAILURES = ( "test_load_returns_rows", "test_region_totals", @@ -73,13 +82,22 @@ def _run_suite(implementation): ) -def run_progress(as_json): +def run_progress(as_json, receipt_out=None): + starter = solution = None + starter_ms = solution_ms = 0 if not _deps_available(): _deps_hint() starter_ok = solution_ok = False else: - starter_ok = _run_suite("starter").returncode == 0 - solution_ok = _run_suite("solution").returncode == 0 + import time + t0 = time.monotonic() + starter = _run_suite("starter") + starter_ms = int((time.monotonic() - t0) * 1000) + t0 = time.monotonic() + solution = _run_suite("solution") + solution_ms = int((time.monotonic() - t0) * 1000) + starter_ok = starter.returncode == 0 + solution_ok = solution.returncode == 0 rows = [] for checkpoint in CHECKPOINTS: gate = checkpoint["gate"] @@ -97,11 +115,33 @@ def run_progress(as_json): row["kind"] = kind row["claim_code"] = code rows.append(row) + secret = claim_receipt.receipts_enabled() if claim_receipt else None + receipts = [] + if secret: + starter_tests = claim_receipt.count_tests(starter.stderr) if starter else 0 + solution_tests = claim_receipt.count_tests(solution.stderr) if solution else 0 + solution_hash = claim_receipt.solution_sha256(ROOT) + for checkpoint in CHECKPOINTS: + gate = checkpoint["gate"] + if gate == "starter-suite": + passed, tests, ms = starter_ok, starter_tests, starter_ms + elif gate == "both-suites": + passed = starter_ok and solution_ok + tests, ms = starter_tests + solution_tests, starter_ms + solution_ms + else: + continue + receipts.append(claim_receipt.make_receipt( + COURSE_ID, checkpoint["id"], passed=passed, tests=tests, + duration_ms=ms, impl_dir=ROOT / "starter", + solution_hash=solution_hash, secret=secret)) if as_json: - print(json.dumps({"course": COURSE_ID, + document = {"course": COURSE_ID, "starter_suite_passed": starter_ok, "solution_suite_passed": solution_ok, - "checkpoints": rows}, ensure_ascii=False, indent=2)) + "checkpoints": rows} + if secret: + document["receipts"] = receipts + print(json.dumps(document, ensure_ascii=False, indent=2)) else: starter_state = "passed" if starter_ok else "not passed" solution_state = "passed" if solution_ok else "not passed" @@ -109,9 +149,17 @@ def run_progress(as_json): print("Suites: starter " + starter_state + " / solution " + solution_state) for row in rows: state = row["status"] + (" (self-attested)" if row["kind"] == "attested" else "") - code = "claim " + row["claim_code"] if row["claim_code"] else "—" + code = "claim code " + row["claim_code"] if row["claim_code"] else "—" print(" " + row["id"] + " " + row["title"] + " [" + state + "] " + code) - print("Claim codes are self-reported evidence recorded on flypython.com — never a certificate.") + print("Claim codes are self-reported evidence, recorded at flypython.com; never a certificate.") + if secret: + print(f"Signed run receipts prepared for {len(receipts)} gated checkpoint(s);" + " submit each with its claim to mark them as a local-run receipt.") + if receipt_out and secret: + Path(receipt_out).write_text( + json.dumps({"receipts": receipts}, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8") + print(f"Wrote {len(receipts)} receipt(s) to {receipt_out}", file=sys.stderr) return 0 @@ -120,10 +168,11 @@ def main() -> int: parser.add_argument("implementation", choices=("progress", "starter", "solution")) parser.add_argument("--json", action="store_true") parser.add_argument("--expect-failure", action="store_true") + parser.add_argument("--receipt-out", metavar="PATH", help="write signed run receipts JSON (requires FLYPYTHON_CLAIM_SECRET)") args = parser.parse_args() if args.implementation == "progress": - return run_progress(args.json) + return run_progress(args.json, args.receipt_out) if not _deps_available(): _deps_hint() @@ -149,6 +198,10 @@ def main() -> int: return 1 print("Expected starter state reproduced: chart functions unimplemented.") return 0 + if result.returncode == 0: + print(f"{args.implementation}: all tests passed") + else: + sys.stderr.write((result.stderr or "") or (result.stdout or "")) return result.returncode diff --git a/courses/hands-on-python-with-claude-code/core-group b/courses/hands-on-python-with-claude-code/core-group new file mode 100644 index 0000000..b56b0eb --- /dev/null +++ b/courses/hands-on-python-with-claude-code/core-group @@ -0,0 +1 @@ +report-tool diff --git a/courses/hands-on-python-with-claude-code/verify.py b/courses/hands-on-python-with-claude-code/verify.py index 60b7750..a088855 100644 --- a/courses/hands-on-python-with-claude-code/verify.py +++ b/courses/hands-on-python-with-claude-code/verify.py @@ -1,10 +1,11 @@ #!/usr/bin/env python3 """Run the course contract against starter or solution. -Objective completion evidence for -"Hands-on Python with Claude Code". Exit 0 means the selected implementation -satisfies the task contract; ``--expect-failure`` reproduces the unfinished -starter state and checks that the failing tests are the intended ones. +Objective completion evidence for a FlyPython shared-core course — the +teaching contract lives in COURSE.md; this file is the exercise. Exit 0 +means the selected implementation satisfies the task contract; +``--expect-failure`` reproduces the unfinished starter state and checks that +the failing tests are the intended ones. """ from __future__ import annotations @@ -20,6 +21,15 @@ ROOT = Path(__file__).resolve().parent +# Optional shared claim-receipt producer (docs/CLAIM-RECEIPT.md). The course +# folder still verifies standalone — without the tools/ sibling or without +# FLYPYTHON_CLAIM_SECRET set, verify.py behaves exactly as before. +try: + sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "tools")) + import claim_receipt +except ImportError: + claim_receipt = None + EXPECTED_STARTER_FAILURES = ( "test_load_json_records_returns_list_of_dicts", "test_unsupported_suffix_raises_value_error", @@ -32,6 +42,10 @@ +# ── PER-COURSE BLOCK ──────────────────────────────────────────── +# The only section allowed to differ across shared-core courses +# (checkpoint ids and gates must still match — titles may carry the +# tool's flavor). tools/verify_courses.py enforces this. COURSE_ID = 'course-claude-code' # Documented constant: claim codes derive deterministically from # (COURSE_ID, checkpoint_id, COURSE_SALT). They are spot-checkable @@ -46,6 +60,7 @@ {"id": "l04", "gate": "both-suites", "title": '验证与审查 / Verify and review'}, {"id": "l05", "gate": "attest", "title": '应用到自己的项目 / Apply to your project'}, ] +# ── END PER-COURSE BLOCK ──────────────────────────────────────── def _claim_code(checkpoint_id): digest = hashlib.sha256( @@ -61,9 +76,14 @@ def _run_suite(implementation): env=environment, check=False, capture_output=True, text=True, ) -def run_progress(as_json): +def run_progress(as_json, receipt_out=None): + import time + t0 = time.monotonic() starter = _run_suite("starter") + starter_ms = int((time.monotonic() - t0) * 1000) + t0 = time.monotonic() solution = _run_suite("solution") + solution_ms = int((time.monotonic() - t0) * 1000) starter_ok = starter.returncode == 0 solution_ok = solution.returncode == 0 rows = [] @@ -83,21 +103,51 @@ def run_progress(as_json): row["kind"] = kind row["claim_code"] = code rows.append(row) + secret = claim_receipt.receipts_enabled() if claim_receipt else None + receipts = [] + if secret: + starter_tests = claim_receipt.count_tests(starter.stderr) + solution_tests = claim_receipt.count_tests(solution.stderr) + solution_hash = claim_receipt.solution_sha256(ROOT) + for checkpoint in CHECKPOINTS: + gate = checkpoint["gate"] + if gate == "starter-suite": + passed, tests, ms = starter_ok, starter_tests, starter_ms + elif gate == "both-suites": + passed = starter_ok and solution_ok + tests, ms = starter_tests + solution_tests, starter_ms + solution_ms + else: + continue + receipts.append(claim_receipt.make_receipt( + COURSE_ID, checkpoint["id"], passed=passed, tests=tests, + duration_ms=ms, impl_dir=ROOT / "starter", + solution_hash=solution_hash, secret=secret)) if as_json: - print(json.dumps({"course": COURSE_ID, + document = {"course": COURSE_ID, "starter_suite_passed": starter_ok, "solution_suite_passed": solution_ok, - "checkpoints": rows}, ensure_ascii=False, indent=2)) + "checkpoints": rows} + if secret: + document["receipts"] = receipts + print(json.dumps(document, ensure_ascii=False, indent=2)) else: - starter_state = "通过" if starter_ok else "未通过" - solution_state = "通过" if solution_ok else "未通过" - print("课程 " + COURSE_ID) - print("实现状态: starter " + starter_state + " / solution " + solution_state) + starter_state = "passed" if starter_ok else "not passed" + solution_state = "passed" if solution_ok else "not passed" + print("Course " + COURSE_ID) + print("Suites: starter " + starter_state + " / solution " + solution_state) for row in rows: - state = row["status"] + ("(自报)" if row["kind"] == "attested" else "") - code = "认领码 " + row["claim_code"] if row["claim_code"] else "—" + state = row["status"] + (" (self-attested)" if row["kind"] == "attested" else "") + code = "claim code " + row["claim_code"] if row["claim_code"] else "—" print(" " + row["id"] + " " + row["title"] + " [" + state + "] " + code) - print("认领码是自我报告的证据,在 flypython.com 记录;绝非证书。") + print("Claim codes are self-reported evidence, recorded at flypython.com; never a certificate.") + if secret: + print(f"Signed run receipts prepared for {len(receipts)} gated checkpoint(s);" + " submit each with its claim to mark them as a local-run receipt.") + if receipt_out and secret: + Path(receipt_out).write_text( + json.dumps({"receipts": receipts}, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8") + print(f"Wrote {len(receipts)} receipt(s) to {receipt_out}", file=sys.stderr) return 0 def main() -> int: @@ -105,10 +155,11 @@ def main() -> int: parser.add_argument("implementation", choices=("progress", "starter", "solution")) parser.add_argument("--json", action="store_true") parser.add_argument("--expect-failure", action="store_true") + parser.add_argument("--receipt-out", metavar="PATH", help="write signed run receipts JSON (requires FLYPYTHON_CLAIM_SECRET)") args = parser.parse_args() if args.implementation == "progress": - return run_progress(args.json) + return run_progress(args.json, args.receipt_out) command = [sys.executable, "-m", "unittest", "discover", "-s", str(ROOT / "tests")] environment = os.environ.copy() @@ -139,6 +190,10 @@ def main() -> int: "directory." ) return 0 + if result.returncode == 0: + print(f"{args.implementation}: all tests passed") + else: + sys.stderr.write((result.stderr or "") or (result.stdout or "")) return result.returncode diff --git a/courses/hands-on-with-cursor/COURSE.md b/courses/hands-on-with-cursor/COURSE.md new file mode 100644 index 0000000..06146dd --- /dev/null +++ b/courses/hands-on-with-cursor/COURSE.md @@ -0,0 +1,112 @@ +--- +id: course-cursor +type: course +title: Hands-on with Cursor +summary: The verified Python workflow — task contract, bounded change, objective verify.py evidence — taught hands-on in Cursor, where Agent mode runs the loop and you supervise diffs and project rules. +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +badge: + id: course-cursor + name_en: Reproduce with Cursor in the loop + name_zh: Cursor 协同复现 + requires: All five checkpoints claimed (L01–L05) +course_id: course-cursor +--- + +# Hands-on with Cursor + +> TL;DR: download this folder, open it as a project in Cursor, open an Agent +> chat, and say **"start lesson 1"**. You finish with a working report tool, +> a reproducible pass/fail command, and the Cursor workflow: Agent/Ask/Plan +> modes, `.cursor/rules` project rules, and diff review. The exercise core is +> the same one the Claude Code and Codex courses use — only the tool you +> drive changes. + +## What you build + +A small Python report tool that reads messy real-world data (CSV or JSON), +isolates invalid rows instead of crashing, aggregates valid rows, and writes +its report atomically. Three scenario "skins" ship with the course so you can +practice on a domain you recognize: + +| Skin | You are… | Data | +| --- | --- | --- | +| `scenario/excel-report/` | turning a weekly orders export into a region summary | `orders.csv` | +| `scenario/data-monitor/` | checking service latency numbers from your servers | `metrics.csv` | +| `scenario/api-tool/` | summarizing ticket data pulled from an internal API | `tickets.json` | + +All three run through the same code and the same tests. The skills — +contracts, error isolation, rounding, atomic writes, verification — are the +actual product; the skins just make them concrete. + +## Teaching contract (read this first, agent) + +If you are the Cursor agent teaching this course, follow these rules: + +- **Audience:** a project owner who has working-with-AI experience but is + stuck on reliability — the script runs, then breaks on real data. Not a + Python beginner tutorial; not a prompt-engineering course. +- **Prerequisites:** Python 3.11+ on PATH and the Cursor editor installed + and signed in. No packages to install — the course is standard library + only. Agent usage limits on the learner's plan are unverified at authoring + time — see `REVIEW.md`. +- **Tool:** authored against Cursor as documented at cursor.com/docs + (Agent/Ask/Plan modes, `.cursor/rules/*.mdc` project rules with + `description`/`globs`/`alwaysApply` front matter, diff review). A live + teaching run-through is pending — `REVIEW.md` records what has and has + not been exercised. A Cursor major release triggers a course re-review. +- **Lesson order:** L01 → L02 → L03 → L04 → L05, one lesson per session. + Never skip the checkpoint at the end of a lesson. +- **Teaching style:** work from the files in this folder, not from general + knowledge. Quote the exact contract line you are satisfying. Make the + smallest change that moves a test from red to green. Never add + dependencies, never edit `solution/`, and ask before touching any file + not named in the current lesson. Read `AGENTS.md` in the repository root + if one exists — Cursor reads it too. +- **When to stop:** a lesson is done when its checkpoint command runs and + the learner can say, in their own words, what failed and why. If the + learner cannot, re-teach from the failing test, not from the solution. +- **How to use `verify.py`:** run it in the integrated terminal, or let the + Agent run it — `python verify.py starter --expect-failure` must reproduce + the listed failures; `python verify.py solution` must pass. "Done" means + the starter implementation passes the same suite after the learner's + bounded change. +- **Honesty rules:** say what you did not verify. Do not claim the code is + production-ready. Do not promise learning or career outcomes. + +## What this course does NOT cover + +Cursor installation, pricing plans, model selection, MCP setup, team/admin +features, or selling anything. For those, the companion repository has +guides and playbooks — link, don't improvise. + +## Badge contract + +- Badge: **Reproduce with Cursor in the loop Badge** (badge id `course-cursor`) — earned by claiming all five checkpoints. +- Challenges: L01–L05 checkpoints, 10 points each; +50 course-badge bonus when all five are claimed on flypython.com. +- Evidence: `python verify.py progress` — L03 (bounded change) and L04 (verify & review) are objectively gated by the suite; L01/L02/L05 are learner-attested. +- Submission: each passed checkpoint prints a deterministic claim code; record it on flypython.com against your account. Self-reported evidence, never a certificate. + +## Folder map + +``` +COURSE.md / COURSE_cn.md this file (EN / 中文) +lessons/L01.md … L05.md lessons (each has an _cn.md pair) +scenario/<skin>/ data files and scenario.json per skin +TASK.md / TASK_cn.md the task contract the change must satisfy +starter/report_tool.py the deliberately unfinished implementation +solution/report_tool.py the reviewed solution (do not copy in lesson 3) +tests/test_report_tool.py the contract suite (read-only) +verify.py objective pass/fail evidence +REVIEW.md maintainer run-through record +``` + +## Evidence and licensing + +The course folder is reviewed content: `REVIEW.md` records the last +run-through with dates, tool versions, and observed deviations. Code in this +folder is MIT-licensed; lesson prose is CC BY 4.0 (see repository `LICENSE`). +Report teaching drift or unclear lessons via the repository's +`course-feedback` issue form. diff --git a/courses/hands-on-with-cursor/COURSE_cn.md b/courses/hands-on-with-cursor/COURSE_cn.md new file mode 100644 index 0000000..adf6a5a --- /dev/null +++ b/courses/hands-on-with-cursor/COURSE_cn.md @@ -0,0 +1,102 @@ +--- +id: course-cursor +type: course +title: "上手 Cursor" +summary: "经过验证的 Python 工作流——任务契约、有界改动、客观 verify.py 证据——在 Cursor 里手把手教:由 Agent 模式跑闭环,你监督 diff 与项目规则。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +badge: + id: course-cursor + name_en: Reproduce with Cursor in the loop + name_zh: Cursor 协同复现 + requires: 认领全部五个检查点(L01–L05) +course_id: course-cursor +--- + +# 上手 Cursor + +> 一句话:下载本文件夹,在 Cursor 里作为项目打开,开一个 Agent 对话, +> 对它说**「开始第 1 课」**。完成时你会得到一个能跑的报表工具、一条 +> 可复现的通过/失败命令,以及 Cursor 工作流:Agent/Ask/Plan 模式、 +> `.cursor/rules` 项目规则与 diff 审查。练习核心与 Claude Code、Codex +> 课程完全相同——变的只是你驱动的工具。 + +## 你要做的东西 + +一个小型 Python 报表工具:读进脏乱的真实数据(CSV 或 JSON)、隔离 +无效行而不是直接崩溃、聚合有效行、原子地写出报告。课程自带三个 +「场景皮肤」,让你在自己熟悉的领域里练习: + +| 皮肤 | 你是…… | 数据 | +| --- | --- | --- | +| `scenario/excel-report/` | 把每周订单导出整理成区域汇总 | `orders.csv` | +| `scenario/data-monitor/` | 检查自家服务器的延迟数据 | `metrics.csv` | +| `scenario/api-tool/` | 汇总从内部 API 拉取的工单数据 | `tickets.json` | + +三个皮肤跑的是同一份代码、同一套测试。真正的产品是这些技能——契约、 +错误隔离、舍入、原子写、验证——皮肤只是让它们变得具体。 + +## 教学契约(先读,Agent) + +如果你是教授本课程的 Cursor Agent,请遵守以下规则: + +- **受众:** 有 AI 协作经验、但困在可靠性上的项目所有者——脚本能跑, + 一遇真实数据就坏。这不是 Python 入门教程,也不是提示词工程课。 +- **前置条件:** PATH 上有 Python 3.11+,装有 Cursor 编辑器并已登录。 + 无需安装任何包——课程只用标准库。学习者套餐的 Agent 额度能否撑完 + 整门课在编写时未经证实——见 `REVIEW.md`。 +- **工具:** 按 cursor.com/docs 上文档描述的 Cursor 编写(Agent/Ask/ + Plan 模式、带 `description`/`globs`/`alwaysApply` front matter 的 + `.cursor/rules/*.mdc` 项目规则、diff 审查)。真实授课实跑尚待 + 完成——`REVIEW.md` 记录已验证与未验证的部分。Cursor 主版本发布 + 会触发课程重审。 +- **课序:** L01 → L02 → L03 → L04 → L05,一次课一个会话。 + 绝不跳过课尾的检查点。 +- **教学风格:** 从本文件夹里的文件出发,不要凭通用知识发挥。引用 + 你正在满足的那条契约原文。做让测试从红变绿的最小改动。绝不加 + 依赖、绝不改 `solution/`、动当前课程未点名的任何文件前先询问。 + 仓库根若有 `AGENTS.md`,读它——Cursor 同样会读。 +- **何时停下:** 一节课的检查点命令跑通、且学习者能用自己的话说清 + 哪里坏了、为什么,这节课才算完。学习者说不出就从失败测试重新教, + 不要从 solution 教。 +- **如何使用 `verify.py`:** 在集成终端里跑,或让 Agent 跑—— + `python verify.py starter --expect-failure` 必须复现列出的失败; + `python verify.py solution` 必须通过。「完成」指学习者做完有界 + 改动后,starter 实现通过同一套测试。 +- **诚实规则:** 说清你没验证什么。不要声称代码达到生产可用。不要 + 承诺学习或职业结果。 + +## 本课程不覆盖什么 + +Cursor 的安装、套餐定价、模型选择、MCP 配置、团队/管理功能,以及 +任何推销内容。这些由配套仓库的指南与手册承载——给链接,不即兴发挥。 + +## 徽章契约 + +- 徽章:**Cursor 协同复现徽章**(badge id `course-cursor`)——认领全部五个检查点获得。 +- 挑战:L01–L05 检查点,各 10 分;在 flypython.com 上集齐五个再加 50 分课程徽章奖励。 +- 证据:`python verify.py progress`——L03(有界改动)与 L04(验证与评审)由套件客观把关;L01/L02/L05 为学习者自我报告。 +- 提交:每个通过的检查点打印一个确定性的认领码;在 flypython.com 上记入你的账号。这是自我报告的证据,从来不是证书。 + +## 文件夹地图 + +``` +COURSE.md / COURSE_cn.md 本文件(EN / 中文) +lessons/L01.md … L05.md 课文(每课都有一个 _cn.md 对) +scenario/<skin>/ 每个皮肤的数据文件与 scenario.json +TASK.md / TASK_cn.md 改动必须满足的任务契约 +starter/report_tool.py 有意未完成的实现 +solution/report_tool.py 评审过的解答(第 3 课不要抄) +tests/test_report_tool.py 契约套件(只读) +verify.py 客观的通过/失败证据 +REVIEW.md 维护者实跑记录 +``` + +## 证据与许可 + +本课程文件夹属于已评审内容:`REVIEW.md` 记录最近一次实跑——日期、 +工具版本、观察到的偏差。文件夹内代码采用 MIT 许可;课文文字采用 +CC BY 4.0(见仓库 `LICENSE`)。发现教学漂移或课程不清楚之处,请 +通过仓库的 `course-feedback` issue 表单反馈。 diff --git a/courses/hands-on-with-cursor/REVIEW.md b/courses/hands-on-with-cursor/REVIEW.md new file mode 100644 index 0000000..3da093a --- /dev/null +++ b/courses/hands-on-with-cursor/REVIEW.md @@ -0,0 +1,30 @@ +# Maintainer run-through record + +## 2026-09-13 — authored from official documentation (mechanical verification only) + +- Course authored against Cursor documentation (cursor.com/docs rules and + agent-mode references) and the shared `report-tool` core. `TASK.md`, + `tests/`, `scenario/`, `starter/`, `solution/` are byte-identical to the + Claude Code / Codex course cores — enforced by the `core-group` check in + `tools/verify_courses.py`. +- Environment for mechanical verification: macOS (arm64), Python 3.13. +- Commands and results: + - `python verify.py starter --expect-failure` — exit 0; expected failures + reproduced. + - `python verify.py solution` — exit 0; 9/9 tests pass. + - `python tools/verify_courses.py` — contract satisfied, core identical. +- **Not done:** any run inside Cursor. No Cursor install or account was + used; every Cursor behavior described in the lessons is sourced from + official documentation, not observed. + +## Pending before this course can be called taught-in-tool + +- One full agent-taught run-through inside Cursor ("start lesson 1" through + the Lesson 5 checkpoint) on a current Cursor build, recording observed + deviations from `COURSE.md` here. +- Whether a free Cursor plan's Agent quota lasts the whole course is + unverified. + +## Deviation log + +(none yet — no live run has happened) diff --git a/courses/hands-on-with-openai-codex-cli/TASK.md b/courses/hands-on-with-cursor/TASK.md similarity index 100% rename from courses/hands-on-with-openai-codex-cli/TASK.md rename to courses/hands-on-with-cursor/TASK.md diff --git a/courses/hands-on-with-openai-codex-cli/TASK_cn.md b/courses/hands-on-with-cursor/TASK_cn.md similarity index 100% rename from courses/hands-on-with-openai-codex-cli/TASK_cn.md rename to courses/hands-on-with-cursor/TASK_cn.md diff --git a/courses/hands-on-with-cursor/core-group b/courses/hands-on-with-cursor/core-group new file mode 100644 index 0000000..b56b0eb --- /dev/null +++ b/courses/hands-on-with-cursor/core-group @@ -0,0 +1 @@ +report-tool diff --git a/courses/hands-on-with-cursor/lessons/L01.md b/courses/hands-on-with-cursor/lessons/L01.md new file mode 100644 index 0000000..3001f7e --- /dev/null +++ b/courses/hands-on-with-cursor/lessons/L01.md @@ -0,0 +1,104 @@ +--- +id: course-cursor-l01 +type: course +title: "Lesson 1: Reproduce the failure inside Cursor" +summary: "Open the folder as a Cursor project, let Agent mode read the teaching contract, and make the failure objective with verify.py — inside the editor, not beside it." +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "What this checkpoint tests" + body: "That you can reproduce the failure before fixing it: `python verify.py starter --expect-failure` must print the named failing tests. In Cursor, Agent mode is where the work happens — the command is still the evidence." + - title: "Ask vs Agent" + body: "Ask mode answers questions without touching files; Agent mode reads, edits, and runs commands. This lesson only needs the Agent to read — tell it not to change anything." + - title: "When you are done" + body: "You can say which nine behaviors the suite proves and why the starter cannot fake them. `python verify.py progress` then shows this checkpoint's claim code." +--- + +# Lesson 1: Reproduce the failure inside Cursor + +## Objective + +By the end of this lesson you can open the course folder in Cursor, run the +course's objective verification command, and explain what "the starter +fails" means in test names. No code is changed in this lesson. + +## Why this lesson exists + +Most AI-written Python dies the same way: the demo input works, the real +input arrives, and the script crashes on row 3 — or worse, quietly produces +a wrong number. The fix does not start with a better prompt. It starts with +turning "it's broken" into a command that anyone can run and get the same +answer. In Cursor, Agent mode runs that command right next to the code it +verifies. + +## Warm-up (2 minutes) + +Open `starter/report_tool.py` in the editor and read it top to bottom. It +looks reasonable: functions with docstrings, type hints, a CLI. This is +exactly what AI-generated code looks like — clean surface, missing +behavior. Now open `TASK.md` and keep it next to you. + +## The lesson + +Open the course folder in Cursor (`File → Open Folder`), then open a chat +and make sure it is in **Agent** mode — the mode that can read files and run +commands. Tell it: + +**"Read COURSE.md — it is the teaching contract for this folder. Then read +TASK.md and starter/report_tool.py. Do not change any file. Tell me which +behaviors the tests expect that the starter does not have."** + +While it reads, run the objective verification yourself in the integrated +terminal: + +```bash +python verify.py starter --expect-failure +``` + +You should see the starter reproduce four real-world failure classes, mapped +to test names: + +| Failing test | Real-world meaning | +| --- | --- | +| `test_load_json_records_returns_list_of_dicts` | the tool only handles CSV; the API scenario crashes | +| `test_unsupported_suffix_raises_value_error` | an `.xlsx` upload becomes a confusing crash instead of a clear rejection | +| `test_invalid_records_are_isolated_with_reasons` | one empty cell aborts the entire run — no partial report, no reason | +| `test_group_totals_are_rounded_to_two_decimals` | `0.1 + 0.2` prints as `0.30000000000000004` in your report | +| `test_write_report_creates_missing_parent_directories` | the tool cannot write into a fresh output directory | +| `test_run_scenario_writes_report_file`, `test_main_prints_summary_and_returns_zero` | end-to-end consequences of the above | + +Then run the reviewed implementation the same way: + +```bash +python verify.py solution +``` + +Both commands exit 0 — but they mean opposite things. The first proves the +starter is *correctly unfinished*; the second proves the target is +reachable. That pair is your completion evidence for the whole course. + +## Exercise + +Pick the scenario skin you most identify with (`excel-report`, +`data-monitor`, or `api-tool`), open its data file, and find the rows that +will be rejected. Write down, before running anything, which test each bad +row will trigger. Then check yourself against the table above — and ask the +agent to confirm, not to fix. + +## Checkpoint + +Run `python verify.py starter --expect-failure` and +`python verify.py solution`. You pass this lesson when you can answer, +without looking: + +1. Which four behaviors separate starter from solution? +2. Why is "expected failure" a success condition, not a contradiction? +3. What does the row you found in the exercise do to the starter — crash, + isolate, or silently pass? + +## Expected evidence + +The transcript of both commands, and your three answers. Keep them; +Lesson 4 will ask for them again. diff --git a/courses/hands-on-with-cursor/lessons/L01_cn.md b/courses/hands-on-with-cursor/lessons/L01_cn.md new file mode 100644 index 0000000..7a6c529 --- /dev/null +++ b/courses/hands-on-with-cursor/lessons/L01_cn.md @@ -0,0 +1,96 @@ +--- +id: course-cursor-l01 +type: course +title: "第 1 课:在 Cursor 里复现故障" +summary: "把文件夹作为 Cursor 项目打开,让 Agent 模式读教学契约,用 verify.py 让失败变得客观——在编辑器内完成,而不是在编辑器旁边。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "本检查点考察什么" + body: "先复现失败再动手修:`python verify.py starter --expect-failure` 必须打印出点名的失败测试。在 Cursor 里,Agent 模式是干活的地方——但命令仍然是证据。" + - title: "Ask 与 Agent 的区别" + body: "Ask 模式只回答问题、不碰文件;Agent 模式会读文件、改代码、跑命令。本课只需要 Agent 读——明确告诉它不要改任何文件。" + - title: "完成的标志" + body: "你能说出套件证明了哪九种行为、为什么 starter 伪造不出来。`python verify.py progress` 随即显示本检查点的认领码。" +--- + +# 第 1 课:在 Cursor 里复现故障 + +## 目标 + +本课结束时,你能在 Cursor 里打开课程文件夹、运行课程的客观验证 +命令,并解释「starter 失败」在测试名里的含义。本课不改任何代码。 + +## 为什么有这节课 + +AI 写的 Python 大多是同一种死法:演示输入能跑,真实输入一到,脚本 +在第 3 行崩掉——或者更糟,悄悄算出一个错的数。修复不从更好的 +提示词开始,而从把「坏了」变成一条任何人都能跑、且得到相同答案的 +命令开始。在 Cursor 里,Agent 模式就在它要验证的代码旁边跑这条 +命令。 + +## 热身(2 分钟) + +在编辑器里打开 `starter/report_tool.py`,从头读到尾。它看起来很 +合理:带 docstring 的函数、类型标注、CLI。这正是 AI 生成代码的 +样子——表面干净、行为缺失。现在打开 `TASK.md`,放在手边。 + +## 本课内容 + +在 Cursor 里打开课程文件夹(`File → Open Folder`),开一个对话并 +确认处于 **Agent** 模式——这个模式才能读文件、跑命令。对它说: + +**“读 COURSE.md——它是本文件夹的教学契约。再读 TASK.md 和 +starter/report_tool.py。不要改任何文件。告诉我测试期望哪些行为、 +starter 缺了哪些。”** + +趁它读的时候,自己在集成终端里跑客观验证: + +```bash +python verify.py starter --expect-failure +``` + +你会看到 starter 复现出四类真实世界故障,对应到测试名: + +| 失败的测试 | 真实含义 | +| --- | --- | +| `test_load_json_records_returns_list_of_dicts` | 工具只处理 CSV;API 场景直接崩 | +| `test_unsupported_suffix_raises_value_error` | 一个 `.xlsx` 上传变成莫名其妙的崩溃,而不是清楚的拒绝 | +| `test_invalid_records_are_isolated_with_reasons` | 一个空单元格中止整个运行——没有部分报告,没有原因 | +| `test_group_totals_are_rounded_to_two_decimals` | `0.1 + 0.2` 在报告里打印成 `0.30000000000000004` | +| `test_write_report_creates_missing_parent_directories` | 工具无法写进新建的输出目录 | +| `test_run_scenario_writes_report_file`、`test_main_prints_summary_and_returns_zero` | 上述问题的端到端后果 | + +然后用同样的方式跑评审过的实现: + +```bash +python verify.py solution +``` + +两条命令都以 0 退出——但含义相反。第一条证明 starter *确实处于 +正确的未完成态*;第二条证明目标可达。这一对命令是整门课的完成 +证据。 + +## 练习 + +挑一个你最有代入感的场景皮肤(`excel-report`、`data-monitor` 或 +`api-tool`),打开它的数据文件,找出会被拒绝的行。先不看运行 +结果,写下每个坏行会触发哪个测试,再对照上面的表自查——并让 +Agent 确认,而不是让它修。 + +## 检查点 + +运行 `python verify.py starter --expect-failure` 和 +`python verify.py solution`。不看材料能回答下面三个问题即算通过: + +1. 哪四个行为把 starter 和 solution 区分开? +2. 为什么「预期失败」是成功条件而不是自相矛盾? +3. 你在练习里找到的那一行会让 starter 崩溃、被隔离,还是悄悄 + 通过? + +## 预期证据 + +两条命令的完整输出记录,加你的三个答案。保留好——第 4 课还会 +用到。 diff --git a/courses/hands-on-with-cursor/lessons/L02.md b/courses/hands-on-with-cursor/lessons/L02.md new file mode 100644 index 0000000..5613e75 --- /dev/null +++ b/courses/hands-on-with-cursor/lessons/L02.md @@ -0,0 +1,87 @@ +--- +id: course-cursor-l02 +type: course +title: "Lesson 2: Write the task contract, let .cursor/rules carry the rules" +summary: 'Turn "make it better" into a bounded, testable contract — then make it durable so every Cursor session starts with the same rules.' +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "What this checkpoint tests" + body: "That you can read `TASK.md` as testable statements, not prose. Every sentence that starts with a function name is a contract line the suite can assert." + - title: "Project rules are the instruction chain" + body: "Cursor reads `.cursor/rules/*.mdc` before doing work. Front matter decides when a rule applies: `description` for agent-chosen rules, `globs` for file-matched rules, `alwaysApply: true` for always-on rules. AGENTS.md is read too." + - title: "The testability check" + body: "If you cannot tell whether a statement is testable, ask: could a suite assert it without reading your mind? Numbers, exit codes, files on disk — never vibes." +--- + +# Lesson 2: Write the task contract, let .cursor/rules carry the rules + +## Objective + +You can read `TASK.md` as a set of testable statements, trace each +statement to a test in `tests/test_report_tool.py`, and write durable rules +a Cursor session will pick up automatically. + +## Why this lesson exists + +Vague requests produce vague code. "Handle bad rows better" gives an agent +permission to guess; "invalid rows are collected in `errors` with index and +reason, valid rows still produce a report" gives it a target and gives you +a way to check. In Cursor there is a second failure mode: a rule you typed +into one chat does not exist in the next. The contract fixes the task; +project rules fix the working rules. + +## The lesson + +Open `TASK.md`. Notice what every line has in common: it names an +observable behavior, not an implementation. Four statements from the +contract, and the tests that pin them: + +| Contract line | Test | +| --- | --- | +| "a JSON file loads into the same record list as CSV" | `test_load_json_records_returns_list_of_dicts` | +| "invalid rows land in `errors` with index and reason; valid rows still aggregate" | `test_invalid_records_are_isolated_with_reasons` | +| "group totals are rounded to two decimals" | `test_group_totals_are_rounded_to_two_decimals` | +| "the report write is atomic and creates missing parents" | `test_write_report_creates_missing_parent_directories` | + +Now the durable half. Cursor reads project rules from `.cursor/rules/` — +each rule is an `.mdc` file whose front matter controls when it applies: +`alwaysApply: true` attaches it to every request, `globs` attaches it when +matching files are in play, and `description` lets the agent pull it in +when relevant. This course's working rules belong in an always-on rule. +Tell the agent: + +**"Create `.cursor/rules/flypython-course.mdc` with `alwaysApply: true` +containing these working rules: only starter/report_tool.py may change; +tests/, solution/, scenario/ are read-only; one failing test group per +turn; run `python verify.py starter` after each; standard library only."** + +Then open a *new* chat and ask it to summarize its working rules. If it +quotes yours back, the rule is applying; if not, check the front matter — +a missing or mistyped `alwaysApply` is the usual cause. + +## Exercise + +Write one contract line for a script you actually own, using the same +shape: inputs, outputs, error cases, and "done means `<command>` exits 0". +Then write two rules you would put in that project's `.cursor/rules/` and +decide which front-matter field controls each. + +## Checkpoint + +Run `python verify.py progress` — this checkpoint's claim code prints when +you can answer: + +1. Which `TASK.md` line does `test_invalid_records_are_isolated_with_reasons` + pin, in your own words? +2. Which mechanism carries rules across sessions — the chat prompt, or + `.cursor/rules`? +3. What is the difference between a rule with `globs` and a rule with + `alwaysApply: true`? + +## Expected evidence + +Your drafted contract line, your two rules with their front-matter choice, +and the new chat's summary of its working rules. diff --git a/courses/hands-on-with-cursor/lessons/L02_cn.md b/courses/hands-on-with-cursor/lessons/L02_cn.md new file mode 100644 index 0000000..e065615 --- /dev/null +++ b/courses/hands-on-with-cursor/lessons/L02_cn.md @@ -0,0 +1,82 @@ +--- +id: course-cursor-l02 +type: course +title: "第 2 课:写任务契约,让 .cursor/rules 承载规则" +summary: "把「改好一点」变成有边界、可测试的契约——再让它持久化,让每个 Cursor 会话都从同一套规则开始。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "本检查点考察什么" + body: "你能把 `TASK.md` 当成可测试的陈述来读,而不是散文。每个以函数名开头的句子都是套件能断言的契约行。" + - title: "项目规则是指令链" + body: "Cursor 干活前先读 `.cursor/rules/*.mdc`。front matter 决定规则何时生效:`description` 让 Agent 按需取用,`globs` 匹配到文件时生效,`alwaysApply: true` 永远生效。AGENTS.md 也会被读取。" + - title: "可测试性检查" + body: "判断一句话能不能测:测试套件不看你的心思能不能断言它?数字、退出码、磁盘上的文件——不看感觉。" +--- + +# 第 2 课:写任务契约,让 .cursor/rules 承载规则 + +## 目标 + +你能把 `TASK.md` 当成一组可测试陈述来读,把每条陈述追查到 +`tests/test_report_tool.py` 里的一个测试,并写出 Cursor 会话会 +自动读取的持久规则。 + +## 为什么有这节课 + +含糊的需求产出含糊的代码。「把坏行处理好一点」是许可 Agent 去猜; +「无效行收进 `errors`、带下标和原因,有效行照常产出报告」是给它 +目标、也给你检查的方法。在 Cursor 里还有第二种失败:你在某个 +对话里敲的规则,下一个对话里根本不存在。契约固定任务本身; +项目规则固定工作规则。 + +## 本课内容 + +打开 `TASK.md`。注意每行的共同点:它描述的是可观察的行为,不是 +实现。契约里的四句陈述,以及钉住它们的测试: + +| 契约行 | 测试 | +| --- | --- | +| 「JSON 文件要加载成与 CSV 相同的记录列表」 | `test_load_json_records_returns_list_of_dicts` | +| 「无效行带下标和原因进 `errors`;有效行照常聚合」 | `test_invalid_records_are_isolated_with_reasons` | +| 「分组总计保留两位小数」 | `test_group_totals_are_rounded_to_two_decimals` | +| 「报告写出是原子的,且会创建缺失的父目录」 | `test_write_report_creates_missing_parent_directories` | + +再看持久的另一半。Cursor 从 `.cursor/rules/` 读项目规则——每条 +规则是一个 `.mdc` 文件,front matter 控制它何时生效: +`alwaysApply: true` 附加到每次请求,`globs` 在匹配文件出现时 +生效,`description` 让 Agent 在相关时自行取用。本课程的工作规则 +应该放一条常驻规则。告诉 Agent: + +**“创建 `.cursor/rules/flypython-course.mdc`,`alwaysApply: true`, +写入这些工作规则:只许改 starter/report_tool.py;tests/、solution/、 +scenario/ 只读;每轮只修一组失败测试;修完跑 +`python verify.py starter`;只用标准库。”** + +然后开一个*新*对话,让它总结自己的工作规则。它把你的规则原样引 +回来,说明规则生效了;没有就检查 front matter——`alwaysApply` +写错或漏写是最常见的原因。 + +## 练习 + +为你真正拥有的一个脚本写一条契约行,用同样的形状:输入、输出、 +错误情况、以及「完成 = `<命令>` 退出码为 0」。再写你会放进那个 +项目 `.cursor/rules/` 的两条规则,并决定各自用哪个 front matter +字段控制生效时机。 + +## 检查点 + +运行 `python verify.py progress`——能回答下面三题时,本检查点的 +认领码就会显示: + +1. 用你自己的话说,`test_invalid_records_are_isolated_with_reasons` + 钉住的是 `TASK.md` 哪一行? +2. 跨会话携带规则的是对话提示词还是 `.cursor/rules`? +3. 带 `globs` 的规则和带 `alwaysApply: true` 的规则有什么区别? + +## 预期证据 + +你起草的契约行、两条带 front matter 选择的规则,以及新对话对 +自己工作规则的总结。 diff --git a/courses/hands-on-with-cursor/lessons/L03.md b/courses/hands-on-with-cursor/lessons/L03.md new file mode 100644 index 0000000..0b010a2 --- /dev/null +++ b/courses/hands-on-with-cursor/lessons/L03.md @@ -0,0 +1,84 @@ +--- +id: course-cursor-l03 +type: course +title: "Lesson 3: The bounded change, reviewed hunk by hunk" +summary: "Drive the starter to green through bounded turns — and use Cursor's diff review to keep every change yours." +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "What this checkpoint tests" + body: "That the starter passes all nine tests through changes you supervised — `python verify.py progress` shows this code only once the starter suite is green." + - title: "The diff is the boundary" + body: "Agent mode shows each edit as a diff you accept or reject. Read every hunk — approving blind is how bounded work becomes a rewrite." + - title: "When you are done" + body: "Every diff maps to a contract line, and you can explain each one without opening solution/." +--- + +# Lesson 3: The bounded change, reviewed hunk by hunk + +## Objective + +The starter passes all nine tests through a sequence of bounded changes, +and you can explain every diff the agent produced without reading the +solution first. + +## Why this lesson exists + +This is the lesson people skip — and the one that builds the actual skill. +Letting Agent mode rewrite the file in one shot teaches you nothing and +gives you a diff you cannot honestly review. Supervising small diffs, each +pinned to a contract line, is how the code stays yours. Cursor's +accept/reject diff flow exists for exactly this. + +## The lesson + +Start a fresh Agent chat (a finished task's chat carries stale context) +and say: + +**"Work through TASK.md against starter/report_tool.py. One failing test +group at a time: JSON loading, then validation isolation, then rounding, +then atomic writes, then the end-to-end tests. After each group, run +`python verify.py starter` and show me the diff before continuing."** + +Hold it to the contract's boundaries: + +- Only `starter/report_tool.py` changes. If a diff touches `tests/`, + `solution/`, or `scenario/`, reject it and ask why. +- No new imports outside the standard library — and no import that is not + needed by the change being made. +- Each change should move toward one contract line. Reject drive-by + refactors ("while I was here I renamed…"). +- If the agent wants to change a test, the answer is no. Tests are the + contract; the code moves. + +Review deliberately. When the agent proposes an edit, Cursor shows it as a +diff — read every hunk before accepting. When it proposes a terminal +command, read it: `python verify.py starter` is safe; `pip install` +anything is a stop sign. A diff you cannot explain is a diff you reject. + +Expect the failing count to drop group by group: 7 → 5 → 4 → 3 → 2 → 0. + +## Exercise + +Pick one hunk the agent produced — ideally the validation-isolation +change — and explain it back in the chat: "line N does X; that satisfies +contract line Y." If you cannot, ask the agent to walk you through its own +diff before accepting the next change. + +## Checkpoint + +Run `python verify.py progress`. This checkpoint's code appears only when +the starter suite is green. You pass the lesson when you can answer: + +1. How many turns did the bounded change take, and what made each turn + bounded? +2. Which diff or command did you reject or narrow, and why? +3. Point at the hunk that implements "isolate invalid rows with reasons" — + where is it? + +## Expected evidence + +A green `python verify.py starter` run, and your hunk-by-hunk explanation +of the change that earned it. diff --git a/courses/hands-on-with-cursor/lessons/L03_cn.md b/courses/hands-on-with-cursor/lessons/L03_cn.md new file mode 100644 index 0000000..c0a01ea --- /dev/null +++ b/courses/hands-on-with-cursor/lessons/L03_cn.md @@ -0,0 +1,74 @@ +--- +id: course-cursor-l03 +type: course +title: "第 3 课:有界改动,逐块审 diff" +summary: "通过有界的轮次把 starter 推到全绿——并用 Cursor 的 diff 审查守住每一处改动。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "本检查点考察什么" + body: "starter 通过你监督的改动让九个测试全绿——`python verify.py progress` 只在 starter 套件变绿后显示本检查点的码。" + - title: "diff 就是边界" + body: "Agent 模式把每处编辑显示为可接受或拒绝的 diff。逐块读完再放行——闭眼接受正是「有界工作」变成「整文件重写」的方式。" + - title: "完成的标志" + body: "每个 diff 都能对上一条契约行,而且不看 solution/ 你也讲得清每一处改动。" +--- + +# 第 3 课:有界改动,逐块审 diff + +## 目标 + +starter 通过一串有界改动让九个测试全绿,并且不看 solution,你也 +讲得清 Agent 产出的每一个 diff。 + +## 为什么有这节课 + +这节课最容易被跳过——也恰恰是长本事的那节。放任 Agent 模式把 +文件一次重写,什么也学不到,只留下一份你没底气审查的 diff。监督 +一连串小 diff、各自钉在一条契约行上,才能让 AI 写的代码仍然是 +你的。Cursor 的接受/拒绝 diff 流程就是为这个存在的。 + +## 本课内容 + +开一个新的 Agent 对话(已完结任务的对话带着过期上下文),说: + +**“按 TASK.md 改 starter/report_tool.py。一次只修一组失败测试: +先 JSON 加载,再无效行隔离,再舍入,再原子写,最后是端到端测试。 +每修完一组跑 `python verify.py starter`,先给我 diff 再继续。”** + +守住契约边界: + +- 只动 `starter/report_tool.py`。diff 碰到 `tests/`、`solution/`、 + `scenario/` 就拒绝并问为什么。 +- 标准库之外不加 import——连与本改动无关的 import 也不要。 +- 每处改动朝一条契约行推进。顺手重构(「我顺便重命名了……」) + 一律拒绝。 +- Agent 想改测试,答案是不行。测试是契约;动的是代码。 + +认真审查。Agent 提议编辑时,Cursor 把它显示为 diff——逐块读完再 +接受。它提议终端命令时读它:`python verify.py starter` 安全; +`pip install` 任何东西都是停止信号。讲不清的 diff 就拒绝。 + +预期失败数逐组下降:7 → 5 → 4 → 3 → 2 → 0。 + +## 练习 + +挑 Agent 产出的一个 hunk——最好是「无效行隔离」那段——在对话里 +讲回给它听:「第 N 行做了 X,满足契约行 Y」。讲不清就先让它走读 +自己的 diff,再接受下一处改动。 + +## 检查点 + +运行 `python verify.py progress`。本检查点的码只在 starter 套件 +全绿时出现。不看材料能回答即算通过: + +1. 这次有界改动走了几轮,是什么让每轮保持有界? +2. 你拒绝或收窄过哪个 diff 或命令,为什么? +3. 指出实现「无效行带原因隔离」的那个 hunk——它在哪? + +## 预期证据 + +一次全绿的 `python verify.py starter` 运行,和你对这次改动的逐块 +讲解。 diff --git a/courses/hands-on-with-cursor/lessons/L04.md b/courses/hands-on-with-cursor/lessons/L04.md new file mode 100644 index 0000000..4fae758 --- /dev/null +++ b/courses/hands-on-with-cursor/lessons/L04.md @@ -0,0 +1,79 @@ +--- +id: course-cursor-l04 +type: course +title: "Lesson 4: Verify like a reviewer, not a spectator" +summary: "Green tests are the floor, not the ceiling — run both suites, walk the scenario skins, and review the accumulated diff the way you would review a colleague's." +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "What this checkpoint tests" + body: "That starter AND solution both pass — `verify.py progress` prints this code only when both suites are green. The gate is the pair, not a single run." + - title: "Review the delta, not the vibes" + body: "Git's working-tree diff shows the whole accumulated change. Read it as a reviewer: every hunk maps to a contract line, nothing extra moved, no test weakened." + - title: "When you are done" + body: "You have written three evidence lines — verified / not verified / known limits — and could defend the change in a code review." +--- + +# Lesson 4: Verify like a reviewer, not a spectator + +## Objective + +Both suites pass, and you hold a written record of what the green runs do +and do not prove — plus a reviewed diff you would defend to a colleague. + +## Why this lesson exists + +"All tests pass" is where most AI-assisted work stops — and where most +silent bugs start. A suite proves the contract lines it asserts; it says +nothing about the rows nobody wrote a test for. Verification is a report +you write, not a feeling you have. + +## The lesson + +Run the full pair in the integrated terminal: + +```bash +python verify.py starter # the implementation you supervised +python verify.py solution # the reviewed reference +``` + +Both green means the contract is satisfied *as written*. Now do the review +the suite cannot do for you: + +1. **Walk a skin end-to-end.** Pick a `scenario/<skin>/` folder and run + the tool on it: `python starter/report_tool.py scenario/<skin>`. Read + the produced report against the input file — recompute one group total + by hand. +2. **Review the accumulated diff.** `git diff` (or Cursor's source-control + panel) shows every hunk that landed across the session. Read it top to + bottom. Every hunk should map to a contract line you named in Lesson 2. + Anything that does not is scope creep — revert or justify it. +3. **Check the edges the tests skip.** Feed the tool an empty CSV, a CSV + with only invalid rows, a missing file. The contract does not pin + these — your judgment does. Write down what it does. + +## Exercise + +Write the three-line evidence record for this change: + +- **Verified:** the commands you ran and what they proved. +- **Not verified:** the inputs and behaviors no test pins. +- **Known limits:** anything you saw that the contract never promised. + +Keep it honest — "not verified" is a list of facts, not a confession. + +## Checkpoint + +Run `python verify.py progress`. This checkpoint's code prints only when +both suites pass. You pass the lesson when you can answer: + +1. What did your by-hand recompute of a group total show? +2. Name one input the suite does not pin and what the tool does with it. +3. Which hunk, if any, would you ask a colleague to justify? + +## Expected evidence + +Both green runs, your three-line evidence record, and the diff review +notes. diff --git a/courses/hands-on-with-cursor/lessons/L04_cn.md b/courses/hands-on-with-cursor/lessons/L04_cn.md new file mode 100644 index 0000000..489be32 --- /dev/null +++ b/courses/hands-on-with-cursor/lessons/L04_cn.md @@ -0,0 +1,75 @@ +--- +id: course-cursor-l04 +type: course +title: "第 4 课:像评审者一样验证,别当观众" +summary: "测试全绿是地板不是天花板——两个套件都跑、三个皮肤都走一遍、按评审同事的标准审这份累积 diff。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "本检查点考察什么" + body: "starter 和 solution 双双通过——`verify.py progress` 只在两个套件都绿时打印本检查点的码。门槛是一对结果,不是一次运行。" + - title: "审增量,不凭感觉" + body: "Git 工作区 diff 展示整个会话累积的改动。按评审者的方式读:每个 hunk 对上一条契约行、没有多余挪动、没有测试被放松。" + - title: "完成的标志" + body: "你已写下三行证据——已验证 / 未验证 / 已知局限——并且能在代码评审里为这次改动辩护。" +--- + +# 第 4 课:像评审者一样验证,别当观众 + +## 目标 + +两个套件都通过,你手握一份书面记录:说清绿色运行证明了什么、没 +证明什么——外加一份你敢向同事辩护的 diff。 + +## 为什么有这节课 + +「测试全过」是大多数 AI 辅助工作停下的地方——也是大多数隐性 +bug 开始的地方。套件只证明它断言过的契约行;没人写过测试的行, +它一概不担保。验证是你写的一份报告,不是一种感觉。 + +## 本课内容 + +在集成终端里跑完整的一对命令: + +```bash +python verify.py starter # 你监督出来的实现 +python verify.py solution # 评审过的参考实现 +``` + +双全绿意味着契约*按字面*被满足。现在做套件替不了你的评审: + +1. **走完一个皮肤。** 挑一个 `scenario/<skin>/` 文件夹,用工具跑 + 它:`python starter/report_tool.py scenario/<skin>`。对照输入 + 文件读产出的报告——亲手重算一个分组总计。 +2. **审累积的 diff。** `git diff`(或 Cursor 的源代码管理面板) + 展示整个会话落地的每个 hunk。从头读到尾。每个 hunk 都应能对 + 上你在第 2 课点名的契约行。对不上的就是范围蔓延——回退或让 + 它解释。 +3. **检查测试没钉的边界。** 喂给工具一个空 CSV、一个全是无效行 + 的 CSV、一个不存在的文件。契约没钉这些——你的判断钉。记下 + 它的行为。 + +## 练习 + +为这次改动写三行证据记录: + +- **已验证:** 你跑过的命令和它们证明了什么。 +- **未验证:** 没有测试钉住的输入与行为。 +- **已知局限:** 你看到的、契约从未承诺过的东西。 + +写得诚实——「未验证」是一份事实清单,不是忏悔书。 + +## 检查点 + +运行 `python verify.py progress`。本检查点的码只在两个套件都过 +时打印。不看材料能回答即算通过: + +1. 你手工重算的分组总计结果如何? +2. 说出一个套件没钉的输入,以及工具对它的实际行为。 +3. 哪个 hunk(如果有)你会请同事来解释? + +## 预期证据 + +两次全绿运行、你的三行证据记录、diff 评审笔记。 diff --git a/courses/hands-on-with-cursor/lessons/L05.md b/courses/hands-on-with-cursor/lessons/L05.md new file mode 100644 index 0000000..3451fdf --- /dev/null +++ b/courses/hands-on-with-cursor/lessons/L05.md @@ -0,0 +1,77 @@ +--- +id: course-cursor-l05 +type: course +title: "Lesson 5: Take the workflow to your own project" +summary: "The tool generalizes: task contract, bounded agent work, diff review — run the same loop on a script you actually own." +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "What this checkpoint tests" + body: "Self-attested: you confirm you applied the loop to your own project. The evidence is your contract, your accepted diffs, and your verification record — not a code we can check." + - title: "Carry the shape, not the code" + body: "What transfers is the loop: contract first, one bounded change at a time, every diff read before accepted, verification written down." + - title: "When you are done" + body: "Your own script has a contract, one bounded change you can explain, and a verified/not-verified/known-limits record — or a documented reason the loop did not fit." +--- + +# Lesson 5: Take the workflow to your own project + +## Objective + +You apply the whole loop — contract, project rules, bounded agent work, +diff review, written verification — to a script or project you actually +own, and record what happened. + +## Why this lesson exists + +A course folder is a safe sandbox; your own codebase is not. The point of +the last four lessons was never the report tool — it was a repeatable way +to drive an agent that keeps you in charge of what ships. This checkpoint +exists to prove the transfer happened. It is self-attested: only you know +whether the loop actually ran. + +## The lesson + +Pick a real script you own — one with a bug you have been putting off, or +a behavior you want but have not written. Then run the same five moves: + +1. **Contract.** Write the task contract first: inputs, outputs, error + cases, and "done means `<command>` exits 0". One short paragraph, every + sentence testable. +2. **Rules that persist.** Open the project in Cursor and put the working + rules in `.cursor/rules/` — the files the agent may touch, the + verification command, the dependency policy. Every new chat inherits + them. +3. **Bounded work.** One Agent chat, one contract line at a time. Read + every diff hunk before accepting; read every command before it runs. +4. **Verify.** Run the verification command, then do the review the suite + cannot do — walk one real input end-to-end and check the output by + hand. +5. **Record.** Write the three lines: verified / not verified / known + limits. + +If the loop fought you — the task was too small to need a contract, or too +vague to test — that is a finding too. Write down where the shape did not +fit; that judgment is the actual skill. + +## Exercise + +If your project has more than one boundary worth pinning, draft the next +contract line now — you will want it when you open the next chat. + +## Checkpoint + +This checkpoint is self-attested: claim it when you can answer: + +1. What was your contract's "done means" command? +2. Which diff or command did you push back on? +3. What is on your "not verified" list that a stranger would assume was + covered? + +## Expected evidence + +Your contract paragraph, the `.cursor/rules` file you wrote, and the +three-line verification record. These are yours — they are the proof the +workflow transferred. diff --git a/courses/hands-on-with-cursor/lessons/L05_cn.md b/courses/hands-on-with-cursor/lessons/L05_cn.md new file mode 100644 index 0000000..9b90321 --- /dev/null +++ b/courses/hands-on-with-cursor/lessons/L05_cn.md @@ -0,0 +1,68 @@ +--- +id: course-cursor-l05 +type: course +title: "第 5 课:把工作流带回自己的项目" +summary: "工具会泛化:任务契约、有界 Agent 工作、diff 评审——在你真正拥有的脚本上跑同一套闭环。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "本检查点考察什么" + body: "自我报告:你确认把这套闭环用到了自己的项目上。证据是你的契约、你接受的 diff、你的验证记录——不是我们查得了的码。" + - title: "带走形状,不是代码" + body: "能迁移的是闭环:契约先行、一次一处有界改动、每个 diff 审过才接受、验证写下来。" + - title: "完成的标志" + body: "你自己的脚本有了一份契约、一处你讲得清的有界改动、一份已验证/未验证/已知局限记录——或一条写清楚的「闭环不合身」的原因。" +--- + +# 第 5 课:把工作流带回自己的项目 + +## 目标 + +你把整套闭环——契约、项目规则、有界 Agent 工作、diff 评审、 +书面验证——用到自己真正拥有的脚本或项目上,并记录发生了什么。 + +## 为什么有这节课 + +课程文件夹是安全沙箱;你自己的代码库不是。前四课的意义从来不是 +那个报表工具——而是一种可复用的、让你始终掌控产出的 Agent 驱动 +方式。本检查点要证明迁移真的发生了。它是自我报告的:闭环跑没跑 +过只有你知道。 + +## 本课内容 + +挑一个你真正拥有的脚本——一个你拖了很久的 bug,或一个想要但 +还没写的行为。然后走同样的五步: + +1. **契约。** 先写任务契约:输入、输出、错误情况、以及「完成 = + `<命令>` 退出码为 0」。一小段,每句都可测试。 +2. **能留存的规则。** 在 Cursor 里打开项目,把工作规则写进 + `.cursor/rules/`——Agent 可以碰哪些文件、验证命令是什么、 + 依赖政策。每个新对话自动继承。 +3. **有界工作。** 一个 Agent 对话、一次一条契约行。每个 diff 的 + 每块都在接受前读完;每条命令都在运行前读完。 +4. **验证。** 跑验证命令,然后做套件做不了的评审——拿一个真实 + 输入端到端走一遍,手工核对输出。 +5. **记录。** 写三行:已验证 / 未验证 / 已知局限。 + +如果闭环跟你打架——任务小到不需要契约,或模糊到不可测——那也 +是发现。写下形状不合身的地方;这个判断本身就是真本事。 + +## 练习 + +如果你的项目还有不止一处值得钉住的边界,现在就起草下一条契约行 +——开下一个对话时你会用得上。 + +## 检查点 + +本检查点自我报告:能回答下面三题时即可认领: + +1. 你的契约里「完成 =」命令是什么? +2. 你在哪个 diff 或命令上顶回去过? +3. 你的「未验证」清单上有什么是陌生人会以为已覆盖的? + +## 预期证据 + +你的契约段落、你写的 `.cursor/rules` 文件、三行验证记录。它们是 +你自己的——是工作流完成迁移的证明。 diff --git a/courses/hands-on-with-openai-codex-cli/scenario/api-tool/scenario.json b/courses/hands-on-with-cursor/scenario/api-tool/scenario.json similarity index 100% rename from courses/hands-on-with-openai-codex-cli/scenario/api-tool/scenario.json rename to courses/hands-on-with-cursor/scenario/api-tool/scenario.json diff --git a/courses/hands-on-with-openai-codex-cli/scenario/api-tool/tickets.json b/courses/hands-on-with-cursor/scenario/api-tool/tickets.json similarity index 100% rename from courses/hands-on-with-openai-codex-cli/scenario/api-tool/tickets.json rename to courses/hands-on-with-cursor/scenario/api-tool/tickets.json diff --git a/courses/hands-on-with-openai-codex-cli/scenario/data-monitor/metrics.csv b/courses/hands-on-with-cursor/scenario/data-monitor/metrics.csv similarity index 100% rename from courses/hands-on-with-openai-codex-cli/scenario/data-monitor/metrics.csv rename to courses/hands-on-with-cursor/scenario/data-monitor/metrics.csv diff --git a/courses/hands-on-with-openai-codex-cli/scenario/data-monitor/scenario.json b/courses/hands-on-with-cursor/scenario/data-monitor/scenario.json similarity index 100% rename from courses/hands-on-with-openai-codex-cli/scenario/data-monitor/scenario.json rename to courses/hands-on-with-cursor/scenario/data-monitor/scenario.json diff --git a/courses/hands-on-with-openai-codex-cli/scenario/excel-report/orders.csv b/courses/hands-on-with-cursor/scenario/excel-report/orders.csv similarity index 100% rename from courses/hands-on-with-openai-codex-cli/scenario/excel-report/orders.csv rename to courses/hands-on-with-cursor/scenario/excel-report/orders.csv diff --git a/courses/hands-on-with-openai-codex-cli/scenario/excel-report/scenario.json b/courses/hands-on-with-cursor/scenario/excel-report/scenario.json similarity index 100% rename from courses/hands-on-with-openai-codex-cli/scenario/excel-report/scenario.json rename to courses/hands-on-with-cursor/scenario/excel-report/scenario.json diff --git a/courses/hands-on-with-openai-codex-cli/solution/report_tool.py b/courses/hands-on-with-cursor/solution/report_tool.py similarity index 100% rename from courses/hands-on-with-openai-codex-cli/solution/report_tool.py rename to courses/hands-on-with-cursor/solution/report_tool.py diff --git a/courses/hands-on-with-openai-codex-cli/starter/report_tool.py b/courses/hands-on-with-cursor/starter/report_tool.py similarity index 100% rename from courses/hands-on-with-openai-codex-cli/starter/report_tool.py rename to courses/hands-on-with-cursor/starter/report_tool.py diff --git a/courses/hands-on-with-openai-codex-cli/tests/test_report_tool.py b/courses/hands-on-with-cursor/tests/test_report_tool.py similarity index 100% rename from courses/hands-on-with-openai-codex-cli/tests/test_report_tool.py rename to courses/hands-on-with-cursor/tests/test_report_tool.py diff --git a/courses/hands-on-with-cursor/verify.py b/courses/hands-on-with-cursor/verify.py new file mode 100644 index 0000000..f23792d --- /dev/null +++ b/courses/hands-on-with-cursor/verify.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +"""Run the course contract against starter or solution. + +Objective completion evidence for a FlyPython shared-core course — the +teaching contract lives in COURSE.md; this file is the exercise. Exit 0 +means the selected implementation satisfies the task contract; +``--expect-failure`` reproduces the unfinished starter state and checks that +the failing tests are the intended ones. +""" + +from __future__ import annotations + +import argparse +import base64 +import hashlib +import json +import os +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent + +# Optional shared claim-receipt producer (docs/CLAIM-RECEIPT.md). The course +# folder still verifies standalone — without the tools/ sibling or without +# FLYPYTHON_CLAIM_SECRET set, verify.py behaves exactly as before. +try: + sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "tools")) + import claim_receipt +except ImportError: + claim_receipt = None + +EXPECTED_STARTER_FAILURES = ( + "test_load_json_records_returns_list_of_dicts", + "test_unsupported_suffix_raises_value_error", + "test_invalid_records_are_isolated_with_reasons", + "test_group_totals_are_rounded_to_two_decimals", + "test_write_report_creates_missing_parent_directories", + "test_run_scenario_writes_report_file", + "test_main_prints_summary_and_returns_zero", +) + + + +# ── PER-COURSE BLOCK ──────────────────────────────────────────── +# The only section allowed to differ across shared-core courses +# (checkpoint ids and gates must still match — titles may carry the +# tool's flavor). tools/verify_courses.py enforces this. +COURSE_ID = 'course-cursor' +# Documented constant: claim codes derive deterministically from +# (COURSE_ID, checkpoint_id, COURSE_SALT). They are spot-checkable +# self-reported evidence, not tamper-proof secrets — see +# docs/repo-plan-0.0.4.md FP-411. +COURSE_SALT = '6b187020c4507d6e' + +CHECKPOINTS = [ + {"id": "l01", "gate": "attest", "title": '在编辑器里复现故障 / Reproduce the failure in the editor'}, + {"id": "l02", "gate": "attest", "title": '任务契约与 Agent 模式 / Task contract and Agent mode'}, + {"id": "l03", "gate": "starter-suite", "title": '逐块审 diff 的有界改动 / Bounded change, hunk by hunk'}, + {"id": "l04", "gate": "both-suites", "title": '跑套件与审阅 / Run the suite and review'}, + {"id": "l05", "gate": "attest", "title": '应用到自己的项目 / Apply to your project'}, +] +# ── END PER-COURSE BLOCK ──────────────────────────────────────── + +def _claim_code(checkpoint_id): + digest = hashlib.sha256( + (COURSE_ID + ":" + checkpoint_id + ":" + COURSE_SALT).encode("utf-8") + ).digest() + return base64.b32encode(digest).decode("ascii")[:8] + +def _run_suite(implementation): + environment = os.environ.copy() + environment["PYTHONPATH"] = str(ROOT / implementation) + return subprocess.run( + [sys.executable, "-m", "unittest", "discover", "-s", str(ROOT / "tests")], + env=environment, check=False, capture_output=True, text=True, + ) + +def run_progress(as_json, receipt_out=None): + import time + t0 = time.monotonic() + starter = _run_suite("starter") + starter_ms = int((time.monotonic() - t0) * 1000) + t0 = time.monotonic() + solution = _run_suite("solution") + solution_ms = int((time.monotonic() - t0) * 1000) + starter_ok = starter.returncode == 0 + solution_ok = solution.returncode == 0 + rows = [] + for checkpoint in CHECKPOINTS: + gate = checkpoint["gate"] + if gate == "attest": + status, kind = "attest", "attested" + elif gate == "starter-suite": + status = "passed" if starter_ok else "open" + kind = "objective" + else: + status = "passed" if (starter_ok and solution_ok) else "open" + kind = "objective" + code = _claim_code(checkpoint["id"]) if status in ("passed", "attest") else None + row = dict(checkpoint) + row["status"] = status + row["kind"] = kind + row["claim_code"] = code + rows.append(row) + secret = claim_receipt.receipts_enabled() if claim_receipt else None + receipts = [] + if secret: + starter_tests = claim_receipt.count_tests(starter.stderr) + solution_tests = claim_receipt.count_tests(solution.stderr) + solution_hash = claim_receipt.solution_sha256(ROOT) + for checkpoint in CHECKPOINTS: + gate = checkpoint["gate"] + if gate == "starter-suite": + passed, tests, ms = starter_ok, starter_tests, starter_ms + elif gate == "both-suites": + passed = starter_ok and solution_ok + tests, ms = starter_tests + solution_tests, starter_ms + solution_ms + else: + continue + receipts.append(claim_receipt.make_receipt( + COURSE_ID, checkpoint["id"], passed=passed, tests=tests, + duration_ms=ms, impl_dir=ROOT / "starter", + solution_hash=solution_hash, secret=secret)) + if as_json: + document = {"course": COURSE_ID, + "starter_suite_passed": starter_ok, + "solution_suite_passed": solution_ok, + "checkpoints": rows} + if secret: + document["receipts"] = receipts + print(json.dumps(document, ensure_ascii=False, indent=2)) + else: + starter_state = "passed" if starter_ok else "not passed" + solution_state = "passed" if solution_ok else "not passed" + print("Course " + COURSE_ID) + print("Suites: starter " + starter_state + " / solution " + solution_state) + for row in rows: + state = row["status"] + (" (self-attested)" if row["kind"] == "attested" else "") + code = "claim code " + row["claim_code"] if row["claim_code"] else "—" + print(" " + row["id"] + " " + row["title"] + " [" + state + "] " + code) + print("Claim codes are self-reported evidence, recorded at flypython.com; never a certificate.") + if secret: + print(f"Signed run receipts prepared for {len(receipts)} gated checkpoint(s);" + " submit each with its claim to mark them as a local-run receipt.") + if receipt_out and secret: + Path(receipt_out).write_text( + json.dumps({"receipts": receipts}, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8") + print(f"Wrote {len(receipts)} receipt(s) to {receipt_out}", file=sys.stderr) + return 0 + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("implementation", choices=("progress", "starter", "solution")) + parser.add_argument("--json", action="store_true") + parser.add_argument("--expect-failure", action="store_true") + parser.add_argument("--receipt-out", metavar="PATH", help="write signed run receipts JSON (requires FLYPYTHON_CLAIM_SECRET)") + args = parser.parse_args() + + if args.implementation == "progress": + return run_progress(args.json, args.receipt_out) + + command = [sys.executable, "-m", "unittest", "discover", "-s", str(ROOT / "tests")] + environment = os.environ.copy() + environment["PYTHONPATH"] = str(ROOT / args.implementation) + result = subprocess.run( + command, env=environment, check=False, capture_output=True, text=True + ) + + if args.expect_failure: + if result.returncode == 0: + print("Expected the starter to fail, but it passed.", file=sys.stderr) + return 1 + output = (result.stdout or "") + (result.stderr or "") + missing = [ + name for name in EXPECTED_STARTER_FAILURES if name not in output + ] + if missing: + print( + "Starter failed for unexpected reasons; missing expected failures:", + file=sys.stderr, + ) + print("\n".join(missing), file=sys.stderr) + print(output, file=sys.stderr) + return 1 + print( + "Expected starter state reproduced: JSON inputs crash, invalid rows " + "abort the run, totals are unrounded, and reports need an existing " + "directory." + ) + return 0 + if result.returncode == 0: + print(f"{args.implementation}: all tests passed") + else: + sys.stderr.write((result.stderr or "") or (result.stdout or "")) + return result.returncode + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/courses/hands-on-with-deepseek-harness/COURSE.md b/courses/hands-on-with-deepseek-harness/COURSE.md new file mode 100644 index 0000000..4202350 --- /dev/null +++ b/courses/hands-on-with-deepseek-harness/COURSE.md @@ -0,0 +1,116 @@ +--- +id: course-deepseek-harness +type: course +title: Hands-on with DeepSeek Harness +summary: The verified Python workflow — task contract, bounded change, objective verify.py evidence — taught hands-on in DeepSeek Harness, where the agent loop is composed from plugins and every step lands in an inspectable trajectory. +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +badge: + id: course-deepseek-harness + name_en: Reproduce with DeepSeek Harness in the loop + name_zh: DeepSeek Harness 协同复现 + requires: All five checkpoints claimed (L01–L05) +course_id: course-deepseek-harness +--- + +# Hands-on with DeepSeek Harness + +> TL;DR: download this folder, point a DeepSeek Harness agent at it, and say +> **"start lesson 1"**. You finish with a working report tool, a reproducible +> pass/fail command, and the harness-native workflow: plugins composed in +> `cordis.yml`, and an append-only trajectory you can inspect, resume, and +> fork. The exercise core is the same one the other agent-tool courses use — +> only the tool you drive changes. + +## What you build + +A small Python report tool that reads messy real-world data (CSV or JSON), +isolates invalid rows instead of crashing, aggregates valid rows, and writes +its report atomically. Three scenario "skins" ship with the course so you can +practice on a domain you recognize: + +| Skin | You are… | Data | +| --- | --- | --- | +| `scenario/excel-report/` | turning a weekly orders export into a region summary | `orders.csv` | +| `scenario/data-monitor/` | checking service latency numbers from your servers | `metrics.csv` | +| `scenario/api-tool/` | summarizing ticket data pulled from an internal API | `tickets.json` | + +All three run through the same code and the same tests. The skills — +contracts, error isolation, rounding, atomic writes, verification — are the +actual product; the skins just make them concrete. + +## Teaching contract (read this first, agent) + +If you are the agent running under DeepSeek Harness teaching this course, +follow these rules: + +- **Audience:** a project owner who has working-with-AI experience but is + stuck on reliability — the script runs, then breaks on real data. Not a + Python beginner tutorial; not a prompt-engineering course; not a harness + plugin-development course. +- **Prerequisites:** Python 3.11+ on PATH, plus a working DeepSeek Harness + setup (Python package from the deepseek-harness project) with a configured + model plugin — that means an API key or local model endpoint the learner + supplies. No third-party packages are needed for the exercise itself — + it is standard library only. Setup specifics and free-tier coverage are + unverified at authoring time — see `REVIEW.md`. +- **Tool:** authored against DeepSeek Harness as documented at + deepseek-harness.github.io/deepseek-harness (plugin-composed agents, + `cordis.yml`, append-only trajectory logs with inspect/resume/fork). + A live teaching run-through is pending — `REVIEW.md` records what has and + has not been exercised. A Harness major release triggers a course + re-review. +- **Lesson order:** L01 → L02 → L03 → L04 → L05, one lesson per session. + Never skip the checkpoint at the end of a lesson. +- **Teaching style:** work from the files in this folder, not from general + knowledge. Quote the exact contract line you are satisfying. Make the + smallest change that moves a test from red to green. Never add + dependencies, never edit `solution/`, and ask before touching any file + not named in the current lesson. +- **When to stop:** a lesson is done when its checkpoint command runs and + the learner can say, in their own words, what failed and why. If the + learner cannot, re-teach from the failing test, not from the solution. +- **How to use `verify.py`:** run it in the session's shell/sandbox tool — + `python verify.py starter --expect-failure` must reproduce the listed + failures; `python verify.py solution` must pass. "Done" means the starter + implementation passes the same suite after the learner's bounded change. +- **Honesty rules:** say what you did not verify. Do not claim the code is + production-ready. Do not promise learning or career outcomes. + +## What this course does NOT cover + +Writing your own Cordis plugins, model-provider configuration beyond the +minimum needed to run an agent, sandbox/security tuning, or selling +anything. For those, the companion repository has guides and playbooks — +link, don't improvise. + +## Badge contract + +- Badge: **Reproduce with DeepSeek Harness in the loop Badge** (badge id `course-deepseek-harness`) — earned by claiming all five checkpoints. +- Challenges: L01–L05 checkpoints, 10 points each; +50 course-badge bonus when all five are claimed on flypython.com. +- Evidence: `python verify.py progress` — L03 (bounded change) and L04 (verify & review) are objectively gated by the suite; L01/L02/L05 are learner-attested. +- Submission: each passed checkpoint prints a deterministic claim code; record it on flypython.com against your account. Self-reported evidence, never a certificate. + +## Folder map + +``` +COURSE.md / COURSE_cn.md this file (EN / 中文) +lessons/L01.md … L05.md lessons (each has an _cn.md pair) +scenario/<skin>/ data files and scenario.json per skin +TASK.md / TASK_cn.md the task contract the change must satisfy +starter/report_tool.py the deliberately unfinished implementation +solution/report_tool.py the reviewed solution (do not copy in lesson 3) +tests/test_report_tool.py the contract suite (read-only) +verify.py objective pass/fail evidence +REVIEW.md maintainer run-through record +``` + +## Evidence and licensing + +The course folder is reviewed content: `REVIEW.md` records the last +run-through with dates, tool versions, and observed deviations. Code in this +folder is MIT-licensed; lesson prose is CC BY 4.0 (see repository `LICENSE`). +Report teaching drift or unclear lessons via the repository's +`course-feedback` issue form. diff --git a/courses/hands-on-with-deepseek-harness/COURSE_cn.md b/courses/hands-on-with-deepseek-harness/COURSE_cn.md new file mode 100644 index 0000000..c4227b1 --- /dev/null +++ b/courses/hands-on-with-deepseek-harness/COURSE_cn.md @@ -0,0 +1,106 @@ +--- +id: course-deepseek-harness +type: course +title: "上手 DeepSeek Harness" +summary: "经过验证的 Python 工作流——任务契约、有界改动、客观 verify.py 证据——在 DeepSeek Harness 里手把手教:Agent 闭环由插件组装,每一步都落进可检查的轨迹日志。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +badge: + id: course-deepseek-harness + name_en: Reproduce with DeepSeek Harness in the loop + name_zh: DeepSeek Harness 协同复现 + requires: 认领全部五个检查点(L01–L05) +course_id: course-deepseek-harness +--- + +# 上手 DeepSeek Harness + +> 一句话:下载本文件夹,让一个 DeepSeek Harness Agent 指向它,对它说 +> **「开始第 1 课」**。完成时你会得到一个能跑的报表工具、一条可复现的 +> 通过/失败命令,以及 Harness 原生工作流:用 `cordis.yml` 组装的插件, +> 和一份可检查、可恢复、可分叉的只增轨迹。练习核心与其他 Agent 工具 +> 课程完全相同——变的只是你驱动的工具。 + +## 你要做的东西 + +一个小型 Python 报表工具:读进脏乱的真实数据(CSV 或 JSON)、隔离 +无效行而不是直接崩溃、聚合有效行、原子地写出报告。课程自带三个 +「场景皮肤」,让你在自己熟悉的领域里练习: + +| 皮肤 | 你是…… | 数据 | +| --- | --- | --- | +| `scenario/excel-report/` | 把每周订单导出整理成区域汇总 | `orders.csv` | +| `scenario/data-monitor/` | 检查自家服务器的延迟数据 | `metrics.csv` | +| `scenario/api-tool/` | 汇总从内部 API 拉取的工单数据 | `tickets.json` | + +三个皮肤跑的是同一份代码、同一套测试。真正的产品是这些技能——契约、 +错误隔离、舍入、原子写、验证——皮肤只是让它们变得具体。 + +## 教学契约(先读,Agent) + +如果你是在 DeepSeek Harness 下运行、教授本课程的 Agent,请遵守以下 +规则: + +- **受众:** 有 AI 协作经验、但困在可靠性上的项目所有者——脚本能跑, + 一遇真实数据就坏。这不是 Python 入门教程,不是提示词工程课,也不 + 是 Harness 插件开发课。 +- **前置条件:** PATH 上有 Python 3.11+,以及一套可用的 DeepSeek + Harness 环境(deepseek-harness 项目的 Python 包)并配置好模型插件 + ——即学习者自备 API key 或本地模型端点。练习本身不需要第三方包 + ——只用标准库。安装细节与免费额度覆盖在编写时未经证实——见 + `REVIEW.md`。 +- **工具:** 按 deepseek-harness.github.io/deepseek-harness 上文档 + 描述的 DeepSeek Harness 编写(插件组装的 Agent、`cordis.yml`、 + 可检查/恢复/分叉的只增轨迹日志)。真实授课实跑尚待完成—— + `REVIEW.md` 记录已验证与未验证的部分。Harness 主版本发布会触发 + 课程重审。 +- **课序:** L01 → L02 → L03 → L04 → L05,一次课一个会话。 + 绝不跳过课尾的检查点。 +- **教学风格:** 从本文件夹里的文件出发,不要凭通用知识发挥。引用 + 你正在满足的那条契约原文。做让测试从红变绿的最小改动。绝不加 + 依赖、绝不改 `solution/`、动当前课程未点名的任何文件前先询问。 +- **何时停下:** 一节课的检查点命令跑通、且学习者能用自己的话说清 + 哪里坏了、为什么,这节课才算完。学习者说不出就从失败测试重新教, + 不要从 solution 教。 +- **如何使用 `verify.py`:** 在会话的 shell/沙箱工具里跑—— + `python verify.py starter --expect-failure` 必须复现列出的失败; + `python verify.py solution` 必须通过。「完成」指学习者做完有界 + 改动后,starter 实现通过同一套测试。 +- **诚实规则:** 说清你没验证什么。不要声称代码达到生产可用。不要 + 承诺学习或职业结果。 + +## 本课程不覆盖什么 + +自己编写 Cordis 插件、超出「跑起一个 Agent」最低需求的模型供应商 +配置、沙箱/安全调优,以及任何推销内容。这些由配套仓库的指南与 +手册承载——给链接,不即兴发挥。 + +## 徽章契约 + +- 徽章:**DeepSeek Harness 协同复现徽章**(badge id `course-deepseek-harness`)——认领全部五个检查点获得。 +- 挑战:L01–L05 检查点,各 10 分;在 flypython.com 上集齐五个再加 50 分课程徽章奖励。 +- 证据:`python verify.py progress`——L03(有界改动)与 L04(验证与评审)由套件客观把关;L01/L02/L05 为学习者自我报告。 +- 提交:每个通过的检查点打印一个确定性的认领码;在 flypython.com 上记入你的账号。这是自我报告的证据,从来不是证书。 + +## 文件夹地图 + +``` +COURSE.md / COURSE_cn.md 本文件(EN / 中文) +lessons/L01.md … L05.md 课文(每课都有一个 _cn.md 对) +scenario/<skin>/ 每个皮肤的数据文件与 scenario.json +TASK.md / TASK_cn.md 改动必须满足的任务契约 +starter/report_tool.py 有意未完成的实现 +solution/report_tool.py 评审过的解答(第 3 课不要抄) +tests/test_report_tool.py 契约套件(只读) +verify.py 客观的通过/失败证据 +REVIEW.md 维护者实跑记录 +``` + +## 证据与许可 + +本课程文件夹属于已评审内容:`REVIEW.md` 记录最近一次实跑——日期、 +工具版本、观察到的偏差。文件夹内代码采用 MIT 许可;课文文字采用 +CC BY 4.0(见仓库 `LICENSE`)。发现教学漂移或课程不清楚之处,请 +通过仓库的 `course-feedback` issue 表单反馈。 diff --git a/courses/hands-on-with-deepseek-harness/REVIEW.md b/courses/hands-on-with-deepseek-harness/REVIEW.md new file mode 100644 index 0000000..0347caf --- /dev/null +++ b/courses/hands-on-with-deepseek-harness/REVIEW.md @@ -0,0 +1,31 @@ +# Maintainer run-through record + +## 2026-09-13 — authored from official documentation (mechanical verification only) + +- Course authored against the DeepSeek Harness documentation + (deepseek-harness.github.io/deepseek-harness: plugin architecture, + `cordis.yml` composition, append-only trajectory logs with + inspect/resume/fork) and the shared `report-tool` core. `TASK.md`, + `tests/`, `scenario/`, `starter/`, `solution/` are byte-identical to the + Claude Code / Codex course cores — enforced by the `core-group` check in + `tools/verify_courses.py`. +- Environment for mechanical verification: macOS (arm64), Python 3.13. +- Commands and results: + - `python verify.py starter --expect-failure` — exit 0; expected failures + reproduced. + - `python verify.py solution` — exit 0; 9/9 tests pass. + - `python tools/verify_courses.py` — contract satisfied, core identical. +- **Not done:** any run under DeepSeek Harness. No harness install, model + endpoint, or API key was used; every harness behavior described in the + lessons is sourced from official documentation, not observed. + +## Pending before this course can be called taught-in-tool + +- One full agent-taught run-through under DeepSeek Harness ("start lesson 1" + through the Lesson 5 checkpoint) on a current release, recording observed + deviations from `COURSE.md` here — including the exact `cordis.yml` used + and the model plugin configured. + +## Deviation log + +(none yet — no live run has happened) diff --git a/courses/hands-on-with-deepseek-harness/TASK.md b/courses/hands-on-with-deepseek-harness/TASK.md new file mode 100644 index 0000000..e923183 --- /dev/null +++ b/courses/hands-on-with-deepseek-harness/TASK.md @@ -0,0 +1,37 @@ +# Task contract: scenario report tool (hands-on course core) + +Change only `starter/report_tool.py`. Standard library only; no new +dependencies. + +- `load_records(path) -> list[dict]`: + - `.csv` files load through `csv.DictReader`. + - `.json` files load as a list of objects; a non-list or non-object payload + raises `ValueError`. + - Any other suffix raises `ValueError` naming the unsupported file. +- `build_report(records, *, required_fields, numeric_field, group_field) -> dict`: + - Returns `{"total", "valid", "invalid", "groups", "errors"}`. + - Invalid rows never abort the run: each is collected in `errors` as + `{"index": <row position>, "reason": <short string>}`. Invalid means: + not a dict, missing or blank required field, or a non-numeric + `numeric_field` value (booleans do not count as numbers). + - Valid rows aggregate into `groups[group_value] = {"count": int, + "total": float}`; each group total is rounded to two decimals. + - `total = valid + invalid` must hold for every input. +- `write_report(report, destination)`: + - Writes JSON (UTF-8, indent 2, trailing newline) atomically: write a + sibling temp file, then `os.replace`. + - Creates missing parent directories. + - Never leaves a `.tmp` file behind on success. +- `run_scenario(scenario_dir) -> dict`: + - Reads `scenario.json` (`data_file`, `required_fields`, `numeric_field`, + `group_field`, `report_file`), processes the data file, writes the + report inside the scenario directory, and returns the report. +- `main(argv=None) -> int`: + - Exactly one argument (the scenario directory). Otherwise print usage to + stderr and return 2. + - On success print `total=... valid=... invalid=...` to stdout and return 0. + - On input failure print `error: ...` to stderr and return 1. + +Done means `python verify.py starter` exits 0 with all nine tests passing, +and `python verify.py starter --expect-failure` exits nonzero because the +starter no longer reproduces the unfinished state. diff --git a/courses/hands-on-with-deepseek-harness/TASK_cn.md b/courses/hands-on-with-deepseek-harness/TASK_cn.md new file mode 100644 index 0000000..7af2609 --- /dev/null +++ b/courses/hands-on-with-deepseek-harness/TASK_cn.md @@ -0,0 +1,34 @@ +# 任务契约:场景报表工具(实战课程核心) + +只修改 `starter/report_tool.py`。仅用标准库;不新增依赖。 + +- `load_records(path) -> list[dict]`: + - `.csv` 文件经 `csv.DictReader` 加载。 + - `.json` 文件按对象列表加载;不是列表或元素不是对象时抛出 + `ValueError`。 + - 其他后缀抛出 `ValueError`,并指明不支持的文件名。 +- `build_report(records, *, required_fields, numeric_field, group_field) -> dict`: + - 返回 `{"total", "valid", "invalid", "groups", "errors"}`。 + - 无效行绝不中止运行:每行以 `{"index": <行位置>, "reason": <简短 + 说明>}` 收集进 `errors`。无效指:不是字典、必填字段缺失或为空、 + `numeric_field` 不是数字(布尔值不算数字)。 + - 有效行聚合为 `groups[分组值] = {"count": int, "total": float}`;每个 + 分组总计保留两位小数。 + - 任何输入下都必须满足 `total = valid + invalid`。 +- `write_report(report, destination)`: + - 原子地写入 JSON(UTF-8、缩进 2、末尾换行):先写同名临时文件,再 + `os.replace`。 + - 自动创建缺失的父目录。 + - 成功后不留任何 `.tmp` 文件。 +- `run_scenario(scenario_dir) -> dict`: + - 读取 `scenario.json`(`data_file`、`required_fields`、 + `numeric_field`、`group_field`、`report_file`),处理数据文件,把 + 报告写进场景目录,并返回报告。 +- `main(argv=None) -> int`: + - 恰好一个参数(场景目录)。否则向 stderr 打印用法并返回 2。 + - 成功时向 stdout 打印 `total=... valid=... invalid=...` 并返回 0。 + - 输入失败时向 stderr 打印 `error: ...` 并返回 1。 + +完成的标准是 `python verify.py starter` 以 0 退出且九个测试全部通过, +同时 `python verify.py starter --expect-failure` 非零退出——因为 +starter 已不再复现未完成状态。 diff --git a/courses/hands-on-with-deepseek-harness/core-group b/courses/hands-on-with-deepseek-harness/core-group new file mode 100644 index 0000000..b56b0eb --- /dev/null +++ b/courses/hands-on-with-deepseek-harness/core-group @@ -0,0 +1 @@ +report-tool diff --git a/courses/hands-on-with-deepseek-harness/lessons/L01.md b/courses/hands-on-with-deepseek-harness/lessons/L01.md new file mode 100644 index 0000000..6c2e3d5 --- /dev/null +++ b/courses/hands-on-with-deepseek-harness/lessons/L01.md @@ -0,0 +1,115 @@ +--- +id: course-deepseek-harness-l01 +type: course +title: "Lesson 1: Reproduce the failure, read it back from the trajectory" +summary: "Run a harness-composed agent on the course folder, make the failure objective with verify.py — then read the append-only trajectory to see exactly what the agent did." +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "What this checkpoint tests" + body: "That you can reproduce the failure before fixing it: `python verify.py starter --expect-failure` must print the named failing tests — whether you ran it or the agent's shell tool did, the command is the evidence." + - title: "Everything is a plugin" + body: "In DeepSeek Harness the agent is composed, not built-in: model, tools, session storage, and the loop itself are plugins wired in `cordis.yml`. If a behavior is missing, the answer is usually a plugin — not a flag." + - title: "The trajectory is your record" + body: "Every step lands in an append-only session/trajectory log you can inspect later. Reading it back is how you verify what the agent actually did — not what it said it did." + - title: "When you are done" + body: "You can say which nine behaviors the suite proves and why the starter cannot fake them. `python verify.py progress` then shows this checkpoint's claim code." +--- + +# Lesson 1: Reproduce the failure, read it back from the trajectory + +## Objective + +By the end of this lesson you have a working DeepSeek Harness session +pointed at the course folder, you have run the course's objective +verification command, and you can explain what "the starter fails" means +in test names — confirmed against the session trajectory. No code is +changed in this lesson. + +## Why this lesson exists + +Most AI-written Python dies the same way: the demo input works, the real +input arrives, and the script crashes on row 3 — or worse, quietly +produces a wrong number. The fix does not start with a better prompt. It +starts with turning "it's broken" into a command that anyone can run and +get the same answer. DeepSeek Harness adds a second layer: the agent you +run is *composed* — model, tools, storage, and loop are plugins wired +together in `cordis.yml` — and every step it takes lands in an append-only +trajectory log you can inspect, search, and replay. "Trust me" becomes +"here is the trace." + +## Warm-up (2 minutes) + +Open `starter/report_tool.py` and read it top to bottom. It looks +reasonable: functions with docstrings, type hints, a CLI. This is exactly +what AI-generated code looks like — clean surface, missing behavior. Now +open `TASK.md` and keep it next to you. + +## The lesson + +With a harness session running in the course folder, tell the agent: + +**"Read COURSE.md — it is the teaching contract for this folder. Then read +TASK.md and starter/report_tool.py. Do not change any file. Tell me which +behaviors the tests expect that the starter does not have."** + +While it reads, run the objective verification — yourself in a terminal, +or through the agent's shell/sandbox tool: + +```bash +python verify.py starter --expect-failure +``` + +You should see the starter reproduce four real-world failure classes, +mapped to test names: + +| Failing test | Real-world meaning | +| --- | --- | +| `test_load_json_records_returns_list_of_dicts` | the tool only handles CSV; the API scenario crashes | +| `test_unsupported_suffix_raises_value_error` | an `.xlsx` upload becomes a confusing crash instead of a clear rejection | +| `test_invalid_records_are_isolated_with_reasons` | one empty cell aborts the entire run — no partial report, no reason | +| `test_group_totals_are_rounded_to_two_decimals` | `0.1 + 0.2` prints as `0.30000000000000004` in your report | +| `test_write_report_creates_missing_parent_directories` | the tool cannot write into a fresh output directory | +| `test_run_scenario_writes_report_file`, `test_main_prints_summary_and_returns_zero` | end-to-end consequences of the above | + +Then run the reviewed implementation: + +```bash +python verify.py solution +``` + +Both commands exit 0 — but they mean opposite things. The first proves the +starter is *correctly unfinished*; the second proves the target is +reachable. + +Now do the harness-native step: inspect the session trajectory. Find the +steps where the agent read `TASK.md`, ran (or was told about) the verify +commands, and produced its summary. The trajectory is append-only — it is +the ground truth of what happened, and it is what makes this agent's work +auditable later. + +## Exercise + +Pick the scenario skin you most identify with (`excel-report`, +`data-monitor`, or `api-tool`), open its data file, and find the rows that +will be rejected. Write down, before running anything, which test each bad +row will trigger. Then check yourself against the table above — and find +the trajectory step where the agent reached the same conclusion. + +## Checkpoint + +Run `python verify.py starter --expect-failure` and +`python verify.py solution`. You pass this lesson when you can answer, +without looking: + +1. Which four behaviors separate starter from solution? +2. Why is "expected failure" a success condition, not a contradiction? +3. Where in the trajectory can you prove the agent actually read + `TASK.md`? + +## Expected evidence + +The transcript of both commands, the trajectory steps you found, and your +three answers. Keep them; Lesson 4 will ask for them again. diff --git a/courses/hands-on-with-deepseek-harness/lessons/L01_cn.md b/courses/hands-on-with-deepseek-harness/lessons/L01_cn.md new file mode 100644 index 0000000..1a9b494 --- /dev/null +++ b/courses/hands-on-with-deepseek-harness/lessons/L01_cn.md @@ -0,0 +1,103 @@ +--- +id: course-deepseek-harness-l01 +type: course +title: "第 1 课:复现故障,从轨迹里读回来" +summary: "让 Harness 组装的 Agent 跑在课程文件夹上,用 verify.py 让失败变得客观——再读只增轨迹,看清 Agent 到底做了什么。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "本检查点考察什么" + body: "先复现失败再动手修:`python verify.py starter --expect-failure` 必须打印出点名的失败测试——你自己跑也好、Agent 的 shell 工具跑也好,命令就是证据。" + - title: "一切皆插件" + body: "DeepSeek Harness 的 Agent 是组装出来的,不是内置的:模型、工具、会话存储、循环本身都是 `cordis.yml` 里接好的插件。某个行为没有,答案通常是插件——不是某个开关。" + - title: "轨迹就是你的记录" + body: "每一步都落进可事后检查的只增会话/轨迹日志。读回轨迹才能验证 Agent 实际做了什么——而不是它声称做了什么。" + - title: "完成的标志" + body: "你能说出套件证明了哪九种行为、为什么 starter 伪造不出来。`python verify.py progress` 随即显示本检查点的认领码。" +--- + +# 第 1 课:复现故障,从轨迹里读回来 + +## 目标 + +本课结束时,你有一个指向课程文件夹的 DeepSeek Harness 会话,跑过 +课程的客观验证命令,并能对照会话轨迹解释「starter 失败」在测试名 +里的含义。本课不改任何代码。 + +## 为什么有这节课 + +AI 写的 Python 大多是同一种死法:演示输入能跑,真实输入一到,脚本 +在第 3 行崩掉——或者更糟,悄悄算出一个错的数。修复不从更好的 +提示词开始,而从把「坏了」变成一条任何人都能跑、且得到相同答案的 +命令开始。DeepSeek Harness 再加一层:你跑的 Agent 是*组装*出来的 +——模型、工具、存储、循环都是 `cordis.yml` 里接好的插件——它走的 +每一步都落进可检查、可搜索、可重放的只增轨迹日志。「相信我」变成 +「这是轨迹」。 + +## 热身(2 分钟) + +打开 `starter/report_tool.py`,从头读到尾。它看起来很合理:带 +docstring 的函数、类型标注、CLI。这正是 AI 生成代码的样子—— +表面干净、行为缺失。现在打开 `TASK.md`,放在手边。 + +## 本课内容 + +让 Harness 会话跑在课程文件夹里,对 Agent 说: + +**“读 COURSE.md——它是本文件夹的教学契约。再读 TASK.md 和 +starter/report_tool.py。不要改任何文件。告诉我测试期望哪些行为、 +starter 缺了哪些。”** + +趁它读的时候,跑客观验证——自己在终端里跑,或通过 Agent 的 +shell/沙箱工具跑: + +```bash +python verify.py starter --expect-failure +``` + +你会看到 starter 复现出四类真实世界故障,对应到测试名: + +| 失败的测试 | 真实含义 | +| --- | --- | +| `test_load_json_records_returns_list_of_dicts` | 工具只处理 CSV;API 场景直接崩 | +| `test_unsupported_suffix_raises_value_error` | 一个 `.xlsx` 上传变成莫名其妙的崩溃,而不是清楚的拒绝 | +| `test_invalid_records_are_isolated_with_reasons` | 一个空单元格中止整个运行——没有部分报告,没有原因 | +| `test_group_totals_are_rounded_to_two_decimals` | `0.1 + 0.2` 在报告里打印成 `0.30000000000000004` | +| `test_write_report_creates_missing_parent_directories` | 工具无法写进新建的输出目录 | +| `test_run_scenario_writes_report_file`、`test_main_prints_summary_and_returns_zero` | 上述问题的端到端后果 | + +然后跑评审过的实现: + +```bash +python verify.py solution +``` + +两条命令都以 0 退出——但含义相反。第一条证明 starter *确实处于 +正确的未完成态*;第二条证明目标可达。 + +现在做 Harness 原生的一步:检查会话轨迹。找到 Agent 读 `TASK.md`、 +跑(或被告知)verify 命令、产出总结的那几步。轨迹是只增的——它是 +发生之事的底账,也是以后让 Agent 工作可审计的依据。 + +## 练习 + +挑一个你最有代入感的场景皮肤(`excel-report`、`data-monitor` 或 +`api-tool`),打开它的数据文件,找出会被拒绝的行。先不看运行结果, +写下每个坏行会触发哪个测试,再对照上面的表自查——并在轨迹里找到 +Agent 得出相同结论的那一步。 + +## 检查点 + +运行 `python verify.py starter --expect-failure` 和 +`python verify.py solution`。不看材料能回答下面三个问题即算通过: + +1. 哪四个行为把 starter 和 solution 区分开? +2. 为什么「预期失败」是成功条件而不是自相矛盾? +3. 在轨迹的哪里能证明 Agent 确实读过 `TASK.md`? + +## 预期证据 + +两条命令的完整输出记录、你找到的轨迹步骤、加你的三个答案。保留好 +——第 4 课还会用到。 diff --git a/courses/hands-on-with-deepseek-harness/lessons/L02.md b/courses/hands-on-with-deepseek-harness/lessons/L02.md new file mode 100644 index 0000000..a76fab8 --- /dev/null +++ b/courses/hands-on-with-deepseek-harness/lessons/L02.md @@ -0,0 +1,88 @@ +--- +id: course-deepseek-harness-l02 +type: course +title: "Lesson 2: Write the task contract, let cordis.yml carry the setup" +summary: 'Turn "make it better" into a bounded, testable contract — then make the working rules part of the composed agent, not a forgotten prompt.' +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "What this checkpoint tests" + body: "That you can read `TASK.md` as testable statements, not prose. Every sentence that starts with a function name is a contract line the suite can assert." + - title: "Composition is the instruction chain" + body: "A harness agent is assembled in `cordis.yml`: which model plugin, which tool plugins, what system instructions, what session storage. Rules you want every run to follow belong in that composition — not retyped per session." + - title: "The testability check" + body: "If you cannot tell whether a statement is testable, ask: could a suite assert it without reading your mind? Numbers, exit codes, files on disk — never vibes." +--- + +# Lesson 2: Write the task contract, let cordis.yml carry the setup + +## Objective + +You can read `TASK.md` as a set of testable statements, trace each +statement to a test in `tests/test_report_tool.py`, and make working rules +part of the composed agent instead of a prompt you have to retype. + +## Why this lesson exists + +Vague requests produce vague code. "Handle bad rows better" gives an agent +permission to guess; "invalid rows are collected in `errors` with index +and reason, valid rows still produce a report" gives it a target and +gives you a way to check. With a composed agent there is a second failure +mode: the rules you typed into one session are not in the next — unless +they live in the composition. + +## The lesson + +Open `TASK.md`. Notice what every line has in common: it names an +observable behavior, not an implementation. Four statements from the +contract, and the tests that pin them: + +| Contract line | Test | +| --- | --- | +| "a JSON file loads into the same record list as CSV" | `test_load_json_records_returns_list_of_dicts` | +| "invalid rows land in `errors` with index and reason; valid rows still aggregate" | `test_invalid_records_are_isolated_with_reasons` | +| "group totals are rounded to two decimals" | `test_group_totals_are_rounded_to_two_decimals` | +| "the report write is atomic and creates missing parents" | `test_write_report_creates_missing_parent_directories` | + +Now the durable half. In DeepSeek Harness the agent is declared in +`cordis.yml`: the model plugin, the tool plugins it may use, the session +storage where trajectories land, and the instructions it starts with. Put +this course's working rules where they survive restarts — in the agent's +configured instructions, for example: + +``` +- Change only starter/report_tool.py. tests/, solution/, scenario/ are read-only. +- One failing test group per step; run `python verify.py starter` after each. +- Standard library only — no new dependencies. +``` + +Start a *new* session and ask the agent to state its working rules. If it +quotes yours back, the composition is carrying them; if not, check whether +the instructions actually landed in the configuration the session loaded. + +## Exercise + +Write one contract line for a script you actually own, using the same +shape: inputs, outputs, error cases, and "done means `<command>` exits 0". +Then write the two rules you would put in that project's agent +composition. + +## Checkpoint + +Run `python verify.py progress` — this checkpoint's claim code prints when +you can answer: + +1. Which `TASK.md` line does + `test_invalid_records_are_isolated_with_reasons` pin, in your own + words? +2. Which artifact carries rules across sessions — the prompt, or the + `cordis.yml` composition? +3. Why does an append-only trajectory make a composed agent easier to + audit than a chat transcript? + +## Expected evidence + +Your drafted contract line, your two rules, and the new session's +statement of its working rules. diff --git a/courses/hands-on-with-deepseek-harness/lessons/L02_cn.md b/courses/hands-on-with-deepseek-harness/lessons/L02_cn.md new file mode 100644 index 0000000..2c022f4 --- /dev/null +++ b/courses/hands-on-with-deepseek-harness/lessons/L02_cn.md @@ -0,0 +1,79 @@ +--- +id: course-deepseek-harness-l02 +type: course +title: "第 2 课:写任务契约,让 cordis.yml 承载配置" +summary: "把「改好一点」变成有边界、可测试的契约——再把工作规则放进组装出来的 Agent 里,而不是会被遗忘的提示词。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "本检查点考察什么" + body: "你能把 `TASK.md` 当成可测试的陈述来读,而不是散文。每个以函数名开头的句子都是套件能断言的契约行。" + - title: "组装就是指令链" + body: "Harness 的 Agent 在 `cordis.yml` 里组装:用哪个模型插件、哪些工具插件、什么系统指令、会话存到哪。想让每次运行都遵守的规则要写进组装里——不要每个会话重打一遍。" + - title: "可测试性检查" + body: "判断一句话能不能测:测试套件不看你的心思能不能断言它?数字、退出码、磁盘上的文件——不看感觉。" +--- + +# 第 2 课:写任务契约,让 cordis.yml 承载配置 + +## 目标 + +你能把 `TASK.md` 当成一组可测试陈述来读,把每条陈述追查到 +`tests/test_report_tool.py` 里的一个测试,并把工作规则做成组装 +Agent 的一部分,而不是每次都要重打的提示词。 + +## 为什么有这节课 + +含糊的需求产出含糊的代码。「把坏行处理好一点」是许可 Agent 去猜; +「无效行收进 `errors`、带下标和原因,有效行照常产出报告」是给它 +目标、也给你检查的方法。用组装式 Agent 还有第二种失败:你在某个 +会话里敲的规则下一个会话没有——除非它们活在组装配置里。 + +## 本课内容 + +打开 `TASK.md`。注意每行的共同点:它描述的是可观察的行为,不是 +实现。契约里的四句陈述,以及钉住它们的测试: + +| 契约行 | 测试 | +| --- | --- | +| 「JSON 文件要加载成与 CSV 相同的记录列表」 | `test_load_json_records_returns_list_of_dicts` | +| 「无效行带下标和原因进 `errors`;有效行照常聚合」 | `test_invalid_records_are_isolated_with_reasons` | +| 「分组总计保留两位小数」 | `test_group_totals_are_rounded_to_two_decimals` | +| 「报告写出是原子的,且会创建缺失的父目录」 | `test_write_report_creates_missing_parent_directories` | + +再看持久的另一半。在 DeepSeek Harness 里,Agent 声明在 +`cordis.yml` 中:模型插件、可用工具插件、轨迹落盘的会话存储、 +以及它启动时带上的指令。把本课程的工作规则放到重启后仍在的地方 +——例如 Agent 的配置指令里: + +``` +- 只改 starter/report_tool.py。tests/、solution/、scenario/ 只读。 +- 每步只修一组失败测试;修完跑 `python verify.py starter`。 +- 只用标准库——不加新依赖。 +``` + +开一个*新*会话,让 Agent 陈述它的工作规则。它把你的规则原样引 +回来,说明组装配置带上了;没有就检查指令是否真的写进了会话加载 +的那份配置。 + +## 练习 + +为你真正拥有的一个脚本写一条契约行,用同样的形状:输入、输出、 +错误情况、以及「完成 = `<命令>` 退出码为 0」。再写你会放进那个 +项目 Agent 组装配置里的两条规则。 + +## 检查点 + +运行 `python verify.py progress`——能回答下面三题时,本检查点的 +认领码就会显示: + +1. 用你自己的话说,`test_invalid_records_are_isolated_with_reasons` + 钉住的是 `TASK.md` 哪一行? +2. 跨会话携带规则的是提示词还是 `cordis.yml` 组装配置? +3. 为什么只增轨迹让组装式 Agent 比聊天转录更好审计? + +## 预期证据 + +你起草的契约行、两条规则,以及新会话对自己工作规则的陈述。 diff --git a/courses/hands-on-with-deepseek-harness/lessons/L03.md b/courses/hands-on-with-deepseek-harness/lessons/L03.md new file mode 100644 index 0000000..5a4e42c --- /dev/null +++ b/courses/hands-on-with-deepseek-harness/lessons/L03.md @@ -0,0 +1,90 @@ +--- +id: course-deepseek-harness-l03 +type: course +title: "Lesson 3: The bounded change, audited from the trajectory" +summary: "Drive the starter to green through bounded steps — then prove the agent stayed in bounds by inspecting the append-only trajectory." +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "What this checkpoint tests" + body: "That the starter passes all nine tests through changes you supervised — `python verify.py progress` shows this code only once the starter suite is green." + - title: "The trajectory is the audit" + body: "Because the log is append-only, you can inspect each tool call and file edit after the fact — and fork the session if a step went somewhere you did not want." + - title: "When you are done" + body: "Every edit maps to a contract line, the trajectory shows nothing outside starter/report_tool.py was touched, and you can explain each change without opening solution/." +--- + +# Lesson 3: The bounded change, audited from the trajectory + +## Objective + +The starter passes all nine tests through a sequence of bounded changes, +the trajectory proves nothing else was touched, and you can explain every +edit without reading the solution first. + +## Why this lesson exists + +This is the lesson people skip — and the one that builds the actual skill. +Letting an agent rewrite the file in one shot teaches you nothing and +leaves you a change you cannot honestly review. Supervising small steps, +each pinned to a contract line, is how the code stays yours. DeepSeek +Harness makes the supervision auditable: the session trajectory is +append-only, so what the agent did is a record you inspect — not a claim +you trust. + +## The lesson + +Start a fresh session and say: + +**"Work through TASK.md against starter/report_tool.py. One failing test +group at a time: JSON loading, then validation isolation, then rounding, +then atomic writes, then the end-to-end tests. After each group, run +`python verify.py starter` and stop for my review."** + +Hold it to the contract's boundaries: + +- Only `starter/report_tool.py` changes. If the trajectory shows a write + to `tests/`, `solution/`, or `scenario/`, stop the session and fork from + before that step. +- No new imports outside the standard library. +- Each change should move toward one contract line. Reject drive-by + refactors. +- If the agent wants to change a test, the answer is no. Tests are the + contract; the code moves. + +Use the harness's strengths deliberately: + +- **Inspect the trajectory after each step.** Did it run the command it + claimed? Did it touch only the file it said? +- **Fork instead of argue.** If a step went wrong, fork the session from + the last good step rather than stacking corrections on a bad state. +- **Read the diff, not the summary.** The trajectory shows the actual + edit; the agent's summary is its own narration. When they disagree, the + trajectory wins. + +Expect the failing count to drop group by group: 7 → 5 → 4 → 3 → 2 → 0. + +## Exercise + +Pick one edit the agent made — ideally the validation-isolation change — +and find it in the trajectory. Explain it back: "this edit does X; that +satisfies contract line Y." If you cannot, ask the agent to walk you +through that trajectory step before continuing. + +## Checkpoint + +Run `python verify.py progress`. This checkpoint's code appears only when +the starter suite is green. You pass the lesson when you can answer: + +1. How many steps did the bounded change take, and what made each step + bounded? +2. Which trajectory entry did you reject or fork away from, and why? +3. Point at the edit that implements "isolate invalid rows with reasons" + — which trajectory step is it? + +## Expected evidence + +A green `python verify.py starter` run, plus the trajectory steps showing +each bounded edit and the verify runs between them. diff --git a/courses/hands-on-with-deepseek-harness/lessons/L03_cn.md b/courses/hands-on-with-deepseek-harness/lessons/L03_cn.md new file mode 100644 index 0000000..f84ce2a --- /dev/null +++ b/courses/hands-on-with-deepseek-harness/lessons/L03_cn.md @@ -0,0 +1,79 @@ +--- +id: course-deepseek-harness-l03 +type: course +title: "第 3 课:有界改动,用轨迹做审计" +summary: "通过有界的步骤把 starter 推到全绿——再检查只增轨迹,证明 Agent 没有越界。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "本检查点考察什么" + body: "starter 通过你监督的改动让九个测试全绿——`python verify.py progress` 只在 starter 套件变绿后显示本检查点的码。" + - title: "轨迹就是审计" + body: "日志是只增的,事后可以逐条检查每次工具调用与文件编辑——某步走偏了就从那步之前分叉会话。" + - title: "完成的标志" + body: "每处编辑都能对上一条契约行,轨迹显示 starter/report_tool.py 之外没动过任何东西,而且不看 solution/ 你也讲得清每处改动。" +--- + +# 第 3 课:有界改动,用轨迹做审计 + +## 目标 + +starter 通过一串有界改动让九个测试全绿,轨迹证明没有碰过别的 +东西,并且不看 solution,你也讲得清每一处编辑。 + +## 为什么有这节课 + +这节课最容易被跳过——也恰恰是长本事的那节。放任 Agent 一次重写 +整个文件,什么也学不到,只留下一份你没底气审查的改动。监督一连串 +小步骤、各自钉在一条契约行上,才能让 AI 写的代码仍然是你的。 +DeepSeek Harness 让监督可审计:会话轨迹是只增的,Agent 做了什么 +是供你检查的记录——不是要你信任的声明。 + +## 本课内容 + +开一个新会话,说: + +**“按 TASK.md 改 starter/report_tool.py。一次只修一组失败测试: +先 JSON 加载,再无效行隔离,再舍入,再原子写,最后是端到端测试。 +每修完一组跑 `python verify.py starter`,然后停下来等我审。”** + +守住契约边界: + +- 只动 `starter/report_tool.py`。轨迹里出现对 `tests/`、 + `solution/`、`scenario/` 的写入,就停掉会话,从那步之前分叉。 +- 标准库之外不加 import。 +- 每处改动朝一条契约行推进。顺手重构一律拒绝。 +- Agent 想改测试,答案是不行。测试是契约;动的是代码。 + +刻意用 Harness 的长处: + +- **每步之后检查轨迹。** 它跑的命令和它声称的一致吗?碰的文件 + 和它说的一致吗? +- **分叉代替争论。** 某步走错了,从上一个好步骤分叉会话,而不是 + 在坏状态上叠补丁。 +- **看 diff,不看总结。** 轨迹里是实际的编辑;Agent 的总结是它的 + 自述。两者不一致时以轨迹为准。 + +预期失败数逐组下降:7 → 5 → 4 → 3 → 2 → 0。 + +## 练习 + +挑 Agent 的一处编辑——最好是「无效行隔离」那段——在轨迹里找到 +它,讲回来:「这处编辑做了 X,满足契约行 Y」。讲不清就先让它走读 +那个轨迹步骤,再继续。 + +## 检查点 + +运行 `python verify.py progress`。本检查点的码只在 starter 套件 +全绿时出现。不看材料能回答即算通过: + +1. 这次有界改动走了几步,是什么让每步保持有界? +2. 你拒绝或分叉掉过哪条轨迹记录,为什么? +3. 指出实现「无效行带原因隔离」的那处编辑——对应哪个轨迹步骤? + +## 预期证据 + +一次全绿的 `python verify.py starter` 运行,加上显示每处有界编辑 +与其间 verify 运行的轨迹步骤。 diff --git a/courses/hands-on-with-deepseek-harness/lessons/L04.md b/courses/hands-on-with-deepseek-harness/lessons/L04.md new file mode 100644 index 0000000..ba22178 --- /dev/null +++ b/courses/hands-on-with-deepseek-harness/lessons/L04.md @@ -0,0 +1,83 @@ +--- +id: course-deepseek-harness-l04 +type: course +title: "Lesson 4: Verify like a reviewer, not a spectator" +summary: "Green tests are the floor, not the ceiling — run both suites, walk the scenario skins, and review the trajectory the way you would review a colleague's work." +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "What this checkpoint tests" + body: "That starter AND solution both pass — `verify.py progress` prints this code only when both suites are green. The gate is the pair, not a single run." + - title: "Review the record, not the narration" + body: "The append-only trajectory is the auditable record of every command and edit. Review it end to end: every edit maps to a contract line, every command really ran, no test was weakened." + - title: "When you are done" + body: "You have written three evidence lines — verified / not verified / known limits — and could defend the change in a code review." +--- + +# Lesson 4: Verify like a reviewer, not a spectator + +## Objective + +Both suites pass, and you hold a written record of what the green runs do +and do not prove — plus a trajectory you have reviewed end to end. + +## Why this lesson exists + +"All tests pass" is where most AI-assisted work stops — and where most +silent bugs start. A suite proves the contract lines it asserts; it says +nothing about the rows nobody wrote a test for. Verification is a report +you write, not a feeling you have. The harness's append-only trajectory +makes the review concrete: every command and edit is already logged — +your job is to actually read it. + +## The lesson + +Run the full pair: + +```bash +python verify.py starter # the implementation you supervised +python verify.py solution # the reviewed reference +``` + +Both green means the contract is satisfied *as written*. Now do the review +the suite cannot do for you: + +1. **Walk a skin end-to-end.** Pick a `scenario/<skin>/` folder and run + the tool on it: `python starter/report_tool.py scenario/<skin>`. Read + the produced report against the input file — recompute one group total + by hand. +2. **Review the trajectory end to end.** Read every step the session + took: the reads, the edits, the commands. Every edit should map to a + contract line you named in Lesson 2; every verify run should actually + appear. Anything unexplained is scope creep — replay or fork the + session around it. +3. **Check the edges the tests skip.** Feed the tool an empty CSV, a CSV + with only invalid rows, a missing file. The contract does not pin + these — your judgment does. Write down what it does. + +## Exercise + +Write the three-line evidence record for this change: + +- **Verified:** the commands that ran (with their trajectory entries) and + what they proved. +- **Not verified:** the inputs and behaviors no test pins. +- **Known limits:** anything you saw that the contract never promised. + +Keep it honest — "not verified" is a list of facts, not a confession. + +## Checkpoint + +Run `python verify.py progress`. This checkpoint's code prints only when +both suites pass. You pass the lesson when you can answer: + +1. What did your by-hand recompute of a group total show? +2. Name one input the suite does not pin and what the tool does with it. +3. Which trajectory step, if any, would you ask a colleague to justify? + +## Expected evidence + +Both green runs, your three-line evidence record, and the reviewed +trajectory. diff --git a/courses/hands-on-with-deepseek-harness/lessons/L04_cn.md b/courses/hands-on-with-deepseek-harness/lessons/L04_cn.md new file mode 100644 index 0000000..01242de --- /dev/null +++ b/courses/hands-on-with-deepseek-harness/lessons/L04_cn.md @@ -0,0 +1,77 @@ +--- +id: course-deepseek-harness-l04 +type: course +title: "第 4 课:像评审者一样验证,别当观众" +summary: "测试全绿是地板不是天花板——两个套件都跑、三个皮肤都走一遍、按评审同事工作的标准审这条轨迹。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "本检查点考察什么" + body: "starter 和 solution 双双通过——`verify.py progress` 只在两个套件都绿时打印本检查点的码。门槛是一对结果,不是一次运行。" + - title: "审记录,不听自述" + body: "只增轨迹是每条命令、每处编辑的可审计记录。从头审到尾:每处编辑对上一条契约行、每条命令真的跑过、没有测试被放松。" + - title: "完成的标志" + body: "你已写下三行证据——已验证 / 未验证 / 已知局限——并且能在代码评审里为这次改动辩护。" +--- + +# 第 4 课:像评审者一样验证,别当观众 + +## 目标 + +两个套件都通过,你手握一份书面记录:说清绿色运行证明了什么、没 +证明什么——外加一条你从头到尾审过的轨迹。 + +## 为什么有这节课 + +「测试全过」是大多数 AI 辅助工作停下的地方——也是大多数隐性 +bug 开始的地方。套件只证明它断言过的契约行;没人写过测试的行, +它一概不担保。验证是你写的一份报告,不是一种感觉。Harness 的 +只增轨迹让评审变具体:每条命令、每处编辑都已落账——你要做的 +是真的去读它。 + +## 本课内容 + +跑完整的一对命令: + +```bash +python verify.py starter # 你监督出来的实现 +python verify.py solution # 评审过的参考实现 +``` + +双全绿意味着契约*按字面*被满足。现在做套件替不了你的评审: + +1. **走完一个皮肤。** 挑一个 `scenario/<skin>/` 文件夹,用工具跑 + 它:`python starter/report_tool.py scenario/<skin>`。对照输入 + 文件读产出的报告——亲手重算一个分组总计。 +2. **把轨迹从头审到尾。** 读会话走过的每一步:读过的文件、做过的 + 编辑、跑过的命令。每处编辑都应能对上你在第 2 课点名的契约行; + 每次 verify 运行都应真实出现。解释不了的就是范围蔓延——围绕 + 它重放或分叉会话。 +3. **检查测试没钉的边界。** 喂给工具一个空 CSV、一个全是无效行 + 的 CSV、一个不存在的文件。契约没钉这些——你的判断钉。记下 + 它的行为。 + +## 练习 + +为这次改动写三行证据记录: + +- **已验证:** 跑过的命令(连同其轨迹条目)和它们证明了什么。 +- **未验证:** 没有测试钉住的输入与行为。 +- **已知局限:** 你看到的、契约从未承诺过的东西。 + +写得诚实——「未验证」是一份事实清单,不是忏悔书。 + +## 检查点 + +运行 `python verify.py progress`。本检查点的码只在两个套件都过 +时打印。不看材料能回答即算通过: + +1. 你手工重算的分组总计结果如何? +2. 说出一个套件没钉的输入,以及工具对它的实际行为。 +3. 哪个轨迹步骤(如果有)你会请同事来解释? + +## 预期证据 + +两次全绿运行、你的三行证据记录、审完的轨迹。 diff --git a/courses/hands-on-with-deepseek-harness/lessons/L05.md b/courses/hands-on-with-deepseek-harness/lessons/L05.md new file mode 100644 index 0000000..6d6a436 --- /dev/null +++ b/courses/hands-on-with-deepseek-harness/lessons/L05.md @@ -0,0 +1,76 @@ +--- +id: course-deepseek-harness-l05 +type: course +title: "Lesson 5: Take the workflow to your own project" +summary: "The pattern generalizes: task contract, composed agent, bounded steps, trajectory audit — run the same loop on a script you actually own." +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "What this checkpoint tests" + body: "Self-attested: you confirm you applied the loop to your own project. The evidence is your contract, your session's trajectory, and your verification record — not a code we can check." + - title: "Carry the shape, not the code" + body: "What transfers is the loop: contract first, rules in the composition, one bounded step at a time, trajectory audited after, verification written down." + - title: "When you are done" + body: "Your own script has a contract, one bounded change you can explain, and a verified/not-verified/known-limits record — or a documented reason the loop did not fit." +--- + +# Lesson 5: Take the workflow to your own project + +## Objective + +You apply the whole loop — contract, composed agent, bounded steps, +trajectory audit, written verification — to a script or project you +actually own, and record what happened. + +## Why this lesson exists + +A course folder is a safe sandbox; your own codebase is not. The point of +the last four lessons was never the report tool — it was a repeatable way +to run an agent that keeps you in charge of what ships. This checkpoint +exists to prove the transfer happened. It is self-attested: only you know +whether the loop actually ran. + +## The lesson + +Pick a real script you own — one with a bug you have been putting off, or +a behavior you want but have not written. Then run the same five moves: + +1. **Contract.** Write the task contract first: inputs, outputs, error + cases, and "done means `<command>` exits 0". One short paragraph, every + sentence testable. +2. **Composition.** Point a harness agent at the project with the working + rules in its configuration — the files it may touch, the verification + command, the dependency policy. +3. **Bounded steps.** One session, one contract line at a time, stopping + for your review between steps. +4. **Verify.** Run the verification command, then do the review the suite + cannot do — walk one real input end-to-end and read the trajectory top + to bottom. +5. **Record.** Write the three lines: verified / not verified / known + limits. + +If the loop fought you — the task was too small to need a contract, or too +vague to test — that is a finding too. Write down where the shape did not +fit; that judgment is the actual skill. + +## Exercise + +If your project has more than one boundary worth pinning, draft the next +contract line now — you will want it when you start the next session. + +## Checkpoint + +This checkpoint is self-attested: claim it when you can answer: + +1. What was your contract's "done means" command? +2. Which trajectory step did you push back on? +3. What is on your "not verified" list that a stranger would assume was + covered? + +## Expected evidence + +Your contract paragraph, the agent configuration you used, and the +three-line verification record. These are yours — they are the proof the +workflow transferred. diff --git a/courses/hands-on-with-deepseek-harness/lessons/L05_cn.md b/courses/hands-on-with-deepseek-harness/lessons/L05_cn.md new file mode 100644 index 0000000..10c07f4 --- /dev/null +++ b/courses/hands-on-with-deepseek-harness/lessons/L05_cn.md @@ -0,0 +1,66 @@ +--- +id: course-deepseek-harness-l05 +type: course +title: "第 5 课:把工作流带回自己的项目" +summary: "模式会泛化:任务契约、组装 Agent、有界步骤、轨迹审计——在你真正拥有的脚本上跑同一套闭环。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "本检查点考察什么" + body: "自我报告:你确认把这套闭环用到了自己的项目上。证据是你的契约、你会话的轨迹、你的验证记录——不是我们查得了的码。" + - title: "带走形状,不是代码" + body: "能迁移的是闭环:契约先行、规则进组装配置、一次一个有界步骤、事后审轨迹、验证写下来。" + - title: "完成的标志" + body: "你自己的脚本有了一份契约、一处你讲得清的有界改动、一份已验证/未验证/已知局限记录——或一条写清楚的「闭环不合身」的原因。" +--- + +# 第 5 课:把工作流带回自己的项目 + +## 目标 + +你把整套闭环——契约、组装 Agent、有界步骤、轨迹审计、书面验证 +——用到自己真正拥有的脚本或项目上,并记录发生了什么。 + +## 为什么有这节课 + +课程文件夹是安全沙箱;你自己的代码库不是。前四课的意义从来不是 +那个报表工具——而是一种可复用的、让你始终掌控产出的 Agent 运行 +方式。本检查点要证明迁移真的发生了。它是自我报告的:闭环跑没跑 +过只有你知道。 + +## 本课内容 + +挑一个你真正拥有的脚本——一个你拖了很久的 bug,或一个想要但 +还没写的行为。然后走同样的五步: + +1. **契约。** 先写任务契约:输入、输出、错误情况、以及「完成 = + `<命令>` 退出码为 0」。一小段,每句都可测试。 +2. **组装。** 让 Harness Agent 指向项目,并把工作规则写进它的 + 配置——它可以碰哪些文件、验证命令是什么、依赖政策。 +3. **有界步骤。** 一个会话、一次一条契约行,每步之间停下来等你审。 +4. **验证。** 跑验证命令,然后做套件做不了的评审——拿一个真实 + 输入端到端走一遍,并把轨迹从头读到尾。 +5. **记录。** 写三行:已验证 / 未验证 / 已知局限。 + +如果闭环跟你打架——任务小到不需要契约,或模糊到不可测——那也 +是发现。写下形状不合身的地方;这个判断本身就是真本事。 + +## 练习 + +如果你的项目还有不止一处值得钉住的边界,现在就起草下一条契约行 +——开下一个会话时你会用得上。 + +## 检查点 + +本检查点自我报告:能回答下面三题时即可认领: + +1. 你的契约里「完成 =」命令是什么? +2. 你在哪个轨迹步骤上顶回去过? +3. 你的「未验证」清单上有什么是陌生人会以为已覆盖的? + +## 预期证据 + +你的契约段落、你用的 Agent 配置、三行验证记录。它们是你自己的 +——是工作流完成迁移的证明。 diff --git a/courses/hands-on-with-deepseek-harness/scenario/api-tool/scenario.json b/courses/hands-on-with-deepseek-harness/scenario/api-tool/scenario.json new file mode 100644 index 0000000..e17e7c2 --- /dev/null +++ b/courses/hands-on-with-deepseek-harness/scenario/api-tool/scenario.json @@ -0,0 +1,8 @@ +{ + "skin": "api-tool", + "data_file": "tickets.json", + "required_fields": ["ticket_id", "priority", "minutes"], + "numeric_field": "minutes", + "group_field": "priority", + "report_file": "report.json" +} diff --git a/courses/hands-on-with-deepseek-harness/scenario/api-tool/tickets.json b/courses/hands-on-with-deepseek-harness/scenario/api-tool/tickets.json new file mode 100644 index 0000000..03e35b7 --- /dev/null +++ b/courses/hands-on-with-deepseek-harness/scenario/api-tool/tickets.json @@ -0,0 +1,9 @@ +[ + {"ticket_id": "T-101", "priority": "high", "minutes": "45"}, + {"ticket_id": "T-102", "priority": "normal", "minutes": "20"}, + {"ticket_id": "T-103", "priority": "low", "minutes": "10"}, + {"ticket_id": "T-104", "priority": "high", "minutes": "90"}, + {"ticket_id": "T-105", "priority": "", "minutes": "30"}, + {"ticket_id": "T-106", "priority": "normal", "minutes": "n/a"}, + {"ticket_id": "T-107", "priority": "low", "minutes": "25"} +] diff --git a/courses/hands-on-with-deepseek-harness/scenario/data-monitor/metrics.csv b/courses/hands-on-with-deepseek-harness/scenario/data-monitor/metrics.csv new file mode 100644 index 0000000..15a5a15 --- /dev/null +++ b/courses/hands-on-with-deepseek-harness/scenario/data-monitor/metrics.csv @@ -0,0 +1,8 @@ +host,service,latency_ms +web-1,api,182 +web-2,api,205 +db-1,postgres,44 +web-3,frontend,96 +db-2,,51 +web-4,api,too-slow +cache-1,redis,12 diff --git a/courses/hands-on-with-deepseek-harness/scenario/data-monitor/scenario.json b/courses/hands-on-with-deepseek-harness/scenario/data-monitor/scenario.json new file mode 100644 index 0000000..b103477 --- /dev/null +++ b/courses/hands-on-with-deepseek-harness/scenario/data-monitor/scenario.json @@ -0,0 +1,8 @@ +{ + "skin": "data-monitor", + "data_file": "metrics.csv", + "required_fields": ["host", "service", "latency_ms"], + "numeric_field": "latency_ms", + "group_field": "service", + "report_file": "report.json" +} diff --git a/courses/hands-on-with-deepseek-harness/scenario/excel-report/orders.csv b/courses/hands-on-with-deepseek-harness/scenario/excel-report/orders.csv new file mode 100644 index 0000000..a41fca1 --- /dev/null +++ b/courses/hands-on-with-deepseek-harness/scenario/excel-report/orders.csv @@ -0,0 +1,8 @@ +order_id,region,amount,item_count +1001,emea,249.99,3 +1002,apac,89.50,1 +1003,emea,,2 +1004,americas,120.00,5 +1005,,310.00,4 +1006,apac,45.25,2 +1007,emea,640.10,6 diff --git a/courses/hands-on-with-deepseek-harness/scenario/excel-report/scenario.json b/courses/hands-on-with-deepseek-harness/scenario/excel-report/scenario.json new file mode 100644 index 0000000..66b2bd9 --- /dev/null +++ b/courses/hands-on-with-deepseek-harness/scenario/excel-report/scenario.json @@ -0,0 +1,8 @@ +{ + "skin": "excel-report", + "data_file": "orders.csv", + "required_fields": ["order_id", "region", "amount"], + "numeric_field": "amount", + "group_field": "region", + "report_file": "report.json" +} diff --git a/courses/hands-on-with-deepseek-harness/solution/report_tool.py b/courses/hands-on-with-deepseek-harness/solution/report_tool.py new file mode 100644 index 0000000..65ba0bf --- /dev/null +++ b/courses/hands-on-with-deepseek-harness/solution/report_tool.py @@ -0,0 +1,134 @@ +"""Scenario report tool (reviewed solution). + +Reads a scenario data file (CSV or JSON), isolates invalid rows instead of +crashing, aggregates valid rows per group with two-decimal rounding, and +writes the report atomically so an interrupted run never leaves a half-written +file. Standard library only. +""" + +from __future__ import annotations + +import csv +import json +import os +import sys +from pathlib import Path + +SUPPORTED_SUFFIXES = {".csv", ".json"} + + +def load_records(path: str | Path) -> list[dict]: + """Load CSV or JSON records from *path* as a list of dicts.""" + source = Path(path) + suffix = source.suffix.lower() + if suffix not in SUPPORTED_SUFFIXES: + raise ValueError(f"unsupported data file type: {source.name}") + if suffix == ".csv": + with source.open("r", encoding="utf-8", newline="") as handle: + records = list(csv.DictReader(handle)) + else: + data = json.loads(source.read_text(encoding="utf-8")) + records = data if isinstance(data, list) else None + if not all(isinstance(record, dict) for record in records): + raise ValueError("data file must contain a list of objects") + return records + + +def _field_text(record: dict, field: str) -> str: + value = record.get(field) + if not isinstance(value, str): + return "" if value is None else str(value).strip() + return value.strip() + + +def build_report( + records: list[dict], + *, + required_fields: list[str], + numeric_field: str, + group_field: str, +) -> dict: + """Aggregate *records*, isolating invalid rows with recorded reasons.""" + groups: dict[str, dict] = {} + errors: list[dict] = [] + for index, record in enumerate(records): + if not isinstance(record, dict): + errors.append({"index": index, "reason": "record is not an object"}) + continue + missing = [ + field for field in required_fields if not _field_text(record, field) + ] + if missing: + errors.append( + {"index": index, "reason": f"missing required field: {missing[0]}"} + ) + continue + raw_number = _field_text(record, numeric_field) + try: + number = float(raw_number) + except ValueError: + errors.append( + { + "index": index, + "reason": f"field {numeric_field!r} is not a number: {raw_number!r}", + } + ) + continue + group = _field_text(record, group_field) + bucket = groups.setdefault(group, {"count": 0, "total": 0.0}) + bucket["count"] += 1 + bucket["total"] = round(bucket["total"] + number, 2) + return { + "total": len(records), + "valid": len(records) - len(errors), + "invalid": len(errors), + "groups": groups, + "errors": errors, + } + + +def write_report(report: dict, destination: str | Path) -> None: + """Atomically write *report* as JSON, creating parent directories.""" + target = Path(destination) + target.parent.mkdir(parents=True, exist_ok=True) + temporary = target.with_name(target.name + ".tmp") + temporary.write_text( + json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8" + ) + os.replace(temporary, target) + + +def run_scenario(scenario_dir: str | Path) -> dict: + """Load scenario.json, process its data file, and write the report.""" + directory = Path(scenario_dir) + config = json.loads((directory / "scenario.json").read_text(encoding="utf-8")) + records = load_records(directory / config["data_file"]) + report = build_report( + records, + required_fields=config["required_fields"], + numeric_field=config["numeric_field"], + group_field=config["group_field"], + ) + write_report(report, directory / config["report_file"]) + return report + + +def main(argv: list[str] | None = None) -> int: + """Run one scenario directory and print a one-line summary.""" + arguments = sys.argv[1:] if argv is None else argv + if len(arguments) != 1: + print("usage: python report_tool.py <scenario-dir>", file=sys.stderr) + return 2 + try: + report = run_scenario(arguments[0]) + except (OSError, ValueError, KeyError, json.JSONDecodeError) as error: + print(f"error: {error}", file=sys.stderr) + return 1 + print( + f"total={report['total']} valid={report['valid']} invalid={report['invalid']}" + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/courses/hands-on-with-deepseek-harness/starter/report_tool.py b/courses/hands-on-with-deepseek-harness/starter/report_tool.py new file mode 100644 index 0000000..019edb8 --- /dev/null +++ b/courses/hands-on-with-deepseek-harness/starter/report_tool.py @@ -0,0 +1,88 @@ +"""Scenario report tool (starter, deliberately incomplete). + +Reads a scenario data file, aggregates valid rows, and writes a JSON report. +This starter reproduces the classic "it runs on the happy path" state of an +AI-written script: JSON inputs crash, invalid rows abort the run, group totals +carry floating-point noise, and reports cannot be written into a fresh +directory. The task contract in ../TASK.md defines the expected behavior. +""" + +from __future__ import annotations + +import csv +import json +import sys +from pathlib import Path + + +def load_records(path: str | Path) -> list[dict]: + """Load CSV or JSON records from *path* as a list of dicts.""" + source = Path(path) + with source.open("r", encoding="utf-8", newline="") as handle: + return list(csv.DictReader(handle)) + + +def build_report( + records: list[dict], + *, + required_fields: list[str], + numeric_field: str, + group_field: str, +) -> dict: + """Aggregate *records* into a summary report.""" + groups: dict[str, dict] = {} + for record in records: + for field in required_fields: + value = (record.get(field) or "").strip() + if not value: + raise KeyError(f"missing required field: {field}") + group = record[group_field].strip() + bucket = groups.setdefault(group, {"count": 0, "total": 0.0}) + bucket["count"] += 1 + bucket["total"] += float(record[numeric_field]) + return { + "total": len(records), + "valid": len(records), + "invalid": 0, + "groups": groups, + "errors": [], + } + + +def write_report(report: dict, destination: str | Path) -> None: + """Write *report* as JSON to *destination*.""" + Path(destination).write_text( + json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8" + ) + + +def run_scenario(scenario_dir: str | Path) -> dict: + """Load scenario.json, process its data file, and write the report.""" + directory = Path(scenario_dir) + config = json.loads((directory / "scenario.json").read_text(encoding="utf-8")) + records = load_records(directory / config["data_file"]) + report = build_report( + records, + required_fields=config["required_fields"], + numeric_field=config["numeric_field"], + group_field=config["group_field"], + ) + write_report(report, directory / config["report_file"]) + return report + + +def main(argv: list[str] | None = None) -> int: + """Run one scenario directory and print a one-line summary.""" + arguments = sys.argv[1:] if argv is None else argv + if len(arguments) != 1: + print("usage: python report_tool.py <scenario-dir>", file=sys.stderr) + return 2 + report = run_scenario(arguments[0]) + print( + f"total={report['total']} valid={report['valid']} invalid={report['invalid']}" + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/courses/hands-on-with-deepseek-harness/tests/test_report_tool.py b/courses/hands-on-with-deepseek-harness/tests/test_report_tool.py new file mode 100644 index 0000000..3fbc19f --- /dev/null +++ b/courses/hands-on-with-deepseek-harness/tests/test_report_tool.py @@ -0,0 +1,115 @@ +"""Contract tests for the scenario report tool. + +The suite is run twice: against ``starter/`` (specific failures expected) and +against ``solution/`` (everything must pass). See ../TASK.md for the contract. +""" + +from __future__ import annotations + +import json +import shutil +import tempfile +import unittest +from pathlib import Path + +import report_tool + +COURSE_ROOT = Path(__file__).resolve().parent.parent +SCENARIO_ROOT = COURSE_ROOT / "scenario" + + +class LoadRecordsTest(unittest.TestCase): + def test_load_csv_records_returns_list_of_dicts(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + data = Path(tmp) / "rows.csv" + data.write_text("id,region,amount\n1,emea,10.5\n2,apac,3\n", encoding="utf-8") + records = report_tool.load_records(data) + self.assertEqual(records, [{"id": "1", "region": "emea", "amount": "10.5"}, {"id": "2", "region": "apac", "amount": "3"}]) + + def test_load_json_records_returns_list_of_dicts(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + data = Path(tmp) / "rows.json" + data.write_text('[{"id": "1", "amount": "2"}]', encoding="utf-8") + records = report_tool.load_records(data) + self.assertEqual(records, [{"id": "1", "amount": "2"}]) + + def test_unsupported_suffix_raises_value_error(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + data = Path(tmp) / "rows.xlsx" + with self.assertRaises(ValueError): + report_tool.load_records(data) + + +class BuildReportTest(unittest.TestCase): + def test_invalid_records_are_isolated_with_reasons(self) -> None: + records = [ + {"id": "1", "region": "emea", "amount": "10"}, + {"id": "2", "region": "", "amount": "5"}, # missing group value + {"id": "", "region": "emea", "amount": "5"}, # missing required id + {"id": "3", "region": "apac", "amount": "n/a"} # non-numeric amount + ] + report = report_tool.build_report( + records, + required_fields=["id", "region", "amount"], + numeric_field="amount", + group_field="region", + ) + self.assertEqual(report["total"], 4) + self.assertEqual(report["valid"], 1) + self.assertEqual(report["invalid"], 3) + self.assertEqual([error["index"] for error in report["errors"]], [1, 2, 3]) + self.assertTrue(all(error["reason"] for error in report["errors"])) + self.assertEqual(report["groups"], {"emea": {"count": 1, "total": 10.0}}) + + def test_group_totals_are_rounded_to_two_decimals(self) -> None: + records = [ + {"id": "1", "region": "emea", "amount": "0.1"}, + {"id": "2", "region": "emea", "amount": "0.2"}, + ] + report = report_tool.build_report( + records, + required_fields=["id", "region", "amount"], + numeric_field="amount", + group_field="region", + ) + self.assertEqual(report["groups"]["emea"]["total"], 0.3) + + +class WriteReportTest(unittest.TestCase): + def test_write_report_creates_missing_parent_directories(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + destination = Path(tmp) / "out" / "nested" / "report.json" + report_tool.write_report({"total": 1}, destination) + self.assertTrue(destination.exists()) + self.assertEqual(json.loads(destination.read_text(encoding="utf-8"))["total"], 1) + self.assertFalse(destination.with_name(destination.name + ".tmp").exists()) + + +class ScenarioTest(unittest.TestCase): + def _copy(self, name: str) -> Path: + target = Path(tempfile.mkdtemp(prefix="scenario-")) / name + shutil.copytree(SCENARIO_ROOT / name, target) + self.addCleanup(shutil.rmtree, target.parent, ignore_errors=True) + return target + + def test_run_scenario_writes_report_file(self) -> None: + scenario = self._copy("api-tool") + report = report_tool.run_scenario(scenario) + written = json.loads((scenario / "report.json").read_text(encoding="utf-8")) + self.assertEqual(written, report) + self.assertEqual(report["total"], 7) + self.assertEqual(report["invalid"], 2) + self.assertEqual(sorted(report["groups"]), ["high", "low", "normal"]) + + def test_main_prints_summary_and_returns_zero(self) -> None: + scenario = self._copy("excel-report") + exit_code = report_tool.main([str(scenario)]) + self.assertEqual(exit_code, 0) + self.assertTrue((scenario / "report.json").exists()) + + def test_main_rejects_missing_argument(self) -> None: + self.assertEqual(report_tool.main([]), 2) + + +if __name__ == "__main__": + unittest.main() diff --git a/courses/hands-on-with-deepseek-harness/verify.py b/courses/hands-on-with-deepseek-harness/verify.py new file mode 100644 index 0000000..c414fb6 --- /dev/null +++ b/courses/hands-on-with-deepseek-harness/verify.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +"""Run the course contract against starter or solution. + +Objective completion evidence for a FlyPython shared-core course — the +teaching contract lives in COURSE.md; this file is the exercise. Exit 0 +means the selected implementation satisfies the task contract; +``--expect-failure`` reproduces the unfinished starter state and checks that +the failing tests are the intended ones. +""" + +from __future__ import annotations + +import argparse +import base64 +import hashlib +import json +import os +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent + +# Optional shared claim-receipt producer (docs/CLAIM-RECEIPT.md). The course +# folder still verifies standalone — without the tools/ sibling or without +# FLYPYTHON_CLAIM_SECRET set, verify.py behaves exactly as before. +try: + sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "tools")) + import claim_receipt +except ImportError: + claim_receipt = None + +EXPECTED_STARTER_FAILURES = ( + "test_load_json_records_returns_list_of_dicts", + "test_unsupported_suffix_raises_value_error", + "test_invalid_records_are_isolated_with_reasons", + "test_group_totals_are_rounded_to_two_decimals", + "test_write_report_creates_missing_parent_directories", + "test_run_scenario_writes_report_file", + "test_main_prints_summary_and_returns_zero", +) + + + +# ── PER-COURSE BLOCK ──────────────────────────────────────────── +# The only section allowed to differ across shared-core courses +# (checkpoint ids and gates must still match — titles may carry the +# tool's flavor). tools/verify_courses.py enforces this. +COURSE_ID = 'course-deepseek-harness' +# Documented constant: claim codes derive deterministically from +# (COURSE_ID, checkpoint_id, COURSE_SALT). They are spot-checkable +# self-reported evidence, not tamper-proof secrets — see +# docs/repo-plan-0.0.4.md FP-411. +COURSE_SALT = '2f61c37ef24e027c' + +CHECKPOINTS = [ + {"id": "l01", "gate": "attest", "title": '复现故障 / Reproduce the failure'}, + {"id": "l02", "gate": "attest", "title": '任务契约 / Task contract'}, + {"id": "l03", "gate": "starter-suite", "title": '插件化下的有界改动 / Bounded change in a plugin harness'}, + {"id": "l04", "gate": "both-suites", "title": '用轨迹视图核对验证 / Verify against the trajectory'}, + {"id": "l05", "gate": "attest", "title": '应用到自己的项目 / Apply to your project'}, +] +# ── END PER-COURSE BLOCK ──────────────────────────────────────── + +def _claim_code(checkpoint_id): + digest = hashlib.sha256( + (COURSE_ID + ":" + checkpoint_id + ":" + COURSE_SALT).encode("utf-8") + ).digest() + return base64.b32encode(digest).decode("ascii")[:8] + +def _run_suite(implementation): + environment = os.environ.copy() + environment["PYTHONPATH"] = str(ROOT / implementation) + return subprocess.run( + [sys.executable, "-m", "unittest", "discover", "-s", str(ROOT / "tests")], + env=environment, check=False, capture_output=True, text=True, + ) + +def run_progress(as_json, receipt_out=None): + import time + t0 = time.monotonic() + starter = _run_suite("starter") + starter_ms = int((time.monotonic() - t0) * 1000) + t0 = time.monotonic() + solution = _run_suite("solution") + solution_ms = int((time.monotonic() - t0) * 1000) + starter_ok = starter.returncode == 0 + solution_ok = solution.returncode == 0 + rows = [] + for checkpoint in CHECKPOINTS: + gate = checkpoint["gate"] + if gate == "attest": + status, kind = "attest", "attested" + elif gate == "starter-suite": + status = "passed" if starter_ok else "open" + kind = "objective" + else: + status = "passed" if (starter_ok and solution_ok) else "open" + kind = "objective" + code = _claim_code(checkpoint["id"]) if status in ("passed", "attest") else None + row = dict(checkpoint) + row["status"] = status + row["kind"] = kind + row["claim_code"] = code + rows.append(row) + secret = claim_receipt.receipts_enabled() if claim_receipt else None + receipts = [] + if secret: + starter_tests = claim_receipt.count_tests(starter.stderr) + solution_tests = claim_receipt.count_tests(solution.stderr) + solution_hash = claim_receipt.solution_sha256(ROOT) + for checkpoint in CHECKPOINTS: + gate = checkpoint["gate"] + if gate == "starter-suite": + passed, tests, ms = starter_ok, starter_tests, starter_ms + elif gate == "both-suites": + passed = starter_ok and solution_ok + tests, ms = starter_tests + solution_tests, starter_ms + solution_ms + else: + continue + receipts.append(claim_receipt.make_receipt( + COURSE_ID, checkpoint["id"], passed=passed, tests=tests, + duration_ms=ms, impl_dir=ROOT / "starter", + solution_hash=solution_hash, secret=secret)) + if as_json: + document = {"course": COURSE_ID, + "starter_suite_passed": starter_ok, + "solution_suite_passed": solution_ok, + "checkpoints": rows} + if secret: + document["receipts"] = receipts + print(json.dumps(document, ensure_ascii=False, indent=2)) + else: + starter_state = "passed" if starter_ok else "not passed" + solution_state = "passed" if solution_ok else "not passed" + print("Course " + COURSE_ID) + print("Suites: starter " + starter_state + " / solution " + solution_state) + for row in rows: + state = row["status"] + (" (self-attested)" if row["kind"] == "attested" else "") + code = "claim code " + row["claim_code"] if row["claim_code"] else "—" + print(" " + row["id"] + " " + row["title"] + " [" + state + "] " + code) + print("Claim codes are self-reported evidence, recorded at flypython.com; never a certificate.") + if secret: + print(f"Signed run receipts prepared for {len(receipts)} gated checkpoint(s);" + " submit each with its claim to mark them as a local-run receipt.") + if receipt_out and secret: + Path(receipt_out).write_text( + json.dumps({"receipts": receipts}, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8") + print(f"Wrote {len(receipts)} receipt(s) to {receipt_out}", file=sys.stderr) + return 0 + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("implementation", choices=("progress", "starter", "solution")) + parser.add_argument("--json", action="store_true") + parser.add_argument("--expect-failure", action="store_true") + parser.add_argument("--receipt-out", metavar="PATH", help="write signed run receipts JSON (requires FLYPYTHON_CLAIM_SECRET)") + args = parser.parse_args() + + if args.implementation == "progress": + return run_progress(args.json, args.receipt_out) + + command = [sys.executable, "-m", "unittest", "discover", "-s", str(ROOT / "tests")] + environment = os.environ.copy() + environment["PYTHONPATH"] = str(ROOT / args.implementation) + result = subprocess.run( + command, env=environment, check=False, capture_output=True, text=True + ) + + if args.expect_failure: + if result.returncode == 0: + print("Expected the starter to fail, but it passed.", file=sys.stderr) + return 1 + output = (result.stdout or "") + (result.stderr or "") + missing = [ + name for name in EXPECTED_STARTER_FAILURES if name not in output + ] + if missing: + print( + "Starter failed for unexpected reasons; missing expected failures:", + file=sys.stderr, + ) + print("\n".join(missing), file=sys.stderr) + print(output, file=sys.stderr) + return 1 + print( + "Expected starter state reproduced: JSON inputs crash, invalid rows " + "abort the run, totals are unrounded, and reports need an existing " + "directory." + ) + return 0 + if result.returncode == 0: + print(f"{args.implementation}: all tests passed") + else: + sys.stderr.write((result.stderr or "") or (result.stdout or "")) + return result.returncode + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/courses/hands-on-with-kimi-code/COURSE.md b/courses/hands-on-with-kimi-code/COURSE.md new file mode 100644 index 0000000..32db26a --- /dev/null +++ b/courses/hands-on-with-kimi-code/COURSE.md @@ -0,0 +1,112 @@ +--- +id: course-kimi-code +type: course +title: Hands-on with Kimi Code +summary: The verified Python workflow — task contract, bounded change, objective verify.py evidence — taught hands-on in Kimi Code, where built-in subagents split read-only exploration, planning, and the edits you approve. +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +badge: + id: course-kimi-code + name_en: Reproduce with Kimi Code in the loop + name_zh: Kimi Code 协同复现 + requires: All five checkpoints claimed (L01–L05) +course_id: course-kimi-code +--- + +# Hands-on with Kimi Code + +> TL;DR: download this folder, run `kimi` in it, and say **"start lesson 1"**. +> You finish with a working report tool, a reproducible pass/fail command, +> and the Kimi Code workflow: the built-in `explore`/`plan`/`coder` +> subagents that keep reading, planning, and editing in separate lanes. The +> exercise core is the same one the other agent-tool courses use — only the +> tool you drive changes. + +## What you build + +A small Python report tool that reads messy real-world data (CSV or JSON), +isolates invalid rows instead of crashing, aggregates valid rows, and writes +its report atomically. Three scenario "skins" ship with the course so you can +practice on a domain you recognize: + +| Skin | You are… | Data | +| --- | --- | --- | +| `scenario/excel-report/` | turning a weekly orders export into a region summary | `orders.csv` | +| `scenario/data-monitor/` | checking service latency numbers from your servers | `metrics.csv` | +| `scenario/api-tool/` | summarizing ticket data pulled from an internal API | `tickets.json` | + +All three run through the same code and the same tests. The skills — +contracts, error isolation, rounding, atomic writes, verification — are the +actual product; the skins just make them concrete. + +## Teaching contract (read this first, agent) + +If you are the Kimi Code agent teaching this course, follow these rules: + +- **Audience:** a project owner who has working-with-AI experience but is + stuck on reliability — the script runs, then breaks on real data. Not a + Python beginner tutorial; not a prompt-engineering course. +- **Prerequisites:** Python 3.11+ on PATH and the `kimi` CLI installed and + authenticated (a Moonshot/Kimi account or API key the learner supplies). + No packages to install — the course is standard library only. Free-tier + coverage is unverified at authoring time — see `REVIEW.md`. +- **Tool:** authored against Kimi Code as documented at + moonshotai.github.io/kimi-code (built-in `coder`/`explore`/`plan` + subagents, `kimi acp` for editor integration). A live teaching + run-through is pending — `REVIEW.md` records what has and has not been + exercised. A Kimi Code major release triggers a course re-review. +- **Lesson order:** L01 → L02 → L03 → L04 → L05, one lesson per session. + Never skip the checkpoint at the end of a lesson. +- **Teaching style:** work from the files in this folder, not from general + knowledge. Quote the exact contract line you are satisfying. Make the + smallest change that moves a test from red to green. Never add + dependencies, never edit `solution/`, and ask before touching any file + not named in the current lesson. Read `AGENTS.md` in the repository root + if one exists. +- **When to stop:** a lesson is done when its checkpoint command runs and + the learner can say, in their own words, what failed and why. If the + learner cannot, re-teach from the failing test, not from the solution. +- **How to use `verify.py`:** run it in a terminal or through the agent's + shell tool — `python verify.py starter --expect-failure` must reproduce + the listed failures; `python verify.py solution` must pass. "Done" means + the starter implementation passes the same suite after the learner's + bounded change. +- **Honesty rules:** say what you did not verify. Do not claim the code is + production-ready. Do not promise learning or career outcomes. + +## What this course does NOT cover + +Kimi Code installation and billing, custom subagent authoring, ACP client +development, or selling anything. For those, the companion repository has +guides and playbooks — link, don't improvise. + +## Badge contract + +- Badge: **Reproduce with Kimi Code in the loop Badge** (badge id `course-kimi-code`) — earned by claiming all five checkpoints. +- Challenges: L01–L05 checkpoints, 10 points each; +50 course-badge bonus when all five are claimed on flypython.com. +- Evidence: `python verify.py progress` — L03 (bounded change) and L04 (verify & review) are objectively gated by the suite; L01/L02/L05 are learner-attested. +- Submission: each passed checkpoint prints a deterministic claim code; record it on flypython.com against your account. Self-reported evidence, never a certificate. + +## Folder map + +``` +COURSE.md / COURSE_cn.md this file (EN / 中文) +lessons/L01.md … L05.md lessons (each has an _cn.md pair) +scenario/<skin>/ data files and scenario.json per skin +TASK.md / TASK_cn.md the task contract the change must satisfy +starter/report_tool.py the deliberately unfinished implementation +solution/report_tool.py the reviewed solution (do not copy in lesson 3) +tests/test_report_tool.py the contract suite (read-only) +verify.py objective pass/fail evidence +REVIEW.md maintainer run-through record +``` + +## Evidence and licensing + +The course folder is reviewed content: `REVIEW.md` records the last +run-through with dates, tool versions, and observed deviations. Code in this +folder is MIT-licensed; lesson prose is CC BY 4.0 (see repository `LICENSE`). +Report teaching drift or unclear lessons via the repository's +`course-feedback` issue form. diff --git a/courses/hands-on-with-kimi-code/COURSE_cn.md b/courses/hands-on-with-kimi-code/COURSE_cn.md new file mode 100644 index 0000000..ed08e38 --- /dev/null +++ b/courses/hands-on-with-kimi-code/COURSE_cn.md @@ -0,0 +1,103 @@ +--- +id: course-kimi-code +type: course +title: "上手 Kimi Code" +summary: "经过验证的 Python 工作流——任务契约、有界改动、客观 verify.py 证据——在 Kimi Code 里手把手教:内置子代理把只读勘察、规划和经你批准的编辑分在三条道上。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +badge: + id: course-kimi-code + name_en: Reproduce with Kimi Code in the loop + name_zh: Kimi Code 协同复现 + requires: 认领全部五个检查点(L01–L05) +course_id: course-kimi-code +--- + +# 上手 Kimi Code + +> 一句话:下载本文件夹,在里面运行 `kimi`,对它说**「开始第 1 课」**。 +> 完成时你会得到一个能跑的报表工具、一条可复现的通过/失败命令, +> 以及 Kimi Code 工作流:内置 `explore`/`plan`/`coder` 子代理把读、 +> 规划、改代码分在三条道上。练习核心与其他 Agent 工具课程完全相同 +> ——变的只是你驱动的工具。 + +## 你要做的东西 + +一个小型 Python 报表工具:读进脏乱的真实数据(CSV 或 JSON)、隔离 +无效行而不是直接崩溃、聚合有效行、原子地写出报告。课程自带三个 +「场景皮肤」,让你在自己熟悉的领域里练习: + +| 皮肤 | 你是…… | 数据 | +| --- | --- | --- | +| `scenario/excel-report/` | 把每周订单导出整理成区域汇总 | `orders.csv` | +| `scenario/data-monitor/` | 检查自家服务器的延迟数据 | `metrics.csv` | +| `scenario/api-tool/` | 汇总从内部 API 拉取的工单数据 | `tickets.json` | + +三个皮肤跑的是同一份代码、同一套测试。真正的产品是这些技能——契约、 +错误隔离、舍入、原子写、验证——皮肤只是让它们变得具体。 + +## 教学契约(先读,Agent) + +如果你是教授本课程的 Kimi Code Agent,请遵守以下规则: + +- **受众:** 有 AI 协作经验、但困在可靠性上的项目所有者——脚本能跑, + 一遇真实数据就坏。这不是 Python 入门教程,也不是提示词工程课。 +- **前置条件:** PATH 上有 Python 3.11+,装有 `kimi` CLI 并完成认证 + (学习者自备 Moonshot/Kimi 账号或 API key)。无需安装任何包—— + 课程只用标准库。免费档能否撑完整门课在编写时未经证实——见 + `REVIEW.md`。 +- **工具:** 按 moonshotai.github.io/kimi-code 上文档描述的 Kimi + Code 编写(内置 `coder`/`explore`/`plan` 子代理、用于编辑器集成 + 的 `kimi acp`)。真实授课实跑尚待完成——`REVIEW.md` 记录已验证 + 与未验证的部分。Kimi Code 主版本发布会触发课程重审。 +- **课序:** L01 → L02 → L03 → L04 → L05,一次课一个会话。 + 绝不跳过课尾的检查点。 +- **教学风格:** 从本文件夹里的文件出发,不要凭通用知识发挥。引用 + 你正在满足的那条契约原文。做让测试从红变绿的最小改动。绝不加 + 依赖、绝不改 `solution/`、动当前课程未点名的任何文件前先询问。 + 仓库根若有 `AGENTS.md`,读它。 +- **何时停下:** 一节课的检查点命令跑通、且学习者能用自己的话说清 + 哪里坏了、为什么,这节课才算完。学习者说不出就从失败测试重新教, + 不要从 solution 教。 +- **如何使用 `verify.py`:** 在终端里跑,或通过 Agent 的 shell 工具 + 跑——`python verify.py starter --expect-failure` 必须复现列出的 + 失败;`python verify.py solution` 必须通过。「完成」指学习者做完 + 有界改动后,starter 实现通过同一套测试。 +- **诚实规则:** 说清你没验证什么。不要声称代码达到生产可用。不要 + 承诺学习或职业结果。 + +## 本课程不覆盖什么 + +Kimi Code 的安装与计费、自定义子代理编写、ACP 客户端开发,以及 +任何推销内容。这些由配套仓库的指南与手册承载——给链接,不即兴 +发挥。 + +## 徽章契约 + +- 徽章:**Kimi Code 协同复现徽章**(badge id `course-kimi-code`)——认领全部五个检查点获得。 +- 挑战:L01–L05 检查点,各 10 分;在 flypython.com 上集齐五个再加 50 分课程徽章奖励。 +- 证据:`python verify.py progress`——L03(有界改动)与 L04(验证与评审)由套件客观把关;L01/L02/L05 为学习者自我报告。 +- 提交:每个通过的检查点打印一个确定性的认领码;在 flypython.com 上记入你的账号。这是自我报告的证据,从来不是证书。 + +## 文件夹地图 + +``` +COURSE.md / COURSE_cn.md 本文件(EN / 中文) +lessons/L01.md … L05.md 课文(每课都有一个 _cn.md 对) +scenario/<skin>/ 每个皮肤的数据文件与 scenario.json +TASK.md / TASK_cn.md 改动必须满足的任务契约 +starter/report_tool.py 有意未完成的实现 +solution/report_tool.py 评审过的解答(第 3 课不要抄) +tests/test_report_tool.py 契约套件(只读) +verify.py 客观的通过/失败证据 +REVIEW.md 维护者实跑记录 +``` + +## 证据与许可 + +本课程文件夹属于已评审内容:`REVIEW.md` 记录最近一次实跑——日期、 +工具版本、观察到的偏差。文件夹内代码采用 MIT 许可;课文文字采用 +CC BY 4.0(见仓库 `LICENSE`)。发现教学漂移或课程不清楚之处,请 +通过仓库的 `course-feedback` issue 表单反馈。 diff --git a/courses/hands-on-with-kimi-code/REVIEW.md b/courses/hands-on-with-kimi-code/REVIEW.md new file mode 100644 index 0000000..bd8acaa --- /dev/null +++ b/courses/hands-on-with-kimi-code/REVIEW.md @@ -0,0 +1,33 @@ +# Maintainer run-through record + +## 2026-09-13 — authored from official documentation (mechanical verification only) + +- Course authored against the Kimi Code documentation + (moonshotai.github.io/kimi-code: built-in `coder`/`explore`/`plan` + subagents — `explore` read-only, `plan` without write/shell tools, + subagents cannot spawn subagents — and `kimi acp` exposing the Agent + Client Protocol) and the shared `report-tool` core. `TASK.md`, `tests/`, + `scenario/`, `starter/`, `solution/` are byte-identical to the Claude + Code / Codex course cores — enforced by the `core-group` check in + `tools/verify_courses.py`. +- Environment for mechanical verification: macOS (arm64), Python 3.13. +- Commands and results: + - `python verify.py starter --expect-failure` — exit 0; expected failures + reproduced. + - `python verify.py solution` — exit 0; 9/9 tests pass. + - `python tools/verify_courses.py` — contract satisfied, core identical. +- **Not done:** any run inside Kimi Code. No `kimi` install or account was + used; every Kimi Code behavior described in the lessons is sourced from + official documentation, not observed. + +## Pending before this course can be called taught-in-tool + +- One full agent-taught run-through inside Kimi Code ("start lesson 1" + through the Lesson 5 checkpoint) on a current release, recording observed + deviations from `COURSE.md` here — including how the subagents were + actually invoked in the current UI. +- Whether a free-tier Kimi account lasts the whole course is unverified. + +## Deviation log + +(none yet — no live run has happened) diff --git a/courses/hands-on-with-kimi-code/TASK.md b/courses/hands-on-with-kimi-code/TASK.md new file mode 100644 index 0000000..e923183 --- /dev/null +++ b/courses/hands-on-with-kimi-code/TASK.md @@ -0,0 +1,37 @@ +# Task contract: scenario report tool (hands-on course core) + +Change only `starter/report_tool.py`. Standard library only; no new +dependencies. + +- `load_records(path) -> list[dict]`: + - `.csv` files load through `csv.DictReader`. + - `.json` files load as a list of objects; a non-list or non-object payload + raises `ValueError`. + - Any other suffix raises `ValueError` naming the unsupported file. +- `build_report(records, *, required_fields, numeric_field, group_field) -> dict`: + - Returns `{"total", "valid", "invalid", "groups", "errors"}`. + - Invalid rows never abort the run: each is collected in `errors` as + `{"index": <row position>, "reason": <short string>}`. Invalid means: + not a dict, missing or blank required field, or a non-numeric + `numeric_field` value (booleans do not count as numbers). + - Valid rows aggregate into `groups[group_value] = {"count": int, + "total": float}`; each group total is rounded to two decimals. + - `total = valid + invalid` must hold for every input. +- `write_report(report, destination)`: + - Writes JSON (UTF-8, indent 2, trailing newline) atomically: write a + sibling temp file, then `os.replace`. + - Creates missing parent directories. + - Never leaves a `.tmp` file behind on success. +- `run_scenario(scenario_dir) -> dict`: + - Reads `scenario.json` (`data_file`, `required_fields`, `numeric_field`, + `group_field`, `report_file`), processes the data file, writes the + report inside the scenario directory, and returns the report. +- `main(argv=None) -> int`: + - Exactly one argument (the scenario directory). Otherwise print usage to + stderr and return 2. + - On success print `total=... valid=... invalid=...` to stdout and return 0. + - On input failure print `error: ...` to stderr and return 1. + +Done means `python verify.py starter` exits 0 with all nine tests passing, +and `python verify.py starter --expect-failure` exits nonzero because the +starter no longer reproduces the unfinished state. diff --git a/courses/hands-on-with-kimi-code/TASK_cn.md b/courses/hands-on-with-kimi-code/TASK_cn.md new file mode 100644 index 0000000..7af2609 --- /dev/null +++ b/courses/hands-on-with-kimi-code/TASK_cn.md @@ -0,0 +1,34 @@ +# 任务契约:场景报表工具(实战课程核心) + +只修改 `starter/report_tool.py`。仅用标准库;不新增依赖。 + +- `load_records(path) -> list[dict]`: + - `.csv` 文件经 `csv.DictReader` 加载。 + - `.json` 文件按对象列表加载;不是列表或元素不是对象时抛出 + `ValueError`。 + - 其他后缀抛出 `ValueError`,并指明不支持的文件名。 +- `build_report(records, *, required_fields, numeric_field, group_field) -> dict`: + - 返回 `{"total", "valid", "invalid", "groups", "errors"}`。 + - 无效行绝不中止运行:每行以 `{"index": <行位置>, "reason": <简短 + 说明>}` 收集进 `errors`。无效指:不是字典、必填字段缺失或为空、 + `numeric_field` 不是数字(布尔值不算数字)。 + - 有效行聚合为 `groups[分组值] = {"count": int, "total": float}`;每个 + 分组总计保留两位小数。 + - 任何输入下都必须满足 `total = valid + invalid`。 +- `write_report(report, destination)`: + - 原子地写入 JSON(UTF-8、缩进 2、末尾换行):先写同名临时文件,再 + `os.replace`。 + - 自动创建缺失的父目录。 + - 成功后不留任何 `.tmp` 文件。 +- `run_scenario(scenario_dir) -> dict`: + - 读取 `scenario.json`(`data_file`、`required_fields`、 + `numeric_field`、`group_field`、`report_file`),处理数据文件,把 + 报告写进场景目录,并返回报告。 +- `main(argv=None) -> int`: + - 恰好一个参数(场景目录)。否则向 stderr 打印用法并返回 2。 + - 成功时向 stdout 打印 `total=... valid=... invalid=...` 并返回 0。 + - 输入失败时向 stderr 打印 `error: ...` 并返回 1。 + +完成的标准是 `python verify.py starter` 以 0 退出且九个测试全部通过, +同时 `python verify.py starter --expect-failure` 非零退出——因为 +starter 已不再复现未完成状态。 diff --git a/courses/hands-on-with-kimi-code/core-group b/courses/hands-on-with-kimi-code/core-group new file mode 100644 index 0000000..b56b0eb --- /dev/null +++ b/courses/hands-on-with-kimi-code/core-group @@ -0,0 +1 @@ +report-tool diff --git a/courses/hands-on-with-kimi-code/lessons/L01.md b/courses/hands-on-with-kimi-code/lessons/L01.md new file mode 100644 index 0000000..50ce567 --- /dev/null +++ b/courses/hands-on-with-kimi-code/lessons/L01.md @@ -0,0 +1,105 @@ +--- +id: course-kimi-code-l01 +type: course +title: "Lesson 1: Reproduce the failure, survey it with the read-only lane" +summary: "Run kimi in the course folder, let the read-only explore subagent map the code, and make the failure objective with verify.py." +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "What this checkpoint tests" + body: "That you can reproduce the failure before fixing it: `python verify.py starter --expect-failure` must print the named failing tests — whether you ran it or the agent's shell tool did, the command is the evidence." + - title: "Lanes, not one blob" + body: "Kimi Code ships built-in subagents: `explore` is read-only, `plan` plans without write or shell tools, `coder` can read, write, and run commands. Read-only work belongs in the read-only lane — it cannot break anything." + - title: "When you are done" + body: "You can say which nine behaviors the suite proves and why the starter cannot fake them. `python verify.py progress` then shows this checkpoint's claim code." +--- + +# Lesson 1: Reproduce the failure, survey it with the read-only lane + +## Objective + +By the end of this lesson you have `kimi` running in the course folder, +you have run the course's objective verification command, and you can +explain what "the starter fails" means in test names. No code is changed +in this lesson. + +## Why this lesson exists + +Most AI-written Python dies the same way: the demo input works, the real +input arrives, and the script crashes on row 3 — or worse, quietly +produces a wrong number. The fix does not start with a better prompt. It +starts with turning "it's broken" into a command that anyone can run and +get the same answer. Kimi Code adds a useful division of labor: built-in +subagents split the work — `explore` reads but cannot write, `plan` +plans but has no shell or write tools, and `coder` does the editing. +Surveying through the read-only lane means the exploration cannot break +anything. + +## Warm-up (2 minutes) + +Open `starter/report_tool.py` and read it top to bottom. It looks +reasonable: functions with docstrings, type hints, a CLI. This is exactly +what AI-generated code looks like — clean surface, missing behavior. Now +open `TASK.md` and keep it next to you. + +## The lesson + +Start `kimi` in the course folder. Ask for a read-only survey first — the +`explore` subagent reads files but cannot change them: + +**"Use the explore subagent to read COURSE.md, TASK.md, and +starter/report_tool.py. Do not change any file. Report which behaviors the +tests expect that the starter does not have."** + +While it reads, run the objective verification yourself in a terminal: + +```bash +python verify.py starter --expect-failure +``` + +You should see the starter reproduce four real-world failure classes, +mapped to test names: + +| Failing test | Real-world meaning | +| --- | --- | +| `test_load_json_records_returns_list_of_dicts` | the tool only handles CSV; the API scenario crashes | +| `test_unsupported_suffix_raises_value_error` | an `.xlsx` upload becomes a confusing crash instead of a clear rejection | +| `test_invalid_records_are_isolated_with_reasons` | one empty cell aborts the entire run — no partial report, no reason | +| `test_group_totals_are_rounded_to_two_decimals` | `0.1 + 0.2` prints as `0.30000000000000004` in your report | +| `test_write_report_creates_missing_parent_directories` | the tool cannot write into a fresh output directory | +| `test_run_scenario_writes_report_file`, `test_main_prints_summary_and_returns_zero` | end-to-end consequences of the above | + +Then run the reviewed implementation the same way: + +```bash +python verify.py solution +``` + +Both commands exit 0 — but they mean opposite things. The first proves the +starter is *correctly unfinished*; the second proves the target is +reachable. That pair is your completion evidence for the whole course. + +## Exercise + +Pick the scenario skin you most identify with (`excel-report`, +`data-monitor`, or `api-tool`), open its data file, and find the rows that +will be rejected. Write down, before running anything, which test each bad +row will trigger. Then check yourself against the table above — and have +`explore` confirm, not `coder` fix. + +## Checkpoint + +Run `python verify.py starter --expect-failure` and +`python verify.py solution`. You pass this lesson when you can answer, +without looking: + +1. Which four behaviors separate starter from solution? +2. Why is "expected failure" a success condition, not a contradiction? +3. Which subagent can change files — and which two cannot? + +## Expected evidence + +The transcript of both commands, and your three answers. Keep them; +Lesson 4 will ask for them again. diff --git a/courses/hands-on-with-kimi-code/lessons/L01_cn.md b/courses/hands-on-with-kimi-code/lessons/L01_cn.md new file mode 100644 index 0000000..998de6b --- /dev/null +++ b/courses/hands-on-with-kimi-code/lessons/L01_cn.md @@ -0,0 +1,96 @@ +--- +id: course-kimi-code-l01 +type: course +title: "第 1 课:复现故障,用只读道做勘察" +summary: "在课程文件夹里运行 kimi,让只读的 explore 子代理摸清代码,用 verify.py 让失败变得客观。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "本检查点考察什么" + body: "先复现失败再动手修:`python verify.py starter --expect-failure` 必须打印出点名的失败测试——你自己跑也好、Agent 的 shell 工具跑也好,命令就是证据。" + - title: "分道,不是一坨" + body: "Kimi Code 自带内置子代理:`explore` 只读、`plan` 没有写和 shell 工具、`coder` 能读写跑命令。只读的活就放只读道——它弄不坏任何东西。" + - title: "完成的标志" + body: "你能说出套件证明了哪九种行为、为什么 starter 伪造不出来。`python verify.py progress` 随即显示本检查点的认领码。" +--- + +# 第 1 课:复现故障,用只读道做勘察 + +## 目标 + +本课结束时,你在课程文件夹里跑起了 `kimi`,运行过课程的客观验证 +命令,并能解释「starter 失败」在测试名里的含义。本课不改任何代码。 + +## 为什么有这节课 + +AI 写的 Python 大多是同一种死法:演示输入能跑,真实输入一到,脚本 +在第 3 行崩掉——或者更糟,悄悄算出一个错的数。修复不从更好的 +提示词开始,而从把「坏了」变成一条任何人都能跑、且得到相同答案的 +命令开始。Kimi Code 带来一种有用的分工:内置子代理把活分开—— +`explore` 只读不写、`plan` 只做规划没有 shell 和写工具、`coder` +负责改代码。用只读道做勘察意味着探索阶段弄不坏任何东西。 + +## 热身(2 分钟) + +打开 `starter/report_tool.py`,从头读到尾。它看起来很合理:带 +docstring 的函数、类型标注、CLI。这正是 AI 生成代码的样子—— +表面干净、行为缺失。现在打开 `TASK.md`,放在手边。 + +## 本课内容 + +在课程文件夹里启动 `kimi`。先要一份只读勘察——`explore` 子代理 +能读文件但不能改: + +**“用 explore 子代理读 COURSE.md、TASK.md 和 +starter/report_tool.py。不要改任何文件。报告测试期望哪些行为、 +starter 缺了哪些。”** + +趁它读的时候,自己在终端里跑客观验证: + +```bash +python verify.py starter --expect-failure +``` + +你会看到 starter 复现出四类真实世界故障,对应到测试名: + +| 失败的测试 | 真实含义 | +| --- | --- | +| `test_load_json_records_returns_list_of_dicts` | 工具只处理 CSV;API 场景直接崩 | +| `test_unsupported_suffix_raises_value_error` | 一个 `.xlsx` 上传变成莫名其妙的崩溃,而不是清楚的拒绝 | +| `test_invalid_records_are_isolated_with_reasons` | 一个空单元格中止整个运行——没有部分报告,没有原因 | +| `test_group_totals_are_rounded_to_two_decimals` | `0.1 + 0.2` 在报告里打印成 `0.30000000000000004` | +| `test_write_report_creates_missing_parent_directories` | 工具无法写进新建的输出目录 | +| `test_run_scenario_writes_report_file`、`test_main_prints_summary_and_returns_zero` | 上述问题的端到端后果 | + +然后用同样的方式跑评审过的实现: + +```bash +python verify.py solution +``` + +两条命令都以 0 退出——但含义相反。第一条证明 starter *确实处于 +正确的未完成态*;第二条证明目标可达。这一对命令是整门课的完成 +证据。 + +## 练习 + +挑一个你最有代入感的场景皮肤(`excel-report`、`data-monitor` 或 +`api-tool`),打开它的数据文件,找出会被拒绝的行。先不看运行 +结果,写下每个坏行会触发哪个测试,再对照上面的表自查——并让 +`explore` 确认,而不是让 `coder` 修。 + +## 检查点 + +运行 `python verify.py starter --expect-failure` 和 +`python verify.py solution`。不看材料能回答下面三个问题即算通过: + +1. 哪四个行为把 starter 和 solution 区分开? +2. 为什么「预期失败」是成功条件而不是自相矛盾? +3. 哪个子代理能改文件——哪两个不能? + +## 预期证据 + +两条命令的完整输出记录,加你的三个答案。保留好——第 4 课还会 +用到。 diff --git a/courses/hands-on-with-kimi-code/lessons/L02.md b/courses/hands-on-with-kimi-code/lessons/L02.md new file mode 100644 index 0000000..13bca2a --- /dev/null +++ b/courses/hands-on-with-kimi-code/lessons/L02.md @@ -0,0 +1,86 @@ +--- +id: course-kimi-code-l02 +type: course +title: "Lesson 2: Write the task contract, plan it in the planning lane" +summary: 'Turn "make it better" into a bounded, testable contract — then let the plan subagent (no write, no shell) turn it into steps before anything edits.' +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "What this checkpoint tests" + body: "That you can read `TASK.md` as testable statements, not prose. Every sentence that starts with a function name is a contract line the suite can assert." + - title: "Plan cannot touch anything" + body: "The built-in `plan` subagent produces a plan without shell or write tools — planning is a separate, side-effect-free lane. Subagents also cannot spawn subagents, so the lanes never recurse out of sight." + - title: "The testability check" + body: "If you cannot tell whether a statement is testable, ask: could a suite assert it without reading your mind? Numbers, exit codes, files on disk — never vibes." +--- + +# Lesson 2: Write the task contract, plan it in the planning lane + +## Objective + +You can read `TASK.md` as a set of testable statements, trace each +statement to a test in `tests/test_report_tool.py`, and use the planning +lane to decompose the contract before any file is touched. + +## Why this lesson exists + +Vague requests produce vague code. "Handle bad rows better" gives an agent +permission to guess; "invalid rows are collected in `errors` with index +and reason, valid rows still produce a report" gives it a target and +gives you a way to check. Kimi Code's built-in lanes make the discipline +literal: `plan` can think about the work but holds no write or shell +tools, so planning cannot accidentally become editing. And because +subagents cannot spawn subagents, the work never recurses out of sight. + +## The lesson + +Open `TASK.md`. Notice what every line has in common: it names an +observable behavior, not an implementation. Four statements from the +contract, and the tests that pin them: + +| Contract line | Test | +| --- | --- | +| "a JSON file loads into the same record list as CSV" | `test_load_json_records_returns_list_of_dicts` | +| "invalid rows land in `errors` with index and reason; valid rows still aggregate" | `test_invalid_records_are_isolated_with_reasons` | +| "group totals are rounded to two decimals" | `test_group_totals_are_rounded_to_two_decimals` | +| "the report write is atomic and creates missing parents" | `test_write_report_creates_missing_parent_directories` | + +Now let the planning lane do its job. Tell `kimi`: + +**"Use the plan subagent to turn TASK.md into an ordered implementation +plan for starter/report_tool.py: one step per failing test group, ending +each step with `python verify.py starter`. Do not edit anything yet."** + +Read the plan it returns. It should name the same four behavior groups +you just traced to tests, in an order where each step is independently +verifiable. If a step is "improve error handling," send it back — that is +not a contract line. A plan you can check against `TASK.md` line by line +is the deliverable of this lesson. + +Also note what carries rules across sessions: if the repository root has +an `AGENTS.md`, Kimi Code reads it — durable working rules live in files, +not in a chat you will close. + +## Exercise + +Write one contract line for a script you actually own, using the same +shape: inputs, outputs, error cases, and "done means `<command>` exits +0". Then sketch the two-step plan the `plan` lane should return for it. + +## Checkpoint + +Run `python verify.py progress` — this checkpoint's claim code prints +when you can answer: + +1. Which `TASK.md` line does + `test_invalid_records_are_isolated_with_reasons` pin, in your own + words? +2. Why does it matter that `plan` has no write or shell tools? +3. Why does "subagents cannot spawn subagents" make review easier? + +## Expected evidence + +Your drafted contract line and the ordered plan you accepted (or the +version you sent back and why). diff --git a/courses/hands-on-with-kimi-code/lessons/L02_cn.md b/courses/hands-on-with-kimi-code/lessons/L02_cn.md new file mode 100644 index 0000000..81ce022 --- /dev/null +++ b/courses/hands-on-with-kimi-code/lessons/L02_cn.md @@ -0,0 +1,79 @@ +--- +id: course-kimi-code-l02 +type: course +title: "第 2 课:写任务契约,在规划道里拆步" +summary: "把「改好一点」变成有边界、可测试的契约——再让 plan 子代理(无写无 shell)先把它拆成步骤,然后才许动文件。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "本检查点考察什么" + body: "你能把 `TASK.md` 当成可测试的陈述来读,而不是散文。每个以函数名开头的句子都是套件能断言的契约行。" + - title: "plan 碰不了任何东西" + body: "内置 `plan` 子代理只产出计划,没有 shell 和写工具——规划是一条独立的、无副作用的道。子代理也不能再生子代理,所以分道不会递归到你视野之外。" + - title: "可测试性检查" + body: "判断一句话能不能测:测试套件不看你的心思能不能断言它?数字、退出码、磁盘上的文件——不看感觉。" +--- + +# 第 2 课:写任务契约,在规划道里拆步 + +## 目标 + +你能把 `TASK.md` 当成一组可测试陈述来读,把每条陈述追查到 +`tests/test_report_tool.py` 里的一个测试,并在任何文件被碰之前, +用规划道把契约拆成步骤。 + +## 为什么有这节课 + +含糊的需求产出含糊的代码。「把坏行处理好一点」是许可 Agent 去猜; +「无效行收进 `errors`、带下标和原因,有效行照常产出报告」是给它 +目标、也给你检查的方法。Kimi Code 的内置分道把纪律变成字面事实: +`plan` 能思考工作但手里没有写和 shell 工具,规划不可能失手变成 +改代码。子代理不能再生子代理,工作也不会递归出你的视野。 + +## 本课内容 + +打开 `TASK.md`。注意每行的共同点:它描述的是可观察的行为,不是 +实现。契约里的四句陈述,以及钉住它们的测试: + +| 契约行 | 测试 | +| --- | --- | +| 「JSON 文件要加载成与 CSV 相同的记录列表」 | `test_load_json_records_returns_list_of_dicts` | +| 「无效行带下标和原因进 `errors`;有效行照常聚合」 | `test_invalid_records_are_isolated_with_reasons` | +| 「分组总计保留两位小数」 | `test_group_totals_are_rounded_to_two_decimals` | +| 「报告写出是原子的,且会创建缺失的父目录」 | `test_write_report_creates_missing_parent_directories` | + +现在让规划道干活。告诉 `kimi`: + +**“用 plan 子代理把 TASK.md 拆成 starter/report_tool.py 的有序 +实现计划:每组失败测试一步,每步以 `python verify.py starter` +收尾。现在不要改任何东西。”** + +读它返回的计划。它应该点名你刚追查到测试的四个行为组,且顺序 +上每一步都可独立验证。如果某步是「改进错误处理」,打回去—— +那不是契约行。一份能逐行对着 `TASK.md` 核对的计划,就是本课的 +交付物。 + +再注意什么在跨会话携带规则:仓库根若有 `AGENTS.md`,Kimi Code +会读它——持久的工作规则活在文件里,不活在你会关掉的对话里。 + +## 练习 + +为你真正拥有的一个脚本写一条契约行,用同样的形状:输入、输出、 +错误情况、以及「完成 = `<命令>` 退出码为 0」。再草拟 `plan` 道 +应该为它返回的两步计划。 + +## 检查点 + +运行 `python verify.py progress`——能回答下面三题时,本检查点的 +认领码就会显示: + +1. 用你自己的话说,`test_invalid_records_are_isolated_with_reasons` + 钉住的是 `TASK.md` 哪一行? +2. `plan` 没有写和 shell 工具为什么重要? +3. 为什么「子代理不能再生子代理」让审查更容易? + +## 预期证据 + +你起草的契约行,和你接受的(或打回去并说明理由的)那份有序计划。 diff --git a/courses/hands-on-with-kimi-code/lessons/L03.md b/courses/hands-on-with-kimi-code/lessons/L03.md new file mode 100644 index 0000000..1b95bbf --- /dev/null +++ b/courses/hands-on-with-kimi-code/lessons/L03.md @@ -0,0 +1,82 @@ +--- +id: course-kimi-code-l03 +type: course +title: "Lesson 3: The bounded change, one contract line at a time" +summary: "Drive the starter to green through the coder lane in bounded steps — and review every edit before the next one lands." +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "What this checkpoint tests" + body: "That the starter passes all nine tests through changes you supervised — `python verify.py progress` shows this code only once the starter suite is green." + - title: "Coder is the only lane that edits" + body: "Only the `coder` subagent can write files and run commands — which means every change funnels through one lane you can watch. Keep each step to one contract line and verify between steps." + - title: "When you are done" + body: "Every edit maps to a contract line, and you can explain each one without opening solution/." +--- + +# Lesson 3: The bounded change, one contract line at a time + +## Objective + +The starter passes all nine tests through a sequence of bounded changes, +and you can explain every edit without reading the solution first. + +## Why this lesson exists + +This is the lesson people skip — and the one that builds the actual skill. +Letting an agent rewrite the file in one shot teaches you nothing and +leaves you a change you cannot honestly review. Supervising small edits, +each pinned to a contract line, is how the code stays yours. Kimi Code's +lane split helps: `explore` and `plan` cannot touch files at all — every +edit funnels through `coder`, the one lane you watch. + +## The lesson + +With the plan from Lesson 2 in hand, tell `kimi`: + +**"Execute the plan against starter/report_tool.py with the coder +subagent. One failing test group at a time: JSON loading, then validation +isolation, then rounding, then atomic writes, then the end-to-end tests. +After each group, run `python verify.py starter` and show me the edit +before continuing."** + +Hold it to the contract's boundaries: + +- Only `starter/report_tool.py` changes. If an edit touches `tests/`, + `solution/`, or `scenario/`, stop and ask why. +- No new imports outside the standard library. +- Each change should move toward one contract line. Reject drive-by + refactors ("while I was here I renamed…"). +- If the agent wants to change a test, the answer is no. Tests are the + contract; the code moves. + +Review deliberately between steps. Read the actual edit, not just the +agent's summary of it — when the two disagree, the diff is the truth. A +change you cannot explain is a change you do not keep; roll it back and +re-dispatch the step with a tighter instruction. + +Expect the failing count to drop group by group: 7 → 5 → 4 → 3 → 2 → 0. + +## Exercise + +Pick one edit `coder` made — ideally the validation-isolation change — +and explain it back: "line N does X; that satisfies contract line Y." If +you cannot, ask for a walkthrough of that edit before the next step runs. + +## Checkpoint + +Run `python verify.py progress`. This checkpoint's code appears only when +the starter suite is green. You pass the lesson when you can answer: + +1. How many steps did the bounded change take, and what made each step + bounded? +2. Which edit did you reject or re-dispatch, and why? +3. Point at the edit that implements "isolate invalid rows with reasons" + — where is it? + +## Expected evidence + +A green `python verify.py starter` run, and your edit-by-edit explanation +of the change that earned it. diff --git a/courses/hands-on-with-kimi-code/lessons/L03_cn.md b/courses/hands-on-with-kimi-code/lessons/L03_cn.md new file mode 100644 index 0000000..ad19f7d --- /dev/null +++ b/courses/hands-on-with-kimi-code/lessons/L03_cn.md @@ -0,0 +1,76 @@ +--- +id: course-kimi-code-l03 +type: course +title: "第 3 课:有界改动,一次一条契约行" +summary: "通过 coder 道的有界步骤把 starter 推到全绿——下一处编辑落地前,先审完这一处。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "本检查点考察什么" + body: "starter 通过你监督的改动让九个测试全绿——`python verify.py progress` 只在 starter 套件变绿后显示本检查点的码。" + - title: "coder 是唯一能编辑的道" + body: "只有 `coder` 子代理能写文件、跑命令——每处改动都汇进你盯着的这一条道。每步守住一条契约行,步间跑验证。" + - title: "完成的标志" + body: "每处编辑都能对上一条契约行,而且不看 solution/ 你也讲得清每一处改动。" +--- + +# 第 3 课:有界改动,一次一条契约行 + +## 目标 + +starter 通过一串有界改动让九个测试全绿,并且不看 solution,你也 +讲得清每一处编辑。 + +## 为什么有这节课 + +这节课最容易被跳过——也恰恰是长本事的那节。放任 Agent 一次重写 +整个文件,什么也学不到,只留下一份你没底气审查的改动。监督一连串 +小编辑、各自钉在一条契约行上,才能让 AI 写的代码仍然是你的。 +Kimi Code 的分道帮了忙:`explore` 和 `plan` 根本碰不了文件—— +每处编辑都汇进 `coder` 这一条你要盯的道。 + +## 本课内容 + +拿着第 2 课的计划,告诉 `kimi`: + +**“用 coder 子代理按计划改 starter/report_tool.py。一次只修一组 +失败测试:先 JSON 加载,再无效行隔离,再舍入,再原子写,最后是 +端到端测试。每修完一组跑 `python verify.py starter`,先给我看 +编辑再继续。”** + +守住契约边界: + +- 只动 `starter/report_tool.py`。编辑碰到 `tests/`、`solution/`、 + `scenario/` 就停下问为什么。 +- 标准库之外不加 import。 +- 每处改动朝一条契约行推进。顺手重构(「我顺便重命名了……」) + 一律拒绝。 +- Agent 想改测试,答案是不行。测试是契约;动的是代码。 + +步与步之间认真审。读实际的编辑,不只读 Agent 对编辑的总结—— +两者不一致时以 diff 为准。讲不清的改动就不留;回退后把这一步 +用更紧的指令重发。 + +预期失败数逐组下降:7 → 5 → 4 → 3 → 2 → 0。 + +## 练习 + +挑 `coder` 的一处编辑——最好是「无效行隔离」那段——讲回来: +「第 N 行做了 X,满足契约行 Y」。讲不清就先让它走读那处编辑, +再跑下一步。 + +## 检查点 + +运行 `python verify.py progress`。本检查点的码只在 starter 套件 +全绿时出现。不看材料能回答即算通过: + +1. 这次有界改动走了几步,是什么让每步保持有界? +2. 你拒绝或重发过哪处编辑,为什么? +3. 指出实现「无效行带原因隔离」的那处编辑——它在哪? + +## 预期证据 + +一次全绿的 `python verify.py starter` 运行,和你对这次改动的逐 +处讲解。 diff --git a/courses/hands-on-with-kimi-code/lessons/L04.md b/courses/hands-on-with-kimi-code/lessons/L04.md new file mode 100644 index 0000000..9c11043 --- /dev/null +++ b/courses/hands-on-with-kimi-code/lessons/L04.md @@ -0,0 +1,79 @@ +--- +id: course-kimi-code-l04 +type: course +title: "Lesson 4: Verify like a reviewer, not a spectator" +summary: "Green tests are the floor, not the ceiling — run both suites, walk the scenario skins, and review the accumulated diff the way you would review a colleague's." +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "What this checkpoint tests" + body: "That starter AND solution both pass — `verify.py progress` prints this code only when both suites are green. The gate is the pair, not a single run." + - title: "Review the delta, not the vibes" + body: "The working-tree diff shows the whole accumulated change. Read it as a reviewer: every hunk maps to a contract line, nothing extra moved, no test weakened." + - title: "When you are done" + body: "You have written three evidence lines — verified / not verified / known limits — and could defend the change in a code review." +--- + +# Lesson 4: Verify like a reviewer, not a spectator + +## Objective + +Both suites pass, and you hold a written record of what the green runs do +and do not prove — plus a reviewed diff you would defend to a colleague. + +## Why this lesson exists + +"All tests pass" is where most AI-assisted work stops — and where most +silent bugs start. A suite proves the contract lines it asserts; it says +nothing about the rows nobody wrote a test for. Verification is a report +you write, not a feeling you have. + +## The lesson + +Run the full pair: + +```bash +python verify.py starter # the implementation you supervised +python verify.py solution # the reviewed reference +``` + +Both green means the contract is satisfied *as written*. Now do the review +the suite cannot do for you: + +1. **Walk a skin end-to-end.** Pick a `scenario/<skin>/` folder and run + the tool on it: `python starter/report_tool.py scenario/<skin>`. Read + the produced report against the input file — recompute one group total + by hand. +2. **Review the accumulated diff.** `git diff` shows every hunk that + landed across the session. Read it top to bottom. Every hunk should + map to a contract line you named in Lesson 2. Anything that does not + is scope creep — revert or justify it. +3. **Check the edges the tests skip.** Feed the tool an empty CSV, a CSV + with only invalid rows, a missing file. The contract does not pin + these — your judgment does. Write down what it does. + +## Exercise + +Write the three-line evidence record for this change: + +- **Verified:** the commands you ran and what they proved. +- **Not verified:** the inputs and behaviors no test pins. +- **Known limits:** anything you saw that the contract never promised. + +Keep it honest — "not verified" is a list of facts, not a confession. + +## Checkpoint + +Run `python verify.py progress`. This checkpoint's code prints only when +both suites pass. You pass the lesson when you can answer: + +1. What did your by-hand recompute of a group total show? +2. Name one input the suite does not pin and what the tool does with it. +3. Which hunk, if any, would you ask a colleague to justify? + +## Expected evidence + +Both green runs, your three-line evidence record, and the diff review +notes. diff --git a/courses/hands-on-with-kimi-code/lessons/L04_cn.md b/courses/hands-on-with-kimi-code/lessons/L04_cn.md new file mode 100644 index 0000000..4457b4b --- /dev/null +++ b/courses/hands-on-with-kimi-code/lessons/L04_cn.md @@ -0,0 +1,74 @@ +--- +id: course-kimi-code-l04 +type: course +title: "第 4 课:像评审者一样验证,别当观众" +summary: "测试全绿是地板不是天花板——两个套件都跑、三个皮肤都走一遍、按评审同事的标准审这份累积 diff。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "本检查点考察什么" + body: "starter 和 solution 双双通过——`verify.py progress` 只在两个套件都绿时打印本检查点的码。门槛是一对结果,不是一次运行。" + - title: "审增量,不凭感觉" + body: "工作区 diff 展示整个会话累积的改动。按评审者的方式读:每个 hunk 对上一条契约行、没有多余挪动、没有测试被放松。" + - title: "完成的标志" + body: "你已写下三行证据——已验证 / 未验证 / 已知局限——并且能在代码评审里为这次改动辩护。" +--- + +# 第 4 课:像评审者一样验证,别当观众 + +## 目标 + +两个套件都通过,你手握一份书面记录:说清绿色运行证明了什么、没 +证明什么——外加一份你敢向同事辩护的 diff。 + +## 为什么有这节课 + +「测试全过」是大多数 AI 辅助工作停下的地方——也是大多数隐性 +bug 开始的地方。套件只证明它断言过的契约行;没人写过测试的行, +它一概不担保。验证是你写的一份报告,不是一种感觉。 + +## 本课内容 + +跑完整的一对命令: + +```bash +python verify.py starter # 你监督出来的实现 +python verify.py solution # 评审过的参考实现 +``` + +双全绿意味着契约*按字面*被满足。现在做套件替不了你的评审: + +1. **走完一个皮肤。** 挑一个 `scenario/<skin>/` 文件夹,用工具跑 + 它:`python starter/report_tool.py scenario/<skin>`。对照输入 + 文件读产出的报告——亲手重算一个分组总计。 +2. **审累积的 diff。** `git diff` 展示整个会话落地的每个 hunk。 + 从头读到尾。每个 hunk 都应能对上你在第 2 课点名的契约行。 + 对不上的就是范围蔓延——回退或让它解释。 +3. **检查测试没钉的边界。** 喂给工具一个空 CSV、一个全是无效行 + 的 CSV、一个不存在的文件。契约没钉这些——你的判断钉。记下 + 它的行为。 + +## 练习 + +为这次改动写三行证据记录: + +- **已验证:** 你跑过的命令和它们证明了什么。 +- **未验证:** 没有测试钉住的输入与行为。 +- **已知局限:** 你看到的、契约从未承诺过的东西。 + +写得诚实——「未验证」是一份事实清单,不是忏悔书。 + +## 检查点 + +运行 `python verify.py progress`。本检查点的码只在两个套件都过 +时打印。不看材料能回答即算通过: + +1. 你手工重算的分组总计结果如何? +2. 说出一个套件没钉的输入,以及工具对它的实际行为。 +3. 哪个 hunk(如果有)你会请同事来解释? + +## 预期证据 + +两次全绿运行、你的三行证据记录、diff 评审笔记。 diff --git a/courses/hands-on-with-kimi-code/lessons/L05.md b/courses/hands-on-with-kimi-code/lessons/L05.md new file mode 100644 index 0000000..7d5f4e8 --- /dev/null +++ b/courses/hands-on-with-kimi-code/lessons/L05.md @@ -0,0 +1,81 @@ +--- +id: course-kimi-code-l05 +type: course +title: "Lesson 5: Take the workflow to your own project" +summary: "The pattern generalizes: contract first, read-only survey, planning lane, bounded edits, written verification — run the same loop on a script you actually own." +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "What this checkpoint tests" + body: "Self-attested: you confirm you applied the loop to your own project. The evidence is your contract, your reviewed diffs, and your verification record — not a code we can check." + - title: "Carry the shape, not the code" + body: "What transfers is the loop: contract first, survey read-only, plan before edits, one bounded change at a time, verification written down. Where your editor supports it, `kimi acp` exposes the same agent over the Agent Client Protocol." + - title: "When you are done" + body: "Your own script has a contract, one bounded change you can explain, and a verified/not-verified/known-limits record — or a documented reason the loop did not fit." +--- + +# Lesson 5: Take the workflow to your own project + +## Objective + +You apply the whole loop — contract, read-only survey, plan, bounded +edits, written verification — to a script or project you actually own, +and record what happened. + +## Why this lesson exists + +A course folder is a safe sandbox; your own codebase is not. The point of +the last four lessons was never the report tool — it was a repeatable way +to drive an agent that keeps you in charge of what ships. This checkpoint +exists to prove the transfer happened. It is self-attested: only you know +whether the loop actually ran. + +## The lesson + +Pick a real script you own — one with a bug you have been putting off, or +a behavior you want but have not written. Then run the same five moves: + +1. **Contract.** Write the task contract first: inputs, outputs, error + cases, and "done means `<command>` exits 0". One short paragraph, every + sentence testable. +2. **Rules that persist.** Put the working rules in the project's + `AGENTS.md` — the files the agent may touch, the verification command, + the dependency policy. +3. **Lanes in order.** Survey with `explore`, decompose with `plan`, then + let `coder` execute one contract line at a time, reviewing each edit + before the next. +4. **Verify.** Run the verification command, then do the review the suite + cannot do — walk one real input end-to-end and check the output by + hand. +5. **Record.** Write the three lines: verified / not verified / known + limits. + +If you work in an editor that speaks the Agent Client Protocol, +`kimi acp` exposes the same agent over JSON-RPC — the loop does not +change, only the window it runs in does. + +If the loop fought you — the task was too small to need a contract, or too +vague to test — that is a finding too. Write down where the shape did not +fit; that judgment is the actual skill. + +## Exercise + +If your project has more than one boundary worth pinning, draft the next +contract line now — you will want it when you open the next session. + +## Checkpoint + +This checkpoint is self-attested: claim it when you can answer: + +1. What was your contract's "done means" command? +2. Which edit did you push back on? +3. What is on your "not verified" list that a stranger would assume was + covered? + +## Expected evidence + +Your contract paragraph, the rules file you wrote, and the three-line +verification record. These are yours — they are the proof the workflow +transferred. diff --git a/courses/hands-on-with-kimi-code/lessons/L05_cn.md b/courses/hands-on-with-kimi-code/lessons/L05_cn.md new file mode 100644 index 0000000..b50f436 --- /dev/null +++ b/courses/hands-on-with-kimi-code/lessons/L05_cn.md @@ -0,0 +1,70 @@ +--- +id: course-kimi-code-l05 +type: course +title: "第 5 课:把工作流带回自己的项目" +summary: "模式会泛化:契约先行、只读勘察、规划道拆步、有界编辑、书面验证——在你真正拥有的脚本上跑同一套闭环。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "本检查点考察什么" + body: "自我报告:你确认把这套闭环用到了自己的项目上。证据是你的契约、你审过的 diff、你的验证记录——不是我们查得了的码。" + - title: "带走形状,不是代码" + body: "能迁移的是闭环:契约先行、只读勘察、先规划后编辑、一次一处有界改动、验证写下来。在支持的编辑器里,`kimi acp` 通过 Agent Client Protocol 暴露同一个 Agent。" + - title: "完成的标志" + body: "你自己的脚本有了一份契约、一处你讲得清的有界改动、一份已验证/未验证/已知局限记录——或一条写清楚的「闭环不合身」的原因。" +--- + +# 第 5 课:把工作流带回自己的项目 + +## 目标 + +你把整套闭环——契约、只读勘察、规划、有界编辑、书面验证——用到 +自己真正拥有的脚本或项目上,并记录发生了什么。 + +## 为什么有这节课 + +课程文件夹是安全沙箱;你自己的代码库不是。前四课的意义从来不是 +那个报表工具——而是一种可复用的、让你始终掌控产出的 Agent 驱动 +方式。本检查点要证明迁移真的发生了。它是自我报告的:闭环跑没跑 +过只有你知道。 + +## 本课内容 + +挑一个你真正拥有的脚本——一个你拖了很久的 bug,或一个想要但 +还没写的行为。然后走同样的五步: + +1. **契约。** 先写任务契约:输入、输出、错误情况、以及「完成 = + `<命令>` 退出码为 0」。一小段,每句都可测试。 +2. **能留存的规则。** 把工作规则写进项目的 `AGENTS.md`——Agent + 可以碰哪些文件、验证命令是什么、依赖政策。 +3. **按道序走。** `explore` 勘察、`plan` 拆步,然后让 `coder` + 一次执行一条契约行,每处编辑审完再走下一处。 +4. **验证。** 跑验证命令,然后做套件做不了的评审——拿一个真实 + 输入端到端走一遍,手工核对输出。 +5. **记录。** 写三行:已验证 / 未验证 / 已知局限。 + +如果你的编辑器支持 Agent Client Protocol,`kimi acp` 用 JSON-RPC +暴露同一个 Agent——闭环不变,变的只是它运行在哪个窗口里。 + +如果闭环跟你打架——任务小到不需要契约,或模糊到不可测——那也 +是发现。写下形状不合身的地方;这个判断本身就是真本事。 + +## 练习 + +如果你的项目还有不止一处值得钉住的边界,现在就起草下一条契约行 +——开下一个会话时你会用得上。 + +## 检查点 + +本检查点自我报告:能回答下面三题时即可认领: + +1. 你的契约里「完成 =」命令是什么? +2. 你在哪处编辑上顶回去过? +3. 你的「未验证」清单上有什么是陌生人会以为已覆盖的? + +## 预期证据 + +你的契约段落、你写的规则文件、三行验证记录。它们是你自己的—— +是工作流完成迁移的证明。 diff --git a/courses/hands-on-with-kimi-code/scenario/api-tool/scenario.json b/courses/hands-on-with-kimi-code/scenario/api-tool/scenario.json new file mode 100644 index 0000000..e17e7c2 --- /dev/null +++ b/courses/hands-on-with-kimi-code/scenario/api-tool/scenario.json @@ -0,0 +1,8 @@ +{ + "skin": "api-tool", + "data_file": "tickets.json", + "required_fields": ["ticket_id", "priority", "minutes"], + "numeric_field": "minutes", + "group_field": "priority", + "report_file": "report.json" +} diff --git a/courses/hands-on-with-kimi-code/scenario/api-tool/tickets.json b/courses/hands-on-with-kimi-code/scenario/api-tool/tickets.json new file mode 100644 index 0000000..03e35b7 --- /dev/null +++ b/courses/hands-on-with-kimi-code/scenario/api-tool/tickets.json @@ -0,0 +1,9 @@ +[ + {"ticket_id": "T-101", "priority": "high", "minutes": "45"}, + {"ticket_id": "T-102", "priority": "normal", "minutes": "20"}, + {"ticket_id": "T-103", "priority": "low", "minutes": "10"}, + {"ticket_id": "T-104", "priority": "high", "minutes": "90"}, + {"ticket_id": "T-105", "priority": "", "minutes": "30"}, + {"ticket_id": "T-106", "priority": "normal", "minutes": "n/a"}, + {"ticket_id": "T-107", "priority": "low", "minutes": "25"} +] diff --git a/courses/hands-on-with-kimi-code/scenario/data-monitor/metrics.csv b/courses/hands-on-with-kimi-code/scenario/data-monitor/metrics.csv new file mode 100644 index 0000000..15a5a15 --- /dev/null +++ b/courses/hands-on-with-kimi-code/scenario/data-monitor/metrics.csv @@ -0,0 +1,8 @@ +host,service,latency_ms +web-1,api,182 +web-2,api,205 +db-1,postgres,44 +web-3,frontend,96 +db-2,,51 +web-4,api,too-slow +cache-1,redis,12 diff --git a/courses/hands-on-with-kimi-code/scenario/data-monitor/scenario.json b/courses/hands-on-with-kimi-code/scenario/data-monitor/scenario.json new file mode 100644 index 0000000..b103477 --- /dev/null +++ b/courses/hands-on-with-kimi-code/scenario/data-monitor/scenario.json @@ -0,0 +1,8 @@ +{ + "skin": "data-monitor", + "data_file": "metrics.csv", + "required_fields": ["host", "service", "latency_ms"], + "numeric_field": "latency_ms", + "group_field": "service", + "report_file": "report.json" +} diff --git a/courses/hands-on-with-kimi-code/scenario/excel-report/orders.csv b/courses/hands-on-with-kimi-code/scenario/excel-report/orders.csv new file mode 100644 index 0000000..a41fca1 --- /dev/null +++ b/courses/hands-on-with-kimi-code/scenario/excel-report/orders.csv @@ -0,0 +1,8 @@ +order_id,region,amount,item_count +1001,emea,249.99,3 +1002,apac,89.50,1 +1003,emea,,2 +1004,americas,120.00,5 +1005,,310.00,4 +1006,apac,45.25,2 +1007,emea,640.10,6 diff --git a/courses/hands-on-with-kimi-code/scenario/excel-report/scenario.json b/courses/hands-on-with-kimi-code/scenario/excel-report/scenario.json new file mode 100644 index 0000000..66b2bd9 --- /dev/null +++ b/courses/hands-on-with-kimi-code/scenario/excel-report/scenario.json @@ -0,0 +1,8 @@ +{ + "skin": "excel-report", + "data_file": "orders.csv", + "required_fields": ["order_id", "region", "amount"], + "numeric_field": "amount", + "group_field": "region", + "report_file": "report.json" +} diff --git a/courses/hands-on-with-kimi-code/solution/report_tool.py b/courses/hands-on-with-kimi-code/solution/report_tool.py new file mode 100644 index 0000000..65ba0bf --- /dev/null +++ b/courses/hands-on-with-kimi-code/solution/report_tool.py @@ -0,0 +1,134 @@ +"""Scenario report tool (reviewed solution). + +Reads a scenario data file (CSV or JSON), isolates invalid rows instead of +crashing, aggregates valid rows per group with two-decimal rounding, and +writes the report atomically so an interrupted run never leaves a half-written +file. Standard library only. +""" + +from __future__ import annotations + +import csv +import json +import os +import sys +from pathlib import Path + +SUPPORTED_SUFFIXES = {".csv", ".json"} + + +def load_records(path: str | Path) -> list[dict]: + """Load CSV or JSON records from *path* as a list of dicts.""" + source = Path(path) + suffix = source.suffix.lower() + if suffix not in SUPPORTED_SUFFIXES: + raise ValueError(f"unsupported data file type: {source.name}") + if suffix == ".csv": + with source.open("r", encoding="utf-8", newline="") as handle: + records = list(csv.DictReader(handle)) + else: + data = json.loads(source.read_text(encoding="utf-8")) + records = data if isinstance(data, list) else None + if not all(isinstance(record, dict) for record in records): + raise ValueError("data file must contain a list of objects") + return records + + +def _field_text(record: dict, field: str) -> str: + value = record.get(field) + if not isinstance(value, str): + return "" if value is None else str(value).strip() + return value.strip() + + +def build_report( + records: list[dict], + *, + required_fields: list[str], + numeric_field: str, + group_field: str, +) -> dict: + """Aggregate *records*, isolating invalid rows with recorded reasons.""" + groups: dict[str, dict] = {} + errors: list[dict] = [] + for index, record in enumerate(records): + if not isinstance(record, dict): + errors.append({"index": index, "reason": "record is not an object"}) + continue + missing = [ + field for field in required_fields if not _field_text(record, field) + ] + if missing: + errors.append( + {"index": index, "reason": f"missing required field: {missing[0]}"} + ) + continue + raw_number = _field_text(record, numeric_field) + try: + number = float(raw_number) + except ValueError: + errors.append( + { + "index": index, + "reason": f"field {numeric_field!r} is not a number: {raw_number!r}", + } + ) + continue + group = _field_text(record, group_field) + bucket = groups.setdefault(group, {"count": 0, "total": 0.0}) + bucket["count"] += 1 + bucket["total"] = round(bucket["total"] + number, 2) + return { + "total": len(records), + "valid": len(records) - len(errors), + "invalid": len(errors), + "groups": groups, + "errors": errors, + } + + +def write_report(report: dict, destination: str | Path) -> None: + """Atomically write *report* as JSON, creating parent directories.""" + target = Path(destination) + target.parent.mkdir(parents=True, exist_ok=True) + temporary = target.with_name(target.name + ".tmp") + temporary.write_text( + json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8" + ) + os.replace(temporary, target) + + +def run_scenario(scenario_dir: str | Path) -> dict: + """Load scenario.json, process its data file, and write the report.""" + directory = Path(scenario_dir) + config = json.loads((directory / "scenario.json").read_text(encoding="utf-8")) + records = load_records(directory / config["data_file"]) + report = build_report( + records, + required_fields=config["required_fields"], + numeric_field=config["numeric_field"], + group_field=config["group_field"], + ) + write_report(report, directory / config["report_file"]) + return report + + +def main(argv: list[str] | None = None) -> int: + """Run one scenario directory and print a one-line summary.""" + arguments = sys.argv[1:] if argv is None else argv + if len(arguments) != 1: + print("usage: python report_tool.py <scenario-dir>", file=sys.stderr) + return 2 + try: + report = run_scenario(arguments[0]) + except (OSError, ValueError, KeyError, json.JSONDecodeError) as error: + print(f"error: {error}", file=sys.stderr) + return 1 + print( + f"total={report['total']} valid={report['valid']} invalid={report['invalid']}" + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/courses/hands-on-with-kimi-code/starter/report_tool.py b/courses/hands-on-with-kimi-code/starter/report_tool.py new file mode 100644 index 0000000..019edb8 --- /dev/null +++ b/courses/hands-on-with-kimi-code/starter/report_tool.py @@ -0,0 +1,88 @@ +"""Scenario report tool (starter, deliberately incomplete). + +Reads a scenario data file, aggregates valid rows, and writes a JSON report. +This starter reproduces the classic "it runs on the happy path" state of an +AI-written script: JSON inputs crash, invalid rows abort the run, group totals +carry floating-point noise, and reports cannot be written into a fresh +directory. The task contract in ../TASK.md defines the expected behavior. +""" + +from __future__ import annotations + +import csv +import json +import sys +from pathlib import Path + + +def load_records(path: str | Path) -> list[dict]: + """Load CSV or JSON records from *path* as a list of dicts.""" + source = Path(path) + with source.open("r", encoding="utf-8", newline="") as handle: + return list(csv.DictReader(handle)) + + +def build_report( + records: list[dict], + *, + required_fields: list[str], + numeric_field: str, + group_field: str, +) -> dict: + """Aggregate *records* into a summary report.""" + groups: dict[str, dict] = {} + for record in records: + for field in required_fields: + value = (record.get(field) or "").strip() + if not value: + raise KeyError(f"missing required field: {field}") + group = record[group_field].strip() + bucket = groups.setdefault(group, {"count": 0, "total": 0.0}) + bucket["count"] += 1 + bucket["total"] += float(record[numeric_field]) + return { + "total": len(records), + "valid": len(records), + "invalid": 0, + "groups": groups, + "errors": [], + } + + +def write_report(report: dict, destination: str | Path) -> None: + """Write *report* as JSON to *destination*.""" + Path(destination).write_text( + json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8" + ) + + +def run_scenario(scenario_dir: str | Path) -> dict: + """Load scenario.json, process its data file, and write the report.""" + directory = Path(scenario_dir) + config = json.loads((directory / "scenario.json").read_text(encoding="utf-8")) + records = load_records(directory / config["data_file"]) + report = build_report( + records, + required_fields=config["required_fields"], + numeric_field=config["numeric_field"], + group_field=config["group_field"], + ) + write_report(report, directory / config["report_file"]) + return report + + +def main(argv: list[str] | None = None) -> int: + """Run one scenario directory and print a one-line summary.""" + arguments = sys.argv[1:] if argv is None else argv + if len(arguments) != 1: + print("usage: python report_tool.py <scenario-dir>", file=sys.stderr) + return 2 + report = run_scenario(arguments[0]) + print( + f"total={report['total']} valid={report['valid']} invalid={report['invalid']}" + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/courses/hands-on-with-kimi-code/tests/test_report_tool.py b/courses/hands-on-with-kimi-code/tests/test_report_tool.py new file mode 100644 index 0000000..3fbc19f --- /dev/null +++ b/courses/hands-on-with-kimi-code/tests/test_report_tool.py @@ -0,0 +1,115 @@ +"""Contract tests for the scenario report tool. + +The suite is run twice: against ``starter/`` (specific failures expected) and +against ``solution/`` (everything must pass). See ../TASK.md for the contract. +""" + +from __future__ import annotations + +import json +import shutil +import tempfile +import unittest +from pathlib import Path + +import report_tool + +COURSE_ROOT = Path(__file__).resolve().parent.parent +SCENARIO_ROOT = COURSE_ROOT / "scenario" + + +class LoadRecordsTest(unittest.TestCase): + def test_load_csv_records_returns_list_of_dicts(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + data = Path(tmp) / "rows.csv" + data.write_text("id,region,amount\n1,emea,10.5\n2,apac,3\n", encoding="utf-8") + records = report_tool.load_records(data) + self.assertEqual(records, [{"id": "1", "region": "emea", "amount": "10.5"}, {"id": "2", "region": "apac", "amount": "3"}]) + + def test_load_json_records_returns_list_of_dicts(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + data = Path(tmp) / "rows.json" + data.write_text('[{"id": "1", "amount": "2"}]', encoding="utf-8") + records = report_tool.load_records(data) + self.assertEqual(records, [{"id": "1", "amount": "2"}]) + + def test_unsupported_suffix_raises_value_error(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + data = Path(tmp) / "rows.xlsx" + with self.assertRaises(ValueError): + report_tool.load_records(data) + + +class BuildReportTest(unittest.TestCase): + def test_invalid_records_are_isolated_with_reasons(self) -> None: + records = [ + {"id": "1", "region": "emea", "amount": "10"}, + {"id": "2", "region": "", "amount": "5"}, # missing group value + {"id": "", "region": "emea", "amount": "5"}, # missing required id + {"id": "3", "region": "apac", "amount": "n/a"} # non-numeric amount + ] + report = report_tool.build_report( + records, + required_fields=["id", "region", "amount"], + numeric_field="amount", + group_field="region", + ) + self.assertEqual(report["total"], 4) + self.assertEqual(report["valid"], 1) + self.assertEqual(report["invalid"], 3) + self.assertEqual([error["index"] for error in report["errors"]], [1, 2, 3]) + self.assertTrue(all(error["reason"] for error in report["errors"])) + self.assertEqual(report["groups"], {"emea": {"count": 1, "total": 10.0}}) + + def test_group_totals_are_rounded_to_two_decimals(self) -> None: + records = [ + {"id": "1", "region": "emea", "amount": "0.1"}, + {"id": "2", "region": "emea", "amount": "0.2"}, + ] + report = report_tool.build_report( + records, + required_fields=["id", "region", "amount"], + numeric_field="amount", + group_field="region", + ) + self.assertEqual(report["groups"]["emea"]["total"], 0.3) + + +class WriteReportTest(unittest.TestCase): + def test_write_report_creates_missing_parent_directories(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + destination = Path(tmp) / "out" / "nested" / "report.json" + report_tool.write_report({"total": 1}, destination) + self.assertTrue(destination.exists()) + self.assertEqual(json.loads(destination.read_text(encoding="utf-8"))["total"], 1) + self.assertFalse(destination.with_name(destination.name + ".tmp").exists()) + + +class ScenarioTest(unittest.TestCase): + def _copy(self, name: str) -> Path: + target = Path(tempfile.mkdtemp(prefix="scenario-")) / name + shutil.copytree(SCENARIO_ROOT / name, target) + self.addCleanup(shutil.rmtree, target.parent, ignore_errors=True) + return target + + def test_run_scenario_writes_report_file(self) -> None: + scenario = self._copy("api-tool") + report = report_tool.run_scenario(scenario) + written = json.loads((scenario / "report.json").read_text(encoding="utf-8")) + self.assertEqual(written, report) + self.assertEqual(report["total"], 7) + self.assertEqual(report["invalid"], 2) + self.assertEqual(sorted(report["groups"]), ["high", "low", "normal"]) + + def test_main_prints_summary_and_returns_zero(self) -> None: + scenario = self._copy("excel-report") + exit_code = report_tool.main([str(scenario)]) + self.assertEqual(exit_code, 0) + self.assertTrue((scenario / "report.json").exists()) + + def test_main_rejects_missing_argument(self) -> None: + self.assertEqual(report_tool.main([]), 2) + + +if __name__ == "__main__": + unittest.main() diff --git a/courses/hands-on-with-kimi-code/verify.py b/courses/hands-on-with-kimi-code/verify.py new file mode 100644 index 0000000..614c9a2 --- /dev/null +++ b/courses/hands-on-with-kimi-code/verify.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +"""Run the course contract against starter or solution. + +Objective completion evidence for a FlyPython shared-core course — the +teaching contract lives in COURSE.md; this file is the exercise. Exit 0 +means the selected implementation satisfies the task contract; +``--expect-failure`` reproduces the unfinished starter state and checks that +the failing tests are the intended ones. +""" + +from __future__ import annotations + +import argparse +import base64 +import hashlib +import json +import os +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent + +# Optional shared claim-receipt producer (docs/CLAIM-RECEIPT.md). The course +# folder still verifies standalone — without the tools/ sibling or without +# FLYPYTHON_CLAIM_SECRET set, verify.py behaves exactly as before. +try: + sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "tools")) + import claim_receipt +except ImportError: + claim_receipt = None + +EXPECTED_STARTER_FAILURES = ( + "test_load_json_records_returns_list_of_dicts", + "test_unsupported_suffix_raises_value_error", + "test_invalid_records_are_isolated_with_reasons", + "test_group_totals_are_rounded_to_two_decimals", + "test_write_report_creates_missing_parent_directories", + "test_run_scenario_writes_report_file", + "test_main_prints_summary_and_returns_zero", +) + + + +# ── PER-COURSE BLOCK ──────────────────────────────────────────── +# The only section allowed to differ across shared-core courses +# (checkpoint ids and gates must still match — titles may carry the +# tool's flavor). tools/verify_courses.py enforces this. +COURSE_ID = 'course-kimi-code' +# Documented constant: claim codes derive deterministically from +# (COURSE_ID, checkpoint_id, COURSE_SALT). They are spot-checkable +# self-reported evidence, not tamper-proof secrets — see +# docs/repo-plan-0.0.4.md FP-411. +COURSE_SALT = '3b3e610344c18ba5' + +CHECKPOINTS = [ + {"id": "l01", "gate": "attest", "title": '复现故障 / Reproduce the failure'}, + {"id": "l02", "gate": "attest", "title": '任务契约 / Task contract'}, + {"id": "l03", "gate": "starter-suite", "title": '子 Agent 协作的有界改动 / Bounded change with sub-agents'}, + {"id": "l04", "gate": "both-suites", "title": '验证与审查 / Verify and review'}, + {"id": "l05", "gate": "attest", "title": '应用到自己的项目 / Apply to your project'}, +] +# ── END PER-COURSE BLOCK ──────────────────────────────────────── + +def _claim_code(checkpoint_id): + digest = hashlib.sha256( + (COURSE_ID + ":" + checkpoint_id + ":" + COURSE_SALT).encode("utf-8") + ).digest() + return base64.b32encode(digest).decode("ascii")[:8] + +def _run_suite(implementation): + environment = os.environ.copy() + environment["PYTHONPATH"] = str(ROOT / implementation) + return subprocess.run( + [sys.executable, "-m", "unittest", "discover", "-s", str(ROOT / "tests")], + env=environment, check=False, capture_output=True, text=True, + ) + +def run_progress(as_json, receipt_out=None): + import time + t0 = time.monotonic() + starter = _run_suite("starter") + starter_ms = int((time.monotonic() - t0) * 1000) + t0 = time.monotonic() + solution = _run_suite("solution") + solution_ms = int((time.monotonic() - t0) * 1000) + starter_ok = starter.returncode == 0 + solution_ok = solution.returncode == 0 + rows = [] + for checkpoint in CHECKPOINTS: + gate = checkpoint["gate"] + if gate == "attest": + status, kind = "attest", "attested" + elif gate == "starter-suite": + status = "passed" if starter_ok else "open" + kind = "objective" + else: + status = "passed" if (starter_ok and solution_ok) else "open" + kind = "objective" + code = _claim_code(checkpoint["id"]) if status in ("passed", "attest") else None + row = dict(checkpoint) + row["status"] = status + row["kind"] = kind + row["claim_code"] = code + rows.append(row) + secret = claim_receipt.receipts_enabled() if claim_receipt else None + receipts = [] + if secret: + starter_tests = claim_receipt.count_tests(starter.stderr) + solution_tests = claim_receipt.count_tests(solution.stderr) + solution_hash = claim_receipt.solution_sha256(ROOT) + for checkpoint in CHECKPOINTS: + gate = checkpoint["gate"] + if gate == "starter-suite": + passed, tests, ms = starter_ok, starter_tests, starter_ms + elif gate == "both-suites": + passed = starter_ok and solution_ok + tests, ms = starter_tests + solution_tests, starter_ms + solution_ms + else: + continue + receipts.append(claim_receipt.make_receipt( + COURSE_ID, checkpoint["id"], passed=passed, tests=tests, + duration_ms=ms, impl_dir=ROOT / "starter", + solution_hash=solution_hash, secret=secret)) + if as_json: + document = {"course": COURSE_ID, + "starter_suite_passed": starter_ok, + "solution_suite_passed": solution_ok, + "checkpoints": rows} + if secret: + document["receipts"] = receipts + print(json.dumps(document, ensure_ascii=False, indent=2)) + else: + starter_state = "passed" if starter_ok else "not passed" + solution_state = "passed" if solution_ok else "not passed" + print("Course " + COURSE_ID) + print("Suites: starter " + starter_state + " / solution " + solution_state) + for row in rows: + state = row["status"] + (" (self-attested)" if row["kind"] == "attested" else "") + code = "claim code " + row["claim_code"] if row["claim_code"] else "—" + print(" " + row["id"] + " " + row["title"] + " [" + state + "] " + code) + print("Claim codes are self-reported evidence, recorded at flypython.com; never a certificate.") + if secret: + print(f"Signed run receipts prepared for {len(receipts)} gated checkpoint(s);" + " submit each with its claim to mark them as a local-run receipt.") + if receipt_out and secret: + Path(receipt_out).write_text( + json.dumps({"receipts": receipts}, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8") + print(f"Wrote {len(receipts)} receipt(s) to {receipt_out}", file=sys.stderr) + return 0 + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("implementation", choices=("progress", "starter", "solution")) + parser.add_argument("--json", action="store_true") + parser.add_argument("--expect-failure", action="store_true") + parser.add_argument("--receipt-out", metavar="PATH", help="write signed run receipts JSON (requires FLYPYTHON_CLAIM_SECRET)") + args = parser.parse_args() + + if args.implementation == "progress": + return run_progress(args.json, args.receipt_out) + + command = [sys.executable, "-m", "unittest", "discover", "-s", str(ROOT / "tests")] + environment = os.environ.copy() + environment["PYTHONPATH"] = str(ROOT / args.implementation) + result = subprocess.run( + command, env=environment, check=False, capture_output=True, text=True + ) + + if args.expect_failure: + if result.returncode == 0: + print("Expected the starter to fail, but it passed.", file=sys.stderr) + return 1 + output = (result.stdout or "") + (result.stderr or "") + missing = [ + name for name in EXPECTED_STARTER_FAILURES if name not in output + ] + if missing: + print( + "Starter failed for unexpected reasons; missing expected failures:", + file=sys.stderr, + ) + print("\n".join(missing), file=sys.stderr) + print(output, file=sys.stderr) + return 1 + print( + "Expected starter state reproduced: JSON inputs crash, invalid rows " + "abort the run, totals are unrounded, and reports need an existing " + "directory." + ) + return 0 + if result.returncode == 0: + print(f"{args.implementation}: all tests passed") + else: + sys.stderr.write((result.stderr or "") or (result.stdout or "")) + return result.returncode + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/courses/hands-on-with-openai-codex-cli/COURSE.md b/courses/hands-on-with-openai-codex-cli/COURSE.md deleted file mode 100644 index de4d448..0000000 --- a/courses/hands-on-with-openai-codex-cli/COURSE.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -id: course-codex-cli -type: course -title: Hands-on with OpenAI Codex CLI -summary: The same verified Python workflow as the Claude Code course — task contract, bounded change, objective verify.py evidence — taught hands-on with the OpenAI Codex CLI instead. -lang: en-US -content_version: 2 -status: reviewed -reviewed_on: 2026-09-12 -badge: - id: course-codex-cli - name_en: Reproduce with Codex in the loop - name_zh: Codex 协同复现 - requires: All five checkpoints claimed (L01–L05) -course_id: course-codex-cli ---- - -# Hands-on with OpenAI Codex CLI - -> TL;DR: download this folder, open it where you run `codex`, and say **"start -> lesson 1"**. You finish with a working report tool, a reproducible -> pass/fail command, and a workflow that transfers to any capable coding -> agent. This course reuses the scenario skins and code core of the Claude -> Code course — only the tool you drive changes. - -## What you build - -The same scenario report tool as the flagship course: messy CSV/JSON input, -invalid rows isolated instead of crashing, rounded aggregates, atomic report -writes. Three scenario skins ship with the folder — pick the domain you know: - -| Skin | You are… | Data | -| --- | --- | --- | -| `scenario/excel-report/` | turning a weekly orders export into a region summary | `orders.csv` | -| `scenario/data-monitor/` | checking service latency numbers from your servers | `metrics.csv` | -| `scenario/api-tool/` | summarizing ticket data pulled from an internal API | `tickets.json` | - -Same code, same nine tests, different agent at the wheel. - -## Teaching contract (read this first, agent) - -- **Audience:** a project owner who has working-with-AI experience and is - stuck on reliability — not a Python beginner tutorial, not prompt tricks. -- **Prerequisites:** Python 3.11+ on PATH, the OpenAI Codex CLI installed - and signed in, and the ability to run shell commands. Standard library - only — nothing to install. -- **Tool:** taught and reviewed against Codex CLI 0.x (reviewed 2026-09-12); - a Codex CLI major release triggers a course re-review. -- **Lesson order:** L01 → L05, one per session; never skip the checkpoint. -- **Teaching style:** work from the files in this folder. Quote the contract - line you are satisfying. Smallest change per failing test group. No new - dependencies, never edit `solution/`, ask before touching files the - current lesson does not name. Respect the repository's AGENTS.md rules — - they apply to you. -- **When to stop:** a lesson is done when its checkpoint command runs and - the learner can explain what failed and why. -- **`verify.py`:** `python verify.py starter --expect-failure` must reproduce - the listed failures; `python verify.py solution` must pass. -- **Honesty rules:** say what you did not verify; no production-ready - claims; no promised outcomes. - -## What this course does NOT cover - -Installing Codex CLI, model selection, prompt engineering, multi-file -architecture, real `.xlsx` files, or deployment. The companion repository's -guides and playbooks cover those — link, don't improvise. - - -## Badge contract - -- Badge: **Reproduce with Codex in the loop Badge** (badge id `course-codex-cli`) - earned by claiming all five checkpoints. -- Challenges: L01-L05 checkpoints, 10 points each; +50 course-badge bonus when all five are claimed on flypython.com. -- Evidence: `python verify.py progress` - L03 (bounded change) and L04 (verify & review) are objectively gated by the suite; L01/L02/L05 are learner-attested. -- Submission: each passed checkpoint prints a deterministic claim code; record it on flypython.com against your account. Self-reported evidence, never a certificate. - -## Folder map - - -Same layout as the Claude Code course: `COURSE.md`/`COURSE_cn.md`, bilingual -`lessons/`, `scenario/` skins, `TASK.md`/`TASK_cn.md`, `starter/`, -`solution/`, `tests/`, `verify.py`, and `REVIEW.md`. The code contract is -identical — see `TASK.md`. - -## Evidence and licensing - -`REVIEW.md` records the run-through state with dates and tool versions. -Code is MIT-licensed; lesson prose is CC BY 4.0 (see repository `LICENSE`). -Teaching drift goes to the `course-feedback` issue form. diff --git a/courses/hands-on-with-openai-codex-cli/COURSE_cn.md b/courses/hands-on-with-openai-codex-cli/COURSE_cn.md deleted file mode 100644 index a8cb632..0000000 --- a/courses/hands-on-with-openai-codex-cli/COURSE_cn.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -id: course-codex-cli -type: course -title: OpenAI Codex CLI 实战 -summary: 与 Claude Code 课程相同的已验证 Python 工作流——任务契约、最小变更、客观 verify.py 证据——改用 OpenAI Codex CLI 完成实战。 -lang: zh-CN -content_version: 2 -status: reviewed -reviewed_on: 2026-09-12 -badge: - id: course-codex-cli - name_en: Reproduce with Codex in the loop - name_zh: Codex 协同复现 - requires: 全部五个检查点认领通过(L01–L05) -course_id: course-codex-cli ---- - -# OpenAI Codex CLI 实战 - -> 摘要:下载本文件夹,在你运行 `codex` 的地方打开,说一句 **“开始第 1 课”**。 -> 课程结束时你会得到一个可运行的报表工具、一条可复现的通过/失败命令, -> 以及可迁移到任何编码 Agent 的工作流。本课程复用 Claude Code 课程的场景 -> 皮肤与代码核心——变的只是你驱动的工具。 - -## 你将做出什么 - -与旗舰课程相同的场景报表工具:混乱的 CSV/JSON 输入、无效行隔离而非 -崩溃、聚合舍入、原子写报告。文件夹附带三个场景皮肤——选你熟悉的领域: - -| 皮肤 | 你的角色 | 数据 | -| --- | --- | --- | -| `scenario/excel-report/` | 把每周订单导出变成区域销售摘要 | `orders.csv` | -| `scenario/data-monitor/` | 检查来自服务器的服务延迟数据 | `metrics.csv` | -| `scenario/api-tool/` | 汇总从内部 API 拉取的工单数据 | `tickets.json` | - -同一份代码、同一套九个测试,换一个 Agent 掌舵。 - -## 教学契约(Agent 请先阅读本节) - -- **受众:** 有 AI 协作经验、卡在可靠性上的项目所有者——不是 Python - 入门教程,也不是提示词工程课。 -- **前置条件:** PATH 中有 Python 3.11+,已安装并登录 OpenAI Codex CLI, - 能运行 shell 命令。只用标准库——无需安装任何包。 -- **工具:** 以 Codex CLI 0.x 完成教学与审核(审核日期 2026-09-12); - Codex CLI 主版本更新会触发课程复审。 -- **课程顺序:** L01 → L05,每次一课;绝不跳过检查点。 -- **教学风格:** 从本文件夹的文件出发。引用你正在满足的契约原文。每个 - 失败测试组做最小变更。不新增依赖,不修改 `solution/`,改动当前课程 - 未授权的文件前先询问。遵守仓库 AGENTS.md 的规则——它同样约束你。 -- **何时停止:** 检查点命令运行通过、且学习者能说清什么失败了、为什么。 -- **`verify.py`:** `python verify.py starter --expect-failure` 必须复现 - 列出的失败;`python verify.py solution` 必须通过。 -- **诚实规则:** 说明哪些没验证过;不宣称生产可用;不承诺结果。 - -## 本课程不涉及的内容 - -Codex CLI 安装、模型选择、提示工程、多文件架构、真正的 `.xlsx` 文件、 -部署。这些在配套仓库的指南与手册里——链接过去,不要现场发挥。 - - -## 徽章契约 - -- 徽章:**让 Codex 进环,先复现故障徽章**(徽章 id `course-codex-cli`)——认领全部五个检查点后获得。 -- 挑战:L01–L05 检查点各 10 分;五项全部在 flypython.com 认领后另加 50 分课程徽章奖励。 -- 证据:`python verify.py progress` —— L03(边界修改)与 L04(验证与审查)由测试套件客观判定;L01/L02/L05 为学习者自报。 -- 提交:每个通过的检查点会打印确定性认领码,在 flypython.com 上记入你的账号。这是自我报告的证据,绝不是证书。 - -## 文件夹结构 - - -与 Claude Code 课程相同的布局:`COURSE.md`/`COURSE_cn.md`、双语 -`lessons/`、`scenario/` 皮肤、`TASK.md`/`TASK_cn.md`、`starter/`、 -`solution/`、`tests/`、`verify.py`、`REVIEW.md`。代码契约完全一致—— -见 `TASK.md`。 - -## 证据与许可 - -`REVIEW.md` 记录带日期与工具版本的试跑状态。代码采用 MIT 许可;课程 -文字采用 CC BY 4.0(见仓库 `LICENSE`)。教学偏差请走 `course-feedback` -issue 表单。 diff --git a/courses/hands-on-with-openai-codex-cli/lessons/L01.md b/courses/hands-on-with-openai-codex-cli/lessons/L01.md deleted file mode 100644 index fb28e0d..0000000 --- a/courses/hands-on-with-openai-codex-cli/lessons/L01.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -id: course-codex-cli-l01 -type: course -title: "Lesson 1: Reproduce the failure with Codex in the loop" -summary: "Run the verifier both ways, map each failing test to a real-world failure class, and learn how to hand the folder to Codex." -lang: en-US -content_version: 1 -status: reviewed -reviewed_on: 2026-09-12 -hints: - - title: "What this checkpoint tests" - body: "Same contract, different driver: run `python verify.py starter --expect-failure` and watch the named failures print — the checkpoint is reproducing the failure objectively, with Codex in the loop." - - title: "Read failure names as the contract" - body: "Each failing test maps to a `TASK.md` line. Hand the folder to Codex with a bounded prompt — 'reproduce the failure and list which tests fail' — and check its answer against the test names." - - title: "When you are done" - body: "You can state which nine behaviors the suite proves. `python verify.py progress` then shows this checkpoint's claim code." ---- - -# Lesson 1: Reproduce the failure with Codex in the loop - -## Objective - -Run the verifier both ways, map each failing test to a real-world failure class, and learn how to hand the folder to Codex. - -## The lesson - -Run `codex` in the course folder and say "start lesson 1". Codex reads COURSE.md first — the teaching contract governs what it may touch. Then run the objective verification yourself: - -## Exercise - -- `test_load_json_records_returns_list_of_dicts` — only CSV works; the API skin crashes -- `test_invalid_records_are_isolated_with_reasons` — one empty cell aborts the whole run -- `test_group_totals_are_rounded_to_two_decimals` — `0.1 + 0.2` prints `0.30000000000000004` -- `test_write_report_creates_missing_parent_directories` — fresh output directories fail - -## Checkpoint - -`python verify.py starter --expect-failure` reproduces the listed failures and `python verify.py solution` passes — and you can name the four missing behaviors without looking. - -## Expected evidence - -The command transcript and your answers. Lesson 4 asks for them again. diff --git a/courses/hands-on-with-openai-codex-cli/lessons/L01_cn.md b/courses/hands-on-with-openai-codex-cli/lessons/L01_cn.md deleted file mode 100644 index 6f1a662..0000000 --- a/courses/hands-on-with-openai-codex-cli/lessons/L01_cn.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -id: course-codex-cli-l01 -type: course -title: "第 1 课:让 Codex 进环,先复现故障" -summary: "双向运行验证器,把每个失败测试对应到真实故障,并学会如何把文件夹交给 Codex。" -lang: zh-CN -content_version: 1 -status: reviewed -reviewed_on: 2026-09-12 -hints: - - title: "这个检查点考什么" - body: "同一份契约、不同的驾驶员:跑 `python verify.py starter --expect-failure`,看具名失败打印出来——考的是你在 Codex 参与下把失败客观复现。" - - title: "把失败名当成契约读" - body: "每个失败测试对应 `TASK.md` 一行。用有边界的指令把文件夹交给 Codex——『复现故障并列出哪些测试失败』——再用测试名核对它的回答。" - - title: "完成标准" - body: "你能说清这九个测试各自证明了什么。`python verify.py progress` 随后显示本检查点的认领码。" ---- - -# 第 1 课:让 Codex 进环,先复现故障 - -## 目标 - -双向运行验证器,把每个失败测试对应到真实故障,并学会如何把文件夹交给 Codex。 - -## 课程内容 - -在课程文件夹里运行 `codex`,说“开始第 1 课”。Codex 会先读 COURSE.md——教学契约约束它能碰什么。然后你自己运行客观验证命令: - -## 练习 - -- `test_load_json_records_returns_list_of_dicts` —— 只认 CSV;API 皮肤直接崩溃 -- `test_invalid_records_are_isolated_with_reasons` —— 一个空单元格让整次运行中止 -- `test_group_totals_are_rounded_to_two_decimals` —— `0.1 + 0.2` 打印出 `0.30000000000000004` -- `test_write_report_creates_missing_parent_directories` —— 新建输出目录写不进去 - -## 检查点 - -`python verify.py starter --expect-failure` 复现列出的失败、`python verify.py solution` 通过——并且你不看资料也能说出四个缺失行为。 - -## 预期证据 - -命令输出记录与你的回答。第 4 课会再次用到。 diff --git a/courses/hands-on-with-openai-codex-cli/lessons/L02.md b/courses/hands-on-with-openai-codex-cli/lessons/L02.md deleted file mode 100644 index 4351925..0000000 --- a/courses/hands-on-with-openai-codex-cli/lessons/L02.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -id: course-codex-cli-l02 -type: course -title: "Lesson 2: Write the task contract before the change" -summary: 'Turn "make it better" into bounded, testable statements — the single highest-leverage skill for driving Codex.' -lang: en-US -content_version: 1 -status: reviewed -reviewed_on: 2026-09-12 -hints: - - title: "What this checkpoint tests" - body: "Reading `TASK.md` as bounded, testable statements — the highest-leverage skill for driving Codex, since it does exactly what the contract says." - - title: "Trace and write one line" - body: "Trace one contract line to its test in `tests/test_report_tool.py`, then write one contract line for your own script: inputs, outputs, error cases, 'done means <command> exits 0'." - - title: "The testability check" - body: "A statement is testable when a suite can assert it without reading your mind: numbers, exit codes, files on disk." ---- - -# Lesson 2: Write the task contract before the change - -## Objective - -Turn "make it better" into bounded, testable statements — the single highest-leverage skill for driving Codex. - -## The lesson - -Open `TASK.md`. Every line names an observable behavior, not an implementation. Trace four statements to their tests, then write one contract line for a script you own. Boundary clauses (only `starter/report_tool.py`, stdlib only, no test edits) are what keep an agent's helpful rewrite from swallowing your project. - -## Exercise - -- Ask Codex to critique your contract against TASK.md — it should find ambiguities, not add features -- Ask: "which of these could you satisfy by cheating?" Fix any line that fails that test - -## Checkpoint - -The lesson's commands run, and you can explain — in your own words — what failed and why. - -## Expected evidence - -The command transcript and your answers. Lesson 4 asks for them again. diff --git a/courses/hands-on-with-openai-codex-cli/lessons/L02_cn.md b/courses/hands-on-with-openai-codex-cli/lessons/L02_cn.md deleted file mode 100644 index 1f389e5..0000000 --- a/courses/hands-on-with-openai-codex-cli/lessons/L02_cn.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -id: course-codex-cli-l02 -type: course -title: "第 2 课:动手之前先写任务契约" -summary: "把“优化一下”变成有边界、可测试的陈述——驱动 Codex 杠杆最大的一项技能。" -lang: zh-CN -content_version: 1 -status: reviewed -reviewed_on: 2026-09-12 -hints: - - title: "这个检查点考什么" - body: "把 `TASK.md` 读成有边界、可测试的语句——这是驱动 Codex 最值钱的技能,因为它严格按契约行事。" - - title: "对一行、写一行" - body: "把一行契约对到 `tests/test_report_tool.py` 里的测试,再为你自己的脚本写一行契约:输入、输出、错误路径、『完成 = 某命令退出 0』。" - - title: "可测试性检查" - body: "一句话可测试,当且仅当套件不用读你心思就能断言它:数字、退出码、磁盘文件。" ---- - -# 第 2 课:动手之前先写任务契约 - -## 目标 - -把“优化一下”变成有边界、可测试的陈述——驱动 Codex 杠杆最大的一项技能。 - -## 课程内容 - -打开 `TASK.md`。每一行都描述可观察的行为而非实现。把四条陈述对应到测试,然后为你自己的脚本写一条契约。边界条款(只改 `starter/report_tool.py`、只用标准库、不改测试)正是防止 Agent 出于好意的重写吞掉你项目的东西。 - -## 练习 - -- 让 Codex 参照 TASK.md 点评你的契约——它应该找歧义,而不是加功能 -- 问它:“这几条里哪条你能靠作弊满足?”修掉任何没通过这一关的行 - -## 检查点 - -本课的命令运行通过,并且你能用自己的话说清什么失败了、为什么。 - -## 预期证据 - -命令输出记录与你的回答。第 4 课会再次用到。 diff --git a/courses/hands-on-with-openai-codex-cli/lessons/L03.md b/courses/hands-on-with-openai-codex-cli/lessons/L03.md deleted file mode 100644 index 8987f5b..0000000 --- a/courses/hands-on-with-openai-codex-cli/lessons/L03.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -id: course-codex-cli-l03 -type: course -title: "Lesson 3: Drive a bounded change, test by test" -summary: "Supervise Codex through seven small diffs — smallest change, no new dependencies, one failing test group at a time." -lang: en-US -content_version: 1 -status: reviewed -reviewed_on: 2026-09-12 -hints: - - title: "What this checkpoint tests" - body: "`python verify.py starter` — nine tests green. Supervise Codex through small diffs: `load_records`, `build_report`, `write_report`, `main` — one failing group per prompt." - - title: "Traps the suite asserts" - body: "Booleans are not numbers — exclude `bool` before `int`. Group totals round to two decimals; `total = valid + invalid` holds even when every row is invalid." - - title: "Atomic writes" - body: "Sibling temp file plus `os.replace`; no `.tmp` survives success. If Codex writes to the destination directly, that is the diff to push back on." ---- - -# Lesson 3: Drive a bounded change, test by test - -## Objective - -Supervise Codex through seven small diffs — smallest change, no new dependencies, one failing test group at a time. - -## The lesson - -Tell Codex: "Work through TASK.md against starter/report_tool.py, one failing test group at a time; after each group run the suite and show me the diff before continuing." Expect the failing count to fall 7 → 5 → 4 → 3 → 2 → 0. If Codex edits `tests/`, `solution/`, or `scenario/` — stop and ask why. If it wants to change a test, the answer is no. - -## Exercise - -- Run PYTHONPATH=starter python -m unittest discover -s tests -v after each group -- Write the last group yourself when only end-to-end tests remain - -## Checkpoint - -The lesson's commands run, and you can explain — in your own words — what failed and why. - -## Expected evidence - -The command transcript and your answers. Lesson 4 asks for them again. diff --git a/courses/hands-on-with-openai-codex-cli/lessons/L03_cn.md b/courses/hands-on-with-openai-codex-cli/lessons/L03_cn.md deleted file mode 100644 index 97c86fd..0000000 --- a/courses/hands-on-with-openai-codex-cli/lessons/L03_cn.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -id: course-codex-cli-l03 -type: course -title: "第 3 课:按测试驱动一次有边界的变更" -summary: "监督 Codex 完成七个小 diff——最小变更、不新增依赖、一次一组失败测试。" -lang: zh-CN -content_version: 1 -status: reviewed -reviewed_on: 2026-09-12 -hints: - - title: "这个检查点考什么" - body: "`python verify.py starter` 九测全绿。监督 Codex 做小步修改:`load_records`、`build_report`、`write_report`、`main`——每条指令只推一组失败。" - - title: "套件断言的坑" - body: "布尔值不是数字——在 `int` 判断前先排除 `bool`。分组总计两位小数;全部行非法时 `total = valid + invalid` 仍须成立。" - - title: "原子写" - body: "同级临时文件加 `os.replace`;成功不留 `.tmp`。如果 Codex 直接写目标文件,那就是你要驳回的 diff。" ---- - -# 第 3 课:按测试驱动一次有边界的变更 - -## 目标 - -监督 Codex 完成七个小 diff——最小变更、不新增依赖、一次一组失败测试。 - -## 课程内容 - -对 Codex 说:“按 TASK.md 修改 starter/report_tool.py,一次只处理一组失败测试;每组完成后跑测试套件、给我看 diff,再继续。”失败数应逐组下降 7 → 5 → 4 → 3 → 2 → 0。它若改 `tests/`、`solution/` 或 `scenario/`——停下来问为什么。它想改测试?答案是不。 - -## 练习 - -- 每组之后运行 PYTHONPATH=starter python -m unittest discover -s tests -v -- 只剩端到端测试时,最后一组自己写 - -## 检查点 - -本课的命令运行通过,并且你能用自己的话说清什么失败了、为什么。 - -## 预期证据 - -命令输出记录与你的回答。第 4 课会再次用到。 diff --git a/courses/hands-on-with-openai-codex-cli/lessons/L04.md b/courses/hands-on-with-openai-codex-cli/lessons/L04.md deleted file mode 100644 index 3715225..0000000 --- a/courses/hands-on-with-openai-codex-cli/lessons/L04.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -id: course-codex-cli-l04 -type: course -title: "Lesson 4: Verify like an engineer, review like a skeptic" -summary: "Complete the evidence loop — both verification directions, all three skins, a scope-creep diff review, and a written record of what is not proven." -lang: en-US -content_version: 1 -status: reviewed -reviewed_on: 2026-09-12 -hints: - - title: "What this checkpoint tests" - body: "Both suites plus a real review: verify both directions, run all three skins, and read the final diff like a skeptic." - - title: "Review against the contract" - body: "Diff each function against its `TASK.md` line — anything extra is scope creep, anything missing is a red test waiting to be found." - - title: "The honest record" - body: "Write down what is still unproven after green — the record that says 'verified' and the list of what it does not cover are the deliverable." ---- - -# Lesson 4: Verify like an engineer, review like a skeptic - -## Objective - -Complete the evidence loop — both verification directions, all three skins, a scope-creep diff review, and a written record of what is not proven. - -## The lesson - -Run `python verify.py starter` (9/9 after your change) and `python verify.py solution` (9/9 reference). Then run each skin end to end like a user, hand-check one group total, and review the diff: every change must be forced by a test. Write the three-line record: verified / not verified / known limits. - -## Exercise - -- Compare your record with REVIEW.md at the course root -- Try an empty data file — what happens? - -## Checkpoint - -The lesson's commands run, and you can explain — in your own words — what failed and why. - -## Expected evidence - -The command transcript and your answers. Lesson 4 asks for them again. diff --git a/courses/hands-on-with-openai-codex-cli/lessons/L04_cn.md b/courses/hands-on-with-openai-codex-cli/lessons/L04_cn.md deleted file mode 100644 index 6bfe783..0000000 --- a/courses/hands-on-with-openai-codex-cli/lessons/L04_cn.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -id: course-codex-cli-l04 -type: course -title: "第 4 课:像工程师一样验证,像怀疑者一样审查" -summary: "完成证据闭环——双向验证、三个皮肤、范围蔓延 diff 审查,以及一份写明未证明内容的记录。" -lang: zh-CN -content_version: 1 -status: reviewed -reviewed_on: 2026-09-12 -hints: - - title: "这个检查点考什么" - body: "双套件加真审查:两个方向都验证、三个皮肤都跑、像怀疑者一样读最终 diff。" - - title: "拿契约审 diff" - body: "每个函数对照它的 `TASK.md` 契约行——多做是范围蔓延,少做是还没被发现的红测。" - - title: "诚实的记录" - body: "写下绿了之后仍未证明的部分——『已验证』的记录加上未覆盖清单,才是这份交付物。" ---- - -# 第 4 课:像工程师一样验证,像怀疑者一样审查 - -## 目标 - -完成证据闭环——双向验证、三个皮肤、范围蔓延 diff 审查,以及一份写明未证明内容的记录。 - -## 课程内容 - -运行 `python verify.py starter`(你改完后应 9/9)与 `python verify.py solution`(参考实现 9/9)。再像用户一样把三个皮肤各跑一遍,手动核算一个分组总计,并审查 diff:每处改动都必须有测试逼着。写下三行记录:已验证 / 未验证 / 已知局限。 - -## 练习 - -- 把你的记录与课程根目录的 REVIEW.md 对照 -- 试试空数据文件——会发生什么? - -## 检查点 - -本课的命令运行通过,并且你能用自己的话说清什么失败了、为什么。 - -## 预期证据 - -命令输出记录与你的回答。第 4 课会再次用到。 diff --git a/courses/hands-on-with-openai-codex-cli/lessons/L05.md b/courses/hands-on-with-openai-codex-cli/lessons/L05.md deleted file mode 100644 index e96a53b..0000000 --- a/courses/hands-on-with-openai-codex-cli/lessons/L05.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -id: course-codex-cli-l05 -type: course -title: "Lesson 5: Apply this to your own project" -summary: "Port the workflow — not the code — to one script you own, with tests, a verify command, and honest limits." -lang: en-US -content_version: 1 -status: reviewed -reviewed_on: 2026-09-12 -hints: - - title: "What this checkpoint tests" - body: "Port the workflow to a script you own — contract, isolation, rounding, atomic writes, honest record — not the code itself." - - title: "Smallest useful slice" - body: "Atomic JSON write, one rounded aggregate, exit codes. Skip what your script does not need." - - title: "Say what is not proven" - body: "One line stating what your new verify command does not cover completes the checkpoint." ---- - -# Lesson 5: Apply this to your own project - -## Objective - -Port the workflow — not the code — to one script you own, with tests, a verify command, and honest limits. - -## The lesson - -Pick the script you run repeatedly and mildly fear. Write its contract (three behavior statements, one boundary clause, one acceptance command), add one failing test for the scariest line, have Codex make it pass under lesson 3's rules, and write the verification record. Remember what passing tests do not prove: tomorrow's data, production scale, upstream schema changes. Your outcome is a user-visible behavior; tests are the evidence. - -## Exercise - -- Run your acceptance command from a clean checkout to prove it does not depend on your machine - -## Checkpoint - -The lesson's commands run, and you can explain — in your own words — what failed and why. - -## Expected evidence - -The command transcript and your answers. Lesson 4 asks for them again. diff --git a/courses/hands-on-with-openai-codex-cli/lessons/L05_cn.md b/courses/hands-on-with-openai-codex-cli/lessons/L05_cn.md deleted file mode 100644 index c8767ee..0000000 --- a/courses/hands-on-with-openai-codex-cli/lessons/L05_cn.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -id: course-codex-cli-l05 -type: course -title: "第 5 课:把这套方法带回你自己的项目" -summary: "迁移工作流——而不是代码——到你拥有的一个脚本上,附测试、验证命令与诚实的局限说明。" -lang: zh-CN -content_version: 1 -status: reviewed -reviewed_on: 2026-09-12 -hints: - - title: "这个检查点考什么" - body: "把工作流搬到你自己一个脚本上——契约、隔离、舍入、原子写、诚实记录——而不是搬代码。" - - title: "最小可用切片" - body: "原子 JSON 写、一个带舍入的聚合、退出码。用不到的不要搬。" - - title: "说清没证明什么" - body: "写一句你的新验证命令不覆盖什么,就完成了这个检查点。" ---- - -# 第 5 课:把这套方法带回你自己的项目 - -## 目标 - -迁移工作流——而不是代码——到你拥有的一个脚本上,附测试、验证命令与诚实的局限说明。 - -## 课程内容 - -选那个你反复运行、且有点心里没底的脚本。写下契约(三条行为陈述、一条边界条款、一条验收命令),为最吓人的那条加一个失败测试,让 Codex 按第 3 课的规则让它通过,再写验证记录。记住测试通过不能证明什么:明天的数据、生产规模、上游改表结构。你的结果是用户可见的行为;测试是证据。 - -## 练习 - -- 在干净检出上运行你的验收命令,证明它不依赖你的机器 - -## 检查点 - -本课的命令运行通过,并且你能用自己的话说清什么失败了、为什么。 - -## 预期证据 - -命令输出记录与你的回答。第 4 课会再次用到。 diff --git a/courses/hands-on-with-openai-codex/COURSE.md b/courses/hands-on-with-openai-codex/COURSE.md new file mode 100644 index 0000000..e2732c4 --- /dev/null +++ b/courses/hands-on-with-openai-codex/COURSE.md @@ -0,0 +1,112 @@ +--- +id: course-codex-cli +type: course +title: Hands-on with the OpenAI Codex App +summary: The verified Python workflow — task contract, bounded change, objective verify.py evidence — taught hands-on in the Codex desktop app, where an agent thread runs the loop and you supervise diffs and approvals. +lang: en-US +content_version: 3 +status: reviewed +reviewed_on: 2026-09-13 +badge: + id: course-codex-cli + name_en: Reproduce with Codex in the loop + name_zh: Codex 协同复现 + requires: All five checkpoints claimed (L01–L05) +course_id: course-codex-cli +--- + +# Hands-on with the OpenAI Codex App + +> TL;DR: download this folder, open it as a project in the Codex desktop app, +> start a thread, and say **"start lesson 1"**. You finish with a working +> report tool, a reproducible pass/fail command, and the desktop-agent +> workflow: threads, approvals, AGENTS.md, and diff review. The exercise core +> is the same one the Claude Code course uses — only the tool you drive +> changes. + +## What you build + +A small Python report tool that reads messy real-world data (CSV or JSON), +isolates invalid rows instead of crashing, aggregates valid rows, and writes +its report atomically. Three scenario "skins" ship with the course so you can +practice on a domain you recognize: + +| Skin | You are… | Data | +| --- | --- | --- | +| `scenario/excel-report/` | turning a weekly orders export into a region summary | `orders.csv` | +| `scenario/data-monitor/` | checking service latency numbers from your servers | `metrics.csv` | +| `scenario/api-tool/` | summarizing ticket data pulled from an internal API | `tickets.json` | + +All three run through the same code and the same tests. The skills — contracts, +error isolation, rounding, atomic writes, verification — are the actual +product; the skins just make them concrete. + +## Teaching contract (read this first, agent) + +If you are the Codex app agent teaching this course, follow these rules: + +- **Audience:** a project owner who has working-with-AI experience but is stuck + on reliability — the script runs, then breaks on real data. Not a Python + beginner tutorial; not a prompt-engineering course. +- **Prerequisites:** Python 3.11+ on PATH and the Codex desktop app (macOS or + Windows) signed in with a ChatGPT account. No packages to install — the + course is standard library only. Whether a free ChatGPT tier lasts the whole + course is unverified at authoring time — see `REVIEW.md`. +- **Tool:** authored against the Codex desktop app as documented on + developers.openai.com/codex (thread model, approval modes, AGENTS.md + instruction chain). A live teaching run-through is pending — `REVIEW.md` + records what has and has not been exercised. A Codex major release + triggers a course re-review. +- **Lesson order:** L01 → L02 → L03 → L04 → L05, one lesson per session. + Never skip the checkpoint at the end of a lesson. +- **Teaching style:** work from the files in this folder, not from general + knowledge. Quote the exact contract line you are satisfying. Make the + smallest change that moves a test from red to green. Never add dependencies, + never edit `solution/`, and ask before touching any file not named in the + current lesson. Read `AGENTS.md` in the repository root if one exists — it + is part of your instruction chain. +- **When to stop:** a lesson is done when its checkpoint command runs and the + learner can say, in their own words, what failed and why. If the learner + cannot, re-teach from the failing test, not from the solution. +- **How to use `verify.py`:** run it in the app's terminal or via a thread + command — `python verify.py starter --expect-failure` must reproduce the + listed failures; `python verify.py solution` must pass. "Done" means the + starter implementation passes the same suite after the learner's bounded + change. +- **Honesty rules:** say what you did not verify. Do not claim the code is + production-ready. Do not promise learning or career outcomes. + +## What this course does NOT cover + +Installing and billing for the Codex app, Codex CLI flags, CI/CD cloud tasks, +MCP server setup, or selling anything. For those, the companion repository has +guides and playbooks — link, don't improvise. + +## Badge contract + +- Badge: **Reproduce with Codex in the loop Badge** (badge id `course-codex-cli`) — earned by claiming all five checkpoints. +- Challenges: L01–L05 checkpoints, 10 points each; +50 course-badge bonus when all five are claimed on flypython.com. +- Evidence: `python verify.py progress` — L03 (bounded change) and L04 (verify & review) are objectively gated by the suite; L01/L02/L05 are learner-attested. +- Submission: each passed checkpoint prints a deterministic claim code; record it on flypython.com against your account. Self-reported evidence, never a certificate. + +## Folder map + +``` +COURSE.md / COURSE_cn.md this file (EN / 中文) +lessons/L01.md … L05.md lessons (each has an _cn.md pair) +scenario/<skin>/ data files and scenario.json per skin +TASK.md / TASK_cn.md the task contract the change must satisfy +starter/report_tool.py the deliberately unfinished implementation +solution/report_tool.py the reviewed solution (do not copy in lesson 3) +tests/test_report_tool.py the contract suite (read-only) +verify.py objective pass/fail evidence +REVIEW.md maintainer run-through record +``` + +## Evidence and licensing + +The course folder is reviewed content: `REVIEW.md` records the last +run-through with dates, tool versions, and observed deviations. Code in this +folder is MIT-licensed; lesson prose is CC BY 4.0 (see repository `LICENSE`). +Report teaching drift or unclear lessons via the repository's +`course-feedback` issue form. diff --git a/courses/hands-on-with-openai-codex/COURSE_cn.md b/courses/hands-on-with-openai-codex/COURSE_cn.md new file mode 100644 index 0000000..399cb27 --- /dev/null +++ b/courses/hands-on-with-openai-codex/COURSE_cn.md @@ -0,0 +1,103 @@ +--- +id: course-codex-cli +type: course +title: "上手 OpenAI Codex 应用" +summary: "经过验证的 Python 工作流——任务契约、有界改动、客观 verify.py 证据——在 Codex 桌面应用里手把手教:由 Agent 线程跑闭环,你监督 diff 与审批。" +lang: zh-CN +content_version: 3 +status: reviewed +reviewed_on: 2026-09-13 +badge: + id: course-codex-cli + name_en: Reproduce with Codex in the loop + name_zh: Codex 协同复现 + requires: 认领全部五个检查点(L01–L05) +course_id: course-codex-cli +--- + +# 上手 OpenAI Codex 应用 + +> 一句话:下载本文件夹,在 Codex 桌面应用里作为项目打开,开一个线程, +> 对它说**「开始第 1 课」**。完成时你会得到一个能跑的报表工具、一条 +> 可复现的通过/失败命令,以及桌面 Agent 工作流:线程、审批、 +> AGENTS.md 与 diff 审查。练习核心与 Claude Code 课程完全相同—— +> 变的只是你驱动的工具。 + +## 你要做的东西 + +一个小型 Python 报表工具:读进脏乱的真实数据(CSV 或 JSON)、隔离 +无效行而不是直接崩溃、聚合有效行、原子地写出报告。课程自带三个 +「场景皮肤」,让你在自己熟悉的领域里练习: + +| 皮肤 | 你是…… | 数据 | +| --- | --- | --- | +| `scenario/excel-report/` | 把每周订单导出整理成区域汇总 | `orders.csv` | +| `scenario/data-monitor/` | 检查自家服务器的延迟数据 | `metrics.csv` | +| `scenario/api-tool/` | 汇总从内部 API 拉取的工单数据 | `tickets.json` | + +三个皮肤跑的是同一份代码、同一套测试。真正的产品是这些技能——契约、 +错误隔离、舍入、原子写、验证——皮肤只是让它们变得具体。 + +## 教学契约(先读,Agent) + +如果你是教授本课程的 Codex 应用 Agent,请遵守以下规则: + +- **受众:** 有 AI 协作经验、但困在可靠性上的项目所有者——脚本能跑, + 一遇真实数据就坏。这不是 Python 入门教程,也不是提示词工程课。 +- **前置条件:** PATH 上有 Python 3.11+,装有 Codex 桌面应用 + (macOS 或 Windows)并已登录 ChatGPT 账号。无需安装任何包—— + 课程只用标准库。免费 ChatGPT 档能否撑完整门课在编写时未经证实 + ——见 `REVIEW.md`。 +- **工具:** 按 developers.openai.com/codex 上文档描述的 Codex 桌面 + 应用编写(线程模型、审批模式、AGENTS.md 指令链)。真实授课实跑 + 尚待完成——`REVIEW.md` 记录已验证与未验证的部分。Codex 主版本 + 发布会触发课程重审。 +- **课序:** L01 → L02 → L03 → L04 → L05,一次课一个会话。 + 绝不跳过课尾的检查点。 +- **教学风格:** 从本文件夹里的文件出发,不要凭通用知识发挥。引用 + 你正在满足的那条契约原文。做让测试从红变绿的最小改动。绝不加 + 依赖、绝不改 `solution/`、动当前课程未点名的任何文件前先询问。 + 仓库根若有 `AGENTS.md`,读它——它属于你的指令链。 +- **何时停下:** 一节课的检查点命令跑通、且学习者能用自己的话说清 + 哪里坏了、为什么,这节课才算完。学习者说不出就从失败测试重新教, + 不要从 solution 教。 +- **如何使用 `verify.py`:** 在应用终端里跑,或通过线程命令—— + `python verify.py starter --expect-failure` 必须复现列出的失败; + `python verify.py solution` 必须通过。「完成」指学习者做完有界 + 改动后,starter 实现通过同一套测试。 +- **诚实规则:** 说清你没验证什么。不要声称代码达到生产可用。不要 + 承诺学习或职业结果。 + +## 本课程不覆盖什么 + +Codex 应用的安装与计费、Codex CLI 的旗标、CI/CD 云端任务、MCP +服务器配置,以及任何推销内容。这些由配套仓库的指南与手册承载—— +给链接,不即兴发挥。 + +## 徽章契约 + +- 徽章:**Codex 协同复现徽章**(badge id `course-codex-cli`)——认领全部五个检查点获得。 +- 挑战:L01–L05 检查点,各 10 分;在 flypython.com 上集齐五个再加 50 分课程徽章奖励。 +- 证据:`python verify.py progress`——L03(有界改动)与 L04(验证与评审)由套件客观把关;L01/L02/L05 为学习者自我报告。 +- 提交:每个通过的检查点打印一个确定性的认领码;在 flypython.com 上记入你的账号。这是自我报告的证据,从来不是证书。 + +## 文件夹地图 + +``` +COURSE.md / COURSE_cn.md 本文件(EN / 中文) +lessons/L01.md … L05.md 课文(每课都有一个 _cn.md 对) +scenario/<skin>/ 每个皮肤的数据文件与 scenario.json +TASK.md / TASK_cn.md 改动必须满足的任务契约 +starter/report_tool.py 有意未完成的实现 +solution/report_tool.py 评审过的解答(第 3 课不要抄) +tests/test_report_tool.py 契约套件(只读) +verify.py 客观的通过/失败证据 +REVIEW.md 维护者实跑记录 +``` + +## 证据与许可 + +本课程文件夹属于已评审内容:`REVIEW.md` 记录最近一次实跑——日期、 +工具版本、观察到的偏差。文件夹内代码采用 MIT 许可;课文文字采用 +CC BY 4.0(见仓库 `LICENSE`)。发现教学漂移或课程不清楚之处,请 +通过仓库的 `course-feedback` issue 表单反馈。 diff --git a/courses/hands-on-with-openai-codex-cli/REVIEW.md b/courses/hands-on-with-openai-codex/REVIEW.md similarity index 54% rename from courses/hands-on-with-openai-codex-cli/REVIEW.md rename to courses/hands-on-with-openai-codex/REVIEW.md index 91340cc..048959a 100644 --- a/courses/hands-on-with-openai-codex-cli/REVIEW.md +++ b/courses/hands-on-with-openai-codex/REVIEW.md @@ -1,5 +1,22 @@ # Maintainer run-through record +## 2026-09-13 — content rewrite: CLI course → Codex desktop app course + +- Folder renamed `hands-on-with-openai-codex-cli` → + `hands-on-with-openai-codex`; `course_id` (`course-codex-cli`) and claim + salt preserved so existing claims and codes remain valid. +- `COURSE.md`/`COURSE_cn.md` and all ten lesson files rewritten against + the Codex app as documented at developers.openai.com/codex (thread + model, approval modes, AGENTS.md instruction chain, diff review). + CLI-specific teaching was removed; `TASK.md`, `tests/`, `scenario/`, + `starter/`, `solution/` are byte-identical to the Claude Code course + core (shared-core group `report-tool`, enforced by + `tools/verify_courses.py`). +- **Not done in this pass:** a live teaching run-through inside the Codex + desktop app. No Codex account/app session was used; the content is + authored from official documentation and the previously verified task + core. Teaching quality in the real app remains pending below. + ## 2026-09-12 — contract verification (mechanical) - Environment: macOS (arm64), Python 3.13; repository clean checkout on @@ -11,13 +28,14 @@ failure names reproduced. - `python verify.py solution` — exit 0; 9/9 tests pass. - All three skins exercised end to end by the shared suite. -- Not verified in this pass: teaching quality with a live Codex CLI session. -## Pending before the public course drop +## Pending before this rewrite can be called taught-in-tool -- One full agent-taught run-through with Codex CLI 0.x ("start lesson 1" - through the Lesson 5 checkpoint), recording observed deviations from - COURSE.md here; the recording doubles as demo-video source material. +- One full agent-taught run-through inside the Codex desktop app + ("start lesson 1" through the Lesson 5 checkpoint), recording observed + deviations from `COURSE.md` here; the recording doubles as demo-video + source material. +- Whether a free ChatGPT tier lasts the whole course is unverified. ## Deviation log diff --git a/courses/hands-on-with-openai-codex/TASK.md b/courses/hands-on-with-openai-codex/TASK.md new file mode 100644 index 0000000..e923183 --- /dev/null +++ b/courses/hands-on-with-openai-codex/TASK.md @@ -0,0 +1,37 @@ +# Task contract: scenario report tool (hands-on course core) + +Change only `starter/report_tool.py`. Standard library only; no new +dependencies. + +- `load_records(path) -> list[dict]`: + - `.csv` files load through `csv.DictReader`. + - `.json` files load as a list of objects; a non-list or non-object payload + raises `ValueError`. + - Any other suffix raises `ValueError` naming the unsupported file. +- `build_report(records, *, required_fields, numeric_field, group_field) -> dict`: + - Returns `{"total", "valid", "invalid", "groups", "errors"}`. + - Invalid rows never abort the run: each is collected in `errors` as + `{"index": <row position>, "reason": <short string>}`. Invalid means: + not a dict, missing or blank required field, or a non-numeric + `numeric_field` value (booleans do not count as numbers). + - Valid rows aggregate into `groups[group_value] = {"count": int, + "total": float}`; each group total is rounded to two decimals. + - `total = valid + invalid` must hold for every input. +- `write_report(report, destination)`: + - Writes JSON (UTF-8, indent 2, trailing newline) atomically: write a + sibling temp file, then `os.replace`. + - Creates missing parent directories. + - Never leaves a `.tmp` file behind on success. +- `run_scenario(scenario_dir) -> dict`: + - Reads `scenario.json` (`data_file`, `required_fields`, `numeric_field`, + `group_field`, `report_file`), processes the data file, writes the + report inside the scenario directory, and returns the report. +- `main(argv=None) -> int`: + - Exactly one argument (the scenario directory). Otherwise print usage to + stderr and return 2. + - On success print `total=... valid=... invalid=...` to stdout and return 0. + - On input failure print `error: ...` to stderr and return 1. + +Done means `python verify.py starter` exits 0 with all nine tests passing, +and `python verify.py starter --expect-failure` exits nonzero because the +starter no longer reproduces the unfinished state. diff --git a/courses/hands-on-with-openai-codex/TASK_cn.md b/courses/hands-on-with-openai-codex/TASK_cn.md new file mode 100644 index 0000000..7af2609 --- /dev/null +++ b/courses/hands-on-with-openai-codex/TASK_cn.md @@ -0,0 +1,34 @@ +# 任务契约:场景报表工具(实战课程核心) + +只修改 `starter/report_tool.py`。仅用标准库;不新增依赖。 + +- `load_records(path) -> list[dict]`: + - `.csv` 文件经 `csv.DictReader` 加载。 + - `.json` 文件按对象列表加载;不是列表或元素不是对象时抛出 + `ValueError`。 + - 其他后缀抛出 `ValueError`,并指明不支持的文件名。 +- `build_report(records, *, required_fields, numeric_field, group_field) -> dict`: + - 返回 `{"total", "valid", "invalid", "groups", "errors"}`。 + - 无效行绝不中止运行:每行以 `{"index": <行位置>, "reason": <简短 + 说明>}` 收集进 `errors`。无效指:不是字典、必填字段缺失或为空、 + `numeric_field` 不是数字(布尔值不算数字)。 + - 有效行聚合为 `groups[分组值] = {"count": int, "total": float}`;每个 + 分组总计保留两位小数。 + - 任何输入下都必须满足 `total = valid + invalid`。 +- `write_report(report, destination)`: + - 原子地写入 JSON(UTF-8、缩进 2、末尾换行):先写同名临时文件,再 + `os.replace`。 + - 自动创建缺失的父目录。 + - 成功后不留任何 `.tmp` 文件。 +- `run_scenario(scenario_dir) -> dict`: + - 读取 `scenario.json`(`data_file`、`required_fields`、 + `numeric_field`、`group_field`、`report_file`),处理数据文件,把 + 报告写进场景目录,并返回报告。 +- `main(argv=None) -> int`: + - 恰好一个参数(场景目录)。否则向 stderr 打印用法并返回 2。 + - 成功时向 stdout 打印 `total=... valid=... invalid=...` 并返回 0。 + - 输入失败时向 stderr 打印 `error: ...` 并返回 1。 + +完成的标准是 `python verify.py starter` 以 0 退出且九个测试全部通过, +同时 `python verify.py starter --expect-failure` 非零退出——因为 +starter 已不再复现未完成状态。 diff --git a/courses/hands-on-with-openai-codex/core-group b/courses/hands-on-with-openai-codex/core-group new file mode 100644 index 0000000..b56b0eb --- /dev/null +++ b/courses/hands-on-with-openai-codex/core-group @@ -0,0 +1 @@ +report-tool diff --git a/courses/hands-on-with-openai-codex/lessons/L01.md b/courses/hands-on-with-openai-codex/lessons/L01.md new file mode 100644 index 0000000..7253033 --- /dev/null +++ b/courses/hands-on-with-openai-codex/lessons/L01.md @@ -0,0 +1,105 @@ +--- +id: course-codex-cli-l01 +type: course +title: "Lesson 1: Reproduce the failure in a Codex thread" +summary: "Open the folder as a Codex project, start your first thread, and make the failure objective with verify.py — inside the app, not beside it." +lang: en-US +content_version: 2 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "What this checkpoint tests" + body: "That you can reproduce the failure before fixing it: `python verify.py starter --expect-failure` must print the named failing tests. In the Codex app the thread is where the work happens — the command is still the evidence." + - title: "Threads are tasks, not chats" + body: "A Codex thread is one bounded job: your prompt plus the agent's tool calls and diffs. Keep this thread to 'reproduce and explain' — the fix belongs to lesson 3, in its own thread." + - title: "When you are done" + body: "You can say out loud which nine behaviors the suite proves and why the starter cannot fake them. `python verify.py progress` then shows this checkpoint's claim code." +--- + +# Lesson 1: Reproduce the failure in a Codex thread + +## Objective + +By the end of this lesson you can open the course folder in the Codex desktop +app, run the course's objective verification command, and explain what "the +starter fails" means in test names. No code is changed in this lesson. + +## Why this lesson exists + +Most AI-written Python dies the same way: the demo input works, the real input +arrives, and the script crashes on row 3 — or worse, quietly produces a wrong +number. The fix does not start with a better prompt. It starts with turning +"it's broken" into a command that anyone can run and get the same answer. The +Codex app keeps that command and the agent's work in one place: a thread is a +task with its own context, diffs, and approvals — not a scrolling chat. + +## Warm-up (2 minutes) + +Open `starter/report_tool.py` in the app's file panel and read it top to +bottom. It looks reasonable: functions with docstrings, type hints, a CLI. +This is exactly what AI-generated code looks like — clean surface, missing +behavior. Now open `TASK.md` and keep it next to you. + +## The lesson + +Open the course folder as a project in the Codex app, then start a new +thread. A thread is a single task: it holds your prompt, the agent's tool +calls, and the resulting diffs. Tell it: + +**"Read COURSE.md — it is the teaching contract for this folder. Then read +TASK.md and starter/report_tool.py. Do not change any file. Tell me which +behaviors the tests expect that the starter does not have."** + +While it reads, run the objective verification yourself — in the app's +terminal, or let the agent run it in the thread (command execution asks for +your approval; read the command before approving): + +```bash +python verify.py starter --expect-failure +``` + +You should see the starter reproduce four real-world failure classes, mapped +to test names: + +| Failing test | Real-world meaning | +| --- | --- | +| `test_load_json_records_returns_list_of_dicts` | the tool only handles CSV; the API scenario crashes | +| `test_unsupported_suffix_raises_value_error` | an `.xlsx` upload becomes a confusing crash instead of a clear rejection | +| `test_invalid_records_are_isolated_with_reasons` | one empty cell aborts the entire run — no partial report, no reason | +| `test_group_totals_are_rounded_to_two_decimals` | `0.1 + 0.2` prints as `0.30000000000000004` in your report | +| `test_write_report_creates_missing_parent_directories` | the tool cannot write into a fresh output directory | +| `test_run_scenario_writes_report_file`, `test_main_prints_summary_and_returns_zero` | end-to-end consequences of the above | + +Then run the reviewed implementation the same way: + +```bash +python verify.py solution +``` + +Both commands exit 0 — but they mean opposite things. The first proves the +starter is *correctly unfinished*; the second proves the target is reachable. +That pair is your completion evidence for the whole course. + +## Exercise + +Pick the scenario skin you most identify with (`excel-report`, +`data-monitor`, or `api-tool`), open its data file, and find the rows that +will be rejected. Write down, before running anything, which test each bad row +will trigger. Then check yourself against the table above — and ask the thread +to confirm, not to fix. + +## Checkpoint + +Run `python verify.py starter --expect-failure` and +`python verify.py solution`. You pass this lesson when you can answer, without +looking: + +1. Which four behaviors separate starter from solution? +2. Why is "expected failure" a success condition, not a contradiction? +3. What does the row you found in the exercise do to the starter — crash, + isolate, or silently pass? + +## Expected evidence + +The transcript of both commands, and your three answers. Keep them; Lesson 4 +will ask for them again. diff --git a/courses/hands-on-with-openai-codex/lessons/L01_cn.md b/courses/hands-on-with-openai-codex/lessons/L01_cn.md new file mode 100644 index 0000000..908b1b4 --- /dev/null +++ b/courses/hands-on-with-openai-codex/lessons/L01_cn.md @@ -0,0 +1,94 @@ +--- +id: course-codex-cli-l01 +type: course +title: "第 1 课:在 Codex 线程里复现故障" +summary: "把文件夹作为 Codex 项目打开,开启第一个线程,用 verify.py 让失败变得客观——在应用内完成,而不是在应用旁边。" +lang: zh-CN +content_version: 2 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "本检查点考察什么" + body: "先复现失败再动手修:`python verify.py starter --expect-failure` 必须打印出点名的失败测试。在 Codex 应用里,线程是干活的地方——但命令仍然是证据。" + - title: "线程是任务,不是聊天" + body: "Codex 线程是一个有边界的任务:你的提示加 Agent 的工具调用和 diff。这个线程只做「复现并解释」——修复属于第 3 课,开新线程做。" + - title: "完成的标志" + body: "你能说出套件证明了哪九种行为、为什么 starter 伪造不出来。`python verify.py progress` 随即显示本检查点的认领码。" +--- + +# 第 1 课:在 Codex 线程里复现故障 + +## 目标 + +本课结束时,你能在 Codex 桌面应用里打开课程文件夹、运行课程的客观 +验证命令,并解释「starter 失败」在测试名里的含义。本课不改任何代码。 + +## 为什么有这节课 + +AI 写的 Python 大多是同一种死法:演示输入能跑,真实输入一到,脚本在 +第 3 行崩掉——或者更糟,悄悄算出一个错的数。修复不从更好的提示词 +开始,而从把「坏了」变成一条任何人都能跑、且得到相同答案的命令开始。 +Codex 应用把这条命令和 Agent 的工作放在一处:线程是带着自己上下文、 +diff 和审批的任务,不是一条滚动的聊天记录。 + +## 热身(2 分钟) + +在应用的文件面板里打开 `starter/report_tool.py`,从头读到尾。它看起来 +很合理:带 docstring 的函数、类型标注、CLI。这正是 AI 生成代码的样子 +——表面干净、行为缺失。现在打开 `TASK.md`,放在手边。 + +## 本课内容 + +在 Codex 应用里把课程文件夹作为项目打开,然后开一个新线程。线程是 +单个任务:它装着你的提示、Agent 的工具调用和产生的 diff。对它说: + +**“读 COURSE.md——它是本文件夹的教学契约。再读 TASK.md 和 +starter/report_tool.py。不要改任何文件。告诉我测试期望哪些行为、 +starter 缺了哪些。”** + +趁它读的时候,自己跑客观验证——在应用的终端里,或者让 Agent 在线程里 +跑(命令执行会请求你的审批;批准前先读命令): + +```bash +python verify.py starter --expect-failure +``` + +你会看到 starter 复现出四类真实世界故障,对应到测试名: + +| 失败的测试 | 真实含义 | +| --- | --- | +| `test_load_json_records_returns_list_of_dicts` | 工具只处理 CSV;API 场景直接崩 | +| `test_unsupported_suffix_raises_value_error` | 一个 `.xlsx` 上传变成莫名其妙的崩溃,而不是清楚的拒绝 | +| `test_invalid_records_are_isolated_with_reasons` | 一个空单元格中止整个运行——没有部分报告,没有原因 | +| `test_group_totals_are_rounded_to_two_decimals` | `0.1 + 0.2` 在报告里打印成 `0.30000000000000004` | +| `test_write_report_creates_missing_parent_directories` | 工具无法写进新建的输出目录 | +| `test_run_scenario_writes_report_file`、`test_main_prints_summary_and_returns_zero` | 上述问题的端到端后果 | + +然后用同样的方式跑评审过的实现: + +```bash +python verify.py solution +``` + +两条命令都以 0 退出——但含义相反。第一条证明 starter *确实处于正确的 +未完成态*;第二条证明目标可达。这一对命令是整门课的完成证据。 + +## 练习 + +挑一个你最有代入感的场景皮肤(`excel-report`、`data-monitor` 或 +`api-tool`),打开它的数据文件,找出会被拒绝的行。先不看运行结果, +写下每个坏行会触发哪个测试,再对照上面的表自查——并让线程确认, +而不是让它修。 + +## 检查点 + +运行 `python verify.py starter --expect-failure` 和 +`python verify.py solution`。不看材料能回答下面三个问题即算通过: + +1. 哪四个行为把 starter 和 solution 区分开? +2. 为什么「预期失败」是成功条件而不是自相矛盾? +3. 你在练习里找到的那一行会让 starter 崩溃、被隔离,还是悄悄通过? + +## 预期证据 + +两条命令的完整输出记录,加你的三个答案。保留好——第 4 课还会用到。 diff --git a/courses/hands-on-with-openai-codex/lessons/L02.md b/courses/hands-on-with-openai-codex/lessons/L02.md new file mode 100644 index 0000000..d72991d --- /dev/null +++ b/courses/hands-on-with-openai-codex/lessons/L02.md @@ -0,0 +1,87 @@ +--- +id: course-codex-cli-l02 +type: course +title: "Lesson 2: Write the task contract, let AGENTS.md carry the rules" +summary: 'Turn "make it better" into a bounded, testable contract — then make it durable so every Codex thread starts with the same rules.' +lang: en-US +content_version: 2 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "What this checkpoint tests" + body: "That you can read `TASK.md` as testable statements, not prose. Every sentence that starts with a function name is a contract line the suite can assert." + - title: "AGENTS.md is the instruction chain" + body: "Codex reads AGENTS.md before doing work: a global file in ~/.codex plus project files from the repo root down. Rules you want every thread to follow live there — not retyped in each prompt." + - title: "The testability check" + body: "If you cannot tell whether a statement is testable, ask: could a suite assert it without reading your mind? Numbers, exit codes, files on disk — never vibes." +--- + +# Lesson 2: Write the task contract, let AGENTS.md carry the rules + +## Objective + +You can read `TASK.md` as a set of testable statements, trace each statement +to a test in `tests/test_report_tool.py`, and write durable rules a Codex +thread will pick up automatically. + +## Why this lesson exists + +Vague requests produce vague code. "Handle bad rows better" gives an agent +permission to guess; "invalid rows are collected in `errors` with index and +reason, valid rows still produce a report" gives it a target and gives you a +way to check. In the Codex app there is a second failure mode: a rule you +typed into one thread does not exist in the next. The contract fixes the task; +`AGENTS.md` fixes the working rules. + +## The lesson + +Open `TASK.md`. Notice what every line has in common: it names an observable +behavior, not an implementation. Four statements from the contract, and the +tests that pin them: + +| Contract line | Test | +| --- | --- | +| "a JSON file loads into the same record list as CSV" | `test_load_json_records_returns_list_of_dicts` | +| "invalid rows land in `errors` with index and reason; valid rows still aggregate" | `test_invalid_records_are_isolated_with_reasons` | +| "group totals are rounded to two decimals" | `test_group_totals_are_rounded_to_two_decimals` | +| "the report write is atomic and creates missing parents" | `test_write_report_creates_missing_parent_directories` | + +Now the durable half. Codex reads `AGENTS.md` files before doing any work — +a global file in `~/.codex`, then project files from the repo root down to +your directory; closer files win. This course's working rules are exactly the +kind of thing that belongs there. Open a scratch file and draft three rules +for this project, for example: + +``` +- Change only starter/report_tool.py. tests/, solution/, scenario/ are read-only. +- One failing test group per turn; run `python verify.py starter` after each. +- Standard library only — no new dependencies. +``` + +Tell the thread: **"Here are the rules I want for this project — write them +to AGENTS.md at the folder root so every future thread starts with them."** +Then start a *new* thread and ask it to summarize its working rules. If it +quotes your three lines back, the instruction chain is working; if not, check +where the file landed. + +## Exercise + +Write one contract line for a script you actually own, using the same shape: +inputs, outputs, error cases, and "done means `<command>` exits 0". Then write +the two rules you would put in that project's AGENTS.md. + +## Checkpoint + +Run `python verify.py progress` — this checkpoint's claim code prints when +you can answer: + +1. Which `TASK.md` line does `test_invalid_records_are_isolated_with_reasons` + pin, in your own words? +2. Which file carries rules across threads — the prompt, or AGENTS.md? +3. Where does a global `~/.codex/AGENTS.md` sit in precedence versus the + project file? + +## Expected evidence + +Your drafted contract line, your two rules, and the thread's summary of its +own working rules from the new thread. diff --git a/courses/hands-on-with-openai-codex/lessons/L02_cn.md b/courses/hands-on-with-openai-codex/lessons/L02_cn.md new file mode 100644 index 0000000..ce331a8 --- /dev/null +++ b/courses/hands-on-with-openai-codex/lessons/L02_cn.md @@ -0,0 +1,81 @@ +--- +id: course-codex-cli-l02 +type: course +title: "第 2 课:写任务契约,让 AGENTS.md 承载规则" +summary: "把「改好一点」变成有边界、可测试的契约——再让它持久化,让每个 Codex 线程都从同一套规则开始。" +lang: zh-CN +content_version: 2 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "本检查点考察什么" + body: "你能把 `TASK.md` 当成可测试的陈述来读,而不是散文。每个以函数名开头的句子都是套件能断言的契约行。" + - title: "AGENTS.md 是指令链" + body: "Codex 干活前先读 AGENTS.md:~/.codex 里的全局文件,加上从仓库根到当前目录的项目文件。想让每个线程都遵守的规则放那里——不要在每个提示里重打。" + - title: "可测试性检查" + body: "判断一句话能不能测:测试套件不看你的心思能不能断言它?数字、退出码、磁盘上的文件——不看感觉。" +--- + +# 第 2 课:写任务契约,让 AGENTS.md 承载规则 + +## 目标 + +你能把 `TASK.md` 当成一组可测试陈述来读,把每条陈述追查到 +`tests/test_report_tool.py` 里的一个测试,并写出 Codex 线程会自动 +读取的持久规则。 + +## 为什么有这节课 + +含糊的需求产出含糊的代码。「把坏行处理好一点」是许可 Agent 去猜; +「无效行收进 `errors`、带下标和原因,有效行照常产出报告」是给它 +目标、也给你检查的方法。在 Codex 应用里还有第二种失败:你在某个 +线程里敲的规则,下一个线程里根本不存在。契约固定任务本身; +`AGENTS.md` 固定工作规则。 + +## 本课内容 + +打开 `TASK.md`。注意每行的共同点:它描述的是可观察的行为,不是 +实现。契约里的四句陈述,以及钉住它们的测试: + +| 契约行 | 测试 | +| --- | --- | +| 「JSON 文件要加载成与 CSV 相同的记录列表」 | `test_load_json_records_returns_list_of_dicts` | +| 「无效行带下标和原因进 `errors`;有效行照常聚合」 | `test_invalid_records_are_isolated_with_reasons` | +| 「分组总计保留两位小数」 | `test_group_totals_are_rounded_to_two_decimals` | +| 「报告写出是原子的,且会创建缺失的父目录」 | `test_write_report_creates_missing_parent_directories` | + +再看持久的另一半。Codex 做任何工作之前都会先读 `AGENTS.md`—— +`~/.codex` 里的全局文件,然后从仓库根一路向下读项目文件;越靠近 +当前目录的文件优先级越高。本课程的工作规则正是该放那里的东西。 +开一个草稿文件,为本项目起草三条规则,例如: + +``` +- 只改 starter/report_tool.py。tests/、solution/、scenario/ 只读。 +- 每轮只修一个失败测试组;修完跑 `python verify.py starter`。 +- 只用标准库——不加新依赖。 +``` + +告诉线程:**“这些是我给项目定的规则——把它们写到文件夹根的 +AGENTS.md 里,让每个新线程开工就带上。”** 然后开一个*新*线程,让它 +总结自己的工作规则。它把你的三行原样引回来,说明指令链通了;没有 +就检查文件落在哪一层。 + +## 练习 + +为你真正拥有的一个脚本写一条契约行,用同样的形状:输入、输出、 +错误情况、以及「完成 = `<命令>` 退出码为 0」。再写你会放进那个 +项目 AGENTS.md 的两条规则。 + +## 检查点 + +运行 `python verify.py progress`——能回答下面三题时,本检查点的 +认领码就会显示: + +1. 用你自己的话说,`test_invalid_records_are_isolated_with_reasons` + 钉住的是 `TASK.md` 哪一行? +2. 跨线程携带规则的是提示词还是 AGENTS.md? +3. 全局 `~/.codex/AGENTS.md` 相对项目文件的优先级顺序是什么? + +## 预期证据 + +你起草的契约行、两条规则,以及新线程对自己工作规则的总结。 diff --git a/courses/hands-on-with-openai-codex/lessons/L03.md b/courses/hands-on-with-openai-codex/lessons/L03.md new file mode 100644 index 0000000..ebbe4af --- /dev/null +++ b/courses/hands-on-with-openai-codex/lessons/L03.md @@ -0,0 +1,84 @@ +--- +id: course-codex-cli-l03 +type: course +title: "Lesson 3: The bounded change, reviewed hunk by hunk" +summary: "Drive the starter to green through bounded turns — and use the app's diff review and approvals to keep every change yours." +lang: en-US +content_version: 2 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "What this checkpoint tests" + body: "That the starter passes all nine tests through changes you supervised — `python verify.py progress` shows this code only once the starter suite is green." + - title: "Approvals are the boundary" + body: "The app asks before running commands and shows each file diff before it lands. Read the command, read the diff — approving blind is how bounded work becomes a rewrite." + - title: "When you are done" + body: "Every diff maps to a contract line, and you can explain each one without opening solution/." +--- + +# Lesson 3: The bounded change, reviewed hunk by hunk + +## Objective + +The starter passes all nine tests through a sequence of bounded changes, and +you can explain every diff the agent produced without reading the solution +first. + +## Why this lesson exists + +This is the lesson people skip — and the one that builds the actual skill. +Letting a thread rewrite the file in one shot teaches you nothing and gives +you a diff you cannot honestly review. Supervising small diffs, each pinned +to a contract line, is how the code stays yours. The Codex app's approval +model exists for exactly this: commands ask before they run, and edits land +as reviewable diffs. + +## The lesson + +Start a fresh thread (a finished task's thread carries stale context) and +say: + +**"Work through TASK.md against starter/report_tool.py. One failing test +group at a time: JSON loading, then validation isolation, then rounding, +then atomic writes, then the end-to-end tests. After each group, run +`python verify.py starter` and show me the diff before continuing."** + +Hold it to the contract's boundaries: + +- Only `starter/report_tool.py` changes. If a diff touches `tests/`, + `solution/`, or `scenario/`, stop and ask why. +- No new imports outside the standard library — and no import that is not + needed by the change being made. +- Each change should move toward one contract line. Reject drive-by refactors + ("while I was here I renamed…"). +- If the agent wants to change a test, the answer is no. Tests are the + contract; the code moves. + +Use the approval prompts deliberately. When the agent proposes a command, +read it — `python verify.py starter` is safe; `pip install` anything is a +stop sign. When it edits a file, open the diff view and read every hunk +before accepting. A diff you cannot explain is a diff you reject. + +Expect the failing count to drop group by group: 7 → 5 → 4 → 3 → 2 → 0. + +## Exercise + +Pick one hunk the agent produced — ideally the validation-isolation change — +and explain it back in the thread: "line N does X; that satisfies contract +line Y." If you cannot, ask the agent to walk you through its own diff before +accepting the next change. + +## Checkpoint + +Run `python verify.py progress`. This checkpoint's code appears only when the +starter suite is green. You pass the lesson when you can answer: + +1. How many turns did the bounded change take, and what made each turn bounded? +2. Which approval prompt did you reject or narrow, and why? +3. Point at the hunk that implements "isolate invalid rows with reasons" — + where is it? + +## Expected evidence + +A green `python verify.py starter` run, and your hunk-by-hunk explanation of +the change that earned it. diff --git a/courses/hands-on-with-openai-codex/lessons/L03_cn.md b/courses/hands-on-with-openai-codex/lessons/L03_cn.md new file mode 100644 index 0000000..43268fd --- /dev/null +++ b/courses/hands-on-with-openai-codex/lessons/L03_cn.md @@ -0,0 +1,75 @@ +--- +id: course-codex-cli-l03 +type: course +title: "第 3 课:有界改动,逐块审 diff" +summary: "通过有界的轮次把 starter 推到全绿——并用应用内的 diff 审查与审批机制守住每一处改动。" +lang: zh-CN +content_version: 2 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "本检查点考察什么" + body: "starter 通过你监督的改动让九个测试全绿——`python verify.py progress` 只在 starter 套件变绿后显示本检查点的码。" + - title: "审批就是边界" + body: "应用会在跑命令前询问、在每个文件 diff 落地前展示。读命令、读 diff——闭眼批准正是「有界工作」变成「整文件重写」的方式。" + - title: "完成的标志" + body: "每个 diff 都能对上一条契约行,而且不看 solution/ 你也讲得清每一处改动。" +--- + +# 第 3 课:有界改动,逐块审 diff + +## 目标 + +starter 通过一串有界改动让九个测试全绿,并且不看 solution,你也讲得清 +Agent 产出的每一个 diff。 + +## 为什么有这节课 + +这节课最容易被跳过——也恰恰是长本事的那节。放任一个线程把文件一次 +重写,什么也学不到,只留下一份你没底气审查的 diff。监督一连串小 +diff、各自钉在一条契约行上,才能让 AI 写的代码仍然是你的。Codex +应用的审批模型就是为这个存在的:命令要先问再跑,编辑以可审查的 +diff 落地。 + +## 本课内容 + +开一个新线程(已完结任务的线程带着过期上下文),说: + +**“按 TASK.md 改 starter/report_tool.py。一次只修一组失败测试:先 +JSON 加载,再无效行隔离,再舍入,再原子写,最后是端到端测试。每修完 +一组跑 `python verify.py starter`,先给我 diff 再继续。”** + +守住契约边界: + +- 只动 `starter/report_tool.py`。diff 碰到 `tests/`、`solution/`、 + `scenario/` 就停下问为什么。 +- 标准库之外不加 import——连与本改动无关的 import 也不要。 +- 每处改动朝一条契约行推进。顺手重构(「我顺便重命名了……」)一律 + 拒绝。 +- Agent 想改测试,答案是不行。测试是契约;动的是代码。 + +认真用审批提示。Agent 提议命令时读它——`python verify.py starter` +安全;`pip install` 任何东西都是停止信号。它改文件时打开 diff 视图, +逐块读完再接受。讲不清的 diff 就拒绝。 + +预期失败数逐组下降:7 → 5 → 4 → 3 → 2 → 0。 + +## 练习 + +挑 Agent 产出的一个 hunk——最好是「无效行隔离」那段——在线程里讲回 +给它听:「第 N 行做了 X,满足契约行 Y」。讲不清就先让它走读自己的 +diff,再接受下一处改动。 + +## 检查点 + +运行 `python verify.py progress`。本检查点的码只在 starter 套件全绿 +时出现。不看材料能回答即算通过: + +1. 这次有界改动走了几轮,是什么让每轮保持有界? +2. 你拒绝或收窄过哪个审批提示,为什么? +3. 指出实现「无效行带原因隔离」的那个 hunk——它在哪? + +## 预期证据 + +一次全绿的 `python verify.py starter` 运行,和你对这次改动的逐块 +讲解。 diff --git a/courses/hands-on-with-openai-codex/lessons/L04.md b/courses/hands-on-with-openai-codex/lessons/L04.md new file mode 100644 index 0000000..a6d2c12 --- /dev/null +++ b/courses/hands-on-with-openai-codex/lessons/L04.md @@ -0,0 +1,80 @@ +--- +id: course-codex-cli-l04 +type: course +title: "Lesson 4: Verify like a reviewer, not a spectator" +summary: "Green tests are the floor, not the ceiling — run both suites, walk the scenario skins, and review the diff the way you would review a colleague's." +lang: en-US +content_version: 2 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "What this checkpoint tests" + body: "That starter AND solution both pass — `verify.py progress` prints this code only when both suites are green. The gate is the pair, not a single run." + - title: "Review the delta, not the vibes" + body: "The app keeps the whole thread's diff reviewable. Read it as a reviewer: every hunk maps to a contract line, nothing extra moved, no test weakened." + - title: "When you are done" + body: "You have written three evidence lines — verified / not verified / known limits — and could defend the change in a code review." +--- + +# Lesson 4: Verify like a reviewer, not a spectator + +## Objective + +Both suites pass, and you hold a written record of what the green runs do and +do not prove — plus a reviewed diff you would defend to a colleague. + +## Why this lesson exists + +"All tests pass" is where most AI-assisted work stops — and where most silent +bugs start. A suite proves the contract lines it asserts; it says nothing +about the rows nobody wrote a test for. Verification is a report you write, +not a feeling you have. The Codex app's thread view makes this concrete: the +diffs, the commands, and their outputs are all on the same task — review them +together. + +## The lesson + +Run the full pair, in the app's terminal or in the thread: + +```bash +python verify.py starter # the implementation you supervised +python verify.py solution # the reviewed reference +``` + +Both green means the contract is satisfied *as written*. Now do the review +the suite cannot do for you: + +1. **Walk a skin end-to-end.** Pick a `scenario/<skin>/` folder and run the + tool on it: `python starter/report_tool.py scenario/<skin>`. Read the + produced report against the input file — recompute one group total by + hand. +2. **Review the accumulated diff.** In the thread's diff view, read the + whole change top to bottom. Every hunk should map to a contract line you + named in Lesson 2. Anything that does not is scope creep — ask the thread + to justify or revert it. +3. **Check the edges the tests skip.** Feed the tool an empty CSV, a CSV with + only invalid rows, a missing file. The contract does not pin these — your + judgment does. Write down what it does. + +## Exercise + +Write the three-line evidence record for this change: + +- **Verified:** the commands you ran and what they proved. +- **Not verified:** the inputs and behaviors no test pins. +- **Known limits:** anything you saw that the contract never promised. + +Keep it honest — "not verified" is a list of facts, not a confession. + +## Checkpoint + +Run `python verify.py progress`. This checkpoint's code prints only when +both suites pass. You pass the lesson when you can answer: + +1. What did your by-hand recompute of a group total show? +2. Name one input the suite does not pin and what the tool does with it. +3. Which hunk, if any, would you ask a colleague to justify? + +## Expected evidence + +Both green runs, your three-line evidence record, and the diff review notes. diff --git a/courses/hands-on-with-openai-codex/lessons/L04_cn.md b/courses/hands-on-with-openai-codex/lessons/L04_cn.md new file mode 100644 index 0000000..cb38e48 --- /dev/null +++ b/courses/hands-on-with-openai-codex/lessons/L04_cn.md @@ -0,0 +1,74 @@ +--- +id: course-codex-cli-l04 +type: course +title: "第 4 课:像评审者一样验证,别当观众" +summary: "测试全绿是地板不是天花板——两个套件都跑、三个皮肤都走一遍、按评审同事的标准审这份 diff。" +lang: zh-CN +content_version: 2 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "本检查点考察什么" + body: "starter 和 solution 双双通过——`verify.py progress` 只在两个套件都绿时打印本检查点的码。门槛是一对结果,不是一次运行。" + - title: "审增量,不凭感觉" + body: "应用把整个线程的 diff 保持为可审查状态。按评审者的方式读:每个 hunk 对上一条契约行、没有多余挪动、没有测试被放松。" + - title: "完成的标志" + body: "你已写下三行证据——已验证 / 未验证 / 已知局限——并且能在代码评审里为这次改动辩护。" +--- + +# 第 4 课:像评审者一样验证,别当观众 + +## 目标 + +两个套件都通过,你手握一份书面记录:说清绿色运行证明了什么、没证明 +什么——外加一份你敢向同事辩护的 diff。 + +## 为什么有这节课 + +「测试全过」是大多数 AI 辅助工作停下的地方——也是大多数隐性 bug +开始的地方。套件只证明它断言过的契约行;没人写过测试的行,它一概 +不担保。验证是你写的一份报告,不是一种感觉。Codex 应用的线程视图让 +这件事变具体:diff、命令和输出都在同一个任务里——放在一起审。 + +## 本课内容 + +跑完整的一对命令,在应用终端或线程里都行: + +```bash +python verify.py starter # 你监督出来的实现 +python verify.py solution # 评审过的参考实现 +``` + +双全绿意味着契约*按字面*被满足。现在做套件替不了你的评审: + +1. **走完一个皮肤。** 挑一个 `scenario/<skin>/` 文件夹,用工具跑它: + `python starter/report_tool.py scenario/<skin>`。对照输入文件读产出 + 的报告——亲手重算一个分组总计。 +2. **审累积的 diff。** 在线程的 diff 视图里把整份改动从头读到尾。 + 每个 hunk 都应能对上你在第 2 课点名的契约行。对不上的就是范围 + 蔓延——让线程解释或回退。 +3. **检查测试没钉的边界。** 喂给工具一个空 CSV、一个全是无效行的 + CSV、一个不存在的文件。契约没钉这些——你的判断钉。记下它的行为。 + +## 练习 + +为这次改动写三行证据记录: + +- **已验证:** 你跑过的命令和它们证明了什么。 +- **未验证:** 没有测试钉住的输入与行为。 +- **已知局限:** 你看到的、契约从未承诺过的东西。 + +写得诚实——「未验证」是一份事实清单,不是忏悔书。 + +## 检查点 + +运行 `python verify.py progress`。本检查点的码只在两个套件都过时 +打印。不看材料能回答即算通过: + +1. 你手工重算的分组总计结果如何? +2. 说出一个套件没钉的输入,以及工具对它的实际行为。 +3. 哪个 hunk(如果有)你会请同事来解释? + +## 预期证据 + +两次全绿运行、你的三行证据记录、diff 评审笔记。 diff --git a/courses/hands-on-with-openai-codex/lessons/L05.md b/courses/hands-on-with-openai-codex/lessons/L05.md new file mode 100644 index 0000000..ed6fce6 --- /dev/null +++ b/courses/hands-on-with-openai-codex/lessons/L05.md @@ -0,0 +1,75 @@ +--- +id: course-codex-cli-l05 +type: course +title: "Lesson 5: Take the workflow to your own project" +summary: "The tool generalizes: task contract, bounded thread, approvals, diff review — run the same loop on a script you actually own." +lang: en-US +content_version: 2 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "What this checkpoint tests" + body: "Self-attested: you confirm you applied the loop to your own project. The evidence is your contract, your thread's diffs, and your verification record — not a code we can check." + - title: "Carry the shape, not the code" + body: "What transfers is the loop: contract first, one bounded thread per step, approvals read before granted, diff reviewed before accepted, verification written down." + - title: "When you are done" + body: "Your own script has a contract, one bounded change you can explain, and a verified/not-verified/known-limits record — or a documented reason the loop did not fit." +--- + +# Lesson 5: Take the workflow to your own project + +## Objective + +You apply the whole loop — contract, bounded thread, approvals, diff review, +written verification — to a script or project you actually own, and record +what happened. + +## Why this lesson exists + +A course folder is a safe sandbox; your own codebase is not. The point of the +last four lessons was never the report tool — it was a repeatable way to +drive an agent that keeps you in charge of what ships. This checkpoint +exists to prove the transfer happened. It is self-attested: only you know +whether the loop actually ran. + +## The lesson + +Pick a real script you own — one with a bug you have been putting off, or a +behavior you want but have not written. Then run the same five moves: + +1. **Contract.** Write the task contract first: inputs, outputs, error + cases, and "done means `<command>` exits 0". One short paragraph, every + sentence testable. +2. **Rules that persist.** Open the project in the Codex app and put the + working rules in its `AGENTS.md` — the files the agent may touch, the + verification command, the dependency policy. The next thread inherits + them. +3. **Bounded thread.** One thread, one contract line at a time. Read every + approval prompt; read every diff hunk before it lands. +4. **Verify.** Run the verification command, then do the review the suite + cannot do — walk one real input end-to-end and check the output by hand. +5. **Record.** Write the three lines: verified / not verified / known limits. + +If the loop fought you — the task was too small to need a contract, or too +vague to test — that is a finding too. Write down where the shape did not +fit; that judgment is the actual skill. + +## Exercise + +If your project has more than one boundary worth pinning, draft the next +contract line now — you will want it when you open the next thread. + +## Checkpoint + +This checkpoint is self-attested: claim it when you can answer: + +1. What was your contract's "done means" command? +2. Which approval or diff did you push back on? +3. What is on your "not verified" list that a stranger would assume was + covered? + +## Expected evidence + +Your contract paragraph, the AGENTS.md rules you wrote, and the three-line +verification record. These are yours — they are the proof the workflow +transferred. diff --git a/courses/hands-on-with-openai-codex/lessons/L05_cn.md b/courses/hands-on-with-openai-codex/lessons/L05_cn.md new file mode 100644 index 0000000..9b8f8fa --- /dev/null +++ b/courses/hands-on-with-openai-codex/lessons/L05_cn.md @@ -0,0 +1,68 @@ +--- +id: course-codex-cli-l05 +type: course +title: "第 5 课:把工作流带回自己的项目" +summary: "工具会泛化:任务契约、有界线程、审批、diff 评审——在你真正拥有的脚本上跑同一套闭环。" +lang: zh-CN +content_version: 2 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "本检查点考察什么" + body: "自我报告:你确认把这套闭环用到了自己的项目上。证据是你的契约、你线程的 diff、你的验证记录——不是我们查得了的码。" + - title: "带走形状,不是代码" + body: "能迁移的是闭环:契约先行、每步一个有界线程、审批先读再放行、diff 审过才落地、验证写下来。" + - title: "完成的标志" + body: "你自己的脚本有了一份契约、一处你讲得清的有界改动、一份已验证/未验证/已知局限记录——或一条写清楚的「闭环不合身」的原因。" +--- + +# 第 5 课:把工作流带回自己的项目 + +## 目标 + +你把整套闭环——契约、有界线程、审批、diff 评审、书面验证——用到 +自己真正拥有的脚本或项目上,并记录发生了什么。 + +## 为什么有这节课 + +课程文件夹是安全沙箱;你自己的代码库不是。前四课的意义从来不是那个 +报表工具——而是一种可复用的、让你始终掌控产出的 Agent 驱动方式。 +本检查点要证明迁移真的发生了。它是自我报告的:闭环跑没跑过只有你 +知道。 + +## 本课内容 + +挑一个你真正拥有的脚本——一个你拖了很久的 bug,或一个想要但还没写 +的行为。然后走同样的五步: + +1. **契约。** 先写任务契约:输入、输出、错误情况、以及「完成 = + `<命令>` 退出码为 0」。一小段,每句都可测试。 +2. **能留存的规则。** 在 Codex 应用里打开项目,把工作规则写进它的 + `AGENTS.md`——Agent 可以碰哪些文件、验证命令是什么、依赖政策。 + 下一个线程自动继承。 +3. **有界线程。** 一个线程、一次一条契约行。每个审批提示都读;每个 + diff 的每块都在落地前读完。 +4. **验证。** 跑验证命令,然后做套件做不了的评审——拿一个真实输入 + 端到端走一遍,手工核对输出。 +5. **记录。** 写三行:已验证 / 未验证 / 已知局限。 + +如果闭环跟你打架——任务小到不需要契约,或模糊到不可测——那也是 +发现。写下形状不合身的地方;这个判断本身就是真本事。 + +## 练习 + +如果你的项目还有不止一处值得钉住的边界,现在就起草下一条契约行 +——开下一个线程时你会用得上。 + +## 检查点 + +本检查点自我报告:能回答下面三题时即可认领: + +1. 你的契约里「完成 =」命令是什么? +2. 你在哪个审批或 diff 上顶回去过? +3. 你的「未验证」清单上有什么是陌生人会以为已覆盖的? + +## 预期证据 + +你的契约段落、你写的 AGENTS.md 规则、三行验证记录。它们是你自己的 +——是工作流完成迁移的证明。 diff --git a/courses/hands-on-with-openai-codex/scenario/api-tool/scenario.json b/courses/hands-on-with-openai-codex/scenario/api-tool/scenario.json new file mode 100644 index 0000000..e17e7c2 --- /dev/null +++ b/courses/hands-on-with-openai-codex/scenario/api-tool/scenario.json @@ -0,0 +1,8 @@ +{ + "skin": "api-tool", + "data_file": "tickets.json", + "required_fields": ["ticket_id", "priority", "minutes"], + "numeric_field": "minutes", + "group_field": "priority", + "report_file": "report.json" +} diff --git a/courses/hands-on-with-openai-codex/scenario/api-tool/tickets.json b/courses/hands-on-with-openai-codex/scenario/api-tool/tickets.json new file mode 100644 index 0000000..03e35b7 --- /dev/null +++ b/courses/hands-on-with-openai-codex/scenario/api-tool/tickets.json @@ -0,0 +1,9 @@ +[ + {"ticket_id": "T-101", "priority": "high", "minutes": "45"}, + {"ticket_id": "T-102", "priority": "normal", "minutes": "20"}, + {"ticket_id": "T-103", "priority": "low", "minutes": "10"}, + {"ticket_id": "T-104", "priority": "high", "minutes": "90"}, + {"ticket_id": "T-105", "priority": "", "minutes": "30"}, + {"ticket_id": "T-106", "priority": "normal", "minutes": "n/a"}, + {"ticket_id": "T-107", "priority": "low", "minutes": "25"} +] diff --git a/courses/hands-on-with-openai-codex/scenario/data-monitor/metrics.csv b/courses/hands-on-with-openai-codex/scenario/data-monitor/metrics.csv new file mode 100644 index 0000000..15a5a15 --- /dev/null +++ b/courses/hands-on-with-openai-codex/scenario/data-monitor/metrics.csv @@ -0,0 +1,8 @@ +host,service,latency_ms +web-1,api,182 +web-2,api,205 +db-1,postgres,44 +web-3,frontend,96 +db-2,,51 +web-4,api,too-slow +cache-1,redis,12 diff --git a/courses/hands-on-with-openai-codex/scenario/data-monitor/scenario.json b/courses/hands-on-with-openai-codex/scenario/data-monitor/scenario.json new file mode 100644 index 0000000..b103477 --- /dev/null +++ b/courses/hands-on-with-openai-codex/scenario/data-monitor/scenario.json @@ -0,0 +1,8 @@ +{ + "skin": "data-monitor", + "data_file": "metrics.csv", + "required_fields": ["host", "service", "latency_ms"], + "numeric_field": "latency_ms", + "group_field": "service", + "report_file": "report.json" +} diff --git a/courses/hands-on-with-openai-codex/scenario/excel-report/orders.csv b/courses/hands-on-with-openai-codex/scenario/excel-report/orders.csv new file mode 100644 index 0000000..a41fca1 --- /dev/null +++ b/courses/hands-on-with-openai-codex/scenario/excel-report/orders.csv @@ -0,0 +1,8 @@ +order_id,region,amount,item_count +1001,emea,249.99,3 +1002,apac,89.50,1 +1003,emea,,2 +1004,americas,120.00,5 +1005,,310.00,4 +1006,apac,45.25,2 +1007,emea,640.10,6 diff --git a/courses/hands-on-with-openai-codex/scenario/excel-report/scenario.json b/courses/hands-on-with-openai-codex/scenario/excel-report/scenario.json new file mode 100644 index 0000000..66b2bd9 --- /dev/null +++ b/courses/hands-on-with-openai-codex/scenario/excel-report/scenario.json @@ -0,0 +1,8 @@ +{ + "skin": "excel-report", + "data_file": "orders.csv", + "required_fields": ["order_id", "region", "amount"], + "numeric_field": "amount", + "group_field": "region", + "report_file": "report.json" +} diff --git a/courses/hands-on-with-openai-codex/solution/report_tool.py b/courses/hands-on-with-openai-codex/solution/report_tool.py new file mode 100644 index 0000000..65ba0bf --- /dev/null +++ b/courses/hands-on-with-openai-codex/solution/report_tool.py @@ -0,0 +1,134 @@ +"""Scenario report tool (reviewed solution). + +Reads a scenario data file (CSV or JSON), isolates invalid rows instead of +crashing, aggregates valid rows per group with two-decimal rounding, and +writes the report atomically so an interrupted run never leaves a half-written +file. Standard library only. +""" + +from __future__ import annotations + +import csv +import json +import os +import sys +from pathlib import Path + +SUPPORTED_SUFFIXES = {".csv", ".json"} + + +def load_records(path: str | Path) -> list[dict]: + """Load CSV or JSON records from *path* as a list of dicts.""" + source = Path(path) + suffix = source.suffix.lower() + if suffix not in SUPPORTED_SUFFIXES: + raise ValueError(f"unsupported data file type: {source.name}") + if suffix == ".csv": + with source.open("r", encoding="utf-8", newline="") as handle: + records = list(csv.DictReader(handle)) + else: + data = json.loads(source.read_text(encoding="utf-8")) + records = data if isinstance(data, list) else None + if not all(isinstance(record, dict) for record in records): + raise ValueError("data file must contain a list of objects") + return records + + +def _field_text(record: dict, field: str) -> str: + value = record.get(field) + if not isinstance(value, str): + return "" if value is None else str(value).strip() + return value.strip() + + +def build_report( + records: list[dict], + *, + required_fields: list[str], + numeric_field: str, + group_field: str, +) -> dict: + """Aggregate *records*, isolating invalid rows with recorded reasons.""" + groups: dict[str, dict] = {} + errors: list[dict] = [] + for index, record in enumerate(records): + if not isinstance(record, dict): + errors.append({"index": index, "reason": "record is not an object"}) + continue + missing = [ + field for field in required_fields if not _field_text(record, field) + ] + if missing: + errors.append( + {"index": index, "reason": f"missing required field: {missing[0]}"} + ) + continue + raw_number = _field_text(record, numeric_field) + try: + number = float(raw_number) + except ValueError: + errors.append( + { + "index": index, + "reason": f"field {numeric_field!r} is not a number: {raw_number!r}", + } + ) + continue + group = _field_text(record, group_field) + bucket = groups.setdefault(group, {"count": 0, "total": 0.0}) + bucket["count"] += 1 + bucket["total"] = round(bucket["total"] + number, 2) + return { + "total": len(records), + "valid": len(records) - len(errors), + "invalid": len(errors), + "groups": groups, + "errors": errors, + } + + +def write_report(report: dict, destination: str | Path) -> None: + """Atomically write *report* as JSON, creating parent directories.""" + target = Path(destination) + target.parent.mkdir(parents=True, exist_ok=True) + temporary = target.with_name(target.name + ".tmp") + temporary.write_text( + json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8" + ) + os.replace(temporary, target) + + +def run_scenario(scenario_dir: str | Path) -> dict: + """Load scenario.json, process its data file, and write the report.""" + directory = Path(scenario_dir) + config = json.loads((directory / "scenario.json").read_text(encoding="utf-8")) + records = load_records(directory / config["data_file"]) + report = build_report( + records, + required_fields=config["required_fields"], + numeric_field=config["numeric_field"], + group_field=config["group_field"], + ) + write_report(report, directory / config["report_file"]) + return report + + +def main(argv: list[str] | None = None) -> int: + """Run one scenario directory and print a one-line summary.""" + arguments = sys.argv[1:] if argv is None else argv + if len(arguments) != 1: + print("usage: python report_tool.py <scenario-dir>", file=sys.stderr) + return 2 + try: + report = run_scenario(arguments[0]) + except (OSError, ValueError, KeyError, json.JSONDecodeError) as error: + print(f"error: {error}", file=sys.stderr) + return 1 + print( + f"total={report['total']} valid={report['valid']} invalid={report['invalid']}" + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/courses/hands-on-with-openai-codex/starter/report_tool.py b/courses/hands-on-with-openai-codex/starter/report_tool.py new file mode 100644 index 0000000..019edb8 --- /dev/null +++ b/courses/hands-on-with-openai-codex/starter/report_tool.py @@ -0,0 +1,88 @@ +"""Scenario report tool (starter, deliberately incomplete). + +Reads a scenario data file, aggregates valid rows, and writes a JSON report. +This starter reproduces the classic "it runs on the happy path" state of an +AI-written script: JSON inputs crash, invalid rows abort the run, group totals +carry floating-point noise, and reports cannot be written into a fresh +directory. The task contract in ../TASK.md defines the expected behavior. +""" + +from __future__ import annotations + +import csv +import json +import sys +from pathlib import Path + + +def load_records(path: str | Path) -> list[dict]: + """Load CSV or JSON records from *path* as a list of dicts.""" + source = Path(path) + with source.open("r", encoding="utf-8", newline="") as handle: + return list(csv.DictReader(handle)) + + +def build_report( + records: list[dict], + *, + required_fields: list[str], + numeric_field: str, + group_field: str, +) -> dict: + """Aggregate *records* into a summary report.""" + groups: dict[str, dict] = {} + for record in records: + for field in required_fields: + value = (record.get(field) or "").strip() + if not value: + raise KeyError(f"missing required field: {field}") + group = record[group_field].strip() + bucket = groups.setdefault(group, {"count": 0, "total": 0.0}) + bucket["count"] += 1 + bucket["total"] += float(record[numeric_field]) + return { + "total": len(records), + "valid": len(records), + "invalid": 0, + "groups": groups, + "errors": [], + } + + +def write_report(report: dict, destination: str | Path) -> None: + """Write *report* as JSON to *destination*.""" + Path(destination).write_text( + json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8" + ) + + +def run_scenario(scenario_dir: str | Path) -> dict: + """Load scenario.json, process its data file, and write the report.""" + directory = Path(scenario_dir) + config = json.loads((directory / "scenario.json").read_text(encoding="utf-8")) + records = load_records(directory / config["data_file"]) + report = build_report( + records, + required_fields=config["required_fields"], + numeric_field=config["numeric_field"], + group_field=config["group_field"], + ) + write_report(report, directory / config["report_file"]) + return report + + +def main(argv: list[str] | None = None) -> int: + """Run one scenario directory and print a one-line summary.""" + arguments = sys.argv[1:] if argv is None else argv + if len(arguments) != 1: + print("usage: python report_tool.py <scenario-dir>", file=sys.stderr) + return 2 + report = run_scenario(arguments[0]) + print( + f"total={report['total']} valid={report['valid']} invalid={report['invalid']}" + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/courses/hands-on-with-openai-codex/tests/test_report_tool.py b/courses/hands-on-with-openai-codex/tests/test_report_tool.py new file mode 100644 index 0000000..3fbc19f --- /dev/null +++ b/courses/hands-on-with-openai-codex/tests/test_report_tool.py @@ -0,0 +1,115 @@ +"""Contract tests for the scenario report tool. + +The suite is run twice: against ``starter/`` (specific failures expected) and +against ``solution/`` (everything must pass). See ../TASK.md for the contract. +""" + +from __future__ import annotations + +import json +import shutil +import tempfile +import unittest +from pathlib import Path + +import report_tool + +COURSE_ROOT = Path(__file__).resolve().parent.parent +SCENARIO_ROOT = COURSE_ROOT / "scenario" + + +class LoadRecordsTest(unittest.TestCase): + def test_load_csv_records_returns_list_of_dicts(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + data = Path(tmp) / "rows.csv" + data.write_text("id,region,amount\n1,emea,10.5\n2,apac,3\n", encoding="utf-8") + records = report_tool.load_records(data) + self.assertEqual(records, [{"id": "1", "region": "emea", "amount": "10.5"}, {"id": "2", "region": "apac", "amount": "3"}]) + + def test_load_json_records_returns_list_of_dicts(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + data = Path(tmp) / "rows.json" + data.write_text('[{"id": "1", "amount": "2"}]', encoding="utf-8") + records = report_tool.load_records(data) + self.assertEqual(records, [{"id": "1", "amount": "2"}]) + + def test_unsupported_suffix_raises_value_error(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + data = Path(tmp) / "rows.xlsx" + with self.assertRaises(ValueError): + report_tool.load_records(data) + + +class BuildReportTest(unittest.TestCase): + def test_invalid_records_are_isolated_with_reasons(self) -> None: + records = [ + {"id": "1", "region": "emea", "amount": "10"}, + {"id": "2", "region": "", "amount": "5"}, # missing group value + {"id": "", "region": "emea", "amount": "5"}, # missing required id + {"id": "3", "region": "apac", "amount": "n/a"} # non-numeric amount + ] + report = report_tool.build_report( + records, + required_fields=["id", "region", "amount"], + numeric_field="amount", + group_field="region", + ) + self.assertEqual(report["total"], 4) + self.assertEqual(report["valid"], 1) + self.assertEqual(report["invalid"], 3) + self.assertEqual([error["index"] for error in report["errors"]], [1, 2, 3]) + self.assertTrue(all(error["reason"] for error in report["errors"])) + self.assertEqual(report["groups"], {"emea": {"count": 1, "total": 10.0}}) + + def test_group_totals_are_rounded_to_two_decimals(self) -> None: + records = [ + {"id": "1", "region": "emea", "amount": "0.1"}, + {"id": "2", "region": "emea", "amount": "0.2"}, + ] + report = report_tool.build_report( + records, + required_fields=["id", "region", "amount"], + numeric_field="amount", + group_field="region", + ) + self.assertEqual(report["groups"]["emea"]["total"], 0.3) + + +class WriteReportTest(unittest.TestCase): + def test_write_report_creates_missing_parent_directories(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + destination = Path(tmp) / "out" / "nested" / "report.json" + report_tool.write_report({"total": 1}, destination) + self.assertTrue(destination.exists()) + self.assertEqual(json.loads(destination.read_text(encoding="utf-8"))["total"], 1) + self.assertFalse(destination.with_name(destination.name + ".tmp").exists()) + + +class ScenarioTest(unittest.TestCase): + def _copy(self, name: str) -> Path: + target = Path(tempfile.mkdtemp(prefix="scenario-")) / name + shutil.copytree(SCENARIO_ROOT / name, target) + self.addCleanup(shutil.rmtree, target.parent, ignore_errors=True) + return target + + def test_run_scenario_writes_report_file(self) -> None: + scenario = self._copy("api-tool") + report = report_tool.run_scenario(scenario) + written = json.loads((scenario / "report.json").read_text(encoding="utf-8")) + self.assertEqual(written, report) + self.assertEqual(report["total"], 7) + self.assertEqual(report["invalid"], 2) + self.assertEqual(sorted(report["groups"]), ["high", "low", "normal"]) + + def test_main_prints_summary_and_returns_zero(self) -> None: + scenario = self._copy("excel-report") + exit_code = report_tool.main([str(scenario)]) + self.assertEqual(exit_code, 0) + self.assertTrue((scenario / "report.json").exists()) + + def test_main_rejects_missing_argument(self) -> None: + self.assertEqual(report_tool.main([]), 2) + + +if __name__ == "__main__": + unittest.main() diff --git a/courses/hands-on-with-openai-codex-cli/verify.py b/courses/hands-on-with-openai-codex/verify.py similarity index 54% rename from courses/hands-on-with-openai-codex-cli/verify.py rename to courses/hands-on-with-openai-codex/verify.py index f21d039..a42d4e3 100644 --- a/courses/hands-on-with-openai-codex-cli/verify.py +++ b/courses/hands-on-with-openai-codex/verify.py @@ -1,10 +1,11 @@ #!/usr/bin/env python3 """Run the course contract against starter or solution. -Objective completion evidence for -"Hands-on with OpenAI Codex CLI". Exit 0 means the selected implementation -satisfies the task contract; ``--expect-failure`` reproduces the unfinished -starter state and checks that the failing tests are the intended ones. +Objective completion evidence for a FlyPython shared-core course — the +teaching contract lives in COURSE.md; this file is the exercise. Exit 0 +means the selected implementation satisfies the task contract; +``--expect-failure`` reproduces the unfinished starter state and checks that +the failing tests are the intended ones. """ from __future__ import annotations @@ -20,6 +21,15 @@ ROOT = Path(__file__).resolve().parent +# Optional shared claim-receipt producer (docs/CLAIM-RECEIPT.md). The course +# folder still verifies standalone — without the tools/ sibling or without +# FLYPYTHON_CLAIM_SECRET set, verify.py behaves exactly as before. +try: + sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "tools")) + import claim_receipt +except ImportError: + claim_receipt = None + EXPECTED_STARTER_FAILURES = ( "test_load_json_records_returns_list_of_dicts", "test_unsupported_suffix_raises_value_error", @@ -32,6 +42,10 @@ +# ── PER-COURSE BLOCK ──────────────────────────────────────────── +# The only section allowed to differ across shared-core courses +# (checkpoint ids and gates must still match — titles may carry the +# tool's flavor). tools/verify_courses.py enforces this. COURSE_ID = 'course-codex-cli' # Documented constant: claim codes derive deterministically from # (COURSE_ID, checkpoint_id, COURSE_SALT). They are spot-checkable @@ -46,6 +60,7 @@ {"id": "l04", "gate": "both-suites", "title": '验证与审查 / Verify and review'}, {"id": "l05", "gate": "attest", "title": '应用到自己的项目 / Apply to your project'}, ] +# ── END PER-COURSE BLOCK ──────────────────────────────────────── def _claim_code(checkpoint_id): digest = hashlib.sha256( @@ -61,9 +76,14 @@ def _run_suite(implementation): env=environment, check=False, capture_output=True, text=True, ) -def run_progress(as_json): +def run_progress(as_json, receipt_out=None): + import time + t0 = time.monotonic() starter = _run_suite("starter") + starter_ms = int((time.monotonic() - t0) * 1000) + t0 = time.monotonic() solution = _run_suite("solution") + solution_ms = int((time.monotonic() - t0) * 1000) starter_ok = starter.returncode == 0 solution_ok = solution.returncode == 0 rows = [] @@ -83,21 +103,51 @@ def run_progress(as_json): row["kind"] = kind row["claim_code"] = code rows.append(row) + secret = claim_receipt.receipts_enabled() if claim_receipt else None + receipts = [] + if secret: + starter_tests = claim_receipt.count_tests(starter.stderr) + solution_tests = claim_receipt.count_tests(solution.stderr) + solution_hash = claim_receipt.solution_sha256(ROOT) + for checkpoint in CHECKPOINTS: + gate = checkpoint["gate"] + if gate == "starter-suite": + passed, tests, ms = starter_ok, starter_tests, starter_ms + elif gate == "both-suites": + passed = starter_ok and solution_ok + tests, ms = starter_tests + solution_tests, starter_ms + solution_ms + else: + continue + receipts.append(claim_receipt.make_receipt( + COURSE_ID, checkpoint["id"], passed=passed, tests=tests, + duration_ms=ms, impl_dir=ROOT / "starter", + solution_hash=solution_hash, secret=secret)) if as_json: - print(json.dumps({"course": COURSE_ID, + document = {"course": COURSE_ID, "starter_suite_passed": starter_ok, "solution_suite_passed": solution_ok, - "checkpoints": rows}, ensure_ascii=False, indent=2)) + "checkpoints": rows} + if secret: + document["receipts"] = receipts + print(json.dumps(document, ensure_ascii=False, indent=2)) else: - starter_state = "通过" if starter_ok else "未通过" - solution_state = "通过" if solution_ok else "未通过" - print("课程 " + COURSE_ID) - print("实现状态: starter " + starter_state + " / solution " + solution_state) + starter_state = "passed" if starter_ok else "not passed" + solution_state = "passed" if solution_ok else "not passed" + print("Course " + COURSE_ID) + print("Suites: starter " + starter_state + " / solution " + solution_state) for row in rows: - state = row["status"] + ("(自报)" if row["kind"] == "attested" else "") - code = "认领码 " + row["claim_code"] if row["claim_code"] else "—" + state = row["status"] + (" (self-attested)" if row["kind"] == "attested" else "") + code = "claim code " + row["claim_code"] if row["claim_code"] else "—" print(" " + row["id"] + " " + row["title"] + " [" + state + "] " + code) - print("认领码是自我报告的证据,在 flypython.com 记录;绝非证书。") + print("Claim codes are self-reported evidence, recorded at flypython.com; never a certificate.") + if secret: + print(f"Signed run receipts prepared for {len(receipts)} gated checkpoint(s);" + " submit each with its claim to mark them as a local-run receipt.") + if receipt_out and secret: + Path(receipt_out).write_text( + json.dumps({"receipts": receipts}, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8") + print(f"Wrote {len(receipts)} receipt(s) to {receipt_out}", file=sys.stderr) return 0 def main() -> int: @@ -105,10 +155,11 @@ def main() -> int: parser.add_argument("implementation", choices=("progress", "starter", "solution")) parser.add_argument("--json", action="store_true") parser.add_argument("--expect-failure", action="store_true") + parser.add_argument("--receipt-out", metavar="PATH", help="write signed run receipts JSON (requires FLYPYTHON_CLAIM_SECRET)") args = parser.parse_args() if args.implementation == "progress": - return run_progress(args.json) + return run_progress(args.json, args.receipt_out) command = [sys.executable, "-m", "unittest", "discover", "-s", str(ROOT / "tests")] environment = os.environ.copy() @@ -139,6 +190,10 @@ def main() -> int: "directory." ) return 0 + if result.returncode == 0: + print(f"{args.implementation}: all tests passed") + else: + sys.stderr.write((result.stderr or "") or (result.stdout or "")) return result.returncode diff --git a/courses/hands-on-with-zcode/COURSE.md b/courses/hands-on-with-zcode/COURSE.md new file mode 100644 index 0000000..b36b7af --- /dev/null +++ b/courses/hands-on-with-zcode/COURSE.md @@ -0,0 +1,111 @@ +--- +id: course-zcode +type: course +title: Hands-on with ZCode +summary: The verified Python workflow — task contract, bounded change, objective verify.py evidence — taught hands-on in the ZCode desktop workspace, where Goal Mode decomposes the work and the Git state shows every change. +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +badge: + id: course-zcode + name_en: Reproduce with ZCode in the loop + name_zh: ZCode 协同复现 + requires: All five checkpoints claimed (L01–L05) +course_id: course-zcode +--- + +# Hands-on with ZCode + +> TL;DR: download this folder, open it as a workspace in the ZCode desktop +> app, and say **"start lesson 1"**. You finish with a working report tool, +> a reproducible pass/fail command, and the ZCode workflow: Goal Mode task +> decomposition, file references, and the workspace Git state as your review +> surface. The exercise core is the same one the other agent-tool courses +> use — only the tool you drive changes. + +## What you build + +A small Python report tool that reads messy real-world data (CSV or JSON), +isolates invalid rows instead of crashing, aggregates valid rows, and writes +its report atomically. Three scenario "skins" ship with the course so you can +practice on a domain you recognize: + +| Skin | You are… | Data | +| --- | --- | --- | +| `scenario/excel-report/` | turning a weekly orders export into a region summary | `orders.csv` | +| `scenario/data-monitor/` | checking service latency numbers from your servers | `metrics.csv` | +| `scenario/api-tool/` | summarizing ticket data pulled from an internal API | `tickets.json` | + +All three run through the same code and the same tests. The skills — +contracts, error isolation, rounding, atomic writes, verification — are the +actual product; the skins just make them concrete. + +## Teaching contract (read this first, agent) + +If you are the ZCode agent teaching this course, follow these rules: + +- **Audience:** a project owner who has working-with-AI experience but is + stuck on reliability — the script runs, then breaks on real data. Not a + Python beginner tutorial; not a prompt-engineering course. +- **Prerequisites:** Python 3.11+ on PATH and the ZCode desktop app + installed and signed in. No packages to install — the course is standard + library only. Plan/usage-limit coverage is unverified at authoring time — + see `REVIEW.md`. +- **Tool:** authored against ZCode as documented at zcode.z.ai/docs + (desktop-native workspace, Goal Mode objective management and recovery, + task lists, file references, remote control). A live teaching run-through + is pending — `REVIEW.md` records what has and has not been exercised. + A ZCode major release triggers a course re-review. +- **Lesson order:** L01 → L02 → L03 → L04 → L05, one lesson per session. + Never skip the checkpoint at the end of a lesson. +- **Teaching style:** work from the files in this folder, not from general + knowledge. Quote the exact contract line you are satisfying. Make the + smallest change that moves a test from red to green. Never add + dependencies, never edit `solution/`, and ask before touching any file + not named in the current lesson. +- **When to stop:** a lesson is done when its checkpoint command runs and + the learner can say, in their own words, what failed and why. If the + learner cannot, re-teach from the failing test, not from the solution. +- **How to use `verify.py`:** run it in the workspace terminal or let the + agent run it as a task step — `python verify.py starter + --expect-failure` must reproduce the listed failures; `python verify.py + solution` must pass. "Done" means the starter implementation passes the + same suite after the learner's bounded change. +- **Honesty rules:** say what you did not verify. Do not claim the code is + production-ready. Do not promise learning or career outcomes. + +## What this course does NOT cover + +ZCode installation and plans, Remote Control pairing setup beyond a +mention, model-provider configuration, or selling anything. For those, the +companion repository has guides and playbooks — link, don't improvise. + +## Badge contract + +- Badge: **Reproduce with ZCode in the loop Badge** (badge id `course-zcode`) — earned by claiming all five checkpoints. +- Challenges: L01–L05 checkpoints, 10 points each; +50 course-badge bonus when all five are claimed on flypython.com. +- Evidence: `python verify.py progress` — L03 (bounded change) and L04 (verify & review) are objectively gated by the suite; L01/L02/L05 are learner-attested. +- Submission: each passed checkpoint prints a deterministic claim code; record it on flypython.com against your account. Self-reported evidence, never a certificate. + +## Folder map + +``` +COURSE.md / COURSE_cn.md this file (EN / 中文) +lessons/L01.md … L05.md lessons (each has an _cn.md pair) +scenario/<skin>/ data files and scenario.json per skin +TASK.md / TASK_cn.md the task contract the change must satisfy +starter/report_tool.py the deliberately unfinished implementation +solution/report_tool.py the reviewed solution (do not copy in lesson 3) +tests/test_report_tool.py the contract suite (read-only) +verify.py objective pass/fail evidence +REVIEW.md maintainer run-through record +``` + +## Evidence and licensing + +The course folder is reviewed content: `REVIEW.md` records the last +run-through with dates, tool versions, and observed deviations. Code in this +folder is MIT-licensed; lesson prose is CC BY 4.0 (see repository `LICENSE`). +Report teaching drift or unclear lessons via the repository's +`course-feedback` issue form. diff --git a/courses/hands-on-with-zcode/COURSE_cn.md b/courses/hands-on-with-zcode/COURSE_cn.md new file mode 100644 index 0000000..b36e8d6 --- /dev/null +++ b/courses/hands-on-with-zcode/COURSE_cn.md @@ -0,0 +1,101 @@ +--- +id: course-zcode +type: course +title: "上手 ZCode" +summary: "经过验证的 Python 工作流——任务契约、有界改动、客观 verify.py 证据——在 ZCode 桌面工作区里手把手教:Goal 模式拆解任务,Git 状态展示每一处改动。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +badge: + id: course-zcode + name_en: Reproduce with ZCode in the loop + name_zh: ZCode 协同复现 + requires: 认领全部五个检查点(L01–L05) +course_id: course-zcode +--- + +# 上手 ZCode + +> 一句话:下载本文件夹,在 ZCode 桌面应用里作为工作区打开,对它说 +> **「开始第 1 课」**。完成时你会得到一个能跑的报表工具、一条可复现的 +> 通过/失败命令,以及 ZCode 工作流:Goal 模式任务拆解、文件引用、 +> 以及作为你审查面的工作区 Git 状态。练习核心与其他 Agent 工具课程 +> 完全相同——变的只是你驱动的工具。 + +## 你要做的东西 + +一个小型 Python 报表工具:读进脏乱的真实数据(CSV 或 JSON)、隔离 +无效行而不是直接崩溃、聚合有效行、原子地写出报告。课程自带三个 +「场景皮肤」,让你在自己熟悉的领域里练习: + +| 皮肤 | 你是…… | 数据 | +| --- | --- | --- | +| `scenario/excel-report/` | 把每周订单导出整理成区域汇总 | `orders.csv` | +| `scenario/data-monitor/` | 检查自家服务器的延迟数据 | `metrics.csv` | +| `scenario/api-tool/` | 汇总从内部 API 拉取的工单数据 | `tickets.json` | + +三个皮肤跑的是同一份代码、同一套测试。真正的产品是这些技能——契约、 +错误隔离、舍入、原子写、验证——皮肤只是让它们变得具体。 + +## 教学契约(先读,Agent) + +如果你是教授本课程的 ZCode Agent,请遵守以下规则: + +- **受众:** 有 AI 协作经验、但困在可靠性上的项目所有者——脚本能跑, + 一遇真实数据就坏。这不是 Python 入门教程,也不是提示词工程课。 +- **前置条件:** PATH 上有 Python 3.11+,装有 ZCode 桌面应用并已 + 登录。无需安装任何包——课程只用标准库。套餐/用量额度覆盖在 + 编写时未经证实——见 `REVIEW.md`。 +- **工具:** 按 zcode.z.ai/docs 上文档描述的 ZCode 编写(桌面原生 + 工作区、Goal 模式目标管理与恢复、任务列表、文件引用、远程控制)。 + 真实授课实跑尚待完成——`REVIEW.md` 记录已验证与未验证的部分。 + ZCode 主版本发布会触发课程重审。 +- **课序:** L01 → L02 → L03 → L04 → L05,一次课一个会话。 + 绝不跳过课尾的检查点。 +- **教学风格:** 从本文件夹里的文件出发,不要凭通用知识发挥。引用 + 你正在满足的那条契约原文。做让测试从红变绿的最小改动。绝不加 + 依赖、绝不改 `solution/`、动当前课程未点名的任何文件前先询问。 +- **何时停下:** 一节课的检查点命令跑通、且学习者能用自己的话说清 + 哪里坏了、为什么,这节课才算完。学习者说不出就从失败测试重新教, + 不要从 solution 教。 +- **如何使用 `verify.py`:** 在工作区终端里跑,或让 Agent 作为任务 + 步骤跑——`python verify.py starter --expect-failure` 必须复现 + 列出的失败;`python verify.py solution` 必须通过。「完成」指 + 学习者做完有界改动后,starter 实现通过同一套测试。 +- **诚实规则:** 说清你没验证什么。不要声称代码达到生产可用。不要 + 承诺学习或职业结果。 + +## 本课程不覆盖什么 + +ZCode 的安装与套餐、Remote Control 的配对设置(仅提及)、模型供应商 +配置,以及任何推销内容。这些由配套仓库的指南与手册承载——给链接, +不即兴发挥。 + +## 徽章契约 + +- 徽章:**ZCode 协同复现徽章**(badge id `course-zcode`)——认领全部五个检查点获得。 +- 挑战:L01–L05 检查点,各 10 分;在 flypython.com 上集齐五个再加 50 分课程徽章奖励。 +- 证据:`python verify.py progress`——L03(有界改动)与 L04(验证与评审)由套件客观把关;L01/L02/L05 为学习者自我报告。 +- 提交:每个通过的检查点打印一个确定性的认领码;在 flypython.com 上记入你的账号。这是自我报告的证据,从来不是证书。 + +## 文件夹地图 + +``` +COURSE.md / COURSE_cn.md 本文件(EN / 中文) +lessons/L01.md … L05.md 课文(每课都有一个 _cn.md 对) +scenario/<skin>/ 每个皮肤的数据文件与 scenario.json +TASK.md / TASK_cn.md 改动必须满足的任务契约 +starter/report_tool.py 有意未完成的实现 +solution/report_tool.py 评审过的解答(第 3 课不要抄) +tests/test_report_tool.py 契约套件(只读) +verify.py 客观的通过/失败证据 +REVIEW.md 维护者实跑记录 +``` + +## 证据与许可 + +本课程文件夹属于已评审内容:`REVIEW.md` 记录最近一次实跑——日期、 +工具版本、观察到的偏差。文件夹内代码采用 MIT 许可;课文文字采用 +CC BY 4.0(见仓库 `LICENSE`)。发现教学漂移或课程不清楚之处,请 +通过仓库的 `course-feedback` issue 表单反馈。 diff --git a/courses/hands-on-with-zcode/REVIEW.md b/courses/hands-on-with-zcode/REVIEW.md new file mode 100644 index 0000000..439baa0 --- /dev/null +++ b/courses/hands-on-with-zcode/REVIEW.md @@ -0,0 +1,32 @@ +# Maintainer run-through record + +## 2026-09-13 — authored from official documentation (mechanical verification only) + +- Course authored against the ZCode documentation (zcode.z.ai/docs: + desktop-native workspace, Goal Mode objective management and recovery, + task lists, file references, remote control from a phone while the + desktop remains the runtime) and the shared `report-tool` core. + `TASK.md`, `tests/`, `scenario/`, `starter/`, `solution/` are + byte-identical to the Claude Code / Codex course cores — enforced by the + `core-group` check in `tools/verify_courses.py`. +- Environment for mechanical verification: macOS (arm64), Python 3.13. +- Commands and results: + - `python verify.py starter --expect-failure` — exit 0; expected failures + reproduced. + - `python verify.py solution` — exit 0; 9/9 tests pass. + - `python tools/verify_courses.py` — contract satisfied, core identical. +- **Not done:** any run inside ZCode. No ZCode install or account was + used; every ZCode behavior described in the lessons is sourced from + official documentation, not observed. + +## Pending before this course can be called taught-in-tool + +- One full agent-taught run-through inside ZCode ("start lesson 1" through + the Lesson 5 checkpoint) on a current build, recording observed + deviations from `COURSE.md` here — including how Goal Mode presented the + lesson checkpoints in practice. +- Whether the free tier lasts the whole course is unverified. + +## Deviation log + +(none yet — no live run has happened) diff --git a/courses/hands-on-with-zcode/TASK.md b/courses/hands-on-with-zcode/TASK.md new file mode 100644 index 0000000..e923183 --- /dev/null +++ b/courses/hands-on-with-zcode/TASK.md @@ -0,0 +1,37 @@ +# Task contract: scenario report tool (hands-on course core) + +Change only `starter/report_tool.py`. Standard library only; no new +dependencies. + +- `load_records(path) -> list[dict]`: + - `.csv` files load through `csv.DictReader`. + - `.json` files load as a list of objects; a non-list or non-object payload + raises `ValueError`. + - Any other suffix raises `ValueError` naming the unsupported file. +- `build_report(records, *, required_fields, numeric_field, group_field) -> dict`: + - Returns `{"total", "valid", "invalid", "groups", "errors"}`. + - Invalid rows never abort the run: each is collected in `errors` as + `{"index": <row position>, "reason": <short string>}`. Invalid means: + not a dict, missing or blank required field, or a non-numeric + `numeric_field` value (booleans do not count as numbers). + - Valid rows aggregate into `groups[group_value] = {"count": int, + "total": float}`; each group total is rounded to two decimals. + - `total = valid + invalid` must hold for every input. +- `write_report(report, destination)`: + - Writes JSON (UTF-8, indent 2, trailing newline) atomically: write a + sibling temp file, then `os.replace`. + - Creates missing parent directories. + - Never leaves a `.tmp` file behind on success. +- `run_scenario(scenario_dir) -> dict`: + - Reads `scenario.json` (`data_file`, `required_fields`, `numeric_field`, + `group_field`, `report_file`), processes the data file, writes the + report inside the scenario directory, and returns the report. +- `main(argv=None) -> int`: + - Exactly one argument (the scenario directory). Otherwise print usage to + stderr and return 2. + - On success print `total=... valid=... invalid=...` to stdout and return 0. + - On input failure print `error: ...` to stderr and return 1. + +Done means `python verify.py starter` exits 0 with all nine tests passing, +and `python verify.py starter --expect-failure` exits nonzero because the +starter no longer reproduces the unfinished state. diff --git a/courses/hands-on-with-zcode/TASK_cn.md b/courses/hands-on-with-zcode/TASK_cn.md new file mode 100644 index 0000000..7af2609 --- /dev/null +++ b/courses/hands-on-with-zcode/TASK_cn.md @@ -0,0 +1,34 @@ +# 任务契约:场景报表工具(实战课程核心) + +只修改 `starter/report_tool.py`。仅用标准库;不新增依赖。 + +- `load_records(path) -> list[dict]`: + - `.csv` 文件经 `csv.DictReader` 加载。 + - `.json` 文件按对象列表加载;不是列表或元素不是对象时抛出 + `ValueError`。 + - 其他后缀抛出 `ValueError`,并指明不支持的文件名。 +- `build_report(records, *, required_fields, numeric_field, group_field) -> dict`: + - 返回 `{"total", "valid", "invalid", "groups", "errors"}`。 + - 无效行绝不中止运行:每行以 `{"index": <行位置>, "reason": <简短 + 说明>}` 收集进 `errors`。无效指:不是字典、必填字段缺失或为空、 + `numeric_field` 不是数字(布尔值不算数字)。 + - 有效行聚合为 `groups[分组值] = {"count": int, "total": float}`;每个 + 分组总计保留两位小数。 + - 任何输入下都必须满足 `total = valid + invalid`。 +- `write_report(report, destination)`: + - 原子地写入 JSON(UTF-8、缩进 2、末尾换行):先写同名临时文件,再 + `os.replace`。 + - 自动创建缺失的父目录。 + - 成功后不留任何 `.tmp` 文件。 +- `run_scenario(scenario_dir) -> dict`: + - 读取 `scenario.json`(`data_file`、`required_fields`、 + `numeric_field`、`group_field`、`report_file`),处理数据文件,把 + 报告写进场景目录,并返回报告。 +- `main(argv=None) -> int`: + - 恰好一个参数(场景目录)。否则向 stderr 打印用法并返回 2。 + - 成功时向 stdout 打印 `total=... valid=... invalid=...` 并返回 0。 + - 输入失败时向 stderr 打印 `error: ...` 并返回 1。 + +完成的标准是 `python verify.py starter` 以 0 退出且九个测试全部通过, +同时 `python verify.py starter --expect-failure` 非零退出——因为 +starter 已不再复现未完成状态。 diff --git a/courses/hands-on-with-zcode/core-group b/courses/hands-on-with-zcode/core-group new file mode 100644 index 0000000..b56b0eb --- /dev/null +++ b/courses/hands-on-with-zcode/core-group @@ -0,0 +1 @@ +report-tool diff --git a/courses/hands-on-with-zcode/lessons/L01.md b/courses/hands-on-with-zcode/lessons/L01.md new file mode 100644 index 0000000..550cd8f --- /dev/null +++ b/courses/hands-on-with-zcode/lessons/L01.md @@ -0,0 +1,106 @@ +--- +id: course-zcode-l01 +type: course +title: "Lesson 1: Reproduce the failure inside the workspace" +summary: "Open the folder as a ZCode workspace, give the agent its first task, and make the failure objective with verify.py — inside the app, not beside it." +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "What this checkpoint tests" + body: "That you can reproduce the failure before fixing it: `python verify.py starter --expect-failure` must print the named failing tests. Whether you ran it in the workspace terminal or the agent ran it as a task step, the command is the evidence." + - title: "A workspace, not a chat" + body: "ZCode is a desktop-native workspace: files, tasks, planning, execution, verification, and Git state in one place. Use file references to point the agent at exact files instead of describing them." + - title: "When you are done" + body: "You can say which nine behaviors the suite proves and why the starter cannot fake them. `python verify.py progress` then shows this checkpoint's claim code." +--- + +# Lesson 1: Reproduce the failure inside the workspace + +## Objective + +By the end of this lesson you have the course folder open as a ZCode +workspace, you have run the course's objective verification command, and +you can explain what "the starter fails" means in test names. No code is +changed in this lesson. + +## Why this lesson exists + +Most AI-written Python dies the same way: the demo input works, the real +input arrives, and the script crashes on row 3 — or worse, quietly +produces a wrong number. The fix does not start with a better prompt. It +starts with turning "it's broken" into a command that anyone can run and +get the same answer. ZCode keeps the whole job in one workspace — files, +tasks, execution, verification, and Git state side by side — and file +references let you point the agent at exact files instead of describing +them in prose. + +## Warm-up (2 minutes) + +Open `starter/report_tool.py` in the workspace and read it top to bottom. +It looks reasonable: functions with docstrings, type hints, a CLI. This +is exactly what AI-generated code looks like — clean surface, missing +behavior. Now open `TASK.md` and keep it next to you. + +## The lesson + +Open the course folder as a ZCode workspace, then give the agent its first +task — referencing the files directly: + +**"Read COURSE.md — it is the teaching contract for this folder. Then read +TASK.md and starter/report_tool.py. Do not change any file. Tell me which +behaviors the tests expect that the starter does not have."** + +While it reads, run the objective verification — in the workspace +terminal, or let the agent run it as a task step: + +```bash +python verify.py starter --expect-failure +``` + +You should see the starter reproduce four real-world failure classes, +mapped to test names: + +| Failing test | Real-world meaning | +| --- | --- | +| `test_load_json_records_returns_list_of_dicts` | the tool only handles CSV; the API scenario crashes | +| `test_unsupported_suffix_raises_value_error` | an `.xlsx` upload becomes a confusing crash instead of a clear rejection | +| `test_invalid_records_are_isolated_with_reasons` | one empty cell aborts the entire run — no partial report, no reason | +| `test_group_totals_are_rounded_to_two_decimals` | `0.1 + 0.2` prints as `0.30000000000000004` in your report | +| `test_write_report_creates_missing_parent_directories` | the tool cannot write into a fresh output directory | +| `test_run_scenario_writes_report_file`, `test_main_prints_summary_and_returns_zero` | end-to-end consequences of the above | + +Then run the reviewed implementation the same way: + +```bash +python verify.py solution +``` + +Both commands exit 0 — but they mean opposite things. The first proves the +starter is *correctly unfinished*; the second proves the target is +reachable. That pair is your completion evidence for the whole course. + +## Exercise + +Pick the scenario skin you most identify with (`excel-report`, +`data-monitor`, or `api-tool`), open its data file, and find the rows that +will be rejected. Write down, before running anything, which test each bad +row will trigger. Then check yourself against the table above — and ask +the agent to confirm, not to fix. + +## Checkpoint + +Run `python verify.py starter --expect-failure` and +`python verify.py solution`. You pass this lesson when you can answer, +without looking: + +1. Which four behaviors separate starter from solution? +2. Why is "expected failure" a success condition, not a contradiction? +3. What does the row you found in the exercise do to the starter — crash, + isolate, or silently pass? + +## Expected evidence + +The transcript of both commands, and your three answers. Keep them; +Lesson 4 will ask for them again. diff --git a/courses/hands-on-with-zcode/lessons/L01_cn.md b/courses/hands-on-with-zcode/lessons/L01_cn.md new file mode 100644 index 0000000..05b1d69 --- /dev/null +++ b/courses/hands-on-with-zcode/lessons/L01_cn.md @@ -0,0 +1,99 @@ +--- +id: course-zcode-l01 +type: course +title: "第 1 课:在工作区里复现故障" +summary: "把文件夹作为 ZCode 工作区打开,给 Agent 第一个任务,用 verify.py 让失败变得客观——在应用内完成,而不是在应用旁边。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "本检查点考察什么" + body: "先复现失败再动手修:`python verify.py starter --expect-failure` 必须打印出点名的失败测试。你在工作区终端跑也好、Agent 作为任务步骤跑也好,命令就是证据。" + - title: "工作区,不是聊天框" + body: "ZCode 是桌面原生工作区:文件、任务、规划、执行、验证和 Git 状态同在一处。用文件引用把 Agent 精确指到文件,不要用文字描述。" + - title: "完成的标志" + body: "你能说出套件证明了哪九种行为、为什么 starter 伪造不出来。`python verify.py progress` 随即显示本检查点的认领码。" +--- + +# 第 1 课:在工作区里复现故障 + +## 目标 + +本课结束时,你把课程文件夹作为 ZCode 工作区打开,运行过课程的 +客观验证命令,并能解释「starter 失败」在测试名里的含义。本课 +不改任何代码。 + +## 为什么有这节课 + +AI 写的 Python 大多是同一种死法:演示输入能跑,真实输入一到,脚本 +在第 3 行崩掉——或者更糟,悄悄算出一个错的数。修复不从更好的 +提示词开始,而从把「坏了」变成一条任何人都能跑、且得到相同答案的 +命令开始。ZCode 把整个任务装进一个工作区——文件、任务、执行、 +验证、Git 状态并排——文件引用让你把 Agent 精确指到文件,而不是 +用散文描述。 + +## 热身(2 分钟) + +在工作区里打开 `starter/report_tool.py`,从头读到尾。它看起来很 +合理:带 docstring 的函数、类型标注、CLI。这正是 AI 生成代码的 +样子——表面干净、行为缺失。现在打开 `TASK.md`,放在手边。 + +## 本课内容 + +把课程文件夹作为 ZCode 工作区打开,然后给 Agent 第一个任务—— +直接引用文件: + +**“读 COURSE.md——它是本文件夹的教学契约。再读 TASK.md 和 +starter/report_tool.py。不要改任何文件。告诉我测试期望哪些行为、 +starter 缺了哪些。”** + +趁它读的时候,跑客观验证——在工作区终端里,或让 Agent 作为任务 +步骤跑: + +```bash +python verify.py starter --expect-failure +``` + +你会看到 starter 复现出四类真实世界故障,对应到测试名: + +| 失败的测试 | 真实含义 | +| --- | --- | +| `test_load_json_records_returns_list_of_dicts` | 工具只处理 CSV;API 场景直接崩 | +| `test_unsupported_suffix_raises_value_error` | 一个 `.xlsx` 上传变成莫名其妙的崩溃,而不是清楚的拒绝 | +| `test_invalid_records_are_isolated_with_reasons` | 一个空单元格中止整个运行——没有部分报告,没有原因 | +| `test_group_totals_are_rounded_to_two_decimals` | `0.1 + 0.2` 在报告里打印成 `0.30000000000000004` | +| `test_write_report_creates_missing_parent_directories` | 工具无法写进新建的输出目录 | +| `test_run_scenario_writes_report_file`、`test_main_prints_summary_and_returns_zero` | 上述问题的端到端后果 | + +然后用同样的方式跑评审过的实现: + +```bash +python verify.py solution +``` + +两条命令都以 0 退出——但含义相反。第一条证明 starter *确实处于 +正确的未完成态*;第二条证明目标可达。这一对命令是整门课的完成 +证据。 + +## 练习 + +挑一个你最有代入感的场景皮肤(`excel-report`、`data-monitor` 或 +`api-tool`),打开它的数据文件,找出会被拒绝的行。先不看运行 +结果,写下每个坏行会触发哪个测试,再对照上面的表自查——并让 +Agent 确认,而不是让它修。 + +## 检查点 + +运行 `python verify.py starter --expect-failure` 和 +`python verify.py solution`。不看材料能回答下面三个问题即算通过: + +1. 哪四个行为把 starter 和 solution 区分开? +2. 为什么「预期失败」是成功条件而不是自相矛盾? +3. 你在练习里找到的那一行会让 starter 崩溃、被隔离,还是悄悄 + 通过? + +## 预期证据 + +两条命令的完整输出记录,加你的三个答案。保留好——第 4 课还会 +用到。 diff --git a/courses/hands-on-with-zcode/lessons/L02.md b/courses/hands-on-with-zcode/lessons/L02.md new file mode 100644 index 0000000..98b4a49 --- /dev/null +++ b/courses/hands-on-with-zcode/lessons/L02.md @@ -0,0 +1,88 @@ +--- +id: course-zcode-l02 +type: course +title: "Lesson 2: Write the task contract, let Goal Mode hold the plan" +summary: 'Turn "make it better" into a bounded, testable contract — then let Goal Mode decompose it into tasks you can watch land one by one.' +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "What this checkpoint tests" + body: "That you can read `TASK.md` as testable statements, not prose. Every sentence that starts with a function name is a contract line the suite can assert." + - title: "Goal Mode is the objective manager" + body: "ZCode's Goal Mode decomposes an objective into a task list the agent works through — and manages recovery when a step stalls. The contract you write is what it decomposes; vague in, vague out." + - title: "The testability check" + body: "If you cannot tell whether a statement is testable, ask: could a suite assert it without reading your mind? Numbers, exit codes, files on disk — never vibes." +--- + +# Lesson 2: Write the task contract, let Goal Mode hold the plan + +## Objective + +You can read `TASK.md` as a set of testable statements, trace each +statement to a test in `tests/test_report_tool.py`, and watch a goal +decompose into tasks before any file is touched. + +## Why this lesson exists + +Vague requests produce vague code. "Handle bad rows better" gives an agent +permission to guess; "invalid rows are collected in `errors` with index +and reason, valid rows still produce a report" gives it a target and +gives you a way to check. In ZCode the decomposition is a first-class +surface: Goal Mode turns your objective into a task list and manages +recovery when a step stalls. The contract you write is what it decomposes +— a precise contract becomes a precise task list. + +## The lesson + +Open `TASK.md`. Notice what every line has in common: it names an +observable behavior, not an implementation. Four statements from the +contract, and the tests that pin them: + +| Contract line | Test | +| --- | --- | +| "a JSON file loads into the same record list as CSV" | `test_load_json_records_returns_list_of_dicts` | +| "invalid rows land in `errors` with index and reason; valid rows still aggregate" | `test_invalid_records_are_isolated_with_reasons` | +| "group totals are rounded to two decimals" | `test_group_totals_are_rounded_to_two_decimals` | +| "the report write is atomic and creates missing parents" | `test_write_report_creates_missing_parent_directories` | + +Now set the goal. Give the agent an objective that is already decomposed +by the contract: + +**"Goal: make starter/report_tool.py satisfy TASK.md. Work one failing +test group at a time — JSON loading, then validation isolation, then +rounding, then atomic writes, then the end-to-end tests — running +`python verify.py starter` after each task. Only starter/report_tool.py +may change; tests/, solution/, scenario/ are read-only; standard library +only."** + +Read the task list Goal Mode produces before letting it execute. Each +task should name a contract line and a verification step — if a task says +"improve error handling," tighten the goal; that is not a contract line. +The task list is the deliverable of this lesson: it is your contract, +executable. + +## Exercise + +Write one contract line for a script you actually own, using the same +shape: inputs, outputs, error cases, and "done means `<command>` exits +0". Then write the goal statement you would give ZCode for it — with the +same boundaries this lesson used. + +## Checkpoint + +Run `python verify.py progress` — this checkpoint's claim code prints +when you can answer: + +1. Which `TASK.md` line does + `test_invalid_records_are_isolated_with_reasons` pin, in your own + words? +2. What does Goal Mode do with your objective — and what does it do when + a step stalls? +3. Why should the goal statement carry the boundaries, not just the task? + +## Expected evidence + +Your drafted contract line, your goal statement, and the task list it +produced (or the version you tightened and why). diff --git a/courses/hands-on-with-zcode/lessons/L02_cn.md b/courses/hands-on-with-zcode/lessons/L02_cn.md new file mode 100644 index 0000000..03f5280 --- /dev/null +++ b/courses/hands-on-with-zcode/lessons/L02_cn.md @@ -0,0 +1,79 @@ +--- +id: course-zcode-l02 +type: course +title: "第 2 课:写任务契约,让 Goal 模式承载计划" +summary: "把「改好一点」变成有边界、可测试的契约——再让 Goal 模式把它拆成你能看着逐一落地的任务。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "本检查点考察什么" + body: "你能把 `TASK.md` 当成可测试的陈述来读,而不是散文。每个以函数名开头的句子都是套件能断言的契约行。" + - title: "Goal 模式是目标管理器" + body: "ZCode 的 Goal 模式把目标拆成 Agent 逐项执行的任务列表,并在某步卡住时管理恢复。它拆的是你写的契约——含糊进去,含糊出来。" + - title: "可测试性检查" + body: "判断一句话能不能测:测试套件不看你的心思能不能断言它?数字、退出码、磁盘上的文件——不看感觉。" +--- + +# 第 2 课:写任务契约,让 Goal 模式承载计划 + +## 目标 + +你能把 `TASK.md` 当成一组可测试陈述来读,把每条陈述追查到 +`tests/test_report_tool.py` 里的一个测试,并在任何文件被碰之前, +看着一个目标被拆成任务列表。 + +## 为什么有这节课 + +含糊的需求产出含糊的代码。「把坏行处理好一点」是许可 Agent 去猜; +「无效行收进 `errors`、带下标和原因,有效行照常产出报告」是给它 +目标、也给你检查的方法。在 ZCode 里,拆解是一等公民:Goal 模式 +把你的目标变成任务列表,并在某步卡住时管理恢复。它拆的是你写的 +契约——精确的契约变成精确的任务列表。 + +## 本课内容 + +打开 `TASK.md`。注意每行的共同点:它描述的是可观察的行为,不是 +实现。契约里的四句陈述,以及钉住它们的测试: + +| 契约行 | 测试 | +| --- | --- | +| 「JSON 文件要加载成与 CSV 相同的记录列表」 | `test_load_json_records_returns_list_of_dicts` | +| 「无效行带下标和原因进 `errors`;有效行照常聚合」 | `test_invalid_records_are_isolated_with_reasons` | +| 「分组总计保留两位小数」 | `test_group_totals_are_rounded_to_two_decimals` | +| 「报告写出是原子的,且会创建缺失的父目录」 | `test_write_report_creates_missing_parent_directories` | + +现在定目标。给 Agent 一个已经按契约拆好的目标: + +**“目标:让 starter/report_tool.py 满足 TASK.md。一次只修一组 +失败测试——先 JSON 加载,再无效行隔离,再舍入,再原子写,最后 +是端到端测试——每个任务后跑 `python verify.py starter`。只许改 +starter/report_tool.py;tests/、solution/、scenario/ 只读;只用 +标准库。”** + +放行执行之前,先读 Goal 模式产出的任务列表。每个任务都应点名 +一条契约行和一个验证步骤——哪个任务写的是「改进错误处理」,就 +收紧目标;那不是契约行。任务列表就是本课的交付物:它是你的契约 +的可执行形态。 + +## 练习 + +为你真正拥有的一个脚本写一条契约行,用同样的形状:输入、输出、 +错误情况、以及「完成 = `<命令>` 退出码为 0」。再写你会给 ZCode +的目标陈述——带上本课用过的同样边界。 + +## 检查点 + +运行 `python verify.py progress`——能回答下面三题时,本检查点的 +认领码就会显示: + +1. 用你自己的话说,`test_invalid_records_are_isolated_with_reasons` + 钉住的是 `TASK.md` 哪一行? +2. Goal 模式拿你的目标做什么——某步卡住时它又做什么? +3. 为什么边界要写进目标陈述,而不只写进任务? + +## 预期证据 + +你起草的契约行、你的目标陈述、它产出的任务列表(或你收紧过的 +版本及理由)。 diff --git a/courses/hands-on-with-zcode/lessons/L03.md b/courses/hands-on-with-zcode/lessons/L03.md new file mode 100644 index 0000000..945f597 --- /dev/null +++ b/courses/hands-on-with-zcode/lessons/L03.md @@ -0,0 +1,82 @@ +--- +id: course-zcode-l03 +type: course +title: "Lesson 3: The bounded change, watched task by task" +summary: "Drive the starter to green through the goal's task list — and use the workspace Git state to review every change that lands." +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "What this checkpoint tests" + body: "That the starter passes all nine tests through changes you supervised — `python verify.py progress` shows this code only once the starter suite is green." + - title: "Git state is the boundary" + body: "The workspace tracks Git state alongside execution — after each task, the working tree shows exactly what changed. A task that touched the wrong file is visible immediately; recover the goal rather than stacking fixes." + - title: "When you are done" + body: "Every change maps to a contract line, the Git state shows nothing outside starter/report_tool.py moved, and you can explain each edit without opening solution/." +--- + +# Lesson 3: The bounded change, watched task by task + +## Objective + +The starter passes all nine tests through a sequence of bounded changes, +the workspace Git state proves nothing else moved, and you can explain +every edit without reading the solution first. + +## Why this lesson exists + +This is the lesson people skip — and the one that builds the actual skill. +Letting an agent rewrite the file in one shot teaches you nothing and +leaves you a change you cannot honestly review. Supervising small changes, +each pinned to a contract line, is how the code stays yours. ZCode keeps +the Git state next to the execution — after each task you can see exactly +what moved, which makes "did it stay in bounds" a look, not a guess. + +## The lesson + +With the goal and task list from Lesson 2 approved, let execution begin — +one task at a time, with `python verify.py starter` between tasks as the +plan says. + +Hold it to the contract's boundaries: + +- Only `starter/report_tool.py` changes. Check the Git state after every + task: if `tests/`, `solution/`, or `scenario/` moved, stop the goal and + use its recovery to back up rather than stacking corrections. +- No new imports outside the standard library. +- Each task should move toward one contract line. Reject drive-by + refactors ("while I was here I renamed…"). +- If the agent wants to change a test, the answer is no. Tests are the + contract; the code moves. + +Review deliberately between tasks. The Git state shows the diff; read the +hunks, not just the task's self-report. A change you cannot explain is a +change you recover away from — Goal Mode's job is managing exactly that: +restart the step with a tighter instruction instead of piling fixes onto +a bad state. + +Expect the failing count to drop task by task: 7 → 5 → 4 → 3 → 2 → 0. + +## Exercise + +Pick one change the agent made — ideally the validation-isolation step — +and find it in the Git diff. Explain it back: "this hunk does X; that +satisfies contract line Y." If you cannot, have the agent walk you through +that hunk before the next task runs. + +## Checkpoint + +Run `python verify.py progress`. This checkpoint's code appears only when +the starter suite is green. You pass the lesson when you can answer: + +1. How many tasks did the bounded change take, and what made each task + bounded? +2. Which change did you reject or recover from, and why? +3. Point at the hunk that implements "isolate invalid rows with reasons" + — where is it in the diff? + +## Expected evidence + +A green `python verify.py starter` run, a Git diff confined to +`starter/report_tool.py`, and your hunk-by-hunk explanation of it. diff --git a/courses/hands-on-with-zcode/lessons/L03_cn.md b/courses/hands-on-with-zcode/lessons/L03_cn.md new file mode 100644 index 0000000..39cc0b1 --- /dev/null +++ b/courses/hands-on-with-zcode/lessons/L03_cn.md @@ -0,0 +1,73 @@ +--- +id: course-zcode-l03 +type: course +title: "第 3 课:有界改动,逐任务盯守" +summary: "沿着目标的任务列表把 starter 推到全绿——用工作区 Git 状态审每一处落地的改动。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "本检查点考察什么" + body: "starter 通过你监督的改动让九个测试全绿——`python verify.py progress` 只在 starter 套件变绿后显示本检查点的码。" + - title: "Git 状态就是边界" + body: "工作区把 Git 状态放在执行旁边——每个任务完成后,工作区立刻显示改了什么。动错文件的任务一眼可见;用目标恢复倒回去,而不是叠补丁。" + - title: "完成的标志" + body: "每处改动都能对上一条契约行,Git 状态显示 starter/report_tool.py 之外没动过任何东西,而且不看 solution/ 你也讲得清每处编辑。" +--- + +# 第 3 课:有界改动,逐任务盯守 + +## 目标 + +starter 通过一串有界改动让九个测试全绿,工作区 Git 状态证明没有 +动过别的东西,并且不看 solution,你也讲得清每一处编辑。 + +## 为什么有这节课 + +这节课最容易被跳过——也恰恰是长本事的那节。放任 Agent 一次重写 +整个文件,什么也学不到,只留下一份你没底气审查的改动。监督一连串 +小改动、各自钉在一条契约行上,才能让 AI 写的代码仍然是你的。 +ZCode 把 Git 状态放在执行旁边——每个任务之后你都能直接看到动了 +什么,「有没有越界」变成看一眼,而不是猜。 + +## 本课内容 + +第 2 课的目标与任务列表批准后,开始执行——一次一个任务,任务 +之间按计划跑 `python verify.py starter`。 + +守住契约边界: + +- 只动 `starter/report_tool.py`。每个任务后看 Git 状态:`tests/`、 + `solution/`、`scenario/` 动过就停掉目标,用它的恢复倒回去, + 而不是往上叠修正。 +- 标准库之外不加 import。 +- 每个任务朝一条契约行推进。顺手重构(「我顺便重命名了……」) + 一律拒绝。 +- Agent 想改测试,答案是不行。测试是契约;动的是代码。 + +任务之间认真审。Git 状态给出 diff;读 hunk,不只读任务的自我 +汇报。讲不清的改动就用恢复倒回去——Goal 模式管的正是这件事: +用更紧的指令重启这一步,而不是在坏状态上叠修复。 + +预期失败数逐任务下降:7 → 5 → 4 → 3 → 2 → 0。 + +## 练习 + +挑 Agent 的一处改动——最好是「无效行隔离」那步——在 Git diff +里找到它,讲回来:「这个 hunk 做了 X,满足契约行 Y」。讲不清就 +让它走读那个 hunk,再跑下一个任务。 + +## 检查点 + +运行 `python verify.py progress`。本检查点的码只在 starter 套件 +全绿时出现。不看材料能回答即算通过: + +1. 这次有界改动走了几个任务,是什么让每个任务保持有界? +2. 你拒绝或恢复掉过哪处改动,为什么? +3. 指出实现「无效行带原因隔离」的那个 hunk——它在 diff 的哪里? + +## 预期证据 + +一次全绿的 `python verify.py starter` 运行、一份只含 +`starter/report_tool.py` 的 Git diff,和你的逐块讲解。 diff --git a/courses/hands-on-with-zcode/lessons/L04.md b/courses/hands-on-with-zcode/lessons/L04.md new file mode 100644 index 0000000..a0ad8ef --- /dev/null +++ b/courses/hands-on-with-zcode/lessons/L04.md @@ -0,0 +1,82 @@ +--- +id: course-zcode-l04 +type: course +title: "Lesson 4: Verify like a reviewer, not a spectator" +summary: "Green tests are the floor, not the ceiling — run both suites, walk the scenario skins, and review the accumulated Git diff the way you would review a colleague's." +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "What this checkpoint tests" + body: "That starter AND solution both pass — `verify.py progress` prints this code only when both suites are green. The gate is the pair, not a single run." + - title: "Review the delta, not the vibes" + body: "The workspace Git state shows the whole accumulated change in one place — even from your phone via remote control. Read it as a reviewer: every hunk maps to a contract line, nothing extra moved, no test weakened." + - title: "When you are done" + body: "You have written three evidence lines — verified / not verified / known limits — and could defend the change in a code review." +--- + +# Lesson 4: Verify like a reviewer, not a spectator + +## Objective + +Both suites pass, and you hold a written record of what the green runs do +and do not prove — plus a reviewed diff you would defend to a colleague. + +## Why this lesson exists + +"All tests pass" is where most AI-assisted work stops — and where most +silent bugs start. A suite proves the contract lines it asserts; it says +nothing about the rows nobody wrote a test for. Verification is a report +you write, not a feeling you have. ZCode keeps verification next to the +work: the same workspace shows the Git state, the task list, and the +terminal output — and remote control lets you check status or send an +instruction from your phone while the desktop keeps running the work. + +## The lesson + +Run the full pair in the workspace terminal: + +```bash +python verify.py starter # the implementation you supervised +python verify.py solution # the reviewed reference +``` + +Both green means the contract is satisfied *as written*. Now do the +review the suite cannot do for you: + +1. **Walk a skin end-to-end.** Pick a `scenario/<skin>/` folder and run + the tool on it: `python starter/report_tool.py scenario/<skin>`. Read + the produced report against the input file — recompute one group + total by hand. +2. **Review the accumulated diff.** The workspace Git state shows every + hunk that landed across the goal. Read it top to bottom. Every hunk + should map to a contract line you named in Lesson 2. Anything that + does not is scope creep — revert or justify it. +3. **Check the edges the tests skip.** Feed the tool an empty CSV, a CSV + with only invalid rows, a missing file. The contract does not pin + these — your judgment does. Write down what it does. + +## Exercise + +Write the three-line evidence record for this change: + +- **Verified:** the commands you ran and what they proved. +- **Not verified:** the inputs and behaviors no test pins. +- **Known limits:** anything you saw that the contract never promised. + +Keep it honest — "not verified" is a list of facts, not a confession. + +## Checkpoint + +Run `python verify.py progress`. This checkpoint's code prints only when +both suites pass. You pass the lesson when you can answer: + +1. What did your by-hand recompute of a group total show? +2. Name one input the suite does not pin and what the tool does with it. +3. Which hunk, if any, would you ask a colleague to justify? + +## Expected evidence + +Both green runs, your three-line evidence record, and the diff review +notes. diff --git a/courses/hands-on-with-zcode/lessons/L04_cn.md b/courses/hands-on-with-zcode/lessons/L04_cn.md new file mode 100644 index 0000000..901ef3c --- /dev/null +++ b/courses/hands-on-with-zcode/lessons/L04_cn.md @@ -0,0 +1,76 @@ +--- +id: course-zcode-l04 +type: course +title: "第 4 课:像评审者一样验证,别当观众" +summary: "测试全绿是地板不是天花板——两个套件都跑、三个皮肤都走一遍、按评审同事的标准审这份累积 Git diff。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "本检查点考察什么" + body: "starter 和 solution 双双通过——`verify.py progress` 只在两个套件都绿时打印本检查点的码。门槛是一对结果,不是一次运行。" + - title: "审增量,不凭感觉" + body: "工作区 Git 状态把整份累积改动放在一处——通过远程控制甚至能在手机上看。按评审者的方式读:每个 hunk 对上一条契约行、没有多余挪动、没有测试被放松。" + - title: "完成的标志" + body: "你已写下三行证据——已验证 / 未验证 / 已知局限——并且能在代码评审里为这次改动辩护。" +--- + +# 第 4 课:像评审者一样验证,别当观众 + +## 目标 + +两个套件都通过,你手握一份书面记录:说清绿色运行证明了什么、没 +证明什么——外加一份你敢向同事辩护的 diff。 + +## 为什么有这节课 + +「测试全过」是大多数 AI 辅助工作停下的地方——也是大多数隐性 +bug 开始的地方。套件只证明它断言过的契约行;没人写过测试的行, +它一概不担保。验证是你写的一份报告,不是一种感觉。ZCode 把验证 +放在工作旁边:同一个工作区里有 Git 状态、任务列表和终端输出—— +远程控制还能让你在手机上查状态、发指令,桌面端继续跑活。 + +## 本课内容 + +在工作区终端里跑完整的一对命令: + +```bash +python verify.py starter # 你监督出来的实现 +python verify.py solution # 评审过的参考实现 +``` + +双全绿意味着契约*按字面*被满足。现在做套件替不了你的评审: + +1. **走完一个皮肤。** 挑一个 `scenario/<skin>/` 文件夹,用工具跑 + 它:`python starter/report_tool.py scenario/<skin>`。对照输入 + 文件读产出的报告——亲手重算一个分组总计。 +2. **审累积的 diff。** 工作区 Git 状态展示整个目标落地的每个 + hunk。从头读到尾。每个 hunk 都应能对上你在第 2 课点名的契约 + 行。对不上的就是范围蔓延——回退或让它解释。 +3. **检查测试没钉的边界。** 喂给工具一个空 CSV、一个全是无效行 + 的 CSV、一个不存在的文件。契约没钉这些——你的判断钉。记下 + 它的行为。 + +## 练习 + +为这次改动写三行证据记录: + +- **已验证:** 你跑过的命令和它们证明了什么。 +- **未验证:** 没有测试钉住的输入与行为。 +- **已知局限:** 你看到的、契约从未承诺过的东西。 + +写得诚实——「未验证」是一份事实清单,不是忏悔书。 + +## 检查点 + +运行 `python verify.py progress`。本检查点的码只在两个套件都过 +时打印。不看材料能回答即算通过: + +1. 你手工重算的分组总计结果如何? +2. 说出一个套件没钉的输入,以及工具对它的实际行为。 +3. 哪个 hunk(如果有)你会请同事来解释? + +## 预期证据 + +两次全绿运行、你的三行证据记录、diff 评审笔记。 diff --git a/courses/hands-on-with-zcode/lessons/L05.md b/courses/hands-on-with-zcode/lessons/L05.md new file mode 100644 index 0000000..b580b14 --- /dev/null +++ b/courses/hands-on-with-zcode/lessons/L05.md @@ -0,0 +1,80 @@ +--- +id: course-zcode-l05 +type: course +title: "Lesson 5: Take the workflow to your own project" +summary: "The pattern generalizes: contract as goal, task list, bounded execution, Git-state review — run the same loop on a project you actually own." +lang: en-US +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "What this checkpoint tests" + body: "Self-attested: you confirm you applied the loop to your own project. The evidence is your contract, your task list, your reviewed diff — not a code we can check." + - title: "Carry the shape, not the code" + body: "What transfers is the loop: contract first, goal decomposed into checkable tasks, boundaries watched in Git state, verification written down. Long tasks can even be monitored remotely — the desktop remains the runtime." + - title: "When you are done" + body: "Your own project has a contract, one bounded change you can explain, and a verified/not-verified/known-limits record — or a documented reason the loop did not fit." +--- + +# Lesson 5: Take the workflow to your own project + +## Objective + +You apply the whole loop — contract, goal, task list, bounded execution, +Git-state review, written verification — to a script or project you +actually own, and record what happened. + +## Why this lesson exists + +A course folder is a safe sandbox; your own codebase is not. The point of +the last four lessons was never the report tool — it was a repeatable way +to drive an agent that keeps you in charge of what ships. This checkpoint +exists to prove the transfer happened. It is self-attested: only you know +whether the loop actually ran. + +## The lesson + +Pick a real script you own — one with a bug you have been putting off, or +a behavior you want but have not written. Then run the same five moves: + +1. **Contract.** Write the task contract first: inputs, outputs, error + cases, and "done means `<command>` exits 0". One short paragraph, every + sentence testable. +2. **Goal.** Open the project as a ZCode workspace and give the agent a + goal built from that contract — including the boundaries: which files + may change, which are read-only, the dependency policy. +3. **Bounded execution.** Read the task list before it runs; check the + Git state after each task; recover instead of stacking fixes. +4. **Verify.** Run the verification command, then do the review the suite + cannot do — walk one real input end-to-end and check the output by + hand. +5. **Record.** Write the three lines: verified / not verified / known + limits. + +On long tasks, remote control lets you read status and send instructions +from your phone while the desktop stays the runtime — useful for checking +in, not a substitute for reviewing the diff when it lands. + +If the loop fought you — the task was too small to need a contract, or too +vague to test — that is a finding too. Write down where the shape did not +fit; that judgment is the actual skill. + +## Exercise + +If your project has more than one boundary worth pinning, draft the next +contract line now — you will want it when you set the next goal. + +## Checkpoint + +This checkpoint is self-attested: claim it when you can answer: + +1. What was your contract's "done means" command? +2. Which task did you push back on or recover? +3. What is on your "not verified" list that a stranger would assume was + covered? + +## Expected evidence + +Your contract paragraph, the goal statement you set, and the three-line +verification record. These are yours — they are the proof the workflow +transferred. diff --git a/courses/hands-on-with-zcode/lessons/L05_cn.md b/courses/hands-on-with-zcode/lessons/L05_cn.md new file mode 100644 index 0000000..6fbe258 --- /dev/null +++ b/courses/hands-on-with-zcode/lessons/L05_cn.md @@ -0,0 +1,71 @@ +--- +id: course-zcode-l05 +type: course +title: "第 5 课:把工作流带回自己的项目" +summary: "模式会泛化:契约变目标、任务列表、有界执行、Git 状态审查——在你真正拥有的项目上跑同一套闭环。" +lang: zh-CN +content_version: 1 +status: reviewed +reviewed_on: 2026-09-13 +hints: + - title: "本检查点考察什么" + body: "自我报告:你确认把这套闭环用到了自己的项目上。证据是你的契约、你的任务列表、你审过的 diff——不是我们查得了的码。" + - title: "带走形状,不是代码" + body: "能迁移的是闭环:契约先行、目标拆成可检查的任务、边界在 Git 状态里盯守、验证写下来。长任务甚至可以远程监控——桌面端仍是运行时。" + - title: "完成的标志" + body: "你自己的项目有了一份契约、一处你讲得清的有界改动、一份已验证/未验证/已知局限记录——或一条写清楚的「闭环不合身」的原因。" +--- + +# 第 5 课:把工作流带回自己的项目 + +## 目标 + +你把整套闭环——契约、目标、任务列表、有界执行、Git 状态审查、 +书面验证——用到自己真正拥有的脚本或项目上,并记录发生了什么。 + +## 为什么有这节课 + +课程文件夹是安全沙箱;你自己的代码库不是。前四课的意义从来不是 +那个报表工具——而是一种可复用的、让你始终掌控产出的 Agent 驱动 +方式。本检查点要证明迁移真的发生了。它是自我报告的:闭环跑没跑 +过只有你知道。 + +## 本课内容 + +挑一个你真正拥有的脚本——一个你拖了很久的 bug,或一个想要但 +还没写的行为。然后走同样的五步: + +1. **契约。** 先写任务契约:输入、输出、错误情况、以及「完成 = + `<命令>` 退出码为 0」。一小段,每句都可测试。 +2. **目标。** 把项目作为 ZCode 工作区打开,给 Agent 一个由这份 + 契约搭出的目标——包括边界:哪些文件可改、哪些只读、依赖 + 政策。 +3. **有界执行。** 任务列表跑之前先读;每个任务后看 Git 状态; + 用恢复代替叠修复。 +4. **验证。** 跑验证命令,然后做套件做不了的评审——拿一个真实 + 输入端到端走一遍,手工核对输出。 +5. **记录。** 写三行:已验证 / 未验证 / 已知局限。 + +长任务上,远程控制能让你在手机上读状态、发指令,桌面端仍是运行 +时——适合查岗,不能替代 diff 落地时的审查。 + +如果闭环跟你打架——任务小到不需要契约,或模糊到不可测——那也 +是发现。写下形状不合身的地方;这个判断本身就是真本事。 + +## 练习 + +如果你的项目还有不止一处值得钉住的边界,现在就起草下一条契约行 +——定下一个目标时你会用得上。 + +## 检查点 + +本检查点自我报告:能回答下面三题时即可认领: + +1. 你的契约里「完成 =」命令是什么? +2. 你在哪个任务上顶回去或恢复过? +3. 你的「未验证」清单上有什么是陌生人会以为已覆盖的? + +## 预期证据 + +你的契约段落、你设的目标陈述、三行验证记录。它们是你自己的—— +是工作流完成迁移的证明。 diff --git a/courses/hands-on-with-zcode/scenario/api-tool/scenario.json b/courses/hands-on-with-zcode/scenario/api-tool/scenario.json new file mode 100644 index 0000000..e17e7c2 --- /dev/null +++ b/courses/hands-on-with-zcode/scenario/api-tool/scenario.json @@ -0,0 +1,8 @@ +{ + "skin": "api-tool", + "data_file": "tickets.json", + "required_fields": ["ticket_id", "priority", "minutes"], + "numeric_field": "minutes", + "group_field": "priority", + "report_file": "report.json" +} diff --git a/courses/hands-on-with-zcode/scenario/api-tool/tickets.json b/courses/hands-on-with-zcode/scenario/api-tool/tickets.json new file mode 100644 index 0000000..03e35b7 --- /dev/null +++ b/courses/hands-on-with-zcode/scenario/api-tool/tickets.json @@ -0,0 +1,9 @@ +[ + {"ticket_id": "T-101", "priority": "high", "minutes": "45"}, + {"ticket_id": "T-102", "priority": "normal", "minutes": "20"}, + {"ticket_id": "T-103", "priority": "low", "minutes": "10"}, + {"ticket_id": "T-104", "priority": "high", "minutes": "90"}, + {"ticket_id": "T-105", "priority": "", "minutes": "30"}, + {"ticket_id": "T-106", "priority": "normal", "minutes": "n/a"}, + {"ticket_id": "T-107", "priority": "low", "minutes": "25"} +] diff --git a/courses/hands-on-with-zcode/scenario/data-monitor/metrics.csv b/courses/hands-on-with-zcode/scenario/data-monitor/metrics.csv new file mode 100644 index 0000000..15a5a15 --- /dev/null +++ b/courses/hands-on-with-zcode/scenario/data-monitor/metrics.csv @@ -0,0 +1,8 @@ +host,service,latency_ms +web-1,api,182 +web-2,api,205 +db-1,postgres,44 +web-3,frontend,96 +db-2,,51 +web-4,api,too-slow +cache-1,redis,12 diff --git a/courses/hands-on-with-zcode/scenario/data-monitor/scenario.json b/courses/hands-on-with-zcode/scenario/data-monitor/scenario.json new file mode 100644 index 0000000..b103477 --- /dev/null +++ b/courses/hands-on-with-zcode/scenario/data-monitor/scenario.json @@ -0,0 +1,8 @@ +{ + "skin": "data-monitor", + "data_file": "metrics.csv", + "required_fields": ["host", "service", "latency_ms"], + "numeric_field": "latency_ms", + "group_field": "service", + "report_file": "report.json" +} diff --git a/courses/hands-on-with-zcode/scenario/excel-report/orders.csv b/courses/hands-on-with-zcode/scenario/excel-report/orders.csv new file mode 100644 index 0000000..a41fca1 --- /dev/null +++ b/courses/hands-on-with-zcode/scenario/excel-report/orders.csv @@ -0,0 +1,8 @@ +order_id,region,amount,item_count +1001,emea,249.99,3 +1002,apac,89.50,1 +1003,emea,,2 +1004,americas,120.00,5 +1005,,310.00,4 +1006,apac,45.25,2 +1007,emea,640.10,6 diff --git a/courses/hands-on-with-zcode/scenario/excel-report/scenario.json b/courses/hands-on-with-zcode/scenario/excel-report/scenario.json new file mode 100644 index 0000000..66b2bd9 --- /dev/null +++ b/courses/hands-on-with-zcode/scenario/excel-report/scenario.json @@ -0,0 +1,8 @@ +{ + "skin": "excel-report", + "data_file": "orders.csv", + "required_fields": ["order_id", "region", "amount"], + "numeric_field": "amount", + "group_field": "region", + "report_file": "report.json" +} diff --git a/courses/hands-on-with-zcode/solution/report_tool.py b/courses/hands-on-with-zcode/solution/report_tool.py new file mode 100644 index 0000000..65ba0bf --- /dev/null +++ b/courses/hands-on-with-zcode/solution/report_tool.py @@ -0,0 +1,134 @@ +"""Scenario report tool (reviewed solution). + +Reads a scenario data file (CSV or JSON), isolates invalid rows instead of +crashing, aggregates valid rows per group with two-decimal rounding, and +writes the report atomically so an interrupted run never leaves a half-written +file. Standard library only. +""" + +from __future__ import annotations + +import csv +import json +import os +import sys +from pathlib import Path + +SUPPORTED_SUFFIXES = {".csv", ".json"} + + +def load_records(path: str | Path) -> list[dict]: + """Load CSV or JSON records from *path* as a list of dicts.""" + source = Path(path) + suffix = source.suffix.lower() + if suffix not in SUPPORTED_SUFFIXES: + raise ValueError(f"unsupported data file type: {source.name}") + if suffix == ".csv": + with source.open("r", encoding="utf-8", newline="") as handle: + records = list(csv.DictReader(handle)) + else: + data = json.loads(source.read_text(encoding="utf-8")) + records = data if isinstance(data, list) else None + if not all(isinstance(record, dict) for record in records): + raise ValueError("data file must contain a list of objects") + return records + + +def _field_text(record: dict, field: str) -> str: + value = record.get(field) + if not isinstance(value, str): + return "" if value is None else str(value).strip() + return value.strip() + + +def build_report( + records: list[dict], + *, + required_fields: list[str], + numeric_field: str, + group_field: str, +) -> dict: + """Aggregate *records*, isolating invalid rows with recorded reasons.""" + groups: dict[str, dict] = {} + errors: list[dict] = [] + for index, record in enumerate(records): + if not isinstance(record, dict): + errors.append({"index": index, "reason": "record is not an object"}) + continue + missing = [ + field for field in required_fields if not _field_text(record, field) + ] + if missing: + errors.append( + {"index": index, "reason": f"missing required field: {missing[0]}"} + ) + continue + raw_number = _field_text(record, numeric_field) + try: + number = float(raw_number) + except ValueError: + errors.append( + { + "index": index, + "reason": f"field {numeric_field!r} is not a number: {raw_number!r}", + } + ) + continue + group = _field_text(record, group_field) + bucket = groups.setdefault(group, {"count": 0, "total": 0.0}) + bucket["count"] += 1 + bucket["total"] = round(bucket["total"] + number, 2) + return { + "total": len(records), + "valid": len(records) - len(errors), + "invalid": len(errors), + "groups": groups, + "errors": errors, + } + + +def write_report(report: dict, destination: str | Path) -> None: + """Atomically write *report* as JSON, creating parent directories.""" + target = Path(destination) + target.parent.mkdir(parents=True, exist_ok=True) + temporary = target.with_name(target.name + ".tmp") + temporary.write_text( + json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8" + ) + os.replace(temporary, target) + + +def run_scenario(scenario_dir: str | Path) -> dict: + """Load scenario.json, process its data file, and write the report.""" + directory = Path(scenario_dir) + config = json.loads((directory / "scenario.json").read_text(encoding="utf-8")) + records = load_records(directory / config["data_file"]) + report = build_report( + records, + required_fields=config["required_fields"], + numeric_field=config["numeric_field"], + group_field=config["group_field"], + ) + write_report(report, directory / config["report_file"]) + return report + + +def main(argv: list[str] | None = None) -> int: + """Run one scenario directory and print a one-line summary.""" + arguments = sys.argv[1:] if argv is None else argv + if len(arguments) != 1: + print("usage: python report_tool.py <scenario-dir>", file=sys.stderr) + return 2 + try: + report = run_scenario(arguments[0]) + except (OSError, ValueError, KeyError, json.JSONDecodeError) as error: + print(f"error: {error}", file=sys.stderr) + return 1 + print( + f"total={report['total']} valid={report['valid']} invalid={report['invalid']}" + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/courses/hands-on-with-zcode/starter/report_tool.py b/courses/hands-on-with-zcode/starter/report_tool.py new file mode 100644 index 0000000..019edb8 --- /dev/null +++ b/courses/hands-on-with-zcode/starter/report_tool.py @@ -0,0 +1,88 @@ +"""Scenario report tool (starter, deliberately incomplete). + +Reads a scenario data file, aggregates valid rows, and writes a JSON report. +This starter reproduces the classic "it runs on the happy path" state of an +AI-written script: JSON inputs crash, invalid rows abort the run, group totals +carry floating-point noise, and reports cannot be written into a fresh +directory. The task contract in ../TASK.md defines the expected behavior. +""" + +from __future__ import annotations + +import csv +import json +import sys +from pathlib import Path + + +def load_records(path: str | Path) -> list[dict]: + """Load CSV or JSON records from *path* as a list of dicts.""" + source = Path(path) + with source.open("r", encoding="utf-8", newline="") as handle: + return list(csv.DictReader(handle)) + + +def build_report( + records: list[dict], + *, + required_fields: list[str], + numeric_field: str, + group_field: str, +) -> dict: + """Aggregate *records* into a summary report.""" + groups: dict[str, dict] = {} + for record in records: + for field in required_fields: + value = (record.get(field) or "").strip() + if not value: + raise KeyError(f"missing required field: {field}") + group = record[group_field].strip() + bucket = groups.setdefault(group, {"count": 0, "total": 0.0}) + bucket["count"] += 1 + bucket["total"] += float(record[numeric_field]) + return { + "total": len(records), + "valid": len(records), + "invalid": 0, + "groups": groups, + "errors": [], + } + + +def write_report(report: dict, destination: str | Path) -> None: + """Write *report* as JSON to *destination*.""" + Path(destination).write_text( + json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8" + ) + + +def run_scenario(scenario_dir: str | Path) -> dict: + """Load scenario.json, process its data file, and write the report.""" + directory = Path(scenario_dir) + config = json.loads((directory / "scenario.json").read_text(encoding="utf-8")) + records = load_records(directory / config["data_file"]) + report = build_report( + records, + required_fields=config["required_fields"], + numeric_field=config["numeric_field"], + group_field=config["group_field"], + ) + write_report(report, directory / config["report_file"]) + return report + + +def main(argv: list[str] | None = None) -> int: + """Run one scenario directory and print a one-line summary.""" + arguments = sys.argv[1:] if argv is None else argv + if len(arguments) != 1: + print("usage: python report_tool.py <scenario-dir>", file=sys.stderr) + return 2 + report = run_scenario(arguments[0]) + print( + f"total={report['total']} valid={report['valid']} invalid={report['invalid']}" + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/courses/hands-on-with-zcode/tests/test_report_tool.py b/courses/hands-on-with-zcode/tests/test_report_tool.py new file mode 100644 index 0000000..3fbc19f --- /dev/null +++ b/courses/hands-on-with-zcode/tests/test_report_tool.py @@ -0,0 +1,115 @@ +"""Contract tests for the scenario report tool. + +The suite is run twice: against ``starter/`` (specific failures expected) and +against ``solution/`` (everything must pass). See ../TASK.md for the contract. +""" + +from __future__ import annotations + +import json +import shutil +import tempfile +import unittest +from pathlib import Path + +import report_tool + +COURSE_ROOT = Path(__file__).resolve().parent.parent +SCENARIO_ROOT = COURSE_ROOT / "scenario" + + +class LoadRecordsTest(unittest.TestCase): + def test_load_csv_records_returns_list_of_dicts(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + data = Path(tmp) / "rows.csv" + data.write_text("id,region,amount\n1,emea,10.5\n2,apac,3\n", encoding="utf-8") + records = report_tool.load_records(data) + self.assertEqual(records, [{"id": "1", "region": "emea", "amount": "10.5"}, {"id": "2", "region": "apac", "amount": "3"}]) + + def test_load_json_records_returns_list_of_dicts(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + data = Path(tmp) / "rows.json" + data.write_text('[{"id": "1", "amount": "2"}]', encoding="utf-8") + records = report_tool.load_records(data) + self.assertEqual(records, [{"id": "1", "amount": "2"}]) + + def test_unsupported_suffix_raises_value_error(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + data = Path(tmp) / "rows.xlsx" + with self.assertRaises(ValueError): + report_tool.load_records(data) + + +class BuildReportTest(unittest.TestCase): + def test_invalid_records_are_isolated_with_reasons(self) -> None: + records = [ + {"id": "1", "region": "emea", "amount": "10"}, + {"id": "2", "region": "", "amount": "5"}, # missing group value + {"id": "", "region": "emea", "amount": "5"}, # missing required id + {"id": "3", "region": "apac", "amount": "n/a"} # non-numeric amount + ] + report = report_tool.build_report( + records, + required_fields=["id", "region", "amount"], + numeric_field="amount", + group_field="region", + ) + self.assertEqual(report["total"], 4) + self.assertEqual(report["valid"], 1) + self.assertEqual(report["invalid"], 3) + self.assertEqual([error["index"] for error in report["errors"]], [1, 2, 3]) + self.assertTrue(all(error["reason"] for error in report["errors"])) + self.assertEqual(report["groups"], {"emea": {"count": 1, "total": 10.0}}) + + def test_group_totals_are_rounded_to_two_decimals(self) -> None: + records = [ + {"id": "1", "region": "emea", "amount": "0.1"}, + {"id": "2", "region": "emea", "amount": "0.2"}, + ] + report = report_tool.build_report( + records, + required_fields=["id", "region", "amount"], + numeric_field="amount", + group_field="region", + ) + self.assertEqual(report["groups"]["emea"]["total"], 0.3) + + +class WriteReportTest(unittest.TestCase): + def test_write_report_creates_missing_parent_directories(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + destination = Path(tmp) / "out" / "nested" / "report.json" + report_tool.write_report({"total": 1}, destination) + self.assertTrue(destination.exists()) + self.assertEqual(json.loads(destination.read_text(encoding="utf-8"))["total"], 1) + self.assertFalse(destination.with_name(destination.name + ".tmp").exists()) + + +class ScenarioTest(unittest.TestCase): + def _copy(self, name: str) -> Path: + target = Path(tempfile.mkdtemp(prefix="scenario-")) / name + shutil.copytree(SCENARIO_ROOT / name, target) + self.addCleanup(shutil.rmtree, target.parent, ignore_errors=True) + return target + + def test_run_scenario_writes_report_file(self) -> None: + scenario = self._copy("api-tool") + report = report_tool.run_scenario(scenario) + written = json.loads((scenario / "report.json").read_text(encoding="utf-8")) + self.assertEqual(written, report) + self.assertEqual(report["total"], 7) + self.assertEqual(report["invalid"], 2) + self.assertEqual(sorted(report["groups"]), ["high", "low", "normal"]) + + def test_main_prints_summary_and_returns_zero(self) -> None: + scenario = self._copy("excel-report") + exit_code = report_tool.main([str(scenario)]) + self.assertEqual(exit_code, 0) + self.assertTrue((scenario / "report.json").exists()) + + def test_main_rejects_missing_argument(self) -> None: + self.assertEqual(report_tool.main([]), 2) + + +if __name__ == "__main__": + unittest.main() diff --git a/courses/hands-on-with-zcode/verify.py b/courses/hands-on-with-zcode/verify.py new file mode 100644 index 0000000..274fa48 --- /dev/null +++ b/courses/hands-on-with-zcode/verify.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +"""Run the course contract against starter or solution. + +Objective completion evidence for a FlyPython shared-core course — the +teaching contract lives in COURSE.md; this file is the exercise. Exit 0 +means the selected implementation satisfies the task contract; +``--expect-failure`` reproduces the unfinished starter state and checks that +the failing tests are the intended ones. +""" + +from __future__ import annotations + +import argparse +import base64 +import hashlib +import json +import os +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent + +# Optional shared claim-receipt producer (docs/CLAIM-RECEIPT.md). The course +# folder still verifies standalone — without the tools/ sibling or without +# FLYPYTHON_CLAIM_SECRET set, verify.py behaves exactly as before. +try: + sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "tools")) + import claim_receipt +except ImportError: + claim_receipt = None + +EXPECTED_STARTER_FAILURES = ( + "test_load_json_records_returns_list_of_dicts", + "test_unsupported_suffix_raises_value_error", + "test_invalid_records_are_isolated_with_reasons", + "test_group_totals_are_rounded_to_two_decimals", + "test_write_report_creates_missing_parent_directories", + "test_run_scenario_writes_report_file", + "test_main_prints_summary_and_returns_zero", +) + + + +# ── PER-COURSE BLOCK ──────────────────────────────────────────── +# The only section allowed to differ across shared-core courses +# (checkpoint ids and gates must still match — titles may carry the +# tool's flavor). tools/verify_courses.py enforces this. +COURSE_ID = 'course-zcode' +# Documented constant: claim codes derive deterministically from +# (COURSE_ID, checkpoint_id, COURSE_SALT). They are spot-checkable +# self-reported evidence, not tamper-proof secrets — see +# docs/repo-plan-0.0.4.md FP-411. +COURSE_SALT = '9432863162868c87' + +CHECKPOINTS = [ + {"id": "l01", "gate": "attest", "title": '在桌面工作区复现故障 / Reproduce in the desktop workspace'}, + {"id": "l02", "gate": "attest", "title": '任务契约 / Task contract'}, + {"id": "l03", "gate": "starter-suite", "title": '计划—执行—自检的有界改动 / Plan–execute–self-check, bounded'}, + {"id": "l04", "gate": "both-suites", "title": '验证与审查 / Verify and review'}, + {"id": "l05", "gate": "attest", "title": '应用到自己的项目 / Apply to your project'}, +] +# ── END PER-COURSE BLOCK ──────────────────────────────────────── + +def _claim_code(checkpoint_id): + digest = hashlib.sha256( + (COURSE_ID + ":" + checkpoint_id + ":" + COURSE_SALT).encode("utf-8") + ).digest() + return base64.b32encode(digest).decode("ascii")[:8] + +def _run_suite(implementation): + environment = os.environ.copy() + environment["PYTHONPATH"] = str(ROOT / implementation) + return subprocess.run( + [sys.executable, "-m", "unittest", "discover", "-s", str(ROOT / "tests")], + env=environment, check=False, capture_output=True, text=True, + ) + +def run_progress(as_json, receipt_out=None): + import time + t0 = time.monotonic() + starter = _run_suite("starter") + starter_ms = int((time.monotonic() - t0) * 1000) + t0 = time.monotonic() + solution = _run_suite("solution") + solution_ms = int((time.monotonic() - t0) * 1000) + starter_ok = starter.returncode == 0 + solution_ok = solution.returncode == 0 + rows = [] + for checkpoint in CHECKPOINTS: + gate = checkpoint["gate"] + if gate == "attest": + status, kind = "attest", "attested" + elif gate == "starter-suite": + status = "passed" if starter_ok else "open" + kind = "objective" + else: + status = "passed" if (starter_ok and solution_ok) else "open" + kind = "objective" + code = _claim_code(checkpoint["id"]) if status in ("passed", "attest") else None + row = dict(checkpoint) + row["status"] = status + row["kind"] = kind + row["claim_code"] = code + rows.append(row) + secret = claim_receipt.receipts_enabled() if claim_receipt else None + receipts = [] + if secret: + starter_tests = claim_receipt.count_tests(starter.stderr) + solution_tests = claim_receipt.count_tests(solution.stderr) + solution_hash = claim_receipt.solution_sha256(ROOT) + for checkpoint in CHECKPOINTS: + gate = checkpoint["gate"] + if gate == "starter-suite": + passed, tests, ms = starter_ok, starter_tests, starter_ms + elif gate == "both-suites": + passed = starter_ok and solution_ok + tests, ms = starter_tests + solution_tests, starter_ms + solution_ms + else: + continue + receipts.append(claim_receipt.make_receipt( + COURSE_ID, checkpoint["id"], passed=passed, tests=tests, + duration_ms=ms, impl_dir=ROOT / "starter", + solution_hash=solution_hash, secret=secret)) + if as_json: + document = {"course": COURSE_ID, + "starter_suite_passed": starter_ok, + "solution_suite_passed": solution_ok, + "checkpoints": rows} + if secret: + document["receipts"] = receipts + print(json.dumps(document, ensure_ascii=False, indent=2)) + else: + starter_state = "passed" if starter_ok else "not passed" + solution_state = "passed" if solution_ok else "not passed" + print("Course " + COURSE_ID) + print("Suites: starter " + starter_state + " / solution " + solution_state) + for row in rows: + state = row["status"] + (" (self-attested)" if row["kind"] == "attested" else "") + code = "claim code " + row["claim_code"] if row["claim_code"] else "—" + print(" " + row["id"] + " " + row["title"] + " [" + state + "] " + code) + print("Claim codes are self-reported evidence, recorded at flypython.com; never a certificate.") + if secret: + print(f"Signed run receipts prepared for {len(receipts)} gated checkpoint(s);" + " submit each with its claim to mark them as a local-run receipt.") + if receipt_out and secret: + Path(receipt_out).write_text( + json.dumps({"receipts": receipts}, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8") + print(f"Wrote {len(receipts)} receipt(s) to {receipt_out}", file=sys.stderr) + return 0 + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("implementation", choices=("progress", "starter", "solution")) + parser.add_argument("--json", action="store_true") + parser.add_argument("--expect-failure", action="store_true") + parser.add_argument("--receipt-out", metavar="PATH", help="write signed run receipts JSON (requires FLYPYTHON_CLAIM_SECRET)") + args = parser.parse_args() + + if args.implementation == "progress": + return run_progress(args.json, args.receipt_out) + + command = [sys.executable, "-m", "unittest", "discover", "-s", str(ROOT / "tests")] + environment = os.environ.copy() + environment["PYTHONPATH"] = str(ROOT / args.implementation) + result = subprocess.run( + command, env=environment, check=False, capture_output=True, text=True + ) + + if args.expect_failure: + if result.returncode == 0: + print("Expected the starter to fail, but it passed.", file=sys.stderr) + return 1 + output = (result.stdout or "") + (result.stderr or "") + missing = [ + name for name in EXPECTED_STARTER_FAILURES if name not in output + ] + if missing: + print( + "Starter failed for unexpected reasons; missing expected failures:", + file=sys.stderr, + ) + print("\n".join(missing), file=sys.stderr) + print(output, file=sys.stderr) + return 1 + print( + "Expected starter state reproduced: JSON inputs crash, invalid rows " + "abort the run, totals are unrounded, and reports need an existing " + "directory." + ) + return 0 + if result.returncode == 0: + print(f"{args.implementation}: all tests passed") + else: + sys.stderr.write((result.stderr or "") or (result.stdout or "")) + return result.returncode + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/courses/mcp-server-in-python/verify.py b/courses/mcp-server-in-python/verify.py index 0f575c4..5009ac0 100644 --- a/courses/mcp-server-in-python/verify.py +++ b/courses/mcp-server-in-python/verify.py @@ -14,6 +14,15 @@ ROOT = Path(__file__).resolve().parent +# Optional shared claim-receipt producer (docs/CLAIM-RECEIPT.md). The course +# folder still verifies standalone — without the tools/ sibling or without +# FLYPYTHON_CLAIM_SECRET set, verify.py behaves exactly as before. +try: + sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "tools")) + import claim_receipt +except ImportError: + claim_receipt = None + COURSE_ID = 'course-mcp-tools' @@ -45,9 +54,14 @@ def _run_suite(implementation): env=environment, check=False, capture_output=True, text=True, ) -def run_progress(as_json): +def run_progress(as_json, receipt_out=None): + import time + t0 = time.monotonic() starter = _run_suite("starter") + starter_ms = int((time.monotonic() - t0) * 1000) + t0 = time.monotonic() solution = _run_suite("solution") + solution_ms = int((time.monotonic() - t0) * 1000) starter_ok = starter.returncode == 0 solution_ok = solution.returncode == 0 rows = [] @@ -67,21 +81,51 @@ def run_progress(as_json): row["kind"] = kind row["claim_code"] = code rows.append(row) + secret = claim_receipt.receipts_enabled() if claim_receipt else None + receipts = [] + if secret: + starter_tests = claim_receipt.count_tests(starter.stderr) + solution_tests = claim_receipt.count_tests(solution.stderr) + solution_hash = claim_receipt.solution_sha256(ROOT) + for checkpoint in CHECKPOINTS: + gate = checkpoint["gate"] + if gate == "starter-suite": + passed, tests, ms = starter_ok, starter_tests, starter_ms + elif gate == "both-suites": + passed = starter_ok and solution_ok + tests, ms = starter_tests + solution_tests, starter_ms + solution_ms + else: + continue + receipts.append(claim_receipt.make_receipt( + COURSE_ID, checkpoint["id"], passed=passed, tests=tests, + duration_ms=ms, impl_dir=ROOT / "starter", + solution_hash=solution_hash, secret=secret)) if as_json: - print(json.dumps({"course": COURSE_ID, + document = {"course": COURSE_ID, "starter_suite_passed": starter_ok, "solution_suite_passed": solution_ok, - "checkpoints": rows}, ensure_ascii=False, indent=2)) + "checkpoints": rows} + if secret: + document["receipts"] = receipts + print(json.dumps(document, ensure_ascii=False, indent=2)) else: - starter_state = "通过" if starter_ok else "未通过" - solution_state = "通过" if solution_ok else "未通过" - print("课程 " + COURSE_ID) - print("实现状态: starter " + starter_state + " / solution " + solution_state) + starter_state = "passed" if starter_ok else "not passed" + solution_state = "passed" if solution_ok else "not passed" + print("Course " + COURSE_ID) + print("Suites: starter " + starter_state + " / solution " + solution_state) for row in rows: - state = row["status"] + ("(自报)" if row["kind"] == "attested" else "") - code = "认领码 " + row["claim_code"] if row["claim_code"] else "—" + state = row["status"] + (" (self-attested)" if row["kind"] == "attested" else "") + code = "claim code " + row["claim_code"] if row["claim_code"] else "—" print(" " + row["id"] + " " + row["title"] + " [" + state + "] " + code) - print("认领码是自我报告的证据,在 flypython.com 记录;绝非证书。") + print("Claim codes are self-reported evidence, recorded at flypython.com; never a certificate.") + if secret: + print(f"Signed run receipts prepared for {len(receipts)} gated checkpoint(s);" + " submit each with its claim to mark them as a local-run receipt.") + if receipt_out and secret: + Path(receipt_out).write_text( + json.dumps({"receipts": receipts}, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8") + print(f"Wrote {len(receipts)} receipt(s) to {receipt_out}", file=sys.stderr) return 0 def main() -> int: @@ -89,10 +133,11 @@ def main() -> int: parser.add_argument("implementation", choices=("progress", "starter", "solution")) parser.add_argument("--json", action="store_true") parser.add_argument("--expect-failure", action="store_true") + parser.add_argument("--receipt-out", metavar="PATH", help="write signed run receipts JSON (requires FLYPYTHON_CLAIM_SECRET)") args = parser.parse_args() if args.implementation == "progress": - return run_progress(args.json) + return run_progress(args.json, args.receipt_out) command = [ sys.executable, @@ -130,6 +175,10 @@ def main() -> int: return 1 print("Expected MCP boundary regression reproduced: unhandled exceptions and missing schema validations fail.") return 0 + if result.returncode == 0: + print(f"{args.implementation}: all tests passed") + else: + sys.stderr.write((result.stderr or "") or (result.stdout or "")) return result.returncode diff --git a/courses/verifying-ai-generated-code/verify.py b/courses/verifying-ai-generated-code/verify.py index 69642b0..7894928 100644 --- a/courses/verifying-ai-generated-code/verify.py +++ b/courses/verifying-ai-generated-code/verify.py @@ -18,6 +18,15 @@ ROOT = Path(__file__).resolve().parent +# Optional shared claim-receipt producer (docs/CLAIM-RECEIPT.md). The course +# folder still verifies standalone — without the tools/ sibling or without +# FLYPYTHON_CLAIM_SECRET set, verify.py behaves exactly as before. +try: + sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "tools")) + import claim_receipt +except ImportError: + claim_receipt = None + EXPECTED_STARTER_FAILURES = ( "test_green_project_produces_ok_check", "test_failing_project_is_failed", @@ -59,9 +68,14 @@ def _run_suite(implementation): env=environment, check=False, capture_output=True, text=True, ) -def run_progress(as_json): +def run_progress(as_json, receipt_out=None): + import time + t0 = time.monotonic() starter = _run_suite("starter") + starter_ms = int((time.monotonic() - t0) * 1000) + t0 = time.monotonic() solution = _run_suite("solution") + solution_ms = int((time.monotonic() - t0) * 1000) starter_ok = starter.returncode == 0 solution_ok = solution.returncode == 0 rows = [] @@ -81,21 +95,51 @@ def run_progress(as_json): row["kind"] = kind row["claim_code"] = code rows.append(row) + secret = claim_receipt.receipts_enabled() if claim_receipt else None + receipts = [] + if secret: + starter_tests = claim_receipt.count_tests(starter.stderr) + solution_tests = claim_receipt.count_tests(solution.stderr) + solution_hash = claim_receipt.solution_sha256(ROOT) + for checkpoint in CHECKPOINTS: + gate = checkpoint["gate"] + if gate == "starter-suite": + passed, tests, ms = starter_ok, starter_tests, starter_ms + elif gate == "both-suites": + passed = starter_ok and solution_ok + tests, ms = starter_tests + solution_tests, starter_ms + solution_ms + else: + continue + receipts.append(claim_receipt.make_receipt( + COURSE_ID, checkpoint["id"], passed=passed, tests=tests, + duration_ms=ms, impl_dir=ROOT / "starter", + solution_hash=solution_hash, secret=secret)) if as_json: - print(json.dumps({"course": COURSE_ID, + document = {"course": COURSE_ID, "starter_suite_passed": starter_ok, "solution_suite_passed": solution_ok, - "checkpoints": rows}, ensure_ascii=False, indent=2)) + "checkpoints": rows} + if secret: + document["receipts"] = receipts + print(json.dumps(document, ensure_ascii=False, indent=2)) else: - starter_state = "通过" if starter_ok else "未通过" - solution_state = "通过" if solution_ok else "未通过" - print("课程 " + COURSE_ID) - print("实现状态: starter " + starter_state + " / solution " + solution_state) + starter_state = "passed" if starter_ok else "not passed" + solution_state = "passed" if solution_ok else "not passed" + print("Course " + COURSE_ID) + print("Suites: starter " + starter_state + " / solution " + solution_state) for row in rows: - state = row["status"] + ("(自报)" if row["kind"] == "attested" else "") - code = "认领码 " + row["claim_code"] if row["claim_code"] else "—" + state = row["status"] + (" (self-attested)" if row["kind"] == "attested" else "") + code = "claim code " + row["claim_code"] if row["claim_code"] else "—" print(" " + row["id"] + " " + row["title"] + " [" + state + "] " + code) - print("认领码是自我报告的证据,在 flypython.com 记录;绝非证书。") + print("Claim codes are self-reported evidence, recorded at flypython.com; never a certificate.") + if secret: + print(f"Signed run receipts prepared for {len(receipts)} gated checkpoint(s);" + " submit each with its claim to mark them as a local-run receipt.") + if receipt_out and secret: + Path(receipt_out).write_text( + json.dumps({"receipts": receipts}, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8") + print(f"Wrote {len(receipts)} receipt(s) to {receipt_out}", file=sys.stderr) return 0 def main() -> int: @@ -103,10 +147,11 @@ def main() -> int: parser.add_argument("implementation", choices=("progress", "starter", "solution")) parser.add_argument("--json", action="store_true") parser.add_argument("--expect-failure", action="store_true") + parser.add_argument("--receipt-out", metavar="PATH", help="write signed run receipts JSON (requires FLYPYTHON_CLAIM_SECRET)") args = parser.parse_args() if args.implementation == "progress": - return run_progress(args.json) + return run_progress(args.json, args.receipt_out) command = [sys.executable, "-m", "unittest", "discover", "-s", str(ROOT / "tests")] environment = os.environ.copy() @@ -135,6 +180,10 @@ def main() -> int: "non-atomic records." ) return 0 + if result.returncode == 0: + print(f"{args.implementation}: all tests passed") + else: + sys.stderr.write((result.stderr or "") or (result.stdout or "")) return result.returncode diff --git a/paths/foundation/modules/m0-orientation.md b/paths/foundation/modules/m0-orientation.md index 584988f..42e4f0d 100644 --- a/paths/foundation/modules/m0-orientation.md +++ b/paths/foundation/modules/m0-orientation.md @@ -2,11 +2,11 @@ id: path-foundation-m0 type: path title: The agent tool landscape -summary: Foundation path module 0 — orientation on Claude Code, Codex CLI, and Cursor; install one agent and observe its evidence habits. +summary: Foundation path module 0 — orientation across six agentic coding tools; pick the one that fits your environment and observe its evidence habits. lang: en-US -content_version: 1 +content_version: 2 status: reviewed -reviewed_on: 2026-09-12 +reviewed_on: 2026-09-13 --- # Module 0: The agent tool landscape @@ -18,25 +18,34 @@ have in common, and which one you will drive first. ## The landscape -Three agentic coding tools matter for this path: +Six agentic coding tools carry this path's hands-on courses. Pick by where +you want to work: | Tool | Where it runs | What it's good at | | --- | --- | --- | | **Claude Code** | terminal | long multi-step tasks, strong instruction following, reads `CLAUDE.md`/`AGENTS.md` | -| **Codex CLI** | terminal | open-source agent loop, sandboxed execution, reads `AGENTS.md` | -| **Cursor** | IDE | inline edits and chat inside an editor, good for people who live in a GUI | +| **Codex (desktop app)** | desktop app | task threads with approvals and diff review, reads `AGENTS.md` | +| **Cursor** | IDE | Agent/Ask/Plan modes inside an editor, `.cursor/rules` project rules | +| **DeepSeek Harness** | Python framework | plugin-composed agents (`cordis.yml`), auditable append-only trajectories | +| **Kimi Code** | terminal | built-in `explore`/`plan`/`coder` subagents that split reading, planning, editing | +| **ZCode** | desktop app | long-horizon Goal Mode, task lists, workspace Git state, remote check-ins | They differ in interface, not in fundamentals. Every one of them: reads your files, follows written instructions, edits code, runs commands, and — this is the important part — **can be wrong in ways that look right**. That is why this path spends so much effort on rules and verification, not on prompts. +**Choosing by environment:** live in the terminal → Claude Code or Kimi Code. +Live in a GUI → Codex app, Cursor, or ZCode. Want to assemble and audit the +agent loop itself → DeepSeek Harness. You only need **one** to start; the +route challenge at the end asks for a second, different one. + ## Your first task (no code) -1. Pick **one** of Claude Code or Codex CLI and install it following the - official docs (linked from the course pages). -2. Open a terminal in an empty folder, start the agent, and ask it: - "create a Python script that prints the current date, then run it." +1. Pick **one** of the six tools and install it following its official docs + (linked from the course pages). +2. Open it on an empty folder and ask it: "create a Python script that + prints the current date, then run it." 3. Watch what it does: which files it creates, which commands it runs, what it reports back. 4. Ask it: "how do you know it works?" — notice whether it *shows evidence* diff --git a/paths/foundation/modules/m0-orientation_cn.md b/paths/foundation/modules/m0-orientation_cn.md index 4084969..e2a6c3d 100644 --- a/paths/foundation/modules/m0-orientation_cn.md +++ b/paths/foundation/modules/m0-orientation_cn.md @@ -2,11 +2,11 @@ id: path-foundation-m0 type: path title: Agent 工具生态总览 -summary: 基础路线模块 0——Claude Code、Codex CLI、Cursor 生态导览;装一个 Agent 并观察它的证据习惯。 +summary: 基础路线模块 0——六个 Agentic 编程工具生态导览;按你的工作环境选一个,并观察它的证据习惯。 lang: zh-CN -content_version: 1 +content_version: 2 status: reviewed -reviewed_on: 2026-09-12 +reviewed_on: 2026-09-13 --- # 模块 0:Agent 工具生态总览 @@ -17,34 +17,39 @@ reviewed_on: 2026-09-12 ## 生态概览 -这条路线里有三个重要的 Agentic 编程工具: +六个 Agentic 编程工具承载本路线的实战课程。按你想工作的位置选: | 工具 | 运行位置 | 擅长什么 | | --- | --- | --- | | **Claude Code** | 终端 | 长链路多步任务,指令遵循强,读 `CLAUDE.md`/`AGENTS.md` | -| **Codex CLI** | 终端 | 开源 Agent 循环,沙箱执行,读 `AGENTS.md` | -| **Cursor** | IDE | 编辑器内嵌改写与对话,适合习惯 GUI 的人 | +| **Codex(桌面应用)** | 桌面应用 | 带审批与 diff 审查的任务线程,读 `AGENTS.md` | +| **Cursor** | IDE | 编辑器内 Agent/Ask/Plan 模式,`.cursor/rules` 项目规则 | +| **DeepSeek Harness** | Python 框架 | 插件组装的 Agent(`cordis.yml`),可审计的只增轨迹 | +| **Kimi Code** | 终端 | 内置 `explore`/`plan`/`coder` 子代理,读、规划、改码分道 | +| **ZCode** | 桌面应用 | 长程 Goal 模式、任务列表、工作区 Git 状态、远程查岗 | 它们的差别在界面,不在本质。每一个都:读你的文件、遵循书面指令、改代码、 跑命令,以及——这是重点——**可能以一种看起来对的方式出错**。所以这条 路线把大量精力放在规则和验证上,而不是提示词上。 -## 你的第一个任务(不写代码) +**按环境选:** 住终端 → Claude Code 或 Kimi Code;住图形界面 → Codex +应用、Cursor 或 ZCode;想自己组装并审计 Agent 循环 → DeepSeek Harness。 +起步只需要**一个**;路线末尾的挑战会要求第二个不同的工具。 -1. 在 Claude Code 或 Codex CLI 里**选一个**,按官方文档装好(课程页有链接)。 -2. 在一个空文件夹里打开终端,启动 Agent,对它说: - “写一个打印当前日期的 Python 脚本,然后运行它。” -3. 观察它的行为:建了哪些文件、跑了哪些命令、汇报了什么。 -4. 问它:“你怎么知道它能跑?”——注意它是**给出证据**还是**只说能跑**。 - 这个差距就是这条路线剩下部分要解决的问题。 +## 第一个任务(不写代码) + +1. 从六个工具里挑**一个**,按官方文档安装(课程页有链接)。 +2. 在一个空文件夹里打开它,让它「写一个打印当前日期的 Python 脚本并运行」。 +3. 观察它的行为:创建了哪些文件、跑了哪些命令、回报了什么。 +4. 问它:「你怎么知道它能跑?」——注意它是*给出证据*还是只*说跑过了*。 + 这个差距就是本路线剩下的内容要讲的。 ## 检查点 -当你能用自己的话回答下面两个问题时,本模块完成: +能用自己的话回答下面两题,本模块即完成: - 你装了哪个 Agent,怎么启动它? -- 当你要日期脚本时它做了什么——它给出的“脚本跑通了”的证据是什么? +- 你让它写日期脚本时它做了什么——它给出的是什么证据? -自我声明检查点:两个问题都能答上,就记录下来——基础路线 M0 模块值 -10 分。诚实自报是本平台的规则:没人检查你的答案,但这里的一切也都 -不是证书。 +自我声明检查点:能答出两题即可认领——这是基础路线的模块 M0,值 10 分。 +诚实自我报告是本站规则:没人检查你的答案,但这里也没有任何证书。 diff --git a/paths/foundation/modules/route-challenge.md b/paths/foundation/modules/route-challenge.md index 4c1a4f5..3fbd20f 100644 --- a/paths/foundation/modules/route-challenge.md +++ b/paths/foundation/modules/route-challenge.md @@ -2,36 +2,39 @@ id: path-foundation-challenge type: path title: "Route challenge: dual-agent, double-verified" -summary: Foundation path capstone — run the same task through Claude Code and Codex CLI and verify both outputs yourself. +summary: Foundation path capstone — run the same task through any two of the six path agents and verify both outputs yourself. lang: en-US -content_version: 1 +content_version: 2 status: reviewed -reviewed_on: 2026-09-12 +reviewed_on: 2026-09-13 --- # Route challenge: dual-agent, double-verified **Points:** 200 · **Type:** project · **Evidence:** objective (`verify.py`) -The final challenge of the Foundation path: prove you can drive **two** -different agents through the same task — and verify both results yourself. +The final challenge of the Foundation path: prove you can drive **two +different** agents through the same task — and verify both results yourself. ## The task Pick a real small task you care about (suggestion: "a CLI that counts the -lines, words, and bytes of a file" or reuse any scenario skin from the four -path courses: `hands-on-python-with-claude-code`, -`hands-on-with-openai-codex-cli`, `agent-rules-single-source`, +lines, words, and bytes of a file" or reuse any scenario skin from the path +courses: `hands-on-python-with-claude-code`, `hands-on-with-openai-codex`, +`hands-on-with-cursor`, `hands-on-with-deepseek-harness`, +`hands-on-with-kimi-code`, `hands-on-with-zcode`, `agent-rules-single-source`, `verifying-ai-generated-code`). -Then: -1. **Round one — Claude Code.** In a fresh folder, write a `TASK.md` +Choose **any two** of the six agent tools this path teaches (Claude Code, +Codex app, Cursor, DeepSeek Harness, Kimi Code, ZCode). Then: + +1. **Round one — agent A.** In a fresh folder, write a `TASK.md` describing the contract (inputs, outputs, edge cases, done = tests pass), - ask Claude Code to implement it, and add a `verify.py`-style check that - passes on the result. -2. **Round two — Codex CLI.** Same task, fresh folder, same `TASK.md`. - Drive Codex CLI to implement it independently — do not copy round one's - code. + drive your first agent to implement it, and add a `verify.py`-style + check that passes on the result. +2. **Round two — agent B.** Same task, fresh folder, same `TASK.md`. + Drive a *different* agent to implement it independently — do not copy + round one's code. 3. **Double verification.** Run your objective check against **both** implementations. Record what differed: structure, edge-case handling, test coverage, anything that surprised you. @@ -50,6 +53,8 @@ Complete when: - Both implementations exist and your objective check passes on each. - Your comparison note names at least one concrete difference with evidence (a file, a test, a command output). +- The two agents were genuinely different tools — not two sessions of the + same one. The claim for this challenge is recorded against the Foundation path badge — 200 points toward **Agent User**. Self-reported evidence, never a certificate. diff --git a/paths/foundation/modules/route-challenge_cn.md b/paths/foundation/modules/route-challenge_cn.md index c849b54..70fdf2a 100644 --- a/paths/foundation/modules/route-challenge_cn.md +++ b/paths/foundation/modules/route-challenge_cn.md @@ -2,47 +2,55 @@ id: path-foundation-challenge type: path title: "路线挑战:双 Agent、双重验证" -summary: 基础路线收官挑战——同一任务分别用 Claude Code 与 Codex CLI 完成并亲手双重验证。 +summary: 基础路线收官——把同一个任务分别交给六个路线 Agent 中任选的两个,并亲手验证两份产出。 lang: zh-CN -content_version: 1 +content_version: 2 status: reviewed -reviewed_on: 2026-09-12 +reviewed_on: 2026-09-13 --- # 路线挑战:双 Agent、双重验证 -**积分:** 200 · **类型:** 项目 · **证据:** 客观验证(`verify.py`) +**积分:** 200 · **类型:** 项目 · **证据:** 客观(`verify.py`) -基础路线的收官挑战:证明你能驱动**两个**不同的 Agent 完成同一项任务—— -并且两个结果都由你亲手验证。 +基础路线的最后一关:证明你能驱动**两个不同**的 Agent 完成同一个任务—— +并且亲手验证两份结果。 ## 任务 -选一个你真正在乎的小任务(建议:“一个统计文件行数、词数、字节数的 -CLI”,或复用路线四门课程 `hands-on-python-with-claude-code`、 -`hands-on-with-openai-codex-cli`、`agent-rules-single-source`、 -`verifying-ai-generated-code` 的任意场景皮)。然后: +挑一个你真正在乎的小任务(建议:「一个统计文件行数、词数、字节数的 +CLI」,或复用路线课程里的任一场景皮肤: +`hands-on-python-with-claude-code`、`hands-on-with-openai-codex`、 +`hands-on-with-cursor`、`hands-on-with-deepseek-harness`、 +`hands-on-with-kimi-code`、`hands-on-with-zcode`、 +`agent-rules-single-source`、`verifying-ai-generated-code`)。 -1. **第一轮 —— Claude Code。** 新建文件夹,写一个 `TASK.md` 描述契约 - (输入、输出、边界情况、完成 = 测试通过),让 Claude Code 实现它, - 并写一个 `verify.py` 式的检查,在结果上跑通。 -2. **第二轮 —— Codex CLI。** 同一任务、新文件夹、同一份 `TASK.md`。 - 用 Codex CLI 独立实现——不要抄第一轮的代码。 -3. **双重验证。** 用你的客观检查对**两份**实现各跑一遍。记录差异: - 结构、边界处理、测试覆盖、任何让你意外的地方。 +从本路线教授的六个 Agent 工具中任选**两个**(Claude Code、Codex 应用、 +Cursor、DeepSeek Harness、Kimi Code、ZCode),然后: -## 需要留下的记录 +1. **第一轮——Agent A。** 在全新文件夹里写一份描述契约的 `TASK.md` + (输入、输出、边界情况、完成 = 测试通过),驱动第一个 Agent 实现, + 并加一个 `verify.py` 式的检查能在结果上通过。 +2. **第二轮——Agent B。** 同一个任务、新文件夹、同一份 `TASK.md`。 + 驱动一个*不同*的 Agent 独立实现——不要抄第一轮的代码。 +3. **双重验证。** 对**两份**实现都跑你的客观检查。记录差异:结构、 + 边界情况处理、测试覆盖、任何让你意外的地方。 -- 两个文件夹都保留原样(它们就是你的证据)。 -- 一段简短笔记:哪个 Agent 的产出需要的修正更少,以及**你为什么这么 - 判断**——这是把 `verifying-ai-generated-code` 一课的怀疑式审查用到跨工具对比上。 +## 要记录什么 + +- 两个文件夹都原样保留(它们就是你的证据)。 +- 一段短笔记:哪个 Agent 的产出需要的修正更少,以及*你为什么这么 + 认为*——这是 `verifying-ai-generated-code` 课程里怀疑式评审习惯 + 的跨工具应用。 ## 检查点 -完成条件: +满足以下条件即完成: -- 两份实现都在,且你的客观检查对每份都通过。 -- 你的对比笔记至少写出一个有证据的具体差异(文件、测试或命令输出)。 +- 两份实现都存在,且你的客观检查在各自上通过。 +- 你的对比笔记点名了至少一处有证据的具体差异(一个文件、一个测试、 + 一段命令输出)。 +- 两个 Agent 确实是不同的工具——不是同一个工具的两个会话。 -本挑战的认领计入基础路线徽章——**「Agent 使用者」**的 200 分。 -自我报告的证据,绝非证书。 +本挑战的认领记入基础路线徽章——为 **Agent 使用者** 贡献 200 分。 +自我报告的证据,从来不是证书。 diff --git a/paths/foundation/path.json b/paths/foundation/path.json index 1d57744..ba31a78 100644 --- a/paths/foundation/path.json +++ b/paths/foundation/path.json @@ -4,12 +4,12 @@ "status": "live", "title_en": "Agent tools foundation", "title_zh": "Agent 工具基础", - "summary_en": "Learn to drive real coding agents - Claude Code and Codex CLI - give them good rules, and verify everything they produce.", - "summary_zh": "学会驱动真正的编码 Agent(Claude Code 与 Codex CLI),给它们好规则,并验证它们产出的一切。", + "summary_en": "Learn to drive real coding agents - Claude Code, Codex, Cursor, DeepSeek Harness, Kimi Code, or ZCode - give them good rules, and verify everything they produce.", + "summary_zh": "学会驱动真正的编码 Agent(Claude Code、Codex、Cursor、DeepSeek Harness、Kimi Code、ZCode 任选),给它们好规则,并验证它们产出的一切。", "audience_en": "Beginners who have never (or barely) used AI coding agents.", "audience_zh": "从未用过(或刚接触)AI 编程 Agent 的初学者。", - "outcome_en": "You can independently drive Claude Code and Codex on small tasks, write a single-source rules file, and verify agent output with your own hands.", - "outcome_zh": "你能独立驱动 Claude Code 与 Codex 完成小任务,写出单一真源规则文件,并亲手验证 Agent 的产出。", + "outcome_en": "You can independently drive two different coding agents on small tasks, write a single-source rules file, and verify agent output with your own hands.", + "outcome_zh": "你能独立驱动两种不同的编码 Agent 完成小任务,写出单一真源规则文件,并亲手验证 Agent 的产出。", "badge": { "id": "agent-user", "name_en": "Agent User", @@ -42,9 +42,9 @@ "order": 2, "id": "m2", "kind": "course", - "course": "hands-on-with-openai-codex-cli", - "title_en": "Codex CLI hands-on", - "title_zh": "Codex CLI 实战", + "course": "hands-on-with-openai-codex", + "title_en": "Codex app hands-on", + "title_zh": "Codex 应用实战", "points": 100, "branch": "first-agent", "branch_label_en": "pick your first agent", @@ -52,6 +52,54 @@ }, { "order": 3, + "id": "m3a", + "kind": "course", + "course": "hands-on-with-cursor", + "title_en": "Cursor hands-on", + "title_zh": "Cursor 实战", + "points": 100, + "branch": "first-agent", + "branch_label_en": "pick your first agent", + "branch_label_zh": "选你的第一个 Agent" + }, + { + "order": 4, + "id": "m3b", + "kind": "course", + "course": "hands-on-with-deepseek-harness", + "title_en": "DeepSeek Harness hands-on", + "title_zh": "DeepSeek Harness 实战", + "points": 100, + "branch": "first-agent", + "branch_label_en": "pick your first agent", + "branch_label_zh": "选你的第一个 Agent" + }, + { + "order": 5, + "id": "m3c", + "kind": "course", + "course": "hands-on-with-kimi-code", + "title_en": "Kimi Code hands-on", + "title_zh": "Kimi Code 实战", + "points": 100, + "branch": "first-agent", + "branch_label_en": "pick your first agent", + "branch_label_zh": "选你的第一个 Agent" + }, + { + "order": 6, + "id": "m3d", + "kind": "course", + "course": "hands-on-with-zcode", + "title_en": "ZCode hands-on", + "title_zh": "ZCode 实战", + "points": 100, + "branch": "first-agent", + "branch_label_en": "pick your first agent", + "branch_label_zh": "选你的第一个 Agent" + }, + { + "order": 7, "id": "m3", "kind": "course", "course": "agent-rules-single-source", @@ -60,7 +108,7 @@ "points": 100 }, { - "order": 4, + "order": 8, "id": "m4", "kind": "course", "course": "verifying-ai-generated-code", @@ -69,7 +117,7 @@ "points": 100 }, { - "order": 5, + "order": 9, "id": "route-challenge", "kind": "module", "ref": "modules/route-challenge.md", diff --git a/tests/test_courses.py b/tests/test_courses.py index 43e61a5..d9a93b3 100644 --- a/tests/test_courses.py +++ b/tests/test_courses.py @@ -13,7 +13,11 @@ def test_courses_directory_is_discovered() -> None: names = {course.name for course in courses} assert { "hands-on-python-with-claude-code", - "hands-on-with-openai-codex-cli", + "hands-on-with-openai-codex", + "hands-on-with-cursor", + "hands-on-with-deepseek-harness", + "hands-on-with-kimi-code", + "hands-on-with-zcode", "agent-rules-single-source", "verifying-ai-generated-code", "mcp-server-in-python", @@ -54,6 +58,10 @@ def test_course_documents_enter_the_content_manifest() -> None: expected_courses = { "course-claude-code", "course-codex-cli", + "course-cursor", + "course-deepseek-harness", + "course-kimi-code", + "course-zcode", "course-agent-rules", "course-verify-ship", "course-mcp-tools", diff --git a/tools/build_solution_hashes.py b/tools/build_solution_hashes.py new file mode 100644 index 0000000..ccd3e97 --- /dev/null +++ b/tools/build_solution_hashes.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Generate or verify course-solutions.json — the per-course solution tree +hashes the website uses to independently recompute receipt.solution_match +(docs/CLAIM-RECEIPT.md, FP-705). + +The hash is exactly tools/claim_receipt.tree_sha256 over the course's +solution/ directory, so producer and verifier agree byte-for-byte. Courses +without a solution/ directory are omitted; courses whose solution tree is +empty hash to the empty-tree digest (still published, still comparable). +""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +OUTPUT = ROOT / "course-solutions.json" +COURSE_ID_PATTERN = re.compile(r'''^COURSE_ID\s*=\s*["']([^"']+)["']''', re.M) + +sys.path.insert(0, str(ROOT / "tools")) +import claim_receipt # noqa: E402 + + +def build(root: Path) -> dict: + courses = [] + for course_dir in sorted((root / "courses").iterdir()): + if not course_dir.is_dir(): + continue + verify = course_dir / "verify.py" + if not verify.exists(): + continue + match = COURSE_ID_PATTERN.search(verify.read_text(encoding="utf-8")) + if not match: + raise SystemExit(f"{course_dir.name}: no COURSE_ID constant in verify.py") + digest = claim_receipt.solution_sha256(course_dir) + if digest is None: + continue + courses.append( + { + "slug": course_dir.name, + "course_id": match.group(1), + "solution_sha256": digest, + } + ) + return {"schema_version": 1, "courses": courses} + + +def dump(document: dict) -> str: + return json.dumps(document, ensure_ascii=False, indent=2) + "\n" + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--check", action="store_true") + args = parser.parse_args() + + rendered = dump(build(ROOT)) + if args.check: + current = OUTPUT.read_text(encoding="utf-8") if OUTPUT.exists() else "" + if current != rendered: + print("course-solutions.json is stale; regenerate it", file=sys.stderr) + return 1 + print("course-solutions.json is current") + return 0 + OUTPUT.write_text(rendered, encoding="utf-8") + print(f"wrote {OUTPUT.name} ({len(json.loads(rendered)['courses'])} courses)") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/claim_receipt.py b/tools/claim_receipt.py new file mode 100644 index 0000000..6ddf0a3 --- /dev/null +++ b/tools/claim_receipt.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +"""Shared claim-receipt production for course verify.py scripts. + +Implements the producer side of docs/CLAIM-RECEIPT.md (flypython.com) — +canonical JSON, the implementation-tree hash, and the HMAC-SHA256 signature. +Receipts are only produced when FLYPYTHON_CLAIM_SECRET is set in the +environment; without it every caller keeps its exact old behavior. + +Imported from courses/<slug>/verify.py via a repo-root sys.path insert, so a +course folder copied out of the repo still verifies — the import is optional +and receipts simply stay off. +""" + +from __future__ import annotations + +import hashlib +import hmac +import json +import secrets +from datetime import datetime, timezone +from pathlib import Path + +SECRET_ENV = "FLYPYTHON_CLAIM_SECRET" + +# Directory names never hashed into an implementation tree — caches, the +# graded suite, the reference implementation, and VCS/venv noise. +SKIP_DIRS = {"__pycache__", "tests", "solution", ".git", ".venv", "venv", "node_modules"} + + +def canonical_json(obj) -> bytes: + """docs/CLAIM-RECEIPT.md §2 — sorted keys, no whitespace, raw UTF-8.""" + return json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8") + + +def tree_sha256(root: Path) -> str: + """docs/CLAIM-RECEIPT.md §3 — sorted (path, content) pairs, NUL-separated.""" + root = Path(root) + entries: list[tuple[str, bytes]] = [] + for path in sorted(root.rglob("*")): + rel = path.relative_to(root) + if any(part in SKIP_DIRS for part in rel.parts): + continue + if not path.is_file(): + continue + entries.append((rel.as_posix(), path.read_bytes())) + entries.sort(key=lambda pair: pair[0]) + h = hashlib.sha256() + for rel, content in entries: + h.update(rel.encode("utf-8")) + h.update(b"\0") + h.update(content) + h.update(b"\0") + return h.hexdigest() + + +def solution_sha256(course_dir: Path) -> str | None: + """Hash the course's solution tree; None when there is none to compare.""" + solution = Path(course_dir) / "solution" + if not solution.is_dir(): + return None + return tree_sha256(solution) + + +def make_receipt( + course_id: str, + checkpoint_id: str, + *, + passed: bool, + tests: int, + duration_ms: int, + impl_dir: Path, + solution_hash: str | None, + secret: str, +) -> dict: + """One receipt per gated checkpoint — see the spec for field semantics.""" + impl_hash = tree_sha256(impl_dir) + receipt = { + "v": 1, + "course": course_id, + "checkpoint": checkpoint_id, + "suite": { + "passed": bool(passed), + "tests": int(tests), + "duration_ms": int(duration_ms), + }, + "impl_sha256": impl_hash, + "solution_match": (impl_hash == solution_hash) if solution_hash else False, + "created_at": datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z"), + "nonce": secrets.token_hex(16), + } + receipt["sig"] = hmac.new( + secret.encode("utf-8"), canonical_json(receipt), hashlib.sha256 + ).hexdigest() + return receipt + + +def receipts_enabled() -> str | None: + """The account secret from the environment, or None to keep old output.""" + secret = __import__("os").environ.get(SECRET_ENV, "").strip() + return secret or None + + +def count_tests(suite_output: str) -> int: + """`unittest` prints 'Ran N tests' to stderr; receipts record N.""" + import re + + match = re.search(r"Ran (\d+) test", suite_output or "") + return int(match.group(1)) if match else 0 diff --git a/tools/verify_courses.py b/tools/verify_courses.py index ada237b..0fea654 100644 --- a/tools/verify_courses.py +++ b/tools/verify_courses.py @@ -8,7 +8,10 @@ from __future__ import annotations +import hashlib +import hmac import json +import os import re import subprocess import sys @@ -17,6 +20,9 @@ ROOT = Path(__file__).resolve().parents[1] LESSON_NAME = re.compile(r"^L\d{2,}\.md$") +sys.path.insert(0, str(ROOT / "tools")) +import claim_receipt # noqa: E402 + def discover_courses(root: Path) -> list[Path]: courses_dir = root / "courses" @@ -74,6 +80,7 @@ def run_course_verifier(course: Path) -> list[str]: f"\n{(result.stdout or '') + (result.stderr or '')}".rstrip() ) problems.extend(check_progress_contract(course)) + problems.extend(check_receipt_contract(course)) return problems @@ -123,6 +130,132 @@ def check_progress_contract(course: Path) -> list[str]: return problems +def check_receipt_contract(course: Path) -> list[str]: + """FP-704: under FLYPYTHON_CLAIM_SECRET, progress --json must attach one + spec-shaped receipt per gated checkpoint with a verifiable HMAC.""" + verify = course / "verify.py" + if not verify.exists(): + return [] + name = course.name + secret = "contract-test-secret" + env = dict(os.environ, FLYPYTHON_CLAIM_SECRET=secret) + result = subprocess.run( + [sys.executable, str(verify), "progress", "--json"], + check=False, capture_output=True, text=True, env=env, + ) + if result.returncode != 0: + return [f"{name}: progress with {claim_receipt.SECRET_ENV} failed\n" + f"{(result.stdout or '') + (result.stderr or '')}".rstrip()] + try: + document = json.loads(result.stdout) + except ValueError: + return [f"{name}: progress --json with secret did not emit valid JSON"] + + receipts = document.get("receipts") + if not isinstance(receipts, list) or not receipts: + return [f"{name}: progress --json emitted no receipts under {claim_receipt.SECRET_ENV}"] + + gated = {item["id"] for item in document.get("checkpoints", []) + if item.get("kind") == "objective"} + problems: list[str] = [] + if {receipt.get("checkpoint") for receipt in receipts} != gated: + problems.append(f"{name}: receipts must cover exactly the gated checkpoints") + for receipt in receipts: + sig = receipt.pop("sig", None) + expected = hmac.new( + secret.encode(), claim_receipt.canonical_json(receipt), hashlib.sha256 + ).hexdigest() + if not isinstance(sig, str) or not hmac.compare_digest(sig, expected): + problems.append(f"{name}: receipt for {receipt.get('checkpoint')} has a bad signature") + for field in ("v", "course", "checkpoint", "suite", "impl_sha256", + "solution_match", "created_at", "nonce"): + if field not in receipt: + problems.append(f"{name}: receipt for {receipt.get('checkpoint')} missing {field!r}") + suite = receipt.get("suite") + if not isinstance(suite, dict) or not all( + key in suite for key in ("passed", "tests", "duration_ms") + ): + problems.append(f"{name}: receipt for {receipt.get('checkpoint')} has a malformed suite block") + return problems + + +# ── Shared-core enforcement (docs/courses/README.md §5.2, FP-711) ───────── +# Agent-tool courses share one exercise — the report-tool scenario skins, +# starter/solution pair, tests, and task contract. Folders stay +# self-contained (a copied course still verifies alone), so instead of a +# shared directory the members carry a `core-group` marker file and this +# check enforces byte-for-byte equality of the core. verify.py is exempt +# only inside its marked PER-COURSE BLOCK (course id, salt, titles). + +SKIP_PARTS = {"__pycache__", ".git", ".venv", "venv", "node_modules"} +CORE_FILES = ("TASK.md", "TASK_cn.md") +CORE_DIRS = ("tests", "starter", "solution", "scenario") +BLOCK_RE = re.compile( + r"# ── PER-COURSE BLOCK ─+\n.*?# ── END PER-COURSE BLOCK ─+", re.S +) + + +def _core_payloads(course: Path) -> dict[str, bytes]: + payloads: dict[str, bytes] = {} + for name in CORE_FILES: + path = course / name + if path.exists(): + payloads[name] = path.read_bytes() + for dirname in CORE_DIRS: + base = course / dirname + if not base.is_dir(): + continue + for path in sorted(base.rglob("*")): + rel = path.relative_to(course) + if not path.is_file() or any(part in SKIP_PARTS for part in rel.parts): + continue + payloads[rel.as_posix()] = path.read_bytes() + verify = course / "verify.py" + if verify.exists(): + normalized = BLOCK_RE.sub("# <per-course block stripped>", verify.read_text(encoding="utf-8")) + payloads["verify.py(normalized)"] = normalized.encode("utf-8") + return payloads + + +def check_shared_core(courses: list[Path]) -> list[str]: + groups: dict[str, list[Path]] = {} + for course in courses: + marker = course / "core-group" + if marker.exists(): + groups.setdefault(marker.read_text(encoding="utf-8").strip(), []).append(course) + + problems: list[str] = [] + for group, members in groups.items(): + if len(members) < 2: + problems.append(f"core-group {group}: only one member ({members[0].name})") + continue + reference = _core_payloads(members[0]) + for member in members[1:]: + other = _core_payloads(member) + for name in sorted(set(reference) | set(other)): + if reference.get(name) != other.get(name): + problems.append( + f"core-group {group}: {members[0].name} vs {member.name} differ on {name}" + ) + # Inside the per-course block, checkpoint ids and gates are core — + # only the display titles may differ. Compare (id, gate) shapes. + shapes = [] + for member in members: + verify = (member / "verify.py").read_text(encoding="utf-8") if (member / "verify.py").exists() else "" + if verify and not BLOCK_RE.search(verify): + problems.append(f"{member.name}: verify.py has no marked PER-COURSE BLOCK") + shapes.append(list(zip( + re.findall(r'"id":\s*"(l\d+|capstone)"', verify), + re.findall(r'"gate":\s*"([a-z-]+)"', verify), + ))) + for member, shape in zip(members[1:], shapes[1:]): + if shape != shapes[0]: + problems.append( + f"core-group {group}: {member.name} checkpoint ids/gates differ from {members[0].name}" + ) + return problems + + def main() -> int: courses = discover_courses(ROOT) if not courses: @@ -133,6 +266,7 @@ def main() -> int: for course in courses: problems.extend(check_bilingual_contract(course)) problems.extend(run_course_verifier(course)) + problems.extend(check_shared_core(courses)) if problems: for problem in problems: From b38667f419b6f9076cfa0c362117ac858e287322 Mon Sep 17 00:00:00 2001 From: pxiaoer <pxiaoer@users.noreply.github.com> Date: Sun, 13 Sep 2026 17:55:20 +0800 Subject: [PATCH 06/10] 0.0.8 A3: bare verify.py default command, standalone folders, tool-course lesson 1 rework FP-820: python verify.py (no args) is the learner's command on all 12 free courses, 3 pro courses, and the data-analysis capstone - suite status, per-checkpoint claim codes, English-first output, non-zero exit while gated checkpoints are open. Published progress --json and --receipt-out formats unchanged; the four explicit usages stay for maintainers. FP-821: verify_courses.py copies each course plus tools/claim_receipt.py into an isolated tree and enforces the default command + receipts there. FP-822: six agent-tool courses' lesson 1 (EN+ZH) rewritten to the fixed order tool -> FlyPython Skill (network on) -> agent fetches the course files; download wording removed from COURSE.md and lessons; solution no longer the completion standard. FP-823: prose checkpoints in three 0.0.3 courses became question lists for the human-in-the-loop gate. FP-824: README/README_cn start section leads with the Skill entry sentence and the six tool courses. --- CHANGELOG.md | 35 ++ README.md | 24 +- README_cn.md | 22 +- content-manifest.json | 318 +++++++++--------- courses/agent-rules-single-source/COURSE.md | 9 +- .../agent-rules-single-source/COURSE_cn.md | 7 +- .../agent-rules-single-source/lessons/L01.md | 7 +- .../lessons/L01_cn.md | 7 +- .../agent-rules-single-source/lessons/L02.md | 7 +- .../lessons/L02_cn.md | 7 +- .../agent-rules-single-source/lessons/L03.md | 7 +- .../lessons/L03_cn.md | 7 +- .../agent-rules-single-source/lessons/L04.md | 7 +- .../lessons/L04_cn.md | 7 +- .../agent-rules-single-source/lessons/L05.md | 7 +- .../lessons/L05_cn.md | 7 +- courses/agent-rules-single-source/verify.py | 50 ++- courses/da-eda/lessons/L01.md | 2 +- courses/da-eda/lessons/L01_cn.md | 2 +- courses/da-eda/lessons/L02.md | 2 +- courses/da-eda/lessons/L02_cn.md | 2 +- courses/da-eda/lessons/L03.md | 2 +- courses/da-eda/lessons/L03_cn.md | 2 +- courses/da-eda/lessons/L04.md | 2 +- courses/da-eda/lessons/L04_cn.md | 2 +- courses/da-eda/lessons/L05.md | 2 +- courses/da-eda/lessons/L05_cn.md | 2 +- courses/da-eda/verify.py | 50 ++- courses/da-report/lessons/L01.md | 2 +- courses/da-report/lessons/L01_cn.md | 2 +- courses/da-report/lessons/L02.md | 2 +- courses/da-report/lessons/L02_cn.md | 2 +- courses/da-report/lessons/L03.md | 2 +- courses/da-report/lessons/L03_cn.md | 2 +- courses/da-report/lessons/L04.md | 2 +- courses/da-report/lessons/L04_cn.md | 2 +- courses/da-report/lessons/L05.md | 2 +- courses/da-report/lessons/L05_cn.md | 2 +- courses/da-report/verify.py | 50 ++- courses/da-visualization/lessons/L01.md | 2 +- courses/da-visualization/lessons/L01_cn.md | 2 +- courses/da-visualization/lessons/L02.md | 2 +- courses/da-visualization/lessons/L02_cn.md | 2 +- courses/da-visualization/lessons/L03.md | 2 +- courses/da-visualization/lessons/L03_cn.md | 2 +- courses/da-visualization/lessons/L04.md | 2 +- courses/da-visualization/lessons/L04_cn.md | 2 +- courses/da-visualization/lessons/L05.md | 2 +- courses/da-visualization/lessons/L05_cn.md | 2 +- courses/da-visualization/verify.py | 50 ++- .../COURSE.md | 20 +- .../COURSE_cn.md | 15 +- .../REVIEW.md | 14 + .../lessons/L01.md | 94 ++++-- .../lessons/L01_cn.md | 89 +++-- .../verify.py | 50 ++- courses/hands-on-with-cursor/COURSE.md | 21 +- courses/hands-on-with-cursor/COURSE_cn.md | 17 +- courses/hands-on-with-cursor/REVIEW.md | 14 + courses/hands-on-with-cursor/lessons/L01.md | 92 +++-- .../hands-on-with-cursor/lessons/L01_cn.md | 82 +++-- courses/hands-on-with-cursor/lessons/L02.md | 2 +- .../hands-on-with-cursor/lessons/L02_cn.md | 2 +- courses/hands-on-with-cursor/lessons/L03.md | 4 +- .../hands-on-with-cursor/lessons/L03_cn.md | 4 +- courses/hands-on-with-cursor/lessons/L04.md | 4 +- .../hands-on-with-cursor/lessons/L04_cn.md | 4 +- courses/hands-on-with-cursor/verify.py | 50 ++- .../hands-on-with-deepseek-harness/COURSE.md | 20 +- .../COURSE_cn.md | 17 +- .../hands-on-with-deepseek-harness/REVIEW.md | 14 + .../lessons/L01.md | 92 +++-- .../lessons/L01_cn.md | 84 +++-- .../lessons/L02.md | 2 +- .../lessons/L02_cn.md | 2 +- .../lessons/L03.md | 4 +- .../lessons/L03_cn.md | 4 +- .../lessons/L04.md | 4 +- .../lessons/L04_cn.md | 4 +- .../hands-on-with-deepseek-harness/verify.py | 50 ++- courses/hands-on-with-kimi-code/COURSE.md | 21 +- courses/hands-on-with-kimi-code/COURSE_cn.md | 16 +- courses/hands-on-with-kimi-code/REVIEW.md | 14 + .../hands-on-with-kimi-code/lessons/L01.md | 92 +++-- .../hands-on-with-kimi-code/lessons/L01_cn.md | 84 +++-- .../hands-on-with-kimi-code/lessons/L02.md | 2 +- .../hands-on-with-kimi-code/lessons/L02_cn.md | 2 +- .../hands-on-with-kimi-code/lessons/L03.md | 4 +- .../hands-on-with-kimi-code/lessons/L03_cn.md | 4 +- .../hands-on-with-kimi-code/lessons/L04.md | 4 +- .../hands-on-with-kimi-code/lessons/L04_cn.md | 4 +- courses/hands-on-with-kimi-code/verify.py | 50 ++- courses/hands-on-with-openai-codex/COURSE.md | 21 +- .../hands-on-with-openai-codex/COURSE_cn.md | 17 +- courses/hands-on-with-openai-codex/REVIEW.md | 14 + .../hands-on-with-openai-codex/lessons/L01.md | 96 ++++-- .../lessons/L01_cn.md | 84 +++-- .../hands-on-with-openai-codex/lessons/L02.md | 2 +- .../lessons/L02_cn.md | 2 +- .../hands-on-with-openai-codex/lessons/L03.md | 4 +- .../lessons/L03_cn.md | 4 +- .../hands-on-with-openai-codex/lessons/L04.md | 4 +- .../lessons/L04_cn.md | 4 +- courses/hands-on-with-openai-codex/verify.py | 50 ++- courses/hands-on-with-zcode/COURSE.md | 21 +- courses/hands-on-with-zcode/COURSE_cn.md | 17 +- courses/hands-on-with-zcode/REVIEW.md | 14 + courses/hands-on-with-zcode/lessons/L01.md | 90 +++-- courses/hands-on-with-zcode/lessons/L01_cn.md | 82 +++-- courses/hands-on-with-zcode/lessons/L02.md | 2 +- courses/hands-on-with-zcode/lessons/L02_cn.md | 2 +- courses/hands-on-with-zcode/lessons/L03.md | 4 +- courses/hands-on-with-zcode/lessons/L03_cn.md | 4 +- courses/hands-on-with-zcode/lessons/L04.md | 4 +- courses/hands-on-with-zcode/lessons/L04_cn.md | 4 +- courses/hands-on-with-zcode/verify.py | 50 ++- courses/mcp-server-in-python/COURSE.md | 9 +- courses/mcp-server-in-python/COURSE_cn.md | 7 +- courses/mcp-server-in-python/lessons/L01.md | 7 +- .../mcp-server-in-python/lessons/L01_cn.md | 7 +- courses/mcp-server-in-python/lessons/L02.md | 7 +- .../mcp-server-in-python/lessons/L02_cn.md | 7 +- courses/mcp-server-in-python/lessons/L03.md | 7 +- .../mcp-server-in-python/lessons/L03_cn.md | 7 +- courses/mcp-server-in-python/lessons/L04.md | 7 +- .../mcp-server-in-python/lessons/L04_cn.md | 7 +- courses/mcp-server-in-python/lessons/L05.md | 7 +- .../mcp-server-in-python/lessons/L05_cn.md | 7 +- courses/mcp-server-in-python/verify.py | 50 ++- courses/verifying-ai-generated-code/COURSE.md | 9 +- .../verifying-ai-generated-code/COURSE_cn.md | 7 +- .../lessons/L01.md | 7 +- .../lessons/L01_cn.md | 7 +- .../lessons/L02.md | 7 +- .../lessons/L02_cn.md | 7 +- .../lessons/L03.md | 7 +- .../lessons/L03_cn.md | 7 +- .../lessons/L04.md | 7 +- .../lessons/L04_cn.md | 7 +- .../lessons/L05.md | 7 +- .../lessons/L05_cn.md | 7 +- courses/verifying-ai-generated-code/verify.py | 50 ++- paths/data-analysis/capstone/verify.py | 2 + tools/verify_courses.py | 105 ++++++ 144 files changed, 2154 insertions(+), 738 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 435ee83..abdc88e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,41 @@ This file records notable catalog-contract and maintenance changes. ## [Unreleased] +0.0.8 — Skill as the main entry (companion-repository half; see the +website plan `docs/product-and-growth-plan-0.0.8.md`). + +### Added + +- FP-820: bare `python verify.py` is the learner's default command on + every course and the data-analysis capstone — runs the suites, prints + per-checkpoint status and earned claim codes (English first, Chinese + after), exits non-zero while gated checkpoints are open. The four + explicit usages remain maintainer checks; the published + `progress --json` / `--receipt-out` formats are unchanged. +- FP-821: `tools/verify_courses.py` now copies each course folder plus + the shared `tools/claim_receipt.py` into an isolated tree and runs the + default command and the receipts contract there — a course fetched via + the files endpoint verifies alone. + +### Changed + +- FP-822: the six agent-tool courses' lesson 1 (EN+ZH) is rewritten + around the fixed order get-the-tool-running → install the FlyPython + Skill (network access included) → let the agent fetch the course + files; every "download this folder" wording is gone from `COURSE.md`, + `COURSE_cn.md`, and the lessons, and `verify.py solution` is no longer + presented as a completion standard. +- FP-823: the prose checkpoints of `agent-rules-single-source`, + `mcp-server-in-python`, and `verifying-ai-generated-code` are question + lists the agent can ask directly at the human-in-the-loop gate; + data-analysis courses keep their concrete checkpoint criteria and + frontmatter hint questions. +- FP-824: both READMEs' "Start in three minutes" leads with the Skill + entry sentence and the six agent-tool courses instead of a clone + + download flow. + +## [Unreleased-0.0.4-phase-A] + 0.0.4 phase A — challenge-platform rework on top of 0.1.0. ### Added diff --git a/README.md b/README.md index 093d2c9..4d07d99 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,24 @@ versions into a browsable learning experience. ## Start in three minutes -No installation is required beyond Python 3.11+ (development pins 3.12 -through `.python-version`): +You need a coding agent that can run commands and reach the network +(Claude Code, the Codex app, Cursor, DeepSeek Harness, Kimi Code, or +ZCode — chat-only web AIs cannot run these courses). Install the +[FlyPython Skill](https://flypython.com/skills/flypython/SKILL.md) in +your agent, then paste this one sentence: + +> Read https://flypython.com/skills/flypython/SKILL.md and start the FlyPython course `da-eda`. + +The agent authorizes you with a one-time link (you never hand it a +password), fetches the course files itself — you download nothing — and +drives the challenges with you. New to driving an agent? Start with the +tool course for *your* agent: [Hands-on Python with Claude Code](courses/hands-on-python-with-claude-code/COURSE.md), +[Codex app](courses/hands-on-with-openai-codex/COURSE.md), [Cursor](courses/hands-on-with-cursor/COURSE.md), +[DeepSeek Harness](courses/hands-on-with-deepseek-harness/COURSE.md), +[Kimi Code](courses/hands-on-with-kimi-code/COURSE.md), or +[ZCode](courses/hands-on-with-zcode/COURSE.md). + +Maintainers can still reproduce an example locally without any agent: ```bash git clone https://github.com/flypythoncom/python.git @@ -35,10 +51,6 @@ python examples/product-slug/verify.py starter --expect-failure python examples/product-slug/verify.py solution ``` -You just reproduced a bug and verified its fixed behavior. Now give the -[task contract](examples/product-slug/TASK.md) and [agent rules](templates/AGENT_RULES.example.md) -to a coding agent (Cursor, Windsurf, Claude Code, Copilot) and make the starter pass without copying the solution. - ## Choose what you need to accomplish | Goal | Start here | What you will produce | diff --git a/README_cn.md b/README_cn.md index ccc596d..fd637d6 100644 --- a/README_cn.md +++ b/README_cn.md @@ -22,7 +22,23 @@ FlyPython 是一个面向实践的双语仓库,帮助你写好 Python,并把 ## 3 分钟开始 -只需要 Python 3.11+(开发环境通过 `.python-version` 固定在 3.12): +你需要一个能执行命令并联网的编码 Agent(Claude Code、Codex 应用、 +Cursor、DeepSeek Harness、Kimi Code 或 ZCode——只能聊天的网页 AI 做 +不了这些课程)。在你的 Agent 里装上 +[FlyPython Skill](https://flypython.com/skills/flypython/SKILL.md), +然后粘贴这一句话: + +> Read https://flypython.com/skills/flypython/SKILL.md and start the FlyPython course `da-eda`. + +Agent 会用一次性授权链接为你授权(你不用把密码交给它),自己取回 +课程文件——你不下载任何东西——并陪你完成挑战。还没用过编码 Agent? +从*你的*工具对应的工具课开始:[Claude Code 实战](courses/hands-on-python-with-claude-code/COURSE_cn.md)、 +[Codex 应用](courses/hands-on-with-openai-codex/COURSE_cn.md)、[Cursor](courses/hands-on-with-cursor/COURSE_cn.md)、 +[DeepSeek Harness](courses/hands-on-with-deepseek-harness/COURSE_cn.md)、 +[Kimi Code](courses/hands-on-with-kimi-code/COURSE_cn.md)、 +[ZCode](courses/hands-on-with-zcode/COURSE_cn.md)。 + +维护者仍然可以不借助任何 Agent 在本地复现示例: ```bash git clone https://github.com/flypythoncom/python.git @@ -31,10 +47,6 @@ python examples/product-slug/verify.py starter --expect-failure python examples/product-slug/verify.py solution ``` -你刚刚复现了一个 Bug,并验证了修复后的行为。接下来把[任务契约](examples/product-slug/TASK_cn.md) -与 [Agent 规则模板](templates/AGENT_RULES.example_cn.md) 提供给 Coding Agent(如 Cursor、Windsurf、Claude Code、Copilot), -让 starter 通过测试,但先不要复制 solution。 - ## 选择你现在要完成的事 | 目标 | 从这里开始 | 最终产出 | diff --git a/content-manifest.json b/content-manifest.json index 748e339..07e3d1c 100644 --- a/content-manifest.json +++ b/content-manifest.json @@ -50,7 +50,7 @@ { "id": "course-agent-rules", "type": "course", - "content_version": 2, + "content_version": 3, "status": "reviewed", "reviewed_on": "2026-09-12", "locales": [ @@ -59,14 +59,14 @@ "path": "courses/agent-rules-single-source/COURSE.md", "title": "One source of truth for agent rules", "summary": "Stop maintaining diverging AGENTS.md, CLAUDE.md, and .cursorrules files — build a checker that proves your repository has exactly one rule source, taught hands-on with your coding agent.", - "sha256": "d7dd7ed26db699c576046c2e103d6d94a2299805dca0d6b6153a4d6ccafcba6e" + "sha256": "ea30434d93440708952bd4acb895efb5c8b59bf9764ccddeaab53f97eaa283e8" }, { "lang": "zh-CN", "path": "courses/agent-rules-single-source/COURSE_cn.md", "title": "Agent 规则的单一真源", "summary": "不再同时维护彼此打架的 AGENTS.md、CLAUDE.md 与 .cursorrules——亲手构建一个能证明仓库只有一处规则真源的检查器,由你的编码 Agent 授课。", - "sha256": "bf81ca5856328e1ef4f204707ad002f49704486ab2589d3b52cf366285e8222f" + "sha256": "5b11e590514fe81dc01b6ad4555a55abfc3b730ca4d14ff1fa4cd5b50fb15bb9" } ] }, @@ -82,14 +82,14 @@ "path": "courses/agent-rules-single-source/lessons/L01.md", "title": "Measure the drift you already have", "summary": "Run the checker against the three scenario repositories and read drift as a test failure, not a vibe.", - "sha256": "cd923a835bc51166ca622ef61414b1dfd75eacc70c649307ab3a559b968617c3" + "sha256": "3e8ccfdeaeca11b1b57c53cbc5a5c1a9fbe93ae41eda9e683c1a8d3abe857e47" }, { "lang": "zh-CN", "path": "courses/agent-rules-single-source/lessons/L01_cn.md", "title": "量化你已经存在的漂移", "summary": "对三个场景仓库运行检查器,把漂移读成测试失败,而不是一种感觉。", - "sha256": "c5f88441065a1bc5208b005c083dd0b231da4a281f0b5bb97710e55280daed6f" + "sha256": "f90fd475497139dfa3ca98d50a5d6abb604d384f283da5f36c65ee120452a8a6" } ] }, @@ -105,14 +105,14 @@ "path": "courses/agent-rules-single-source/lessons/L02.md", "title": "Decide the source of truth", "summary": "AGENTS.md becomes the one file agents read; everything else is a pointer or a copy — decide, then write it down.", - "sha256": "298d598f44af59a493f44a402d3faf9ab384a4f83088509bcfae70fc7a006999" + "sha256": "9144cf869703c4d7b94d464143dc026c6a1031158a76d4dcafd47e28b9646b02" }, { "lang": "zh-CN", "path": "courses/agent-rules-single-source/lessons/L02_cn.md", "title": "确定唯一真源", "summary": "AGENTS.md 成为 Agent 唯一阅读的文件;其余都只能是指针或副本——先决定,再写下来。", - "sha256": "1a9a3a076663df37d3f45c110ad4de1b805db7a220dcdefb413d9eb241748ce1" + "sha256": "e275ea26dd9d4fd044f788a875533d0c47c7eff9eddc6e986eefecb70276b3df" } ] }, @@ -128,14 +128,14 @@ "path": "courses/agent-rules-single-source/lessons/L03.md", "title": "Drive a bounded change, test by test", "summary": "Make the starter detect source, pointer, copy, and drift — one failing test group at a time.", - "sha256": "b1b6dacafce0158965fc1721910514ceede367fd1b0d213adb8163d9e72dc4fc" + "sha256": "6c6d008258ce483f52b4f8c826a2c221ada0b1567faede6c41ee87c361aab77d" }, { "lang": "zh-CN", "path": "courses/agent-rules-single-source/lessons/L03_cn.md", "title": "按测试驱动一次有边界的变更", "summary": "让 starter 依次识别真源、指针、副本与漂移——一次一组失败测试。", - "sha256": "4a58994b8080514e435124259416d325af4622275e5e3a70ecc0564f8949969d" + "sha256": "d5c62d126ee58d2b381cbe3ee0a5cbe5d8298cb1f256aae6162f7fbde6c1f39a" } ] }, @@ -151,14 +151,14 @@ "path": "courses/agent-rules-single-source/lessons/L04.md", "title": "Verify and wire it in", "summary": "Prove both verifier directions, run all three skins, then add the checker to your repository's review loop.", - "sha256": "24245a7159551c4565456f9f702a37595dcb895a2b153f6ea9191d9e2229521d" + "sha256": "6cafad36be88ca25059da2b3ddee675278a06b2881067f8292506ccfc1ee6fcd" }, { "lang": "zh-CN", "path": "courses/agent-rules-single-source/lessons/L04_cn.md", "title": "验证并接入流程", "summary": "证明验证器双向通过、跑完三个皮肤,然后把检查器接入你仓库的审查环。", - "sha256": "bcdc8ecf6d6f864c157f81339f73517e8f3b76d94b23e8446d9737d415f3882b" + "sha256": "67eb91c39cb750aaa8e6f147f2977ba727aed20de2c33d8056da2fc62d3bcb02" } ] }, @@ -174,21 +174,21 @@ "path": "courses/agent-rules-single-source/lessons/L05.md", "title": "Apply this to your repository", "summary": "One AGENTS.md, thin pointers everywhere else, and a failing check when it breaks.", - "sha256": "ce747d851a4844298b00415593f2153510605f0f0a69f3e289e7acb3370a0082" + "sha256": "65125d7478161c64152fe4ba65c0d5922b98db0e65054ca3688880042c1707bc" }, { "lang": "zh-CN", "path": "courses/agent-rules-single-source/lessons/L05_cn.md", "title": "把这套方法带回你的仓库", "summary": "一个 AGENTS.md,其余全是瘦指针,一旦破坏检查就失败。", - "sha256": "f3dd9ef2c01e90f90897ebbdf7c048961fed8469173fa8a92bb1617eb353213f" + "sha256": "f62e618183d27ced9c5c3de84b4ab7eaab5afccc3a002f1747fb004da7a0ede4" } ] }, { "id": "course-claude-code", "type": "course", - "content_version": 2, + "content_version": 3, "status": "reviewed", "reviewed_on": "2026-09-12", "locales": [ @@ -197,37 +197,37 @@ "path": "courses/hands-on-python-with-claude-code/COURSE.md", "title": "Hands-on Python with Claude Code", "summary": "An agent-taught course that takes you from a cloned folder to a tested, verified Python report tool using Claude Code — including the task contract, the bounded change, and the objective pass/fail evidence.", - "sha256": "50165b3b9aa9979d565ab34057977b7b7264cec739be1900ec3988876731c132" + "sha256": "44b874c07240c58bf7d3b240fa45fe52a0981802d2b4bbe95900567f20c12d69" }, { "lang": "zh-CN", "path": "courses/hands-on-python-with-claude-code/COURSE_cn.md", "title": "用 Claude Code 实战 Python", "summary": "一门由 Agent 授课的课程:从下载课程文件夹到交付一个经过测试与验证的 Python 报表工具——包括任务契约、最小变更和客观的通过/失败证据。", - "sha256": "8c73c31d5901ff9bbe9e79e97342688b6737657ab05f1b71a767125ca398e2d3" + "sha256": "6c385a6554c914efabb37ec5b94f7f65c194094cb75d353db0bba2a35da0c6d4" } ] }, { "id": "course-claude-code-l01", "type": "course", - "content_version": 1, + "content_version": 2, "status": "reviewed", "reviewed_on": "2026-09-12", "locales": [ { "lang": "en-US", "path": "courses/hands-on-python-with-claude-code/lessons/L01.md", - "title": "Lesson 1: Reproduce the failure before you fix anything", - "summary": "Clone the folder, meet the starter, and make the failure objective with verify.py — the habit that separates \"it runs\" from \"it ships\".", - "sha256": "824df537303329d4b3b48e3d4e03402a3025e8de6d38f85d2e8e8701b3a700c6" + "title": "Lesson 1: Set up Claude Code, install the FlyPython Skill, fetch the course", + "summary": "Get Claude Code running, install the FlyPython Skill with network access approved, and let the agent fetch this course's files — you download nothing. Then make the starter's failure objective.", + "sha256": "8979f72d7ad8b886bbe954e2af2dba4c30886104bbfa83f61470d6375b45226c" }, { "lang": "zh-CN", "path": "courses/hands-on-python-with-claude-code/lessons/L01_cn.md", - "title": "第 1 课:先复现故障,再谈修复", - "summary": "打开课程文件夹,认识 starter,用 verify.py 把\"坏了\"变成客观证据——这是\"能跑\"与\"能交付\"的分水岭。", - "sha256": "055e8764b490399ce1a107448f5a8e9471aabba91d239448894f96d76a74b3f7" + "title": "第 1 课:装好 Claude Code、装上 FlyPython Skill、让 Agent 取回课程", + "summary": "先把 Claude Code 跑起来,装好 FlyPython Skill 并打开联网权限,让 Agent 取回本课文件——你不下载任何东西,然后让 starter 的失败变得客观。", + "sha256": "a038f7ff91b277d640aaf078d0a93fc9dd84431e59d3ff842d4ab9a3eb492e61" } ] }, @@ -326,7 +326,7 @@ { "id": "course-codex-cli", "type": "course", - "content_version": 3, + "content_version": 4, "status": "reviewed", "reviewed_on": "2026-09-13", "locales": [ @@ -335,37 +335,37 @@ "path": "courses/hands-on-with-openai-codex/COURSE.md", "title": "Hands-on with the OpenAI Codex App", "summary": "The verified Python workflow — task contract, bounded change, objective verify.py evidence — taught hands-on in the Codex desktop app, where an agent thread runs the loop and you supervise diffs and approvals.", - "sha256": "7a0648d4b0547e4b8a39ccf7dea8de2aae02908d24c26880a2520e50c2252651" + "sha256": "da4f82fcfce2751c335d1a140cc2e55fafa9f6b1b5353f7925f5ca342ebc6da7" }, { "lang": "zh-CN", "path": "courses/hands-on-with-openai-codex/COURSE_cn.md", "title": "上手 OpenAI Codex 应用", "summary": "经过验证的 Python 工作流——任务契约、有界改动、客观 verify.py 证据——在 Codex 桌面应用里手把手教:由 Agent 线程跑闭环,你监督 diff 与审批。", - "sha256": "68338e68b7145fc324472e05c6b8e649fae10cd88861510f8e7fe3dba0be6121" + "sha256": "481e2e7fa7873b9695f5ad4c9427ea68f456b54ce6901cb9f01ac57b3f2cf6a3" } ] }, { "id": "course-codex-cli-l01", "type": "course", - "content_version": 2, + "content_version": 3, "status": "reviewed", "reviewed_on": "2026-09-13", "locales": [ { "lang": "en-US", "path": "courses/hands-on-with-openai-codex/lessons/L01.md", - "title": "Lesson 1: Reproduce the failure in a Codex thread", - "summary": "Open the folder as a Codex project, start your first thread, and make the failure objective with verify.py — inside the app, not beside it.", - "sha256": "8ff98ae641562d417a00c68650c5a4b48d4d04af52fc97f1368ad566e620d25b" + "title": "Lesson 1: Set up the Codex app, install the FlyPython Skill, fetch the course", + "summary": "Get the Codex app running, install the FlyPython Skill with network access approved, and let the agent fetch this course's files — you download nothing. Then make the starter's failure objective.", + "sha256": "4b3aa0b156a613504be0a7e4d098e82ba5c1158fd15c5a2a10b6015f5208d16e" }, { "lang": "zh-CN", "path": "courses/hands-on-with-openai-codex/lessons/L01_cn.md", - "title": "第 1 课:在 Codex 线程里复现故障", - "summary": "把文件夹作为 Codex 项目打开,开启第一个线程,用 verify.py 让失败变得客观——在应用内完成,而不是在应用旁边。", - "sha256": "d8758b7e3ba9ba3dcc8bb731f84035128d4bfec826615467ee9ca4331c773268" + "title": "第 1 课:装好 the Codex app、装上 FlyPython Skill、让 Agent 取回课程", + "summary": "先把 the Codex app 跑起来,装好 FlyPython Skill 并打开联网权限,让 Agent 取回本课文件——你不下载任何东西,然后让 starter 的失败变得客观。", + "sha256": "e5623346801e89ef88bc8a7970583ab9f17e9cfbc45b831e5db13b302e1de380" } ] }, @@ -381,14 +381,14 @@ "path": "courses/hands-on-with-openai-codex/lessons/L02.md", "title": "Lesson 2: Write the task contract, let AGENTS.md carry the rules", "summary": "Turn \"make it better\" into a bounded, testable contract — then make it durable so every Codex thread starts with the same rules.", - "sha256": "d7180997e42e69bd8c80976287f4f9fc394492a729a2e87ff7f8134ea98e5d33" + "sha256": "e8a8c74a0ae01e129101db976e812f12e4f129820d3aabca648272d9d95e0e85" }, { "lang": "zh-CN", "path": "courses/hands-on-with-openai-codex/lessons/L02_cn.md", "title": "第 2 课:写任务契约,让 AGENTS.md 承载规则", "summary": "把「改好一点」变成有边界、可测试的契约——再让它持久化,让每个 Codex 线程都从同一套规则开始。", - "sha256": "3c817a30bd204b6b6907336616f5c88228f3bb0d9aa1561e08a77f345f9971e8" + "sha256": "3fc7dfa8bff4401c45f36fb45f0062184eccf57b4730b1cc8ce6e9013f80d965" } ] }, @@ -404,14 +404,14 @@ "path": "courses/hands-on-with-openai-codex/lessons/L03.md", "title": "Lesson 3: The bounded change, reviewed hunk by hunk", "summary": "Drive the starter to green through bounded turns — and use the app's diff review and approvals to keep every change yours.", - "sha256": "0cb1e5e60b38236ac2c477155d422417e692c038a210f995922c0e7dc387c6cd" + "sha256": "278defc13c2bfa1fdd2328c34854a2ec4389a0b0d591c02ee3556e6b0812f521" }, { "lang": "zh-CN", "path": "courses/hands-on-with-openai-codex/lessons/L03_cn.md", "title": "第 3 课:有界改动,逐块审 diff", "summary": "通过有界的轮次把 starter 推到全绿——并用应用内的 diff 审查与审批机制守住每一处改动。", - "sha256": "be8d68a96423506755e081aca4729ca7011cfc576f3998cad69abd8062760480" + "sha256": "a3c4191eb8e5aa672c4360caaf658d02936522b5a35c8cc44b6714f56542eb61" } ] }, @@ -427,14 +427,14 @@ "path": "courses/hands-on-with-openai-codex/lessons/L04.md", "title": "Lesson 4: Verify like a reviewer, not a spectator", "summary": "Green tests are the floor, not the ceiling — run both suites, walk the scenario skins, and review the diff the way you would review a colleague's.", - "sha256": "b99b86470990cc15cd9d740cc88e0076b6cc7f51793e26627f98cb2bfff45506" + "sha256": "751c405fefd78f70b46967958f78d51e0c9175725741a7cdf45d81720c0701bd" }, { "lang": "zh-CN", "path": "courses/hands-on-with-openai-codex/lessons/L04_cn.md", "title": "第 4 课:像评审者一样验证,别当观众", "summary": "测试全绿是地板不是天花板——两个套件都跑、三个皮肤都走一遍、按评审同事的标准审这份 diff。", - "sha256": "5e9994c4288119ddc388d9b75fb739ef13ac13255304903c8f9d6fb12df9645f" + "sha256": "7c3d2ff74ea861133208b96060e31d7dc62bfad121df5c74f944edd1f4ed0529" } ] }, @@ -464,7 +464,7 @@ { "id": "course-cursor", "type": "course", - "content_version": 1, + "content_version": 2, "status": "reviewed", "reviewed_on": "2026-09-13", "locales": [ @@ -473,37 +473,37 @@ "path": "courses/hands-on-with-cursor/COURSE.md", "title": "Hands-on with Cursor", "summary": "The verified Python workflow — task contract, bounded change, objective verify.py evidence — taught hands-on in Cursor, where Agent mode runs the loop and you supervise diffs and project rules.", - "sha256": "e4543bbb100c2f4dd645c135b0f140cc09c51051c09a5825c229022cb985bdf4" + "sha256": "242e0a0838ba61eadf6ed1b2e88c87989e34cd47dfc5b569f6b1455b3139c3b6" }, { "lang": "zh-CN", "path": "courses/hands-on-with-cursor/COURSE_cn.md", "title": "上手 Cursor", "summary": "经过验证的 Python 工作流——任务契约、有界改动、客观 verify.py 证据——在 Cursor 里手把手教:由 Agent 模式跑闭环,你监督 diff 与项目规则。", - "sha256": "a40bf93f87c12fbbc46c0895db42279caa74e0325518f3d4a6561efd7518b338" + "sha256": "567a8e1a76d5be9b72aa0dd445af38159132cb5660246a74053d634dc75b8d9d" } ] }, { "id": "course-cursor-l01", "type": "course", - "content_version": 1, + "content_version": 2, "status": "reviewed", "reviewed_on": "2026-09-13", "locales": [ { "lang": "en-US", "path": "courses/hands-on-with-cursor/lessons/L01.md", - "title": "Lesson 1: Reproduce the failure inside Cursor", - "summary": "Open the folder as a Cursor project, let Agent mode read the teaching contract, and make the failure objective with verify.py — inside the editor, not beside it.", - "sha256": "176848098e3b8a64cbd312c6a2e4a2cb49e4fcaa855b17ea43cc9be651922ab0" + "title": "Lesson 1: Set up Cursor, install the FlyPython Skill, fetch the course", + "summary": "Get Cursor running, install the FlyPython Skill with network access approved, and let the agent fetch this course's files — you download nothing. Then make the starter's failure objective.", + "sha256": "ecd44f10871f0b2eaa37b742647ad911ff53c66b5f58e31a9c1d6bdd0637c9d3" }, { "lang": "zh-CN", "path": "courses/hands-on-with-cursor/lessons/L01_cn.md", - "title": "第 1 课:在 Cursor 里复现故障", - "summary": "把文件夹作为 Cursor 项目打开,让 Agent 模式读教学契约,用 verify.py 让失败变得客观——在编辑器内完成,而不是在编辑器旁边。", - "sha256": "c07cec1f61a0ac751f50804e793ddb1a4de6026fd7d2b87f2955a2cabf27d3b2" + "title": "第 1 课:装好 Cursor、装上 FlyPython Skill、让 Agent 取回课程", + "summary": "先把 Cursor 跑起来,装好 FlyPython Skill 并打开联网权限,让 Agent 取回本课文件——你不下载任何东西,然后让 starter 的失败变得客观。", + "sha256": "2eab233ad6f2ed9acac57c7155e6be2cedb8dc6195c909c2bde7d651c07c3e43" } ] }, @@ -519,14 +519,14 @@ "path": "courses/hands-on-with-cursor/lessons/L02.md", "title": "Lesson 2: Write the task contract, let .cursor/rules carry the rules", "summary": "Turn \"make it better\" into a bounded, testable contract — then make it durable so every Cursor session starts with the same rules.", - "sha256": "aaf059429373464079c242dcf7946616bc7b40b0a1ba07480950ed5f8ba0dfaa" + "sha256": "f2fb837f9239298608358381309bdaeceec9b1190b0332736ecdaf108b90e40c" }, { "lang": "zh-CN", "path": "courses/hands-on-with-cursor/lessons/L02_cn.md", "title": "第 2 课:写任务契约,让 .cursor/rules 承载规则", "summary": "把「改好一点」变成有边界、可测试的契约——再让它持久化,让每个 Cursor 会话都从同一套规则开始。", - "sha256": "dd6012d7884e95644dd5ea59896cade1e8d572b503639a832445d141d9e475b1" + "sha256": "0c9f2e3745a2b84910793bf4717ed483442cc0d88afae5b1c1eecd1fd93cc45f" } ] }, @@ -542,14 +542,14 @@ "path": "courses/hands-on-with-cursor/lessons/L03.md", "title": "Lesson 3: The bounded change, reviewed hunk by hunk", "summary": "Drive the starter to green through bounded turns — and use Cursor's diff review to keep every change yours.", - "sha256": "ff0006c574cee6f659e8ab72f4462b3c069bb858556819f9ba1ec83ea4bf7a66" + "sha256": "e99ab81a589813b738d23dcebb1e173966045636ede917ca1ce2c656705269f4" }, { "lang": "zh-CN", "path": "courses/hands-on-with-cursor/lessons/L03_cn.md", "title": "第 3 课:有界改动,逐块审 diff", "summary": "通过有界的轮次把 starter 推到全绿——并用 Cursor 的 diff 审查守住每一处改动。", - "sha256": "753c49006c87d870fd5927ce91b52324f321f67cde01dbafcc263c320fdacad9" + "sha256": "035d38ac6d769dc34cb67adea20e92a335176f4de21f3c3f186af09fd37df62a" } ] }, @@ -565,14 +565,14 @@ "path": "courses/hands-on-with-cursor/lessons/L04.md", "title": "Lesson 4: Verify like a reviewer, not a spectator", "summary": "Green tests are the floor, not the ceiling — run both suites, walk the scenario skins, and review the accumulated diff the way you would review a colleague's.", - "sha256": "13ae3ea383d7dfd939d5a02028a6fa2e813cc5893b4a4bbdf1b294ff539141ef" + "sha256": "7ef2e4d42c31593047cd3985b5ad45b7afa096153dfd7cb1ab51b8be715453ce" }, { "lang": "zh-CN", "path": "courses/hands-on-with-cursor/lessons/L04_cn.md", "title": "第 4 课:像评审者一样验证,别当观众", "summary": "测试全绿是地板不是天花板——两个套件都跑、三个皮肤都走一遍、按评审同事的标准审这份累积 diff。", - "sha256": "54b31f4c2b8438d71a2c0dd4504deca2496c861d2e92e935f9a65e8c665f8d05" + "sha256": "93b63e90fc05fadf71db3371c5a9ca63ef7f643624328c2b78f0b714a111e799" } ] }, @@ -634,14 +634,14 @@ "path": "courses/da-eda/lessons/L01.md", "title": "Challenge 01: What messy data looks like", "summary": "Profile the shop export before touching pandas: exact duplicates, unparseable amounts, one bad date — count the dirt first.", - "sha256": "8c57798d7811e0ee23535e79bd03d17c963d25396892b78eb69047b4e290bfa4" + "sha256": "5160d16d0a8d8a66a72d871c869756959a4c2150986036b0c948f7231338abd0" }, { "lang": "zh-CN", "path": "courses/da-eda/lessons/L01_cn.md", "title": "挑战 01:脏数据长什么样", "summary": "动手写 pandas 之前先给商店导出数据画像:完全重复、无法解析的金额、一条坏日期——先把脏数清楚。", - "sha256": "735a4a37b713c45c4ae342afcddbf7f9410dfd8019b5833b8db608a673153448" + "sha256": "44ca5cd6ae144929a1f1143ed2cd3880b0d2119a7de3d86c3bacaf5954caca65" } ] }, @@ -657,14 +657,14 @@ "path": "courses/da-eda/lessons/L02.md", "title": "Challenge 02: Define the answer first", "summary": "Sketch results.json — keys, types, rounding — before letting the agent write eda.py.", - "sha256": "4383de459c198d45aa36c148cf5239d32f8e3061855fd91ce4043011acf94d79" + "sha256": "2fd93c0dfee425c9aa0280e59492da6011f0871d71a516198db07e8ba56a5a45" }, { "lang": "zh-CN", "path": "courses/da-eda/lessons/L02_cn.md", "title": "挑战 02:先定答案", "summary": "先勾出 results.json 的键、类型与舍入规则,再让 Agent 写 eda.py。", - "sha256": "70a68166ca660c84362157c8bea7e92abd16e22827f07f20a6cf1a5ca650e6d4" + "sha256": "ba8914add01f3042caaff485e107b6fb6445b4c0810729100a7c1a39df931237" } ] }, @@ -680,14 +680,14 @@ "path": "courses/da-eda/lessons/L03.md", "title": "Challenge 03: Clean and summarize", "summary": "Implement load_transactions, clean_transactions, summarize, write_results, main until all twelve tests pass.", - "sha256": "347a34a7afa47c33b3db027e1fcb0824fb1e0de313281d03b5e241e7a8b72fee" + "sha256": "d69ed99baf5a80b7a91cd2336f1ca37e5b5f3ad164edf3c9326918fa5d5b833b" }, { "lang": "zh-CN", "path": "courses/da-eda/lessons/L03_cn.md", "title": "挑战 03:清洗与统计", "summary": "实现 load_transactions、clean_transactions、summarize、write_results、main,十二个测试全绿。", - "sha256": "98b835ab40e7c9bf537a6c20b9ee7783a69761f71493699b9793a4e84bc6e40f" + "sha256": "8a08b92ce945a433a20ecf2dc2a5923ec3c27031eb0aed765160bafef21eff45" } ] }, @@ -703,14 +703,14 @@ "path": "courses/da-eda/lessons/L04.md", "title": "Challenge 04: Check against ground truth", "summary": "The suite asserts exact numbers — 142 in, 137 clean, revenue 33347.89, top category Books; plausible-but-different fails.", - "sha256": "bed37fda0b4c2e51adf9f6f5f9adeb0479e7089ce9de29786b2857e5904ce2d8" + "sha256": "da1727dea278986ba6c718f123a747a48318f69553a8398505328df7e9bda7c2" }, { "lang": "zh-CN", "path": "courses/da-eda/lessons/L04_cn.md", "title": "挑战 04:核对真值", "summary": "套件断言精确数字——142 进 137 净、营收 33347.89、品类第一 Books;看着合理但不同即失败。", - "sha256": "fe7c5b6dd9d8ee37db271b152b21a647565c3a913ae6953d680d43a36a1ff381" + "sha256": "79cfeb70ff534f352174cef72bc12ebd4ab1ae34dddbfa5253af6d6b163f2fe5" } ] }, @@ -726,14 +726,14 @@ "path": "courses/da-eda/lessons/L05.md", "title": "Challenge 05: Run it on your own data", "summary": "Port the pipeline to a real export you own and record which assumptions break.", - "sha256": "58eed4e2eb41ce3771a9508e772d003279d4664a92a6a1bc8f7db50675a6e4c6" + "sha256": "7aea36acb4baeea5abf3f6f4eb853025f0a359e73ccb5154549548da85c9f246" }, { "lang": "zh-CN", "path": "courses/da-eda/lessons/L05_cn.md", "title": "挑战 05:换成你自己的数据", "summary": "把流水线搬到你自己一份真实导出上,记下哪些假设崩了。", - "sha256": "e8b859e7ff90b5c13f354374a14fb6f40bd1efef1bc3f440ca8889bdea47cbec" + "sha256": "1a75fa8195e74eb054281f6f3976bcd10b4252ff56eaac231df9b974736d0d03" } ] }, @@ -772,14 +772,14 @@ "path": "courses/da-report/lessons/L01.md", "title": "Challenge 01: A report is a contract with the reader", "summary": "List which numbers the report must carry and where each comes from in results.json.", - "sha256": "0aedc261a20d54aa9fb79c6b294f5710b52bf7df421efb18c3ce913db52a542f" + "sha256": "edca71e61fa83e86da5d4e7188a713741bbae666e132fd5102eebf6d9618c612" }, { "lang": "zh-CN", "path": "courses/da-report/lessons/L01_cn.md", "title": "挑战 01:报告是与读者的契约", "summary": "列出报告必须携带哪些数字,以及各自来自 results.json 的哪个字段。", - "sha256": "e0a0df3a5e537aa2f58feb5552d47ae3f1ba87cd0fe965ebbe440767ef189204" + "sha256": "80b854d5871e09629d8231b0bda5401a203510b39739f5e2264b0544562358cb" } ] }, @@ -795,14 +795,14 @@ "path": "courses/da-report/lessons/L02.md", "title": "Challenge 02: Structure before prose", "summary": "Title, exact section order Data quality → Findings → Appendix, and the metrics block — before any sentence.", - "sha256": "b307e17338d44f3860b29fbedd3f12365a23ff3d28bac7fed0aacb8ba6590292" + "sha256": "ec7b1721d60834402918d00d749cf1a3a87c6a081de83326fbf7334c929613b9" }, { "lang": "zh-CN", "path": "courses/da-report/lessons/L02_cn.md", "title": "挑战 02:先结构,后文字", "summary": "标题、Data quality → Findings → Appendix 的确切顺序、metrics 块——先于任何句子。", - "sha256": "a48efa135060f1e092dbecc061fcaedf5babb0710fbaaa1afdb28bfb0a51a992" + "sha256": "9b1202017349afdbacc3bb81bb59c9f282dd5ff6e6abaf9bab664f2d4b2d38c3" } ] }, @@ -818,14 +818,14 @@ "path": "courses/da-report/lessons/L03.md", "title": "Challenge 03: Build and render the report", "summary": "Carry metrics through unchanged, emit # title then ## sections in order, money at two decimals.", - "sha256": "3b9eb25b0c3377062114cdfff2ad560d6ba9241ccba55c0dcac695a83a0414e9" + "sha256": "860c859e433c56fbe24282839bbbc20d9f2455a950cf21459f0e130be958df55" }, { "lang": "zh-CN", "path": "courses/da-report/lessons/L03_cn.md", "title": "挑战 03:构建并渲染报告", "summary": "metrics 原样透传,先 # 标题再按序 ## 小节,金额两位小数。", - "sha256": "01571a51e5c21976bac10b5d655e292b3de882b5acdd6fd0c197fbe257a98155" + "sha256": "dddbe7184a8d48de81b07acded2631b0136e05917887dd4a6e8785fb26f1b3e6" } ] }, @@ -841,14 +841,14 @@ "path": "courses/da-report/lessons/L04.md", "title": "Challenge 04: Numbers traceable to inputs", "summary": "Every metric in report.json equals its source field — the audit answer is a field name, not an explanation.", - "sha256": "501e1fbb684a5e94701559311ad098ea1a3f31fb63da46cd5ddb3f3a37ce2bcf" + "sha256": "002b778f1a17aab30f5717adef0860ebef3ccd686d729e3ce093b26583842a61" }, { "lang": "zh-CN", "path": "courses/da-report/lessons/L04_cn.md", "title": "挑战 04:数字可回溯到输入", "summary": "report.json 每个指标等于源字段——审计答案是字段名,不是解释。", - "sha256": "6603a3bb907ed7f72ef685e2edc68d7a9f21c68cc8b8471347c71e9f8acfa00a" + "sha256": "039df9f5b59c1e8e4126fcd15e272a503f05b66e5480922b501bb63502009ae1" } ] }, @@ -864,14 +864,14 @@ "path": "courses/da-report/lessons/L05.md", "title": "Challenge 05: What report verification cannot see", "summary": "Structure and number consistency are checked; insight and honest conclusions are not — write the boundary sentence.", - "sha256": "e267e245dbb43dfaf905189469fc1507fb46f814fc4778666363837e55b7eab4" + "sha256": "c33d694858ff58eb599ba5ace3fd5947c0a8a1b3b2b7c8888b8cad2177976878" }, { "lang": "zh-CN", "path": "courses/da-report/lessons/L05_cn.md", "title": "挑战 05:报告验证看不到的东西", "summary": "结构与数字一致性被查;洞察与结论诚实不在其中——写下边界句。", - "sha256": "60e8b47da3754b8bd76a9306a09cdb7a056f9c5b601132e71457c80113654b28" + "sha256": "61f78115e8942a8bba28b399e02de2caeb67783494ab7cf17040831459326094" } ] }, @@ -910,14 +910,14 @@ "path": "courses/da-visualization/lessons/L01.md", "title": "Challenge 01: A chart is packaged evidence", "summary": "Say what claim each required PNG makes before writing code — the JSON summary is the auditable layer.", - "sha256": "d3640c032fca4b748b83fef983ebda39fd0e7cda5dff210dac3113bfe0a9809e" + "sha256": "6ea61d76247157f99af1385fd6227d48ad41b22528ac1cb1fe6c0ea352a81b9c" }, { "lang": "zh-CN", "path": "courses/da-visualization/lessons/L01_cn.md", "title": "挑战 01:图是证据的包装", "summary": "写码前先说出每张 PNG 声明什么——JSON 摘要才是可审计层。", - "sha256": "5b1acf730a1aa6d73e127370cf29c497550c74c3b6be1884f1c066d673304ed0" + "sha256": "1b490bbad3b7727eb29fc341c8f99b91f995d21d93a0905d9d390d3f22022b21" } ] }, @@ -933,14 +933,14 @@ "path": "courses/da-visualization/lessons/L02.md", "title": "Challenge 02: Spec first, chart second", "summary": "Write which aggregation feeds each chart and what summary.json must contain before matplotlib opens.", - "sha256": "ac12fed1fec38f46ab665999e219c570f6cb765088a3da3a8532e0d8cab478bb" + "sha256": "e2f7b94b7e7431acc11895a4341b003b3bec35ef34147d6998e41e4388915c30" }, { "lang": "zh-CN", "path": "courses/da-visualization/lessons/L02_cn.md", "title": "挑战 02:先写规格,再画图", "summary": "在打开 matplotlib 之前写清每张图吃哪个聚合、summary.json 必须装什么。", - "sha256": "5f860f5b8de7bad0cf408a690752246820ef56bc44ad1bb43e74e26c27ef3420" + "sha256": "5268e3dcd77086d848a2c40065ca73bd325a665d3c63e7ec2c51b53035b6cf8a" } ] }, @@ -956,14 +956,14 @@ "path": "courses/da-visualization/lessons/L03.md", "title": "Challenge 03: Three charts and a summary", "summary": "Implement the contract in starter/charts.py — Agg before pyplot, parsed dates, real PNG bytes.", - "sha256": "16ddbb48daf45a5c9802424690ab028ce86bc6ea42c8f4b677b7d03da3b36b51" + "sha256": "fb07259f034ca60d4d1eca4c12e44b33224efe3481d5a589cff775bea2fde944" }, { "lang": "zh-CN", "path": "courses/da-visualization/lessons/L03_cn.md", "title": "挑战 03:三张图与摘要", "summary": "在 starter/charts.py 实现契约——Agg 先于 pyplot、日期先解析、真 PNG 字节。", - "sha256": "53532ad9115b7b3295ba0f693b59ea8f961b54a489a1aa8f0ecc1880dfe90ef9" + "sha256": "0dd5926407a92d207a91785a3bc74022e11b0a74cc06068f6460e0b78902251d" } ] }, @@ -979,14 +979,14 @@ "path": "courses/da-visualization/lessons/L04.md", "title": "Challenge 04: Numbers match the charts", "summary": "summary.json is checked against ground truth; a chart disagreeing with the JSON is a failed chart.", - "sha256": "5669523f09d8877d325c54f561dd595d713bf521a70d768095e009400c2ac4ca" + "sha256": "4c4dd1cf0f2bf15f5d7ee11cff3e4d915cbd787bece2be4fc51d57309fd30955" }, { "lang": "zh-CN", "path": "courses/da-visualization/lessons/L04_cn.md", "title": "挑战 04:数字与图一致", "summary": "summary.json 对照真值校验;图与 JSON 不一致就是失败的图。", - "sha256": "08f3321adfa7764d58b5e81eec1aa1cb513ba7bfbfbd16ad2b98488d74b0cf28" + "sha256": "2dad0ea079ae52782273d0570546399cebed2b294a86c41eefa56779ce6d1a8a" } ] }, @@ -1002,21 +1002,21 @@ "path": "courses/da-visualization/lessons/L05.md", "title": "Challenge 05: Honest boundary", "summary": "The suite checks files and numbers — not readability or honest scales; state what was not verified.", - "sha256": "2a2a7ce46664f4653b73b6c719071757a76456b0a10eb35694cee60fac4df6e8" + "sha256": "087690387fe9646a9bbb2edae83a19f6a1726a598c7ee6899e1ec8a8a6df3977" }, { "lang": "zh-CN", "path": "courses/da-visualization/lessons/L05_cn.md", "title": "挑战 05:诚实边界", "summary": "套件查文件与数字——不查可读性与刻度诚实;写清没验证什么。", - "sha256": "7ac94ceda7e8e89e5c192399fcb0a1b0d9c410924d1ba0531ee9c4481e1ce595" + "sha256": "9047f23b6c3b53d2ecf27841bca16c91466a1e79fbceaadcee35fb26d1f0ab8b" } ] }, { "id": "course-deepseek-harness", "type": "course", - "content_version": 1, + "content_version": 2, "status": "reviewed", "reviewed_on": "2026-09-13", "locales": [ @@ -1025,37 +1025,37 @@ "path": "courses/hands-on-with-deepseek-harness/COURSE.md", "title": "Hands-on with DeepSeek Harness", "summary": "The verified Python workflow — task contract, bounded change, objective verify.py evidence — taught hands-on in DeepSeek Harness, where the agent loop is composed from plugins and every step lands in an inspectable trajectory.", - "sha256": "5d98c134e69fa157738bf06e558460a5a698f08e12d07560d56a9f8b2855252a" + "sha256": "fb3b25a0f5233613b96f51e44f31292e1ba844603b12d9b1baeac34bd9e8504f" }, { "lang": "zh-CN", "path": "courses/hands-on-with-deepseek-harness/COURSE_cn.md", "title": "上手 DeepSeek Harness", "summary": "经过验证的 Python 工作流——任务契约、有界改动、客观 verify.py 证据——在 DeepSeek Harness 里手把手教:Agent 闭环由插件组装,每一步都落进可检查的轨迹日志。", - "sha256": "04664097ac975bea0316aee9091f76d70f491045b40bc90e63f816c2c193d761" + "sha256": "cabe3a68415798d70463afcfa1051d76a247793611b041b0077bba9fcb8cbbae" } ] }, { "id": "course-deepseek-harness-l01", "type": "course", - "content_version": 1, + "content_version": 2, "status": "reviewed", "reviewed_on": "2026-09-13", "locales": [ { "lang": "en-US", "path": "courses/hands-on-with-deepseek-harness/lessons/L01.md", - "title": "Lesson 1: Reproduce the failure, read it back from the trajectory", - "summary": "Run a harness-composed agent on the course folder, make the failure objective with verify.py — then read the append-only trajectory to see exactly what the agent did.", - "sha256": "7b5ef7dc5536e049372b58a4a36915bbeba4713c6bc3a4ec8ee0de3a62fc8b5c" + "title": "Lesson 1: Set up DeepSeek Harness, install the FlyPython Skill, fetch the course", + "summary": "Get DeepSeek Harness running, install the FlyPython Skill with network access approved, and let the agent fetch this course's files — you download nothing. Then make the starter's failure objective.", + "sha256": "b05961c1d052d53094e651a7906df7868f78ab1ea3bed98d2c435ebb08d15696" }, { "lang": "zh-CN", "path": "courses/hands-on-with-deepseek-harness/lessons/L01_cn.md", - "title": "第 1 课:复现故障,从轨迹里读回来", - "summary": "让 Harness 组装的 Agent 跑在课程文件夹上,用 verify.py 让失败变得客观——再读只增轨迹,看清 Agent 到底做了什么。", - "sha256": "ffbe560ec8df1b9940dc078630408a84514aaa1702c9d52b1b6e8fc07222b806" + "title": "第 1 课:装好 DeepSeek Harness、装上 FlyPython Skill、让 Agent 取回课程", + "summary": "先把 DeepSeek Harness 跑起来,装好 FlyPython Skill 并打开联网权限,让 Agent 取回本课文件——你不下载任何东西,然后让 starter 的失败变得客观。", + "sha256": "d50f8cc860b18080777562960519c47d13d28d00336892bfc043d2ade0948b67" } ] }, @@ -1071,14 +1071,14 @@ "path": "courses/hands-on-with-deepseek-harness/lessons/L02.md", "title": "Lesson 2: Write the task contract, let cordis.yml carry the setup", "summary": "Turn \"make it better\" into a bounded, testable contract — then make the working rules part of the composed agent, not a forgotten prompt.", - "sha256": "52c04fb47a4b0667b64632f872857f276f8f9001c80601d95475ec38b67c3bd3" + "sha256": "64d4f1e4f8dc54c6d151ddcbbb9c530d866f232ac9673423c90a777d7fbd6280" }, { "lang": "zh-CN", "path": "courses/hands-on-with-deepseek-harness/lessons/L02_cn.md", "title": "第 2 课:写任务契约,让 cordis.yml 承载配置", "summary": "把「改好一点」变成有边界、可测试的契约——再把工作规则放进组装出来的 Agent 里,而不是会被遗忘的提示词。", - "sha256": "e681aec8275c6762a41c92e8a96efd161696069a2d33f4669066179bdf72579f" + "sha256": "6a540ca0dc99c2d91c44ab1c195993c5157299fcac57f3803b6b4c0c02bff26f" } ] }, @@ -1094,14 +1094,14 @@ "path": "courses/hands-on-with-deepseek-harness/lessons/L03.md", "title": "Lesson 3: The bounded change, audited from the trajectory", "summary": "Drive the starter to green through bounded steps — then prove the agent stayed in bounds by inspecting the append-only trajectory.", - "sha256": "0c361ebd8dc019b1b12c8877e76c6ba372d10276c741acaac9999f8832f0da7b" + "sha256": "f9b2867d86bb4257086e250f6585b9604e5299430893e47d2eefc457decc6d0d" }, { "lang": "zh-CN", "path": "courses/hands-on-with-deepseek-harness/lessons/L03_cn.md", "title": "第 3 课:有界改动,用轨迹做审计", "summary": "通过有界的步骤把 starter 推到全绿——再检查只增轨迹,证明 Agent 没有越界。", - "sha256": "305088bf5a3833bab6055230328f666075742095809be20a119c7b53eeede903" + "sha256": "dd24dcb13eeff9144f5fbb7c64219bc0a36fc88aed125e08e92c374adbcfd358" } ] }, @@ -1117,14 +1117,14 @@ "path": "courses/hands-on-with-deepseek-harness/lessons/L04.md", "title": "Lesson 4: Verify like a reviewer, not a spectator", "summary": "Green tests are the floor, not the ceiling — run both suites, walk the scenario skins, and review the trajectory the way you would review a colleague's work.", - "sha256": "10163a3baeee6756ef8f3b60fc11d46fdb427e79c789a9e3312e25b9e1f6a5cc" + "sha256": "f0eeca44e39b33504977d8f2acf0d4e3639f0e184bdccd4e9dd622f5144a3150" }, { "lang": "zh-CN", "path": "courses/hands-on-with-deepseek-harness/lessons/L04_cn.md", "title": "第 4 课:像评审者一样验证,别当观众", "summary": "测试全绿是地板不是天花板——两个套件都跑、三个皮肤都走一遍、按评审同事工作的标准审这条轨迹。", - "sha256": "ea979e9b3827458f408fea3e7d7c8f83b8ed9e209e36d8682417ac31a6adb15d" + "sha256": "f7e24bccba2a86153b115e9b9a5b456f564601f6d7594f242ff78c832f9c63bd" } ] }, @@ -1154,7 +1154,7 @@ { "id": "course-kimi-code", "type": "course", - "content_version": 1, + "content_version": 2, "status": "reviewed", "reviewed_on": "2026-09-13", "locales": [ @@ -1163,37 +1163,37 @@ "path": "courses/hands-on-with-kimi-code/COURSE.md", "title": "Hands-on with Kimi Code", "summary": "The verified Python workflow — task contract, bounded change, objective verify.py evidence — taught hands-on in Kimi Code, where built-in subagents split read-only exploration, planning, and the edits you approve.", - "sha256": "21c176b260918802fd71e2065b8fdfeef334f2ba896f98a6407f5d753500dbe1" + "sha256": "749ea832bc28f7074f99770e0c827255d221159cef48a79a316a8de9c2b55296" }, { "lang": "zh-CN", "path": "courses/hands-on-with-kimi-code/COURSE_cn.md", "title": "上手 Kimi Code", "summary": "经过验证的 Python 工作流——任务契约、有界改动、客观 verify.py 证据——在 Kimi Code 里手把手教:内置子代理把只读勘察、规划和经你批准的编辑分在三条道上。", - "sha256": "9ecaf4e7cf782ada0a71201a2132c5f635c296568ce94f0573d8960516926e2a" + "sha256": "b080c36ac7ffbb1bfaeaad98db2ef68ca61a5f6c72f445c860a5801acf57e762" } ] }, { "id": "course-kimi-code-l01", "type": "course", - "content_version": 1, + "content_version": 2, "status": "reviewed", "reviewed_on": "2026-09-13", "locales": [ { "lang": "en-US", "path": "courses/hands-on-with-kimi-code/lessons/L01.md", - "title": "Lesson 1: Reproduce the failure, survey it with the read-only lane", - "summary": "Run kimi in the course folder, let the read-only explore subagent map the code, and make the failure objective with verify.py.", - "sha256": "1f22cfd82fa640440e2cba270490ccdb9b92f7b4e3d129879530f8d79b48b16e" + "title": "Lesson 1: Set up Kimi Code, install the FlyPython Skill, fetch the course", + "summary": "Get Kimi Code running, install the FlyPython Skill with network access approved, and let the agent fetch this course's files — you download nothing. Then make the starter's failure objective.", + "sha256": "4ea8eadb4e6e2bbf6451dc9700b1cc6779dc00eee2474d4c579b609c972f4e2f" }, { "lang": "zh-CN", "path": "courses/hands-on-with-kimi-code/lessons/L01_cn.md", - "title": "第 1 课:复现故障,用只读道做勘察", - "summary": "在课程文件夹里运行 kimi,让只读的 explore 子代理摸清代码,用 verify.py 让失败变得客观。", - "sha256": "f42a1d5a7898d710edb683c780c4638f262947d30f47e9463741d94523b35814" + "title": "第 1 课:装好 Kimi Code、装上 FlyPython Skill、让 Agent 取回课程", + "summary": "先把 Kimi Code 跑起来,装好 FlyPython Skill 并打开联网权限,让 Agent 取回本课文件——你不下载任何东西,然后让 starter 的失败变得客观。", + "sha256": "276b400a542a5436f231fa89739ebc1be795fc49b2cf8700fbf2de478b25f106" } ] }, @@ -1209,14 +1209,14 @@ "path": "courses/hands-on-with-kimi-code/lessons/L02.md", "title": "Lesson 2: Write the task contract, plan it in the planning lane", "summary": "Turn \"make it better\" into a bounded, testable contract — then let the plan subagent (no write, no shell) turn it into steps before anything edits.", - "sha256": "a6099e33340091887fad8b39743f7831809756a26830b638039c572b3f856dcb" + "sha256": "10b65aecb5e665973ecbfa1808c7418c9b9689da3e6195d400182e8d86bbe8b9" }, { "lang": "zh-CN", "path": "courses/hands-on-with-kimi-code/lessons/L02_cn.md", "title": "第 2 课:写任务契约,在规划道里拆步", "summary": "把「改好一点」变成有边界、可测试的契约——再让 plan 子代理(无写无 shell)先把它拆成步骤,然后才许动文件。", - "sha256": "07296ecbd33c9bcfa38a392b8bdc10ca869f2cd9f1436c983df1f69036946b95" + "sha256": "5021e765800ea1dd28baacf709720610cee1ad49873e5956111dbea6e7de6231" } ] }, @@ -1232,14 +1232,14 @@ "path": "courses/hands-on-with-kimi-code/lessons/L03.md", "title": "Lesson 3: The bounded change, one contract line at a time", "summary": "Drive the starter to green through the coder lane in bounded steps — and review every edit before the next one lands.", - "sha256": "c9a411e86737ee10dbfc9e1c5a600e22b27b85f355da7f8570bf42b8e6e8dfb3" + "sha256": "a4152a672227b999c7b7b9da4a282786925035a6abf5b7d675f574db5ff52508" }, { "lang": "zh-CN", "path": "courses/hands-on-with-kimi-code/lessons/L03_cn.md", "title": "第 3 课:有界改动,一次一条契约行", "summary": "通过 coder 道的有界步骤把 starter 推到全绿——下一处编辑落地前,先审完这一处。", - "sha256": "840c27b463e7b25555cf45b41c97849624265887e5777f0d6eb941cafad91aaf" + "sha256": "0be0b2a66186028eae871cc8ed46d1fcf6152dc2dd98319c1e0caa4e9dce17e3" } ] }, @@ -1255,14 +1255,14 @@ "path": "courses/hands-on-with-kimi-code/lessons/L04.md", "title": "Lesson 4: Verify like a reviewer, not a spectator", "summary": "Green tests are the floor, not the ceiling — run both suites, walk the scenario skins, and review the accumulated diff the way you would review a colleague's.", - "sha256": "ccb695a57c39ca61d5c412b54b50cf83cb1984a783178605997626a95d2fde2d" + "sha256": "279887be163fd98ab3a478c698a7ad1500593f2127750f1929c77b77da51033b" }, { "lang": "zh-CN", "path": "courses/hands-on-with-kimi-code/lessons/L04_cn.md", "title": "第 4 课:像评审者一样验证,别当观众", "summary": "测试全绿是地板不是天花板——两个套件都跑、三个皮肤都走一遍、按评审同事的标准审这份累积 diff。", - "sha256": "590857d8349d73071796a856601ac34508ae8291792b10887e45ad1e5e475f8f" + "sha256": "7f6c9b4a4f75c1f4f469224384a4921ea690844849be40b0c5bf0d11907c0357" } ] }, @@ -1292,7 +1292,7 @@ { "id": "course-mcp-tools", "type": "course", - "content_version": 2, + "content_version": 3, "status": "reviewed", "reviewed_on": "2026-09-12", "locales": [ @@ -1301,14 +1301,14 @@ "path": "courses/mcp-server-in-python/COURSE.md", "title": "Give your agent tools with MCP (Python)", "summary": "Build a stateless Model Context Protocol tool server in pure Python — JSON-RPC 2.0 dispatch, schema validation, error isolation, and the 2026-07-28 input_required round-trip — and learn where a real tool belongs.", - "sha256": "de677071dab7c06244ad0634a7266372619af38a8de60b8257af0570de1b9cb4" + "sha256": "33af5c6245ce1dc1f9fbba6431ba6fa911c63ee2b7137d9e91600370685f44d8" }, { "lang": "zh-CN", "path": "courses/mcp-server-in-python/COURSE_cn.md", "title": "用 MCP 给你的 Agent 装上工具(Python)", "summary": "用纯 Python 构建一个无状态的 Model Context Protocol 工具服务——JSON-RPC 2.0 分发、schema 校验、错误隔离与 2026-07-28 的 input_required 多轮交互——并理解真正的工具该放在哪里。", - "sha256": "d125779e096e3b76b97caf51db25577869ee9fadd23447e02f9aea1d5d5066ee" + "sha256": "322660423993cb400ec53c8b0c79b55a276229765fbc1c3fa1891729d7c1c72c" } ] }, @@ -1324,14 +1324,14 @@ "path": "courses/mcp-server-in-python/lessons/L01.md", "title": "What MCP actually is (2026-07-28)", "summary": "A stateless JSON-RPC 2.0 surface for tools — no handshake, no sessions, versions in _meta.", - "sha256": "a50dbc4673a7178344e7ff3d19dac2f53abbabe584ed3bfa4639b5b263064fc0" + "sha256": "96de808002e5441672c198682dc7ae2b25ecca60fbe6735c1fd7448add92ca2f" }, { "lang": "zh-CN", "path": "courses/mcp-server-in-python/lessons/L01_cn.md", "title": "MCP 到底是什么(2026-07-28)", "summary": "一个面向工具的无状态 JSON-RPC 2.0 接口——无握手、无会话、版本放在 _meta 里。", - "sha256": "1f89bd8f063956870cb01fe57593779ab0eb2566f52404c4a2bc9e76c48dc29f" + "sha256": "d598f5b026942aa917f90d08650de30839e4858a0580427580b9b5fb4733d7de" } ] }, @@ -1347,14 +1347,14 @@ "path": "courses/mcp-server-in-python/lessons/L02.md", "title": "The JSON-RPC dispatch contract", "summary": "Valid requests echo their id; invalid ones get -32600; unknown or removed methods get -32601.", - "sha256": "2b0b57cd587d214f17f24864ba0b7fd632b1f8f18af113469e07843503fa8baf" + "sha256": "bfe4fe19e51256a5f387f83caf62dda1f02ff51888f1603e6842339cb3cc0ff6" }, { "lang": "zh-CN", "path": "courses/mcp-server-in-python/lessons/L02_cn.md", "title": "JSON-RPC 分发契约", "summary": "合法请求回显 id;非法请求得 -32600;未知或已移除的方法得 -32601。", - "sha256": "9cea20a365eb994ffd0a6db1d07df4b494dea86593d77f078c0825c9e9aa9995" + "sha256": "0fb8429550ce58383938809824aff13a91df741eca2263d1800dc29def99337f" } ] }, @@ -1370,14 +1370,14 @@ "path": "courses/mcp-server-in-python/lessons/L03.md", "title": "Validation and error isolation", "summary": "Arguments are untrusted input: validate before execution, and never let a handler crash the transport.", - "sha256": "7d55d1fab063f988ffc819f1f2dfa945ceeb4bc5208d7dadbf0e009c7dfa7196" + "sha256": "6b31bd6ea95bd294590be3bbc3a326d20333cee67ed3d9c1131a47f659a135e1" }, { "lang": "zh-CN", "path": "courses/mcp-server-in-python/lessons/L03_cn.md", "title": "校验与错误隔离", "summary": "参数是不可信输入:先校验再执行,绝不让处理函数打断传输。", - "sha256": "bd546854dc1b42f03baafddf00eed80a7c601d0ed228a5f951d7e3daef8d4a0f" + "sha256": "cc570c73e4269dcfba2c09f9cdb24b197773b7339f1dd029404dade02a8851bc" } ] }, @@ -1393,14 +1393,14 @@ "path": "courses/mcp-server-in-python/lessons/L04.md", "title": "The input_required round-trip", "summary": "A tool can ask the client a question mid-call — return the requests, finish on the retry carrying answers.", - "sha256": "2ba1145cdc807afabd09737b860c9531acdbdbf933870e51f640c5f1bbb3a631" + "sha256": "4d8b445585b3e139badba149c7037dab34fb83760eefae9c7f8c11bf47d870cf" }, { "lang": "zh-CN", "path": "courses/mcp-server-in-python/lessons/L04_cn.md", "title": "input_required 多轮交互", "summary": "工具可以在调用中途向客户端提问——先返回问题,客户端带答案重试后完成。", - "sha256": "8170e1bbec1865410e7ea0c8feb1b8cc182b3e119ce608f3c4e8fdc822ddcc70" + "sha256": "89b23a1cc7ec4a88efdf9e469ada40c8a39e91f0a1713083de4b355030f60577" } ] }, @@ -1416,21 +1416,21 @@ "path": "courses/mcp-server-in-python/lessons/L05.md", "title": "Expose one real tool to your agent", "summary": "Wrap one function you actually own behind the validated, isolated boundary you just built.", - "sha256": "8b7c0a177ee77b0e9a9c8972d42f89dca94576e5aa9c234e845d11d65cb128c6" + "sha256": "ef5f5b3588419591faadb1d5e63d701e95e54e4589ab079e59f0d60917588722" }, { "lang": "zh-CN", "path": "courses/mcp-server-in-python/lessons/L05_cn.md", "title": "给你的 Agent 暴露一个真实工具", "summary": "把你真正拥有的一个函数包进刚建好的“校验 + 隔离”边界。", - "sha256": "4b33bc46cf447a9df0d0b7b0a37cab36eb825b9769d1c42ead1b0823af75014f" + "sha256": "f5a906e707393326ece9a984916dd3624cf2573dd6cefc0371ca362e0a02b7c7" } ] }, { "id": "course-verify-ship", "type": "course", - "content_version": 2, + "content_version": 3, "status": "reviewed", "reviewed_on": "2026-09-12", "locales": [ @@ -1439,14 +1439,14 @@ "path": "courses/verifying-ai-generated-code/COURSE.md", "title": "From \"it runs\" to \"it ships\": verifying AI-generated code", "summary": "Build the release-evidence machine your AI-written project is missing — a ship check that runs the tests, parses what actually ran, refuses zero-test projects, and writes an honest delivery record.", - "sha256": "88b1a775241a69b11b868bfae72e2954ad93b608789a9230a46431549444e9ed" + "sha256": "f329af811dee9fb767f0789c5b176fef435e6bf9a2bd6e36a86e98992e232d6d" }, { "lang": "zh-CN", "path": "courses/verifying-ai-generated-code/COURSE_cn.md", "title": "从「能跑」到「能上线」:验证 AI 写的代码", "summary": "为你的 AI 项目补上缺失的发布证据机器——一个 ship check:运行测试、解析实际跑了什么、拒绝零测试项目,并写出诚实的交付记录。", - "sha256": "fe2cc54cd9e37ddebb2c80c2cba52f40f5ce272566fd818c448b97e80c03c8ba" + "sha256": "4e9f9675fd80851670b5585a48161b78be080640d832b1a4f40a0ad75078b83d" } ] }, @@ -1462,14 +1462,14 @@ "path": "courses/verifying-ai-generated-code/lessons/L01.md", "title": "\"It runs\" is not evidence", "summary": "Watch the starter bless a project with no tests and a project with a failing one — then want better.", - "sha256": "96f3b58d3884b1e28b189c944aaaa94e8bbe900f1209f22bbb6269994f92445e" + "sha256": "02703933706d1557470a3f6305d331daa07983c3ede05fe5d8c462a3835e0273" }, { "lang": "zh-CN", "path": "courses/verifying-ai-generated-code/lessons/L01_cn.md", "title": "“能跑”不是证据", "summary": "看着 starter 给零测试的项目和有失败的项目都盖章放行——然后想要更好的东西。", - "sha256": "8ec0c0cdd759871e4ce758cec41d077dc83c20d4864b596a062de47904e85e24" + "sha256": "d4561bad447524145ac0618c8940ed856766f38b458bec68c4ab8d91af5664c3" } ] }, @@ -1485,14 +1485,14 @@ "path": "courses/verifying-ai-generated-code/lessons/L02.md", "title": "Define what a release record must prove", "summary": "Command, exit code, parsed count and outcome, an unverified list, and a verdict that refuses zero-test projects.", - "sha256": "d39ff4180a0dbabca31ea3d2a66fe9bb82a4f87c2830bc32f165ef1f93d90c62" + "sha256": "3910f5128e43027769f3b0b03f4f483ff916a39af1abdfe8d6efc3984d014a71" }, { "lang": "zh-CN", "path": "courses/verifying-ai-generated-code/lessons/L02_cn.md", "title": "定义发布记录必须证明什么", "summary": "命令、退出码、解析出的数量与结果、一份未验证清单,以及一个拒绝零测试项目的判定。", - "sha256": "b57eab0170a48047ab0e56d61bb92aac5d17680346acf8e82c5b0293c42852f5" + "sha256": "babb9e3127e75928c76d0f706faab3c0dfc1844d05d182007237a485880fd500" } ] }, @@ -1508,14 +1508,14 @@ "path": "courses/verifying-ai-generated-code/lessons/L03.md", "title": "Build the ship check, test by test", "summary": "Parsing, verdicts, the unverified list, atomic writes, exit codes — one failing group at a time.", - "sha256": "6d0491d3f534add4b354e311fbaa957b73b0abd388e06fb20366dc830d95395a" + "sha256": "512e4562c2279e0a064d79e2417c2a42aaceae60b6b6cfae3e653f514150d09a" }, { "lang": "zh-CN", "path": "courses/verifying-ai-generated-code/lessons/L03_cn.md", "title": "按测试逐组构建 ship check", "summary": "解析、判定、未验证清单、原子写入、退出码——一次一组失败测试。", - "sha256": "5013df14fbad78914a55dd9fac568aaa60809c33b13d180ab958be8e5cd62eb5" + "sha256": "bd910efe67f4d17a30b129f65f3d1adb8b5f13af22d33dafbc809f3ff35e7a67" } ] }, @@ -1531,14 +1531,14 @@ "path": "courses/verifying-ai-generated-code/lessons/L04.md", "title": "Read the record like a skeptic", "summary": "Green, red, and no-tests mean different things — prove you can tell them apart and say what the record does not prove.", - "sha256": "14326a8b19d5c8db834bcd3071598135b6cc8a11a9e8aac974a6bf3c8fc82ced" + "sha256": "d8017ac4bfdfa42be12a80f1325921ce033c6d5b5b0e1e80d59e2a596b8a0231" }, { "lang": "zh-CN", "path": "courses/verifying-ai-generated-code/lessons/L04_cn.md", "title": "像怀疑者一样读记录", "summary": "绿、红、零测试是三件不同的事——证明你能分辨,并说清记录不能证明什么。", - "sha256": "263921bf47ea5db21b98548442fe638794fb3ad9a84abef885b7ff66b94da919" + "sha256": "f59ef01d2c7f6476e3e0755da892738b816b13412ea8adb94d7ea41a464e94fa" } ] }, @@ -1554,21 +1554,21 @@ "path": "courses/verifying-ai-generated-code/lessons/L05.md", "title": "Gate your next release", "summary": "Take the ship check to a project you own and make the record part of shipping.", - "sha256": "9e664fc5e21c28b4ab5b1217c6be273d62f8ae139a774634c826bda4814c2bf6" + "sha256": "e3700ed08ce5a0fd2d154da5275b7edeead88ff70275552f67f23da3b11af1c8" }, { "lang": "zh-CN", "path": "courses/verifying-ai-generated-code/lessons/L05_cn.md", "title": "给你的下一次发布装上门禁", "summary": "把 ship check 带到你自己的项目,让记录成为交付的一部分。", - "sha256": "667023ea75adf0c47c97de409f8a364027f897975d377b7fd3a7b9d00cda9e80" + "sha256": "dda3c53b3a73121efd5eb602f5893dddd0151ba9f0f6ef97980f7ca482c94ae1" } ] }, { "id": "course-zcode", "type": "course", - "content_version": 1, + "content_version": 2, "status": "reviewed", "reviewed_on": "2026-09-13", "locales": [ @@ -1577,37 +1577,37 @@ "path": "courses/hands-on-with-zcode/COURSE.md", "title": "Hands-on with ZCode", "summary": "The verified Python workflow — task contract, bounded change, objective verify.py evidence — taught hands-on in the ZCode desktop workspace, where Goal Mode decomposes the work and the Git state shows every change.", - "sha256": "e8a90d90aa5daa83d3dd116eb53676f2d3c9bdf78d6cb12da4b732b703c976eb" + "sha256": "f1549e06594afe047d386bbf379467006df7dcaad24a9e3b848d8be0451e8f6c" }, { "lang": "zh-CN", "path": "courses/hands-on-with-zcode/COURSE_cn.md", "title": "上手 ZCode", "summary": "经过验证的 Python 工作流——任务契约、有界改动、客观 verify.py 证据——在 ZCode 桌面工作区里手把手教:Goal 模式拆解任务,Git 状态展示每一处改动。", - "sha256": "2d33bc8c5b449b929de8d0de7628e2fa8aa900efb58683b373ea9b0013a48e3d" + "sha256": "38a1ede3d23dc65fd44e0b764914d4a5f637aa1156c5b07a3d6ed5c1c141c034" } ] }, { "id": "course-zcode-l01", "type": "course", - "content_version": 1, + "content_version": 2, "status": "reviewed", "reviewed_on": "2026-09-13", "locales": [ { "lang": "en-US", "path": "courses/hands-on-with-zcode/lessons/L01.md", - "title": "Lesson 1: Reproduce the failure inside the workspace", - "summary": "Open the folder as a ZCode workspace, give the agent its first task, and make the failure objective with verify.py — inside the app, not beside it.", - "sha256": "f3ba2aeff5234732dd2f6d72d99eee7635956584a7f1ce270a0569dc34c4c628" + "title": "Lesson 1: Set up ZCode, install the FlyPython Skill, fetch the course", + "summary": "Get ZCode running, install the FlyPython Skill with network access approved, and let the agent fetch this course's files — you download nothing. Then make the starter's failure objective.", + "sha256": "c0ebabcd982edcdfee9360d0b4019d0f139573ab84bcc3ce9b9cfe8b729cbfaf" }, { "lang": "zh-CN", "path": "courses/hands-on-with-zcode/lessons/L01_cn.md", - "title": "第 1 课:在工作区里复现故障", - "summary": "把文件夹作为 ZCode 工作区打开,给 Agent 第一个任务,用 verify.py 让失败变得客观——在应用内完成,而不是在应用旁边。", - "sha256": "fa359a53e2743a7f55076a0a1b046db7d0fb340579becf3d8ad538e1a41ce4f2" + "title": "第 1 课:装好 ZCode、装上 FlyPython Skill、让 Agent 取回课程", + "summary": "先把 ZCode 跑起来,装好 FlyPython Skill 并打开联网权限,让 Agent 取回本课文件——你不下载任何东西,然后让 starter 的失败变得客观。", + "sha256": "2803669b8d5526f585975b8221ccdc5cf661f973f118d853fb7c218cf68472f9" } ] }, @@ -1623,14 +1623,14 @@ "path": "courses/hands-on-with-zcode/lessons/L02.md", "title": "Lesson 2: Write the task contract, let Goal Mode hold the plan", "summary": "Turn \"make it better\" into a bounded, testable contract — then let Goal Mode decompose it into tasks you can watch land one by one.", - "sha256": "d6ae0f3b3a62a196f3f2b476ced1b7f8c7ad2b87e26be82fd0e62667c15e3710" + "sha256": "8d82172aa73a5350511ab0e07b88c0725417228ab88766149836c892fb5931a6" }, { "lang": "zh-CN", "path": "courses/hands-on-with-zcode/lessons/L02_cn.md", "title": "第 2 课:写任务契约,让 Goal 模式承载计划", "summary": "把「改好一点」变成有边界、可测试的契约——再让 Goal 模式把它拆成你能看着逐一落地的任务。", - "sha256": "afdbf2423f916b2dbae40e7a94d7d6b6d65d73ad3fc7536cab19b3a678cf8a8e" + "sha256": "8ccfc361351b45a791ae70bd4ee0faeb59e350898f746b9916f85f79ed564eb5" } ] }, @@ -1646,14 +1646,14 @@ "path": "courses/hands-on-with-zcode/lessons/L03.md", "title": "Lesson 3: The bounded change, watched task by task", "summary": "Drive the starter to green through the goal's task list — and use the workspace Git state to review every change that lands.", - "sha256": "448c845acb70a0d4c18334d6f216f80a3a47e8aa9d6b7d39518d136567112d8e" + "sha256": "0bd215e38f23a1bb54f13b8ab8e93280694332cefafa5d5ba31c516bd7609840" }, { "lang": "zh-CN", "path": "courses/hands-on-with-zcode/lessons/L03_cn.md", "title": "第 3 课:有界改动,逐任务盯守", "summary": "沿着目标的任务列表把 starter 推到全绿——用工作区 Git 状态审每一处落地的改动。", - "sha256": "e571522f7b5ab9e68b5c1c854029e36fea64b278dcd75373dbd78129b679ab2a" + "sha256": "91c25c0373c7b132d1d8ddb57f6aeca107b542caa7aa02db307edb97d593ddaa" } ] }, @@ -1669,14 +1669,14 @@ "path": "courses/hands-on-with-zcode/lessons/L04.md", "title": "Lesson 4: Verify like a reviewer, not a spectator", "summary": "Green tests are the floor, not the ceiling — run both suites, walk the scenario skins, and review the accumulated Git diff the way you would review a colleague's.", - "sha256": "005f3a1f1fb638f9751b87e459988f22b4d4058f4d9e89d069ba90c949779f16" + "sha256": "c488567de7ff1f2f1ca1e0616a3eae14da324cf294dfe632ea7bb3ac23c3c556" }, { "lang": "zh-CN", "path": "courses/hands-on-with-zcode/lessons/L04_cn.md", "title": "第 4 课:像评审者一样验证,别当观众", "summary": "测试全绿是地板不是天花板——两个套件都跑、三个皮肤都走一遍、按评审同事的标准审这份累积 Git diff。", - "sha256": "657f9b5f3b31b9ba7f0ca1d15b9e5a58e1aac5971343ae049fe5425783638471" + "sha256": "e9d723ddff1c5586556c1535fadccd8b5ded06f70157b4f7b66919f045b1dae3" } ] }, diff --git a/courses/agent-rules-single-source/COURSE.md b/courses/agent-rules-single-source/COURSE.md index e2c3dac..5702146 100644 --- a/courses/agent-rules-single-source/COURSE.md +++ b/courses/agent-rules-single-source/COURSE.md @@ -4,7 +4,7 @@ type: course title: One source of truth for agent rules summary: Stop maintaining diverging AGENTS.md, CLAUDE.md, and .cursorrules files — build a checker that proves your repository has exactly one rule source, taught hands-on with your coding agent. lang: en-US -content_version: 2 +content_version: 3 status: reviewed reviewed_on: 2026-09-12 badge: @@ -17,8 +17,9 @@ course_id: course-agent-rules # One source of truth for agent rules -> TL;DR: download this folder, open it in your coding agent, and say -> **"start lesson 1"**. You finish with a working rule-consistency checker +> TL;DR: install the FlyPython Skill in your coding agent and let it fetch +> this course, then say **"start lesson 1"**. (Lesson files arrive via +> the Skill — you download nothing by hand.) You finish with a working rule-consistency checker > (`rules_check.py`) that fails CI the moment a rule file drifts from > AGENTS.md — plus the single-source setup applied to your own repository. > Tool-agnostic by design: Claude Code, Codex CLI, and Cursor all read these @@ -69,7 +70,7 @@ directory, three file names, one truth. - Badge: **Measure the drift Badge** (badge id `course-agent-rules`) - earned by claiming all five checkpoints. - Challenges: L01-L05 checkpoints, 10 points each; +50 course-badge bonus when all five are claimed on flypython.com. -- Evidence: `python verify.py progress` - L03 (bounded change) and L04 (verify & review) are objectively gated by the suite; L01/L02/L05 are learner-attested. +- Evidence: `python verify.py` - L03 (bounded change) and L04 (verify & review) are objectively gated by the suite; L01/L02/L05 are learner-attested. - Submission: each passed checkpoint prints a deterministic claim code; record it on flypython.com against your account. Self-reported evidence, never a certificate. ## Folder map diff --git a/courses/agent-rules-single-source/COURSE_cn.md b/courses/agent-rules-single-source/COURSE_cn.md index bb6c3f2..aba1764 100644 --- a/courses/agent-rules-single-source/COURSE_cn.md +++ b/courses/agent-rules-single-source/COURSE_cn.md @@ -4,7 +4,7 @@ type: course title: Agent 规则的单一真源 summary: 不再同时维护彼此打架的 AGENTS.md、CLAUDE.md 与 .cursorrules——亲手构建一个能证明仓库只有一处规则真源的检查器,由你的编码 Agent 授课。 lang: zh-CN -content_version: 2 +content_version: 3 status: reviewed reviewed_on: 2026-09-12 badge: @@ -17,7 +17,8 @@ course_id: course-agent-rules # Agent 规则的单一真源 -> 摘要:下载本文件夹,在你的编码 Agent 中打开,说一句 **“开始第 1 课”**。 +> 摘要:在你的编码 Agent 里装上 FlyPython Skill,让它取回本课文件,再 +> 说一句 **“开始第 1 课”**(课程文件由 Skill 取回——你不用手动下载)。 > 课程结束时你拥有一个可运行的规则一致性检查器(`rules_check.py`)—— > 任何规则文件偏离 AGENTS.md 的那一刻,CI 就会失败——外加应用到你自己 > 仓库的单一真源配置。天然工具无关:Claude Code、Codex CLI、Cursor @@ -64,7 +65,7 @@ flypython.com 上的 AGENTS.md 指南)、多仓库配置、机器策略强制 - 徽章:**量化漂移徽章**(徽章 id `course-agent-rules`)——认领全部五个检查点后获得。 - 挑战:L01–L05 检查点各 10 分;五项全部在 flypython.com 认领后另加 50 分课程徽章奖励。 -- 证据:`python verify.py progress` —— L03(边界修改)与 L04(验证与审查)由测试套件客观判定;L01/L02/L05 为学习者自报。 +- 证据:`python verify.py` —— L03(边界修改)与 L04(验证与审查)由测试套件客观判定;L01/L02/L05 为学习者自报。 - 提交:每个通过的检查点会打印确定性认领码,在 flypython.com 上记入你的账号。这是自我报告的证据,绝不是证书。 ## 文件夹结构 diff --git a/courses/agent-rules-single-source/lessons/L01.md b/courses/agent-rules-single-source/lessons/L01.md index ef7e497..f2b5b19 100644 --- a/courses/agent-rules-single-source/lessons/L01.md +++ b/courses/agent-rules-single-source/lessons/L01.md @@ -37,7 +37,12 @@ python verify.py starter --expect-failure ## Checkpoint -The lesson's commands run, and you can explain — in your own words — what failed and why. +The lesson's commands run, and you can answer these in your own words +(the agent asks; you answer — that is the gate): + +1. Which command did this lesson run, and what did it decide? +2. What failed at the start, and why — in your own words? +3. Before trusting a similar change next time, what would you check first? ## Expected evidence diff --git a/courses/agent-rules-single-source/lessons/L01_cn.md b/courses/agent-rules-single-source/lessons/L01_cn.md index c0edbee..42d233e 100644 --- a/courses/agent-rules-single-source/lessons/L01_cn.md +++ b/courses/agent-rules-single-source/lessons/L01_cn.md @@ -37,7 +37,12 @@ python verify.py starter --expect-failure ## 检查点 -本课的命令运行通过,并且你能用自己的话说清什么失败了、为什么。 +本课的命令运行通过,并且你能用自己的话回答下面的问题(由 Agent +提问、你作答——这就是关口): + +1. 本课跑了哪条命令,它判定了什么? +2. 一开始什么失败了、为什么——用你自己的话说? +3. 下次再信任类似的改动之前,你会先检查什么? ## 预期证据 diff --git a/courses/agent-rules-single-source/lessons/L02.md b/courses/agent-rules-single-source/lessons/L02.md index 7146ad2..979064a 100644 --- a/courses/agent-rules-single-source/lessons/L02.md +++ b/courses/agent-rules-single-source/lessons/L02.md @@ -33,7 +33,12 @@ Read the repository's own AGENTS.md and `templates/AGENT_RULES.example.md`. The ## Checkpoint -The lesson's commands run, and you can explain — in your own words — what failed and why. +The lesson's commands run, and you can answer these in your own words +(the agent asks; you answer — that is the gate): + +1. Which command did this lesson run, and what did it decide? +2. What failed at the start, and why — in your own words? +3. Before trusting a similar change next time, what would you check first? ## Expected evidence diff --git a/courses/agent-rules-single-source/lessons/L02_cn.md b/courses/agent-rules-single-source/lessons/L02_cn.md index 76a68b5..62bd7d5 100644 --- a/courses/agent-rules-single-source/lessons/L02_cn.md +++ b/courses/agent-rules-single-source/lessons/L02_cn.md @@ -33,7 +33,12 @@ AGENTS.md 成为 Agent 唯一阅读的文件;其余都只能是指针或副本 ## 检查点 -本课的命令运行通过,并且你能用自己的话说清什么失败了、为什么。 +本课的命令运行通过,并且你能用自己的话回答下面的问题(由 Agent +提问、你作答——这就是关口): + +1. 本课跑了哪条命令,它判定了什么? +2. 一开始什么失败了、为什么——用你自己的话说? +3. 下次再信任类似的改动之前,你会先检查什么? ## 预期证据 diff --git a/courses/agent-rules-single-source/lessons/L03.md b/courses/agent-rules-single-source/lessons/L03.md index 0556aad..3f12084 100644 --- a/courses/agent-rules-single-source/lessons/L03.md +++ b/courses/agent-rules-single-source/lessons/L03.md @@ -33,7 +33,12 @@ Tell the agent: "Work through TASK.md against starter/rules_check.py, one failin ## Checkpoint -The lesson's commands run, and you can explain — in your own words — what failed and why. +The lesson's commands run, and you can answer these in your own words +(the agent asks; you answer — that is the gate): + +1. Which command did this lesson run, and what did it decide? +2. What failed at the start, and why — in your own words? +3. Before trusting a similar change next time, what would you check first? ## Expected evidence diff --git a/courses/agent-rules-single-source/lessons/L03_cn.md b/courses/agent-rules-single-source/lessons/L03_cn.md index 5674642..4fb07a7 100644 --- a/courses/agent-rules-single-source/lessons/L03_cn.md +++ b/courses/agent-rules-single-source/lessons/L03_cn.md @@ -33,7 +33,12 @@ hints: ## 检查点 -本课的命令运行通过,并且你能用自己的话说清什么失败了、为什么。 +本课的命令运行通过,并且你能用自己的话回答下面的问题(由 Agent +提问、你作答——这就是关口): + +1. 本课跑了哪条命令,它判定了什么? +2. 一开始什么失败了、为什么——用你自己的话说? +3. 下次再信任类似的改动之前,你会先检查什么? ## 预期证据 diff --git a/courses/agent-rules-single-source/lessons/L04.md b/courses/agent-rules-single-source/lessons/L04.md index ddbc361..fba1399 100644 --- a/courses/agent-rules-single-source/lessons/L04.md +++ b/courses/agent-rules-single-source/lessons/L04.md @@ -33,7 +33,12 @@ Run `python verify.py starter` (11/11 after your change) and `python verify.py s ## Checkpoint -The lesson's commands run, and you can explain — in your own words — what failed and why. +The lesson's commands run, and you can answer these in your own words +(the agent asks; you answer — that is the gate): + +1. Which command did this lesson run, and what did it decide? +2. What failed at the start, and why — in your own words? +3. Before trusting a similar change next time, what would you check first? ## Expected evidence diff --git a/courses/agent-rules-single-source/lessons/L04_cn.md b/courses/agent-rules-single-source/lessons/L04_cn.md index 7513778..c8a0a4d 100644 --- a/courses/agent-rules-single-source/lessons/L04_cn.md +++ b/courses/agent-rules-single-source/lessons/L04_cn.md @@ -33,7 +33,12 @@ hints: ## 检查点 -本课的命令运行通过,并且你能用自己的话说清什么失败了、为什么。 +本课的命令运行通过,并且你能用自己的话回答下面的问题(由 Agent +提问、你作答——这就是关口): + +1. 本课跑了哪条命令,它判定了什么? +2. 一开始什么失败了、为什么——用你自己的话说? +3. 下次再信任类似的改动之前,你会先检查什么? ## 预期证据 diff --git a/courses/agent-rules-single-source/lessons/L05.md b/courses/agent-rules-single-source/lessons/L05.md index f0d0ff8..6357a86 100644 --- a/courses/agent-rules-single-source/lessons/L05.md +++ b/courses/agent-rules-single-source/lessons/L05.md @@ -32,7 +32,12 @@ In your own repository: write (or prune) AGENTS.md as the single rule source, re ## Checkpoint -The lesson's commands run, and you can explain — in your own words — what failed and why. +The lesson's commands run, and you can answer these in your own words +(the agent asks; you answer — that is the gate): + +1. Which command did this lesson run, and what did it decide? +2. What failed at the start, and why — in your own words? +3. Before trusting a similar change next time, what would you check first? ## Expected evidence diff --git a/courses/agent-rules-single-source/lessons/L05_cn.md b/courses/agent-rules-single-source/lessons/L05_cn.md index 0c6c10b..86e3c8e 100644 --- a/courses/agent-rules-single-source/lessons/L05_cn.md +++ b/courses/agent-rules-single-source/lessons/L05_cn.md @@ -32,7 +32,12 @@ hints: ## 检查点 -本课的命令运行通过,并且你能用自己的话说清什么失败了、为什么。 +本课的命令运行通过,并且你能用自己的话回答下面的问题(由 Agent +提问、你作答——这就是关口): + +1. 本课跑了哪条命令,它判定了什么? +2. 一开始什么失败了、为什么——用你自己的话说? +3. 下次再信任类似的改动之前,你会先检查什么? ## 预期证据 diff --git a/courses/agent-rules-single-source/verify.py b/courses/agent-rules-single-source/verify.py index 91cb081..e5cd879 100644 --- a/courses/agent-rules-single-source/verify.py +++ b/courses/agent-rules-single-source/verify.py @@ -58,6 +58,16 @@ def _claim_code(checkpoint_id): ).digest() return base64.b32encode(digest).decode("ascii")[:8] + +def _display_title(title): + # FP-820: shared-core checkpoint titles are stored "Chinese / English"; + # the default command prints English first, Chinese after (FP-709 debt). + if " / " in title: + left, _, right = title.partition(" / ") + if any("\u4e00" <= character <= "\u9fff" for character in left): + return right + " / " + left + return title + def _run_suite(implementation): environment = os.environ.copy() environment["PYTHONPATH"] = str(ROOT / implementation) @@ -72,7 +82,7 @@ def _suite_stats(run): return tests, int(getattr(run, "_elapsed_ms", 0)) -def run_progress(as_json, receipt_out=None): +def run_progress(as_json, receipt_out=None, default=False): import time t0 = time.monotonic() starter = _run_suite("starter") @@ -118,6 +128,33 @@ def run_progress(as_json, receipt_out=None): COURSE_ID, checkpoint["id"], passed=passed, tests=tests, duration_ms=ms, impl_dir=ROOT / "starter", solution_hash=solution_hash, secret=secret)) + if default and not as_json: + # FP-820: bare ``python verify.py`` — check the learner's own + # implementation, print per-checkpoint status and earned claim codes, + # English first then Chinese (FP-709 debt). ``progress`` keeps its + # exact published output; this block is the only new surface. + print("Course " + COURSE_ID) + print("Suites: starter " + ("passed" if starter_ok else "not passed") + + " / solution " + ("passed" if solution_ok else "not passed")) + for row in rows: + state = row["status"] + (" (self-attested)" if row["kind"] == "attested" else "") + code = row["claim_code"] if row["claim_code"] else "\u2014" + print(" " + row["id"] + " " + _display_title(row["title"]) + " [" + state + "] " + code) + open_gates = [row["id"] for row in rows + if row["kind"] == "objective" and row["status"] != "passed"] + if open_gates: + print("Next: keep implementing starter/ until " + ", ".join(open_gates) + + " show [passed]; then re-run: python verify.py") + else: + print("All objective checkpoints passed — the codes above are ready" + " to submit (batch POST /api/claims, see SKILL.md §5).") + print("\u4e2d\u6587\uff1a\u68c0\u67e5\u70b9\u72b6\u6001\u4e0e\u8ba4\u9886\u7801\u89c1\u4e0a\uff1b\u5e26 [passed]/[attest] \u7684\u884c\u6709\u8ba4\u9886\u7801\uff0c[open] \u7684\u884c\u7ee7\u7eed\u5728 starter/ \u91cc\u5b9e\u73b0\u540e\u518d\u8dd1\u3002") + print("Claim codes are self-reported evidence, recorded at flypython.com; never a certificate.") + print("\u8ba4\u9886\u7801\u662f\u81ea\u6211\u62a5\u544a\u7684\u8bc1\u636e\uff0c\u8bb0\u5f55\u5728 flypython.com\uff1b\u4e0d\u662f\u8bc1\u4e66\u3002") + if secret: + print(f"Signed run receipts prepared for {len(receipts)} gated checkpoint(s);" + " submit each with its claim to mark it as a local-run receipt.") + return 1 if open_gates else 0 if as_json: document = {"course": COURSE_ID, "starter_suite_passed": starter_ok, @@ -148,14 +185,19 @@ def run_progress(as_json, receipt_out=None): def main() -> int: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("implementation", choices=("progress", "starter", "solution")) + parser.add_argument("implementation", choices=("progress", "starter", "solution"), + nargs="?", default=None) parser.add_argument("--json", action="store_true") parser.add_argument("--expect-failure", action="store_true") parser.add_argument("--receipt-out", metavar="PATH", help="write signed run receipts JSON (requires FLYPYTHON_CLAIM_SECRET)") args = parser.parse_args() - if args.implementation == "progress": - return run_progress(args.json, args.receipt_out) + if args.implementation in (None, "progress"): + # FP-820: the bare command is the learner's default — same engine as + # ``progress``, English-first output, exit 1 while gates stay open. + # The four explicit usages remain for maintainers. + return run_progress(args.json, args.receipt_out, + default=args.implementation is None) command = [sys.executable, "-m", "unittest", "discover", "-s", str(ROOT / "tests")] environment = os.environ.copy() diff --git a/courses/da-eda/lessons/L01.md b/courses/da-eda/lessons/L01.md index 0208b42..eb87b53 100644 --- a/courses/da-eda/lessons/L01.md +++ b/courses/da-eda/lessons/L01.md @@ -30,4 +30,4 @@ the dirt yourself: **Checkpoint:** you can state the three numbers without running the test suite — total rows, duplicates, unparsed values. Record your claim with -`python verify.py progress` (L01 is self-attested). +`python verify.py` (L01 is self-attested). diff --git a/courses/da-eda/lessons/L01_cn.md b/courses/da-eda/lessons/L01_cn.md index 0e70e4f..00523ff 100644 --- a/courses/da-eda/lessons/L01_cn.md +++ b/courses/da-eda/lessons/L01_cn.md @@ -29,4 +29,4 @@ hints: - 哪些 `date` 值不是日期? **检查点:** 不跑测试套件也能说出三个数——总行数、重复数、不可解析 -值数。用 `python verify.py progress` 记录你的认领(L01 为自我声明)。 +值数。用 `python verify.py` 记录你的认领(L01 为自我声明)。 diff --git a/courses/da-eda/lessons/L02.md b/courses/da-eda/lessons/L02.md index 83a7d25..a69799d 100644 --- a/courses/da-eda/lessons/L02.md +++ b/courses/da-eda/lessons/L02.md @@ -31,4 +31,4 @@ code, it printed something, ship it." EDA only counts if the numbers are **Checkpoint:** you have a written list of the expected keys and you can explain what each one means. Self-attested; record via -`python verify.py progress`. +`python verify.py`. diff --git a/courses/da-eda/lessons/L02_cn.md b/courses/da-eda/lessons/L02_cn.md index 8f86a76..43e77ed 100644 --- a/courses/da-eda/lessons/L02_cn.md +++ b/courses/da-eda/lessons/L02_cn.md @@ -28,4 +28,4 @@ hints: EDA 只有数字可核查才算数。 **检查点:** 你已写下期望键清单,并能解释每个键的含义。自我声明; -用 `python verify.py progress` 记录。 +用 `python verify.py` 记录。 diff --git a/courses/da-eda/lessons/L03.md b/courses/da-eda/lessons/L03.md index 014e7db..75a7174 100644 --- a/courses/da-eda/lessons/L03.md +++ b/courses/da-eda/lessons/L03.md @@ -32,4 +32,4 @@ Watch for the classic EDA traps: - `groupby().sum()` silently drops NaN — count them *before* cleaning. **Checkpoint:** `python verify.py starter` exits 0 — all twelve tests -pass. Claim code prints via `python verify.py progress`. +pass. Claim code prints via `python verify.py`. diff --git a/courses/da-eda/lessons/L03_cn.md b/courses/da-eda/lessons/L03_cn.md index d8f51bb..70b7152 100644 --- a/courses/da-eda/lessons/L03_cn.md +++ b/courses/da-eda/lessons/L03_cn.md @@ -31,4 +31,4 @@ Agent;它写的每一行都要过目。 - `groupby().sum()` 会悄悄丢掉 NaN——清洗*之前*就要数清楚。 **检查点:** `python verify.py starter` 退出 0——12 个测试全过。 -用 `python verify.py progress` 打印认领码。 +用 `python verify.py` 打印认领码。 diff --git a/courses/da-eda/lessons/L04.md b/courses/da-eda/lessons/L04.md index ef535d4..b6ad917 100644 --- a/courses/da-eda/lessons/L04.md +++ b/courses/da-eda/lessons/L04.md @@ -30,4 +30,4 @@ now (the starter is no longer unfinished). Then `python verify.py solution` — green. **Checkpoint:** both gates pass. Objective claim code via -`python verify.py progress`. +`python verify.py`. diff --git a/courses/da-eda/lessons/L04_cn.md b/courses/da-eda/lessons/L04_cn.md index 0b3159b..df0147c 100644 --- a/courses/da-eda/lessons/L04_cn.md +++ b/courses/da-eda/lessons/L04_cn.md @@ -29,5 +29,5 @@ hints: (starter 已不处于未完成状态)。再跑 `python verify.py solution`—— 全绿。 -**检查点:** 两道门禁都过。用 `python verify.py progress` 取客观 +**检查点:** 两道门禁都过。用 `python verify.py` 取客观 认领码。 diff --git a/courses/da-eda/lessons/L05.md b/courses/da-eda/lessons/L05.md index d5bbca4..5a9b1f7 100644 --- a/courses/da-eda/lessons/L05.md +++ b/courses/da-eda/lessons/L05.md @@ -27,4 +27,4 @@ assumptions break: column names, encodings, date formats, currencies. **Checkpoint:** you can name one assumption from this course's dataset that did *not* hold on your real data. Self-attested; record via -`python verify.py progress`. +`python verify.py`. diff --git a/courses/da-eda/lessons/L05_cn.md b/courses/da-eda/lessons/L05_cn.md index 0946aa2..a1b55a6 100644 --- a/courses/da-eda/lessons/L05_cn.md +++ b/courses/da-eda/lessons/L05_cn.md @@ -25,4 +25,4 @@ hints: 而在于发现哪些假设会崩:列名、编码、日期格式、币种。 **检查点:** 你能说出本课程数据集的哪一条假设在你的真实数据上 -*不成立*。自我声明;用 `python verify.py progress` 记录。 +*不成立*。自我声明;用 `python verify.py` 记录。 diff --git a/courses/da-eda/verify.py b/courses/da-eda/verify.py index fff86d0..dd7c580 100644 --- a/courses/da-eda/verify.py +++ b/courses/da-eda/verify.py @@ -84,6 +84,16 @@ def _claim_code(checkpoint_id): return base64.b32encode(digest).decode("ascii")[:8] +def _display_title(title): + # FP-820: shared-core checkpoint titles are stored "Chinese / English"; + # the default command prints English first, Chinese after (FP-709 debt). + if " / " in title: + left, _, right = title.partition(" / ") + if any("\u4e00" <= character <= "\u9fff" for character in left): + return right + " / " + left + return title + + def _run_suite(implementation): environment = os.environ.copy() environment["PYTHONPATH"] = str(ROOT / implementation) @@ -93,7 +103,7 @@ def _run_suite(implementation): ) -def run_progress(as_json, receipt_out=None): +def run_progress(as_json, receipt_out=None, default=False): starter = solution = None starter_ms = solution_ms = 0 if not _deps_available(): @@ -145,6 +155,33 @@ def run_progress(as_json, receipt_out=None): COURSE_ID, checkpoint["id"], passed=passed, tests=tests, duration_ms=ms, impl_dir=ROOT / "starter", solution_hash=solution_hash, secret=secret)) + if default and not as_json: + # FP-820: bare ``python verify.py`` — check the learner's own + # implementation, print per-checkpoint status and earned claim codes, + # English first then Chinese (FP-709 debt). ``progress`` keeps its + # exact published output; this block is the only new surface. + print("Course " + COURSE_ID) + print("Suites: starter " + ("passed" if starter_ok else "not passed") + + " / solution " + ("passed" if solution_ok else "not passed")) + for row in rows: + state = row["status"] + (" (self-attested)" if row["kind"] == "attested" else "") + code = row["claim_code"] if row["claim_code"] else "\u2014" + print(" " + row["id"] + " " + _display_title(row["title"]) + " [" + state + "] " + code) + open_gates = [row["id"] for row in rows + if row["kind"] == "objective" and row["status"] != "passed"] + if open_gates: + print("Next: keep implementing starter/ until " + ", ".join(open_gates) + + " show [passed]; then re-run: python verify.py") + else: + print("All objective checkpoints passed — the codes above are ready" + " to submit (batch POST /api/claims, see SKILL.md §5).") + print("\u4e2d\u6587\uff1a\u68c0\u67e5\u70b9\u72b6\u6001\u4e0e\u8ba4\u9886\u7801\u89c1\u4e0a\uff1b\u5e26 [passed]/[attest] \u7684\u884c\u6709\u8ba4\u9886\u7801\uff0c[open] \u7684\u884c\u7ee7\u7eed\u5728 starter/ \u91cc\u5b9e\u73b0\u540e\u518d\u8dd1\u3002") + print("Claim codes are self-reported evidence, recorded at flypython.com; never a certificate.") + print("\u8ba4\u9886\u7801\u662f\u81ea\u6211\u62a5\u544a\u7684\u8bc1\u636e\uff0c\u8bb0\u5f55\u5728 flypython.com\uff1b\u4e0d\u662f\u8bc1\u4e66\u3002") + if secret: + print(f"Signed run receipts prepared for {len(receipts)} gated checkpoint(s);" + " submit each with its claim to mark it as a local-run receipt.") + return 1 if open_gates else 0 if as_json: document = {"course": COURSE_ID, "starter_suite_passed": starter_ok, @@ -176,14 +213,19 @@ def run_progress(as_json, receipt_out=None): def main() -> int: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("implementation", choices=("progress", "starter", "solution")) + parser.add_argument("implementation", choices=("progress", "starter", "solution"), + nargs="?", default=None) parser.add_argument("--json", action="store_true") parser.add_argument("--expect-failure", action="store_true") parser.add_argument("--receipt-out", metavar="PATH", help="write signed run receipts JSON (requires FLYPYTHON_CLAIM_SECRET)") args = parser.parse_args() - if args.implementation == "progress": - return run_progress(args.json, args.receipt_out) + if args.implementation in (None, "progress"): + # FP-820: the bare command is the learner's default — same engine as + # ``progress``, English-first output, exit 1 while gates stay open. + # The four explicit usages remain for maintainers. + return run_progress(args.json, args.receipt_out, + default=args.implementation is None) if not _deps_available(): _deps_hint() diff --git a/courses/da-report/lessons/L01.md b/courses/da-report/lessons/L01.md index bd16806..a895ceb 100644 --- a/courses/da-report/lessons/L01.md +++ b/courses/da-report/lessons/L01.md @@ -26,4 +26,4 @@ Before writing code, list which numbers your report must carry — and where each one comes from in `results.json`. **Checkpoint:** you can trace every required number to its source field. -Self-attested; record via `python verify.py progress`. +Self-attested; record via `python verify.py`. diff --git a/courses/da-report/lessons/L01_cn.md b/courses/da-report/lessons/L01_cn.md index 8734053..22b62c3 100644 --- a/courses/da-report/lessons/L01_cn.md +++ b/courses/da-report/lessons/L01_cn.md @@ -25,4 +25,4 @@ hints: `revenue_by_region`——以及它们在 `results.json` 里的出处。 **检查点:** 每个必需数字你都能回溯到来源字段。自我声明;用 -`python verify.py progress` 记录。 +`python verify.py` 记录。 diff --git a/courses/da-report/lessons/L02.md b/courses/da-report/lessons/L02.md index 68f3c49..727be0b 100644 --- a/courses/da-report/lessons/L02.md +++ b/courses/da-report/lessons/L02.md @@ -25,4 +25,4 @@ Decide the skeleton first: title, section order (`Data quality` → `metrics` block. Prose fills structure — it never substitutes for it. **Checkpoint:** your section plan exists on paper before you touch -`starter/report.py`. Self-attested; record via `python verify.py progress`. +`starter/report.py`. Self-attested; record via `python verify.py`. diff --git a/courses/da-report/lessons/L02_cn.md b/courses/da-report/lessons/L02_cn.md index 1289240..5b0ca16 100644 --- a/courses/da-report/lessons/L02_cn.md +++ b/courses/da-report/lessons/L02_cn.md @@ -25,4 +25,4 @@ hints: 永远替代不了结构。 **检查点:** 动 `starter/report.py` 之前,章节计划已经写在纸上。 -自我声明;用 `python verify.py progress` 记录。 +自我声明;用 `python verify.py` 记录。 diff --git a/courses/da-report/lessons/L03.md b/courses/da-report/lessons/L03.md index 0e086c7..1000417 100644 --- a/courses/da-report/lessons/L03.md +++ b/courses/da-report/lessons/L03.md @@ -29,4 +29,4 @@ Implement the contract in `starter/report.py`. Watch for: - Number formatting: `33347.89` must appear with two decimals. **Checkpoint:** `python verify.py starter` exits 0. Claim code via -`python verify.py progress`. +`python verify.py`. diff --git a/courses/da-report/lessons/L03_cn.md b/courses/da-report/lessons/L03_cn.md index 6c06489..0ae333b 100644 --- a/courses/da-report/lessons/L03_cn.md +++ b/courses/da-report/lessons/L03_cn.md @@ -28,4 +28,4 @@ hints: - 数字格式:`33347.89` 必须以两位小数出现。 **检查点:** `python verify.py starter` 退出 0。用 -`python verify.py progress` 取认领码。 +`python verify.py` 取认领码。 diff --git a/courses/da-report/lessons/L04.md b/courses/da-report/lessons/L04.md index 69182b1..7890e70 100644 --- a/courses/da-report/lessons/L04.md +++ b/courses/da-report/lessons/L04.md @@ -29,4 +29,4 @@ explanation. `python verify.py solution` must be green. **Checkpoint:** both gates pass. Objective claim code via -`python verify.py progress`. +`python verify.py`. diff --git a/courses/da-report/lessons/L04_cn.md b/courses/da-report/lessons/L04_cn.md index 4686f85..9724f51 100644 --- a/courses/da-report/lessons/L04_cn.md +++ b/courses/da-report/lessons/L04_cn.md @@ -27,5 +27,5 @@ hints: `python verify.py starter --expect-failure` 现在必须退出非零; `python verify.py solution` 必须全绿。 -**检查点:** 两道门禁都过。用 `python verify.py progress` 取客观 +**检查点:** 两道门禁都过。用 `python verify.py` 取客观 认领码。 diff --git a/courses/da-report/lessons/L05.md b/courses/da-report/lessons/L05.md index 87fd066..4b655e6 100644 --- a/courses/da-report/lessons/L05.md +++ b/courses/da-report/lessons/L05.md @@ -26,4 +26,4 @@ or your conclusions honest. Write one sentence stating what was verified in your report and what was not. **Checkpoint:** that sentence exists. Self-attested; record via -`python verify.py progress`. +`python verify.py`. diff --git a/courses/da-report/lessons/L05_cn.md b/courses/da-report/lessons/L05_cn.md index f615db3..17d2029 100644 --- a/courses/da-report/lessons/L05_cn.md +++ b/courses/da-report/lessons/L05_cn.md @@ -25,4 +25,4 @@ hints: 哪些没有。 **检查点:** 这句话已写下。自我声明;用 -`python verify.py progress` 记录。 +`python verify.py` 记录。 diff --git a/courses/da-report/verify.py b/courses/da-report/verify.py index d6cf99c..ce7ad0c 100644 --- a/courses/da-report/verify.py +++ b/courses/da-report/verify.py @@ -59,6 +59,16 @@ def _claim_code(checkpoint_id): return base64.b32encode(digest).decode("ascii")[:8] +def _display_title(title): + # FP-820: shared-core checkpoint titles are stored "Chinese / English"; + # the default command prints English first, Chinese after (FP-709 debt). + if " / " in title: + left, _, right = title.partition(" / ") + if any("\u4e00" <= character <= "\u9fff" for character in left): + return right + " / " + left + return title + + def _run_suite(implementation): environment = os.environ.copy() environment["PYTHONPATH"] = str(ROOT / implementation) @@ -68,7 +78,7 @@ def _run_suite(implementation): ) -def run_progress(as_json, receipt_out=None): +def run_progress(as_json, receipt_out=None, default=False): import time t0 = time.monotonic() starter = _run_suite("starter") @@ -114,6 +124,33 @@ def run_progress(as_json, receipt_out=None): COURSE_ID, checkpoint["id"], passed=passed, tests=tests, duration_ms=ms, impl_dir=ROOT / "starter", solution_hash=solution_hash, secret=secret)) + if default and not as_json: + # FP-820: bare ``python verify.py`` — check the learner's own + # implementation, print per-checkpoint status and earned claim codes, + # English first then Chinese (FP-709 debt). ``progress`` keeps its + # exact published output; this block is the only new surface. + print("Course " + COURSE_ID) + print("Suites: starter " + ("passed" if starter_ok else "not passed") + + " / solution " + ("passed" if solution_ok else "not passed")) + for row in rows: + state = row["status"] + (" (self-attested)" if row["kind"] == "attested" else "") + code = row["claim_code"] if row["claim_code"] else "\u2014" + print(" " + row["id"] + " " + _display_title(row["title"]) + " [" + state + "] " + code) + open_gates = [row["id"] for row in rows + if row["kind"] == "objective" and row["status"] != "passed"] + if open_gates: + print("Next: keep implementing starter/ until " + ", ".join(open_gates) + + " show [passed]; then re-run: python verify.py") + else: + print("All objective checkpoints passed — the codes above are ready" + " to submit (batch POST /api/claims, see SKILL.md §5).") + print("\u4e2d\u6587\uff1a\u68c0\u67e5\u70b9\u72b6\u6001\u4e0e\u8ba4\u9886\u7801\u89c1\u4e0a\uff1b\u5e26 [passed]/[attest] \u7684\u884c\u6709\u8ba4\u9886\u7801\uff0c[open] \u7684\u884c\u7ee7\u7eed\u5728 starter/ \u91cc\u5b9e\u73b0\u540e\u518d\u8dd1\u3002") + print("Claim codes are self-reported evidence, recorded at flypython.com; never a certificate.") + print("\u8ba4\u9886\u7801\u662f\u81ea\u6211\u62a5\u544a\u7684\u8bc1\u636e\uff0c\u8bb0\u5f55\u5728 flypython.com\uff1b\u4e0d\u662f\u8bc1\u4e66\u3002") + if secret: + print(f"Signed run receipts prepared for {len(receipts)} gated checkpoint(s);" + " submit each with its claim to mark it as a local-run receipt.") + return 1 if open_gates else 0 if as_json: document = {"course": COURSE_ID, "starter_suite_passed": starter_ok, @@ -145,14 +182,19 @@ def run_progress(as_json, receipt_out=None): def main() -> int: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("implementation", choices=("progress", "starter", "solution")) + parser.add_argument("implementation", choices=("progress", "starter", "solution"), + nargs="?", default=None) parser.add_argument("--json", action="store_true") parser.add_argument("--expect-failure", action="store_true") parser.add_argument("--receipt-out", metavar="PATH", help="write signed run receipts JSON (requires FLYPYTHON_CLAIM_SECRET)") args = parser.parse_args() - if args.implementation == "progress": - return run_progress(args.json, args.receipt_out) + if args.implementation in (None, "progress"): + # FP-820: the bare command is the learner's default — same engine as + # ``progress``, English-first output, exit 1 while gates stay open. + # The four explicit usages remain for maintainers. + return run_progress(args.json, args.receipt_out, + default=args.implementation is None) environment = os.environ.copy() environment["PYTHONPATH"] = str(ROOT / args.implementation) diff --git a/courses/da-visualization/lessons/L01.md b/courses/da-visualization/lessons/L01.md index 9c3df44..a59b9a8 100644 --- a/courses/da-visualization/lessons/L01.md +++ b/courses/da-visualization/lessons/L01.md @@ -31,4 +31,4 @@ what each of the three required charts *claims*: If the claim is wrong, a beautiful chart is worse than no chart. **Checkpoint:** you can state the claim behind each chart. Self-attested; -record via `python verify.py progress`. +record via `python verify.py`. diff --git a/courses/da-visualization/lessons/L01_cn.md b/courses/da-visualization/lessons/L01_cn.md index 7084f98..d91c71c 100644 --- a/courses/da-visualization/lessons/L01_cn.md +++ b/courses/da-visualization/lessons/L01_cn.md @@ -30,4 +30,4 @@ hints: 声称要是错了,图越漂亮越糟糕。 **检查点:** 你能说出每张图背后的声称。自我声明;用 -`python verify.py progress` 记录。 +`python verify.py` 记录。 diff --git a/courses/da-visualization/lessons/L02.md b/courses/da-visualization/lessons/L02.md index e2ea29c..c3d8a64 100644 --- a/courses/da-visualization/lessons/L02.md +++ b/courses/da-visualization/lessons/L02.md @@ -26,4 +26,4 @@ will happily render *something* — your job is to define the numbers it must render. **Checkpoint:** you have a written spec mapping each output file to its -source aggregation. Self-attested; record via `python verify.py progress`. +source aggregation. Self-attested; record via `python verify.py`. diff --git a/courses/da-visualization/lessons/L02_cn.md b/courses/da-visualization/lessons/L02_cn.md index 59602a0..1302d93 100644 --- a/courses/da-visualization/lessons/L02_cn.md +++ b/courses/da-visualization/lessons/L02_cn.md @@ -25,4 +25,4 @@ hints: 你要做的是定义它必须渲染的数字。 **检查点:** 你已写下规格,把每个输出文件映射到它的聚合来源。 -自我声明;用 `python verify.py progress` 记录。 +自我声明;用 `python verify.py` 记录。 diff --git a/courses/da-visualization/lessons/L03.md b/courses/da-visualization/lessons/L03.md index 6ecc92b..33c5972 100644 --- a/courses/da-visualization/lessons/L03.md +++ b/courses/da-visualization/lessons/L03.md @@ -30,4 +30,4 @@ agent: - Tests check PNG magic bytes: a file that is not a real PNG fails. **Checkpoint:** `python verify.py starter` exits 0. Claim code via -`python verify.py progress`. +`python verify.py`. diff --git a/courses/da-visualization/lessons/L03_cn.md b/courses/da-visualization/lessons/L03_cn.md index 2d25951..4f004d8 100644 --- a/courses/da-visualization/lessons/L03_cn.md +++ b/courses/da-visualization/lessons/L03_cn.md @@ -28,4 +28,4 @@ hints: - 测试检查 PNG 魔数:不是真 PNG 的文件会失败。 **检查点:** `python verify.py starter` 退出 0。用 -`python verify.py progress` 取认领码。 +`python verify.py` 取认领码。 diff --git a/courses/da-visualization/lessons/L04.md b/courses/da-visualization/lessons/L04.md index 7609542..c4d1828 100644 --- a/courses/da-visualization/lessons/L04.md +++ b/courses/da-visualization/lessons/L04.md @@ -29,4 +29,4 @@ chart — the JSON is the auditable layer. `python verify.py solution` must be green. **Checkpoint:** both gates pass. Objective claim code via -`python verify.py progress`. +`python verify.py`. diff --git a/courses/da-visualization/lessons/L04_cn.md b/courses/da-visualization/lessons/L04_cn.md index dac8a50..c1b26e5 100644 --- a/courses/da-visualization/lessons/L04_cn.md +++ b/courses/da-visualization/lessons/L04_cn.md @@ -27,5 +27,5 @@ South 11200.48、总计 37830.12、45 天)。图里呈现的数字和 JSON 不 `python verify.py starter --expect-failure` 现在必须退出非零; `python verify.py solution` 必须全绿。 -**检查点:** 两道门禁都过。用 `python verify.py progress` 取客观 +**检查点:** 两道门禁都过。用 `python verify.py` 取客观 认领码。 diff --git a/courses/da-visualization/lessons/L05.md b/courses/da-visualization/lessons/L05.md index 519d0f6..b384c9b 100644 --- a/courses/da-visualization/lessons/L05.md +++ b/courses/da-visualization/lessons/L05.md @@ -28,4 +28,4 @@ Write one sentence you would attach to these charts stating exactly what was and was not verified. **Checkpoint:** you have that sentence written down. Self-attested; -record via `python verify.py progress`. +record via `python verify.py`. diff --git a/courses/da-visualization/lessons/L05_cn.md b/courses/da-visualization/lessons/L05_cn.md index 7ec011d..fefea89 100644 --- a/courses/da-visualization/lessons/L05_cn.md +++ b/courses/da-visualization/lessons/L05_cn.md @@ -27,4 +27,4 @@ hints: 写一句话附在这些图旁边,准确说明哪些东西验证过、哪些没有。 **检查点:** 这句话你已写下来。自我声明;用 -`python verify.py progress` 记录。 +`python verify.py` 记录。 diff --git a/courses/da-visualization/verify.py b/courses/da-visualization/verify.py index 56c2759..6d6f237 100644 --- a/courses/da-visualization/verify.py +++ b/courses/da-visualization/verify.py @@ -73,6 +73,16 @@ def _claim_code(checkpoint_id): return base64.b32encode(digest).decode("ascii")[:8] +def _display_title(title): + # FP-820: shared-core checkpoint titles are stored "Chinese / English"; + # the default command prints English first, Chinese after (FP-709 debt). + if " / " in title: + left, _, right = title.partition(" / ") + if any("\u4e00" <= character <= "\u9fff" for character in left): + return right + " / " + left + return title + + def _run_suite(implementation): environment = os.environ.copy() environment["PYTHONPATH"] = str(ROOT / implementation) @@ -82,7 +92,7 @@ def _run_suite(implementation): ) -def run_progress(as_json, receipt_out=None): +def run_progress(as_json, receipt_out=None, default=False): starter = solution = None starter_ms = solution_ms = 0 if not _deps_available(): @@ -134,6 +144,33 @@ def run_progress(as_json, receipt_out=None): COURSE_ID, checkpoint["id"], passed=passed, tests=tests, duration_ms=ms, impl_dir=ROOT / "starter", solution_hash=solution_hash, secret=secret)) + if default and not as_json: + # FP-820: bare ``python verify.py`` — check the learner's own + # implementation, print per-checkpoint status and earned claim codes, + # English first then Chinese (FP-709 debt). ``progress`` keeps its + # exact published output; this block is the only new surface. + print("Course " + COURSE_ID) + print("Suites: starter " + ("passed" if starter_ok else "not passed") + + " / solution " + ("passed" if solution_ok else "not passed")) + for row in rows: + state = row["status"] + (" (self-attested)" if row["kind"] == "attested" else "") + code = row["claim_code"] if row["claim_code"] else "\u2014" + print(" " + row["id"] + " " + _display_title(row["title"]) + " [" + state + "] " + code) + open_gates = [row["id"] for row in rows + if row["kind"] == "objective" and row["status"] != "passed"] + if open_gates: + print("Next: keep implementing starter/ until " + ", ".join(open_gates) + + " show [passed]; then re-run: python verify.py") + else: + print("All objective checkpoints passed — the codes above are ready" + " to submit (batch POST /api/claims, see SKILL.md §5).") + print("\u4e2d\u6587\uff1a\u68c0\u67e5\u70b9\u72b6\u6001\u4e0e\u8ba4\u9886\u7801\u89c1\u4e0a\uff1b\u5e26 [passed]/[attest] \u7684\u884c\u6709\u8ba4\u9886\u7801\uff0c[open] \u7684\u884c\u7ee7\u7eed\u5728 starter/ \u91cc\u5b9e\u73b0\u540e\u518d\u8dd1\u3002") + print("Claim codes are self-reported evidence, recorded at flypython.com; never a certificate.") + print("\u8ba4\u9886\u7801\u662f\u81ea\u6211\u62a5\u544a\u7684\u8bc1\u636e\uff0c\u8bb0\u5f55\u5728 flypython.com\uff1b\u4e0d\u662f\u8bc1\u4e66\u3002") + if secret: + print(f"Signed run receipts prepared for {len(receipts)} gated checkpoint(s);" + " submit each with its claim to mark it as a local-run receipt.") + return 1 if open_gates else 0 if as_json: document = {"course": COURSE_ID, "starter_suite_passed": starter_ok, @@ -165,14 +202,19 @@ def run_progress(as_json, receipt_out=None): def main() -> int: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("implementation", choices=("progress", "starter", "solution")) + parser.add_argument("implementation", choices=("progress", "starter", "solution"), + nargs="?", default=None) parser.add_argument("--json", action="store_true") parser.add_argument("--expect-failure", action="store_true") parser.add_argument("--receipt-out", metavar="PATH", help="write signed run receipts JSON (requires FLYPYTHON_CLAIM_SECRET)") args = parser.parse_args() - if args.implementation == "progress": - return run_progress(args.json, args.receipt_out) + if args.implementation in (None, "progress"): + # FP-820: the bare command is the learner's default — same engine as + # ``progress``, English-first output, exit 1 while gates stay open. + # The four explicit usages remain for maintainers. + return run_progress(args.json, args.receipt_out, + default=args.implementation is None) if not _deps_available(): _deps_hint() diff --git a/courses/hands-on-python-with-claude-code/COURSE.md b/courses/hands-on-python-with-claude-code/COURSE.md index 66fd60b..756fc75 100644 --- a/courses/hands-on-python-with-claude-code/COURSE.md +++ b/courses/hands-on-python-with-claude-code/COURSE.md @@ -4,7 +4,7 @@ type: course title: Hands-on Python with Claude Code summary: An agent-taught course that takes you from a cloned folder to a tested, verified Python report tool using Claude Code — including the task contract, the bounded change, and the objective pass/fail evidence. lang: en-US -content_version: 2 +content_version: 3 status: reviewed reviewed_on: 2026-09-12 badge: @@ -17,8 +17,9 @@ course_id: course-claude-code # Hands-on Python with Claude Code -> TL;DR: download this folder, open it in Claude Code, say **"start lesson 1"**, -> and the agent teaches you a verified workflow for AI-written Python: task +> TL;DR: set up Claude Code, install the FlyPython Skill, and let the agent +> fetch this course — lesson 1 walks you through all three steps and you +> download nothing. Then say **"start lesson 1"**, and the agent teaches you a verified workflow for AI-written Python: task > contract → smallest change → tests → objective verification. You finish with > a working report tool, a reproducible pass/fail command, and a pattern you > can apply to your own project. Claude Code is the tool used in the lessons, @@ -63,11 +64,12 @@ If you are the coding agent teaching this course, follow these rules: - **When to stop:** a lesson is done when its checkpoint command runs and the learner can say, in their own words, what failed and why. If the learner cannot, re-teach from the failing test, not from the solution. -- **How to use `verify.py`:** - `python verify.py starter --expect-failure` must reproduce the listed - failures; `python verify.py solution` must pass. "Done" means the starter - implementation passes the same suite after the learner's bounded change. -- **Honesty rules:** say what you did not verify. Do not claim the code is +- **How to use `verify.py`:** the learner's command is plain + `python verify.py` — it runs the suites, prints per-checkpoint status + and earned claim codes, and exits non-zero while the gated checkpoints + (l03, l04) are still open. `starter --expect-failure` and `solution` + remain maintainer checks. "Done" means the default command shows every + gate `[passed]` after the learner's bounded change.- **Honesty rules:** say what you did not verify. Do not claim the code is production-ready. Do not promise learning or career outcomes. ## What this course does NOT cover @@ -81,7 +83,7 @@ companion repository has guides and playbooks — link, don't improvise. - Badge: **Reproduce the failure Badge** (badge id `course-claude-code`) - earned by claiming all five checkpoints. - Challenges: L01-L05 checkpoints, 10 points each; +50 course-badge bonus when all five are claimed on flypython.com. -- Evidence: `python verify.py progress` - L03 (bounded change) and L04 (verify & review) are objectively gated by the suite; L01/L02/L05 are learner-attested. +- Evidence: `python verify.py` - L03 (bounded change) and L04 (verify & review) are objectively gated by the suite; L01/L02/L05 are learner-attested. - Submission: each passed checkpoint prints a deterministic claim code; record it on flypython.com against your account. Self-reported evidence, never a certificate. ## Folder map diff --git a/courses/hands-on-python-with-claude-code/COURSE_cn.md b/courses/hands-on-python-with-claude-code/COURSE_cn.md index 3b8e887..9a08206 100644 --- a/courses/hands-on-python-with-claude-code/COURSE_cn.md +++ b/courses/hands-on-python-with-claude-code/COURSE_cn.md @@ -4,7 +4,7 @@ type: course title: 用 Claude Code 实战 Python summary: 一门由 Agent 授课的课程:从下载课程文件夹到交付一个经过测试与验证的 Python 报表工具——包括任务契约、最小变更和客观的通过/失败证据。 lang: zh-CN -content_version: 2 +content_version: 3 status: reviewed reviewed_on: 2026-09-12 badge: @@ -17,7 +17,8 @@ course_id: course-claude-code # 用 Claude Code 实战 Python -> 摘要:下载本文件夹,在 Claude Code 中打开,说一句 **"开始第 1 课"**,Agent +> 摘要:装好 Claude Code、装上 FlyPython Skill、让 Agent 取回本课文件(第 1 课 +> 就是这三步,你什么都不用下载),说一句 **"开始第 1 课"**,Agent > 就会带你走完一套经过验证的 AI 写 Python 工作流:任务契约 → 最小变更 → > 测试 → 客观验证。课程结束时你会得到一个可运行的报表工具、一条可复现的 > 通过/失败命令,以及能迁移到自己项目里的模式。课程以 Claude Code 为教学 @@ -56,10 +57,10 @@ course_id: course-claude-code - **何时停止:** 当检查点命令运行通过、且学习者能用自己的话说清"什么 失败了、为什么"时,一课才算完成。说不出来就基于失败的测试重新讲, 而不是直接看答案。 -- **`verify.py` 用法:** `python verify.py starter --expect-failure` 必须 - 复现列出的失败;`python verify.py solution` 必须全部通过。"完成"指 - 学习者的最小变更让 starter 实现通过同一套测试。 -- **诚实规则:** 说明哪些内容没有验证过。不要宣称代码已达到生产可用。 +- **如何使用 `verify.py`:** 学员命令就是 `python verify.py`——它跑套件、 + 打印逐检查点状态与已获认领码,在门控检查点(l03、l04)未全通过时以 + 非零退出。`starter --expect-failure` 与 `solution` 留作维护者检查。 + 「完成」指学习者做完有界改动后,默认命令把所有门显示为 `[passed]`。- **诚实规则:** 说明哪些内容没有验证过。不要宣称代码已达到生产可用。 不承诺任何学习或职业结果。 ## 本课程不涉及的内容 @@ -73,7 +74,7 @@ Claude Code 安装、提示词技巧、多文件架构、pandas 或真正的 Exc - 徽章:**复现故障徽章**(徽章 id `course-claude-code`)——认领全部五个检查点后获得。 - 挑战:L01–L05 检查点各 10 分;五项全部在 flypython.com 认领后另加 50 分课程徽章奖励。 -- 证据:`python verify.py progress` —— L03(边界修改)与 L04(验证与审查)由测试套件客观判定;L01/L02/L05 为学习者自报。 +- 证据:`python verify.py` —— L03(边界修改)与 L04(验证与审查)由测试套件客观判定;L01/L02/L05 为学习者自报。 - 提交:每个通过的检查点会打印确定性认领码,在 flypython.com 上记入你的账号。这是自我报告的证据,绝不是证书。 ## 文件夹结构 diff --git a/courses/hands-on-python-with-claude-code/REVIEW.md b/courses/hands-on-python-with-claude-code/REVIEW.md index 6e4e844..d1a63b5 100644 --- a/courses/hands-on-python-with-claude-code/REVIEW.md +++ b/courses/hands-on-python-with-claude-code/REVIEW.md @@ -23,6 +23,20 @@ - Course status remains `reviewed` for content and code; the agent-teaching sample is tracked as launch evidence, not a content blocker. +## 2026-09-13 — 0.0.8 rework: lesson 1 is now tool → Skill → agent fetch (FP-822) + +- L01 (EN+ZH) rewritten around the fixed order: get the tool running → + install the FlyPython Skill (network access included) → let the agent + fetch this course's files via the files endpoint. All "download this + folder" wording removed from `COURSE.md` and lessons. +- The learner's check command across lessons is now the bare + `python verify.py` (FP-820): per-checkpoint status + claim codes, + non-zero exit while gated checkpoints are open. `solution` is no longer + presented as a completion standard. +- Tool-specific Skill install and network steps are authored from the + same official documentation as the course; **not yet exercised inside + the tool** — the pending live run-through below still stands. + ## Deviation log ### 2026-09-13 — agent solvability run (challenge mode, not taught mode) diff --git a/courses/hands-on-python-with-claude-code/lessons/L01.md b/courses/hands-on-python-with-claude-code/lessons/L01.md index 71aa5c1..22283dc 100644 --- a/courses/hands-on-python-with-claude-code/lessons/L01.md +++ b/courses/hands-on-python-with-claude-code/lessons/L01.md @@ -1,28 +1,69 @@ --- id: course-claude-code-l01 type: course -title: "Lesson 1: Reproduce the failure before you fix anything" -summary: 'Clone the folder, meet the starter, and make the failure objective with verify.py — the habit that separates "it runs" from "it ships".' +title: "Lesson 1: Set up Claude Code, install the FlyPython Skill, fetch the course" +summary: "Get Claude Code running, install the FlyPython Skill with network access approved, and let the agent fetch this course's files — you download nothing. Then make the starter's failure objective." lang: en-US -content_version: 1 +content_version: 2 status: reviewed reviewed_on: 2026-09-12 hints: - title: "What this checkpoint tests" - body: "That you can reproduce the failure before fixing it: `python verify.py starter --expect-failure` must print the named failing tests. The checkpoint is seeing the failure objectively, not repairing it." + body: "That you can reproduce the failure before fixing it: `python verify.py` must show l03 and l04 as `[open]` (`starter --expect-failure` prints the named tests). The checkpoint is seeing the failure objectively, not repairing it." - title: "Read the failure names as a contract" body: "Each failing test name maps to one line in `TASK.md` — e.g. `test_non_numeric_amount_is_invalid` is the contract's 'a non-numeric numeric_field value'. If a name maps to no contract line, that drift is worth noticing." - title: "When you are done" - body: "You can say out loud which nine behaviors the suite proves and why the starter cannot fake them. `python verify.py progress` then shows this checkpoint's claim code." + body: "You can say out loud which nine behaviors the suite proves and why the starter cannot fake them. `python verify.py` then shows this checkpoint's claim code." --- -# Lesson 1: Reproduce the failure before you fix anything +# Lesson 1: Set up Claude Code, install the FlyPython Skill, fetch the course ## Objective -By the end of this lesson you can run the course's objective verification -command, explain what "the starter fails" means in test names, and say which -four product behaviors are missing. No code is changed in this lesson. +By the end of this lesson Claude Code is running, the FlyPython Skill is +installed in it (with network access approved), and the agent has fetched +this course's files into your working folder — you downloaded nothing by +hand. With the folder in place you run the course's objective check and +explain what "the starter fails" means in test names. No code is changed +in this lesson. + +## Step 1 — Get Claude Code running + +Install the Claude Code CLI +(`npm install -g @anthropic-ai/claude-code`), create an empty working +folder, and start `claude` in it. + +## Step 2 — Install the FlyPython Skill (network included) + +Save the Skill as a project skill — run this in the working folder: + +```bash +mkdir -p .claude/skills/flypython +curl -s https://flypython.com/skills/flypython/SKILL.md -o .claude/skills/flypython/SKILL.md +``` + +(A personal copy for all projects: the same command under `~/.claude/skills/`.) +Claude Code reaches the web through permission prompts — when it asks to +fetch or curl, approve. If you run it inside a network-blocking sandbox, +run the curl yourself and paste the file instead. + +The Skill is one file, the same in every tool: it tells the agent how to +authorize you, fetch course files, verify, and submit claim codes. + +## Step 3 — Let the agent fetch this course (you download nothing) + +Start a new session/chat/thread in the working folder and paste this one +sentence: + +> Read https://flypython.com/skills/flypython/SKILL.md and start the FlyPython course `hands-on-python-with-claude-code`. + +The agent gives you an authorization link with a short code. Open it, log +in, make sure the code matches what the agent showed you, and click +Allow — then tell the agent "done". It takes a token once and writes this +course's files to `courses/hands-on-python-with-claude-code/`. + +**Prerequisite:** these courses need a coding agent that can run commands +*and* reach the network. A chat-only web AI cannot do them. ## Why this lesson exists @@ -40,10 +81,18 @@ AI-generated code looks like — clean surface, missing behavior. Now open ## The lesson -Run the objective verification command from the course root: +The course files are in `courses/hands-on-python-with-claude-code/` now — no download happened on +your side. Ask the agent to survey them without changing anything: + +**"Read COURSE.md — it is the teaching contract for this folder. Then read +TASK.md and starter/report_tool.py. Do not change any file. Tell me which +behaviors the tests expect that the starter does not have."** + +While it reads, run the learner's command — your single check for the +rest of the course: ```bash -python verify.py starter --expect-failure +python verify.py ``` You should see the starter reproduce four real-world failure classes, mapped @@ -58,14 +107,16 @@ to test names: | `test_write_report_creates_missing_parent_directories` | the tool cannot write into a fresh output directory | | `test_run_scenario_writes_report_file`, `test_main_prints_summary_and_returns_zero` | end-to-end consequences of the above | -Then run the reviewed implementation the same way: +If you want the missing behaviors as exact failing test names, the +maintainer command prints them: ```bash -python verify.py solution +python verify.py starter --expect-failure ``` -Both commands exit 0 — but they mean opposite things. The first proves the -starter is *correctly unfinished*; the second proves the target is reachable. +(The reference `solution/` exists so maintainers can prove the target is +reachable. You never need to run it, and it is never the completion +standard — your implementation in `starter/` is.) That pair is your completion evidence for the whole course. ## Exercise @@ -77,16 +128,19 @@ will trigger. Then check yourself against the table above. ## Checkpoint -Run `python verify.py starter --expect-failure` and -`python verify.py solution`. You pass this lesson when you can answer, without -looking: +Run `python verify.py` in the course folder. You pass this lesson when you +can answer, without looking: 1. Which four behaviors separate starter from solution? -2. Why is "expected failure" a success condition, not a contradiction? +2. Why does `python verify.py` exit non-zero on purpose — what state is + it reporting, and why is that a success condition rather than an error? 3. What does the row you found in the exercise do to the starter — crash, isolate, or silently pass? +This checkpoint's claim code is already in the default command's output — +submit it only once you can answer the questions. + ## Expected evidence -The transcript of both commands, and your three answers. Keep them; Lesson 4 +The transcript of the default command, and your three answers. Keep them; Lesson 4 will ask for them again. diff --git a/courses/hands-on-python-with-claude-code/lessons/L01_cn.md b/courses/hands-on-python-with-claude-code/lessons/L01_cn.md index 987ef5c..cffd1d3 100644 --- a/courses/hands-on-python-with-claude-code/lessons/L01_cn.md +++ b/courses/hands-on-python-with-claude-code/lessons/L01_cn.md @@ -1,10 +1,10 @@ --- id: course-claude-code-l01 type: course -title: "第 1 课:先复现故障,再谈修复" -summary: 打开课程文件夹,认识 starter,用 verify.py 把"坏了"变成客观证据——这是"能跑"与"能交付"的分水岭。 +title: "第 1 课:装好 Claude Code、装上 FlyPython Skill、让 Agent 取回课程" +summary: "先把 Claude Code 跑起来,装好 FlyPython Skill 并打开联网权限,让 Agent 取回本课文件——你不下载任何东西,然后让 starter 的失败变得客观。" lang: zh-CN -content_version: 1 +content_version: 2 status: reviewed reviewed_on: 2026-09-12 hints: @@ -13,17 +13,54 @@ hints: - title: "把失败名当成契约读" body: "每个失败的测试名都对应 `TASK.md` 里的一行——比如 `test_non_numeric_amount_is_invalid` 对应契约的『非数值的 numeric_field 值』。如果有名字对不上契约,那处漂移值得注意。" - title: "完成标准" - body: "你能说清楚这九个测试各自证明了什么、为什么 starter 装不出来。然后 `python verify.py progress` 就会显示本检查点的认领码。" + body: "你能说清楚这九个测试各自证明了什么、为什么 starter 装不出来。然后 `python verify.py` 就会显示本检查点的认领码。" --- -# 第 1 课:先复现故障,再谈修复 +# 第 1 课:装好 Claude Code、装上 FlyPython Skill、让 Agent 取回课程 ## 目标 -完成本课后,你能运行课程的客观验证命令,用测试名说清"starter 失败" -的含义,并指出缺失的四个产品行为。本课不改任何代码。 +本课结束时,Claude Code 已经跑起来、FlyPython Skill 已装好(联网权限已 +打开),Agent 已把本课文件取回你的工作目录——你没有手动下载任何 +东西。文件夹就位后,你运行课程的客观检查命令,并解释「starter +失败」在测试名里的含义。本课不改任何代码。 -## 为什么要有这一课 +## 第 1 步——把 Claude Code 跑起来 + +安装 Claude Code CLI +(`npm install -g @anthropic-ai/claude-code`),新建一个空工作目录, +在里面运行 `claude`。 + +## 第 2 步——装 FlyPython Skill(含联网权限) + +把 Skill 存为项目级技能——在工作目录里运行: + +```bash +mkdir -p .claude/skills/flypython +curl -s https://flypython.com/skills/flypython/SKILL.md -o .claude/skills/flypython/SKILL.md +``` + +(全项目通用的个人副本:在 `~/.claude/skills/` 下执行同样命令。) +Claude Code 通过权限提示访问网络——它请求 fetch 或 curl 时批准即可。 +如果你在禁网的沙箱里运行,就自己在终端跑 curl,把文件粘贴过去。 + +Skill 只是一个文件,在每个工具里都一样:它告诉 Agent 如何为你 +授权、取课程文件、验证、提交认领码。 + +## 第 3 步——让 Agent 取回本课文件(你不下载) + +在工作目录里新开一个会话/对话/线程,粘贴这一句话: + +> Read https://flypython.com/skills/flypython/SKILL.md and start the FlyPython course `hands-on-python-with-claude-code`. + +Agent 会给你一个授权链接和一组短码。打开链接、登录、核对码与 +Agent 显示的一致后点「允许」,然后对 Agent 说「好了」。它取一次 +token,把本课文件写到 `courses/hands-on-python-with-claude-code/`。 + +**前提**:这些课程需要一个能执行命令**并且**能联网的编码 Agent。 +只能聊天的网页 AI 做不了。 + +## 为什么有这一课 大多数 AI 写的 Python 死法相同:演示输入没问题,真实输入一到,脚本 在第 3 行崩溃——或者更糟,悄悄算出一个错误的数字。修复不是从更好的 @@ -36,12 +73,19 @@ hints: 的函数、类型标注、命令行入口。这正是 AI 生成代码的典型样子——表面 干净,行为缺失。然后打开 `TASK.md` 放在手边。 -## 课程内容 +## 本课内容 + +本课文件现在已经位于 `courses/hands-on-python-with-claude-code/`——你这边没有发生任何下载。 +让 Agent 先只读不动手: -在课程根目录运行客观验证命令: +**“读 COURSE.md——它是本文件夹的教学契约。再读 TASK.md 和 +starter/report_tool.py。不要改任何文件。告诉我测试期望哪些行为、 +starter 缺了哪些。”** + +趁它读的时候,运行学员命令——整个课程你只需要这一条检查命令: ```bash -python verify.py starter --expect-failure +python verify.py ``` 你会看到 starter 复现四类真实世界的故障,并与测试名一一对应: @@ -55,14 +99,14 @@ python verify.py starter --expect-failure | `test_write_report_creates_missing_parent_directories` | 工具无法写入新建的输出目录 | | `test_run_scenario_writes_report_file`、`test_main_prints_summary_and_returns_zero` | 以上问题在端到端层面的体现 | -再用同样的方式运行已审核的实现: +想看缺失行为对应的失败测试名,维护者命令会打印它们: ```bash -python verify.py solution +python verify.py starter --expect-failure ``` -两条命令都退出 0——但含义相反。第一条证明 starter 处于"正确的未完成" -状态;第二条证明目标可达。这一对命令就是整个课程的完成证据。 +(参考答案 `solution/` 是给维护者证明目标可达用的。你不需要运行 +它,它也从来不是完成标准——你的实现(`starter/`)才是。)这一对命令就是整个课程的完成证据。 ## 练习 @@ -72,12 +116,17 @@ python verify.py solution ## 检查点 -运行 `python verify.py starter --expect-failure` 和 -`python verify.py solution`。不看资料回答出以下三问,即通过本课: +在课程文件夹里运行 `python verify.py`。不看材料能回答下面三个 +问题即算通过: + +1. 哪四个行为把 starter 和 solution 区分开? +2. `python verify.py` 为什么故意以非零退出——它在报告什么状态? + 为什么这是成功条件而不是报错? +3. 你在练习里找到的那一行会让 starter 崩溃、被隔离,还是悄悄 + 通过? -1. 分隔 starter 与 solution 的是哪四个行为? -2. 为什么"预期失败"是成功条件而不是矛盾? -3. 你在练习中找到的那一行,对 starter 是崩溃、隔离还是悄悄通过? +本检查点的认领码已经在默认命令的输出里——能回答这些问题之后再 +提交。 ## 预期证据 diff --git a/courses/hands-on-python-with-claude-code/verify.py b/courses/hands-on-python-with-claude-code/verify.py index a088855..400e691 100644 --- a/courses/hands-on-python-with-claude-code/verify.py +++ b/courses/hands-on-python-with-claude-code/verify.py @@ -68,6 +68,16 @@ def _claim_code(checkpoint_id): ).digest() return base64.b32encode(digest).decode("ascii")[:8] + +def _display_title(title): + # FP-820: shared-core checkpoint titles are stored "Chinese / English"; + # the default command prints English first, Chinese after (FP-709 debt). + if " / " in title: + left, _, right = title.partition(" / ") + if any("\u4e00" <= character <= "\u9fff" for character in left): + return right + " / " + left + return title + def _run_suite(implementation): environment = os.environ.copy() environment["PYTHONPATH"] = str(ROOT / implementation) @@ -76,7 +86,7 @@ def _run_suite(implementation): env=environment, check=False, capture_output=True, text=True, ) -def run_progress(as_json, receipt_out=None): +def run_progress(as_json, receipt_out=None, default=False): import time t0 = time.monotonic() starter = _run_suite("starter") @@ -122,6 +132,33 @@ def run_progress(as_json, receipt_out=None): COURSE_ID, checkpoint["id"], passed=passed, tests=tests, duration_ms=ms, impl_dir=ROOT / "starter", solution_hash=solution_hash, secret=secret)) + if default and not as_json: + # FP-820: bare ``python verify.py`` — check the learner's own + # implementation, print per-checkpoint status and earned claim codes, + # English first then Chinese (FP-709 debt). ``progress`` keeps its + # exact published output; this block is the only new surface. + print("Course " + COURSE_ID) + print("Suites: starter " + ("passed" if starter_ok else "not passed") + + " / solution " + ("passed" if solution_ok else "not passed")) + for row in rows: + state = row["status"] + (" (self-attested)" if row["kind"] == "attested" else "") + code = row["claim_code"] if row["claim_code"] else "\u2014" + print(" " + row["id"] + " " + _display_title(row["title"]) + " [" + state + "] " + code) + open_gates = [row["id"] for row in rows + if row["kind"] == "objective" and row["status"] != "passed"] + if open_gates: + print("Next: keep implementing starter/ until " + ", ".join(open_gates) + + " show [passed]; then re-run: python verify.py") + else: + print("All objective checkpoints passed — the codes above are ready" + " to submit (batch POST /api/claims, see SKILL.md §5).") + print("\u4e2d\u6587\uff1a\u68c0\u67e5\u70b9\u72b6\u6001\u4e0e\u8ba4\u9886\u7801\u89c1\u4e0a\uff1b\u5e26 [passed]/[attest] \u7684\u884c\u6709\u8ba4\u9886\u7801\uff0c[open] \u7684\u884c\u7ee7\u7eed\u5728 starter/ \u91cc\u5b9e\u73b0\u540e\u518d\u8dd1\u3002") + print("Claim codes are self-reported evidence, recorded at flypython.com; never a certificate.") + print("\u8ba4\u9886\u7801\u662f\u81ea\u6211\u62a5\u544a\u7684\u8bc1\u636e\uff0c\u8bb0\u5f55\u5728 flypython.com\uff1b\u4e0d\u662f\u8bc1\u4e66\u3002") + if secret: + print(f"Signed run receipts prepared for {len(receipts)} gated checkpoint(s);" + " submit each with its claim to mark it as a local-run receipt.") + return 1 if open_gates else 0 if as_json: document = {"course": COURSE_ID, "starter_suite_passed": starter_ok, @@ -152,14 +189,19 @@ def run_progress(as_json, receipt_out=None): def main() -> int: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("implementation", choices=("progress", "starter", "solution")) + parser.add_argument("implementation", choices=("progress", "starter", "solution"), + nargs="?", default=None) parser.add_argument("--json", action="store_true") parser.add_argument("--expect-failure", action="store_true") parser.add_argument("--receipt-out", metavar="PATH", help="write signed run receipts JSON (requires FLYPYTHON_CLAIM_SECRET)") args = parser.parse_args() - if args.implementation == "progress": - return run_progress(args.json, args.receipt_out) + if args.implementation in (None, "progress"): + # FP-820: the bare command is the learner's default — same engine as + # ``progress``, English-first output, exit 1 while gates stay open. + # The four explicit usages remain for maintainers. + return run_progress(args.json, args.receipt_out, + default=args.implementation is None) command = [sys.executable, "-m", "unittest", "discover", "-s", str(ROOT / "tests")] environment = os.environ.copy() diff --git a/courses/hands-on-with-cursor/COURSE.md b/courses/hands-on-with-cursor/COURSE.md index 06146dd..935b53a 100644 --- a/courses/hands-on-with-cursor/COURSE.md +++ b/courses/hands-on-with-cursor/COURSE.md @@ -4,7 +4,7 @@ type: course title: Hands-on with Cursor summary: The verified Python workflow — task contract, bounded change, objective verify.py evidence — taught hands-on in Cursor, where Agent mode runs the loop and you supervise diffs and project rules. lang: en-US -content_version: 1 +content_version: 2 status: reviewed reviewed_on: 2026-09-13 badge: @@ -17,8 +17,9 @@ course_id: course-cursor # Hands-on with Cursor -> TL;DR: download this folder, open it as a project in Cursor, open an Agent -> chat, and say **"start lesson 1"**. You finish with a working report tool, +> TL;DR: set up Cursor, install the FlyPython Skill, and let the agent fetch +> this course — lesson 1 walks you through all three steps and you download +> nothing. Open an Agent chat and say **"start lesson 1"**. You finish with a working report tool, > a reproducible pass/fail command, and the Cursor workflow: Agent/Ask/Plan > modes, `.cursor/rules` project rules, and diff review. The exercise core is > the same one the Claude Code and Codex courses use — only the tool you @@ -68,12 +69,12 @@ If you are the Cursor agent teaching this course, follow these rules: - **When to stop:** a lesson is done when its checkpoint command runs and the learner can say, in their own words, what failed and why. If the learner cannot, re-teach from the failing test, not from the solution. -- **How to use `verify.py`:** run it in the integrated terminal, or let the - Agent run it — `python verify.py starter --expect-failure` must reproduce - the listed failures; `python verify.py solution` must pass. "Done" means - the starter implementation passes the same suite after the learner's - bounded change. -- **Honesty rules:** say what you did not verify. Do not claim the code is +- **How to use `verify.py`:** the learner's command is plain + `python verify.py` — it runs the suites, prints per-checkpoint status + and earned claim codes, and exits non-zero while the gated checkpoints + (l03, l04) are still open. `starter --expect-failure` and `solution` + remain maintainer checks. "Done" means the default command shows every + gate `[passed]` after the learner's bounded change.- **Honesty rules:** say what you did not verify. Do not claim the code is production-ready. Do not promise learning or career outcomes. ## What this course does NOT cover @@ -86,7 +87,7 @@ guides and playbooks — link, don't improvise. - Badge: **Reproduce with Cursor in the loop Badge** (badge id `course-cursor`) — earned by claiming all five checkpoints. - Challenges: L01–L05 checkpoints, 10 points each; +50 course-badge bonus when all five are claimed on flypython.com. -- Evidence: `python verify.py progress` — L03 (bounded change) and L04 (verify & review) are objectively gated by the suite; L01/L02/L05 are learner-attested. +- Evidence: `python verify.py` — L03 (bounded change) and L04 (verify & review) are objectively gated by the suite; L01/L02/L05 are learner-attested. - Submission: each passed checkpoint prints a deterministic claim code; record it on flypython.com against your account. Self-reported evidence, never a certificate. ## Folder map diff --git a/courses/hands-on-with-cursor/COURSE_cn.md b/courses/hands-on-with-cursor/COURSE_cn.md index adf6a5a..b06a955 100644 --- a/courses/hands-on-with-cursor/COURSE_cn.md +++ b/courses/hands-on-with-cursor/COURSE_cn.md @@ -4,7 +4,7 @@ type: course title: "上手 Cursor" summary: "经过验证的 Python 工作流——任务契约、有界改动、客观 verify.py 证据——在 Cursor 里手把手教:由 Agent 模式跑闭环,你监督 diff 与项目规则。" lang: zh-CN -content_version: 1 +content_version: 2 status: reviewed reviewed_on: 2026-09-13 badge: @@ -17,8 +17,8 @@ course_id: course-cursor # 上手 Cursor -> 一句话:下载本文件夹,在 Cursor 里作为项目打开,开一个 Agent 对话, -> 对它说**「开始第 1 课」**。完成时你会得到一个能跑的报表工具、一条 +> 一句话:装好 Cursor、装上 FlyPython Skill、让 Agent 取回本课文件(第 1 课 +> 就是这三步,你什么都不用下载),开一个 Agent 对话,对它说**「开始第 1 课」**。完成时你会得到一个能跑的报表工具、一条 > 可复现的通过/失败命令,以及 Cursor 工作流:Agent/Ask/Plan 模式、 > `.cursor/rules` 项目规则与 diff 审查。练习核心与 Claude Code、Codex > 课程完全相同——变的只是你驱动的工具。 @@ -61,11 +61,10 @@ course_id: course-cursor - **何时停下:** 一节课的检查点命令跑通、且学习者能用自己的话说清 哪里坏了、为什么,这节课才算完。学习者说不出就从失败测试重新教, 不要从 solution 教。 -- **如何使用 `verify.py`:** 在集成终端里跑,或让 Agent 跑—— - `python verify.py starter --expect-failure` 必须复现列出的失败; - `python verify.py solution` 必须通过。「完成」指学习者做完有界 - 改动后,starter 实现通过同一套测试。 -- **诚实规则:** 说清你没验证什么。不要声称代码达到生产可用。不要 +- **如何使用 `verify.py`:** 学员命令就是 `python verify.py`——它跑套件、 + 打印逐检查点状态与已获认领码,在门控检查点(l03、l04)未全通过时以 + 非零退出。`starter --expect-failure` 与 `solution` 留作维护者检查。 + 「完成」指学习者做完有界改动后,默认命令把所有门显示为 `[passed]`。- **诚实规则:** 说清你没验证什么。不要声称代码达到生产可用。不要 承诺学习或职业结果。 ## 本课程不覆盖什么 @@ -77,7 +76,7 @@ Cursor 的安装、套餐定价、模型选择、MCP 配置、团队/管理功 - 徽章:**Cursor 协同复现徽章**(badge id `course-cursor`)——认领全部五个检查点获得。 - 挑战:L01–L05 检查点,各 10 分;在 flypython.com 上集齐五个再加 50 分课程徽章奖励。 -- 证据:`python verify.py progress`——L03(有界改动)与 L04(验证与评审)由套件客观把关;L01/L02/L05 为学习者自我报告。 +- 证据:`python verify.py`——L03(有界改动)与 L04(验证与评审)由套件客观把关;L01/L02/L05 为学习者自我报告。 - 提交:每个通过的检查点打印一个确定性的认领码;在 flypython.com 上记入你的账号。这是自我报告的证据,从来不是证书。 ## 文件夹地图 diff --git a/courses/hands-on-with-cursor/REVIEW.md b/courses/hands-on-with-cursor/REVIEW.md index 3da093a..256a26c 100644 --- a/courses/hands-on-with-cursor/REVIEW.md +++ b/courses/hands-on-with-cursor/REVIEW.md @@ -17,6 +17,20 @@ used; every Cursor behavior described in the lessons is sourced from official documentation, not observed. +## 2026-09-13 — 0.0.8 rework: lesson 1 is now tool → Skill → agent fetch (FP-822) + +- L01 (EN+ZH) rewritten around the fixed order: get the tool running → + install the FlyPython Skill (network access included) → let the agent + fetch this course's files via the files endpoint. All "download this + folder" wording removed from `COURSE.md` and lessons. +- The learner's check command across lessons is now the bare + `python verify.py` (FP-820): per-checkpoint status + claim codes, + non-zero exit while gated checkpoints are open. `solution` is no longer + presented as a completion standard. +- Tool-specific Skill install and network steps are authored from the + same official documentation as the course; **not yet exercised inside + the tool** — the pending live run-through below still stands. + ## Pending before this course can be called taught-in-tool - One full agent-taught run-through inside Cursor ("start lesson 1" through diff --git a/courses/hands-on-with-cursor/lessons/L01.md b/courses/hands-on-with-cursor/lessons/L01.md index 3001f7e..aa1b258 100644 --- a/courses/hands-on-with-cursor/lessons/L01.md +++ b/courses/hands-on-with-cursor/lessons/L01.md @@ -1,28 +1,67 @@ --- id: course-cursor-l01 type: course -title: "Lesson 1: Reproduce the failure inside Cursor" -summary: "Open the folder as a Cursor project, let Agent mode read the teaching contract, and make the failure objective with verify.py — inside the editor, not beside it." +title: "Lesson 1: Set up Cursor, install the FlyPython Skill, fetch the course" +summary: "Get Cursor running, install the FlyPython Skill with network access approved, and let the agent fetch this course's files — you download nothing. Then make the starter's failure objective." lang: en-US -content_version: 1 +content_version: 2 status: reviewed reviewed_on: 2026-09-13 hints: - title: "What this checkpoint tests" - body: "That you can reproduce the failure before fixing it: `python verify.py starter --expect-failure` must print the named failing tests. In Cursor, Agent mode is where the work happens — the command is still the evidence." + body: "That you can reproduce the failure before fixing it: `python verify.py` must show l03 and l04 as `[open]` (`starter --expect-failure` prints the named tests). In Cursor, Agent mode is where the work happens — the command is still the evidence." - title: "Ask vs Agent" body: "Ask mode answers questions without touching files; Agent mode reads, edits, and runs commands. This lesson only needs the Agent to read — tell it not to change anything." - title: "When you are done" - body: "You can say which nine behaviors the suite proves and why the starter cannot fake them. `python verify.py progress` then shows this checkpoint's claim code." + body: "You can say which nine behaviors the suite proves and why the starter cannot fake them. `python verify.py` then shows this checkpoint's claim code." --- -# Lesson 1: Reproduce the failure inside Cursor +# Lesson 1: Set up Cursor, install the FlyPython Skill, fetch the course ## Objective -By the end of this lesson you can open the course folder in Cursor, run the -course's objective verification command, and explain what "the starter -fails" means in test names. No code is changed in this lesson. +By the end of this lesson Cursor is running, the FlyPython Skill is +installed in it (with network access approved), and the agent has fetched +this course's files into your working folder — you downloaded nothing by +hand. With the folder in place you run the course's objective check and +explain what "the starter fails" means in test names. No code is changed +in this lesson. + +## Step 1 — Get Cursor running + +Install Cursor, then `File → Open Folder` +on an empty working folder. + +## Step 2 — Install the FlyPython Skill (network included) + +Save the Skill as a project rule: + +```bash +mkdir -p .cursor/rules +curl -s https://flypython.com/skills/flypython/SKILL.md -o .cursor/rules/flypython.mdc +``` + +Open a chat in **Agent** mode — that mode reads files and runs commands. +Agent mode fetches URLs with approval; approve when it asks. If your +setup blocks the network, run the curl yourself and paste the file. + +The Skill is one file, the same in every tool: it tells the agent how to +authorize you, fetch course files, verify, and submit claim codes. + +## Step 3 — Let the agent fetch this course (you download nothing) + +Start a new session/chat/thread in the working folder and paste this one +sentence: + +> Read https://flypython.com/skills/flypython/SKILL.md and start the FlyPython course `hands-on-with-cursor`. + +The agent gives you an authorization link with a short code. Open it, log +in, make sure the code matches what the agent showed you, and click +Allow — then tell the agent "done". It takes a token once and writes this +course's files to `courses/hands-on-with-cursor/`. + +**Prerequisite:** these courses need a coding agent that can run commands +*and* reach the network. A chat-only web AI cannot do them. ## Why this lesson exists @@ -42,19 +81,18 @@ behavior. Now open `TASK.md` and keep it next to you. ## The lesson -Open the course folder in Cursor (`File → Open Folder`), then open a chat -and make sure it is in **Agent** mode — the mode that can read files and run -commands. Tell it: +The course files are in `courses/hands-on-with-cursor/` now — no download happened on +your side. Ask the agent to survey them without changing anything: **"Read COURSE.md — it is the teaching contract for this folder. Then read TASK.md and starter/report_tool.py. Do not change any file. Tell me which behaviors the tests expect that the starter does not have."** -While it reads, run the objective verification yourself in the integrated -terminal: +While it reads, run the learner's command — your single check for the +rest of the course: ```bash -python verify.py starter --expect-failure +python verify.py ``` You should see the starter reproduce four real-world failure classes, mapped @@ -69,15 +107,16 @@ to test names: | `test_write_report_creates_missing_parent_directories` | the tool cannot write into a fresh output directory | | `test_run_scenario_writes_report_file`, `test_main_prints_summary_and_returns_zero` | end-to-end consequences of the above | -Then run the reviewed implementation the same way: +If you want the missing behaviors as exact failing test names, the +maintainer command prints them: ```bash -python verify.py solution +python verify.py starter --expect-failure ``` -Both commands exit 0 — but they mean opposite things. The first proves the -starter is *correctly unfinished*; the second proves the target is -reachable. That pair is your completion evidence for the whole course. +(The reference `solution/` exists so maintainers can prove the target is +reachable. You never need to run it, and it is never the completion +standard — your implementation in `starter/` is.) ## Exercise @@ -89,16 +128,19 @@ agent to confirm, not to fix. ## Checkpoint -Run `python verify.py starter --expect-failure` and -`python verify.py solution`. You pass this lesson when you can answer, -without looking: +Run `python verify.py` in the course folder. You pass this lesson when you +can answer, without looking: 1. Which four behaviors separate starter from solution? -2. Why is "expected failure" a success condition, not a contradiction? +2. Why does `python verify.py` exit non-zero on purpose — what state is + it reporting, and why is that a success condition rather than an error? 3. What does the row you found in the exercise do to the starter — crash, isolate, or silently pass? +This checkpoint's claim code is already in the default command's output — +submit it only once you can answer the questions. + ## Expected evidence -The transcript of both commands, and your three answers. Keep them; +The transcript of the default command, and your three answers. Keep them; Lesson 4 will ask for them again. diff --git a/courses/hands-on-with-cursor/lessons/L01_cn.md b/courses/hands-on-with-cursor/lessons/L01_cn.md index 7a6c529..6b3d14b 100644 --- a/courses/hands-on-with-cursor/lessons/L01_cn.md +++ b/courses/hands-on-with-cursor/lessons/L01_cn.md @@ -1,29 +1,65 @@ --- id: course-cursor-l01 type: course -title: "第 1 课:在 Cursor 里复现故障" -summary: "把文件夹作为 Cursor 项目打开,让 Agent 模式读教学契约,用 verify.py 让失败变得客观——在编辑器内完成,而不是在编辑器旁边。" +title: "第 1 课:装好 Cursor、装上 FlyPython Skill、让 Agent 取回课程" +summary: "先把 Cursor 跑起来,装好 FlyPython Skill 并打开联网权限,让 Agent 取回本课文件——你不下载任何东西,然后让 starter 的失败变得客观。" lang: zh-CN -content_version: 1 +content_version: 2 status: reviewed reviewed_on: 2026-09-13 hints: - title: "本检查点考察什么" - body: "先复现失败再动手修:`python verify.py starter --expect-failure` 必须打印出点名的失败测试。在 Cursor 里,Agent 模式是干活的地方——但命令仍然是证据。" + body: "先复现失败再动手修:`python verify.py` 必须把 l03 和 l04 显示为 `[open]`(`starter --expect-failure` 会打印点名测试)。在 Cursor 里,Agent 模式是干活的地方——但命令仍然是证据。" - title: "Ask 与 Agent 的区别" body: "Ask 模式只回答问题、不碰文件;Agent 模式会读文件、改代码、跑命令。本课只需要 Agent 读——明确告诉它不要改任何文件。" - title: "完成的标志" - body: "你能说出套件证明了哪九种行为、为什么 starter 伪造不出来。`python verify.py progress` 随即显示本检查点的认领码。" + body: "你能说出套件证明了哪九种行为、为什么 starter 伪造不出来。`python verify.py` 随即显示本检查点的认领码。" --- -# 第 1 课:在 Cursor 里复现故障 +# 第 1 课:装好 Cursor、装上 FlyPython Skill、让 Agent 取回课程 ## 目标 -本课结束时,你能在 Cursor 里打开课程文件夹、运行课程的客观验证 -命令,并解释「starter 失败」在测试名里的含义。本课不改任何代码。 +本课结束时,Cursor 已经跑起来、FlyPython Skill 已装好(联网权限已 +打开),Agent 已把本课文件取回你的工作目录——你没有手动下载任何 +东西。文件夹就位后,你运行课程的客观检查命令,并解释「starter +失败」在测试名里的含义。本课不改任何代码。 -## 为什么有这节课 +## 第 1 步——把 Cursor 跑起来 + +安装 Cursor,然后 `File → Open Folder` +打开一个空工作目录。 + +## 第 2 步——装 FlyPython Skill(含联网权限) + +把 Skill 存为项目规则: + +```bash +mkdir -p .cursor/rules +curl -s https://flypython.com/skills/flypython/SKILL.md -o .cursor/rules/flypython.mdc +``` + +开一个对话并确认处于 **Agent** 模式——这个模式才能读文件、跑命令。 +Agent 模式抓取 URL 需要批准——被询问时批准。如果你的环境禁网, +就自己跑 curl,把文件粘贴过去。 + +Skill 只是一个文件,在每个工具里都一样:它告诉 Agent 如何为你 +授权、取课程文件、验证、提交认领码。 + +## 第 3 步——让 Agent 取回本课文件(你不下载) + +在工作目录里新开一个会话/对话/线程,粘贴这一句话: + +> Read https://flypython.com/skills/flypython/SKILL.md and start the FlyPython course `hands-on-with-cursor`. + +Agent 会给你一个授权链接和一组短码。打开链接、登录、核对码与 +Agent 显示的一致后点「允许」,然后对 Agent 说「好了」。它取一次 +token,把本课文件写到 `courses/hands-on-with-cursor/`。 + +**前提**:这些课程需要一个能执行命令**并且**能联网的编码 Agent。 +只能聊天的网页 AI 做不了。 + +## 为什么有这一课 AI 写的 Python 大多是同一种死法:演示输入能跑,真实输入一到,脚本 在第 3 行崩掉——或者更糟,悄悄算出一个错的数。修复不从更好的 @@ -39,17 +75,17 @@ AI 写的 Python 大多是同一种死法:演示输入能跑,真实输入一 ## 本课内容 -在 Cursor 里打开课程文件夹(`File → Open Folder`),开一个对话并 -确认处于 **Agent** 模式——这个模式才能读文件、跑命令。对它说: +本课文件现在已经位于 `courses/hands-on-with-cursor/`——你这边没有发生任何下载。 +让 Agent 先只读不动手: **“读 COURSE.md——它是本文件夹的教学契约。再读 TASK.md 和 starter/report_tool.py。不要改任何文件。告诉我测试期望哪些行为、 starter 缺了哪些。”** -趁它读的时候,自己在集成终端里跑客观验证: +趁它读的时候,运行学员命令——整个课程你只需要这一条检查命令: ```bash -python verify.py starter --expect-failure +python verify.py ``` 你会看到 starter 复现出四类真实世界故障,对应到测试名: @@ -63,14 +99,14 @@ python verify.py starter --expect-failure | `test_write_report_creates_missing_parent_directories` | 工具无法写进新建的输出目录 | | `test_run_scenario_writes_report_file`、`test_main_prints_summary_and_returns_zero` | 上述问题的端到端后果 | -然后用同样的方式跑评审过的实现: +想看缺失行为对应的失败测试名,维护者命令会打印它们: ```bash -python verify.py solution +python verify.py starter --expect-failure ``` -两条命令都以 0 退出——但含义相反。第一条证明 starter *确实处于 -正确的未完成态*;第二条证明目标可达。这一对命令是整门课的完成 +(参考答案 `solution/` 是给维护者证明目标可达用的。你不需要运行 +它,它也从来不是完成标准——你的实现(`starter/`)才是。)这一对命令是整门课的完成 证据。 ## 练习 @@ -82,15 +118,19 @@ Agent 确认,而不是让它修。 ## 检查点 -运行 `python verify.py starter --expect-failure` 和 -`python verify.py solution`。不看材料能回答下面三个问题即算通过: +在课程文件夹里运行 `python verify.py`。不看材料能回答下面三个 +问题即算通过: 1. 哪四个行为把 starter 和 solution 区分开? -2. 为什么「预期失败」是成功条件而不是自相矛盾? +2. `python verify.py` 为什么故意以非零退出——它在报告什么状态? + 为什么这是成功条件而不是报错? 3. 你在练习里找到的那一行会让 starter 崩溃、被隔离,还是悄悄 通过? +本检查点的认领码已经在默认命令的输出里——能回答这些问题之后再 +提交。 + ## 预期证据 -两条命令的完整输出记录,加你的三个答案。保留好——第 4 课还会 +默认命令的完整输出记录,加你的三个答案。保留好——第 4 课还会 用到。 diff --git a/courses/hands-on-with-cursor/lessons/L02.md b/courses/hands-on-with-cursor/lessons/L02.md index 5613e75..4622595 100644 --- a/courses/hands-on-with-cursor/lessons/L02.md +++ b/courses/hands-on-with-cursor/lessons/L02.md @@ -71,7 +71,7 @@ decide which front-matter field controls each. ## Checkpoint -Run `python verify.py progress` — this checkpoint's claim code prints when +Run `python verify.py` — this checkpoint's claim code prints when you can answer: 1. Which `TASK.md` line does `test_invalid_records_are_isolated_with_reasons` diff --git a/courses/hands-on-with-cursor/lessons/L02_cn.md b/courses/hands-on-with-cursor/lessons/L02_cn.md index e065615..f32afed 100644 --- a/courses/hands-on-with-cursor/lessons/L02_cn.md +++ b/courses/hands-on-with-cursor/lessons/L02_cn.md @@ -68,7 +68,7 @@ scenario/ 只读;每轮只修一组失败测试;修完跑 ## 检查点 -运行 `python verify.py progress`——能回答下面三题时,本检查点的 +运行 `python verify.py`——能回答下面三题时,本检查点的 认领码就会显示: 1. 用你自己的话说,`test_invalid_records_are_isolated_with_reasons` diff --git a/courses/hands-on-with-cursor/lessons/L03.md b/courses/hands-on-with-cursor/lessons/L03.md index 0b010a2..2c176da 100644 --- a/courses/hands-on-with-cursor/lessons/L03.md +++ b/courses/hands-on-with-cursor/lessons/L03.md @@ -9,7 +9,7 @@ status: reviewed reviewed_on: 2026-09-13 hints: - title: "What this checkpoint tests" - body: "That the starter passes all nine tests through changes you supervised — `python verify.py progress` shows this code only once the starter suite is green." + body: "That the starter passes all nine tests through changes you supervised — `python verify.py` shows this code only once the starter suite is green." - title: "The diff is the boundary" body: "Agent mode shows each edit as a diff you accept or reject. Read every hunk — approving blind is how bounded work becomes a rewrite." - title: "When you are done" @@ -69,7 +69,7 @@ diff before accepting the next change. ## Checkpoint -Run `python verify.py progress`. This checkpoint's code appears only when +Run `python verify.py`. This checkpoint's code appears only when the starter suite is green. You pass the lesson when you can answer: 1. How many turns did the bounded change take, and what made each turn diff --git a/courses/hands-on-with-cursor/lessons/L03_cn.md b/courses/hands-on-with-cursor/lessons/L03_cn.md index c0a01ea..74898d9 100644 --- a/courses/hands-on-with-cursor/lessons/L03_cn.md +++ b/courses/hands-on-with-cursor/lessons/L03_cn.md @@ -9,7 +9,7 @@ status: reviewed reviewed_on: 2026-09-13 hints: - title: "本检查点考察什么" - body: "starter 通过你监督的改动让九个测试全绿——`python verify.py progress` 只在 starter 套件变绿后显示本检查点的码。" + body: "starter 通过你监督的改动让九个测试全绿——`python verify.py` 只在 starter 套件变绿后显示本检查点的码。" - title: "diff 就是边界" body: "Agent 模式把每处编辑显示为可接受或拒绝的 diff。逐块读完再放行——闭眼接受正是「有界工作」变成「整文件重写」的方式。" - title: "完成的标志" @@ -61,7 +61,7 @@ starter 通过一串有界改动让九个测试全绿,并且不看 solution, ## 检查点 -运行 `python verify.py progress`。本检查点的码只在 starter 套件 +运行 `python verify.py`。本检查点的码只在 starter 套件 全绿时出现。不看材料能回答即算通过: 1. 这次有界改动走了几轮,是什么让每轮保持有界? diff --git a/courses/hands-on-with-cursor/lessons/L04.md b/courses/hands-on-with-cursor/lessons/L04.md index 4fae758..5e9a247 100644 --- a/courses/hands-on-with-cursor/lessons/L04.md +++ b/courses/hands-on-with-cursor/lessons/L04.md @@ -9,7 +9,7 @@ status: reviewed reviewed_on: 2026-09-13 hints: - title: "What this checkpoint tests" - body: "That starter AND solution both pass — `verify.py progress` prints this code only when both suites are green. The gate is the pair, not a single run." + body: "That starter AND solution both pass — `verify.py` prints this code only when both suites are green. The gate is the pair, not a single run." - title: "Review the delta, not the vibes" body: "Git's working-tree diff shows the whole accumulated change. Read it as a reviewer: every hunk maps to a contract line, nothing extra moved, no test weakened." - title: "When you are done" @@ -66,7 +66,7 @@ Keep it honest — "not verified" is a list of facts, not a confession. ## Checkpoint -Run `python verify.py progress`. This checkpoint's code prints only when +Run `python verify.py`. This checkpoint's code prints only when both suites pass. You pass the lesson when you can answer: 1. What did your by-hand recompute of a group total show? diff --git a/courses/hands-on-with-cursor/lessons/L04_cn.md b/courses/hands-on-with-cursor/lessons/L04_cn.md index 489be32..5cd02b5 100644 --- a/courses/hands-on-with-cursor/lessons/L04_cn.md +++ b/courses/hands-on-with-cursor/lessons/L04_cn.md @@ -9,7 +9,7 @@ status: reviewed reviewed_on: 2026-09-13 hints: - title: "本检查点考察什么" - body: "starter 和 solution 双双通过——`verify.py progress` 只在两个套件都绿时打印本检查点的码。门槛是一对结果,不是一次运行。" + body: "starter 和 solution 双双通过——`verify.py` 只在两个套件都绿时打印本检查点的码。门槛是一对结果,不是一次运行。" - title: "审增量,不凭感觉" body: "Git 工作区 diff 展示整个会话累积的改动。按评审者的方式读:每个 hunk 对上一条契约行、没有多余挪动、没有测试被放松。" - title: "完成的标志" @@ -63,7 +63,7 @@ python verify.py solution # 评审过的参考实现 ## 检查点 -运行 `python verify.py progress`。本检查点的码只在两个套件都过 +运行 `python verify.py`。本检查点的码只在两个套件都过 时打印。不看材料能回答即算通过: 1. 你手工重算的分组总计结果如何? diff --git a/courses/hands-on-with-cursor/verify.py b/courses/hands-on-with-cursor/verify.py index f23792d..b653ff6 100644 --- a/courses/hands-on-with-cursor/verify.py +++ b/courses/hands-on-with-cursor/verify.py @@ -68,6 +68,16 @@ def _claim_code(checkpoint_id): ).digest() return base64.b32encode(digest).decode("ascii")[:8] + +def _display_title(title): + # FP-820: shared-core checkpoint titles are stored "Chinese / English"; + # the default command prints English first, Chinese after (FP-709 debt). + if " / " in title: + left, _, right = title.partition(" / ") + if any("\u4e00" <= character <= "\u9fff" for character in left): + return right + " / " + left + return title + def _run_suite(implementation): environment = os.environ.copy() environment["PYTHONPATH"] = str(ROOT / implementation) @@ -76,7 +86,7 @@ def _run_suite(implementation): env=environment, check=False, capture_output=True, text=True, ) -def run_progress(as_json, receipt_out=None): +def run_progress(as_json, receipt_out=None, default=False): import time t0 = time.monotonic() starter = _run_suite("starter") @@ -122,6 +132,33 @@ def run_progress(as_json, receipt_out=None): COURSE_ID, checkpoint["id"], passed=passed, tests=tests, duration_ms=ms, impl_dir=ROOT / "starter", solution_hash=solution_hash, secret=secret)) + if default and not as_json: + # FP-820: bare ``python verify.py`` — check the learner's own + # implementation, print per-checkpoint status and earned claim codes, + # English first then Chinese (FP-709 debt). ``progress`` keeps its + # exact published output; this block is the only new surface. + print("Course " + COURSE_ID) + print("Suites: starter " + ("passed" if starter_ok else "not passed") + + " / solution " + ("passed" if solution_ok else "not passed")) + for row in rows: + state = row["status"] + (" (self-attested)" if row["kind"] == "attested" else "") + code = row["claim_code"] if row["claim_code"] else "\u2014" + print(" " + row["id"] + " " + _display_title(row["title"]) + " [" + state + "] " + code) + open_gates = [row["id"] for row in rows + if row["kind"] == "objective" and row["status"] != "passed"] + if open_gates: + print("Next: keep implementing starter/ until " + ", ".join(open_gates) + + " show [passed]; then re-run: python verify.py") + else: + print("All objective checkpoints passed — the codes above are ready" + " to submit (batch POST /api/claims, see SKILL.md §5).") + print("\u4e2d\u6587\uff1a\u68c0\u67e5\u70b9\u72b6\u6001\u4e0e\u8ba4\u9886\u7801\u89c1\u4e0a\uff1b\u5e26 [passed]/[attest] \u7684\u884c\u6709\u8ba4\u9886\u7801\uff0c[open] \u7684\u884c\u7ee7\u7eed\u5728 starter/ \u91cc\u5b9e\u73b0\u540e\u518d\u8dd1\u3002") + print("Claim codes are self-reported evidence, recorded at flypython.com; never a certificate.") + print("\u8ba4\u9886\u7801\u662f\u81ea\u6211\u62a5\u544a\u7684\u8bc1\u636e\uff0c\u8bb0\u5f55\u5728 flypython.com\uff1b\u4e0d\u662f\u8bc1\u4e66\u3002") + if secret: + print(f"Signed run receipts prepared for {len(receipts)} gated checkpoint(s);" + " submit each with its claim to mark it as a local-run receipt.") + return 1 if open_gates else 0 if as_json: document = {"course": COURSE_ID, "starter_suite_passed": starter_ok, @@ -152,14 +189,19 @@ def run_progress(as_json, receipt_out=None): def main() -> int: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("implementation", choices=("progress", "starter", "solution")) + parser.add_argument("implementation", choices=("progress", "starter", "solution"), + nargs="?", default=None) parser.add_argument("--json", action="store_true") parser.add_argument("--expect-failure", action="store_true") parser.add_argument("--receipt-out", metavar="PATH", help="write signed run receipts JSON (requires FLYPYTHON_CLAIM_SECRET)") args = parser.parse_args() - if args.implementation == "progress": - return run_progress(args.json, args.receipt_out) + if args.implementation in (None, "progress"): + # FP-820: the bare command is the learner's default — same engine as + # ``progress``, English-first output, exit 1 while gates stay open. + # The four explicit usages remain for maintainers. + return run_progress(args.json, args.receipt_out, + default=args.implementation is None) command = [sys.executable, "-m", "unittest", "discover", "-s", str(ROOT / "tests")] environment = os.environ.copy() diff --git a/courses/hands-on-with-deepseek-harness/COURSE.md b/courses/hands-on-with-deepseek-harness/COURSE.md index 4202350..7331a39 100644 --- a/courses/hands-on-with-deepseek-harness/COURSE.md +++ b/courses/hands-on-with-deepseek-harness/COURSE.md @@ -4,7 +4,7 @@ type: course title: Hands-on with DeepSeek Harness summary: The verified Python workflow — task contract, bounded change, objective verify.py evidence — taught hands-on in DeepSeek Harness, where the agent loop is composed from plugins and every step lands in an inspectable trajectory. lang: en-US -content_version: 1 +content_version: 2 status: reviewed reviewed_on: 2026-09-13 badge: @@ -17,8 +17,9 @@ course_id: course-deepseek-harness # Hands-on with DeepSeek Harness -> TL;DR: download this folder, point a DeepSeek Harness agent at it, and say -> **"start lesson 1"**. You finish with a working report tool, a reproducible +> TL;DR: set up DeepSeek Harness, install the FlyPython Skill, and let the +> agent fetch this course — lesson 1 walks you through all three steps and +> you download nothing. Say **"start lesson 1"**. You finish with a working report tool, a reproducible > pass/fail command, and the harness-native workflow: plugins composed in > `cordis.yml`, and an append-only trajectory you can inspect, resume, and > fork. The exercise core is the same one the other agent-tool courses use — @@ -72,11 +73,12 @@ follow these rules: - **When to stop:** a lesson is done when its checkpoint command runs and the learner can say, in their own words, what failed and why. If the learner cannot, re-teach from the failing test, not from the solution. -- **How to use `verify.py`:** run it in the session's shell/sandbox tool — - `python verify.py starter --expect-failure` must reproduce the listed - failures; `python verify.py solution` must pass. "Done" means the starter - implementation passes the same suite after the learner's bounded change. -- **Honesty rules:** say what you did not verify. Do not claim the code is +- **How to use `verify.py`:** the learner's command is plain + `python verify.py` — it runs the suites, prints per-checkpoint status + and earned claim codes, and exits non-zero while the gated checkpoints + (l03, l04) are still open. `starter --expect-failure` and `solution` + remain maintainer checks. "Done" means the default command shows every + gate `[passed]` after the learner's bounded change.- **Honesty rules:** say what you did not verify. Do not claim the code is production-ready. Do not promise learning or career outcomes. ## What this course does NOT cover @@ -90,7 +92,7 @@ link, don't improvise. - Badge: **Reproduce with DeepSeek Harness in the loop Badge** (badge id `course-deepseek-harness`) — earned by claiming all five checkpoints. - Challenges: L01–L05 checkpoints, 10 points each; +50 course-badge bonus when all five are claimed on flypython.com. -- Evidence: `python verify.py progress` — L03 (bounded change) and L04 (verify & review) are objectively gated by the suite; L01/L02/L05 are learner-attested. +- Evidence: `python verify.py` — L03 (bounded change) and L04 (verify & review) are objectively gated by the suite; L01/L02/L05 are learner-attested. - Submission: each passed checkpoint prints a deterministic claim code; record it on flypython.com against your account. Self-reported evidence, never a certificate. ## Folder map diff --git a/courses/hands-on-with-deepseek-harness/COURSE_cn.md b/courses/hands-on-with-deepseek-harness/COURSE_cn.md index c4227b1..7de7a92 100644 --- a/courses/hands-on-with-deepseek-harness/COURSE_cn.md +++ b/courses/hands-on-with-deepseek-harness/COURSE_cn.md @@ -4,7 +4,7 @@ type: course title: "上手 DeepSeek Harness" summary: "经过验证的 Python 工作流——任务契约、有界改动、客观 verify.py 证据——在 DeepSeek Harness 里手把手教:Agent 闭环由插件组装,每一步都落进可检查的轨迹日志。" lang: zh-CN -content_version: 1 +content_version: 2 status: reviewed reviewed_on: 2026-09-13 badge: @@ -17,8 +17,8 @@ course_id: course-deepseek-harness # 上手 DeepSeek Harness -> 一句话:下载本文件夹,让一个 DeepSeek Harness Agent 指向它,对它说 -> **「开始第 1 课」**。完成时你会得到一个能跑的报表工具、一条可复现的 +> 一句话:装好 DeepSeek Harness、装上 FlyPython Skill、让 Agent 取回本课文件 +> (第 1 课就是这三步,你什么都不用下载),对它说**「开始第 1 课」**。完成时你会得到一个能跑的报表工具、一条可复现的 > 通过/失败命令,以及 Harness 原生工作流:用 `cordis.yml` 组装的插件, > 和一份可检查、可恢复、可分叉的只增轨迹。练习核心与其他 Agent 工具 > 课程完全相同——变的只是你驱动的工具。 @@ -64,11 +64,10 @@ course_id: course-deepseek-harness - **何时停下:** 一节课的检查点命令跑通、且学习者能用自己的话说清 哪里坏了、为什么,这节课才算完。学习者说不出就从失败测试重新教, 不要从 solution 教。 -- **如何使用 `verify.py`:** 在会话的 shell/沙箱工具里跑—— - `python verify.py starter --expect-failure` 必须复现列出的失败; - `python verify.py solution` 必须通过。「完成」指学习者做完有界 - 改动后,starter 实现通过同一套测试。 -- **诚实规则:** 说清你没验证什么。不要声称代码达到生产可用。不要 +- **如何使用 `verify.py`:** 学员命令就是 `python verify.py`——它跑套件、 + 打印逐检查点状态与已获认领码,在门控检查点(l03、l04)未全通过时以 + 非零退出。`starter --expect-failure` 与 `solution` 留作维护者检查。 + 「完成」指学习者做完有界改动后,默认命令把所有门显示为 `[passed]`。- **诚实规则:** 说清你没验证什么。不要声称代码达到生产可用。不要 承诺学习或职业结果。 ## 本课程不覆盖什么 @@ -81,7 +80,7 @@ course_id: course-deepseek-harness - 徽章:**DeepSeek Harness 协同复现徽章**(badge id `course-deepseek-harness`)——认领全部五个检查点获得。 - 挑战:L01–L05 检查点,各 10 分;在 flypython.com 上集齐五个再加 50 分课程徽章奖励。 -- 证据:`python verify.py progress`——L03(有界改动)与 L04(验证与评审)由套件客观把关;L01/L02/L05 为学习者自我报告。 +- 证据:`python verify.py`——L03(有界改动)与 L04(验证与评审)由套件客观把关;L01/L02/L05 为学习者自我报告。 - 提交:每个通过的检查点打印一个确定性的认领码;在 flypython.com 上记入你的账号。这是自我报告的证据,从来不是证书。 ## 文件夹地图 diff --git a/courses/hands-on-with-deepseek-harness/REVIEW.md b/courses/hands-on-with-deepseek-harness/REVIEW.md index 0347caf..35ed464 100644 --- a/courses/hands-on-with-deepseek-harness/REVIEW.md +++ b/courses/hands-on-with-deepseek-harness/REVIEW.md @@ -19,6 +19,20 @@ endpoint, or API key was used; every harness behavior described in the lessons is sourced from official documentation, not observed. +## 2026-09-13 — 0.0.8 rework: lesson 1 is now tool → Skill → agent fetch (FP-822) + +- L01 (EN+ZH) rewritten around the fixed order: get the tool running → + install the FlyPython Skill (network access included) → let the agent + fetch this course's files via the files endpoint. All "download this + folder" wording removed from `COURSE.md` and lessons. +- The learner's check command across lessons is now the bare + `python verify.py` (FP-820): per-checkpoint status + claim codes, + non-zero exit while gated checkpoints are open. `solution` is no longer + presented as a completion standard. +- Tool-specific Skill install and network steps are authored from the + same official documentation as the course; **not yet exercised inside + the tool** — the pending live run-through below still stands. + ## Pending before this course can be called taught-in-tool - One full agent-taught run-through under DeepSeek Harness ("start lesson 1" diff --git a/courses/hands-on-with-deepseek-harness/lessons/L01.md b/courses/hands-on-with-deepseek-harness/lessons/L01.md index 6c2e3d5..86cdc07 100644 --- a/courses/hands-on-with-deepseek-harness/lessons/L01.md +++ b/courses/hands-on-with-deepseek-harness/lessons/L01.md @@ -1,32 +1,69 @@ --- id: course-deepseek-harness-l01 type: course -title: "Lesson 1: Reproduce the failure, read it back from the trajectory" -summary: "Run a harness-composed agent on the course folder, make the failure objective with verify.py — then read the append-only trajectory to see exactly what the agent did." +title: "Lesson 1: Set up DeepSeek Harness, install the FlyPython Skill, fetch the course" +summary: "Get DeepSeek Harness running, install the FlyPython Skill with network access approved, and let the agent fetch this course's files — you download nothing. Then make the starter's failure objective." lang: en-US -content_version: 1 +content_version: 2 status: reviewed reviewed_on: 2026-09-13 hints: - title: "What this checkpoint tests" - body: "That you can reproduce the failure before fixing it: `python verify.py starter --expect-failure` must print the named failing tests — whether you ran it or the agent's shell tool did, the command is the evidence." + body: "That you can reproduce the failure before fixing it: `python verify.py` must show l03 and l04 as `[open]` (`starter --expect-failure` prints the named tests) — whether you ran it or the agent's shell tool did, the command is the evidence." - title: "Everything is a plugin" body: "In DeepSeek Harness the agent is composed, not built-in: model, tools, session storage, and the loop itself are plugins wired in `cordis.yml`. If a behavior is missing, the answer is usually a plugin — not a flag." - title: "The trajectory is your record" body: "Every step lands in an append-only session/trajectory log you can inspect later. Reading it back is how you verify what the agent actually did — not what it said it did." - title: "When you are done" - body: "You can say which nine behaviors the suite proves and why the starter cannot fake them. `python verify.py progress` then shows this checkpoint's claim code." + body: "You can say which nine behaviors the suite proves and why the starter cannot fake them. `python verify.py` then shows this checkpoint's claim code." --- -# Lesson 1: Reproduce the failure, read it back from the trajectory +# Lesson 1: Set up DeepSeek Harness, install the FlyPython Skill, fetch the course ## Objective -By the end of this lesson you have a working DeepSeek Harness session -pointed at the course folder, you have run the course's objective -verification command, and you can explain what "the starter fails" means -in test names — confirmed against the session trajectory. No code is -changed in this lesson. +By the end of this lesson DeepSeek Harness is running, the FlyPython Skill is +installed in it (with network access approved), and the agent has fetched +this course's files into your working folder — you downloaded nothing by +hand. With the folder in place you run the course's objective check and +explain what "the starter fails" means in test names. No code is changed +in this lesson. + +## Step 1 — Get DeepSeek Harness running + +Set up DeepSeek Harness (the Python +package from the deepseek-harness project) with a configured model plugin — +an API key or local endpoint you supply — and start a session in an empty +working folder. + +## Step 2 — Install the FlyPython Skill (network included) + +The harness reads `AGENTS.md` from the project root: + +```bash +curl -s https://flypython.com/skills/flypython/SKILL.md >> AGENTS.md +``` + +Approve the agent's network tool use when it asks; if your sandbox blocks +it, run the curl yourself and paste the file. + +The Skill is one file, the same in every tool: it tells the agent how to +authorize you, fetch course files, verify, and submit claim codes. + +## Step 3 — Let the agent fetch this course (you download nothing) + +Start a new session/chat/thread in the working folder and paste this one +sentence: + +> Read https://flypython.com/skills/flypython/SKILL.md and start the FlyPython course `hands-on-with-deepseek-harness`. + +The agent gives you an authorization link with a short code. Open it, log +in, make sure the code matches what the agent showed you, and click +Allow — then tell the agent "done". It takes a token once and writes this +course's files to `courses/hands-on-with-deepseek-harness/`. + +**Prerequisite:** these courses need a coding agent that can run commands +*and* reach the network. A chat-only web AI cannot do them. ## Why this lesson exists @@ -49,17 +86,18 @@ open `TASK.md` and keep it next to you. ## The lesson -With a harness session running in the course folder, tell the agent: +The course files are in `courses/hands-on-with-deepseek-harness/` now — no download happened on +your side. Ask the agent to survey them without changing anything: **"Read COURSE.md — it is the teaching contract for this folder. Then read TASK.md and starter/report_tool.py. Do not change any file. Tell me which behaviors the tests expect that the starter does not have."** -While it reads, run the objective verification — yourself in a terminal, -or through the agent's shell/sandbox tool: +While it reads, run the learner's command — your single check for the +rest of the course: ```bash -python verify.py starter --expect-failure +python verify.py ``` You should see the starter reproduce four real-world failure classes, @@ -74,15 +112,16 @@ mapped to test names: | `test_write_report_creates_missing_parent_directories` | the tool cannot write into a fresh output directory | | `test_run_scenario_writes_report_file`, `test_main_prints_summary_and_returns_zero` | end-to-end consequences of the above | -Then run the reviewed implementation: +If you want the missing behaviors as exact failing test names, the +maintainer command prints them: ```bash -python verify.py solution +python verify.py starter --expect-failure ``` -Both commands exit 0 — but they mean opposite things. The first proves the -starter is *correctly unfinished*; the second proves the target is -reachable. +(The reference `solution/` exists so maintainers can prove the target is +reachable. You never need to run it, and it is never the completion +standard — your implementation in `starter/` is.) Now do the harness-native step: inspect the session trajectory. Find the steps where the agent read `TASK.md`, ran (or was told about) the verify @@ -100,16 +139,19 @@ the trajectory step where the agent reached the same conclusion. ## Checkpoint -Run `python verify.py starter --expect-failure` and -`python verify.py solution`. You pass this lesson when you can answer, -without looking: +Run `python verify.py` in the course folder. You pass this lesson when you +can answer, without looking: 1. Which four behaviors separate starter from solution? -2. Why is "expected failure" a success condition, not a contradiction? +2. Why does `python verify.py` exit non-zero on purpose — what state is + it reporting, and why is that a success condition rather than an error? 3. Where in the trajectory can you prove the agent actually read `TASK.md`? +This checkpoint's claim code is already in the default command's output — +submit it only once you can answer the questions. + ## Expected evidence -The transcript of both commands, the trajectory steps you found, and your +The transcript of the default command, the trajectory steps you found, and your three answers. Keep them; Lesson 4 will ask for them again. diff --git a/courses/hands-on-with-deepseek-harness/lessons/L01_cn.md b/courses/hands-on-with-deepseek-harness/lessons/L01_cn.md index 1a9b494..e423f82 100644 --- a/courses/hands-on-with-deepseek-harness/lessons/L01_cn.md +++ b/courses/hands-on-with-deepseek-harness/lessons/L01_cn.md @@ -1,32 +1,66 @@ --- id: course-deepseek-harness-l01 type: course -title: "第 1 课:复现故障,从轨迹里读回来" -summary: "让 Harness 组装的 Agent 跑在课程文件夹上,用 verify.py 让失败变得客观——再读只增轨迹,看清 Agent 到底做了什么。" +title: "第 1 课:装好 DeepSeek Harness、装上 FlyPython Skill、让 Agent 取回课程" +summary: "先把 DeepSeek Harness 跑起来,装好 FlyPython Skill 并打开联网权限,让 Agent 取回本课文件——你不下载任何东西,然后让 starter 的失败变得客观。" lang: zh-CN -content_version: 1 +content_version: 2 status: reviewed reviewed_on: 2026-09-13 hints: - title: "本检查点考察什么" - body: "先复现失败再动手修:`python verify.py starter --expect-failure` 必须打印出点名的失败测试——你自己跑也好、Agent 的 shell 工具跑也好,命令就是证据。" + body: "先复现失败再动手修:`python verify.py` 必须把 l03 和 l04 显示为 `[open]`(`starter --expect-failure` 会打印点名测试)——你自己跑也好、Agent 的 shell 工具跑也好,命令就是证据。" - title: "一切皆插件" body: "DeepSeek Harness 的 Agent 是组装出来的,不是内置的:模型、工具、会话存储、循环本身都是 `cordis.yml` 里接好的插件。某个行为没有,答案通常是插件——不是某个开关。" - title: "轨迹就是你的记录" body: "每一步都落进可事后检查的只增会话/轨迹日志。读回轨迹才能验证 Agent 实际做了什么——而不是它声称做了什么。" - title: "完成的标志" - body: "你能说出套件证明了哪九种行为、为什么 starter 伪造不出来。`python verify.py progress` 随即显示本检查点的认领码。" + body: "你能说出套件证明了哪九种行为、为什么 starter 伪造不出来。`python verify.py` 随即显示本检查点的认领码。" --- -# 第 1 课:复现故障,从轨迹里读回来 +# 第 1 课:装好 DeepSeek Harness、装上 FlyPython Skill、让 Agent 取回课程 ## 目标 -本课结束时,你有一个指向课程文件夹的 DeepSeek Harness 会话,跑过 -课程的客观验证命令,并能对照会话轨迹解释「starter 失败」在测试名 -里的含义。本课不改任何代码。 +本课结束时,DeepSeek Harness 已经跑起来、FlyPython Skill 已装好(联网权限已 +打开),Agent 已把本课文件取回你的工作目录——你没有手动下载任何 +东西。文件夹就位后,你运行课程的客观检查命令,并解释「starter +失败」在测试名里的含义。本课不改任何代码。 -## 为什么有这节课 +## 第 1 步——把 DeepSeek Harness 跑起来 + +装好 DeepSeek Harness(deepseek-harness +项目的 Python 包)并配置好模型插件——你自己提供的 API key 或本地 +端点——在一个空工作目录里启动会话。 + +## 第 2 步——装 FlyPython Skill(含联网权限) + +Harness 会读取项目根目录的 `AGENTS.md`: + +```bash +curl -s https://flypython.com/skills/flypython/SKILL.md >> AGENTS.md +``` + +Agent 请求联网时批准;如果沙箱禁网,就自己跑 curl,把文件粘贴 +过去。 + +Skill 只是一个文件,在每个工具里都一样:它告诉 Agent 如何为你 +授权、取课程文件、验证、提交认领码。 + +## 第 3 步——让 Agent 取回本课文件(你不下载) + +在工作目录里新开一个会话/对话/线程,粘贴这一句话: + +> Read https://flypython.com/skills/flypython/SKILL.md and start the FlyPython course `hands-on-with-deepseek-harness`. + +Agent 会给你一个授权链接和一组短码。打开链接、登录、核对码与 +Agent 显示的一致后点「允许」,然后对 Agent 说「好了」。它取一次 +token,把本课文件写到 `courses/hands-on-with-deepseek-harness/`。 + +**前提**:这些课程需要一个能执行命令**并且**能联网的编码 Agent。 +只能聊天的网页 AI 做不了。 + +## 为什么有这一课 AI 写的 Python 大多是同一种死法:演示输入能跑,真实输入一到,脚本 在第 3 行崩掉——或者更糟,悄悄算出一个错的数。修复不从更好的 @@ -44,17 +78,17 @@ docstring 的函数、类型标注、CLI。这正是 AI 生成代码的样子— ## 本课内容 -让 Harness 会话跑在课程文件夹里,对 Agent 说: +本课文件现在已经位于 `courses/hands-on-with-deepseek-harness/`——你这边没有发生任何下载。 +让 Agent 先只读不动手: **“读 COURSE.md——它是本文件夹的教学契约。再读 TASK.md 和 starter/report_tool.py。不要改任何文件。告诉我测试期望哪些行为、 starter 缺了哪些。”** -趁它读的时候,跑客观验证——自己在终端里跑,或通过 Agent 的 -shell/沙箱工具跑: +趁它读的时候,运行学员命令——整个课程你只需要这一条检查命令: ```bash -python verify.py starter --expect-failure +python verify.py ``` 你会看到 starter 复现出四类真实世界故障,对应到测试名: @@ -68,14 +102,14 @@ python verify.py starter --expect-failure | `test_write_report_creates_missing_parent_directories` | 工具无法写进新建的输出目录 | | `test_run_scenario_writes_report_file`、`test_main_prints_summary_and_returns_zero` | 上述问题的端到端后果 | -然后跑评审过的实现: +想看缺失行为对应的失败测试名,维护者命令会打印它们: ```bash -python verify.py solution +python verify.py starter --expect-failure ``` -两条命令都以 0 退出——但含义相反。第一条证明 starter *确实处于 -正确的未完成态*;第二条证明目标可达。 +(参考答案 `solution/` 是给维护者证明目标可达用的。你不需要运行 +它,它也从来不是完成标准——你的实现(`starter/`)才是。) 现在做 Harness 原生的一步:检查会话轨迹。找到 Agent 读 `TASK.md`、 跑(或被告知)verify 命令、产出总结的那几步。轨迹是只增的——它是 @@ -90,14 +124,18 @@ Agent 得出相同结论的那一步。 ## 检查点 -运行 `python verify.py starter --expect-failure` 和 -`python verify.py solution`。不看材料能回答下面三个问题即算通过: +在课程文件夹里运行 `python verify.py`。不看材料能回答下面三个 +问题即算通过: 1. 哪四个行为把 starter 和 solution 区分开? -2. 为什么「预期失败」是成功条件而不是自相矛盾? -3. 在轨迹的哪里能证明 Agent 确实读过 `TASK.md`? +2. `python verify.py` 为什么故意以非零退出——它在报告什么状态? + 为什么这是成功条件而不是报错? +3. 在轨迹的哪个位置能证明 Agent 确实读过 `TASK.md`? + +本检查点的认领码已经在默认命令的输出里——能回答这些问题之后再 +提交。 ## 预期证据 -两条命令的完整输出记录、你找到的轨迹步骤、加你的三个答案。保留好 +默认命令的完整输出记录、你找到的轨迹步骤、加你的三个答案。保留好 ——第 4 课还会用到。 diff --git a/courses/hands-on-with-deepseek-harness/lessons/L02.md b/courses/hands-on-with-deepseek-harness/lessons/L02.md index a76fab8..6a83855 100644 --- a/courses/hands-on-with-deepseek-harness/lessons/L02.md +++ b/courses/hands-on-with-deepseek-harness/lessons/L02.md @@ -71,7 +71,7 @@ composition. ## Checkpoint -Run `python verify.py progress` — this checkpoint's claim code prints when +Run `python verify.py` — this checkpoint's claim code prints when you can answer: 1. Which `TASK.md` line does diff --git a/courses/hands-on-with-deepseek-harness/lessons/L02_cn.md b/courses/hands-on-with-deepseek-harness/lessons/L02_cn.md index 2c022f4..ba524cb 100644 --- a/courses/hands-on-with-deepseek-harness/lessons/L02_cn.md +++ b/courses/hands-on-with-deepseek-harness/lessons/L02_cn.md @@ -66,7 +66,7 @@ Agent 的一部分,而不是每次都要重打的提示词。 ## 检查点 -运行 `python verify.py progress`——能回答下面三题时,本检查点的 +运行 `python verify.py`——能回答下面三题时,本检查点的 认领码就会显示: 1. 用你自己的话说,`test_invalid_records_are_isolated_with_reasons` diff --git a/courses/hands-on-with-deepseek-harness/lessons/L03.md b/courses/hands-on-with-deepseek-harness/lessons/L03.md index 5a4e42c..95950eb 100644 --- a/courses/hands-on-with-deepseek-harness/lessons/L03.md +++ b/courses/hands-on-with-deepseek-harness/lessons/L03.md @@ -9,7 +9,7 @@ status: reviewed reviewed_on: 2026-09-13 hints: - title: "What this checkpoint tests" - body: "That the starter passes all nine tests through changes you supervised — `python verify.py progress` shows this code only once the starter suite is green." + body: "That the starter passes all nine tests through changes you supervised — `python verify.py` shows this code only once the starter suite is green." - title: "The trajectory is the audit" body: "Because the log is append-only, you can inspect each tool call and file edit after the fact — and fork the session if a step went somewhere you did not want." - title: "When you are done" @@ -75,7 +75,7 @@ through that trajectory step before continuing. ## Checkpoint -Run `python verify.py progress`. This checkpoint's code appears only when +Run `python verify.py`. This checkpoint's code appears only when the starter suite is green. You pass the lesson when you can answer: 1. How many steps did the bounded change take, and what made each step diff --git a/courses/hands-on-with-deepseek-harness/lessons/L03_cn.md b/courses/hands-on-with-deepseek-harness/lessons/L03_cn.md index f84ce2a..67f5571 100644 --- a/courses/hands-on-with-deepseek-harness/lessons/L03_cn.md +++ b/courses/hands-on-with-deepseek-harness/lessons/L03_cn.md @@ -9,7 +9,7 @@ status: reviewed reviewed_on: 2026-09-13 hints: - title: "本检查点考察什么" - body: "starter 通过你监督的改动让九个测试全绿——`python verify.py progress` 只在 starter 套件变绿后显示本检查点的码。" + body: "starter 通过你监督的改动让九个测试全绿——`python verify.py` 只在 starter 套件变绿后显示本检查点的码。" - title: "轨迹就是审计" body: "日志是只增的,事后可以逐条检查每次工具调用与文件编辑——某步走偏了就从那步之前分叉会话。" - title: "完成的标志" @@ -66,7 +66,7 @@ DeepSeek Harness 让监督可审计:会话轨迹是只增的,Agent 做了什 ## 检查点 -运行 `python verify.py progress`。本检查点的码只在 starter 套件 +运行 `python verify.py`。本检查点的码只在 starter 套件 全绿时出现。不看材料能回答即算通过: 1. 这次有界改动走了几步,是什么让每步保持有界? diff --git a/courses/hands-on-with-deepseek-harness/lessons/L04.md b/courses/hands-on-with-deepseek-harness/lessons/L04.md index ba22178..787689b 100644 --- a/courses/hands-on-with-deepseek-harness/lessons/L04.md +++ b/courses/hands-on-with-deepseek-harness/lessons/L04.md @@ -9,7 +9,7 @@ status: reviewed reviewed_on: 2026-09-13 hints: - title: "What this checkpoint tests" - body: "That starter AND solution both pass — `verify.py progress` prints this code only when both suites are green. The gate is the pair, not a single run." + body: "That starter AND solution both pass — `verify.py` prints this code only when both suites are green. The gate is the pair, not a single run." - title: "Review the record, not the narration" body: "The append-only trajectory is the auditable record of every command and edit. Review it end to end: every edit maps to a contract line, every command really ran, no test was weakened." - title: "When you are done" @@ -70,7 +70,7 @@ Keep it honest — "not verified" is a list of facts, not a confession. ## Checkpoint -Run `python verify.py progress`. This checkpoint's code prints only when +Run `python verify.py`. This checkpoint's code prints only when both suites pass. You pass the lesson when you can answer: 1. What did your by-hand recompute of a group total show? diff --git a/courses/hands-on-with-deepseek-harness/lessons/L04_cn.md b/courses/hands-on-with-deepseek-harness/lessons/L04_cn.md index 01242de..f70e183 100644 --- a/courses/hands-on-with-deepseek-harness/lessons/L04_cn.md +++ b/courses/hands-on-with-deepseek-harness/lessons/L04_cn.md @@ -9,7 +9,7 @@ status: reviewed reviewed_on: 2026-09-13 hints: - title: "本检查点考察什么" - body: "starter 和 solution 双双通过——`verify.py progress` 只在两个套件都绿时打印本检查点的码。门槛是一对结果,不是一次运行。" + body: "starter 和 solution 双双通过——`verify.py` 只在两个套件都绿时打印本检查点的码。门槛是一对结果,不是一次运行。" - title: "审记录,不听自述" body: "只增轨迹是每条命令、每处编辑的可审计记录。从头审到尾:每处编辑对上一条契约行、每条命令真的跑过、没有测试被放松。" - title: "完成的标志" @@ -65,7 +65,7 @@ python verify.py solution # 评审过的参考实现 ## 检查点 -运行 `python verify.py progress`。本检查点的码只在两个套件都过 +运行 `python verify.py`。本检查点的码只在两个套件都过 时打印。不看材料能回答即算通过: 1. 你手工重算的分组总计结果如何? diff --git a/courses/hands-on-with-deepseek-harness/verify.py b/courses/hands-on-with-deepseek-harness/verify.py index c414fb6..3e71d12 100644 --- a/courses/hands-on-with-deepseek-harness/verify.py +++ b/courses/hands-on-with-deepseek-harness/verify.py @@ -68,6 +68,16 @@ def _claim_code(checkpoint_id): ).digest() return base64.b32encode(digest).decode("ascii")[:8] + +def _display_title(title): + # FP-820: shared-core checkpoint titles are stored "Chinese / English"; + # the default command prints English first, Chinese after (FP-709 debt). + if " / " in title: + left, _, right = title.partition(" / ") + if any("\u4e00" <= character <= "\u9fff" for character in left): + return right + " / " + left + return title + def _run_suite(implementation): environment = os.environ.copy() environment["PYTHONPATH"] = str(ROOT / implementation) @@ -76,7 +86,7 @@ def _run_suite(implementation): env=environment, check=False, capture_output=True, text=True, ) -def run_progress(as_json, receipt_out=None): +def run_progress(as_json, receipt_out=None, default=False): import time t0 = time.monotonic() starter = _run_suite("starter") @@ -122,6 +132,33 @@ def run_progress(as_json, receipt_out=None): COURSE_ID, checkpoint["id"], passed=passed, tests=tests, duration_ms=ms, impl_dir=ROOT / "starter", solution_hash=solution_hash, secret=secret)) + if default and not as_json: + # FP-820: bare ``python verify.py`` — check the learner's own + # implementation, print per-checkpoint status and earned claim codes, + # English first then Chinese (FP-709 debt). ``progress`` keeps its + # exact published output; this block is the only new surface. + print("Course " + COURSE_ID) + print("Suites: starter " + ("passed" if starter_ok else "not passed") + + " / solution " + ("passed" if solution_ok else "not passed")) + for row in rows: + state = row["status"] + (" (self-attested)" if row["kind"] == "attested" else "") + code = row["claim_code"] if row["claim_code"] else "\u2014" + print(" " + row["id"] + " " + _display_title(row["title"]) + " [" + state + "] " + code) + open_gates = [row["id"] for row in rows + if row["kind"] == "objective" and row["status"] != "passed"] + if open_gates: + print("Next: keep implementing starter/ until " + ", ".join(open_gates) + + " show [passed]; then re-run: python verify.py") + else: + print("All objective checkpoints passed — the codes above are ready" + " to submit (batch POST /api/claims, see SKILL.md §5).") + print("\u4e2d\u6587\uff1a\u68c0\u67e5\u70b9\u72b6\u6001\u4e0e\u8ba4\u9886\u7801\u89c1\u4e0a\uff1b\u5e26 [passed]/[attest] \u7684\u884c\u6709\u8ba4\u9886\u7801\uff0c[open] \u7684\u884c\u7ee7\u7eed\u5728 starter/ \u91cc\u5b9e\u73b0\u540e\u518d\u8dd1\u3002") + print("Claim codes are self-reported evidence, recorded at flypython.com; never a certificate.") + print("\u8ba4\u9886\u7801\u662f\u81ea\u6211\u62a5\u544a\u7684\u8bc1\u636e\uff0c\u8bb0\u5f55\u5728 flypython.com\uff1b\u4e0d\u662f\u8bc1\u4e66\u3002") + if secret: + print(f"Signed run receipts prepared for {len(receipts)} gated checkpoint(s);" + " submit each with its claim to mark it as a local-run receipt.") + return 1 if open_gates else 0 if as_json: document = {"course": COURSE_ID, "starter_suite_passed": starter_ok, @@ -152,14 +189,19 @@ def run_progress(as_json, receipt_out=None): def main() -> int: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("implementation", choices=("progress", "starter", "solution")) + parser.add_argument("implementation", choices=("progress", "starter", "solution"), + nargs="?", default=None) parser.add_argument("--json", action="store_true") parser.add_argument("--expect-failure", action="store_true") parser.add_argument("--receipt-out", metavar="PATH", help="write signed run receipts JSON (requires FLYPYTHON_CLAIM_SECRET)") args = parser.parse_args() - if args.implementation == "progress": - return run_progress(args.json, args.receipt_out) + if args.implementation in (None, "progress"): + # FP-820: the bare command is the learner's default — same engine as + # ``progress``, English-first output, exit 1 while gates stay open. + # The four explicit usages remain for maintainers. + return run_progress(args.json, args.receipt_out, + default=args.implementation is None) command = [sys.executable, "-m", "unittest", "discover", "-s", str(ROOT / "tests")] environment = os.environ.copy() diff --git a/courses/hands-on-with-kimi-code/COURSE.md b/courses/hands-on-with-kimi-code/COURSE.md index 32db26a..aa47d2d 100644 --- a/courses/hands-on-with-kimi-code/COURSE.md +++ b/courses/hands-on-with-kimi-code/COURSE.md @@ -4,7 +4,7 @@ type: course title: Hands-on with Kimi Code summary: The verified Python workflow — task contract, bounded change, objective verify.py evidence — taught hands-on in Kimi Code, where built-in subagents split read-only exploration, planning, and the edits you approve. lang: en-US -content_version: 1 +content_version: 2 status: reviewed reviewed_on: 2026-09-13 badge: @@ -17,8 +17,9 @@ course_id: course-kimi-code # Hands-on with Kimi Code -> TL;DR: download this folder, run `kimi` in it, and say **"start lesson 1"**. -> You finish with a working report tool, a reproducible pass/fail command, +> TL;DR: set up the `kimi` CLI, install the FlyPython Skill, and let the +> agent fetch this course — lesson 1 walks you through all three steps and +> you download nothing. Say **"start lesson 1"**. You finish with a working report tool, a reproducible pass/fail command, > and the Kimi Code workflow: the built-in `explore`/`plan`/`coder` > subagents that keep reading, planning, and editing in separate lanes. The > exercise core is the same one the other agent-tool courses use — only the @@ -68,12 +69,12 @@ If you are the Kimi Code agent teaching this course, follow these rules: - **When to stop:** a lesson is done when its checkpoint command runs and the learner can say, in their own words, what failed and why. If the learner cannot, re-teach from the failing test, not from the solution. -- **How to use `verify.py`:** run it in a terminal or through the agent's - shell tool — `python verify.py starter --expect-failure` must reproduce - the listed failures; `python verify.py solution` must pass. "Done" means - the starter implementation passes the same suite after the learner's - bounded change. -- **Honesty rules:** say what you did not verify. Do not claim the code is +- **How to use `verify.py`:** the learner's command is plain + `python verify.py` — it runs the suites, prints per-checkpoint status + and earned claim codes, and exits non-zero while the gated checkpoints + (l03, l04) are still open. `starter --expect-failure` and `solution` + remain maintainer checks. "Done" means the default command shows every + gate `[passed]` after the learner's bounded change.- **Honesty rules:** say what you did not verify. Do not claim the code is production-ready. Do not promise learning or career outcomes. ## What this course does NOT cover @@ -86,7 +87,7 @@ guides and playbooks — link, don't improvise. - Badge: **Reproduce with Kimi Code in the loop Badge** (badge id `course-kimi-code`) — earned by claiming all five checkpoints. - Challenges: L01–L05 checkpoints, 10 points each; +50 course-badge bonus when all five are claimed on flypython.com. -- Evidence: `python verify.py progress` — L03 (bounded change) and L04 (verify & review) are objectively gated by the suite; L01/L02/L05 are learner-attested. +- Evidence: `python verify.py` — L03 (bounded change) and L04 (verify & review) are objectively gated by the suite; L01/L02/L05 are learner-attested. - Submission: each passed checkpoint prints a deterministic claim code; record it on flypython.com against your account. Self-reported evidence, never a certificate. ## Folder map diff --git a/courses/hands-on-with-kimi-code/COURSE_cn.md b/courses/hands-on-with-kimi-code/COURSE_cn.md index ed08e38..fe28282 100644 --- a/courses/hands-on-with-kimi-code/COURSE_cn.md +++ b/courses/hands-on-with-kimi-code/COURSE_cn.md @@ -4,7 +4,7 @@ type: course title: "上手 Kimi Code" summary: "经过验证的 Python 工作流——任务契约、有界改动、客观 verify.py 证据——在 Kimi Code 里手把手教:内置子代理把只读勘察、规划和经你批准的编辑分在三条道上。" lang: zh-CN -content_version: 1 +content_version: 2 status: reviewed reviewed_on: 2026-09-13 badge: @@ -17,7 +17,8 @@ course_id: course-kimi-code # 上手 Kimi Code -> 一句话:下载本文件夹,在里面运行 `kimi`,对它说**「开始第 1 课」**。 +> 一句话:装好 `kimi` CLI、装上 FlyPython Skill、让 Agent 取回本课文件(第 1 课 +> 就是这三步,你什么都不用下载),对它说**「开始第 1 课」**。 > 完成时你会得到一个能跑的报表工具、一条可复现的通过/失败命令, > 以及 Kimi Code 工作流:内置 `explore`/`plan`/`coder` 子代理把读、 > 规划、改代码分在三条道上。练习核心与其他 Agent 工具课程完全相同 @@ -61,11 +62,10 @@ course_id: course-kimi-code - **何时停下:** 一节课的检查点命令跑通、且学习者能用自己的话说清 哪里坏了、为什么,这节课才算完。学习者说不出就从失败测试重新教, 不要从 solution 教。 -- **如何使用 `verify.py`:** 在终端里跑,或通过 Agent 的 shell 工具 - 跑——`python verify.py starter --expect-failure` 必须复现列出的 - 失败;`python verify.py solution` 必须通过。「完成」指学习者做完 - 有界改动后,starter 实现通过同一套测试。 -- **诚实规则:** 说清你没验证什么。不要声称代码达到生产可用。不要 +- **如何使用 `verify.py`:** 学员命令就是 `python verify.py`——它跑套件、 + 打印逐检查点状态与已获认领码,在门控检查点(l03、l04)未全通过时以 + 非零退出。`starter --expect-failure` 与 `solution` 留作维护者检查。 + 「完成」指学习者做完有界改动后,默认命令把所有门显示为 `[passed]`。- **诚实规则:** 说清你没验证什么。不要声称代码达到生产可用。不要 承诺学习或职业结果。 ## 本课程不覆盖什么 @@ -78,7 +78,7 @@ Kimi Code 的安装与计费、自定义子代理编写、ACP 客户端开发, - 徽章:**Kimi Code 协同复现徽章**(badge id `course-kimi-code`)——认领全部五个检查点获得。 - 挑战:L01–L05 检查点,各 10 分;在 flypython.com 上集齐五个再加 50 分课程徽章奖励。 -- 证据:`python verify.py progress`——L03(有界改动)与 L04(验证与评审)由套件客观把关;L01/L02/L05 为学习者自我报告。 +- 证据:`python verify.py`——L03(有界改动)与 L04(验证与评审)由套件客观把关;L01/L02/L05 为学习者自我报告。 - 提交:每个通过的检查点打印一个确定性的认领码;在 flypython.com 上记入你的账号。这是自我报告的证据,从来不是证书。 ## 文件夹地图 diff --git a/courses/hands-on-with-kimi-code/REVIEW.md b/courses/hands-on-with-kimi-code/REVIEW.md index bd8acaa..e00c929 100644 --- a/courses/hands-on-with-kimi-code/REVIEW.md +++ b/courses/hands-on-with-kimi-code/REVIEW.md @@ -20,6 +20,20 @@ used; every Kimi Code behavior described in the lessons is sourced from official documentation, not observed. +## 2026-09-13 — 0.0.8 rework: lesson 1 is now tool → Skill → agent fetch (FP-822) + +- L01 (EN+ZH) rewritten around the fixed order: get the tool running → + install the FlyPython Skill (network access included) → let the agent + fetch this course's files via the files endpoint. All "download this + folder" wording removed from `COURSE.md` and lessons. +- The learner's check command across lessons is now the bare + `python verify.py` (FP-820): per-checkpoint status + claim codes, + non-zero exit while gated checkpoints are open. `solution` is no longer + presented as a completion standard. +- Tool-specific Skill install and network steps are authored from the + same official documentation as the course; **not yet exercised inside + the tool** — the pending live run-through below still stands. + ## Pending before this course can be called taught-in-tool - One full agent-taught run-through inside Kimi Code ("start lesson 1" diff --git a/courses/hands-on-with-kimi-code/lessons/L01.md b/courses/hands-on-with-kimi-code/lessons/L01.md index 50ce567..7995421 100644 --- a/courses/hands-on-with-kimi-code/lessons/L01.md +++ b/courses/hands-on-with-kimi-code/lessons/L01.md @@ -1,30 +1,67 @@ --- id: course-kimi-code-l01 type: course -title: "Lesson 1: Reproduce the failure, survey it with the read-only lane" -summary: "Run kimi in the course folder, let the read-only explore subagent map the code, and make the failure objective with verify.py." +title: "Lesson 1: Set up Kimi Code, install the FlyPython Skill, fetch the course" +summary: "Get Kimi Code running, install the FlyPython Skill with network access approved, and let the agent fetch this course's files — you download nothing. Then make the starter's failure objective." lang: en-US -content_version: 1 +content_version: 2 status: reviewed reviewed_on: 2026-09-13 hints: - title: "What this checkpoint tests" - body: "That you can reproduce the failure before fixing it: `python verify.py starter --expect-failure` must print the named failing tests — whether you ran it or the agent's shell tool did, the command is the evidence." + body: "That you can reproduce the failure before fixing it: `python verify.py` must show l03 and l04 as `[open]` (`starter --expect-failure` prints the named tests) — whether you ran it or the agent's shell tool did, the command is the evidence." - title: "Lanes, not one blob" body: "Kimi Code ships built-in subagents: `explore` is read-only, `plan` plans without write or shell tools, `coder` can read, write, and run commands. Read-only work belongs in the read-only lane — it cannot break anything." - title: "When you are done" - body: "You can say which nine behaviors the suite proves and why the starter cannot fake them. `python verify.py progress` then shows this checkpoint's claim code." + body: "You can say which nine behaviors the suite proves and why the starter cannot fake them. `python verify.py` then shows this checkpoint's claim code." --- -# Lesson 1: Reproduce the failure, survey it with the read-only lane +# Lesson 1: Set up Kimi Code, install the FlyPython Skill, fetch the course ## Objective -By the end of this lesson you have `kimi` running in the course folder, -you have run the course's objective verification command, and you can +By the end of this lesson Kimi Code is running, the FlyPython Skill is +installed in it (with network access approved), and the agent has fetched +this course's files into your working folder — you downloaded nothing by +hand. With the folder in place you run the course's objective check and explain what "the starter fails" means in test names. No code is changed in this lesson. +## Step 1 — Get Kimi Code running + +Install the `kimi` CLI and sign in with +your Moonshot/Kimi account or API key, then start `kimi` in an empty +working folder. + +## Step 2 — Install the FlyPython Skill (network included) + +`kimi` reads `AGENTS.md` from the project root: + +```bash +curl -s https://flypython.com/skills/flypython/SKILL.md >> AGENTS.md +``` + +Approve the agent's network commands when it asks; if blocked, run the +curl yourself and paste the file. + +The Skill is one file, the same in every tool: it tells the agent how to +authorize you, fetch course files, verify, and submit claim codes. + +## Step 3 — Let the agent fetch this course (you download nothing) + +Start a new session/chat/thread in the working folder and paste this one +sentence: + +> Read https://flypython.com/skills/flypython/SKILL.md and start the FlyPython course `hands-on-with-kimi-code`. + +The agent gives you an authorization link with a short code. Open it, log +in, make sure the code matches what the agent showed you, and click +Allow — then tell the agent "done". It takes a token once and writes this +course's files to `courses/hands-on-with-kimi-code/`. + +**Prerequisite:** these courses need a coding agent that can run commands +*and* reach the network. A chat-only web AI cannot do them. + ## Why this lesson exists Most AI-written Python dies the same way: the demo input works, the real @@ -46,17 +83,18 @@ open `TASK.md` and keep it next to you. ## The lesson -Start `kimi` in the course folder. Ask for a read-only survey first — the -`explore` subagent reads files but cannot change them: +The course files are in `courses/hands-on-with-kimi-code/` now — no download happened on +your side. Ask the agent to survey them without changing anything: -**"Use the explore subagent to read COURSE.md, TASK.md, and -starter/report_tool.py. Do not change any file. Report which behaviors the -tests expect that the starter does not have."** +**"Read COURSE.md — it is the teaching contract for this folder. Then read +TASK.md and starter/report_tool.py. Do not change any file. Tell me which +behaviors the tests expect that the starter does not have."** -While it reads, run the objective verification yourself in a terminal: +While it reads, run the learner's command — your single check for the +rest of the course: ```bash -python verify.py starter --expect-failure +python verify.py ``` You should see the starter reproduce four real-world failure classes, @@ -71,15 +109,16 @@ mapped to test names: | `test_write_report_creates_missing_parent_directories` | the tool cannot write into a fresh output directory | | `test_run_scenario_writes_report_file`, `test_main_prints_summary_and_returns_zero` | end-to-end consequences of the above | -Then run the reviewed implementation the same way: +If you want the missing behaviors as exact failing test names, the +maintainer command prints them: ```bash -python verify.py solution +python verify.py starter --expect-failure ``` -Both commands exit 0 — but they mean opposite things. The first proves the -starter is *correctly unfinished*; the second proves the target is -reachable. That pair is your completion evidence for the whole course. +(The reference `solution/` exists so maintainers can prove the target is +reachable. You never need to run it, and it is never the completion +standard — your implementation in `starter/` is.) ## Exercise @@ -91,15 +130,18 @@ row will trigger. Then check yourself against the table above — and have ## Checkpoint -Run `python verify.py starter --expect-failure` and -`python verify.py solution`. You pass this lesson when you can answer, -without looking: +Run `python verify.py` in the course folder. You pass this lesson when you +can answer, without looking: 1. Which four behaviors separate starter from solution? -2. Why is "expected failure" a success condition, not a contradiction? +2. Why does `python verify.py` exit non-zero on purpose — what state is + it reporting, and why is that a success condition rather than an error? 3. Which subagent can change files — and which two cannot? +This checkpoint's claim code is already in the default command's output — +submit it only once you can answer the questions. + ## Expected evidence -The transcript of both commands, and your three answers. Keep them; +The transcript of the default command, and your three answers. Keep them; Lesson 4 will ask for them again. diff --git a/courses/hands-on-with-kimi-code/lessons/L01_cn.md b/courses/hands-on-with-kimi-code/lessons/L01_cn.md index 998de6b..2384dc8 100644 --- a/courses/hands-on-with-kimi-code/lessons/L01_cn.md +++ b/courses/hands-on-with-kimi-code/lessons/L01_cn.md @@ -1,29 +1,63 @@ --- id: course-kimi-code-l01 type: course -title: "第 1 课:复现故障,用只读道做勘察" -summary: "在课程文件夹里运行 kimi,让只读的 explore 子代理摸清代码,用 verify.py 让失败变得客观。" +title: "第 1 课:装好 Kimi Code、装上 FlyPython Skill、让 Agent 取回课程" +summary: "先把 Kimi Code 跑起来,装好 FlyPython Skill 并打开联网权限,让 Agent 取回本课文件——你不下载任何东西,然后让 starter 的失败变得客观。" lang: zh-CN -content_version: 1 +content_version: 2 status: reviewed reviewed_on: 2026-09-13 hints: - title: "本检查点考察什么" - body: "先复现失败再动手修:`python verify.py starter --expect-failure` 必须打印出点名的失败测试——你自己跑也好、Agent 的 shell 工具跑也好,命令就是证据。" + body: "先复现失败再动手修:`python verify.py` 必须把 l03 和 l04 显示为 `[open]`(`starter --expect-failure` 会打印点名测试)——你自己跑也好、Agent 的 shell 工具跑也好,命令就是证据。" - title: "分道,不是一坨" body: "Kimi Code 自带内置子代理:`explore` 只读、`plan` 没有写和 shell 工具、`coder` 能读写跑命令。只读的活就放只读道——它弄不坏任何东西。" - title: "完成的标志" - body: "你能说出套件证明了哪九种行为、为什么 starter 伪造不出来。`python verify.py progress` 随即显示本检查点的认领码。" + body: "你能说出套件证明了哪九种行为、为什么 starter 伪造不出来。`python verify.py` 随即显示本检查点的认领码。" --- -# 第 1 课:复现故障,用只读道做勘察 +# 第 1 课:装好 Kimi Code、装上 FlyPython Skill、让 Agent 取回课程 ## 目标 -本课结束时,你在课程文件夹里跑起了 `kimi`,运行过课程的客观验证 -命令,并能解释「starter 失败」在测试名里的含义。本课不改任何代码。 +本课结束时,Kimi Code 已经跑起来、FlyPython Skill 已装好(联网权限已 +打开),Agent 已把本课文件取回你的工作目录——你没有手动下载任何 +东西。文件夹就位后,你运行课程的客观检查命令,并解释「starter +失败」在测试名里的含义。本课不改任何代码。 -## 为什么有这节课 +## 第 1 步——把 Kimi Code 跑起来 + +安装 `kimi` CLI,用你的 Moonshot/Kimi +账号或 API key 登录,在一个空工作目录里启动 `kimi`。 + +## 第 2 步——装 FlyPython Skill(含联网权限) + +`kimi` 会读取项目根目录的 `AGENTS.md`: + +```bash +curl -s https://flypython.com/skills/flypython/SKILL.md >> AGENTS.md +``` + +Agent 请求联网命令时批准;如果被禁,就自己跑 curl,把文件粘贴 +过去。 + +Skill 只是一个文件,在每个工具里都一样:它告诉 Agent 如何为你 +授权、取课程文件、验证、提交认领码。 + +## 第 3 步——让 Agent 取回本课文件(你不下载) + +在工作目录里新开一个会话/对话/线程,粘贴这一句话: + +> Read https://flypython.com/skills/flypython/SKILL.md and start the FlyPython course `hands-on-with-kimi-code`. + +Agent 会给你一个授权链接和一组短码。打开链接、登录、核对码与 +Agent 显示的一致后点「允许」,然后对 Agent 说「好了」。它取一次 +token,把本课文件写到 `courses/hands-on-with-kimi-code/`。 + +**前提**:这些课程需要一个能执行命令**并且**能联网的编码 Agent。 +只能聊天的网页 AI 做不了。 + +## 为什么有这一课 AI 写的 Python 大多是同一种死法:演示输入能跑,真实输入一到,脚本 在第 3 行崩掉——或者更糟,悄悄算出一个错的数。修复不从更好的 @@ -40,17 +74,17 @@ docstring 的函数、类型标注、CLI。这正是 AI 生成代码的样子— ## 本课内容 -在课程文件夹里启动 `kimi`。先要一份只读勘察——`explore` 子代理 -能读文件但不能改: +本课文件现在已经位于 `courses/hands-on-with-kimi-code/`——你这边没有发生任何下载。 +让 Agent 先只读不动手: -**“用 explore 子代理读 COURSE.md、TASK.md 和 -starter/report_tool.py。不要改任何文件。报告测试期望哪些行为、 +**“读 COURSE.md——它是本文件夹的教学契约。再读 TASK.md 和 +starter/report_tool.py。不要改任何文件。告诉我测试期望哪些行为、 starter 缺了哪些。”** -趁它读的时候,自己在终端里跑客观验证: +趁它读的时候,运行学员命令——整个课程你只需要这一条检查命令: ```bash -python verify.py starter --expect-failure +python verify.py ``` 你会看到 starter 复现出四类真实世界故障,对应到测试名: @@ -64,14 +98,14 @@ python verify.py starter --expect-failure | `test_write_report_creates_missing_parent_directories` | 工具无法写进新建的输出目录 | | `test_run_scenario_writes_report_file`、`test_main_prints_summary_and_returns_zero` | 上述问题的端到端后果 | -然后用同样的方式跑评审过的实现: +想看缺失行为对应的失败测试名,维护者命令会打印它们: ```bash -python verify.py solution +python verify.py starter --expect-failure ``` -两条命令都以 0 退出——但含义相反。第一条证明 starter *确实处于 -正确的未完成态*;第二条证明目标可达。这一对命令是整门课的完成 +(参考答案 `solution/` 是给维护者证明目标可达用的。你不需要运行 +它,它也从来不是完成标准——你的实现(`starter/`)才是。)这一对命令是整门课的完成 证据。 ## 练习 @@ -83,14 +117,18 @@ python verify.py solution ## 检查点 -运行 `python verify.py starter --expect-failure` 和 -`python verify.py solution`。不看材料能回答下面三个问题即算通过: +在课程文件夹里运行 `python verify.py`。不看材料能回答下面三个 +问题即算通过: 1. 哪四个行为把 starter 和 solution 区分开? -2. 为什么「预期失败」是成功条件而不是自相矛盾? +2. `python verify.py` 为什么故意以非零退出——它在报告什么状态? + 为什么这是成功条件而不是报错? 3. 哪个子代理能改文件——哪两个不能? +本检查点的认领码已经在默认命令的输出里——能回答这些问题之后再 +提交。 + ## 预期证据 -两条命令的完整输出记录,加你的三个答案。保留好——第 4 课还会 +默认命令的完整输出记录,加你的三个答案。保留好——第 4 课还会 用到。 diff --git a/courses/hands-on-with-kimi-code/lessons/L02.md b/courses/hands-on-with-kimi-code/lessons/L02.md index 13bca2a..ccc1305 100644 --- a/courses/hands-on-with-kimi-code/lessons/L02.md +++ b/courses/hands-on-with-kimi-code/lessons/L02.md @@ -71,7 +71,7 @@ shape: inputs, outputs, error cases, and "done means `<command>` exits ## Checkpoint -Run `python verify.py progress` — this checkpoint's claim code prints +Run `python verify.py` — this checkpoint's claim code prints when you can answer: 1. Which `TASK.md` line does diff --git a/courses/hands-on-with-kimi-code/lessons/L02_cn.md b/courses/hands-on-with-kimi-code/lessons/L02_cn.md index 81ce022..381138a 100644 --- a/courses/hands-on-with-kimi-code/lessons/L02_cn.md +++ b/courses/hands-on-with-kimi-code/lessons/L02_cn.md @@ -66,7 +66,7 @@ hints: ## 检查点 -运行 `python verify.py progress`——能回答下面三题时,本检查点的 +运行 `python verify.py`——能回答下面三题时,本检查点的 认领码就会显示: 1. 用你自己的话说,`test_invalid_records_are_isolated_with_reasons` diff --git a/courses/hands-on-with-kimi-code/lessons/L03.md b/courses/hands-on-with-kimi-code/lessons/L03.md index 1b95bbf..4f84987 100644 --- a/courses/hands-on-with-kimi-code/lessons/L03.md +++ b/courses/hands-on-with-kimi-code/lessons/L03.md @@ -9,7 +9,7 @@ status: reviewed reviewed_on: 2026-09-13 hints: - title: "What this checkpoint tests" - body: "That the starter passes all nine tests through changes you supervised — `python verify.py progress` shows this code only once the starter suite is green." + body: "That the starter passes all nine tests through changes you supervised — `python verify.py` shows this code only once the starter suite is green." - title: "Coder is the only lane that edits" body: "Only the `coder` subagent can write files and run commands — which means every change funnels through one lane you can watch. Keep each step to one contract line and verify between steps." - title: "When you are done" @@ -67,7 +67,7 @@ you cannot, ask for a walkthrough of that edit before the next step runs. ## Checkpoint -Run `python verify.py progress`. This checkpoint's code appears only when +Run `python verify.py`. This checkpoint's code appears only when the starter suite is green. You pass the lesson when you can answer: 1. How many steps did the bounded change take, and what made each step diff --git a/courses/hands-on-with-kimi-code/lessons/L03_cn.md b/courses/hands-on-with-kimi-code/lessons/L03_cn.md index ad19f7d..2aa3b41 100644 --- a/courses/hands-on-with-kimi-code/lessons/L03_cn.md +++ b/courses/hands-on-with-kimi-code/lessons/L03_cn.md @@ -9,7 +9,7 @@ status: reviewed reviewed_on: 2026-09-13 hints: - title: "本检查点考察什么" - body: "starter 通过你监督的改动让九个测试全绿——`python verify.py progress` 只在 starter 套件变绿后显示本检查点的码。" + body: "starter 通过你监督的改动让九个测试全绿——`python verify.py` 只在 starter 套件变绿后显示本检查点的码。" - title: "coder 是唯一能编辑的道" body: "只有 `coder` 子代理能写文件、跑命令——每处改动都汇进你盯着的这一条道。每步守住一条契约行,步间跑验证。" - title: "完成的标志" @@ -63,7 +63,7 @@ Kimi Code 的分道帮了忙:`explore` 和 `plan` 根本碰不了文件—— ## 检查点 -运行 `python verify.py progress`。本检查点的码只在 starter 套件 +运行 `python verify.py`。本检查点的码只在 starter 套件 全绿时出现。不看材料能回答即算通过: 1. 这次有界改动走了几步,是什么让每步保持有界? diff --git a/courses/hands-on-with-kimi-code/lessons/L04.md b/courses/hands-on-with-kimi-code/lessons/L04.md index 9c11043..19dc8cc 100644 --- a/courses/hands-on-with-kimi-code/lessons/L04.md +++ b/courses/hands-on-with-kimi-code/lessons/L04.md @@ -9,7 +9,7 @@ status: reviewed reviewed_on: 2026-09-13 hints: - title: "What this checkpoint tests" - body: "That starter AND solution both pass — `verify.py progress` prints this code only when both suites are green. The gate is the pair, not a single run." + body: "That starter AND solution both pass — `verify.py` prints this code only when both suites are green. The gate is the pair, not a single run." - title: "Review the delta, not the vibes" body: "The working-tree diff shows the whole accumulated change. Read it as a reviewer: every hunk maps to a contract line, nothing extra moved, no test weakened." - title: "When you are done" @@ -66,7 +66,7 @@ Keep it honest — "not verified" is a list of facts, not a confession. ## Checkpoint -Run `python verify.py progress`. This checkpoint's code prints only when +Run `python verify.py`. This checkpoint's code prints only when both suites pass. You pass the lesson when you can answer: 1. What did your by-hand recompute of a group total show? diff --git a/courses/hands-on-with-kimi-code/lessons/L04_cn.md b/courses/hands-on-with-kimi-code/lessons/L04_cn.md index 4457b4b..0dd51ab 100644 --- a/courses/hands-on-with-kimi-code/lessons/L04_cn.md +++ b/courses/hands-on-with-kimi-code/lessons/L04_cn.md @@ -9,7 +9,7 @@ status: reviewed reviewed_on: 2026-09-13 hints: - title: "本检查点考察什么" - body: "starter 和 solution 双双通过——`verify.py progress` 只在两个套件都绿时打印本检查点的码。门槛是一对结果,不是一次运行。" + body: "starter 和 solution 双双通过——`verify.py` 只在两个套件都绿时打印本检查点的码。门槛是一对结果,不是一次运行。" - title: "审增量,不凭感觉" body: "工作区 diff 展示整个会话累积的改动。按评审者的方式读:每个 hunk 对上一条契约行、没有多余挪动、没有测试被放松。" - title: "完成的标志" @@ -62,7 +62,7 @@ python verify.py solution # 评审过的参考实现 ## 检查点 -运行 `python verify.py progress`。本检查点的码只在两个套件都过 +运行 `python verify.py`。本检查点的码只在两个套件都过 时打印。不看材料能回答即算通过: 1. 你手工重算的分组总计结果如何? diff --git a/courses/hands-on-with-kimi-code/verify.py b/courses/hands-on-with-kimi-code/verify.py index 614c9a2..4a178a1 100644 --- a/courses/hands-on-with-kimi-code/verify.py +++ b/courses/hands-on-with-kimi-code/verify.py @@ -68,6 +68,16 @@ def _claim_code(checkpoint_id): ).digest() return base64.b32encode(digest).decode("ascii")[:8] + +def _display_title(title): + # FP-820: shared-core checkpoint titles are stored "Chinese / English"; + # the default command prints English first, Chinese after (FP-709 debt). + if " / " in title: + left, _, right = title.partition(" / ") + if any("\u4e00" <= character <= "\u9fff" for character in left): + return right + " / " + left + return title + def _run_suite(implementation): environment = os.environ.copy() environment["PYTHONPATH"] = str(ROOT / implementation) @@ -76,7 +86,7 @@ def _run_suite(implementation): env=environment, check=False, capture_output=True, text=True, ) -def run_progress(as_json, receipt_out=None): +def run_progress(as_json, receipt_out=None, default=False): import time t0 = time.monotonic() starter = _run_suite("starter") @@ -122,6 +132,33 @@ def run_progress(as_json, receipt_out=None): COURSE_ID, checkpoint["id"], passed=passed, tests=tests, duration_ms=ms, impl_dir=ROOT / "starter", solution_hash=solution_hash, secret=secret)) + if default and not as_json: + # FP-820: bare ``python verify.py`` — check the learner's own + # implementation, print per-checkpoint status and earned claim codes, + # English first then Chinese (FP-709 debt). ``progress`` keeps its + # exact published output; this block is the only new surface. + print("Course " + COURSE_ID) + print("Suites: starter " + ("passed" if starter_ok else "not passed") + + " / solution " + ("passed" if solution_ok else "not passed")) + for row in rows: + state = row["status"] + (" (self-attested)" if row["kind"] == "attested" else "") + code = row["claim_code"] if row["claim_code"] else "\u2014" + print(" " + row["id"] + " " + _display_title(row["title"]) + " [" + state + "] " + code) + open_gates = [row["id"] for row in rows + if row["kind"] == "objective" and row["status"] != "passed"] + if open_gates: + print("Next: keep implementing starter/ until " + ", ".join(open_gates) + + " show [passed]; then re-run: python verify.py") + else: + print("All objective checkpoints passed — the codes above are ready" + " to submit (batch POST /api/claims, see SKILL.md §5).") + print("\u4e2d\u6587\uff1a\u68c0\u67e5\u70b9\u72b6\u6001\u4e0e\u8ba4\u9886\u7801\u89c1\u4e0a\uff1b\u5e26 [passed]/[attest] \u7684\u884c\u6709\u8ba4\u9886\u7801\uff0c[open] \u7684\u884c\u7ee7\u7eed\u5728 starter/ \u91cc\u5b9e\u73b0\u540e\u518d\u8dd1\u3002") + print("Claim codes are self-reported evidence, recorded at flypython.com; never a certificate.") + print("\u8ba4\u9886\u7801\u662f\u81ea\u6211\u62a5\u544a\u7684\u8bc1\u636e\uff0c\u8bb0\u5f55\u5728 flypython.com\uff1b\u4e0d\u662f\u8bc1\u4e66\u3002") + if secret: + print(f"Signed run receipts prepared for {len(receipts)} gated checkpoint(s);" + " submit each with its claim to mark it as a local-run receipt.") + return 1 if open_gates else 0 if as_json: document = {"course": COURSE_ID, "starter_suite_passed": starter_ok, @@ -152,14 +189,19 @@ def run_progress(as_json, receipt_out=None): def main() -> int: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("implementation", choices=("progress", "starter", "solution")) + parser.add_argument("implementation", choices=("progress", "starter", "solution"), + nargs="?", default=None) parser.add_argument("--json", action="store_true") parser.add_argument("--expect-failure", action="store_true") parser.add_argument("--receipt-out", metavar="PATH", help="write signed run receipts JSON (requires FLYPYTHON_CLAIM_SECRET)") args = parser.parse_args() - if args.implementation == "progress": - return run_progress(args.json, args.receipt_out) + if args.implementation in (None, "progress"): + # FP-820: the bare command is the learner's default — same engine as + # ``progress``, English-first output, exit 1 while gates stay open. + # The four explicit usages remain for maintainers. + return run_progress(args.json, args.receipt_out, + default=args.implementation is None) command = [sys.executable, "-m", "unittest", "discover", "-s", str(ROOT / "tests")] environment = os.environ.copy() diff --git a/courses/hands-on-with-openai-codex/COURSE.md b/courses/hands-on-with-openai-codex/COURSE.md index e2732c4..78ed58d 100644 --- a/courses/hands-on-with-openai-codex/COURSE.md +++ b/courses/hands-on-with-openai-codex/COURSE.md @@ -4,7 +4,7 @@ type: course title: Hands-on with the OpenAI Codex App summary: The verified Python workflow — task contract, bounded change, objective verify.py evidence — taught hands-on in the Codex desktop app, where an agent thread runs the loop and you supervise diffs and approvals. lang: en-US -content_version: 3 +content_version: 4 status: reviewed reviewed_on: 2026-09-13 badge: @@ -17,8 +17,9 @@ course_id: course-codex-cli # Hands-on with the OpenAI Codex App -> TL;DR: download this folder, open it as a project in the Codex desktop app, -> start a thread, and say **"start lesson 1"**. You finish with a working +> TL;DR: set up the Codex app, install the FlyPython Skill, and let the agent +> fetch this course — lesson 1 walks you through all three steps and you +> download nothing. Start a thread and say **"start lesson 1"**. You finish with a working > report tool, a reproducible pass/fail command, and the desktop-agent > workflow: threads, approvals, AGENTS.md, and diff review. The exercise core > is the same one the Claude Code course uses — only the tool you drive @@ -68,12 +69,12 @@ If you are the Codex app agent teaching this course, follow these rules: - **When to stop:** a lesson is done when its checkpoint command runs and the learner can say, in their own words, what failed and why. If the learner cannot, re-teach from the failing test, not from the solution. -- **How to use `verify.py`:** run it in the app's terminal or via a thread - command — `python verify.py starter --expect-failure` must reproduce the - listed failures; `python verify.py solution` must pass. "Done" means the - starter implementation passes the same suite after the learner's bounded - change. -- **Honesty rules:** say what you did not verify. Do not claim the code is +- **How to use `verify.py`:** the learner's command is plain + `python verify.py` — it runs the suites, prints per-checkpoint status + and earned claim codes, and exits non-zero while the gated checkpoints + (l03, l04) are still open. `starter --expect-failure` and `solution` + remain maintainer checks. "Done" means the default command shows every + gate `[passed]` after the learner's bounded change.- **Honesty rules:** say what you did not verify. Do not claim the code is production-ready. Do not promise learning or career outcomes. ## What this course does NOT cover @@ -86,7 +87,7 @@ guides and playbooks — link, don't improvise. - Badge: **Reproduce with Codex in the loop Badge** (badge id `course-codex-cli`) — earned by claiming all five checkpoints. - Challenges: L01–L05 checkpoints, 10 points each; +50 course-badge bonus when all five are claimed on flypython.com. -- Evidence: `python verify.py progress` — L03 (bounded change) and L04 (verify & review) are objectively gated by the suite; L01/L02/L05 are learner-attested. +- Evidence: `python verify.py` — L03 (bounded change) and L04 (verify & review) are objectively gated by the suite; L01/L02/L05 are learner-attested. - Submission: each passed checkpoint prints a deterministic claim code; record it on flypython.com against your account. Self-reported evidence, never a certificate. ## Folder map diff --git a/courses/hands-on-with-openai-codex/COURSE_cn.md b/courses/hands-on-with-openai-codex/COURSE_cn.md index 399cb27..de0bb91 100644 --- a/courses/hands-on-with-openai-codex/COURSE_cn.md +++ b/courses/hands-on-with-openai-codex/COURSE_cn.md @@ -4,7 +4,7 @@ type: course title: "上手 OpenAI Codex 应用" summary: "经过验证的 Python 工作流——任务契约、有界改动、客观 verify.py 证据——在 Codex 桌面应用里手把手教:由 Agent 线程跑闭环,你监督 diff 与审批。" lang: zh-CN -content_version: 3 +content_version: 4 status: reviewed reviewed_on: 2026-09-13 badge: @@ -17,8 +17,8 @@ course_id: course-codex-cli # 上手 OpenAI Codex 应用 -> 一句话:下载本文件夹,在 Codex 桌面应用里作为项目打开,开一个线程, -> 对它说**「开始第 1 课」**。完成时你会得到一个能跑的报表工具、一条 +> 一句话:装好 Codex 桌面应用、装上 FlyPython Skill、让 Agent 取回本课文件 +> (第 1 课就是这三步,你什么都不用下载),开一个线程,对它说**「开始第 1 课」**。完成时你会得到一个能跑的报表工具、一条 > 可复现的通过/失败命令,以及桌面 Agent 工作流:线程、审批、 > AGENTS.md 与 diff 审查。练习核心与 Claude Code 课程完全相同—— > 变的只是你驱动的工具。 @@ -61,11 +61,10 @@ course_id: course-codex-cli - **何时停下:** 一节课的检查点命令跑通、且学习者能用自己的话说清 哪里坏了、为什么,这节课才算完。学习者说不出就从失败测试重新教, 不要从 solution 教。 -- **如何使用 `verify.py`:** 在应用终端里跑,或通过线程命令—— - `python verify.py starter --expect-failure` 必须复现列出的失败; - `python verify.py solution` 必须通过。「完成」指学习者做完有界 - 改动后,starter 实现通过同一套测试。 -- **诚实规则:** 说清你没验证什么。不要声称代码达到生产可用。不要 +- **如何使用 `verify.py`:** 学员命令就是 `python verify.py`——它跑套件、 + 打印逐检查点状态与已获认领码,在门控检查点(l03、l04)未全通过时以 + 非零退出。`starter --expect-failure` 与 `solution` 留作维护者检查。 + 「完成」指学习者做完有界改动后,默认命令把所有门显示为 `[passed]`。- **诚实规则:** 说清你没验证什么。不要声称代码达到生产可用。不要 承诺学习或职业结果。 ## 本课程不覆盖什么 @@ -78,7 +77,7 @@ Codex 应用的安装与计费、Codex CLI 的旗标、CI/CD 云端任务、MCP - 徽章:**Codex 协同复现徽章**(badge id `course-codex-cli`)——认领全部五个检查点获得。 - 挑战:L01–L05 检查点,各 10 分;在 flypython.com 上集齐五个再加 50 分课程徽章奖励。 -- 证据:`python verify.py progress`——L03(有界改动)与 L04(验证与评审)由套件客观把关;L01/L02/L05 为学习者自我报告。 +- 证据:`python verify.py`——L03(有界改动)与 L04(验证与评审)由套件客观把关;L01/L02/L05 为学习者自我报告。 - 提交:每个通过的检查点打印一个确定性的认领码;在 flypython.com 上记入你的账号。这是自我报告的证据,从来不是证书。 ## 文件夹地图 diff --git a/courses/hands-on-with-openai-codex/REVIEW.md b/courses/hands-on-with-openai-codex/REVIEW.md index 048959a..13ad8f9 100644 --- a/courses/hands-on-with-openai-codex/REVIEW.md +++ b/courses/hands-on-with-openai-codex/REVIEW.md @@ -37,6 +37,20 @@ source material. - Whether a free ChatGPT tier lasts the whole course is unverified. +## 2026-09-13 — 0.0.8 rework: lesson 1 is now tool → Skill → agent fetch (FP-822) + +- L01 (EN+ZH) rewritten around the fixed order: get the tool running → + install the FlyPython Skill (network access included) → let the agent + fetch this course's files via the files endpoint. All "download this + folder" wording removed from `COURSE.md` and lessons. +- The learner's check command across lessons is now the bare + `python verify.py` (FP-820): per-checkpoint status + claim codes, + non-zero exit while gated checkpoints are open. `solution` is no longer + presented as a completion standard. +- Tool-specific Skill install and network steps are authored from the + same official documentation as the course; **not yet exercised inside + the tool** — the pending live run-through below still stands. + ## Deviation log ### 2026-09-13 — agent solvability run (challenge mode, not taught mode) diff --git a/courses/hands-on-with-openai-codex/lessons/L01.md b/courses/hands-on-with-openai-codex/lessons/L01.md index 7253033..93989ce 100644 --- a/courses/hands-on-with-openai-codex/lessons/L01.md +++ b/courses/hands-on-with-openai-codex/lessons/L01.md @@ -1,28 +1,67 @@ --- id: course-codex-cli-l01 type: course -title: "Lesson 1: Reproduce the failure in a Codex thread" -summary: "Open the folder as a Codex project, start your first thread, and make the failure objective with verify.py — inside the app, not beside it." +title: "Lesson 1: Set up the Codex app, install the FlyPython Skill, fetch the course" +summary: "Get the Codex app running, install the FlyPython Skill with network access approved, and let the agent fetch this course's files — you download nothing. Then make the starter's failure objective." lang: en-US -content_version: 2 +content_version: 3 status: reviewed reviewed_on: 2026-09-13 hints: - title: "What this checkpoint tests" - body: "That you can reproduce the failure before fixing it: `python verify.py starter --expect-failure` must print the named failing tests. In the Codex app the thread is where the work happens — the command is still the evidence." + body: "That you can reproduce the failure before fixing it: `python verify.py` must show l03 and l04 as `[open]` (`starter --expect-failure` prints the named tests). In the Codex app the thread is where the work happens — the command is still the evidence." - title: "Threads are tasks, not chats" body: "A Codex thread is one bounded job: your prompt plus the agent's tool calls and diffs. Keep this thread to 'reproduce and explain' — the fix belongs to lesson 3, in its own thread." - title: "When you are done" - body: "You can say out loud which nine behaviors the suite proves and why the starter cannot fake them. `python verify.py progress` then shows this checkpoint's claim code." + body: "You can say out loud which nine behaviors the suite proves and why the starter cannot fake them. `python verify.py` then shows this checkpoint's claim code." --- -# Lesson 1: Reproduce the failure in a Codex thread +# Lesson 1: Set up the Codex app, install the FlyPython Skill, fetch the course ## Objective -By the end of this lesson you can open the course folder in the Codex desktop -app, run the course's objective verification command, and explain what "the -starter fails" means in test names. No code is changed in this lesson. +By the end of this lesson the Codex app is running, the FlyPython Skill is +installed in it (with network access approved), and the agent has fetched +this course's files into your working folder — you downloaded nothing by +hand. With the folder in place you run the course's objective check and +explain what "the starter fails" means in test names. No code is changed +in this lesson. + +## Step 1 — Get the Codex app running + +Install the OpenAI Codex desktop app, +sign in, and open an empty folder as the project. + +## Step 2 — Install the FlyPython Skill (network included) + +The app reads `AGENTS.md` from the project — append the Skill to it: + +```bash +curl -s https://flypython.com/skills/flypython/SKILL.md >> AGENTS.md +``` + +Start the thread in that folder. Network: the app approves commands and +web access per thread — approve them when asked. (The Codex CLI's default +sandbox blocks network; if your setup blocks the fetch, run the curl +yourself in a terminal — the agent reads the file from disk.) + +The Skill is one file, the same in every tool: it tells the agent how to +authorize you, fetch course files, verify, and submit claim codes. + +## Step 3 — Let the agent fetch this course (you download nothing) + +Start a new session/chat/thread in the working folder and paste this one +sentence: + +> Read https://flypython.com/skills/flypython/SKILL.md and start the FlyPython course `hands-on-with-openai-codex`. + +The agent gives you an authorization link with a short code. Open it, log +in, make sure the code matches what the agent showed you, and click +Allow — then tell the agent "done". It takes a token once and writes this +course's files to `courses/hands-on-with-openai-codex/`. + +**Prerequisite:** these courses need a coding agent that can run commands +*and* reach the network. A chat-only web AI cannot do them. ## Why this lesson exists @@ -42,20 +81,18 @@ behavior. Now open `TASK.md` and keep it next to you. ## The lesson -Open the course folder as a project in the Codex app, then start a new -thread. A thread is a single task: it holds your prompt, the agent's tool -calls, and the resulting diffs. Tell it: +The course files are in `courses/hands-on-with-openai-codex/` now — no download happened on +your side. Ask the agent to survey them without changing anything: **"Read COURSE.md — it is the teaching contract for this folder. Then read TASK.md and starter/report_tool.py. Do not change any file. Tell me which behaviors the tests expect that the starter does not have."** -While it reads, run the objective verification yourself — in the app's -terminal, or let the agent run it in the thread (command execution asks for -your approval; read the command before approving): +While it reads, run the learner's command — your single check for the +rest of the course: ```bash -python verify.py starter --expect-failure +python verify.py ``` You should see the starter reproduce four real-world failure classes, mapped @@ -70,14 +107,16 @@ to test names: | `test_write_report_creates_missing_parent_directories` | the tool cannot write into a fresh output directory | | `test_run_scenario_writes_report_file`, `test_main_prints_summary_and_returns_zero` | end-to-end consequences of the above | -Then run the reviewed implementation the same way: +If you want the missing behaviors as exact failing test names, the +maintainer command prints them: ```bash -python verify.py solution +python verify.py starter --expect-failure ``` -Both commands exit 0 — but they mean opposite things. The first proves the -starter is *correctly unfinished*; the second proves the target is reachable. +(The reference `solution/` exists so maintainers can prove the target is +reachable. You never need to run it, and it is never the completion +standard — your implementation in `starter/` is.) That pair is your completion evidence for the whole course. ## Exercise @@ -90,16 +129,19 @@ to confirm, not to fix. ## Checkpoint -Run `python verify.py starter --expect-failure` and -`python verify.py solution`. You pass this lesson when you can answer, without -looking: +Run `python verify.py` in the course folder. You pass this lesson when you +can answer, without looking: 1. Which four behaviors separate starter from solution? -2. Why is "expected failure" a success condition, not a contradiction? -3. What does the row you found in the exercise do to the starter — crash, - isolate, or silently pass? +2. Why does `python verify.py` exit non-zero on purpose — what state is + it reporting, and why is that a success condition rather than an error? +3. Where in the thread can you see the command the agent ran — and who + approved it? + +This checkpoint's claim code is already in the default command's output — +submit it only once you can answer the questions. ## Expected evidence -The transcript of both commands, and your three answers. Keep them; Lesson 4 +The transcript of the default command, and your three answers. Keep them; Lesson 4 will ask for them again. diff --git a/courses/hands-on-with-openai-codex/lessons/L01_cn.md b/courses/hands-on-with-openai-codex/lessons/L01_cn.md index 908b1b4..f2b96c5 100644 --- a/courses/hands-on-with-openai-codex/lessons/L01_cn.md +++ b/courses/hands-on-with-openai-codex/lessons/L01_cn.md @@ -1,29 +1,64 @@ --- id: course-codex-cli-l01 type: course -title: "第 1 课:在 Codex 线程里复现故障" -summary: "把文件夹作为 Codex 项目打开,开启第一个线程,用 verify.py 让失败变得客观——在应用内完成,而不是在应用旁边。" +title: "第 1 课:装好 the Codex app、装上 FlyPython Skill、让 Agent 取回课程" +summary: "先把 the Codex app 跑起来,装好 FlyPython Skill 并打开联网权限,让 Agent 取回本课文件——你不下载任何东西,然后让 starter 的失败变得客观。" lang: zh-CN -content_version: 2 +content_version: 3 status: reviewed reviewed_on: 2026-09-13 hints: - title: "本检查点考察什么" - body: "先复现失败再动手修:`python verify.py starter --expect-failure` 必须打印出点名的失败测试。在 Codex 应用里,线程是干活的地方——但命令仍然是证据。" + body: "先复现失败再动手修:`python verify.py` 必须把 l03 和 l04 显示为 `[open]`(`starter --expect-failure` 会打印点名测试)。在 Codex 应用里,线程是干活的地方——但命令仍然是证据。" - title: "线程是任务,不是聊天" body: "Codex 线程是一个有边界的任务:你的提示加 Agent 的工具调用和 diff。这个线程只做「复现并解释」——修复属于第 3 课,开新线程做。" - title: "完成的标志" - body: "你能说出套件证明了哪九种行为、为什么 starter 伪造不出来。`python verify.py progress` 随即显示本检查点的认领码。" + body: "你能说出套件证明了哪九种行为、为什么 starter 伪造不出来。`python verify.py` 随即显示本检查点的认领码。" --- -# 第 1 课:在 Codex 线程里复现故障 +# 第 1 课:装好 the Codex app、装上 FlyPython Skill、让 Agent 取回课程 ## 目标 -本课结束时,你能在 Codex 桌面应用里打开课程文件夹、运行课程的客观 -验证命令,并解释「starter 失败」在测试名里的含义。本课不改任何代码。 +本课结束时,the Codex app 已经跑起来、FlyPython Skill 已装好(联网权限已 +打开),Agent 已把本课文件取回你的工作目录——你没有手动下载任何 +东西。文件夹就位后,你运行课程的客观检查命令,并解释「starter +失败」在测试名里的含义。本课不改任何代码。 -## 为什么有这节课 +## 第 1 步——把 the Codex app 跑起来 + +安装 OpenAI Codex 桌面应用并登录, +把一个空文件夹作为项目打开。 + +## 第 2 步——装 FlyPython Skill(含联网权限) + +应用会读取项目里的 `AGENTS.md`——把 Skill 追加进去: + +```bash +curl -s https://flypython.com/skills/flypython/SKILL.md >> AGENTS.md +``` + +在这个文件夹里开线程。联网:应用按线程批准命令和网络访问——被 +询问时批准。(Codex 命令行版默认沙箱禁网;如果你的环境取不到 +文件,就在终端里自己跑 curl——Agent 会从磁盘读它。) + +Skill 只是一个文件,在每个工具里都一样:它告诉 Agent 如何为你 +授权、取课程文件、验证、提交认领码。 + +## 第 3 步——让 Agent 取回本课文件(你不下载) + +在工作目录里新开一个会话/对话/线程,粘贴这一句话: + +> Read https://flypython.com/skills/flypython/SKILL.md and start the FlyPython course `hands-on-with-openai-codex`. + +Agent 会给你一个授权链接和一组短码。打开链接、登录、核对码与 +Agent 显示的一致后点「允许」,然后对 Agent 说「好了」。它取一次 +token,把本课文件写到 `courses/hands-on-with-openai-codex/`。 + +**前提**:这些课程需要一个能执行命令**并且**能联网的编码 Agent。 +只能聊天的网页 AI 做不了。 + +## 为什么有这一课 AI 写的 Python 大多是同一种死法:演示输入能跑,真实输入一到,脚本在 第 3 行崩掉——或者更糟,悄悄算出一个错的数。修复不从更好的提示词 @@ -39,18 +74,17 @@ diff 和审批的任务,不是一条滚动的聊天记录。 ## 本课内容 -在 Codex 应用里把课程文件夹作为项目打开,然后开一个新线程。线程是 -单个任务:它装着你的提示、Agent 的工具调用和产生的 diff。对它说: +本课文件现在已经位于 `courses/hands-on-with-openai-codex/`——你这边没有发生任何下载。 +让 Agent 先只读不动手: **“读 COURSE.md——它是本文件夹的教学契约。再读 TASK.md 和 starter/report_tool.py。不要改任何文件。告诉我测试期望哪些行为、 starter 缺了哪些。”** -趁它读的时候,自己跑客观验证——在应用的终端里,或者让 Agent 在线程里 -跑(命令执行会请求你的审批;批准前先读命令): +趁它读的时候,运行学员命令——整个课程你只需要这一条检查命令: ```bash -python verify.py starter --expect-failure +python verify.py ``` 你会看到 starter 复现出四类真实世界故障,对应到测试名: @@ -64,14 +98,14 @@ python verify.py starter --expect-failure | `test_write_report_creates_missing_parent_directories` | 工具无法写进新建的输出目录 | | `test_run_scenario_writes_report_file`、`test_main_prints_summary_and_returns_zero` | 上述问题的端到端后果 | -然后用同样的方式跑评审过的实现: +想看缺失行为对应的失败测试名,维护者命令会打印它们: ```bash -python verify.py solution +python verify.py starter --expect-failure ``` -两条命令都以 0 退出——但含义相反。第一条证明 starter *确实处于正确的 -未完成态*;第二条证明目标可达。这一对命令是整门课的完成证据。 +(参考答案 `solution/` 是给维护者证明目标可达用的。你不需要运行 +它,它也从来不是完成标准——你的实现(`starter/`)才是。)这一对命令是整门课的完成证据。 ## 练习 @@ -82,13 +116,17 @@ python verify.py solution ## 检查点 -运行 `python verify.py starter --expect-failure` 和 -`python verify.py solution`。不看材料能回答下面三个问题即算通过: +在课程文件夹里运行 `python verify.py`。不看材料能回答下面三个 +问题即算通过: 1. 哪四个行为把 starter 和 solution 区分开? -2. 为什么「预期失败」是成功条件而不是自相矛盾? -3. 你在练习里找到的那一行会让 starter 崩溃、被隔离,还是悄悄通过? +2. `python verify.py` 为什么故意以非零退出——它在报告什么状态? + 为什么这是成功条件而不是报错? +3. 在线程的哪里能看到 Agent 执行过的命令——又是谁批准的? + +本检查点的认领码已经在默认命令的输出里——能回答这些问题之后再 +提交。 ## 预期证据 -两条命令的完整输出记录,加你的三个答案。保留好——第 4 课还会用到。 +默认命令的完整输出记录,加你的三个答案。保留好——第 4 课还会用到。 diff --git a/courses/hands-on-with-openai-codex/lessons/L02.md b/courses/hands-on-with-openai-codex/lessons/L02.md index d72991d..a031eda 100644 --- a/courses/hands-on-with-openai-codex/lessons/L02.md +++ b/courses/hands-on-with-openai-codex/lessons/L02.md @@ -72,7 +72,7 @@ the two rules you would put in that project's AGENTS.md. ## Checkpoint -Run `python verify.py progress` — this checkpoint's claim code prints when +Run `python verify.py` — this checkpoint's claim code prints when you can answer: 1. Which `TASK.md` line does `test_invalid_records_are_isolated_with_reasons` diff --git a/courses/hands-on-with-openai-codex/lessons/L02_cn.md b/courses/hands-on-with-openai-codex/lessons/L02_cn.md index ce331a8..4f264dd 100644 --- a/courses/hands-on-with-openai-codex/lessons/L02_cn.md +++ b/courses/hands-on-with-openai-codex/lessons/L02_cn.md @@ -68,7 +68,7 @@ AGENTS.md 里,让每个新线程开工就带上。”** 然后开一个*新* ## 检查点 -运行 `python verify.py progress`——能回答下面三题时,本检查点的 +运行 `python verify.py`——能回答下面三题时,本检查点的 认领码就会显示: 1. 用你自己的话说,`test_invalid_records_are_isolated_with_reasons` diff --git a/courses/hands-on-with-openai-codex/lessons/L03.md b/courses/hands-on-with-openai-codex/lessons/L03.md index ebbe4af..6710855 100644 --- a/courses/hands-on-with-openai-codex/lessons/L03.md +++ b/courses/hands-on-with-openai-codex/lessons/L03.md @@ -9,7 +9,7 @@ status: reviewed reviewed_on: 2026-09-13 hints: - title: "What this checkpoint tests" - body: "That the starter passes all nine tests through changes you supervised — `python verify.py progress` shows this code only once the starter suite is green." + body: "That the starter passes all nine tests through changes you supervised — `python verify.py` shows this code only once the starter suite is green." - title: "Approvals are the boundary" body: "The app asks before running commands and shows each file diff before it lands. Read the command, read the diff — approving blind is how bounded work becomes a rewrite." - title: "When you are done" @@ -70,7 +70,7 @@ accepting the next change. ## Checkpoint -Run `python verify.py progress`. This checkpoint's code appears only when the +Run `python verify.py`. This checkpoint's code appears only when the starter suite is green. You pass the lesson when you can answer: 1. How many turns did the bounded change take, and what made each turn bounded? diff --git a/courses/hands-on-with-openai-codex/lessons/L03_cn.md b/courses/hands-on-with-openai-codex/lessons/L03_cn.md index 43268fd..a633ee9 100644 --- a/courses/hands-on-with-openai-codex/lessons/L03_cn.md +++ b/courses/hands-on-with-openai-codex/lessons/L03_cn.md @@ -9,7 +9,7 @@ status: reviewed reviewed_on: 2026-09-13 hints: - title: "本检查点考察什么" - body: "starter 通过你监督的改动让九个测试全绿——`python verify.py progress` 只在 starter 套件变绿后显示本检查点的码。" + body: "starter 通过你监督的改动让九个测试全绿——`python verify.py` 只在 starter 套件变绿后显示本检查点的码。" - title: "审批就是边界" body: "应用会在跑命令前询问、在每个文件 diff 落地前展示。读命令、读 diff——闭眼批准正是「有界工作」变成「整文件重写」的方式。" - title: "完成的标志" @@ -62,7 +62,7 @@ diff,再接受下一处改动。 ## 检查点 -运行 `python verify.py progress`。本检查点的码只在 starter 套件全绿 +运行 `python verify.py`。本检查点的码只在 starter 套件全绿 时出现。不看材料能回答即算通过: 1. 这次有界改动走了几轮,是什么让每轮保持有界? diff --git a/courses/hands-on-with-openai-codex/lessons/L04.md b/courses/hands-on-with-openai-codex/lessons/L04.md index a6d2c12..32ebdd7 100644 --- a/courses/hands-on-with-openai-codex/lessons/L04.md +++ b/courses/hands-on-with-openai-codex/lessons/L04.md @@ -9,7 +9,7 @@ status: reviewed reviewed_on: 2026-09-13 hints: - title: "What this checkpoint tests" - body: "That starter AND solution both pass — `verify.py progress` prints this code only when both suites are green. The gate is the pair, not a single run." + body: "That starter AND solution both pass — `verify.py` prints this code only when both suites are green. The gate is the pair, not a single run." - title: "Review the delta, not the vibes" body: "The app keeps the whole thread's diff reviewable. Read it as a reviewer: every hunk maps to a contract line, nothing extra moved, no test weakened." - title: "When you are done" @@ -68,7 +68,7 @@ Keep it honest — "not verified" is a list of facts, not a confession. ## Checkpoint -Run `python verify.py progress`. This checkpoint's code prints only when +Run `python verify.py`. This checkpoint's code prints only when both suites pass. You pass the lesson when you can answer: 1. What did your by-hand recompute of a group total show? diff --git a/courses/hands-on-with-openai-codex/lessons/L04_cn.md b/courses/hands-on-with-openai-codex/lessons/L04_cn.md index cb38e48..82aa536 100644 --- a/courses/hands-on-with-openai-codex/lessons/L04_cn.md +++ b/courses/hands-on-with-openai-codex/lessons/L04_cn.md @@ -9,7 +9,7 @@ status: reviewed reviewed_on: 2026-09-13 hints: - title: "本检查点考察什么" - body: "starter 和 solution 双双通过——`verify.py progress` 只在两个套件都绿时打印本检查点的码。门槛是一对结果,不是一次运行。" + body: "starter 和 solution 双双通过——`verify.py` 只在两个套件都绿时打印本检查点的码。门槛是一对结果,不是一次运行。" - title: "审增量,不凭感觉" body: "应用把整个线程的 diff 保持为可审查状态。按评审者的方式读:每个 hunk 对上一条契约行、没有多余挪动、没有测试被放松。" - title: "完成的标志" @@ -62,7 +62,7 @@ python verify.py solution # 评审过的参考实现 ## 检查点 -运行 `python verify.py progress`。本检查点的码只在两个套件都过时 +运行 `python verify.py`。本检查点的码只在两个套件都过时 打印。不看材料能回答即算通过: 1. 你手工重算的分组总计结果如何? diff --git a/courses/hands-on-with-openai-codex/verify.py b/courses/hands-on-with-openai-codex/verify.py index a42d4e3..e13bcec 100644 --- a/courses/hands-on-with-openai-codex/verify.py +++ b/courses/hands-on-with-openai-codex/verify.py @@ -68,6 +68,16 @@ def _claim_code(checkpoint_id): ).digest() return base64.b32encode(digest).decode("ascii")[:8] + +def _display_title(title): + # FP-820: shared-core checkpoint titles are stored "Chinese / English"; + # the default command prints English first, Chinese after (FP-709 debt). + if " / " in title: + left, _, right = title.partition(" / ") + if any("\u4e00" <= character <= "\u9fff" for character in left): + return right + " / " + left + return title + def _run_suite(implementation): environment = os.environ.copy() environment["PYTHONPATH"] = str(ROOT / implementation) @@ -76,7 +86,7 @@ def _run_suite(implementation): env=environment, check=False, capture_output=True, text=True, ) -def run_progress(as_json, receipt_out=None): +def run_progress(as_json, receipt_out=None, default=False): import time t0 = time.monotonic() starter = _run_suite("starter") @@ -122,6 +132,33 @@ def run_progress(as_json, receipt_out=None): COURSE_ID, checkpoint["id"], passed=passed, tests=tests, duration_ms=ms, impl_dir=ROOT / "starter", solution_hash=solution_hash, secret=secret)) + if default and not as_json: + # FP-820: bare ``python verify.py`` — check the learner's own + # implementation, print per-checkpoint status and earned claim codes, + # English first then Chinese (FP-709 debt). ``progress`` keeps its + # exact published output; this block is the only new surface. + print("Course " + COURSE_ID) + print("Suites: starter " + ("passed" if starter_ok else "not passed") + + " / solution " + ("passed" if solution_ok else "not passed")) + for row in rows: + state = row["status"] + (" (self-attested)" if row["kind"] == "attested" else "") + code = row["claim_code"] if row["claim_code"] else "\u2014" + print(" " + row["id"] + " " + _display_title(row["title"]) + " [" + state + "] " + code) + open_gates = [row["id"] for row in rows + if row["kind"] == "objective" and row["status"] != "passed"] + if open_gates: + print("Next: keep implementing starter/ until " + ", ".join(open_gates) + + " show [passed]; then re-run: python verify.py") + else: + print("All objective checkpoints passed — the codes above are ready" + " to submit (batch POST /api/claims, see SKILL.md §5).") + print("\u4e2d\u6587\uff1a\u68c0\u67e5\u70b9\u72b6\u6001\u4e0e\u8ba4\u9886\u7801\u89c1\u4e0a\uff1b\u5e26 [passed]/[attest] \u7684\u884c\u6709\u8ba4\u9886\u7801\uff0c[open] \u7684\u884c\u7ee7\u7eed\u5728 starter/ \u91cc\u5b9e\u73b0\u540e\u518d\u8dd1\u3002") + print("Claim codes are self-reported evidence, recorded at flypython.com; never a certificate.") + print("\u8ba4\u9886\u7801\u662f\u81ea\u6211\u62a5\u544a\u7684\u8bc1\u636e\uff0c\u8bb0\u5f55\u5728 flypython.com\uff1b\u4e0d\u662f\u8bc1\u4e66\u3002") + if secret: + print(f"Signed run receipts prepared for {len(receipts)} gated checkpoint(s);" + " submit each with its claim to mark it as a local-run receipt.") + return 1 if open_gates else 0 if as_json: document = {"course": COURSE_ID, "starter_suite_passed": starter_ok, @@ -152,14 +189,19 @@ def run_progress(as_json, receipt_out=None): def main() -> int: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("implementation", choices=("progress", "starter", "solution")) + parser.add_argument("implementation", choices=("progress", "starter", "solution"), + nargs="?", default=None) parser.add_argument("--json", action="store_true") parser.add_argument("--expect-failure", action="store_true") parser.add_argument("--receipt-out", metavar="PATH", help="write signed run receipts JSON (requires FLYPYTHON_CLAIM_SECRET)") args = parser.parse_args() - if args.implementation == "progress": - return run_progress(args.json, args.receipt_out) + if args.implementation in (None, "progress"): + # FP-820: the bare command is the learner's default — same engine as + # ``progress``, English-first output, exit 1 while gates stay open. + # The four explicit usages remain for maintainers. + return run_progress(args.json, args.receipt_out, + default=args.implementation is None) command = [sys.executable, "-m", "unittest", "discover", "-s", str(ROOT / "tests")] environment = os.environ.copy() diff --git a/courses/hands-on-with-zcode/COURSE.md b/courses/hands-on-with-zcode/COURSE.md index b36b7af..543765a 100644 --- a/courses/hands-on-with-zcode/COURSE.md +++ b/courses/hands-on-with-zcode/COURSE.md @@ -4,7 +4,7 @@ type: course title: Hands-on with ZCode summary: The verified Python workflow — task contract, bounded change, objective verify.py evidence — taught hands-on in the ZCode desktop workspace, where Goal Mode decomposes the work and the Git state shows every change. lang: en-US -content_version: 1 +content_version: 2 status: reviewed reviewed_on: 2026-09-13 badge: @@ -17,8 +17,9 @@ course_id: course-zcode # Hands-on with ZCode -> TL;DR: download this folder, open it as a workspace in the ZCode desktop -> app, and say **"start lesson 1"**. You finish with a working report tool, +> TL;DR: set up the ZCode desktop app, install the FlyPython Skill, and let +> the agent fetch this course — lesson 1 walks you through all three steps +> and you download nothing. Say **"start lesson 1"**. You finish with a working report tool, > a reproducible pass/fail command, and the ZCode workflow: Goal Mode task > decomposition, file references, and the workspace Git state as your review > surface. The exercise core is the same one the other agent-tool courses @@ -67,12 +68,12 @@ If you are the ZCode agent teaching this course, follow these rules: - **When to stop:** a lesson is done when its checkpoint command runs and the learner can say, in their own words, what failed and why. If the learner cannot, re-teach from the failing test, not from the solution. -- **How to use `verify.py`:** run it in the workspace terminal or let the - agent run it as a task step — `python verify.py starter - --expect-failure` must reproduce the listed failures; `python verify.py - solution` must pass. "Done" means the starter implementation passes the - same suite after the learner's bounded change. -- **Honesty rules:** say what you did not verify. Do not claim the code is +- **How to use `verify.py`:** the learner's command is plain + `python verify.py` — it runs the suites, prints per-checkpoint status + and earned claim codes, and exits non-zero while the gated checkpoints + (l03, l04) are still open. `starter --expect-failure` and `solution` + remain maintainer checks. "Done" means the default command shows every + gate `[passed]` after the learner's bounded change.- **Honesty rules:** say what you did not verify. Do not claim the code is production-ready. Do not promise learning or career outcomes. ## What this course does NOT cover @@ -85,7 +86,7 @@ companion repository has guides and playbooks — link, don't improvise. - Badge: **Reproduce with ZCode in the loop Badge** (badge id `course-zcode`) — earned by claiming all five checkpoints. - Challenges: L01–L05 checkpoints, 10 points each; +50 course-badge bonus when all five are claimed on flypython.com. -- Evidence: `python verify.py progress` — L03 (bounded change) and L04 (verify & review) are objectively gated by the suite; L01/L02/L05 are learner-attested. +- Evidence: `python verify.py` — L03 (bounded change) and L04 (verify & review) are objectively gated by the suite; L01/L02/L05 are learner-attested. - Submission: each passed checkpoint prints a deterministic claim code; record it on flypython.com against your account. Self-reported evidence, never a certificate. ## Folder map diff --git a/courses/hands-on-with-zcode/COURSE_cn.md b/courses/hands-on-with-zcode/COURSE_cn.md index b36e8d6..c279e61 100644 --- a/courses/hands-on-with-zcode/COURSE_cn.md +++ b/courses/hands-on-with-zcode/COURSE_cn.md @@ -4,7 +4,7 @@ type: course title: "上手 ZCode" summary: "经过验证的 Python 工作流——任务契约、有界改动、客观 verify.py 证据——在 ZCode 桌面工作区里手把手教:Goal 模式拆解任务,Git 状态展示每一处改动。" lang: zh-CN -content_version: 1 +content_version: 2 status: reviewed reviewed_on: 2026-09-13 badge: @@ -17,8 +17,8 @@ course_id: course-zcode # 上手 ZCode -> 一句话:下载本文件夹,在 ZCode 桌面应用里作为工作区打开,对它说 -> **「开始第 1 课」**。完成时你会得到一个能跑的报表工具、一条可复现的 +> 一句话:装好 ZCode 桌面应用、装上 FlyPython Skill、让 Agent 取回本课文件 +> (第 1 课就是这三步,你什么都不用下载),对它说**「开始第 1 课」**。完成时你会得到一个能跑的报表工具、一条可复现的 > 通过/失败命令,以及 ZCode 工作流:Goal 模式任务拆解、文件引用、 > 以及作为你审查面的工作区 Git 状态。练习核心与其他 Agent 工具课程 > 完全相同——变的只是你驱动的工具。 @@ -59,11 +59,10 @@ course_id: course-zcode - **何时停下:** 一节课的检查点命令跑通、且学习者能用自己的话说清 哪里坏了、为什么,这节课才算完。学习者说不出就从失败测试重新教, 不要从 solution 教。 -- **如何使用 `verify.py`:** 在工作区终端里跑,或让 Agent 作为任务 - 步骤跑——`python verify.py starter --expect-failure` 必须复现 - 列出的失败;`python verify.py solution` 必须通过。「完成」指 - 学习者做完有界改动后,starter 实现通过同一套测试。 -- **诚实规则:** 说清你没验证什么。不要声称代码达到生产可用。不要 +- **如何使用 `verify.py`:** 学员命令就是 `python verify.py`——它跑套件、 + 打印逐检查点状态与已获认领码,在门控检查点(l03、l04)未全通过时以 + 非零退出。`starter --expect-failure` 与 `solution` 留作维护者检查。 + 「完成」指学习者做完有界改动后,默认命令把所有门显示为 `[passed]`。- **诚实规则:** 说清你没验证什么。不要声称代码达到生产可用。不要 承诺学习或职业结果。 ## 本课程不覆盖什么 @@ -76,7 +75,7 @@ ZCode 的安装与套餐、Remote Control 的配对设置(仅提及)、模 - 徽章:**ZCode 协同复现徽章**(badge id `course-zcode`)——认领全部五个检查点获得。 - 挑战:L01–L05 检查点,各 10 分;在 flypython.com 上集齐五个再加 50 分课程徽章奖励。 -- 证据:`python verify.py progress`——L03(有界改动)与 L04(验证与评审)由套件客观把关;L01/L02/L05 为学习者自我报告。 +- 证据:`python verify.py`——L03(有界改动)与 L04(验证与评审)由套件客观把关;L01/L02/L05 为学习者自我报告。 - 提交:每个通过的检查点打印一个确定性的认领码;在 flypython.com 上记入你的账号。这是自我报告的证据,从来不是证书。 ## 文件夹地图 diff --git a/courses/hands-on-with-zcode/REVIEW.md b/courses/hands-on-with-zcode/REVIEW.md index 439baa0..e171f0d 100644 --- a/courses/hands-on-with-zcode/REVIEW.md +++ b/courses/hands-on-with-zcode/REVIEW.md @@ -19,6 +19,20 @@ used; every ZCode behavior described in the lessons is sourced from official documentation, not observed. +## 2026-09-13 — 0.0.8 rework: lesson 1 is now tool → Skill → agent fetch (FP-822) + +- L01 (EN+ZH) rewritten around the fixed order: get the tool running → + install the FlyPython Skill (network access included) → let the agent + fetch this course's files via the files endpoint. All "download this + folder" wording removed from `COURSE.md` and lessons. +- The learner's check command across lessons is now the bare + `python verify.py` (FP-820): per-checkpoint status + claim codes, + non-zero exit while gated checkpoints are open. `solution` is no longer + presented as a completion standard. +- Tool-specific Skill install and network steps are authored from the + same official documentation as the course; **not yet exercised inside + the tool** — the pending live run-through below still stands. + ## Pending before this course can be called taught-in-tool - One full agent-taught run-through inside ZCode ("start lesson 1" through diff --git a/courses/hands-on-with-zcode/lessons/L01.md b/courses/hands-on-with-zcode/lessons/L01.md index 550cd8f..8245998 100644 --- a/courses/hands-on-with-zcode/lessons/L01.md +++ b/courses/hands-on-with-zcode/lessons/L01.md @@ -1,29 +1,65 @@ --- id: course-zcode-l01 type: course -title: "Lesson 1: Reproduce the failure inside the workspace" -summary: "Open the folder as a ZCode workspace, give the agent its first task, and make the failure objective with verify.py — inside the app, not beside it." +title: "Lesson 1: Set up ZCode, install the FlyPython Skill, fetch the course" +summary: "Get ZCode running, install the FlyPython Skill with network access approved, and let the agent fetch this course's files — you download nothing. Then make the starter's failure objective." lang: en-US -content_version: 1 +content_version: 2 status: reviewed reviewed_on: 2026-09-13 hints: - title: "What this checkpoint tests" - body: "That you can reproduce the failure before fixing it: `python verify.py starter --expect-failure` must print the named failing tests. Whether you ran it in the workspace terminal or the agent ran it as a task step, the command is the evidence." + body: "That you can reproduce the failure before fixing it: `python verify.py` must show l03 and l04 as `[open]` (`starter --expect-failure` prints the named tests). Whether you ran it in the workspace terminal or the agent ran it as a task step, the command is the evidence." - title: "A workspace, not a chat" body: "ZCode is a desktop-native workspace: files, tasks, planning, execution, verification, and Git state in one place. Use file references to point the agent at exact files instead of describing them." - title: "When you are done" - body: "You can say which nine behaviors the suite proves and why the starter cannot fake them. `python verify.py progress` then shows this checkpoint's claim code." + body: "You can say which nine behaviors the suite proves and why the starter cannot fake them. `python verify.py` then shows this checkpoint's claim code." --- -# Lesson 1: Reproduce the failure inside the workspace +# Lesson 1: Set up ZCode, install the FlyPython Skill, fetch the course ## Objective -By the end of this lesson you have the course folder open as a ZCode -workspace, you have run the course's objective verification command, and -you can explain what "the starter fails" means in test names. No code is -changed in this lesson. +By the end of this lesson ZCode is running, the FlyPython Skill is +installed in it (with network access approved), and the agent has fetched +this course's files into your working folder — you downloaded nothing by +hand. With the folder in place you run the course's objective check and +explain what "the starter fails" means in test names. No code is changed +in this lesson. + +## Step 1 — Get ZCode running + +Install the ZCode desktop app and open an +empty folder as a workspace. + +## Step 2 — Install the FlyPython Skill (network included) + +ZCode reads `AGENTS.md` instruction files from the workspace root: + +```bash +curl -s https://flypython.com/skills/flypython/SKILL.md >> AGENTS.md +``` + +Approve the agent's network tool use when it asks; if blocked, run the +curl yourself and paste the file. + +The Skill is one file, the same in every tool: it tells the agent how to +authorize you, fetch course files, verify, and submit claim codes. + +## Step 3 — Let the agent fetch this course (you download nothing) + +Start a new session/chat/thread in the working folder and paste this one +sentence: + +> Read https://flypython.com/skills/flypython/SKILL.md and start the FlyPython course `hands-on-with-zcode`. + +The agent gives you an authorization link with a short code. Open it, log +in, make sure the code matches what the agent showed you, and click +Allow — then tell the agent "done". It takes a token once and writes this +course's files to `courses/hands-on-with-zcode/`. + +**Prerequisite:** these courses need a coding agent that can run commands +*and* reach the network. A chat-only web AI cannot do them. ## Why this lesson exists @@ -45,18 +81,18 @@ behavior. Now open `TASK.md` and keep it next to you. ## The lesson -Open the course folder as a ZCode workspace, then give the agent its first -task — referencing the files directly: +The course files are in `courses/hands-on-with-zcode/` now — no download happened on +your side. Ask the agent to survey them without changing anything: **"Read COURSE.md — it is the teaching contract for this folder. Then read TASK.md and starter/report_tool.py. Do not change any file. Tell me which behaviors the tests expect that the starter does not have."** -While it reads, run the objective verification — in the workspace -terminal, or let the agent run it as a task step: +While it reads, run the learner's command — your single check for the +rest of the course: ```bash -python verify.py starter --expect-failure +python verify.py ``` You should see the starter reproduce four real-world failure classes, @@ -71,15 +107,16 @@ mapped to test names: | `test_write_report_creates_missing_parent_directories` | the tool cannot write into a fresh output directory | | `test_run_scenario_writes_report_file`, `test_main_prints_summary_and_returns_zero` | end-to-end consequences of the above | -Then run the reviewed implementation the same way: +If you want the missing behaviors as exact failing test names, the +maintainer command prints them: ```bash -python verify.py solution +python verify.py starter --expect-failure ``` -Both commands exit 0 — but they mean opposite things. The first proves the -starter is *correctly unfinished*; the second proves the target is -reachable. That pair is your completion evidence for the whole course. +(The reference `solution/` exists so maintainers can prove the target is +reachable. You never need to run it, and it is never the completion +standard — your implementation in `starter/` is.) ## Exercise @@ -91,16 +128,19 @@ the agent to confirm, not to fix. ## Checkpoint -Run `python verify.py starter --expect-failure` and -`python verify.py solution`. You pass this lesson when you can answer, -without looking: +Run `python verify.py` in the course folder. You pass this lesson when you +can answer, without looking: 1. Which four behaviors separate starter from solution? -2. Why is "expected failure" a success condition, not a contradiction? +2. Why does `python verify.py` exit non-zero on purpose — what state is + it reporting, and why is that a success condition rather than an error? 3. What does the row you found in the exercise do to the starter — crash, isolate, or silently pass? +This checkpoint's claim code is already in the default command's output — +submit it only once you can answer the questions. + ## Expected evidence -The transcript of both commands, and your three answers. Keep them; +The transcript of the default command, and your three answers. Keep them; Lesson 4 will ask for them again. diff --git a/courses/hands-on-with-zcode/lessons/L01_cn.md b/courses/hands-on-with-zcode/lessons/L01_cn.md index 05b1d69..6933c26 100644 --- a/courses/hands-on-with-zcode/lessons/L01_cn.md +++ b/courses/hands-on-with-zcode/lessons/L01_cn.md @@ -1,30 +1,63 @@ --- id: course-zcode-l01 type: course -title: "第 1 课:在工作区里复现故障" -summary: "把文件夹作为 ZCode 工作区打开,给 Agent 第一个任务,用 verify.py 让失败变得客观——在应用内完成,而不是在应用旁边。" +title: "第 1 课:装好 ZCode、装上 FlyPython Skill、让 Agent 取回课程" +summary: "先把 ZCode 跑起来,装好 FlyPython Skill 并打开联网权限,让 Agent 取回本课文件——你不下载任何东西,然后让 starter 的失败变得客观。" lang: zh-CN -content_version: 1 +content_version: 2 status: reviewed reviewed_on: 2026-09-13 hints: - title: "本检查点考察什么" - body: "先复现失败再动手修:`python verify.py starter --expect-failure` 必须打印出点名的失败测试。你在工作区终端跑也好、Agent 作为任务步骤跑也好,命令就是证据。" + body: "先复现失败再动手修:`python verify.py` 必须把 l03 和 l04 显示为 `[open]`(`starter --expect-failure` 会打印点名测试)。你在工作区终端跑也好、Agent 作为任务步骤跑也好,命令就是证据。" - title: "工作区,不是聊天框" body: "ZCode 是桌面原生工作区:文件、任务、规划、执行、验证和 Git 状态同在一处。用文件引用把 Agent 精确指到文件,不要用文字描述。" - title: "完成的标志" - body: "你能说出套件证明了哪九种行为、为什么 starter 伪造不出来。`python verify.py progress` 随即显示本检查点的认领码。" + body: "你能说出套件证明了哪九种行为、为什么 starter 伪造不出来。`python verify.py` 随即显示本检查点的认领码。" --- -# 第 1 课:在工作区里复现故障 +# 第 1 课:装好 ZCode、装上 FlyPython Skill、让 Agent 取回课程 ## 目标 -本课结束时,你把课程文件夹作为 ZCode 工作区打开,运行过课程的 -客观验证命令,并能解释「starter 失败」在测试名里的含义。本课 -不改任何代码。 +本课结束时,ZCode 已经跑起来、FlyPython Skill 已装好(联网权限已 +打开),Agent 已把本课文件取回你的工作目录——你没有手动下载任何 +东西。文件夹就位后,你运行课程的客观检查命令,并解释「starter +失败」在测试名里的含义。本课不改任何代码。 -## 为什么有这节课 +## 第 1 步——把 ZCode 跑起来 + +安装 ZCode 桌面应用,把一个空文件夹 +作为工作区打开。 + +## 第 2 步——装 FlyPython Skill(含联网权限) + +ZCode 会读取工作区根目录的 `AGENTS.md` 指令文件: + +```bash +curl -s https://flypython.com/skills/flypython/SKILL.md >> AGENTS.md +``` + +Agent 请求联网时批准;如果被禁,就自己跑 curl,把文件粘贴 +过去。 + +Skill 只是一个文件,在每个工具里都一样:它告诉 Agent 如何为你 +授权、取课程文件、验证、提交认领码。 + +## 第 3 步——让 Agent 取回本课文件(你不下载) + +在工作目录里新开一个会话/对话/线程,粘贴这一句话: + +> Read https://flypython.com/skills/flypython/SKILL.md and start the FlyPython course `hands-on-with-zcode`. + +Agent 会给你一个授权链接和一组短码。打开链接、登录、核对码与 +Agent 显示的一致后点「允许」,然后对 Agent 说「好了」。它取一次 +token,把本课文件写到 `courses/hands-on-with-zcode/`。 + +**前提**:这些课程需要一个能执行命令**并且**能联网的编码 Agent。 +只能聊天的网页 AI 做不了。 + +## 为什么有这一课 AI 写的 Python 大多是同一种死法:演示输入能跑,真实输入一到,脚本 在第 3 行崩掉——或者更糟,悄悄算出一个错的数。修复不从更好的 @@ -41,18 +74,17 @@ AI 写的 Python 大多是同一种死法:演示输入能跑,真实输入一 ## 本课内容 -把课程文件夹作为 ZCode 工作区打开,然后给 Agent 第一个任务—— -直接引用文件: +本课文件现在已经位于 `courses/hands-on-with-zcode/`——你这边没有发生任何下载。 +让 Agent 先只读不动手: **“读 COURSE.md——它是本文件夹的教学契约。再读 TASK.md 和 starter/report_tool.py。不要改任何文件。告诉我测试期望哪些行为、 starter 缺了哪些。”** -趁它读的时候,跑客观验证——在工作区终端里,或让 Agent 作为任务 -步骤跑: +趁它读的时候,运行学员命令——整个课程你只需要这一条检查命令: ```bash -python verify.py starter --expect-failure +python verify.py ``` 你会看到 starter 复现出四类真实世界故障,对应到测试名: @@ -66,14 +98,14 @@ python verify.py starter --expect-failure | `test_write_report_creates_missing_parent_directories` | 工具无法写进新建的输出目录 | | `test_run_scenario_writes_report_file`、`test_main_prints_summary_and_returns_zero` | 上述问题的端到端后果 | -然后用同样的方式跑评审过的实现: +想看缺失行为对应的失败测试名,维护者命令会打印它们: ```bash -python verify.py solution +python verify.py starter --expect-failure ``` -两条命令都以 0 退出——但含义相反。第一条证明 starter *确实处于 -正确的未完成态*;第二条证明目标可达。这一对命令是整门课的完成 +(参考答案 `solution/` 是给维护者证明目标可达用的。你不需要运行 +它,它也从来不是完成标准——你的实现(`starter/`)才是。)这一对命令是整门课的完成 证据。 ## 练习 @@ -85,15 +117,19 @@ Agent 确认,而不是让它修。 ## 检查点 -运行 `python verify.py starter --expect-failure` 和 -`python verify.py solution`。不看材料能回答下面三个问题即算通过: +在课程文件夹里运行 `python verify.py`。不看材料能回答下面三个 +问题即算通过: 1. 哪四个行为把 starter 和 solution 区分开? -2. 为什么「预期失败」是成功条件而不是自相矛盾? +2. `python verify.py` 为什么故意以非零退出——它在报告什么状态? + 为什么这是成功条件而不是报错? 3. 你在练习里找到的那一行会让 starter 崩溃、被隔离,还是悄悄 通过? +本检查点的认领码已经在默认命令的输出里——能回答这些问题之后再 +提交。 + ## 预期证据 -两条命令的完整输出记录,加你的三个答案。保留好——第 4 课还会 +默认命令的完整输出记录,加你的三个答案。保留好——第 4 课还会 用到。 diff --git a/courses/hands-on-with-zcode/lessons/L02.md b/courses/hands-on-with-zcode/lessons/L02.md index 98b4a49..3c87513 100644 --- a/courses/hands-on-with-zcode/lessons/L02.md +++ b/courses/hands-on-with-zcode/lessons/L02.md @@ -72,7 +72,7 @@ same boundaries this lesson used. ## Checkpoint -Run `python verify.py progress` — this checkpoint's claim code prints +Run `python verify.py` — this checkpoint's claim code prints when you can answer: 1. Which `TASK.md` line does diff --git a/courses/hands-on-with-zcode/lessons/L02_cn.md b/courses/hands-on-with-zcode/lessons/L02_cn.md index 03f5280..4011683 100644 --- a/courses/hands-on-with-zcode/lessons/L02_cn.md +++ b/courses/hands-on-with-zcode/lessons/L02_cn.md @@ -65,7 +65,7 @@ starter/report_tool.py;tests/、solution/、scenario/ 只读;只用 ## 检查点 -运行 `python verify.py progress`——能回答下面三题时,本检查点的 +运行 `python verify.py`——能回答下面三题时,本检查点的 认领码就会显示: 1. 用你自己的话说,`test_invalid_records_are_isolated_with_reasons` diff --git a/courses/hands-on-with-zcode/lessons/L03.md b/courses/hands-on-with-zcode/lessons/L03.md index 945f597..4fd7543 100644 --- a/courses/hands-on-with-zcode/lessons/L03.md +++ b/courses/hands-on-with-zcode/lessons/L03.md @@ -9,7 +9,7 @@ status: reviewed reviewed_on: 2026-09-13 hints: - title: "What this checkpoint tests" - body: "That the starter passes all nine tests through changes you supervised — `python verify.py progress` shows this code only once the starter suite is green." + body: "That the starter passes all nine tests through changes you supervised — `python verify.py` shows this code only once the starter suite is green." - title: "Git state is the boundary" body: "The workspace tracks Git state alongside execution — after each task, the working tree shows exactly what changed. A task that touched the wrong file is visible immediately; recover the goal rather than stacking fixes." - title: "When you are done" @@ -67,7 +67,7 @@ that hunk before the next task runs. ## Checkpoint -Run `python verify.py progress`. This checkpoint's code appears only when +Run `python verify.py`. This checkpoint's code appears only when the starter suite is green. You pass the lesson when you can answer: 1. How many tasks did the bounded change take, and what made each task diff --git a/courses/hands-on-with-zcode/lessons/L03_cn.md b/courses/hands-on-with-zcode/lessons/L03_cn.md index 39cc0b1..f0fa07f 100644 --- a/courses/hands-on-with-zcode/lessons/L03_cn.md +++ b/courses/hands-on-with-zcode/lessons/L03_cn.md @@ -9,7 +9,7 @@ status: reviewed reviewed_on: 2026-09-13 hints: - title: "本检查点考察什么" - body: "starter 通过你监督的改动让九个测试全绿——`python verify.py progress` 只在 starter 套件变绿后显示本检查点的码。" + body: "starter 通过你监督的改动让九个测试全绿——`python verify.py` 只在 starter 套件变绿后显示本检查点的码。" - title: "Git 状态就是边界" body: "工作区把 Git 状态放在执行旁边——每个任务完成后,工作区立刻显示改了什么。动错文件的任务一眼可见;用目标恢复倒回去,而不是叠补丁。" - title: "完成的标志" @@ -60,7 +60,7 @@ ZCode 把 Git 状态放在执行旁边——每个任务之后你都能直接看 ## 检查点 -运行 `python verify.py progress`。本检查点的码只在 starter 套件 +运行 `python verify.py`。本检查点的码只在 starter 套件 全绿时出现。不看材料能回答即算通过: 1. 这次有界改动走了几个任务,是什么让每个任务保持有界? diff --git a/courses/hands-on-with-zcode/lessons/L04.md b/courses/hands-on-with-zcode/lessons/L04.md index a0ad8ef..a9d5d51 100644 --- a/courses/hands-on-with-zcode/lessons/L04.md +++ b/courses/hands-on-with-zcode/lessons/L04.md @@ -9,7 +9,7 @@ status: reviewed reviewed_on: 2026-09-13 hints: - title: "What this checkpoint tests" - body: "That starter AND solution both pass — `verify.py progress` prints this code only when both suites are green. The gate is the pair, not a single run." + body: "That starter AND solution both pass — `verify.py` prints this code only when both suites are green. The gate is the pair, not a single run." - title: "Review the delta, not the vibes" body: "The workspace Git state shows the whole accumulated change in one place — even from your phone via remote control. Read it as a reviewer: every hunk maps to a contract line, nothing extra moved, no test weakened." - title: "When you are done" @@ -69,7 +69,7 @@ Keep it honest — "not verified" is a list of facts, not a confession. ## Checkpoint -Run `python verify.py progress`. This checkpoint's code prints only when +Run `python verify.py`. This checkpoint's code prints only when both suites pass. You pass the lesson when you can answer: 1. What did your by-hand recompute of a group total show? diff --git a/courses/hands-on-with-zcode/lessons/L04_cn.md b/courses/hands-on-with-zcode/lessons/L04_cn.md index 901ef3c..50b0010 100644 --- a/courses/hands-on-with-zcode/lessons/L04_cn.md +++ b/courses/hands-on-with-zcode/lessons/L04_cn.md @@ -9,7 +9,7 @@ status: reviewed reviewed_on: 2026-09-13 hints: - title: "本检查点考察什么" - body: "starter 和 solution 双双通过——`verify.py progress` 只在两个套件都绿时打印本检查点的码。门槛是一对结果,不是一次运行。" + body: "starter 和 solution 双双通过——`verify.py` 只在两个套件都绿时打印本检查点的码。门槛是一对结果,不是一次运行。" - title: "审增量,不凭感觉" body: "工作区 Git 状态把整份累积改动放在一处——通过远程控制甚至能在手机上看。按评审者的方式读:每个 hunk 对上一条契约行、没有多余挪动、没有测试被放松。" - title: "完成的标志" @@ -64,7 +64,7 @@ python verify.py solution # 评审过的参考实现 ## 检查点 -运行 `python verify.py progress`。本检查点的码只在两个套件都过 +运行 `python verify.py`。本检查点的码只在两个套件都过 时打印。不看材料能回答即算通过: 1. 你手工重算的分组总计结果如何? diff --git a/courses/hands-on-with-zcode/verify.py b/courses/hands-on-with-zcode/verify.py index 274fa48..3724123 100644 --- a/courses/hands-on-with-zcode/verify.py +++ b/courses/hands-on-with-zcode/verify.py @@ -68,6 +68,16 @@ def _claim_code(checkpoint_id): ).digest() return base64.b32encode(digest).decode("ascii")[:8] + +def _display_title(title): + # FP-820: shared-core checkpoint titles are stored "Chinese / English"; + # the default command prints English first, Chinese after (FP-709 debt). + if " / " in title: + left, _, right = title.partition(" / ") + if any("\u4e00" <= character <= "\u9fff" for character in left): + return right + " / " + left + return title + def _run_suite(implementation): environment = os.environ.copy() environment["PYTHONPATH"] = str(ROOT / implementation) @@ -76,7 +86,7 @@ def _run_suite(implementation): env=environment, check=False, capture_output=True, text=True, ) -def run_progress(as_json, receipt_out=None): +def run_progress(as_json, receipt_out=None, default=False): import time t0 = time.monotonic() starter = _run_suite("starter") @@ -122,6 +132,33 @@ def run_progress(as_json, receipt_out=None): COURSE_ID, checkpoint["id"], passed=passed, tests=tests, duration_ms=ms, impl_dir=ROOT / "starter", solution_hash=solution_hash, secret=secret)) + if default and not as_json: + # FP-820: bare ``python verify.py`` — check the learner's own + # implementation, print per-checkpoint status and earned claim codes, + # English first then Chinese (FP-709 debt). ``progress`` keeps its + # exact published output; this block is the only new surface. + print("Course " + COURSE_ID) + print("Suites: starter " + ("passed" if starter_ok else "not passed") + + " / solution " + ("passed" if solution_ok else "not passed")) + for row in rows: + state = row["status"] + (" (self-attested)" if row["kind"] == "attested" else "") + code = row["claim_code"] if row["claim_code"] else "\u2014" + print(" " + row["id"] + " " + _display_title(row["title"]) + " [" + state + "] " + code) + open_gates = [row["id"] for row in rows + if row["kind"] == "objective" and row["status"] != "passed"] + if open_gates: + print("Next: keep implementing starter/ until " + ", ".join(open_gates) + + " show [passed]; then re-run: python verify.py") + else: + print("All objective checkpoints passed — the codes above are ready" + " to submit (batch POST /api/claims, see SKILL.md §5).") + print("\u4e2d\u6587\uff1a\u68c0\u67e5\u70b9\u72b6\u6001\u4e0e\u8ba4\u9886\u7801\u89c1\u4e0a\uff1b\u5e26 [passed]/[attest] \u7684\u884c\u6709\u8ba4\u9886\u7801\uff0c[open] \u7684\u884c\u7ee7\u7eed\u5728 starter/ \u91cc\u5b9e\u73b0\u540e\u518d\u8dd1\u3002") + print("Claim codes are self-reported evidence, recorded at flypython.com; never a certificate.") + print("\u8ba4\u9886\u7801\u662f\u81ea\u6211\u62a5\u544a\u7684\u8bc1\u636e\uff0c\u8bb0\u5f55\u5728 flypython.com\uff1b\u4e0d\u662f\u8bc1\u4e66\u3002") + if secret: + print(f"Signed run receipts prepared for {len(receipts)} gated checkpoint(s);" + " submit each with its claim to mark it as a local-run receipt.") + return 1 if open_gates else 0 if as_json: document = {"course": COURSE_ID, "starter_suite_passed": starter_ok, @@ -152,14 +189,19 @@ def run_progress(as_json, receipt_out=None): def main() -> int: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("implementation", choices=("progress", "starter", "solution")) + parser.add_argument("implementation", choices=("progress", "starter", "solution"), + nargs="?", default=None) parser.add_argument("--json", action="store_true") parser.add_argument("--expect-failure", action="store_true") parser.add_argument("--receipt-out", metavar="PATH", help="write signed run receipts JSON (requires FLYPYTHON_CLAIM_SECRET)") args = parser.parse_args() - if args.implementation == "progress": - return run_progress(args.json, args.receipt_out) + if args.implementation in (None, "progress"): + # FP-820: the bare command is the learner's default — same engine as + # ``progress``, English-first output, exit 1 while gates stay open. + # The four explicit usages remain for maintainers. + return run_progress(args.json, args.receipt_out, + default=args.implementation is None) command = [sys.executable, "-m", "unittest", "discover", "-s", str(ROOT / "tests")] environment = os.environ.copy() diff --git a/courses/mcp-server-in-python/COURSE.md b/courses/mcp-server-in-python/COURSE.md index 2a0716a..5aa52b0 100644 --- a/courses/mcp-server-in-python/COURSE.md +++ b/courses/mcp-server-in-python/COURSE.md @@ -4,7 +4,7 @@ type: course title: Give your agent tools with MCP (Python) summary: Build a stateless Model Context Protocol tool server in pure Python — JSON-RPC 2.0 dispatch, schema validation, error isolation, and the 2026-07-28 input_required round-trip — and learn where a real tool belongs. lang: en-US -content_version: 2 +content_version: 3 status: reviewed reviewed_on: 2026-09-12 badge: @@ -17,8 +17,9 @@ course_id: course-mcp-tools # Give your agent tools with MCP (Python) -> TL;DR: download this folder, open it in your coding agent, and say -> **"start lesson 1"**. You finish with a working MCP tool server that +> TL;DR: install the FlyPython Skill in your coding agent and let it fetch +> this course, then say **"start lesson 1"**. (Lesson files arrive via +> the Skill — you download nothing by hand.) You finish with a working MCP tool server that > implements the stateless 2026-07-28 specification — no initialize > handshake, validated tool arguments, isolated handler failures, and the > multi-round input_required flow — proven by a failing starter and a @@ -73,7 +74,7 @@ moving existing servers to the 2026-07-28 specification. - Badge: **MCP Tool Server Badge** (badge id `course-mcp-tools`) - earned by claiming all five checkpoints. - Challenges: L01-L05 checkpoints, 10 points each; +50 course-badge bonus when all five are claimed on flypython.com. -- Evidence: `python verify.py progress` - L03 (validation & isolation) and L04 (the round-trip) are objectively gated by the suite; L01/L02/L05 are learner-attested. +- Evidence: `python verify.py` - L03 (validation & isolation) and L04 (the round-trip) are objectively gated by the suite; L01/L02/L05 are learner-attested. - Submission: each passed checkpoint prints a deterministic claim code; record it on flypython.com against your account. Self-reported evidence, never a certificate. ## Folder map diff --git a/courses/mcp-server-in-python/COURSE_cn.md b/courses/mcp-server-in-python/COURSE_cn.md index ead6cfe..e2837fd 100644 --- a/courses/mcp-server-in-python/COURSE_cn.md +++ b/courses/mcp-server-in-python/COURSE_cn.md @@ -4,7 +4,7 @@ type: course title: 用 MCP 给你的 Agent 装上工具(Python) summary: 用纯 Python 构建一个无状态的 Model Context Protocol 工具服务——JSON-RPC 2.0 分发、schema 校验、错误隔离与 2026-07-28 的 input_required 多轮交互——并理解真正的工具该放在哪里。 lang: zh-CN -content_version: 2 +content_version: 3 status: reviewed reviewed_on: 2026-09-12 badge: @@ -17,7 +17,8 @@ course_id: course-mcp-tools # 用 MCP 给你的 Agent 装上工具(Python) -> 摘要:下载本文件夹,在你的编码 Agent 中打开,说一句 **“开始第 1 课”**。 +> 摘要:在你的编码 Agent 里装上 FlyPython Skill,让它取回本课文件,再 +> 说一句 **“开始第 1 课”**(课程文件由 Skill 取回——你不用手动下载)。 > 课程结束时你拥有一个实现无状态 2026-07-28 规范的 MCP 工具服务—— > 无 initialize 握手、参数校验、处理异常隔离、input_required 多轮交互 > ——由一个失败的 starter 和一个通过的 solution 共同证明。仅用标准库; @@ -65,7 +66,7 @@ course_id: course-mcp-tools - 徽章:**MCP 工具服务徽章**(徽章 id `course-mcp-tools`)——认领全部五个检查点后获得。 - 挑战:L01–L05 检查点各 10 分;五项全部在 flypython.com 认领后另加 50 分课程徽章奖励。 -- 证据:`python verify.py progress` —— L03(校验与错误隔离)与 L04(多轮交互)由测试套件客观判定;L01/L02/L05 为学习者自报。 +- 证据:`python verify.py` —— L03(校验与错误隔离)与 L04(多轮交互)由测试套件客观判定;L01/L02/L05 为学习者自报。 - 提交:每个通过的检查点会打印确定性认领码,在 flypython.com 上记入你的账号。这是自我报告的证据,绝不是证书。 ## 文件夹结构 diff --git a/courses/mcp-server-in-python/lessons/L01.md b/courses/mcp-server-in-python/lessons/L01.md index c69ac6a..ded4f7a 100644 --- a/courses/mcp-server-in-python/lessons/L01.md +++ b/courses/mcp-server-in-python/lessons/L01.md @@ -33,7 +33,12 @@ Read TASK.md, then run the verifier both ways. The 2026-07-28 specification remo ## Checkpoint -The lesson's commands run, and you can explain — in your own words — what failed and why. +The lesson's commands run, and you can answer these in your own words +(the agent asks; you answer — that is the gate): + +1. Which command did this lesson run, and what did it decide? +2. What failed at the start, and why — in your own words? +3. Before trusting a similar change next time, what would you check first? ## Expected evidence diff --git a/courses/mcp-server-in-python/lessons/L01_cn.md b/courses/mcp-server-in-python/lessons/L01_cn.md index 56f2d32..4a01a2d 100644 --- a/courses/mcp-server-in-python/lessons/L01_cn.md +++ b/courses/mcp-server-in-python/lessons/L01_cn.md @@ -33,7 +33,12 @@ hints: ## 检查点 -本课的命令运行通过,并且你能用自己的话说清什么失败了、为什么。 +本课的命令运行通过,并且你能用自己的话回答下面的问题(由 Agent +提问、你作答——这就是关口): + +1. 本课跑了哪条命令,它判定了什么? +2. 一开始什么失败了、为什么——用你自己的话说? +3. 下次再信任类似的改动之前,你会先检查什么? ## 预期证据 diff --git a/courses/mcp-server-in-python/lessons/L02.md b/courses/mcp-server-in-python/lessons/L02.md index 2ba6a4c..f2f606b 100644 --- a/courses/mcp-server-in-python/lessons/L02.md +++ b/courses/mcp-server-in-python/lessons/L02.md @@ -33,7 +33,12 @@ The dispatch rules are small and testable: check jsonrpc and method, preserve id ## Checkpoint -The lesson's commands run, and you can explain — in your own words — what failed and why. +The lesson's commands run, and you can answer these in your own words +(the agent asks; you answer — that is the gate): + +1. Which command did this lesson run, and what did it decide? +2. What failed at the start, and why — in your own words? +3. Before trusting a similar change next time, what would you check first? ## Expected evidence diff --git a/courses/mcp-server-in-python/lessons/L02_cn.md b/courses/mcp-server-in-python/lessons/L02_cn.md index 1641046..1fd7b57 100644 --- a/courses/mcp-server-in-python/lessons/L02_cn.md +++ b/courses/mcp-server-in-python/lessons/L02_cn.md @@ -33,7 +33,12 @@ hints: ## 检查点 -本课的命令运行通过,并且你能用自己的话说清什么失败了、为什么。 +本课的命令运行通过,并且你能用自己的话回答下面的问题(由 Agent +提问、你作答——这就是关口): + +1. 本课跑了哪条命令,它判定了什么? +2. 一开始什么失败了、为什么——用你自己的话说? +3. 下次再信任类似的改动之前,你会先检查什么? ## 预期证据 diff --git a/courses/mcp-server-in-python/lessons/L03.md b/courses/mcp-server-in-python/lessons/L03.md index 968f767..667667e 100644 --- a/courses/mcp-server-in-python/lessons/L03.md +++ b/courses/mcp-server-in-python/lessons/L03.md @@ -33,7 +33,12 @@ Two boundaries make a tool server safe: validate required arguments against the ## Checkpoint -The lesson's commands run, and you can explain — in your own words — what failed and why. +The lesson's commands run, and you can answer these in your own words +(the agent asks; you answer — that is the gate): + +1. Which command did this lesson run, and what did it decide? +2. What failed at the start, and why — in your own words? +3. Before trusting a similar change next time, what would you check first? ## Expected evidence diff --git a/courses/mcp-server-in-python/lessons/L03_cn.md b/courses/mcp-server-in-python/lessons/L03_cn.md index ca5619a..67624ef 100644 --- a/courses/mcp-server-in-python/lessons/L03_cn.md +++ b/courses/mcp-server-in-python/lessons/L03_cn.md @@ -33,7 +33,12 @@ hints: ## 检查点 -本课的命令运行通过,并且你能用自己的话说清什么失败了、为什么。 +本课的命令运行通过,并且你能用自己的话回答下面的问题(由 Agent +提问、你作答——这就是关口): + +1. 本课跑了哪条命令,它判定了什么? +2. 一开始什么失败了、为什么——用你自己的话说? +3. 下次再信任类似的改动之前,你会先检查什么? ## 预期证据 diff --git a/courses/mcp-server-in-python/lessons/L04.md b/courses/mcp-server-in-python/lessons/L04.md index db8151c..388e9b7 100644 --- a/courses/mcp-server-in-python/lessons/L04.md +++ b/courses/mcp-server-in-python/lessons/L04.md @@ -33,7 +33,12 @@ The 2026-07-28 replacement for server-initiated elicitation: a handler raises In ## Checkpoint -The lesson's commands run, and you can explain — in your own words — what failed and why. +The lesson's commands run, and you can answer these in your own words +(the agent asks; you answer — that is the gate): + +1. Which command did this lesson run, and what did it decide? +2. What failed at the start, and why — in your own words? +3. Before trusting a similar change next time, what would you check first? ## Expected evidence diff --git a/courses/mcp-server-in-python/lessons/L04_cn.md b/courses/mcp-server-in-python/lessons/L04_cn.md index e1db923..a962378 100644 --- a/courses/mcp-server-in-python/lessons/L04_cn.md +++ b/courses/mcp-server-in-python/lessons/L04_cn.md @@ -33,7 +33,12 @@ hints: ## 检查点 -本课的命令运行通过,并且你能用自己的话说清什么失败了、为什么。 +本课的命令运行通过,并且你能用自己的话回答下面的问题(由 Agent +提问、你作答——这就是关口): + +1. 本课跑了哪条命令,它判定了什么? +2. 一开始什么失败了、为什么——用你自己的话说? +3. 下次再信任类似的改动之前,你会先检查什么? ## 预期证据 diff --git a/courses/mcp-server-in-python/lessons/L05.md b/courses/mcp-server-in-python/lessons/L05.md index 6ddf615..7538f64 100644 --- a/courses/mcp-server-in-python/lessons/L05.md +++ b/courses/mcp-server-in-python/lessons/L05.md @@ -33,7 +33,12 @@ Pick one read-only function from your own project — a lookup, a summary, a cal ## Checkpoint -The lesson's commands run, and you can explain — in your own words — what failed and why. +The lesson's commands run, and you can answer these in your own words +(the agent asks; you answer — that is the gate): + +1. Which command did this lesson run, and what did it decide? +2. What failed at the start, and why — in your own words? +3. Before trusting a similar change next time, what would you check first? ## Expected evidence diff --git a/courses/mcp-server-in-python/lessons/L05_cn.md b/courses/mcp-server-in-python/lessons/L05_cn.md index ecc4898..aecea2c 100644 --- a/courses/mcp-server-in-python/lessons/L05_cn.md +++ b/courses/mcp-server-in-python/lessons/L05_cn.md @@ -33,7 +33,12 @@ hints: ## 检查点 -本课的命令运行通过,并且你能用自己的话说清什么失败了、为什么。 +本课的命令运行通过,并且你能用自己的话回答下面的问题(由 Agent +提问、你作答——这就是关口): + +1. 本课跑了哪条命令,它判定了什么? +2. 一开始什么失败了、为什么——用你自己的话说? +3. 下次再信任类似的改动之前,你会先检查什么? ## 预期证据 diff --git a/courses/mcp-server-in-python/verify.py b/courses/mcp-server-in-python/verify.py index 5009ac0..c46944e 100644 --- a/courses/mcp-server-in-python/verify.py +++ b/courses/mcp-server-in-python/verify.py @@ -46,6 +46,16 @@ def _claim_code(checkpoint_id): ).digest() return base64.b32encode(digest).decode("ascii")[:8] + +def _display_title(title): + # FP-820: shared-core checkpoint titles are stored "Chinese / English"; + # the default command prints English first, Chinese after (FP-709 debt). + if " / " in title: + left, _, right = title.partition(" / ") + if any("\u4e00" <= character <= "\u9fff" for character in left): + return right + " / " + left + return title + def _run_suite(implementation): environment = os.environ.copy() environment["PYTHONPATH"] = str(ROOT / implementation) @@ -54,7 +64,7 @@ def _run_suite(implementation): env=environment, check=False, capture_output=True, text=True, ) -def run_progress(as_json, receipt_out=None): +def run_progress(as_json, receipt_out=None, default=False): import time t0 = time.monotonic() starter = _run_suite("starter") @@ -100,6 +110,33 @@ def run_progress(as_json, receipt_out=None): COURSE_ID, checkpoint["id"], passed=passed, tests=tests, duration_ms=ms, impl_dir=ROOT / "starter", solution_hash=solution_hash, secret=secret)) + if default and not as_json: + # FP-820: bare ``python verify.py`` — check the learner's own + # implementation, print per-checkpoint status and earned claim codes, + # English first then Chinese (FP-709 debt). ``progress`` keeps its + # exact published output; this block is the only new surface. + print("Course " + COURSE_ID) + print("Suites: starter " + ("passed" if starter_ok else "not passed") + + " / solution " + ("passed" if solution_ok else "not passed")) + for row in rows: + state = row["status"] + (" (self-attested)" if row["kind"] == "attested" else "") + code = row["claim_code"] if row["claim_code"] else "\u2014" + print(" " + row["id"] + " " + _display_title(row["title"]) + " [" + state + "] " + code) + open_gates = [row["id"] for row in rows + if row["kind"] == "objective" and row["status"] != "passed"] + if open_gates: + print("Next: keep implementing starter/ until " + ", ".join(open_gates) + + " show [passed]; then re-run: python verify.py") + else: + print("All objective checkpoints passed — the codes above are ready" + " to submit (batch POST /api/claims, see SKILL.md §5).") + print("\u4e2d\u6587\uff1a\u68c0\u67e5\u70b9\u72b6\u6001\u4e0e\u8ba4\u9886\u7801\u89c1\u4e0a\uff1b\u5e26 [passed]/[attest] \u7684\u884c\u6709\u8ba4\u9886\u7801\uff0c[open] \u7684\u884c\u7ee7\u7eed\u5728 starter/ \u91cc\u5b9e\u73b0\u540e\u518d\u8dd1\u3002") + print("Claim codes are self-reported evidence, recorded at flypython.com; never a certificate.") + print("\u8ba4\u9886\u7801\u662f\u81ea\u6211\u62a5\u544a\u7684\u8bc1\u636e\uff0c\u8bb0\u5f55\u5728 flypython.com\uff1b\u4e0d\u662f\u8bc1\u4e66\u3002") + if secret: + print(f"Signed run receipts prepared for {len(receipts)} gated checkpoint(s);" + " submit each with its claim to mark it as a local-run receipt.") + return 1 if open_gates else 0 if as_json: document = {"course": COURSE_ID, "starter_suite_passed": starter_ok, @@ -130,14 +167,19 @@ def run_progress(as_json, receipt_out=None): def main() -> int: parser = argparse.ArgumentParser() - parser.add_argument("implementation", choices=("progress", "starter", "solution")) + parser.add_argument("implementation", choices=("progress", "starter", "solution"), + nargs="?", default=None) parser.add_argument("--json", action="store_true") parser.add_argument("--expect-failure", action="store_true") parser.add_argument("--receipt-out", metavar="PATH", help="write signed run receipts JSON (requires FLYPYTHON_CLAIM_SECRET)") args = parser.parse_args() - if args.implementation == "progress": - return run_progress(args.json, args.receipt_out) + if args.implementation in (None, "progress"): + # FP-820: the bare command is the learner's default — same engine as + # ``progress``, English-first output, exit 1 while gates stay open. + # The four explicit usages remain for maintainers. + return run_progress(args.json, args.receipt_out, + default=args.implementation is None) command = [ sys.executable, diff --git a/courses/verifying-ai-generated-code/COURSE.md b/courses/verifying-ai-generated-code/COURSE.md index fd23b16..b19d3ab 100644 --- a/courses/verifying-ai-generated-code/COURSE.md +++ b/courses/verifying-ai-generated-code/COURSE.md @@ -4,7 +4,7 @@ type: course title: "From \"it runs\" to \"it ships\": verifying AI-generated code" summary: Build the release-evidence machine your AI-written project is missing — a ship check that runs the tests, parses what actually ran, refuses zero-test projects, and writes an honest delivery record. lang: en-US -content_version: 2 +content_version: 3 status: reviewed reviewed_on: 2026-09-12 badge: @@ -17,8 +17,9 @@ course_id: course-verify-ship # From "it runs" to "it ships": verifying AI-generated code -> TL;DR: download this folder, open it in your coding agent, and say -> **"start lesson 1"**. You finish with `ship_check.py`: one command that +> TL;DR: install the FlyPython Skill in your coding agent and let it fetch +> this course, then say **"start lesson 1"**. (Lesson files arrive via +> the Skill — you download nothing by hand.) You finish with `ship_check.py`: one command that > turns "the demo worked" into a written delivery record — command, exit > code, parsed test count, a verdict that refuses projects with no tests, > and an explicit unverified list. Tool-agnostic; Python standard library @@ -67,7 +68,7 @@ release checklist; this course builds the evidence core. - Badge: **It runs is not evidence Badge** (badge id `course-verify-ship`) - earned by claiming all five checkpoints. - Challenges: L01-L05 checkpoints, 10 points each; +50 course-badge bonus when all five are claimed on flypython.com. -- Evidence: `python verify.py progress` - L03 (bounded change) and L04 (verify & review) are objectively gated by the suite; L01/L02/L05 are learner-attested. +- Evidence: `python verify.py` - L03 (bounded change) and L04 (verify & review) are objectively gated by the suite; L01/L02/L05 are learner-attested. - Submission: each passed checkpoint prints a deterministic claim code; record it on flypython.com against your account. Self-reported evidence, never a certificate. ## Folder map diff --git a/courses/verifying-ai-generated-code/COURSE_cn.md b/courses/verifying-ai-generated-code/COURSE_cn.md index 7590047..9a33bc0 100644 --- a/courses/verifying-ai-generated-code/COURSE_cn.md +++ b/courses/verifying-ai-generated-code/COURSE_cn.md @@ -4,7 +4,7 @@ type: course title: "从「能跑」到「能上线」:验证 AI 写的代码" summary: 为你的 AI 项目补上缺失的发布证据机器——一个 ship check:运行测试、解析实际跑了什么、拒绝零测试项目,并写出诚实的交付记录。 lang: zh-CN -content_version: 2 +content_version: 3 status: reviewed reviewed_on: 2026-09-12 badge: @@ -17,7 +17,8 @@ course_id: course-verify-ship # 从「能跑」到「能上线」:验证 AI 写的代码 -> 摘要:下载本文件夹,在你的编码 Agent 中打开,说一句 **“开始第 1 课”**。 +> 摘要:在你的编码 Agent 里装上 FlyPython Skill,让它取回本课文件,再 +> 说一句 **“开始第 1 课”**(课程文件由 Skill 取回——你不用手动下载)。 > 课程结束时你拥有 `ship_check.py`:一条命令把“演示能跑”变成一份书面 > 交付记录——命令、退出码、解析出的测试数、一个拒绝零测试项目的判定, > 以及一份显式的未验证清单。工具无关;仅用 Python 标准库。 @@ -61,7 +62,7 @@ course_id: course-verify-ship - 徽章:**「能跑」不是证据徽章**(徽章 id `course-verify-ship`)——认领全部五个检查点后获得。 - 挑战:L01–L05 检查点各 10 分;五项全部在 flypython.com 认领后另加 50 分课程徽章奖励。 -- 证据:`python verify.py progress` —— L03(边界修改)与 L04(验证与审查)由测试套件客观判定;L01/L02/L05 为学习者自报。 +- 证据:`python verify.py` —— L03(边界修改)与 L04(验证与审查)由测试套件客观判定;L01/L02/L05 为学习者自报。 - 提交:每个通过的检查点会打印确定性认领码,在 flypython.com 上记入你的账号。这是自我报告的证据,绝不是证书。 ## 文件夹结构 diff --git a/courses/verifying-ai-generated-code/lessons/L01.md b/courses/verifying-ai-generated-code/lessons/L01.md index a60c424..a1180a8 100644 --- a/courses/verifying-ai-generated-code/lessons/L01.md +++ b/courses/verifying-ai-generated-code/lessons/L01.md @@ -33,7 +33,12 @@ Run `python starter/ship_check.py scenario/green-project` and then `.../no-tests ## Checkpoint -The lesson's commands run, and you can explain — in your own words — what failed and why. +The lesson's commands run, and you can answer these in your own words +(the agent asks; you answer — that is the gate): + +1. Which command did this lesson run, and what did it decide? +2. What failed at the start, and why — in your own words? +3. Before trusting a similar change next time, what would you check first? ## Expected evidence diff --git a/courses/verifying-ai-generated-code/lessons/L01_cn.md b/courses/verifying-ai-generated-code/lessons/L01_cn.md index b1ad520..578ee8f 100644 --- a/courses/verifying-ai-generated-code/lessons/L01_cn.md +++ b/courses/verifying-ai-generated-code/lessons/L01_cn.md @@ -33,7 +33,12 @@ hints: ## 检查点 -本课的命令运行通过,并且你能用自己的话说清什么失败了、为什么。 +本课的命令运行通过,并且你能用自己的话回答下面的问题(由 Agent +提问、你作答——这就是关口): + +1. 本课跑了哪条命令,它判定了什么? +2. 一开始什么失败了、为什么——用你自己的话说? +3. 下次再信任类似的改动之前,你会先检查什么? ## 预期证据 diff --git a/courses/verifying-ai-generated-code/lessons/L02.md b/courses/verifying-ai-generated-code/lessons/L02.md index ee02480..0c8792c 100644 --- a/courses/verifying-ai-generated-code/lessons/L02.md +++ b/courses/verifying-ai-generated-code/lessons/L02.md @@ -33,7 +33,12 @@ Read TASK.md. The record is honest by construction: `no-tests` is its own verdic ## Checkpoint -The lesson's commands run, and you can explain — in your own words — what failed and why. +The lesson's commands run, and you can answer these in your own words +(the agent asks; you answer — that is the gate): + +1. Which command did this lesson run, and what did it decide? +2. What failed at the start, and why — in your own words? +3. Before trusting a similar change next time, what would you check first? ## Expected evidence diff --git a/courses/verifying-ai-generated-code/lessons/L02_cn.md b/courses/verifying-ai-generated-code/lessons/L02_cn.md index 05827e4..9ed6520 100644 --- a/courses/verifying-ai-generated-code/lessons/L02_cn.md +++ b/courses/verifying-ai-generated-code/lessons/L02_cn.md @@ -33,7 +33,12 @@ hints: ## 检查点 -本课的命令运行通过,并且你能用自己的话说清什么失败了、为什么。 +本课的命令运行通过,并且你能用自己的话回答下面的问题(由 Agent +提问、你作答——这就是关口): + +1. 本课跑了哪条命令,它判定了什么? +2. 一开始什么失败了、为什么——用你自己的话说? +3. 下次再信任类似的改动之前,你会先检查什么? ## 预期证据 diff --git a/courses/verifying-ai-generated-code/lessons/L03.md b/courses/verifying-ai-generated-code/lessons/L03.md index 0b4b55a..b52bcd3 100644 --- a/courses/verifying-ai-generated-code/lessons/L03.md +++ b/courses/verifying-ai-generated-code/lessons/L03.md @@ -33,7 +33,12 @@ Tell the agent: "Work through TASK.md against starter/ship_check.py, one failing ## Checkpoint -The lesson's commands run, and you can explain — in your own words — what failed and why. +The lesson's commands run, and you can answer these in your own words +(the agent asks; you answer — that is the gate): + +1. Which command did this lesson run, and what did it decide? +2. What failed at the start, and why — in your own words? +3. Before trusting a similar change next time, what would you check first? ## Expected evidence diff --git a/courses/verifying-ai-generated-code/lessons/L03_cn.md b/courses/verifying-ai-generated-code/lessons/L03_cn.md index 0bf78e0..706059a 100644 --- a/courses/verifying-ai-generated-code/lessons/L03_cn.md +++ b/courses/verifying-ai-generated-code/lessons/L03_cn.md @@ -33,7 +33,12 @@ hints: ## 检查点 -本课的命令运行通过,并且你能用自己的话说清什么失败了、为什么。 +本课的命令运行通过,并且你能用自己的话回答下面的问题(由 Agent +提问、你作答——这就是关口): + +1. 本课跑了哪条命令,它判定了什么? +2. 一开始什么失败了、为什么——用你自己的话说? +3. 下次再信任类似的改动之前,你会先检查什么? ## 预期证据 diff --git a/courses/verifying-ai-generated-code/lessons/L04.md b/courses/verifying-ai-generated-code/lessons/L04.md index e9d4c24..cd2bc5d 100644 --- a/courses/verifying-ai-generated-code/lessons/L04.md +++ b/courses/verifying-ai-generated-code/lessons/L04.md @@ -33,7 +33,12 @@ Run the finished check on all three scenario projects. Read each SHIP-RECORD.jso ## Checkpoint -The lesson's commands run, and you can explain — in your own words — what failed and why. +The lesson's commands run, and you can answer these in your own words +(the agent asks; you answer — that is the gate): + +1. Which command did this lesson run, and what did it decide? +2. What failed at the start, and why — in your own words? +3. Before trusting a similar change next time, what would you check first? ## Expected evidence diff --git a/courses/verifying-ai-generated-code/lessons/L04_cn.md b/courses/verifying-ai-generated-code/lessons/L04_cn.md index 7e423f6..c2ac8f2 100644 --- a/courses/verifying-ai-generated-code/lessons/L04_cn.md +++ b/courses/verifying-ai-generated-code/lessons/L04_cn.md @@ -33,7 +33,12 @@ hints: ## 检查点 -本课的命令运行通过,并且你能用自己的话说清什么失败了、为什么。 +本课的命令运行通过,并且你能用自己的话回答下面的问题(由 Agent +提问、你作答——这就是关口): + +1. 本课跑了哪条命令,它判定了什么? +2. 一开始什么失败了、为什么——用你自己的话说? +3. 下次再信任类似的改动之前,你会先检查什么? ## 预期证据 diff --git a/courses/verifying-ai-generated-code/lessons/L05.md b/courses/verifying-ai-generated-code/lessons/L05.md index eb0bf1c..1321928 100644 --- a/courses/verifying-ai-generated-code/lessons/L05.md +++ b/courses/verifying-ai-generated-code/lessons/L05.md @@ -32,7 +32,12 @@ Add ship.json to one of your projects (command + unverified list), drop in ship_ ## Checkpoint -The lesson's commands run, and you can explain — in your own words — what failed and why. +The lesson's commands run, and you can answer these in your own words +(the agent asks; you answer — that is the gate): + +1. Which command did this lesson run, and what did it decide? +2. What failed at the start, and why — in your own words? +3. Before trusting a similar change next time, what would you check first? ## Expected evidence diff --git a/courses/verifying-ai-generated-code/lessons/L05_cn.md b/courses/verifying-ai-generated-code/lessons/L05_cn.md index eecb794..ea7d48f 100644 --- a/courses/verifying-ai-generated-code/lessons/L05_cn.md +++ b/courses/verifying-ai-generated-code/lessons/L05_cn.md @@ -32,7 +32,12 @@ hints: ## 检查点 -本课的命令运行通过,并且你能用自己的话说清什么失败了、为什么。 +本课的命令运行通过,并且你能用自己的话回答下面的问题(由 Agent +提问、你作答——这就是关口): + +1. 本课跑了哪条命令,它判定了什么? +2. 一开始什么失败了、为什么——用你自己的话说? +3. 下次再信任类似的改动之前,你会先检查什么? ## 预期证据 diff --git a/courses/verifying-ai-generated-code/verify.py b/courses/verifying-ai-generated-code/verify.py index 7894928..37dcd96 100644 --- a/courses/verifying-ai-generated-code/verify.py +++ b/courses/verifying-ai-generated-code/verify.py @@ -60,6 +60,16 @@ def _claim_code(checkpoint_id): ).digest() return base64.b32encode(digest).decode("ascii")[:8] + +def _display_title(title): + # FP-820: shared-core checkpoint titles are stored "Chinese / English"; + # the default command prints English first, Chinese after (FP-709 debt). + if " / " in title: + left, _, right = title.partition(" / ") + if any("\u4e00" <= character <= "\u9fff" for character in left): + return right + " / " + left + return title + def _run_suite(implementation): environment = os.environ.copy() environment["PYTHONPATH"] = str(ROOT / implementation) @@ -68,7 +78,7 @@ def _run_suite(implementation): env=environment, check=False, capture_output=True, text=True, ) -def run_progress(as_json, receipt_out=None): +def run_progress(as_json, receipt_out=None, default=False): import time t0 = time.monotonic() starter = _run_suite("starter") @@ -114,6 +124,33 @@ def run_progress(as_json, receipt_out=None): COURSE_ID, checkpoint["id"], passed=passed, tests=tests, duration_ms=ms, impl_dir=ROOT / "starter", solution_hash=solution_hash, secret=secret)) + if default and not as_json: + # FP-820: bare ``python verify.py`` — check the learner's own + # implementation, print per-checkpoint status and earned claim codes, + # English first then Chinese (FP-709 debt). ``progress`` keeps its + # exact published output; this block is the only new surface. + print("Course " + COURSE_ID) + print("Suites: starter " + ("passed" if starter_ok else "not passed") + + " / solution " + ("passed" if solution_ok else "not passed")) + for row in rows: + state = row["status"] + (" (self-attested)" if row["kind"] == "attested" else "") + code = row["claim_code"] if row["claim_code"] else "\u2014" + print(" " + row["id"] + " " + _display_title(row["title"]) + " [" + state + "] " + code) + open_gates = [row["id"] for row in rows + if row["kind"] == "objective" and row["status"] != "passed"] + if open_gates: + print("Next: keep implementing starter/ until " + ", ".join(open_gates) + + " show [passed]; then re-run: python verify.py") + else: + print("All objective checkpoints passed — the codes above are ready" + " to submit (batch POST /api/claims, see SKILL.md §5).") + print("\u4e2d\u6587\uff1a\u68c0\u67e5\u70b9\u72b6\u6001\u4e0e\u8ba4\u9886\u7801\u89c1\u4e0a\uff1b\u5e26 [passed]/[attest] \u7684\u884c\u6709\u8ba4\u9886\u7801\uff0c[open] \u7684\u884c\u7ee7\u7eed\u5728 starter/ \u91cc\u5b9e\u73b0\u540e\u518d\u8dd1\u3002") + print("Claim codes are self-reported evidence, recorded at flypython.com; never a certificate.") + print("\u8ba4\u9886\u7801\u662f\u81ea\u6211\u62a5\u544a\u7684\u8bc1\u636e\uff0c\u8bb0\u5f55\u5728 flypython.com\uff1b\u4e0d\u662f\u8bc1\u4e66\u3002") + if secret: + print(f"Signed run receipts prepared for {len(receipts)} gated checkpoint(s);" + " submit each with its claim to mark it as a local-run receipt.") + return 1 if open_gates else 0 if as_json: document = {"course": COURSE_ID, "starter_suite_passed": starter_ok, @@ -144,14 +181,19 @@ def run_progress(as_json, receipt_out=None): def main() -> int: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("implementation", choices=("progress", "starter", "solution")) + parser.add_argument("implementation", choices=("progress", "starter", "solution"), + nargs="?", default=None) parser.add_argument("--json", action="store_true") parser.add_argument("--expect-failure", action="store_true") parser.add_argument("--receipt-out", metavar="PATH", help="write signed run receipts JSON (requires FLYPYTHON_CLAIM_SECRET)") args = parser.parse_args() - if args.implementation == "progress": - return run_progress(args.json, args.receipt_out) + if args.implementation in (None, "progress"): + # FP-820: the bare command is the learner's default — same engine as + # ``progress``, English-first output, exit 1 while gates stay open. + # The four explicit usages remain for maintainers. + return run_progress(args.json, args.receipt_out, + default=args.implementation is None) command = [sys.executable, "-m", "unittest", "discover", "-s", str(ROOT / "tests")] environment = os.environ.copy() diff --git a/paths/data-analysis/capstone/verify.py b/paths/data-analysis/capstone/verify.py index 224b630..4e66f6c 100644 --- a/paths/data-analysis/capstone/verify.py +++ b/paths/data-analysis/capstone/verify.py @@ -96,7 +96,9 @@ def main() -> int: return 1 print(f"capstone verified — claim code: {_claim_code()}") + print("\u4e2d\u6587\uff1a\u6bd5\u4e1a\u9879\u76ee\u5df2\u901a\u8fc7\uff0c\u8ba4\u9886\u7801\u89c1\u4e0a\uff1b\u63d0\u4ea4\u5230 flypython.com\u3002") print("Self-reported evidence for flypython.com, never a certificate.") + print("\u8ba4\u9886\u7801\u662f\u81ea\u6211\u62a5\u544a\u7684\u8bc1\u636e\uff0c\u4e0d\u662f\u8bc1\u4e66\u3002") return 0 diff --git a/tools/verify_courses.py b/tools/verify_courses.py index 0fea654..d31009f 100644 --- a/tools/verify_courses.py +++ b/tools/verify_courses.py @@ -13,8 +13,10 @@ import json import os import re +import shutil import subprocess import sys +import tempfile from pathlib import Path ROOT = Path(__file__).resolve().parents[1] @@ -179,6 +181,107 @@ def check_receipt_contract(course: Path) -> list[str]: return problems +# ── Default command contract (FP-820) ────────────────────────────────────── +# Bare ``python verify.py`` is the learner's command: it runs the same engine +# as ``progress``, prints English-first output with the Chinese lines after, +# and exits 1 while objective gates are open (a shipped starter intentionally +# fails). The published ``progress --json`` / ``--receipt-out`` formats must +# stay byte-compatible with 0.0.7 — check_progress/check_receipt above own +# those; this check owns the new surface. + + +def check_default_command(course: Path) -> list[str]: + verify = course / "verify.py" + if not verify.exists(): + return [] + name = course.name + problems: list[str] = [] + result = subprocess.run( + [sys.executable, str(verify)], + check=False, capture_output=True, text=True, + ) + if result.returncode != 1: + problems.append( + f"{name}: default command exited {result.returncode}" + " (expected 1: the shipped starter intentionally fails)") + lines = result.stdout.splitlines() + if not any(line.startswith("Suites: starter ") for line in lines): + problems.append(f"{name}: default command missing the Suites line") + if not any("[open]" in line and "l03" in line for line in lines): + problems.append(f"{name}: default command does not show l03 as open") + if not any("[attest" in line for line in lines): + problems.append(f"{name}: default command does not show self-attested checkpoints") + # English before Chinese: checkpoint titles must not lead with Chinese + # (FP-709 mixed-order debt) — shared-core titles are stored "zh / en". + for line in lines: + if re.match(r" l\d{2} ", line): + title = line.split(" ", 3)[2] if line.count(" ") >= 3 else "" + if re.match(r"^[\u4e00-\u9fff]", title): + problems.append(f"{name}: default command prints a Chinese-first checkpoint title") + break + return problems + + +# ── Standalone-folder contract (FP-821) ──────────────────────────────────── +# A course fetched through /api/challenges/<slug>/files must verify alone: +# only the course folder plus the shared files the endpoint attaches (the +# repo-relative tools/ tree, e.g. claim_receipt.py). This check rebuilds that +# layout in a temp directory and runs the default command + receipts there. + +STANDALONE_SHARED = ("tools/claim_receipt.py",) + + +def check_standalone_run(course: Path) -> list[str]: + name = course.name + problems: list[str] = [] + with tempfile.TemporaryDirectory(prefix="fp-standalone-") as tmp: + root = Path(tmp) + isolated = root / "courses" / name + shutil.copytree( + course, isolated, + ignore=shutil.ignore_patterns("__pycache__", "*.pyc", ".pytest_cache"), + ) + for shared in STANDALONE_SHARED: + source = ROOT / shared + if not source.exists(): + problems.append(f"{name}: shared file {shared} missing from repo") + continue + (root / shared).parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source, root / shared) + + default = subprocess.run( + [sys.executable, str(isolated / "verify.py")], + check=False, capture_output=True, text=True, + ) + if default.returncode != 1: + problems.append( + f"{name}: standalone default command exited {default.returncode}" + " (expected 1: the shipped starter intentionally fails)") + elif "Suites:" not in default.stdout or "l03" not in default.stdout: + problems.append(f"{name}: standalone default command did not print checkpoint status") + + env = dict(os.environ, FLYPYTHON_CLAIM_SECRET="standalone-test-secret") + receipts = subprocess.run( + [sys.executable, str(isolated / "verify.py"), "progress", "--json"], + check=False, capture_output=True, text=True, env=env, + ) + if receipts.returncode != 0: + problems.append( + f"{name}: standalone progress failed\n" + f"{(receipts.stdout or '') + (receipts.stderr or '')}".rstrip()) + else: + try: + document = json.loads(receipts.stdout) + except ValueError: + problems.append(f"{name}: standalone progress --json did not emit valid JSON") + document = {} + if not document.get("receipts"): + problems.append( + f"{name}: standalone run produced no receipts — the shared" + " tools/ file the files endpoint attaches is not loadable") + return problems + + # ── Shared-core enforcement (docs/courses/README.md §5.2, FP-711) ───────── # Agent-tool courses share one exercise — the report-tool scenario skins, # starter/solution pair, tests, and task contract. Folders stay @@ -266,6 +369,8 @@ def main() -> int: for course in courses: problems.extend(check_bilingual_contract(course)) problems.extend(run_course_verifier(course)) + problems.extend(check_default_command(course)) + problems.extend(check_standalone_run(course)) problems.extend(check_shared_core(courses)) if problems: From b762a88b6023b9276aa9a31ea9a9c5392f0c8640 Mon Sep 17 00:00:00 2001 From: pxiaoer <pxiaoer@users.noreply.github.com> Date: Sun, 13 Sep 2026 20:52:41 +0800 Subject: [PATCH 07/10] docs: 0.1.0 planning for community-contributed courses (parked until 0.1.0) First draft from the parallel planning session: permanently-free community courses, review states, and open questions. Referenced by the website's 0.0.9 plan revision 4. --- docs/repo-plan-0.1.0.md | 112 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 docs/repo-plan-0.1.0.md diff --git a/docs/repo-plan-0.1.0.md b/docs/repo-plan-0.1.0.md new file mode 100644 index 0000000..aa95c36 --- /dev/null +++ b/docs/repo-plan-0.1.0.md @@ -0,0 +1,112 @@ +# FlyPython 仓库 0.1.0 规划:社区贡献课程 + +版本:0.1.0(规划,第 1 稿) +更新日期:2026-09-13 +状态:**规划,0.1.0 之前不实施。** +关联:网站仓库 `docs/product-and-growth-plan-0.0.9.md`(附录 B 记录了移出决定)· +网站仓库 `docs/courses/README.md`(课程标准)· 本仓库 `CONTRIBUTING.md`、 +`docs/CURATION_POLICY.md`、`LICENSE` + +## 决定 + +- 所有者 2026-09-13:支持网友通过本仓库提交课程;**社区贡献课程永久免费**。 +- 同日决定:**0.1.0 之前不考虑社区课程**,先把规划写在本仓库,届时再立项、分配 + 工作项编号。 +- 官方示范(视频、终端回放、文字稿)是 Pro 专属;社区课是否配官方示范、如何开放, + 届时按网站当时的 Pro 规则另行决定。本规划不承诺社区课有示范。 + +## 现状(2026-09-13 核对) + +- `LICENSE` 已是双许可:代码(starter、solution、`verify.py`、工具)MIT;课文等书面 + 内容 CC BY 4.0,署名「FlyPython (flypython.com)」。 +- `CONTRIBUTING.md` 有资源、项目、课程反馈、失效链接、安全报告等流程,**没有「贡献 + 一门课程」的流程**。 +- 课程标准已存在:一门课一个文件夹(`COURSE.md` / `TASK.md` 与中文对、五课、starter / + solution / tests、`verify.py`、`REVIEW.md`),`tools/verify_courses.py` 校验; + 0.0.8 起课程文件夹单独取出也能运行 `python verify.py`。 +- 网站的课程数据与认领契约目前是手工镜像本仓库,每加一门课都要改网站代码—— + 社区课一多就成为瓶颈(见第 6 项)。 + +## 待办(0.1.0 立项时细化) + +## 1. 贡献流程与课程模板 [高] + +- [ ] 仓库新增「Course proposal」issue 表单:先提目标学员、要解决的真实任务、使用 + 的工具、判定方式,维护者同意后再写 +- [ ] `CONTRIBUTING.md` 增加「贡献一门课程」一节,指向课程标准(网站仓库 `docs/courses/README.md` §2) +- [ ] 课程脚手架(`templates/course/` 或 `tools/new_course.py`):生成 `COURSE.md` / + `_cn`、`TASK.md` / `_cn`、`lessons/L01–L05` 与 `_cn`、starter / solution / tests、 + `verify.py`、`REVIEW.md` 骨架 +- [ ] 提交清单:中英同步、确切工具与版本、贡献者自己的实跑记录(REVIEW.md) + +## 2. 自动校验(CI) [高] + +- [ ] PR 上自动运行:`tools/verify_courses.py`、双语对齐、content-manifest、 + `verify.py starter --expect-failure` 与 `verify.py solution` +- [ ] 在**无网络**的 CI 环境里运行;课程目录体积与文件数上限;依赖只允许锁定版本 + 且在允许清单内 +- [ ] 若加入工具课共享核心组,逐字节一致性检查自动生效 + +## 3. 安全审查 [高] + +社区课的 `verify.py`、测试与 starter 会**在学员电脑上由 Agent 执行**,恶意或粗心的 +代码直接伤害学员。 + +- [ ] 维护者审查清单:不访问网络、不在课程目录之外读写、除测试运行器外不起子进程、 + 无混淆代码、无遥测;依赖在允许清单内 +- [ ] `courses/` 设 CODEOWNERS,合并必须经维护者批准 +- [ ] 合并后才会进入网站固定的内容版本;未合并的 PR 不会出现在站点与 Skill 里 + +## 4. 编辑审查与标注 [中] + +- [ ] 按 `CURATION_POLICY.md` 人工审查课文事实与定位;描述由人执笔 +- [ ] 课程页与课程列表标注「Community course · 贡献者 · FlyPython 审核于 <日期>」, + 与官方课程区分 +- [ ] 维护者至少实跑一次(Agent 可解性),记录进 REVIEW.md + +## 5. 署名与许可 [中] + +- [ ] `COURSE.md` frontmatter 增加 `authors`(贡献者 GitHub 账号),课程页与课程文档 + 显示署名 +- [ ] `LICENSE` / `CONTRIBUTING.md` 补充:内容署名在「FlyPython (flypython.com)」之外 + 同时列出贡献者;提交即声明有权提交(DCO / sign-off) +- [ ] 与网站 0.0.9 FP-903(署名方式)一并决定展示格式 + +## 6. 网站自动接入,去掉手工镜像 [高,网站仓库] + +本项工作在网站仓库完成,本仓库需要保证课程契约字段完整、机器可读。 + +- [ ] 构建期从固定的仓库版本读取课程契约(元数据、检查点、判定方式、`course_id`、 + salt、难度、时长、工具),**生成**网站课程数据与认领契约,替代手写 + `courses.ts` 与 `COURSE_CONTRACTS` +- [ ] 社区课合并后,网站只需更新内容 pin 即可上线(中英课程页、题目页、文件接口、 + 选题页自动出现) +- [ ] 网站课程文档 `docs/courses/<slug>.md` 由脚本生成骨架,维护者补「迭代方向」 +- [ ] 测试:生成数据与现有 15 门课逐字段一致后再切换 + +## 7. 永久免费的保证 [高] + +- [ ] 课程契约增加 `source: official | community`;社区课**不能**带 `pro` 标记—— + 生成脚本与站点测试双重拒绝 +- [ ] 社区课可认领、计积分、得课程徽章(与官方免费课一致);是否计入官方路线由维护者 + 单独决定(默认不计入,待决) +- [ ] 网站定价页 Free 卡写明「社区课程永久免费」 + +## 8. 维护与下线 [中] + +- [ ] 工具大版本发布后,社区课标注「需要复审」,通知贡献者;30 天(待决)内无人 + 处理则转为「存档」:页面保留、仍可做题,但从课程列表默认隐藏 +- [ ] 已有认领与积分永远保留 + +## 不做 + +- 不接受社区贡献 Pro 课程;社区课不能被标为 Pro。 +- 不做收入分成或付费悬赏承诺。 +- 未经维护者审查合并的 PR 不进入网站固定的内容版本,也不出现在 Skill 与简报里。 + +## 立项前需要决定 + +1. 社区课是否可计入官方路线(默认不计入)。 +2. 「需要复审」转为「存档」的期限(建议 30 天)。 +3. 贡献者署名在课程页的展示格式(与网站署名方式一并决定)。 +4. 每月审核社区课的容量上限(维护者时间与官方 Pro 上新共用)。 From 61bccae3f1f2ff548381a6808fdb76ed9cb7c35b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=E5=B0=8F=E4=BA=8C?= <xxg1413@gmail.com> Date: Sun, 13 Sep 2026 23:23:00 +0800 Subject: [PATCH 08/10] 0.0.9 I-part repo side: optional foundation intro + tool L01 optionality - paths/foundation: m0 orientation is now an optional extra-credit module (never first step, never badge-required); verify_paths understands "optional" and badge math excludes it. Route-challenge copy: the module pages live on flypython.com now, repo keeps the canonical markdown. - six tool-course L01s (en+zh): step 2 pre-installs the Skill as optional (the entry sentence reads it from the URL); starter failure count corrected to "five classes + two end-to-end consequences = seven named failures". - READMEs: new positioning line, same change both languages. make check green (12 courses, 2 paths). Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- README.md | 4 +- README_cn.md | 4 +- content-manifest.json | 24 ++++++------ .../lessons/L01.md | 14 +++++-- .../lessons/L01_cn.md | 8 ++-- courses/hands-on-with-cursor/lessons/L01.md | 14 +++++-- .../hands-on-with-cursor/lessons/L01_cn.md | 8 ++-- .../lessons/L01.md | 12 ++++-- .../lessons/L01_cn.md | 8 ++-- .../hands-on-with-kimi-code/lessons/L01.md | 12 ++++-- .../hands-on-with-kimi-code/lessons/L01_cn.md | 8 ++-- .../hands-on-with-openai-codex/lessons/L01.md | 14 +++++-- .../lessons/L01_cn.md | 8 ++-- courses/hands-on-with-zcode/lessons/L01.md | 12 ++++-- courses/hands-on-with-zcode/lessons/L01_cn.md | 8 ++-- paths/README.md | 11 ++++-- paths/README_cn.md | 11 ++++-- paths/foundation/path.json | 37 ++++++++++--------- tools/verify_paths.py | 4 ++ 19 files changed, 142 insertions(+), 79 deletions(-) diff --git a/README.md b/README.md index 4d07d99..d45799a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ -# FlyPython: Build Better Products with Python and AI Coding +# FlyPython: Learn to ship Python with AI coding agents +Learn to ship Python with AI coding agents — real projects; your agent does +the typing, `verify.py` decides when you're done. [![GitHub stars](https://img.shields.io/github/stars/flypythoncom/python?style=flat-square&label=stars)](https://github.com/flypythoncom/python/stargazers) [![Validate](https://github.com/flypythoncom/python/actions/workflows/validate.yml/badge.svg)](https://github.com/flypythoncom/python/actions/workflows/validate.yml) [![Website](https://img.shields.io/badge/Website-flypython.com-blue?style=flat-square)](https://flypython.com) diff --git a/README_cn.md b/README_cn.md index fd637d6..569c8a8 100644 --- a/README_cn.md +++ b/README_cn.md @@ -1,5 +1,7 @@ -# FlyPython:用 Python 和 AI Coding 做出好产品 +# FlyPython:用 AI 编码 Agent 把 Python 真正交付 +用 AI 编码 Agent 把 Python 真正交付——真实项目;Agent 负责敲键盘, +`verify.py` 决定你什么时候算完成。 [![GitHub stars](https://img.shields.io/github/stars/flypythoncom/python?style=flat-square&label=stars)](https://github.com/flypythoncom/python/stargazers) [![Validate](https://github.com/flypythoncom/python/actions/workflows/validate.yml/badge.svg)](https://github.com/flypythoncom/python/actions/workflows/validate.yml) [![Website](https://img.shields.io/badge/Website-flypython.com-blue?style=flat-square)](https://flypython.com) diff --git a/content-manifest.json b/content-manifest.json index 07e3d1c..b0156a3 100644 --- a/content-manifest.json +++ b/content-manifest.json @@ -220,14 +220,14 @@ "path": "courses/hands-on-python-with-claude-code/lessons/L01.md", "title": "Lesson 1: Set up Claude Code, install the FlyPython Skill, fetch the course", "summary": "Get Claude Code running, install the FlyPython Skill with network access approved, and let the agent fetch this course's files — you download nothing. Then make the starter's failure objective.", - "sha256": "8979f72d7ad8b886bbe954e2af2dba4c30886104bbfa83f61470d6375b45226c" + "sha256": "82369069fc4bd61c79235d87d0fad6cfb154c9fd7cef72a35588bd93d07406ae" }, { "lang": "zh-CN", "path": "courses/hands-on-python-with-claude-code/lessons/L01_cn.md", "title": "第 1 课:装好 Claude Code、装上 FlyPython Skill、让 Agent 取回课程", "summary": "先把 Claude Code 跑起来,装好 FlyPython Skill 并打开联网权限,让 Agent 取回本课文件——你不下载任何东西,然后让 starter 的失败变得客观。", - "sha256": "a038f7ff91b277d640aaf078d0a93fc9dd84431e59d3ff842d4ab9a3eb492e61" + "sha256": "92ecf820ba6e85711fe75f3949ece009e7f32e1f3ffdcc41e6d39bd8ad8795f5" } ] }, @@ -358,14 +358,14 @@ "path": "courses/hands-on-with-openai-codex/lessons/L01.md", "title": "Lesson 1: Set up the Codex app, install the FlyPython Skill, fetch the course", "summary": "Get the Codex app running, install the FlyPython Skill with network access approved, and let the agent fetch this course's files — you download nothing. Then make the starter's failure objective.", - "sha256": "4b3aa0b156a613504be0a7e4d098e82ba5c1158fd15c5a2a10b6015f5208d16e" + "sha256": "ee24b9e1e542eb8c740867f6e47467fcf9fa97f16a02458a15586c34b80927a5" }, { "lang": "zh-CN", "path": "courses/hands-on-with-openai-codex/lessons/L01_cn.md", "title": "第 1 课:装好 the Codex app、装上 FlyPython Skill、让 Agent 取回课程", "summary": "先把 the Codex app 跑起来,装好 FlyPython Skill 并打开联网权限,让 Agent 取回本课文件——你不下载任何东西,然后让 starter 的失败变得客观。", - "sha256": "e5623346801e89ef88bc8a7970583ab9f17e9cfbc45b831e5db13b302e1de380" + "sha256": "01aaf0281525e670a7707e2860e778a72c2497c86a6ea5070ae4bc5e5ae68cdb" } ] }, @@ -496,14 +496,14 @@ "path": "courses/hands-on-with-cursor/lessons/L01.md", "title": "Lesson 1: Set up Cursor, install the FlyPython Skill, fetch the course", "summary": "Get Cursor running, install the FlyPython Skill with network access approved, and let the agent fetch this course's files — you download nothing. Then make the starter's failure objective.", - "sha256": "ecd44f10871f0b2eaa37b742647ad911ff53c66b5f58e31a9c1d6bdd0637c9d3" + "sha256": "de071fa887d8199000873a79feab7bb1ea0e4627f848cd87ba161a4fb0f0a5f7" }, { "lang": "zh-CN", "path": "courses/hands-on-with-cursor/lessons/L01_cn.md", "title": "第 1 课:装好 Cursor、装上 FlyPython Skill、让 Agent 取回课程", "summary": "先把 Cursor 跑起来,装好 FlyPython Skill 并打开联网权限,让 Agent 取回本课文件——你不下载任何东西,然后让 starter 的失败变得客观。", - "sha256": "2eab233ad6f2ed9acac57c7155e6be2cedb8dc6195c909c2bde7d651c07c3e43" + "sha256": "54202b2349b7f846e71d5a18f876922c69a5c84b717e5350f1683537397ed386" } ] }, @@ -1048,14 +1048,14 @@ "path": "courses/hands-on-with-deepseek-harness/lessons/L01.md", "title": "Lesson 1: Set up DeepSeek Harness, install the FlyPython Skill, fetch the course", "summary": "Get DeepSeek Harness running, install the FlyPython Skill with network access approved, and let the agent fetch this course's files — you download nothing. Then make the starter's failure objective.", - "sha256": "b05961c1d052d53094e651a7906df7868f78ab1ea3bed98d2c435ebb08d15696" + "sha256": "966ec591705e7351461ec85a7d3f615dcdc001dc559767b32b9a362efde7aaf7" }, { "lang": "zh-CN", "path": "courses/hands-on-with-deepseek-harness/lessons/L01_cn.md", "title": "第 1 课:装好 DeepSeek Harness、装上 FlyPython Skill、让 Agent 取回课程", "summary": "先把 DeepSeek Harness 跑起来,装好 FlyPython Skill 并打开联网权限,让 Agent 取回本课文件——你不下载任何东西,然后让 starter 的失败变得客观。", - "sha256": "d50f8cc860b18080777562960519c47d13d28d00336892bfc043d2ade0948b67" + "sha256": "0f83e1dc5c7d1ec13b6cc04b4eb2b5050d957ec39da99faaa05adb4f5dc51d4b" } ] }, @@ -1186,14 +1186,14 @@ "path": "courses/hands-on-with-kimi-code/lessons/L01.md", "title": "Lesson 1: Set up Kimi Code, install the FlyPython Skill, fetch the course", "summary": "Get Kimi Code running, install the FlyPython Skill with network access approved, and let the agent fetch this course's files — you download nothing. Then make the starter's failure objective.", - "sha256": "4ea8eadb4e6e2bbf6451dc9700b1cc6779dc00eee2474d4c579b609c972f4e2f" + "sha256": "40c376c81b7a887e17a986908a8e0fce271c494f9e169bea1dd3b9c19e35bd3b" }, { "lang": "zh-CN", "path": "courses/hands-on-with-kimi-code/lessons/L01_cn.md", "title": "第 1 课:装好 Kimi Code、装上 FlyPython Skill、让 Agent 取回课程", "summary": "先把 Kimi Code 跑起来,装好 FlyPython Skill 并打开联网权限,让 Agent 取回本课文件——你不下载任何东西,然后让 starter 的失败变得客观。", - "sha256": "276b400a542a5436f231fa89739ebc1be795fc49b2cf8700fbf2de478b25f106" + "sha256": "5bb681bb60e03cbb2145b76bb671ddfd831f3680e1e617efcf637dc93592f6b6" } ] }, @@ -1600,14 +1600,14 @@ "path": "courses/hands-on-with-zcode/lessons/L01.md", "title": "Lesson 1: Set up ZCode, install the FlyPython Skill, fetch the course", "summary": "Get ZCode running, install the FlyPython Skill with network access approved, and let the agent fetch this course's files — you download nothing. Then make the starter's failure objective.", - "sha256": "c0ebabcd982edcdfee9360d0b4019d0f139573ab84bcc3ce9b9cfe8b729cbfaf" + "sha256": "2fbfe9fff71fc209d2b89558f34c47c44985711d02cdb0f7a674db606271e665" }, { "lang": "zh-CN", "path": "courses/hands-on-with-zcode/lessons/L01_cn.md", "title": "第 1 课:装好 ZCode、装上 FlyPython Skill、让 Agent 取回课程", "summary": "先把 ZCode 跑起来,装好 FlyPython Skill 并打开联网权限,让 Agent 取回本课文件——你不下载任何东西,然后让 starter 的失败变得客观。", - "sha256": "2803669b8d5526f585975b8221ccdc5cf661f973f118d853fb7c218cf68472f9" + "sha256": "1e13ba6f4e3562d25b84de6610cdac2a8fdab4915496181143b179762b6e95d9" } ] }, diff --git a/courses/hands-on-python-with-claude-code/lessons/L01.md b/courses/hands-on-python-with-claude-code/lessons/L01.md index 22283dc..1357d80 100644 --- a/courses/hands-on-python-with-claude-code/lessons/L01.md +++ b/courses/hands-on-python-with-claude-code/lessons/L01.md @@ -33,7 +33,11 @@ Install the Claude Code CLI (`npm install -g @anthropic-ai/claude-code`), create an empty working folder, and start `claude` in it. -## Step 2 — Install the FlyPython Skill (network included) +## Step 2 — (Optional) Pre-install the FlyPython Skill + +Optional: Step 3's entry sentence reads the Skill straight from its +URL, so the course works without this step — pre-installing just saves a +permission round-trip on tools that keep skills between sessions. Save the Skill as a project skill — run this in the working folder: @@ -95,8 +99,9 @@ rest of the course: python verify.py ``` -You should see the starter reproduce four real-world failure classes, mapped -to test names: +You should see the starter reproduce five real-world failure classes +plus two end-to-end consequences — seven named failing tests in all — +mapped to test names: | Failing test | Real-world meaning | | --- | --- | @@ -131,7 +136,8 @@ will trigger. Then check yourself against the table above. Run `python verify.py` in the course folder. You pass this lesson when you can answer, without looking: -1. Which four behaviors separate starter from solution? +1. Which five behaviors separate starter from solution, and which two + failing tests are their end-to-end consequences? 2. Why does `python verify.py` exit non-zero on purpose — what state is it reporting, and why is that a success condition rather than an error? 3. What does the row you found in the exercise do to the starter — crash, diff --git a/courses/hands-on-python-with-claude-code/lessons/L01_cn.md b/courses/hands-on-python-with-claude-code/lessons/L01_cn.md index cffd1d3..c07228e 100644 --- a/courses/hands-on-python-with-claude-code/lessons/L01_cn.md +++ b/courses/hands-on-python-with-claude-code/lessons/L01_cn.md @@ -31,7 +31,9 @@ hints: (`npm install -g @anthropic-ai/claude-code`),新建一个空工作目录, 在里面运行 `claude`。 -## 第 2 步——装 FlyPython Skill(含联网权限) +## 第 2 步——(可选)预装 FlyPython Skill + +这一步可选:第 3 步的开课句子会直接从 URL 读取 Skill,不装也能上课——预装只是省掉一次权限往返。 把 Skill 存为项目级技能——在工作目录里运行: @@ -88,7 +90,7 @@ starter 缺了哪些。”** python verify.py ``` -你会看到 starter 复现四类真实世界的故障,并与测试名一一对应: +你会看到 starter 复现五类真实世界故障,外加两个端到端后果——共七个具名失败测试——并与测试名一一对应: | 失败的测试 | 现实含义 | | --- | --- | @@ -119,7 +121,7 @@ python verify.py starter --expect-failure 在课程文件夹里运行 `python verify.py`。不看材料能回答下面三个 问题即算通过: -1. 哪四个行为把 starter 和 solution 区分开? +1. 哪五个行为把 starter 和 solution 区分开?其中哪两个失败测试是它们的端到端后果? 2. `python verify.py` 为什么故意以非零退出——它在报告什么状态? 为什么这是成功条件而不是报错? 3. 你在练习里找到的那一行会让 starter 崩溃、被隔离,还是悄悄 diff --git a/courses/hands-on-with-cursor/lessons/L01.md b/courses/hands-on-with-cursor/lessons/L01.md index aa1b258..7ee25e5 100644 --- a/courses/hands-on-with-cursor/lessons/L01.md +++ b/courses/hands-on-with-cursor/lessons/L01.md @@ -32,7 +32,11 @@ in this lesson. Install Cursor, then `File → Open Folder` on an empty working folder. -## Step 2 — Install the FlyPython Skill (network included) +## Step 2 — (Optional) Pre-install the FlyPython Skill + +Optional: Step 3's entry sentence reads the Skill straight from its +URL, so the course works without this step — pre-installing just saves a +permission round-trip on tools that keep skills between sessions. Save the Skill as a project rule: @@ -95,8 +99,9 @@ rest of the course: python verify.py ``` -You should see the starter reproduce four real-world failure classes, mapped -to test names: +You should see the starter reproduce five real-world failure classes +plus two end-to-end consequences — seven named failing tests in all — +mapped to test names: | Failing test | Real-world meaning | | --- | --- | @@ -131,7 +136,8 @@ agent to confirm, not to fix. Run `python verify.py` in the course folder. You pass this lesson when you can answer, without looking: -1. Which four behaviors separate starter from solution? +1. Which five behaviors separate starter from solution, and which two + failing tests are their end-to-end consequences? 2. Why does `python verify.py` exit non-zero on purpose — what state is it reporting, and why is that a success condition rather than an error? 3. What does the row you found in the exercise do to the starter — crash, diff --git a/courses/hands-on-with-cursor/lessons/L01_cn.md b/courses/hands-on-with-cursor/lessons/L01_cn.md index 6b3d14b..d8c7c97 100644 --- a/courses/hands-on-with-cursor/lessons/L01_cn.md +++ b/courses/hands-on-with-cursor/lessons/L01_cn.md @@ -30,7 +30,9 @@ hints: 安装 Cursor,然后 `File → Open Folder` 打开一个空工作目录。 -## 第 2 步——装 FlyPython Skill(含联网权限) +## 第 2 步——(可选)预装 FlyPython Skill + +这一步可选:第 3 步的开课句子会直接从 URL 读取 Skill,不装也能上课——预装只是省掉一次权限往返。 把 Skill 存为项目规则: @@ -88,7 +90,7 @@ starter 缺了哪些。”** python verify.py ``` -你会看到 starter 复现出四类真实世界故障,对应到测试名: +你会看到 starter 复现五类真实世界故障,外加两个端到端后果——共七个具名失败测试——并与测试名一一对应: | 失败的测试 | 真实含义 | | --- | --- | @@ -121,7 +123,7 @@ Agent 确认,而不是让它修。 在课程文件夹里运行 `python verify.py`。不看材料能回答下面三个 问题即算通过: -1. 哪四个行为把 starter 和 solution 区分开? +1. 哪五个行为把 starter 和 solution 区分开?其中哪两个失败测试是它们的端到端后果? 2. `python verify.py` 为什么故意以非零退出——它在报告什么状态? 为什么这是成功条件而不是报错? 3. 你在练习里找到的那一行会让 starter 崩溃、被隔离,还是悄悄 diff --git a/courses/hands-on-with-deepseek-harness/lessons/L01.md b/courses/hands-on-with-deepseek-harness/lessons/L01.md index 86cdc07..e65f3dc 100644 --- a/courses/hands-on-with-deepseek-harness/lessons/L01.md +++ b/courses/hands-on-with-deepseek-harness/lessons/L01.md @@ -36,7 +36,11 @@ package from the deepseek-harness project) with a configured model plugin — an API key or local endpoint you supply — and start a session in an empty working folder. -## Step 2 — Install the FlyPython Skill (network included) +## Step 2 — (Optional) Pre-install the FlyPython Skill + +Optional: Step 3's entry sentence reads the Skill straight from its +URL, so the course works without this step — pre-installing just saves a +permission round-trip on tools that keep skills between sessions. The harness reads `AGENTS.md` from the project root: @@ -100,7 +104,8 @@ rest of the course: python verify.py ``` -You should see the starter reproduce four real-world failure classes, +You should see the starter reproduce five real-world failure classes +plus two end-to-end consequences — seven named failing tests in all — mapped to test names: | Failing test | Real-world meaning | @@ -142,7 +147,8 @@ the trajectory step where the agent reached the same conclusion. Run `python verify.py` in the course folder. You pass this lesson when you can answer, without looking: -1. Which four behaviors separate starter from solution? +1. Which five behaviors separate starter from solution, and which two + failing tests are their end-to-end consequences? 2. Why does `python verify.py` exit non-zero on purpose — what state is it reporting, and why is that a success condition rather than an error? 3. Where in the trajectory can you prove the agent actually read diff --git a/courses/hands-on-with-deepseek-harness/lessons/L01_cn.md b/courses/hands-on-with-deepseek-harness/lessons/L01_cn.md index e423f82..98796b3 100644 --- a/courses/hands-on-with-deepseek-harness/lessons/L01_cn.md +++ b/courses/hands-on-with-deepseek-harness/lessons/L01_cn.md @@ -33,7 +33,9 @@ hints: 项目的 Python 包)并配置好模型插件——你自己提供的 API key 或本地 端点——在一个空工作目录里启动会话。 -## 第 2 步——装 FlyPython Skill(含联网权限) +## 第 2 步——(可选)预装 FlyPython Skill + +这一步可选:第 3 步的开课句子会直接从 URL 读取 Skill,不装也能上课——预装只是省掉一次权限往返。 Harness 会读取项目根目录的 `AGENTS.md`: @@ -91,7 +93,7 @@ starter 缺了哪些。”** python verify.py ``` -你会看到 starter 复现出四类真实世界故障,对应到测试名: +你会看到 starter 复现五类真实世界故障,外加两个端到端后果——共七个具名失败测试——并与测试名一一对应: | 失败的测试 | 真实含义 | | --- | --- | @@ -127,7 +129,7 @@ Agent 得出相同结论的那一步。 在课程文件夹里运行 `python verify.py`。不看材料能回答下面三个 问题即算通过: -1. 哪四个行为把 starter 和 solution 区分开? +1. 哪五个行为把 starter 和 solution 区分开?其中哪两个失败测试是它们的端到端后果? 2. `python verify.py` 为什么故意以非零退出——它在报告什么状态? 为什么这是成功条件而不是报错? 3. 在轨迹的哪个位置能证明 Agent 确实读过 `TASK.md`? diff --git a/courses/hands-on-with-kimi-code/lessons/L01.md b/courses/hands-on-with-kimi-code/lessons/L01.md index 7995421..5369446 100644 --- a/courses/hands-on-with-kimi-code/lessons/L01.md +++ b/courses/hands-on-with-kimi-code/lessons/L01.md @@ -33,7 +33,11 @@ Install the `kimi` CLI and sign in with your Moonshot/Kimi account or API key, then start `kimi` in an empty working folder. -## Step 2 — Install the FlyPython Skill (network included) +## Step 2 — (Optional) Pre-install the FlyPython Skill + +Optional: Step 3's entry sentence reads the Skill straight from its +URL, so the course works without this step — pre-installing just saves a +permission round-trip on tools that keep skills between sessions. `kimi` reads `AGENTS.md` from the project root: @@ -97,7 +101,8 @@ rest of the course: python verify.py ``` -You should see the starter reproduce four real-world failure classes, +You should see the starter reproduce five real-world failure classes +plus two end-to-end consequences — seven named failing tests in all — mapped to test names: | Failing test | Real-world meaning | @@ -133,7 +138,8 @@ row will trigger. Then check yourself against the table above — and have Run `python verify.py` in the course folder. You pass this lesson when you can answer, without looking: -1. Which four behaviors separate starter from solution? +1. Which five behaviors separate starter from solution, and which two + failing tests are their end-to-end consequences? 2. Why does `python verify.py` exit non-zero on purpose — what state is it reporting, and why is that a success condition rather than an error? 3. Which subagent can change files — and which two cannot? diff --git a/courses/hands-on-with-kimi-code/lessons/L01_cn.md b/courses/hands-on-with-kimi-code/lessons/L01_cn.md index 2384dc8..866fee1 100644 --- a/courses/hands-on-with-kimi-code/lessons/L01_cn.md +++ b/courses/hands-on-with-kimi-code/lessons/L01_cn.md @@ -30,7 +30,9 @@ hints: 安装 `kimi` CLI,用你的 Moonshot/Kimi 账号或 API key 登录,在一个空工作目录里启动 `kimi`。 -## 第 2 步——装 FlyPython Skill(含联网权限) +## 第 2 步——(可选)预装 FlyPython Skill + +这一步可选:第 3 步的开课句子会直接从 URL 读取 Skill,不装也能上课——预装只是省掉一次权限往返。 `kimi` 会读取项目根目录的 `AGENTS.md`: @@ -87,7 +89,7 @@ starter 缺了哪些。”** python verify.py ``` -你会看到 starter 复现出四类真实世界故障,对应到测试名: +你会看到 starter 复现五类真实世界故障,外加两个端到端后果——共七个具名失败测试——并与测试名一一对应: | 失败的测试 | 真实含义 | | --- | --- | @@ -120,7 +122,7 @@ python verify.py starter --expect-failure 在课程文件夹里运行 `python verify.py`。不看材料能回答下面三个 问题即算通过: -1. 哪四个行为把 starter 和 solution 区分开? +1. 哪五个行为把 starter 和 solution 区分开?其中哪两个失败测试是它们的端到端后果? 2. `python verify.py` 为什么故意以非零退出——它在报告什么状态? 为什么这是成功条件而不是报错? 3. 哪个子代理能改文件——哪两个不能? diff --git a/courses/hands-on-with-openai-codex/lessons/L01.md b/courses/hands-on-with-openai-codex/lessons/L01.md index 93989ce..cb05ea1 100644 --- a/courses/hands-on-with-openai-codex/lessons/L01.md +++ b/courses/hands-on-with-openai-codex/lessons/L01.md @@ -32,7 +32,11 @@ in this lesson. Install the OpenAI Codex desktop app, sign in, and open an empty folder as the project. -## Step 2 — Install the FlyPython Skill (network included) +## Step 2 — (Optional) Pre-install the FlyPython Skill + +Optional: Step 3's entry sentence reads the Skill straight from its +URL, so the course works without this step — pre-installing just saves a +permission round-trip on tools that keep skills between sessions. The app reads `AGENTS.md` from the project — append the Skill to it: @@ -95,8 +99,9 @@ rest of the course: python verify.py ``` -You should see the starter reproduce four real-world failure classes, mapped -to test names: +You should see the starter reproduce five real-world failure classes +plus two end-to-end consequences — seven named failing tests in all — +mapped to test names: | Failing test | Real-world meaning | | --- | --- | @@ -132,7 +137,8 @@ to confirm, not to fix. Run `python verify.py` in the course folder. You pass this lesson when you can answer, without looking: -1. Which four behaviors separate starter from solution? +1. Which five behaviors separate starter from solution, and which two + failing tests are their end-to-end consequences? 2. Why does `python verify.py` exit non-zero on purpose — what state is it reporting, and why is that a success condition rather than an error? 3. Where in the thread can you see the command the agent ran — and who diff --git a/courses/hands-on-with-openai-codex/lessons/L01_cn.md b/courses/hands-on-with-openai-codex/lessons/L01_cn.md index f2b96c5..416a8a9 100644 --- a/courses/hands-on-with-openai-codex/lessons/L01_cn.md +++ b/courses/hands-on-with-openai-codex/lessons/L01_cn.md @@ -30,7 +30,9 @@ hints: 安装 OpenAI Codex 桌面应用并登录, 把一个空文件夹作为项目打开。 -## 第 2 步——装 FlyPython Skill(含联网权限) +## 第 2 步——(可选)预装 FlyPython Skill + +这一步可选:第 3 步的开课句子会直接从 URL 读取 Skill,不装也能上课——预装只是省掉一次权限往返。 应用会读取项目里的 `AGENTS.md`——把 Skill 追加进去: @@ -87,7 +89,7 @@ starter 缺了哪些。”** python verify.py ``` -你会看到 starter 复现出四类真实世界故障,对应到测试名: +你会看到 starter 复现五类真实世界故障,外加两个端到端后果——共七个具名失败测试——并与测试名一一对应: | 失败的测试 | 真实含义 | | --- | --- | @@ -119,7 +121,7 @@ python verify.py starter --expect-failure 在课程文件夹里运行 `python verify.py`。不看材料能回答下面三个 问题即算通过: -1. 哪四个行为把 starter 和 solution 区分开? +1. 哪五个行为把 starter 和 solution 区分开?其中哪两个失败测试是它们的端到端后果? 2. `python verify.py` 为什么故意以非零退出——它在报告什么状态? 为什么这是成功条件而不是报错? 3. 在线程的哪里能看到 Agent 执行过的命令——又是谁批准的? diff --git a/courses/hands-on-with-zcode/lessons/L01.md b/courses/hands-on-with-zcode/lessons/L01.md index 8245998..2b4f2f3 100644 --- a/courses/hands-on-with-zcode/lessons/L01.md +++ b/courses/hands-on-with-zcode/lessons/L01.md @@ -32,7 +32,11 @@ in this lesson. Install the ZCode desktop app and open an empty folder as a workspace. -## Step 2 — Install the FlyPython Skill (network included) +## Step 2 — (Optional) Pre-install the FlyPython Skill + +Optional: Step 3's entry sentence reads the Skill straight from its +URL, so the course works without this step — pre-installing just saves a +permission round-trip on tools that keep skills between sessions. ZCode reads `AGENTS.md` instruction files from the workspace root: @@ -95,7 +99,8 @@ rest of the course: python verify.py ``` -You should see the starter reproduce four real-world failure classes, +You should see the starter reproduce five real-world failure classes +plus two end-to-end consequences — seven named failing tests in all — mapped to test names: | Failing test | Real-world meaning | @@ -131,7 +136,8 @@ the agent to confirm, not to fix. Run `python verify.py` in the course folder. You pass this lesson when you can answer, without looking: -1. Which four behaviors separate starter from solution? +1. Which five behaviors separate starter from solution, and which two + failing tests are their end-to-end consequences? 2. Why does `python verify.py` exit non-zero on purpose — what state is it reporting, and why is that a success condition rather than an error? 3. What does the row you found in the exercise do to the starter — crash, diff --git a/courses/hands-on-with-zcode/lessons/L01_cn.md b/courses/hands-on-with-zcode/lessons/L01_cn.md index 6933c26..008b144 100644 --- a/courses/hands-on-with-zcode/lessons/L01_cn.md +++ b/courses/hands-on-with-zcode/lessons/L01_cn.md @@ -30,7 +30,9 @@ hints: 安装 ZCode 桌面应用,把一个空文件夹 作为工作区打开。 -## 第 2 步——装 FlyPython Skill(含联网权限) +## 第 2 步——(可选)预装 FlyPython Skill + +这一步可选:第 3 步的开课句子会直接从 URL 读取 Skill,不装也能上课——预装只是省掉一次权限往返。 ZCode 会读取工作区根目录的 `AGENTS.md` 指令文件: @@ -87,7 +89,7 @@ starter 缺了哪些。”** python verify.py ``` -你会看到 starter 复现出四类真实世界故障,对应到测试名: +你会看到 starter 复现五类真实世界故障,外加两个端到端后果——共七个具名失败测试——并与测试名一一对应: | 失败的测试 | 真实含义 | | --- | --- | @@ -120,7 +122,7 @@ Agent 确认,而不是让它修。 在课程文件夹里运行 `python verify.py`。不看材料能回答下面三个 问题即算通过: -1. 哪四个行为把 starter 和 solution 区分开? +1. 哪五个行为把 starter 和 solution 区分开?其中哪两个失败测试是它们的端到端后果? 2. `python verify.py` 为什么故意以非零退出——它在报告什么状态? 为什么这是成功条件而不是报错? 3. 你在练习里找到的那一行会让 starter 崩溃、被隔离,还是悄悄 diff --git a/paths/README.md b/paths/README.md index 4432332..21af3b5 100644 --- a/paths/README.md +++ b/paths/README.md @@ -4,10 +4,12 @@ Paths sequence challenge courses into badge routes. Each path lives in its own folder with a machine-checked `path.json` contract plus bilingual module and challenge documents. -- [Agent tools foundation](foundation/) — drive Claude Code and Codex CLI, +- [Agent tools foundation](foundation/) — drive a coding agent (Claude + Code, Codex, Cursor, DeepSeek Harness, Kimi Code, or ZCode — pick one), write a single-source rules file, and verify agent output by hand. - Modules: an orientation module, the four hands-on courses, and a - dual-agent route challenge (200 points). + Modules: the pick-one tool courses, the rules and verification courses, + and a dual-agent route challenge (200 points). The orientation module is + optional extra credit (10 points) — not required for the badge. - [Data analysis with an agent](data-analysis/) — clean, explore, visualize, and report on a real dataset. Modules: the `da-eda`, `da-visualization`, and `da-report` courses plus an end-to-end capstone (200 points). @@ -15,7 +17,8 @@ and challenge documents. ## Conventions - `path.json` is the contract: module order, course references, points, and - badge definitions. `python tools/verify_paths.py` checks every path + badge definitions; `optional: true` marks extra-credit modules that do not + count toward the badge. `python tools/verify_paths.py` checks every path against the courses that actually exist in `courses/`. - Module documents (`modules/*.md`) always ship with a `_cn.md` pair in the same change. diff --git a/paths/README_cn.md b/paths/README_cn.md index 4967e45..d1356f0 100644 --- a/paths/README_cn.md +++ b/paths/README_cn.md @@ -3,16 +3,19 @@ 学习路线把挑战课程串成徽章路线。每条路线一个文件夹,包含机器可校验的 `path.json` 契约与双语的模块、挑战文档。 -- [Agent 工具基础](foundation/)——驱动 Claude Code 与 Codex CLI,写出单一 - 真源规则文件,并亲手验证 Agent 的产出。模块:一个入门模块、四门实战 - 课程,以及一个双 Agent 路线挑战(200 分)。 +- [Agent 工具基础](foundation/)——驱动一门编码 Agent(Claude Code、 + Codex、Cursor、DeepSeek Harness、Kimi Code、ZCode 六选一),写出单一 + 真源规则文件,并亲手验证 Agent 的产出。模块:六选一工具课、规则与 + 验证两门课,以及一个双 Agent 路线挑战(200 分)。入门总览模块为 + 可选加分项(10 分)——不计入徽章。 - [用 Agent 做数据分析](data-analysis/)——对真实数据集完成清洗、探索、 可视化与报告。模块:`da-eda`、`da-visualization`、`da-report` 三门课程, 外加一个端到端综合项目(200 分)。 ## 约定 -- `path.json` 是契约:模块顺序、课程引用、积分与徽章定义。 +- `path.json` 是契约:模块顺序、课程引用、积分与徽章定义; + `optional: true` 标记不计入徽章的加分模块。 `python tools/verify_paths.py` 会把每条路线与 `courses/` 中实际存在的 课程对照校验。 - 模块文档(`modules/*.md`)必须在同一次变更中附带 `_cn.md` 配对。 diff --git a/paths/foundation/path.json b/paths/foundation/path.json index ba31a78..9d026f5 100644 --- a/paths/foundation/path.json +++ b/paths/foundation/path.json @@ -18,16 +18,6 @@ "modules": [ { "order": 0, - "id": "m0", - "kind": "module", - "ref": "modules/m0-orientation.md", - "title_en": "The agent tool landscape", - "title_zh": "Agent 工具生态总览", - "points": 10, - "kind_label": "orientation" - }, - { - "order": 1, "id": "m1", "kind": "course", "course": "hands-on-python-with-claude-code", @@ -39,7 +29,7 @@ "branch_label_zh": "选你的第一个 Agent" }, { - "order": 2, + "order": 1, "id": "m2", "kind": "course", "course": "hands-on-with-openai-codex", @@ -51,7 +41,7 @@ "branch_label_zh": "选你的第一个 Agent" }, { - "order": 3, + "order": 2, "id": "m3a", "kind": "course", "course": "hands-on-with-cursor", @@ -63,7 +53,7 @@ "branch_label_zh": "选你的第一个 Agent" }, { - "order": 4, + "order": 3, "id": "m3b", "kind": "course", "course": "hands-on-with-deepseek-harness", @@ -75,7 +65,7 @@ "branch_label_zh": "选你的第一个 Agent" }, { - "order": 5, + "order": 4, "id": "m3c", "kind": "course", "course": "hands-on-with-kimi-code", @@ -87,7 +77,7 @@ "branch_label_zh": "选你的第一个 Agent" }, { - "order": 6, + "order": 5, "id": "m3d", "kind": "course", "course": "hands-on-with-zcode", @@ -99,7 +89,7 @@ "branch_label_zh": "选你的第一个 Agent" }, { - "order": 7, + "order": 6, "id": "m3", "kind": "course", "course": "agent-rules-single-source", @@ -108,7 +98,7 @@ "points": 100 }, { - "order": 8, + "order": 7, "id": "m4", "kind": "course", "course": "verifying-ai-generated-code", @@ -117,7 +107,7 @@ "points": 100 }, { - "order": 9, + "order": 8, "id": "route-challenge", "kind": "module", "ref": "modules/route-challenge.md", @@ -125,6 +115,17 @@ "title_zh": "路线挑战:双 Agent、双重验证", "points": 200, "kind_label": "project" + }, + { + "order": 9, + "id": "m0", + "kind": "module", + "ref": "modules/m0-orientation.md", + "title_en": "The agent tool landscape", + "title_zh": "Agent 工具生态总览", + "points": 10, + "kind_label": "orientation", + "optional": true } ] } diff --git a/tools/verify_paths.py b/tools/verify_paths.py index 97d044f..ff8d302 100644 --- a/tools/verify_paths.py +++ b/tools/verify_paths.py @@ -56,6 +56,10 @@ def check_path(path_file: Path) -> list[str]: continue if "points" not in module: problems.append(f"{name}: module {mid!r} missing points") + if "optional" in module and not isinstance(module["optional"], bool): + problems.append(f"{name}: module {mid!r} optional must be a boolean") + if module.get("optional") and modules[0] is module: + problems.append(f"{name}: optional module {mid!r} cannot be the first step") if kind == "course": slug = module.get("course", "") if not (ROOT / "courses" / slug).is_dir(): From d19ae88537053c6603f1981105d2e1dc874d6dec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=E5=B0=8F=E4=BA=8C?= <xxg1413@gmail.com> Date: Mon, 14 Sep 2026 01:04:30 +0800 Subject: [PATCH 09/10] 0.1.0: rename community-course plan to 0.1.x + changelog entries FP-1012: docs/repo-plan-0.1.0.md -> docs/repo-plan-0.1.x.md (community courses are a post-launch 0.1.x track, not the launch version); CHANGELOG gains the 0.0.9 and 0.1.0 sections recording FP-983/FP-984 and the rename. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- CHANGELOG.md | 24 +++++++++++++++++++ ...{repo-plan-0.1.0.md => repo-plan-0.1.x.md} | 13 +++++----- 2 files changed, 31 insertions(+), 6 deletions(-) rename docs/{repo-plan-0.1.0.md => repo-plan-0.1.x.md} (90%) diff --git a/CHANGELOG.md b/CHANGELOG.md index abdc88e..d3dcc72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,30 @@ This file records notable catalog-contract and maintenance changes. ## [Unreleased] +0.1.0 — companion-repository half of the public launch (website +`docs/product-and-growth-plan-0.1.0.md`; see the website `CHANGELOG.md` +for the platform half). + +### Changed + +- FP-1012: `docs/repo-plan-0.1.0.md` renamed to `docs/repo-plan-0.1.x.md` + — community courses are a 0.1.x track that starts after 0.1.0 ships, + not part of the launch version. + +0.0.9 — optional entry points (companion-repository half; see the +website plan `docs/product-and-growth-plan-0.0.9.md`). + +### Changed + +- FP-983: all six agent-tool courses' lesson 1 (EN+ZH) makes the + FlyPython Skill install step optional-but-recommended, and the named + failure count is unified as "five categories plus two end-to-end — + seven named failures" everywhere. +- FP-984: the foundation path's M0 orientation module is optional + extra credit (`paths/foundation/path.json`, `tools/verify_paths.py` + badge math updated); the route challenge moved off repository files + onto the site's `/tracks/<slug>/<module>` pages. + 0.0.8 — Skill as the main entry (companion-repository half; see the website plan `docs/product-and-growth-plan-0.0.8.md`). diff --git a/docs/repo-plan-0.1.0.md b/docs/repo-plan-0.1.x.md similarity index 90% rename from docs/repo-plan-0.1.0.md rename to docs/repo-plan-0.1.x.md index aa95c36..09a22f7 100644 --- a/docs/repo-plan-0.1.0.md +++ b/docs/repo-plan-0.1.x.md @@ -1,8 +1,9 @@ -# FlyPython 仓库 0.1.0 规划:社区贡献课程 +# FlyPython 仓库 0.1.x 规划:社区贡献课程 -版本:0.1.0(规划,第 1 稿) -更新日期:2026-09-13 -状态:**规划,0.1.0 之前不实施。** +版本:0.1.x(规划,第 3 稿;2026-09-14 从 `repo-plan-0.1.0.md` 改名,避免与已发布标签混淆) +更新日期:2026-09-14 +状态:**规划,网站 0.1.0 实际发布之后再立项;当前目标日为 2026-09-19,若发布闸门导致 +顺延,本计划同样顺延,发布前不实施。** 关联:网站仓库 `docs/product-and-growth-plan-0.0.9.md`(附录 B 记录了移出决定)· 网站仓库 `docs/courses/README.md`(课程标准)· 本仓库 `CONTRIBUTING.md`、 `docs/CURATION_POLICY.md`、`LICENSE` @@ -10,7 +11,7 @@ ## 决定 - 所有者 2026-09-13:支持网友通过本仓库提交课程;**社区贡献课程永久免费**。 -- 同日决定:**0.1.0 之前不考虑社区课程**,先把规划写在本仓库,届时再立项、分配 +- 同日决定:**0.1.0 之前不考虑社区课程**;随后决定放到 **0.1.x**(0.1.0 发布之后)。先把规划写在本仓库,届时再立项、分配 工作项编号。 - 官方示范(视频、终端回放、文字稿)是 Pro 专属;社区课是否配官方示范、如何开放, 届时按网站当时的 Pro 规则另行决定。本规划不承诺社区课有示范。 @@ -27,7 +28,7 @@ - 网站的课程数据与认领契约目前是手工镜像本仓库,每加一门课都要改网站代码—— 社区课一多就成为瓶颈(见第 6 项)。 -## 待办(0.1.0 立项时细化) +## 待办(0.1.x 立项时细化) ## 1. 贡献流程与课程模板 [高] From db85cdd32ba108a562365de80186c12a8deaf13b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=E5=B0=8F=E4=BA=8C?= <xxg1413@gmail.com> Date: Mon, 14 Sep 2026 01:11:10 +0800 Subject: [PATCH 10/10] 0.1.0: bump repo to 0.1.1 + immutable-tag rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FP-1012: pyproject 0.1.1 (the v0.1.0 tag stays immutable on e420fca); CONTRIBUTING gains the release/tag rules — tags immutable, independent repo/website versions linked by CHANGELOG + Release mapping, website consumes only a pinned full SHA. CHANGELOG records the v0.1.0-website / v0.1.1-repo compatibility. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- CHANGELOG.md | 4 ++++ CONTRIBUTING.md | 14 ++++++++++++++ pyproject.toml | 2 +- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3dcc72..3c0a179 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ This file records notable catalog-contract and maintenance changes. `docs/product-and-growth-plan-0.1.0.md`; see the website `CHANGELOG.md` for the platform half). +Release mapping (FP-1012): this repository ships **v0.1.1** on this +release train — the earlier `v0.1.0` tag stays immutably on `e420fca`. +Compatibility: website `v0.1.0` ↔ repository `v0.1.1`. + ### Changed - FP-1012: `docs/repo-plan-0.1.0.md` renamed to `docs/repo-plan-0.1.x.md` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 714a2b7..eac0d39 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -120,3 +120,17 @@ justify removing a resource. Passing automation does not replace editorial review. After merge, a website consumer must deliberately update its pinned catalog commit before the new data appears on flypython.com. + +## Release and tag rules + +- A pushed tag is **immutable**: never delete, move, or reuse it. `v0.1.0` + stays pointed at its original commit forever. +- This repository and the website (`flypythoncom/flypython.com`) are + independent release artifacts. They may ship different version numbers in + the same release train; the compatibility mapping is recorded in both + CHANGELOGs and in the GitHub Releases (e.g. website `v0.1.0` ↔ repository + `v0.1.1`), each naming the exact commit SHA. +- The website consumes this repository at a pinned full commit SHA + (`content-pin.json`) — never a moving branch or tag. +- `pyproject.toml` `version` tracks the repository release; bump it in the + change that cuts the release. diff --git a/pyproject.toml b/pyproject.toml index e307124..2e9a0a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "flypython" -version = "0.1.0" +version = "0.1.1" description = "Community-maintained source for building reliable Python products in the AI-coding era." readme = "README.md" requires-python = ">=3.11"