Skip to content

PR3: a roadmap edit without its fragment is refused — the contention #3297 removed cannot come back - #3352

Merged
noahgift merged 3 commits into
mainfrom
PMAT-3296-fragment-required
Sep 16, 2026
Merged

noahgift merged 3 commits into
mainfrom
PMAT-3296-fragment-required

Conversation

@noahgift

Copy link
Copy Markdown
Contributor

§3 of the queue architecture, and a gate the 0.68 cut requires. #3297 (§1) moved roadmap entries to docs/roadmaps/entries/<ID>.yaml so PRs are pairwise disjoint on the roadmap — but nothing stopped a PR from writing the monolith again, and two workers had to revert exactly that tonight, because pmat work add still writes docs/roadmaps/roadmap.yaml.

The measurement that decided the rule

Checking the aggregate alone is not enough: roadmap_fragments.py aggregate --check takes roadmap.yaml as its own base, so an entry written straight into the base is a fixed point — pmat work add + aggregate --check returns rc 0.

Sharpest RED evidence, a real commit adding PMAT-3294 at its sorted slot, monolith only:

existing guard verdict
check_roadmap_sorted.sh rc 0
check_roadmap_diff_additive.sh rc 0 (added=1 reserialised=0)
roadmap_fragments.py aggregate --check rc 0
this guard rc 1

No existing guard can see it. The diff rule is the load-bearing half.

The rule

Over base..head: every top-level entry whose bytes change in roadmap.yaml must have docs/roadmaps/entries/<ID>.yaml changing in the same diff; and whenever either side changes, head's roadmap.yaml must equal aggregate(head's entries/). A preamble-only change and a diff touching neither side still pass.

It declines to judge what it cannot see

resolve_base judges HEAD^1 in push shape, so a bare run on main would grade main's last merge — which retroactively refused 2 of the last 8 first-parent commits. A guard that turns main red for changes that landed before it existed is re-litigating history, not measuring a change. So in push shape it prints the shape, the base it would have used, and what it therefore left ungraded, and exits 0. Rows 13/14 are the pair that proves the point: same commit, same fragment-less content — origin/main=HEAD → SKIP rc 0; origin/main=HEAD~1 → REFUSE rc 1. Retro over the last 8 first-parent commits: 8/8 exit 0 in push shape.

Proof

14 case-table rows, hermetic, in temp repos, --self-test 14/14. Two mutations, each turning named rows red: neutering the aggregate conjunct → row 3; comparing id sets instead of bytes → rows 5, 6, 8, 11. bashrs 7.4.1: 0 errors.

What a ticket does now, and it is in the failure message

pmat work add "<title>" --github-issue <N>
python3 scripts/lib/roadmap_fragments.py adopt <ID>

adopt moves the entry into its fragment — proved to parse alone, to carry the same mapping, and to refuse an entry that defines an anchor other entries alias — then regenerates the aggregate, which also relocates pmat's tail-append to its sorted slot. Proved on PMAT-3294: sorted PASS (890 ids), added=1 lifecycle=0 reserialised=0 deleted=0, pmat work validate PASS.

Found, not fixed, and filed separately: scripts/lib/resolve_base.sh tests git rev-list --first-parent … | grep -qx, and grep -q's early exit SIGPIPEs rev-list — rc 0 without pipefail, 141 with it, which every caller sets. check_roadmap_diff_additive.sh sources the same defect.

Refs #3294, #3296, #3297

no-close: #3294 stays open — it is the five-whys on drain rate, and this closes only the roadmap-contention leg.

ont-delta: none this PR adds a guard and a case table; it adds no obligation, equation or contract row.

🤖 Generated with Claude Code

noahgift and others added 2 commits September 16, 2026 10:52
#3297 made docs/roadmaps/roadmap.yaml a GENERATED aggregate of
docs/roadmaps/entries/ so that two PRs touching the roadmap touch two
different files. Nothing enforced it. Measured on a clean worktree cut
from origin/main, 2026-09-16:

    $ pmat work add "..." --github-issue 3294
    M docs/roadmaps/roadmap.yaml        <- the MONOLITH, 16 lines, no fragment
    $ python3 scripts/lib/roadmap_fragments.py aggregate --check
    ok  roadmap.yaml == aggregate(1 fragment(s)), idempotent   # rc=0

The existing aggregate check PASSES that edit and cannot do otherwise:
`aggregate` takes roadmap.yaml as its own base, so an entry written
straight into the base is a fixed point. It answers "is the aggregate
consistent?", never "did this change come through the fragment path?".

check_roadmap_fragment_required.sh asks the second question, over the
base..head diff:

  1. every top-level entry whose BYTES change in roadmap.yaml must have
     docs/roadmaps/entries/<ID>.yaml changing in the same diff;
  2. whenever either side changes, head's roadmap.yaml must equal
     aggregate(head's entries/) -- IMPORTED from roadmap_fragments.py,
     never restated, so guard and generator cannot drift.

A preamble-only change and a diff touching neither side still pass; an id
that cannot be a filename has no fragment path at all and is refused with
that stated (RMFR-OB-005).

Retro-verdicts over the last 8 first-parent commits of origin/main: the
one PR that used the fragment path (#3297) PASSES; the two fragment-less
roadmap edits (#3348, 3346466) are refused.

`pmat work add` writes the monolith, so the gate would block every future
ticket. The remedy is real and is NAMED IN THE FAILURE MESSAGE:
`roadmap_fragments.py adopt <ID>` moves the entry into entries/<ID>.yaml
(proved to parse alone and to carry the same mapping) and regenerates the
aggregate, which also moves it to its sorted slot. This commit's own
PMAT-3294 entry took that path: check_roadmap_sorted.sh PASS,
check_roadmap_diff_additive.sh added=1 reserialised=0.

Case table: 12 rows, hermetic fixture repos. Mutations: neutering rule 2
turns row 3 (drift) RED; comparing id SETS only turns rows 5, 6, 8 and 11
RED. bashrs lint --no-ignore --level error: 0 errors.

Pmat-Ticket: PMAT-3296
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A differential guard run bare on main is handed HEAD^1 as its base, because
resolve_base's push-shape arm exists to avoid judging a commit against
itself. For THIS guard that base is wrong: HEAD^1..HEAD is the PREVIOUS
merge's diff, not this change. Measured before the fix: 2 of the last 8
first-parent commits of origin/main (#3348, 3346466) are fragment-less,
so a bare run on main refused changes that landed before the guard existed
and would have red-lined guard_tree from the moment it merged. That is
re-litigating history, not measuring this change.

In push shape the guard now reports a named SKIP and exits 0, and says what
it did NOT check: the shape detected, the base it would have used, and the
roadmap.yaml <-> entries/ pairing it therefore left ungraded -- which the
change's own pull_request / merge_group run grades against a real
merge-base. PR shape is untouched.

The decision is read from BASE_HOW, the one place resolve_base makes it, so
this cannot drift from it.

Two case-table rows, over the real DISPATCH rather than judge() -- the
fixture carries its own copy of the guard and the libraries it sources, so
$REPO_ROOT is the fixture and origin/main is whatever the row points at.
Same commit, same fragment-less content, two verdicts:

  row 13  origin/main IS this commit   -> SKIP, exit 0   (the row that
                                          would have red-lined main)
  row 14  origin/main is its parent    -> REFUSE, exit 1

14/14 rows, 0 failed. bashrs lint --no-ignore --level error: 0 errors.

RETRO over the last 8 first-parent commits, after the fix: 8/8 exit 0 in
push shape (none refused), while PR shape still refuses the same 2. Note
7 of those 8 exit 0 via base==head rather than via SKIP: resolve_base's
"behind the tip" arm tests `git rev-list --first-parent | grep -qx`, and
grep -q's early exit SIGPIPEs rev-list (rc 141 under the `set -o pipefail`
every caller sets), so only the tip itself -- which short-circuits on
string equality -- reaches the push-shape arm. Both routes exit 0, so the
§8 requirement holds either way; the SIGPIPE is a pre-existing defect in
scripts/lib/resolve_base.sh (shared with check_roadmap_diff_additive.sh),
OUT OF SCOPE here and reported rather than fixed.

Pmat-Ticket: PMAT-3296
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@noahgift noahgift added this to the 0.68.0 milestone Sep 16, 2026
@noahgift
noahgift enabled auto-merge September 16, 2026 09:15
check_no_pipe_into_grep_q refuses it, and correctly: under pipefail the producer's SIGPIPE is what the pipeline reports, not grep's verdict — so a case-table row could pass on a death rather than on a match. The three sites read a here-string now. No row's meaning changes.

Pmat-Ticket: PMAT-3296
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

§13.11 rung 1 — quorum shadow verdict

S13-SHADOW pr=3352 head=1d132820f30c051b4634ea61ddde382020a40835 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
noahgift added this pull request to the merge queue Sep 16, 2026
Merged via the queue into main with commit 4b00761 Sep 16, 2026
18 of 19 checks passed
@noahgift
noahgift deleted the PMAT-3296-fragment-required branch September 16, 2026 11:38
noahgift added a commit that referenced this pull request Sep 16, 2026
…g today

main 4b00761 (#3352) landed check_roadmap_fragment_required.sh AFTER this branch's
base: roadmap.yaml is now a GENERATED aggregate of docs/roadmaps/entries/, and an
entry edited in the monolith without its fragment is refused. This branch edits
PMAT-3233's entry, so guard-tree went red on a rule that did not exist when the edit
was made.

Adopted per the guard's own remedy (roadmap_fragments.py adopt, then
make roadmap-aggregate), with both sides staged. The fragment SUPERSEDES the base
entry, so it carries the corrected title -- which names census.json, `make contracts`,
the README count and R-10 provenance linting -- and spec: paiml-ontology.md, the
document whose ONT-1 row actually defines this work.

Pmat-Ticket: PMAT-3233
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
noahgift added a commit that referenced this pull request Sep 16, 2026
… landed on main after this branch was cut

check_roadmap_fragment_required.sh (main since 11:38Z) rejects a roadmap.yaml entry added with no docs/roadmaps/entries/<ID>.yaml; this PR sat at merge-queue position 1 and would have ejected every entry batched behind it. Adopted via scripts/lib/roadmap_fragments.py adopt PMAT-3231; roadmap.yaml regenerated, not hand-edited.

Pmat-Ticket: PMAT-3231

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
noahgift added a commit that referenced this pull request Sep 16, 2026
… — NEW docs/roadmaps/entries/PMAT-3233.yaml; EDITS docs/roadmaps/roadmap.yaml (regenerated from 4 fragments); no code change

The branch's first commit carried `Pmat-Ticket: PMAT-3224` but never committed
that entry; #3224 is an unrelated merged ONT-2a issue. The row's ticket id is
derived from its GitHub issue (#3233, half 1 of 2), through
`pmat work add --github-issue 3233` and `roadmap_fragments.py adopt`, so the
entry arrives as a fragment and check_roadmap_fragment_required.sh (#3352) can
see it. The quorum-review brief reads this ticket's title.

Pmat-Ticket: PMAT-3233
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@noahgift noahgift mentioned this pull request Sep 16, 2026
noahgift added a commit that referenced this pull request Sep 18, 2026
…nt — NEW docs/roadmaps/entries/PMAT-3273.yaml; EDITS docs/roadmaps/roadmap.yaml (regenerated from fragments); no code change

The branch never committed a roadmap entry, and the quorum-review brief reads
the ticket's title through `pmat work status`. No GitHub issue exists for
EV-2a, so the id is allocated from the PR's own number (#3273), which GitHub
hands out from the same central sequence as issues and therefore cannot
collide (`pmat work add --id`). The entry arrives as a fragment so
check_roadmap_fragment_required.sh (#3352) can see it.

Pmat-Ticket: PMAT-3273
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
guyernest pushed a commit to guyernest/aprender that referenced this pull request Sep 29, 2026
…he wrong instrument (paiml#3278)

* triage(branches): 35 branches that never had a PR — commit count is the wrong instrument, path residue is the right one

Standing operator rule: tickets, PRs and branches that are not triaged are P0.
`git ls-remote --heads origin` = 112; branches with no PR in any state = 35, and
none had ever been classified.

The obvious measure is wrong by construction. `rev-list --count main..<b>` says 30
of the 35 carry unique commits — but this repo squash-merges from a merge queue, so
a branch whose content LANDED still shows every original commit as unique. What
decides is path residue: of the paths a branch changes against its merge-base, how
many do not exist on main at all.

  4 + 1 + 11 + 6 + 2 + 11 = 35
  DELETE  4  merge-queue artifacts
  DELETE  1  zero commits ahead
  DELETE 11  zero path residue
  DELETE  6  residue is a fixture RENAME only
  DELETE  2  superseded by OPEN paiml#3021
  REVIEW 11  genuinely absent from main

The prrev stack (12 branches, 2026-08-30) looked like it had dropped a discriminating
test row: `row-07-honest-docs-only-all-not-triggered` is absent from main. It had not.
Main carries `row-07-honest-docs-only-pmat-consulted`, plus row-16/17 renumbered to
row-23/24 — every residue in that stack is a rename. Counted by ROWS rather than by
directory name, main has 113 fixture rows against the stack's 24. The subsystem
shipped by another route with 4.7x the coverage.

That correction is the point of the method note: residue BOUNDS the question, it does
not answer it. PMAT-1094 is the other worked example — 8 absent paths, 7 of them
throwaway `fix_derives*.py` / `fix_dry*.py` scratch scripts.

Two REVIEW rows matter to this spec: `agent/R-5` holds
`.github/workflows/release-assets.yml` and `contracts/apr-publish-cascade-v1.yaml`,
neither of which exists on main, and both are T-3/T-4 surface.

No branch is deleted here. kind:triage is classify and link only: this diff touches
docs/audits/** and docs/roadmaps/roadmap.yaml and nothing else.

Pmat-Ticket: PMAT-3231
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(roadmap): my union inserted a stray blank line — roadmap_trim.py collapses it back to base bytes

check_roadmap_diff_additive.sh: VIOLATION reserialised: id=PMAT-3229 (bytes differ,
no field actually changed). The union resolver I hand-rolled rejoined entry blocks
with a newline and left an extra blank line after PMAT-3229's `notes: null`, so the
entry ABOVE my insertion re-serialised without any field changing.

PMAT-980 (#2874) is exactly this rule, and the guard names its own remedy:
`scripts/roadmap_trim.py` collapses a re-serialisation back to base bytes. Ran it.

The entry is byte-identical to origin/main again (438 = 438 bytes).

Pmat-Ticket: PMAT-3231

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* triage(branches): the universe excluded 37 branches by construction — a CLOSED PR still leaves unlanded work

The first pass asked "which branches never had a PR?". A branch whose PR was
opened and then closed unmerged is excluded by that question — it HAS had a PR —
and is exactly a branch carrying unlanded work with no path forward.

117 remote = 41 open-PR + 38 never-PR'd + 37 closed-PR + main. Classified with
the same path-residue instrument: 31 DELETE (0 residue or 0 ahead), 6 REVIEW.

Zero branches are merged-with-branch-left-behind, so deletion-on-merge works;
all 37 are abandoned PRs — consistent with 29 of 146 PRs over 10 days (20% of
production) closed unmerged. Five-whys on that rate: paiml#3294.

Batched into this PR rather than opened as a 43rd: paiml#3294 Why 5.

Pmat-Ticket: PMAT-3231

* roadmap(PMAT-3231): adopt the entry into its fragment — the guard paiml#3352 landed on main after this branch was cut

check_roadmap_fragment_required.sh (main since 11:38Z) rejects a roadmap.yaml entry added with no docs/roadmaps/entries/<ID>.yaml; this PR sat at merge-queue position 1 and would have ejected every entry batched behind it. Adopted via scripts/lib/roadmap_fragments.py adopt PMAT-3231; roadmap.yaml regenerated, not hand-edited.

Pmat-Ticket: PMAT-3231

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
guyernest pushed a commit to guyernest/aprender that referenced this pull request Sep 29, 2026
…ring decoration (paiml#3281)

* feat(ont): ONT-1 — `pv census`, the consumer whose absence made anchoring decoration

PMAT-3232 landed §11.2's ratchet and PROVED, rather than assumed, that anchoring the
corpus today would be theater: `pv validate` accepts `entity:` by ignoring it, no
`pv census` existed, and nothing in either contracts crate read the key. That ratchet
refuses a rise in `contracts_anchored` while `consumer_present` is false.

This is that consumer. ONT-001 v4.3: "Every figure in §1 is [C]-from-snapshot or [U]
and is quote-frozen until ONT-1 emits one census."

    $ pv census contracts
    contracts: 1837
    by_anchoring
      unanchored   1837   (no entity: — a law, pattern or policy; optional by R-5)
      class           0   (entity: {type})
      instance        0   (entity: {type, ref})

    $ pv census contracts --json
    {"total":1837,"by_anchoring":{"unanchored":1837,"class":0,"instance":0},"by_entity_type":{}}

WHY RAW YAML AND NOT `Contract`. `Contract` is not `deny_unknown_fields`, so serde
silently DROPS an `entity:` block it does not model — which is exactly why
`pv validate` calls an anchored contract valid. A census built on the typed struct
would report `unanchored` for every contract FOREVER, including after the corpus was
fully anchored, and the ratchet reading it would record 0 while the work was done.

THE GATE OPENS BY ITSELF, verified end to end:

    ratchet probe, installed pv 0.65.2   consumer_present: false
    ratchet probe, this pv on PATH       consumer_present: true

No edit to the guard: the probe is derived from `pv --help`, so ONT-1 landing is what
flips it. Anchoring still does not rise here, and that is correct — the probe measures
the pv that is AVAILABLE, so on CI it stays false until the fleet carries this build.
converge -> prove -> assert, enforced by the ratchet rather than by discipline.

A STALE TREE CAUGHT THE CENSUS OUT FIRST, and the tool was right:

    pv census from ~/src/aprender    reported 1818
    that checkout is on branch       PMAT-272-yoga-nightly-implementation
    origin/main contracts/*.yaml     1837

Fourth time this lineage that the main checkout's stale branch produced a wrong
number. Work moved to a clean worktree off origin/main; the checkout was restored.

Case table, 10 rows, pure over raw documents: absent -> unanchored · inline {type} ->
class · inline {type, ref} -> instance · nested block -> instance · nested type-only
-> class · `entity:` with NO type -> NOT an anchor (a malformed block must not inflate
the ratchet) · INDENTED `entity:` is not the top-level block · quoted values unquoted ·
trailing comment not part of the type · missing directory is an ERROR, not a census of
zero (R-2 on the instrument).

The first draft was REFUSED by the pre-commit complexity gate — `entity_block` measured
cognitive 26 against a ceiling of 25. Split into `entity_block` + `indented_block`
(3/10 and 4/11). The gate did its job on a first draft, and the sha check caught the
consequence: the rejected commit left the branch at main's sha, so the push that
followed carried no content at all.

cargo test -p aprender-contracts-cli: 193 passed, 0 failed. clippy -D warnings: 0.
fmt clean. pv_surface_gate 8/8, including every_advertised_subcommand_is_reachable.

ont-delta: type census — `by_entity_type` and `by_anchoring` over ONT-001 §0.0's open
type registry become computable for the first time (form 1).

Pmat-Ticket: PMAT-3233
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(registry): declare `pv census` — FALSIFY-CLI-006 caught a surface the contract did not know about

`workspace-test` went red on this branch while 193 local tests passed, because the
quick tier selects by DIFF and reached a target my crate-scoped run never did:

    crates/apr-cli/tests/cli_commands.rs:747
    every_subcommand_in_the_binary_is_declared ... FAILED
    FALSIFY-CLI-006: the binary offers depth-2 commands the contract does not
    declare: ["pv census"]

`pv` ships BOTH as the standalone binary and as `apr pv`, so a new `pv` subcommand
widens `apr`'s depth-2 surface, and `contracts/apr-cli-commands-v1.yaml` asserts that
surface exactly. Adding a subcommand to one binary is a change to the other's
contract; the falsifier is what makes that non-obvious fact non-silent.

Declared at its sorted slot: `[audit, book, census, certify, ...]`.

RED-turning mutation, because "the test passes now" is not the same claim as "this
line is what makes it pass":

    census removed from the registry  -> FAILED, names ["pv census"]
    census present                    -> ok

cargo test -p apr-cli --test cli_commands: 15 passed, 0 failed.

Worth recording for the next feature: `cargo test -p <crate>` is NOT the CI selection.
BSE-17's quick tier adds the touched crates' direct reverse dependents plus every test
target that reads the tree, so a crate-scoped green says nothing about the targets a
sibling crate owns over your change.

ont-delta: none — a registry declaration for the subcommand this PR adds; the delta is
already recorded against `pv census` itself.

Pmat-Ticket: PMAT-3233
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(roadmap): collapse the union's stray blank line back to base bytes (roadmap_trim.py)

Same defect as PMAT-3231: my hand-rolled union rejoined entry blocks and left an
extra blank line, re-serialising the entry above the insertion. PMAT-980 (#2874).

Pmat-Ticket: PMAT-3233

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(audit): 27 surface_audit rows cited a cli.rs line that my own insertion moved

    G2.1 freshness  FAIL  this branch changes 1 cited evidence file(s) and does
                          NOT touch docs/audits/surface_audit.csv:
          - crates/aprender-contracts-cli/src/cli.rs
                          Audited code moved; re-run the audit for those rows.

Correct, and mine. Adding the `Census` variant to the command enum shifted every
variant declared after it, and 76 audit rows cite that file BY LINE. 27 of them
pointed at the wrong declaration.

Line numbers are DERIVED from the current file, not adjusted by an assumed offset:
the variant name is read out of cli.rs and kebab-cased back to the subcommand, so
the mapping is recomputed rather than patched. Spot-checked in both directions —

    apr pv audit     -> cli.rs:59   `Audit {`
    apr pv census    -> cli.rs:80   `Census {`
    apr pv coverage  -> cli.rs:89   `Coverage {`

WHAT THIS DOES NOT DO, and why that is the ratchet talking rather than a shortcut.

`apr pv census` gets no row here. Adding one modelled on its siblings takes
`low-confidence AND ungated` from 204 to 205, and G2.3 refuses that:

    G2.3 floors FAIL: low-confidence AND ungated is 205, must be <= 204
                      (floor derived from the comparand, not from this tree)

All 39 `apr pv` rows are `in_dogfood_skill=no`, so census would not be an outlier —
it would be one more unit of exactly the debt that floor exists to stop growing. The
guard does not require a row per command (G2.2 asks only that no row is LOST), so
the drift fix stands alone and all five gates pass.

The honest reading is that the ratchet is right: a new command should earn its audit
row by being dogfooded, not by being declared low-confidence and ungated on arrival.
`apr pv census` should get its row together with dogfood coverage for it, and that is
a larger change than this PR.

    G2.1 freshness       PASS  78 cited evidence files, 1 changed by this branch
    G2.2 reconciliation  PASS  no row lost (837 comparand rows all present)
    G2.3 floors          PASS  144/837 covered (17.2%), 28 per-binary floors held
    G2.4 waivers         PASS  44 broken-and-ungated, 44 triaged, 0 new
    G2.5 membership      PASS  0 feature(s) changed cluster

Pmat-Ticket: PMAT-3233
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(PMAT-3233): RED — ONT-1 says an empty corpus declines at 2 and a parse error rejects at 1; this census does neither

cargo test -p aprender-contracts-cli --lib census:

test commands::census::tests::an_empty_corpus_declines_at_exit_2 ... FAILED
test commands::census::tests::a_parse_error_rejects_at_exit_1_and_is_never_counted ... FAILED

failures:

---- commands::census::tests::an_empty_corpus_declines_at_exit_2 stdout ----

thread 'commands::census::tests::an_empty_corpus_declines_at_exit_2' (2397058) panicked at crates/aprender-contracts-cli/src/commands/census.rs:318:9:
assertion `left == right` failed: an empty corpus must decline (exit 2), not fail: 0 contracts under /tmp/.tmp8ZGk9m — refusing to report a census nothing was read for
  left: 1
 right: 2
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

---- commands::census::tests::a_parse_error_rejects_at_exit_1_and_is_never_counted stdout ----

thread 'commands::census::tests::a_parse_error_rejects_at_exit_1_and_is_never_counted' (2397055) panicked at crates/aprender-contracts-cli/src/commands/census.rs:340:14:
a corpus with an unparseable file is rejected, never censused: Census { total: 4, unanchored: 4, class: 0, instance: 0, by_entity_type: {} }

ONT-001 §5 ONT-1 RED text: "fixture 3 valid + 1 malformed → exit 1 `reject: 1 parse error under <path>`; empty dir → exit 2 `decline: 0 contracts under <path>`".

Pmat-Ticket: PMAT-3233
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(PMAT-3233): ONT-1 — the census walks with pv lint, declines at 2, rejects a parse error at 1, and is the ONE number the README prints

ONT-001 §5 ONT-1. The census now collects with provable_contracts::lint's walker —
the one `pv lint` and `pv validate` already share — so the corpus has ONE
cardinality instead of the three §1 quote-freezes (1818 · 1460 · 1331):

  pv census contracts --format json   n_files=1790 = n_parsed, 0 parse errors
  pv census <empty dir>               rc 2  decline: 0 contracts under <path>
  pv census <3 valid + 1 malformed>   rc 1  reject: 1 parse error under <path>

- contract_walk::verdict_for is the one definition of decline/reject/error, so
  the word pv prints and the exit code cannot drift apart; ParseErrors carries the
  row's own wording.
- census.json: schema, git_sha (null — a commit sha is unknowable for the commit
  that contains it; operator ruling 2026-09-16, id_set_sha256 is the content
  address), n_files/n_parsed/n_parse_errors, parse_errors[], quarantined_n,
  by_kind, by_entity_type, by_anchoring, id_set_sha256, declared_external[],
  timing{n_runs: 5, values null until PVL EV-9 measures them on the CI host class}.
- external-corpora.yaml declares the archived corpus with the command that counted
  it: 397 contracts/*.yaml, 82 .lean at 626868c240 [V 2026-09-16]. ONT-001 §1's
  "313" is PVL EV-13c's ORPHAN subset, not the corpus size.
- ONE definition of "a contract file" everywhere: external-corpora.yaml and
  quarantine/ join binding.yaml in the shared rule, and check_readme_claims.sh's
  object-store instrument applies it too. Otherwise the README states 1790 while
  the ratchet compares against 1841 for ever — a lag the guard tolerates, which is
  how a guard stops discriminating (its row 6 went green under exactly that).
- README: 1841 -> 1790. The number published is now the number the gate validates.
- make contracts asserts census.json is tracked, regenerates it, diffs it, checks
  the README and runs lint-provenance.sh (R-10, interim, with a self-test).

Verified: cli --lib 95 passed / 0 failed; contracts --lib 1505 passed; clippy
--all-targets -D warnings clean; readme ratchet suite 12 checks 0 failed;
check_readme_claims --self-test 7/7 and live PASS (base=1790 merge=1790 delta=+0);
make contracts rc 0; bashrs lint --level error clean on every script touched.
ONT-1's probe conjuncts all pass on this tree; only `merged ONT-1` is false, as it
must be while the row's PR is open.

Pmat-Ticket: PMAT-3233
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(PMAT-3233): the README names the instrument it quotes — census.json .n_files, not find

The count moved to the census in the previous commit; this line still credited `find contracts/ -name '*.yaml'`, which measures 1841. A claim that names the wrong instrument is the drift these guards exist to catch, and it is not generated text — only the digits between the markers are.

Pmat-Ticket: PMAT-3233
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(PMAT-3233): the reject line carries BOTH the row's wording and the measured file count

ONT-001 §5 ONT-1 pins the first line (`reject: 1 parse error under <path>`); PVL-1 (PMAT-1099) pins that a measured failure reports how many files were measured, asserted by tests/pvl_zero_contracts.rs:483 as the substring `1 of 2 contract files`. Dropping the count satisfied one spec and broke the other — caught by the CLI crate's integration targets, which `--lib` alone does not run.

  reject: 1 parse error under /tmp/x
    1 of 4 contract files measured did not parse
    /tmp/x/garbage.yaml: Failed to parse YAML: ...

cargo test -p aprender-contracts-cli: every target passes (lib 95, pvl_zero_contracts 22).

Pmat-Ticket: PMAT-3233
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* census: name every file in this PR by the ONT-1 clause that requires it

Quorum round 2 lane 1 reported scripts/lint-provenance.sh "out of scope". The lane
receives Title/Status/Priority and nothing else, and this receipt did not exist for
it to read — so the finding is a briefing gap, and the file it asked for is the fix.
The scope table names each of the 20 changed files against the clause of ONT-001 v4.3
row ONT-1 that requires it.

Also records, rather than silently dropping, the lane's second finding: adding a
census row to surface_audit.csv was MEASURED to break the dogfood gate
(G2.3 low-confidence AND ungated 206 > 204), because a new surface row is born
low-confidence and ungated. Gating it is ONT-6's subject, not ONT-1's.

And attributes the two local guard_tree failures away from this branch: both are
pmat 3.40.1-vs-3.40.2 tool_version mismatches that fail identically at the
merge-base with none of this branch's code present.

Pmat-Ticket: PMAT-3233
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* census: round-3 quorum refuted two claims in this receipt, both mine

Lane 1 (gemini-3.1-pro-high, 119k in / 17k thinking) read the receipt this PR added
and found two defects in it. Both are real and both are corrected here:

  1841 -> 1842. The README count this PR rewrites is 1842, not 1841; 1841 was carried
  over from the 2026-09-15 escalate receipt instead of being read from the diff. main
  carries 1842 in both CONTRACT_COUNT markers. Measured, not conceded.

  15 of 21 -> 21 of 21. The table's own sentence claimed every file was named by a
  clause, while omitting six: apr-cli-commands-v1.yaml (which DECLARES the census
  subcommand, so the probe's surface is legitimate), Cargo.toml/Cargo.lock (serde for
  the JSON, sha2 for id_set_sha256), commands/mod.rs, roadmap.yaml, and the receipt.
  A completeness claim wider than the table under it is the defect this repo names.

Lanes 2 and 3 returned PASS with zero findings on 843k and 147k input tokens.

Pmat-Ticket: PMAT-3233
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* census: the ticket, not the diff, was out of scope

Quorum rounds 2 and 4, lane 1: scripts/lint-provenance.sh and its Makefile hook are
"out of scope" because "Ticket PMAT-3233 strictly focuses on ONT-1: pv census".

The lane's premise was TRUE and its conclusion wrong, and the fault was the ticket's.
MEASURED: `pmat work status` prints Title/Status/Priority/Progress and nothing else,
no ticket in this roadmap carries a description: field at all, and PMAT-3233's spec:
pointed at APR-RELEASE-001-train-and-build-kaizen.md -- a spec that does not contain
this row. So the title was the ONLY channel that could tell a reviewer what ONT-001
v4.3's ONT-1 row actually requires, and it named one of the row's clauses.

Both are corrected here: the title now names census.json + `make contracts`, the
README count, and R-10 provenance linting; spec: points at paiml-ontology.md, the
document whose ONT-1 row defines the work. No code changes.

Pmat-Ticket: PMAT-3233
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* census: restore the executable bit on three scripts my own edits dropped

Quorum round 5, lane 1: scripts/check_readme_claims.sh, scripts/readme_sync.sh and
scripts/tests/ratchet_semantics_test.sh went 100755 -> 100644 in this diff, "doing
something the ticket does not ask for". Correct, and self-inflicted: an
`awk ... > file.new && mv file.new file` edit creates the replacement with default
permissions, so the mode was lost silently while every caller that says
`bash scripts/...` kept working and hid it.

That is the same shape as the sed corruption repaired earlier on this branch: a
scripted edit whose collateral effect nobody measured. Restored with chmod 755; the
diff now carries no mode change at all.

Round 5 also RETIRED lane 1's scope finding from rounds 2 and 4 -- with the ticket
corrected, the lane no longer reads lint-provenance.sh as out of scope. Lanes 2 and 3:
PASS, zero findings.

Pmat-Ticket: PMAT-3233
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* census: the round-6 quorum artifact — 3/3 PASS, zero findings

Rounds 2-5 were NOT AGREED, and every one of lane 1's dissents was a real defect of
mine, not a false positive:

  round 2/4  scripts/lint-provenance.sh "out of scope" -- TRUE of the TICKET, whose
             title named one clause of the ONT-001 row and whose spec: pointed at a
             document that does not contain this row. Fixed in the ticket.
  round 3    the receipt claimed README 1841 (main carries 1842) and a completeness
             the table did not deliver (15 of 21 files). Both corrected.
  round 5    three scripts silently went 100755 -> 100644 under my own
             `awk > file.new && mv` edits. Restored.

Round 6: lane 1 gemini-3.1-pro-high PASS, lane 2 gemini-3.8-flash-high PASS,
lane 3 gemini-3.7-flash-high PASS, zero findings, judged diff db5e395233.

This commit also carries a merge of origin/main d83592a. guard-tree failed on
eba0f63 for a reason that was NOT this diff: CI's shallow checkout has no
merge-base, so it compares against main's TIP, and paths that d83592a REMOVED from
tests_encode_decode.rs and gguf_real.rs read as paths this branch ADDS. Merging main
makes both sides agree. The judged diff is byte-identical across the merge
(db5e39523393 before and after), which is the master-merge cascade pmat-merge accepts.

Pmat-Ticket: PMAT-3233
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* census: adopt PMAT-3233's roadmap fragment, which main began requiring today

main 4b00761 (paiml#3352) landed check_roadmap_fragment_required.sh AFTER this branch's
base: roadmap.yaml is now a GENERATED aggregate of docs/roadmaps/entries/, and an
entry edited in the monolith without its fragment is refused. This branch edits
PMAT-3233's entry, so guard-tree went red on a rule that did not exist when the edit
was made.

Adopted per the guard's own remedy (roadmap_fragments.py adopt, then
make roadmap-aggregate), with both sides staged. The fragment SUPERSEDES the base
entry, so it carries the corrected title -- which names census.json, `make contracts`,
the README count and R-10 provenance linting -- and spec: paiml-ontology.md, the
document whose ONT-1 row actually defines this work.

Pmat-Ticket: PMAT-3233
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* census: the R-10 linter examined ZERO claims on the only file it lints

Quorum round 7, two independent lanes, re-measured here before accepting:
lint-provenance.sh filtered lines with `rg -N '^\s*(-|\|)\s*\S'` -- Markdown list and
table rows -- while its one production target, contracts/external-corpora.yaml, states
its claims as YAML key-values. It examined ONE line, that line had no digit, and it
exited 0. Appending `unmarked_total: 4242` also exited 0.

"0 violations over 0 files" is this fleet's signature defect, and here it was aimed at
the guard written to prevent it. The self-test missed it because both fixtures were .md
-- a fixture per FORM, where the rule is a fixture per FORM VARIANT.

Fixed: Markdown rows AND YAML key-values are scanned; schema/ref/repo/name/mark/
counted_by/item_type/id are exempt, so a schema URI is not read as a measurement; the
NUMBER OF CLAIMS EXAMINED IS PRINTED, so a silent zero cannot recur; the self-test gains
a yaml red/green pair, an exempt-key case, and an assertion that the count is non-zero.

PROVEN to discriminate: restoring the markdown-only filter turns the self-test RED on
three assertions, one of them `the YAML fixture examined 0 claims`.

external-corpora.yaml's two real claims (head:, n_files:) now carry inline marks. pv
census re-derived census.json BYTE-IDENTICALLY, so no published figure moved.

Also: the receipt's file table, which round 7 measured as claiming 21 against a diff of
23, now names every file.

Pmat-Ticket: PMAT-3233
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* census: round 8 quorum artifact — 3/3 PASS on 161039d, diff 544da6b623d3

Pmat-Ticket: PMAT-3233
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
guyernest pushed a commit to guyernest/aprender that referenced this pull request Sep 29, 2026
…10) with a CRAN-golden set that catches the paper's own erratum — NEW crates/aprender-viz/src/breaks.rs (pub fn extended, extended_loose; Q_DEFAULT, W_DEFAULT=[0.25,0.2,0.5,0.05] per the reference code, not the prose), tests/breaks_golden.rs (44 tests: 36 goldens, 6 properties, anti-vacuity floor, W_DEFAULT-swap mutation), fixtures/breaks/{manifest.json, README.md (R transcription, verbatim), generate.py}; EDITS Cargo.toml (+libm), src/lib.rs (+pub mod breaks), Cargo.lock; no renderer change, no new rendering dep (paiml#3259)

* feat(viz): extended-Wilkinson tick placement (Talbot-Lin-Hanrahan 2010) with a golden set that catches the paper's own erratum

Closes paiml#3233. Unblocks apex APEX-001 EV-14, which lowers its Plot IR onto this
surface and has been STOP(blocked-upstream) on it.

`breaks::extended(dmin, dmax, m, q, w) -> Vec<f64>`, plus `extended_loose` for
the case where the labeling must contain the data. Transcendentals go through
the pure-Rust `libm` so log10/pow do not depend on the platform libm.

THE FINDING THIS ROW TURNS ON. The paper's prose (section 3.2) writes the
objective as 0.2*simplicity + 0.25*coverage + 0.5*density + 0.05*legibility.
BOTH of the first author's own implementations disagree with that sentence and
agree with each other:

  CRAN labeling::extended   w[1]*s + w[2]*c, w = c(0.25, 0.2, 0.5, 0.05)
  jtalbot/Labeling C#       w[0]*s + w[1]*c, w = {0.25, 0.2, 0.5, 0.05}
  the paper's prose         0.2*simplicity + 0.25*coverage

So the first two weights are swapped between the paper and the reference code.
An implementer following the paper lands on the wrong tie-break, and every
property still holds: in range, strictly increasing, uniform step, mantissa in
Q, count near m. That is precisely why this needs golden fixtures and not only a
property suite.

The goldens were produced by running a literal transcription of the CRAN R
reference over 36 domains. That transcription is committed verbatim in
fixtures/breaks/README.md and generate.py extracts and executes it, so the set is
reproducible rather than asserted. It is kept in Markdown, unrefactored, on
purpose: it is an external artefact reproduced for provenance, not this project's
source, and an oracle refactored to share the implementation's structure can
share its mistakes. Executing it from the README is what stops it rotting --
regeneration fails if the block stops parsing. Regeneration is idempotent and the
relocation left all 36 domains byte-identical. Domains were chosen
where the algorithm's decisions turn — round and unround bounds, data that does
and does not include zero, negatives, offsets forcing labels inside the data
range, six decades of magnitude, and sweeps of m and of offset.

Proved to have teeth rather than claimed: swapping W_DEFAULT to the paper's
order turns four tests red (golden_10, golden_12, golden_25 and the mutation
test); restored, 44/44 pass. Worth noting the margin is thin by nature — only 3
of 36 domains move under the swap, because the swap only changes which candidate
wins a tie. Widening the set structurally took detection from 2/24 to 3/36 and
added golden_25 as a detector. A handful of examples would very likely have
missed it entirely.

44 tests: 36 golden (one per domain, so a failure names the domain that moved),
6 properties, an anti-vacuity floor on the golden set itself, and the mutation.
clippy -D warnings clean, fmt clean, doc-test passes.

Two notes on what the pre-commit complexity gate shaped here. The search is a
four-deep nested loop and measured Cognitive well over the gate's 25, so it is
decomposed by loop level into scan_counts/scan_powers/scan_starts over a Ctx and
a Best, which reads better than the transcription did anyway; all 44 tests are
unchanged across that split. The Python reference measured Cognitive 120 and
cannot be decomposed without destroying the line-for-line diffability against the
R source that is its whole reason to exist, which is why it moved into the README
rather than being restructured. That fork went to a 3-lane quorum, which split
2-1 for relocating it; the dissent's objection was that a code block in Markdown
rots silently, and having generate.py execute the block is the answer to it.

Pmat-Ticket: PMAT-3224
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* APEX-2b: roadmap ticket PMAT-3233 (id from issue paiml#3233) as a fragment — NEW docs/roadmaps/entries/PMAT-3233.yaml; EDITS docs/roadmaps/roadmap.yaml (regenerated from 4 fragments); no code change

The branch's first commit carried `Pmat-Ticket: PMAT-3224` but never committed
that entry; paiml#3224 is an unrelated merged ONT-2a issue. The row's ticket id is
derived from its GitHub issue (paiml#3233, half 1 of 2), through
`pmat work add --github-issue 3233` and `roadmap_fragments.py adopt`, so the
entry arrives as a fragment and check_roadmap_fragment_required.sh (paiml#3352) can
see it. The quorum-review brief reads this ticket's title.

Pmat-Ticket: PMAT-3233
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Quorum verdict for PMAT-3233 (APEX-2b): 3/3 PASS — docs/audits/quorum-PMAT-3233.json; artifact only, no other path

Lanes gemini-3.1-pro-high, gemini-3.8-flash-high, gemini-3.7-flash-high, all PASS
with no findings, agreed=true, judged head 7bf24be against origin/main.

Pmat-Ticket: PMAT-3233
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* APEX-2b: release ticket id PMAT-3233 — it collides with ONT-1's PMAT-3233 on main (paiml#3281); REMOVES docs/roadmaps/entries/PMAT-3233.yaml and docs/audits/quorum-PMAT-3233.json, RESTORES docs/roadmaps/roadmap.yaml to 7787f5e; no code path touched

Measured: git merge origin/main conflicts add/add on exactly these three files and on nothing under crates/. The id was derived from issue paiml#3233; ONT-1 took the same number with github_issue: null. The row is re-ticketed as PMAT-3259 (the PR number, the EV-2a precedent) in the next commit, after merging main. The old 3/3 verdict lapses with the diff and is re-run.

Pmat-Ticket: PMAT-3259
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* APEX-2b: roadmap ticket PMAT-3259 (id from PR paiml#3259) as a fragment — NEW docs/roadmaps/entries/PMAT-3259.yaml; EDITS docs/roadmaps/roadmap.yaml (regenerated by make roadmap-aggregate, +23 lines, one entry); no code path touched

Replaces PMAT-3233, which collided with ONT-1 on main. aggregate --check rc 0, check_roadmap_sorted PASS, check_roadmap_ids_unique PASS.

Pmat-Ticket: PMAT-3259
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Quorum verdict for PMAT-3259 (APEX-2b): 3/3 PASS — docs/audits/quorum-PMAT-3259.json; artifact only, no other path

Judged head 11060be. Lanes gemini-3.1-pro-high, gemini-3.8-flash-high, gemini-3.7-flash-high, each model measured; author Fable 5.1; no findings.

Pmat-Ticket: PMAT-3259
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
guyernest pushed a commit to guyernest/aprender that referenced this pull request Sep 29, 2026
…RM64; NEW viz manifest.rs/text.rs/render_determinism.rs (EV-2b breaks as ticks), ci determinism+compare jobs, libm-ban-live.sh; EDITS .clippy.toml (libm bans), breaks.rs powi->sq, svg/lib/scale/plots (paiml#3273)

* APEX-2a: deterministic render primitive — NEW crates/aprender-viz/src/{manifest.rs,text.rs} + tests/render_determinism.rs (7 tests) + scripts/ci/libm-ban-live.sh; EDITS .github/workflows/ci.yml (NEW determinism matrix X64+ARM64 and determinism-compare jobs, wired into gate.needs), crates/aprender-viz/{Cargo.toml (features text-path/raster + libm/sha2), .clippy.toml (12 libm bans), src/lib.rs (deny + quantise + COORD_GRID), src/error.rs, src/output/{png_encoder.rs,svg.rs}, src/scale.rs, src/plots/{histogram,boxplot,force_graph}.rs, examples/{roc_pr_curves,loss_training}.rs}; NO new crate; default-features tree gains libm+sha2 ONLY, zero rendering deps

Part of paiml#3233. APEX-001 EV-2a. Ends at STOP(PUBLISH: aprender-viz <version>) — publishing is
operator-only and is NOT done here.

WHAT IS HERE, BY THE ROW'S FIVE RULES

  (1) text::to_path(text, font_bytes, size_px) -> kurbo::BezPath, via skrifa, behind `text-path`.
      The font is BYTES THE CALLER PINS. There is deliberately no lookup, no fallback, and no
      fontdb::load_system_fonts: a machine's installed font set is an undeclared input, and the
      same source then renders differently on two machines with nothing in the output saying why.
  (2) SvgEncoder::text_as_path emits a <path>, so a figure carries shapes rather than a request
      that the viewer go and find a font. The existing `text()` is untouched.
  (3) PNG: compression, filter and adaptive-filter all pinned explicitly, in ONE `pin()` helper
      both entry points call so `write_to_file` and `to_bytes` cannot drift apart. No tIME, no
      tEXt/iTXt/zTXt — a wall clock and an encoder name are not properties of an image.
  (4) manifest::{digest_bytes, Descriptor, Manifest{insert, canonical, root, verify}} moved from
      rmedia-types with its tests, including the FALSIFY-LOCK-001 test that the digest is over
      RAW bytes and not over a canonicalisation. The contract proc-macro attributes did not come
      with it (they bind to rmedia's own YAML); `verify` returns this crate's Error.
  (5) Every transcendental on the render path goes through pure-Rust libm — 17 call sites, the
      load-bearing one being LogScale::scale, the only place a transcendental reaches a rendered
      coordinate. Emitted coordinates are quantised to COORD_GRID = 1e-3.

FOUR DEFECTS IN THE SPEC ROW, EACH MEASURED

  1. THE PROBE NAMES A FILE THAT CANNOT ENFORCE THE RULE. EV-2a's probe asserts a
     `disallowed-methods` list in the REPOSITORY ROOT `.clippy.toml`. Clippy reads exactly one
     config — the nearest — and `crates/aprender-viz/.clippy.toml` already exists. Planting a ban
     at the root produced NO lint here while firing for crates that have no config of their own.
     A root list would have satisfied the probe and enforced nothing. The list is in the crate
     config; the spec probe needs amending, and that is a separate apex change.

  2. THE SPEC'S BAN LIST CATCHES ZERO CALL SITES. The row names `f64::ln`, `f64::log10`,
     `f64::log2`, `f64::exp`, `f64::powf`, `f64::powi`. Every transcendental in this crate is
     f32. Measured: the f64-only list found 0 violations; adding the f32 twins found 13. The
     list here carries both.

  3. THREE OF FIVE PLAUSIBLE PATH SPELLINGS ARE SILENT NO-OPS. `f64::log10` bans;
     `<f64>::log10`, `std::f64::log10` and `core::f64::log10` all resolve and lint NOTHING, with
     no warning; `std::primitive::f64::log10` at least warns. So the ban cannot be verified by
     reading the config — hence scripts/ci/libm-ban-live.sh, which plants a call to every banned
     method and requires the build to fail. It also catches the other death: without
     `#![deny(clippy::disallowed_methods)]` the lints are WARNINGS and clippy exits 0.

  4. "BYTE-IDENTICAL DEPENDENCY TREE" IS NOT SATISFIABLE AS WRITTEN. The row says
     `cargo tree -p aprender-viz` with default features must be byte-identical before and after.
     Measured, it gains 10 crates: libm, sha2, and sha2's chain. Both are required by the row's
     OWN rules — rule 5 covers the default render path, and rule 4 puts the manifest in
     unconditionally. What the claim was actually about — the lane's objection that skrifa,
     kurbo and resvg would bloat every consumer — IS satisfied: measured 0 rendering
     dependencies in the default tree and 8 with the features on.

THE TWO-HOST JOB

  `determinism` is a matrix over [X64, ARM64] on clean-room runners; `determinism-compare` is a
  separate job because only something that can see BOTH receipts may set `svg_identical` —
  deciding identity from one side would be asserting the thing under test. It is wired into
  `gate.needs` and read in gate's check block, because a required check `gate` does not read is
  not a required check. PNG cross-architecture identity is RECORDED and deliberately not
  asserted: its verdict decides EV-15's shape (one manifest, or one keyed by target triple).

MUTATIONS, RUN

  wall-clock tEXt chunk in the PNG          -> 4 tests RED (incl. the chunk test by name)
  one libm::logf back to platform f32::ln   -> clippy RED, `use of a disallowed method f32::ln`
  unpin the compression level               -> SURVIVES on one host (rc 0), exactly as the row
                                               predicts — which is why it calls for a second job
  remove #![deny(...)]                      -> libm-ban-live.sh RED
  misspell one banned path                  -> libm-ban-live.sh RED
  delete one ban entry                      -> libm-ban-live.sh RED
  two hosts disagree (synthetic receipts)   -> determinism-compare RED
  one host reports                          -> determinism-compare RED (denominator check)

  An earlier attempt at the tIME mutation FAILED TO COMPILE (png 0.17 has no set_time) and I
  nearly recorded that as a proof. A mutation that does not build proves nothing; it was redone
  with add_text_chunk, which is the form this crate can actually emit.

MEASURED

  cargo test -p aprender-viz --features text-path,raster   561 lib + 7 determinism + 6 doc, 0 failed
  cargo test -p aprender-viz (default)                      558 lib + 7 + 5 doc, 0 failed
  cargo clippy --all-targets, both feature sets, -D warnings  rc 0
  cargo fmt --all -- --check                                  rc 0
  pmat analyze complexity, all 13 changed files               clean (greping Errors:, not $?)
  scripts/ci/libm-ban-live.sh                                 rc 0, 12 bans enforced
  downstream aprender-profile / -present-terminal / -train    check ok

Five design forks went to a 3-lane quorum: B/C/D/E unanimous (plant-a-violation proof; f64 public
surface narrowing to f32 at skrifa; Compression::Fast + NoFilter; drop the contract attributes).
Fork A split three ways and was settled by measurement instead: putting the bans at the root, as
two lanes proposed, breaks aprender-common (rc 101) and would reach 34 of 84 crates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* APEX-2a: svg_identical is decided from SVG bytes, not from a PNG-only manifest — NEW scripts/ci/determinism-compare.sh (compare + --self-test over 6 planted receipt pairs + a manifest_root mutant that must fail); EDITS crates/aprender-viz/tests/render_determinism.rs (SVG fixture through SvgEncoder over the same LogScale points, svg_sha256/svg_bytes in the receipt, fixture.svg in the manifest, +2 tests), src/output/svg.rs (every numeric attribute through c() = format_coord, +2 tests), src/lib.rs (NEW pub fn format_coord, +3 tests), .github/workflows/ci.yml (determinism: CARGO_TARGET_DIR under runner.temp + stale-receipt rm; determinism-compare: the script replaces the inline jq), scripts/ci/libm-ban-live.sh (3 `printf | grep -q` sites → here-strings), scripts/tree_reader_tests.txt (+render_determinism)

What was wrong (APEX-001 EV-2a, apex#71 v4.7 amendment):

- The fixture rendered only a PNG. Each host's receipt had `png_sha256` and a
  `manifest_root` over ONE entry, `fixture.png`. The compare job computed
  `svg_identical` from `manifest_root`, so the field named SVG, measured PNG,
  and compared no SVG byte at all. The row's REQUIRED assertion ("SVG cmp
  equal across X64 and ARM64") was never made; the PNG, which the row says is
  recorded and not asserted, was effectively the assertion.
- The SVG writer printed raw f32 Display for every coordinate; only text
  paths were quantised. Rule 5 says EMITTED coordinates are on COORD_GRID.
- `determinism (X64)` failed with `could not parse/generate dep info at
  …/deps/hashbrown-….d` — the aprender#2822 race from running cargo in the
  runner's persistent workspace target while other jobs use per-run dirs.
  Fixed inside the job, not by a rerun.
- scripts/tree_reader_tests.txt did not register the new integration test,
  and libm-ban-live.sh piped printf into grep -q (the ratchet is at 75).

What now decides the row:

- render_determinism.rs renders the same 200 LogScale points as SVG
  (polyline + a circle per point) and records `svg_sha256` over the bytes.
  `the_same_figure_renders_to_identical_svg_bytes_twice` and
  `every_number_in_the_svg_is_on_the_grid` (≤3 decimals, ≥800 numbers
  checked) are red when c() prints the raw float (measured: 1 of 9 red).
- svg.rs routes rect/circle/line/polyline/text/image numbers and every
  stroke-width and font-size through c() → format_coord. Two unit tests: the
  exact `name="7.123"` form for a value 0.0004567 off the grid, and two
  values one ulp apart rendering identical bytes. Both red on the mutant.
- determinism-compare.sh refuses a receipt without a 64-hex svg_sha256 or
  png_sha256, refuses one host or one architecture, sets svg_identical from
  svg_sha256 ONLY and png_identical from png_sha256 (recorded, printed,
  never asserted). `--self-test` runs identical / png-only-diff (must pass) /
  svg-only-diff (must fail) / one-host / one-arch / old-shape, then re-runs
  the table with svg_identical decided from manifest_root and requires ≥1
  case to fail (measured: 1). The compare job runs the self-test first.
- The determinism job builds in `${{ runner.temp }}/determinism-target` and
  removes `target/aprender-viz` before the test, so a receipt from an earlier
  run on the same runner cannot be uploaded as this run's.

Gates, measured locally in the worktree: cargo fmt --check clean; clippy
--all-targets -D warnings clean on default and on text-path,raster; cargo
test -p aprender-viz 563+5+9 and 566+6+9 green; check_no_pipe_into_grep_q,
check_tree_reader_tests, check_workflow_cargo_packages,
check_workflow_env_defined, check_workflow_path_filters, check_bashrs_gate,
check_guards_are_wired, check_roadmap_fragment_required,
check_ci_reusable_workflow_pinned all PASS; bashrs --level error clean on
both scripts.

Not in this commit: the EV-2b `breaks::extended` ticks in the SVG fixture,
which land after paiml#3259 merges (EV-2a depends_on EV-2b, apex#71).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* APEX-2a: roadmap ticket PMAT-3273 (id from the PR number) as a fragment — NEW docs/roadmaps/entries/PMAT-3273.yaml; EDITS docs/roadmaps/roadmap.yaml (regenerated from fragments); no code change

The branch never committed a roadmap entry, and the quorum-review brief reads
the ticket's title through `pmat work status`. No GitHub issue exists for
EV-2a, so the id is allocated from the PR's own number (paiml#3273), which GitHub
hands out from the same central sequence as issues and therefore cannot
collide (`pmat work add --id`). The entry arrives as a fragment so
check_roadmap_fragment_required.sh (paiml#3352) can see it.

Pmat-Ticket: PMAT-3273
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* APEX-2a: breaks.rs squares by multiplication — EV-2a's crate .clippy.toml bans f64::powi and merged EV-2b carried four .powi(2) sites (coverage x3, coverage_max x1); NEW fn sq(x)=x*x replaces them, bit-identical

RED on the merged tree before this commit: cargo clippy -p aprender-viz --all-targets -- -D warnings -> 4x 'use of a disallowed method f64::powi' at breaks.rs:57:32, 57:56, 57:81, 64:65. GREEN after: clippy clean on default and --all-features; breaks_golden 44 passed (goldens unchanged).

Pmat-Ticket: PMAT-3273

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* APEX-2a: the SVG determinism fixture carries EV-2b's breaks as its axis ticks (spec RED (b)) — EDITS crates/aprender-viz/tests/render_determinism.rs ONLY: decade_ticks() = breaks::extended over each axis's log10 domain, mapped back through libm::pow + LogScale and drawn as <line> ticks; fixture parametrised by domain; receipt gains ticks:{x:5,y:4}; +1 test (10 total)

Mutation, cargo test -p aprender-viz --test render_determinism, each RED 9 passed/1 failed at a DIFFERENT assertion of the_axis_ticks_are_the_ev2b_breaks_of_the_domain, GREEN 10 passed after restore: M1 decade_ticks returns a hand-written decade list -> :226 (assert_eq against extended on a domain the fixture does not use); M3 the renderer draws a hand-written x list while decade_ticks stays honest -> :232 (tick <line> absent from the SVG); M2 y ticks computed and never drawn -> :239-242. discriminates: the list [0,1,2,3,4] agrees with extended tick-for-tick on the receipt fixture's own domain, so only a second domain can tell them apart. Also measured: clippy -D warnings clean on default and --all-features; crate tests 679 passed/0 failed (--all-features); determinism-compare.sh --self-test cases=6 failed=0, mutant failed=1; libm-ban-live.sh ok (12 diagnostics).

Pmat-Ticket: PMAT-3273

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* APEX-2a: ci.yml determinism job sets CARGO_TARGET_DIR from a step, not a job-level env — the runner context is unavailable there and the workflow failed at STARTUP with zero jobs (run 35319640264, actionlint: 'context runner is not allowed here'); PMAT-3273 fragment inventories breaks.rs and the exact gate wiring (quorum round 1 lane-1 findings)

Round-1 quorum: 2 PASS / 1 FAIL (gemini-3.1-pro-high). Finding 1: the ticket said 'determinism, determinism-compare, both in gate.needs' while gate.needs names only determinism-compare (which needs determinism, so a failed leg skips the compare and gate reads its non-success) — the text was wrong, the mechanism was not; the text is now exact. Finding 2: src/breaks.rs was edited (15827af) and absent from the EDITS list — now listed. Both are ticket-text repairs; ci.yml changes only where the startup failure was.

Pmat-Ticket: PMAT-3273

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* APEX-2a: PMAT-3273 fragment lists crates/aprender-viz/.clippy.toml under EDITS, not NEW — the file existed on main (git diff --name-status: M); quorum round 2 lane-1 finding, ticket text only

Pmat-Ticket: PMAT-3273

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* APEX-2a: PMAT-3273 title unpromises PNG identity — SVG identity is asserted, PNG identity is recorded (png_identical) and never asserted, as APEX-001 v4.7 states and as determinism-compare.sh:63 and render_determinism.rs:16 already say; quorum round 3 2/3 FAIL on exactly this title clause, ticket text only

Pmat-Ticket: PMAT-3273

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* APEX-2a: quorum verdict — 3/3 PASS on e89de51 (gemini-3.1-pro-high, gemini-3.8-flash-high, gemini-3.7-flash-high; author fable-5-1), round 4; NEW docs/audits/quorum-PMAT-3273.json only

Rounds: 1 = 2/3 (ticket said 'both in gate.needs'; breaks.rs absent from EDITS) · 2 = 2/3 (.clippy.toml listed NEW, is an edit) · 3 = 1/3 (title promised PNG identity the row records and never asserts) · 4 = 3/3. Every fix was ticket text; no code changed after 6939ce7.

Pmat-Ticket: PMAT-3273

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <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