@hecer/yoke 1.20.0 → 1.22.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 +46 -0
- package/README.md +6 -1
- 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/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 +86 -8
- package/dist/code-intelligence/budgets.js +138 -0
- package/dist/code-intelligence/contracts.js +2 -0
- package/dist/code-intelligence/coordinator.js +156 -84
- package/dist/code-intelligence/evidence.js +87 -34
- package/dist/code-intelligence/mcp-client.js +10 -2
- package/dist/code-intelligence/mcp-server.js +10 -10
- package/dist/dashboard/analytics.js +5 -3
- package/dist/goals/command.js +183 -53
- package/dist/goals/usage.js +87 -0
- package/dist/loop/candidate-cleanup.js +47 -17
- package/dist/loop/candidates.js +17 -11
- package/dist/loop/cleanup.js +100 -0
- 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 +100 -66
- package/dist/loop/parallel-adapters.js +22 -4
- package/dist/loop/parallel-command.js +32 -5
- package/dist/loop/recovery.js +23 -5
- package/dist/loop/reporter.js +21 -4
- package/dist/loop/run-command.js +102 -48
- package/dist/loop/runner.js +5 -4
- package/dist/loop/worker.js +145 -91
- package/dist/observability/history.js +1 -1
- package/dist/observability/invocation.js +42 -0
- package/dist/observability/usage.js +17 -0
- package/dist/prd/command.js +20 -7
- package/dist/prd/decompose.js +5 -2
- package/dist/retrofit/command.js +22 -0
- package/dist/retrofit/config.js +26 -1
- package/dist/retrofit/gitignore.js +2 -0
- package/dist/retrofit/planners/claude.js +4 -4
- package/dist/retrofit/wsl.js +23 -3
- 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 +280 -127
- package/dist/setup/command.js +54 -6
- package/dist/smoke/command.js +302 -74
- 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 +199 -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/parallel-execution.md +37 -9
- 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 {
|
|
@@ -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) {
|
|
@@ -116,23 +145,15 @@ export class CodeIntelligenceCoordinator {
|
|
|
116
145
|
assertSnapshotCurrent(this.root, snapshot, this.options.policy);
|
|
117
146
|
return snapshot;
|
|
118
147
|
}
|
|
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
148
|
coverage(used, missing) {
|
|
128
149
|
return {
|
|
129
150
|
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') ? '
|
|
151
|
+
structural: used.includes('graft') ? 'partial' : 'unavailable',
|
|
152
|
+
semantic: used.includes('serena-lsp') ? 'partial' : 'unavailable',
|
|
153
|
+
documents: used.includes('graphify') ? 'partial' : 'unavailable',
|
|
133
154
|
};
|
|
134
155
|
}
|
|
135
|
-
async call(backend, tool, args, timeout, state) {
|
|
156
|
+
async call(backend, tool, args, timeout, state, adapter = this.adapters[backend]) {
|
|
136
157
|
const seen = new Set([...state.used, ...state.missing]);
|
|
137
158
|
const maxBackends = this.options.limits?.maxBackends ?? 3;
|
|
138
159
|
if (!seen.has(backend) && seen.size >= maxBackends) {
|
|
@@ -140,8 +161,17 @@ export class CodeIntelligenceCoordinator {
|
|
|
140
161
|
state.warnings.push(`${backend}: backend budget exceeded`);
|
|
141
162
|
return null;
|
|
142
163
|
}
|
|
164
|
+
let timer;
|
|
143
165
|
try {
|
|
144
|
-
const
|
|
166
|
+
const remaining = Math.min(timeout, state.deadline - nowMs());
|
|
167
|
+
if (remaining <= 0)
|
|
168
|
+
throw new Error('request deadline exceeded; backend was not started');
|
|
169
|
+
const expired = new Promise((_resolve, reject) => {
|
|
170
|
+
timer = setTimeout(() => { reject(new Error('request deadline exceeded')); void adapter.close().catch(() => { }); }, remaining);
|
|
171
|
+
});
|
|
172
|
+
const value = await Promise.race([adapter.call({ tool, arguments: args }, remaining), expired]);
|
|
173
|
+
if (nowMs() >= state.deadline)
|
|
174
|
+
throw new Error('request deadline exceeded; late backend result discarded');
|
|
145
175
|
if (!state.used.includes(backend))
|
|
146
176
|
state.used.push(backend);
|
|
147
177
|
return value;
|
|
@@ -149,35 +179,39 @@ export class CodeIntelligenceCoordinator {
|
|
|
149
179
|
catch (error) {
|
|
150
180
|
if (!state.missing.includes(backend))
|
|
151
181
|
state.missing.push(backend);
|
|
152
|
-
state.warnings.push(`${backend}: ${error.message}`);
|
|
182
|
+
state.warnings.push(`${backend}: ${shorten(error.message, 256)}`);
|
|
153
183
|
return null;
|
|
154
184
|
}
|
|
185
|
+
finally {
|
|
186
|
+
if (timer)
|
|
187
|
+
clearTimeout(timer);
|
|
188
|
+
}
|
|
155
189
|
}
|
|
156
190
|
evidenceOptions(snapshot, backend, maxChars = 12000) {
|
|
157
191
|
return { root: this.root, snapshot, adapter: this.adapters[backend], maxChars };
|
|
158
192
|
}
|
|
159
|
-
async context(input, snapshot) {
|
|
160
|
-
const state = { used: [], missing: [], warnings: [] };
|
|
193
|
+
async context(input, snapshot, state) {
|
|
161
194
|
const provenance = [];
|
|
162
195
|
let items = [];
|
|
163
|
-
const raw = await this.call('graft', 'context', { query: input.query, limit: Math.max(1, Math.floor(
|
|
196
|
+
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
197
|
if (raw !== null) {
|
|
165
198
|
const normalized = normalizeItems(raw, this.evidenceOptions(snapshot, 'graft'), 'code', 100);
|
|
166
199
|
items.push(...normalized.items);
|
|
167
200
|
provenance.push(...normalized.provenance);
|
|
201
|
+
state.warnings.push(...normalized.warnings);
|
|
168
202
|
}
|
|
169
203
|
if (input.include_docs) {
|
|
170
|
-
const docs = await this.call('graphify', 'context', { question: input.query, depth: 3, token_budget:
|
|
204
|
+
const docs = await this.call('graphify', 'context', { question: input.query, depth: 3, token_budget: state.limits.tokenBudget }, input.timeout_ms, state);
|
|
171
205
|
if (docs !== null) {
|
|
172
206
|
const normalized = normalizeItems(docs, this.evidenceOptions(snapshot, 'graphify'), 'document', 50);
|
|
173
207
|
items.push(...normalized.items);
|
|
174
208
|
provenance.push(...normalized.provenance);
|
|
209
|
+
state.warnings.push(...normalized.warnings);
|
|
175
210
|
}
|
|
176
211
|
}
|
|
177
212
|
return { data: { items, next_cursor: null }, provenance, ...state, warnings: state.warnings };
|
|
178
213
|
}
|
|
179
|
-
async symbol(input, snapshot) {
|
|
180
|
-
const state = { used: [], missing: [], warnings: [] };
|
|
214
|
+
async symbol(input, snapshot, state) {
|
|
181
215
|
const provenance = [];
|
|
182
216
|
let symbols = [];
|
|
183
217
|
let references = [];
|
|
@@ -196,13 +230,15 @@ export class CodeIntelligenceCoordinator {
|
|
|
196
230
|
const normalized = normalizeSymbols(symbol, this.evidenceOptions(snapshot, 'serena-lsp'));
|
|
197
231
|
symbols.push(...normalized.symbols);
|
|
198
232
|
provenance.push(...normalized.provenance);
|
|
233
|
+
state.warnings.push(...normalized.warnings);
|
|
199
234
|
}
|
|
200
235
|
if (input.include.includes('references')) {
|
|
201
236
|
const raw = await this.call('serena-lsp', 'references', reference, input.timeout_ms, state);
|
|
202
237
|
if (raw !== null) {
|
|
203
|
-
const normalized =
|
|
204
|
-
references = normalized.
|
|
238
|
+
const normalized = normalizeSymbols(raw, this.evidenceOptions(snapshot, 'serena-lsp'));
|
|
239
|
+
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
240
|
provenance.push(...normalized.provenance);
|
|
241
|
+
state.warnings.push(...normalized.warnings);
|
|
206
242
|
}
|
|
207
243
|
}
|
|
208
244
|
if (input.include.includes('implementations')) {
|
|
@@ -211,45 +247,83 @@ export class CodeIntelligenceCoordinator {
|
|
|
211
247
|
const normalized = normalizeSymbols(raw, this.evidenceOptions(snapshot, 'serena-lsp'));
|
|
212
248
|
symbols.push(...normalized.symbols);
|
|
213
249
|
provenance.push(...normalized.provenance);
|
|
250
|
+
state.warnings.push(...normalized.warnings);
|
|
214
251
|
}
|
|
215
252
|
}
|
|
216
253
|
if (input.include.includes('diagnostics')) {
|
|
217
254
|
const raw = await this.call('serena-lsp', 'diagnostics', { relative_path: reference.relative_path }, input.timeout_ms, state);
|
|
218
255
|
if (raw !== null) {
|
|
219
256
|
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 }));
|
|
257
|
+
diagnostics = normalized.items.map(item => ({ path: item.path, line: null, severity: 'unknown', message: shorten(item.excerpt, 4000), evidence_ids: item.evidence_ids }));
|
|
221
258
|
provenance.push(...normalized.provenance);
|
|
259
|
+
state.warnings.push(...normalized.warnings);
|
|
222
260
|
}
|
|
223
261
|
}
|
|
224
262
|
return { data: { symbols, references, diagnostics }, provenance, ...state, warnings: state.warnings };
|
|
225
263
|
}
|
|
226
|
-
async trace(input, snapshot) {
|
|
227
|
-
const state = { used: [], missing: [], warnings: [] };
|
|
264
|
+
async trace(input, snapshot, state) {
|
|
228
265
|
const provenance = [];
|
|
229
266
|
let nodes = [];
|
|
230
267
|
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');
|
|
268
|
+
const reference = symbolArgs(input.symbol_id);
|
|
269
|
+
if (input.relations.includes('calls') && !input.require_resolved) {
|
|
270
|
+
const raw = await this.call('graft', 'trace', { symbol: reference.name_path, direction: input.direction, depth: input.max_depth }, input.timeout_ms, state);
|
|
271
|
+
if (raw !== null) {
|
|
272
|
+
const normalized = normalizeEdges(raw, this.evidenceOptions(snapshot, 'graft'), 'calls');
|
|
242
273
|
nodes.push(...normalized.nodes);
|
|
243
|
-
edges.push(...normalized.edges);
|
|
244
274
|
provenance.push(...normalized.provenance);
|
|
275
|
+
state.warnings.push(...normalized.warnings);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const supported = input.direction === 'in' ? input.relations.filter((relation) => relation === 'references' || relation === 'implements') : [];
|
|
279
|
+
if (supported.length && !reference.relative_path) {
|
|
280
|
+
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);
|
|
281
|
+
if (raw !== null) {
|
|
282
|
+
const normalized = normalizeSymbols(raw, this.evidenceOptions(snapshot, 'serena-lsp'));
|
|
283
|
+
provenance.push(...normalized.provenance);
|
|
284
|
+
state.warnings.push(...normalized.warnings);
|
|
285
|
+
if (normalized.symbols.length === 1)
|
|
286
|
+
Object.assign(reference, symbolArgs(normalized.symbols[0].symbol_id));
|
|
287
|
+
else
|
|
288
|
+
state.warnings.push('reference target is unresolved or ambiguous; use a path-bound symbol_id');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (supported.length && reference.relative_path) {
|
|
292
|
+
let validTarget = false;
|
|
293
|
+
try {
|
|
294
|
+
validTarget = snapshot.files.some(file => file.path === assertSafePath(this.root, reference.relative_path, this.options.policy));
|
|
245
295
|
}
|
|
296
|
+
catch { /* unresolved target is reported below */ }
|
|
297
|
+
if (!validTarget)
|
|
298
|
+
state.warnings.push('reference target is absent from the permitted snapshot');
|
|
299
|
+
else
|
|
300
|
+
for (const relation of supported) {
|
|
301
|
+
const raw = await this.call('serena-lsp', relation === 'implements' ? 'implementations' : 'references', reference, input.timeout_ms, state);
|
|
302
|
+
if (raw === null)
|
|
303
|
+
continue;
|
|
304
|
+
const target = { ...reference, symbol_id: serenaSymbolId(reference.name_path, reference.relative_path) };
|
|
305
|
+
const normalized = normalizeReferenceEdges(raw, this.evidenceOptions(snapshot, 'serena-lsp'), target, relation);
|
|
306
|
+
nodes.unshift(...normalized.nodes);
|
|
307
|
+
edges.push(...normalized.edges);
|
|
308
|
+
provenance.push(...normalized.provenance);
|
|
309
|
+
state.warnings.push(...normalized.warnings);
|
|
310
|
+
}
|
|
311
|
+
if (input.max_depth > 1)
|
|
312
|
+
state.warnings.push('Semantic trace contains one-hop results; deeper traversal is unverified.');
|
|
246
313
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
314
|
+
const unsupported = input.relations.filter(relation => !supported.includes(relation));
|
|
315
|
+
if (unsupported.length)
|
|
316
|
+
state.warnings.push(`No validated ${input.direction} edge contract for: ${unsupported.join(', ')}`);
|
|
317
|
+
const allNodes = [...new Map(nodes.map(node => [node.id, node])).values()];
|
|
318
|
+
nodes = allNodes.slice(0, input.max_nodes);
|
|
319
|
+
const ids = new Set(nodes.map(node => node.id));
|
|
320
|
+
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()];
|
|
321
|
+
const omitted = allNodes.length - nodes.length;
|
|
322
|
+
if (omitted)
|
|
323
|
+
state.warnings.push(`${omitted} trace nodes omitted by max_nodes; traversal is incomplete.`);
|
|
324
|
+
return { data: { nodes, edges, frontier_remaining: omitted, traversal_complete: false }, provenance, ...state, warnings: state.warnings };
|
|
250
325
|
}
|
|
251
|
-
async impact(input, snapshot) {
|
|
252
|
-
const state = { used: [], missing: [], warnings: [] };
|
|
326
|
+
async impact(input, snapshot, state) {
|
|
253
327
|
const provenance = [];
|
|
254
328
|
const structural = [];
|
|
255
329
|
const semantic = [];
|
|
@@ -287,7 +361,7 @@ export class CodeIntelligenceCoordinator {
|
|
|
287
361
|
}
|
|
288
362
|
}
|
|
289
363
|
if (input.include_docs) {
|
|
290
|
-
const docs = await this.call('graphify', 'context', { question: `architecture impact ${JSON.stringify(targets)}`, depth: input.max_depth, token_budget:
|
|
364
|
+
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
365
|
if (docs !== null) {
|
|
292
366
|
const normalized = normalizeItems(docs, this.evidenceOptions(snapshot, 'graphify'), 'document', 50);
|
|
293
367
|
documents.push(...normalized.items);
|
|
@@ -297,10 +371,9 @@ export class CodeIntelligenceCoordinator {
|
|
|
297
371
|
const suggested = [...new Set(snapshot.files.map(file => file.path).filter(path => /(^|\/)(test|tests|__tests__|spec)(\/|\.)/u.test(path)))].slice(0, 20);
|
|
298
372
|
return { data: { semantic_findings: semantic, structural_candidates: structural, documents, suggested_test_paths: suggested, unresolved }, provenance, ...state, warnings: state.warnings };
|
|
299
373
|
}
|
|
300
|
-
async preview(input, snapshot) {
|
|
374
|
+
async preview(input, snapshot, state) {
|
|
301
375
|
if (this.options.mode !== 'active')
|
|
302
376
|
throw new Error('code edits require code-intelligence mode=active');
|
|
303
|
-
const state = { used: [], missing: [], warnings: [] };
|
|
304
377
|
const provenance = [];
|
|
305
378
|
const sandbox = join(this.root, '.yoke', 'code-intelligence', 'worktrees', `preview-${randomUUID()}`);
|
|
306
379
|
mkdirSync(sandbox, { recursive: true });
|
|
@@ -317,15 +390,8 @@ export class CodeIntelligenceCoordinator {
|
|
|
317
390
|
const reference = symbolArgs(operation.symbol_id);
|
|
318
391
|
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
392
|
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}`);
|
|
393
|
+
if (await this.call('serena-lsp', tool, args, input.timeout_ms, state, adapter) === null)
|
|
327
394
|
break;
|
|
328
|
-
}
|
|
329
395
|
}
|
|
330
396
|
}
|
|
331
397
|
finally {
|
|
@@ -350,22 +416,28 @@ export class CodeIntelligenceCoordinator {
|
|
|
350
416
|
const plan = createEditPlan(this.root, snapshot, sandbox, input.operations, state.missing.length ? 'incomplete' : 'passed', input.validation_profile, [...changed], diff);
|
|
351
417
|
return { data: plan, provenance, ...state, warnings: state.warnings };
|
|
352
418
|
}
|
|
353
|
-
async apply(input, snapshot) {
|
|
419
|
+
async apply(input, snapshot, state) {
|
|
354
420
|
if (this.options.mode !== 'active')
|
|
355
421
|
throw new Error('code edits require code-intelligence mode=active');
|
|
356
|
-
|
|
422
|
+
if (nowMs() >= state.deadline)
|
|
423
|
+
throw new Error('request deadline exceeded before apply');
|
|
357
424
|
const plan = loadEditPlan(this.root, input.plan_id);
|
|
358
425
|
if (plan.workspace_id !== this.workspace_id || plan.snapshot_id !== input.expected_snapshot_id)
|
|
359
426
|
throw new Error('edit plan scope does not match workspace or expected snapshot');
|
|
360
427
|
const expected = loadSnapshot(this.root, input.expected_snapshot_id);
|
|
361
428
|
assertSnapshotCurrent(this.root, expected, this.options.policy);
|
|
429
|
+
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 };
|
|
430
|
+
if (!state.receiptFits?.(receipt))
|
|
431
|
+
throw new Error('Budget cannot represent the required apply receipt; no transaction was started');
|
|
432
|
+
if (nowMs() >= state.deadline)
|
|
433
|
+
throw new Error('request deadline exceeded before apply');
|
|
362
434
|
const result = applyEditPlan(this.root, plan, expected, input.idempotency_key);
|
|
363
435
|
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
436
|
}
|
|
365
437
|
errorResponse(requestId, workspace, snapshotId, started, error) {
|
|
366
438
|
const code = errorCode(error.message);
|
|
367
439
|
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: '
|
|
440
|
+
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
441
|
}
|
|
370
442
|
}
|
|
371
443
|
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
|
}
|
|
@@ -20,8 +20,14 @@ export class McpStdioClient {
|
|
|
20
20
|
this.maxBytes = maxBytes;
|
|
21
21
|
}
|
|
22
22
|
async call(tool, arguments_, timeoutMs) {
|
|
23
|
+
const deadline = Date.now() + timeoutMs;
|
|
23
24
|
await this.start(timeoutMs);
|
|
24
|
-
|
|
25
|
+
const remaining = deadline - Date.now();
|
|
26
|
+
if (remaining <= 0) {
|
|
27
|
+
void this.close();
|
|
28
|
+
throw new Error('MCP call deadline exceeded during initialization');
|
|
29
|
+
}
|
|
30
|
+
return this.request('tools/call', { name: tool, arguments: arguments_ }, remaining);
|
|
25
31
|
}
|
|
26
32
|
async close() {
|
|
27
33
|
if (!this.child)
|
|
@@ -35,7 +41,7 @@ export class McpStdioClient {
|
|
|
35
41
|
this.pending.clear();
|
|
36
42
|
child.kill('SIGTERM');
|
|
37
43
|
await new Promise(resolve => {
|
|
38
|
-
const timer = setTimeout(() => { if (
|
|
44
|
+
const timer = setTimeout(() => { if (child.exitCode === null && child.signalCode === null)
|
|
39
45
|
child.kill('SIGKILL'); resolve(); }, 500);
|
|
40
46
|
child.once('close', () => { clearTimeout(timer); resolve(); });
|
|
41
47
|
});
|
|
@@ -60,6 +66,8 @@ export class McpStdioClient {
|
|
|
60
66
|
return Promise.resolve();
|
|
61
67
|
}
|
|
62
68
|
request(method, params, timeoutMs) {
|
|
69
|
+
if (timeoutMs <= 0)
|
|
70
|
+
return Promise.reject(new Error(`MCP request deadline exceeded: ${method}`));
|
|
63
71
|
if (!this.child)
|
|
64
72
|
return Promise.reject(new Error('MCP process is not running'));
|
|
65
73
|
const id = this.nextId++;
|