fix(quality): anchor quantified registry populations
ci/woodpecker/pr/ci Pipeline was successful

This commit is contained in:
2026-08-01 12:39:49 -05:00
parent 32b490a712
commit fbb6191298
14 changed files with 594 additions and 37 deletions
+3 -1
View File
@@ -30,7 +30,9 @@ Woodpecker runs `gate-verify` on every pull request and protected-main push with
Provider evidence input is an optional JSON array of normalized pipeline records containing `commit`, globally unique integer pipeline `number`, pipeline `status`, and a `gate-verify` step status. Collection-wide identity/type validation occurs before commit filtering; a duplicate number across two commits fails subject binding. The highest numbered valid rerun for one commit is authoritative. Its retention window is provider-controlled and is not overstated by this repository.
Do not add or update an activation SHA in the manifest. The verifier derives the immutable history boundary from Git as the parent of the first first-parent registry-introduction commit. HEAD, HEAD's parent, and the introduction commit are registered rejected seam candidates.
Do not add or update an activation SHA in the manifest. The verifier derives the audited feature range from Git's merge-base with provider target `refs/remotes/origin/main`, so commits before a delayed registry introduction remain covered. HEAD, HEAD's parent, the introduction commit, and delayed introduction after a gate change are registered rejected constructions.
**DOES:** The merge-base is outside branch-author control when main cannot be rewritten. **DOES NOT:** This bootstrap cannot protect a compromised or rewritten main; Builds 1-2 own that residual. Production verification also requires non-empty criteria/gates/prose/scenario populations and anchors the seven required gate IDs to canonical sources before any “all registered” claim.
PR CI executes current-tree verification only, unprivileged and fail-closed. It does not execute isolated own-tree replay: RM-60 must provide a protected launcher or runner-level rootless sandbox before any PR-controlled executable/configuration is evaluated. Repo-only code cannot safely grant itself the capability intended to contain itself.
@@ -10,7 +10,7 @@ pnpm gate:verify
The registry covers root typecheck, lint, and format checks; RM-01 checkout preflight; the Mosaic CI queue guard; and root Husky pre-commit/pre-push hooks. It does not imply repository-wide coverage. Framework scripts, package-local build/test scripts, templates, and deployment/release scripts remain assigned to RM-54.
Every gate declares exact invocations, observed and required outcomes, criterion bindings, and a single exact inerting mutation. Every must-fail case requires a non-empty reason diagnostic. The verifier rejects a stale, ambiguous, crashing, or ineffective mutation. Fixture and mutation writes reject path traversal and final-component symlinks. Every nested manifest object used by outcomes, mutations, fixtures, deployments, defects, compatibility, provenance, coverage, and merge assertions has closed keys and strict field types; a misspelling cannot silently turn a required comparison into an absent optional field. Independent validation phases collect labeled failures instead of letting one thrown fixture, claim, discovery, deployment, mutation, or compatibility error mask already-known stable-ID diagnostics. This proves detection of the **declared** inerting mutation, not every possible semantic weakening.
Every gate declares exact invocations, an evidence subject equal to its stable gate ID, observed and required outcomes, criterion bindings, and a single exact inerting mutation. Every must-fail case requires a non-empty reason diagnostic. The verifier rejects a stale, ambiguous, crashing, or ineffective mutation. Fixture and mutation writes reject path traversal and final-component symlinks. Every nested manifest object used by outcomes, mutations, fixtures, deployments, defects, compatibility, provenance, coverage, and merge assertions has closed keys and strict field types; a misspelling cannot silently turn a required comparison into an absent optional field. Independent validation phases collect labeled failures instead of letting one thrown fixture, claim, discovery, deployment, mutation, or compatibility error mask already-known stable-ID diagnostics. This proves detection of the **declared** inerting mutation, not every possible semantic weakening.
## Required versus actual
@@ -42,6 +42,10 @@ The replay implementation and abuse-case tests remain fail-closed: when invoked
Retained provider evidence can assert terminal-success **current-tree** records for prior commits when supplied through `GATE_PROVIDER_EVIDENCE_FILE`. Each normalized record contains `commit`, globally unique integer pipeline `number`, pipeline `status`, and exactly one `gate-verify` step; the highest-numbered rerun for a commit is authoritative. The full collection is validated before commit filtering, so one pipeline identity cannot certify two commit subjects. Ambiguous duplicates fail. Absent, expired, or currently-running evidence is reported explicitly and never inferred as success.
The history seam is not a manifest field. `gate-history.mjs` derives it from Git as the parent of the first first-parent commit adding `gates/gates.manifest.json`, which necessarily includes the introduction commit and all later first-parent registry commits. Registered controls reject HEAD, HEAD's parent, and the introduction commit as candidate seams.
The history seam is not a manifest field and is not derived from an author-positioned registry path. `gate-history.mjs` derives the feature range from Git's merge-base with the provider target `refs/remotes/origin/main`; a gate change committed before delayed registry introduction therefore remains in range and fails because its own tree has no registry. Registered controls reject HEAD, HEAD's parent, the introduction commit, and delayed introduction after a gate change.
**DOES:** This bootstrap is sound against a branch author who cannot rewrite main: reordering or splitting commits cannot move the target merge-base. **DOES NOT:** It does not establish integrity if main itself is compromised or rewritten. Builds 1-2 own that residual main-integrity dependency.
Universal checks first prove populations non-empty. The production profile anchors the required seven gate IDs to their canonical sources, while `gateRefs` on the D-38/D-40 criteria must exactly span every registered gate. Population controls mutate evidence-subject and type-strict comparison inputs one gate at a time and require rejection across the complete inventory.
Once RM-60 supplies the external pre-execution anchor, protected post-merge/main replay is detection, not pre-merge prevention. A failed replay requires quarantine of the affected result and revert of the offending merge. It must never be represented as proof that CI blocked that merge. RM-25 tracks provider enforcement.
+6 -4
View File
@@ -38,9 +38,10 @@ Existing deterministic gates can return success without enforcing their stated p
8. `RM02-REQ-08`: Every gate with a deployed counterpart SHALL register source/deployed byte identity and a must-fail drift control. Gates without a deployed counterpart SHALL say so explicitly.
9. `RM02-REQ-09`: CI SHALL run `pnpm gate:verify` on every pull request without path filtering and on protected-main pushes.
10. `RM02-REQ-10` (restated): PR CI SHALL perform unprivileged, fail-closed current-tree verification only. Isolated per-commit replay SHALL remain deferred to RM-60's protected post-merge/main authority, cross-referenced with RM-59. That future replay is detection with a quarantine/revert response, not pre-merge prevention; inability to establish its sandbox is terminal nonzero, never skip/pass. Retained provider evidence SHALL remain distinct and SHALL never be inferred when absent.
11. `RM02-REQ-11`: The history activation boundary SHALL be derived, not manifest-authored, as the parent of the first first-parent commit introducing `gates/gates.manifest.json`. The introduction commit SHALL be included. Author-controlled candidates equal to HEAD, HEAD's parent, or the introduction commit SHALL be rejected by registered must-fail controls.
12. `RM02-REQ-12` (`D-38`): Evidence SHALL be bound to exactly one subject. Provider pipeline identity uniqueness SHALL be validated over the full evidence collection before commit filtering; a duplicate pipeline number across different commits SHALL fail.
13. `RM02-REQ-13` (`D-40`): Every registry object used for discrimination, comparison, mutation, fixture construction, provenance, deployment, or outcome assessment SHALL have a recursively closed, type-strict schema. Unknown or misspelled nested fields SHALL fail rather than becoming absent optional assertions.
11. `RM02-REQ-11`: The audited branch range SHALL begin at the Git merge-base with the provider target `origin/main`, not at a manifest-authored value or author-selected introduction path. A gate-affecting commit before delayed registry introduction SHALL remain in range and fail on its missing own-tree registry. This bootstrap is sound against a branch author who cannot rewrite main; it is not sound against compromise or rewrite of main, whose integrity is the Builds 1-2 dependency.
12. `RM02-REQ-12` (`D-38`): For every gate in the mechanically anchored required inventory, evidence SHALL be bound to exactly that gate subject under review. Provider pipeline shape, gate-step cardinality, and identity uniqueness SHALL be validated over the full evidence collection before commit filtering; a duplicate pipeline number across different commits SHALL fail.
13. `RM02-REQ-13` (`D-40`): For every gate in the mechanically anchored required inventory, each discriminator and comparison input SHALL have a recursively closed, type-strict schema. Unknown, misspelled, wrong-type, or present-but-empty nested assertion fields SHALL fail rather than disabling an assertion.
14. `RM02-REQ-14` (`D-46`): No universally quantified registry check SHALL run until its population is proven non-empty and anchored. The required seven-gate inventory, criteria, prose claims, and compatibility scenarios SHALL reject empty populations before reporting that all registered cases ran.
#### RM02-REQ-10 meaning-change provenance
@@ -57,7 +58,8 @@ Existing deterministic gates can return success without enforcing their stated p
5. `RM02-AC-05`: Unbound or semantically misbound criteria, prose claims bound to unrelated cases, unbound governing prose markers, ownerless behavior deltas, stale mutations, source/deployed drift, and modeled compatibility conflicts each return nonzero with the responsible stable ID. A simultaneous stale fixture or independent phase error SHALL NOT mask responsible stable-ID diagnostics. Registered meta-negative controls move a criterion binding, remove meaning provenance, and redirect a prose claim to an unrelated case; each is observed red for its stated reason.
6. `RM02-AC-06`: CI configuration invokes the verifier unconditionally on every pull request.
7. `RM02-AC-07`: PR output states adjacent `DOES`/`DOES NOT` boundaries: current-tree gates and inerting mutations execute unprivileged and fail-closed; isolated own-tree replay does not execute in repository-controlled PR CI. RM-60/RM-59 are named, retained provider evidence is never inferred, and future protected post-merge detection specifies quarantine/revert rather than claiming pre-merge prevention.
8. `RM02-AC-08`: Registered must-fail controls reject history seam candidates at HEAD, HEAD's parent, and the registry introduction; a cross-commit duplicate provider pipeline identity; a misspelled nested outcome field; a wrong outcome field type; and a present-but-empty outcome pattern. The `RM02-EVIDENCE-SUBJECT-BINDING` and `RM02-TYPE-STRICT-SCHEMA` criteria are bidirectionally bound to their controls.
8. `RM02-AC-08`: Registered must-fail controls reject history seam candidates at HEAD, HEAD's parent, and the registry introduction; delayed registry introduction after an earlier gate change; an emptied registry; a cross-commit duplicate provider pipeline identity; a misspelled nested outcome field; a wrong outcome field type; and a present-but-empty outcome pattern.
9. `RM02-AC-09`: Population controls iterate every gate in the anchored inventory and prove evidence-subject mismatch and wrong-type comparison input are rejected for each gate. `RM02-EVIDENCE-SUBJECT-BINDING`, `RM02-TYPE-STRICT-SCHEMA`, `RM02-HISTORY-BOUNDARY`, and `RM02-NONEMPTY-ANCHORED-QUANTIFICATION` are bidirectionally bound to their must-fail controls.
### Risks, dependencies, and verification boundary
+3 -1
View File
@@ -39,7 +39,7 @@ Deliver the seven-gate registry and RED-first anti-inert verifier on `feat/rm-02
- CI wiring RED: package script and unconditional Woodpecker step tests both failed before wiring.
- History RED: history test failed with missing module before own-tree manifest selection/provider classification was implemented.
- `pnpm gate:verify`: exit 0; seven gates each reported `META-NEGATIVE-CONTROL ... observed red`; queue source/deployed drift control observed red; six queue behavior deltas printed as `DEFECT (owner: RM-03)`.
- Focused Node tests: 48/48 pass after review hardening (31 verifier/wiring plus 17 history/provider tests).
- Focused Node tests: 54/54 pass after third-round hardening (36 verifier/wiring plus 18 history/provider tests).
- `pnpm typecheck`: pass (45/45 Turbo tasks).
- `pnpm lint`: pass (25/25 Turbo tasks).
- `pnpm format:check`: pass.
@@ -68,6 +68,8 @@ The queue guard's `get_state_from_status_json` runs `python3 - <<'PY'` while pro
- Exact-head review at `83d2ecb2` found four silent-defeat paths. Genuine RED-first tests on the pre-fix code proved: author-controlled HEAD/parent/introduction seams produced no boundary failure; cross-commit duplicate pipeline number 7 returned terminal-success; and misspelling `outputPattern` as `outputPatern` in required/actual left canonical verification green. Fixes derive the seam from Git, validate provider identity globally before subject filtering, and recursively close/type-check nested schemas. New registry criteria `RM02-EVIDENCE-SUBJECT-BINDING`, `RM02-TYPE-STRICT-SCHEMA`, and `RM02-HISTORY-BOUNDARY` are bidirectionally bound to eight registered must-fail controls. The broad nested-object typo table and exact derived-boundary test are regression guards added after implementation, not claimed as RED-first. Pre-commit Codex review then found that global evidence failure was only observed—not failed—when HEAD was the sole prospective commit, and that non-object collection entries threw before normalization. Both were reproduced RED-first, then fixed by one collection validator used before iteration and by per-commit assessment. Follow-up review found collection validation still omitted exactly-one-gate-step cardinality for unrelated subjects; a focused test reproduced terminal-success RED-first, and collection validation now rejects that ambiguity globally. Security review then found present-but-empty outcome patterns were truthy-optional assertion bypasses; a reason-specific test reproduced that they lacked the required schema diagnostic, and present pattern fields now require non-whitespace content.
- Exact-head review at `32b490a7` found three population-level blockers. Genuine RED-first tests proved a gate change before author-delayed registry introduction fell outside the range, and empty criteria/gates/prose/scenario populations returned zero. History now anchors at the provider target merge-base; delayed introduction is a registered must-fail control. Production verification requires non-empty populations and a hardcoded seven-gate ID/source inventory before quantified checks. D-38/D-40 criteria now quantify over `gateRefs` exactly spanning every registered gate; each gate declares its evidence subject, and population controls mutate evidence subject and comparison type for every gate. The history record states both bootstrap directions: sound against a branch author unable to rewrite main, not sound against compromised/rewritten main, with residual owned by Builds 1-2. Pre-commit review rejected an initial production CLI `--fixture-profile` test relaxation as a vacuity bypass. That flag was removed; synthetic fixtures now use a non-executable test-support runner, while regression tests prove the shipped CLI rejects the flag and production population checks remain mandatory. Follow-up review then proved deleting `gateRefs` skipped population validation; a RED-first loop reproduced all three deletions, and the three general criterion IDs now require the field before exact-span validation.
## Documentation checklist
- PRD, developer guide, admin guide, governing claim index, sitemap, plan, and scratchpad updated.
+132 -20
View File
@@ -40,6 +40,10 @@
"checkout-preflight/misspelled-outcome-field",
"checkout-preflight/wrong-outcome-field-type",
"checkout-preflight/empty-outcome-pattern",
"checkout-preflight/delayed-registry-introduction",
"checkout-preflight/empty-registry-populations",
"checkout-preflight/all-gates-evidence-subject-bound",
"checkout-preflight/all-gates-type-strict",
"ci-queue-wait/no-status-required",
"ci-queue-wait/unknown-option",
"hook-pre-commit/lint-staged-failure",
@@ -234,37 +238,78 @@
},
{
"id": "RM02-HISTORY-BOUNDARY",
"originalText": "The history activation boundary is derived as the parent of the first first-parent commit that introduces the registry.",
"currentText": "The history activation boundary is derived as the parent of the first first-parent commit that introduces the registry.",
"originalText": "The audited branch range begins at the provider target merge-base, sound against a branch author who cannot rewrite main but not against main compromise; Builds 1-2 own the residual.",
"currentText": "The audited branch range begins at the provider target merge-base, sound against a branch author who cannot rewrite main but not against main compromise; Builds 1-2 own the residual.",
"claimType": "integrity",
"source": "docs/remediation/TASKS.md#d-17",
"meaningChanges": [],
"caseRefs": [
"checkout-preflight/history-seam-head",
"checkout-preflight/history-seam-parent",
"checkout-preflight/history-seam-introduction"
"checkout-preflight/history-seam-introduction",
"checkout-preflight/delayed-registry-introduction"
]
},
{
"id": "RM02-EVIDENCE-SUBJECT-BINDING",
"originalText": "A provider evidence identity cannot establish success for more than one commit subject.",
"currentText": "A provider evidence identity cannot establish success for more than one commit subject.",
"originalText": "For every registered gate, evidence is bound to that gate subject under review; no evidence identity can certify a different or second subject.",
"currentText": "For every registered gate, evidence is bound to that gate subject under review; no evidence identity can certify a different or second subject.",
"claimType": "integrity",
"source": "docs/remediation/TASKS.md#d-38",
"meaningChanges": [],
"caseRefs": ["checkout-preflight/provider-cross-commit-duplicate"]
"caseRefs": [
"checkout-preflight/provider-cross-commit-duplicate",
"checkout-preflight/all-gates-evidence-subject-bound"
],
"gateRefs": [
"quality-typecheck",
"quality-lint",
"quality-format",
"checkout-preflight",
"ci-queue-wait",
"hook-pre-commit",
"hook-pre-push"
]
},
{
"id": "RM02-TYPE-STRICT-SCHEMA",
"originalText": "Every registry discriminator and comparison field is recursively closed and type-strict so a misspelling cannot disable an assertion.",
"currentText": "Every registry discriminator and comparison field is recursively closed and type-strict so a misspelling cannot disable an assertion.",
"originalText": "For every registered gate, each discriminator and comparison input is recursively closed and type-strict so malformed values cannot disable its assertion.",
"currentText": "For every registered gate, each discriminator and comparison input is recursively closed and type-strict so malformed values cannot disable its assertion.",
"claimType": "integrity",
"source": "docs/remediation/TASKS.md#d-40",
"meaningChanges": [],
"caseRefs": [
"checkout-preflight/misspelled-outcome-field",
"checkout-preflight/wrong-outcome-field-type",
"checkout-preflight/empty-outcome-pattern"
"checkout-preflight/empty-outcome-pattern",
"checkout-preflight/all-gates-type-strict"
],
"gateRefs": [
"quality-typecheck",
"quality-lint",
"quality-format",
"checkout-preflight",
"ci-queue-wait",
"hook-pre-commit",
"hook-pre-push"
]
},
{
"id": "RM02-NONEMPTY-ANCHORED-QUANTIFICATION",
"originalText": "No universally quantified registry check runs until its population is proven non-empty and anchored.",
"currentText": "No universally quantified registry check runs until its population is proven non-empty and anchored.",
"claimType": "integrity",
"source": "docs/remediation/TASKS.md#d-46",
"meaningChanges": [],
"caseRefs": ["checkout-preflight/empty-registry-populations"],
"gateRefs": [
"quality-typecheck",
"quality-lint",
"quality-format",
"checkout-preflight",
"ci-queue-wait",
"hook-pre-commit",
"hook-pre-push"
]
}
],
@@ -348,7 +393,7 @@
"mergeAssertions": {
"mode": "unprivileged-current-tree-pr-verification",
"deferredReplayOwner": "RM-60",
"trustDependencies": ["RM-25", "RM-59", "RM-60"],
"trustDependencies": ["RM-25", "RM-59", "RM-60", "Builds 1-2 main-integrity bootstrap"],
"providerEvidence": "assert retained current-tree terminal-success records for prior commits; report absent, expired, or current-running evidence without inference",
"postMergeResponse": "protected isolated replay is detection, not prevention; quarantine and revert on failure"
},
@@ -409,7 +454,8 @@
]
}
}
]
],
"evidenceSubject": "quality-typecheck"
},
{
"id": "quality-lint",
@@ -467,7 +513,8 @@
]
}
}
]
],
"evidenceSubject": "quality-lint"
},
{
"id": "quality-format",
@@ -522,7 +569,8 @@
]
}
}
]
],
"evidenceSubject": "quality-format"
},
{
"id": "checkout-preflight",
@@ -733,7 +781,7 @@
"exitCode": 1,
"outputPattern": "history boundary candidate head rejected"
},
"reasonPattern": "derived activation is parent of registry introduction"
"reasonPattern": "derived activation is provider target merge-base"
},
{
"id": "history-seam-parent",
@@ -748,7 +796,7 @@
"exitCode": 1,
"outputPattern": "history boundary candidate parent rejected"
},
"reasonPattern": "derived activation is parent of registry introduction"
"reasonPattern": "derived activation is provider target merge-base"
},
{
"id": "history-seam-introduction",
@@ -763,7 +811,7 @@
"exitCode": 1,
"outputPattern": "history boundary candidate introduction rejected"
},
"reasonPattern": "derived activation is parent of registry introduction"
"reasonPattern": "derived activation is provider target merge-base"
},
{
"id": "provider-cross-commit-duplicate",
@@ -890,8 +938,69 @@
}
]
}
},
{
"id": "delayed-registry-introduction",
"criterionIds": ["RM02-CHECK-RIGHT", "RM02-HISTORY-BOUNDARY"],
"mustFail": true,
"invocation": ["node", "scripts/gate-delayed-introduction-control.mjs"],
"required": {
"exitCode": 1,
"outputPattern": "delayed registry introduction rejected"
},
"actual": {
"exitCode": 1,
"outputPattern": "delayed registry introduction rejected"
},
"reasonPattern": "own-tree registry cannot be read"
},
{
"id": "empty-registry-populations",
"criterionIds": ["RM02-CHECK-RIGHT", "RM02-NONEMPTY-ANCHORED-QUANTIFICATION"],
"mustFail": true,
"invocation": ["node", "scripts/gate-empty-population-control.mjs"],
"required": {
"exitCode": 1,
"outputPattern": "empty universally quantified registry populations rejected"
},
"actual": {
"exitCode": 1,
"outputPattern": "empty universally quantified registry populations rejected"
},
"reasonPattern": "empty universally quantified registry populations rejected"
},
{
"id": "all-gates-evidence-subject-bound",
"criterionIds": ["RM02-CHECK-RIGHT", "RM02-EVIDENCE-SUBJECT-BINDING"],
"mustFail": true,
"invocation": ["node", "scripts/gate-population-control.mjs", "evidence-subject"],
"required": {
"exitCode": 1,
"outputPattern": "evidence-subject population control rejected every registered gate"
},
"actual": {
"exitCode": 1,
"outputPattern": "evidence-subject population control rejected every registered gate"
},
"reasonPattern": "evidence-subject population control rejected every registered gate"
},
{
"id": "all-gates-type-strict",
"criterionIds": ["RM02-CHECK-RIGHT", "RM02-TYPE-STRICT-SCHEMA"],
"mustFail": true,
"invocation": ["node", "scripts/gate-population-control.mjs", "type-strict"],
"required": {
"exitCode": 1,
"outputPattern": "type-strict population control rejected every registered gate"
},
"actual": {
"exitCode": 1,
"outputPattern": "type-strict population control rejected every registered gate"
},
"reasonPattern": "type-strict population control rejected every registered gate"
}
]
],
"evidenceSubject": "checkout-preflight"
},
{
"id": "ci-queue-wait",
@@ -1267,7 +1376,8 @@
"writeFiles": []
}
}
]
],
"evidenceSubject": "ci-queue-wait"
},
{
"id": "hook-pre-commit",
@@ -1343,7 +1453,8 @@
]
}
}
]
],
"evidenceSubject": "hook-pre-commit"
},
{
"id": "hook-pre-push",
@@ -1419,7 +1530,8 @@
]
}
}
]
],
"evidenceSubject": "hook-pre-push"
}
]
}
@@ -0,0 +1,56 @@
#!/usr/bin/env node
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import { verifyHistory } from './gate-history.mjs';
const root = await mkdtemp(path.join(os.tmpdir(), 'gate-delayed-introduction-'));
function git(...args) {
const result = spawnSync(
'git',
['-c', 'user.name=gate-control', '-c', '[email protected]', ...args],
{ cwd: root, encoding: 'utf8' },
);
if (result.status !== 0) throw new Error(result.stderr || result.stdout);
return result.stdout.trim();
}
try {
git('init', '-q');
await writeFile(path.join(root, 'baseline.txt'), 'baseline\n');
git('add', '.');
git('commit', '-m', 'provider target baseline');
const baseline = git('rev-parse', 'HEAD');
git('update-ref', 'refs/remotes/origin/main', baseline);
await mkdir(path.join(root, 'scripts'), { recursive: true });
await writeFile(path.join(root, 'scripts', 'preflight.mjs'), 'process.exit(0);\n');
git('add', '.');
git('commit', '-m', 'gate change before registry');
const unregisteredCommit = git('rev-parse', 'HEAD');
await mkdir(path.join(root, 'gates'), { recursive: true });
await writeFile(path.join(root, 'gates', 'gates.manifest.json'), '{"schemaVersion":1}\n');
git('add', '.');
git('commit', '-m', 'delayed registry introduction');
const previousBranch = process.env.CI_COMMIT_BRANCH;
process.env.CI_COMMIT_BRANCH = 'feature/delayed-introduction-control';
let result;
try {
result = await verifyHistory({ root, manifest: { schemaVersion: 1 } });
} finally {
if (previousBranch === undefined) delete process.env.CI_COMMIT_BRANCH;
else process.env.CI_COMMIT_BRANCH = previousBranch;
}
const detail = result.failures.join('\n');
if (detail.includes(unregisteredCommit) && /own-tree registry cannot be read/i.test(detail)) {
process.stderr.write(`delayed registry introduction rejected: ${detail}\n`);
process.exitCode = 1;
} else {
process.stdout.write('delayed registry introduction was not rejected\n');
}
} finally {
await rm(root, { recursive: true, force: true });
}
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env node
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { verifyRegistry } from './gate-verify.mjs';
const root = process.cwd();
const manifest = JSON.parse(await readFile(path.join(root, 'gates/gates.manifest.json'), 'utf8'));
manifest.gateRoots = ['.mosaic-empty-gate-root'];
manifest.criteria = [];
manifest.gates = [];
manifest.proseClaims = [];
manifest.compatibilityScenarios = [];
const directory = await mkdtemp(path.join(os.tmpdir(), 'gate-empty-population-'));
try {
const manifestPath = path.join(directory, 'manifest.json');
await writeFile(manifestPath, `${JSON.stringify(manifest)}\n`);
const result = await verifyRegistry({
root,
manifest: manifestPath,
skipHistory: true,
structureOnly: true,
fixtureProfile: false,
});
const required = ['criteria', 'gates', 'proseClaims', 'compatibilityScenarios'];
const missing = required.filter(
(population) =>
!result.failures.some((failure) =>
failure.includes(`${population} population must be non-empty and anchored`),
),
);
if (missing.length > 0) {
process.stdout.write(`empty populations were not rejected: ${missing.join(', ')}\n`);
process.exitCode = 0;
} else {
process.stderr.write(
'empty universally quantified registry populations rejected before evaluation\n',
);
process.exitCode = 1;
}
} finally {
await rm(directory, { recursive: true, force: true });
}
+1 -1
View File
@@ -35,6 +35,6 @@ if (candidate === boundary.activationCommit) {
process.exit(0);
}
process.stderr.write(
`history boundary candidate ${candidateKind} rejected: derived activation is parent of registry introduction\n`,
`history boundary candidate ${candidateKind} rejected: derived activation is provider target merge-base\n`,
);
process.exit(1);
+26 -4
View File
@@ -23,6 +23,11 @@ export async function listProspectiveCommits(root, activationCommit, head = 'HEA
}
export function deriveHistoryBoundary(root, head = 'HEAD') {
const targetRef = 'refs/remotes/origin/main';
const target = git(root, ['rev-parse', '--verify', targetRef], { allowFailure: true });
if (target.status !== 0 || !target.stdout.trim()) {
throw new Error(`history boundary cannot be derived: provider target ${targetRef} is absent`);
}
const introductions = git(root, [
'log',
'--first-parent',
@@ -40,13 +45,23 @@ export function deriveHistoryBoundary(root, head = 'HEAD') {
throw new Error('history boundary cannot be derived: registry introduction is absent');
}
const introductionCommit = introductions[0];
const parent = git(root, ['rev-parse', `${introductionCommit}^`], { allowFailure: true });
if (parent.status !== 0 || !parent.stdout.trim()) {
const headOnTarget = git(root, ['merge-base', '--is-ancestor', head, targetRef], {
allowFailure: true,
});
const activation =
headOnTarget.status === 0
? git(root, ['rev-parse', `${head}^`], { allowFailure: true })
: git(root, ['merge-base', head, targetRef], { allowFailure: true });
if (activation.status !== 0 || !activation.stdout.trim()) {
throw new Error(
`history boundary cannot be derived: registry introduction ${introductionCommit} has no parent`,
`history boundary cannot be derived: provider target merge-base for ${head} is unavailable`,
);
}
return { activationCommit: parent.stdout.trim(), introductionCommit };
return {
activationCommit: activation.stdout.trim(),
introductionCommit,
targetRef,
};
}
export async function readManifestAtCommit(root, commit) {
@@ -342,6 +357,13 @@ export async function verifyHistory({ root, manifest }) {
return { failures, observations };
}
const onMain = isMainCommit(root, head);
// RM-02 history bootstrap boundary (Builds 1-2), kept adjacent in both directions:
// DOES: anchor feature history to the provider target merge-base, sound against an author who
// cannot rewrite main.
// DOES NOT: establish integrity when main itself is compromised; Builds 1-2 own that residual.
observations.push(
`RM-02 HISTORY BOOTSTRAP BOUNDARY ${head}: DOES: anchor the audited range to provider target ${boundary.targetRef} at merge-base ${boundary.activationCommit}, sound against a branch author who cannot rewrite main; DOES NOT: protect against compromise or rewrite of main; residual owner Builds 1-2`,
);
// RM-02 execution boundary (RM-60, cross-reference RM-59), kept adjacent in both directions:
// DOES: run every registered current-tree gate and declared inerting mutation on PR CI,
// unprivileged and fail-closed.
+46
View File
@@ -289,6 +289,13 @@ test('PR verification states the RM-60 boundary without executing an intermediat
manifest: { schemaVersion: 1 },
});
assert.deepEqual(result.failures, []);
assert.ok(
result.observations.some((observation) =>
/HISTORY BOOTSTRAP BOUNDARY.*DOES:.*provider target.*sound.*cannot rewrite main.*DOES NOT:.*compromise.*main.*Builds 1-2/i.test(
observation,
),
),
);
assert.ok(
result.observations.some((observation) =>
/DOES:.*current tree.*DOES NOT:.*isolated.*RM-60.*RM-59/i.test(observation),
@@ -306,6 +313,42 @@ test('PR verification states the RM-60 boundary without executing an intermediat
}
});
test('target merge-base includes gate changes committed before registry introduction', async () => {
const root = `${fixtureRoot}-delayed-introduction`;
await rm(root, { recursive: true, force: true });
await mkdir(root, { recursive: true });
git(root, 'init', '-q');
git(root, 'config', 'user.name', 'gate-test');
git(root, 'config', 'user.email', '[email protected]');
await writeFile(path.join(root, 'baseline.txt'), 'baseline\n');
git(root, 'add', '.');
git(root, 'commit', '-m', 'target baseline');
const baseline = git(root, 'rev-parse', 'HEAD');
git(root, 'update-ref', 'refs/remotes/origin/main', baseline);
await mkdir(path.join(root, 'scripts'), { recursive: true });
await writeFile(path.join(root, 'scripts', 'preflight.mjs'), 'process.exit(0);\n');
git(root, 'add', '.');
git(root, 'commit', '-m', 'gate change before registry');
const preRegistryGateChange = git(root, 'rev-parse', 'HEAD');
await mkdir(path.join(root, 'gates'), { recursive: true });
await writeFile(path.join(root, 'gates', 'gates.manifest.json'), '{"schemaVersion":1}\n');
git(root, 'add', '.');
git(root, 'commit', '-m', 'delayed registry introduction');
const previousBranch = process.env.CI_COMMIT_BRANCH;
process.env.CI_COMMIT_BRANCH = 'feature/delayed-introduction';
try {
const result = await verifyHistory({ root, manifest: { schemaVersion: 1 } });
assert.match(
result.failures.join('\n'),
new RegExp(`${preRegistryGateChange}.*own-tree registry cannot be read`, 'i'),
);
} finally {
if (previousBranch === undefined) delete process.env.CI_COMMIT_BRANCH;
else process.env.CI_COMMIT_BRANCH = previousBranch;
}
});
test('derived history boundary includes the registry-introduction commit', async () => {
const root = `${fixtureRoot}-derived-boundary`;
await rm(root, { recursive: true, force: true });
@@ -317,6 +360,7 @@ test('derived history boundary includes the registry-introduction commit', async
git(root, 'add', '.');
git(root, 'commit', '-m', 'baseline');
const baseline = git(root, 'rev-parse', 'HEAD');
git(root, 'update-ref', 'refs/remotes/origin/main', baseline);
await mkdir(path.join(root, 'gates'), { recursive: true });
await writeFile(path.join(root, 'gates', 'gates.manifest.json'), '{"schemaVersion":1}\n');
git(root, 'add', '.');
@@ -330,6 +374,7 @@ test('derived history boundary includes the registry-introduction commit', async
assert.deepEqual(deriveHistoryBoundary(root, head), {
activationCommit: baseline,
introductionCommit: introduction,
targetRef: 'refs/remotes/origin/main',
});
assert.deepEqual(await listProspectiveCommits(root, baseline, head), [introduction, head]);
});
@@ -380,6 +425,7 @@ test('globally invalid provider evidence fails when HEAD is the only prospective
await writeFile(path.join(root, 'baseline.txt'), 'baseline\n');
git(root, 'add', '.');
git(root, 'commit', '-m', 'baseline');
git(root, 'update-ref', 'refs/remotes/origin/main', git(root, 'rev-parse', 'HEAD'));
await mkdir(path.join(root, 'gates'), { recursive: true });
await writeFile(path.join(root, 'gates', 'gates.manifest.json'), '{"schemaVersion":1}\n');
git(root, 'add', '.');
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env node
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { verifyRegistry } from './gate-verify.mjs';
const mode = process.argv[2];
const root = process.cwd();
const source = JSON.parse(await readFile(path.join(root, 'gates/gates.manifest.json'), 'utf8'));
const expectedGateIds = source.gates.map((gate) => gate.id);
if (expectedGateIds.length === 0) {
process.stderr.write('gate population control requires a non-empty anchored inventory\n');
process.exit(2);
}
async function rejectedForEveryGate(mutate, diagnostic) {
for (const gateId of expectedGateIds) {
const manifest = structuredClone(source);
const gate = manifest.gates.find((candidate) => candidate.id === gateId);
mutate(gate);
const directory = await mkdtemp(path.join(os.tmpdir(), 'gate-population-control-'));
const manifestPath = path.join(directory, 'manifest.json');
try {
await writeFile(manifestPath, `${JSON.stringify(manifest)}\n`);
const result = await verifyRegistry({
root,
manifest: manifestPath,
skipHistory: true,
structureOnly: true,
fixtureProfile: false,
});
if (!result.failures.some((failure) => diagnostic(failure, gateId))) return false;
} finally {
await rm(directory, { recursive: true, force: true });
}
}
return true;
}
let rejected;
if (mode === 'evidence-subject') {
rejected = await rejectedForEveryGate(
(gate) => {
gate.evidenceSubject = 'different-gate-subject';
},
(failure, gateId) =>
failure.includes(`gate ${gateId}: evidence subject`) &&
failure.includes('does not match gate id'),
);
} else if (mode === 'type-strict') {
rejected = await rejectedForEveryGate(
(gate) => {
gate.cases[0].actual.exitCode = '0';
},
(failure, gateId) =>
failure.includes(
`${gateId}/${source.gates.find((gate) => gate.id === gateId).cases[0].id}.actual.exitCode`,
) && failure.includes('expected an integer'),
);
} else {
process.stderr.write(`unknown gate population control ${String(mode)}\n`);
process.exit(2);
}
if (!rejected) {
process.stdout.write(`${mode} population control did not reject every registered gate\n`);
process.exit(0);
}
process.stderr.write(`${mode} population control rejected every registered gate\n`);
process.exit(1);
+65 -4
View File
@@ -23,6 +23,20 @@ import { spawnSync } from 'node:child_process';
import { verifyHistory } from './gate-history.mjs';
const COPY_SKIP = new Set(['.git', '.mosaic-test-work', '.next', '.turbo', 'coverage', 'dist']);
const POPULATION_CRITERION_IDS = new Set([
'RM02-EVIDENCE-SUBJECT-BINDING',
'RM02-TYPE-STRICT-SCHEMA',
'RM02-NONEMPTY-ANCHORED-QUANTIFICATION',
]);
const REQUIRED_GATE_INVENTORY = new Map([
['quality-typecheck', 'package.json'],
['quality-lint', 'package.json'],
['quality-format', 'package.json'],
['checkout-preflight', 'scripts/preflight.mjs'],
['ci-queue-wait', 'packages/mosaic/framework/tools/git/ci-queue-wait.sh'],
['hook-pre-commit', '.husky/pre-commit'],
['hook-pre-push', '.husky/pre-push'],
]);
function parseArgs(argv) {
const options = {
@@ -382,7 +396,14 @@ function validateEnvironment(environment, label, failures) {
}
}
function validateClosedSchema(manifest, failures) {
function validateClosedSchema(manifest, failures, { fixtureProfile = false } = {}) {
if (!fixtureProfile) {
for (const population of ['criteria', 'gates', 'proseClaims', 'compatibilityScenarios']) {
if (!Array.isArray(manifest[population]) || manifest[population].length === 0) {
failures.push(`${population} population must be non-empty and anchored before evaluation`);
}
}
}
if (manifest.schemaVersion !== 1)
failures.push(`unsupported schemaVersion ${String(manifest.schemaVersion)}`);
rejectUnknownKeys(
@@ -450,6 +471,7 @@ function validateClosedSchema(manifest, failures) {
'source',
'meaningChanges',
'caseRefs',
'gateRefs',
]),
`criterion ${criterion.id}`,
failures,
@@ -475,6 +497,12 @@ function validateClosedSchema(manifest, failures) {
);
}
}
if (POPULATION_CRITERION_IDS.has(criterion.id) && criterion.gateRefs === undefined) {
failures.push(`${criterion.id}: gateRefs population binding is required`);
}
if (criterion.gateRefs !== undefined) {
validateStringArray(criterion.gateRefs, `criterion ${criterion.id}.gateRefs`, failures);
}
if (!Array.isArray(criterion.caseRefs) || criterion.caseRefs.length === 0) {
failures.push(`${criterion.id}: no declared exercising cases`);
} else {
@@ -535,6 +563,16 @@ function validateClosedSchema(manifest, failures) {
validateFixture(scenario.fixture, `${scenario.id}.fixture`, failures);
}
rejectDuplicateIds(manifest.gates, 'gate', failures);
if (!fixtureProfile) {
for (const [requiredId, requiredSource] of REQUIRED_GATE_INVENTORY) {
const registered = (manifest.gates ?? []).find((gate) => gate?.id === requiredId);
if (!registered || registered.source !== requiredSource) {
failures.push(
`gates population is not anchored: required ${requiredId} at ${requiredSource}`,
);
}
}
}
for (const gate of manifest.gates ?? []) {
rejectUnknownKeys(
gate,
@@ -546,12 +584,19 @@ function validateClosedSchema(manifest, failures) {
'inertMutation',
'cases',
'discoveryAliases',
'evidenceSubject',
]),
`gate ${gate.id}`,
failures,
);
requireString(gate.id, `gate ${gate.id}.id`, failures);
requireString(gate.source, `gate ${gate.id}.source`, failures);
requireString(gate.evidenceSubject, `gate ${gate.id}.evidenceSubject`, failures);
if (gate.evidenceSubject !== gate.id) {
failures.push(
`gate ${gate.id}: evidence subject ${String(gate.evidenceSubject)} does not match gate id`,
);
}
rejectDuplicateIds(gate.cases, `case in gate ${gate.id}`, failures);
if (!Array.isArray(gate.invocation) || gate.invocation.length === 0) {
failures.push(`${gate.id}: exact invocation is missing`);
@@ -657,8 +702,8 @@ function validateClosedSchema(manifest, failures) {
}
}
function validateStructure(manifest, failures) {
validateClosedSchema(manifest, failures);
function validateStructure(manifest, failures, options = {}) {
validateClosedSchema(manifest, failures, options);
const criteria = new Map((manifest.criteria ?? []).map((criterion) => [criterion.id, criterion]));
const boundCriteria = new Set();
const negativeBoundCriteria = new Set();
@@ -686,6 +731,22 @@ function validateStructure(manifest, failures) {
}
}
const registeredGateIds = new Set((manifest.gates ?? []).map((gate) => gate.id));
for (const criterion of criteria.values()) {
if (criterion.gateRefs === undefined) continue;
const referenced = new Set(criterion.gateRefs);
for (const gateId of registeredGateIds) {
if (!referenced.has(gateId)) {
failures.push(`${criterion.id}: gate population binding is missing ${gateId}`);
}
}
for (const gateId of referenced) {
if (!registeredGateIds.has(gateId)) {
failures.push(`${criterion.id}: gate population binding references unknown gate ${gateId}`);
}
}
}
for (const claim of manifest.proseClaims ?? []) {
if (!criteria.has(claim.criterionId)) {
failures.push(`GATE-CLAIM:${claim.id} references unknown criterion ${claim.criterionId}`);
@@ -996,7 +1057,7 @@ export async function verifyRegistry(options) {
const manifestPath = path.resolve(options.root, options.manifest);
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
validateStructure(manifest, failures);
validateStructure(manifest, failures, options);
if (options.structureOnly) return { failures, manifest, observations };
async function collectPhaseFailure(label, action) {
+105
View File
@@ -6,6 +6,12 @@ import { spawnSync } from 'node:child_process';
import test from 'node:test';
const verifier = path.join(process.cwd(), 'scripts', 'gate-verify.mjs');
const fixtureRunner = path.join(
process.cwd(),
'scripts',
'test-support',
'gate-verify-fixture-runner.mjs',
);
const fixtureBase = path.join(process.cwd(), '.mosaic-test-work', `gate-verify-${process.pid}`);
async function fixture(name = 'case') {
@@ -38,6 +44,7 @@ function baseManifest() {
{
id: 'meta-fixture',
source: 'gates/meta-fixture.sh',
evidenceSubject: 'meta-fixture',
invocation: ['gates/meta-fixture.sh'],
deployment: { kind: 'none', reason: 'test fixture only' },
inertMutation: {
@@ -73,6 +80,14 @@ async function writeManifest(root, manifest) {
}
function verify(root, extraArgs = []) {
return spawnSync(
process.execPath,
[fixtureRunner, '--root', root, '--manifest', 'gates/gates.manifest.json', ...extraArgs],
{ cwd: root, encoding: 'utf8', env: { ...process.env, HOME: os.homedir() } },
);
}
function verifyProductionStructure(root, extraArgs = []) {
return spawnSync(
process.execPath,
[
@@ -82,6 +97,7 @@ function verify(root, extraArgs = []) {
'--manifest',
'gates/gates.manifest.json',
'--skip-history',
'--structure-only',
...extraArgs,
],
{ cwd: root, encoding: 'utf8', env: { ...process.env, HOME: os.homedir() } },
@@ -96,6 +112,95 @@ test.after(async () => {
await rm(fixtureBase, { recursive: true, force: true });
});
test('universally quantified registry checks reject empty populations before evaluation', async () => {
const root = await fixture('empty-registry-populations');
await mkdir(path.join(root, 'empty-gate-root'), { recursive: true });
const manifest = baseManifest();
manifest.gateRoots = ['empty-gate-root'];
manifest.criteria = [];
manifest.proseClaims = [];
manifest.compatibilityScenarios = [];
manifest.gates = [];
await writeManifest(root, manifest);
const result = verifyProductionStructure(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /criteria population.*non-empty.*anchored/i);
assert.match(output(result), /gates population.*non-empty.*anchored/i);
assert.match(output(result), /proseClaims population.*non-empty.*anchored/i);
assert.match(output(result), /compatibilityScenarios population.*non-empty.*anchored/i);
});
test('production verifier exposes no fixture-profile population bypass', async () => {
const root = await fixture('no-production-fixture-profile');
const result = verifyProductionStructure(root, ['--fixture-profile']);
assert.notEqual(result.status, 0);
assert.match(output(result), /unknown option: --fixture-profile/i);
});
test('anchored gate inventory and population criteria cannot shrink together', async () => {
const source = JSON.parse(
await readFile(path.join(process.cwd(), 'gates', 'gates.manifest.json'), 'utf8'),
);
const root = await fixture('shrunken-gate-population');
source.gates = source.gates.filter((gate) => gate.id !== 'hook-pre-push');
for (const criterion of source.criteria) {
if (criterion.gateRefs) {
criterion.gateRefs = criterion.gateRefs.filter((gateId) => gateId !== 'hook-pre-push');
}
criterion.caseRefs = criterion.caseRefs.filter(
(caseRef) => !caseRef.startsWith('hook-pre-push/'),
);
}
await writeManifest(root, source);
const result = verifyProductionStructure(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /gates population is not anchored.*hook-pre-push/i);
});
test('general population criteria cannot delete their gateRefs binding', async () => {
const requiredCriteria = [
'RM02-EVIDENCE-SUBJECT-BINDING',
'RM02-TYPE-STRICT-SCHEMA',
'RM02-NONEMPTY-ANCHORED-QUANTIFICATION',
];
for (const criterionId of requiredCriteria) {
const source = JSON.parse(
await readFile(path.join(process.cwd(), 'gates', 'gates.manifest.json'), 'utf8'),
);
const root = await fixture(`missing-gate-refs-${criterionId}`);
delete source.criteria.find((criterion) => criterion.id === criterionId).gateRefs;
await writeManifest(root, source);
const result = verifyProductionStructure(root);
assert.notEqual(result.status, 0);
assert.match(
output(result),
new RegExp(`${criterionId}.*gateRefs.*required`, 'i'),
criterionId,
);
}
});
test('general population criteria must span every registered gate', async () => {
const source = JSON.parse(
await readFile(path.join(process.cwd(), 'gates', 'gates.manifest.json'), 'utf8'),
);
const root = await fixture('incomplete-gate-refs');
source.criteria.find((criterion) => criterion.id === 'RM02-EVIDENCE-SUBJECT-BINDING').gateRefs =
source.criteria
.find((criterion) => criterion.id === 'RM02-EVIDENCE-SUBJECT-BINDING')
.gateRefs.filter((gateId) => gateId !== 'quality-lint');
await writeManifest(root, source);
const result = verifyProductionStructure(root);
assert.notEqual(result.status, 0);
assert.match(
output(result),
/RM02-EVIDENCE-SUBJECT-BINDING.*gate population binding is missing quality-lint/i,
);
});
test('an externally inerted failure branch makes verification nonzero and names the gate', async () => {
const root = await fixture('external-inert');
await writeGate(root, '#!/bin/sh\nexit 0\n');
@@ -0,0 +1,28 @@
#!/usr/bin/env node
import path from 'node:path';
import { verifyRegistry } from '../gate-verify.mjs';
let root;
let manifest = 'gates/gates.manifest.json';
let structureOnly = false;
for (let index = 0; index < process.argv.slice(2).length; index += 1) {
const args = process.argv.slice(2);
const value = args[index];
if (value === '--root') root = path.resolve(args[++index]);
else if (value === '--manifest') manifest = args[++index];
else if (value === '--structure-only') structureOnly = true;
else throw new Error(`unknown fixture-runner option: ${value}`);
}
if (!root) throw new Error('fixture runner requires --root');
const { failures, observations } = await verifyRegistry({
root,
manifest,
skipHistory: true,
structureOnly,
fixtureProfile: true,
});
for (const observation of observations) process.stdout.write(`${observation}\n`);
for (const failure of failures) process.stderr.write(`GATE VERIFY FAILED: ${failure}\n`);
if (failures.length > 0) process.exitCode = 1;