harness-everything 0.40.0 → 0.41.1
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/.agents/harness-everything/project-docs.json +9 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +51 -0
- package/benchmarks/results/f1-doc-path-baseline/1-source-clean.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/1-source-clean.stdout.json +20 -0
- package/benchmarks/results/f1-doc-path-baseline/2-injected-architecture.md +229 -0
- package/benchmarks/results/f1-doc-path-baseline/2-source-injected.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/2-source-injected.stdout.json +33 -0
- package/benchmarks/results/f1-doc-path-baseline/3-source-reverted.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/3-source-reverted.stdout.json +20 -0
- package/benchmarks/results/f1-doc-path-baseline/4a-packaged-clean.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/4a-packaged-clean.stdout.json +20 -0
- package/benchmarks/results/f1-doc-path-baseline/4b-packaged-injected.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/4b-packaged-injected.stdout.json +33 -0
- package/benchmarks/results/f1-doc-path-baseline/5-source-missing-config.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/5-source-missing-config.stdout.json +15 -0
- package/benchmarks/results/f1-doc-path-baseline/5b-packaged-missing-config.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/5b-packaged-missing-config.stdout.json +15 -0
- package/benchmarks/results/f1-doc-path-baseline/README.md +54 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/1-source-clean.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/1-source-clean.stdout.json +115 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/2-injected-document.md +328 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/2-source-injected.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/2-source-injected.stdout.json +127 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/3-source-reverted.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/3-source-reverted.stdout.json +115 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/4a-packaged-clean.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/4a-packaged-clean.stdout.json +115 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/4b-packaged-injected.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/4b-packaged-injected.stdout.json +127 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/5-source-missing-config.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/5-source-missing-config.stdout.json +15 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/5b-packaged-missing-config.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/5b-packaged-missing-config.stdout.json +15 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/6-supplementary-fixture-ok.input.md +8 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/6-supplementary-fixture-ok.stdout.json +16 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/README.md +34 -0
- package/benchmarks/results/f1-doc-path-baseline/probe-31-docs/after.json +248 -0
- package/benchmarks/results/f1-doc-path-baseline/probe-31-docs/before.json +254 -0
- package/benchmarks/results/f1-doc-path-baseline/probe-31-docs/probe.js +26 -0
- package/benchmarks/results/f2-doc-craft-baseline/README.md +32 -0
- package/benchmarks/results/f2-doc-craft-baseline/arm-f1-report.md +84 -0
- package/benchmarks/results/f2-doc-craft-baseline/arm-f1-task.md +21 -0
- package/benchmarks/results/f2-doc-craft-baseline/arm-f1.diff +19 -0
- package/benchmarks/results/f2-doc-craft-baseline/arm-f2-report.md +110 -0
- package/benchmarks/results/f2-doc-craft-baseline/arm-f2-task.md +21 -0
- package/benchmarks/results/f2-doc-craft-baseline/arm-f2.diff +19 -0
- package/benchmarks/results/f3-derived-views-baseline/README.md +34 -0
- package/benchmarks/results/f3-derived-views-baseline/arm-f2-report.md +43 -0
- package/benchmarks/results/f3-derived-views-baseline/arm-f2-task.md +21 -0
- package/benchmarks/results/f3-derived-views-baseline/arm-f2.diff +21 -0
- package/benchmarks/results/f3-derived-views-baseline/arm-f3-report.md +57 -0
- package/benchmarks/results/f3-derived-views-baseline/arm-f3-task.md +21 -0
- package/benchmarks/results/f3-derived-views-baseline/arm-f3.diff +21 -0
- package/benchmarks/results/live-host/claude-325-3b-s2b/README.md +99 -0
- package/benchmarks/results/live-host/claude-325-3b-s2b/capture.sanitized.jsonl +26 -0
- package/ci/archive/325-prep/README.md +71 -0
- package/ci/{prep → archive/325-prep}/doc-reconciliation-red.js +9 -7
- package/ci/archive/325-prep/f4-expected-red.json +34 -0
- package/ci/archive/325-prep/historical-qualification.test.js +143 -0
- package/ci/fixtures/doc-obligations/reconciliation-prep/cases.json +17 -4
- package/ci/fixtures/doc-obligations/reconciliation-prep/pending-red.json +8 -56
- package/ci/fixtures/doc-obligations/reconciliation-prep/trust-contract.json +2 -2
- package/ci/fixtures/doc-obligations/reconciliation-s1/cases.json +17 -5
- package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/harness.js +415 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/off-handoff-golden.json +16 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-post-tool-use-failure.json +11 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-post-tool-use-success-minimal.json +9 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-post-tool-use-success.json +13 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-subagent-start.json +7 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-subagent-stop-freetext-reject.json +11 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-subagent-stop-reject.json +11 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-subagent-stop.json +11 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-task-result.json +19 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/off-capture-only.json +99 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/off-legacy.json +67 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/on-concurrent-writers.json +62 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/on-lookalikes-runtime-mutations.json +67 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/on-malformed.json +85 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/on-normal-session.json +74 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/on-runtime-drift-later-npm-test.json +58 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/on-valid-direct-quoted-review.json +85 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/f4/cases.json +2267 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/wiring/cases.json +1110 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/wiring/legacy-off-golden.json +674 -0
- package/ci/lib/doc-reconciliation-file-spy.js +23 -0
- package/ci/lib/doc-reconciliation-prep-fixtures.js +136 -10
- package/ci/lib/doc-reconciliation-qualification-preload.js +37 -0
- package/ci/lib/doc-reconciliation-replay.js +104 -0
- package/ci/lib/doc-reconciliation-staged-tree.js +102 -0
- package/ci/mechanism-2p-opencode-v2-entrypoint.test.js +179 -0
- package/ci/mechanism-325-3b-preparation.test.js +27 -101
- package/ci/mechanism-325-3b-s2b-capture.test.js +561 -0
- package/ci/mechanism-325-3b-s2b-wiring.test.js +668 -0
- package/ci/mechanism-325-f1-doc-check.test.js +674 -0
- package/ci/mechanism-325-f2-doc-craft.test.js +597 -0
- package/ci/mechanism-325-f3-derived-views.test.js +798 -0
- package/ci/mechanism-325-f4-invariants.test.js +606 -0
- package/ci/mechanism-325-f4-projections.test.js +382 -0
- package/ci/mechanism-325-f4-qualification.test.js +228 -0
- package/ci/mechanism-325-f4-trust-boundary.test.js +880 -0
- package/ci/mechanism-365-step-edit-accounting.test.js +395 -0
- package/ci/mutation-check.js +26 -3
- package/docs/architecture.md +5 -0
- package/docs/doc-workflow-reconciliation.md +331 -4
- package/docs/document-lifecycle.md +11 -0
- package/docs/governed-index.md +20 -0
- package/docs/platform-capabilities.md +22 -2
- package/grill-with-docs/SKILL.md +1 -1
- package/grill-with-docs/references/session-playbook.md +4 -0
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/references/doc-check.md +186 -0
- package/harness-everything/references/documentation/_shared/maintenance.md +41 -0
- package/harness-everything/references/documentation/_shared/writing-craft.md +59 -0
- package/harness-everything/references/triage-and-tiers.md +4 -0
- package/harness-everything/scripts/doc-check.js +442 -0
- package/harness-everything/scripts/doc-lifecycle.js +3 -0
- package/hooks/hooks.json +46 -0
- package/hooks/scripts/lib/doc-reconciliation-capture.sanitized.jsonl +26 -0
- package/hooks/scripts/lib/doc-reconciliation-pilot.js +171 -2
- package/hooks/scripts/lib/doc-reconciliation-provider.js +1059 -0
- package/hooks/scripts/lib/doc-reconciliation-qualification.json +34 -0
- package/hooks/scripts/lib/workflow-obligations.js +7 -0
- package/hooks/scripts/state-persist.js +105 -3
- package/hooks/scripts/workflow-stop-gate.js +7 -2
- package/opencode-plugin/README.md +15 -0
- package/opencode-plugin/index.mjs +77 -0
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-capture.sanitized.jsonl +26 -0
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-pilot.js +171 -2
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-provider.js +1059 -0
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-qualification.json +34 -0
- package/plugins/harness-everything/hooks/scripts/lib/workflow-obligations.js +7 -0
- package/plugins/harness-everything/hooks/scripts/state-persist.js +105 -3
- package/plugins/harness-everything/hooks/scripts/workflow-stop-gate.js +7 -2
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/grill-with-docs/SKILL.md +1 -1
- package/plugins/harness-everything/skills/grill-with-docs/references/session-playbook.md +4 -0
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/references/doc-check.md +186 -0
- package/plugins/harness-everything/skills/harness-everything/references/documentation/_shared/maintenance.md +41 -0
- package/plugins/harness-everything/skills/harness-everything/references/documentation/_shared/writing-craft.md +59 -0
- package/plugins/harness-everything/skills/harness-everything/references/triage-and-tiers.md +4 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-check.js +442 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-lifecycle.js +3 -0
- package/plugins/harness-everything/skills/repo-docs/SKILL.md +1 -1
- package/plugins/harness-everything/skills/repo-docs/references/process-guide.md +4 -0
- package/plugins/harness-everything/skills/to-spec/SKILL.md +1 -1
- package/plugins/harness-everything/skills/to-spec/references/process.md +4 -0
- package/plugins/harness-everything/skills/to-tickets/SKILL.md +1 -1
- package/plugins/harness-everything/skills/to-tickets/references/publishing-and-templates.md +4 -0
- package/repo-docs/SKILL.md +1 -1
- package/repo-docs/references/process-guide.md +4 -0
- package/sbom.cdx.json +7 -7
- package/to-spec/SKILL.md +1 -1
- package/to-spec/references/process.md +4 -0
- package/to-tickets/SKILL.md +1 -1
- package/to-tickets/references/publishing-and-templates.md +4 -0
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// #325 F4-T12: the I1–I4 invariant matrix over the C1 corpus (F4-Z1 §2 gap matrix, §4 replays and mutation table).
|
|
5
|
+
//
|
|
6
|
+
// One cell per (site × invariant) where the Z1 gap matrix gives a verdict. Each cell is owned by exactly one site and
|
|
7
|
+
// runs its own replay variants through the real hook, workflow and Stop entries (source and packaged legs), so a failure
|
|
8
|
+
// at one site cannot mask another. A cell is CONTROL when the invariant holds today and RED when it does not; a RED must
|
|
9
|
+
// fail for its stated invariant, never for an infrastructure fault. SKIP is reserved for the case-variant probe on a
|
|
10
|
+
// case-sensitive filesystem and is always reported.
|
|
11
|
+
//
|
|
12
|
+
// Modes:
|
|
13
|
+
// (default) run the matrix; exit 1 while any RED exists. Every RED is an actual failure, never a frozen expectation.
|
|
14
|
+
// --json print a machine-readable summary (used by the mutation runner).
|
|
15
|
+
// --mutants run the mutation table (Z1 §4) on scratch copies; fail if a mutant is not applied, is not detected by
|
|
16
|
+
// its owning cell, or is detected only by a different cell.
|
|
17
|
+
|
|
18
|
+
const crypto = require('crypto');
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const os = require('os');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const { spawnSync } = require('child_process');
|
|
23
|
+
const fx = require(path.join(__dirname, 'lib', 'doc-reconciliation-prep-fixtures'));
|
|
24
|
+
const { stage, EXPORT_FILE, SIBLING_CAPTURE } = require(path.join(__dirname, 'lib', 'doc-reconciliation-staged-tree'));
|
|
25
|
+
const replayHelper = require(path.join(__dirname, 'lib', 'doc-reconciliation-replay'));
|
|
26
|
+
|
|
27
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
28
|
+
const CORPUS = path.join(__dirname, 'fixtures', 'doc-obligations', 'reconciliation-s2b', 'corpus');
|
|
29
|
+
const CASES = path.join(__dirname, 'fixtures', 'doc-obligations', 'reconciliation-s2b', 'f4', 'cases.json');
|
|
30
|
+
const LEGS = ['source', 'packaged'];
|
|
31
|
+
const POS_CASE = fx.readCaseCorpus(CASES).cases.find(item => item.id === 'F4-POS-body-edit');
|
|
32
|
+
const REQUIREMENTS = [
|
|
33
|
+
{ id: 'req-docs', stepType: 'doc-change', summary: 'Correct the checkout retry rule', acceptance: 'the governed reference body is reviewed' },
|
|
34
|
+
{ id: 'req-review', stepType: 'review', summary: 'Review the change', acceptance: 'an independent host review approves it' },
|
|
35
|
+
{ id: 'req-verify', stepType: 'verification', summary: 'Verify the change', acceptance: 'objective doc checks are recorded' },
|
|
36
|
+
];
|
|
37
|
+
const REVIEWER = 'agent-f4t12-reviewer';
|
|
38
|
+
const CANDIDATE = POS_CASE.input.candidateFiles['docs/a.md'];
|
|
39
|
+
const LINT_ARGS = ['check', '--workspace', '.', '--config', '.agents/harness-everything/project-docs.json', '--changed-path', 'docs/a.md'];
|
|
40
|
+
const LINT_TEXT = 'node {{DOC_CHECK}} check --workspace . --config .agents/harness-everything/project-docs.json --changed-path docs/a.md';
|
|
41
|
+
const TOOL = (command, toolUseId, extra = {}) => ({ event: 'PostToolUse', toolName: 'Bash', command, toolUseId, success: true, ...extra });
|
|
42
|
+
const NPM_TEST = { id: 'npm-test', event: 'PostToolUse', toolName: 'Bash', command: 'npm test', toolUseId: 'tool-npm', success: true, checkpointAfter: 'generic' };
|
|
43
|
+
const EDIT = { id: 'edit-check', op: 'editDocCheck', payload: { append: '\n// F4-T12 matrix: one-byte change to the installed doc-check\n' } };
|
|
44
|
+
const VERIFY_BIND = { id: 'verify-bind', op: 'binding', payload: { stepId: 'req-verify', bindingId: 'verification-loop', disposition: 'loaded', evidence: 'verification loop skill located' }, expectExit: { on: [0, 2] } };
|
|
45
|
+
const REVIEW_PASS = { id: 'review-pass', op: 'step', payload: { stepId: 'req-review', disposition: 'pass', evidence: 'independent reviewer approved' }, expectExit: { on: [0, 2] } };
|
|
46
|
+
const VERIFY_PASS = { id: 'verify-pass', op: 'step', payload: { stepId: 'req-verify', disposition: 'pass', evidence: 'objective doc check recorded' }, expectExit: { on: [0, 2] } };
|
|
47
|
+
const OBSERVE = { id: 'observe-state', op: 'observeStateTree' };
|
|
48
|
+
const STOP = [{ id: 'stop', op: 'stop' }, { id: 'stop-generic', op: 'stopGeneric' }];
|
|
49
|
+
const APPROVE = `Review verdict: approve\nSummary: The governed document is consistent.\nReviewed artifact: docs/a.md sha256:${sha(CANDIDATE)}`;
|
|
50
|
+
const FAKE_REPORT = JSON.stringify({ schema: 'harness.doc-check.check.v1', command: 'harness.doc-check.check.v1', lint: { exit: 0, evidence: {}, diagnostics: [] }, links: { checked: 1, broken: [] }, coverage: { unsupported: [] }, reminders: [], capabilities: { 'doc-lint': { state: 'pass', checkedPaths: ['docs/a.md'], skipped: [] }, 'link-check': { state: 'pass', checkedPaths: ['docs/a.md'], skipped: [] } } }, null, 2) + '\n';
|
|
51
|
+
|
|
52
|
+
function sha(text) { return crypto.createHash('sha256').update(text).digest('hex'); }
|
|
53
|
+
const clone = value => JSON.parse(JSON.stringify(value));
|
|
54
|
+
// Content digests of the state files the observer parses, and the capture journal. An in-place change is a change even when
|
|
55
|
+
// the path list is the same, so S1-I2 compares these as well as the paths.
|
|
56
|
+
const STATE_KEYS = ['workflow', 'obligations', 'handoff', 'capture'];
|
|
57
|
+
const digestOf = value => sha(JSON.stringify(value ?? null));
|
|
58
|
+
const stateDigests = state => Object.fromEntries(STATE_KEYS.map(key => [key, digestOf(state[key])]));
|
|
59
|
+
// Paths added, and digests changed, between two observations of one state tree.
|
|
60
|
+
const hookDelta = (before, after) => ({
|
|
61
|
+
added: after.stateFiles.filter(file => !before.stateFiles.includes(file)),
|
|
62
|
+
changed: Object.keys(before.digests).filter(key => before.digests[key] !== after.digests[key]),
|
|
63
|
+
});
|
|
64
|
+
const streamFile = id => JSON.parse(fs.readFileSync(path.join(CORPUS, `${id}.json`), 'utf8'));
|
|
65
|
+
|
|
66
|
+
// Host probes. The case-variant check runs only where a case-variant spelling of the installed file resolves to it.
|
|
67
|
+
function caseVariantOf(file) { return path.join(path.dirname(file), path.basename(file).toUpperCase()); }
|
|
68
|
+
function caseVariantProbe(installed) {
|
|
69
|
+
const variant = caseVariantOf(installed);
|
|
70
|
+
if (!fs.existsSync(variant)) return { available: false, reason: 'the case-variant spelling does not resolve on this filesystem (case-sensitive host)' };
|
|
71
|
+
try {
|
|
72
|
+
return fs.realpathSync.native(variant) === fs.realpathSync.native(installed)
|
|
73
|
+
? { available: true, command: `node ${variant} check --workspace . --config .agents/harness-everything/project-docs.json --changed-path docs/a.md` }
|
|
74
|
+
: { available: false, reason: 'the case-variant spelling does not resolve to the installed file' };
|
|
75
|
+
} catch (_) { return { available: false, reason: 'the case-variant spelling could not be resolved' }; }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// A byte-identical copy of the installed doc-check outside the install, with a valid-looking report: only the realpath
|
|
79
|
+
// decision (I1) can refuse it, because the pin accepts identical bytes.
|
|
80
|
+
const OUTSIDE_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'f4t12-outside-'));
|
|
81
|
+
const OUTSIDE_COPY = path.join(OUTSIDE_DIR, 'harness-everything', 'scripts', 'doc-check.js');
|
|
82
|
+
fs.mkdirSync(path.dirname(OUTSIDE_COPY), { recursive: true });
|
|
83
|
+
fs.copyFileSync(fx.DISTRIBUTIONS.source.docCheck, OUTSIDE_COPY);
|
|
84
|
+
|
|
85
|
+
function tracePreloader(file, trace) {
|
|
86
|
+
fs.writeFileSync(file, `const M=require('module'), fs=require('fs'); const old=M._load; M._load=function(id,p,m){const x=old.apply(this,arguments);if(typeof id==='string'&&/(doc-reconciliation|state-persist|workflow-disposition|stop-gate)/.test(id))fs.appendFileSync(${JSON.stringify(trace)},JSON.stringify({module:id,requester:m&&m.filename})+'\\n');return x};\n`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// A variant is a transformation of a stream's events and of the action sequence around them.
|
|
90
|
+
const VARIANTS = {
|
|
91
|
+
base: {},
|
|
92
|
+
'candidates-only': { only: event => /doc-check/.test(event.command || '') && !/npm test/.test(event.command || '') },
|
|
93
|
+
'pre-edit': { preEdit: true },
|
|
94
|
+
'post-edit': { postEdit: true },
|
|
95
|
+
review: { review: true },
|
|
96
|
+
'review-pre-edit': { review: true, preEdit: true },
|
|
97
|
+
'review-post-edit': { review: true, postEdit: true },
|
|
98
|
+
executor: { review: true, executor: true },
|
|
99
|
+
'quoted-only': { only: event => !/"/.test(event.command || '') || event.id === 'event-02' },
|
|
100
|
+
'copy-only': { only: () => false, copyOutside: true },
|
|
101
|
+
'case-only': { only: () => false, caseVariant: true },
|
|
102
|
+
'direct-only': { only: event => event.id === 'event-01' },
|
|
103
|
+
drift: { driftEdit: true },
|
|
104
|
+
'no-post-check': { noPostCheck: true },
|
|
105
|
+
'drift-no-post-check': { driftEdit: true, noPostCheck: true },
|
|
106
|
+
'base-snapshot': { snapshot: true },
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
function eventAction(event, index) {
|
|
110
|
+
const id = `ev-${String(index + 1).padStart(2, '0')}`;
|
|
111
|
+
const actions = [];
|
|
112
|
+
if (event.runArgs) actions.push({ id: `${id}-run`, op: 'hostRun', payload: { command: 'doc-check', args: event.runArgs } });
|
|
113
|
+
const payload = { event: event.event };
|
|
114
|
+
if (event.toolName) payload.toolName = event.toolName;
|
|
115
|
+
if (event.command !== undefined) payload.command = event.command;
|
|
116
|
+
if (event.toolUseId) payload.toolUseId = event.toolUseId;
|
|
117
|
+
if (event.runArgs && event.event === 'PostToolUse') payload.fromRun = `${id}-run`;
|
|
118
|
+
if (event.event === 'PostToolUse') payload.response = { stdout: event.runArgs ? 'doc-check completed' : 'completed', stderr: '', interrupted: false };
|
|
119
|
+
if (event.event === 'PostToolUseFailure') { payload.error = 'stable fixture failure'; payload.isInterrupt = false; }
|
|
120
|
+
if (event.event === 'SubagentStart' || event.event === 'SubagentStop') { payload.agentId = event.agentId || `agent-${index + 1}`; payload.agentType = 'reviewer'; }
|
|
121
|
+
if (event.event === 'SubagentStop') payload.lastAssistantMessage = event.lastAssistantMessage || 'APPROVE: reviewed the governed document.';
|
|
122
|
+
actions.push({ id, op: 'hookDeliver', payload });
|
|
123
|
+
return actions;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const isCandidate = event => /doc-check/.test(event.command || '') || Boolean(event.runArgs);
|
|
127
|
+
|
|
128
|
+
// Builds the definition for one (stream, variant) pair. The stream's events are the corpus data, unchanged.
|
|
129
|
+
function definitionFor(label, streamEvents, variant, installed) {
|
|
130
|
+
const spec = VARIANTS[variant];
|
|
131
|
+
const events = clone(streamEvents).filter(event => (spec.only ? spec.only(event) : true));
|
|
132
|
+
const review = Boolean(spec.review);
|
|
133
|
+
const actions = [
|
|
134
|
+
{ id: 'route', op: 'route', payload: { prompt: 'Update docs/a.md, review it independently, and verify it.' } },
|
|
135
|
+
{ id: 'plan', op: 'plan', payload: { requirements: '$requirements', strategy: 'iterative-single', evidence: 'bounded documentation loop' } },
|
|
136
|
+
{ id: 'start', op: 'start', payload: {} },
|
|
137
|
+
{ id: 'doc-pass', op: 'step', payload: { stepId: 'req-docs', disposition: 'pass', evidence: 'sentence corrected' } },
|
|
138
|
+
];
|
|
139
|
+
if (!review) actions.push(clone(VERIFY_BIND));
|
|
140
|
+
if (spec.snapshot) actions.push({ id: 'observe-pre', op: 'observeStateTree' });
|
|
141
|
+
const candidateIndex = events.findIndex(isCandidate);
|
|
142
|
+
let reviewInserted = false;
|
|
143
|
+
events.forEach((event, index) => {
|
|
144
|
+
if (spec.preEdit && index === candidateIndex) actions.push(clone(EDIT));
|
|
145
|
+
if (review && event.event === 'SubagentStop') {
|
|
146
|
+
event = { ...event, agentId: REVIEWER, lastAssistantMessage: APPROVE };
|
|
147
|
+
actions.push({ id: 'agent-surface', op: 'hookDeliver', payload: { event: 'PostToolUse', toolName: 'Agent', command: 'agent-launch', toolUseId: 'toolu_f4t12_agent', response: { stdout: '', stderr: '', interrupted: false, agentId: REVIEWER } } });
|
|
148
|
+
if (spec.executor) actions.push({ id: 'executor-run', op: 'hookDeliver', payload: { event: 'PostToolUse', toolName: 'Bash', command: LINT_TEXT, toolUseId: 'toolu_f4t12_exec', agentId: REVIEWER, response: { stdout: 'completed', stderr: '', interrupted: false } } });
|
|
149
|
+
reviewInserted = true;
|
|
150
|
+
}
|
|
151
|
+
actions.push(...eventAction(event, index));
|
|
152
|
+
if (event.checkpointAfter === 'generic') actions.push({ id: 'observe-generic', op: 'observeStateTree' });
|
|
153
|
+
if (spec.postEdit && index === candidateIndex) actions.push(clone(EDIT));
|
|
154
|
+
});
|
|
155
|
+
if (review && !reviewInserted) throw new Error(`${label}: review variant needs a SubagentStop event`);
|
|
156
|
+
if (spec.copyOutside) actions.push({ id: 'copy-run', op: 'hookDeliver', payload: { event: 'PostToolUse', toolName: 'Bash', command: `node ${OUTSIDE_COPY} check --workspace . --config .agents/harness-everything/project-docs.json --changed-path docs/a.md`, toolUseId: 'toolu_f4t12_copy', response: { stdout: FAKE_REPORT, stderr: '', interrupted: false } } });
|
|
157
|
+
if (spec.caseVariant) {
|
|
158
|
+
const probe = caseVariantProbe(installed);
|
|
159
|
+
if (probe.available) {
|
|
160
|
+
actions.push({ id: 'case-run', op: 'hostRun', payload: { command: 'doc-check', args: LINT_ARGS } });
|
|
161
|
+
actions.push({ id: 'case-post', op: 'hookDeliver', payload: { event: 'PostToolUse', command: probe.command, toolUseId: 'toolu_f4t12_case', fromRun: 'case-run' } });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (spec.driftEdit) actions.push(clone(EDIT));
|
|
165
|
+
if (review) actions.push(clone(REVIEW_PASS), clone(VERIFY_BIND));
|
|
166
|
+
actions.push(clone(OBSERVE));
|
|
167
|
+
if (!spec.noPostCheck) actions.push(clone(VERIFY_PASS), { id: 'observe-after', op: 'observeStateTree' });
|
|
168
|
+
actions.push(...clone(STOP));
|
|
169
|
+
if (spec.noPostCheck) actions.push({ id: 'observe-after', op: 'observeStateTree' });
|
|
170
|
+
return { id: label, requirements: clone(review ? REQUIREMENTS : POS_CASE.requirements), actions, input: clone(POS_CASE.input) };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Synthetic streams for the drift and withhold replays (Z1 §4 "runtime drift" and "malformed" rows). They use the corpus
|
|
174
|
+
// event shape, so the same builder runs them.
|
|
175
|
+
const SYNTHETIC = {
|
|
176
|
+
'lookalike-npm-drift': [{ id: 'event-01', event: 'PostToolUse', toolName: 'Bash', command: 'node ./scripts/doc-check.js check', toolUseId: 'tool-1', success: true }, clone(NPM_TEST)],
|
|
177
|
+
'malformed-npm-drift': [{ id: 'event-01', event: 'PostToolUse', toolName: 'Bash', command: `sh -c "node ${'{{DOC_CHECK}}'} check"`, toolUseId: 'tool-1', success: true }, clone(NPM_TEST)],
|
|
178
|
+
'runtime-npm-drift': [streamFile('on-runtime-drift-later-npm-test').events[0], clone(NPM_TEST)],
|
|
179
|
+
'generic-doc-runtime-drift': [clone(NPM_TEST), streamFile('on-runtime-drift-later-npm-test').events[0]],
|
|
180
|
+
'generic-edit-doc-runtime-drift': [clone(NPM_TEST), { id: 'edit-doc', event: 'PostToolUse', toolName: 'Edit', command: 'docs/a.md', toolUseId: 'tool-edit-doc', success: true }, streamFile('on-runtime-drift-later-npm-test').events[0]],
|
|
181
|
+
'doc-only-runtime-drift': [streamFile('on-runtime-drift-later-npm-test').events[0]],
|
|
182
|
+
};
|
|
183
|
+
function streamEvents(id) { return SYNTHETIC[id] ? SYNTHETIC[id] : streamFile(id).events; }
|
|
184
|
+
|
|
185
|
+
// Preserved corpus streams and their mode, as the C1 helper runs them.
|
|
186
|
+
const STREAM_MODE = {
|
|
187
|
+
'off-capture-only': 'off', 'off-legacy': 'off', 'on-normal-session': 'on', 'on-valid-direct-quoted-review': 'on',
|
|
188
|
+
'on-malformed': 'on', 'on-lookalikes-runtime-mutations': 'on', 'on-runtime-drift-later-npm-test': 'on', 'on-concurrent-writers': 'on',
|
|
189
|
+
'generic-doc-runtime-drift': 'on', 'generic-edit-doc-runtime-drift': 'on', 'doc-only-runtime-drift': 'on',
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const traceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'f4t12-trace-'));
|
|
193
|
+
const runs = new Map();
|
|
194
|
+
|
|
195
|
+
function observe(definition, root, mode, leg, modules) {
|
|
196
|
+
const state = (root.actions['observe-state'] && root.actions['observe-state'].state) || {};
|
|
197
|
+
const json = (root.actions['observe-state'] && root.actions['observe-state'].json) || {};
|
|
198
|
+
const records = (state.capture && state.capture.records) || [];
|
|
199
|
+
return {
|
|
200
|
+
leg, mode,
|
|
201
|
+
faults: fx.infrastructureFaults(definition, root, mode).map(item => `${item.actionId}: ${item.message}`),
|
|
202
|
+
pilot: root.pilot || null,
|
|
203
|
+
handoff: state.handoff || {},
|
|
204
|
+
preHook: root.actions['observe-pre'] ? { stateFiles: root.actions['observe-pre'].json.stateFiles, digests: stateDigests(root.actions['observe-pre'].state || {}) } : null,
|
|
205
|
+
obligations: state.obligations || null,
|
|
206
|
+
stateFiles: json.stateFiles || [],
|
|
207
|
+
digests: stateDigests(state),
|
|
208
|
+
after: (root.actions['observe-after'] && root.actions['observe-after'].state && root.actions['observe-after'].state.handoff) || null,
|
|
209
|
+
captures: records,
|
|
210
|
+
modules,
|
|
211
|
+
verify: { exit: root.actions['verify-pass'] && root.actions['verify-pass'].exitCode, stdout: root.actions['verify-pass'] && root.actions['verify-pass'].stdout },
|
|
212
|
+
review: { exit: root.actions['review-pass'] && root.actions['review-pass'].exitCode },
|
|
213
|
+
genericCheckpoint: (root.actions['observe-generic'] && root.actions['observe-generic'].state && root.actions['observe-generic'].state.handoff) || null,
|
|
214
|
+
stop: { exit: root.actions.stop && root.actions.stop.exitCode, stderr: (root.actions.stop && root.actions.stop.stderr) || '' },
|
|
215
|
+
generic: { exit: root.actions['stop-generic'] && root.actions['stop-generic'].exitCode, stderr: (root.actions['stop-generic'] && root.actions['stop-generic'].stderr) || '' },
|
|
216
|
+
rootActions: Object.keys(root.actions),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// The shipped qualification export and its sibling capture are copied into each staged tree (the staged tree starts without
|
|
221
|
+
// them), so the ON gate opens exactly as it does for users.
|
|
222
|
+
const LIB_OF_LEG = { source: path.join(ROOT, 'hooks', 'scripts', 'lib'), packaged: path.join(ROOT, 'plugins', 'harness-everything', 'hooks', 'scripts', 'lib') };
|
|
223
|
+
|
|
224
|
+
// The provider's verdict and reason codes for every doc-check and review record, read from the staged provider module.
|
|
225
|
+
function verdictsFor(libDir, captures) {
|
|
226
|
+
const out = {};
|
|
227
|
+
const identityRecord = captures.find(record => record && record.observedIdentity);
|
|
228
|
+
if (!identityRecord) return out;
|
|
229
|
+
const provider = require(path.join(libDir, 'doc-reconciliation-provider.js'));
|
|
230
|
+
const identity = identityRecord.observedIdentity;
|
|
231
|
+
const origin = provider.createClaudeOriginProvider({ hostId: 'claude-code', sessionId: identityRecord.session_id, workspaceRoot: identityRecord.cwd, workflowId: identity.workflowId, workflowRevision: identity.workflowRevision, taskId: 'task-f4t12', principals: { author: identityRecord.session_id, executors: [] }, baseline: identity.baselinePin, captures });
|
|
232
|
+
for (const record of captures) {
|
|
233
|
+
if (!record || !record.ref) continue;
|
|
234
|
+
const references = record.event === 'SubagentStop' ? [record.ref] : (/doc-check/.test(record.command || '') ? [`${record.ref}#doc-lint`] : []);
|
|
235
|
+
for (const reference of references) {
|
|
236
|
+
const response = origin.resolveEvent({ reference, identity: null });
|
|
237
|
+
out[reference] = { status: response.status, codes: (response.diagnostics || []).map(item => item && item.code) };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return out;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function runVariant(leg, streamId, variant, mode) {
|
|
244
|
+
const key = `${leg}|${streamId}|${mode}|${variant}`;
|
|
245
|
+
if (runs.has(key)) return runs.get(key);
|
|
246
|
+
const label = `F4T12-${streamId}-${variant}-${leg}`;
|
|
247
|
+
const staged = stage(leg);
|
|
248
|
+
let result;
|
|
249
|
+
try {
|
|
250
|
+
fs.copyFileSync(path.join(LIB_OF_LEG[leg], EXPORT_FILE), staged.exportFile);
|
|
251
|
+
fs.copyFileSync(path.join(LIB_OF_LEG[leg], SIBLING_CAPTURE), staged.captureFile);
|
|
252
|
+
const definition = definitionFor(label, streamEvents(streamId), variant, staged.dist.docCheck);
|
|
253
|
+
const spy = path.join(traceDir, `${sha(key).slice(0, 12)}-spy.js`);
|
|
254
|
+
const trace = path.join(traceDir, `${sha(key).slice(0, 12)}.jsonl`);
|
|
255
|
+
tracePreloader(spy, trace);
|
|
256
|
+
const previous = process.env.F4_C1_TRACE;
|
|
257
|
+
process.env.F4_C1_TRACE = trace;
|
|
258
|
+
try {
|
|
259
|
+
const root = fx.replayCase(definition, staged.dist, { ...(mode === 'on' ? { pilot: '1' } : {}), nodeArgs: ['--require', spy] });
|
|
260
|
+
const modules = fs.existsSync(trace) ? fs.readFileSync(trace, 'utf8').trim().split('\n').filter(Boolean).map(JSON.parse) : [];
|
|
261
|
+
result = observe(definition, root, mode, leg, modules);
|
|
262
|
+
result.verdicts = verdictsFor(staged.libDir, result.captures);
|
|
263
|
+
result.caseProbe = (VARIANTS[variant] || {}).caseVariant ? caseVariantProbe(staged.dist.docCheck) : null;
|
|
264
|
+
} catch (error) {
|
|
265
|
+
result = { leg, mode, faults: [`replay threw: ${error.message}`], error: true };
|
|
266
|
+
} finally {
|
|
267
|
+
if (previous === undefined) delete process.env.F4_C1_TRACE; else process.env.F4_C1_TRACE = previous;
|
|
268
|
+
}
|
|
269
|
+
} finally {
|
|
270
|
+
staged.cleanup();
|
|
271
|
+
}
|
|
272
|
+
runs.set(key, result);
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const run = (leg, streamId, variant) => runVariant(leg, streamId, variant, STREAM_MODE[streamId] || 'on');
|
|
277
|
+
|
|
278
|
+
// ---- Predicates. Each takes the runs for one leg and returns { pass, actual }. -------------------------------------------
|
|
279
|
+
|
|
280
|
+
const lastVerify = obs => obs.handoff.lastVerifyAt ?? null;
|
|
281
|
+
const creditedOf = obs => (obs.pilot && obs.pilot.creditedCapabilities ? [...obs.pilot.creditedCapabilities].sort() : []);
|
|
282
|
+
const acceptedRefs = obs => (obs.pilot && obs.pilot.acceptedRefs) || [];
|
|
283
|
+
const docRecords = obs => obs.captures.filter(record => record.event === 'PostToolUse' && /doc-check/.test(record.command || ''));
|
|
284
|
+
const quotedRecords = obs => obs.captures.filter(record => record.event === 'PostToolUse' && /node "/.test(record.command || ''));
|
|
285
|
+
const diagCodes = obs => ((obs.pilot && obs.pilot.diagnostics) || []).map(item => (item && item.code) || item);
|
|
286
|
+
|
|
287
|
+
// S1-I2 negative control: an in-place append to a pre-existing state file, on a copy. The path list is unchanged, so only
|
|
288
|
+
// the content digest can report it; a path-only comparison must miss it.
|
|
289
|
+
function inPlaceAppendControl() {
|
|
290
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'f4t4-s1i2-'));
|
|
291
|
+
const file = path.join(dir, 'handoff-state.json');
|
|
292
|
+
fs.writeFileSync(file, '{"lastVerifyAt":1}\n', 'utf8');
|
|
293
|
+
const snap = () => ({ stateFiles: fs.readdirSync(dir), digests: { handoff: sha(fs.readFileSync(file, 'utf8')) } });
|
|
294
|
+
const before = snap();
|
|
295
|
+
fs.appendFileSync(file, '{"lastVerifyAt":2}\n', 'utf8');
|
|
296
|
+
const delta = hookDelta(before, snap());
|
|
297
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
298
|
+
return { caught: delta.changed.join() === 'handoff', pathOnlyMisses: delta.added.length === 0 };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Each cell: site, invariant, the runs it needs (stream, variant), the predicate, and the invariant it states.
|
|
302
|
+
const CELLS = [
|
|
303
|
+
// locks: unresolved — OFF pre-switch binding is retained I2 (A4), not in the A/B/C/R3 table. The delta is taken between
|
|
304
|
+
// observe-pre and observe-state (paths and content digests); the ON leg is the positive control that the delta sees a write.
|
|
305
|
+
{ id: 'S1-I2', site: 'state-persist entry and workspace binding', inv: 'I2', stated: 'the OFF capture-only route adds no file and changes no existing state file or capture record (hook delta); the ON control adds one',
|
|
306
|
+
runs: [['off-capture-only', 'base-snapshot'], ['on-valid-direct-quoted-review', 'base-snapshot']],
|
|
307
|
+
check: r => {
|
|
308
|
+
const off = hookDelta(r['off-capture-only|base-snapshot'].preHook, r['off-capture-only|base-snapshot']);
|
|
309
|
+
const on = hookDelta(r['on-valid-direct-quoted-review|base-snapshot'].preHook, r['on-valid-direct-quoted-review|base-snapshot']);
|
|
310
|
+
const control = inPlaceAppendControl();
|
|
311
|
+
return { pass: off.added.length === 0 && off.changed.length === 0 && on.added.length > 0 && control.caught && control.pathOnlyMisses,
|
|
312
|
+
actual: { offHookAdded: off.added, offHookChanged: off.changed, onControlAdded: on.added.length, control } };
|
|
313
|
+
} },
|
|
314
|
+
// locks: A — "Pilot ON credits valid evidence (no false PASS)"; control: S6-I4
|
|
315
|
+
{ id: 'S2-I1', site: 'state-persist verification', inv: 'I1', stated: 'a refused doc-check candidate never writes the doc-check timestamp, and a valid direct run does',
|
|
316
|
+
runs: [['on-malformed', 'candidates-only'], ['on-valid-direct-quoted-review', 'direct-only']],
|
|
317
|
+
check: r => { const refused = lastVerify(r['on-malformed|candidates-only']); const valid = lastVerify(r['on-valid-direct-quoted-review|direct-only']); return { pass: refused === null && valid !== null, actual: { refused, valid } }; } },
|
|
318
|
+
// locks: C — "unavailable observations ... cannot become a false PASS"; control: S6-I1
|
|
319
|
+
{ id: 'S2-I2', site: 'state-persist verification', inv: 'I2', stated: 'a changed-before-run doc-check never writes the timestamp before pilot acceptance',
|
|
320
|
+
runs: [['on-lookalikes-runtime-mutations', 'pre-edit']],
|
|
321
|
+
check: r => ({ pass: lastVerify(r['on-lookalikes-runtime-mutations|pre-edit']) === null, actual: lastVerify(r['on-lookalikes-runtime-mutations|pre-edit']) }) },
|
|
322
|
+
// locks: R3 — "only affected evidence is revoked"; control: S7-I1
|
|
323
|
+
{ id: 'S2-I3', site: 'state-persist verification', inv: 'I3', stated: 'after a runtime change the doc-check record is withdrawn by its own source tag, the generic record survives, and the workflow Stop still names req-verify',
|
|
324
|
+
runs: [['runtime-npm-drift', 'drift-no-post-check']],
|
|
325
|
+
check: r => {
|
|
326
|
+
const obs = r['runtime-npm-drift|drift-no-post-check'];
|
|
327
|
+
const docBefore = obs.handoff.lastDocVerifyAt ?? null;
|
|
328
|
+
const docAfter = obs.after?.lastDocVerifyAt ?? null;
|
|
329
|
+
const genericBefore = obs.handoff.lastGenericVerifyAt ?? null;
|
|
330
|
+
const pass = typeof docBefore === 'number' && docAfter === null && typeof genericBefore === 'number'
|
|
331
|
+
&& obs.after?.lastVerifyAt === genericBefore && /step:req-verify:pending/.test(obs.stop.stderr);
|
|
332
|
+
return { pass, actual: { docBefore, docAfter, genericBefore, aggregateAfter: obs.after?.lastVerifyAt ?? null, workflowStop: obs.stop.stderr.trim().slice(0, 120) } };
|
|
333
|
+
} },
|
|
334
|
+
// locks: B — "host payload fields are re-validated at assessment time"; control: S6-I4
|
|
335
|
+
{ id: 'S2-I4', site: 'state-persist verification', inv: 'I4', stated: 'state-persist and the provider agree on a safely quoted doc-check invocation: the timestamp is set exactly when the provider accepts that record',
|
|
336
|
+
runs: [['on-valid-direct-quoted-review', 'quoted-only']],
|
|
337
|
+
check: r => { const obs = r['on-valid-direct-quoted-review|quoted-only']; const quoted = quotedRecords(obs); const accepted = quoted.length > 0 && acceptedRefs(obs).some(ref => ref.startsWith(quoted[0].ref)); return { pass: (lastVerify(obs) !== null) === accepted, actual: { timestamp: lastVerify(obs) !== null, providerAccepted: accepted } }; } },
|
|
338
|
+
// locks: B — "host payload fields are re-validated at assessment time"; control: S6-I4
|
|
339
|
+
{ id: 'S3-I1', site: 'capture builder and event/journal writer', inv: 'I1', stated: 'a wrong-path candidate records no doc-check material (no digest, no stdout)',
|
|
340
|
+
runs: [['on-lookalikes-runtime-mutations', 'base']],
|
|
341
|
+
check: r => { const wrong = r['on-lookalikes-runtime-mutations|base'].captures.find(record => /\.\/scripts\/doc-check\.js/.test(record.command || '')); return { pass: Boolean(wrong) && !wrong.implementationDigest && typeof wrong.stdout !== 'string', actual: wrong ? { digest: Boolean(wrong.implementationDigest), stdout: typeof wrong.stdout === 'string' } : 'no record' }; } },
|
|
342
|
+
// locks: unresolved — OFF capture-only writes no record before the switch: retained I2 (A4), not in the A/B/C/R3 table
|
|
343
|
+
{ id: 'S3-I2', site: 'capture builder and event/journal writer', inv: 'I2', stated: 'the OFF capture-only route writes no capture record before the pilot switch',
|
|
344
|
+
runs: [['off-capture-only', 'base']],
|
|
345
|
+
check: r => ({ pass: r['off-capture-only|base'].captures.length === 0, actual: r['off-capture-only|base'].captures.length }) },
|
|
346
|
+
// locks: B — "host payload fields are re-validated at assessment time"; control: S6-I4
|
|
347
|
+
{ id: 'S3-I3', site: 'capture builder and event/journal writer', inv: 'I3', stated: 'capture records keep ref, session and tool correlation (concurrent writers are not covered by this cell)',
|
|
348
|
+
runs: [['on-valid-direct-quoted-review', 'review']],
|
|
349
|
+
check: r => { const records = r['on-valid-direct-quoted-review|review'].captures.filter(record => record.event === 'PostToolUse'); const ok = records.length > 0 && records.every(record => record.ref && record.session_id && record.tool_use_id); return { pass: ok, actual: records.map(record => ({ ref: Boolean(record.ref), session: Boolean(record.session_id), tool: Boolean(record.tool_use_id) })) }; } },
|
|
350
|
+
// locks: B — "host payload fields are re-validated at assessment time"; control: S6-I1
|
|
351
|
+
{ id: 'S3-I4', site: 'capture builder and event/journal writer', inv: 'I4', stated: 'a candidate with changed bytes retains no stdout or digest for the pinned check',
|
|
352
|
+
runs: [['on-valid-direct-quoted-review', 'review-pre-edit']],
|
|
353
|
+
check: r => { const records = docRecords(r['on-valid-direct-quoted-review|review-pre-edit']); return { pass: records.length > 0 && records.every(record => typeof record.stdout !== 'string'), actual: records.map(record => ({ stdout: typeof record.stdout === 'string', digest: Boolean(record.implementationDigest) })) }; } },
|
|
354
|
+
// locks: B — "copied-record replay tests are required negatives"; control: S6-I4
|
|
355
|
+
{ id: 'S4-I1', site: 'parseRegisteredCheck / registeredInstalledCheck callers', inv: 'I1', stated: 'a byte-identical copy outside the install is not the installed runtime and is not accepted',
|
|
356
|
+
runs: [['on-valid-direct-quoted-review', 'copy-only']],
|
|
357
|
+
check: r => { const obs = r['on-valid-direct-quoted-review|copy-only']; const copy = obs.captures.find(record => (record.command || '').includes(OUTSIDE_COPY)); const accepted = Boolean(copy) && acceptedRefs(obs).some(ref => ref.startsWith(copy.ref)); return { pass: Boolean(copy) && !accepted, actual: { copyRecorded: Boolean(copy), accepted } }; } },
|
|
358
|
+
// locks: C — "unavailable observations ... cannot become a false PASS"; control: S6-I4
|
|
359
|
+
{ id: 'S4-I4', site: 'parseRegisteredCheck / registeredInstalledCheck callers', inv: 'I4', stated: 'every explicit malformed candidate is refused with a reason diagnostic',
|
|
360
|
+
runs: [['on-malformed', 'base']],
|
|
361
|
+
check: r => ({ pass: creditedOf(r['on-malformed|base']).length === 0 && diagCodes(r['on-malformed|base']).includes('unregistered-command'), actual: diagCodes(r['on-malformed|base']) }) },
|
|
362
|
+
// locks: A — "Pilot ON credits valid evidence (no false PASS)"; control: S6-I4
|
|
363
|
+
{ id: 'S5-I1', site: 'capture candidate selection', inv: 'I1', stated: 'ordinary Edit, Agent and npm events create no doc-check references',
|
|
364
|
+
runs: [['on-normal-session', 'base']],
|
|
365
|
+
check: r => { const codes = diagCodes(r['on-normal-session|base']); return { pass: !codes.includes('unregistered-command'), actual: codes.filter(code => code === 'unregistered-command').length }; } },
|
|
366
|
+
// locks: A — "Pilot ON credits valid evidence (no false PASS)"; control: S6-I4
|
|
367
|
+
{ id: 'S5-I4', site: 'capture candidate selection', inv: 'I4', stated: 'a grammar-valid candidate spelled with a case variant is offered and accepted, never silently omitted',
|
|
368
|
+
runs: [['on-valid-direct-quoted-review', 'case-only']],
|
|
369
|
+
check: (r, leg) => {
|
|
370
|
+
const obs = r['on-valid-direct-quoted-review|case-only'];
|
|
371
|
+
const probe = obs.caseProbe;
|
|
372
|
+
if (!probe || !probe.available) return { skip: probe ? probe.reason : 'case-variant probe not run' };
|
|
373
|
+
const records = obs.captures.filter(record => record.tool_use_id === 'toolu_f4t12_case');
|
|
374
|
+
const accepted = records.length === 1 && acceptedRefs(obs).some(ref => ref.startsWith(records[0].ref));
|
|
375
|
+
return { pass: accepted, actual: { records: records.length, accepted } };
|
|
376
|
+
} },
|
|
377
|
+
// locks: B — "host payload fields are re-validated at assessment time"; control: S6-I4
|
|
378
|
+
{ id: 'S6-I1', site: 'provider resolveEvent, event adapter and core', inv: 'I1', stated: 'the installed identity decides: a valid direct run is accepted; a post-capture or pre-run change is refused with its own code; an independent approve is accepted; a reviewer who ran evidence is refused with reviewer-not-independent',
|
|
379
|
+
runs: [['on-valid-direct-quoted-review', 'base'], ['on-valid-direct-quoted-review', 'post-edit'], ['on-valid-direct-quoted-review', 'pre-edit'], ['on-valid-direct-quoted-review', 'review'], ['on-valid-direct-quoted-review', 'executor']],
|
|
380
|
+
check: r => {
|
|
381
|
+
const docRef = obs => { const record = obs.captures.find(item => item.event === 'PostToolUse' && /doc-check/.test(item.command || '') && !/"/.test(item.command || '')); return record ? record.ref + '#doc-lint' : null; };
|
|
382
|
+
const verdictOf = (obs, ref) => (ref && obs.verdicts && obs.verdicts[ref]) || { status: 'missing', codes: [] };
|
|
383
|
+
const reviewRef = obs => { const record = obs.captures.find(item => item.event === 'SubagentStop'); return record ? record.ref : null; };
|
|
384
|
+
const base = r['on-valid-direct-quoted-review|base'];
|
|
385
|
+
const post = r['on-valid-direct-quoted-review|post-edit'];
|
|
386
|
+
const pre = r['on-valid-direct-quoted-review|pre-edit'];
|
|
387
|
+
const review = r['on-valid-direct-quoted-review|review'];
|
|
388
|
+
const executor = r['on-valid-direct-quoted-review|executor'];
|
|
389
|
+
const parts = {
|
|
390
|
+
directAccepted: acceptedRefs(base).includes(docRef(base)),
|
|
391
|
+
postEditRefusedByDigest: verdictOf(post, docRef(post)).codes.includes('implementation-digest-mismatch'),
|
|
392
|
+
preEditRefusedByPin: verdictOf(pre, docRef(pre)).codes.includes('implementation-pin-mismatch'),
|
|
393
|
+
independentApproveAccepted: review.review.exit === 0 && verdictOf(review, reviewRef(review)).status === 'accepted',
|
|
394
|
+
executorRefused: executor.review.exit === 2 && verdictOf(executor, reviewRef(executor)).codes.includes('reviewer-not-independent'),
|
|
395
|
+
};
|
|
396
|
+
return { pass: Object.values(parts).every(Boolean), actual: parts };
|
|
397
|
+
} },
|
|
398
|
+
// locks: historical — retired 2026-10-23 (owner decision 5): S6-I3 acceptedEvents schema predicate; see ci/archive/325-prep/README.md
|
|
399
|
+
// locks: A — "Pilot ON credits valid evidence (no false PASS)"; control: S2-I1
|
|
400
|
+
{ id: 'S6-I4', site: 'provider resolveEvent, event adapter and core', inv: 'I4', stated: 'the provider accepts the safely quoted installed invocation',
|
|
401
|
+
runs: [['on-valid-direct-quoted-review', 'review']],
|
|
402
|
+
check: r => { const obs = r['on-valid-direct-quoted-review|review']; const quoted = quotedRecords(obs); return { pass: quoted.length === 1 && acceptedRefs(obs).some(ref => ref.startsWith(quoted[0].ref)), actual: { quotedRecords: quoted.length, accepted: acceptedRefs(obs).length } }; } },
|
|
403
|
+
// locks: R3 — "only affected evidence is revoked"; control: S10-I2
|
|
404
|
+
{ id: 'S7-I1', site: 'assessment withdrawal and pilot report', inv: 'I1', stated: 'withdrawal after a runtime change removes only the doc-check record; ordinary verification survives',
|
|
405
|
+
runs: [['runtime-npm-drift', 'drift']],
|
|
406
|
+
check: r => {
|
|
407
|
+
const obs = r['runtime-npm-drift|drift'];
|
|
408
|
+
const before = obs.handoff;
|
|
409
|
+
const after = obs.after || {};
|
|
410
|
+
return { pass: typeof before.lastVerifyAt === 'number' && after.lastVerifyAt === before.lastVerifyAt
|
|
411
|
+
&& after.lastVerifyExitCode === before.lastVerifyExitCode,
|
|
412
|
+
actual: { before: { lastVerifyAt: before.lastVerifyAt ?? null, lastVerifyExitCode: before.lastVerifyExitCode ?? null },
|
|
413
|
+
after: { lastVerifyAt: after.lastVerifyAt ?? null, lastVerifyExitCode: after.lastVerifyExitCode ?? null } } };
|
|
414
|
+
} },
|
|
415
|
+
// locks: R3 — "only affected evidence is revoked"; control: S7-I1
|
|
416
|
+
{ id: 'S7-I2', site: 'assessment withdrawal and pilot report', inv: 'I2', stated: 'withdrawal runs only for a session with a valid registered doc-check capture; a look-alike session keeps its ordinary timestamp',
|
|
417
|
+
runs: [['lookalike-npm-drift', 'drift']],
|
|
418
|
+
check: r => ({ pass: ((r['lookalike-npm-drift|drift'].after) || {}).lastVerifyAt ?? null !== null, actual: ((r['lookalike-npm-drift|drift'].after) || {}).lastVerifyAt ?? null }) },
|
|
419
|
+
// locks: historical — retired 2026-10-23 (owner decision 5): S7-I3 pilot identity schema predicate; see ci/archive/325-prep/README.md
|
|
420
|
+
// locks: R3 — "only affected evidence is revoked"; control: S7-I2
|
|
421
|
+
{ id: 'S7-I4', site: 'assessment withdrawal and pilot report', inv: 'I4', stated: 'a malformed candidate is not a registered capture, so withdrawal ignores it',
|
|
422
|
+
runs: [['malformed-npm-drift', 'drift']],
|
|
423
|
+
check: r => ({ pass: ((r['malformed-npm-drift|drift'].after) || {}).lastVerifyAt ?? null !== null, actual: ((r['malformed-npm-drift|drift'].after) || {}).lastVerifyAt ?? null }) },
|
|
424
|
+
// locks: unresolved — OFF withholding absence is retained I2 (A4), not in the A/B/C/R3 table
|
|
425
|
+
{ id: 'S8-I2', site: 'workflow withholding', inv: 'I2', stated: 'OFF returns without any pilot withholding: the legacy verify step passes',
|
|
426
|
+
runs: [['off-legacy', 'base']],
|
|
427
|
+
check: r => ({ pass: r['off-legacy|base'].verify.exit === 0, actual: r['off-legacy|base'].verify.exit }) },
|
|
428
|
+
// locks: historical — retired 2026-10-23 (owner decision 5): S8-I3 origin-marker key predicate; see ci/archive/325-prep/README.md
|
|
429
|
+
// locks: unresolved — OFF mounts and early return are an owner decision in A4, not in the A/B/C/R3 table
|
|
430
|
+
{ id: 'S9-I2', site: 'workflow callers and baseline capture', inv: 'I2', stated: 'OFF keeps its mounts and returns early: a legacy OFF workflow performs no pilot assessment and writes no capture (owner decision: mounts stay)',
|
|
431
|
+
runs: [['off-legacy', 'base']],
|
|
432
|
+
check: r => { const obs = r['off-legacy|base']; return { pass: obs.pilot === null && obs.captures.length === 0, actual: { pilot: obs.pilot === null ? null : 'report', captures: obs.captures.length } }; } },
|
|
433
|
+
// locks: R3 — "only affected evidence is revoked"; control: S7-I1
|
|
434
|
+
{ id: 'S10-I2', site: 'generic verification wrapper and runtime CLI', inv: 'I2', stated: 'the generic verifier is unchanged by the pilot switch: a legacy success sets the timestamp',
|
|
435
|
+
runs: [['off-legacy', 'base']],
|
|
436
|
+
check: r => ({ pass: lastVerify(r['off-legacy|base']) !== null, actual: lastVerify(r['off-legacy|base']) }) },
|
|
437
|
+
// locks: R3 — "only affected evidence is revoked"; control: S7-I1
|
|
438
|
+
{ id: 'S10-I3', site: 'generic verification wrapper and runtime CLI', inv: 'I3', stated: 'after the same runtime change the generic record keeps its own source tag, the aggregate equals it, and the generic Stop stays silent',
|
|
439
|
+
runs: [['runtime-npm-drift', 'drift-no-post-check']],
|
|
440
|
+
check: r => {
|
|
441
|
+
const obs = r['runtime-npm-drift|drift-no-post-check'];
|
|
442
|
+
const genericBefore = obs.handoff.lastGenericVerifyAt ?? null;
|
|
443
|
+
const genericAfter = obs.after?.lastGenericVerifyAt ?? null;
|
|
444
|
+
const pass = typeof genericBefore === 'number' && genericAfter === genericBefore && obs.after?.lastVerifyAt === genericBefore
|
|
445
|
+
&& !/\[Verification Reminder\]/.test(obs.generic.stderr);
|
|
446
|
+
return { pass, actual: { genericBefore, genericAfter, aggregateAfter: obs.after?.lastVerifyAt ?? null, genericStop: obs.generic.stderr.trim().slice(0, 120) } };
|
|
447
|
+
} },
|
|
448
|
+
// locks: R3 — "only affected evidence is revoked"; control: doc-only current runtime
|
|
449
|
+
{ id: 'WB-R3b', site: 'assessment withdrawal and current generic evidence', inv: 'I3', stated: 'withdrawal restores generic evidence in either order, keeps it stale after a later edit, and withdraws doc-only evidence',
|
|
450
|
+
runs: [['generic-doc-runtime-drift', 'drift-no-post-check'], ['generic-edit-doc-runtime-drift', 'drift-no-post-check'],
|
|
451
|
+
['doc-only-runtime-drift', 'drift-no-post-check'], ['doc-only-runtime-drift', 'no-post-check']],
|
|
452
|
+
check: r => {
|
|
453
|
+
const genericThenDoc = r['generic-doc-runtime-drift|drift-no-post-check'];
|
|
454
|
+
const editedAfterGeneric = r['generic-edit-doc-runtime-drift|drift-no-post-check'];
|
|
455
|
+
const docOnlyDrift = r['doc-only-runtime-drift|drift-no-post-check'];
|
|
456
|
+
const docOnlyCurrent = r['doc-only-runtime-drift|no-post-check'];
|
|
457
|
+
const genericMatches = obs => obs.genericCheckpoint && typeof obs.genericCheckpoint.lastVerifyAt === 'number'
|
|
458
|
+
&& obs.after?.lastVerifyAt === obs.genericCheckpoint.lastVerifyAt
|
|
459
|
+
&& obs.after?.lastVerifyExitCode === obs.genericCheckpoint.lastVerifyExitCode;
|
|
460
|
+
const genericStaysStale = genericMatches(editedAfterGeneric)
|
|
461
|
+
&& editedAfterGeneric.after.lastVerifyAt < editedAfterGeneric.after.lastEditAt
|
|
462
|
+
&& /verification-after-edit-missing/.test(editedAfterGeneric.stop.stderr);
|
|
463
|
+
const docOnlyWithdrawn = typeof docOnlyDrift.handoff.lastVerifyAt === 'number'
|
|
464
|
+
&& docOnlyDrift.after?.lastVerifyAt === undefined && /step:req-verify:pending/.test(docOnlyDrift.stop.stderr);
|
|
465
|
+
const docOnlyControl = typeof docOnlyCurrent.handoff.lastVerifyAt === 'number'
|
|
466
|
+
&& docOnlyCurrent.after?.lastVerifyAt === docOnlyCurrent.handoff.lastVerifyAt;
|
|
467
|
+
return { pass: genericMatches(genericThenDoc) && genericStaysStale && docOnlyWithdrawn && docOnlyControl,
|
|
468
|
+
actual: { genericThenDoc: { before: genericThenDoc.genericCheckpoint?.lastVerifyAt ?? null, after: genericThenDoc.after?.lastVerifyAt ?? null },
|
|
469
|
+
genericAfterEdit: { verify: editedAfterGeneric.after?.lastVerifyAt ?? null, edit: editedAfterGeneric.after?.lastEditAt ?? null, reminder: editedAfterGeneric.stop.stderr },
|
|
470
|
+
docOnlyDrift: { before: docOnlyDrift.handoff.lastVerifyAt ?? null, after: docOnlyDrift.after?.lastVerifyAt ?? null, stop: docOnlyDrift.stop.stderr },
|
|
471
|
+
docOnlyCurrent: { credit: docOnlyCurrent.pilot?.pilotCredit ?? null, before: docOnlyCurrent.handoff.lastVerifyAt ?? null, after: docOnlyCurrent.after?.lastVerifyAt ?? null } } };
|
|
472
|
+
} },
|
|
473
|
+
// locks: B, C, R3 — "claims alone ... cannot become a false PASS" and "only affected evidence is revoked"; control: lookalike-only
|
|
474
|
+
{ id: 'WB-consumers', site: 'workflow and generic Stop readers', inv: 'I3', stated: 'both Stop paths read current evidence; generic success never satisfies stale document MUSTs, and lookalikes do not trigger withdrawal',
|
|
475
|
+
runs: [['runtime-npm-drift', 'drift-no-post-check'], ['lookalike-npm-drift', 'drift-no-post-check']],
|
|
476
|
+
check: r => {
|
|
477
|
+
const stale = r['runtime-npm-drift|drift-no-post-check'];
|
|
478
|
+
const lookalike = r['lookalike-npm-drift|drift-no-post-check'];
|
|
479
|
+
const expectedStops = obs => /step:req-verify:pending/.test(obs.stop.stderr) && !/\[Verification Reminder\]/.test(obs.generic.stderr);
|
|
480
|
+
const genericPreserved = stale.after?.lastVerifyAt === stale.genericCheckpoint?.lastVerifyAt
|
|
481
|
+
&& stale.pilot?.pilotCredit === false;
|
|
482
|
+
const lookalikePreserved = typeof lookalike.after?.lastVerifyAt === 'number'
|
|
483
|
+
&& lookalike.after.lastVerifyAt === lookalike.handoff.lastVerifyAt;
|
|
484
|
+
return { pass: genericPreserved && lookalikePreserved && expectedStops(stale) && expectedStops(lookalike),
|
|
485
|
+
actual: { stale: { credit: stale.pilot?.pilotCredit ?? null, before: stale.handoff.lastVerifyAt ?? null, after: stale.after?.lastVerifyAt ?? null,
|
|
486
|
+
workflowStop: stale.stop.stderr, genericStop: stale.generic.stderr },
|
|
487
|
+
lookalike: { before: lookalike.handoff.lastVerifyAt ?? null, after: lookalike.after?.lastVerifyAt ?? null,
|
|
488
|
+
workflowStop: lookalike.stop.stderr, genericStop: lookalike.generic.stderr } } };
|
|
489
|
+
} },
|
|
490
|
+
];
|
|
491
|
+
|
|
492
|
+
// ---- Evaluation ---------------------------------------------------------------------------------------------------------
|
|
493
|
+
|
|
494
|
+
function evaluate(cell, leg) {
|
|
495
|
+
const map = {};
|
|
496
|
+
for (const [stream, variant] of cell.runs) map[`${stream}|${variant}`] = run(leg, stream, variant);
|
|
497
|
+
const failed = Object.entries(map).filter(([, obs]) => obs.error || obs.faults.length);
|
|
498
|
+
if (failed.length) return { status: 'INFRA', reason: failed.map(([key, obs]) => `${key}: ${obs.faults.join('; ') || 'replay error'}`).join(' | ') };
|
|
499
|
+
// Variants are keyed by stream and variant; the predicate reads them by the same keys.
|
|
500
|
+
const outcome = cell.check(map, leg);
|
|
501
|
+
if (outcome.skip) return { status: 'SKIP', reason: outcome.skip };
|
|
502
|
+
return { status: outcome.pass ? 'CONTROL' : 'RED', stated: cell.stated, actual: outcome.actual };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Mutation table (F4-Z1 §4). `apply` is a text needle in the source file; the same edit is applied to the packaged mirror.
|
|
506
|
+
// `owner` is the cell that must fail for this mutant. Probes whose target code does not exist yet are declared with
|
|
507
|
+
// `pending`, so the runner reports them as NOT APPLIED instead of hiding them.
|
|
508
|
+
const MUTANTS = [
|
|
509
|
+
{ id: 'M1-realpath-accepts-any-path', file: 'hooks/scripts/lib/doc-reconciliation-provider.js', owner: 'S4-I1',
|
|
510
|
+
from: ' return argv === installed ? { tokens, installed } : null;', to: ' return { tokens, installed: argv };' },
|
|
511
|
+
{ id: 'M2-drop-assessment-digest-check', file: 'hooks/scripts/lib/doc-reconciliation-provider.js', owner: 'S6-I1',
|
|
512
|
+
from: " if (cap.implementationDigest !== fileDigest(registered.installed)) return rejected('implementation-digest-mismatch', 'the installed doc-check digest differs from the one observed at capture; no credit');\n", to: '' },
|
|
513
|
+
{ id: 'M3-drop-executor-exclusion', file: 'hooks/scripts/lib/doc-reconciliation-provider.js', owner: 'S6-I1',
|
|
514
|
+
from: 'function journalExecutors(captures, author) {\n const executors = new Set(textOf(author) ? [author] : []);', to: 'function journalExecutors(captures, author) {\n return textOf(author) ? [author] : [];\n const executors = new Set(textOf(author) ? [author] : []);' },
|
|
515
|
+
{ id: 'M4-drop-shipped-pin-check', file: 'hooks/scripts/lib/doc-reconciliation-provider.js', owner: 'S6-I1',
|
|
516
|
+
from: " if (fileDigest(registered.installed) !== SHIPPED_DOC_CHECK_DIGEST) return rejected('implementation-pin-mismatch', 'the installed doc-check is not the shipped script (canonical digest differs from SHIPPED_DOC_CHECK_DIGEST); no credit');\n", to: '' },
|
|
517
|
+
{ id: 'M5-revert-capture-candidate-filter', file: 'hooks/scripts/lib/doc-reconciliation-pilot.js', owner: 'S5-I1',
|
|
518
|
+
from: '(item.event === \'SubagentStop\' || docCheckCandidate(item))', to: '(item.event === \'SubagentStop\' || item.event === \'PostToolUse\')' },
|
|
519
|
+
{ id: 'M6-revert-state-persist-pin-guard', file: 'hooks/scripts/state-persist.js', owner: 'S2-I2',
|
|
520
|
+
from: ' ? docCheck.fileDigest(installedCheck.installed) === docCheck.SHIPPED_DOC_CHECK_DIGEST', to: ' ? true' },
|
|
521
|
+
{ id: 'P-retain-unpinned-stdout', file: 'hooks/scripts/lib/doc-reconciliation-provider.js', owner: 'S3-I4',
|
|
522
|
+
from: ' if (installedCheck && record.implementationDigest === SHIPPED_DOC_CHECK_DIGEST) {', to: ' if (installedCheck) {' },
|
|
523
|
+
{ id: 'P-state-persist-resolver-caller-removed', file: 'hooks/scripts/state-persist.js', owner: 'S2-I1',
|
|
524
|
+
from: 'const installedCheck = docCheck && docCheck.registeredInstalledCheck(command, cwdOf(payload, root));', to: 'const installedCheck = null;' },
|
|
525
|
+
{ id: 'P-move-switch-after-workspace-bind', pending: 'needs the Packet B entry gate (switch before workspace binding); the current order is not a single edit', owner: 'S1-I2' },
|
|
526
|
+
{ id: 'P-drop-capture-provenance', pending: 'needs source-keyed capture provenance (Packet B); no such field exists to drop', owner: 'S3-I3' },
|
|
527
|
+
{ id: 'P-drop-adapter-provenance-identity', pending: 'the adapter required-field list is shared with the provenance tag introduced by W7; separating it needs Packet A', owner: 'S6-I3' },
|
|
528
|
+
{ id: 'P-delete-generic-timestamps-in-withdrawal', pending: 'the withdrawal already deletes both timestamps (the defect); the probe needs the source-scoped withdrawal of Packet B', owner: 'S7-I2' },
|
|
529
|
+
{ id: 'P-overwrite-concurrent-source-record', pending: 'no source-keyed records exist yet (Packet B)', owner: 'S8-I3' },
|
|
530
|
+
{ id: 'P-bypass-stop-source-selection', pending: 'no Stop source selection exists yet (Packet B); Z1 §4 names a Stop-reader site that Z1 §2 maps to no invariant cell, so this probe has no owner cell yet', owner: 'NO-CELL' },
|
|
531
|
+
];
|
|
532
|
+
|
|
533
|
+
async function main(argv) {
|
|
534
|
+
const json = argv.includes('--json');
|
|
535
|
+
const verbose = argv.includes('--verbose');
|
|
536
|
+
if (argv.includes('--mutants')) return mutantRun();
|
|
537
|
+
const results = {};
|
|
538
|
+
for (const leg of LEGS) for (const cell of CELLS) {
|
|
539
|
+
const outcome = evaluate(cell, leg);
|
|
540
|
+
results[`${cell.id}@${leg}`] = { cell: cell.id, leg, site: cell.site, inv: cell.inv, ...outcome };
|
|
541
|
+
}
|
|
542
|
+
const counts = {};
|
|
543
|
+
for (const item of Object.values(results)) counts[item.status] = (counts[item.status] || 0) + 1;
|
|
544
|
+
if (json) { process.stdout.write(JSON.stringify({ results, counts }) + '\n'); return 0; }
|
|
545
|
+
for (const item of Object.values(results)) {
|
|
546
|
+
const detail = item.status === 'RED' || (verbose && item.status === 'CONTROL') ? `${item.stated}; actual ${JSON.stringify(item.actual).slice(0, 180)}` : item.status === 'SKIP' || item.status === 'INFRA' ? item.reason : (item.stated || '');
|
|
547
|
+
console.log(` ${item.status.padEnd(7)} ${item.cell}@${item.leg} [${item.site} / ${item.inv}] - ${detail}`);
|
|
548
|
+
}
|
|
549
|
+
const reds = Object.values(results).filter(item => item.status === 'RED').map(item => `${item.cell}@${item.leg}`).sort();
|
|
550
|
+
const infra = Object.values(results).filter(item => item.status === 'INFRA').length;
|
|
551
|
+
console.log(`\nF4 invariant matrix: ${counts.CONTROL || 0} CONTROL, ${counts.RED || 0} RED, ${counts.SKIP || 0} SKIP, ${infra} INFRA`);
|
|
552
|
+
console.log(reds.length || infra ? `STATUS: NOT-GREEN (red ${reds.length}, infra ${infra})` : 'STATUS: GREEN');
|
|
553
|
+
return reds.length || infra ? 1 : 0;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Mutation runner: each mutant runs on a fresh export of HEAD with this suite overlaid; the mutant's needles must be applied
|
|
557
|
+
// to both legs, and the owning cell must be the one that changes to RED. A mutant that is not applied, or that is detected
|
|
558
|
+
// only by another cell, fails. Pending probes fail as NOT APPLIED.
|
|
559
|
+
function mutantRun() {
|
|
560
|
+
const base = fs.mkdtempSync(path.join(os.tmpdir(), 'f4t12-mutbase-'));
|
|
561
|
+
const archive = spawnSync('git', ['-C', ROOT, 'archive', 'HEAD'], { encoding: 'buffer', maxBuffer: 512 * 1024 * 1024 });
|
|
562
|
+
if (archive.status !== 0) { console.log('INFRA: git archive HEAD failed'); return 2; }
|
|
563
|
+
spawnSync('tar', ['-x', '-C', base], { input: archive.stdout });
|
|
564
|
+
for (const rel of ['ci/mechanism-325-f4-invariants.test.js', 'ci/lib/doc-reconciliation-prep-fixtures.js', 'ci/lib/doc-reconciliation-replay.js', 'ci/lib/doc-reconciliation-staged-tree.js']) {
|
|
565
|
+
fs.mkdirSync(path.dirname(path.join(base, rel)), { recursive: true });
|
|
566
|
+
fs.copyFileSync(path.join(ROOT, rel), path.join(base, rel));
|
|
567
|
+
}
|
|
568
|
+
const ran = spawnSync(process.execPath, [path.join(base, 'ci', 'mechanism-325-f4-invariants.test.js'), '--json'], { cwd: base, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
|
|
569
|
+
const baseline = JSON.parse(ran.stdout || '{"results":{}}').results;
|
|
570
|
+
let failures = 0;
|
|
571
|
+
const report = [];
|
|
572
|
+
for (const mutant of MUTANTS) {
|
|
573
|
+
if (mutant.pending) { failures += 1; report.push(`NOT APPLIED ${mutant.id} (owner ${mutant.owner}): ${mutant.pending}`); continue; }
|
|
574
|
+
const dir = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'f4t12-mut-')), 'copy');
|
|
575
|
+
spawnSync('cp', ['-R', base, dir]);
|
|
576
|
+
let applied = true;
|
|
577
|
+
for (const rel of [mutant.file, mutant.file.replace('hooks/scripts/', 'plugins/harness-everything/hooks/scripts/')]) {
|
|
578
|
+
const file = path.join(dir, rel);
|
|
579
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
580
|
+
if (text.split(mutant.from).length !== 2) { applied = false; break; }
|
|
581
|
+
fs.writeFileSync(file, text.replace(mutant.from, mutant.to));
|
|
582
|
+
}
|
|
583
|
+
if (!applied) { failures += 1; report.push(`NOT APPLIED ${mutant.id}: the needle is not present exactly once`); fs.rmSync(dir, { recursive: true, force: true }); continue; }
|
|
584
|
+
const out = spawnSync(process.execPath, [path.join(dir, 'ci', 'mechanism-325-f4-invariants.test.js'), '--json'], { cwd: dir, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
|
|
585
|
+
const mutated = JSON.parse(out.stdout || '{"results":{}}').results;
|
|
586
|
+
const changed = [...new Set(Object.values(mutated).filter(item => item.status === 'RED' && (baseline[`${item.cell}@${item.leg}`] || {}).status !== 'RED').map(item => item.cell))];
|
|
587
|
+
const owner = changed.includes(mutant.owner);
|
|
588
|
+
const verdict = owner ? (changed.length === 1 ? 'DETECTED' : 'DETECTED (owner plus others)') : changed.length ? 'WRONG ASSERTION' : 'NOT DETECTED';
|
|
589
|
+
if (!owner) failures += 1;
|
|
590
|
+
report.push(`${verdict.padEnd(18)} ${mutant.id} (owner ${mutant.owner}): ${changed.join(', ') || 'no cell changed'}`);
|
|
591
|
+
fs.rmSync(path.dirname(dir), { recursive: true, force: true });
|
|
592
|
+
}
|
|
593
|
+
console.log(report.join('\n'));
|
|
594
|
+
console.log(failures ? `STATUS: MUTANTS-FAILED (${failures})` : 'STATUS: MUTANTS-DETECTED');
|
|
595
|
+
return failures ? 1 : 0;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (require.main === module) {
|
|
599
|
+
main(process.argv.slice(2)).then(code => {
|
|
600
|
+
fs.rmSync(traceDir, { recursive: true, force: true });
|
|
601
|
+
fs.rmSync(OUTSIDE_DIR, { recursive: true, force: true });
|
|
602
|
+
process.exitCode = code;
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
module.exports = { CELLS, MUTANTS };
|