ultra-builder-pro-cli 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ultra-template/docs/research/README.md +21 -44
- package/.ultra-template/specs/architecture.md +54 -223
- package/.ultra-template/specs/discovery.md +41 -228
- package/.ultra-template/specs/product.md +44 -200
- package/.ultra-template/tasks/contexts/TEMPLATE.md +36 -0
- package/.ultra-template/tasks/tasks.json +6 -0
- package/CHANGELOG.md +94 -1
- package/README.md +37 -9
- package/adapters/_shared/codex-assets.cjs +1 -1
- package/adapters/_shared/handbook.cjs +5 -4
- package/adapters/_shared/tests/handbook.test.cjs +4 -0
- package/adapters/opencode.js +8 -2
- package/adapters/tests/codex.test.cjs +10 -2
- package/adapters/tests/kimi-hook.test.cjs +1 -1
- package/adapters/tests/opencode.test.cjs +36 -1
- package/commands/ultra-init.md +3 -3
- package/docs/AGENT-CONTEXT.md +17 -8
- package/docs/ARCHITECTURE.md +51 -8
- package/docs/PLAN.zh-CN.md +9 -6
- package/docs/ROADMAP.md +5 -3
- package/docs/RUNTIME-COMPAT-MATRIX.md +8 -5
- package/docs/STATE-DB-ACCESS-POLICY.md +19 -13
- package/docs/USER-HANDBOOK-CONTRACT.md +9 -5
- package/hooks/README.md +4 -3
- package/hooks/adapters/codex.py +1 -1
- package/hooks/adapters/kimi.py +1 -1
- package/hooks/context_spine.py +153 -16
- package/hooks/health_check.py +11 -0
- package/hooks/pre_stop_check.py +6 -5
- package/mcp-server/lib/archive-journal.cjs +192 -0
- package/mcp-server/lib/archive-journal.test.cjs +35 -0
- package/mcp-server/lib/baseline-workflow.cjs +781 -0
- package/mcp-server/lib/baseline-workflow.test.cjs +477 -0
- package/mcp-server/lib/change-workflow.cjs +256 -85
- package/mcp-server/lib/change-workflow.test.cjs +436 -6
- package/mcp-server/lib/context-spine.cjs +74 -9
- package/mcp-server/lib/context-spine.test.cjs +20 -0
- package/mcp-server/lib/doctor.cjs +28 -1
- package/mcp-server/lib/doctor.test.cjs +63 -1
- package/mcp-server/lib/init-project.cjs +334 -16
- package/mcp-server/lib/provider-refs.cjs +42 -0
- package/mcp-server/lib/spec-learning.test.cjs +5 -0
- package/mcp-server/lib/state-authority.cjs +9 -3
- package/mcp-server/lib/state-db.cjs +202 -21
- package/mcp-server/lib/state-db.test.cjs +140 -3
- package/mcp-server/lib/state-ops.cjs +68 -6
- package/mcp-server/lib/task-expander.cjs +7 -1
- package/mcp-server/lib/task-expander.test.cjs +71 -0
- package/mcp-server/server.cjs +62 -6
- package/mcp-server/tests/init-project.test.cjs +216 -9
- package/mcp-server/tests/migrate.test.cjs +33 -0
- package/mcp-server/tests/server-contract.test.cjs +478 -3
- package/mcp-server/tests/state-db.test.cjs +23 -2
- package/mcp-server/tests/state-ops.test.cjs +108 -0
- package/package.json +1 -1
- package/skills/ultra-change/SKILL.md +32 -13
- package/skills/ultra-deliver/SKILL.md +19 -5
- package/skills/ultra-dev/SKILL.md +8 -2
- package/skills/ultra-doctor/SKILL.md +30 -9
- package/skills/ultra-init/SKILL.md +107 -66
- package/skills/ultra-init/references/brownfield-adoption.md +93 -0
- package/skills/ultra-plan/SKILL.md +15 -5
- package/skills/ultra-research/SKILL.md +17 -5
- package/skills/ultra-status/SKILL.md +15 -6
- package/spec/cli-protocol.md +15 -1
- package/spec/mcp-tools.yaml +316 -11
- package/spec/schemas/mcp-tools.schema.json +1 -1
- package/spec/schemas/state-db.sql +44 -2
- package/ultra-tools/cli.cjs +9 -5
- package/ultra-tools/commands/migrate.cjs +62 -36
- package/ultra-tools/commands/status.cjs +29 -2
- package/ultra-tools/commands/system.cjs +340 -6
- package/ultra-tools/commands/task.cjs +11 -2
- package/ultra-tools/status-command.test.cjs +49 -0
- package/ultra-tools/system-command.test.cjs +256 -0
|
@@ -10,6 +10,8 @@ const { execFileSync } = require('node:child_process');
|
|
|
10
10
|
const { initStateDb, closeStateDb } = require('./state-db.cjs');
|
|
11
11
|
const ops = require('./state-ops.cjs');
|
|
12
12
|
const changes = require('./change-workflow.cjs');
|
|
13
|
+
const baselines = require('./baseline-workflow.cjs');
|
|
14
|
+
const archiveJournal = require('./archive-journal.cjs');
|
|
13
15
|
|
|
14
16
|
function fixture() {
|
|
15
17
|
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-change-'));
|
|
@@ -20,6 +22,11 @@ function fixture() {
|
|
|
20
22
|
execFileSync('git', ['add', 'README.md'], { cwd: rootDir });
|
|
21
23
|
execFileSync('git', ['commit', '-q', '-m', 'seed'], { cwd: rootDir });
|
|
22
24
|
const { db } = initStateDb(path.join(rootDir, '.ultra', 'state.db'));
|
|
25
|
+
db.prepare(
|
|
26
|
+
`INSERT INTO baselines
|
|
27
|
+
(id, project_name, mode, status, approved_by, approval_note, converged_at)
|
|
28
|
+
VALUES ('test-baseline', 'fixture', 'greenfield', 'ready', 'test', 'accepted fixture', ?)`,
|
|
29
|
+
).run(new Date().toISOString());
|
|
23
30
|
return { rootDir, db };
|
|
24
31
|
}
|
|
25
32
|
|
|
@@ -124,6 +131,163 @@ test('createChange persists a change and an inspectable external-provider contex
|
|
|
124
131
|
}
|
|
125
132
|
});
|
|
126
133
|
|
|
134
|
+
test('new ordinary changes require a ready baseline while incidents require an explicit break-glass record', () => {
|
|
135
|
+
const fx = fixture();
|
|
136
|
+
try {
|
|
137
|
+
assert.throws(
|
|
138
|
+
() => changes.createChange(fx.db, {
|
|
139
|
+
id: 'chg-invalid-bypass', title: 'Ordinary work', kind: 'quick',
|
|
140
|
+
intent: 'Do not attach break-glass approval to ordinary work.',
|
|
141
|
+
baseline_bypass: { reason: 'Not applicable.', approved_by: 'owner' },
|
|
142
|
+
}, { rootDir: fx.rootDir }),
|
|
143
|
+
(error) => error.code === 'VALIDATION_ERROR',
|
|
144
|
+
);
|
|
145
|
+
fx.db.prepare('DELETE FROM baselines').run();
|
|
146
|
+
assert.throws(
|
|
147
|
+
() => changes.createChange(fx.db, {
|
|
148
|
+
id: 'chg-normal-without-baseline', title: 'Do ordinary work', kind: 'quick',
|
|
149
|
+
intent: 'Do not start ordinary work before adoption.',
|
|
150
|
+
}, { rootDir: fx.rootDir }),
|
|
151
|
+
(error) => error.code === 'BASELINE_NOT_READY',
|
|
152
|
+
);
|
|
153
|
+
assert.throws(
|
|
154
|
+
() => changes.createChange(fx.db, {
|
|
155
|
+
id: 'chg-incident-without-approval', title: 'Recover production', kind: 'incident',
|
|
156
|
+
intent: 'Restore the public runtime without waiting for adoption.',
|
|
157
|
+
}, { rootDir: fx.rootDir }),
|
|
158
|
+
(error) => error.code === 'BASELINE_BYPASS_REQUIRED',
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const created = changes.createChange(fx.db, {
|
|
162
|
+
id: 'chg-incident-break-glass', title: 'Recover production', kind: 'incident',
|
|
163
|
+
intent: 'Restore the public runtime without waiting for adoption.',
|
|
164
|
+
baseline_bypass: {
|
|
165
|
+
reason: 'Production is unavailable and baseline adoption cannot precede recovery.',
|
|
166
|
+
approved_by: 'incident-commander',
|
|
167
|
+
},
|
|
168
|
+
}, { rootDir: fx.rootDir });
|
|
169
|
+
assert.equal(created.change.baseline_bypass.approved_by, 'incident-commander');
|
|
170
|
+
assert.equal(created.change.baseline_bypass.mode, 'incident_break_glass');
|
|
171
|
+
assert.match(created.change.baseline_bypass.recorded_at, /^\d{4}-\d{2}-\d{2}T/);
|
|
172
|
+
} finally { cleanup(fx); }
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('task creation authority rejects missing and terminal change ownership even with a healthy baseline', () => {
|
|
176
|
+
const fx = fixture();
|
|
177
|
+
try {
|
|
178
|
+
assert.throws(
|
|
179
|
+
() => changes.assertTaskCreationAllowed(fx.db, { change_id: 'missing-change' }, { rootDir: fx.rootDir }),
|
|
180
|
+
(error) => error.code === 'CHANGE_NOT_FOUND',
|
|
181
|
+
);
|
|
182
|
+
for (const status of ['ready', 'archived', 'cancelled']) {
|
|
183
|
+
const id = `terminal-${status}`;
|
|
184
|
+
changes.createChange(fx.db, {
|
|
185
|
+
id, title: `Terminal ${status}`, kind: 'quick',
|
|
186
|
+
intent: 'Verify terminal changes cannot receive new tasks.',
|
|
187
|
+
}, { rootDir: fx.rootDir });
|
|
188
|
+
fx.db.prepare('UPDATE changes SET status = ? WHERE id = ?').run(status, id);
|
|
189
|
+
assert.throws(
|
|
190
|
+
() => changes.assertTaskCreationAllowed(fx.db, { change_id: id }, { rootDir: fx.rootDir }),
|
|
191
|
+
(error) => error.code === 'CHANGE_NOT_MUTABLE',
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
} finally { cleanup(fx); }
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('incident break-glass remains executable and archives with a mandatory baseline reconciliation gap', () => {
|
|
198
|
+
const fx = fixture();
|
|
199
|
+
try {
|
|
200
|
+
fx.db.prepare(
|
|
201
|
+
"UPDATE baselines SET mode = 'brownfield', status = 'adopting' WHERE id = 'test-baseline'",
|
|
202
|
+
).run();
|
|
203
|
+
const created = changes.createChange(fx.db, {
|
|
204
|
+
id: 'chg-break-glass-closure', title: 'Restore the production path', kind: 'incident',
|
|
205
|
+
intent: 'Repair the urgent runtime failure before baseline adoption can finish.',
|
|
206
|
+
docs_impact: { status: 'none', files: [], rationale: 'Internal runtime recovery.' },
|
|
207
|
+
baseline_bypass: {
|
|
208
|
+
reason: 'Production recovery cannot wait for the incomplete brownfield baseline.',
|
|
209
|
+
approved_by: 'incident-commander',
|
|
210
|
+
},
|
|
211
|
+
}, { rootDir: fx.rootDir });
|
|
212
|
+
ops.createTask(fx.db, {
|
|
213
|
+
id: 'break-glass-task', title: 'Repair urgent runtime failure', type: 'bugfix', priority: 'P0',
|
|
214
|
+
change_id: 'chg-break-glass-closure',
|
|
215
|
+
});
|
|
216
|
+
ops.updateTaskStatus(fx.db, 'break-glass-task', 'in_progress');
|
|
217
|
+
ops.updateTaskStatus(fx.db, 'break-glass-task', 'completed');
|
|
218
|
+
const compiled = changes.compileContext(fx.db, {
|
|
219
|
+
id: 'chg-break-glass-closure', ...executionContext('break-glass-task'),
|
|
220
|
+
}, { rootDir: fx.rootDir });
|
|
221
|
+
assert.equal(compiled.manifest.readiness.status, 'ready');
|
|
222
|
+
assert.ok(compiled.manifest.readiness.warnings.includes('BASELINE_NOT_READY:adopting'));
|
|
223
|
+
|
|
224
|
+
const diagnosisPath = path.join(path.dirname(created.intent_path), 'diagnosis.md');
|
|
225
|
+
fs.writeFileSync(diagnosisPath, [
|
|
226
|
+
'# Incident diagnosis: Restore the production path', '',
|
|
227
|
+
'## Reproduction', '', 'The public runtime fails on the terminal transition.', '',
|
|
228
|
+
'## Hypotheses', '', 'The transition loses its durable terminal write.', '',
|
|
229
|
+
'## Root cause', '', 'The terminal path returns before the durable write completes.', '',
|
|
230
|
+
'## Regression test', '', 'The regression test reproduces the lost write and then passes.', '',
|
|
231
|
+
'## Recovery', '', 'Replay the terminal write and verify the public runtime.', '',
|
|
232
|
+
].join('\n'));
|
|
233
|
+
|
|
234
|
+
const converged = changes.convergeChange(fx.db, {
|
|
235
|
+
id: 'chg-break-glass-closure', evidence: incidentEvidence(),
|
|
236
|
+
}, { rootDir: fx.rootDir });
|
|
237
|
+
assert.equal(converged.ready, true);
|
|
238
|
+
|
|
239
|
+
const archived = changes.archiveChange(fx.db, {
|
|
240
|
+
id: 'chg-break-glass-closure', summary: 'Production restored with regression evidence.',
|
|
241
|
+
no_baseline_change_reason: 'Baseline adoption is tracked by the reconciliation gap.',
|
|
242
|
+
}, { rootDir: fx.rootDir });
|
|
243
|
+
assert.equal(archived.change.status, 'archived');
|
|
244
|
+
assert.equal(archived.baseline_bypass, true);
|
|
245
|
+
const baseline = baselines.readBaseline(fx.db, 'test-baseline');
|
|
246
|
+
assert.equal(baseline.status, 'adopting');
|
|
247
|
+
assert.deepEqual(
|
|
248
|
+
baseline.gaps.map((gap) => ({ id: gap.id, category: gap.category, status: gap.status, blocking: gap.blocking })),
|
|
249
|
+
[{
|
|
250
|
+
id: 'incident-chg-break-glass-closure-reconciliation',
|
|
251
|
+
category: 'baseline_blocker', status: 'open', blocking: true,
|
|
252
|
+
}],
|
|
253
|
+
);
|
|
254
|
+
assert.equal(baseline.gaps[0].owner, 'incident-commander');
|
|
255
|
+
assert.ok(baseline.gaps[0].evidence_refs.some((ref) => ref.includes('changes/archive/')));
|
|
256
|
+
} finally { cleanup(fx); }
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test('an already-active ordinary change remains executable when baseline adoption later becomes incomplete', () => {
|
|
260
|
+
const fx = fixture();
|
|
261
|
+
try {
|
|
262
|
+
changes.createChange(fx.db, {
|
|
263
|
+
id: 'chg-grandfathered-context', title: 'Finish active work', kind: 'quick',
|
|
264
|
+
intent: 'Preserve an already-active bounded change while baseline recovery runs.',
|
|
265
|
+
}, { rootDir: fx.rootDir });
|
|
266
|
+
ops.createTask(fx.db, {
|
|
267
|
+
id: 'grandfathered-task', title: 'Finish active work', type: 'bugfix', priority: 'P0',
|
|
268
|
+
change_id: 'chg-grandfathered-context',
|
|
269
|
+
});
|
|
270
|
+
fx.db.prepare(
|
|
271
|
+
"UPDATE baselines SET status = 'adopting', mode = 'brownfield' WHERE id = 'test-baseline'",
|
|
272
|
+
).run();
|
|
273
|
+
|
|
274
|
+
assert.doesNotThrow(() => changes.assertTaskCreationAllowed(fx.db, {
|
|
275
|
+
change_id: 'chg-grandfathered-context',
|
|
276
|
+
}, { rootDir: fx.rootDir }));
|
|
277
|
+
assert.throws(
|
|
278
|
+
() => changes.assertTaskCreationAllowed(fx.db, {}, { rootDir: fx.rootDir }),
|
|
279
|
+
(error) => error.code === 'BASELINE_NOT_READY',
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const compiled = changes.compileContext(fx.db, {
|
|
283
|
+
id: 'chg-grandfathered-context', ...executionContext('grandfathered-task'),
|
|
284
|
+
}, { rootDir: fx.rootDir });
|
|
285
|
+
assert.equal(compiled.manifest.readiness.status, 'ready');
|
|
286
|
+
assert.ok(compiled.manifest.readiness.warnings.includes('BASELINE_NOT_READY:adopting'));
|
|
287
|
+
assert.deepEqual(compiled.manifest.readiness.blockers, []);
|
|
288
|
+
} finally { cleanup(fx); }
|
|
289
|
+
});
|
|
290
|
+
|
|
127
291
|
test('convergeChange blocks incomplete work and marks a fully evidenced standard change ready', () => {
|
|
128
292
|
const fx = fixture();
|
|
129
293
|
try {
|
|
@@ -179,14 +343,103 @@ test('convergeChange blocks incomplete work and marks a fully evidenced standard
|
|
|
179
343
|
}
|
|
180
344
|
});
|
|
181
345
|
|
|
346
|
+
test('convergeChange requires an approved baseline after active work has remained executable', () => {
|
|
347
|
+
const fx = fixture();
|
|
348
|
+
try {
|
|
349
|
+
changes.createChange(fx.db, {
|
|
350
|
+
id: 'chg-baseline-gate', title: 'Keep runtime work moving', kind: 'quick',
|
|
351
|
+
intent: 'Defer baseline closure to convergence without losing implementation progress.',
|
|
352
|
+
docs_impact: { status: 'none', files: [], rationale: 'Internal workflow behavior only.' },
|
|
353
|
+
}, { rootDir: fx.rootDir });
|
|
354
|
+
ops.createTask(fx.db, {
|
|
355
|
+
id: 'baseline-gate-task', title: 'Exercise convergence boundary', type: 'feature', priority: 'P0',
|
|
356
|
+
change_id: 'chg-baseline-gate',
|
|
357
|
+
});
|
|
358
|
+
ops.updateTaskStatus(fx.db, 'baseline-gate-task', 'in_progress');
|
|
359
|
+
ops.updateTaskStatus(fx.db, 'baseline-gate-task', 'completed');
|
|
360
|
+
changes.compileContext(fx.db, {
|
|
361
|
+
id: 'chg-baseline-gate', ...executionContext('baseline-gate-task'),
|
|
362
|
+
}, { rootDir: fx.rootDir });
|
|
363
|
+
fx.db.prepare('DELETE FROM baselines').run();
|
|
364
|
+
|
|
365
|
+
const result = changes.convergeChange(fx.db, {
|
|
366
|
+
id: 'chg-baseline-gate',
|
|
367
|
+
evidence: [
|
|
368
|
+
{ category: 'diff', status: 'pass', evidence: 'diff reviewed' },
|
|
369
|
+
{ category: 'tests', status: 'pass', evidence: 'workflow regression passed', seam: 'convergence gate' },
|
|
370
|
+
{ category: 'spec', status: 'pass', evidence: 'change remains aligned' },
|
|
371
|
+
],
|
|
372
|
+
}, { rootDir: fx.rootDir });
|
|
373
|
+
|
|
374
|
+
assert.equal(result.ready, false);
|
|
375
|
+
assert.ok(result.blockers.includes('BASELINE_MISSING'));
|
|
376
|
+
} finally {
|
|
377
|
+
cleanup(fx);
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test('change creation rejects a schema-migration compatibility baseline until re-adoption', () => {
|
|
382
|
+
const fx = fixture();
|
|
383
|
+
try {
|
|
384
|
+
fx.db.prepare(
|
|
385
|
+
`UPDATE baselines SET mode = 'migrated', approved_by = 'schema-migration',
|
|
386
|
+
approval_note = 'compatibility only' WHERE id = 'test-baseline'`,
|
|
387
|
+
).run();
|
|
388
|
+
assert.throws(
|
|
389
|
+
() => changes.createChange(fx.db, {
|
|
390
|
+
id: 'chg-migrated-gate', title: 'Require owner adoption', kind: 'quick',
|
|
391
|
+
intent: 'Do not let schema migration impersonate project-owner approval.',
|
|
392
|
+
docs_impact: { status: 'none', files: [], rationale: 'Gate behavior only.' },
|
|
393
|
+
}, { rootDir: fx.rootDir }),
|
|
394
|
+
(error) => error.code === 'BASELINE_NOT_READY'
|
|
395
|
+
&& error.details.blockers.includes('BASELINE_MIGRATION_REVIEW_REQUIRED'),
|
|
396
|
+
);
|
|
397
|
+
} finally {
|
|
398
|
+
cleanup(fx);
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
|
|
182
402
|
test('archiveChange moves a ready change into immutable history and records baseline reconciliation', () => {
|
|
183
403
|
const fx = fixture();
|
|
184
404
|
try {
|
|
405
|
+
fs.mkdirSync(path.join(fx.rootDir, '.ultra', 'specs'), { recursive: true });
|
|
406
|
+
fs.writeFileSync(path.join(fx.rootDir, '.ultra', 'specs', 'product.md'), '# Product\n\nCurrent behavior.\n');
|
|
407
|
+
fs.writeFileSync(path.join(fx.rootDir, '.ultra', 'specs', 'architecture.md'), '# Architecture\n\nCurrent boundary.\n');
|
|
408
|
+
const fileDigest = (file) => require('node:crypto').createHash('sha256')
|
|
409
|
+
.update(fs.readFileSync(file)).digest('hex');
|
|
410
|
+
fx.db.prepare(
|
|
411
|
+
`UPDATE baselines SET mode = 'brownfield', repository_revision = ?, spec_refs_json = ?
|
|
412
|
+
WHERE id = 'test-baseline'`,
|
|
413
|
+
).run(
|
|
414
|
+
execFileSync('git', ['rev-parse', 'HEAD'], { cwd: fx.rootDir, encoding: 'utf8' }).trim(),
|
|
415
|
+
JSON.stringify([
|
|
416
|
+
{
|
|
417
|
+
kind: 'product', path: '.ultra/specs/product.md',
|
|
418
|
+
digest: fileDigest(path.join(fx.rootDir, '.ultra', 'specs', 'product.md')),
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
kind: 'architecture', path: '.ultra/specs/architecture.md',
|
|
422
|
+
digest: fileDigest(path.join(fx.rootDir, '.ultra', 'specs', 'architecture.md')),
|
|
423
|
+
},
|
|
424
|
+
]),
|
|
425
|
+
);
|
|
185
426
|
changes.createChange(fx.db, {
|
|
186
427
|
id: 'chg-archive', title: 'Archive verified change', kind: 'quick',
|
|
187
428
|
intent: 'Preserve the completed change as project history.',
|
|
188
429
|
docs_impact: { status: 'none', files: [], rationale: 'Internal test-only behavior.' },
|
|
189
430
|
}, { rootDir: fx.rootDir });
|
|
431
|
+
fs.appendFileSync(
|
|
432
|
+
path.join(fx.rootDir, '.ultra', 'specs', 'product.md'),
|
|
433
|
+
'\nBehavior learned and approved by this change.\n',
|
|
434
|
+
);
|
|
435
|
+
execFileSync('git', ['add', '.ultra/specs/product.md'], { cwd: fx.rootDir });
|
|
436
|
+
execFileSync('git', ['commit', '-q', '-m', 'update baseline behavior'], { cwd: fx.rootDir });
|
|
437
|
+
const changedHead = execFileSync(
|
|
438
|
+
'git', ['rev-parse', 'HEAD'], { cwd: fx.rootDir, encoding: 'utf8' },
|
|
439
|
+
).trim();
|
|
440
|
+
const drift = baselines.inspectBaseline(fx.db, { rootDir: fx.rootDir });
|
|
441
|
+
assert.ok(drift.blockers.includes('BASELINE_HEAD_STALE'));
|
|
442
|
+
assert.ok(drift.blockers.includes('BASELINE_SPEC_STALE:.ultra/specs/product.md'));
|
|
190
443
|
ops.createTask(fx.db, {
|
|
191
444
|
id: 'archive-task', title: 'Complete archive path', type: 'bugfix', priority: 'P1',
|
|
192
445
|
change_id: 'chg-archive',
|
|
@@ -214,7 +467,7 @@ test('archiveChange moves a ready change into immutable history and records base
|
|
|
214
467
|
|
|
215
468
|
const archived = changes.archiveChange(fx.db, {
|
|
216
469
|
id: 'chg-archive', summary: 'Verified quick fix archived.',
|
|
217
|
-
|
|
470
|
+
baseline_updates: ['.ultra/specs/product.md'],
|
|
218
471
|
}, { rootDir: fx.rootDir });
|
|
219
472
|
assert.equal(archived.change.status, 'archived');
|
|
220
473
|
assert.ok(fs.existsSync(archived.archive_path));
|
|
@@ -228,6 +481,139 @@ test('archiveChange moves a ready change into immutable history and records base
|
|
|
228
481
|
assert.doesNotMatch(artifact.path, /changes\/active/);
|
|
229
482
|
assert.ok(fs.existsSync(path.join(fx.rootDir, artifact.path)), artifact.path);
|
|
230
483
|
}
|
|
484
|
+
const reconciliation = fx.db.prepare(
|
|
485
|
+
"SELECT type, payload_json FROM events WHERE type = 'baseline_reconciled' ORDER BY id DESC LIMIT 1",
|
|
486
|
+
).get();
|
|
487
|
+
assert.equal(reconciliation.type, 'baseline_reconciled');
|
|
488
|
+
assert.deepEqual(JSON.parse(reconciliation.payload_json).baseline_updates, ['.ultra/specs/product.md']);
|
|
489
|
+
const reconciled = baselines.inspectBaseline(fx.db, { rootDir: fx.rootDir });
|
|
490
|
+
assert.equal(reconciled.status, 'pass');
|
|
491
|
+
assert.equal(reconciled.baseline.repository_revision, changedHead);
|
|
492
|
+
} finally {
|
|
493
|
+
cleanup(fx);
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
test('archiveChange rolls back when declared reconciliation leaves a tracked specification stale', () => {
|
|
498
|
+
const fx = fixture();
|
|
499
|
+
try {
|
|
500
|
+
const specPath = path.join(fx.rootDir, '.ultra', 'specs', 'product.md');
|
|
501
|
+
fs.mkdirSync(path.dirname(specPath), { recursive: true });
|
|
502
|
+
fs.writeFileSync(specPath, '# Product\n\nOriginal behavior.\n');
|
|
503
|
+
const originalHead = execFileSync(
|
|
504
|
+
'git', ['rev-parse', 'HEAD'], { cwd: fx.rootDir, encoding: 'utf8' },
|
|
505
|
+
).trim();
|
|
506
|
+
const originalDigest = require('node:crypto').createHash('sha256')
|
|
507
|
+
.update(fs.readFileSync(specPath)).digest('hex');
|
|
508
|
+
fx.db.prepare(
|
|
509
|
+
`UPDATE baselines SET mode = 'brownfield', repository_revision = ?, spec_refs_json = ?
|
|
510
|
+
WHERE id = 'test-baseline'`,
|
|
511
|
+
).run(originalHead, JSON.stringify([
|
|
512
|
+
{ kind: 'product', path: '.ultra/specs/product.md', digest: originalDigest },
|
|
513
|
+
]));
|
|
514
|
+
changes.createChange(fx.db, {
|
|
515
|
+
id: 'chg-reconcile-rollback', title: 'Reject incomplete reconciliation', kind: 'quick',
|
|
516
|
+
intent: 'Keep the active change recoverable until every tracked specification is reconciled.',
|
|
517
|
+
docs_impact: { status: 'none', files: [], rationale: 'Workflow integrity only.' },
|
|
518
|
+
}, { rootDir: fx.rootDir });
|
|
519
|
+
fs.appendFileSync(specPath, '\nChanged behavior that must be declared.\n');
|
|
520
|
+
execFileSync('git', ['add', '.ultra/specs/product.md'], { cwd: fx.rootDir });
|
|
521
|
+
execFileSync('git', ['commit', '-q', '-m', 'change tracked product spec'], { cwd: fx.rootDir });
|
|
522
|
+
ops.createTask(fx.db, {
|
|
523
|
+
id: 'reconcile-rollback-task', title: 'Exercise archive rollback', type: 'feature', priority: 'P0',
|
|
524
|
+
change_id: 'chg-reconcile-rollback',
|
|
525
|
+
});
|
|
526
|
+
ops.updateTaskStatus(fx.db, 'reconcile-rollback-task', 'in_progress');
|
|
527
|
+
ops.updateTaskStatus(fx.db, 'reconcile-rollback-task', 'completed');
|
|
528
|
+
changes.compileContext(fx.db, {
|
|
529
|
+
id: 'chg-reconcile-rollback', ...executionContext('reconcile-rollback-task'),
|
|
530
|
+
}, { rootDir: fx.rootDir });
|
|
531
|
+
const converged = changes.convergeChange(fx.db, {
|
|
532
|
+
id: 'chg-reconcile-rollback',
|
|
533
|
+
evidence: [
|
|
534
|
+
{ category: 'diff', status: 'pass', evidence: 'archive diff reviewed' },
|
|
535
|
+
{
|
|
536
|
+
category: 'tests', status: 'pass', evidence: 'archive rollback regression passed',
|
|
537
|
+
seam: 'archive reconciliation boundary',
|
|
538
|
+
},
|
|
539
|
+
{ category: 'spec', status: 'pass', evidence: 'tracked specification change identified' },
|
|
540
|
+
],
|
|
541
|
+
}, { rootDir: fx.rootDir });
|
|
542
|
+
assert.equal(converged.ready, true);
|
|
543
|
+
|
|
544
|
+
assert.throws(
|
|
545
|
+
() => changes.archiveChange(fx.db, {
|
|
546
|
+
id: 'chg-reconcile-rollback', summary: 'Attempt an incomplete archive.',
|
|
547
|
+
no_baseline_change_reason: 'No baseline update declared.',
|
|
548
|
+
}, { rootDir: fx.rootDir }),
|
|
549
|
+
(error) => error.code === 'BASELINE_RECONCILIATION_INCOMPLETE'
|
|
550
|
+
&& error.details.blockers.includes('BASELINE_SPEC_STALE:.ultra/specs/product.md'),
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
const active = changes.readChange(fx.db, 'chg-reconcile-rollback');
|
|
554
|
+
assert.equal(active.status, 'ready');
|
|
555
|
+
assert.ok(fs.existsSync(path.join(fx.rootDir, active.artifact_root)));
|
|
556
|
+
assert.equal(fs.existsSync(path.join(fx.rootDir, active.artifact_root, 'archive-summary.md')), false);
|
|
557
|
+
assert.equal(baselines.readBaseline(fx.db).repository_revision, originalHead);
|
|
558
|
+
const reconciliationEvents = fx.db.prepare(
|
|
559
|
+
"SELECT COUNT(*) AS count FROM events WHERE type = 'baseline_reconciled' AND change_id = ?",
|
|
560
|
+
).get('chg-reconcile-rollback');
|
|
561
|
+
assert.equal(reconciliationEvents.count, 0);
|
|
562
|
+
} finally {
|
|
563
|
+
cleanup(fx);
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
test('archiveChange resumes a durable journal after the filesystem move outlives the process', () => {
|
|
568
|
+
const fx = fixture();
|
|
569
|
+
try {
|
|
570
|
+
changes.createChange(fx.db, {
|
|
571
|
+
id: 'chg-crash-resume', title: 'Resume interrupted archive', kind: 'quick',
|
|
572
|
+
intent: 'Finish an authorized archive after a process crash.',
|
|
573
|
+
docs_impact: { status: 'none', files: [], rationale: 'Recovery-only behavior.' },
|
|
574
|
+
}, { rootDir: fx.rootDir });
|
|
575
|
+
ops.createTask(fx.db, {
|
|
576
|
+
id: 'crash-resume-task', title: 'Verify archive recovery', type: 'bugfix', priority: 'P0',
|
|
577
|
+
change_id: 'chg-crash-resume',
|
|
578
|
+
});
|
|
579
|
+
ops.updateTaskStatus(fx.db, 'crash-resume-task', 'in_progress');
|
|
580
|
+
ops.updateTaskStatus(fx.db, 'crash-resume-task', 'completed');
|
|
581
|
+
changes.compileContext(fx.db, {
|
|
582
|
+
id: 'chg-crash-resume', ...executionContext('crash-resume-task'),
|
|
583
|
+
}, { rootDir: fx.rootDir });
|
|
584
|
+
const converged = changes.convergeChange(fx.db, {
|
|
585
|
+
id: 'chg-crash-resume',
|
|
586
|
+
evidence: [
|
|
587
|
+
{ category: 'diff', status: 'pass', evidence: 'recovery diff reviewed' },
|
|
588
|
+
{
|
|
589
|
+
category: 'tests', status: 'pass', evidence: 'crash recovery passed', seam: 'archive retry',
|
|
590
|
+
signal: {
|
|
591
|
+
command: 'node --test archive-journal.test.cjs',
|
|
592
|
+
expected_red: 'archive retry fails after the filesystem move',
|
|
593
|
+
observed_red: true, observed_green: true, deterministic: true, duration_ms: 10,
|
|
594
|
+
},
|
|
595
|
+
},
|
|
596
|
+
{ category: 'spec', status: 'not_applicable', evidence: 'No contract change.' },
|
|
597
|
+
],
|
|
598
|
+
}, { rootDir: fx.rootDir });
|
|
599
|
+
assert.equal(converged.ready, true);
|
|
600
|
+
const input = {
|
|
601
|
+
id: 'chg-crash-resume', summary: 'Resume the interrupted archive safely.',
|
|
602
|
+
no_baseline_change_reason: 'No baseline content changed.',
|
|
603
|
+
};
|
|
604
|
+
const change = changes.readChange(fx.db, input.id);
|
|
605
|
+
const interrupted = archiveJournal.prepareArchiveMove({
|
|
606
|
+
rootDir: fx.rootDir, change, summary: input.summary, baselineUpdates: [],
|
|
607
|
+
noBaselineChangeReason: input.no_baseline_change_reason,
|
|
608
|
+
});
|
|
609
|
+
assert.equal(changes.readChange(fx.db, input.id).status, 'ready');
|
|
610
|
+
assert.equal(fs.existsSync(interrupted.source), false);
|
|
611
|
+
assert.equal(fs.existsSync(interrupted.destination), true);
|
|
612
|
+
|
|
613
|
+
const resumed = changes.archiveChange(fx.db, input, { rootDir: fx.rootDir });
|
|
614
|
+
assert.equal(resumed.change.status, 'archived');
|
|
615
|
+
assert.equal(resumed.archive_path, interrupted.destination);
|
|
616
|
+
assert.equal(archiveJournal.listArchiveIntents(fx.rootDir).length, 0);
|
|
231
617
|
} finally {
|
|
232
618
|
cleanup(fx);
|
|
233
619
|
}
|
|
@@ -447,12 +833,12 @@ test('compileContext v2 persists role-scoped context, budget, execution contract
|
|
|
447
833
|
}
|
|
448
834
|
});
|
|
449
835
|
|
|
450
|
-
test('compileContext blocks a required missing ref
|
|
836
|
+
test('compileContext blocks a required missing ref', () => {
|
|
451
837
|
const fx = fixture();
|
|
452
838
|
try {
|
|
453
839
|
changes.createChange(fx.db, {
|
|
454
840
|
id: 'chg-context-budget', title: 'Bound context packet', kind: 'quick',
|
|
455
|
-
intent: 'Fail closed when required context is missing
|
|
841
|
+
intent: 'Fail closed when required context is missing while reporting size pressure.',
|
|
456
842
|
}, { rootDir: fx.rootDir });
|
|
457
843
|
ops.createTask(fx.db, {
|
|
458
844
|
id: 'budget-task', title: 'Compile bounded packet', type: 'bugfix', priority: 'P0',
|
|
@@ -466,14 +852,58 @@ test('compileContext blocks a required missing ref or an over-budget role packet
|
|
|
466
852
|
{ ref: 'missing-contract.md', kind: 'spec', reason: 'Required contract', required: true },
|
|
467
853
|
{ ref: 'README.md', kind: 'source', reason: 'Current implementation', required: true },
|
|
468
854
|
],
|
|
469
|
-
budget: { max_tokens:
|
|
855
|
+
budget: { max_tokens: 2_000, max_files: 4 },
|
|
470
856
|
}),
|
|
471
857
|
}, { rootDir: fx.rootDir });
|
|
472
858
|
|
|
473
859
|
assert.equal(compiled.manifest.readiness.status, 'blocked');
|
|
474
860
|
assert.ok(compiled.manifest.readiness.blockers.includes('CONTEXT_REQUIRED_REF_MISSING:missing-contract.md'));
|
|
475
|
-
|
|
476
|
-
|
|
861
|
+
} finally {
|
|
862
|
+
cleanup(fx);
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
test('an incident can proceed with four necessary large files while context budgets remain advisory', () => {
|
|
867
|
+
const fx = fixture();
|
|
868
|
+
try {
|
|
869
|
+
const refs = [];
|
|
870
|
+
for (let index = 1; index <= 4; index += 1) {
|
|
871
|
+
const ref = `incident-runtime-${index}.cjs`;
|
|
872
|
+
fs.writeFileSync(path.join(fx.rootDir, ref), 'x'.repeat(8_000));
|
|
873
|
+
refs.push({
|
|
874
|
+
ref, kind: 'source', reason: `Required incident runtime boundary ${index}`, required: true,
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
changes.createChange(fx.db, {
|
|
878
|
+
id: 'chg-incident-context', title: 'Recover IM runtime', kind: 'incident',
|
|
879
|
+
intent: 'Keep an urgent root-cause fix executable when its required files exceed guidance.',
|
|
880
|
+
}, { rootDir: fx.rootDir });
|
|
881
|
+
ops.createTask(fx.db, {
|
|
882
|
+
id: 'incident-context-task', title: 'Repair IM terminal transition', type: 'bugfix', priority: 'P0',
|
|
883
|
+
change_id: 'chg-incident-context',
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
const compiled = changes.compileContext(fx.db, {
|
|
887
|
+
id: 'chg-incident-context',
|
|
888
|
+
...executionContext('incident-context-task', {
|
|
889
|
+
context_refs: refs,
|
|
890
|
+
budget: { max_tokens: 100, max_files: 2 },
|
|
891
|
+
execution_contract: {
|
|
892
|
+
slice_kind: 'tracer_bullet',
|
|
893
|
+
public_seam: 'IM terminal transition',
|
|
894
|
+
verification_command: 'node --test im-terminal.test.cjs',
|
|
895
|
+
context_budget_percent: 80,
|
|
896
|
+
},
|
|
897
|
+
}),
|
|
898
|
+
}, { rootDir: fx.rootDir });
|
|
899
|
+
|
|
900
|
+
assert.equal(compiled.manifest.readiness.status, 'ready');
|
|
901
|
+
assert.deepEqual(compiled.manifest.readiness.blockers, []);
|
|
902
|
+
assert.ok(compiled.manifest.readiness.warnings.includes('CONTEXT_FILE_BUDGET_EXCEEDED'));
|
|
903
|
+
assert.ok(compiled.manifest.readiness.warnings.includes('CONTEXT_TOKEN_BUDGET_EXCEEDED'));
|
|
904
|
+
assert.ok(compiled.manifest.readiness.warnings.includes('EXECUTION_CONTEXT_BUDGET_ADVISORY'));
|
|
905
|
+
assert.equal(compiled.manifest.context.file_count, 4);
|
|
906
|
+
assert.ok(compiled.manifest.context.token_estimate > 100);
|
|
477
907
|
} finally {
|
|
478
908
|
cleanup(fx);
|
|
479
909
|
}
|