ultra-builder-pro-cli 0.14.0 → 0.16.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 +77 -1
- package/README.md +46 -24
- package/adapters/_shared/codex-assets.cjs +6 -2
- package/adapters/_shared/file-ops.cjs +6 -2
- package/adapters/_shared/handbook.cjs +2 -1
- package/adapters/claude.js +3 -2
- package/adapters/codex.js +1 -0
- package/adapters/kimi.js +3 -2
- package/adapters/opencode.js +3 -2
- package/agents/review-coordinator.md +9 -6
- package/commands/ultra-change.md +1 -1
- package/commands/ultra-plan.md +1 -1
- package/commands/ultra-research.md +1 -1
- package/commands/ultra-think.md +2 -2
- package/docs/AGENT-CONTEXT.md +43 -19
- package/docs/ARCHITECTURE.md +54 -13
- package/docs/PLAN.zh-CN.md +4 -2
- package/docs/ROADMAP.md +10 -4
- package/docs/RUNTIME-COMPAT-MATRIX.md +4 -4
- package/docs/STATE-DB-ACCESS-POLICY.md +33 -29
- package/docs/USER-HANDBOOK-CONTRACT.md +8 -2
- package/docs/WORKFLOW-LIFECYCLE.md +342 -0
- package/hooks/README.md +6 -1
- package/hooks/health_check.py +12 -3
- package/hooks/subagent_tracker.py +52 -77
- 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 +598 -85
- package/mcp-server/lib/context-spine.cjs +305 -25
- package/mcp-server/lib/decision-dialogue.cjs +713 -0
- package/mcp-server/lib/doctor.cjs +42 -3
- package/mcp-server/lib/init-project.cjs +210 -16
- package/mcp-server/lib/plan-store.cjs +4 -1
- package/mcp-server/lib/pricing.cjs +7 -10
- package/mcp-server/lib/project-breadcrumb.cjs +15 -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 +168 -8
- package/mcp-server/lib/state-ops.cjs +160 -9
- package/mcp-server/lib/workflow-state.cjs +2305 -0
- package/mcp-server/server.cjs +215 -15
- package/orchestrator/daemon.cjs +1 -2
- package/orchestrator/dispatch-rules.cjs +5 -31
- package/orchestrator/planner/plan-builder.cjs +21 -12
- package/package.json +13 -3
- package/skills/ultra-change/SKILL.md +94 -80
- package/skills/ultra-change/references/change-contract.md +36 -0
- package/skills/ultra-deliver/SKILL.md +76 -79
- package/skills/ultra-deliver/references/baseline-reconciliation.md +20 -0
- package/skills/ultra-dev/SKILL.md +62 -59
- package/skills/ultra-doctor/SKILL.md +61 -82
- package/skills/ultra-init/SKILL.md +101 -103
- package/skills/ultra-init/references/brownfield-adoption.md +2 -2
- package/skills/ultra-plan/SKILL.md +99 -85
- package/skills/ultra-plan/references/semantic-preflight.md +18 -0
- package/skills/ultra-research/SKILL.md +102 -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 +63 -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 -41
- package/skills/ultra-test/SKILL.md +54 -55
- package/skills/ultra-think/SKILL.md +26 -12
- package/skills/ultra-think/references/decision-dialogue.md +130 -0
- package/skills/ultra-verify/SKILL.md +1 -1
- package/skills/ultra-verify/references/{confidence-system.md → evidence-status.md} +3 -3
- package/spec/cli-protocol.md +17 -3
- package/spec/mcp-tools.yaml +683 -64
- 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 +140 -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 +228 -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 -149
- 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 -138
- package/adapters/tests/codex-hook.test.cjs +0 -142
- package/adapters/tests/codex.test.cjs +0 -430
- package/adapters/tests/kimi-hook.test.cjs +0 -149
- package/adapters/tests/kimi.test.cjs +0 -259
- package/adapters/tests/opencode.test.cjs +0 -357
- 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/plan-store.test.cjs +0 -132
- package/mcp-server/lib/prd-parser.test.cjs +0 -82
- package/mcp-server/lib/project-breadcrumb.test.cjs +0 -112
- 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 -169
- 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 -1467
- 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/plan-builder.test.cjs +0 -165
- 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,18 @@ 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 decisions = require('./decision-dialogue.cjs');
|
|
17
|
+
const reconciliationSchema = require('../../spec/schemas/baseline-reconciliation.v1.schema.json');
|
|
18
|
+
|
|
19
|
+
const reconciliationAjv = new Ajv({ allErrors: true, strict: false });
|
|
20
|
+
const validateReconciliationSchema = reconciliationAjv.compile(reconciliationSchema);
|
|
14
21
|
|
|
15
22
|
const CHANGE_ID = /^[a-zA-Z0-9_-]+$/;
|
|
16
|
-
const CHANGE_PATCH_FIELDS = new Set([
|
|
23
|
+
const CHANGE_PATCH_FIELDS = new Set([
|
|
24
|
+
'title', 'intent', 'status', 'docs_impact', 'provider_refs',
|
|
25
|
+
'contract', 'classification', 'research_disposition',
|
|
26
|
+
]);
|
|
17
27
|
const CHANGE_TRANSITIONS = Object.freeze({
|
|
18
28
|
active: new Set(['active', 'blocked', 'cancelled']),
|
|
19
29
|
blocked: new Set(['active', 'blocked', 'cancelled']),
|
|
@@ -21,12 +31,14 @@ const CHANGE_TRANSITIONS = Object.freeze({
|
|
|
21
31
|
archived: new Set(),
|
|
22
32
|
cancelled: new Set(),
|
|
23
33
|
});
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
const CHANGE_KINDS = new Set(['quick', 'standard', 'major', 'incident']);
|
|
35
|
+
const CHANGE_RISK_FLAGS = new Set([
|
|
36
|
+
'public_contract', 'schema_migration', 'data_migration', 'authorization', 'security',
|
|
37
|
+
'multi_module', 'external_integration', 'release_semantics',
|
|
38
|
+
]);
|
|
39
|
+
const QUICK_EXCLUSION_FLAGS = new Set(CHANGE_RISK_FLAGS);
|
|
40
|
+
const RESEARCH_DISPOSITIONS = new Set(['none', 'bounded', 'required']);
|
|
41
|
+
const CHANGE_RESEARCH_MODES = new Set(['product', 'feature', 'architecture', 'custom']);
|
|
30
42
|
const INCIDENT_DIAGNOSIS_SECTIONS = Object.freeze([
|
|
31
43
|
{ heading: 'Reproduction', key: 'reproduction' },
|
|
32
44
|
{ heading: 'Hypotheses', key: 'hypotheses' },
|
|
@@ -63,10 +75,18 @@ function rowToChange(row) {
|
|
|
63
75
|
provider_refs: parseJson(row.provider_refs_json, 'provider_refs_json'),
|
|
64
76
|
baseline_bypass: row.baseline_bypass_json
|
|
65
77
|
? parseJson(row.baseline_bypass_json, 'baseline_bypass_json') : null,
|
|
78
|
+
contract: parseJson(row.contract_json || '{}', 'contract_json'),
|
|
79
|
+
classification: parseJson(row.classification_json || '{}', 'classification_json'),
|
|
80
|
+
research_disposition: parseJson(
|
|
81
|
+
row.research_disposition_json || '{}', 'research_disposition_json',
|
|
82
|
+
),
|
|
66
83
|
};
|
|
67
84
|
delete change.docs_impact_json;
|
|
68
85
|
delete change.provider_refs_json;
|
|
69
86
|
delete change.baseline_bypass_json;
|
|
87
|
+
delete change.contract_json;
|
|
88
|
+
delete change.classification_json;
|
|
89
|
+
delete change.research_disposition_json;
|
|
70
90
|
return change;
|
|
71
91
|
}
|
|
72
92
|
|
|
@@ -88,6 +108,140 @@ function normalizeProviderRefs(value) {
|
|
|
88
108
|
return providerRefs.normalizeProviderRefs(value, ChangeWorkflowError);
|
|
89
109
|
}
|
|
90
110
|
|
|
111
|
+
function requiredText(value, field) {
|
|
112
|
+
const text = typeof value === 'string' ? value.trim() : '';
|
|
113
|
+
if (!text) throw new ChangeWorkflowError('CHANGE_CONTRACT_REQUIRED', `${field} is required`);
|
|
114
|
+
return text;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function stringList(value, field, { allowEmpty = false } = {}) {
|
|
118
|
+
if (!Array.isArray(value) || (!allowEmpty && value.length === 0)
|
|
119
|
+
|| value.some((item) => typeof item !== 'string' || !item.trim())) {
|
|
120
|
+
throw new ChangeWorkflowError(
|
|
121
|
+
'CHANGE_CONTRACT_REQUIRED', `${field} must be ${allowEmpty ? 'an' : 'a non-empty'} array of strings`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
return [...new Set(value.map((item) => item.trim()))];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function normalizeChangeContract(value) {
|
|
128
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
129
|
+
throw new ChangeWorkflowError('CHANGE_CONTRACT_REQUIRED', 'contract is required');
|
|
130
|
+
}
|
|
131
|
+
if (!Array.isArray(value.acceptance) || value.acceptance.length === 0) {
|
|
132
|
+
throw new ChangeWorkflowError('CHANGE_CONTRACT_REQUIRED', 'contract.acceptance is required');
|
|
133
|
+
}
|
|
134
|
+
const ids = new Set();
|
|
135
|
+
const acceptance = value.acceptance.map((item, index) => {
|
|
136
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
137
|
+
throw new ChangeWorkflowError('CHANGE_CONTRACT_REQUIRED', `contract.acceptance[${index}] is invalid`);
|
|
138
|
+
}
|
|
139
|
+
const id = requiredText(item.id, `contract.acceptance[${index}].id`);
|
|
140
|
+
if (!CHANGE_ID.test(id) || ids.has(id)) {
|
|
141
|
+
throw new ChangeWorkflowError('CHANGE_CONTRACT_REQUIRED', `acceptance id is invalid or duplicated: ${id}`);
|
|
142
|
+
}
|
|
143
|
+
ids.add(id);
|
|
144
|
+
return {
|
|
145
|
+
id,
|
|
146
|
+
criterion: requiredText(item.criterion, `contract.acceptance[${index}].criterion`),
|
|
147
|
+
verification: requiredText(item.verification, `contract.acceptance[${index}].verification`),
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
const recovery = value.recovery;
|
|
151
|
+
if (!recovery || typeof recovery !== 'object' || Array.isArray(recovery)) {
|
|
152
|
+
throw new ChangeWorkflowError('CHANGE_CONTRACT_REQUIRED', 'contract.recovery is required');
|
|
153
|
+
}
|
|
154
|
+
if (!Array.isArray(value.unresolved_decisions)) {
|
|
155
|
+
throw new ChangeWorkflowError(
|
|
156
|
+
'CHANGE_CONTRACT_REQUIRED', 'contract.unresolved_decisions must be an explicit array',
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
const decisions = value.unresolved_decisions.map((item, index) => {
|
|
160
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
161
|
+
throw new ChangeWorkflowError('CHANGE_CONTRACT_REQUIRED', `unresolved decision ${index} is invalid`);
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
id: requiredText(item.id, `contract.unresolved_decisions[${index}].id`),
|
|
165
|
+
summary: requiredText(item.summary, `contract.unresolved_decisions[${index}].summary`),
|
|
166
|
+
blocking: item.blocking === true,
|
|
167
|
+
owner: item.owner == null ? null : requiredText(item.owner, `contract.unresolved_decisions[${index}].owner`),
|
|
168
|
+
};
|
|
169
|
+
});
|
|
170
|
+
return {
|
|
171
|
+
outcome: requiredText(value.outcome, 'contract.outcome'),
|
|
172
|
+
acceptance,
|
|
173
|
+
non_goals: stringList(value.non_goals, 'contract.non_goals'),
|
|
174
|
+
public_seams: stringList(value.public_seams, 'contract.public_seams'),
|
|
175
|
+
recovery: {
|
|
176
|
+
strategy: requiredText(recovery.strategy, 'contract.recovery.strategy'),
|
|
177
|
+
verification: requiredText(recovery.verification, 'contract.recovery.verification'),
|
|
178
|
+
},
|
|
179
|
+
unresolved_decisions: decisions,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function normalizeClassification(value, kind) {
|
|
184
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
185
|
+
throw new ChangeWorkflowError('CHANGE_CLASSIFICATION_REQUIRED', 'classification is required');
|
|
186
|
+
}
|
|
187
|
+
const flags = stringList(value.risk_flags, 'classification.risk_flags', { allowEmpty: true });
|
|
188
|
+
const invalid = flags.filter((flag) => !CHANGE_RISK_FLAGS.has(flag));
|
|
189
|
+
if (invalid.length > 0) {
|
|
190
|
+
throw new ChangeWorkflowError('CHANGE_CLASSIFICATION_REQUIRED', `unknown risk flags: ${invalid.join(', ')}`);
|
|
191
|
+
}
|
|
192
|
+
if (kind === 'quick') {
|
|
193
|
+
const excluded = flags.filter((flag) => QUICK_EXCLUSION_FLAGS.has(flag));
|
|
194
|
+
if (excluded.length > 0) {
|
|
195
|
+
throw new ChangeWorkflowError(
|
|
196
|
+
'CHANGE_PROFILE_ESCALATION_REQUIRED',
|
|
197
|
+
`quick changes cannot carry these risk flags: ${excluded.join(', ')}`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
rationale: requiredText(value.rationale, 'classification.rationale'),
|
|
203
|
+
risk_flags: flags,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function normalizeResearchDisposition(value, kind) {
|
|
208
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
209
|
+
throw new ChangeWorkflowError('CHANGE_RESEARCH_DISPOSITION_REQUIRED', 'research_disposition is required');
|
|
210
|
+
}
|
|
211
|
+
const status = String(value.status || '').trim();
|
|
212
|
+
if (!RESEARCH_DISPOSITIONS.has(status)) {
|
|
213
|
+
throw new ChangeWorkflowError('CHANGE_RESEARCH_DISPOSITION_REQUIRED', 'research disposition status is invalid');
|
|
214
|
+
}
|
|
215
|
+
const selectedSteps = stringList(
|
|
216
|
+
value.selected_steps || [], 'research_disposition.selected_steps', { allowEmpty: true },
|
|
217
|
+
);
|
|
218
|
+
const mode = value.mode == null ? null : String(value.mode).trim();
|
|
219
|
+
if (status === 'none' && (mode !== null || selectedSteps.length > 0)) {
|
|
220
|
+
throw new ChangeWorkflowError(
|
|
221
|
+
'CHANGE_RESEARCH_DISPOSITION_REQUIRED', 'research status none cannot select a mode or steps',
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
if (status !== 'none' && !CHANGE_RESEARCH_MODES.has(mode)) {
|
|
225
|
+
throw new ChangeWorkflowError(
|
|
226
|
+
'CHANGE_RESEARCH_DISPOSITION_REQUIRED', 'bounded or required research needs a supported mode',
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
if (mode === 'custom' && selectedSteps.length === 0) {
|
|
230
|
+
throw new ChangeWorkflowError(
|
|
231
|
+
'CHANGE_RESEARCH_DISPOSITION_REQUIRED', 'custom research needs selected_steps',
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
if (kind === 'quick' && status !== 'none') {
|
|
235
|
+
throw new ChangeWorkflowError(
|
|
236
|
+
'CHANGE_PROFILE_ESCALATION_REQUIRED', 'a change that requires research cannot use the quick profile',
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
status, mode, selected_steps: selectedSteps,
|
|
241
|
+
rationale: requiredText(value.rationale, 'research_disposition.rationale'),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
91
245
|
function normalizeBaselineBypass(value) {
|
|
92
246
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
93
247
|
throw new ChangeWorkflowError(
|
|
@@ -130,6 +284,138 @@ function safeRelativePath(rootDir, candidate) {
|
|
|
130
284
|
return target;
|
|
131
285
|
}
|
|
132
286
|
|
|
287
|
+
function readReconciliationManifest(db, change, input, rootDir) {
|
|
288
|
+
const relative = typeof input.reconciliation_path === 'string'
|
|
289
|
+
? input.reconciliation_path.trim() : '';
|
|
290
|
+
if (!relative) {
|
|
291
|
+
throw new ChangeWorkflowError(
|
|
292
|
+
'BASELINE_RECONCILIATION_MANIFEST_REQUIRED', 'reconciliation_path is required',
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
let file = safeRelativePath(rootDir, relative);
|
|
296
|
+
const artifactRoot = path.resolve(rootDir, change.artifact_root);
|
|
297
|
+
if (!file.startsWith(`${artifactRoot}${path.sep}`)) {
|
|
298
|
+
throw new ChangeWorkflowError(
|
|
299
|
+
'BASELINE_RECONCILIATION_MANIFEST_REQUIRED',
|
|
300
|
+
'reconciliation manifest must be a file inside the active change artifact root',
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
let manifest;
|
|
304
|
+
if (!fs.existsSync(file)) {
|
|
305
|
+
const pending = archiveJournal.listArchiveIntents(rootDir).find(
|
|
306
|
+
(item) => !item.error && item.intent.change_id === change.id
|
|
307
|
+
&& item.intent.reconciliation_path === relative,
|
|
308
|
+
);
|
|
309
|
+
if (pending) {
|
|
310
|
+
const suffix = path.relative(pending.intent.source, relative);
|
|
311
|
+
file = path.join(pending.destination, suffix);
|
|
312
|
+
manifest = pending.intent.reconciliation_manifest;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
|
|
316
|
+
throw new ChangeWorkflowError(
|
|
317
|
+
'BASELINE_RECONCILIATION_MANIFEST_REQUIRED', 'reconciliation manifest file is missing',
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
try { manifest ||= JSON.parse(fs.readFileSync(file, 'utf8')); }
|
|
321
|
+
catch (error) {
|
|
322
|
+
throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_INVALID', error.message);
|
|
323
|
+
}
|
|
324
|
+
if (!validateReconciliationSchema(manifest)) {
|
|
325
|
+
throw new ChangeWorkflowError(
|
|
326
|
+
'BASELINE_RECONCILIATION_MANIFEST_INVALID',
|
|
327
|
+
`reconciliation manifest does not satisfy its published schema: ${reconciliationAjv.errorsText(validateReconciliationSchema.errors)}`,
|
|
328
|
+
validateReconciliationSchema.errors,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
const baseline = baselines.readBaseline(db);
|
|
332
|
+
const updates = Array.isArray(input.baseline_updates) ? input.baseline_updates : [];
|
|
333
|
+
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)
|
|
334
|
+
|| manifest.$schema !== 'ultra-baseline-reconciliation-v1'
|
|
335
|
+
|| manifest.change_id !== change.id || manifest.baseline_id !== (baseline?.id || null)
|
|
336
|
+
|| JSON.stringify(manifest.baseline_updates) !== JSON.stringify(updates)
|
|
337
|
+
|| !Array.isArray(manifest.semantic_changes)
|
|
338
|
+
|| !Array.isArray(manifest.resolved_gap_ids)
|
|
339
|
+
|| !Array.isArray(manifest.resolved_unknowns)
|
|
340
|
+
|| !Array.isArray(manifest.verification) || manifest.verification.length === 0) {
|
|
341
|
+
throw new ChangeWorkflowError(
|
|
342
|
+
'BASELINE_RECONCILIATION_MANIFEST_INVALID', 'reconciliation manifest shape or authority binding is invalid',
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
if (manifest.verification.some((item) => (
|
|
346
|
+
!item || typeof item !== 'object' || item.status !== 'pass'
|
|
347
|
+
|| !String(item.name || '').trim() || !String(item.command || '').trim()
|
|
348
|
+
|| !String(item.evidence || '').trim()
|
|
349
|
+
))) {
|
|
350
|
+
throw new ChangeWorkflowError(
|
|
351
|
+
'BASELINE_RECONCILIATION_MANIFEST_INVALID', 'every reconciliation verification must be an evidenced pass',
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
if (updates.length === 0) {
|
|
355
|
+
if (manifest.semantic_changes.length > 0
|
|
356
|
+
|| String(manifest.semantic_no_change_reason || '').trim().length < 3) {
|
|
357
|
+
throw new ChangeWorkflowError(
|
|
358
|
+
'BASELINE_RECONCILIATION_MANIFEST_INVALID',
|
|
359
|
+
'no-change reconciliation requires an explicit semantic_no_change_reason and no semantic changes',
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
} else if (manifest.semantic_changes.length === 0) {
|
|
363
|
+
throw new ChangeWorkflowError(
|
|
364
|
+
'BASELINE_RECONCILIATION_MANIFEST_INVALID', 'baseline updates require semantic change records',
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
const specRefs = new Map((baseline?.spec_refs || []).map((ref) => [ref.path, ref]));
|
|
368
|
+
const semanticPaths = new Set();
|
|
369
|
+
for (const item of manifest.semantic_changes) {
|
|
370
|
+
if (!item || typeof item !== 'object' || !CHANGE_ID.test(String(item.id || ''))
|
|
371
|
+
|| !['add', 'update'].includes(item.action)
|
|
372
|
+
|| !/^[0-9a-f]{64}$/.test(String(item.after_digest || ''))) {
|
|
373
|
+
throw new ChangeWorkflowError(
|
|
374
|
+
'BASELINE_RECONCILIATION_MANIFEST_INVALID', 'semantic change record is invalid',
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
let resolved;
|
|
378
|
+
try { resolved = workflows.resolveProjectSourceRef(rootDir, item.source_ref, 'semantic_change.source_ref'); }
|
|
379
|
+
catch (error) {
|
|
380
|
+
throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_INVALID', error.message);
|
|
381
|
+
}
|
|
382
|
+
semanticPaths.add(resolved.relative);
|
|
383
|
+
if (!updates.includes(resolved.relative) || resolved.digest !== item.after_digest) {
|
|
384
|
+
throw new ChangeWorkflowError(
|
|
385
|
+
'BASELINE_RECONCILIATION_MANIFEST_INVALID',
|
|
386
|
+
`semantic change digest or baseline update binding is invalid: ${item.source_ref}`,
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
const before = specRefs.get(resolved.relative)?.digest || null;
|
|
390
|
+
if ((item.before_digest || null) !== before || item.action !== (before ? 'update' : 'add')) {
|
|
391
|
+
throw new ChangeWorkflowError(
|
|
392
|
+
'BASELINE_RECONCILIATION_MANIFEST_INVALID',
|
|
393
|
+
`semantic change before-state is invalid: ${item.source_ref}`,
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const missingSemanticUpdates = updates.filter((update) => !semanticPaths.has(update));
|
|
398
|
+
if (missingSemanticUpdates.length > 0) {
|
|
399
|
+
throw new ChangeWorkflowError(
|
|
400
|
+
'BASELINE_RECONCILIATION_MANIFEST_INVALID',
|
|
401
|
+
`baseline updates have no semantic record: ${missingSemanticUpdates.join(', ')}`,
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
const gapIds = new Set((baseline?.gaps || []).map((gap) => gap.id));
|
|
405
|
+
const unknowns = new Set((baseline?.unknowns || []).map((unknown) => unknown.summary));
|
|
406
|
+
if (manifest.resolved_gap_ids.some((id) => !gapIds.has(id))
|
|
407
|
+
|| manifest.resolved_unknowns.some((summary) => !unknowns.has(summary))) {
|
|
408
|
+
throw new ChangeWorkflowError(
|
|
409
|
+
'BASELINE_RECONCILIATION_MANIFEST_INVALID', 'resolved gap or unknown does not exist in baseline authority',
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
const serialized = fs.readFileSync(file);
|
|
413
|
+
return {
|
|
414
|
+
manifest, relative, file,
|
|
415
|
+
digest: crypto.createHash('sha256').update(serialized).digest('hex'),
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
133
419
|
function readChange(db, id) {
|
|
134
420
|
return rowToChange(db.prepare('SELECT * FROM changes WHERE id = ?').get(id));
|
|
135
421
|
}
|
|
@@ -151,11 +437,25 @@ function writeIntent(file, change) {
|
|
|
151
437
|
`- Kind: \`${change.kind}\``,
|
|
152
438
|
`- Base commit: \`${change.base_commit || 'unavailable'}\``,
|
|
153
439
|
`- Documentation impact: \`${impact.status}\``,
|
|
440
|
+
`- Research disposition: \`${change.research_disposition.status}\``,
|
|
441
|
+
`- Alignment checkpoint: \`${change.alignment_thread_id || 'not-required'}\``,
|
|
154
442
|
'',
|
|
155
443
|
'## Intent',
|
|
156
444
|
'',
|
|
157
445
|
change.intent,
|
|
158
446
|
'',
|
|
447
|
+
'## Outcome', '', change.contract.outcome, '',
|
|
448
|
+
'## Acceptance', '',
|
|
449
|
+
...change.contract.acceptance.flatMap((item) => [
|
|
450
|
+
`### ${item.id}`, '', item.criterion, '', `Verification: \`${item.verification}\``, '',
|
|
451
|
+
]),
|
|
452
|
+
'## Public seams', '', ...change.contract.public_seams.map((item) => `- ${item}`), '',
|
|
453
|
+
'## Non-goals', '', ...change.contract.non_goals.map((item) => `- ${item}`), '',
|
|
454
|
+
'## Recovery', '', change.contract.recovery.strategy, '',
|
|
455
|
+
`Verification: \`${change.contract.recovery.verification}\``, '',
|
|
456
|
+
'## Classification', '', change.classification.rationale, '',
|
|
457
|
+
`Risk flags: ${change.classification.risk_flags.join(', ') || 'none'}`, '',
|
|
458
|
+
'## Research routing', '', change.research_disposition.rationale, '',
|
|
159
459
|
];
|
|
160
460
|
if (impact.rationale) lines.push('## Documentation rationale', '', impact.rationale, '');
|
|
161
461
|
if (change.baseline_bypass) {
|
|
@@ -240,23 +540,27 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
240
540
|
? change.provider_refs
|
|
241
541
|
: normalizeProviderRefs(input.provider_refs);
|
|
242
542
|
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) }));
|
|
543
|
+
'SELECT id FROM tasks WHERE change_id = ? ORDER BY created_at ASC',
|
|
544
|
+
).all(change.id).map((task) => ops.readTask(db, task.id));
|
|
247
545
|
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
546
|
const spine = contextSpine.compileRoleContext(db, { input, change, tasks, rootDir });
|
|
547
|
+
const snapshotId = `ctx-${crypto.randomUUID().slice(0, 12)}`;
|
|
251
548
|
const manifest = {
|
|
252
549
|
schema_version: '2.0',
|
|
550
|
+
snapshot_id: snapshotId,
|
|
253
551
|
generated_at: nowIso(),
|
|
254
552
|
change: {
|
|
255
553
|
id: change.id, title: change.title, kind: change.kind, status: change.status,
|
|
256
554
|
intent: change.intent, docs_impact: change.docs_impact, base_commit: change.base_commit,
|
|
257
555
|
baseline_bypass: change.baseline_bypass,
|
|
556
|
+
contract: change.contract, classification: change.classification,
|
|
557
|
+
research_disposition: change.research_disposition,
|
|
558
|
+
},
|
|
559
|
+
git: {
|
|
560
|
+
head: spine.resume.git_head,
|
|
561
|
+
worktree_state: spine.resume.worktree_state,
|
|
562
|
+
worktree_digest: spine.resume.worktree_digest,
|
|
258
563
|
},
|
|
259
|
-
git: { head },
|
|
260
564
|
tasks,
|
|
261
565
|
selected_task: spine.selected_task,
|
|
262
566
|
specs,
|
|
@@ -270,11 +574,11 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
270
574
|
resume: spine.resume,
|
|
271
575
|
providers,
|
|
272
576
|
provider_boundary: 'metadata references only; memory and code graph content remain external',
|
|
273
|
-
tool_policy: { allowed_paths: allowedPaths },
|
|
274
577
|
};
|
|
275
578
|
const artifactDir = path.resolve(rootDir, change.artifact_root);
|
|
276
579
|
fs.mkdirSync(artifactDir, { recursive: true });
|
|
277
|
-
const manifestPath = path.join(artifactDir, '
|
|
580
|
+
const manifestPath = path.join(artifactDir, 'contexts', `${snapshotId}.json`);
|
|
581
|
+
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
278
582
|
const serialized = `${JSON.stringify(manifest, null, 2)}\n`;
|
|
279
583
|
fs.writeFileSync(manifestPath, serialized);
|
|
280
584
|
const hash = crypto.createHash('sha256').update(serialized).digest('hex');
|
|
@@ -288,8 +592,8 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
288
592
|
token_estimate, token_budget)
|
|
289
593
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
290
594
|
).run(
|
|
291
|
-
|
|
292
|
-
|
|
595
|
+
snapshotId, change.id, input.task_id || null,
|
|
596
|
+
spine.resume.git_head, JSON.stringify(providers), relative, hash,
|
|
293
597
|
spine.role, spine.gate, spine.next_action, spine.readiness.status,
|
|
294
598
|
JSON.stringify(spine.readiness.blockers), JSON.stringify(spine),
|
|
295
599
|
spine.context.token_estimate, spine.context.budget.max_tokens,
|
|
@@ -298,7 +602,9 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
298
602
|
change_id: change.id, task_id: input.task_id || null, kind: 'context_manifest',
|
|
299
603
|
artifactPath: relative, contentHash: hash,
|
|
300
604
|
metadata: {
|
|
301
|
-
git_head:
|
|
605
|
+
git_head: spine.resume.git_head,
|
|
606
|
+
worktree_digest: spine.resume.worktree_digest,
|
|
607
|
+
role: spine.role, gate: spine.gate,
|
|
302
608
|
readiness: spine.readiness.status, token_estimate: spine.context.token_estimate,
|
|
303
609
|
},
|
|
304
610
|
});
|
|
@@ -314,11 +620,35 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
314
620
|
ops.appendEventInTx(db, {
|
|
315
621
|
type: 'context_updated', change_id: change.id, task_id: input.task_id || null,
|
|
316
622
|
payload: {
|
|
317
|
-
manifest_path: relative, manifest_hash: hash, git_head:
|
|
623
|
+
manifest_path: relative, manifest_hash: hash, git_head: spine.resume.git_head,
|
|
624
|
+
worktree_digest: spine.resume.worktree_digest,
|
|
318
625
|
role: spine.role, gate: spine.gate, readiness: spine.readiness.status,
|
|
319
626
|
next_action: spine.next_action,
|
|
320
627
|
},
|
|
321
628
|
});
|
|
629
|
+
const linkedWorkflow = db.prepare(
|
|
630
|
+
`SELECT id FROM workflow_runs
|
|
631
|
+
WHERE kind = 'change' AND change_id = ? AND current_step = 'compile-context'
|
|
632
|
+
AND status IN ('active', 'blocked')
|
|
633
|
+
ORDER BY updated_at DESC, rowid DESC LIMIT 1`,
|
|
634
|
+
).get(change.id);
|
|
635
|
+
if (linkedWorkflow) {
|
|
636
|
+
if (spine.readiness.status === 'ready') {
|
|
637
|
+
workflows.recordWorkflowStep(db, {
|
|
638
|
+
id: linkedWorkflow.id, step_id: 'compile-context', status: 'completed',
|
|
639
|
+
evidence: [{
|
|
640
|
+
kind: 'context', ref: relative,
|
|
641
|
+
summary: `Role ${spine.role} context is ready for ${input.task_id || 'change planning'}.`,
|
|
642
|
+
}],
|
|
643
|
+
outputs: [{ path: relative, kind: 'context-manifest' }],
|
|
644
|
+
}, { rootDir });
|
|
645
|
+
} else {
|
|
646
|
+
workflows.recordWorkflowStep(db, {
|
|
647
|
+
id: linkedWorkflow.id, step_id: 'compile-context', status: 'blocked',
|
|
648
|
+
blockers: spine.readiness.blockers,
|
|
649
|
+
}, { rootDir });
|
|
650
|
+
}
|
|
651
|
+
}
|
|
322
652
|
});
|
|
323
653
|
return { manifest, context_manifest_path: manifestPath, manifest_hash: hash };
|
|
324
654
|
}
|
|
@@ -332,7 +662,7 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
332
662
|
'id plus non-blank title and intent of at least three characters are required',
|
|
333
663
|
);
|
|
334
664
|
}
|
|
335
|
-
if (!
|
|
665
|
+
if (!CHANGE_KINDS.has(input.kind)) {
|
|
336
666
|
throw new ChangeWorkflowError('VALIDATION_ERROR', `invalid change kind: ${input.kind}`);
|
|
337
667
|
}
|
|
338
668
|
if (input.baseline_bypass !== undefined && input.kind !== 'incident') {
|
|
@@ -357,8 +687,46 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
357
687
|
'VALIDATION_ERROR', 'baseline_bypass is not valid while the project baseline is healthy',
|
|
358
688
|
);
|
|
359
689
|
}
|
|
690
|
+
const alignmentGate = decisions.decisionGate(
|
|
691
|
+
db, { baseline_id: baselineHealth.baseline?.id || null }, { rootDir },
|
|
692
|
+
);
|
|
693
|
+
if (!alignmentGate.ready) {
|
|
694
|
+
throw new ChangeWorkflowError(
|
|
695
|
+
'CHANGE_ALIGNMENT_REQUIRED',
|
|
696
|
+
`resolve and confirm the active baseline alignment before creating a change: ${alignmentGate.blockers.join(', ')}`,
|
|
697
|
+
{ blockers: alignmentGate.blockers, thread_id: alignmentGate.thread?.id || null },
|
|
698
|
+
);
|
|
699
|
+
}
|
|
360
700
|
const docsImpact = normalizeDocsImpact(input.docs_impact);
|
|
361
701
|
const providers = normalizeProviderRefs(input.provider_refs);
|
|
702
|
+
const contract = normalizeChangeContract(input.contract);
|
|
703
|
+
const classification = normalizeClassification(input.classification, input.kind);
|
|
704
|
+
const researchDisposition = normalizeResearchDisposition(input.research_disposition, input.kind);
|
|
705
|
+
let alignmentThread = null;
|
|
706
|
+
if (input.alignment_thread_id !== undefined) {
|
|
707
|
+
try {
|
|
708
|
+
alignmentThread = decisions.assertConfirmedDecisionCheckpoint(
|
|
709
|
+
db, input.alignment_thread_id, { rootDir, requireArtifact: true },
|
|
710
|
+
);
|
|
711
|
+
} catch (error) {
|
|
712
|
+
throw new ChangeWorkflowError(
|
|
713
|
+
'CHANGE_ALIGNMENT_REQUIRED',
|
|
714
|
+
`alignment_thread_id must name a current artifact-bound checkpoint: ${error.message}`,
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
if (alignmentThread.change_id) {
|
|
718
|
+
throw new ChangeWorkflowError(
|
|
719
|
+
'CHANGE_ALIGNMENT_REQUIRED', `alignment thread already belongs to ${alignmentThread.change_id}`,
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
if (!baselineHealth.baseline?.id || alignmentThread.baseline_id !== baselineHealth.baseline.id
|
|
723
|
+
|| alignmentThread.workflow_run_id) {
|
|
724
|
+
throw new ChangeWorkflowError(
|
|
725
|
+
'CHANGE_ALIGNMENT_REQUIRED',
|
|
726
|
+
'change alignment must be bound only to the current baseline before change creation',
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
362
730
|
const artifactRoot = path.join('.ultra', 'changes', 'active', input.id);
|
|
363
731
|
const artifactDir = path.resolve(rootDir, artifactRoot);
|
|
364
732
|
if (fs.existsSync(artifactDir)) {
|
|
@@ -368,6 +736,8 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
368
736
|
const row = {
|
|
369
737
|
id: input.id, title, kind: input.kind, status: 'active',
|
|
370
738
|
intent, docs_impact: docsImpact, provider_refs: providers, baseline_bypass: baselineBypass,
|
|
739
|
+
contract, classification, research_disposition: researchDisposition,
|
|
740
|
+
alignment_thread_id: alignmentThread?.id || null,
|
|
371
741
|
base_commit: input.base_commit || gitHead(rootDir), artifact_root: artifactRoot,
|
|
372
742
|
created_at: ts, updated_at: ts,
|
|
373
743
|
};
|
|
@@ -376,14 +746,23 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
376
746
|
db.prepare(
|
|
377
747
|
`INSERT INTO changes
|
|
378
748
|
(id, title, kind, status, intent, docs_impact_json, provider_refs_json,
|
|
379
|
-
baseline_bypass_json,
|
|
380
|
-
|
|
749
|
+
baseline_bypass_json, contract_json, classification_json,
|
|
750
|
+
research_disposition_json, alignment_thread_id, base_commit, artifact_root,
|
|
751
|
+
created_at, updated_at)
|
|
752
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
381
753
|
).run(
|
|
382
754
|
row.id, row.title, row.kind, row.status, row.intent, JSON.stringify(row.docs_impact),
|
|
383
755
|
JSON.stringify(row.provider_refs), row.baseline_bypass
|
|
384
756
|
? JSON.stringify(row.baseline_bypass) : null,
|
|
757
|
+
JSON.stringify(row.contract), JSON.stringify(row.classification),
|
|
758
|
+
JSON.stringify(row.research_disposition), row.alignment_thread_id,
|
|
385
759
|
row.base_commit, row.artifact_root, ts, ts,
|
|
386
760
|
);
|
|
761
|
+
if (row.alignment_thread_id) {
|
|
762
|
+
db.prepare(
|
|
763
|
+
'UPDATE decision_threads SET change_id = ?, updated_at = ? WHERE id = ?',
|
|
764
|
+
).run(row.id, ts, row.alignment_thread_id);
|
|
765
|
+
}
|
|
387
766
|
fs.mkdirSync(artifactDir, { recursive: true });
|
|
388
767
|
const intentPath = path.join(artifactDir, 'intent.md');
|
|
389
768
|
writeIntent(intentPath, row);
|
|
@@ -404,12 +783,46 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
404
783
|
}
|
|
405
784
|
ops.appendEventInTx(db, {
|
|
406
785
|
type: 'change_created', change_id: row.id,
|
|
407
|
-
payload: {
|
|
786
|
+
payload: {
|
|
787
|
+
kind: row.kind, artifact_root: row.artifact_root,
|
|
788
|
+
alignment_thread_id: row.alignment_thread_id,
|
|
789
|
+
},
|
|
408
790
|
});
|
|
791
|
+
let workflow = workflows.startWorkflow(db, {
|
|
792
|
+
kind: 'change', baseline_id: baselineHealth.baseline?.id || null,
|
|
793
|
+
change_id: row.id, subject: `Establish executable readiness for ${row.title}.`,
|
|
794
|
+
metadata: { change_kind: row.kind, base_commit: row.base_commit },
|
|
795
|
+
}, { rootDir });
|
|
409
796
|
const context = compileContext(db, { id: row.id }, { rootDir });
|
|
797
|
+
workflow = workflows.recordWorkflowStep(db, {
|
|
798
|
+
id: workflow.id, step_id: 'bind-baseline', status: 'completed',
|
|
799
|
+
evidence: [{
|
|
800
|
+
kind: 'baseline', ref: baselineHealth.baseline?.id || 'incident-break-glass',
|
|
801
|
+
summary: baselineHealth.status === 'pass'
|
|
802
|
+
? `Bound ready baseline at ${baselineHealth.baseline.repository_revision}.`
|
|
803
|
+
: 'Bound the approved incident break-glass authority.',
|
|
804
|
+
}],
|
|
805
|
+
}, { rootDir });
|
|
806
|
+
workflow = workflows.recordWorkflowStep(db, {
|
|
807
|
+
id: workflow.id, step_id: 'classify-change', status: 'completed',
|
|
808
|
+
decisions: [{
|
|
809
|
+
kind: 'change_kind', value: row.kind,
|
|
810
|
+
rationale: row.classification.rationale, risk_flags: row.classification.risk_flags,
|
|
811
|
+
}],
|
|
812
|
+
}, { rootDir });
|
|
813
|
+
workflow = workflows.recordWorkflowStep(db, {
|
|
814
|
+
id: workflow.id, step_id: 'record-intent', status: 'completed',
|
|
815
|
+
evidence: [{ kind: 'intent', ref: path.relative(rootDir, intentPath), summary: row.intent }],
|
|
816
|
+
outputs: [{ path: path.relative(rootDir, intentPath), kind: 'change-intent' }],
|
|
817
|
+
decisions: [{
|
|
818
|
+
kind: 'change_contract', acceptance_ids: row.contract.acceptance.map((item) => item.id),
|
|
819
|
+
research_disposition: row.research_disposition,
|
|
820
|
+
}],
|
|
821
|
+
}, { rootDir });
|
|
410
822
|
return {
|
|
411
823
|
change: readChange(db, row.id), intent_path: intentPath,
|
|
412
824
|
context_manifest_path: context.context_manifest_path,
|
|
825
|
+
workflow,
|
|
413
826
|
};
|
|
414
827
|
});
|
|
415
828
|
} catch (error) {
|
|
@@ -428,15 +841,14 @@ function assertTaskCreationAllowed(db, input = {}, { rootDir = process.cwd() } =
|
|
|
428
841
|
}
|
|
429
842
|
const baselineHealth = baselines.inspectBaseline(db, { rootDir });
|
|
430
843
|
if (baselineHealth.status === 'pass') return;
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
}
|
|
844
|
+
const gate = change
|
|
845
|
+
? contextSpine.baselineGateForChange(db, change, baselineHealth)
|
|
846
|
+
: { blockers: baselineHealth.blockers };
|
|
847
|
+
if (gate.blockers.length === 0) return;
|
|
436
848
|
throw new ChangeWorkflowError(
|
|
437
849
|
'BASELINE_NOT_READY',
|
|
438
|
-
`
|
|
439
|
-
{ blockers:
|
|
850
|
+
`task creation requires healthy baseline authority or an approved incident break-glass record: ${gate.blockers.join(', ')}`,
|
|
851
|
+
{ blockers: gate.blockers, change_id: input.change_id || null },
|
|
440
852
|
);
|
|
441
853
|
}
|
|
442
854
|
|
|
@@ -451,7 +863,9 @@ function updateChange(db, id, patch = {}, { rootDir = process.cwd() } = {}) {
|
|
|
451
863
|
throw new ChangeWorkflowError('VALIDATION_ERROR', `change field ${field} is not patchable`);
|
|
452
864
|
}
|
|
453
865
|
}
|
|
454
|
-
const syncIntent = [
|
|
866
|
+
const syncIntent = [
|
|
867
|
+
'title', 'intent', 'docs_impact', 'contract', 'classification', 'research_disposition',
|
|
868
|
+
].some((field) => patch[field] !== undefined);
|
|
455
869
|
const intentPath = path.resolve(rootDir, current.artifact_root, 'intent.md');
|
|
456
870
|
const previousIntent = syncIntent && fs.existsSync(intentPath)
|
|
457
871
|
? fs.readFileSync(intentPath)
|
|
@@ -476,6 +890,17 @@ function updateChange(db, id, patch = {}, { rootDir = process.cwd() } = {}) {
|
|
|
476
890
|
if (patch.provider_refs !== undefined) {
|
|
477
891
|
sets.push('provider_refs_json = ?'); values.push(JSON.stringify(normalizeProviderRefs(patch.provider_refs)));
|
|
478
892
|
}
|
|
893
|
+
if (patch.contract !== undefined) {
|
|
894
|
+
sets.push('contract_json = ?'); values.push(JSON.stringify(normalizeChangeContract(patch.contract)));
|
|
895
|
+
}
|
|
896
|
+
if (patch.classification !== undefined) {
|
|
897
|
+
sets.push('classification_json = ?');
|
|
898
|
+
values.push(JSON.stringify(normalizeClassification(patch.classification, current.kind)));
|
|
899
|
+
}
|
|
900
|
+
if (patch.research_disposition !== undefined) {
|
|
901
|
+
sets.push('research_disposition_json = ?');
|
|
902
|
+
values.push(JSON.stringify(normalizeResearchDisposition(patch.research_disposition, current.kind)));
|
|
903
|
+
}
|
|
479
904
|
if (patch.status !== undefined) {
|
|
480
905
|
const allowed = CHANGE_TRANSITIONS[current.status] || new Set();
|
|
481
906
|
if (!allowed.has(patch.status)) {
|
|
@@ -522,65 +947,87 @@ function filesUnder(dir) {
|
|
|
522
947
|
return out;
|
|
523
948
|
}
|
|
524
949
|
|
|
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;
|
|
950
|
+
function baselineConvergenceBlockers(db, change, rootDir) {
|
|
951
|
+
const baselineHealth = baselines.inspectBaseline(db, { rootDir });
|
|
952
|
+
return contextSpine.baselineGateForChange(db, change, baselineHealth).blockers;
|
|
563
953
|
}
|
|
564
954
|
|
|
565
|
-
function
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
955
|
+
function deriveConvergenceEvidence(db, change, tasks, workflowGate, diagnosis, rootDir) {
|
|
956
|
+
const devRuns = db.prepare(
|
|
957
|
+
`SELECT id, task_id, summary_json FROM workflow_runs
|
|
958
|
+
WHERE kind = 'dev' AND change_id = ? AND status = 'completed'
|
|
959
|
+
ORDER BY completed_at ASC, rowid ASC`,
|
|
960
|
+
).all(change.id).map((row) => ({
|
|
961
|
+
id: row.id, task_id: row.task_id, summary: parseJson(row.summary_json, 'workflow_runs.summary_json'),
|
|
962
|
+
}));
|
|
963
|
+
const deltaPaths = filesUnder(path.join(rootDir, change.artifact_root, 'delta'))
|
|
964
|
+
.map((file) => path.relative(rootDir, file));
|
|
965
|
+
const appliedLearning = specLearning.listSpecLearning(db, change.id)
|
|
966
|
+
.filter((candidate) => candidate.status === 'applied')
|
|
967
|
+
.map((candidate) => candidate.target_ref);
|
|
968
|
+
const rows = [
|
|
969
|
+
{
|
|
970
|
+
category: 'diff', status: 'pass',
|
|
971
|
+
evidence: `Completed dev workflows: ${devRuns.map((run) => `${run.task_id}:${run.id}`).join(', ')}`,
|
|
972
|
+
refs: devRuns.map((run) => run.id),
|
|
973
|
+
},
|
|
974
|
+
{
|
|
975
|
+
category: 'tests', status: 'pass',
|
|
976
|
+
evidence: `DB-derived passing test workflow ${workflowGate.test_workflow_id}.`,
|
|
977
|
+
refs: [workflowGate.test_summary.report_path, workflowGate.test_summary.report_digest].filter(Boolean),
|
|
978
|
+
regression_signal: workflowGate.test_summary.regression_signal || null,
|
|
979
|
+
},
|
|
980
|
+
{
|
|
981
|
+
category: 'spec', status: 'pass',
|
|
982
|
+
evidence: deltaPaths.length > 0
|
|
983
|
+
? `Current delta artifacts: ${deltaPaths.join(', ')}`
|
|
984
|
+
: `Change intent and resolved specification learning for ${change.id}.`,
|
|
985
|
+
refs: [...deltaPaths, ...appliedLearning],
|
|
986
|
+
},
|
|
987
|
+
{
|
|
988
|
+
category: 'docs', status: 'pass',
|
|
989
|
+
evidence: change.docs_impact.status === 'required'
|
|
990
|
+
? `Declared documentation files: ${change.docs_impact.files.join(', ')}`
|
|
991
|
+
: `No documentation change: ${change.docs_impact.rationale}`,
|
|
992
|
+
refs: change.docs_impact.files,
|
|
993
|
+
},
|
|
994
|
+
...['spec_fidelity', 'engineering_standards'].map((axis) => ({
|
|
995
|
+
category: 'review', axis, status: 'pass',
|
|
996
|
+
evidence: `DB-derived ${axis} verdict from ${workflowGate.review_workflow_id}.`,
|
|
997
|
+
refs: workflowGate.review_summary.axes?.[axis]?.evidence_refs || [],
|
|
998
|
+
})),
|
|
999
|
+
];
|
|
1000
|
+
if (diagnosis.artifact) {
|
|
1001
|
+
rows.unshift({
|
|
1002
|
+
category: 'diagnosis', status: 'pass',
|
|
1003
|
+
evidence: `Structured incident diagnosis ${diagnosis.artifact.artifactPath}.`,
|
|
1004
|
+
refs: [diagnosis.artifact.artifactPath, diagnosis.artifact.contentHash],
|
|
1005
|
+
});
|
|
570
1006
|
}
|
|
571
|
-
|
|
572
|
-
if (baseline.mode === 'migrated') return ['BASELINE_MIGRATION_REVIEW_REQUIRED'];
|
|
573
|
-
return [`BASELINE_NOT_READY:${baseline.status}`];
|
|
1007
|
+
return rows;
|
|
574
1008
|
}
|
|
575
1009
|
|
|
576
1010
|
function convergeChange(db, input, { rootDir = process.cwd() } = {}) {
|
|
1011
|
+
if (Object.hasOwn(input || {}, 'evidence')) {
|
|
1012
|
+
throw new ChangeWorkflowError(
|
|
1013
|
+
'CONVERGENCE_EVIDENCE_AUTHORITY_VIOLATION',
|
|
1014
|
+
'change convergence evidence is derived from DB-backed dev, test, review, specification, and documentation authority',
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
577
1017
|
const change = readChange(db, input.id);
|
|
578
1018
|
if (!change) throw new ChangeWorkflowError('CHANGE_NOT_FOUND', `change ${input.id} not found`);
|
|
579
1019
|
if (!['active', 'blocked'].includes(change.status)) {
|
|
580
1020
|
throw new ChangeWorkflowError('CHANGE_NOT_CONVERGEABLE', `change ${input.id} is ${change.status}`);
|
|
581
1021
|
}
|
|
582
|
-
const tasks = db.prepare('SELECT id
|
|
583
|
-
|
|
1022
|
+
const tasks = db.prepare('SELECT id FROM tasks WHERE change_id = ? ORDER BY id')
|
|
1023
|
+
.all(change.id).map((task) => ops.readTask(db, task.id));
|
|
1024
|
+
const blockers = new Set();
|
|
1025
|
+
let workflowGate = null;
|
|
1026
|
+
try {
|
|
1027
|
+
workflowGate = workflows.validateDeliveryPrerequisites(db, { change_id: change.id }, rootDir);
|
|
1028
|
+
} catch (error) {
|
|
1029
|
+
blockers.add(error.code || 'WORKFLOW_GATE_UNAVAILABLE');
|
|
1030
|
+
}
|
|
584
1031
|
for (const blocker of baselineConvergenceBlockers(db, change, rootDir)) blockers.add(blocker);
|
|
585
1032
|
if (tasks.length === 0) blockers.add('NO_TASKS');
|
|
586
1033
|
if (tasks.some((task) => !['completed', 'expanded'].includes(task.status))) blockers.add('TASKS_INCOMPLETE');
|
|
@@ -624,11 +1071,24 @@ function convergeChange(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
624
1071
|
if (filesUnder(path.join(artifactDir, 'delta')).length === 0) blockers.add('SPEC_DELTA_MISSING');
|
|
625
1072
|
if (!fs.existsSync(path.join(artifactDir, 'plan.md'))) blockers.add('CHANGE_PLAN_MISSING');
|
|
626
1073
|
}
|
|
627
|
-
const
|
|
628
|
-
|
|
1074
|
+
const latestContext = db.prepare(
|
|
1075
|
+
`SELECT manifest_path, manifest_hash FROM context_snapshots
|
|
1076
|
+
WHERE change_id = ? ORDER BY created_at DESC, rowid DESC LIMIT 1`,
|
|
1077
|
+
).get(change.id);
|
|
1078
|
+
const manifestPath = latestContext?.manifest_path
|
|
1079
|
+
? safeRelativePath(rootDir, latestContext.manifest_path)
|
|
1080
|
+
: null;
|
|
1081
|
+
if (!manifestPath || !fs.existsSync(manifestPath)) blockers.add('CONTEXT_MANIFEST_MISSING');
|
|
629
1082
|
else {
|
|
630
1083
|
const manifest = parseJson(fs.readFileSync(manifestPath, 'utf8'), 'context-manifest.json');
|
|
1084
|
+
const manifestHash = crypto.createHash('sha256').update(fs.readFileSync(manifestPath)).digest('hex');
|
|
1085
|
+
if (latestContext.manifest_hash !== manifestHash) blockers.add('CONTEXT_MANIFEST_STALE');
|
|
631
1086
|
if (manifest.git && manifest.git.head !== gitHead(rootDir)) blockers.add('CONTEXT_HEAD_STALE');
|
|
1087
|
+
const checkout = baselines.gitWorktreeSnapshot(rootDir, ['.']);
|
|
1088
|
+
if (manifest.git?.worktree_digest && checkout.digest
|
|
1089
|
+
&& manifest.git.worktree_digest !== checkout.digest) {
|
|
1090
|
+
blockers.add('CONTEXT_WORKTREE_STALE');
|
|
1091
|
+
}
|
|
632
1092
|
const manifestTasks = (manifest.tasks || []).map((task) => task.id).sort();
|
|
633
1093
|
const currentTasks = tasks.map((task) => task.id).sort();
|
|
634
1094
|
if (JSON.stringify(manifestTasks) !== JSON.stringify(currentTasks)) blockers.add('CONTEXT_TASK_SET_STALE');
|
|
@@ -652,10 +1112,13 @@ function convergeChange(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
652
1112
|
return { ready: false, status: 'blocked', blockers: blockerList };
|
|
653
1113
|
}
|
|
654
1114
|
|
|
1115
|
+
const derivedEvidence = deriveConvergenceEvidence(
|
|
1116
|
+
db, change, tasks, workflowGate, diagnosis, rootDir,
|
|
1117
|
+
);
|
|
655
1118
|
const verificationPath = path.join(artifactDir, 'verification.md');
|
|
656
1119
|
const lines = [
|
|
657
1120
|
`# Verification: ${change.title}`, '',
|
|
658
|
-
...
|
|
1121
|
+
...derivedEvidence.flatMap((row) => [
|
|
659
1122
|
`## ${row.category}${row.axis ? `:${row.axis}` : ''}`, '',
|
|
660
1123
|
`Status: **${row.status}**`, '', String(row.evidence).trim(), '',
|
|
661
1124
|
]),
|
|
@@ -668,11 +1131,11 @@ function convergeChange(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
668
1131
|
upsertArtifact(db, {
|
|
669
1132
|
change_id: change.id, kind: 'verification', artifactPath: path.relative(rootDir, verificationPath),
|
|
670
1133
|
contentHash: crypto.createHash('sha256').update(fs.readFileSync(verificationPath)).digest('hex'),
|
|
671
|
-
metadata: { evidence:
|
|
1134
|
+
metadata: { evidence: derivedEvidence },
|
|
672
1135
|
});
|
|
673
1136
|
ops.appendEventInTx(db, {
|
|
674
1137
|
type: 'change_converged', change_id: change.id,
|
|
675
|
-
payload: { evidence_categories:
|
|
1138
|
+
payload: { evidence_categories: derivedEvidence.map((row) => row.category) },
|
|
676
1139
|
});
|
|
677
1140
|
});
|
|
678
1141
|
return { ready: true, status: 'ready', blockers: [], verification_path: verificationPath };
|
|
@@ -711,6 +1174,36 @@ function finalizeArchive(db, intent, { rootDir }) {
|
|
|
711
1174
|
db.prepare('UPDATE artifacts SET path = ?, updated_at = ? WHERE id = ?')
|
|
712
1175
|
.run(archivedPath, nowIso(), artifact.id);
|
|
713
1176
|
}
|
|
1177
|
+
const workflowOutputs = db.prepare(
|
|
1178
|
+
`SELECT ws.run_id, ws.step_id, ws.outputs_json
|
|
1179
|
+
FROM workflow_steps ws JOIN workflow_runs wr ON wr.id = ws.run_id
|
|
1180
|
+
WHERE wr.change_id = ?`,
|
|
1181
|
+
).all(change.id);
|
|
1182
|
+
for (const step of workflowOutputs) {
|
|
1183
|
+
const outputs = parseJson(step.outputs_json, 'workflow_steps.outputs_json');
|
|
1184
|
+
let changed = false;
|
|
1185
|
+
const moved = outputs.map((output) => {
|
|
1186
|
+
if (output.path !== intent.source && !output.path.startsWith(prefix)) return output;
|
|
1187
|
+
changed = true;
|
|
1188
|
+
const suffix = path.relative(intent.source, output.path);
|
|
1189
|
+
return { ...output, path: suffix ? path.join(relative, suffix) : relative };
|
|
1190
|
+
});
|
|
1191
|
+
if (changed) {
|
|
1192
|
+
db.prepare(
|
|
1193
|
+
'UPDATE workflow_steps SET outputs_json = ?, updated_at = ? WHERE run_id = ? AND step_id = ?',
|
|
1194
|
+
).run(JSON.stringify(moved), nowIso(), step.run_id, step.step_id);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
const contextRows = db.prepare(
|
|
1198
|
+
'SELECT id, manifest_path FROM context_snapshots WHERE change_id = ?',
|
|
1199
|
+
).all(change.id);
|
|
1200
|
+
for (const snapshot of contextRows) {
|
|
1201
|
+
if (snapshot.manifest_path !== intent.source && !snapshot.manifest_path.startsWith(prefix)) continue;
|
|
1202
|
+
const suffix = path.relative(intent.source, snapshot.manifest_path);
|
|
1203
|
+
const archivedPath = suffix ? path.join(relative, suffix) : relative;
|
|
1204
|
+
db.prepare('UPDATE context_snapshots SET manifest_path = ? WHERE id = ?')
|
|
1205
|
+
.run(archivedPath, snapshot.id);
|
|
1206
|
+
}
|
|
714
1207
|
const archivedSummary = path.join(destination, 'archive-summary.md');
|
|
715
1208
|
upsertArtifact(db, {
|
|
716
1209
|
change_id: change.id, kind: 'archive_summary',
|
|
@@ -721,6 +1214,19 @@ function finalizeArchive(db, intent, { rootDir }) {
|
|
|
721
1214
|
no_baseline_change_reason: intent.no_baseline_change_reason,
|
|
722
1215
|
},
|
|
723
1216
|
});
|
|
1217
|
+
const reconciliationSuffix = path.relative(intent.source, intent.reconciliation_path);
|
|
1218
|
+
const archivedReconciliationPath = path.join(relative, reconciliationSuffix);
|
|
1219
|
+
upsertArtifact(db, {
|
|
1220
|
+
change_id: change.id, kind: 'baseline_reconciliation',
|
|
1221
|
+
artifactPath: archivedReconciliationPath,
|
|
1222
|
+
contentHash: intent.reconciliation_digest,
|
|
1223
|
+
metadata: {
|
|
1224
|
+
schema: intent.reconciliation_manifest.$schema,
|
|
1225
|
+
semantic_changes: intent.reconciliation_manifest.semantic_changes.map((item) => item.id),
|
|
1226
|
+
resolved_gap_ids: intent.reconciliation_manifest.resolved_gap_ids,
|
|
1227
|
+
resolved_unknowns: intent.reconciliation_manifest.resolved_unknowns,
|
|
1228
|
+
},
|
|
1229
|
+
});
|
|
724
1230
|
db.prepare("UPDATE artifacts SET status = 'archived', updated_at = ? WHERE change_id = ?")
|
|
725
1231
|
.run(nowIso(), change.id);
|
|
726
1232
|
if (breakGlass) {
|
|
@@ -742,6 +1248,7 @@ function finalizeArchive(db, intent, { rootDir }) {
|
|
|
742
1248
|
} else {
|
|
743
1249
|
baselines.reconcileBaseline(db, {
|
|
744
1250
|
baseline_updates: intent.baseline_updates, change_id: change.id,
|
|
1251
|
+
reconciliation: intent.reconciliation_manifest,
|
|
745
1252
|
}, { rootDir });
|
|
746
1253
|
const baselineHealth = baselines.inspectBaseline(db, { rootDir });
|
|
747
1254
|
if (baselineHealth.blockers.length > 0) {
|
|
@@ -757,6 +1264,8 @@ function finalizeArchive(db, intent, { rootDir }) {
|
|
|
757
1264
|
payload: {
|
|
758
1265
|
archive_path: relative, baseline_updates: intent.baseline_updates,
|
|
759
1266
|
no_baseline_change_reason: intent.no_baseline_change_reason,
|
|
1267
|
+
reconciliation_path: archivedReconciliationPath,
|
|
1268
|
+
reconciliation_digest: intent.reconciliation_digest,
|
|
760
1269
|
baseline_bypass: breakGlass,
|
|
761
1270
|
},
|
|
762
1271
|
});
|
|
@@ -786,9 +1295,13 @@ function archiveChange(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
786
1295
|
if (!['ready', 'archived'].includes(change.status)) {
|
|
787
1296
|
throw new ChangeWorkflowError('CHANGE_NOT_READY', `change ${input.id} must converge before archive`);
|
|
788
1297
|
}
|
|
1298
|
+
const reconciliation = readReconciliationManifest(db, change, input, rootDir);
|
|
789
1299
|
const prepared = archiveJournal.prepareArchiveMove({
|
|
790
1300
|
rootDir, change, summary: String(input.summary), baselineUpdates: updates,
|
|
791
1301
|
noBaselineChangeReason: noChangeReason || null,
|
|
1302
|
+
reconciliationPath: reconciliation.relative,
|
|
1303
|
+
reconciliationDigest: reconciliation.digest,
|
|
1304
|
+
reconciliationManifest: reconciliation.manifest,
|
|
792
1305
|
});
|
|
793
1306
|
let result;
|
|
794
1307
|
try {
|