diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3edae037b1..d668aa8af9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1201,13 +1201,58 @@ 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: | 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) + # `!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 }} + 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/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md b/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md index 4827d1fab5..95a85330d2 100644 --- a/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md +++ b/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md @@ -16,34 +16,42 @@ 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. +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 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 +60,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 +82,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.)* @@ -102,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, GPU, every `cargo run --example` | 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 | @@ -111,6 +120,92 @@ 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. + +### §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 @@ -118,9 +213,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. @@ -137,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 @@ -166,7 +262,34 @@ 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. -## §6 Triage pass — once per train, no judgement calls — and the T-5 reconcile, a HARD gate +### §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 — 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, @@ -176,6 +299,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 | @@ -191,12 +381,23 @@ 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. - 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`. @@ -207,11 +408,18 @@ 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 -triage: arrival | closure | open PRs (age p95 ) | untriaged +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 +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: next: train eligible at @@ -220,23 +428,343 @@ 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. - 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. -- Any step would need an invented threshold, a second concurrent aprender PR in CI, - `--allow-dirty`, or SSH into a host → stop. +- 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 `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. +- 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, `--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, 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. +- 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 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 + +§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 `. + + +## §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. Upstream ids are written `ONT R-n` / `ONT F-n` / `ONT §n` here; a bare `§n` is +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 + +| 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 (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 | 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 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 + +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 **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 + (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 + 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. + +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` (ONT §3.9) — the file and the `make` target +land with ONT-1/ONT-6; neither exists today: + +| 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` (ONT R-5: kernel-kind with a binding, or naming a file) | ↓ | `[U]` | + +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 + +- **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. +- **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 + `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 + +§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. #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 (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 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. +- Every quorum receipt records `premise=` and `verdict=`. + +### §11.5 Report lines + +Two lines change in §7, and only there: `ontology:` is added, and `quorum:` gains +`| lattice `. + +### §11.6 Stop conditions + +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 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` | + +## §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 | 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 "$@"