harness-everything 0.36.4 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +24 -0
- package/ci/fixtures/doc-obligations/providers-s3/cases.json +535 -0
- package/ci/fixtures/doc-obligations/reconciliation-prep/cases.json +70 -11
- package/ci/fixtures/doc-obligations/reconciliation-prep/pending-red.json +24 -556
- package/ci/fixtures/doc-obligations/reconciliation-prep/provider-contract.json +1 -1
- package/ci/fixtures/doc-obligations/reconciliation-s1/cases.json +6842 -0
- package/ci/fixtures/doc-obligations/reconciliation-s1/fail-session-writes.js +116 -0
- package/ci/lib/doc-reconciliation-prep-fixtures.js +116 -10
- package/ci/mechanism-325-3b-preparation.test.js +8 -8
- package/ci/mechanism-325-3b-s1-review.test.js +831 -0
- package/ci/mechanism-325-3b-s1.test.js +188 -0
- package/ci/mechanism-325-3b-s3-providers.test.js +378 -0
- package/ci/prep/doc-reconciliation-red.js +2 -1
- package/docs/doc-workflow-reconciliation.md +333 -28
- package/docs/document-lifecycle.md +4 -4
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/scripts/doc-lifecycle.js +1 -1
- package/harness-everything/scripts/doc-providers.js +113 -0
- package/hooks/scripts/lib/doc-reconciliation-pilot.js +152 -0
- package/hooks/scripts/lib/workflow-obligations.js +44 -0
- package/hooks/scripts/workflow-disposition.js +2 -0
- package/hooks/scripts/workflow-stop-gate.js +24 -1
- 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-pilot.js +152 -0
- package/plugins/harness-everything/hooks/scripts/lib/workflow-obligations.js +44 -0
- package/plugins/harness-everything/hooks/scripts/workflow-disposition.js +2 -0
- package/plugins/harness-everything/hooks/scripts/workflow-stop-gate.js +24 -1
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-lifecycle.js +1 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-providers.js +113 -0
- package/sbom.cdx.json +7 -7
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// #325 Phase 3b S1 strict suite: switch, trusted scope and fail-closed no-credit gate (RED).
|
|
5
|
+
//
|
|
6
|
+
// Every assertion in reconciliation-s1/cases.json is a strict S1 contract assertion, run against the real CLI, hook and
|
|
7
|
+
// Stop entry points on both legs (source and packaged) and both modes (OFF and ON with the switch set to exactly "1").
|
|
8
|
+
// Each assertion is tagged s1Kind:
|
|
9
|
+
// retired one of the 26 S1 retire IDs in docs/doc-workflow-reconciliation.md; expected to FAIL on the current base
|
|
10
|
+
// control behaviour that must PASS now and after GREEN (P06, P07u, P02d, OFF legacy, Stop exit 0, switch values)
|
|
11
|
+
// additional a named S1 contract case beyond the retire table (governed scope, direct aggregate, OFF-seeded, ON->OFF->ON)
|
|
12
|
+
// This suite grants no pilot credit and never consults the prep registry: a FAIL here is a real gap, not a qualified one.
|
|
13
|
+
// Infrastructure faults (spawn error, deadline, output bound, exit outside the declared policy, malformed output) are
|
|
14
|
+
// reported as INFRA and are distinct from assertion FAIL. Exit 1 means at least one assertion FAILED; exit 2 means
|
|
15
|
+
// infrastructure. The suite cannot pass until GREEN lands; that is the RED contract.
|
|
16
|
+
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const fx = require('./lib/doc-reconciliation-prep-fixtures');
|
|
19
|
+
|
|
20
|
+
const CORPUS_FILE = path.join(__dirname, 'fixtures', 'doc-obligations', 'reconciliation-s1', 'cases.json');
|
|
21
|
+
const corpus = fx.readCaseCorpus(CORPUS_FILE);
|
|
22
|
+
const LEGS = ['source', 'packaged'];
|
|
23
|
+
const MODES = ['off', 'on'];
|
|
24
|
+
const ALLOWED_OPERATORS = new Set(['equals', 'notEquals', 'includes', 'absent']);
|
|
25
|
+
const S1_KINDS = new Set(['retired', 'control', 'additional']);
|
|
26
|
+
const REQUIRED_CASE_FIELDS = ['id', 'family', 'description', 'topology', 'requirements', 'input', 'actions', 'expectations'];
|
|
27
|
+
|
|
28
|
+
// The doc's "S1 retire" table: 26 IDs × 2 legs, by case. Any drift between this list and the corpus is a failure.
|
|
29
|
+
const RETIRED_BY_CASE = {
|
|
30
|
+
P01: ['verify-exit-refused', 'verify-obligation-pending', 'stop-unsatisfied', 'stop-reports-verification'],
|
|
31
|
+
P04: ['direct-execute-refused', 'direct-verify-refused', 'verify-obligation-pending'],
|
|
32
|
+
P02a: ['verify-pass-exit', 'stop-state', 'stop-reports'],
|
|
33
|
+
P02b: ['verify-pass-exit', 'stop-state', 'stop-reports'],
|
|
34
|
+
P02c: ['verify-pass-exit', 'stop-state', 'stop-reports'],
|
|
35
|
+
P02g: ['verify-pass-exit', 'stop-state', 'stop-reports'],
|
|
36
|
+
P02f: ['verify-pass-exit', 'stop-state', 'stop-reports'],
|
|
37
|
+
P03: ['verify-pass-exit'],
|
|
38
|
+
'P07-fable-staged': ['pilot-status-unavailable'],
|
|
39
|
+
'P07-fable-parallel': ['pilot-status-unavailable'],
|
|
40
|
+
'P07-fable-multi-agent-workspace': ['pilot-status-unavailable']
|
|
41
|
+
};
|
|
42
|
+
const RETIRED_COUNT = Object.values(RETIRED_BY_CASE).reduce((total, ids) => total + ids.length, 0);
|
|
43
|
+
|
|
44
|
+
let passed = 0;
|
|
45
|
+
let failed = 0;
|
|
46
|
+
let infra = 0;
|
|
47
|
+
const failures = [];
|
|
48
|
+
const infrastructure = [];
|
|
49
|
+
|
|
50
|
+
const show = value => (value === undefined ? 'undefined' : JSON.stringify(value));
|
|
51
|
+
|
|
52
|
+
// The reason a FAIL names, in the terms the doc uses: "exit 0 where 2 desired", "state satisfied where running desired".
|
|
53
|
+
function reasonFor(assertion, actual) {
|
|
54
|
+
const last = assertion.path[assertion.path.length - 1];
|
|
55
|
+
const label = ({ exitCode: 'exit', state: 'state', unresolved: 'unresolved', pilotStatus: 'pilot status',
|
|
56
|
+
pilotReportOrigin: 'pilot origin', pilot: 'pilot' })[last]
|
|
57
|
+
|| (assertion.path.includes('obligations') && last === 'status' ? 'obligation status' : null)
|
|
58
|
+
|| (assertion.path.includes('requirements') && last === 'status' ? 'step status' : null)
|
|
59
|
+
|| assertion.path.slice(-2).join('.');
|
|
60
|
+
const absent = actual && typeof actual === 'object' && actual.kind === 'absent';
|
|
61
|
+
if (assertion.operator === 'includes') {
|
|
62
|
+
return absent ? `${label} absent where ${show(assertion.expected)} desired`
|
|
63
|
+
: `${label} ${show(actual)} lacks ${show(assertion.expected)}`;
|
|
64
|
+
}
|
|
65
|
+
if (assertion.operator === 'notEquals') return `${label} ${show(actual)} where a value other than ${show(assertion.expected)} desired`;
|
|
66
|
+
if (assertion.operator === 'absent') return `${label} present where absent desired`;
|
|
67
|
+
if (absent) return `${label} absent where ${show(assertion.expected)} desired`;
|
|
68
|
+
return `${label} ${show(actual)} where ${show(assertion.expected)} desired`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function record(status, id, detail) {
|
|
72
|
+
if (status === 'PASS') passed += 1;
|
|
73
|
+
else if (status === 'FAIL') { failed += 1; failures.push(id); }
|
|
74
|
+
else { infra += 1; infrastructure.push(id); }
|
|
75
|
+
console.log(` ${status} ${id}${detail ? ` — ${detail}` : ''}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Parity between the two legs for one replay, mirroring the prep runner's comparable() view.
|
|
79
|
+
function comparable(root) {
|
|
80
|
+
const entries = Object.entries(root.actions).map(([id, action]) => [id, {
|
|
81
|
+
exitCode: action.exitCode,
|
|
82
|
+
signal: action.signal,
|
|
83
|
+
stdout: action.json === null ? action.stdout : null,
|
|
84
|
+
json: action.json,
|
|
85
|
+
stderr: action.stderr,
|
|
86
|
+
state: action.state
|
|
87
|
+
}]);
|
|
88
|
+
return fx.normalize(Object.fromEntries([...entries, ['$final', root.state]]));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.log('=== #325 3b S1: strict switch, trusted scope and fail-closed no-credit suite (RED) ===');
|
|
92
|
+
|
|
93
|
+
// ---- Inventory: the corpus is well formed and marks exactly the doc's retire table -----------------------------
|
|
94
|
+
{
|
|
95
|
+
const problems = [];
|
|
96
|
+
const caseIds = corpus.cases.map(item => item.id);
|
|
97
|
+
if (new Set(caseIds).size !== caseIds.length) problems.push('case ids are not unique');
|
|
98
|
+
for (const item of corpus.cases) {
|
|
99
|
+
for (const field of REQUIRED_CASE_FIELDS) {
|
|
100
|
+
if (item[field] === undefined) problems.push(`${item.id} lacks ${field}`);
|
|
101
|
+
}
|
|
102
|
+
const actionIds = item.actions.map(action => action.id);
|
|
103
|
+
if (new Set(actionIds).size !== actionIds.length) problems.push(`${item.id} action ids are not unique`);
|
|
104
|
+
for (const action of item.actions) {
|
|
105
|
+
if (!fx.ALL_OPS.has(action.op)) problems.push(`${item.id}.${action.id} op ${action.op} is not allowed`);
|
|
106
|
+
for (const mode of MODES) {
|
|
107
|
+
try { fx.exitPolicy(action, mode); } catch (error) { problems.push(error.message); }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
for (const mode of MODES) {
|
|
111
|
+
const ids = item.expectations[mode].map(assertion => assertion.id);
|
|
112
|
+
if (new Set(ids).size !== ids.length) problems.push(`${item.id}.${mode} assertion ids are not unique`);
|
|
113
|
+
for (const assertion of item.expectations[mode]) {
|
|
114
|
+
if (!ALLOWED_OPERATORS.has(assertion.operator)) problems.push(`${item.id}.${assertion.id} operator ${assertion.operator}`);
|
|
115
|
+
if (!Array.isArray(assertion.path) || assertion.path.length === 0) problems.push(`${item.id}.${assertion.id} has no path`);
|
|
116
|
+
if (!S1_KINDS.has(assertion.s1Kind)) problems.push(`${item.id}.${assertion.id} lacks a valid s1Kind`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const retired = [];
|
|
121
|
+
for (const item of corpus.cases) {
|
|
122
|
+
for (const assertion of item.expectations.on) {
|
|
123
|
+
if (assertion.s1Kind === 'retired') retired.push(`${item.id}:${assertion.id}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const expected = Object.entries(RETIRED_BY_CASE).flatMap(([caseId, ids]) => ids.map(id => `${caseId}:${id}`));
|
|
127
|
+
const missing = expected.filter(entry => !retired.includes(entry));
|
|
128
|
+
const extra = retired.filter(entry => !expected.includes(entry));
|
|
129
|
+
if (missing.length || extra.length) problems.push(`retired set differs from the doc table: missing ${show(missing)} extra ${show(extra)}`);
|
|
130
|
+
if (expected.length !== RETIRED_COUNT || RETIRED_COUNT !== 26) problems.push(`the doc table names ${RETIRED_COUNT} IDs, not 26`);
|
|
131
|
+
if (problems.length) record('FAIL', 'inventory: corpus is well formed and marks the 26 S1 retire IDs', problems.join('; '));
|
|
132
|
+
else record('PASS', `inventory: corpus is well formed and marks the ${RETIRED_COUNT} S1 retire IDs`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---- Replays: every case, both legs, both modes ----------------------------------------------------------------
|
|
136
|
+
for (const item of corpus.cases) {
|
|
137
|
+
for (const mode of MODES) {
|
|
138
|
+
const roots = {};
|
|
139
|
+
for (const leg of LEGS) {
|
|
140
|
+
let root;
|
|
141
|
+
try {
|
|
142
|
+
root = fx.scrubReplay(fx.replayCase(item, leg, mode === 'on' ? { pilot: '1' } : undefined));
|
|
143
|
+
} catch (error) {
|
|
144
|
+
infrastructure.push(`${item.id}.${mode} [${leg}]`);
|
|
145
|
+
infra += 1;
|
|
146
|
+
console.log(` INFRA ${item.id}.${mode} [${leg}] — replay failed: ${error.message.split('\n')[0]}`);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
roots[leg] = root;
|
|
150
|
+
const faults = fx.infrastructureFaults(item, root, mode);
|
|
151
|
+
if (faults.length) {
|
|
152
|
+
infra += 1;
|
|
153
|
+
infrastructure.push(`${item.id}.${mode} [${leg}]`);
|
|
154
|
+
for (const fault of faults) console.log(` INFRA ${item.id}.${mode} [${leg}] ${fault.actionId}: ${fault.message}`);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
for (const assertion of item.expectations[mode]) {
|
|
158
|
+
const result = fx.evaluateAssertion(root, assertion);
|
|
159
|
+
const id = `${item.id}.${mode}.${assertion.id} [${leg}]`;
|
|
160
|
+
const tag = `(${assertion.s1Kind})`;
|
|
161
|
+
if (result.pass) record('PASS', id, tag);
|
|
162
|
+
else record('FAIL', id, `${tag} ${reasonFor(assertion, result.actual)}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (roots.source && roots.packaged && !fx.infrastructureFaults(item, roots.source, mode).length
|
|
166
|
+
&& !fx.infrastructureFaults(item, roots.packaged, mode).length) {
|
|
167
|
+
const same = JSON.stringify(comparable(roots.source)) === JSON.stringify(comparable(roots.packaged));
|
|
168
|
+
const id = `${item.id}.${mode}.parity [source=packaged]`;
|
|
169
|
+
if (same) record('PASS', id, '(control)');
|
|
170
|
+
else record('FAIL', id, '(control) source and packaged legs disagree');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ---- Summary -----------------------------------------------------------------------------------------------------
|
|
176
|
+
console.log(`\nS1 strict summary: ${passed} PASS, ${failed} FAIL, ${infra} INFRA across ${corpus.cases.length} cases (pilotCredit: false)`);
|
|
177
|
+
if (failures.length) console.log(`FAILED assertions (${failures.length}):\n ${failures.join('\n ')}`);
|
|
178
|
+
if (infrastructure.length) console.log(`INFRASTRUCTURE (${infrastructure.length}):\n ${infrastructure.join('\n ')}`);
|
|
179
|
+
|
|
180
|
+
if (infra > 0) {
|
|
181
|
+
console.error('S1 suite: infrastructure faults present; these are not assertion results.');
|
|
182
|
+
process.exitCode = 2;
|
|
183
|
+
} else if (failed > 0) {
|
|
184
|
+
console.error('S1 suite: assertion FAIL present (expected on the RED base; GREEN must clear every FAIL).');
|
|
185
|
+
process.exitCode = 1;
|
|
186
|
+
} else {
|
|
187
|
+
console.log('S1 suite: all assertions PASS.');
|
|
188
|
+
}
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// #325 Phase 3b S3 RED suite: advisory provider suggestions (pure suggestDocProviders).
|
|
5
|
+
//
|
|
6
|
+
// Every assertion runs against both legs: the canonical harness-everything/scripts/doc-providers.js and the packaged
|
|
7
|
+
// plugins/harness-everything/skills/harness-everything/scripts/doc-providers.js. A missing or unloadable module is reported
|
|
8
|
+
// per assertion and per leg as a module-missing (or module-load) reason; the harness itself does not crash. Byte parity
|
|
9
|
+
// between the two copies is one assertion over both files. Expected outputs are hand-derived from the S3 spec in
|
|
10
|
+
// docs/doc-workflow-reconciliation.md and from reconciliation-prep/provider-contract.json, never from an implementation.
|
|
11
|
+
// Exit 1 means at least one assertion FAILED; exit 2 means an infrastructure fault in this suite (unreadable fixture).
|
|
12
|
+
|
|
13
|
+
const crypto = require('crypto');
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const http = require('http');
|
|
17
|
+
const https = require('https');
|
|
18
|
+
const net = require('net');
|
|
19
|
+
const childProcess = require('child_process');
|
|
20
|
+
|
|
21
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
22
|
+
const FIXTURE_DIR = path.join(__dirname, 'fixtures', 'doc-obligations');
|
|
23
|
+
const CASES_FILE = path.join(FIXTURE_DIR, 'providers-s3', 'cases.json');
|
|
24
|
+
const CONTRACT_FILE = path.join(FIXTURE_DIR, 'reconciliation-prep', 'provider-contract.json');
|
|
25
|
+
const DERIVE_DIR = path.join(FIXTURE_DIR, 'derive');
|
|
26
|
+
const MODULE = 'doc-providers.js';
|
|
27
|
+
const LEGS = [
|
|
28
|
+
{ name: 'source', dir: path.join(ROOT, 'harness-everything', 'scripts') },
|
|
29
|
+
{ name: 'packaged', dir: path.join(ROOT, 'plugins', 'harness-everything', 'skills', 'harness-everything', 'scripts') }
|
|
30
|
+
];
|
|
31
|
+
const INVALID = 'invalid-input';
|
|
32
|
+
const R_OK = 'active-step capability and host requirements match';
|
|
33
|
+
const NO_META = 'no available provider metadata for the active-step capability';
|
|
34
|
+
const VALID_KEYS = ['suggestions', 'unavailable'];
|
|
35
|
+
const INVALID_KEYS = ['diagnostics', 'suggestions', 'unavailable'];
|
|
36
|
+
const SUGGESTION_KEYS = ['availability', 'capability', 'providerId', 'reason'];
|
|
37
|
+
const UNAVAILABLE_KEYS = ['capability', 'reason'];
|
|
38
|
+
const AVAILABILITY_ENUM = ['available', 'unavailable', 'unknown'];
|
|
39
|
+
|
|
40
|
+
let corpus;
|
|
41
|
+
let contract;
|
|
42
|
+
let deriveConfig;
|
|
43
|
+
try {
|
|
44
|
+
corpus = JSON.parse(fs.readFileSync(CASES_FILE, 'utf8'));
|
|
45
|
+
contract = JSON.parse(fs.readFileSync(CONTRACT_FILE, 'utf8'));
|
|
46
|
+
deriveConfig = JSON.parse(fs.readFileSync(path.join(DERIVE_DIR, 'config.json'), 'utf8'));
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error(`[s3-providers] infrastructure: fixture unreadable: ${error.message.split('\n')[0]}`);
|
|
49
|
+
process.exitCode = 2;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let passed = 0;
|
|
54
|
+
let failed = 0;
|
|
55
|
+
const failures = [];
|
|
56
|
+
const infrastructure = [];
|
|
57
|
+
|
|
58
|
+
function record(status, id, leg, detail) {
|
|
59
|
+
const where = leg ? ` [${leg}]` : '';
|
|
60
|
+
if (status === 'PASS') passed += 1;
|
|
61
|
+
else { failed += 1; failures.push(`${id}${where}`); }
|
|
62
|
+
console.log(` ${status} ${id}${where}${detail ? ` — ${detail}` : ''}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Stable JSON: object keys sorted recursively, so comparisons do not depend on property order.
|
|
66
|
+
function stable(value) {
|
|
67
|
+
if (Array.isArray(value)) return `[${value.map(stable).join(',')}]`;
|
|
68
|
+
if (value && typeof value === 'object') {
|
|
69
|
+
return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${stable(value[key])}`).join(',')}}`;
|
|
70
|
+
}
|
|
71
|
+
return JSON.stringify(value);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function truncate(text) {
|
|
75
|
+
return text.length > 300 ? `${text.slice(0, 300)}…` : text;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function same(actual, expected, label) {
|
|
79
|
+
const a = stable(actual);
|
|
80
|
+
const e = stable(expected);
|
|
81
|
+
if (a !== e) throw new Error(`${label}: got ${truncate(a)} expected ${truncate(e)}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Candidate-set comparison: order-insensitive, no ranking implied.
|
|
85
|
+
function sameSet(actual, expected, label) {
|
|
86
|
+
const a = (Array.isArray(actual) ? actual : []).map(stable).sort();
|
|
87
|
+
const e = expected.map(stable).sort();
|
|
88
|
+
if (JSON.stringify(a) !== JSON.stringify(e)) throw new Error(`${label}: set got ${truncate(a.join(' | '))} expected ${truncate(e.join(' | '))}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function deepFreeze(value) {
|
|
92
|
+
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
93
|
+
Object.freeze(value);
|
|
94
|
+
for (const item of Object.values(value)) deepFreeze(item);
|
|
95
|
+
}
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function cloneJson(value) {
|
|
100
|
+
return value === undefined ? undefined : JSON.parse(JSON.stringify(value));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Module loading is per leg and cached; a load failure is an assertion FAIL for that leg, not a harness crash.
|
|
104
|
+
const loaded = {};
|
|
105
|
+
function loadLeg(leg) {
|
|
106
|
+
if (loaded[leg.name]) return loaded[leg.name];
|
|
107
|
+
const file = path.join(leg.dir, MODULE);
|
|
108
|
+
let result;
|
|
109
|
+
if (!fs.existsSync(file)) {
|
|
110
|
+
result = { error: `module-missing: ${path.relative(ROOT, file)} does not exist` };
|
|
111
|
+
} else {
|
|
112
|
+
try {
|
|
113
|
+
delete require.cache[require.resolve(file)];
|
|
114
|
+
const mod = require(file);
|
|
115
|
+
if (typeof mod.suggestDocProviders !== 'function') result = { error: 'module-missing-export: suggestDocProviders is not a function' };
|
|
116
|
+
else result = { mod };
|
|
117
|
+
} catch (error) {
|
|
118
|
+
result = { error: `module-load: ${error.message.split('\n')[0]}` };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
loaded[leg.name] = result;
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Runs one assertion on each leg. The callback receives the suggest function and the leg name.
|
|
126
|
+
function assertPerLeg(id, fn) {
|
|
127
|
+
for (const leg of LEGS) {
|
|
128
|
+
const { mod, error } = loadLeg(leg);
|
|
129
|
+
if (error) { record('FAIL', id, leg.name, error); continue; }
|
|
130
|
+
try {
|
|
131
|
+
fn(mod.suggestDocProviders, leg);
|
|
132
|
+
record('PASS', id, leg.name);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
record('FAIL', id, leg.name, error.message.split('\n')[0]);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Runs one assertion over both legs at once (parity). Reports a missing leg as module-missing for that leg.
|
|
140
|
+
function assertPair(id, fn) {
|
|
141
|
+
const missing = LEGS.filter(leg => loadLeg(leg).error);
|
|
142
|
+
if (missing.length) {
|
|
143
|
+
for (const leg of missing) record('FAIL', id, leg.name, loadLeg(leg).error);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
fn();
|
|
148
|
+
record('PASS', id, 'both');
|
|
149
|
+
} catch (error) {
|
|
150
|
+
record('FAIL', id, 'both', error.message.split('\n')[0]);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Validity of one output for a valid input: exact keys, then exact ordered content.
|
|
155
|
+
function checkValidOutput(out, expected) {
|
|
156
|
+
same(Object.keys(out).sort(), VALID_KEYS, 'top-level keys (valid input has no diagnostics)');
|
|
157
|
+
same(out.suggestions, expected.suggestions, 'suggestions');
|
|
158
|
+
same(out.unavailable, expected.unavailable, 'unavailable');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Validity of one structured invalid-input result.
|
|
162
|
+
function checkInvalidOutput(out, expected) {
|
|
163
|
+
same(Object.keys(out).sort(), INVALID_KEYS, 'top-level keys (invalid input)');
|
|
164
|
+
same(out.suggestions, [], 'suggestions must be empty for invalid input');
|
|
165
|
+
same(out.unavailable, expected.unavailable, 'unavailable (retained needed capabilities)');
|
|
166
|
+
if (!Array.isArray(out.diagnostics) || out.diagnostics.length === 0) throw new Error('diagnostics must be a nonempty array');
|
|
167
|
+
for (const item of out.diagnostics) {
|
|
168
|
+
same(item.code, INVALID, 'diagnostic code');
|
|
169
|
+
if (typeof item.path !== 'string' || !item.path.trim()) throw new Error('diagnostic path must be a nonempty string');
|
|
170
|
+
if (typeof item.message !== 'string' || !item.message.trim()) throw new Error('diagnostic message must be a nonempty string');
|
|
171
|
+
}
|
|
172
|
+
for (const prefix of expected.diagnosticPathPrefixes) {
|
|
173
|
+
if (!out.diagnostics.some(item => item.path.startsWith(prefix))) throw new Error(`no diagnostic path starts with ${prefix}`);
|
|
174
|
+
}
|
|
175
|
+
if (expected.minDiagnostics !== undefined && out.diagnostics.length < expected.minDiagnostics) {
|
|
176
|
+
throw new Error(`expected at least ${expected.minDiagnostics} diagnostics, got ${out.diagnostics.length}`);
|
|
177
|
+
}
|
|
178
|
+
const sorted = out.diagnostics.map(item => [item.path, item.message]).slice().sort((x, y) => (x[0] < y[0] ? -1 : x[0] > y[0] ? 1 : x[1] < y[1] ? -1 : x[1] > y[1] ? 1 : 0));
|
|
179
|
+
same(out.diagnostics.map(item => [item.path, item.message]), sorted, 'diagnostics ordered by path then message');
|
|
180
|
+
if (expected.forbiddenText) {
|
|
181
|
+
const text = JSON.stringify(out);
|
|
182
|
+
for (const forbidden of expected.forbiddenText) {
|
|
183
|
+
if (text.includes(forbidden)) throw new Error('diagnostics expose an unrelated record value');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
console.log('=== #325 3b S3: advisory provider suggestions (RED; canonical and packaged legs) ===');
|
|
189
|
+
|
|
190
|
+
// ---- Contract examples: candidate sets, both legs -----------------------------------------------------------------
|
|
191
|
+
for (const example of contract.examples) {
|
|
192
|
+
assertPerLeg(`contract:${example.id}`, suggest => {
|
|
193
|
+
const out = suggest(cloneJson(example.input));
|
|
194
|
+
same(Object.keys(out).sort(), VALID_KEYS, 'top-level keys');
|
|
195
|
+
sameSet(out.suggestions, example.expected.suggestions, 'suggestions');
|
|
196
|
+
sameSet(out.unavailable, example.expected.unavailable, 'unavailable');
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
assertPerLeg('contract:swap-provider-order-same-candidate-set', suggest => {
|
|
201
|
+
const example = contract.examples[0];
|
|
202
|
+
const swapped = cloneJson(example.input);
|
|
203
|
+
swapped.availableProviderMetadata.reverse();
|
|
204
|
+
const out = suggest(swapped);
|
|
205
|
+
sameSet(out.suggestions, example.expected.suggestions, 'suggestions after swapping provider order');
|
|
206
|
+
sameSet(out.unavailable, example.expected.unavailable, 'unavailable after swapping provider order');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// ---- Fixture cases: exact outputs (valid) and structured invalid-input results (malformed) -------------------------
|
|
210
|
+
for (const item of corpus.cases) {
|
|
211
|
+
assertPerLeg(`case:${item.id}`, suggest => {
|
|
212
|
+
const out = suggest(cloneJson(item.input));
|
|
213
|
+
if (item.kind === 'valid') checkValidOutput(out, item.expected);
|
|
214
|
+
else if (item.kind === 'malformed') checkInvalidOutput(out, item.expected);
|
|
215
|
+
else throw new Error(`fixture kind ${item.kind} is unknown`);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ---- H1: obligations are unchanged by any suggestion metadata ------------------------------------------------------
|
|
220
|
+
function readTree(root) {
|
|
221
|
+
const files = {};
|
|
222
|
+
if (!fs.existsSync(root)) return files;
|
|
223
|
+
const walk = dir => {
|
|
224
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
225
|
+
const full = path.join(dir, entry.name);
|
|
226
|
+
if (entry.isDirectory()) walk(full);
|
|
227
|
+
else {
|
|
228
|
+
const rel = path.relative(root, full).split(path.sep).join('/');
|
|
229
|
+
files[rel.endsWith('.fixture') ? rel.slice(0, -'.fixture'.length) : rel] = fs.readFileSync(full, 'utf8');
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
walk(root);
|
|
234
|
+
return files;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function obligationsFor(leg) {
|
|
238
|
+
const { deriveDocFacts, deriveDocObligations } = require(path.join(leg.dir, 'doc-obligations.js'));
|
|
239
|
+
const caseDir = path.join(DERIVE_DIR, 'mixed-docs-code');
|
|
240
|
+
const facts = deriveDocFacts({
|
|
241
|
+
base: { files: readTree(path.join(caseDir, 'base')) },
|
|
242
|
+
candidate: { files: readTree(path.join(caseDir, 'candidate')) },
|
|
243
|
+
config: deriveConfig
|
|
244
|
+
});
|
|
245
|
+
return stable(deriveDocObligations(facts.facts));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
for (const leg of LEGS) {
|
|
249
|
+
const id = 'H1:suggestions-leave-derived-obligations-unchanged';
|
|
250
|
+
const { mod, error } = loadLeg(leg);
|
|
251
|
+
if (error) { record('FAIL', id, leg.name, error); continue; }
|
|
252
|
+
try {
|
|
253
|
+
const before = obligationsFor(leg);
|
|
254
|
+
for (const example of contract.examples) mod.suggestDocProviders(cloneJson(example.input));
|
|
255
|
+
for (const item of corpus.cases) mod.suggestDocProviders(cloneJson(item.input));
|
|
256
|
+
const after = obligationsFor(leg);
|
|
257
|
+
if (before !== after) throw new Error('deriveDocObligations output changed after suggestDocProviders calls');
|
|
258
|
+
record('PASS', id, leg.name);
|
|
259
|
+
} catch (error) {
|
|
260
|
+
record('FAIL', id, leg.name, error.message.split('\n')[0]);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ---- Output shape: no channel that could remove or waive a capability -----------------------------------------------
|
|
265
|
+
assertPerLeg('output:exact-key-shapes-and-no-removal-channel', suggest => {
|
|
266
|
+
const outputs = [
|
|
267
|
+
...contract.examples.map(example => suggest(cloneJson(example.input))),
|
|
268
|
+
...corpus.cases.map(item => suggest(cloneJson(item.input)))
|
|
269
|
+
];
|
|
270
|
+
for (const out of outputs) {
|
|
271
|
+
const keys = Object.keys(out).sort();
|
|
272
|
+
if (JSON.stringify(keys) !== JSON.stringify(VALID_KEYS) && JSON.stringify(keys) !== JSON.stringify(INVALID_KEYS)) {
|
|
273
|
+
throw new Error(`unexpected top-level keys ${keys.join(',')}`);
|
|
274
|
+
}
|
|
275
|
+
for (const suggestion of out.suggestions) same(Object.keys(suggestion).sort(), SUGGESTION_KEYS, 'suggestion keys (no locator or extra metadata)');
|
|
276
|
+
for (const gap of out.unavailable) same(Object.keys(gap).sort(), UNAVAILABLE_KEYS, 'unavailable keys');
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// ---- Host: availability enum; unknown is reserved and never emitted ------------------------------------------------
|
|
281
|
+
assertPerLeg('host:unknown-availability-never-emitted', suggest => {
|
|
282
|
+
const outputs = [
|
|
283
|
+
...contract.examples.map(example => suggest(cloneJson(example.input))),
|
|
284
|
+
...corpus.cases.map(item => suggest(cloneJson(item.input)))
|
|
285
|
+
];
|
|
286
|
+
for (const out of outputs) {
|
|
287
|
+
for (const suggestion of out.suggestions) {
|
|
288
|
+
if (!['available', 'unavailable'].includes(suggestion.availability)) {
|
|
289
|
+
throw new Error(`availability ${suggestion.availability} is outside the S3 emitted set`);
|
|
290
|
+
}
|
|
291
|
+
if (!AVAILABILITY_ENUM.includes(suggestion.availability)) throw new Error('availability outside the contract enum');
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// ---- Purity: input unchanged (including frozen), determinism, no file/network/process activity -----------------------
|
|
297
|
+
const everyInput = [...contract.examples.map(example => example.input), ...corpus.cases.map(item => item.input)];
|
|
298
|
+
|
|
299
|
+
assertPerLeg('purity:input-unchanged-including-frozen', suggest => {
|
|
300
|
+
for (const input of everyInput) {
|
|
301
|
+
const copy = cloneJson(input);
|
|
302
|
+
const before = stable(copy);
|
|
303
|
+
suggest(copy);
|
|
304
|
+
same(stable(copy), before, 'input after the call');
|
|
305
|
+
const frozen = deepFreeze(cloneJson(input));
|
|
306
|
+
suggest(frozen);
|
|
307
|
+
same(stable(frozen), before, 'frozen input after the call');
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
assertPerLeg('purity:deterministic-two-calls', suggest => {
|
|
312
|
+
for (const input of everyInput) {
|
|
313
|
+
const first = stable(suggest(cloneJson(input)));
|
|
314
|
+
const second = stable(suggest(cloneJson(input)));
|
|
315
|
+
same(second, first, 'second call output');
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
assertPerLeg('purity:no-file-network-process-activity-during-calls', suggest => {
|
|
320
|
+
const calls = [];
|
|
321
|
+
const trap = (owner, name) => {
|
|
322
|
+
const original = owner[name];
|
|
323
|
+
owner[name] = (...args) => { calls.push(`${name}`); throw new Error(`forbidden ${name} call`); };
|
|
324
|
+
return () => { owner[name] = original; };
|
|
325
|
+
};
|
|
326
|
+
const restores = [
|
|
327
|
+
trap(fs, 'readFileSync'), trap(fs, 'readdirSync'), trap(fs, 'existsSync'), trap(fs, 'statSync'),
|
|
328
|
+
trap(fs, 'lstatSync'), trap(fs, 'openSync'), trap(childProcess, 'spawnSync'), trap(childProcess, 'execFileSync'),
|
|
329
|
+
trap(childProcess, 'execSync'), trap(childProcess, 'spawn'), trap(childProcess, 'exec'), trap(childProcess, 'fork'),
|
|
330
|
+
trap(net, 'connect'), trap(net, 'createConnection'), trap(http, 'request'), trap(https, 'request')
|
|
331
|
+
];
|
|
332
|
+
try {
|
|
333
|
+
for (const input of everyInput) suggest(cloneJson(input));
|
|
334
|
+
} finally {
|
|
335
|
+
for (const restore of restores) restore();
|
|
336
|
+
}
|
|
337
|
+
same(calls, [], 'forbidden calls during suggestDocProviders');
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// ---- Static checks on both source copies --------------------------------------------------------------------------
|
|
341
|
+
const FORBIDDEN_REQUIRE = /require\(\s*['"](?:node:)?(?:fs|child_process|net|http|https)(?:\/promises)?['"]\s*\)/;
|
|
342
|
+
const FORBIDDEN_IMPORT = /\bfrom\s+['"](?:node:)?(?:fs|child_process|net|http|https)(?:\/promises)?['"]/;
|
|
343
|
+
const SKILL_LITERAL = /['"`][^'"`\n]*SKILL\.md[^'"`\n]*['"`]/;
|
|
344
|
+
|
|
345
|
+
function sourceText(leg) {
|
|
346
|
+
return fs.readFileSync(path.join(leg.dir, MODULE), 'utf8');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
assertPerLeg('static:no-forbidden-requires', (suggest, leg) => {
|
|
350
|
+
const text = sourceText(leg);
|
|
351
|
+
if (FORBIDDEN_REQUIRE.test(text) || FORBIDDEN_IMPORT.test(text)) {
|
|
352
|
+
throw new Error('module requires fs, child_process, net, http or https');
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
assertPerLeg('static:never-references-SKILL.md-as-a-path', (suggest, leg) => {
|
|
357
|
+
if (SKILL_LITERAL.test(sourceText(leg))) throw new Error('module contains a SKILL.md path literal');
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
assertPair('parity:source-and-packaged-byte-identical', () => {
|
|
361
|
+
const digest = leg => crypto.createHash('sha256').update(fs.readFileSync(path.join(leg.dir, MODULE))).digest('hex');
|
|
362
|
+
const [source, packaged] = LEGS.map(digest);
|
|
363
|
+
if (source !== packaged) throw new Error(`source ${source.slice(0, 12)} differs from packaged ${packaged.slice(0, 12)}`);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// ---- Summary ------------------------------------------------------------------------------------------------------
|
|
367
|
+
console.log(`\nS3 provider suggestion summary: ${passed} PASS, ${failed} FAIL, ${infrastructure.length} INFRA across ${corpus.cases.length} fixture cases and ${contract.examples.length} contract examples`);
|
|
368
|
+
if (failures.length) console.log(`FAILED assertions (${failures.length}):\n ${failures.join('\n ')}`);
|
|
369
|
+
|
|
370
|
+
if (infrastructure.length) {
|
|
371
|
+
console.error('S3 suite: infrastructure faults present; these are not assertion results.');
|
|
372
|
+
process.exitCode = 2;
|
|
373
|
+
} else if (failed > 0) {
|
|
374
|
+
console.error('S3 suite: assertion FAIL present (expected on the RED base; GREEN must clear every FAIL).');
|
|
375
|
+
process.exitCode = 1;
|
|
376
|
+
} else {
|
|
377
|
+
console.log('S3 suite: all assertions PASS.');
|
|
378
|
+
}
|
|
@@ -38,7 +38,8 @@ const ENTRY_KEYS = ['caseId', 'assertionId', 'distribution', 'baselineSha', 'act
|
|
|
38
38
|
const FINGERPRINT_FILES = [
|
|
39
39
|
'hooks/scripts/lib/workflow-obligations.js',
|
|
40
40
|
'hooks/scripts/workflow-disposition.js',
|
|
41
|
-
'hooks/scripts/workflow-stop-gate.js'
|
|
41
|
+
'hooks/scripts/workflow-stop-gate.js',
|
|
42
|
+
'hooks/scripts/lib/doc-reconciliation-pilot.js'
|
|
42
43
|
];
|
|
43
44
|
const CANDIDATE_FILE = 'ci/fixtures/doc-obligations/reconciliation-prep/pending-red.candidate.json';
|
|
44
45
|
const UNREVIEWED_REASON = 'needs-reason';
|