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] 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