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,920 @@
|
|
|
1
|
+
import { PROTOCOL_VERSION, STATE_SCHEMA_VERSION } from '../contracts.js';
|
|
2
|
+
import { WorkflowError } from '../errors.js';
|
|
3
|
+
import { createUlid } from '../ulid.js';
|
|
4
|
+
import { assertHash64, canonicalJsonStringify, computeSidecarEventHash, sha256Hex, } from './store-sidecars.js';
|
|
5
|
+
const ADOPTION_POSTURE_SIDECAR = 'adoption-posture.jsonl';
|
|
6
|
+
const ACTIVE_TASK_BASELINE_STATUSES = new Set([
|
|
7
|
+
'planning',
|
|
8
|
+
'awaiting_execution_authorization',
|
|
9
|
+
'ready',
|
|
10
|
+
'in_progress',
|
|
11
|
+
'validating',
|
|
12
|
+
'needs_fix',
|
|
13
|
+
'awaiting_final_acceptance',
|
|
14
|
+
'ready_to_merge',
|
|
15
|
+
'merging',
|
|
16
|
+
'blocked',
|
|
17
|
+
]);
|
|
18
|
+
const EVENT_TYPES = new Set(['project-registration', 'adoption-apply']);
|
|
19
|
+
export function readCurrentAdoptionPosture(store, projectId) {
|
|
20
|
+
const sidecarPath = store.projectSidecarPath(projectId, ADOPTION_POSTURE_SIDECAR);
|
|
21
|
+
const events = store.readSidecarEvents(store.projectRoot(projectId), ADOPTION_POSTURE_SIDECAR, {
|
|
22
|
+
validateEvent: (event, context) => validateAdoptionPostureEvent(event, projectId, context.file, context.line),
|
|
23
|
+
});
|
|
24
|
+
validateAdoptionPostureHistory(events, projectId, sidecarPath);
|
|
25
|
+
return events.at(-1) ?? null;
|
|
26
|
+
}
|
|
27
|
+
export function status(input, currentPosture) {
|
|
28
|
+
const prepared = buildAdoptionPreparation(input, currentPosture);
|
|
29
|
+
return {
|
|
30
|
+
status: prepared.status,
|
|
31
|
+
safe: prepared.safe,
|
|
32
|
+
blockers: prepared.blockers,
|
|
33
|
+
currentPosture: prepared.currentPosture,
|
|
34
|
+
baselineDigestHash: prepared.baselineDigestHash,
|
|
35
|
+
sidecarBaselineHash: prepared.sidecarBaselineHash,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function buildAdoptionPreparation(input, currentPosture) {
|
|
39
|
+
const normalized = normalizePreparationInput(input);
|
|
40
|
+
const posture = currentPosture ?? null;
|
|
41
|
+
const sidecarBaselineHash = computeSidecarBaselineHash(normalized.sidecarInventory);
|
|
42
|
+
const terminalMilestones = normalized.milestones
|
|
43
|
+
.filter((milestone) => milestone.status === 'accepted' || milestone.status === 'cancelled')
|
|
44
|
+
.map((milestone) => ({
|
|
45
|
+
milestoneId: milestone.id,
|
|
46
|
+
revision: milestone.revision,
|
|
47
|
+
status: milestone.status,
|
|
48
|
+
}))
|
|
49
|
+
.sort(compareById('milestoneId'));
|
|
50
|
+
const terminalTasks = normalized.tasks
|
|
51
|
+
.filter((task) => task.status === 'merged' || task.status === 'cancelled')
|
|
52
|
+
.map((task) => ({
|
|
53
|
+
taskId: task.id,
|
|
54
|
+
revision: task.revision,
|
|
55
|
+
status: task.status,
|
|
56
|
+
}))
|
|
57
|
+
.sort(compareById('taskId'));
|
|
58
|
+
const activeTasks = normalized.tasks
|
|
59
|
+
.filter((task) => ACTIVE_TASK_BASELINE_STATUSES.has(task.status))
|
|
60
|
+
.map((task) => buildActiveTaskBaseline(task))
|
|
61
|
+
.sort(compareById('taskId'));
|
|
62
|
+
const tasksSummary = normalized.tasks
|
|
63
|
+
.map((task) => ({ taskId: task.id, revision: task.revision, status: task.status }))
|
|
64
|
+
.sort(compareById('taskId'));
|
|
65
|
+
const milestonesSummary = normalized.milestones
|
|
66
|
+
.map((milestone) => ({ milestoneId: milestone.id, revision: milestone.revision, status: milestone.status }))
|
|
67
|
+
.sort(compareById('milestoneId'));
|
|
68
|
+
const adoptionBoundaryAt = normalized.now.toISOString();
|
|
69
|
+
const baselineDigestHash = sha256Hex(canonicalJsonStringify({
|
|
70
|
+
version: 'alpha6-adoption-baseline-v1',
|
|
71
|
+
projectId: normalized.projectId,
|
|
72
|
+
repositoryIdentity: normalized.repositoryIdentity,
|
|
73
|
+
packageVersion: normalized.packageVersion,
|
|
74
|
+
protocolVersion: normalized.protocolVersion,
|
|
75
|
+
stateSchemaVersion: normalized.stateSchemaVersion,
|
|
76
|
+
sidecarBaselineHash,
|
|
77
|
+
terminalMilestones,
|
|
78
|
+
terminalTasks,
|
|
79
|
+
activeTasks,
|
|
80
|
+
tasksSummary,
|
|
81
|
+
milestonesSummary,
|
|
82
|
+
}));
|
|
83
|
+
if (posture) {
|
|
84
|
+
assertAdoptionBaselinePreserved(posture, normalized.tasks, normalized.milestones);
|
|
85
|
+
return {
|
|
86
|
+
status: 'applied',
|
|
87
|
+
safe: normalized.blockers.length === 0,
|
|
88
|
+
blockers: normalized.blockers,
|
|
89
|
+
currentPosture: posture,
|
|
90
|
+
baselineDigestHash,
|
|
91
|
+
sidecarBaselineHash,
|
|
92
|
+
confirmationCodeBindingHash: null,
|
|
93
|
+
confirmationCode: null,
|
|
94
|
+
candidateEvent: null,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const candidateEvent = createAdoptionPostureEvent({
|
|
98
|
+
eventType: 'adoption-apply',
|
|
99
|
+
actor: null,
|
|
100
|
+
packageVersion: normalized.packageVersion,
|
|
101
|
+
projectId: normalized.projectId,
|
|
102
|
+
protocolVersion: normalized.protocolVersion,
|
|
103
|
+
stateSchemaVersion: normalized.stateSchemaVersion,
|
|
104
|
+
adoptionBoundaryAt,
|
|
105
|
+
baselineDigestHash,
|
|
106
|
+
sidecarBaselineHash,
|
|
107
|
+
repositoryIdentity: normalized.repositoryIdentity,
|
|
108
|
+
terminalMilestones,
|
|
109
|
+
terminalTasks,
|
|
110
|
+
activeTasks,
|
|
111
|
+
previousEventHash: null,
|
|
112
|
+
now: normalized.now,
|
|
113
|
+
confirmationCodeBindingHash: null,
|
|
114
|
+
});
|
|
115
|
+
const bindingHash = computeAdoptionConfirmationBindingHash(candidateEvent);
|
|
116
|
+
candidateEvent.confirmationCodeBindingHash = bindingHash;
|
|
117
|
+
candidateEvent.eventHash = computeSidecarEventHash(candidateEvent);
|
|
118
|
+
return {
|
|
119
|
+
status: normalized.blockers.length === 0 ? 'ready' : 'missing',
|
|
120
|
+
safe: normalized.blockers.length === 0,
|
|
121
|
+
blockers: normalized.blockers,
|
|
122
|
+
currentPosture: null,
|
|
123
|
+
baselineDigestHash,
|
|
124
|
+
sidecarBaselineHash,
|
|
125
|
+
confirmationCodeBindingHash: bindingHash,
|
|
126
|
+
confirmationCode: adoptionConfirmationCode(bindingHash),
|
|
127
|
+
candidateEvent,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
export function initializeProjectRegistrationAdoption(store, input) {
|
|
131
|
+
const normalized = normalizeProjectRegistrationInput(input);
|
|
132
|
+
const current = readCurrentAdoptionPosture(store, normalized.projectId);
|
|
133
|
+
if (current)
|
|
134
|
+
return current;
|
|
135
|
+
const event = buildProjectRegistrationAdoptionEvent({
|
|
136
|
+
projectId: normalized.projectId,
|
|
137
|
+
repositoryIdentity: normalized.repositoryIdentity,
|
|
138
|
+
packageVersion: normalized.packageVersion,
|
|
139
|
+
protocolVersion: normalized.protocolVersion,
|
|
140
|
+
stateSchemaVersion: normalized.stateSchemaVersion,
|
|
141
|
+
tasks: [],
|
|
142
|
+
milestones: [],
|
|
143
|
+
sidecarInventory: [],
|
|
144
|
+
blockers: [],
|
|
145
|
+
now: normalized.now,
|
|
146
|
+
});
|
|
147
|
+
return store.appendSidecarEvent(store.projectRoot(normalized.projectId), ADOPTION_POSTURE_SIDECAR, event, {
|
|
148
|
+
validateEvent: (candidate, context) => validateAdoptionPostureEvent(candidate, normalized.projectId, context.file, context.line),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
export function applyAdoptionPosture(store, input, actor, confirmationCode) {
|
|
152
|
+
const normalizedActor = normalizeHumanActor(actor, 'Adoption apply actor');
|
|
153
|
+
const currentPosture = readCurrentAdoptionPosture(store, input.projectId);
|
|
154
|
+
if (currentPosture) {
|
|
155
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Project ${input.projectId} already has an adoption posture.`, {
|
|
156
|
+
projectId: input.projectId,
|
|
157
|
+
eventType: currentPosture.eventType,
|
|
158
|
+
eventId: currentPosture.eventId,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
const prepared = buildAdoptionPreparation(input, currentPosture);
|
|
162
|
+
if (prepared.status === 'applied' || !prepared.candidateEvent || !prepared.confirmationCodeBindingHash || !prepared.confirmationCode) {
|
|
163
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Project ${input.projectId} does not have a pending adoption candidate.`, {
|
|
164
|
+
projectId: input.projectId,
|
|
165
|
+
status: prepared.status,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
if (prepared.blockers.length > 0) {
|
|
169
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Alpha.6 adoption apply requires a safe boundary.', {
|
|
170
|
+
projectId: input.projectId,
|
|
171
|
+
blockers: prepared.blockers,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
if (confirmationCode.trim() !== prepared.confirmationCode) {
|
|
175
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Adoption confirmation does not match the current project baseline.', {
|
|
176
|
+
projectId: input.projectId,
|
|
177
|
+
expectedConfirmationCode: prepared.confirmationCode,
|
|
178
|
+
actualConfirmationCode: confirmationCode.trim(),
|
|
179
|
+
confirmationCodeBindingHash: prepared.confirmationCodeBindingHash,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
const event = {
|
|
183
|
+
...prepared.candidateEvent,
|
|
184
|
+
adoptionBoundaryAt: input.now.toISOString(),
|
|
185
|
+
recordedAt: input.now.toISOString(),
|
|
186
|
+
actor: normalizedActor,
|
|
187
|
+
confirmationCodeBindingHash: prepared.confirmationCodeBindingHash,
|
|
188
|
+
};
|
|
189
|
+
event.eventHash = computeSidecarEventHash(event);
|
|
190
|
+
const projectRoot = store.projectRoot(input.projectId);
|
|
191
|
+
return store.appendSidecarEvent(projectRoot, ADOPTION_POSTURE_SIDECAR, event, {
|
|
192
|
+
validateEvent: (candidate, context) => validateAdoptionPostureEvent(candidate, input.projectId, context.file, context.line),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
export function buildProjectRegistrationAdoptionEvent(input) {
|
|
196
|
+
const normalized = normalizePreparationInput(input);
|
|
197
|
+
const currentPosture = null;
|
|
198
|
+
const prepared = buildAdoptionPreparation(normalized, currentPosture);
|
|
199
|
+
if (prepared.currentPosture) {
|
|
200
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Project ${normalized.projectId} already has an adoption posture sidecar.`, {
|
|
201
|
+
projectId: normalized.projectId,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
return createAdoptionPostureEvent({
|
|
205
|
+
eventType: 'project-registration',
|
|
206
|
+
actor: null,
|
|
207
|
+
packageVersion: normalized.packageVersion,
|
|
208
|
+
projectId: normalized.projectId,
|
|
209
|
+
protocolVersion: normalized.protocolVersion,
|
|
210
|
+
stateSchemaVersion: normalized.stateSchemaVersion,
|
|
211
|
+
adoptionBoundaryAt: normalized.now.toISOString(),
|
|
212
|
+
baselineDigestHash: prepared.baselineDigestHash,
|
|
213
|
+
sidecarBaselineHash: prepared.sidecarBaselineHash,
|
|
214
|
+
repositoryIdentity: normalized.repositoryIdentity,
|
|
215
|
+
terminalMilestones: [],
|
|
216
|
+
terminalTasks: [],
|
|
217
|
+
activeTasks: [],
|
|
218
|
+
previousEventHash: null,
|
|
219
|
+
now: normalized.now,
|
|
220
|
+
confirmationCodeBindingHash: null,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
export function assertAdoptionBaselinePreserved(posture, tasks, milestones) {
|
|
224
|
+
const tasksById = new Map(tasks.map((task) => [task.id, task]));
|
|
225
|
+
const milestonesById = new Map(milestones.map((milestone) => [milestone.id, milestone]));
|
|
226
|
+
for (const terminalMilestone of posture.terminalMilestones) {
|
|
227
|
+
const current = milestonesById.get(terminalMilestone.milestoneId);
|
|
228
|
+
if (!current || current.revision !== terminalMilestone.revision || current.status !== terminalMilestone.status) {
|
|
229
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption posture terminal Milestone baseline no longer matches canonical state.', {
|
|
230
|
+
milestoneId: terminalMilestone.milestoneId,
|
|
231
|
+
expectedRevision: terminalMilestone.revision,
|
|
232
|
+
actualRevision: current?.revision ?? null,
|
|
233
|
+
expectedStatus: terminalMilestone.status,
|
|
234
|
+
actualStatus: current?.status ?? null,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
for (const terminalTask of posture.terminalTasks) {
|
|
239
|
+
const current = tasksById.get(terminalTask.taskId);
|
|
240
|
+
if (!current || current.revision !== terminalTask.revision || current.status !== terminalTask.status) {
|
|
241
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption posture terminal Task baseline no longer matches canonical state.', {
|
|
242
|
+
taskId: terminalTask.taskId,
|
|
243
|
+
expectedRevision: terminalTask.revision,
|
|
244
|
+
actualRevision: current?.revision ?? null,
|
|
245
|
+
expectedStatus: terminalTask.status,
|
|
246
|
+
actualStatus: current?.status ?? null,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
for (const baseline of posture.activeTasks) {
|
|
251
|
+
const current = tasksById.get(baseline.taskId);
|
|
252
|
+
if (!current) {
|
|
253
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption posture active Task baseline no longer matches canonical state.', {
|
|
254
|
+
taskId: baseline.taskId,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
for (const completed of baseline.completedSteps) {
|
|
258
|
+
const step = current.steps.find((candidate) => candidate.id === completed.stepId);
|
|
259
|
+
if (!step) {
|
|
260
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption posture completed Step baseline is missing from the current Task.', {
|
|
261
|
+
taskId: baseline.taskId,
|
|
262
|
+
stepId: completed.stepId,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
if (step.status !== completed.status) {
|
|
266
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption posture completed Step baseline status no longer matches the current Task.', {
|
|
267
|
+
taskId: baseline.taskId,
|
|
268
|
+
stepId: completed.stepId,
|
|
269
|
+
expectedStatus: completed.status,
|
|
270
|
+
actualStatus: step.status,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
const actualEvidenceHash = step.evidence ? sha256Hex(canonicalJsonStringify(step.evidence)) : null;
|
|
274
|
+
if (actualEvidenceHash !== completed.evidenceHash) {
|
|
275
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption posture completed Step evidence hash no longer matches the current Task.', {
|
|
276
|
+
taskId: baseline.taskId,
|
|
277
|
+
stepId: completed.stepId,
|
|
278
|
+
expectedEvidenceHash: completed.evidenceHash,
|
|
279
|
+
actualEvidenceHash,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function validateAdoptionPostureEvent(event, expectedProjectId, file, line) {
|
|
286
|
+
assertPlainObject(event, 'Adoption posture event', file, line);
|
|
287
|
+
assertExactKeys(event, [
|
|
288
|
+
'eventId',
|
|
289
|
+
'projectId',
|
|
290
|
+
'eventType',
|
|
291
|
+
'actor',
|
|
292
|
+
'packageVersion',
|
|
293
|
+
'protocolVersion',
|
|
294
|
+
'stateSchemaVersion',
|
|
295
|
+
'adoptionBoundaryAt',
|
|
296
|
+
'baselineDigestHash',
|
|
297
|
+
'sidecarBaselineHash',
|
|
298
|
+
'confirmationCodeBindingHash',
|
|
299
|
+
'repositoryIdentity',
|
|
300
|
+
'terminalMilestones',
|
|
301
|
+
'terminalTasks',
|
|
302
|
+
'activeTasks',
|
|
303
|
+
'previousEventHash',
|
|
304
|
+
'recordedAt',
|
|
305
|
+
'eventHash',
|
|
306
|
+
], 'Adoption posture event contains unexpected fields.', file, line);
|
|
307
|
+
requireNonEmptyString(event.eventId, 'Adoption posture eventId', { file, line });
|
|
308
|
+
if (event.projectId !== expectedProjectId) {
|
|
309
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption posture event project binding does not match its project root.', {
|
|
310
|
+
file,
|
|
311
|
+
line,
|
|
312
|
+
expectedProjectId,
|
|
313
|
+
actualProjectId: event.projectId,
|
|
314
|
+
eventId: event.eventId,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
if (!EVENT_TYPES.has(event.eventType)) {
|
|
318
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption posture eventType is invalid.', {
|
|
319
|
+
file,
|
|
320
|
+
line,
|
|
321
|
+
eventId: event.eventId,
|
|
322
|
+
eventType: event.eventType,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
requireNonEmptyString(event.packageVersion, 'Adoption posture packageVersion', { file, line, eventId: event.eventId });
|
|
326
|
+
if (event.protocolVersion !== PROTOCOL_VERSION) {
|
|
327
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption posture protocolVersion is invalid.', {
|
|
328
|
+
file,
|
|
329
|
+
line,
|
|
330
|
+
eventId: event.eventId,
|
|
331
|
+
protocolVersion: event.protocolVersion,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
if (event.stateSchemaVersion !== STATE_SCHEMA_VERSION) {
|
|
335
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption posture stateSchemaVersion is invalid.', {
|
|
336
|
+
file,
|
|
337
|
+
line,
|
|
338
|
+
eventId: event.eventId,
|
|
339
|
+
stateSchemaVersion: event.stateSchemaVersion,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
requireIsoTimestamp(event.adoptionBoundaryAt, 'Adoption posture adoptionBoundaryAt', { file, line, eventId: event.eventId });
|
|
343
|
+
requireIsoTimestamp(event.recordedAt, 'Adoption posture recordedAt', { file, line, eventId: event.eventId });
|
|
344
|
+
if (typeof event.baselineDigestHash !== 'string') {
|
|
345
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption posture baselineDigestHash must be sha256.', {
|
|
346
|
+
file,
|
|
347
|
+
line,
|
|
348
|
+
eventId: event.eventId,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
if (typeof event.sidecarBaselineHash !== 'string') {
|
|
352
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption posture sidecarBaselineHash must be sha256.', {
|
|
353
|
+
file,
|
|
354
|
+
line,
|
|
355
|
+
eventId: event.eventId,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
assertHash64(event.baselineDigestHash, 'Adoption posture baselineDigestHash');
|
|
359
|
+
assertHash64(event.sidecarBaselineHash, 'Adoption posture sidecarBaselineHash');
|
|
360
|
+
if (event.confirmationCodeBindingHash !== null) {
|
|
361
|
+
if (typeof event.confirmationCodeBindingHash !== 'string') {
|
|
362
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption posture confirmationCodeBindingHash must be null or sha256.', {
|
|
363
|
+
file,
|
|
364
|
+
line,
|
|
365
|
+
eventId: event.eventId,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
assertHash64(event.confirmationCodeBindingHash, 'Adoption posture confirmationCodeBindingHash');
|
|
369
|
+
}
|
|
370
|
+
if (event.previousEventHash !== null) {
|
|
371
|
+
if (typeof event.previousEventHash !== 'string') {
|
|
372
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption posture previousEventHash must be null or sha256.', {
|
|
373
|
+
file,
|
|
374
|
+
line,
|
|
375
|
+
eventId: event.eventId,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
assertHash64(event.previousEventHash, 'Adoption posture previousEventHash');
|
|
379
|
+
}
|
|
380
|
+
if (typeof event.eventHash !== 'string') {
|
|
381
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption posture eventHash must be sha256.', {
|
|
382
|
+
file,
|
|
383
|
+
line,
|
|
384
|
+
eventId: event.eventId,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
assertHash64(event.eventHash, 'Adoption posture eventHash');
|
|
388
|
+
validateRepositoryIdentity(event.repositoryIdentity, file, line, event.eventId);
|
|
389
|
+
validateTerminalMilestones(event, file, line);
|
|
390
|
+
validateTerminalTasks(event, file, line);
|
|
391
|
+
validateActiveTasks(event, file, line);
|
|
392
|
+
if (event.eventType === 'project-registration') {
|
|
393
|
+
if (event.actor !== null || event.confirmationCodeBindingHash !== null) {
|
|
394
|
+
throw new WorkflowError('STATE_CORRUPT', 'Project registration adoption posture must not record actor or confirmation binding.', {
|
|
395
|
+
file,
|
|
396
|
+
line,
|
|
397
|
+
eventId: event.eventId,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
else if (event.actor === null) {
|
|
402
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption apply posture must record the human actor.', {
|
|
403
|
+
file,
|
|
404
|
+
line,
|
|
405
|
+
eventId: event.eventId,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
validateStoredHumanActor(event.actor, 'Adoption apply actor', {
|
|
410
|
+
file,
|
|
411
|
+
line,
|
|
412
|
+
eventId: event.eventId,
|
|
413
|
+
});
|
|
414
|
+
if (event.confirmationCodeBindingHash === null) {
|
|
415
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption apply posture must record confirmationCodeBindingHash.', {
|
|
416
|
+
file,
|
|
417
|
+
line,
|
|
418
|
+
eventId: event.eventId,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
function validateAdoptionPostureHistory(events, projectId, file) {
|
|
424
|
+
let seenApply = false;
|
|
425
|
+
for (const [index, event] of events.entries()) {
|
|
426
|
+
const previous = index > 0 ? events[index - 1] : null;
|
|
427
|
+
if (!previous) {
|
|
428
|
+
if (event.previousEventHash !== null) {
|
|
429
|
+
throw new WorkflowError('STATE_CORRUPT', `Adoption posture first event must not have previousEventHash: ${file}`, {
|
|
430
|
+
line: index + 1,
|
|
431
|
+
eventId: event.eventId,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
if (event.previousEventHash !== previous.eventHash) {
|
|
437
|
+
throw new WorkflowError('STATE_CORRUPT', `Adoption posture chain is broken: ${file}`, {
|
|
438
|
+
line: index + 1,
|
|
439
|
+
eventId: event.eventId,
|
|
440
|
+
previousEventId: previous.eventId,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
if (event.projectId !== previous.projectId) {
|
|
444
|
+
throw new WorkflowError('STATE_CORRUPT', `Adoption posture project changed across history: ${file}`, {
|
|
445
|
+
line: index + 1,
|
|
446
|
+
projectId,
|
|
447
|
+
eventId: event.eventId,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
if (previous.eventType === 'adoption-apply') {
|
|
451
|
+
throw new WorkflowError('STATE_CORRUPT', `Adoption posture cannot append events after adoption-apply: ${file}`, {
|
|
452
|
+
line: index + 1,
|
|
453
|
+
eventId: event.eventId,
|
|
454
|
+
previousEventId: previous.eventId,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
if (event.eventType === 'project-registration') {
|
|
458
|
+
throw new WorkflowError('STATE_CORRUPT', `Adoption posture cannot append a later project-registration event: ${file}`, {
|
|
459
|
+
line: index + 1,
|
|
460
|
+
eventId: event.eventId,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (event.eventType === 'adoption-apply')
|
|
465
|
+
seenApply = true;
|
|
466
|
+
if (seenApply && index !== events.length - 1) {
|
|
467
|
+
throw new WorkflowError('STATE_CORRUPT', `Adoption posture cannot contain events after adoption-apply: ${file}`, {
|
|
468
|
+
line: index + 1,
|
|
469
|
+
eventId: event.eventId,
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
if (event.eventType === 'adoption-apply') {
|
|
473
|
+
const expectedBinding = computeAdoptionConfirmationBindingHash(event);
|
|
474
|
+
if (event.confirmationCodeBindingHash !== expectedBinding) {
|
|
475
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption posture confirmationCodeBindingHash is inconsistent.', {
|
|
476
|
+
line: index + 1,
|
|
477
|
+
eventId: event.eventId,
|
|
478
|
+
expectedBindingHash: expectedBinding,
|
|
479
|
+
actualBindingHash: event.confirmationCodeBindingHash,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
function createAdoptionPostureEvent(options) {
|
|
486
|
+
const event = {
|
|
487
|
+
eventId: `ADO-${createUlid(options.now.getTime())}`,
|
|
488
|
+
projectId: options.projectId,
|
|
489
|
+
eventType: options.eventType,
|
|
490
|
+
actor: options.actor,
|
|
491
|
+
packageVersion: options.packageVersion,
|
|
492
|
+
protocolVersion: options.protocolVersion,
|
|
493
|
+
stateSchemaVersion: options.stateSchemaVersion,
|
|
494
|
+
adoptionBoundaryAt: options.adoptionBoundaryAt,
|
|
495
|
+
baselineDigestHash: options.baselineDigestHash,
|
|
496
|
+
sidecarBaselineHash: options.sidecarBaselineHash,
|
|
497
|
+
confirmationCodeBindingHash: options.confirmationCodeBindingHash,
|
|
498
|
+
repositoryIdentity: options.repositoryIdentity,
|
|
499
|
+
terminalMilestones: options.terminalMilestones,
|
|
500
|
+
terminalTasks: options.terminalTasks,
|
|
501
|
+
activeTasks: options.activeTasks,
|
|
502
|
+
previousEventHash: options.previousEventHash,
|
|
503
|
+
recordedAt: options.now.toISOString(),
|
|
504
|
+
eventHash: '',
|
|
505
|
+
};
|
|
506
|
+
event.eventHash = computeSidecarEventHash(event);
|
|
507
|
+
return event;
|
|
508
|
+
}
|
|
509
|
+
function computeAdoptionConfirmationBindingHash(event) {
|
|
510
|
+
return sha256Hex(canonicalJsonStringify(adoptionConfirmationProjection(event)));
|
|
511
|
+
}
|
|
512
|
+
function adoptionConfirmationCode(bindingHash) {
|
|
513
|
+
return `ADA-${bindingHash.slice(0, 24).toUpperCase()}`;
|
|
514
|
+
}
|
|
515
|
+
function buildActiveTaskBaseline(task) {
|
|
516
|
+
const stepIds = task.steps.map((step) => step.id);
|
|
517
|
+
assertUniqueStrings(stepIds, 'Task Step ids must be unique for adoption baseline.', {
|
|
518
|
+
taskId: task.id,
|
|
519
|
+
stepIds,
|
|
520
|
+
});
|
|
521
|
+
const completedSteps = task.steps
|
|
522
|
+
.filter((step) => step.status === 'completed' || step.status === 'skipped')
|
|
523
|
+
.map((step) => ({
|
|
524
|
+
stepId: step.id,
|
|
525
|
+
status: step.status,
|
|
526
|
+
evidenceHash: step.evidence ? sha256Hex(canonicalJsonStringify(step.evidence)) : null,
|
|
527
|
+
}))
|
|
528
|
+
.sort(compareById('stepId'));
|
|
529
|
+
const completedStepIds = completedSteps.map((step) => step.stepId);
|
|
530
|
+
const remainingStepIds = task.steps
|
|
531
|
+
.filter((step) => step.status !== 'completed' && step.status !== 'skipped')
|
|
532
|
+
.map((step) => step.id)
|
|
533
|
+
.sort();
|
|
534
|
+
assertUniqueStrings(completedStepIds, 'Adoption completed Step ids must be unique.', {
|
|
535
|
+
taskId: task.id,
|
|
536
|
+
completedStepIds,
|
|
537
|
+
});
|
|
538
|
+
assertUniqueStrings(remainingStepIds, 'Adoption remaining Step ids must be unique.', {
|
|
539
|
+
taskId: task.id,
|
|
540
|
+
remainingStepIds,
|
|
541
|
+
});
|
|
542
|
+
const overlap = remainingStepIds.filter((stepId) => completedStepIds.includes(stepId));
|
|
543
|
+
if (overlap.length > 0) {
|
|
544
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption task baseline cannot overlap completed and remaining Steps.', {
|
|
545
|
+
taskId: task.id,
|
|
546
|
+
overlappingStepIds: overlap,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
return {
|
|
550
|
+
taskId: task.id,
|
|
551
|
+
taskRevision: task.revision,
|
|
552
|
+
status: task.status,
|
|
553
|
+
completedSteps,
|
|
554
|
+
remainingStepIds,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
function computeSidecarBaselineHash(entries) {
|
|
558
|
+
const normalized = entries
|
|
559
|
+
.filter((entry) => entry.sidecarName !== ADOPTION_POSTURE_SIDECAR)
|
|
560
|
+
.map((entry) => normalizeSidecarInventoryEntry(entry))
|
|
561
|
+
.sort(compareInventoryEntries);
|
|
562
|
+
return sha256Hex(canonicalJsonStringify(normalized));
|
|
563
|
+
}
|
|
564
|
+
function normalizePreparationInput(input) {
|
|
565
|
+
requireNonEmptyString(input.projectId, 'Adoption projectId', { projectId: input.projectId });
|
|
566
|
+
requireNonEmptyString(input.packageVersion, 'Adoption packageVersion', { packageVersion: input.packageVersion });
|
|
567
|
+
if (input.protocolVersion !== PROTOCOL_VERSION) {
|
|
568
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Alpha.6 adoption helper requires protocol version 1.', {
|
|
569
|
+
protocolVersion: input.protocolVersion,
|
|
570
|
+
expectedProtocolVersion: PROTOCOL_VERSION,
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
if (input.stateSchemaVersion !== STATE_SCHEMA_VERSION) {
|
|
574
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Alpha.6 adoption helper requires canonical state schema version 2.', {
|
|
575
|
+
stateSchemaVersion: input.stateSchemaVersion,
|
|
576
|
+
expectedStateSchemaVersion: STATE_SCHEMA_VERSION,
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
if (!(input.now instanceof Date) || Number.isNaN(input.now.getTime())) {
|
|
580
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Adoption now must be a valid Date.');
|
|
581
|
+
}
|
|
582
|
+
return {
|
|
583
|
+
...input,
|
|
584
|
+
repositoryIdentity: normalizeRepositoryIdentity(input.repositoryIdentity),
|
|
585
|
+
blockers: normalizeBlockers(input.blockers),
|
|
586
|
+
sidecarInventory: normalizeSidecarInventory(input.sidecarInventory),
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
function normalizeProjectRegistrationInput(input) {
|
|
590
|
+
requireInvalidArgumentNonEmptyString(input.projectId, 'Adoption projectId', { projectId: input.projectId });
|
|
591
|
+
requireInvalidArgumentNonEmptyString(input.packageVersion, 'Adoption packageVersion', { packageVersion: input.packageVersion });
|
|
592
|
+
if (input.protocolVersion !== PROTOCOL_VERSION) {
|
|
593
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Alpha.6 adoption helper requires protocol version 1.', {
|
|
594
|
+
protocolVersion: input.protocolVersion,
|
|
595
|
+
expectedProtocolVersion: PROTOCOL_VERSION,
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
if (input.stateSchemaVersion !== STATE_SCHEMA_VERSION) {
|
|
599
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Alpha.6 adoption helper requires canonical state schema version 2.', {
|
|
600
|
+
stateSchemaVersion: input.stateSchemaVersion,
|
|
601
|
+
expectedStateSchemaVersion: STATE_SCHEMA_VERSION,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
if (!(input.now instanceof Date) || Number.isNaN(input.now.getTime())) {
|
|
605
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Adoption now must be a valid Date.');
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
...input,
|
|
609
|
+
repositoryIdentity: normalizeRepositoryIdentity(input.repositoryIdentity),
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
function normalizeRepositoryIdentity(identity) {
|
|
613
|
+
return {
|
|
614
|
+
repositoryRoot: requireNonEmptyString(identity.repositoryRoot, 'Adoption repositoryRoot', { repositoryRoot: identity.repositoryRoot }),
|
|
615
|
+
gitCommonDir: requireNonEmptyString(identity.gitCommonDir, 'Adoption gitCommonDir', { gitCommonDir: identity.gitCommonDir }),
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
function normalizeSidecarInventoryEntry(entry) {
|
|
619
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
620
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Adoption sidecar inventory entry must be an object.');
|
|
621
|
+
}
|
|
622
|
+
if (entry.scope !== 'project' && entry.scope !== 'task' && entry.scope !== 'milestone') {
|
|
623
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Adoption sidecar inventory scope is invalid.', {
|
|
624
|
+
scope: entry.scope,
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
const ownerId = requireNonEmptyString(entry.ownerId, 'Adoption sidecar inventory ownerId', {
|
|
628
|
+
ownerId: entry.ownerId,
|
|
629
|
+
scope: entry.scope,
|
|
630
|
+
});
|
|
631
|
+
const sidecarName = requireNonEmptyString(entry.sidecarName, 'Adoption sidecar inventory sidecarName', {
|
|
632
|
+
ownerId,
|
|
633
|
+
scope: entry.scope,
|
|
634
|
+
});
|
|
635
|
+
assertHash64(entry.contentHash, 'Adoption sidecar inventory contentHash');
|
|
636
|
+
return { scope: entry.scope, ownerId, sidecarName, contentHash: entry.contentHash };
|
|
637
|
+
}
|
|
638
|
+
function normalizeSidecarInventory(entries) {
|
|
639
|
+
if (!Array.isArray(entries)) {
|
|
640
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Adoption sidecar inventory must be an array.');
|
|
641
|
+
}
|
|
642
|
+
const normalized = entries.map((entry) => normalizeSidecarInventoryEntry(entry));
|
|
643
|
+
const identityKeys = normalized.map((entry) => `${entry.scope}\u0000${entry.ownerId}\u0000${entry.sidecarName}`);
|
|
644
|
+
if (new Set(identityKeys).size !== identityKeys.length) {
|
|
645
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Adoption sidecar inventory contains duplicate sidecar identities.', {
|
|
646
|
+
duplicates: identityKeys,
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
return normalized;
|
|
650
|
+
}
|
|
651
|
+
function normalizeBlockers(blockers) {
|
|
652
|
+
if (!Array.isArray(blockers)) {
|
|
653
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Adoption blockers must be an array.');
|
|
654
|
+
}
|
|
655
|
+
return [...new Set(blockers.map((blocker) => requireNonEmptyString(blocker, 'Adoption blocker', { blocker })))].sort();
|
|
656
|
+
}
|
|
657
|
+
function validateRepositoryIdentity(repositoryIdentity, file, line, eventId) {
|
|
658
|
+
assertPlainObject(repositoryIdentity, 'Adoption posture repositoryIdentity', file, line);
|
|
659
|
+
assertExactKeys(repositoryIdentity, ['repositoryRoot', 'gitCommonDir'], 'Adoption posture repositoryIdentity contains unexpected fields.', file, line);
|
|
660
|
+
requireNonEmptyString(repositoryIdentity.repositoryRoot, 'Adoption posture repositoryRoot', { file, line, eventId });
|
|
661
|
+
requireNonEmptyString(repositoryIdentity.gitCommonDir, 'Adoption posture gitCommonDir', { file, line, eventId });
|
|
662
|
+
}
|
|
663
|
+
function validateTerminalMilestones(event, file, line) {
|
|
664
|
+
if (!Array.isArray(event.terminalMilestones)) {
|
|
665
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption posture terminalMilestones must be an array.', {
|
|
666
|
+
file,
|
|
667
|
+
line,
|
|
668
|
+
eventId: event.eventId,
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
const milestoneIds = [];
|
|
672
|
+
for (const record of event.terminalMilestones) {
|
|
673
|
+
assertPlainObject(record, 'Adoption terminal Milestone record', file, line);
|
|
674
|
+
assertExactKeys(record, ['milestoneId', 'revision', 'status'], 'Adoption terminal Milestone record contains unexpected fields.', file, line);
|
|
675
|
+
milestoneIds.push(requireNonEmptyString(record.milestoneId, 'Adoption terminal Milestone milestoneId', { file, line, eventId: event.eventId }));
|
|
676
|
+
requirePositiveInteger(record.revision, 'Adoption terminal Milestone revision', { file, line, eventId: event.eventId });
|
|
677
|
+
if (record.status !== 'accepted' && record.status !== 'cancelled') {
|
|
678
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption terminal Milestone status is invalid.', {
|
|
679
|
+
file,
|
|
680
|
+
line,
|
|
681
|
+
eventId: event.eventId,
|
|
682
|
+
status: record.status,
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
assertUniqueStrings(milestoneIds, 'Adoption terminal Milestone ids must be unique.', {
|
|
687
|
+
file,
|
|
688
|
+
line,
|
|
689
|
+
eventId: event.eventId,
|
|
690
|
+
milestoneIds,
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
function validateTerminalTasks(event, file, line) {
|
|
694
|
+
if (!Array.isArray(event.terminalTasks)) {
|
|
695
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption posture terminalTasks must be an array.', {
|
|
696
|
+
file,
|
|
697
|
+
line,
|
|
698
|
+
eventId: event.eventId,
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
const taskIds = [];
|
|
702
|
+
for (const record of event.terminalTasks) {
|
|
703
|
+
assertPlainObject(record, 'Adoption terminal Task record', file, line);
|
|
704
|
+
assertExactKeys(record, ['taskId', 'revision', 'status'], 'Adoption terminal Task record contains unexpected fields.', file, line);
|
|
705
|
+
taskIds.push(requireNonEmptyString(record.taskId, 'Adoption terminal Task taskId', { file, line, eventId: event.eventId }));
|
|
706
|
+
requirePositiveInteger(record.revision, 'Adoption terminal Task revision', { file, line, eventId: event.eventId });
|
|
707
|
+
if (record.status !== 'merged' && record.status !== 'cancelled') {
|
|
708
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption terminal Task status is invalid.', {
|
|
709
|
+
file,
|
|
710
|
+
line,
|
|
711
|
+
eventId: event.eventId,
|
|
712
|
+
status: record.status,
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
assertUniqueStrings(taskIds, 'Adoption terminal Task ids must be unique.', {
|
|
717
|
+
file,
|
|
718
|
+
line,
|
|
719
|
+
eventId: event.eventId,
|
|
720
|
+
taskIds,
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
function validateActiveTasks(event, file, line) {
|
|
724
|
+
if (!Array.isArray(event.activeTasks)) {
|
|
725
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption posture activeTasks must be an array.', {
|
|
726
|
+
file,
|
|
727
|
+
line,
|
|
728
|
+
eventId: event.eventId,
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
const terminalTaskIds = new Set(event.terminalTasks.map((record) => record.taskId));
|
|
732
|
+
const activeTaskIds = [];
|
|
733
|
+
for (const baseline of event.activeTasks) {
|
|
734
|
+
assertPlainObject(baseline, 'Adoption active Task baseline', file, line);
|
|
735
|
+
assertExactKeys(baseline, ['taskId', 'taskRevision', 'status', 'completedSteps', 'remainingStepIds'], 'Adoption active Task baseline contains unexpected fields.', file, line);
|
|
736
|
+
const taskId = requireNonEmptyString(baseline.taskId, 'Adoption active Task baseline taskId', { file, line, eventId: event.eventId });
|
|
737
|
+
activeTaskIds.push(taskId);
|
|
738
|
+
if (terminalTaskIds.has(taskId)) {
|
|
739
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption posture cannot classify the same Task as both terminal and active.', {
|
|
740
|
+
file,
|
|
741
|
+
line,
|
|
742
|
+
eventId: event.eventId,
|
|
743
|
+
taskId,
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
requirePositiveInteger(baseline.taskRevision, 'Adoption active Task baseline taskRevision', { file, line, eventId: event.eventId, taskId });
|
|
747
|
+
if (!ACTIVE_TASK_BASELINE_STATUSES.has(baseline.status)) {
|
|
748
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption active Task baseline status is invalid.', {
|
|
749
|
+
file,
|
|
750
|
+
line,
|
|
751
|
+
eventId: event.eventId,
|
|
752
|
+
taskId,
|
|
753
|
+
status: baseline.status,
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
if (!Array.isArray(baseline.completedSteps)) {
|
|
757
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption active Task baseline completedSteps must be an array.', {
|
|
758
|
+
file,
|
|
759
|
+
line,
|
|
760
|
+
eventId: event.eventId,
|
|
761
|
+
taskId,
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
if (!Array.isArray(baseline.remainingStepIds)) {
|
|
765
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption active Task baseline remainingStepIds must be an array.', {
|
|
766
|
+
file,
|
|
767
|
+
line,
|
|
768
|
+
eventId: event.eventId,
|
|
769
|
+
taskId,
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
const completedStepIds = [];
|
|
773
|
+
for (const completed of baseline.completedSteps) {
|
|
774
|
+
assertPlainObject(completed, 'Adoption completed Step baseline', file, line);
|
|
775
|
+
assertExactKeys(completed, ['stepId', 'status', 'evidenceHash'], 'Adoption completed Step baseline contains unexpected fields.', file, line);
|
|
776
|
+
const stepId = requireNonEmptyString(completed.stepId, 'Adoption completed Step stepId', { file, line, eventId: event.eventId, taskId });
|
|
777
|
+
completedStepIds.push(stepId);
|
|
778
|
+
if (completed.status !== 'completed' && completed.status !== 'skipped') {
|
|
779
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption completed Step status is invalid.', {
|
|
780
|
+
file,
|
|
781
|
+
line,
|
|
782
|
+
eventId: event.eventId,
|
|
783
|
+
taskId,
|
|
784
|
+
stepId,
|
|
785
|
+
status: completed.status,
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
if (completed.evidenceHash !== null) {
|
|
789
|
+
if (typeof completed.evidenceHash !== 'string') {
|
|
790
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption completed Step evidenceHash must be null or sha256.', {
|
|
791
|
+
file,
|
|
792
|
+
line,
|
|
793
|
+
eventId: event.eventId,
|
|
794
|
+
taskId,
|
|
795
|
+
stepId,
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
assertHash64(completed.evidenceHash, 'Adoption completed Step evidenceHash');
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
assertUniqueStrings(completedStepIds, 'Adoption completed Step ids must be unique.', {
|
|
802
|
+
file,
|
|
803
|
+
line,
|
|
804
|
+
eventId: event.eventId,
|
|
805
|
+
taskId,
|
|
806
|
+
completedStepIds,
|
|
807
|
+
});
|
|
808
|
+
const remainingStepIds = baseline.remainingStepIds.map((stepId) => requireNonEmptyString(stepId, 'Adoption remaining Step id', { file, line, eventId: event.eventId, taskId }));
|
|
809
|
+
assertUniqueStrings(remainingStepIds, 'Adoption remaining Step ids must be unique.', {
|
|
810
|
+
file,
|
|
811
|
+
line,
|
|
812
|
+
eventId: event.eventId,
|
|
813
|
+
taskId,
|
|
814
|
+
remainingStepIds,
|
|
815
|
+
});
|
|
816
|
+
const overlap = remainingStepIds.filter((stepId) => completedStepIds.includes(stepId));
|
|
817
|
+
if (overlap.length > 0) {
|
|
818
|
+
throw new WorkflowError('STATE_CORRUPT', 'Adoption active Task baseline cannot overlap completed and remaining Steps.', {
|
|
819
|
+
file,
|
|
820
|
+
line,
|
|
821
|
+
eventId: event.eventId,
|
|
822
|
+
taskId,
|
|
823
|
+
overlappingStepIds: overlap,
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
assertUniqueStrings(activeTaskIds, 'Adoption active Task ids must be unique.', {
|
|
828
|
+
file,
|
|
829
|
+
line,
|
|
830
|
+
eventId: event.eventId,
|
|
831
|
+
activeTaskIds,
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
function normalizeHumanActor(value, label) {
|
|
835
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
836
|
+
throw new WorkflowError('INVALID_ARGUMENT', `${label} must be a non-empty string.`, { actor: value });
|
|
837
|
+
}
|
|
838
|
+
const actor = value.trim();
|
|
839
|
+
if (actor.startsWith('agent:')) {
|
|
840
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `${label} must be a human actor.`, {
|
|
841
|
+
actor,
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
return actor;
|
|
845
|
+
}
|
|
846
|
+
function validateStoredHumanActor(value, label, context) {
|
|
847
|
+
const actor = requireNonEmptyString(value, label, context);
|
|
848
|
+
if (actor.startsWith('agent:')) {
|
|
849
|
+
throw new WorkflowError('STATE_CORRUPT', `${label} must be a human actor.`, {
|
|
850
|
+
...context,
|
|
851
|
+
actor,
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
return actor;
|
|
855
|
+
}
|
|
856
|
+
function requireNonEmptyString(value, label, context) {
|
|
857
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
858
|
+
throw new WorkflowError('STATE_CORRUPT', `${label} must be a non-empty string.`, context);
|
|
859
|
+
}
|
|
860
|
+
return value.trim();
|
|
861
|
+
}
|
|
862
|
+
function requireInvalidArgumentNonEmptyString(value, label, context) {
|
|
863
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
864
|
+
throw new WorkflowError('INVALID_ARGUMENT', `${label} must be a non-empty string.`, context);
|
|
865
|
+
}
|
|
866
|
+
return value.trim();
|
|
867
|
+
}
|
|
868
|
+
function requireIsoTimestamp(value, label, context) {
|
|
869
|
+
const timestamp = requireNonEmptyString(value, label, context);
|
|
870
|
+
if (Number.isNaN(Date.parse(timestamp))) {
|
|
871
|
+
throw new WorkflowError('STATE_CORRUPT', `${label} must be an ISO timestamp.`, context);
|
|
872
|
+
}
|
|
873
|
+
return timestamp;
|
|
874
|
+
}
|
|
875
|
+
function requirePositiveInteger(value, label, context) {
|
|
876
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
877
|
+
throw new WorkflowError('STATE_CORRUPT', `${label} must be a positive integer.`, context);
|
|
878
|
+
}
|
|
879
|
+
return value;
|
|
880
|
+
}
|
|
881
|
+
function assertPlainObject(value, label, file, line) {
|
|
882
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
883
|
+
throw new WorkflowError('STATE_CORRUPT', `${label} must be an object.`, { file, line });
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
function assertExactKeys(record, allowedKeys, message, file, line) {
|
|
887
|
+
const allowed = new Set(allowedKeys);
|
|
888
|
+
const extraKeys = Object.keys(record).filter((key) => !allowed.has(key));
|
|
889
|
+
if (extraKeys.length > 0) {
|
|
890
|
+
throw new WorkflowError('STATE_CORRUPT', message, { file, line, extraKeys });
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
function assertUniqueStrings(values, message, context) {
|
|
894
|
+
if (new Set(values).size !== values.length) {
|
|
895
|
+
throw new WorkflowError('STATE_CORRUPT', message, context);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
function compareInventoryEntries(left, right) {
|
|
899
|
+
return `${left.scope}\u0000${left.ownerId}\u0000${left.sidecarName}\u0000${left.contentHash}`
|
|
900
|
+
.localeCompare(`${right.scope}\u0000${right.ownerId}\u0000${right.sidecarName}\u0000${right.contentHash}`);
|
|
901
|
+
}
|
|
902
|
+
function compareById(key) {
|
|
903
|
+
return (left, right) => left[key].localeCompare(right[key]);
|
|
904
|
+
}
|
|
905
|
+
function adoptionConfirmationProjection(event) {
|
|
906
|
+
return {
|
|
907
|
+
version: 'alpha6-adoption-confirmation-v1',
|
|
908
|
+
projectId: event.projectId,
|
|
909
|
+
repositoryIdentity: event.repositoryIdentity,
|
|
910
|
+
packageVersion: event.packageVersion,
|
|
911
|
+
protocolVersion: event.protocolVersion,
|
|
912
|
+
stateSchemaVersion: event.stateSchemaVersion,
|
|
913
|
+
baselineDigestHash: event.baselineDigestHash,
|
|
914
|
+
sidecarBaselineHash: event.sidecarBaselineHash,
|
|
915
|
+
terminalMilestones: event.terminalMilestones,
|
|
916
|
+
terminalTasks: event.terminalTasks,
|
|
917
|
+
activeTasks: event.activeTasks,
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
//# sourceMappingURL=adoption.js.map
|