ultra-builder-pro-cli 0.24.2 → 0.25.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/adapters/claude.js +104 -0
  3. package/adapters/codex.js +363 -40
  4. package/adapters/grok.js +1254 -58
  5. package/adapters/kimi.js +682 -13
  6. package/adapters/opencode.js +132 -11
  7. package/docs/AGENT-CONTEXT.md +14 -0
  8. package/docs/ARTIFACT-AUTHORITY.md +13 -0
  9. package/docs/COMMIT-HASH-BACKFILL.md +25 -18
  10. package/docs/RUNTIME-COMPAT-MATRIX.md +18 -2
  11. package/mcp-server/lib/artifact-registry.cjs +181 -51
  12. package/mcp-server/lib/baseline-workflow.cjs +160 -23
  13. package/mcp-server/lib/change-workflow.cjs +21 -16
  14. package/mcp-server/lib/context-envelope.cjs +124 -51
  15. package/mcp-server/lib/decision-records.cjs +164 -78
  16. package/mcp-server/lib/kernel-change-workflow.cjs +2029 -0
  17. package/mcp-server/lib/legacy-change-workflow.cjs +294 -71
  18. package/mcp-server/lib/managed-file-write.cjs +56 -0
  19. package/mcp-server/lib/prd-parser.cjs +2 -2
  20. package/mcp-server/lib/stage-checkpoints.cjs +163 -9
  21. package/mcp-server/lib/state-db.cjs +186 -8
  22. package/mcp-server/lib/state-ops.cjs +214 -31
  23. package/mcp-server/lib/task-contract.cjs +404 -0
  24. package/mcp-server/lib/task-expander.cjs +2 -2
  25. package/mcp-server/lib/task-ledger.cjs +175 -62
  26. package/mcp-server/lib/ultra-facade.cjs +1786 -169
  27. package/mcp-server/lib/worker-packet.cjs +2 -2
  28. package/mcp-server/server.cjs +23 -4
  29. package/orchestrator/parallel-orchestrator.cjs +114 -27
  30. package/orchestrator/session-runner.cjs +25 -136
  31. package/package.json +1 -1
  32. package/skills/ultra-change/SKILL.md +6 -1
  33. package/skills/ultra-dev/SKILL.md +9 -4
  34. package/skills/ultra-plan/SKILL.md +13 -4
  35. package/skills/ultra-plan/references/semantic-preflight.md +6 -4
  36. package/spec/mcp-tools.yaml +114 -9
  37. package/spec/schemas/context-file.v4.5.schema.json +2 -2
  38. package/spec/schemas/state-db.sql +10 -6
  39. package/spec/schemas/task-ledger.v2.schema.json +23 -8
  40. package/spec/schemas/tasks.v4.5.schema.json +34 -12
@@ -0,0 +1,2029 @@
1
+ 'use strict';
2
+ const crypto = require('node:crypto');
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const {spawnSync} = require('node:child_process');
6
+ const Ajv = require('ajv/dist/2020');
7
+ const ops = require('./state-ops.cjs');
8
+ const artifacts = require('./artifact-registry.cjs');
9
+ const canonical = require('./canonical-json.cjs');
10
+ const changeAuthority = require('./change-authority.cjs');
11
+ const {writeManagedFile} = require('./managed-file-write.cjs');
12
+ const providerRefs = require('./provider-refs.cjs');
13
+ const baselines = require('./baseline-workflow.cjs');
14
+ const archiveJournal = require('./archive-journal.cjs');
15
+ const changePacket = require('./change-packet.cjs');
16
+ const deliveryTransaction = require('./delivery-transaction.cjs');
17
+ const {openStableProjectRead, readStableProjectFile, walkStableProjectTree} = require('./safe-project-file.cjs');
18
+ const stageCheckpoints = require('./stage-checkpoints.cjs');
19
+ const reconciliationSchema = require('../../spec/schemas/baseline-reconciliation.v1.schema.json');
20
+ const reconciliationAjv = new Ajv({
21
+ allErrors: true,
22
+ strict: false
23
+ });
24
+ const validateReconciliationSchema = reconciliationAjv.compile(reconciliationSchema);
25
+ const CHANGE_ID = /^[a-zA-Z0-9_-]+$/;
26
+ const CHANGE_PATCH_FIELDS = new Set([
27
+ 'title',
28
+ 'intent',
29
+ 'status',
30
+ 'docs_impact',
31
+ 'provider_refs',
32
+ 'contract',
33
+ 'classification',
34
+ 'research_disposition'
35
+ ]);
36
+ const CHANGE_CREATE_FIELDS = new Set([
37
+ 'id',
38
+ 'title',
39
+ 'kind',
40
+ 'intent',
41
+ 'docs_impact',
42
+ 'provider_refs',
43
+ 'baseline_bypass',
44
+ 'contract',
45
+ 'classification',
46
+ 'research_disposition',
47
+ 'base_commit',
48
+ 'supersedes_id'
49
+ ]);
50
+ const SEMANTIC_AUTHORITY_FIELDS = new Set([
51
+ 'intent',
52
+ 'docs_impact',
53
+ 'provider_refs',
54
+ 'contract',
55
+ 'classification',
56
+ 'research_disposition'
57
+ ]);
58
+ const INCIDENT_DIAGNOSIS_SECTIONS = Object.freeze([
59
+ {
60
+ heading: 'Reproduction',
61
+ key: 'reproduction'
62
+ },
63
+ {
64
+ heading: 'Hypotheses',
65
+ key: 'hypotheses'
66
+ },
67
+ {
68
+ heading: 'Root cause',
69
+ key: 'root-cause'
70
+ },
71
+ {
72
+ heading: 'Regression test',
73
+ key: 'regression-test'
74
+ },
75
+ {
76
+ heading: 'Recovery',
77
+ key: 'recovery'
78
+ }
79
+ ]);
80
+ const LIST_CHANGES_SQL = 'SELECT * FROM changes WHERE (@status IS NULL OR status = @status) AND (@kind IS NULL OR kind = @kind) ORDER BY created_at DESC LIMIT @maxn';
81
+ class ChangeWorkflowError extends Error {
82
+ constructor(code, message, details) {
83
+ super(message);
84
+ this.name = 'ChangeWorkflowError';
85
+ this.code = code;
86
+ if (details !== undefined)
87
+ this.details = details;
88
+ }
89
+ }
90
+ function nowIso() {
91
+ return new Date().toISOString();
92
+ }
93
+ function parseJson(value, field) {
94
+ try {
95
+ return JSON.parse(value);
96
+ } catch (error) {
97
+ throw new ChangeWorkflowError('STATE_CORRUPT', `invalid ${ field }: ${ error.message }`);
98
+ }
99
+ }
100
+ function rowToChange(row) {
101
+ if (!row)
102
+ return null;
103
+ const change = {
104
+ ...row,
105
+ docs_impact: parseJson(row.docs_impact_json, 'docs_impact_json'),
106
+ provider_refs: parseJson(row.provider_refs_json, 'provider_refs_json'),
107
+ baseline_bypass: row.baseline_bypass_json ? parseJson(row.baseline_bypass_json, 'baseline_bypass_json') : null,
108
+ contract: parseJson(row.contract_json || '{}', 'contract_json'),
109
+ classification: parseJson(row.classification_json || '{}', 'classification_json'),
110
+ research_disposition: parseJson(row.research_disposition_json || '{}', 'research_disposition_json')
111
+ };
112
+ delete change.docs_impact_json;
113
+ delete change.provider_refs_json;
114
+ delete change.baseline_bypass_json;
115
+ delete change.contract_json;
116
+ delete change.classification_json;
117
+ delete change.research_disposition_json;
118
+ return change;
119
+ }
120
+ function assertExactFields(value, allowed, field) {
121
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
122
+ throw new ChangeWorkflowError('VALIDATION_ERROR', `${ field } must be an object`);
123
+ }
124
+ const unknown = Object.keys(value).filter(key => !allowed.has(key));
125
+ if (unknown.length > 0) {
126
+ throw new ChangeWorkflowError('VALIDATION_ERROR', `${ field }.${ unknown[0] } is not allowed`);
127
+ }
128
+ }
129
+ function normalizeDocsImpact(value) {
130
+ const input = value === undefined ? {} : value;
131
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
132
+ throw new ChangeWorkflowError('VALIDATION_ERROR', 'docs impact must be an object');
133
+ }
134
+ assertExactFields(input, new Set([
135
+ 'status',
136
+ 'files',
137
+ 'rationale'
138
+ ]), 'docs_impact');
139
+ const status = input.status === undefined ? 'unknown' : input.status;
140
+ if (typeof status !== 'string') {
141
+ throw new ChangeWorkflowError('VALIDATION_ERROR', 'docs impact status must be a string');
142
+ }
143
+ if (![
144
+ 'unknown',
145
+ 'required',
146
+ 'none'
147
+ ].includes(status)) {
148
+ throw new ChangeWorkflowError('VALIDATION_ERROR', `invalid docs impact status: ${ status }`);
149
+ }
150
+ const files = input.files === undefined ? [] : input.files;
151
+ if (!Array.isArray(files) || files.some(file => typeof file !== 'string' || !file.trim())) {
152
+ throw new ChangeWorkflowError('VALIDATION_ERROR', 'docs impact files must be non-empty strings');
153
+ }
154
+ if (input.rationale != null && typeof input.rationale !== 'string') {
155
+ throw new ChangeWorkflowError('VALIDATION_ERROR', 'docs impact rationale must be a string or null');
156
+ }
157
+ const rationale = input.rationale == null ? null : input.rationale.trim();
158
+ return {
159
+ status,
160
+ files: [...new Set(files.map(file => file.trim()))],
161
+ rationale
162
+ };
163
+ }
164
+ function normalizeProviderRefs(value) {
165
+ return providerRefs.normalizeProviderRefs(value, ChangeWorkflowError);
166
+ }
167
+ function requiredText(value, field) {
168
+ const text = typeof value === 'string' ? value.trim() : '';
169
+ if (!text)
170
+ throw new ChangeWorkflowError('CHANGE_CONTRACT_REQUIRED', `${ field } is required`);
171
+ return text;
172
+ }
173
+ function stringList(value, field, {
174
+ allowEmpty = false
175
+ } = {}) {
176
+ if (!Array.isArray(value) || !allowEmpty && value.length === 0 || value.some(item => typeof item !== 'string' || !item.trim())) {
177
+ throw new ChangeWorkflowError('CHANGE_CONTRACT_REQUIRED', `${ field } must be ${ allowEmpty ? 'an' : 'a non-empty' } array of strings`);
178
+ }
179
+ return [...new Set(value.map(item => item.trim()))];
180
+ }
181
+ function optionalTrimmedText(value, field, {
182
+ maxLength = 4000
183
+ } = {}) {
184
+ if (value === undefined || value === null)
185
+ return '';
186
+ if (typeof value !== 'string') {
187
+ throw new ChangeWorkflowError('VALIDATION_ERROR', `${ field } must be a string`);
188
+ }
189
+ const text = value.trim();
190
+ if (text.length > maxLength) {
191
+ throw new ChangeWorkflowError('VALIDATION_ERROR', `${ field } must not exceed ${ maxLength } characters`);
192
+ }
193
+ return text;
194
+ }
195
+ function boundedVocabulary(value, field) {
196
+ const text = optionalTrimmedText(value, field, { maxLength: 80 });
197
+ if (!text) {
198
+ throw new ChangeWorkflowError('VALIDATION_ERROR', `${ field } is required`);
199
+ }
200
+ return text;
201
+ }
202
+ function normalizeChangeContract(value) {
203
+ const input = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
204
+ assertExactFields(input, new Set([
205
+ 'outcome',
206
+ 'acceptance',
207
+ 'non_goals',
208
+ 'public_seams',
209
+ 'recovery',
210
+ 'unresolved_decisions'
211
+ ]), 'contract');
212
+ if (input.acceptance !== undefined && !Array.isArray(input.acceptance)) {
213
+ throw new ChangeWorkflowError('VALIDATION_ERROR', 'contract.acceptance must be an array');
214
+ }
215
+ const ids = new Set();
216
+ const acceptance = (input.acceptance || []).map((item, index) => {
217
+ if (!item || typeof item !== 'object' || Array.isArray(item)) {
218
+ throw new ChangeWorkflowError('CHANGE_CONTRACT_REQUIRED', `contract.acceptance[${ index }] is invalid`);
219
+ }
220
+ assertExactFields(item, new Set([
221
+ 'id',
222
+ 'criterion',
223
+ 'verification'
224
+ ]), `contract.acceptance[${ index }]`);
225
+ const id = requiredText(item.id, `contract.acceptance[${ index }].id`);
226
+ if (!CHANGE_ID.test(id) || ids.has(id)) {
227
+ throw new ChangeWorkflowError('CHANGE_CONTRACT_REQUIRED', `acceptance id is invalid or duplicated: ${ id }`);
228
+ }
229
+ ids.add(id);
230
+ return {
231
+ id,
232
+ criterion: requiredText(item.criterion, `contract.acceptance[${ index }].criterion`),
233
+ verification: requiredText(item.verification, `contract.acceptance[${ index }].verification`)
234
+ };
235
+ });
236
+ const recovery = input.recovery;
237
+ if (recovery !== undefined && recovery !== null) {
238
+ assertExactFields(recovery, new Set([
239
+ 'strategy',
240
+ 'verification'
241
+ ]), 'contract.recovery');
242
+ }
243
+ if (input.unresolved_decisions !== undefined && !Array.isArray(input.unresolved_decisions)) {
244
+ throw new ChangeWorkflowError('VALIDATION_ERROR', 'contract.unresolved_decisions must be an array');
245
+ }
246
+ const decisions = (input.unresolved_decisions || []).map((item, index) => {
247
+ if (!item || typeof item !== 'object' || Array.isArray(item)) {
248
+ throw new ChangeWorkflowError('CHANGE_CONTRACT_REQUIRED', `unresolved decision ${ index } is invalid`);
249
+ }
250
+ assertExactFields(item, new Set([
251
+ 'id',
252
+ 'summary',
253
+ 'blocking',
254
+ 'owner'
255
+ ]), `contract.unresolved_decisions[${ index }]`);
256
+ if (item.blocking !== undefined && typeof item.blocking !== 'boolean') {
257
+ throw new ChangeWorkflowError('VALIDATION_ERROR', `contract.unresolved_decisions[${ index }].blocking must be a boolean`);
258
+ }
259
+ return {
260
+ id: requiredText(item.id, `contract.unresolved_decisions[${ index }].id`),
261
+ summary: requiredText(item.summary, `contract.unresolved_decisions[${ index }].summary`),
262
+ blocking: item.blocking === true,
263
+ owner: item.owner == null ? null : requiredText(item.owner, `contract.unresolved_decisions[${ index }].owner`)
264
+ };
265
+ });
266
+ return {
267
+ outcome: optionalTrimmedText(input.outcome, 'contract.outcome'),
268
+ acceptance,
269
+ non_goals: stringList(input.non_goals || [], 'contract.non_goals', { allowEmpty: true }),
270
+ public_seams: stringList(input.public_seams || [], 'contract.public_seams', { allowEmpty: true }),
271
+ recovery: recovery ? {
272
+ strategy: optionalTrimmedText(recovery.strategy, 'contract.recovery.strategy'),
273
+ verification: optionalTrimmedText(recovery.verification, 'contract.recovery.verification')
274
+ } : null,
275
+ unresolved_decisions: decisions
276
+ };
277
+ }
278
+ function normalizeClassification(value) {
279
+ const input = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
280
+ assertExactFields(input, new Set([
281
+ 'rationale',
282
+ 'risk_flags'
283
+ ]), 'classification');
284
+ const flags = stringList(input.risk_flags === undefined ? [] : input.risk_flags, 'classification.risk_flags', { allowEmpty: true });
285
+ return {
286
+ rationale: optionalTrimmedText(input.rationale, 'classification.rationale'),
287
+ risk_flags: flags
288
+ };
289
+ }
290
+ function normalizeResearchDisposition(value) {
291
+ const input = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
292
+ assertExactFields(input, new Set([
293
+ 'status',
294
+ 'mode',
295
+ 'selected_steps',
296
+ 'rationale'
297
+ ]), 'research_disposition');
298
+ const status = input.status === undefined ? 'unresolved' : boundedVocabulary(input.status, 'research_disposition.status');
299
+ const selectedSteps = stringList(input.selected_steps === undefined ? [] : input.selected_steps, 'research_disposition.selected_steps', { allowEmpty: true });
300
+ if (input.mode !== undefined && input.mode !== null && typeof input.mode !== 'string') {
301
+ throw new ChangeWorkflowError('VALIDATION_ERROR', 'research_disposition.mode must be a string or null');
302
+ }
303
+ const mode = input.mode == null ? null : boundedVocabulary(input.mode, 'research_disposition.mode');
304
+ return {
305
+ status,
306
+ mode,
307
+ selected_steps: selectedSteps,
308
+ rationale: optionalTrimmedText(input.rationale, 'research_disposition.rationale')
309
+ };
310
+ }
311
+ function normalizeBaselineBypass(value) {
312
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
313
+ throw new ChangeWorkflowError('BASELINE_BYPASS_REQUIRED', 'incident work without a ready baseline requires an explicit baseline_bypass record');
314
+ }
315
+ assertExactFields(value, new Set([
316
+ 'reason',
317
+ 'approved_by'
318
+ ]), 'baseline_bypass');
319
+ const reason = typeof value.reason === 'string' ? value.reason.trim() : '';
320
+ const approvedBy = typeof value.approved_by === 'string' ? value.approved_by.trim() : '';
321
+ if (reason.length < 3 || !approvedBy) {
322
+ throw new ChangeWorkflowError('BASELINE_BYPASS_REQUIRED', 'baseline_bypass requires reason and approved_by');
323
+ }
324
+ return {
325
+ mode: 'incident_break_glass',
326
+ reason,
327
+ approved_by: approvedBy,
328
+ recorded_at: nowIso()
329
+ };
330
+ }
331
+ function gitHead(rootDir) {
332
+ const result = spawnSync('git', [
333
+ 'rev-parse',
334
+ 'HEAD'
335
+ ], {
336
+ cwd: rootDir,
337
+ encoding: 'utf8',
338
+ stdio: [
339
+ 'ignore',
340
+ 'pipe',
341
+ 'pipe'
342
+ ]
343
+ });
344
+ return result.status === 0 ? result.stdout.trim() : null;
345
+ }
346
+ function safeRelativePath(rootDir, candidate) {
347
+ if (typeof candidate !== 'string' || !candidate.trim() || path.isAbsolute(candidate)) {
348
+ throw new ChangeWorkflowError('VALIDATION_ERROR', `path must be project-relative: ${ candidate }`);
349
+ }
350
+ const root = path.resolve(rootDir);
351
+ const target = path.resolve(root, candidate);
352
+ if (target !== root && !target.startsWith(`${ root }${ path.sep }`)) {
353
+ throw new ChangeWorkflowError('VALIDATION_ERROR', `path escapes project root: ${ candidate }`);
354
+ }
355
+ return target;
356
+ }
357
+ function markdownSlug(value) {
358
+ return String(value).trim().toLowerCase().replace(/<[^>]+>/g, '').replace(/[^\p{L}\p{N}\s-]/gu, '').replace(/\s+/g, '-').replace(/-+/g, '-');
359
+ }
360
+ function resolveProjectSourceRef(rootDir, sourceRef, field) {
361
+ const ref = typeof sourceRef === 'string' ? sourceRef.trim() : '';
362
+ const separator = ref.lastIndexOf('#');
363
+ if (separator <= 0 || separator === ref.length - 1) {
364
+ throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_INVALID', `${ field } must use path#anchor`);
365
+ }
366
+ const relative = artifacts.normalizeRelativePath(ref.slice(0, separator));
367
+ const anchor = ref.slice(separator + 1).trim().toLowerCase();
368
+ const resolved = readStableProjectFile(rootDir, relative, { encoding: 'utf8' });
369
+ const anchors = new Set();
370
+ for (const match of resolved.text.matchAll(/^#{1,6}\s+(.+?)\s*#*\s*$/gm)) {
371
+ anchors.add(markdownSlug(match[1]));
372
+ }
373
+ for (const match of resolved.text.matchAll(/<a\s+(?:name|id)=["']([^"']+)["'][^>]*>/gi)) {
374
+ anchors.add(match[1].trim().toLowerCase());
375
+ }
376
+ if (!anchors.has(anchor)) {
377
+ throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_INVALID', `${ field } anchor does not exist: ${ ref }`);
378
+ }
379
+ return {
380
+ ref,
381
+ relative,
382
+ anchor,
383
+ digest: resolved.digest
384
+ };
385
+ }
386
+ function rebindRegistryPath(candidate, source, destination) {
387
+ const current = artifacts.normalizeRelativePath(candidate);
388
+ const from = artifacts.normalizeRelativePath(source);
389
+ const to = artifacts.normalizeRelativePath(destination);
390
+ if (current !== from && !current.startsWith(`${ from }/`))
391
+ return null;
392
+ const suffix = current === from ? '' : path.posix.relative(from, current);
393
+ return suffix ? path.posix.join(to, suffix) : to;
394
+ }
395
+ function assertReconciliationAuthority(db, change, relative, digest) {
396
+ const rows = db.prepare('SELECT * FROM artifacts WHERE status <> \'archived\' ORDER BY id').all().filter(item => {
397
+ try {
398
+ return artifacts.normalizeRelativePath(item.path) === relative;
399
+ } catch {
400
+ return false;
401
+ }
402
+ });
403
+ const row = rows.length === 1 ? rows[0] : null;
404
+ const authorityDigest = row?.digest || row?.content_hash || null;
405
+ if (!row || row.managed !== 1 || row.owner_type !== 'change' || row.owner_id !== change.id || row.change_id !== change.id || row.kind !== 'baseline_reconciliation' || row.status !== 'current' || authorityDigest !== digest) {
406
+ throw new ChangeWorkflowError('BASELINE_RECONCILIATION_AUTHORITY_INVALID', 'reconciliation manifest must match one current managed Change artifact authority', {
407
+ path: relative,
408
+ artifact_ids: rows.map(item => item.id),
409
+ expected_owner: `change:${ change.id }`,
410
+ expected_kind: 'baseline_reconciliation',
411
+ expected_digest: digest,
412
+ actual_digest: authorityDigest
413
+ });
414
+ }
415
+ return row;
416
+ }
417
+ function openReconciliationReader(rootDir, relative) {
418
+ try {
419
+ return openStableProjectRead(rootDir, relative);
420
+ } catch (error) {
421
+ if (error?.code === 'PROJECT_FILE_MISSING')
422
+ return null;
423
+ throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_UNSAFE', `reconciliation manifest cannot be read safely: ${ relative }`, { cause: error.code || error.message });
424
+ }
425
+ }
426
+ function readReconciliationManifest(db, change, input, rootDir) {
427
+ const requested = typeof input.reconciliation_path === 'string' ? input.reconciliation_path.trim() : '';
428
+ if (!requested) {
429
+ throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_REQUIRED', 'reconciliation_path is required');
430
+ }
431
+ let relative;
432
+ try {
433
+ relative = artifacts.normalizeRelativePath(requested);
434
+ } catch (error) {
435
+ throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_REQUIRED', error.message);
436
+ }
437
+ let file = safeRelativePath(rootDir, relative);
438
+ const artifactRoot = path.resolve(rootDir, change.artifact_root);
439
+ if (!file.startsWith(`${ artifactRoot }${ path.sep }`)) {
440
+ throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_REQUIRED', 'reconciliation manifest must be a file inside the active change artifact root');
441
+ }
442
+ let reader = openReconciliationReader(rootDir, relative);
443
+ let manifest;
444
+ let digest;
445
+ let expectedDigest = null;
446
+ let authorityDigest = null;
447
+ let journalIntent = null;
448
+ if (!reader) {
449
+ const pending = archiveJournal.listArchiveIntents(rootDir).find(item => !item.error && item.intent.change_id === change.id && item.intent.reconciliation_path === relative);
450
+ if (pending) {
451
+ journalIntent = pending.intent;
452
+ const suffix = path.posix.relative(pending.intent.source, relative);
453
+ const archivedRelative = path.posix.join(pending.intent.destination, suffix);
454
+ file = safeRelativePath(rootDir, archivedRelative);
455
+ reader = openReconciliationReader(rootDir, archivedRelative);
456
+ expectedDigest = pending.intent.reconciliation_digest;
457
+ authorityDigest = pending.intent.reconciliation_digest;
458
+ const rebind = archiveJournal.readArchiveRebind(rootDir, pending.intent);
459
+ const rebound = rebind?.entries.find(entry => entry.relative_path === path.posix.relative(pending.intent.destination, archivedRelative));
460
+ if (rebound) {
461
+ if (rebound.before_digest !== pending.intent.reconciliation_digest) {
462
+ throw new ChangeWorkflowError('BASELINE_RECONCILIATION_AUTHORITY_INVALID', 'archive rebind journal does not begin from the prepared reconciliation authority', {
463
+ expected_digest: pending.intent.reconciliation_digest,
464
+ actual_digest: rebound.before_digest
465
+ });
466
+ }
467
+ expectedDigest = rebound.after_digest;
468
+ authorityDigest = rebound.before_digest;
469
+ }
470
+ }
471
+ }
472
+ if (!reader) {
473
+ throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_REQUIRED', 'reconciliation manifest file is missing');
474
+ }
475
+ try {
476
+ digest = reader.digest;
477
+ if (expectedDigest && digest !== expectedDigest) {
478
+ throw new ChangeWorkflowError('BASELINE_RECONCILIATION_AUTHORITY_INVALID', 'archived reconciliation bytes do not match the prepared journal digest', {
479
+ expected_digest: expectedDigest,
480
+ actual_digest: digest
481
+ });
482
+ }
483
+ assertReconciliationAuthority(db, change, relative, authorityDigest || digest);
484
+ manifest = JSON.parse(reader.bytes.toString('utf8'));
485
+ reader.verify();
486
+ } catch (error) {
487
+ if (error instanceof ChangeWorkflowError)
488
+ throw error;
489
+ if (error?.code === 'PROJECT_FILE_CHANGED' || error?.code === 'PROJECT_FILE_UNSAFE') {
490
+ throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_UNSAFE', `reconciliation manifest changed while it was read: ${ relative }`, { cause: error.code });
491
+ }
492
+ throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_INVALID', error.message);
493
+ } finally {
494
+ reader.close();
495
+ }
496
+ if (!validateReconciliationSchema(manifest)) {
497
+ throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_INVALID', `reconciliation manifest does not satisfy its published schema: ${ reconciliationAjv.errorsText(validateReconciliationSchema.errors) }`, validateReconciliationSchema.errors);
498
+ }
499
+ const baseline = baselines.readBaseline(db);
500
+ const updates = Array.isArray(input.baseline_updates) ? input.baseline_updates : [];
501
+ if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest) || manifest.$schema !== 'ultra-baseline-reconciliation-v1' || manifest.change_id !== change.id || manifest.baseline_id !== (baseline?.id || null) || JSON.stringify(manifest.baseline_updates) !== JSON.stringify(updates) || !Array.isArray(manifest.semantic_changes) || !Array.isArray(manifest.resolved_gap_ids) || !Array.isArray(manifest.resolved_unknowns) || !Array.isArray(manifest.verification) || manifest.verification.length === 0) {
502
+ throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_INVALID', 'reconciliation manifest shape or authority binding is invalid');
503
+ }
504
+ if (manifest.verification.some(item => !item || typeof item !== 'object' || item.status !== 'pass' || !String(item.name || '').trim() || !String(item.command || '').trim() || !String(item.evidence || '').trim())) {
505
+ throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_INVALID', 'every reconciliation verification must be an evidenced pass');
506
+ }
507
+ if (updates.length === 0) {
508
+ if (manifest.semantic_changes.length > 0 || String(manifest.semantic_no_change_reason || '').trim().length < 3) {
509
+ throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_INVALID', 'no-change reconciliation requires an explicit semantic_no_change_reason and no semantic changes');
510
+ }
511
+ } else if (manifest.semantic_changes.length === 0) {
512
+ throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_INVALID', 'baseline updates require semantic change records');
513
+ }
514
+ const specRefs = new Map((baseline?.spec_refs || []).map(ref => [
515
+ ref.path,
516
+ ref
517
+ ]));
518
+ const semanticPaths = new Set();
519
+ for (const item of manifest.semantic_changes) {
520
+ if (!item || typeof item !== 'object' || !CHANGE_ID.test(String(item.id || '')) || ![
521
+ 'add',
522
+ 'update'
523
+ ].includes(item.action) || !/^[0-9a-f]{64}$/.test(String(item.after_digest || ''))) {
524
+ throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_INVALID', 'semantic change record is invalid');
525
+ }
526
+ let resolved;
527
+ try {
528
+ resolved = resolveProjectSourceRef(rootDir, item.source_ref, 'semantic_change.source_ref');
529
+ } catch (error) {
530
+ throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_INVALID', error.message);
531
+ }
532
+ semanticPaths.add(resolved.relative);
533
+ if (!updates.includes(resolved.relative) || resolved.digest !== item.after_digest) {
534
+ throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_INVALID', `semantic change digest or baseline update binding is invalid: ${ item.source_ref }`);
535
+ }
536
+ const before = specRefs.get(resolved.relative)?.digest || null;
537
+ if ((item.before_digest || null) !== before || item.action !== (before ? 'update' : 'add')) {
538
+ throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_INVALID', `semantic change before-state is invalid: ${ item.source_ref }`);
539
+ }
540
+ }
541
+ const missingSemanticUpdates = updates.filter(update => !semanticPaths.has(update));
542
+ if (missingSemanticUpdates.length > 0) {
543
+ throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_INVALID', `baseline updates have no semantic record: ${ missingSemanticUpdates.join(', ') }`);
544
+ }
545
+ const gapIds = new Set((baseline?.gaps || []).map(gap => gap.id));
546
+ const unknowns = new Set((baseline?.unknowns || []).map(unknown => unknown.summary));
547
+ if (manifest.resolved_gap_ids.some(id => !gapIds.has(id)) || manifest.resolved_unknowns.some(summary => !unknowns.has(summary))) {
548
+ throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_INVALID', 'resolved gap or unknown does not exist in baseline authority');
549
+ }
550
+ return {
551
+ manifest,
552
+ relative,
553
+ file,
554
+ digest,
555
+ journal_intent: journalIntent
556
+ };
557
+ }
558
+ function readChange(db, id) {
559
+ return rowToChange(db.prepare('SELECT * FROM changes WHERE id = ?').get(id));
560
+ }
561
+ function listChanges(db, {status = null, kind = null, limit = 100} = {}) {
562
+ return db.prepare(LIST_CHANGES_SQL).all({
563
+ status,
564
+ kind,
565
+ maxn: Math.min(Math.max(limit, 1), 500)
566
+ }).map(rowToChange);
567
+ }
568
+ function writeIntent(file, change) {
569
+ const impact = change.docs_impact;
570
+ const recovery = change.contract.recovery;
571
+ const lines = [
572
+ `# ${ change.title }`,
573
+ '',
574
+ `- Change: \`${ change.id }\``,
575
+ `- Kind: \`${ change.kind }\``,
576
+ `- Base commit: \`${ change.base_commit || 'unavailable' }\``,
577
+ `- Documentation impact: \`${ impact.status }\``,
578
+ `- Research disposition: \`${ change.research_disposition.status }\``,
579
+ `- Alignment checkpoint: \`${ change.alignment_thread_id || 'not-required' }\``,
580
+ '',
581
+ '## Intent',
582
+ '',
583
+ change.intent,
584
+ '',
585
+ '## Outcome',
586
+ '',
587
+ change.contract.outcome,
588
+ '',
589
+ '## Acceptance',
590
+ '',
591
+ ...change.contract.acceptance.flatMap(item => [
592
+ `### ${ item.id }`,
593
+ '',
594
+ item.criterion,
595
+ '',
596
+ `Verification: \`${ item.verification }\``,
597
+ ''
598
+ ]),
599
+ '## Public seams',
600
+ '',
601
+ ...change.contract.public_seams.map(item => `- ${ item }`),
602
+ '',
603
+ '## Non-goals',
604
+ '',
605
+ ...change.contract.non_goals.map(item => `- ${ item }`),
606
+ '',
607
+ '## Recovery',
608
+ '',
609
+ recovery?.strategy || 'Not yet decided.',
610
+ '',
611
+ `Verification: \`${ recovery?.verification || 'not yet decided' }\``,
612
+ '',
613
+ '## Classification',
614
+ '',
615
+ change.classification.rationale,
616
+ '',
617
+ `Risk flags: ${ change.classification.risk_flags.join(', ') || 'none' }`,
618
+ '',
619
+ '## Research routing',
620
+ '',
621
+ change.research_disposition.rationale,
622
+ ''
623
+ ];
624
+ if (impact.rationale)
625
+ lines.push('## Documentation rationale', '', impact.rationale, '');
626
+ if (change.baseline_bypass) {
627
+ lines.push('## Baseline break-glass', '', `- Approved by: ${ change.baseline_bypass.approved_by }`, `- Recorded at: ${ change.baseline_bypass.recorded_at }`, '', change.baseline_bypass.reason, '');
628
+ }
629
+ fs.writeFileSync(file, `${ lines.join('\n') }\n`);
630
+ }
631
+ function writeIncidentDiagnosis(file, change) {
632
+ const lines = [
633
+ `# Incident diagnosis: ${ change.title }`,
634
+ '',
635
+ ...INCIDENT_DIAGNOSIS_SECTIONS.flatMap(({heading}) => [
636
+ `## ${ heading }`,
637
+ '',
638
+ ''
639
+ ])
640
+ ];
641
+ fs.writeFileSync(file, `${ lines.join('\n') }\n`);
642
+ }
643
+ function upsertArtifact(db, {change_id, task_id = null, kind, artifactPath, contentHash, metadata, consumer_ref = null, terminal = false, writer = 'ultra.record', rootDir = process.cwd()}) {
644
+ const ownerType = task_id ? 'task' : 'change';
645
+ const ownerId = task_id || change_id;
646
+ const consumer = terminal ? null : consumer_ref || (task_id ? {
647
+ type: 'task',
648
+ id: task_id,
649
+ relation: 'consumed_by'
650
+ } : null);
651
+ if (!terminal && !consumer) {
652
+ throw new ChangeWorkflowError('ARTIFACT_CONSUMER_MISSING', 'artifact requires an explicit consumer');
653
+ }
654
+ return artifacts.recordArtifactInTx(db, {
655
+ owner_type: ownerType,
656
+ owner_id: ownerId,
657
+ kind,
658
+ path: artifactPath,
659
+ content_digest: contentHash || undefined,
660
+ source_refs: [{
661
+ type: 'change',
662
+ id: change_id,
663
+ relation: 'produced_for'
664
+ }],
665
+ consumer_refs: consumer ? [consumer] : [],
666
+ provenance: { writer },
667
+ metadata: {
668
+ ...metadata || {},
669
+ ...terminal ? { terminal_role: true } : {}
670
+ }
671
+ }, { rootDir });
672
+ }
673
+ function createChange(db, input, {rootDir = process.cwd()} = {}) {
674
+ assertExactFields(input, CHANGE_CREATE_FIELDS, 'change');
675
+ const id = typeof input?.id === 'string' ? input.id.trim() : '';
676
+ const title = typeof input?.title === 'string' ? input.title.trim() : '';
677
+ const intent = typeof input?.intent === 'string' ? input.intent.trim() : '';
678
+ if (!input || !CHANGE_ID.test(id) || title.length < 3 || intent.length < 3) {
679
+ throw new ChangeWorkflowError('VALIDATION_ERROR', 'id plus non-blank title and intent of at least three characters are required');
680
+ }
681
+ const kind = boundedVocabulary(input.kind, 'change.kind');
682
+ if (input.base_commit !== undefined && input.base_commit !== null && typeof input.base_commit !== 'string') {
683
+ throw new ChangeWorkflowError('VALIDATION_ERROR', 'base_commit must be a string or null');
684
+ }
685
+ if (input.supersedes_id !== undefined && input.supersedes_id !== null && (typeof input.supersedes_id !== 'string' || !CHANGE_ID.test(input.supersedes_id.trim()))) {
686
+ throw new ChangeWorkflowError('VALIDATION_ERROR', 'supersedes_id must be a valid Change id');
687
+ }
688
+ const baseCommit = typeof input.base_commit === 'string' ? input.base_commit.trim() : null;
689
+ if (input.baseline_bypass !== undefined) {
690
+ throw new ChangeWorkflowError(
691
+ 'VALIDATION_ERROR',
692
+ 'baseline_bypass is not part of the public Change draft contract',
693
+ );
694
+ }
695
+ if (readChange(db, id))
696
+ throw new ChangeWorkflowError('DUPLICATE_CHANGE_ID', `change ${ id } exists`);
697
+ const baselineHealth = baselines.inspectBaseline(db, { rootDir });
698
+ const docsImpact = normalizeDocsImpact(input.docs_impact);
699
+ const providers = normalizeProviderRefs(input.provider_refs);
700
+ const contract = normalizeChangeContract(input.contract);
701
+ const classification = normalizeClassification(input.classification);
702
+ const researchDisposition = normalizeResearchDisposition(input.research_disposition);
703
+ const artifactRoot = path.join('.ultra', 'changes', 'active', id);
704
+ const artifactDir = path.resolve(rootDir, artifactRoot);
705
+ if (fs.existsSync(artifactDir)) {
706
+ throw new ChangeWorkflowError('CHANGE_ARTIFACT_EXISTS', `change artifact already exists: ${ artifactDir }`);
707
+ }
708
+ const ts = nowIso();
709
+ const row = {
710
+ id,
711
+ title,
712
+ kind,
713
+ status: 'active',
714
+ intent,
715
+ docs_impact: docsImpact,
716
+ provider_refs: providers,
717
+ baseline_bypass: null,
718
+ contract,
719
+ classification,
720
+ research_disposition: researchDisposition,
721
+ alignment_thread_id: null,
722
+ supersedes_id: input.supersedes_id?.trim() || null,
723
+ base_commit: baseCommit || gitHead(rootDir),
724
+ artifact_root: artifactRoot,
725
+ created_at: ts,
726
+ updated_at: ts
727
+ };
728
+ try {
729
+ return ops.tx(db, () => {
730
+ db.prepare(`INSERT INTO changes
731
+ (id, title, kind, status, intent, docs_impact_json, provider_refs_json,
732
+ baseline_bypass_json, contract_json, classification_json,
733
+ research_disposition_json, alignment_thread_id, base_commit, supersedes_id, artifact_root,
734
+ created_at, updated_at)
735
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(row.id, row.title, row.kind, row.status, row.intent, JSON.stringify(row.docs_impact), JSON.stringify(row.provider_refs), row.baseline_bypass ? JSON.stringify(row.baseline_bypass) : null, JSON.stringify(row.contract), JSON.stringify(row.classification), JSON.stringify(row.research_disposition), row.alignment_thread_id, row.base_commit, row.supersedes_id, row.artifact_root, ts, ts);
736
+ fs.mkdirSync(artifactDir, { recursive: true });
737
+ const intentPath = path.join(artifactDir, 'intent.md');
738
+ writeIntent(intentPath, row);
739
+ if (row.kind === 'incident') {
740
+ const diagnosisPath = path.join(artifactDir, 'diagnosis.md');
741
+ writeIncidentDiagnosis(diagnosisPath, row);
742
+ }
743
+ ops.appendEventInTx(db, {
744
+ type: 'change_created',
745
+ change_id: row.id,
746
+ payload: {
747
+ kind: row.kind,
748
+ artifact_root: row.artifact_root,
749
+ alignment_thread_id: row.alignment_thread_id,
750
+ supersedes_id: row.supersedes_id
751
+ }
752
+ });
753
+ upsertArtifact(db, {
754
+ change_id: row.id,
755
+ kind: 'intent',
756
+ artifactPath: path.relative(rootDir, intentPath),
757
+ contentHash: crypto.createHash('sha256').update(fs.readFileSync(intentPath)).digest('hex'),
758
+ consumer_ref: {
759
+ type: 'external',
760
+ id: 'ultra-plan',
761
+ relation: 'consumed_by'
762
+ },
763
+ writer: 'ultra.record',
764
+ rootDir
765
+ });
766
+ if (row.kind === 'incident') {
767
+ const diagnosisPath = path.join(artifactDir, 'diagnosis.md');
768
+ upsertArtifact(db, {
769
+ change_id: row.id,
770
+ kind: 'diagnosis',
771
+ artifactPath: path.relative(rootDir, diagnosisPath),
772
+ contentHash: crypto.createHash('sha256').update(fs.readFileSync(diagnosisPath)).digest('hex'),
773
+ metadata: { required_sections: INCIDENT_DIAGNOSIS_SECTIONS.map(({key}) => key) },
774
+ consumer_ref: {
775
+ type: 'external',
776
+ id: 'ultra-dev',
777
+ relation: 'consumed_by'
778
+ },
779
+ writer: 'ultra.record',
780
+ rootDir
781
+ });
782
+ }
783
+ return {
784
+ change: readChange(db, row.id),
785
+ intent_path: intentPath,
786
+ context_manifest_path: null,
787
+ workflow: null,
788
+ diagnostics: baselineHealth.status === 'pass' ? [] : (baselineHealth.blockers || []).map(code => ({
789
+ code,
790
+ severity: 'needs_attention'
791
+ }))
792
+ };
793
+ });
794
+ } catch (error) {
795
+ fs.rmSync(artifactDir, {
796
+ recursive: true,
797
+ force: true
798
+ });
799
+ throw error;
800
+ }
801
+ }
802
+ function assertTaskCreationAllowed(db, input = {}, {
803
+ rootDir = process.cwd()
804
+ } = {}) {
805
+ const change = input.change_id ? readChange(db, input.change_id) : null;
806
+ if (input.change_id && !change) {
807
+ throw new ChangeWorkflowError('CHANGE_NOT_FOUND', 'change ' + input.change_id + ' not found');
808
+ }
809
+ if (change && ![
810
+ 'active',
811
+ 'blocked'
812
+ ].includes(change.status)) {
813
+ throw new ChangeWorkflowError('CHANGE_NOT_MUTABLE', 'change ' + change.id + ' is ' + change.status);
814
+ }
815
+ const health = baselines.inspectBaseline(db, { rootDir });
816
+ return {
817
+ allowed: true,
818
+ diagnostics: health.status === 'pass' ? [] : (health.blockers || []).map(code => ({
819
+ code,
820
+ severity: 'needs_attention'
821
+ }))
822
+ };
823
+ }
824
+ function updateChange(db, id, patch = {}, {rootDir = process.cwd()} = {}) {
825
+ const normalizedId = typeof id === 'string' ? id.trim() : '';
826
+ if (!CHANGE_ID.test(normalizedId)) {
827
+ throw new ChangeWorkflowError('VALIDATION_ERROR', 'change id is invalid');
828
+ }
829
+ if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
830
+ throw new ChangeWorkflowError('VALIDATION_ERROR', 'change patch must be an object');
831
+ }
832
+ const current = readChange(db, normalizedId);
833
+ if (!current) {
834
+ throw new ChangeWorkflowError('CHANGE_NOT_FOUND', `change ${ normalizedId } not found`);
835
+ }
836
+ if ([
837
+ 'archived',
838
+ 'cancelled'
839
+ ].includes(current.status)) {
840
+ throw new ChangeWorkflowError('CHANGE_NOT_MUTABLE', `change ${ id } is ${ current.status }`);
841
+ }
842
+ for (const field of Object.keys(patch)) {
843
+ if (!CHANGE_PATCH_FIELDS.has(field)) {
844
+ throw new ChangeWorkflowError('VALIDATION_ERROR', `change field ${ field } is not patchable`);
845
+ }
846
+ }
847
+ if (patch.status !== undefined && ![
848
+ 'active',
849
+ 'cancelled'
850
+ ].includes(patch.status)) {
851
+ throw new ChangeWorkflowError('VALIDATION_ERROR', 'Change status may only be active or cancelled; readiness is derived from checkpoints');
852
+ }
853
+ const syncIntent = [
854
+ 'title',
855
+ 'intent',
856
+ 'docs_impact',
857
+ 'contract',
858
+ 'classification',
859
+ 'research_disposition'
860
+ ].some(field => patch[field] !== undefined);
861
+ const intentPath = path.resolve(rootDir, current.artifact_root, 'intent.md');
862
+ const previousIntent = syncIntent && fs.existsSync(intentPath) ? fs.readFileSync(intentPath) : null;
863
+ try {
864
+ return ops.tx(db, () => {
865
+ const sets = [];
866
+ const values = [];
867
+ if (patch.title !== undefined) {
868
+ const title = typeof patch.title === 'string' ? patch.title.trim() : '';
869
+ if (!title)
870
+ throw new ChangeWorkflowError('VALIDATION_ERROR', 'change title cannot be empty');
871
+ sets.push('title = ?');
872
+ values.push(title);
873
+ }
874
+ if (patch.intent !== undefined) {
875
+ const intent = typeof patch.intent === 'string' ? patch.intent.trim() : '';
876
+ if (!intent)
877
+ throw new ChangeWorkflowError('VALIDATION_ERROR', 'change intent cannot be empty');
878
+ sets.push('intent = ?');
879
+ values.push(intent);
880
+ }
881
+ if (patch.docs_impact !== undefined) {
882
+ sets.push('docs_impact_json = ?');
883
+ values.push(JSON.stringify(normalizeDocsImpact(patch.docs_impact)));
884
+ }
885
+ if (patch.provider_refs !== undefined) {
886
+ sets.push('provider_refs_json = ?');
887
+ values.push(JSON.stringify(normalizeProviderRefs(patch.provider_refs)));
888
+ }
889
+ if (patch.contract !== undefined) {
890
+ sets.push('contract_json = ?');
891
+ values.push(JSON.stringify(normalizeChangeContract(patch.contract)));
892
+ }
893
+ if (patch.classification !== undefined) {
894
+ sets.push('classification_json = ?');
895
+ values.push(JSON.stringify(normalizeClassification(patch.classification)));
896
+ }
897
+ if (patch.research_disposition !== undefined) {
898
+ sets.push('research_disposition_json = ?');
899
+ values.push(JSON.stringify(normalizeResearchDisposition(patch.research_disposition)));
900
+ }
901
+ if (patch.status !== undefined) {
902
+ const allowed = new Set([
903
+ 'active',
904
+ 'cancelled'
905
+ ]);
906
+ if (!allowed.has(patch.status)) {
907
+ throw new ChangeWorkflowError('ILLEGAL_CHANGE_TRANSITION', `cannot transition change ${ id } from ${ current.status } to ${ patch.status }`);
908
+ }
909
+ sets.push('status = ?');
910
+ values.push(patch.status);
911
+ } else if ([
912
+ 'ready',
913
+ 'blocked'
914
+ ].includes(current.status) && Object.keys(patch).length > 0) {
915
+ sets.push('status = ?');
916
+ values.push('active');
917
+ }
918
+ if (sets.length === 0)
919
+ return current;
920
+ sets.push('updated_at = ?');
921
+ values.push(nowIso(), normalizedId);
922
+ db.prepare(`UPDATE changes SET ${ sets.join(', ') } WHERE id = ?`).run(...values);
923
+ const updated = readChange(db, normalizedId);
924
+ const invalidated = [];
925
+ let intentArtifactId = null;
926
+ if (syncIntent) {
927
+ fs.mkdirSync(path.dirname(intentPath), { recursive: true });
928
+ writeIntent(intentPath, updated);
929
+ const recorded = upsertArtifact(db, {
930
+ change_id: normalizedId,
931
+ kind: 'intent',
932
+ artifactPath: path.relative(rootDir, intentPath),
933
+ contentHash: crypto.createHash('sha256').update(fs.readFileSync(intentPath)).digest('hex'),
934
+ consumer_ref: {
935
+ type: 'external',
936
+ id: 'ultra-plan',
937
+ relation: 'consumed_by'
938
+ },
939
+ writer: 'ultra.record',
940
+ rootDir
941
+ });
942
+ intentArtifactId = recorded.artifact.id;
943
+ invalidated.push(...recorded.invalidated);
944
+ }
945
+ const semanticFields = Object.keys(patch).filter(field => SEMANTIC_AUTHORITY_FIELDS.has(field));
946
+ if (semanticFields.length > 0 && changeAuthority.changeStateDigest(current) !== changeAuthority.changeStateDigest(updated)) {
947
+ invalidated.push(...artifacts.invalidateConsumersFromEndpointInTx(db, {
948
+ type: 'change',
949
+ id: normalizedId
950
+ }, {
951
+ reason: 'change_semantic_authority_updated',
952
+ exclude: intentArtifactId ? [{
953
+ type: 'artifact',
954
+ id: intentArtifactId
955
+ }] : []
956
+ }));
957
+ }
958
+ const invalidatedTasks = [...new Set(invalidated.filter(item => item.type === 'task').map(item => item.id))].sort();
959
+ ops.appendEventInTx(db, {
960
+ type: 'change_updated',
961
+ change_id: normalizedId,
962
+ payload: {
963
+ fields: Object.keys(patch),
964
+ invalidated_tasks: invalidatedTasks,
965
+ invalidated_authorities: invalidated
966
+ }
967
+ });
968
+ return updated;
969
+ });
970
+ } catch (error) {
971
+ if (syncIntent) {
972
+ if (previousIntent === null)
973
+ fs.rmSync(intentPath, { force: true });
974
+ else
975
+ fs.writeFileSync(intentPath, previousIntent);
976
+ }
977
+ throw error;
978
+ }
979
+ }
980
+ function recordDelta(db, input, {
981
+ rootDir = process.cwd()
982
+ } = {}) {
983
+ const change = readChange(db, input?.id);
984
+ if (!change) {
985
+ throw new ChangeWorkflowError('CHANGE_NOT_FOUND', `change ${ input?.id || '(missing)' } not found`);
986
+ }
987
+ return changePacket.recordDelta(db, change, input, { rootDir });
988
+ }
989
+ function recordDocumentationReconciliation(db, input, {
990
+ rootDir = process.cwd()
991
+ } = {}) {
992
+ const change = readChange(db, input?.id);
993
+ if (!change) {
994
+ throw new ChangeWorkflowError('CHANGE_NOT_FOUND', `change ${ input?.id || '(missing)' } not found`);
995
+ }
996
+ return changePacket.recordDocumentationReconciliation(db, change, input, { rootDir });
997
+ }
998
+ function markdownAnchor(text, fallback) {
999
+ const heading = String(text).match(/^#{1,6}\s+(.+?)\s*#*\s*$/m)?.[1] || fallback;
1000
+ return String(heading || 'change').trim().toLowerCase().replace(/[^\p{L}\p{N}\s-]/gu, '').replace(/\s+/g, '-').replace(/-+/g, '-');
1001
+ }
1002
+ function generatePacketReconciliation(db, change, packet, rootDir) {
1003
+ const baseline = baselines.readBaseline(db);
1004
+ const semanticChanges = packet.delta.value.mutations.map(mutation => {
1005
+ const overlay = safeRelativePath(rootDir, mutation.overlay_path);
1006
+ const anchor = markdownAnchor(fs.readFileSync(overlay, 'utf8'), path.basename(mutation.target_path, path.extname(mutation.target_path)));
1007
+ return {
1008
+ id: `delta-${ mutation.id }`,
1009
+ action: mutation.action,
1010
+ source_ref: `${ mutation.target_path }#${ anchor }`,
1011
+ before_digest: mutation.before_digest,
1012
+ after_digest: mutation.after_digest
1013
+ };
1014
+ });
1015
+ const verification = [{
1016
+ name: 'typed Change packet read-back',
1017
+ command: 'change.delta',
1018
+ status: 'pass',
1019
+ evidence: `Change delta ${ packet.delta.artifact.id } @ ${ packet.delta.artifact.digest } and documentation reconciliation ${ packet.documentation?.artifact.id || 'none-required' } were read back before Deliver.`
1020
+ }];
1021
+ const manifest = {
1022
+ $schema: 'ultra-baseline-reconciliation-v1',
1023
+ change_id: change.id,
1024
+ baseline_id: baseline?.id || null,
1025
+ baseline_updates: packet.baseline_updates,
1026
+ semantic_changes: semanticChanges,
1027
+ resolved_gap_ids: [],
1028
+ resolved_unknowns: [],
1029
+ verification,
1030
+ ...semanticChanges.length === 0 ? { semantic_no_change_reason: packet.delta.value.no_semantic_change_reason || 'The accepted Change has no baseline specification mutation.' } : {}
1031
+ };
1032
+ const relative = `${ change.artifact_root }/baseline-reconciliation.json`;
1033
+ const file = safeRelativePath(rootDir, relative);
1034
+ const previous = fs.existsSync(file) ? fs.readFileSync(file) : null;
1035
+ const serialized = `${ JSON.stringify(manifest, null, 2) }\n`;
1036
+ fs.writeFileSync(file, serialized);
1037
+ try {
1038
+ const recorded = artifacts.recordArtifact(db, {
1039
+ id: `${ change.id }-baseline-reconciliation`,
1040
+ owner_type: 'change',
1041
+ owner_id: change.id,
1042
+ kind: 'baseline_reconciliation',
1043
+ path: relative,
1044
+ content_digest: crypto.createHash('sha256').update(serialized).digest('hex'),
1045
+ source_refs: [
1046
+ {
1047
+ type: 'artifact',
1048
+ id: packet.delta.artifact.id,
1049
+ relation: 'reconciles'
1050
+ },
1051
+ ...packet.documentation ? [{
1052
+ type: 'artifact',
1053
+ id: packet.documentation.artifact.id,
1054
+ relation: 'reconciles_documentation'
1055
+ }] : []
1056
+ ],
1057
+ consumer_refs: [],
1058
+ provenance: {
1059
+ writer: 'change.deliver',
1060
+ generated: true
1061
+ },
1062
+ metadata: {
1063
+ terminal_role: true,
1064
+ schema: manifest.$schema
1065
+ }
1066
+ }, { rootDir }).artifact;
1067
+ return {
1068
+ manifest,
1069
+ relative,
1070
+ file,
1071
+ digest: recorded.digest,
1072
+ artifact: recorded
1073
+ };
1074
+ } catch (error) {
1075
+ if (previous === null)
1076
+ fs.rmSync(file, { force: true });
1077
+ else
1078
+ fs.writeFileSync(file, previous);
1079
+ throw error;
1080
+ }
1081
+ }
1082
+ function generateCallerNoChangeReconciliation(db, change, input, rootDir) {
1083
+ const baseline = baselines.readBaseline(db);
1084
+ const manifest = {
1085
+ $schema: 'ultra-baseline-reconciliation-v1',
1086
+ change_id: change.id,
1087
+ baseline_id: baseline?.id || null,
1088
+ baseline_updates: [],
1089
+ semantic_changes: [],
1090
+ semantic_no_change_reason: String(input.no_baseline_change_reason).trim(),
1091
+ resolved_gap_ids: [],
1092
+ resolved_unknowns: [],
1093
+ verification: [{
1094
+ name: 'explicit archive handoff',
1095
+ command: 'ultra.archive',
1096
+ status: 'pass',
1097
+ evidence: String(input.summary).trim()
1098
+ }]
1099
+ };
1100
+ const relative = `${ change.artifact_root }/baseline-reconciliation.json`;
1101
+ const serialized = `${ JSON.stringify(manifest, null, 2) }\n`;
1102
+ const file = safeRelativePath(rootDir, relative);
1103
+ const previous = fs.existsSync(file) ? fs.readFileSync(file) : null;
1104
+ const published = writeManagedFile(rootDir, relative, Buffer.from(serialized));
1105
+ try {
1106
+ const recorded = artifacts.recordArtifact(db, {
1107
+ id: `${ change.id }-baseline-reconciliation`,
1108
+ owner_type: 'change',
1109
+ owner_id: change.id,
1110
+ kind: 'baseline_reconciliation',
1111
+ path: relative,
1112
+ content_digest: published.digest,
1113
+ source_refs: [],
1114
+ consumer_refs: [],
1115
+ provenance: {
1116
+ writer: 'ultra.archive',
1117
+ generated: true
1118
+ },
1119
+ metadata: {
1120
+ terminal_role: true,
1121
+ schema: manifest.$schema,
1122
+ caller_declared: true
1123
+ }
1124
+ }, { rootDir }).artifact;
1125
+ return {
1126
+ manifest,
1127
+ relative,
1128
+ file,
1129
+ digest: recorded.digest,
1130
+ artifact: recorded
1131
+ };
1132
+ } catch (error) {
1133
+ if (previous === null)
1134
+ fs.rmSync(file, { force: true });
1135
+ else
1136
+ fs.writeFileSync(file, previous);
1137
+ throw error;
1138
+ }
1139
+ }
1140
+ function replaceArchiveAuthorityText(text, source, destination, digestMap = new Map(), {
1141
+ normalizePath = false
1142
+ } = {}) {
1143
+ const original = String(text);
1144
+ const windowsSource = source.replaceAll('/', '\\');
1145
+ let result = original.replaceAll(source, destination).replaceAll(windowsSource, destination);
1146
+ if (normalizePath && original.includes(windowsSource)) {
1147
+ result = result.replaceAll('\\', '/');
1148
+ }
1149
+ for (const [before, after] of digestMap) {
1150
+ if (before !== after)
1151
+ result = result.replaceAll(before, after);
1152
+ }
1153
+ return result;
1154
+ }
1155
+ function kernelArchiveContextPath(change, id) {
1156
+ return `${ change.artifact_root }/authority/context-envelopes/${ id }.json`;
1157
+ }
1158
+ function kernelArchiveWorkerPath(change, id) {
1159
+ return `${ change.artifact_root }/authority/worker-packets/${ id }.json`;
1160
+ }
1161
+ function materializeKernelArchiveAuthority(db, change, rootDir) {
1162
+ const taskIds = db.prepare('SELECT id FROM tasks WHERE change_id = ? ORDER BY id').all(change.id).map(row => row.id);
1163
+ const contexts = db.prepare(`SELECT * FROM context_envelopes
1164
+ WHERE (scope_type = 'change' AND scope_id = ?)
1165
+ OR (scope_type = 'task' AND scope_id IN (
1166
+ SELECT id FROM tasks WHERE change_id = ?
1167
+ ))
1168
+ ORDER BY id`).all(change.id, change.id);
1169
+ for (const row of contexts) {
1170
+ const read = readStableProjectFile(rootDir, row.artifact_path);
1171
+ if (!row.file_digest || read.digest !== row.file_digest) {
1172
+ throw new ChangeWorkflowError('CONTEXT_ENVELOPE_FILE_DRIFT', `cannot archive unbound Context Envelope ${ row.id }`, {
1173
+ path: row.artifact_path,
1174
+ expected: row.file_digest || null,
1175
+ actual: read.digest
1176
+ });
1177
+ }
1178
+ const target = kernelArchiveContextPath(change, row.id);
1179
+ const published = writeManagedFile(rootDir, target, read.bytes);
1180
+ upsertArtifact(db, {
1181
+ change_id: change.id,
1182
+ task_id: row.scope_type === 'task' ? row.scope_id : null,
1183
+ kind: 'archive_context_envelope',
1184
+ artifactPath: target,
1185
+ contentHash: published.digest,
1186
+ consumer_ref: {
1187
+ type: 'external',
1188
+ id: 'ultra.archive',
1189
+ relation: 'archived_by'
1190
+ },
1191
+ writer: 'ultra.archive',
1192
+ rootDir
1193
+ });
1194
+ }
1195
+ if (taskIds.length === 0)
1196
+ return;
1197
+ const packets = db.prepare(`SELECT * FROM worker_packets
1198
+ WHERE scope_type = 'task' AND status = 'assigned'
1199
+ AND scope_id IN (SELECT id FROM tasks WHERE change_id = ?)
1200
+ ORDER BY id`).all(change.id);
1201
+ for (const row of packets) {
1202
+ const read = readStableProjectFile(rootDir, row.packet_path);
1203
+ if (!row.file_digest || read.digest !== row.file_digest) {
1204
+ throw new ChangeWorkflowError('WORKER_PACKET_FILE_DRIFT', `cannot archive unbound Worker Packet ${ row.id }`, {
1205
+ path: row.packet_path,
1206
+ expected: row.file_digest || null,
1207
+ actual: read.digest
1208
+ });
1209
+ }
1210
+ const target = kernelArchiveWorkerPath(change, row.id);
1211
+ const published = writeManagedFile(rootDir, target, read.bytes);
1212
+ upsertArtifact(db, {
1213
+ change_id: change.id,
1214
+ task_id: row.scope_id,
1215
+ kind: 'archive_worker_packet',
1216
+ artifactPath: target,
1217
+ contentHash: published.digest,
1218
+ consumer_ref: {
1219
+ type: 'external',
1220
+ id: 'ultra.archive',
1221
+ relation: 'archived_by'
1222
+ },
1223
+ writer: 'ultra.archive',
1224
+ rootDir
1225
+ });
1226
+ }
1227
+ }
1228
+ function normalizedArchiveDocument(bytes, intent) {
1229
+ const text = bytes.toString('utf8');
1230
+ if (!Buffer.from(text, 'utf8').equals(bytes))
1231
+ return {
1232
+ bytes,
1233
+ semanticDigest: null
1234
+ };
1235
+ let document;
1236
+ try {
1237
+ document = JSON.parse(text);
1238
+ } catch {
1239
+ return {
1240
+ bytes,
1241
+ semanticDigest: null
1242
+ };
1243
+ }
1244
+ let semanticDigest = null;
1245
+ if (document?.schema_version === '1.0' && document.detail === 'full' && document.envelope && document.digest) {
1246
+ if (document.envelope.change?.id === intent.change_id) {
1247
+ document.envelope.change.status = 'archived';
1248
+ }
1249
+ semanticDigest = canonical.digest(document.envelope);
1250
+ document.digest = semanticDigest;
1251
+ } else if (document?.packet_version && document.packet_digest) {
1252
+ const {
1253
+ packet_digest: _prior,
1254
+ ...value
1255
+ } = document;
1256
+ semanticDigest = canonical.digest(value);
1257
+ document.packet_digest = semanticDigest;
1258
+ } else if (document?.schema_version === '1.0' && document.question && document.selection && document.digest) {
1259
+ const {
1260
+ digest: _prior,
1261
+ ...value
1262
+ } = document;
1263
+ semanticDigest = canonical.digest(value);
1264
+ document.digest = semanticDigest;
1265
+ } else if (document?.change?.id === intent.change_id) {
1266
+ document.change.status = 'archived';
1267
+ }
1268
+ return {
1269
+ bytes: Buffer.from(`${ JSON.stringify(document, null, 2) }\n`),
1270
+ semanticDigest
1271
+ };
1272
+ }
1273
+ function archiveRebindAuthorityValue(value, source, destination, digestMap) {
1274
+ if (typeof value === 'string') {
1275
+ return replaceArchiveAuthorityText(value, source, destination, digestMap, { normalizePath: true });
1276
+ }
1277
+ if (Array.isArray(value)) {
1278
+ return value.map(item => archiveRebindAuthorityValue(item, source, destination, digestMap));
1279
+ }
1280
+ if (value && typeof value === 'object') {
1281
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
1282
+ key,
1283
+ archiveRebindAuthorityValue(item, source, destination, digestMap)
1284
+ ]));
1285
+ }
1286
+ return value;
1287
+ }
1288
+ function buildArchiveRebindEntries(db, intent, rootDir) {
1289
+ const tree = walkStableProjectTree(rootDir, intent.destination, { ignore: relative => relative === `${ intent.destination }/${ archiveJournal.INTENT_FILE }` || relative === `${ intent.destination }/${ archiveJournal.REBIND_FILE }` });
1290
+ if (tree.unsafe.length > 0) {
1291
+ throw new ChangeWorkflowError('ARCHIVE_PATH_UNSAFE', `archive packet contains unsafe entries: ${ tree.unsafe.map(item => item.path).join(', ') }`, { unsafe: tree.unsafe });
1292
+ }
1293
+ const originalByPath = new Map();
1294
+ const semanticByPath = new Map();
1295
+ for (const artifact of db.prepare('SELECT path, digest, content_hash FROM artifacts WHERE change_id = ?').all(intent.change_id)) {
1296
+ const archivedPath = rebindRegistryPath(artifact.path, intent.source, intent.destination);
1297
+ const digest = artifact.digest || artifact.content_hash;
1298
+ if (archivedPath && digest)
1299
+ originalByPath.set(archivedPath, digest);
1300
+ }
1301
+ const contextPaths = new Set();
1302
+ for (const snapshot of db.prepare('SELECT manifest_path, manifest_hash FROM context_snapshots WHERE change_id = ?').all(intent.change_id)) {
1303
+ const archivedPath = rebindRegistryPath(snapshot.manifest_path, intent.source, intent.destination);
1304
+ if (archivedPath) {
1305
+ contextPaths.add(archivedPath);
1306
+ originalByPath.set(archivedPath, snapshot.manifest_hash);
1307
+ }
1308
+ }
1309
+ for (const row of db.prepare(`SELECT id, digest FROM decision_records
1310
+ WHERE scope_type = 'change' AND scope_id = ?`).all(intent.change_id)) {
1311
+ const archivedPath = `${ intent.destination }/decisions/${ row.id }.json`;
1312
+ semanticByPath.set(archivedPath, row.digest);
1313
+ }
1314
+ for (const row of db.prepare(`SELECT id, digest FROM context_envelopes
1315
+ WHERE (scope_type = 'change' AND scope_id = ?)
1316
+ OR (scope_type = 'task' AND scope_id IN (
1317
+ SELECT id FROM tasks WHERE change_id = ?
1318
+ ))`).all(intent.change_id, intent.change_id)) {
1319
+ semanticByPath.set(`${ intent.destination }/authority/context-envelopes/${ row.id }.json`, row.digest);
1320
+ }
1321
+ for (const row of db.prepare(`SELECT id, packet_digest FROM worker_packets
1322
+ WHERE scope_type = 'task' AND status = 'assigned'
1323
+ AND scope_id IN (SELECT id FROM tasks WHERE change_id = ?)`).all(intent.change_id)) {
1324
+ semanticByPath.set(`${ intent.destination }/authority/worker-packets/${ row.id }.json`, row.packet_digest);
1325
+ }
1326
+ const original = new Map();
1327
+ const pathRebound = new Map();
1328
+ for (const relative of tree.files) {
1329
+ const read = readStableProjectFile(rootDir, relative);
1330
+ original.set(relative, read.bytes);
1331
+ const text = read.bytes.toString('utf8');
1332
+ if (!Buffer.from(text, 'utf8').equals(read.bytes)) {
1333
+ if (read.bytes.includes(Buffer.from(intent.source))) {
1334
+ throw new ChangeWorkflowError('ARCHIVE_REBIND_UNSUPPORTED', `archive packet contains a binary active-root reference: ${ relative }`);
1335
+ }
1336
+ pathRebound.set(relative, read.bytes);
1337
+ continue;
1338
+ }
1339
+ let next = replaceArchiveAuthorityText(text, intent.source, intent.destination);
1340
+ if (contextPaths.has(relative)) {
1341
+ let manifest;
1342
+ try {
1343
+ manifest = JSON.parse(next);
1344
+ } catch (error) {
1345
+ throw new ChangeWorkflowError('ARCHIVE_CONTEXT_INVALID', `archived Context Manifest cannot be parsed: ${ relative }: ${ error.message }`);
1346
+ }
1347
+ if (manifest.change?.id !== intent.change_id) {
1348
+ throw new ChangeWorkflowError('ARCHIVE_CONTEXT_INVALID', `archived Context Manifest belongs to another change: ${ relative }`);
1349
+ }
1350
+ manifest.change.status = 'archived';
1351
+ next = `${ JSON.stringify(manifest, null, 2) }\n`;
1352
+ }
1353
+ pathRebound.set(relative, normalizedArchiveDocument(Buffer.from(next), intent).bytes);
1354
+ }
1355
+ let current = new Map(pathRebound);
1356
+ for (let iteration = 0; iteration < 20; iteration += 1) {
1357
+ const digestMap = new Map();
1358
+ for (const [relative, beforeDigest] of originalByPath) {
1359
+ const bytes = current.get(relative);
1360
+ if (bytes) {
1361
+ digestMap.set(beforeDigest, crypto.createHash('sha256').update(bytes).digest('hex'));
1362
+ }
1363
+ }
1364
+ for (const [relative, beforeDigest] of semanticByPath) {
1365
+ const bytes = current.get(relative);
1366
+ if (!bytes)
1367
+ continue;
1368
+ const normalized = normalizedArchiveDocument(bytes, intent);
1369
+ if (normalized.semanticDigest) {
1370
+ digestMap.set(beforeDigest, normalized.semanticDigest);
1371
+ }
1372
+ }
1373
+ const next = new Map();
1374
+ let changed = false;
1375
+ for (const [relative, baseBytes] of pathRebound) {
1376
+ const text = baseBytes.toString('utf8');
1377
+ if (!Buffer.from(text, 'utf8').equals(baseBytes)) {
1378
+ next.set(relative, baseBytes);
1379
+ continue;
1380
+ }
1381
+ const replaced = Buffer.from(replaceArchiveAuthorityText(text, intent.source, intent.destination, digestMap));
1382
+ const bytes = normalizedArchiveDocument(replaced, intent).bytes;
1383
+ next.set(relative, bytes);
1384
+ if (!bytes.equals(current.get(relative)))
1385
+ changed = true;
1386
+ }
1387
+ current = next;
1388
+ if (!changed)
1389
+ break;
1390
+ if (iteration === 19) {
1391
+ throw new ChangeWorkflowError('ARCHIVE_REBIND_CYCLE', 'archive packet digest references did not converge');
1392
+ }
1393
+ }
1394
+ return [...original.entries()].flatMap(([relative, before]) => {
1395
+ const after = current.get(relative);
1396
+ if (after.equals(before))
1397
+ return [];
1398
+ return [{
1399
+ relative_path: path.posix.relative(intent.destination, relative),
1400
+ before_digest: crypto.createHash('sha256').update(before).digest('hex'),
1401
+ after_digest: crypto.createHash('sha256').update(after).digest('hex'),
1402
+ before_base64: before.toString('base64'),
1403
+ after_base64: after.toString('base64')
1404
+ }];
1405
+ }).sort((left, right) => left.relative_path.localeCompare(right.relative_path));
1406
+ }
1407
+ function archiveRebindState(db, intent, rootDir) {
1408
+ const existing = archiveJournal.readArchiveRebind(rootDir, intent);
1409
+ const entries = existing?.entries || buildArchiveRebindEntries(db, intent, rootDir);
1410
+ const journal = archiveJournal.prepareArchiveRebind(rootDir, intent, existing ? null : entries);
1411
+ const byPath = new Map(journal.entries.map(entry => [
1412
+ `${ intent.destination }/${ entry.relative_path }`,
1413
+ entry
1414
+ ]));
1415
+ const digestMap = new Map(journal.entries.map(entry => [
1416
+ entry.before_digest,
1417
+ entry.after_digest
1418
+ ]));
1419
+ const semanticEntry = relativePath => byPath.get(relativePath);
1420
+ for (const row of db.prepare(`SELECT id, digest FROM decision_records
1421
+ WHERE scope_type = 'change' AND scope_id = ?`).all(intent.change_id)) {
1422
+ const entry = semanticEntry(`${ intent.destination }/decisions/${ row.id }.json`);
1423
+ if (!entry)
1424
+ continue;
1425
+ const document = JSON.parse(Buffer.from(entry.after_base64, 'base64').toString('utf8'));
1426
+ if (document.digest)
1427
+ digestMap.set(row.digest, document.digest);
1428
+ }
1429
+ for (const row of db.prepare(`SELECT id, digest FROM context_envelopes
1430
+ WHERE (scope_type = 'change' AND scope_id = ?)
1431
+ OR (scope_type = 'task' AND scope_id IN (
1432
+ SELECT id FROM tasks WHERE change_id = ?
1433
+ ))`).all(intent.change_id, intent.change_id)) {
1434
+ const entry = semanticEntry(`${ intent.destination }/authority/context-envelopes/${ row.id }.json`);
1435
+ if (!entry)
1436
+ continue;
1437
+ const document = JSON.parse(Buffer.from(entry.after_base64, 'base64').toString('utf8'));
1438
+ if (document.digest)
1439
+ digestMap.set(row.digest, document.digest);
1440
+ }
1441
+ for (const row of db.prepare(`SELECT id, packet_digest FROM worker_packets
1442
+ WHERE scope_type = 'task' AND status = 'assigned'
1443
+ AND scope_id IN (SELECT id FROM tasks WHERE change_id = ?)`).all(intent.change_id)) {
1444
+ const entry = semanticEntry(`${ intent.destination }/authority/worker-packets/${ row.id }.json`);
1445
+ if (!entry)
1446
+ continue;
1447
+ const document = JSON.parse(Buffer.from(entry.after_base64, 'base64').toString('utf8'));
1448
+ if (document.packet_digest) {
1449
+ digestMap.set(row.packet_digest, document.packet_digest);
1450
+ }
1451
+ }
1452
+ return {
1453
+ journal,
1454
+ byPath,
1455
+ digestMap
1456
+ };
1457
+ }
1458
+ function finalizeArchive(db, intent, {rootDir, journalIntent = intent}) {
1459
+ const change = readChange(db, intent.change_id);
1460
+ if (!change)
1461
+ throw new ChangeWorkflowError('CHANGE_NOT_FOUND', `change ${ intent.change_id } not found`);
1462
+ try {
1463
+ archiveJournal.verifyArchiveIntent(rootDir, journalIntent, { location: 'destination' });
1464
+ } catch (error) {
1465
+ throw new ChangeWorkflowError('ARCHIVE_PATH_UNSAFE', `archive transition cannot trust its physical destination: ${ error.message }`, { cause: error.code || error.message });
1466
+ }
1467
+ const relative = artifacts.normalizeRelativePath(intent.destination);
1468
+ const destination = safeRelativePath(rootDir, relative);
1469
+ if (change.status === 'archived' && artifacts.normalizeRelativePath(change.artifact_root) === relative) {
1470
+ return {
1471
+ change,
1472
+ archive_path: destination
1473
+ };
1474
+ }
1475
+ const acceptedStatuses = [
1476
+ 'active',
1477
+ 'ready'
1478
+ ];
1479
+ if (!acceptedStatuses.includes(change.status)) {
1480
+ throw new ChangeWorkflowError('CHANGE_NOT_READY', `change ${ change.id } must have complete accepted checkpoints to finish archive`);
1481
+ }
1482
+ const rebind = archiveRebindState(db, journalIntent, rootDir);
1483
+ const archivedReconciliationPath = rebindRegistryPath(intent.reconciliation_path, intent.source, relative);
1484
+ const reconciliationRead = readStableProjectFile(rootDir, archivedReconciliationPath, { encoding: 'utf8' });
1485
+ let reconciliationManifest;
1486
+ try {
1487
+ reconciliationManifest = JSON.parse(reconciliationRead.text);
1488
+ } catch (error) {
1489
+ throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_INVALID', `archived reconciliation manifest is invalid JSON: ${ error.message }`);
1490
+ }
1491
+ if (!validateReconciliationSchema(reconciliationManifest) || reconciliationManifest.change_id !== change.id) {
1492
+ throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_INVALID', 'archived reconciliation manifest no longer matches its accepted schema and Change', { errors: validateReconciliationSchema.errors || [] });
1493
+ }
1494
+ const reconciliationDigest = reconciliationRead.digest;
1495
+ let summaryReader;
1496
+ try {
1497
+ summaryReader = openStableProjectRead(rootDir, `${ relative }/archive-summary.md`);
1498
+ } catch (error) {
1499
+ throw new ChangeWorkflowError('ARCHIVE_PATH_UNSAFE', 'archive summary cannot be read safely', { cause: error.code || error.message });
1500
+ }
1501
+ const baselineHealthBefore = baselines.inspectBaseline(db, { rootDir });
1502
+ const breakGlass = Boolean(baselineHealthBefore.status !== 'pass' && change.kind === 'incident' && change.baseline_bypass?.mode === 'incident_break_glass');
1503
+ try {
1504
+ ops.tx(db, () => {
1505
+ db.prepare('UPDATE changes SET status = \'archived\', artifact_root = ?, updated_at = ?, closed_at = ? WHERE id = ?').run(relative, nowIso(), nowIso(), change.id);
1506
+ const artifactRows = db.prepare(`SELECT id, path, digest, content_hash, before_digest, metadata_json, provenance_json
1507
+ FROM artifacts WHERE change_id = ?`).all(change.id);
1508
+ for (const artifact of artifactRows) {
1509
+ const archivedPath = rebindRegistryPath(artifact.path, intent.source, relative);
1510
+ if (!archivedPath)
1511
+ continue;
1512
+ const rebound = rebind.byPath.get(archivedPath);
1513
+ if (rebound) {
1514
+ db.prepare(`UPDATE artifacts
1515
+ SET digest = ?, content_hash = ?, before_digest = COALESCE(before_digest, ?),
1516
+ after_digest = ?, metadata_json = ?, provenance_json = ?
1517
+ WHERE id = ?`).run(rebound.after_digest, rebound.after_digest, artifact.digest || artifact.content_hash || rebound.before_digest, rebound.after_digest, JSON.stringify(archiveRebindAuthorityValue(parseJson(artifact.metadata_json || '{}', 'artifacts.metadata_json'), intent.source, relative, rebind.digestMap)), JSON.stringify(archiveRebindAuthorityValue(parseJson(artifact.provenance_json || '{}', 'artifacts.provenance_json'), intent.source, relative, rebind.digestMap)), artifact.id);
1518
+ }
1519
+ artifacts.moveArtifactInTx(db, artifact.id, archivedPath, { rootDir });
1520
+ }
1521
+ const workflowOutputs = db.prepare(`SELECT ws.run_id, ws.step_id, ws.outputs_json, ws.evidence_json,
1522
+ ws.decisions_json, ws.semantic_records_json, ws.blockers_json
1523
+ FROM workflow_steps ws JOIN workflow_runs wr ON wr.id = ws.run_id
1524
+ WHERE wr.change_id = ?`).all(change.id);
1525
+ for (const step of workflowOutputs) {
1526
+ const rebound = Object.fromEntries([
1527
+ [
1528
+ 'outputs_json',
1529
+ parseJson(step.outputs_json, 'workflow_steps.outputs_json')
1530
+ ],
1531
+ [
1532
+ 'evidence_json',
1533
+ parseJson(step.evidence_json, 'workflow_steps.evidence_json')
1534
+ ],
1535
+ [
1536
+ 'decisions_json',
1537
+ parseJson(step.decisions_json, 'workflow_steps.decisions_json')
1538
+ ],
1539
+ [
1540
+ 'semantic_records_json',
1541
+ parseJson(step.semantic_records_json, 'workflow_steps.semantic_records_json')
1542
+ ],
1543
+ [
1544
+ 'blockers_json',
1545
+ parseJson(step.blockers_json, 'workflow_steps.blockers_json')
1546
+ ]
1547
+ ].map(([field, value]) => [
1548
+ field,
1549
+ JSON.stringify(archiveRebindAuthorityValue(value, intent.source, relative, rebind.digestMap))
1550
+ ]));
1551
+ db.prepare(`UPDATE workflow_steps
1552
+ SET outputs_json = ?, evidence_json = ?, decisions_json = ?,
1553
+ semantic_records_json = ?, blockers_json = ?, updated_at = ?
1554
+ WHERE run_id = ? AND step_id = ?`).run(rebound.outputs_json, rebound.evidence_json, rebound.decisions_json, rebound.semantic_records_json, rebound.blockers_json, nowIso(), step.run_id, step.step_id);
1555
+ }
1556
+ const contextRows = db.prepare(`SELECT id, manifest_path, manifest_hash, context_json
1557
+ FROM context_snapshots WHERE change_id = ?`).all(change.id);
1558
+ for (const snapshot of contextRows) {
1559
+ const archivedPath = rebindRegistryPath(snapshot.manifest_path, intent.source, relative);
1560
+ if (!archivedPath)
1561
+ continue;
1562
+ const rebound = rebind.byPath.get(archivedPath);
1563
+ db.prepare(`UPDATE context_snapshots
1564
+ SET manifest_path = ?, manifest_hash = ?, context_json = ? WHERE id = ?`).run(archivedPath, rebound?.after_digest || snapshot.manifest_hash, JSON.stringify(archiveRebindAuthorityValue(parseJson(snapshot.context_json || '{}', 'context_snapshots.context_json'), intent.source, relative, rebind.digestMap)), snapshot.id);
1565
+ }
1566
+ for (const decision of db.prepare(`SELECT id FROM decision_records
1567
+ WHERE scope_type = 'change' AND scope_id = ?`).all(change.id)) {
1568
+ const archivedPath = `${ relative }/decisions/${ decision.id }.json`;
1569
+ const rebound = rebind.byPath.get(archivedPath);
1570
+ if (!rebound) {
1571
+ throw new ChangeWorkflowError('ARCHIVE_DECISION_MISSING', `archive is missing Decision Record ${ decision.id }`);
1572
+ }
1573
+ const document = JSON.parse(Buffer.from(rebound.after_base64, 'base64').toString('utf8'));
1574
+ db.prepare(`UPDATE decision_records
1575
+ SET question = ?, recommendation = ?, selection = ?,
1576
+ effects_json = ?, non_goals_json = ?, owner = ?, source = ?,
1577
+ provenance_json = ?, applied_refs_json = ?, digest = ?,
1578
+ artifact_path = ?, updated_at = ?
1579
+ WHERE id = ?`).run(document.question, document.recommendation, document.selection, JSON.stringify(document.effects || {}), JSON.stringify(document.non_goals || []), document.owner, document.source, JSON.stringify(document.provenance || {}), JSON.stringify(document.applied_refs || []), document.digest, archivedPath, nowIso(), decision.id);
1580
+ }
1581
+ for (const envelope of db.prepare(`SELECT id FROM context_envelopes
1582
+ WHERE (scope_type = 'change' AND scope_id = ?)
1583
+ OR (scope_type = 'task' AND scope_id IN (
1584
+ SELECT id FROM tasks WHERE change_id = ?
1585
+ ))`).all(change.id, change.id)) {
1586
+ const archivedPath = `${ relative }/authority/context-envelopes/${ envelope.id }.json`;
1587
+ const rebound = rebind.byPath.get(archivedPath);
1588
+ if (!rebound) {
1589
+ throw new ChangeWorkflowError('ARCHIVE_CONTEXT_MISSING', `archive is missing Context Envelope ${ envelope.id }`);
1590
+ }
1591
+ const document = JSON.parse(Buffer.from(rebound.after_base64, 'base64').toString('utf8'));
1592
+ db.prepare(`UPDATE context_envelopes
1593
+ SET digest = ?, file_digest = ?, payload_json = ?, artifact_path = ?
1594
+ WHERE id = ?`).run(document.digest, rebound.after_digest, JSON.stringify(document.envelope), archivedPath, envelope.id);
1595
+ }
1596
+ for (const packet of db.prepare(`SELECT id FROM worker_packets
1597
+ WHERE scope_type = 'task' AND status = 'assigned'
1598
+ AND scope_id IN (SELECT id FROM tasks WHERE change_id = ?)`).all(change.id)) {
1599
+ const archivedPath = `${ relative }/authority/worker-packets/${ packet.id }.json`;
1600
+ const rebound = rebind.byPath.get(archivedPath);
1601
+ if (!rebound) {
1602
+ throw new ChangeWorkflowError('ARCHIVE_WORKER_PACKET_MISSING', `archive is missing Worker Packet ${ packet.id }`);
1603
+ }
1604
+ const document = JSON.parse(Buffer.from(rebound.after_base64, 'base64').toString('utf8'));
1605
+ const decisionDigest = canonical.digest((document.accepted_decisions || []).map(decision => ({
1606
+ id: decision.id,
1607
+ digest: decision.digest,
1608
+ status: 'accepted'
1609
+ })));
1610
+ db.prepare(`UPDATE worker_packets
1611
+ SET context_digest = ?, task_digest = ?, decision_digest = ?,
1612
+ packet_digest = ?, file_digest = ?, packet_path = ?, output_path = ?
1613
+ WHERE id = ?`).run(document.context_envelope?.digest || null, document.task?.digest || null, decisionDigest, document.packet_digest, rebound.after_digest, archivedPath, document.output?.path, packet.id);
1614
+ }
1615
+ const checkpointRows = db.prepare(`SELECT *
1616
+ FROM stage_checkpoints
1617
+ WHERE (scope_type = 'change' AND scope_id = ?)
1618
+ OR (scope_type = 'task' AND scope_id IN (
1619
+ SELECT id FROM tasks WHERE change_id = ?
1620
+ ))
1621
+ ORDER BY revision`).all(change.id, change.id);
1622
+ for (const row of checkpointRows) {
1623
+ const value = {
1624
+ stage: row.stage,
1625
+ scope_type: row.scope_type,
1626
+ scope_id: row.scope_id,
1627
+ revision: row.revision,
1628
+ payload: archiveRebindAuthorityValue(parseJson(row.payload_json || '{}', 'stage_checkpoints.payload_json'), intent.source, relative, rebind.digestMap),
1629
+ evidence: archiveRebindAuthorityValue(parseJson(row.evidence_json || '[]', 'stage_checkpoints.evidence_json'), intent.source, relative, rebind.digestMap),
1630
+ diagnostics: archiveRebindAuthorityValue(parseJson(row.diagnostics_json || '[]', 'stage_checkpoints.diagnostics_json'), intent.source, relative, rebind.digestMap),
1631
+ context_envelope_id: row.context_envelope_id,
1632
+ supersedes_id: row.supersedes_id
1633
+ };
1634
+ db.prepare(`UPDATE stage_checkpoints
1635
+ SET payload_json = ?, evidence_json = ?, diagnostics_json = ?, digest = ?,
1636
+ updated_at = ?
1637
+ WHERE id = ?`).run(JSON.stringify(value.payload), JSON.stringify(value.evidence), JSON.stringify(value.diagnostics), stageCheckpoints.checkpointDigest(value), nowIso(), row.id);
1638
+ }
1639
+ for (const task of db.prepare(`SELECT id, context_refs_json, docs_impact_json, ownership_json
1640
+ FROM tasks WHERE change_id = ?`).all(change.id)) {
1641
+ db.prepare(`UPDATE tasks
1642
+ SET context_refs_json = ?, docs_impact_json = ?, ownership_json = ?,
1643
+ updated_at = ?
1644
+ WHERE id = ?`).run(JSON.stringify(archiveRebindAuthorityValue(parseJson(task.context_refs_json || '[]', 'tasks.context_refs_json'), intent.source, relative, rebind.digestMap)), JSON.stringify(archiveRebindAuthorityValue(parseJson(task.docs_impact_json || '{}', 'tasks.docs_impact_json'), intent.source, relative, rebind.digestMap)), JSON.stringify(archiveRebindAuthorityValue(parseJson(task.ownership_json || '{}', 'tasks.ownership_json'), intent.source, relative, rebind.digestMap)), nowIso(), task.id);
1645
+ }
1646
+ for (const event of db.prepare(`SELECT id, payload_json FROM events
1647
+ WHERE change_id = ?
1648
+ OR task_id IN (SELECT id FROM tasks WHERE change_id = ?)`).all(change.id, change.id)) {
1649
+ db.prepare('UPDATE events SET payload_json = ? WHERE id = ?').run(JSON.stringify(archiveRebindAuthorityValue(parseJson(event.payload_json || '{}', 'events.payload_json'), intent.source, relative, rebind.digestMap)), event.id);
1650
+ }
1651
+ for (const run of db.prepare(`SELECT id, metadata_json, blockers_json, summary_json, approval_json
1652
+ FROM workflow_runs WHERE change_id = ?`).all(change.id)) {
1653
+ db.prepare(`UPDATE workflow_runs
1654
+ SET metadata_json = ?, blockers_json = ?, summary_json = ?, approval_json = ?
1655
+ WHERE id = ?`).run(JSON.stringify(archiveRebindAuthorityValue(parseJson(run.metadata_json || '{}', 'workflow_runs.metadata_json'), intent.source, relative, rebind.digestMap)), JSON.stringify(archiveRebindAuthorityValue(parseJson(run.blockers_json || '[]', 'workflow_runs.blockers_json'), intent.source, relative, rebind.digestMap)), JSON.stringify(archiveRebindAuthorityValue(parseJson(run.summary_json || '{}', 'workflow_runs.summary_json'), intent.source, relative, rebind.digestMap)), run.approval_json === null ? null : JSON.stringify(archiveRebindAuthorityValue(parseJson(run.approval_json, 'workflow_runs.approval_json'), intent.source, relative, rebind.digestMap)), run.id);
1656
+ }
1657
+ db.prepare(`UPDATE trace_links
1658
+ SET source_ref = REPLACE(source_ref, ?, ?),
1659
+ target_ref = REPLACE(target_ref, ?, ?)
1660
+ WHERE change_id = ?`).run(intent.source, relative, intent.source, relative, change.id);
1661
+ upsertArtifact(db, {
1662
+ change_id: change.id,
1663
+ kind: 'archive_summary',
1664
+ artifactPath: `${ relative }/archive-summary.md`,
1665
+ contentHash: summaryReader.digest,
1666
+ metadata: {
1667
+ baseline_updates: intent.baseline_updates,
1668
+ no_baseline_change_reason: intent.no_baseline_change_reason
1669
+ },
1670
+ terminal: true,
1671
+ rootDir
1672
+ });
1673
+ upsertArtifact(db, {
1674
+ change_id: change.id,
1675
+ kind: 'baseline_reconciliation',
1676
+ artifactPath: archivedReconciliationPath,
1677
+ contentHash: reconciliationDigest,
1678
+ metadata: {
1679
+ schema: reconciliationManifest.$schema,
1680
+ semantic_changes: reconciliationManifest.semantic_changes.map(item => item.id),
1681
+ resolved_gap_ids: reconciliationManifest.resolved_gap_ids,
1682
+ resolved_unknowns: reconciliationManifest.resolved_unknowns
1683
+ },
1684
+ terminal: true,
1685
+ rootDir
1686
+ });
1687
+ for (const artifact of db.prepare('SELECT id FROM artifacts WHERE change_id = ? ORDER BY id').all(change.id)) {
1688
+ artifacts.setArtifactStatusInTx(db, artifact.id, 'archived');
1689
+ }
1690
+ if (breakGlass) {
1691
+ const currentBaseline = baselines.readBaseline(db);
1692
+ if (currentBaseline) {
1693
+ baselines.appendGapInTx(db, {
1694
+ baseline_id: currentBaseline.id,
1695
+ gap: {
1696
+ id: `incident-${ change.id }-reconciliation`,
1697
+ category: 'baseline_blocker',
1698
+ status: 'open',
1699
+ blocking: true,
1700
+ summary: `Incident ${ change.id } was archived under break-glass and requires baseline reconciliation.`,
1701
+ evidence_refs: [relative],
1702
+ owner: change.baseline_bypass.approved_by
1703
+ }
1704
+ });
1705
+ }
1706
+ } else {
1707
+ baselines.reconcileBaseline(db, {
1708
+ baseline_updates: intent.baseline_updates,
1709
+ change_id: change.id,
1710
+ reconciliation: reconciliationManifest,
1711
+ accept_delivery_worktree: true
1712
+ }, { rootDir });
1713
+ const baselineHealth = baselines.inspectBaseline(db, { rootDir });
1714
+ if (baselineHealth.blockers.length > 0) {
1715
+ throw new ChangeWorkflowError('BASELINE_RECONCILIATION_INCOMPLETE', `baseline reconciliation remains incomplete: ${ baselineHealth.blockers.join(', ') }`, { blockers: baselineHealth.blockers });
1716
+ }
1717
+ }
1718
+ ops.appendEventInTx(db, {
1719
+ type: 'change_archived',
1720
+ change_id: change.id,
1721
+ payload: {
1722
+ archive_path: relative,
1723
+ baseline_updates: intent.baseline_updates,
1724
+ no_baseline_change_reason: intent.no_baseline_change_reason,
1725
+ reconciliation_path: archivedReconciliationPath,
1726
+ reconciliation_digest: reconciliationDigest,
1727
+ baseline_bypass: breakGlass
1728
+ }
1729
+ });
1730
+ summaryReader.verify();
1731
+ try {
1732
+ archiveJournal.verifyArchiveIntent(rootDir, journalIntent, { location: 'destination' });
1733
+ } catch (error) {
1734
+ throw new ChangeWorkflowError('ARCHIVE_PATH_UNSAFE', `archive transition changed physical identity before commit: ${ error.message }`, { cause: error.code || error.message });
1735
+ }
1736
+ });
1737
+ } finally {
1738
+ summaryReader.close();
1739
+ }
1740
+ return {
1741
+ change: readChange(db, change.id),
1742
+ archive_path: destination,
1743
+ baseline_bypass: breakGlass
1744
+ };
1745
+ }
1746
+ function archiveChange(db, input, {rootDir = process.cwd(), deferDeliveryCleanup = false} = {}) {
1747
+ const change = readChange(db, input.id);
1748
+ if (!change)
1749
+ throw new ChangeWorkflowError('CHANGE_NOT_FOUND', `change ${ input.id } not found`);
1750
+ if (!input.summary || String(input.summary).trim().length < 3) {
1751
+ throw new ChangeWorkflowError('VALIDATION_ERROR', 'archive summary required');
1752
+ }
1753
+ const allowedArchiveStatuses = [
1754
+ 'active',
1755
+ 'ready',
1756
+ 'archived'
1757
+ ];
1758
+ if (!allowedArchiveStatuses.includes(change.status)) {
1759
+ throw new ChangeWorkflowError('CHANGE_NOT_READY', `change ${ input.id } must converge before archive`);
1760
+ }
1761
+ if (change.status === 'archived') {
1762
+ if (!deferDeliveryCleanup) {
1763
+ deliveryTransaction.completeDeliveryTransaction({
1764
+ rootDir,
1765
+ changeId: change.id
1766
+ });
1767
+ }
1768
+ return {
1769
+ change,
1770
+ archive_path: safeRelativePath(rootDir, change.artifact_root),
1771
+ idempotent: true
1772
+ };
1773
+ }
1774
+ let packet = null;
1775
+ if (changePacket.readDeltaAuthority(db, change.id)) {
1776
+ packet = changePacket.deliveryEntries(db, change, { rootDir });
1777
+ }
1778
+ const requestedUpdates = Array.isArray(input.baseline_updates) ? input.baseline_updates.map(item => artifacts.normalizeRelativePath(item)) : null;
1779
+ const updates = packet ? packet.baseline_updates : requestedUpdates || [];
1780
+ if (packet && requestedUpdates && JSON.stringify([...requestedUpdates].sort()) !== JSON.stringify([...updates].sort())) {
1781
+ throw new ChangeWorkflowError('BASELINE_RECONCILIATION_CONFLICT', 'baseline_updates must match the typed Change delta exactly');
1782
+ }
1783
+ const requestedNoChange = input.no_baseline_change_reason && String(input.no_baseline_change_reason).trim();
1784
+ const noChangeReason = packet ? packet.delta.value.no_semantic_change_reason || requestedNoChange || null : requestedNoChange;
1785
+ if (updates.length === 0 && !noChangeReason) {
1786
+ throw new ChangeWorkflowError('BASELINE_RECONCILIATION_REQUIRED', 'baseline updates or no-change reason required');
1787
+ }
1788
+ const reconciliation = packet ? generatePacketReconciliation(db, change, packet, rootDir) : updates.length === 0 && !String(input.reconciliation_path || '').trim() ? generateCallerNoChangeReconciliation(db, change, {
1789
+ summary: input.summary,
1790
+ no_baseline_change_reason: noChangeReason
1791
+ }, rootDir) : readReconciliationManifest(db, change, input, rootDir);
1792
+ materializeKernelArchiveAuthority(db, change, rootDir);
1793
+ let delivery = null;
1794
+ let prepared = null;
1795
+ try {
1796
+ if (packet) {
1797
+ delivery = deliveryTransaction.beginDeliveryTransaction({
1798
+ rootDir,
1799
+ changeId: change.id,
1800
+ entries: packet.entries
1801
+ });
1802
+ }
1803
+ for (const file of updates) {
1804
+ if (!fs.existsSync(safeRelativePath(rootDir, file))) {
1805
+ throw new ChangeWorkflowError('BASELINE_FILE_MISSING', `baseline update missing: ${ file }`);
1806
+ }
1807
+ }
1808
+ prepared = archiveJournal.prepareArchiveMove({
1809
+ rootDir,
1810
+ change,
1811
+ summary: String(input.summary),
1812
+ baselineUpdates: updates,
1813
+ noBaselineChangeReason: noChangeReason || null,
1814
+ reconciliationPath: reconciliation.relative,
1815
+ reconciliationDigest: reconciliation.journal_intent?.reconciliation_digest || reconciliation.digest,
1816
+ reconciliationManifest: reconciliation.journal_intent?.reconciliation_manifest || reconciliation.manifest
1817
+ });
1818
+ } catch (error) {
1819
+ if (delivery) {
1820
+ try {
1821
+ deliveryTransaction.rollbackDeliveryTransaction({
1822
+ rootDir,
1823
+ changeId: change.id
1824
+ });
1825
+ } catch (rollbackError) {
1826
+ throw new ChangeWorkflowError('DELIVERY_RECOVERY_REQUIRED', error.message, {
1827
+ cause: error.code || error.message,
1828
+ rollback: rollbackError.code || rollbackError.message
1829
+ });
1830
+ }
1831
+ }
1832
+ throw error;
1833
+ }
1834
+ const authoritativeIntent = {
1835
+ ...prepared.intent,
1836
+ reconciliation_digest: reconciliation.digest,
1837
+ reconciliation_manifest: reconciliation.manifest
1838
+ };
1839
+ let result;
1840
+ try {
1841
+ result = finalizeArchive(db, authoritativeIntent, {
1842
+ rootDir,
1843
+ journalIntent: prepared.intent
1844
+ });
1845
+ } catch (error) {
1846
+ try {
1847
+ archiveJournal.rollbackArchiveIntent(rootDir, prepared.intent);
1848
+ } catch (rollbackError) {
1849
+ throw new ChangeWorkflowError('ARCHIVE_RECOVERY_REQUIRED', error.message, {
1850
+ cause: error.code || error.message,
1851
+ rollback: rollbackError.code || rollbackError.message
1852
+ });
1853
+ }
1854
+ if (delivery) {
1855
+ try {
1856
+ deliveryTransaction.rollbackDeliveryTransaction({
1857
+ rootDir,
1858
+ changeId: change.id
1859
+ });
1860
+ } catch (rollbackError) {
1861
+ throw new ChangeWorkflowError('DELIVERY_RECOVERY_REQUIRED', error.message, {
1862
+ cause: error.code || error.message,
1863
+ rollback: rollbackError.code || rollbackError.message
1864
+ });
1865
+ }
1866
+ }
1867
+ throw error;
1868
+ }
1869
+ try {
1870
+ archiveJournal.completeArchiveIntent(rootDir, prepared.intent);
1871
+ } catch (error) {
1872
+ result.recovery_warning = `ARCHIVE_JOURNAL_CLEANUP_PENDING:${ error.message }`;
1873
+ }
1874
+ if (delivery && !deferDeliveryCleanup) {
1875
+ try {
1876
+ deliveryTransaction.completeDeliveryTransaction({
1877
+ rootDir,
1878
+ changeId: change.id
1879
+ });
1880
+ } catch (error) {
1881
+ result.recovery_warning = [
1882
+ result.recovery_warning,
1883
+ `DELIVERY_JOURNAL_CLEANUP_PENDING:${ error.message }`
1884
+ ].filter(Boolean).join(';');
1885
+ }
1886
+ }
1887
+ return result;
1888
+ }
1889
+ function recoverInterruptedArchives(db, {
1890
+ rootDir = process.cwd()
1891
+ } = {}) {
1892
+ const records = archiveJournal.listArchiveIntents(rootDir);
1893
+ const result = {
1894
+ found: records.length,
1895
+ resumed: 0,
1896
+ rolled_back: 0,
1897
+ cleaned: 0,
1898
+ failed: 0,
1899
+ items: []
1900
+ };
1901
+ for (const record of records) {
1902
+ if (record.error) {
1903
+ result.failed += 1;
1904
+ result.items.push({
1905
+ file: record.file,
1906
+ status: 'failed',
1907
+ error: record.error.code || record.error.message
1908
+ });
1909
+ continue;
1910
+ }
1911
+ const change = readChange(db, record.intent.change_id);
1912
+ try {
1913
+ if (change?.status === 'archived' && change.artifact_root === record.intent.destination) {
1914
+ archiveJournal.completeArchiveIntent(rootDir, record.intent);
1915
+ result.cleaned += 1;
1916
+ result.items.push({
1917
+ change_id: change.id,
1918
+ status: 'cleaned'
1919
+ });
1920
+ } else if ([
1921
+ 'active',
1922
+ 'ready'
1923
+ ].includes(change?.status) && record.location === 'destination') {
1924
+ const reconciliation = readReconciliationManifest(db, change, {
1925
+ reconciliation_path: record.intent.reconciliation_path,
1926
+ baseline_updates: record.intent.baseline_updates
1927
+ }, rootDir);
1928
+ const authoritativeIntent = {
1929
+ ...record.intent,
1930
+ reconciliation_digest: reconciliation.digest,
1931
+ reconciliation_manifest: reconciliation.manifest
1932
+ };
1933
+ finalizeArchive(db, authoritativeIntent, {
1934
+ rootDir,
1935
+ journalIntent: record.intent
1936
+ });
1937
+ archiveJournal.completeArchiveIntent(rootDir, record.intent);
1938
+ result.resumed += 1;
1939
+ result.items.push({
1940
+ change_id: change.id,
1941
+ status: 'resumed'
1942
+ });
1943
+ } else {
1944
+ archiveJournal.rollbackArchiveIntent(rootDir, record.intent);
1945
+ result.rolled_back += 1;
1946
+ result.items.push({
1947
+ change_id: record.intent.change_id,
1948
+ status: 'rolled_back'
1949
+ });
1950
+ }
1951
+ } catch (error) {
1952
+ try {
1953
+ archiveJournal.rollbackArchiveIntent(rootDir, record.intent);
1954
+ result.rolled_back += 1;
1955
+ result.items.push({
1956
+ change_id: record.intent.change_id,
1957
+ status: 'rolled_back',
1958
+ error: error.code || error.message
1959
+ });
1960
+ } catch (rollbackError) {
1961
+ result.failed += 1;
1962
+ result.items.push({
1963
+ change_id: record.intent.change_id,
1964
+ status: 'failed',
1965
+ error: `${ error.code || error.message }; ${ rollbackError.code || rollbackError.message }`
1966
+ });
1967
+ }
1968
+ }
1969
+ }
1970
+ const archivedChangeIds = new Set(db.prepare('SELECT id FROM changes WHERE status = \'archived\'').all().map(row => row.id));
1971
+ result.delivery = deliveryTransaction.recoverDeliveryTransactions({
1972
+ rootDir,
1973
+ archivedChangeIds
1974
+ });
1975
+ result.failed += result.delivery.failed;
1976
+ return result;
1977
+ }
1978
+ function supersedeKernelChange(db, input = {}, options = {}) {
1979
+ const sourceId = typeof input.id === 'string' ? input.id.trim() : '';
1980
+ const successorId = typeof input.successor_id === 'string' ? input.successor_id.trim() : '';
1981
+ if (!CHANGE_ID.test(sourceId) || !CHANGE_ID.test(successorId) || sourceId === successorId) {
1982
+ throw new ChangeWorkflowError('VALIDATION_ERROR', 'supersede requires distinct valid id and successor_id values');
1983
+ }
1984
+ const source = readChange(db, sourceId);
1985
+ if (!source) {
1986
+ throw new ChangeWorkflowError('CHANGE_NOT_FOUND', `change ${ sourceId } not found`);
1987
+ }
1988
+ if (![
1989
+ 'archived',
1990
+ 'cancelled'
1991
+ ].includes(source.status)) {
1992
+ throw new ChangeWorkflowError('CHANGE_SUCCESSOR_NOT_ALLOWED', `change ${ sourceId } is ${ source.status }; revise mutable Changes instead`);
1993
+ }
1994
+ const created = createChange(db, {
1995
+ id: successorId,
1996
+ title: input.title || `${ source.title } successor`,
1997
+ kind: input.kind || source.kind,
1998
+ intent: input.intent || source.intent,
1999
+ docs_impact: source.docs_impact,
2000
+ provider_refs: source.provider_refs,
2001
+ contract: source.contract,
2002
+ classification: source.classification,
2003
+ research_disposition: source.research_disposition,
2004
+ base_commit: source.base_commit,
2005
+ supersedes_id: source.id
2006
+ }, options);
2007
+ return {
2008
+ source,
2009
+ successor: created.change,
2010
+ intent_path: created.intent_path,
2011
+ diagnostics: created.diagnostics || []
2012
+ };
2013
+ }
2014
+ module.exports = {
2015
+ ChangeWorkflowError,
2016
+ createChange,
2017
+ supersedeChange: supersedeKernelChange,
2018
+ updateChange,
2019
+ archiveChange,
2020
+ readChange,
2021
+ listChanges,
2022
+ recordDelta,
2023
+ recordDocumentationReconciliation,
2024
+ normalizeDocsImpact,
2025
+ normalizeProviderRefs,
2026
+ normalizeBaselineBypass,
2027
+ assertTaskCreationAllowed,
2028
+ recoverInterruptedArchives
2029
+ };