@hecer/yoke 1.21.1 → 1.23.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +48 -0
- package/README.md +8 -1
- package/TODOS.md +6 -0
- package/bench/analyze-codex-comparison.mjs +90 -17
- package/bench/compare-codex.mjs +159 -36
- package/bench/result-schema.mjs +132 -0
- package/canon/manifest.yaml +1 -1
- package/canon/skills/visual-verification/SKILL.md +25 -2
- package/canon/tools/codex-rtk-hook.mjs +6 -16
- package/dist/agents/pi-telemetry.js +2 -1
- package/dist/agents/process-streams.js +12 -64
- package/dist/agents/provider-selection.js +12 -0
- package/dist/agents/telemetry.js +52 -52
- package/dist/change/inbox.js +8 -3
- package/dist/check/command.js +69 -17
- package/dist/check/delivery.js +121 -0
- package/dist/cli.js +91 -3
- package/dist/code-intelligence/adapters/mcp.js +1 -0
- package/dist/code-intelligence/budgets.js +138 -0
- package/dist/code-intelligence/contracts.js +2 -0
- package/dist/code-intelligence/coordinator.js +159 -85
- package/dist/code-intelligence/evidence.js +87 -34
- package/dist/code-intelligence/index.js +1 -0
- package/dist/code-intelligence/mcp-client.js +25 -6
- package/dist/code-intelligence/mcp-server.js +14 -11
- package/dist/code-intelligence/preflight.js +71 -0
- package/dist/dashboard/analytics.js +5 -3
- package/dist/goals/command.js +183 -53
- package/dist/goals/usage.js +87 -0
- package/dist/loop/cache-isolation.js +36 -0
- package/dist/loop/candidate-cleanup.js +47 -17
- package/dist/loop/candidates.js +17 -11
- package/dist/loop/dispatcher.js +89 -26
- package/dist/loop/failure.js +104 -0
- package/dist/loop/gate-snapshot.js +19 -0
- package/dist/loop/git.js +1 -1
- package/dist/loop/loop.js +124 -70
- package/dist/loop/parallel-adapters.js +57 -6
- package/dist/loop/parallel-command.js +49 -7
- package/dist/loop/proof-retention.js +70 -0
- package/dist/loop/recovery.js +23 -5
- package/dist/loop/reporter.js +22 -5
- package/dist/loop/run-command.js +101 -47
- package/dist/loop/runner.js +6 -5
- package/dist/loop/worker.js +152 -91
- package/dist/observability/history.js +2 -1
- package/dist/observability/invocation.js +42 -0
- package/dist/observability/local-report.js +120 -0
- package/dist/observability/usage.js +19 -0
- package/dist/prd/command.js +20 -7
- package/dist/prd/decompose.js +5 -2
- package/dist/retrofit/config.js +29 -2
- package/dist/retrofit/gitignore.js +12 -0
- package/dist/retrofit/planners/codex.js +20 -20
- package/dist/routing/attempts.js +241 -0
- package/dist/routing/capability.js +13 -9
- package/dist/routing/optimization.js +73 -0
- package/dist/routing/registry.js +7 -1
- package/dist/routing/router.js +282 -127
- package/dist/setup/command.js +8 -2
- package/dist/smoke/command.js +387 -85
- package/dist/update/check.js +1 -1
- package/docs/BENCHMARK-MANIFEST.md +131 -0
- package/docs/CODE-INTELLIGENCE.md +43 -1
- package/docs/CODEX-COMPARISON-2026-09-29.md +15 -0
- package/docs/DELIVERY-JOURNEYS.md +206 -0
- package/docs/ECONOMIC-ROUTING.md +180 -0
- package/docs/GOALS.md +61 -4
- package/docs/RELEASE-VALIDATION-1.22.0.md +115 -0
- package/docs/RELEASE-VALIDATION-1.23.0.md +39 -0
- package/docs/benchmarks/2026-10-04-efficiency/ANALYSE.md +182 -0
- package/docs/benchmarks/2026-10-04-efficiency/compare-help.py +55 -0
- package/docs/benchmarks/2026-10-04-efficiency/manifest.json +125 -0
- package/docs/benchmarks/2026-10-04-efficiency/provenance-analysis.json +90 -0
- package/docs/benchmarks/2026-10-04-efficiency/provenance-design.json +90 -0
- package/docs/benchmarks/2026-10-04-efficiency/provenance-original-report.json +90 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/DEVELOPMENT_ANALYSIS.md +142 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/RESULT.md +21 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/commands.jsonl +26 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/environment.json +31 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/final-yoke-smoke.json +40 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/model-purpose-hints.csv +19 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/observations.jsonl +21 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/observer-command-phases.csv +12 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/roles.csv +5 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/shell-categories.csv +8 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/stories.csv +8 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/summary.json +469 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/yoke-history.jsonl +104 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/yoke-loop-1.log +58 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/yoke-loop-2.log +29 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/yoke-loop-3.log +5 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/yoke-loop-4.log +12 -0
- package/docs/benchmarks/2026-10-04-efficiency/raw/yoke-phases.csv +10 -0
- package/docs/benchmarks/2026-10-04-efficiency/regression-comparison.json +104 -0
- package/docs/parallel-execution.md +37 -9
- package/docs/superpowers/plans/2026-10-04-yoke-1.23-efficiency-prd.json +11 -0
- package/docs/superpowers/plans/2026-10-04-yoke-1.23-efficiency.md +83 -0
- package/docs/superpowers/specs/2026-10-04-yoke-1.23-efficiency-design.md +120 -0
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
|
@@ -3,19 +3,18 @@ import { copyFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { ContextInputSchema, EditApplyInputSchema, EditPreviewInputSchema, ImpactInputSchema, SymbolInputSchema, TraceInputSchema, } from './contracts.js';
|
|
5
5
|
import { createGraftAdapter, createGraphifyAdapter, createSerenaAdapter } from './adapters/index.js';
|
|
6
|
-
import { normalizeEdges, normalizeItems, normalizeSymbols } from './evidence.js';
|
|
7
|
-
import {
|
|
6
|
+
import { normalizeEdges, normalizeItems, normalizeSymbols, normalizeReferenceEdges, serenaSymbolId, shorten } from './evidence.js';
|
|
7
|
+
import { budgetError, fitResponse, measureResponse, responseByteLimit } from './budgets.js';
|
|
8
|
+
import { assertSafePath, assertSnapshotCurrent, createSnapshot, loadSnapshot, workspaceId } from './snapshots.js';
|
|
8
9
|
import { applyEditPlan } from './transactions.js';
|
|
9
10
|
import { createEditPlan, loadEditPlan } from './edit-plans.js';
|
|
10
11
|
function nowMs() { return Date.now(); }
|
|
11
|
-
function compact(value, maxBytes) {
|
|
12
|
-
const encoded = JSON.stringify(value) ?? '';
|
|
13
|
-
if (Buffer.byteLength(encoded) <= maxBytes)
|
|
14
|
-
return value;
|
|
15
|
-
return { truncated: true, excerpt: encoded.slice(0, Math.max(0, maxBytes - 64)) };
|
|
16
|
-
}
|
|
17
12
|
function statusFor(missing, hard = false) { return hard ? 'error' : missing.length ? 'partial' : 'success'; }
|
|
18
13
|
function errorCode(message) {
|
|
14
|
+
if (/deadline|timed out/iu.test(message))
|
|
15
|
+
return 'BACKEND_TIMEOUT';
|
|
16
|
+
if (/budget/iu.test(message))
|
|
17
|
+
return 'BUDGET_EXCEEDED';
|
|
19
18
|
if (/outside|workspace|symbolic/u.test(message))
|
|
20
19
|
return 'WORKSPACE_DENIED';
|
|
21
20
|
if (/stale/u.test(message))
|
|
@@ -34,6 +33,8 @@ function errorCode(message) {
|
|
|
34
33
|
return 'SCOPE_VIOLATION';
|
|
35
34
|
return 'INVALID_ARGUMENT';
|
|
36
35
|
}
|
|
36
|
+
const INPUT_SCHEMAS = { code_context: ContextInputSchema, code_symbol: SymbolInputSchema, code_trace: TraceInputSchema, code_impact: ImpactInputSchema, code_edit_preview: EditPreviewInputSchema, code_edit_apply: EditApplyInputSchema };
|
|
37
|
+
const SYNCHRONOUS_DEADLINE = 'Request deadline exceeded during synchronous processing; retained receipts describe completed operations.';
|
|
37
38
|
function symbolArgs(symbol, fallbackPath = '') {
|
|
38
39
|
if (symbol.startsWith('serena:')) {
|
|
39
40
|
try {
|
|
@@ -52,7 +53,7 @@ export class CodeIntelligenceCoordinator {
|
|
|
52
53
|
adapters;
|
|
53
54
|
constructor(root, options = {}) {
|
|
54
55
|
this.root = root;
|
|
55
|
-
this.workspace_id = workspaceId(root);
|
|
56
|
+
this.workspace_id = options.workspaceId ?? workspaceId(root);
|
|
56
57
|
this.options = options;
|
|
57
58
|
this.adapters = {
|
|
58
59
|
graft: options.adapters?.graft ?? createGraftAdapter(root, options.graft),
|
|
@@ -64,47 +65,75 @@ export class CodeIntelligenceCoordinator {
|
|
|
64
65
|
async dispatch(name, input) {
|
|
65
66
|
const requestId = randomUUID();
|
|
66
67
|
const started = nowMs();
|
|
67
|
-
const suppliedWorkspace = typeof input === 'object' && input !== null && typeof input.workspace_id === 'string' ? input.workspace_id : this.workspace_id;
|
|
68
68
|
let snapshot;
|
|
69
|
+
let limits = { tokenBudget: this.options.limits?.tokenBudget ?? 2400, maxBytes: this.options.limits?.maxBytes ?? 2_000_000 };
|
|
69
70
|
try {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
71
|
+
const schema = INPUT_SCHEMAS[name];
|
|
72
|
+
if (!schema)
|
|
73
|
+
throw new Error(`unknown code-intelligence tool: ${shorten(name, 64)}`);
|
|
74
|
+
const parsed = schema.parse(input);
|
|
75
|
+
if ('token_budget' in parsed)
|
|
76
|
+
limits = { ...limits, tokenBudget: Math.min(limits.tokenBudget, parsed.token_budget) };
|
|
77
|
+
const timeout = Math.min(this.options.limits?.timeoutMs ?? 10000, 'timeout_ms' in parsed ? parsed.timeout_ms : 10000);
|
|
78
|
+
const state = { used: [], missing: [], warnings: [], deadline: started + timeout, limits };
|
|
79
|
+
const minimum = budgetError(this.errorResponse(requestId, this.workspace_id, '0'.repeat(64), started, new Error('budget')));
|
|
80
|
+
if (minimum.metrics.returned_bytes > responseByteLimit(limits))
|
|
81
|
+
return minimum;
|
|
82
|
+
snapshot = this.snapshotFor(parsed);
|
|
83
|
+
state.receiptFits = data => {
|
|
84
|
+
const envelope = {
|
|
85
|
+
...this.errorResponse(requestId, this.workspace_id, snapshot.snapshot_id, started, new Error('budget')),
|
|
86
|
+
status: 'partial', data, error: null, warnings: [SYNCHRONOUS_DEADLINE],
|
|
87
|
+
};
|
|
88
|
+
// Reserve a late-operation warning and the maximum finite latency width
|
|
89
|
+
// before a transaction can mutate. Apply returns this exact public shape.
|
|
90
|
+
envelope.metrics.latency_ms = Number.MAX_SAFE_INTEGER;
|
|
91
|
+
return measureResponse(envelope).metrics.returned_bytes <= responseByteLimit(limits);
|
|
92
|
+
};
|
|
76
93
|
let result;
|
|
77
94
|
switch (name) {
|
|
78
95
|
case 'code_context':
|
|
79
|
-
result = await this.context(
|
|
96
|
+
result = await this.context(parsed, snapshot, state);
|
|
80
97
|
break;
|
|
81
98
|
case 'code_symbol':
|
|
82
|
-
result = await this.symbol(
|
|
99
|
+
result = await this.symbol(parsed, snapshot, state);
|
|
83
100
|
break;
|
|
84
101
|
case 'code_trace':
|
|
85
|
-
result = await this.trace(
|
|
102
|
+
result = await this.trace(parsed, snapshot, state);
|
|
86
103
|
break;
|
|
87
104
|
case 'code_impact':
|
|
88
|
-
result = await this.impact(
|
|
105
|
+
result = await this.impact(parsed, snapshot, state);
|
|
89
106
|
break;
|
|
90
107
|
case 'code_edit_preview':
|
|
91
|
-
result = await this.preview(
|
|
108
|
+
result = await this.preview(parsed, snapshot, state);
|
|
92
109
|
break;
|
|
93
110
|
case 'code_edit_apply':
|
|
94
|
-
result = await this.apply(
|
|
111
|
+
result = await this.apply(parsed, snapshot, state);
|
|
95
112
|
break;
|
|
96
113
|
default: throw new Error(`unknown code-intelligence tool: ${name}`);
|
|
97
114
|
}
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
115
|
+
const read = !name.startsWith('code_edit_');
|
|
116
|
+
if (read)
|
|
117
|
+
result.warnings.push('Backend freshness and exhaustive coverage are unverified; candidates may require source checks.');
|
|
118
|
+
let response = fitResponse({
|
|
101
119
|
schema_version: '0.1.0', request_id: requestId, workspace_id: this.workspace_id, snapshot_id: snapshot.snapshot_id,
|
|
102
|
-
status: statusFor(result.missing
|
|
103
|
-
warnings: result.warnings, metrics: { latency_ms: nowMs() - started, result_tokens:
|
|
104
|
-
};
|
|
120
|
+
status: result.hard ? 'error' : read ? 'partial' : statusFor(result.missing), data: result.data, coverage: this.coverage(result.used, result.missing), provenance: result.provenance,
|
|
121
|
+
warnings: [...new Set(result.warnings)], metrics: { latency_ms: nowMs() - started, result_tokens: 0, token_count_kind: 'estimated', returned_bytes: 0 }, artifact_uri: null, error: null,
|
|
122
|
+
}, limits);
|
|
123
|
+
response.metrics.latency_ms = nowMs() - started;
|
|
124
|
+
response = fitResponse(response, limits);
|
|
125
|
+
// A filesystem operation or normalization cannot be preempted mid-call.
|
|
126
|
+
// Check after reduction too, and never hide a receipt for an applied edit.
|
|
127
|
+
if (nowMs() >= state.deadline && response.status !== 'error' && response.status !== 'blocked') {
|
|
128
|
+
response.status = 'partial';
|
|
129
|
+
response.warnings.push(SYNCHRONOUS_DEADLINE);
|
|
130
|
+
response.metrics.latency_ms = nowMs() - started;
|
|
131
|
+
response = fitResponse(response, limits);
|
|
132
|
+
}
|
|
133
|
+
return response;
|
|
105
134
|
}
|
|
106
135
|
catch (error) {
|
|
107
|
-
return this.errorResponse(requestId, this.workspace_id, snapshot.
|
|
136
|
+
return fitResponse(this.errorResponse(requestId, this.workspace_id, snapshot?.snapshot_id ?? '0'.repeat(64), started, error), limits);
|
|
108
137
|
}
|
|
109
138
|
}
|
|
110
139
|
snapshotFor(input) {
|
|
@@ -112,27 +141,21 @@ export class CodeIntelligenceCoordinator {
|
|
|
112
141
|
if (candidate.workspace_id && candidate.workspace_id !== this.workspace_id)
|
|
113
142
|
throw new Error('workspace is not permitted');
|
|
114
143
|
const snapshot = candidate.snapshot_id ? loadSnapshot(this.root, candidate.snapshot_id) : createSnapshot(this.root, this.options.policy, this.workspace_id);
|
|
144
|
+
if (snapshot.workspace_id !== this.workspace_id)
|
|
145
|
+
throw new Error('snapshot workspace identifier differs from the configured workspace; create a new snapshot');
|
|
115
146
|
if (candidate.snapshot_id)
|
|
116
147
|
assertSnapshotCurrent(this.root, snapshot, this.options.policy);
|
|
117
148
|
return snapshot;
|
|
118
149
|
}
|
|
119
|
-
safeSnapshotId() {
|
|
120
|
-
try {
|
|
121
|
-
return createSnapshot(this.root, this.options.policy, this.workspace_id).snapshot_id;
|
|
122
|
-
}
|
|
123
|
-
catch {
|
|
124
|
-
return '0'.repeat(64);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
150
|
coverage(used, missing) {
|
|
128
151
|
return {
|
|
129
152
|
backends_requested: [...new Set([...used, ...missing])], backends_used: [...new Set(used)], backends_missing: [...new Set(missing)],
|
|
130
|
-
structural: used.includes('graft') ? '
|
|
131
|
-
semantic: used.includes('serena-lsp') ? '
|
|
132
|
-
documents: used.includes('graphify') ? '
|
|
153
|
+
structural: used.includes('graft') ? 'partial' : 'unavailable',
|
|
154
|
+
semantic: used.includes('serena-lsp') ? 'partial' : 'unavailable',
|
|
155
|
+
documents: used.includes('graphify') ? 'partial' : 'unavailable',
|
|
133
156
|
};
|
|
134
157
|
}
|
|
135
|
-
async call(backend, tool, args, timeout, state) {
|
|
158
|
+
async call(backend, tool, args, timeout, state, adapter = this.adapters[backend]) {
|
|
136
159
|
const seen = new Set([...state.used, ...state.missing]);
|
|
137
160
|
const maxBackends = this.options.limits?.maxBackends ?? 3;
|
|
138
161
|
if (!seen.has(backend) && seen.size >= maxBackends) {
|
|
@@ -140,8 +163,17 @@ export class CodeIntelligenceCoordinator {
|
|
|
140
163
|
state.warnings.push(`${backend}: backend budget exceeded`);
|
|
141
164
|
return null;
|
|
142
165
|
}
|
|
166
|
+
let timer;
|
|
143
167
|
try {
|
|
144
|
-
const
|
|
168
|
+
const remaining = Math.min(timeout, state.deadline - nowMs());
|
|
169
|
+
if (remaining <= 0)
|
|
170
|
+
throw new Error('request deadline exceeded; backend was not started');
|
|
171
|
+
const expired = new Promise((_resolve, reject) => {
|
|
172
|
+
timer = setTimeout(() => { reject(new Error('request deadline exceeded')); void adapter.close().catch(() => { }); }, remaining);
|
|
173
|
+
});
|
|
174
|
+
const value = await Promise.race([adapter.call({ tool, arguments: args }, remaining), expired]);
|
|
175
|
+
if (nowMs() >= state.deadline)
|
|
176
|
+
throw new Error('request deadline exceeded; late backend result discarded');
|
|
145
177
|
if (!state.used.includes(backend))
|
|
146
178
|
state.used.push(backend);
|
|
147
179
|
return value;
|
|
@@ -149,35 +181,39 @@ export class CodeIntelligenceCoordinator {
|
|
|
149
181
|
catch (error) {
|
|
150
182
|
if (!state.missing.includes(backend))
|
|
151
183
|
state.missing.push(backend);
|
|
152
|
-
state.warnings.push(`${backend}: ${error.message}`);
|
|
184
|
+
state.warnings.push(`${backend}: ${shorten(error.message, 256)}`);
|
|
153
185
|
return null;
|
|
154
186
|
}
|
|
187
|
+
finally {
|
|
188
|
+
if (timer)
|
|
189
|
+
clearTimeout(timer);
|
|
190
|
+
}
|
|
155
191
|
}
|
|
156
192
|
evidenceOptions(snapshot, backend, maxChars = 12000) {
|
|
157
193
|
return { root: this.root, snapshot, adapter: this.adapters[backend], maxChars };
|
|
158
194
|
}
|
|
159
|
-
async context(input, snapshot) {
|
|
160
|
-
const state = { used: [], missing: [], warnings: [] };
|
|
195
|
+
async context(input, snapshot, state) {
|
|
161
196
|
const provenance = [];
|
|
162
197
|
let items = [];
|
|
163
|
-
const raw = await this.call('graft', 'context', { query: input.query, limit: Math.max(1, Math.floor(
|
|
198
|
+
const raw = await this.call('graft', 'context', { query: input.query, limit: Math.max(1, Math.floor(state.limits.tokenBudget / 300)), ...(input.paths[0] ? { in: input.paths[0] } : {}) }, input.timeout_ms, state);
|
|
164
199
|
if (raw !== null) {
|
|
165
200
|
const normalized = normalizeItems(raw, this.evidenceOptions(snapshot, 'graft'), 'code', 100);
|
|
166
201
|
items.push(...normalized.items);
|
|
167
202
|
provenance.push(...normalized.provenance);
|
|
203
|
+
state.warnings.push(...normalized.warnings);
|
|
168
204
|
}
|
|
169
205
|
if (input.include_docs) {
|
|
170
|
-
const docs = await this.call('graphify', 'context', { question: input.query, depth: 3, token_budget:
|
|
206
|
+
const docs = await this.call('graphify', 'context', { question: input.query, depth: 3, token_budget: state.limits.tokenBudget }, input.timeout_ms, state);
|
|
171
207
|
if (docs !== null) {
|
|
172
208
|
const normalized = normalizeItems(docs, this.evidenceOptions(snapshot, 'graphify'), 'document', 50);
|
|
173
209
|
items.push(...normalized.items);
|
|
174
210
|
provenance.push(...normalized.provenance);
|
|
211
|
+
state.warnings.push(...normalized.warnings);
|
|
175
212
|
}
|
|
176
213
|
}
|
|
177
214
|
return { data: { items, next_cursor: null }, provenance, ...state, warnings: state.warnings };
|
|
178
215
|
}
|
|
179
|
-
async symbol(input, snapshot) {
|
|
180
|
-
const state = { used: [], missing: [], warnings: [] };
|
|
216
|
+
async symbol(input, snapshot, state) {
|
|
181
217
|
const provenance = [];
|
|
182
218
|
let symbols = [];
|
|
183
219
|
let references = [];
|
|
@@ -196,13 +232,15 @@ export class CodeIntelligenceCoordinator {
|
|
|
196
232
|
const normalized = normalizeSymbols(symbol, this.evidenceOptions(snapshot, 'serena-lsp'));
|
|
197
233
|
symbols.push(...normalized.symbols);
|
|
198
234
|
provenance.push(...normalized.provenance);
|
|
235
|
+
state.warnings.push(...normalized.warnings);
|
|
199
236
|
}
|
|
200
237
|
if (input.include.includes('references')) {
|
|
201
238
|
const raw = await this.call('serena-lsp', 'references', reference, input.timeout_ms, state);
|
|
202
239
|
if (raw !== null) {
|
|
203
|
-
const normalized =
|
|
204
|
-
references = normalized.
|
|
240
|
+
const normalized = normalizeSymbols(raw, this.evidenceOptions(snapshot, 'serena-lsp'));
|
|
241
|
+
references = normalized.symbols.map(symbol => ({ symbol_id: input.symbol_id ?? serenaSymbolId(reference.name_path, reference.relative_path), path: symbol.path, line: null, excerpt: shorten(symbol.signature ?? '', 4000), evidence_ids: symbol.evidence_ids }));
|
|
205
242
|
provenance.push(...normalized.provenance);
|
|
243
|
+
state.warnings.push(...normalized.warnings);
|
|
206
244
|
}
|
|
207
245
|
}
|
|
208
246
|
if (input.include.includes('implementations')) {
|
|
@@ -211,45 +249,83 @@ export class CodeIntelligenceCoordinator {
|
|
|
211
249
|
const normalized = normalizeSymbols(raw, this.evidenceOptions(snapshot, 'serena-lsp'));
|
|
212
250
|
symbols.push(...normalized.symbols);
|
|
213
251
|
provenance.push(...normalized.provenance);
|
|
252
|
+
state.warnings.push(...normalized.warnings);
|
|
214
253
|
}
|
|
215
254
|
}
|
|
216
255
|
if (input.include.includes('diagnostics')) {
|
|
217
256
|
const raw = await this.call('serena-lsp', 'diagnostics', { relative_path: reference.relative_path }, input.timeout_ms, state);
|
|
218
257
|
if (raw !== null) {
|
|
219
258
|
const normalized = normalizeItems(raw, this.evidenceOptions(snapshot, 'serena-lsp'), 'code', 100);
|
|
220
|
-
diagnostics = normalized.items.map(item => ({ path: item.path, line: null, severity: 'unknown', message: item.excerpt, evidence_ids: item.evidence_ids }));
|
|
259
|
+
diagnostics = normalized.items.map(item => ({ path: item.path, line: null, severity: 'unknown', message: shorten(item.excerpt, 4000), evidence_ids: item.evidence_ids }));
|
|
221
260
|
provenance.push(...normalized.provenance);
|
|
261
|
+
state.warnings.push(...normalized.warnings);
|
|
222
262
|
}
|
|
223
263
|
}
|
|
224
264
|
return { data: { symbols, references, diagnostics }, provenance, ...state, warnings: state.warnings };
|
|
225
265
|
}
|
|
226
|
-
async trace(input, snapshot) {
|
|
227
|
-
const state = { used: [], missing: [], warnings: [] };
|
|
266
|
+
async trace(input, snapshot, state) {
|
|
228
267
|
const provenance = [];
|
|
229
268
|
let nodes = [];
|
|
230
269
|
let edges = [];
|
|
231
|
-
const
|
|
232
|
-
if (
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
provenance.push(...normalized.provenance);
|
|
237
|
-
}
|
|
238
|
-
if (input.require_resolved || input.relations.some(relation => relation === 'implements' || relation === 'extends')) {
|
|
239
|
-
const rawSemantic = await this.call('serena-lsp', 'references', symbolArgs(input.symbol_id), input.timeout_ms, state);
|
|
240
|
-
if (rawSemantic !== null) {
|
|
241
|
-
const normalized = normalizeEdges(rawSemantic, this.evidenceOptions(snapshot, 'serena-lsp'), 'references');
|
|
270
|
+
const reference = symbolArgs(input.symbol_id);
|
|
271
|
+
if (input.relations.includes('calls') && !input.require_resolved) {
|
|
272
|
+
const raw = await this.call('graft', 'trace', { symbol: reference.name_path, direction: input.direction, depth: input.max_depth }, input.timeout_ms, state);
|
|
273
|
+
if (raw !== null) {
|
|
274
|
+
const normalized = normalizeEdges(raw, this.evidenceOptions(snapshot, 'graft'), 'calls');
|
|
242
275
|
nodes.push(...normalized.nodes);
|
|
243
|
-
edges.push(...normalized.edges);
|
|
244
276
|
provenance.push(...normalized.provenance);
|
|
277
|
+
state.warnings.push(...normalized.warnings);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const supported = input.direction === 'in' ? input.relations.filter((relation) => relation === 'references' || relation === 'implements') : [];
|
|
281
|
+
if (supported.length && !reference.relative_path) {
|
|
282
|
+
const raw = await this.call('serena-lsp', 'symbol', { name_path_pattern: reference.name_path, relative_path: '', depth: 0, include_body: false, max_matches: 2 }, input.timeout_ms, state);
|
|
283
|
+
if (raw !== null) {
|
|
284
|
+
const normalized = normalizeSymbols(raw, this.evidenceOptions(snapshot, 'serena-lsp'));
|
|
285
|
+
provenance.push(...normalized.provenance);
|
|
286
|
+
state.warnings.push(...normalized.warnings);
|
|
287
|
+
if (normalized.symbols.length === 1)
|
|
288
|
+
Object.assign(reference, symbolArgs(normalized.symbols[0].symbol_id));
|
|
289
|
+
else
|
|
290
|
+
state.warnings.push('reference target is unresolved or ambiguous; use a path-bound symbol_id');
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (supported.length && reference.relative_path) {
|
|
294
|
+
let validTarget = false;
|
|
295
|
+
try {
|
|
296
|
+
validTarget = snapshot.files.some(file => file.path === assertSafePath(this.root, reference.relative_path, this.options.policy));
|
|
245
297
|
}
|
|
298
|
+
catch { /* unresolved target is reported below */ }
|
|
299
|
+
if (!validTarget)
|
|
300
|
+
state.warnings.push('reference target is absent from the permitted snapshot');
|
|
301
|
+
else
|
|
302
|
+
for (const relation of supported) {
|
|
303
|
+
const raw = await this.call('serena-lsp', relation === 'implements' ? 'implementations' : 'references', reference, input.timeout_ms, state);
|
|
304
|
+
if (raw === null)
|
|
305
|
+
continue;
|
|
306
|
+
const target = { ...reference, symbol_id: serenaSymbolId(reference.name_path, reference.relative_path) };
|
|
307
|
+
const normalized = normalizeReferenceEdges(raw, this.evidenceOptions(snapshot, 'serena-lsp'), target, relation);
|
|
308
|
+
nodes.unshift(...normalized.nodes);
|
|
309
|
+
edges.push(...normalized.edges);
|
|
310
|
+
provenance.push(...normalized.provenance);
|
|
311
|
+
state.warnings.push(...normalized.warnings);
|
|
312
|
+
}
|
|
313
|
+
if (input.max_depth > 1)
|
|
314
|
+
state.warnings.push('Semantic trace contains one-hop results; deeper traversal is unverified.');
|
|
246
315
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
316
|
+
const unsupported = input.relations.filter(relation => !supported.includes(relation));
|
|
317
|
+
if (unsupported.length)
|
|
318
|
+
state.warnings.push(`No validated ${input.direction} edge contract for: ${unsupported.join(', ')}`);
|
|
319
|
+
const allNodes = [...new Map(nodes.map(node => [node.id, node])).values()];
|
|
320
|
+
nodes = allNodes.slice(0, input.max_nodes);
|
|
321
|
+
const ids = new Set(nodes.map(node => node.id));
|
|
322
|
+
edges = [...new Map(edges.filter(edge => ids.has(edge.from) && ids.has(edge.to)).map(edge => [`${edge.from}\0${edge.to}\0${edge.relation}`, edge])).values()];
|
|
323
|
+
const omitted = allNodes.length - nodes.length;
|
|
324
|
+
if (omitted)
|
|
325
|
+
state.warnings.push(`${omitted} trace nodes omitted by max_nodes; traversal is incomplete.`);
|
|
326
|
+
return { data: { nodes, edges, frontier_remaining: omitted, traversal_complete: false }, provenance, ...state, warnings: state.warnings };
|
|
250
327
|
}
|
|
251
|
-
async impact(input, snapshot) {
|
|
252
|
-
const state = { used: [], missing: [], warnings: [] };
|
|
328
|
+
async impact(input, snapshot, state) {
|
|
253
329
|
const provenance = [];
|
|
254
330
|
const structural = [];
|
|
255
331
|
const semantic = [];
|
|
@@ -287,7 +363,7 @@ export class CodeIntelligenceCoordinator {
|
|
|
287
363
|
}
|
|
288
364
|
}
|
|
289
365
|
if (input.include_docs) {
|
|
290
|
-
const docs = await this.call('graphify', 'context', { question: `architecture impact ${JSON.stringify(targets)}`, depth: input.max_depth, token_budget:
|
|
366
|
+
const docs = await this.call('graphify', 'context', { question: `architecture impact ${JSON.stringify(targets)}`, depth: input.max_depth, token_budget: state.limits.tokenBudget }, input.timeout_ms, state);
|
|
291
367
|
if (docs !== null) {
|
|
292
368
|
const normalized = normalizeItems(docs, this.evidenceOptions(snapshot, 'graphify'), 'document', 50);
|
|
293
369
|
documents.push(...normalized.items);
|
|
@@ -297,10 +373,9 @@ export class CodeIntelligenceCoordinator {
|
|
|
297
373
|
const suggested = [...new Set(snapshot.files.map(file => file.path).filter(path => /(^|\/)(test|tests|__tests__|spec)(\/|\.)/u.test(path)))].slice(0, 20);
|
|
298
374
|
return { data: { semantic_findings: semantic, structural_candidates: structural, documents, suggested_test_paths: suggested, unresolved }, provenance, ...state, warnings: state.warnings };
|
|
299
375
|
}
|
|
300
|
-
async preview(input, snapshot) {
|
|
376
|
+
async preview(input, snapshot, state) {
|
|
301
377
|
if (this.options.mode !== 'active')
|
|
302
378
|
throw new Error('code edits require code-intelligence mode=active');
|
|
303
|
-
const state = { used: [], missing: [], warnings: [] };
|
|
304
379
|
const provenance = [];
|
|
305
380
|
const sandbox = join(this.root, '.yoke', 'code-intelligence', 'worktrees', `preview-${randomUUID()}`);
|
|
306
381
|
mkdirSync(sandbox, { recursive: true });
|
|
@@ -317,15 +392,8 @@ export class CodeIntelligenceCoordinator {
|
|
|
317
392
|
const reference = symbolArgs(operation.symbol_id);
|
|
318
393
|
const args = 'new_name' in operation ? { ...reference, new_name: operation.new_name } : operation.kind === 'safe_delete' ? { name_path_pattern: reference.name_path, relative_path: reference.relative_path } : { ...reference, body: 'content' in operation ? operation.content : '' };
|
|
319
394
|
const tool = operation.kind === 'safe_delete' ? 'safe_delete' : operation.kind;
|
|
320
|
-
|
|
321
|
-
await adapter.call({ tool, arguments: args }, input.timeout_ms);
|
|
322
|
-
state.used.push('serena-lsp');
|
|
323
|
-
}
|
|
324
|
-
catch (error) {
|
|
325
|
-
state.missing.push('serena-lsp');
|
|
326
|
-
state.warnings.push(`serena-lsp: ${error.message}`);
|
|
395
|
+
if (await this.call('serena-lsp', tool, args, input.timeout_ms, state, adapter) === null)
|
|
327
396
|
break;
|
|
328
|
-
}
|
|
329
397
|
}
|
|
330
398
|
}
|
|
331
399
|
finally {
|
|
@@ -350,22 +418,28 @@ export class CodeIntelligenceCoordinator {
|
|
|
350
418
|
const plan = createEditPlan(this.root, snapshot, sandbox, input.operations, state.missing.length ? 'incomplete' : 'passed', input.validation_profile, [...changed], diff);
|
|
351
419
|
return { data: plan, provenance, ...state, warnings: state.warnings };
|
|
352
420
|
}
|
|
353
|
-
async apply(input, snapshot) {
|
|
421
|
+
async apply(input, snapshot, state) {
|
|
354
422
|
if (this.options.mode !== 'active')
|
|
355
423
|
throw new Error('code edits require code-intelligence mode=active');
|
|
356
|
-
|
|
424
|
+
if (nowMs() >= state.deadline)
|
|
425
|
+
throw new Error('request deadline exceeded before apply');
|
|
357
426
|
const plan = loadEditPlan(this.root, input.plan_id);
|
|
358
427
|
if (plan.workspace_id !== this.workspace_id || plan.snapshot_id !== input.expected_snapshot_id)
|
|
359
428
|
throw new Error('edit plan scope does not match workspace or expected snapshot');
|
|
360
429
|
const expected = loadSnapshot(this.root, input.expected_snapshot_id);
|
|
361
430
|
assertSnapshotCurrent(this.root, expected, this.options.policy);
|
|
431
|
+
const receipt = { plan_id: plan.plan_id, transaction_id: `tx-${'0'.repeat(36)}`, changed_paths: plan.changed_paths, new_snapshot_id: expected.snapshot_id, validation: plan.validation, committed: false, merged: false };
|
|
432
|
+
if (!state.receiptFits?.(receipt))
|
|
433
|
+
throw new Error('Budget cannot represent the required apply receipt; no transaction was started');
|
|
434
|
+
if (nowMs() >= state.deadline)
|
|
435
|
+
throw new Error('request deadline exceeded before apply');
|
|
362
436
|
const result = applyEditPlan(this.root, plan, expected, input.idempotency_key);
|
|
363
437
|
return { data: { plan_id: result.plan_id, transaction_id: result.transaction_id, changed_paths: result.changed_paths, new_snapshot_id: result.new_snapshot_id, validation: result.validation, committed: false, merged: false }, provenance: [], ...state, warnings: state.warnings };
|
|
364
438
|
}
|
|
365
439
|
errorResponse(requestId, workspace, snapshotId, started, error) {
|
|
366
440
|
const code = errorCode(error.message);
|
|
367
441
|
const blocked = ['WORKSPACE_DENIED', 'SNAPSHOT_STALE', 'APPROVAL_REQUIRED', 'PLAN_EXPIRED', 'CAPABILITY_UNAVAILABLE', 'SCOPE_VIOLATION', 'WRITE_CONFLICT'].includes(code);
|
|
368
|
-
return { schema_version: '0.1.0', request_id: requestId, workspace_id: workspace, snapshot_id: snapshotId, status: blocked ? 'blocked' : 'error', data: null, coverage: { backends_requested: [], backends_used: [], backends_missing: [], structural: 'unavailable', semantic: 'unavailable', documents: 'unavailable' }, provenance: [], warnings: [], metrics: { latency_ms: nowMs() - started, result_tokens: 0, token_count_kind: '
|
|
442
|
+
return measureResponse({ schema_version: '0.1.0', request_id: requestId, workspace_id: workspace, snapshot_id: snapshotId, status: blocked ? 'blocked' : 'error', data: null, coverage: { backends_requested: [], backends_used: [], backends_missing: [], structural: 'unavailable', semantic: 'unavailable', documents: 'unavailable' }, provenance: [], warnings: [], metrics: { latency_ms: nowMs() - started, result_tokens: 0, token_count_kind: 'estimated', returned_bytes: 0 }, artifact_uri: null, error: { code, message: shorten(error.message, 256) } });
|
|
369
443
|
}
|
|
370
444
|
}
|
|
371
445
|
export function createCodeIntelligenceCoordinator(root, options) { return new CodeIntelligenceCoordinator(root, options); }
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { assertSafePath } from './snapshots.js';
|
|
3
|
+
/** Truncate only between Unicode code points, including at normalizer limits. */
|
|
4
|
+
export function shorten(value, maxChars) {
|
|
5
|
+
const clipped = value.slice(0, Math.max(0, maxChars));
|
|
6
|
+
return /[\uD800-\uDBFF]$/u.test(clipped) ? clipped.slice(0, -1) : clipped;
|
|
7
|
+
}
|
|
3
8
|
function hash(value) { return createHash('sha256').update(value).digest('hex'); }
|
|
4
9
|
function textOf(raw) {
|
|
5
10
|
if (typeof raw === 'string')
|
|
@@ -22,32 +27,46 @@ function locate(text, root) {
|
|
|
22
27
|
return { path: null, line: null };
|
|
23
28
|
}
|
|
24
29
|
}
|
|
25
|
-
export function makeProvenance(options, text, resolution = '
|
|
30
|
+
export function makeProvenance(options, text, resolution = 'unresolved') {
|
|
26
31
|
const location = locate(text, options.root);
|
|
27
|
-
const file = location.path ? options.snapshot.files.find(item => item.path === location.path) : undefined;
|
|
28
32
|
return {
|
|
29
|
-
backend: options.adapter.name, version: options.adapter.version, source_path: location.path, content_hash:
|
|
30
|
-
|
|
33
|
+
backend: options.adapter.name, version: options.adapter.version, source_path: location.path, content_hash: null,
|
|
34
|
+
// A current workspace snapshot does not prove that a backend index read it.
|
|
35
|
+
// None of the supported backend wire contracts supplies a validated index
|
|
36
|
+
// snapshot binding. Do not trust raw `freshness` or attach a newer file hash.
|
|
37
|
+
byte_range: null, origin: options.origin ?? (options.adapter.semantic ? 'lsp' : 'parser'), resolution, freshness: 'unknown',
|
|
31
38
|
};
|
|
32
39
|
}
|
|
33
40
|
export function evidenceId(provenance, excerpt) {
|
|
34
|
-
|
|
41
|
+
const { evidence_id: _id, ...fields } = provenance;
|
|
42
|
+
return `evidence-${hash(JSON.stringify({ provenance: fields, excerpt })).slice(0, 24)}`;
|
|
43
|
+
}
|
|
44
|
+
function candidates(raw) {
|
|
45
|
+
if (Array.isArray(raw))
|
|
46
|
+
return raw;
|
|
47
|
+
if (raw && typeof raw === 'object' && Array.isArray(raw.results))
|
|
48
|
+
return raw.results;
|
|
49
|
+
return null;
|
|
35
50
|
}
|
|
36
51
|
export function normalizeItems(raw, options, kind = 'code', limit = 100) {
|
|
37
|
-
const
|
|
38
|
-
const chunks = Array.isArray(raw) ? raw : (raw && typeof raw === 'object' && Array.isArray(raw.results) ? raw.results : [raw]);
|
|
52
|
+
const chunks = candidates(raw) ?? (raw === null || raw === undefined || raw === '' ? [] : [raw]);
|
|
39
53
|
const provenance = [];
|
|
40
54
|
const items = [];
|
|
41
|
-
const
|
|
55
|
+
const warnings = [];
|
|
56
|
+
const maxChars = Math.min(12000, options.maxChars ?? 12000);
|
|
57
|
+
if (chunks.length > limit)
|
|
58
|
+
warnings.push('backend result list truncated');
|
|
42
59
|
for (const candidate of chunks.slice(0, limit)) {
|
|
43
|
-
const
|
|
60
|
+
const text = textOf(candidate);
|
|
61
|
+
const excerpt = shorten(text, maxChars);
|
|
62
|
+
if (excerpt !== text)
|
|
63
|
+
warnings.push('backend result excerpt truncated');
|
|
44
64
|
if (!excerpt)
|
|
45
65
|
continue;
|
|
46
|
-
const p = makeProvenance(options, excerpt
|
|
47
|
-
if (candidate && typeof candidate === 'object' && typeof candidate.relative_path === 'string') {
|
|
66
|
+
const p = makeProvenance(options, excerpt);
|
|
67
|
+
if (candidate && typeof candidate === 'object' && 'relative_path' in candidate && typeof candidate.relative_path === 'string') {
|
|
48
68
|
try {
|
|
49
69
|
p.source_path = assertSafePath(options.root, candidate.relative_path);
|
|
50
|
-
p.content_hash = options.snapshot.files.find(file => file.path === p.source_path)?.hash ?? null;
|
|
51
70
|
}
|
|
52
71
|
catch {
|
|
53
72
|
p.source_path = null;
|
|
@@ -56,33 +75,67 @@ export function normalizeItems(raw, options, kind = 'code', limit = 100) {
|
|
|
56
75
|
}
|
|
57
76
|
}
|
|
58
77
|
const id = evidenceId(p, excerpt);
|
|
78
|
+
p.evidence_id = id;
|
|
59
79
|
provenance.push(p);
|
|
60
80
|
items.push({ item_id: `item-${hash(excerpt).slice(0, 24)}`, kind, path: p.source_path, excerpt, evidence_ids: [id], rank: items.length });
|
|
61
81
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
provenance.push(p);
|
|
67
|
-
items.push({ item_id: `item-${hash(excerpt).slice(0, 24)}`, kind, path: p.source_path, excerpt, evidence_ids: [id], rank: 0 });
|
|
68
|
-
}
|
|
69
|
-
return { items, provenance };
|
|
82
|
+
return { items, provenance, warnings: [...new Set(warnings)] };
|
|
83
|
+
}
|
|
84
|
+
export function serenaSymbolId(namePath, relativePath) {
|
|
85
|
+
return `serena:${encodeURIComponent(JSON.stringify({ name_path: namePath, relative_path: relativePath }))}`;
|
|
70
86
|
}
|
|
71
87
|
export function normalizeSymbols(raw, options) {
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
88
|
+
const list = candidates(raw);
|
|
89
|
+
const symbols = [];
|
|
90
|
+
const provenance = [];
|
|
91
|
+
const warnings = [];
|
|
92
|
+
if (!list) {
|
|
93
|
+
const normalized = normalizeItems(raw, options, 'symbol');
|
|
94
|
+
return { symbols, references: [], diagnostics: [], provenance: normalized.provenance, warnings: [...normalized.warnings, 'unrecognized semantic symbol response'] };
|
|
95
|
+
}
|
|
96
|
+
if (list.length > 100)
|
|
97
|
+
warnings.push('semantic symbol result list truncated');
|
|
98
|
+
for (const value of list.slice(0, 100)) {
|
|
99
|
+
const normalized = normalizeItems([value], options, 'symbol', 1);
|
|
100
|
+
warnings.push(...normalized.warnings);
|
|
101
|
+
const item = normalized.items[0];
|
|
102
|
+
const p = normalized.provenance[0];
|
|
103
|
+
if (!item || !p)
|
|
104
|
+
continue;
|
|
105
|
+
const candidate = value && typeof value === 'object' ? value : {};
|
|
106
|
+
const namePath = typeof candidate.name_path === 'string' ? candidate.name_path.trim() : '';
|
|
107
|
+
if (!options.adapter.semantic || !namePath || !item.path || !options.snapshot.files.some(file => file.path === item.path)) {
|
|
108
|
+
provenance.push(p);
|
|
109
|
+
warnings.push('unresolved semantic symbol record');
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
p.resolution = 'resolved';
|
|
113
|
+
p.evidence_id = evidenceId(p, item.excerpt);
|
|
114
|
+
provenance.push(p);
|
|
115
|
+
symbols.push({ symbol_id: serenaSymbolId(namePath, item.path), name: typeof candidate.name === 'string' && candidate.name ? candidate.name : namePath.split('/').at(-1) || namePath,
|
|
116
|
+
path: item.path, kind: typeof candidate.kind === 'string' ? candidate.kind : 'unknown', signature: shorten(item.excerpt, 500), evidence_ids: [p.evidence_id] });
|
|
117
|
+
}
|
|
118
|
+
return { symbols, references: [], diagnostics: [], provenance, warnings: [...new Set(warnings)] };
|
|
82
119
|
}
|
|
83
|
-
|
|
120
|
+
/** Graft prose/search hits are candidates, not an ordered graph path. */
|
|
121
|
+
export function normalizeEdges(raw, options, _relation) {
|
|
84
122
|
const normalized = normalizeItems(raw, options, 'code');
|
|
85
|
-
const nodes = normalized.items.map(item => ({ id: item.item_id, label: item.excerpt
|
|
86
|
-
|
|
87
|
-
|
|
123
|
+
const nodes = normalized.items.map(item => ({ id: item.item_id, label: shorten(item.excerpt, 160), path: item.path, evidence_ids: item.evidence_ids }));
|
|
124
|
+
return { nodes, edges: [], provenance: normalized.provenance, warnings: [...normalized.warnings, 'structural trace response has no validated edge contract'] };
|
|
125
|
+
}
|
|
126
|
+
export function normalizeReferenceEdges(raw, options, target, relation = 'references') {
|
|
127
|
+
const normalized = normalizeSymbols(raw, options);
|
|
128
|
+
const nodes = normalized.symbols.map(symbol => ({ id: symbol.symbol_id, label: symbol.name, path: symbol.path, evidence_ids: symbol.evidence_ids }));
|
|
129
|
+
let targetPath;
|
|
130
|
+
try {
|
|
131
|
+
targetPath = assertSafePath(options.root, target.relative_path);
|
|
132
|
+
if (!options.snapshot.files.some(file => file.path === targetPath))
|
|
133
|
+
throw new Error('target is absent from snapshot');
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return { nodes, edges: [], provenance: normalized.provenance, warnings: [...normalized.warnings, 'reference target is unresolved'] };
|
|
137
|
+
}
|
|
138
|
+
const targetNode = { id: target.symbol_id, label: target.name_path, path: targetPath, evidence_ids: [] };
|
|
139
|
+
const edges = nodes.map(node => ({ from: node.id, to: targetNode.id, relation, evidence_ids: node.evidence_ids }));
|
|
140
|
+
return { nodes: [targetNode, ...nodes], edges, provenance: normalized.provenance, warnings: normalized.warnings };
|
|
88
141
|
}
|