harness-everything 0.38.0 → 0.40.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/AGENTS.md +79 -63
- package/CHANGELOG.md +32 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/README.md +76 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/a-reviews-b.md +191 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/b-reviews-a.md +100 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/baseline-probes.log +47 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/bom-red.log +85 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/bom-transition.txt +25 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/coordinator-probes.txt +84 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/cross-suite.txt +18 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/final-bom.log +3 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/final-consistency.log.gz +0 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/final-core.log +198 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/final-probes.log +27 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/final-producer.log +83 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/final-review.md +89 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/final-transitions.log +5 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/implementation-a.md +166 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/implementation-b.md +264 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/logs.sha256 +15 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/peer-a-probe.log +62 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/peer-a-probe.txt +313 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/peer-b-probe.log +51 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/peer-b-probe.txt +94 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/semantic-transitions.txt +25 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/source.sha256 +9 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/tests-a-on-b-core.log +201 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/tests-a-on-b-producer.log +82 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/tests-b-on-a-core.log +197 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/tests-b-on-a-producer.log +82 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/verify-all.log.gz +0 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/verify-gate-summary.json +22 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2a/harness.js +489 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2a/vectors.json +617 -0
- package/ci/mechanism-325-3b-s2a-core.test.js +840 -0
- package/ci/mechanism-325-3b-s2a-producer.test.js +564 -0
- package/code-review/SKILL.md +3 -3
- package/code-review/references/review-playbook.md +4 -0
- package/code-review/templates/finding.template.md +6 -0
- package/docs/doc-workflow-reconciliation.md +357 -0
- package/evals/zoom-out/tasks/negative-first-edge-case-review.yaml +16 -0
- package/evals/zoom-out/tasks/positive-repair-loop.yaml +15 -0
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/scripts/project-docs-resolver.js +42 -8
- package/harness-everything/scripts/routing-keywords.json +4 -0
- package/hooks/scripts/lib/doc-reconciliation-core.js +280 -0
- package/hooks/scripts/lib/doc-reconciliation-events.js +141 -0
- package/hooks/scripts/lib/doc-reconciliation-producer.js +206 -0
- package/multi-agent-workspace/SKILL.md +1 -1
- package/multi-agent-workspace/scripts/project-docs-resolver.js +42 -8
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +2 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-core.js +280 -0
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-events.js +141 -0
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-producer.js +206 -0
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/code-review/SKILL.md +3 -3
- package/plugins/harness-everything/skills/code-review/references/review-playbook.md +4 -0
- package/plugins/harness-everything/skills/code-review/templates/finding.template.md +6 -0
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/project-docs-resolver.js +42 -8
- package/plugins/harness-everything/skills/harness-everything/scripts/routing-keywords.json +4 -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 +42 -8
- package/plugins/harness-everything/skills/to-spec/SKILL.md +1 -1
- package/plugins/harness-everything/skills/to-spec/scripts/project-docs-resolver.js +42 -8
- package/plugins/harness-everything/skills/zoom-out/SKILL.md +4 -3
- package/plugins/harness-everything/skills/zoom-out/references/circuit-breaker.md +7 -0
- package/plugins/harness-everything/skills/zoom-out/templates/zoom-out-report.template.md +4 -2
- package/sbom.cdx.json +7 -7
- package/scripts/errors-sync.js +90 -0
- package/to-spec/SKILL.md +1 -1
- package/to-spec/scripts/project-docs-resolver.js +42 -8
- package/zoom-out/SKILL.md +4 -3
- package/zoom-out/references/circuit-breaker.md +7 -0
- package/zoom-out/templates/zoom-out-report.template.md +4 -2
|
@@ -122,15 +122,17 @@ function readHostProjectDocsConfigs(workspaceRoot) {
|
|
|
122
122
|
return manifests;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
function inspectHostProjectDocs(workspaceRoot) {
|
|
125
|
+
function inspectHostProjectDocs(workspaceRoot, readSource = null) {
|
|
126
126
|
const entries = [];
|
|
127
127
|
for (const home of PLATFORM_HOMES) {
|
|
128
128
|
const file = path.join(workspaceRoot, home, 'harness-everything', 'manifest.json');
|
|
129
|
-
|
|
129
|
+
const captured = readSource ? readSource(file) : null;
|
|
130
|
+
if (captured ? captured.presence === 'absent' : !fs.existsSync(file)) continue;
|
|
130
131
|
const entry = { file, home, projectDocs: null, warnings: [], diagnostics: [], hasProjectDocs: false };
|
|
131
132
|
let manifest;
|
|
132
133
|
try {
|
|
133
|
-
|
|
134
|
+
if (captured && captured.presence !== 'present') throw new Error('unreadable source');
|
|
135
|
+
manifest = JSON.parse(captured ? captured.bytes.toString('utf8') : fs.readFileSync(file, 'utf8'));
|
|
134
136
|
} catch {
|
|
135
137
|
entry.diagnostics.push({
|
|
136
138
|
code: 'invalid-host-config',
|
|
@@ -349,16 +351,18 @@ function configSource(workspaceRoot, file) {
|
|
|
349
351
|
return relativePath(workspaceRoot, file);
|
|
350
352
|
}
|
|
351
353
|
|
|
352
|
-
function
|
|
354
|
+
function resolveDocsConfigSelection(workspaceRoot, options, readSource) {
|
|
353
355
|
const resolvedRoot = workspaceRoot || getWorkspaceRoot();
|
|
354
356
|
if (!resolvedRoot) throw new Error('cannot resolve project docs outside a git workspace; pass a workspace root explicitly');
|
|
355
357
|
const root = path.resolve(resolvedRoot);
|
|
356
358
|
const projectFile = projectDocsConfigPath(root);
|
|
357
|
-
|
|
359
|
+
const capturedProject = readSource(projectFile);
|
|
360
|
+
if (capturedProject.presence !== 'absent') {
|
|
358
361
|
const source = configSource(root, projectFile);
|
|
359
362
|
let config;
|
|
360
363
|
try {
|
|
361
|
-
|
|
364
|
+
if (capturedProject.presence !== 'present') throw new Error('unreadable source');
|
|
365
|
+
config = JSON.parse(capturedProject.bytes.toString('utf8'));
|
|
362
366
|
} catch {
|
|
363
367
|
return {
|
|
364
368
|
status: 'invalid',
|
|
@@ -376,7 +380,7 @@ function resolveDocsConfig(workspaceRoot, options = {}) {
|
|
|
376
380
|
return { status: 'invalid', sources: [source], config: null, warnings, diagnostics };
|
|
377
381
|
}
|
|
378
382
|
|
|
379
|
-
const hosts = inspectHostProjectDocs(root);
|
|
383
|
+
const hosts = inspectHostProjectDocs(root, readSource);
|
|
380
384
|
const hostDiagnostics = hosts.flatMap(entry => entry.diagnostics);
|
|
381
385
|
const hostWarnings = hosts.flatMap(entry => entry.warnings);
|
|
382
386
|
const validHosts = hosts.filter(entry => entry.projectDocs);
|
|
@@ -393,7 +397,7 @@ function resolveDocsConfig(workspaceRoot, options = {}) {
|
|
|
393
397
|
return { status: 'project', sources: [source], config, warnings: [...warnings, ...hostWarnings], diagnostics: [...diagnostics, ...hostDiagnostics] };
|
|
394
398
|
}
|
|
395
399
|
|
|
396
|
-
const hosts = inspectHostProjectDocs(root);
|
|
400
|
+
const hosts = inspectHostProjectDocs(root, readSource);
|
|
397
401
|
const hostDiagnostics = hosts.flatMap(entry => entry.diagnostics);
|
|
398
402
|
const hostWarnings = hosts.flatMap(entry => entry.warnings);
|
|
399
403
|
const activeHome = normalizedHostHome(options.activeHost);
|
|
@@ -457,6 +461,35 @@ function resolveDocsConfig(workspaceRoot, options = {}) {
|
|
|
457
461
|
};
|
|
458
462
|
}
|
|
459
463
|
|
|
464
|
+
function resolveDocsConfigCaptured(workspaceRoot, options = {}) {
|
|
465
|
+
const resolvedRoot = workspaceRoot || getWorkspaceRoot();
|
|
466
|
+
if (!resolvedRoot) throw new Error('cannot resolve project docs outside a git workspace; pass a workspace root explicitly');
|
|
467
|
+
const root = path.resolve(resolvedRoot);
|
|
468
|
+
const records = new Map();
|
|
469
|
+
const readSource = file => {
|
|
470
|
+
const key = configSource(root, file);
|
|
471
|
+
if (!records.has(key)) {
|
|
472
|
+
let bytes = Buffer.alloc(0);
|
|
473
|
+
let presence = 'present';
|
|
474
|
+
try { bytes = Buffer.from(fs.readFileSync(file)); }
|
|
475
|
+
catch (error) { presence = error.code === 'ENOENT' ? 'absent' : 'unreadable'; }
|
|
476
|
+
records.set(key, { path: key, presence, bytes, digest: crypto.createHash('sha256').update(bytes).digest('hex') });
|
|
477
|
+
}
|
|
478
|
+
return records.get(key);
|
|
479
|
+
};
|
|
480
|
+
const resolution = resolveDocsConfigSelection(root, options, readSource);
|
|
481
|
+
let activeHost = null;
|
|
482
|
+
try { activeHost = JSON.parse(JSON.stringify(options.activeHost)); } catch { /* no serializable host context */ }
|
|
483
|
+
const sources = [...records.values()].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
484
|
+
const selectionInputs = sources.map(({ path: sourcePath, presence }) => ({ path: sourcePath, presence }));
|
|
485
|
+
selectionInputs.push({ status: resolution.status, sources: [...resolution.sources], activeHost, config: resolution.config });
|
|
486
|
+
return { resolution, capture: { activeHost, sources, selectionInputs } };
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function resolveDocsConfig(workspaceRoot, options = {}) {
|
|
490
|
+
return resolveDocsConfigCaptured(workspaceRoot, options).resolution;
|
|
491
|
+
}
|
|
492
|
+
|
|
460
493
|
function cleanConfiguredPath(value) {
|
|
461
494
|
if (typeof value !== 'string') return null;
|
|
462
495
|
const trimmed = value.trim();
|
|
@@ -769,6 +802,7 @@ module.exports = {
|
|
|
769
802
|
getRepoManifestHomes,
|
|
770
803
|
readProjectDocs,
|
|
771
804
|
resolveDocsConfig,
|
|
805
|
+
resolveDocsConfigCaptured,
|
|
772
806
|
validateDocsConfig,
|
|
773
807
|
normalizeHostProjectDocs,
|
|
774
808
|
parseContextMap,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "harness-everything",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.0",
|
|
4
4
|
"description": "Harness OS: An Orchestrated Agent Operating System for AI-driven Development",
|
|
5
5
|
"main": "harness-everything/scripts/bootstrap.js",
|
|
6
6
|
"bin": {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"tdd:quality": "node tdd/scripts/quality-gate.js",
|
|
18
18
|
"repo:contract": "node scripts/repository-contract.js",
|
|
19
19
|
"docs:sync": "node scripts/sync-current-docs.js",
|
|
20
|
+
"errors:sync": "node scripts/errors-sync.js",
|
|
20
21
|
"test:repo-contract": "node ci/repository-contract.test.js",
|
|
21
22
|
"test:consistency": "node ci/consistency-check.js && node ci/doc-capability-consistency.test.js && node ci/repository-contract.test.js && node ci/mechanism-2ao-plugin-sync.test.js && node ci/prompt-contract-regressions.test.js",
|
|
22
23
|
"test:docs:capabilities": "node ci/doc-capability-consistency.test.js",
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
// Both layouts are shipped by plugin:sync. Resolve modules at import, never during a decision.
|
|
6
|
+
function runtimeModule(name) {
|
|
7
|
+
try { return require('../../../harness-everything/scripts/' + name); }
|
|
8
|
+
catch (error) {
|
|
9
|
+
if (error.code !== 'MODULE_NOT_FOUND' || !error.message.startsWith("Cannot find module '../../../harness-everything/scripts/" + name + "'")) throw error;
|
|
10
|
+
return require('../../../skills/harness-everything/scripts/' + name);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const { checkDocObligations } = runtimeModule('doc-evidence.js');
|
|
14
|
+
const { deriveDocFacts, deriveDocObligations } = runtimeModule('doc-obligations.js');
|
|
15
|
+
const text = value => typeof value === 'string' && value.trim().length > 0;
|
|
16
|
+
const record = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
17
|
+
const compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
18
|
+
const bytes = value => Buffer.isBuffer(value) ? Buffer.from(value)
|
|
19
|
+
: value && value.type === 'Buffer' && Array.isArray(value.data) ? Buffer.from(value.data)
|
|
20
|
+
: (() => { throw new Error('inventory-unsupported'); })();
|
|
21
|
+
function canonical(value) {
|
|
22
|
+
if (Buffer.isBuffer(value)) return { type: 'Buffer', data: [...value] };
|
|
23
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
24
|
+
if (!record(value)) return value;
|
|
25
|
+
return Object.fromEntries(Object.keys(value).sort(compare).map(key => [key, canonical(value[key])]));
|
|
26
|
+
}
|
|
27
|
+
const stable = value => JSON.stringify(canonical(value));
|
|
28
|
+
const clone = value => JSON.parse(stable(value));
|
|
29
|
+
const sha = value => crypto.createHash('sha256').update(value).digest('hex');
|
|
30
|
+
const frame = value => { const b = Buffer.isBuffer(value) ? value : Buffer.from(String(value)); return Buffer.concat([Buffer.from(b.length + ':'), b]); };
|
|
31
|
+
const hashFrames = values => sha(Buffer.concat(values.map(frame)));
|
|
32
|
+
const structuredDigest = (domain, value) => hashFrames([domain, stable(value)]);
|
|
33
|
+
const identityFields = ['workspaceId', 'sessionId', 'workflowId', 'workflowRevision', 'taskId', 'taskScopeDigest', 'baselineDigest', 'candidateDigest', 'configDigest', 'prdRoot'];
|
|
34
|
+
const validIdentity = id => record(id) && ['workspaceId', 'sessionId', 'workflowId', 'taskId'].every(key => text(id[key]))
|
|
35
|
+
&& Number.isInteger(id.workflowRevision) && id.workflowRevision >= 0 && id.prdRoot === 'docs/prd/'
|
|
36
|
+
&& ['taskScopeDigest', 'baselineDigest', 'candidateDigest', 'configDigest'].every(key => /^[a-f0-9]{64}$/.test(id[key] || ''));
|
|
37
|
+
const sameIdentity = (a, b) => validIdentity(a) && validIdentity(b) && identityFields.every(key => a[key] === b[key]);
|
|
38
|
+
const diagnostic = (code, context = {}) => ({ code, message: code.replace(/-/g, ' '), ...context });
|
|
39
|
+
const pathCompare = (a, b) => Buffer.compare(Buffer.from(a.path), Buffer.from(b.path));
|
|
40
|
+
const codedError = code => Object.assign(new Error(code), { code });
|
|
41
|
+
// The derive contract (doc-obligations.js) decides applicability from governed/legacy documents, derived
|
|
42
|
+
// outputs and code/REQ-test paths. Records outside them cannot affect document obligations.
|
|
43
|
+
// shortcut: mirrors derive path rules, update them together until a shared classifier is exposed.
|
|
44
|
+
const CODE_EXTENSION = /\.(?:c|cc|cpp|h|hpp|m|mm|go|rs|java|kt|swift|js|jsx|mjs|cjs|ts|tsx|py|rb|php|sh|ps1|sql)$/i;
|
|
45
|
+
const TEST_PATH = /(?:^|\/)(?:test|tests|__tests__|spec)(?:\/|$)|(?:^|\/)[^/]*(?:\.test|\.spec)\.[^/]+$/i;
|
|
46
|
+
// Glob semantics of the derive contract: '**' crosses directories, '*' stays inside one segment.
|
|
47
|
+
function globToRegExp(glob) {
|
|
48
|
+
const source = String(glob).replace(/\\/g, '/').replace(/^\.\//, '');
|
|
49
|
+
let pattern = '^';
|
|
50
|
+
for (let index = 0; index < source.length; index++) {
|
|
51
|
+
const char = source[index];
|
|
52
|
+
if (char === '*' && source[index + 1] === '*') {
|
|
53
|
+
if (source[index + 2] === '/') { pattern += '(?:.*/)?'; index += 2; }
|
|
54
|
+
else { pattern += '.*'; index += 1; }
|
|
55
|
+
} else if (char === '*') pattern += '[^/]*';
|
|
56
|
+
else pattern += char.replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
|
|
57
|
+
}
|
|
58
|
+
return new RegExp(`${pattern}$`);
|
|
59
|
+
}
|
|
60
|
+
// Semantic relevance of a path under the derive contract; null means the path cannot change applicability.
|
|
61
|
+
function semanticRelevance(path, config) {
|
|
62
|
+
const governed = Array.isArray(config && config.governed) ? config.governed : [];
|
|
63
|
+
const legacy = Array.isArray(config && config.legacy) ? config.legacy : [];
|
|
64
|
+
const derived = Array.isArray(config && config.derived) ? config.derived : [];
|
|
65
|
+
const matches = globs => globs.some(glob => typeof glob === 'string' && globToRegExp(glob).test(path));
|
|
66
|
+
if (governed.some(entry => entry && typeof entry.path === 'string' && matches([entry.path]))) return 'document';
|
|
67
|
+
if (matches(legacy)) return 'document';
|
|
68
|
+
if (derived.some(entry => entry && typeof entry.path === 'string'
|
|
69
|
+
&& (matches([entry.path]) || matches([entry.source, ...(Array.isArray(entry.sources) ? entry.sources : [])])))) return 'derived';
|
|
70
|
+
if (CODE_EXTENSION.test(path)) return 'code';
|
|
71
|
+
if (TEST_PATH.test(path)) return 'test';
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
// The derive contract reads the frontmatter of every captured file, so a record that opens a frontmatter
|
|
75
|
+
// block is observed by it even when the record's bytes cannot be decoded. Match TextDecoder's BOM handling.
|
|
76
|
+
function opensFrontmatter(stored) {
|
|
77
|
+
return stored.subarray(0, 11).toString('utf8').replace(/^\uFEFF/, '').split('\n')[0].replace(/\r$/, '') === '---';
|
|
78
|
+
}
|
|
79
|
+
// A whole-project inventory keeps every stored byte in the identity framing. Semantic derivation converts only
|
|
80
|
+
// records it can decode; a record the derive contract can observe (governed/derived/code/test path or a
|
|
81
|
+
// frontmatter document) fails closed instead of being silently dropped.
|
|
82
|
+
function deriveFiles(records, config, options = {}) {
|
|
83
|
+
const files = {};
|
|
84
|
+
for (const item of records || []) {
|
|
85
|
+
if (!record(item) || item.presence !== 'present') continue;
|
|
86
|
+
// The unchanged derive API uses ordinary object maps, whose __proto__ setter drops this path.
|
|
87
|
+
if (item.path === '__proto__') throw codedError('inventory-unsupported');
|
|
88
|
+
const stored = bytes(item.storedBytes);
|
|
89
|
+
if (options.strict && (item.digest !== sha(stored) || item.type !== 'file' || !['100644', '100755'].includes(item.mode))) {
|
|
90
|
+
throw codedError('invalid-input');
|
|
91
|
+
}
|
|
92
|
+
let content;
|
|
93
|
+
try { content = new TextDecoder('utf-8', { fatal: true }).decode(stored); }
|
|
94
|
+
catch {
|
|
95
|
+
if (semanticRelevance(item.path, config) || opensFrontmatter(stored)) throw codedError('inventory-unsupported');
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
files[item.path] = content;
|
|
99
|
+
}
|
|
100
|
+
return files;
|
|
101
|
+
}
|
|
102
|
+
const identityTupleFields = ['workspaceId', 'sessionId', 'workflowId', 'workflowRevision', 'taskId'];
|
|
103
|
+
// The captured origin context is the snapshot's only origin-correlated identity: every field it carries must be
|
|
104
|
+
// the identity the snapshot claims, the captured plan must agree with both, and the captured host must be the
|
|
105
|
+
// host the captured config was selected for (PR #353 review P1).
|
|
106
|
+
function originBound(snapshot) {
|
|
107
|
+
const context = snapshot.originContext;
|
|
108
|
+
const id = snapshot.identity;
|
|
109
|
+
if (!record(context) || !validIdentity(id)) return false;
|
|
110
|
+
for (const key of identityTupleFields) if (context[key] !== id[key]) return false;
|
|
111
|
+
const plan = snapshot.taskSemantics;
|
|
112
|
+
if (!record(plan) || !record(plan.workflow) || !record(plan.task)) return false;
|
|
113
|
+
if (plan.workflow.workflowId !== id.workflowId || plan.workflow.workflowRevision !== id.workflowRevision
|
|
114
|
+
|| plan.workflow.sessionId !== id.sessionId || plan.task.taskId !== id.taskId) return false;
|
|
115
|
+
if (plan.prdRoot !== id.prdRoot || plan.configDigest !== id.configDigest) return false;
|
|
116
|
+
const captured = context.activeHost === undefined || context.activeHost === null ? null : context.activeHost;
|
|
117
|
+
const selected = snapshot.config && snapshot.config.capture ? snapshot.config.capture.activeHost : null;
|
|
118
|
+
const chosen = selected === undefined || selected === null ? null : selected;
|
|
119
|
+
if (captured === null && chosen === null) return true;
|
|
120
|
+
return stable(captured) === stable(chosen);
|
|
121
|
+
}
|
|
122
|
+
// The captured config bytes must be the bytes the captured inventory holds for the same path; otherwise the
|
|
123
|
+
// config digest binds a parsed value that the whole-project capture contradicts (the producer's own rule).
|
|
124
|
+
function configBytesBound(snapshot) {
|
|
125
|
+
const sources = snapshot.config && snapshot.config.capture ? snapshot.config.capture.sources : null;
|
|
126
|
+
if (!Array.isArray(sources)) return false;
|
|
127
|
+
const records = Array.isArray(snapshot.candidate.records) ? snapshot.candidate.records : [];
|
|
128
|
+
for (const source of sources) {
|
|
129
|
+
const file = records.find(item => item && item.path === source.path && item.presence === 'present');
|
|
130
|
+
if (source.presence === 'present' ? !file || !bytes(source.bytes).equals(bytes(file.storedBytes)) : Boolean(file)) return false;
|
|
131
|
+
}
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
function snapshotDigest(kind, baseline, coverage, records) {
|
|
135
|
+
const fields = ['doc-snapshot-v1', kind, baseline.commit, baseline.treeRef, structuredDigest('doc-coverage-v1', coverage), String(records.length)];
|
|
136
|
+
for (const item of [...records].sort(pathCompare)) fields.push(item.path, item.presence, item.type, item.mode, bytes(item.storedBytes));
|
|
137
|
+
return hashFrames(fields);
|
|
138
|
+
}
|
|
139
|
+
function configDigest(config) {
|
|
140
|
+
const sources = [...config.capture.sources].sort(pathCompare);
|
|
141
|
+
const fields = ['doc-config-v1', stable({ activeHost: config.capture.activeHost, selectionInputs: config.capture.selectionInputs, resolution: config.resolution }), String(sources.length)];
|
|
142
|
+
for (const source of sources) fields.push(source.path, source.presence, bytes(source.bytes));
|
|
143
|
+
return hashFrames(fields);
|
|
144
|
+
}
|
|
145
|
+
function validateSnapshot(snapshot) {
|
|
146
|
+
const errors = [];
|
|
147
|
+
try {
|
|
148
|
+
if (!validIdentity(snapshot.identity) || !record(snapshot.facts) || !Array.isArray(snapshot.obligations)) throw new Error('invalid-input');
|
|
149
|
+
// Roots are canonicalized at capture; keep reconciliation detached and free of filesystem reads.
|
|
150
|
+
const context = snapshot.originContext;
|
|
151
|
+
if (!record(context) || !record(context.baseline) || !text(context.workspaceRoot)
|
|
152
|
+
|| context.baseline.commit !== snapshot.baseline.commit || context.baseline.root !== context.workspaceRoot) {
|
|
153
|
+
errors.push(diagnostic('baseline-unavailable'));
|
|
154
|
+
}
|
|
155
|
+
// The captured origin identity, the captured plan and the selected host must be the identity the snapshot
|
|
156
|
+
// claims; otherwise the provider half and the event half of the trust contract stay disconnected.
|
|
157
|
+
if (!originBound(snapshot)) errors.push(diagnostic('identity-mismatch'));
|
|
158
|
+
if (snapshotDigest('baseline', snapshot.baseline, snapshot.candidate.coverage, snapshot.baseline.records) !== snapshot.identity.baselineDigest
|
|
159
|
+
|| snapshotDigest('candidate', snapshot.baseline, snapshot.candidate.coverage, snapshot.candidate.records) !== snapshot.identity.candidateDigest
|
|
160
|
+
|| snapshot.baseline.digest !== snapshot.identity.baselineDigest || snapshot.candidate.digest !== snapshot.identity.candidateDigest) errors.push(diagnostic('identity-mismatch'));
|
|
161
|
+
if (configDigest(snapshot.config) !== snapshot.identity.configDigest || snapshot.config.digest !== snapshot.identity.configDigest) errors.push(diagnostic('config-capture-mismatch'));
|
|
162
|
+
if (!configBytesBound(snapshot)) errors.push(diagnostic('config-capture-mismatch'));
|
|
163
|
+
if (structuredDigest('doc-task-scope-v1', snapshot.taskSemantics) !== snapshot.identity.taskScopeDigest) errors.push(diagnostic('identity-mismatch'));
|
|
164
|
+
const config = snapshot.config.resolution.config;
|
|
165
|
+
const derived = deriveDocFacts({ base: { files: deriveFiles(snapshot.baseline.records, config, { strict: true }) },
|
|
166
|
+
candidate: { files: deriveFiles(snapshot.candidate.records, config, { strict: true }) }, config });
|
|
167
|
+
if (stable(derived.facts) !== stable(snapshot.facts)
|
|
168
|
+
|| stable(deriveDocObligations(derived.facts)) !== stable(snapshot.obligations)) errors.push(diagnostic('identity-mismatch'));
|
|
169
|
+
for (const obligation of snapshot.obligations) {
|
|
170
|
+
const reason = snapshot.derivationReasons && snapshot.derivationReasons[obligation.id];
|
|
171
|
+
if (obligation.strength === 'MUST' && obligation.applicability === 'N/A'
|
|
172
|
+
&& (!reason || reason.applicability !== 'N/A' || !text(reason.reason) || !Array.isArray(reason.factRefs) || !reason.factRefs.length)) {
|
|
173
|
+
errors.push(diagnostic('na-reason-missing', { obligationId: obligation.id }));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} catch (error) {
|
|
177
|
+
const code = error && ['invalid-input', 'inventory-incomplete', 'inventory-unsupported'].includes(error.message)
|
|
178
|
+
? error.message : 'invalid-input';
|
|
179
|
+
errors.push(diagnostic(code));
|
|
180
|
+
}
|
|
181
|
+
return errors;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function reconcileDocResults(input = {}) {
|
|
185
|
+
const { snapshot, accepted, checkerOutput, mode = 'verification' } = record(input) ? input : {};
|
|
186
|
+
const diagnostics = [];
|
|
187
|
+
const unsatisfied = [];
|
|
188
|
+
const results = Array.isArray(checkerOutput && checkerOutput.results) ? checkerOutput.results : [];
|
|
189
|
+
const base = { decision: 'unresolved', eligible: false, expectedMustIds: [], checkedMustIds: [], results, unsatisfied, diagnostics };
|
|
190
|
+
if (!record(snapshot) || !record(accepted) || !record(checkerOutput) || !Array.isArray(checkerOutput.results)
|
|
191
|
+
|| !results.every(record) || !Array.isArray(checkerOutput.diagnostics) || !checkerOutput.diagnostics.every(record)
|
|
192
|
+
|| !['verification', 'review'].includes(mode)
|
|
193
|
+
|| !['ready', 'unresolved', 'unavailable'].includes(accepted.state)
|
|
194
|
+
|| !Array.isArray(accepted.diagnostics) || !accepted.diagnostics.every(record)
|
|
195
|
+
|| !Array.isArray(accepted.rejected) || !accepted.rejected.every(record) || !record(accepted.acceptedIndex)) {
|
|
196
|
+
diagnostics.push(diagnostic('invalid-input'));
|
|
197
|
+
return base;
|
|
198
|
+
}
|
|
199
|
+
diagnostics.push(...validateSnapshot(snapshot));
|
|
200
|
+
if (!Array.isArray(snapshot.obligations) || !snapshot.obligations.every(o => record(o) && record(o.scope) && Array.isArray(o.scope.paths))) return base;
|
|
201
|
+
const expected = snapshot.obligations.filter(o => o.strength === 'MUST' && o.applicability !== 'N/A');
|
|
202
|
+
base.expectedMustIds = expected.map(o => o.id).sort(compare);
|
|
203
|
+
const selected = mode === 'review' ? expected.filter(o => o.capability === 'quality-review') : expected;
|
|
204
|
+
base.checkedMustIds = selected.map(o => o.id).sort(compare);
|
|
205
|
+
const fail = (code, obligationId, state = 'pending') => {
|
|
206
|
+
diagnostics.push(diagnostic(code, obligationId ? { obligationId } : {}));
|
|
207
|
+
if (obligationId) unsatisfied.push({ obligationId, state, reasonCode: code });
|
|
208
|
+
};
|
|
209
|
+
if (diagnostics.length) return base;
|
|
210
|
+
const capturedArtifacts = Object.fromEntries(snapshot.candidate.records.filter(r => r.presence === 'present').map(r => [r.path, { digest: r.digest }]));
|
|
211
|
+
const capturedBaseline = { digest: snapshot.identity.baselineDigest,
|
|
212
|
+
artifacts: Object.fromEntries(snapshot.baseline.records.filter(r => r.presence === 'present').map(r => [r.path, { digest: r.digest }])) };
|
|
213
|
+
if (stable(accepted.checkerInputs?.artifacts) !== stable(capturedArtifacts)
|
|
214
|
+
|| stable(accepted.checkerInputs?.baseline) !== stable(capturedBaseline)
|
|
215
|
+
|| stable(accepted.checkerInputs?.obligations) !== stable(snapshot.obligations)) fail('artifact-mismatch');
|
|
216
|
+
if (!sameIdentity(snapshot.identity, accepted.identity) || stable(snapshot.originContext) !== stable(accepted.originContext)) fail('identity-mismatch');
|
|
217
|
+
if (snapshot.identity.configDigest !== accepted.identity?.configDigest) fail('config-capture-mismatch');
|
|
218
|
+
for (const item of [...(accepted.diagnostics || []), ...(accepted.rejected || [])]) {
|
|
219
|
+
diagnostics.push(item);
|
|
220
|
+
// Only rejected evidence for a checked expected MUST, or a global origin/identity fault, gates credit;
|
|
221
|
+
// a rejected SHOULD or N/A event stays a visible diagnostic (PR #353 review P2).
|
|
222
|
+
if (!item.obligationId || selected.some(o => o.id === item.obligationId)) {
|
|
223
|
+
unsatisfied.push({ obligationId: item.obligationId || 'origin', state: item.code === 'origin-unavailable' ? 'unavailable' : 'pending', reasonCode: item.code });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (accepted.state === 'unavailable') fail('origin-unavailable');
|
|
227
|
+
if (checkerOutput?.unavailable) fail('checker-unavailable');
|
|
228
|
+
const known = new Set(snapshot.obligations.map(o => o.id));
|
|
229
|
+
for (const result of results) if (!known.has(result.obligationId)) fail('result-misattributed', result.obligationId);
|
|
230
|
+
for (const obligation of selected) {
|
|
231
|
+
const id = obligation.id;
|
|
232
|
+
if (obligation.applicability === 'unknown') { fail('unknown-applicability', id, 'unknown'); continue; }
|
|
233
|
+
const matches = results.filter(r => r.obligationId === id);
|
|
234
|
+
if (matches.length !== 1) { fail(matches.length ? 'result-duplicate' : 'result-missing', id); continue; }
|
|
235
|
+
const result = matches[0];
|
|
236
|
+
if (result.observation !== 'observed' || result.outcome !== 'pass') {
|
|
237
|
+
const hasAccepted = Object.values(accepted.acceptedIndex || {}).some(item => item.obligationId === id);
|
|
238
|
+
fail(hasAccepted ? 'result-not-pass' : 'result-missing', id);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const artifactDigest = obligation.scope.paths.length
|
|
242
|
+
? obligation.scope.paths.map(p => `${p}=${capturedArtifacts[p]?.digest || 'missing'}`).join('|')
|
|
243
|
+
: snapshot.identity.baselineDigest;
|
|
244
|
+
if (result.baseDigest !== snapshot.identity.baselineDigest || result.artifactDigest !== artifactDigest) fail('result-misattributed', id);
|
|
245
|
+
if (!Array.isArray(result.recordRefs) || !result.recordRefs.length) { fail('result-untrusted', id); continue; }
|
|
246
|
+
for (const ref of result.recordRefs) {
|
|
247
|
+
const item = accepted.acceptedIndex?.[ref];
|
|
248
|
+
if (!item || item.obligationId !== id || !sameIdentity(item.identity, snapshot.identity)
|
|
249
|
+
|| !text(item.originRef) || stable(item.paths) !== stable(obligation.scope.paths)
|
|
250
|
+
|| item.artifactBinding !== snapshot.identity.candidateDigest) fail('result-untrusted', id);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// Validation failures must never be hidden by a cached report or a passing result subset.
|
|
254
|
+
// Global faults and diagnostics for a checked expected MUST block; SHOULD/N/A diagnostics stay visible only.
|
|
255
|
+
const blocking = diagnostics.filter(item => !item.obligationId || selected.some(o => o.id === item.obligationId));
|
|
256
|
+
// Checker diagnostics stay visible; eligibility is decided from the expected MUST set above.
|
|
257
|
+
// In particular, later verifier diagnostics cannot stall an independent review.
|
|
258
|
+
if (Array.isArray(checkerOutput.diagnostics)) diagnostics.push(...checkerOutput.diagnostics);
|
|
259
|
+
if (blocking.length || unsatisfied.length) {
|
|
260
|
+
if (blocking.some(d => ['origin-unavailable', 'checker-unavailable'].includes(d.code))) base.decision = 'unavailable';
|
|
261
|
+
return base;
|
|
262
|
+
}
|
|
263
|
+
if (!selected.length) { base.decision = 'legacy'; diagnostics.push(diagnostic('no-document-musts')); return base; }
|
|
264
|
+
base.decision = 'eligible';
|
|
265
|
+
base.eligible = true;
|
|
266
|
+
return base;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function checkAcceptedDocEvidence(input = {}) {
|
|
270
|
+
const { snapshot, accepted, mode = 'verification' } = record(input) ? input : {};
|
|
271
|
+
let checkerOutput;
|
|
272
|
+
try {
|
|
273
|
+
if (!accepted?.checkerInputs || !snapshot) throw new Error('invalid-input');
|
|
274
|
+
checkerOutput = checkDocObligations({ ...accepted.checkerInputs, obligations: snapshot.obligations });
|
|
275
|
+
} catch { checkerOutput = { results: [], diagnostics: [], unavailable: true }; }
|
|
276
|
+
return reconcileDocResults({ snapshot, accepted, checkerOutput, mode });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
module.exports = { checkAcceptedDocEvidence, reconcileDocResults,
|
|
280
|
+
_internals: { runtimeModule, text, record, bytes, stable, clone, sha, structuredDigest, snapshotDigest, configDigest, sameIdentity, diagnostic, validateSnapshot, compare, deriveFiles, semanticRelevance, originBound, configBytesBound } };
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { _internals: U } = require('./doc-reconciliation-core');
|
|
6
|
+
|
|
7
|
+
// Directory identity as in the producer: native realpath (Windows 8.3 names), case-insensitive on Windows.
|
|
8
|
+
function samePath(a, b) {
|
|
9
|
+
try {
|
|
10
|
+
const left = fs.realpathSync.native(path.resolve(a));
|
|
11
|
+
const right = fs.realpathSync.native(path.resolve(b));
|
|
12
|
+
return process.platform === 'win32' ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
13
|
+
} catch { return false; }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function artifactsFor(records) {
|
|
17
|
+
return Object.fromEntries(records.filter(item => item.presence === 'present').map(item => [item.path, { digest: item.digest }]));
|
|
18
|
+
}
|
|
19
|
+
function prdDigest(artifacts) {
|
|
20
|
+
return U.sha(JSON.stringify(Object.keys(artifacts).filter(p => p.startsWith('docs/prd/')).sort().map(p => [p, artifacts[p].digest])));
|
|
21
|
+
}
|
|
22
|
+
function acceptDocEvents(input = {}, dependencies = {}) {
|
|
23
|
+
const { snapshot, eventRefs } = U.record(input) ? input : {};
|
|
24
|
+
const originProvider = dependencies?.originProvider;
|
|
25
|
+
const diagnostics = [];
|
|
26
|
+
const rejected = [];
|
|
27
|
+
const acceptedIndex = Object.create(null);
|
|
28
|
+
const empty = { state: 'unavailable', checkerInputs: null, acceptedIndex, rejected, diagnostics };
|
|
29
|
+
try {
|
|
30
|
+
if (!snapshot || !Array.isArray(eventRefs) || !eventRefs.every(U.text)) {
|
|
31
|
+
diagnostics.push(U.diagnostic('invalid-input'));
|
|
32
|
+
return empty;
|
|
33
|
+
}
|
|
34
|
+
diagnostics.push(...U.validateSnapshot(snapshot));
|
|
35
|
+
if (diagnostics.length) return { ...empty, state: 'unresolved' };
|
|
36
|
+
const artifacts = artifactsFor(snapshot.candidate.records);
|
|
37
|
+
const baselineArtifacts = artifactsFor(snapshot.baseline.records);
|
|
38
|
+
const checkerInputs = { obligations: U.clone(snapshot.obligations), artifacts,
|
|
39
|
+
baseline: { digest: snapshot.identity.baselineDigest, artifacts: baselineArtifacts }, executionRecords: [], reviewRecords: [] };
|
|
40
|
+
const out = { state: 'ready', identity: U.clone(snapshot.identity), originContext: U.clone(snapshot.originContext), checkerInputs, acceptedIndex, rejected, diagnostics };
|
|
41
|
+
if (!originProvider || typeof originProvider.resolveEvent !== 'function' || typeof originProvider.resolveContext !== 'function') {
|
|
42
|
+
diagnostics.push(U.diagnostic('origin-unavailable'));
|
|
43
|
+
out.state = 'unavailable';
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
const contextResponse = originProvider.resolveContext({ workspaceRoot: snapshot.originContext.workspaceRoot, workflowId: snapshot.identity.workflowId, sessionId: snapshot.identity.sessionId });
|
|
47
|
+
if (contextResponse?.status !== 'accepted' || !U.text(contextResponse.originRef)) {
|
|
48
|
+
diagnostics.push(U.diagnostic(contextResponse?.status === 'rejected' ? 'origin-untrusted' : 'origin-unavailable'));
|
|
49
|
+
out.state = 'unavailable';
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
if (contextResponse.originRef !== snapshot.originContext.originRef) diagnostics.push(U.diagnostic('identity-mismatch'));
|
|
53
|
+
const context = contextResponse.context;
|
|
54
|
+
for (const key of ['workspaceId', 'sessionId', 'workflowId', 'workflowRevision', 'taskId', 'principals', 'activeHost', 'baselineReference']) {
|
|
55
|
+
if (U.stable(context?.[key]) !== U.stable(snapshot.originContext[key])) diagnostics.push(U.diagnostic('identity-mismatch'));
|
|
56
|
+
}
|
|
57
|
+
// The concrete workflow-start pin and worktree bound at capture must still be what the provider issues;
|
|
58
|
+
// a different pin under the same baselineReference is not the captured baseline (PR #353 review P1).
|
|
59
|
+
const pin = snapshot.originContext.baseline;
|
|
60
|
+
if (!U.record(context?.baseline) || !U.record(pin) || context.baseline.commit !== pin.commit
|
|
61
|
+
|| !U.text(context.baseline.root) || !samePath(context.baseline.root, pin.root)) diagnostics.push(U.diagnostic('baseline-unavailable'));
|
|
62
|
+
if (context?.workspaceRoot !== undefined && !(U.text(context.workspaceRoot) && samePath(context.workspaceRoot, snapshot.originContext.workspaceRoot))) {
|
|
63
|
+
diagnostics.push(U.diagnostic('identity-mismatch'));
|
|
64
|
+
}
|
|
65
|
+
const seenIds = new Set();
|
|
66
|
+
const seenCapabilities = new Set();
|
|
67
|
+
for (const reference of eventRefs) {
|
|
68
|
+
let response;
|
|
69
|
+
try { response = originProvider.resolveEvent({ reference, identity: U.clone(snapshot.identity) }); }
|
|
70
|
+
catch { response = { status: 'unavailable' }; }
|
|
71
|
+
if (response?.status !== 'accepted') {
|
|
72
|
+
const supplied = Array.isArray(response?.diagnostics) ? response.diagnostics : [];
|
|
73
|
+
rejected.push(...(supplied.length ? U.clone(supplied) : [U.diagnostic(response?.status === 'rejected' ? 'origin-untrusted' : 'origin-unavailable', { reference })]));
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const event = U.clone(response.event);
|
|
77
|
+
const obligation = snapshot.obligations.find(o => o.capability === event?.capability);
|
|
78
|
+
const obligationId = obligation?.id;
|
|
79
|
+
const problems = [];
|
|
80
|
+
const reject = code => problems.push(U.diagnostic(code, { reference, ...(obligationId ? { obligationId } : {}) }));
|
|
81
|
+
if (!U.record(event) || !['eventId', 'producerId', 'originRef', 'principalId', 'provenance', 'capability'].every(k => U.text(event[k]))
|
|
82
|
+
|| !U.text(response.originRef) || event.originRef !== response.originRef || !['execution', 'verification', 'review'].includes(event.kind)) reject('event-invalid');
|
|
83
|
+
if (!U.sameIdentity(event?.identity, snapshot.identity)) {
|
|
84
|
+
reject('identity-mismatch');
|
|
85
|
+
if (event?.identity?.configDigest !== snapshot.identity.configDigest) reject('config-capture-mismatch');
|
|
86
|
+
}
|
|
87
|
+
if (!obligation) reject('capability-mismatch');
|
|
88
|
+
if (seenIds.has(event?.eventId)) reject('event-duplicate');
|
|
89
|
+
if (seenCapabilities.has(event?.capability)) reject('event-conflict');
|
|
90
|
+
seenIds.add(event?.eventId);
|
|
91
|
+
seenCapabilities.add(event?.capability);
|
|
92
|
+
const paths = obligation?.scope?.paths || [];
|
|
93
|
+
if (!Array.isArray(event?.paths) || U.stable([...event.paths].sort()) !== U.stable([...paths].sort())) reject('scope-mismatch');
|
|
94
|
+
const digests = event?.kind === 'review' ? event.reviewedArtifactDigests : event?.artifactDigests;
|
|
95
|
+
if (!U.record(digests)) reject('artifact-mismatch');
|
|
96
|
+
else {
|
|
97
|
+
for (const p of paths) {
|
|
98
|
+
if (artifacts[p] && digests[p] !== artifacts[p].digest) reject('artifact-mismatch');
|
|
99
|
+
if (!artifacts[p] && (!baselineArtifacts[p] || event.baselineArtifacts?.[p] !== baselineArtifacts[p].digest || Object.hasOwn(digests, p))) reject('artifact-mismatch');
|
|
100
|
+
}
|
|
101
|
+
// Supplied extra digests must also describe this capture, never another tree.
|
|
102
|
+
for (const p of Object.keys(digests)) if (!artifacts[p] || digests[p] !== artifacts[p].digest) reject('artifact-mismatch');
|
|
103
|
+
}
|
|
104
|
+
if (event?.kind === 'review') {
|
|
105
|
+
if (event.verdict !== 'approve' || !U.text(event.summary)) reject('event-invalid');
|
|
106
|
+
const principals = context?.principals;
|
|
107
|
+
const independent = principals && [principals.author, ...(principals.executors || [])];
|
|
108
|
+
if (!independent || independent.includes(event.principalId) || !Array.isArray(event.independentOf)
|
|
109
|
+
|| !independent.every(p => event.independentOf.includes(p))) reject('review-not-independent');
|
|
110
|
+
} else {
|
|
111
|
+
if (!U.text(event?.command) || !Array.isArray(event?.argv) || !event.argv.length || !event.argv.every(U.text)
|
|
112
|
+
|| !U.text(event.cwd) || !Number.isInteger(event.exitCode) || event.exitCode !== 0 || event.signal !== null
|
|
113
|
+
|| !U.text(event.toolCorrelationId) || response.correlation?.toolCorrelationId !== event.toolCorrelationId
|
|
114
|
+
|| !U.text(event.resultRef)) reject('event-invalid');
|
|
115
|
+
if (!U.sameIdentity(event?.startIdentity, snapshot.identity) || !U.sameIdentity(event?.endIdentity, snapshot.identity)) reject('snapshot-changed');
|
|
116
|
+
// Provider acceptance covers command semantics; the adapter additionally rejects command/argv substitution.
|
|
117
|
+
if (Array.isArray(event?.argv) && event.command !== event.argv.join(' ')) reject('capability-mismatch');
|
|
118
|
+
if (!context?.principals?.executors?.includes(event?.principalId)) reject('origin-untrusted');
|
|
119
|
+
}
|
|
120
|
+
if (problems.length) { rejected.push(...problems); continue; }
|
|
121
|
+
const adapted = { id: reference, obligationId, kind: event.kind === 'review' ? 'review' : 'execution',
|
|
122
|
+
paths: [...paths], provenance: event.provenance, baseDigest: snapshot.identity.baselineDigest,
|
|
123
|
+
artifactDigests: U.clone(digests), ...(event.baselineArtifacts ? { baselineArtifacts: U.clone(event.baselineArtifacts) } : {}) };
|
|
124
|
+
if (event.kind === 'review') Object.assign(adapted, { verdict: event.verdict, summary: event.summary });
|
|
125
|
+
else Object.assign(adapted, { command: event.command, exitCode: event.exitCode });
|
|
126
|
+
if (event.capability === 'conflict-check') {
|
|
127
|
+
adapted.candidatePrdSet = { digest: prdDigest(artifacts), complete: true, paths: Object.keys(artifacts).filter(p => p.startsWith('docs/prd/')).sort() };
|
|
128
|
+
}
|
|
129
|
+
(event.kind === 'review' ? checkerInputs.reviewRecords : checkerInputs.executionRecords).push(adapted);
|
|
130
|
+
acceptedIndex[reference] = { reference, eventId: event.eventId, obligationId, capability: event.capability,
|
|
131
|
+
paths: [...paths], identity: U.clone(event.identity), originRef: response.originRef, artifactBinding: snapshot.identity.candidateDigest,
|
|
132
|
+
artifactDigests: U.clone(digests), event };
|
|
133
|
+
}
|
|
134
|
+
if (diagnostics.length || rejected.length) out.state = 'unresolved';
|
|
135
|
+
return out;
|
|
136
|
+
} catch {
|
|
137
|
+
diagnostics.push(U.diagnostic('invalid-input'));
|
|
138
|
+
return empty;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
module.exports = { acceptDocEvents };
|