codex-workflow-v2 2.0.0-alpha.4 → 2.0.0-alpha.6
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/README.md +86 -10
- package/dist/src/alpha6/adoption.d.ts +55 -0
- package/dist/src/alpha6/adoption.js +920 -0
- package/dist/src/alpha6/adoption.js.map +1 -0
- package/dist/src/alpha6/handoff.d.ts +39 -0
- package/dist/src/alpha6/handoff.js +975 -0
- package/dist/src/alpha6/handoff.js.map +1 -0
- package/dist/src/alpha6/journal.d.ts +30 -0
- package/dist/src/alpha6/journal.js +369 -0
- package/dist/src/alpha6/journal.js.map +1 -0
- package/dist/src/alpha6/milestone.d.ts +49 -0
- package/dist/src/alpha6/milestone.js +1049 -0
- package/dist/src/alpha6/milestone.js.map +1 -0
- package/dist/src/alpha6/plan-risk.d.ts +32 -0
- package/dist/src/alpha6/plan-risk.js +847 -0
- package/dist/src/alpha6/plan-risk.js.map +1 -0
- package/dist/src/alpha6/remediation.d.ts +20 -0
- package/dist/src/alpha6/remediation.js +748 -0
- package/dist/src/alpha6/remediation.js.map +1 -0
- package/dist/src/alpha6/review.d.ts +46 -0
- package/dist/src/alpha6/review.js +785 -0
- package/dist/src/alpha6/review.js.map +1 -0
- package/dist/src/alpha6/store-sidecars.d.ts +35 -0
- package/dist/src/alpha6/store-sidecars.js +281 -0
- package/dist/src/alpha6/store-sidecars.js.map +1 -0
- package/dist/src/cli.js +88 -19
- package/dist/src/cli.js.map +1 -1
- package/dist/src/contracts.d.ts +259 -0
- package/dist/src/git.js +2 -1
- package/dist/src/git.js.map +1 -1
- package/dist/src/index.d.ts +2 -0
- package/dist/src/reviewer.d.ts +6 -1
- package/dist/src/reviewer.js +145 -32
- package/dist/src/reviewer.js.map +1 -1
- package/dist/src/state/lock.d.ts +1 -0
- package/dist/src/state/lock.js +7 -1
- package/dist/src/state/lock.js.map +1 -1
- package/dist/src/state/store.d.ts +39 -1
- package/dist/src/state/store.js +127 -1
- package/dist/src/state/store.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/workflow.d.ts +78 -8
- package/dist/src/workflow.js +1312 -74
- package/dist/src/workflow.js.map +1 -1
- package/docs/autonomy-guardrails.md +143 -0
- package/docs/decisions.md +23 -0
- package/docs/delegated-approval.md +30 -5
- package/docs/development-flow.md +40 -1
- package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +155 -136
- package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +236 -223
- package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +225 -206
- package/docs/project-memory.md +7 -0
- package/docs/release.md +7 -0
- package/docs/updating-existing-project.md +53 -5
- package/docs/validation-report.md +47 -34
- package/package.json +1 -1
- package/plugins/codex-workflow-gateway/references/protocol.md +150 -6
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +16 -1
- package/references/git-policy.md +5 -2
- package/references/state-machine.md +36 -0
- package/references/validation-and-review.md +28 -1
- package/roles/delivery-coordinator.md +11 -0
- package/roles/independent-reviewer.md +3 -0
- package/roles/technical-planner.md +7 -0
- package/roles/worker.md +4 -0
- package/schemas/adoption-posture-event.schema.json +129 -0
- package/schemas/corrective-decision-event.schema.json +55 -0
- package/schemas/corrective-plan-audit.schema.json +20 -0
- package/schemas/milestone-scope-change-event.schema.json +68 -0
- package/schemas/milestone-transaction-journal.schema.json +95 -0
- package/schemas/plan-risk-audit-event.schema.json +90 -0
- package/schemas/remediation-event.schema.json +53 -0
- package/schemas/reviewer-attestation-event.schema.json +49 -0
- package/schemas/step-review-event.schema.json +74 -0
- package/schemas/task-handoff-event.schema.json +116 -0
|
@@ -0,0 +1,847 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { WorkflowError } from '../errors.js';
|
|
4
|
+
import { assertHash64, canonicalJsonStringify, sha256Hex } from './store-sidecars.js';
|
|
5
|
+
import { createUlid } from '../ulid.js';
|
|
6
|
+
const PLAN_RISK_AUDIT_SIDECAR = 'plan-risk-audits.jsonl';
|
|
7
|
+
const ADOPTION_POSTURE_SIDECAR = 'adoption-posture.jsonl';
|
|
8
|
+
const PLAN_RISK_DECISIONS = new Set([
|
|
9
|
+
'approved',
|
|
10
|
+
'approved-with-rationale',
|
|
11
|
+
'split-required',
|
|
12
|
+
'stop-escalate',
|
|
13
|
+
]);
|
|
14
|
+
const ADOPTION_POSTURE_EVENT_TYPES = new Set([
|
|
15
|
+
'project-registration',
|
|
16
|
+
'adoption-apply',
|
|
17
|
+
]);
|
|
18
|
+
const ACTIVE_ADOPTION_TASK_STATUSES = new Set([
|
|
19
|
+
'planning',
|
|
20
|
+
'awaiting_execution_authorization',
|
|
21
|
+
'ready',
|
|
22
|
+
'in_progress',
|
|
23
|
+
'validating',
|
|
24
|
+
'needs_fix',
|
|
25
|
+
'awaiting_final_acceptance',
|
|
26
|
+
'blocked',
|
|
27
|
+
]);
|
|
28
|
+
const PLAN_RISK_RECORDABLE_TASK_STATUSES = new Set([
|
|
29
|
+
'awaiting_execution_authorization',
|
|
30
|
+
'ready',
|
|
31
|
+
'in_progress',
|
|
32
|
+
'validating',
|
|
33
|
+
'needs_fix',
|
|
34
|
+
'awaiting_final_acceptance',
|
|
35
|
+
'blocked',
|
|
36
|
+
]);
|
|
37
|
+
const GUARDED_RISK_CATEGORY_ORDER = [
|
|
38
|
+
'migration',
|
|
39
|
+
'concurrency',
|
|
40
|
+
'restart-replay',
|
|
41
|
+
'crash-window',
|
|
42
|
+
'external-provider',
|
|
43
|
+
'runtime-composition',
|
|
44
|
+
'security-boundary',
|
|
45
|
+
'auth-secrets',
|
|
46
|
+
'irreversible-execution',
|
|
47
|
+
];
|
|
48
|
+
const GUARDED_RISK_CATEGORY_SET = new Set(GUARDED_RISK_CATEGORY_ORDER);
|
|
49
|
+
const GUARDED_RISK_CATEGORY_INDEX = new Map(GUARDED_RISK_CATEGORY_ORDER.map((category, index) => [category, index]));
|
|
50
|
+
export function validatePlanRiskAuditCandidate(store, task, input) {
|
|
51
|
+
const boundary = readPlanRiskAuditBoundary(store, task);
|
|
52
|
+
if (!boundary) {
|
|
53
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Plan Risk Audit requires an active adoption posture boundary.', {
|
|
54
|
+
taskId: task.id,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
assertRecordableTask(task);
|
|
58
|
+
if (!task.planHash || task.knowledgeMapRevision === null || !task.knowledgeMapHash) {
|
|
59
|
+
throw new WorkflowError('STATE_CORRUPT', 'Candidate Task is missing Plan or Knowledge bindings for risk audit.', {
|
|
60
|
+
taskId: task.id,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
validatePlanRiskAuditPayload(task, {
|
|
64
|
+
taskId: task.id,
|
|
65
|
+
taskRevision: task.revision + 1,
|
|
66
|
+
briefHash: task.briefHash,
|
|
67
|
+
planHash: task.planHash,
|
|
68
|
+
knowledgeMapRevision: task.knowledgeMapRevision,
|
|
69
|
+
knowledgeMapHash: task.knowledgeMapHash,
|
|
70
|
+
planner: input.planner,
|
|
71
|
+
auditor: input.auditor,
|
|
72
|
+
summary: input.summary,
|
|
73
|
+
decision: input.decision,
|
|
74
|
+
splitRationale: input.splitRationale,
|
|
75
|
+
stepClassifications: input.stepClassifications,
|
|
76
|
+
}, boundary.expectedStepIds, 'INVALID_ARGUMENT', {
|
|
77
|
+
taskId: task.id,
|
|
78
|
+
expectedStepIds: boundary.expectedStepIds,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
export function readPlanRiskAuditBoundary(store, task) {
|
|
82
|
+
const posture = readCurrentAdoptionPosture(store, task.projectId);
|
|
83
|
+
if (!posture)
|
|
84
|
+
return null;
|
|
85
|
+
const scope = derivePlanRiskScope(task, posture);
|
|
86
|
+
return {
|
|
87
|
+
scope: scope.scope,
|
|
88
|
+
postureEventType: posture.eventType,
|
|
89
|
+
adoptionBoundaryAt: posture.adoptionBoundaryAt,
|
|
90
|
+
expectedStepIds: scope.expectedStepIds,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
export function readCurrentPlanRiskAudit(store, task, options = {}) {
|
|
94
|
+
const boundary = readPlanRiskAuditBoundary(store, task);
|
|
95
|
+
if (!boundary)
|
|
96
|
+
return null;
|
|
97
|
+
const taskRoot = store.taskRoot(task.projectId, task.id);
|
|
98
|
+
const events = store.readSidecarEvents(taskRoot, PLAN_RISK_AUDIT_SIDECAR, {
|
|
99
|
+
validateEvent: (event, context) => validateStoredPlanRiskAuditEvent(event, task, context.file, context.line),
|
|
100
|
+
});
|
|
101
|
+
const audit = events.at(-1);
|
|
102
|
+
if (!audit) {
|
|
103
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} requires a current Plan Risk Audit before execution can proceed.`, {
|
|
104
|
+
taskId: task.id,
|
|
105
|
+
scope: boundary.scope,
|
|
106
|
+
expectedStepIds: boundary.expectedStepIds,
|
|
107
|
+
sidecar: PLAN_RISK_AUDIT_SIDECAR,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
const reviewRequiredStepIds = validateCurrentPlanRiskAudit(store, task, audit, boundary.expectedStepIds, options);
|
|
111
|
+
return {
|
|
112
|
+
...boundary,
|
|
113
|
+
audit,
|
|
114
|
+
reviewRequiredStepIds,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
export function buildPlanRiskAuditEvent(store, task, input, recordedAt, now = Date.now()) {
|
|
118
|
+
const boundary = readPlanRiskAuditBoundary(store, task);
|
|
119
|
+
if (!boundary) {
|
|
120
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Plan Risk Audit recording requires an active adoption posture boundary.', { taskId: task.id, sidecar: ADOPTION_POSTURE_SIDECAR });
|
|
121
|
+
}
|
|
122
|
+
assertRecordableTask(task);
|
|
123
|
+
const entityRoot = store.taskRoot(task.projectId, task.id);
|
|
124
|
+
const briefHash = store.hashArtifact(entityRoot, 'brief.md');
|
|
125
|
+
const planHash = store.hashArtifact(entityRoot, 'plan.md');
|
|
126
|
+
if (task.briefHash !== briefHash || task.planHash !== planHash) {
|
|
127
|
+
throw new WorkflowError('STATE_CORRUPT', 'Task state does not match the current Brief or Plan artifacts required for Plan Risk Audit recording.', {
|
|
128
|
+
taskId: task.id,
|
|
129
|
+
stateBriefHash: task.briefHash,
|
|
130
|
+
currentBriefHash: briefHash,
|
|
131
|
+
statePlanHash: task.planHash,
|
|
132
|
+
currentPlanHash: planHash,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
if (task.knowledgeMapRevision === null || !task.knowledgeMapHash) {
|
|
136
|
+
throw new WorkflowError('STATE_CORRUPT', `Task ${task.id} has no current knowledge binding for Plan Risk Audit.`);
|
|
137
|
+
}
|
|
138
|
+
const event = {
|
|
139
|
+
eventId: `PRA-${createUlid(now)}`,
|
|
140
|
+
recordedAt,
|
|
141
|
+
taskId: task.id,
|
|
142
|
+
taskRevision: task.revision,
|
|
143
|
+
briefHash,
|
|
144
|
+
planHash,
|
|
145
|
+
knowledgeMapRevision: task.knowledgeMapRevision,
|
|
146
|
+
knowledgeMapHash: task.knowledgeMapHash,
|
|
147
|
+
planner: normalizeNonEmptyString(input.planner, 'Plan Risk Audit planner'),
|
|
148
|
+
auditor: normalizeNonEmptyString(input.auditor, 'Plan Risk Audit auditor'),
|
|
149
|
+
summary: normalizeNonEmptyString(input.summary, 'Plan Risk Audit summary'),
|
|
150
|
+
decision: normalizePlanRiskDecision(input.decision, 'INVALID_ARGUMENT', {
|
|
151
|
+
taskId: task.id,
|
|
152
|
+
value: input.decision,
|
|
153
|
+
}),
|
|
154
|
+
splitRationale: normalizeOptionalNonEmptyString(input.splitRationale, 'Plan Risk Audit splitRationale'),
|
|
155
|
+
stepClassifications: normalizePlanRiskClassifications(input.stepClassifications, 'INVALID_ARGUMENT', {
|
|
156
|
+
taskId: task.id,
|
|
157
|
+
}),
|
|
158
|
+
};
|
|
159
|
+
validatePlanRiskAuditPayload(task, event, boundary.expectedStepIds, 'INVALID_ARGUMENT', {
|
|
160
|
+
taskId: task.id,
|
|
161
|
+
expectedStepIds: boundary.expectedStepIds,
|
|
162
|
+
});
|
|
163
|
+
return {
|
|
164
|
+
boundary,
|
|
165
|
+
entityRoot,
|
|
166
|
+
event,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
export function appendCurrentPlanRiskAudit(store, task, input, now) {
|
|
170
|
+
const prepared = buildPlanRiskAuditEvent(store, task, input, now.toISOString(), now.getTime());
|
|
171
|
+
const existingEvents = readStoredPlanRiskAuditEvents(store, task);
|
|
172
|
+
assertPlanRiskAuditAppendAllowed(task, prepared.boundary, existingEvents);
|
|
173
|
+
return store.appendSidecarEvent(prepared.entityRoot, PLAN_RISK_AUDIT_SIDECAR, prepared.event, {
|
|
174
|
+
validateEvent: (event, context) => validateStoredPlanRiskAuditEvent(event, task, context.file, context.line),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
export function appendReboundPlanRiskAudit(store, sourceTask, reboundTask, sourceAudit, now) {
|
|
178
|
+
assertReboundPlanRiskAuditAllowed(store, sourceTask, reboundTask, sourceAudit);
|
|
179
|
+
return appendCurrentPlanRiskAudit(store, reboundTask, {
|
|
180
|
+
planner: sourceAudit.audit.planner,
|
|
181
|
+
auditor: sourceAudit.audit.auditor,
|
|
182
|
+
summary: `${sourceAudit.audit.summary} Rebound automatically after content-only Task context refresh.`,
|
|
183
|
+
decision: sourceAudit.audit.decision,
|
|
184
|
+
splitRationale: sourceAudit.audit.splitRationale,
|
|
185
|
+
stepClassifications: sourceAudit.audit.stepClassifications.map((classification) => ({
|
|
186
|
+
...classification,
|
|
187
|
+
categories: [...classification.categories],
|
|
188
|
+
failureModes: [...classification.failureModes],
|
|
189
|
+
requiredEvidence: [...classification.requiredEvidence],
|
|
190
|
+
})),
|
|
191
|
+
}, now);
|
|
192
|
+
}
|
|
193
|
+
function readCurrentAdoptionPosture(store, projectId) {
|
|
194
|
+
const sidecarFile = path.join(store.projectRoot(projectId), ADOPTION_POSTURE_SIDECAR);
|
|
195
|
+
if (!existsSync(sidecarFile))
|
|
196
|
+
return null;
|
|
197
|
+
const events = store.readSidecarEvents(store.projectRoot(projectId), ADOPTION_POSTURE_SIDECAR, {
|
|
198
|
+
validateEvent: (event, context) => validateStoredAdoptionPostureEvent(event, projectId, context.file, context.line),
|
|
199
|
+
});
|
|
200
|
+
return events.at(-1) ?? null;
|
|
201
|
+
}
|
|
202
|
+
function assertReboundPlanRiskAuditAllowed(store, sourceTask, reboundTask, sourceAudit) {
|
|
203
|
+
if (sourceTask.projectId !== reboundTask.projectId || sourceTask.id !== reboundTask.id) {
|
|
204
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Plan Risk Audit rebound requires the same Task identity before and after content-only refresh.', {
|
|
205
|
+
sourceTaskId: sourceTask.id,
|
|
206
|
+
reboundTaskId: reboundTask.id,
|
|
207
|
+
sourceProjectId: sourceTask.projectId,
|
|
208
|
+
reboundProjectId: reboundTask.projectId,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
if (sourceTask.briefHash !== reboundTask.briefHash) {
|
|
212
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Plan Risk Audit rebound is limited to content-only refresh and cannot cross a Brief change.', {
|
|
213
|
+
taskId: sourceTask.id,
|
|
214
|
+
sourceBriefHash: sourceTask.briefHash,
|
|
215
|
+
reboundBriefHash: reboundTask.briefHash,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
if (sourceTask.planHash === reboundTask.planHash) {
|
|
219
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Plan Risk Audit rebound requires a current rebound Plan binding.', {
|
|
220
|
+
taskId: sourceTask.id,
|
|
221
|
+
planHash: sourceTask.planHash,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
if (sourceTask.knowledgeMapRevision === reboundTask.knowledgeMapRevision
|
|
225
|
+
&& sourceTask.knowledgeMapHash === reboundTask.knowledgeMapHash) {
|
|
226
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Plan Risk Audit rebound requires a changed Task knowledge binding.', {
|
|
227
|
+
taskId: sourceTask.id,
|
|
228
|
+
knowledgeMapRevision: sourceTask.knowledgeMapRevision,
|
|
229
|
+
knowledgeMapHash: sourceTask.knowledgeMapHash,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
const reboundBoundary = readPlanRiskAuditBoundary(store, reboundTask);
|
|
233
|
+
if (!reboundBoundary) {
|
|
234
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Plan Risk Audit rebound requires an active adoption posture boundary.', {
|
|
235
|
+
taskId: reboundTask.id,
|
|
236
|
+
sidecar: ADOPTION_POSTURE_SIDECAR,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
assertExactIdSet(sourceAudit.expectedStepIds, reboundBoundary.expectedStepIds, 'TRANSITION_BLOCKED', 'Plan Risk Audit rebound requires the exact same classified Step set after content-only refresh.', {
|
|
240
|
+
taskId: reboundTask.id,
|
|
241
|
+
sourceExpectedStepIds: sourceAudit.expectedStepIds,
|
|
242
|
+
reboundExpectedStepIds: reboundBoundary.expectedStepIds,
|
|
243
|
+
});
|
|
244
|
+
const sourceStepDefinitions = buildTaskStepDefinitionHashes(sourceTask);
|
|
245
|
+
const reboundStepDefinitions = buildTaskStepDefinitionHashes(reboundTask);
|
|
246
|
+
assertExactIdSet([...sourceStepDefinitions.keys()], [...reboundStepDefinitions.keys()], 'TRANSITION_BLOCKED', 'Plan Risk Audit rebound requires the same Task Step identifiers after content-only refresh.', {
|
|
247
|
+
taskId: reboundTask.id,
|
|
248
|
+
sourceStepIds: [...sourceStepDefinitions.keys()],
|
|
249
|
+
reboundStepIds: [...reboundStepDefinitions.keys()],
|
|
250
|
+
});
|
|
251
|
+
for (const [stepId, sourceDefinitionHash] of sourceStepDefinitions.entries()) {
|
|
252
|
+
if (reboundStepDefinitions.get(stepId) === sourceDefinitionHash)
|
|
253
|
+
continue;
|
|
254
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Plan Risk Audit rebound is limited to content-only refresh and cannot cross a Step definition change.', {
|
|
255
|
+
taskId: reboundTask.id,
|
|
256
|
+
stepId,
|
|
257
|
+
sourceDefinitionHash,
|
|
258
|
+
reboundDefinitionHash: reboundStepDefinitions.get(stepId) ?? null,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function derivePlanRiskScope(task, posture) {
|
|
263
|
+
const currentTaskStepIds = task.steps.map((step) => step.id);
|
|
264
|
+
assertUniqueValues(currentTaskStepIds, 'STATE_CORRUPT', 'Task contains duplicate Step identifiers.', {
|
|
265
|
+
taskId: task.id,
|
|
266
|
+
stepIds: currentTaskStepIds,
|
|
267
|
+
});
|
|
268
|
+
const baseline = posture.activeTasks.find((candidate) => candidate.taskId === task.id) ?? null;
|
|
269
|
+
if (!baseline) {
|
|
270
|
+
return {
|
|
271
|
+
scope: 'current-task-steps',
|
|
272
|
+
expectedStepIds: currentTaskStepIds,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
validateAdoptionBaseline(task, baseline, currentTaskStepIds, posture);
|
|
276
|
+
const completedStepIdSet = new Set(baseline.completedSteps.map((step) => step.stepId));
|
|
277
|
+
return {
|
|
278
|
+
scope: 'legacy-remaining-scope',
|
|
279
|
+
expectedStepIds: currentTaskStepIds.filter((stepId) => !completedStepIdSet.has(stepId)),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
function validateStoredAdoptionPostureEvent(event, expectedProjectId, file, line) {
|
|
283
|
+
assertPlainObject(event, 'Adoption posture event', file, line);
|
|
284
|
+
const eventType = normalizeAdoptionPostureEventType(event.eventType, file, line, event.eventId);
|
|
285
|
+
if (event.projectId !== expectedProjectId) {
|
|
286
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption posture event project binding does not match its project root.', {
|
|
287
|
+
file,
|
|
288
|
+
line,
|
|
289
|
+
eventId: event.eventId,
|
|
290
|
+
expectedProjectId,
|
|
291
|
+
actualProjectId: event.projectId,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
normalizeNonEmptyString(event.adoptionBoundaryAt, 'Adoption posture adoptionBoundaryAt', {
|
|
295
|
+
code: 'STATE_CORRUPT',
|
|
296
|
+
file,
|
|
297
|
+
line,
|
|
298
|
+
eventId: event.eventId,
|
|
299
|
+
});
|
|
300
|
+
if (!Array.isArray(event.activeTasks)) {
|
|
301
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption posture activeTasks must be an array.', {
|
|
302
|
+
file,
|
|
303
|
+
line,
|
|
304
|
+
eventId: event.eventId,
|
|
305
|
+
eventType,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
assertUniqueValues(event.activeTasks.map((candidate) => candidate?.taskId), 'STATE_CORRUPT', 'Adoption posture contains duplicate active Task baselines.', { file, line, eventId: event.eventId });
|
|
309
|
+
for (const baseline of event.activeTasks) {
|
|
310
|
+
validateStoredAdoptionTaskBaseline(baseline, file, line, event.eventId);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function validateStoredAdoptionTaskBaseline(baseline, file, line, eventId) {
|
|
314
|
+
assertPlainObject(baseline, 'Adoption Task baseline', file, line);
|
|
315
|
+
normalizeNonEmptyString(baseline.taskId, 'Adoption Task baseline taskId', {
|
|
316
|
+
code: 'STATE_CORRUPT',
|
|
317
|
+
file,
|
|
318
|
+
line,
|
|
319
|
+
eventId,
|
|
320
|
+
});
|
|
321
|
+
assertPositiveInteger(baseline.taskRevision, 'Adoption Task baseline taskRevision', file, line, eventId);
|
|
322
|
+
if (!ACTIVE_ADOPTION_TASK_STATUSES.has(baseline.status)) {
|
|
323
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption Task baseline status is invalid.', {
|
|
324
|
+
file,
|
|
325
|
+
line,
|
|
326
|
+
eventId,
|
|
327
|
+
taskId: baseline.taskId,
|
|
328
|
+
status: baseline.status,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
if (!Array.isArray(baseline.completedSteps)) {
|
|
332
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption Task baseline completedSteps must be an array.', {
|
|
333
|
+
file,
|
|
334
|
+
line,
|
|
335
|
+
eventId,
|
|
336
|
+
taskId: baseline.taskId,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
if (!Array.isArray(baseline.remainingStepIds)) {
|
|
340
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption Task baseline remainingStepIds must be an array.', {
|
|
341
|
+
file,
|
|
342
|
+
line,
|
|
343
|
+
eventId,
|
|
344
|
+
taskId: baseline.taskId,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
const completedStepIds = [];
|
|
348
|
+
for (const completed of baseline.completedSteps) {
|
|
349
|
+
assertPlainObject(completed, 'Adoption completed Step baseline', file, line);
|
|
350
|
+
const stepId = normalizeNonEmptyString(completed.stepId, 'Adoption completed Step stepId', {
|
|
351
|
+
code: 'STATE_CORRUPT',
|
|
352
|
+
file,
|
|
353
|
+
line,
|
|
354
|
+
eventId,
|
|
355
|
+
taskId: baseline.taskId,
|
|
356
|
+
});
|
|
357
|
+
completedStepIds.push(stepId);
|
|
358
|
+
if (completed.status !== 'completed' && completed.status !== 'skipped') {
|
|
359
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption completed Step status is invalid.', {
|
|
360
|
+
file,
|
|
361
|
+
line,
|
|
362
|
+
eventId,
|
|
363
|
+
taskId: baseline.taskId,
|
|
364
|
+
stepId,
|
|
365
|
+
status: completed.status,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
if (completed.evidenceHash !== null) {
|
|
369
|
+
if (typeof completed.evidenceHash !== 'string') {
|
|
370
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption completed Step evidenceHash must be null or a hash.', {
|
|
371
|
+
file,
|
|
372
|
+
line,
|
|
373
|
+
eventId,
|
|
374
|
+
taskId: baseline.taskId,
|
|
375
|
+
stepId,
|
|
376
|
+
evidenceHash: completed.evidenceHash,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
assertHash64(completed.evidenceHash, 'Adoption completed Step evidenceHash');
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
assertUniqueValues(completedStepIds, 'STATE_CORRUPT', 'Adoption completed Step baseline must not contain duplicates.', {
|
|
383
|
+
file,
|
|
384
|
+
line,
|
|
385
|
+
eventId,
|
|
386
|
+
taskId: baseline.taskId,
|
|
387
|
+
});
|
|
388
|
+
const remainingStepIds = baseline.remainingStepIds.map((stepId) => normalizeNonEmptyString(stepId, 'Adoption remaining Step id', {
|
|
389
|
+
code: 'STATE_CORRUPT',
|
|
390
|
+
file,
|
|
391
|
+
line,
|
|
392
|
+
eventId,
|
|
393
|
+
taskId: baseline.taskId,
|
|
394
|
+
}));
|
|
395
|
+
assertUniqueValues(remainingStepIds, 'STATE_CORRUPT', 'Adoption posture remaining Step baseline must not contain duplicates.', {
|
|
396
|
+
file,
|
|
397
|
+
line,
|
|
398
|
+
eventId,
|
|
399
|
+
taskId: baseline.taskId,
|
|
400
|
+
});
|
|
401
|
+
const completedSet = new Set(completedStepIds);
|
|
402
|
+
const overlap = remainingStepIds.filter((stepId) => completedSet.has(stepId));
|
|
403
|
+
if (overlap.length > 0) {
|
|
404
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption Task baseline cannot list the same Step as both completed and remaining.', {
|
|
405
|
+
file,
|
|
406
|
+
line,
|
|
407
|
+
eventId,
|
|
408
|
+
taskId: baseline.taskId,
|
|
409
|
+
overlappingStepIds: overlap,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
function validateAdoptionBaseline(task, baseline, currentTaskStepIds, posture) {
|
|
414
|
+
if (baseline.taskRevision > task.revision) {
|
|
415
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption posture baseline refers to a future Task revision.', {
|
|
416
|
+
taskId: task.id,
|
|
417
|
+
baselineTaskRevision: baseline.taskRevision,
|
|
418
|
+
currentTaskRevision: task.revision,
|
|
419
|
+
adoptionBoundaryAt: posture.adoptionBoundaryAt,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
const currentTaskStepIdSet = new Set(currentTaskStepIds);
|
|
423
|
+
const completedStepIds = baseline.completedSteps.map((step) => step.stepId);
|
|
424
|
+
const completedStepIdSet = new Set(completedStepIds);
|
|
425
|
+
const missingCompletedFromTask = completedStepIds.filter((stepId) => !currentTaskStepIdSet.has(stepId));
|
|
426
|
+
if (missingCompletedFromTask.length > 0) {
|
|
427
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Adoption posture remaining-scope baseline no longer matches the current Task Step set.', {
|
|
428
|
+
taskId: task.id,
|
|
429
|
+
adoptionBoundaryAt: posture.adoptionBoundaryAt,
|
|
430
|
+
missingCompletedStepIds: missingCompletedFromTask,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
const overlap = baseline.remainingStepIds.filter((stepId) => completedStepIdSet.has(stepId));
|
|
434
|
+
if (overlap.length > 0) {
|
|
435
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption posture baseline cannot mark the same Step as completed and remaining.', {
|
|
436
|
+
taskId: task.id,
|
|
437
|
+
adoptionBoundaryAt: posture.adoptionBoundaryAt,
|
|
438
|
+
overlappingStepIds: overlap,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
if (baseline.taskRevision === task.revision) {
|
|
442
|
+
const missingRemainingFromTask = baseline.remainingStepIds.filter((stepId) => !currentTaskStepIdSet.has(stepId));
|
|
443
|
+
if (missingRemainingFromTask.length > 0) {
|
|
444
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Adoption posture remaining-scope baseline no longer matches the Task Step set at the posture boundary revision.', {
|
|
445
|
+
taskId: task.id,
|
|
446
|
+
adoptionBoundaryAt: posture.adoptionBoundaryAt,
|
|
447
|
+
missingStepIds: missingRemainingFromTask,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
const partitionIds = [...completedStepIds, ...baseline.remainingStepIds];
|
|
451
|
+
assertExactIdSet(currentTaskStepIds, partitionIds, 'STATE_CORRUPT', 'Adoption posture baseline must partition the Task Step set at the posture boundary revision.', {
|
|
452
|
+
taskId: task.id,
|
|
453
|
+
adoptionBoundaryAt: posture.adoptionBoundaryAt,
|
|
454
|
+
currentTaskRevision: task.revision,
|
|
455
|
+
completedStepIds,
|
|
456
|
+
remainingStepIds: baseline.remainingStepIds,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
function validateStoredPlanRiskAuditEvent(event, task, file, line) {
|
|
461
|
+
validatePlanRiskAuditPayload(task, event, null, 'STATE_CORRUPT', {
|
|
462
|
+
file,
|
|
463
|
+
line,
|
|
464
|
+
eventId: event.eventId,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
function validatePlanRiskAuditPayload(task, event, expectedStepIds, errorCode, details) {
|
|
468
|
+
if (event.taskId !== task.id) {
|
|
469
|
+
throw new WorkflowError(errorCode, 'Plan Risk Audit task binding does not match its task root.', {
|
|
470
|
+
...details,
|
|
471
|
+
expectedTaskId: task.id,
|
|
472
|
+
actualTaskId: event.taskId,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
assertPositiveInteger(event.taskRevision, 'Plan Risk Audit taskRevision', details.file, details.line, details.eventId, errorCode);
|
|
476
|
+
assertHash64String(event.briefHash, 'Plan Risk Audit briefHash', errorCode, details);
|
|
477
|
+
assertHash64String(event.planHash, 'Plan Risk Audit planHash', errorCode, details);
|
|
478
|
+
assertPositiveInteger(event.knowledgeMapRevision, 'Plan Risk Audit knowledgeMapRevision', details.file, details.line, details.eventId, errorCode);
|
|
479
|
+
assertHash64String(event.knowledgeMapHash, 'Plan Risk Audit knowledgeMapHash', errorCode, details);
|
|
480
|
+
const planner = normalizeNonEmptyString(event.planner, 'Plan Risk Audit planner', { code: errorCode, ...details });
|
|
481
|
+
const auditor = normalizeNonEmptyString(event.auditor, 'Plan Risk Audit auditor', { code: errorCode, ...details });
|
|
482
|
+
if (planner === auditor) {
|
|
483
|
+
throw new WorkflowError(errorCode, 'Plan Risk Audit auditor must be independent from the Plan author.', {
|
|
484
|
+
...details,
|
|
485
|
+
taskId: task.id,
|
|
486
|
+
planner,
|
|
487
|
+
auditor,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
normalizeNonEmptyString(event.summary, 'Plan Risk Audit summary', { code: errorCode, ...details });
|
|
491
|
+
normalizePlanRiskDecision(event.decision, errorCode, details);
|
|
492
|
+
normalizeOptionalNonEmptyString(event.splitRationale, 'Plan Risk Audit splitRationale', { code: errorCode, ...details });
|
|
493
|
+
if (!Array.isArray(event.stepClassifications)) {
|
|
494
|
+
throw new WorkflowError(errorCode, 'Plan Risk Audit stepClassifications must be an array.', details);
|
|
495
|
+
}
|
|
496
|
+
validateStepClassificationSet(task, event.stepClassifications, event.decision, event.splitRationale, expectedStepIds, errorCode, details);
|
|
497
|
+
}
|
|
498
|
+
function validateStepClassificationSet(task, classifications, decision, splitRationale, expectedStepIds, errorCode, details) {
|
|
499
|
+
const currentTaskStepIds = new Set(task.steps.map((step) => step.id));
|
|
500
|
+
const normalizedStepIds = classifications.map((classification) => normalizeNonEmptyString(classification?.stepId, 'Plan Risk Audit classification stepId', {
|
|
501
|
+
code: errorCode,
|
|
502
|
+
...details,
|
|
503
|
+
}));
|
|
504
|
+
assertUniqueValues(normalizedStepIds, errorCode, 'Plan Risk Audit must classify each Step exactly once.', {
|
|
505
|
+
...details,
|
|
506
|
+
taskId: task.id,
|
|
507
|
+
});
|
|
508
|
+
if (expectedStepIds) {
|
|
509
|
+
assertExactIdSet(expectedStepIds, normalizedStepIds, errorCode, 'Plan Risk Audit Step classifications do not match the required Step set.', {
|
|
510
|
+
...details,
|
|
511
|
+
taskId: task.id,
|
|
512
|
+
expectedStepIds,
|
|
513
|
+
actualStepIds: normalizedStepIds,
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
let reviewRequiredCount = 0;
|
|
517
|
+
const guardedCategories = new Set();
|
|
518
|
+
for (const classification of classifications) {
|
|
519
|
+
if (expectedStepIds && !currentTaskStepIds.has(classification.stepId)) {
|
|
520
|
+
throw new WorkflowError(errorCode, 'Plan Risk Audit references a Step that is not present in the Task.', {
|
|
521
|
+
...details,
|
|
522
|
+
taskId: task.id,
|
|
523
|
+
stepId: classification.stepId,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
if (classification.reviewRequired)
|
|
527
|
+
reviewRequiredCount += 1;
|
|
528
|
+
validateStepClassification(task, classification, errorCode, details);
|
|
529
|
+
classification.categories.forEach((category) => guardedCategories.add(category));
|
|
530
|
+
}
|
|
531
|
+
if ((decision === 'split-required' || decision === 'stop-escalate') && reviewRequiredCount === 0) {
|
|
532
|
+
throw new WorkflowError(errorCode, `Plan Risk Audit decision ${decision} requires at least one guarded Step.`, {
|
|
533
|
+
...details,
|
|
534
|
+
taskId: task.id,
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
const coupledRecoveryDomains = [
|
|
538
|
+
'restart-replay',
|
|
539
|
+
'crash-window',
|
|
540
|
+
'runtime-composition',
|
|
541
|
+
].filter((category) => guardedCategories.has(category));
|
|
542
|
+
const requiresSplitRationale = guardedCategories.has('migration') && coupledRecoveryDomains.length > 0;
|
|
543
|
+
if (requiresSplitRationale && decision !== 'approved-with-rationale') {
|
|
544
|
+
throw new WorkflowError(errorCode, 'A Plan combining migration with recovery or runtime-composition risk requires approved-with-rationale.', {
|
|
545
|
+
...details,
|
|
546
|
+
taskId: task.id,
|
|
547
|
+
guardedCategories: [...guardedCategories],
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
if (decision === 'approved-with-rationale' && !splitRationale) {
|
|
551
|
+
throw new WorkflowError(errorCode, 'Plan Risk Audit approved-with-rationale requires an explicit split rationale.', {
|
|
552
|
+
...details,
|
|
553
|
+
taskId: task.id,
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
if (decision !== 'approved-with-rationale' && splitRationale !== null) {
|
|
557
|
+
throw new WorkflowError(errorCode, 'Plan Risk Audit splitRationale is only valid with approved-with-rationale.', {
|
|
558
|
+
...details,
|
|
559
|
+
taskId: task.id,
|
|
560
|
+
decision,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
function validateCurrentPlanRiskAudit(store, task, audit, expectedStepIds, options) {
|
|
565
|
+
const taskRoot = store.taskRoot(task.projectId, task.id);
|
|
566
|
+
const briefHash = store.hashArtifact(taskRoot, 'brief.md');
|
|
567
|
+
const planHash = store.hashArtifact(taskRoot, 'plan.md');
|
|
568
|
+
if (audit.briefHash !== briefHash || audit.planHash !== planHash) {
|
|
569
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Plan Risk Audit is stale for the current Brief or Plan.', {
|
|
570
|
+
taskId: task.id,
|
|
571
|
+
auditEventId: audit.eventId,
|
|
572
|
+
auditBriefHash: audit.briefHash,
|
|
573
|
+
currentBriefHash: briefHash,
|
|
574
|
+
auditPlanHash: audit.planHash,
|
|
575
|
+
currentPlanHash: planHash,
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
if (audit.knowledgeMapRevision !== task.knowledgeMapRevision || audit.knowledgeMapHash !== task.knowledgeMapHash) {
|
|
579
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Plan Risk Audit is stale for the current Task knowledge binding.', {
|
|
580
|
+
taskId: task.id,
|
|
581
|
+
auditEventId: audit.eventId,
|
|
582
|
+
auditKnowledgeMapRevision: audit.knowledgeMapRevision,
|
|
583
|
+
currentKnowledgeMapRevision: task.knowledgeMapRevision,
|
|
584
|
+
auditKnowledgeMapHash: audit.knowledgeMapHash,
|
|
585
|
+
currentKnowledgeMapHash: task.knowledgeMapHash,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
if (options.requireCurrentTaskRevision && audit.taskRevision !== task.revision) {
|
|
589
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Plan Risk Audit must bind the current Task revision before execution authorization.', {
|
|
590
|
+
taskId: task.id,
|
|
591
|
+
auditEventId: audit.eventId,
|
|
592
|
+
auditTaskRevision: audit.taskRevision,
|
|
593
|
+
currentTaskRevision: task.revision,
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
if (audit.taskRevision > task.revision) {
|
|
597
|
+
throw new WorkflowError('STATE_CORRUPT', 'Plan Risk Audit refers to a future Task revision.', {
|
|
598
|
+
taskId: task.id,
|
|
599
|
+
auditEventId: audit.eventId,
|
|
600
|
+
auditTaskRevision: audit.taskRevision,
|
|
601
|
+
currentTaskRevision: task.revision,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
validateStepClassificationSet(task, audit.stepClassifications, audit.decision, audit.splitRationale, expectedStepIds, 'TRANSITION_BLOCKED', {
|
|
605
|
+
taskId: task.id,
|
|
606
|
+
auditEventId: audit.eventId,
|
|
607
|
+
expectedStepIds,
|
|
608
|
+
});
|
|
609
|
+
if (audit.decision === 'split-required' || audit.decision === 'stop-escalate') {
|
|
610
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Plan Risk Audit decision ${audit.decision} blocks execution authorization.`, {
|
|
611
|
+
taskId: task.id,
|
|
612
|
+
auditEventId: audit.eventId,
|
|
613
|
+
decision: audit.decision,
|
|
614
|
+
requiredAction: audit.decision === 'split-required'
|
|
615
|
+
? 'Split the guarded work into separate Tasks and replace the Plan.'
|
|
616
|
+
: 'Escalate the Plan to the coordinator before continuing.',
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
return audit.stepClassifications
|
|
620
|
+
.filter((classification) => classification.reviewRequired)
|
|
621
|
+
.map((classification) => classification.stepId);
|
|
622
|
+
}
|
|
623
|
+
function validateStepClassification(task, classification, errorCode, details) {
|
|
624
|
+
if (!Array.isArray(classification.categories)) {
|
|
625
|
+
throw new WorkflowError(errorCode, 'Plan Risk Audit categories must be an array.', {
|
|
626
|
+
...details,
|
|
627
|
+
taskId: task.id,
|
|
628
|
+
stepId: classification.stepId,
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
if (typeof classification.reviewRequired !== 'boolean') {
|
|
632
|
+
throw new WorkflowError(errorCode, 'Plan Risk Audit reviewRequired must be boolean.', {
|
|
633
|
+
...details,
|
|
634
|
+
taskId: task.id,
|
|
635
|
+
stepId: classification.stepId,
|
|
636
|
+
reviewRequired: classification.reviewRequired,
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
const normalizedCategories = classification.categories.map((category) => normalizeGuardedRiskCategory(category, errorCode, {
|
|
640
|
+
...details,
|
|
641
|
+
taskId: task.id,
|
|
642
|
+
stepId: classification.stepId,
|
|
643
|
+
}));
|
|
644
|
+
assertUniqueValues(normalizedCategories, errorCode, 'Plan Risk Audit categories must be unique per Step.', {
|
|
645
|
+
...details,
|
|
646
|
+
taskId: task.id,
|
|
647
|
+
stepId: classification.stepId,
|
|
648
|
+
categories: normalizedCategories,
|
|
649
|
+
});
|
|
650
|
+
const canonicalCategories = [...normalizedCategories]
|
|
651
|
+
.sort((left, right) => compareGuardedRiskCategories(left, right));
|
|
652
|
+
if (JSON.stringify(canonicalCategories) !== JSON.stringify(normalizedCategories)) {
|
|
653
|
+
throw new WorkflowError(errorCode, 'Plan Risk Audit categories must be unique and in canonical order.', {
|
|
654
|
+
...details,
|
|
655
|
+
taskId: task.id,
|
|
656
|
+
stepId: classification.stepId,
|
|
657
|
+
expectedCategories: canonicalCategories,
|
|
658
|
+
actualCategories: normalizedCategories,
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
const expectedReviewRequired = normalizedCategories.length > 0;
|
|
662
|
+
if (classification.reviewRequired !== expectedReviewRequired) {
|
|
663
|
+
throw new WorkflowError(errorCode, 'Plan Risk Audit reviewRequired must match category presence for each Step.', {
|
|
664
|
+
...details,
|
|
665
|
+
taskId: task.id,
|
|
666
|
+
stepId: classification.stepId,
|
|
667
|
+
categories: normalizedCategories,
|
|
668
|
+
reviewRequired: classification.reviewRequired,
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
const failureModes = normalizeStringArray(classification.failureModes, 'Plan Risk Audit failureModes', errorCode, { ...details, taskId: task.id, stepId: classification.stepId });
|
|
672
|
+
const requiredEvidence = normalizeStringArray(classification.requiredEvidence, 'Plan Risk Audit requiredEvidence', errorCode, { ...details, taskId: task.id, stepId: classification.stepId });
|
|
673
|
+
if (expectedReviewRequired && (failureModes.length === 0 || requiredEvidence.length === 0)) {
|
|
674
|
+
throw new WorkflowError(errorCode, 'Every guarded Step must declare failure modes and required evidence.', {
|
|
675
|
+
...details,
|
|
676
|
+
taskId: task.id,
|
|
677
|
+
stepId: classification.stepId,
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
if (!expectedReviewRequired && (failureModes.length > 0 || requiredEvidence.length > 0)) {
|
|
681
|
+
throw new WorkflowError(errorCode, 'An unguarded Step cannot declare guarded failure modes or evidence.', {
|
|
682
|
+
...details,
|
|
683
|
+
taskId: task.id,
|
|
684
|
+
stepId: classification.stepId,
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
const step = task.steps.find((candidate) => candidate.id === classification.stepId);
|
|
688
|
+
if (step) {
|
|
689
|
+
const missingChecks = requiredEvidence.filter((evidence) => !step.checks.includes(evidence));
|
|
690
|
+
if (missingChecks.length > 0) {
|
|
691
|
+
throw new WorkflowError(errorCode, 'Plan Risk Audit required evidence must be backed by exact executable Step checks.', {
|
|
692
|
+
...details,
|
|
693
|
+
taskId: task.id,
|
|
694
|
+
stepId: classification.stepId,
|
|
695
|
+
missingChecks,
|
|
696
|
+
declaredChecks: step.checks,
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
function buildTaskStepDefinitionHashes(task) {
|
|
702
|
+
return new Map(task.steps.map((step) => [step.id, sha256Hex(canonicalJsonStringify(stripTaskStepRuntime(step)))]));
|
|
703
|
+
}
|
|
704
|
+
function stripTaskStepRuntime(step) {
|
|
705
|
+
const { status: _status, evidence: _evidence, ...definition } = step;
|
|
706
|
+
return definition;
|
|
707
|
+
}
|
|
708
|
+
function assertRecordableTask(task) {
|
|
709
|
+
if (!PLAN_RISK_RECORDABLE_TASK_STATUSES.has(task.status)) {
|
|
710
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} cannot record a Plan Risk Audit while ${task.status}.`, { taskId: task.id, status: task.status });
|
|
711
|
+
}
|
|
712
|
+
if (!task.planHash || task.steps.length === 0) {
|
|
713
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Plan Risk Audit recording requires a current Task Plan with explicit Steps.', { taskId: task.id, status: task.status });
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
function readStoredPlanRiskAuditEvents(store, task) {
|
|
717
|
+
const taskRoot = store.taskRoot(task.projectId, task.id);
|
|
718
|
+
return store.readSidecarEvents(taskRoot, PLAN_RISK_AUDIT_SIDECAR, {
|
|
719
|
+
validateEvent: (event, context) => validateStoredPlanRiskAuditEvent(event, task, context.file, context.line),
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
function assertPlanRiskAuditAppendAllowed(task, boundary, existingEvents) {
|
|
723
|
+
if (task.status === 'awaiting_execution_authorization')
|
|
724
|
+
return;
|
|
725
|
+
if (existingEvents.length > 0) {
|
|
726
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Replacing a Plan Risk Audit requires returning the Task to awaiting_execution_authorization through a legitimate replan.', {
|
|
727
|
+
taskId: task.id,
|
|
728
|
+
status: task.status,
|
|
729
|
+
existingAuditCount: existingEvents.length,
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
if (boundary.scope !== 'legacy-remaining-scope') {
|
|
733
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Mid-execution Plan Risk Audit recording is limited to the first legacy bootstrap audit.', {
|
|
734
|
+
taskId: task.id,
|
|
735
|
+
status: task.status,
|
|
736
|
+
scope: boundary.scope,
|
|
737
|
+
postureEventType: boundary.postureEventType,
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
function normalizePlanRiskDecision(decision, errorCode, details) {
|
|
742
|
+
if (typeof decision !== 'string' || !PLAN_RISK_DECISIONS.has(decision)) {
|
|
743
|
+
throw new WorkflowError(errorCode, 'Plan Risk Audit decision is invalid.', {
|
|
744
|
+
...details,
|
|
745
|
+
decision,
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
return decision;
|
|
749
|
+
}
|
|
750
|
+
function normalizeAdoptionPostureEventType(value, file, line, eventId) {
|
|
751
|
+
if (typeof value !== 'string' || !ADOPTION_POSTURE_EVENT_TYPES.has(value)) {
|
|
752
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption posture eventType is invalid.', {
|
|
753
|
+
file,
|
|
754
|
+
line,
|
|
755
|
+
eventId,
|
|
756
|
+
eventType: value,
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
return value;
|
|
760
|
+
}
|
|
761
|
+
function normalizeGuardedRiskCategory(value, errorCode, details) {
|
|
762
|
+
if (typeof value !== 'string' || !GUARDED_RISK_CATEGORY_SET.has(value)) {
|
|
763
|
+
throw new WorkflowError(errorCode, 'Plan Risk Audit category is invalid.', {
|
|
764
|
+
...details,
|
|
765
|
+
category: value,
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
return value;
|
|
769
|
+
}
|
|
770
|
+
function normalizePlanRiskClassifications(value, errorCode, details) {
|
|
771
|
+
if (!Array.isArray(value)) {
|
|
772
|
+
throw new WorkflowError(errorCode, 'Plan Risk Audit stepClassifications must be an array.', details);
|
|
773
|
+
}
|
|
774
|
+
return value;
|
|
775
|
+
}
|
|
776
|
+
function normalizeStringArray(value, label, errorCode, details) {
|
|
777
|
+
if (!Array.isArray(value)) {
|
|
778
|
+
throw new WorkflowError(errorCode, `${label} must be an array.`, details);
|
|
779
|
+
}
|
|
780
|
+
const normalized = value.map((entry) => normalizeNonEmptyString(entry, label, { code: errorCode, ...details }));
|
|
781
|
+
assertUniqueValues(normalized, errorCode, `${label} must contain unique values.`, details);
|
|
782
|
+
return normalized;
|
|
783
|
+
}
|
|
784
|
+
function normalizeOptionalNonEmptyString(value, label, details = {}) {
|
|
785
|
+
if (value === null)
|
|
786
|
+
return null;
|
|
787
|
+
return normalizeNonEmptyString(value, label, details);
|
|
788
|
+
}
|
|
789
|
+
function normalizeNonEmptyString(value, label, details = {}) {
|
|
790
|
+
const errorCode = details.code ?? 'INVALID_ARGUMENT';
|
|
791
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
792
|
+
throw new WorkflowError(errorCode, `${label} must be a non-empty string.`, details);
|
|
793
|
+
}
|
|
794
|
+
return value.trim();
|
|
795
|
+
}
|
|
796
|
+
function assertPositiveInteger(value, label, file, line, eventId, errorCode = 'STATE_CORRUPT') {
|
|
797
|
+
if (!Number.isInteger(value) || Number(value) < 1) {
|
|
798
|
+
throw new WorkflowError(errorCode, `${label} must be a positive integer.`, {
|
|
799
|
+
...(file !== undefined ? { file } : {}),
|
|
800
|
+
...(line !== undefined ? { line } : {}),
|
|
801
|
+
...(eventId !== undefined ? { eventId } : {}),
|
|
802
|
+
value,
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
function assertHash64String(value, label, errorCode, details) {
|
|
807
|
+
if (typeof value !== 'string') {
|
|
808
|
+
throw new WorkflowError(errorCode, `${label} must be a sha256 hex digest.`, {
|
|
809
|
+
...details,
|
|
810
|
+
value,
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
try {
|
|
814
|
+
assertHash64(value, label);
|
|
815
|
+
}
|
|
816
|
+
catch (error) {
|
|
817
|
+
if (error instanceof WorkflowError) {
|
|
818
|
+
throw new WorkflowError(errorCode, error.message, { ...details, ...error.details });
|
|
819
|
+
}
|
|
820
|
+
throw error;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
function assertPlainObject(value, label, file, line) {
|
|
824
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
825
|
+
throw new WorkflowError('STATE_CORRUPT', `${label} must be an object.`, { file, line });
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
function compareGuardedRiskCategories(left, right) {
|
|
829
|
+
return (GUARDED_RISK_CATEGORY_INDEX.get(left) ?? Number.MAX_SAFE_INTEGER)
|
|
830
|
+
- (GUARDED_RISK_CATEGORY_INDEX.get(right) ?? Number.MAX_SAFE_INTEGER);
|
|
831
|
+
}
|
|
832
|
+
function assertUniqueValues(values, errorCode, message, details) {
|
|
833
|
+
const unique = new Set(values);
|
|
834
|
+
if (unique.size !== values.length) {
|
|
835
|
+
throw new WorkflowError(errorCode, message, details);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
function assertExactIdSet(expected, actual, errorCode, message, details) {
|
|
839
|
+
if (expected.length !== actual.length) {
|
|
840
|
+
throw new WorkflowError(errorCode, message, details);
|
|
841
|
+
}
|
|
842
|
+
const expectedSet = new Set(expected);
|
|
843
|
+
if (actual.some((value) => !expectedSet.has(value))) {
|
|
844
|
+
throw new WorkflowError(errorCode, message, details);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
//# sourceMappingURL=plan-risk.js.map
|