harness-everything 0.36.3 → 0.37.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.
Files changed (51) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +18 -0
  4. package/ci/fixtures/doc-obligations/providers-s3/cases.json +535 -0
  5. package/ci/fixtures/doc-obligations/read-boundary/boundary-controls.json +211 -0
  6. package/ci/fixtures/doc-obligations/read-boundary/boundary-policy.json +325 -0
  7. package/ci/fixtures/doc-obligations/read-boundary/cases.json +14058 -0
  8. package/ci/fixtures/doc-obligations/read-boundary/expected.json +1 -0
  9. package/ci/fixtures/doc-obligations/read-boundary/identity-cases.json +119 -0
  10. package/ci/fixtures/doc-obligations/read-boundary/identity-expected.json +1 -0
  11. package/ci/fixtures/doc-obligations/read-boundary/scope-cases.json +208 -0
  12. package/ci/fixtures/doc-obligations/read-boundary/scope-expected.json +1 -0
  13. package/ci/fixtures/doc-obligations/reconciliation-prep/cases.json +2751 -0
  14. package/ci/fixtures/doc-obligations/reconciliation-prep/flood-entrypoint.js +5 -0
  15. package/ci/fixtures/doc-obligations/reconciliation-prep/hang-entrypoint.js +5 -0
  16. package/ci/fixtures/doc-obligations/reconciliation-prep/pending-red.json +648 -0
  17. package/ci/fixtures/doc-obligations/reconciliation-prep/provider-contract.json +100 -0
  18. package/ci/fixtures/doc-obligations/reconciliation-prep/trust-contract.json +188 -0
  19. package/ci/lib/doc-evidence-boundary-check.js +1240 -0
  20. package/ci/lib/doc-evidence-fixtures.js +459 -0
  21. package/ci/lib/doc-evidence-trace.js +171 -0
  22. package/ci/lib/doc-reconciliation-prep-fixtures.js +502 -0
  23. package/ci/lib/snapshot-stdin.js +27 -0
  24. package/ci/mechanism-325-3b-preparation.test.js +382 -0
  25. package/ci/mechanism-325-3b-s3-providers.test.js +378 -0
  26. package/ci/mechanism-341-boundary-enforcement.test.js +660 -0
  27. package/ci/mechanism-341-capture-module.test.js +357 -0
  28. package/ci/mechanism-341-mutation-runner.test.js +413 -0
  29. package/ci/mechanism-341-read-boundary.test.js +388 -0
  30. package/ci/mechanism-341-snapshot-stdin.test.js +40 -0
  31. package/ci/mutation-check.js +805 -45
  32. package/ci/mutation-loader.js +544 -57
  33. package/ci/prep/doc-reconciliation-red.js +407 -0
  34. package/ci/stress-341-boundary-control.js +85 -0
  35. package/docs/doc-evidence-baseline-binding.md +105 -19
  36. package/docs/doc-evidence-boundary.md +135 -0
  37. package/docs/doc-workflow-reconciliation.md +338 -0
  38. package/docs/document-lifecycle.md +8 -1
  39. package/harness-everything/SKILL.md +1 -1
  40. package/harness-everything/scripts/doc-evidence-capture.js +331 -0
  41. package/harness-everything/scripts/doc-evidence.js +72 -223
  42. package/harness-everything/scripts/doc-providers.js +113 -0
  43. package/opencode-plugin/plugin.json +1 -1
  44. package/package.json +5 -3
  45. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  46. package/plugins/harness-everything/plugin.json +1 -1
  47. package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
  48. package/plugins/harness-everything/skills/harness-everything/scripts/doc-evidence-capture.js +331 -0
  49. package/plugins/harness-everything/skills/harness-everything/scripts/doc-evidence.js +72 -223
  50. package/plugins/harness-everything/skills/harness-everything/scripts/doc-providers.js +113 -0
  51. package/sbom.cdx.json +43 -7
@@ -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
+ }