harness-everything 0.43.1 → 0.45.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 (29) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +17 -1
  4. package/ci/fixtures/doc-obligations/reconciliation-s2a/harness.js +19 -1
  5. package/ci/mechanism-2ax-doc-lifecycle.test.js +186 -2
  6. package/ci/mechanism-325-f2-doc-craft.test.js +2 -2
  7. package/ci/mechanism-325-f3-derived-views.test.js +5 -2
  8. package/ci/mechanism-pr-a-conflict-check.test.js +507 -0
  9. package/docs/doc-workflow-reconciliation.md +2 -1
  10. package/docs/own-repo-governance-e1-plan-2026-10-25-2139.md +2 -0
  11. package/docs/platform-capabilities.md +4 -0
  12. package/harness-everything/SKILL.md +1 -1
  13. package/harness-everything/scripts/doc-check.js +84 -6
  14. package/harness-everything/scripts/doc-lifecycle.js +51 -6
  15. package/hooks/scripts/lib/doc-reconciliation-events.js +17 -0
  16. package/hooks/scripts/lib/doc-reconciliation-pilot.js +3 -0
  17. package/hooks/scripts/lib/doc-reconciliation-provider.js +80 -7
  18. package/opencode-plugin/plugin.json +1 -1
  19. package/package.json +1 -1
  20. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  21. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-events.js +17 -0
  22. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-pilot.js +3 -0
  23. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-provider.js +80 -7
  24. package/plugins/harness-everything/plugin.json +1 -1
  25. package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
  26. package/plugins/harness-everything/skills/harness-everything/scripts/doc-check.js +84 -6
  27. package/plugins/harness-everything/skills/harness-everything/scripts/doc-lifecycle.js +51 -6
  28. package/sbom.cdx.json +7 -7
  29. package/scripts/doc-governance-guard.js +77 -21
@@ -46,7 +46,9 @@ const HEX64 = /^[0-9a-f]{64}$/;
46
46
  const PRE_CAPTURE_TOOLS = ['Bash', 'PowerShell', 'Task', 'Agent'];
47
47
  const STDOUT_CAP_BYTES = 1024 * 1024;
48
48
  const DOC_CHECK_FLAGS = new Set(['--workspace', '--config', '--changed-path', '--base']);
49
+ const DOC_CHECK_BOOLEAN_FLAGS = new Set(['--conflict-assessment']);
49
50
  const DOC_CHECK_SCHEMA = 'harness.doc-check.check.v1';
51
+ const DOC_CHECK_CONFLICT_SCHEMA = 'harness.doc-check.conflict-assessment.v1';
50
52
  const DOC_CHECK_SHELLS = new Set(['sh', 'bash', 'dash', 'zsh', 'ksh', 'fish']);
51
53
  const DOC_CHECK_LAUNCHERS = new Set(['node', 'nodejs', 'bun', 'deno', 'tsx', 'ts-node']);
52
54
  const DOC_CHECK_LAUNCHER_VALUE_FLAGS = new Set(['-r', '--require', '--import', '--loader', '--experimental-loader', '--inspect-port', '--input-type', '--conditions', '--env-file']);
@@ -65,7 +67,7 @@ function isUncPathPrefix(token, command, index) {
65
67
  return token === '' && command[index] === '\\' && command[index + 1] === '\\';
66
68
  }
67
69
  // Canonical SHA-256 of harness-everything/scripts/doc-check.js (CRLF normalised to LF). Re-pin deliberately when that script changes.
68
- const SHIPPED_DOC_CHECK_DIGEST = 'da5e51a763eda6680afb63ee781b566253df9d358eec1e65436786c9ecebbbdb';
70
+ const SHIPPED_DOC_CHECK_DIGEST = '1afd94d66b356c94206d2441529ec0b7fafa1bcb453b0f1e2cf6994b1e4858e5';
69
71
  // This install's own doc-check runtime, relative to hooks/scripts/lib: the source checkout or the packaged plugin.
70
72
  const DOC_CHECK_INSTALL_LAYOUTS = [
71
73
  ['..', '..', '..', 'harness-everything', 'scripts', 'doc-check.js'],
@@ -357,10 +359,16 @@ function resolveDocCheckInvocation(command, cwd) {
357
359
 
358
360
  const values = new Map();
359
361
  const changedPaths = [];
360
- for (let index = 3; index < tokens.length; index += 2) {
361
- const flag = tokens[index];
362
- const value = tokens[index + 1];
362
+ let conflictAssessment = false;
363
+ for (let index = 3; index < tokens.length;) {
364
+ const flag = tokens[index++];
365
+ if (DOC_CHECK_BOOLEAN_FLAGS.has(flag)) {
366
+ if (conflictAssessment) return rejected('duplicate-flag');
367
+ conflictAssessment = true;
368
+ continue;
369
+ }
363
370
  if (!DOC_CHECK_FLAGS.has(flag)) return rejected('unknown-flag');
371
+ const value = tokens[index++];
364
372
  if (value === undefined || !value || value.startsWith('--')) return rejected('flag-value-missing');
365
373
  if (flag === '--changed-path') changedPaths.push(value);
366
374
  else {
@@ -371,6 +379,8 @@ function resolveDocCheckInvocation(command, cwd) {
371
379
  if (!values.has('--workspace') || !values.has('--config') || !changedPaths.length) {
372
380
  return rejected('required-flag-missing');
373
381
  }
382
+ if (conflictAssessment && !values.has('--base')) return rejected('conflict-assessment-base-required');
383
+ if (conflictAssessment && !/^[0-9a-f]{40,64}$/.test(values.get('--base'))) return rejected('conflict-assessment-base-unpinned');
374
384
 
375
385
  installed = installedDocCheck();
376
386
  if (!installed) return rejected('implementation-not-installed');
@@ -382,7 +392,8 @@ function resolveDocCheckInvocation(command, cwd) {
382
392
  if (fileDigest(registered.installed) !== SHIPPED_DOC_CHECK_DIGEST) return rejected('implementation-pin-mismatch', 'the installed doc-check is not the shipped script (canonical digest differs from SHIPPED_DOC_CHECK_DIGEST); no credit');
383
393
  }
384
394
  }
385
- return { kind: 'accepted', reason: null, installed: registered.installed, digest, argv };
395
+ return { kind: 'accepted', reason: null, installed: registered.installed, digest, argv,
396
+ conflictAssessment, base: values.get('--base') || null, changedPaths };
386
397
  }
387
398
 
388
399
  // Observation-time digests of the checked paths, read from the workspace when the check ran. Absent paths are omitted.
@@ -714,6 +725,9 @@ const createClaudeOriginProvider = input => {
714
725
  if (cap.event !== 'PostToolUse') return rejected('capability-mismatch', 'only a PostToolUse check invocation can cover a doc capability');
715
726
  const registered = resolveDocCheckInvocation(cap.command, cap.cwd);
716
727
  if (registered.kind === 'not-candidate') return rejected('unregistered-command', 'the command is not a registered doc-check invocation; an arbitrary successful shell command never covers a doc capability');
728
+ if (capability === 'conflict-check' && registered.conflictAssessment !== true) {
729
+ return rejected('capability-mismatch', 'conflict-check requires the structurally parsed --conflict-assessment mode');
730
+ }
717
731
  if (registered.kind === 'rejected' && registered.reason !== 'implementation-pin-mismatch') {
718
732
  if (registered.reason === 'implementation-not-installed') return rejected('implementation-not-installed', 'the registered check does not resolve to this install\'s own doc-check runtime; a different path earns no credit');
719
733
  return rejected('unregistered-command', `doc-check invocation rejected (${registered.reason}); no credit`);
@@ -744,11 +758,55 @@ const createClaudeOriginProvider = input => {
744
758
  }
745
759
  const paths = Array.isArray(projection.checkedPaths) ? projection.checkedPaths.slice() : [];
746
760
  if (!paths.length) return rejected('scope-empty', `${capability} checked no governed paths`);
761
+ let corpusBinding;
762
+ let baselineArtifactDigests;
763
+ if (capability === 'conflict-check') {
764
+ const assessment = report.conflictAssessment;
765
+ const corpus = assessment && assessment.corpus;
766
+ const config = corpus && corpus.config;
767
+ if (registered.conflictAssessment !== true || report.mode !== 'conflict-assessment'
768
+ || assessment?.schema !== DOC_CHECK_CONFLICT_SCHEMA || assessment.state !== 'pass'
769
+ || projection.corpus?.complete !== true || corpus?.complete !== true
770
+ || !Array.isArray(corpus.paths) || !corpus.paths.length || !recordOf(corpus.digests)
771
+ || !recordOf(config) || !textOf(config.path) || path.isAbsolute(config.path)
772
+ || config.path.split(/[\\/]/).includes('..') || !HEX64.test(String(config.digest || ''))
773
+ || !/^[0-9a-f]{40,64}$/.test(String(corpus.baseCommit || ''))
774
+ || JSON.stringify([...new Set(corpus.paths)].sort(compareKeys)) !== JSON.stringify(corpus.paths)
775
+ || JSON.stringify(Object.keys(corpus.digests).sort(compareKeys)) !== JSON.stringify(corpus.paths)
776
+ || corpus.paths.some(relative => !textOf(relative) || path.isAbsolute(relative)
777
+ || relative.split(/[\\/]/).includes('..') || !HEX64.test(String(corpus.digests[relative] || '')))) {
778
+ return rejected('corpus-binding-invalid', 'conflict-check stdout does not carry a complete, structured full-corpus assessment');
779
+ }
780
+ const observedCorpus = recordOf(cap.observedCorpusDigests) ? cap.observedCorpusDigests : {};
781
+ if (corpus.paths.some(relative => observedCorpus[relative] !== corpus.digests[relative])) {
782
+ return rejected('corpus-changed-after-check', 'a full-corpus path is missing or changed between assessment and capture');
783
+ }
784
+ if (cap.observedConfigDigest !== config.digest) {
785
+ return rejected('config-changed-after-check', 'the project-docs config changed between assessment and capture');
786
+ }
787
+ if (!recordOf(request.identity) || !HEX64.test(String(request.identity.baselineDigest || ''))
788
+ || !HEX64.test(String(request.identity.candidateDigest || '')) || !HEX64.test(String(request.identity.configDigest || ''))) {
789
+ return rejected('identity-missing', 'conflict-check needs the complete snapshot identity');
790
+ }
791
+ corpusBinding = {
792
+ complete: true,
793
+ paths: corpus.paths.slice(),
794
+ digests: { ...corpus.digests },
795
+ config: { ...config },
796
+ baseCommit: corpus.baseCommit,
797
+ baselineDigest: request.identity.baselineDigest,
798
+ candidateDigest: request.identity.candidateDigest,
799
+ configIdentityDigest: request.identity.configDigest,
800
+ };
801
+ baselineArtifactDigests = recordOf(assessment.baselineArtifacts) ? assessment.baselineArtifacts : {};
802
+ }
747
803
  const observed = recordOf(cap.observedDigests) ? cap.observedDigests : {};
748
804
  const artifactDigests = {};
805
+ const baselineArtifacts = {};
749
806
  for (const relative of paths) {
750
- if (typeof observed[relative] !== 'string') return rejected('artifact-unobserved', `no observation-time digest for ${relative}`);
751
- artifactDigests[relative] = observed[relative];
807
+ if (typeof observed[relative] === 'string') artifactDigests[relative] = observed[relative];
808
+ else if (capability === 'conflict-check' && HEX64.test(String(baselineArtifactDigests[relative] || ''))) baselineArtifacts[relative] = baselineArtifactDigests[relative];
809
+ else return rejected('artifact-unobserved', `no observation-time digest for ${relative}`);
752
810
  }
753
811
  const argv = ['node', ...registered.argv];
754
812
  const command = argv.join(' ');
@@ -772,6 +830,8 @@ const createClaudeOriginProvider = input => {
772
830
  endIdentity: recordOf(request.identity) ? JSON.parse(JSON.stringify(request.identity)) : null,
773
831
  paths,
774
832
  artifactDigests,
833
+ ...(Object.keys(baselineArtifacts).length ? { baselineArtifacts } : {}),
834
+ ...(corpusBinding ? { corpusBinding } : {}),
775
835
  captured: { responseRef: cap.responseRef, responseDigest: cap.responseDigest },
776
836
  };
777
837
  return { status: 'accepted', originRef, event, correlation: { toolCorrelationId: cap.tool_use_id, resultRef: cap.responseRef } };
@@ -1086,12 +1146,25 @@ function buildCaptureRecord(payload, eventName, workspaceRoot) {
1086
1146
  record.toolResponse = toolResponse;
1087
1147
  } else { record.stdout = null; record.stdoutTruncated = true; }
1088
1148
  let checkedPaths = [];
1149
+ let conflictCorpus = null;
1089
1150
  try {
1090
1151
  const report = JSON.parse(stdout);
1091
1152
  checkedPaths = ['doc-lint', 'link-check'].flatMap(capability => (recordOf(report.capabilities) && recordOf(report.capabilities[capability])
1092
1153
  && Array.isArray(report.capabilities[capability].checkedPaths) ? report.capabilities[capability].checkedPaths : []));
1154
+ const conflict = recordOf(report.capabilities) && report.capabilities['conflict-check'];
1155
+ if (installedCheck.conflictAssessment === true && report.mode === 'conflict-assessment'
1156
+ && recordOf(conflict) && conflict.state === 'pass' && recordOf(conflict.corpus)) {
1157
+ checkedPaths.push(...(Array.isArray(conflict.checkedPaths) ? conflict.checkedPaths : []));
1158
+ conflictCorpus = report.conflictAssessment && report.conflictAssessment.corpus;
1159
+ }
1093
1160
  } catch { /* not JSON: no paths observed, the event will be withheld */ }
1094
1161
  record.observedDigests = typeof workspaceRoot === 'string' && workspaceRoot ? observedDigestsFor(workspaceRoot, [...new Set(checkedPaths)]) : {};
1162
+ if (recordOf(conflictCorpus) && Array.isArray(conflictCorpus.paths) && recordOf(conflictCorpus.config)) {
1163
+ record.observedCorpusDigests = typeof workspaceRoot === 'string' && workspaceRoot
1164
+ ? observedDigestsFor(workspaceRoot, conflictCorpus.paths) : {};
1165
+ record.observedConfigDigest = typeof workspaceRoot === 'string' && workspaceRoot
1166
+ ? observedDigestsFor(workspaceRoot, [conflictCorpus.config.path])[conflictCorpus.config.path] || null : null;
1167
+ }
1095
1168
  }
1096
1169
  } else if (event === 'PostToolUseFailure') {
1097
1170
  record.is_interrupt = Boolean(payload.is_interrupt);
@@ -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.43.1",
4
+ "version": "0.45.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.45.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
 
@@ -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:e4de25fd-21a9-593b-841e-c8e0c3a9d6ad",
5
+ "serialNumber": "urn:uuid:16eef945-b68a-5405-9f2c-9d18949bad33",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-10-27T09:10:22.642Z",
8
+ "timestamp": "2026-10-27T11:52:14.799Z",
9
9
  "tools": [
10
10
  {
11
11
  "vendor": "Harness Everything",
12
12
  "name": "generate-sbom.js",
13
- "version": "0.43.1"
13
+ "version": "0.45.0"
14
14
  }
15
15
  ],
16
16
  "component": {
17
17
  "type": "application",
18
- "bom-ref": "pkg:npm/harness-everything@0.43.1",
18
+ "bom-ref": "pkg:npm/harness-everything@0.45.0",
19
19
  "name": "harness-everything",
20
- "version": "0.43.1",
21
- "purl": "pkg:npm/harness-everything@0.43.1"
20
+ "version": "0.45.0",
21
+ "purl": "pkg:npm/harness-everything@0.45.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.43.1",
2036
+ "ref": "pkg:npm/harness-everything@0.45.0",
2037
2037
  "dependsOn": [
2038
2038
  "pkg:npm/%40commitlint/cli@19.8.1",
2039
2039
  "pkg:npm/%40commitlint/config-conventional@19.8.1",
@@ -121,9 +121,8 @@ function validateProjectDocs(value, label) {
121
121
  if (!isRecord(record) || typeof record.path !== 'string' || !record.path || typeof record[field] !== 'string' || !record[field]) {
122
122
  throw new Error(`${label} has a malformed ${kind} record`);
123
123
  }
124
- if (record.path.startsWith('/') || record.path.split('/').includes('..') || /[*?\[\]]/.test(record.path)) {
125
- // shortcut: the baseline registry uses exact Git paths; glob support needs a Git-aware matcher before adoption.
126
- throw new Error(`${label} has a non-exact governed or derived path: ${record.path}`);
124
+ if (record.path.startsWith('/') || record.path.split('/').includes('..') || ['?', '[', ']', '{', '}'].some(char => record.path.includes(char))) {
125
+ throw new Error(`${label} has an unsupported governed or derived path pattern: ${record.path}`);
127
126
  }
128
127
  const key = `${kind}:${record.path}`;
129
128
  if (seen.has(key)) throw new Error(`${label} has duplicate ${kind} path: ${record.path}`);
@@ -159,8 +158,9 @@ function policyChanges(base, candidate, kind) {
159
158
  path: record.path,
160
159
  base: record,
161
160
  candidate: byPath.get(record.path) || null
162
- }));
163
- const added = candidate[kind].filter(record => !basePaths.has(record.path));
161
+ })).sort((left, right) => Buffer.compare(Buffer.from(left.path), Buffer.from(right.path)));
162
+ const added = candidate[kind].filter(record => !basePaths.has(record.path))
163
+ .sort((left, right) => Buffer.compare(Buffer.from(left.path), Buffer.from(right.path)));
164
164
  return { state: missingOrChanged.length || added.length ? 'review_required' : 'pass', preserved: base[kind].length - missingOrChanged.length, missingOrChanged, added };
165
165
  }
166
166
 
@@ -173,6 +173,32 @@ function pathText(pathBytes) {
173
173
  return Buffer.from(value, 'utf8').equals(pathBytes) ? value : null;
174
174
  }
175
175
 
176
+ function globToRegExp(glob) {
177
+ const source = Buffer.from(String(glob).replace(/\\/g, '/'), 'utf8').toString('latin1');
178
+ let pattern = '^';
179
+ for (let index = 0; index < source.length; index++) {
180
+ const char = source[index];
181
+ if (char === '*' && source[index + 1] === '*') {
182
+ if (source[index + 2] === '/') {
183
+ pattern += '(?:[\\s\\S]*/)?';
184
+ index += 2;
185
+ } else {
186
+ pattern += '[\\s\\S]*';
187
+ index++;
188
+ }
189
+ } else if (char === '*') {
190
+ pattern += '[^/]*';
191
+ } else {
192
+ pattern += char.replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
193
+ }
194
+ }
195
+ return new RegExp(`${pattern}$`);
196
+ }
197
+
198
+ function matchesConfiguredPath(record, pathBytes, matcher) {
199
+ return matcher ? matcher.test(pathBytes.toString('latin1')) : Buffer.from(record.path, 'utf8').equals(pathBytes);
200
+ }
201
+
176
202
  function identity(entry) {
177
203
  return entry ? { mode: entry.mode, type: entry.type, oid: entry.oid } : null;
178
204
  }
@@ -223,27 +249,57 @@ function unknownNewDocuments(baseTree, candidateTree) {
223
249
 
224
250
  function checkConfiguredPaths(baseTree, candidateTree, config) {
225
251
  const details = [];
252
+ const patterns = [];
226
253
  for (const kind of ['governed', 'derived']) {
227
254
  for (const record of config[kind]) {
228
- const key = pathKey(record.path);
229
- const before = baseTree.get(key);
230
- const after = candidateTree.get(key);
231
- if (!regularBlob(before)) throw new Error(`baseline ${kind} path is missing or not a regular file: ${record.path}`);
232
- const sameRegularMode = regularBlob(after) && before.mode === after.mode;
233
- const revised = kind === 'governed' && sameRegularMode && before.oid !== after.oid;
234
- const identityPreserved = sameRegularMode && before.oid === after.oid;
235
- details.push({
236
- kind,
237
- path: record.path,
238
- state: revised ? 'revised' : identityPreserved ? 'pass' : 'review_required',
239
- base: identity(before),
240
- candidate: identity(after),
241
- ...(revised ? { reason: 'Content revisions are tracked by Git identity but are not semantically validated by the guard.' } : {})
242
- });
255
+ const glob = record.path.includes('*');
256
+ const matcher = glob ? globToRegExp(record.path) : null;
257
+ const collect = sourceTree => {
258
+ if (!glob) {
259
+ const key = pathKey(record.path);
260
+ const entry = sourceTree.get(key);
261
+ return new Map(entry ? [[key, entry]] : []);
262
+ }
263
+ const matches = new Map();
264
+ for (const [key, entry] of sourceTree) {
265
+ if (matchesConfiguredPath(record, entry.pathBytes, matcher)) matches.set(key, entry);
266
+ }
267
+ return matches;
268
+ };
269
+ const baseMembers = collect(baseTree);
270
+ const candidateMembers = collect(candidateTree);
271
+ if (!glob && !regularBlob(baseMembers.values().next().value)) {
272
+ throw new Error(`baseline ${kind} path is missing or not a regular file: ${record.path}`);
273
+ }
274
+ if (glob) patterns.push({ kind, path: record.path, baseMembers: baseMembers.size, candidateMembers: candidateMembers.size });
275
+ const keys = new Set([...baseMembers.keys(), ...candidateMembers.keys()]);
276
+ for (const key of keys) {
277
+ const before = baseMembers.get(key) || null;
278
+ const after = candidateMembers.get(key) || null;
279
+ const entry = before || after;
280
+ const sameRegularMode = regularBlob(before) && regularBlob(after) && before.mode === after.mode;
281
+ const revised = kind === 'governed' && sameRegularMode && before.oid !== after.oid;
282
+ const identityPreserved = sameRegularMode && before.oid === after.oid;
283
+ const memberPath = pathText(entry.pathBytes);
284
+ details.push({
285
+ kind,
286
+ ...(glob ? { pattern: record.path } : {}),
287
+ path: memberPath,
288
+ ...(memberPath === null ? { pathBase64: key } : {}),
289
+ state: revised ? 'revised' : identityPreserved ? 'pass' : 'review_required',
290
+ base: identity(before),
291
+ candidate: identity(after),
292
+ ...(revised ? { reason: 'Content revisions are tracked by Git identity but are not semantically validated by the guard.' } : {})
293
+ });
294
+ }
243
295
  }
244
296
  }
297
+ patterns.sort((left, right) => Buffer.compare(Buffer.from(left.kind), Buffer.from(right.kind)) || Buffer.compare(Buffer.from(left.path), Buffer.from(right.path)));
298
+ details.sort((left, right) => Buffer.compare(Buffer.from(left.pathBase64 || Buffer.from(left.path, 'utf8').toString('base64'), 'base64'), Buffer.from(right.pathBase64 || Buffer.from(right.path, 'utf8').toString('base64'), 'base64'))
299
+ || Buffer.compare(Buffer.from(left.kind), Buffer.from(right.kind))
300
+ || Buffer.compare(Buffer.from(left.pattern || ''), Buffer.from(right.pattern || '')));
245
301
  const failures = details.filter(item => item.state === 'review_required');
246
- return { state: failures.length ? 'review_required' : 'pass', paths: details, missingOrChanged: failures };
302
+ return { state: failures.length ? 'review_required' : 'pass', patterns, paths: details, missingOrChanged: failures };
247
303
  }
248
304
 
249
305
  function parseArgs(argv) {