ultra-builder-pro-cli 0.25.1 → 0.26.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/contexts/TEMPLATE.md +87 -0
- package/.ultra-template/decisions/.gitkeep +1 -0
- package/.ultra-template/evidence/.gitkeep +1 -0
- package/.ultra-template/north-star.md +46 -0
- package/.ultra-template/research/.gitkeep +1 -0
- package/.ultra-template/specs/discovery.md +2 -2
- package/.ultra-template/tasks.json +3 -0
- package/.ultra-template/test-report.json +27 -0
- package/CHANGELOG.md +40 -0
- package/README.md +217 -579
- package/adapters/_shared/delegate-profiles/kimi-read-only.md +15 -0
- package/adapters/_shared/delegate-profiles/kimi-write.md +17 -0
- package/adapters/_shared/file-ops.cjs +76 -2
- package/adapters/_shared/host-profile.cjs +97 -0
- package/adapters/_shared/plugin-core.cjs +204 -0
- package/adapters/_shared/runtime-assets.cjs +31 -96
- package/adapters/claude.js +92 -291
- package/adapters/codex.js +309 -842
- package/adapters/grok.js +143 -1469
- package/adapters/kimi.js +124 -1053
- package/adapters/opencode.js +230 -742
- package/bin/delegate-worker.cjs +323 -0
- package/bin/delegate.cjs +456 -0
- package/bin/install.js +38 -11
- package/docs/ARCHITECTURE.md +122 -216
- package/docs/ARTIFACT-AUTHORITY.md +90 -268
- package/docs/DECISIONS.md +75 -87
- package/docs/PHILOSOPHY.md +177 -0
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +62 -39
- package/docs/RUNTIME-COMPAT-MATRIX.md +99 -156
- package/docs/SKILL-AUTHORING.md +94 -73
- package/docs/WORKFLOW-LIFECYCLE.md +93 -189
- package/hooks/README.md +17 -37
- package/hooks/_common.py +113 -0
- package/hooks/adapters/codex.py +9 -16
- package/hooks/adapters/grok.py +5 -5
- package/hooks/adapters/kimi.py +8 -14
- package/hooks/block_dangerous_commands.py +95 -0
- package/hooks/compact_context.py +65 -0
- package/hooks/mid_workflow_recall.py +25 -0
- package/hooks/post_edit_guard.py +105 -0
- package/hooks/session_context.py +30 -0
- package/package.json +17 -52
- package/skills/ultra-change/SKILL.md +97 -50
- package/skills/ultra-change/references/change-contract.md +40 -1
- package/skills/ultra-delegate/SKILL.md +82 -0
- package/skills/ultra-delegate/references/delegation-contract.md +51 -0
- package/skills/ultra-delegate/scripts/delegate_wait.py +86 -0
- package/skills/ultra-deliver/SKILL.md +61 -42
- package/skills/ultra-deliver/references/baseline-reconciliation.md +39 -20
- package/skills/ultra-dev/SKILL.md +96 -62
- package/skills/ultra-dev/references/debugging.md +22 -0
- package/skills/ultra-domain-modeling/SKILL.md +79 -0
- package/skills/ultra-grilling/SKILL.md +79 -0
- package/skills/ultra-init/SKILL.md +94 -37
- package/skills/ultra-init/references/brownfield-adoption.md +33 -83
- package/skills/ultra-init/scripts/init_project.cjs +84 -0
- package/skills/ultra-plan/SKILL.md +88 -68
- package/skills/ultra-research/SKILL.md +62 -48
- package/skills/ultra-research/references/00-problem-validation.md +5 -5
- package/skills/ultra-research/references/01-opportunity-discovery.md +3 -3
- package/skills/ultra-research/references/02-market-assessment.md +5 -4
- package/skills/ultra-research/references/03-competitive-landscape.md +3 -3
- package/skills/ultra-research/references/04-product-strategy.md +3 -3
- package/skills/ultra-research/references/05-assumptions-validation.md +5 -5
- package/skills/ultra-research/references/10-user-personas.md +3 -2
- package/skills/ultra-research/references/11-user-scenarios.md +3 -3
- package/skills/ultra-research/references/20-user-stories.md +4 -3
- package/skills/ultra-research/references/21-features-scope.md +3 -2
- package/skills/ultra-research/references/22-success-metrics.md +3 -3
- package/skills/ultra-research/references/30-architecture-context.md +3 -3
- package/skills/ultra-research/references/31-solution-strategy.md +3 -3
- package/skills/ultra-research/references/32-building-blocks.md +3 -3
- package/skills/ultra-research/references/40-deployment.md +3 -3
- package/skills/ultra-research/references/41-quality-risks.md +3 -3
- package/skills/ultra-research/references/99-synthesis.md +7 -7
- package/skills/ultra-review/SKILL.md +59 -32
- package/skills/ultra-review/references/code.md +17 -0
- package/skills/ultra-review/references/comments.md +12 -0
- package/skills/ultra-review/references/design.md +12 -0
- package/skills/ultra-review/references/errors.md +13 -0
- package/skills/ultra-review/references/spec.md +13 -0
- package/skills/ultra-review/references/tests.md +12 -0
- package/skills/ultra-review/references/worker-packet.md +40 -0
- package/skills/ultra-review/scripts/review_wait.py +26 -15
- package/skills/ultra-status/SKILL.md +61 -36
- package/skills/ultra-tdd/SKILL.md +81 -0
- package/skills/ultra-tdd/references/templates/README.md +20 -0
- package/skills/ultra-tdd/references/templates/feature-flag-default-audit.sh +79 -0
- package/skills/ultra-tdd/references/templates/persistence-real.ts +78 -0
- package/skills/ultra-tdd/references/templates/testcontainer-postgres.py +78 -0
- package/skills/ultra-tdd/references/templates/testcontainer-postgres.ts +73 -0
- package/skills/ultra-tdd/references/templates/vertical-slice.ts +66 -0
- package/skills/ultra-tdd/references/test-execution.md +18 -0
- package/skills/ultra-test/SKILL.md +70 -30
- package/skills/ultra-test/references/export-syntax.md +18 -0
- package/skills/ultra-test/scripts/worktree_digest.cjs +54 -0
- package/skills/ultra-think/SKILL.md +49 -27
- package/skills/ultra-think/references/autonomy-boundary.md +48 -0
- package/skills/ultra-think/references/interaction-boundary.md +18 -33
- package/.ultra-template/docs/research/README.md +0 -55
- package/.ultra-template/reports/templates/delivery-report.json +0 -17
- package/.ultra-template/reports/templates/test-report.json +0 -18
- package/.ultra-template/tasks/tasks.json +0 -15
- package/.ultra-template/templates/task-context.md +0 -19
- package/adapters/_shared/codex-assets.cjs +0 -725
- package/adapters/_shared/codex-ultra-tools-entry.cjs +0 -11
- package/adapters/_shared/interaction-contract.cjs +0 -129
- package/adapters/_shared/path-rewrite.cjs +0 -79
- package/adapters/_shared/runtime-native.cjs +0 -132
- package/adapters/_shared/settings-merge.cjs +0 -76
- package/agents/code-reviewer.md +0 -42
- package/agents/debugger.md +0 -40
- package/agents/review-code.md +0 -49
- package/agents/review-comments.md +0 -38
- package/agents/review-coordinator.md +0 -56
- package/agents/review-design.md +0 -40
- package/agents/review-errors.md +0 -43
- package/agents/review-spec.md +0 -39
- package/agents/review-tests.md +0 -40
- package/agents/tdd-runner.md +0 -38
- package/bin/orchestrator.js +0 -383
- package/commands/ultra-change.md +0 -10
- package/commands/ultra-deliver.md +0 -10
- package/commands/ultra-dev.md +0 -10
- package/commands/ultra-doctor.md +0 -10
- package/commands/ultra-init.md +0 -10
- package/commands/ultra-plan.md +0 -10
- package/commands/ultra-research.md +0 -10
- package/commands/ultra-review.md +0 -10
- package/commands/ultra-status.md +0 -10
- package/commands/ultra-test.md +0 -10
- package/commands/ultra-think.md +0 -10
- package/docs/AGENT-CONTEXT.md +0 -191
- package/docs/COMMIT-HASH-BACKFILL.md +0 -51
- package/docs/LEGACY-CLI-CROSSWALK.md +0 -175
- package/docs/ROADMAP.md +0 -101
- package/docs/STATE-DB-ACCESS-POLICY.md +0 -161
- package/hooks/active_task_context.py +0 -152
- package/hooks/context_envelope.py +0 -100
- package/hooks/health_check.py +0 -196
- package/hooks/pre_stop_check.py +0 -66
- package/hooks/runtime_paths.py +0 -396
- package/hooks/subagent_tracker.py +0 -88
- package/hooks/workflow_checkpoint.py +0 -76
- package/hooks/workflow_context.py +0 -48
- package/hooks/workflow_resume.py +0 -84
- package/mcp-server/hook-context.cjs +0 -38
- package/mcp-server/hook-event.cjs +0 -34
- package/mcp-server/lib/archive-journal.cjs +0 -1119
- package/mcp-server/lib/archive-mutation-worker.py +0 -365
- package/mcp-server/lib/artifact-registry.cjs +0 -1232
- package/mcp-server/lib/baseline-workflow.cjs +0 -1193
- package/mcp-server/lib/canonical-json.cjs +0 -19
- package/mcp-server/lib/change-authority.cjs +0 -26
- package/mcp-server/lib/change-packet.cjs +0 -790
- package/mcp-server/lib/change-workflow.cjs +0 -65
- package/mcp-server/lib/context-envelope.cjs +0 -581
- package/mcp-server/lib/context-paths.cjs +0 -142
- package/mcp-server/lib/decision-records.cjs +0 -423
- package/mcp-server/lib/delivery-transaction.cjs +0 -471
- package/mcp-server/lib/doctor-backup-worker.cjs +0 -328
- package/mcp-server/lib/doctor.cjs +0 -627
- package/mcp-server/lib/git-bootstrap.cjs +0 -651
- package/mcp-server/lib/init-project.cjs +0 -1021
- package/mcp-server/lib/kernel-change-workflow.cjs +0 -2029
- package/mcp-server/lib/legacy-change-workflow.cjs +0 -3032
- package/mcp-server/lib/managed-file-write.cjs +0 -172
- package/mcp-server/lib/plan-checkpoint.cjs +0 -177
- package/mcp-server/lib/plan-publication-worker.cjs +0 -830
- package/mcp-server/lib/plan-store.cjs +0 -794
- package/mcp-server/lib/prd-parser.cjs +0 -83
- package/mcp-server/lib/pricing.cjs +0 -44
- package/mcp-server/lib/project-context.cjs +0 -178
- package/mcp-server/lib/projector.cjs +0 -386
- package/mcp-server/lib/provider-refs.cjs +0 -42
- package/mcp-server/lib/runtime-paths.cjs +0 -1714
- package/mcp-server/lib/runtime-state.cjs +0 -222
- package/mcp-server/lib/safe-project-file.cjs +0 -385
- package/mcp-server/lib/stage-checkpoints.cjs +0 -463
- package/mcp-server/lib/state-authority.cjs +0 -95
- package/mcp-server/lib/state-db.cjs +0 -1580
- package/mcp-server/lib/state-ops.cjs +0 -1428
- package/mcp-server/lib/task-contract.cjs +0 -404
- package/mcp-server/lib/task-expander.cjs +0 -105
- package/mcp-server/lib/task-ledger.cjs +0 -1964
- package/mcp-server/lib/telemetry.cjs +0 -85
- package/mcp-server/lib/topo.cjs +0 -131
- package/mcp-server/lib/ultra-facade.cjs +0 -2684
- package/mcp-server/lib/worker-packet.cjs +0 -358
- package/mcp-server/server.cjs +0 -437
- package/orchestrator/auto-merge.cjs +0 -128
- package/orchestrator/daemon.cjs +0 -264
- package/orchestrator/dispatch-rules.cjs +0 -78
- package/orchestrator/parallel-orchestrator.cjs +0 -903
- package/orchestrator/planner/plan-builder.cjs +0 -125
- package/orchestrator/recovery.cjs +0 -188
- package/orchestrator/session-close-journal-worker.cjs +0 -5
- package/orchestrator/session-close-journal.cjs +0 -1014
- package/orchestrator/session-runner.cjs +0 -1153
- package/orchestrator/worktree-manager.cjs +0 -282
- package/settings.json +0 -7
- package/skills/cc-collab/SKILL.md +0 -42
- package/skills/code-review-expert/SKILL.md +0 -43
- package/skills/code-review-expert/references/correctness-reliability.md +0 -30
- package/skills/code-review-expert/references/design-boundaries.md +0 -26
- package/skills/code-review-expert/references/removal-plan.md +0 -24
- package/skills/codex-collab/SKILL.md +0 -49
- package/skills/codex-collab/evals/evals.json +0 -35
- package/skills/integration-rules/SKILL.md +0 -32
- package/skills/security-rules/SKILL.md +0 -38
- package/skills/testing-rules/SKILL.md +0 -32
- package/skills/ultra-doctor/SKILL.md +0 -41
- package/skills/ultra-plan/references/semantic-preflight.md +0 -20
- package/skills/ultra-verify/SKILL.md +0 -46
- package/skills/ultra-verify/evals/evals.json +0 -46
- package/skills/ultra-verify/references/cross-verify-modes.md +0 -28
- package/skills/ultra-verify/references/evidence-status.md +0 -21
- package/skills/ultra-verify/scripts/verify_wait.py +0 -94
- package/spec/cli-protocol.md +0 -102
- package/spec/mcp-tools.yaml +0 -312
- package/spec/schemas/baseline-reconciliation.v1.schema.json +0 -73
- package/spec/schemas/change-delta.v1.schema.json +0 -153
- package/spec/schemas/command-manifest.schema.json +0 -29
- package/spec/schemas/context-file.v4.5.schema.json +0 -54
- package/spec/schemas/delivery-report.v1.schema.json +0 -66
- package/spec/schemas/documentation-reconciliation.v1.schema.json +0 -106
- package/spec/schemas/mcp-tools.schema.json +0 -136
- package/spec/schemas/skill-manifest.schema.json +0 -24
- package/spec/schemas/state-db.sql +0 -641
- package/spec/schemas/task-ledger.v2.schema.json +0 -296
- package/spec/schemas/tasks.v4.5.schema.json +0 -96
- package/spec/schemas/test-report.v1.schema.json +0 -225
- package/ultra-tools/cli.cjs +0 -129
- package/ultra-tools/commands/db.cjs +0 -226
- package/ultra-tools/commands/legacy-memory.cjs +0 -159
- package/ultra-tools/commands/migrate.cjs +0 -1386
- package/ultra-tools/commands/session.cjs +0 -213
- package/ultra-tools/commands/status.cjs +0 -559
- package/ultra-tools/commands/system.cjs +0 -498
- package/ultra-tools/commands/task.cjs +0 -105
|
@@ -1,1964 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const crypto = require('node:crypto');
|
|
4
|
-
const fs = require('node:fs');
|
|
5
|
-
const path = require('node:path');
|
|
6
|
-
const util = require('node:util');
|
|
7
|
-
const Ajv = require('ajv/dist/2020');
|
|
8
|
-
const addFormats = require('ajv-formats');
|
|
9
|
-
|
|
10
|
-
const canonical = require('./canonical-json.cjs');
|
|
11
|
-
const decisionRecords = require('./decision-records.cjs');
|
|
12
|
-
const {
|
|
13
|
-
restoreManagedFile,
|
|
14
|
-
snapshotManagedFile,
|
|
15
|
-
writeManagedFile,
|
|
16
|
-
} = require('./managed-file-write.cjs');
|
|
17
|
-
const ops = require('./state-ops.cjs');
|
|
18
|
-
const { readStableProjectFile } = require('./safe-project-file.cjs');
|
|
19
|
-
const stageCheckpoints = require('./stage-checkpoints.cjs');
|
|
20
|
-
const taskContract = require('./task-contract.cjs');
|
|
21
|
-
const taskLedgerSchema = require('../../spec/schemas/task-ledger.v2.schema.json');
|
|
22
|
-
|
|
23
|
-
const taskLedgerAjv = new Ajv({ allErrors: true, strict: false });
|
|
24
|
-
addFormats(taskLedgerAjv);
|
|
25
|
-
const validateTaskLedgerSchema = taskLedgerAjv.compile(taskLedgerSchema);
|
|
26
|
-
|
|
27
|
-
const LEDGER_KIND = 'ultra-team-task-ledger';
|
|
28
|
-
const LEDGER_SCHEMA_VERSION = '2.0';
|
|
29
|
-
const LEGACY_LEDGER_SCHEMA_VERSION = '1.0';
|
|
30
|
-
const LEDGER_RELATIVE_PATH = '.ultra/tasks/tasks.json';
|
|
31
|
-
const HASH_PATTERN = /^[0-9a-f]{64}$/;
|
|
32
|
-
const MAX_ANCESTORS = 64;
|
|
33
|
-
const PUBLICATION_ROLLBACK = Symbol('task-ledger-publication-rollback');
|
|
34
|
-
const DURABLE_BASELINE_FIELDS = Object.freeze([
|
|
35
|
-
'id',
|
|
36
|
-
'project_name',
|
|
37
|
-
'project_type',
|
|
38
|
-
'stack',
|
|
39
|
-
'mode',
|
|
40
|
-
'status',
|
|
41
|
-
'scope',
|
|
42
|
-
'repository_revision',
|
|
43
|
-
'repository_branch',
|
|
44
|
-
'worktree_state',
|
|
45
|
-
'worktree_digest',
|
|
46
|
-
'worktree_accepted',
|
|
47
|
-
'known_red_accepted',
|
|
48
|
-
'spec_refs',
|
|
49
|
-
'evidence',
|
|
50
|
-
'verification',
|
|
51
|
-
'unknowns',
|
|
52
|
-
'gaps',
|
|
53
|
-
'classification',
|
|
54
|
-
'provider_refs',
|
|
55
|
-
'research_checkpoint_id',
|
|
56
|
-
'approved_by',
|
|
57
|
-
'approval_note',
|
|
58
|
-
'converged_at',
|
|
59
|
-
]);
|
|
60
|
-
const LEGACY_DURABLE_BASELINE_FIELDS = Object.freeze(
|
|
61
|
-
DURABLE_BASELINE_FIELDS.map((field) => (
|
|
62
|
-
field === 'research_checkpoint_id' ? 'research_run_id' : field
|
|
63
|
-
)),
|
|
64
|
-
);
|
|
65
|
-
const DURABLE_TASK_FIELDS = taskContract.DURABLE_TASK_FIELDS;
|
|
66
|
-
const DURABLE_CHANGE_FIELDS = Object.freeze([
|
|
67
|
-
'id',
|
|
68
|
-
'title',
|
|
69
|
-
'kind',
|
|
70
|
-
'status',
|
|
71
|
-
'intent',
|
|
72
|
-
'docs_impact',
|
|
73
|
-
'provider_refs',
|
|
74
|
-
'baseline_bypass',
|
|
75
|
-
'contract',
|
|
76
|
-
'classification',
|
|
77
|
-
'research_disposition',
|
|
78
|
-
'base_commit',
|
|
79
|
-
'supersedes_id',
|
|
80
|
-
'artifact_root',
|
|
81
|
-
'closed_at',
|
|
82
|
-
]);
|
|
83
|
-
const DURABLE_DECISION_FIELDS = Object.freeze([
|
|
84
|
-
'id',
|
|
85
|
-
'scope_type',
|
|
86
|
-
'scope_id',
|
|
87
|
-
'selection',
|
|
88
|
-
'effects',
|
|
89
|
-
'status',
|
|
90
|
-
'artifact_path',
|
|
91
|
-
'artifact_digest',
|
|
92
|
-
'supersedes_id',
|
|
93
|
-
]);
|
|
94
|
-
const DURABLE_CHECKPOINT_FIELDS = Object.freeze([
|
|
95
|
-
'stage',
|
|
96
|
-
'scope_type',
|
|
97
|
-
'scope_id',
|
|
98
|
-
'revision',
|
|
99
|
-
'payload',
|
|
100
|
-
'evidence',
|
|
101
|
-
'diagnostics',
|
|
102
|
-
'status',
|
|
103
|
-
]);
|
|
104
|
-
|
|
105
|
-
class TaskLedgerError extends Error {
|
|
106
|
-
constructor(code, message, details) {
|
|
107
|
-
super(message);
|
|
108
|
-
this.name = 'TaskLedgerError';
|
|
109
|
-
this.code = code;
|
|
110
|
-
if (details) this.details = details;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function ledgerPath(rootDir) {
|
|
115
|
-
return path.join(path.resolve(rootDir || '.'), ...LEDGER_RELATIVE_PATH.split('/'));
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function safeDirectoryChain(rootDir, components, { create = false } = {}) {
|
|
119
|
-
const root = path.resolve(rootDir || '.');
|
|
120
|
-
const rootStat = fs.lstatSync(root, { throwIfNoEntry: false });
|
|
121
|
-
if (!rootStat || rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
122
|
-
throw new TaskLedgerError('TASK_LEDGER_UNSAFE', `project root is unsafe: ${root}`);
|
|
123
|
-
}
|
|
124
|
-
const physicalRoot = fs.realpathSync.native(root);
|
|
125
|
-
let current = root;
|
|
126
|
-
for (const component of components) {
|
|
127
|
-
current = path.join(current, component);
|
|
128
|
-
let stat = fs.lstatSync(current, { throwIfNoEntry: false });
|
|
129
|
-
if (!stat && create) {
|
|
130
|
-
fs.mkdirSync(current);
|
|
131
|
-
stat = fs.lstatSync(current);
|
|
132
|
-
}
|
|
133
|
-
if (!stat) return null;
|
|
134
|
-
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
135
|
-
throw new TaskLedgerError(
|
|
136
|
-
'TASK_LEDGER_UNSAFE',
|
|
137
|
-
`task ledger directory is unsafe: ${current}`,
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
const physical = fs.realpathSync.native(current);
|
|
141
|
-
if (physical !== physicalRoot && !physical.startsWith(`${physicalRoot}${path.sep}`)) {
|
|
142
|
-
throw new TaskLedgerError(
|
|
143
|
-
'TASK_LEDGER_UNSAFE',
|
|
144
|
-
`task ledger directory escapes the project: ${current}`,
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
return current;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function sha256(value) {
|
|
152
|
-
return crypto.createHash('sha256').update(
|
|
153
|
-
typeof value === 'string' ? value : JSON.stringify(value),
|
|
154
|
-
).digest('hex');
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function parseJson(value, fallback) {
|
|
158
|
-
if (value == null || value === '') return fallback;
|
|
159
|
-
try { return JSON.parse(value); }
|
|
160
|
-
catch { return fallback; }
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function compactObject(entries) {
|
|
164
|
-
return Object.fromEntries(entries.filter(([, value]) => value !== null && value !== undefined));
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function portableBaseline(row) {
|
|
168
|
-
if (!row) return null;
|
|
169
|
-
return compactObject([
|
|
170
|
-
['id', row.id],
|
|
171
|
-
['project_name', row.project_name],
|
|
172
|
-
['project_type', row.project_type],
|
|
173
|
-
['stack', row.stack],
|
|
174
|
-
['mode', row.mode],
|
|
175
|
-
['status', row.status],
|
|
176
|
-
['scope', Array.isArray(row.scope) ? row.scope : parseJson(row.scope_json, ['.'])],
|
|
177
|
-
['repository_revision', row.repository_revision],
|
|
178
|
-
['repository_branch', row.repository_branch],
|
|
179
|
-
['worktree_state', row.worktree_state],
|
|
180
|
-
['worktree_digest', row.worktree_digest],
|
|
181
|
-
['worktree_accepted', Boolean(row.worktree_accepted)],
|
|
182
|
-
['known_red_accepted', Boolean(row.known_red_accepted)],
|
|
183
|
-
['spec_refs', Array.isArray(row.spec_refs) ? row.spec_refs : parseJson(row.spec_refs_json, [])],
|
|
184
|
-
['evidence', Array.isArray(row.evidence) ? row.evidence : parseJson(row.evidence_json, [])],
|
|
185
|
-
['verification', Array.isArray(row.verification)
|
|
186
|
-
? row.verification : parseJson(row.verification_json, [])],
|
|
187
|
-
['unknowns', Array.isArray(row.unknowns) ? row.unknowns : parseJson(row.unknowns_json, [])],
|
|
188
|
-
['gaps', Array.isArray(row.gaps) ? row.gaps : parseJson(row.gaps_json, [])],
|
|
189
|
-
['classification', row.classification && typeof row.classification === 'object'
|
|
190
|
-
? row.classification : parseJson(row.classification_json, {})],
|
|
191
|
-
['provider_refs', row.provider_refs && typeof row.provider_refs === 'object'
|
|
192
|
-
? row.provider_refs : parseJson(row.provider_refs_json, {})],
|
|
193
|
-
['research_checkpoint_id', row.research_checkpoint_id || row.research_run_id],
|
|
194
|
-
['approved_by', row.approved_by],
|
|
195
|
-
['approval_note', row.approval_note],
|
|
196
|
-
['converged_at', row.converged_at],
|
|
197
|
-
]);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function durableBaseline(row, previous = null) {
|
|
201
|
-
if (!row) return null;
|
|
202
|
-
const value = portableBaseline(row);
|
|
203
|
-
const digest = sha256(value);
|
|
204
|
-
if (previous?.digest === digest) return previous;
|
|
205
|
-
return {
|
|
206
|
-
...value,
|
|
207
|
-
revision: previous ? Number(previous.revision || 0) + 1 : 1,
|
|
208
|
-
parent_digest: previous?.digest || null,
|
|
209
|
-
digest,
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function readBaselineForLedger(db, previous = null) {
|
|
214
|
-
const row = db.prepare(
|
|
215
|
-
`SELECT * FROM baselines
|
|
216
|
-
WHERE status <> 'superseded'
|
|
217
|
-
ORDER BY CASE status WHEN 'ready' THEN 0 ELSE 1 END, updated_at DESC, rowid DESC
|
|
218
|
-
LIMIT 1`,
|
|
219
|
-
).get();
|
|
220
|
-
return durableBaseline(row, previous);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function portableChange(row) {
|
|
224
|
-
return compactObject([
|
|
225
|
-
['id', row.id],
|
|
226
|
-
['title', row.title],
|
|
227
|
-
['kind', row.kind],
|
|
228
|
-
['status', ['blocked', 'ready'].includes(row.status) ? 'active' : row.status],
|
|
229
|
-
['intent', row.intent],
|
|
230
|
-
['docs_impact', parseJson(row.docs_impact_json, {})],
|
|
231
|
-
['provider_refs', parseJson(row.provider_refs_json, {})],
|
|
232
|
-
['baseline_bypass', parseJson(row.baseline_bypass_json, null)],
|
|
233
|
-
['contract', parseJson(row.contract_json, {})],
|
|
234
|
-
['classification', parseJson(row.classification_json, {})],
|
|
235
|
-
['research_disposition', parseJson(row.research_disposition_json, {})],
|
|
236
|
-
['base_commit', row.base_commit],
|
|
237
|
-
['supersedes_id', row.supersedes_id],
|
|
238
|
-
['artifact_root', row.artifact_root],
|
|
239
|
-
['closed_at', row.closed_at],
|
|
240
|
-
]);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function durableChange(row, previous = null) {
|
|
244
|
-
const value = portableChange(row);
|
|
245
|
-
const digest = sha256(value);
|
|
246
|
-
if (previous?.digest === digest) return previous;
|
|
247
|
-
return {
|
|
248
|
-
...value,
|
|
249
|
-
revision: previous ? Number(previous.revision || 0) + 1 : 1,
|
|
250
|
-
parent_digest: previous?.digest || null,
|
|
251
|
-
digest,
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function readChangesForLedger(db, taskRows, previousChanges = new Map()) {
|
|
256
|
-
const rows = db.prepare('SELECT * FROM changes ORDER BY id').all();
|
|
257
|
-
const byId = new Map(rows.map((row) => [row.id, row]));
|
|
258
|
-
for (const task of taskRows) {
|
|
259
|
-
if (!task.change_id) continue;
|
|
260
|
-
const row = byId.get(task.change_id);
|
|
261
|
-
if (!row) {
|
|
262
|
-
throw new TaskLedgerError(
|
|
263
|
-
'TASK_LEDGER_CHANGE_MISSING',
|
|
264
|
-
`task ledger cannot publish missing owning change ${task.change_id}`,
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
return rows.map((row) => durableChange(row, previousChanges.get(row.id)));
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function durableDecision(row) {
|
|
272
|
-
const value = compactObject([
|
|
273
|
-
['id', row.id],
|
|
274
|
-
['scope_type', row.scope_type],
|
|
275
|
-
['scope_id', row.scope_id],
|
|
276
|
-
['selection', row.selection],
|
|
277
|
-
['effects', parseJson(row.effects_json, {})],
|
|
278
|
-
['status', row.status],
|
|
279
|
-
['artifact_path', row.artifact_path],
|
|
280
|
-
['artifact_digest', row.digest],
|
|
281
|
-
['supersedes_id', row.supersedes_id],
|
|
282
|
-
]);
|
|
283
|
-
return {
|
|
284
|
-
...value,
|
|
285
|
-
digest: sha256(value),
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
function readDecisionsForLedger(db) {
|
|
290
|
-
return db.prepare(
|
|
291
|
-
`SELECT * FROM decision_records
|
|
292
|
-
WHERE status IN ('accepted', 'superseded')
|
|
293
|
-
ORDER BY scope_type, scope_id, created_at, id`,
|
|
294
|
-
).all().map(durableDecision);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function durableCheckpoint(row) {
|
|
298
|
-
const value = compactObject([
|
|
299
|
-
['stage', row.stage],
|
|
300
|
-
['scope_type', row.scope_type],
|
|
301
|
-
['scope_id', row.scope_id],
|
|
302
|
-
['revision', row.revision],
|
|
303
|
-
['payload', parseJson(row.payload_json, {})],
|
|
304
|
-
['evidence', parseJson(row.evidence_json, [])],
|
|
305
|
-
['diagnostics', parseJson(row.diagnostics_json, [])],
|
|
306
|
-
['status', row.status],
|
|
307
|
-
]);
|
|
308
|
-
return {
|
|
309
|
-
...value,
|
|
310
|
-
digest: sha256(value),
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function readCheckpointsForLedger(db) {
|
|
315
|
-
return db.prepare(
|
|
316
|
-
`SELECT * FROM stage_checkpoints
|
|
317
|
-
WHERE status = 'accepted'
|
|
318
|
-
ORDER BY stage, scope_type, scope_id, revision`,
|
|
319
|
-
).all().map(durableCheckpoint);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function durableStatus(task, previous) {
|
|
323
|
-
if (task.status !== 'in_progress') return task.status;
|
|
324
|
-
if (previous && previous.status !== 'in_progress') return previous.status;
|
|
325
|
-
return 'pending';
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function assertValidTaskContract(entry) {
|
|
329
|
-
try {
|
|
330
|
-
taskContract.validateDurableTask(entry, { metadata: false });
|
|
331
|
-
} catch (error) {
|
|
332
|
-
if (!(error instanceof taskContract.TaskContractError)) throw error;
|
|
333
|
-
throw new TaskLedgerError(
|
|
334
|
-
'TASK_LEDGER_INVALID',
|
|
335
|
-
`task ledger entry ${entry?.id || '(unknown)'} violates the Task Contract: ${error.message}`,
|
|
336
|
-
error.details,
|
|
337
|
-
);
|
|
338
|
-
}
|
|
339
|
-
return entry;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
function durableTask(task, previous = null) {
|
|
343
|
-
const value = compactObject([
|
|
344
|
-
['id', task.id],
|
|
345
|
-
['title', task.title],
|
|
346
|
-
['type', task.type],
|
|
347
|
-
['priority', task.priority],
|
|
348
|
-
['complexity', task.complexity],
|
|
349
|
-
['estimated_days', task.estimated_days],
|
|
350
|
-
['status', durableStatus(task, previous)],
|
|
351
|
-
['deps', task.deps == null ? [] : task.deps],
|
|
352
|
-
['files_modified', task.files_modified == null ? [] : task.files_modified],
|
|
353
|
-
['stale', task.stale == null ? false : task.stale],
|
|
354
|
-
['trace_to', task.trace_to],
|
|
355
|
-
['outcome', task.outcome],
|
|
356
|
-
['slice_kind', task.slice_kind],
|
|
357
|
-
['public_seam', task.public_seam],
|
|
358
|
-
['verification_command', task.verification_command],
|
|
359
|
-
['acceptance', task.acceptance == null ? [] : task.acceptance],
|
|
360
|
-
['context_refs', task.context_refs == null ? [] : task.context_refs],
|
|
361
|
-
['docs_impact', task.docs_impact == null
|
|
362
|
-
? { status: 'unknown', files: [], rationale: null }
|
|
363
|
-
: task.docs_impact],
|
|
364
|
-
['ownership', task.ownership == null ? {} : task.ownership],
|
|
365
|
-
['change_id', task.change_id],
|
|
366
|
-
['parent_id', task.parent_id],
|
|
367
|
-
]);
|
|
368
|
-
const digest = sha256(value);
|
|
369
|
-
const entry = {
|
|
370
|
-
...value,
|
|
371
|
-
revision: previous ? Number(previous.revision || 0) + 1 : 1,
|
|
372
|
-
parent_digest: previous?.digest || null,
|
|
373
|
-
digest,
|
|
374
|
-
};
|
|
375
|
-
assertValidTaskContract(entry);
|
|
376
|
-
if (previous && previous.digest === digest) return previous;
|
|
377
|
-
return entry;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function statePayload({
|
|
381
|
-
baseline,
|
|
382
|
-
changes,
|
|
383
|
-
decisions,
|
|
384
|
-
checkpoints,
|
|
385
|
-
tasks,
|
|
386
|
-
}) {
|
|
387
|
-
return {
|
|
388
|
-
baseline: baseline || null,
|
|
389
|
-
changes: Array.isArray(changes) ? changes : [],
|
|
390
|
-
decisions: Array.isArray(decisions) ? decisions : [],
|
|
391
|
-
checkpoints: Array.isArray(checkpoints) ? checkpoints : [],
|
|
392
|
-
tasks: Array.isArray(tasks) ? tasks : [],
|
|
393
|
-
};
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
function validateTask(entry) {
|
|
397
|
-
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
398
|
-
throw new TaskLedgerError('TASK_LEDGER_INVALID', 'every task ledger entry must be an object');
|
|
399
|
-
}
|
|
400
|
-
for (const field of [
|
|
401
|
-
'id', 'title', 'type', 'priority', 'status', 'revision', 'digest',
|
|
402
|
-
]) {
|
|
403
|
-
if (entry[field] === undefined || entry[field] === null || entry[field] === '') {
|
|
404
|
-
throw new TaskLedgerError(
|
|
405
|
-
'TASK_LEDGER_INVALID',
|
|
406
|
-
`task ledger entry ${entry.id || '(unknown)'} is missing ${field}`,
|
|
407
|
-
);
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
if (!Object.prototype.hasOwnProperty.call(entry, 'parent_digest')) {
|
|
411
|
-
throw new TaskLedgerError(
|
|
412
|
-
'TASK_LEDGER_INVALID',
|
|
413
|
-
`task ledger entry ${entry.id || '(unknown)'} is missing parent_digest`,
|
|
414
|
-
);
|
|
415
|
-
}
|
|
416
|
-
assertValidTaskContract(entry);
|
|
417
|
-
if (!Number.isInteger(entry.revision) || entry.revision < 1) {
|
|
418
|
-
throw new TaskLedgerError(
|
|
419
|
-
'TASK_LEDGER_INVALID',
|
|
420
|
-
`task ledger entry ${entry.id} has an invalid revision`,
|
|
421
|
-
);
|
|
422
|
-
}
|
|
423
|
-
if (!HASH_PATTERN.test(entry.digest) || entry.digest !== sha256(
|
|
424
|
-
Object.fromEntries(DURABLE_TASK_FIELDS
|
|
425
|
-
.filter((field) => entry[field] !== undefined)
|
|
426
|
-
.map((field) => [field, entry[field]])),
|
|
427
|
-
)) {
|
|
428
|
-
throw new TaskLedgerError(
|
|
429
|
-
'TASK_LEDGER_DIGEST_MISMATCH',
|
|
430
|
-
`task ledger entry ${entry.id} does not match its digest`,
|
|
431
|
-
);
|
|
432
|
-
}
|
|
433
|
-
if (entry.parent_digest != null && !HASH_PATTERN.test(entry.parent_digest)) {
|
|
434
|
-
throw new TaskLedgerError(
|
|
435
|
-
'TASK_LEDGER_INVALID',
|
|
436
|
-
`task ledger entry ${entry.id} has an invalid parent_digest`,
|
|
437
|
-
);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
function validateLegacyTask(entry) {
|
|
442
|
-
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
443
|
-
throw new TaskLedgerError('TASK_LEDGER_INVALID', 'every legacy task entry must be an object');
|
|
444
|
-
}
|
|
445
|
-
for (const field of ['id', 'title', 'type', 'priority', 'status', 'revision', 'digest']) {
|
|
446
|
-
if (entry[field] === undefined || entry[field] === null || entry[field] === '') {
|
|
447
|
-
throw new TaskLedgerError(
|
|
448
|
-
'TASK_LEDGER_INVALID',
|
|
449
|
-
`legacy task entry ${entry.id || '(unknown)'} is missing ${field}`,
|
|
450
|
-
);
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
const value = Object.fromEntries(DURABLE_TASK_FIELDS
|
|
454
|
-
.filter((field) => entry[field] !== undefined)
|
|
455
|
-
.map((field) => [field, entry[field]]));
|
|
456
|
-
if (!HASH_PATTERN.test(entry.digest) || entry.digest !== sha256(value)) {
|
|
457
|
-
throw new TaskLedgerError(
|
|
458
|
-
'TASK_LEDGER_DIGEST_MISMATCH',
|
|
459
|
-
`legacy task entry ${entry.id} does not match its digest`,
|
|
460
|
-
);
|
|
461
|
-
}
|
|
462
|
-
if (entry.parent_digest != null && !HASH_PATTERN.test(entry.parent_digest)) {
|
|
463
|
-
throw new TaskLedgerError(
|
|
464
|
-
'TASK_LEDGER_INVALID',
|
|
465
|
-
`legacy task entry ${entry.id} has an invalid parent_digest`,
|
|
466
|
-
);
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
function validateBaseline(entry) {
|
|
471
|
-
if (entry == null) return;
|
|
472
|
-
if (typeof entry !== 'object' || Array.isArray(entry)) {
|
|
473
|
-
throw new TaskLedgerError('TASK_LEDGER_INVALID', 'baseline ledger entry must be an object');
|
|
474
|
-
}
|
|
475
|
-
for (const field of [
|
|
476
|
-
'id', 'project_name', 'mode', 'status', 'scope', 'revision', 'digest',
|
|
477
|
-
]) {
|
|
478
|
-
if (entry[field] === undefined || entry[field] === null || entry[field] === '') {
|
|
479
|
-
throw new TaskLedgerError(
|
|
480
|
-
'TASK_LEDGER_INVALID',
|
|
481
|
-
`baseline ledger entry ${entry.id || '(unknown)'} is missing ${field}`,
|
|
482
|
-
);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
const value = Object.fromEntries(DURABLE_BASELINE_FIELDS
|
|
486
|
-
.filter((field) => entry[field] !== undefined)
|
|
487
|
-
.map((field) => [field, entry[field]]));
|
|
488
|
-
if (!HASH_PATTERN.test(entry.digest) || entry.digest !== sha256(value)) {
|
|
489
|
-
throw new TaskLedgerError(
|
|
490
|
-
'TASK_LEDGER_DIGEST_MISMATCH',
|
|
491
|
-
`baseline ledger entry ${entry.id} does not match its digest`,
|
|
492
|
-
);
|
|
493
|
-
}
|
|
494
|
-
if (entry.parent_digest != null && !HASH_PATTERN.test(entry.parent_digest)) {
|
|
495
|
-
throw new TaskLedgerError(
|
|
496
|
-
'TASK_LEDGER_INVALID',
|
|
497
|
-
`baseline ledger entry ${entry.id} has an invalid parent_digest`,
|
|
498
|
-
);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
function validateLegacyBaseline(entry) {
|
|
503
|
-
if (entry == null) return;
|
|
504
|
-
if (typeof entry !== 'object' || Array.isArray(entry)) {
|
|
505
|
-
throw new TaskLedgerError('TASK_LEDGER_INVALID', 'baseline ledger entry must be an object');
|
|
506
|
-
}
|
|
507
|
-
for (const field of [
|
|
508
|
-
'id', 'project_name', 'mode', 'status', 'scope', 'revision', 'digest',
|
|
509
|
-
]) {
|
|
510
|
-
if (entry[field] === undefined || entry[field] === null || entry[field] === '') {
|
|
511
|
-
throw new TaskLedgerError(
|
|
512
|
-
'TASK_LEDGER_INVALID',
|
|
513
|
-
`baseline ledger entry ${entry.id || '(unknown)'} is missing ${field}`,
|
|
514
|
-
);
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
const value = Object.fromEntries(LEGACY_DURABLE_BASELINE_FIELDS
|
|
518
|
-
.filter((field) => entry[field] !== undefined)
|
|
519
|
-
.map((field) => [field, entry[field]]));
|
|
520
|
-
if (!HASH_PATTERN.test(entry.digest) || entry.digest !== sha256(value)) {
|
|
521
|
-
throw new TaskLedgerError(
|
|
522
|
-
'TASK_LEDGER_DIGEST_MISMATCH',
|
|
523
|
-
`baseline ledger entry ${entry.id} does not match its v1 digest`,
|
|
524
|
-
);
|
|
525
|
-
}
|
|
526
|
-
if (entry.parent_digest != null && !HASH_PATTERN.test(entry.parent_digest)) {
|
|
527
|
-
throw new TaskLedgerError(
|
|
528
|
-
'TASK_LEDGER_INVALID',
|
|
529
|
-
`baseline ledger entry ${entry.id} has an invalid parent_digest`,
|
|
530
|
-
);
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
function validateChange(entry) {
|
|
535
|
-
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
536
|
-
throw new TaskLedgerError('TASK_LEDGER_INVALID', 'every Change ledger entry must be an object');
|
|
537
|
-
}
|
|
538
|
-
for (const field of [
|
|
539
|
-
'id', 'title', 'kind', 'status', 'intent', 'artifact_root', 'revision', 'digest',
|
|
540
|
-
]) {
|
|
541
|
-
if (entry[field] === undefined || entry[field] === null || entry[field] === '') {
|
|
542
|
-
throw new TaskLedgerError(
|
|
543
|
-
'TASK_LEDGER_INVALID',
|
|
544
|
-
`Change ledger entry ${entry.id || '(unknown)'} is missing ${field}`,
|
|
545
|
-
);
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
const value = Object.fromEntries(DURABLE_CHANGE_FIELDS
|
|
549
|
-
.filter((field) => entry[field] !== undefined)
|
|
550
|
-
.map((field) => [field, entry[field]]));
|
|
551
|
-
if (!HASH_PATTERN.test(entry.digest) || entry.digest !== sha256(value)) {
|
|
552
|
-
throw new TaskLedgerError(
|
|
553
|
-
'TASK_LEDGER_DIGEST_MISMATCH',
|
|
554
|
-
`Change ledger entry ${entry.id} does not match its digest`,
|
|
555
|
-
);
|
|
556
|
-
}
|
|
557
|
-
if (entry.parent_digest != null && !HASH_PATTERN.test(entry.parent_digest)) {
|
|
558
|
-
throw new TaskLedgerError(
|
|
559
|
-
'TASK_LEDGER_INVALID',
|
|
560
|
-
`Change ledger entry ${entry.id} has an invalid parent_digest`,
|
|
561
|
-
);
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
function validateDecision(entry) {
|
|
566
|
-
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
567
|
-
throw new TaskLedgerError('TASK_LEDGER_INVALID', 'every decision entry must be an object');
|
|
568
|
-
}
|
|
569
|
-
for (const field of [
|
|
570
|
-
'id', 'scope_type', 'scope_id', 'selection', 'status',
|
|
571
|
-
'artifact_path', 'artifact_digest', 'digest',
|
|
572
|
-
]) {
|
|
573
|
-
if (entry[field] === undefined || entry[field] === null || entry[field] === '') {
|
|
574
|
-
throw new TaskLedgerError(
|
|
575
|
-
'TASK_LEDGER_INVALID',
|
|
576
|
-
`decision entry ${entry.id || '(unknown)'} is missing ${field}`,
|
|
577
|
-
);
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
const value = Object.fromEntries(DURABLE_DECISION_FIELDS
|
|
581
|
-
.filter((field) => entry[field] !== undefined)
|
|
582
|
-
.map((field) => [field, entry[field]]));
|
|
583
|
-
if (!HASH_PATTERN.test(entry.artifact_digest)
|
|
584
|
-
|| !HASH_PATTERN.test(entry.digest)
|
|
585
|
-
|| entry.digest !== sha256(value)) {
|
|
586
|
-
throw new TaskLedgerError(
|
|
587
|
-
'TASK_LEDGER_DIGEST_MISMATCH',
|
|
588
|
-
`decision entry ${entry.id} does not match its digest`,
|
|
589
|
-
);
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
function validateCheckpoint(entry) {
|
|
594
|
-
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
595
|
-
throw new TaskLedgerError('TASK_LEDGER_INVALID', 'every checkpoint entry must be an object');
|
|
596
|
-
}
|
|
597
|
-
for (const field of [
|
|
598
|
-
'stage', 'scope_type', 'scope_id', 'revision', 'payload',
|
|
599
|
-
'evidence', 'diagnostics', 'status', 'digest',
|
|
600
|
-
]) {
|
|
601
|
-
if (entry[field] === undefined || entry[field] === null) {
|
|
602
|
-
throw new TaskLedgerError(
|
|
603
|
-
'TASK_LEDGER_INVALID',
|
|
604
|
-
`checkpoint entry ${entry.stage || '(unknown)'} is missing ${field}`,
|
|
605
|
-
);
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
const value = Object.fromEntries(DURABLE_CHECKPOINT_FIELDS
|
|
609
|
-
.filter((field) => entry[field] !== undefined)
|
|
610
|
-
.map((field) => [field, entry[field]]));
|
|
611
|
-
if (entry.status !== 'accepted'
|
|
612
|
-
|| !HASH_PATTERN.test(entry.digest)
|
|
613
|
-
|| entry.digest !== sha256(value)) {
|
|
614
|
-
throw new TaskLedgerError(
|
|
615
|
-
'TASK_LEDGER_DIGEST_MISMATCH',
|
|
616
|
-
`checkpoint ${entry.stage}:${entry.scope_type}:${entry.scope_id} is invalid`,
|
|
617
|
-
);
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
function validateLegacyLedger(document, file) {
|
|
622
|
-
if (!document || document.kind !== LEDGER_KIND
|
|
623
|
-
|| document.schema_version !== LEGACY_LEDGER_SCHEMA_VERSION
|
|
624
|
-
|| !Array.isArray(document.tasks)
|
|
625
|
-
|| !Array.isArray(document.changes)
|
|
626
|
-
|| !Array.isArray(document.ancestors)) {
|
|
627
|
-
return false;
|
|
628
|
-
}
|
|
629
|
-
validateLegacyBaseline(document.baseline);
|
|
630
|
-
document.tasks.forEach(validateLegacyTask);
|
|
631
|
-
document.changes.forEach(validateChange);
|
|
632
|
-
const expected = sha256({
|
|
633
|
-
baseline: document.baseline || null,
|
|
634
|
-
changes: document.changes,
|
|
635
|
-
tasks: document.tasks,
|
|
636
|
-
});
|
|
637
|
-
if (!HASH_PATTERN.test(document.state_digest || '')
|
|
638
|
-
|| document.state_digest !== expected) {
|
|
639
|
-
throw new TaskLedgerError(
|
|
640
|
-
'TASK_LEDGER_DIGEST_MISMATCH',
|
|
641
|
-
`${file} legacy state_digest does not match its durable payload`,
|
|
642
|
-
);
|
|
643
|
-
}
|
|
644
|
-
return true;
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
function validateLedger(document, file = '(memory)') {
|
|
648
|
-
if (document?.schema_version === LEGACY_LEDGER_SCHEMA_VERSION
|
|
649
|
-
&& validateLegacyLedger(document, file)) {
|
|
650
|
-
throw new TaskLedgerError(
|
|
651
|
-
'TASK_LEDGER_SCHEMA_MIGRATION_REQUIRED',
|
|
652
|
-
`${file} is a valid v${LEGACY_LEDGER_SCHEMA_VERSION} ledger and must migrate to v${LEDGER_SCHEMA_VERSION}`,
|
|
653
|
-
{
|
|
654
|
-
from: LEGACY_LEDGER_SCHEMA_VERSION,
|
|
655
|
-
to: LEDGER_SCHEMA_VERSION,
|
|
656
|
-
},
|
|
657
|
-
);
|
|
658
|
-
}
|
|
659
|
-
if (!document || typeof document !== 'object' || Array.isArray(document)
|
|
660
|
-
|| document.kind !== LEDGER_KIND
|
|
661
|
-
|| document.schema_version !== LEDGER_SCHEMA_VERSION
|
|
662
|
-
|| !Number.isInteger(document.generation)
|
|
663
|
-
|| document.generation < 0
|
|
664
|
-
|| !Array.isArray(document.ancestors)
|
|
665
|
-
|| !Array.isArray(document.tasks)
|
|
666
|
-
|| !Array.isArray(document.changes)
|
|
667
|
-
|| !Array.isArray(document.decisions)
|
|
668
|
-
|| !Array.isArray(document.checkpoints)) {
|
|
669
|
-
throw new TaskLedgerError(
|
|
670
|
-
'TASK_LEDGER_INVALID',
|
|
671
|
-
`${file} is not an Ultra team task ledger v${LEDGER_SCHEMA_VERSION}`,
|
|
672
|
-
);
|
|
673
|
-
}
|
|
674
|
-
if (!validateTaskLedgerSchema(document)) {
|
|
675
|
-
const first = validateTaskLedgerSchema.errors?.[0];
|
|
676
|
-
const location = first?.instancePath || '(root)';
|
|
677
|
-
throw new TaskLedgerError(
|
|
678
|
-
'TASK_LEDGER_INVALID',
|
|
679
|
-
`${file} violates the exact v${LEDGER_SCHEMA_VERSION} schema at ${location}: `
|
|
680
|
-
+ `${first?.message || 'schema validation failed'}`,
|
|
681
|
-
{ errors: validateTaskLedgerSchema.errors || [] },
|
|
682
|
-
);
|
|
683
|
-
}
|
|
684
|
-
if (document.ancestors.length > MAX_ANCESTORS
|
|
685
|
-
|| document.ancestors.some((digest) => !HASH_PATTERN.test(digest))) {
|
|
686
|
-
throw new TaskLedgerError(
|
|
687
|
-
'TASK_LEDGER_INVALID',
|
|
688
|
-
`${file} contains an invalid checkpoint ancestry chain`,
|
|
689
|
-
);
|
|
690
|
-
}
|
|
691
|
-
validateBaseline(document.baseline);
|
|
692
|
-
const seen = new Set();
|
|
693
|
-
for (const task of document.tasks) {
|
|
694
|
-
validateTask(task);
|
|
695
|
-
if (seen.has(task.id)) {
|
|
696
|
-
throw new TaskLedgerError('TASK_LEDGER_INVALID', `${file} contains duplicate task ${task.id}`);
|
|
697
|
-
}
|
|
698
|
-
seen.add(task.id);
|
|
699
|
-
}
|
|
700
|
-
const seenChanges = new Set();
|
|
701
|
-
for (const change of document.changes) {
|
|
702
|
-
validateChange(change);
|
|
703
|
-
if (seenChanges.has(change.id)) {
|
|
704
|
-
throw new TaskLedgerError(
|
|
705
|
-
'TASK_LEDGER_INVALID',
|
|
706
|
-
`${file} contains duplicate Change ${change.id}`,
|
|
707
|
-
);
|
|
708
|
-
}
|
|
709
|
-
seenChanges.add(change.id);
|
|
710
|
-
}
|
|
711
|
-
const seenDecisions = new Set();
|
|
712
|
-
for (const decision of document.decisions) {
|
|
713
|
-
validateDecision(decision);
|
|
714
|
-
if (seenDecisions.has(decision.id)) {
|
|
715
|
-
throw new TaskLedgerError(
|
|
716
|
-
'TASK_LEDGER_INVALID',
|
|
717
|
-
`${file} contains duplicate decision ${decision.id}`,
|
|
718
|
-
);
|
|
719
|
-
}
|
|
720
|
-
seenDecisions.add(decision.id);
|
|
721
|
-
}
|
|
722
|
-
const seenCheckpoints = new Set();
|
|
723
|
-
for (const checkpoint of document.checkpoints) {
|
|
724
|
-
validateCheckpoint(checkpoint);
|
|
725
|
-
const key = `${checkpoint.stage}:${checkpoint.scope_type}:${checkpoint.scope_id}`;
|
|
726
|
-
if (seenCheckpoints.has(key)) {
|
|
727
|
-
throw new TaskLedgerError(
|
|
728
|
-
'TASK_LEDGER_INVALID',
|
|
729
|
-
`${file} contains multiple accepted checkpoints for ${key}`,
|
|
730
|
-
);
|
|
731
|
-
}
|
|
732
|
-
seenCheckpoints.add(key);
|
|
733
|
-
}
|
|
734
|
-
const expected = sha256(statePayload(document));
|
|
735
|
-
if (!HASH_PATTERN.test(document.state_digest || '') || document.state_digest !== expected) {
|
|
736
|
-
throw new TaskLedgerError(
|
|
737
|
-
'TASK_LEDGER_DIGEST_MISMATCH',
|
|
738
|
-
`${file} state_digest does not match its durable payload`,
|
|
739
|
-
);
|
|
740
|
-
}
|
|
741
|
-
if (document.parent_digest != null && !HASH_PATTERN.test(document.parent_digest)) {
|
|
742
|
-
throw new TaskLedgerError('TASK_LEDGER_INVALID', `${file} has an invalid parent_digest`);
|
|
743
|
-
}
|
|
744
|
-
return document;
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
function readTaskLedger(rootDir, { optional = true } = {}) {
|
|
748
|
-
const file = ledgerPath(rootDir);
|
|
749
|
-
const directory = safeDirectoryChain(rootDir, ['.ultra', 'tasks']);
|
|
750
|
-
if (!directory) {
|
|
751
|
-
if (optional) return null;
|
|
752
|
-
throw new TaskLedgerError('TASK_LEDGER_INVALID', `task ledger is missing: ${file}`);
|
|
753
|
-
}
|
|
754
|
-
let stat;
|
|
755
|
-
try { stat = fs.lstatSync(file); }
|
|
756
|
-
catch (error) {
|
|
757
|
-
if (error.code === 'ENOENT' && optional) return null;
|
|
758
|
-
throw error;
|
|
759
|
-
}
|
|
760
|
-
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
761
|
-
throw new TaskLedgerError('TASK_LEDGER_UNSAFE', `task ledger must be a regular file: ${file}`);
|
|
762
|
-
}
|
|
763
|
-
let parsed;
|
|
764
|
-
try { parsed = JSON.parse(fs.readFileSync(file, 'utf8')); }
|
|
765
|
-
catch (error) {
|
|
766
|
-
throw new TaskLedgerError('TASK_LEDGER_INVALID', `cannot read ${file}: ${error.message}`);
|
|
767
|
-
}
|
|
768
|
-
return validateLedger(parsed, file);
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
function writeLedgerAtomic(rootDir, document) {
|
|
772
|
-
writeManagedFile(
|
|
773
|
-
rootDir,
|
|
774
|
-
LEDGER_RELATIVE_PATH,
|
|
775
|
-
`${JSON.stringify(document, null, 2)}\n`,
|
|
776
|
-
);
|
|
777
|
-
return ledgerPath(rootDir);
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
function migrateLegacyProjection(db, rootDir, originalError) {
|
|
781
|
-
const file = ledgerPath(rootDir);
|
|
782
|
-
let bytes;
|
|
783
|
-
let document;
|
|
784
|
-
try {
|
|
785
|
-
bytes = fs.readFileSync(file);
|
|
786
|
-
document = JSON.parse(bytes.toString('utf8'));
|
|
787
|
-
} catch {
|
|
788
|
-
throw originalError;
|
|
789
|
-
}
|
|
790
|
-
if (document?.kind === LEDGER_KIND
|
|
791
|
-
|| !document || !Array.isArray(document.tasks)
|
|
792
|
-
|| !['4.4', '4.5'].includes(String(document.version || document.schema_version))) {
|
|
793
|
-
throw originalError;
|
|
794
|
-
}
|
|
795
|
-
const legacyIds = document.tasks.map((task) => String(task?.id || '')).sort();
|
|
796
|
-
if (legacyIds.some((id) => !id) || new Set(legacyIds).size !== legacyIds.length) {
|
|
797
|
-
throw new TaskLedgerError(
|
|
798
|
-
'TASK_LEDGER_LEGACY_CONFLICT',
|
|
799
|
-
'legacy tasks projection has missing or duplicate task ids',
|
|
800
|
-
);
|
|
801
|
-
}
|
|
802
|
-
const currentIds = ops.listTasks(db, { status: 'any', limit: 1000 })
|
|
803
|
-
.map((task) => task.id)
|
|
804
|
-
.sort();
|
|
805
|
-
if (JSON.stringify(legacyIds) !== JSON.stringify(currentIds)) {
|
|
806
|
-
throw new TaskLedgerError(
|
|
807
|
-
'TASK_LEDGER_LEGACY_CONFLICT',
|
|
808
|
-
'legacy tasks projection does not match current SQLite task authority',
|
|
809
|
-
{ legacy_task_ids: legacyIds, current_task_ids: currentIds },
|
|
810
|
-
);
|
|
811
|
-
}
|
|
812
|
-
const currentById = new Map(
|
|
813
|
-
ops.listTasks(db, { status: 'any', limit: 1000 }).map((task) => [task.id, task]),
|
|
814
|
-
);
|
|
815
|
-
for (const legacyTask of document.tasks) {
|
|
816
|
-
const current = currentById.get(legacyTask.id);
|
|
817
|
-
for (const field of DURABLE_TASK_FIELDS) {
|
|
818
|
-
if (legacyTask[field] === undefined) continue;
|
|
819
|
-
if (!util.isDeepStrictEqual(legacyTask[field], current[field])) {
|
|
820
|
-
throw new TaskLedgerError(
|
|
821
|
-
'TASK_LEDGER_LEGACY_CONFLICT',
|
|
822
|
-
`legacy task ${legacyTask.id} field ${field} does not match SQLite authority`,
|
|
823
|
-
{
|
|
824
|
-
task_id: legacyTask.id,
|
|
825
|
-
field,
|
|
826
|
-
legacy_value: legacyTask[field],
|
|
827
|
-
current_value: current[field],
|
|
828
|
-
},
|
|
829
|
-
);
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
const digest = sha256(bytes);
|
|
834
|
-
const backupDir = safeDirectoryChain(
|
|
835
|
-
rootDir,
|
|
836
|
-
['.ultra', '.runtime', 'backups', 'task-ledger'],
|
|
837
|
-
{ create: true },
|
|
838
|
-
);
|
|
839
|
-
const backup = path.join(backupDir, `legacy-tasks-${digest.slice(0, 16)}.json`);
|
|
840
|
-
if (fs.existsSync(backup)) {
|
|
841
|
-
const stat = fs.lstatSync(backup);
|
|
842
|
-
if (stat.isSymbolicLink() || !stat.isFile()
|
|
843
|
-
|| !fs.readFileSync(backup).equals(bytes)) {
|
|
844
|
-
throw new TaskLedgerError(
|
|
845
|
-
'TASK_LEDGER_UNSAFE',
|
|
846
|
-
`legacy task ledger backup is unsafe or conflicting: ${backup}`,
|
|
847
|
-
);
|
|
848
|
-
}
|
|
849
|
-
} else {
|
|
850
|
-
fs.writeFileSync(backup, bytes, { flag: 'wx' });
|
|
851
|
-
}
|
|
852
|
-
return { previous: null, migrated: true, backup };
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
function migrateLegacyTeamLedger(rootDir, originalError) {
|
|
856
|
-
const file = ledgerPath(rootDir);
|
|
857
|
-
let bytes;
|
|
858
|
-
let document;
|
|
859
|
-
try {
|
|
860
|
-
bytes = fs.readFileSync(file);
|
|
861
|
-
document = JSON.parse(bytes.toString('utf8'));
|
|
862
|
-
if (!validateLegacyLedger(document, file)) throw originalError;
|
|
863
|
-
} catch (error) {
|
|
864
|
-
if (error instanceof TaskLedgerError) throw error;
|
|
865
|
-
throw originalError;
|
|
866
|
-
}
|
|
867
|
-
const digest = sha256(bytes);
|
|
868
|
-
const backupDir = safeDirectoryChain(
|
|
869
|
-
rootDir,
|
|
870
|
-
['.ultra', '.runtime', 'backups', 'task-ledger'],
|
|
871
|
-
{ create: true },
|
|
872
|
-
);
|
|
873
|
-
const backup = path.join(backupDir, `team-ledger-v1-${digest.slice(0, 16)}.json`);
|
|
874
|
-
if (fs.existsSync(backup)) {
|
|
875
|
-
const stat = fs.lstatSync(backup);
|
|
876
|
-
if (stat.isSymbolicLink() || !stat.isFile()
|
|
877
|
-
|| !fs.readFileSync(backup).equals(bytes)) {
|
|
878
|
-
throw new TaskLedgerError(
|
|
879
|
-
'TASK_LEDGER_UNSAFE',
|
|
880
|
-
`legacy team ledger backup is unsafe or conflicting: ${backup}`,
|
|
881
|
-
);
|
|
882
|
-
}
|
|
883
|
-
} else {
|
|
884
|
-
fs.writeFileSync(backup, bytes, { flag: 'wx' });
|
|
885
|
-
}
|
|
886
|
-
return {
|
|
887
|
-
previous: {
|
|
888
|
-
...document,
|
|
889
|
-
decisions: [],
|
|
890
|
-
checkpoints: [],
|
|
891
|
-
},
|
|
892
|
-
migrated: true,
|
|
893
|
-
backup,
|
|
894
|
-
migration_kind: 'team-ledger-v1',
|
|
895
|
-
};
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
function priorLedgerForPublish(db, rootDir) {
|
|
899
|
-
try {
|
|
900
|
-
return { previous: readTaskLedger(rootDir), migrated: false, backup: null };
|
|
901
|
-
} catch (error) {
|
|
902
|
-
if (error.code === 'TASK_LEDGER_SCHEMA_MIGRATION_REQUIRED') {
|
|
903
|
-
return migrateLegacyTeamLedger(rootDir, error);
|
|
904
|
-
}
|
|
905
|
-
if (!['TASK_LEDGER_INVALID', 'TASK_LEDGER_DIGEST_MISMATCH'].includes(error.code)) {
|
|
906
|
-
throw error;
|
|
907
|
-
}
|
|
908
|
-
return migrateLegacyProjection(db, rootDir, error);
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
function syncTaskLedger(db, { rootDir = '.' } = {}) {
|
|
913
|
-
let document;
|
|
914
|
-
try {
|
|
915
|
-
document = readTaskLedger(rootDir);
|
|
916
|
-
} catch (error) {
|
|
917
|
-
if (error.code === 'TASK_LEDGER_SCHEMA_MIGRATION_REQUIRED') {
|
|
918
|
-
return { status: 'legacy', imported: 0, already_current: false };
|
|
919
|
-
}
|
|
920
|
-
if (!['TASK_LEDGER_INVALID', 'TASK_LEDGER_DIGEST_MISMATCH'].includes(error.code)) {
|
|
921
|
-
throw error;
|
|
922
|
-
}
|
|
923
|
-
try {
|
|
924
|
-
const raw = JSON.parse(fs.readFileSync(ledgerPath(rootDir), 'utf8'));
|
|
925
|
-
if (raw && Array.isArray(raw.tasks)
|
|
926
|
-
&& ['4.4', '4.5'].includes(String(raw.version || raw.schema_version))) {
|
|
927
|
-
return { status: 'legacy', imported: 0, already_current: false };
|
|
928
|
-
}
|
|
929
|
-
} catch {
|
|
930
|
-
// Preserve the typed ledger error from the validated read.
|
|
931
|
-
}
|
|
932
|
-
throw error;
|
|
933
|
-
}
|
|
934
|
-
if (!document) {
|
|
935
|
-
return { status: 'missing', imported: 0, already_current: false };
|
|
936
|
-
}
|
|
937
|
-
const checkpoint = latestImportedCheckpoint(db);
|
|
938
|
-
if (checkpoint.state_digest === document.state_digest) {
|
|
939
|
-
return {
|
|
940
|
-
status: 'current',
|
|
941
|
-
imported: 0,
|
|
942
|
-
preserved: document.tasks.length,
|
|
943
|
-
deleted: 0,
|
|
944
|
-
imported_changes: 0,
|
|
945
|
-
preserved_changes: document.changes.length,
|
|
946
|
-
imported_decisions: 0,
|
|
947
|
-
preserved_decisions: document.decisions.length,
|
|
948
|
-
imported_checkpoints: 0,
|
|
949
|
-
preserved_checkpoints: document.checkpoints.length,
|
|
950
|
-
imported_baseline: false,
|
|
951
|
-
preserved_baseline: Boolean(document.baseline),
|
|
952
|
-
generation: document.generation,
|
|
953
|
-
state_digest: document.state_digest,
|
|
954
|
-
requires_plan_revalidation: false,
|
|
955
|
-
requires_baseline_revalidation: false,
|
|
956
|
-
already_current: true,
|
|
957
|
-
};
|
|
958
|
-
}
|
|
959
|
-
return {
|
|
960
|
-
status: 'imported',
|
|
961
|
-
...importTaskLedger(db, { rootDir }),
|
|
962
|
-
};
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
function taskDigestMap(tasks) {
|
|
966
|
-
return Object.fromEntries(tasks.map((task) => [task.id, task.digest]));
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
function checkpointDigestMap(checkpoints) {
|
|
970
|
-
return Object.fromEntries(checkpoints.map((checkpoint) => [
|
|
971
|
-
`${checkpoint.stage}:${checkpoint.scope_type}:${checkpoint.scope_id}`,
|
|
972
|
-
checkpoint.digest,
|
|
973
|
-
]));
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
function recordLedgerEvent(db, type, ledger, extra = {}) {
|
|
977
|
-
ops.appendEvent(db, {
|
|
978
|
-
type,
|
|
979
|
-
payload: {
|
|
980
|
-
generation: ledger.generation,
|
|
981
|
-
state_digest: ledger.state_digest,
|
|
982
|
-
baseline_digest: ledger.baseline?.digest || null,
|
|
983
|
-
baseline_checkpoint: ledger.baseline || null,
|
|
984
|
-
task_digests: taskDigestMap(ledger.tasks),
|
|
985
|
-
change_digests: taskDigestMap(ledger.changes),
|
|
986
|
-
decision_digests: taskDigestMap(ledger.decisions || []),
|
|
987
|
-
checkpoint_digests: checkpointDigestMap(ledger.checkpoints || []),
|
|
988
|
-
...extra,
|
|
989
|
-
},
|
|
990
|
-
});
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
function publishTaskLedger(db, {
|
|
994
|
-
rootDir = '.',
|
|
995
|
-
reason = 'manual_checkpoint',
|
|
996
|
-
} = {}) {
|
|
997
|
-
let prior = priorLedgerForPublish(db, rootDir);
|
|
998
|
-
if (prior.previous && !prior.migrated) {
|
|
999
|
-
syncTaskLedger(db, { rootDir });
|
|
1000
|
-
prior = {
|
|
1001
|
-
previous: readTaskLedger(rootDir),
|
|
1002
|
-
migrated: false,
|
|
1003
|
-
backup: null,
|
|
1004
|
-
};
|
|
1005
|
-
}
|
|
1006
|
-
const previous = prior.previous;
|
|
1007
|
-
const priorTasks = new Map((previous?.tasks || []).map((task) => [task.id, task]));
|
|
1008
|
-
const priorChanges = new Map((previous?.changes || []).map((change) => [change.id, change]));
|
|
1009
|
-
const rows = ops.listTasks(db, { status: 'any', limit: 1000 })
|
|
1010
|
-
.sort((left, right) => left.id.localeCompare(right.id));
|
|
1011
|
-
const tasks = rows.map((task) => durableTask(task, priorTasks.get(task.id)));
|
|
1012
|
-
const baseline = readBaselineForLedger(db, previous?.baseline || null);
|
|
1013
|
-
if (baseline?.gaps?.some((gap) => (
|
|
1014
|
-
gap.id === 'team-ledger-revalidation-required' && gap.status === 'open'
|
|
1015
|
-
))) {
|
|
1016
|
-
throw new TaskLedgerError(
|
|
1017
|
-
'TASK_LEDGER_REVALIDATION_REQUIRED',
|
|
1018
|
-
'the imported team baseline must converge in this checkout before publication',
|
|
1019
|
-
{ baseline_id: baseline.id, ledger_path: LEDGER_RELATIVE_PATH },
|
|
1020
|
-
);
|
|
1021
|
-
}
|
|
1022
|
-
const changes = readChangesForLedger(db, rows, priorChanges);
|
|
1023
|
-
const decisions = readDecisionsForLedger(db);
|
|
1024
|
-
const checkpoints = readCheckpointsForLedger(db);
|
|
1025
|
-
const digest = sha256(statePayload({
|
|
1026
|
-
baseline,
|
|
1027
|
-
changes,
|
|
1028
|
-
decisions,
|
|
1029
|
-
checkpoints,
|
|
1030
|
-
tasks,
|
|
1031
|
-
}));
|
|
1032
|
-
if (previous?.state_digest === digest) {
|
|
1033
|
-
return {
|
|
1034
|
-
changed: false,
|
|
1035
|
-
path: ledgerPath(rootDir),
|
|
1036
|
-
ledger: previous,
|
|
1037
|
-
};
|
|
1038
|
-
}
|
|
1039
|
-
const document = {
|
|
1040
|
-
kind: LEDGER_KIND,
|
|
1041
|
-
schema_version: LEDGER_SCHEMA_VERSION,
|
|
1042
|
-
generation: Number(previous?.generation || 0) + 1,
|
|
1043
|
-
parent_digest: previous?.state_digest || null,
|
|
1044
|
-
ancestors: previous
|
|
1045
|
-
? [previous.state_digest, ...(previous.ancestors || [])].slice(0, MAX_ANCESTORS)
|
|
1046
|
-
: [],
|
|
1047
|
-
state_digest: digest,
|
|
1048
|
-
published_at: new Date().toISOString(),
|
|
1049
|
-
reason,
|
|
1050
|
-
baseline,
|
|
1051
|
-
changes,
|
|
1052
|
-
decisions,
|
|
1053
|
-
checkpoints,
|
|
1054
|
-
tasks,
|
|
1055
|
-
};
|
|
1056
|
-
validateLedger(document);
|
|
1057
|
-
const snapshot = snapshotManagedFile(rootDir, LEDGER_RELATIVE_PATH);
|
|
1058
|
-
let file;
|
|
1059
|
-
try {
|
|
1060
|
-
file = writeLedgerAtomic(rootDir, document);
|
|
1061
|
-
recordLedgerEvent(db, 'task_ledger_published', document, {
|
|
1062
|
-
reason,
|
|
1063
|
-
migrated_legacy_projection: prior.migrated,
|
|
1064
|
-
legacy_backup_path: prior.backup,
|
|
1065
|
-
});
|
|
1066
|
-
} catch (error) {
|
|
1067
|
-
try {
|
|
1068
|
-
restoreManagedFile(rootDir, snapshot);
|
|
1069
|
-
} catch (rollbackError) {
|
|
1070
|
-
throw new TaskLedgerError(
|
|
1071
|
-
'STATE_CORRUPT',
|
|
1072
|
-
'task ledger publication failed and the previous ledger bytes could not be restored',
|
|
1073
|
-
{
|
|
1074
|
-
cause: error.code || error.message,
|
|
1075
|
-
rollback: rollbackError.code || rollbackError.message,
|
|
1076
|
-
path: LEDGER_RELATIVE_PATH,
|
|
1077
|
-
},
|
|
1078
|
-
);
|
|
1079
|
-
}
|
|
1080
|
-
throw error;
|
|
1081
|
-
}
|
|
1082
|
-
const result = {
|
|
1083
|
-
changed: true,
|
|
1084
|
-
path: file,
|
|
1085
|
-
ledger: document,
|
|
1086
|
-
migrated_legacy_projection: prior.migrated,
|
|
1087
|
-
legacy_backup_path: prior.backup,
|
|
1088
|
-
};
|
|
1089
|
-
Object.defineProperty(result, PUBLICATION_ROLLBACK, {
|
|
1090
|
-
configurable: false,
|
|
1091
|
-
enumerable: false,
|
|
1092
|
-
writable: false,
|
|
1093
|
-
value: { rootDir: path.resolve(rootDir), snapshot },
|
|
1094
|
-
});
|
|
1095
|
-
return result;
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
function rollbackTaskLedgerPublication(publication) {
|
|
1099
|
-
const rollback = publication?.[PUBLICATION_ROLLBACK];
|
|
1100
|
-
if (!rollback) return { restored: false, reason: 'NO_PUBLICATION_ROLLBACK' };
|
|
1101
|
-
restoreManagedFile(rollback.rootDir, rollback.snapshot);
|
|
1102
|
-
return {
|
|
1103
|
-
restored: true,
|
|
1104
|
-
path: rollback.snapshot.path,
|
|
1105
|
-
previous_existed: rollback.snapshot.existed,
|
|
1106
|
-
};
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
function latestImportedCheckpoint(db) {
|
|
1110
|
-
const row = db.prepare(
|
|
1111
|
-
`SELECT payload_json FROM events
|
|
1112
|
-
WHERE type IN ('task_ledger_imported', 'task_ledger_published')
|
|
1113
|
-
ORDER BY id DESC LIMIT 1`,
|
|
1114
|
-
).get();
|
|
1115
|
-
const payload = parseJson(row?.payload_json, {});
|
|
1116
|
-
return {
|
|
1117
|
-
generation: Number.isInteger(payload.generation) ? payload.generation : null,
|
|
1118
|
-
state_digest: HASH_PATTERN.test(payload.state_digest || '') ? payload.state_digest : null,
|
|
1119
|
-
task_digests: payload.task_digests && typeof payload.task_digests === 'object'
|
|
1120
|
-
? payload.task_digests
|
|
1121
|
-
: {},
|
|
1122
|
-
change_digests: payload.change_digests && typeof payload.change_digests === 'object'
|
|
1123
|
-
? payload.change_digests
|
|
1124
|
-
: {},
|
|
1125
|
-
decision_digests: payload.decision_digests && typeof payload.decision_digests === 'object'
|
|
1126
|
-
? payload.decision_digests
|
|
1127
|
-
: {},
|
|
1128
|
-
checkpoint_digests: payload.checkpoint_digests
|
|
1129
|
-
&& typeof payload.checkpoint_digests === 'object'
|
|
1130
|
-
? payload.checkpoint_digests
|
|
1131
|
-
: {},
|
|
1132
|
-
baseline_digest: HASH_PATTERN.test(payload.baseline_digest || '')
|
|
1133
|
-
? payload.baseline_digest
|
|
1134
|
-
: null,
|
|
1135
|
-
baseline_checkpoint: payload.baseline_checkpoint
|
|
1136
|
-
&& typeof payload.baseline_checkpoint === 'object'
|
|
1137
|
-
? payload.baseline_checkpoint
|
|
1138
|
-
: null,
|
|
1139
|
-
};
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
function baselineForLocalCheckout(baseline) {
|
|
1143
|
-
const value = portableBaseline(baseline);
|
|
1144
|
-
const requiresRevalidation = baseline.status === 'ready';
|
|
1145
|
-
if (!requiresRevalidation) return { value, requiresRevalidation: false };
|
|
1146
|
-
const gaps = [...(value.gaps || [])];
|
|
1147
|
-
if (requiresRevalidation && !gaps.some((gap) => gap.id === 'team-ledger-revalidation-required')) {
|
|
1148
|
-
gaps.push({
|
|
1149
|
-
id: 'team-ledger-revalidation-required',
|
|
1150
|
-
category: 'baseline_blocker',
|
|
1151
|
-
status: 'open',
|
|
1152
|
-
blocking: true,
|
|
1153
|
-
summary: 'The imported team checkpoint must be revalidated against this checkout.',
|
|
1154
|
-
evidence_refs: [LEDGER_RELATIVE_PATH],
|
|
1155
|
-
owner: 'project-owner',
|
|
1156
|
-
});
|
|
1157
|
-
}
|
|
1158
|
-
return {
|
|
1159
|
-
value: compactObject(Object.entries({
|
|
1160
|
-
...value,
|
|
1161
|
-
status: value.mode === 'greenfield' ? 'draft' : 'adopting',
|
|
1162
|
-
worktree_accepted: false,
|
|
1163
|
-
known_red_accepted: false,
|
|
1164
|
-
gaps,
|
|
1165
|
-
research_checkpoint_id: undefined,
|
|
1166
|
-
approved_by: undefined,
|
|
1167
|
-
approval_note: undefined,
|
|
1168
|
-
converged_at: undefined,
|
|
1169
|
-
})),
|
|
1170
|
-
requiresRevalidation: true,
|
|
1171
|
-
};
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
function writeBaseline(db, baseline, { update = false } = {}) {
|
|
1175
|
-
const local = baselineForLocalCheckout(baseline);
|
|
1176
|
-
const value = local.value;
|
|
1177
|
-
if (update) {
|
|
1178
|
-
db.prepare(
|
|
1179
|
-
`UPDATE baselines SET
|
|
1180
|
-
project_name = ?, project_type = ?, stack = ?, mode = ?, status = ?,
|
|
1181
|
-
scope_json = ?, repository_revision = ?, repository_branch = ?,
|
|
1182
|
-
worktree_state = ?, worktree_digest = ?, worktree_accepted = ?,
|
|
1183
|
-
known_red_accepted = ?, spec_refs_json = ?, evidence_json = ?,
|
|
1184
|
-
verification_json = ?, unknowns_json = ?, gaps_json = ?,
|
|
1185
|
-
classification_json = ?, provider_refs_json = ?,
|
|
1186
|
-
research_checkpoint_id = ?, research_run_id = NULL,
|
|
1187
|
-
approved_by = ?, approval_note = ?, converged_at = ?, updated_at = ?
|
|
1188
|
-
WHERE id = ?`,
|
|
1189
|
-
).run(
|
|
1190
|
-
value.project_name,
|
|
1191
|
-
value.project_type || null,
|
|
1192
|
-
value.stack || null,
|
|
1193
|
-
value.mode,
|
|
1194
|
-
value.status,
|
|
1195
|
-
JSON.stringify(value.scope || ['.']),
|
|
1196
|
-
value.repository_revision || null,
|
|
1197
|
-
value.repository_branch || null,
|
|
1198
|
-
value.worktree_state || 'unavailable',
|
|
1199
|
-
value.worktree_digest || null,
|
|
1200
|
-
value.worktree_accepted ? 1 : 0,
|
|
1201
|
-
value.known_red_accepted ? 1 : 0,
|
|
1202
|
-
JSON.stringify(value.spec_refs || []),
|
|
1203
|
-
JSON.stringify(value.evidence || []),
|
|
1204
|
-
JSON.stringify(value.verification || []),
|
|
1205
|
-
JSON.stringify(value.unknowns || []),
|
|
1206
|
-
JSON.stringify(value.gaps || []),
|
|
1207
|
-
JSON.stringify(value.classification || {}),
|
|
1208
|
-
JSON.stringify(value.provider_refs || {}),
|
|
1209
|
-
value.research_checkpoint_id || null,
|
|
1210
|
-
value.approved_by || null,
|
|
1211
|
-
value.approval_note || null,
|
|
1212
|
-
value.converged_at || null,
|
|
1213
|
-
new Date().toISOString(),
|
|
1214
|
-
value.id,
|
|
1215
|
-
);
|
|
1216
|
-
return local;
|
|
1217
|
-
}
|
|
1218
|
-
db.prepare(
|
|
1219
|
-
`INSERT INTO baselines
|
|
1220
|
-
(id, project_name, project_type, stack, mode, status, repository_root, scope_json,
|
|
1221
|
-
repository_revision, repository_branch, worktree_state, worktree_digest,
|
|
1222
|
-
worktree_accepted, known_red_accepted, spec_refs_json, evidence_json,
|
|
1223
|
-
verification_json, unknowns_json, gaps_json, classification_json,
|
|
1224
|
-
provider_refs_json, research_checkpoint_id, research_run_id,
|
|
1225
|
-
approved_by, approval_note, converged_at)
|
|
1226
|
-
VALUES (?, ?, ?, ?, ?, ?, '.', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?)`,
|
|
1227
|
-
).run(
|
|
1228
|
-
value.id,
|
|
1229
|
-
value.project_name,
|
|
1230
|
-
value.project_type || null,
|
|
1231
|
-
value.stack || null,
|
|
1232
|
-
value.mode,
|
|
1233
|
-
value.status,
|
|
1234
|
-
JSON.stringify(value.scope || ['.']),
|
|
1235
|
-
value.repository_revision || null,
|
|
1236
|
-
value.repository_branch || null,
|
|
1237
|
-
value.worktree_state || 'unavailable',
|
|
1238
|
-
value.worktree_digest || null,
|
|
1239
|
-
value.worktree_accepted ? 1 : 0,
|
|
1240
|
-
value.known_red_accepted ? 1 : 0,
|
|
1241
|
-
JSON.stringify(value.spec_refs || []),
|
|
1242
|
-
JSON.stringify(value.evidence || []),
|
|
1243
|
-
JSON.stringify(value.verification || []),
|
|
1244
|
-
JSON.stringify(value.unknowns || []),
|
|
1245
|
-
JSON.stringify(value.gaps || []),
|
|
1246
|
-
JSON.stringify(value.classification || {}),
|
|
1247
|
-
JSON.stringify(value.provider_refs || {}),
|
|
1248
|
-
value.research_checkpoint_id || null,
|
|
1249
|
-
value.approved_by || null,
|
|
1250
|
-
value.approval_note || null,
|
|
1251
|
-
value.converged_at || null,
|
|
1252
|
-
);
|
|
1253
|
-
return local;
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
function localBaselineMatchesRevalidation(row, checkpointBaseline) {
|
|
1257
|
-
if (!row || !checkpointBaseline || row.id !== checkpointBaseline.id
|
|
1258
|
-
|| checkpointBaseline.status !== 'ready') return false;
|
|
1259
|
-
const expected = baselineForLocalCheckout(checkpointBaseline).value;
|
|
1260
|
-
return util.isDeepStrictEqual(portableBaseline(row), expected);
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
function mergeBaseline(db, incoming, checkpoint) {
|
|
1264
|
-
if (!incoming) {
|
|
1265
|
-
return { imported: false, preserved: false, requiresRevalidation: false };
|
|
1266
|
-
}
|
|
1267
|
-
const active = db.prepare(
|
|
1268
|
-
`SELECT * FROM baselines
|
|
1269
|
-
WHERE status <> 'superseded'
|
|
1270
|
-
ORDER BY CASE status WHEN 'ready' THEN 0 ELSE 1 END, updated_at DESC, rowid DESC
|
|
1271
|
-
LIMIT 1`,
|
|
1272
|
-
).get();
|
|
1273
|
-
if (!active) {
|
|
1274
|
-
const written = writeBaseline(db, incoming);
|
|
1275
|
-
return {
|
|
1276
|
-
imported: true,
|
|
1277
|
-
preserved: false,
|
|
1278
|
-
requiresRevalidation: written.requiresRevalidation,
|
|
1279
|
-
};
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
const current = durableBaseline(active, null);
|
|
1283
|
-
const lastDigest = checkpoint.baseline_digest;
|
|
1284
|
-
const effectiveCurrentDigest = localBaselineMatchesRevalidation(
|
|
1285
|
-
active,
|
|
1286
|
-
checkpoint.baseline_checkpoint,
|
|
1287
|
-
) ? lastDigest : current.digest;
|
|
1288
|
-
if (active.id === incoming.id && current.digest === incoming.digest) {
|
|
1289
|
-
return { imported: false, preserved: true, requiresRevalidation: false };
|
|
1290
|
-
}
|
|
1291
|
-
if (!lastDigest) {
|
|
1292
|
-
throw new TaskLedgerError(
|
|
1293
|
-
'TASK_LEDGER_BASELINE_CONFLICT',
|
|
1294
|
-
`baseline ${active.id} exists locally without a common team checkpoint`,
|
|
1295
|
-
{
|
|
1296
|
-
local_baseline_id: active.id,
|
|
1297
|
-
incoming_baseline_id: incoming.id,
|
|
1298
|
-
local_digest: current.digest,
|
|
1299
|
-
incoming_digest: incoming.digest,
|
|
1300
|
-
},
|
|
1301
|
-
);
|
|
1302
|
-
}
|
|
1303
|
-
if (effectiveCurrentDigest !== lastDigest && incoming.digest === lastDigest) {
|
|
1304
|
-
return { imported: false, preserved: true, requiresRevalidation: false };
|
|
1305
|
-
}
|
|
1306
|
-
if (effectiveCurrentDigest !== lastDigest && incoming.digest !== lastDigest) {
|
|
1307
|
-
throw new TaskLedgerError(
|
|
1308
|
-
'TASK_LEDGER_BASELINE_CONFLICT',
|
|
1309
|
-
'baseline changed both locally and in the Git ledger',
|
|
1310
|
-
{
|
|
1311
|
-
local_baseline_id: active.id,
|
|
1312
|
-
incoming_baseline_id: incoming.id,
|
|
1313
|
-
last_digest: lastDigest,
|
|
1314
|
-
local_digest: current.digest,
|
|
1315
|
-
incoming_digest: incoming.digest,
|
|
1316
|
-
},
|
|
1317
|
-
);
|
|
1318
|
-
}
|
|
1319
|
-
if (active.id !== incoming.id) {
|
|
1320
|
-
db.prepare(
|
|
1321
|
-
"UPDATE baselines SET status = 'superseded', updated_at = ? WHERE id = ?",
|
|
1322
|
-
).run(new Date().toISOString(), active.id);
|
|
1323
|
-
const written = writeBaseline(db, incoming);
|
|
1324
|
-
return {
|
|
1325
|
-
imported: true,
|
|
1326
|
-
preserved: false,
|
|
1327
|
-
requiresRevalidation: written.requiresRevalidation,
|
|
1328
|
-
};
|
|
1329
|
-
}
|
|
1330
|
-
const written = writeBaseline(db, incoming, { update: true });
|
|
1331
|
-
return {
|
|
1332
|
-
imported: true,
|
|
1333
|
-
preserved: false,
|
|
1334
|
-
requiresRevalidation: written.requiresRevalidation,
|
|
1335
|
-
};
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
function insertChange(db, change) {
|
|
1339
|
-
db.prepare(
|
|
1340
|
-
`INSERT INTO changes
|
|
1341
|
-
(id, title, kind, status, intent, docs_impact_json, provider_refs_json,
|
|
1342
|
-
baseline_bypass_json, contract_json, classification_json,
|
|
1343
|
-
research_disposition_json, base_commit, supersedes_id, artifact_root, closed_at)
|
|
1344
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1345
|
-
).run(
|
|
1346
|
-
change.id,
|
|
1347
|
-
change.title,
|
|
1348
|
-
change.kind,
|
|
1349
|
-
change.status,
|
|
1350
|
-
change.intent,
|
|
1351
|
-
JSON.stringify(change.docs_impact || {}),
|
|
1352
|
-
JSON.stringify(change.provider_refs || {}),
|
|
1353
|
-
change.baseline_bypass ? JSON.stringify(change.baseline_bypass) : null,
|
|
1354
|
-
JSON.stringify(change.contract || {}),
|
|
1355
|
-
JSON.stringify(change.classification || {}),
|
|
1356
|
-
JSON.stringify(change.research_disposition || {}),
|
|
1357
|
-
change.base_commit || null,
|
|
1358
|
-
change.supersedes_id || null,
|
|
1359
|
-
change.artifact_root,
|
|
1360
|
-
change.closed_at || null,
|
|
1361
|
-
);
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
function updateChangeFromLedger(db, change) {
|
|
1365
|
-
db.prepare(
|
|
1366
|
-
`UPDATE changes SET
|
|
1367
|
-
title = ?, kind = ?, status = ?, intent = ?, docs_impact_json = ?,
|
|
1368
|
-
provider_refs_json = ?, baseline_bypass_json = ?, contract_json = ?,
|
|
1369
|
-
classification_json = ?, research_disposition_json = ?, base_commit = ?,
|
|
1370
|
-
supersedes_id = ?, artifact_root = ?, closed_at = ?, updated_at = ?
|
|
1371
|
-
WHERE id = ?`,
|
|
1372
|
-
).run(
|
|
1373
|
-
change.title,
|
|
1374
|
-
change.kind,
|
|
1375
|
-
change.status,
|
|
1376
|
-
change.intent,
|
|
1377
|
-
JSON.stringify(change.docs_impact || {}),
|
|
1378
|
-
JSON.stringify(change.provider_refs || {}),
|
|
1379
|
-
change.baseline_bypass ? JSON.stringify(change.baseline_bypass) : null,
|
|
1380
|
-
JSON.stringify(change.contract || {}),
|
|
1381
|
-
JSON.stringify(change.classification || {}),
|
|
1382
|
-
JSON.stringify(change.research_disposition || {}),
|
|
1383
|
-
change.base_commit || null,
|
|
1384
|
-
change.supersedes_id || null,
|
|
1385
|
-
change.artifact_root,
|
|
1386
|
-
change.closed_at || null,
|
|
1387
|
-
new Date().toISOString(),
|
|
1388
|
-
change.id,
|
|
1389
|
-
);
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
function insertTaskFromLedger(db, task) {
|
|
1393
|
-
const created = ops.createTask(db, {
|
|
1394
|
-
id: task.id,
|
|
1395
|
-
title: task.title,
|
|
1396
|
-
type: task.type,
|
|
1397
|
-
priority: task.priority,
|
|
1398
|
-
complexity: task.complexity,
|
|
1399
|
-
estimated_days: task.estimated_days,
|
|
1400
|
-
deps: task.deps || [],
|
|
1401
|
-
files_modified: task.files_modified || [],
|
|
1402
|
-
stale: Boolean(task.stale),
|
|
1403
|
-
trace_to: task.trace_to,
|
|
1404
|
-
outcome: task.outcome,
|
|
1405
|
-
slice_kind: task.slice_kind,
|
|
1406
|
-
public_seam: task.public_seam,
|
|
1407
|
-
verification_command: task.verification_command,
|
|
1408
|
-
acceptance: task.acceptance || [],
|
|
1409
|
-
context_refs: task.context_refs || [],
|
|
1410
|
-
docs_impact: task.docs_impact || { status: 'unknown', files: [], rationale: null },
|
|
1411
|
-
ownership: task.ownership || {},
|
|
1412
|
-
change_id: task.change_id || null,
|
|
1413
|
-
parent_id: null,
|
|
1414
|
-
});
|
|
1415
|
-
if (task.status !== created.status || Boolean(task.stale) !== created.stale) {
|
|
1416
|
-
updateTaskFromLedger(db, task);
|
|
1417
|
-
}
|
|
1418
|
-
return ops.readTask(db, task.id);
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
function updateTaskFromLedger(db, task) {
|
|
1422
|
-
db.prepare(
|
|
1423
|
-
`UPDATE tasks SET
|
|
1424
|
-
title = ?, type = ?, priority = ?, complexity = ?, estimated_days = ?,
|
|
1425
|
-
status = ?, deps = ?, files_modified = ?, session_id = NULL, stale = ?,
|
|
1426
|
-
trace_to = ?, outcome = ?, slice_kind = ?, public_seam = ?,
|
|
1427
|
-
verification_command = ?, acceptance_json = ?, context_refs_json = ?,
|
|
1428
|
-
docs_impact_json = ?, ownership_json = ?, change_id = ?, updated_at = ?
|
|
1429
|
-
WHERE id = ?`,
|
|
1430
|
-
).run(
|
|
1431
|
-
task.title,
|
|
1432
|
-
task.type,
|
|
1433
|
-
task.priority,
|
|
1434
|
-
task.complexity ?? null,
|
|
1435
|
-
task.estimated_days ?? null,
|
|
1436
|
-
task.status,
|
|
1437
|
-
JSON.stringify(task.deps || []),
|
|
1438
|
-
JSON.stringify(task.files_modified || []),
|
|
1439
|
-
task.stale ? 1 : 0,
|
|
1440
|
-
task.trace_to || null,
|
|
1441
|
-
task.outcome || null,
|
|
1442
|
-
task.slice_kind || null,
|
|
1443
|
-
task.public_seam || null,
|
|
1444
|
-
task.verification_command || null,
|
|
1445
|
-
JSON.stringify(task.acceptance || []),
|
|
1446
|
-
JSON.stringify(task.context_refs || []),
|
|
1447
|
-
JSON.stringify(task.docs_impact || { status: 'unknown', files: [], rationale: null }),
|
|
1448
|
-
JSON.stringify(task.ownership || {}),
|
|
1449
|
-
task.change_id || null,
|
|
1450
|
-
new Date().toISOString(),
|
|
1451
|
-
task.id,
|
|
1452
|
-
);
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
function decisionDocument(rootDir, entry) {
|
|
1456
|
-
const read = readStableProjectFile(rootDir, entry.artifact_path, { encoding: 'utf8' });
|
|
1457
|
-
let document;
|
|
1458
|
-
try { document = JSON.parse(read.text); }
|
|
1459
|
-
catch (error) {
|
|
1460
|
-
throw new TaskLedgerError(
|
|
1461
|
-
'TASK_LEDGER_DECISION_INVALID',
|
|
1462
|
-
`decision artifact ${entry.artifact_path} is invalid JSON: ${error.message}`,
|
|
1463
|
-
);
|
|
1464
|
-
}
|
|
1465
|
-
const { digest, ...value } = document;
|
|
1466
|
-
if (digest !== entry.artifact_digest || canonical.digest(value) !== digest) {
|
|
1467
|
-
throw new TaskLedgerError(
|
|
1468
|
-
'TASK_LEDGER_DECISION_CONFLICT',
|
|
1469
|
-
`decision artifact ${entry.artifact_path} does not match the team ledger`,
|
|
1470
|
-
{
|
|
1471
|
-
decision_id: entry.id,
|
|
1472
|
-
ledger_digest: entry.artifact_digest,
|
|
1473
|
-
artifact_digest: digest || null,
|
|
1474
|
-
},
|
|
1475
|
-
);
|
|
1476
|
-
}
|
|
1477
|
-
return document;
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
function importDecisionEntry(db, rootDir, entry) {
|
|
1481
|
-
const existing = decisionRecords.readDecision(db, entry.id);
|
|
1482
|
-
if (existing) {
|
|
1483
|
-
if (existing.digest !== entry.artifact_digest) {
|
|
1484
|
-
throw new TaskLedgerError(
|
|
1485
|
-
'TASK_LEDGER_DECISION_CONFLICT',
|
|
1486
|
-
`decision ${entry.id} changed both locally and in the team ledger`,
|
|
1487
|
-
);
|
|
1488
|
-
}
|
|
1489
|
-
return false;
|
|
1490
|
-
}
|
|
1491
|
-
const document = decisionDocument(rootDir, entry);
|
|
1492
|
-
const accepted = decisionRecords.acceptDecision(db, {
|
|
1493
|
-
id: document.id,
|
|
1494
|
-
scope: document.scope.type === 'baseline'
|
|
1495
|
-
? { baseline_id: document.scope.id }
|
|
1496
|
-
: { change_id: document.scope.id },
|
|
1497
|
-
question: document.question,
|
|
1498
|
-
recommendation: document.recommendation,
|
|
1499
|
-
selection: document.selection,
|
|
1500
|
-
effects: document.effects,
|
|
1501
|
-
non_goals: document.non_goals,
|
|
1502
|
-
owner: document.owner,
|
|
1503
|
-
source: document.source,
|
|
1504
|
-
provenance: document.provenance,
|
|
1505
|
-
applied_refs: document.applied_refs,
|
|
1506
|
-
supersedes_id: document.supersedes_id || undefined,
|
|
1507
|
-
}, { rootDir });
|
|
1508
|
-
if (accepted.digest !== entry.artifact_digest) {
|
|
1509
|
-
throw new TaskLedgerError(
|
|
1510
|
-
'TASK_LEDGER_DECISION_CONFLICT',
|
|
1511
|
-
`decision ${entry.id} changed while it was imported`,
|
|
1512
|
-
);
|
|
1513
|
-
}
|
|
1514
|
-
return true;
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
function mergeTeamDecisions(db, rootDir, entries) {
|
|
1518
|
-
const pending = new Map(entries.map((entry) => [entry.id, entry]));
|
|
1519
|
-
let imported = 0;
|
|
1520
|
-
while (pending.size > 0) {
|
|
1521
|
-
let progressed = false;
|
|
1522
|
-
for (const [id, entry] of pending) {
|
|
1523
|
-
if (entry.supersedes_id
|
|
1524
|
-
&& pending.has(entry.supersedes_id)
|
|
1525
|
-
&& !decisionRecords.readDecision(db, entry.supersedes_id)) {
|
|
1526
|
-
continue;
|
|
1527
|
-
}
|
|
1528
|
-
if (importDecisionEntry(db, rootDir, entry)) imported += 1;
|
|
1529
|
-
pending.delete(id);
|
|
1530
|
-
progressed = true;
|
|
1531
|
-
}
|
|
1532
|
-
if (!progressed) {
|
|
1533
|
-
throw new TaskLedgerError(
|
|
1534
|
-
'TASK_LEDGER_DECISION_CONFLICT',
|
|
1535
|
-
'decision supersession graph cannot be imported deterministically',
|
|
1536
|
-
{ decision_ids: [...pending.keys()].sort() },
|
|
1537
|
-
);
|
|
1538
|
-
}
|
|
1539
|
-
}
|
|
1540
|
-
return imported;
|
|
1541
|
-
}
|
|
1542
|
-
|
|
1543
|
-
function checkpointKey(entry) {
|
|
1544
|
-
return `${entry.stage}:${entry.scope_type}:${entry.scope_id}`;
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
|
-
function mergeTeamCheckpoints(db, entries, lastDigests = {}) {
|
|
1548
|
-
let imported = 0;
|
|
1549
|
-
for (const entry of entries) {
|
|
1550
|
-
const current = db.prepare(
|
|
1551
|
-
`SELECT * FROM stage_checkpoints
|
|
1552
|
-
WHERE stage = ? AND scope_type = ? AND scope_id = ? AND status = 'accepted'
|
|
1553
|
-
ORDER BY revision DESC LIMIT 1`,
|
|
1554
|
-
).get(entry.stage, entry.scope_type, entry.scope_id);
|
|
1555
|
-
const currentDurable = current ? durableCheckpoint(current) : null;
|
|
1556
|
-
if (currentDurable?.digest === entry.digest) continue;
|
|
1557
|
-
const key = checkpointKey(entry);
|
|
1558
|
-
const last = lastDigests[key] || null;
|
|
1559
|
-
if (currentDurable && (!last || currentDurable.digest !== last)) {
|
|
1560
|
-
throw new TaskLedgerError(
|
|
1561
|
-
'TASK_LEDGER_CHECKPOINT_CONFLICT',
|
|
1562
|
-
`checkpoint ${key} changed both locally and in the team ledger`,
|
|
1563
|
-
{
|
|
1564
|
-
checkpoint: key,
|
|
1565
|
-
local_digest: currentDurable.digest,
|
|
1566
|
-
incoming_digest: entry.digest,
|
|
1567
|
-
last_digest: last,
|
|
1568
|
-
},
|
|
1569
|
-
);
|
|
1570
|
-
}
|
|
1571
|
-
if (current) {
|
|
1572
|
-
db.prepare(
|
|
1573
|
-
`UPDATE stage_checkpoints SET status = 'superseded',
|
|
1574
|
-
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
1575
|
-
WHERE id = ?`,
|
|
1576
|
-
).run(current.id);
|
|
1577
|
-
}
|
|
1578
|
-
const importedId = `checkpoint-team-${entry.digest.slice(0, 24)}`;
|
|
1579
|
-
const importedDigest = stageCheckpoints.checkpointDigest({
|
|
1580
|
-
stage: entry.stage,
|
|
1581
|
-
scope_type: entry.scope_type,
|
|
1582
|
-
scope_id: entry.scope_id,
|
|
1583
|
-
revision: entry.revision,
|
|
1584
|
-
payload: entry.payload,
|
|
1585
|
-
evidence: entry.evidence,
|
|
1586
|
-
diagnostics: entry.diagnostics,
|
|
1587
|
-
context_envelope_id: null,
|
|
1588
|
-
supersedes_id: current?.id || null,
|
|
1589
|
-
});
|
|
1590
|
-
db.prepare(
|
|
1591
|
-
`INSERT INTO stage_checkpoints
|
|
1592
|
-
(id, stage, scope_type, scope_id, revision, status, payload_json,
|
|
1593
|
-
evidence_json, diagnostics_json, context_envelope_id, digest,
|
|
1594
|
-
supersedes_id, idempotency_key, accepted_at)
|
|
1595
|
-
VALUES (?, ?, ?, ?, ?, 'accepted', ?, ?, ?, NULL, ?, ?, ?,
|
|
1596
|
-
strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`,
|
|
1597
|
-
).run(
|
|
1598
|
-
importedId,
|
|
1599
|
-
entry.stage,
|
|
1600
|
-
entry.scope_type,
|
|
1601
|
-
entry.scope_id,
|
|
1602
|
-
entry.revision,
|
|
1603
|
-
JSON.stringify(entry.payload),
|
|
1604
|
-
JSON.stringify(entry.evidence),
|
|
1605
|
-
JSON.stringify(entry.diagnostics),
|
|
1606
|
-
importedDigest,
|
|
1607
|
-
current?.id || null,
|
|
1608
|
-
`team-import:${entry.digest}`,
|
|
1609
|
-
);
|
|
1610
|
-
imported += 1;
|
|
1611
|
-
}
|
|
1612
|
-
return imported;
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
function importTaskLedger(db, {
|
|
1616
|
-
rootDir = '.',
|
|
1617
|
-
} = {}) {
|
|
1618
|
-
const document = readTaskLedger(rootDir, { optional: false });
|
|
1619
|
-
const checkpoint = latestImportedCheckpoint(db);
|
|
1620
|
-
const lastDigests = checkpoint.task_digests;
|
|
1621
|
-
const lastChangeDigests = checkpoint.change_digests;
|
|
1622
|
-
if (checkpoint.state_digest && checkpoint.state_digest !== document.state_digest) {
|
|
1623
|
-
if (document.generation < checkpoint.generation) {
|
|
1624
|
-
throw new TaskLedgerError(
|
|
1625
|
-
'TASK_LEDGER_STALE',
|
|
1626
|
-
`task ledger generation ${document.generation} is older than local generation ${checkpoint.generation}`,
|
|
1627
|
-
{
|
|
1628
|
-
incoming_generation: document.generation,
|
|
1629
|
-
local_generation: checkpoint.generation,
|
|
1630
|
-
},
|
|
1631
|
-
);
|
|
1632
|
-
}
|
|
1633
|
-
if (document.generation === checkpoint.generation
|
|
1634
|
-
|| !document.ancestors.includes(checkpoint.state_digest)) {
|
|
1635
|
-
throw new TaskLedgerError(
|
|
1636
|
-
'TASK_LEDGER_HISTORY_CONFLICT',
|
|
1637
|
-
'task ledger does not descend from the last local team checkpoint',
|
|
1638
|
-
{
|
|
1639
|
-
incoming_generation: document.generation,
|
|
1640
|
-
local_generation: checkpoint.generation,
|
|
1641
|
-
incoming_parent_digest: document.parent_digest,
|
|
1642
|
-
local_state_digest: checkpoint.state_digest,
|
|
1643
|
-
},
|
|
1644
|
-
);
|
|
1645
|
-
}
|
|
1646
|
-
}
|
|
1647
|
-
if (checkpoint.state_digest === document.state_digest) {
|
|
1648
|
-
return {
|
|
1649
|
-
imported: 0,
|
|
1650
|
-
preserved: document.tasks.length,
|
|
1651
|
-
deleted: 0,
|
|
1652
|
-
imported_changes: 0,
|
|
1653
|
-
preserved_changes: document.changes.length,
|
|
1654
|
-
imported_decisions: 0,
|
|
1655
|
-
preserved_decisions: document.decisions.length,
|
|
1656
|
-
imported_checkpoints: 0,
|
|
1657
|
-
preserved_checkpoints: document.checkpoints.length,
|
|
1658
|
-
imported_baseline: false,
|
|
1659
|
-
preserved_baseline: Boolean(document.baseline),
|
|
1660
|
-
generation: document.generation,
|
|
1661
|
-
state_digest: document.state_digest,
|
|
1662
|
-
requires_plan_revalidation: false,
|
|
1663
|
-
requires_baseline_revalidation: false,
|
|
1664
|
-
already_current: true,
|
|
1665
|
-
ledger: document,
|
|
1666
|
-
};
|
|
1667
|
-
}
|
|
1668
|
-
const parentUpdates = [];
|
|
1669
|
-
let imported = 0;
|
|
1670
|
-
let preserved = 0;
|
|
1671
|
-
let deleted = 0;
|
|
1672
|
-
let importedChanges = 0;
|
|
1673
|
-
let preservedChanges = 0;
|
|
1674
|
-
let importedDecisions = 0;
|
|
1675
|
-
let importedCheckpoints = 0;
|
|
1676
|
-
let baselineImport = {
|
|
1677
|
-
imported: false,
|
|
1678
|
-
preserved: false,
|
|
1679
|
-
requiresRevalidation: false,
|
|
1680
|
-
};
|
|
1681
|
-
const changesById = new Set((document.changes || []).map((change) => change.id));
|
|
1682
|
-
for (const task of document.tasks) {
|
|
1683
|
-
if (task.change_id && !changesById.has(task.change_id)
|
|
1684
|
-
&& !db.prepare('SELECT 1 FROM changes WHERE id = ?').get(task.change_id)) {
|
|
1685
|
-
throw new TaskLedgerError(
|
|
1686
|
-
'TASK_LEDGER_CHANGE_MISSING',
|
|
1687
|
-
`task ${task.id} references missing change ${task.change_id}`,
|
|
1688
|
-
);
|
|
1689
|
-
}
|
|
1690
|
-
}
|
|
1691
|
-
|
|
1692
|
-
ops.tx(db, () => {
|
|
1693
|
-
baselineImport = mergeBaseline(db, document.baseline, checkpoint);
|
|
1694
|
-
for (const incoming of document.changes) {
|
|
1695
|
-
const row = db.prepare('SELECT * FROM changes WHERE id = ?').get(incoming.id);
|
|
1696
|
-
if (!row) {
|
|
1697
|
-
insertChange(db, incoming);
|
|
1698
|
-
importedChanges += 1;
|
|
1699
|
-
continue;
|
|
1700
|
-
}
|
|
1701
|
-
const current = durableChange(row, null);
|
|
1702
|
-
const last = lastChangeDigests[incoming.id] || null;
|
|
1703
|
-
if (current.digest === incoming.digest) {
|
|
1704
|
-
preservedChanges += 1;
|
|
1705
|
-
continue;
|
|
1706
|
-
}
|
|
1707
|
-
if (!last) {
|
|
1708
|
-
throw new TaskLedgerError(
|
|
1709
|
-
'TASK_LEDGER_CHANGE_CONFLICT',
|
|
1710
|
-
`Change ${incoming.id} exists locally without a common team checkpoint`,
|
|
1711
|
-
{
|
|
1712
|
-
change_id: incoming.id,
|
|
1713
|
-
local_digest: current.digest,
|
|
1714
|
-
incoming_digest: incoming.digest,
|
|
1715
|
-
},
|
|
1716
|
-
);
|
|
1717
|
-
}
|
|
1718
|
-
if (current.digest !== last && incoming.digest === last) {
|
|
1719
|
-
preservedChanges += 1;
|
|
1720
|
-
continue;
|
|
1721
|
-
}
|
|
1722
|
-
if (current.digest !== last && incoming.digest !== last) {
|
|
1723
|
-
throw new TaskLedgerError(
|
|
1724
|
-
'TASK_LEDGER_CHANGE_CONFLICT',
|
|
1725
|
-
`Change ${incoming.id} changed both locally and in the Git ledger`,
|
|
1726
|
-
{
|
|
1727
|
-
change_id: incoming.id,
|
|
1728
|
-
last_digest: last,
|
|
1729
|
-
local_digest: current.digest,
|
|
1730
|
-
incoming_digest: incoming.digest,
|
|
1731
|
-
},
|
|
1732
|
-
);
|
|
1733
|
-
}
|
|
1734
|
-
updateChangeFromLedger(db, incoming);
|
|
1735
|
-
importedChanges += 1;
|
|
1736
|
-
}
|
|
1737
|
-
for (const incoming of document.tasks) {
|
|
1738
|
-
const current = ops.readTask(db, incoming.id);
|
|
1739
|
-
const last = lastDigests[incoming.id] || null;
|
|
1740
|
-
if (!current) {
|
|
1741
|
-
insertTaskFromLedger(db, incoming);
|
|
1742
|
-
parentUpdates.push([incoming.id, incoming.parent_id || null]);
|
|
1743
|
-
imported += 1;
|
|
1744
|
-
continue;
|
|
1745
|
-
}
|
|
1746
|
-
const currentDurable = durableTask(current, null);
|
|
1747
|
-
if (currentDurable.digest === incoming.digest) {
|
|
1748
|
-
preserved += 1;
|
|
1749
|
-
parentUpdates.push([incoming.id, incoming.parent_id || null]);
|
|
1750
|
-
continue;
|
|
1751
|
-
}
|
|
1752
|
-
const runningSession = db.prepare(
|
|
1753
|
-
"SELECT sid FROM sessions WHERE task_id = ? AND status = 'running' LIMIT 1",
|
|
1754
|
-
).get(incoming.id);
|
|
1755
|
-
if (current.session_id || runningSession) {
|
|
1756
|
-
throw new TaskLedgerError(
|
|
1757
|
-
'TASK_LEDGER_ACTIVE_TASK_CONFLICT',
|
|
1758
|
-
`task ${incoming.id} changed in Git while a local session still owns it`,
|
|
1759
|
-
{
|
|
1760
|
-
task_id: incoming.id,
|
|
1761
|
-
local_status: current.status,
|
|
1762
|
-
task_session_id: current.session_id || null,
|
|
1763
|
-
running_session_id: runningSession?.sid || null,
|
|
1764
|
-
},
|
|
1765
|
-
);
|
|
1766
|
-
}
|
|
1767
|
-
if (current.status === 'in_progress' && last && incoming.digest !== last) {
|
|
1768
|
-
throw new TaskLedgerError(
|
|
1769
|
-
'TASK_LEDGER_ACTIVE_TASK_CONFLICT',
|
|
1770
|
-
`task ${incoming.id} changed in Git while a local session is in progress`,
|
|
1771
|
-
{ task_id: incoming.id, local_status: current.status },
|
|
1772
|
-
);
|
|
1773
|
-
}
|
|
1774
|
-
if (last && currentDurable.digest !== last && incoming.digest === last) {
|
|
1775
|
-
preserved += 1;
|
|
1776
|
-
continue;
|
|
1777
|
-
}
|
|
1778
|
-
if (last && currentDurable.digest !== last && incoming.digest !== last) {
|
|
1779
|
-
throw new TaskLedgerError(
|
|
1780
|
-
'TASK_LEDGER_CONFLICT',
|
|
1781
|
-
`task ${incoming.id} changed both locally and in the Git ledger`,
|
|
1782
|
-
{
|
|
1783
|
-
task_id: incoming.id,
|
|
1784
|
-
last_digest: last,
|
|
1785
|
-
local_digest: currentDurable.digest,
|
|
1786
|
-
incoming_digest: incoming.digest,
|
|
1787
|
-
},
|
|
1788
|
-
);
|
|
1789
|
-
}
|
|
1790
|
-
updateTaskFromLedger(db, incoming);
|
|
1791
|
-
parentUpdates.push([incoming.id, incoming.parent_id || null]);
|
|
1792
|
-
imported += 1;
|
|
1793
|
-
}
|
|
1794
|
-
const incomingIds = new Set(document.tasks.map((task) => task.id));
|
|
1795
|
-
for (const [id, last] of Object.entries(lastDigests)) {
|
|
1796
|
-
if (incomingIds.has(id)) continue;
|
|
1797
|
-
const current = ops.readTask(db, id);
|
|
1798
|
-
if (!current) continue;
|
|
1799
|
-
const activeSession = db.prepare(
|
|
1800
|
-
"SELECT sid FROM sessions WHERE task_id = ? AND status = 'running' LIMIT 1",
|
|
1801
|
-
).get(id);
|
|
1802
|
-
if (current.status === 'in_progress' || current.session_id || activeSession) {
|
|
1803
|
-
throw new TaskLedgerError(
|
|
1804
|
-
'TASK_LEDGER_ACTIVE_TASK_CONFLICT',
|
|
1805
|
-
`task ${id} was removed from Git while local work is in progress`,
|
|
1806
|
-
{ task_id: id, local_status: current.status },
|
|
1807
|
-
);
|
|
1808
|
-
}
|
|
1809
|
-
const currentDurable = durableTask(current, null);
|
|
1810
|
-
if (currentDurable.digest !== last) {
|
|
1811
|
-
throw new TaskLedgerError(
|
|
1812
|
-
'TASK_LEDGER_CONFLICT',
|
|
1813
|
-
`task ${id} was removed from Git after it changed locally`,
|
|
1814
|
-
{
|
|
1815
|
-
task_id: id,
|
|
1816
|
-
last_digest: last,
|
|
1817
|
-
local_digest: currentDurable.digest,
|
|
1818
|
-
incoming_digest: null,
|
|
1819
|
-
},
|
|
1820
|
-
);
|
|
1821
|
-
}
|
|
1822
|
-
const sessionCount = db.prepare(
|
|
1823
|
-
'SELECT COUNT(*) AS count FROM sessions WHERE task_id = ?',
|
|
1824
|
-
).get(id).count;
|
|
1825
|
-
if (sessionCount > 0) {
|
|
1826
|
-
throw new TaskLedgerError(
|
|
1827
|
-
'TASK_LEDGER_CONFLICT',
|
|
1828
|
-
`task ${id} cannot be removed because local session history still references it`,
|
|
1829
|
-
{ task_id: id, session_count: sessionCount },
|
|
1830
|
-
);
|
|
1831
|
-
}
|
|
1832
|
-
ops.deleteTask(db, id, { rootDir });
|
|
1833
|
-
deleted += 1;
|
|
1834
|
-
}
|
|
1835
|
-
for (const [id, parentId] of parentUpdates) {
|
|
1836
|
-
if (parentId && !db.prepare('SELECT 1 FROM tasks WHERE id = ?').get(parentId)) {
|
|
1837
|
-
throw new TaskLedgerError(
|
|
1838
|
-
'TASK_LEDGER_PARENT_MISSING',
|
|
1839
|
-
`task ${id} references missing parent ${parentId}`,
|
|
1840
|
-
);
|
|
1841
|
-
}
|
|
1842
|
-
db.prepare('UPDATE tasks SET parent_id = ? WHERE id = ?').run(parentId, id);
|
|
1843
|
-
}
|
|
1844
|
-
importedDecisions = mergeTeamDecisions(db, rootDir, document.decisions);
|
|
1845
|
-
importedCheckpoints = mergeTeamCheckpoints(
|
|
1846
|
-
db,
|
|
1847
|
-
document.checkpoints,
|
|
1848
|
-
checkpoint.checkpoint_digests,
|
|
1849
|
-
);
|
|
1850
|
-
recordLedgerEvent(db, 'task_ledger_imported', document, {
|
|
1851
|
-
imported,
|
|
1852
|
-
preserved,
|
|
1853
|
-
deleted,
|
|
1854
|
-
imported_changes: importedChanges,
|
|
1855
|
-
preserved_changes: preservedChanges,
|
|
1856
|
-
imported_decisions: importedDecisions,
|
|
1857
|
-
imported_checkpoints: importedCheckpoints,
|
|
1858
|
-
});
|
|
1859
|
-
});
|
|
1860
|
-
return {
|
|
1861
|
-
imported,
|
|
1862
|
-
preserved,
|
|
1863
|
-
deleted,
|
|
1864
|
-
imported_changes: importedChanges,
|
|
1865
|
-
preserved_changes: preservedChanges,
|
|
1866
|
-
imported_decisions: importedDecisions,
|
|
1867
|
-
preserved_decisions: document.decisions.length - importedDecisions,
|
|
1868
|
-
imported_checkpoints: importedCheckpoints,
|
|
1869
|
-
preserved_checkpoints: document.checkpoints.length - importedCheckpoints,
|
|
1870
|
-
imported_baseline: baselineImport.imported,
|
|
1871
|
-
preserved_baseline: baselineImport.preserved,
|
|
1872
|
-
generation: document.generation,
|
|
1873
|
-
state_digest: document.state_digest,
|
|
1874
|
-
requires_plan_revalidation: imported > 0
|
|
1875
|
-
|| deleted > 0
|
|
1876
|
-
|| importedChanges > 0
|
|
1877
|
-
|| baselineImport.imported,
|
|
1878
|
-
requires_baseline_revalidation: baselineImport.requiresRevalidation,
|
|
1879
|
-
already_current: false,
|
|
1880
|
-
ledger: document,
|
|
1881
|
-
};
|
|
1882
|
-
}
|
|
1883
|
-
|
|
1884
|
-
function inspectTaskLedger(db, { rootDir = '.' } = {}) {
|
|
1885
|
-
const document = readTaskLedger(rootDir);
|
|
1886
|
-
if (!document) {
|
|
1887
|
-
return {
|
|
1888
|
-
status: 'missing',
|
|
1889
|
-
path: ledgerPath(rootDir),
|
|
1890
|
-
generation: null,
|
|
1891
|
-
state_digest: null,
|
|
1892
|
-
};
|
|
1893
|
-
}
|
|
1894
|
-
const localTasks = ops.listTasks(db, { status: 'any', limit: 1000 });
|
|
1895
|
-
const previous = new Map(document.tasks.map((task) => [task.id, task]));
|
|
1896
|
-
const local = localTasks.map((task) => durableTask(task, previous.get(task.id)))
|
|
1897
|
-
.sort((left, right) => left.id.localeCompare(right.id));
|
|
1898
|
-
const localChanges = readChangesForLedger(
|
|
1899
|
-
db,
|
|
1900
|
-
localTasks,
|
|
1901
|
-
new Map(document.changes.map((change) => [change.id, change])),
|
|
1902
|
-
);
|
|
1903
|
-
const localDecisions = readDecisionsForLedger(db);
|
|
1904
|
-
const localCheckpoints = readCheckpointsForLedger(db);
|
|
1905
|
-
const localBaseline = readBaselineForLedger(db, document.baseline || null);
|
|
1906
|
-
const localDigest = sha256(statePayload({
|
|
1907
|
-
baseline: localBaseline,
|
|
1908
|
-
changes: localChanges,
|
|
1909
|
-
decisions: localDecisions,
|
|
1910
|
-
checkpoints: localCheckpoints,
|
|
1911
|
-
tasks: local,
|
|
1912
|
-
}));
|
|
1913
|
-
const baselineRequiresRevalidation = Array.isArray(localBaseline?.gaps)
|
|
1914
|
-
&& localBaseline.gaps.some((gap) => (
|
|
1915
|
-
gap.id === 'team-ledger-revalidation-required' && gap.status === 'open'
|
|
1916
|
-
));
|
|
1917
|
-
const importedPayloadDigest = baselineRequiresRevalidation
|
|
1918
|
-
? sha256(statePayload({
|
|
1919
|
-
baseline: document.baseline,
|
|
1920
|
-
changes: localChanges,
|
|
1921
|
-
decisions: localDecisions,
|
|
1922
|
-
checkpoints: localCheckpoints,
|
|
1923
|
-
tasks: local,
|
|
1924
|
-
}))
|
|
1925
|
-
: null;
|
|
1926
|
-
const status = localDigest === document.state_digest
|
|
1927
|
-
? 'current'
|
|
1928
|
-
: (importedPayloadDigest === document.state_digest
|
|
1929
|
-
? 'revalidation_required'
|
|
1930
|
-
: 'drifted');
|
|
1931
|
-
return {
|
|
1932
|
-
status,
|
|
1933
|
-
path: ledgerPath(rootDir),
|
|
1934
|
-
generation: document.generation,
|
|
1935
|
-
state_digest: document.state_digest,
|
|
1936
|
-
local_state_digest: localDigest,
|
|
1937
|
-
baseline_revalidation_required: baselineRequiresRevalidation,
|
|
1938
|
-
};
|
|
1939
|
-
}
|
|
1940
|
-
|
|
1941
|
-
module.exports = {
|
|
1942
|
-
DURABLE_BASELINE_FIELDS,
|
|
1943
|
-
DURABLE_CHANGE_FIELDS,
|
|
1944
|
-
DURABLE_CHECKPOINT_FIELDS,
|
|
1945
|
-
DURABLE_DECISION_FIELDS,
|
|
1946
|
-
DURABLE_TASK_FIELDS,
|
|
1947
|
-
LEDGER_KIND,
|
|
1948
|
-
LEDGER_RELATIVE_PATH,
|
|
1949
|
-
LEDGER_SCHEMA_VERSION,
|
|
1950
|
-
TaskLedgerError,
|
|
1951
|
-
durableChange,
|
|
1952
|
-
durableCheckpoint,
|
|
1953
|
-
durableDecision,
|
|
1954
|
-
durableBaseline,
|
|
1955
|
-
durableTask,
|
|
1956
|
-
importTaskLedger,
|
|
1957
|
-
inspectTaskLedger,
|
|
1958
|
-
ledgerPath,
|
|
1959
|
-
publishTaskLedger,
|
|
1960
|
-
readTaskLedger,
|
|
1961
|
-
rollbackTaskLedgerPublication,
|
|
1962
|
-
syncTaskLedger,
|
|
1963
|
-
validateLedger,
|
|
1964
|
-
};
|