codex-workflow-v2 2.0.0-beta.1 → 2.0.0-beta.11
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 +94 -12
- package/dist/src/alpha6/adoption.js +13 -3
- package/dist/src/alpha6/adoption.js.map +1 -1
- package/dist/src/alpha6/handoff.d.ts +26 -2
- package/dist/src/alpha6/handoff.js +277 -29
- package/dist/src/alpha6/handoff.js.map +1 -1
- package/dist/src/alpha6/mechanical-feasibility.d.ts +7 -0
- package/dist/src/alpha6/mechanical-feasibility.js +303 -0
- package/dist/src/alpha6/mechanical-feasibility.js.map +1 -0
- package/dist/src/alpha6/milestone.d.ts +37 -2
- package/dist/src/alpha6/milestone.js +472 -33
- package/dist/src/alpha6/milestone.js.map +1 -1
- package/dist/src/alpha6/plan-integrity.d.ts +14 -0
- package/dist/src/alpha6/plan-integrity.js +127 -0
- package/dist/src/alpha6/plan-integrity.js.map +1 -0
- package/dist/src/alpha6/plan-risk.d.ts +1 -0
- package/dist/src/alpha6/plan-risk.js +15 -7
- package/dist/src/alpha6/plan-risk.js.map +1 -1
- package/dist/src/alpha6/remediation.d.ts +2 -1
- package/dist/src/alpha6/remediation.js +186 -9
- package/dist/src/alpha6/remediation.js.map +1 -1
- package/dist/src/alpha6/review.d.ts +3 -0
- package/dist/src/alpha6/review.js +24 -0
- package/dist/src/alpha6/review.js.map +1 -1
- package/dist/src/cli.js +486 -46
- package/dist/src/cli.js.map +1 -1
- package/dist/src/contracts.d.ts +238 -6
- package/dist/src/contracts.js +1 -1
- package/dist/src/credential-output.d.ts +10 -0
- package/dist/src/credential-output.js +73 -0
- package/dist/src/credential-output.js.map +1 -0
- package/dist/src/dependency-provenance.d.ts +19 -0
- package/dist/src/dependency-provenance.js +185 -0
- package/dist/src/dependency-provenance.js.map +1 -0
- package/dist/src/errors.d.ts +1 -1
- package/dist/src/errors.js.map +1 -1
- package/dist/src/git.d.ts +1 -1
- package/dist/src/git.js +13 -5
- package/dist/src/git.js.map +1 -1
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.js +2 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/lifecycle/canonical-hash.d.ts +6 -0
- package/dist/src/lifecycle/canonical-hash.js +24 -0
- package/dist/src/lifecycle/canonical-hash.js.map +1 -0
- package/dist/src/lifecycle/catalog.d.ts +26 -0
- package/dist/src/lifecycle/catalog.js +208 -0
- package/dist/src/lifecycle/catalog.js.map +1 -0
- package/dist/src/lifecycle/compiler-inspection.d.ts +48 -0
- package/dist/src/lifecycle/compiler-inspection.js +29 -0
- package/dist/src/lifecycle/compiler-inspection.js.map +1 -0
- package/dist/src/lifecycle/core-static-readiness.d.ts +6 -0
- package/dist/src/lifecycle/core-static-readiness.js +130 -0
- package/dist/src/lifecycle/core-static-readiness.js.map +1 -0
- package/dist/src/lifecycle/corrective-replan-authority.d.ts +53 -0
- package/dist/src/lifecycle/corrective-replan-authority.js +139 -0
- package/dist/src/lifecycle/corrective-replan-authority.js.map +1 -0
- package/dist/src/lifecycle/corrective-replan-binding-manifest.d.ts +21 -0
- package/dist/src/lifecycle/corrective-replan-binding-manifest.js +35 -0
- package/dist/src/lifecycle/corrective-replan-binding-manifest.js.map +1 -0
- package/dist/src/lifecycle/corrective-replan-credential-core.d.ts +86 -0
- package/dist/src/lifecycle/corrective-replan-credential-core.js +563 -0
- package/dist/src/lifecycle/corrective-replan-credential-core.js.map +1 -0
- package/dist/src/lifecycle/corrective-replan-credential-schema.d.ts +17 -0
- package/dist/src/lifecycle/corrective-replan-credential-schema.js +44 -0
- package/dist/src/lifecycle/corrective-replan-credential-schema.js.map +1 -0
- package/dist/src/lifecycle/corrective-replan-credentials.d.ts +47 -0
- package/dist/src/lifecycle/corrective-replan-credentials.js +103 -0
- package/dist/src/lifecycle/corrective-replan-credentials.js.map +1 -0
- package/dist/src/lifecycle/corrective-replan.d.ts +64 -0
- package/dist/src/lifecycle/corrective-replan.js +553 -0
- package/dist/src/lifecycle/corrective-replan.js.map +1 -0
- package/dist/src/lifecycle/evaluator.d.ts +2 -0
- package/dist/src/lifecycle/evaluator.js +35 -0
- package/dist/src/lifecycle/evaluator.js.map +1 -0
- package/dist/src/lifecycle/fingerprint.d.ts +31 -0
- package/dist/src/lifecycle/fingerprint.js +388 -0
- package/dist/src/lifecycle/fingerprint.js.map +1 -0
- package/dist/src/lifecycle/immutable.d.ts +1 -0
- package/dist/src/lifecycle/immutable.js +10 -0
- package/dist/src/lifecycle/immutable.js.map +1 -0
- package/dist/src/lifecycle/implementation-table.d.ts +29 -0
- package/dist/src/lifecycle/implementation-table.js +87 -0
- package/dist/src/lifecycle/implementation-table.js.map +1 -0
- package/dist/src/lifecycle/index.d.ts +4 -0
- package/dist/src/lifecycle/index.js +5 -0
- package/dist/src/lifecycle/index.js.map +1 -0
- package/dist/src/lifecycle/schema-artifact.d.ts +12 -0
- package/dist/src/lifecycle/schema-artifact.js +214 -0
- package/dist/src/lifecycle/schema-artifact.js.map +1 -0
- package/dist/src/lifecycle/semantic-registry.d.ts +6 -0
- package/dist/src/lifecycle/semantic-registry.js +489 -0
- package/dist/src/lifecycle/semantic-registry.js.map +1 -0
- package/dist/src/lifecycle/types.d.ts +718 -0
- package/dist/src/lifecycle/types.js +4 -0
- package/dist/src/lifecycle/types.js.map +1 -0
- package/dist/src/observation.d.ts +20 -0
- package/dist/src/observation.js +171 -0
- package/dist/src/observation.js.map +1 -0
- package/dist/src/reviewer.d.ts +43 -1
- package/dist/src/reviewer.js +203 -50
- package/dist/src/reviewer.js.map +1 -1
- package/dist/src/state/corrective-replan-executor.d.ts +64 -0
- package/dist/src/state/corrective-replan-executor.js +608 -0
- package/dist/src/state/corrective-replan-executor.js.map +1 -0
- package/dist/src/state/corrective-replan-public-schema.d.ts +3 -0
- package/dist/src/state/corrective-replan-public-schema.js +43 -0
- package/dist/src/state/corrective-replan-public-schema.js.map +1 -0
- package/dist/src/state/corrective-replan-public.d.ts +95 -0
- package/dist/src/state/corrective-replan-public.js +665 -0
- package/dist/src/state/corrective-replan-public.js.map +1 -0
- package/dist/src/state/corrective-replan-transaction.d.ts +160 -0
- package/dist/src/state/corrective-replan-transaction.js +730 -0
- package/dist/src/state/corrective-replan-transaction.js.map +1 -0
- package/dist/src/state/corrective-yield-executor.d.ts +31 -0
- package/dist/src/state/corrective-yield-executor.js +236 -0
- package/dist/src/state/corrective-yield-executor.js.map +1 -0
- package/dist/src/state/corrective-yield-transaction.d.ts +97 -0
- package/dist/src/state/corrective-yield-transaction.js +591 -0
- package/dist/src/state/corrective-yield-transaction.js.map +1 -0
- package/dist/src/state/lock.d.ts +76 -3
- package/dist/src/state/lock.js +611 -30
- package/dist/src/state/lock.js.map +1 -1
- package/dist/src/state/store.d.ts +3 -0
- package/dist/src/state/store.js +65 -12
- package/dist/src/state/store.js.map +1 -1
- package/dist/src/transition-core.d.ts +67 -0
- package/dist/src/transition-core.js +318 -0
- package/dist/src/transition-core.js.map +1 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/version.js.map +1 -1
- package/dist/src/workflow.d.ts +88 -24
- package/dist/src/workflow.js +1647 -270
- package/dist/src/workflow.js.map +1 -1
- package/docs/autonomy-guardrails.md +61 -1
- package/docs/beta11-plan-integrity-recovery-brief.md +38 -0
- package/docs/beta2-initial-assembly-navigation-brief.md +616 -0
- package/docs/delegated-approval.md +26 -1
- package/docs/development-flow.md +63 -8
- package/docs/lifecycle/state-machine-stabilization.md +641 -0
- package/docs/pdf/README.md +24 -0
- package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
- package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
- package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
- package/docs/pdf/requirements.txt +1 -0
- package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +234 -0
- package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +334 -0
- package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +413 -0
- package/docs/problem-briefs/01-pre-implementation-integrity.md +478 -0
- package/docs/problem-briefs/02-minimal-step-integrity.md +411 -0
- package/docs/problem-briefs/03-minimal-agent-context-integrity.md +358 -0
- package/docs/problem-briefs/04-task-dependency-and-structural-replacement-integrity.md +566 -0
- package/docs/problem-briefs/BRIEF-TEMPLATE.md +56 -0
- package/docs/problem-briefs/README.md +120 -0
- package/docs/problem-briefs/evidence/p01-mechanical-feasibility-corpus.md +90 -0
- package/docs/problem-briefs/evidence/signal-v4-pre-m3-replay.md +246 -0
- package/docs/release.md +30 -6
- package/docs/split-required-recovery.md +19 -25
- package/docs/stable-release-defect-register.md +465 -3
- package/docs/updating-existing-project.md +59 -4
- package/docs/validation-report.md +124 -56
- package/package.json +9 -1
- package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +10 -3
- package/plugins/codex-workflow-gateway/references/protocol.md +92 -4
- package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +376 -0
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +263 -4
- package/references/state-machine.md +13 -6
- package/roles/delivery-coordinator.md +39 -0
- package/schemas/adoption-posture-event.schema.json +1 -1
- package/schemas/authorization-event.schema.json +64 -1
- package/schemas/context-envelope.schema.json +1 -1
- package/schemas/corrective-decision-event.schema.json +48 -3
- package/schemas/corrective-replan-credentials.private.schema.json +98 -0
- package/schemas/corrective-replan-execution-event.private.schema.json +53 -0
- package/schemas/corrective-replan-public.schema.json +142 -0
- package/schemas/corrective-replan-task-transaction.private.schema.json +151 -0
- package/schemas/corrective-yield-task-transaction.private.schema.json +98 -0
- package/schemas/milestone-scope-change-event.schema.json +6 -1
- package/schemas/milestone.schema.json +6 -1
- package/schemas/remediation-event.schema.json +11 -1
- package/schemas/task-handoff-event.schema.json +19 -3
- package/schemas/task.schema.json +24 -1
- package/schemas/transition-definition.schema.json +2609 -0
- package/schemas/transition-payloads.schema.json +140 -0
- package/scripts/generate-pdf-docs.py +512 -0
- package/scripts/run-pdf-docs.mjs +62 -0
|
@@ -2,9 +2,11 @@ import { existsSync, lstatSync, readFileSync, realpathSync, readdirSync, rmSync
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { WorkflowError } from '../errors.js';
|
|
4
4
|
import { createEntityId } from '../ulid.js';
|
|
5
|
+
import { readMilestoneAutonomyEvents } from '../alpha7/autonomy.js';
|
|
5
6
|
import { createJournalSnapshotFromContent, captureJournalTargetSnapshot, listMilestoneTransactionJournals, readJournalSnapshotContent, removeMilestoneTransactionJournal, resolveMilestoneTransactionTargetPath, updateMilestoneTransactionJournalPhase, writeMilestoneTransactionJournal, } from './journal.js';
|
|
6
7
|
import { assertHash64, assertNoSymlinkPathComponents, canonicalJsonStringify, computeSidecarEventHash, prepareSidecarAppend, sha256Hex, } from './store-sidecars.js';
|
|
7
8
|
const SCOPE_CHANGE_SIDECAR = 'scope-change-events.jsonl';
|
|
9
|
+
const INITIAL_PLAN_TARGETS = ['state.json', 'plan.json'];
|
|
8
10
|
const MILESTONE_TARGETS = ['state.json', 'plan.json', SCOPE_CHANGE_SIDECAR];
|
|
9
11
|
const SCOPE_CHANGE_ALLOWED_STATUSES = new Set([
|
|
10
12
|
'planning',
|
|
@@ -67,22 +69,172 @@ export function prepareMilestoneScopeChangeBundle(options) {
|
|
|
67
69
|
};
|
|
68
70
|
}
|
|
69
71
|
export function normalizeMilestonePlan(plan) {
|
|
70
|
-
|
|
72
|
+
const normalized = {
|
|
71
73
|
outcome: plan.outcome.trim(),
|
|
72
74
|
successSignal: plan.successSignal.trim(),
|
|
73
75
|
acceptance: [...plan.acceptance],
|
|
74
|
-
memberships: plan.memberships.map((membership) =>
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
memberships: plan.memberships.map((membership, index) => {
|
|
77
|
+
if (!membership || typeof membership !== 'object' || Array.isArray(membership)) {
|
|
78
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone membership must be an object.', {
|
|
79
|
+
membershipIndex: index,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (typeof membership.taskId !== 'string' || !membership.taskId.trim()) {
|
|
83
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone membership requires a Task ID.', {
|
|
84
|
+
membershipIndex: index,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (typeof membership.reason !== 'string') {
|
|
88
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone membership reason must be a string.', {
|
|
89
|
+
membershipIndex: index,
|
|
90
|
+
taskId: membership.taskId,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
if (!Array.isArray(membership.dependsOnTaskIds)) {
|
|
94
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Every Milestone membership must declare dependsOnTaskIds explicitly.', {
|
|
95
|
+
membershipIndex: index,
|
|
96
|
+
taskId: membership.taskId,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (membership.dependsOnTaskIds.some((taskId) => typeof taskId !== 'string' || !taskId.trim())) {
|
|
100
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone dependency IDs must be non-empty strings.', {
|
|
101
|
+
membershipIndex: index,
|
|
102
|
+
taskId: membership.taskId,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
taskId: membership.taskId.trim(),
|
|
107
|
+
disposition: membership.disposition,
|
|
108
|
+
reason: membership.reason.trim(),
|
|
109
|
+
dependsOnTaskIds: [...membership.dependsOnTaskIds].map((taskId) => taskId.trim()).sort(),
|
|
110
|
+
};
|
|
111
|
+
}),
|
|
78
112
|
checks: [...plan.checks],
|
|
79
|
-
taskIds: plan.memberships.map((membership) => membership.taskId),
|
|
113
|
+
taskIds: plan.memberships.map((membership) => membership.taskId.trim()),
|
|
114
|
+
};
|
|
115
|
+
assertCanonicalMilestoneDependencyGraph(normalized.memberships, 'INVALID_ARGUMENT', 'Milestone Plan');
|
|
116
|
+
return normalized;
|
|
117
|
+
}
|
|
118
|
+
export function assessMilestoneTaskDependencies(milestone, taskId, readTask) {
|
|
119
|
+
const membership = milestone.memberships.find((candidate) => candidate.taskId === taskId);
|
|
120
|
+
if (!membership) {
|
|
121
|
+
return {
|
|
122
|
+
dependencyState: milestone.memberships.every((candidate) => Array.isArray(candidate.dependsOnTaskIds))
|
|
123
|
+
? 'canonical'
|
|
124
|
+
: 'legacy-unknown',
|
|
125
|
+
dependsOnTaskIds: [],
|
|
126
|
+
unsatisfiedPredecessors: [],
|
|
127
|
+
runnable: false,
|
|
128
|
+
blockReason: `Task ${taskId} is not a Milestone member.`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if (milestone.memberships.some((candidate) => !Array.isArray(candidate.dependsOnTaskIds))) {
|
|
132
|
+
return {
|
|
133
|
+
dependencyState: 'legacy-unknown',
|
|
134
|
+
dependsOnTaskIds: [],
|
|
135
|
+
unsatisfiedPredecessors: [],
|
|
136
|
+
runnable: false,
|
|
137
|
+
blockReason: 'Milestone dependency graph is unknown for this legacy nonterminal Plan.',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
assertCanonicalMilestoneDependencyGraph(milestone.memberships, 'STATE_CORRUPT', `Milestone ${milestone.id}`);
|
|
141
|
+
const dependsOnTaskIds = [...(membership.dependsOnTaskIds ?? [])];
|
|
142
|
+
const unsatisfiedPredecessors = dependsOnTaskIds.flatMap((predecessorTaskId) => {
|
|
143
|
+
const predecessorMembership = milestone.memberships.find((candidate) => candidate.taskId === predecessorTaskId);
|
|
144
|
+
let predecessor = null;
|
|
145
|
+
try {
|
|
146
|
+
predecessor = readTask(predecessorTaskId);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (!(error instanceof WorkflowError) || error.code !== 'NOT_FOUND')
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
if (predecessorMembership?.disposition === 'required' && predecessor?.status === 'merged')
|
|
153
|
+
return [];
|
|
154
|
+
return [{
|
|
155
|
+
taskId: predecessorTaskId,
|
|
156
|
+
disposition: predecessorMembership?.disposition ?? 'missing',
|
|
157
|
+
status: predecessor?.status ?? 'missing',
|
|
158
|
+
}];
|
|
159
|
+
});
|
|
160
|
+
const runnable = membership.disposition === 'required' && unsatisfiedPredecessors.length === 0;
|
|
161
|
+
return {
|
|
162
|
+
dependencyState: 'canonical',
|
|
163
|
+
dependsOnTaskIds,
|
|
164
|
+
unsatisfiedPredecessors,
|
|
165
|
+
runnable,
|
|
166
|
+
blockReason: membership.disposition !== 'required'
|
|
167
|
+
? `Milestone membership is ${membership.disposition}, not required.`
|
|
168
|
+
: unsatisfiedPredecessors.length > 0
|
|
169
|
+
? 'Required predecessor Tasks have not all merged.'
|
|
170
|
+
: null,
|
|
80
171
|
};
|
|
81
172
|
}
|
|
173
|
+
export function assertMilestoneDependenciesDeclared(milestone) {
|
|
174
|
+
if (milestone.memberships.some((membership) => !Array.isArray(membership.dependsOnTaskIds))) {
|
|
175
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestone.id} has no canonical dependency graph.`, {
|
|
176
|
+
diagnosticCode: 'MILESTONE_DEPENDENCIES_UNKNOWN',
|
|
177
|
+
milestoneId: milestone.id,
|
|
178
|
+
membershipRevision: milestone.membershipRevision,
|
|
179
|
+
requiredAction: 'Apply an explicit Milestone scope change containing dependsOnTaskIds for every membership.',
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
assertCanonicalMilestoneDependencyGraph(milestone.memberships, 'STATE_CORRUPT', `Milestone ${milestone.id}`);
|
|
183
|
+
}
|
|
184
|
+
export function assertMilestoneTaskRunnable(milestone, taskId, readTask) {
|
|
185
|
+
const assessment = assessMilestoneTaskDependencies(milestone, taskId, readTask);
|
|
186
|
+
if (milestone.status !== 'active' || !assessment.runnable) {
|
|
187
|
+
const diagnosticCode = assessment.dependencyState === 'legacy-unknown'
|
|
188
|
+
? 'MILESTONE_DEPENDENCIES_UNKNOWN'
|
|
189
|
+
: milestone.status !== 'active'
|
|
190
|
+
? 'MILESTONE_NOT_ACTIVE'
|
|
191
|
+
: assessment.unsatisfiedPredecessors.length > 0
|
|
192
|
+
? 'MILESTONE_PREDECESSORS_UNSATISFIED'
|
|
193
|
+
: 'MILESTONE_MEMBERSHIP_NOT_REQUIRED';
|
|
194
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} is not runnable in Milestone ${milestone.id}.`, {
|
|
195
|
+
diagnosticCode,
|
|
196
|
+
milestoneId: milestone.id,
|
|
197
|
+
milestoneStatus: milestone.status,
|
|
198
|
+
taskId,
|
|
199
|
+
...assessment,
|
|
200
|
+
requiredAction: assessment.dependencyState === 'legacy-unknown'
|
|
201
|
+
? 'Apply an explicit Milestone scope change containing dependsOnTaskIds for every membership.'
|
|
202
|
+
: milestone.status !== 'active'
|
|
203
|
+
? 'Authorize the current Milestone Plan before continuing this Task.'
|
|
204
|
+
: assessment.unsatisfiedPredecessors.length > 0
|
|
205
|
+
? 'Merge every required predecessor Task before continuing this Task.'
|
|
206
|
+
: 'Restore a required Milestone membership before continuing this Task.',
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
return assessment;
|
|
210
|
+
}
|
|
82
211
|
export function readMilestoneWithIntegrity(store, projectId, milestoneId, readTask, listTasks) {
|
|
83
212
|
recoverMilestoneScopeChangeTransactions(store, projectId, milestoneId, readTask);
|
|
84
213
|
const milestone = store.readMilestone(projectId, milestoneId);
|
|
85
214
|
assertMilestonePlanAgreement(store, milestone);
|
|
215
|
+
const initialAssembly = assessMilestoneInitialAssembly({
|
|
216
|
+
store,
|
|
217
|
+
projectId,
|
|
218
|
+
milestone,
|
|
219
|
+
...(listTasks ? { listTasks } : {}),
|
|
220
|
+
});
|
|
221
|
+
if (initialAssembly.eligible)
|
|
222
|
+
return milestone;
|
|
223
|
+
assertMilestoneMembershipIntegrity(milestone, readTask, {}, listTasks);
|
|
224
|
+
return milestone;
|
|
225
|
+
}
|
|
226
|
+
/** Read-only integrity validation for observation APIs. Pending journals must be assessed first. */
|
|
227
|
+
export function inspectMilestoneWithIntegrity(store, projectId, milestoneId, readTask, listTasks) {
|
|
228
|
+
const milestone = store.readMilestone(projectId, milestoneId);
|
|
229
|
+
assertMilestonePlanAgreement(store, milestone);
|
|
230
|
+
const initialAssembly = assessMilestoneInitialAssembly({
|
|
231
|
+
store,
|
|
232
|
+
projectId,
|
|
233
|
+
milestone,
|
|
234
|
+
...(listTasks ? { listTasks } : {}),
|
|
235
|
+
});
|
|
236
|
+
if (initialAssembly.eligible)
|
|
237
|
+
return milestone;
|
|
86
238
|
assertMilestoneMembershipIntegrity(milestone, readTask, {}, listTasks);
|
|
87
239
|
return milestone;
|
|
88
240
|
}
|
|
@@ -90,6 +242,14 @@ export function readMilestoneForScopeChange(store, projectId, milestoneId, readT
|
|
|
90
242
|
recoverMilestoneScopeChangeTransactions(store, projectId, milestoneId, readTask);
|
|
91
243
|
const milestone = store.readMilestone(projectId, milestoneId);
|
|
92
244
|
assertMilestonePlanAgreement(store, milestone);
|
|
245
|
+
const initialAssembly = assessMilestoneInitialAssembly({
|
|
246
|
+
store,
|
|
247
|
+
projectId,
|
|
248
|
+
milestone,
|
|
249
|
+
...(listTasks ? { listTasks } : {}),
|
|
250
|
+
});
|
|
251
|
+
if (initialAssembly.eligible)
|
|
252
|
+
return milestone;
|
|
93
253
|
assertMilestoneMembershipIntegrity(milestone, readTask, { allowRepairableRequiredCancelled: true, allowRepairableReverseOrphans: true }, listTasks);
|
|
94
254
|
return milestone;
|
|
95
255
|
}
|
|
@@ -102,6 +262,37 @@ export function listMilestonesWithIntegrity(store, projectId, readTask, listTask
|
|
|
102
262
|
.map((entry) => readMilestoneWithIntegrity(store, projectId, entry.name, readTask, listTasks))
|
|
103
263
|
.sort((left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id));
|
|
104
264
|
}
|
|
265
|
+
export function assessMilestoneInitialAssembly(options) {
|
|
266
|
+
const { milestone, listTasks } = options;
|
|
267
|
+
if (!listTasks)
|
|
268
|
+
return { eligible: false, orphanTaskIds: [] };
|
|
269
|
+
if (milestone.status !== 'planning'
|
|
270
|
+
|| milestone.planHash !== null
|
|
271
|
+
|| milestone.membershipRevision !== 0
|
|
272
|
+
|| milestone.memberships.length !== 0
|
|
273
|
+
|| milestone.taskIds.length !== 0
|
|
274
|
+
|| milestone.successSignal !== ''
|
|
275
|
+
|| milestone.checks.length !== 0
|
|
276
|
+
|| milestone.authorizations.length !== 0
|
|
277
|
+
|| milestone.resultHash !== null
|
|
278
|
+
|| milestone.evidenceHash !== null
|
|
279
|
+
|| milestone.acceptedHead !== null
|
|
280
|
+
|| milestone.cancellationReason !== null) {
|
|
281
|
+
return { eligible: false, orphanTaskIds: [] };
|
|
282
|
+
}
|
|
283
|
+
const reverseOrphans = listTasks().filter((task) => task.milestoneId === milestone.id);
|
|
284
|
+
if (hasActiveMilestoneScopeChangeTransaction(options.store, options.projectId, milestone.id)) {
|
|
285
|
+
return { eligible: false, orphanTaskIds: reverseOrphans.map((task) => task.id) };
|
|
286
|
+
}
|
|
287
|
+
if (readMilestoneScopeChangeEvents(options.store, options.projectId, milestone.id).length > 0) {
|
|
288
|
+
return { eligible: false, orphanTaskIds: reverseOrphans.map((task) => task.id) };
|
|
289
|
+
}
|
|
290
|
+
if (hasMilestoneAutonomyAuthority(options.store, options.projectId, milestone.id)) {
|
|
291
|
+
return { eligible: false, orphanTaskIds: reverseOrphans.map((task) => task.id) };
|
|
292
|
+
}
|
|
293
|
+
const eligible = reverseOrphans.every((task) => isPristineInitialAssemblyTask(task));
|
|
294
|
+
return { eligible, orphanTaskIds: reverseOrphans.map((task) => task.id) };
|
|
295
|
+
}
|
|
105
296
|
export function assertMilestoneMembershipIntegrity(milestone, readTask, options = {}, listTasks) {
|
|
106
297
|
if (!Number.isInteger(milestone.membershipRevision) || milestone.membershipRevision < 0) {
|
|
107
298
|
throw new WorkflowError('STATE_CORRUPT', `Milestone ${milestone.id} has invalid membershipRevision.`, {
|
|
@@ -129,6 +320,17 @@ export function assertMilestoneMembershipIntegrity(milestone, readTask, options
|
|
|
129
320
|
membershipTaskIds,
|
|
130
321
|
});
|
|
131
322
|
}
|
|
323
|
+
const dependencyArraysPresent = milestone.memberships.filter((membership) => Array.isArray(membership.dependsOnTaskIds)).length;
|
|
324
|
+
if (dependencyArraysPresent > 0 && dependencyArraysPresent !== milestone.memberships.length) {
|
|
325
|
+
throw new WorkflowError('STATE_CORRUPT', `Milestone ${milestone.id} mixes canonical and legacy dependency memberships.`, {
|
|
326
|
+
diagnosticCode: 'MILESTONE_DEPENDENCY_GRAPH_PARTIAL',
|
|
327
|
+
canonicalMemberships: dependencyArraysPresent,
|
|
328
|
+
totalMemberships: milestone.memberships.length,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
if (dependencyArraysPresent === milestone.memberships.length && milestone.memberships.length > 0) {
|
|
332
|
+
assertCanonicalMilestoneDependencyGraph(milestone.memberships, 'STATE_CORRUPT', `Milestone ${milestone.id}`);
|
|
333
|
+
}
|
|
132
334
|
for (const membership of milestone.memberships) {
|
|
133
335
|
const task = readTask(membership.taskId);
|
|
134
336
|
if (task.milestoneId !== milestone.id) {
|
|
@@ -169,6 +371,113 @@ export function assertMilestoneMembershipIntegrity(milestone, readTask, options
|
|
|
169
371
|
}
|
|
170
372
|
}
|
|
171
373
|
}
|
|
374
|
+
function assertCanonicalMilestoneDependencyGraph(memberships, code, label) {
|
|
375
|
+
const byTaskId = new Map(memberships.map((membership) => [membership.taskId, membership]));
|
|
376
|
+
if (byTaskId.size !== memberships.length) {
|
|
377
|
+
throw new WorkflowError(code, `${label} memberships contain duplicate Task IDs.`);
|
|
378
|
+
}
|
|
379
|
+
for (const membership of memberships) {
|
|
380
|
+
if (!Array.isArray(membership.dependsOnTaskIds)) {
|
|
381
|
+
throw new WorkflowError(code, `${label} membership ${membership.taskId} is missing dependsOnTaskIds.`);
|
|
382
|
+
}
|
|
383
|
+
if (new Set(membership.dependsOnTaskIds).size !== membership.dependsOnTaskIds.length) {
|
|
384
|
+
throw new WorkflowError(code, `${label} membership ${membership.taskId} contains duplicate dependencies.`, {
|
|
385
|
+
taskId: membership.taskId,
|
|
386
|
+
dependsOnTaskIds: membership.dependsOnTaskIds,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
if (membership.dependsOnTaskIds.some((taskId) => !taskId)) {
|
|
390
|
+
throw new WorkflowError(code, `${label} membership ${membership.taskId} contains an empty dependency.`);
|
|
391
|
+
}
|
|
392
|
+
const sorted = [...membership.dependsOnTaskIds].sort();
|
|
393
|
+
if (canonicalJsonStringify(sorted) !== canonicalJsonStringify(membership.dependsOnTaskIds)) {
|
|
394
|
+
throw new WorkflowError(code, `${label} membership ${membership.taskId} dependencies are not canonical.`, {
|
|
395
|
+
taskId: membership.taskId,
|
|
396
|
+
expected: sorted,
|
|
397
|
+
actual: membership.dependsOnTaskIds,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
if (membership.dependsOnTaskIds.includes(membership.taskId)) {
|
|
401
|
+
throw new WorkflowError(code, `${label} membership ${membership.taskId} cannot depend on itself.`);
|
|
402
|
+
}
|
|
403
|
+
for (const predecessorTaskId of membership.dependsOnTaskIds) {
|
|
404
|
+
const predecessor = byTaskId.get(predecessorTaskId);
|
|
405
|
+
if (!predecessor) {
|
|
406
|
+
throw new WorkflowError(code, `${label} membership ${membership.taskId} has a dangling dependency.`, {
|
|
407
|
+
taskId: membership.taskId,
|
|
408
|
+
predecessorTaskId,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
if (membership.disposition === 'required' && predecessor.disposition !== 'required') {
|
|
412
|
+
throw new WorkflowError(code, `${label} required membership ${membership.taskId} depends on a non-required provider.`, {
|
|
413
|
+
taskId: membership.taskId,
|
|
414
|
+
predecessorTaskId,
|
|
415
|
+
predecessorDisposition: predecessor.disposition,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const visiting = new Set();
|
|
421
|
+
const visited = new Set();
|
|
422
|
+
const visit = (taskId, path) => {
|
|
423
|
+
if (visiting.has(taskId)) {
|
|
424
|
+
const cycleStart = path.indexOf(taskId);
|
|
425
|
+
throw new WorkflowError(code, `${label} dependency graph contains a cycle.`, {
|
|
426
|
+
cycle: [...path.slice(Math.max(cycleStart, 0)), taskId],
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
if (visited.has(taskId))
|
|
430
|
+
return;
|
|
431
|
+
visiting.add(taskId);
|
|
432
|
+
const membership = byTaskId.get(taskId);
|
|
433
|
+
for (const predecessorTaskId of membership.dependsOnTaskIds) {
|
|
434
|
+
visit(predecessorTaskId, [...path, taskId]);
|
|
435
|
+
}
|
|
436
|
+
visiting.delete(taskId);
|
|
437
|
+
visited.add(taskId);
|
|
438
|
+
};
|
|
439
|
+
for (const membership of memberships)
|
|
440
|
+
visit(membership.taskId, []);
|
|
441
|
+
}
|
|
442
|
+
function hasActiveMilestoneScopeChangeTransaction(store, projectId, milestoneId) {
|
|
443
|
+
const milestoneRoot = resolveAlpha6MilestoneRoot(store, projectId, milestoneId);
|
|
444
|
+
return listMilestoneTransactionJournals(milestoneRoot, { expectedMilestoneId: milestoneId })
|
|
445
|
+
.some((journal) => journal.phase === 'prepared' || journal.phase === 'applying');
|
|
446
|
+
}
|
|
447
|
+
function hasMilestoneAutonomyAuthority(store, projectId, milestoneId) {
|
|
448
|
+
return readMilestoneAutonomyEvents(store, projectId, milestoneId).length > 0;
|
|
449
|
+
}
|
|
450
|
+
function isPristineInitialAssemblyTask(task) {
|
|
451
|
+
return task.status === 'planning'
|
|
452
|
+
&& task.workspaceOwner === null
|
|
453
|
+
&& task.taskBranch === null
|
|
454
|
+
&& task.baseCommit === null
|
|
455
|
+
&& task.planHash === null
|
|
456
|
+
&& task.planObjective === null
|
|
457
|
+
&& task.resultHash === null
|
|
458
|
+
&& task.evidenceHash === null
|
|
459
|
+
&& task.knowledgeImpact === null
|
|
460
|
+
&& task.knowledgeImpactReason === null
|
|
461
|
+
&& task.knowledgeTargets.length === 0
|
|
462
|
+
&& task.knowledgeMapRevision === null
|
|
463
|
+
&& task.knowledgeMapHash === null
|
|
464
|
+
&& task.graphUse === null
|
|
465
|
+
&& task.steps.length === 0
|
|
466
|
+
&& task.authorizations.length === 0
|
|
467
|
+
&& task.knowledgeRebinds.length === 0
|
|
468
|
+
&& task.review.status === 'pending'
|
|
469
|
+
&& task.review.reviewer === ''
|
|
470
|
+
&& task.review.recordedAt === null
|
|
471
|
+
&& task.review.headCommit === null
|
|
472
|
+
&& task.review.briefHash === null
|
|
473
|
+
&& task.review.planHash === null
|
|
474
|
+
&& task.review.evidenceHash === null
|
|
475
|
+
&& task.review.summary === ''
|
|
476
|
+
&& task.review.findings.length === 0
|
|
477
|
+
&& task.blockReason === null
|
|
478
|
+
&& task.mergedCommit === null
|
|
479
|
+
&& task.systemCommits.length === 0;
|
|
480
|
+
}
|
|
172
481
|
export function assertMilestoneScopeChangeStatusAllowed(milestone) {
|
|
173
482
|
if (!SCOPE_CHANGE_ALLOWED_STATUSES.has(milestone.status)) {
|
|
174
483
|
throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestone.id} cannot change scope while ${milestone.status}.`, {
|
|
@@ -381,6 +690,90 @@ export function applyMilestoneScopeChangeTransaction(options) {
|
|
|
381
690
|
expectedRevisionAfterApply: expectedRevision + 1,
|
|
382
691
|
});
|
|
383
692
|
}
|
|
693
|
+
export function applyInitialMilestonePlanTransaction(options) {
|
|
694
|
+
const { store, milestone, expectedRevision, normalizedPlan, now, readTask, listTasks } = options;
|
|
695
|
+
if (milestone.status !== 'planning'
|
|
696
|
+
|| milestone.planHash !== null
|
|
697
|
+
|| milestone.membershipRevision !== 0
|
|
698
|
+
|| milestone.taskIds.length > 0
|
|
699
|
+
|| milestone.memberships.length > 0) {
|
|
700
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestone.id} is not in the initial Plan posture.`);
|
|
701
|
+
}
|
|
702
|
+
const milestoneRoot = store.milestoneRoot(milestone.projectId, milestone.id);
|
|
703
|
+
const alpha6MilestoneRoot = resolveAlpha6MilestoneRoot(store, milestone.projectId, milestone.id);
|
|
704
|
+
const planText = renderMilestonePlanArtifact(normalizedPlan);
|
|
705
|
+
const stagedPlan = store.stageArtifact(milestoneRoot, 'plan.json', planText);
|
|
706
|
+
const authorizations = milestone.authorizations.map((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved'
|
|
707
|
+
? { ...authorization, decision: 'superseded' }
|
|
708
|
+
: authorization);
|
|
709
|
+
const candidateState = {
|
|
710
|
+
...milestone,
|
|
711
|
+
revision: milestone.revision + 1,
|
|
712
|
+
updatedAt: now.toISOString(),
|
|
713
|
+
status: 'awaiting_execution_authorization',
|
|
714
|
+
outcome: normalizedPlan.outcome,
|
|
715
|
+
successSignal: normalizedPlan.successSignal,
|
|
716
|
+
acceptance: normalizedPlan.acceptance,
|
|
717
|
+
taskIds: normalizedPlan.taskIds,
|
|
718
|
+
memberships: normalizedPlan.memberships,
|
|
719
|
+
checks: normalizedPlan.checks,
|
|
720
|
+
membershipRevision: 1,
|
|
721
|
+
authorizations,
|
|
722
|
+
planHash: stagedPlan.hash,
|
|
723
|
+
resultHash: null,
|
|
724
|
+
evidenceHash: null,
|
|
725
|
+
acceptedHead: null,
|
|
726
|
+
cancellationReason: null,
|
|
727
|
+
};
|
|
728
|
+
assertMilestoneMembershipIntegrity(candidateState, readTask, {}, listTasks);
|
|
729
|
+
const stagedState = store.stageArtifact(milestoneRoot, 'state.json', `${JSON.stringify(candidateState, null, 2)}\n`);
|
|
730
|
+
const transactionId = `tx-${createEntityId('MS', now.getTime())}`;
|
|
731
|
+
const actor = 'workflow:milestone-plan-set';
|
|
732
|
+
const confirmationCodeBindingHash = sha256Hex(canonicalJsonStringify({
|
|
733
|
+
kind: 'milestone-initial-plan-v1',
|
|
734
|
+
milestoneId: milestone.id,
|
|
735
|
+
expectedRevision,
|
|
736
|
+
planHash: stagedPlan.hash,
|
|
737
|
+
membershipRevision: candidateState.membershipRevision,
|
|
738
|
+
}));
|
|
739
|
+
const journal = {
|
|
740
|
+
transactionId,
|
|
741
|
+
milestoneId: milestone.id,
|
|
742
|
+
expectedMilestoneRevision: milestone.revision,
|
|
743
|
+
actor,
|
|
744
|
+
confirmationCodeBindingHash,
|
|
745
|
+
phase: 'prepared',
|
|
746
|
+
targetFiles: [
|
|
747
|
+
{
|
|
748
|
+
path: 'state.json',
|
|
749
|
+
preimage: captureJournalTargetSnapshot(alpha6MilestoneRoot, 'state.json'),
|
|
750
|
+
staged: createJournalSnapshotFromContent(alpha6MilestoneRoot, readFileSync(stagedState.stagedFile), { mode: 'inline' }),
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
path: 'plan.json',
|
|
754
|
+
preimage: captureJournalTargetSnapshot(alpha6MilestoneRoot, 'plan.json'),
|
|
755
|
+
staged: createJournalSnapshotFromContent(alpha6MilestoneRoot, readFileSync(stagedPlan.stagedFile), { mode: 'inline' }),
|
|
756
|
+
},
|
|
757
|
+
],
|
|
758
|
+
preparedAt: now.toISOString(),
|
|
759
|
+
updatedAt: now.toISOString(),
|
|
760
|
+
};
|
|
761
|
+
writeMilestoneTransactionJournal(alpha6MilestoneRoot, journal);
|
|
762
|
+
try {
|
|
763
|
+
updateMilestoneTransactionJournalPhase(alpha6MilestoneRoot, transactionId, 'applying', now.toISOString(), {
|
|
764
|
+
expectedMilestoneId: milestone.id,
|
|
765
|
+
});
|
|
766
|
+
store.publishArtifact(stagedPlan);
|
|
767
|
+
store.publishArtifact(stagedState);
|
|
768
|
+
}
|
|
769
|
+
catch (error) {
|
|
770
|
+
rollbackMilestoneTransaction(store, milestone.projectId, milestone.id, journal, readTask);
|
|
771
|
+
throw error;
|
|
772
|
+
}
|
|
773
|
+
return finalizeCompletedMilestoneTransaction(store, milestone.projectId, milestone.id, journal, readTask, {
|
|
774
|
+
expectedRevisionAfterApply: expectedRevision + 1,
|
|
775
|
+
});
|
|
776
|
+
}
|
|
384
777
|
function recoverMilestoneScopeChangeTransactions(store, projectId, milestoneId, readTask) {
|
|
385
778
|
const alpha6MilestoneRoot = resolveAlpha6MilestoneRoot(store, projectId, milestoneId);
|
|
386
779
|
const transactionDir = path.join(alpha6MilestoneRoot, '.transactions');
|
|
@@ -406,7 +799,17 @@ function assertMilestonePlanAgreement(store, milestone) {
|
|
|
406
799
|
if (milestone.planHash === null)
|
|
407
800
|
return;
|
|
408
801
|
const milestoneRoot = store.milestoneRoot(milestone.projectId, milestone.id);
|
|
409
|
-
|
|
802
|
+
let currentPlanHash;
|
|
803
|
+
try {
|
|
804
|
+
currentPlanHash = store.hashArtifact(milestoneRoot, 'plan.json');
|
|
805
|
+
}
|
|
806
|
+
catch (error) {
|
|
807
|
+
throw new WorkflowError('STATE_CORRUPT', `Milestone ${milestone.id} declares a planHash but plan.json is unavailable.`, {
|
|
808
|
+
milestoneId: milestone.id,
|
|
809
|
+
expectedPlanHash: milestone.planHash,
|
|
810
|
+
cause: error instanceof Error ? error.message : String(error),
|
|
811
|
+
});
|
|
812
|
+
}
|
|
410
813
|
if (currentPlanHash !== milestone.planHash) {
|
|
411
814
|
throw new WorkflowError('STATE_CORRUPT', `Milestone ${milestone.id} planHash does not match plan.json.`, {
|
|
412
815
|
expectedPlanHash: milestone.planHash,
|
|
@@ -531,12 +934,13 @@ function validateMilestoneScopeChangeEvent(event, expectedMilestoneId) {
|
|
|
531
934
|
function validateMilestoneTransactionTargets(journal) {
|
|
532
935
|
const paths = journal.targetFiles.map((target) => target.path);
|
|
533
936
|
const unique = new Set(paths);
|
|
534
|
-
|
|
937
|
+
const expectedTargets = unique.has(SCOPE_CHANGE_SIDECAR) ? MILESTONE_TARGETS : INITIAL_PLAN_TARGETS;
|
|
938
|
+
if (unique.size !== expectedTargets.length || paths.length !== expectedTargets.length) {
|
|
535
939
|
throw new WorkflowError('STATE_CORRUPT', `Milestone transaction ${journal.transactionId} target set is invalid.`, {
|
|
536
940
|
paths,
|
|
537
941
|
});
|
|
538
942
|
}
|
|
539
|
-
for (const target of
|
|
943
|
+
for (const target of expectedTargets) {
|
|
540
944
|
if (!unique.has(target)) {
|
|
541
945
|
throw new WorkflowError('STATE_CORRUPT', `Milestone transaction ${journal.transactionId} is missing ${target}.`);
|
|
542
946
|
}
|
|
@@ -647,7 +1051,7 @@ function readStagedMilestoneTransactionBundle(store, projectId, milestoneId, jou
|
|
|
647
1051
|
const stateSnapshot = journal.targetFiles.find((target) => target.path === 'state.json')?.staged;
|
|
648
1052
|
const planSnapshot = journal.targetFiles.find((target) => target.path === 'plan.json')?.staged;
|
|
649
1053
|
const sidecarSnapshot = journal.targetFiles.find((target) => target.path === SCOPE_CHANGE_SIDECAR)?.staged;
|
|
650
|
-
if (!stateSnapshot || !planSnapshot
|
|
1054
|
+
if (!stateSnapshot || !planSnapshot) {
|
|
651
1055
|
throw new WorkflowError('STATE_CORRUPT', `Milestone transaction ${journal.transactionId} is missing a staged bundle.`);
|
|
652
1056
|
}
|
|
653
1057
|
const alpha6MilestoneRoot = resolveAlpha6MilestoneRoot(store, projectId, milestoneId);
|
|
@@ -659,26 +1063,27 @@ function readStagedMilestoneTransactionBundle(store, projectId, milestoneId, jou
|
|
|
659
1063
|
expectedMilestoneId: milestoneId,
|
|
660
1064
|
transactionId: journal.transactionId,
|
|
661
1065
|
});
|
|
662
|
-
const sidecarBuffer =
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
1066
|
+
const sidecarBuffer = sidecarSnapshot
|
|
1067
|
+
? readJournalSnapshotContent(alpha6MilestoneRoot, sidecarSnapshot, {
|
|
1068
|
+
expectedMilestoneId: milestoneId,
|
|
1069
|
+
transactionId: journal.transactionId,
|
|
1070
|
+
})
|
|
1071
|
+
: null;
|
|
1072
|
+
if (!stateBuffer || !planBuffer || (sidecarSnapshot && !sidecarBuffer)) {
|
|
667
1073
|
throw new WorkflowError('STATE_CORRUPT', `Milestone transaction ${journal.transactionId} has an incomplete staged bundle.`);
|
|
668
1074
|
}
|
|
669
1075
|
const state = parseStagedMilestoneStateArtifact(stateBuffer.toString('utf8'), projectId, journal.milestoneId, journal.transactionId);
|
|
670
1076
|
const plan = parseMilestonePlanArtifact(planBuffer.toString('utf8'), journal.milestoneId);
|
|
671
|
-
const events =
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|| latestEvent.confirmationCodeBindingHash !== journal.confirmationCodeBindingHash) {
|
|
1077
|
+
const events = sidecarBuffer
|
|
1078
|
+
? parseMilestoneScopeChangeEvents(sidecarBuffer.toString('utf8'), journal.milestoneId)
|
|
1079
|
+
: [];
|
|
1080
|
+
const latestEvent = events.at(-1) ?? null;
|
|
1081
|
+
if (canonicalJsonStringify(milestoneScopeSnapshotFromPlan(plan)) !== canonicalJsonStringify(milestoneScopeSnapshotFromState(state))
|
|
1082
|
+
|| (latestEvent !== null && (latestEvent.candidatePlanHash !== state.planHash
|
|
1083
|
+
|| latestEvent.candidateMembershipRevision !== state.membershipRevision
|
|
1084
|
+
|| canonicalJsonStringify(latestEvent.after) !== canonicalJsonStringify(milestoneScopeSnapshotFromState(state))
|
|
1085
|
+
|| latestEvent.actor !== journal.actor
|
|
1086
|
+
|| latestEvent.confirmationCodeBindingHash !== journal.confirmationCodeBindingHash))) {
|
|
682
1087
|
throw new WorkflowError('STATE_CORRUPT', `Milestone transaction ${journal.transactionId} staged bundle is inconsistent.`);
|
|
683
1088
|
}
|
|
684
1089
|
if (state.revision !== journal.expectedMilestoneRevision + 1) {
|
|
@@ -687,7 +1092,7 @@ function readStagedMilestoneTransactionBundle(store, projectId, milestoneId, jou
|
|
|
687
1092
|
actualRevision: state.revision,
|
|
688
1093
|
});
|
|
689
1094
|
}
|
|
690
|
-
if (latestEvent.previousMilestoneRevision !== journal.expectedMilestoneRevision) {
|
|
1095
|
+
if (latestEvent && latestEvent.previousMilestoneRevision !== journal.expectedMilestoneRevision) {
|
|
691
1096
|
throw new WorkflowError('STATE_CORRUPT', `Milestone transaction ${journal.transactionId} latest event revision does not match journal precondition.`, {
|
|
692
1097
|
expectedPreviousMilestoneRevision: journal.expectedMilestoneRevision,
|
|
693
1098
|
actualPreviousMilestoneRevision: latestEvent.previousMilestoneRevision,
|
|
@@ -725,13 +1130,21 @@ function parseMilestonePlanArtifact(content, milestoneId) {
|
|
|
725
1130
|
const acceptance = parseStringArray(record.acceptance, `Milestone ${milestoneId} plan.json acceptance`);
|
|
726
1131
|
const checks = parseStringArray(record.checks, `Milestone ${milestoneId} plan.json checks`);
|
|
727
1132
|
const memberships = parseMilestoneMembershipArray(record.memberships, milestoneId);
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
1133
|
+
const canonicalMembershipCount = memberships.filter((membership) => Array.isArray(membership.dependsOnTaskIds)).length;
|
|
1134
|
+
if (canonicalMembershipCount > 0 && canonicalMembershipCount !== memberships.length) {
|
|
1135
|
+
throw new WorkflowError('STATE_CORRUPT', `Milestone ${milestoneId} plan.json mixes canonical and legacy dependency memberships.`);
|
|
1136
|
+
}
|
|
1137
|
+
if (canonicalMembershipCount === memberships.length) {
|
|
1138
|
+
assertCanonicalMilestoneDependencyGraph(memberships, 'STATE_CORRUPT', `Milestone ${milestoneId} plan.json`);
|
|
1139
|
+
}
|
|
1140
|
+
return {
|
|
1141
|
+
outcome: record.outcome.trim(),
|
|
1142
|
+
successSignal: record.successSignal.trim(),
|
|
731
1143
|
acceptance,
|
|
732
1144
|
memberships,
|
|
733
1145
|
checks,
|
|
734
|
-
|
|
1146
|
+
taskIds: memberships.map((membership) => membership.taskId),
|
|
1147
|
+
};
|
|
735
1148
|
}
|
|
736
1149
|
function parseMilestoneScopeChangeEvents(content, milestoneId) {
|
|
737
1150
|
if (content.length === 0) {
|
|
@@ -949,7 +1362,7 @@ function parseMilestoneMembership(value, milestoneId, index, line) {
|
|
|
949
1362
|
throw new WorkflowError('STATE_CORRUPT', `Milestone ${milestoneId} membership at index ${index} must be an object.`, line !== undefined ? { line, index } : { index });
|
|
950
1363
|
}
|
|
951
1364
|
const record = value;
|
|
952
|
-
const allowedKeys = new Set(['taskId', 'disposition', 'reason']);
|
|
1365
|
+
const allowedKeys = new Set(['taskId', 'disposition', 'reason', 'dependsOnTaskIds']);
|
|
953
1366
|
const extraKeys = Object.keys(record).filter((key) => !allowedKeys.has(key));
|
|
954
1367
|
if (extraKeys.length > 0) {
|
|
955
1368
|
throw new WorkflowError('STATE_CORRUPT', `Milestone ${milestoneId} membership contains unexpected fields.`, line !== undefined ? { line, index, extraKeys } : { index, extraKeys });
|
|
@@ -964,7 +1377,33 @@ function parseMilestoneMembership(value, milestoneId, index, line) {
|
|
|
964
1377
|
if (disposition !== 'required' && reason.length === 0) {
|
|
965
1378
|
throw new WorkflowError('STATE_CORRUPT', `Milestone ${milestoneId} membership ${disposition} requires a reason.`, line !== undefined ? { line, index } : { index });
|
|
966
1379
|
}
|
|
967
|
-
|
|
1380
|
+
const dependsOnTaskIds = record.dependsOnTaskIds === undefined
|
|
1381
|
+
? undefined
|
|
1382
|
+
: parseTaskDependencyArray(record.dependsOnTaskIds, `Milestone ${milestoneId} membership.dependsOnTaskIds`, line !== undefined ? { line, index } : { index });
|
|
1383
|
+
return {
|
|
1384
|
+
taskId,
|
|
1385
|
+
disposition,
|
|
1386
|
+
reason,
|
|
1387
|
+
...(dependsOnTaskIds === undefined ? {} : { dependsOnTaskIds }),
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
function parseTaskDependencyArray(value, label, details = {}) {
|
|
1391
|
+
if (!Array.isArray(value)) {
|
|
1392
|
+
throw new WorkflowError('STATE_CORRUPT', `${label} must be an array.`, details);
|
|
1393
|
+
}
|
|
1394
|
+
const taskIds = value.map((entry, index) => {
|
|
1395
|
+
if (typeof entry !== 'string' || !entry.trim()) {
|
|
1396
|
+
throw new WorkflowError('STATE_CORRUPT', `${label}[${index}] must be a non-empty Task ID.`, {
|
|
1397
|
+
...details,
|
|
1398
|
+
index,
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
return entry.trim();
|
|
1402
|
+
});
|
|
1403
|
+
if (new Set(taskIds).size !== taskIds.length) {
|
|
1404
|
+
throw new WorkflowError('STATE_CORRUPT', `${label} contains duplicate Task IDs.`, { ...details, taskIds });
|
|
1405
|
+
}
|
|
1406
|
+
return taskIds;
|
|
968
1407
|
}
|
|
969
1408
|
function parseStringArray(value, label, details = {}) {
|
|
970
1409
|
if (!Array.isArray(value) || value.length === 0) {
|