From f71d81c6f5bc358d07712a5dc8aa24124ef0b283 Mon Sep 17 00:00:00 2001 From: Dan Thompson Date: Fri, 14 Aug 2026 01:51:22 -0500 Subject: [PATCH 1/4] docs: correct the README claims the Python build made wrong MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audited every checkable claim in README.md against the code on main, with the commands run rather than read. Five were wrong or incomplete: - "every script here uses --env-file" — `usage` does not, and never did; it reads a CSV and calls nothing. Narrowed to scripts that need a key. - "Run those from the repo root ... where both languages expect to find usage.csv" — the Python side deliberately does NOT depend on the working directory. pyweather/usage.py resolves the ledger from __file__ and __init__.py finds .env by walking up, precisely so `uv run` lands on the same ledger from anywhere. `uv run weather` from inside pyweather/ works. - the command-parity gate is described as having one exception; it has two kinds (the assistant:streaming rename, and the three npm-only gates), and it also checks that each entry point resolves to a real `def main`. - `typecheck` was missing from the list of npm scripts with no Python counterpart, and pyweather/'s own helpers went unmentioned. - the cost table omitted `agent`, `injection`, and `verify:docs`, and nothing said which commands need WEATHER_API_KEY as well. Gates: typecheck, typecheck:py, verify:docs all exit 0. Co-Authored-By: Claude Opus 5 --- README.md | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 9591386..9cd7db6 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,8 @@ together. That is the argument made physical rather than asserted. ## Quick start You need **Node.js 20.6 or newer** — 20.6 is when `--env-file` arrived, and -every script here uses it. Check with `node --version`. +every script that needs a key uses it to read your `.env`. Check with +`node --version`. ```bash git clone https://github.com/rdtiv/tirocine.git @@ -117,8 +118,11 @@ uv run weather # needs WEATHER_API_KEY, but makes no Claude call uv run dev # the same first Claude call, in Python ``` -Run those from the **repo root**, not from inside `pyweather/` — that is where -`usage.csv` lives, and where both languages expect to find it. +`usage.csv` lives at the **repo root**, next to `package.json`, and both builds +write to that one file. npm gets there for free, because it runs its scripts +from the project root; the Python side does not lean on that — `pyweather/` +resolves both the ledger and `.env` from its own location on disk, so `uv run` +finds them from whichever directory you happened to be standing in. --- @@ -161,11 +165,13 @@ file it teaches ever disagree, CI fails. It checks six things. Every Markdown file in the repo is structurally sound — fences balanced, links resolving. The two languages' command lists stay in sync — every npm script and every Python entry point in `pyproject.toml` names -the other, with the one documented exception below. Then, once per document -that has companion code: the document's code typechecks (including the -earlier version of any file built in stages), no edit instruction tells you to -make a change already present, every finished listing matches the real file -exactly, and nothing in the source tree is left unexplained. +the other, apart from the exceptions documented below, and every entry point +resolves to a module that really defines the function it names. Then, once +per document that has companion code: the document's code typechecks +(including the earlier version of any file built in stages), no edit +instruction tells you to make a change already present, every finished listing +matches the real file exactly, and nothing in the source tree is left +unexplained. Document 2 is held to that standard by `tsc`, document 3 by `pyright`. Adding a second language meant adding a row to a table in `scripts/check-docs.ts`, not a @@ -209,8 +215,13 @@ Document 3 gives every lesson script above a Python counterpart under agent`, `uv run parse`, and so on. The names match on purpose, with one exception: `assistant:streaming` becomes `assistant-streaming`, because a colon isn't legal in a Python entry-point name. `typecheck:py` is document -3's own correctness gate, not a per-script counterpart, and `verify:docs` -isn't mirrored at all — it already checks both trees. +3's own correctness gate, not a per-script counterpart, and `typecheck` and +`verify:docs` aren't mirrored at all — `verify:docs` already checks both trees. + +The four helpers have counterparts too — `pyweather/text.py`, `config.py`, +`usage.py`, `weather.py` — plus one with no TypeScript equivalent: +`pyweather/__init__.py`, which loads `.env` once for the whole package where +the npm scripts each pass `--env-file`. --- @@ -221,11 +232,17 @@ costs in TypeScript, because it is the same call. | Script | Approximate | |---|---| -| `weather`, `typecheck`, `typecheck:py`, `usage` | Free — no Claude call | +| `typecheck`, `typecheck:py`, `verify:docs`, `usage` | Free, and no key needed at all | +| `weather` | Free — needs `WEATHER_API_KEY`, but makes no Claude call | | `dev`, `truncate`, `stream`, `parse`, `models` | A fraction of a cent each | +| `agent`, `injection` | A fraction of a cent — a short tool loop, a few calls | | `bench` | About 2¢ — nine calls across three models, most of it Opus | | `chat`, `assistant`, `assistant:streaming` | Pennies per session | +`agent`, `injection`, `assistant`, and `assistant:streaming` look up live +weather, so they need **both** keys. Everything else that calls Claude needs +only `ANTHROPIC_API_KEY`. + Every call is logged to `usage.csv`, so you never have to guess. **Set a spend limit on your Anthropic account anyway.** A loop with a mistake in From 57d5ee183000ed81309792cc0453302b21bc5f0f Mon Sep 17 00:00:00 2001 From: Dan Thompson Date: Fri, 14 Aug 2026 01:56:06 -0500 Subject: [PATCH 2/4] docs: teach the timeout Part 7 gained but never explained MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The merge added an explicit request timeout to both src/weather.ts and pyweather/weather.py — the one substantive thing Part 7 gained in both languages. verify:docs kept the listings in sync, but it cannot see that the prose around them stopped describing the file. - docs/typescript.md's "Read what you just wrote" still promised "four ideas in that file" and enumerated the pre-merge four, skipping the most transferable one. fetch having NO default timeout is exactly the kind of thing a beginner assumes is handled for them. - docs/python.md's Part 7 comparison table had no row for it, though the defaults genuinely differ in kind: fetch has none, httpx silently has 5s. That divergence is what the table exists to make visible. Prose only — no code block touched. typecheck, typecheck:py, verify:docs all exit 0. Co-Authored-By: Claude Opus 5 --- docs/python.md | 1 + docs/typescript.md | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/python.md b/docs/python.md index d49f822..b9a69cd 100644 --- a/docs/python.md +++ b/docs/python.md @@ -1274,6 +1274,7 @@ Now the comparison this Part exists for. Read it against `src/weather.ts`: | Make an HTTP request | `fetch(url)` | `httpx.get(url)` | | Safe URL encoding | `URLSearchParams` | `params={...}` | | Check before trusting | `if (!response.ok)` | `if not response.is_success` | +| Time limit on the request | `AbortSignal.timeout(10_000)` — **no default at all** | `timeout=10.0` — httpx defaults to 5s | | Raise a failure | `throw new Error(...)` | `raise RuntimeError(...)` | | Read JSON | `await response.json()` | `response.json()` | | Trust the JSON's shape | `data as WeatherApiResponse` | `.model_validate(data)` — **a real check** | diff --git a/docs/typescript.md b/docs/typescript.md index 13312b9..5d3e5cd 100644 --- a/docs/typescript.md +++ b/docs/typescript.md @@ -1363,7 +1363,7 @@ Add that line to the end of `src/weather-test.ts` and run `npm run weather` agai ### Read what you just wrote -Four ideas in that file, and all four transfer to every API you'll ever call: +Five ideas in that file, and all five transfer to every API you'll ever call: **`fetch` makes the HTTP request.** Same thing `curl.exe` did, from inside your program. @@ -1371,6 +1371,8 @@ Four ideas in that file, and all four transfer to every API you'll ever call: **`response.ok` is a check you cannot skip.** If the API returns a 401 or a 404, `fetch` does *not* throw. It hands you a response object with a bad status and moves on. Skipping this check is how you end up with `undefined` errors three functions away from the actual problem. +**`fetch` has no timeout at all.** Not a long one — none. A server that accepts your connection and then goes quiet leaves this call waiting forever, and from Part 9 onward it hangs your tool loop along with it. `AbortSignal.timeout(10_000)` is you saying the limit out loud, because there is no default to inherit. Most HTTP clients in other languages *do* ship one, which is exactly why this is worth knowing rather than assuming. + **The two interfaces are doing different jobs.** `WeatherApiResponse` describes what the *service* sends — their shape, their naming, their `feelslike_f`. `Weather` is what *your* program uses. Keeping them separate means the day you switch weather providers, you change one file and nothing else breaks. That's not beginner over-engineering; it's the reason the next section is easy. From 9cc97943341454f90c2cd6c7073f72653be38cb0 Mon Sep 17 00:00:00 2001 From: Dan Date: Fri, 14 Aug 2026 20:39:28 -0500 Subject: [PATCH 3/4] fix: correct the timeout claim this audit got wrong, in all six places MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR 5 promoted "fetch has no timeout at all" to a Part 7 teaching point. It is false. undici, the HTTP engine behind Node's fetch, defaults headersTimeout and bodyTimeout to 300 seconds. Measured on Node v24.14.1 against a server that accepts the connection and never responds, fetch rejects after 301.0s with UND_ERR_HEADERS_TIMEOUT. The claim lived in six places, not the two a first read found: docs/typescript.md the Part 7 prose, and the src/weather.ts listing docs/python.md the comparison-table row, and the weather.py listing src/weather.ts the comment above the fetch call pyweather/weather.py the comment above the httpx.get call All six now say the same thing: both runtimes ship a default, they differ by 60x (300s against 5s), and they differ in kind as well as size — AbortSignal.timeout is one deadline for the whole call, where httpx applies its timeout to each operation separately. That divergence is a better Part 7 lesson than the absolute it replaces, and it is exactly what the Idea | TypeScript | Python table exists to surface. Also in this pass: - docs/python.md still carried the "run every command from the repository root" instruction this PR deleted from README.md, 380 lines before the LEDGER comment that disproves it. It was the highest-traffic copy. - The comparison table gained the follow_redirects row that the code comment names and the table skipped. - src/weather.ts and pyweather/weather.py headers said "four ideas" where the document now says five. verify:docs strips comments, so no gate sees this. - README: models moved out of the paid bucket (models.list() spends no tokens and logs no row), the --env-file overclaim removed where it had been reintroduced 150 lines after being fixed, the typecheck "not mirrored" sentence narrowed to what is true, uv's directory independence bounded to inside the repo, and the Part 7 summary row updated to match the lesson. Gates: npm run typecheck 0, npm run typecheck:py 0, npm run verify:docs 0. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NsAeobZPagwRXXXDzqE6bW --- README.md | 21 ++++++++++++--------- docs/python.md | 29 ++++++++++++++++++----------- docs/typescript.md | 9 ++++++--- pyweather/weather.py | 28 +++++++++++++++++----------- src/weather.ts | 16 +++++++++------- 5 files changed, 62 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 9cd7db6..8778d36 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,8 @@ uv run dev # the same first Claude call, in Python write to that one file. npm gets there for free, because it runs its scripts from the project root; the Python side does not lean on that — `pyweather/` resolves both the ledger and `.env` from its own location on disk, so `uv run` -finds them from whichever directory you happened to be standing in. +finds them from whichever directory inside the repo you happened to be standing +in. --- @@ -130,8 +131,8 @@ finds them from whichever directory you happened to be standing in. ### You can see what you spend -An API key gets you no dashboard. So every call in every script appends a row to -`usage.csv` — fifteen columns you can open in Excel and add up: +An API key gets you no dashboard. So every call that spends tokens appends a row +to `usage.csv` — fifteen columns you can open in Excel and add up: - **when and what** — `timestamp`, `run_id`, `script`, `model`, `message_id` - **what went in** — `input_tokens`, `cache_read`, `cache_write` @@ -193,7 +194,7 @@ listing pointing at the wrong endpoint. | `npm run truncate` | `src/truncate.ts` | 5 | `max_tokens: 30` cuts the answer off mid-sentence. `stop_reason` is how you find out. | | `npm run bench` | `src/bench.ts` | 6 | Haiku vs Sonnet vs Opus on three tasks of rising difficulty. Time, cost, and quality side by side. | | `npm run usage` | `src/usage-report.ts` | 6 | Reads `usage.csv` and totals it. No API key needed. | -| `npm run weather` | `src/weather-test.ts` | 7 | `fetch`, `await`, and `response.ok`. No AI in this one at all. | +| `npm run weather` | `src/weather-test.ts` | 7 | `fetch`, `await`, `response.ok`, a time limit you set yourself, and two types where you'd expect one. No AI in this one at all. | | `npm run parse` | `src/parse-request.ts` | 8 | Structured output. Stop parsing prose out of model replies. | | `npm run agent` | `src/agent.ts` | 9 | Tools. The model requests; **your code executes**. The loop, hand-written. | | `npm run assistant` | `src/assistant.ts` | 9 | The finished project — a chat loop with a tool loop inside it. | @@ -214,14 +215,15 @@ Document 3 gives every lesson script above a Python counterpart under `pyweather/`, run the same way with `uv run` instead of `npm run` — `uv run agent`, `uv run parse`, and so on. The names match on purpose, with one exception: `assistant:streaming` becomes `assistant-streaming`, because a -colon isn't legal in a Python entry-point name. `typecheck:py` is document -3's own correctness gate, not a per-script counterpart, and `typecheck` and -`verify:docs` aren't mirrored at all — `verify:docs` already checks both trees. +colon isn't legal in a Python entry-point name. Three names have no `uv run` +counterpart at all — `typecheck`, `typecheck:py` and `verify:docs` — because +they are gates rather than lessons. You reach the Python typecheck through +`npm run typecheck:py`, and `verify:docs` already checks both trees at once. The four helpers have counterparts too — `pyweather/text.py`, `config.py`, `usage.py`, `weather.py` — plus one with no TypeScript equivalent: `pyweather/__init__.py`, which loads `.env` once for the whole package where -the npm scripts each pass `--env-file`. +the scripts that need a key pass `--env-file`. --- @@ -234,7 +236,8 @@ costs in TypeScript, because it is the same call. |---|---| | `typecheck`, `typecheck:py`, `verify:docs`, `usage` | Free, and no key needed at all | | `weather` | Free — needs `WEATHER_API_KEY`, but makes no Claude call | -| `dev`, `truncate`, `stream`, `parse`, `models` | A fraction of a cent each | +| `models` | Free — needs `ANTHROPIC_API_KEY`, but only lists models; it spends no tokens | +| `dev`, `truncate`, `stream`, `parse` | A fraction of a cent each | | `agent`, `injection` | A fraction of a cent — a short tool loop, a few calls | | `bench` | About 2¢ — nine calls across three models, most of it Opus | | `chat`, `assistant`, `assistant:streaming` | Pennies per session | diff --git a/docs/python.md b/docs/python.md index b9a69cd..9885624 100644 --- a/docs/python.md +++ b/docs/python.md @@ -148,8 +148,11 @@ with one difference worth knowing: plain `uv sync` may *update* the lockfile if `pyproject.toml` has changed, whereas `npm ci` refuses. The strict equivalent is `uv sync --locked`, which is what CI runs. -> **Run every command from the repository root**, not from inside `pyweather/`. -> That is where `usage.csv` lives and where `npm run` puts you automatically. +> **These work from any directory inside the repo.** `npm run` always puts you +> at the project root; `uv run` runs wherever you are standing. So `pyweather/` +> resolves both `usage.csv` and `.env` from its own location on disk instead — +> you'll write that line yourself in Part 6, and it is why the directory +> doesn't matter here. ### The file TypeScript doesn't have @@ -1206,14 +1209,17 @@ def get_weather(location: str) -> Weather: # params={...} handles the percent-encoding for you, the way # URLSearchParams does in the TypeScript version. # - # httpx ships two defaults that fetch() in src/weather.ts does not: a - # 5-second timeout, and no automatic following of redirects. Both are - # arguably SAFER defaults than fetch's "wait forever, follow anything" — - # but this tutorial's whole point is that the two languages run the same - # program, so this is one of the few places that claim needed help. - # follow_redirects=True matches fetch's behavior; the explicit (longer) - # timeout replaces httpx's silent 5-second one so a slow response fails - # the same way for both readers instead of surprising only this one. + # httpx and fetch() in src/weather.ts disagree about two defaults: how long + # to wait, and whether to follow redirects. httpx gives up after 5 seconds + # and follows nothing; fetch gives up after 300 (undici's default) and + # follows redirects. The timeouts differ in kind as well as size — + # timeout=10.0 gives each operation 10 seconds (connect, read, write, + # pool), where AbortSignal.timeout(10_000) is one deadline for the whole + # call. This tutorial's whole point is that the two languages run the same + # program, so both say 10s out loud: follow_redirects=True matches fetch, + # and the explicit timeout replaces httpx's silent 5-second one so a slow + # response fails the same way for both readers instead of surprising only + # this one. try: response = httpx.get( "https://api.weatherapi.com/v1/current.json", @@ -1274,7 +1280,8 @@ Now the comparison this Part exists for. Read it against `src/weather.ts`: | Make an HTTP request | `fetch(url)` | `httpx.get(url)` | | Safe URL encoding | `URLSearchParams` | `params={...}` | | Check before trusting | `if (!response.ok)` | `if not response.is_success` | -| Time limit on the request | `AbortSignal.timeout(10_000)` — **no default at all** | `timeout=10.0` — httpx defaults to 5s | +| Time limit on the request | `AbortSignal.timeout(10_000)` — one deadline for the whole call; undici defaults to 300s | `timeout=10.0` — 10s *per operation*; httpx defaults to 5s | +| Follow redirects | automatic in `fetch` | `follow_redirects=True` — httpx defaults to off | | Raise a failure | `throw new Error(...)` | `raise RuntimeError(...)` | | Read JSON | `await response.json()` | `response.json()` | | Trust the JSON's shape | `data as WeatherApiResponse` | `.model_validate(data)` — **a real check** | diff --git a/docs/typescript.md b/docs/typescript.md index 5d3e5cd..cfe7ed0 100644 --- a/docs/typescript.md +++ b/docs/typescript.md @@ -1289,8 +1289,11 @@ export async function getWeather(location: string): Promise { const params = new URLSearchParams({ key: apiKey, q: location }); const url = `https://api.weatherapi.com/v1/current.json?${params}`; - // fetch has NO timeout by default — a server that accepts the connection - // and then goes quiet hangs this call forever. Say the limit out loud. + // fetch's default timeout is not one you'd want to inherit: undici, the HTTP + // engine behind it, gives up after 300 seconds. httpx (the Python build) + // gives up after 5. Both builds say 10s out loud, so the number comes from + // the program rather than from whichever engine is underneath — and a server + // that goes quiet doesn't take Part 9's tool loop down with it. const response = await fetch(url, { signal: AbortSignal.timeout(10_000) }); if (!response.ok) { @@ -1371,7 +1374,7 @@ Five ideas in that file, and all five transfer to every API you'll ever call: **`response.ok` is a check you cannot skip.** If the API returns a 401 or a 404, `fetch` does *not* throw. It hands you a response object with a bad status and moves on. Skipping this check is how you end up with `undefined` errors three functions away from the actual problem. -**`fetch` has no timeout at all.** Not a long one — none. A server that accepts your connection and then goes quiet leaves this call waiting forever, and from Part 9 onward it hangs your tool loop along with it. `AbortSignal.timeout(10_000)` is you saying the limit out loud, because there is no default to inherit. Most HTTP clients in other languages *do* ship one, which is exactly why this is worth knowing rather than assuming. +**`fetch`'s timeout is not one you'd want to inherit.** There is one: undici, the HTTP engine behind Node's `fetch`, gives up after 300 seconds. But five minutes is not a limit, it's an outage — and the number appears nowhere in the `fetch` documentation you'd think to read. Python's `httpx` gives up after 5 seconds, sixty times sooner. Neither default is wrong exactly; they just disagree, and a server that accepts your connection and then goes quiet is the case that finds out. `AbortSignal.timeout(10_000)` is you saying the limit out loud, so the number comes from your program instead of from whichever engine happens to be underneath. From Part 9 onward, a request that hangs hangs your tool loop with it. **The two interfaces are doing different jobs.** `WeatherApiResponse` describes what the *service* sends — their shape, their naming, their `feelslike_f`. `Weather` is what *your* program uses. Keeping them separate means the day you switch weather providers, you change one file and nothing else breaks. That's not beginner over-engineering; it's the reason the next section is easy. diff --git a/pyweather/weather.py b/pyweather/weather.py index d1e2ba3..bf13234 100644 --- a/pyweather/weather.py +++ b/pyweather/weather.py @@ -1,17 +1,20 @@ """Part 7 — The same call in Python. No AI in this file at all. -Four ideas in here, and all four transfer to every API you'll ever call: +Five ideas in here, and all five transfer to every API you'll ever call: 1. `httpx.get` makes the HTTP request — same thing curl.exe did, from code. 2. There is no `await`. This is the one real difference from TypeScript. 3. The status check is one you cannot skip. httpx does NOT raise on a 401 or 404; it hands you a response with a bad status and moves on. (Same as fetch. Same as almost every HTTP client.) - 4. The two shapes do different jobs. WeatherApiResponse describes what the + 4. The time limit is yours to set. httpx does have a default — 5 seconds, + applied to each operation separately — but the two languages disagree + about the number, so say it out loud rather than inherit either one. + 5. The two shapes do different jobs. WeatherApiResponse describes what the SERVICE sends. Weather is what YOUR program uses. Keeping them separate means switching providers changes one file. -On the fourth point, compare src/weather.ts: it uses two `interface` +On the fifth point, compare src/weather.ts: it uses two `interface` declarations, which vanish at compile time. Here they are pydantic models, which exist at runtime and actually validate the JSON. That is a real difference in kind, not just syntax — TypeScript's `as WeatherApiResponse` is @@ -77,14 +80,17 @@ def get_weather(location: str) -> Weather: # params={...} handles the percent-encoding for you, the way # URLSearchParams does in the TypeScript version. # - # httpx ships two defaults that fetch() in src/weather.ts does not: a - # 5-second timeout, and no automatic following of redirects. Both are - # arguably SAFER defaults than fetch's "wait forever, follow anything" — - # but this tutorial's whole point is that the two languages run the same - # program, so this is one of the few places that claim needed help. - # follow_redirects=True matches fetch's behavior; the explicit (longer) - # timeout replaces httpx's silent 5-second one so a slow response fails - # the same way for both readers instead of surprising only this one. + # httpx and fetch() in src/weather.ts disagree about two defaults: how long + # to wait, and whether to follow redirects. httpx gives up after 5 seconds + # and follows nothing; fetch gives up after 300 (undici's default) and + # follows redirects. The timeouts differ in kind as well as size — + # timeout=10.0 gives each operation 10 seconds (connect, read, write, + # pool), where AbortSignal.timeout(10_000) is one deadline for the whole + # call. This tutorial's whole point is that the two languages run the same + # program, so both say 10s out loud: follow_redirects=True matches fetch, + # and the explicit timeout replaces httpx's silent 5-second one so a slow + # response fails the same way for both readers instead of surprising only + # this one. try: response = httpx.get( "https://api.weatherapi.com/v1/current.json", diff --git a/src/weather.ts b/src/weather.ts index 528e486..98225e1 100644 --- a/src/weather.ts +++ b/src/weather.ts @@ -1,12 +1,14 @@ // Part 7 — The same call in TypeScript. No AI in this file at all. // -// Four ideas in here, and all four transfer to every API you'll ever call: +// Five ideas in here, and all five transfer to every API you'll ever call: // // 1. `fetch` makes the HTTP request — same thing curl.exe did, from code. // 2. `await` waits for the network. // 3. `response.ok` is a check you cannot skip. fetch does NOT throw on a // 401 or 404; it hands you a response with a bad status and moves on. -// 4. The two interfaces do different jobs. WeatherApiResponse describes what +// 4. The time limit is yours to set. fetch does have a default, but it is +// undici's 300 seconds — say the limit out loud instead of inheriting it. +// 5. The two interfaces do different jobs. WeatherApiResponse describes what // the SERVICE sends. Weather is what YOUR program uses. Keeping them // separate means switching providers changes one file. @@ -49,11 +51,11 @@ export async function getWeather(location: string): Promise { const params = new URLSearchParams({ key: apiKey, q: location }); const url = `https://api.weatherapi.com/v1/current.json?${params}`; - // fetch has NO timeout by default: a server that accepts your connection and - // then says nothing hangs this call forever, and Part 9's tool loop with it. - // httpx (the Python build) ships a 5s default for exactly this reason; both - // builds now say 10s out loud, so neither depends on a default you'd have to - // go and read. + // fetch's default timeout is not one you'd want to inherit: undici, the HTTP + // engine behind it, gives up after 300 seconds. httpx (the Python build) + // gives up after 5. Both builds say 10s out loud, so the number comes from + // the program rather than from whichever engine is underneath — and a server + // that goes quiet doesn't take Part 9's tool loop down with it. const response = await fetch(url, { signal: AbortSignal.timeout(10_000) }); if (!response.ok) { From b10fdf81af07c386aa73464a31fb51fe733286b8 Mon Sep 17 00:00:00 2001 From: Dan Date: Fri, 14 Aug 2026 20:46:11 -0500 Subject: [PATCH 4/4] docs: say what verify:docs cannot see MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit README promised that `verify:docs` catches a `src/` file drifting from its listing in `docs/typescript.md`, and named the exact line it would report. That is true of code and false of comments: `stripTypeScript` and `stripPython` remove comments from BOTH sides before the diff gate compares them, so a teaching comment can drift out of step with its listing — or say something flatly untrue — with all six gates green. This is not incidental. It is how the timeout claim corrected in the previous commit stayed wrong in six places through a passing CI, and in this repo the comments ARE the teaching. The promise now carries its exception. Also: "Every call is logged to usage.csv" had the same defect as the cost table above it — `models` calls the API and logs no row. Narrowed to calls that spend tokens, matching the fix at line 134. Gates: npm run typecheck 0, npm run typecheck:py 0, npm run verify:docs 0. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NsAeobZPagwRXXXDzqE6bW --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8778d36..7c05cbc 100644 --- a/README.md +++ b/README.md @@ -246,7 +246,8 @@ costs in TypeScript, because it is the same call. weather, so they need **both** keys. Everything else that calls Claude needs only `ANTHROPIC_API_KEY`. -Every call is logged to `usage.csv`, so you never have to guess. +Every call that spends tokens is logged to `usage.csv`, so you never have to +guess. **Set a spend limit on your Anthropic account anyway.** A loop with a mistake in it can call the API thousands of times a minute, and you will write one, because @@ -262,7 +263,10 @@ secrets. If you change a file in `src/`, change the matching code block in `docs/typescript.md` too; likewise `pyweather/` and `docs/python.md`. -`verify:docs` will tell you if you forget, and it names the exact line. +`verify:docs` will tell you if you forget, and it names the exact line — with +one blind spot worth knowing: it strips comments from both sides before it +compares, so a teaching comment can drift out of step with its listing while +every gate still passes. Those you have to keep in sync yourself. Changing one language is usually a reason to look at the other. The two builds are meant to stay the same program, and `pyweather/usage.py` in particular