Skip to content

docs(audit): record F6 — the spine's lock pins floating refs, so the template teaches non-pinning - #811

Merged
hyperpolymath merged 7 commits into
mainfrom
feat/canon-binding-phase-1
Sep 17, 2026
Merged

hyperpolymath merged 7 commits into
mainfrom
feat/canon-binding-phase-1

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

docs(audit): record F6 — the spine's lock pins floating refs, so the template teaches non-pinning

Found while wiring Gate A's counterpart into the spine, and worth recording

because it INVERTS the usual reading of the estate board.

The estate's canon requires SHA-pinned uses:. The canon's own actions.lock
enforces it (it records actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1).
But rsr-template-repo's actions.lock records FLOATING refs:

'actions/checkout@v7.0.1'
'hyperpolymath/deed-ecosystem@main'
'hyperpolymath/k9-ecosystem@main'

Measured 2026-09-17: 54 @vN/@main references across that repo's
.github/workflows/.

THAT IS UPSTREAM OF THE BOARD, NOT DOWNSTREAM OF IT

docs/ESTATE-BOARD.adoc reports sha_pinning_required: false on 8 repos and
allowed_actions: all on 148, and reads those as loose settings. They are
compatible with the template's own example. A repo minted from this spine
inherits workflows that pin tags and point at @main, and a lock that accepts
them, so the template teaches the practice the canon forbids — and no repo has
to make a decision to end up there.

WHY IT IS NOT FIXED IN THIS PR

It is a 54-reference change across .github/workflows/ plus a regenerated lock,
and the lock is load-time enforced: a mistaken edit kills every consumer with
jobs=0 and no annotation. It needs the canon's own check-allowed-actions
gate as the witness and its own PR. Filed as F6 with severity High, because the
blast radius is every repo minted from this spine rather than any single repo.

ci(gates): wire Gate A and Gate D into CI + lock the new workflow

Adds .github/workflows/canon-spine-lockstep.yml with three jobs:

lockstep Gate A — the canon and the spine are on the same law.
Checks out BOTH repos (canon at fetch-depth 0, because
assertion 2 diffs against a base ref and assertion 1 hashes
directories with git ls-files -s), then runs
scripts/check-canon-lockstep.sh. GH_TOKEN is passed because
assertion 4 reads the spine's last dogfood-gate conclusion;
without it the script degrades to SKIP rather than passing
silently.
workflow_dispatch exposes a strict input to promote
assertions 3/4/5 from SKIP to FAIL.

map Gate D — standards-map.toml integrity, in both directions.

self-conformance The canon against its own gate table. Before this change
the repository that SHIPS scripts/check-rsr-profile.sh
exited 2 on itself.

The workflow file is triggered only on paths that can actually change the
answer, so an unrelated docs PR does not pay for a two-repo checkout.

ON actions.lock — hand-edited DELIBERATELY

actions.lock enumerates every workflow file and is enforced at workflow LOAD
time. A lock that does not know about a new workflow kills every consumer with
jobs=0 and NO annotation — the exact silent failure documented in the
message of 092deda. So a new workflow file REQUIRES a lock entry in the same
commit.

gh actions-lock is not available here, and the estate has already established
that its bare update mode is not trustworthy on this tree: 092deda records it
prepending a duplicate banner above the SPDX header on all 42 workflows,
rewriting uses: ./.github/actions/signed-push to the invalid uses: $/…
spelling, and leaving genuinely-stale rows in place while exiting 0 reporting
"All 42 workflows valid".

The VERIFIER is trustworthy even though the updater is not, and the entry is
trivially checkable, so the row was added by hand and then witnessed:

yaml.safe_load('.github/workflows/actions.lock')
-> 42 workflows in lock (was 41)
-> every lock entry corresponds to a real file: []
-> uses: in the new workflow == lock entry for the new workflow: MATCH

The single pin used is actions/checkout@3d3c42e
(# v7.0.1) — the same pin as the other 61 uses in this repo. No new action is
introduced, and no secret is referenced (only github.token), so the pin SET
did not change. That is what makes a hand-edit safe here specifically.

Negative control: the entry is placed alphabetically (boj-build <
canon-spine-lockstep < casket-pages). A stray entry would fail the
"lock entries with no file" check; a missing one fails at load time.

WHAT THIS DOES NOT DO

Assertions 3/4/5 SKIP unless --strict or the workflow_dispatch input is set.
They cannot pass yet: the spine does not carry a [canon] block, and the one
normative oracle (hypatia:rsr-conformance) is still marked "to be implemented"
in the criteria SSOT. Wiring them as hard failures now would block every PR to
this repository. They become hard failures in execution-plan §Phase 5.4,
after the spine adopts this canon.

feat(map): add standards-map.toml — the repository, readable by machine

THE COMPLAINT, QUANTIFIED

This repository has 118 top-level entries (57 files, 61 directories) and 2,639
tracked files. .machine_readable/REGISTRY.a2ml indexes 33 of them — the
SPECS — and does it well. The other 85 have no declared domain, kind,
lifecycle, relationship or gate. THAT ABSENCE IS THE "UNREADABLE" COMPLAINT.

It is not fixable by prose, because the eight kinds of thing interleaved at one
level are not distinguishable by convention that exists in anyone's head: law,
format specs, protocol specs, grading frameworks, lifecycle gates, enforcement
machinery, generated telemetry, migration debris. Nothing tells a reader why
audits/ and docs/audits/ both exist, or licenses/ and LICENSES/, or
tasks/ versus tools/.

WHAT THIS ADDS

standards-map.toml — one [[entry]] per top-level entry, ALL 118 plus the two
files this series introduces. Per entry: target district, kind, lifecycle
(versioned | generated | frozen | deprecated), canonical (may it be linked
normatively by other repos?), canon_slot (join key into canon.lock),
canonical_doc, and the CI gate that fails if it is wrong.

Four things fall out of it that are impossible today:

  • canonical = true filtered by district IS the answer to "can every spec be
    pointed at?" — the SSOT rule, checkable rather than asserted.
  • lifecycle = "generated" IS the do-not-hand-edit list, which is currently
    prose repeated in three files that disagree.
  • canon_slot IS the join between the map and canon.lock — which files, when
    changed, force a canon release.
  • gate IS the answer to "what fails if I break this?"

BIDIRECTIONAL, DELIBERATELY

scripts/check-standards-map.sh copies the hardest-won lesson in the estate, and
it is the template's own words in machine-readable/root-allow.txt:

"The check is BIDIRECTIONAL:
   * anything at root that is not listed here is drift, and fails;
   * anything listed here WITHOUT the '?' marker must exist, and its
     absence fails.
 The second half is the important one. … A one-directional allowlist only
 ever ratchets open."

That comment was written after a root cleanup left stale PERMISSIONS behind, so
the allowlist had "quietly become a licence for the very drift it was written to
prevent". A map of this repository has exactly the same failure mode, so it gets
exactly the same test.

WHY IT LANDS BEFORE ANYTHING MOVES

On the unmoved tree this is trivially green, and it proves the checker works.
After the reorganisation it is the artefact that proves the move was COMPLETE.
A checker first exercised BY the move is a checker nobody trusts.

It has already earned its keep: on its first run against the working tree it
correctly flagged the two files this same change series had just added at root
(canon.lock, standards-map.toml) as unmapped, rather than silently passing.

WITNESSES

scripts/check-standards-map.sh --repo .
[1] all 120 mapped paths exist
[2] all 120 top-level entries are mapped
[3] every canonical entry names a canonical_doc
[4] all canon_slot values resolve:
applicability constitution contractile-spec criteria
deed-grammar gates lifecycle rsr-spec-home
[5] entry_count = 120
-> GATE D PASSED

Negative control (the check is fallible, not merely green):
an unmapped entry fails [2]; a listed path that is gone fails [1].

NOTES ON CONTENT

  • target records the post-reorg path; from is the current one. The map is
    therefore also the migration manifest for 03-STANDARDS-REORG.md, and the
    executes-in-reviewable-steps claim is verifiable rather than promised.
  • The three canon.split-candidates slots (deed-grammar, contractile-spec,
    rsr-spec-home) resolve against canon.lock, so a future split cannot
    silently orphan a map entry.

feat(gates): add role=canon — unblocks the law being subject to the law

THE MECHANICAL BLOCKER THIS REMOVES

find . -name "rsr-profile*" in this repository returned NOTHING. The repo
that ships scripts/check-rsr-profile.sh exited 2 on itself: no profile at ./.machine_readable/rsr-profile.a2ml. THE LAW WAS NOT SUBJECT TO THE LAW.

The reason was structural, not backlog. rsr-criteria-v2.a2ml's applicable-set
rule is:

A criterion is APPLICABLE iff its `gate` is `universal` OR the repo's
rsr-profile declares the gating capability.

template-capability-gates.toml carried a [carrier] section for role = "spine"
— the template legitimately carries rust-ci.yml while having no Rust — but had
NO equivalent for the canon. So the moment this repo got a profile it was
scored against gates that are impossible for it to satisfy: it carries the
criteria SSOT (which no capability gates — the criteria are the document that
DEFINES the gates, not an instance of one), docs/proofs/ (283 files, which are
proof artefacts OF THE ESTATE, not proofs of this repo's own code), and
.github/workflows/*-reusable.yml (the gates every other repo CALLS and this
repo SERVES). Compliance was unreachable BY CONSTRUCTION.

WHAT THIS COMMIT DOES

  1. template-capability-gates.toml

    • "canon" added to [capabilities].known — without it, role = "canon" is
      rejected as an unknown CAPABILITY rather than accepted as a role
    • new [canon] section listing the 11 paths the canon may carry exempt from
      VESTIGIAL, in the same voice and for the same reason as [carrier]
    • new canon preset = ["bash", "docs-site", "governance-tier"]
  2. scripts/check-rsr-profile.sh

    • the ROLE switch gains a canon arm selecting [canon] paths, and an
      explicit warning for an unknown role instead of silently exempting nothing
  3. .machine_readable/rsr-profile.a2ml (NEW)

    • role = "canon"; declares bash (107 gate scripts), docs-site (casket-pages),
      governance-tier, reproducible-build (guix.scm is a real Guix manifest)
    • a [canon] block carrying the criteria and gates hashes, so this file is
      the spine-side half of the lockstep
    • an [notes] block giving a REASON for every omitted capability, the same
      discipline rsr-template-repo applies in its own profile
  4. docs/AUDIT.adoc, docs/AFFIRMATION.adoc (NEW)

    • declaring governance-tier exposed two genuine gaps the moment the
      checker could read the profile: the canon carried GOVERNANCE.adoc and
      MAINTAINERS.adoc at root but had NO AUDIT.adoc and NO AFFIRMATION.adoc.
    • AFFIRMATION.adoc is authored in profile B (MUST / INTEND / WISH),
      because this repo is a policy surface and its own
      docs/AFFIRMATION-STANDARD.adoc §"Profile B — required sections" says so.
    • It is marked DRAFT — UNSIGNED. Every claim is measured and reproducible;
      the attestation block is deliberately empty, because a signature is the
      owner's to give and not an agent's. IT MUST NOT BE CITED UNTIL SIGNED.
    • AUDIT.adoc adds the gate inventory and the carried findings table, so the
      audit posture is visible without discovering the standards first.

WITNESSES

scripts/check-rsr-profile.sh .
BEFORE: ERROR: no profile at ./.machine_readable/rsr-profile.a2ml
AFTER: role: canon - [canon] paths exempt from VESTIGIAL
rsr-profile check: OK — scaffold matches declared capabilities.

The FIRST run after adding the profile returned three real findings, all
resolved honestly rather than suppressed:
VESTIGIAL (no 'reproducible-build'): guix.scm|build/guix.scm|flake.nix
-> guix.scm IS a real Guix manifest; reproducible-build is now DECLARED.
Declaring it is the truthful resolution; adding the path to [canon]
would have been a suppression.
MISSING (governance-tier): docs/AUDIT.adoc|AUDIT.adoc
MISSING (governance-tier): docs/AFFIRMATION.adoc|AFFIRMATION.adoc
-> both authored. Not stubbed, not carrier-exempted.

scripts/check-canon-lockstep.sh --canon . --base HEAD
-> assertion 1: 5/5 verified, including the RECOMPUTED gates hash
-> assertion 2: PASS (gates changed AND canon.lock bumped in this commit,
which is assertion 2's whole purpose, demonstrated on itself)
-> assertion 5: now PASSES the existence + role check

python3 -c "import tomllib; tomllib.load(...)" -> TOML OK
bash -n scripts/check-rsr-profile.sh -> syntax OK

WHAT IS STILL RED

Nothing in this commit. The canon now satisfies the checker it ships. It does
NOT yet hold Gold under rsr-criteria-v2 — that requires the one normative
oracle (hypatia:rsr-conformance), which is still marked "to be implemented".
docs/AFFIRMATION.adoc records that under Held rather than claiming a tier.

feat(canon): add canon.lock — the released identity of the canon

WHAT THIS IS

canon.lock makes "which standards?" a question with a hashable answer.

Today the standards -> template binding is two free-text strings in
rsr-template-repo/machine-readable/rsr-profile.a2ml:

spec             = "rsr-criteria-v2"
declares-against = "2.0.0-draft"

Nothing fails when the criteria change, when the gate table changes, or when
the applicability policy changes. A grep for any version identifier across both
repos returns exactly ONE hit: that string.

This file is the mechanism rsr-criteria-v2.a2ml already asks for under
[versioning]:

freeze-mechanism = "A released major is frozen by pinning this file's
                    byte-hash ... NOT by a prose 'FROZEN' banner."

It promotes that per-file pin to a single release point for the whole canon:
version + sha256 of every law artefact + the reusable-workflow refs + the roles.

WHAT IT RECORDS

[canon.artifacts] criteria, gates, applicability, lifecycle, constitution —
each with a REAL sha256 computed against this tree
[canon.split-candidates] deed-grammar, contractile-spec, rsr-spec — listed NOW
so a future split does not orphan a standards-map slot
[canon.workflows] the reusable gates every repo calls
[canon.roles] canon / spine / composer / oracle / fleet
[canon.lockstep] Gate A's five assertions and their ORDER

THE RULE

Any change to a file named in [canon.artifacts] is a CANON CHANGE and MUST,
in the same commit, bump [canon].version and rewrite the hash. That is
assertion 2 of Gate A, implemented in scripts/check-canon-lockstep.sh.

Assertion 4 is a deliberate reversal and is the point of the exercise:

YOU MAY NOT TIGHTEN THE CRITERIA UNTIL THE REFERENCE IMPLEMENTATION
PASSES THEM.

PATHS AT THIS REVISION

The paths recorded are the CURRENT in-tree locations, not the post-reorg paths
in docs/binding/03-STANDARDS-REORG.md. This file must be green on the tree it
lands on. Because every artefact move is a path change to a hash-pinned file,
THE MOVE COMMIT MUST BUMP THIS FILE IN THE SAME COMMIT — which is assertion 2
doing exactly what it was built for.

WHAT IS STILL RED, AND WHY THAT IS CORRECT

Assertion 5 fails: this repo has NO rsr-profile.a2ml, so
scripts/check-rsr-profile.sh exits 2 on the repo that ships it. The law is not
subject to the law. That is fixed by the next commit in this series; it is left
failing here rather than stubbed green.

Witnesses:
scripts/check-canon-lockstep.sh --canon . --base HEAD
-> assertion 1: 5/5 artefacts verified against the tree
-> assertion 2: PASS
-> assertion 5: FAIL (correct — no profile exists yet)

WHAT THIS IS

`canon.lock` makes "which standards?" a question with a hashable answer.

Today the standards -> template binding is two free-text strings in
rsr-template-repo/machine-readable/rsr-profile.a2ml:

    spec             = "rsr-criteria-v2"
    declares-against = "2.0.0-draft"

Nothing fails when the criteria change, when the gate table changes, or when
the applicability policy changes. A grep for any version identifier across both
repos returns exactly ONE hit: that string.

This file is the mechanism rsr-criteria-v2.a2ml already asks for under
[versioning]:

    freeze-mechanism = "A released major is frozen by pinning this file's
                        byte-hash ... NOT by a prose 'FROZEN' banner."

It promotes that per-file pin to a single release point for the whole canon:
version + sha256 of every law artefact + the reusable-workflow refs + the roles.

WHAT IT RECORDS

  [canon.artifacts]   criteria, gates, applicability, lifecycle, constitution —
                      each with a REAL sha256 computed against this tree
  [canon.split-candidates]  deed-grammar, contractile-spec, rsr-spec — listed NOW
                      so a future split does not orphan a standards-map slot
  [canon.workflows]   the reusable gates every repo calls
  [canon.roles]       canon / spine / composer / oracle / fleet
  [canon.lockstep]    Gate A's five assertions and their ORDER

THE RULE

Any change to a file named in [canon.artifacts] is a CANON CHANGE and MUST,
in the same commit, bump [canon].version and rewrite the hash. That is
assertion 2 of Gate A, implemented in scripts/check-canon-lockstep.sh.

Assertion 4 is a deliberate reversal and is the point of the exercise:

    YOU MAY NOT TIGHTEN THE CRITERIA UNTIL THE REFERENCE IMPLEMENTATION
    PASSES THEM.

PATHS AT THIS REVISION

The paths recorded are the CURRENT in-tree locations, not the post-reorg paths
in docs/binding/03-STANDARDS-REORG.md. This file must be green on the tree it
lands on. Because every artefact move is a path change to a hash-pinned file,
THE MOVE COMMIT MUST BUMP THIS FILE IN THE SAME COMMIT — which is assertion 2
doing exactly what it was built for.

WHAT IS STILL RED, AND WHY THAT IS CORRECT

Assertion 5 fails: this repo has NO rsr-profile.a2ml, so
scripts/check-rsr-profile.sh exits 2 on the repo that ships it. The law is not
subject to the law. That is fixed by the next commit in this series; it is left
failing here rather than stubbed green.

Witnesses:
  scripts/check-canon-lockstep.sh --canon . --base HEAD
    -> assertion 1: 5/5 artefacts verified against the tree
    -> assertion 2: PASS
    -> assertion 5: FAIL (correct — no profile exists yet)
THE MECHANICAL BLOCKER THIS REMOVES

`find . -name "rsr-profile*"` in this repository returned NOTHING. The repo
that ships scripts/check-rsr-profile.sh exited 2 on itself: `no profile at
./.machine_readable/rsr-profile.a2ml`. THE LAW WAS NOT SUBJECT TO THE LAW.

The reason was structural, not backlog. rsr-criteria-v2.a2ml's applicable-set
rule is:

    A criterion is APPLICABLE iff its `gate` is `universal` OR the repo's
    rsr-profile declares the gating capability.

template-capability-gates.toml carried a [carrier] section for role = "spine"
— the template legitimately carries rust-ci.yml while having no Rust — but had
NO equivalent for the canon. So the moment this repo got a profile it was
scored against gates that are impossible for it to satisfy: it carries the
criteria SSOT (which no capability gates — the criteria are the document that
DEFINES the gates, not an instance of one), docs/proofs/ (283 files, which are
proof artefacts OF THE ESTATE, not proofs of this repo's own code), and
.github/workflows/*-reusable.yml (the gates every other repo CALLS and this
repo SERVES). Compliance was unreachable BY CONSTRUCTION.

WHAT THIS COMMIT DOES

1. template-capability-gates.toml
   - "canon" added to [capabilities].known — without it, role = "canon" is
     rejected as an unknown CAPABILITY rather than accepted as a role
   - new [canon] section listing the 11 paths the canon may carry exempt from
     VESTIGIAL, in the same voice and for the same reason as [carrier]
   - new `canon` preset = ["bash", "docs-site", "governance-tier"]

2. scripts/check-rsr-profile.sh
   - the ROLE switch gains a `canon` arm selecting [canon] paths, and an
     explicit warning for an unknown role instead of silently exempting nothing

3. .machine_readable/rsr-profile.a2ml (NEW)
   - role = "canon"; declares bash (107 gate scripts), docs-site (casket-pages),
     governance-tier, reproducible-build (guix.scm is a real Guix manifest)
   - a [canon] block carrying the criteria and gates hashes, so this file is
     the spine-side half of the lockstep
   - an [notes] block giving a REASON for every omitted capability, the same
     discipline rsr-template-repo applies in its own profile

4. docs/AUDIT.adoc, docs/AFFIRMATION.adoc (NEW)
   - declaring `governance-tier` exposed two genuine gaps the moment the
     checker could read the profile: the canon carried GOVERNANCE.adoc and
     MAINTAINERS.adoc at root but had NO AUDIT.adoc and NO AFFIRMATION.adoc.
   - AFFIRMATION.adoc is authored in **profile B** (MUST / INTEND / WISH),
     because this repo is a *policy surface* and its own
     docs/AFFIRMATION-STANDARD.adoc §"Profile B — required sections" says so.
   - It is marked DRAFT — UNSIGNED. Every claim is measured and reproducible;
     the attestation block is deliberately empty, because a signature is the
     owner's to give and not an agent's. IT MUST NOT BE CITED UNTIL SIGNED.
   - AUDIT.adoc adds the gate inventory and the carried findings table, so the
     audit posture is visible without discovering the standards first.

WITNESSES

  scripts/check-rsr-profile.sh .
    BEFORE: ERROR: no profile at ./.machine_readable/rsr-profile.a2ml
    AFTER:  role: canon - [canon] paths exempt from VESTIGIAL
            rsr-profile check: OK — scaffold matches declared capabilities.

  The FIRST run after adding the profile returned three real findings, all
  resolved honestly rather than suppressed:
    VESTIGIAL (no 'reproducible-build'): guix.scm|build/guix.scm|flake.nix
        -> guix.scm IS a real Guix manifest; reproducible-build is now DECLARED.
           Declaring it is the truthful resolution; adding the path to [canon]
           would have been a suppression.
    MISSING (governance-tier): docs/AUDIT.adoc|AUDIT.adoc
    MISSING (governance-tier): docs/AFFIRMATION.adoc|AFFIRMATION.adoc
        -> both authored. Not stubbed, not carrier-exempted.

  scripts/check-canon-lockstep.sh --canon . --base HEAD
    -> assertion 1: 5/5 verified, including the RECOMPUTED gates hash
    -> assertion 2: PASS (gates changed AND canon.lock bumped in this commit,
       which is assertion 2's whole purpose, demonstrated on itself)
    -> assertion 5: now PASSES the existence + role check

  python3 -c "import tomllib; tomllib.load(...)"  -> TOML OK
  bash -n scripts/check-rsr-profile.sh            -> syntax OK

WHAT IS STILL RED

Nothing in this commit. The canon now satisfies the checker it ships. It does
NOT yet hold Gold under rsr-criteria-v2 — that requires the one normative
oracle (hypatia:rsr-conformance), which is still marked "to be implemented".
docs/AFFIRMATION.adoc records that under *Held* rather than claiming a tier.
THE COMPLAINT, QUANTIFIED

This repository has 118 top-level entries (57 files, 61 directories) and 2,639
tracked files. `.machine_readable/REGISTRY.a2ml` indexes 33 of them — the
SPECS — and does it well. The other 85 have no declared domain, kind,
lifecycle, relationship or gate. THAT ABSENCE IS THE "UNREADABLE" COMPLAINT.

It is not fixable by prose, because the eight kinds of thing interleaved at one
level are not distinguishable by convention that exists in anyone's head: law,
format specs, protocol specs, grading frameworks, lifecycle gates, enforcement
machinery, generated telemetry, migration debris. Nothing tells a reader why
`audits/` and `docs/audits/` both exist, or `licenses/` and `LICENSES/`, or
`tasks/` versus `tools/`.

WHAT THIS ADDS

standards-map.toml — one [[entry]] per top-level entry, ALL 118 plus the two
files this series introduces. Per entry: target district, kind, lifecycle
(versioned | generated | frozen | deprecated), canonical (may it be linked
normatively by other repos?), canon_slot (join key into canon.lock),
canonical_doc, and the CI gate that fails if it is wrong.

Four things fall out of it that are impossible today:

  * `canonical = true` filtered by district IS the answer to "can every spec be
    pointed at?" — the SSOT rule, checkable rather than asserted.
  * `lifecycle = "generated"` IS the do-not-hand-edit list, which is currently
    prose repeated in three files that disagree.
  * `canon_slot` IS the join between the map and canon.lock — which files, when
    changed, force a canon release.
  * `gate` IS the answer to "what fails if I break this?"

BIDIRECTIONAL, DELIBERATELY

scripts/check-standards-map.sh copies the hardest-won lesson in the estate, and
it is the template's own words in machine-readable/root-allow.txt:

    "The check is BIDIRECTIONAL:
       * anything at root that is not listed here is drift, and fails;
       * anything listed here WITHOUT the '?' marker must exist, and its
         absence fails.
     The second half is the important one. … A one-directional allowlist only
     ever ratchets open."

That comment was written after a root cleanup left stale PERMISSIONS behind, so
the allowlist had "quietly become a licence for the very drift it was written to
prevent". A map of this repository has exactly the same failure mode, so it gets
exactly the same test.

WHY IT LANDS BEFORE ANYTHING MOVES

On the unmoved tree this is trivially green, and it proves the checker works.
After the reorganisation it is the artefact that proves the move was COMPLETE.
A checker first exercised BY the move is a checker nobody trusts.

It has already earned its keep: on its first run against the working tree it
correctly flagged the two files this same change series had just added at root
(canon.lock, standards-map.toml) as unmapped, rather than silently passing.

WITNESSES

  scripts/check-standards-map.sh --repo .
    [1] all 120 mapped paths exist
    [2] all 120 top-level entries are mapped
    [3] every canonical entry names a canonical_doc
    [4] all canon_slot values resolve:
        applicability constitution contractile-spec criteria
        deed-grammar gates lifecycle rsr-spec-home
    [5] entry_count = 120
    -> GATE D PASSED

  Negative control (the check is fallible, not merely green):
    an unmapped entry fails [2]; a listed path that is gone fails [1].

NOTES ON CONTENT

  * `target` records the post-reorg path; `from` is the current one. The map is
    therefore also the migration manifest for 03-STANDARDS-REORG.md, and the
    executes-in-reviewable-steps claim is verifiable rather than promised.
  * The three `canon.split-candidates` slots (deed-grammar, contractile-spec,
    rsr-spec-home) resolve against canon.lock, so a future split cannot
    silently orphan a map entry.
Adds .github/workflows/canon-spine-lockstep.yml with three jobs:

  lockstep          Gate A — the canon and the spine are on the same law.
                    Checks out BOTH repos (canon at fetch-depth 0, because
                    assertion 2 diffs against a base ref and assertion 1 hashes
                    directories with `git ls-files -s`), then runs
                    scripts/check-canon-lockstep.sh. GH_TOKEN is passed because
                    assertion 4 reads the spine's last dogfood-gate conclusion;
                    without it the script degrades to SKIP rather than passing
                    silently.
                    `workflow_dispatch` exposes a `strict` input to promote
                    assertions 3/4/5 from SKIP to FAIL.

  map               Gate D — standards-map.toml integrity, in both directions.

  self-conformance  The canon against its own gate table. Before this change
                    the repository that SHIPS scripts/check-rsr-profile.sh
                    exited 2 on itself.

The workflow file is triggered only on paths that can actually change the
answer, so an unrelated docs PR does not pay for a two-repo checkout.

ON actions.lock — hand-edited DELIBERATELY

actions.lock enumerates every workflow file and is enforced at workflow LOAD
time. A lock that does not know about a new workflow kills every consumer with
`jobs=0` and NO annotation — the exact silent failure documented in the
message of 092deda. So a new workflow file REQUIRES a lock entry in the same
commit.

`gh actions-lock` is not available here, and the estate has already established
that its bare *update* mode is not trustworthy on this tree: 092deda records it
prepending a duplicate banner above the SPDX header on all 42 workflows,
rewriting `uses: ./.github/actions/signed-push` to the invalid `uses: $/…`
spelling, and leaving genuinely-stale rows in place while exiting 0 reporting
"All 42 workflows valid".

The VERIFIER is trustworthy even though the updater is not, and the entry is
trivially checkable, so the row was added by hand and then witnessed:

  yaml.safe_load('.github/workflows/actions.lock')
    -> 42 workflows in lock (was 41)
    -> every lock entry corresponds to a real file: []
    -> uses: in the new workflow == lock entry for the new workflow: MATCH

  The single pin used is actions/checkout@3d3c42e
  (# v7.0.1) — the same pin as the other 61 uses in this repo. No new action is
  introduced, and no secret is referenced (only github.token), so the pin SET
  did not change. That is what makes a hand-edit safe here specifically.

  Negative control: the entry is placed alphabetically (boj-build <
  canon-spine-lockstep < casket-pages). A stray entry would fail the
  "lock entries with no file" check; a missing one fails at load time.

WHAT THIS DOES NOT DO

Assertions 3/4/5 SKIP unless --strict or the workflow_dispatch input is set.
They cannot pass yet: the spine does not carry a [canon] block, and the one
normative oracle (hypatia:rsr-conformance) is still marked "to be implemented"
in the criteria SSOT. Wiring them as hard failures now would block every PR to
this repository. They become hard failures in execution-plan §Phase 5.4,
after the spine adopts this canon.
…template teaches non-pinning

Found while wiring Gate A's counterpart into the spine, and worth recording
because it INVERTS the usual reading of the estate board.

The estate's canon requires SHA-pinned `uses:`. The canon's own actions.lock
enforces it (it records `actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1`).
But rsr-template-repo's actions.lock records FLOATING refs:

    'actions/checkout@v7.0.1'
    'hyperpolymath/deed-ecosystem@main'
    'hyperpolymath/k9-ecosystem@main'

Measured 2026-09-17: 54 `@vN`/`@main` references across that repo's
.github/workflows/.

THAT IS UPSTREAM OF THE BOARD, NOT DOWNSTREAM OF IT

docs/ESTATE-BOARD.adoc reports `sha_pinning_required: false` on 8 repos and
`allowed_actions: all` on 148, and reads those as loose settings. They are
compatible with the template's own example. A repo minted from this spine
inherits workflows that pin tags and point at `@main`, and a lock that accepts
them, so the template teaches the practice the canon forbids — and no repo has
to make a decision to end up there.

WHY IT IS NOT FIXED IN THIS PR

It is a 54-reference change across .github/workflows/ plus a regenerated lock,
and the lock is load-time enforced: a mistaken edit kills every consumer with
`jobs=0` and no annotation. It needs the canon's own `check-allowed-actions`
gate as the witness and its own PR. Filed as F6 with severity High, because the
blast radius is every repo minted from this spine rather than any single repo.
@coderabbitai

coderabbitai Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: d664b414-efd9-4d1c-93d6-610d57d0dfe0

📥 Commits

Reviewing files that changed from the base of the PR and between bbecaee and 44fbf58.

⛔ Files ignored due to path filters (2)
  • .github/workflows/actions.lock is excluded by !**/*.lock
  • canon.lock is excluded by !**/*.lock
📒 Files selected for processing (9)
  • .github/workflows/canon-spine-lockstep.yml
  • .machine_readable/rsr-profile.a2ml
  • .machine_readable/template-capability-gates.toml
  • docs/AFFIRMATION.adoc
  • docs/AUDIT.adoc
  • scripts/check-canon-lockstep.sh
  • scripts/check-rsr-profile.sh
  • scripts/check-standards-map.sh
  • standards-map.toml
 ______________________________________________________________
< Your function has more side effects than my caffeine intake. >
 --------------------------------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@@ -0,0 +1,126 @@
// SPDX-License-Identifier: MPL-2.0
Found by the estate's own CI on PR #811 — my gate as written made its own PR
unmergeable, and inverted the ordering rule it was built to enforce.

THE BUG

The commit that wired this gate claimed "Assertions 3/4/5 SKIP unless --strict
or the workflow_dispatch input is set". That was FALSE as implemented. Every
workflow invocation passes --spine, and assertion 3 failed HARD whenever a spine
directory was present — regardless of --strict. Result:

  Canon / spine lockstep  ->  failure  (exit 1)

Why that is self-defeating, not merely inconvenient: assertion 3 asks "has the
spine adopted THIS canon?". The spine's adoption necessarily pins hashes that
exist only in this unmerged PR. So a hard assertion 3 means NO canon release can
ever merge before the spine has adopted it, and the spine cannot adopt it before
it is released. The gate deadlocked itself.

canon.lock [canon.lockstep].order says "spine-adopts-then-canon-releases". The
script must not contradict the file it reads.

THE FIX — three classes of outcome, not two

  fail()      a HARD invariant (assertions 1 and 2). Always fails.
  softfail()  a REAL deviation (spine on a different canon, canon has no
              profile, profile role wrong). FAILS under --strict; reported as
              INFO otherwise.
  skip()      the check could not RUN (no `gh`, no `hypatia`, no --spine). ALWAYS
              informational, and NEVER promoted by --strict.

That last point is a second bug fixed here. Previously skip() promoted to failure
under --strict, so --strict could never pass in any environment lacking `gh` and
`hypatia` — and `hypatia`'s oracle is marked "to be implemented" in the criteria
SSOT, so --strict was permanently red. An absent TOOL is not a lockstep
DEVIATION; conflating them trains people to ignore the gate.

A green build must not read as "fully verified", so skipped assertions are now
collected and printed as an explicit NOT VERIFIED block:

  passed 8   failed 0   skipped 2

  NOT VERIFIED (these assertions did not run — a green result is NOT a full
  verification):
      - gh not available; cannot read the spine's last dogfood-gate conclusion
      - hypatia (the one normative oracle) not available; oracle is marked
        'to be implemented'

WHERE DRIFT IS ACTUALLY CAUGHT

This makes canon CI informational for adoption status, which is correct — it is
the SPINE's job to track the canon, and the spine's own `Canon lockstep` job
(dogfood-gate.yml, in PR hyperpolymath/rsr-template-repo#101) fails hard on a
mismatch. Canon CI: "have you adopted?" Spine CI: "are you still on it?"

WITNESSES

  non-strict, real spine branch          -> passed 8  failed 0  skipped 2
                                            GATE A PASSED
  --strict, real spine branch            -> passed 8  failed 0  skipped 2
                                            GATE A PASSED (+ NOT VERIFIED block)
  NEGATIVE, spine pin corrupted:
    non-strict                           -> INFO  "spine is on a DIFFERENT canon"
                                            GATE A PASSED   (correct: canon CI
                                            does not gate on adoption status)
    --strict                             -> FAIL  "spine is on a DIFFERENT canon"
                                            GATE A FAILED, exit 1
  NEGATIVE, --strict with no spine       -> FAIL, exit 1
  bash -n                                -> syntax OK
…ed on spelling

The real cause of the CI failure on this PR, found by reading the job log rather
than by inference. Assertions 1 and 2 were accusing a valid PR of two things it
had not done:

  FAIL  constitution  constitution/
        -> the law changed without re-releasing canon.lock (bump version + rewrite hash)
  FAIL  canon artefacts changed ( gates ) but canon.lock is untouched
        -> Bump [canon].version and rewrite the hash in the SAME commit.

Neither was true. The law had not changed, and canon.lock WAS bumped. Both
assertions were reading paths that git never matched.

THE BUG

Git pathspecs resolve against the REPOSITORY ROOT and know nothing about the
--canon prefix. Two places handed git "$CANON/..." instead of a repo-relative
path:

  hash_path()   cd "$CANON" && git ls-files -s "$CANON/constitution/"
                -> matched no path, printed nothing, and sha256sum faithfully
                   hashed the EMPTY stream: e3b0c442…, the digest of nothing.
                   Compared against the real 7fbd1015… -> "the law changed".

  assertion 2   git -C "$CANON" diff --quiet "$BASE_REF"...HEAD -- "$CANON/canon.lock"
                -> matched no path, so `git diff --quiet` exited 0 ("nothing
                   changed") and the gate reported the lock as untouched.

WHY CI AND MY LOCAL RUN DISAGREED

I had only ever invoked it as `--canon .`. With that spelling "./constitution/"
and "./canon.lock" happen to resolve, so the pathspec matched by accident and
the gate reported "passed 8  failed 0  GATE A PASSED". CI invokes it as
`--canon canon`; the same code then reported "passed 6  failed 2  GATE A
FAILED". The verdict was a function of how the argument was SPELLED, not of the
state of the repository. A gate that can be made green by relabelling its input
is not a gate.

Reproduced before fixing, byte-for-byte:

  --canon standards  -> passed 6   failed 2   skipped 2   GATE A FAILED   (= CI)
  --canon .          -> passed 8   failed 0   skipped 2   GATE A PASSED

THE FIX

  * hash_path() strips the --canon prefix before handing the path to git, and
    REFUSES to hash an empty listing. The old code's failure mode was invisible
    because hashing nothing yields a plausible 64-hex digest; a wrong pathspec
    can no longer be mistaken for a hash.
  * assertion 1 reports "could not hash" instead of comparing against it.
  * $LOCK_REL, the repo-relative twin of $LOCK, is used for the `git diff`
    pathspec in assertion 2.

WITNESSES AFTER THE FIX — all three spellings now agree

  --canon standards                    -> passed 8  failed 0  skipped 2  GATE A PASSED
  --canon .                            -> passed 8  failed 0  skipped 2  GATE A PASSED
  --canon /home/user/work/standards    -> passed 8  failed 0  skipped 2  GATE A PASSED

  NEGATIVE CONTROL, untracked directory declared in canon.lock:
    ERROR: git ls-files matched no files for 'zzz/' in .
    FAIL  constitution: could not hash 'zzz/' — see the error above
  (the old code would have hashed the empty stream and produced a digest)

Also carried from the previous commit on this branch: assertions 3/4/5 are
informational unless --strict, and skip() is no longer promoted by --strict
(an absent tool is not a deviation). With this fix, non-strict on this PR
yields 1-2-4-5 pass, 3 informational pending the spine's adoption in
hyperpolymath/rsr-template-repo#101, and a loud NOT VERIFIED block.

bash -n -> syntax OK.
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
C Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@hyperpolymath
hyperpolymath merged commit 41a6d7b into main Sep 17, 2026
26 of 32 checks passed
@hyperpolymath
hyperpolymath deleted the feat/canon-binding-phase-1 branch September 17, 2026 17:43
hyperpolymath added a commit that referenced this pull request Sep 17, 2026
…t left by #811 (#813)

Follow-up to #811, which merged at `ca1c24e`. This is the one commit
from that branch that was **not** part of the merge: `236fcb8`.

## Why this exists

Triage of the remaining CI failures on #811 turned up two defects in the
merged work itself. Both are still live on `main`.

### 1. `Hypatia` was not a workflow

I triaged it last because it looked like an unrelated pre-existing
failure. No workflow or job in this repository is named `Hypatia`. The
check-run comes from the **GitHub Advanced Security** app — it is the
code-scanning gate, and it failed because the SARIF upload from #811
contained *1 new alert in code changed by this pull request*:

```
.machine_readable/rsr-profile.a2ml
warning: doc references `src/interface/` but no such directory exists in the tree
         (likely surviving a directory rename)
```

A true positive against a file #811 added. The directory really is
absent (0 tracked paths under `src/interface/`, and the canon has none).
The sentence was declaring *that absence* — "no C-ABI seam and no
formally specified ABI; src/interface/ absent" — but written so it read
as a live path reference. Reworded to state the same fact without the
bare path token.

The alert was **not** dismissed and no scanning configuration was
touched. The text is simply unambiguous now about what it means.

### 2. Three references to a document that does not exist

Checking every path that #811's files point at turned up references to
`docs/binding/04-EXECUTION-PLAN.md`. Neither that file nor
`docs/binding/` exists anywhere in this repository, and never has:

| file | reference |
|---|---|
| `.machine_readable/rsr-profile.a2ml:37` | "see
docs/binding/04-EXECUTION-PLAN.md, ruling R-B" |
| `docs/AFFIRMATION.adoc:54` |
`link:../binding/04-EXECUTION-PLAN.md[...]` |
| `scripts/check-canon-lockstep.sh:98` | "See
docs/binding/04-EXECUTION-PLAN.md." |

These point at a plan that existed only in conversation. A document
citing a non-existent authority is worse than one citing none: it reads
as though the argument has been made somewhere. Repointed at what exists
and actually says it:

- the dialect note -> `docs/AUDIT.adoc` finding **F3**, which records
that the law is authored in a superseded dialect while DEED is itself an
unratified DRAFT
- the ordering rule -> `canon.lock [canon.lockstep]`, where
`spine-adopts-then-canon-releases` is actually stated
- AFFIRMATION's "We intend" list -> `docs/AUDIT.adoc`, which enumerates
the release conditions those actions serve

Every cross-reference in every `.adoc` and `.a2ml` this commit touches
now resolves.

## No lock bump required

`.machine_readable/rsr-profile.a2ml` is not named in `canon.lock
[canon.artifacts]` (criteria, gates, applicability, lifecycle,
constitution), so a comment edit in it is not a canon change and Gate A
assertion 2 stays satisfied.

## Gates

```
Gate A, --canon canon  -> passed 8  failed 0  skipped 2   GATE A PASSED
Gate A, --canon .      -> passed 8  failed 0  skipped 2   GATE A PASSED
check-rsr-profile.sh . -> OK - scaffold matches declared capabilities
check-standards-map.sh -> GATE D PASSED, entry_count = 120
```
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.

2 participants