@crouton-kit/tsym 0.1.0 → 0.2.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/README.md +1 -1
- package/dist/commands/handlers/find.js +11 -10
- package/dist/commands/handlers/inspect.js +64 -20
- package/dist/commands/handlers/outline.js +25 -14
- package/dist/commands/handlers/show.js +6 -5
- package/dist/commands/handlers/type.js +3 -1
- package/dist/commands/help.js +1 -1
- package/dist/commands/tree.js +2 -2
- package/dist/commands/types.d.ts +6 -0
- package/dist/context-exposure.d.ts +9 -0
- package/dist/context-exposure.js +55 -0
- package/dist/core/members.d.ts +9 -0
- package/dist/core/members.js +12 -0
- package/dist/index/build.d.ts +2 -1
- package/dist/index/build.js +3 -3
- package/dist/index/refresh.d.ts +2 -1
- package/dist/index/refresh.js +2 -2
- package/dist/output.js +10 -3
- package/dist/relations/store-relations.d.ts +14 -0
- package/dist/relations/store-relations.js +24 -0
- package/dist/server/client.js +7 -2
- package/dist/server/main.js +129 -53
- package/dist/server/wire.d.ts +4 -1
- package/dist/store/load.js +2 -2
- package/dist/store/meta.d.ts +13 -9
- package/dist/store/meta.js +30 -18
- package/dist/store/schema.d.ts +1 -1
- package/dist/store/schema.js +1 -1
- package/dist/store/seed.d.ts +13 -0
- package/dist/store/seed.js +185 -0
- package/dist/store/store.d.ts +2 -0
- package/dist/store/store.js +11 -2
- package/package.json +2 -2
package/dist/output.js
CHANGED
|
@@ -7,7 +7,7 @@ export function xml(name, attributes = {}, body = '') {
|
|
|
7
7
|
export function escapeAttribute(value) { return value.replace(/"/g, '"'); }
|
|
8
8
|
export function renderCompletion(completion, json) {
|
|
9
9
|
const exitCode = completionCode(completion);
|
|
10
|
-
const payload = json ? JSON.stringify(completion) + '\n' : render(completion) + '\n';
|
|
10
|
+
const payload = json ? JSON.stringify(publicCompletion(completion)) + '\n' : render(completion) + '\n';
|
|
11
11
|
const stdout = payload;
|
|
12
12
|
const stderr = receipt(completion);
|
|
13
13
|
return { stdout, stderr, exitCode };
|
|
@@ -21,11 +21,17 @@ export function completionCode(completion) {
|
|
|
21
21
|
}
|
|
22
22
|
function render(completion) {
|
|
23
23
|
if (isAnswer(completion))
|
|
24
|
-
return nodes(completion.data)
|
|
24
|
+
return `${completion.context ? `${completion.context.endsWith('\n') ? completion.context : `${completion.context}\n`}` : ''}${nodes(completion.data)}`;
|
|
25
25
|
if (completion.category === 'invocation')
|
|
26
26
|
return renderInvocationError(completion);
|
|
27
27
|
return renderToolError(completion);
|
|
28
28
|
}
|
|
29
|
+
function publicCompletion(completion) {
|
|
30
|
+
if (!isAnswer(completion))
|
|
31
|
+
return completion;
|
|
32
|
+
const { displayedFiles: _, context: __, contextError: ___, ...answer } = completion;
|
|
33
|
+
return answer;
|
|
34
|
+
}
|
|
29
35
|
function nodes(data) { return (Array.isArray(data) ? data : [data]).map(renderNode).join('\n'); }
|
|
30
36
|
export function renderNode(node) {
|
|
31
37
|
const attributes = Object.entries(node.attributes ?? {}).filter(([, value]) => value !== undefined).map(([key, value]) => ` ${key}="${escapeAttribute(String(value))}"`).join('');
|
|
@@ -47,7 +53,8 @@ function receipt(completion) {
|
|
|
47
53
|
if (isAnswer(completion)) {
|
|
48
54
|
const answer = completion;
|
|
49
55
|
const base = answer.receipt ?? `${answer.count} answer${answer.count === 1 ? '' : 's'}, 0 ms`;
|
|
50
|
-
|
|
56
|
+
const receipt = answer.startupMs === undefined ? base : `${base} (server started, ${answer.startupMs} ms)`;
|
|
57
|
+
return answer.contextError ? `${answer.contextError}\n${receipt}` : receipt;
|
|
51
58
|
}
|
|
52
59
|
return completion.category === 'invocation' ? `invocation rejected, 0 ms` : `tool failed, 0 ms`;
|
|
53
60
|
}
|
|
@@ -9,6 +9,20 @@ export interface ReferenceRow {
|
|
|
9
9
|
qualifiedName: string;
|
|
10
10
|
role: string;
|
|
11
11
|
}
|
|
12
|
+
export interface MemberSummary {
|
|
13
|
+
key: string;
|
|
14
|
+
address: string;
|
|
15
|
+
path: string;
|
|
16
|
+
line: number;
|
|
17
|
+
col: number;
|
|
18
|
+
kind: string;
|
|
19
|
+
qualifiedName: string;
|
|
20
|
+
callers: number;
|
|
21
|
+
calls: number;
|
|
22
|
+
refs: number;
|
|
23
|
+
container: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function memberSummaries(context: SemanticContext, record: DeclarationRecord): Promise<MemberSummary[]>;
|
|
12
26
|
export declare function references(context: SemanticContext, record: DeclarationRecord, filters: {
|
|
13
27
|
in: readonly string[];
|
|
14
28
|
exclude: readonly string[];
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { directMembers } from '../core/members.js';
|
|
2
3
|
import { matchesPathFilters } from '../core/receipt.js';
|
|
3
4
|
import { query } from '../store/query.js';
|
|
4
5
|
import { graphAfterExclusion, GraphBuilder, projectGraph } from './graph.js';
|
|
5
6
|
import { nodeFor } from './types.js';
|
|
7
|
+
export async function memberSummaries(context, record) {
|
|
8
|
+
const container = recordKey(context, record);
|
|
9
|
+
const members = directMembers(storeRows(context, 'MATCH (member:Declaration) WHERE member.container = $container RETURN member.key AS key, member.at AS at, member.file AS file, member.line AS line, member.col AS col, member.kind AS kind, member.qname AS qname, member.container AS container', { container }).map((row) => ({ key: text(row, 'key'), address: text(row, 'at'), path: text(row, 'file'), line: number(row, 'line'), col: number(row, 'col'), kind: text(row, 'kind'), qualifiedName: text(row, 'qname'), container: text(row, 'container') })), container);
|
|
10
|
+
const callers = relatedKeys(context, 'MATCH (caller:Declaration)-[call:CALLS]->(member:Declaration) WHERE member.container = $container AND call.dispatch = false RETURN member.key AS member, caller.key AS related', container);
|
|
11
|
+
addRelatedKeys(callers, relatedKeys(context, 'MATCH (member:Declaration)-[:IMPLEMENTS]->(:Declaration)<-[call:CALLS]-(caller:Declaration) WHERE member.container = $container AND call.dispatch = true RETURN member.key AS member, caller.key AS related', container));
|
|
12
|
+
const calls = relatedKeys(context, 'MATCH (member:Declaration)-[call:CALLS]->(target:Declaration) WHERE member.container = $container AND call.dispatch = false RETURN member.key AS member, target.key AS related', container);
|
|
13
|
+
addRelatedKeys(calls, relatedKeys(context, 'MATCH (member:Declaration)-[call:CALLS]->(contract:Declaration)<-[:IMPLEMENTS]-(target:Declaration) WHERE member.container = $container AND call.dispatch = true RETURN member.key AS member, target.key AS related', container));
|
|
14
|
+
const refs = new Map(storeRows(context, 'MATCH (:Declaration)-[reference:REFERENCES]->(member:Declaration) WHERE member.container = $container RETURN member.key AS member, count(reference) AS refs', { container }).map((row) => [text(row, 'member'), number(row, 'refs')]));
|
|
15
|
+
return members.map((member) => ({ ...member, callers: callers.get(member.key)?.size ?? 0, calls: calls.get(member.key)?.size ?? 0, refs: refs.get(member.key) ?? 0 }));
|
|
16
|
+
}
|
|
6
17
|
export async function references(context, record, filters) {
|
|
7
18
|
const rows = storeRows(context, 'MATCH (owner:Declaration)-[reference:REFERENCES]->(target:Declaration) WHERE target.key = $key RETURN owner.file AS file, owner.kind AS kind, owner.qname AS qname, reference.line AS line, reference.col AS col, reference.role AS role', { key: recordKey(context, record) });
|
|
8
19
|
return rows.flatMap((row) => {
|
|
@@ -156,3 +167,16 @@ function unresolvedSite(record, row, name, reason, dispatch) {
|
|
|
156
167
|
function text(row, key) { return String(row[key] ?? ''); }
|
|
157
168
|
function number(row, key) { return Number(row[key]); }
|
|
158
169
|
function boolean(row, key) { return row[key] === true; }
|
|
170
|
+
function relatedKeys(context, statement, container) {
|
|
171
|
+
const related = new Map();
|
|
172
|
+
for (const row of storeRows(context, statement, { container })) {
|
|
173
|
+
const member = text(row, 'member');
|
|
174
|
+
(related.get(member) ?? related.set(member, new Set()).get(member)).add(text(row, 'related'));
|
|
175
|
+
}
|
|
176
|
+
return related;
|
|
177
|
+
}
|
|
178
|
+
function addRelatedKeys(target, additional) {
|
|
179
|
+
for (const [member, keys] of additional)
|
|
180
|
+
for (const key of keys)
|
|
181
|
+
(target.get(member) ?? target.set(member, new Set()).get(member)).add(key);
|
|
182
|
+
}
|
package/dist/server/client.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { closeSync, existsSync, openSync } from 'node:fs';
|
|
3
|
+
import { attachReadContext } from '../context-exposure.js';
|
|
3
4
|
import { connect } from 'node:net';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
5
6
|
import { discoverRoot, ensureStateDirectory, pathsForRoot } from './lifecycle.js';
|
|
@@ -19,7 +20,7 @@ class DeadlineExpired extends Error {
|
|
|
19
20
|
}
|
|
20
21
|
export async function invokeResident(invocation) {
|
|
21
22
|
const root = discoverRoot(invocation.root);
|
|
22
|
-
return send(root, 'invoke', invocation, true);
|
|
23
|
+
return attachReadContext(invocation, await send(root, 'invoke', invocation, true));
|
|
23
24
|
}
|
|
24
25
|
export async function serverStatus(rootOverride) {
|
|
25
26
|
const root = discoverRoot(rootOverride);
|
|
@@ -151,9 +152,11 @@ function exchange(paths, payload) {
|
|
|
151
152
|
break;
|
|
152
153
|
case 'project-load-start':
|
|
153
154
|
case 'project-load-end':
|
|
155
|
+
deadlinePhase = 'project-load';
|
|
156
|
+
break;
|
|
154
157
|
case 'index-build-start':
|
|
155
158
|
case 'index-build-end':
|
|
156
|
-
deadlinePhase = '
|
|
159
|
+
deadlinePhase = 'index-build';
|
|
157
160
|
break;
|
|
158
161
|
case 'running':
|
|
159
162
|
deadlinePhase = 'running';
|
|
@@ -203,6 +206,8 @@ function withStartup(completion, startupStartedAt) {
|
|
|
203
206
|
function deadlineFailure(paths, deadline) {
|
|
204
207
|
if (deadline.phase === 'project-load')
|
|
205
208
|
return { category: 'tool', code: 'project-load-timeout', message: 'TypeScript did not load the workspace project before its deadline.', logPath: paths.log, next: 'Repair the project, then retry.' };
|
|
209
|
+
if (deadline.phase === 'index-build')
|
|
210
|
+
return { category: 'tool', code: 'index-build-timeout', message: 'The workspace index did not build before its deadline.', logPath: paths.log, next: 'Repair the project, then retry.' };
|
|
206
211
|
if (deadline.phase === 'running')
|
|
207
212
|
return { category: 'tool', code: 'request-timeout', message: 'The semantic request exceeded its deadline.', logPath: paths.log, received: deadline.command ?? 'no semantic operation started', next: 'Retry after repairing the workspace or narrowing the command.' };
|
|
208
213
|
return { category: 'tool', code: 'server-unavailable', message: 'The resident did not begin this request before its admission deadline.', logPath: paths.log, next: 'Retry after the current request completes.' };
|
package/dist/server/main.js
CHANGED
|
@@ -2,6 +2,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
2
2
|
import { DatabaseSync } from 'node:sqlite';
|
|
3
3
|
import { createServer } from 'node:net';
|
|
4
4
|
import { appendFileSync, chmodSync, existsSync, openSync, readdirSync } from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
5
6
|
import { discoverRoot, ensureStateDirectory, idleSeconds, pathsForRoot, publishState, removeEndpoint } from './lifecycle.js';
|
|
6
7
|
import { NdjsonReader, WIRE_VERSION, writeFrame } from './wire.js';
|
|
7
8
|
import { createSemanticContext } from './context.js';
|
|
@@ -11,7 +12,8 @@ import { ingestCommits } from '../index/commits.js';
|
|
|
11
12
|
import { refreshIndex } from '../index/refresh.js';
|
|
12
13
|
import { IndexStateController } from '../index/state.js';
|
|
13
14
|
import { watchIndex } from '../index/watch.js';
|
|
14
|
-
import {
|
|
15
|
+
import { discardIncompatibleStore, formatIdentity, hasDrift, metadataFor, readMetadata, writeMetadata } from '../store/meta.js';
|
|
16
|
+
import { copyStore, refuseSeed, seedFromSibling } from '../store/seed.js';
|
|
15
17
|
import { EngineBinaryError, Store } from '../store/store.js';
|
|
16
18
|
import { executeSemantic } from '../commands/handlers.js';
|
|
17
19
|
import { findNode } from '../commands/tree.js';
|
|
@@ -46,6 +48,7 @@ async function main() {
|
|
|
46
48
|
const bootedAt = new Date();
|
|
47
49
|
const bootedAtMonotonic = performance.now();
|
|
48
50
|
let bootstrapped = false;
|
|
51
|
+
let seededFrom;
|
|
49
52
|
let loading = false;
|
|
50
53
|
let stopping = false;
|
|
51
54
|
let retired = false;
|
|
@@ -53,6 +56,7 @@ async function main() {
|
|
|
53
56
|
let queue = [];
|
|
54
57
|
let currentCommand;
|
|
55
58
|
let loadDeadlineAt;
|
|
59
|
+
let indexBuildDeadlineAt;
|
|
56
60
|
let activeDeadlineAt;
|
|
57
61
|
let activeTimer;
|
|
58
62
|
let idleTimer;
|
|
@@ -210,10 +214,13 @@ async function main() {
|
|
|
210
214
|
socket.destroy();
|
|
211
215
|
exitAfterCleanup();
|
|
212
216
|
};
|
|
213
|
-
const
|
|
217
|
+
const startupDeadlineFailure = (phase) => phase === 'project-load'
|
|
218
|
+
? toolError('project-load-timeout', 'TypeScript did not load the workspace project before its deadline.', 'Repair the project, then retry.')
|
|
219
|
+
: toolError('index-build-timeout', 'The workspace index did not build before its deadline.', 'Repair the project, then retry.');
|
|
220
|
+
const startupTimeout = (phase) => {
|
|
214
221
|
if (retired)
|
|
215
222
|
return;
|
|
216
|
-
const completion =
|
|
223
|
+
const completion = startupDeadlineFailure(phase);
|
|
217
224
|
if (active) {
|
|
218
225
|
respond(active.socket, active.request.requestId, completion);
|
|
219
226
|
finishSocket(active.socket);
|
|
@@ -238,68 +245,109 @@ async function main() {
|
|
|
238
245
|
if (active)
|
|
239
246
|
sendPhase(active.socket, active.request.requestId, 'running', 120_000);
|
|
240
247
|
};
|
|
241
|
-
const loadProjects = async () => {
|
|
248
|
+
const loadProjects = async (startup) => {
|
|
242
249
|
loading = true;
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
250
|
+
const sendStartupPhase = (phase, remainingMs) => {
|
|
251
|
+
if (active)
|
|
252
|
+
sendPhase(active.socket, active.request.requestId, phase, remainingMs);
|
|
253
|
+
for (const queued of queue)
|
|
254
|
+
sendPhase(queued.socket, queued.request.requestId, phase, remainingMs);
|
|
255
|
+
if (startup)
|
|
256
|
+
sendPhase(startup.socket, startup.requestId, phase, remainingMs);
|
|
257
|
+
};
|
|
251
258
|
let deadlineTimer;
|
|
259
|
+
const deadline = (phase, durationMs) => new Promise((_, reject) => {
|
|
260
|
+
deadlineTimer = setTimeout(() => reject(new DeadlineExceeded(phase)), durationMs);
|
|
261
|
+
deadlineTimer.unref();
|
|
262
|
+
});
|
|
252
263
|
try {
|
|
253
|
-
|
|
264
|
+
loadDeadlineAt = performance.now() + 60_000;
|
|
265
|
+
for (const queued of queue)
|
|
266
|
+
pauseQueueDeadline(queued);
|
|
267
|
+
sendStartupPhase('project-load-start', 60_000);
|
|
268
|
+
semantic ??= createSemanticContext(root);
|
|
269
|
+
const { program, projects } = semantic;
|
|
270
|
+
const { configured, identity, expected } = await Promise.race([
|
|
254
271
|
(async () => {
|
|
255
|
-
semantic ??= createSemanticContext(root);
|
|
256
|
-
const { program, projects } = semantic;
|
|
257
272
|
await projects.bootstrap();
|
|
258
|
-
store = new Store(paths.store);
|
|
259
|
-
if (active)
|
|
260
|
-
sendPhase(active.socket, active.request.requestId, 'index-build-start', remaining(loadDeadlineAt));
|
|
261
|
-
// The watcher starts before the build, not after it: buildIndex takes tens of seconds on a large workspace,
|
|
262
|
-
// and an edit landing in that window would otherwise never be recorded, publishing a stale index as current.
|
|
263
|
-
// buildIndex is synchronous, so no watch callback can run until index is assigned below.
|
|
264
|
-
watcher = watchIndex(root, projects.configured(), refresh, (error) => append(paths, `index refresh failure: ${safe(error)}`), (flush) => index?.markStale(flush));
|
|
265
273
|
const configured = projects.configured();
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
274
|
+
const identity = formatIdentity();
|
|
275
|
+
if (discardIncompatibleStore(paths.store, identity))
|
|
276
|
+
append(paths, 'discarded an incompatible store');
|
|
277
|
+
const expected = metadataFor(root, configured, headCommit(root), 0, identity);
|
|
278
|
+
if (!forceRebuild && readMetadata(paths.store) === null) {
|
|
279
|
+
const seeded = await seedFromSibling(root, paths, expected, identity);
|
|
280
|
+
if (seeded) {
|
|
281
|
+
seededFrom = seeded.root;
|
|
282
|
+
append(paths, `store seeded from ${seeded.root} bytes=${seeded.bytes} duration=${Math.round(seeded.durationMs)}ms`);
|
|
283
|
+
}
|
|
284
|
+
else
|
|
285
|
+
append(paths, 'store seed skipped: no identical sibling');
|
|
286
|
+
}
|
|
287
|
+
return { configured, identity, expected };
|
|
288
|
+
})(),
|
|
289
|
+
deadline('project-load', 60_000),
|
|
290
|
+
]);
|
|
291
|
+
if (deadlineTimer)
|
|
292
|
+
clearTimeout(deadlineTimer);
|
|
293
|
+
deadlineTimer = undefined;
|
|
294
|
+
loadDeadlineAt = undefined;
|
|
295
|
+
store = new Store(paths.store);
|
|
296
|
+
indexBuildDeadlineAt = performance.now() + 180_000;
|
|
297
|
+
sendStartupPhase('index-build-start', 180_000);
|
|
298
|
+
// The watcher starts before the build, not after it: buildIndex takes tens of seconds on a large workspace,
|
|
299
|
+
// and an edit landing in that window would otherwise never be recorded, publishing a stale index as current.
|
|
300
|
+
// buildIndex is synchronous, so no watch callback can run until index is assigned below.
|
|
301
|
+
watcher = watchIndex(root, configured, refresh, (error) => append(paths, `index refresh failure: ${safe(error)}`), (flush) => index?.markStale(flush));
|
|
302
|
+
const metadata = readMetadata(store.path);
|
|
303
|
+
let built;
|
|
304
|
+
await Promise.race([
|
|
305
|
+
(async () => {
|
|
306
|
+
if (!forceRebuild && metadata !== null) {
|
|
270
307
|
const reused = reuseIndex(store);
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
308
|
+
if (hasDrift(metadata, expected)) {
|
|
309
|
+
const refreshed = refreshIndex(root, program, configured, store, reused, { fileChanges: { invalidateAll: true } }, identity);
|
|
310
|
+
append(paths, `index refresh stored=${refreshed.stored} changedFiles=${refreshed.changedFiles.length} duration=${Math.round(refreshed.built.receipt.durationMs)}ms`);
|
|
311
|
+
built = refreshed.built;
|
|
312
|
+
}
|
|
313
|
+
else
|
|
314
|
+
built = reused;
|
|
274
315
|
}
|
|
275
316
|
else {
|
|
276
|
-
built = buildIndex(root, program, configured, store);
|
|
317
|
+
built = buildIndex(root, program, configured, store, identity);
|
|
277
318
|
}
|
|
278
|
-
index = new IndexStateController({ indexedAt: indexedAt(root), state: 'current', built }, (snapshot) => {
|
|
279
|
-
semantic.index = snapshot;
|
|
280
|
-
publishState(paths, publishedState(snapshot));
|
|
281
|
-
});
|
|
282
|
-
semantic.store = store;
|
|
283
|
-
if (active)
|
|
284
|
-
sendPhase(active.socket, active.request.requestId, 'index-build-end', remaining(loadDeadlineAt));
|
|
285
319
|
})(),
|
|
286
|
-
|
|
320
|
+
deadline('index-build', 180_000),
|
|
287
321
|
]);
|
|
322
|
+
if (deadlineTimer)
|
|
323
|
+
clearTimeout(deadlineTimer);
|
|
324
|
+
deadlineTimer = undefined;
|
|
325
|
+
const indexBuildRemaining = remaining(indexBuildDeadlineAt);
|
|
326
|
+
indexBuildDeadlineAt = undefined;
|
|
327
|
+
index = new IndexStateController({ indexedAt: indexedAt(root), state: 'current', built: built }, (snapshot) => {
|
|
328
|
+
semantic.index = snapshot;
|
|
329
|
+
publishState(paths, publishedState(snapshot));
|
|
330
|
+
});
|
|
331
|
+
semantic.store = store;
|
|
332
|
+
sendStartupPhase('index-build-end', indexBuildRemaining);
|
|
288
333
|
bootstrapped = true;
|
|
289
334
|
}
|
|
290
335
|
finally {
|
|
291
336
|
if (deadlineTimer)
|
|
292
337
|
clearTimeout(deadlineTimer);
|
|
338
|
+
if (!bootstrapped) {
|
|
339
|
+
// A failed load must leave nothing open, or the next attempt fails on store ownership instead of its real cause.
|
|
340
|
+
watcher?.close();
|
|
341
|
+
watcher = undefined;
|
|
342
|
+
store?.close();
|
|
343
|
+
store = undefined;
|
|
344
|
+
}
|
|
293
345
|
loading = false;
|
|
294
346
|
loadDeadlineAt = undefined;
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
for (const queued of queue) {
|
|
299
|
-
sendPhase(queued.socket, queued.request.requestId, 'project-load-end', 60_000);
|
|
347
|
+
indexBuildDeadlineAt = undefined;
|
|
348
|
+
if (bootstrapped && !retired)
|
|
349
|
+
for (const queued of queue)
|
|
300
350
|
armQueueDeadline(queued);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
351
|
}
|
|
304
352
|
};
|
|
305
353
|
const processQueue = async () => {
|
|
@@ -343,8 +391,8 @@ async function main() {
|
|
|
343
391
|
respond(queued.socket, queued.request.requestId, completion);
|
|
344
392
|
}
|
|
345
393
|
catch (error) {
|
|
346
|
-
if (error instanceof DeadlineExceeded
|
|
347
|
-
|
|
394
|
+
if (error instanceof DeadlineExceeded) {
|
|
395
|
+
startupTimeout(error.phase);
|
|
348
396
|
return;
|
|
349
397
|
}
|
|
350
398
|
if (error instanceof Ts7ServerDiedError) {
|
|
@@ -357,8 +405,16 @@ async function main() {
|
|
|
357
405
|
respond(queued.socket, queued.request.requestId, { category: 'tool', code: error.code, attributes: { platform: error.platform }, message: error.message });
|
|
358
406
|
return;
|
|
359
407
|
}
|
|
360
|
-
if (!retired)
|
|
361
|
-
|
|
408
|
+
if (!retired) {
|
|
409
|
+
if (!bootstrapped)
|
|
410
|
+
respond(queued.socket, queued.request.requestId, toolError('project-load-failed', 'TypeScript could not load the workspace project.', error instanceof Error ? error.message : 'Read the log and repair the project.'));
|
|
411
|
+
else {
|
|
412
|
+
append(paths, `command failed command=${currentCommand ?? 'unknown'}\n${error instanceof Error && error.stack ? error.stack : safe(error)}`);
|
|
413
|
+
const failure = toolError('command-failed', safe(error), 'Read the resident log, then retry the command.');
|
|
414
|
+
failure.received = currentCommand ?? 'unknown';
|
|
415
|
+
respond(queued.socket, queued.request.requestId, failure);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
362
418
|
}
|
|
363
419
|
finally {
|
|
364
420
|
if (activeTimer)
|
|
@@ -415,12 +471,32 @@ async function main() {
|
|
|
415
471
|
finishSocket(socket);
|
|
416
472
|
return;
|
|
417
473
|
}
|
|
474
|
+
if (request.operation === 'seed') {
|
|
475
|
+
const dest = request.seed?.dest;
|
|
476
|
+
const refusal = refuseSeed(dest, path.dirname(paths.state), bootstrapped && store !== undefined, store?.busy ?? false);
|
|
477
|
+
if (refusal)
|
|
478
|
+
respond(socket, request.requestId, toolError('server-unavailable', refusal, 'Retry after the resident is ready with a new destination under the tsym cache.'));
|
|
479
|
+
else {
|
|
480
|
+
try {
|
|
481
|
+
store.checkpoint();
|
|
482
|
+
const bytes = copyStore(paths.store, dest);
|
|
483
|
+
respond(socket, request.requestId, { data: { element: 'seed', attributes: { root, store: dest, bytes } }, count: 1 });
|
|
484
|
+
}
|
|
485
|
+
catch (error) {
|
|
486
|
+
respond(socket, request.requestId, toolError('server-unavailable', 'The resident could not copy its store.', error instanceof Error ? error.message : 'Retry after the current refresh completes.'));
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
finishSocket(socket);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
418
492
|
if (request.operation === 'status' && forceRebuild && !bootstrapped) {
|
|
419
493
|
try {
|
|
420
|
-
await loadProjects();
|
|
494
|
+
await loadProjects({ socket, requestId: request.requestId });
|
|
421
495
|
}
|
|
422
496
|
catch (error) {
|
|
423
|
-
if (error instanceof
|
|
497
|
+
if (error instanceof DeadlineExceeded)
|
|
498
|
+
respond(socket, request.requestId, startupDeadlineFailure(error.phase));
|
|
499
|
+
else if (error instanceof EngineBinaryError || error instanceof TypeScriptBinaryError)
|
|
424
500
|
respond(socket, request.requestId, { category: 'tool', code: error.code, attributes: { platform: error.platform }, message: error.message });
|
|
425
501
|
else
|
|
426
502
|
respond(socket, request.requestId, toolError('project-load-failed', 'TypeScript could not load the workspace project.', error instanceof Error ? error.message : 'Read the log and repair the project.'));
|
|
@@ -442,7 +518,7 @@ async function main() {
|
|
|
442
518
|
const snapshot = index?.current();
|
|
443
519
|
const receipt = snapshot?.built.receipt;
|
|
444
520
|
const indexedCommit = readMetadata(paths.store)?.indexedCommit;
|
|
445
|
-
respond(socket, request.requestId, { data: { element: 'server', attributes: { root, state, pid: process.pid, port: paths.port, log: paths.log, idle: `${idleSeconds()}s`, uptime: `${Math.floor(performance.now() - bootedAtMonotonic)}ms`, memory, typescript: semantic ? bundledTypeScriptVersion() : undefined, store: paths.store, indexedCommit, declarations: receipt?.declarations, edges: receipt ? Object.values(receipt.relationships).reduce((total, count) => total + count, 0) : undefined, notInProgram: semantic ? unconfiguredFileCount(root, semantic.projects.records()) : undefined, index: snapshot?.state, indexDuration: receipt ? `${Math.round(receipt.durationMs)}ms` : undefined } }, count: 1 });
|
|
521
|
+
respond(socket, request.requestId, { data: { element: 'server', attributes: { root, state, pid: process.pid, port: paths.port, log: paths.log, idle: `${idleSeconds()}s`, uptime: `${Math.floor(performance.now() - bootedAtMonotonic)}ms`, memory, typescript: semantic ? bundledTypeScriptVersion() : undefined, store: paths.store, seededFrom, indexedCommit, declarations: receipt?.declarations, edges: receipt ? Object.values(receipt.relationships).reduce((total, count) => total + count, 0) : undefined, notInProgram: semantic ? unconfiguredFileCount(root, semantic.projects.records()) : undefined, index: snapshot?.state, indexDuration: receipt ? `${Math.round(receipt.durationMs)}ms` : undefined } }, count: 1 });
|
|
446
522
|
return;
|
|
447
523
|
}
|
|
448
524
|
if (request.operation === 'stop') {
|
|
@@ -453,7 +529,7 @@ async function main() {
|
|
|
453
529
|
clearTimeout(idleTimer);
|
|
454
530
|
publishState(paths, publishedState());
|
|
455
531
|
for (const [invokeSocket, requestId] of invokeSockets) {
|
|
456
|
-
const deadlineAt = active?.socket === invokeSocket ? (loading ? loadDeadlineAt : activeDeadlineAt) : undefined;
|
|
532
|
+
const deadlineAt = active?.socket === invokeSocket ? (loading ? indexBuildDeadlineAt ?? loadDeadlineAt : activeDeadlineAt) : undefined;
|
|
457
533
|
sendPhase(invokeSocket, requestId, 'stopping', deadlineAt === undefined ? undefined : remaining(deadlineAt));
|
|
458
534
|
}
|
|
459
535
|
const completion = toolError('server-unavailable', 'The resident is stopping.', 'Wait for stop to complete before invoking tsym.');
|
package/dist/server/wire.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Answer, Invocation, InvocationError, ToolError } from '../commands/types.js';
|
|
2
2
|
export declare const WIRE_VERSION = 1;
|
|
3
|
-
export type Operation = 'invoke' | 'status' | 'stop';
|
|
3
|
+
export type Operation = 'invoke' | 'status' | 'stop' | 'seed';
|
|
4
4
|
export interface WireInvocation {
|
|
5
5
|
path: string[];
|
|
6
6
|
cwd: string;
|
|
@@ -14,6 +14,9 @@ export interface WireRequest {
|
|
|
14
14
|
root: string;
|
|
15
15
|
operation: Operation;
|
|
16
16
|
invocation?: WireInvocation;
|
|
17
|
+
seed?: {
|
|
18
|
+
dest: string;
|
|
19
|
+
};
|
|
17
20
|
}
|
|
18
21
|
export interface PhaseFrame {
|
|
19
22
|
type: 'phase';
|
package/dist/store/load.js
CHANGED
|
@@ -31,8 +31,8 @@ export function replaceFiles(store, files, graph) {
|
|
|
31
31
|
validateGraph(graph);
|
|
32
32
|
return store.transaction(() => {
|
|
33
33
|
const history = checkpointHistory(store, replaced);
|
|
34
|
-
|
|
35
|
-
store.execute(
|
|
34
|
+
if (replaced.size > 0)
|
|
35
|
+
store.execute('MATCH (d:Declaration) WHERE d.file IN $files DETACH DELETE d', { files: [...replaced] });
|
|
36
36
|
copyRows(store, 'Declaration', declarationColumns, graph.declarations);
|
|
37
37
|
copyMissingExternals(store, graph.externals);
|
|
38
38
|
copyMissingCommits(store, graph.commits ?? []);
|
package/dist/store/meta.d.ts
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import type { ConfiguredProjects } from '../ts7/projects.js';
|
|
2
|
-
export
|
|
3
|
-
size: number;
|
|
4
|
-
mtimeMs: number;
|
|
5
|
-
}
|
|
2
|
+
export type IndexedFileMeta = string;
|
|
6
3
|
export interface StoreMetadata {
|
|
7
4
|
schemaVersion: number;
|
|
8
5
|
tsVersion: string;
|
|
9
6
|
tsymBuild: string;
|
|
7
|
+
root: string;
|
|
10
8
|
indexedCommit: string;
|
|
11
9
|
configSet: string[];
|
|
12
10
|
configHashes: Record<string, string>;
|
|
@@ -18,11 +16,17 @@ export interface StoreMetadata {
|
|
|
18
16
|
}
|
|
19
17
|
export declare function metadataPath(storePath: string): string;
|
|
20
18
|
export declare function readMetadata(storePath: string): StoreMetadata | null;
|
|
19
|
+
/** A store this build cannot read, or without readable metadata, is deleted so the next build starts from nothing. */
|
|
20
|
+
export declare function discardIncompatibleStore(storePath: string, identity: StoreFormatIdentity): boolean;
|
|
21
21
|
export declare function writeMetadata(storePath: string, metadata: StoreMetadata): void;
|
|
22
22
|
/** Captures every input whose change can alter the configured-program graph. */
|
|
23
|
-
export
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
export interface StoreFormatIdentity {
|
|
24
|
+
schemaVersion: number;
|
|
25
|
+
tsVersion: string;
|
|
26
|
+
tsymBuild: string;
|
|
27
|
+
}
|
|
28
|
+
export declare function formatIdentity(tsVersion?: string): StoreFormatIdentity;
|
|
29
|
+
export declare function metadataFor(root: string, projects: ConfiguredProjects, indexedCommit: string, indexDurationMs?: number, identity?: StoreFormatIdentity): StoreMetadata;
|
|
30
|
+
export declare function hasDrift(metadata: StoreMetadata, expected: StoreMetadata): boolean;
|
|
31
|
+
export declare function sameFormat(metadata: Pick<StoreMetadata, 'schemaVersion' | 'tsVersion' | 'tsymBuild'>, identity: StoreFormatIdentity): boolean;
|
|
28
32
|
export declare function hashTsymBuild(dist?: string): string;
|
package/dist/store/meta.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
-
import { mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import * as ts from 'ts5';
|
|
@@ -20,6 +20,22 @@ export function readMetadata(storePath) {
|
|
|
20
20
|
throw error;
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
+
/** A store this build cannot read, or without readable metadata, is deleted so the next build starts from nothing. */
|
|
24
|
+
export function discardIncompatibleStore(storePath, identity) {
|
|
25
|
+
if (!existsSync(storePath))
|
|
26
|
+
return false;
|
|
27
|
+
let metadata = null;
|
|
28
|
+
try {
|
|
29
|
+
metadata = readMetadata(storePath);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
metadata = null;
|
|
33
|
+
}
|
|
34
|
+
if (metadata !== null && sameFormat(metadata, identity))
|
|
35
|
+
return false;
|
|
36
|
+
rmSync(storePath, { recursive: true, force: true });
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
23
39
|
export function writeMetadata(storePath, metadata) {
|
|
24
40
|
if (metadata.schemaVersion !== STORE_SCHEMA_VERSION)
|
|
25
41
|
throw new Error(`Store metadata schema version must be ${STORE_SCHEMA_VERSION}.`);
|
|
@@ -29,37 +45,33 @@ export function writeMetadata(storePath, metadata) {
|
|
|
29
45
|
writeFileSync(temporary, `${JSON.stringify(metadata)}\n`, { mode: 0o600 });
|
|
30
46
|
renameSync(temporary, target);
|
|
31
47
|
}
|
|
32
|
-
|
|
33
|
-
|
|
48
|
+
export function formatIdentity(tsVersion = bundledTsVersion()) {
|
|
49
|
+
return { schemaVersion: STORE_SCHEMA_VERSION, tsVersion, tsymBuild: hashTsymBuild() };
|
|
50
|
+
}
|
|
51
|
+
export function metadataFor(root, projects, indexedCommit, indexDurationMs = 0, identity = formatIdentity()) {
|
|
34
52
|
const configs = [...new Set(projects.records().map((project) => path.resolve(project.config)))].sort();
|
|
35
53
|
const shapeConfigs = inheritedConfigs(configs);
|
|
36
54
|
const files = [...new Set(projects.records().flatMap((project) => [...project.files]))].sort();
|
|
37
55
|
return {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
tsymBuild: hashTsymBuild(),
|
|
56
|
+
...identity,
|
|
57
|
+
root,
|
|
41
58
|
indexedCommit,
|
|
42
59
|
configSet: configs.map((file) => relative(root, file)),
|
|
43
60
|
configHashes: Object.fromEntries(shapeConfigs.map((file) => [relative(root, file), hashFile(file)])),
|
|
44
61
|
packageHash: hashOptional(path.join(root, 'package.json')),
|
|
45
62
|
lockfileHash: lockfiles.map((name) => `${name}:${hashOptional(path.join(root, name))}`).join('|'),
|
|
46
63
|
indexDurationMs,
|
|
47
|
-
files: Object.fromEntries(files.map((file) =>
|
|
48
|
-
const stat = statSync(file);
|
|
49
|
-
return [relative(root, file), { size: stat.size, mtimeMs: stat.mtimeMs }];
|
|
50
|
-
})),
|
|
64
|
+
files: Object.fromEntries(files.map((file) => [relative(root, file), hashFile(file)])),
|
|
51
65
|
};
|
|
52
66
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return metadata !== null && JSON.stringify(shapeOf(metadata)) === JSON.stringify(shapeOf(expected));
|
|
67
|
+
export function hasDrift(metadata, expected) {
|
|
68
|
+
return JSON.stringify(driftShape(metadata)) !== JSON.stringify(driftShape(expected));
|
|
56
69
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return JSON.stringify(metadata.files) !== JSON.stringify(expected.files);
|
|
70
|
+
export function sameFormat(metadata, identity) {
|
|
71
|
+
return metadata.schemaVersion === identity.schemaVersion && metadata.tsVersion === identity.tsVersion && metadata.tsymBuild === identity.tsymBuild;
|
|
60
72
|
}
|
|
61
|
-
function
|
|
62
|
-
const {
|
|
73
|
+
function driftShape(metadata) {
|
|
74
|
+
const { indexedCommit: _commit, indexDurationMs: _duration, ...shape } = metadata;
|
|
63
75
|
return shape;
|
|
64
76
|
}
|
|
65
77
|
function inheritedConfigs(configs) {
|
package/dist/store/schema.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const STORE_SCHEMA_VERSION =
|
|
1
|
+
export declare const STORE_SCHEMA_VERSION = 3;
|
|
2
2
|
export declare const NODE_TABLES: readonly ["CREATE NODE TABLE Declaration(key STRING, at STRING, name STRING, qname STRING, kind STRING, exported BOOL, file STRING, dir STRING, top STRING, line INT64, col INT64, endLine INT64, lines INT64, declCount INT64, container STRING, PRIMARY KEY(key))", "CREATE NODE TABLE External(key STRING, module STRING, name STRING, PRIMARY KEY(key))", "CREATE NODE TABLE Commit(hash STRING, ts STRING, author STRING, subject STRING, PRIMARY KEY(hash))"];
|
|
3
3
|
export declare const RELATION_TABLES: readonly ["CREATE REL TABLE CONTAINS(FROM Declaration TO Declaration)", "CREATE REL TABLE REFERENCES(FROM Declaration TO Declaration, FROM Declaration TO External, line INT64, col INT64, role STRING)", "CREATE REL TABLE CALLS(FROM Declaration TO Declaration, FROM Declaration TO External, line INT64, col INT64, dispatch BOOL)", "CREATE REL TABLE IMPLEMENTS(FROM Declaration TO Declaration)", "CREATE REL TABLE EXTENDS(FROM Declaration TO Declaration)", "CREATE REL TABLE TYPE_MENTION(FROM Declaration TO Declaration, FROM Declaration TO External, line INT64, col INT64, position STRING)", "CREATE REL TABLE IMPORTS(FROM Declaration TO Declaration, FROM Declaration TO External, line INT64, names STRING)", "CREATE REL TABLE EXPORTS(FROM Declaration TO Declaration, name STRING, line INT64, col INT64)", "CREATE REL TABLE CHANGED(FROM Commit TO Declaration, added INT64, removed INT64)", "CREATE REL TABLE RENAMED_FROM(FROM Declaration TO Commit, oldKey STRING, basis STRING)"];
|
|
4
4
|
export declare const SCHEMA_STATEMENTS: readonly ["CREATE NODE TABLE Declaration(key STRING, at STRING, name STRING, qname STRING, kind STRING, exported BOOL, file STRING, dir STRING, top STRING, line INT64, col INT64, endLine INT64, lines INT64, declCount INT64, container STRING, PRIMARY KEY(key))", "CREATE NODE TABLE External(key STRING, module STRING, name STRING, PRIMARY KEY(key))", "CREATE NODE TABLE Commit(hash STRING, ts STRING, author STRING, subject STRING, PRIMARY KEY(hash))", "CREATE REL TABLE CONTAINS(FROM Declaration TO Declaration)", "CREATE REL TABLE REFERENCES(FROM Declaration TO Declaration, FROM Declaration TO External, line INT64, col INT64, role STRING)", "CREATE REL TABLE CALLS(FROM Declaration TO Declaration, FROM Declaration TO External, line INT64, col INT64, dispatch BOOL)", "CREATE REL TABLE IMPLEMENTS(FROM Declaration TO Declaration)", "CREATE REL TABLE EXTENDS(FROM Declaration TO Declaration)", "CREATE REL TABLE TYPE_MENTION(FROM Declaration TO Declaration, FROM Declaration TO External, line INT64, col INT64, position STRING)", "CREATE REL TABLE IMPORTS(FROM Declaration TO Declaration, FROM Declaration TO External, line INT64, names STRING)", "CREATE REL TABLE EXPORTS(FROM Declaration TO Declaration, name STRING, line INT64, col INT64)", "CREATE REL TABLE CHANGED(FROM Commit TO Declaration, added INT64, removed INT64)", "CREATE REL TABLE RENAMED_FROM(FROM Declaration TO Commit, oldKey STRING, basis STRING)"];
|
package/dist/store/schema.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const STORE_SCHEMA_VERSION =
|
|
1
|
+
export const STORE_SCHEMA_VERSION = 3;
|
|
2
2
|
export const NODE_TABLES = [
|
|
3
3
|
'CREATE NODE TABLE Declaration(key STRING, at STRING, name STRING, qname STRING, kind STRING, exported BOOL, file STRING, dir STRING, top STRING, line INT64, col INT64, endLine INT64, lines INT64, declCount INT64, container STRING, PRIMARY KEY(key))',
|
|
4
4
|
'CREATE NODE TABLE External(key STRING, module STRING, name STRING, PRIMARY KEY(key))',
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ServerPaths } from '../server/lifecycle.js';
|
|
2
|
+
import { type StoreFormatIdentity, type StoreMetadata } from './meta.js';
|
|
3
|
+
export interface SeedReceipt {
|
|
4
|
+
root: string;
|
|
5
|
+
bytes: number;
|
|
6
|
+
durationMs: number;
|
|
7
|
+
}
|
|
8
|
+
/** Finds compatible sibling stores and installs one only after a complete copy validates. */
|
|
9
|
+
export declare function seedFromSibling(root: string, paths: ServerPaths, expected: StoreMetadata, identity: StoreFormatIdentity): Promise<SeedReceipt | undefined>;
|
|
10
|
+
/** Copies a resident's store after the owner checkpointed it. The owner excludes its process-local marker. */
|
|
11
|
+
export declare function copyStore(source: string, destination: string): number;
|
|
12
|
+
/** Returns the wire refusal reason, keeping the owner’s destination safety contract testable. */
|
|
13
|
+
export declare function refuseSeed(dest: unknown, cacheBase: string, bootstrapped: boolean, busy: boolean): string | undefined;
|