From 8fc1051ad354448298aafadc8876e92877e934c4 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Mon, 14 Sep 2026 14:56:10 +0200 Subject: [PATCH 01/10] =?UTF-8?q?docs(spec):=20APR-RELEASE-001=20=C2=A74.1?= =?UTF-8?q?/=C2=A74.2/=C2=A75.1/=C2=A710=20=E2=80=94=20the=20work=20the=20?= =?UTF-8?q?train=20did=20not=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A day spent on T-1 surfaced five things the spec did not cover. Four are now clauses; the fifth turned out to be covered already and is only made precise. §4.1 FEATURE MATRIX — T-1 named it from the start and it was never built. Its first run (#3262) measured 100 of 430 (crate, feature) pairs RED, every one unreachable from any default set — which is exactly why it could go dark: `cargo check --workspace` stays green over all of it because feature unification hands each crate whatever its siblings enabled. The section defines the universe (per-pair from `cargo metadata`, never a powerset — aprender- orchestrate alone declares 78 features), the five shapes that accounted for all 100, the known-red list and why a listed pair that PASSES must be fatal, and the `compile_error!` + private `__x-linked` form for a feature that cannot be built at all. Struct drift is listed as shape 5 because nothing else in the train watches for an upstream type growing a field. §4.2 EXAMPLES — T-1 says every `cargo run --example`, which is a different clause from building them (83 s vs ~2 h). Measured: three of five random examples ran past a 60 s cap. They are compute demos, not CLIs, so TIMEOUT IS A PASS and the assertion owed is "starts and does not crash". Asserting a duration would be a wall-clock assertion in a required check. Both clauses carry a vacuity floor: a discovery that finds nothing reports zero failures, which reads exactly like a pass. §5.1 THE DEBT TAX — the pre-commit gate refuses any commit touching a file with a function over cyclomatic 30 / cognitive 25, and `--no-verify` is banned, so a one-line fix costs the decomposition of every offender in that file. Measured in one day: 11 pre-existing violations paid down, worst cognitive 91, 73, 61, none in code that day's changes wrote. It is not a build row because it is not schedulable — it is a toll on whatever you touch. Now it is at least measured: `debt:` in §7. §10 DECISION PROCEDURE — §6 is deliberately "no judgement calls", so design forks had nowhere to go. Codifies what worked: fan out through agy not Claude subagents; the brief carries the measurements so lanes do not each measure the premise differently; plant one trap question; a verdict is a claim until the orchestrator re-runs the acceptance command; **a premise error voids the vote and the fix is another round, not the orchestrator's judgement** (#3179 round 2 overturned round 1 unanimously once three facts were read out of the tree); overriding the majority is allowed once, only on a fact no lane had, and must be recorded with the losing argument quoted; prefer the reversible option when the vote is close. §6 — `untriaged` must be counted PER SURFACE. Measured 2026-09-14: issues were 319/320 triaged while PRs were 20 of 34 with no milestone at all, and one number reported the clean surface while hiding the breached one. Also states plainly that triage is not disposal: the ledger grew net +149 over ten days while ~100 % triaged, with 312 of 320 open issues opened by the agent itself. This is the same finding §6 already recorded for the 0.67 train ("filing was the work product and closing was nobody's"); it now has a stop rule. §8 — three stop conditions: a declared full-time build host at 0 % occupancy while a queue has pressure (mini, measured all of 2026-09-14) is a routing defect, not spare capacity; a stale known-red list; a design fork goes to §10. readme_contract 15/15. Pmat-Ticket: PMAT-1098 Co-Authored-By: Claude Opus 5 (1M context) --- .../APR-RELEASE-001-train-and-build-kaizen.md | 143 +++++++++++++++++- 1 file changed, 141 insertions(+), 2 deletions(-) diff --git a/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md b/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md index 4827d1fab5..50415d166f 100644 --- a/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md +++ b/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md @@ -102,7 +102,7 @@ Live `forjar.yaml` beats this table. Record the diff in the receipt and continue | Step | Action | Skip if | |---|---|---| | **T-0 Cut** | cut sha = `main` HEAD; bump minor; `CHANGELOG` from merged PR titles since last tag | tag `v0.N.0` exists | -| **T-1 Deep** | `ci / deep` green on cut sha: full tests, doctests, `--no-default-features`, feature matrix, GPU, every `cargo run --example` | green run recorded for this sha | +| **T-1 Deep** | `ci / deep` green on cut sha: full tests, doctests, `--no-default-features`, feature matrix (§4.1), GPU, every `cargo run --example` (§4.2) | green run recorded for this sha | | **T-2 Dogfood** | `apr-dogfood` skill go/no-go receipt; `apr-cookbook` current; release notes generated | receipt exists for this sha | | **T-3 Promote** | tag; clean-room on the tag; GitHub release with T-2 receipt attached | release `v0.N.0` exists | | **T-4 Publish** | **automated** — the autopilot runs `cascade-drain.sh` after T-3's preflight; record cascade wall minutes; attended minutes are 0 by construction | all crates at `0.N.0` on crates.io | @@ -111,6 +111,56 @@ Live `forjar.yaml` beats this table. Record the diff in the receipt and continue Any step RED → SKIPPED, no partial promotion. Scope is assigned to trains after the fact: 0.67 contains whatever merged before the 0.67 cut, by definition. +### §4.1 Feature matrix — what T-1 means by it + +One `cargo check -p --no-default-features --features ` per **(crate, feature) +pair**, not a powerset: a powerset is 2^n per crate (aprender-orchestrate alone declares 78 features) +and infeasible. The universe comes from `cargo metadata`, never a written list. + +This clause was in T-1 from the start and was never built. Its first run, 2026-09-14 (#3262), +measured **100 of 430 pairs RED — every one unreachable from any crate's default set**. That is the +whole reason it can go dark: `cargo check --workspace` stays green over all of it, because feature +unification hands each crate whatever its siblings enabled. + +Five shapes account for all 100; check them in this order: + +1. **Definition gated, caller not.** Often the gate is over-broad — **un-gate it, do not spread it**. +2. **Declared in `Cargo.toml`, never applied in code.** `grep -rn 'feature = "X"' --include='*.rs'` + returning nothing is the test. +3. **The implicit dep-feature used where the composite was meant.** An optional dep `foo` creates + feature `foo`, which links foo and nothing else. +4. **A feature that supplies nothing its code needs** — usually a dependency removed for lock bloat + with the gated code left behind. **Measure `Cargo.lock` before re-adding**: a version already + locked by a sibling costs zero new packages. +5. **Struct drift** — an upstream type grows a field and the one initializer in the tree is never + updated. Nothing else in the train watches for this; the matrix is what catches it. + +**Known-red list.** A pair that cannot be made green in the same train is listed with a date and an +issue reference, and is reported, not fatal — a lane born red is a lane taught to be ignored. A +listed pair that **PASSES is fatal**: the list may only shrink, and it shrinks by deleting a line. +Re-derive the list against the tree immediately before the lane is undrafted; a dated list is +evidence of when it was true, not that it still is. + +**A feature that cannot be built at all** — its dependency is deliberately absent, or circular — +emits ONE `compile_error!` naming the dependency and the exact edit that would enable it, with the +real gate moved to a private `__x-linked` feature so the refusal is the only diagnostic. Precedent +in tree: `aprender-simulate/z3-proofs`, `aprender-core/showcase-profile`, `aprender-zram-core/cuda`. + +Fixing one cause exposes the next — the nextest-fail-fast class. Re-sweep after every fix; a +single-cause sweep is not a measurement. + +### §4.2 Examples — run, not just build + +T-1 says *every* `cargo run --example`. Building them is a different clause and much cheaper (83 s +for 980 targets vs ~2 h to run them). Measured 2026-09-14 on a random five with `-- --help` and a +60 s cap: **three ran past it**. They are not CLIs, they are compute demos that ignore argv, so +**timeout is a PASS**. The assertion T-1 actually owes is *every example starts and does not crash*. +Asserting a duration here would be a wall-clock assertion in a required check, which +`check_no_timing_in_required.sh` exists to forbid. + +Both clauses carry a **vacuity floor** (measured 980 examples / 430 pairs; floors 800 / 400): a +discovery that finds almost nothing reports zero failures, which reads exactly like a pass. + ## §5 Build rows — in order, one PR each, only when no train is due **P0 · Pack** *(operator 2026-09-12; precedes everything while it fails)*. Every wakeup: sample @@ -166,6 +216,33 @@ zero ENOSPC in 10 trains. job, low priority, report-only: target = measured baseline + 2 points per ratchet toward 95 %. Re-evaluate each train; ratchet downward only, ≤ 10 % per step. +### §5.1 The debt tax — paid on every row, scheduled by none + +*(measured 2026-09-14; the largest single time sink of that day)* The pre-commit +gate refuses ANY commit touching a file that carries a function over cyclomatic 30 / cognitive 25, +and `--no-verify` is banned. So a one-line fix in a debt-carrying file costs the decomposition of +every offender in that file. Measured on one day's work: **11 pre-existing violations** paid down to +land small fixes — worst cases cognitive 91 (`execute_llm_score`), 73 (`help_producer_truth::resolve`), +61 (`execute_llm_load`). None were in code that day's changes wrote. + +This is a real cost and it is not optional, so it is scheduled rather than absorbed silently: + +- The decomposition lands in the SAME commit as the fix that triggered it, and the commit message + names each offender with its before-number. A stack green only at the tip hides which change + broke what. +- Extraction, not rewriting. Prefer moving a whole block to a named function over clever + restructuring; a behaviour-preserving refactor is the only kind admissible here. +- When a match arm is extracted, re-match behind a `let … else` that refuses by name. Never panic, + never `unwrap()`. +- Repeated identical blocks are the cheapest win — one helper taking the two things that differ + collapsed nine sites in one file. +- **Record per train:** `debt: files_touched | violations_paid | worst_before `. After 10 + trains, if `violations_paid` is not falling, the gate's thresholds or the tree's debt is the build + row, not the fixes. + +*Done:* no train spends more turns on debt paydown than on the change that required it, measured +over a trailing 5 trains `[U]`. + ## §6 Triage pass — once per train, no judgement calls — and the T-5 reconcile, a HARD gate **Measured 2026-09-13 (0.67.0 train):** 308 open issues, 424 opened vs 124 closed in 30 d, 95 open @@ -197,6 +274,16 @@ mechanism. - Every issue opened since the last train: labelled and sized. **`untriaged = 0`** at the end of every pass — hard target today, needs no baseline. +- **Counted per surface, never as one number.** Measured 2026-09-14: issues were 319/320 triaged + while PRs were 20 of 34 with no milestone at all. A single `untriaged` figure reported the clean + surface and hid the breached one. The pass ends only when `untriaged_issues = 0` AND + `untriaged_prs = 0`. +- **Triage is not disposal.** Over the trailing 10 days the issue ledger grew net **+149** while + being ~100 % triaged, and **312 of 320 open issues were opened by the agent itself**. Classifying + a finding does not close it. If `arrival > closure` for 3 consecutive trains, filing new findings + as issues stops being free: the next train's build row is closure, not features. +- **Mechanical PR states are the pass's job, not a reviewer's**: `BEHIND` → `gh pr update-branch` + (measured: 14 → 4 in one pass), `DIRTY` → named on the PR with the conflicting files. - PR lifecycle, deterministic: no activity for 2 trains → label `stale` + comment; `stale` and no activity for 1 more train → closed. (~9 days, no discretion.) - Record per train: `arrival closure open_prs age_p95 untriaged`. @@ -211,7 +298,10 @@ train: step reached | skip reason | cascade wall min | PR | records added gate: p95 ci/gate | max PRs/train | queue p95 intel yoga gx10 pack: intel / | gx10 / | yoga / | intel-pressure | verdict -triage: arrival | closure | open PRs (age p95 ) | untriaged +triage: arrival | closure | open PRs (age p95 ) | untriaged issues prs +matrix: pairs | red | known-red | stale-known-red | examples started / +debt: files touched | violations paid | worst before +quorum: rounds | width | verdicts | overridden reconcile: R1 fixed-open | R2 no-close | R3 dead-branches | R4 dirty>1train | R5 closure/arrival | receipt stops: next: train eligible at @@ -229,6 +319,14 @@ next: train eligible at stop the current build row, pack first (arm, route, reap), report the `pack:` line. - A runner box at or below `REAPER_CRITICAL_GB` free, or any job dead on ENOSPC → P0: reclaim over SSH now, then land the forjar change that makes it automatic, before anything else. +- A host DECLARED a full-time build host sits at 0 % occupancy for a whole session while any + queue has pressure → P0: that is a routing or job-class defect, not spare capacity. (`mini` + measured at 0.0 % all of 2026-09-14 while declared full-time in #3205 — its ceiling is the + macOS-capable job classes, not capacity.) +- A `ci / deep` known-red list names a pair that now PASSES → stop: the list is stale and the lane + is asserting something that is no longer true (§4.1). +- A design fork appears that §6 cannot decide without judgement → §10, not a coin flip and not a + question to the operator. - Any step would need an invented threshold, a second concurrent aprender PR in CI, `--allow-dirty`, or SSH into a host → stop. @@ -240,3 +338,44 @@ or 2 consecutive skips `[A]`. The 82-crate cascade is automated (ruling 2026-09-13) and its wall time is unmeasured. Record cascade wall minutes at T-4 on the `0.67` train. Above ~20 min `[A]` it delays the post-publish receipts and the cascade — not the build — becomes the next kaizen target. + +## §10 Decision procedure — for the forks §6 refuses to make + +§6 is deliberately "no judgement calls". Design forks still occur — four ways to fix one broken +feature declaration, restore-vs-delete a dead GPU path — and they are not triage. They go here. + +**Fan out through agy, not through more Claude subagents.** Lanes cost agy credits; orchestrator +turns are the scarce budget. `lane-group.sh run --out-dir D --width 3 --scratch -- agy-lane.sh +--mode plan --repo-root --prompt …`. Width 3 is the default; 10 is the cap. + +**The brief carries the measurements, not the question.** Name the files, the line numbers, the +acceptance command, and every option including the ones you dislike. A lane that has to go and +measure the premise will measure it differently from the next lane, and the vote becomes noise. + +**Plant the trap.** Include one question whose obvious answer is wrong — a rename that looks +mechanical but is not. A quorum that misses it has told you how much its other answers are worth. +(2026-09-14: `Lz4WarpShuffleKernel` → `Lz4WarpCompressKernel` reads as a rename and is a +literal-only encoder swapped for a real match-finder. All three lanes caught it.) + +**A verdict is a claim until re-run.** The orchestrator re-executes the acceptance command itself +and records both columns. A lane reporting "all 9 selections green, 84 tests pass" is evidence of +what the lane believes. + +**A premise error voids the vote, and the fix is another round — not the orchestrator's judgement.** +Measured on #3179: round 1 voted 2/3 to make a dead path compile. Three facts were then verified by +reading the tree (a stub decompress kernel, a launched entry point that does not exist, a host-memory +API that never existed here). Round 2 with those facts overturned round 1 unanimously. **Re-run the +quorum with the corrected premise; do not silently substitute your own conclusion.** + +**Overriding the majority is allowed exactly once and must be recorded.** Only on a fact no lane in +that round had, stated in the commit and the PR, with the losing option's own argument quoted. On +#3179 round 2 split 2/1 to delete; the minority option shipped because the crate's default build is +green and the module in question is live public API — both deleting lanes had argued from "a dark, +unused feature". When the fact is not decisive, the majority wins and your discomfort is not +evidence. + +**Prefer the reversible option when the vote is close.** A `compile_error!` can be deleted later; a +deleted module and its public types cannot be recovered from a reviewer's memory. + +**Record per train:** `quorum: rounds | width | verdicts | overridden `. + From 28951449c2e29a86d0d8c142049f5bc21a31960c Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Mon, 14 Sep 2026 15:00:03 +0200 Subject: [PATCH 02/10] =?UTF-8?q?docs(spec):=20=C2=A76.1/=C2=A76.2/=C2=A76?= =?UTF-8?q?.3=20=E2=80=94=20triage=20is=20continuous,=20covers=20branches,?= =?UTF-8?q?=20and=20a=20milestone=20must=20fit=20its=20date?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three gaps in §6, each measured 2026-09-14. §6.1 CADENCE. "Once per train" is what let 20 of 34 open PRs carry no milestone at all, every one opened in the preceding two days. A train is 48-72 h; a PR opened an hour after the pass is invisible for the rest of it. Triage now runs on the P0 · Pack wakeup, beside the fleet sample — same cadence, same receipt, and equally P0 per the operator ("ticket, pull requests, branches that are not triaged are P0"). The per-wakeup pass is mechanical and bounded; the once-per-train pass keeps only what needs the whole window: the §6.3 capacity check and the T-5 reconcile. §6.2 BRANCHES were the unwatched surface. 107 remote branches, 35 with an open PR, 72 without: 32 younger than 7 d, 27 in a 7-14 d band NO RULE LOOKS AT, 13 already R-3-eligible. R-3 archives a branch with no PR and a tip older than 14 d, so work that stalls on day 8 is invisible for six more days and is then deleted without ever having been seen. New test: no open PR and a tip older than 7 d must get a PR (draft is fine) or be archived now. A branch with no PR is not work in progress, it is work nobody can see. §6.3 PRIORITISATION. §4 says scope is assigned after the fact — right for what a train CONTAINS, wrong as a plan for what it PROMISES. With no capacity rule a milestone is a dumping ground with a date on it. Measured at closure = 6.1 issues/day over the trailing 7 days: 0.68.0 280 open due in 1 d needs ~46 d OVER BY 45 DAYS 0.69.0 53 open due in 4 d needs ~9 d over by 5 d 0.70.0 15 open due in 7 d needs ~2 d fits A date 45 days of arithmetic away from its content is not a commitment; it is a label, and every number computed from it is fiction. The rule is arithmetic, so it stays inside §6's no-judgement-calls design: capacity = days_remaining x measured closure_rate_p50. Over capacity is reported every wakeup, not treated as an error. At T-0 an overcommitted next milestone SPILLS lowest-priority-first until it fits — P0 never spills, then P1, then unlabelled, then oldest kept. The operator sets priority by labelling; the arithmetic sets the cut line, so no train needs a judgement call about scope. A P0 set that alone exceeds capacity is a STOP (§8): that is over-promising at the one level the operator controls, and only the operator can cut it. closure_rate is MEASURED; under 7 days of data it reports [U] and spills nothing. Arrival is the other half: 6.1/day closure against a ledger that grew net +149 in ten days means the cut line moves further out every train however it is drawn. R-5 is the control on that; capacity only decides what a date may claim. readme_contract 15/15. Pmat-Ticket: PMAT-1098 Co-Authored-By: Claude Opus 5 (1M context) --- .../APR-RELEASE-001-train-and-build-kaizen.md | 73 ++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md b/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md index 50415d166f..aa283265bc 100644 --- a/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md +++ b/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md @@ -243,7 +243,7 @@ This is a real cost and it is not optional, so it is scheduled rather than absor *Done:* no train spends more turns on debt paydown than on the change that required it, measured over a trailing 5 trains `[U]`. -## §6 Triage pass — once per train, no judgement calls — and the T-5 reconcile, a HARD gate +## §6 Triage — continuous (§6.1), three surfaces (§6.2), capacity-bounded (§6.3) — and the T-5 reconcile, a HARD gate **Measured 2026-09-13 (0.67.0 train):** 308 open issues, 424 opened vs 124 closed in 30 d, 95 open issues already cited by a commit on `main`, 48 merged PRs of which 9 carried a closing reference, @@ -253,6 +253,73 @@ attributes, not decisions. **Operator ruling: reconciliation is part of every tr a separate chore.** A train that ships code and leaves its own tickets, branches and dead PRs behind is not done. +### §6.1 Cadence — every wakeup, not once per train + +"Once per train" is what let 20 of 34 open PRs carry no milestone at all on 2026-09-14, every one +of them opened in the preceding two days. A train is 48–72 h; a PR opened an hour after the pass is +invisible for the rest of it. Triage runs on the **P0 · Pack** wakeup, beside the fleet sample — +same cadence, same receipt, and it is equally P0 (operator: "ticket, pull requests, branches that +are not triaged are P0"). + +The per-wakeup pass is mechanical and bounded: anything opened since the last sample gets a +milestone and a label; `BEHIND` PRs get `gh pr update-branch`; `DIRTY` PRs get their conflicting +files named in a comment. Nothing here is a judgement call. The once-per-train pass keeps only +what needs the whole window: the §6.3 capacity check and the T-5 reconcile. + +### §6.2 Three surfaces — issues, PRs, and BRANCHES + +Branches were the unwatched one. Measured 2026-09-14: **107 remote branches, 35 with an open PR, +72 without**. Of those 72 — 32 younger than 7 d, **27 in a 7–14 d band that no rule looks at**, 13 +already R-3-eligible. R-3 archives a branch with no PR and a tip older than 14 d, so work that +stalls on day 8 is invisible for six more days and then deleted without ever having been seen. + +| Surface | Per-wakeup test | Fix | +|---|---|---| +| Issue | opened since last sample → milestone + label | assign; `untriaged_issues = 0` | +| PR | open → milestone; `BEHIND` → update-branch; `DIRTY` → conflicting files named | `untriaged_prs = 0` | +| Branch | no open PR and tip older than **7 d** | open a PR (draft is fine) or archive it now — do not wait for R-3 to delete it at 14 d | + +A branch with no PR is not work in progress; it is work nobody can see. Seven days is the point at +which it must become visible or become history. + +### §6.3 Prioritisation — capacity, not preference + +§4 says scope is assigned after the fact: "0.67 contains whatever merged before the 0.67 cut, by +definition." That is right for what a train *contains* and wrong as a plan for what it *promises*. +With no capacity rule, a milestone is a dumping ground with a date on it. + +**Measured 2026-09-14**, closure = **6.1 issues/day** over the trailing 7 days: + +| Milestone | Open | Due in | Needs | Verdict | +|---|---:|---:|---:|---| +| 0.68.0 | 280 | 1 d | ~46 d | **over by 45 d** | +| 0.69.0 | 53 | 4 d | ~9 d | over by 5 d | +| 0.70.0 | 15 | 7 d | ~2 d | fits | + +A date that is 45 days of arithmetic away from its content is not a commitment; it is a label, and +every number computed from it is fiction. + +**The rule, and it is arithmetic so it stays inside §6's no-judgement-calls design:** + +``` +capacity(M) = days_remaining(M) × closure_rate_p50(trailing 7 d) +``` + +- `open(M) > capacity(M)` → the milestone is **OVERCOMMITTED**. Report it on the `triage:` line + every wakeup. It is not an error; it is a number that must be visible. +- At T-0 of each train, an overcommitted next-milestone **spills**, lowest priority first, until + `open(M) ≤ capacity(M)`. Spill order is by label — `P0` never spills, then `P1`, then unlabelled, + then by age, oldest kept. The operator sets priority by labelling; **the arithmetic sets the cut + line**, so no train needs a judgement call about scope. +- A `P0` set that alone exceeds capacity is a **stop** (§8): the release is over-promised at the + priority level the operator controls, and only the operator can resolve that. +- `closure_rate` is MEASURED, never assumed. First train with fewer than 7 days of data reports + `[U]` and spills nothing. + +**Arrival is the other half.** Closure of 6.1/day against a ledger that grew net +149 in ten days +means the cut line moves further out every train no matter how it is drawn. §6's arrival/closure +ratio (R-5) is the control on that; capacity only decides what a date is allowed to claim. + ### T-5 reconcile predicates — every one must read 0 in the receipt | Predicate | Derive with | Fix | @@ -299,6 +366,8 @@ build: row | PR | records added gate: p95 ci/gate | max PRs/train | queue p95 intel yoga gx10 pack: intel / | gx10 / | yoga / | intel-pressure | verdict triage: arrival | closure | open PRs (age p95 ) | untriaged issues prs +branches: total | no-PR | no-PR >7d | archived this pass +capacity: milestone open | closure/day | capacity | verdict d> | spilled matrix: pairs | red | known-red | stale-known-red | examples started / debt: files touched | violations paid | worst before quorum: rounds | width | verdicts | overridden @@ -327,6 +396,8 @@ next: train eligible at is asserting something that is no longer true (§4.1). - A design fork appears that §6 cannot decide without judgement → §10, not a coin flip and not a question to the operator. +- The `P0`-labelled set of the next milestone alone exceeds its capacity (§6.3) → stop: the release + is over-promised at the one priority level the operator controls, and only the operator can cut it. - Any step would need an invented threshold, a second concurrent aprender PR in CI, `--allow-dirty`, or SSH into a host → stop. From f16a0e3f19605244eda0919c64521a9a86be0949 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Mon, 14 Sep 2026 15:07:30 +0200 Subject: [PATCH 03/10] =?UTF-8?q?docs(spec):=20=C2=A711=20=E2=80=94=20a=20?= =?UTF-8?q?surface=20sweep=20ends=20in=20`pv`,=20or=20it=20did=20not=20end?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ONT-001 v4.3 §6 assigns aprender every ontology row but three. Measured at fa6e35f23: 1 of 17 merged, 0 of 1818 contracts carry `entity:`, `shape:` or `evidence:`, no `pv census`, no `pv extract`, no `ontology/` module. Two of those measurements are the reason for this section. `pv kaizen` IS the kaizen loop and it is code-only — bindings, call sites, E0/E1/E2 assertions. The train sweeps features, examples, README, CLAUDE.md, workflows, model files and CSVs, and the loop that is supposed to improve on each sweep cannot see one of them. The upstream spec is UNTRACKED in infra. No commit, no history, unfetchable from gx10, yoga or mini — so a quorum lane cannot read the premise at all and every ontology verdict it returns is unverifiable by construction. Stop condition, fixed in infra (ONT-P), not here. §11.1 makes the rule mechanical: a surface sweep closes with one of four deltas — an entity type + extractor, a shape whose violation is the defect class just found, a new Unknown{} reason, or a `resolves:` target — or with a named `ont-delta: none `. Unnamed is P0. This spec's own §4.1 is the counter-example: 100 red pairs as an awk matcher inside night.yml, no contract, no shape, re-derived by hand before every undraft. §11.4 is why this makes the quorum more effective, which is the point. Premises cite ids, verdicts are ONT-6 lattice elements, reduce is meet=min rather than a vote count, and the planted trap becomes Unknown{PositiveControlFailed} by rule instead of by the orchestrator noticing. #3179 round 1 was a 2/3 majority over verdicts that had no lattice meaning; under §11.4 it does not reduce to Pass. §11.2 ratchets five counters, §11.3 puts one row per train (16 rows, ~40 days [A]) and lands every gate unarmed, §11.5 adds the `ontology:` report line and `lattice` to `quorum:`, §11.7 gives five falsifiers. Pmat-Ticket: PMAT-1098 Co-Authored-By: Claude Opus 5 (1M context) --- .../APR-RELEASE-001-train-and-build-kaizen.md | 131 +++++++++++++++++- 1 file changed, 130 insertions(+), 1 deletion(-) diff --git a/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md b/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md index aa283265bc..26929f6516 100644 --- a/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md +++ b/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md @@ -370,7 +370,8 @@ branches: total | no-PR | no-PR >7d | archived this pass capacity: milestone open | closure/day | capacity | verdict d> | spilled matrix: pairs | red | known-red | stale-known-red | examples started / debt: files touched | violations paid | worst before -quorum: rounds | width | verdicts | overridden +quorum: rounds | width | verdicts | overridden | lattice +ontology: types /16 | extractors /14 | anchored / | shaped | bindable-unanchored | row merged | upstream sha <12hex|DRIFT> reconcile: R1 fixed-open | R2 no-close | R3 dead-branches | R4 dirty>1train | R5 closure/arrival | receipt stops: next: train eligible at @@ -450,3 +451,131 @@ deleted module and its public types cannot be recovered from a reviewer's memory **Record per train:** `quorum: rounds | width | verdicts | overridden `. + +## §11 Ontology kaizen — a surface sweep ends in `pv`, or it did not end + +Upstream: `ONT-001 v4.3` (`infra/docs/specifications/paiml-ontology.md`, sha256 `512a16d5e09c…`). +Its §6 assigns **aprender** every row but three. This section is how those rows get worked +continuously instead of in one heroic push, and what the train owes the ontology every time it +sweeps a surface. + +### §11.0 Measured baseline — `main` @ `fa6e35f23`, 2026-09-14 + +| ONT-001 requires | aprender has | derive with | +|---|---|---| +| contracts may carry `entity:` / `shape:` / `evidence:` | **0 / 0 / 0** of 1818 | `grep -rlE '^entity:' contracts --include='*.yaml' \| wc -l` | +| `pv census` — one cardinality, `by_entity_type`, `by_anchoring` (ONT-1) | absent | `pv --help` | +| `pv extract` → `contracts.nt`, the only triple producer (R-18) | absent | `pv --help` | +| `crates/aprender-contracts/src/ontology/` (decision 4) | directory does not exist | `ls` | +| 16 entity types named (§0.0); 14 extractors named, 7 in v1alpha1 scope (§3.7) | **0** | — | +| aprender implements every ONT row (§6) | **1 of 17** merged — ONT-2a, #3224 | `git log --grep='ONT-' origin/main` | +| an aprender ticket per row | 1 of 17 (#3222, closed, no milestone) | `gh issue list --search 'ONT in:title' --state all` | +| `pv kaizen` **is** the kaizen loop | code-only: bindings, call sites, E0/E1/E2 assertions | `crates/aprender-contracts-cli/src/commands/kaizen.rs` | +| the upstream spec itself | **untracked in `infra`** — no commit, no history, unfetchable from any other host | `git -C ../infra ls-files --error-unmatch docs/specifications/paiml-ontology.md` | + +Two of these are the whole point of this section. **`pv kaizen` is blind to every surface that is +not Rust**: the train sweeps features, examples, README, `CLAUDE.md`, workflows, model files and +CSVs, and the loop that is supposed to improve on each sweep cannot see any of them. And **the +upstream spec is untracked**, so a quorum lane on gx10, yoga or mini cannot read the premise at all +— every ontology verdict it returns is unverifiable by construction. + +### §11.1 The rule — a sweep closes with an ontology delta, not a paragraph + +A **surface** is anything the train already sweeps: the `(crate, feature)` matrix (§4.1), the +examples (§4.2), the `apr` command registry, `contracts/`, the published docs, `.github/workflows/`, +model files, and the three triage surfaces (§6.2). + +A **delta** is one of exactly four things, any one of which closes the sweep: + +1. an **entity type + extractor** registered in Σ, with its `pc_extract` planted defect (§3.7, R-3); +2. a **shape** whose violation *is* the defect class just found, with the found instance as its RED + fixture; +3. a **verdict reason** added to the ONT-6 `Unknown{…}` set when the sweep could not decide — never + a new way to `Pass`; +4. a **`resolves:` target** (`path` · `symbol` · `ci-step` · `contract` · `comply-rule`) that turns a + prose claim into a checkable one. + +A sweep that ends in a known-red list, an issue body or a spec paragraph and nothing else **is not +finished**: it produced a fact only a human re-reading can use. The live example is this spec's own +§4.1 — 100 red pairs encoded as an `awk` matcher inside `night.yml`, no contract, no shape, and a +list that must be re-derived by hand before every undraft. That is the shape of debt §11 exists to +stop creating. + +Closing without a delta is allowed and must be named: `ont-delta: none ` in the PR body. +Unnamed is P0. + +### §11.2 The ratchet — four counters up, one down, `make ont-ratchet` only + +In `contracts/lint-baseline.json` beside `armed_gates` (§3.9): + +| Counter | Direction | Today | +|---|---|---| +| `ont.entity_types_registered` | ↑ | 0 | +| `ont.extractors_implemented` | ↑ | 0 | +| `ont.contracts_anchored` (`entity:` present) | ↑ | 0 | +| `ont.contracts_shaped` (`shape:` present) | ↑ | 0 | +| `ont.unanchored_but_bindable` (R-5: kernel-kind with a binding, or naming a file) | ↓ | `[U]` | + +Moves only through `make ont-ratchet` (R-6). A PR that lowers an ↑ counter or raises the ↓ one is +RED. No bulk rewrite: ≤5 corpus files per PR except a named ratchet touch (R-5, F-8). + +### §11.3 Cadence — one row per train, computed everywhere, armed per repo + +- **Nightly**, in the same job as §4.1/§4.2 and under the same vacuity floors: recompute the five + counters. Until `pv census` exists they come from the greps in §11.0 and are marked `[U]`, never + silently reported as 0. +- **One ONT row per train.** 16 rows outstanding; trains run every 2–3 days ⇒ ≈40 days to ONT-10 + `[A]`. That is a derived horizon, not a promise — the rows spill by §6.3 like any other work. +- **New gates arrive unarmed** (R-8, §3.9). An ONT gate lands computing everywhere; arming is a + later, separate PR whose body shows the counter it moved. +- **T-5 reads the `ontology:` line but does not gate on it** until `ont.extractors_implemented ≥ 1`. + A gate over an empty extractor set is the R-2 vacuity class — `Unknown`, never `Pass`. + +### §11.4 Why this makes the quorum more effective — the premise stops being prose + +§10 already requires the brief to carry the measurements. #3179 round 1 is what it costs when it +cannot: three facts had to be read out of the tree by hand before the vote meant anything, and the +round was void. An ontology is the mechanical form of that rule. + +- **Premises cite ids.** A brief names `contract:`, `symbol:::`, or a + `(crate, feature)` pair — something every lane resolves identically — not a sentence every lane + re-measures differently. +- **Verdicts are ONT-6 lattice elements**: `Pass · Unknown{} · Fail`, reason from the closed + set (§3.4). A lane answering outside the lattice has answered `Unknown{Prose}`: it does not arm, + and it does not count toward a majority. +- **Reduce is `meet = min`, not a vote count.** One `Fail` is `Fail`; `Pass` ∧ `Unknown` is + `Unknown`. A 2/3 majority over lattice-invalid verdicts is not a majority — which is precisely + what #3179 round 1 was. +- **The plant is a lattice element too.** `pc_shape` / `pc_extract` are the mechanical form of §10's + planted trap: a round whose planted defect was not caught is `Unknown{PositiveControlFailed}` and + the round is void, by rule rather than by the orchestrator noticing. +- Every quorum receipt records `premise=` and `verdict=`. + +### §11.5 Report lines + +Added to §7: + +``` +ontology: types /16 | extractors /14 | anchored / | shaped | bindable-unanchored | row merged | upstream sha <12hex|DRIFT> +``` + +and `quorum:` gains a final field `| lattice `. + +### §11.6 Stop conditions — added to §8 + +- **The upstream spec is not reachable at a committed sha** (today: untracked in `infra`, sha256 + `512a16d5e09c…`) → stop. No lane on another host can read the premise; every ontology verdict is + unverifiable by construction. Fixed in `infra` (ONT-P), not here. +- A train closed a surface sweep with no ontology delta and no `ont-delta: none ` → P0. +- An `ont.*` counter moved outside `make ont-ratchet` → RED (R-6). +- A gate special-cases `entity.type` beyond `proof` applicability → RED (R-17, F-25). + +### §11.7 Falsifiers + +| # | Rule | Assertion | Mutation | +|---|---|---|---| +| FR-1 | §11.1 | every train's `ontology:` line names the row merged, or `none: ` | omit the reason → report lint RED | +| FR-2 | §11.2 | the five counters are recomputed from the tree at T-5 and compared to `lint-baseline.json` | hand-raise `contracts_anchored` → RED | +| FR-3 | §11.3 | an ONT gate's landing PR arms nothing | arm it in the landing PR → RED | +| FR-4 | §11.4 | a quorum receipt whose verdict is not an ONT-6 element never reduces to `Pass` | record "looks fine" as a verdict → reduce RED | +| FR-5 | §11.0 | the sha256 pinned above matches the upstream file at read time | edit upstream → `upstream sha DRIFT` | From 6b14708e3f0fb6e1cb220d00e3d4aa5586833475 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Mon, 14 Sep 2026 15:15:14 +0200 Subject: [PATCH 04/10] =?UTF-8?q?docs(spec):=20=C2=A711=20review=20?= =?UTF-8?q?=E2=80=94=20ids=20disambiguated,=20#3179=20claim=20corrected,?= =?UTF-8?q?=20stops=20and=20report=20lines=20in=20one=20place?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review of the §11 draft against ONT-001 and against this spec's own conventions: - ONT-001's R-n/F-n/§n ids collided with this spec's T-5 predicates R-1..R-5. Upstream ids are now written `ONT R-n` / `ONT F-n` / `ONT §n` throughout §11. - The draft said #3179 round 1 was "a 2/3 majority over lattice-invalid verdicts". It was not: the verdicts were well-formed, the PREMISE was false (a launched kernel entry point that does not exist in the tree). Corrected to what the ontology actually does about it — `resolves: symbol` on the premise returns Unknown{…} at extraction, before any lane votes. - `contracts/lint-baseline.json` and `make ont-ratchet` do not exist in aprender yet; §11.0 and §11.2 now say so instead of naming them as if present. - A "sweep PR" is now a file predicate (night.yml, docs/specifications/**, the CLI registry, README.md, CLAUDE.md, any known-red list) so FR-1 can be a `check_pr_closes_issue.sh`-class PR-body check rather than a reading. #3268 itself is one and carries `ont-delta: none`. - The four stop conditions live in §8 and the two report lines in §7, once; §11.5/§11.6 point there instead of duplicating them. `ontology:` gains `deltas /` so §11.1 is measurable at T-5. - ONT §0.2's push constraint (never while a release-titled run is in progress) is named as §3's one-PR rule seen from the other repo. Pmat-Ticket: PMAT-1098 Co-Authored-By: Claude Fable 5.1 --- .../APR-RELEASE-001-train-and-build-kaizen.md | 97 +++++++++++-------- 1 file changed, 56 insertions(+), 41 deletions(-) diff --git a/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md b/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md index 26929f6516..d61ede5caa 100644 --- a/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md +++ b/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md @@ -371,7 +371,7 @@ capacity: milestone open | closure/day | capacity | verdict matrix: pairs | red | known-red | stale-known-red | examples started / debt: files touched | violations paid | worst before quorum: rounds | width | verdicts | overridden | lattice -ontology: types /16 | extractors /14 | anchored / | shaped | bindable-unanchored | row merged | upstream sha <12hex|DRIFT> +ontology: types /16 | extractors /14 | anchored / | shaped | bindable-unanchored | deltas / | row merged | upstream sha <12hex|DRIFT> reconcile: R1 fixed-open | R2 no-close | R3 dead-branches | R4 dirty>1train | R5 closure/arrival | receipt stops: next: train eligible at @@ -402,6 +402,15 @@ next: train eligible at - Any step would need an invented threshold, a second concurrent aprender PR in CI, `--allow-dirty`, or SSH into a host → stop. +- The upstream ontology spec is not reachable at a committed sha (today: untracked in `infra`, + sha256 `512a16d5e09c…`) → stop: no lane on another host can read the premise, and every ontology + verdict is unverifiable by construction. Fixed in `infra` (ONT-P), not here (§11). +- A sweep PR (§11.1) merged with no `ont-delta:` line → P0: a finding went into a prose sink and + nothing mechanical can read it back. +- An `ont.*` counter in `contracts/lint-baseline.json` moved outside `make ont-ratchet` → RED + (ONT R-6). +- A gate special-cases `entity.type` beyond `proof` applicability → RED (ONT R-17, ONT F-25). + ## §9 Budget and the one risk to measure first K̂ = 14 sessions (10 trains to `0.76` + P0, P1, infra, P2) · K = 18 · andon at 16 sessions @@ -457,7 +466,8 @@ deleted module and its public types cannot be recovered from a reviewer's memory Upstream: `ONT-001 v4.3` (`infra/docs/specifications/paiml-ontology.md`, sha256 `512a16d5e09c…`). Its §6 assigns **aprender** every row but three. This section is how those rows get worked continuously instead of in one heroic push, and what the train owes the ontology every time it -sweeps a surface. +sweeps a surface. Upstream ids are written `ONT R-n` / `ONT F-n` / `ONT §n` here; a bare `§n` is +this spec, and `R-1`–`R-5` without the prefix are the T-5 predicates (§6.3). ### §11.0 Measured baseline — `main` @ `fa6e35f23`, 2026-09-14 @@ -465,13 +475,14 @@ sweeps a surface. |---|---|---| | contracts may carry `entity:` / `shape:` / `evidence:` | **0 / 0 / 0** of 1818 | `grep -rlE '^entity:' contracts --include='*.yaml' \| wc -l` | | `pv census` — one cardinality, `by_entity_type`, `by_anchoring` (ONT-1) | absent | `pv --help` | -| `pv extract` → `contracts.nt`, the only triple producer (R-18) | absent | `pv --help` | -| `crates/aprender-contracts/src/ontology/` (decision 4) | directory does not exist | `ls` | -| 16 entity types named (§0.0); 14 extractors named, 7 in v1alpha1 scope (§3.7) | **0** | — | -| aprender implements every ONT row (§6) | **1 of 17** merged — ONT-2a, #3224 | `git log --grep='ONT-' origin/main` | -| an aprender ticket per row | 1 of 17 (#3222, closed, no milestone) | `gh issue list --search 'ONT in:title' --state all` | +| `pv extract` → `contracts.nt`, the only triple producer (ONT R-18) | absent | `pv --help` | +| `crates/aprender-contracts/src/ontology/` (ONT decision 4) | directory does not exist | `ls` | +| 16 entity types named (ONT §0.0); 14 extractors named, 7 in v1alpha1 scope (ONT §3.7) | **0** | — | +| `contracts/lint-baseline.json` with `armed_gates` (ONT §3.9) · `make ont-ratchet` (ONT R-6) | neither exists | `git ls-files contracts/lint-baseline.json` · `grep -n '^ont-ratchet' Makefile` | +| aprender implements every ONT row (ONT §6) | **1 of 17** merged — ONT-2a, #3224 | `git log --grep='ONT-' origin/main` | +| an aprender ticket per row | 1 of 17 (#3222, closed, no milestone) → epic #3269 | `gh issue list --search 'ONT in:title' --state all` | | `pv kaizen` **is** the kaizen loop | code-only: bindings, call sites, E0/E1/E2 assertions | `crates/aprender-contracts-cli/src/commands/kaizen.rs` | -| the upstream spec itself | **untracked in `infra`** — no commit, no history, unfetchable from any other host | `git -C ../infra ls-files --error-unmatch docs/specifications/paiml-ontology.md` | +| the upstream spec itself | **untracked in `infra`** — not gitignored; 23 of its 25 siblings are tracked | `git -C ../infra ls-files --error-unmatch docs/specifications/paiml-ontology.md` | Two of these are the whole point of this section. **`pv kaizen` is blind to every surface that is not Rust**: the train sweeps features, examples, README, `CLAUDE.md`, workflows, model files and @@ -483,11 +494,15 @@ upstream spec is untracked**, so a quorum lane on gx10, yoga or mini cannot read A **surface** is anything the train already sweeps: the `(crate, feature)` matrix (§4.1), the examples (§4.2), the `apr` command registry, `contracts/`, the published docs, `.github/workflows/`, -model files, and the three triage surfaces (§6.2). +model files, and the three triage surfaces (§6.2). A **sweep PR** is one that writes a finding into +a prose sink — operationally, one touching any of `.github/workflows/night.yml`, +`docs/specifications/**`, `contracts/apr-cli-commands-v1.yaml`, `README.md`, `CLAUDE.md`, or a +known-red list anywhere. A **delta** is one of exactly four things, any one of which closes the sweep: -1. an **entity type + extractor** registered in Σ, with its `pc_extract` planted defect (§3.7, R-3); +1. an **entity type + extractor** registered in Σ, with its `pc_extract` planted defect + (ONT §3.7, ONT R-3); 2. a **shape** whose violation *is* the defect class just found, with the found instance as its RED fixture; 3. a **verdict reason** added to the ONT-6 `Unknown{…}` set when the sweep could not decide — never @@ -501,12 +516,15 @@ finished**: it produced a fact only a human re-reading can use. The live example list that must be re-derived by hand before every undraft. That is the shape of debt §11 exists to stop creating. -Closing without a delta is allowed and must be named: `ont-delta: none ` in the PR body. -Unnamed is P0. +Every sweep PR body carries one line, in the form `scripts/check_pr_closes_issue.sh` already +enforces for `Closes #N`: `ont-delta: ` or +`ont-delta: none `. Absent is a PR-body lint failure, not a review comment. (#3268, the PR +that adds this section, is a spec paragraph and carries `ont-delta: none`.) ### §11.2 The ratchet — four counters up, one down, `make ont-ratchet` only -In `contracts/lint-baseline.json` beside `armed_gates` (§3.9): +In `contracts/lint-baseline.json` beside `armed_gates` (ONT §3.9) — the file and the `make` target +land with ONT-1/ONT-6; neither exists today: | Counter | Direction | Today | |---|---|---| @@ -514,10 +532,10 @@ In `contracts/lint-baseline.json` beside `armed_gates` (§3.9): | `ont.extractors_implemented` | ↑ | 0 | | `ont.contracts_anchored` (`entity:` present) | ↑ | 0 | | `ont.contracts_shaped` (`shape:` present) | ↑ | 0 | -| `ont.unanchored_but_bindable` (R-5: kernel-kind with a binding, or naming a file) | ↓ | `[U]` | +| `ont.unanchored_but_bindable` (ONT R-5: kernel-kind with a binding, or naming a file) | ↓ | `[U]` | -Moves only through `make ont-ratchet` (R-6). A PR that lowers an ↑ counter or raises the ↓ one is -RED. No bulk rewrite: ≤5 corpus files per PR except a named ratchet touch (R-5, F-8). +Moves only through `make ont-ratchet` (ONT R-6). A PR that lowers an ↑ counter or raises the ↓ one +is RED. No bulk rewrite: ≤5 corpus files per PR except a named ratchet touch (ONT R-5, ONT F-8). ### §11.3 Cadence — one row per train, computed everywhere, armed per repo @@ -526,10 +544,14 @@ RED. No bulk rewrite: ≤5 corpus files per PR except a named ratchet touch (R-5 silently reported as 0. - **One ONT row per train.** 16 rows outstanding; trains run every 2–3 days ⇒ ≈40 days to ONT-10 `[A]`. That is a derived horizon, not a promise — the rows spill by §6.3 like any other work. -- **New gates arrive unarmed** (R-8, §3.9). An ONT gate lands computing everywhere; arming is a - later, separate PR whose body shows the counter it moved. -- **T-5 reads the `ontology:` line but does not gate on it** until `ont.extractors_implemented ≥ 1`. - A gate over an empty extractor set is the R-2 vacuity class — `Unknown`, never `Pass`. +- **An ONT row PR obeys ONT §0.2**: never pushed while a release-titled run is in progress. It is + §3's one-aprender-PR-in-CI rule seen from the other repo. +- **New gates arrive unarmed** (ONT R-8, ONT §3.9). An ONT gate lands computing everywhere; arming + is a later, separate PR whose body shows the counter it moved. +- **T-5 carries the `ontology:` line but does not gate on it** until + `ont.extractors_implemented ≥ 1`. A gate over an empty extractor set is the ONT R-2 vacuity class + — `Unknown`, never `Pass`. The ratchet check (§11.2) is separate and arms from its first commit: + it is about the JSON file, not the extractors. ### §11.4 Why this makes the quorum more effective — the premise stops being prose @@ -539,13 +561,14 @@ round was void. An ontology is the mechanical form of that rule. - **Premises cite ids.** A brief names `contract:`, `symbol:::`, or a `(crate, feature)` pair — something every lane resolves identically — not a sentence every lane - re-measures differently. + re-measures differently. #3179's round-1 premise named a launched kernel entry point that does + not exist in the tree; as `resolves: symbol` it returns `Unknown{…}` at extraction, before any + lane votes, and an `Unknown` premise cannot reduce to `Pass`. - **Verdicts are ONT-6 lattice elements**: `Pass · Unknown{} · Fail`, reason from the closed - set (§3.4). A lane answering outside the lattice has answered `Unknown{Prose}`: it does not arm, - and it does not count toward a majority. + set (ONT §3.4). A lane answering outside the lattice has answered `Unknown{Prose}`: it does not + arm, and it does not count toward a majority. - **Reduce is `meet = min`, not a vote count.** One `Fail` is `Fail`; `Pass` ∧ `Unknown` is - `Unknown`. A 2/3 majority over lattice-invalid verdicts is not a majority — which is precisely - what #3179 round 1 was. + `Unknown`. A 2/3 majority with one lattice-invalid verdict reduces to `Unknown`, not to a majority. - **The plant is a lattice element too.** `pc_shape` / `pc_extract` are the mechanical form of §10's planted trap: a round whose planted defect was not caught is `Unknown{PositiveControlFailed}` and the round is void, by rule rather than by the orchestrator noticing. @@ -553,29 +576,21 @@ round was void. An ontology is the mechanical form of that rule. ### §11.5 Report lines -Added to §7: - -``` -ontology: types /16 | extractors /14 | anchored / | shaped | bindable-unanchored | row merged | upstream sha <12hex|DRIFT> -``` - -and `quorum:` gains a final field `| lattice `. +Two lines change in §7, and only there: `ontology:` is added, and `quorum:` gains +`| lattice `. -### §11.6 Stop conditions — added to §8 +### §11.6 Stop conditions -- **The upstream spec is not reachable at a committed sha** (today: untracked in `infra`, sha256 - `512a16d5e09c…`) → stop. No lane on another host can read the premise; every ontology verdict is - unverifiable by construction. Fixed in `infra` (ONT-P), not here. -- A train closed a surface sweep with no ontology delta and no `ont-delta: none ` → P0. -- An `ont.*` counter moved outside `make ont-ratchet` → RED (R-6). -- A gate special-cases `entity.type` beyond `proof` applicability → RED (R-17, F-25). +Four added to §8, and only there: an unreachable upstream sha; a merged sweep PR with no +`ont-delta:`; an `ont.*` counter moved outside `make ont-ratchet`; a gate special-casing +`entity.type` beyond `proof`. ### §11.7 Falsifiers | # | Rule | Assertion | Mutation | |---|---|---|---| -| FR-1 | §11.1 | every train's `ontology:` line names the row merged, or `none: ` | omit the reason → report lint RED | -| FR-2 | §11.2 | the five counters are recomputed from the tree at T-5 and compared to `lint-baseline.json` | hand-raise `contracts_anchored` → RED | +| FR-1 | §11.1 | every sweep PR body carries `ont-delta:` — a `scripts/check_pr_closes_issue.sh`-class predicate, run where that one runs in `ci.yml` | delete the line from a sweep PR → RED | +| FR-2 | §11.2 | the five counters are recomputed from the tree and compared to `lint-baseline.json`; a hand-moved counter is RED regardless of arming | hand-raise `contracts_anchored` → RED | | FR-3 | §11.3 | an ONT gate's landing PR arms nothing | arm it in the landing PR → RED | | FR-4 | §11.4 | a quorum receipt whose verdict is not an ONT-6 element never reduces to `Pass` | record "looks fine" as a verdict → reduce RED | | FR-5 | §11.0 | the sha256 pinned above matches the upstream file at read time | edit upstream → `upstream sha DRIFT` | From 7f94d8bb5cecf7830cac76fdbefb4d43c2bf4128 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Mon, 14 Sep 2026 15:20:11 +0200 Subject: [PATCH 05/10] =?UTF-8?q?docs(spec):=20final=20review=20=E2=80=94?= =?UTF-8?q?=20the=20amendments=20contradicted=20the=20original=20text=20in?= =?UTF-8?q?=20seven=20places?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Read start to finish. Every finding is a place where a dated amendment (§3.4 merge-queue parallelism, §3.5 SSH, §6.1 continuous triage, #3205 mini, the one-subagent rule) was landed beside original text that still said the opposite, so a reader could cite either. - §1 measured the fleet with `gh api …/actions/runners` — the call the operator rejected, a `busy` snapshot that cannot see ephemeral runners, and the hourly average that read 9.5 % while 15/16 workers were busy. Now the fleet-pack ledger record, instantaneous busy/online, both traps named. - §1 said "one PR in CI at a time means gate latency IS throughput"; §3.4 was amended to 3-parallel on 2026-09-12. Bound is now 3 × 72 h / p95. - `mini` is a declared full-time build host (#3205) and appeared only in a §8 bullet. Added to §0 row 0, §1, §5 P0·Pack fields and Done, §7 pack:, §8. - §0 row 3 still scheduled triage once per train; §6.1 made it per-wakeup. - §2 named the required check `ci / gate`; the rules API says `gate` and `workspace-test`, and `present` is not required. - §3.4 allowed "≤ 3 read-only subagents" against the one-at-a-time rule and §10's fan-out-through-agy. - §8's last bullet stopped on "a second concurrent aprender PR in CI" and on "SSH into a host" — both allowed by the amended §3.4/§3.5. Now stops on a host CONFIG change over SSH instead of forjar. - §9 asked for the 0.67 cascade wall to be measured; it was: 70 min, attended 0. That is 3.5× the [A] line, so by §9's own rule the cascade is the next kaizen target; where the minutes go is [U]. - §7 train: line said T-0..T-4; T-5 exists. §6's tail paragraph gets a §6.4 heading. §11.3 cited the one-PR rule §3.4 no longer has; fixed. - `make build-report` does not exist on main (P0·Instrument not done) — said so where p95 is marked [U]. Pmat-Ticket: PMAT-1098 Co-Authored-By: Claude Fable 5.1 --- .../APR-RELEASE-001-train-and-build-kaizen.md | 62 +++++++++++-------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md b/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md index d61ede5caa..ba79828e67 100644 --- a/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md +++ b/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md @@ -16,10 +16,10 @@ Implement docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md autonomo | # | If | Do | |---|---|---| -| **0** | `yoga` or `gx10` is under-utilised (§1 packing rule) while intel has queue pressure | **P0, minutes not a session:** arm every green PR, route what can leave intel, reap disk (§5 P0·Pack, P0·Reap); record the `pack:` line; then continue to the first matching row below | +| **0** | `yoga`, `gx10` or `mini` is under-utilised (§1 packing rule) while intel has queue pressure — and, every wakeup regardless, anything arrived since the last sample is untriaged (§6.1) | **P0, minutes not a session:** arm every green PR, route what can leave intel, reap disk (§5 P0·Pack, P0·Reap); triage what arrived (§6.1); record the `pack:` and `triage:` lines; then continue to the first matching row below | | 1 | ≥ 48 h since the last tag on `main` **and** no SKIPPED record for the current HEAD | run the train (§4) | | 2 | else a §5 row whose *Done* test fails at HEAD | do the first such row, one PR | -| 3 | else the last train (shipped or skipped) has no triage record | do the triage pass (§6) | +| 3 | else the last train (shipped or skipped) has no once-per-train triage record | do the once-per-train pass: §6.3 capacity check and the T-5 reconcile receipt | | 4 | else | emit the §7 report, exit 0 | Nothing in this spec asks a question. Running it ten times a day is safe. @@ -28,22 +28,28 @@ Nothing in this spec asks a question. Running it ten times a day is safe. Ship a tag every 48–72 h (`0.67 → 0.68 → …`) **on a clock, not on scope**, and keep shrinking the wall-clock from *PR opened* to *tag published* so the clock stays cheap. -The objective is elapsed time and green trains. **Packing rule (operator, 2026-09-12, verbatim):** "these two boxes: yoga and gx10 should be always 80% full of PRs from aprender if ANY queue pressure on intel … not acceptable to have slow releases when boxes are idel". Utilisation is not the goal for its own sake; an idle GPU box next to an intel queue is lost release time and is a **P0 defect**, not a state to tolerate. Measure it every wakeup: +The objective is elapsed time and green trains. **Packing rule (operator, 2026-09-12, verbatim):** "these two boxes: yoga and gx10 should be always 80% full of PRs from aprender if ANY queue pressure on intel … not acceptable to have slow releases when boxes are idel". Utilisation is not the goal for its own sake; an idle GPU box next to an intel queue is lost release time and is a **P0 defect**, not a state to tolerate. `mini` (Apple M4) was declared a full-time aprender build host on 2026-09-13 (#3205) and is under the same rule; its ceiling is the macOS-capable job classes, not capacity. Measure it every wakeup, **from the ledger, never from a runner-list snapshot**: ``` -gh api --paginate orgs/paiml/actions/runners --jq '.runners[] | "\(.name) \(.status) \(.busy)"' # busy/online per host prefix -intel pressure = any aprender job queued, or a workspace-test running, on intel -under-utilised = intel pressure AND (busy/online < 0.8 on yoga OR on gx10) +record = docs/build-ledger//--fleet-pack-*.json # written by §5 P0·Pack +intel pressure = aprender_runs_queued > 0, or a workspace-test running, on intel +under-utilised = intel pressure AND (busy/online < 0.8 on yoga, on gx10, OR on mini) ``` +Two measurement traps, both already paid for: a `busy` snapshot from the runners API cannot see +ephemeral runners, and an hourly **average** hides saturation — `occ_1h` read 9.5 % on 2026-09-14 +while 15 of 16 intel workers were busy at load 162. The `pack:` line carries the instantaneous +`busy/online` at sample time; an average is a trend, not a verdict. + Coupling, one line: ``` -max PRs per train ≈ 72 h / p95 `ci / gate` wall-clock (upper bound) +max PRs per train ≈ 3 × 72 h / p95 `ci / gate` wall-clock (upper bound; 3 = merge-queue parallelism, §3.4) ``` -One PR in CI at a time means gate latency *is* release throughput. Compute this on every -train. p95 is `[U]` until §5 P0 lands. +The merge queue builds 3 entries in parallel (§3.4); gate latency still bounds release throughput. +Compute this on every train. p95 is `[U]` until §5 P0 lands — `make build-report` does not exist on +`main` as of 2026-09-14. ## §2 Ground truth — verify at HEAD before writing anything @@ -52,7 +58,7 @@ train. p95 is `[U]` until §5 P0 lands. | `intel` — clean-room runner, 8 concurrent, memory-bound, 3.6 TB NVMe | `infra/machines/intel/forjar.yaml` | `[V]` snapshot | | `yoga` — CI runner, RTX 4060 8 GB, 32 GB RAM, 10G NIC needs `bolt.service` | `infra/machines/yoga/forjar.yaml` | `[V]` snapshot | | `gx10` — aarch64 GB10, sm_121, 120 GB unified; **not** a documented general runner | `infra/machines/gx10/forjar.yaml` | `[V]` snapshot | -| `main` protected; required check literally named `ci / gate` | org ruleset | `[V]` | +| `main` protected; required checks are exactly `gate` and `workspace-test` — `present` is NOT required, it is the review-receipt backlog | `gh api repos/paiml/aprender/rules/branches/main` | `[V]` 2026-09-14 | | Last tag = `git describe --tags --abbrev=0` on `main`; next minor = that + 1 | git | `[V]` live | | p95 `ci / gate`, tag→publish, cascade wall time (automated) | — | `[U]` unmeasured | | `workspace-test` is pinned `runs-on: [self-hosted, X64, Linux, clean-room]` (#3104) — the long pole never lands on gx10; #3139 lifts the pin (795 s on gx10-pool3, 34693750990) | `.github/workflows/ci.yml` | `[V]` 2026-09-12 | @@ -74,7 +80,8 @@ Live `forjar.yaml` beats this table. Record the diff in the receipt and continue (roadmap-additive guard, a red required check) and fix or trim it rather than park it. Under intel pressure, arm every green PR and prefer the ones whose jobs can land on `yoga`/`gx10`. Splitting *one* gate across hosts (§5 P2) is the other half of the same rule. "Parallel" - inside a session still means ≤ 3 read-only subagents, never two sessions merging. + inside a session means ONE Claude subagent at a time and fan-out through agy (§10) — never two + sessions merging. *(Amended 2026-09-12 by operator ruling; the previous text said "one aprender PR in CI at a time" and this session dequeued four PRs on it — #3175's group was running its workspace-test on yoga at the time.)* @@ -168,9 +175,10 @@ busy/online per host (§1), decide `intel pressure`, and act in minutes — arm withdrawn CI, trim non-additive roadmap diffs, route arch-neutral work off intel, dispatch the nightlies that produce T-1 evidence on the idle GPU boxes. Record the sample as a ledger record `docs/build-ledger//-fleet-pack.json` `{at, intel_busy, intel_online, gx10_busy, -gx10_online, yoga_busy, yoga_online, intel_pressure, verdict}`. +gx10_online, yoga_busy, yoga_online, mini_busy, mini_online, intel_pressure, aprender_runs_queued, +aprender_runs_live, idle_gpu_runners, verdict}` (5 records on `main` as of 2026-09-14, none yet with the `mini` fields). *Done:* over the trailing 10 trains, in every sample with `intel_pressure=true`, median busy/online -≥ 0.8 on both `yoga` and `gx10`; the §7 `pack:` line is never `P0-UNDERUTILIZED` two wakeups in a row. +≥ 0.8 on `yoga`, `gx10` and `mini`; the §7 `pack:` line is never `P0-UNDERUTILIZED` two wakeups in a row. The structural lever is `workspace-test` leaving its `X64` pin (#3139: 82,085 tests in 795 s on gx10 against 1,000–6,000 s on intel); until it lands, `pack` is bounded by the short jobs. @@ -335,6 +343,7 @@ and is committed to the ledger; the autopilot writes `DONE` only after `check_re every predicate 0. Falsifier: seed one fixed-but-open issue (a closed test issue reopened) — the check must go RED. +### §6.4 Lifecycle — stale, the closure ratchet, and what is recorded Queues stabilise when closure ≥ arrival; age falls when WIP is capped. That is the whole mechanism. @@ -361,10 +370,10 @@ mechanism. ``` APR-RELEASE-001 | did= | train=v0..0 | verdict= -train: step reached | skip reason | cascade wall min | attended min 0 +train: step reached | skip reason | cascade wall min | attended min 0 build: row | PR | records added gate: p95 ci/gate | max PRs/train | queue p95 intel yoga gx10 -pack: intel / | gx10 / | yoga / | intel-pressure | verdict +pack: intel / | gx10 / | yoga / | mini / | intel-pressure | verdict triage: arrival | closure | open PRs (age p95 ) | untriaged issues prs branches: total | no-PR | no-PR >7d | archived this pass capacity: milestone open | closure/day | capacity | verdict d> | spilled @@ -385,7 +394,7 @@ next: train eligible at - Measured max PRs/train < 10 `[A]` → stop cutting trains; finish P1/P2 first. - `yoga` or `gx10` has no runner unit in live `forjar.yaml` and the infra PR is unmerged → stop at P2. - Fewer than 20 ledger records → stop at P0. -- `yoga` or `gx10` under-utilised (§1) while intel has queue pressure, two wakeups in a row → P0: +- `yoga`, `gx10` or `mini` under-utilised (§1) while intel has queue pressure, two wakeups in a row → P0: stop the current build row, pack first (arm, route, reap), report the `pack:` line. - A runner box at or below `REAPER_CRITICAL_GB` free, or any job dead on ENOSPC → P0: reclaim over SSH now, then land the forjar change that makes it automatic, before anything else. @@ -399,9 +408,8 @@ next: train eligible at question to the operator. - The `P0`-labelled set of the next milestone alone exceeds its capacity (§6.3) → stop: the release is over-promised at the one priority level the operator controls, and only the operator can cut it. -- Any step would need an invented threshold, a second concurrent aprender PR in CI, - `--allow-dirty`, or SSH into a host → stop. - +- Any step would need an invented threshold, `--allow-dirty`, or a host CONFIG change made over SSH + instead of through forjar (§3.5 — SSH for measurement and for unclogging is expected) → stop. - The upstream ontology spec is not reachable at a committed sha (today: untracked in `infra`, sha256 `512a16d5e09c…`) → stop: no lane on another host can read the premise, and every ontology verdict is unverifiable by construction. Fixed in `infra` (ONT-P), not here (§11). @@ -416,9 +424,12 @@ next: train eligible at K̂ = 14 sessions (10 trains to `0.76` + P0, P1, infra, P2) · K = 18 · andon at 16 sessions or 2 consecutive skips `[A]`. -The 82-crate cascade is automated (ruling 2026-09-13) and its wall time is unmeasured. -Record cascade wall minutes at T-4 on the `0.67` train. Above ~20 min `[A]` it delays the -post-publish receipts and the cascade — not the build — becomes the next kaizen target. +The 82-crate cascade is automated (ruling 2026-09-13). **Measured on the `0.67` train** +(`docs/build-ledger/2026-09-13/e45eaab47-lambda-vector-train.json`): `t4_wall_minutes: 70`, +`attended_minutes: 0`. That is 3.5× the ~20 min `[A]` line this paragraph drew before any +measurement existed, so by its own rule the cascade — not the build — is the next kaizen target. +Where the 70 minutes go is `[U]`: record per-crate wall on the `0.68` train before naming a fix. +The ratchet takes §5 P3's shape (≤ 0.70 × baseline, `[A]`) once three trains have measured it. ## §10 Decision procedure — for the forks §6 refuses to make @@ -467,7 +478,8 @@ Upstream: `ONT-001 v4.3` (`infra/docs/specifications/paiml-ontology.md`, sha256 Its §6 assigns **aprender** every row but three. This section is how those rows get worked continuously instead of in one heroic push, and what the train owes the ontology every time it sweeps a surface. Upstream ids are written `ONT R-n` / `ONT F-n` / `ONT §n` here; a bare `§n` is -this spec, and `R-1`–`R-5` without the prefix are the T-5 predicates (§6.3). +this spec; a bare `R-1`–`R-5` is a T-5 predicate (§6.3) and an unhyphenated `R1–R6` is the publish +preflight (§3.7). ### §11.0 Measured baseline — `main` @ `fa6e35f23`, 2026-09-14 @@ -544,8 +556,8 @@ is RED. No bulk rewrite: ≤5 corpus files per PR except a named ratchet touch ( silently reported as 0. - **One ONT row per train.** 16 rows outstanding; trains run every 2–3 days ⇒ ≈40 days to ONT-10 `[A]`. That is a derived horizon, not a promise — the rows spill by §6.3 like any other work. -- **An ONT row PR obeys ONT §0.2**: never pushed while a release-titled run is in progress. It is - §3's one-aprender-PR-in-CI rule seen from the other repo. +- **An ONT row PR obeys ONT §0.2**: never pushed while a release-titled run is in progress, so a + moving branch never races T-0..T-3 of a train. - **New gates arrive unarmed** (ONT R-8, ONT §3.9). An ONT gate lands computing everywhere; arming is a later, separate PR whose body shows the counter it moved. - **T-5 carries the `ontology:` line but does not gate on it** until From 9678009c2485d997ba315a92e2eff26e45459199 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Mon, 14 Sep 2026 15:24:17 +0200 Subject: [PATCH 06/10] =?UTF-8?q?docs(spec):=20=C2=A712=20=E2=80=94=20the?= =?UTF-8?q?=20chain=20of=20reasoning:=20why=20the=20loop=20never=20termina?= =?UTF-8?q?tes=20and=20the=20four=20things=20it=20moves?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The spec had eleven sections of mechanism and no argument. §12 is the argument, step by step, each with its mechanism and its falsifier: §12.1 why it runs forever — the selector is total (row 4 always matches), a stop stops the session never the loop (every §8 line names the mechanism that prevents its recurrence), every counter is a ratchet, no number is a guess and a guess that becomes measurable is replaced (§9's 70-min cascade is the worked example), the ledger is the memory, the clock cuts the train. §12.2 the four axes — the repo, the released binaries, the CRUX competitors, the fleet — on the pv/ontology substrate. Each with its dated position, its mechanism, its ratchet and its §7 line. The spec had NO competitor axis before this: the train shipped binaries and nothing in it said where they stand. CRUX monitors 9 competitors through 275 stories (✅39 🔨80 ❌156 at v2.2 intake [C]; FALSIFY-CRUX-010 declared, not found under crates/ or scripts/ on main — [U] until landed). BEATS has 16 contracts; Ollama GPU decode is PARITY with a 0.90 floor, llama.cpp c=1 a narrow loss, fail-closed WON. Approaching = ❌→🔨→✅ by demand tier, which §6.3 already schedules; surpassing = a beat threshold that is a floor first and moves above 1.0 only on three agreeing medians on the PUBLISHED binary — which is why the post-publish dogfood (#3202) precedes any ratio. Hooks: `beats:` line in §7, a §8 stop on a beat RED on the published binary or a `measured-on published` claim from a dev build, a §0 pointer. Pmat-Ticket: PMAT-1098 Co-Authored-By: Claude Fable 5.1 --- .../APR-RELEASE-001-train-and-build-kaizen.md | 127 +++++++++++++++++- 1 file changed, 126 insertions(+), 1 deletion(-) diff --git a/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md b/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md index ba79828e67..3717756fb2 100644 --- a/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md +++ b/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md @@ -22,7 +22,9 @@ Implement docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md autonomo | 3 | else the last train (shipped or skipped) has no once-per-train triage record | do the once-per-train pass: §6.3 capacity check and the T-5 reconcile receipt | | 4 | else | emit the §7 report, exit 0 | -Nothing in this spec asks a question. Running it ten times a day is safe. +Nothing in this spec asks a question. Running it ten times a day is safe. Why the loop never +terminates and the four things it moves — repo, released binaries, CRUX competitors, fleet — on the +`pv` ontology substrate: §12. ## §1 Goal @@ -380,6 +382,7 @@ capacity: milestone open | closure/day | capacity | verdict matrix: pairs | red | known-red | stale-known-red | examples started / debt: files touched | violations paid | worst before quorum: rounds | width | verdicts | overridden | lattice +beats: won | parity | loss | measured-on | crux ✅ 🔨 ❌ | drift ontology: types /16 | extractors /14 | anchored / | shaped | bindable-unanchored | deltas / | row merged | upstream sha <12hex|DRIFT> reconcile: R1 fixed-open | R2 no-close | R3 dead-branches | R4 dirty>1train | R5 closure/arrival | receipt stops: @@ -418,6 +421,9 @@ next: train eligible at - An `ont.*` counter in `contracts/lint-baseline.json` moved outside `make ont-ratchet` → RED (ONT R-6). - A gate special-cases `entity.type` beyond `proof` applicability → RED (ONT R-17, ONT F-25). +- A beat contract RED on the published binary, or a `beats:` line saying `measured-on published` + from a dev build → stop the claim: the scoreboard says LOSS or `[U]`, the beat is the next build + row, and nothing is published ahead of its measurement (`docs/BEATS.md` withdrawal precedent). ## §9 Budget and the one risk to measure first @@ -606,3 +612,122 @@ Four added to §8, and only there: an unreachable upstream sha; a merged sweep P | FR-3 | §11.3 | an ONT gate's landing PR arms nothing | arm it in the landing PR → RED | | FR-4 | §11.4 | a quorum receipt whose verdict is not an ONT-6 element never reduces to `Pass` | record "looks fine" as a verdict → reduce RED | | FR-5 | §11.0 | the sha256 pinned above matches the upstream file at read time | edit upstream → `upstream sha DRIFT` | + +## §12 The chain of reasoning — why this loop never terminates, and the four things it moves + +This section explains the spec instead of adding to it. Read it as an argument: each step says +what the loop does, why, which section is the mechanism, and what would falsify the step. Numbers +are dated or marked `[U]`/`[C]`/`[A]`, as everywhere else. + +### §12.1 Why it runs forever + +1. **The selector is total.** §0 is "first match wins" and row 4 always matches: emit the §7 + report, exit 0. There is no state in which a session has nothing legal to do, so there is no + state in which the loop ends. Idle is a report, not a stop. *Falsified by:* a session that + exits without a §7 block. +2. **A stop stops the session, never the loop.** Every §8 line names the mechanism whose absence + caused it — a forjar unit, a ratchet target, a tracked spec, a comply rule. The next session's + first match is the row that lands that mechanism, so the same stop cannot recur. That is the + difference between kaizen and a retry loop. *Falsified by:* one §8 line stopping two consecutive + sessions with no PR between them. +3. **Every counter is a ratchet.** The known-red list only shrinks (§4.1); `armed_gates` never + shrinks (ONT §3.9); `ont.*` moves only through `make ont-ratchet` (§11.2); the stale label + tightens only after closure ≥ arrival held five trains (§6.4); P3 tightens ≤ 0.70× and only + downward (§5). Session N cannot undo session N−1, so the direction of a thousand sessions is + the direction of one. *Falsified by:* any counter moving the wrong way outside its named target. +4. **No number is a guess, and a guess that becomes measurable is replaced.** §3.3: every threshold + cites its command or is `[U]` and does not gate. The worked example is §9 — "~20 min `[A]`" was + drawn before a cascade had ever been timed; the 0.67 train measured 70, and §9 now says so and + names the cascade as the next target. That is the loop reading its own ledger and moving. + *Falsified by:* a threshold that gates while marked `[U]` or `[A]`. +5. **The ledger is the memory, and it is append-only.** §3.6: one file per run, never a shared + file. A session starts by reading it (§2, "verify at HEAD before writing anything") and ends by + writing to it (§7). Nothing the loop learns depends on a session remembering it. *Falsified + by:* a rule in this spec whose measurement cannot be derived from `docs/build-ledger/`. +6. **The clock cuts the train, not the scope.** §1: a tag every 48–72 h; §4: scope is whatever + merged before the cut. There is no "not ready" — a cut that cannot go green is SKIPPED against + that HEAD and the next train is 48 h away, so the loop cannot stall waiting for a feature. + *Falsified by:* two consecutive SKIPPED trains — §8's andon, the one place the loop escalates + instead of continuing. + +### §12.2 What it moves — four axes on one substrate + +Every wakeup is: pack + triage in minutes (§0 row 0) → the train if one is due (row 1) → else one +build row (row 2) → else the once-per-train pass (row 3) → else the report (row 4). Each axis +below has its measured position, its mechanism, its ratchet and its §7 line. The ledger carries +all of them, and the ratchets read the ledger. + +**A. The repo.** Position, 2026-09-14: 100 of 430 feature pairs RED and never built (§4.1); 727 +integration binaries against 73 the CI ever runs (the dark-targets triage) `[C]`; 11 complexity +violations paid in one day (§5.1); issues +149 net in 10 d at 6.1 closures/day (§6); 0 of 1818 +contracts anchored (§11.0). Mechanism: every sweep surfaces one class of dark defect, fixes it in +the same PR, and pays the debt tax on the file it touched. Ratchets: known-red shrinks, +`violations_paid` falls, closure ≥ arrival, `ont.*` up. Lines: `matrix:` `debt:` `triage:` +`branches:` `ontology:`. + +**B. The released binaries.** Position: v0.67.0 shipped 2026-09-13 by the automated train, +`attended_minutes: 0`, cascade 70 min; CUDA `apr` assets for arm64 and x86_64 are required on every +tag (operator, 2026-09-10). Mechanism: T-0..T-5 (§4) on the clock. The binary improves at exactly +the rate the repo does, because scope is what merged. The improvement that reaches a user is +measured on what the user installs — `cargo install aprender`, then the dogfood on *that* binary +(`apr-dogfood` G13 with `DOGFOOD_ALLOW_UNPINNED=1`, its deliberate crates.io mode) — in flight as +#3202, the post-publish phase. Until it lands, a train's binary claims are claims about the dev +build. Ratchet: §5 P3, tag→publish ≤ 0.70× baseline. Line: `train:`. + +**C. The competitors.** Before this section the spec had no competitor line: the train shipped +binaries and nothing in it said where they stand. Two instruments exist and both are ratchets. + +- **CRUX** (`docs/specifications/crux-competitive-research-ux-workflows.md`): 275 user stories + across 9 monitored competitors — Ollama, llama.cpp, PyTorch, Hugging Face, vLLM, OpenCLAW, + ecosystem interop, HF kernels-community, the APR-QA playbook. Coverage at v2.2 intake `[C]`: + ✅ 39 · 🔨 80 · ❌ 156 (56.7 % missing); registry drift is open as #3172 (0.69.0). + **Approaching** is ❌ → 🔨 → ✅ one story at a time, demand tier 5 first. Those are tickets, so + §6.3's capacity arithmetic already schedules them and no judgement call is needed. CRUX's + declared falsifier `FALSIFY-CRUX-010` is not found under `crates/` or `scripts/` on `main` + (2026-09-14): the coverage count is `[U]` until it is, and landing it is the first CRUX row. +- **BEATS** (`docs/BEATS.md`, 16 `contracts/beat-*.yaml`): Pillar 4 has Ollama GPU decode at + **parity** — 1.015–1.109× on sm_89, the 1.371× headline withdrawn, `beat_threshold: 0.9000` a + no-collapse floor and not a win; llama.cpp c=1 decode a narrow loss (1.55× faster); fail-closed + correctness WON 10/10. **Surpassing** is a beat contract per competitor verb whose threshold is a + floor first and moves above 1.0 only on three agreeing medians, measured on the published binary + on the host class the user gets. A CPU-only published binary made the llama.cpp ratio + uncomputable (APR-PARITY-001) — which is why axis B's post-publish dogfood precedes any ratio. + +Rules already in force and kept: never blanket-concede speed; never claim ahead of the +measurement; a withdrawn claim stays withdrawn until re-measured; a beat RED on the published +binary is the next build row and the scoreboard says LOSS. Line: `beats:`. + +**D. The fleet.** Position: intel is 16 workers on one 32-core box shared across repos; gx10 is +GB10 sm_121 aarch64; yoga is RTX 4060 sm_89; mini is Apple M4 on the macOS job classes, declared +full-time in #3205 and measured at 0.0 % all of 2026-09-14. Mechanism: §1's packing rule (≥ 80 % +of yoga, gx10 and mini under any intel pressure — a P0), §5 P0·Pack every wakeup, P2 routing by +capability then idleness (x86 CUDA → yoga; sm_121 → gx10; clean-room → intel; macOS classes → +mini), the merge queue 3-parallel (§3.4). "As quickly as possible" is one equation, +`max PRs/train ≈ 3 × 72 h / p95 gate`: gate latency *is* release throughput, so an idle box beside a +queue is lost release time, never spare capacity. Ratchet: P3, p95 ≤ 0.70× baseline. Line: `pack:`. + +**E. `pv` and the ontology — the substrate under A–D.** §11: `pv kaizen` is the loop's own +improvement instrument and today sees only Rust. Every surface in A–D becomes an entity type with +an extractor and a shape, so a sweep's finding is a `Fail` the next sweep computes instead of a +paragraph a human re-reads, and a quorum's premise is an id every lane resolves the same way. +Sixteen rows, one per train, every gate landing unarmed. This is what lets the loop's *judgement* +improve and not only its counters: each round's premise is checkable by the next. + +### §12.3 How the axes compose + +A improves B by construction — scope is what merged. B is claimable only through C's instruments, +on the published artifact. C's ❌ stories are A's tickets, scheduled by §6.3's arithmetic. D sets +the rate of all three, `3 × 72 h / p95`. E makes each finding of A–D machine-readable, so the next +session — or the next quorum lane — starts from a fact and not a memory. Remove any one and the +loop still runs (§12.1); it learns slower. That is why none of them is a stop condition and all of +them are report lines. + +### §12.4 Falsifiers + +| # | Step | Assertion | Mutation | +|---|---|---|---| +| FX-1 | §12.1.1 | every session ends with a §7 block, `NOOP` included | end one without → receipt lint RED | +| FX-2 | §12.1.2 | no §8 line stops two consecutive sessions with no PR between them | repeat a stop → andon | +| FX-3 | §12.1.3 | every counter on a §7 line has a named ratchet target and moves one way | move one backward by hand → RED | +| FX-4 | §12.2.C | `beats:` says `measured-on published` only when the dogfood ran on a `cargo install aprender` binary (G13: no embedded SHA, `DOGFOOD_ALLOW_UNPINNED=1`) | report `published` from a dev build → RED | +| FX-5 | §12.2.C | a beat's status in `docs/BEATS.md` matches its contract | change the status without the contract → `readme_contract` RED | From af7adb22936a26eb0f4c08eeaea1275579804817 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Mon, 14 Sep 2026 15:29:06 +0200 Subject: [PATCH 07/10] =?UTF-8?q?docs(spec):=20=C2=A711=20=E2=80=94=20infr?= =?UTF-8?q?a=20main=20tracks=20ONT-001=20v3.1;=20it=20is=20v4.3=20that=20i?= =?UTF-8?q?s=20untracked?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous wording said the upstream spec was untracked with "no commit, no history". Measured against origin/main in a fresh worktree: main has v3.1 (414 lines, f1269d0, infra#570). The untracked file is v4.3 (972 lines, sha256 512a16d5…), the version §11 is measured against. Substance unchanged — no other host can fetch v4.3 — detail corrected in §11.0 and §8. Pmat-Ticket: PMAT-1098 Co-Authored-By: Claude Fable 5.1 --- .../APR-RELEASE-001-train-and-build-kaizen.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md b/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md index 3717756fb2..98b7cdb102 100644 --- a/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md +++ b/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md @@ -413,8 +413,8 @@ next: train eligible at is over-promised at the one priority level the operator controls, and only the operator can cut it. - Any step would need an invented threshold, `--allow-dirty`, or a host CONFIG change made over SSH instead of through forjar (§3.5 — SSH for measurement and for unclogging is expected) → stop. -- The upstream ontology spec is not reachable at a committed sha (today: untracked in `infra`, - sha256 `512a16d5e09c…`) → stop: no lane on another host can read the premise, and every ontology +- The upstream ontology spec, at the version this spec cites, is not reachable at a committed sha + (2026-09-14: infra `main` has v3.1; v4.3, sha256 `512a16d5e09c…`, is an untracked draft) → stop: no lane on another host can read the premise, and every ontology verdict is unverifiable by construction. Fixed in `infra` (ONT-P), not here (§11). - A sweep PR (§11.1) merged with no `ont-delta:` line → P0: a finding went into a prose sink and nothing mechanical can read it back. @@ -500,13 +500,14 @@ preflight (§3.7). | aprender implements every ONT row (ONT §6) | **1 of 17** merged — ONT-2a, #3224 | `git log --grep='ONT-' origin/main` | | an aprender ticket per row | 1 of 17 (#3222, closed, no milestone) → epic #3269 | `gh issue list --search 'ONT in:title' --state all` | | `pv kaizen` **is** the kaizen loop | code-only: bindings, call sites, E0/E1/E2 assertions | `crates/aprender-contracts-cli/src/commands/kaizen.rs` | -| the upstream spec itself | **untracked in `infra`** — not gitignored; 23 of its 25 siblings are tracked | `git -C ../infra ls-files --error-unmatch docs/specifications/paiml-ontology.md` | +| the upstream spec itself | infra `main` tracks **v3.1** (414 lines, `f1269d0`); **v4.3** — the version this section is measured against, sha256 `512a16d5e09c…` — is an untracked draft on one box | `git -C ../infra show origin/main:docs/specifications/paiml-ontology.md \| head -1` vs `head -1` of the local file | Two of these are the whole point of this section. **`pv kaizen` is blind to every surface that is not Rust**: the train sweeps features, examples, README, `CLAUDE.md`, workflows, model files and CSVs, and the loop that is supposed to improve on each sweep cannot see any of them. And **the -upstream spec is untracked**, so a quorum lane on gx10, yoga or mini cannot read the premise at all -— every ontology verdict it returns is unverifiable by construction. +upstream version this section cites is untracked** — `main` has v3.1, the v4.3 draft lives on one +box — so a quorum lane on gx10, yoga or mini cannot read the premise at all, and every ontology +verdict it returns is unverifiable by construction. ### §11.1 The rule — a sweep closes with an ontology delta, not a paragraph From 85e7cdfda5d39bf8c43fbd7c3e876cf4ba65e6d6 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Mon, 14 Sep 2026 19:34:02 +0200 Subject: [PATCH 08/10] =?UTF-8?q?docs(spec):=20=C2=A74.3=20=E2=80=94=20the?= =?UTF-8?q?=20check=20is=20`deep`,=20and=20T-1=20cannot=20be=20satisfied?= =?UTF-8?q?=20by=20the=20lane=20merely=20EXISTING?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This spec named `ci / deep` in five places. No such check can exist in this repo, so T-1's already-done test was reading for a string that would never appear. GitHub prefixes a check with the job that CALLS it. `ci.yml` calls the org-wide `sovereign-ci.yml` as a job named `ci`, which is why this repo reports `ci / lint`, `ci / test`, `ci / gate` — and why `workspace-test`, `guard-tree`, `guard-cargo` and `gate`, which are top-level jobs in a workflow file, appear bare. Measured on this PR's own check list, both halves. `ci / deep` would therefore require a `deep` job inside the ORG-WIDE reusable workflow, with blast radius across every consuming repo. #3260's lane is `.github/workflows/deep.yml` with a job named `deep`, emitting `deep`. Amending this document is the cheap half of that trade; amending an org-wide workflow to match a string this document happened to write is the expensive half. §4.3 also records the sequencing hazard, which is the part that would have cost a train: $ gh workflow run deep.yml --ref PMAT-1098-ci-deep-lane HTTP 404: workflow deep.yml not found on the default branch `workflow_dispatch` is honoured only on the default branch, and a deep lane deliberately has no `pull_request` trigger. So the lane cannot be exercised AT ALL before it merges — its first execution would be the cut it gates — and §4 turns a red step into SKIPPED, so a lane born red costs the train silently instead of failing loudly. T-1 is consequently not satisfied by `deep` existing. The already-done test is a green `deep` run recorded against a sha ON MAIN, and the first such run must be a deliberate `gh workflow run deep.yml --ref main` after the lane lands and before a cut is attempted. A gate whose first run is the thing it certifies is the defect class this document exists to remove. spec_conformance.sh: exit 0. Pmat-Ticket: PMAT-1098 Co-Authored-By: Claude Opus 5 (1M context) --- .../APR-RELEASE-001-train-and-build-kaizen.md | 46 +++++++++++++++++-- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md b/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md index 98b7cdb102..95a85330d2 100644 --- a/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md +++ b/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md @@ -111,7 +111,7 @@ Live `forjar.yaml` beats this table. Record the diff in the receipt and continue | Step | Action | Skip if | |---|---|---| | **T-0 Cut** | cut sha = `main` HEAD; bump minor; `CHANGELOG` from merged PR titles since last tag | tag `v0.N.0` exists | -| **T-1 Deep** | `ci / deep` green on cut sha: full tests, doctests, `--no-default-features`, feature matrix (§4.1), GPU, every `cargo run --example` (§4.2) | green run recorded for this sha | +| **T-1 Deep** | `deep` green on cut sha (**the check is `deep`, not `ci / deep`** — §4.3): full tests, doctests, `--no-default-features`, feature matrix (§4.1), GPU, every `cargo run --example` (§4.2) | green run recorded for this sha | | **T-2 Dogfood** | `apr-dogfood` skill go/no-go receipt; `apr-cookbook` current; release notes generated | receipt exists for this sha | | **T-3 Promote** | tag; clean-room on the tag; GitHub release with T-2 receipt attached | release `v0.N.0` exists | | **T-4 Publish** | **automated** — the autopilot runs `cascade-drain.sh` after T-3's preflight; record cascade wall minutes; attended minutes are 0 by construction | all crates at `0.N.0` on crates.io | @@ -170,6 +170,42 @@ Asserting a duration here would be a wall-clock assertion in a required check, w Both clauses carry a **vacuity floor** (measured 980 examples / 430 pairs; floors 800 / 400): a discovery that finds almost nothing reports zero failures, which reads exactly like a pass. +### §4.3 The check is `deep`, and it cannot be proven before it is merged + +This spec said `ci / deep` in five places. No such check can exist here, and the T-1 +already-done test was reading for a string that would never appear. + +GitHub names a check after the job that emits it. A job inside a reusable workflow is +prefixed with the name of the job that CALLS it — `ci.yml` calls `paiml/.github`'s +`sovereign-ci.yml` as a job named `ci`, which is why this repo reports `ci / lint`, +`ci / test`, `ci / gate`. A job in a workflow file of its own carries no prefix, which is +why `workspace-test`, `guard-tree`, `guard-cargo` and `gate` appear bare. + +So `ci / deep` would require adding a `deep` job to the ORG-WIDE reusable workflow, with +blast radius across every repo that consumes it. The deep lane lives in its own +`.github/workflows/deep.yml` with a job named `deep`, and emits **`deep`**. Amending this +spec is the cheap half of that trade; amending an org-wide workflow to match a string this +document happened to write is the expensive half. + +**The sequencing hazard, measured 2026-09-14.** `deep.yml` fires only on `workflow_dispatch` +and a cron — deliberately, since a deep lane must not run per-PR. But: + +``` +$ gh workflow run deep.yml --ref PMAT-1098-ci-deep-lane +HTTP 404: workflow deep.yml not found on the default branch +``` + +`workflow_dispatch` is honoured only for workflows already on the default branch. A lane that +has no `pull_request` trigger and is not yet on `main` therefore **cannot be exercised at all +before it merges** — its first execution would be the cut it gates. §4's rule for a red step is +SKIPPED, so a lane born red costs the train silently rather than failing loudly. + +T-1 is therefore not satisfied by `deep` EXISTING. The train's already-done test is a green +`deep` run **recorded against a sha on `main`**, and the first such run must be a deliberate +`gh workflow run deep.yml --ref main` after the lane lands and before a cut is attempted. A +gate whose first run is the thing it certifies is the defect class this document exists to +remove; it must not be reintroduced by the gate that closes it. + ## §5 Build rows — in order, one PR each, only when no train is due **P0 · Pack** *(operator 2026-09-12; precedes everything while it fails)*. Every wakeup: sample @@ -197,13 +233,13 @@ total_s peak_rss_mb free_disk_gb exit`. Add `make build-report`: p50/p95 `total_ `queue_wait_s` per host, 10 slowest test targets, and the §1 PRs-per-train number. *Done:* ≥ 20 records; `make build-report` runs on a clean checkout. -**P1 · Two lanes.** `ci / gate` (every PR) = compile + **fast set**. `ci / deep` (tags + +**P1 · Two lanes.** `ci / gate` (every PR) = compile + **fast set**. `deep` (tags + nightly) = everything else. Fast set is derived from the ledger, not opinion: rank tests by failures-caught-per-second, keep the shortest prefix that caught every failure the full suite caught; record the cut and the escape count. Runner: `cargo nextest` for the fast set only if measured faster than `cargo test` on the same sha three times; record the delta either way. -*Done:* one sha runs both lanes; a tag cannot publish with `ci / deep` red; escape count +*Done:* one sha runs both lanes; a tag cannot publish with `deep` red; escape count over the next 10 merges is in the ledger. **P2 · Shard across hosts.** Infra PR first (`paiml/infra`): runner labels for `yoga` and @@ -392,7 +428,7 @@ next: train eligible at ## §8 Stop conditions — stop and report, do not work around - Two consecutive SKIPPED trains, or ≥ 72 h with no runnable train → andon. -- `ci / deep` red on a tag → no publish. +- `deep` red on a tag → no publish. - Cascade dry-run non-zero → stop before any real publish. - Measured max PRs/train < 10 `[A]` → stop cutting trains; finish P1/P2 first. - `yoga` or `gx10` has no runner unit in live `forjar.yaml` and the infra PR is unmerged → stop at P2. @@ -405,7 +441,7 @@ next: train eligible at queue has pressure → P0: that is a routing or job-class defect, not spare capacity. (`mini` measured at 0.0 % all of 2026-09-14 while declared full-time in #3205 — its ceiling is the macOS-capable job classes, not capacity.) -- A `ci / deep` known-red list names a pair that now PASSES → stop: the list is stale and the lane +- A `deep` known-red list names a pair that now PASSES → stop: the list is stale and the lane is asserting something that is no longer true (§4.1). - A design fork appears that §6 cannot decide without judgement → §10, not a coin flip and not a question to the operator. From 4e898e15f9905f312c3ff2d9d7f0db984a84c357 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Mon, 14 Sep 2026 19:37:58 +0200 Subject: [PATCH 09/10] =?UTF-8?q?feat(ont):=20=C2=A711.1=20gets=20a=20call?= =?UTF-8?q?er=20=E2=80=94=20"absent=20is=20a=20PR-body=20lint=20failure"?= =?UTF-8?q?=20was=20itself=20only=20a=20review=20comment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §11.1 states: "Every sweep PR body carries one line ... Absent is a PR-body lint failure, not a review comment." $ grep -rl ont-delta scripts/ .github/ Makefile (nothing) The rule shipped as prose in #3268 and nothing read it. A rule whose enforcement is "not a review comment", enforced only by review comment, is this repo's anti-theater class one level up from the guard it now sits beside in ci.yml. WHAT A SWEEP PR IS — and only half of it may be a list. * the prose sinks §11.1 NAMES are constants here, quoted, and printed on every run so drift between spec and guard is visible instead of silent. * "a known-red list anywhere" is DERIVED: the union of a working-tree `find` and the index, the same rule check_baseline_ratchets.sh uses and for the same two reasons — a new baseline arriving unclassified is how the class survives, and a tracked-only universe is a free pass for a file present but not yet added. The derived half is what makes it non-trivial, measured on real PRs: #3268 sweep via prose sink docs/specifications/... PASS (carries none+reason) #3277 sweep via KNOWN-RED LIST scripts/cb200_baseline.txt FAIL -> now fixed #3278 not a sweep PASS #3245 not a sweep PASS #3277 touches no prose sink at all. A hand-typed sink list would have passed it, and it is a true positive: that PR withdraws a wrong FAIL and adds an ONT-6 Unknown reason, which is precisely §11.1 form 3. Its body now carries `ont-delta: reason ont6-unread-window`. Case table, 15 rows, and it DISCRIMINATES: deleting the vocabulary check turns the table red (verified by mutation, not by reading). Rows cover kind-outside-the- vocabulary, none-without-a-reason, id-absent, case, leading space, and empty body. Vacuity floor: an empty changed-file list exits 2, because "not a sweep PR" is a verdict this guard could not have reached. Two defects found writing it, both kept as comments: * a RETURN trap runs after bash destroys the function's locals, so `rm -rf "$tmp"` died on an unbound variable AFTER fifteen green rows — a self-test that passed and exited 1. * bashrs SEC011: an unvalidated `rm -rf "$var"` is a delete-anything primitive. Now shape-checked before the sweep. bashrs 7.4.1: 0 errors. WORKFLOW CHANGE, stated rather than buried: this adds one step to ci.yml. §11.1 cannot exist without a caller, and guard_tree.sh runs check_*.sh BARE — which would run only the self-test, fifteen green rows judging no PR body, the exact failure the neighbouring step's comment documents. ont-delta: resolves scripts/check_pr_ont_delta.sh — §11.1 was a prose claim; this turns it into a checkable one (form 4). Pmat-Ticket: PMAT-1098 Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 29 +++++ scripts/check_pr_ont_delta.sh | 209 ++++++++++++++++++++++++++++++++++ 2 files changed, 238 insertions(+) create mode 100755 scripts/check_pr_ont_delta.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3edae037b1..e82b3e9a3f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1208,6 +1208,35 @@ jobs: set -euo pipefail printf '%s' "$PR_BODY" > "$RUNNER_TEMP/pr-body.txt" bash scripts/check_pr_closes_issue.sh --body "$RUNNER_TEMP/pr-body.txt" + # APR-RELEASE-001 §11.1, WIRED. The section shipped as prose in #3268 and + # nothing read it: `grep -rl ont-delta scripts/ .github/ Makefile` returned + # NOTHING, while §11.1's own text says "Absent is a PR-body lint failure, + # not a review comment". A rule whose enforcement is "not a review comment" + # and whose only mechanism IS a review comment is this repo's anti-theater + # class, one level up from the guard it sits beside. + # + # Same body-handling rule as the step above: the body arrives through + # `env:` as DATA and is never interpolated into the script. It is + # attacker-controlled by construction. + # + # The changed-file list is the other input, and it is what makes the guard + # non-trivial: half the sweep surface ("a known-red list anywhere") is + # DERIVED from the tree, not typed. Measured on #3277, which touches + # scripts/cb200_baseline.txt and no prose sink at all -- a hand-typed sink + # list would have passed it. + - name: A sweep PR closes with an ontology delta, not a paragraph (§11.1) + if: github.event_name == 'pull_request' + env: + PR_BODY: ${{ github.event.pull_request.body }} + PR_NUMBER: ${{ github.event.pull_request.number }} + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + printf '%s' "$PR_BODY" > "$RUNNER_TEMP/ont-body.txt" + gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files" --paginate \ + --jq '.[].filename' > "$RUNNER_TEMP/ont-files.txt" + bash scripts/check_pr_ont_delta.sh \ + --body "$RUNNER_TEMP/ont-body.txt" --changed "$RUNNER_TEMP/ont-files.txt" # aprender#2822: THE REPORT. Runs even when the job dies mid-cargo, # and takes its own final probe first -- without that, a failure at # step K leaves the last recorded boundary BEFORE K and the table diff --git a/scripts/check_pr_ont_delta.sh b/scripts/check_pr_ont_delta.sh new file mode 100755 index 0000000000..bffd96f94b --- /dev/null +++ b/scripts/check_pr_ont_delta.sh @@ -0,0 +1,209 @@ +#!/usr/bin/env bash +# check_pr_ont_delta.sh — APR-RELEASE-001 §11.1, WIRED. +# +# §11.1: "Every sweep PR body carries one line, in the form +# scripts/check_pr_closes_issue.sh already enforces for `Closes #N`: +# `ont-delta: ` or `ont-delta: none `. +# Absent is a PR-body lint failure, not a review comment." +# +# It was a review comment, because no lint existed. §11 shipped as prose in +# #3268 and nothing read it — `grep -rl ont-delta scripts/ .github/ Makefile` +# returned nothing. A rule whose enforcement is "not a review comment" and whose +# mechanism IS a review comment is the anti-theater class this repo keeps +# finding; this file is that rule's caller. +# +# WHAT A SWEEP PR IS (§11.1). Two halves, and only one of them may be a list: +# +# * the prose sinks the spec NAMES. These are quoted from §11.1 and change +# only when §11.1 changes, so they are constants here and are printed on +# every run so a drift between the two is visible rather than silent. +# * "a known-red list anywhere" — DERIVED, never typed. A new baseline file +# arriving unclassified is exactly how this class stays alive +# (check_baseline_ratchets.sh reason 1), and a tracked-only universe is a +# free pass for a baseline that is present but not yet added (reason 3). +# The universe is the UNION of a working-tree `find` and the index. +# +# WHAT IT DOES NOT DO. It does not judge whether the delta is GOOD — that is +# §11.2's ratchet, which moves only through `make ont-ratchet`. It decides only +# whether the sweep closed with one, which is the thing §11.1 states and nothing +# measured. +# +# bash scripts/check_pr_ont_delta.sh --self-test +# bash scripts/check_pr_ont_delta.sh --body B.txt --changed FILES.txt +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +usage() { + printf 'usage: %s (--body FILE --changed FILE) | --self-test\n' "$(basename "$0")" >&2 + exit 2 +} + +# The sinks §11.1 names, verbatim. A `docs/specifications/` entry is a PREFIX. +ONT_SWEEP_SINKS=( + ".github/workflows/night.yml" + "docs/specifications/" + "contracts/apr-cli-commands-v1.yaml" + "README.md" + "CLAUDE.md" +) + +# Known-red lists: union of the working tree and the index, never one alone. +known_red_lists() { + { + find "$REPO_ROOT/scripts" -maxdepth 1 -name '*baseline*.txt' -type f -printf 'scripts/%f\n' 2>/dev/null || true + git -C "$REPO_ROOT" ls-files 'scripts/*baseline*.txt' 2>/dev/null || true + } | LC_ALL=C sort -u +} + +# True when any changed path is a sweep surface. Prints the reason it fired. +sweep_reason() { # sweep_reason CHANGED_FILE + local changed="$1" f sink + local -a reds + mapfile -t reds < <(known_red_lists) + while IFS= read -r f; do + [ -n "$f" ] || continue + for sink in "${ONT_SWEEP_SINKS[@]}"; do + case "$sink" in + */) [ "${f##"$sink"}" != "$f" ] && { printf 'prose sink %s (§11.1)\n' "$f"; return 0; } ;; + *) [ "$f" = "$sink" ] && { printf 'prose sink %s (§11.1)\n' "$f"; return 0; } ;; + esac + done + for sink in "${reds[@]}"; do + [ "$f" = "$sink" ] && { printf 'known-red list %s (derived)\n' "$f"; return 0; } + done + done < "$changed" + return 1 +} + +# ont-delta line vocabulary. Anything else is OUTSIDE it, which is a failure and +# not a pass: an unknown key is a setting that does not exist. +ONT_DELTA_KINDS="type shape reason resolves" + +check_body_text() { # check_body_text BODY_FILE -> 0 ok, 1 bad; prints the verdict + local body="$1" line kind rest k ok + line="$(grep -iE '^[[:space:]]*ont-delta:' "$body" | head -1 || true)" + if [ -z "$line" ]; then + printf 'FAIL no `ont-delta:` line. §11.1: a sweep PR closes with a delta,\n' + printf ' not a paragraph. Use one of:\n' + printf ' ont-delta: type|shape|reason|resolves \n' + printf ' ont-delta: none \n' + return 1 + fi + rest="${line#*:}" + rest="${rest#"${rest%%[![:space:]]*}"}" + kind="${rest%%[[:space:]]*}" + rest="${rest#"$kind"}" + rest="${rest#"${rest%%[![:space:]]*}"}" + kind="$(printf '%s' "$kind" | tr '[:upper:]' '[:lower:]')" + if [ "$kind" = "none" ]; then + if [ -z "$rest" ]; then + printf 'FAIL `ont-delta: none` with no reason. "none" is a CLAIM about the\n' + printf ' sweep; an empty one is the paragraph §11.1 refuses.\n' + return 1 + fi + printf 'ok ont-delta: none — %s\n' "$rest" + return 0 + fi + ok=0 + for k in $ONT_DELTA_KINDS; do [ "$kind" = "$k" ] && ok=1; done + if [ "$ok" -ne 1 ]; then + printf 'FAIL `ont-delta: %s` is outside the vocabulary {%s|none}.\n' "$kind" "${ONT_DELTA_KINDS// /|}" + printf ' An unrecognised kind is a delta that does not exist (§11.1).\n' + return 1 + fi + if [ -z "$rest" ]; then + printf 'FAIL `ont-delta: %s` with no id. The delta must NAME the thing it added.\n' "$kind" + return 1 + fi + printf 'ok ont-delta: %s %s\n' "$kind" "$rest" + return 0 +} + +self_test() { + # NOT a RETURN trap: bash destroys the function's locals before the trap + # body runs, so `rm -rf "$tmp"` there dies on an unbound variable under + # `set -u` -- and the self-test had already PASSED by then, so the exit + # status flipped 0 -> 1 with fifteen green rows above it. Explicit cleanup. + local tmp pass=0 fail=0 rc + tmp="$(mktemp -d)" + # SEC011: an unvalidated `rm -rf "$tmp"` is a delete-anything primitive if the + # variable is ever empty or `/`. Prove the shape before the sweep, not after. + case "$tmp" in + /tmp/*|/var/tmp/*) : ;; + *) printf 'NO-GO: mktemp -d gave an unexpected path: %s\n' "$tmp" >&2; return 2 ;; + esac + + row() { # row NAME WANT BODY + local name="$1" want="$2" body="$3" + printf '%s' "$body" > "$tmp/b.txt" + set +e; check_body_text "$tmp/b.txt" >/dev/null 2>&1; rc=$?; set -e + if [ "$rc" -eq "$want" ]; then pass=$((pass+1)); printf ' ok %-46s want=%s\n' "$name" "$want" + else fail=$((fail+1)); printf ' FAIL %-46s want=%s got=%s\n' "$name" "$want" "$rc"; fi + } + printf 'check_pr_ont_delta self-test\n' + row "type with an id" 0 'ont-delta: type ONT-entity-run' + row "shape with an id" 0 'ont-delta: shape lockfile-pin-shape' + row "reason with an id" 0 'ont-delta: reason ont6-unread-window' + row "resolves with an id" 0 'ont-delta: resolves scripts/x.sh' + row "none WITH a reason" 0 'ont-delta: none spec paragraph only' + row "case-insensitive key" 0 'ONT-DELTA: type ONT-x' + row "leading whitespace" 0 ' ont-delta: type ONT-x' + row "body with other lines" 0 'Closes #1 + +ont-delta: type ONT-x + +trailer' + row "absent" 1 'a body that says nothing' + row "none with NO reason" 1 'ont-delta: none' + row "none, trailing spaces only" 1 'ont-delta: none ' + row "kind outside the vocabulary" 1 'ont-delta: entity ONT-x' + row "type with NO id" 1 'ont-delta: type' + row "shape with NO id" 1 'ont-delta: shape ' + row "empty body" 1 '' + printf 'self-test: %s passed, %s failed\n' "$pass" "$fail" + [ -n "$tmp" ] && [ -d "$tmp" ] && rm -rf "$tmp" + [ "$fail" -eq 0 ] +} + +main() { + local body="" changed="" reason + if [ $# -eq 0 ] || [ "${1:-}" = "--self-test" ]; then + self_test; return $? + fi + while [ $# -gt 0 ]; do + case "$1" in + --body) body="${2:-}"; shift 2 ;; + --changed) changed="${2:-}"; shift 2 ;; + *) usage ;; + esac + done + [ -n "$body" ] && [ -n "$changed" ] || usage + [ -f "$body" ] || { printf 'NO-GO: body file %s does not exist.\n' "$body" >&2; exit 2; } + [ -f "$changed" ] || { printf 'NO-GO: changed-files file %s does not exist.\n' "$changed" >&2; exit 2; } + + # Vacuity floor. A PR that changed nothing is not a PR; judging it as + # "not a sweep" would be a pass this guard never earned. + if [ ! -s "$changed" ]; then + printf 'NO-GO: the changed-file list is EMPTY, so "not a sweep PR" is a verdict\n' >&2 + printf 'this guard cannot have reached. Refusing rather than passing vacuously.\n' >&2 + exit 2 + fi + + printf '== ont-delta (APR-RELEASE-001 §11.1) ==\n' + printf 'prose sinks named by §11.1: %s\n' "${ONT_SWEEP_SINKS[*]}" + printf 'known-red lists derived: %s\n' "$(known_red_lists | wc -l)" + printf 'changed paths: %s\n' "$(grep -cve '^[[:space:]]*$' "$changed" || true)" + + if ! reason="$(sweep_reason "$changed")"; then + printf 'ok not a sweep PR — no prose sink and no known-red list touched.\n' + printf 'PASS\n' + return 0 + fi + printf 'sweep PR: %s\n' "$reason" + if check_body_text "$body"; then printf 'PASS\n'; return 0; fi + printf 'FAILED: §11.1 — a sweep that ends in prose and nothing else is not finished.\n' + return 1 +} + +main "$@" From 2abbc3953b6463aff93a9de76f12261620134417 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Mon, 14 Sep 2026 20:05:12 +0200 Subject: [PATCH 10/10] =?UTF-8?q?fix(ci):=20the=20=C2=A76=20R-2=20PR-body?= =?UTF-8?q?=20gate=20is=20dark=20on=20every=20PR=20=E2=80=94=20Actions=20s?= =?UTF-8?q?teps=20are=20fail-fast=20and=20one=20red=20guard=20skipped=2048?= =?UTF-8?q?=20of=2059?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Measured on run 34875945193 (PR #3268), job `guard-tree`: total steps 59 ran 10 skipped after the step-8 failure 48 step 8 failure Every cargo-free guard runs, and every failure is reported step 55 skipped A PR body must close every issue it cites (§6 R-2) step 56 skipped A sweep PR closes with an ontology delta (§11.1) GitHub Actions steps are fail-fast: one red step darkens every step after it. Why this is worse than a missed run. check_pr_closes_issue.sh exists because the 0.67.0 T-5 reconcile found 48 merged PRs since the previous tag of which only NINE closed anything. Its own wiring comment, four lines above this change, records that running it bare executes only its self-test — "Nine green rows about a regex, on every PR, judging NO PR body" — and that a real caller was the remedy. It got a real caller. The caller is masked. On any PR where one cargo-free guard is red the guard is dark exactly as it was before it was wired, and step 8 is currently red on EVERY PR (the pin/advisory deadlock, #3277), so §6 R-2 has been dark fleet-wide for the duration. `!cancelled()` rather than a bare event check: a step whose `if:` contains no status function is still skipped on a prior failure. These two read `github.event.pull_request.body` and nothing else, so no guard result can be their precondition — which is what makes this the narrow, defensible half of the fix. The other 46 skipped steps are mostly CASE TABLES — the mutation-verification proving the neighbouring guards can still go red. A case table that does not run is the theater this repo keeps deleting. They are NOT swept here: some (`target-watch:` markers) plausibly do depend on ordering, and a blanket always() over 48 steps would be its own kind of wrong. #3282 carries the classification. Stated rather than buried: unmasking means these steps now report on PRs that are already red for another reason, so the first sweep will surface findings that have been invisible for as long as the masking has. Same class as nextest --fail-fast hiding four dark failures across seven rounds; that lesson said "sweep the CI selection" and nothing had swept the STEP surface. ont-delta: none — a CI wiring fix; this PR's delta is already recorded against scripts/check_pr_ont_delta.sh. Pmat-Ticket: PMAT-1098 Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e82b3e9a3f..d668aa8af9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1201,7 +1201,15 @@ jobs: # pull_request only: on merge_group there is no pull_request payload, and # a body is fixed before a PR is ever enqueued. - name: A PR body must close every issue it cites, or say why not (§6 R-2) - if: github.event_name == 'pull_request' + # `!cancelled()` and NOT a bare event check. Actions steps are fail-fast: + # measured on run 34875945193, step 8 went red and 48 of this job's 59 + # steps were SKIPPED -- including this one and its §11.1 neighbour. This + # guard exists because the 0.67.0 reconcile found 48 merged PRs of which + # only 9 closed anything; masked by an unrelated guard, it is dark exactly + # as it was before it had a caller. It reads a PR body and nothing else, + # so no guard result can be its precondition. (#3282 classifies the other + # 46, which is a judgment call per step rather than one blanket edit.) + if: "!cancelled() && github.event_name == 'pull_request'" env: PR_BODY: ${{ github.event.pull_request.body }} run: | @@ -1225,7 +1233,15 @@ jobs: # scripts/cb200_baseline.txt and no prose sink at all -- a hand-typed sink # list would have passed it. - name: A sweep PR closes with an ontology delta, not a paragraph (§11.1) - if: github.event_name == 'pull_request' + # `!cancelled()` and NOT a bare event check. Actions steps are fail-fast: + # measured on run 34875945193, step 8 went red and 48 of this job's 59 + # steps were SKIPPED -- including this one and its §11.1 neighbour. This + # guard exists because the 0.67.0 reconcile found 48 merged PRs of which + # only 9 closed anything; masked by an unrelated guard, it is dark exactly + # as it was before it had a caller. It reads a PR body and nothing else, + # so no guard result can be its precondition. (#3282 classifies the other + # 46, which is a judgment call per step rather than one blanket edit.) + if: "!cancelled() && github.event_name == 'pull_request'" env: PR_BODY: ${{ github.event.pull_request.body }} PR_NUMBER: ${{ github.event.pull_request.number }}