harness-everything 0.38.0 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/AGENTS.md +79 -63
- package/CHANGELOG.md +32 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/README.md +76 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/a-reviews-b.md +191 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/b-reviews-a.md +100 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/baseline-probes.log +47 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/bom-red.log +85 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/bom-transition.txt +25 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/coordinator-probes.txt +84 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/cross-suite.txt +18 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/final-bom.log +3 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/final-consistency.log.gz +0 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/final-core.log +198 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/final-probes.log +27 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/final-producer.log +83 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/final-review.md +89 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/final-transitions.log +5 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/implementation-a.md +166 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/implementation-b.md +264 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/logs.sha256 +15 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/peer-a-probe.log +62 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/peer-a-probe.txt +313 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/peer-b-probe.log +51 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/peer-b-probe.txt +94 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/semantic-transitions.txt +25 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/source.sha256 +9 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/tests-a-on-b-core.log +201 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/tests-a-on-b-producer.log +82 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/tests-b-on-a-core.log +197 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/tests-b-on-a-producer.log +82 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/verify-all.log.gz +0 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/verify-gate-summary.json +22 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2a/harness.js +489 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2a/vectors.json +617 -0
- package/ci/mechanism-325-3b-s2a-core.test.js +840 -0
- package/ci/mechanism-325-3b-s2a-producer.test.js +564 -0
- package/code-review/SKILL.md +3 -3
- package/code-review/references/review-playbook.md +4 -0
- package/code-review/templates/finding.template.md +6 -0
- package/docs/doc-workflow-reconciliation.md +357 -0
- package/evals/zoom-out/tasks/negative-first-edge-case-review.yaml +16 -0
- package/evals/zoom-out/tasks/positive-repair-loop.yaml +15 -0
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/scripts/project-docs-resolver.js +42 -8
- package/harness-everything/scripts/routing-keywords.json +4 -0
- package/hooks/scripts/lib/doc-reconciliation-core.js +280 -0
- package/hooks/scripts/lib/doc-reconciliation-events.js +141 -0
- package/hooks/scripts/lib/doc-reconciliation-producer.js +206 -0
- package/multi-agent-workspace/SKILL.md +1 -1
- package/multi-agent-workspace/scripts/project-docs-resolver.js +42 -8
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +2 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-core.js +280 -0
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-events.js +141 -0
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-producer.js +206 -0
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/code-review/SKILL.md +3 -3
- package/plugins/harness-everything/skills/code-review/references/review-playbook.md +4 -0
- package/plugins/harness-everything/skills/code-review/templates/finding.template.md +6 -0
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/project-docs-resolver.js +42 -8
- package/plugins/harness-everything/skills/harness-everything/scripts/routing-keywords.json +4 -0
- package/plugins/harness-everything/skills/multi-agent-workspace/SKILL.md +1 -1
- package/plugins/harness-everything/skills/multi-agent-workspace/scripts/project-docs-resolver.js +42 -8
- package/plugins/harness-everything/skills/to-spec/SKILL.md +1 -1
- package/plugins/harness-everything/skills/to-spec/scripts/project-docs-resolver.js +42 -8
- package/plugins/harness-everything/skills/zoom-out/SKILL.md +4 -3
- package/plugins/harness-everything/skills/zoom-out/references/circuit-breaker.md +7 -0
- package/plugins/harness-everything/skills/zoom-out/templates/zoom-out-report.template.md +4 -2
- package/sbom.cdx.json +7 -7
- package/scripts/errors-sync.js +90 -0
- package/to-spec/SKILL.md +1 -1
- package/to-spec/scripts/project-docs-resolver.js +42 -8
- package/zoom-out/SKILL.md +4 -3
- package/zoom-out/references/circuit-breaker.md +7 -0
- package/zoom-out/templates/zoom-out-report.template.md +4 -2
|
@@ -0,0 +1,840 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// #325 Phase 3b S2a RED suite: producer snapshot -> accepted events -> expected-set reconciliation core.
|
|
5
|
+
//
|
|
6
|
+
// Every assertion runs on both legs (canonical hooks/scripts/lib and the packaged copy). Missing modules and missing
|
|
7
|
+
// exports are FAIL with a module-missing or export-missing reason; the harness never crashes. Positive vectors and
|
|
8
|
+
// single-field flips use explicitly fake origin providers (test fixtures, not production attestation). Expected MUST
|
|
9
|
+
// sets and regression pins come from the existing unchanged derive/checker APIs; every other expectation is hand-derived
|
|
10
|
+
// from docs/doc-workflow-reconciliation.md (S2a). INFRA (fixture repository or harness faults) is distinct from FAIL.
|
|
11
|
+
// Exit 1 means at least one assertion FAILED; exit 2 means an infrastructure fault.
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
const childProcess = require('child_process');
|
|
17
|
+
const net = require('net');
|
|
18
|
+
const H = require('./fixtures/doc-obligations/reconciliation-s2a/harness.js');
|
|
19
|
+
|
|
20
|
+
const REQ_PRODUCER = { where: 'lib', file: 'doc-reconciliation-producer.js', exports: ['produceDocSnapshot'] };
|
|
21
|
+
const REQ_EVENTS = { where: 'lib', file: 'doc-reconciliation-events.js', exports: ['acceptDocEvents'] };
|
|
22
|
+
const REQ_CORE = { where: 'lib', file: 'doc-reconciliation-core.js', exports: ['checkAcceptedDocEvidence', 'reconcileDocResults'] };
|
|
23
|
+
const REQ_DERIVE = { where: 'scripts', file: 'doc-obligations.js', exports: ['deriveDocFacts', 'deriveDocObligations'] };
|
|
24
|
+
const REQ_CHECKER = { where: 'scripts', file: 'doc-evidence.js', exports: ['checkDocObligations'] };
|
|
25
|
+
const PRODUCER = 'doc-reconciliation-producer.js';
|
|
26
|
+
const EVENTS = 'doc-reconciliation-events.js';
|
|
27
|
+
const CORE = 'doc-reconciliation-core.js';
|
|
28
|
+
const DERIVE = 'doc-obligations.js';
|
|
29
|
+
const CHECKER = 'doc-evidence.js';
|
|
30
|
+
const ALL_NEW = [REQ_PRODUCER, REQ_EVENTS, REQ_CORE];
|
|
31
|
+
const { VECTORS, PRINCIPALS, ZERO_DIGEST } = H;
|
|
32
|
+
|
|
33
|
+
const runner = H.createRunner('S2a core');
|
|
34
|
+
const sorted = list => [...list].sort();
|
|
35
|
+
|
|
36
|
+
// Runs the complete positive flow for one vector in a fresh repository: produce -> accept -> decide.
|
|
37
|
+
function withVector(key, fn, options = {}) {
|
|
38
|
+
const files = H.vectorFiles(key);
|
|
39
|
+
const repo = H.makeRepo(files.base);
|
|
40
|
+
try {
|
|
41
|
+
H.writeFiles(repo.root, files.candidate);
|
|
42
|
+
fn(repo);
|
|
43
|
+
} finally {
|
|
44
|
+
H.removeRepo(repo.root);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function flow(mods, repo, key, options = {}) {
|
|
49
|
+
const P = mods[PRODUCER];
|
|
50
|
+
const E = mods[EVENTS];
|
|
51
|
+
const C = mods[CORE];
|
|
52
|
+
const vec = VECTORS.vectors[key];
|
|
53
|
+
const origin = new H.FakeOrigin({ context: H.fakeContext(options.context, repo), mode: options.mode || 'accept' });
|
|
54
|
+
const produced = P.produceDocSnapshot(H.producerInput(repo, options.producer), { originProvider: origin });
|
|
55
|
+
if (produced.state !== 'ready' || !produced.snapshot) {
|
|
56
|
+
return { produced, origin, codes: H.codesOf(produced) };
|
|
57
|
+
}
|
|
58
|
+
const snapshot = produced.snapshot;
|
|
59
|
+
const events = options.events || H.buildEvents(snapshot, repo, vec);
|
|
60
|
+
for (const [ref, event] of Object.entries(events)) {
|
|
61
|
+
if (!event.__notIssued) origin.issueEvent(ref, event);
|
|
62
|
+
}
|
|
63
|
+
const eventRefs = Object.keys(events);
|
|
64
|
+
const accepted = E.acceptDocEvents({ snapshot, eventRefs }, { originProvider: origin });
|
|
65
|
+
const decision = C.checkAcceptedDocEvidence({ snapshot, accepted, mode: options.mode === 'review' ? 'review' : 'verification' });
|
|
66
|
+
return { produced, origin, snapshot, events, accepted, decision, codes: H.codesOf(produced, accepted, decision) };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function requireEligible(result, label) {
|
|
70
|
+
if (!result.decision) throw new Error(`${label}: no decision (producer ${result.produced.state}: ${result.codes.join(',') || 'no codes'})`);
|
|
71
|
+
if (result.decision.decision !== 'eligible' || result.decision.eligible !== true) {
|
|
72
|
+
throw new Error(`${label}: decision ${result.decision.decision} with codes ${JSON.stringify(result.codes)}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---- A01: complete positive vectors and the review-not-forced control --------------------------------------------
|
|
77
|
+
|
|
78
|
+
runner.run('A01:normative-complete-vector-eligible-with-exact-expected-set', ALL_NEW, mods => withVector('normative', repo => {
|
|
79
|
+
const vec = VECTORS.vectors.normative;
|
|
80
|
+
const r = flow(mods, repo, 'normative');
|
|
81
|
+
requireEligible(r, 'normative vector');
|
|
82
|
+
H.sameStable(sorted(r.decision.expectedMustIds), sorted(vec.expectedMustIds), 'expectedMustIds (derived by the unchanged derive API)');
|
|
83
|
+
H.sameStable(sorted(r.decision.checkedMustIds), sorted(vec.expectedMustIds), 'checkedMustIds (verification mode checks the full set)');
|
|
84
|
+
H.sameStable(r.decision.unsatisfied || [], [], 'unsatisfied');
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
runner.run('A01:normative-checker-results-unchanged-from-direct-checker-call', [...ALL_NEW, REQ_CHECKER], (mods) => withVector('normative', repo => {
|
|
88
|
+
const r = flow(mods, repo, 'normative');
|
|
89
|
+
requireEligible(r, 'normative vector');
|
|
90
|
+
const direct = mods[CHECKER].checkDocObligations(r.accepted.checkerInputs);
|
|
91
|
+
H.sameStable(r.decision.results, direct.results, 'results retained unchanged from checkDocObligations(checkerInputs)');
|
|
92
|
+
}));
|
|
93
|
+
|
|
94
|
+
runner.run('A01:nonnormative-positive-is-eligible-and-review-not-forced', ALL_NEW, mods => withVector('nonnormative', repo => {
|
|
95
|
+
const vec = VECTORS.vectors.nonnormative;
|
|
96
|
+
const r = flow(mods, repo, 'nonnormative');
|
|
97
|
+
requireEligible(r, 'nonnormative vector');
|
|
98
|
+
H.sameStable(sorted(r.decision.expectedMustIds), sorted(vec.expectedMustIds), 'expectedMustIds without quality-review or conflict-check');
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
// ---- A02: task substitution, narrowed scope, changed requirements/bindings and workflow revision -----------------
|
|
102
|
+
|
|
103
|
+
runner.run('A02:same-bytes-same-task-positive-control', ALL_NEW, mods => withVector('normative', repo => {
|
|
104
|
+
const first = flow(mods, repo, 'normative');
|
|
105
|
+
requireEligible(first, 'first capture');
|
|
106
|
+
const second = flow(mods, repo, 'normative');
|
|
107
|
+
requireEligible(second, 'second capture with the same task');
|
|
108
|
+
H.sameStable(second.snapshot.identity, first.snapshot.identity, 'identity of an unchanged repository and task');
|
|
109
|
+
}));
|
|
110
|
+
|
|
111
|
+
// ---- A03: captured config binding (the resolver seam is exercised through the producer) ------------------------
|
|
112
|
+
|
|
113
|
+
runner.run('A03:parse-A-then-replace-with-B-same-value-rejected', [...ALL_NEW], mods => withVector('normative', repo => {
|
|
114
|
+
const first = flow(mods, repo, 'normative');
|
|
115
|
+
requireEligible(first, 'config A');
|
|
116
|
+
const configFile = path.join(repo.root, H.CONFIG_REL);
|
|
117
|
+
fs.writeFileSync(configFile, JSON.stringify(H.DERIVE_CONFIG, null, 4) + '\n', 'utf8');
|
|
118
|
+
const origin = new H.FakeOrigin({ context: H.fakeContext({}, repo) });
|
|
119
|
+
const produced = mods[PRODUCER].produceDocSnapshot(H.producerInput(repo), { originProvider: origin });
|
|
120
|
+
if (!produced.snapshot) throw new Error(`second capture not ready: ${H.codesOf(produced).join(',')}`);
|
|
121
|
+
if (produced.snapshot.identity.configDigest === first.snapshot.identity.configDigest) {
|
|
122
|
+
throw new Error('equal parsed config with different bytes must produce a different configDigest');
|
|
123
|
+
}
|
|
124
|
+
const decision = mods[CORE].checkAcceptedDocEvidence({ snapshot: produced.snapshot, accepted: first.accepted, mode: 'verification' });
|
|
125
|
+
if (decision.eligible) throw new Error('evidence bound to config A must not be eligible against config B');
|
|
126
|
+
H.expectCodes(H.codesOf(decision), ['config-capture-mismatch', 'identity-mismatch'], 'config A evidence under config B');
|
|
127
|
+
}));
|
|
128
|
+
|
|
129
|
+
runner.run('A03:active-host-selection-change-rejected', ALL_NEW, mods => withVector('normative', repo => {
|
|
130
|
+
const first = flow(mods, repo, 'normative');
|
|
131
|
+
requireEligible(first, 'host A');
|
|
132
|
+
const origin = new H.FakeOrigin({ context: H.fakeContext({ activeHost: { hostId: 'other-host' } }, repo) });
|
|
133
|
+
const produced = mods[PRODUCER].produceDocSnapshot(H.producerInput(repo, { top: { activeHost: { hostId: 'other-host' } } }), { originProvider: origin });
|
|
134
|
+
if (!produced.snapshot) throw new Error(`host B capture not ready: ${H.codesOf(produced).join(',')}`);
|
|
135
|
+
const decision = mods[CORE].checkAcceptedDocEvidence({ snapshot: produced.snapshot, accepted: first.accepted, mode: 'verification' });
|
|
136
|
+
if (decision.eligible) throw new Error('evidence bound to host A must not be eligible under host B');
|
|
137
|
+
H.expectCodes(H.codesOf(decision), ['config-capture-mismatch', 'identity-mismatch'], 'host A evidence under host B');
|
|
138
|
+
}));
|
|
139
|
+
|
|
140
|
+
// ---- A06: independent review progression; self and unknown provenance rejected without prose matching ------------
|
|
141
|
+
|
|
142
|
+
runner.run('A06:independent-review-eligible-in-review-mode-before-any-verifier', ALL_NEW, mods => withVector('normative', repo => {
|
|
143
|
+
const full = H.buildEvents(flow(mods, repo, 'normative').snapshot, repo, VECTORS.vectors.normative);
|
|
144
|
+
const reviewOnly = Object.fromEntries(Object.entries(full).filter(([, event]) => event.kind === 'review'));
|
|
145
|
+
const r = flow(mods, repo, 'normative', { mode: 'review', events: reviewOnly });
|
|
146
|
+
if (!r.decision) throw new Error(`review mode produced no decision: ${r.codes.join(',')}`);
|
|
147
|
+
if (r.decision.decision !== 'eligible') throw new Error(`review mode decision ${r.decision.decision}`);
|
|
148
|
+
H.sameStable(sorted(r.decision.checkedMustIds), ['doc-change:quality-review'], 'checkedMustIds in review mode (current quality-review MUST)');
|
|
149
|
+
H.sameStable(sorted(r.decision.expectedMustIds), sorted(VECTORS.vectors.normative.expectedMustIds), 'expectedMustIds keeps the full set');
|
|
150
|
+
}));
|
|
151
|
+
|
|
152
|
+
runner.run('A06:verification-stays-unresolved-with-review-only-evidence', ALL_NEW, mods => withVector('normative', repo => {
|
|
153
|
+
const full = H.buildEvents(flow(mods, repo, 'normative').snapshot, repo, VECTORS.vectors.normative);
|
|
154
|
+
const reviewOnly = Object.fromEntries(Object.entries(full).filter(([, event]) => event.kind === 'review'));
|
|
155
|
+
const r = flow(mods, repo, 'normative', { events: reviewOnly });
|
|
156
|
+
if (!r.decision) throw new Error(`no decision: ${r.codes.join(',')}`);
|
|
157
|
+
if (r.decision.eligible) throw new Error('verification must not be eligible without execution evidence');
|
|
158
|
+
H.expectCodes(H.codesOf(r.decision), ['result-missing'], 'missing execution evidence');
|
|
159
|
+
}));
|
|
160
|
+
|
|
161
|
+
runner.run('A06:later-accepted-execution-makes-verification-retry-eligible', ALL_NEW, mods => withVector('normative', repo => {
|
|
162
|
+
const full = H.buildEvents(flow(mods, repo, 'normative').snapshot, repo, VECTORS.vectors.normative);
|
|
163
|
+
const reviewOnly = Object.fromEntries(Object.entries(full).filter(([, event]) => event.kind === 'review'));
|
|
164
|
+
const before = flow(mods, repo, 'normative', { events: reviewOnly });
|
|
165
|
+
if (!before.decision || before.decision.eligible) throw new Error('review-only verification must stay unresolved before the retry');
|
|
166
|
+
const retry = flow(mods, repo, 'normative', { events: full });
|
|
167
|
+
requireEligible(retry, 'later accepted execution with the same review tuple');
|
|
168
|
+
}));
|
|
169
|
+
|
|
170
|
+
runner.run('A06:self-review-rejected-even-when-summary-claims-independence', ALL_NEW, mods => withVector('normative', repo => {
|
|
171
|
+
const snapshot = flow(mods, repo, 'normative').snapshot;
|
|
172
|
+
const events = H.buildEvents(snapshot, repo, VECTORS.vectors.normative);
|
|
173
|
+
for (const event of Object.values(events)) {
|
|
174
|
+
if (event.kind === 'review') {
|
|
175
|
+
event.principalId = PRINCIPALS.author;
|
|
176
|
+
event.summary = 'independently reviewed by a different principal: approve';
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const r = flow(mods, repo, 'normative', { events });
|
|
180
|
+
if (r.decision && r.decision.eligible) throw new Error('self-review must not be eligible');
|
|
181
|
+
H.expectCodes(r.codes, ['review-not-independent'], 'self-review with independence prose');
|
|
182
|
+
}));
|
|
183
|
+
|
|
184
|
+
runner.run('A06:unknown-independent-provenance-rejected', ALL_NEW, mods => withVector('normative', repo => {
|
|
185
|
+
const snapshot = flow(mods, repo, 'normative').snapshot;
|
|
186
|
+
const events = H.buildEvents(snapshot, repo, VECTORS.vectors.normative);
|
|
187
|
+
for (const event of Object.values(events)) if (event.kind === 'review') event.independentOf = [];
|
|
188
|
+
const r = flow(mods, repo, 'normative', { events });
|
|
189
|
+
if (r.decision && r.decision.eligible) throw new Error('review with unknown independence must not be eligible');
|
|
190
|
+
H.expectCodes(r.codes, ['review-not-independent'], 'review without independentOf provenance');
|
|
191
|
+
}));
|
|
192
|
+
|
|
193
|
+
// ---- A07: expected set is derived; omitted, subset and authored-step variants ----------------------------------
|
|
194
|
+
|
|
195
|
+
runner.run('A07:with-and-without-authored-doc-change-derive-the-same-musts', ALL_NEW, mods => withVector('normative', repo => {
|
|
196
|
+
const withDocStep = flow(mods, repo, 'normative');
|
|
197
|
+
requireEligible(withDocStep, 'with authored doc-change');
|
|
198
|
+
const withoutDocStep = flow(mods, repo, 'normative', { producer: { task: { requirements: VECTORS.fixedTask.requirements.slice(1) } } });
|
|
199
|
+
if (!withoutDocStep.decision) throw new Error(`without authored doc step produced no decision: ${withoutDocStep.codes.join(',')}`);
|
|
200
|
+
H.sameStable(sorted(withoutDocStep.decision.expectedMustIds), sorted(withDocStep.decision.expectedMustIds), 'expectedMustIds with and without the authored doc step');
|
|
201
|
+
}));
|
|
202
|
+
|
|
203
|
+
for (const [id, omit] of [
|
|
204
|
+
['omit-one-result-quality-review', ref => ref.startsWith('ref-quality-review')],
|
|
205
|
+
['omit-all-results', () => true],
|
|
206
|
+
['subset-results-only-lint-and-link', ref => !ref.startsWith('ref-doc-lint') && !ref.startsWith('ref-link-check')]
|
|
207
|
+
]) {
|
|
208
|
+
runner.run(`A07:${id}-loses-eligibility-even-when-returned-results-pass`, ALL_NEW, mods => withVector('normative', repo => {
|
|
209
|
+
const full = H.buildEvents(flow(mods, repo, 'normative').snapshot, repo, VECTORS.vectors.normative);
|
|
210
|
+
const kept = Object.fromEntries(Object.entries(full).filter(([ref]) => !omit(ref)));
|
|
211
|
+
const r = flow(mods, repo, 'normative', { events: kept });
|
|
212
|
+
if (r.decision && r.decision.eligible) throw new Error('an omitted MUST must not be eligible');
|
|
213
|
+
H.expectCodes(r.codes, ['result-missing'], id);
|
|
214
|
+
}));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ---- A08: legacy snapshot, then governed scope with old tuple and old results --------------------------------------
|
|
218
|
+
|
|
219
|
+
runner.run('A08:previously-nondocument-snapshot-is-legacy-never-verified', ALL_NEW, mods => withVector('nondocument', repo => {
|
|
220
|
+
const r = flow(mods, repo, 'nondocument');
|
|
221
|
+
if (!r.decision) throw new Error(`legacy case produced no decision: ${r.codes.join(',')}`);
|
|
222
|
+
H.sameStable(r.decision.decision, 'legacy', 'decision');
|
|
223
|
+
if (r.decision.eligible !== false) throw new Error('legacy must never be eligible for verified credit');
|
|
224
|
+
H.expectCodes(H.codesOf(r.decision), ['no-document-musts'], 'legacy diagnostic');
|
|
225
|
+
}));
|
|
226
|
+
|
|
227
|
+
runner.run('A08:new-governed-scope-rejects-old-legacy-tuple-and-produces-current-musts', ALL_NEW, mods => withVector('nondocument', repo => {
|
|
228
|
+
const legacy = flow(mods, repo, 'nondocument');
|
|
229
|
+
const oldIdentity = H.clone(legacy.snapshot.identity);
|
|
230
|
+
// A08's own candidate: the revised asset from the nondocument run plus the new normative PRD.
|
|
231
|
+
H.writeFiles(repo.root, { 'docs/prd/graph.md': VECTORS.vectors.a08.files.candidate['docs/prd/graph.md'] });
|
|
232
|
+
const governed = flow(mods, repo, 'a08', { events: {} });
|
|
233
|
+
if (!governed.snapshot) throw new Error(`governed capture not ready: ${governed.codes.join(',')}`);
|
|
234
|
+
H.sameStable(sorted(governed.decision.expectedMustIds || []), sorted(VECTORS.vectors.a08.expectedMustIds), 'current MUSTs for the A08 snapshot');
|
|
235
|
+
const events = H.buildEvents(governed.snapshot, repo, VECTORS.vectors.a08);
|
|
236
|
+
for (const event of Object.values(events)) event.identity = H.clone(oldIdentity);
|
|
237
|
+
const replay = flow(mods, repo, 'a08', { events });
|
|
238
|
+
if (replay.decision && replay.decision.eligible) throw new Error('old legacy tuple must not satisfy governed MUSTs');
|
|
239
|
+
H.expectCodes(replay.codes, ['identity-mismatch'], 'legacy tuple against governed snapshot');
|
|
240
|
+
}));
|
|
241
|
+
|
|
242
|
+
// ---- A12: origin provenance -------------------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
runner.run('A12:copied-journal-without-current-acceptance-rejected', ALL_NEW, mods => withVector('normative', repo => {
|
|
245
|
+
const snapshot = flow(mods, repo, 'normative').snapshot;
|
|
246
|
+
const events = H.buildEvents(snapshot, repo, VECTORS.vectors.normative);
|
|
247
|
+
const execRef = Object.keys(events).find(ref => events[ref].kind === 'execution');
|
|
248
|
+
events[execRef].trusted = true;
|
|
249
|
+
events[execRef].originRef = 'copied-from-another-session';
|
|
250
|
+
events['copied-journal-ref'] = { ...events[execRef] };
|
|
251
|
+
delete events[execRef];
|
|
252
|
+
events['copied-journal-ref'].__notIssued = true;
|
|
253
|
+
const r = flow(mods, repo, 'normative', { events });
|
|
254
|
+
if (r.decision && r.decision.eligible) throw new Error('copied journal must not be eligible');
|
|
255
|
+
H.expectCodes(r.codes, ['origin-untrusted'], 'copied journal with forged trusted flag');
|
|
256
|
+
}));
|
|
257
|
+
|
|
258
|
+
runner.run('A12:missing-origin-yields-origin-unavailable', ALL_NEW, mods => withVector('normative', repo => {
|
|
259
|
+
const r = flow(mods, repo, 'normative', { mode: 'missing' });
|
|
260
|
+
if (r.decision && r.decision.eligible) throw new Error('missing origin must not be eligible');
|
|
261
|
+
H.expectCodes(r.codes, ['origin-unavailable'], 'missing origin');
|
|
262
|
+
}));
|
|
263
|
+
|
|
264
|
+
runner.run('A12:expired-origin-yields-origin-unavailable', ALL_NEW, mods => withVector('normative', repo => {
|
|
265
|
+
const r = flow(mods, repo, 'normative', { mode: 'expired' });
|
|
266
|
+
if (r.decision && r.decision.eligible) throw new Error('expired origin must not be eligible');
|
|
267
|
+
H.expectCodes(r.codes, ['origin-unavailable'], 'expired origin');
|
|
268
|
+
}));
|
|
269
|
+
|
|
270
|
+
// ---- A13: applicability matrix ----------------------------------------------------------------------------------
|
|
271
|
+
|
|
272
|
+
runner.run('A13:unknown-must-stays-unresolved', ALL_NEW, mods => withVector('code-only', repo => {
|
|
273
|
+
const r = flow(mods, repo, 'code-only', { events: {} });
|
|
274
|
+
if (!r.decision) throw new Error(`no decision: ${r.codes.join(',')}`);
|
|
275
|
+
if (r.decision.eligible) throw new Error('an unknown MUST must not be eligible');
|
|
276
|
+
H.expectCodes(H.codesOf(r.decision), ['unknown-applicability'], 'unknown MUST');
|
|
277
|
+
if (r.decision.decision !== 'unresolved') throw new Error(`decision ${r.decision.decision} where unresolved is required`);
|
|
278
|
+
}));
|
|
279
|
+
|
|
280
|
+
runner.run('A13:na-without-trusted-derivation-reason-is-unresolved', ALL_NEW, mods => withVector('normative', repo => {
|
|
281
|
+
const first = flow(mods, repo, 'normative');
|
|
282
|
+
const tampered = H.clone(first.snapshot);
|
|
283
|
+
if (!tampered.derivationReasons || !tampered.derivationReasons['doc-change:derived-regenerate']) {
|
|
284
|
+
throw new Error('the snapshot lacks derivationReasons for the N/A MUST doc-change:derived-regenerate');
|
|
285
|
+
}
|
|
286
|
+
tampered.derivationReasons['doc-change:derived-regenerate'].reason = '';
|
|
287
|
+
const origin = new H.FakeOrigin({ context: H.fakeContext({}, repo) });
|
|
288
|
+
for (const [ref, event] of Object.entries(first.events)) origin.issueEvent(ref, event);
|
|
289
|
+
const accepted = mods[EVENTS].acceptDocEvents({ snapshot: tampered, eventRefs: Object.keys(first.events) }, { originProvider: origin });
|
|
290
|
+
const r = mods[CORE].checkAcceptedDocEvidence({ snapshot: tampered, accepted, mode: 'verification' });
|
|
291
|
+
if (r.eligible) throw new Error('N/A without a trusted derivation reason must not be eligible');
|
|
292
|
+
H.expectCodes(H.codesOf(r, accepted), ['na-reason-missing'], 'N/A without reason');
|
|
293
|
+
}));
|
|
294
|
+
|
|
295
|
+
runner.run('A13:should-outcome-never-gates-a-complete-normative-vector', ALL_NEW, mods => withVector('normative', repo => {
|
|
296
|
+
const r = flow(mods, repo, 'normative');
|
|
297
|
+
requireEligible(r, 'normative with SHOULD reminders present');
|
|
298
|
+
const gating = H.codesOf(r.decision).filter(code => /should/i.test(code));
|
|
299
|
+
H.sameStable(gating, [], 'SHOULD-related codes that gate');
|
|
300
|
+
}));
|
|
301
|
+
|
|
302
|
+
// ---- R2d P2b: rejected SHOULD / N/A evidence must gate nothing (PR #353 review P2) --------------------------------
|
|
303
|
+
|
|
304
|
+
runner.run('R2d-P2b:rejected-should-never-gates-a-complete-must-set', ALL_NEW, mods => withVector('normative', repo => {
|
|
305
|
+
const base = flow(mods, repo, 'normative');
|
|
306
|
+
requireEligible(base, 'complete normative base vector');
|
|
307
|
+
const snapshot = base.snapshot;
|
|
308
|
+
const should = snapshot.obligations.find(o => o.capability === 'prose-reminder');
|
|
309
|
+
if (!should || should.strength !== 'SHOULD') throw new Error('the normative snapshot lacks the prose-reminder SHOULD obligation');
|
|
310
|
+
const events = base.events;
|
|
311
|
+
// A deliberately invalid event for the SHOULD capability: rejected, but with its obligationId kept visible.
|
|
312
|
+
const smoke = { ...H.clone(events['ref-doc-lint']), eventId: 'evt-prose-reminder-smoke',
|
|
313
|
+
capability: 'prose-reminder', paths: [...(should.scope?.paths || [])], exitCode: 1 };
|
|
314
|
+
base.origin.issueEvent('ref-prose-reminder-smoke', smoke);
|
|
315
|
+
const accepted2 = mods[EVENTS].acceptDocEvents({ snapshot, eventRefs: [...Object.keys(events), 'ref-prose-reminder-smoke'] },
|
|
316
|
+
{ originProvider: base.origin });
|
|
317
|
+
if (!accepted2.rejected.find(item => item.code === 'event-invalid' && item.obligationId === should.id)) {
|
|
318
|
+
throw new Error(`the rejected SHOULD event must keep its obligationId visible: ${JSON.stringify(accepted2.rejected)}`);
|
|
319
|
+
}
|
|
320
|
+
const decision = mods[CORE].checkAcceptedDocEvidence({ snapshot, accepted: accepted2, mode: 'verification' });
|
|
321
|
+
if (decision.decision !== 'eligible' || decision.eligible !== true) {
|
|
322
|
+
throw new Error(`a rejected SHOULD (prose-reminder) must not gate a complete MUST set: decision ${decision.decision} with codes ${JSON.stringify(H.codesOf(accepted2, decision))}`);
|
|
323
|
+
}
|
|
324
|
+
if (decision.unsatisfied.some(item => item.obligationId === should.id)) {
|
|
325
|
+
throw new Error('the rejected SHOULD produced an unsatisfied entry');
|
|
326
|
+
}
|
|
327
|
+
}));
|
|
328
|
+
|
|
329
|
+
runner.run('R2d-P2b:rejected-na-obligation-event-never-gates-a-complete-must-set', ALL_NEW, mods => withVector('normative', repo => {
|
|
330
|
+
const base = flow(mods, repo, 'normative');
|
|
331
|
+
requireEligible(base, 'complete normative base vector');
|
|
332
|
+
const snapshot = base.snapshot;
|
|
333
|
+
const na = snapshot.obligations.find(o => o.id === 'doc-change:derived-regenerate');
|
|
334
|
+
if (!na || na.strength !== 'MUST' || na.applicability !== 'N/A') throw new Error('the normative snapshot lacks the derived-regenerate N/A MUST');
|
|
335
|
+
const events = base.events;
|
|
336
|
+
const smoke = { ...H.clone(events['ref-doc-lint']), eventId: 'evt-derived-regenerate-smoke',
|
|
337
|
+
capability: na.capability, paths: [...(na.scope?.paths || [])], exitCode: 1 };
|
|
338
|
+
base.origin.issueEvent('ref-derived-regenerate-smoke', smoke);
|
|
339
|
+
const accepted2 = mods[EVENTS].acceptDocEvents({ snapshot, eventRefs: [...Object.keys(events), 'ref-derived-regenerate-smoke'] },
|
|
340
|
+
{ originProvider: base.origin });
|
|
341
|
+
if (!accepted2.rejected.find(item => item.code === 'event-invalid' && item.obligationId === na.id)) {
|
|
342
|
+
throw new Error(`the rejected N/A event must keep its obligationId visible: ${JSON.stringify(accepted2.rejected)}`);
|
|
343
|
+
}
|
|
344
|
+
const decision = mods[CORE].checkAcceptedDocEvidence({ snapshot, accepted: accepted2, mode: 'verification' });
|
|
345
|
+
if (decision.decision !== 'eligible' || decision.eligible !== true) {
|
|
346
|
+
throw new Error(`a rejected N/A obligation event must not gate a complete MUST set: decision ${decision.decision} with codes ${JSON.stringify(H.codesOf(accepted2, decision))}`);
|
|
347
|
+
}
|
|
348
|
+
if (decision.unsatisfied.some(item => item.obligationId === na.id)) {
|
|
349
|
+
throw new Error('the rejected N/A event produced an unsatisfied entry');
|
|
350
|
+
}
|
|
351
|
+
}));
|
|
352
|
+
|
|
353
|
+
runner.run('R2d-P2b:rejected-applicable-must-still-blocks-eligibility', ALL_NEW, mods => withVector('normative', repo => {
|
|
354
|
+
// Control: a rejected event for an expected applicable MUST keeps gating (now and after the P2 fix).
|
|
355
|
+
const base = flow(mods, repo, 'normative');
|
|
356
|
+
requireEligible(base, 'complete normative base vector');
|
|
357
|
+
const events = base.events;
|
|
358
|
+
const key = Object.keys(events['ref-doc-lint'].artifactDigests)[0];
|
|
359
|
+
events['ref-doc-lint'].artifactDigests[key] = H.ZERO_DIGEST;
|
|
360
|
+
const r = flow(mods, repo, 'normative', { events });
|
|
361
|
+
if (r.decision && r.decision.eligible) throw new Error('a rejected applicable MUST event must still block eligibility');
|
|
362
|
+
H.expectCodes(r.codes, ['artifact-mismatch'], 'rejected applicable MUST event');
|
|
363
|
+
}));
|
|
364
|
+
|
|
365
|
+
// ---- PR #353 third pass: the event-acceptance boundary revalidates the concrete start pin ------------------------
|
|
366
|
+
|
|
367
|
+
function acceptAfterContextChange(mods, repo, change) {
|
|
368
|
+
const base = flow(mods, repo, 'normative');
|
|
369
|
+
requireEligible(base, 'complete normative base vector');
|
|
370
|
+
change(base.origin.context);
|
|
371
|
+
const accepted = mods[EVENTS].acceptDocEvents({ snapshot: base.snapshot, eventRefs: Object.keys(base.events) }, { originProvider: base.origin });
|
|
372
|
+
const decision = mods[CORE].checkAcceptedDocEvidence({ snapshot: base.snapshot, accepted, mode: 'verification' });
|
|
373
|
+
return { accepted, decision };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
runner.run('R2e-P1:changed-provider-pin-commit-rejected-at-event-acceptance', ALL_NEW, mods => withVector('normative', repo => {
|
|
377
|
+
// Same baselineReference, originRef, session, task and workflow; only the concrete pin commit changes.
|
|
378
|
+
const { accepted, decision } = acceptAfterContextChange(mods, repo, context => { context.baseline = { ...context.baseline, commit: 'b'.repeat(40) }; });
|
|
379
|
+
if (decision.eligible) throw new Error('a provider pin changed after capture must not leave prior events eligible');
|
|
380
|
+
H.expectCodes(H.codesOf(accepted, decision), ['identity-mismatch', 'baseline-unavailable'], 'changed provider pin commit');
|
|
381
|
+
}));
|
|
382
|
+
|
|
383
|
+
runner.run('R2e-P1:changed-provider-pin-root-rejected-at-event-acceptance', ALL_NEW, mods => withVector('normative', repo => {
|
|
384
|
+
const other = fs.mkdtempSync(path.join(os.tmpdir(), 'doc-s2a-other-root-'));
|
|
385
|
+
try {
|
|
386
|
+
const { accepted, decision } = acceptAfterContextChange(mods, repo, context => { context.baseline = { ...context.baseline, root: other }; });
|
|
387
|
+
if (decision.eligible) throw new Error('a provider pin root changed after capture must not leave prior events eligible');
|
|
388
|
+
H.expectCodes(H.codesOf(accepted, decision), ['identity-mismatch', 'baseline-unavailable'], 'changed provider pin root');
|
|
389
|
+
} finally { fs.rmSync(other, { recursive: true, force: true }); }
|
|
390
|
+
}));
|
|
391
|
+
|
|
392
|
+
runner.run('R2e-P1:unchanged-provider-context-stays-eligible-control', ALL_NEW, mods => withVector('normative', repo => {
|
|
393
|
+
const { decision } = acceptAfterContextChange(mods, repo, () => {});
|
|
394
|
+
if (!decision.eligible) throw new Error(`an unchanged provider context must stay eligible: ${JSON.stringify(decision.unsatisfied)}`);
|
|
395
|
+
}));
|
|
396
|
+
|
|
397
|
+
// ---- R2f P1: the captured origin pin must agree with the hashed baseline -----------------------------------------
|
|
398
|
+
|
|
399
|
+
for (const field of ['commit', 'root', 'missing-pin']) {
|
|
400
|
+
runner.run(`R2f-P1:joint-origin-and-snapshot-pin-${field}-tampering-rejected`, ALL_NEW, mods => withVector('normative', repo => {
|
|
401
|
+
const base = flow(mods, repo, 'normative');
|
|
402
|
+
requireEligible(base, 'complete normative control');
|
|
403
|
+
const snapshot = H.clone(base.snapshot);
|
|
404
|
+
const other = H.makeRepo({ 'other.txt': 'other root\n' });
|
|
405
|
+
try {
|
|
406
|
+
if (field === 'missing-pin') {
|
|
407
|
+
delete snapshot.originContext.baseline;
|
|
408
|
+
delete base.origin.context.baseline;
|
|
409
|
+
} else {
|
|
410
|
+
const value = field === 'commit' ? other.commit : fs.realpathSync.native(other.root);
|
|
411
|
+
snapshot.originContext.baseline[field] = value;
|
|
412
|
+
base.origin.context.baseline[field] = value;
|
|
413
|
+
}
|
|
414
|
+
H.sameStable(snapshot.baseline, H.clone(base.snapshot.baseline), 'hashed baseline unchanged');
|
|
415
|
+
H.sameStable(snapshot.identity, base.snapshot.identity, 'event identity unchanged');
|
|
416
|
+
const accepted = mods[EVENTS].acceptDocEvents({ snapshot, eventRefs: Object.keys(base.events) }, { originProvider: base.origin });
|
|
417
|
+
H.expectCodes(H.codesOf(accepted), ['baseline-unavailable'], 'inconsistent snapshot pin');
|
|
418
|
+
H.sameStable(Object.keys(accepted.acceptedIndex), [], 'no accepted events from an inconsistent snapshot');
|
|
419
|
+
for (const mode of ['verification', 'review']) {
|
|
420
|
+
const decision = mods[CORE].checkAcceptedDocEvidence({ snapshot, accepted, mode });
|
|
421
|
+
if (decision.eligible) throw new Error(`${mode}: inconsistent snapshot pin earned credit`);
|
|
422
|
+
H.expectCodes(H.codesOf(decision), ['baseline-unavailable'], `${mode} invariant`);
|
|
423
|
+
const cached = H.clone(base.accepted);
|
|
424
|
+
cached.originContext = H.clone(snapshot.originContext);
|
|
425
|
+
const replay = mods[CORE].checkAcceptedDocEvidence({ snapshot, accepted: cached, mode });
|
|
426
|
+
if (replay.eligible) throw new Error(`${mode}: cached accepted evidence bypassed the snapshot invariant`);
|
|
427
|
+
H.expectCodes(H.codesOf(replay), ['baseline-unavailable'], `${mode} cached invariant`);
|
|
428
|
+
}
|
|
429
|
+
} finally { H.removeRepo(other.root); }
|
|
430
|
+
}));
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
runner.run('R2f-P1:equivalent-provider-root-is-canonicalized-control', ALL_NEW, mods => withVector('normative', repo => {
|
|
434
|
+
const r = flow(mods, repo, 'normative', { context: { baseline: { commit: repo.commit, root: repo.root + '/.' } } });
|
|
435
|
+
requireEligible(r, 'equivalent provider root');
|
|
436
|
+
H.sameStable(r.snapshot.originContext.baseline.root, r.snapshot.originContext.workspaceRoot, 'canonical captured roots');
|
|
437
|
+
}));
|
|
438
|
+
|
|
439
|
+
// ---- PR #353 review P1: the captured origin identity must be the snapshot identity ---------------------------
|
|
440
|
+
//
|
|
441
|
+
// The captured origin context is the only origin-correlated identity in a snapshot. Events compare the
|
|
442
|
+
// provider context with that capture and the event identity with snapshot.identity, so a jointly substituted
|
|
443
|
+
// capture/provider identity disconnected the two halves: verification and review could earn credit for a
|
|
444
|
+
// snapshot whose identity named a different task, session, workflow or workspace than the origin it claims.
|
|
445
|
+
|
|
446
|
+
const IDENTITY_TUPLE_FIELDS = ['workspaceId', 'sessionId', 'workflowId', 'workflowRevision', 'taskId'];
|
|
447
|
+
|
|
448
|
+
// Substitutes one origin-correlated field in the captured origin context and in the provider context together,
|
|
449
|
+
// keeping the snapshot identity and the issued events at the original values.
|
|
450
|
+
function substituteOrigin(mods, repo, mutate) {
|
|
451
|
+
const base = flow(mods, repo, 'normative');
|
|
452
|
+
requireEligible(base, 'complete normative control');
|
|
453
|
+
const snapshot = H.clone(base.snapshot);
|
|
454
|
+
mutate(snapshot.originContext, base.origin.context);
|
|
455
|
+
const accepted = mods[EVENTS].acceptDocEvents({ snapshot, eventRefs: Object.keys(base.events) }, { originProvider: base.origin });
|
|
456
|
+
for (const mode of ['verification', 'review']) {
|
|
457
|
+
const decision = mods[CORE].checkAcceptedDocEvidence({ snapshot, accepted, mode });
|
|
458
|
+
if (decision.eligible) throw new Error(`${mode}: a substituted origin identity must not earn credit`);
|
|
459
|
+
H.expectCodes(H.codesOf(decision), ['identity-mismatch'], `${mode} substituted origin identity`);
|
|
460
|
+
// A cached acceptance from the substituted tuple must not bypass the same invariant.
|
|
461
|
+
const cached = H.clone(base.accepted);
|
|
462
|
+
cached.originContext = H.clone(snapshot.originContext);
|
|
463
|
+
const replay = mods[CORE].checkAcceptedDocEvidence({ snapshot, accepted: cached, mode });
|
|
464
|
+
if (replay.eligible) throw new Error(`${mode}: cached accepted evidence bypassed the origin identity invariant`);
|
|
465
|
+
H.expectCodes(H.codesOf(replay), ['identity-mismatch'], `${mode} cached substituted origin identity`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
for (const field of IDENTITY_TUPLE_FIELDS) {
|
|
470
|
+
runner.run(`R3-P1:joint-origin-and-identity-${field}-substitution-rejected`, ALL_NEW, mods => withVector('normative', repo => {
|
|
471
|
+
substituteOrigin(mods, repo, (context, providerContext) => {
|
|
472
|
+
const value = field === 'workflowRevision' ? context[field] + 1 : `${context[field]}-substituted`;
|
|
473
|
+
context[field] = value;
|
|
474
|
+
providerContext[field] = value;
|
|
475
|
+
});
|
|
476
|
+
}));
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
runner.run('R3-P1:joint-origin-and-active-host-substitution-rejected', ALL_NEW, mods => withVector('normative', repo => {
|
|
480
|
+
// The active host selected the captured config; a substituted host must not match the captured selection.
|
|
481
|
+
substituteOrigin(mods, repo, (context, providerContext) => {
|
|
482
|
+
context.activeHost = { hostId: 'other-host' };
|
|
483
|
+
providerContext.activeHost = { hostId: 'other-host' };
|
|
484
|
+
});
|
|
485
|
+
}));
|
|
486
|
+
|
|
487
|
+
runner.run('R3-P1:joint-plan-and-identity-substitution-rejected', ALL_NEW, mods => withVector('normative', repo => {
|
|
488
|
+
// The captured plan and the identity are substituted together and the plan digest is recomputed, so only the
|
|
489
|
+
// captured origin context can still name the original tuple. A stale acceptance rewritten to match the
|
|
490
|
+
// substituted tuple must not earn credit.
|
|
491
|
+
const base = flow(mods, repo, 'normative');
|
|
492
|
+
requireEligible(base, 'complete normative control');
|
|
493
|
+
const snapshot = H.clone(base.snapshot);
|
|
494
|
+
snapshot.identity.workflowId = 'wf-substituted';
|
|
495
|
+
snapshot.taskSemantics.workflow.workflowId = 'wf-substituted';
|
|
496
|
+
snapshot.identity.taskScopeDigest = mods[CORE]._internals.structuredDigest('doc-task-scope-v1', snapshot.taskSemantics);
|
|
497
|
+
const cached = H.clone(base.accepted);
|
|
498
|
+
cached.identity = H.clone(snapshot.identity);
|
|
499
|
+
cached.originContext = H.clone(snapshot.originContext);
|
|
500
|
+
for (const key of Object.keys(cached.acceptedIndex || {})) cached.acceptedIndex[key].identity = H.clone(snapshot.identity);
|
|
501
|
+
for (const mode of ['verification', 'review']) {
|
|
502
|
+
const decision = mods[CORE].checkAcceptedDocEvidence({ snapshot, accepted: cached, mode });
|
|
503
|
+
if (decision.eligible) throw new Error(`${mode}: a substituted plan and identity must not earn credit`);
|
|
504
|
+
H.expectCodes(H.codesOf(decision), ['identity-mismatch'], `${mode} substituted plan and identity`);
|
|
505
|
+
}
|
|
506
|
+
}));
|
|
507
|
+
|
|
508
|
+
runner.run('R3-P1:unchanged-origin-identity-stays-eligible-control', ALL_NEW, mods => withVector('normative', repo => {
|
|
509
|
+
const r = flow(mods, repo, 'normative');
|
|
510
|
+
requireEligible(r, 'unchanged origin identity');
|
|
511
|
+
}));
|
|
512
|
+
|
|
513
|
+
runner.run('R3-P1:config-capture-bytes-must-match-the-captured-inventory', ALL_NEW, mods => withVector('normative', repo => {
|
|
514
|
+
// The config digest binds the captured bytes and the parsed value; the whole-project capture must hold the
|
|
515
|
+
// same bytes for that path, or the two halves of the capture describe different files.
|
|
516
|
+
const base = flow(mods, repo, 'normative');
|
|
517
|
+
requireEligible(base, 'complete normative control');
|
|
518
|
+
const snapshot = H.clone(base.snapshot);
|
|
519
|
+
const source = snapshot.config.capture.sources.find(item => item.presence === 'present');
|
|
520
|
+
if (!source) throw new Error('the captured config has no present source');
|
|
521
|
+
source.bytes = Buffer.from('{"governed":[]}\n');
|
|
522
|
+
source.digest = H.sha256(source.bytes);
|
|
523
|
+
snapshot.config.digest = mods[CORE]._internals.configDigest(snapshot.config);
|
|
524
|
+
snapshot.identity.configDigest = snapshot.config.digest;
|
|
525
|
+
// The plan carries the config digest, so it is restated with it: only the link between the captured config
|
|
526
|
+
// bytes and the captured inventory can still detect the substitution.
|
|
527
|
+
snapshot.taskSemantics.configDigest = snapshot.config.digest;
|
|
528
|
+
snapshot.identity.taskScopeDigest = mods[CORE]._internals.structuredDigest('doc-task-scope-v1', snapshot.taskSemantics);
|
|
529
|
+
// The cached acceptance is restated with the substituted config digest, so only the link between the
|
|
530
|
+
// captured config bytes and the captured inventory can still detect the substitution.
|
|
531
|
+
const cached = H.clone(base.accepted);
|
|
532
|
+
cached.identity = H.clone(snapshot.identity);
|
|
533
|
+
cached.originContext = H.clone(snapshot.originContext);
|
|
534
|
+
for (const key of Object.keys(cached.acceptedIndex || {})) cached.acceptedIndex[key].identity = H.clone(snapshot.identity);
|
|
535
|
+
const decision = mods[CORE].checkAcceptedDocEvidence({ snapshot, accepted: cached, mode: 'verification' });
|
|
536
|
+
if (decision.eligible) throw new Error('config bytes the captured inventory contradicts must not earn credit');
|
|
537
|
+
H.expectCodes(H.codesOf(decision), ['config-capture-mismatch'], 'config capture bytes against the inventory');
|
|
538
|
+
}));
|
|
539
|
+
|
|
540
|
+
// ---- PR #353 review P2: a whole-project inventory that contains binary records ------------------------------
|
|
541
|
+
//
|
|
542
|
+
// The inventory is whole-project, so a normal tracked binary asset must not make the capture unusable. Its
|
|
543
|
+
// stored bytes stay in the identity framing; only semantic derivation needs decoded text.
|
|
544
|
+
|
|
545
|
+
function withBinaryRepo(binaryFiles, fn) {
|
|
546
|
+
const files = H.vectorFiles('normative');
|
|
547
|
+
const repo = H.makeRepo(files.base, binaryFiles);
|
|
548
|
+
try {
|
|
549
|
+
H.writeFiles(repo.root, files.candidate);
|
|
550
|
+
fn(repo);
|
|
551
|
+
} finally {
|
|
552
|
+
H.removeRepo(repo.root);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
runner.run('R3-P2:irrelevant-binary-record-captures-and-reconciles', ALL_NEW, mods => withBinaryRepo({ 'assets/logo.png': H.BINARY_BYTES }, repo => {
|
|
557
|
+
const r = flow(mods, repo, 'normative');
|
|
558
|
+
requireEligible(r, 'capture with an irrelevant binary record');
|
|
559
|
+
const record = (r.snapshot.candidate.records || []).find(item => item.path === 'assets/logo.png');
|
|
560
|
+
if (!record || record.presence !== 'present') throw new Error('the binary record must stay in the candidate inventory');
|
|
561
|
+
H.sameStable(record.digest, H.sha256(H.BINARY_BYTES), 'binary record digest of its stored bytes');
|
|
562
|
+
H.sameStable(sorted(r.decision.expectedMustIds), sorted(VECTORS.vectors.normative.expectedMustIds), 'expectedMustIds with an irrelevant binary record');
|
|
563
|
+
}));
|
|
564
|
+
|
|
565
|
+
runner.run('R3-P2:core-fails-closed-on-a-relevant-undecodable-record', ALL_NEW, mods => withVector('normative', repo => {
|
|
566
|
+
// A governed document whose bytes cannot be decoded changes applicability, so the core must refuse to
|
|
567
|
+
// verify the snapshot instead of deriving facts without it.
|
|
568
|
+
const base = flow(mods, repo, 'normative');
|
|
569
|
+
requireEligible(base, 'complete normative control');
|
|
570
|
+
const snapshot = H.clone(base.snapshot);
|
|
571
|
+
const record = { path: 'docs/prd/binary.md', presence: 'present', type: 'file', mode: '100644',
|
|
572
|
+
storedBytes: { type: 'Buffer', data: [...H.BINARY_BYTES] }, digest: H.sha256(H.BINARY_BYTES) };
|
|
573
|
+
snapshot.candidate.records = snapshot.candidate.records.concat([record]);
|
|
574
|
+
const digest = mods[CORE]._internals.snapshotDigest('candidate', snapshot.baseline, snapshot.candidate.coverage, snapshot.candidate.records);
|
|
575
|
+
snapshot.candidate.digest = digest;
|
|
576
|
+
snapshot.identity.candidateDigest = digest;
|
|
577
|
+
const errors = mods[CORE]._internals.validateSnapshot(snapshot);
|
|
578
|
+
if (!errors.some(item => item.code === 'inventory-unsupported')) {
|
|
579
|
+
throw new Error(`the core must fail closed on a relevant undecodable record: ${JSON.stringify(errors)}`);
|
|
580
|
+
}
|
|
581
|
+
}));
|
|
582
|
+
|
|
583
|
+
// ---- A14: stable captures, capture boundary and status-only progress --------------------------------------------
|
|
584
|
+
|
|
585
|
+
runner.run('A14:unchanged-repository-captures-identically', ALL_NEW, mods => withVector('normative', repo => {
|
|
586
|
+
const first = flow(mods, repo, 'normative');
|
|
587
|
+
const second = flow(mods, repo, 'normative');
|
|
588
|
+
H.sameStable(second.snapshot && second.snapshot.identity, first.snapshot && first.snapshot.identity, 'identity across two captures');
|
|
589
|
+
H.sameStable(second.snapshot && second.snapshot.obligations, first.snapshot && first.snapshot.obligations, 'obligations across two captures');
|
|
590
|
+
}));
|
|
591
|
+
|
|
592
|
+
runner.run('A14:context-advancing-during-capture-rejected-as-snapshot-changed', ALL_NEW, mods => withVector('normative', repo => {
|
|
593
|
+
const origin = new H.FakeOrigin({ context: H.fakeContext({}, repo) });
|
|
594
|
+
origin.contextAt = call => (call > 1 ? H.fakeContext({ taskId: 'task-s2a-002' }, repo) : H.fakeContext({}, repo));
|
|
595
|
+
const produced = mods[PRODUCER].produceDocSnapshot(H.producerInput(repo), { originProvider: origin });
|
|
596
|
+
if (produced.snapshot) throw new Error('a context that advances during capture must not yield a usable snapshot');
|
|
597
|
+
H.expectCodes(H.codesOf(produced), ['snapshot-changed'], 'before/after context disagreement');
|
|
598
|
+
}));
|
|
599
|
+
|
|
600
|
+
runner.run('A14:status-only-workflow-progress-does-not-stale-the-task-scope', ALL_NEW, mods => withVector('normative', repo => {
|
|
601
|
+
const before = flow(mods, repo, 'normative');
|
|
602
|
+
const progressed = H.clone(VECTORS.fixedTask);
|
|
603
|
+
for (const requirement of progressed.requirements) {
|
|
604
|
+
requirement.status = 'pass';
|
|
605
|
+
requirement.evidence = 'progress note';
|
|
606
|
+
requirement.updatedAt = '2026-02-01T00:00:00Z';
|
|
607
|
+
}
|
|
608
|
+
const after = flow(mods, repo, 'normative', { producer: { task: { requirements: progressed.requirements } } });
|
|
609
|
+
if (!after.snapshot) throw new Error(`progressed capture not ready: ${after.codes.join(',')}`);
|
|
610
|
+
H.sameStable(after.snapshot.identity.taskScopeDigest, before.snapshot.identity.taskScopeDigest, 'taskScopeDigest with status-only progress');
|
|
611
|
+
H.sameStable(after.snapshot.identity.candidateDigest, before.snapshot.identity.candidateDigest, 'candidateDigest with status-only progress');
|
|
612
|
+
}));
|
|
613
|
+
|
|
614
|
+
// ---- Single-field flips from one eligible vector -----------------------------------------------------------------
|
|
615
|
+
|
|
616
|
+
function mutateEvents(mutator, events, ctx) {
|
|
617
|
+
const refs = Object.keys(events);
|
|
618
|
+
const execRef = refs.find(ref => events[ref].kind === 'execution' && events[ref].capability === 'doc-lint');
|
|
619
|
+
const reviewRef = refs.find(ref => events[ref].kind === 'review');
|
|
620
|
+
const firstExec = () => events[execRef];
|
|
621
|
+
const firstReview = () => events[reviewRef];
|
|
622
|
+
if (mutator.startsWith('identityField:')) {
|
|
623
|
+
const field = mutator.slice('identityField:'.length);
|
|
624
|
+
for (const event of Object.values(events)) {
|
|
625
|
+
const value = event.identity[field];
|
|
626
|
+
if (field === 'workflowRevision') event.identity[field] = value + 1;
|
|
627
|
+
else if (field === 'prdRoot') event.identity[field] = 'docs/other/';
|
|
628
|
+
else event.identity[field] = `${value}-mutated`;
|
|
629
|
+
}
|
|
630
|
+
return events;
|
|
631
|
+
}
|
|
632
|
+
switch (mutator) {
|
|
633
|
+
case 'prdSetDigestAsCandidate':
|
|
634
|
+
for (const event of Object.values(events)) event.identity.candidateDigest = H.prdSetDigest(ctx.repo.root);
|
|
635
|
+
return events;
|
|
636
|
+
case 'reviewerIsAuthor': firstReview().principalId = PRINCIPALS.author; return events;
|
|
637
|
+
case 'reviewerIsExecutor': firstReview().principalId = PRINCIPALS.executor; return events;
|
|
638
|
+
case 'independentOfMissing': firstReview().independentOf = []; return events;
|
|
639
|
+
case 'exitNull': firstExec().exitCode = null; return events;
|
|
640
|
+
case 'exitString': firstExec().exitCode = '0'; return events;
|
|
641
|
+
case 'exitNonzero': firstExec().exitCode = 1; return events;
|
|
642
|
+
case 'signalSet': firstExec().signal = 'SIGTERM'; return events;
|
|
643
|
+
case 'capabilityUnrelated': firstExec().command = 'npm test'; return events;
|
|
644
|
+
case 'pathsNarrowed': firstExec().paths = []; return events;
|
|
645
|
+
case 'artifactDigestWrong': {
|
|
646
|
+
const key = Object.keys(firstExec().artifactDigests)[0];
|
|
647
|
+
firstExec().artifactDigests[key] = ZERO_DIGEST;
|
|
648
|
+
return events;
|
|
649
|
+
}
|
|
650
|
+
case 'verdictReject': firstReview().verdict = 'reject'; return events;
|
|
651
|
+
case 'summaryEmpty': firstReview().summary = ''; return events;
|
|
652
|
+
case 'eventDuplicated': events['ref-duplicate'] = { ...firstExec() }; return events;
|
|
653
|
+
case 'eventConflicting': events['ref-conflict'] = { ...firstExec(), eventId: 'evt-doc-lint-conflict', exitCode: 1 }; return events;
|
|
654
|
+
case 'providerStale': firstReview().providerStatus = 'stale'; return events;
|
|
655
|
+
case 'journalCopied': {
|
|
656
|
+
const copy = { ...firstExec(), originRef: 'copied-from-another-session', trusted: true };
|
|
657
|
+
delete events[execRef];
|
|
658
|
+
events['copied-journal-ref'] = { ...copy, __notIssued: true };
|
|
659
|
+
return events;
|
|
660
|
+
}
|
|
661
|
+
case 'omitOneMust': {
|
|
662
|
+
for (const ref of refs) if (events[ref].capability === 'quality-review') delete events[ref];
|
|
663
|
+
return events;
|
|
664
|
+
}
|
|
665
|
+
default:
|
|
666
|
+
throw new H.InfraError(`unknown event mutator ${mutator}`);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function applyRecapture(mutator, repo, state) {
|
|
671
|
+
switch (mutator) {
|
|
672
|
+
case 'recapture:taskId':
|
|
673
|
+
state.producer = { task: { taskId: 'task-s2a-002' } };
|
|
674
|
+
state.context = { taskId: 'task-s2a-002' };
|
|
675
|
+
return;
|
|
676
|
+
case 'recapture:requirementsNarrowed':
|
|
677
|
+
state.producer = { task: { requirements: [VECTORS.fixedTask.requirements[0]] } };
|
|
678
|
+
return;
|
|
679
|
+
case 'recapture:acceptanceChanged': {
|
|
680
|
+
const requirements = H.clone(VECTORS.fixedTask.requirements);
|
|
681
|
+
requirements[0].acceptance = `${requirements[0].acceptance} (changed)`;
|
|
682
|
+
state.producer = { task: { requirements } };
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
case 'recapture:bindingChanged': {
|
|
686
|
+
const requirements = H.clone(VECTORS.fixedTask.requirements);
|
|
687
|
+
requirements[1].bindings = [];
|
|
688
|
+
state.producer = { task: { requirements } };
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
case 'recapture:workflowRevision':
|
|
692
|
+
state.producer = { workflow: { workflowRevision: VECTORS.fixedWorkflow.workflowRevision + 1 } };
|
|
693
|
+
state.context = { workflowRevision: VECTORS.fixedWorkflow.workflowRevision + 1 };
|
|
694
|
+
return;
|
|
695
|
+
case 'recapture:configBytesReformatted':
|
|
696
|
+
fs.writeFileSync(path.join(repo.root, H.CONFIG_REL), JSON.stringify(H.DERIVE_CONFIG, null, 4) + '\n', 'utf8');
|
|
697
|
+
return;
|
|
698
|
+
case 'recapture:activeHostChanged':
|
|
699
|
+
state.producer = { top: { activeHost: { hostId: 'other-host' } } };
|
|
700
|
+
state.context = { activeHost: { hostId: 'other-host' } };
|
|
701
|
+
return;
|
|
702
|
+
case 'recapture:otherTreeBytes':
|
|
703
|
+
fs.writeFileSync(path.join(repo.root, 'docs/prd/graph.md'), H.readTree(path.join(H.ROOT, 'ci/fixtures/doc-obligations/derive/docs-only/candidate'))['docs/reference/guide.md'], 'utf8');
|
|
704
|
+
return;
|
|
705
|
+
case 'recapture:emptyFileDeleted':
|
|
706
|
+
fs.rmSync(path.join(repo.root, 'assets/extra.txt'), { force: true });
|
|
707
|
+
return;
|
|
708
|
+
case 'recapture:modeDrift':
|
|
709
|
+
if (process.platform === 'win32') throw new H.UncheckedError('win32: no POSIX execute bit (core.fileMode=false), mode drift is not observable');
|
|
710
|
+
fs.chmodSync(path.join(repo.root, 'docs/prd/graph.md'), 0o755);
|
|
711
|
+
return;
|
|
712
|
+
default:
|
|
713
|
+
throw new H.InfraError(`unknown recapture mutator ${mutator}`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
for (const flip of VECTORS.flips) {
|
|
718
|
+
runner.run(`flip:${flip.id}`, ALL_NEW, mods => {
|
|
719
|
+
const setup = flip.setup === 'emptyExtra' ? 'emptyExtra' : null;
|
|
720
|
+
const files = H.vectorFiles('normative');
|
|
721
|
+
if (setup === 'emptyExtra') {
|
|
722
|
+
files.base['assets/extra.txt'] = 'extra asset with content\n';
|
|
723
|
+
files.candidate['assets/extra.txt'] = '';
|
|
724
|
+
}
|
|
725
|
+
const repo = H.makeRepo(files.base);
|
|
726
|
+
try {
|
|
727
|
+
H.writeFiles(repo.root, files.candidate);
|
|
728
|
+
const requireBase = (result, label) => requireEligible(result, label);
|
|
729
|
+
if (flip.kind === 'event' || flip.kind === 'core') {
|
|
730
|
+
const base = flow(mods, repo, 'normative');
|
|
731
|
+
requireBase(base, 'eligible base vector');
|
|
732
|
+
if (flip.kind === 'core') {
|
|
733
|
+
const dup = base.decision.results.concat([base.decision.results[0]]);
|
|
734
|
+
const r = mods[CORE].reconcileDocResults({ snapshot: base.snapshot, accepted: base.accepted, checkerOutput: { results: dup, diagnostics: [] }, mode: 'verification' });
|
|
735
|
+
if (r.eligible) throw new Error('a duplicated checker result must not be eligible');
|
|
736
|
+
H.expectCodes(H.codesOf(r), flip.expect, flip.id);
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
const events = H.buildEvents(base.snapshot, repo, VECTORS.vectors.normative);
|
|
740
|
+
const mutated = mutateEvents(flip.mutator, events, { repo });
|
|
741
|
+
const r = flow(mods, repo, 'normative', { events: mutated });
|
|
742
|
+
if (r.decision && r.decision.eligible) throw new Error(`flip ${flip.id} remained eligible`);
|
|
743
|
+
H.expectCodes(r.codes, flip.expect, flip.id);
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
if (flip.kind === 'origin') {
|
|
747
|
+
const r = flow(mods, repo, 'normative', { mode: flip.mutator === 'originMissing' ? 'missing' : 'expired' });
|
|
748
|
+
if (r.decision && r.decision.eligible) throw new Error(`flip ${flip.id} remained eligible`);
|
|
749
|
+
H.expectCodes(r.codes, flip.expect, flip.id);
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
if (flip.kind === 'recapture') {
|
|
753
|
+
const base = flow(mods, repo, 'normative');
|
|
754
|
+
requireBase(base, 'eligible base vector');
|
|
755
|
+
const oldEvents = base.events;
|
|
756
|
+
const state = {};
|
|
757
|
+
applyRecapture(flip.mutator, repo, state);
|
|
758
|
+
const origin = base.origin;
|
|
759
|
+
if (state.context) origin.context = { ...origin.context, ...state.context };
|
|
760
|
+
const producerInputs = H.producerInput(repo, state.producer || {});
|
|
761
|
+
const produced = mods[PRODUCER].produceDocSnapshot(producerInputs, { originProvider: origin });
|
|
762
|
+
if (!produced.snapshot) { H.expectCodes(H.codesOf(produced), flip.expect, flip.id); return; }
|
|
763
|
+
const accepted = mods[EVENTS].acceptDocEvents({ snapshot: produced.snapshot, eventRefs: Object.keys(oldEvents) }, { originProvider: origin });
|
|
764
|
+
const decision = mods[CORE].checkAcceptedDocEvidence({ snapshot: produced.snapshot, accepted, mode: 'verification' });
|
|
765
|
+
if (decision.eligible) throw new Error(`flip ${flip.id} remained eligible after the producer re-captured`);
|
|
766
|
+
H.expectCodes(H.codesOf(accepted, decision), flip.expect, flip.id);
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
throw new H.InfraError(`unknown flip kind ${flip.kind}`);
|
|
770
|
+
} finally {
|
|
771
|
+
H.removeRepo(repo.root);
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// ---- Regression: the existing derive and checker APIs are unchanged on the fixed vectors ------------------------
|
|
777
|
+
|
|
778
|
+
for (const key of ['normative', 'nonnormative', 'code-only', 'nondocument']) {
|
|
779
|
+
const pin = VECTORS.pins[key];
|
|
780
|
+
runner.run(`regression:derive-and-checker-pins-${key}`, [REQ_DERIVE, REQ_CHECKER], (mods) => {
|
|
781
|
+
const files = H.vectorFiles(key);
|
|
782
|
+
const facts = mods[DERIVE].deriveDocFacts({ base: { files: files.base }, candidate: { files: files.candidate }, config: H.DERIVE_CONFIG });
|
|
783
|
+
const obligations = mods[DERIVE].deriveDocObligations(facts.facts);
|
|
784
|
+
const checker = mods[CHECKER].checkDocObligations({ obligations, artifacts: {}, baseline: {}, executionRecords: [], reviewRecords: [] });
|
|
785
|
+
H.sameStable(H.sha256(H.stable(facts)), pin.deriveFactsSha256, 'deriveDocFacts output digest');
|
|
786
|
+
H.sameStable(H.sha256(H.stable(obligations)), pin.deriveObligationsSha256, 'deriveDocObligations output digest');
|
|
787
|
+
H.sameStable(H.sha256(H.stable(checker)), pin.checkerResultsSha256, 'checkDocObligations output digest');
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
runner.run('regression:exported-signatures-unchanged', [REQ_DERIVE, REQ_CHECKER], (mods) => {
|
|
792
|
+
H.sameStable(Object.keys(mods[DERIVE]).sort(), ['deriveDocFacts', 'deriveDocObligations'], 'doc-obligations exports');
|
|
793
|
+
H.sameStable(Object.keys(mods[CHECKER]).sort(), ['checkDocObligations'], 'doc-evidence exports');
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
// ---- Purity: the core evaluation performs no file, process or network activity -----------------------------------
|
|
797
|
+
|
|
798
|
+
runner.run('purity:core-decisions-perform-no-io', ALL_NEW, (mods, leg) => withVector('normative', repo => {
|
|
799
|
+
const r = flow(mods, repo, 'normative');
|
|
800
|
+
requireEligible(r, 'purity base vector');
|
|
801
|
+
const abs = path.join(leg.lib, CORE);
|
|
802
|
+
const log = H.recordingIo(rec => {
|
|
803
|
+
const core = H.freshModule(abs);
|
|
804
|
+
core.checkAcceptedDocEvidence({ snapshot: r.snapshot, accepted: r.accepted, mode: 'verification' });
|
|
805
|
+
core.checkAcceptedDocEvidence({ snapshot: r.snapshot, accepted: r.accepted, mode: 'review' });
|
|
806
|
+
return rec;
|
|
807
|
+
});
|
|
808
|
+
H.sameStable(log.writes, [], 'file writes during core evaluation');
|
|
809
|
+
H.sameStable(log.spawns, [], 'spawned commands during core evaluation');
|
|
810
|
+
H.sameStable(log.net, [], 'network connects during core evaluation');
|
|
811
|
+
}));
|
|
812
|
+
|
|
813
|
+
// ---- Parity: the new modules are byte-identical across canonical and packaged copies ------------------------------
|
|
814
|
+
|
|
815
|
+
for (const file of [PRODUCER, EVENTS, CORE]) {
|
|
816
|
+
runner.pair(`parity:${file}-byte-identical`, [{ where: 'lib', file }], () => {
|
|
817
|
+
const source = H.sha256(fs.readFileSync(path.join(H.LEGS[0].lib, file)));
|
|
818
|
+
const packaged = H.sha256(fs.readFileSync(path.join(H.LEGS[1].lib, file)));
|
|
819
|
+
H.sameStable(source, packaged, `${file} sha256 (source vs packaged)`);
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
runner.run('resolver:captured-export-and-copies-synchronized', [{ where: 'scripts', file: 'project-docs-resolver.js', exports: ['resolveDocsConfigCaptured'] }], (mods, leg) => {
|
|
824
|
+
if (leg.name !== 'source') return;
|
|
825
|
+
const canonical = H.sha256(fs.readFileSync(path.join(H.ROOT, 'harness-everything', 'scripts', 'project-docs-resolver.js')));
|
|
826
|
+
for (const copy of ['to-spec/scripts/project-docs-resolver.js', 'multi-agent-workspace/scripts/project-docs-resolver.js']) {
|
|
827
|
+
H.sameStable(H.sha256(fs.readFileSync(path.join(H.ROOT, copy))), canonical, `${copy} matches the canonical resolver`);
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
const counts = runner.summary();
|
|
832
|
+
if (counts.INFRA > 0) {
|
|
833
|
+
console.error('S2a core suite: infrastructure faults present; these are not assertion results.');
|
|
834
|
+
process.exitCode = 2;
|
|
835
|
+
} else if (counts.FAIL > 0) {
|
|
836
|
+
console.error('S2a core suite: assertion FAIL present (expected on the RED base; GREEN must clear every FAIL).');
|
|
837
|
+
process.exitCode = 1;
|
|
838
|
+
} else {
|
|
839
|
+
console.log('S2a core suite: all assertions PASS.');
|
|
840
|
+
}
|