@haposoft/cafekit 0.16.0-rc.2 → 0.16.0-rc.4
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 +794 -21
- package/bin/__tests__/codex-native.test.js +228 -7
- package/bin/__tests__/develop-contract.test.js +1838 -112
- package/bin/__tests__/installer-safety.test.js +82 -29
- package/bin/__tests__/opencode-spec-gate.test.js +799 -0
- package/bin/__tests__/package-inventory.test.js +841 -10
- package/bin/__tests__/specs-v2-execution-closeout.test.js +421 -0
- package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +601 -0
- package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +952 -0
- package/bin/__tests__/test-runtime-dependency-closure.cjs +182 -0
- package/bin/__tests__/validator-grounding.test.js +2469 -71
- package/bin/lib/managed-writer.js +12 -3
- package/bin/phases/codex-runtime.js +17 -1
- package/package.json +1 -1
- package/src/claude/CLAUDE.md +6 -0
- package/src/claude/agents/code-auditor.md +20 -0
- package/src/claude/agents/spec-maker.md +153 -202
- package/src/claude/hooks/__tests__/completion-authority.test.js +953 -0
- package/src/claude/hooks/__tests__/privacy-block.test.js +172 -0
- package/src/claude/hooks/__tests__/semantic-review-authority.test.js +257 -0
- package/src/claude/hooks/__tests__/spec-gate.test.js +604 -56
- package/src/claude/hooks/completion-authority-check.cjs +56 -0
- package/src/claude/hooks/completion-authority-state.cjs +527 -0
- package/src/claude/hooks/completion-authority.cjs +154 -0
- package/src/claude/hooks/docs-sync.cjs +5 -2
- package/src/claude/hooks/lib/privacy-command-analysis.cjs +393 -0
- package/src/claude/hooks/lib/runtime-path-safety.cjs +119 -0
- package/src/claude/hooks/privacy-block.cjs +77 -30
- package/src/claude/hooks/semantic-review-authority.cjs +152 -0
- package/src/claude/hooks/session.cjs +33 -19
- package/src/claude/hooks/spec-gate.cjs +181 -139
- package/src/claude/hooks/spec-state.cjs +112 -47
- package/src/claude/migration-manifest.json +15 -2
- package/src/claude/rules/state-sync.md +52 -21
- package/src/claude/rules/workflow.md +17 -9
- package/src/claude/scripts/provenance.cjs +315 -0
- package/src/claude/scripts/spec-final-state.cjs +244 -0
- package/src/claude/scripts/spec-ground.cjs +535 -173
- package/src/claude/scripts/spec-readiness.cjs +118 -0
- package/src/claude/scripts/spec-receipt.cjs +162 -0
- package/src/claude/scripts/spec-resolver.cjs +501 -0
- package/src/claude/scripts/spec-scaffold.cjs +1945 -55
- package/src/claude/scripts/spec-semantic-model.cjs +396 -0
- package/src/claude/scripts/validate-spec-output.cjs +3204 -262
- package/src/claude/scripts/workflow-policy.cjs +1888 -203
- package/src/claude/settings/settings.json +12 -0
- package/src/claude/skills/code-review/SKILL.md +94 -66
- package/src/claude/skills/code-review/references/spec-compliance-review.md +10 -2
- package/src/claude/skills/code-review/references/verification-gate.md +13 -0
- package/src/claude/skills/develop/SKILL.md +138 -283
- package/src/claude/skills/develop/references/parallel-waves.md +9 -6
- package/src/claude/skills/develop/references/quality-gate.md +123 -148
- package/src/claude/skills/develop/references/subagent-patterns.md +42 -37
- package/src/claude/skills/specs/SKILL.md +192 -397
- package/src/claude/skills/specs/references/ask-user-question-gates.md +27 -18
- package/src/claude/skills/specs/references/cross-spec-dependency.md +8 -4
- package/src/claude/skills/specs/references/research-strategy.md +4 -1
- package/src/claude/skills/specs/references/review.md +200 -260
- package/src/claude/skills/specs/references/scope-inquiry.md +18 -5
- package/src/claude/skills/specs/rules/design-discovery-full.md +33 -81
- package/src/claude/skills/specs/rules/design-discovery-light.md +21 -42
- package/src/claude/skills/specs/rules/design-principles.md +136 -180
- package/src/claude/skills/specs/rules/design-review.md +42 -97
- package/src/claude/skills/specs/rules/ears-format.md +42 -50
- package/src/claude/skills/specs/rules/phase-decision-matrix.md +42 -41
- package/src/claude/skills/specs/rules/task-scoring-rubric.md +46 -46
- package/src/claude/skills/specs/rules/tasks-generation.md +189 -271
- package/src/claude/skills/specs/templates/design.md +49 -263
- package/src/claude/skills/specs/templates/requirements.md +26 -36
- package/src/claude/skills/specs/templates/research.md +9 -98
- package/src/claude/skills/specs/templates/spec-state.json +26 -67
- package/src/claude/skills/specs/templates/task.md +30 -82
- package/src/claude/skills/sync/SKILL.md +45 -15
- package/src/claude/skills/sync/references/sync-protocols.md +34 -16
- package/src/claude/skills/test/SKILL.md +151 -167
- package/src/codex/AGENTS.md +6 -0
- package/src/codex/hooks/completion-authority-check.cjs +63 -0
- package/src/codex/hooks/completion-authority-state.cjs +527 -0
- package/src/codex/hooks/completion-authority.cjs +106 -0
- package/src/codex/hooks/lib/hook-context.cjs +58 -4
- package/src/codex/hooks/lib/privacy-command-analysis.cjs +3 -0
- package/src/codex/hooks/lib/privacy-state.cjs +38 -16
- package/src/codex/hooks/lib/runtime-path-safety.cjs +3 -0
- package/src/codex/hooks/lib/spec-receipt.cjs +86 -115
- package/src/codex/hooks/lib/spec-utils.cjs +104 -27
- package/src/codex/hooks/privacy-approval.cjs +1 -1
- package/src/codex/hooks/privacy-block.cjs +67 -11
- package/src/codex/hooks/semantic-review-authority.cjs +130 -0
- package/src/codex/hooks/session.cjs +2 -0
- package/src/codex/hooks/spec-gate.cjs +151 -24
- package/src/codex/hooks/spec-state.cjs +65 -12
- package/src/codex/hooks/state.cjs +2 -0
- package/src/codex/hooks.json +16 -1
- package/src/codex/rules/state-sync.md +47 -27
- package/src/common/AGENTS.md +1 -1
- 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,22 +1,90 @@
|
|
|
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
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
const PLANNING_DEPTHS = Object.freeze(['None', 'Compact', 'Full']);
|
|
14
|
+
const ASSURANCE_LEVELS = Object.freeze(['Routine', 'Elevated', 'Strict']);
|
|
15
|
+
const ARTIFACT_PROFILES = Object.freeze(['targeted', 'bounded', 'strict']);
|
|
16
|
+
const PLANNING_OBLIGATIONS = Object.freeze([
|
|
17
|
+
'needsRequirements',
|
|
18
|
+
'needsDesign',
|
|
19
|
+
]);
|
|
20
|
+
const PROOF_OBLIGATIONS = Object.freeze([
|
|
21
|
+
'needsInspection',
|
|
22
|
+
'needsExecutionProof',
|
|
23
|
+
'needsIndependentAudit',
|
|
24
|
+
'needsDurableTaskState',
|
|
25
|
+
]);
|
|
26
|
+
const READ_COMPAT_PROOF_OBLIGATIONS = Object.freeze([
|
|
27
|
+
...PROOF_OBLIGATIONS,
|
|
28
|
+
'needsResearchGrounding',
|
|
29
|
+
]);
|
|
30
|
+
const WORKFLOW_POLICY_VERSION = '2';
|
|
31
|
+
const CANONICAL_WORKFLOW_POLICY_VERSION = '2.1';
|
|
32
|
+
const CANONICAL_WORKFLOW_POLICY_FIELDS = Object.freeze([
|
|
33
|
+
'version',
|
|
34
|
+
'planning_depth',
|
|
35
|
+
'assurance_level',
|
|
36
|
+
'classified_minimum',
|
|
37
|
+
'risks',
|
|
38
|
+
]);
|
|
39
|
+
const WORKFLOW_POLICY_FIELDS = Object.freeze([
|
|
40
|
+
'version',
|
|
41
|
+
'planning_depth',
|
|
42
|
+
'automatic_planning_depth',
|
|
43
|
+
'assurance_level',
|
|
44
|
+
'automatic_assurance_level',
|
|
45
|
+
'lane',
|
|
46
|
+
'automatic_lane',
|
|
47
|
+
'risks',
|
|
48
|
+
'artifact_profile',
|
|
49
|
+
'planning_obligations',
|
|
50
|
+
'proof_obligations',
|
|
51
|
+
'actor_needs',
|
|
52
|
+
'override_receipt',
|
|
53
|
+
]);
|
|
54
|
+
const RECEIPT_BINDING_FIELDS = Object.freeze(['expectedProvenance', 'requireProvenanceBinding']);
|
|
55
|
+
const INDEPENDENT_AUDIT_SCHEMA_VERSION = '1';
|
|
56
|
+
const INDEPENDENT_AUDIT_FIELDS = Object.freeze([
|
|
57
|
+
'schema_version',
|
|
58
|
+
'reviewer_session_id',
|
|
59
|
+
'implementation_session_id',
|
|
60
|
+
'expected_provenance',
|
|
61
|
+
'evidence',
|
|
62
|
+
'verdict',
|
|
63
|
+
]);
|
|
64
|
+
const FLASH_PROMOTION_FIELDS = Object.freeze(['readyForSync', 'flashTransition', 'promotionReceipt']);
|
|
65
|
+
const LEGACY_TIER_LANES = Object.freeze({ Light: 'Direct', Standard: 'Standard', Deep: 'Critical' });
|
|
66
|
+
const V1_POLICY_FIELDS = Object.freeze([
|
|
67
|
+
'version', 'lane', 'automatic_lane', 'risks', 'artifact_profile',
|
|
68
|
+
'proof_obligations', 'actor_needs', 'override_receipt',
|
|
69
|
+
]);
|
|
70
|
+
const ARTIFACT_PROFILE_BY_LANE = Object.freeze({ Direct: 'targeted', Standard: 'bounded', Critical: 'strict' });
|
|
71
|
+
const ARTIFACT_PROFILE_BY_PLANNING = Object.freeze({ None: 'targeted', Compact: 'bounded', Full: 'strict' });
|
|
72
|
+
const ACTOR_NEEDS_BY_OBLIGATION = Object.freeze({
|
|
73
|
+
needsInspection: Object.freeze({ capability: 'inspection', independence: 'same-session' }),
|
|
74
|
+
needsExecutionProof: Object.freeze({ capability: 'execution-proof', independence: 'same-session' }),
|
|
75
|
+
needsIndependentAudit: Object.freeze({ capability: 'audit', independence: 'independent' }),
|
|
76
|
+
needsDurableTaskState: Object.freeze({ capability: 'durable-task-state', independence: 'same-session' }),
|
|
77
|
+
needsResearchGrounding: Object.freeze({ capability: 'research-grounding', independence: 'same-session' }),
|
|
12
78
|
});
|
|
79
|
+
const ACTOR_CAPABILITIES = new Set(Object.values(ACTOR_NEEDS_BY_OBLIGATION).map((need) => need.capability));
|
|
80
|
+
const ACTOR_INDEPENDENCE = new Set(['same-session', 'independent']);
|
|
13
81
|
const LANE_RISK_KEYS = Object.freeze({
|
|
14
82
|
reversibility: ['reversible', 'reversibility', 'irreversible', 'non-reversible'],
|
|
15
83
|
destructive: ['destructive', 'deletion', 'delete', 'destroy'],
|
|
16
84
|
auth: ['auth', 'authentication', 'authorization'],
|
|
17
85
|
payment: ['payment', 'billing', 'charge'],
|
|
18
86
|
privacy: ['privacy', 'pii', 'personal data'],
|
|
19
|
-
data: ['data', '
|
|
87
|
+
data: ['data loss', 'loss of data', 'data corruption', 'corrupt data', 'data integrity'],
|
|
20
88
|
schema: ['schema'],
|
|
21
89
|
migration: ['migration', 'migrate', 'database migration'],
|
|
22
90
|
publicContract: ['publicContract', 'public_contract', 'public contract', 'api contract', 'breaking change', 'backward compatibility'],
|
|
@@ -25,8 +93,351 @@ const LANE_RISK_KEYS = Object.freeze({
|
|
|
25
93
|
rollback: ['rollback', 'rollback difficulty', 'hard to rollback', 'cannot rollback', 'no rollback'],
|
|
26
94
|
});
|
|
27
95
|
|
|
96
|
+
function hasOwn(value, key) {
|
|
97
|
+
return value !== null
|
|
98
|
+
&& (typeof value === 'object' || typeof value === 'function')
|
|
99
|
+
&& Object.prototype.hasOwnProperty.call(value, key);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const PLACEHOLDER_TOKENS = new Set(['TBD','TODO','N/A','NA','NONE','UNKNOWN','PENDING','PLACEHOLDER','REPLACE_ME','-','?']);
|
|
103
|
+
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;
|
|
104
|
+
const ARTIFACT_DECLARATION_KEY = 'artifacts';
|
|
105
|
+
const TASK_ARTIFACT_KEYS = Object.freeze(['artifact', 'artifact_ref', 'artifact_path']);
|
|
106
|
+
const PROVENANCE_VALUE_RE = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/i;
|
|
107
|
+
const PROSE_RECEIPT_FIELD_RE = /^\s*(?:Notes?|Description|Command(?:\(s\))?)\s*:/i;
|
|
108
|
+
function isPlaceholderToken(value) {
|
|
109
|
+
if (typeof value !== 'string') return true;
|
|
110
|
+
const v = value.trim();
|
|
111
|
+
if (v === '') return true;
|
|
112
|
+
if (/^\{\{.*\}\}$/.test(v)) return true;
|
|
113
|
+
if (/^<.*>$/.test(v)) return true;
|
|
114
|
+
const upper = v.toUpperCase();
|
|
115
|
+
if (PLACEHOLDER_TOKENS.has(upper)) return true;
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
function isTapMetadataHeading(line) {
|
|
119
|
+
return typeof line === 'string' && TAP_METADATA_HEADING_RE.test(line);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function hasConcreteArtifactDeclaration(value) {
|
|
123
|
+
if (typeof value === 'string') return !isPlaceholderToken(value);
|
|
124
|
+
if (Array.isArray(value)) return value.some(hasConcreteArtifactDeclaration);
|
|
125
|
+
return Boolean(value && typeof value === 'object');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isSafeArtifactPath(value) {
|
|
129
|
+
if (typeof value !== 'string' || value !== value.trim() || isPlaceholderToken(value)) return false;
|
|
130
|
+
if (/^(?:[a-z]:[\\/]|[\\/]|https?:\/\/)/i.test(value)) return false;
|
|
131
|
+
return !value.split(/[\\/]+/).includes('..');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function isPathInside(root, target) {
|
|
135
|
+
const relative = path.relative(path.resolve(root), path.resolve(target));
|
|
136
|
+
return relative !== '' && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function stableStat(stat) {
|
|
140
|
+
return [stat.dev, stat.ino, stat.size, stat.mtimeNs?.toString(), stat.ctimeNs?.toString()].join(':');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function hashValidatedArtifact(root, relativePath, expectedHash) {
|
|
144
|
+
if (!isSafeArtifactPath(relativePath) || typeof root !== 'string' || root.trim() === '') return false;
|
|
145
|
+
const rootPath = path.resolve(root);
|
|
146
|
+
const target = path.resolve(rootPath, relativePath);
|
|
147
|
+
if (!isPathInside(rootPath, target)) return false;
|
|
148
|
+
|
|
149
|
+
const segments = path.relative(rootPath, target).split(path.sep).filter(Boolean);
|
|
150
|
+
let current = rootPath;
|
|
151
|
+
try {
|
|
152
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
153
|
+
current = path.join(current, segments[index]);
|
|
154
|
+
const stat = fs.lstatSync(current);
|
|
155
|
+
if (stat.isSymbolicLink()) return false;
|
|
156
|
+
if (index < segments.length - 1 && !stat.isDirectory()) return false;
|
|
157
|
+
}
|
|
158
|
+
const rootReal = fs.realpathSync(rootPath);
|
|
159
|
+
const targetReal = fs.realpathSync(target);
|
|
160
|
+
if (!isPathInside(rootReal, targetReal)) return false;
|
|
161
|
+
const before = fs.statSync(target);
|
|
162
|
+
if (!before.isFile()) return false;
|
|
163
|
+
const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0);
|
|
164
|
+
const fd = fs.openSync(target, flags);
|
|
165
|
+
try {
|
|
166
|
+
const opened = fs.fstatSync(fd);
|
|
167
|
+
if (!opened.isFile() || stableStat(opened) !== stableStat(before)) return false;
|
|
168
|
+
const bytes = fs.readFileSync(fd);
|
|
169
|
+
const after = fs.fstatSync(fd);
|
|
170
|
+
if (stableStat(after) !== stableStat(opened)) return false;
|
|
171
|
+
const actual = crypto.createHash('sha256').update(bytes).digest('hex');
|
|
172
|
+
return actual.toLowerCase() === String(expectedHash).toLowerCase();
|
|
173
|
+
} finally {
|
|
174
|
+
fs.closeSync(fd);
|
|
175
|
+
}
|
|
176
|
+
} catch (_) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function artifactDeclaration(task = {}) {
|
|
182
|
+
if (Object.prototype.hasOwnProperty.call(task, ARTIFACT_DECLARATION_KEY)) {
|
|
183
|
+
const artifacts = task[ARTIFACT_DECLARATION_KEY];
|
|
184
|
+
const unique = Array.isArray(artifacts) ? new Set(artifacts).size === artifacts.length : false;
|
|
185
|
+
return {
|
|
186
|
+
declared: true,
|
|
187
|
+
valid: Array.isArray(artifacts)
|
|
188
|
+
&& artifacts.length > 0
|
|
189
|
+
&& unique
|
|
190
|
+
&& artifacts.every(isSafeArtifactPath),
|
|
191
|
+
paths: Array.isArray(artifacts) ? [...artifacts] : [],
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Read legacy aliases only for compatibility with pre-P0 task registries.
|
|
196
|
+
const legacyKey = TASK_ARTIFACT_KEYS.find((key) => hasConcreteArtifactDeclaration(task?.[key]));
|
|
197
|
+
const legacyValue = legacyKey ? task[legacyKey] : null;
|
|
198
|
+
const legacyPaths = typeof legacyValue === 'string'
|
|
199
|
+
? [legacyValue]
|
|
200
|
+
: Array.isArray(legacyValue) && legacyValue.every((value) => typeof value === 'string')
|
|
201
|
+
? [...legacyValue]
|
|
202
|
+
: [];
|
|
203
|
+
return {
|
|
204
|
+
declared: Boolean(legacyKey),
|
|
205
|
+
valid: !legacyKey || (legacyPaths.length > 0 && legacyPaths.every(isSafeArtifactPath)),
|
|
206
|
+
legacy: true,
|
|
207
|
+
paths: legacyPaths,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function normalizeProvenanceValue(value) {
|
|
212
|
+
if (typeof value !== 'string') return null;
|
|
213
|
+
const normalized = value.trim().replace(/^sha256:/i, '');
|
|
214
|
+
return normalized || null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function isConcreteProvenanceValue(value) {
|
|
218
|
+
return PROVENANCE_VALUE_RE.test(normalizeProvenanceValue(value) || '');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function runtimeContextFromContext(context) {
|
|
222
|
+
const candidates = [
|
|
223
|
+
context?.runtime_context,
|
|
224
|
+
context?.runtimeContext,
|
|
225
|
+
context?.provenance_context,
|
|
226
|
+
context?.provenanceContext,
|
|
227
|
+
context?.receipt_binding?.runtimeContext,
|
|
228
|
+
context?.receiptBinding?.runtimeContext,
|
|
229
|
+
context?.provenance_binding?.runtimeContext,
|
|
230
|
+
context?.provenanceBinding?.runtimeContext,
|
|
231
|
+
];
|
|
232
|
+
const candidate = candidates.find((value) => PROVENANCE.isTrustedRuntimeContext(value));
|
|
233
|
+
if (!candidate) return null;
|
|
234
|
+
try { return PROVENANCE.recomputeRuntimeContext(candidate); } catch (_) { return null; }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function firstDefined(source, keys) {
|
|
238
|
+
if (!source || typeof source !== 'object') return undefined;
|
|
239
|
+
for (const key of keys) {
|
|
240
|
+
if (Object.prototype.hasOwnProperty.call(source, key)) return source[key];
|
|
241
|
+
}
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function expectedProvenanceFromSource(source) {
|
|
246
|
+
if (!source || typeof source !== 'object' || Array.isArray(source)) return null;
|
|
247
|
+
const nested = firstDefined(source, [
|
|
248
|
+
'expectedProvenance',
|
|
249
|
+
'expected_provenance',
|
|
250
|
+
]);
|
|
251
|
+
const candidate = nested && typeof nested === 'object' && !Array.isArray(nested)
|
|
252
|
+
? nested
|
|
253
|
+
: source;
|
|
254
|
+
const base = firstDefined(candidate, [
|
|
255
|
+
'base', 'Base', 'base_sha', 'baseSha', 'expectedBase', 'expected_base',
|
|
256
|
+
'expectedBaseSha', 'expected_base_sha',
|
|
257
|
+
]);
|
|
258
|
+
const head = firstDefined(candidate, [
|
|
259
|
+
'head', 'Head', 'head_sha', 'headSha', 'expectedHead', 'expected_head',
|
|
260
|
+
'expectedHeadSha', 'expected_head_sha',
|
|
261
|
+
]);
|
|
262
|
+
if (base === undefined && head === undefined) return null;
|
|
263
|
+
return { base, head };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function createReceiptBinding(runtimeContext) {
|
|
267
|
+
if (!PROVENANCE.isTrustedRuntimeContext(runtimeContext)) {
|
|
268
|
+
throw new TypeError('Receipt binding requires a runtime-derived provenance context');
|
|
269
|
+
}
|
|
270
|
+
try { return PROVENANCE.createReceiptBinding(runtimeContext); } catch (error) {
|
|
271
|
+
throw new TypeError(error.message);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function receiptValidatorOptions(task = {}, {
|
|
276
|
+
requireProvenanceBinding = false,
|
|
277
|
+
requireExplicitBinding = false,
|
|
278
|
+
runtimeContext,
|
|
279
|
+
} = {}) {
|
|
280
|
+
const declaration = artifactDeclaration(task);
|
|
281
|
+
let trusted = null;
|
|
282
|
+
if (runtimeContext !== undefined && PROVENANCE.isTrustedRuntimeContext(runtimeContext)) {
|
|
283
|
+
try { trusted = PROVENANCE.recomputeRuntimeContext(runtimeContext); } catch (_) { trusted = null; }
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
requireArtifactHash: declaration.declared,
|
|
287
|
+
artifactDeclarationValid: declaration.valid,
|
|
288
|
+
artifactPaths: declaration.paths,
|
|
289
|
+
artifactRoot: trusted?.project_root || null,
|
|
290
|
+
verifyArtifactBytes: Boolean(trusted?.project_root),
|
|
291
|
+
expectedProvenance: trusted ? { base: trusted.base, head: trusted.head } : null,
|
|
292
|
+
requireProvenanceBinding: requireProvenanceBinding || (runtimeContext !== undefined && runtimeContext !== null) || requireExplicitBinding,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function sha256ValuesFromLine(line) {
|
|
297
|
+
const values = [];
|
|
298
|
+
const labels = [...line.matchAll(/\b(?:artifact[_-])?sha-?256\s*:/gi)];
|
|
299
|
+
for (const label of labels) {
|
|
300
|
+
const remainder = line.slice(label.index + label[0].length);
|
|
301
|
+
const token = remainder.match(/^\s*([^\s)\],;`]+)/);
|
|
302
|
+
values.push(token ? token[1].trim() : '');
|
|
303
|
+
}
|
|
304
|
+
return values;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function artifactPathText(line) {
|
|
308
|
+
return line
|
|
309
|
+
.replace(/^\s*Artifacts?\s*:\s*/i, '')
|
|
310
|
+
.replace(/^\s*Artifact\s+produced\s*:?\s*/i, '')
|
|
311
|
+
.replace(/\b(?:artifact[_-])?sha-?256\s*:\s*[^\s)\],;`]+/gi, '')
|
|
312
|
+
.replace(/[()[\]{}]/g, ' ')
|
|
313
|
+
.replace(/\s+/g, ' ')
|
|
314
|
+
.trim()
|
|
315
|
+
.replace(/(?:\s*[+,;]\s*)+$/, '')
|
|
316
|
+
.trim();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function parseArtifactDeclarations(body) {
|
|
320
|
+
const declarations = [];
|
|
321
|
+
let previous = null;
|
|
322
|
+
for (const raw of body.split('\n')) {
|
|
323
|
+
if (PROSE_RECEIPT_FIELD_RE.test(raw)) {
|
|
324
|
+
previous = null;
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
const isDeclaration = /^\s*Artifacts?\s*:/i.test(raw)
|
|
328
|
+
|| /^\s*Artifact\s+produced\b/i.test(raw)
|
|
329
|
+
|| /^\s*artifact[_-]?sha-?256\s*:/i.test(raw);
|
|
330
|
+
if (isDeclaration) {
|
|
331
|
+
previous = {
|
|
332
|
+
raw,
|
|
333
|
+
pathText: artifactPathText(raw),
|
|
334
|
+
hashes: sha256ValuesFromLine(raw),
|
|
335
|
+
};
|
|
336
|
+
declarations.push(previous);
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (previous && /^\s*sha-?256\s*:/i.test(raw)) {
|
|
340
|
+
previous.hashes.push(...sha256ValuesFromLine(raw));
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (raw.trim() !== '') previous = null;
|
|
344
|
+
}
|
|
345
|
+
return declarations;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function artifactSha256Values(body) {
|
|
349
|
+
return parseArtifactDeclarations(body).flatMap((declaration) => declaration.hashes);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function hasCanonicalArtifactHash(body) {
|
|
353
|
+
const hashes = artifactSha256Values(body);
|
|
354
|
+
return hashes.length > 0 && hashes.every((value) => /^[a-f0-9]{64}$/i.test(value));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function hasConcreteArtifactHash(body) {
|
|
358
|
+
const hashes = artifactSha256Values(body);
|
|
359
|
+
return hashes.length > 0 && hashes.every((value) => value !== '' && !isPlaceholderToken(value));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function hasExplicitFailure(body) {
|
|
363
|
+
if (typeof body !== 'string') return false;
|
|
364
|
+
const lines = body.split('\n');
|
|
365
|
+
for (const raw of lines) {
|
|
366
|
+
if (PROSE_RECEIPT_FIELD_RE.test(raw)) continue;
|
|
367
|
+
const trimmed = raw.trim();
|
|
368
|
+
if (trimmed === '') continue;
|
|
369
|
+
// 1. Zero execution (structured, anchored): zero tests, zero passing, or no collection.
|
|
370
|
+
if (/^\s*(?:#|ℹ)?\s*tests?\s+0\b/i.test(raw)) return true;
|
|
371
|
+
if (/^\s*(?:#|ℹ)?\s*0\s+tests?\b/i.test(raw)) return true;
|
|
372
|
+
if (/^\s*(?:#|ℹ)?\s*tests?\s+run\s*:\s*0\b/i.test(raw)) return true;
|
|
373
|
+
if (/^\s*0\s+(?:passing|passed)\b/i.test(raw)) return true;
|
|
374
|
+
if (/^\s*Tests?\s*:\s*0\s+(?:passing|passed)\b/i.test(raw)) return true;
|
|
375
|
+
if (/^\s*Tests?\s+passed\s*:\s*0\b/i.test(raw)) return true;
|
|
376
|
+
if (/^\s*Passed\s*:\s*0\b/i.test(raw)) return true;
|
|
377
|
+
if (/^\s*Tests:\s*0\s+total\b/i.test(raw)) return true;
|
|
378
|
+
if (/^\s*collected\s+0\s+items\b/i.test(raw)) return true;
|
|
379
|
+
if (/^\s*No tests? found\b/i.test(raw)) return true;
|
|
380
|
+
// 2. Cancellation (optional marker #/ℹ, cancelled/canceled, both orders, count >0; 0 passes)
|
|
381
|
+
if (/^\s*(?:#|ℹ)?\s*cancel(?:l)?ed\s*[:\s]*[1-9]\d*\b/i.test(raw)) return true;
|
|
382
|
+
if (/^\s*(?:#|ℹ)?\s*[1-9]\d*\s+cancel(?:l)?ed\b/i.test(raw)) return true;
|
|
383
|
+
// 3. Suite / file failures: Test Suites: N failed, Test Files N failed (N>0)
|
|
384
|
+
if (/^\s*Test Suites?:?\s*[1-9]\d*\s+failed\b/i.test(raw)) return true;
|
|
385
|
+
if (/^\s*Test Files?:?\s*[1-9]\d*\s+failed\b/i.test(raw)) return true;
|
|
386
|
+
if (/^\s*Test Suites?:?\s*0\s+failed\b[^\r\n]*\b0\s+passed\b/i.test(raw)) return true;
|
|
387
|
+
if (/^\s*Test Files?:?\s*0\s+failed\b[^\r\n]*\b0\s+passed\b/i.test(raw)) return true;
|
|
388
|
+
// 4. Pytest short-summary entries (anchored to a Python test path)
|
|
389
|
+
if (/^\s*FAILED[ \t]+\S+\.py(?:::\S+)?(?:[ \t]+-[^\r\n]*)?\s*$/i.test(raw)) return true;
|
|
390
|
+
// 5. Runner FAIL (anchored exact FAIL, horizontal whitespace/colon/end; --- FAIL:)
|
|
391
|
+
if (/^\s*FAIL(?:[ \t]+|:|$)/.test(raw)) return true;
|
|
392
|
+
if (/^\s*---\s*FAIL:/.test(raw)) return true;
|
|
393
|
+
// 6. Error/failure summaries (anchored, N>0; prose Notes/Description/Command skipped)
|
|
394
|
+
if (/^\s*[1-9]\d*\s+errors?\b/i.test(raw)) return true;
|
|
395
|
+
if (/^\s*errors?\s*[:\s]*[1-9]\d*\b/i.test(raw)) return true;
|
|
396
|
+
if (/^\s*failures?\s*[:\s]*[1-9]\d*\b/i.test(raw)) return true;
|
|
397
|
+
if (/^\s*failure\s+summary\s*[:.]?\s*$/i.test(raw)) return true;
|
|
398
|
+
if (/^\s*Found\s+[1-9]\d*\s+errors?\b/i.test(raw)) return true;
|
|
399
|
+
if (/^\s*failureCount\s*:\s*[1-9]\d*\s*[,;]?\s*$/i.test(raw)) return true;
|
|
400
|
+
if (/^\s*ERROR\s*$/i.test(raw)) return true;
|
|
401
|
+
if (/^\s*ERROR\s+collecting\b/i.test(raw)) return true;
|
|
402
|
+
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;
|
|
403
|
+
// Maven Surefire's structured failure lines; do not treat arbitrary [ERROR] prose as failure.
|
|
404
|
+
if (/^\s*\[ERROR\]\s+Tests run:\s*\d+\b[^\r\n]*\b(?:Failures?|Errors?):\s*[1-9]\d*\b/i.test(raw)) return true;
|
|
405
|
+
if (/^\s*\[ERROR\]\s+Failed to execute goal\b/i.test(raw)) return true;
|
|
406
|
+
// 7. Gradle (anchored, optional)
|
|
407
|
+
if (/^\s*\d+\s+tests?\s+completed,\s*[1-9]\d*\s+failed\b/i.test(raw)) return true;
|
|
408
|
+
// Existing structured checks (preserve, anchored where needed to avoid prose false positive)
|
|
409
|
+
if (/^\s*Tests?\s+failed\s*$/i.test(raw)) return true;
|
|
410
|
+
if (/^\s*Tests?\s+failed\s*:\s*[1-9]\d*\b/i.test(raw)) return true;
|
|
411
|
+
if (/\b(?:Verification|Result|Status|Outcome)\s*:\s*FAIL(?:ED|URE)?\b/i.test(raw)) return true;
|
|
412
|
+
if (/^\s*FAIL\s*$/i.test(trimmed)) return true;
|
|
413
|
+
if (/^\s*FAIL\s*:/i.test(trimmed)) return true;
|
|
414
|
+
if (/^\s*FAILED\s*$/i.test(trimmed)) return true;
|
|
415
|
+
if (/^\s*FAILED\s*:/i.test(trimmed)) return true;
|
|
416
|
+
if (/^\s*FAILURE\s*$/i.test(trimmed)) return true;
|
|
417
|
+
if (/^\s*FAILURE\s*:/i.test(trimmed)) return true;
|
|
418
|
+
if (/^\s*one\s+failing(?:\s+(?:test|tests|suite|suites))?\s*[,:]?\s*$/i.test(raw)) return true;
|
|
419
|
+
if (/^\s*(?:#|ℹ)?\s*fail\s+[1-9]/i.test(raw)) return true;
|
|
420
|
+
if (/^\s*not ok\b/i.test(raw)) return true;
|
|
421
|
+
if (/^\s*Tests\s*:?\s*[1-9]/i.test(raw) && /\bfailed\b/i.test(raw)) return true;
|
|
422
|
+
if (/^\s*\d+\s+failed\b/i.test(raw)) {
|
|
423
|
+
const m = raw.match(/^\s*(\d+)\s+failed\b/i);
|
|
424
|
+
if (m && parseInt(m[1], 10) !== 0) return true;
|
|
425
|
+
}
|
|
426
|
+
if (/exit\s+code\s*[:=]?\s*[1-9]/i.test(raw)) return true;
|
|
427
|
+
}
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
const trustedClassifications = new WeakSet();
|
|
431
|
+
const trustedV1Adapters = new WeakSet();
|
|
432
|
+
|
|
433
|
+
function clonePolicySnapshot(snapshot) {
|
|
434
|
+
const clone = JSON.parse(JSON.stringify(snapshot));
|
|
435
|
+
if (trustedV1Adapters.has(snapshot)) trustedV1Adapters.add(clone);
|
|
436
|
+
return clone;
|
|
437
|
+
}
|
|
438
|
+
|
|
28
439
|
const APPROVAL_SCHEMA_VERSION = '2.0';
|
|
29
|
-
const LEGACY_APPROVAL_ERROR = `Legacy approval state detected (field \`approved\`). Migration required: replace \`approved\` with \`agent_validated\`
|
|
440
|
+
const LEGACY_APPROVAL_ERROR = `Legacy approval state detected (field \`approved\`). Migration required: replace \`approved\` with \`agent_validated\` per schema v${APPROVAL_SCHEMA_VERSION} (schema_version: "${APPROVAL_SCHEMA_VERSION}"). See spec-state.json template.`;
|
|
30
441
|
|
|
31
442
|
function assertLane(lane) {
|
|
32
443
|
if (!LANES.includes(lane)) {
|
|
@@ -35,6 +446,35 @@ function assertLane(lane) {
|
|
|
35
446
|
return lane;
|
|
36
447
|
}
|
|
37
448
|
|
|
449
|
+
function assertPlanningDepth(depth) {
|
|
450
|
+
if (!PLANNING_DEPTHS.includes(depth)) {
|
|
451
|
+
throw new TypeError(`Unsupported planning depth: ${String(depth)}`);
|
|
452
|
+
}
|
|
453
|
+
return depth;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function assertAssuranceLevel(level) {
|
|
457
|
+
if (!ASSURANCE_LEVELS.includes(level)) {
|
|
458
|
+
throw new TypeError(`Unsupported assurance level: ${String(level)}`);
|
|
459
|
+
}
|
|
460
|
+
return level;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function compatibilityLane(planningDepth, assuranceLevel) {
|
|
464
|
+
assertPlanningDepth(planningDepth);
|
|
465
|
+
assertAssuranceLevel(assuranceLevel);
|
|
466
|
+
if (assuranceLevel === 'Strict') return 'Critical';
|
|
467
|
+
if (planningDepth === 'None' && assuranceLevel === 'Routine') return 'Direct';
|
|
468
|
+
return 'Standard';
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function v1Axes(lane) {
|
|
472
|
+
assertLane(lane);
|
|
473
|
+
if (lane === 'Direct') return { planningDepth: 'None', assuranceLevel: 'Routine' };
|
|
474
|
+
if (lane === 'Critical') return { planningDepth: 'Full', assuranceLevel: 'Strict' };
|
|
475
|
+
return { planningDepth: 'Compact', assuranceLevel: 'Routine' };
|
|
476
|
+
}
|
|
477
|
+
|
|
38
478
|
function asTrue(value) {
|
|
39
479
|
return value === true || value === 1 || value === 'true' || value === 'yes';
|
|
40
480
|
}
|
|
@@ -53,11 +493,13 @@ function textValue(input) {
|
|
|
53
493
|
|
|
54
494
|
function classifyRiskSignals(input = {}) {
|
|
55
495
|
const nestedSignals = input.signals && typeof input.signals === 'object' ? input.signals : {};
|
|
56
|
-
const
|
|
496
|
+
const declaredRiskValues = [input.riskSignals, input.risks]
|
|
57
497
|
.flatMap((value) => Array.isArray(value) ? value : [])
|
|
58
|
-
.filter((value) => typeof value === 'string')
|
|
498
|
+
.filter((value) => typeof value === 'string');
|
|
499
|
+
const declaredSignals = declaredRiskValues
|
|
59
500
|
.join(' ')
|
|
60
501
|
.toLowerCase();
|
|
502
|
+
const explicitRisk = (terms) => declaredRiskValues.some((value) => terms.includes(value.trim().toLowerCase()));
|
|
61
503
|
const source = {
|
|
62
504
|
...input,
|
|
63
505
|
...nestedSignals,
|
|
@@ -78,7 +520,9 @@ function classifyRiskSignals(input = {}) {
|
|
|
78
520
|
auth: signalValue(source, LANE_RISK_KEYS.auth) || hasTerm(LANE_RISK_KEYS.auth),
|
|
79
521
|
payment: signalValue(source, LANE_RISK_KEYS.payment) || hasTerm(LANE_RISK_KEYS.payment),
|
|
80
522
|
privacy: signalValue(source, LANE_RISK_KEYS.privacy) || hasTerm(LANE_RISK_KEYS.privacy),
|
|
81
|
-
data: signalValue(source,
|
|
523
|
+
data: signalValue(source, ['data', 'dataset', 'records'])
|
|
524
|
+
|| explicitRisk(['data', 'dataset', 'records'])
|
|
525
|
+
|| hasTerm(LANE_RISK_KEYS.data),
|
|
82
526
|
schema: signalValue(source, LANE_RISK_KEYS.schema) || hasTerm(LANE_RISK_KEYS.schema),
|
|
83
527
|
migration: signalValue(source, LANE_RISK_KEYS.migration) || hasTerm(LANE_RISK_KEYS.migration),
|
|
84
528
|
publicContract: signalValue(source, LANE_RISK_KEYS.publicContract) || hasTerm(LANE_RISK_KEYS.publicContract),
|
|
@@ -108,95 +552,603 @@ function overrideLane(input = {}) {
|
|
|
108
552
|
return input.override ?? input.laneOverride ?? input.requestedLane ?? input.lane ?? null;
|
|
109
553
|
}
|
|
110
554
|
|
|
111
|
-
function
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
555
|
+
function freezeClassification(obj) {
|
|
556
|
+
Object.freeze(obj);
|
|
557
|
+
if (Array.isArray(obj.warnings)) Object.freeze(obj.warnings);
|
|
558
|
+
if (Array.isArray(obj.risks)) Object.freeze(obj.risks);
|
|
559
|
+
if (obj.signals && typeof obj.signals === 'object') Object.freeze(obj.signals);
|
|
560
|
+
trustedClassifications.add(obj);
|
|
561
|
+
return obj;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function requestedAxis(input, snake, camel) {
|
|
565
|
+
return input[snake] ?? input[camel] ?? null;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function inferredPlanningDepth(input, taskCount) {
|
|
569
|
+
const complexity = String(input.systemComplexity ?? input.system_complexity ?? input.complexity ?? '').toLowerCase();
|
|
570
|
+
const scope = String(input.scope ?? input.scopeSize ?? input.scope_size ?? '').toLowerCase();
|
|
571
|
+
if (['high', 'complex', 'system', 'cross-system'].includes(complexity)
|
|
572
|
+
|| ['large', 'broad', 'multi-package', 'system'].includes(scope)
|
|
573
|
+
|| taskCount >= 5) return 'Full';
|
|
574
|
+
if (explicitReversible(input) && input.lowRisk === true && input.isolated === true && taskCount <= 2) return 'None';
|
|
575
|
+
return 'Compact';
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function minimumAssuranceForRisks(risks = []) {
|
|
579
|
+
const listed = Array.isArray(risks) ? risks : [];
|
|
580
|
+
return listed.length > 0 ? 'Elevated' : 'Routine';
|
|
123
581
|
}
|
|
124
582
|
|
|
125
583
|
function classifyLane(input = {}) {
|
|
126
|
-
if (typeof input === 'string')
|
|
584
|
+
if (typeof input === 'string') {
|
|
585
|
+
const axes = v1Axes(input);
|
|
586
|
+
return freezeClassification({
|
|
587
|
+
...axes,
|
|
588
|
+
automaticPlanningDepth: axes.planningDepth,
|
|
589
|
+
automaticAssuranceLevel: axes.assuranceLevel,
|
|
590
|
+
lane: input,
|
|
591
|
+
automaticLane: input,
|
|
592
|
+
overridden: false,
|
|
593
|
+
warnings: [], risks: [], signals: {},
|
|
594
|
+
});
|
|
595
|
+
}
|
|
127
596
|
if (!input || typeof input !== 'object') throw new TypeError('Lane classification input must be an object');
|
|
128
|
-
|
|
129
|
-
const signals = classifyRiskSignals(input);
|
|
130
|
-
const risks = Object.entries(signals).filter(([, active]) => active).map(([name]) => name);
|
|
131
597
|
const taskCount = input.taskCount === undefined ? 1 : input.taskCount;
|
|
132
598
|
if (!Number.isInteger(taskCount) || taskCount < 1) throw new RangeError('taskCount must be a positive integer');
|
|
133
599
|
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
600
|
+
const signals = classifyRiskSignals(input);
|
|
601
|
+
const explicitRisks = Array.isArray(input.risks)
|
|
602
|
+
? input.risks.filter((risk) => typeof risk === 'string' && risk.trim() !== '')
|
|
603
|
+
: [];
|
|
604
|
+
const risks = [...new Set([
|
|
605
|
+
...Object.entries(signals).filter(([, active]) => active).map(([name]) => name),
|
|
606
|
+
...explicitRisks,
|
|
607
|
+
])];
|
|
608
|
+
const inferredPlanning = inferredPlanningDepth(input, taskCount);
|
|
609
|
+
const automaticAssurance = minimumAssuranceForRisks(risks);
|
|
610
|
+
const legacyRequested = overrideLane(input);
|
|
611
|
+
const legacyAxes = legacyRequested === null || legacyRequested === undefined ? null : v1Axes(assertLane(legacyRequested));
|
|
612
|
+
const requestedPlanning = requestedAxis(input, 'planning_depth', 'planningDepth') ?? legacyAxes?.planningDepth;
|
|
613
|
+
const requestedAssurance = requestedAxis(input, 'assurance_level', 'assuranceLevel') ?? legacyAxes?.assuranceLevel;
|
|
614
|
+
if (requestedPlanning !== null && requestedPlanning !== undefined) assertPlanningDepth(requestedPlanning);
|
|
615
|
+
if (requestedAssurance !== null && requestedAssurance !== undefined) assertAssuranceLevel(requestedAssurance);
|
|
616
|
+
if (requestedPlanning && PLANNING_DEPTHS.indexOf(requestedPlanning) < PLANNING_DEPTHS.indexOf(inferredPlanning)) {
|
|
617
|
+
throw new Error(`planning_depth downgrade from ${inferredPlanning} to ${requestedPlanning} is not permitted`);
|
|
149
618
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
619
|
+
if (requestedAssurance && ASSURANCE_LEVELS.indexOf(requestedAssurance) < ASSURANCE_LEVELS.indexOf(automaticAssurance)) {
|
|
620
|
+
throw new Error(`assurance_level downgrade from ${automaticAssurance} to ${requestedAssurance} is blocked for risks: ${risks.join(', ')}`);
|
|
621
|
+
}
|
|
622
|
+
const planningDepth = requestedPlanning || inferredPlanning;
|
|
623
|
+
const assuranceLevel = requestedAssurance || automaticAssurance;
|
|
624
|
+
const lane = compatibilityLane(planningDepth, assuranceLevel);
|
|
625
|
+
const automaticLane = compatibilityLane(inferredPlanning, automaticAssurance);
|
|
626
|
+
return freezeClassification({
|
|
627
|
+
planningDepth,
|
|
628
|
+
automaticPlanningDepth: inferredPlanning,
|
|
629
|
+
assuranceLevel,
|
|
630
|
+
automaticAssuranceLevel: automaticAssurance,
|
|
631
|
+
lane,
|
|
632
|
+
automaticLane,
|
|
633
|
+
overridden: Boolean(requestedPlanning || requestedAssurance || legacyAxes),
|
|
634
|
+
warnings: legacyAxes ? [`Legacy lane ${legacyRequested} adapted to Specs v2 axes.`] : [],
|
|
635
|
+
risks,
|
|
636
|
+
signals,
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function uniqueStrings(values, field) {
|
|
641
|
+
if (!Array.isArray(values)) return { valid: false, error: `${field} must be an array` };
|
|
642
|
+
if (values.some((value) => typeof value !== 'string' || value.trim() === '')) {
|
|
643
|
+
return { valid: false, error: `${field} must contain non-empty strings` };
|
|
644
|
+
}
|
|
645
|
+
if (new Set(values).size !== values.length) return { valid: false, error: `${field} must not contain duplicates` };
|
|
646
|
+
return { valid: true };
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function planningObligationsFor(depth) {
|
|
650
|
+
assertPlanningDepth(depth);
|
|
651
|
+
if (depth === 'None') return [];
|
|
652
|
+
if (depth === 'Compact') return ['needsRequirements', 'needsDesign'];
|
|
653
|
+
return [...PLANNING_OBLIGATIONS];
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function obligationsForAssurance(level, risks = []) {
|
|
657
|
+
assertAssuranceLevel(level);
|
|
658
|
+
const obligations = new Set(['needsExecutionProof']);
|
|
659
|
+
if (level === 'Elevated') obligations.add('needsInspection');
|
|
660
|
+
if (level === 'Strict') {
|
|
661
|
+
obligations.add('needsInspection');
|
|
662
|
+
obligations.add('needsIndependentAudit');
|
|
663
|
+
}
|
|
664
|
+
return PROOF_OBLIGATIONS.filter((obligation) => obligations.has(obligation));
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function obligationsForLane(lane, risks = []) {
|
|
668
|
+
return obligationsForAssurance(v1Axes(assertLane(lane)).assuranceLevel, risks);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function legacyObligationsForLane(lane, risks = []) {
|
|
672
|
+
assertLane(lane);
|
|
673
|
+
const riskSet = new Set(risks);
|
|
674
|
+
const selected = new Set(['needsExecutionProof']);
|
|
675
|
+
if (lane !== 'Direct') selected.add('needsInspection');
|
|
676
|
+
if (lane === 'Critical') selected.add('needsIndependentAudit');
|
|
677
|
+
if (lane === 'Critical' && [
|
|
678
|
+
'destructive', 'reversibility', 'rollback', 'data', 'schema', 'migration',
|
|
679
|
+
'privacy', 'payment', 'crossRuntime',
|
|
680
|
+
].some((risk) => riskSet.has(risk))) selected.add('needsDurableTaskState');
|
|
681
|
+
if (lane === 'Critical' && [
|
|
682
|
+
'auth', 'payment', 'privacy', 'schema', 'migration', 'publicContract',
|
|
683
|
+
'crossRuntime', 'ambiguity',
|
|
684
|
+
].some((risk) => riskSet.has(risk))) selected.add('needsResearchGrounding');
|
|
685
|
+
return [...PROOF_OBLIGATIONS, 'needsResearchGrounding'].filter((obligation) => selected.has(obligation));
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function actorNeedsFor(obligations) {
|
|
689
|
+
return obligations.map((obligation) => ({ ...ACTOR_NEEDS_BY_OBLIGATION[obligation] }));
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function mergeProofObligations(...groups) {
|
|
693
|
+
const selected = new Set(groups.flat());
|
|
694
|
+
return READ_COMPAT_PROOF_OBLIGATIONS.filter((obligation) => selected.has(obligation));
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function snapshotFromClassification(classification) {
|
|
698
|
+
const planningDepth = assertPlanningDepth(classification.planningDepth);
|
|
699
|
+
const automaticPlanningDepth = assertPlanningDepth(classification.automaticPlanningDepth || planningDepth);
|
|
700
|
+
const assuranceLevel = assertAssuranceLevel(classification.assuranceLevel);
|
|
701
|
+
const automaticAssuranceLevel = assertAssuranceLevel(classification.automaticAssuranceLevel || assuranceLevel);
|
|
702
|
+
if (PLANNING_DEPTHS.indexOf(planningDepth) < PLANNING_DEPTHS.indexOf(automaticPlanningDepth)) {
|
|
703
|
+
throw new Error(`Workflow policy cannot downgrade planning_depth ${automaticPlanningDepth} to ${planningDepth}`);
|
|
704
|
+
}
|
|
705
|
+
if (ASSURANCE_LEVELS.indexOf(assuranceLevel) < ASSURANCE_LEVELS.indexOf(automaticAssuranceLevel)) {
|
|
706
|
+
throw new Error(`Workflow policy cannot downgrade assurance_level ${automaticAssuranceLevel} to ${assuranceLevel}`);
|
|
707
|
+
}
|
|
708
|
+
const risks = Array.isArray(classification.risks) ? [...new Set(classification.risks)] : [];
|
|
709
|
+
const minimumAssurance = minimumAssuranceForRisks(risks);
|
|
710
|
+
if (ASSURANCE_LEVELS.indexOf(assuranceLevel) < ASSURANCE_LEVELS.indexOf(minimumAssurance)) {
|
|
711
|
+
throw new Error(`Workflow policy assurance_level ${assuranceLevel} is below ${minimumAssurance} for risks: ${risks.join(', ')}`);
|
|
712
|
+
}
|
|
713
|
+
const lane = compatibilityLane(planningDepth, assuranceLevel);
|
|
714
|
+
const automaticLane = compatibilityLane(automaticPlanningDepth, automaticAssuranceLevel);
|
|
715
|
+
const proofObligations = obligationsForAssurance(assuranceLevel, risks);
|
|
716
|
+
return {
|
|
717
|
+
version: WORKFLOW_POLICY_VERSION,
|
|
718
|
+
planning_depth: planningDepth,
|
|
719
|
+
automatic_planning_depth: automaticPlanningDepth,
|
|
720
|
+
assurance_level: assuranceLevel,
|
|
721
|
+
automatic_assurance_level: automaticAssuranceLevel,
|
|
722
|
+
lane,
|
|
723
|
+
automatic_lane: automaticLane,
|
|
724
|
+
risks,
|
|
725
|
+
artifact_profile: ARTIFACT_PROFILE_BY_PLANNING[planningDepth],
|
|
726
|
+
planning_obligations: planningObligationsFor(planningDepth),
|
|
727
|
+
proof_obligations: proofObligations,
|
|
728
|
+
actor_needs: actorNeedsFor(proofObligations),
|
|
729
|
+
override_receipt: null,
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function canonicalSnapshotFromAxes({
|
|
734
|
+
planningDepth,
|
|
735
|
+
assuranceLevel,
|
|
736
|
+
automaticPlanningDepth,
|
|
737
|
+
automaticAssuranceLevel,
|
|
738
|
+
risks = [],
|
|
739
|
+
}) {
|
|
740
|
+
assertPlanningDepth(planningDepth);
|
|
741
|
+
assertAssuranceLevel(assuranceLevel);
|
|
742
|
+
assertPlanningDepth(automaticPlanningDepth);
|
|
743
|
+
assertAssuranceLevel(automaticAssuranceLevel);
|
|
744
|
+
if (PLANNING_DEPTHS.indexOf(planningDepth) < PLANNING_DEPTHS.indexOf(automaticPlanningDepth)) {
|
|
745
|
+
throw new Error(`Workflow policy cannot downgrade planning_depth ${automaticPlanningDepth} to ${planningDepth}`);
|
|
746
|
+
}
|
|
747
|
+
if (ASSURANCE_LEVELS.indexOf(assuranceLevel) < ASSURANCE_LEVELS.indexOf(automaticAssuranceLevel)) {
|
|
748
|
+
throw new Error(`Workflow policy cannot downgrade assurance_level ${automaticAssuranceLevel} to ${assuranceLevel}`);
|
|
749
|
+
}
|
|
750
|
+
const uniqueRisks = [...new Set(risks)];
|
|
751
|
+
const minimumForRisks = minimumAssuranceForRisks(uniqueRisks);
|
|
752
|
+
if (ASSURANCE_LEVELS.indexOf(assuranceLevel) < ASSURANCE_LEVELS.indexOf(minimumForRisks)) {
|
|
753
|
+
throw new Error(`Workflow policy assurance_level ${assuranceLevel} is below ${minimumForRisks} for risks: ${uniqueRisks.join(', ')}`);
|
|
754
|
+
}
|
|
755
|
+
return {
|
|
756
|
+
version: CANONICAL_WORKFLOW_POLICY_VERSION,
|
|
757
|
+
planning_depth: planningDepth,
|
|
758
|
+
assurance_level: assuranceLevel,
|
|
759
|
+
classified_minimum: {
|
|
760
|
+
planning_depth: automaticPlanningDepth,
|
|
761
|
+
assurance_level: automaticAssuranceLevel,
|
|
762
|
+
},
|
|
763
|
+
risks: uniqueRisks,
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function canonicalWorkflowPolicySnapshot(input = {}) {
|
|
768
|
+
if (input && typeof input === 'object' && hasOwn(input, 'workflow_policy')) {
|
|
769
|
+
const view = readWorkflowPolicySnapshot(input);
|
|
770
|
+
return canonicalSnapshotFromAxes({
|
|
771
|
+
planningDepth: view.planning_depth,
|
|
772
|
+
assuranceLevel: view.assurance_level,
|
|
773
|
+
automaticPlanningDepth: view.automatic_planning_depth,
|
|
774
|
+
automaticAssuranceLevel: view.automatic_assurance_level,
|
|
775
|
+
risks: view.risks,
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
const classification = trustedClassifications.has(input) ? input : classifyLane(input);
|
|
779
|
+
return canonicalSnapshotFromAxes({
|
|
780
|
+
planningDepth: classification.planningDepth,
|
|
781
|
+
assuranceLevel: classification.assuranceLevel,
|
|
782
|
+
automaticPlanningDepth: classification.automaticPlanningDepth,
|
|
783
|
+
automaticAssuranceLevel: classification.automaticAssuranceLevel,
|
|
784
|
+
risks: classification.risks,
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function isCanonicalTasklessTerminalSpec(spec, specDir) {
|
|
789
|
+
if (!spec || typeof spec !== 'object' || Array.isArray(spec) || spec.schema_version !== '2.1') return false;
|
|
790
|
+
const policy = spec.workflow_policy;
|
|
791
|
+
const validation = validateWorkflowPolicySnapshot(policy);
|
|
792
|
+
if (!validation.valid || policy.version !== CANONICAL_WORKFLOW_POLICY_VERSION
|
|
793
|
+
|| !['Compact', 'Full'].includes(policy.planning_depth)) return false;
|
|
794
|
+
if (Object.prototype.hasOwnProperty.call(spec, 'task_files')
|
|
795
|
+
&& (!Array.isArray(spec.task_files) || spec.task_files.length !== 0)) return false;
|
|
796
|
+
if (Object.prototype.hasOwnProperty.call(spec, 'task_registry')
|
|
797
|
+
&& (!spec.task_registry || typeof spec.task_registry !== 'object'
|
|
798
|
+
|| Array.isArray(spec.task_registry) || Object.keys(spec.task_registry).length !== 0)) return false;
|
|
799
|
+
if (typeof specDir !== 'string' || specDir.trim() === '') return false;
|
|
800
|
+
try {
|
|
801
|
+
const entries = fs.readdirSync(path.join(specDir, 'tasks'));
|
|
802
|
+
return entries.length === 0;
|
|
803
|
+
} catch (error) {
|
|
804
|
+
return error && error.code === 'ENOENT';
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function canonicalPolicyView(snapshot) {
|
|
809
|
+
const planningDepth = snapshot.planning_depth;
|
|
810
|
+
const assuranceLevel = snapshot.assurance_level;
|
|
811
|
+
const automaticPlanningDepth = snapshot.classified_minimum.planning_depth;
|
|
812
|
+
const automaticAssuranceLevel = snapshot.classified_minimum.assurance_level;
|
|
813
|
+
const proofObligations = obligationsForAssurance(assuranceLevel, snapshot.risks);
|
|
814
|
+
return {
|
|
815
|
+
...JSON.parse(JSON.stringify(snapshot)),
|
|
816
|
+
automatic_planning_depth: automaticPlanningDepth,
|
|
817
|
+
automatic_assurance_level: automaticAssuranceLevel,
|
|
818
|
+
lane: compatibilityLane(planningDepth, assuranceLevel),
|
|
819
|
+
automatic_lane: compatibilityLane(automaticPlanningDepth, automaticAssuranceLevel),
|
|
820
|
+
artifact_profile: ARTIFACT_PROFILE_BY_PLANNING[planningDepth],
|
|
821
|
+
planning_obligations: planningObligationsFor(planningDepth),
|
|
822
|
+
proof_obligations: proofObligations,
|
|
823
|
+
actor_needs: actorNeedsFor(proofObligations),
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function workflowPolicySnapshot(input = {}) {
|
|
828
|
+
if (input && typeof input === 'object' && hasOwn(input, 'workflow_policy')) {
|
|
829
|
+
return readWorkflowPolicySnapshot(input);
|
|
830
|
+
}
|
|
831
|
+
if (input && typeof input === 'object' && input.design_context?.execution_tier) {
|
|
832
|
+
return readWorkflowPolicySnapshot(input);
|
|
833
|
+
}
|
|
834
|
+
if (input && typeof input === 'object' && isWorkflowPolicySnapshot(input)) {
|
|
835
|
+
assertWorkflowPolicySnapshot(input);
|
|
836
|
+
if (input.version === WORKFLOW_POLICY_VERSION) return clonePolicySnapshot(input);
|
|
837
|
+
return readWorkflowPolicySnapshot({ workflow_policy: input });
|
|
158
838
|
}
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
839
|
+
const classification = trustedClassifications.has(input)
|
|
840
|
+
? input
|
|
841
|
+
: classifyLane(input);
|
|
842
|
+
return snapshotFromClassification(classification);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function isWorkflowPolicySnapshot(value) {
|
|
846
|
+
return Boolean(value && typeof value === 'object' && Object.prototype.hasOwnProperty.call(value, 'version'));
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function validateV1Snapshot(snapshot) {
|
|
850
|
+
const errors = [];
|
|
851
|
+
if (JSON.stringify(Object.keys(snapshot).sort()) !== JSON.stringify([...V1_POLICY_FIELDS].sort())) {
|
|
852
|
+
errors.push(`workflow_policy v1 fields must be exactly ${V1_POLICY_FIELDS.join(', ')}`);
|
|
853
|
+
}
|
|
854
|
+
if (!LANES.includes(snapshot.lane)) errors.push(`workflow_policy.lane must be one of ${LANES.join(', ')}`);
|
|
855
|
+
if (!LANES.includes(snapshot.automatic_lane)) errors.push(`workflow_policy.automatic_lane must be one of ${LANES.join(', ')}`);
|
|
856
|
+
if (LANES.includes(snapshot.lane) && LANES.includes(snapshot.automatic_lane)
|
|
857
|
+
&& LANES.indexOf(snapshot.lane) < LANES.indexOf(snapshot.automatic_lane)) errors.push('workflow_policy.lane cannot be lower than automatic_lane');
|
|
858
|
+
const risks = uniqueStrings(snapshot.risks, 'workflow_policy.risks');
|
|
859
|
+
if (!risks.valid) errors.push(risks.error);
|
|
860
|
+
if (risks.valid && LANES.includes(snapshot.lane)) {
|
|
861
|
+
const minimum = compatibilityLane('None', minimumAssuranceForRisks(snapshot.risks));
|
|
862
|
+
if (LANES.indexOf(snapshot.lane) < LANES.indexOf(minimum)) errors.push(`workflow_policy.lane must be at least ${minimum} for its risks`);
|
|
863
|
+
}
|
|
864
|
+
if (LANES.includes(snapshot.lane) && snapshot.artifact_profile !== ARTIFACT_PROFILE_BY_LANE[snapshot.lane]) {
|
|
865
|
+
errors.push(`workflow_policy.artifact_profile must be ${ARTIFACT_PROFILE_BY_LANE[snapshot.lane]} for ${snapshot.lane}`);
|
|
866
|
+
}
|
|
867
|
+
const obligations = uniqueStrings(snapshot.proof_obligations, 'workflow_policy.proof_obligations');
|
|
868
|
+
if (!obligations.valid) errors.push(obligations.error);
|
|
869
|
+
else if (LANES.includes(snapshot.lane) && risks.valid) {
|
|
870
|
+
const expected = legacyObligationsForLane(snapshot.lane, snapshot.risks);
|
|
871
|
+
if (JSON.stringify(snapshot.proof_obligations) !== JSON.stringify(expected)) errors.push('workflow_policy.proof_obligations do not match v1 lane and risks');
|
|
872
|
+
}
|
|
873
|
+
if (!Array.isArray(snapshot.actor_needs)) errors.push('workflow_policy.actor_needs must be an array');
|
|
874
|
+
else if (obligations.valid && JSON.stringify(snapshot.actor_needs) !== JSON.stringify(actorNeedsFor(snapshot.proof_obligations))) errors.push('workflow_policy.actor_needs must correspond to proof_obligations');
|
|
875
|
+
if (snapshot.override_receipt !== null) errors.push('workflow_policy.override_receipt must be null');
|
|
876
|
+
return { valid: errors.length === 0, errors };
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function validateWorkflowPolicySnapshot(snapshot) {
|
|
880
|
+
const errors = [];
|
|
881
|
+
if (!snapshot || typeof snapshot !== 'object' || Array.isArray(snapshot)) {
|
|
882
|
+
return { valid: false, errors: ['workflow_policy must be an object'] };
|
|
883
|
+
}
|
|
884
|
+
if (snapshot.version === CANONICAL_WORKFLOW_POLICY_VERSION) {
|
|
885
|
+
const keys = Object.keys(snapshot).sort();
|
|
886
|
+
if (JSON.stringify(keys) !== JSON.stringify([...CANONICAL_WORKFLOW_POLICY_FIELDS].sort())) {
|
|
887
|
+
errors.push(`workflow_policy v2.1 fields must be exactly ${CANONICAL_WORKFLOW_POLICY_FIELDS.join(', ')}`);
|
|
888
|
+
}
|
|
889
|
+
if (!PLANNING_DEPTHS.includes(snapshot.planning_depth)) {
|
|
890
|
+
errors.push(`workflow_policy.planning_depth must be one of ${PLANNING_DEPTHS.join(', ')}`);
|
|
891
|
+
}
|
|
892
|
+
if (!ASSURANCE_LEVELS.includes(snapshot.assurance_level)) {
|
|
893
|
+
errors.push(`workflow_policy.assurance_level must be one of ${ASSURANCE_LEVELS.join(', ')}`);
|
|
894
|
+
}
|
|
895
|
+
const minimum = snapshot.classified_minimum;
|
|
896
|
+
if (!minimum || typeof minimum !== 'object' || Array.isArray(minimum)
|
|
897
|
+
|| Object.keys(minimum).sort().join(',') !== 'assurance_level,planning_depth') {
|
|
898
|
+
errors.push('workflow_policy.classified_minimum must contain exactly planning_depth and assurance_level');
|
|
899
|
+
} else {
|
|
900
|
+
if (!PLANNING_DEPTHS.includes(minimum.planning_depth)) {
|
|
901
|
+
errors.push(`workflow_policy.classified_minimum.planning_depth must be one of ${PLANNING_DEPTHS.join(', ')}`);
|
|
902
|
+
}
|
|
903
|
+
if (!ASSURANCE_LEVELS.includes(minimum.assurance_level)) {
|
|
904
|
+
errors.push(`workflow_policy.classified_minimum.assurance_level must be one of ${ASSURANCE_LEVELS.join(', ')}`);
|
|
905
|
+
}
|
|
906
|
+
if (PLANNING_DEPTHS.includes(snapshot.planning_depth) && PLANNING_DEPTHS.includes(minimum.planning_depth)
|
|
907
|
+
&& PLANNING_DEPTHS.indexOf(snapshot.planning_depth) < PLANNING_DEPTHS.indexOf(minimum.planning_depth)) {
|
|
908
|
+
errors.push('workflow_policy.planning_depth cannot be lower than classified_minimum.planning_depth');
|
|
909
|
+
}
|
|
910
|
+
if (ASSURANCE_LEVELS.includes(snapshot.assurance_level) && ASSURANCE_LEVELS.includes(minimum.assurance_level)
|
|
911
|
+
&& ASSURANCE_LEVELS.indexOf(snapshot.assurance_level) < ASSURANCE_LEVELS.indexOf(minimum.assurance_level)) {
|
|
912
|
+
errors.push('workflow_policy.assurance_level cannot be lower than classified_minimum.assurance_level');
|
|
166
913
|
}
|
|
167
914
|
}
|
|
915
|
+
const risks = uniqueStrings(snapshot.risks, 'workflow_policy.risks');
|
|
916
|
+
if (!risks.valid) errors.push(risks.error);
|
|
917
|
+
if (risks.valid && ASSURANCE_LEVELS.includes(snapshot.assurance_level)) {
|
|
918
|
+
const riskMinimum = minimumAssuranceForRisks(snapshot.risks);
|
|
919
|
+
if (ASSURANCE_LEVELS.indexOf(snapshot.assurance_level) < ASSURANCE_LEVELS.indexOf(riskMinimum)) {
|
|
920
|
+
errors.push(`workflow_policy.assurance_level must be at least ${riskMinimum} for its risks`);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
return { valid: errors.length === 0, errors };
|
|
168
924
|
}
|
|
169
|
-
return
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
925
|
+
if (snapshot.version === '1') return validateV1Snapshot(snapshot);
|
|
926
|
+
const keys = Object.keys(snapshot).sort();
|
|
927
|
+
const expectedKeys = [...WORKFLOW_POLICY_FIELDS].sort();
|
|
928
|
+
if (JSON.stringify(keys) !== JSON.stringify(expectedKeys)) {
|
|
929
|
+
errors.push(`workflow_policy fields must be exactly ${WORKFLOW_POLICY_FIELDS.join(', ')}`);
|
|
930
|
+
}
|
|
931
|
+
if (snapshot.version !== WORKFLOW_POLICY_VERSION) errors.push('workflow_policy.version must be "2"');
|
|
932
|
+
if (!PLANNING_DEPTHS.includes(snapshot.planning_depth)) errors.push(`workflow_policy.planning_depth must be one of ${PLANNING_DEPTHS.join(', ')}`);
|
|
933
|
+
if (!PLANNING_DEPTHS.includes(snapshot.automatic_planning_depth)) errors.push(`workflow_policy.automatic_planning_depth must be one of ${PLANNING_DEPTHS.join(', ')}`);
|
|
934
|
+
if (!ASSURANCE_LEVELS.includes(snapshot.assurance_level)) errors.push(`workflow_policy.assurance_level must be one of ${ASSURANCE_LEVELS.join(', ')}`);
|
|
935
|
+
if (!ASSURANCE_LEVELS.includes(snapshot.automatic_assurance_level)) errors.push(`workflow_policy.automatic_assurance_level must be one of ${ASSURANCE_LEVELS.join(', ')}`);
|
|
936
|
+
if (!LANES.includes(snapshot.lane)) errors.push(`workflow_policy.lane must be one of ${LANES.join(', ')}`);
|
|
937
|
+
if (!LANES.includes(snapshot.automatic_lane)) errors.push(`workflow_policy.automatic_lane must be one of ${LANES.join(', ')}`);
|
|
938
|
+
if (PLANNING_DEPTHS.includes(snapshot.planning_depth) && PLANNING_DEPTHS.includes(snapshot.automatic_planning_depth)
|
|
939
|
+
&& PLANNING_DEPTHS.indexOf(snapshot.planning_depth) < PLANNING_DEPTHS.indexOf(snapshot.automatic_planning_depth)) errors.push('workflow_policy.planning_depth cannot be lower than automatic_planning_depth');
|
|
940
|
+
if (ASSURANCE_LEVELS.includes(snapshot.assurance_level) && ASSURANCE_LEVELS.includes(snapshot.automatic_assurance_level)
|
|
941
|
+
&& ASSURANCE_LEVELS.indexOf(snapshot.assurance_level) < ASSURANCE_LEVELS.indexOf(snapshot.automatic_assurance_level)) errors.push('workflow_policy.assurance_level cannot be lower than automatic_assurance_level');
|
|
942
|
+
if (PLANNING_DEPTHS.includes(snapshot.planning_depth) && ASSURANCE_LEVELS.includes(snapshot.assurance_level)
|
|
943
|
+
&& snapshot.lane !== compatibilityLane(snapshot.planning_depth, snapshot.assurance_level)) errors.push('workflow_policy.lane is not the derived compatibility lane');
|
|
944
|
+
if (PLANNING_DEPTHS.includes(snapshot.automatic_planning_depth) && ASSURANCE_LEVELS.includes(snapshot.automatic_assurance_level)
|
|
945
|
+
&& snapshot.automatic_lane !== compatibilityLane(snapshot.automatic_planning_depth, snapshot.automatic_assurance_level)) errors.push('workflow_policy.automatic_lane is not the derived compatibility lane');
|
|
946
|
+
if (PLANNING_DEPTHS.includes(snapshot.planning_depth) && snapshot.artifact_profile !== ARTIFACT_PROFILE_BY_PLANNING[snapshot.planning_depth]) errors.push('workflow_policy.artifact_profile does not match planning_depth');
|
|
947
|
+
const risks = uniqueStrings(snapshot.risks, 'workflow_policy.risks');
|
|
948
|
+
if (!risks.valid) errors.push(risks.error);
|
|
949
|
+
if (risks.valid && ASSURANCE_LEVELS.includes(snapshot.assurance_level)) {
|
|
950
|
+
const minimumAssurance = minimumAssuranceForRisks(snapshot.risks);
|
|
951
|
+
if (ASSURANCE_LEVELS.indexOf(snapshot.assurance_level) < ASSURANCE_LEVELS.indexOf(minimumAssurance)) {
|
|
952
|
+
errors.push(`workflow_policy.assurance_level must be at least ${minimumAssurance} for its risks`);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
const planning = uniqueStrings(snapshot.planning_obligations, 'workflow_policy.planning_obligations');
|
|
956
|
+
if (!planning.valid) errors.push(planning.error);
|
|
957
|
+
else if (PLANNING_DEPTHS.includes(snapshot.planning_depth)
|
|
958
|
+
&& JSON.stringify(snapshot.planning_obligations) !== JSON.stringify(planningObligationsFor(snapshot.planning_depth))) errors.push('workflow_policy.planning_obligations do not match planning_depth');
|
|
959
|
+
const obligations = uniqueStrings(snapshot.proof_obligations, 'workflow_policy.proof_obligations');
|
|
960
|
+
if (!obligations.valid) {
|
|
961
|
+
errors.push(obligations.error);
|
|
962
|
+
} else {
|
|
963
|
+
for (const obligation of snapshot.proof_obligations) {
|
|
964
|
+
if (!READ_COMPAT_PROOF_OBLIGATIONS.includes(obligation)) errors.push(`workflow_policy.proof_obligations has unknown obligation ${obligation}`);
|
|
965
|
+
}
|
|
966
|
+
if (!snapshot.proof_obligations.includes('needsExecutionProof')) {
|
|
967
|
+
errors.push('workflow_policy.proof_obligations must include needsExecutionProof');
|
|
968
|
+
}
|
|
969
|
+
if (ASSURANCE_LEVELS.includes(snapshot.assurance_level) && risks.valid) {
|
|
970
|
+
const expected = obligationsForAssurance(snapshot.assurance_level, snapshot.risks);
|
|
971
|
+
const exact = JSON.stringify(snapshot.proof_obligations) === JSON.stringify(expected);
|
|
972
|
+
const trustedCompat = trustedV1Adapters.has(snapshot)
|
|
973
|
+
&& JSON.stringify(snapshot.proof_obligations) === JSON.stringify(mergeProofObligations(expected, snapshot.proof_obligations));
|
|
974
|
+
if (!exact && !trustedCompat) {
|
|
975
|
+
errors.push('workflow_policy.proof_obligations do not match assurance_level and risks');
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
if (!Array.isArray(snapshot.actor_needs)) {
|
|
980
|
+
errors.push('workflow_policy.actor_needs must be an array');
|
|
981
|
+
} else {
|
|
982
|
+
for (const need of snapshot.actor_needs) {
|
|
983
|
+
if (!need || typeof need !== 'object' || Array.isArray(need)
|
|
984
|
+
|| Object.keys(need).sort().join(',') !== 'capability,independence'
|
|
985
|
+
|| !ACTOR_CAPABILITIES.has(need.capability)
|
|
986
|
+
|| !ACTOR_INDEPENDENCE.has(need.independence)) {
|
|
987
|
+
errors.push('workflow_policy.actor_needs must describe capability and independence, never an agent name');
|
|
988
|
+
break;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
if (ASSURANCE_LEVELS.includes(snapshot.assurance_level) && obligations.valid
|
|
992
|
+
&& JSON.stringify(snapshot.actor_needs) !== JSON.stringify(actorNeedsFor(snapshot.proof_obligations))) {
|
|
993
|
+
errors.push('workflow_policy.actor_needs must correspond to proof_obligations');
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
if (snapshot.override_receipt !== null) errors.push('legacy workflow_policy.override_receipt is inert and must be null');
|
|
997
|
+
return { valid: errors.length === 0, errors };
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function assertWorkflowPolicySnapshot(snapshot) {
|
|
1001
|
+
const result = validateWorkflowPolicySnapshot(snapshot);
|
|
1002
|
+
if (!result.valid) throw new TypeError(`Invalid workflow policy snapshot: ${result.errors.join('; ')}`);
|
|
1003
|
+
return snapshot;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function readWorkflowPolicySnapshot(spec = {}) {
|
|
1007
|
+
if (!spec || typeof spec !== 'object') throw new TypeError('Spec state must be an object');
|
|
1008
|
+
if (hasOwn(spec, 'workflow_policy')) {
|
|
1009
|
+
let persisted = spec.workflow_policy;
|
|
1010
|
+
if (persisted?.version === CANONICAL_WORKFLOW_POLICY_VERSION
|
|
1011
|
+
&& hasOwn(persisted, 'override_receipt')) {
|
|
1012
|
+
if (persisted.override_receipt !== null) {
|
|
1013
|
+
throw new TypeError('Invalid workflow policy snapshot: legacy workflow_policy.override_receipt is inert and must be null');
|
|
1014
|
+
}
|
|
1015
|
+
persisted = { ...persisted };
|
|
1016
|
+
delete persisted.override_receipt;
|
|
1017
|
+
}
|
|
1018
|
+
assertWorkflowPolicySnapshot(persisted);
|
|
1019
|
+
if (persisted.version === CANONICAL_WORKFLOW_POLICY_VERSION) {
|
|
1020
|
+
return canonicalPolicyView(persisted);
|
|
1021
|
+
}
|
|
1022
|
+
if (persisted.version === WORKFLOW_POLICY_VERSION) return clonePolicySnapshot(persisted);
|
|
1023
|
+
const selected = v1Axes(persisted.lane);
|
|
1024
|
+
const automatic = v1Axes(persisted.automatic_lane);
|
|
1025
|
+
const adapted = snapshotFromClassification({
|
|
1026
|
+
...selected,
|
|
1027
|
+
automaticPlanningDepth: automatic.planningDepth,
|
|
1028
|
+
automaticAssuranceLevel: automatic.assuranceLevel,
|
|
1029
|
+
risks: persisted.risks,
|
|
1030
|
+
});
|
|
1031
|
+
const proofObligations = mergeProofObligations(adapted.proof_obligations, persisted.proof_obligations);
|
|
1032
|
+
const result = {
|
|
1033
|
+
...adapted,
|
|
1034
|
+
proof_obligations: proofObligations,
|
|
1035
|
+
actor_needs: actorNeedsFor(proofObligations),
|
|
1036
|
+
};
|
|
1037
|
+
trustedV1Adapters.add(result);
|
|
1038
|
+
return result;
|
|
1039
|
+
}
|
|
1040
|
+
// Legacy state is read-compatible only. The fallback is deliberately not
|
|
1041
|
+
// written back and cannot authorize a downgrade or override new input.
|
|
1042
|
+
const rawLegacyTier = spec.design_context && spec.design_context.execution_tier;
|
|
1043
|
+
const legacyTier = typeof rawLegacyTier === 'string'
|
|
1044
|
+
? Object.keys(LEGACY_TIER_LANES).find((tier) => tier.toLowerCase() === rawLegacyTier.toLowerCase())
|
|
1045
|
+
: null;
|
|
1046
|
+
const legacyLane = legacyTier ? LEGACY_TIER_LANES[legacyTier] : null;
|
|
1047
|
+
if (legacyLane) {
|
|
1048
|
+
const axes = v1Axes(legacyLane);
|
|
1049
|
+
return snapshotFromClassification({ ...axes, automaticPlanningDepth: axes.planningDepth, automaticAssuranceLevel: axes.assuranceLevel, risks: [] });
|
|
1050
|
+
}
|
|
1051
|
+
throw new TypeError('workflow_policy snapshot is missing at the spec boundary');
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function persistWorkflowPolicySnapshot(spec, input = {}) {
|
|
1055
|
+
if (!spec || typeof spec !== 'object' || Array.isArray(spec)) throw new TypeError('Spec state must be an object');
|
|
1056
|
+
const next = { ...spec };
|
|
1057
|
+
delete next.override_receipt;
|
|
1058
|
+
if (hasOwn(spec, 'workflow_policy')) {
|
|
1059
|
+
return { ...next, workflow_policy: canonicalWorkflowPolicySnapshot(spec) };
|
|
1060
|
+
}
|
|
1061
|
+
return { ...next, workflow_policy: canonicalWorkflowPolicySnapshot(input) };
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
const persistWorkflowPolicy = persistWorkflowPolicySnapshot;
|
|
1065
|
+
const createWorkflowPolicySnapshot = workflowPolicySnapshot;
|
|
1066
|
+
|
|
1067
|
+
function candidateAssuranceForRisks(input, risks) {
|
|
1068
|
+
// A risk severity label is evidence for inspection, not independent-audit
|
|
1069
|
+
// authority. Strict is selected only through the explicit assurance axis.
|
|
1070
|
+
const explicitRiskLevel = String(input?.riskLevel || input?.risk_level || input?.level || '').toLowerCase();
|
|
1071
|
+
const riskLevelAssurance = ['standard', 'medium', 'high', 'critical', 'deep'].includes(explicitRiskLevel)
|
|
1072
|
+
? 'Elevated'
|
|
1073
|
+
: null;
|
|
1074
|
+
const riskLevel = minimumAssuranceForRisks(risks);
|
|
1075
|
+
const candidates = [riskLevelAssurance, riskLevel]
|
|
1076
|
+
.filter((level) => level !== null && level !== undefined);
|
|
1077
|
+
if (candidates.length === 0 || candidates.every((level) => level === 'Routine')) return null;
|
|
1078
|
+
return ASSURANCE_LEVELS[Math.max(...candidates.map((level) => ASSURANCE_LEVELS.indexOf(level)))];
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function escalateWorkflowPolicy(policyOrSpec, riskInput = {}) {
|
|
1082
|
+
const canonicalInput = policyOrSpec && typeof policyOrSpec === 'object'
|
|
1083
|
+
&& (policyOrSpec.version === CANONICAL_WORKFLOW_POLICY_VERSION
|
|
1084
|
+
|| policyOrSpec.workflow_policy?.version === CANONICAL_WORKFLOW_POLICY_VERSION);
|
|
1085
|
+
const current = policyOrSpec && typeof policyOrSpec === 'object' && hasOwn(policyOrSpec, 'workflow_policy')
|
|
1086
|
+
? readWorkflowPolicySnapshot(policyOrSpec)
|
|
1087
|
+
: workflowPolicySnapshot(policyOrSpec);
|
|
1088
|
+
const discovered = classifyRiskSignals(riskInput);
|
|
1089
|
+
const explicitRisks = Array.isArray(riskInput?.risks)
|
|
1090
|
+
? riskInput.risks.filter((risk) => typeof risk === 'string' && risk.trim() !== '')
|
|
1091
|
+
: [];
|
|
1092
|
+
const newRisks = [...new Set([
|
|
1093
|
+
...Object.entries(discovered).filter(([, active]) => active).map(([name]) => name),
|
|
1094
|
+
...explicitRisks,
|
|
1095
|
+
])];
|
|
1096
|
+
const risks = [...new Set([...current.risks, ...newRisks])];
|
|
1097
|
+
const requestedAssurance = riskInput?.assurance_level ?? riskInput?.assuranceLevel;
|
|
1098
|
+
if (requestedAssurance !== undefined && requestedAssurance !== null) {
|
|
1099
|
+
assertAssuranceLevel(requestedAssurance);
|
|
1100
|
+
}
|
|
1101
|
+
const hasNewRisk = newRisks.some((risk) => !current.risks.includes(risk));
|
|
1102
|
+
const hasRiskLevel = ['standard', 'medium', 'high', 'critical', 'deep'].includes(
|
|
1103
|
+
String(riskInput?.riskLevel || riskInput?.risk_level || riskInput?.level || '').toLowerCase(),
|
|
1104
|
+
);
|
|
1105
|
+
const raisesRequestedAssurance = requestedAssurance
|
|
1106
|
+
&& ASSURANCE_LEVELS.indexOf(requestedAssurance) > ASSURANCE_LEVELS.indexOf(current.assurance_level);
|
|
1107
|
+
if (!hasNewRisk && !hasRiskLevel && !raisesRequestedAssurance) return current;
|
|
1108
|
+
const candidate = candidateAssuranceForRisks(riskInput, risks);
|
|
1109
|
+
if (!candidate && !raisesRequestedAssurance) return current;
|
|
1110
|
+
const automaticAssuranceLevel = ASSURANCE_LEVELS[Math.max(
|
|
1111
|
+
ASSURANCE_LEVELS.indexOf(current.automatic_assurance_level),
|
|
1112
|
+
ASSURANCE_LEVELS.indexOf(candidate || 'Routine'),
|
|
1113
|
+
)];
|
|
1114
|
+
const assuranceLevel = ASSURANCE_LEVELS[Math.max(
|
|
1115
|
+
ASSURANCE_LEVELS.indexOf(current.assurance_level),
|
|
1116
|
+
ASSURANCE_LEVELS.indexOf(automaticAssuranceLevel),
|
|
1117
|
+
ASSURANCE_LEVELS.indexOf(requestedAssurance || 'Routine'),
|
|
1118
|
+
)];
|
|
1119
|
+
const axes = {
|
|
1120
|
+
planningDepth: current.planning_depth,
|
|
1121
|
+
automaticPlanningDepth: current.automatic_planning_depth,
|
|
1122
|
+
assuranceLevel,
|
|
1123
|
+
automaticAssuranceLevel,
|
|
174
1124
|
risks,
|
|
175
|
-
signals,
|
|
176
1125
|
};
|
|
1126
|
+
return canonicalInput ? canonicalSnapshotFromAxes(axes) : snapshotFromClassification(axes);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function policyForSpec(spec, riskInput = null) {
|
|
1130
|
+
const snapshot = riskInput ? escalateWorkflowPolicy(spec, riskInput) : readWorkflowPolicySnapshot(spec);
|
|
1131
|
+
return lanePolicy(snapshot);
|
|
177
1132
|
}
|
|
178
1133
|
|
|
179
1134
|
function lanePolicy(input = {}) {
|
|
180
|
-
const
|
|
181
|
-
?
|
|
182
|
-
: input &&
|
|
183
|
-
? input
|
|
184
|
-
:
|
|
185
|
-
const lane = assertLane(
|
|
186
|
-
const delegated = [...LANE_DELEGATION[lane]];
|
|
1135
|
+
const snapshot = input && typeof input === 'object' && hasOwn(input, 'workflow_policy')
|
|
1136
|
+
? readWorkflowPolicySnapshot(input)
|
|
1137
|
+
: input && typeof input === 'object' && isWorkflowPolicySnapshot(input)
|
|
1138
|
+
? (assertWorkflowPolicySnapshot(input), clonePolicySnapshot(input))
|
|
1139
|
+
: workflowPolicySnapshot(input);
|
|
1140
|
+
const lane = assertLane(snapshot.lane);
|
|
187
1141
|
return {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
requiresSpec: lane !== 'Direct',
|
|
196
|
-
requiresState: lane !== 'Direct',
|
|
1142
|
+
...snapshot,
|
|
1143
|
+
proof_obligations: [...snapshot.proof_obligations],
|
|
1144
|
+
actor_needs: snapshot.actor_needs.map((need) => ({ ...need })),
|
|
1145
|
+
planning_obligations: [...snapshot.planning_obligations],
|
|
1146
|
+
requiresSpec: snapshot.planning_depth !== 'None',
|
|
1147
|
+
requiresState: snapshot.planning_depth === 'Full'
|
|
1148
|
+
|| snapshot.proof_obligations.includes('needsDurableTaskState'),
|
|
197
1149
|
qualityGate: lane === 'Direct' ? 'main-session' : lane === 'Standard' ? 'combined-feature-review' : 'strict-evidence',
|
|
198
1150
|
shipPoint: lane === 'Direct' ? 'task' : 'feature',
|
|
199
|
-
evidence:
|
|
1151
|
+
evidence: snapshot.artifact_profile,
|
|
200
1152
|
};
|
|
201
1153
|
}
|
|
202
1154
|
|
|
@@ -205,85 +1157,105 @@ function isLegacyApprovalObject(obj) {
|
|
|
205
1157
|
}
|
|
206
1158
|
|
|
207
1159
|
function validateApprovalSchema(spec) {
|
|
208
|
-
if (!spec || typeof spec !== 'object') return { valid: false, legacy: false, error: 'spec must be
|
|
1160
|
+
if (!spec || typeof spec !== 'object' || Array.isArray(spec)) return { valid: false, absent: false, legacy: false, error: 'spec must be a non-array object' };
|
|
1161
|
+
if (!Object.prototype.hasOwnProperty.call(spec, 'approvals')) {
|
|
1162
|
+
return { valid: true, absent: true, legacy: false };
|
|
1163
|
+
}
|
|
209
1164
|
const approvals = spec.approvals;
|
|
210
|
-
if (
|
|
1165
|
+
if (approvals === null || typeof approvals !== 'object' || Array.isArray(approvals)) {
|
|
1166
|
+
return { valid: false, absent: false, legacy: false, error: 'approvals must be a non-empty object' };
|
|
1167
|
+
}
|
|
1168
|
+
if (Object.keys(approvals).length === 0) {
|
|
1169
|
+
return { valid: false, absent: false, legacy: false, error: 'approvals must not be empty' };
|
|
1170
|
+
}
|
|
211
1171
|
// Check for legacy field `approved` in any stage
|
|
212
1172
|
for (const [stage, val] of Object.entries(approvals)) {
|
|
213
1173
|
if (isLegacyApprovalObject(val)) {
|
|
214
1174
|
return {
|
|
215
1175
|
valid: false,
|
|
1176
|
+
absent: false,
|
|
216
1177
|
legacy: true,
|
|
217
1178
|
error: `Legacy approval state at approvals.${stage}.approved. ${LEGACY_APPROVAL_ERROR}`,
|
|
218
1179
|
};
|
|
219
1180
|
}
|
|
220
|
-
if (val
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
1181
|
+
if (!val || typeof val !== 'object' || Array.isArray(val)) {
|
|
1182
|
+
return { valid: false, absent: false, legacy: false, error: `approvals.${stage} must be an approval object` };
|
|
1183
|
+
}
|
|
1184
|
+
const keys = Object.keys(val).sort();
|
|
1185
|
+
if (!['agent_validated,generated', 'agent_validated,generated,user_approved'].includes(keys.join(','))) {
|
|
1186
|
+
return { valid: false, absent: false, legacy: false, error: `approvals.${stage} must contain generated and agent_validated; legacy user_approved is readable but has zero authority` };
|
|
1187
|
+
}
|
|
1188
|
+
if (keys.some((key) => typeof val[key] !== 'boolean')) {
|
|
1189
|
+
return { valid: false, absent: false, legacy: false, error: `approvals.${stage} fields must be boolean` };
|
|
225
1190
|
}
|
|
226
1191
|
}
|
|
227
1192
|
// Check schema version
|
|
228
|
-
const version = spec
|
|
1193
|
+
const version = Object.prototype.hasOwnProperty.call(spec, 'schema_version')
|
|
1194
|
+
? spec.schema_version
|
|
1195
|
+
: spec.approval_schema_version;
|
|
1196
|
+
if (version !== undefined && typeof version !== 'string') {
|
|
1197
|
+
return { valid: false, absent: false, legacy: false, error: 'schema_version must be a string' };
|
|
1198
|
+
}
|
|
229
1199
|
if (version && version !== APPROVAL_SCHEMA_VERSION) {
|
|
230
|
-
return { valid: false, legacy: false, error: `Unsupported schema_version ${version}, expected ${APPROVAL_SCHEMA_VERSION}` };
|
|
1200
|
+
return { valid: false, absent: false, legacy: false, error: `Unsupported schema_version ${version}, expected ${APPROVAL_SCHEMA_VERSION}` };
|
|
231
1201
|
}
|
|
232
1202
|
if (!version && Object.keys(approvals).length > 0) {
|
|
233
1203
|
// No version but uses new schema - allow for backward compat but warn that version should be present
|
|
234
1204
|
// Fail closed only if legacy detected above; otherwise allow.
|
|
235
|
-
return { valid: true, legacy: false, warning: `Missing schema_version, expected ${APPROVAL_SCHEMA_VERSION}` };
|
|
1205
|
+
return { valid: true, absent: false, legacy: false, warning: `Missing schema_version, expected ${APPROVAL_SCHEMA_VERSION}` };
|
|
236
1206
|
}
|
|
237
|
-
return { valid: true, legacy: false };
|
|
1207
|
+
return { valid: true, absent: false, legacy: false };
|
|
238
1208
|
}
|
|
239
1209
|
|
|
240
1210
|
function approvalState(state = {}) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
1211
|
+
if (!state || typeof state !== 'object' || Array.isArray(state)) {
|
|
1212
|
+
throw new TypeError('approval state must be an object');
|
|
1213
|
+
}
|
|
1214
|
+
if (Object.prototype.hasOwnProperty.call(state, 'approvals')) {
|
|
1215
|
+
const validation = validateApprovalSchema(state);
|
|
1216
|
+
if (!validation.valid) throw new Error(validation.error);
|
|
1217
|
+
return {
|
|
1218
|
+
generated: false,
|
|
1219
|
+
agent_validated: false,
|
|
1220
|
+
ready: false,
|
|
1221
|
+
present: true,
|
|
1222
|
+
schema_version: APPROVAL_SCHEMA_VERSION,
|
|
1223
|
+
legacy: false,
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
if (isLegacyApprovalObject(state)) throw new Error(LEGACY_APPROVAL_ERROR);
|
|
1227
|
+
|
|
1228
|
+
const version = Object.prototype.hasOwnProperty.call(state, 'schema_version')
|
|
1229
|
+
? state.schema_version
|
|
1230
|
+
: state.approval_schema_version;
|
|
1231
|
+
if (version !== undefined && (typeof version !== 'string' || version !== APPROVAL_SCHEMA_VERSION)) {
|
|
1232
|
+
throw new Error(`Unsupported schema_version ${String(version)}, expected ${APPROVAL_SCHEMA_VERSION}`);
|
|
249
1233
|
}
|
|
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);
|
|
1234
|
+
|
|
1235
|
+
const known = ['generated', 'agent_validated', 'user_approved'];
|
|
1236
|
+
const present = known.some((key) => Object.prototype.hasOwnProperty.call(state, key));
|
|
1237
|
+
const unknown = Object.keys(state).filter((key) => !known.includes(key) && key !== 'schema_version' && key !== 'approval_schema_version');
|
|
1238
|
+
if (unknown.length > 0) throw new TypeError(`malformed approval state fields: ${unknown.join(', ')}`);
|
|
1239
|
+
for (const key of known) {
|
|
1240
|
+
if (Object.prototype.hasOwnProperty.call(state, key) && typeof state[key] !== 'boolean') {
|
|
1241
|
+
throw new TypeError(`approval state field ${key} must be boolean`);
|
|
1242
|
+
}
|
|
275
1243
|
}
|
|
276
1244
|
const result = {
|
|
277
|
-
generated:
|
|
278
|
-
agent_validated:
|
|
279
|
-
|
|
1245
|
+
generated: state.generated === true,
|
|
1246
|
+
agent_validated: state.agent_validated === true,
|
|
1247
|
+
};
|
|
1248
|
+
return {
|
|
1249
|
+
...result,
|
|
1250
|
+
ready: result.generated && result.agent_validated,
|
|
1251
|
+
present,
|
|
1252
|
+
schema_version: APPROVAL_SCHEMA_VERSION,
|
|
1253
|
+
legacy_user_approved: state.user_approved === true,
|
|
280
1254
|
};
|
|
281
|
-
return { ...result, ready: result.generated && result.agent_validated && result.user_approved, schema_version: APPROVAL_SCHEMA_VERSION };
|
|
282
1255
|
}
|
|
283
|
-
|
|
284
1256
|
function assertVerdict(verdict) {
|
|
285
|
-
if (!
|
|
286
|
-
throw new TypeError(`Unsupported
|
|
1257
|
+
if (!CANONICAL_VERDICTS.includes(verdict)) {
|
|
1258
|
+
throw new TypeError(`Unsupported canonical verdict: ${String(verdict)}`);
|
|
287
1259
|
}
|
|
288
1260
|
return verdict;
|
|
289
1261
|
}
|
|
@@ -310,10 +1282,9 @@ function delegationPlan({ tier, lane = null, mode = 'full-spec', taskCount = 1 }
|
|
|
310
1282
|
const policy = lanePolicy(lane);
|
|
311
1283
|
return {
|
|
312
1284
|
lane: policy.lane,
|
|
313
|
-
executionTier: policy.executionTier,
|
|
314
|
-
tier: tier || null,
|
|
315
1285
|
mode,
|
|
316
|
-
|
|
1286
|
+
proof_obligations: [...policy.proof_obligations],
|
|
1287
|
+
actor_needs: policy.actor_needs.map((need) => ({ ...need })),
|
|
317
1288
|
qualityGate: policy.qualityGate,
|
|
318
1289
|
shipPoint: policy.shipPoint,
|
|
319
1290
|
};
|
|
@@ -325,65 +1296,477 @@ function delegationPlan({ tier, lane = null, mode = 'full-spec', taskCount = 1 }
|
|
|
325
1296
|
throw new RangeError('taskCount must be a positive integer');
|
|
326
1297
|
}
|
|
327
1298
|
|
|
328
|
-
|
|
329
|
-
|
|
1299
|
+
// Legacy tier input is read-only compatibility. It is translated once to
|
|
1300
|
+
// the canonical lane contract and never emits a new tier authority or an
|
|
1301
|
+
// agent sequence.
|
|
1302
|
+
const legacyLane = LEGACY_TIER_LANES[tier];
|
|
1303
|
+
const policy = lanePolicy(legacyLane);
|
|
1304
|
+
return {
|
|
1305
|
+
tier,
|
|
1306
|
+
legacy: true,
|
|
1307
|
+
lane: policy.lane,
|
|
1308
|
+
mode,
|
|
1309
|
+
proof_obligations: [...policy.proof_obligations],
|
|
1310
|
+
actor_needs: policy.actor_needs.map((need) => ({ ...need })),
|
|
1311
|
+
qualityGate: policy.qualityGate,
|
|
1312
|
+
shipPoint: policy.shipPoint,
|
|
1313
|
+
taskCount,
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
function normalizeVerdict(verdict) {
|
|
1318
|
+
if (CANONICAL_VERDICTS.includes(verdict)) return { verdict, diagnostic: null };
|
|
1319
|
+
if (LEGACY_DIAGNOSTIC_VERDICTS.includes(verdict)) return { verdict: 'BLOCKED', diagnostic: verdict };
|
|
1320
|
+
throw new TypeError(`Unsupported canonical verdict: ${String(verdict)}`);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
function unfinishedVerdictDecision(verdict, context = {}, diagnostic = null) {
|
|
1324
|
+
const action = diagnostic === 'PARTIAL'
|
|
1325
|
+
? 'continue-verification'
|
|
1326
|
+
: diagnostic === 'NO_TESTS'
|
|
1327
|
+
? 'configure-or-run-tests'
|
|
1328
|
+
: verdict === 'FAIL'
|
|
1329
|
+
? 'fix-and-rerun'
|
|
1330
|
+
: 'stop';
|
|
1331
|
+
return {
|
|
1332
|
+
verdict,
|
|
1333
|
+
...(diagnostic ? { diagnostic } : {}),
|
|
1334
|
+
action,
|
|
1335
|
+
completion: 'unfinished',
|
|
1336
|
+
terminal: verdict !== 'FAIL',
|
|
1337
|
+
unfinished: true,
|
|
1338
|
+
retry: verdict === 'FAIL',
|
|
1339
|
+
blocker: context.blocker || (
|
|
1340
|
+
diagnostic === 'PARTIAL'
|
|
1341
|
+
? 'verification is partial; required proof remains unfinished'
|
|
1342
|
+
: diagnostic === 'NO_TESTS'
|
|
1343
|
+
? 'no tests were executed'
|
|
1344
|
+
: verdict === 'FAIL'
|
|
1345
|
+
? 'verification returned FAIL'
|
|
1346
|
+
: 'verification is blocked; resolve the blocker before retrying'
|
|
1347
|
+
),
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
function adaptVerdict(verdict, context = {}) {
|
|
1352
|
+
const normalized = normalizeVerdict(verdict);
|
|
1353
|
+
if (normalized.verdict === 'PASS' || normalized.verdict === 'PASS_WITH_WARNINGS') {
|
|
1354
|
+
return {
|
|
1355
|
+
verdict: normalized.verdict,
|
|
1356
|
+
...(normalized.diagnostic ? { diagnostic: normalized.diagnostic } : {}),
|
|
1357
|
+
action: normalized.verdict === 'PASS_WITH_WARNINGS' ? 'review-passed-with-warnings' : 'review-passed',
|
|
1358
|
+
completion: 'unfinished',
|
|
1359
|
+
terminal: true,
|
|
1360
|
+
unfinished: true,
|
|
1361
|
+
retry: false,
|
|
1362
|
+
blocker: context.blocker || 'completion decision requires canonical execution proof and workflow obligations',
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
return unfinishedVerdictDecision(normalized.verdict, context, normalized.diagnostic);
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
function workflowPolicyObligations(context) {
|
|
1369
|
+
if (!context || typeof context !== 'object' || Array.isArray(context)) return null;
|
|
1370
|
+
const policy = hasOwn(context, 'workflow_policy')
|
|
1371
|
+
? context.workflow_policy
|
|
1372
|
+
: hasOwn(context, 'workflowPolicy')
|
|
1373
|
+
? context.workflowPolicy
|
|
1374
|
+
: hasOwn(context, 'policy')
|
|
1375
|
+
? context.policy
|
|
1376
|
+
: context.task_context && typeof context.task_context === 'object'
|
|
1377
|
+
? (hasOwn(context.task_context, 'workflow_policy')
|
|
1378
|
+
? context.task_context.workflow_policy
|
|
1379
|
+
: hasOwn(context.task_context, 'workflowPolicy')
|
|
1380
|
+
? context.task_context.workflowPolicy
|
|
1381
|
+
: null)
|
|
1382
|
+
: null;
|
|
1383
|
+
if (!validateWorkflowPolicySnapshot(policy).valid) return null;
|
|
1384
|
+
return [...readWorkflowPolicySnapshot({ workflow_policy: policy }).proof_obligations];
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
function executionReceiptFromContext(context) {
|
|
1388
|
+
const candidates = [
|
|
1389
|
+
context?.execution_receipt,
|
|
1390
|
+
context?.executionReceipt,
|
|
1391
|
+
context?.canonical_execution_receipt,
|
|
1392
|
+
context?.canonicalExecutionReceipt,
|
|
1393
|
+
context?.execution?.receipt,
|
|
1394
|
+
context?.execution?.body,
|
|
1395
|
+
context?.receipt,
|
|
1396
|
+
];
|
|
1397
|
+
return candidates.find((value) => typeof value === 'string' && value.trim() !== '') || null;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
function proofContainers(context) {
|
|
1401
|
+
return [
|
|
1402
|
+
context?.proofs,
|
|
1403
|
+
context?.workflow_proofs,
|
|
1404
|
+
context?.workflowProofs,
|
|
1405
|
+
context?.evidence,
|
|
1406
|
+
context?.obligation_evidence,
|
|
1407
|
+
context?.obligationEvidence,
|
|
1408
|
+
].filter((value) => value && typeof value === 'object' && !Array.isArray(value));
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
function concreteEvidence(value) {
|
|
1412
|
+
if (typeof value === 'string') return value.trim() !== '' && !isPlaceholderToken(value);
|
|
1413
|
+
if (Array.isArray(value)) return value.some(concreteEvidence);
|
|
1414
|
+
if (!value || typeof value !== 'object') return false;
|
|
1415
|
+
return ['evidence', 'proof', 'receipt', 'report', 'summary']
|
|
1416
|
+
.some((key) => concreteEvidence(value[key]));
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
function evidenceForObligation(context, obligation) {
|
|
1420
|
+
for (const container of proofContainers(context)) {
|
|
1421
|
+
if (Object.prototype.hasOwnProperty.call(container, obligation) && concreteEvidence(container[obligation])) {
|
|
1422
|
+
return container[obligation];
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
const aliases = {
|
|
1426
|
+
needsInspection: ['inspection', 'inspectionEvidence', 'inspection_evidence'],
|
|
1427
|
+
needsDurableTaskState: ['durableTaskState', 'durable_task_state', 'taskState', 'task_state'],
|
|
1428
|
+
needsResearchGrounding: ['research', 'researchGrounding', 'research_grounding'],
|
|
1429
|
+
};
|
|
1430
|
+
for (const key of aliases[obligation] || []) {
|
|
1431
|
+
if (concreteEvidence(context?.[key])) return context[key];
|
|
1432
|
+
}
|
|
1433
|
+
return null;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
function independentAuditEvidence(context) {
|
|
1437
|
+
const runtimeContext = runtimeContextFromContext(context);
|
|
1438
|
+
const expectedProvenance = runtimeContext
|
|
1439
|
+
? { base: runtimeContext.base, head: runtimeContext.head }
|
|
1440
|
+
: null;
|
|
1441
|
+
const candidates = [
|
|
1442
|
+
...proofContainers(context).map((container) => container.needsIndependentAudit),
|
|
1443
|
+
context?.independentAudit,
|
|
1444
|
+
context?.independent_audit,
|
|
1445
|
+
context?.audit,
|
|
1446
|
+
];
|
|
1447
|
+
return candidates.find((value) => {
|
|
1448
|
+
if (Array.isArray(value)) return false;
|
|
1449
|
+
return validateIndependentAuditEvidence(value, expectedProvenance, runtimeContext).valid;
|
|
1450
|
+
}) || null;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
function validateIndependentAuditEvidence(value, expectedProvenance, runtimeContext) {
|
|
1454
|
+
const errors = [];
|
|
1455
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1456
|
+
return { valid: false, errors: ['audit must be an object'] };
|
|
1457
|
+
}
|
|
1458
|
+
if (JSON.stringify(Object.keys(value).sort()) !== JSON.stringify([...INDEPENDENT_AUDIT_FIELDS].sort())) {
|
|
1459
|
+
errors.push(`audit fields must be exactly ${INDEPENDENT_AUDIT_FIELDS.join(', ')}`);
|
|
1460
|
+
}
|
|
1461
|
+
if (value.schema_version !== INDEPENDENT_AUDIT_SCHEMA_VERSION) {
|
|
1462
|
+
errors.push(`audit.schema_version must be "${INDEPENDENT_AUDIT_SCHEMA_VERSION}"`);
|
|
1463
|
+
}
|
|
1464
|
+
for (const field of ['reviewer_session_id', 'implementation_session_id']) {
|
|
1465
|
+
if (typeof value[field] !== 'string' || isPlaceholderToken(value[field])) {
|
|
1466
|
+
errors.push(`audit.${field} must be concrete`);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
if (typeof value.reviewer_session_id === 'string'
|
|
1470
|
+
&& typeof value.implementation_session_id === 'string'
|
|
1471
|
+
&& value.reviewer_session_id.trim() === value.implementation_session_id.trim()) {
|
|
1472
|
+
errors.push('audit reviewer and implementation sessions must be distinct');
|
|
1473
|
+
}
|
|
1474
|
+
if (typeof value.evidence !== 'string' || isPlaceholderToken(value.evidence)) {
|
|
1475
|
+
errors.push('audit.evidence must be concrete');
|
|
1476
|
+
}
|
|
1477
|
+
if (value.verdict !== 'PASS') errors.push('audit.verdict must be literal PASS');
|
|
1478
|
+
|
|
1479
|
+
if (!PROVENANCE.isTrustedRuntimeContext(runtimeContext)) {
|
|
1480
|
+
errors.push('audit runtime provenance/session context is unavailable');
|
|
1481
|
+
} else if (typeof value.implementation_session_id === 'string'
|
|
1482
|
+
&& value.implementation_session_id.trim() !== runtimeContext.runtime_session) {
|
|
1483
|
+
errors.push('audit implementation session does not match the runtime session');
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
if (!value.expected_provenance
|
|
1487
|
+
|| typeof value.expected_provenance !== 'object'
|
|
1488
|
+
|| Array.isArray(value.expected_provenance)
|
|
1489
|
+
|| JSON.stringify(Object.keys(value.expected_provenance).sort()) !== JSON.stringify(['base', 'head'])) {
|
|
1490
|
+
errors.push('audit.expected_provenance must contain exactly base and head');
|
|
1491
|
+
}
|
|
1492
|
+
const actualBinding = expectedProvenanceFromSource(value.expected_provenance);
|
|
1493
|
+
const runtimeBinding = PROVENANCE.isTrustedRuntimeContext(runtimeContext)
|
|
1494
|
+
? { base: runtimeContext.base, head: runtimeContext.head }
|
|
1495
|
+
: null;
|
|
1496
|
+
if (!actualBinding
|
|
1497
|
+
|| !isConcreteProvenanceValue(actualBinding.base)
|
|
1498
|
+
|| !isConcreteProvenanceValue(actualBinding.head)
|
|
1499
|
+
|| !expectedProvenance
|
|
1500
|
+
|| !isConcreteProvenanceValue(expectedProvenance.base)
|
|
1501
|
+
|| !isConcreteProvenanceValue(expectedProvenance.head)
|
|
1502
|
+
|| normalizeProvenanceValue(actualBinding.base) !== normalizeProvenanceValue(expectedProvenance.base)
|
|
1503
|
+
|| normalizeProvenanceValue(actualBinding.head) !== normalizeProvenanceValue(expectedProvenance.head)) {
|
|
1504
|
+
errors.push('audit expected Base and Head binding does not match the runtime binding');
|
|
1505
|
+
}
|
|
1506
|
+
if (runtimeBinding
|
|
1507
|
+
&& (normalizeProvenanceValue(actualBinding?.base) !== runtimeBinding.base
|
|
1508
|
+
|| normalizeProvenanceValue(actualBinding?.head) !== runtimeBinding.head)) {
|
|
1509
|
+
errors.push('audit expected Base and Head do not match the freshly derived runtime binding');
|
|
1510
|
+
}
|
|
1511
|
+
return { valid: errors.length === 0, errors };
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
function completionReceiptOptions(context) {
|
|
1515
|
+
const task = context?.task_context || context?.taskContext || context?.task || context;
|
|
1516
|
+
const runtimeContext = runtimeContextFromContext(context);
|
|
1517
|
+
const derived = receiptValidatorOptions(task && typeof task === 'object' ? task : {}, {
|
|
1518
|
+
requireProvenanceBinding: true,
|
|
1519
|
+
runtimeContext,
|
|
1520
|
+
});
|
|
1521
|
+
const supplied = context?.receipt_options || context?.receiptOptions;
|
|
1522
|
+
if (!supplied || typeof supplied !== 'object' || Array.isArray(supplied)) return derived;
|
|
1523
|
+
return {
|
|
1524
|
+
...derived,
|
|
1525
|
+
...supplied,
|
|
1526
|
+
artifactDeclarationValid: derived.artifactDeclarationValid === false
|
|
1527
|
+
? false
|
|
1528
|
+
: supplied.artifactDeclarationValid !== false,
|
|
1529
|
+
requireArtifactHash: derived.requireArtifactHash === true || supplied.requireArtifactHash === true,
|
|
1530
|
+
artifactPaths: derived.artifactPaths.length > 0 ? derived.artifactPaths : (Array.isArray(supplied.artifactPaths) ? supplied.artifactPaths : []),
|
|
1531
|
+
artifactRoot: derived.artifactRoot,
|
|
1532
|
+
verifyArtifactBytes: derived.verifyArtifactBytes === true || supplied.verifyArtifactBytes === true,
|
|
1533
|
+
expectedProvenance: derived.expectedProvenance,
|
|
1534
|
+
requireProvenanceBinding: true,
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
function runtimeBoundProofsFromSpec(spec, runtimeContext) {
|
|
1539
|
+
// Spec files are worker-writable: plain proof strings are never execution
|
|
1540
|
+
// evidence. Only canonical receipts bound to this fresh runtime may enter
|
|
1541
|
+
// the completion decision; independent audit remains a strict object schema.
|
|
1542
|
+
const proofSources = [
|
|
1543
|
+
'completion_proofs',
|
|
1544
|
+
'completionProofs',
|
|
1545
|
+
'workflow_proofs',
|
|
1546
|
+
'workflowProofs',
|
|
1547
|
+
'obligation_evidence',
|
|
1548
|
+
'obligationEvidence',
|
|
1549
|
+
'proofs',
|
|
1550
|
+
];
|
|
1551
|
+
const sourceKey = proofSources.find((key) => spec && spec[key] && typeof spec[key] === 'object' && !Array.isArray(spec[key]));
|
|
1552
|
+
if (!sourceKey) return null;
|
|
1553
|
+
const source = spec[sourceKey];
|
|
1554
|
+
const trusted = {};
|
|
1555
|
+
const receiptOptions = receiptValidatorOptions({}, {
|
|
1556
|
+
runtimeContext,
|
|
1557
|
+
requireProvenanceBinding: true,
|
|
1558
|
+
});
|
|
1559
|
+
for (const [obligation, value] of Object.entries(source)) {
|
|
1560
|
+
if (obligation === 'needsIndependentAudit') {
|
|
1561
|
+
trusted[obligation] = value;
|
|
1562
|
+
continue;
|
|
1563
|
+
}
|
|
1564
|
+
const candidates = value && typeof value === 'object' && !Array.isArray(value)
|
|
1565
|
+
? [value.receipt, value.execution_receipt, value.executionReceipt]
|
|
1566
|
+
: [];
|
|
1567
|
+
const receipt = candidates.find((candidate) => (
|
|
1568
|
+
typeof candidate === 'string'
|
|
1569
|
+
&& validateCanonicalReceipt(candidate, receiptOptions).length === 0
|
|
1570
|
+
));
|
|
1571
|
+
if (receipt) trusted[obligation] = { receipt };
|
|
330
1572
|
}
|
|
331
|
-
|
|
1573
|
+
return trusted;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
function completionDecisionForSpec(spec, {
|
|
1577
|
+
runtimeContext,
|
|
1578
|
+
executionReceipt,
|
|
1579
|
+
taskContext,
|
|
1580
|
+
} = {}) {
|
|
1581
|
+
const policy = spec && typeof spec === 'object' ? spec.workflow_policy : null;
|
|
1582
|
+
const policyView = policy ? readWorkflowPolicySnapshot({ workflow_policy: policy }) : null;
|
|
1583
|
+
const context = {
|
|
1584
|
+
workflow_policy: policy,
|
|
1585
|
+
runtime_context: runtimeContext,
|
|
1586
|
+
execution_receipt: executionReceipt,
|
|
1587
|
+
task_context: taskContext,
|
|
1588
|
+
};
|
|
1589
|
+
const trustedProofs = runtimeBoundProofsFromSpec(spec, runtimeContext);
|
|
1590
|
+
if (trustedProofs) context.proofs = trustedProofs;
|
|
1591
|
+
if (policyView && policyView.lane === 'Direct') {
|
|
332
1592
|
return {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
1593
|
+
completion: 'not_applicable',
|
|
1594
|
+
unfinished: false,
|
|
1595
|
+
missingProof: [],
|
|
1596
|
+
blocker: null,
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
return completionDecision('PASS', context);
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
function completionDecision(verdict, context = {}) {
|
|
1603
|
+
if (!context || typeof context !== 'object' || Array.isArray(context)) context = {};
|
|
1604
|
+
const normalized = normalizeVerdict(verdict);
|
|
1605
|
+
if (normalized.verdict === 'PASS_WITH_WARNINGS') {
|
|
1606
|
+
return {
|
|
1607
|
+
verdict: normalized.verdict,
|
|
1608
|
+
action: 'review-passed-with-warnings',
|
|
1609
|
+
completion: 'unfinished',
|
|
1610
|
+
terminal: true,
|
|
1611
|
+
unfinished: true,
|
|
1612
|
+
retry: false,
|
|
1613
|
+
blocker: context.blocker || 'PASS_WITH_WARNINGS is not a completion verdict; literal PASS is required',
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
if (normalized.verdict !== 'PASS') {
|
|
1617
|
+
return unfinishedVerdictDecision(normalized.verdict, context, normalized.diagnostic);
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
const runtimeContext = runtimeContextFromContext(context);
|
|
1621
|
+
if (!runtimeContext) {
|
|
1622
|
+
return {
|
|
1623
|
+
verdict: normalized.verdict,
|
|
1624
|
+
action: 'blocked-runtime-provenance',
|
|
1625
|
+
completion: 'unfinished',
|
|
1626
|
+
terminal: true,
|
|
1627
|
+
unfinished: true,
|
|
1628
|
+
retry: false,
|
|
1629
|
+
blocker: 'completion blocked: runtime-derived provenance and session context are unavailable or stale',
|
|
1630
|
+
missingProof: ['runtime_provenance'],
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
const decisionContext = { ...context, runtime_context: runtimeContext };
|
|
1634
|
+
|
|
1635
|
+
const missing = [];
|
|
1636
|
+
const receipt = executionReceiptFromContext(decisionContext);
|
|
1637
|
+
const receiptFailures = receipt
|
|
1638
|
+
? validateCanonicalReceipt(receipt, completionReceiptOptions(decisionContext))
|
|
1639
|
+
: ['verification_state'];
|
|
1640
|
+
if (receiptFailures.length > 0) missing.push(`execution_receipt:${receiptFailures.join(',')}`);
|
|
1641
|
+
|
|
1642
|
+
const obligations = workflowPolicyObligations(decisionContext);
|
|
1643
|
+
if (!obligations) {
|
|
1644
|
+
missing.push('workflow_policy.proof_obligations');
|
|
1645
|
+
} else {
|
|
1646
|
+
for (const obligation of obligations) {
|
|
1647
|
+
if (obligation === 'needsExecutionProof') continue;
|
|
1648
|
+
if (obligation === 'needsIndependentAudit') {
|
|
1649
|
+
if (!independentAuditEvidence(decisionContext)) missing.push(obligation);
|
|
1650
|
+
continue;
|
|
1651
|
+
}
|
|
1652
|
+
if (!evidenceForObligation(decisionContext, obligation)) missing.push(obligation);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
if (missing.length > 0) {
|
|
1656
|
+
return {
|
|
1657
|
+
verdict: normalized.verdict,
|
|
1658
|
+
...(normalized.diagnostic ? { diagnostic: normalized.diagnostic } : {}),
|
|
1659
|
+
action: 'await-required-proof',
|
|
1660
|
+
completion: 'unfinished',
|
|
1661
|
+
terminal: true,
|
|
1662
|
+
unfinished: true,
|
|
1663
|
+
retry: false,
|
|
1664
|
+
blocker: decisionContext.blocker || `completion blocked: missing ${missing.join(', ')}`,
|
|
1665
|
+
missingProof: missing,
|
|
338
1666
|
};
|
|
339
1667
|
}
|
|
340
1668
|
return {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
1669
|
+
verdict: normalized.verdict,
|
|
1670
|
+
action: 'complete',
|
|
1671
|
+
completion: 'complete',
|
|
1672
|
+
terminal: true,
|
|
1673
|
+
unfinished: false,
|
|
1674
|
+
retry: false,
|
|
1675
|
+
blocker: null,
|
|
346
1676
|
};
|
|
347
1677
|
}
|
|
348
1678
|
|
|
349
|
-
function consumeReviewVerdict(verdict) {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
1679
|
+
function consumeReviewVerdict(verdict, context = {}) {
|
|
1680
|
+
const result = adaptVerdict(verdict, context);
|
|
1681
|
+
return {
|
|
1682
|
+
...result,
|
|
1683
|
+
source: 'review',
|
|
1684
|
+
completion: 'unfinished',
|
|
1685
|
+
unfinished: true,
|
|
1686
|
+
createsCompletion: false,
|
|
1687
|
+
completionDecisionRequired: true,
|
|
1688
|
+
action: result.verdict === 'PASS' || result.verdict === 'PASS_WITH_WARNINGS'
|
|
1689
|
+
? 'review-result-only'
|
|
1690
|
+
: result.action,
|
|
1691
|
+
blocker: result.verdict === 'PASS' || result.verdict === 'PASS_WITH_WARNINGS'
|
|
1692
|
+
? context.blocker || 'review verdict is not completion proof'
|
|
1693
|
+
: result.blocker,
|
|
1694
|
+
};
|
|
354
1695
|
}
|
|
355
1696
|
|
|
356
1697
|
function isFlashUnverified(task) {
|
|
357
1698
|
return task?.status === 'in_progress' && task.receipt === 'FLASH_UNVERIFIED';
|
|
358
1699
|
}
|
|
359
1700
|
|
|
1701
|
+
function isCanonicalFlashUnverified(task) {
|
|
1702
|
+
return isFlashUnverified(task)
|
|
1703
|
+
&& task.dependencyBlocked === true
|
|
1704
|
+
&& task.unblocks === false
|
|
1705
|
+
&& typeof task.blocker === 'string'
|
|
1706
|
+
&& !isPlaceholderToken(task.blocker)
|
|
1707
|
+
&& !FLASH_PROMOTION_FIELDS.some((field) => hasOwn(task, field));
|
|
1708
|
+
}
|
|
1709
|
+
|
|
360
1710
|
function isStaleFlashDone(task) {
|
|
361
1711
|
return task?.status === 'done' && task.receipt === 'FLASH_UNVERIFIED';
|
|
362
1712
|
}
|
|
363
1713
|
|
|
364
|
-
|
|
365
|
-
|
|
1714
|
+
const SYNC_FINALIZE_CAPABILITY = Symbol('sync-finalize-capability');
|
|
1715
|
+
const trustedFlashPromotions = new WeakMap();
|
|
1716
|
+
|
|
1717
|
+
function flashRuntimeContext(value) {
|
|
1718
|
+
if (!PROVENANCE.isTrustedRuntimeContext(value)) return null;
|
|
1719
|
+
try { return PROVENANCE.recomputeRuntimeContext(value); } catch (_) { return null; }
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
function evaluateFlashPromotion(task, verdict, proof = 'Verification: PASS', capability = null, runtimeContext) {
|
|
1723
|
+
const trustedRuntime = flashRuntimeContext(runtimeContext);
|
|
1724
|
+
if (!trustedRuntime) {
|
|
1725
|
+
return blockedFlashSync(task, 'flash promotion blocked: runtime-derived provenance and session context are unavailable or stale');
|
|
1726
|
+
}
|
|
1727
|
+
const normalized = normalizeVerdict(verdict);
|
|
366
1728
|
if (!isFlashUnverified(task)) return task;
|
|
367
1729
|
|
|
368
|
-
if (verdict !== 'PASS') {
|
|
1730
|
+
if (normalized.verdict !== 'PASS') {
|
|
369
1731
|
return {
|
|
370
1732
|
...task,
|
|
371
1733
|
status: 'in_progress',
|
|
372
1734
|
receipt: 'FLASH_UNVERIFIED',
|
|
373
|
-
blocker:
|
|
1735
|
+
blocker: normalized.diagnostic === 'NO_TESTS'
|
|
374
1736
|
? 'awaiting test proof from /hapo:test <feature>'
|
|
375
|
-
:
|
|
1737
|
+
: normalized.diagnostic === 'PARTIAL'
|
|
1738
|
+
? 'verification is partial; awaiting complete test proof'
|
|
1739
|
+
: `verification returned ${normalized.verdict}`,
|
|
376
1740
|
dependencyBlocked: true,
|
|
377
1741
|
unblocks: false,
|
|
378
1742
|
readyForSync: false,
|
|
1743
|
+
flashTransition: 'unverified',
|
|
1744
|
+
promotionReceipt: null,
|
|
379
1745
|
};
|
|
380
1746
|
}
|
|
381
1747
|
|
|
382
|
-
if (typeof proof !== 'string'
|
|
1748
|
+
if (typeof proof !== 'string') {
|
|
383
1749
|
throw new TypeError('PASS promotion requires a concrete Verification: PASS receipt');
|
|
384
1750
|
}
|
|
1751
|
+
const failures = validateCanonicalReceipt(proof, receiptValidatorOptions(task, {
|
|
1752
|
+
requireProvenanceBinding: true,
|
|
1753
|
+
runtimeContext: trustedRuntime,
|
|
1754
|
+
}));
|
|
1755
|
+
if (failures.length > 0) {
|
|
1756
|
+
return {
|
|
1757
|
+
...task,
|
|
1758
|
+
status: 'in_progress',
|
|
1759
|
+
receipt: 'FLASH_UNVERIFIED',
|
|
1760
|
+
blocker: `canonical receipt required for promotion: missing ${failures.join(', ')}`,
|
|
1761
|
+
dependencyBlocked: true,
|
|
1762
|
+
unblocks: false,
|
|
1763
|
+
readyForSync: false,
|
|
1764
|
+
flashTransition: 'unverified',
|
|
1765
|
+
promotionReceipt: null,
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
385
1768
|
|
|
386
|
-
|
|
1769
|
+
const promoted = {
|
|
387
1770
|
...task,
|
|
388
1771
|
status: 'in_progress',
|
|
389
1772
|
receipt: proof.trim(),
|
|
@@ -391,24 +1774,108 @@ function promoteFlashTask(task, verdict, proof = 'Verification: PASS') {
|
|
|
391
1774
|
dependencyBlocked: true,
|
|
392
1775
|
unblocks: false,
|
|
393
1776
|
readyForSync: true,
|
|
1777
|
+
flashTransition: 'promoted',
|
|
1778
|
+
promotionReceipt: proof.trim(),
|
|
394
1779
|
};
|
|
1780
|
+
if (capability === SYNC_FINALIZE_CAPABILITY) trustedFlashPromotions.set(promoted, capability);
|
|
1781
|
+
return promoted;
|
|
395
1782
|
}
|
|
396
1783
|
|
|
397
|
-
function
|
|
398
|
-
|
|
1784
|
+
function promoteFlashTask(task, verdict, proof = 'Verification: PASS', runtimeContext) {
|
|
1785
|
+
const candidate = runtimeContext
|
|
1786
|
+
|| task?.runtime_context
|
|
1787
|
+
|| task?.runtimeContext
|
|
1788
|
+
|| task?.receipt_binding?.runtimeContext
|
|
1789
|
+
|| task?.receiptBinding?.runtimeContext;
|
|
1790
|
+
return evaluateFlashPromotion(task, verdict, proof, null, candidate);
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
function finalizeFlashTaskInternal(task, operation, capability, runtimeContext) {
|
|
1794
|
+
if (operation !== 'sync-finalize' || capability !== SYNC_FINALIZE_CAPABILITY || trustedFlashPromotions.get(task) !== capability) {
|
|
1795
|
+
return blockedFlashSync(task, 'sync-finalize blocked: only trusted same-operation promotion may finalize');
|
|
1796
|
+
}
|
|
399
1797
|
if (
|
|
400
1798
|
task?.status !== 'in_progress'
|
|
401
|
-
|| !/^Verification:\s*PASS\b/.test(String(task.receipt || '').trim())
|
|
402
1799
|
|| task.readyForSync !== true
|
|
403
|
-
) return task;
|
|
1800
|
+
) return blockedFlashSync(task, 'sync-finalize blocked: trusted promotion state is incomplete');
|
|
1801
|
+
const receipt = String(task.receipt || '');
|
|
1802
|
+
if (task.flashTransition !== 'promoted' || task.promotionReceipt !== receipt) {
|
|
1803
|
+
return blockedFlashSync(task, 'sync-finalize blocked: trusted promotion state is incomplete');
|
|
1804
|
+
}
|
|
1805
|
+
const failures = validateCanonicalReceipt(receipt, receiptValidatorOptions(task, {
|
|
1806
|
+
requireProvenanceBinding: true,
|
|
1807
|
+
runtimeContext,
|
|
1808
|
+
}));
|
|
1809
|
+
if (failures.length > 0) {
|
|
1810
|
+
return blockedFlashSync(task, `sync-finalize blocked: canonical receipt required (${failures.join(', ')})`);
|
|
1811
|
+
}
|
|
404
1812
|
|
|
1813
|
+
trustedFlashPromotions.delete(task);
|
|
405
1814
|
return {
|
|
406
1815
|
...task,
|
|
407
1816
|
status: 'done',
|
|
408
1817
|
dependencyBlocked: false,
|
|
409
1818
|
unblocks: true,
|
|
410
1819
|
readyForSync: false,
|
|
1820
|
+
flashTransition: 'finalized',
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
function finalizeFlashTask(task, operation) {
|
|
1825
|
+
if (operation !== 'sync-finalize') return task;
|
|
1826
|
+
return blockedFlashSync(task, 'sync-finalize blocked: only syncFinalizeFlashTask owns finalization');
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
function blockedFlashSync(task, blocker) {
|
|
1830
|
+
return {
|
|
1831
|
+
...task,
|
|
1832
|
+
status: 'in_progress',
|
|
1833
|
+
receipt: 'FLASH_UNVERIFIED',
|
|
1834
|
+
dependencyBlocked: true,
|
|
1835
|
+
unblocks: false,
|
|
1836
|
+
readyForSync: false,
|
|
1837
|
+
flashTransition: 'unverified',
|
|
1838
|
+
promotionReceipt: null,
|
|
1839
|
+
blocker,
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
function syncFinalizeFlashTask(task, verdict, proof, runtimeContext) {
|
|
1844
|
+
if (!isCanonicalFlashUnverified(task)) {
|
|
1845
|
+
return blockedFlashSync(
|
|
1846
|
+
task,
|
|
1847
|
+
'sync-finalize blocked: current task must be FLASH_UNVERIFIED in the exact canonical stored state; caller promotion state is not trusted',
|
|
1848
|
+
);
|
|
1849
|
+
}
|
|
1850
|
+
if (verdict !== 'PASS') {
|
|
1851
|
+
return blockedFlashSync(task, 'sync-finalize blocked: explicit PASS verdict is required');
|
|
1852
|
+
}
|
|
1853
|
+
if (typeof proof !== 'string' || proof.trim() === '') {
|
|
1854
|
+
return blockedFlashSync(task, 'sync-finalize blocked: explicit canonical --proof is required');
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
// Treat all promotion fields from --task-json as untrusted. Rebuild the
|
|
1858
|
+
// unverified state, then derive promotion and finalization in this operation.
|
|
1859
|
+
const current = {
|
|
1860
|
+
...task,
|
|
1861
|
+
readyForSync: false,
|
|
1862
|
+
flashTransition: 'unverified',
|
|
1863
|
+
promotionReceipt: null,
|
|
1864
|
+
dependencyBlocked: true,
|
|
1865
|
+
unblocks: false,
|
|
411
1866
|
};
|
|
1867
|
+
const candidate = runtimeContext
|
|
1868
|
+
|| task?.runtime_context
|
|
1869
|
+
|| task?.runtimeContext
|
|
1870
|
+
|| task?.receipt_binding?.runtimeContext
|
|
1871
|
+
|| task?.receiptBinding?.runtimeContext;
|
|
1872
|
+
const trustedRuntime = flashRuntimeContext(candidate);
|
|
1873
|
+
if (!trustedRuntime) {
|
|
1874
|
+
return blockedFlashSync(task, 'sync-finalize blocked: runtime-derived provenance and session context are unavailable or stale');
|
|
1875
|
+
}
|
|
1876
|
+
const promoted = evaluateFlashPromotion(current, 'PASS', proof, SYNC_FINALIZE_CAPABILITY, trustedRuntime);
|
|
1877
|
+
if (promoted.readyForSync !== true) return promoted;
|
|
1878
|
+
return finalizeFlashTaskInternal(promoted, 'sync-finalize', SYNC_FINALIZE_CAPABILITY, trustedRuntime);
|
|
412
1879
|
}
|
|
413
1880
|
|
|
414
1881
|
function flashState(taskRegistry = {}) {
|
|
@@ -420,38 +1887,157 @@ function flashState(taskRegistry = {}) {
|
|
|
420
1887
|
function validateCanonicalReceipt(body, options = {}) {
|
|
421
1888
|
if (typeof body !== 'string') return ['verification_state'];
|
|
422
1889
|
const failures = [];
|
|
1890
|
+
const addFailure = (failure) => {
|
|
1891
|
+
if (!failures.includes(failure)) failures.push(failure);
|
|
1892
|
+
};
|
|
1893
|
+
// Fail-closed on placeholders anywhere — provenance, command, or artifact must not be templated
|
|
1894
|
+
if (/\{\{[^}]+\}\}/.test(body)) {
|
|
1895
|
+
addFailure('placeholder');
|
|
1896
|
+
// placeholder also implies missing concrete provenance/command; keep explicit failures for mapping
|
|
1897
|
+
}
|
|
1898
|
+
// Explicit failure outcome anywhere in body must make canonical validator fail (structured only)
|
|
1899
|
+
if (hasExplicitFailure(body)) {
|
|
1900
|
+
addFailure('verification_state');
|
|
1901
|
+
}
|
|
423
1902
|
// Unambiguous verification state: must be Verification: PASS exactly
|
|
424
1903
|
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
|
-
|
|
1904
|
+
addFailure('verification_state');
|
|
1905
|
+
}
|
|
1906
|
+
// Command must be present with non-empty concrete value on same line, not placeholder
|
|
1907
|
+
const cmdLine = body.split('\n').find((l) => /^\s*Command(?:\(s\))?\s*:/m.test(l)) || null;
|
|
1908
|
+
if (!cmdLine || !/^\s*Command(?:\(s\))?\s*:[ \t]*\S/m.test(cmdLine)) {
|
|
1909
|
+
addFailure('command');
|
|
1910
|
+
} else {
|
|
1911
|
+
const m = cmdLine.match(/^\s*Command(?:\(s\))?\s*:[ \t]*(.*)$/m);
|
|
1912
|
+
const val = m ? m[1].trim() : '';
|
|
1913
|
+
if (isPlaceholderToken(val)) {
|
|
1914
|
+
addFailure('command');
|
|
1915
|
+
} else if (/\{\{[^}]+\}\}/.test(cmdLine)) {
|
|
1916
|
+
addFailure('command');
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
// 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
|
|
1920
|
+
const lines = body.split('\n');
|
|
1921
|
+
const exitValues = [];
|
|
1922
|
+
const resultValues = [];
|
|
1923
|
+
for (const line of lines) {
|
|
1924
|
+
if (/^\s*Exit\s*:/i.test(line)) {
|
|
1925
|
+
const m = line.match(/^\s*Exit\s*:\s*(.*)$/i);
|
|
1926
|
+
exitValues.push(m ? m[1].trim() : '');
|
|
1927
|
+
} else if (/exit\s+code\s*[:=]/i.test(line)) {
|
|
1928
|
+
const m = line.match(/exit\s+code\s*[:=]\s*(.*)$/i);
|
|
1929
|
+
if (m) exitValues.push(m[1].trim());
|
|
1930
|
+
}
|
|
1931
|
+
if (/^\s*Result\s*:/i.test(line)) {
|
|
1932
|
+
const m = line.match(/^\s*Result\s*:\s*(.+?)\s*$/i);
|
|
1933
|
+
resultValues.push(m ? m[1].trim() : '');
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
if (exitValues.length === 0) {
|
|
1938
|
+
if (resultValues.length === 0 || resultValues.some((v) => v !== 'PASS')) {
|
|
1939
|
+
addFailure('exit_result');
|
|
1940
|
+
}
|
|
1941
|
+
} else {
|
|
1942
|
+
let exitOk = true;
|
|
1943
|
+
for (const v of exitValues) {
|
|
1944
|
+
if (!/^-?\d+$/.test(v)) { exitOk = false; break; }
|
|
1945
|
+
const n = Number.parseInt(v, 10);
|
|
1946
|
+
if (n !== 0) { exitOk = false; break; }
|
|
1947
|
+
}
|
|
1948
|
+
if (!exitOk) addFailure('exit_result');
|
|
1949
|
+
if (resultValues.length > 0 && resultValues.some((v) => v !== 'PASS')) {
|
|
1950
|
+
addFailure('exit_result');
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
// Provenance is either a concrete Base/Head pair or a concrete base_sha/head_sha pair.
|
|
1954
|
+
// Without expected values this validates schema only; it does not claim identity binding.
|
|
1955
|
+
const receiptLines = body.split('\n');
|
|
1956
|
+
const readField = (name) => receiptLines
|
|
1957
|
+
.filter((line) => new RegExp(`^\\s*${name}\\s*:`, 'i').test(line))
|
|
1958
|
+
.map((line) => line.replace(new RegExp(`^\\s*${name}\\s*:\\s*`, 'i'), '').trim());
|
|
1959
|
+
const baseValues = readField('Base');
|
|
1960
|
+
const headValues = readField('Head');
|
|
1961
|
+
const baseShaValues = readField('base_sha');
|
|
1962
|
+
const headShaValues = readField('head_sha');
|
|
1963
|
+
const labelStyle = baseValues.length > 0 || headValues.length > 0;
|
|
1964
|
+
const shaStyle = baseShaValues.length > 0 || headShaValues.length > 0;
|
|
1965
|
+
let actualProvenance = null;
|
|
1966
|
+
if (labelStyle === shaStyle || labelStyle && (baseShaValues.length > 0 || headShaValues.length > 0)) {
|
|
1967
|
+
addFailure('provenance');
|
|
1968
|
+
} else {
|
|
1969
|
+
const base = labelStyle ? baseValues : baseShaValues;
|
|
1970
|
+
const head = labelStyle ? headValues : headShaValues;
|
|
1971
|
+
if (base.length !== 1 || head.length !== 1 || !isConcreteProvenanceValue(base[0]) || !isConcreteProvenanceValue(head[0])) {
|
|
1972
|
+
addFailure('provenance');
|
|
1973
|
+
} else {
|
|
1974
|
+
actualProvenance = { base: normalizeProvenanceValue(base[0]), head: normalizeProvenanceValue(head[0]) };
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
const expected = options.expectedProvenance || options.expected_provenance || options.expected || null;
|
|
1978
|
+
const expectedBase = expected && typeof expected === 'object'
|
|
1979
|
+
? firstDefined(expected, ['base', 'Base', 'base_sha', 'baseSha', 'expectedBase', 'expected_base'])
|
|
1980
|
+
: options.expectedBase ?? options.expected_base ?? options.expectedBaseSha ?? options.expected_base_sha;
|
|
1981
|
+
const expectedHead = expected && typeof expected === 'object'
|
|
1982
|
+
? firstDefined(expected, ['head', 'Head', 'head_sha', 'headSha', 'expectedHead', 'expected_head'])
|
|
1983
|
+
: options.expectedHead ?? options.expected_head ?? options.expectedHeadSha ?? options.expected_head_sha;
|
|
1984
|
+
if (options.requireProvenanceBinding === true || expectedBase !== undefined || expectedHead !== undefined) {
|
|
1985
|
+
if (!actualProvenance
|
|
1986
|
+
|| !isConcreteProvenanceValue(expectedBase)
|
|
1987
|
+
|| !isConcreteProvenanceValue(expectedHead)
|
|
1988
|
+
|| normalizeProvenanceValue(expectedBase) !== actualProvenance.base
|
|
1989
|
+
|| normalizeProvenanceValue(expectedHead) !== actualProvenance.head) {
|
|
1990
|
+
addFailure('provenance');
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
if (options.artifactDeclarationValid === false) addFailure('artifact_declaration');
|
|
1995
|
+
const artifactDeclarations = parseArtifactDeclarations(body);
|
|
1996
|
+
for (const declaration of artifactDeclarations) {
|
|
1997
|
+
if (declaration.hashes.length === 0 || declaration.hashes.some((hash) => !/^[a-f0-9]{64}$/i.test(hash))) {
|
|
1998
|
+
addFailure('artifact_hash');
|
|
1999
|
+
}
|
|
2000
|
+
const declaredPaths = declaration.pathText
|
|
2001
|
+
.split(/\s*(?:\+|,|;|\band\b)\s*/i)
|
|
2002
|
+
.map((value) => value.trim())
|
|
2003
|
+
.filter(Boolean);
|
|
2004
|
+
if (declaredPaths.length > 1 && declaration.hashes.length !== declaredPaths.length) {
|
|
2005
|
+
addFailure('artifact_hash');
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
const artifactPaths = Array.isArray(options.artifactPaths) ? options.artifactPaths : [];
|
|
2009
|
+
if (options.requireArtifactHash) {
|
|
2010
|
+
if (artifactDeclarations.length === 0) addFailure('artifact_hash');
|
|
2011
|
+
for (const artifactPath of artifactPaths) {
|
|
2012
|
+
const matches = artifactDeclarations.filter((declaration) => {
|
|
2013
|
+
const candidates = declaration.pathText.split(/\s*(?:\+|,|;|\band\b)\s*/i).map((value) => value.trim()).filter(Boolean);
|
|
2014
|
+
return candidates.length === 1 && candidates.includes(artifactPath);
|
|
2015
|
+
});
|
|
2016
|
+
if (matches.length !== 1 || matches[0].hashes.length !== 1 || !/^[a-f0-9]{64}$/i.test(matches[0].hashes[0])) {
|
|
2017
|
+
addFailure('artifact_hash');
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
if (options.verifyArtifactBytes === true || typeof options.artifactRoot === 'string') {
|
|
2022
|
+
if (typeof options.artifactRoot !== 'string' || options.artifactRoot.trim() === '') {
|
|
2023
|
+
if (artifactDeclarations.length > 0 || options.requireArtifactHash) addFailure('artifact_hash');
|
|
2024
|
+
} else {
|
|
2025
|
+
for (const declaration of artifactDeclarations) {
|
|
2026
|
+
const declaredPaths = declaration.pathText
|
|
2027
|
+
.split(/\s*(?:\+|,|;|\band\b)\s*/i)
|
|
2028
|
+
.map((value) => value.trim())
|
|
2029
|
+
.filter(Boolean);
|
|
2030
|
+
if (declaredPaths.length === 0 || declaration.hashes.length !== declaredPaths.length) {
|
|
2031
|
+
addFailure('artifact_hash');
|
|
2032
|
+
continue;
|
|
2033
|
+
}
|
|
2034
|
+
for (let index = 0; index < declaredPaths.length; index += 1) {
|
|
2035
|
+
if (!hashValidatedArtifact(options.artifactRoot, declaredPaths[index], declaration.hashes[index])) {
|
|
2036
|
+
addFailure('artifact_hash');
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
455
2041
|
}
|
|
456
2042
|
return failures;
|
|
457
2043
|
}
|
|
@@ -466,9 +2052,16 @@ function parseCliArgs(argv) {
|
|
|
466
2052
|
action: null,
|
|
467
2053
|
lane: null,
|
|
468
2054
|
override: null,
|
|
469
|
-
proof:
|
|
2055
|
+
proof: null,
|
|
470
2056
|
userAuthorized: false,
|
|
471
2057
|
origin: null,
|
|
2058
|
+
projectRoot: null,
|
|
2059
|
+
specsRoot: null,
|
|
2060
|
+
specFile: null,
|
|
2061
|
+
featureName: null,
|
|
2062
|
+
runtimeSession: null,
|
|
2063
|
+
provenanceMode: null,
|
|
2064
|
+
runtimeContextInput: undefined,
|
|
472
2065
|
};
|
|
473
2066
|
const args = [...argv];
|
|
474
2067
|
for (let i = 0; i < args.length; i += 1) {
|
|
@@ -483,6 +2076,9 @@ function parseCliArgs(argv) {
|
|
|
483
2076
|
else if (arg === '--sync-finalize') options.action = 'sync-finalize';
|
|
484
2077
|
else if (arg === '--classify-lane') options.action = 'classify-lane';
|
|
485
2078
|
else if (arg === '--lane-policy') options.action = 'lane-policy';
|
|
2079
|
+
else if (arg === '--policy-snapshot') options.action = 'policy-snapshot';
|
|
2080
|
+
else if (arg === '--validate-policy') options.action = 'validate-policy';
|
|
2081
|
+
else if (arg === '--completion-decision') options.action = 'completion-decision';
|
|
486
2082
|
else if (arg === '--approval-state') options.action = 'approval-state';
|
|
487
2083
|
else if (arg === '--validate-approval') options.action = 'validate-approval';
|
|
488
2084
|
else if (arg === '--validate-receipt') options.action = 'validate-receipt';
|
|
@@ -498,6 +2094,16 @@ function parseCliArgs(argv) {
|
|
|
498
2094
|
} else if (arg === '--proof') {
|
|
499
2095
|
options.proof = args[++i];
|
|
500
2096
|
if (!options.proof) throw new Error('--proof requires a value');
|
|
2097
|
+
} else if (arg === '--project-root') options.projectRoot = args[++i];
|
|
2098
|
+
else if (arg === '--specs-root') options.specsRoot = args[++i];
|
|
2099
|
+
else if (arg === '--spec-file') options.specFile = args[++i];
|
|
2100
|
+
else if (arg === '--feature-name') options.featureName = args[++i];
|
|
2101
|
+
else if (arg === '--runtime-session' || arg === '--session') options.runtimeSession = args[++i];
|
|
2102
|
+
else if (arg === '--provenance-mode') options.provenanceMode = args[++i];
|
|
2103
|
+
else if (arg === '--runtime-context') {
|
|
2104
|
+
const raw = args[++i];
|
|
2105
|
+
if (!raw) throw new Error('--runtime-context requires a JSON value');
|
|
2106
|
+
options.runtimeContextInput = JSON.parse(raw);
|
|
501
2107
|
} else {
|
|
502
2108
|
throw new Error(`Unknown option: ${arg}`);
|
|
503
2109
|
}
|
|
@@ -505,6 +2111,23 @@ function parseCliArgs(argv) {
|
|
|
505
2111
|
return options;
|
|
506
2112
|
}
|
|
507
2113
|
|
|
2114
|
+
function cliRuntimeContext(options) {
|
|
2115
|
+
if (options.runtimeContextInput !== undefined) {
|
|
2116
|
+
throw new Error('--runtime-context JSON is not trusted; provide runtime identity flags');
|
|
2117
|
+
}
|
|
2118
|
+
const supplied = [options.projectRoot, options.specsRoot, options.specFile, options.featureName, options.runtimeSession]
|
|
2119
|
+
.some((value) => value !== null && value !== undefined);
|
|
2120
|
+
if (!supplied) return null;
|
|
2121
|
+
return PROVENANCE.deriveRuntimeContext({
|
|
2122
|
+
projectRoot: options.projectRoot,
|
|
2123
|
+
specsRoot: options.specsRoot,
|
|
2124
|
+
specFile: options.specFile,
|
|
2125
|
+
featureName: options.featureName,
|
|
2126
|
+
runtimeSession: options.runtimeSession,
|
|
2127
|
+
provenanceMode: options.provenanceMode || undefined,
|
|
2128
|
+
});
|
|
2129
|
+
}
|
|
2130
|
+
|
|
508
2131
|
function cliResult(result, json) {
|
|
509
2132
|
process.stdout.write(json ? `${JSON.stringify(result)}\n` : `${result.message}\n`);
|
|
510
2133
|
return result.exitCode;
|
|
@@ -541,6 +2164,18 @@ function runCli(argv = process.argv.slice(2)) {
|
|
|
541
2164
|
const policy = lanePolicy(input);
|
|
542
2165
|
return cliResult({ ok: true, policy, exitCode: 0, message: `Lane: ${policy.lane}` }, options.json);
|
|
543
2166
|
}
|
|
2167
|
+
if (options.action === 'policy-snapshot') {
|
|
2168
|
+
if (!options.task) throw new Error('--policy-snapshot requires --task-json');
|
|
2169
|
+
const snapshot = canonicalWorkflowPolicySnapshot(options.task);
|
|
2170
|
+
const view = readWorkflowPolicySnapshot({ workflow_policy: snapshot });
|
|
2171
|
+
return cliResult({ ok: true, snapshot, exitCode: 0, message: `Policy snapshot: ${view.lane}` }, options.json);
|
|
2172
|
+
}
|
|
2173
|
+
if (options.action === 'validate-policy') {
|
|
2174
|
+
if (!options.task) throw new Error('--validate-policy requires --task-json');
|
|
2175
|
+
const snapshot = hasOwn(options.task, 'workflow_policy') ? options.task.workflow_policy : options.task;
|
|
2176
|
+
const result = validateWorkflowPolicySnapshot(snapshot);
|
|
2177
|
+
return cliResult({ ok: result.valid, ...result, exitCode: result.valid ? 0 : 2, message: result.valid ? 'Workflow policy snapshot valid' : result.errors.join('; ') }, options.json);
|
|
2178
|
+
}
|
|
544
2179
|
if (options.action === 'approval-state') {
|
|
545
2180
|
if (!options.task) throw new Error('--approval-state requires --task-json');
|
|
546
2181
|
const state = approvalState(options.task);
|
|
@@ -554,22 +2189,30 @@ function runCli(argv = process.argv.slice(2)) {
|
|
|
554
2189
|
if (options.action === 'validate-receipt') {
|
|
555
2190
|
if (!options.task) throw new Error('--validate-receipt requires --task-json');
|
|
556
2191
|
const body = options.task.body || options.task.receipt || '';
|
|
557
|
-
const failures = validateCanonicalReceipt(body);
|
|
2192
|
+
const failures = validateCanonicalReceipt(body, receiptValidatorOptions(options.task, { runtimeContext: cliRuntimeContext(options) }));
|
|
558
2193
|
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
2194
|
}
|
|
560
2195
|
if (options.action === 'consume-verdict') {
|
|
561
|
-
const result = consumeReviewVerdict(options.verdict);
|
|
2196
|
+
const result = consumeReviewVerdict(options.verdict, options.task || {});
|
|
562
2197
|
return cliResult({ ok: true, ...result, exitCode: 0, message: `Verdict ${options.verdict}: ${result.action}` }, options.json);
|
|
563
2198
|
}
|
|
2199
|
+
if (options.action === 'completion-decision') {
|
|
2200
|
+
const runtimeContext = cliRuntimeContext(options);
|
|
2201
|
+
const result = completionDecision(options.verdict, runtimeContext
|
|
2202
|
+
? { ...(options.task || {}), runtime_context: runtimeContext }
|
|
2203
|
+
: (options.task || {}));
|
|
2204
|
+
return cliResult({ ok: true, ...result, exitCode: 0, message: `Verdict ${options.verdict}: ${result.completion}` }, options.json);
|
|
2205
|
+
}
|
|
564
2206
|
if (options.action === 'promote-flash') {
|
|
565
2207
|
if (!options.task) throw new Error('--promote-flash requires --task-json');
|
|
566
|
-
const result = promoteFlashTask(options.task, options.verdict, options.proof);
|
|
2208
|
+
const result = promoteFlashTask(options.task, options.verdict, options.proof, cliRuntimeContext(options));
|
|
567
2209
|
return cliResult({ ok: true, task: result, exitCode: 0, message: 'Flash task promotion evaluated.' }, options.json);
|
|
568
2210
|
}
|
|
569
2211
|
if (options.action === 'sync-finalize') {
|
|
570
2212
|
if (!options.task) throw new Error('--sync-finalize requires --task-json');
|
|
571
|
-
const result =
|
|
572
|
-
|
|
2213
|
+
const result = syncFinalizeFlashTask(options.task, options.verdict, options.proof, cliRuntimeContext(options));
|
|
2214
|
+
const ok = result.status === 'done';
|
|
2215
|
+
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
2216
|
}
|
|
574
2217
|
|
|
575
2218
|
return cliResult({ ok: true, contract: 'execution-policy', ...policy, exitCode: 0, message: `Execution mode: ${policy.mode}` }, options.json);
|
|
@@ -583,26 +2226,68 @@ if (require.main === module) process.exitCode = runCli();
|
|
|
583
2226
|
|
|
584
2227
|
module.exports = {
|
|
585
2228
|
REVIEW_VERDICTS,
|
|
2229
|
+
CANONICAL_VERDICTS,
|
|
2230
|
+
LEGACY_DIAGNOSTIC_VERDICTS,
|
|
586
2231
|
EXECUTION_TIERS,
|
|
587
2232
|
LANES,
|
|
2233
|
+
PLANNING_DEPTHS,
|
|
2234
|
+
ASSURANCE_LEVELS,
|
|
2235
|
+
ARTIFACT_PROFILES,
|
|
2236
|
+
PLANNING_OBLIGATIONS,
|
|
2237
|
+
PROOF_OBLIGATIONS,
|
|
2238
|
+
WORKFLOW_POLICY_VERSION,
|
|
2239
|
+
WORKFLOW_POLICY_FIELDS,
|
|
2240
|
+
CANONICAL_WORKFLOW_POLICY_VERSION,
|
|
2241
|
+
CANONICAL_WORKFLOW_POLICY_FIELDS,
|
|
2242
|
+
RECEIPT_BINDING_FIELDS,
|
|
2243
|
+
INDEPENDENT_AUDIT_SCHEMA_VERSION,
|
|
2244
|
+
INDEPENDENT_AUDIT_FIELDS,
|
|
588
2245
|
APPROVAL_SCHEMA_VERSION,
|
|
589
2246
|
LEGACY_APPROVAL_ERROR,
|
|
590
|
-
DEEP_TASK_SEQUENCE,
|
|
591
|
-
CRITICAL_LANE_SEQUENCE,
|
|
592
2247
|
executionPolicy,
|
|
593
2248
|
classifyLane,
|
|
2249
|
+
compatibilityLane,
|
|
2250
|
+
planningObligationsFor,
|
|
2251
|
+
obligationsForAssurance,
|
|
2252
|
+
obligationsForLane,
|
|
2253
|
+
actorNeedsFor,
|
|
2254
|
+
isWorkflowPolicySnapshot,
|
|
2255
|
+
validateWorkflowPolicySnapshot,
|
|
2256
|
+
assertWorkflowPolicySnapshot,
|
|
2257
|
+
workflowPolicySnapshot,
|
|
2258
|
+
canonicalWorkflowPolicySnapshot,
|
|
2259
|
+
isCanonicalTasklessTerminalSpec,
|
|
2260
|
+
createWorkflowPolicySnapshot,
|
|
2261
|
+
persistWorkflowPolicySnapshot,
|
|
2262
|
+
persistWorkflowPolicy,
|
|
2263
|
+
readWorkflowPolicySnapshot,
|
|
2264
|
+
escalateWorkflowPolicy,
|
|
2265
|
+
policyForSpec,
|
|
594
2266
|
lanePolicy,
|
|
595
2267
|
approvalState,
|
|
596
2268
|
validateApprovalSchema,
|
|
597
2269
|
isLegacyApprovalObject,
|
|
598
|
-
|
|
2270
|
+
isTapMetadataHeading,
|
|
2271
|
+
deriveRuntimeContext: PROVENANCE.deriveRuntimeContext,
|
|
2272
|
+
recomputeRuntimeContext: PROVENANCE.recomputeRuntimeContext,
|
|
2273
|
+
isTrustedRuntimeContext: PROVENANCE.isTrustedRuntimeContext,
|
|
2274
|
+
createReceiptBinding,
|
|
2275
|
+
receiptValidatorOptions,
|
|
599
2276
|
validateCanonicalReceipt,
|
|
2277
|
+
validateIndependentAuditEvidence,
|
|
2278
|
+
completionDecisionForSpec,
|
|
600
2279
|
delegationPlan,
|
|
2280
|
+
assertVerdict,
|
|
2281
|
+
normalizeVerdict,
|
|
2282
|
+
adaptVerdict,
|
|
2283
|
+
completionDecision,
|
|
601
2284
|
consumeReviewVerdict,
|
|
602
2285
|
isFlashUnverified,
|
|
2286
|
+
isCanonicalFlashUnverified,
|
|
603
2287
|
isStaleFlashDone,
|
|
604
2288
|
promoteFlashTask,
|
|
605
2289
|
finalizeFlashTask,
|
|
2290
|
+
syncFinalizeFlashTask,
|
|
606
2291
|
flashState,
|
|
607
2292
|
parseCliArgs,
|
|
608
2293
|
runCli,
|