From 3257960f7837e97fc7c77f595fdd24ced67c76a3 Mon Sep 17 00:00:00 2001 From: Ethan Hill Date: Tue, 6 Oct 2026 22:40:54 -0400 Subject: [PATCH] docs: record the 2026-10-06 evening rulings and the overnight queue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The owner frontloaded fifteen rulings on 2026-10-06 and slept; five workflows then ran unattended and opened fifteen PRs (nine small fixes, quiz wave 3's six banks, #228 steps 0 and 1 stacked) and merged #645 (#331) and #646 (#330) on his grant. Every ledger write from the night lands here in one PR, per the orchestrator-only rule, so no two lanes could pick the same entry number. The handoff is rewritten against a grounded state (main @ a2ad39f, scratch merge a3cef82 of all fifteen PRs: 1242 passed / 0 failed / 1 skipped), with the predecessor's twelve verified corrections carried in. - docs/audits/2026-10-reentry/rulings.md — §19: the fifteen rulings, the overnight output list, four process notes. - docs/audits/2026-10-reentry/walk-2026-10-07.md — the morning walk: fifteen PRs in order, each with a summary and review pointers; screenshot links on the hub preview. - docs/next-session-handoff.md — rewritten; shipped sections removed, thirteen corrections to the previous draft, the work in order. - docs/codebase-issues.md — #324 #328 #329 #330 #331 resolved with the shipped shapes (#331 records the push:main runs and the SKIPPED-twin wrinkle; its "three chain PRs" is corrected to two); #228 partially addressed (steps 0–1); new #332–#337 from the lanes' by-catch. - docs/content-audit.md — #91 #92 #94 #95 #96 #99 resolved, #93 accepted; new #100–#104. - CLAUDE.md — Workflow: the stacked-PR paragraph says two PRs and names the #645 fix with its two wrinkles; Local preview: the home-relative default, three guard checks and --dry-run from #646; the hero badge pointer relabelled Featured (~L400) and marked a seasonal pick. - docs/site-ideas-and-friction.md — §Quiz expansion heading: wave 3 opened. - docs/audits/2026-10-reentry/dashboard/dashboard-extra.json — mainSha and suite baseline refreshed, the night's rulings and events added. Co-Authored-By: Claude Fable 5.1 --- CLAUDE.md | 55 ++- .../dashboard/dashboard-extra.json | 32 +- docs/audits/2026-10-reentry/rulings.md | 81 +++ .../audits/2026-10-reentry/walk-2026-10-07.md | 71 +++ docs/codebase-issues.md | 192 +++++++- docs/content-audit.md | 157 +++++- docs/next-session-handoff.md | 464 ++++++++++-------- docs/site-ideas-and-friction.md | 2 +- 8 files changed, 787 insertions(+), 267 deletions(-) create mode 100644 docs/audits/2026-10-reentry/walk-2026-10-07.md diff --git a/CLAUDE.md b/CLAUDE.md index 96c0ff61..a0a32529 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1138,10 +1138,13 @@ section headers). as exhaustive, and this step wasn't in the checklist — which is how the README drifted 24 page bullets (plus the Practice counts: 8 quizzes and 3 drills unlisted) behind by 2026-07-01. -6. Consider bumping the home-page hero's `Latest: ` badge - to point at the new tool — `html/index.html`, the - `

` paragraph (~L382). Editorial pick; skip - on small revisions. +6. Consider pointing the home-page hero's `Featured · ` badge + at the new tool — `html/index.html`, the `

` + paragraph (~L400; relabelled from `Latest:` 2026-07-19, owner + decision). Editorial pick; skip on small revisions. As of 2026-10-06 + it is a **seasonal pick** — the Coil Freeze Protection lesson for + freezestat season, with the DDC Workbench anchor preserved in an + HTML comment beside it for the spring revert. 7. Bump `package.json.version` when shipping something notable; the footer reads it via `html/_data/site.js`. A new tool is a minor bump (`1.X.0`); a bug fix is a patch bump (`1.X.Y`). Bump with @@ -1266,7 +1269,7 @@ opt-out. 7. Add the quiz/drill to `README.md`'s Practice groups and bump its count sentence (same reason as tools step 5b — the tour is exhaustive). -8. Same `Latest:` badge + `package.json.version` rules as tools. +8. Same `Featured` badge + `package.json.version` rules as tools. ## Git conventions @@ -1480,17 +1483,25 @@ re-submit); add `--dry-run` to print the URL list without POSTing. opens against its parent's branch (no run), and when the parent merges GitHub retargets the child to `main` with an `edited` event (still no run) — so it shows **no `test` check at all**, `mergeStateStatus` reads -CLEAN, and `gh pr merge` goes through untested. Three chain PRs merged +CLEAN, and `gh pr merge` goes through untested. Two chain PRs merged that way on 2026-10-06 before anyone noticed (a full local suite on -`main` came back clean, 1223/0/1). **Rule:** after a parent merges, -rebase the child onto `origin/main` and force-push (the push is a +`main` came back clean, 1223/0/1). **Fixed on the workflow side +2026-10-06 (PR #645, codebase-issues #331):** `pull_request` now also +listens for `edited`, gated so only a base change runs the job, and +`push: branches: [main]` tests `main` itself after every merge. Two +things to know: a title or body edit still fires `edited`, the gate +skips the job, and the rollup then shows a `test` check with conclusion +SKIPPED **beside** the real run on the same head — read SUCCESS on the +head SHA and ignore the SKIPPED twin. And the automatic-retarget arm +was unproven when this was written (no stacked child had merged since), +so the belt-and-braces **rule stands:** after a parent merges, rebase +the child onto `origin/main` and force-push (the push is a `synchronize`, which runs), then merge only when `gh pr view N --json statusCheckRollup` shows a `test` check with -`SUCCESS` — a green parent is not evidence about the child. The -workflow-side fix is codebase-issues #331. Before a merge session, a -pairwise `git merge-tree --write-tree A B` over the whole queue finds -the adjacent-hunk ledger conflicts GitHub only reports once a parent -has merged. +`SUCCESS` — a green parent is not evidence about the child. Before a +merge session, a pairwise `git merge-tree --write-tree A B` over the +whole queue finds the adjacent-hunk ledger conflicts GitHub only +reports once a parent has merged. ## Local preview & tests @@ -1501,13 +1512,17 @@ has merged. home server's hub docroot, where a rootless Caddy serves it at `https://cfdev.home.arpa/`. **Owner's box only** — it is a home-lab convenience, not part of the deploy path, and it no-ops nowhere - else (the destination guard refuses any path not ending - `/caddy/dashboard/cfdev`). **From a worktree outside the repo's - parent directory, pass `CF_PREVIEW_DIR=$HOME/caddy/dashboard/cfdev`** - — the default resolves `../caddy/dashboard/cfdev` relative to the - checkout, and the suffix guard accepts the sibling folder that - creates, so the script reports "live" while the hub serves the old - build (codebase-issues #330). It publishes a **snapshot, not a + else. **The default destination is home-relative** + (`~/caddy/dashboard/cfdev`, since PR #646 / codebase-issues #330), so + it works from any checkout or worktree; `CF_PREVIEW_DIR` overrides it. + The guard has three checks and refuses on any of them: the path must + end `/caddy/dashboard/cfdev`, sit under `$HOME`, and its + `caddy/dashboard` parent must already exist (the script no longer + creates it — that third check is what catches an in-home lane + worktree, where the old repo-relative default passed the other two + and reported "live" while the hub served the old build). + `npm run publish:preview -- --dry-run` runs every guard and prints + the plan without touching the docroot. It publishes a **snapshot, not a server**: nothing watches, so every build you want to see needs another publish. It also can't exercise the Worker — clean-URL 301s, the legacy tool redirects and `POST /api/contact` are all diff --git a/docs/audits/2026-10-reentry/dashboard/dashboard-extra.json b/docs/audits/2026-10-reentry/dashboard/dashboard-extra.json index d929d55d..722e2879 100644 --- a/docs/audits/2026-10-reentry/dashboard/dashboard-extra.json +++ b/docs/audits/2026-10-reentry/dashboard/dashboard-extra.json @@ -1,6 +1,6 @@ { - "mainSha": "a7d0d46", - "lastMergeDate": "2026-10-06", + "mainSha": "a2ad39f", + "lastMergeDate": "2026-10-07", "banksAtOrAbove": [ { "slug": "bacnet-basics", @@ -44,7 +44,7 @@ "to": "4.147.0" } ], - "suiteBaseline": "**1223 passed, 0 failed, 1 skipped** in 8.3 min on `:9489`, 2026-10-06, against `main` @ `1bf23ce` (after the three chain A merges that had no CI run of their own); chain B and the lesson merged after it, each CI-green on `main`.", + "suiteBaseline": "**1242 passed, 0 failed, 1 skipped in 8.2 min on `:9661`, 2026-10-07, against the scratch merge `a3cef82` of all fifteen open PRs on `main` @ `a2ad39f`** (the owner-walk queue); the 2026-10-06 baseline on `main` @ `1bf23ce` was 1223/0/1.", "rulings": [ "**All sixteen agenda items ruled 2026-10-05**, one by one with pros and cons; the owner reached the same call as the recommendation on every item before reading it. Record: `docs/audits/2026-10-reentry/rulings.md` (PR #611, merged).", "**The arc is picked: freeze season + quizzes.** Coil-freeze-protection lesson first, quiz wave 2 alongside, #228 in the background; F4 case-file mockup at the arc's end; §7.2 after that.", @@ -52,25 +52,15 @@ "**The status grammar gained a `ruled` state:** decided-but-unshipped entries no longer count as decision-class. Decision-class fell from 6 to 2 (#228, #283).", "**PR #602** (Korean localization) closed courteously with the fork route; **#318** Speed Brain off; **#319** AI Crawl Control verified safe.", "**Review-day rulings (2026-10-06, rulings.md §18):** no Td on reheat coils as a teaching default (bank question replaced, D callout tempered); coil valves are two-way *or* three-way; the PICV sentence scoped to *modulating* valves; the ASHRAE 135 paraphrases verified (\"Clauses check out\"); the #228 design-note defaults accepted (\"I like your defaults\"); filter racks are drawn as pleated media site-wide.", - "**Owner on the format:** *\"Forcing both of us to take a look at each PR in sequence may be the most balanced workflow we've used.\"*" + "**Owner on the format:** *\"Forcing both of us to take a look at each PR in sequence may be the most balanced workflow we've used.\"*", + "**Evening rulings before the overnight lanes (2026-10-06, rulings.md §19):** the lesson adopts 35–38 °F (#99); #331 both triggers, merge on green; #328 narrow to scope statements; #324 reword as field advice; #91/#92/#94/#95/#96 fix, #93 no page change; hero badge → the Coil Freeze Protection lesson for the season; quiz wave 3 = six banks in two triples; F4 mockup on hold; #228 step 0 + step 1 as stacked PRs; combined LAN preview for the walk; the Workbench filter rack redrawn pleated." ], "today": [ - "`main` pulled to `e33a825` (PR #601, 2026-09-02) — it had been two commits behind — and then to `178af5c` after the four PRs below merged.", - "Eight merged local branches deleted (four `worktree-agent-*` leftovers, four shipped feature branches); every lane worktree removed afterwards.", - "PR #603 **merged** — 12 codebase-issues headings and all 66 content-audit headings now carry the ledger's own status marker, so status reads from the heading line.", - "PR #604 **merged** — the state map, per-entry appendix and PR #602 assessment live under `docs/audits/2026-10-reentry/`; ledger #322 records the PR #602 ruling.", - "PR #605 **merged** — `npm run status`, the readout this page is rendered from.", - "Full Playwright suite run on a free high port as the session baseline: 1215 passed, 0 failed.", - "Cloudflare dashboard checked by the owner: Speed Brain turned off (#318); AI Crawl Control confirmed on with zero refused requests (#319).", - "PR #606 **merged** — six more ledger headings trued up to the marker grammar, and the two Cloudflare rulings recorded in the ledger.", - "PR #611 **merged** — every ruling written into its ledger entry; the durable record is `docs/audits/2026-10-reentry/rulings.md`.", - "PR #607 opened — the three dependency bumps plus the matching CI Playwright image, green; owner merges (Worker toolchain).", - "Night 1: sixteen cleanup lanes, three quiz banks, the coil-freeze lesson, the two BACnet softenings, the PID aside and the #228 design note all ran as Opus/Fable lanes with adversarial verification; everything that reaches a live page is a PR for the owner, listed under **Pick up here**.", - "Three merge chains were pre-built so nothing conflicts on merge: the ledger-tail chain #619 → #632 → #626 → #630 → #634 (each appends a new ledger entry), and the version/bank chain ending in the lesson PR #620 (see each PR’s *Stacked on*). Merge top-down; GitHub retargets the child when the parent lands.", - "**Day 2 (2026-10-06): the whole queue merged, PR by PR, with the owner.** 25 PRs landed — the 23 from night 1 plus #635 (two lesson sentences from the review) and #636 (Air Handlers filter racks). **The Coil Freeze Protection lesson is live** at forced-air step 3; `main` is v3.91.0; zero open PRs.", - "Before the walk: two PRs had gone CONFLICTING overnight (adjacent-hunk ledger collisions) and chain B would have on first merge — all three fixed by rebase; a stray duplicate ledger entry on #625 was dropped.", - "Process gap caught mid-session: a stacked PR gets **no CI run** when GitHub retargets it to `main`. Three chain A PRs merged that way; a full suite on `main` was clean (1223/0/1), and the chain B children were rebase-pushed before merging. Logged as codebase-issues #331 (owner picks the trigger fix) and as a CLAUDE.md Workflow rule.", - "Wrap-up docs PR #637 (rulings §18, #228 accepted, #90 closed, new #99 / #330 / #331, CLAUDE.md notes) merges on green. LAN preview back on `main`." + "**Overnight 2026-10-06 → 07 (autonomous):** the handoff was verified as a workflow first (140 claims: 127 verified, 12 corrected, 1 unverifiable — counts and framings carried forward), then five sequential workflows ran: nine small-fix lanes, two quiz triples, #228 steps 0–1, and the wrap-up.", + "**Merged on the owner's grant:** #645 (CI: `edited` gate + `push: main`, #331) and #646 (preview-script guard, #330). The first two `push: main` suite runs both passed — `main` had never been tested directly before.", + "**Fifteen PRs wait for the walk:** #639–#644 and #647 (small content fixes, the hero badge, the Workbench filter rack), #648/#649/#650 and #653/#654/#655 (wave 3, all six banks to 15), #651 → #652 (#228 steps 0 and 1, stacked). Zero conflicting pairs in the merge-tree matrix; the scratch merge of all fifteen passed the full suite (see *Repo and toolchain*) and is published at cfdev.home.arpa.", + "Walk notes with review pointers: `docs/audits/2026-10-reentry/walk-2026-10-07.md`. Ledger by-catch from the night: codebase-issues #332–#337, content-audit #100–#104 (docs PR, merged on green).", + "Every lane reported at least one drifted line cite in the entry it closed; CLAUDE.md's Workflow and LAN-preview bullets trued up for #645/#646, the hero-badge pointer relabelled `Featured`." ], "agenda": [ { @@ -282,4 +272,4 @@ "**#633** — arc pick recorded (rulings.md §17), the two skipped annotations, coming-soon scan widened to the \"its own page\" shapes.", "**Day 2, merged with the owner (2026-10-06):** #607 deps · #608 #609 #610 quiz banks · #613 author/breadcrumb · #615 BACnet softenings · #616 PID aside · #617 FAQ rationale · #618 AHU hedge · #619 → #632 → #626 → #630 → #634 (ledger-tail chain) · #621 balancing · #622 narrowed clauses · #625 subhead ids · #631 design note · #612 → #623 → #624 → #627 → #620 (version chain, the lesson) · #635 lesson sentences · #636 filter racks." ] -} \ No newline at end of file +} diff --git a/docs/audits/2026-10-reentry/rulings.md b/docs/audits/2026-10-reentry/rulings.md index 4d982534..79a9ef25 100644 --- a/docs/audits/2026-10-reentry/rulings.md +++ b/docs/audits/2026-10-reentry/rulings.md @@ -265,3 +265,84 @@ sequence may be the most balanced workflow we've used."* **Merged 2026-10-06:** #607 #608 #609 #610 #612 #613 #615 #616 #617 #618 #619 #620 #621 #622 #623 #624 #625 #626 #627 #630 #631 #632 #634 #635 #636. Open PRs after: none. `main` at 3.91.0. + +## 19. Evening rulings before the overnight lanes *(2026-10-06 — frontloaded in the first two hours, then the assistant worked the queue alone)* + +The session opened with `/verify-handoff` on `docs/next-session-handoff.md` +(140 claims: 127 verified, 12 corrected, 1 unverifiable — the corrections +are in the handoff's *Corrections* block) and then put every open call to +the owner in four rounds. He answered each one, then slept; the lanes ran +overnight and their PRs wait for the morning walk. Where a +recommendation was offered, he took it on every item but two (marked). + +1. **content-audit #99 — the lesson adopts 35–38 °F** as the practical + freezestat band (*What Actually Freezes* and protection-stack item 1 in + `coil-freeze-protection.html`); the tool's `bandTxt` stays. *Against + the recommendation*, which had been to relabel the tool. PR #639. +2. **codebase-issues #331 — both triggers**: `pull_request` listens for + `edited` with a base-change gate, AND `push: branches: [main]` runs the + suite on `main` after every merge. **Merge on green granted** (the + recommendation had been to hold it for the walk). PR #645, merged + 2026-10-07 01:14Z; the first `push` runs on `main` followed immediately. +3. **codebase-issues #328 — narrow to scope statements.** All five + *What this page didn't cover* bullets on `bacnet-networking.html` + (segmentation, BACnet/SC, capture-driven troubleshooting, alarms and + events, vendor profiles) plus `bacnet-basics.html`'s "their own future + pages" line. The topics stay listed; the promise goes. PR #643. +4. **codebase-issues #324 — reword as field advice.** The tuner's + cheat-sheet row now says to flip direct ↔ reverse on the real + controller, not on this page. PR #641. +5. **content-audit #91 + #92 — fix, two sentences.** One prose pointer + near the sensor-strip widget to the freeze lesson's 38 °F figure, and + one sentence giving the MA-T-reads-the-coil point in prose (the story + itself stays in the widget). PR #640. +6. **content-audit #94 + #95 — fix.** `pid-basics.html` softens "fast, + clean loop" (duct static is noisy: D has little lag to anticipate and + plenty of noise to amplify) and names the measured variable; the + tuner's Fast row names duct static pressure and mixed-air temperature + as the PVs, with the outputs they drive. PR #641 (with #324 and #329). +7. **content-audit #96 — fix, one disclosure sentence.** "Lockout" is used + at both ends of changeover — high-limit lockout of free cooling, and a + low-ambient / low-limit lockout at the cold end. Name both, legislate + neither (the deadband-convention register). PR #642. +8. **content-audit #93 — no page change.** The damper-stroke caveat is a + candidate question for a later air-handlers wave; the entry stays open + as that pointer. +9. **Home hero badge — feature the Coil Freeze Protection lesson** for + freezestat season; revert to the DDC Workbench in spring (the old + anchor is preserved in an HTML comment beside the badge). PR #644. +10. **Quiz wave 3 — six banks, two batches of three.** `building-pressure`, + `duct-static-control`, `vav-systems`, then `psychrometrics-basics`, + `hydronic-loops`, `load-piping` — freeze-season neighbours of the new + lesson first. Owner merges each bank PR. +11. **F4 case-file mockup — hold** until wave 3 is further along. +12. **#228 — step 0 + step 1 as stacked PRs.** Engine helpers + (`Psychro.mixAir` / `mixFraction`) + `tests/psychro-mixair.spec.js` + + the minor bump first; the psychrometric-chart page with its + before/after fixture stacked on it. Owner merges both; the child is + rebase-pushed after the parent lands (or rides #331's fix). +13. **Morning walk — combined LAN preview.** Every live-page PR merged + into one scratch tree, published to `cfdev.home.arpa`, with full-page + screenshots (dark + light) in the walk notes. +14. **DDC Workbench filter rack — redraw it tonight** with the pleated + idiom (the handoff verification found the Workbench's `#ahu-filter` + still drew the old parallel-line rack; §18.6's "site-wide" had + covered the three lesson racks only). PR #647. +15. **Session shape.** Owner: *"use Sonnet and Opus agents for mechanical + tasks, you judge if Opus is needed based on complexity. But use other + Fable agents for design tasks … let's try to frontload that into the + first 2 hours or so and then I'll want you to work after I go to + sleep."* Merge authority overnight: #330, #331 and the wrap-up docs PR + only; everything that reaches a live page waits for the walk. + +**Overnight output (for the morning walk, in order):** #639 freeze band · #640 air-handlers pointer · #641 pid loops · #642 economizer lockout · #643 bacnet scope · #644 hero badge · #647 workbench filter rack · #650 building-pressure · #648 duct-static-control · #649 vav-systems · #651 psychro step 0 · #652 psych chart step 1 (stacked on #651) · #655 psychrometrics-basics · #654 hydronic-loops · #653 load-piping. Merged on the grant: #645 (#331), #646 (#330). Walk notes: `docs/audits/2026-10-reentry/walk-2026-10-07.md`. + +**Process notes from the night.** (a) PR #645's `push: main` arm ran on +its own merge commit and on #646's and passed both times — the first +suite runs `main` has ever had. (b) A PR body edit now leaves a SKIPPED +`test` check beside the real one (codebase-issues #331 resolution). (c) +The quiz workflow's second refute round can leave an open item that the +Opus fixer then applies without a third refute; both wave-3 batch-1 +open items were applied verbatim and are called out in the walk notes. +(d) Every lane reported at least one drifted line cite in the entry it +was fixing (the 2026-08-12 lesson, again). diff --git a/docs/audits/2026-10-reentry/walk-2026-10-07.md b/docs/audits/2026-10-reentry/walk-2026-10-07.md new file mode 100644 index 00000000..1e6f945b --- /dev/null +++ b/docs/audits/2026-10-reentry/walk-2026-10-07.md @@ -0,0 +1,71 @@ +# Morning walk — 2026-10-07 + +Order: small content fixes first (one file each, fast reads), then the home badge and the Workbench drawing, then the three wave-3 batch-1 banks, then #228's stacked pair, then wave-3 batch-2. Every PR below has a `test` check reading SUCCESS on its head (a SKIPPED twin beside it is a body edit, not a failure). Merge each on your word; nothing here is stacked except #652 on #651. + +Combined preview of every live-page PR: https://cfdev.home.arpa/ (`_built.txt` names the scratch merge commit). Full-page screenshots, dark and light, per changed page: [home](https://cfdev.home.arpa/_walk-2026-10-07/home-dark.png) / [light](https://cfdev.home.arpa/_walk-2026-10-07/home-light.png) · [coil-freeze-protection](https://cfdev.home.arpa/_walk-2026-10-07/coil-freeze-protection-dark.png) / [light](https://cfdev.home.arpa/_walk-2026-10-07/coil-freeze-protection-light.png) · [air-handlers](https://cfdev.home.arpa/_walk-2026-10-07/air-handlers-dark.png) / [light](https://cfdev.home.arpa/_walk-2026-10-07/air-handlers-light.png) · [pid-basics](https://cfdev.home.arpa/_walk-2026-10-07/pid-basics-dark.png) / [light](https://cfdev.home.arpa/_walk-2026-10-07/pid-basics-light.png) · [pid-tuner](https://cfdev.home.arpa/_walk-2026-10-07/pid-tuner-dark.png) / [light](https://cfdev.home.arpa/_walk-2026-10-07/pid-tuner-light.png) · [economizers](https://cfdev.home.arpa/_walk-2026-10-07/economizers-dark.png) / [light](https://cfdev.home.arpa/_walk-2026-10-07/economizers-light.png) · [bacnet-networking](https://cfdev.home.arpa/_walk-2026-10-07/bacnet-networking-dark.png) / [light](https://cfdev.home.arpa/_walk-2026-10-07/bacnet-networking-light.png) · [bacnet-basics](https://cfdev.home.arpa/_walk-2026-10-07/bacnet-basics-dark.png) / [light](https://cfdev.home.arpa/_walk-2026-10-07/bacnet-basics-light.png) · [ddc-workbench](https://cfdev.home.arpa/_walk-2026-10-07/ddc-workbench-dark.png) / [light](https://cfdev.home.arpa/_walk-2026-10-07/ddc-workbench-light.png) · [psychrometric-chart](https://cfdev.home.arpa/_walk-2026-10-07/psychrometric-chart-dark.png) / [light](https://cfdev.home.arpa/_walk-2026-10-07/psychrometric-chart-light.png) — served by the hub beside the preview (they vanish on the next `npm run publish:preview`, which rsyncs with `--delete`; the originals are in the session scratchpad). + +Already merged overnight on your grant: **#645** (CI: `edited` + `push: main`, #331 — both `push: main` runs green) and **#646** (preview-script guard, #330). + +## 1. #639 — coil-freeze: practical freezestat band 35–38 °F (content-audit #99) +`html/education/coil-freeze-protection.html` only. *What Actually Freezes* (`:103`) and protection-stack item 1 (`:115`) now say 35–38 °F (1.7–3.3 °C); the sentence keeps "published guidance clusters between 35 and 37 °F" and says field settings reach 38 at the top. +**Read for:** the 38 °F "at the top" rests on your own practice (the research record traces 38 only to the PR #488 house setting) — it is your claim, not a cited one. Refuter nit: "a national figure" for a two-number range reads oddly; "a national band" would be smoother. + +## 2. #640 — air-handlers: sensor-strip prose points at the freeze lesson and the coil-reading MA-T (content-audit #91, #92) +`html/education/air-handlers.html` only; two sentences appended to the `#sensor-strip` intro paragraph (`:~510`). Refuter round 2 reworded the first so it stays true under #639. +**Read for:** the page now has three in-prose anchors to the freeze lesson (`:503`, `:510`, `:579`); the `:579` ref-note clause "what stands behind it when it is" dangles (content-audit #103 nit) — trim if you like. The widget keys its "freeze-stat territory" fault off *mixed-air* temperature while the lesson's 38 °F is a leaving-air stat setting; the new sentence claims only that the figure is a stat setting (content-audit #103 asks whether to relabel the widget). Spelling: the widget string and bank say "freeze-stat", the lessons "freezestat" (codebase-issues #332 — your call on the house spelling). + +## 3. #641 — pid: measured variable, "clean" softened, wiresheet close anchored (content-audit #94 #95, codebase-issues #324 #329) +Three files: `pid-basics.html:58` + the closing paragraph gets `id="pid-on-a-wiresheet"`; `pid-tuner.html` Fast row `:383` and cheat-sheet row `:423`; `html/_data/quizzes/pid-basics.js` (`learnMore` retargeted; `fast-loop-skips-derivative` prompt/explain reworded to match the lesson — a disclosed scope extension, id and key unchanged). +**Read for:** the Fast row now lists "mixed-air temperature (held by the mixing-box dampers)" — is MAT really a fast loop once damper stroke time is counted? If not, drop it from the row rather than move it (Medium already has three). The Medium row still lists two valve outputs as loops (content-audit #100, same shape, not in this PR). + +## 4. #642 — economizers: "lockout" is used at both ends (content-audit #96) +`html/education/economizers.html:219`, one sentence plus "The simplest version" → "The simplest changeover". +**Read for:** "at both ends of the range" has no antecedent in the paragraph — "at both ends of changeover" would be tighter; the high-limit sense is now stated twice back to back (the surviving parenthetical and the new sentence). Fold if you want it leaner. + +## 5. #643 — bacnet: not-covered bullets become scope statements (codebase-issues #328) +`bacnet-networking.html` (five bullets, Niagara/EBO names gone from the vendor bullet) and `bacnet-basics.html:715`. `npm run status` §5 reports 0 coming-soon candidates on the branch. +**Read for:** "the one place it usually surfaces in the field" (segmentation bullet) slightly overclaims — large RPM / trend reads trip it too. The intro above the list still says "out of scope across" the BACnet lessons while the MS/TP bullet says it has its own page (content-audit #101, one clause). The same promise shape survives on `bacnet-services.html:227` and `bacnet-mstp.html:238` in words the scan does not see (codebase-issues #333) — not this PR. + +## 6. #644 — home: feature the Coil Freeze Protection lesson for freezestat season +`html/index.html:400`; badge text is the lesson description's clause "what stands between a cold morning and a burst tube"; the DDC Workbench anchor is preserved in an HTML comment for the spring revert. `home-hero.spec.js` 7/7. +**Read for:** at 375 px the badge wraps to three lines (was two) — breaks cleanly, no overflow. The fuller "why water coils split, and …" version is a one-string swap if you prefer it. + +## 7. #647 — ddcw-ahu: filter rack as pleated media +`html/simulators/ddc-workbench.html` only: `#ahu-filter` is one zigzag path (26 units wide, 12-unit half-pitch, three full pleats), the `#ahu-desc` sentence about the rack reworded to match, and the media stroke moved from `--border` to `--text-dim` (the lane's one step beyond the brief — the lessons' glyph uses `--text-dim`, and a `--border` zigzag was near-invisible in light). 302 workbench specs green. The fullscreen cockpit reuses the same group; the hidden mockup page is untouched by design. +**Read for:** does it read as a filter at the cockpit's smaller scale (≈8 px per leg at 1440 fullscreen)? Confirm the `--text-dim` stroke. + +## 8. #650 — quiz: building-pressure 10 → 15 +Five new: tracking-in-flow (mcq), tracking-offset-sizing (numeric, 3,200 CFM), vav-deposit-moves (tf), windward-reference (gotcha), duct-static-not-building (mcq). Key spread a2/b5/c3/d1. +**Read for:** the windward-reference trend snippet is invented — replace with one of yours if you have it. `bp-duct-static-not-building` choice (c) reuses a distractor from `bp-pressure-residual`. The "Not the same pressure" callout has no `id` so the question links `#measuring-it` (codebase-issues #337). + +## 9. #648 — quiz: duct-static-control 10 → 15 +Five new: loop-speed (mcq), pressure-profile (tf), cube-law (numeric), tube-stuck-high (gotcha), duct-breach (mcq). +**Read for:** `ds-tube-stuck-high` — the round-2 refuter said the front-end signature is also produced by an honest transducer with the trunk dead-ended downstream of the sensor; the fixer applied its remedy verbatim (prompt asks for "the first thing to suspect"; the explain's manometer line now discriminates both ways). No third refute ran — this is the one question to read closely. All ten original questions key on "a" (codebase-issues #335, bank-wide sweep proposed). + +## 10. #649 — quiz: vav-systems 10 → 15 +Five new: fan-part-load, uncalibrated-gotcha, oa-at-part-flow, chw-vs-dx, tons-per-airflow. +**Read for:** `vav-oa-at-part-flow` explain — the round-2 refuter struck a "not a fixed share of whatever the fan moves" overclaim that contradicted the building-pressure bank; the fixer applied the replacement sentence verbatim ("The code dose is a CFM, not a fraction"). Same no-third-refute caveat as #648. Seven original mcq keys on "a" (#335). The lane also found the engine grades "10,000" as 10 (codebase-issues #334). + +## 11. #651 — psychro: `Psychro.mixAir` / `mixFraction` (#228 step 0, v3.92.0) +Shared script loaded by nine live pages → yours to merge. Private `cp(W)` folded into the three `0.240 + 0.444·W` sites and the inversion; `tests/psychro-mixair.spec.js` (17); the mass basis reproduces `mixStreams` bit for bit (836-pair sweep, 0 mismatches); §1 table reproduced unchanged; `docs/engine-standardization.md` gains a step-0 "Landed" note. +**Read for (two design choices the verifier asks you to confirm):** a missing `P` defaults to `streams[0].state.P`, not sea level, so an altitude state is never re-mixed at sea level; a kernel `ok:false` is returned as-is. Pre-existing by-catch: `buildState` has no positive-`P` guard (codebase-issues #336). + +## 12. #652 — psychrometric-chart on `mixAir` (#228 step 1) — STACKED on #651 +`html/tools/psychrometric-chart.html` only (+14/−9): MA step calls `mixAir` (basis `'mass'`), label "Outdoor air (% by mass)", MA ref-note names the basis and the fog re-solve. Before/after fixture: 21 cases × US/metric, 1,419 fields, byte-identical except past saturation, where the §1 fog corner moves 10.4 → 17.7 °F as #236 intended. Full contrast sweep green. +**Merge order:** after #651 lands, GitHub retargets this to `main`; with #645 in, that retarget should now dispatch a `test` run by itself — this PR is the first live test of that arm. If no run appears within a minute, rebase-push it (the rule in CLAUDE.md) and merge on SUCCESS. + +## 13. #655 — quiz: psychrometrics-basics 10 → 15 +Five new: wet-bulb-floor (mcq), coil-leaving-pair (tf), cooling-dehumidification (mcq), cfm-to-mass-flow (numeric, 44,444 lb/h ±250), pool-glass-at-spec (gotcha on the widget's default case). Every figure reproduced against `psychro-engine.js` by the refuter. +**Read for:** `psy-cfm-to-mass-flow`'s prompt writes "10,000 CFM" with a comma and the engine parses a typed "44,444" as 44 (codebase-issues #334 — the engine fix is the durable one). `psy-pool-glass-at-spec` shares a beat with two existing dew-point questions (not a duplicate, but a 10-of-15 draw can show all three). The lane could not read `quiz-engine.js` (permission classifier refused the path) and built from the bank + spec + DOM instead — worth allowing for future lanes. Original bank keys 8 of 9 on "b" (#335). + +## 14. #654 — quiz: hydronic-loops 10 → 15 +Five new: topology-applies-to-chilled-water (tf), return-layout-tradeoff (mcq), twin-t-mixed-supply-temp (numeric, 148 °F), loads-see-mixed-temp (mcq), injection-pump-stopped (gotcha at 0 Hz on the `#d3` widget). +**Read for:** `loads-see-mixed-temp` — the round-2 refuter found its explain leaked the numeric sibling's answer (148 °F); the fixer applied the replacement sentence verbatim (flows named, result withheld). No third refute — read that explain. Original six mcq/gotcha keys all "b" and always the longest (#335). + +## 15. #653 — quiz: load-piping 10 → 15 +Five new, no numeric by design (the lesson prose carries no arithmetic and the landing pill reads `MCQ · TF · Gotcha`). Gotcha links `#tie-back` because the "See what the bypass does" subhead has no `id`. +**Read for:** the lane's note on a possible lesson gap (bypass floor 25 % and 20 GPM per load are widget constants the prose never states). Original seven keys all "b" (#335). + +## Not in this walk +`docs/engine-standardization.md` wants two follow-ups once #651/#652 land (the §2 table's stale cp line cites; a "Landed" line under §4 step 1) — left out of the docs PR to avoid colliding with #651's hunk. Practice pages were not screenshotted: the bank changes are data, and the page renders a sampled run. + diff --git a/docs/codebase-issues.md b/docs/codebase-issues.md index d8dbe78c..c4b65a89 100644 --- a/docs/codebase-issues.md +++ b/docs/codebase-issues.md @@ -9441,7 +9441,7 @@ prohibited) are itemised in `textContent` rewritten unguarded at 10 Hz (item 18) — shipped with (a) above; see there for why it could not wait. -### 228. Engineering math is re-implemented per page — air mixing carries three disagreeing forms and no shared helper exists *(noticed 2026-07-27, owner direction — scheduled separately, not this arc)* *(ruled 2026-10-06 — the design note's §6 defaults accepted; execution lane pending, `docs/engine-standardization.md` §4)* +### 228. Engineering math is re-implemented per page — air mixing carries three disagreeing forms and no shared helper exists *(noticed 2026-07-27, owner direction — scheduled separately, not this arc)* *(ruled 2026-10-06 — the design note's §6 defaults accepted; execution lane pending, `docs/engine-standardization.md` §4)* *(partially addressed 2026-10-06 · PR #651 step 0, PR #652 step 1; steps 2+ pending)* Owner direction, 2026-07-27: *"The air mixing disagreeing seems like an issue I'd like to fix, with all these things using it, it may be good to @@ -9759,6 +9759,27 @@ purpose — this entry stays open until the owner picks. Owner picked the §6 de `mixFraction`, keep `mixStreams`; no engine caveat text; no workbench mass-delta readout); the execution lane is open to schedule. +**Execution, 2026-10-06 (rulings.md §19.12):** step 0 is PR #651 — +`Psychro.mixAir` / `mixFraction` in `psychro-engine.js`, a private +`cp(W)` folded into the three `0.240 + 0.444·W` sites and the inversion +(`:224/:231/:261/:517` on `a2ad39f`, not the `:209/:216/:246/:502` the +design note cites), `tests/psychro-mixair.spec.js` (17 tests; the mass +basis reproduces `mixStreams` exactly — a 836-pair × 4-weight +`Object.is` sweep found 0 mismatches), the §1 table reproduced from the +engine (all seven rows unchanged) and the minor bump 3.91.0 → 3.92.0. +Design choices the verifier asks the owner to confirm: a missing `P` +defaults to `streams[0].state.P` (not `P_STD`) so an altitude state is +never re-mixed at sea level; a kernel `ok:false` is returned as-is. Step +1 is PR #652, stacked on #651 — `psychrometric-chart.html` calls +`mixAir` (basis `'mass'`), the label reads "Outdoor air (% by mass)", +the MA ref-note names the mass basis and the fog re-solve; the +before/after fixture (21 cases × US/metric, 1,419 fields) is byte- +identical except past saturation, where the §1 fog corner moves 10.4 → +17.7 °F as #236 intended. Steps 2+ (air-mixing, the workbench last, +display-only) are still to schedule. Two doc follow-ups once these +merge, left out of the docs PR to avoid colliding with #651's hunk: the +§2 table's stale cp cites, and a "Landed" line under §4 step 1. By- +catch: #336. ### 229. `#fcu-ovr-state` is a live region rewritten on every 10 Hz host tick *(noticed 2026-07-27; the AHU's twin shipped guarded 2026-07-30 — **RESOLVED 2026-08-09 · PR #493**, COV announcer per the owner-decision note; closing record at the end)* @@ -15581,7 +15602,7 @@ mirror in one snapshot. Measured: that spec 96/96 green, smoke's landed in `ddc-workbench-ahu-page.spec.js` (the spec that drives the built page), not `ddcw-ahu-unit.spec.js`, which is engine-direct. -### 324. The PID tuner's cheat sheet advises "flip acting (direct ↔ reverse)" — a control the tuner does not have *(noticed 2026-10-05, decision sitting — by-catch of the pid-basics direct/reverse aside; LOW, not ruled)* +### 324. The PID tuner's cheat sheet advises "flip acting (direct ↔ reverse)" — a control the tuner does not have *(noticed 2026-10-05, decision sitting — by-catch of the pid-basics direct/reverse aside; LOW, not ruled)* *(ruled 2026-10-06 — reword as field advice)* *(addressed 2026-10-06 · PR #641)* `html/simulators/pid-tuner.html:423` — the Symptom→Move table's row *"PV runs away when the loop acts → flip acting (direct ↔ reverse)"*. @@ -15601,6 +15622,14 @@ toggle to the tuner — bigger, touches the engine and every preset; (friction file, *PID basics — surface direct vs reverse acting?*). Open; not on the 2026-10-05 agenda. +**Resolution (2026-10-06, rulings.md §19.4):** option (1). The row now +reads "wrong action on the real controller — flip direct ↔ reverse +there", so it is advice about the reader's loop and no longer points at +a toggle this page lacks. Shipped on PR #641 with content-audit #94 / +#95 and #329 (one `pid-tuner.html` + `pid-basics.html` lane). Refuter +note: the cell is now the longest in the terse cheat sheet (four lines +at 375 px, no overflow). + ### 325. `buildState` ok:false reaches two unguarded consumers — coil-sizing heating NaN, psych-chart HC stage *(noticed 2026-10-05, #238 fix round)* *(addressed 2026-10-05 · PR #632)* #238's guard makes `Psychro.buildState` return `{ ok: false, error }` for a @@ -15720,7 +15749,7 @@ lifetime), not a disclosure gap. Numbering note: #325 and #326 were already claimed on other branches when this was written, so this entry takes #327. -### 328. `bacnet-networking.html`'s "What this page didn't cover" list promises unbuilt pages in lesson body prose *(noticed 2026-10-05, #314 lane verifier)* +### 328. `bacnet-networking.html`'s "What this page didn't cover" list promises unbuilt pages in lesson body prose *(noticed 2026-10-05, #314 lane verifier)* *(ruled 2026-10-06 — narrow to scope statements)* *(addressed 2026-10-06 · PR #643)* `html/education/bacnet-networking.html`, the `What this page didn't cover` subhead (`:598`) and its bullet list. Two bullets promise a page @@ -15763,7 +15792,18 @@ lane) gives the same `bacnet-networking.html` subhead an `id="not-covered"`; a reword lane should land after it or rebase onto it. Open. -### 329. `pid-basics.html`'s closing "PID block on a wiresheet" paragraph has no `id`, so no quiz `learnMore` can deep-link it *(noticed 2026-10-05, quiz wave-2 pid-basics lane)* +**Resolution (2026-10-06, rulings.md §19.3):** option (1), applied to +all five bullets (segmentation, BACnet/SC, capture-driven +troubleshooting, alarms and events, vendor profiles) plus `bacnet- +basics.html`'s "their own future pages" line. Each bullet keeps its +description of the topic and states why it sits outside this page; no +page is promised, and the vendor names left with the vendor bullet. `npm +run status` §5 reports 0 coming-soon candidates on the branch. The +subhead is `:601` (not `:598`) after PR #625's `id="not-covered"`. Two +sibling lessons carry the same shape in words the §5 regex does not see +— logged as #333. + +### 329. `pid-basics.html`'s closing "PID block on a wiresheet" paragraph has no `id`, so no quiz `learnMore` can deep-link it *(noticed 2026-10-05, quiz wave-2 pid-basics lane)* *(addressed 2026-10-06 · PR #641)* `html/education/pid-basics.html:229-231` — the paragraph that opens *"In a building-automation controller, the PID you've just been tuning lives @@ -15786,8 +15826,16 @@ same PR. No guard proposed, for #314's reason. Open — log-and-fix-opportunistically: any lane already touching `pid-basics.html` takes it. +**Resolution (2026-10-06):** `id="pid-on-a-wiresheet"` on the paragraph +itself (`pid-basics.html:~228`, with a keep-it comment naming it a quiz +deep-link target; no subhead added), and `pid-block-on-wiresheet`'s +`learnMore` retargeted to `/education/pid-basics.html#pid-on-a- +wiresheet` with the label "PID Basics — The PID block on a wiresheet". +The bank's header comment, which said the paragraph had no anchor, was +trued up in the same commit. `link-integrity.spec.js`'s fragment arm +resolves it. -### 330. `publish-preview.mjs`'s destination guard is suffix-only, so a worktree outside the repo's parent publishes to a sibling folder it creates *(noticed 2026-10-06, review-and-merge session; LOW)* +### 330. `publish-preview.mjs`'s destination guard is suffix-only, so a worktree outside the repo's parent publishes to a sibling folder it creates *(noticed 2026-10-06, review-and-merge session; LOW)* *(addressed 2026-10-06 · PR #646, merged)* `.github/scripts/publish-preview.mjs:80-84` resolves the default destination as `../caddy/dashboard/cfdev` relative to the checkout and @@ -15807,7 +15855,22 @@ the guard assert the path is under `$HOME` as well as ending with the suffix; keep `CF_PREVIEW_DIR` as the override. Script only → merge on green. -### 331. `test.yml` never runs for a stacked PR that GitHub retargets to `main` after its parent merges *(noticed 2026-10-06, review-and-merge session; process gap, owner's call on the fix)* +**Resolution (2026-10-06):** the default destination is now +`path.join(os.homedir(), 'caddy', 'dashboard', 'cfdev')` +(`CF_PREVIEW_DIR` still overrides; a relative override still resolves +against the checkout), and the guard has **three** checks, not the two +the fix shape named: the suffix, under-`$HOME` (a lexical +`path.relative` test, no symlink following), and **the `caddy/dashboard` +parent must already exist** — `mkdirSync(DEST)` no longer creates it. +The third check is load-bearing, not polish: from an in-home lane +worktree (`.claude/worktrees/`) the old repo-relative default +resolves to `/.claude/worktrees/caddy/dashboard/cfdev`, which +passes BOTH of the ruled checks and is refused only by the parent check. +A `--dry-run` flag runs every guard and prints the plan without touching +the docroot. Six-case guard matrix in the PR body; CLAUDE.md's LAN- +preview bullet rewritten to match. + +### 331. `test.yml` never runs for a stacked PR that GitHub retargets to `main` after its parent merges *(noticed 2026-10-06, review-and-merge session; process gap, owner's call on the fix)* *(ruled 2026-10-06 — both triggers; merge on green granted)* *(addressed 2026-10-06 · PR #645, merged)* `.github/workflows/test.yml` triggers on `pull_request` with `branches: [main]`, and the default activity types are `opened`, @@ -15816,8 +15879,9 @@ parent's branch, so nothing runs; when the parent merges and GitHub retargets the child to `main`, that is an `edited` event (with `changes.base`), which does not run either. The child shows no `test` check at all, `mergeStateStatus` reads CLEAN, and `gh pr merge` goes -through. Three chain A PRs (#632, #626, #630) merged that way on -2026-10-06 before the gap was noticed. A full local suite on `main` +through. Two chain A PRs (#632, #630) merged that way on 2026-10-06 +before the gap was noticed *(corrected 2026-10-06 — this entry first +said three and named #626, whose merged SHA carries a `test` SUCCESS)*. A full local suite on `main` afterwards was clean (1223 passed, 1 skipped, 0 failed), and the chain B children (#623, #624, #627, #620) were rebased onto `main` and force-pushed before merging — the push is a `synchronize`, which runs. @@ -15830,3 +15894,115 @@ Alternatively (or also) add `push: branches: [main]`, so `main` itself is tested after every merge, not only the PR before it. CI config → owner picks. CLAUDE.md's Workflow section carries the rebase-push rule in the meantime. + +**Resolution (2026-10-06, rulings.md §19.2):** both shapes shipped. +`pull_request` now carries `types: [opened, synchronize, reopened, +edited]` and the job is gated with the **shipped** expression `if: +github.event_name != 'pull_request' || github.event.action != 'edited' +|| github.event.changes.base != null` — the `event_name` clause is not +in the sketch above and is needed so a `push` event (where `action` is +null) passes on its own terms, not by accident. `push: branches: [main]` +runs the suite on `main` after every merge; runs 37556093557 (`6d659c2`, +after #645) and 37556097855 (`a2ad39f`, after #646) are the first two +and both passed. Two observed consequences: (a) a title or body edit +fires `edited`, the gate skips the job, and `gh pr view N --json +statusCheckRollup` lists a `test` check with conclusion SKIPPED +**beside** the real run on the same head — read SUCCESS on the head SHA, +ignore the SKIPPED twin; (b) the automatic retarget `edited` event has +not yet been observed to dispatch a run (no stacked child has merged +since) — PR #652 on #651 is the first test; until then the rebase-push +rule in CLAUDE.md stays as the backstop. CLAUDE.md *Workflow* trued up +(two PRs, fix landed). + +### 332. "freeze-stat" vs "freezestat" — the lessons spell it one way, the widget strings and quiz banks the other *(noticed 2026-10-06, Batch A air-handlers refuter; owner spelling ruling + sweep)* + +Every lesson's prose writes **freezestat** (`coil-freeze-protection.html`, +`air-handlers.html` after PR #640, the glossary entry), while the +visible widget status string in `air-handlers.html` (`:761`, *"Mixed air +near freezing — freeze-stat territory"*), the air-handlers bank +(`html/_data/quizzes/air-handlers.js:196,200`), `economizers.js`, +`sequencing-scenarios.js`, `boolean-logic-latches.html:229` and comments +in `glossary.js` write **freeze-stat** — 22 hyphenated hits site-wide at +`a2ad39f`. PR #640 avoided quoting the widget string in prose so one +sentence would not spell it both ways. Both forms are current in the +trade; the site should pick one. **Fix shape:** owner picks the house +spelling (the lessons' unhyphenated form is the larger set), then one +sweep PR over strings and banks — `quiz-banks.spec.js` is shape-only, so +no spec changes. Open. + +### 333. Two more lessons promise unbuilt work in body prose, in shapes the §5 coming-soon scan does not carry *(noticed 2026-10-06, Batch A bacnet-scope refuter — same family as #328)* + +`html/education/bacnet-services.html:227` — the out-of-scope paragraph +says segmentation is *"worth its own treatment"*; and +`html/education/bacnet-mstp.html:238` — the "What this page didn't +cover" paragraph says a bus simulator is *"on the roadmap as this page's +practice pair"*, names the Controller Wiring Simulator's NET terminals +as *"the seam it will plug into"*, and says BACnet/SC *"will eventually +inherit much of this work"*. Both are plain `

` lesson body prose, so +both sit on the banned side of the 2026-10-05 prominence amendment, and +`npm run status` §5 reports 0 for both: its regex carries the "own +page" shapes added 2026-10-05 but not `worth its own treatment` or `on +the roadmap`. Also pre-existing on `bacnet-networking.html:607-608`: the +intro above the not-covered list says the topics are *"deliberately out +of scope across"* the BACnet lessons while the first bullet says MS/TP is +*"Now its own page"* — one clause ("out of scope on this page") fixes +it. **Fix shape:** narrow the two paragraphs to scope statements per the +#328 ruling (the mstp one is a simulator promise rather than a page +promise, so the owner may want to call it), and add `worth its own +treatment` and `on the roadmap` to the §5 banned-shape set in +`.github/scripts/project-status.mjs` so the scan's zero means more than +it does today. Open. + +### 334. The quiz engine grades a numeric answer typed with a thousands separator as its leading digits *(noticed 2026-10-06, Batch B vav-systems lane; engine, LOW)* + +`html/scripts/quiz-engine.js` reads numeric answers with `parseFloat`, +so a reader who types `10,000` is graded as `10` — and marked wrong with +no hint why. Banks with answers at or above 1,000 are exposed today: +`bacnet-basics` (47808), `building-pressure` (3000 and the new 3200), +`field-wiring-sensors`, `surviving-first-months` (10000). The wave-3 +lanes avoided adding more. **Fix shape:** strip `,` (and the thin/narrow +no-break space a metric reader may paste) before parsing, in one place +in the engine; a one-line test in `quiz-engine`'s spec. Live script on +42 practice pages → owner merges. Open. + +### 335. Several ten-question banks key every choice on "a", and the keyed choice is usually the longest — the engine does not shuffle choices *(noticed 2026-10-06, Batch B duct-static-control + vav-systems lanes; bank-wide sweep wanted)* + +`duct-static-control.js` keys all ten original questions on `a`; +`vav-systems.js` keys all seven of its original mcq/gotcha questions on +`a`, and in six of those the key is also the longest choice (e.g. +`ds-cutout`, 206 characters against 95). `quiz-engine.js` shuffles +question order but presents choices in authored order, so a test-wise +reader learns the letter. The wave-3 lanes kept their five new keys off +`a` and no longer than the longest distractor, which moves the split +only to a10/b1/c1/d1 and a7/b1/c1/d1. **Fix shape:** a sweep over every +bank that reorders choices (ids are per-question, nothing references +them) and trims over-long keys, bank by bank for the owner to read — +the air-handlers refuter did this for one bank in wave 2. No spec +change: `quiz-banks.spec.js` does not read letter distribution, and +`modbus-decoding` is walked deterministically by smoke and must keep its +order. Open. + +### 336. `Psychro.buildState` has no finite/positive-`P` guard, so a bad pressure is reported as "at or above the boiling point" *(noticed 2026-10-06, #228 step 0 verifier; pre-existing, LOW)* + +`html/scripts/psychro-engine.js` `buildState(tdb, W, P)` (`~:218`) takes +`P` positionally and never validates it, while `solveState` does +(`![tdb, second, P].every(isFinite) || P <= 0`). A caller passing `NaN`, +`null` or `0` drives `satHumRatio` to `NaN` and gets the #238 +`ok:false` refusal *"Dry-bulb is at or above the boiling point for this +pressure…"* — a cause-dishonest message in the same family audit-2026-06 +polished out of the over-saturation path. `mixAir` / `mixFraction` +(PR #651) inherit it: an explicit bad `P` passes their `=== undefined` +default check and reaches the kernel. No page hits it today (every +caller `parseFloat`s its inputs first). **Fix shape:** one guard at the +top of `buildState` returning the engine's "Enter numeric values" shape; +a spec line in `psychro-engine.spec.js`. Shared script → owner merges. +Open. + +### 337. `building-pressure.html`'s "Not the same pressure" callout has no `id`, so the new bank question links its parent section *(noticed 2026-10-06, Batch B building-pressure lane; #314 family, log-and-fix-opportunistically)* + +The callout that separates building static from duct static carries no +`id`; `bp-duct-static-not-building` (PR #650) tests exactly that beat +and sends its `learnMore` to `#measuring-it`, the section that hosts it +(the bank's header comment says so). Same shape and same fix as #314 / +#329: one `id` on the callout, retarget the `learnMore` in the same PR. +Any lane touching `building-pressure.html` takes it. Open. diff --git a/docs/content-audit.md b/docs/content-audit.md index 90304a93..7f49cd63 100644 --- a/docs/content-audit.md +++ b/docs/content-audit.md @@ -3285,7 +3285,7 @@ throttling valve and the three-way mixing or diverting valve. Owner's words on the ruling: *"a modulating valve could still be 2 way or 3 way diverting/mixing."* -### 91. air-handlers.html — the 38 °F freeze threshold lives only in the widget; the prose names none *(flagged 2026-10-05, quiz-growth wave-2 refutation round)* +### 91. air-handlers.html — the 38 °F freeze threshold lives only in the widget; the prose names none *(flagged 2026-10-05, quiz-growth wave-2 refutation round)* *(ruled 2026-10-06 — fix)* *(addressed 2026-10-06 · PR #640)* **Location:** `html/education/air-handlers.html` — the sensor-strip widget's `const FREEZE_AT = 38;` (`:676`, commented *"freeze-stat @@ -3305,7 +3305,20 @@ Do not promise the lesson in copy before it is live (CLAUDE.md, *No coming-soon copy*); PR #620 currently adds it to the page's `relatedLinks` only. -### 92. air-handlers.html — the MA-T-reads-the-coil war story exists only as a script string behind the failure preset *(flagged 2026-10-05, quiz-growth wave-2 refutation round)* +**Resolution (2026-10-06, rulings.md §19.5):** one sentence appended to +the `#sensor-strip` intro paragraph (`air-handlers.html:~510`): once the +preset drags the mix below 38 °F (3.3 °C) the strip raises a freeze +fault — that figure is a freezestat setting the Coil Freeze Protection +lesson walks through, and that lesson is the place to read where freeze +trouble starts. The number itself stays owned by the freeze lesson. Line +cites in the entry above have drifted (`FREEZE_AT` is at `:681`, the +status string at `:761`, the anecdote at `:785-787`, the preset button +at `:520`). The widget keys its fault off mixed-air (coil-entering) +temperature while the lesson's 38 °F is a leaving-air freezestat +setting; the sentence claims only that the figure IS a stat setting (see +#103). + +### 92. air-handlers.html — the MA-T-reads-the-coil war story exists only as a script string behind the failure preset *(flagged 2026-10-05, quiz-growth wave-2 refutation round)* *(ruled 2026-10-06 — fix)* *(addressed 2026-10-06 · PR #640)* **Location:** `html/education/air-handlers.html:782` — the `widget-anecdote` HTML string (MA-T *"sawtoothing in lockstep with the @@ -3325,7 +3338,15 @@ the coil reads the coil, not the mix), leaving the war story itself in the widget — or accept, on the ground that the economizers page discloses where it is. -### 93. air-handlers.html — the worked example's "damper flow isn't linear with stroke" caveat is untested *(flagged 2026-10-05, quiz-growth wave-2 refutation round)* +**Resolution (2026-10-06, rulings.md §19.5):** the suggested direction's +first option — one prose sentence after the #91 pointer: the preset also +unlocks a field story whose point holds even if you never press it — a +mixed-air sensor mounted too close to the coil reads the coil, not the +mix. The war story itself stays in the widget's anecdote string. Follow- +on nit: `coil-freeze-protection.html:118` still says the widget "hides" +the story, which is now a slight overstatement. + +### 93. air-handlers.html — the worked example's "damper flow isn't linear with stroke" caveat is untested *(flagged 2026-10-05, quiz-growth wave-2 refutation round)* *(accepted 2026-10-06 — no page change; candidate question for a later air-handlers wave)* **Location:** `html/education/air-handlers.html:352`, the mixing worked example's aside *"calling the commanded position a flow fraction @@ -3345,7 +3366,12 @@ branch …"*). Not logged as a finding. **Suggested direction:** a candidate question for the next air-handlers wave; no page change needed. -### 94. pid-basics.html calls duct static a "fast, clean loop" — in the field it is a famously jittery signal *(flagged 2026-10-05, quiz-growth wave-2 refutation round)* +**Ruling (2026-10-06, rulings.md §19.8):** no page change. The damper- +stroke caveat is a candidate question whenever the air-handlers bank +grows again (it sits at 15 after wave 2); the entry stays as that +pointer. + +### 94. pid-basics.html calls duct static a "fast, clean loop" — in the field it is a famously jittery signal *(flagged 2026-10-05, quiz-growth wave-2 refutation round)* *(ruled 2026-10-06 — fix)* *(addressed 2026-10-06 · PR #641)* **Location:** `html/education/pid-basics.html:58`, the D callout's *When it earns its keep* paragraph — *"On a fast, clean loop (duct @@ -3362,7 +3388,15 @@ direction:** soften "clean" — e.g. "a fast loop (duct pressure), where D has little lag to anticipate and plenty of noise to amplify" — or name the variation (some installs filter it smooth, many don't). -### 95. pid-basics.html and the PID tuner list "VFD speed" as a loop — it is the OUTPUT of the static loop *(flagged 2026-10-05, quiz-growth wave-2 refutation round)* +**Resolution (2026-10-06, rulings.md §19.6):** `pid-basics.html:58` now +reads, in substance, "on a fast loop like duct static pressure — which +the loop holds by commanding fan speed — don't bother: D has little lag +to anticipate and plenty of noise to amplify." The bank's `fast-loop- +skips-derivative` prompt and explain, which carried the same "fast, +clean loop" wording, were reworded in the same PR so the quiz and the +lesson agree (question id and key unchanged). + +### 95. pid-basics.html and the PID tuner list "VFD speed" as a loop — it is the OUTPUT of the static loop *(flagged 2026-10-05, quiz-growth wave-2 refutation round)* *(ruled 2026-10-06 — fix)* *(addressed 2026-10-06 · PR #641)* **Location:** `html/education/pid-basics.html:58` (*"duct pressure, VFD speed"* as examples of a fast loop) and @@ -3380,7 +3414,17 @@ confirmed by grep. **Suggested direction:** name the measured variable output it drives ("duct static, which the loop holds by commanding fan speed"). -### 96. economizers.html defines "lockout" only as the high-limit half of changeover; other surfaces use it for the cold end *(flagged 2026-10-05, quiz-growth wave-2 refutation round)* +**Resolution (2026-10-06, rulings.md §19.6):** the lesson names the +measured variable (above, #94), and the tuner's Fast row (`:383`) now +lists "duct static pressure (held by commanding fan speed)" and "mixed- +air temperature (held by the mixing-box dampers)". Walk pointer: the +refuter questions whether mixed-air temperature belongs in the Fast +bucket once damper stroke time is counted; if the owner agrees, drop it +from the row rather than move it — Medium already carries three +examples. The Medium row itself has the same output-as-loop shape +(#100). + +### 96. economizers.html defines "lockout" only as the high-limit half of changeover; other surfaces use it for the cold end *(flagged 2026-10-05, quiz-growth wave-2 refutation round)* *(ruled 2026-10-06 — fix, one disclosure sentence)* *(addressed 2026-10-06 · PR #642)* **Location:** `html/education/economizers.html:219` — *"you'll also hear high-limit lockout for its other half — the condition that slams @@ -3401,6 +3445,17 @@ cooling; a low-ambient lockout or low-limit at the cold end), in the disclosure register the deadband convention uses — name both senses, don't legislate one. +**Resolution (2026-10-06, rulings.md §19.7):** one sentence after the +high-limit parenthetical at `economizers.html:219`: the field uses +*lockout* at both ends of the range — a high-limit lockout of free +cooling when it is too warm or humid outside, and a *low-ambient* or +*low-limit* lockout at the cold end that drives the dampers back toward +minimum to protect the coil — so when a sequence or a tech says *locked +out*, ask which end they mean. The following sentence's "The simplest +version" became "The simplest changeover" so it does not read as a +version of lockout. Drifted cites: the Workbench branch is at `ddcw-ahu- +unit.js:1846`, the pill text at `economizers.html:526`. + ### 97. practice/economizers.html says "Ten questions" over a 15-question bank *(flagged 2026-10-05, quiz-growth wave-2 refutation round)* *(accepted 2026-10-05 — run claim, not a bank count; see #241)* **Location:** `html/practice/economizers.html:3` (`description`) and @@ -3439,7 +3494,7 @@ grates, the owner's own freeze-season story is the material that would differentiate it. -### 99. coil-freeze-risk.html's `bandTxt` says "Typical settings run 35–38 °F" while the Coil Freeze Protection lesson calls 35–37 °F the device band and 38 a house choice *(flagged 2026-10-06, PR #620 verifier round 3; owner's call)* +### 99. coil-freeze-risk.html's `bandTxt` says "Typical settings run 35–38 °F" while the Coil Freeze Protection lesson calls 35–37 °F the device band and 38 a house choice *(flagged 2026-10-06, PR #620 verifier round 3; owner's call)* *(ruled 2026-10-06 — the lesson adopts 35–38 °F; `bandTxt` stays)* *(addressed 2026-10-06 · PR #639)* **Location:** `html/tools/coil-freeze-risk.html` — the `bandTxt` string in the page script (pre-existing, outside PR #620) vs @@ -3455,3 +3510,91 @@ device. **Suggested direction:** owner picks one — either the tool's `bandTxt` becomes "Typical device bands run 35–37 °F; 38 °F is a common house setting a degree above", or the lesson adopts 35–38 as the practical band. Tool page → owner merges either way. + +**Resolution (2026-10-06, rulings.md §19.1):** the owner chose the +second option, against the recommendation. `coil-freeze-protection.html` +*What Actually Freezes* (`:103`) and protection-stack item 1 (`:115`) +now state the practical freezestat band as 35–38 °F (1.7–3.3 °C); the +sentence keeps the research record's finding that published guidance +clusters at 35–37 °F and says field settings reach 38 °F at the top — +that 38 rests on the owner's own practice (the record traces it to PR +#488's Northeast house setting; no published source gives it), which is +his claim to read on the walk. The tool's `bandTxt` is unchanged. The +research record's device-band evidence is §2 *Freeze-stat practice* +(Honeywell 35, HPAC 37, EAB 35, NIH 37), not "§4" as the entry says; §4 +is *Still not established*. A different string on the tool is logged as +#102. + +### 100. pid-tuner.html's Medium row lists two valve outputs as loops — the #95 shape one row below the fixed one *(flagged 2026-10-06, Batch A pid-loops refuter)* + +**Location:** `html/simulators/pid-tuner.html:389`, the Loop Speed +Reference table's Medium row — *"Discharge air temperature, chilled-water +valve, hot-water valve"*. **Lens:** working engineer. **Issue:** the same +output-named-as-loop shape content-audit #95 removed from the Fast row: +the chilled- and hot-water valves are what the discharge-air loop +commands, not loops of their own. **Verification:** confirmed by reading +the row on PR #641's branch. **Suggested direction:** "Discharge-air +temperature (held by the chilled- or hot-water valve)", matching the +fixed Fast row's phrasing. Also noted: at 375 px the table scrolls +sideways inside `.table-scroll` with the examples column ~70 px wide, so +the longer Fast cell wraps to ~9 lines — pre-existing wrapper behaviour, +cosmetic. + +### 101. bacnet-networking.html's not-covered intro says the topics are out of scope "across" the BACnet lessons while its first bullet says MS/TP has its own page *(flagged 2026-10-06, Batch A bacnet-scope refuter)* + +**Location:** `html/education/bacnet-networking.html:607-608` (the +intro above the *What this page didn't cover* list) vs `:614-618` (the +MS/TP bullet, *"Now its own page"*). **Lens:** reader. **Issue:** a +one-clause contradiction — the list is out of scope on *this* page, not +across the shelf, and the alarms bullet now says alarms are "object-model +and services territory" while `bacnet-basics.html:714` and +`bacnet-services.html:227` both disclaim them too, so no BACnet lesson +owns alarms. **Verification:** confirmed by reading all three pages. +**Suggested direction:** "deliberately out of scope on this page"; and +note alarms/event notification as unowned across the BACnet shelf in +the friction file rather than promising it anywhere. The promise-shaped +copy on the two sibling lessons is codebase-issues #333. + +### 102. coil-freeze-risk.html's mute text calls 38 °F "the common default" while the lesson says stats ship set around 35 °F *(flagged 2026-10-06, Batch A freeze-band refuter)* + +**Location:** `html/tools/coil-freeze-risk.html:484`, the muted hint +*"38 °F (3.3 °C) is the common default"*. **Lens:** working engineer. +**Issue:** "default" reads as the device's factory setting, and the +lesson (after #99) says the stats ship around 35 °F with field settings +reaching 38 at the top; the tool means its own input default. The +`bandTxt` string the owner held as-is under #99 is a different string. +**Verification:** confirmed by grep. **Suggested direction:** "a common +field setting" or "this tool's starting point". Tool page → owner +merges. + +### 103. air-handlers.html's sensor-strip widget models a mixed-air low-limit but labels it freeze-stat territory *(flagged 2026-10-06, Batch A air-handlers refuter; owner glance)* + +**Location:** `html/education/air-handlers.html:759-761` — the fault +branch keys off MA-T < 38 °F (the coil-entering mix) and the failure +preset runs in heat mode, so the DA-T readout sits at the heating target +while the status announces *"freeze-stat territory"*; +`coil-freeze-protection.html:115` places the freezestat on the coil's +leaving-air side. **Lens:** working engineer. **Issue:** the widget is +modelling a mixed-air low-limit (a legitimate protection, the lesson's +own protection-stack item) and naming it as the stat. PR #640's prose +pointer was written so it claims only that 38 °F IS a stat setting the +freeze lesson walks through, never that the widget measures what the +stat measures. **Verification:** confirmed by reading the widget script. +**Suggested direction:** either relabel the status string ("mixed-air +low-limit territory") or leave the widget and let the lesson pointer +carry the distinction — the owner's call, since the widget is his field +story. Adjacent nit: the ref-note at `:579` ends *"and Coil Freeze +Protection is what stands behind it when it is"* — the trailing "when it +is" has no clear antecedent. + +### 104. psychrometric-chart.html's MA ref-note says the mixed point "falls on the straight line between OA and RA" — true only to ~0.8 °F *(flagged 2026-10-06, #228 step 1 verifier; LOW)* + +**Location:** `html/tools/psychrometric-chart.html:~337`, the MA +ref-note's pre-existing sentence. **Lens:** working engineer. **Issue:** +in tdb–W space the cp-weighted exact dry-bulb sits up to ~0.84 °F off +the OA–RA chord across the design note's §1 sweep (the chart's drag +inversion projects onto the chord, which is consistent with a mass +fraction since W is mass-linear). Invisible at chart scale; a (b)-class +prose claim. **Verification:** by computation in the step-1 verify +round. **Suggested direction:** "on (very nearly) the straight line". +Not this PR's wording; take it with step 2 of #228. diff --git a/docs/next-session-handoff.md b/docs/next-session-handoff.md index 11f576f6..021e30f7 100644 --- a/docs/next-session-handoff.md +++ b/docs/next-session-handoff.md @@ -1,233 +1,277 @@ -# Session handoff — Coil Freeze Protection live, the night-1 queue cleared (2026-10-06) - -> **Lifecycle:** written 2026-10-06 at the end of the review-and-merge -> session, superseding nothing in this slot — the previous rolling brief -> was retired 2026-08-21 (`docs/glossary-arc.md`), and the 2026-10-05 -> re-entry worked from `docs/audits/2026-10-reentry/` plus `npm run status` -> instead. Retire this file when the owner has ruled the items under -> *Decisions waiting on the owner* **and** quiz wave 3's first PRs are -> open; archive it to `docs/audits/2026-10-reentry/handoff.md` with a -> disposition header when you do. +# Session handoff — fifteen PRs queued for the morning walk; #330 and #331 merged (2026-10-07) + +> **Lifecycle:** written 2026-10-07 (early, at the end of the 2026-10-06 +> overnight session), superseding the 2026-10-06 brief. Out of that brief, +> items 3 and 4 (the small fixes) and the first half of items 1 and 2 +> (quiz wave 3, #228 steps 0–1) shipped as PRs and had their sections +> removed; its *Decisions waiting* block was ruled in full +> (`docs/audits/2026-10-reentry/rulings.md` §19). Retire this file when +> the morning walk has merged or closed the fifteen PRs below **and** +> either #228 step 2 or the next quiz wave has opened; archive it to +> `docs/audits/2026-10-reentry/handoff.md` with a disposition header. ## Read this first -Every claim in this file is a hypothesis. The repo is the truth: run -`npm run status` for the ledger and content numbers, `npm run dashboard` -for the rendered page, and `/verify-handoff` before dispatching anything -from here. The predecessor records failed this session in a specific way: -**plausible procedural facts that nobody had tested** — GitHub's green -check on a stacked PR, the preview script's "live at" message, a ledger -entry's "no longer reachable" claim. Each cost real time; each is in -*Corrections* below so no lane rediscovers it. +Every claim in this file is a hypothesis. The repo is the truth: `npm run +status` for ledger and content numbers, `npm run dashboard` for the +rendered page, `/verify-handoff` before dispatching anything from here. +The predecessor failed in one specific way, measured: **140 claims, 127 +verified, 12 corrected, 1 unverifiable** (workflow `wf_40a4ec5c-c8d`, +two independent refuters on every correction) — and every correction was +a **count or framing carried forward** (stale SHA, "25 PRs" for a 36-PR +window, "three" untested chain PRs for two, "three" ledger collisions +for four). None invalidated the work; all cost a lane time. Separately, +**every overnight lane found at least one drifted line cite** in the +ledger entry it was closing. Cite the commit, derive the count, and tell +the lane the brief is a hypothesis. ## Where things stand -`main` @ `510d5e6`, **v3.91.0**, clean tree, **0 open PRs**, no remote -branch but `main`. (Measurements below cite `510d5e6`, the commit they -were taken at — that is deliberate, not stale.) Counts: **41 education -lessons · 34 content quizzes + 7 field drills · 32 tools · 9 canonical -simulators** (plus the hidden AHU depiction mockup under -`html/simulators/`). - -**Shipped 2026-10-05 → 06** (25 PRs, every one reviewed with the owner -in sequence — `docs/audits/2026-10-reentry/rulings.md` §18 has the list -and his words): - -- **The Coil Freeze Protection lesson** (#620) — forced-air step 3, - written against `docs/warm-climate-freeze-protection.md`, every chapter - surface plumbed, version → 3.91.0. The owner's one review catch was the - capstone's filter rack; both Air Handlers racks got the same pleated - redraw (#636). -- **Quiz wave 2** (#608 #609 #610): pid-basics, air-handlers, economizers - at 15. The owner replaced one question outright (derivative on a reheat - coil) and reworded another; both lesson sentences they came from were - trued up (#635). -- **Sixteen agenda rulings executed** (#612 #613 #615 #616 #617 #618 #619 - #621 #622 #623 #624 #625 #626 #627 #630 #632) plus the ledger-tail - by-catch (#634), the deps bump (#607), and the #228 design note (#631, - defaults accepted). -- Ledger after the day: `codebase-issues.md` to **#331**, `content-audit.md` - to **#99**; `npm run status` reports the open/partial set (the dashboard - showed 21 at `a7d0d46`; derive it fresh). - -**Durable state:** rulings §1–18 · `npm run status` · the dashboard -(`npm run dashboard` renders it to `_dashboard/dashboard.html`; the -standing Artifact is https://claude.ai/artifact/GmJYbFLQMrfPhBXvvniwsz — -from a new conversation the Artifact tool must `read` that URL once before -it can publish to it; the hand-authored narrative it renders lives in -`docs/audits/2026-10-reentry/dashboard/dashboard-extra.json`). - -## Corrections to the previous records — do not rediscover these - -1. **A stacked PR shows no CI and merges anyway.** `test.yml` runs on - `pull_request` pushes to a PR whose base is `main`; a child opened - against its parent's branch never runs, and GitHub's retarget on - parent-merge is an `edited` event that does not run either. Three - chain PRs merged untested on 2026-10-06 (a full suite on `main` was - clean, 1223/0/1). **Rebase the child onto `origin/main` and force-push - before merging; look for a `test` check reading `SUCCESS`, not a green - parent.** Fix shape: codebase-issues #331 (owner picks). -2. **GitHub's `mergeable` on a stacked PR is against the PARENT.** Two - night-1 PRs went CONFLICTING overnight and chain B would have on first - merge — adjacent-hunk ledger collisions GitHub reports only once the - parent lands. A pairwise `git merge-tree --write-tree A B` matrix over - the whole queue found all three before the session started. -3. **`publish-preview.mjs` run from a scratchpad worktree reports "live" - while publishing to a sibling folder it created** — the default - destination is `../caddy/dashboard/cfdev` relative to the checkout and - the guard is suffix-only. Pass - `CF_PREVIEW_DIR=$HOME/caddy/dashboard/cfdev` from any worktree - (codebase-issues #330; CLAUDE.md *Local preview & tests*). -4. **Two lanes appended "#325" to the ledger tail.** Any PR that appends a - new numbered entry conflicts with every other such PR, and two lanes - can pick the same number. Stack them in entry order or let the - orchestrator write the ledger (the handoff skill's standing rule). -5. **Ledger entries carried wrong premises** (verify-the-entry-first held - again): #238 said the AHU no longer reached the above-boiling region — - its heating coil relied on it at a starved fan (#619 adapted the coil); - `psychro-engine.js` has **nine** live consumers, not eight (CLAUDE.md - trued up on this PR); #314 named the wrong second question (one was - COV and stayed put); the #88 brief's IANA 47808–47823 block does not - exist (only 47808 is registered); the PID-aside ruling's premise "every - mini-sim is a heating loop" was wrong (Fast is duct static — still - reverse-acting, so the aside held); the boiling-guard lane brief's - altitude case (200 °F at 12.2 psia) was below boiling, not above. -6. **A drawing idiom does not survive a change of aspect.** The two-stroke - filter hatch read fine in a 70 px rack and as "two lines crossing at an - odd angle" in a 150 px one. Filter racks are now pleated media - site-wide (#620, #636); check any copied SVG idiom at the new size. +`main` @ `a2ad39f`, **v3.91.0** (3.92.0 arrives with PR #651), clean tree, +**15 open PRs** (all opened overnight; one stacked pair). (Measurements +below cite `a2ad39f`, the commit they were taken at — that is deliberate, +not stale.) Counts, derived: **41 education lessons · 34 content quizzes ++ 7 field drills · 32 tools · 9 canonical simulators** (10 files under +`html/simulators/`; the tenth is the hidden AHU depiction mockup). +Quiz banks: 41, **34 under the 15 target on `main`** (30 at 10, 3 at 11, +1 at 13; `modbus-decoding` frozen by design) — **28 once the six wave-3 +PRs merge** (measured on the scratch merge: 28 under — 24 at 10, 3 at 11, 1 at 13; 479 questions in 41 banks; `npm run status` §5 coming-soon candidates 0; 135 canonical pages, `tests/pages.js` in sync). + +**Merged overnight on the owner's grant (2026-10-06 rulings §19.2, +§19.15):** + +- **#645** — `test.yml` listens for `edited` (gated to base changes) and + runs on `push: main`; codebase-issues #331 resolved. The first two + `push: main` suite runs (37556093557 on `6d659c2`, 37556097855 on + `a2ad39f`) both passed — `main` had never been tested directly before. +- **#646** — `publish-preview.mjs` resolves its default against the home + directory and guards with three checks (suffix, under `$HOME`, parent + exists) plus `--dry-run`; codebase-issues #330 resolved. CLAUDE.md's + LAN-preview bullet rewritten to match. + +**Open, for the walk — `docs/audits/2026-10-reentry/walk-2026-10-07.md` +has the per-PR summary and review pointers, in order:** + +| PR | What | Merge | +|---|---|---| +| #639 | coil-freeze lesson: practical band 35–38 °F (content-audit #99) | owner | +| #640 | air-handlers: sensor-strip prose → freeze lesson + coil-reading MA-T (#91, #92) | owner | +| #641 | pid-basics `:58` + tuner Fast/cheat-sheet rows + bank retarget (#94 #95, #324 #329) | owner | +| #642 | economizers: "lockout" at both ends, one sentence (#96) | owner | +| #643 | bacnet-networking five bullets + bacnet-basics line → scope statements (#328) | owner | +| #644 | home hero badge → Coil Freeze Protection (seasonal; Workbench anchor kept in a comment) | owner | +| #647 | DDC Workbench `#ahu-filter` → one pleated zigzag path | owner | +| #650 #648 #649 | wave 3 batch 1: building-pressure, duct-static-control, vav-systems → 15 | owner | +| #651 | `Psychro.mixAir` / `mixFraction`, `tests/psychro-mixair.spec.js`, 3.92.0 (#228 step 0) | owner | +| #652 | psychrometric-chart on `mixAir`, "% by mass" (#228 step 1) — **stacked on #651** | owner, after #651 | +| #655 #654 #653 | wave 3 batch 2: psychrometrics-basics, hydronic-loops, load-piping → 15 | owner | + +Every PR has its own `test` check SUCCESS on its head (re-check with +`gh pr view N --json statusCheckRollup`; a SKIPPED entry beside it is a +body edit, see *Process notes*). A pairwise `git merge-tree --write-tree` +matrix over all fifteen found **zero** conflicting pairs, and a scratch +merge of all of them built clean and passed the full suite: +**1242 passed, 0 failed, 1 skipped in 8.2 min on `:9661`, 2026-10-07, against the scratch merge `a3cef82` of all fifteen open PRs on `main` @ `a2ad39f`**. The combined preview of that scratch merge is published at +`https://cfdev.home.arpa/` (`_built.txt` names the scratch commit; dirty +by design — it is unmerged work). + +**Ledger after the night** (on the docs PR, merged on green): +`codebase-issues.md` to **#337**, `content-audit.md` to **#104**; +rulings.md gains **§19** (the fifteen evening rulings and the overnight +output). `npm run status` on `main` still reports the two +`bacnet-networking.html` coming-soon candidates — they are #643's +change; the scratch merge reports 0. + +## Corrections to the previous draft — do not rediscover these + +1. **"Three chain PRs merged untested" was two** (#632, #630; #626's + merged SHA carries a `test` SUCCESS). Fixed in CLAUDE.md *Workflow*, + codebase-issues #331 and rulings §18 is left as written with the + correction recorded in #331. +2. **"Shipped 2026-10-05 → 06 (25 PRs)" conflated two sets.** 35 PRs + (#603–#637) merged in that window plus #638 at 00:03Z on the 7th; the + 25 is rulings §18's walked-with-the-owner set. The 16 PRs the brief + listed as "sixteen agenda rulings executed" execute **nine** rulings + (eight PRs); the other eight close unrelated ledger items. +3. **"GitHub's green check on a stacked PR"** — there was no `test` check + at all on #630/#632 (rollups showed only the Workers Build); the trap + was a CLEAN merge state with the check absent. +4. **Four ledger collisions, not three** (the fourth: #615 × #634, + surfaced mid-session). +5. **"Filter racks are pleated site-wide" covered the three lesson racks + only** — the Workbench `#ahu-filter` still drew the parallel-line rack + (ruled and redrawn overnight, PR #647). +6. **`grep -L pairedLesson html/practice/*.html` returns eight** + (index.html matches); derive the drills with + `grep -l '^category: field'`. +7. **CLAUDE.md's "nine consumers" was trued on #638, not #619.** +8. **The "#88 brief" is content-audit #88**, not codebase-issues #88. +9. **"Bank target 15 (ruled 2026-08-20)"** — the date is not evidenced; + friction §*Quiz expansion* dates the direction 2026-08-14. Cite the + friction file, not a date. +10. **content-audit #97/#98 were already accepted 2026-10-05**; the open + by-catch was #91–#96 (all now ruled and shipped). +11. **`tests/psychro-mixstreams.spec.js` exists** and is the closer + template for engine-direct specs than `psychro-engine.spec.js` alone. +12. **The #228 design note's cp line cites (`:209/:216/:246/:502`) had + drifted** to `:224/:231/:261/:517` on `a2ad39f`; `buildState` has + **no** `P` default (the note's "defaults the way buildState's does" + had no referent — #651 chose `streams[0].state.P`, spec-pinned). +13. **The dashboard's `dashboard-extra.json` hand-pins `mainSha`** — it + is a perishable; refresh it on every docs PR. ## The work, in order -### 1. Quiz wave 3 — `docs/site-ideas-and-friction.md` §*Quiz expansion* (L6415 @ `510d5e6`) - -**Owner decision (2026-10-05): the arc is "freeze season plus quizzes"; bank target 15 (ruled 2026-08-20); owner merges every bank PR.** - -Verified state: 34 of 41 banks are under 15 (`npm run status`, *content* -section lists them; none is a BACnet bank; `modbus-decoding` is frozen at -10 by design and is not in the list). 27 of the 34 pair 1:1 with a lesson -at `html/education/.html`; **seven are field drills with no paired -lesson** — `controller-swap`, `controls-commissioning`, -`field-wiring-sensors`, `sequencing-scenarios`, `surviving-first-months`, -`troubleshooting`, `wiresheet-traces` (derived: `grep -L pairedLesson -html/practice/*.html`) — and need their source lessons named explicitly. - -Shape: the lane is a committed named Workflow, -**`.claude/workflows/quiz-bank-growth.js`** — the wave-2 script -parametrised (`args.banks`, `args.sessionUrl`, `args.target`, -`args.basePort`; header documents it). Opus drafts, Fable refutes up to -twice, Opus fixes, one PR per bank. Suggested first batch, freeze-season -neighbours of the new lesson first: `building-pressure`, -`duct-static-control`, `vav-systems`; then `psychrometrics-basics`, -`hydronic-loops`, `load-piping`. Run **two or three banks at a time** — -four concurrent worktree lanes pinned this box at 99 % CPU. Then walk the -PRs with the owner in sequence (he asked for that format; §18). -⚠️ Refuters catch structure (duplicates, key-letter tells, metric -rounding); the owner catches field truth — budget his read. - -### 2. #228 execution — `docs/engine-standardization.md` §4 - -**Owner decision (2026-10-06): the design note's defaults stand** — add -`Psychro.mixAir` / `mixFraction` and keep `mixStreams`; no caveat text in -the engine; no workbench mass-delta readout for now. - -Step 0 adds the two helpers to `psychro-engine.js`, creates -`tests/psychro-mixair.spec.js` (the `psychro-engine.spec.js` vm pattern — -it does not exist yet) and takes a **minor version bump** (shared-script -cache busting); then one page per PR -with a before/after fixture proving zero change clear of saturation, fog -cases enumerated; workbench last and display-only. Every step touches a -live script or page → owner merges each. Background tax lane, not the -arc's headline. - -### 3. Small fixes that need no new ruling - -- **codebase-issues #330** — `publish-preview.mjs` destination guard - (resolve against `os.homedir()`, assert under `$HOME`). Script only → - merge on green. -- **codebase-issues #329** — give `pid-basics.html`'s closing wiresheet - paragraph an `id` and retarget `pid-block-on-wiresheet`'s `learnMore` - back to it (it points at `function-blocks.html#families` today). Live - lesson + bank → owner merges. - -### 4. Small fixes once the owner rules (see *Decisions waiting*) - -content-audit **#99** (`bandTxt`), codebase-issues **#328** -(bacnet-networking's two "its own page" bullets — the amended coming-soon -rule says narrow, not delete), **#324** (the PID tuner's "flip acting" -row), content-audit **#91–#98** (the wave-2 refuters' by-catch, each a -one-line call). All live pages → owner merges; bundle by file. - -### 5. The F4 case-file mockup lane — friction §*The owner's dev-arc brief* (L6445 @ `510d5e6`) - -**Owner decision (2026-10-05, rulings §17): ONE mockup lane at the arc's -end; stories owner-supplied only after he has seen a shape.** Mockup-first -means a hidden page (no `canonical`, `noindex`, excluded from -collections) with its own hand-written spec — merge freely until it -graduates. The scenario-engine consumer stays parked. Do not draft -placeholder war stories. +### 1. The morning walk — fifteen PRs + +**Owner decision (2026-10-06, rulings §19.13 and §18):** PR by PR, in +sequence, on the combined LAN preview; the assistant presents, the owner +approves, the assistant merges on the word. + +Order and pointers: `docs/audits/2026-10-reentry/walk-2026-10-07.md` +(small content fixes → badge and Workbench drawing → batch-1 banks → +#651 then #652 → batch-2 banks). Things that need the owner's eye rather +than a diff read: the 38 °F "field settings reach … at the top" claim on +#639 is his, not cited; whether mixed-air temperature belongs in the +tuner's Fast row (#641); the `--text-dim` stroke and the cockpit-scale +legibility on #647; the two quiz explains the fixer rewrote after the +second refute round with no third round (`ds-tube-stuck-high` on #648, +`loads-see-mixed-temp` on #654); the `P` default on #651. + +⚠️ **#652 is the first live test of #645's retarget arm.** When #651 +merges, GitHub retargets #652 to `main` with an `edited` event; the gate +should now run the suite. If no `test` run appears within a minute, +rebase-push #652 onto `origin/main` (the standing rule) and merge only on +SUCCESS. Record which way it went in codebase-issues #331. + +After each merge, `push: main` runs the suite on `main` — check the run +list once at the end rather than per PR. + +### 2. Quiz growth after wave 3 — `.claude/workflows/quiz-bank-growth.js` + +**Owner decision (2026-10-06, rulings §19.10):** six banks in two +batches; both batches are open. **Verified on the scratch merge:** 28 +banks remain under 15 after they merge (derive again post-merge with +`npm run status`). None is BACnet; the remaining set includes the seven +field drills (no `pairedLesson` — name their source lessons in `args`). + +Two workflow defects to fix before the next batch (the orchestrator +owns the script; lanes do not touch it): + +- **A second refute round's open item is applied by the fixer with no + third round** — both batch-1 open items and the batch-2 one were + applied verbatim and correctly, but nothing checked. Add a cheap + verify-remedy stage after the last fix (Fable, read-only, "does the + head carry the prescribed text and does the spec still pass"). +- **A lane's read of `html/scripts/quiz-engine.js` was refused** by the + permission classifier (psychrometrics-basics lane); it worked from the + bank, the spec and the DOM instead. UNVERIFIED — whether this is the + path or the classifier's mood; allow it or point lanes at + `tests/quiz-engine*.spec.js` for the schema. + +Bank-wide follow-ups the lanes surfaced, logged for the owner's call: +codebase-issues **#335** (several ten-question banks key everything on +one letter and the key is the longest choice; the engine does not shuffle +choices — a reorder sweep, bank by bank) and **#334** (the engine parses +`10,000` as 10 — strip separators in one place). + +### 3. #228 steps 2+ — `docs/engine-standardization.md` §4 + +**Owner decision (2026-10-06, rulings §18.5 and §19.12):** defaults +accepted; steps 0 and 1 are PRs #651 / #652. Next: air-mixing (the page +with three disagreeing forms), then the Workbench last, display-only; +one page per PR with a before/after fixture, fog cases enumerated. + +Two doc follow-ups **deliberately left out of the overnight docs PR** so +it would not collide with #651's hunk in `engine-standardization.md`: +the §2 table's stale cp line cites, and a "Landed" line under §4 step 1. +Take them with step 2. By-catch to fold in: codebase-issues **#336** +(`buildState` has no positive-`P` guard) and content-audit **#104** (the +chart's "falls on the straight line" claim is true to ~0.8 °F). + +### 4. Small by-catch, each a one-file change once ruled + +codebase-issues **#332** (house spelling: *freezestat* vs *freeze-stat*, +22 hyphenated hits), **#333** (`bacnet-services.html:227` and +`bacnet-mstp.html:238` carry the #328 promise shape in words the §5 scan +does not see; widen the regex with `worth its own treatment` / `on the +roadmap`), **#337** (`building-pressure.html` callout `id`, #314 family); +content-audit **#100** (tuner Medium row names valve outputs as loops), +**#101** (bacnet-networking's "out of scope across" vs "now its own +page"), **#102** (coil-freeze-risk `:484` "common default"), **#103** +(the air-handlers widget labels a mixed-air low-limit as the stat; plus +the dangling "when it is" at `:579`). All live pages → owner merges; +bundle by file. + +### 5. The F4 case-file mockup lane — friction §*The owner's dev-arc brief* (L6445 @ `a2ad39f`) + +**Owner decision (2026-10-06, rulings §19.11): HOLD until wave 3 is +further along.** Wave 3 is now fully open; raise it once the banks have +merged. Mockup-first (hidden page, own spec, merge freely until it +graduates); no placeholder stories. ### 6. After the arc -Glossary **§7.2** quiz-bank component (`docs/glossary-arc.md`, sequenced -after the arc 2026-10-05); the **short sitting** on the brief items A1, -A4, C4, D2, D4, E2, the §0 N2 gap and the LON go/no-go (friction §*dev-arc -brief* has the banked analysis); a **fresh Search Console export for the -pillar re-read** — the last read was off the 2026-08-07 export (friction -L7214 @ `510d5e6`); the late-October timing comes from the 2026-10-05 -agenda, not from the friction file. +Unchanged: glossary **§7.2** (`docs/glossary-arc.md`, sequenced after +the arc); the **short sitting** on A1, A4, C4, D2, D4, E2, the §0 N2 gap +and the LON go/no-go (friction §*dev-arc brief*); a **fresh Search +Console export** for the pillar re-read — the late-October timing is the +2026-10-05 agenda's (`state-map.md:120`), the last read was off the +2026-08-07 export. **Explicitly declined or parked — do not carry as open work:** a -`[future:]`-marker cleanup of the friction file (the 2026-10-05 drift PR -#629 did the stale ones); renaming `mixStreams` (Q1 default: no); a -workbench mass-delta readout (Q3: not now); any southern-setpoint claim in -the freeze lesson (research record §4 — no source). +`[future:]`-marker cleanup of the friction file (#629 did the stale +ones); renaming `mixStreams` (Q1: no); a workbench mass-delta readout +(Q3: not now); a southern-setpoint claim in the freeze lesson (research +record §4 — no source); the hero badge's spring revert (a copy-paste from +the comment beside it, when the season turns). ## Decisions waiting on the owner -- **content-audit #99** — the coil-freeze-risk tool says "typical settings - run 35–38 °F"; the lesson says 35–37 °F is the device band and 38 a - house choice a degree above. One sentence moves either way; carrying it - means two numbers for one device on two live pages. -- **codebase-issues #331** — which CI trigger fix: `edited` with a - base-change gate, `push: branches: [main]`, or both (recommendation: - both — the second also tests `main` itself after every merge). -- **codebase-issues #328** — the two bacnet-networking bullets: narrow to - scope statements, or leave under the amended rule. -- **codebase-issues #324** — the PID tuner's "flip acting" cheat-sheet row - (the tuner has no such control). -- **content-audit #91–#98** — the wave-2 by-catch; each is a one-line - call (e.g. #97 "Ten questions" is a RUN claim by the standing ruling and - may just be accepted). -- **Home hero badge** — still *Featured: DDC Workbench*; the lesson lane - left it per the 2026-07-19 relabel. Say if the lesson should be featured. - -None of these block the work above. +- **#332** — which spelling, *freezestat* (the lessons) or *freeze-stat* + (widget strings, banks)? One sweep PR either way. +- **#333** — `bacnet-mstp.html:238` promises a bus *simulator*, not a + page; narrow it like #328, or keep under the amendment? +- **#335** — go/no-go on the choice-reorder sweep across the one-letter + banks (bank by bank, each a PR for him to read). +- **content-audit #103** — relabel the air-handlers widget's + "freeze-stat territory" (it models a mixed-air low-limit) or leave it + and let the new prose pointer carry the distinction? +- **#651** — confirm the `P` default (`streams[0].state.P`, not sea + level) and the pass-through of a kernel `ok:false`. +- **#641** — keep mixed-air temperature in the tuner's Fast row, or drop + it (not move it)? + +None of these block the walk. ## Process notes that earned their keep -- **Walk the PR queue together, in sequence** — assistant presents, owner - approves, assistant merges on the word. Owner: *"may be the most - balanced workflow we've used."* Six content rulings came out of one - walk that the lanes' refuters had not found. -- **Rebase-push stacked children before merging** (Corrections 1–2); run - the merge-tree matrix first. -- **Give the owner the rendered page, not the diff**, for a lesson: the - LAN preview (`CF_PREVIEW_DIR=…` from a worktree) and a full-page - screenshot sent to his phone. The filter catch came from the rendering. -- **Lane-harness facts:** a fresh worktree has no `node_modules` (`npm ci`); - a bash command containing the literal `.github` is refused by the - worktree-isolation checker (use `npm run status`); another worktree - holding a branch name blocks `checkout -B` (use a local name and push - `local:remote`); `pkill -f