From 61bccae3f1f2ff548381a6808fdb76ed9cb7c35b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=E5=B0=8F=E4=BA=8C?= Date: Sun, 13 Sep 2026 23:23:00 +0800 Subject: [PATCH 1/4] 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?= Date: Mon, 14 Sep 2026 01:04:30 +0800 Subject: [PATCH 2/4] 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//` 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?= Date: Mon, 14 Sep 2026 01:11:10 +0800 Subject: [PATCH 3/4] 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" From 39a3e79be939f491ac3c779494bf5c655c5588a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=E5=B0=8F=E4=BA=8C?= Date: Wed, 16 Sep 2026 19:03:01 +0800 Subject: [PATCH 4/4] Make learner verification require explicit checkpoint attestation --- README.md | 10 +++ README_cn.md | 9 +++ content-manifest.json | 48 +++++++------- courses/agent-rules-single-source/COURSE.md | 2 +- .../agent-rules-single-source/COURSE_cn.md | 2 +- courses/agent-rules-single-source/verify.py | 62 ++++++++++++++++--- courses/da-eda/COURSE.md | 4 +- courses/da-eda/COURSE_cn.md | 4 +- courses/da-eda/verify.py | 62 ++++++++++++++++--- courses/da-report/COURSE.md | 2 +- courses/da-report/COURSE_cn.md | 2 +- courses/da-report/verify.py | 62 ++++++++++++++++--- courses/da-visualization/COURSE.md | 2 +- courses/da-visualization/COURSE_cn.md | 2 +- courses/da-visualization/verify.py | 62 ++++++++++++++++--- .../COURSE.md | 2 +- .../COURSE_cn.md | 2 +- .../verify.py | 62 ++++++++++++++++--- courses/hands-on-with-cursor/COURSE.md | 2 +- courses/hands-on-with-cursor/COURSE_cn.md | 2 +- courses/hands-on-with-cursor/verify.py | 62 ++++++++++++++++--- .../hands-on-with-deepseek-harness/COURSE.md | 2 +- .../COURSE_cn.md | 2 +- .../hands-on-with-deepseek-harness/verify.py | 62 ++++++++++++++++--- courses/hands-on-with-kimi-code/COURSE.md | 2 +- courses/hands-on-with-kimi-code/COURSE_cn.md | 2 +- courses/hands-on-with-kimi-code/verify.py | 62 ++++++++++++++++--- courses/hands-on-with-openai-codex/COURSE.md | 2 +- .../hands-on-with-openai-codex/COURSE_cn.md | 2 +- courses/hands-on-with-openai-codex/verify.py | 62 ++++++++++++++++--- courses/hands-on-with-zcode/COURSE.md | 2 +- courses/hands-on-with-zcode/COURSE_cn.md | 2 +- courses/hands-on-with-zcode/verify.py | 62 ++++++++++++++++--- courses/mcp-server-in-python/COURSE.md | 2 +- courses/mcp-server-in-python/COURSE_cn.md | 2 +- courses/mcp-server-in-python/verify.py | 62 ++++++++++++++++--- courses/verifying-ai-generated-code/COURSE.md | 2 +- .../verifying-ai-generated-code/COURSE_cn.md | 2 +- courses/verifying-ai-generated-code/verify.py | 62 ++++++++++++++++--- tools/verify_courses.py | 42 ++++++++++--- 40 files changed, 763 insertions(+), 142 deletions(-) diff --git a/README.md b/README.md index d45799a..b7aaec5 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,16 @@ python examples/product-slug/verify.py starter --expect-failure python examples/product-slug/verify.py solution ``` +## Verify and record a course + +In the course folder, run `python verify.py` after each change. It tests only +`starter/`: `[open]` means the task is unfinished, `[passed]` gives a claim +code, and `[pending]` is a reflection checkpoint. Answer that lesson's +questions, then run `python verify.py --attest l01` (repeat for each completed +reflection) to get its code. Submit only `[passed]` and `[attested]` codes +through your agent or the dashboard. `check --json` is the versioned v2 +interface; `progress --json` remains for older signed-receipt integrations. + ## Choose what you need to accomplish | Goal | Start here | What you will produce | diff --git a/README_cn.md b/README_cn.md index 569c8a8..3f4a18d 100644 --- a/README_cn.md +++ b/README_cn.md @@ -49,6 +49,15 @@ python examples/product-slug/verify.py starter --expect-failure python examples/product-slug/verify.py solution ``` +## 验证并记录课程进度 + +在课程文件夹里,每次修改后运行 `python verify.py`。它只测试 `starter/`: +`[open]` 表示任务未完成,`[passed]` 会给出认领码,`[pending]` 是需要 +本人回答课后问题的自报检查点。回答后运行 `python verify.py --attest l01` +(每个已完成的自报检查点分别确认),取得 `[attested]` 认领码。只通过 +Agent 或工作台提交 `[passed]` 和 `[attested]` 的码。`check --json` 是 +版本化的 v2 接口;`progress --json` 只为旧版签名回执集成保留。 + ## 选择你现在要完成的事 | 目标 | 从这里开始 | 最终产出 | diff --git a/content-manifest.json b/content-manifest.json index b0156a3..4b85980 100644 --- a/content-manifest.json +++ b/content-manifest.json @@ -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": "ea30434d93440708952bd4acb895efb5c8b59bf9764ccddeaab53f97eaa283e8" + "sha256": "b75cb686586e3ae0ecb92d9b68c220212e9364b752808b5ba63c273da339fe87" }, { "lang": "zh-CN", "path": "courses/agent-rules-single-source/COURSE_cn.md", "title": "Agent 规则的单一真源", "summary": "不再同时维护彼此打架的 AGENTS.md、CLAUDE.md 与 .cursorrules——亲手构建一个能证明仓库只有一处规则真源的检查器,由你的编码 Agent 授课。", - "sha256": "5b11e590514fe81dc01b6ad4555a55abfc3b730ca4d14ff1fa4cd5b50fb15bb9" + "sha256": "9d5efe0d5ee5615be9d90001fe8a53c209b285a9e7ca6364222f71779d83eedf" } ] }, @@ -197,14 +197,14 @@ "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": "44b874c07240c58bf7d3b240fa45fe52a0981802d2b4bbe95900567f20c12d69" + "sha256": "3076ed936b16ecae0891ecfa881b6bb553080175639e270b1bc0031a01753006" }, { "lang": "zh-CN", "path": "courses/hands-on-python-with-claude-code/COURSE_cn.md", "title": "用 Claude Code 实战 Python", "summary": "一门由 Agent 授课的课程:从下载课程文件夹到交付一个经过测试与验证的 Python 报表工具——包括任务契约、最小变更和客观的通过/失败证据。", - "sha256": "6c385a6554c914efabb37ec5b94f7f65c194094cb75d353db0bba2a35da0c6d4" + "sha256": "a7cb245d0247f6057f11e07e58963513358e698dde1eaf63e52633a4eea2b829" } ] }, @@ -335,14 +335,14 @@ "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": "da4f82fcfce2751c335d1a140cc2e55fafa9f6b1b5353f7925f5ca342ebc6da7" + "sha256": "597bb0d7a6e4529dcbf63a755d4516793ceac21681fa78728540aa42c430c688" }, { "lang": "zh-CN", "path": "courses/hands-on-with-openai-codex/COURSE_cn.md", "title": "上手 OpenAI Codex 应用", "summary": "经过验证的 Python 工作流——任务契约、有界改动、客观 verify.py 证据——在 Codex 桌面应用里手把手教:由 Agent 线程跑闭环,你监督 diff 与审批。", - "sha256": "481e2e7fa7873b9695f5ad4c9427ea68f456b54ce6901cb9f01ac57b3f2cf6a3" + "sha256": "60bdd00ee11995fbefe8f46aded4878e936efbf437586fcdcd543fa50b7d0956" } ] }, @@ -473,14 +473,14 @@ "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": "242e0a0838ba61eadf6ed1b2e88c87989e34cd47dfc5b569f6b1455b3139c3b6" + "sha256": "ae57993829cf1c271f1b1ad5d723103d484acb7dcb2dd4a03c318f2b5b012bc7" }, { "lang": "zh-CN", "path": "courses/hands-on-with-cursor/COURSE_cn.md", "title": "上手 Cursor", "summary": "经过验证的 Python 工作流——任务契约、有界改动、客观 verify.py 证据——在 Cursor 里手把手教:由 Agent 模式跑闭环,你监督 diff 与项目规则。", - "sha256": "567a8e1a76d5be9b72aa0dd445af38159132cb5660246a74053d634dc75b8d9d" + "sha256": "1e6536d0e6ef22d9152615d1c6a3af6d9a17dba5253a4383748abf6c03786886" } ] }, @@ -611,14 +611,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": "d36b48bd7f5ee3864fd7b2653dff42703469f64728c440d343a9a5c8076fa99b" + "sha256": "e50b0ca1ec8d3dcccfeb8a9597a31bf3a9b351a869be95c72495a9861ee0b224" }, { "lang": "zh-CN", "path": "courses/da-eda/COURSE_cn.md", "title": "用 Agent 做探索性数据分析", "summary": "挑战式课程——让你的 Agent 处理一份脏的商店导出数据,产出一份经过真值校验的 EDA 摘要(results.json)。目录中第一门非标准库课程;pandas 由 uv 管理。", - "sha256": "693d2da589858d6716c0dabf34042ac012875620092eefdea688ba5601e7a41b" + "sha256": "83b2a03bbb124d95b81e3d537b8a6a89b0b7be4600e43e3585dbbc5b07af18c1" } ] }, @@ -749,14 +749,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": "f0aea2909ed3e187497e9a79cf008d88324cf22e0ffc4251540825ecd8f08a46" + "sha256": "3c80de32162187285964142406f1b482dc6c422608de63825783e0444a0f582a" }, { "lang": "zh-CN", "path": "courses/da-report/COURSE_cn.md", "title": "用 Agent 从分析到报告", "summary": "挑战课程——把经过验证的 EDA 摘要变成结构化报告(report.json + report.md),其中每个数字都可回溯到输入。验证检查结构与数字一致性,不评价文笔。", - "sha256": "f141a356b60de463d329fc72bb13374a8194cdbe5ecfc973a4f6f8c0e13c64c1" + "sha256": "40e0e079eaa4c9419b0ff72756a6ee28936bfe5c31cbcc530c48082574b2bd5a" } ] }, @@ -887,14 +887,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": "8f2dba8784b18a8ca15ac578629f5099245299e8a80184a82bea2e29b3e4545c" + "sha256": "fe7a7d00197b676cff35f62b1f098b39501c529b8dff93f034f716e623cd467b" }, { "lang": "zh-CN", "path": "courses/da-visualization/COURSE_cn.md", "title": "用 Agent 做数据可视化", "summary": "挑战课程——把干净的销售数据变成三张有规格的图和一份 summary.json,其中数字会与数据集校验。图必须由数据生成,不许硬编码。", - "sha256": "80f412edb8375f5e2027285a6b27f828e7670cf536d149400fde4cf2948d297b" + "sha256": "d4e4447b9f31c9c31936b94cc0985e9f37e36090da3c562dd6450dbf1147c7f8" } ] }, @@ -1025,14 +1025,14 @@ "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": "fb3b25a0f5233613b96f51e44f31292e1ba844603b12d9b1baeac34bd9e8504f" + "sha256": "f3e928e96dfa6f45547bc2b07ba151271551af6cdd22dff4281d032df5276186" }, { "lang": "zh-CN", "path": "courses/hands-on-with-deepseek-harness/COURSE_cn.md", "title": "上手 DeepSeek Harness", "summary": "经过验证的 Python 工作流——任务契约、有界改动、客观 verify.py 证据——在 DeepSeek Harness 里手把手教:Agent 闭环由插件组装,每一步都落进可检查的轨迹日志。", - "sha256": "cabe3a68415798d70463afcfa1051d76a247793611b041b0077bba9fcb8cbbae" + "sha256": "3bef27e9425c37709924781f53d12f42b883cd5174eb94cc32675c01f5d1bfac" } ] }, @@ -1163,14 +1163,14 @@ "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": "749ea832bc28f7074f99770e0c827255d221159cef48a79a316a8de9c2b55296" + "sha256": "d574ba24d74c189cbc4cfa599f14d64ed327a65299272564300c2e85e6d6a272" }, { "lang": "zh-CN", "path": "courses/hands-on-with-kimi-code/COURSE_cn.md", "title": "上手 Kimi Code", "summary": "经过验证的 Python 工作流——任务契约、有界改动、客观 verify.py 证据——在 Kimi Code 里手把手教:内置子代理把只读勘察、规划和经你批准的编辑分在三条道上。", - "sha256": "b080c36ac7ffbb1bfaeaad98db2ef68ca61a5f6c72f445c860a5801acf57e762" + "sha256": "a7c09dafda26d905430d182258485d6792b43a55703822a3438ce552518c5815" } ] }, @@ -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": "33af5c6245ce1dc1f9fbba6431ba6fa911c63ee2b7137d9e91600370685f44d8" + "sha256": "6df68da45b69a9a62ba46cdba2b39963edc443759cdfdea00f94b9cdd1f4de40" }, { "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": "322660423993cb400ec53c8b0c79b55a276229765fbc1c3fa1891729d7c1c72c" + "sha256": "197797063a2f01da277d2c51a3cb3816077feca8eae9dfa00b76706bc0ef4887" } ] }, @@ -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": "f329af811dee9fb767f0789c5b176fef435e6bf9a2bd6e36a86e98992e232d6d" + "sha256": "f21f75bd6a716245b614ee2d9b8d782354e33d890694683a66fbd7a1fe53f46b" }, { "lang": "zh-CN", "path": "courses/verifying-ai-generated-code/COURSE_cn.md", "title": "从「能跑」到「能上线」:验证 AI 写的代码", "summary": "为你的 AI 项目补上缺失的发布证据机器——一个 ship check:运行测试、解析实际跑了什么、拒绝零测试项目,并写出诚实的交付记录。", - "sha256": "4e9f9675fd80851670b5585a48161b78be080640d832b1a4f40a0ad75078b83d" + "sha256": "ade471c2a4f88526bdb5630b67791b4b5c0757e2a6daf59d2a56566b72f34da9" } ] }, @@ -1577,14 +1577,14 @@ "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": "f1549e06594afe047d386bbf379467006df7dcaad24a9e3b848d8be0451e8f6c" + "sha256": "051fee38652ea32840258ec50e304190124bdef5965309c0e24578ecbf5c5481" }, { "lang": "zh-CN", "path": "courses/hands-on-with-zcode/COURSE_cn.md", "title": "上手 ZCode", "summary": "经过验证的 Python 工作流——任务契约、有界改动、客观 verify.py 证据——在 ZCode 桌面工作区里手把手教:Goal 模式拆解任务,Git 状态展示每一处改动。", - "sha256": "38a1ede3d23dc65fd44e0b764914d4a5f637aa1156c5b07a3d6ed5c1c141c034" + "sha256": "a3c9916af904b3c054bab2ae84c486d8ee629c699ec024ddbb8c7e993c8fe3a5" } ] }, diff --git a/courses/agent-rules-single-source/COURSE.md b/courses/agent-rules-single-source/COURSE.md index 5702146..79cca49 100644 --- a/courses/agent-rules-single-source/COURSE.md +++ b/courses/agent-rules-single-source/COURSE.md @@ -71,7 +71,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` - 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. +- Submission: each test-passed checkpoint prints a deterministic claim code; a reflection checkpoint prints one only after you answer its questions and run `python verify.py --attest ID`; 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 aba1764..8384ea4 100644 --- a/courses/agent-rules-single-source/COURSE_cn.md +++ b/courses/agent-rules-single-source/COURSE_cn.md @@ -66,7 +66,7 @@ flypython.com 上的 AGENTS.md 指南)、多仓库配置、机器策略强制 - 徽章:**量化漂移徽章**(徽章 id `course-agent-rules`)——认领全部五个检查点后获得。 - 挑战:L01–L05 检查点各 10 分;五项全部在 flypython.com 认领后另加 50 分课程徽章奖励。 - 证据:`python verify.py` —— L03(边界修改)与 L04(验证与审查)由测试套件客观判定;L01/L02/L05 为学习者自报。 -- 提交:每个通过的检查点会打印确定性认领码,在 flypython.com 上记入你的账号。这是自我报告的证据,绝不是证书。 +- 提交:测试通过的检查点打印确定性认领码;自报检查点要先回答课后问题,再运行 `python verify.py --attest ID` 才打印码,在 flypython.com 上记入你的账号。这是自我报告的证据,绝不是证书。 ## 文件夹结构 diff --git a/courses/agent-rules-single-source/verify.py b/courses/agent-rules-single-source/verify.py index e5cd879..0f3c3ba 100644 --- a/courses/agent-rules-single-source/verify.py +++ b/courses/agent-rules-single-source/verify.py @@ -183,21 +183,69 @@ def run_progress(as_json, receipt_out=None, default=False): print(f"Wrote {len(receipts)} receipt(s) to {receipt_out}", file=sys.stderr) return 0 +def run_check(as_json=False, attested=()): + """Learner-facing v2 check: run only starter/ and require explicit attestation. + + The published progress command remains the v1 claim-receipt interface. + The reference solution is checked by the maintainer's course validation, + not rerun on every learner check. + """ + selected = set(attested) + allowed = {item["id"] for item in CHECKPOINTS if item["gate"] == "attest"} + unknown = selected - allowed + if unknown: + print("Only self-reported checkpoints can be attested: " + ", ".join(sorted(unknown)), file=sys.stderr) + return 2 + dependency_check = globals().get("_deps_available") + blocked = callable(dependency_check) and not dependency_check() + result = None if blocked else _run_suite("starter") + passed = result is not None and result.returncode == 0 + rows = [] + for item in CHECKPOINTS: + self_report = item["gate"] == "attest" + status = ("attested" if item["id"] in selected else "pending") if self_report else ("blocked" if blocked else "passed" if passed else "open") + rows.append({"id": item["id"], "title": _display_title(item["title"]), + "kind": "self-reported" if self_report else "objective", + "status": status, + "claim_code": _claim_code(item["id"]) if status in ("passed", "attested") else None}) + document = {"v": 2, "course": COURSE_ID, "implementation": "starter", + "suite": {"status": "blocked" if blocked else "passed" if passed else "failed", + "reason": "Install this course's requirements first." if blocked else None}, + "checkpoints": rows} + if as_json: + print(json.dumps(document, ensure_ascii=False, indent=2)) + else: + print("Course " + COURSE_ID) + print("Suites: starter " + ("blocked: install course requirements" if blocked else "passed" if passed else "not passed")) + for row in rows: + print(" " + row["id"] + " " + row["title"] + " [" + row["status"] + "] " + (row["claim_code"] or "—")) + pending = [row["id"] for row in rows if row["status"] == "pending"] + if pending: + print("Confirm completed reflection checkpoints explicitly with --attest ID (repeat for each): " + ", ".join(pending)) + if result is not None and not passed: + print((result.stderr or result.stdout or "").strip()[-3000:], file=sys.stderr) + print("Codes record self-reported progress, not a certificate. / 认领码只记录自报进度,不是证书。") + return 0 if all(row["status"] in ("passed", "attested") for row in rows) else 1 + + def main() -> int: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("implementation", choices=("progress", "starter", "solution"), + parser.add_argument("implementation", choices=("check", "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)") + parser.add_argument("--attest", action="append", default=[], metavar="ID", help="confirm one self-reported checkpoint after doing its work") args = parser.parse_args() - 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 args.implementation in (None, "check"): + if args.receipt_out or args.expect_failure: + parser.error("--receipt-out and --expect-failure are for legacy progress/fixture commands") + return run_check(args.json, args.attest) + if args.attest: + parser.error("--attest is only valid with the learner check command") + 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() diff --git a/courses/da-eda/COURSE.md b/courses/da-eda/COURSE.md index f4141bb..6572334 100644 --- a/courses/da-eda/COURSE.md +++ b/courses/da-eda/COURSE.md @@ -58,9 +58,9 @@ uv sync # or: pip install -r requirements.txt five checkpoints. - Challenges: L01–L05, 10 points each; +50 course-badge bonus when all five are claimed on flypython.com. -- Evidence: `python verify.py progress` — L03 and L04 are objectively +- Evidence: `python verify.py` — L03 and L04 are objectively gated by the suite; L01/L02/L05 are learner-attested. -- Submission: each passed checkpoint prints a deterministic claim code; +- Submission: each test-passed checkpoint prints a deterministic claim code; a reflection checkpoint prints one only after you answer its questions and run `python verify.py --attest ID`; record it on flypython.com against your account. Self-reported evidence, never a certificate. diff --git a/courses/da-eda/COURSE_cn.md b/courses/da-eda/COURSE_cn.md index 22faaa0..06759bf 100644 --- a/courses/da-eda/COURSE_cn.md +++ b/courses/da-eda/COURSE_cn.md @@ -56,9 +56,9 @@ uv sync # 或:pip install -r requirements.txt 认领后获得。 - 挑战:L01–L05 各 10 分;五个全部在 flypython.com 认领后 +50 课程 徽章奖励分。 -- 证据:`python verify.py progress`——L03/L04 由套件客观判卷, +- 证据:`python verify.py`——L03/L04 由套件客观判卷, L01/L02/L05 为学习者自报。 -- 提交:每个通过的检查点打印一个确定性认领码,在 flypython.com 上 +- 提交:测试通过的检查点打印确定性认领码;自报检查点要先回答课后问题,再运行 `python verify.py --attest ID` 才打印码,在 flypython.com 上 记入你的账号。自我报告的证据,绝非证书。 ## 本课程不覆盖的内容 diff --git a/courses/da-eda/verify.py b/courses/da-eda/verify.py index dd7c580..875c5f5 100644 --- a/courses/da-eda/verify.py +++ b/courses/da-eda/verify.py @@ -211,21 +211,69 @@ def run_progress(as_json, receipt_out=None, default=False): return 0 +def run_check(as_json=False, attested=()): + """Learner-facing v2 check: run only starter/ and require explicit attestation. + + The published progress command remains the v1 claim-receipt interface. + The reference solution is checked by the maintainer's course validation, + not rerun on every learner check. + """ + selected = set(attested) + allowed = {item["id"] for item in CHECKPOINTS if item["gate"] == "attest"} + unknown = selected - allowed + if unknown: + print("Only self-reported checkpoints can be attested: " + ", ".join(sorted(unknown)), file=sys.stderr) + return 2 + dependency_check = globals().get("_deps_available") + blocked = callable(dependency_check) and not dependency_check() + result = None if blocked else _run_suite("starter") + passed = result is not None and result.returncode == 0 + rows = [] + for item in CHECKPOINTS: + self_report = item["gate"] == "attest" + status = ("attested" if item["id"] in selected else "pending") if self_report else ("blocked" if blocked else "passed" if passed else "open") + rows.append({"id": item["id"], "title": _display_title(item["title"]), + "kind": "self-reported" if self_report else "objective", + "status": status, + "claim_code": _claim_code(item["id"]) if status in ("passed", "attested") else None}) + document = {"v": 2, "course": COURSE_ID, "implementation": "starter", + "suite": {"status": "blocked" if blocked else "passed" if passed else "failed", + "reason": "Install this course's requirements first." if blocked else None}, + "checkpoints": rows} + if as_json: + print(json.dumps(document, ensure_ascii=False, indent=2)) + else: + print("Course " + COURSE_ID) + print("Suites: starter " + ("blocked: install course requirements" if blocked else "passed" if passed else "not passed")) + for row in rows: + print(" " + row["id"] + " " + row["title"] + " [" + row["status"] + "] " + (row["claim_code"] or "—")) + pending = [row["id"] for row in rows if row["status"] == "pending"] + if pending: + print("Confirm completed reflection checkpoints explicitly with --attest ID (repeat for each): " + ", ".join(pending)) + if result is not None and not passed: + print((result.stderr or result.stdout or "").strip()[-3000:], file=sys.stderr) + print("Codes record self-reported progress, not a certificate. / 认领码只记录自报进度,不是证书。") + return 0 if all(row["status"] in ("passed", "attested") for row in rows) else 1 + + def main() -> int: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("implementation", choices=("progress", "starter", "solution"), + parser.add_argument("implementation", choices=("check", "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)") + parser.add_argument("--attest", action="append", default=[], metavar="ID", help="confirm one self-reported checkpoint after doing its work") args = parser.parse_args() - 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 args.implementation in (None, "check"): + if args.receipt_out or args.expect_failure: + parser.error("--receipt-out and --expect-failure are for legacy progress/fixture commands") + return run_check(args.json, args.attest) + if args.attest: + parser.error("--attest is only valid with the learner check command") + if args.implementation == "progress": + return run_progress(args.json, args.receipt_out) if not _deps_available(): _deps_hint() diff --git a/courses/da-report/COURSE.md b/courses/da-report/COURSE.md index bab9f36..e4ed25b 100644 --- a/courses/da-report/COURSE.md +++ b/courses/da-report/COURSE.md @@ -51,7 +51,7 @@ cd courses/da-report checkpoints claimed. - Challenges: L01–L05, 10 points each; +50 course-badge bonus on flypython.com. -- Evidence: `python verify.py progress` — L03/L04 objective, L01/L02/L05 +- Evidence: `python verify.py` — L03/L04 objective, L01/L02/L05 attested. Self-reported evidence, never a certificate. ## What this course does NOT cover diff --git a/courses/da-report/COURSE_cn.md b/courses/da-report/COURSE_cn.md index 28d9ab3..b2b0bd4 100644 --- a/courses/da-report/COURSE_cn.md +++ b/courses/da-report/COURSE_cn.md @@ -48,7 +48,7 @@ cd courses/da-report - 徽章:**分析报告挑战者**(badge id `da-report`)——五个检查点 全部认领。 - 挑战:L01–L05 各 10 分;flypython.com 上 +50 课程徽章奖励分。 -- 证据:`python verify.py progress`——L03/L04 客观判卷, +- 证据:`python verify.py`——L03/L04 客观判卷, L01/L02/L05 自报。自我报告的证据,绝非证书。 ## 本课程不覆盖的内容 diff --git a/courses/da-report/verify.py b/courses/da-report/verify.py index ce7ad0c..62fbe35 100644 --- a/courses/da-report/verify.py +++ b/courses/da-report/verify.py @@ -180,21 +180,69 @@ def run_progress(as_json, receipt_out=None, default=False): return 0 +def run_check(as_json=False, attested=()): + """Learner-facing v2 check: run only starter/ and require explicit attestation. + + The published progress command remains the v1 claim-receipt interface. + The reference solution is checked by the maintainer's course validation, + not rerun on every learner check. + """ + selected = set(attested) + allowed = {item["id"] for item in CHECKPOINTS if item["gate"] == "attest"} + unknown = selected - allowed + if unknown: + print("Only self-reported checkpoints can be attested: " + ", ".join(sorted(unknown)), file=sys.stderr) + return 2 + dependency_check = globals().get("_deps_available") + blocked = callable(dependency_check) and not dependency_check() + result = None if blocked else _run_suite("starter") + passed = result is not None and result.returncode == 0 + rows = [] + for item in CHECKPOINTS: + self_report = item["gate"] == "attest" + status = ("attested" if item["id"] in selected else "pending") if self_report else ("blocked" if blocked else "passed" if passed else "open") + rows.append({"id": item["id"], "title": _display_title(item["title"]), + "kind": "self-reported" if self_report else "objective", + "status": status, + "claim_code": _claim_code(item["id"]) if status in ("passed", "attested") else None}) + document = {"v": 2, "course": COURSE_ID, "implementation": "starter", + "suite": {"status": "blocked" if blocked else "passed" if passed else "failed", + "reason": "Install this course's requirements first." if blocked else None}, + "checkpoints": rows} + if as_json: + print(json.dumps(document, ensure_ascii=False, indent=2)) + else: + print("Course " + COURSE_ID) + print("Suites: starter " + ("blocked: install course requirements" if blocked else "passed" if passed else "not passed")) + for row in rows: + print(" " + row["id"] + " " + row["title"] + " [" + row["status"] + "] " + (row["claim_code"] or "—")) + pending = [row["id"] for row in rows if row["status"] == "pending"] + if pending: + print("Confirm completed reflection checkpoints explicitly with --attest ID (repeat for each): " + ", ".join(pending)) + if result is not None and not passed: + print((result.stderr or result.stdout or "").strip()[-3000:], file=sys.stderr) + print("Codes record self-reported progress, not a certificate. / 认领码只记录自报进度,不是证书。") + return 0 if all(row["status"] in ("passed", "attested") for row in rows) else 1 + + def main() -> int: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("implementation", choices=("progress", "starter", "solution"), + parser.add_argument("implementation", choices=("check", "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)") + parser.add_argument("--attest", action="append", default=[], metavar="ID", help="confirm one self-reported checkpoint after doing its work") args = parser.parse_args() - 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 args.implementation in (None, "check"): + if args.receipt_out or args.expect_failure: + parser.error("--receipt-out and --expect-failure are for legacy progress/fixture commands") + return run_check(args.json, args.attest) + if args.attest: + parser.error("--attest is only valid with the learner check command") + if args.implementation == "progress": + return run_progress(args.json, args.receipt_out) environment = os.environ.copy() environment["PYTHONPATH"] = str(ROOT / args.implementation) diff --git a/courses/da-visualization/COURSE.md b/courses/da-visualization/COURSE.md index 5d98e85..c6cf8aa 100644 --- a/courses/da-visualization/COURSE.md +++ b/courses/da-visualization/COURSE.md @@ -52,7 +52,7 @@ uv sync # pandas + matplotlib (or: pip install -r requirements.txt) all five checkpoints claimed. - Challenges: L01–L05, 10 points each; +50 course-badge bonus on flypython.com. -- Evidence: `python verify.py progress` — L03/L04 objective, L01/L02/L05 +- Evidence: `python verify.py` — L03/L04 objective, L01/L02/L05 attested. Self-reported evidence, never a certificate. ## What this course does NOT cover diff --git a/courses/da-visualization/COURSE_cn.md b/courses/da-visualization/COURSE_cn.md index 1fafda2..17eb7ab 100644 --- a/courses/da-visualization/COURSE_cn.md +++ b/courses/da-visualization/COURSE_cn.md @@ -50,7 +50,7 @@ uv sync # pandas + matplotlib(或 pip install -r requirements.txt - 徽章:**可视化挑战者**(badge id `da-visualization`)——五个检查点 全部认领。 - 挑战:L01–L05 各 10 分;flypython.com 上 +50 课程徽章奖励分。 -- 证据:`python verify.py progress`——L03/L04 客观判卷,L01/L02/L05 +- 证据:`python verify.py`——L03/L04 客观判卷,L01/L02/L05 自报。自我报告的证据,绝非证书。 ## 本课程不覆盖的内容 diff --git a/courses/da-visualization/verify.py b/courses/da-visualization/verify.py index 6d6f237..3087273 100644 --- a/courses/da-visualization/verify.py +++ b/courses/da-visualization/verify.py @@ -200,21 +200,69 @@ def run_progress(as_json, receipt_out=None, default=False): return 0 +def run_check(as_json=False, attested=()): + """Learner-facing v2 check: run only starter/ and require explicit attestation. + + The published progress command remains the v1 claim-receipt interface. + The reference solution is checked by the maintainer's course validation, + not rerun on every learner check. + """ + selected = set(attested) + allowed = {item["id"] for item in CHECKPOINTS if item["gate"] == "attest"} + unknown = selected - allowed + if unknown: + print("Only self-reported checkpoints can be attested: " + ", ".join(sorted(unknown)), file=sys.stderr) + return 2 + dependency_check = globals().get("_deps_available") + blocked = callable(dependency_check) and not dependency_check() + result = None if blocked else _run_suite("starter") + passed = result is not None and result.returncode == 0 + rows = [] + for item in CHECKPOINTS: + self_report = item["gate"] == "attest" + status = ("attested" if item["id"] in selected else "pending") if self_report else ("blocked" if blocked else "passed" if passed else "open") + rows.append({"id": item["id"], "title": _display_title(item["title"]), + "kind": "self-reported" if self_report else "objective", + "status": status, + "claim_code": _claim_code(item["id"]) if status in ("passed", "attested") else None}) + document = {"v": 2, "course": COURSE_ID, "implementation": "starter", + "suite": {"status": "blocked" if blocked else "passed" if passed else "failed", + "reason": "Install this course's requirements first." if blocked else None}, + "checkpoints": rows} + if as_json: + print(json.dumps(document, ensure_ascii=False, indent=2)) + else: + print("Course " + COURSE_ID) + print("Suites: starter " + ("blocked: install course requirements" if blocked else "passed" if passed else "not passed")) + for row in rows: + print(" " + row["id"] + " " + row["title"] + " [" + row["status"] + "] " + (row["claim_code"] or "—")) + pending = [row["id"] for row in rows if row["status"] == "pending"] + if pending: + print("Confirm completed reflection checkpoints explicitly with --attest ID (repeat for each): " + ", ".join(pending)) + if result is not None and not passed: + print((result.stderr or result.stdout or "").strip()[-3000:], file=sys.stderr) + print("Codes record self-reported progress, not a certificate. / 认领码只记录自报进度,不是证书。") + return 0 if all(row["status"] in ("passed", "attested") for row in rows) else 1 + + def main() -> int: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("implementation", choices=("progress", "starter", "solution"), + parser.add_argument("implementation", choices=("check", "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)") + parser.add_argument("--attest", action="append", default=[], metavar="ID", help="confirm one self-reported checkpoint after doing its work") args = parser.parse_args() - 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 args.implementation in (None, "check"): + if args.receipt_out or args.expect_failure: + parser.error("--receipt-out and --expect-failure are for legacy progress/fixture commands") + return run_check(args.json, args.attest) + if args.attest: + parser.error("--attest is only valid with the learner check command") + if args.implementation == "progress": + return run_progress(args.json, args.receipt_out) 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 756fc75..a1a8c37 100644 --- a/courses/hands-on-python-with-claude-code/COURSE.md +++ b/courses/hands-on-python-with-claude-code/COURSE.md @@ -84,7 +84,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` - 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. +- Submission: each test-passed checkpoint prints a deterministic claim code; a reflection checkpoint prints one only after you answer its questions and run `python verify.py --attest ID`; 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 9a08206..3c8bd04 100644 --- a/courses/hands-on-python-with-claude-code/COURSE_cn.md +++ b/courses/hands-on-python-with-claude-code/COURSE_cn.md @@ -75,7 +75,7 @@ Claude Code 安装、提示词技巧、多文件架构、pandas 或真正的 Exc - 徽章:**复现故障徽章**(徽章 id `course-claude-code`)——认领全部五个检查点后获得。 - 挑战:L01–L05 检查点各 10 分;五项全部在 flypython.com 认领后另加 50 分课程徽章奖励。 - 证据:`python verify.py` —— L03(边界修改)与 L04(验证与审查)由测试套件客观判定;L01/L02/L05 为学习者自报。 -- 提交:每个通过的检查点会打印确定性认领码,在 flypython.com 上记入你的账号。这是自我报告的证据,绝不是证书。 +- 提交:测试通过的检查点打印确定性认领码;自报检查点要先回答课后问题,再运行 `python verify.py --attest ID` 才打印码,在 flypython.com 上记入你的账号。这是自我报告的证据,绝不是证书。 ## 文件夹结构 diff --git a/courses/hands-on-python-with-claude-code/verify.py b/courses/hands-on-python-with-claude-code/verify.py index 400e691..d692cdd 100644 --- a/courses/hands-on-python-with-claude-code/verify.py +++ b/courses/hands-on-python-with-claude-code/verify.py @@ -187,21 +187,69 @@ def run_progress(as_json, receipt_out=None, default=False): print(f"Wrote {len(receipts)} receipt(s) to {receipt_out}", file=sys.stderr) return 0 +def run_check(as_json=False, attested=()): + """Learner-facing v2 check: run only starter/ and require explicit attestation. + + The published progress command remains the v1 claim-receipt interface. + The reference solution is checked by the maintainer's course validation, + not rerun on every learner check. + """ + selected = set(attested) + allowed = {item["id"] for item in CHECKPOINTS if item["gate"] == "attest"} + unknown = selected - allowed + if unknown: + print("Only self-reported checkpoints can be attested: " + ", ".join(sorted(unknown)), file=sys.stderr) + return 2 + dependency_check = globals().get("_deps_available") + blocked = callable(dependency_check) and not dependency_check() + result = None if blocked else _run_suite("starter") + passed = result is not None and result.returncode == 0 + rows = [] + for item in CHECKPOINTS: + self_report = item["gate"] == "attest" + status = ("attested" if item["id"] in selected else "pending") if self_report else ("blocked" if blocked else "passed" if passed else "open") + rows.append({"id": item["id"], "title": _display_title(item["title"]), + "kind": "self-reported" if self_report else "objective", + "status": status, + "claim_code": _claim_code(item["id"]) if status in ("passed", "attested") else None}) + document = {"v": 2, "course": COURSE_ID, "implementation": "starter", + "suite": {"status": "blocked" if blocked else "passed" if passed else "failed", + "reason": "Install this course's requirements first." if blocked else None}, + "checkpoints": rows} + if as_json: + print(json.dumps(document, ensure_ascii=False, indent=2)) + else: + print("Course " + COURSE_ID) + print("Suites: starter " + ("blocked: install course requirements" if blocked else "passed" if passed else "not passed")) + for row in rows: + print(" " + row["id"] + " " + row["title"] + " [" + row["status"] + "] " + (row["claim_code"] or "—")) + pending = [row["id"] for row in rows if row["status"] == "pending"] + if pending: + print("Confirm completed reflection checkpoints explicitly with --attest ID (repeat for each): " + ", ".join(pending)) + if result is not None and not passed: + print((result.stderr or result.stdout or "").strip()[-3000:], file=sys.stderr) + print("Codes record self-reported progress, not a certificate. / 认领码只记录自报进度,不是证书。") + return 0 if all(row["status"] in ("passed", "attested") for row in rows) else 1 + + def main() -> int: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("implementation", choices=("progress", "starter", "solution"), + parser.add_argument("implementation", choices=("check", "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)") + parser.add_argument("--attest", action="append", default=[], metavar="ID", help="confirm one self-reported checkpoint after doing its work") args = parser.parse_args() - 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 args.implementation in (None, "check"): + if args.receipt_out or args.expect_failure: + parser.error("--receipt-out and --expect-failure are for legacy progress/fixture commands") + return run_check(args.json, args.attest) + if args.attest: + parser.error("--attest is only valid with the learner check command") + 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() diff --git a/courses/hands-on-with-cursor/COURSE.md b/courses/hands-on-with-cursor/COURSE.md index 935b53a..8864dec 100644 --- a/courses/hands-on-with-cursor/COURSE.md +++ b/courses/hands-on-with-cursor/COURSE.md @@ -88,7 +88,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` — 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. +- Submission: each test-passed checkpoint prints a deterministic claim code; a reflection checkpoint prints one only after you answer its questions and run `python verify.py --attest ID`; 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 b06a955..fa09bca 100644 --- a/courses/hands-on-with-cursor/COURSE_cn.md +++ b/courses/hands-on-with-cursor/COURSE_cn.md @@ -77,7 +77,7 @@ Cursor 的安装、套餐定价、模型选择、MCP 配置、团队/管理功 - 徽章:**Cursor 协同复现徽章**(badge id `course-cursor`)——认领全部五个检查点获得。 - 挑战:L01–L05 检查点,各 10 分;在 flypython.com 上集齐五个再加 50 分课程徽章奖励。 - 证据:`python verify.py`——L03(有界改动)与 L04(验证与评审)由套件客观把关;L01/L02/L05 为学习者自我报告。 -- 提交:每个通过的检查点打印一个确定性的认领码;在 flypython.com 上记入你的账号。这是自我报告的证据,从来不是证书。 +- 提交:测试通过的检查点打印确定性认领码;自报检查点要先回答课后问题,再运行 `python verify.py --attest ID` 才打印码;在 flypython.com 上记入你的账号。这是自我报告的证据,从来不是证书。 ## 文件夹地图 diff --git a/courses/hands-on-with-cursor/verify.py b/courses/hands-on-with-cursor/verify.py index b653ff6..1b07828 100644 --- a/courses/hands-on-with-cursor/verify.py +++ b/courses/hands-on-with-cursor/verify.py @@ -187,21 +187,69 @@ def run_progress(as_json, receipt_out=None, default=False): print(f"Wrote {len(receipts)} receipt(s) to {receipt_out}", file=sys.stderr) return 0 +def run_check(as_json=False, attested=()): + """Learner-facing v2 check: run only starter/ and require explicit attestation. + + The published progress command remains the v1 claim-receipt interface. + The reference solution is checked by the maintainer's course validation, + not rerun on every learner check. + """ + selected = set(attested) + allowed = {item["id"] for item in CHECKPOINTS if item["gate"] == "attest"} + unknown = selected - allowed + if unknown: + print("Only self-reported checkpoints can be attested: " + ", ".join(sorted(unknown)), file=sys.stderr) + return 2 + dependency_check = globals().get("_deps_available") + blocked = callable(dependency_check) and not dependency_check() + result = None if blocked else _run_suite("starter") + passed = result is not None and result.returncode == 0 + rows = [] + for item in CHECKPOINTS: + self_report = item["gate"] == "attest" + status = ("attested" if item["id"] in selected else "pending") if self_report else ("blocked" if blocked else "passed" if passed else "open") + rows.append({"id": item["id"], "title": _display_title(item["title"]), + "kind": "self-reported" if self_report else "objective", + "status": status, + "claim_code": _claim_code(item["id"]) if status in ("passed", "attested") else None}) + document = {"v": 2, "course": COURSE_ID, "implementation": "starter", + "suite": {"status": "blocked" if blocked else "passed" if passed else "failed", + "reason": "Install this course's requirements first." if blocked else None}, + "checkpoints": rows} + if as_json: + print(json.dumps(document, ensure_ascii=False, indent=2)) + else: + print("Course " + COURSE_ID) + print("Suites: starter " + ("blocked: install course requirements" if blocked else "passed" if passed else "not passed")) + for row in rows: + print(" " + row["id"] + " " + row["title"] + " [" + row["status"] + "] " + (row["claim_code"] or "—")) + pending = [row["id"] for row in rows if row["status"] == "pending"] + if pending: + print("Confirm completed reflection checkpoints explicitly with --attest ID (repeat for each): " + ", ".join(pending)) + if result is not None and not passed: + print((result.stderr or result.stdout or "").strip()[-3000:], file=sys.stderr) + print("Codes record self-reported progress, not a certificate. / 认领码只记录自报进度,不是证书。") + return 0 if all(row["status"] in ("passed", "attested") for row in rows) else 1 + + def main() -> int: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("implementation", choices=("progress", "starter", "solution"), + parser.add_argument("implementation", choices=("check", "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)") + parser.add_argument("--attest", action="append", default=[], metavar="ID", help="confirm one self-reported checkpoint after doing its work") args = parser.parse_args() - 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 args.implementation in (None, "check"): + if args.receipt_out or args.expect_failure: + parser.error("--receipt-out and --expect-failure are for legacy progress/fixture commands") + return run_check(args.json, args.attest) + if args.attest: + parser.error("--attest is only valid with the learner check command") + 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() diff --git a/courses/hands-on-with-deepseek-harness/COURSE.md b/courses/hands-on-with-deepseek-harness/COURSE.md index 7331a39..2ef23b1 100644 --- a/courses/hands-on-with-deepseek-harness/COURSE.md +++ b/courses/hands-on-with-deepseek-harness/COURSE.md @@ -93,7 +93,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` — 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. +- Submission: each test-passed checkpoint prints a deterministic claim code; a reflection checkpoint prints one only after you answer its questions and run `python verify.py --attest ID`; 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 7de7a92..2d05251 100644 --- a/courses/hands-on-with-deepseek-harness/COURSE_cn.md +++ b/courses/hands-on-with-deepseek-harness/COURSE_cn.md @@ -81,7 +81,7 @@ course_id: course-deepseek-harness - 徽章:**DeepSeek Harness 协同复现徽章**(badge id `course-deepseek-harness`)——认领全部五个检查点获得。 - 挑战:L01–L05 检查点,各 10 分;在 flypython.com 上集齐五个再加 50 分课程徽章奖励。 - 证据:`python verify.py`——L03(有界改动)与 L04(验证与评审)由套件客观把关;L01/L02/L05 为学习者自我报告。 -- 提交:每个通过的检查点打印一个确定性的认领码;在 flypython.com 上记入你的账号。这是自我报告的证据,从来不是证书。 +- 提交:测试通过的检查点打印确定性认领码;自报检查点要先回答课后问题,再运行 `python verify.py --attest ID` 才打印码;在 flypython.com 上记入你的账号。这是自我报告的证据,从来不是证书。 ## 文件夹地图 diff --git a/courses/hands-on-with-deepseek-harness/verify.py b/courses/hands-on-with-deepseek-harness/verify.py index 3e71d12..4b7d970 100644 --- a/courses/hands-on-with-deepseek-harness/verify.py +++ b/courses/hands-on-with-deepseek-harness/verify.py @@ -187,21 +187,69 @@ def run_progress(as_json, receipt_out=None, default=False): print(f"Wrote {len(receipts)} receipt(s) to {receipt_out}", file=sys.stderr) return 0 +def run_check(as_json=False, attested=()): + """Learner-facing v2 check: run only starter/ and require explicit attestation. + + The published progress command remains the v1 claim-receipt interface. + The reference solution is checked by the maintainer's course validation, + not rerun on every learner check. + """ + selected = set(attested) + allowed = {item["id"] for item in CHECKPOINTS if item["gate"] == "attest"} + unknown = selected - allowed + if unknown: + print("Only self-reported checkpoints can be attested: " + ", ".join(sorted(unknown)), file=sys.stderr) + return 2 + dependency_check = globals().get("_deps_available") + blocked = callable(dependency_check) and not dependency_check() + result = None if blocked else _run_suite("starter") + passed = result is not None and result.returncode == 0 + rows = [] + for item in CHECKPOINTS: + self_report = item["gate"] == "attest" + status = ("attested" if item["id"] in selected else "pending") if self_report else ("blocked" if blocked else "passed" if passed else "open") + rows.append({"id": item["id"], "title": _display_title(item["title"]), + "kind": "self-reported" if self_report else "objective", + "status": status, + "claim_code": _claim_code(item["id"]) if status in ("passed", "attested") else None}) + document = {"v": 2, "course": COURSE_ID, "implementation": "starter", + "suite": {"status": "blocked" if blocked else "passed" if passed else "failed", + "reason": "Install this course's requirements first." if blocked else None}, + "checkpoints": rows} + if as_json: + print(json.dumps(document, ensure_ascii=False, indent=2)) + else: + print("Course " + COURSE_ID) + print("Suites: starter " + ("blocked: install course requirements" if blocked else "passed" if passed else "not passed")) + for row in rows: + print(" " + row["id"] + " " + row["title"] + " [" + row["status"] + "] " + (row["claim_code"] or "—")) + pending = [row["id"] for row in rows if row["status"] == "pending"] + if pending: + print("Confirm completed reflection checkpoints explicitly with --attest ID (repeat for each): " + ", ".join(pending)) + if result is not None and not passed: + print((result.stderr or result.stdout or "").strip()[-3000:], file=sys.stderr) + print("Codes record self-reported progress, not a certificate. / 认领码只记录自报进度,不是证书。") + return 0 if all(row["status"] in ("passed", "attested") for row in rows) else 1 + + def main() -> int: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("implementation", choices=("progress", "starter", "solution"), + parser.add_argument("implementation", choices=("check", "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)") + parser.add_argument("--attest", action="append", default=[], metavar="ID", help="confirm one self-reported checkpoint after doing its work") args = parser.parse_args() - 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 args.implementation in (None, "check"): + if args.receipt_out or args.expect_failure: + parser.error("--receipt-out and --expect-failure are for legacy progress/fixture commands") + return run_check(args.json, args.attest) + if args.attest: + parser.error("--attest is only valid with the learner check command") + 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() diff --git a/courses/hands-on-with-kimi-code/COURSE.md b/courses/hands-on-with-kimi-code/COURSE.md index aa47d2d..12ca154 100644 --- a/courses/hands-on-with-kimi-code/COURSE.md +++ b/courses/hands-on-with-kimi-code/COURSE.md @@ -88,7 +88,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` — 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. +- Submission: each test-passed checkpoint prints a deterministic claim code; a reflection checkpoint prints one only after you answer its questions and run `python verify.py --attest ID`; 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 fe28282..c393657 100644 --- a/courses/hands-on-with-kimi-code/COURSE_cn.md +++ b/courses/hands-on-with-kimi-code/COURSE_cn.md @@ -79,7 +79,7 @@ Kimi Code 的安装与计费、自定义子代理编写、ACP 客户端开发, - 徽章:**Kimi Code 协同复现徽章**(badge id `course-kimi-code`)——认领全部五个检查点获得。 - 挑战:L01–L05 检查点,各 10 分;在 flypython.com 上集齐五个再加 50 分课程徽章奖励。 - 证据:`python verify.py`——L03(有界改动)与 L04(验证与评审)由套件客观把关;L01/L02/L05 为学习者自我报告。 -- 提交:每个通过的检查点打印一个确定性的认领码;在 flypython.com 上记入你的账号。这是自我报告的证据,从来不是证书。 +- 提交:测试通过的检查点打印确定性认领码;自报检查点要先回答课后问题,再运行 `python verify.py --attest ID` 才打印码;在 flypython.com 上记入你的账号。这是自我报告的证据,从来不是证书。 ## 文件夹地图 diff --git a/courses/hands-on-with-kimi-code/verify.py b/courses/hands-on-with-kimi-code/verify.py index 4a178a1..54bf486 100644 --- a/courses/hands-on-with-kimi-code/verify.py +++ b/courses/hands-on-with-kimi-code/verify.py @@ -187,21 +187,69 @@ def run_progress(as_json, receipt_out=None, default=False): print(f"Wrote {len(receipts)} receipt(s) to {receipt_out}", file=sys.stderr) return 0 +def run_check(as_json=False, attested=()): + """Learner-facing v2 check: run only starter/ and require explicit attestation. + + The published progress command remains the v1 claim-receipt interface. + The reference solution is checked by the maintainer's course validation, + not rerun on every learner check. + """ + selected = set(attested) + allowed = {item["id"] for item in CHECKPOINTS if item["gate"] == "attest"} + unknown = selected - allowed + if unknown: + print("Only self-reported checkpoints can be attested: " + ", ".join(sorted(unknown)), file=sys.stderr) + return 2 + dependency_check = globals().get("_deps_available") + blocked = callable(dependency_check) and not dependency_check() + result = None if blocked else _run_suite("starter") + passed = result is not None and result.returncode == 0 + rows = [] + for item in CHECKPOINTS: + self_report = item["gate"] == "attest" + status = ("attested" if item["id"] in selected else "pending") if self_report else ("blocked" if blocked else "passed" if passed else "open") + rows.append({"id": item["id"], "title": _display_title(item["title"]), + "kind": "self-reported" if self_report else "objective", + "status": status, + "claim_code": _claim_code(item["id"]) if status in ("passed", "attested") else None}) + document = {"v": 2, "course": COURSE_ID, "implementation": "starter", + "suite": {"status": "blocked" if blocked else "passed" if passed else "failed", + "reason": "Install this course's requirements first." if blocked else None}, + "checkpoints": rows} + if as_json: + print(json.dumps(document, ensure_ascii=False, indent=2)) + else: + print("Course " + COURSE_ID) + print("Suites: starter " + ("blocked: install course requirements" if blocked else "passed" if passed else "not passed")) + for row in rows: + print(" " + row["id"] + " " + row["title"] + " [" + row["status"] + "] " + (row["claim_code"] or "—")) + pending = [row["id"] for row in rows if row["status"] == "pending"] + if pending: + print("Confirm completed reflection checkpoints explicitly with --attest ID (repeat for each): " + ", ".join(pending)) + if result is not None and not passed: + print((result.stderr or result.stdout or "").strip()[-3000:], file=sys.stderr) + print("Codes record self-reported progress, not a certificate. / 认领码只记录自报进度,不是证书。") + return 0 if all(row["status"] in ("passed", "attested") for row in rows) else 1 + + def main() -> int: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("implementation", choices=("progress", "starter", "solution"), + parser.add_argument("implementation", choices=("check", "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)") + parser.add_argument("--attest", action="append", default=[], metavar="ID", help="confirm one self-reported checkpoint after doing its work") args = parser.parse_args() - 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 args.implementation in (None, "check"): + if args.receipt_out or args.expect_failure: + parser.error("--receipt-out and --expect-failure are for legacy progress/fixture commands") + return run_check(args.json, args.attest) + if args.attest: + parser.error("--attest is only valid with the learner check command") + 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() diff --git a/courses/hands-on-with-openai-codex/COURSE.md b/courses/hands-on-with-openai-codex/COURSE.md index 78ed58d..468808e 100644 --- a/courses/hands-on-with-openai-codex/COURSE.md +++ b/courses/hands-on-with-openai-codex/COURSE.md @@ -88,7 +88,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` — 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. +- Submission: each test-passed checkpoint prints a deterministic claim code; a reflection checkpoint prints one only after you answer its questions and run `python verify.py --attest ID`; 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 de0bb91..4dbd825 100644 --- a/courses/hands-on-with-openai-codex/COURSE_cn.md +++ b/courses/hands-on-with-openai-codex/COURSE_cn.md @@ -78,7 +78,7 @@ Codex 应用的安装与计费、Codex CLI 的旗标、CI/CD 云端任务、MCP - 徽章:**Codex 协同复现徽章**(badge id `course-codex-cli`)——认领全部五个检查点获得。 - 挑战:L01–L05 检查点,各 10 分;在 flypython.com 上集齐五个再加 50 分课程徽章奖励。 - 证据:`python verify.py`——L03(有界改动)与 L04(验证与评审)由套件客观把关;L01/L02/L05 为学习者自我报告。 -- 提交:每个通过的检查点打印一个确定性的认领码;在 flypython.com 上记入你的账号。这是自我报告的证据,从来不是证书。 +- 提交:测试通过的检查点打印确定性认领码;自报检查点要先回答课后问题,再运行 `python verify.py --attest ID` 才打印码;在 flypython.com 上记入你的账号。这是自我报告的证据,从来不是证书。 ## 文件夹地图 diff --git a/courses/hands-on-with-openai-codex/verify.py b/courses/hands-on-with-openai-codex/verify.py index e13bcec..e424a37 100644 --- a/courses/hands-on-with-openai-codex/verify.py +++ b/courses/hands-on-with-openai-codex/verify.py @@ -187,21 +187,69 @@ def run_progress(as_json, receipt_out=None, default=False): print(f"Wrote {len(receipts)} receipt(s) to {receipt_out}", file=sys.stderr) return 0 +def run_check(as_json=False, attested=()): + """Learner-facing v2 check: run only starter/ and require explicit attestation. + + The published progress command remains the v1 claim-receipt interface. + The reference solution is checked by the maintainer's course validation, + not rerun on every learner check. + """ + selected = set(attested) + allowed = {item["id"] for item in CHECKPOINTS if item["gate"] == "attest"} + unknown = selected - allowed + if unknown: + print("Only self-reported checkpoints can be attested: " + ", ".join(sorted(unknown)), file=sys.stderr) + return 2 + dependency_check = globals().get("_deps_available") + blocked = callable(dependency_check) and not dependency_check() + result = None if blocked else _run_suite("starter") + passed = result is not None and result.returncode == 0 + rows = [] + for item in CHECKPOINTS: + self_report = item["gate"] == "attest" + status = ("attested" if item["id"] in selected else "pending") if self_report else ("blocked" if blocked else "passed" if passed else "open") + rows.append({"id": item["id"], "title": _display_title(item["title"]), + "kind": "self-reported" if self_report else "objective", + "status": status, + "claim_code": _claim_code(item["id"]) if status in ("passed", "attested") else None}) + document = {"v": 2, "course": COURSE_ID, "implementation": "starter", + "suite": {"status": "blocked" if blocked else "passed" if passed else "failed", + "reason": "Install this course's requirements first." if blocked else None}, + "checkpoints": rows} + if as_json: + print(json.dumps(document, ensure_ascii=False, indent=2)) + else: + print("Course " + COURSE_ID) + print("Suites: starter " + ("blocked: install course requirements" if blocked else "passed" if passed else "not passed")) + for row in rows: + print(" " + row["id"] + " " + row["title"] + " [" + row["status"] + "] " + (row["claim_code"] or "—")) + pending = [row["id"] for row in rows if row["status"] == "pending"] + if pending: + print("Confirm completed reflection checkpoints explicitly with --attest ID (repeat for each): " + ", ".join(pending)) + if result is not None and not passed: + print((result.stderr or result.stdout or "").strip()[-3000:], file=sys.stderr) + print("Codes record self-reported progress, not a certificate. / 认领码只记录自报进度,不是证书。") + return 0 if all(row["status"] in ("passed", "attested") for row in rows) else 1 + + def main() -> int: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("implementation", choices=("progress", "starter", "solution"), + parser.add_argument("implementation", choices=("check", "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)") + parser.add_argument("--attest", action="append", default=[], metavar="ID", help="confirm one self-reported checkpoint after doing its work") args = parser.parse_args() - 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 args.implementation in (None, "check"): + if args.receipt_out or args.expect_failure: + parser.error("--receipt-out and --expect-failure are for legacy progress/fixture commands") + return run_check(args.json, args.attest) + if args.attest: + parser.error("--attest is only valid with the learner check command") + 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() diff --git a/courses/hands-on-with-zcode/COURSE.md b/courses/hands-on-with-zcode/COURSE.md index 543765a..c72017a 100644 --- a/courses/hands-on-with-zcode/COURSE.md +++ b/courses/hands-on-with-zcode/COURSE.md @@ -87,7 +87,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` — 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. +- Submission: each test-passed checkpoint prints a deterministic claim code; a reflection checkpoint prints one only after you answer its questions and run `python verify.py --attest ID`; 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 c279e61..992b4be 100644 --- a/courses/hands-on-with-zcode/COURSE_cn.md +++ b/courses/hands-on-with-zcode/COURSE_cn.md @@ -76,7 +76,7 @@ ZCode 的安装与套餐、Remote Control 的配对设置(仅提及)、模 - 徽章:**ZCode 协同复现徽章**(badge id `course-zcode`)——认领全部五个检查点获得。 - 挑战:L01–L05 检查点,各 10 分;在 flypython.com 上集齐五个再加 50 分课程徽章奖励。 - 证据:`python verify.py`——L03(有界改动)与 L04(验证与评审)由套件客观把关;L01/L02/L05 为学习者自我报告。 -- 提交:每个通过的检查点打印一个确定性的认领码;在 flypython.com 上记入你的账号。这是自我报告的证据,从来不是证书。 +- 提交:测试通过的检查点打印确定性认领码;自报检查点要先回答课后问题,再运行 `python verify.py --attest ID` 才打印码;在 flypython.com 上记入你的账号。这是自我报告的证据,从来不是证书。 ## 文件夹地图 diff --git a/courses/hands-on-with-zcode/verify.py b/courses/hands-on-with-zcode/verify.py index 3724123..2353490 100644 --- a/courses/hands-on-with-zcode/verify.py +++ b/courses/hands-on-with-zcode/verify.py @@ -187,21 +187,69 @@ def run_progress(as_json, receipt_out=None, default=False): print(f"Wrote {len(receipts)} receipt(s) to {receipt_out}", file=sys.stderr) return 0 +def run_check(as_json=False, attested=()): + """Learner-facing v2 check: run only starter/ and require explicit attestation. + + The published progress command remains the v1 claim-receipt interface. + The reference solution is checked by the maintainer's course validation, + not rerun on every learner check. + """ + selected = set(attested) + allowed = {item["id"] for item in CHECKPOINTS if item["gate"] == "attest"} + unknown = selected - allowed + if unknown: + print("Only self-reported checkpoints can be attested: " + ", ".join(sorted(unknown)), file=sys.stderr) + return 2 + dependency_check = globals().get("_deps_available") + blocked = callable(dependency_check) and not dependency_check() + result = None if blocked else _run_suite("starter") + passed = result is not None and result.returncode == 0 + rows = [] + for item in CHECKPOINTS: + self_report = item["gate"] == "attest" + status = ("attested" if item["id"] in selected else "pending") if self_report else ("blocked" if blocked else "passed" if passed else "open") + rows.append({"id": item["id"], "title": _display_title(item["title"]), + "kind": "self-reported" if self_report else "objective", + "status": status, + "claim_code": _claim_code(item["id"]) if status in ("passed", "attested") else None}) + document = {"v": 2, "course": COURSE_ID, "implementation": "starter", + "suite": {"status": "blocked" if blocked else "passed" if passed else "failed", + "reason": "Install this course's requirements first." if blocked else None}, + "checkpoints": rows} + if as_json: + print(json.dumps(document, ensure_ascii=False, indent=2)) + else: + print("Course " + COURSE_ID) + print("Suites: starter " + ("blocked: install course requirements" if blocked else "passed" if passed else "not passed")) + for row in rows: + print(" " + row["id"] + " " + row["title"] + " [" + row["status"] + "] " + (row["claim_code"] or "—")) + pending = [row["id"] for row in rows if row["status"] == "pending"] + if pending: + print("Confirm completed reflection checkpoints explicitly with --attest ID (repeat for each): " + ", ".join(pending)) + if result is not None and not passed: + print((result.stderr or result.stdout or "").strip()[-3000:], file=sys.stderr) + print("Codes record self-reported progress, not a certificate. / 认领码只记录自报进度,不是证书。") + return 0 if all(row["status"] in ("passed", "attested") for row in rows) else 1 + + def main() -> int: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("implementation", choices=("progress", "starter", "solution"), + parser.add_argument("implementation", choices=("check", "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)") + parser.add_argument("--attest", action="append", default=[], metavar="ID", help="confirm one self-reported checkpoint after doing its work") args = parser.parse_args() - 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 args.implementation in (None, "check"): + if args.receipt_out or args.expect_failure: + parser.error("--receipt-out and --expect-failure are for legacy progress/fixture commands") + return run_check(args.json, args.attest) + if args.attest: + parser.error("--attest is only valid with the learner check command") + 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() diff --git a/courses/mcp-server-in-python/COURSE.md b/courses/mcp-server-in-python/COURSE.md index 5aa52b0..c46c8b4 100644 --- a/courses/mcp-server-in-python/COURSE.md +++ b/courses/mcp-server-in-python/COURSE.md @@ -75,7 +75,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` - 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. +- Submission: each test-passed checkpoint prints a deterministic claim code; a reflection checkpoint prints one only after you answer its questions and run `python verify.py --attest ID`; 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 e2837fd..73db6a6 100644 --- a/courses/mcp-server-in-python/COURSE_cn.md +++ b/courses/mcp-server-in-python/COURSE_cn.md @@ -67,7 +67,7 @@ course_id: course-mcp-tools - 徽章:**MCP 工具服务徽章**(徽章 id `course-mcp-tools`)——认领全部五个检查点后获得。 - 挑战:L01–L05 检查点各 10 分;五项全部在 flypython.com 认领后另加 50 分课程徽章奖励。 - 证据:`python verify.py` —— L03(校验与错误隔离)与 L04(多轮交互)由测试套件客观判定;L01/L02/L05 为学习者自报。 -- 提交:每个通过的检查点会打印确定性认领码,在 flypython.com 上记入你的账号。这是自我报告的证据,绝不是证书。 +- 提交:测试通过的检查点打印确定性认领码;自报检查点要先回答课后问题,再运行 `python verify.py --attest ID` 才打印码,在 flypython.com 上记入你的账号。这是自我报告的证据,绝不是证书。 ## 文件夹结构 diff --git a/courses/mcp-server-in-python/verify.py b/courses/mcp-server-in-python/verify.py index c46944e..734df16 100644 --- a/courses/mcp-server-in-python/verify.py +++ b/courses/mcp-server-in-python/verify.py @@ -165,21 +165,69 @@ def run_progress(as_json, receipt_out=None, default=False): print(f"Wrote {len(receipts)} receipt(s) to {receipt_out}", file=sys.stderr) return 0 +def run_check(as_json=False, attested=()): + """Learner-facing v2 check: run only starter/ and require explicit attestation. + + The published progress command remains the v1 claim-receipt interface. + The reference solution is checked by the maintainer's course validation, + not rerun on every learner check. + """ + selected = set(attested) + allowed = {item["id"] for item in CHECKPOINTS if item["gate"] == "attest"} + unknown = selected - allowed + if unknown: + print("Only self-reported checkpoints can be attested: " + ", ".join(sorted(unknown)), file=sys.stderr) + return 2 + dependency_check = globals().get("_deps_available") + blocked = callable(dependency_check) and not dependency_check() + result = None if blocked else _run_suite("starter") + passed = result is not None and result.returncode == 0 + rows = [] + for item in CHECKPOINTS: + self_report = item["gate"] == "attest" + status = ("attested" if item["id"] in selected else "pending") if self_report else ("blocked" if blocked else "passed" if passed else "open") + rows.append({"id": item["id"], "title": _display_title(item["title"]), + "kind": "self-reported" if self_report else "objective", + "status": status, + "claim_code": _claim_code(item["id"]) if status in ("passed", "attested") else None}) + document = {"v": 2, "course": COURSE_ID, "implementation": "starter", + "suite": {"status": "blocked" if blocked else "passed" if passed else "failed", + "reason": "Install this course's requirements first." if blocked else None}, + "checkpoints": rows} + if as_json: + print(json.dumps(document, ensure_ascii=False, indent=2)) + else: + print("Course " + COURSE_ID) + print("Suites: starter " + ("blocked: install course requirements" if blocked else "passed" if passed else "not passed")) + for row in rows: + print(" " + row["id"] + " " + row["title"] + " [" + row["status"] + "] " + (row["claim_code"] or "—")) + pending = [row["id"] for row in rows if row["status"] == "pending"] + if pending: + print("Confirm completed reflection checkpoints explicitly with --attest ID (repeat for each): " + ", ".join(pending)) + if result is not None and not passed: + print((result.stderr or result.stdout or "").strip()[-3000:], file=sys.stderr) + print("Codes record self-reported progress, not a certificate. / 认领码只记录自报进度,不是证书。") + return 0 if all(row["status"] in ("passed", "attested") for row in rows) else 1 + + def main() -> int: parser = argparse.ArgumentParser() - parser.add_argument("implementation", choices=("progress", "starter", "solution"), + parser.add_argument("implementation", choices=("check", "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)") + parser.add_argument("--attest", action="append", default=[], metavar="ID", help="confirm one self-reported checkpoint after doing its work") args = parser.parse_args() - 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 args.implementation in (None, "check"): + if args.receipt_out or args.expect_failure: + parser.error("--receipt-out and --expect-failure are for legacy progress/fixture commands") + return run_check(args.json, args.attest) + if args.attest: + parser.error("--attest is only valid with the learner check command") + if args.implementation == "progress": + return run_progress(args.json, args.receipt_out) command = [ sys.executable, diff --git a/courses/verifying-ai-generated-code/COURSE.md b/courses/verifying-ai-generated-code/COURSE.md index b19d3ab..4fa1b4a 100644 --- a/courses/verifying-ai-generated-code/COURSE.md +++ b/courses/verifying-ai-generated-code/COURSE.md @@ -69,7 +69,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` - 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. +- Submission: each test-passed checkpoint prints a deterministic claim code; a reflection checkpoint prints one only after you answer its questions and run `python verify.py --attest ID`; 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 9a33bc0..9ddd4d3 100644 --- a/courses/verifying-ai-generated-code/COURSE_cn.md +++ b/courses/verifying-ai-generated-code/COURSE_cn.md @@ -63,7 +63,7 @@ course_id: course-verify-ship - 徽章:**「能跑」不是证据徽章**(徽章 id `course-verify-ship`)——认领全部五个检查点后获得。 - 挑战:L01–L05 检查点各 10 分;五项全部在 flypython.com 认领后另加 50 分课程徽章奖励。 - 证据:`python verify.py` —— L03(边界修改)与 L04(验证与审查)由测试套件客观判定;L01/L02/L05 为学习者自报。 -- 提交:每个通过的检查点会打印确定性认领码,在 flypython.com 上记入你的账号。这是自我报告的证据,绝不是证书。 +- 提交:测试通过的检查点打印确定性认领码;自报检查点要先回答课后问题,再运行 `python verify.py --attest ID` 才打印码,在 flypython.com 上记入你的账号。这是自我报告的证据,绝不是证书。 ## 文件夹结构 diff --git a/courses/verifying-ai-generated-code/verify.py b/courses/verifying-ai-generated-code/verify.py index 37dcd96..d03b3c6 100644 --- a/courses/verifying-ai-generated-code/verify.py +++ b/courses/verifying-ai-generated-code/verify.py @@ -179,21 +179,69 @@ def run_progress(as_json, receipt_out=None, default=False): print(f"Wrote {len(receipts)} receipt(s) to {receipt_out}", file=sys.stderr) return 0 +def run_check(as_json=False, attested=()): + """Learner-facing v2 check: run only starter/ and require explicit attestation. + + The published progress command remains the v1 claim-receipt interface. + The reference solution is checked by the maintainer's course validation, + not rerun on every learner check. + """ + selected = set(attested) + allowed = {item["id"] for item in CHECKPOINTS if item["gate"] == "attest"} + unknown = selected - allowed + if unknown: + print("Only self-reported checkpoints can be attested: " + ", ".join(sorted(unknown)), file=sys.stderr) + return 2 + dependency_check = globals().get("_deps_available") + blocked = callable(dependency_check) and not dependency_check() + result = None if blocked else _run_suite("starter") + passed = result is not None and result.returncode == 0 + rows = [] + for item in CHECKPOINTS: + self_report = item["gate"] == "attest" + status = ("attested" if item["id"] in selected else "pending") if self_report else ("blocked" if blocked else "passed" if passed else "open") + rows.append({"id": item["id"], "title": _display_title(item["title"]), + "kind": "self-reported" if self_report else "objective", + "status": status, + "claim_code": _claim_code(item["id"]) if status in ("passed", "attested") else None}) + document = {"v": 2, "course": COURSE_ID, "implementation": "starter", + "suite": {"status": "blocked" if blocked else "passed" if passed else "failed", + "reason": "Install this course's requirements first." if blocked else None}, + "checkpoints": rows} + if as_json: + print(json.dumps(document, ensure_ascii=False, indent=2)) + else: + print("Course " + COURSE_ID) + print("Suites: starter " + ("blocked: install course requirements" if blocked else "passed" if passed else "not passed")) + for row in rows: + print(" " + row["id"] + " " + row["title"] + " [" + row["status"] + "] " + (row["claim_code"] or "—")) + pending = [row["id"] for row in rows if row["status"] == "pending"] + if pending: + print("Confirm completed reflection checkpoints explicitly with --attest ID (repeat for each): " + ", ".join(pending)) + if result is not None and not passed: + print((result.stderr or result.stdout or "").strip()[-3000:], file=sys.stderr) + print("Codes record self-reported progress, not a certificate. / 认领码只记录自报进度,不是证书。") + return 0 if all(row["status"] in ("passed", "attested") for row in rows) else 1 + + def main() -> int: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("implementation", choices=("progress", "starter", "solution"), + parser.add_argument("implementation", choices=("check", "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)") + parser.add_argument("--attest", action="append", default=[], metavar="ID", help="confirm one self-reported checkpoint after doing its work") args = parser.parse_args() - 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 args.implementation in (None, "check"): + if args.receipt_out or args.expect_failure: + parser.error("--receipt-out and --expect-failure are for legacy progress/fixture commands") + return run_check(args.json, args.attest) + if args.attest: + parser.error("--attest is only valid with the learner check command") + 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() diff --git a/tools/verify_courses.py b/tools/verify_courses.py index d31009f..1cf4f96 100644 --- a/tools/verify_courses.py +++ b/tools/verify_courses.py @@ -182,12 +182,10 @@ def check_receipt_contract(course: Path) -> list[str]: # ── 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. +# Bare ``python verify.py`` is the learner's command: it runs starter only, +# leaves self-reported checkpoints pending until --attest ID, and exits 1 +# while anything remains unfinished. Published progress/receipt v1 formats +# remain compatible; the check --json command is a separate v2 interface. def check_default_command(course: Path) -> list[str]: @@ -209,8 +207,10 @@ def check_default_command(course: Path) -> list[str]: 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") + if not any("[pending]" in line for line in lines): + problems.append(f"{name}: default command does not leave self-reported checkpoints pending") + if any(re.search(r"\[pending\]\s+[A-Z2-7]{8}", line) for line in lines): + problems.append(f"{name}: default command printed a code for a pending checkpoint") # 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: @@ -219,6 +219,32 @@ def check_default_command(course: Path) -> list[str]: if re.match(r"^[\u4e00-\u9fff]", title): problems.append(f"{name}: default command prints a Chinese-first checkpoint title") break + machine = subprocess.run( + [sys.executable, str(verify), "check", "--json"], + check=False, capture_output=True, text=True, + ) + if machine.returncode != 1: + problems.append(f"{name}: check --json must exit 1 for the shipped starter") + else: + try: + document = json.loads(machine.stdout) + assert document["v"] == 2 and document["implementation"] == "starter" + assert len(document["checkpoints"]) == 5 + assert all(row["claim_code"] is None for row in document["checkpoints"] if row["status"] == "pending") + except (ValueError, KeyError, AssertionError, TypeError): + problems.append(f"{name}: check --json does not provide a valid v2 learner result") + attested = subprocess.run( + [sys.executable, str(verify), "check", "--json", "--attest", "l01"], + check=False, capture_output=True, text=True, + ) + try: + rows = {row["id"]: row for row in json.loads(attested.stdout)["checkpoints"]} + assert attested.returncode == 1 # other checkpoints remain unfinished + assert rows["l01"]["status"] == "attested" + assert re.fullmatch(r"[A-Z2-7]{8}", rows["l01"]["claim_code"]) + assert rows["l02"]["status"] == "pending" and rows["l02"]["claim_code"] is None + except (ValueError, KeyError, AssertionError, TypeError): + problems.append(f"{name}: explicit l01 attestation must print only its own code") return problems