Skip to content

fix(cbtop): a machine with no /proc was rendered as a machine with no RAM - #3245

Open
noahgift wants to merge 4 commits into
mainfrom
PMAT-1098-cbtop-macos-memory
Open

noahgift wants to merge 4 commits into
mainfrom
PMAT-1098-cbtop-macos-memory

Conversation

@noahgift

@noahgift noahgift commented Sep 14, 2026

Copy link
Copy Markdown
Contributor
F043 FALSIFIED: Total memory is 0

Measured on mini-m4 (Apple M4, 16 GB) while sweeping the integration targets on
darwin. The collector read /proc/meminfo unconditionally and swallowed the error:

let metrics = self.read_meminfo().unwrap_or_default();

There is no /proc on macOS, so that read is Err(ENOENT), unwrap_or_default()
turned it into an all-zero MemoryMetrics, and cbtop rendered 0 total / 0
available / 0 swap — presented exactly like a measurement, with nothing anywhere
saying the read had failed.

Reporting "0 bytes of memory" is worse than reporting nothing. It is the same shape as
the VRAM ledger's is_alive (#3205): an unmeasurable condition rendered as a
definite, wrong value. There the default released live reservations; here it reports a
machine with no RAM. The old Default was indistinguishable from a reading, so no
caller could have told the difference even if it wanted to.

Two fixes, and they are not the same fix

  1. The fail-open. MemoryMetrics carries measured, false on every failure path,
    and collect() uses unwrap_or_else(MemoryMetrics::unavailable) rather than a
    Default that looks like data. "0 because this host has no swap" and "0 because
    nothing was read" are now different values.

  2. The blindness. darwin gets a real reader — sysctl hw.memsize + hw.pagesize,
    vm_stat, sysctl vm.swapusage — so apr cbtop measures the box instead of being
    honest about not measuring it. mini is a full-time aprender build host; a monitor
    that cannot see it is not much of a monitor. An unsupported platform still returns
    Err and renders unavailable: no invented constant.

The issue asked which of these was wanted and called it a product call. apr cbtop is
a shipped subcommand and mini is now a first-class host, so it is both — (1) is
correct unconditionally, (2) is what makes (1) not merely honest.

The parsers are pure, which is the only reason the darwin rows ever run

No CI host lacks /proc, so a darwin-gated test is dark by construction.
parse_vm_stat and parse_swapusage take text, so their rows execute on Linux CI
against real output captured from mini over SSH:

sysctl -n hw.memsize   17179869184
sysctl -n hw.pagesize  16384
vm_stat                Pages free: 505274.  inactive: 143073.  speculative: 18831.
vm.swapusage           total = 1024.00M  used = 29.69M  free = 994.31M  (encrypted)

Eight unit rows; each mutation turns exactly its own red:

mutation rows red
measured = total_kb > 0= true a_meminfo_without_memtotal_is_not_a_measurement
Default { measured: true } unavailable_is_distinguishable_from_a_measurement
page size hardcoded to 4096 vm_stat_pages_become_kb_…, the_page_size_is_load_bearing
available = free (drop inactive+speculative) vm_stat_pages_become_kb_…

The page-size row earns its place: arm64 macOS uses 16 KiB pages and x86_64 uses 4 KiB,
so assuming 4096 under-reports an M-series box by — a wrong number rather than a
missing one, which is the whole complaint here.

Deleted: a unit test that asserted the defect — it required total_kb == 0
wherever /proc/meminfo was absent, encoding the zero as expected behaviour.

Verified on the box

A /tmp worktree on mini (macOS 26.6.2, arm64), removed afterwards:

cargo test -p aprender-cbtop --lib bricks::collectors::memory    8 passed
cargo test -p aprender-cbtop --test falsification f043           1 passed

F043 passes on darwin for the right reason — a real reading — rather than being relaxed
to accommodate a zero. On this box: 8 unit + 36 falsification pass, cargo fmt clean,
cargo clippy -D warnings clean.

And the target was dark

workspace-test is --workspace --lib, so tests/ never ran, and ci.yml's explicit
--test chain — the only thing that reaches an integration target — named no
aprender-cbtop target at all. F043 has never executed in CI on any platform. Added
to that chain, so the next regression is caught by CI rather than by hand on a laptop
months later. Part of #3239.

Not fixed here, filed as a note in the commit: crates/apr-cli/src/commands/cbtop_get_cpu_memory.rs
returns a hardcoded 64 on non-Linux, which reports 64 GB for mini's 16. Same class,
different file.

Closes #3236

no-close: #3239 stays OPEN. This wires ONE dark target of the 1,424 it counts; the
tiering defect it describes is untouched. #3205 is referenced only as the prior
instance of the same fail-open shape, not as work this PR does.

🤖 Generated with Claude Code

… RAM

    F043 FALSIFIED: Total memory is 0

Measured on mini-m4 (Apple M4, 16 GB) while sweeping the integration targets on
darwin. The collector read /proc/meminfo unconditionally and swallowed the error:

    let metrics = self.read_meminfo().unwrap_or_default();

There is no /proc on macOS, so that read is Err(ENOENT), unwrap_or_default()
turned it into an all-zero MemoryMetrics, and cbtop rendered 0 total / 0
available / 0 swap -- presented exactly like a measurement, with nothing
anywhere saying the read had failed.

Reporting "0 bytes of memory" is worse than reporting nothing. It is the same
shape as the VRAM ledger's is_alive (#3205): an UNMEASURABLE condition rendered
as a definite, wrong value. There the default released live reservations; here it
reports a machine with no RAM. The old Default was indistinguishable from a
reading, so no caller could have told the difference even if it wanted to.

Two fixes, and they are not the same fix.

1. THE FAIL-OPEN. MemoryMetrics carries `measured`, false on every failure path,
   and collect() uses unwrap_or_else(MemoryMetrics::unavailable) rather than a
   Default that looks like data. "0 because this host has no swap" and "0 because
   nothing was read" are now different values.

2. THE BLINDNESS. darwin gets a real reader -- sysctl hw.memsize + hw.pagesize,
   vm_stat, sysctl vm.swapusage -- so `apr cbtop` measures the box instead of
   being honest about not measuring it. mini is a full-time aprender build host;
   a monitor that cannot see it is not much of a monitor. An unsupported platform
   still returns Err and renders unavailable: no invented constant.

   (Note for a separate ticket: crates/apr-cli/src/commands/cbtop_get_cpu_memory.rs
   returns a hardcoded 64 on non-Linux, which reports 64 GB for mini's 16. Same
   class, different file, not touched here.)

THE PARSERS ARE PURE, WHICH IS THE ONLY REASON THE DARWIN ROWS EVER RUN. No CI
host lacks /proc, so a darwin-gated test is dark by construction. parse_vm_stat
and parse_swapusage take text, so their rows execute on Linux CI against real
output captured from mini:

    sysctl -n hw.memsize   17179869184
    sysctl -n hw.pagesize  16384
    vm_stat                Pages free: 505274.  inactive: 143073.  speculative: 18831.
    vm.swapusage           total = 1024.00M  used = 29.69M  free = 994.31M

Eight unit rows, each mutation turning exactly its own red:

  measured = total_kb > 0  ->  = true        a_meminfo_without_memtotal_...
  Default { measured: true }                 unavailable_is_distinguishable_...
  page size hardcoded to 4096                vm_stat_pages_...,  the_page_size_is_load_bearing
  available = free (drop inactive+spec)      vm_stat_pages_become_kb_...

The page-size row earns its place: arm64 macOS uses 16 KiB pages and x86_64 uses
4 KiB, so assuming 4096 under-reports an M-series box by 4x -- a wrong number
rather than a missing one, which is the whole complaint here.

Deleted: a unit test that asserted the DEFECT. It required total_kb == 0 wherever
/proc/meminfo was absent -- encoding the zero as expected behaviour.

VERIFIED ON THE BOX. /tmp worktree on mini (macOS 26.6.2, arm64), removed after:

  cargo test -p aprender-cbtop --lib bricks::collectors::memory   8 passed
  cargo test -p aprender-cbtop --test falsification f043          1 passed

F043 passes on darwin for the right reason -- a real reading -- rather than being
relaxed to accommodate a zero. On this box: 8 unit + 36 falsification pass,
cargo fmt clean, clippy -D warnings clean.

AND THE TARGET WAS DARK. workspace-test is --workspace --lib, so tests/ never
ran; ci.yml's explicit --test chain is the only thing that reaches an integration
target and named no aprender-cbtop target at all. F043 has never executed in CI
on any platform. Added to that chain, so the next regression is caught by CI
instead of by hand on a laptop months later. Part of #3239.

Pmat-Ticket: PMAT-1098

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@noahgift
noahgift enabled auto-merge September 14, 2026 05:03
@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown

§13.11 rung 1 — quorum shadow verdict

S13-SHADOW pr=3245 head=9344d80f8ab2df24ab5311571740ce2fb909128b verdict=REFUSE class=Q1 arm_rc=1

Shadow mode: this records a verdict and merges nothing. A refusal
to arm is not a block (§13 adds zero rows to §7) — the pull request is
exactly as green as it was.

noahgift added a commit that referenced this pull request Sep 14, 2026
…eliberately left

    assertion `left == right` failed: v2 F32 byte length drifted from golden_v2.apr
      left: 516    right: 1092

Not a writer regression, and not a stale fixture to regenerate either. Measured
by parsing both artifacts rather than reasoning about them:

    produced metadata  189 bytes,  9 keys
    golden   metadata  715 bytes, 35 keys
    ONLY IN GOLDEN: 26 keys -- architecture, author, chat_format, hidden_size,
                    num_layers, rope_theta, vocab_size, ...   every one null
    ONLY IN PRODUCED: none
    DIFFERENT VALUES: none

Every key the two share holds an identical value.

`git log -S skip_serializing_if` names the change: #2254, `fix(finetune): apr
finetune --merge produces a directly-runnable .apr`, which added
`#[serde(default, skip_serializing_if = "Option::is_none")]` to AprV2Metadata.
golden_v2.apr was captured in #2236, before it. The writer is correct, the change
was deliberate, and 516 bytes carries exactly what 1092 did.

REGENERATING golden_v2.apr WOULD HAVE BEEN THE EASY FIX AND THE WRONG ONE.
That file is the PRE-EXTRACTION oracle -- bytes written by the format code while
it still lived in aprender-core -- and `golden_v2_loads_in_leaf` exists to prove
the extracted leaf still READS them. It passes today (3 passed, 1 failed: only
the byte-identity row was red). Overwrite it and that test proves nothing but
that the leaf reads its own output.

So the oracle STAYS at 1092 bytes and byte-identity gets a fixture of its own,
golden_v2_current.apr (516). Both properties kept: old artifacts still parse, and
today's writer is still byte-pinned against silent drift.

A SECOND GOLDEN IS A LICENCE TO SMUGGLE, so it does not stand alone.
`the_two_v2_goldens_carry_the_same_model` parses both and proves the 516-byte
fixture is the same model as the oracle -- metadata values, tensor names, shapes,
F32 payload -- and not some other artifact substituted for one that would not
match.

AND THE FAILURE NOW SAYS WHICH KIND IT IS. A byte diff has two causes with
opposite fixes; reporting "bytes differ" makes every reader re-derive that, which
is how this sat for two months. Both branches proved by mutation:

  fixture := the 1092 oracle   -> "SERIALIZATION drift ... both carry the same
                                   model ... regenerate golden_v2_current.apr ...
                                   golden_v2.apr must NOT be regenerated"
  one weight 8.0 -> 8.5        -> "CONTENT drift ... do NOT carry the same model.
                                   This is a writer defect, not a spelling change."

The first reproduces #3235's original report verbatim and answers it.

`same_v2_model` is what the split rests on, so it gets rows of its own rather
than only what the committed pair happens to exercise -- they differ ONLY by
null-spelled fields, which drives one of its two symmetric directions. The new
row drives both, plus a real value difference and a real tensor difference;
deleting either direction turns exactly that row red.

Decomposed into fields_agree / metadata_agrees / tensors_agree /
one_tensor_agrees because the pre-commit gate refused the first draft at
Cyclomatic 30 / Cognitive 25 -- correctly: a comparison nobody can read is a
comparison nobody checks.

Not fixed here: the target is still dark. `grep -c golden_fixtures
.github/workflows/*.yml` is 0, which is why this ran for two months after #2254
without failing. The `--test` chain is edited by #3245 this cycle and only one PR
may hold that line, so the wiring goes there.

clippy -D warnings clean; apr-format 94 lib + 6 golden + 2 sovereignty pass.

Closes #3235

Pmat-Ticket: PMAT-1098

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
noahgift added a commit that referenced this pull request Sep 14, 2026
…e it

`grep -c golden_fixtures .github/workflows/*.yml` was 0. workspace-test is
`--workspace --lib` plus one explicit `--test` list, and this target was not on
it, so a GOLDEN guard for the on-disk format has never executed in CI. That is
the worst kind of dark target: a golden test exists precisely to catch silent
format drift, so its absence is invisible by construction -- it missed #2254's
serialization change for two months.

WIRED FROM THIS PR RATHER THAN #3245, ON PURPOSE. golden_fixtures is RED on main
today. A PR that arms it without also fixing it turns main red the moment it
merges first -- the "shrink-only baseline split across two PRs strands the second"
shape, one step worse because here the stranded half is main itself. #3245 wires
its own dark target (aprender-cbtop) and carries its own fix; this PR does the
same for apr-format. Whichever lands second resolves a one-line conflict on this
chain, which is the cheap half of the trade.

Pmat-Ticket: PMAT-1098

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@noahgift noahgift added this to the 0.68.0 milestone Sep 14, 2026
noahgift added a commit that referenced this pull request Sep 14, 2026
…as itself only a review comment

§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) <noreply@anthropic.com>
@noahgift
noahgift disabled auto-merge September 14, 2026 17:42
@noahgift
noahgift enabled auto-merge September 14, 2026 18:38
@noahgift
noahgift added this pull request to the merge queue Sep 14, 2026
@noahgift
noahgift removed this pull request from the merge queue due to a manual request Sep 14, 2026
noahgift added a commit to guyernest/aprender that referenced this pull request Sep 15, 2026
…tinuous triage, decision procedure, ontology kaizen (§11), and the chain of reasoning (§12) (paiml#3268)

* docs(spec): APR-RELEASE-001 §4.1/§4.2/§5.1/§10 — the work the train did not name

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 (paiml#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** (paiml#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) <noreply@anthropic.com>

* docs(spec): §6.1/§6.2/§6.3 — triage is continuous, covers branches, and a milestone must fit its date

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) <noreply@anthropic.com>

* docs(spec): §11 — a surface sweep ends in `pv`, or it did not end

ONT-001 v4.3 §6 assigns aprender every ontology row but three. Measured at
fa6e35f: 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 <reason>`. 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. paiml#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) <noreply@anthropic.com>

* docs(spec): §11 review — ids disambiguated, paiml#3179 claim corrected, stops and report lines in one place

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 paiml#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.
  paiml#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 <n>/<sweep PRs>` 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 <noreply@anthropic.com>

* docs(spec): final review — the amendments contradicted the original text in seven places

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, paiml#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 (paiml#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 <noreply@anthropic.com>

* docs(spec): §12 — the chain of reasoning: why the loop never terminates and the four things it moves

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 (paiml#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 <noreply@anthropic.com>

* docs(spec): §11 — infra main tracks ONT-001 v3.1; it is v4.3 that is untracked

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 <noreply@anthropic.com>

* docs(spec): §4.3 — the check is `deep`, and T-1 cannot be satisfied by the lane merely EXISTING

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. paiml#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) <noreply@anthropic.com>

* feat(ont): §11.1 gets a caller — "absent is a PR-body lint failure" was itself only a review comment

§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 paiml#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:

  paiml#3268  sweep via prose sink docs/specifications/...        PASS (carries none+reason)
  paiml#3277  sweep via KNOWN-RED LIST scripts/cb200_baseline.txt FAIL -> now fixed
  paiml#3278  not a sweep                                          PASS
  paiml#3245  not a sweep                                          PASS

paiml#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) <noreply@anthropic.com>

* fix(ci): the §6 R-2 PR-body gate is dark on every PR — Actions steps are fail-fast and one red guard skipped 48 of 59

Measured on run 34875945193 (PR paiml#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, paiml#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. paiml#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) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ci.yml:570 is a single ~4000-character line holding every explicit `--test`
target as one `bash -c 'a && b && ...'` string. Every PR that adds a target
edits that one line, so two such PRs always collide -- the same shared-mutable
-file contention roadmap.yaml has, on one line instead of a file.

Both sides only APPENDED: main added 3 targets, this branch added 1
(`cargo test -p aprender-cbtop --test falsification`). The resolution is the
union with main's order authoritative, 38 + 1 = 39. Nothing was removed on
either side, so no intent is being overridden. ci.yml still parses.

Pmat-Ticket: PMAT-3228

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant