Skip to content

ci: make the projection fan-out HOLD mechanically answerable - #820

Merged
jason931225 merged 1 commit into
mainfrom
ci/hold-release-conditions
Aug 19, 2026
Merged

ci: make the projection fan-out HOLD mechanically answerable#820
jason931225 merged 1 commit into
mainfrom
ci/hold-release-conditions

Conversation

@jason931225

Copy link
Copy Markdown
Collaborator

What

docs/current/PRODUCT.md:38 holds Company, Person, Employment and PayRun projection fan-out until "each has an explicit owning port and a proven single-writer boundary." This adds one command that decides whether that condition currently holds, and cites the evidence.

MET      Company      (named in hold) owner=console-ontology-canonical-adapter-postgres tables=2 ci=yes
         proof: backend/crates/ontology/canonical-adapter-postgres/tests/company_port_as_runtime_role.rs
...
MET      PayRun       (named in hold) owner=console-payroll-adapter-postgres tables=6 ci=yes
         proof: backend/crates/payroll/adapter-postgres/tests/pay_run_port_as_runtime_role.rs

hold-release-conditions: every leg met for all 6 canonical objects

What this is NOT

It adds no enforcement. Every clause of the hold is already enforced, and enforced well — canonical_contract.rs (owner + owned tables, sized against ObjectKey::ALL), gate_detects_violation.rs (static gate catches a second writer, incl. misspelled evasions, table set derived from the same ALL), and topology.canonical_enforcement (refuses to claim enforcement over zero tables). Duplicating any of that would be the mistake, not the fix.

The actual friction

The release condition is prose; its evidence spans six files whose naming conventions disagree:

layer spelling
registry PayRun
suite on disk pay_run_port_as_runtime_role.rs
CI map payroll-adapter-postgres-pay-run-port-as-runtime-role-pg

Answering the question meant re-deriving three transforms by hand. That derivation is error-prone in a way that matters: a find under the canonical adapter's test dir returns five of six suites and misses PayRun, whose owner is the payroll adapter — which reads as "PayRun is barely covered" when it owns the largest table set of the six and has 17 tests. That wrong turn is why this exists, and it is pinned by a test.

Fails closed — verified by mutation, not asserted

mutation result
un-wire PayRun's suite from the workflow postgres job RED, naming that suite
hold names an object that is not an ObjectKey RED — a hold over a non-existent object can never be evaluated
empty an object's owned-table set RED, "owns no table, so no writer rule applies to it"
delete the hold bullet throws, rather than passing over an empty subject list

The hold's subject is parsed from the bullet rather than hardcoded, so editing the bullet moves the check with it.

Authority

Releasing the hold stays a separate authority decision. The command reports evidence; it does not confer it. The PRODUCT.md edit adds a pointer to the verifier and says so explicitly — it does not change the hold's status.

Also

Fixes the tree walker: readdirSync(withFileTypes) reports a symlinked directory as neither file nor directory, so the naive isDirectory() walk skipped it and under-reported proofs.

Verification

13/13 gate sweep green in the worktree, both reachability scanners green (60 suites; dark 0), executed-tests-baseline green.

Base

Stacked on #818, which carries tools/ci/gate-sweep.json. Registering there rather than in package.json avoids re-introducing an && link that #819 exists to remove.

🤖 Generated with Claude Code

@jason931225
jason931225 force-pushed the fix/lane-fanout-sccache-and-slice branch from fa93203 to 33baaec Compare August 19, 2026 04:49
@jason931225
jason931225 force-pushed the ci/hold-release-conditions branch 2 times, most recently from f78ef75 to 88d29bb Compare August 19, 2026 04:55
Base automatically changed from fix/lane-fanout-sccache-and-slice to main August 19, 2026 05:26
PRODUCT.md holds Company, Person, Employment and PayRun projection
fan-out until "each has an explicit owning port and a proven
single-writer boundary". Every clause of that is already enforced, and
enforced well:

  - canonical_contract.rs proves each ObjectKey names an owner crate and
    at least one table it alone may write, sized against ObjectKey::ALL
    so a seventh key cannot slip through unowned;
  - gate_detects_violation.rs derives its owned-table set from the same
    ALL and proves the static gate catches a second writer, including
    deliberately misspelled evasions;
  - topology.canonical_enforcement refuses at runtime to claim
    enforcement over zero tables.

This adds NO enforcement. Duplicating any of the above would be the
mistake, not the fix.

What was missing is composition. The release condition is prose and its
evidence is spread over six files whose naming conventions disagree: the
registry spells a key PayRun, the suite on disk is
pay_run_port_as_runtime_role.rs, and the CI map calls that same suite
payroll-adapter-postgres-pay-run-port-as-runtime-role-pg. Deciding
whether the hold may lift therefore meant re-deriving three transforms
by hand, and that derivation is genuinely error-prone: a find(1) under
the canonical adapter's test directory returns five of six suites and
misses PayRun entirely, because PayRun's owner is the payroll adapter.
That wrong turn reads as "PayRun is barely covered" when it has the
largest owned-table set of the six and seventeen tests.

So: one command, three transforms applied consistently, evidence cited.
It reports each object's owner, owned tables, the suite proving its
boundary, and whether that suite runs in the workflow PostgreSQL job.

Measured on this tree: every leg met for all six canonical objects.

Fails closed, verified by mutation rather than asserted:

  - un-wiring PayRun's suite from the workflow postgres job
    -> RED, naming that suite;
  - naming an object in the hold that is not an ObjectKey
    -> RED, because a hold over a non-existent object can never be
       evaluated, let alone released;
  - emptying an object's owned-table set
    -> RED ("owns no table, so no writer rule applies to it");
  - deleting the hold bullet -> throws, rather than passing over an
    empty subject list.

The hold's subject is parsed from the bullet, not hardcoded, so editing
the bullet moves the check with it and the prose cannot drift away from
the registry it describes.

Releasing the hold stays a separate authority decision. This reports
evidence; it does not confer it.

Also fixes the tree walker: readdirSync(withFileTypes) reports a
symlinked directory as neither file nor directory, so the naive
isDirectory() walk skipped it and under-reported proofs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jason931225
jason931225 force-pushed the ci/hold-release-conditions branch from 88d29bb to 8f0b523 Compare August 19, 2026 05:34
@jason931225
jason931225 added this pull request to the merge queue Aug 19, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 19, 2026
@jason931225
jason931225 added this pull request to the merge queue Aug 19, 2026
Merged via the queue into main with commit 2849ac4 Aug 19, 2026
32 checks passed
@jason931225
jason931225 deleted the ci/hold-release-conditions branch August 19, 2026 07:15
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.

1 participant