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.
- package/.agents/harness-everything/project-docs.json +3 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +27 -0
- package/ci/fixtures/doc-obligations/read-boundary/boundary-policy.json +2 -2
- package/ci/fixtures/doc-obligations/reconciliation-s2a/harness.js +19 -1
- package/ci/mechanism-2ax-doc-lifecycle.test.js +2 -1
- package/ci/mechanism-325-3b-s2b-capture.test.js +6 -1
- package/ci/mechanism-325-f2-doc-craft.test.js +2 -2
- package/ci/mechanism-325-f3-derived-views.test.js +5 -2
- package/ci/mechanism-325-f4-projections.test.js +11 -1
- package/ci/mechanism-pr-a-conflict-check.test.js +507 -0
- package/ci/mechanism-prb-outline-review.test.js +1265 -0
- package/docs/adr/DOCS-001-project-docs-config.md +32 -0
- package/docs/adr/DOCS-002-adr-identity-supersession-index.md +34 -0
- package/docs/adr/DOCS-003-prd-revision-contract.md +29 -0
- package/docs/adr/DOCS-004-doc-change-step-type.md +28 -0
- package/docs/adr/DOCS-005-doc-obligations-evidence-producer.md +29 -0
- package/docs/adr/DOCS-006-reconciliation-pilot.md +50 -0
- package/docs/adr/DOCS-007-shared-doc-craft-derived-views.md +31 -0
- package/docs/adr/DOCS-008-obligation-abstraction-bounded-consumer.md +30 -0
- package/docs/doc-workflow-reconciliation.md +32 -1
- package/docs/governed-index.md +8 -0
- package/docs/platform-capabilities.md +6 -0
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/scripts/doc-check.js +84 -6
- package/harness-everything/scripts/doc-evidence.js +55 -0
- package/harness-everything/scripts/doc-lifecycle.js +51 -6
- package/hooks/hooks.json +1 -1
- package/hooks/scripts/lib/doc-reconciliation-events.js +138 -9
- package/hooks/scripts/lib/doc-reconciliation-pilot.js +131 -15
- package/hooks/scripts/lib/doc-reconciliation-provider.js +941 -13
- package/hooks/scripts/state-persist.js +10 -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-events.js +138 -9
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-pilot.js +131 -15
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-provider.js +941 -13
- package/plugins/harness-everything/hooks/scripts/state-persist.js +10 -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-check.js +84 -6
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-evidence.js +55 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-lifecycle.js +51 -6
- 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.
|
|
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",
|
|
@@ -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
|
|
84
|
+
for (let index = 0; index < rest.length; index++) {
|
|
83
85
|
const flag = rest[index];
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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:
|
|
5
|
+
"serialNumber": "urn:uuid:4e82c3bc-ded1-5f78-8aa6-6dd26488f41f",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-10-
|
|
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.
|
|
13
|
+
"version": "0.46.0"
|
|
14
14
|
}
|
|
15
15
|
],
|
|
16
16
|
"component": {
|
|
17
17
|
"type": "application",
|
|
18
|
-
"bom-ref": "pkg:npm/harness-everything@0.
|
|
18
|
+
"bom-ref": "pkg:npm/harness-everything@0.46.0",
|
|
19
19
|
"name": "harness-everything",
|
|
20
|
-
"version": "0.
|
|
21
|
-
"purl": "pkg:npm/harness-everything@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.
|
|
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",
|