Skip to content

Commit c87db55

Browse files
0.0.7 B: shared-core tool courses + Codex app rewrite
- Rename hands-on-with-openai-codex-cli → hands-on-with-openai-codex and rewrite all content for the Codex desktop app (threads, approvals, AGENTS.md, diff review); course_id course-codex-cli and salt preserved. - Add four new report-tool core courses authored from official docs: hands-on-with-cursor, hands-on-with-deepseek-harness, hands-on-with-kimi-code, hands-on-with-zcode. Each ships EN+ZH COURSE/lessons, honest REVIEW.md (docs-authored, live run-through pending), and a verify.py with its own course_id + salt. - Enforce shared-core byte equality via core-group marker checked by tools/verify_courses.py. - Foundation path: first-agent branch now offers all six tools; m0 orientation and route-challenge rewritten accordingly (any two tools). - tools/claim_receipt.py + verify.py receipt support and build_solution_hashes.py → course-solutions.json (claim receipts A-side). Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent fbe9af6 commit c87db55

161 files changed

Lines changed: 9704 additions & 810 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

content-manifest.json

Lines changed: 623 additions & 71 deletions
Large diffs are not rendered by default.

course-solutions.json

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{
2+
"schema_version": 1,
3+
"courses": [
4+
{
5+
"slug": "agent-rules-single-source",
6+
"course_id": "course-agent-rules",
7+
"solution_sha256": "92757cc9943502021864bab3d508a168a4239b369b8a8ce55d6d3df5d1ca1fb8"
8+
},
9+
{
10+
"slug": "da-eda",
11+
"course_id": "course-da-eda",
12+
"solution_sha256": "9eb0075ac148b788d9756ed4bf45242cb1c085601669cc7f0fe84f3af015397d"
13+
},
14+
{
15+
"slug": "da-report",
16+
"course_id": "course-da-report",
17+
"solution_sha256": "f825baf3a1d4aac1292b5a24f3f2b3465cb02b5e9f79ebda6843c5e560f38629"
18+
},
19+
{
20+
"slug": "da-visualization",
21+
"course_id": "course-da-visualization",
22+
"solution_sha256": "04d39d6f0c09e8c46d83835cf8d838b59a6ea3933828233f59b1b6ea2d935f69"
23+
},
24+
{
25+
"slug": "hands-on-python-with-claude-code",
26+
"course_id": "course-claude-code",
27+
"solution_sha256": "d0aa4b9a5486f9d50abf2fe34f9166815174591e7fc0c2fb23ae3111a5a16699"
28+
},
29+
{
30+
"slug": "hands-on-with-cursor",
31+
"course_id": "course-cursor",
32+
"solution_sha256": "d0aa4b9a5486f9d50abf2fe34f9166815174591e7fc0c2fb23ae3111a5a16699"
33+
},
34+
{
35+
"slug": "hands-on-with-deepseek-harness",
36+
"course_id": "course-deepseek-harness",
37+
"solution_sha256": "d0aa4b9a5486f9d50abf2fe34f9166815174591e7fc0c2fb23ae3111a5a16699"
38+
},
39+
{
40+
"slug": "hands-on-with-kimi-code",
41+
"course_id": "course-kimi-code",
42+
"solution_sha256": "d0aa4b9a5486f9d50abf2fe34f9166815174591e7fc0c2fb23ae3111a5a16699"
43+
},
44+
{
45+
"slug": "hands-on-with-openai-codex",
46+
"course_id": "course-codex-cli",
47+
"solution_sha256": "d0aa4b9a5486f9d50abf2fe34f9166815174591e7fc0c2fb23ae3111a5a16699"
48+
},
49+
{
50+
"slug": "hands-on-with-zcode",
51+
"course_id": "course-zcode",
52+
"solution_sha256": "d0aa4b9a5486f9d50abf2fe34f9166815174591e7fc0c2fb23ae3111a5a16699"
53+
},
54+
{
55+
"slug": "mcp-server-in-python",
56+
"course_id": "course-mcp-tools",
57+
"solution_sha256": "e9a0c348e12bd1f04d83783689f299e3ece8884f3f8a5be2f8a40655e211b790"
58+
},
59+
{
60+
"slug": "verifying-ai-generated-code",
61+
"course_id": "course-verify-ship",
62+
"solution_sha256": "3426cef9b1a5ee074e3839bb152b0ddff186e3c9a18928d99099795654073e16"
63+
}
64+
]
65+
}

courses/agent-rules-single-source/verify.py

Lines changed: 66 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@
1717

1818
ROOT = Path(__file__).resolve().parent
1919

20+
# Optional shared claim-receipt producer (docs/CLAIM-RECEIPT.md). The course
21+
# folder still verifies standalone — without the tools/ sibling or without
22+
# FLYPYTHON_CLAIM_SECRET set, verify.py behaves exactly as before.
23+
try:
24+
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "tools"))
25+
import claim_receipt
26+
except ImportError:
27+
claim_receipt = None
28+
2029
EXPECTED_STARTER_FAILURES = (
2130
"test_thin_pointer_that_references_agents_md_is_a_pointer",
2231
"test_missing_agents_md_is_reported",
@@ -57,9 +66,20 @@ def _run_suite(implementation):
5766
env=environment, check=False, capture_output=True, text=True,
5867
)
5968

60-
def run_progress(as_json):
69+
def _suite_stats(run):
70+
"""(tests_run, duration_ms) for a completed suite subprocess."""
71+
tests = claim_receipt.count_tests(run.stderr) if claim_receipt else 0
72+
return tests, int(getattr(run, "_elapsed_ms", 0))
73+
74+
75+
def run_progress(as_json, receipt_out=None):
76+
import time
77+
t0 = time.monotonic()
6178
starter = _run_suite("starter")
79+
starter_ms = int((time.monotonic() - t0) * 1000)
80+
t0 = time.monotonic()
6281
solution = _run_suite("solution")
82+
solution_ms = int((time.monotonic() - t0) * 1000)
6383
starter_ok = starter.returncode == 0
6484
solution_ok = solution.returncode == 0
6585
rows = []
@@ -79,32 +99,63 @@ def run_progress(as_json):
7999
row["kind"] = kind
80100
row["claim_code"] = code
81101
rows.append(row)
102+
secret = claim_receipt.receipts_enabled() if claim_receipt else None
103+
receipts = []
104+
if secret:
105+
starter_tests = claim_receipt.count_tests(starter.stderr)
106+
solution_tests = claim_receipt.count_tests(solution.stderr)
107+
solution_hash = claim_receipt.solution_sha256(ROOT)
108+
for checkpoint in CHECKPOINTS:
109+
gate = checkpoint["gate"]
110+
if gate == "starter-suite":
111+
passed, tests, ms = starter_ok, starter_tests, starter_ms
112+
elif gate == "both-suites":
113+
passed = starter_ok and solution_ok
114+
tests, ms = starter_tests + solution_tests, starter_ms + solution_ms
115+
else:
116+
continue
117+
receipts.append(claim_receipt.make_receipt(
118+
COURSE_ID, checkpoint["id"], passed=passed, tests=tests,
119+
duration_ms=ms, impl_dir=ROOT / "starter",
120+
solution_hash=solution_hash, secret=secret))
82121
if as_json:
83-
print(json.dumps({"course": COURSE_ID,
122+
document = {"course": COURSE_ID,
84123
"starter_suite_passed": starter_ok,
85124
"solution_suite_passed": solution_ok,
86-
"checkpoints": rows}, ensure_ascii=False, indent=2))
125+
"checkpoints": rows}
126+
if secret:
127+
document["receipts"] = receipts
128+
print(json.dumps(document, ensure_ascii=False, indent=2))
87129
else:
88-
starter_state = "通过" if starter_ok else "未通过"
89-
solution_state = "通过" if solution_ok else "未通过"
90-
print("课程 " + COURSE_ID)
91-
print("实现状态: starter " + starter_state + " / solution " + solution_state)
130+
starter_state = "passed" if starter_ok else "failed"
131+
solution_state = "passed" if solution_ok else "failed"
132+
print("Course " + COURSE_ID)
133+
print("Suites: starter " + starter_state + " / solution " + solution_state)
92134
for row in rows:
93-
state = row["status"] + ("(自报)" if row["kind"] == "attested" else "")
94-
code = "认领码 " + row["claim_code"] if row["claim_code"] else "—"
135+
state = row["status"] + (" (self-attested)" if row["kind"] == "attested" else "")
136+
code = "claim code " + row["claim_code"] if row["claim_code"] else "—"
95137
print(" " + row["id"] + " " + row["title"] + " [" + state + "] " + code)
96-
print("认领码是自我报告的证据,在 flypython.com 记录;绝非证书。")
138+
print("Claim codes are self-reported evidence, recorded at flypython.com; never a certificate.")
139+
if secret:
140+
print(f"Signed run receipts prepared for {len(receipts)} gated checkpoint(s);"
141+
" submit each with its claim to mark it as a local-run receipt.")
142+
if receipt_out and secret:
143+
Path(receipt_out).write_text(
144+
json.dumps({"receipts": receipts}, ensure_ascii=False, indent=2) + "\n",
145+
encoding="utf-8")
146+
print(f"Wrote {len(receipts)} receipt(s) to {receipt_out}", file=sys.stderr)
97147
return 0
98148

99149
def main() -> int:
100150
parser = argparse.ArgumentParser(description=__doc__)
101151
parser.add_argument("implementation", choices=("progress", "starter", "solution"))
102152
parser.add_argument("--json", action="store_true")
103153
parser.add_argument("--expect-failure", action="store_true")
154+
parser.add_argument("--receipt-out", metavar="PATH", help="write signed run receipts JSON (requires FLYPYTHON_CLAIM_SECRET)")
104155
args = parser.parse_args()
105156

106157
if args.implementation == "progress":
107-
return run_progress(args.json)
158+
return run_progress(args.json, args.receipt_out)
108159

109160
command = [sys.executable, "-m", "unittest", "discover", "-s", str(ROOT / "tests")]
110161
environment = os.environ.copy()
@@ -132,6 +183,10 @@ def main() -> int:
132183
"requirement, no drift reporting, and CLI exit codes never signal failure."
133184
)
134185
return 0
186+
if result.returncode == 0:
187+
print(f"{args.implementation}: all tests passed")
188+
else:
189+
sys.stderr.write((result.stderr or "") or (result.stdout or ""))
135190
return result.returncode
136191

137192

courses/da-eda/verify.py

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@
2525

2626
ROOT = Path(__file__).resolve().parent
2727

28+
# Optional shared claim-receipt producer (docs/CLAIM-RECEIPT.md). The course
29+
# folder still verifies standalone — without the tools/ sibling or without
30+
# FLYPYTHON_CLAIM_SECRET set, verify.py behaves exactly as before.
31+
try:
32+
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "tools"))
33+
import claim_receipt
34+
except ImportError:
35+
claim_receipt = None
36+
2837
EXPECTED_STARTER_FAILURES = (
2938
"test_load_returns_dataframe",
3039
"test_load_has_expected_columns",
@@ -84,13 +93,20 @@ def _run_suite(implementation):
8493
)
8594

8695

87-
def run_progress(as_json):
96+
def run_progress(as_json, receipt_out=None):
97+
starter = solution = None
98+
starter_ms = solution_ms = 0
8899
if not _deps_available():
89100
_deps_hint()
90101
starter_ok = solution_ok = False
91102
else:
103+
import time
104+
t0 = time.monotonic()
92105
starter = _run_suite("starter")
106+
starter_ms = int((time.monotonic() - t0) * 1000)
107+
t0 = time.monotonic()
93108
solution = _run_suite("solution")
109+
solution_ms = int((time.monotonic() - t0) * 1000)
94110
starter_ok = starter.returncode == 0
95111
solution_ok = solution.returncode == 0
96112
rows = []
@@ -110,21 +126,51 @@ def run_progress(as_json):
110126
row["kind"] = kind
111127
row["claim_code"] = code
112128
rows.append(row)
129+
secret = claim_receipt.receipts_enabled() if claim_receipt else None
130+
receipts = []
131+
if secret:
132+
starter_tests = claim_receipt.count_tests(starter.stderr) if starter else 0
133+
solution_tests = claim_receipt.count_tests(solution.stderr) if solution else 0
134+
solution_hash = claim_receipt.solution_sha256(ROOT)
135+
for checkpoint in CHECKPOINTS:
136+
gate = checkpoint["gate"]
137+
if gate == "starter-suite":
138+
passed, tests, ms = starter_ok, starter_tests, starter_ms
139+
elif gate == "both-suites":
140+
passed = starter_ok and solution_ok
141+
tests, ms = starter_tests + solution_tests, starter_ms + solution_ms
142+
else:
143+
continue
144+
receipts.append(claim_receipt.make_receipt(
145+
COURSE_ID, checkpoint["id"], passed=passed, tests=tests,
146+
duration_ms=ms, impl_dir=ROOT / "starter",
147+
solution_hash=solution_hash, secret=secret))
113148
if as_json:
114-
print(json.dumps({"course": COURSE_ID,
149+
document = {"course": COURSE_ID,
115150
"starter_suite_passed": starter_ok,
116151
"solution_suite_passed": solution_ok,
117-
"checkpoints": rows}, ensure_ascii=False, indent=2))
152+
"checkpoints": rows}
153+
if secret:
154+
document["receipts"] = receipts
155+
print(json.dumps(document, ensure_ascii=False, indent=2))
118156
else:
119157
starter_state = "passed" if starter_ok else "not passed"
120158
solution_state = "passed" if solution_ok else "not passed"
121159
print("Course " + COURSE_ID)
122160
print("Suites: starter " + starter_state + " / solution " + solution_state)
123161
for row in rows:
124162
state = row["status"] + (" (self-attested)" if row["kind"] == "attested" else "")
125-
code = "claim " + row["claim_code"] if row["claim_code"] else "—"
163+
code = "claim code " + row["claim_code"] if row["claim_code"] else "—"
126164
print(" " + row["id"] + " " + row["title"] + " [" + state + "] " + code)
127-
print("Claim codes are self-reported evidence recorded on flypython.com — never a certificate.")
165+
print("Claim codes are self-reported evidence, recorded at flypython.com; never a certificate.")
166+
if secret:
167+
print(f"Signed run receipts prepared for {len(receipts)} gated checkpoint(s);"
168+
" submit each with its claim to mark it as a local-run receipt.")
169+
if receipt_out and secret:
170+
Path(receipt_out).write_text(
171+
json.dumps({"receipts": receipts}, ensure_ascii=False, indent=2) + "\n",
172+
encoding="utf-8")
173+
print(f"Wrote {len(receipts)} receipt(s) to {receipt_out}", file=sys.stderr)
128174
return 0
129175

130176

@@ -133,10 +179,11 @@ def main() -> int:
133179
parser.add_argument("implementation", choices=("progress", "starter", "solution"))
134180
parser.add_argument("--json", action="store_true")
135181
parser.add_argument("--expect-failure", action="store_true")
182+
parser.add_argument("--receipt-out", metavar="PATH", help="write signed run receipts JSON (requires FLYPYTHON_CLAIM_SECRET)")
136183
args = parser.parse_args()
137184

138185
if args.implementation == "progress":
139-
return run_progress(args.json)
186+
return run_progress(args.json, args.receipt_out)
140187

141188
if not _deps_available():
142189
_deps_hint()
@@ -170,6 +217,10 @@ def main() -> int:
170217
"because the analysis functions are not implemented."
171218
)
172219
return 0
220+
if result.returncode == 0:
221+
print(f"{args.implementation}: all tests passed")
222+
else:
223+
sys.stderr.write((result.stderr or "") or (result.stdout or ""))
173224
return result.returncode
174225

175226

0 commit comments

Comments
 (0)