Skip to content

ci(sovereign): roadmap-fragment-parity — a PR writes ONE fragment, never the generated aggregate - #73

Merged
noahgift merged 1 commit into
mainfrom
PMAT-3296-fragment-gate
Sep 15, 2026
Merged

noahgift merged 1 commit into
mainfrom
PMAT-3296-fragment-gate

Conversation

@noahgift

Copy link
Copy Markdown
Contributor

PR2 of the roadmap-fragment sequence. docs/roadmaps/roadmap.yaml becomes a
GENERATED aggregate of docs/roadmaps/entries/<id>.yaml; a pull request writes one
fragment, named for its own Pmat-Ticket trailer, and never touches the aggregate.

Why

Every PR writing the roadmap makes it a shared mutable file on the merge path —
Amdahl serial fraction 1, so N pull requests contend on one file however disjoint their
code is.

Measured in paiml/aprender 2026-09-15: 146 PRs opened over 10 days against 88 merged
(production 14.6/day, drain 8.8/day), and of the last 25 merge_group CI runs
7 succeeded, 15 were CANCELLED. On a nine-PR batch the textual merge placed
PMAT-3226 after PMAT-3229 and the sorted-roadmap guard went RED while all nine
inputs were individually sorted and green.

.gitattributes could not fix it: merge=roadmap is a custom driver registered
per-invocation locally, so GitHub's merge-queue server never runs it. Only git's
built-in drivers apply server-side, and union is correct for a line-delimited ledger
and wrong for a file needing sorted insertion.

Opt-in on entries/ — load-bearing, not caution

This workflow runs in every consumer repo. rmedia, infra, apex and the rest still
write roadmap.yaml legitimately, and an unconditional gate here lands in all of them
at once — the shape that took every consumer red when continue-on-error was removed
upstream.

No docs/roadmaps/entries/ ⟹ NOT-RUN, printed, never a silent pass. Same discipline
as the roadmap-valid step it sits beside. It also fails closed on an unreachable base:
a check that cannot read its input does not pass.

Evidence

The logic is the one proved in paiml/aprender's scripts/check_roadmap_fragment_parity.sh
— 6 rows, 0 red, including a registered mutation: removing the aggregate check
must make the "PR edits the aggregate" row pass, and it does.

row verdict
no entries/ dir → NOT-RUN ok
fragment matching its Pmat-Ticket trailer ok
fragment whose id is not a trailer on this branch refused
PR edits the generated aggregate refused
PR touching no fragment passes, reporting 0 ok
mutation: without the aggregate check, row 4 passes caught

Inline here rather than calling that script, because a script in the consumer repo is a
gate the producer can edit.

Not armed for aprender yet

aprender pins this workflow by SHA (70e51ec045d95983e9a3d02342d13283bf5f9dc1). It takes
effect there only when that pin is bumped — a separate PR (PR3) whose RED→GREEN evidence
is a fragment-less PR failing under the new SHA.

Becomes pmat comply

This is the bespoke form. paiml/paiml-mcp-agent-toolkit#1363 ships the check in
pmat comply, and this step should call that once 3.41.0 is on the fleet pin. Tracked
there, not forgotten.

no-close: paiml/aprender#3296 and paiml/paiml-mcp-agent-toolkit#1363 are cross-repo
references. This PR is one step of the sequence they describe and closes neither — #3296
needs PR3's pin bump, #1363 needs the pmat implementation and release.

🤖 Generated with Claude Code

…ver the aggregate

docs/roadmaps/roadmap.yaml is a GENERATED aggregate of
docs/roadmaps/entries/<id>.yaml. A pull request writes one fragment, named for
its own Pmat-Ticket trailer, and never touches the aggregate.

WHY. Every PR writing the roadmap makes it a shared mutable file on the merge
path — Amdahl serial fraction 1, so N pull requests contend on one file however
disjoint their code is. Measured in paiml/aprender 2026-09-15: 146 PRs opened
over 10 days against 88 merged, and of the last 25 merge_group CI runs 7
succeeded and 15 were CANCELLED. On a nine-PR batch the textual merge placed
PMAT-3226 after PMAT-3229 and the sorted-roadmap guard went RED while all nine
inputs were individually green.

OPT-IN ON entries/, and that is load-bearing, not caution. This workflow runs in
EVERY consumer repo, and the ones that have not migrated still write
roadmap.yaml legitimately. An unconditional gate here lands everywhere at once —
the shape that took every consumer red when continue-on-error was removed
upstream. No entries/ dir => NOT-RUN, printed, never a silent pass. Same
discipline as the roadmap-valid step it sits beside: a result that was not
measured must never read as a pass.

Also fail-closed on an unreachable base: a check that cannot read its input does
not pass.

The logic is the one proved in paiml/aprender's
scripts/check_roadmap_fragment_parity.sh — 6 rows, 0 red, including a REGISTERED
mutation (removing the aggregate check must make the "PR edits the aggregate"
row pass, and does). It is inline here rather than calling that script because a
script in the consumer repo is a gate the producer can edit.

This is the bespoke form. It becomes `pmat comply` once pmat ships the check
(paiml/paiml-mcp-agent-toolkit#1363), which is tracked there, not forgotten.

NOT ARMED FOR aprender YET: aprender pins this workflow by SHA
(70e51ec). It takes effect there only when that
pin is bumped, which is a separate PR whose RED->GREEN evidence is a
fragment-less PR failing under the new SHA.

Refs paiml/aprender#3296, paiml/paiml-mcp-agent-toolkit#1363.

Pmat-Ticket: PMAT-1363
@noahgift
noahgift merged commit f02067e into main Sep 15, 2026
2 checks passed
@noahgift
noahgift deleted the PMAT-3296-fragment-gate branch September 15, 2026 07:59
noahgift added a commit to paiml/aprender that referenced this pull request Sep 15, 2026
…tream, and this one ran

I said this guard was "wired nowhere, dark on purpose". That was WRONG.
`guard_tree.sh` discovers every script in `scripts/` and runs it; `skip_reason`
exempts a guard only when it is wired WITH ARGS in a workflow, release-time, or
an unwired baseline. Mine is none of those, so guard_tree ran it BARE — with its
default `--base $(git merge-base origin/main HEAD)` — against the one branch
that legitimately rewrites roadmap.yaml, and it failed exactly as designed:

  FAIL  docs/roadmaps/roadmap.yaml is GENERATED — run `make roadmap-aggregate`

It also pushed check_no_pipe_into_grep_q.sh over its ceiling: baseline 75,
measured 78, all three sites mine (lines 49, 61 and the mutation needle at 157).

Deleting it rather than exempting it, on merit:

  * The real gate SHIPPED UPSTREAM in paiml/.github#73 (merged, f02067e), inline
    in sovereign-ci.yml where the consumer repo cannot edit it.
  * Keeping a copy here is precisely the producer-owns-the-gate problem that
    PR's own body gives as the reason for inlining it.
  * Its case table did its job: 6 rows 0 red with a registered mutation, and
    that is what the upstream logic was proved against. It is preserved in this
    branch's history at 411e718 and cited from #73.

Also regenerates the README CONTRACT_COUNT block: this branch adds
contracts/apr-roadmap-fragments-v1.yaml, and FALSIFY-README-002 compares the
block against the MERGE tree, not the diff — 1840 -> 1841. The block is
generated, so it is an equality and not a ratchet.

Verified after:
  check_no_pipe_into_grep_q.sh   baseline 75  measured 75   rc=0
  check_readme_claims.sh         rc=0 (5/5)
  make roadmap-aggregate-check   rc=0
  roadmap_fragments --selftest   14 rows, 0 red

Pmat-Ticket: PMAT-3296
noahgift added a commit that referenced this pull request Sep 21, 2026
… untracked at the base (paiml/aprender#3709) (#77)

#73's step opts in on `docs/roadmaps/entries/` and assumes that a repo with
entries/ has stopped committing `roadmap.yaml`. aprender has entries/ and
still commits the aggregate on purpose: its own roadmap-aggregate-check
requires the committed file in sync. So bumping aprender's pin past
f02067e (needed for `coverage_on: tag`, paiml/aprender#3676) would have
refused every aprender PR, and it would also have refused the migration PR
that is meant to arm this check.

The step is now NOT-ACTIVE while `roadmap.yaml` is TRACKED at the BASE, and
says so on one line. It arms by itself once the aggregate is untracked, with
no flag to forget. It keys on the BASE tree, not the PR head: a PR that
re-adds the aggregate cannot switch its own gate off, and the PR that deletes
it is not refused by the gate that deletion arms.

Case rows, run from this step's own `run:` text in fixture repos, 7/7 ok:
- no entries/ -> NOT-RUN
- aggregate tracked at base (aprender today) -> NOT-ACTIVE
- untracked, fragment named by its Pmat-Ticket trailer -> ok
- untracked, fragment with no trailer -> refused
- untracked at base, PR re-adds the aggregate -> refused
- the migration PR deleting the tracked aggregate -> NOT-ACTIVE
- untracked, no fragment touched -> ok, 0 fragment(s)

On 69482db (before this change) the aprender-today row and the migration row
are both RED ("is GENERATED"). Mutant: keyed on HEAD instead of the base, the
re-add row passes (a hole) and the migration row is refused.

Refs paiml/aprender#3709 paiml/aprender#3676 paiml/aprender#3650

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

Development

Successfully merging this pull request may close these issues.

roadmap fragments: one file per ticket so the merge path has no shared mutable file — Amdahl serial fraction is currently 1

1 participant