Skip to content

guard(G-10b): check_pmat_pinned.sh — every analyser reference resolves through scripts/pmat_bin.sh, counted and shrink-only against a measured baseline (PMAT-1063) - #3021

Open
noahgift wants to merge 15 commits into
mainfrom
agent/G-10b

Conversation

@noahgift

@noahgift noahgift commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

PP-066 DAG row G-10b · ticket PMAT-1063 · Closes #3013 · refs #2999 · epic #2873. Receipt: docs/audits/impl-PMAT-1063-receipt.md. Follows PR-A (#3011, merged b0a0a51).

What lands. scripts/check_pmat_pinned.sh — the operator assertion as a shrink-only guard: grep -rEn '(^|[^_/])pmat ' scripts/ .github/workflows/ | grep -v pmat_bin, counted against scripts/pmat_unpinned_baseline.txt. The baseline is 243, measured by the guard itself at the commit it names (--update writes the command and the sha into the file; the "281" of the driver was the pre-PR-A count). A count above the baseline is RED naming every line; below it is an improvement to record; a missing or INVALID baseline is ENV (exit 2), never a pass. Kind-table entry (count) in check_baseline_ratchets.sh; two CI steps in guard-runner-labels (case table, then live); contract apr-pinned-analyser-ratchet-v1 1.0.0 → 1.1.0 (PIN-OB-005 / PIN-F-005). The sweep to 0 is G-10c (#3014).

Mutation evidence (I3) — on this branch, never in the queue.

leg commit what run
RED c4f6b618a mutant: one bare pmat analyze satd comment appended to scripts/ci_target_watch.sh FAIL check_pmat_pinned: unpinned=244 baseline=243 — 1 new line(s) … naming the line run id filled in after CI reports
GREEN the revert (next commit) PASS … unpinned=243 baseline=243 run id filled in

Case table bash scripts/check_pmat_pinned.sh --self-test20/20: rows 1–11 the spellings (five match, six sanctioned do not), rows 12–15 the resolver (at-pin resolves; off-pin and absent refused; option-neutral), rows 16–20 the ratchet on a fixture tree (baseline 2 PASS · baseline 1 RED naming both lines · baseline 3 PASS + improvement · no baseline ENV 2 · INVALID ENV 2).

Acceptance (re-run by the orchestrator on the re-cut base b0a0a51.pr/G-10b-verify.log): self-test 20/20 · live unpinned=243 baseline=243 · check_baseline_ratchets.sh PASS · check_guards_are_wired.sh PASS · pv validate valid · check_shell_lint_ratchet.sh PASS · check_no_claim_literals.sh rc 0.
Write set: the guard, the baseline, the kind-table line, two ci.yml steps, the contract, the receipt. No DAG/roadmap/README/spec edit.

no-close: #2873 is the epic this row belongs to and #2999 / #3014 are cited for context; this PR closes #3013 only, and an epic is not closed by one of its rows.

…cripts/ and .github/workflows/ resolves through scripts/pmat_bin.sh, counted and shrink-only against scripts/pmat_unpinned_baseline.txt (243, measured by the guard at this commit, never typed; a missing or INVALID baseline is ENV); 20-row case table incl. the ratchet's both polarities; wired case table then live; contract 1.1.0 PIN-OB-005/PIN-F-005 (PMAT-1063, #3013)

Pmat-Ticket: PMAT-1063
…get_watch.sh for the CI RED leg (I3) — check_pmat_pinned.sh must report unpinned=244 > baseline 243. REVERTED in the next commit

Pmat-Ticket: PMAT-1063
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown

§13.11 rung 1 — quorum shadow verdict

S13-SHADOW pr=3021 head=32c4854e3e565cc9f920b1b40bf4e7b1581c7ecf 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.

…D on the shrink-only step (unpinned=244 > 243); receipt PMAT-1063 complete (v4: the receipt says complete in the PR before auto-merge is armed)

Pmat-Ticket: PMAT-1063
@noahgift
noahgift enabled auto-merge September 7, 2026 07:03
…ed tree — main gained analyser mentions (#3020, #3030) after the first stamp; the baseline is introduced by this PR, so its initial count is the merged tree's

Pmat-Ticket: PMAT-1063
@noahgift
noahgift disabled auto-merge September 7, 2026 11:53
@noahgift noahgift added this to the 0.68.0 milestone Sep 10, 2026
@noahgift noahgift added the needs-rebase mergeStateStatus DIRTY: conflicts with main; rebase before it can enter the queue label Sep 11, 2026
paiml-implement lane and others added 2 commits September 13, 2026 11:42
…sh as the union of both sides (T-5 R-4)

Pmat-Ticket: PMAT-1098

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… count (288), basis re-dated; not a raise, the ratchet did not exist on main

Pmat-Ticket: PMAT-1098

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@noahgift
noahgift enabled auto-merge September 13, 2026 09:54
…4 --check)

guard-tree on the merge reported DRIFT between the spec's rendered block and docs/specifications/pp-066-dag.yaml; scripts/render_dag.py render pasted between the markers, --check green.

Pmat-Ticket: PMAT-1098

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
noahgift and others added 2 commits September 13, 2026 12:49
check_roadmap_diff_additive.sh reported base id(s) missing at head: this branch forked before they landed on main, and a plain update takes one side of the file whole. Resolved as main's roadmap plus this branch's own new entries, each at its sorted slot among same-prefix peers. The other three roadmap guards pass on the result.

Pmat-Ticket: PMAT-1098

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
noahgift and others added 2 commits September 14, 2026 20:57
# Conflicts:
#	.github/workflows/ci.yml
…ard-cargo fails on the header, before it ever reads the count

    FAIL  header  scripts/pmat_unpinned_baseline.txt declares no instrument
    3 of 19 baseline(s) name a pinned tool; MISMATCH

check_baseline_ratchets.sh requires every baseline to name the instrument that
produced it, because "a ratchet compares (tree, instrument), and a baseline that
never names its instrument cannot tell 'the count changed' from 'the analyser
changed'" (lib_baseline_ratchet.sh). This file is new in this PR and carries no
`# tool_version=` line.

The honest value is `none`, and the reason is worth stating rather than copying:
this count is produced by a grep over scripts/ and .github/workflows/, so it is a
property of the TREE. No pmat or bashrs release changes it. Pinning an instrument
here would assert a dependency that does not exist — the opposite failure to the
one the rule guards against.

The count parse is unaffected: baseline_count() reads
`sed -nE 's/^[[:space:]]*([0-9]+)...'`, which skips `#` lines, and the file still
parses as 288.

THIS FIXES THE HEADER, NOT THE COUNT. check_pmat_pinned.sh is separately red at
`unpinned=299 baseline=288`, because merging current main brought 11 more matches —
the same drift the baseline's own comment already records once ("main gained 36
unpinned references between the branch fork and the merge"). That is this PR's
design question, not a merge artifact, and it is left for its author; see the PR
comment for the measurement.

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

Copy link
Copy Markdown
Contributor Author

Two separate reds here. I fixed one and am leaving the other, because it is this PR's design
question rather than a merge artifact.

Fixed: the baseline declares no instrument

FAIL  header  scripts/pmat_unpinned_baseline.txt declares no instrument
3 of 19 baseline(s) name a pinned tool; MISMATCH

guard-cargo failed on the header before it ever read the count.
lib_baseline_ratchet.sh requires it because "a ratchet compares (tree, instrument), and a
baseline that never names its instrument cannot tell 'the count changed' from 'the analyser
changed'."

Added tool_version=none, with the reason stated rather than copied from a sibling: this count
comes from a grep over scripts/ and .github/workflows/, so it is a property of the tree.
No pmat or bashrs release changes it, and pinning an instrument here would assert a dependency
that does not exist. Count parse is unaffected — baseline_count() skips # lines and still
reads 288.

Not fixed: the count, and why it may not be fixable as specified

FAIL  check_pmat_pinned: unpinned=299 baseline=288 — 11 new line(s)

Merging current main brought 11 more matches. The baseline's own comment already records this
drift once — "main gained 36 unpinned references between the branch fork and the merge" — so
it is structural, not a one-off.

The measurement that matters:

listed references  60
  COMMENT lines    31   (52%)
  code lines       29

Over half of what this ratchet counts is prose:

scripts/dogfood-book.sh:11:        # - Use pmat comply for cross-contract gating
scripts/check_pr_review_receipt.sh:594:  # OPERATOR RULING: "'pmat doesn't work' is never accepted"
scripts/tests/baseline_tool_version_test.sh:8:  # the fleet moved pmat 3.31.0 -> 3.37.0

That third one is a comment about pinning instruments, counted as an unpinned reference.

Two consequences, both worth deciding deliberately:

  1. The ceiling drifts upward on documentation. Every comment written about pmat raises the
    count, so the baseline needs re-recording whenever main gains prose — which is what just
    happened, twice.
  2. G-10c's stated goal of sweeping to 0 is unreachable without deleting comments, because a
    comment cannot be "pinned" through scripts/pmat_bin.sh.

A regex that reads prose as code is the same shape this repo keeps finding elsewhere — I hit it
myself today when check_no_pipe_into_grep_q.sh flagged the comment I wrote explaining how to
avoid a pipe into a quiet grep. Restricting the scan to non-comment lines would make the ratchet
measure what its name says, and would make 0 an achievable target.

I have not made that change: it alters what this PR is measuring, and that is yours to decide.

🤖 Generated with Claude Code

noahgift added a commit that referenced this pull request Sep 14, 2026
…false RED costs the most

`check_no_pipe_into_grep_q.sh` — the guard this PR already fixes the regex of — flags
two sites in scripts/tests/check_roadmap_sorted_test.sh, and both are the worst
placement for this construct:

    54:  if [ "$rc" = "$want" ] && printf '%s' "$out" | grep -qE -- "$pat"; then
    165: if [ "$self_rc" = 0 ] && printf '%s' "$self_out" | grep -qE '...'; then

Line 54 is a row() helper; line 165 asserts the guard's own self-test is green.

Under `pipefail` the quiet grep exits on first match, printf takes SIGPIPE and
returns 141, and the PIPELINE is 141 — so the row reports FAILED precisely when the
pattern MATCHED. In a row() helper that false-REDs the entire table, and in the
self-test-is-green row it reports a working guard as broken. Both directions teach
the reader to stop trusting the harness, which is worse than the harness not existing.

Not hypothetical: the same construct false-REDded `guard-cargo` twice in one afternoon
(runs 34759351727, 34765202728), both times printing the wanted string in their own
failure output, both times ejecting work from the merge queue.

Converted to the here-string the guard prescribes — a here-string has no producer to
kill, so nothing can take SIGPIPE.

    harness:  9/9 checks, 0 failed
    sites:    81 -> 79, baseline restamped (shrink-only)
    bashrs:   0 errors

These two are PRE-EXISTING on main, not introduced here; main sits at exactly its
ceiling (83/83) with zero headroom, which is why they surfaced now — #3021's merge
pushed the count over and named them. Fixing them here rather than there because this
PR already owns the guard and its baseline, and a shrink-only baseline split across
two PRs strands the second.

Pmat-Ticket: PMAT-3232
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@noahgift
noahgift disabled auto-merge September 14, 2026 22:41
noahgift added a commit to guyernest/aprender that referenced this pull request Sep 15, 2026
…th no consumer is refused, not counted (paiml#3280)

* feat(ont): §11.2's five counters become measurable — and an anchor with no consumer is refused, not counted

§11.2 gives five counters, a direction each, and records "Today" as 0/0/0/0/[U].
Those numbers lived in a spec table: nothing computed them, `git ls-files
contracts/lint-baseline.json` was empty, and `grep -c '^ont-ratchet' Makefile`
was 0. A ratchet whose counters are typed into prose cannot turn.

Measured now, on the real tree:

  consumer_present           false
  contracts_total             1837      (§11.0 said 1818 — main has grown since fa6e35f)
  entity_types_registered        0
  extractors_implemented         0
  contracts_anchored             0
  contracts_shaped               0
  unanchored_but_bindable      297      <- §11.2 had this as [U]

THE GATE, and the reason this is not just a counter.

  $ printf 'entity: kernel\n' | cat - contracts/<any>.yaml > /tmp/a.yaml
  $ pv validate /tmp/a.yaml
  0 error(s), 0 warning(s)   Contract is valid.

`pv validate` ACCEPTS `entity:` — by ignoring it. There is no `pv census`, no
`pv extract`, no crates/aprender-contracts/src/ontology/, and nothing in either
contracts crate reads the key (the one grep hit was `identity:` in a test
message). So anchoring the corpus today would write 1837 rows that every gate
calls valid and no code reads, while `ont.contracts_anchored` climbed to 1837.
A counter measuring its own decoration.

ONT-001 R-2 — "zero is a decline, never an accept" — applies to the instrument
as much as to a verdict. `contracts_anchored` may not RISE while
`consumer_present` is false; the guard refuses it and names ONT-1 as the fix.
The probe is derived from `pv --help`, never declared, so it flips on its own
the day census lands.

Case table: 10 rows, and the deciding one is mutation-verified — replacing the
consumer gate with `if false` turns the table RED.

Three defects found writing it, each kept as a comment where it bit:

  * `set -euo pipefail` + a grep that matches NOTHING = pipeline exit 1 = silent
    death. Every counter here starts at zero, so the FIRST real measurement was
    the one that died: `--write` produced no file, no error, exit 1, and only
    `bash -x` showed it stopping one line after `anchored=0`.
  * a RETURN trap runs after bash destroys the function's locals, so cleanup
    died on an unbound variable AFTER the rows had passed.
  * `field()` stripped the wrong characters and compared "7" against "7}}".

AND A DEFECT IN A NEIGHBOURING GUARD. check_no_pipe_into_grep_q.sh's HAZARD
regex matched the SECOND bar of `||`, so `cmd || grep -q PAT file` — a logical
OR with no producer to kill and no SIGPIPE to misread — was flagged as a pipe.
Two pre-existing sites in this tree (dogfood.sh:1477 greps a FILE,
qwen-story.sh:281 greps a HERE-STRING) plus every new one. The regex now
requires a single bar and ships four rows in BOTH directions: `||` not flagged,
here-string not flagged, a real pipe still flagged, and a real pipe LATER on a
line that also contains `||` still flagged. Its shrink-only baseline restamped
83 -> 81.

Wiring: no workflow edit. guard_universe() is `git ls-files 'scripts/check_*.sh'`
— tracked-only — so the guard was invisible to the dispatcher until it was
staged, which is the free-pass shape that has cost this repo four guards.
Staged, `guard_tree.sh --dry-run` reports `run: scripts/check_ont_ratchet.sh`
and check_guards_are_wired.sh exits 0.

guard_tree --no-cargo: 55 checks (was 53), 2 failed — both the pmat 3.39.0
baselines this branch inherits from main, which paiml#3277 fixes. bashrs 7.4.1: 0
errors on both touched guards.

ont-delta: resolves scripts/check_ont_ratchet.sh — §11.2 was a table of numbers
nothing computed; it is now a measurement with a refusal (form 4).

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

* fix(ont): the R-5 rule, not just its number — `kernel OR binding` admits a case R-5 excludes

ONT R-5, verbatim: "Only contracts that *should* be anchored (kernel-kind with a
binding, and any contract naming a file) count against the
`unanchored_but_bindable` ratchet."

That is two disjuncts and the first is a CONJUNCTION:

    (kernel-kind AND a binding)  OR  (names a file)

The first draft spelled it `kernel OR binding OR file`, which admits a kernel-kind
contract that has no binding. Both spellings return 297 on today's corpus, because
that set is currently EMPTY:

    as shipped (kernel OR binding OR file):  297
    ONT R-5 ((kernel AND binding) OR file):  297
    kernel-kind with NO binding:               0

So the NUMBER agreed while the RULE did not, and it would have diverged silently the
first time such a contract was written — a guard that is right by accident of the
corpus is a guard that goes wrong without anything changing in it.

Also worth stating because the ratchet's direction depends on it: an unanchored
contract is NOT a defect. R-5 makes `entity:` optional by design — "many contracts
are laws, patterns or policies with nothing to anchor, and an absent entity is not
inferred to be code" (ONT-001 v4.3 §0.0, v4.1's correction to v4). Only the bindable
ones are the backlog this ↓ counter drains, which is why §11.2 made it the counter
that goes DOWN rather than making `contracts_anchored` the one that must go up.

Upstream is now readable fleet-wide: infra#583 landed v4.3 on infra main
(972 lines), so this premise is fetchable from gx10, yoga and mini rather than
living on one box.

ont-delta: none — this corrects a rule already landed in this PR; the delta is the
one already recorded against scripts/check_ont_ratchet.sh.

Pmat-Ticket: PMAT-3232
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-3232

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

* fix(tests): two pipe-into-quiet-grep rows in a test HARNESS, where a false RED costs the most

`check_no_pipe_into_grep_q.sh` — the guard this PR already fixes the regex of — flags
two sites in scripts/tests/check_roadmap_sorted_test.sh, and both are the worst
placement for this construct:

    54:  if [ "$rc" = "$want" ] && printf '%s' "$out" | grep -qE -- "$pat"; then
    165: if [ "$self_rc" = 0 ] && printf '%s' "$self_out" | grep -qE '...'; then

Line 54 is a row() helper; line 165 asserts the guard's own self-test is green.

Under `pipefail` the quiet grep exits on first match, printf takes SIGPIPE and
returns 141, and the PIPELINE is 141 — so the row reports FAILED precisely when the
pattern MATCHED. In a row() helper that false-REDs the entire table, and in the
self-test-is-green row it reports a working guard as broken. Both directions teach
the reader to stop trusting the harness, which is worse than the harness not existing.

Not hypothetical: the same construct false-REDded `guard-cargo` twice in one afternoon
(runs 34759351727, 34765202728), both times printing the wanted string in their own
failure output, both times ejecting work from the merge queue.

Converted to the here-string the guard prescribes — a here-string has no producer to
kill, so nothing can take SIGPIPE.

    harness:  9/9 checks, 0 failed
    sites:    81 -> 79, baseline restamped (shrink-only)
    bashrs:   0 errors

These two are PRE-EXISTING on main, not introduced here; main sits at exactly its
ceiling (83/83) with zero headroom, which is why they surfaced now — paiml#3021's merge
pushed the count over and named them. Fixing them here rather than there because this
PR already owns the guard and its baseline, and a shrink-only baseline split across
two PRs strands the second.

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

* fix(tests): four more row() helpers that report FAILED precisely when they MATCHED

Three test harnesses, four sites, same construct as the two fixed earlier in this PR
and the one in paiml#3134:

    scripts/tests/baseline_tool_version_test.sh:62   row() helper
    scripts/tests/baseline_tool_version_test.sh:152  the mismatch assertion
    scripts/tests/check_tool_versions_test.sh:43     row() helper
    scripts/tests/ci_gpu_touched_test.sh:34          row() helper

Under `pipefail` the quiet grep exits on first match, printf takes SIGPIPE and
returns 141, and the PIPELINE is 141 — so the row reports FAILED exactly when the
pattern MATCHED. In a row() helper that inverts the whole case table, and a case
table is what every other guard in this repo is proven by.

Chosen deliberately over the other five sites the guard still lists. These are test
HARNESSES: a mistake here cannot change what production CI decides. `ci_test_tier.sh`
(6 sites) and `ci_gpu_touched.sh` (5) decide the test tier and whether the GPU lane
fires; those stay listed in the PR comment rather than swept at this hour.

VERIFIED PER FILE, not in aggregate:

    baseline_tool_version_test.sh   rows 2-6 ok   (row 1 fails IDENTICALLY before and
                                                   after — it is this box's pmat
                                                   3.40.1 against a 3.40.0 baseline,
                                                   the fleet split in aprender#3291,
                                                   not this change; compared by
                                                   running origin/main's copy in the
                                                   same worktree)
    check_tool_versions_test.sh     6/6 checks, 0 failed
    ci_gpu_touched_test.sh          20 checks, 0 failed

    sites 79 -> 75, baseline restamped (shrink-only)
    bashrs 7.4.1: 0 errors on all three

One defect of my own on the way: the edit to line 152 left a stray `b_match=1 &&`
and a `grep` with no input, which would have read stdin and hung. Reading the result
rather than trusting the substitution is what caught it — the same habit this whole
class of fix is about.

ci_gpu_touched_test.sh:63 is left alone: the pipe is inside a single-quoted script
the test GENERATES, so converting it changes the fixture's own text rather than the
harness, and that deserves its own reasoning.

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

---------

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

needs-rebase mergeStateStatus DIRTY: conflicts with main; rebase before it can enter the queue

Projects

None yet

1 participant