harness-everything 0.47.5 → 0.49.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 +2 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +36 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2a/run13-workflow-requirements.json +42 -0
- package/ci/mechanism-e1-dependency-artifact-scope.test.js +657 -0
- package/ci/mechanism-prb-outline-review.test.js +966 -48
- package/docs/doc-workflow-reconciliation.md +50 -2
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/scripts/doc-lifecycle.js +11 -3
- package/harness-everything/scripts/project-docs-resolver.js +12 -0
- package/hooks/scripts/lib/doc-reconciliation-core.js +56 -0
- package/hooks/scripts/lib/doc-reconciliation-producer.js +274 -12
- package/hooks/scripts/lib/doc-reconciliation-provider.js +334 -10
- package/hooks/scripts/state-persist.js +5 -0
- package/multi-agent-workspace/SKILL.md +1 -1
- package/multi-agent-workspace/scripts/project-docs-resolver.js +12 -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-core.js +56 -0
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-producer.js +274 -12
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-provider.js +334 -10
- package/plugins/harness-everything/hooks/scripts/state-persist.js +5 -0
- 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 +11 -3
- package/plugins/harness-everything/skills/harness-everything/scripts/project-docs-resolver.js +12 -0
- package/plugins/harness-everything/skills/multi-agent-workspace/SKILL.md +1 -1
- package/plugins/harness-everything/skills/multi-agent-workspace/scripts/project-docs-resolver.js +12 -0
- package/plugins/harness-everything/skills/to-spec/SKILL.md +1 -1
- package/plugins/harness-everything/skills/to-spec/scripts/project-docs-resolver.js +12 -0
- package/sbom.cdx.json +7 -7
- package/to-spec/SKILL.md +1 -1
- package/to-spec/scripts/project-docs-resolver.js +12 -0
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const assert = require('assert');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { spawnSync } = require('child_process');
|
|
9
|
+
const H = require('./fixtures/doc-obligations/reconciliation-s2a/harness.js');
|
|
10
|
+
const { canonical } = require(path.resolve(__dirname, '../hooks/scripts/lib/workflow-isolation.js'));
|
|
11
|
+
const RUN13_REQUIREMENTS = require('./fixtures/doc-obligations/reconciliation-s2a/run13-workflow-requirements.json');
|
|
12
|
+
|
|
13
|
+
const PRODUCER = 'doc-reconciliation-producer.js';
|
|
14
|
+
const EVENTS = 'doc-reconciliation-events.js';
|
|
15
|
+
const CORE = 'doc-reconciliation-core.js';
|
|
16
|
+
const RESOLVER = 'project-docs-resolver.js';
|
|
17
|
+
const LIFECYCLE = 'doc-lifecycle.js';
|
|
18
|
+
const EXPECTED_ROOTS = ['.husky/_', 'node_modules'];
|
|
19
|
+
const primaryRedOnly = process.env.E1_DEP_SCOPE_PRIMARY_ONLY === '1';
|
|
20
|
+
const failures = [];
|
|
21
|
+
|
|
22
|
+
const REQUIREMENTS = {
|
|
23
|
+
producer: { where: 'lib', file: PRODUCER, exports: ['produceDocSnapshot'] },
|
|
24
|
+
events: { where: 'lib', file: EVENTS, exports: ['acceptDocEvents'] },
|
|
25
|
+
core: { where: 'lib', file: CORE, exports: ['checkAcceptedDocEvidence'] },
|
|
26
|
+
resolver: { where: 'scripts', file: RESOLVER, exports: ['validateDocsConfig'] },
|
|
27
|
+
lifecycle: { where: 'scripts', file: LIFECYCLE, exports: ['convertWorkspaceFiles'] }
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function artifactConfig(roots = EXPECTED_ROOTS) {
|
|
31
|
+
return { ...H.clone(H.DERIVE_CONFIG), inventory: { dependencyArtifactRoots: roots.slice() } };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function withRepo(options, fn) {
|
|
35
|
+
const vectors = options.vectors || H.vectorFiles('normative');
|
|
36
|
+
const config = options.config === undefined ? H.DERIVE_CONFIG : options.config;
|
|
37
|
+
const made = H.makeRepo({
|
|
38
|
+
...vectors.base,
|
|
39
|
+
...options.baseFiles,
|
|
40
|
+
[H.CONFIG_REL]: `${JSON.stringify(config, null, 2)}\n`
|
|
41
|
+
});
|
|
42
|
+
// Windows 8.3 short temp paths (RUNNER~1) must match the producer's canonical long root (as in c508e431).
|
|
43
|
+
const repo = { ...made, root: canonical(made.root) };
|
|
44
|
+
try {
|
|
45
|
+
H.writeFiles(repo.root, vectors.candidate);
|
|
46
|
+
H.writeFiles(repo.root, options.candidateFiles || {});
|
|
47
|
+
if (options.install !== false) writeInstalledTree(repo.root);
|
|
48
|
+
fn(repo, vectors);
|
|
49
|
+
} finally {
|
|
50
|
+
H.removeRepo(repo.root);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function writeInstalledTree(root) {
|
|
55
|
+
const installedPackage = path.join(H.ROOT, 'node_modules', 'husky');
|
|
56
|
+
assert(fs.statSync(installedPackage).isDirectory(), 'npm-installed Husky package must exist before the witness runs');
|
|
57
|
+
const packageDir = path.join(root, 'node_modules', 'husky');
|
|
58
|
+
const binDir = path.join(root, 'node_modules', '.bin');
|
|
59
|
+
fs.mkdirSync(packageDir, { recursive: true });
|
|
60
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
61
|
+
for (const file of ['bin.js', 'husky', 'index.js', 'package.json']) fs.copyFileSync(path.join(installedPackage, file), path.join(packageDir, file));
|
|
62
|
+
fs.symlinkSync('../husky/bin.js', path.join(binDir, 'husky'));
|
|
63
|
+
const install = spawnSync(process.execPath, [path.join(packageDir, 'bin.js'), 'install'], {
|
|
64
|
+
cwd: root, env: { ...process.env, HUSKY: '1' }, encoding: 'utf8'
|
|
65
|
+
});
|
|
66
|
+
assert.strictEqual(install.status, 0, `installed Husky package must generate its fixture hooks: ${install.stderr || install.stdout}`);
|
|
67
|
+
assert(fs.statSync(path.join(packageDir, 'bin.js')).isFile(), 'installed package witness must be real Husky JavaScript');
|
|
68
|
+
assert(fs.lstatSync(path.join(binDir, 'husky')).isSymbolicLink(), 'installed .bin witness must be a real symlink');
|
|
69
|
+
assert(fs.statSync(path.join(root, '.husky', '_', 'husky.sh')).isFile(), 'generated Husky witness must come from the installed tree');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function writeRegularArtifacts(root) {
|
|
73
|
+
fs.mkdirSync(path.join(root, 'node_modules', 'fixture-package'), { recursive: true });
|
|
74
|
+
fs.mkdirSync(path.join(root, '.husky', '_'), { recursive: true });
|
|
75
|
+
fs.writeFileSync(path.join(root, 'node_modules', 'fixture-package', 'index.js'), 'module.exports = "host-config control";\n');
|
|
76
|
+
fs.writeFileSync(path.join(root, '.husky', '_', 'husky.sh'), '# generated hook artifact\n');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function removeProjectConfigFromBaseline(repo, vectors) {
|
|
80
|
+
for (const [key, value] of Object.entries({ 'user.name': 'e1-fixture', 'user.email': 'e1-fixture@invalid', 'commit.gpgsign': 'false' })) {
|
|
81
|
+
H.git(repo.root, ['config', key, value]);
|
|
82
|
+
}
|
|
83
|
+
H.git(repo.root, ['checkout', '--', '.']);
|
|
84
|
+
H.git(repo.root, ['rm', '-q', '--', H.CONFIG_REL]);
|
|
85
|
+
H.git(repo.root, ['commit', '-q', '-m', 'baseline without project docs config']);
|
|
86
|
+
repo.commit = H.git(repo.root, ['rev-parse', 'HEAD']).trim();
|
|
87
|
+
H.writeFiles(repo.root, vectors.candidate);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function produce(mods, repo, overrides = {}) {
|
|
91
|
+
const origin = new H.FakeOrigin({ context: H.fakeContext({}, repo) });
|
|
92
|
+
return { result: mods[PRODUCER].produceDocSnapshot(H.producerInput(repo, overrides), { originProvider: origin }), origin };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function realTaskPin(repo) {
|
|
96
|
+
const stateHome = fs.mkdtempSync(path.join(os.tmpdir(), 'e1-dep-scope-state-'));
|
|
97
|
+
const sessionId = `e1-dep-scope-${process.pid}-${Date.now()}`;
|
|
98
|
+
const requirements = [
|
|
99
|
+
{ id: 'dep-scope-change', stepType: 'doc-change', summary: 'Change dependency artifact scope.', acceptance: 'The pinned task can capture the installed dependency tree.' },
|
|
100
|
+
{ id: 'dep-scope-verify', stepType: 'verification', summary: 'Verify dependency artifact scope.', acceptance: 'The pinned task and selector overlap guards are verified.' }
|
|
101
|
+
];
|
|
102
|
+
const env = { ...process.env, HARNESS_STATE_HOME: stateHome, HARNESS_WORKSPACE_ROOT: repo.root,
|
|
103
|
+
HARNESS_DOC_RECONCILIATION_PILOT: '1' };
|
|
104
|
+
const run = (script, args, input) => {
|
|
105
|
+
const child = spawnSync(process.execPath, [script, ...args], { cwd: repo.root, env, encoding: 'utf8', input });
|
|
106
|
+
assert.strictEqual(child.status, 0, `${path.basename(script)} ${args[0] || ''}: ${child.stderr || child.stdout}`);
|
|
107
|
+
};
|
|
108
|
+
try {
|
|
109
|
+
run(path.join(H.ROOT, 'harness-everything/scripts/kernel-router.js'), [], JSON.stringify({
|
|
110
|
+
session_id: sessionId, cwd: repo.root,
|
|
111
|
+
prompt: 'Implement a bounded software behavior change, write a focused test, and run integration verification for the dependency artifact scope.'
|
|
112
|
+
}));
|
|
113
|
+
const controller = path.join(H.ROOT, 'hooks/scripts/workflow-disposition.js');
|
|
114
|
+
run(controller, ['plan', '--session-id', sessionId, '--strategy', 'iterative-single', '--evidence', 'real task pin regression witness',
|
|
115
|
+
'--requirements-json', JSON.stringify(requirements)]);
|
|
116
|
+
run(controller, ['start', '--session-id', sessionId]);
|
|
117
|
+
|
|
118
|
+
const previousStateHome = process.env.HARNESS_STATE_HOME;
|
|
119
|
+
process.env.HARNESS_STATE_HOME = stateHome;
|
|
120
|
+
let sessionDir;
|
|
121
|
+
try { sessionDir = require(path.join(H.ROOT, 'hooks/scripts/lib/harness-state.js')).getSessionDir(repo.root, sessionId); }
|
|
122
|
+
finally {
|
|
123
|
+
if (previousStateHome === undefined) delete process.env.HARNESS_STATE_HOME;
|
|
124
|
+
else process.env.HARNESS_STATE_HOME = previousStateHome;
|
|
125
|
+
}
|
|
126
|
+
const workflow = JSON.parse(fs.readFileSync(path.join(sessionDir, 'workflow-run.json'), 'utf8'));
|
|
127
|
+
assert.strictEqual(workflow.docReconciliationTask?.taskId, 'task-dep-scope-change+dep-scope-verify',
|
|
128
|
+
'shipped plan/start must produce the task pin used by the consumer-positive');
|
|
129
|
+
assert.strictEqual(workflow.docReconciliationTask?.baselineRoot, repo.root, 'real task pin carries its absolute baseline identity');
|
|
130
|
+
return { workflow, task: { ...workflow.docReconciliationTask, requirements } };
|
|
131
|
+
} finally {
|
|
132
|
+
fs.rmSync(stateHome, { recursive: true, force: true });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function produceWithRealTaskPin(mods, repo, pin) {
|
|
137
|
+
const workflow = {
|
|
138
|
+
workflowId: pin.workflow.workflowId,
|
|
139
|
+
sessionId: pin.workflow.sessionId,
|
|
140
|
+
workflowRevision: pin.workflow.revision
|
|
141
|
+
};
|
|
142
|
+
const activeHost = { hostId: 'fake-host' };
|
|
143
|
+
const origin = new H.FakeOrigin({ context: H.fakeContext({ ...workflow, taskId: pin.task.taskId, activeHost }, repo) });
|
|
144
|
+
const input = H.producerInput(repo, { task: pin.task, workflow });
|
|
145
|
+
return { result: mods[PRODUCER].produceDocSnapshot(input, { originProvider: origin }), origin };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function ready(result, label) {
|
|
149
|
+
assert.strictEqual(result.state, 'ready', `${label}: expected ready, got ${JSON.stringify(H.codesOf(result))}`);
|
|
150
|
+
assert(result.snapshot, `${label}: ready result must contain a snapshot`);
|
|
151
|
+
return result.snapshot;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function unavailable(result, label, reasonCode) {
|
|
155
|
+
assert.strictEqual(result.state, 'unavailable', `${label}: expected unavailable, got ${result.state}`);
|
|
156
|
+
assert.strictEqual(result.snapshot, null, `${label}: unavailable capture must not expose a snapshot`);
|
|
157
|
+
if (reasonCode) assert.strictEqual(result.diagnostics[0]?.reasonCode, reasonCode, `${label}: reason code`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function runCase(name, required, fn) {
|
|
161
|
+
if (primaryRedOnly && name !== 'primary-installed-tree-and-truthful-coverage') return;
|
|
162
|
+
for (const leg of H.LEGS) {
|
|
163
|
+
const modules = {};
|
|
164
|
+
let missing = null;
|
|
165
|
+
for (const item of required) {
|
|
166
|
+
const loaded = H.loadModule(leg, item.where, item.file);
|
|
167
|
+
if (loaded.error) { missing = loaded.error; break; }
|
|
168
|
+
for (const name of item.exports || []) {
|
|
169
|
+
if (typeof loaded.mod[name] !== 'function') { missing = `${item.file} export ${name} missing`; break; }
|
|
170
|
+
}
|
|
171
|
+
if (missing) break;
|
|
172
|
+
modules[item.file] = loaded.mod;
|
|
173
|
+
}
|
|
174
|
+
if (missing) {
|
|
175
|
+
failures.push(`${name} [${leg.name}]`);
|
|
176
|
+
console.error(`FAIL ${name} [${leg.name}]: ${missing}`);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
fn(modules, leg);
|
|
181
|
+
console.log(`PASS ${name} [${leg.name}]`);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
if (error instanceof H.UncheckedError) console.log(`UNCHECKED ${name} [${leg.name}]: ${error.message}`);
|
|
184
|
+
else {
|
|
185
|
+
failures.push(`${name} [${leg.name}]`);
|
|
186
|
+
console.error(`FAIL ${name} [${leg.name}]: ${error.message}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
runCase('primary-installed-tree-and-truthful-coverage', [REQUIREMENTS.producer], (mods, leg) => {
|
|
193
|
+
withRepo({ config: artifactConfig(), baseFiles: { 'docs/prd/retired.md': 'old baseline file\n' }, candidateFiles: { 'docs/prd/retired.md': null } }, repo => {
|
|
194
|
+
const pin = realTaskPin(repo);
|
|
195
|
+
const snapshot = ready(produceWithRealTaskPin(mods, repo, pin).result, 'installed-tree capture from shipped plan/start task pin');
|
|
196
|
+
const coverage = snapshot.candidate.coverage;
|
|
197
|
+
assert.strictEqual(coverage.kind, 'project-with-dependency-artifact-exclusions');
|
|
198
|
+
assert.strictEqual(coverage.policyVersion, 'dependency-artifact-scope/1');
|
|
199
|
+
assert.deepStrictEqual(coverage.dependencyArtifactRoots, EXPECTED_ROOTS);
|
|
200
|
+
assert.deepStrictEqual(coverage.excluded, ['.git', ...EXPECTED_ROOTS]);
|
|
201
|
+
assert.strictEqual(coverage.policySource.path, H.CONFIG_REL);
|
|
202
|
+
assert.strictEqual(coverage.policySource.baselineCommit, repo.commit);
|
|
203
|
+
assert.match(coverage.policySource.storedBytesDigest, /^[a-f0-9]{64}$/);
|
|
204
|
+
assert.deepStrictEqual(coverage.guardOutcomes.map(item => [item.guard, item.state]), [
|
|
205
|
+
['baseline-paths', 'clear'], ['index-paths', 'clear'], ['config-sources', 'clear'],
|
|
206
|
+
['config-selectors', 'clear'], ['task-inputs', 'clear'], ['excluded-root-types', 'clear']
|
|
207
|
+
]);
|
|
208
|
+
assert.deepStrictEqual(coverage.excludedRootStates, EXPECTED_ROOTS.map(root => ({ path: root, presence: 'present-directory' })));
|
|
209
|
+
assert(!snapshot.candidate.records.some(item => item.path.startsWith('node_modules/') || item.path.startsWith('.husky/_/')),
|
|
210
|
+
'approved artifacts must be absent from candidate records');
|
|
211
|
+
assert(snapshot.candidate.records.some(item => item.path === 'docs/prd/retired.md' && item.presence === 'deleted'),
|
|
212
|
+
'an omitted retained path must remain an explicit deletion tombstone');
|
|
213
|
+
assert.deepStrictEqual(snapshot.obligations.filter(item => item.strength === 'MUST' && item.applicability !== 'N/A')
|
|
214
|
+
.map(item => item.id).sort(), [...H.VECTORS.vectors.normative.expectedMustIds].sort());
|
|
215
|
+
const core = H.loadModule(leg, 'lib', CORE).mod;
|
|
216
|
+
const lifecycle = H.loadModule(leg, 'scripts', LIFECYCLE).mod;
|
|
217
|
+
if (typeof core?._internals?.validateSnapshot === 'function') {
|
|
218
|
+
assert.deepStrictEqual(core._internals.validateSnapshot(snapshot), [], 'core re-derivation must accept the narrowed inventory');
|
|
219
|
+
} else if (!primaryRedOnly) throw new Error('core snapshot validator missing');
|
|
220
|
+
const retained = snapshot.candidate.records.filter(item => item.presence === 'present');
|
|
221
|
+
if (typeof lifecycle?.convertWorkspaceFiles === 'function') {
|
|
222
|
+
const converted = lifecycle.convertWorkspaceFiles(repo.root, retained.map(item => ({
|
|
223
|
+
path: item.path, bytes: fs.readFileSync(path.join(repo.root, item.path))
|
|
224
|
+
})));
|
|
225
|
+
assert.deepStrictEqual(converted.map(item => [item.path, item.bytes]), retained.map(item => [item.path, item.storedBytes]),
|
|
226
|
+
'lifecycle stored-byte conversion must agree on the producer’s explicit retained inventory');
|
|
227
|
+
} else if (!primaryRedOnly) throw new Error('lifecycle explicit-inventory converter missing');
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
runCase('no-authority-candidate-only-and-policy-change-stay-unavailable', [REQUIREMENTS.producer], mods => {
|
|
232
|
+
withRepo({ config: H.DERIVE_CONFIG }, repo => unavailable(produce(mods, repo).result, 'missing opt-in'));
|
|
233
|
+
withRepo({ config: H.DERIVE_CONFIG, candidateFiles: { [H.CONFIG_REL]: `${JSON.stringify(artifactConfig(), null, 2)}\n` } }, repo => {
|
|
234
|
+
unavailable(produce(mods, repo).result, 'candidate-only opt-in', 'dependency-artifact-policy-changed-or-unpinned');
|
|
235
|
+
});
|
|
236
|
+
withRepo({ config: artifactConfig(), candidateFiles: { [H.CONFIG_REL]: `${JSON.stringify(artifactConfig(['node_modules']), null, 2)}\n` } }, repo => {
|
|
237
|
+
unavailable(produce(mods, repo).result, 'changed baseline policy', 'dependency-artifact-policy-changed-or-unpinned');
|
|
238
|
+
});
|
|
239
|
+
withRepo({ config: { ...H.DERIVE_CONFIG, inventory: { dependencyArtifactRoots: ['vendor'] } } }, repo => {
|
|
240
|
+
unavailable(produce(mods, repo).result, 'invalid baseline policy');
|
|
241
|
+
});
|
|
242
|
+
withRepo({ config: artifactConfig(), candidateFiles: { [H.CONFIG_REL]: `${JSON.stringify(artifactConfig(['node_modules', '.husky/_']), null, 2)}\n` } }, repo => {
|
|
243
|
+
const snapshot = ready(produce(mods, repo).result, 'canonical root-order match');
|
|
244
|
+
assert.deepStrictEqual(snapshot.candidate.coverage.dependencyArtifactRoots, EXPECTED_ROOTS,
|
|
245
|
+
'policy roots are canonically ordered independent of the config array order');
|
|
246
|
+
});
|
|
247
|
+
withRepo({ config: H.DERIVE_CONFIG, install: false }, (repo, vectors) => {
|
|
248
|
+
removeProjectConfigFromBaseline(repo, vectors);
|
|
249
|
+
H.writeFiles(repo.root, { [H.CONFIG_REL]: `${JSON.stringify(artifactConfig(), null, 2)}\n` });
|
|
250
|
+
writeInstalledTree(repo.root);
|
|
251
|
+
unavailable(produce(mods, repo).result, 'candidate policy without baseline project config', 'dependency-artifact-policy-changed-or-unpinned');
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
runCase('host-config-opt-in-cannot-prune-dependency-artifacts', [REQUIREMENTS.producer], mods => {
|
|
256
|
+
withRepo({ config: H.DERIVE_CONFIG, install: false }, (repo, vectors) => {
|
|
257
|
+
removeProjectConfigFromBaseline(repo, vectors);
|
|
258
|
+
H.writeFiles(repo.root, { '.claude/harness-everything/manifest.json': `${JSON.stringify({ projectDocs: artifactConfig() }, null, 2)}\n` });
|
|
259
|
+
writeRegularArtifacts(repo.root);
|
|
260
|
+
const origin = new H.FakeOrigin({ context: H.fakeContext({ activeHost: 'claude' }, repo) });
|
|
261
|
+
const result = mods[PRODUCER].produceDocSnapshot(H.producerInput(repo, { top: { activeHost: 'claude' } }), { originProvider: origin });
|
|
262
|
+
const snapshot = ready(result, 'host-config control');
|
|
263
|
+
assert.strictEqual(snapshot.candidate.coverage.kind, 'whole-project', 'host policy must not create dependency exclusions');
|
|
264
|
+
assert(snapshot.candidate.records.some(item => item.path === 'node_modules/fixture-package/index.js' && item.presence === 'present'));
|
|
265
|
+
assert(snapshot.candidate.records.some(item => item.path === '.husky/_/husky.sh' && item.presence === 'present'));
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
runCase('run13-external-absolute-binding-path-allows-narrowed-snapshot', [REQUIREMENTS.producer], mods => {
|
|
270
|
+
withRepo({ config: artifactConfig() }, repo => {
|
|
271
|
+
const pin = realTaskPin(repo);
|
|
272
|
+
const task = { ...pin.task, requirements: RUN13_REQUIREMENTS };
|
|
273
|
+
const snapshot = ready(produceWithRealTaskPin(mods, repo, { ...pin, task }).result,
|
|
274
|
+
'real run13 requirements with an outside-workspace absolute binding path');
|
|
275
|
+
assert.deepStrictEqual(snapshot.candidate.coverage.dependencyArtifactRoots, EXPECTED_ROOTS,
|
|
276
|
+
'the explicit baseline policy still produces the narrowed snapshot');
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
runCase('outside-absolute-path-containing-root-segment-remains-disjoint', [REQUIREMENTS.producer], mods => {
|
|
281
|
+
withRepo({ config: artifactConfig() }, repo => {
|
|
282
|
+
const pin = realTaskPin(repo);
|
|
283
|
+
const requirements = JSON.parse(JSON.stringify(RUN13_REQUIREMENTS));
|
|
284
|
+
requirements[1].requiredBindings[0].resolvedPath = path.join(os.tmpdir(), 'node_modules', 'external-binding', 'SKILL.md');
|
|
285
|
+
const task = { ...pin.task, requirements };
|
|
286
|
+
const snapshot = ready(produceWithRealTaskPin(mods, repo, { ...pin, task }).result,
|
|
287
|
+
'outside-workspace absolute path mentioning node_modules');
|
|
288
|
+
assert.deepStrictEqual(snapshot.candidate.coverage.dependencyArtifactRoots, EXPECTED_ROOTS);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
runCase('task-pin-identity-containing-root-name-is-not-a-path-selector', [REQUIREMENTS.producer], mods => {
|
|
293
|
+
withRepo({ config: artifactConfig() }, repo => {
|
|
294
|
+
const pin = realTaskPin(repo);
|
|
295
|
+
const task = { ...pin.task, taskId: 'node_modules' };
|
|
296
|
+
const snapshot = ready(produceWithRealTaskPin(mods, repo, { ...pin, task }).result,
|
|
297
|
+
'task identity equal to an approved-root name');
|
|
298
|
+
assert.deepStrictEqual(snapshot.candidate.coverage.dependencyArtifactRoots, EXPECTED_ROOTS);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
runCase('config-validator-accepts-only-the-two-exact-optional-roots', [REQUIREMENTS.resolver], mods => {
|
|
303
|
+
for (const roots of [[], ['node_modules'], ['.husky/_'], EXPECTED_ROOTS]) {
|
|
304
|
+
assert.strictEqual(mods[RESOLVER].validateDocsConfig({ inventory: { dependencyArtifactRoots: roots } }).valid, true,
|
|
305
|
+
`valid roots: ${JSON.stringify(roots)}`);
|
|
306
|
+
}
|
|
307
|
+
for (const roots of [['vendor'], ['node_modules', 'node_modules'], ['node_modules/child'], ['../node_modules'], ['/node_modules'], ['node_modules/']]) {
|
|
308
|
+
assert.strictEqual(mods[RESOLVER].validateDocsConfig({ inventory: { dependencyArtifactRoots: roots } }).valid, false,
|
|
309
|
+
`invalid roots: ${JSON.stringify(roots)}`);
|
|
310
|
+
}
|
|
311
|
+
assert.strictEqual(mods[RESOLVER].validateDocsConfig({ inventory: { dependencyArtifactRoots: ['node_modules'], exclusions: ['tmp'] } }).valid, false,
|
|
312
|
+
'the narrow policy rejects extra exclusion controls');
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
runCase('baseline-index-and-index-removed-paths-block-pruning', [REQUIREMENTS.producer], mods => {
|
|
316
|
+
withRepo({ config: artifactConfig(), baseFiles: { 'node_modules/tracked.js': 'tracked at B\n' } }, repo => {
|
|
317
|
+
unavailable(produce(mods, repo).result, 'baseline tracked path', 'baseline-path-overlap');
|
|
318
|
+
});
|
|
319
|
+
withRepo({ config: artifactConfig() }, repo => {
|
|
320
|
+
const indexed = path.join(repo.root, 'node_modules', 'staged.js');
|
|
321
|
+
fs.mkdirSync(path.dirname(indexed), { recursive: true });
|
|
322
|
+
fs.writeFileSync(indexed, 'staged in current index\n');
|
|
323
|
+
H.git(repo.root, ['add', '-f', 'node_modules/staged.js']);
|
|
324
|
+
unavailable(produce(mods, repo).result, 'current index path', 'index-path-overlap');
|
|
325
|
+
});
|
|
326
|
+
withRepo({ config: artifactConfig(), baseFiles: { 'node_modules/index-removed.js': 'retained in worktree\n' } }, repo => {
|
|
327
|
+
H.git(repo.root, ['rm', '--cached', '-f', '-q', '--', 'node_modules/index-removed.js']);
|
|
328
|
+
assert(fs.existsSync(path.join(repo.root, 'node_modules/index-removed.js')), 'index-removed witness must remain on disk');
|
|
329
|
+
unavailable(produce(mods, repo).result, 'index-removed baseline path', 'baseline-path-overlap');
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
runCase('governed-derived-broad-and-task-input-overlaps-block-pruning', [REQUIREMENTS.producer], mods => {
|
|
334
|
+
const cases = [
|
|
335
|
+
{ label: 'governed selector', config: { governed: [{ path: 'node_modules/**', role: 'reference' }] }, reason: 'config-selector-overlap' },
|
|
336
|
+
{ label: 'ambiguous broad selector', config: { legacy: ['**'] }, reason: 'config-selector-overlap' },
|
|
337
|
+
{ label: 'single-segment broad selector', config: { legacy: ['*'] }, reason: 'config-selector-overlap' },
|
|
338
|
+
{ label: 'derived output', config: { derived: [{ path: 'node_modules/generated.js', generator: 'node scripts/gen.js' }] }, reason: 'config-selector-overlap' },
|
|
339
|
+
{ label: 'derived source', config: { derived: [{ path: 'docs/generated.md', generator: 'node node_modules/generate.js' }] }, reason: 'config-selector-overlap' },
|
|
340
|
+
{ label: 'relative explicit requirement input', makeTask: () => {
|
|
341
|
+
const task = H.taskInput(); task.requirements[0].inputPath = 'node_modules/fixture-package/index.js'; return task;
|
|
342
|
+
}, reason: 'task-input-overlap' },
|
|
343
|
+
{ label: 'empty broad task selector', makeTask: () => {
|
|
344
|
+
const task = H.taskInput(); task.requirements[0].inputPath = ''; return task;
|
|
345
|
+
}, reason: 'task-input-overlap' },
|
|
346
|
+
{ label: 'absolute explicit requirement input', makeTask: root => {
|
|
347
|
+
const task = H.taskInput(); task.requirements[0].inputPath = path.join(root, 'node_modules/fixture-package/index.js'); return task;
|
|
348
|
+
}, reason: 'task-input-overlap' },
|
|
349
|
+
{ label: 'case-alias absolute requirement input', makeTask: root => {
|
|
350
|
+
const task = H.taskInput(); task.requirements[0].inputPath = path.join(root.toUpperCase(), 'node_modules/fixture-package/index.js'); return task;
|
|
351
|
+
}, reason: 'task-input-overlap' },
|
|
352
|
+
{ label: 'relative scope selector', makeScope: () => ({ prdRoot: 'docs/prd/', paths: ['node_modules/fixture-package/index.js'] }), reason: 'task-input-overlap' },
|
|
353
|
+
{ label: 'absolute scope selector', makeScope: root => ({ prdRoot: 'docs/prd/', paths: [path.join(root, 'node_modules/fixture-package/index.js')] }), reason: 'task-input-overlap' }
|
|
354
|
+
];
|
|
355
|
+
for (const item of cases) {
|
|
356
|
+
const config = { ...artifactConfig(), ...item.config };
|
|
357
|
+
withRepo({ config }, repo => {
|
|
358
|
+
const overrides = {};
|
|
359
|
+
if (item.makeTask) overrides.task = item.makeTask(repo.root);
|
|
360
|
+
if (item.makeScope) overrides.top = { scope: item.makeScope(repo.root) };
|
|
361
|
+
unavailable(produce(mods, repo, overrides).result, item.label, item.reason);
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
const DISJOINT_DIR = canonical(fs.mkdtempSync(path.join(os.tmpdir(), 'e1-dep-scope-disjoint-')));
|
|
367
|
+
process.on('exit', () => fs.rmSync(DISJOINT_DIR, { recursive: true, force: true }));
|
|
368
|
+
|
|
369
|
+
runCase('glob-selector-metacharacters-cannot-hide-dependency-root-overlaps', [REQUIREMENTS.producer], mods => {
|
|
370
|
+
const cases = [
|
|
371
|
+
{ label: 'absolute question wildcard selector', makeScope: root => ({ prdRoot: 'docs/prd/', paths: [path.join(root, '?ode_modules', '**')] }) },
|
|
372
|
+
{ label: 'relative question wildcard selector', makeScope: () => ({ prdRoot: 'docs/prd/', paths: ['?ode_modules/**'] }) },
|
|
373
|
+
{ label: 'husky question wildcard selector', makeScope: () => ({ prdRoot: 'docs/prd/', paths: ['.husk?/_/**'] }) },
|
|
374
|
+
{ label: 'bracket class selector', makeScope: () => ({ prdRoot: 'docs/prd/', paths: ['[n]ode_modules/**'] }) },
|
|
375
|
+
{ label: 'brace selector after leading literal', makeScope: () => ({ prdRoot: 'docs/prd/', paths: ['docs/{foo,../node_modules}/**'] }) },
|
|
376
|
+
{ label: 'brace selector crossing to husky after leading literal', makeScope: () => ({ prdRoot: 'docs/prd/', paths: ['docs/{foo,../.husky}/_/**'] }) },
|
|
377
|
+
{ label: 'bracket and parent traversal before dependency root', makeScope: () => ({ prdRoot: 'docs/prd/', paths: ['docs/[a]/../node_modules/**'] }) },
|
|
378
|
+
{ label: 'parent traversal before dependency root', makeScope: () => ({ prdRoot: 'docs/prd/', paths: ['docs/../node_modules/**'] }) },
|
|
379
|
+
{ label: 'parent traversal cannot hide a question wildcard overlap', makeScope: () => ({ prdRoot: 'docs/prd/', paths: ['docs/../?ode_modules/**'] }) },
|
|
380
|
+
{ label: 'repeated dot segments cannot hide a question wildcard overlap', makeScope: () => ({ prdRoot: 'docs/prd/', paths: ['././?ode_modules/**'] }) },
|
|
381
|
+
{ label: 'inner dot and empty segments cannot hide husky overlap', makeScope: () => ({ prdRoot: 'docs/prd/', paths: ['./.husk?/./_//**'] }) },
|
|
382
|
+
...['?ODE_MODULES/**', 'Node_Module?/**', '*_MODULES/**', '.?USKY/_/**', '.HUSK*/_/**', '.husky/?/**', './/?ODE_MODULES\\**'].map(selector => (
|
|
383
|
+
{ label: `mixed-case wildcard ${selector}`, makeScope: () => ({ prdRoot: 'docs/prd/', paths: [selector] }) })),
|
|
384
|
+
{ label: 'absolute mixed-case wildcard selector', makeScope: root => ({ prdRoot: 'docs/prd/', paths: [path.join(root, '?ODE_MODULES', '**')] }) },
|
|
385
|
+
// Globs before the workspace boundary can expand into the workspace (#396 review 2026-11-07).
|
|
386
|
+
{ label: 'absolute star before workspace segment', makeScope: root => ({ prdRoot: 'docs/prd/', paths: [path.join(path.dirname(root), '*', 'node_modules', '**')] }) },
|
|
387
|
+
{ label: 'absolute question before workspace segment', makeScope: root => ({ prdRoot: 'docs/prd/', paths: [path.join(path.dirname(root), `${path.basename(root).slice(0, -1)}?`, 'node_modules', '**')] }) },
|
|
388
|
+
{ label: 'absolute globstar before workspace segment', makeScope: root => ({ prdRoot: 'docs/prd/', paths: [path.join(path.dirname(root), '**', 'node_modules', '**')] }) },
|
|
389
|
+
{ label: 'absolute bracket before workspace segment', makeScope: root => ({ prdRoot: 'docs/prd/', paths: [path.join(path.dirname(root), `[${path.basename(root)[0]}]${path.basename(root).slice(1)}`, '.husky', '_', '**')] }) },
|
|
390
|
+
{ label: 'absolute glob under a non-canonical tmp alias', makeScope: () => ({ prdRoot: 'docs/prd/', paths: [path.join(os.tmpdir(), '*', 'node_modules', '**')] }) },
|
|
391
|
+
{ label: 'absolute glob under a disjoint sibling with a symlink into the workspace', makeScope: root => {
|
|
392
|
+
fs.rmSync(path.join(DISJOINT_DIR, 'link'), { recursive: true, force: true });
|
|
393
|
+
fs.symlinkSync(root, path.join(DISJOINT_DIR, 'link'), 'junction');
|
|
394
|
+
return { prdRoot: 'docs/prd/', paths: [path.join(DISJOINT_DIR, '*', 'node_modules', '**')] };
|
|
395
|
+
} },
|
|
396
|
+
{ label: 'absolute node_modules selector', makeScope: root => ({ prdRoot: 'docs/prd/', paths: [path.join(root, 'node_modules', '**')] }) }
|
|
397
|
+
];
|
|
398
|
+
for (const item of cases) {
|
|
399
|
+
withRepo({ config: artifactConfig() }, repo => {
|
|
400
|
+
unavailable(produce(mods, repo, { top: { scope: item.makeScope(repo.root) } }).result,
|
|
401
|
+
item.label, 'task-input-overlap');
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
for (const item of [
|
|
405
|
+
{ label: 'absolute docs selector', makeScope: root => ({ prdRoot: 'docs/prd/', paths: [path.join(root, 'docs', 'prd', '**')] }) },
|
|
406
|
+
{ label: 'relative question wildcard outside roots', makeScope: () => ({ prdRoot: 'docs/prd/', paths: ['docs/pr?/**'] }) },
|
|
407
|
+
{ label: 'absolute literal path under a disjoint sibling directory', makeScope: () => ({ prdRoot: 'docs/prd/', paths: [path.join(DISJOINT_DIR, 'docs', 'prd', 'graph.md')] }) }
|
|
408
|
+
]) {
|
|
409
|
+
withRepo({ config: artifactConfig() }, repo => {
|
|
410
|
+
ready(produce(mods, repo, { top: { scope: item.makeScope(repo.root) } }).result, item.label);
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
if (process.platform !== 'win32') runCase('unresolved-windows-drive-path-stays-conservative', [REQUIREMENTS.producer], mods => {
|
|
416
|
+
withRepo({ config: artifactConfig() }, repo => {
|
|
417
|
+
const task = H.taskInput();
|
|
418
|
+
task.requirements[0].inputPath = 'C:\\workspace\\node_modules\\fixture-package\\index.js';
|
|
419
|
+
unavailable(produce(mods, repo, { task }).result, 'unresolved Windows drive path', 'task-input-overlap');
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
runCase('captured-config-source-overlap-blocks-pruning', [REQUIREMENTS.producer], (_mods, leg) => {
|
|
424
|
+
const resolverPath = path.join(leg.scripts, RESOLVER);
|
|
425
|
+
const producerPath = path.join(leg.lib, PRODUCER);
|
|
426
|
+
const resolver = require(resolverPath);
|
|
427
|
+
const originalResolve = resolver.resolveDocsConfigCaptured;
|
|
428
|
+
delete require.cache[require.resolve(producerPath)];
|
|
429
|
+
resolver.resolveDocsConfigCaptured = (...args) => {
|
|
430
|
+
const result = originalResolve(...args);
|
|
431
|
+
result.capture.sources.push({ path: 'node_modules/project-docs.json', presence: 'present', bytes: Buffer.from('{}'), digest: '0'.repeat(64) });
|
|
432
|
+
return result;
|
|
433
|
+
};
|
|
434
|
+
try {
|
|
435
|
+
const producer = require(producerPath);
|
|
436
|
+
withRepo({ config: artifactConfig() }, repo => {
|
|
437
|
+
unavailable(producer.produceDocSnapshot(H.producerInput(repo), {
|
|
438
|
+
originProvider: new H.FakeOrigin({ context: H.fakeContext({}, repo) })
|
|
439
|
+
}), 'captured config source under artifact root', 'config-source-overlap');
|
|
440
|
+
});
|
|
441
|
+
} finally {
|
|
442
|
+
resolver.resolveDocsConfigCaptured = originalResolve;
|
|
443
|
+
delete require.cache[require.resolve(producerPath)];
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
runCase('F4-ignored-rename-and-ignore-rule-change-remain-in-inventory', [REQUIREMENTS.producer], mods => {
|
|
448
|
+
withRepo({
|
|
449
|
+
config: artifactConfig(),
|
|
450
|
+
baseFiles: { '.gitignore': '', 'docs/prd/old.md': 'old path\n' },
|
|
451
|
+
candidateFiles: {
|
|
452
|
+
'.gitignore': 'docs/prd/renamed.md\n',
|
|
453
|
+
'docs/prd/old.md': null,
|
|
454
|
+
'docs/prd/renamed.md': 'ignored rename destination\n'
|
|
455
|
+
}
|
|
456
|
+
}, repo => {
|
|
457
|
+
const snapshot = ready(produce(mods, repo).result, 'ignored rename');
|
|
458
|
+
assert(snapshot.candidate.records.some(item => item.path === 'docs/prd/renamed.md' && item.presence === 'present'),
|
|
459
|
+
'ignored rename destination must be included');
|
|
460
|
+
assert(snapshot.candidate.records.some(item => item.path === 'docs/prd/old.md' && item.presence === 'deleted'),
|
|
461
|
+
'rename source tombstone must be retained');
|
|
462
|
+
});
|
|
463
|
+
withRepo({ config: artifactConfig(), baseFiles: { '.gitignore': 'tmp/\n' }, candidateFiles: { 'tmp/ignored.log': 'outside approved roots\n' } }, repo => {
|
|
464
|
+
const snapshot = ready(produce(mods, repo).result, 'unrelated ignored content');
|
|
465
|
+
assert(snapshot.candidate.records.some(item => item.path === 'tmp/ignored.log' && item.presence === 'present'),
|
|
466
|
+
'unrelated ignored content must remain captured');
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
runCase('optional-root-selection-keeps-the-other-root-included', [REQUIREMENTS.producer], mods => {
|
|
471
|
+
withRepo({ config: artifactConfig(['node_modules']) }, repo => {
|
|
472
|
+
const snapshot = ready(produce(mods, repo).result, 'single-root opt-in');
|
|
473
|
+
assert(snapshot.candidate.records.some(item => item.path === '.husky/_/husky.sh' && item.presence === 'present'),
|
|
474
|
+
'.husky/_ must remain included when not approved');
|
|
475
|
+
assert(!snapshot.candidate.records.some(item => item.path.startsWith('node_modules/')),
|
|
476
|
+
'only the individually approved root may be pruned');
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
runCase('excluded-root-and-retained-path-symlinks-fail-closed', [REQUIREMENTS.producer], mods => {
|
|
481
|
+
withRepo({ config: artifactConfig(), install: false }, repo => {
|
|
482
|
+
const external = fs.mkdtempSync(path.join(os.tmpdir(), 'dep-scope-root-link-'));
|
|
483
|
+
try {
|
|
484
|
+
fs.writeFileSync(path.join(external, 'installed.js'), 'outside\n');
|
|
485
|
+
try { fs.symlinkSync(external, path.join(repo.root, 'node_modules'), 'dir'); }
|
|
486
|
+
catch (error) { throw new H.UncheckedError(`directory symlink unavailable: ${error.code}`); }
|
|
487
|
+
unavailable(produce(mods, repo).result, 'excluded root symlink', 'dependency-root-not-plain-directory');
|
|
488
|
+
} finally { fs.rmSync(external, { recursive: true, force: true }); }
|
|
489
|
+
});
|
|
490
|
+
withRepo({ config: artifactConfig(), install: false }, repo => {
|
|
491
|
+
const external = fs.mkdtempSync(path.join(os.tmpdir(), 'dep-scope-parent-link-'));
|
|
492
|
+
try {
|
|
493
|
+
fs.mkdirSync(path.join(external, '_'));
|
|
494
|
+
try { fs.symlinkSync(external, path.join(repo.root, '.husky'), 'dir'); }
|
|
495
|
+
catch (error) { throw new H.UncheckedError(`directory symlink unavailable: ${error.code}`); }
|
|
496
|
+
unavailable(produce(mods, repo).result, 'excluded-root ancestor symlink', 'dependency-root-not-plain-directory');
|
|
497
|
+
} finally { fs.rmSync(external, { recursive: true, force: true }); }
|
|
498
|
+
});
|
|
499
|
+
withRepo({ config: artifactConfig(), install: false }, repo => {
|
|
500
|
+
const target = path.join(repo.root, 'docs/prd/graph.md');
|
|
501
|
+
const source = path.join(repo.root, 'docs/prd/retained.md');
|
|
502
|
+
fs.writeFileSync(source, 'retained target\n');
|
|
503
|
+
fs.rmSync(target, { force: true });
|
|
504
|
+
try { fs.symlinkSync(source, target); }
|
|
505
|
+
catch (error) { throw new H.UncheckedError(`file symlink unavailable: ${error.code}`); }
|
|
506
|
+
unavailable(produce(mods, repo).result, 'retained path symlink');
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
runCase('retained-unreadable-and-byte-limit-controls-remain', [REQUIREMENTS.producer], mods => {
|
|
511
|
+
if (process.platform === 'win32') throw new H.UncheckedError('win32 does not expose POSIX unreadable mode bits');
|
|
512
|
+
withRepo({ config: artifactConfig(), install: false }, repo => {
|
|
513
|
+
const unreadable = path.join(repo.root, 'zzz-unreadable.txt');
|
|
514
|
+
fs.writeFileSync(unreadable, 'permission witness\n');
|
|
515
|
+
fs.chmodSync(unreadable, 0);
|
|
516
|
+
try { unavailable(produce(mods, repo).result, 'retained unreadable path'); }
|
|
517
|
+
finally { fs.chmodSync(unreadable, 0o600); }
|
|
518
|
+
});
|
|
519
|
+
withRepo({ config: artifactConfig(), install: false }, repo => {
|
|
520
|
+
const big = path.join(repo.root, 'zzzz-over-limit.bin');
|
|
521
|
+
const fd = fs.openSync(big, 'w');
|
|
522
|
+
try { fs.ftruncateSync(fd, 64 * 1024 * 1024 + 1); } finally { fs.closeSync(fd); }
|
|
523
|
+
unavailable(produce(mods, repo).result, 'retained byte-limit control');
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
runCase('excluded-byte-mutation-preserves-scoped-identity', [REQUIREMENTS.producer], mods => {
|
|
528
|
+
withRepo({ config: artifactConfig() }, repo => {
|
|
529
|
+
const first = ready(produce(mods, repo).result, 'before excluded mutation');
|
|
530
|
+
fs.writeFileSync(path.join(repo.root, 'node_modules/husky/bin.js'), 'changed install artifact bytes\n');
|
|
531
|
+
fs.writeFileSync(path.join(repo.root, '.husky/_/husky.sh'), '# changed generated hook\n');
|
|
532
|
+
const second = ready(produce(mods, repo).result, 'after excluded mutation');
|
|
533
|
+
assert.strictEqual(second.identity.candidateDigest, first.identity.candidateDigest);
|
|
534
|
+
assert.strictEqual(second.identity.taskScopeDigest, first.identity.taskScopeDigest);
|
|
535
|
+
assert.strictEqual(second.candidate.coverage.policySource.storedBytesDigest, first.candidate.coverage.policySource.storedBytesDigest);
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
runCase('included-source-or-config-mutation-during-capture-is-unavailable', [REQUIREMENTS.producer], mods => {
|
|
540
|
+
for (const relative of ['docs/prd/graph.md', H.CONFIG_REL]) {
|
|
541
|
+
withRepo({ config: artifactConfig() }, repo => {
|
|
542
|
+
const target = path.join(repo.root, relative);
|
|
543
|
+
const originalRead = fs.readFileSync;
|
|
544
|
+
let mutated = false;
|
|
545
|
+
fs.readFileSync = function(file, ...args) {
|
|
546
|
+
const result = originalRead.call(this, file, ...args);
|
|
547
|
+
if (!mutated && path.resolve(String(file)) === target) {
|
|
548
|
+
mutated = true;
|
|
549
|
+
originalRead.call(fs, target);
|
|
550
|
+
fs.writeFileSync(target, `${result.toString('utf8')}\nmid-capture mutation\n`);
|
|
551
|
+
}
|
|
552
|
+
return result;
|
|
553
|
+
};
|
|
554
|
+
let result;
|
|
555
|
+
try { result = produce(mods, repo).result; }
|
|
556
|
+
finally { fs.readFileSync = originalRead; }
|
|
557
|
+
assert(mutated, `${relative} must be read during capture`);
|
|
558
|
+
unavailable(result, `${relative} changed during capture`);
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
runCase('row17-six-musts-survive-production-consumer-flow', [REQUIREMENTS.producer, REQUIREMENTS.events, REQUIREMENTS.core], mods => {
|
|
564
|
+
const sixMusts = [
|
|
565
|
+
'doc-change:conflict-check', 'doc-change:doc-lint', 'doc-change:link-check',
|
|
566
|
+
'doc-change:outline-confirmation', 'doc-change:quality-review', 'doc-change:requirement-sync'
|
|
567
|
+
].sort();
|
|
568
|
+
const fixture = path.join(H.ROOT, 'ci', 'fixtures', 'doc-obligations', 'derive', 'req-revision');
|
|
569
|
+
const vectors = {
|
|
570
|
+
base: H.readTree(path.join(fixture, 'base')),
|
|
571
|
+
candidate: H.readTree(path.join(fixture, 'candidate'))
|
|
572
|
+
};
|
|
573
|
+
const target = 'docs/prd/graph/r002.md';
|
|
574
|
+
const eventsVector = {
|
|
575
|
+
expectedMustIds: sixMusts,
|
|
576
|
+
eventKindByMust: {
|
|
577
|
+
'doc-change:doc-lint': 'execution',
|
|
578
|
+
'doc-change:link-check': 'execution',
|
|
579
|
+
'doc-change:quality-review': 'review',
|
|
580
|
+
'doc-change:conflict-check': 'execution',
|
|
581
|
+
'doc-change:requirement-sync': 'execution'
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
withRepo({ vectors, config: artifactConfig(), install: false }, repo => {
|
|
585
|
+
H.git(repo.root, ['config', 'user.name', 'e1-fixture']);
|
|
586
|
+
H.git(repo.root, ['config', 'user.email', 'e1-fixture@invalid']);
|
|
587
|
+
H.git(repo.root, ['config', 'commit.gpgsign', 'false']);
|
|
588
|
+
H.writeFiles(repo.root, { [target]: `---\nid: PRD-GRPH-001\nrevision: 2\nstatus: proposed\n---\n\n# Graph\n\n## Outline\n- Context: TBD\n- Decision: TBD\n` });
|
|
589
|
+
H.git(repo.root, ['add', target]);
|
|
590
|
+
H.git(repo.root, ['commit', '-q', '-m', 'O outline']);
|
|
591
|
+
const outlineCommit = H.git(repo.root, ['rev-parse', 'HEAD']).trim();
|
|
592
|
+
const outlineTree = H.git(repo.root, ['rev-parse', 'HEAD^{tree}']).trim();
|
|
593
|
+
const outlineBytes = Buffer.from(H.git(repo.root, ['show', `${outlineCommit}:${target}`]));
|
|
594
|
+
const outlineBlob = H.git(repo.root, ['rev-parse', `${outlineCommit}:${target}`]).trim();
|
|
595
|
+
H.writeFiles(repo.root, vectors.candidate);
|
|
596
|
+
H.git(repo.root, ['add', '-A']);
|
|
597
|
+
H.git(repo.root, ['commit', '-q', '-m', 'D final requirement revision']);
|
|
598
|
+
const draftCommit = H.git(repo.root, ['rev-parse', 'HEAD']).trim();
|
|
599
|
+
const draftTree = H.git(repo.root, ['rev-parse', 'HEAD^{tree}']).trim();
|
|
600
|
+
writeInstalledTree(repo.root);
|
|
601
|
+
const context = H.fakeContext({ activeHost: 'claude-code' }, repo);
|
|
602
|
+
const origin = new H.FakeOrigin({ context });
|
|
603
|
+
const produced = mods[PRODUCER].produceDocSnapshot(H.producerInput(repo, { top: { activeHost: 'claude-code' } }), { originProvider: origin });
|
|
604
|
+
const snapshot = ready(produced, 'row-17 producer');
|
|
605
|
+
const expected = snapshot.obligations.filter(item => item.strength === 'MUST' && item.applicability !== 'N/A')
|
|
606
|
+
.map(item => item.id).sort();
|
|
607
|
+
assert.deepStrictEqual(expected, sixMusts, 'the six row-17 obligations must remain applicable');
|
|
608
|
+
const events = H.buildEvents(snapshot, repo, eventsVector);
|
|
609
|
+
const outline = snapshot.obligations.find(item => item.capability === 'outline-confirmation');
|
|
610
|
+
const paths = outline.scope.paths.slice().sort();
|
|
611
|
+
assert.deepStrictEqual(paths, [target]);
|
|
612
|
+
const draftDigests = Object.fromEntries(paths.map(file => [file,
|
|
613
|
+
snapshot.candidate.records.find(item => item.path === file && item.presence === 'present').digest]));
|
|
614
|
+
const draftBlobs = Object.fromEntries(paths.map(file => [file, H.git(repo.root, ['rev-parse', `${draftCommit}:${file}`]).trim()]));
|
|
615
|
+
const outlineDigests = Object.fromEntries(paths.map(file => [file, H.sha256(outlineBytes)]));
|
|
616
|
+
const outlineBlobs = Object.fromEntries(paths.map(file => [file, outlineBlob]));
|
|
617
|
+
const inventoryBytes = Buffer.from('deterministic row 17 inventory fixture\n');
|
|
618
|
+
const inventoryDigest = H.sha256(inventoryBytes);
|
|
619
|
+
const inventoryPath = `doc-reconciliation/outline-confirmation/inventory/${inventoryDigest}.md`;
|
|
620
|
+
const evidence = {
|
|
621
|
+
schema: 'harness.outline-confirmation.v1', state: 'validated', taskId: snapshot.identity.taskId, attemptId: 'attempt-row17',
|
|
622
|
+
inventoryRowId: '17', domain: 'GRPH', inventoryPinDigest: inventoryDigest, inventoryArtifactDigest: inventoryDigest,
|
|
623
|
+
baselineCommit: repo.commit, outlineCommit, outlineTree, draftCommit, draftTree, mergedCommit: draftCommit,
|
|
624
|
+
outlineArtifactDigests: outlineDigests, draftArtifactDigests: draftDigests,
|
|
625
|
+
outlineBlobOids: outlineBlobs, draftBlobOids: draftBlobs, targetPaths: paths,
|
|
626
|
+
handoffDigest: H.sha256('handoff'), reviewCaptureDigest: H.sha256('review-capture'), reviewDigest: H.sha256('review'),
|
|
627
|
+
seamDigest: H.sha256('seam'), dispatchInputDigest: H.sha256('dispatch'), writerLeaseDigest: H.sha256('lease'),
|
|
628
|
+
coordinatorPrincipal: H.PRINCIPALS.author, reviewerPrincipal: H.PRINCIPALS.reviewer, workerPrincipal: H.PRINCIPALS.executor,
|
|
629
|
+
dispatchToolUseId: 'outline-dispatch-row17',
|
|
630
|
+
ancestry: 'strict-descendant', mergeCorrespondence: null
|
|
631
|
+
};
|
|
632
|
+
events['ref-outline-confirmation'] = {
|
|
633
|
+
eventId: 'evt-outline-confirmation', producerId: 'fixture', identity: H.clone(snapshot.identity), paths,
|
|
634
|
+
provenance: 'deterministic row-17 host fixture', capability: 'outline-confirmation',
|
|
635
|
+
originRef: 'fake-origin:event:ref-outline-confirmation', kind: 'review', principalId: H.PRINCIPALS.reviewer,
|
|
636
|
+
verdict: 'approve', summary: 'Independent deterministic row 17 outline review.',
|
|
637
|
+
reviewedArtifactDigests: { ...outlineDigests, [inventoryPath]: inventoryDigest },
|
|
638
|
+
draftArtifactDigests: draftDigests, outlineEvidence: evidence, independentOf: [H.PRINCIPALS.author, H.PRINCIPALS.executor]
|
|
639
|
+
};
|
|
640
|
+
for (const [reference, event] of Object.entries(events)) origin.issueEvent(reference, event);
|
|
641
|
+
const accepted = mods[EVENTS].acceptDocEvents({ snapshot, eventRefs: Object.keys(events) }, { originProvider: origin });
|
|
642
|
+
const decision = mods[CORE].checkAcceptedDocEvidence({ snapshot, accepted, mode: 'verification' });
|
|
643
|
+
assert.strictEqual(accepted.state, 'ready', `production adapter must accept deterministic evidence: ${JSON.stringify(H.codesOf(accepted))}`);
|
|
644
|
+
assert.strictEqual(decision.decision, 'eligible', `production reconciliation must pass: ${JSON.stringify(H.codesOf(decision))}`);
|
|
645
|
+
assert.strictEqual(decision.eligible, true);
|
|
646
|
+
assert.deepStrictEqual(decision.expectedMustIds.sort(), sixMusts);
|
|
647
|
+
assert.deepStrictEqual(decision.checkedMustIds.sort(), sixMusts);
|
|
648
|
+
assert.deepStrictEqual(decision.unsatisfied, []);
|
|
649
|
+
});
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
if (failures.length) {
|
|
653
|
+
console.error(`\nDependency-artifact scope: ${failures.length} failing assertion-legs`);
|
|
654
|
+
process.exitCode = 1;
|
|
655
|
+
} else {
|
|
656
|
+
console.log('\nDependency-artifact scope: all assertions passed or explicitly unchecked');
|
|
657
|
+
}
|