ultra-builder-pro-cli 0.13.0 → 0.15.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/.ultra-template/docs/research/README.md +11 -7
- package/.ultra-template/specs/architecture.md +36 -36
- package/.ultra-template/specs/discovery.md +46 -33
- package/.ultra-template/specs/product.md +38 -29
- package/.ultra-template/specs/research-distillate.md +48 -0
- package/.ultra-template/tasks/contexts/TEMPLATE.md +11 -28
- package/CHANGELOG.md +86 -1
- package/README.md +40 -26
- package/adapters/_shared/codex-assets.cjs +29 -165
- package/adapters/_shared/file-ops.cjs +6 -2
- package/adapters/_shared/runtime-assets.cjs +1 -0
- package/adapters/claude.js +3 -2
- package/adapters/codex.js +1 -0
- package/adapters/kimi.js +3 -2
- package/adapters/opencode.js +33 -122
- package/agents/review-coordinator.md +9 -6
- package/docs/AGENT-CONTEXT.md +21 -11
- package/docs/ARCHITECTURE.md +41 -13
- package/docs/PLAN.zh-CN.md +3 -2
- package/docs/ROADMAP.md +9 -5
- package/docs/RUNTIME-COMPAT-MATRIX.md +6 -6
- package/docs/STATE-DB-ACCESS-POLICY.md +27 -25
- package/docs/WORKFLOW-LIFECYCLE.md +266 -0
- package/hooks/README.md +6 -1
- package/hooks/active_task_context.py +22 -50
- package/hooks/context_spine.py +61 -332
- package/hooks/health_check.py +29 -18
- package/hooks/pre_stop_check.py +28 -33
- package/hooks/subagent_tracker.py +54 -85
- package/hooks/workflow_checkpoint.py +39 -47
- package/hooks/workflow_context.py +4 -9
- package/hooks/workflow_resume.py +48 -128
- package/mcp-server/breadcrumb.cjs +22 -0
- package/mcp-server/hook-event.cjs +34 -0
- package/mcp-server/lib/archive-journal.cjs +17 -5
- package/mcp-server/lib/baseline-workflow.cjs +158 -18
- package/mcp-server/lib/change-workflow.cjs +550 -85
- package/mcp-server/lib/context-spine.cjs +242 -24
- package/mcp-server/lib/doctor.cjs +33 -3
- package/mcp-server/lib/init-project.cjs +210 -16
- package/mcp-server/lib/plan-store.cjs +4 -1
- package/mcp-server/lib/prd-parser.cjs +14 -41
- package/mcp-server/lib/pricing.cjs +7 -10
- package/mcp-server/lib/project-breadcrumb.cjs +145 -0
- package/mcp-server/lib/projector.cjs +57 -3
- package/mcp-server/lib/spec-learning.cjs +79 -2
- package/mcp-server/lib/state-db.cjs +145 -8
- package/mcp-server/lib/state-ops.cjs +160 -9
- package/mcp-server/lib/task-expander.cjs +9 -38
- package/mcp-server/lib/workflow-state.cjs +2292 -0
- package/mcp-server/server.cjs +182 -50
- package/orchestrator/daemon.cjs +1 -2
- package/orchestrator/dispatch-rules.cjs +5 -31
- package/orchestrator/planner/plan-builder.cjs +21 -12
- package/package.json +14 -6
- package/skills/cc-collab/SKILL.md +2 -2
- package/skills/codex-collab/SKILL.md +2 -3
- package/skills/ultra-change/SKILL.md +53 -68
- package/skills/ultra-change/references/change-contract.md +36 -0
- package/skills/ultra-deliver/SKILL.md +69 -79
- package/skills/ultra-deliver/references/baseline-reconciliation.md +20 -0
- package/skills/ultra-dev/SKILL.md +54 -59
- package/skills/ultra-doctor/SKILL.md +40 -71
- package/skills/ultra-init/SKILL.md +96 -103
- package/skills/ultra-init/references/brownfield-adoption.md +2 -2
- package/skills/ultra-plan/SKILL.md +79 -75
- package/skills/ultra-plan/references/semantic-preflight.md +18 -0
- package/skills/ultra-research/SKILL.md +87 -60
- package/skills/ultra-research/references/00-problem-validation.md +36 -0
- package/skills/ultra-research/references/01-opportunity-discovery.md +33 -0
- package/skills/ultra-research/references/02-market-assessment.md +30 -0
- package/skills/ultra-research/references/03-competitive-landscape.md +31 -0
- package/skills/ultra-research/references/04-product-strategy.md +32 -0
- package/skills/ultra-research/references/05-assumptions-validation.md +31 -0
- package/skills/ultra-research/references/10-user-personas.md +29 -0
- package/skills/ultra-research/references/11-user-scenarios.md +28 -0
- package/skills/ultra-research/references/20-user-stories.md +30 -0
- package/skills/ultra-research/references/21-features-scope.md +29 -0
- package/skills/ultra-research/references/22-success-metrics.md +28 -0
- package/skills/ultra-research/references/30-architecture-context.md +30 -0
- package/skills/ultra-research/references/31-solution-strategy.md +28 -0
- package/skills/ultra-research/references/32-building-blocks.md +28 -0
- package/skills/ultra-research/references/40-deployment.md +29 -0
- package/skills/ultra-research/references/41-quality-risks.md +28 -0
- package/skills/ultra-research/references/99-synthesis.md +35 -0
- package/skills/ultra-review/SKILL.md +57 -73
- package/skills/ultra-review/references/review-modes.md +27 -0
- package/skills/ultra-review/references/unified-schema.md +25 -0
- package/skills/ultra-review/scripts/review_wait.py +40 -3
- package/skills/ultra-status/SKILL.md +41 -45
- package/skills/ultra-test/SKILL.md +48 -55
- package/skills/ultra-verify/SKILL.md +22 -31
- package/skills/ultra-verify/references/{confidence-system.md → evidence-status.md} +3 -3
- package/skills/ultra-verify/scripts/verify_wait.py +54 -54
- package/spec/cli-protocol.md +7 -2
- package/spec/mcp-tools.yaml +422 -73
- package/spec/schemas/baseline-reconciliation.v1.schema.json +73 -0
- package/spec/schemas/delivery-report.v1.schema.json +111 -0
- package/spec/schemas/mcp-tools.schema.json +1 -1
- package/spec/schemas/state-db.sql +82 -1
- package/spec/schemas/tasks.v4.5.schema.json +8 -1
- package/spec/schemas/test-report.v1.schema.json +194 -0
- package/ultra-tools/cli.cjs +1 -1
- package/ultra-tools/commands/migrate.cjs +3 -3
- package/ultra-tools/commands/status.cjs +213 -12
- package/.ultra-template/delivery-report.json +0 -9
- package/.ultra-template/test-report.json +0 -8
- package/adapters/_shared/tests/file-ops.test.cjs +0 -54
- package/adapters/_shared/tests/frontmatter.test.cjs +0 -47
- package/adapters/_shared/tests/handbook.test.cjs +0 -101
- package/adapters/_shared/tests/path-rewrite.test.cjs +0 -60
- package/adapters/_shared/tests/provenance.test.cjs +0 -133
- package/adapters/_shared/tests/resolve-target.test.cjs +0 -75
- package/adapters/_shared/tests/runtime-assets.test.cjs +0 -131
- package/adapters/_shared/tests/settings-merge.test.cjs +0 -85
- package/adapters/_shared/tests/validate.test.cjs +0 -30
- package/adapters/tests/claude.test.cjs +0 -137
- package/adapters/tests/codex-hook.test.cjs +0 -115
- package/adapters/tests/codex.test.cjs +0 -423
- package/adapters/tests/kimi-hook.test.cjs +0 -122
- package/adapters/tests/kimi.test.cjs +0 -259
- package/adapters/tests/opencode.test.cjs +0 -371
- package/mcp-server/lib/archive-journal.test.cjs +0 -35
- package/mcp-server/lib/baseline-workflow.test.cjs +0 -477
- package/mcp-server/lib/change-workflow.test.cjs +0 -971
- package/mcp-server/lib/context-spine.test.cjs +0 -93
- package/mcp-server/lib/doctor.test.cjs +0 -148
- package/mcp-server/lib/llm-client.cjs +0 -157
- package/mcp-server/lib/llm-client.test.cjs +0 -156
- package/mcp-server/lib/plan-store.test.cjs +0 -132
- package/mcp-server/lib/prd-parser.test.cjs +0 -171
- package/mcp-server/lib/runtime-state.test.cjs +0 -92
- package/mcp-server/lib/spec-learning.test.cjs +0 -67
- package/mcp-server/lib/state-db.test.cjs +0 -314
- package/mcp-server/lib/task-expander.test.cjs +0 -324
- package/mcp-server/lib/topo.test.cjs +0 -158
- package/mcp-server/tests/circuit-breaker.test.cjs +0 -125
- package/mcp-server/tests/commit-hash-flow.test.cjs +0 -117
- package/mcp-server/tests/concurrency-update.test.cjs +0 -96
- package/mcp-server/tests/concurrency.test.cjs +0 -112
- package/mcp-server/tests/db-ops.test.cjs +0 -121
- package/mcp-server/tests/init-project.test.cjs +0 -393
- package/mcp-server/tests/migrate.test.cjs +0 -232
- package/mcp-server/tests/projector.test.cjs +0 -162
- package/mcp-server/tests/server-contract.test.cjs +0 -1471
- package/mcp-server/tests/staleness.test.cjs +0 -190
- package/mcp-server/tests/state-db.test.cjs +0 -106
- package/mcp-server/tests/state-ops.test.cjs +0 -449
- package/mcp-server/tests/tagged-tasks.test.cjs +0 -163
- package/mcp-server/tests/telemetry.test.cjs +0 -219
- package/orchestrator/planner/expand-prompt.cjs +0 -62
- package/orchestrator/planner/expand-prompt.test.cjs +0 -52
- package/orchestrator/planner/plan-builder.test.cjs +0 -165
- package/orchestrator/planner/prd-prompt.cjs +0 -44
- package/orchestrator/planner/prd-prompt.test.cjs +0 -30
- package/orchestrator/tests/auto-merge.test.cjs +0 -216
- package/orchestrator/tests/daemon.test.cjs +0 -311
- package/orchestrator/tests/dispatch-rules.test.cjs +0 -124
- package/orchestrator/tests/parallel-orchestrator.test.cjs +0 -233
- package/orchestrator/tests/phase-8b-gate.test.cjs +0 -183
- package/orchestrator/tests/recovery.test.cjs +0 -190
- package/orchestrator/tests/session-runner.test.cjs +0 -314
- package/orchestrator/tests/skill-miner.test.cjs +0 -147
- package/orchestrator/tests/worktree-manager.test.cjs +0 -148
- package/skills/ultra-research/references/architecture.md +0 -38
- package/skills/ultra-research/references/problem-product.md +0 -39
- package/skills/ultra-research/references/quality-delivery.md +0 -34
- package/skills/ultra-research/references/synthesis.md +0 -38
- package/skills/ultra-research/references/users-scope.md +0 -40
- package/ultra-tools/legacy-memory.test.cjs +0 -80
- package/ultra-tools/status-command.test.cjs +0 -181
- package/ultra-tools/system-command.test.cjs +0 -309
|
@@ -4,6 +4,7 @@ const crypto = require('node:crypto');
|
|
|
4
4
|
const fs = require('node:fs');
|
|
5
5
|
const path = require('node:path');
|
|
6
6
|
const { spawnSync } = require('node:child_process');
|
|
7
|
+
const Ajv = require('ajv/dist/2020');
|
|
7
8
|
|
|
8
9
|
const ops = require('./state-ops.cjs');
|
|
9
10
|
const contextSpine = require('./context-spine.cjs');
|
|
@@ -11,9 +12,17 @@ const specLearning = require('./spec-learning.cjs');
|
|
|
11
12
|
const providerRefs = require('./provider-refs.cjs');
|
|
12
13
|
const baselines = require('./baseline-workflow.cjs');
|
|
13
14
|
const archiveJournal = require('./archive-journal.cjs');
|
|
15
|
+
const workflows = require('./workflow-state.cjs');
|
|
16
|
+
const reconciliationSchema = require('../../spec/schemas/baseline-reconciliation.v1.schema.json');
|
|
17
|
+
|
|
18
|
+
const reconciliationAjv = new Ajv({ allErrors: true, strict: false });
|
|
19
|
+
const validateReconciliationSchema = reconciliationAjv.compile(reconciliationSchema);
|
|
14
20
|
|
|
15
21
|
const CHANGE_ID = /^[a-zA-Z0-9_-]+$/;
|
|
16
|
-
const CHANGE_PATCH_FIELDS = new Set([
|
|
22
|
+
const CHANGE_PATCH_FIELDS = new Set([
|
|
23
|
+
'title', 'intent', 'status', 'docs_impact', 'provider_refs',
|
|
24
|
+
'contract', 'classification', 'research_disposition',
|
|
25
|
+
]);
|
|
17
26
|
const CHANGE_TRANSITIONS = Object.freeze({
|
|
18
27
|
active: new Set(['active', 'blocked', 'cancelled']),
|
|
19
28
|
blocked: new Set(['active', 'blocked', 'cancelled']),
|
|
@@ -21,12 +30,14 @@ const CHANGE_TRANSITIONS = Object.freeze({
|
|
|
21
30
|
archived: new Set(),
|
|
22
31
|
cancelled: new Set(),
|
|
23
32
|
});
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
const CHANGE_KINDS = new Set(['quick', 'standard', 'major', 'incident']);
|
|
34
|
+
const CHANGE_RISK_FLAGS = new Set([
|
|
35
|
+
'public_contract', 'schema_migration', 'data_migration', 'authorization', 'security',
|
|
36
|
+
'multi_module', 'external_integration', 'release_semantics',
|
|
37
|
+
]);
|
|
38
|
+
const QUICK_EXCLUSION_FLAGS = new Set(CHANGE_RISK_FLAGS);
|
|
39
|
+
const RESEARCH_DISPOSITIONS = new Set(['none', 'bounded', 'required']);
|
|
40
|
+
const CHANGE_RESEARCH_MODES = new Set(['product', 'feature', 'architecture', 'custom']);
|
|
30
41
|
const INCIDENT_DIAGNOSIS_SECTIONS = Object.freeze([
|
|
31
42
|
{ heading: 'Reproduction', key: 'reproduction' },
|
|
32
43
|
{ heading: 'Hypotheses', key: 'hypotheses' },
|
|
@@ -63,10 +74,18 @@ function rowToChange(row) {
|
|
|
63
74
|
provider_refs: parseJson(row.provider_refs_json, 'provider_refs_json'),
|
|
64
75
|
baseline_bypass: row.baseline_bypass_json
|
|
65
76
|
? parseJson(row.baseline_bypass_json, 'baseline_bypass_json') : null,
|
|
77
|
+
contract: parseJson(row.contract_json || '{}', 'contract_json'),
|
|
78
|
+
classification: parseJson(row.classification_json || '{}', 'classification_json'),
|
|
79
|
+
research_disposition: parseJson(
|
|
80
|
+
row.research_disposition_json || '{}', 'research_disposition_json',
|
|
81
|
+
),
|
|
66
82
|
};
|
|
67
83
|
delete change.docs_impact_json;
|
|
68
84
|
delete change.provider_refs_json;
|
|
69
85
|
delete change.baseline_bypass_json;
|
|
86
|
+
delete change.contract_json;
|
|
87
|
+
delete change.classification_json;
|
|
88
|
+
delete change.research_disposition_json;
|
|
70
89
|
return change;
|
|
71
90
|
}
|
|
72
91
|
|
|
@@ -88,6 +107,140 @@ function normalizeProviderRefs(value) {
|
|
|
88
107
|
return providerRefs.normalizeProviderRefs(value, ChangeWorkflowError);
|
|
89
108
|
}
|
|
90
109
|
|
|
110
|
+
function requiredText(value, field) {
|
|
111
|
+
const text = typeof value === 'string' ? value.trim() : '';
|
|
112
|
+
if (!text) throw new ChangeWorkflowError('CHANGE_CONTRACT_REQUIRED', `${field} is required`);
|
|
113
|
+
return text;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function stringList(value, field, { allowEmpty = false } = {}) {
|
|
117
|
+
if (!Array.isArray(value) || (!allowEmpty && value.length === 0)
|
|
118
|
+
|| value.some((item) => typeof item !== 'string' || !item.trim())) {
|
|
119
|
+
throw new ChangeWorkflowError(
|
|
120
|
+
'CHANGE_CONTRACT_REQUIRED', `${field} must be ${allowEmpty ? 'an' : 'a non-empty'} array of strings`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
return [...new Set(value.map((item) => item.trim()))];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function normalizeChangeContract(value) {
|
|
127
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
128
|
+
throw new ChangeWorkflowError('CHANGE_CONTRACT_REQUIRED', 'contract is required');
|
|
129
|
+
}
|
|
130
|
+
if (!Array.isArray(value.acceptance) || value.acceptance.length === 0) {
|
|
131
|
+
throw new ChangeWorkflowError('CHANGE_CONTRACT_REQUIRED', 'contract.acceptance is required');
|
|
132
|
+
}
|
|
133
|
+
const ids = new Set();
|
|
134
|
+
const acceptance = value.acceptance.map((item, index) => {
|
|
135
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
136
|
+
throw new ChangeWorkflowError('CHANGE_CONTRACT_REQUIRED', `contract.acceptance[${index}] is invalid`);
|
|
137
|
+
}
|
|
138
|
+
const id = requiredText(item.id, `contract.acceptance[${index}].id`);
|
|
139
|
+
if (!CHANGE_ID.test(id) || ids.has(id)) {
|
|
140
|
+
throw new ChangeWorkflowError('CHANGE_CONTRACT_REQUIRED', `acceptance id is invalid or duplicated: ${id}`);
|
|
141
|
+
}
|
|
142
|
+
ids.add(id);
|
|
143
|
+
return {
|
|
144
|
+
id,
|
|
145
|
+
criterion: requiredText(item.criterion, `contract.acceptance[${index}].criterion`),
|
|
146
|
+
verification: requiredText(item.verification, `contract.acceptance[${index}].verification`),
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
const recovery = value.recovery;
|
|
150
|
+
if (!recovery || typeof recovery !== 'object' || Array.isArray(recovery)) {
|
|
151
|
+
throw new ChangeWorkflowError('CHANGE_CONTRACT_REQUIRED', 'contract.recovery is required');
|
|
152
|
+
}
|
|
153
|
+
if (!Array.isArray(value.unresolved_decisions)) {
|
|
154
|
+
throw new ChangeWorkflowError(
|
|
155
|
+
'CHANGE_CONTRACT_REQUIRED', 'contract.unresolved_decisions must be an explicit array',
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
const decisions = value.unresolved_decisions.map((item, index) => {
|
|
159
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
160
|
+
throw new ChangeWorkflowError('CHANGE_CONTRACT_REQUIRED', `unresolved decision ${index} is invalid`);
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
id: requiredText(item.id, `contract.unresolved_decisions[${index}].id`),
|
|
164
|
+
summary: requiredText(item.summary, `contract.unresolved_decisions[${index}].summary`),
|
|
165
|
+
blocking: item.blocking === true,
|
|
166
|
+
owner: item.owner == null ? null : requiredText(item.owner, `contract.unresolved_decisions[${index}].owner`),
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
return {
|
|
170
|
+
outcome: requiredText(value.outcome, 'contract.outcome'),
|
|
171
|
+
acceptance,
|
|
172
|
+
non_goals: stringList(value.non_goals, 'contract.non_goals'),
|
|
173
|
+
public_seams: stringList(value.public_seams, 'contract.public_seams'),
|
|
174
|
+
recovery: {
|
|
175
|
+
strategy: requiredText(recovery.strategy, 'contract.recovery.strategy'),
|
|
176
|
+
verification: requiredText(recovery.verification, 'contract.recovery.verification'),
|
|
177
|
+
},
|
|
178
|
+
unresolved_decisions: decisions,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function normalizeClassification(value, kind) {
|
|
183
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
184
|
+
throw new ChangeWorkflowError('CHANGE_CLASSIFICATION_REQUIRED', 'classification is required');
|
|
185
|
+
}
|
|
186
|
+
const flags = stringList(value.risk_flags, 'classification.risk_flags', { allowEmpty: true });
|
|
187
|
+
const invalid = flags.filter((flag) => !CHANGE_RISK_FLAGS.has(flag));
|
|
188
|
+
if (invalid.length > 0) {
|
|
189
|
+
throw new ChangeWorkflowError('CHANGE_CLASSIFICATION_REQUIRED', `unknown risk flags: ${invalid.join(', ')}`);
|
|
190
|
+
}
|
|
191
|
+
if (kind === 'quick') {
|
|
192
|
+
const excluded = flags.filter((flag) => QUICK_EXCLUSION_FLAGS.has(flag));
|
|
193
|
+
if (excluded.length > 0) {
|
|
194
|
+
throw new ChangeWorkflowError(
|
|
195
|
+
'CHANGE_PROFILE_ESCALATION_REQUIRED',
|
|
196
|
+
`quick changes cannot carry these risk flags: ${excluded.join(', ')}`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
rationale: requiredText(value.rationale, 'classification.rationale'),
|
|
202
|
+
risk_flags: flags,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function normalizeResearchDisposition(value, kind) {
|
|
207
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
208
|
+
throw new ChangeWorkflowError('CHANGE_RESEARCH_DISPOSITION_REQUIRED', 'research_disposition is required');
|
|
209
|
+
}
|
|
210
|
+
const status = String(value.status || '').trim();
|
|
211
|
+
if (!RESEARCH_DISPOSITIONS.has(status)) {
|
|
212
|
+
throw new ChangeWorkflowError('CHANGE_RESEARCH_DISPOSITION_REQUIRED', 'research disposition status is invalid');
|
|
213
|
+
}
|
|
214
|
+
const selectedSteps = stringList(
|
|
215
|
+
value.selected_steps || [], 'research_disposition.selected_steps', { allowEmpty: true },
|
|
216
|
+
);
|
|
217
|
+
const mode = value.mode == null ? null : String(value.mode).trim();
|
|
218
|
+
if (status === 'none' && (mode !== null || selectedSteps.length > 0)) {
|
|
219
|
+
throw new ChangeWorkflowError(
|
|
220
|
+
'CHANGE_RESEARCH_DISPOSITION_REQUIRED', 'research status none cannot select a mode or steps',
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
if (status !== 'none' && !CHANGE_RESEARCH_MODES.has(mode)) {
|
|
224
|
+
throw new ChangeWorkflowError(
|
|
225
|
+
'CHANGE_RESEARCH_DISPOSITION_REQUIRED', 'bounded or required research needs a supported mode',
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
if (mode === 'custom' && selectedSteps.length === 0) {
|
|
229
|
+
throw new ChangeWorkflowError(
|
|
230
|
+
'CHANGE_RESEARCH_DISPOSITION_REQUIRED', 'custom research needs selected_steps',
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
if (kind === 'quick' && status !== 'none') {
|
|
234
|
+
throw new ChangeWorkflowError(
|
|
235
|
+
'CHANGE_PROFILE_ESCALATION_REQUIRED', 'a change that requires research cannot use the quick profile',
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
status, mode, selected_steps: selectedSteps,
|
|
240
|
+
rationale: requiredText(value.rationale, 'research_disposition.rationale'),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
91
244
|
function normalizeBaselineBypass(value) {
|
|
92
245
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
93
246
|
throw new ChangeWorkflowError(
|
|
@@ -130,6 +283,138 @@ function safeRelativePath(rootDir, candidate) {
|
|
|
130
283
|
return target;
|
|
131
284
|
}
|
|
132
285
|
|
|
286
|
+
function readReconciliationManifest(db, change, input, rootDir) {
|
|
287
|
+
const relative = typeof input.reconciliation_path === 'string'
|
|
288
|
+
? input.reconciliation_path.trim() : '';
|
|
289
|
+
if (!relative) {
|
|
290
|
+
throw new ChangeWorkflowError(
|
|
291
|
+
'BASELINE_RECONCILIATION_MANIFEST_REQUIRED', 'reconciliation_path is required',
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
let file = safeRelativePath(rootDir, relative);
|
|
295
|
+
const artifactRoot = path.resolve(rootDir, change.artifact_root);
|
|
296
|
+
if (!file.startsWith(`${artifactRoot}${path.sep}`)) {
|
|
297
|
+
throw new ChangeWorkflowError(
|
|
298
|
+
'BASELINE_RECONCILIATION_MANIFEST_REQUIRED',
|
|
299
|
+
'reconciliation manifest must be a file inside the active change artifact root',
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
let manifest;
|
|
303
|
+
if (!fs.existsSync(file)) {
|
|
304
|
+
const pending = archiveJournal.listArchiveIntents(rootDir).find(
|
|
305
|
+
(item) => !item.error && item.intent.change_id === change.id
|
|
306
|
+
&& item.intent.reconciliation_path === relative,
|
|
307
|
+
);
|
|
308
|
+
if (pending) {
|
|
309
|
+
const suffix = path.relative(pending.intent.source, relative);
|
|
310
|
+
file = path.join(pending.destination, suffix);
|
|
311
|
+
manifest = pending.intent.reconciliation_manifest;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
|
|
315
|
+
throw new ChangeWorkflowError(
|
|
316
|
+
'BASELINE_RECONCILIATION_MANIFEST_REQUIRED', 'reconciliation manifest file is missing',
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
try { manifest ||= JSON.parse(fs.readFileSync(file, 'utf8')); }
|
|
320
|
+
catch (error) {
|
|
321
|
+
throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_INVALID', error.message);
|
|
322
|
+
}
|
|
323
|
+
if (!validateReconciliationSchema(manifest)) {
|
|
324
|
+
throw new ChangeWorkflowError(
|
|
325
|
+
'BASELINE_RECONCILIATION_MANIFEST_INVALID',
|
|
326
|
+
`reconciliation manifest does not satisfy its published schema: ${reconciliationAjv.errorsText(validateReconciliationSchema.errors)}`,
|
|
327
|
+
validateReconciliationSchema.errors,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
const baseline = baselines.readBaseline(db);
|
|
331
|
+
const updates = Array.isArray(input.baseline_updates) ? input.baseline_updates : [];
|
|
332
|
+
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)
|
|
333
|
+
|| manifest.$schema !== 'ultra-baseline-reconciliation-v1'
|
|
334
|
+
|| manifest.change_id !== change.id || manifest.baseline_id !== (baseline?.id || null)
|
|
335
|
+
|| JSON.stringify(manifest.baseline_updates) !== JSON.stringify(updates)
|
|
336
|
+
|| !Array.isArray(manifest.semantic_changes)
|
|
337
|
+
|| !Array.isArray(manifest.resolved_gap_ids)
|
|
338
|
+
|| !Array.isArray(manifest.resolved_unknowns)
|
|
339
|
+
|| !Array.isArray(manifest.verification) || manifest.verification.length === 0) {
|
|
340
|
+
throw new ChangeWorkflowError(
|
|
341
|
+
'BASELINE_RECONCILIATION_MANIFEST_INVALID', 'reconciliation manifest shape or authority binding is invalid',
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
if (manifest.verification.some((item) => (
|
|
345
|
+
!item || typeof item !== 'object' || item.status !== 'pass'
|
|
346
|
+
|| !String(item.name || '').trim() || !String(item.command || '').trim()
|
|
347
|
+
|| !String(item.evidence || '').trim()
|
|
348
|
+
))) {
|
|
349
|
+
throw new ChangeWorkflowError(
|
|
350
|
+
'BASELINE_RECONCILIATION_MANIFEST_INVALID', 'every reconciliation verification must be an evidenced pass',
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
if (updates.length === 0) {
|
|
354
|
+
if (manifest.semantic_changes.length > 0
|
|
355
|
+
|| String(manifest.semantic_no_change_reason || '').trim().length < 3) {
|
|
356
|
+
throw new ChangeWorkflowError(
|
|
357
|
+
'BASELINE_RECONCILIATION_MANIFEST_INVALID',
|
|
358
|
+
'no-change reconciliation requires an explicit semantic_no_change_reason and no semantic changes',
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
} else if (manifest.semantic_changes.length === 0) {
|
|
362
|
+
throw new ChangeWorkflowError(
|
|
363
|
+
'BASELINE_RECONCILIATION_MANIFEST_INVALID', 'baseline updates require semantic change records',
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
const specRefs = new Map((baseline?.spec_refs || []).map((ref) => [ref.path, ref]));
|
|
367
|
+
const semanticPaths = new Set();
|
|
368
|
+
for (const item of manifest.semantic_changes) {
|
|
369
|
+
if (!item || typeof item !== 'object' || !CHANGE_ID.test(String(item.id || ''))
|
|
370
|
+
|| !['add', 'update'].includes(item.action)
|
|
371
|
+
|| !/^[0-9a-f]{64}$/.test(String(item.after_digest || ''))) {
|
|
372
|
+
throw new ChangeWorkflowError(
|
|
373
|
+
'BASELINE_RECONCILIATION_MANIFEST_INVALID', 'semantic change record is invalid',
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
let resolved;
|
|
377
|
+
try { resolved = workflows.resolveProjectSourceRef(rootDir, item.source_ref, 'semantic_change.source_ref'); }
|
|
378
|
+
catch (error) {
|
|
379
|
+
throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_INVALID', error.message);
|
|
380
|
+
}
|
|
381
|
+
semanticPaths.add(resolved.relative);
|
|
382
|
+
if (!updates.includes(resolved.relative) || resolved.digest !== item.after_digest) {
|
|
383
|
+
throw new ChangeWorkflowError(
|
|
384
|
+
'BASELINE_RECONCILIATION_MANIFEST_INVALID',
|
|
385
|
+
`semantic change digest or baseline update binding is invalid: ${item.source_ref}`,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
const before = specRefs.get(resolved.relative)?.digest || null;
|
|
389
|
+
if ((item.before_digest || null) !== before || item.action !== (before ? 'update' : 'add')) {
|
|
390
|
+
throw new ChangeWorkflowError(
|
|
391
|
+
'BASELINE_RECONCILIATION_MANIFEST_INVALID',
|
|
392
|
+
`semantic change before-state is invalid: ${item.source_ref}`,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const missingSemanticUpdates = updates.filter((update) => !semanticPaths.has(update));
|
|
397
|
+
if (missingSemanticUpdates.length > 0) {
|
|
398
|
+
throw new ChangeWorkflowError(
|
|
399
|
+
'BASELINE_RECONCILIATION_MANIFEST_INVALID',
|
|
400
|
+
`baseline updates have no semantic record: ${missingSemanticUpdates.join(', ')}`,
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
const gapIds = new Set((baseline?.gaps || []).map((gap) => gap.id));
|
|
404
|
+
const unknowns = new Set((baseline?.unknowns || []).map((unknown) => unknown.summary));
|
|
405
|
+
if (manifest.resolved_gap_ids.some((id) => !gapIds.has(id))
|
|
406
|
+
|| manifest.resolved_unknowns.some((summary) => !unknowns.has(summary))) {
|
|
407
|
+
throw new ChangeWorkflowError(
|
|
408
|
+
'BASELINE_RECONCILIATION_MANIFEST_INVALID', 'resolved gap or unknown does not exist in baseline authority',
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
const serialized = fs.readFileSync(file);
|
|
412
|
+
return {
|
|
413
|
+
manifest, relative, file,
|
|
414
|
+
digest: crypto.createHash('sha256').update(serialized).digest('hex'),
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
133
418
|
function readChange(db, id) {
|
|
134
419
|
return rowToChange(db.prepare('SELECT * FROM changes WHERE id = ?').get(id));
|
|
135
420
|
}
|
|
@@ -151,11 +436,24 @@ function writeIntent(file, change) {
|
|
|
151
436
|
`- Kind: \`${change.kind}\``,
|
|
152
437
|
`- Base commit: \`${change.base_commit || 'unavailable'}\``,
|
|
153
438
|
`- Documentation impact: \`${impact.status}\``,
|
|
439
|
+
`- Research disposition: \`${change.research_disposition.status}\``,
|
|
154
440
|
'',
|
|
155
441
|
'## Intent',
|
|
156
442
|
'',
|
|
157
443
|
change.intent,
|
|
158
444
|
'',
|
|
445
|
+
'## Outcome', '', change.contract.outcome, '',
|
|
446
|
+
'## Acceptance', '',
|
|
447
|
+
...change.contract.acceptance.flatMap((item) => [
|
|
448
|
+
`### ${item.id}`, '', item.criterion, '', `Verification: \`${item.verification}\``, '',
|
|
449
|
+
]),
|
|
450
|
+
'## Public seams', '', ...change.contract.public_seams.map((item) => `- ${item}`), '',
|
|
451
|
+
'## Non-goals', '', ...change.contract.non_goals.map((item) => `- ${item}`), '',
|
|
452
|
+
'## Recovery', '', change.contract.recovery.strategy, '',
|
|
453
|
+
`Verification: \`${change.contract.recovery.verification}\``, '',
|
|
454
|
+
'## Classification', '', change.classification.rationale, '',
|
|
455
|
+
`Risk flags: ${change.classification.risk_flags.join(', ') || 'none'}`, '',
|
|
456
|
+
'## Research routing', '', change.research_disposition.rationale, '',
|
|
159
457
|
];
|
|
160
458
|
if (impact.rationale) lines.push('## Documentation rationale', '', impact.rationale, '');
|
|
161
459
|
if (change.baseline_bypass) {
|
|
@@ -240,23 +538,27 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
240
538
|
? change.provider_refs
|
|
241
539
|
: normalizeProviderRefs(input.provider_refs);
|
|
242
540
|
const tasks = db.prepare(
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
FROM tasks WHERE change_id = ? ORDER BY created_at ASC`,
|
|
246
|
-
).all(change.id).map((task) => ({ ...task, stale: Boolean(task.stale) }));
|
|
541
|
+
'SELECT id FROM tasks WHERE change_id = ? ORDER BY created_at ASC',
|
|
542
|
+
).all(change.id).map((task) => ops.readTask(db, task.id));
|
|
247
543
|
const specs = Array.isArray(input.spec_refs) ? input.spec_refs : [];
|
|
248
|
-
const allowedPaths = Array.isArray(input.allowed_paths) ? input.allowed_paths : [];
|
|
249
|
-
const head = gitHead(rootDir);
|
|
250
544
|
const spine = contextSpine.compileRoleContext(db, { input, change, tasks, rootDir });
|
|
545
|
+
const snapshotId = `ctx-${crypto.randomUUID().slice(0, 12)}`;
|
|
251
546
|
const manifest = {
|
|
252
547
|
schema_version: '2.0',
|
|
548
|
+
snapshot_id: snapshotId,
|
|
253
549
|
generated_at: nowIso(),
|
|
254
550
|
change: {
|
|
255
551
|
id: change.id, title: change.title, kind: change.kind, status: change.status,
|
|
256
552
|
intent: change.intent, docs_impact: change.docs_impact, base_commit: change.base_commit,
|
|
257
553
|
baseline_bypass: change.baseline_bypass,
|
|
554
|
+
contract: change.contract, classification: change.classification,
|
|
555
|
+
research_disposition: change.research_disposition,
|
|
556
|
+
},
|
|
557
|
+
git: {
|
|
558
|
+
head: spine.resume.git_head,
|
|
559
|
+
worktree_state: spine.resume.worktree_state,
|
|
560
|
+
worktree_digest: spine.resume.worktree_digest,
|
|
258
561
|
},
|
|
259
|
-
git: { head },
|
|
260
562
|
tasks,
|
|
261
563
|
selected_task: spine.selected_task,
|
|
262
564
|
specs,
|
|
@@ -270,11 +572,11 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
270
572
|
resume: spine.resume,
|
|
271
573
|
providers,
|
|
272
574
|
provider_boundary: 'metadata references only; memory and code graph content remain external',
|
|
273
|
-
tool_policy: { allowed_paths: allowedPaths },
|
|
274
575
|
};
|
|
275
576
|
const artifactDir = path.resolve(rootDir, change.artifact_root);
|
|
276
577
|
fs.mkdirSync(artifactDir, { recursive: true });
|
|
277
|
-
const manifestPath = path.join(artifactDir, '
|
|
578
|
+
const manifestPath = path.join(artifactDir, 'contexts', `${snapshotId}.json`);
|
|
579
|
+
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
278
580
|
const serialized = `${JSON.stringify(manifest, null, 2)}\n`;
|
|
279
581
|
fs.writeFileSync(manifestPath, serialized);
|
|
280
582
|
const hash = crypto.createHash('sha256').update(serialized).digest('hex');
|
|
@@ -288,8 +590,8 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
288
590
|
token_estimate, token_budget)
|
|
289
591
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
290
592
|
).run(
|
|
291
|
-
|
|
292
|
-
|
|
593
|
+
snapshotId, change.id, input.task_id || null,
|
|
594
|
+
spine.resume.git_head, JSON.stringify(providers), relative, hash,
|
|
293
595
|
spine.role, spine.gate, spine.next_action, spine.readiness.status,
|
|
294
596
|
JSON.stringify(spine.readiness.blockers), JSON.stringify(spine),
|
|
295
597
|
spine.context.token_estimate, spine.context.budget.max_tokens,
|
|
@@ -298,7 +600,9 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
298
600
|
change_id: change.id, task_id: input.task_id || null, kind: 'context_manifest',
|
|
299
601
|
artifactPath: relative, contentHash: hash,
|
|
300
602
|
metadata: {
|
|
301
|
-
git_head:
|
|
603
|
+
git_head: spine.resume.git_head,
|
|
604
|
+
worktree_digest: spine.resume.worktree_digest,
|
|
605
|
+
role: spine.role, gate: spine.gate,
|
|
302
606
|
readiness: spine.readiness.status, token_estimate: spine.context.token_estimate,
|
|
303
607
|
},
|
|
304
608
|
});
|
|
@@ -314,11 +618,35 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
314
618
|
ops.appendEventInTx(db, {
|
|
315
619
|
type: 'context_updated', change_id: change.id, task_id: input.task_id || null,
|
|
316
620
|
payload: {
|
|
317
|
-
manifest_path: relative, manifest_hash: hash, git_head:
|
|
621
|
+
manifest_path: relative, manifest_hash: hash, git_head: spine.resume.git_head,
|
|
622
|
+
worktree_digest: spine.resume.worktree_digest,
|
|
318
623
|
role: spine.role, gate: spine.gate, readiness: spine.readiness.status,
|
|
319
624
|
next_action: spine.next_action,
|
|
320
625
|
},
|
|
321
626
|
});
|
|
627
|
+
const linkedWorkflow = db.prepare(
|
|
628
|
+
`SELECT id FROM workflow_runs
|
|
629
|
+
WHERE kind = 'change' AND change_id = ? AND current_step = 'compile-context'
|
|
630
|
+
AND status IN ('active', 'blocked')
|
|
631
|
+
ORDER BY updated_at DESC, rowid DESC LIMIT 1`,
|
|
632
|
+
).get(change.id);
|
|
633
|
+
if (linkedWorkflow) {
|
|
634
|
+
if (spine.readiness.status === 'ready') {
|
|
635
|
+
workflows.recordWorkflowStep(db, {
|
|
636
|
+
id: linkedWorkflow.id, step_id: 'compile-context', status: 'completed',
|
|
637
|
+
evidence: [{
|
|
638
|
+
kind: 'context', ref: relative,
|
|
639
|
+
summary: `Role ${spine.role} context is ready for ${input.task_id || 'change planning'}.`,
|
|
640
|
+
}],
|
|
641
|
+
outputs: [{ path: relative, kind: 'context-manifest' }],
|
|
642
|
+
}, { rootDir });
|
|
643
|
+
} else {
|
|
644
|
+
workflows.recordWorkflowStep(db, {
|
|
645
|
+
id: linkedWorkflow.id, step_id: 'compile-context', status: 'blocked',
|
|
646
|
+
blockers: spine.readiness.blockers,
|
|
647
|
+
}, { rootDir });
|
|
648
|
+
}
|
|
649
|
+
}
|
|
322
650
|
});
|
|
323
651
|
return { manifest, context_manifest_path: manifestPath, manifest_hash: hash };
|
|
324
652
|
}
|
|
@@ -332,7 +660,7 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
332
660
|
'id plus non-blank title and intent of at least three characters are required',
|
|
333
661
|
);
|
|
334
662
|
}
|
|
335
|
-
if (!
|
|
663
|
+
if (!CHANGE_KINDS.has(input.kind)) {
|
|
336
664
|
throw new ChangeWorkflowError('VALIDATION_ERROR', `invalid change kind: ${input.kind}`);
|
|
337
665
|
}
|
|
338
666
|
if (input.baseline_bypass !== undefined && input.kind !== 'incident') {
|
|
@@ -359,6 +687,9 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
359
687
|
}
|
|
360
688
|
const docsImpact = normalizeDocsImpact(input.docs_impact);
|
|
361
689
|
const providers = normalizeProviderRefs(input.provider_refs);
|
|
690
|
+
const contract = normalizeChangeContract(input.contract);
|
|
691
|
+
const classification = normalizeClassification(input.classification, input.kind);
|
|
692
|
+
const researchDisposition = normalizeResearchDisposition(input.research_disposition, input.kind);
|
|
362
693
|
const artifactRoot = path.join('.ultra', 'changes', 'active', input.id);
|
|
363
694
|
const artifactDir = path.resolve(rootDir, artifactRoot);
|
|
364
695
|
if (fs.existsSync(artifactDir)) {
|
|
@@ -368,6 +699,7 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
368
699
|
const row = {
|
|
369
700
|
id: input.id, title, kind: input.kind, status: 'active',
|
|
370
701
|
intent, docs_impact: docsImpact, provider_refs: providers, baseline_bypass: baselineBypass,
|
|
702
|
+
contract, classification, research_disposition: researchDisposition,
|
|
371
703
|
base_commit: input.base_commit || gitHead(rootDir), artifact_root: artifactRoot,
|
|
372
704
|
created_at: ts, updated_at: ts,
|
|
373
705
|
};
|
|
@@ -376,13 +708,15 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
376
708
|
db.prepare(
|
|
377
709
|
`INSERT INTO changes
|
|
378
710
|
(id, title, kind, status, intent, docs_impact_json, provider_refs_json,
|
|
379
|
-
baseline_bypass_json,
|
|
380
|
-
|
|
711
|
+
baseline_bypass_json, contract_json, classification_json,
|
|
712
|
+
research_disposition_json, base_commit, artifact_root, created_at, updated_at)
|
|
713
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
381
714
|
).run(
|
|
382
715
|
row.id, row.title, row.kind, row.status, row.intent, JSON.stringify(row.docs_impact),
|
|
383
716
|
JSON.stringify(row.provider_refs), row.baseline_bypass
|
|
384
717
|
? JSON.stringify(row.baseline_bypass) : null,
|
|
385
|
-
row.
|
|
718
|
+
JSON.stringify(row.contract), JSON.stringify(row.classification),
|
|
719
|
+
JSON.stringify(row.research_disposition), row.base_commit, row.artifact_root, ts, ts,
|
|
386
720
|
);
|
|
387
721
|
fs.mkdirSync(artifactDir, { recursive: true });
|
|
388
722
|
const intentPath = path.join(artifactDir, 'intent.md');
|
|
@@ -406,10 +740,41 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
406
740
|
type: 'change_created', change_id: row.id,
|
|
407
741
|
payload: { kind: row.kind, artifact_root: row.artifact_root },
|
|
408
742
|
});
|
|
743
|
+
let workflow = workflows.startWorkflow(db, {
|
|
744
|
+
kind: 'change', baseline_id: baselineHealth.baseline?.id || null,
|
|
745
|
+
change_id: row.id, subject: `Establish executable readiness for ${row.title}.`,
|
|
746
|
+
metadata: { change_kind: row.kind, base_commit: row.base_commit },
|
|
747
|
+
}, { rootDir });
|
|
409
748
|
const context = compileContext(db, { id: row.id }, { rootDir });
|
|
749
|
+
workflow = workflows.recordWorkflowStep(db, {
|
|
750
|
+
id: workflow.id, step_id: 'bind-baseline', status: 'completed',
|
|
751
|
+
evidence: [{
|
|
752
|
+
kind: 'baseline', ref: baselineHealth.baseline?.id || 'incident-break-glass',
|
|
753
|
+
summary: baselineHealth.status === 'pass'
|
|
754
|
+
? `Bound ready baseline at ${baselineHealth.baseline.repository_revision}.`
|
|
755
|
+
: 'Bound the approved incident break-glass authority.',
|
|
756
|
+
}],
|
|
757
|
+
}, { rootDir });
|
|
758
|
+
workflow = workflows.recordWorkflowStep(db, {
|
|
759
|
+
id: workflow.id, step_id: 'classify-change', status: 'completed',
|
|
760
|
+
decisions: [{
|
|
761
|
+
kind: 'change_kind', value: row.kind,
|
|
762
|
+
rationale: row.classification.rationale, risk_flags: row.classification.risk_flags,
|
|
763
|
+
}],
|
|
764
|
+
}, { rootDir });
|
|
765
|
+
workflow = workflows.recordWorkflowStep(db, {
|
|
766
|
+
id: workflow.id, step_id: 'record-intent', status: 'completed',
|
|
767
|
+
evidence: [{ kind: 'intent', ref: path.relative(rootDir, intentPath), summary: row.intent }],
|
|
768
|
+
outputs: [{ path: path.relative(rootDir, intentPath), kind: 'change-intent' }],
|
|
769
|
+
decisions: [{
|
|
770
|
+
kind: 'change_contract', acceptance_ids: row.contract.acceptance.map((item) => item.id),
|
|
771
|
+
research_disposition: row.research_disposition,
|
|
772
|
+
}],
|
|
773
|
+
}, { rootDir });
|
|
410
774
|
return {
|
|
411
775
|
change: readChange(db, row.id), intent_path: intentPath,
|
|
412
776
|
context_manifest_path: context.context_manifest_path,
|
|
777
|
+
workflow,
|
|
413
778
|
};
|
|
414
779
|
});
|
|
415
780
|
} catch (error) {
|
|
@@ -428,15 +793,14 @@ function assertTaskCreationAllowed(db, input = {}, { rootDir = process.cwd() } =
|
|
|
428
793
|
}
|
|
429
794
|
const baselineHealth = baselines.inspectBaseline(db, { rootDir });
|
|
430
795
|
if (baselineHealth.status === 'pass') return;
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
}
|
|
796
|
+
const gate = change
|
|
797
|
+
? contextSpine.baselineGateForChange(db, change, baselineHealth)
|
|
798
|
+
: { blockers: baselineHealth.blockers };
|
|
799
|
+
if (gate.blockers.length === 0) return;
|
|
436
800
|
throw new ChangeWorkflowError(
|
|
437
801
|
'BASELINE_NOT_READY',
|
|
438
|
-
`
|
|
439
|
-
{ blockers:
|
|
802
|
+
`task creation requires healthy baseline authority or an approved incident break-glass record: ${gate.blockers.join(', ')}`,
|
|
803
|
+
{ blockers: gate.blockers, change_id: input.change_id || null },
|
|
440
804
|
);
|
|
441
805
|
}
|
|
442
806
|
|
|
@@ -451,7 +815,9 @@ function updateChange(db, id, patch = {}, { rootDir = process.cwd() } = {}) {
|
|
|
451
815
|
throw new ChangeWorkflowError('VALIDATION_ERROR', `change field ${field} is not patchable`);
|
|
452
816
|
}
|
|
453
817
|
}
|
|
454
|
-
const syncIntent = [
|
|
818
|
+
const syncIntent = [
|
|
819
|
+
'title', 'intent', 'docs_impact', 'contract', 'classification', 'research_disposition',
|
|
820
|
+
].some((field) => patch[field] !== undefined);
|
|
455
821
|
const intentPath = path.resolve(rootDir, current.artifact_root, 'intent.md');
|
|
456
822
|
const previousIntent = syncIntent && fs.existsSync(intentPath)
|
|
457
823
|
? fs.readFileSync(intentPath)
|
|
@@ -476,6 +842,17 @@ function updateChange(db, id, patch = {}, { rootDir = process.cwd() } = {}) {
|
|
|
476
842
|
if (patch.provider_refs !== undefined) {
|
|
477
843
|
sets.push('provider_refs_json = ?'); values.push(JSON.stringify(normalizeProviderRefs(patch.provider_refs)));
|
|
478
844
|
}
|
|
845
|
+
if (patch.contract !== undefined) {
|
|
846
|
+
sets.push('contract_json = ?'); values.push(JSON.stringify(normalizeChangeContract(patch.contract)));
|
|
847
|
+
}
|
|
848
|
+
if (patch.classification !== undefined) {
|
|
849
|
+
sets.push('classification_json = ?');
|
|
850
|
+
values.push(JSON.stringify(normalizeClassification(patch.classification, current.kind)));
|
|
851
|
+
}
|
|
852
|
+
if (patch.research_disposition !== undefined) {
|
|
853
|
+
sets.push('research_disposition_json = ?');
|
|
854
|
+
values.push(JSON.stringify(normalizeResearchDisposition(patch.research_disposition, current.kind)));
|
|
855
|
+
}
|
|
479
856
|
if (patch.status !== undefined) {
|
|
480
857
|
const allowed = CHANGE_TRANSITIONS[current.status] || new Set();
|
|
481
858
|
if (!allowed.has(patch.status)) {
|
|
@@ -522,65 +899,87 @@ function filesUnder(dir) {
|
|
|
522
899
|
return out;
|
|
523
900
|
}
|
|
524
901
|
|
|
525
|
-
function
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
const byCategory = new Map(rows.map((row) => [row.category, row]));
|
|
529
|
-
for (const category of REQUIRED_EVIDENCE[change.kind].filter((value) => value !== 'review')) {
|
|
530
|
-
const row = byCategory.get(category);
|
|
531
|
-
if (!row) blockers.push(`EVIDENCE_MISSING:${category}`);
|
|
532
|
-
else if (!['pass', 'not_applicable'].includes(row.status)) blockers.push(`EVIDENCE_FAILED:${category}`);
|
|
533
|
-
else if (!row.evidence || String(row.evidence).trim().length < 3) blockers.push(`EVIDENCE_EMPTY:${category}`);
|
|
534
|
-
}
|
|
535
|
-
if (['standard', 'major'].includes(change.kind)) {
|
|
536
|
-
const reviews = rows.filter((row) => row.category === 'review');
|
|
537
|
-
for (const axis of ['spec_fidelity', 'engineering_standards']) {
|
|
538
|
-
const row = reviews.find((candidate) => candidate.axis === axis);
|
|
539
|
-
if (!row) blockers.push(`REVIEW_AXIS_MISSING:${axis}`);
|
|
540
|
-
else if (row.status !== 'pass') blockers.push(`REVIEW_AXIS_FAILED:${axis}`);
|
|
541
|
-
else if (!row.evidence || String(row.evidence).trim().length < 3) {
|
|
542
|
-
blockers.push(`REVIEW_AXIS_EMPTY:${axis}`);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
const tests = byCategory.get('tests');
|
|
547
|
-
if (tests && ['standard', 'major', 'incident'].includes(change.kind)
|
|
548
|
-
&& (!tests.seam || String(tests.seam).trim().length < 3)) {
|
|
549
|
-
blockers.push('TEST_SEAM_MISSING');
|
|
550
|
-
}
|
|
551
|
-
if (tests && (change.kind === 'incident' || tasks.some((task) => task.type === 'bugfix'))) {
|
|
552
|
-
const signal = tests.signal;
|
|
553
|
-
if (!signal) blockers.push('TEST_RED_SIGNAL_MISSING');
|
|
554
|
-
else {
|
|
555
|
-
if (!signal.command || String(signal.command).trim().length < 3) blockers.push('TEST_SIGNAL_COMMAND_MISSING');
|
|
556
|
-
if (!signal.expected_red || String(signal.expected_red).trim().length < 3) blockers.push('TEST_EXPECTED_RED_MISSING');
|
|
557
|
-
if (signal.observed_red !== true) blockers.push('TEST_RED_NOT_OBSERVED');
|
|
558
|
-
if (signal.observed_green !== true) blockers.push('TEST_GREEN_NOT_OBSERVED');
|
|
559
|
-
if (signal.deterministic !== true) blockers.push('TEST_SIGNAL_NOT_DETERMINISTIC');
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
return blockers;
|
|
902
|
+
function baselineConvergenceBlockers(db, change, rootDir) {
|
|
903
|
+
const baselineHealth = baselines.inspectBaseline(db, { rootDir });
|
|
904
|
+
return contextSpine.baselineGateForChange(db, change, baselineHealth).blockers;
|
|
563
905
|
}
|
|
564
906
|
|
|
565
|
-
function
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
907
|
+
function deriveConvergenceEvidence(db, change, tasks, workflowGate, diagnosis, rootDir) {
|
|
908
|
+
const devRuns = db.prepare(
|
|
909
|
+
`SELECT id, task_id, summary_json FROM workflow_runs
|
|
910
|
+
WHERE kind = 'dev' AND change_id = ? AND status = 'completed'
|
|
911
|
+
ORDER BY completed_at ASC, rowid ASC`,
|
|
912
|
+
).all(change.id).map((row) => ({
|
|
913
|
+
id: row.id, task_id: row.task_id, summary: parseJson(row.summary_json, 'workflow_runs.summary_json'),
|
|
914
|
+
}));
|
|
915
|
+
const deltaPaths = filesUnder(path.join(rootDir, change.artifact_root, 'delta'))
|
|
916
|
+
.map((file) => path.relative(rootDir, file));
|
|
917
|
+
const appliedLearning = specLearning.listSpecLearning(db, change.id)
|
|
918
|
+
.filter((candidate) => candidate.status === 'applied')
|
|
919
|
+
.map((candidate) => candidate.target_ref);
|
|
920
|
+
const rows = [
|
|
921
|
+
{
|
|
922
|
+
category: 'diff', status: 'pass',
|
|
923
|
+
evidence: `Completed dev workflows: ${devRuns.map((run) => `${run.task_id}:${run.id}`).join(', ')}`,
|
|
924
|
+
refs: devRuns.map((run) => run.id),
|
|
925
|
+
},
|
|
926
|
+
{
|
|
927
|
+
category: 'tests', status: 'pass',
|
|
928
|
+
evidence: `DB-derived passing test workflow ${workflowGate.test_workflow_id}.`,
|
|
929
|
+
refs: [workflowGate.test_summary.report_path, workflowGate.test_summary.report_digest].filter(Boolean),
|
|
930
|
+
regression_signal: workflowGate.test_summary.regression_signal || null,
|
|
931
|
+
},
|
|
932
|
+
{
|
|
933
|
+
category: 'spec', status: 'pass',
|
|
934
|
+
evidence: deltaPaths.length > 0
|
|
935
|
+
? `Current delta artifacts: ${deltaPaths.join(', ')}`
|
|
936
|
+
: `Change intent and resolved specification learning for ${change.id}.`,
|
|
937
|
+
refs: [...deltaPaths, ...appliedLearning],
|
|
938
|
+
},
|
|
939
|
+
{
|
|
940
|
+
category: 'docs', status: 'pass',
|
|
941
|
+
evidence: change.docs_impact.status === 'required'
|
|
942
|
+
? `Declared documentation files: ${change.docs_impact.files.join(', ')}`
|
|
943
|
+
: `No documentation change: ${change.docs_impact.rationale}`,
|
|
944
|
+
refs: change.docs_impact.files,
|
|
945
|
+
},
|
|
946
|
+
...['spec_fidelity', 'engineering_standards'].map((axis) => ({
|
|
947
|
+
category: 'review', axis, status: 'pass',
|
|
948
|
+
evidence: `DB-derived ${axis} verdict from ${workflowGate.review_workflow_id}.`,
|
|
949
|
+
refs: workflowGate.review_summary.axes?.[axis]?.evidence_refs || [],
|
|
950
|
+
})),
|
|
951
|
+
];
|
|
952
|
+
if (diagnosis.artifact) {
|
|
953
|
+
rows.unshift({
|
|
954
|
+
category: 'diagnosis', status: 'pass',
|
|
955
|
+
evidence: `Structured incident diagnosis ${diagnosis.artifact.artifactPath}.`,
|
|
956
|
+
refs: [diagnosis.artifact.artifactPath, diagnosis.artifact.contentHash],
|
|
957
|
+
});
|
|
570
958
|
}
|
|
571
|
-
|
|
572
|
-
if (baseline.mode === 'migrated') return ['BASELINE_MIGRATION_REVIEW_REQUIRED'];
|
|
573
|
-
return [`BASELINE_NOT_READY:${baseline.status}`];
|
|
959
|
+
return rows;
|
|
574
960
|
}
|
|
575
961
|
|
|
576
962
|
function convergeChange(db, input, { rootDir = process.cwd() } = {}) {
|
|
963
|
+
if (Object.hasOwn(input || {}, 'evidence')) {
|
|
964
|
+
throw new ChangeWorkflowError(
|
|
965
|
+
'CONVERGENCE_EVIDENCE_AUTHORITY_VIOLATION',
|
|
966
|
+
'change convergence evidence is derived from DB-backed dev, test, review, specification, and documentation authority',
|
|
967
|
+
);
|
|
968
|
+
}
|
|
577
969
|
const change = readChange(db, input.id);
|
|
578
970
|
if (!change) throw new ChangeWorkflowError('CHANGE_NOT_FOUND', `change ${input.id} not found`);
|
|
579
971
|
if (!['active', 'blocked'].includes(change.status)) {
|
|
580
972
|
throw new ChangeWorkflowError('CHANGE_NOT_CONVERGEABLE', `change ${input.id} is ${change.status}`);
|
|
581
973
|
}
|
|
582
|
-
const tasks = db.prepare('SELECT id
|
|
583
|
-
|
|
974
|
+
const tasks = db.prepare('SELECT id FROM tasks WHERE change_id = ? ORDER BY id')
|
|
975
|
+
.all(change.id).map((task) => ops.readTask(db, task.id));
|
|
976
|
+
const blockers = new Set();
|
|
977
|
+
let workflowGate = null;
|
|
978
|
+
try {
|
|
979
|
+
workflowGate = workflows.validateDeliveryPrerequisites(db, { change_id: change.id }, rootDir);
|
|
980
|
+
} catch (error) {
|
|
981
|
+
blockers.add(error.code || 'WORKFLOW_GATE_UNAVAILABLE');
|
|
982
|
+
}
|
|
584
983
|
for (const blocker of baselineConvergenceBlockers(db, change, rootDir)) blockers.add(blocker);
|
|
585
984
|
if (tasks.length === 0) blockers.add('NO_TASKS');
|
|
586
985
|
if (tasks.some((task) => !['completed', 'expanded'].includes(task.status))) blockers.add('TASKS_INCOMPLETE');
|
|
@@ -624,11 +1023,24 @@ function convergeChange(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
624
1023
|
if (filesUnder(path.join(artifactDir, 'delta')).length === 0) blockers.add('SPEC_DELTA_MISSING');
|
|
625
1024
|
if (!fs.existsSync(path.join(artifactDir, 'plan.md'))) blockers.add('CHANGE_PLAN_MISSING');
|
|
626
1025
|
}
|
|
627
|
-
const
|
|
628
|
-
|
|
1026
|
+
const latestContext = db.prepare(
|
|
1027
|
+
`SELECT manifest_path, manifest_hash FROM context_snapshots
|
|
1028
|
+
WHERE change_id = ? ORDER BY created_at DESC, rowid DESC LIMIT 1`,
|
|
1029
|
+
).get(change.id);
|
|
1030
|
+
const manifestPath = latestContext?.manifest_path
|
|
1031
|
+
? safeRelativePath(rootDir, latestContext.manifest_path)
|
|
1032
|
+
: null;
|
|
1033
|
+
if (!manifestPath || !fs.existsSync(manifestPath)) blockers.add('CONTEXT_MANIFEST_MISSING');
|
|
629
1034
|
else {
|
|
630
1035
|
const manifest = parseJson(fs.readFileSync(manifestPath, 'utf8'), 'context-manifest.json');
|
|
1036
|
+
const manifestHash = crypto.createHash('sha256').update(fs.readFileSync(manifestPath)).digest('hex');
|
|
1037
|
+
if (latestContext.manifest_hash !== manifestHash) blockers.add('CONTEXT_MANIFEST_STALE');
|
|
631
1038
|
if (manifest.git && manifest.git.head !== gitHead(rootDir)) blockers.add('CONTEXT_HEAD_STALE');
|
|
1039
|
+
const checkout = baselines.gitWorktreeSnapshot(rootDir, ['.']);
|
|
1040
|
+
if (manifest.git?.worktree_digest && checkout.digest
|
|
1041
|
+
&& manifest.git.worktree_digest !== checkout.digest) {
|
|
1042
|
+
blockers.add('CONTEXT_WORKTREE_STALE');
|
|
1043
|
+
}
|
|
632
1044
|
const manifestTasks = (manifest.tasks || []).map((task) => task.id).sort();
|
|
633
1045
|
const currentTasks = tasks.map((task) => task.id).sort();
|
|
634
1046
|
if (JSON.stringify(manifestTasks) !== JSON.stringify(currentTasks)) blockers.add('CONTEXT_TASK_SET_STALE');
|
|
@@ -652,10 +1064,13 @@ function convergeChange(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
652
1064
|
return { ready: false, status: 'blocked', blockers: blockerList };
|
|
653
1065
|
}
|
|
654
1066
|
|
|
1067
|
+
const derivedEvidence = deriveConvergenceEvidence(
|
|
1068
|
+
db, change, tasks, workflowGate, diagnosis, rootDir,
|
|
1069
|
+
);
|
|
655
1070
|
const verificationPath = path.join(artifactDir, 'verification.md');
|
|
656
1071
|
const lines = [
|
|
657
1072
|
`# Verification: ${change.title}`, '',
|
|
658
|
-
...
|
|
1073
|
+
...derivedEvidence.flatMap((row) => [
|
|
659
1074
|
`## ${row.category}${row.axis ? `:${row.axis}` : ''}`, '',
|
|
660
1075
|
`Status: **${row.status}**`, '', String(row.evidence).trim(), '',
|
|
661
1076
|
]),
|
|
@@ -668,11 +1083,11 @@ function convergeChange(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
668
1083
|
upsertArtifact(db, {
|
|
669
1084
|
change_id: change.id, kind: 'verification', artifactPath: path.relative(rootDir, verificationPath),
|
|
670
1085
|
contentHash: crypto.createHash('sha256').update(fs.readFileSync(verificationPath)).digest('hex'),
|
|
671
|
-
metadata: { evidence:
|
|
1086
|
+
metadata: { evidence: derivedEvidence },
|
|
672
1087
|
});
|
|
673
1088
|
ops.appendEventInTx(db, {
|
|
674
1089
|
type: 'change_converged', change_id: change.id,
|
|
675
|
-
payload: { evidence_categories:
|
|
1090
|
+
payload: { evidence_categories: derivedEvidence.map((row) => row.category) },
|
|
676
1091
|
});
|
|
677
1092
|
});
|
|
678
1093
|
return { ready: true, status: 'ready', blockers: [], verification_path: verificationPath };
|
|
@@ -711,6 +1126,36 @@ function finalizeArchive(db, intent, { rootDir }) {
|
|
|
711
1126
|
db.prepare('UPDATE artifacts SET path = ?, updated_at = ? WHERE id = ?')
|
|
712
1127
|
.run(archivedPath, nowIso(), artifact.id);
|
|
713
1128
|
}
|
|
1129
|
+
const workflowOutputs = db.prepare(
|
|
1130
|
+
`SELECT ws.run_id, ws.step_id, ws.outputs_json
|
|
1131
|
+
FROM workflow_steps ws JOIN workflow_runs wr ON wr.id = ws.run_id
|
|
1132
|
+
WHERE wr.change_id = ?`,
|
|
1133
|
+
).all(change.id);
|
|
1134
|
+
for (const step of workflowOutputs) {
|
|
1135
|
+
const outputs = parseJson(step.outputs_json, 'workflow_steps.outputs_json');
|
|
1136
|
+
let changed = false;
|
|
1137
|
+
const moved = outputs.map((output) => {
|
|
1138
|
+
if (output.path !== intent.source && !output.path.startsWith(prefix)) return output;
|
|
1139
|
+
changed = true;
|
|
1140
|
+
const suffix = path.relative(intent.source, output.path);
|
|
1141
|
+
return { ...output, path: suffix ? path.join(relative, suffix) : relative };
|
|
1142
|
+
});
|
|
1143
|
+
if (changed) {
|
|
1144
|
+
db.prepare(
|
|
1145
|
+
'UPDATE workflow_steps SET outputs_json = ?, updated_at = ? WHERE run_id = ? AND step_id = ?',
|
|
1146
|
+
).run(JSON.stringify(moved), nowIso(), step.run_id, step.step_id);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
const contextRows = db.prepare(
|
|
1150
|
+
'SELECT id, manifest_path FROM context_snapshots WHERE change_id = ?',
|
|
1151
|
+
).all(change.id);
|
|
1152
|
+
for (const snapshot of contextRows) {
|
|
1153
|
+
if (snapshot.manifest_path !== intent.source && !snapshot.manifest_path.startsWith(prefix)) continue;
|
|
1154
|
+
const suffix = path.relative(intent.source, snapshot.manifest_path);
|
|
1155
|
+
const archivedPath = suffix ? path.join(relative, suffix) : relative;
|
|
1156
|
+
db.prepare('UPDATE context_snapshots SET manifest_path = ? WHERE id = ?')
|
|
1157
|
+
.run(archivedPath, snapshot.id);
|
|
1158
|
+
}
|
|
714
1159
|
const archivedSummary = path.join(destination, 'archive-summary.md');
|
|
715
1160
|
upsertArtifact(db, {
|
|
716
1161
|
change_id: change.id, kind: 'archive_summary',
|
|
@@ -721,6 +1166,19 @@ function finalizeArchive(db, intent, { rootDir }) {
|
|
|
721
1166
|
no_baseline_change_reason: intent.no_baseline_change_reason,
|
|
722
1167
|
},
|
|
723
1168
|
});
|
|
1169
|
+
const reconciliationSuffix = path.relative(intent.source, intent.reconciliation_path);
|
|
1170
|
+
const archivedReconciliationPath = path.join(relative, reconciliationSuffix);
|
|
1171
|
+
upsertArtifact(db, {
|
|
1172
|
+
change_id: change.id, kind: 'baseline_reconciliation',
|
|
1173
|
+
artifactPath: archivedReconciliationPath,
|
|
1174
|
+
contentHash: intent.reconciliation_digest,
|
|
1175
|
+
metadata: {
|
|
1176
|
+
schema: intent.reconciliation_manifest.$schema,
|
|
1177
|
+
semantic_changes: intent.reconciliation_manifest.semantic_changes.map((item) => item.id),
|
|
1178
|
+
resolved_gap_ids: intent.reconciliation_manifest.resolved_gap_ids,
|
|
1179
|
+
resolved_unknowns: intent.reconciliation_manifest.resolved_unknowns,
|
|
1180
|
+
},
|
|
1181
|
+
});
|
|
724
1182
|
db.prepare("UPDATE artifacts SET status = 'archived', updated_at = ? WHERE change_id = ?")
|
|
725
1183
|
.run(nowIso(), change.id);
|
|
726
1184
|
if (breakGlass) {
|
|
@@ -742,6 +1200,7 @@ function finalizeArchive(db, intent, { rootDir }) {
|
|
|
742
1200
|
} else {
|
|
743
1201
|
baselines.reconcileBaseline(db, {
|
|
744
1202
|
baseline_updates: intent.baseline_updates, change_id: change.id,
|
|
1203
|
+
reconciliation: intent.reconciliation_manifest,
|
|
745
1204
|
}, { rootDir });
|
|
746
1205
|
const baselineHealth = baselines.inspectBaseline(db, { rootDir });
|
|
747
1206
|
if (baselineHealth.blockers.length > 0) {
|
|
@@ -757,6 +1216,8 @@ function finalizeArchive(db, intent, { rootDir }) {
|
|
|
757
1216
|
payload: {
|
|
758
1217
|
archive_path: relative, baseline_updates: intent.baseline_updates,
|
|
759
1218
|
no_baseline_change_reason: intent.no_baseline_change_reason,
|
|
1219
|
+
reconciliation_path: archivedReconciliationPath,
|
|
1220
|
+
reconciliation_digest: intent.reconciliation_digest,
|
|
760
1221
|
baseline_bypass: breakGlass,
|
|
761
1222
|
},
|
|
762
1223
|
});
|
|
@@ -786,9 +1247,13 @@ function archiveChange(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
786
1247
|
if (!['ready', 'archived'].includes(change.status)) {
|
|
787
1248
|
throw new ChangeWorkflowError('CHANGE_NOT_READY', `change ${input.id} must converge before archive`);
|
|
788
1249
|
}
|
|
1250
|
+
const reconciliation = readReconciliationManifest(db, change, input, rootDir);
|
|
789
1251
|
const prepared = archiveJournal.prepareArchiveMove({
|
|
790
1252
|
rootDir, change, summary: String(input.summary), baselineUpdates: updates,
|
|
791
1253
|
noBaselineChangeReason: noChangeReason || null,
|
|
1254
|
+
reconciliationPath: reconciliation.relative,
|
|
1255
|
+
reconciliationDigest: reconciliation.digest,
|
|
1256
|
+
reconciliationManifest: reconciliation.manifest,
|
|
792
1257
|
});
|
|
793
1258
|
let result;
|
|
794
1259
|
try {
|