harness-everything 0.40.0 → 0.41.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/.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 +43 -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/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 +78 -2
- 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 +78 -2
- 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,382 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// #325 Phase 3b F4-T1: satisfiable strict RED for credited reconciliation (hook replay transport).
|
|
5
|
+
//
|
|
6
|
+
// Contract: docs/doc-workflow-reconciliation.md, section "F4: credited reconciliation (Claude-only, pilot ON)".
|
|
7
|
+
// Design: .worktrees/packets/feat-path/A3-advisor-report.md sections 2, 3 and 6.
|
|
8
|
+
//
|
|
9
|
+
// Transport under test. Host events reach the runtime only through the real entries: the doc-check entry runs
|
|
10
|
+
// through hostRun, and each raw payload goes to the real hooks/scripts/state-persist.js through hookDeliver. Replays
|
|
11
|
+
// that are ON run every child with the CI-only qualification preloader (`node --require`). It is not packaged and no
|
|
12
|
+
// production switch reads it. Production replays set no preloader, so the live gate stays unmet.
|
|
13
|
+
//
|
|
14
|
+
// Statuses, as in the S2b mechanism suites:
|
|
15
|
+
// CONTROL must pass now and stay true at GREEN (transport, refusal, OFF legacy, exit codes, non-vacuity).
|
|
16
|
+
// RED must fail now for the stated missing-implementation reason and pass at GREEN.
|
|
17
|
+
// FAIL an unexpected failure, or a RED assertion that passed already (the RED is not real).
|
|
18
|
+
// INFRA a harness crash, a child fault or a fixture fault. Never counted as RED.
|
|
19
|
+
// Strict by default: any RED or FAIL exits 1. `--expect-red` exits 0 only when every CONTROL passes and every RED
|
|
20
|
+
// fails for its stated reason. It is a separate stage and does not weaken the default.
|
|
21
|
+
//
|
|
22
|
+
// The `creditedCapabilities` report field is named here by the test. The contract requires per-capability credit in
|
|
23
|
+
// the report but does not name it; the GREEN schema must match or amend this name in the same change.
|
|
24
|
+
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const os = require('os');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
const { spawnSync } = require('child_process');
|
|
29
|
+
|
|
30
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
31
|
+
const fx = require(path.join(__dirname, 'lib', 'doc-reconciliation-prep-fixtures'));
|
|
32
|
+
const { stage } = require(path.join(__dirname, 'lib', 'doc-reconciliation-staged-tree'));
|
|
33
|
+
const PRELOAD = path.join(__dirname, 'lib', 'doc-reconciliation-qualification-preload.js');
|
|
34
|
+
const F4_CASES = path.join(__dirname, 'fixtures', 'doc-obligations', 'reconciliation-s2b', 'f4', 'cases.json');
|
|
35
|
+
const CAPTURE_DIR = path.join(__dirname, 'fixtures', 'doc-obligations', 'reconciliation-s2b', 'capture');
|
|
36
|
+
const LIVE_REL = path.join('benchmarks', 'results', 'live-host', 'claude-325-3b-s2b');
|
|
37
|
+
|
|
38
|
+
const LEGS = [
|
|
39
|
+
{ id: 'source', scripts: path.join(ROOT, 'hooks', 'scripts'), lib: path.join(ROOT, 'hooks', 'scripts', 'lib') },
|
|
40
|
+
{
|
|
41
|
+
id: 'packaged',
|
|
42
|
+
scripts: path.join(ROOT, 'plugins', 'harness-everything', 'hooks', 'scripts'),
|
|
43
|
+
lib: path.join(ROOT, 'plugins', 'harness-everything', 'hooks', 'scripts', 'lib'),
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
// The fake qualification is the explicit DI object the S2b provider already accepts. Nothing else opens the gate.
|
|
48
|
+
const FAKE_DI = { source: 'fake-test-fixture', faked: true, ref: 'f4-t1-di', toolUseIdPopulated: true, agentIdPopulated: true };
|
|
49
|
+
const CASE = id => fx.readCaseCorpus(F4_CASES).cases.find(item => item.id === id);
|
|
50
|
+
// {{DOC_CHECK}} resolves per leg to that leg's installed doc-check path (F4-T5 conflict 2): a positive run invokes it by absolute path.
|
|
51
|
+
const DOC_CHECK_TOKEN = '{{DOC_CHECK}}';
|
|
52
|
+
const LINT_COMMAND = 'node {{DOC_CHECK}} check --workspace . --config .agents/harness-everything/project-docs.json --changed-path docs/a.md';
|
|
53
|
+
const FAKE_COMMAND = 'node scripts/fake-check.js check --workspace . --config .agents/harness-everything/project-docs.json --changed-path docs/a.md';
|
|
54
|
+
|
|
55
|
+
const RED_IMPLEMENTATION = 'missing-implementation: the origin provider is not wired into the workflow credit boundary; pilot.assess reads no host events, so S1 withholds every document credit';
|
|
56
|
+
const RED_CAPABILITY = 'missing-implementation: the runtime report carries no per-capability credit and no doc-lint/link-check projection exists (A3 section 3)';
|
|
57
|
+
const RED_VERIFY = 'missing-implementation: S1 refuses every document verification step (document verification pending: S1 grants no document credit)';
|
|
58
|
+
|
|
59
|
+
// Journal reads inside a replay: the capture journal is the only host-event record the hooks write.
|
|
60
|
+
const journal = (action, index, key) => ['actions', action, 'state', 'capture', 'records', index, key];
|
|
61
|
+
|
|
62
|
+
// Plan entries: CONTROL asserts behaviour that must hold now and at GREEN, RED asserts the stated missing piece.
|
|
63
|
+
// F4-T2: entries that were RED until I1 and I2b are CONTROL now (status CONTROL, retired: F4-T2). Their RED reason stays in
|
|
64
|
+
// `reason` as the retirement record. No assertion was weakened: each keeps its path, operator and expected value.
|
|
65
|
+
const PLAN = {
|
|
66
|
+
'F4-POS-body-edit@on': [
|
|
67
|
+
{ id: 'pos.run-exit', path: ['actions', 'lint-run', 'exitCode'], operator: 'equals', expected: 0, status: 'CONTROL' },
|
|
68
|
+
{ id: 'pos.journal-event', path: journal('post-lint', 0, 'event'), operator: 'equals', expected: 'PostToolUse', status: 'CONTROL', note: 'the real hook journals the delivered host event' },
|
|
69
|
+
{ id: 'pos.journal-command', path: journal('post-lint', 0, 'command'), operator: 'equals', expected: LINT_COMMAND, status: 'CONTROL' },
|
|
70
|
+
{ id: 'pos.stop-exit', path: ['actions', 'stop', 'exitCode'], operator: 'equals', expected: 0, status: 'CONTROL' },
|
|
71
|
+
{ id: 'pos.stop-generic-exit', path: ['actions', 'stop-generic', 'exitCode'], operator: 'equals', expected: 0, status: 'CONTROL' },
|
|
72
|
+
{ id: 'pos.verify-pass', path: ['actions', 'verify-pass', 'exitCode'], operator: 'equals', expected: 0, status: 'CONTROL', retired: 'F4-T2', reason: RED_VERIFY, note: 'a body-only edit with current lint and link evidence passes verification' },
|
|
73
|
+
{ id: 'pos.pilot-credit', path: ['pilot', 'pilotCredit'], operator: 'equals', expected: true, status: 'CONTROL', retired: 'F4-T2', reason: RED_IMPLEMENTATION },
|
|
74
|
+
{ id: 'pos.lint-credited', path: ['pilot', 'creditedCapabilities'], operator: 'includes', expected: 'doc-lint', status: 'CONTROL', retired: 'F4-T2', reason: RED_CAPABILITY },
|
|
75
|
+
{ id: 'pos.link-credited', path: ['pilot', 'creditedCapabilities'], operator: 'includes', expected: 'link-check', status: 'CONTROL', retired: 'F4-T2', reason: RED_CAPABILITY },
|
|
76
|
+
{ id: 'pos.stop-satisfied', path: ['actions', 'stop', 'state', 'workflow', 'state'], operator: 'equals', expected: 'satisfied', status: 'CONTROL', retired: 'F4-T2', reason: RED_IMPLEMENTATION },
|
|
77
|
+
],
|
|
78
|
+
// Production control (F4-T3 decision 2): no export in the staged tree -> no credit. The staged copy has no qualification
|
|
79
|
+
// export and no preloader, so the transport still journals and nothing may be credited.
|
|
80
|
+
'F4-POS-body-edit@production': [
|
|
81
|
+
{ id: 'prod.journal-event', path: journal('post-lint', 0, 'event'), operator: 'equals', expected: 'PostToolUse', status: 'CONTROL' },
|
|
82
|
+
{ id: 'prod.pilot-credit-false', path: ['pilot', 'pilotCredit'], operator: 'equals', expected: false, status: 'CONTROL', note: 'no export in staged tree -> no credit' },
|
|
83
|
+
{ id: 'prod.stop-not-satisfied', path: ['actions', 'stop', 'state', 'workflow', 'state'], operator: 'notEquals', expected: 'satisfied', status: 'CONTROL' },
|
|
84
|
+
{ id: 'prod.stop-exit', path: ['actions', 'stop', 'exitCode'], operator: 'equals', expected: 0, status: 'CONTROL' },
|
|
85
|
+
],
|
|
86
|
+
// OFF: legacy behaviour, with no pilot capture, assessment or report writes.
|
|
87
|
+
'F4-POS-body-edit@off': [
|
|
88
|
+
{ id: 'off.pilot-null', path: ['pilot'], operator: 'equals', expected: null, status: 'CONTROL' },
|
|
89
|
+
{ id: 'off.no-journal', path: ['actions', 'post-lint', 'state', 'capture'], operator: 'absent', status: 'CONTROL', note: 'OFF writes no pilot capture' },
|
|
90
|
+
{ id: 'off.stop-exit', path: ['actions', 'stop', 'exitCode'], operator: 'equals', expected: 0, status: 'CONTROL' },
|
|
91
|
+
{ id: 'off.stop-generic-exit', path: ['actions', 'stop-generic', 'exitCode'], operator: 'equals', expected: 0, status: 'CONTROL' },
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Negatives: each must withhold, and each must show its triggering event really arrived (non-vacuity control).
|
|
96
|
+
// The paired positive above is the RED that proves credit is reachable, so these cannot pass by construction.
|
|
97
|
+
const NEGATIVE = (caseId, delivered) => [
|
|
98
|
+
{ id: 'neg.pilot-credit-false', path: ['pilot', 'pilotCredit'], operator: 'equals', expected: false, status: 'CONTROL' },
|
|
99
|
+
{ id: 'neg.stop-not-satisfied', path: ['actions', 'stop', 'state', 'workflow', 'state'], operator: 'notEquals', expected: 'satisfied', status: 'CONTROL' },
|
|
100
|
+
{ id: 'neg.stop-exit', path: ['actions', 'stop', 'exitCode'], operator: 'equals', expected: 0, status: 'CONTROL' },
|
|
101
|
+
{ id: 'neg.stop-generic-exit', path: ['actions', 'stop-generic', 'exitCode'], operator: 'equals', expected: 0, status: 'CONTROL' },
|
|
102
|
+
...(delivered ? [{ id: 'neg.non-vacuity', ...delivered, status: 'CONTROL', note: `${caseId}: the triggering event reached the real hook` }] : []),
|
|
103
|
+
];
|
|
104
|
+
const PLAN_NEGATIVE = {
|
|
105
|
+
'F4-NEG-echo-pass': NEGATIVE('F4-NEG-echo-pass', { path: journal('post-echo', 0, 'command'), operator: 'equals', expected: 'echo PASS' }),
|
|
106
|
+
'F4-NEG-unregistered-stdout': NEGATIVE('F4-NEG-unregistered-stdout', { path: journal('post-fake', 0, 'command'), operator: 'equals', expected: FAKE_COMMAND }),
|
|
107
|
+
// The contract requires the no-domains gap to be reported, so it cannot be silently full coverage (F1 reports it here).
|
|
108
|
+
'F4-NEG-no-domains': NEGATIVE('F4-NEG-no-domains', { path: ['actions', 'lint-run', 'json', 'coverage', 'unsupported'], operator: 'notEquals', expected: [] }),
|
|
109
|
+
'F4-NEG-later-failure': NEGATIVE('F4-NEG-later-failure', { path: journal('post-fail', 1, 'event'), operator: 'equals', expected: 'PostToolUseFailure' }),
|
|
110
|
+
'F4-NEG-duplicate-tool-id': NEGATIVE('F4-NEG-duplicate-tool-id', { path: journal('post-dup', 1, 'tool_use_id'), operator: 'equals', expected: 'toolu_f4_check_01' }),
|
|
111
|
+
'F4-NEG-interrupted': NEGATIVE('F4-NEG-interrupted', { path: journal('post-interrupted', 0, 'interrupted'), operator: 'equals', expected: true }),
|
|
112
|
+
// F4-T5 conflict 2: a relative invocation is a negative. F1 refuses the unresolved path, and the delivered command is journaled.
|
|
113
|
+
'F4-NEG-relative-path': NEGATIVE('F4-NEG-relative-path', { path: journal('post-lint', 0, 'command'), operator: 'equals', expected: 'node harness-everything/scripts/doc-check.js check --workspace . --config .agents/harness-everything/project-docs.json --changed-path docs/a.md' }),
|
|
114
|
+
// The other-session record is journaled under its own session directory, so it never enters this workflow's journal.
|
|
115
|
+
'F4-NEG-other-session': NEGATIVE('F4-NEG-other-session', { path: ['actions', 'post-other', 'state', 'capture'], operator: 'absent' }),
|
|
116
|
+
'F4-NEG-zero-events': NEGATIVE('F4-NEG-zero-events', { path: ['actions', 'verify-pass', 'state', 'capture'], operator: 'absent' }),
|
|
117
|
+
'F4-NEG-source-change': NEGATIVE('F4-NEG-source-change', { path: journal('post-lint', 0, 'event'), operator: 'equals', expected: 'PostToolUse' }),
|
|
118
|
+
'F4-NEG-edited-payload': NEGATIVE('F4-NEG-edited-payload', { path: ['actions', 'edit-journal', 'state', 'capture', 'records', 0, 'responseDigest'], operator: 'equals', expected: 'f'.repeat(64) }),
|
|
119
|
+
'F4-NEG-self-reported-pass': [
|
|
120
|
+
{ id: 'neg.bare-pass-refused', path: ['actions', 'verify-bare', 'exitCode'], operator: 'equals', expected: 2, status: 'CONTROL', note: 'a bare PASS string is refused' },
|
|
121
|
+
...NEGATIVE('F4-NEG-self-reported-pass'),
|
|
122
|
+
],
|
|
123
|
+
// Path-scoped coverage (coordinator gap 1). Each path an observed check does not cover, or reports as unusable,
|
|
124
|
+
// earns no link or lint credit. The observed check's own JSON must show the defect (non-vacuity).
|
|
125
|
+
'F4-NEG-partial-path': NEGATIVE('F4-NEG-partial-path', { path: ['actions', 'lint-run', 'json', 'links', 'checked'], operator: 'equals', expected: 1 }),
|
|
126
|
+
'F4-NEG-unsupported-link': NEGATIVE('F4-NEG-unsupported-link', { path: ['actions', 'lint-run', 'json', 'coverage', 'unsupported'], operator: 'notEquals', expected: [] }),
|
|
127
|
+
'F4-NEG-not-governed': NEGATIVE('F4-NEG-not-governed', { path: ['actions', 'lint-run', 'json', 'coverage', 'unsupported', 0, 'reason'], operator: 'equals', expected: 'not governed' }),
|
|
128
|
+
'F4-NEG-not-on-disk': NEGATIVE('F4-NEG-not-on-disk', { path: ['actions', 'lint-run', 'json', 'coverage', 'unsupported', 0, 'reason'], operator: 'equals', expected: 'not on disk' }),
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Reminder non-credit (coordinator gap 2). The same valid edit with and without the F2 prose-share reminder: credit must
|
|
132
|
+
// match, and the reminder must appear in no credit field. Both carry the positive's RED for credit.
|
|
133
|
+
const PLAN_REMINDER = {
|
|
134
|
+
'F4-REM-prose-share-present': [
|
|
135
|
+
{ id: 'rem-present.reminder-observed', path: ['actions', 'lint-run', 'json', 'reminders', 0, 'code'], operator: 'equals', expected: 'prose-share', status: 'CONTROL', note: 'the reminder is really present in the observed check' },
|
|
136
|
+
{ id: 'rem-present.pilot-credit', path: ['pilot', 'pilotCredit'], operator: 'equals', expected: true, status: 'CONTROL', retired: 'F4-T2', reason: RED_IMPLEMENTATION },
|
|
137
|
+
],
|
|
138
|
+
'F4-REM-prose-share-absent': [
|
|
139
|
+
{ id: 'rem-absent.no-reminder', path: ['actions', 'lint-run', 'json', 'reminders'], operator: 'equals', expected: [], status: 'CONTROL', note: 'the reminder is really absent in the observed check' },
|
|
140
|
+
{ id: 'rem-absent.pilot-credit', path: ['pilot', 'pilotCredit'], operator: 'equals', expected: true, status: 'CONTROL', retired: 'F4-T2', reason: RED_IMPLEMENTATION },
|
|
141
|
+
],
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
let passCount = 0;
|
|
145
|
+
let failCount = 0;
|
|
146
|
+
let redCount = 0;
|
|
147
|
+
let infraCount = 0;
|
|
148
|
+
|
|
149
|
+
function record(status, id, detail) {
|
|
150
|
+
const suffix = detail ? ` - ${detail}` : '';
|
|
151
|
+
if (status === 'PASS') passCount += 1;
|
|
152
|
+
else if (status === 'RED') redCount += 1;
|
|
153
|
+
else if (status === 'FAIL') failCount += 1;
|
|
154
|
+
else infraCount += 1;
|
|
155
|
+
console.log(` ${status.padEnd(5)} ${id}${suffix}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Runs one replay and checks its plan. Child faults are INFRA, and a RED that passes is a FAIL.
|
|
159
|
+
function checkReplay(caseId, distribution, mode, options, plan, legName = distribution) {
|
|
160
|
+
const label = `${caseId}@${mode}`;
|
|
161
|
+
const item = CASE(caseId);
|
|
162
|
+
const docCheck = typeof distribution === 'string' ? fx.DISTRIBUTIONS[distribution].docCheck : distribution.docCheck;
|
|
163
|
+
const resolved = value => (typeof value === 'string' ? value.split(DOC_CHECK_TOKEN).join(docCheck) : value);
|
|
164
|
+
let root;
|
|
165
|
+
try {
|
|
166
|
+
root = fx.replayCase(item, distribution, options);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
record('INFRA', `${label}@${legName}`, `replay crash: ${error.message}`);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const faults = fx.infrastructureFaults(item, root, options.pilot === undefined ? 'off' : 'on');
|
|
172
|
+
if (faults.length) {
|
|
173
|
+
record('INFRA', `${label}@${legName}`, faults.map(fault => `${fault.actionId}: ${fault.message}`).join('; '));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const results = fx.evaluateAll(root, plan.map(entry => ({ id: entry.id, path: entry.path, operator: entry.operator, expected: resolved(entry.expected) })));
|
|
177
|
+
plan.forEach((entry, index) => {
|
|
178
|
+
const result = results[index];
|
|
179
|
+
const id = `${label}.${entry.id}@${legName}`;
|
|
180
|
+
if (entry.status === 'CONTROL') {
|
|
181
|
+
const detail = result.pass ? '' : `expected ${JSON.stringify(result.expected)} at ${entry.path.join('.')}, got ${JSON.stringify(result.actual).slice(0, 160)}`;
|
|
182
|
+
record(result.pass ? 'PASS' : 'FAIL', id, detail);
|
|
183
|
+
} else if (result.pass) {
|
|
184
|
+
record('FAIL', id, 'RED assertion unexpectedly passed now (expected to fail for its stated missing-implementation reason)');
|
|
185
|
+
} else {
|
|
186
|
+
record('RED', id, `${entry.reason}; actual ${JSON.stringify(result.actual).slice(0, 120)}`);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
return root;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Reminder non-credit across the two replays: identical credit, and no pilot report field names the reminder.
|
|
193
|
+
function reminderNonCredit(withReminder, withoutReminder, distribution) {
|
|
194
|
+
const creditOf = root => JSON.stringify([root.pilot && root.pilot.pilotCredit, root.pilot && root.pilot.creditedCapabilities]);
|
|
195
|
+
const same = creditOf(withReminder) === creditOf(withoutReminder);
|
|
196
|
+
record(same ? 'PASS' : 'FAIL', `rem.identical-credit@${distribution}`, same ? '' : `credit differs: ${creditOf(withReminder)} vs ${creditOf(withoutReminder)}`);
|
|
197
|
+
const named = JSON.stringify(withReminder.pilot || null).includes('prose-share');
|
|
198
|
+
record(named ? 'FAIL' : 'PASS', `rem.not-in-credit-field@${distribution}`, named ? 'the reminder name appears in the pilot report' : '');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ---- Part A: replays through the real hook and CLI entries, both legs ---------------------------------------
|
|
202
|
+
|
|
203
|
+
function partA() {
|
|
204
|
+
console.log('\n== Part A: F4 replay through the real entries (ON with preloader, production, OFF) ==');
|
|
205
|
+
for (const distribution of LEGS.map(leg => leg.id)) {
|
|
206
|
+
checkReplay('F4-POS-body-edit', distribution, 'on', { pilot: '1', preload: true }, PLAN['F4-POS-body-edit@on']);
|
|
207
|
+
// Production is hermetic (F4-T2): a staged copy of the leg with no qualification export, so this control asserts what
|
|
208
|
+
// the repository's live tree cannot guarantee once an export exists.
|
|
209
|
+
const staged = stage(distribution);
|
|
210
|
+
try {
|
|
211
|
+
checkReplay('F4-POS-body-edit', staged.dist, 'production', { pilot: '1' }, PLAN['F4-POS-body-edit@production'], distribution);
|
|
212
|
+
} finally {
|
|
213
|
+
staged.cleanup();
|
|
214
|
+
}
|
|
215
|
+
checkReplay('F4-POS-body-edit', distribution, 'off', {}, PLAN['F4-POS-body-edit@off']);
|
|
216
|
+
for (const caseId of Object.keys(PLAN_NEGATIVE)) {
|
|
217
|
+
checkReplay(caseId, distribution, 'on', { pilot: '1', preload: true }, PLAN_NEGATIVE[caseId]);
|
|
218
|
+
}
|
|
219
|
+
const withReminder = checkReplay('F4-REM-prose-share-present', distribution, 'on', { pilot: '1', preload: true }, PLAN_REMINDER['F4-REM-prose-share-present']);
|
|
220
|
+
const withoutReminder = checkReplay('F4-REM-prose-share-absent', distribution, 'on', { pilot: '1', preload: true }, PLAN_REMINDER['F4-REM-prose-share-absent']);
|
|
221
|
+
if (withReminder && withoutReminder) reminderNonCredit(withReminder, withoutReminder, distribution);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ---- Part B: provider-level negatives, which the replay cannot reach because the provider is not wired ----------
|
|
226
|
+
|
|
227
|
+
function providerFor(leg, input) {
|
|
228
|
+
return require(path.join(leg.lib, 'doc-reconciliation-provider.js')).createClaudeOriginProvider(input);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function captureOf(name, edit) {
|
|
232
|
+
const payload = JSON.parse(fs.readFileSync(path.join(CAPTURE_DIR, name), 'utf8'));
|
|
233
|
+
if (edit) edit(payload);
|
|
234
|
+
return payload;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function partB() {
|
|
238
|
+
console.log('\n== Part B: provider negatives and live-gate boundary (per leg) ==');
|
|
239
|
+
for (const leg of LEGS) {
|
|
240
|
+
const builder = require(path.join(leg.lib, 'doc-reconciliation-provider.js')).buildCaptureRecord;
|
|
241
|
+
|
|
242
|
+
// A SubagentStop whose final message says REJECT must not close a review as approve (A3 section 3).
|
|
243
|
+
{
|
|
244
|
+
const payload = captureOf('payload-subagent-stop.json', p => { p.last_assistant_message = 'REJECT: the document is incorrect.'; });
|
|
245
|
+
const record = builder(payload, 'SubagentStop', ROOT);
|
|
246
|
+
record.ref = 'f4-rev';
|
|
247
|
+
const provider = providerFor(leg, { hostId: 'claude-code', sessionId: payload.session_id, workspaceRoot: ROOT, taskId: 'f4-task', workflowId: 'f4-wf', workflowRevision: 1, principals: { author: 'f4-author-session', executors: [] }, captures: [record], qualification: FAKE_DI });
|
|
248
|
+
const response = provider.resolveEvent({ reference: 'f4-rev', identity: null });
|
|
249
|
+
const approved = response.status === 'accepted' && response.event && response.event.verdict === 'approve';
|
|
250
|
+
strictWhen(approved, `F4-PROV-reject-not-approved@${leg.id}`, 'a REJECT final message must never be accepted with verdict approve');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Absent `interrupted` is unknown, not false, so the success mapping must not credit it (A3 section 1).
|
|
254
|
+
{
|
|
255
|
+
const payload = captureOf('payload-post-tool-use-success.json', p => { delete p.tool_response.interrupted; });
|
|
256
|
+
const record = builder(payload, 'PostToolUse', ROOT);
|
|
257
|
+
record.ref = 'f4-exec';
|
|
258
|
+
const provider = providerFor(leg, { hostId: 'claude-code', sessionId: payload.session_id, workspaceRoot: ROOT, taskId: 'f4-task', workflowId: 'f4-wf', workflowRevision: 1, principals: { author: 'f4-author-session', executors: [] }, captures: [record], qualification: FAKE_DI });
|
|
259
|
+
const response = provider.resolveEvent({ reference: 'f4-exec', identity: null });
|
|
260
|
+
strictWhen(response.status === 'accepted', `F4-PROV-interrupted-absent-not-credited@${leg.id}`, 'an absent interrupted field must never be credited as success');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// A non-empty garbage file in the retained live directory is not qualification (A3 section 4, size-only acceptance).
|
|
264
|
+
{
|
|
265
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'f4-live-'));
|
|
266
|
+
try {
|
|
267
|
+
const copy = path.join(tmp, 'hooks', 'scripts', 'lib');
|
|
268
|
+
fs.mkdirSync(copy, { recursive: true });
|
|
269
|
+
fs.copyFileSync(path.join(leg.lib, 'doc-reconciliation-provider.js'), path.join(copy, 'doc-reconciliation-provider.js'));
|
|
270
|
+
const live = path.join(tmp, LIVE_REL);
|
|
271
|
+
fs.mkdirSync(live, { recursive: true });
|
|
272
|
+
fs.writeFileSync(path.join(live, 'README.md'), 'garbage\n');
|
|
273
|
+
fs.writeFileSync(path.join(live, 'capture.sanitized.jsonl'), 'garbage\n');
|
|
274
|
+
const isolated = require(path.join(copy, 'doc-reconciliation-provider.js')).createClaudeOriginProvider({ hostId: 'claude-code', sessionId: 'f4-sess', workspaceRoot: tmp });
|
|
275
|
+
const context = isolated.resolveContext({});
|
|
276
|
+
strictWhen(context.status === 'accepted', `F4-LIVE-garbage-withholds@${leg.id}`, 'non-empty garbage in the retained live directory must never open the gate');
|
|
277
|
+
} finally {
|
|
278
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// The preloader boundary: with it, only the qualification changes. Without it, the production gate stays unmet.
|
|
283
|
+
partBPreloadBoundary(leg);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// A provider-level RED. `violated` is the current failure, which is the expected RED. A requirement that already holds
|
|
288
|
+
// makes the RED unreal, so it is a FAIL.
|
|
289
|
+
// Provider requirement (retired from RED in F4-T2): `forbidden` is the outcome the requirement rules out. It must be
|
|
290
|
+
// absent, so the check passes when the outcome does not occur and fails when it does.
|
|
291
|
+
function strictWhen(forbidden, id, requirement) {
|
|
292
|
+
if (forbidden) record('FAIL', id, `${requirement}; the forbidden outcome occurred`);
|
|
293
|
+
else record('PASS', id, '');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Retired reasons (F4-T2): the requirements below are asserted strictly now. Kept as the retirement record.
|
|
297
|
+
const RED_REJECT = 'missing-implementation: a quality-review verdict is hard-coded to approve and no structured verdict is parsed; SubagentStop alone must never approve (A3 section 3)';
|
|
298
|
+
const RED_INTERRUPTED = 'missing-implementation: buildCaptureRecord coerces an absent interrupted field to false, so unknown outcome becomes success; absence must stay unknown (A3 section 1)';
|
|
299
|
+
const RED_LIVE_GATE = 'missing-implementation: the retained live gate accepts any non-empty file, size-only acceptance; qualification must validate payloads (A3 section 4)';
|
|
300
|
+
|
|
301
|
+
function partBPreloadBoundary(leg) {
|
|
302
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'f4-preload-'));
|
|
303
|
+
try {
|
|
304
|
+
const entry = path.join(tmp, 'entry');
|
|
305
|
+
fs.mkdirSync(path.join(entry, 'lib'), { recursive: true });
|
|
306
|
+
fs.copyFileSync(path.join(leg.lib, 'doc-reconciliation-provider.js'), path.join(entry, 'lib', 'doc-reconciliation-provider.js'));
|
|
307
|
+
const probe = path.join(entry, 'probe.js');
|
|
308
|
+
fs.writeFileSync(probe, [
|
|
309
|
+
"const provider = require('./lib/doc-reconciliation-provider.js');",
|
|
310
|
+
'const captures = JSON.parse(process.env.F4_PROBE_CAPTURES || \'[]\');',
|
|
311
|
+
"const origin = provider.createClaudeOriginProvider({ hostId: 'claude-code', sessionId: 'f4-sess', workspaceRoot: process.cwd(), taskId: 'f4-task', workflowId: 'f4-wf', workflowRevision: 1, principals: { author: 'f4-author', executors: [] }, captures });",
|
|
312
|
+
"const context = origin.resolveContext({});",
|
|
313
|
+
"const event = origin.resolveEvent({ reference: 'f4-exec', identity: null });",
|
|
314
|
+
"process.stdout.write(JSON.stringify({ context: context.status, event: event.status }));",
|
|
315
|
+
].join('\n') + '\n');
|
|
316
|
+
const capture = { ref: 'f4-exec', event: 'PostToolUse', command: LINT_COMMAND.split(DOC_CHECK_TOKEN).join(fx.DISTRIBUTIONS[leg.id].docCheck), cwd: tmp, tool_use_id: 'toolu_f4_probe', session_id: 'f4-sess', interrupted: false, responseDigest: 'a'.repeat(64), responseRef: 'host-payload/PostToolUse/f4' };
|
|
317
|
+
const run = (args, captureList) => {
|
|
318
|
+
const result = spawnSync(process.execPath, args, { cwd: entry, encoding: 'utf8', env: { ...process.env, F4_PROBE_CAPTURES: JSON.stringify(captureList) }, timeout: 10000 });
|
|
319
|
+
if (result.status !== 0) throw new Error(`probe exited ${result.status}: ${String(result.stderr).slice(0, 200)}`);
|
|
320
|
+
return JSON.parse(result.stdout);
|
|
321
|
+
};
|
|
322
|
+
const withPreload = run(['--require', PRELOAD, probe], [capture]);
|
|
323
|
+
record(withPreload.context === 'accepted' && withPreload.event === 'accepted' ? 'PASS' : 'FAIL', `C-PRELOAD-SUPPLIES-QUALIFICATION@${leg.id}`, withPreload.context === 'accepted' && withPreload.event === 'accepted' ? '' : JSON.stringify(withPreload));
|
|
324
|
+
const emptyWithPreload = run(['--require', PRELOAD, probe], []);
|
|
325
|
+
record(emptyWithPreload.event === 'unavailable' ? 'PASS' : 'FAIL', `C-PRELOAD-FABRICATES-NO-EVENT@${leg.id}`, emptyWithPreload.event === 'unavailable' ? '' : JSON.stringify(emptyWithPreload));
|
|
326
|
+
const production = run([probe], [capture]);
|
|
327
|
+
record(production.context === 'unavailable' ? 'PASS' : 'FAIL', `C-PRODUCTION-GATE-UNMET-WITHOUT-PRELOAD@${leg.id}`, production.context === 'unavailable' ? '' : JSON.stringify(production));
|
|
328
|
+
} finally {
|
|
329
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ---- Part C: preloader is test-only and not packaged or read by production code -------------------------------
|
|
334
|
+
|
|
335
|
+
function listFiles(dir, out = []) {
|
|
336
|
+
if (!fs.existsSync(dir)) return out;
|
|
337
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
338
|
+
if (entry.name === 'node_modules') continue;
|
|
339
|
+
const full = path.join(dir, entry.name);
|
|
340
|
+
if (entry.isDirectory()) listFiles(full, out);
|
|
341
|
+
else if (/\.(js|json)$/.test(entry.name)) out.push(full);
|
|
342
|
+
}
|
|
343
|
+
return out;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function partC() {
|
|
347
|
+
console.log('\n== Part C: preloader is test-only (not packaged, no production reader) ==');
|
|
348
|
+
const packaged = listFiles(path.join(ROOT, 'plugins'));
|
|
349
|
+
const notPackaged = !packaged.some(file => path.basename(file).includes('qualification-preload'));
|
|
350
|
+
record(notPackaged ? 'PASS' : 'FAIL', 'C-PRELOAD-NOT-PACKAGED', notPackaged ? '' : 'a preloader copy exists under plugins/');
|
|
351
|
+
const readers = [...listFiles(path.join(ROOT, 'hooks')), ...listFiles(path.join(ROOT, 'harness-everything', 'scripts'))]
|
|
352
|
+
.filter(file => fs.readFileSync(file, 'utf8').includes('qualification-preload'));
|
|
353
|
+
record(readers.length === 0 ? 'PASS' : 'FAIL', 'C-PRELOAD-NO-PRODUCTION-READER', readers.length === 0 ? '' : `production files reference the preloader: ${readers.map(file => path.relative(ROOT, file)).join(', ')}`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function main() {
|
|
357
|
+
partA();
|
|
358
|
+
partB();
|
|
359
|
+
partC();
|
|
360
|
+
console.log('\n== F4 RED evidence ==');
|
|
361
|
+
console.log(`controls and passing checks: ${passCount}`);
|
|
362
|
+
console.log(`RED assertions confirmed: ${redCount}`);
|
|
363
|
+
console.log(`unexpected failures: ${failCount}`);
|
|
364
|
+
console.log(`infrastructure faults: ${infraCount}`);
|
|
365
|
+
const expectRed = process.argv.includes('--expect-red');
|
|
366
|
+
if (!expectRed) {
|
|
367
|
+
if (failCount === 0 && infraCount === 0 && redCount === 0) {
|
|
368
|
+
console.log('\nSTATUS: GREEN (all assertions passed)');
|
|
369
|
+
return 0;
|
|
370
|
+
}
|
|
371
|
+
console.log(`\nSTATUS: NOT-GREEN (red ${redCount}, unexpected ${failCount}, infra ${infraCount})`);
|
|
372
|
+
return 1;
|
|
373
|
+
}
|
|
374
|
+
if (failCount === 0 && infraCount === 0) {
|
|
375
|
+
console.log('\nSTATUS: RED-PLAN-MATCHED (all controls passed, all RED assertions failed for their stated reasons)');
|
|
376
|
+
return 0;
|
|
377
|
+
}
|
|
378
|
+
console.log('\nSTATUS: RED-PLAN-MISMATCH');
|
|
379
|
+
return 1;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
process.exitCode = main();
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// #325 Phase 3b F4-T2: qualification tests, run through the real entries.
|
|
5
|
+
//
|
|
6
|
+
// Contract: docs/doc-workflow-reconciliation.md, "Test transport" and the qualification section. The origin provider's
|
|
7
|
+
// qualification check must resolve the retained evidence, validate captured kinds, IDs, correlation and recorded digests,
|
|
8
|
+
// and return unavailable (no credit) for missing, malformed, mismatched or unresolvable evidence. A valid export opens
|
|
9
|
+
// the gate, and credit still requires current accepted evidence.
|
|
10
|
+
//
|
|
11
|
+
// Every replay runs the real hook, workflow and Stop entries from a staged copy of one leg (ci/lib/doc-reconciliation-
|
|
12
|
+
// staged-tree.js). The staged copy carries the retained live-host capture and the export the scenario chooses, so no
|
|
13
|
+
// test writes into the repository, and the live tree's own export never decides a result.
|
|
14
|
+
//
|
|
15
|
+
// Statuses: CONTROL must pass now and at GREEN. RED fails now for its stated reason and passes at GREEN. FAIL is an
|
|
16
|
+
// unexpected result, including a RED that already passes. INFRA is a harness crash and never counts as RED.
|
|
17
|
+
// Strict by default; `--expect-red` exits 0 only when every CONTROL passes and every RED fails for its reason.
|
|
18
|
+
//
|
|
19
|
+
// Spy (test (e)): the file-access spy (ci/lib/doc-reconciliation-file-spy.js) is loaded before the CI preloader. It
|
|
20
|
+
// shows whether a child read the qualification export, so the test can assert that the preloader never does, and
|
|
21
|
+
// that the spy really sees a production read.
|
|
22
|
+
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const os = require('os');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
|
|
27
|
+
const fx = require(path.join(__dirname, 'lib', 'doc-reconciliation-prep-fixtures'));
|
|
28
|
+
const { stage, SIBLING_CAPTURE } = require(path.join(__dirname, 'lib', 'doc-reconciliation-staged-tree'));
|
|
29
|
+
const SPY = path.join(__dirname, 'lib', 'doc-reconciliation-file-spy.js');
|
|
30
|
+
const F4_CASES = path.join(__dirname, 'fixtures', 'doc-obligations', 'reconciliation-s2b', 'f4', 'cases.json');
|
|
31
|
+
const LEGS = ['source', 'packaged'];
|
|
32
|
+
|
|
33
|
+
const CASE = id => fx.readCaseCorpus(F4_CASES).cases.find(item => item.id === id);
|
|
34
|
+
|
|
35
|
+
// Assertions on the production replay of the first positive. Credit and the stop are the public outcomes.
|
|
36
|
+
const CREDIT = { id: 'credit', path: ['pilot', 'pilotCredit'], operator: 'equals', expected: true, status: 'CONTROL' };
|
|
37
|
+
const NO_CREDIT = { id: 'no-credit', path: ['pilot', 'pilotCredit'], operator: 'equals', expected: false, status: 'CONTROL' };
|
|
38
|
+
const STOP_SATISFIED = { id: 'stop-satisfied', path: ['actions', 'stop', 'state', 'workflow', 'state'], operator: 'equals', expected: 'satisfied', status: 'CONTROL' };
|
|
39
|
+
const STOP_NOT_SATISFIED = { id: 'stop-not-satisfied', path: ['actions', 'stop', 'state', 'workflow', 'state'], operator: 'notEquals', expected: 'satisfied', status: 'CONTROL' };
|
|
40
|
+
const STOP_EXIT = { id: 'stop-exit', path: ['actions', 'stop', 'exitCode'], operator: 'equals', expected: 0, status: 'CONTROL' };
|
|
41
|
+
|
|
42
|
+
// RED until F4 I2a: the checkpoint validator still reads the earlier export form (captureSha256 beside the retained capture).
|
|
43
|
+
// It refuses a correct export of the in-flight form (harness.doc-reconciliation.qualification.v1, sibling sanitized capture)
|
|
44
|
+
// as 'does not bind the retained capture digest', so a valid export earns no credit at the checkpoint.
|
|
45
|
+
const RED_QUALIFICATION = 'missing-implementation: the checkpoint provider does not recognise the qualification export form (schema harness.doc-reconciliation.qualification.v1, sanitized capture as a sibling); a valid export is refused as not binding the digest (F4 I2a)';
|
|
46
|
+
// Retired from RED in F4-T3 (I2a implements the export form): strict CONTROL assertions, reason kept as the record.
|
|
47
|
+
const CREDIT_STRICT = { ...CREDIT, retired: 'F4-T3', reason: RED_QUALIFICATION };
|
|
48
|
+
const STOP_SATISFIED_STRICT = { ...STOP_SATISFIED, retired: 'F4-T3', reason: RED_QUALIFICATION };
|
|
49
|
+
|
|
50
|
+
let passCount = 0;
|
|
51
|
+
let redCount = 0;
|
|
52
|
+
let failCount = 0;
|
|
53
|
+
let infraCount = 0;
|
|
54
|
+
|
|
55
|
+
function record(status, id, detail) {
|
|
56
|
+
const suffix = detail ? ` - ${detail}` : '';
|
|
57
|
+
if (status === 'PASS') passCount += 1;
|
|
58
|
+
else if (status === 'RED') redCount += 1;
|
|
59
|
+
else if (status === 'FAIL') failCount += 1;
|
|
60
|
+
else infraCount += 1;
|
|
61
|
+
console.log(` ${status.padEnd(5)} ${id}${suffix}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Replays one scenario and evaluates its plan. A RED that already passes is a FAIL, as in the other mechanism suites.
|
|
65
|
+
function replayAndCheck(label, item, dist, options, plan) {
|
|
66
|
+
let root;
|
|
67
|
+
try {
|
|
68
|
+
root = fx.replayCase(item, dist, options);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
record('INFRA', label, `replay crash: ${error.message}`);
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const faults = fx.infrastructureFaults(item, root, options.pilot === undefined ? 'off' : 'on');
|
|
74
|
+
if (faults.length) {
|
|
75
|
+
record('INFRA', label, faults.map(fault => `${fault.actionId}: ${fault.message}`).join('; '));
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const results = fx.evaluateAll(root, plan.map(entry => ({ id: entry.id, path: entry.path, operator: entry.operator, expected: entry.expected })));
|
|
79
|
+
plan.forEach((entry, index) => {
|
|
80
|
+
const result = results[index];
|
|
81
|
+
const id = `${label}.${entry.id}`;
|
|
82
|
+
if (entry.status === 'CONTROL') {
|
|
83
|
+
record(result.pass ? 'PASS' : 'FAIL', id, result.pass ? '' : `expected ${JSON.stringify(result.expected)} at ${entry.path.join('.')}, got ${JSON.stringify(result.actual).slice(0, 160)}`);
|
|
84
|
+
} else if (result.pass) {
|
|
85
|
+
record('FAIL', id, 'RED assertion unexpectedly passed now (expected to fail for its stated reason)');
|
|
86
|
+
} else {
|
|
87
|
+
record('RED', id, `${entry.reason}; actual ${JSON.stringify(result.actual).slice(0, 120)}`);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
return root;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---- (a) production with no export: withheld ----------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
function partA() {
|
|
96
|
+
console.log('\n== (a) production mode with no export is withheld ==');
|
|
97
|
+
for (const leg of LEGS) {
|
|
98
|
+
const staged = stage(leg);
|
|
99
|
+
try {
|
|
100
|
+
record(fs.existsSync(staged.exportFile) ? 'FAIL' : 'PASS', `(a).precondition-no-export@${leg}`, fs.existsSync(staged.exportFile) ? 'the staged tree already has an export' : '');
|
|
101
|
+
replayAndCheck(`(a).production@${leg}`, CASE('F4-POS-body-edit'), staged.dist, { pilot: '1' }, [NO_CREDIT, STOP_NOT_SATISFIED, STOP_EXIT]);
|
|
102
|
+
} finally {
|
|
103
|
+
staged.cleanup();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---- (b) an edited export (one field changed): withheld --------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
function partB() {
|
|
111
|
+
console.log('\n== (b) an export with one edited field is withheld ==');
|
|
112
|
+
for (const leg of LEGS) {
|
|
113
|
+
const staged = stage(leg);
|
|
114
|
+
try {
|
|
115
|
+
const valid = staged.validExport();
|
|
116
|
+
// One field changed: the correlation claim no longer matches what the sanitized capture shows.
|
|
117
|
+
staged.writeExport(staged.validExport({ correlation: { ...valid.correlation, agentResultAliasCorrelated: false } }));
|
|
118
|
+
replayAndCheck(`(b).edited-field@${leg}`, CASE('F4-POS-body-edit'), staged.dist, { pilot: '1' }, [NO_CREDIT, STOP_NOT_SATISFIED, STOP_EXIT]);
|
|
119
|
+
} finally {
|
|
120
|
+
staged.cleanup();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---- (c) an edited sanitized capture whose digest no longer matches: withheld ----------------------------------
|
|
126
|
+
|
|
127
|
+
function partC() {
|
|
128
|
+
console.log('\n== (c) a sanitized capture whose digest no longer matches is withheld ==');
|
|
129
|
+
for (const leg of LEGS) {
|
|
130
|
+
const staged = stage(leg);
|
|
131
|
+
try {
|
|
132
|
+
staged.writeExport(staged.validExport());
|
|
133
|
+
// The export is valid for the bytes it names. Editing those bytes (a trailing blank line) breaks only the digest.
|
|
134
|
+
fs.appendFileSync(path.join(staged.libDir, SIBLING_CAPTURE), '\n');
|
|
135
|
+
replayAndCheck(`(c).edited-capture@${leg}`, CASE('F4-POS-body-edit'), staged.dist, { pilot: '1' }, [NO_CREDIT, STOP_NOT_SATISFIED, STOP_EXIT]);
|
|
136
|
+
} finally {
|
|
137
|
+
staged.cleanup();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---- (d) the valid export passes qualification; credit still requires current accepted evidence -------------
|
|
143
|
+
|
|
144
|
+
function partD() {
|
|
145
|
+
console.log('\n== (d) the valid export passes qualification; credit requires current accepted evidence ==');
|
|
146
|
+
for (const leg of LEGS) {
|
|
147
|
+
const staged = stage(leg);
|
|
148
|
+
try {
|
|
149
|
+
staged.writeExport(staged.validExport());
|
|
150
|
+
// Valid export and the real positive evidence: qualification passes and the first positive is credited.
|
|
151
|
+
replayAndCheck(`(d).valid-export-credit@${leg}`, CASE('F4-POS-body-edit'), staged.dist, { pilot: '1' }, [CREDIT_STRICT, STOP_SATISFIED_STRICT, STOP_EXIT]);
|
|
152
|
+
} finally {
|
|
153
|
+
staged.cleanup();
|
|
154
|
+
}
|
|
155
|
+
const withNoEvidence = stage(leg);
|
|
156
|
+
try {
|
|
157
|
+
withNoEvidence.writeExport(withNoEvidence.validExport());
|
|
158
|
+
// The same valid export with no delivered host events: qualification alone never grants credit.
|
|
159
|
+
replayAndCheck(`(d).valid-export-no-evidence@${leg}`, CASE('F4-NEG-zero-events'), withNoEvidence.dist, { pilot: '1' }, [NO_CREDIT, STOP_NOT_SATISFIED, STOP_EXIT]);
|
|
160
|
+
} finally {
|
|
161
|
+
withNoEvidence.cleanup();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ---- (e) the CI preloader never reads the real export file (file-access spy) ----------------------------------
|
|
167
|
+
|
|
168
|
+
function readsOfExport(logFile) {
|
|
169
|
+
if (!fs.existsSync(logFile)) return [];
|
|
170
|
+
return fs.readFileSync(logFile, 'utf8').split('\n').filter(line => /doc-reconciliation-qualification\.json/.test(line));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function partE() {
|
|
174
|
+
console.log('\n== (e) the CI preloader never reads the real export file (file-access spy) ==');
|
|
175
|
+
const previous = process.env.F4_SPY_LOG;
|
|
176
|
+
for (const leg of LEGS) {
|
|
177
|
+
const staged = stage(leg);
|
|
178
|
+
const logDir = fs.mkdtempSync(path.join(os.tmpdir(), 'f4-spy-'));
|
|
179
|
+
try {
|
|
180
|
+
staged.writeExport(staged.validExport());
|
|
181
|
+
// Preloader run: the spy is loaded first, then the CI preloader supplies the fake qualification.
|
|
182
|
+
const preloadLog = path.join(logDir, 'preload.log');
|
|
183
|
+
process.env.F4_SPY_LOG = preloadLog;
|
|
184
|
+
const preloadRoot = replayAndCheck(`(e).preload-credit@${leg}`, CASE('F4-POS-body-edit'), staged.dist, { pilot: '1', preload: true, nodeArgs: ['--require', SPY] }, [CREDIT, STOP_EXIT]);
|
|
185
|
+
record(preloadRoot && readsOfExport(preloadLog).length === 0 ? 'PASS' : 'FAIL', `(e).preloader-never-reads-export@${leg}`, readsOfExport(preloadLog).length === 0 ? '' : `the preloaded replay read the export: ${readsOfExport(preloadLog).join(' | ').slice(0, 200)}`);
|
|
186
|
+
// Production run of the same staged tree: the spy must see the real export read, or the preloader result means nothing.
|
|
187
|
+
const productionLog = path.join(logDir, 'production.log');
|
|
188
|
+
process.env.F4_SPY_LOG = productionLog;
|
|
189
|
+
replayAndCheck(`(e).production-reads-export-control@${leg}`, CASE('F4-POS-body-edit'), staged.dist, { pilot: '1', nodeArgs: ['--require', SPY] }, [STOP_EXIT]);
|
|
190
|
+
record(readsOfExport(productionLog).length > 0 ? 'PASS' : 'FAIL', `(e).spy-sees-production-read@${leg}`, readsOfExport(productionLog).length > 0 ? '' : 'the spy recorded no export read in production mode, so the preload result is not evidence');
|
|
191
|
+
} finally {
|
|
192
|
+
if (previous === undefined) delete process.env.F4_SPY_LOG;
|
|
193
|
+
else process.env.F4_SPY_LOG = previous;
|
|
194
|
+
staged.cleanup();
|
|
195
|
+
fs.rmSync(logDir, { recursive: true, force: true });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function main() {
|
|
201
|
+
partA();
|
|
202
|
+
partB();
|
|
203
|
+
partC();
|
|
204
|
+
partD();
|
|
205
|
+
partE();
|
|
206
|
+
console.log('\n== F4 qualification evidence ==');
|
|
207
|
+
console.log(`controls and passing checks: ${passCount}`);
|
|
208
|
+
console.log(`RED assertions confirmed: ${redCount}`);
|
|
209
|
+
console.log(`unexpected failures: ${failCount}`);
|
|
210
|
+
console.log(`infrastructure faults: ${infraCount}`);
|
|
211
|
+
const expectRed = process.argv.includes('--expect-red');
|
|
212
|
+
if (!expectRed) {
|
|
213
|
+
if (failCount === 0 && infraCount === 0 && redCount === 0) {
|
|
214
|
+
console.log('\nSTATUS: GREEN (all assertions passed)');
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
217
|
+
console.log(`\nSTATUS: NOT-GREEN (red ${redCount}, unexpected ${failCount}, infra ${infraCount})`);
|
|
218
|
+
return 1;
|
|
219
|
+
}
|
|
220
|
+
if (failCount === 0 && infraCount === 0) {
|
|
221
|
+
console.log('\nSTATUS: RED-PLAN-MATCHED (all controls passed, all RED assertions failed for their stated reasons)');
|
|
222
|
+
return 0;
|
|
223
|
+
}
|
|
224
|
+
console.log('\nSTATUS: RED-PLAN-MISMATCH');
|
|
225
|
+
return 1;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
process.exitCode = main();
|