@haposoft/cafekit 0.16.0-rc.1 → 0.16.0-rc.3
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/bin/__tests__/codex-hooks.test.js +355 -12
- package/bin/__tests__/codex-native.test.js +1 -1
- package/bin/__tests__/develop-contract.test.js +1374 -78
- package/bin/__tests__/installer-safety.test.js +43 -29
- package/bin/__tests__/opencode-spec-gate.test.js +791 -0
- package/bin/__tests__/package-inventory.test.js +157 -0
- package/bin/__tests__/validator-grounding.test.js +707 -2
- package/package.json +1 -1
- package/src/claude/agents/code-auditor.md +8 -1
- package/src/claude/agents/spec-maker.md +107 -200
- package/src/claude/hooks/__tests__/completion-authority.test.js +720 -0
- package/src/claude/hooks/__tests__/spec-gate.test.js +511 -38
- package/src/claude/hooks/completion-authority-check.cjs +307 -0
- package/src/claude/hooks/completion-authority-state.cjs +525 -0
- package/src/claude/hooks/completion-authority.cjs +148 -0
- package/src/claude/hooks/session.cjs +33 -19
- package/src/claude/hooks/spec-gate.cjs +173 -61
- package/src/claude/hooks/spec-state.cjs +104 -45
- package/src/claude/migration-manifest.json +6 -1
- package/src/claude/scripts/provenance.cjs +315 -0
- package/src/claude/scripts/spec-resolver.cjs +450 -0
- package/src/claude/scripts/spec-scaffold.cjs +716 -41
- package/src/claude/scripts/validate-spec-output.cjs +355 -53
- package/src/claude/scripts/workflow-policy.cjs +1529 -162
- package/src/claude/settings/settings.json +8 -0
- package/src/claude/skills/backend-development/references/backend-security.md +52 -0
- package/src/claude/skills/code-review/SKILL.md +85 -66
- package/src/claude/skills/develop/SKILL.md +153 -285
- package/src/claude/skills/develop/references/quality-gate.md +112 -124
- package/src/claude/skills/develop/references/subagent-patterns.md +42 -37
- package/src/claude/skills/specs/SKILL.md +170 -381
- package/src/claude/skills/specs/rules/task-scoring-rubric.md +45 -46
- package/src/claude/skills/specs/rules/tasks-generation.md +49 -20
- package/src/claude/skills/specs/templates/spec-state.json +23 -2
- package/src/claude/skills/specs/templates/task.md +20 -0
- package/src/claude/skills/sync/SKILL.md +15 -8
- package/src/claude/skills/sync/references/sync-protocols.md +4 -4
- package/src/claude/skills/test/SKILL.md +141 -167
- package/src/codex/hooks/completion-authority-check.cjs +260 -0
- package/src/codex/hooks/completion-authority-state.cjs +525 -0
- package/src/codex/hooks/completion-authority.cjs +100 -0
- package/src/codex/hooks/lib/hook-context.cjs +42 -2
- package/src/codex/hooks/lib/spec-receipt.cjs +76 -43
- package/src/codex/hooks/lib/spec-utils.cjs +53 -27
- package/src/codex/hooks/session.cjs +2 -0
- package/src/codex/hooks/spec-gate.cjs +132 -20
- package/src/codex/hooks/spec-state.cjs +56 -10
- package/src/codex/hooks.json +10 -0
- package/src/opencode/AGENTS.md +4 -2
- package/src/opencode/plugins/spec-gate.ts +296 -122
- package/src/opencode/plugins/spec-state.ts +183 -116
|
@@ -1,15 +1,70 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const PROVENANCE = require('./provenance.cjs');
|
|
7
|
+
|
|
8
|
+
const CANONICAL_VERDICTS = Object.freeze(['PASS', 'PASS_WITH_WARNINGS', 'FAIL', 'BLOCKED']);
|
|
9
|
+
const LEGACY_DIAGNOSTIC_VERDICTS = Object.freeze(['PARTIAL', 'NO_TESTS']);
|
|
10
|
+
const REVIEW_VERDICTS = CANONICAL_VERDICTS;
|
|
4
11
|
const EXECUTION_TIERS = Object.freeze(['Light', 'Standard', 'Deep']);
|
|
5
12
|
const LANES = Object.freeze(['Direct', 'Standard', 'Critical']);
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
const ARTIFACT_PROFILES = Object.freeze(['targeted', 'bounded', 'strict']);
|
|
14
|
+
const PROOF_OBLIGATIONS = Object.freeze([
|
|
15
|
+
'needsInspection',
|
|
16
|
+
'needsExecutionProof',
|
|
17
|
+
'needsIndependentAudit',
|
|
18
|
+
'needsDurableTaskState',
|
|
19
|
+
'needsResearchGrounding',
|
|
20
|
+
]);
|
|
21
|
+
const WORKFLOW_POLICY_VERSION = '1';
|
|
22
|
+
const WORKFLOW_POLICY_FIELDS = Object.freeze([
|
|
23
|
+
'version',
|
|
24
|
+
'lane',
|
|
25
|
+
'automatic_lane',
|
|
26
|
+
'risks',
|
|
27
|
+
'artifact_profile',
|
|
28
|
+
'proof_obligations',
|
|
29
|
+
'actor_needs',
|
|
30
|
+
'override_receipt',
|
|
31
|
+
]);
|
|
32
|
+
const RECEIPT_BINDING_FIELDS = Object.freeze(['expectedProvenance', 'requireProvenanceBinding']);
|
|
33
|
+
const INDEPENDENT_AUDIT_SCHEMA_VERSION = '1';
|
|
34
|
+
const INDEPENDENT_AUDIT_FIELDS = Object.freeze([
|
|
35
|
+
'schema_version',
|
|
36
|
+
'reviewer_session_id',
|
|
37
|
+
'implementation_session_id',
|
|
38
|
+
'expected_provenance',
|
|
39
|
+
'evidence',
|
|
40
|
+
'verdict',
|
|
41
|
+
]);
|
|
42
|
+
const FLASH_PROMOTION_FIELDS = Object.freeze(['readyForSync', 'flashTransition', 'promotionReceipt']);
|
|
43
|
+
const LEGACY_TIER_LANES = Object.freeze({ Light: 'Direct', Standard: 'Standard', Deep: 'Critical' });
|
|
44
|
+
const ARTIFACT_PROFILE_BY_LANE = Object.freeze({ Direct: 'targeted', Standard: 'bounded', Critical: 'strict' });
|
|
45
|
+
const ACTOR_NEEDS_BY_OBLIGATION = Object.freeze({
|
|
46
|
+
needsInspection: Object.freeze({ capability: 'inspection', independence: 'same-session' }),
|
|
47
|
+
needsExecutionProof: Object.freeze({ capability: 'execution-proof', independence: 'same-session' }),
|
|
48
|
+
needsIndependentAudit: Object.freeze({ capability: 'audit', independence: 'independent' }),
|
|
49
|
+
needsDurableTaskState: Object.freeze({ capability: 'durable-task-state', independence: 'same-session' }),
|
|
50
|
+
needsResearchGrounding: Object.freeze({ capability: 'research-grounding', independence: 'same-session' }),
|
|
12
51
|
});
|
|
52
|
+
const ACTOR_CAPABILITIES = new Set(Object.values(ACTOR_NEEDS_BY_OBLIGATION).map((need) => need.capability));
|
|
53
|
+
const ACTOR_INDEPENDENCE = new Set(['same-session', 'independent']);
|
|
54
|
+
const CRITICAL_RISKS = new Set([
|
|
55
|
+
'reversibility',
|
|
56
|
+
'destructive',
|
|
57
|
+
'auth',
|
|
58
|
+
'payment',
|
|
59
|
+
'privacy',
|
|
60
|
+
'data',
|
|
61
|
+
'schema',
|
|
62
|
+
'migration',
|
|
63
|
+
'publicContract',
|
|
64
|
+
'crossRuntime',
|
|
65
|
+
'ambiguity',
|
|
66
|
+
'rollback',
|
|
67
|
+
]);
|
|
13
68
|
const LANE_RISK_KEYS = Object.freeze({
|
|
14
69
|
reversibility: ['reversible', 'reversibility', 'irreversible', 'non-reversible'],
|
|
15
70
|
destructive: ['destructive', 'deletion', 'delete', 'destroy'],
|
|
@@ -25,6 +80,342 @@ const LANE_RISK_KEYS = Object.freeze({
|
|
|
25
80
|
rollback: ['rollback', 'rollback difficulty', 'hard to rollback', 'cannot rollback', 'no rollback'],
|
|
26
81
|
});
|
|
27
82
|
|
|
83
|
+
function hasOwn(value, key) {
|
|
84
|
+
return value !== null
|
|
85
|
+
&& (typeof value === 'object' || typeof value === 'function')
|
|
86
|
+
&& Object.prototype.hasOwnProperty.call(value, key);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const PLACEHOLDER_TOKENS = new Set(['TBD','TODO','N/A','NA','NONE','UNKNOWN','PENDING','PLACEHOLDER','REPLACE_ME','-','?']);
|
|
90
|
+
const TAP_METADATA_HEADING_RE = /^\s*#\s*(?:(?:tests?|suites?|pass|fail|cancel(?:l)?ed|skipped|todo)\s+\d+|duration(?:_ms)?\s+\d+(?:\.\d+)?)\s*$/i;
|
|
91
|
+
const ARTIFACT_DECLARATION_KEY = 'artifacts';
|
|
92
|
+
const TASK_ARTIFACT_KEYS = Object.freeze(['artifact', 'artifact_ref', 'artifact_path']);
|
|
93
|
+
const PROVENANCE_VALUE_RE = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/i;
|
|
94
|
+
const PROSE_RECEIPT_FIELD_RE = /^\s*(?:Notes?|Description|Command(?:\(s\))?)\s*:/i;
|
|
95
|
+
function isPlaceholderToken(value) {
|
|
96
|
+
if (typeof value !== 'string') return true;
|
|
97
|
+
const v = value.trim();
|
|
98
|
+
if (v === '') return true;
|
|
99
|
+
if (/^\{\{.*\}\}$/.test(v)) return true;
|
|
100
|
+
if (/^<.*>$/.test(v)) return true;
|
|
101
|
+
const upper = v.toUpperCase();
|
|
102
|
+
if (PLACEHOLDER_TOKENS.has(upper)) return true;
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
function isTapMetadataHeading(line) {
|
|
106
|
+
return typeof line === 'string' && TAP_METADATA_HEADING_RE.test(line);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function hasConcreteArtifactDeclaration(value) {
|
|
110
|
+
if (typeof value === 'string') return !isPlaceholderToken(value);
|
|
111
|
+
if (Array.isArray(value)) return value.some(hasConcreteArtifactDeclaration);
|
|
112
|
+
return Boolean(value && typeof value === 'object');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isSafeArtifactPath(value) {
|
|
116
|
+
if (typeof value !== 'string' || value !== value.trim() || isPlaceholderToken(value)) return false;
|
|
117
|
+
if (/^(?:[a-z]:[\\/]|[\\/]|https?:\/\/)/i.test(value)) return false;
|
|
118
|
+
return !value.split(/[\\/]+/).includes('..');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isPathInside(root, target) {
|
|
122
|
+
const relative = path.relative(path.resolve(root), path.resolve(target));
|
|
123
|
+
return relative !== '' && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function stableStat(stat) {
|
|
127
|
+
return [stat.dev, stat.ino, stat.size, stat.mtimeNs?.toString(), stat.ctimeNs?.toString()].join(':');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function hashValidatedArtifact(root, relativePath, expectedHash) {
|
|
131
|
+
if (!isSafeArtifactPath(relativePath) || typeof root !== 'string' || root.trim() === '') return false;
|
|
132
|
+
const rootPath = path.resolve(root);
|
|
133
|
+
const target = path.resolve(rootPath, relativePath);
|
|
134
|
+
if (!isPathInside(rootPath, target)) return false;
|
|
135
|
+
|
|
136
|
+
const segments = path.relative(rootPath, target).split(path.sep).filter(Boolean);
|
|
137
|
+
let current = rootPath;
|
|
138
|
+
try {
|
|
139
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
140
|
+
current = path.join(current, segments[index]);
|
|
141
|
+
const stat = fs.lstatSync(current);
|
|
142
|
+
if (stat.isSymbolicLink()) return false;
|
|
143
|
+
if (index < segments.length - 1 && !stat.isDirectory()) return false;
|
|
144
|
+
}
|
|
145
|
+
const rootReal = fs.realpathSync(rootPath);
|
|
146
|
+
const targetReal = fs.realpathSync(target);
|
|
147
|
+
if (!isPathInside(rootReal, targetReal)) return false;
|
|
148
|
+
const before = fs.statSync(target);
|
|
149
|
+
if (!before.isFile()) return false;
|
|
150
|
+
const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0);
|
|
151
|
+
const fd = fs.openSync(target, flags);
|
|
152
|
+
try {
|
|
153
|
+
const opened = fs.fstatSync(fd);
|
|
154
|
+
if (!opened.isFile() || stableStat(opened) !== stableStat(before)) return false;
|
|
155
|
+
const bytes = fs.readFileSync(fd);
|
|
156
|
+
const after = fs.fstatSync(fd);
|
|
157
|
+
if (stableStat(after) !== stableStat(opened)) return false;
|
|
158
|
+
const actual = crypto.createHash('sha256').update(bytes).digest('hex');
|
|
159
|
+
return actual.toLowerCase() === String(expectedHash).toLowerCase();
|
|
160
|
+
} finally {
|
|
161
|
+
fs.closeSync(fd);
|
|
162
|
+
}
|
|
163
|
+
} catch (_) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function artifactDeclaration(task = {}) {
|
|
169
|
+
if (Object.prototype.hasOwnProperty.call(task, ARTIFACT_DECLARATION_KEY)) {
|
|
170
|
+
const artifacts = task[ARTIFACT_DECLARATION_KEY];
|
|
171
|
+
const unique = Array.isArray(artifacts) ? new Set(artifacts).size === artifacts.length : false;
|
|
172
|
+
return {
|
|
173
|
+
declared: true,
|
|
174
|
+
valid: Array.isArray(artifacts)
|
|
175
|
+
&& artifacts.length > 0
|
|
176
|
+
&& unique
|
|
177
|
+
&& artifacts.every(isSafeArtifactPath),
|
|
178
|
+
paths: Array.isArray(artifacts) ? [...artifacts] : [],
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Read legacy aliases only for compatibility with pre-P0 task registries.
|
|
183
|
+
const legacyKey = TASK_ARTIFACT_KEYS.find((key) => hasConcreteArtifactDeclaration(task?.[key]));
|
|
184
|
+
const legacyValue = legacyKey ? task[legacyKey] : null;
|
|
185
|
+
const legacyPaths = typeof legacyValue === 'string'
|
|
186
|
+
? [legacyValue]
|
|
187
|
+
: Array.isArray(legacyValue) && legacyValue.every((value) => typeof value === 'string')
|
|
188
|
+
? [...legacyValue]
|
|
189
|
+
: [];
|
|
190
|
+
return {
|
|
191
|
+
declared: Boolean(legacyKey),
|
|
192
|
+
valid: !legacyKey || (legacyPaths.length > 0 && legacyPaths.every(isSafeArtifactPath)),
|
|
193
|
+
legacy: true,
|
|
194
|
+
paths: legacyPaths,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function normalizeProvenanceValue(value) {
|
|
199
|
+
if (typeof value !== 'string') return null;
|
|
200
|
+
const normalized = value.trim().replace(/^sha256:/i, '');
|
|
201
|
+
return normalized || null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function isConcreteProvenanceValue(value) {
|
|
205
|
+
return PROVENANCE_VALUE_RE.test(normalizeProvenanceValue(value) || '');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function runtimeContextFromContext(context) {
|
|
209
|
+
const candidates = [
|
|
210
|
+
context?.runtime_context,
|
|
211
|
+
context?.runtimeContext,
|
|
212
|
+
context?.provenance_context,
|
|
213
|
+
context?.provenanceContext,
|
|
214
|
+
context?.receipt_binding?.runtimeContext,
|
|
215
|
+
context?.receiptBinding?.runtimeContext,
|
|
216
|
+
context?.provenance_binding?.runtimeContext,
|
|
217
|
+
context?.provenanceBinding?.runtimeContext,
|
|
218
|
+
];
|
|
219
|
+
const candidate = candidates.find((value) => PROVENANCE.isTrustedRuntimeContext(value));
|
|
220
|
+
if (!candidate) return null;
|
|
221
|
+
try { return PROVENANCE.recomputeRuntimeContext(candidate); } catch (_) { return null; }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function firstDefined(source, keys) {
|
|
225
|
+
if (!source || typeof source !== 'object') return undefined;
|
|
226
|
+
for (const key of keys) {
|
|
227
|
+
if (Object.prototype.hasOwnProperty.call(source, key)) return source[key];
|
|
228
|
+
}
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function expectedProvenanceFromSource(source) {
|
|
233
|
+
if (!source || typeof source !== 'object' || Array.isArray(source)) return null;
|
|
234
|
+
const nested = firstDefined(source, [
|
|
235
|
+
'expectedProvenance',
|
|
236
|
+
'expected_provenance',
|
|
237
|
+
]);
|
|
238
|
+
const candidate = nested && typeof nested === 'object' && !Array.isArray(nested)
|
|
239
|
+
? nested
|
|
240
|
+
: source;
|
|
241
|
+
const base = firstDefined(candidate, [
|
|
242
|
+
'base', 'Base', 'base_sha', 'baseSha', 'expectedBase', 'expected_base',
|
|
243
|
+
'expectedBaseSha', 'expected_base_sha',
|
|
244
|
+
]);
|
|
245
|
+
const head = firstDefined(candidate, [
|
|
246
|
+
'head', 'Head', 'head_sha', 'headSha', 'expectedHead', 'expected_head',
|
|
247
|
+
'expectedHeadSha', 'expected_head_sha',
|
|
248
|
+
]);
|
|
249
|
+
if (base === undefined && head === undefined) return null;
|
|
250
|
+
return { base, head };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function createReceiptBinding(runtimeContext) {
|
|
254
|
+
if (!PROVENANCE.isTrustedRuntimeContext(runtimeContext)) {
|
|
255
|
+
throw new TypeError('Receipt binding requires a runtime-derived provenance context');
|
|
256
|
+
}
|
|
257
|
+
try { return PROVENANCE.createReceiptBinding(runtimeContext); } catch (error) {
|
|
258
|
+
throw new TypeError(error.message);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function receiptValidatorOptions(task = {}, {
|
|
263
|
+
requireProvenanceBinding = false,
|
|
264
|
+
requireExplicitBinding = false,
|
|
265
|
+
runtimeContext,
|
|
266
|
+
} = {}) {
|
|
267
|
+
const declaration = artifactDeclaration(task);
|
|
268
|
+
let trusted = null;
|
|
269
|
+
if (runtimeContext !== undefined && PROVENANCE.isTrustedRuntimeContext(runtimeContext)) {
|
|
270
|
+
try { trusted = PROVENANCE.recomputeRuntimeContext(runtimeContext); } catch (_) { trusted = null; }
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
requireArtifactHash: declaration.declared,
|
|
274
|
+
artifactDeclarationValid: declaration.valid,
|
|
275
|
+
artifactPaths: declaration.paths,
|
|
276
|
+
artifactRoot: trusted?.project_root || null,
|
|
277
|
+
verifyArtifactBytes: Boolean(trusted?.project_root),
|
|
278
|
+
expectedProvenance: trusted ? { base: trusted.base, head: trusted.head } : null,
|
|
279
|
+
requireProvenanceBinding: requireProvenanceBinding || (runtimeContext !== undefined && runtimeContext !== null) || requireExplicitBinding,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function sha256ValuesFromLine(line) {
|
|
284
|
+
const values = [];
|
|
285
|
+
const labels = [...line.matchAll(/\b(?:artifact[_-])?sha-?256\s*:/gi)];
|
|
286
|
+
for (const label of labels) {
|
|
287
|
+
const remainder = line.slice(label.index + label[0].length);
|
|
288
|
+
const token = remainder.match(/^\s*([^\s)\],;`]+)/);
|
|
289
|
+
values.push(token ? token[1].trim() : '');
|
|
290
|
+
}
|
|
291
|
+
return values;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function artifactPathText(line) {
|
|
295
|
+
return line
|
|
296
|
+
.replace(/^\s*Artifacts?\s*:\s*/i, '')
|
|
297
|
+
.replace(/^\s*Artifact\s+produced\s*:?\s*/i, '')
|
|
298
|
+
.replace(/\b(?:artifact[_-])?sha-?256\s*:\s*[^\s)\],;`]+/gi, '')
|
|
299
|
+
.replace(/[()[\]{}]/g, ' ')
|
|
300
|
+
.replace(/\s+/g, ' ')
|
|
301
|
+
.trim()
|
|
302
|
+
.replace(/(?:\s*[+,;]\s*)+$/, '')
|
|
303
|
+
.trim();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function parseArtifactDeclarations(body) {
|
|
307
|
+
const declarations = [];
|
|
308
|
+
let previous = null;
|
|
309
|
+
for (const raw of body.split('\n')) {
|
|
310
|
+
if (PROSE_RECEIPT_FIELD_RE.test(raw)) {
|
|
311
|
+
previous = null;
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
const isDeclaration = /^\s*Artifacts?\s*:/i.test(raw)
|
|
315
|
+
|| /^\s*Artifact\s+produced\b/i.test(raw)
|
|
316
|
+
|| /^\s*artifact[_-]?sha-?256\s*:/i.test(raw);
|
|
317
|
+
if (isDeclaration) {
|
|
318
|
+
previous = {
|
|
319
|
+
raw,
|
|
320
|
+
pathText: artifactPathText(raw),
|
|
321
|
+
hashes: sha256ValuesFromLine(raw),
|
|
322
|
+
};
|
|
323
|
+
declarations.push(previous);
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (previous && /^\s*sha-?256\s*:/i.test(raw)) {
|
|
327
|
+
previous.hashes.push(...sha256ValuesFromLine(raw));
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (raw.trim() !== '') previous = null;
|
|
331
|
+
}
|
|
332
|
+
return declarations;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function artifactSha256Values(body) {
|
|
336
|
+
return parseArtifactDeclarations(body).flatMap((declaration) => declaration.hashes);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function hasCanonicalArtifactHash(body) {
|
|
340
|
+
const hashes = artifactSha256Values(body);
|
|
341
|
+
return hashes.length > 0 && hashes.every((value) => /^[a-f0-9]{64}$/i.test(value));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function hasConcreteArtifactHash(body) {
|
|
345
|
+
const hashes = artifactSha256Values(body);
|
|
346
|
+
return hashes.length > 0 && hashes.every((value) => value !== '' && !isPlaceholderToken(value));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function hasExplicitFailure(body) {
|
|
350
|
+
if (typeof body !== 'string') return false;
|
|
351
|
+
const lines = body.split('\n');
|
|
352
|
+
for (const raw of lines) {
|
|
353
|
+
if (PROSE_RECEIPT_FIELD_RE.test(raw)) continue;
|
|
354
|
+
const trimmed = raw.trim();
|
|
355
|
+
if (trimmed === '') continue;
|
|
356
|
+
// 1. Zero execution (structured, anchored): zero tests, zero passing, or no collection.
|
|
357
|
+
if (/^\s*(?:#|ℹ)?\s*tests?\s+0\b/i.test(raw)) return true;
|
|
358
|
+
if (/^\s*(?:#|ℹ)?\s*0\s+tests?\b/i.test(raw)) return true;
|
|
359
|
+
if (/^\s*(?:#|ℹ)?\s*tests?\s+run\s*:\s*0\b/i.test(raw)) return true;
|
|
360
|
+
if (/^\s*0\s+(?:passing|passed)\b/i.test(raw)) return true;
|
|
361
|
+
if (/^\s*Tests?\s*:\s*0\s+(?:passing|passed)\b/i.test(raw)) return true;
|
|
362
|
+
if (/^\s*Tests?\s+passed\s*:\s*0\b/i.test(raw)) return true;
|
|
363
|
+
if (/^\s*Passed\s*:\s*0\b/i.test(raw)) return true;
|
|
364
|
+
if (/^\s*Tests:\s*0\s+total\b/i.test(raw)) return true;
|
|
365
|
+
if (/^\s*collected\s+0\s+items\b/i.test(raw)) return true;
|
|
366
|
+
if (/^\s*No tests? found\b/i.test(raw)) return true;
|
|
367
|
+
// 2. Cancellation (optional marker #/ℹ, cancelled/canceled, both orders, count >0; 0 passes)
|
|
368
|
+
if (/^\s*(?:#|ℹ)?\s*cancel(?:l)?ed\s*[:\s]*[1-9]\d*\b/i.test(raw)) return true;
|
|
369
|
+
if (/^\s*(?:#|ℹ)?\s*[1-9]\d*\s+cancel(?:l)?ed\b/i.test(raw)) return true;
|
|
370
|
+
// 3. Suite / file failures: Test Suites: N failed, Test Files N failed (N>0)
|
|
371
|
+
if (/^\s*Test Suites?:?\s*[1-9]\d*\s+failed\b/i.test(raw)) return true;
|
|
372
|
+
if (/^\s*Test Files?:?\s*[1-9]\d*\s+failed\b/i.test(raw)) return true;
|
|
373
|
+
if (/^\s*Test Suites?:?\s*0\s+failed\b[^\r\n]*\b0\s+passed\b/i.test(raw)) return true;
|
|
374
|
+
if (/^\s*Test Files?:?\s*0\s+failed\b[^\r\n]*\b0\s+passed\b/i.test(raw)) return true;
|
|
375
|
+
// 4. Pytest short-summary entries (anchored to a Python test path)
|
|
376
|
+
if (/^\s*FAILED[ \t]+\S+\.py(?:::\S+)?(?:[ \t]+-[^\r\n]*)?\s*$/i.test(raw)) return true;
|
|
377
|
+
// 5. Runner FAIL (anchored exact FAIL, horizontal whitespace/colon/end; --- FAIL:)
|
|
378
|
+
if (/^\s*FAIL(?:[ \t]+|:|$)/.test(raw)) return true;
|
|
379
|
+
if (/^\s*---\s*FAIL:/.test(raw)) return true;
|
|
380
|
+
// 6. Error/failure summaries (anchored, N>0; prose Notes/Description/Command skipped)
|
|
381
|
+
if (/^\s*[1-9]\d*\s+errors?\b/i.test(raw)) return true;
|
|
382
|
+
if (/^\s*errors?\s*[:\s]*[1-9]\d*\b/i.test(raw)) return true;
|
|
383
|
+
if (/^\s*failures?\s*[:\s]*[1-9]\d*\b/i.test(raw)) return true;
|
|
384
|
+
if (/^\s*failure\s+summary\s*[:.]?\s*$/i.test(raw)) return true;
|
|
385
|
+
if (/^\s*Found\s+[1-9]\d*\s+errors?\b/i.test(raw)) return true;
|
|
386
|
+
if (/^\s*failureCount\s*:\s*[1-9]\d*\s*[,;]?\s*$/i.test(raw)) return true;
|
|
387
|
+
if (/^\s*ERROR\s*$/i.test(raw)) return true;
|
|
388
|
+
if (/^\s*ERROR\s+collecting\b/i.test(raw)) return true;
|
|
389
|
+
if (/^\s*Tests run\s*:/i.test(raw) && (/\b(?:Failures?|Errors?)\s*[:=]\s*[1-9]/i.test(raw) || /\b0\s+(?:tests?|passing|passed)\b/i.test(raw))) return true;
|
|
390
|
+
// Maven Surefire's structured failure lines; do not treat arbitrary [ERROR] prose as failure.
|
|
391
|
+
if (/^\s*\[ERROR\]\s+Tests run:\s*\d+\b[^\r\n]*\b(?:Failures?|Errors?):\s*[1-9]\d*\b/i.test(raw)) return true;
|
|
392
|
+
if (/^\s*\[ERROR\]\s+Failed to execute goal\b/i.test(raw)) return true;
|
|
393
|
+
// 7. Gradle (anchored, optional)
|
|
394
|
+
if (/^\s*\d+\s+tests?\s+completed,\s*[1-9]\d*\s+failed\b/i.test(raw)) return true;
|
|
395
|
+
// Existing structured checks (preserve, anchored where needed to avoid prose false positive)
|
|
396
|
+
if (/^\s*Tests?\s+failed\s*$/i.test(raw)) return true;
|
|
397
|
+
if (/^\s*Tests?\s+failed\s*:\s*[1-9]\d*\b/i.test(raw)) return true;
|
|
398
|
+
if (/\b(?:Verification|Result|Status|Outcome)\s*:\s*FAIL(?:ED|URE)?\b/i.test(raw)) return true;
|
|
399
|
+
if (/^\s*FAIL\s*$/i.test(trimmed)) return true;
|
|
400
|
+
if (/^\s*FAIL\s*:/i.test(trimmed)) return true;
|
|
401
|
+
if (/^\s*FAILED\s*$/i.test(trimmed)) return true;
|
|
402
|
+
if (/^\s*FAILED\s*:/i.test(trimmed)) return true;
|
|
403
|
+
if (/^\s*FAILURE\s*$/i.test(trimmed)) return true;
|
|
404
|
+
if (/^\s*FAILURE\s*:/i.test(trimmed)) return true;
|
|
405
|
+
if (/^\s*one\s+failing(?:\s+(?:test|tests|suite|suites))?\s*[,:]?\s*$/i.test(raw)) return true;
|
|
406
|
+
if (/^\s*(?:#|ℹ)?\s*fail\s+[1-9]/i.test(raw)) return true;
|
|
407
|
+
if (/^\s*not ok\b/i.test(raw)) return true;
|
|
408
|
+
if (/^\s*Tests\s*:?\s*[1-9]/i.test(raw) && /\bfailed\b/i.test(raw)) return true;
|
|
409
|
+
if (/^\s*\d+\s+failed\b/i.test(raw)) {
|
|
410
|
+
const m = raw.match(/^\s*(\d+)\s+failed\b/i);
|
|
411
|
+
if (m && parseInt(m[1], 10) !== 0) return true;
|
|
412
|
+
}
|
|
413
|
+
if (/exit\s+code\s*[:=]?\s*[1-9]/i.test(raw)) return true;
|
|
414
|
+
}
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
const trustedClassifications = new WeakSet();
|
|
418
|
+
|
|
28
419
|
const APPROVAL_SCHEMA_VERSION = '2.0';
|
|
29
420
|
const LEGACY_APPROVAL_ERROR = `Legacy approval state detected (field \`approved\`). Migration required: replace \`approved\` with \`agent_validated\` and \`user_approved\` per schema v${APPROVAL_SCHEMA_VERSION} (schema_version: "${APPROVAL_SCHEMA_VERSION}"). See spec-state.json template. Refusing to infer user approval.`;
|
|
30
421
|
|
|
@@ -108,22 +499,37 @@ function overrideLane(input = {}) {
|
|
|
108
499
|
return input.override ?? input.laneOverride ?? input.requestedLane ?? input.lane ?? null;
|
|
109
500
|
}
|
|
110
501
|
|
|
111
|
-
function
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
502
|
+
function isValidOverrideReceipt(receipt, automaticLane, requestedLane, risks = []) {
|
|
503
|
+
// P0.4 fail-closed: no trusted runtime issuer exists yet.
|
|
504
|
+
// Even a correctly shaped object with source:runtime must NOT be considered valid provenance.
|
|
505
|
+
// Downgrade is blocked until a verifiable, runtime-issued override receipt is implemented
|
|
506
|
+
// (bound to automaticLane/requestedLane/risks/session and verifiable by that issuer).
|
|
507
|
+
// Keep this function fail-closed; do not accept plain objects — always return false for P0.
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function isUserAuthorizedForDowngrade(input = {}, automaticLane, requestedLane, risks) {
|
|
512
|
+
// P0.4: downgrade requires structured override receipt with runtime provenance.
|
|
513
|
+
// Boolean flags (userAuthorized, user_approved, etc.) are never trusted.
|
|
514
|
+
// Fail closed — no trusted issuer exists yet, so always false.
|
|
515
|
+
// Explicitly ignore all boolean/string downgrade flags to prevent forged approvals.
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function freezeClassification(obj) {
|
|
520
|
+
Object.freeze(obj);
|
|
521
|
+
if (Array.isArray(obj.warnings)) Object.freeze(obj.warnings);
|
|
522
|
+
if (Array.isArray(obj.risks)) Object.freeze(obj.risks);
|
|
523
|
+
if (obj.signals && typeof obj.signals === 'object') Object.freeze(obj.signals);
|
|
524
|
+
trustedClassifications.add(obj);
|
|
525
|
+
return obj;
|
|
123
526
|
}
|
|
124
527
|
|
|
125
528
|
function classifyLane(input = {}) {
|
|
126
|
-
if (typeof input === 'string')
|
|
529
|
+
if (typeof input === 'string') {
|
|
530
|
+
const c = { lane: assertLane(input), automaticLane: input, overridden: false, warnings: [], risks: [], signals: {} };
|
|
531
|
+
return freezeClassification(c);
|
|
532
|
+
}
|
|
127
533
|
if (!input || typeof input !== 'object') throw new TypeError('Lane classification input must be an object');
|
|
128
534
|
|
|
129
535
|
const signals = classifyRiskSignals(input);
|
|
@@ -138,7 +544,7 @@ function classifyLane(input = {}) {
|
|
|
138
544
|
: 'Standard';
|
|
139
545
|
const requested = overrideLane(input);
|
|
140
546
|
if (requested === null || requested === undefined) {
|
|
141
|
-
|
|
547
|
+
const c = {
|
|
142
548
|
lane: automaticLane,
|
|
143
549
|
automaticLane,
|
|
144
550
|
overridden: false,
|
|
@@ -146,14 +552,15 @@ function classifyLane(input = {}) {
|
|
|
146
552
|
risks,
|
|
147
553
|
signals,
|
|
148
554
|
};
|
|
555
|
+
return freezeClassification(c);
|
|
149
556
|
}
|
|
150
557
|
assertLane(requested);
|
|
151
558
|
const isDowngrade = LANES.indexOf(requested) < LANES.indexOf(automaticLane);
|
|
152
|
-
if (isDowngrade && !isUserAuthorizedForDowngrade(input)) {
|
|
559
|
+
if (isDowngrade && !isUserAuthorizedForDowngrade(input, automaticLane, requested, risks)) {
|
|
153
560
|
throw new Error(
|
|
154
|
-
`Downgrade from ${automaticLane} to ${requested}
|
|
561
|
+
`Downgrade from ${automaticLane} to ${requested} is blocked: no trusted runtime issuer is available to authorize Critical downgrades. ` +
|
|
155
562
|
`Automatic ${automaticLane} due to: ${risks.join(', ') || 'default risk policy'}. ` +
|
|
156
|
-
`
|
|
563
|
+
`Keep ${automaticLane} or escalate; downgrade is unavailable until a verifiable runtime override receipt is implemented.`
|
|
157
564
|
);
|
|
158
565
|
}
|
|
159
566
|
const warnings = [`Explicit lane override selected: ${requested}.`];
|
|
@@ -161,12 +568,12 @@ function classifyLane(input = {}) {
|
|
|
161
568
|
warnings.push(`Override changes automatic ${automaticLane} classification to ${requested}.`);
|
|
162
569
|
if (LANES.indexOf(requested) < LANES.indexOf(automaticLane)) {
|
|
163
570
|
warnings.push(`Downgrading risk lane may reduce review and evidence coverage: ${risks.join(', ') || 'default risk policy'}.`);
|
|
164
|
-
if (isUserAuthorizedForDowngrade(input)) {
|
|
571
|
+
if (isUserAuthorizedForDowngrade(input, automaticLane, requested, risks)) {
|
|
165
572
|
warnings.push(`Downgrade authorized by user - proceeding with ${requested} despite ${automaticLane} signals.`);
|
|
166
573
|
}
|
|
167
574
|
}
|
|
168
575
|
}
|
|
169
|
-
|
|
576
|
+
const c2 = {
|
|
170
577
|
lane: requested,
|
|
171
578
|
automaticLane,
|
|
172
579
|
overridden: true,
|
|
@@ -174,29 +581,265 @@ function classifyLane(input = {}) {
|
|
|
174
581
|
risks,
|
|
175
582
|
signals,
|
|
176
583
|
};
|
|
584
|
+
return freezeClassification(c2);
|
|
177
585
|
}
|
|
178
586
|
|
|
179
|
-
function
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
:
|
|
183
|
-
|
|
184
|
-
|
|
587
|
+
function uniqueStrings(values, field) {
|
|
588
|
+
if (!Array.isArray(values)) return { valid: false, error: `${field} must be an array` };
|
|
589
|
+
if (values.some((value) => typeof value !== 'string' || value.trim() === '')) {
|
|
590
|
+
return { valid: false, error: `${field} must contain non-empty strings` };
|
|
591
|
+
}
|
|
592
|
+
if (new Set(values).size !== values.length) return { valid: false, error: `${field} must not contain duplicates` };
|
|
593
|
+
return { valid: true };
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function obligationsForLane(lane, risks = []) {
|
|
597
|
+
assertLane(lane);
|
|
598
|
+
const riskSet = new Set(risks);
|
|
599
|
+
const obligations = new Set(['needsExecutionProof']);
|
|
600
|
+
if (lane === 'Standard') obligations.add('needsInspection');
|
|
601
|
+
if (lane === 'Critical') {
|
|
602
|
+
obligations.add('needsInspection');
|
|
603
|
+
obligations.add('needsIndependentAudit');
|
|
604
|
+
if ([
|
|
605
|
+
'destructive', 'reversibility', 'rollback', 'data', 'schema', 'migration',
|
|
606
|
+
'privacy', 'payment', 'crossRuntime',
|
|
607
|
+
].some((risk) => riskSet.has(risk))) {
|
|
608
|
+
obligations.add('needsDurableTaskState');
|
|
609
|
+
}
|
|
610
|
+
if ([
|
|
611
|
+
'auth', 'payment', 'privacy', 'schema', 'migration', 'publicContract',
|
|
612
|
+
'crossRuntime', 'ambiguity',
|
|
613
|
+
].some((risk) => riskSet.has(risk))) {
|
|
614
|
+
obligations.add('needsResearchGrounding');
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return PROOF_OBLIGATIONS.filter((obligation) => obligations.has(obligation));
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function actorNeedsFor(obligations) {
|
|
621
|
+
return obligations.map((obligation) => ({ ...ACTOR_NEEDS_BY_OBLIGATION[obligation] }));
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function minimumLaneForRisks(risks = []) {
|
|
625
|
+
const listedRisks = Array.isArray(risks) ? risks : [];
|
|
626
|
+
const classifiedRisks = classifyRiskSignals({ risks: listedRisks });
|
|
627
|
+
if (Object.entries(classifiedRisks).some(([risk, active]) => active && CRITICAL_RISKS.has(risk))) {
|
|
628
|
+
return 'Critical';
|
|
629
|
+
}
|
|
630
|
+
if (listedRisks.length > 0) return 'Standard';
|
|
631
|
+
return 'Direct';
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function snapshotFromClassification(classification) {
|
|
185
635
|
const lane = assertLane(classification.lane);
|
|
186
|
-
const
|
|
636
|
+
const automaticLane = assertLane(classification.automaticLane || lane);
|
|
637
|
+
if (LANES.indexOf(lane) < LANES.indexOf(automaticLane)) {
|
|
638
|
+
throw new Error(`Workflow policy cannot downgrade ${automaticLane} to ${lane} without a trusted override receipt`);
|
|
639
|
+
}
|
|
640
|
+
const risks = Array.isArray(classification.risks) ? [...new Set(classification.risks)] : [];
|
|
641
|
+
const minimumLane = minimumLaneForRisks(risks);
|
|
642
|
+
if (LANES.indexOf(lane) < LANES.indexOf(minimumLane)) {
|
|
643
|
+
throw new Error(`Workflow policy lane ${lane} is below ${minimumLane} for risks: ${risks.join(', ')}`);
|
|
644
|
+
}
|
|
187
645
|
return {
|
|
646
|
+
version: WORKFLOW_POLICY_VERSION,
|
|
188
647
|
lane,
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
648
|
+
automatic_lane: automaticLane,
|
|
649
|
+
risks,
|
|
650
|
+
artifact_profile: ARTIFACT_PROFILE_BY_LANE[lane],
|
|
651
|
+
proof_obligations: obligationsForLane(lane, risks),
|
|
652
|
+
actor_needs: actorNeedsFor(obligationsForLane(lane, risks)),
|
|
653
|
+
override_receipt: null,
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function workflowPolicySnapshot(input = {}) {
|
|
658
|
+
if (input && typeof input === 'object' && hasOwn(input, 'workflow_policy')) {
|
|
659
|
+
return readWorkflowPolicySnapshot(input);
|
|
660
|
+
}
|
|
661
|
+
if (input && typeof input === 'object' && input.design_context?.execution_tier) {
|
|
662
|
+
return readWorkflowPolicySnapshot(input);
|
|
663
|
+
}
|
|
664
|
+
if (input && typeof input === 'object' && isWorkflowPolicySnapshot(input)) {
|
|
665
|
+
assertWorkflowPolicySnapshot(input);
|
|
666
|
+
return JSON.parse(JSON.stringify(input));
|
|
667
|
+
}
|
|
668
|
+
const classification = trustedClassifications.has(input)
|
|
669
|
+
? input
|
|
670
|
+
: classifyLane(input);
|
|
671
|
+
return snapshotFromClassification(classification);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function isWorkflowPolicySnapshot(value) {
|
|
675
|
+
return Boolean(value && typeof value === 'object' && Object.prototype.hasOwnProperty.call(value, 'version')
|
|
676
|
+
&& Object.prototype.hasOwnProperty.call(value, 'automatic_lane')
|
|
677
|
+
&& Object.prototype.hasOwnProperty.call(value, 'artifact_profile'));
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function validateWorkflowPolicySnapshot(snapshot) {
|
|
681
|
+
const errors = [];
|
|
682
|
+
if (!snapshot || typeof snapshot !== 'object' || Array.isArray(snapshot)) {
|
|
683
|
+
return { valid: false, errors: ['workflow_policy must be an object'] };
|
|
684
|
+
}
|
|
685
|
+
const keys = Object.keys(snapshot).sort();
|
|
686
|
+
const expectedKeys = [...WORKFLOW_POLICY_FIELDS].sort();
|
|
687
|
+
if (JSON.stringify(keys) !== JSON.stringify(expectedKeys)) {
|
|
688
|
+
errors.push(`workflow_policy fields must be exactly ${WORKFLOW_POLICY_FIELDS.join(', ')}`);
|
|
689
|
+
}
|
|
690
|
+
if (snapshot.version !== WORKFLOW_POLICY_VERSION) errors.push('workflow_policy.version must be "1"');
|
|
691
|
+
if (!LANES.includes(snapshot.lane)) errors.push(`workflow_policy.lane must be one of ${LANES.join(', ')}`);
|
|
692
|
+
if (!LANES.includes(snapshot.automatic_lane)) errors.push(`workflow_policy.automatic_lane must be one of ${LANES.join(', ')}`);
|
|
693
|
+
if (LANES.includes(snapshot.lane) && LANES.includes(snapshot.automatic_lane)
|
|
694
|
+
&& LANES.indexOf(snapshot.lane) < LANES.indexOf(snapshot.automatic_lane)) {
|
|
695
|
+
errors.push('workflow_policy.lane cannot be lower than automatic_lane without a trusted override receipt');
|
|
696
|
+
}
|
|
697
|
+
if (LANES.includes(snapshot.lane) && snapshot.artifact_profile !== ARTIFACT_PROFILE_BY_LANE[snapshot.lane]) {
|
|
698
|
+
errors.push(`workflow_policy.artifact_profile must be ${ARTIFACT_PROFILE_BY_LANE[snapshot.lane]} for ${snapshot.lane}`);
|
|
699
|
+
}
|
|
700
|
+
const risks = uniqueStrings(snapshot.risks, 'workflow_policy.risks');
|
|
701
|
+
if (!risks.valid) errors.push(risks.error);
|
|
702
|
+
if (risks.valid && LANES.includes(snapshot.lane)) {
|
|
703
|
+
const minimumLane = minimumLaneForRisks(snapshot.risks);
|
|
704
|
+
if (LANES.indexOf(snapshot.lane) < LANES.indexOf(minimumLane)) {
|
|
705
|
+
errors.push(`workflow_policy.lane must be at least ${minimumLane} for its risks`);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
const obligations = uniqueStrings(snapshot.proof_obligations, 'workflow_policy.proof_obligations');
|
|
709
|
+
if (!obligations.valid) {
|
|
710
|
+
errors.push(obligations.error);
|
|
711
|
+
} else {
|
|
712
|
+
for (const obligation of snapshot.proof_obligations) {
|
|
713
|
+
if (!PROOF_OBLIGATIONS.includes(obligation)) errors.push(`workflow_policy.proof_obligations has unknown obligation ${obligation}`);
|
|
714
|
+
}
|
|
715
|
+
if (!snapshot.proof_obligations.includes('needsExecutionProof')) {
|
|
716
|
+
errors.push('workflow_policy.proof_obligations must include needsExecutionProof');
|
|
717
|
+
}
|
|
718
|
+
if (LANES.includes(snapshot.lane) && risks.valid) {
|
|
719
|
+
const expected = obligationsForLane(snapshot.lane, snapshot.risks);
|
|
720
|
+
if (JSON.stringify(snapshot.proof_obligations) !== JSON.stringify(expected)) {
|
|
721
|
+
errors.push('workflow_policy.proof_obligations do not match lane and risks');
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
if (!Array.isArray(snapshot.actor_needs)) {
|
|
726
|
+
errors.push('workflow_policy.actor_needs must be an array');
|
|
727
|
+
} else {
|
|
728
|
+
for (const need of snapshot.actor_needs) {
|
|
729
|
+
if (!need || typeof need !== 'object' || Array.isArray(need)
|
|
730
|
+
|| Object.keys(need).sort().join(',') !== 'capability,independence'
|
|
731
|
+
|| !ACTOR_CAPABILITIES.has(need.capability)
|
|
732
|
+
|| !ACTOR_INDEPENDENCE.has(need.independence)) {
|
|
733
|
+
errors.push('workflow_policy.actor_needs must describe capability and independence, never an agent name');
|
|
734
|
+
break;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
if (LANES.includes(snapshot.lane) && obligations.valid
|
|
738
|
+
&& JSON.stringify(snapshot.actor_needs) !== JSON.stringify(actorNeedsFor(snapshot.proof_obligations))) {
|
|
739
|
+
errors.push('workflow_policy.actor_needs must correspond to proof_obligations');
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
if (snapshot.override_receipt !== null) {
|
|
743
|
+
errors.push('workflow_policy.override_receipt must be null until a trusted downgrade issuer exists');
|
|
744
|
+
}
|
|
745
|
+
return { valid: errors.length === 0, errors };
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function assertWorkflowPolicySnapshot(snapshot) {
|
|
749
|
+
const result = validateWorkflowPolicySnapshot(snapshot);
|
|
750
|
+
if (!result.valid) throw new TypeError(`Invalid workflow policy snapshot: ${result.errors.join('; ')}`);
|
|
751
|
+
return snapshot;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function readWorkflowPolicySnapshot(spec = {}) {
|
|
755
|
+
if (!spec || typeof spec !== 'object') throw new TypeError('Spec state must be an object');
|
|
756
|
+
if (hasOwn(spec, 'workflow_policy')) {
|
|
757
|
+
assertWorkflowPolicySnapshot(spec.workflow_policy);
|
|
758
|
+
return JSON.parse(JSON.stringify(spec.workflow_policy));
|
|
759
|
+
}
|
|
760
|
+
// Legacy state is read-compatible only. The fallback is deliberately not
|
|
761
|
+
// written back and cannot authorize a downgrade or override new input.
|
|
762
|
+
const rawLegacyTier = spec.design_context && spec.design_context.execution_tier;
|
|
763
|
+
const legacyTier = typeof rawLegacyTier === 'string'
|
|
764
|
+
? Object.keys(LEGACY_TIER_LANES).find((tier) => tier.toLowerCase() === rawLegacyTier.toLowerCase())
|
|
765
|
+
: null;
|
|
766
|
+
const legacyLane = legacyTier ? LEGACY_TIER_LANES[legacyTier] : null;
|
|
767
|
+
if (legacyLane) return snapshotFromClassification({ lane: legacyLane, automaticLane: legacyLane, risks: [] });
|
|
768
|
+
throw new TypeError('workflow_policy snapshot is missing at the spec boundary');
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function persistWorkflowPolicySnapshot(spec, input = {}) {
|
|
772
|
+
if (!spec || typeof spec !== 'object' || Array.isArray(spec)) throw new TypeError('Spec state must be an object');
|
|
773
|
+
const next = { ...spec };
|
|
774
|
+
delete next.override_receipt;
|
|
775
|
+
if (hasOwn(spec, 'workflow_policy')) {
|
|
776
|
+
assertWorkflowPolicySnapshot(spec.workflow_policy);
|
|
777
|
+
return { ...next, workflow_policy: JSON.parse(JSON.stringify(spec.workflow_policy)) };
|
|
778
|
+
}
|
|
779
|
+
return { ...next, workflow_policy: workflowPolicySnapshot(input) };
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const persistWorkflowPolicy = persistWorkflowPolicySnapshot;
|
|
783
|
+
const createWorkflowPolicySnapshot = workflowPolicySnapshot;
|
|
784
|
+
|
|
785
|
+
function candidateLaneForRisks(input, risks) {
|
|
786
|
+
const explicitLevel = String(input?.riskLevel || input?.risk_level || input?.level || '').toLowerCase();
|
|
787
|
+
const explicitLane = ['critical', 'high', 'deep'].includes(explicitLevel)
|
|
788
|
+
? 'Critical'
|
|
789
|
+
: ['standard', 'medium'].includes(explicitLevel)
|
|
790
|
+
? 'Standard'
|
|
791
|
+
: null;
|
|
792
|
+
const riskLane = minimumLaneForRisks(risks);
|
|
793
|
+
if (explicitLane === null && riskLane === 'Direct') return null;
|
|
794
|
+
if (explicitLane === null) return riskLane;
|
|
795
|
+
return LANES[Math.max(LANES.indexOf(explicitLane), LANES.indexOf(riskLane))];
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function escalateWorkflowPolicy(policyOrSpec, riskInput = {}) {
|
|
799
|
+
const current = policyOrSpec && typeof policyOrSpec === 'object' && hasOwn(policyOrSpec, 'workflow_policy')
|
|
800
|
+
? readWorkflowPolicySnapshot(policyOrSpec)
|
|
801
|
+
: workflowPolicySnapshot(policyOrSpec);
|
|
802
|
+
const discovered = classifyRiskSignals(riskInput);
|
|
803
|
+
const explicitRisks = Array.isArray(riskInput?.risks)
|
|
804
|
+
? riskInput.risks.filter((risk) => typeof risk === 'string' && risk.trim() !== '')
|
|
805
|
+
: [];
|
|
806
|
+
const newRisks = [...new Set([
|
|
807
|
+
...Object.entries(discovered).filter(([, active]) => active).map(([name]) => name),
|
|
808
|
+
...explicitRisks,
|
|
809
|
+
])];
|
|
810
|
+
const risks = [...new Set([...current.risks, ...newRisks])];
|
|
811
|
+
if (newRisks.every((risk) => current.risks.includes(risk))) return current;
|
|
812
|
+
const candidate = candidateLaneForRisks(riskInput, newRisks);
|
|
813
|
+
if (!candidate) return current;
|
|
814
|
+
const lane = LANES[Math.max(LANES.indexOf(current.lane), LANES.indexOf(candidate))];
|
|
815
|
+
return snapshotFromClassification({
|
|
816
|
+
lane,
|
|
817
|
+
automaticLane: current.automatic_lane,
|
|
818
|
+
risks,
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function policyForSpec(spec, riskInput = null) {
|
|
823
|
+
const snapshot = riskInput ? escalateWorkflowPolicy(spec, riskInput) : readWorkflowPolicySnapshot(spec);
|
|
824
|
+
return lanePolicy(snapshot);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function lanePolicy(input = {}) {
|
|
828
|
+
const snapshot = input && typeof input === 'object' && hasOwn(input, 'workflow_policy')
|
|
829
|
+
? readWorkflowPolicySnapshot(input)
|
|
830
|
+
: input && typeof input === 'object' && isWorkflowPolicySnapshot(input)
|
|
831
|
+
? (assertWorkflowPolicySnapshot(input), JSON.parse(JSON.stringify(input)))
|
|
832
|
+
: workflowPolicySnapshot(input);
|
|
833
|
+
const lane = assertLane(snapshot.lane);
|
|
834
|
+
return {
|
|
835
|
+
...snapshot,
|
|
836
|
+
proof_obligations: [...snapshot.proof_obligations],
|
|
837
|
+
actor_needs: snapshot.actor_needs.map((need) => ({ ...need })),
|
|
195
838
|
requiresSpec: lane !== 'Direct',
|
|
196
|
-
requiresState: lane
|
|
839
|
+
requiresState: lane === 'Critical',
|
|
197
840
|
qualityGate: lane === 'Direct' ? 'main-session' : lane === 'Standard' ? 'combined-feature-review' : 'strict-evidence',
|
|
198
841
|
shipPoint: lane === 'Direct' ? 'task' : 'feature',
|
|
199
|
-
evidence:
|
|
842
|
+
evidence: snapshot.artifact_profile,
|
|
200
843
|
};
|
|
201
844
|
}
|
|
202
845
|
|
|
@@ -205,85 +848,106 @@ function isLegacyApprovalObject(obj) {
|
|
|
205
848
|
}
|
|
206
849
|
|
|
207
850
|
function validateApprovalSchema(spec) {
|
|
208
|
-
if (!spec || typeof spec !== 'object') return { valid: false, legacy: false, error: 'spec must be
|
|
851
|
+
if (!spec || typeof spec !== 'object' || Array.isArray(spec)) return { valid: false, absent: false, legacy: false, error: 'spec must be a non-array object' };
|
|
852
|
+
if (!Object.prototype.hasOwnProperty.call(spec, 'approvals')) {
|
|
853
|
+
return { valid: true, absent: true, legacy: false };
|
|
854
|
+
}
|
|
209
855
|
const approvals = spec.approvals;
|
|
210
|
-
if (
|
|
856
|
+
if (approvals === null || typeof approvals !== 'object' || Array.isArray(approvals)) {
|
|
857
|
+
return { valid: false, absent: false, legacy: false, error: 'approvals must be a non-empty object' };
|
|
858
|
+
}
|
|
859
|
+
if (Object.keys(approvals).length === 0) {
|
|
860
|
+
return { valid: false, absent: false, legacy: false, error: 'approvals must not be empty' };
|
|
861
|
+
}
|
|
211
862
|
// Check for legacy field `approved` in any stage
|
|
212
863
|
for (const [stage, val] of Object.entries(approvals)) {
|
|
213
864
|
if (isLegacyApprovalObject(val)) {
|
|
214
865
|
return {
|
|
215
866
|
valid: false,
|
|
867
|
+
absent: false,
|
|
216
868
|
legacy: true,
|
|
217
869
|
error: `Legacy approval state at approvals.${stage}.approved. ${LEGACY_APPROVAL_ERROR}`,
|
|
218
870
|
};
|
|
219
871
|
}
|
|
220
|
-
if (val
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
872
|
+
if (!val || typeof val !== 'object' || Array.isArray(val)) {
|
|
873
|
+
return { valid: false, absent: false, legacy: false, error: `approvals.${stage} must be an approval object` };
|
|
874
|
+
}
|
|
875
|
+
const keys = Object.keys(val).sort();
|
|
876
|
+
if (keys.join(',') !== 'agent_validated,generated,user_approved') {
|
|
877
|
+
return { valid: false, absent: false, legacy: false, error: `approvals.${stage} must contain exactly generated, agent_validated, and user_approved` };
|
|
878
|
+
}
|
|
879
|
+
if (keys.some((key) => typeof val[key] !== 'boolean')) {
|
|
880
|
+
return { valid: false, absent: false, legacy: false, error: `approvals.${stage} fields must be boolean` };
|
|
225
881
|
}
|
|
226
882
|
}
|
|
227
883
|
// Check schema version
|
|
228
|
-
const version = spec
|
|
884
|
+
const version = Object.prototype.hasOwnProperty.call(spec, 'schema_version')
|
|
885
|
+
? spec.schema_version
|
|
886
|
+
: spec.approval_schema_version;
|
|
887
|
+
if (version !== undefined && typeof version !== 'string') {
|
|
888
|
+
return { valid: false, absent: false, legacy: false, error: 'schema_version must be a string' };
|
|
889
|
+
}
|
|
229
890
|
if (version && version !== APPROVAL_SCHEMA_VERSION) {
|
|
230
|
-
return { valid: false, legacy: false, error: `Unsupported schema_version ${version}, expected ${APPROVAL_SCHEMA_VERSION}` };
|
|
891
|
+
return { valid: false, absent: false, legacy: false, error: `Unsupported schema_version ${version}, expected ${APPROVAL_SCHEMA_VERSION}` };
|
|
231
892
|
}
|
|
232
893
|
if (!version && Object.keys(approvals).length > 0) {
|
|
233
894
|
// No version but uses new schema - allow for backward compat but warn that version should be present
|
|
234
895
|
// Fail closed only if legacy detected above; otherwise allow.
|
|
235
|
-
return { valid: true, legacy: false, warning: `Missing schema_version, expected ${APPROVAL_SCHEMA_VERSION}` };
|
|
896
|
+
return { valid: true, absent: false, legacy: false, warning: `Missing schema_version, expected ${APPROVAL_SCHEMA_VERSION}` };
|
|
236
897
|
}
|
|
237
|
-
return { valid: true, legacy: false };
|
|
898
|
+
return { valid: true, absent: false, legacy: false };
|
|
238
899
|
}
|
|
239
900
|
|
|
240
901
|
function approvalState(state = {}) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
902
|
+
if (!state || typeof state !== 'object' || Array.isArray(state)) {
|
|
903
|
+
throw new TypeError('approval state must be an object');
|
|
904
|
+
}
|
|
905
|
+
if (Object.prototype.hasOwnProperty.call(state, 'approvals')) {
|
|
906
|
+
const validation = validateApprovalSchema(state);
|
|
907
|
+
if (!validation.valid) throw new Error(validation.error);
|
|
908
|
+
return {
|
|
909
|
+
generated: false,
|
|
910
|
+
agent_validated: false,
|
|
911
|
+
user_approved: false,
|
|
912
|
+
ready: false,
|
|
913
|
+
present: true,
|
|
914
|
+
schema_version: APPROVAL_SCHEMA_VERSION,
|
|
915
|
+
legacy: false,
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
if (isLegacyApprovalObject(state)) throw new Error(LEGACY_APPROVAL_ERROR);
|
|
919
|
+
|
|
920
|
+
const version = Object.prototype.hasOwnProperty.call(state, 'schema_version')
|
|
921
|
+
? state.schema_version
|
|
922
|
+
: state.approval_schema_version;
|
|
923
|
+
if (version !== undefined && (typeof version !== 'string' || version !== APPROVAL_SCHEMA_VERSION)) {
|
|
924
|
+
throw new Error(`Unsupported schema_version ${String(version)}, expected ${APPROVAL_SCHEMA_VERSION}`);
|
|
249
925
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const target = approvalsMap ? null : state;
|
|
260
|
-
if (target && isLegacyApprovalObject(target)) {
|
|
261
|
-
throw new Error(LEGACY_APPROVAL_ERROR);
|
|
262
|
-
}
|
|
263
|
-
// For single stage case, use target
|
|
264
|
-
// For map case, we shouldn't be called with map; but if called with spec object containing approvals map, we return not-ready?
|
|
265
|
-
if (approvalsMap) {
|
|
266
|
-
// This is a spec-level object, not a stage. Return aggregate? For compatibility, treat as not ready.
|
|
267
|
-
// Check if any stage has valid approval? But spec-level ready requires all stages.
|
|
268
|
-
// We'll just return not ready with legacy check already done.
|
|
269
|
-
return { generated: false, agent_validated: false, user_approved: false, ready: false, schema_version: APPROVAL_SCHEMA_VERSION, legacy: false };
|
|
270
|
-
}
|
|
271
|
-
const src = target || state;
|
|
272
|
-
// Final legacy check on src
|
|
273
|
-
if (isLegacyApprovalObject(src)) {
|
|
274
|
-
throw new Error(LEGACY_APPROVAL_ERROR);
|
|
926
|
+
|
|
927
|
+
const known = ['generated', 'agent_validated', 'user_approved'];
|
|
928
|
+
const present = known.some((key) => Object.prototype.hasOwnProperty.call(state, key));
|
|
929
|
+
const unknown = Object.keys(state).filter((key) => !known.includes(key) && key !== 'schema_version' && key !== 'approval_schema_version');
|
|
930
|
+
if (unknown.length > 0) throw new TypeError(`malformed approval state fields: ${unknown.join(', ')}`);
|
|
931
|
+
for (const key of known) {
|
|
932
|
+
if (Object.prototype.hasOwnProperty.call(state, key) && typeof state[key] !== 'boolean') {
|
|
933
|
+
throw new TypeError(`approval state field ${key} must be boolean`);
|
|
934
|
+
}
|
|
275
935
|
}
|
|
276
936
|
const result = {
|
|
277
|
-
generated:
|
|
278
|
-
agent_validated:
|
|
279
|
-
user_approved:
|
|
937
|
+
generated: state.generated === true,
|
|
938
|
+
agent_validated: state.agent_validated === true,
|
|
939
|
+
user_approved: state.user_approved === true,
|
|
940
|
+
};
|
|
941
|
+
return {
|
|
942
|
+
...result,
|
|
943
|
+
ready: result.generated && result.agent_validated && result.user_approved,
|
|
944
|
+
present,
|
|
945
|
+
schema_version: APPROVAL_SCHEMA_VERSION,
|
|
280
946
|
};
|
|
281
|
-
return { ...result, ready: result.generated && result.agent_validated && result.user_approved, schema_version: APPROVAL_SCHEMA_VERSION };
|
|
282
947
|
}
|
|
283
|
-
|
|
284
948
|
function assertVerdict(verdict) {
|
|
285
|
-
if (!
|
|
286
|
-
throw new TypeError(`Unsupported
|
|
949
|
+
if (!CANONICAL_VERDICTS.includes(verdict)) {
|
|
950
|
+
throw new TypeError(`Unsupported canonical verdict: ${String(verdict)}`);
|
|
287
951
|
}
|
|
288
952
|
return verdict;
|
|
289
953
|
}
|
|
@@ -310,10 +974,9 @@ function delegationPlan({ tier, lane = null, mode = 'full-spec', taskCount = 1 }
|
|
|
310
974
|
const policy = lanePolicy(lane);
|
|
311
975
|
return {
|
|
312
976
|
lane: policy.lane,
|
|
313
|
-
executionTier: policy.executionTier,
|
|
314
|
-
tier: tier || null,
|
|
315
977
|
mode,
|
|
316
|
-
|
|
978
|
+
proof_obligations: [...policy.proof_obligations],
|
|
979
|
+
actor_needs: policy.actor_needs.map((need) => ({ ...need })),
|
|
317
980
|
qualityGate: policy.qualityGate,
|
|
318
981
|
shipPoint: policy.shipPoint,
|
|
319
982
|
};
|
|
@@ -325,65 +988,476 @@ function delegationPlan({ tier, lane = null, mode = 'full-spec', taskCount = 1 }
|
|
|
325
988
|
throw new RangeError('taskCount must be a positive integer');
|
|
326
989
|
}
|
|
327
990
|
|
|
328
|
-
|
|
329
|
-
|
|
991
|
+
// Legacy tier input is read-only compatibility. It is translated once to
|
|
992
|
+
// the canonical lane contract and never emits a new tier authority or an
|
|
993
|
+
// agent sequence.
|
|
994
|
+
const legacyLane = LEGACY_TIER_LANES[tier];
|
|
995
|
+
const policy = lanePolicy(legacyLane);
|
|
996
|
+
return {
|
|
997
|
+
tier,
|
|
998
|
+
legacy: true,
|
|
999
|
+
lane: policy.lane,
|
|
1000
|
+
mode,
|
|
1001
|
+
proof_obligations: [...policy.proof_obligations],
|
|
1002
|
+
actor_needs: policy.actor_needs.map((need) => ({ ...need })),
|
|
1003
|
+
qualityGate: policy.qualityGate,
|
|
1004
|
+
shipPoint: policy.shipPoint,
|
|
1005
|
+
taskCount,
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function normalizeVerdict(verdict) {
|
|
1010
|
+
if (CANONICAL_VERDICTS.includes(verdict)) return { verdict, diagnostic: null };
|
|
1011
|
+
if (LEGACY_DIAGNOSTIC_VERDICTS.includes(verdict)) return { verdict: 'BLOCKED', diagnostic: verdict };
|
|
1012
|
+
throw new TypeError(`Unsupported canonical verdict: ${String(verdict)}`);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function unfinishedVerdictDecision(verdict, context = {}, diagnostic = null) {
|
|
1016
|
+
const action = diagnostic === 'PARTIAL'
|
|
1017
|
+
? 'continue-verification'
|
|
1018
|
+
: diagnostic === 'NO_TESTS'
|
|
1019
|
+
? 'configure-or-run-tests'
|
|
1020
|
+
: verdict === 'FAIL'
|
|
1021
|
+
? 'fix-and-rerun'
|
|
1022
|
+
: 'stop';
|
|
1023
|
+
return {
|
|
1024
|
+
verdict,
|
|
1025
|
+
...(diagnostic ? { diagnostic } : {}),
|
|
1026
|
+
action,
|
|
1027
|
+
completion: 'unfinished',
|
|
1028
|
+
terminal: verdict !== 'FAIL',
|
|
1029
|
+
unfinished: true,
|
|
1030
|
+
retry: verdict === 'FAIL',
|
|
1031
|
+
blocker: context.blocker || (
|
|
1032
|
+
diagnostic === 'PARTIAL'
|
|
1033
|
+
? 'verification is partial; required proof remains unfinished'
|
|
1034
|
+
: diagnostic === 'NO_TESTS'
|
|
1035
|
+
? 'no tests were executed'
|
|
1036
|
+
: verdict === 'FAIL'
|
|
1037
|
+
? 'verification returned FAIL'
|
|
1038
|
+
: 'verification is blocked; resolve the blocker before retrying'
|
|
1039
|
+
),
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function adaptVerdict(verdict, context = {}) {
|
|
1044
|
+
const normalized = normalizeVerdict(verdict);
|
|
1045
|
+
if (normalized.verdict === 'PASS' || normalized.verdict === 'PASS_WITH_WARNINGS') {
|
|
1046
|
+
return {
|
|
1047
|
+
verdict: normalized.verdict,
|
|
1048
|
+
...(normalized.diagnostic ? { diagnostic: normalized.diagnostic } : {}),
|
|
1049
|
+
action: normalized.verdict === 'PASS_WITH_WARNINGS' ? 'review-passed-with-warnings' : 'review-passed',
|
|
1050
|
+
completion: 'unfinished',
|
|
1051
|
+
terminal: true,
|
|
1052
|
+
unfinished: true,
|
|
1053
|
+
retry: false,
|
|
1054
|
+
blocker: context.blocker || 'completion decision requires canonical execution proof and workflow obligations',
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
return unfinishedVerdictDecision(normalized.verdict, context, normalized.diagnostic);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
function workflowPolicyObligations(context) {
|
|
1061
|
+
if (!context || typeof context !== 'object' || Array.isArray(context)) return null;
|
|
1062
|
+
const policy = hasOwn(context, 'workflow_policy')
|
|
1063
|
+
? context.workflow_policy
|
|
1064
|
+
: hasOwn(context, 'workflowPolicy')
|
|
1065
|
+
? context.workflowPolicy
|
|
1066
|
+
: hasOwn(context, 'policy')
|
|
1067
|
+
? context.policy
|
|
1068
|
+
: context.task_context && typeof context.task_context === 'object'
|
|
1069
|
+
? (hasOwn(context.task_context, 'workflow_policy')
|
|
1070
|
+
? context.task_context.workflow_policy
|
|
1071
|
+
: hasOwn(context.task_context, 'workflowPolicy')
|
|
1072
|
+
? context.task_context.workflowPolicy
|
|
1073
|
+
: null)
|
|
1074
|
+
: null;
|
|
1075
|
+
if (!validateWorkflowPolicySnapshot(policy).valid) return null;
|
|
1076
|
+
return [...policy.proof_obligations];
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function executionReceiptFromContext(context) {
|
|
1080
|
+
const candidates = [
|
|
1081
|
+
context?.execution_receipt,
|
|
1082
|
+
context?.executionReceipt,
|
|
1083
|
+
context?.canonical_execution_receipt,
|
|
1084
|
+
context?.canonicalExecutionReceipt,
|
|
1085
|
+
context?.execution?.receipt,
|
|
1086
|
+
context?.execution?.body,
|
|
1087
|
+
context?.receipt,
|
|
1088
|
+
];
|
|
1089
|
+
return candidates.find((value) => typeof value === 'string' && value.trim() !== '') || null;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function proofContainers(context) {
|
|
1093
|
+
return [
|
|
1094
|
+
context?.proofs,
|
|
1095
|
+
context?.workflow_proofs,
|
|
1096
|
+
context?.workflowProofs,
|
|
1097
|
+
context?.evidence,
|
|
1098
|
+
context?.obligation_evidence,
|
|
1099
|
+
context?.obligationEvidence,
|
|
1100
|
+
].filter((value) => value && typeof value === 'object' && !Array.isArray(value));
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function concreteEvidence(value) {
|
|
1104
|
+
if (typeof value === 'string') return value.trim() !== '' && !isPlaceholderToken(value);
|
|
1105
|
+
if (Array.isArray(value)) return value.some(concreteEvidence);
|
|
1106
|
+
if (!value || typeof value !== 'object') return false;
|
|
1107
|
+
return ['evidence', 'proof', 'receipt', 'report', 'summary']
|
|
1108
|
+
.some((key) => concreteEvidence(value[key]));
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
function evidenceForObligation(context, obligation) {
|
|
1112
|
+
for (const container of proofContainers(context)) {
|
|
1113
|
+
if (Object.prototype.hasOwnProperty.call(container, obligation) && concreteEvidence(container[obligation])) {
|
|
1114
|
+
return container[obligation];
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
const aliases = {
|
|
1118
|
+
needsInspection: ['inspection', 'inspectionEvidence', 'inspection_evidence'],
|
|
1119
|
+
needsDurableTaskState: ['durableTaskState', 'durable_task_state', 'taskState', 'task_state'],
|
|
1120
|
+
needsResearchGrounding: ['research', 'researchGrounding', 'research_grounding'],
|
|
1121
|
+
};
|
|
1122
|
+
for (const key of aliases[obligation] || []) {
|
|
1123
|
+
if (concreteEvidence(context?.[key])) return context[key];
|
|
330
1124
|
}
|
|
331
|
-
|
|
1125
|
+
return null;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
function independentAuditEvidence(context) {
|
|
1129
|
+
const runtimeContext = runtimeContextFromContext(context);
|
|
1130
|
+
const expectedProvenance = runtimeContext
|
|
1131
|
+
? { base: runtimeContext.base, head: runtimeContext.head }
|
|
1132
|
+
: null;
|
|
1133
|
+
const candidates = [
|
|
1134
|
+
...proofContainers(context).map((container) => container.needsIndependentAudit),
|
|
1135
|
+
context?.independentAudit,
|
|
1136
|
+
context?.independent_audit,
|
|
1137
|
+
context?.audit,
|
|
1138
|
+
];
|
|
1139
|
+
return candidates.find((value) => {
|
|
1140
|
+
if (Array.isArray(value)) return false;
|
|
1141
|
+
return validateIndependentAuditEvidence(value, expectedProvenance, runtimeContext).valid;
|
|
1142
|
+
}) || null;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
function validateIndependentAuditEvidence(value, expectedProvenance, runtimeContext) {
|
|
1146
|
+
const errors = [];
|
|
1147
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1148
|
+
return { valid: false, errors: ['audit must be an object'] };
|
|
1149
|
+
}
|
|
1150
|
+
if (JSON.stringify(Object.keys(value).sort()) !== JSON.stringify([...INDEPENDENT_AUDIT_FIELDS].sort())) {
|
|
1151
|
+
errors.push(`audit fields must be exactly ${INDEPENDENT_AUDIT_FIELDS.join(', ')}`);
|
|
1152
|
+
}
|
|
1153
|
+
if (value.schema_version !== INDEPENDENT_AUDIT_SCHEMA_VERSION) {
|
|
1154
|
+
errors.push(`audit.schema_version must be "${INDEPENDENT_AUDIT_SCHEMA_VERSION}"`);
|
|
1155
|
+
}
|
|
1156
|
+
for (const field of ['reviewer_session_id', 'implementation_session_id']) {
|
|
1157
|
+
if (typeof value[field] !== 'string' || isPlaceholderToken(value[field])) {
|
|
1158
|
+
errors.push(`audit.${field} must be concrete`);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
if (typeof value.reviewer_session_id === 'string'
|
|
1162
|
+
&& typeof value.implementation_session_id === 'string'
|
|
1163
|
+
&& value.reviewer_session_id.trim() === value.implementation_session_id.trim()) {
|
|
1164
|
+
errors.push('audit reviewer and implementation sessions must be distinct');
|
|
1165
|
+
}
|
|
1166
|
+
if (typeof value.evidence !== 'string' || isPlaceholderToken(value.evidence)) {
|
|
1167
|
+
errors.push('audit.evidence must be concrete');
|
|
1168
|
+
}
|
|
1169
|
+
if (value.verdict !== 'PASS') errors.push('audit.verdict must be literal PASS');
|
|
1170
|
+
|
|
1171
|
+
if (!PROVENANCE.isTrustedRuntimeContext(runtimeContext)) {
|
|
1172
|
+
errors.push('audit runtime provenance/session context is unavailable');
|
|
1173
|
+
} else if (typeof value.implementation_session_id === 'string'
|
|
1174
|
+
&& value.implementation_session_id.trim() !== runtimeContext.runtime_session) {
|
|
1175
|
+
errors.push('audit implementation session does not match the runtime session');
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
if (!value.expected_provenance
|
|
1179
|
+
|| typeof value.expected_provenance !== 'object'
|
|
1180
|
+
|| Array.isArray(value.expected_provenance)
|
|
1181
|
+
|| JSON.stringify(Object.keys(value.expected_provenance).sort()) !== JSON.stringify(['base', 'head'])) {
|
|
1182
|
+
errors.push('audit.expected_provenance must contain exactly base and head');
|
|
1183
|
+
}
|
|
1184
|
+
const actualBinding = expectedProvenanceFromSource(value.expected_provenance);
|
|
1185
|
+
const runtimeBinding = PROVENANCE.isTrustedRuntimeContext(runtimeContext)
|
|
1186
|
+
? { base: runtimeContext.base, head: runtimeContext.head }
|
|
1187
|
+
: null;
|
|
1188
|
+
if (!actualBinding
|
|
1189
|
+
|| !isConcreteProvenanceValue(actualBinding.base)
|
|
1190
|
+
|| !isConcreteProvenanceValue(actualBinding.head)
|
|
1191
|
+
|| !expectedProvenance
|
|
1192
|
+
|| !isConcreteProvenanceValue(expectedProvenance.base)
|
|
1193
|
+
|| !isConcreteProvenanceValue(expectedProvenance.head)
|
|
1194
|
+
|| normalizeProvenanceValue(actualBinding.base) !== normalizeProvenanceValue(expectedProvenance.base)
|
|
1195
|
+
|| normalizeProvenanceValue(actualBinding.head) !== normalizeProvenanceValue(expectedProvenance.head)) {
|
|
1196
|
+
errors.push('audit expected Base and Head binding does not match the runtime binding');
|
|
1197
|
+
}
|
|
1198
|
+
if (runtimeBinding
|
|
1199
|
+
&& (normalizeProvenanceValue(actualBinding?.base) !== runtimeBinding.base
|
|
1200
|
+
|| normalizeProvenanceValue(actualBinding?.head) !== runtimeBinding.head)) {
|
|
1201
|
+
errors.push('audit expected Base and Head do not match the freshly derived runtime binding');
|
|
1202
|
+
}
|
|
1203
|
+
return { valid: errors.length === 0, errors };
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
function completionReceiptOptions(context) {
|
|
1207
|
+
const task = context?.task_context || context?.taskContext || context?.task || context;
|
|
1208
|
+
const runtimeContext = runtimeContextFromContext(context);
|
|
1209
|
+
const derived = receiptValidatorOptions(task && typeof task === 'object' ? task : {}, {
|
|
1210
|
+
requireProvenanceBinding: true,
|
|
1211
|
+
runtimeContext,
|
|
1212
|
+
});
|
|
1213
|
+
const supplied = context?.receipt_options || context?.receiptOptions;
|
|
1214
|
+
if (!supplied || typeof supplied !== 'object' || Array.isArray(supplied)) return derived;
|
|
1215
|
+
return {
|
|
1216
|
+
...derived,
|
|
1217
|
+
...supplied,
|
|
1218
|
+
artifactDeclarationValid: derived.artifactDeclarationValid === false
|
|
1219
|
+
? false
|
|
1220
|
+
: supplied.artifactDeclarationValid !== false,
|
|
1221
|
+
requireArtifactHash: derived.requireArtifactHash === true || supplied.requireArtifactHash === true,
|
|
1222
|
+
artifactPaths: derived.artifactPaths.length > 0 ? derived.artifactPaths : (Array.isArray(supplied.artifactPaths) ? supplied.artifactPaths : []),
|
|
1223
|
+
artifactRoot: derived.artifactRoot,
|
|
1224
|
+
verifyArtifactBytes: derived.verifyArtifactBytes === true || supplied.verifyArtifactBytes === true,
|
|
1225
|
+
expectedProvenance: derived.expectedProvenance,
|
|
1226
|
+
requireProvenanceBinding: true,
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
function runtimeBoundProofsFromSpec(spec, runtimeContext) {
|
|
1231
|
+
// Spec files are worker-writable: plain proof strings are never execution
|
|
1232
|
+
// evidence. Only canonical receipts bound to this fresh runtime may enter
|
|
1233
|
+
// the completion decision; independent audit remains a strict object schema.
|
|
1234
|
+
const proofSources = [
|
|
1235
|
+
'completion_proofs',
|
|
1236
|
+
'completionProofs',
|
|
1237
|
+
'workflow_proofs',
|
|
1238
|
+
'workflowProofs',
|
|
1239
|
+
'obligation_evidence',
|
|
1240
|
+
'obligationEvidence',
|
|
1241
|
+
'proofs',
|
|
1242
|
+
];
|
|
1243
|
+
const sourceKey = proofSources.find((key) => spec && spec[key] && typeof spec[key] === 'object' && !Array.isArray(spec[key]));
|
|
1244
|
+
if (!sourceKey) return null;
|
|
1245
|
+
const source = spec[sourceKey];
|
|
1246
|
+
const trusted = {};
|
|
1247
|
+
const receiptOptions = receiptValidatorOptions({}, {
|
|
1248
|
+
runtimeContext,
|
|
1249
|
+
requireProvenanceBinding: true,
|
|
1250
|
+
});
|
|
1251
|
+
for (const [obligation, value] of Object.entries(source)) {
|
|
1252
|
+
if (obligation === 'needsIndependentAudit') {
|
|
1253
|
+
trusted[obligation] = value;
|
|
1254
|
+
continue;
|
|
1255
|
+
}
|
|
1256
|
+
const candidates = value && typeof value === 'object' && !Array.isArray(value)
|
|
1257
|
+
? [value.receipt, value.execution_receipt, value.executionReceipt]
|
|
1258
|
+
: [];
|
|
1259
|
+
const receipt = candidates.find((candidate) => (
|
|
1260
|
+
typeof candidate === 'string'
|
|
1261
|
+
&& validateCanonicalReceipt(candidate, receiptOptions).length === 0
|
|
1262
|
+
));
|
|
1263
|
+
if (receipt) trusted[obligation] = { receipt };
|
|
1264
|
+
}
|
|
1265
|
+
return trusted;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
function completionDecisionForSpec(spec, {
|
|
1269
|
+
runtimeContext,
|
|
1270
|
+
executionReceipt,
|
|
1271
|
+
taskContext,
|
|
1272
|
+
} = {}) {
|
|
1273
|
+
const policy = spec && typeof spec === 'object' ? spec.workflow_policy : null;
|
|
1274
|
+
const context = {
|
|
1275
|
+
workflow_policy: policy,
|
|
1276
|
+
runtime_context: runtimeContext,
|
|
1277
|
+
execution_receipt: executionReceipt,
|
|
1278
|
+
task_context: taskContext,
|
|
1279
|
+
};
|
|
1280
|
+
const trustedProofs = runtimeBoundProofsFromSpec(spec, runtimeContext);
|
|
1281
|
+
if (trustedProofs) context.proofs = trustedProofs;
|
|
1282
|
+
if (policy && policy.lane === 'Direct') {
|
|
332
1283
|
return {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
1284
|
+
completion: 'not_applicable',
|
|
1285
|
+
unfinished: false,
|
|
1286
|
+
missingProof: [],
|
|
1287
|
+
blocker: null,
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
return completionDecision('PASS', context);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
function completionDecision(verdict, context = {}) {
|
|
1294
|
+
if (!context || typeof context !== 'object' || Array.isArray(context)) context = {};
|
|
1295
|
+
const normalized = normalizeVerdict(verdict);
|
|
1296
|
+
if (normalized.verdict === 'PASS_WITH_WARNINGS') {
|
|
1297
|
+
return {
|
|
1298
|
+
verdict: normalized.verdict,
|
|
1299
|
+
action: 'review-passed-with-warnings',
|
|
1300
|
+
completion: 'unfinished',
|
|
1301
|
+
terminal: true,
|
|
1302
|
+
unfinished: true,
|
|
1303
|
+
retry: false,
|
|
1304
|
+
blocker: context.blocker || 'PASS_WITH_WARNINGS is not a completion verdict; literal PASS is required',
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
if (normalized.verdict !== 'PASS') {
|
|
1308
|
+
return unfinishedVerdictDecision(normalized.verdict, context, normalized.diagnostic);
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
const runtimeContext = runtimeContextFromContext(context);
|
|
1312
|
+
if (!runtimeContext) {
|
|
1313
|
+
return {
|
|
1314
|
+
verdict: normalized.verdict,
|
|
1315
|
+
action: 'blocked-runtime-provenance',
|
|
1316
|
+
completion: 'unfinished',
|
|
1317
|
+
terminal: true,
|
|
1318
|
+
unfinished: true,
|
|
1319
|
+
retry: false,
|
|
1320
|
+
blocker: 'completion blocked: runtime-derived provenance and session context are unavailable or stale',
|
|
1321
|
+
missingProof: ['runtime_provenance'],
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
const decisionContext = { ...context, runtime_context: runtimeContext };
|
|
1325
|
+
|
|
1326
|
+
const missing = [];
|
|
1327
|
+
const receipt = executionReceiptFromContext(decisionContext);
|
|
1328
|
+
const receiptFailures = receipt
|
|
1329
|
+
? validateCanonicalReceipt(receipt, completionReceiptOptions(decisionContext))
|
|
1330
|
+
: ['verification_state'];
|
|
1331
|
+
if (receiptFailures.length > 0) missing.push(`execution_receipt:${receiptFailures.join(',')}`);
|
|
1332
|
+
|
|
1333
|
+
const obligations = workflowPolicyObligations(decisionContext);
|
|
1334
|
+
if (!obligations) {
|
|
1335
|
+
missing.push('workflow_policy.proof_obligations');
|
|
1336
|
+
} else {
|
|
1337
|
+
for (const obligation of obligations) {
|
|
1338
|
+
if (obligation === 'needsExecutionProof') continue;
|
|
1339
|
+
if (obligation === 'needsIndependentAudit') {
|
|
1340
|
+
if (!independentAuditEvidence(decisionContext)) missing.push(obligation);
|
|
1341
|
+
continue;
|
|
1342
|
+
}
|
|
1343
|
+
if (!evidenceForObligation(decisionContext, obligation)) missing.push(obligation);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
if (missing.length > 0) {
|
|
1347
|
+
return {
|
|
1348
|
+
verdict: normalized.verdict,
|
|
1349
|
+
...(normalized.diagnostic ? { diagnostic: normalized.diagnostic } : {}),
|
|
1350
|
+
action: 'await-required-proof',
|
|
1351
|
+
completion: 'unfinished',
|
|
1352
|
+
terminal: true,
|
|
1353
|
+
unfinished: true,
|
|
1354
|
+
retry: false,
|
|
1355
|
+
blocker: decisionContext.blocker || `completion blocked: missing ${missing.join(', ')}`,
|
|
1356
|
+
missingProof: missing,
|
|
338
1357
|
};
|
|
339
1358
|
}
|
|
340
1359
|
return {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
1360
|
+
verdict: normalized.verdict,
|
|
1361
|
+
action: 'complete',
|
|
1362
|
+
completion: 'complete',
|
|
1363
|
+
terminal: true,
|
|
1364
|
+
unfinished: false,
|
|
1365
|
+
retry: false,
|
|
1366
|
+
blocker: null,
|
|
346
1367
|
};
|
|
347
1368
|
}
|
|
348
1369
|
|
|
349
|
-
function consumeReviewVerdict(verdict) {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
1370
|
+
function consumeReviewVerdict(verdict, context = {}) {
|
|
1371
|
+
const result = adaptVerdict(verdict, context);
|
|
1372
|
+
return {
|
|
1373
|
+
...result,
|
|
1374
|
+
source: 'review',
|
|
1375
|
+
completion: 'unfinished',
|
|
1376
|
+
unfinished: true,
|
|
1377
|
+
createsCompletion: false,
|
|
1378
|
+
completionDecisionRequired: true,
|
|
1379
|
+
action: result.verdict === 'PASS' || result.verdict === 'PASS_WITH_WARNINGS'
|
|
1380
|
+
? 'review-result-only'
|
|
1381
|
+
: result.action,
|
|
1382
|
+
blocker: result.verdict === 'PASS' || result.verdict === 'PASS_WITH_WARNINGS'
|
|
1383
|
+
? context.blocker || 'review verdict is not completion proof'
|
|
1384
|
+
: result.blocker,
|
|
1385
|
+
};
|
|
354
1386
|
}
|
|
355
1387
|
|
|
356
1388
|
function isFlashUnverified(task) {
|
|
357
1389
|
return task?.status === 'in_progress' && task.receipt === 'FLASH_UNVERIFIED';
|
|
358
1390
|
}
|
|
359
1391
|
|
|
1392
|
+
function isCanonicalFlashUnverified(task) {
|
|
1393
|
+
return isFlashUnverified(task)
|
|
1394
|
+
&& task.dependencyBlocked === true
|
|
1395
|
+
&& task.unblocks === false
|
|
1396
|
+
&& typeof task.blocker === 'string'
|
|
1397
|
+
&& !isPlaceholderToken(task.blocker)
|
|
1398
|
+
&& !FLASH_PROMOTION_FIELDS.some((field) => hasOwn(task, field));
|
|
1399
|
+
}
|
|
1400
|
+
|
|
360
1401
|
function isStaleFlashDone(task) {
|
|
361
1402
|
return task?.status === 'done' && task.receipt === 'FLASH_UNVERIFIED';
|
|
362
1403
|
}
|
|
363
1404
|
|
|
364
|
-
|
|
365
|
-
|
|
1405
|
+
const SYNC_FINALIZE_CAPABILITY = Symbol('sync-finalize-capability');
|
|
1406
|
+
const trustedFlashPromotions = new WeakMap();
|
|
1407
|
+
|
|
1408
|
+
function flashRuntimeContext(value) {
|
|
1409
|
+
if (!PROVENANCE.isTrustedRuntimeContext(value)) return null;
|
|
1410
|
+
try { return PROVENANCE.recomputeRuntimeContext(value); } catch (_) { return null; }
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
function evaluateFlashPromotion(task, verdict, proof = 'Verification: PASS', capability = null, runtimeContext) {
|
|
1414
|
+
const trustedRuntime = flashRuntimeContext(runtimeContext);
|
|
1415
|
+
if (!trustedRuntime) {
|
|
1416
|
+
return blockedFlashSync(task, 'flash promotion blocked: runtime-derived provenance and session context are unavailable or stale');
|
|
1417
|
+
}
|
|
1418
|
+
const normalized = normalizeVerdict(verdict);
|
|
366
1419
|
if (!isFlashUnverified(task)) return task;
|
|
367
1420
|
|
|
368
|
-
if (verdict !== 'PASS') {
|
|
1421
|
+
if (normalized.verdict !== 'PASS') {
|
|
369
1422
|
return {
|
|
370
1423
|
...task,
|
|
371
1424
|
status: 'in_progress',
|
|
372
1425
|
receipt: 'FLASH_UNVERIFIED',
|
|
373
|
-
blocker:
|
|
1426
|
+
blocker: normalized.diagnostic === 'NO_TESTS'
|
|
374
1427
|
? 'awaiting test proof from /hapo:test <feature>'
|
|
375
|
-
:
|
|
1428
|
+
: normalized.diagnostic === 'PARTIAL'
|
|
1429
|
+
? 'verification is partial; awaiting complete test proof'
|
|
1430
|
+
: `verification returned ${normalized.verdict}`,
|
|
376
1431
|
dependencyBlocked: true,
|
|
377
1432
|
unblocks: false,
|
|
378
1433
|
readyForSync: false,
|
|
1434
|
+
flashTransition: 'unverified',
|
|
1435
|
+
promotionReceipt: null,
|
|
379
1436
|
};
|
|
380
1437
|
}
|
|
381
1438
|
|
|
382
|
-
if (typeof proof !== 'string'
|
|
1439
|
+
if (typeof proof !== 'string') {
|
|
383
1440
|
throw new TypeError('PASS promotion requires a concrete Verification: PASS receipt');
|
|
384
1441
|
}
|
|
1442
|
+
const failures = validateCanonicalReceipt(proof, receiptValidatorOptions(task, {
|
|
1443
|
+
requireProvenanceBinding: true,
|
|
1444
|
+
runtimeContext: trustedRuntime,
|
|
1445
|
+
}));
|
|
1446
|
+
if (failures.length > 0) {
|
|
1447
|
+
return {
|
|
1448
|
+
...task,
|
|
1449
|
+
status: 'in_progress',
|
|
1450
|
+
receipt: 'FLASH_UNVERIFIED',
|
|
1451
|
+
blocker: `canonical receipt required for promotion: missing ${failures.join(', ')}`,
|
|
1452
|
+
dependencyBlocked: true,
|
|
1453
|
+
unblocks: false,
|
|
1454
|
+
readyForSync: false,
|
|
1455
|
+
flashTransition: 'unverified',
|
|
1456
|
+
promotionReceipt: null,
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
385
1459
|
|
|
386
|
-
|
|
1460
|
+
const promoted = {
|
|
387
1461
|
...task,
|
|
388
1462
|
status: 'in_progress',
|
|
389
1463
|
receipt: proof.trim(),
|
|
@@ -391,26 +1465,110 @@ function promoteFlashTask(task, verdict, proof = 'Verification: PASS') {
|
|
|
391
1465
|
dependencyBlocked: true,
|
|
392
1466
|
unblocks: false,
|
|
393
1467
|
readyForSync: true,
|
|
1468
|
+
flashTransition: 'promoted',
|
|
1469
|
+
promotionReceipt: proof.trim(),
|
|
394
1470
|
};
|
|
1471
|
+
if (capability === SYNC_FINALIZE_CAPABILITY) trustedFlashPromotions.set(promoted, capability);
|
|
1472
|
+
return promoted;
|
|
395
1473
|
}
|
|
396
1474
|
|
|
397
|
-
function
|
|
398
|
-
|
|
1475
|
+
function promoteFlashTask(task, verdict, proof = 'Verification: PASS', runtimeContext) {
|
|
1476
|
+
const candidate = runtimeContext
|
|
1477
|
+
|| task?.runtime_context
|
|
1478
|
+
|| task?.runtimeContext
|
|
1479
|
+
|| task?.receipt_binding?.runtimeContext
|
|
1480
|
+
|| task?.receiptBinding?.runtimeContext;
|
|
1481
|
+
return evaluateFlashPromotion(task, verdict, proof, null, candidate);
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
function finalizeFlashTaskInternal(task, operation, capability, runtimeContext) {
|
|
1485
|
+
if (operation !== 'sync-finalize' || capability !== SYNC_FINALIZE_CAPABILITY || trustedFlashPromotions.get(task) !== capability) {
|
|
1486
|
+
return blockedFlashSync(task, 'sync-finalize blocked: only trusted same-operation promotion may finalize');
|
|
1487
|
+
}
|
|
399
1488
|
if (
|
|
400
1489
|
task?.status !== 'in_progress'
|
|
401
|
-
|| !/^Verification:\s*PASS\b/.test(String(task.receipt || '').trim())
|
|
402
1490
|
|| task.readyForSync !== true
|
|
403
|
-
) return task;
|
|
1491
|
+
) return blockedFlashSync(task, 'sync-finalize blocked: trusted promotion state is incomplete');
|
|
1492
|
+
const receipt = String(task.receipt || '');
|
|
1493
|
+
if (task.flashTransition !== 'promoted' || task.promotionReceipt !== receipt) {
|
|
1494
|
+
return blockedFlashSync(task, 'sync-finalize blocked: trusted promotion state is incomplete');
|
|
1495
|
+
}
|
|
1496
|
+
const failures = validateCanonicalReceipt(receipt, receiptValidatorOptions(task, {
|
|
1497
|
+
requireProvenanceBinding: true,
|
|
1498
|
+
runtimeContext,
|
|
1499
|
+
}));
|
|
1500
|
+
if (failures.length > 0) {
|
|
1501
|
+
return blockedFlashSync(task, `sync-finalize blocked: canonical receipt required (${failures.join(', ')})`);
|
|
1502
|
+
}
|
|
404
1503
|
|
|
1504
|
+
trustedFlashPromotions.delete(task);
|
|
405
1505
|
return {
|
|
406
1506
|
...task,
|
|
407
1507
|
status: 'done',
|
|
408
1508
|
dependencyBlocked: false,
|
|
409
1509
|
unblocks: true,
|
|
410
1510
|
readyForSync: false,
|
|
1511
|
+
flashTransition: 'finalized',
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
function finalizeFlashTask(task, operation) {
|
|
1516
|
+
if (operation !== 'sync-finalize') return task;
|
|
1517
|
+
return blockedFlashSync(task, 'sync-finalize blocked: only syncFinalizeFlashTask owns finalization');
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
function blockedFlashSync(task, blocker) {
|
|
1521
|
+
return {
|
|
1522
|
+
...task,
|
|
1523
|
+
status: 'in_progress',
|
|
1524
|
+
receipt: 'FLASH_UNVERIFIED',
|
|
1525
|
+
dependencyBlocked: true,
|
|
1526
|
+
unblocks: false,
|
|
1527
|
+
readyForSync: false,
|
|
1528
|
+
flashTransition: 'unverified',
|
|
1529
|
+
promotionReceipt: null,
|
|
1530
|
+
blocker,
|
|
411
1531
|
};
|
|
412
1532
|
}
|
|
413
1533
|
|
|
1534
|
+
function syncFinalizeFlashTask(task, verdict, proof, runtimeContext) {
|
|
1535
|
+
if (!isCanonicalFlashUnverified(task)) {
|
|
1536
|
+
return blockedFlashSync(
|
|
1537
|
+
task,
|
|
1538
|
+
'sync-finalize blocked: current task must be FLASH_UNVERIFIED in the exact canonical stored state; caller promotion state is not trusted',
|
|
1539
|
+
);
|
|
1540
|
+
}
|
|
1541
|
+
if (verdict !== 'PASS') {
|
|
1542
|
+
return blockedFlashSync(task, 'sync-finalize blocked: explicit PASS verdict is required');
|
|
1543
|
+
}
|
|
1544
|
+
if (typeof proof !== 'string' || proof.trim() === '') {
|
|
1545
|
+
return blockedFlashSync(task, 'sync-finalize blocked: explicit canonical --proof is required');
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
// Treat all promotion fields from --task-json as untrusted. Rebuild the
|
|
1549
|
+
// unverified state, then derive promotion and finalization in this operation.
|
|
1550
|
+
const current = {
|
|
1551
|
+
...task,
|
|
1552
|
+
readyForSync: false,
|
|
1553
|
+
flashTransition: 'unverified',
|
|
1554
|
+
promotionReceipt: null,
|
|
1555
|
+
dependencyBlocked: true,
|
|
1556
|
+
unblocks: false,
|
|
1557
|
+
};
|
|
1558
|
+
const candidate = runtimeContext
|
|
1559
|
+
|| task?.runtime_context
|
|
1560
|
+
|| task?.runtimeContext
|
|
1561
|
+
|| task?.receipt_binding?.runtimeContext
|
|
1562
|
+
|| task?.receiptBinding?.runtimeContext;
|
|
1563
|
+
const trustedRuntime = flashRuntimeContext(candidate);
|
|
1564
|
+
if (!trustedRuntime) {
|
|
1565
|
+
return blockedFlashSync(task, 'sync-finalize blocked: runtime-derived provenance and session context are unavailable or stale');
|
|
1566
|
+
}
|
|
1567
|
+
const promoted = evaluateFlashPromotion(current, 'PASS', proof, SYNC_FINALIZE_CAPABILITY, trustedRuntime);
|
|
1568
|
+
if (promoted.readyForSync !== true) return promoted;
|
|
1569
|
+
return finalizeFlashTaskInternal(promoted, 'sync-finalize', SYNC_FINALIZE_CAPABILITY, trustedRuntime);
|
|
1570
|
+
}
|
|
1571
|
+
|
|
414
1572
|
function flashState(taskRegistry = {}) {
|
|
415
1573
|
return Object.entries(taskRegistry)
|
|
416
1574
|
.filter(([, task]) => isFlashUnverified(task))
|
|
@@ -420,38 +1578,157 @@ function flashState(taskRegistry = {}) {
|
|
|
420
1578
|
function validateCanonicalReceipt(body, options = {}) {
|
|
421
1579
|
if (typeof body !== 'string') return ['verification_state'];
|
|
422
1580
|
const failures = [];
|
|
1581
|
+
const addFailure = (failure) => {
|
|
1582
|
+
if (!failures.includes(failure)) failures.push(failure);
|
|
1583
|
+
};
|
|
1584
|
+
// Fail-closed on placeholders anywhere — provenance, command, or artifact must not be templated
|
|
1585
|
+
if (/\{\{[^}]+\}\}/.test(body)) {
|
|
1586
|
+
addFailure('placeholder');
|
|
1587
|
+
// placeholder also implies missing concrete provenance/command; keep explicit failures for mapping
|
|
1588
|
+
}
|
|
1589
|
+
// Explicit failure outcome anywhere in body must make canonical validator fail (structured only)
|
|
1590
|
+
if (hasExplicitFailure(body)) {
|
|
1591
|
+
addFailure('verification_state');
|
|
1592
|
+
}
|
|
423
1593
|
// Unambiguous verification state: must be Verification: PASS exactly
|
|
424
1594
|
if (!/^\s*Verification:\s*PASS\s*$/m.test(body)) {
|
|
425
|
-
|
|
426
|
-
}
|
|
427
|
-
// Command must be present
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
1595
|
+
addFailure('verification_state');
|
|
1596
|
+
}
|
|
1597
|
+
// Command must be present with non-empty concrete value on same line, not placeholder
|
|
1598
|
+
const cmdLine = body.split('\n').find((l) => /^\s*Command(?:\(s\))?\s*:/m.test(l)) || null;
|
|
1599
|
+
if (!cmdLine || !/^\s*Command(?:\(s\))?\s*:[ \t]*\S/m.test(cmdLine)) {
|
|
1600
|
+
addFailure('command');
|
|
1601
|
+
} else {
|
|
1602
|
+
const m = cmdLine.match(/^\s*Command(?:\(s\))?\s*:[ \t]*(.*)$/m);
|
|
1603
|
+
const val = m ? m[1].trim() : '';
|
|
1604
|
+
if (isPlaceholderToken(val)) {
|
|
1605
|
+
addFailure('command');
|
|
1606
|
+
} else if (/\{\{[^}]+\}\}/.test(cmdLine)) {
|
|
1607
|
+
addFailure('command');
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
// Exit / Result handling: collect all Result lines; if no Exit then need at least one Result and all PASS; if has Exit then every Exit integer 0 and every Result if any all PASS
|
|
1611
|
+
const lines = body.split('\n');
|
|
1612
|
+
const exitValues = [];
|
|
1613
|
+
const resultValues = [];
|
|
1614
|
+
for (const line of lines) {
|
|
1615
|
+
if (/^\s*Exit\s*:/i.test(line)) {
|
|
1616
|
+
const m = line.match(/^\s*Exit\s*:\s*(.*)$/i);
|
|
1617
|
+
exitValues.push(m ? m[1].trim() : '');
|
|
1618
|
+
} else if (/exit\s+code\s*[:=]/i.test(line)) {
|
|
1619
|
+
const m = line.match(/exit\s+code\s*[:=]\s*(.*)$/i);
|
|
1620
|
+
if (m) exitValues.push(m[1].trim());
|
|
1621
|
+
}
|
|
1622
|
+
if (/^\s*Result\s*:/i.test(line)) {
|
|
1623
|
+
const m = line.match(/^\s*Result\s*:\s*(.+?)\s*$/i);
|
|
1624
|
+
resultValues.push(m ? m[1].trim() : '');
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
if (exitValues.length === 0) {
|
|
1629
|
+
if (resultValues.length === 0 || resultValues.some((v) => v !== 'PASS')) {
|
|
1630
|
+
addFailure('exit_result');
|
|
1631
|
+
}
|
|
1632
|
+
} else {
|
|
1633
|
+
let exitOk = true;
|
|
1634
|
+
for (const v of exitValues) {
|
|
1635
|
+
if (!/^-?\d+$/.test(v)) { exitOk = false; break; }
|
|
1636
|
+
const n = Number.parseInt(v, 10);
|
|
1637
|
+
if (n !== 0) { exitOk = false; break; }
|
|
1638
|
+
}
|
|
1639
|
+
if (!exitOk) addFailure('exit_result');
|
|
1640
|
+
if (resultValues.length > 0 && resultValues.some((v) => v !== 'PASS')) {
|
|
1641
|
+
addFailure('exit_result');
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
// Provenance is either a concrete Base/Head pair or a concrete base_sha/head_sha pair.
|
|
1645
|
+
// Without expected values this validates schema only; it does not claim identity binding.
|
|
1646
|
+
const receiptLines = body.split('\n');
|
|
1647
|
+
const readField = (name) => receiptLines
|
|
1648
|
+
.filter((line) => new RegExp(`^\\s*${name}\\s*:`, 'i').test(line))
|
|
1649
|
+
.map((line) => line.replace(new RegExp(`^\\s*${name}\\s*:\\s*`, 'i'), '').trim());
|
|
1650
|
+
const baseValues = readField('Base');
|
|
1651
|
+
const headValues = readField('Head');
|
|
1652
|
+
const baseShaValues = readField('base_sha');
|
|
1653
|
+
const headShaValues = readField('head_sha');
|
|
1654
|
+
const labelStyle = baseValues.length > 0 || headValues.length > 0;
|
|
1655
|
+
const shaStyle = baseShaValues.length > 0 || headShaValues.length > 0;
|
|
1656
|
+
let actualProvenance = null;
|
|
1657
|
+
if (labelStyle === shaStyle || labelStyle && (baseShaValues.length > 0 || headShaValues.length > 0)) {
|
|
1658
|
+
addFailure('provenance');
|
|
1659
|
+
} else {
|
|
1660
|
+
const base = labelStyle ? baseValues : baseShaValues;
|
|
1661
|
+
const head = labelStyle ? headValues : headShaValues;
|
|
1662
|
+
if (base.length !== 1 || head.length !== 1 || !isConcreteProvenanceValue(base[0]) || !isConcreteProvenanceValue(head[0])) {
|
|
1663
|
+
addFailure('provenance');
|
|
1664
|
+
} else {
|
|
1665
|
+
actualProvenance = { base: normalizeProvenanceValue(base[0]), head: normalizeProvenanceValue(head[0]) };
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
const expected = options.expectedProvenance || options.expected_provenance || options.expected || null;
|
|
1669
|
+
const expectedBase = expected && typeof expected === 'object'
|
|
1670
|
+
? firstDefined(expected, ['base', 'Base', 'base_sha', 'baseSha', 'expectedBase', 'expected_base'])
|
|
1671
|
+
: options.expectedBase ?? options.expected_base ?? options.expectedBaseSha ?? options.expected_base_sha;
|
|
1672
|
+
const expectedHead = expected && typeof expected === 'object'
|
|
1673
|
+
? firstDefined(expected, ['head', 'Head', 'head_sha', 'headSha', 'expectedHead', 'expected_head'])
|
|
1674
|
+
: options.expectedHead ?? options.expected_head ?? options.expectedHeadSha ?? options.expected_head_sha;
|
|
1675
|
+
if (options.requireProvenanceBinding === true || expectedBase !== undefined || expectedHead !== undefined) {
|
|
1676
|
+
if (!actualProvenance
|
|
1677
|
+
|| !isConcreteProvenanceValue(expectedBase)
|
|
1678
|
+
|| !isConcreteProvenanceValue(expectedHead)
|
|
1679
|
+
|| normalizeProvenanceValue(expectedBase) !== actualProvenance.base
|
|
1680
|
+
|| normalizeProvenanceValue(expectedHead) !== actualProvenance.head) {
|
|
1681
|
+
addFailure('provenance');
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
if (options.artifactDeclarationValid === false) addFailure('artifact_declaration');
|
|
1686
|
+
const artifactDeclarations = parseArtifactDeclarations(body);
|
|
1687
|
+
for (const declaration of artifactDeclarations) {
|
|
1688
|
+
if (declaration.hashes.length === 0 || declaration.hashes.some((hash) => !/^[a-f0-9]{64}$/i.test(hash))) {
|
|
1689
|
+
addFailure('artifact_hash');
|
|
1690
|
+
}
|
|
1691
|
+
const declaredPaths = declaration.pathText
|
|
1692
|
+
.split(/\s*(?:\+|,|;|\band\b)\s*/i)
|
|
1693
|
+
.map((value) => value.trim())
|
|
1694
|
+
.filter(Boolean);
|
|
1695
|
+
if (declaredPaths.length > 1 && declaration.hashes.length !== declaredPaths.length) {
|
|
1696
|
+
addFailure('artifact_hash');
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
const artifactPaths = Array.isArray(options.artifactPaths) ? options.artifactPaths : [];
|
|
1700
|
+
if (options.requireArtifactHash) {
|
|
1701
|
+
if (artifactDeclarations.length === 0) addFailure('artifact_hash');
|
|
1702
|
+
for (const artifactPath of artifactPaths) {
|
|
1703
|
+
const matches = artifactDeclarations.filter((declaration) => {
|
|
1704
|
+
const candidates = declaration.pathText.split(/\s*(?:\+|,|;|\band\b)\s*/i).map((value) => value.trim()).filter(Boolean);
|
|
1705
|
+
return candidates.length === 1 && candidates.includes(artifactPath);
|
|
1706
|
+
});
|
|
1707
|
+
if (matches.length !== 1 || matches[0].hashes.length !== 1 || !/^[a-f0-9]{64}$/i.test(matches[0].hashes[0])) {
|
|
1708
|
+
addFailure('artifact_hash');
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
if (options.verifyArtifactBytes === true || typeof options.artifactRoot === 'string') {
|
|
1713
|
+
if (typeof options.artifactRoot !== 'string' || options.artifactRoot.trim() === '') {
|
|
1714
|
+
if (artifactDeclarations.length > 0 || options.requireArtifactHash) addFailure('artifact_hash');
|
|
1715
|
+
} else {
|
|
1716
|
+
for (const declaration of artifactDeclarations) {
|
|
1717
|
+
const declaredPaths = declaration.pathText
|
|
1718
|
+
.split(/\s*(?:\+|,|;|\band\b)\s*/i)
|
|
1719
|
+
.map((value) => value.trim())
|
|
1720
|
+
.filter(Boolean);
|
|
1721
|
+
if (declaredPaths.length === 0 || declaration.hashes.length !== declaredPaths.length) {
|
|
1722
|
+
addFailure('artifact_hash');
|
|
1723
|
+
continue;
|
|
1724
|
+
}
|
|
1725
|
+
for (let index = 0; index < declaredPaths.length; index += 1) {
|
|
1726
|
+
if (!hashValidatedArtifact(options.artifactRoot, declaredPaths[index], declaration.hashes[index])) {
|
|
1727
|
+
addFailure('artifact_hash');
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
455
1732
|
}
|
|
456
1733
|
return failures;
|
|
457
1734
|
}
|
|
@@ -466,9 +1743,16 @@ function parseCliArgs(argv) {
|
|
|
466
1743
|
action: null,
|
|
467
1744
|
lane: null,
|
|
468
1745
|
override: null,
|
|
469
|
-
proof:
|
|
1746
|
+
proof: null,
|
|
470
1747
|
userAuthorized: false,
|
|
471
1748
|
origin: null,
|
|
1749
|
+
projectRoot: null,
|
|
1750
|
+
specsRoot: null,
|
|
1751
|
+
specFile: null,
|
|
1752
|
+
featureName: null,
|
|
1753
|
+
runtimeSession: null,
|
|
1754
|
+
provenanceMode: null,
|
|
1755
|
+
runtimeContextInput: undefined,
|
|
472
1756
|
};
|
|
473
1757
|
const args = [...argv];
|
|
474
1758
|
for (let i = 0; i < args.length; i += 1) {
|
|
@@ -483,6 +1767,9 @@ function parseCliArgs(argv) {
|
|
|
483
1767
|
else if (arg === '--sync-finalize') options.action = 'sync-finalize';
|
|
484
1768
|
else if (arg === '--classify-lane') options.action = 'classify-lane';
|
|
485
1769
|
else if (arg === '--lane-policy') options.action = 'lane-policy';
|
|
1770
|
+
else if (arg === '--policy-snapshot') options.action = 'policy-snapshot';
|
|
1771
|
+
else if (arg === '--validate-policy') options.action = 'validate-policy';
|
|
1772
|
+
else if (arg === '--completion-decision') options.action = 'completion-decision';
|
|
486
1773
|
else if (arg === '--approval-state') options.action = 'approval-state';
|
|
487
1774
|
else if (arg === '--validate-approval') options.action = 'validate-approval';
|
|
488
1775
|
else if (arg === '--validate-receipt') options.action = 'validate-receipt';
|
|
@@ -498,6 +1785,16 @@ function parseCliArgs(argv) {
|
|
|
498
1785
|
} else if (arg === '--proof') {
|
|
499
1786
|
options.proof = args[++i];
|
|
500
1787
|
if (!options.proof) throw new Error('--proof requires a value');
|
|
1788
|
+
} else if (arg === '--project-root') options.projectRoot = args[++i];
|
|
1789
|
+
else if (arg === '--specs-root') options.specsRoot = args[++i];
|
|
1790
|
+
else if (arg === '--spec-file') options.specFile = args[++i];
|
|
1791
|
+
else if (arg === '--feature-name') options.featureName = args[++i];
|
|
1792
|
+
else if (arg === '--runtime-session' || arg === '--session') options.runtimeSession = args[++i];
|
|
1793
|
+
else if (arg === '--provenance-mode') options.provenanceMode = args[++i];
|
|
1794
|
+
else if (arg === '--runtime-context') {
|
|
1795
|
+
const raw = args[++i];
|
|
1796
|
+
if (!raw) throw new Error('--runtime-context requires a JSON value');
|
|
1797
|
+
options.runtimeContextInput = JSON.parse(raw);
|
|
501
1798
|
} else {
|
|
502
1799
|
throw new Error(`Unknown option: ${arg}`);
|
|
503
1800
|
}
|
|
@@ -505,6 +1802,23 @@ function parseCliArgs(argv) {
|
|
|
505
1802
|
return options;
|
|
506
1803
|
}
|
|
507
1804
|
|
|
1805
|
+
function cliRuntimeContext(options) {
|
|
1806
|
+
if (options.runtimeContextInput !== undefined) {
|
|
1807
|
+
throw new Error('--runtime-context JSON is not trusted; provide runtime identity flags');
|
|
1808
|
+
}
|
|
1809
|
+
const supplied = [options.projectRoot, options.specsRoot, options.specFile, options.featureName, options.runtimeSession]
|
|
1810
|
+
.some((value) => value !== null && value !== undefined);
|
|
1811
|
+
if (!supplied) return null;
|
|
1812
|
+
return PROVENANCE.deriveRuntimeContext({
|
|
1813
|
+
projectRoot: options.projectRoot,
|
|
1814
|
+
specsRoot: options.specsRoot,
|
|
1815
|
+
specFile: options.specFile,
|
|
1816
|
+
featureName: options.featureName,
|
|
1817
|
+
runtimeSession: options.runtimeSession,
|
|
1818
|
+
provenanceMode: options.provenanceMode || undefined,
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1821
|
+
|
|
508
1822
|
function cliResult(result, json) {
|
|
509
1823
|
process.stdout.write(json ? `${JSON.stringify(result)}\n` : `${result.message}\n`);
|
|
510
1824
|
return result.exitCode;
|
|
@@ -541,6 +1855,17 @@ function runCli(argv = process.argv.slice(2)) {
|
|
|
541
1855
|
const policy = lanePolicy(input);
|
|
542
1856
|
return cliResult({ ok: true, policy, exitCode: 0, message: `Lane: ${policy.lane}` }, options.json);
|
|
543
1857
|
}
|
|
1858
|
+
if (options.action === 'policy-snapshot') {
|
|
1859
|
+
if (!options.task) throw new Error('--policy-snapshot requires --task-json');
|
|
1860
|
+
const snapshot = workflowPolicySnapshot(options.task);
|
|
1861
|
+
return cliResult({ ok: true, snapshot, exitCode: 0, message: `Policy snapshot: ${snapshot.lane}` }, options.json);
|
|
1862
|
+
}
|
|
1863
|
+
if (options.action === 'validate-policy') {
|
|
1864
|
+
if (!options.task) throw new Error('--validate-policy requires --task-json');
|
|
1865
|
+
const snapshot = hasOwn(options.task, 'workflow_policy') ? options.task.workflow_policy : options.task;
|
|
1866
|
+
const result = validateWorkflowPolicySnapshot(snapshot);
|
|
1867
|
+
return cliResult({ ok: result.valid, ...result, exitCode: result.valid ? 0 : 2, message: result.valid ? 'Workflow policy snapshot valid' : result.errors.join('; ') }, options.json);
|
|
1868
|
+
}
|
|
544
1869
|
if (options.action === 'approval-state') {
|
|
545
1870
|
if (!options.task) throw new Error('--approval-state requires --task-json');
|
|
546
1871
|
const state = approvalState(options.task);
|
|
@@ -554,22 +1879,30 @@ function runCli(argv = process.argv.slice(2)) {
|
|
|
554
1879
|
if (options.action === 'validate-receipt') {
|
|
555
1880
|
if (!options.task) throw new Error('--validate-receipt requires --task-json');
|
|
556
1881
|
const body = options.task.body || options.task.receipt || '';
|
|
557
|
-
const failures = validateCanonicalReceipt(body);
|
|
1882
|
+
const failures = validateCanonicalReceipt(body, receiptValidatorOptions(options.task, { runtimeContext: cliRuntimeContext(options) }));
|
|
558
1883
|
return cliResult({ ok: failures.length === 0, failures, exitCode: failures.length === 0 ? 0 : 2, message: failures.length === 0 ? 'Receipt valid' : `Receipt missing: ${failures.join(', ')}` }, options.json);
|
|
559
1884
|
}
|
|
560
1885
|
if (options.action === 'consume-verdict') {
|
|
561
|
-
const result = consumeReviewVerdict(options.verdict);
|
|
1886
|
+
const result = consumeReviewVerdict(options.verdict, options.task || {});
|
|
562
1887
|
return cliResult({ ok: true, ...result, exitCode: 0, message: `Verdict ${options.verdict}: ${result.action}` }, options.json);
|
|
563
1888
|
}
|
|
1889
|
+
if (options.action === 'completion-decision') {
|
|
1890
|
+
const runtimeContext = cliRuntimeContext(options);
|
|
1891
|
+
const result = completionDecision(options.verdict, runtimeContext
|
|
1892
|
+
? { ...(options.task || {}), runtime_context: runtimeContext }
|
|
1893
|
+
: (options.task || {}));
|
|
1894
|
+
return cliResult({ ok: true, ...result, exitCode: 0, message: `Verdict ${options.verdict}: ${result.completion}` }, options.json);
|
|
1895
|
+
}
|
|
564
1896
|
if (options.action === 'promote-flash') {
|
|
565
1897
|
if (!options.task) throw new Error('--promote-flash requires --task-json');
|
|
566
|
-
const result = promoteFlashTask(options.task, options.verdict, options.proof);
|
|
1898
|
+
const result = promoteFlashTask(options.task, options.verdict, options.proof, cliRuntimeContext(options));
|
|
567
1899
|
return cliResult({ ok: true, task: result, exitCode: 0, message: 'Flash task promotion evaluated.' }, options.json);
|
|
568
1900
|
}
|
|
569
1901
|
if (options.action === 'sync-finalize') {
|
|
570
1902
|
if (!options.task) throw new Error('--sync-finalize requires --task-json');
|
|
571
|
-
const result =
|
|
572
|
-
|
|
1903
|
+
const result = syncFinalizeFlashTask(options.task, options.verdict, options.proof, cliRuntimeContext(options));
|
|
1904
|
+
const ok = result.status === 'done';
|
|
1905
|
+
return cliResult({ ok, task: result, exitCode: ok ? 0 : 2, message: ok ? 'Flash task sync-finalize evaluated.' : (result.blocker || 'Flash task sync-finalize blocked.') }, options.json);
|
|
573
1906
|
}
|
|
574
1907
|
|
|
575
1908
|
return cliResult({ ok: true, contract: 'execution-policy', ...policy, exitCode: 0, message: `Execution mode: ${policy.mode}` }, options.json);
|
|
@@ -583,26 +1916,60 @@ if (require.main === module) process.exitCode = runCli();
|
|
|
583
1916
|
|
|
584
1917
|
module.exports = {
|
|
585
1918
|
REVIEW_VERDICTS,
|
|
1919
|
+
CANONICAL_VERDICTS,
|
|
1920
|
+
LEGACY_DIAGNOSTIC_VERDICTS,
|
|
586
1921
|
EXECUTION_TIERS,
|
|
587
1922
|
LANES,
|
|
1923
|
+
ARTIFACT_PROFILES,
|
|
1924
|
+
PROOF_OBLIGATIONS,
|
|
1925
|
+
WORKFLOW_POLICY_VERSION,
|
|
1926
|
+
WORKFLOW_POLICY_FIELDS,
|
|
1927
|
+
RECEIPT_BINDING_FIELDS,
|
|
1928
|
+
INDEPENDENT_AUDIT_SCHEMA_VERSION,
|
|
1929
|
+
INDEPENDENT_AUDIT_FIELDS,
|
|
588
1930
|
APPROVAL_SCHEMA_VERSION,
|
|
589
1931
|
LEGACY_APPROVAL_ERROR,
|
|
590
|
-
DEEP_TASK_SEQUENCE,
|
|
591
|
-
CRITICAL_LANE_SEQUENCE,
|
|
592
1932
|
executionPolicy,
|
|
593
1933
|
classifyLane,
|
|
1934
|
+
obligationsForLane,
|
|
1935
|
+
actorNeedsFor,
|
|
1936
|
+
isWorkflowPolicySnapshot,
|
|
1937
|
+
validateWorkflowPolicySnapshot,
|
|
1938
|
+
assertWorkflowPolicySnapshot,
|
|
1939
|
+
workflowPolicySnapshot,
|
|
1940
|
+
createWorkflowPolicySnapshot,
|
|
1941
|
+
persistWorkflowPolicySnapshot,
|
|
1942
|
+
persistWorkflowPolicy,
|
|
1943
|
+
readWorkflowPolicySnapshot,
|
|
1944
|
+
escalateWorkflowPolicy,
|
|
1945
|
+
policyForSpec,
|
|
594
1946
|
lanePolicy,
|
|
595
1947
|
approvalState,
|
|
596
1948
|
validateApprovalSchema,
|
|
597
1949
|
isLegacyApprovalObject,
|
|
598
1950
|
isUserAuthorizedForDowngrade,
|
|
1951
|
+
isValidOverrideReceipt,
|
|
1952
|
+
isTapMetadataHeading,
|
|
1953
|
+
deriveRuntimeContext: PROVENANCE.deriveRuntimeContext,
|
|
1954
|
+
recomputeRuntimeContext: PROVENANCE.recomputeRuntimeContext,
|
|
1955
|
+
isTrustedRuntimeContext: PROVENANCE.isTrustedRuntimeContext,
|
|
1956
|
+
createReceiptBinding,
|
|
1957
|
+
receiptValidatorOptions,
|
|
599
1958
|
validateCanonicalReceipt,
|
|
1959
|
+
validateIndependentAuditEvidence,
|
|
1960
|
+
completionDecisionForSpec,
|
|
600
1961
|
delegationPlan,
|
|
1962
|
+
assertVerdict,
|
|
1963
|
+
normalizeVerdict,
|
|
1964
|
+
adaptVerdict,
|
|
1965
|
+
completionDecision,
|
|
601
1966
|
consumeReviewVerdict,
|
|
602
1967
|
isFlashUnverified,
|
|
1968
|
+
isCanonicalFlashUnverified,
|
|
603
1969
|
isStaleFlashDone,
|
|
604
1970
|
promoteFlashTask,
|
|
605
1971
|
finalizeFlashTask,
|
|
1972
|
+
syncFinalizeFlashTask,
|
|
606
1973
|
flashState,
|
|
607
1974
|
parseCliArgs,
|
|
608
1975
|
runCli,
|