ultra-builder-pro-cli 0.22.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ultra-template/docs/research/README.md +4 -4
- package/CHANGELOG.md +83 -0
- package/README.md +68 -35
- package/adapters/_shared/codex-assets.cjs +121 -60
- package/adapters/_shared/interaction-contract.cjs +20 -7
- package/adapters/_shared/provenance.cjs +7 -2
- package/adapters/_shared/runtime-assets.cjs +4 -3
- package/adapters/_shared/runtime-native.cjs +132 -0
- package/adapters/claude.js +61 -31
- package/adapters/codex.js +178 -81
- package/adapters/grok.js +372 -0
- package/adapters/kimi.js +8 -4
- package/adapters/opencode.js +211 -33
- package/agents/code-reviewer.md +4 -2
- package/agents/debugger.md +4 -1
- package/agents/review-code.md +7 -3
- package/agents/review-comments.md +7 -4
- package/agents/review-coordinator.md +11 -7
- package/agents/review-design.md +8 -4
- package/agents/review-errors.md +8 -4
- package/agents/review-spec.md +8 -5
- package/agents/review-tests.md +7 -4
- package/agents/tdd-runner.md +5 -2
- package/bin/install.js +9 -4
- package/bin/orchestrator.js +4 -13
- package/docs/AGENT-CONTEXT.md +141 -231
- package/docs/ARCHITECTURE.md +203 -498
- package/docs/ARTIFACT-AUTHORITY.md +38 -35
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +12 -9
- package/docs/ROADMAP.md +85 -110
- package/docs/RUNTIME-COMPAT-MATRIX.md +139 -174
- package/docs/SKILL-AUTHORING.md +3 -3
- package/docs/STATE-DB-ACCESS-POLICY.md +25 -20
- package/docs/WORKFLOW-LIFECYCLE.md +174 -442
- package/hooks/README.md +11 -8
- package/hooks/active_task_context.py +16 -16
- package/hooks/adapters/grok.py +134 -0
- package/hooks/{context_spine.py → context_envelope.py} +26 -22
- package/hooks/health_check.py +19 -29
- package/hooks/pre_stop_check.py +22 -12
- package/hooks/subagent_tracker.py +1 -1
- package/hooks/workflow_checkpoint.py +14 -13
- package/hooks/workflow_context.py +11 -12
- package/hooks/workflow_resume.py +14 -13
- package/mcp-server/{breadcrumb.cjs → hook-context.cjs} +7 -9
- package/mcp-server/lib/artifact-registry.cjs +25 -1
- package/mcp-server/lib/baseline-workflow.cjs +36 -38
- package/mcp-server/lib/canonical-json.cjs +19 -0
- package/mcp-server/lib/change-workflow.cjs +637 -49
- package/mcp-server/lib/context-envelope.cjs +508 -0
- package/mcp-server/lib/decision-records.cjs +337 -0
- package/mcp-server/lib/doctor.cjs +91 -33
- package/mcp-server/lib/init-project.cjs +78 -39
- package/mcp-server/lib/managed-file-write.cjs +116 -0
- package/mcp-server/lib/plan-checkpoint.cjs +177 -0
- package/mcp-server/lib/project-context.cjs +178 -0
- package/mcp-server/lib/stage-checkpoints.cjs +309 -0
- package/mcp-server/lib/state-db.cjs +155 -24
- package/mcp-server/lib/task-ledger.cjs +522 -13
- package/mcp-server/lib/ultra-facade.cjs +834 -0
- package/mcp-server/lib/worker-packet.cjs +358 -0
- package/mcp-server/server.cjs +208 -1059
- package/orchestrator/daemon.cjs +27 -9
- package/orchestrator/parallel-orchestrator.cjs +111 -76
- package/orchestrator/session-runner.cjs +135 -34
- package/package.json +10 -3
- package/skills/ultra-change/SKILL.md +39 -98
- package/skills/ultra-deliver/SKILL.md +42 -65
- package/skills/ultra-dev/SKILL.md +52 -99
- package/skills/ultra-doctor/SKILL.md +35 -57
- package/skills/ultra-init/SKILL.md +48 -88
- package/skills/ultra-init/references/brownfield-adoption.md +11 -8
- package/skills/ultra-plan/SKILL.md +54 -132
- package/skills/ultra-research/SKILL.md +58 -112
- package/skills/ultra-review/SKILL.md +28 -46
- package/skills/ultra-review/references/unified-schema.md +2 -0
- package/skills/ultra-review/scripts/review_wait.py +8 -0
- package/skills/ultra-status/SKILL.md +22 -45
- package/skills/ultra-test/SKILL.md +27 -61
- package/skills/ultra-think/SKILL.md +29 -48
- package/skills/ultra-think/references/interaction-boundary.md +56 -0
- package/spec/cli-protocol.md +80 -187
- package/spec/mcp-tools.yaml +130 -2738
- package/spec/schemas/mcp-tools.schema.json +1 -1
- package/spec/schemas/state-db.sql +107 -4
- package/spec/schemas/{task-ledger.v1.schema.json → task-ledger.v2.schema.json} +70 -6
- package/ultra-tools/commands/status.cjs +369 -243
- package/mcp-server/lib/context-spine.cjs +0 -1208
- package/mcp-server/lib/decision-dialogue.cjs +0 -1128
- package/mcp-server/lib/project-breadcrumb.cjs +0 -173
- package/mcp-server/lib/spec-learning.cjs +0 -284
- package/mcp-server/lib/workflow-state.cjs +0 -2965
- package/skills/ultra-think/references/decision-dialogue.md +0 -153
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
|
|
5
|
+
const { version: PACKAGE_VERSION } = require('../../package.json');
|
|
6
|
+
const artifactRegistry = require('./artifact-registry.cjs');
|
|
7
|
+
const baselines = require('./baseline-workflow.cjs');
|
|
8
|
+
const canonical = require('./canonical-json.cjs');
|
|
9
|
+
const changes = require('./change-workflow.cjs');
|
|
10
|
+
const decisions = require('./decision-records.cjs');
|
|
11
|
+
const { writeManagedJson } = require('./managed-file-write.cjs');
|
|
12
|
+
const ops = require('./state-ops.cjs');
|
|
13
|
+
const { readStableProjectFile } = require('./safe-project-file.cjs');
|
|
14
|
+
const checkpoints = require('./stage-checkpoints.cjs');
|
|
15
|
+
const taskLedger = require('./task-ledger.cjs');
|
|
16
|
+
|
|
17
|
+
const SUMMARY_LIMIT = 16 * 1024;
|
|
18
|
+
const FULL_LIMIT = 64 * 1024;
|
|
19
|
+
|
|
20
|
+
class ContextEnvelopeError extends Error {
|
|
21
|
+
constructor(code, message, details) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = 'ContextEnvelopeError';
|
|
24
|
+
this.code = code;
|
|
25
|
+
if (details !== undefined) this.details = details;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseJson(value, fallback) {
|
|
30
|
+
try { return value == null ? fallback : JSON.parse(value); }
|
|
31
|
+
catch { return fallback; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function text(value, max) {
|
|
35
|
+
if (typeof value !== 'string') return value;
|
|
36
|
+
if (value.length <= max) return value;
|
|
37
|
+
return `${value.slice(0, Math.max(0, max - 32))}…[truncated ${value.length - max + 32} chars]`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function compactValue(value, {
|
|
41
|
+
depth = 0,
|
|
42
|
+
maxDepth = 5,
|
|
43
|
+
textLimit = 2000,
|
|
44
|
+
arrayLimit = 50,
|
|
45
|
+
} = {}) {
|
|
46
|
+
if (typeof value === 'string') return text(value, textLimit);
|
|
47
|
+
if (value === null || value === undefined || typeof value !== 'object') return value;
|
|
48
|
+
if (depth >= maxDepth) return Array.isArray(value) ? `[${value.length} items]` : '[object]';
|
|
49
|
+
if (Array.isArray(value)) {
|
|
50
|
+
return value.slice(0, arrayLimit).map((item) => compactValue(item, {
|
|
51
|
+
depth: depth + 1, maxDepth, textLimit, arrayLimit,
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => [
|
|
55
|
+
key,
|
|
56
|
+
compactValue(child, { depth: depth + 1, maxDepth, textLimit, arrayLimit }),
|
|
57
|
+
]));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function selectedChange(db, changeId) {
|
|
61
|
+
if (!changeId) return null;
|
|
62
|
+
return changes.readChange(db, changeId);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function selectedTask(db, taskId) {
|
|
66
|
+
if (!taskId) return null;
|
|
67
|
+
return ops.readTask(db, taskId);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function resolvedScope(db, input = {}) {
|
|
71
|
+
const requested = input.scope || {};
|
|
72
|
+
const task = selectedTask(db, requested.task_id);
|
|
73
|
+
const changeId = requested.change_id || task?.change_id || null;
|
|
74
|
+
const change = selectedChange(db, changeId);
|
|
75
|
+
const baseline = baselines.readBaseline(db);
|
|
76
|
+
if (task) return { type: 'task', id: task.id, baseline, change, task };
|
|
77
|
+
if (change) return { type: 'change', id: change.id, baseline, change, task: null };
|
|
78
|
+
if (!requested.task_id && !requested.change_id) {
|
|
79
|
+
const activeChangeRow = db.prepare(
|
|
80
|
+
`SELECT id FROM changes
|
|
81
|
+
WHERE status IN ('active', 'blocked', 'ready')
|
|
82
|
+
ORDER BY
|
|
83
|
+
CASE status WHEN 'active' THEN 0 WHEN 'blocked' THEN 1 ELSE 2 END,
|
|
84
|
+
updated_at DESC,
|
|
85
|
+
rowid DESC
|
|
86
|
+
LIMIT 1`,
|
|
87
|
+
).get();
|
|
88
|
+
if (activeChangeRow) {
|
|
89
|
+
const activeChange = selectedChange(db, activeChangeRow.id);
|
|
90
|
+
const activeTaskRow = db.prepare(
|
|
91
|
+
`SELECT id FROM tasks
|
|
92
|
+
WHERE change_id = ? AND status IN ('in_progress', 'blocked', 'pending')
|
|
93
|
+
ORDER BY
|
|
94
|
+
CASE status WHEN 'in_progress' THEN 0 WHEN 'blocked' THEN 1 ELSE 2 END,
|
|
95
|
+
updated_at DESC,
|
|
96
|
+
rowid DESC
|
|
97
|
+
LIMIT 1`,
|
|
98
|
+
).get(activeChangeRow.id);
|
|
99
|
+
const activeTask = activeTaskRow ? selectedTask(db, activeTaskRow.id) : null;
|
|
100
|
+
if (activeTask) {
|
|
101
|
+
return {
|
|
102
|
+
type: 'task',
|
|
103
|
+
id: activeTask.id,
|
|
104
|
+
baseline,
|
|
105
|
+
change: activeChange,
|
|
106
|
+
task: activeTask,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
type: 'change',
|
|
111
|
+
id: activeChange.id,
|
|
112
|
+
baseline,
|
|
113
|
+
change: activeChange,
|
|
114
|
+
task: null,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (baseline) return { type: 'baseline', id: baseline.id, baseline, change: null, task: null };
|
|
119
|
+
return { type: 'project', id: 'project', baseline: null, change: null, task: null };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function acceptedDecisionRows(db, scope, rootDir) {
|
|
123
|
+
const rows = [];
|
|
124
|
+
if (scope.baseline) {
|
|
125
|
+
rows.push(...decisions.listAcceptedDecisions(
|
|
126
|
+
db,
|
|
127
|
+
{ baseline_id: scope.baseline.id },
|
|
128
|
+
{ limit: 200, rootDir, validateFiles: true },
|
|
129
|
+
));
|
|
130
|
+
}
|
|
131
|
+
if (scope.change) {
|
|
132
|
+
rows.push(...decisions.listAcceptedDecisions(
|
|
133
|
+
db,
|
|
134
|
+
{ change_id: scope.change.id },
|
|
135
|
+
{ limit: 200, rootDir, validateFiles: true },
|
|
136
|
+
));
|
|
137
|
+
}
|
|
138
|
+
return rows;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function checkpointRows(db, scope) {
|
|
142
|
+
const rows = [];
|
|
143
|
+
if (scope.change) {
|
|
144
|
+
rows.push(...checkpoints.listCheckpoints(
|
|
145
|
+
db,
|
|
146
|
+
{ scope: { change_id: scope.change.id }, limit: 100 },
|
|
147
|
+
).filter((item) => item.status !== 'superseded'));
|
|
148
|
+
}
|
|
149
|
+
if (scope.task) {
|
|
150
|
+
rows.push(...checkpoints.listCheckpoints(
|
|
151
|
+
db,
|
|
152
|
+
{ scope: { task_id: scope.task.id }, limit: 100 },
|
|
153
|
+
).filter((item) => item.status !== 'superseded'));
|
|
154
|
+
}
|
|
155
|
+
if (!scope.change && scope.baseline) {
|
|
156
|
+
rows.push(...checkpoints.listCheckpoints(
|
|
157
|
+
db,
|
|
158
|
+
{ scope: { baseline_id: scope.baseline.id }, limit: 100 },
|
|
159
|
+
).filter((item) => item.status !== 'superseded'));
|
|
160
|
+
}
|
|
161
|
+
return rows;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function artifactRows(db, scope) {
|
|
165
|
+
const clauses = [
|
|
166
|
+
"status <> 'archived'",
|
|
167
|
+
"kind NOT IN ('context_envelope', 'worker_packet')",
|
|
168
|
+
];
|
|
169
|
+
const params = [];
|
|
170
|
+
if (scope.task) {
|
|
171
|
+
clauses.push('(task_id = ? OR change_id = ?)');
|
|
172
|
+
params.push(scope.task.id, scope.change?.id || '');
|
|
173
|
+
} else if (scope.change) {
|
|
174
|
+
clauses.push('change_id = ?');
|
|
175
|
+
params.push(scope.change.id);
|
|
176
|
+
} else if (scope.baseline) {
|
|
177
|
+
clauses.push("(owner_type = 'baseline' AND owner_id = ?)");
|
|
178
|
+
params.push(scope.baseline.id);
|
|
179
|
+
} else {
|
|
180
|
+
clauses.push("owner_type = 'project'");
|
|
181
|
+
}
|
|
182
|
+
return db.prepare(
|
|
183
|
+
`SELECT id, owner_type, owner_id, kind, path, digest, status, provenance_json
|
|
184
|
+
FROM artifacts WHERE ${clauses.join(' AND ')}
|
|
185
|
+
ORDER BY updated_at DESC, id ASC LIMIT 500`,
|
|
186
|
+
).all(...params).map((row) => ({
|
|
187
|
+
id: row.id,
|
|
188
|
+
owner_type: row.owner_type,
|
|
189
|
+
owner_id: row.owner_id,
|
|
190
|
+
kind: row.kind,
|
|
191
|
+
ref: row.path,
|
|
192
|
+
digest: row.digest,
|
|
193
|
+
status: row.status,
|
|
194
|
+
provenance: parseJson(row.provenance_json, {}),
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function executionState(db, scope) {
|
|
199
|
+
if (!scope.task) return { session: null };
|
|
200
|
+
const session = db.prepare(
|
|
201
|
+
`SELECT sid, task_id, runtime, worktree_path, artifact_dir, status,
|
|
202
|
+
lease_expires_at, heartbeat_at, started_at
|
|
203
|
+
FROM sessions WHERE task_id = ?
|
|
204
|
+
ORDER BY started_at DESC, rowid DESC LIMIT 1`,
|
|
205
|
+
).get(scope.task.id) || null;
|
|
206
|
+
const packet = db.prepare(
|
|
207
|
+
`SELECT id, role, context_envelope_id, context_digest, task_digest,
|
|
208
|
+
decision_digest, packet_digest, packet_path, output_path, created_at
|
|
209
|
+
FROM worker_packets WHERE scope_type = 'task' AND scope_id = ?
|
|
210
|
+
AND status = 'assigned'
|
|
211
|
+
ORDER BY created_at DESC, rowid DESC LIMIT 1`,
|
|
212
|
+
).get(scope.task.id) || null;
|
|
213
|
+
return { session, worker_packet: packet };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function ledgerHealth(db, rootDir) {
|
|
217
|
+
try {
|
|
218
|
+
const result = taskLedger.inspectTaskLedger(db, { rootDir });
|
|
219
|
+
return {
|
|
220
|
+
status: result.status || 'available',
|
|
221
|
+
generation: result.generation ?? null,
|
|
222
|
+
state_digest: result.state_digest || null,
|
|
223
|
+
path: taskLedger.LEDGER_RELATIVE_PATH,
|
|
224
|
+
};
|
|
225
|
+
} catch (error) {
|
|
226
|
+
return {
|
|
227
|
+
status: 'migration_required',
|
|
228
|
+
generation: null,
|
|
229
|
+
state_digest: null,
|
|
230
|
+
path: taskLedger.LEDGER_RELATIVE_PATH,
|
|
231
|
+
migration: {
|
|
232
|
+
required: true,
|
|
233
|
+
code: error.code || 'TASK_LEDGER_INVALID',
|
|
234
|
+
action: 'ultra.sync migrate',
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function authorityPayload(db, input, options) {
|
|
241
|
+
const rootDir = path.resolve(options.rootDir || process.cwd());
|
|
242
|
+
const stage = input.stage || 'project';
|
|
243
|
+
const scope = resolvedScope(db, input);
|
|
244
|
+
const baselineHealth = scope.baseline
|
|
245
|
+
? baselines.inspectBaseline(db, { rootDir })
|
|
246
|
+
: { blockers: [], warnings: [] };
|
|
247
|
+
const checkpointList = checkpointRows(db, scope);
|
|
248
|
+
const diagnostics = {
|
|
249
|
+
warnings: (baselineHealth.warnings || []).map((code) => ({ code, severity: 'warning' })),
|
|
250
|
+
needs_attention: checkpointList.flatMap((item) => (
|
|
251
|
+
item.diagnostics.filter((diagnostic) => diagnostic.severity === 'needs_attention')
|
|
252
|
+
)),
|
|
253
|
+
hard_conflicts: checkpointList.flatMap((item) => (
|
|
254
|
+
item.diagnostics.filter((diagnostic) => diagnostic.severity === 'hard_conflict')
|
|
255
|
+
)),
|
|
256
|
+
};
|
|
257
|
+
for (const code of baselineHealth.blockers || []) {
|
|
258
|
+
diagnostics.needs_attention.push({ code, severity: 'needs_attention' });
|
|
259
|
+
}
|
|
260
|
+
const ledger = ledgerHealth(db, rootDir);
|
|
261
|
+
if (ledger.migration?.required) {
|
|
262
|
+
diagnostics.needs_attention.push({
|
|
263
|
+
code: ledger.migration.code,
|
|
264
|
+
severity: 'needs_attention',
|
|
265
|
+
action: ledger.migration.action,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
schema_version: '1.0',
|
|
270
|
+
project: {
|
|
271
|
+
root: '.',
|
|
272
|
+
runtime: options.runtime || 'unknown',
|
|
273
|
+
plugin_version: PACKAGE_VERSION,
|
|
274
|
+
health: diagnostics.hard_conflicts.length > 0 ? 'conflict' : 'available',
|
|
275
|
+
},
|
|
276
|
+
git: scope.baseline ? {
|
|
277
|
+
head: baselineHealth.current_repository_revision || scope.baseline.repository_revision || null,
|
|
278
|
+
branch: scope.baseline.repository_branch || null,
|
|
279
|
+
scoped_worktree_digest: scope.baseline.worktree_digest || null,
|
|
280
|
+
drift: {
|
|
281
|
+
metadata_only: (baselineHealth.warnings || []).includes('BASELINE_METADATA_ONLY_DRIFT'),
|
|
282
|
+
source_or_spec: (baselineHealth.blockers || []).some((code) => (
|
|
283
|
+
String(code).includes('SOURCE') || String(code).includes('SPEC')
|
|
284
|
+
)),
|
|
285
|
+
},
|
|
286
|
+
} : null,
|
|
287
|
+
baseline: scope.baseline,
|
|
288
|
+
change: scope.change,
|
|
289
|
+
task: scope.task,
|
|
290
|
+
decisions: acceptedDecisionRows(db, scope, rootDir),
|
|
291
|
+
checkpoints: checkpointList,
|
|
292
|
+
evidence: [
|
|
293
|
+
...(scope.baseline?.evidence || []),
|
|
294
|
+
...artifactRows(db, scope),
|
|
295
|
+
],
|
|
296
|
+
execution: {
|
|
297
|
+
stage,
|
|
298
|
+
...executionState(db, scope),
|
|
299
|
+
},
|
|
300
|
+
team_checkpoint: ledger,
|
|
301
|
+
diagnostics,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function presentation(authority, detail) {
|
|
306
|
+
const summary = detail !== 'full';
|
|
307
|
+
return compactValue(authority, {
|
|
308
|
+
maxDepth: summary ? 4 : 7,
|
|
309
|
+
textLimit: summary ? 500 : 4000,
|
|
310
|
+
arrayLimit: summary ? 12 : 100,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function fitEnvelope(authority, detail, digest) {
|
|
315
|
+
const limit = detail === 'full' ? FULL_LIMIT : SUMMARY_LIMIT;
|
|
316
|
+
let envelope = presentation(authority, detail);
|
|
317
|
+
let output = { schema_version: '1.0', digest, detail, envelope };
|
|
318
|
+
if (Buffer.byteLength(JSON.stringify(output)) <= limit) return output;
|
|
319
|
+
envelope = {
|
|
320
|
+
...envelope,
|
|
321
|
+
evidence: (envelope.evidence || []).slice(0, detail === 'full' ? 30 : 6),
|
|
322
|
+
decisions: (envelope.decisions || []).slice(0, detail === 'full' ? 30 : 8),
|
|
323
|
+
checkpoints: (envelope.checkpoints || []).slice(0, detail === 'full' ? 20 : 8),
|
|
324
|
+
cursors: {
|
|
325
|
+
evidence: authority.evidence.length > (detail === 'full' ? 30 : 6)
|
|
326
|
+
? { available: authority.evidence.length, action: 'ultra.context full with narrower scope' }
|
|
327
|
+
: null,
|
|
328
|
+
decisions: authority.decisions.length > (detail === 'full' ? 30 : 8)
|
|
329
|
+
? { available: authority.decisions.length, action: 'ultra.context full with narrower scope' }
|
|
330
|
+
: null,
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
output = { schema_version: '1.0', digest, detail, envelope };
|
|
334
|
+
if (Buffer.byteLength(JSON.stringify(output)) <= limit) return output;
|
|
335
|
+
envelope = compactValue(envelope, {
|
|
336
|
+
maxDepth: 4,
|
|
337
|
+
textLimit: detail === 'full' ? 1000 : 240,
|
|
338
|
+
arrayLimit: detail === 'full' ? 20 : 6,
|
|
339
|
+
});
|
|
340
|
+
output = { schema_version: '1.0', digest, detail, envelope };
|
|
341
|
+
if (Buffer.byteLength(JSON.stringify(output)) > limit) {
|
|
342
|
+
const error = new Error(`context envelope exceeds ${limit} byte limit after compaction`);
|
|
343
|
+
error.code = 'CONTEXT_ENVELOPE_LIMIT_EXCEEDED';
|
|
344
|
+
throw error;
|
|
345
|
+
}
|
|
346
|
+
return output;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function buildEnvelope(db, input = {}, options = {}) {
|
|
350
|
+
const authority = authorityPayload(db, input, options);
|
|
351
|
+
const digest = canonical.digest(authority);
|
|
352
|
+
const output = fitEnvelope(authority, input.detail || 'summary', digest);
|
|
353
|
+
output.bytes = Buffer.byteLength(JSON.stringify(output));
|
|
354
|
+
return output;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function contextArtifactPath(scope, id) {
|
|
358
|
+
if (scope.task) return `.ultra/.runtime/projections/contexts/${id}.json`;
|
|
359
|
+
if (scope.change) return `${scope.change.artifact_root}/contexts/${id}.json`;
|
|
360
|
+
return `.ultra/.runtime/projections/contexts/${id}.json`;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function readEnvelope(db, id, { rootDir = process.cwd() } = {}) {
|
|
364
|
+
const row = db.prepare('SELECT * FROM context_envelopes WHERE id = ?').get(id);
|
|
365
|
+
if (!row) {
|
|
366
|
+
throw new ContextEnvelopeError(
|
|
367
|
+
'CONTEXT_ENVELOPE_NOT_FOUND',
|
|
368
|
+
`Context Envelope not found: ${id}`,
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
const read = readStableProjectFile(rootDir, row.artifact_path, { encoding: 'utf8' });
|
|
372
|
+
if (!row.file_digest || read.digest !== row.file_digest) {
|
|
373
|
+
throw new ContextEnvelopeError(
|
|
374
|
+
'CONTEXT_ENVELOPE_FILE_DRIFT',
|
|
375
|
+
`Context Envelope bytes no longer match ${id}`,
|
|
376
|
+
{
|
|
377
|
+
path: row.artifact_path,
|
|
378
|
+
expected: row.file_digest || null,
|
|
379
|
+
actual: read.digest,
|
|
380
|
+
},
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
let document;
|
|
384
|
+
try {
|
|
385
|
+
document = JSON.parse(read.text);
|
|
386
|
+
} catch (cause) {
|
|
387
|
+
throw new ContextEnvelopeError(
|
|
388
|
+
'CONTEXT_ENVELOPE_INVALID',
|
|
389
|
+
`Context Envelope is not valid JSON: ${row.artifact_path}`,
|
|
390
|
+
{ cause: cause.message },
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
const storedPayload = parseJson(row.payload_json, null);
|
|
394
|
+
if (!storedPayload
|
|
395
|
+
|| document.schema_version !== '1.0'
|
|
396
|
+
|| document.detail !== 'full'
|
|
397
|
+
|| document.digest !== row.digest
|
|
398
|
+
|| canonical.canonicalJson(document.envelope)
|
|
399
|
+
!== canonical.canonicalJson(storedPayload)) {
|
|
400
|
+
throw new ContextEnvelopeError(
|
|
401
|
+
'CONTEXT_ENVELOPE_DIGEST_MISMATCH',
|
|
402
|
+
`Context Envelope authority does not match its database record: ${id}`,
|
|
403
|
+
{ path: row.artifact_path },
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
return {
|
|
407
|
+
id: row.id,
|
|
408
|
+
stage: row.stage,
|
|
409
|
+
scope_type: row.scope_type,
|
|
410
|
+
scope_id: row.scope_id,
|
|
411
|
+
digest: row.digest,
|
|
412
|
+
file_digest: row.file_digest,
|
|
413
|
+
artifact_path: row.artifact_path,
|
|
414
|
+
document,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function persistEnvelope(db, input = {}, options = {}) {
|
|
419
|
+
const full = buildEnvelope(db, { ...input, detail: 'full' }, options);
|
|
420
|
+
const existing = db.prepare(
|
|
421
|
+
'SELECT * FROM context_envelopes WHERE digest = ?',
|
|
422
|
+
).get(full.digest);
|
|
423
|
+
if (existing) {
|
|
424
|
+
readEnvelope(db, existing.id, options);
|
|
425
|
+
return {
|
|
426
|
+
id: existing.id,
|
|
427
|
+
digest: existing.digest,
|
|
428
|
+
artifact_path: existing.artifact_path,
|
|
429
|
+
stage: existing.stage,
|
|
430
|
+
scope_type: existing.scope_type,
|
|
431
|
+
scope_id: existing.scope_id,
|
|
432
|
+
file_digest: existing.file_digest,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
const scope = resolvedScope(db, input);
|
|
436
|
+
const id = `context-${full.digest.slice(0, 24)}`;
|
|
437
|
+
const artifactPath = contextArtifactPath(scope, id);
|
|
438
|
+
const published = writeManagedJson(options.rootDir || process.cwd(), artifactPath, full);
|
|
439
|
+
ops.tx(db, () => {
|
|
440
|
+
db.prepare(
|
|
441
|
+
`INSERT INTO context_envelopes
|
|
442
|
+
(id, stage, scope_type, scope_id, digest, file_digest, payload_json, artifact_path)
|
|
443
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
444
|
+
).run(
|
|
445
|
+
id,
|
|
446
|
+
input.stage || 'project',
|
|
447
|
+
scope.type,
|
|
448
|
+
scope.id,
|
|
449
|
+
full.digest,
|
|
450
|
+
published.digest,
|
|
451
|
+
JSON.stringify(full.envelope),
|
|
452
|
+
artifactPath,
|
|
453
|
+
);
|
|
454
|
+
if (!artifactPath.startsWith('.ultra/.runtime/')) {
|
|
455
|
+
artifactRegistry.recordArtifactInTx(db, {
|
|
456
|
+
id: `artifact-${id}`,
|
|
457
|
+
owner_type: scope.change ? 'change' : (scope.baseline ? 'baseline' : 'project'),
|
|
458
|
+
owner_id: scope.change?.id || scope.baseline?.id || 'project',
|
|
459
|
+
kind: 'context_envelope',
|
|
460
|
+
path: artifactPath,
|
|
461
|
+
content_digest: published.digest,
|
|
462
|
+
source_refs: [
|
|
463
|
+
...(scope.change
|
|
464
|
+
? [{ type: 'change', id: scope.change.id, relation: 'compiled_from' }]
|
|
465
|
+
: []),
|
|
466
|
+
...(scope.task
|
|
467
|
+
? [{ type: 'task', id: scope.task.id, relation: 'compiled_from_task_contract' }]
|
|
468
|
+
: []),
|
|
469
|
+
...(scope.baseline && !scope.change
|
|
470
|
+
? [{ type: 'baseline', id: scope.baseline.id, relation: 'compiled_from' }]
|
|
471
|
+
: []),
|
|
472
|
+
],
|
|
473
|
+
consumer_refs: [{
|
|
474
|
+
type: 'external',
|
|
475
|
+
id: 'ultra-context-consumer',
|
|
476
|
+
relation: 'consumed_by',
|
|
477
|
+
}],
|
|
478
|
+
provenance: {
|
|
479
|
+
writer: 'context-envelope',
|
|
480
|
+
envelope_digest: full.digest,
|
|
481
|
+
stage: input.stage || 'project',
|
|
482
|
+
},
|
|
483
|
+
metadata: {
|
|
484
|
+
envelope_id: id,
|
|
485
|
+
semantic_digest: full.digest,
|
|
486
|
+
},
|
|
487
|
+
}, { rootDir: options.rootDir || process.cwd() });
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
return {
|
|
491
|
+
id,
|
|
492
|
+
digest: full.digest,
|
|
493
|
+
artifact_path: artifactPath,
|
|
494
|
+
stage: input.stage || 'project',
|
|
495
|
+
scope_type: scope.type,
|
|
496
|
+
scope_id: scope.id,
|
|
497
|
+
file_digest: published.digest,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
module.exports = {
|
|
502
|
+
ContextEnvelopeError,
|
|
503
|
+
SUMMARY_LIMIT,
|
|
504
|
+
FULL_LIMIT,
|
|
505
|
+
buildEnvelope,
|
|
506
|
+
persistEnvelope,
|
|
507
|
+
readEnvelope,
|
|
508
|
+
};
|