sdd-mcp-server 5.3.0 → 5.5.0
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/CHANGELOG.md +49 -0
- package/README.md +85 -13
- package/agents/security-auditor.md +2 -2
- package/dist/__tests__/helpers/message-clarity.d.ts +15 -0
- package/dist/__tests__/helpers/message-clarity.js +26 -0
- package/dist/__tests__/helpers/message-clarity.js.map +1 -0
- package/dist/__tests__/helpers/message-extractor.d.ts +22 -0
- package/dist/__tests__/helpers/message-extractor.js +135 -0
- package/dist/__tests__/helpers/message-extractor.js.map +1 -0
- package/dist/__tests__/unit/jev/helpers.d.ts +84 -0
- package/dist/__tests__/unit/jev/helpers.js +188 -0
- package/dist/__tests__/unit/jev/helpers.js.map +1 -0
- package/dist/adapters/cli/SDDToolAdapter.d.ts +5 -1
- package/dist/adapters/cli/SDDToolAdapter.js +22 -3
- package/dist/adapters/cli/SDDToolAdapter.js.map +1 -1
- package/dist/application/services/JevAdvisoryService.d.ts +53 -0
- package/dist/application/services/JevAdvisoryService.js +310 -0
- package/dist/application/services/JevAdvisoryService.js.map +1 -0
- package/dist/application/services/JevBaseRef.d.ts +21 -0
- package/dist/application/services/JevBaseRef.js +46 -0
- package/dist/application/services/JevBaseRef.js.map +1 -0
- package/dist/application/services/JevChangeManifest.d.ts +33 -0
- package/dist/application/services/JevChangeManifest.js +208 -0
- package/dist/application/services/JevChangeManifest.js.map +1 -0
- package/dist/application/services/JevCheckTypes.d.ts +74 -0
- package/dist/application/services/JevCheckTypes.js +2 -0
- package/dist/application/services/JevCheckTypes.js.map +1 -0
- package/dist/application/services/JevCodeExcerpts.d.ts +23 -0
- package/dist/application/services/JevCodeExcerpts.js +102 -0
- package/dist/application/services/JevCodeExcerpts.js.map +1 -0
- package/dist/application/services/JevCommitLookup.d.ts +17 -0
- package/dist/application/services/JevCommitLookup.js +72 -0
- package/dist/application/services/JevCommitLookup.js.map +1 -0
- package/dist/application/services/JevExchange.d.ts +48 -0
- package/dist/application/services/JevExchange.js +119 -0
- package/dist/application/services/JevExchange.js.map +1 -0
- package/dist/application/services/JevQuestionCatalog.d.ts +28 -0
- package/dist/application/services/JevQuestionCatalog.js +194 -0
- package/dist/application/services/JevQuestionCatalog.js.map +1 -0
- package/dist/application/services/JevRedaction.d.ts +12 -0
- package/dist/application/services/JevRedaction.js +56 -0
- package/dist/application/services/JevRedaction.js.map +1 -0
- package/dist/application/services/JevReport.d.ts +41 -0
- package/dist/application/services/JevReport.js +101 -0
- package/dist/application/services/JevReport.js.map +1 -0
- package/dist/application/services/JevResponseValidator.d.ts +34 -0
- package/dist/application/services/JevResponseValidator.js +98 -0
- package/dist/application/services/JevResponseValidator.js.map +1 -0
- package/dist/application/services/JevResults.d.ts +3 -0
- package/dist/application/services/JevResults.js +7 -0
- package/dist/application/services/JevResults.js.map +1 -0
- package/dist/application/services/JevReviewCheck.d.ts +20 -0
- package/dist/application/services/JevReviewCheck.js +120 -0
- package/dist/application/services/JevReviewCheck.js.map +1 -0
- package/dist/application/services/JevSettings.d.ts +18 -0
- package/dist/application/services/JevSettings.js +72 -0
- package/dist/application/services/JevSettings.js.map +1 -0
- package/dist/application/services/JevSidecarStore.d.ts +68 -0
- package/dist/application/services/JevSidecarStore.js +201 -0
- package/dist/application/services/JevSidecarStore.js.map +1 -0
- package/dist/application/services/JevSpecState.d.ts +38 -0
- package/dist/application/services/JevSpecState.js +109 -0
- package/dist/application/services/JevSpecState.js.map +1 -0
- package/dist/application/services/JevTestcaseState.d.ts +22 -0
- package/dist/application/services/JevTestcaseState.js +85 -0
- package/dist/application/services/JevTestcaseState.js.map +1 -0
- package/dist/application/services/RedEvidenceClassifier.d.ts +10 -0
- package/dist/application/services/RedEvidenceClassifier.js +32 -0
- package/dist/application/services/RedEvidenceClassifier.js.map +1 -0
- package/dist/application/services/WorkflowEngineService.d.ts +28 -0
- package/dist/application/services/WorkflowEngineService.js +111 -63
- package/dist/application/services/WorkflowEngineService.js.map +1 -1
- package/dist/application/services/WorkflowValidationService.d.ts +16 -0
- package/dist/application/services/WorkflowValidationService.js +39 -34
- package/dist/application/services/WorkflowValidationService.js.map +1 -1
- package/dist/cli/install-skills.js +0 -0
- package/dist/cli/install-target.d.ts +8 -6
- package/dist/cli/install-target.js +11 -3
- package/dist/cli/install-target.js.map +1 -1
- package/dist/cli/tool-support/root-guidance.js +1 -0
- package/dist/cli/tool-support/root-guidance.js.map +1 -1
- package/dist/cli/tool-support/target-installer.d.ts +1 -0
- package/dist/cli/tool-support/target-installer.js +9 -5
- package/dist/cli/tool-support/target-installer.js.map +1 -1
- package/dist/domain/ports.d.ts +34 -0
- package/dist/domain/ports.js +11 -1
- package/dist/domain/ports.js.map +1 -1
- package/dist/infrastructure/adapters/FetchJevHttpAdapter.d.ts +4 -0
- package/dist/infrastructure/adapters/FetchJevHttpAdapter.js +73 -0
- package/dist/infrastructure/adapters/FetchJevHttpAdapter.js.map +1 -0
- package/dist/infrastructure/adapters/NodeGitAdapter.d.ts +5 -0
- package/dist/infrastructure/adapters/NodeGitAdapter.js +31 -0
- package/dist/infrastructure/adapters/NodeGitAdapter.js.map +1 -0
- package/dist/infrastructure/di/container.js +10 -0
- package/dist/infrastructure/di/container.js.map +1 -1
- package/dist/infrastructure/di/types.d.ts +4 -0
- package/dist/infrastructure/di/types.js +4 -0
- package/dist/infrastructure/di/types.js.map +1 -1
- package/dist/infrastructure/mcp/sddToolDefinitions.d.ts +1 -1
- package/dist/infrastructure/mcp/sddToolDefinitions.js +72 -1
- package/dist/infrastructure/mcp/sddToolDefinitions.js.map +1 -1
- package/dist/shared/version.d.ts +1 -1
- package/dist/shared/version.js +1 -1
- package/package.json +1 -1
- package/skills/output-clarity-ladder/SKILL.md +43 -0
- package/skills/output-clarity-ladder/references/ja.md +30 -0
- package/skills/output-clarity-ladder/references/zh-TW.md +30 -0
- package/skills/sdd-commit/SKILL.md +4 -3
- package/skills/sdd-design/SKILL.md +1 -1
- package/skills/sdd-implement/SKILL.md +4 -4
- package/skills/sdd-requirements/SKILL.md +6 -6
- package/skills/sdd-review/SKILL.md +3 -2
- package/skills/sdd-security-check/SKILL.md +3 -2
- package/skills/sdd-tasks/REFERENCE.md +4 -0
- package/skills/sdd-tasks/SKILL.md +8 -6
- package/templates/CLAUDE.md +1 -1
- package/templates/codex-AGENTS.md +1 -1
|
@@ -19,7 +19,7 @@ import { TYPES } from '../../infrastructure/di/types.js';
|
|
|
19
19
|
import { WorkflowPhase, } from '../../domain/types.js';
|
|
20
20
|
import { ContextCompactionService, } from './ContextCompactionService.js';
|
|
21
21
|
import { InvalidFeatureNameError, SpecPathResolver, validateFeatureName } from './SpecPathResolver.js';
|
|
22
|
-
import { WorkflowValidationService } from './WorkflowValidationService.js';
|
|
22
|
+
import { WorkflowValidationService, taskAcceptanceCriteria } from './WorkflowValidationService.js';
|
|
23
23
|
import { GovernanceError } from './WorkflowErrors.js';
|
|
24
24
|
import { FilesystemLockCompromisedError, withFilesystemLock } from '../../utils/withFilesystemLock.js';
|
|
25
25
|
const PHASES = ['requirements', 'design', 'tasks'];
|
|
@@ -76,7 +76,10 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
76
76
|
const initializationLock = path.join(resolved.specsRoot, `.${request.featureName}.init.lock`);
|
|
77
77
|
await resolver.assertContained(resolved.specsRoot, initializationLock);
|
|
78
78
|
return withFilesystemLock(initializationLock, async (lease) => {
|
|
79
|
-
|
|
79
|
+
// Older versions left empty feature directories behind; reuse those, never populated ones.
|
|
80
|
+
const occupied = await this.fileSystem.exists(resolved.featureRoot)
|
|
81
|
+
&& (await this.fileSystem.readdir(resolved.featureRoot)).length > 0;
|
|
82
|
+
if (occupied) {
|
|
80
83
|
throw new GovernanceError('StateInvariantViolation', `Feature already exists: ${request.featureName}`);
|
|
81
84
|
}
|
|
82
85
|
await this.fileSystem.mkdir(resolved.featureRoot);
|
|
@@ -125,14 +128,14 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
125
128
|
const record = spec.approvals[request.phase];
|
|
126
129
|
this.assertPriorApproved(request.phase, spec.approvals);
|
|
127
130
|
if (record.approved)
|
|
128
|
-
throw new GovernanceError('PhaseNotApproved', `${request.phase} is already approved
|
|
131
|
+
throw new GovernanceError('PhaseNotApproved', `${request.phase} is already approved. Reload status and continue with the next phase.`);
|
|
129
132
|
if (record.revision !== request.expectedRevision)
|
|
130
|
-
throw new GovernanceError('RevisionConflict', `Expected revision ${request.expectedRevision}, found ${record.revision}
|
|
133
|
+
throw new GovernanceError('RevisionConflict', `Expected revision ${request.expectedRevision}, found ${record.revision}. Reload status and submit again with the current revision.`);
|
|
131
134
|
const artifactPath = path.join(featureRoot, `${request.phase}.md`);
|
|
132
135
|
const priorArtifact = await this.readOptional(artifactPath);
|
|
133
136
|
const observedHash = priorArtifact === null ? null : sha256(priorArtifact);
|
|
134
137
|
if (observedHash !== request.expectedArtifactSha256) {
|
|
135
|
-
throw new GovernanceError('ArtifactDrift', `${request.phase}.md changed since it was observed
|
|
138
|
+
throw new GovernanceError('ArtifactDrift', `${request.phase}.md changed since it was observed. Reread the artifact and submit again with its current content.`, { observedArtifactSha256: observedHash });
|
|
136
139
|
}
|
|
137
140
|
if (request.phase === 'tasks') {
|
|
138
141
|
if (typeof request.reviewTestCases !== 'boolean')
|
|
@@ -172,27 +175,27 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
172
175
|
await this.assertNoApprovedArtifactDrift(featureRoot, spec);
|
|
173
176
|
const record = spec.approvals[request.phase];
|
|
174
177
|
if (!record.generated)
|
|
175
|
-
throw new GovernanceError('PhaseNotApproved', `${request.phase} has not been submitted
|
|
178
|
+
throw new GovernanceError('PhaseNotApproved', `${request.phase} has not been submitted. Submit the phase artifact before approval.`);
|
|
176
179
|
if (record.revision !== request.expectedRevision)
|
|
177
|
-
throw new GovernanceError('RevisionConflict', 'The reviewed revision is stale');
|
|
180
|
+
throw new GovernanceError('RevisionConflict', 'The reviewed revision is stale. Reload status and review the current revision.');
|
|
178
181
|
if (record.artifactSha256 !== request.expectedArtifactSha256)
|
|
179
|
-
throw new GovernanceError('ArtifactDrift', 'The reviewed artifact hash is stale');
|
|
182
|
+
throw new GovernanceError('ArtifactDrift', 'The reviewed artifact hash is stale. Reload status and review the current artifact.');
|
|
180
183
|
const artifact = await this.requireArtifact(featureRoot, request.phase);
|
|
181
184
|
if (sha256(artifact) !== record.artifactSha256)
|
|
182
|
-
throw new GovernanceError('ArtifactDrift', `${request.phase}.md differs from the submitted artifact
|
|
185
|
+
throw new GovernanceError('ArtifactDrift', `${request.phase}.md differs from the submitted artifact. Inspect the changed artifact, then revise it in its phase Skill.`);
|
|
183
186
|
this.assertPriorApproved(request.phase, spec.approvals);
|
|
184
187
|
if (record.validation.status !== 'passed' && record.validation.status !== 'legacy-accepted')
|
|
185
|
-
throw new GovernanceError('PhaseValidationFailed', `${request.phase} did not pass validation
|
|
188
|
+
throw new GovernanceError('PhaseValidationFailed', `${request.phase} did not pass validation. Fix the listed blockers and submit the phase again.`, { blockers: record.validation.blockers });
|
|
186
189
|
if (record.validation.status === 'passed') {
|
|
187
190
|
const upstream = await this.readUpstream(featureRoot, request.phase);
|
|
188
191
|
const validation = this.validate(request.phase, artifact, upstream.requirements, upstream.design);
|
|
189
192
|
if (validation.status !== 'passed')
|
|
190
|
-
throw new GovernanceError('PhaseValidationFailed', `${request.phase} no longer validates
|
|
193
|
+
throw new GovernanceError('PhaseValidationFailed', `${request.phase} no longer validates. Fix the listed blockers and submit the phase again.`, { blockers: validation.blockers });
|
|
191
194
|
}
|
|
192
195
|
const checkpoint = spec.checkpoints.test_cases;
|
|
193
196
|
if (request.phase === 'tasks' && spec.workflow_options.review_test_cases === true &&
|
|
194
197
|
(!checkpoint.reviewed || checkpoint.reviewed_revision !== record.revision || checkpoint.reviewed_artifact_sha256 !== record.artifactSha256)) {
|
|
195
|
-
throw new GovernanceError('PhaseNotApproved', 'The current tasks revision requires explicit test-case review');
|
|
198
|
+
throw new GovernanceError('PhaseNotApproved', 'The current tasks revision requires explicit test-case review. Record the test-case review decision before approving tasks.');
|
|
196
199
|
}
|
|
197
200
|
if (!record.approved) {
|
|
198
201
|
spec.approvals[request.phase] = { ...record, approved: true };
|
|
@@ -211,18 +214,18 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
211
214
|
if (spec.workflow_options.review_test_cases !== true)
|
|
212
215
|
throw new GovernanceError('StateInvariantViolation', 'Test-case review is not required');
|
|
213
216
|
if (!tasks.generated)
|
|
214
|
-
throw new GovernanceError('PhaseNotApproved', 'Tasks are not
|
|
217
|
+
throw new GovernanceError('PhaseNotApproved', 'Tasks are not generated yet. Submit the tasks phase before test-case review.');
|
|
215
218
|
if (tasks.revision !== request.expectedTasksRevision)
|
|
216
|
-
throw new GovernanceError('RevisionConflict', 'The tasks review revision is stale');
|
|
219
|
+
throw new GovernanceError('RevisionConflict', 'The tasks review revision is stale. Reload status and review the current tasks revision.');
|
|
217
220
|
if (tasks.artifactSha256 !== request.expectedArtifactSha256 || sha256(await this.requireArtifact(featureRoot, 'tasks')) !== request.expectedArtifactSha256)
|
|
218
|
-
throw new GovernanceError('ArtifactDrift', 'The tasks artifact changed before review');
|
|
221
|
+
throw new GovernanceError('ArtifactDrift', 'The tasks artifact changed before review. Reload status and review the current tasks artifact.');
|
|
219
222
|
const checkpoint = spec.checkpoints.test_cases;
|
|
220
223
|
const alreadyReviewed = checkpoint.reviewed
|
|
221
224
|
&& checkpoint.reviewed_revision === tasks.revision
|
|
222
225
|
&& checkpoint.reviewed_artifact_sha256 === tasks.artifactSha256;
|
|
223
226
|
if (!alreadyReviewed) {
|
|
224
227
|
if (tasks.validation.status !== 'passed' || !spec.approvals.design.approved || tasks.approved)
|
|
225
|
-
throw new GovernanceError('PhaseNotApproved', 'Tasks are not ready for test-case review');
|
|
228
|
+
throw new GovernanceError('PhaseNotApproved', 'Tasks are not ready for test-case review. Submit passing tasks after design approval and before tasks approval.');
|
|
226
229
|
spec.checkpoints.test_cases = { required: true, reviewed: true, reviewed_at: new Date().toISOString(), reviewed_revision: tasks.revision, reviewed_artifact_sha256: tasks.artifactSha256 };
|
|
227
230
|
spec.updated_at = new Date().toISOString();
|
|
228
231
|
await commitSpec(spec);
|
|
@@ -236,13 +239,13 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
236
239
|
await this.assertNoApprovedArtifactDrift(featureRoot, spec);
|
|
237
240
|
const tasksRecord = spec.approvals.tasks;
|
|
238
241
|
if (!tasksRecord.approved)
|
|
239
|
-
throw new GovernanceError('PhaseNotApproved', 'Tasks are not approved');
|
|
242
|
+
throw new GovernanceError('PhaseNotApproved', 'Tasks are not approved. Approve the tasks phase before starting implementation.');
|
|
240
243
|
if (sha256(await this.requireArtifact(featureRoot, 'tasks')) !== tasksRecord.artifactSha256)
|
|
241
|
-
throw new GovernanceError('ArtifactDrift', 'Approved tasks artifact has drifted');
|
|
244
|
+
throw new GovernanceError('ArtifactDrift', 'Approved tasks artifact has drifted. Inspect the changed artifact, then revise it in the tasks phase Skill.');
|
|
242
245
|
if (spec.workflow_options.review_test_cases === true) {
|
|
243
246
|
const cp = spec.checkpoints.test_cases;
|
|
244
247
|
if (!cp.reviewed || cp.reviewed_revision !== tasksRecord.revision || cp.reviewed_artifact_sha256 !== tasksRecord.artifactSha256)
|
|
245
|
-
throw new GovernanceError('PhaseNotApproved', 'Tasks review checkpoint is stale');
|
|
248
|
+
throw new GovernanceError('PhaseNotApproved', 'Tasks review checkpoint is stale. Record test-case review for the current tasks revision.');
|
|
246
249
|
}
|
|
247
250
|
if (!spec.implementation) {
|
|
248
251
|
const content = await this.requireArtifact(featureRoot, 'tasks');
|
|
@@ -251,12 +254,12 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
251
254
|
return this.parseLegacyTasks(content);
|
|
252
255
|
const current = this.validator.parseTasks(content);
|
|
253
256
|
if (current.status !== 'passed') {
|
|
254
|
-
throw new GovernanceError('PhaseValidationFailed', 'Approved tasks no longer satisfy the executable task contract', { blockers: current.blockers });
|
|
257
|
+
throw new GovernanceError('PhaseValidationFailed', 'Approved tasks no longer satisfy the executable task contract. Fix the listed blockers in the tasks phase Skill.', { blockers: current.blockers });
|
|
255
258
|
}
|
|
256
259
|
return Object.entries(current.tasks).map(([id, task]) => ({ id, ...task }));
|
|
257
260
|
})();
|
|
258
261
|
if (parsed.length === 0)
|
|
259
|
-
throw new GovernanceError('LegacyTaskConflict', 'Tasks artifact contains no executable leaf tasks');
|
|
262
|
+
throw new GovernanceError('LegacyTaskConflict', 'Tasks artifact contains no executable leaf tasks. Add at least one executable task in the tasks phase Skill.');
|
|
260
263
|
const taskStates = {};
|
|
261
264
|
for (const task of parsed) {
|
|
262
265
|
taskStates[task.id] = {
|
|
@@ -282,15 +285,15 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
282
285
|
await this.assertNoApprovedArtifactDrift(featureRoot, spec);
|
|
283
286
|
const implementation = spec.implementation;
|
|
284
287
|
if (!implementation)
|
|
285
|
-
throw new GovernanceError('TaskTransitionInvalid', 'Implementation has not started');
|
|
288
|
+
throw new GovernanceError('TaskTransitionInvalid', 'Implementation has not started. Start implementation before changing task state.');
|
|
286
289
|
if (implementation.revision !== request.expectedRevision)
|
|
287
|
-
throw new GovernanceError('RevisionConflict', 'Implementation revision is stale');
|
|
290
|
+
throw new GovernanceError('RevisionConflict', 'Implementation revision is stale. Reload status and submit again with the current revision.');
|
|
288
291
|
const task = implementation.tasks[request.taskNumber];
|
|
289
292
|
if (!task)
|
|
290
|
-
throw new GovernanceError('TaskTransitionInvalid', `Unknown task ${request.taskNumber}
|
|
293
|
+
throw new GovernanceError('TaskTransitionInvalid', `Unknown task ${request.taskNumber}. Use a task number from the approved tasks artifact.`);
|
|
291
294
|
const incomplete = task.dependencies.filter((dependency) => implementation.tasks[dependency]?.status !== 'completed');
|
|
292
295
|
if (request.action === 'start' && incomplete.length > 0)
|
|
293
|
-
throw new GovernanceError('TaskDependencyIncomplete', 'Task dependencies are incomplete', { dependencies: incomplete });
|
|
296
|
+
throw new GovernanceError('TaskDependencyIncomplete', 'Task dependencies are incomplete. Complete the listed dependency tasks first.', { dependencies: incomplete });
|
|
294
297
|
const prior = task.status;
|
|
295
298
|
if (request.action === 'start') {
|
|
296
299
|
if (prior === 'pending')
|
|
@@ -321,7 +324,7 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
321
324
|
this.invalidTransition(request.action, prior);
|
|
322
325
|
const artifacts = this.normalizeArtifacts(request.affectedArtifacts ?? []);
|
|
323
326
|
if (task.planned_artifacts.length > 0 && artifacts.length === 0)
|
|
324
|
-
throw new GovernanceError('TaskTransitionInvalid', 'At least one observed artifact is required');
|
|
327
|
+
throw new GovernanceError('TaskTransitionInvalid', 'At least one observed artifact is required. Name the changed artifacts when completing the task.');
|
|
325
328
|
task.status = 'completed';
|
|
326
329
|
task.evidence.verification = this.evidence(request.evidence);
|
|
327
330
|
task.observed_artifacts = artifacts;
|
|
@@ -375,6 +378,47 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
375
378
|
}
|
|
376
379
|
return statuses;
|
|
377
380
|
}
|
|
381
|
+
/**
|
|
382
|
+
* Read-only view for advisory checks. Never commits state, publishes a handoff, or creates
|
|
383
|
+
* files for an unknown feature (the lock helper would create the feature directory).
|
|
384
|
+
*/
|
|
385
|
+
async readAdvisorySnapshot(request) {
|
|
386
|
+
validateFeatureName(request.featureName);
|
|
387
|
+
if (!this.fileSystem)
|
|
388
|
+
throw new GovernanceError('StateInvariantViolation', 'FileSystemPort is required');
|
|
389
|
+
const specPath = path.join(path.resolve(request.projectRoot), '.spec', 'specs', request.featureName, 'spec.json');
|
|
390
|
+
if (!(await this.fileSystem.exists(specPath))) {
|
|
391
|
+
throw new GovernanceError('StateInvariantViolation', `Feature metadata not found: ${request.featureName}`);
|
|
392
|
+
}
|
|
393
|
+
return this.withLockedFeature(request.projectRoot, request.featureName, false, async ({ featureRoot, spec }) => {
|
|
394
|
+
const artifact = (name) => this.readOptional(path.join(featureRoot, `${name}.md`));
|
|
395
|
+
const record = spec.approvals[request.phase];
|
|
396
|
+
const content = await artifact(request.phase);
|
|
397
|
+
const requirementsContent = request.phase === 'requirements' ? null : await artifact('requirements');
|
|
398
|
+
const designContent = request.phase === 'tasks' ? await artifact('design') : null;
|
|
399
|
+
const tasksContent = spec.implementation ? (request.phase === 'tasks' ? content : await artifact('tasks')) : null;
|
|
400
|
+
const criteria = taskAcceptanceCriteria(tasksContent ?? '');
|
|
401
|
+
return {
|
|
402
|
+
featureName: request.featureName,
|
|
403
|
+
description: spec.description,
|
|
404
|
+
phase: request.phase,
|
|
405
|
+
revision: record.revision,
|
|
406
|
+
recordedSha256: record.artifactSha256,
|
|
407
|
+
observedSha256: content === null ? undefined : sha256(content),
|
|
408
|
+
validationStatus: record.validation.status,
|
|
409
|
+
content: content ?? '',
|
|
410
|
+
requirementsContent: requirementsContent ?? undefined,
|
|
411
|
+
designContent: designContent ?? undefined,
|
|
412
|
+
implementation: spec.implementation && {
|
|
413
|
+
revision: spec.implementation.revision,
|
|
414
|
+
tasks: Object.fromEntries(Object.entries(spec.implementation.tasks).map(([id, task]) => [
|
|
415
|
+
id,
|
|
416
|
+
{ status: task.status, tddRequired: task.tdd_required, acceptanceCriteria: criteria[id] ?? '' },
|
|
417
|
+
])),
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
});
|
|
421
|
+
}
|
|
378
422
|
async loadProject(request) {
|
|
379
423
|
return this.withLockedFeature(request.projectRoot, request.featureName, false, async ({ projectRoot, spec }) => this.hydrateProject(projectRoot, request.featureName, spec));
|
|
380
424
|
}
|
|
@@ -390,6 +434,10 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
390
434
|
throw new GovernanceError('StateInvariantViolation', 'FileSystemPort is required');
|
|
391
435
|
const resolver = new SpecPathResolver(this.fileSystem);
|
|
392
436
|
const resolved = await resolver.resolve(projectRoot, featureName);
|
|
437
|
+
// The lock helper creates its parent directory; never let it materialize a missing feature.
|
|
438
|
+
if (!(await this.fileSystem.exists(resolved.featureRoot))) {
|
|
439
|
+
throw new GovernanceError('StateInvariantViolation', `Feature metadata not found: ${featureName}`);
|
|
440
|
+
}
|
|
393
441
|
const lockPath = path.join(resolved.featureRoot, '.workflow.lock');
|
|
394
442
|
await resolver.assertContained(resolved.featureRoot, lockPath);
|
|
395
443
|
return withFilesystemLock(lockPath, async (lease) => {
|
|
@@ -428,12 +476,12 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
428
476
|
&& (!Number.isInteger(raw.schema_version)
|
|
429
477
|
|| raw.schema_version < 1
|
|
430
478
|
|| raw.schema_version > 4)) {
|
|
431
|
-
throw new GovernanceError('LegacyStateConflict', `Unsupported workflow schema: ${String(raw.schema_version)}
|
|
479
|
+
throw new GovernanceError('LegacyStateConflict', `Unsupported workflow schema: ${String(raw.schema_version)}. Restore a supported workflow state file.`);
|
|
432
480
|
}
|
|
433
481
|
const accepted = new Set(['init', 'requirements-generated', 'requirements-approved', 'design-generated', 'design-approved', 'tasks-generated', 'tasks-approved', 'implementation-ready', 'implementation', 'completed', 'implementation-completed']);
|
|
434
482
|
const legacyPhase = typeof raw.phase === 'string' ? raw.phase : 'init';
|
|
435
483
|
if (!accepted.has(legacyPhase))
|
|
436
|
-
throw new GovernanceError('LegacyStateConflict', `Unknown legacy phase: ${legacyPhase}
|
|
484
|
+
throw new GovernanceError('LegacyStateConflict', `Unknown legacy phase: ${legacyPhase}. Correct the phase in the workflow state file.`);
|
|
437
485
|
const legacyApprovals = raw.approvals && typeof raw.approvals === 'object' ? raw.approvals : {};
|
|
438
486
|
const approvals = {};
|
|
439
487
|
for (const phase of PHASES) {
|
|
@@ -441,10 +489,10 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
441
489
|
const generated = value.generated === true;
|
|
442
490
|
const approved = value.approved === true;
|
|
443
491
|
if (approved && !generated)
|
|
444
|
-
throw new GovernanceError('LegacyStateConflict', `${phase} is approved but not generated
|
|
492
|
+
throw new GovernanceError('LegacyStateConflict', `${phase} is approved but not generated. Correct the approval records in the workflow state file.`);
|
|
445
493
|
const artifact = this.readOptionalSync(path.join(featureRoot, `${phase}.md`));
|
|
446
494
|
if (generated && artifact === null)
|
|
447
|
-
throw new GovernanceError('LegacyStateConflict', `Generated ${phase} artifact is missing
|
|
495
|
+
throw new GovernanceError('LegacyStateConflict', `Generated ${phase} artifact is missing. Restore the artifact file or correct the workflow state file.`);
|
|
448
496
|
approvals[phase] = {
|
|
449
497
|
generated,
|
|
450
498
|
approved,
|
|
@@ -482,14 +530,14 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
482
530
|
const completedRank = legacyPhase === 'completed' || legacyPhase === 'implementation-completed';
|
|
483
531
|
const implementationRank = ['implementation-ready', 'implementation', 'completed', 'implementation-completed'].includes(legacyPhase);
|
|
484
532
|
if (implementationRank && (!approvals.tasks.approved || (choice === true && !reviewed)))
|
|
485
|
-
throw new GovernanceError('LegacyStateConflict', 'Legacy implementation rank does not satisfy tasks governance');
|
|
533
|
+
throw new GovernanceError('LegacyStateConflict', 'Legacy implementation rank does not satisfy tasks governance. Approve and review the tasks phase first.');
|
|
486
534
|
const description = typeof raw.description === 'string' ? raw.description : '';
|
|
487
535
|
const language = typeof raw.language === 'string' ? raw.language : 'en';
|
|
488
536
|
if (description.length > 20_000) {
|
|
489
|
-
throw new GovernanceError('LegacyStateConflict', 'Legacy description exceeds the schema-v5 limit');
|
|
537
|
+
throw new GovernanceError('LegacyStateConflict', 'Legacy description exceeds the schema-v5 limit. Shorten the description in the workflow state file.');
|
|
490
538
|
}
|
|
491
539
|
if (!['en', 'ja', 'zh-TW'].includes(language)) {
|
|
492
|
-
throw new GovernanceError('LegacyStateConflict', `Unsupported legacy language: ${language}
|
|
540
|
+
throw new GovernanceError('LegacyStateConflict', `Unsupported legacy language: ${language}. Use a supported language in the workflow state file.`);
|
|
493
541
|
}
|
|
494
542
|
const updatedAt = typeof raw.updated_at === 'string'
|
|
495
543
|
? raw.updated_at
|
|
@@ -511,10 +559,10 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
511
559
|
if (implementationRank) {
|
|
512
560
|
const tasks = this.parseLegacyTasks(this.readOptionalSync(path.join(featureRoot, 'tasks.md')) ?? '');
|
|
513
561
|
if (tasks.length === 0)
|
|
514
|
-
throw new GovernanceError('LegacyTaskConflict', 'Legacy tasks contain no leaves');
|
|
562
|
+
throw new GovernanceError('LegacyTaskConflict', 'Legacy tasks contain no leaves. Add at least one executable task to the tasks artifact.');
|
|
515
563
|
spec.implementation = { revision: 1, tasks_revision: approvals.tasks.revision, legacy_imported: true, tasks: Object.fromEntries(tasks.map((task) => [task.id, { title: task.title, tdd_required: task.tddRequired, dependencies: [], status: task.completed ? 'completed' : 'pending', evidence: {}, planned_artifacts: [], observed_artifacts: [] }])) };
|
|
516
564
|
if (completedRank && !this.allComplete(spec.implementation))
|
|
517
|
-
throw new GovernanceError('LegacyStateConflict', 'Completed legacy phase has unchecked tasks');
|
|
565
|
+
throw new GovernanceError('LegacyStateConflict', 'Completed legacy phase has unchecked tasks. Complete the unchecked tasks or correct the legacy phase.');
|
|
518
566
|
}
|
|
519
567
|
return spec;
|
|
520
568
|
}
|
|
@@ -592,7 +640,7 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
592
640
|
const content = await this.readOptional(path.join(featureRoot, `${phase}.md`));
|
|
593
641
|
const observedArtifactSha256 = content === null ? undefined : sha256(content);
|
|
594
642
|
if (observedArtifactSha256 !== record.artifactSha256) {
|
|
595
|
-
throw new GovernanceError('ArtifactDrift', `Approved ${phase}.md differs from revision ${record.revision}
|
|
643
|
+
throw new GovernanceError('ArtifactDrift', `Approved ${phase}.md differs from revision ${record.revision}. Inspect the changed artifact, then revise it in its phase Skill.`, { phase, observedArtifactSha256 });
|
|
596
644
|
}
|
|
597
645
|
}
|
|
598
646
|
}
|
|
@@ -607,7 +655,7 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
607
655
|
nextArtifact,
|
|
608
656
|
];
|
|
609
657
|
if (endpoints.some((value) => Buffer.byteLength(value, 'utf8') > JOURNAL_LIMIT)) {
|
|
610
|
-
throw new GovernanceError('RecoveryConflict', 'Submission endpoint exceeds the journal size limit');
|
|
658
|
+
throw new GovernanceError('RecoveryConflict', 'Submission endpoint exceeds the journal size limit. Shorten the artifact and submit again.');
|
|
611
659
|
}
|
|
612
660
|
const journal = {
|
|
613
661
|
schema_version: 1,
|
|
@@ -631,7 +679,7 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
631
679
|
await this.replaceCas(journalPath, journalBytes, null, assertHeld);
|
|
632
680
|
}
|
|
633
681
|
catch (rollbackError) {
|
|
634
|
-
throw new GovernanceError('RecoveryConflict', 'Submission failed and CAS rollback could not safely restore prior bytes', { cause: errorSummary(error), rollback: errorSummary(rollbackError) });
|
|
682
|
+
throw new GovernanceError('RecoveryConflict', 'Submission failed and CAS rollback could not safely restore prior bytes. Inspect the spec files before retrying.', { cause: errorSummary(error), rollback: errorSummary(rollbackError) });
|
|
635
683
|
}
|
|
636
684
|
throw error;
|
|
637
685
|
}
|
|
@@ -643,7 +691,7 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
643
691
|
if (await this.fileSystem.exists(journalPath)) {
|
|
644
692
|
const journalStats = await stat(journalPath);
|
|
645
693
|
if (!journalStats.isFile() || journalStats.size > JOURNAL_FILE_LIMIT) {
|
|
646
|
-
throw new GovernanceError('RecoveryConflict', 'Submission journal exceeds its raw size limit');
|
|
694
|
+
throw new GovernanceError('RecoveryConflict', 'Submission journal exceeds its raw size limit. Inspect the journal and spec files, then restore a known state.');
|
|
647
695
|
}
|
|
648
696
|
}
|
|
649
697
|
const raw = await this.readOptional(journalPath);
|
|
@@ -654,10 +702,10 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
654
702
|
journal = JSON.parse(raw);
|
|
655
703
|
}
|
|
656
704
|
catch {
|
|
657
|
-
throw new GovernanceError('RecoveryConflict', 'Submission journal is malformed');
|
|
705
|
+
throw new GovernanceError('RecoveryConflict', 'Submission journal is malformed. Inspect the journal and spec files, then restore a known state.');
|
|
658
706
|
}
|
|
659
707
|
if (journal.schema_version !== 1) {
|
|
660
|
-
throw new GovernanceError('RecoveryConflict', 'Submission journal schema is unsupported');
|
|
708
|
+
throw new GovernanceError('RecoveryConflict', 'Submission journal schema is unsupported. Inspect the journal and spec files, then restore a known state.');
|
|
661
709
|
}
|
|
662
710
|
const priorSpec = decode(journal.spec?.prior);
|
|
663
711
|
const nextSpec = decode(journal.spec?.next);
|
|
@@ -670,10 +718,10 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
670
718
|
const currentArtifact = await this.readOptional(artifactPath);
|
|
671
719
|
const convergeNext = currentSpec === nextSpec;
|
|
672
720
|
if (currentSpec !== priorSpec && !convergeNext) {
|
|
673
|
-
throw new GovernanceError('RecoveryConflict', 'Current spec bytes match neither journal endpoint');
|
|
721
|
+
throw new GovernanceError('RecoveryConflict', 'Current spec bytes match neither journal endpoint. Inspect the spec file and restore a known state.');
|
|
674
722
|
}
|
|
675
723
|
if (currentArtifact !== priorArtifact && currentArtifact !== nextArtifact) {
|
|
676
|
-
throw new GovernanceError('RecoveryConflict', 'Current artifact bytes match neither journal endpoint');
|
|
724
|
+
throw new GovernanceError('RecoveryConflict', 'Current artifact bytes match neither journal endpoint. Inspect the artifact and restore a known state.');
|
|
677
725
|
}
|
|
678
726
|
await this.assertJournalValidation(featureRoot, journalPhase, nextSpec, nextArtifact);
|
|
679
727
|
await this.replaceCas(artifactPath, currentArtifact, convergeNext ? nextArtifact : priorArtifact, assertHeld);
|
|
@@ -688,13 +736,13 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
688
736
|
await assertHeld();
|
|
689
737
|
return;
|
|
690
738
|
}
|
|
691
|
-
throw new GovernanceError('RecoveryConflict', `Concurrent byte change at ${path.basename(filePath)}
|
|
739
|
+
throw new GovernanceError('RecoveryConflict', `Concurrent byte change at ${path.basename(filePath)}. Retry after the other writer finishes.`);
|
|
692
740
|
}
|
|
693
741
|
if (desired === null) {
|
|
694
742
|
if (current !== null)
|
|
695
743
|
await unlink(filePath);
|
|
696
744
|
if (await this.readOptional(filePath) !== null) {
|
|
697
|
-
throw new GovernanceError('RecoveryConflict', `Delete verification failed for ${path.basename(filePath)}
|
|
745
|
+
throw new GovernanceError('RecoveryConflict', `Delete verification failed for ${path.basename(filePath)}. Inspect the file and retry.`);
|
|
698
746
|
}
|
|
699
747
|
}
|
|
700
748
|
else {
|
|
@@ -716,7 +764,7 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
716
764
|
if (heading) {
|
|
717
765
|
const id = heading[1];
|
|
718
766
|
if (headings.has(id)) {
|
|
719
|
-
throw new GovernanceError('LegacyTaskConflict', `Duplicate legacy task ${id}
|
|
767
|
+
throw new GovernanceError('LegacyTaskConflict', `Duplicate legacy task ${id}. Rename one task so each identifier is unique.`);
|
|
720
768
|
}
|
|
721
769
|
const body = [];
|
|
722
770
|
for (let cursor = index + 1; cursor < lines.length && !headingPattern.test(lines[cursor]); cursor += 1) {
|
|
@@ -745,12 +793,12 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
745
793
|
}
|
|
746
794
|
const ids = new Set([...headings.keys(), ...checklist.keys()]);
|
|
747
795
|
if (ids.size > 5_000) {
|
|
748
|
-
throw new GovernanceError('LegacyTaskConflict', 'Legacy tasks contain too many task identifiers');
|
|
796
|
+
throw new GovernanceError('LegacyTaskConflict', 'Legacy tasks contain too many task identifiers. Split the work into a smaller tasks artifact.');
|
|
749
797
|
}
|
|
750
798
|
for (const id of ids) {
|
|
751
799
|
const title = headings.get(id)?.title ?? checklist.get(id)?.title ?? id;
|
|
752
800
|
if (id.length > 50 || title.trim().length === 0 || title.length > 500) {
|
|
753
|
-
throw new GovernanceError('LegacyTaskConflict', `Legacy task ${id.slice(0, 50)} exceeds schema-v5 field limits
|
|
801
|
+
throw new GovernanceError('LegacyTaskConflict', `Legacy task ${id.slice(0, 50)} exceeds schema-v5 field limits. Shorten its fields in the tasks artifact.`);
|
|
754
802
|
}
|
|
755
803
|
}
|
|
756
804
|
const parents = new Set();
|
|
@@ -763,7 +811,7 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
763
811
|
}
|
|
764
812
|
const leaves = [...ids].filter((id) => !parents.has(id));
|
|
765
813
|
if (leaves.length > 1_000) {
|
|
766
|
-
throw new GovernanceError('LegacyTaskConflict', 'Legacy tasks contain more than 1000 executable leaves');
|
|
814
|
+
throw new GovernanceError('LegacyTaskConflict', 'Legacy tasks contain more than 1000 executable leaves. Merge tasks to stay within 1000 leaves.');
|
|
767
815
|
}
|
|
768
816
|
return leaves.map((id) => {
|
|
769
817
|
const heading = headings.get(id);
|
|
@@ -813,7 +861,7 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
813
861
|
assertPriorApproved(phase, approvals) {
|
|
814
862
|
for (const prior of PHASES.slice(0, PHASES.indexOf(phase)))
|
|
815
863
|
if (!approvals[prior].approved)
|
|
816
|
-
throw new GovernanceError('PhaseNotApproved', `${prior} is not approved
|
|
864
|
+
throw new GovernanceError('PhaseNotApproved', `${prior} is not approved. Approve the ${prior} phase first.`);
|
|
817
865
|
}
|
|
818
866
|
assertLegacyOrdering(approvals) {
|
|
819
867
|
for (const [index, phase] of PHASES.entries()) {
|
|
@@ -821,7 +869,7 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
821
869
|
continue;
|
|
822
870
|
for (const prior of PHASES.slice(0, index)) {
|
|
823
871
|
if (!approvals[prior].approved) {
|
|
824
|
-
throw new GovernanceError('LegacyStateConflict', `${phase} exists before ${prior} approval
|
|
872
|
+
throw new GovernanceError('LegacyStateConflict', `${phase} exists before ${prior} approval. Approve ${prior} first, or move the ${phase} artifact out of the feature folder.`);
|
|
825
873
|
}
|
|
826
874
|
}
|
|
827
875
|
}
|
|
@@ -842,15 +890,15 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
842
890
|
? 'requirements-generated'
|
|
843
891
|
: 'init';
|
|
844
892
|
if (!implementationRank && legacyPhase !== expected) {
|
|
845
|
-
throw new GovernanceError('LegacyStateConflict', `Legacy phase ${legacyPhase} disagrees with approval records (${expected})
|
|
893
|
+
throw new GovernanceError('LegacyStateConflict', `Legacy phase ${legacyPhase} disagrees with approval records (${expected}). Correct the phase in the workflow state file.`);
|
|
846
894
|
}
|
|
847
895
|
if (typeof raw.ready_for_implementation === 'boolean' && raw.ready_for_implementation !== implementationRank) {
|
|
848
|
-
throw new GovernanceError('LegacyStateConflict', 'ready_for_implementation disagrees with legacy phase rank');
|
|
896
|
+
throw new GovernanceError('LegacyStateConflict', 'ready_for_implementation disagrees with legacy phase rank. Correct the flag in the workflow state file.');
|
|
849
897
|
}
|
|
850
898
|
if (typeof raw.implementation_completed === 'boolean') {
|
|
851
899
|
const completedRank = legacyPhase === 'completed' || legacyPhase === 'implementation-completed';
|
|
852
900
|
if (raw.implementation_completed !== completedRank) {
|
|
853
|
-
throw new GovernanceError('LegacyStateConflict', 'implementation_completed disagrees with legacy phase rank');
|
|
901
|
+
throw new GovernanceError('LegacyStateConflict', 'implementation_completed disagrees with legacy phase rank. Correct the flag in the workflow state file.');
|
|
854
902
|
}
|
|
855
903
|
}
|
|
856
904
|
}
|
|
@@ -861,7 +909,7 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
861
909
|
return WorkflowPhase.REQUIREMENTS; return WorkflowPhase.INIT; }
|
|
862
910
|
implementationCounts(value) { const tasks = Object.values(value.tasks); return { completed: tasks.filter((t) => t.status === 'completed').length, total: tasks.length, active: tasks.filter((t) => ['in-progress', 'red-observed', 'green-observed'].includes(t.status)).length, blocked: tasks.filter((t) => t.status === 'blocked').length }; }
|
|
863
911
|
allComplete(value) { const tasks = Object.values(value.tasks); return tasks.length > 0 && tasks.every((task) => task.status === 'completed'); }
|
|
864
|
-
invalidTransition(action, status) { throw new GovernanceError('TaskTransitionInvalid', `Cannot ${action} from ${status}
|
|
912
|
+
invalidTransition(action, status) { throw new GovernanceError('TaskTransitionInvalid', `Cannot ${action} from ${status}. Select a task action valid for its current status.`); }
|
|
865
913
|
evidence(value) {
|
|
866
914
|
if (typeof value.command !== 'string'
|
|
867
915
|
|| value.command.trim().length === 0
|
|
@@ -1382,7 +1430,7 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
1382
1430
|
const priorRaw = this.parseSpecBytes(priorBytes);
|
|
1383
1431
|
const nextRaw = this.parseSpecBytes(nextBytes);
|
|
1384
1432
|
if (priorRaw.schema_version !== 5 || nextRaw.schema_version !== 5) {
|
|
1385
|
-
throw new GovernanceError('RecoveryConflict', 'Submission journal endpoints must use schema-v5');
|
|
1433
|
+
throw new GovernanceError('RecoveryConflict', 'Submission journal endpoints must use schema-v5. Inspect the journal and spec files, then restore a known state.');
|
|
1386
1434
|
}
|
|
1387
1435
|
let prior;
|
|
1388
1436
|
let next;
|
|
@@ -1391,14 +1439,14 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
1391
1439
|
next = this.normalizeV5(nextRaw);
|
|
1392
1440
|
}
|
|
1393
1441
|
catch (error) {
|
|
1394
|
-
throw new GovernanceError('RecoveryConflict', 'Submission journal contains invalid workflow state', { cause: errorSummary(error) });
|
|
1442
|
+
throw new GovernanceError('RecoveryConflict', 'Submission journal contains invalid workflow state. Inspect the journal and spec files, then restore a known state.', { cause: errorSummary(error) });
|
|
1395
1443
|
}
|
|
1396
1444
|
const changed = PHASES.filter((phase) => next.approvals[phase].revision === prior.approvals[phase].revision + 1);
|
|
1397
1445
|
if (changed.length !== 1
|
|
1398
1446
|
|| prior.feature_name !== next.feature_name
|
|
1399
1447
|
|| PHASES.some((phase) => phase !== changed[0]
|
|
1400
1448
|
&& next.approvals[phase].revision !== prior.approvals[phase].revision)) {
|
|
1401
|
-
throw new GovernanceError('RecoveryConflict', 'Submission journal does not describe one phase revision');
|
|
1449
|
+
throw new GovernanceError('RecoveryConflict', 'Submission journal does not describe one phase revision. Inspect the journal and spec files, then restore a known state.');
|
|
1402
1450
|
}
|
|
1403
1451
|
const changedPhase = changed[0];
|
|
1404
1452
|
const priorRecord = prior.approvals[changedPhase];
|
|
@@ -1433,7 +1481,7 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
1433
1481
|
|| !priorArtifactValid
|
|
1434
1482
|
|| !submissionRecordValid
|
|
1435
1483
|
|| !workflowMetadataValid) {
|
|
1436
|
-
throw new GovernanceError('RecoveryConflict', 'Submission journal does not describe a valid phase submission');
|
|
1484
|
+
throw new GovernanceError('RecoveryConflict', 'Submission journal does not describe a valid phase submission. Inspect the journal and spec files, then restore a known state.');
|
|
1437
1485
|
}
|
|
1438
1486
|
return changedPhase;
|
|
1439
1487
|
}
|
|
@@ -1449,7 +1497,7 @@ let WorkflowEngineService = class WorkflowEngineService {
|
|
|
1449
1497
|
}
|
|
1450
1498
|
}
|
|
1451
1499
|
catch (error) {
|
|
1452
|
-
throw new GovernanceError('RecoveryConflict', 'Submission journal validation cannot be reproduced', { cause: errorSummary(error) });
|
|
1500
|
+
throw new GovernanceError('RecoveryConflict', 'Submission journal validation cannot be reproduced. Inspect the artifact, then submit the phase again.', { cause: errorSummary(error) });
|
|
1453
1501
|
}
|
|
1454
1502
|
}
|
|
1455
1503
|
journalAncillaryState(spec) {
|
|
@@ -1511,8 +1559,8 @@ export { WorkflowEngineService };
|
|
|
1511
1559
|
function sha256(content) { return createHash('sha256').update(content).digest('hex'); }
|
|
1512
1560
|
function encode(content) { return { base64: Buffer.from(content).toString('base64'), sha256: sha256(content) }; }
|
|
1513
1561
|
function decode(value) { if (!value || typeof value.base64 !== 'string' || typeof value.sha256 !== 'string')
|
|
1514
|
-
throw new GovernanceError('RecoveryConflict', 'Journal byte record is malformed'); const bytes = Buffer.from(value.base64, 'base64'); if (bytes.length > JOURNAL_LIMIT)
|
|
1515
|
-
throw new GovernanceError('RecoveryConflict', 'Journal byte record exceeds size limit'); const content = bytes.toString('utf8'); if (sha256(content) !== value.sha256 || Buffer.from(content).toString('base64') !== value.base64)
|
|
1516
|
-
throw new GovernanceError('RecoveryConflict', 'Journal byte hash or encoding is invalid'); return content; }
|
|
1562
|
+
throw new GovernanceError('RecoveryConflict', 'Journal byte record is malformed. Inspect the journal and spec files, then restore a known state.'); const bytes = Buffer.from(value.base64, 'base64'); if (bytes.length > JOURNAL_LIMIT)
|
|
1563
|
+
throw new GovernanceError('RecoveryConflict', 'Journal byte record exceeds size limit. Inspect the journal and spec files, then restore a known state.'); const content = bytes.toString('utf8'); if (sha256(content) !== value.sha256 || Buffer.from(content).toString('base64') !== value.base64)
|
|
1564
|
+
throw new GovernanceError('RecoveryConflict', 'Journal byte hash or encoding is invalid. Inspect the journal and spec files, then restore a known state.'); return content; }
|
|
1517
1565
|
function errorSummary(error) { return String(error).slice(0, 2_000); }
|
|
1518
1566
|
//# sourceMappingURL=WorkflowEngineService.js.map
|