ultra-builder-pro-cli 0.23.0 → 0.24.1
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 +4 -4
- package/.ultra-template/tasks/tasks.json +4 -2
- package/CHANGELOG.md +72 -0
- package/README.md +55 -39
- package/adapters/_shared/codex-assets.cjs +121 -65
- package/adapters/_shared/interaction-contract.cjs +7 -1
- package/adapters/_shared/provenance.cjs +7 -2
- package/adapters/_shared/runtime-assets.cjs +3 -2
- package/adapters/_shared/runtime-native.cjs +132 -0
- package/adapters/claude.js +60 -30
- package/adapters/codex.js +178 -81
- package/adapters/grok.js +372 -0
- package/adapters/kimi.js +7 -3
- package/adapters/opencode.js +210 -32
- package/agents/code-reviewer.md +4 -2
- package/agents/debugger.md +4 -1
- package/agents/review-code.md +7 -3
- package/agents/review-comments.md +7 -4
- package/agents/review-coordinator.md +11 -7
- package/agents/review-design.md +8 -4
- package/agents/review-errors.md +8 -4
- package/agents/review-spec.md +8 -5
- package/agents/review-tests.md +7 -4
- package/agents/tdd-runner.md +5 -2
- package/bin/install.js +9 -4
- package/bin/orchestrator.js +4 -13
- package/docs/AGENT-CONTEXT.md +143 -229
- package/docs/ARCHITECTURE.md +205 -494
- package/docs/ARTIFACT-AUTHORITY.md +35 -28
- package/docs/LEGACY-CLI-CROSSWALK.md +1 -1
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +12 -9
- package/docs/ROADMAP.md +85 -110
- package/docs/RUNTIME-COMPAT-MATRIX.md +139 -175
- package/docs/SKILL-AUTHORING.md +3 -3
- package/docs/STATE-DB-ACCESS-POLICY.md +13 -8
- package/docs/WORKFLOW-LIFECYCLE.md +189 -448
- package/hooks/README.md +11 -8
- package/hooks/active_task_context.py +16 -16
- package/hooks/adapters/grok.py +134 -0
- package/hooks/{context_spine.py → context_envelope.py} +26 -22
- package/hooks/health_check.py +29 -34
- package/hooks/pre_stop_check.py +22 -12
- package/hooks/subagent_tracker.py +1 -1
- package/hooks/workflow_checkpoint.py +14 -13
- package/hooks/workflow_context.py +11 -12
- package/hooks/workflow_resume.py +14 -13
- package/mcp-server/{breadcrumb.cjs → hook-context.cjs} +7 -9
- package/mcp-server/lib/baseline-workflow.cjs +108 -82
- package/mcp-server/lib/canonical-json.cjs +19 -0
- package/mcp-server/lib/change-authority.cjs +26 -0
- package/mcp-server/lib/change-workflow.cjs +630 -45
- package/mcp-server/lib/context-envelope.cjs +508 -0
- package/mcp-server/lib/decision-records.cjs +337 -0
- package/mcp-server/lib/doctor.cjs +91 -33
- package/mcp-server/lib/init-project.cjs +78 -39
- package/mcp-server/lib/managed-file-write.cjs +116 -0
- package/mcp-server/lib/plan-checkpoint.cjs +177 -0
- package/mcp-server/lib/project-context.cjs +178 -0
- package/mcp-server/lib/stage-checkpoints.cjs +309 -0
- package/mcp-server/lib/state-db.cjs +155 -24
- package/mcp-server/lib/state-ops.cjs +21 -3
- package/mcp-server/lib/task-ledger.cjs +522 -13
- package/mcp-server/lib/ultra-facade.cjs +757 -442
- package/mcp-server/lib/worker-packet.cjs +358 -0
- package/mcp-server/server.cjs +200 -1103
- package/orchestrator/daemon.cjs +27 -9
- package/orchestrator/parallel-orchestrator.cjs +111 -76
- package/orchestrator/session-runner.cjs +135 -34
- package/package.json +10 -3
- package/skills/ultra-change/SKILL.md +10 -8
- package/skills/ultra-deliver/SKILL.md +17 -19
- package/skills/ultra-dev/SKILL.md +20 -11
- package/skills/ultra-doctor/SKILL.md +4 -4
- package/skills/ultra-init/SKILL.md +6 -5
- package/skills/ultra-init/references/brownfield-adoption.md +20 -14
- package/skills/ultra-plan/SKILL.md +13 -9
- package/skills/ultra-research/SKILL.md +18 -9
- package/skills/ultra-review/SKILL.md +14 -9
- package/skills/ultra-review/references/unified-schema.md +2 -0
- package/skills/ultra-review/scripts/review_wait.py +8 -0
- package/skills/ultra-status/SKILL.md +2 -2
- package/skills/ultra-test/SKILL.md +10 -6
- package/skills/ultra-think/SKILL.md +8 -7
- package/skills/ultra-think/references/interaction-boundary.md +56 -0
- package/spec/cli-protocol.md +80 -199
- package/spec/mcp-tools.yaml +43 -2806
- package/spec/schemas/state-db.sql +107 -4
- package/spec/schemas/{task-ledger.v1.schema.json → task-ledger.v2.schema.json} +70 -6
- package/ultra-tools/commands/status.cjs +369 -243
- package/mcp-server/lib/context-spine.cjs +0 -1208
- package/mcp-server/lib/decision-dialogue.cjs +0 -1128
- package/mcp-server/lib/project-breadcrumb.cjs +0 -173
- package/mcp-server/lib/spec-learning.cjs +0 -284
- package/mcp-server/lib/workflow-state.cjs +0 -3129
- package/skills/ultra-think/references/decision-dialogue.md +0 -155
|
@@ -5,10 +5,15 @@ const fs = require('node:fs');
|
|
|
5
5
|
const path = require('node:path');
|
|
6
6
|
const util = require('node:util');
|
|
7
7
|
|
|
8
|
+
const canonical = require('./canonical-json.cjs');
|
|
9
|
+
const decisionRecords = require('./decision-records.cjs');
|
|
8
10
|
const ops = require('./state-ops.cjs');
|
|
11
|
+
const { readStableProjectFile } = require('./safe-project-file.cjs');
|
|
12
|
+
const stageCheckpoints = require('./stage-checkpoints.cjs');
|
|
9
13
|
|
|
10
14
|
const LEDGER_KIND = 'ultra-team-task-ledger';
|
|
11
|
-
const LEDGER_SCHEMA_VERSION = '
|
|
15
|
+
const LEDGER_SCHEMA_VERSION = '2.0';
|
|
16
|
+
const LEGACY_LEDGER_SCHEMA_VERSION = '1.0';
|
|
12
17
|
const LEDGER_RELATIVE_PATH = '.ultra/tasks/tasks.json';
|
|
13
18
|
const HASH_PATTERN = /^[0-9a-f]{64}$/;
|
|
14
19
|
const MAX_ANCESTORS = 64;
|
|
@@ -33,11 +38,16 @@ const DURABLE_BASELINE_FIELDS = Object.freeze([
|
|
|
33
38
|
'gaps',
|
|
34
39
|
'classification',
|
|
35
40
|
'provider_refs',
|
|
36
|
-
'
|
|
41
|
+
'research_checkpoint_id',
|
|
37
42
|
'approved_by',
|
|
38
43
|
'approval_note',
|
|
39
44
|
'converged_at',
|
|
40
45
|
]);
|
|
46
|
+
const LEGACY_DURABLE_BASELINE_FIELDS = Object.freeze(
|
|
47
|
+
DURABLE_BASELINE_FIELDS.map((field) => (
|
|
48
|
+
field === 'research_checkpoint_id' ? 'research_run_id' : field
|
|
49
|
+
)),
|
|
50
|
+
);
|
|
41
51
|
const DURABLE_TASK_FIELDS = Object.freeze([
|
|
42
52
|
'id',
|
|
43
53
|
'title',
|
|
@@ -77,6 +87,27 @@ const DURABLE_CHANGE_FIELDS = Object.freeze([
|
|
|
77
87
|
'artifact_root',
|
|
78
88
|
'closed_at',
|
|
79
89
|
]);
|
|
90
|
+
const DURABLE_DECISION_FIELDS = Object.freeze([
|
|
91
|
+
'id',
|
|
92
|
+
'scope_type',
|
|
93
|
+
'scope_id',
|
|
94
|
+
'selection',
|
|
95
|
+
'effects',
|
|
96
|
+
'status',
|
|
97
|
+
'artifact_path',
|
|
98
|
+
'artifact_digest',
|
|
99
|
+
'supersedes_id',
|
|
100
|
+
]);
|
|
101
|
+
const DURABLE_CHECKPOINT_FIELDS = Object.freeze([
|
|
102
|
+
'stage',
|
|
103
|
+
'scope_type',
|
|
104
|
+
'scope_id',
|
|
105
|
+
'revision',
|
|
106
|
+
'payload',
|
|
107
|
+
'evidence',
|
|
108
|
+
'diagnostics',
|
|
109
|
+
'status',
|
|
110
|
+
]);
|
|
80
111
|
|
|
81
112
|
class TaskLedgerError extends Error {
|
|
82
113
|
constructor(code, message, details) {
|
|
@@ -166,7 +197,7 @@ function portableBaseline(row) {
|
|
|
166
197
|
? row.classification : parseJson(row.classification_json, {})],
|
|
167
198
|
['provider_refs', row.provider_refs && typeof row.provider_refs === 'object'
|
|
168
199
|
? row.provider_refs : parseJson(row.provider_refs_json, {})],
|
|
169
|
-
['
|
|
200
|
+
['research_checkpoint_id', row.research_checkpoint_id || row.research_run_id],
|
|
170
201
|
['approved_by', row.approved_by],
|
|
171
202
|
['approval_note', row.approval_note],
|
|
172
203
|
['converged_at', row.converged_at],
|
|
@@ -201,7 +232,7 @@ function portableChange(row) {
|
|
|
201
232
|
['id', row.id],
|
|
202
233
|
['title', row.title],
|
|
203
234
|
['kind', row.kind],
|
|
204
|
-
['status', row.status],
|
|
235
|
+
['status', ['blocked', 'ready'].includes(row.status) ? 'active' : row.status],
|
|
205
236
|
['intent', row.intent],
|
|
206
237
|
['docs_impact', parseJson(row.docs_impact_json, {})],
|
|
207
238
|
['provider_refs', parseJson(row.provider_refs_json, {})],
|
|
@@ -243,6 +274,57 @@ function readChangesForLedger(db, taskRows, previousChanges = new Map()) {
|
|
|
243
274
|
return rows.map((row) => durableChange(row, previousChanges.get(row.id)));
|
|
244
275
|
}
|
|
245
276
|
|
|
277
|
+
function durableDecision(row) {
|
|
278
|
+
const value = compactObject([
|
|
279
|
+
['id', row.id],
|
|
280
|
+
['scope_type', row.scope_type],
|
|
281
|
+
['scope_id', row.scope_id],
|
|
282
|
+
['selection', row.selection],
|
|
283
|
+
['effects', parseJson(row.effects_json, {})],
|
|
284
|
+
['status', row.status],
|
|
285
|
+
['artifact_path', row.artifact_path],
|
|
286
|
+
['artifact_digest', row.digest],
|
|
287
|
+
['supersedes_id', row.supersedes_id],
|
|
288
|
+
]);
|
|
289
|
+
return {
|
|
290
|
+
...value,
|
|
291
|
+
digest: sha256(value),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function readDecisionsForLedger(db) {
|
|
296
|
+
return db.prepare(
|
|
297
|
+
`SELECT * FROM decision_records
|
|
298
|
+
WHERE status IN ('accepted', 'superseded')
|
|
299
|
+
ORDER BY scope_type, scope_id, created_at, id`,
|
|
300
|
+
).all().map(durableDecision);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function durableCheckpoint(row) {
|
|
304
|
+
const value = compactObject([
|
|
305
|
+
['stage', row.stage],
|
|
306
|
+
['scope_type', row.scope_type],
|
|
307
|
+
['scope_id', row.scope_id],
|
|
308
|
+
['revision', row.revision],
|
|
309
|
+
['payload', parseJson(row.payload_json, {})],
|
|
310
|
+
['evidence', parseJson(row.evidence_json, [])],
|
|
311
|
+
['diagnostics', parseJson(row.diagnostics_json, [])],
|
|
312
|
+
['status', row.status],
|
|
313
|
+
]);
|
|
314
|
+
return {
|
|
315
|
+
...value,
|
|
316
|
+
digest: sha256(value),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function readCheckpointsForLedger(db) {
|
|
321
|
+
return db.prepare(
|
|
322
|
+
`SELECT * FROM stage_checkpoints
|
|
323
|
+
WHERE status = 'accepted'
|
|
324
|
+
ORDER BY stage, scope_type, scope_id, revision`,
|
|
325
|
+
).all().map(durableCheckpoint);
|
|
326
|
+
}
|
|
327
|
+
|
|
246
328
|
function durableStatus(task, previous) {
|
|
247
329
|
if (task.status !== 'in_progress') return task.status;
|
|
248
330
|
if (previous && previous.status !== 'in_progress') return previous.status;
|
|
@@ -283,10 +365,18 @@ function durableTask(task, previous = null) {
|
|
|
283
365
|
};
|
|
284
366
|
}
|
|
285
367
|
|
|
286
|
-
function statePayload({
|
|
368
|
+
function statePayload({
|
|
369
|
+
baseline,
|
|
370
|
+
changes,
|
|
371
|
+
decisions,
|
|
372
|
+
checkpoints,
|
|
373
|
+
tasks,
|
|
374
|
+
}) {
|
|
287
375
|
return {
|
|
288
376
|
baseline: baseline || null,
|
|
289
377
|
changes: Array.isArray(changes) ? changes : [],
|
|
378
|
+
decisions: Array.isArray(decisions) ? decisions : [],
|
|
379
|
+
checkpoints: Array.isArray(checkpoints) ? checkpoints : [],
|
|
290
380
|
tasks: Array.isArray(tasks) ? tasks : [],
|
|
291
381
|
};
|
|
292
382
|
}
|
|
@@ -353,6 +443,38 @@ function validateBaseline(entry) {
|
|
|
353
443
|
}
|
|
354
444
|
}
|
|
355
445
|
|
|
446
|
+
function validateLegacyBaseline(entry) {
|
|
447
|
+
if (entry == null) return;
|
|
448
|
+
if (typeof entry !== 'object' || Array.isArray(entry)) {
|
|
449
|
+
throw new TaskLedgerError('TASK_LEDGER_INVALID', 'baseline ledger entry must be an object');
|
|
450
|
+
}
|
|
451
|
+
for (const field of [
|
|
452
|
+
'id', 'project_name', 'mode', 'status', 'scope', 'revision', 'digest',
|
|
453
|
+
]) {
|
|
454
|
+
if (entry[field] === undefined || entry[field] === null || entry[field] === '') {
|
|
455
|
+
throw new TaskLedgerError(
|
|
456
|
+
'TASK_LEDGER_INVALID',
|
|
457
|
+
`baseline ledger entry ${entry.id || '(unknown)'} is missing ${field}`,
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
const value = Object.fromEntries(LEGACY_DURABLE_BASELINE_FIELDS
|
|
462
|
+
.filter((field) => entry[field] !== undefined)
|
|
463
|
+
.map((field) => [field, entry[field]]));
|
|
464
|
+
if (!HASH_PATTERN.test(entry.digest) || entry.digest !== sha256(value)) {
|
|
465
|
+
throw new TaskLedgerError(
|
|
466
|
+
'TASK_LEDGER_DIGEST_MISMATCH',
|
|
467
|
+
`baseline ledger entry ${entry.id} does not match its v1 digest`,
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
if (entry.parent_digest != null && !HASH_PATTERN.test(entry.parent_digest)) {
|
|
471
|
+
throw new TaskLedgerError(
|
|
472
|
+
'TASK_LEDGER_INVALID',
|
|
473
|
+
`baseline ledger entry ${entry.id} has an invalid parent_digest`,
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
356
478
|
function validateChange(entry) {
|
|
357
479
|
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
358
480
|
throw new TaskLedgerError('TASK_LEDGER_INVALID', 'every Change ledger entry must be an object');
|
|
@@ -384,7 +506,100 @@ function validateChange(entry) {
|
|
|
384
506
|
}
|
|
385
507
|
}
|
|
386
508
|
|
|
509
|
+
function validateDecision(entry) {
|
|
510
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
511
|
+
throw new TaskLedgerError('TASK_LEDGER_INVALID', 'every decision entry must be an object');
|
|
512
|
+
}
|
|
513
|
+
for (const field of [
|
|
514
|
+
'id', 'scope_type', 'scope_id', 'selection', 'status',
|
|
515
|
+
'artifact_path', 'artifact_digest', 'digest',
|
|
516
|
+
]) {
|
|
517
|
+
if (entry[field] === undefined || entry[field] === null || entry[field] === '') {
|
|
518
|
+
throw new TaskLedgerError(
|
|
519
|
+
'TASK_LEDGER_INVALID',
|
|
520
|
+
`decision entry ${entry.id || '(unknown)'} is missing ${field}`,
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
const value = Object.fromEntries(DURABLE_DECISION_FIELDS
|
|
525
|
+
.filter((field) => entry[field] !== undefined)
|
|
526
|
+
.map((field) => [field, entry[field]]));
|
|
527
|
+
if (!HASH_PATTERN.test(entry.artifact_digest)
|
|
528
|
+
|| !HASH_PATTERN.test(entry.digest)
|
|
529
|
+
|| entry.digest !== sha256(value)) {
|
|
530
|
+
throw new TaskLedgerError(
|
|
531
|
+
'TASK_LEDGER_DIGEST_MISMATCH',
|
|
532
|
+
`decision entry ${entry.id} does not match its digest`,
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function validateCheckpoint(entry) {
|
|
538
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
539
|
+
throw new TaskLedgerError('TASK_LEDGER_INVALID', 'every checkpoint entry must be an object');
|
|
540
|
+
}
|
|
541
|
+
for (const field of [
|
|
542
|
+
'stage', 'scope_type', 'scope_id', 'revision', 'payload',
|
|
543
|
+
'evidence', 'diagnostics', 'status', 'digest',
|
|
544
|
+
]) {
|
|
545
|
+
if (entry[field] === undefined || entry[field] === null) {
|
|
546
|
+
throw new TaskLedgerError(
|
|
547
|
+
'TASK_LEDGER_INVALID',
|
|
548
|
+
`checkpoint entry ${entry.stage || '(unknown)'} is missing ${field}`,
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
const value = Object.fromEntries(DURABLE_CHECKPOINT_FIELDS
|
|
553
|
+
.filter((field) => entry[field] !== undefined)
|
|
554
|
+
.map((field) => [field, entry[field]]));
|
|
555
|
+
if (entry.status !== 'accepted'
|
|
556
|
+
|| !HASH_PATTERN.test(entry.digest)
|
|
557
|
+
|| entry.digest !== sha256(value)) {
|
|
558
|
+
throw new TaskLedgerError(
|
|
559
|
+
'TASK_LEDGER_DIGEST_MISMATCH',
|
|
560
|
+
`checkpoint ${entry.stage}:${entry.scope_type}:${entry.scope_id} is invalid`,
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function validateLegacyLedger(document, file) {
|
|
566
|
+
if (!document || document.kind !== LEDGER_KIND
|
|
567
|
+
|| document.schema_version !== LEGACY_LEDGER_SCHEMA_VERSION
|
|
568
|
+
|| !Array.isArray(document.tasks)
|
|
569
|
+
|| !Array.isArray(document.changes)
|
|
570
|
+
|| !Array.isArray(document.ancestors)) {
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
validateLegacyBaseline(document.baseline);
|
|
574
|
+
document.tasks.forEach(validateTask);
|
|
575
|
+
document.changes.forEach(validateChange);
|
|
576
|
+
const expected = sha256({
|
|
577
|
+
baseline: document.baseline || null,
|
|
578
|
+
changes: document.changes,
|
|
579
|
+
tasks: document.tasks,
|
|
580
|
+
});
|
|
581
|
+
if (!HASH_PATTERN.test(document.state_digest || '')
|
|
582
|
+
|| document.state_digest !== expected) {
|
|
583
|
+
throw new TaskLedgerError(
|
|
584
|
+
'TASK_LEDGER_DIGEST_MISMATCH',
|
|
585
|
+
`${file} legacy state_digest does not match its durable payload`,
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
return true;
|
|
589
|
+
}
|
|
590
|
+
|
|
387
591
|
function validateLedger(document, file = '(memory)') {
|
|
592
|
+
if (document?.schema_version === LEGACY_LEDGER_SCHEMA_VERSION
|
|
593
|
+
&& validateLegacyLedger(document, file)) {
|
|
594
|
+
throw new TaskLedgerError(
|
|
595
|
+
'TASK_LEDGER_SCHEMA_MIGRATION_REQUIRED',
|
|
596
|
+
`${file} is a valid v${LEGACY_LEDGER_SCHEMA_VERSION} ledger and must migrate to v${LEDGER_SCHEMA_VERSION}`,
|
|
597
|
+
{
|
|
598
|
+
from: LEGACY_LEDGER_SCHEMA_VERSION,
|
|
599
|
+
to: LEDGER_SCHEMA_VERSION,
|
|
600
|
+
},
|
|
601
|
+
);
|
|
602
|
+
}
|
|
388
603
|
if (!document || typeof document !== 'object' || Array.isArray(document)
|
|
389
604
|
|| document.kind !== LEDGER_KIND
|
|
390
605
|
|| document.schema_version !== LEDGER_SCHEMA_VERSION
|
|
@@ -392,7 +607,9 @@ function validateLedger(document, file = '(memory)') {
|
|
|
392
607
|
|| document.generation < 0
|
|
393
608
|
|| !Array.isArray(document.ancestors)
|
|
394
609
|
|| !Array.isArray(document.tasks)
|
|
395
|
-
|| !Array.isArray(document.changes)
|
|
610
|
+
|| !Array.isArray(document.changes)
|
|
611
|
+
|| !Array.isArray(document.decisions)
|
|
612
|
+
|| !Array.isArray(document.checkpoints)) {
|
|
396
613
|
throw new TaskLedgerError(
|
|
397
614
|
'TASK_LEDGER_INVALID',
|
|
398
615
|
`${file} is not an Ultra team task ledger v${LEDGER_SCHEMA_VERSION}`,
|
|
@@ -425,6 +642,29 @@ function validateLedger(document, file = '(memory)') {
|
|
|
425
642
|
}
|
|
426
643
|
seenChanges.add(change.id);
|
|
427
644
|
}
|
|
645
|
+
const seenDecisions = new Set();
|
|
646
|
+
for (const decision of document.decisions) {
|
|
647
|
+
validateDecision(decision);
|
|
648
|
+
if (seenDecisions.has(decision.id)) {
|
|
649
|
+
throw new TaskLedgerError(
|
|
650
|
+
'TASK_LEDGER_INVALID',
|
|
651
|
+
`${file} contains duplicate decision ${decision.id}`,
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
seenDecisions.add(decision.id);
|
|
655
|
+
}
|
|
656
|
+
const seenCheckpoints = new Set();
|
|
657
|
+
for (const checkpoint of document.checkpoints) {
|
|
658
|
+
validateCheckpoint(checkpoint);
|
|
659
|
+
const key = `${checkpoint.stage}:${checkpoint.scope_type}:${checkpoint.scope_id}`;
|
|
660
|
+
if (seenCheckpoints.has(key)) {
|
|
661
|
+
throw new TaskLedgerError(
|
|
662
|
+
'TASK_LEDGER_INVALID',
|
|
663
|
+
`${file} contains multiple accepted checkpoints for ${key}`,
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
seenCheckpoints.add(key);
|
|
667
|
+
}
|
|
428
668
|
const expected = sha256(statePayload(document));
|
|
429
669
|
if (!HASH_PATTERN.test(document.state_digest || '') || document.state_digest !== expected) {
|
|
430
670
|
throw new TaskLedgerError(
|
|
@@ -556,10 +796,56 @@ function migrateLegacyProjection(db, rootDir, originalError) {
|
|
|
556
796
|
return { previous: null, migrated: true, backup };
|
|
557
797
|
}
|
|
558
798
|
|
|
799
|
+
function migrateLegacyTeamLedger(rootDir, originalError) {
|
|
800
|
+
const file = ledgerPath(rootDir);
|
|
801
|
+
let bytes;
|
|
802
|
+
let document;
|
|
803
|
+
try {
|
|
804
|
+
bytes = fs.readFileSync(file);
|
|
805
|
+
document = JSON.parse(bytes.toString('utf8'));
|
|
806
|
+
if (!validateLegacyLedger(document, file)) throw originalError;
|
|
807
|
+
} catch (error) {
|
|
808
|
+
if (error instanceof TaskLedgerError) throw error;
|
|
809
|
+
throw originalError;
|
|
810
|
+
}
|
|
811
|
+
const digest = sha256(bytes);
|
|
812
|
+
const backupDir = safeDirectoryChain(
|
|
813
|
+
rootDir,
|
|
814
|
+
['.ultra', '.runtime', 'backups', 'task-ledger'],
|
|
815
|
+
{ create: true },
|
|
816
|
+
);
|
|
817
|
+
const backup = path.join(backupDir, `team-ledger-v1-${digest.slice(0, 16)}.json`);
|
|
818
|
+
if (fs.existsSync(backup)) {
|
|
819
|
+
const stat = fs.lstatSync(backup);
|
|
820
|
+
if (stat.isSymbolicLink() || !stat.isFile()
|
|
821
|
+
|| !fs.readFileSync(backup).equals(bytes)) {
|
|
822
|
+
throw new TaskLedgerError(
|
|
823
|
+
'TASK_LEDGER_UNSAFE',
|
|
824
|
+
`legacy team ledger backup is unsafe or conflicting: ${backup}`,
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
} else {
|
|
828
|
+
fs.writeFileSync(backup, bytes, { flag: 'wx' });
|
|
829
|
+
}
|
|
830
|
+
return {
|
|
831
|
+
previous: {
|
|
832
|
+
...document,
|
|
833
|
+
decisions: [],
|
|
834
|
+
checkpoints: [],
|
|
835
|
+
},
|
|
836
|
+
migrated: true,
|
|
837
|
+
backup,
|
|
838
|
+
migration_kind: 'team-ledger-v1',
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
|
|
559
842
|
function priorLedgerForPublish(db, rootDir) {
|
|
560
843
|
try {
|
|
561
844
|
return { previous: readTaskLedger(rootDir), migrated: false, backup: null };
|
|
562
845
|
} catch (error) {
|
|
846
|
+
if (error.code === 'TASK_LEDGER_SCHEMA_MIGRATION_REQUIRED') {
|
|
847
|
+
return migrateLegacyTeamLedger(rootDir, error);
|
|
848
|
+
}
|
|
563
849
|
if (!['TASK_LEDGER_INVALID', 'TASK_LEDGER_DIGEST_MISMATCH'].includes(error.code)) {
|
|
564
850
|
throw error;
|
|
565
851
|
}
|
|
@@ -572,6 +858,9 @@ function syncTaskLedger(db, { rootDir = '.' } = {}) {
|
|
|
572
858
|
try {
|
|
573
859
|
document = readTaskLedger(rootDir);
|
|
574
860
|
} catch (error) {
|
|
861
|
+
if (error.code === 'TASK_LEDGER_SCHEMA_MIGRATION_REQUIRED') {
|
|
862
|
+
return { status: 'legacy', imported: 0, already_current: false };
|
|
863
|
+
}
|
|
575
864
|
if (!['TASK_LEDGER_INVALID', 'TASK_LEDGER_DIGEST_MISMATCH'].includes(error.code)) {
|
|
576
865
|
throw error;
|
|
577
866
|
}
|
|
@@ -598,6 +887,10 @@ function syncTaskLedger(db, { rootDir = '.' } = {}) {
|
|
|
598
887
|
deleted: 0,
|
|
599
888
|
imported_changes: 0,
|
|
600
889
|
preserved_changes: document.changes.length,
|
|
890
|
+
imported_decisions: 0,
|
|
891
|
+
preserved_decisions: document.decisions.length,
|
|
892
|
+
imported_checkpoints: 0,
|
|
893
|
+
preserved_checkpoints: document.checkpoints.length,
|
|
601
894
|
imported_baseline: false,
|
|
602
895
|
preserved_baseline: Boolean(document.baseline),
|
|
603
896
|
generation: document.generation,
|
|
@@ -617,6 +910,13 @@ function taskDigestMap(tasks) {
|
|
|
617
910
|
return Object.fromEntries(tasks.map((task) => [task.id, task.digest]));
|
|
618
911
|
}
|
|
619
912
|
|
|
913
|
+
function checkpointDigestMap(checkpoints) {
|
|
914
|
+
return Object.fromEntries(checkpoints.map((checkpoint) => [
|
|
915
|
+
`${checkpoint.stage}:${checkpoint.scope_type}:${checkpoint.scope_id}`,
|
|
916
|
+
checkpoint.digest,
|
|
917
|
+
]));
|
|
918
|
+
}
|
|
919
|
+
|
|
620
920
|
function recordLedgerEvent(db, type, ledger, extra = {}) {
|
|
621
921
|
ops.appendEvent(db, {
|
|
622
922
|
type,
|
|
@@ -627,6 +927,8 @@ function recordLedgerEvent(db, type, ledger, extra = {}) {
|
|
|
627
927
|
baseline_checkpoint: ledger.baseline || null,
|
|
628
928
|
task_digests: taskDigestMap(ledger.tasks),
|
|
629
929
|
change_digests: taskDigestMap(ledger.changes),
|
|
930
|
+
decision_digests: taskDigestMap(ledger.decisions || []),
|
|
931
|
+
checkpoint_digests: checkpointDigestMap(ledger.checkpoints || []),
|
|
630
932
|
...extra,
|
|
631
933
|
},
|
|
632
934
|
});
|
|
@@ -662,7 +964,15 @@ function publishTaskLedger(db, {
|
|
|
662
964
|
);
|
|
663
965
|
}
|
|
664
966
|
const changes = readChangesForLedger(db, rows, priorChanges);
|
|
665
|
-
const
|
|
967
|
+
const decisions = readDecisionsForLedger(db);
|
|
968
|
+
const checkpoints = readCheckpointsForLedger(db);
|
|
969
|
+
const digest = sha256(statePayload({
|
|
970
|
+
baseline,
|
|
971
|
+
changes,
|
|
972
|
+
decisions,
|
|
973
|
+
checkpoints,
|
|
974
|
+
tasks,
|
|
975
|
+
}));
|
|
666
976
|
if (previous?.state_digest === digest) {
|
|
667
977
|
return {
|
|
668
978
|
changed: false,
|
|
@@ -683,6 +993,8 @@ function publishTaskLedger(db, {
|
|
|
683
993
|
reason,
|
|
684
994
|
baseline,
|
|
685
995
|
changes,
|
|
996
|
+
decisions,
|
|
997
|
+
checkpoints,
|
|
686
998
|
tasks,
|
|
687
999
|
};
|
|
688
1000
|
validateLedger(document);
|
|
@@ -717,6 +1029,13 @@ function latestImportedCheckpoint(db) {
|
|
|
717
1029
|
change_digests: payload.change_digests && typeof payload.change_digests === 'object'
|
|
718
1030
|
? payload.change_digests
|
|
719
1031
|
: {},
|
|
1032
|
+
decision_digests: payload.decision_digests && typeof payload.decision_digests === 'object'
|
|
1033
|
+
? payload.decision_digests
|
|
1034
|
+
: {},
|
|
1035
|
+
checkpoint_digests: payload.checkpoint_digests
|
|
1036
|
+
&& typeof payload.checkpoint_digests === 'object'
|
|
1037
|
+
? payload.checkpoint_digests
|
|
1038
|
+
: {},
|
|
720
1039
|
baseline_digest: HASH_PATTERN.test(payload.baseline_digest || '')
|
|
721
1040
|
? payload.baseline_digest
|
|
722
1041
|
: null,
|
|
@@ -750,7 +1069,7 @@ function baselineForLocalCheckout(baseline) {
|
|
|
750
1069
|
worktree_accepted: false,
|
|
751
1070
|
known_red_accepted: false,
|
|
752
1071
|
gaps,
|
|
753
|
-
|
|
1072
|
+
research_checkpoint_id: undefined,
|
|
754
1073
|
approved_by: undefined,
|
|
755
1074
|
approval_note: undefined,
|
|
756
1075
|
converged_at: undefined,
|
|
@@ -770,7 +1089,8 @@ function writeBaseline(db, baseline, { update = false } = {}) {
|
|
|
770
1089
|
worktree_state = ?, worktree_digest = ?, worktree_accepted = ?,
|
|
771
1090
|
known_red_accepted = ?, spec_refs_json = ?, evidence_json = ?,
|
|
772
1091
|
verification_json = ?, unknowns_json = ?, gaps_json = ?,
|
|
773
|
-
classification_json = ?, provider_refs_json = ?,
|
|
1092
|
+
classification_json = ?, provider_refs_json = ?,
|
|
1093
|
+
research_checkpoint_id = ?, research_run_id = NULL,
|
|
774
1094
|
approved_by = ?, approval_note = ?, converged_at = ?, updated_at = ?
|
|
775
1095
|
WHERE id = ?`,
|
|
776
1096
|
).run(
|
|
@@ -793,7 +1113,7 @@ function writeBaseline(db, baseline, { update = false } = {}) {
|
|
|
793
1113
|
JSON.stringify(value.gaps || []),
|
|
794
1114
|
JSON.stringify(value.classification || {}),
|
|
795
1115
|
JSON.stringify(value.provider_refs || {}),
|
|
796
|
-
value.
|
|
1116
|
+
value.research_checkpoint_id || null,
|
|
797
1117
|
value.approved_by || null,
|
|
798
1118
|
value.approval_note || null,
|
|
799
1119
|
value.converged_at || null,
|
|
@@ -808,8 +1128,9 @@ function writeBaseline(db, baseline, { update = false } = {}) {
|
|
|
808
1128
|
repository_revision, repository_branch, worktree_state, worktree_digest,
|
|
809
1129
|
worktree_accepted, known_red_accepted, spec_refs_json, evidence_json,
|
|
810
1130
|
verification_json, unknowns_json, gaps_json, classification_json,
|
|
811
|
-
provider_refs_json,
|
|
812
|
-
|
|
1131
|
+
provider_refs_json, research_checkpoint_id, research_run_id,
|
|
1132
|
+
approved_by, approval_note, converged_at)
|
|
1133
|
+
VALUES (?, ?, ?, ?, ?, ?, '.', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?)`,
|
|
813
1134
|
).run(
|
|
814
1135
|
value.id,
|
|
815
1136
|
value.project_name,
|
|
@@ -831,7 +1152,7 @@ function writeBaseline(db, baseline, { update = false } = {}) {
|
|
|
831
1152
|
JSON.stringify(value.gaps || []),
|
|
832
1153
|
JSON.stringify(value.classification || {}),
|
|
833
1154
|
JSON.stringify(value.provider_refs || {}),
|
|
834
|
-
value.
|
|
1155
|
+
value.research_checkpoint_id || null,
|
|
835
1156
|
value.approved_by || null,
|
|
836
1157
|
value.approval_note || null,
|
|
837
1158
|
value.converged_at || null,
|
|
@@ -1034,6 +1355,166 @@ function updateTaskFromLedger(db, task) {
|
|
|
1034
1355
|
);
|
|
1035
1356
|
}
|
|
1036
1357
|
|
|
1358
|
+
function decisionDocument(rootDir, entry) {
|
|
1359
|
+
const read = readStableProjectFile(rootDir, entry.artifact_path, { encoding: 'utf8' });
|
|
1360
|
+
let document;
|
|
1361
|
+
try { document = JSON.parse(read.text); }
|
|
1362
|
+
catch (error) {
|
|
1363
|
+
throw new TaskLedgerError(
|
|
1364
|
+
'TASK_LEDGER_DECISION_INVALID',
|
|
1365
|
+
`decision artifact ${entry.artifact_path} is invalid JSON: ${error.message}`,
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1368
|
+
const { digest, ...value } = document;
|
|
1369
|
+
if (digest !== entry.artifact_digest || canonical.digest(value) !== digest) {
|
|
1370
|
+
throw new TaskLedgerError(
|
|
1371
|
+
'TASK_LEDGER_DECISION_CONFLICT',
|
|
1372
|
+
`decision artifact ${entry.artifact_path} does not match the team ledger`,
|
|
1373
|
+
{
|
|
1374
|
+
decision_id: entry.id,
|
|
1375
|
+
ledger_digest: entry.artifact_digest,
|
|
1376
|
+
artifact_digest: digest || null,
|
|
1377
|
+
},
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
return document;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
function importDecisionEntry(db, rootDir, entry) {
|
|
1384
|
+
const existing = decisionRecords.readDecision(db, entry.id);
|
|
1385
|
+
if (existing) {
|
|
1386
|
+
if (existing.digest !== entry.artifact_digest) {
|
|
1387
|
+
throw new TaskLedgerError(
|
|
1388
|
+
'TASK_LEDGER_DECISION_CONFLICT',
|
|
1389
|
+
`decision ${entry.id} changed both locally and in the team ledger`,
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1392
|
+
return false;
|
|
1393
|
+
}
|
|
1394
|
+
const document = decisionDocument(rootDir, entry);
|
|
1395
|
+
const accepted = decisionRecords.acceptDecision(db, {
|
|
1396
|
+
id: document.id,
|
|
1397
|
+
scope: document.scope.type === 'baseline'
|
|
1398
|
+
? { baseline_id: document.scope.id }
|
|
1399
|
+
: { change_id: document.scope.id },
|
|
1400
|
+
question: document.question,
|
|
1401
|
+
recommendation: document.recommendation,
|
|
1402
|
+
selection: document.selection,
|
|
1403
|
+
effects: document.effects,
|
|
1404
|
+
non_goals: document.non_goals,
|
|
1405
|
+
owner: document.owner,
|
|
1406
|
+
source: document.source,
|
|
1407
|
+
provenance: document.provenance,
|
|
1408
|
+
applied_refs: document.applied_refs,
|
|
1409
|
+
supersedes_id: document.supersedes_id || undefined,
|
|
1410
|
+
}, { rootDir });
|
|
1411
|
+
if (accepted.digest !== entry.artifact_digest) {
|
|
1412
|
+
throw new TaskLedgerError(
|
|
1413
|
+
'TASK_LEDGER_DECISION_CONFLICT',
|
|
1414
|
+
`decision ${entry.id} changed while it was imported`,
|
|
1415
|
+
);
|
|
1416
|
+
}
|
|
1417
|
+
return true;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
function mergeTeamDecisions(db, rootDir, entries) {
|
|
1421
|
+
const pending = new Map(entries.map((entry) => [entry.id, entry]));
|
|
1422
|
+
let imported = 0;
|
|
1423
|
+
while (pending.size > 0) {
|
|
1424
|
+
let progressed = false;
|
|
1425
|
+
for (const [id, entry] of pending) {
|
|
1426
|
+
if (entry.supersedes_id
|
|
1427
|
+
&& pending.has(entry.supersedes_id)
|
|
1428
|
+
&& !decisionRecords.readDecision(db, entry.supersedes_id)) {
|
|
1429
|
+
continue;
|
|
1430
|
+
}
|
|
1431
|
+
if (importDecisionEntry(db, rootDir, entry)) imported += 1;
|
|
1432
|
+
pending.delete(id);
|
|
1433
|
+
progressed = true;
|
|
1434
|
+
}
|
|
1435
|
+
if (!progressed) {
|
|
1436
|
+
throw new TaskLedgerError(
|
|
1437
|
+
'TASK_LEDGER_DECISION_CONFLICT',
|
|
1438
|
+
'decision supersession graph cannot be imported deterministically',
|
|
1439
|
+
{ decision_ids: [...pending.keys()].sort() },
|
|
1440
|
+
);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
return imported;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
function checkpointKey(entry) {
|
|
1447
|
+
return `${entry.stage}:${entry.scope_type}:${entry.scope_id}`;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
function mergeTeamCheckpoints(db, entries, lastDigests = {}) {
|
|
1451
|
+
let imported = 0;
|
|
1452
|
+
for (const entry of entries) {
|
|
1453
|
+
const current = db.prepare(
|
|
1454
|
+
`SELECT * FROM stage_checkpoints
|
|
1455
|
+
WHERE stage = ? AND scope_type = ? AND scope_id = ? AND status = 'accepted'
|
|
1456
|
+
ORDER BY revision DESC LIMIT 1`,
|
|
1457
|
+
).get(entry.stage, entry.scope_type, entry.scope_id);
|
|
1458
|
+
const currentDurable = current ? durableCheckpoint(current) : null;
|
|
1459
|
+
if (currentDurable?.digest === entry.digest) continue;
|
|
1460
|
+
const key = checkpointKey(entry);
|
|
1461
|
+
const last = lastDigests[key] || null;
|
|
1462
|
+
if (currentDurable && (!last || currentDurable.digest !== last)) {
|
|
1463
|
+
throw new TaskLedgerError(
|
|
1464
|
+
'TASK_LEDGER_CHECKPOINT_CONFLICT',
|
|
1465
|
+
`checkpoint ${key} changed both locally and in the team ledger`,
|
|
1466
|
+
{
|
|
1467
|
+
checkpoint: key,
|
|
1468
|
+
local_digest: currentDurable.digest,
|
|
1469
|
+
incoming_digest: entry.digest,
|
|
1470
|
+
last_digest: last,
|
|
1471
|
+
},
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1474
|
+
if (current) {
|
|
1475
|
+
db.prepare(
|
|
1476
|
+
`UPDATE stage_checkpoints SET status = 'superseded',
|
|
1477
|
+
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
1478
|
+
WHERE id = ?`,
|
|
1479
|
+
).run(current.id);
|
|
1480
|
+
}
|
|
1481
|
+
const importedId = `checkpoint-team-${entry.digest.slice(0, 24)}`;
|
|
1482
|
+
const importedDigest = stageCheckpoints.checkpointDigest({
|
|
1483
|
+
stage: entry.stage,
|
|
1484
|
+
scope_type: entry.scope_type,
|
|
1485
|
+
scope_id: entry.scope_id,
|
|
1486
|
+
revision: entry.revision,
|
|
1487
|
+
payload: entry.payload,
|
|
1488
|
+
evidence: entry.evidence,
|
|
1489
|
+
diagnostics: entry.diagnostics,
|
|
1490
|
+
context_envelope_id: null,
|
|
1491
|
+
supersedes_id: current?.id || null,
|
|
1492
|
+
});
|
|
1493
|
+
db.prepare(
|
|
1494
|
+
`INSERT INTO stage_checkpoints
|
|
1495
|
+
(id, stage, scope_type, scope_id, revision, status, payload_json,
|
|
1496
|
+
evidence_json, diagnostics_json, context_envelope_id, digest,
|
|
1497
|
+
supersedes_id, idempotency_key, accepted_at)
|
|
1498
|
+
VALUES (?, ?, ?, ?, ?, 'accepted', ?, ?, ?, NULL, ?, ?, ?,
|
|
1499
|
+
strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`,
|
|
1500
|
+
).run(
|
|
1501
|
+
importedId,
|
|
1502
|
+
entry.stage,
|
|
1503
|
+
entry.scope_type,
|
|
1504
|
+
entry.scope_id,
|
|
1505
|
+
entry.revision,
|
|
1506
|
+
JSON.stringify(entry.payload),
|
|
1507
|
+
JSON.stringify(entry.evidence),
|
|
1508
|
+
JSON.stringify(entry.diagnostics),
|
|
1509
|
+
importedDigest,
|
|
1510
|
+
current?.id || null,
|
|
1511
|
+
`team-import:${entry.digest}`,
|
|
1512
|
+
);
|
|
1513
|
+
imported += 1;
|
|
1514
|
+
}
|
|
1515
|
+
return imported;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1037
1518
|
function importTaskLedger(db, {
|
|
1038
1519
|
rootDir = '.',
|
|
1039
1520
|
} = {}) {
|
|
@@ -1073,6 +1554,10 @@ function importTaskLedger(db, {
|
|
|
1073
1554
|
deleted: 0,
|
|
1074
1555
|
imported_changes: 0,
|
|
1075
1556
|
preserved_changes: document.changes.length,
|
|
1557
|
+
imported_decisions: 0,
|
|
1558
|
+
preserved_decisions: document.decisions.length,
|
|
1559
|
+
imported_checkpoints: 0,
|
|
1560
|
+
preserved_checkpoints: document.checkpoints.length,
|
|
1076
1561
|
imported_baseline: false,
|
|
1077
1562
|
preserved_baseline: Boolean(document.baseline),
|
|
1078
1563
|
generation: document.generation,
|
|
@@ -1089,6 +1574,8 @@ function importTaskLedger(db, {
|
|
|
1089
1574
|
let deleted = 0;
|
|
1090
1575
|
let importedChanges = 0;
|
|
1091
1576
|
let preservedChanges = 0;
|
|
1577
|
+
let importedDecisions = 0;
|
|
1578
|
+
let importedCheckpoints = 0;
|
|
1092
1579
|
let baselineImport = {
|
|
1093
1580
|
imported: false,
|
|
1094
1581
|
preserved: false,
|
|
@@ -1242,12 +1729,20 @@ function importTaskLedger(db, {
|
|
|
1242
1729
|
}
|
|
1243
1730
|
db.prepare('UPDATE tasks SET parent_id = ? WHERE id = ?').run(parentId, id);
|
|
1244
1731
|
}
|
|
1732
|
+
importedDecisions = mergeTeamDecisions(db, rootDir, document.decisions);
|
|
1733
|
+
importedCheckpoints = mergeTeamCheckpoints(
|
|
1734
|
+
db,
|
|
1735
|
+
document.checkpoints,
|
|
1736
|
+
checkpoint.checkpoint_digests,
|
|
1737
|
+
);
|
|
1245
1738
|
recordLedgerEvent(db, 'task_ledger_imported', document, {
|
|
1246
1739
|
imported,
|
|
1247
1740
|
preserved,
|
|
1248
1741
|
deleted,
|
|
1249
1742
|
imported_changes: importedChanges,
|
|
1250
1743
|
preserved_changes: preservedChanges,
|
|
1744
|
+
imported_decisions: importedDecisions,
|
|
1745
|
+
imported_checkpoints: importedCheckpoints,
|
|
1251
1746
|
});
|
|
1252
1747
|
});
|
|
1253
1748
|
return {
|
|
@@ -1256,6 +1751,10 @@ function importTaskLedger(db, {
|
|
|
1256
1751
|
deleted,
|
|
1257
1752
|
imported_changes: importedChanges,
|
|
1258
1753
|
preserved_changes: preservedChanges,
|
|
1754
|
+
imported_decisions: importedDecisions,
|
|
1755
|
+
preserved_decisions: document.decisions.length - importedDecisions,
|
|
1756
|
+
imported_checkpoints: importedCheckpoints,
|
|
1757
|
+
preserved_checkpoints: document.checkpoints.length - importedCheckpoints,
|
|
1259
1758
|
imported_baseline: baselineImport.imported,
|
|
1260
1759
|
preserved_baseline: baselineImport.preserved,
|
|
1261
1760
|
generation: document.generation,
|
|
@@ -1289,10 +1788,14 @@ function inspectTaskLedger(db, { rootDir = '.' } = {}) {
|
|
|
1289
1788
|
localTasks,
|
|
1290
1789
|
new Map(document.changes.map((change) => [change.id, change])),
|
|
1291
1790
|
);
|
|
1791
|
+
const localDecisions = readDecisionsForLedger(db);
|
|
1792
|
+
const localCheckpoints = readCheckpointsForLedger(db);
|
|
1292
1793
|
const localBaseline = readBaselineForLedger(db, document.baseline || null);
|
|
1293
1794
|
const localDigest = sha256(statePayload({
|
|
1294
1795
|
baseline: localBaseline,
|
|
1295
1796
|
changes: localChanges,
|
|
1797
|
+
decisions: localDecisions,
|
|
1798
|
+
checkpoints: localCheckpoints,
|
|
1296
1799
|
tasks: local,
|
|
1297
1800
|
}));
|
|
1298
1801
|
const baselineRequiresRevalidation = Array.isArray(localBaseline?.gaps)
|
|
@@ -1303,6 +1806,8 @@ function inspectTaskLedger(db, { rootDir = '.' } = {}) {
|
|
|
1303
1806
|
? sha256(statePayload({
|
|
1304
1807
|
baseline: document.baseline,
|
|
1305
1808
|
changes: localChanges,
|
|
1809
|
+
decisions: localDecisions,
|
|
1810
|
+
checkpoints: localCheckpoints,
|
|
1306
1811
|
tasks: local,
|
|
1307
1812
|
}))
|
|
1308
1813
|
: null;
|
|
@@ -1324,12 +1829,16 @@ function inspectTaskLedger(db, { rootDir = '.' } = {}) {
|
|
|
1324
1829
|
module.exports = {
|
|
1325
1830
|
DURABLE_BASELINE_FIELDS,
|
|
1326
1831
|
DURABLE_CHANGE_FIELDS,
|
|
1832
|
+
DURABLE_CHECKPOINT_FIELDS,
|
|
1833
|
+
DURABLE_DECISION_FIELDS,
|
|
1327
1834
|
DURABLE_TASK_FIELDS,
|
|
1328
1835
|
LEDGER_KIND,
|
|
1329
1836
|
LEDGER_RELATIVE_PATH,
|
|
1330
1837
|
LEDGER_SCHEMA_VERSION,
|
|
1331
1838
|
TaskLedgerError,
|
|
1332
1839
|
durableChange,
|
|
1840
|
+
durableCheckpoint,
|
|
1841
|
+
durableDecision,
|
|
1333
1842
|
durableBaseline,
|
|
1334
1843
|
durableTask,
|
|
1335
1844
|
importTaskLedger,
|