harness-everything 0.44.0 → 0.46.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 (46) hide show
  1. package/.agents/harness-everything/project-docs.json +3 -1
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/CHANGELOG.md +27 -0
  5. package/ci/fixtures/doc-obligations/read-boundary/boundary-policy.json +2 -2
  6. package/ci/fixtures/doc-obligations/reconciliation-s2a/harness.js +19 -1
  7. package/ci/mechanism-2ax-doc-lifecycle.test.js +2 -1
  8. package/ci/mechanism-325-3b-s2b-capture.test.js +6 -1
  9. package/ci/mechanism-325-f2-doc-craft.test.js +2 -2
  10. package/ci/mechanism-325-f3-derived-views.test.js +5 -2
  11. package/ci/mechanism-325-f4-projections.test.js +11 -1
  12. package/ci/mechanism-pr-a-conflict-check.test.js +507 -0
  13. package/ci/mechanism-prb-outline-review.test.js +1265 -0
  14. package/docs/adr/DOCS-001-project-docs-config.md +32 -0
  15. package/docs/adr/DOCS-002-adr-identity-supersession-index.md +34 -0
  16. package/docs/adr/DOCS-003-prd-revision-contract.md +29 -0
  17. package/docs/adr/DOCS-004-doc-change-step-type.md +28 -0
  18. package/docs/adr/DOCS-005-doc-obligations-evidence-producer.md +29 -0
  19. package/docs/adr/DOCS-006-reconciliation-pilot.md +50 -0
  20. package/docs/adr/DOCS-007-shared-doc-craft-derived-views.md +31 -0
  21. package/docs/adr/DOCS-008-obligation-abstraction-bounded-consumer.md +30 -0
  22. package/docs/doc-workflow-reconciliation.md +32 -1
  23. package/docs/governed-index.md +8 -0
  24. package/docs/platform-capabilities.md +6 -0
  25. package/harness-everything/SKILL.md +1 -1
  26. package/harness-everything/scripts/doc-check.js +84 -6
  27. package/harness-everything/scripts/doc-evidence.js +55 -0
  28. package/harness-everything/scripts/doc-lifecycle.js +51 -6
  29. package/hooks/hooks.json +1 -1
  30. package/hooks/scripts/lib/doc-reconciliation-events.js +138 -9
  31. package/hooks/scripts/lib/doc-reconciliation-pilot.js +131 -15
  32. package/hooks/scripts/lib/doc-reconciliation-provider.js +941 -13
  33. package/hooks/scripts/state-persist.js +10 -0
  34. package/opencode-plugin/plugin.json +1 -1
  35. package/package.json +1 -1
  36. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  37. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-events.js +138 -9
  38. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-pilot.js +131 -15
  39. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-provider.js +941 -13
  40. package/plugins/harness-everything/hooks/scripts/state-persist.js +10 -0
  41. package/plugins/harness-everything/plugin.json +1 -1
  42. package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
  43. package/plugins/harness-everything/skills/harness-everything/scripts/doc-check.js +84 -6
  44. package/plugins/harness-everything/skills/harness-everything/scripts/doc-evidence.js +55 -0
  45. package/plugins/harness-everything/skills/harness-everything/scripts/doc-lifecycle.js +51 -6
  46. package/sbom.cdx.json +7 -7
@@ -148,6 +148,16 @@ function persistPilotCapture(payload, eventName, sessionId, root) {
148
148
  // Observation-time identity: the workflow and pin in force when the host event was captured.
149
149
  const observed = provider.observedIdentityFor(sessionDir);
150
150
  if (observed) entry.observedIdentity = observed;
151
+ if (eventName === 'PreToolUse' && (entry.tool_name === 'Agent' || entry.tool_name === 'Task')) {
152
+ try {
153
+ const existing = provider.readCaptureView(sessionDir);
154
+ entry.outlineDispatch = existing.tampered
155
+ ? { schema: 'harness.outline-dispatch.v1', state: 'unresolved', reasons: ['capture-view-tampered'] }
156
+ : provider.captureOutlineDispatch({ payload, capture: entry, sessionDir, workspaceRoot: root, captures: existing.records });
157
+ } catch {
158
+ entry.outlineDispatch = { schema: 'harness.outline-dispatch.v1', state: 'unresolved', reasons: ['dispatch-capture-failed'] };
159
+ }
160
+ }
151
161
  // Each capture is one exclusively created event file; the compatibility journal is rebuilt from them (not tamper-proof).
152
162
  provider.writeImmutableEvent(sessionDir, entry);
153
163
  provider.rebuildJournalView(sessionDir);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "harness-everything",
4
- "version": "0.44.0",
4
+ "version": "0.46.0",
5
5
  "description": "Invariant-first engineering harness for routing, verification, and failure recovery while preserving agent autonomy.",
6
6
  "author": {
7
7
  "name": "Miya Daniel",
@@ -4,7 +4,7 @@ description: "Route software/project work through Harness: classify tier/topolog
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  author: Miya Daniel
7
- version: 0.43.0
7
+ version: 0.46.0
8
8
  ---
9
9
 
10
10
  # Harness Everything
@@ -6,14 +6,16 @@
6
6
  // Prints one JSON result. Exit 0 = both pass, 1 = defect, 2 = usage/config error.
7
7
  const fs = require('fs');
8
8
  const path = require('path');
9
+ const crypto = require('crypto');
9
10
  const { execFileSync, spawnSync } = require('child_process');
10
11
  const { collectDocs } = require('./doc-lifecycle');
11
12
 
12
13
  // Logical command IDs (A3 section 3): stdout that repeats an ID is not trusted as credit by itself.
13
14
  const CHECK_ID = 'harness.doc-check.check.v1';
14
15
  const COMPARE_ID = 'harness.doc-check.compare-derived.v1';
15
- const USAGE = 'usage: doc-check.js check --workspace <dir> --config <json-file, relative to --workspace> --changed-path <path> [--changed-path <path> ...] [--base <git-rev>]';
16
+ const USAGE = 'usage: doc-check.js check --workspace <dir> --config <json-file, relative to --workspace> --changed-path <path> [--changed-path <path> ...] [--base <git-rev>] [--conflict-assessment]';
16
17
  const VALUE_FLAGS = ['--workspace', '--config', '--changed-path', '--base'];
18
+ const CONFLICT_SCHEMA = 'harness.doc-check.conflict-assessment.v1';
17
19
  const EXTERNAL = /^(?:https?:|mailto:)/i;
18
20
  const SCHEME = /^[a-z][a-z0-9+.-]*:/i;
19
21
 
@@ -79,10 +81,15 @@ function parseArgs(argv) {
79
81
  const [command, ...rest] = argv;
80
82
  if (command !== 'check') throw new Error(USAGE);
81
83
  const options = { changedPaths: [] };
82
- for (let index = 0; index < rest.length; index += 2) {
84
+ for (let index = 0; index < rest.length; index++) {
83
85
  const flag = rest[index];
84
- const value = rest[index + 1];
85
- if (!VALUE_FLAGS.includes(flag) || value === undefined) throw new Error(`${USAGE}\nunknown or incomplete argument: ${flag}`);
86
+ if (flag === '--conflict-assessment') {
87
+ if (options.conflictAssessment) throw new Error(`${USAGE}\nrepeated argument: ${flag}`);
88
+ options.conflictAssessment = true;
89
+ continue;
90
+ }
91
+ if (!VALUE_FLAGS.includes(flag) || rest[index + 1] === undefined) throw new Error(`${USAGE}\nunknown or incomplete argument: ${flag}`);
92
+ const value = rest[++index];
86
93
  if (value.startsWith('--')) throw new Error(`${USAGE}\n${flag} requires a value, not a flag: ${value}`);
87
94
  if (flag === '--changed-path') options.changedPaths.push(value);
88
95
  else {
@@ -92,16 +99,47 @@ function parseArgs(argv) {
92
99
  }
93
100
  if (!options.workspace || !options.config) throw new Error(`${USAGE}\n--workspace and --config are required`);
94
101
  if (!options.changedPaths.length) throw new Error(`${USAGE}\n--changed-path is required`);
102
+ if (options.conflictAssessment && options.base === undefined) throw new Error(`${USAGE}\n--conflict-assessment requires --base`);
95
103
  return options;
96
104
  }
97
105
 
98
106
  function runLint(workspace, options) {
99
107
  const args = [path.join(__dirname, 'doc-lifecycle.js'), 'lint', '--workspace', workspace, '--config', options.config];
100
- for (const changed of options.changedPaths) args.push('--changed-path', changed);
108
+ if (options.conflictAssessment) args.push('--all');
109
+ else for (const changed of options.changedPaths) args.push('--changed-path', changed);
101
110
  if (options.base !== undefined) args.push('--base', options.base);
102
111
  return spawnSync(process.execPath, args, { encoding: 'utf8' });
103
112
  }
104
113
 
114
+ function workspaceRelative(workspace, value) {
115
+ const absolute = path.resolve(workspace, value);
116
+ const relative = path.relative(workspace, absolute);
117
+ if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) return null;
118
+ return relative.split(path.sep).join('/');
119
+ }
120
+
121
+ function resolvedBaseCommit(workspace, base) {
122
+ if (typeof base !== 'string' || !base || base.startsWith('-')) return null;
123
+ try {
124
+ const value = execFileSync('git', ['-C', workspace, 'rev-parse', '--verify', `${base}^{commit}`], {
125
+ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore']
126
+ }).trim();
127
+ return /^[0-9a-f]{40,64}$/.test(value) ? value : null;
128
+ } catch { return null; }
129
+ }
130
+
131
+ function baselineArtifacts(workspace, base, paths) {
132
+ const artifacts = {};
133
+ for (const relative of paths) {
134
+ if (fs.existsSync(path.join(workspace, relative))) continue;
135
+ try {
136
+ const bytes = execFileSync('git', ['-C', workspace, 'show', `${base}:${relative}`], { encoding: 'buffer', stdio: ['ignore', 'pipe', 'ignore'] });
137
+ artifacts[relative] = crypto.createHash('sha256').update(bytes).digest('hex');
138
+ } catch { /* Paths absent from both snapshots need no baseline artifact. */ }
139
+ }
140
+ return artifacts;
141
+ }
142
+
105
143
  // Scan nested label spans depth-first, then resume after each destination. Matching brackets are precomputed per line,
106
144
  // and an explicit stack keeps nesting from consuming the JS call stack. A nested link inside a label remains an
107
145
  // intentional over-check: CommonMark keeps only the inner link, so both targets are checked and neither is silent.
@@ -340,7 +378,8 @@ function check(options) {
340
378
  try { analysis = JSON.parse(lint.stdout); } catch {
341
379
  return emit({ schema: CHECK_ID, command: CHECK_ID, lint: { exit: 2, error: 'lint output was not JSON', diagnostics: [] }, links: { checked: 0, broken: [], skipped: 'lint-usage-error' }, coverage: { unsupported: [] } }, 2);
342
380
  }
343
- const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
381
+ const configBytes = fs.readFileSync(configPath);
382
+ const config = JSON.parse(configBytes.toString('utf8'));
344
383
  const docs = collectDocs(workspace, config);
345
384
  const governed = new Set(docs.filter(doc => doc.role).map(doc => doc.path));
346
385
 
@@ -408,6 +447,7 @@ function check(options) {
408
447
  const result = {
409
448
  schema: CHECK_ID,
410
449
  command: CHECK_ID,
450
+ ...(options.conflictAssessment ? { mode: 'conflict-assessment' } : {}),
411
451
  lint: { exit: lintExit, evidence: analysis.evidence, diagnostics: analysis.diagnostics },
412
452
  links: { checked, broken },
413
453
  coverage: { unsupported },
@@ -425,6 +465,44 @@ function check(options) {
425
465
  },
426
466
  }
427
467
  };
468
+ if (options.conflictAssessment) {
469
+ const scopePaths = [...new Set(options.changedPaths.map(value => workspaceRelative(workspace, value)).filter(Boolean))].sort();
470
+ const corpus = analysis.corpus;
471
+ const baseCommit = resolvedBaseCommit(workspace, options.base);
472
+ const configRelative = workspaceRelative(workspace, options.config);
473
+ let reason = null;
474
+ if (!scopePaths.length || scopePaths.length !== new Set(options.changedPaths.map(value => workspaceRelative(workspace, value))).size) reason = 'scope-unavailable';
475
+ else if (!Array.isArray(config.domains) || config.domains.length === 0) reason = 'domains-unknown';
476
+ else if (!corpus || corpus.complete !== true || !Array.isArray(corpus.paths) || !corpus.paths.length) reason = 'corpus-incomplete';
477
+ else if (!corpus.digests || Object.keys(corpus.digests).length !== corpus.paths.length
478
+ || corpus.paths.some(relative => !/^[0-9a-f]{64}$/.test(corpus.digests[relative] || ''))) reason = 'corpus-digests-incomplete';
479
+ else if (!baseCommit) reason = 'base-unavailable';
480
+ else if (options.base !== baseCommit) reason = 'base-not-pinned';
481
+ else if (!configRelative) reason = 'config-outside-workspace';
482
+ else if (!corpus.config || corpus.config.path !== configRelative) reason = 'config-unavailable';
483
+ else if (corpus.config.digest !== crypto.createHash('sha256').update(configBytes).digest('hex')) reason = 'config-changed-during-check';
484
+ const hasErrors = lintExit !== 0 || !Array.isArray(analysis.diagnostics)
485
+ || analysis.diagnostics.some(item => item && item.severity === 'error');
486
+ const corpusIdentity = corpus && {
487
+ complete: corpus.complete === true,
488
+ paths: corpus.paths,
489
+ digests: corpus.digests,
490
+ config: corpus.config,
491
+ baseCommit,
492
+ };
493
+ const state = reason ? 'unavailable' : hasErrors ? 'fail' : 'pass';
494
+ result.conflictAssessment = {
495
+ schema: CONFLICT_SCHEMA,
496
+ state,
497
+ ...(reason ? { reason } : {}),
498
+ ...(corpusIdentity ? { corpus: corpusIdentity } : {}),
499
+ ...(state === 'pass' ? { baselineArtifacts: baselineArtifacts(workspace, baseCommit, scopePaths) } : {}),
500
+ };
501
+ if (state === 'pass') result.capabilities['conflict-check'] = {
502
+ state: 'pass', checkedPaths: scopePaths, corpus: corpusIdentity,
503
+ };
504
+ return emit(result, state === 'unavailable' ? 2 : lintExit === 1 || broken.length ? 1 : 0);
505
+ }
428
506
  return emit(result, lintExit === 1 || broken.length ? 1 : 0);
429
507
  }
430
508
 
@@ -433,6 +433,61 @@ function evaluateObligation(obligation, context) {
433
433
  return entry ? baselineBound(item, entry) : candidateBound(item, path);
434
434
  };
435
435
 
436
+ if (capability === 'outline-confirmation') {
437
+ const candidates = matching.filter(item => reviewList.includes(item) && item.kind === 'outline-confirmation');
438
+ if (candidates.length !== 1) {
439
+ return result('unresolved', 'none',
440
+ 'outline-confirmation requires one validated pre-draft O/review/handoff/dispatch record and a final D; generic execution and waivers do not qualify',
441
+ candidates.map(item => item.id));
442
+ }
443
+ const item = candidates[0];
444
+ const evidence = isPlainObject(item.outlineEvidence) ? item.outlineEvidence : null;
445
+ const sha = value => isSha256(value);
446
+ const digestsMatch = map => isPlainObject(map)
447
+ && Object.keys(map).sort(compareText).join('\0') === scopePaths.slice().sort(compareText).join('\0')
448
+ && scopePaths.every(path => sha(map[path]));
449
+ const identityValid = Boolean(evidence)
450
+ && evidence.schema === 'harness.outline-confirmation.v1' && evidence.state === 'validated'
451
+ && nonEmptyString(evidence.taskId) && nonEmptyString(evidence.attemptId)
452
+ && /^\d{1,2}$/.test(String(evidence.inventoryRowId || ''))
453
+ && /^[A-Z][A-Z0-9]{1,9}$/.test(String(evidence.domain || ''))
454
+ && sha(evidence.inventoryPinDigest) && sha(evidence.inventoryArtifactDigest)
455
+ && /^[0-9a-f]{40,64}$/.test(String(evidence.baselineCommit || ''))
456
+ && /^[0-9a-f]{40,64}$/.test(String(evidence.outlineCommit || ''))
457
+ && /^[0-9a-f]{40,64}$/.test(String(evidence.draftCommit || ''))
458
+ && /^[0-9a-f]{40,64}$/.test(String(evidence.mergedCommit || ''))
459
+ && evidence.outlineCommit !== evidence.draftCommit
460
+ && evidence.ancestry === 'strict-descendant'
461
+ && sha(evidence.handoffDigest) && sha(evidence.reviewCaptureDigest) && sha(evidence.reviewDigest)
462
+ && sha(evidence.seamDigest) && sha(evidence.dispatchInputDigest) && sha(evidence.writerLeaseDigest)
463
+ && nonEmptyString(evidence.dispatchToolUseId) && nonEmptyString(evidence.coordinatorPrincipal)
464
+ && nonEmptyString(evidence.reviewerPrincipal) && nonEmptyString(evidence.workerPrincipal)
465
+ && evidence.reviewerPrincipal !== evidence.coordinatorPrincipal
466
+ && evidence.reviewerPrincipal !== evidence.workerPrincipal
467
+ && Array.isArray(evidence.targetPaths)
468
+ && evidence.targetPaths.slice().sort(compareText).join('\0') === scopePaths.slice().sort(compareText).join('\0')
469
+ && digestsMatch(evidence.outlineArtifactDigests) && digestsMatch(evidence.draftArtifactDigests)
470
+ && digestsMatch(item.artifactDigests)
471
+ && scopePaths.every(path => evidence.draftArtifactDigests[path] === item.artifactDigests[path]
472
+ && candidateBound(item, path));
473
+ const mergeValid = evidence && (evidence.mergeCorrespondence === null
474
+ || (isPlainObject(evidence.mergeCorrespondence)
475
+ && ['accepted', 'changed'].includes(evidence.mergeCorrespondence.state)
476
+ && Array.isArray(evidence.mergeCorrespondence.targetPaths)
477
+ && evidence.mergeCorrespondence.targetPaths.slice().sort(compareText).join('\0')
478
+ === scopePaths.slice().sort(compareText).join('\0')));
479
+ const outlineProblems = [];
480
+ if (!identityValid) outlineProblems.push('envelope-or-current-D-binding');
481
+ if (!mergeValid) outlineProblems.push('merge-correspondence');
482
+ if (item.baseDigest !== baselineDigest) outlineProblems.push('original-B-digest');
483
+ if (outlineProblems.length) {
484
+ diagnostics.push({ code: 'outline-evidence-unresolved', obligationId,
485
+ message: `the outline evidence is incomplete or inconsistent: ${outlineProblems.join(', ')}` });
486
+ return result('unresolved', 'none', 'outline evidence is incomplete or unbound to the original B and final D', [item.id]);
487
+ }
488
+ return result('observed', 'pass', 'validated independent review of exact O, coordinator handoff, clean one-use dispatch, pinned inventory, and strict O-to-D evidence', [item.id]);
489
+ }
490
+
436
491
  for (const item of matching) {
437
492
  if (executionList.includes(item) && !completeExecution(item)) {
438
493
  diagnostics.push({
@@ -265,7 +265,16 @@ function directoryCanContainMatch(relativeDirectory, globs, exactPaths = []) {
265
265
  || exactPathCanMatchDescendant(relativeDirectory, exactPaths);
266
266
  }
267
267
 
268
- function collectDocs(root, config) {
268
+ function documentCorpusPaths(records, config) {
269
+ const selectors = [
270
+ ...(Array.isArray(config && config.governed) ? config.governed.map(entry => entry && entry.path) : []),
271
+ ...(Array.isArray(config && config.legacy) ? config.legacy : [])
272
+ ].filter(value => typeof value === 'string');
273
+ return [...new Set((records || []).filter(item => item && item.presence === 'present' && item.type === 'file'
274
+ && typeof item.path === 'string' && matchesAny(item.path, selectors)).map(item => item.path))].sort(compareText);
275
+ }
276
+
277
+ function collectDocs(root, config, coverage = null) {
269
278
  const workspace = path.resolve(root);
270
279
  const governed = Array.isArray(config && config.governed) ? config.governed : [];
271
280
  const legacyGlobs = Array.isArray(config && config.legacy) ? config.legacy : [];
@@ -274,9 +283,14 @@ function collectDocs(root, config) {
274
283
  ...legacyGlobs
275
284
  ].filter(value => typeof value === 'string');
276
285
  const files = [];
286
+ const skipped = [];
287
+ const skip = (relative, reason) => skipped.push({ path: relative || '.', reason });
277
288
  function walk(directory, relativeDirectory) {
278
289
  let entries;
279
- try { entries = fs.readdirSync(directory, { withFileTypes: true }); } catch { return; }
290
+ try { entries = fs.readdirSync(directory, { withFileTypes: true }); } catch {
291
+ skip(relativeDirectory, 'unreadable-directory');
292
+ return;
293
+ }
280
294
  entries.sort((a, b) => compareText(a.name, b.name));
281
295
  for (const entry of entries) {
282
296
  if (entry.name === '.git' || entry.name === 'node_modules') continue;
@@ -285,6 +299,11 @@ function collectDocs(root, config) {
285
299
  if (entry.isDirectory()) {
286
300
  if (directoryCanContainMatch(relativePath, globs)) walk(absolute, relativePath);
287
301
  } else if (entry.isFile() && matchesAny(relativePath, globs)) files.push(absolute);
302
+ else if (entry.isSymbolicLink() && (matchesAny(relativePath, globs) || directoryCanContainMatch(relativePath, globs))) {
303
+ skip(relativePath, 'unsupported-symlink');
304
+ } else if (!entry.isDirectory() && !entry.isFile() && matchesAny(relativePath, globs)) {
305
+ skip(relativePath, 'unsupported-entry');
306
+ }
288
307
  }
289
308
  }
290
309
  walk(workspace, '');
@@ -296,9 +315,23 @@ function collectDocs(root, config) {
296
315
  const legacy = matchesAny(relativePath, legacyGlobs);
297
316
  if (!roleEntry && !legacy) continue;
298
317
  let bytes;
299
- try { bytes = fs.readFileSync(absolute); } catch { continue; }
318
+ try { bytes = fs.readFileSync(absolute); } catch {
319
+ skip(relativePath, 'unreadable-file');
320
+ continue;
321
+ }
300
322
  selected.push({ relativePath, role: roleEntry ? roleEntry.role : null, legacy, bytes });
301
323
  }
324
+ for (const selector of globs.filter(value => !/[*?\[\]{}]/.test(value))) {
325
+ if (!files.includes(path.resolve(workspace, selector))) skip(selector, 'missing-configured-path');
326
+ }
327
+ if (coverage) {
328
+ coverage.complete = globs.length > 0 && skipped.length === 0;
329
+ coverage.skipped = skipped;
330
+ coverage.paths = selected.map(item => item.relativePath).sort(compareText);
331
+ coverage.digests = Object.fromEntries(selected
332
+ .sort((a, b) => compareText(a.relativePath, b.relativePath))
333
+ .map(item => [item.relativePath, crypto.createHash('sha256').update(item.bytes).digest('hex')]));
334
+ }
302
335
  const rules = readWorkingTreeRules(workspace, selected.filter(item => item.bytes.includes(0x0d)).map(item => item.relativePath));
303
336
  const docs = [];
304
337
  for (const item of selected) {
@@ -2664,7 +2697,11 @@ function main(argv = process.argv.slice(2)) {
2664
2697
  ? path.resolve(options.configFile)
2665
2698
  : path.resolve(workspacePath, options.configFile);
2666
2699
  let config;
2667
- try { config = JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch (error) {
2700
+ let configBytes;
2701
+ try {
2702
+ configBytes = fs.readFileSync(configPath);
2703
+ config = JSON.parse(configBytes.toString('utf8'));
2704
+ } catch (error) {
2668
2705
  process.stderr.write(`cannot read config: ${error.message}\n`);
2669
2706
  return 2;
2670
2707
  }
@@ -2672,7 +2709,14 @@ function main(argv = process.argv.slice(2)) {
2672
2709
  process.stderr.write(`${error.message}\n`);
2673
2710
  return 2;
2674
2711
  }
2675
- const docs = collectDocs(workspacePath, config);
2712
+ const corpus = options.all ? { complete: false, skipped: [], paths: [], digests: {} } : null;
2713
+ const docs = collectDocs(workspacePath, config, corpus);
2714
+ if (corpus) {
2715
+ const relativeConfig = path.relative(workspacePath, configPath);
2716
+ corpus.config = relativeConfig && relativeConfig !== '..' && !relativeConfig.startsWith(`..${path.sep}`)
2717
+ ? { path: relativeConfig.split(path.sep).join('/'), digest: crypto.createHash('sha256').update(configBytes).digest('hex') }
2718
+ : null;
2719
+ }
2676
2720
  const mode = {
2677
2721
  ...(options.all ? { all: true } : { changedPaths: options.changedPaths }),
2678
2722
  workspace: workspacePath, base: options.base, configPath,
@@ -2688,10 +2732,11 @@ function main(argv = process.argv.slice(2)) {
2688
2732
  }));
2689
2733
  return 0;
2690
2734
  }
2735
+ if (corpus) analysis.corpus = corpus;
2691
2736
  process.stdout.write(`${JSON.stringify(analysis, null, 2)}\n`);
2692
2737
  return analysis.diagnostics.some(item => item.severity === 'error') ? 1 : 0;
2693
2738
  }
2694
2739
 
2695
- module.exports = { readGitBaseInventory, scanWorkspaceFiles, parseFrontmatter, parseRequirementBlocks, collectDocs, analyzeDocs, resolveRef, lintDocs, buildIndex, diffRevisions, buildGitDocumentInventory };
2740
+ module.exports = { readGitBaseInventory, scanWorkspaceFiles, parseFrontmatter, parseRequirementBlocks, collectDocs, documentCorpusPaths, analyzeDocs, resolveRef, lintDocs, buildIndex, diffRevisions, buildGitDocumentInventory };
2696
2741
 
2697
2742
  if (require.main === module) process.exitCode = main();
package/sbom.cdx.json CHANGED
@@ -2,23 +2,23 @@
2
2
  "$schema": "https://cyclonedx.org/schema/bom-1.7.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.7",
5
- "serialNumber": "urn:uuid:062d938e-8477-5564-9f89-fa21e9121182",
5
+ "serialNumber": "urn:uuid:4e82c3bc-ded1-5f78-8aa6-6dd26488f41f",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-10-27T10:11:43.769Z",
8
+ "timestamp": "2026-10-28T00:05:02.172Z",
9
9
  "tools": [
10
10
  {
11
11
  "vendor": "Harness Everything",
12
12
  "name": "generate-sbom.js",
13
- "version": "0.44.0"
13
+ "version": "0.46.0"
14
14
  }
15
15
  ],
16
16
  "component": {
17
17
  "type": "application",
18
- "bom-ref": "pkg:npm/harness-everything@0.44.0",
18
+ "bom-ref": "pkg:npm/harness-everything@0.46.0",
19
19
  "name": "harness-everything",
20
- "version": "0.44.0",
21
- "purl": "pkg:npm/harness-everything@0.44.0"
20
+ "version": "0.46.0",
21
+ "purl": "pkg:npm/harness-everything@0.46.0"
22
22
  }
23
23
  },
24
24
  "components": [
@@ -2033,7 +2033,7 @@
2033
2033
  ]
2034
2034
  },
2035
2035
  {
2036
- "ref": "pkg:npm/harness-everything@0.44.0",
2036
+ "ref": "pkg:npm/harness-everything@0.46.0",
2037
2037
  "dependsOn": [
2038
2038
  "pkg:npm/%40commitlint/cli@19.8.1",
2039
2039
  "pkg:npm/%40commitlint/config-conventional@19.8.1",