@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/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
- return answer.startupMs === undefined ? base : `${base} (server started, ${answer.startupMs} ms)`;
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
+ }
@@ -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 = 'project-load';
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.' };
@@ -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 { canReuse, hasSourceDrift, metadataFor, readMetadata, writeMetadata } from '../store/meta.js';
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 projectLoadTimeout = () => {
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 = toolError('project-load-timeout', 'TypeScript did not load the workspace project before its deadline.', 'Repair the project, then retry.');
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
- loadDeadlineAt = performance.now() + 60_000;
244
- if (active)
245
- sendPhase(active.socket, active.request.requestId, 'project-load-start', 60_000);
246
- for (const queued of queue) {
247
- pauseQueueDeadline(queued);
248
- sendPhase(queued.socket, queued.request.requestId, 'project-load-start', 60_000);
249
- }
250
- let timedOut = false;
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
- await Promise.race([
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 expected = metadataFor(root, configured, headCommit(root));
267
- const metadata = readMetadata(store.path);
268
- let built;
269
- if (!forceRebuild && canReuse(metadata, expected)) {
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
- built = hasSourceDrift(metadata, expected)
272
- ? refreshIndex(root, program, configured, store, reused, { fileChanges: { invalidateAll: true } }).built
273
- : reused;
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
- new Promise((_, reject) => { deadlineTimer = setTimeout(() => { timedOut = true; reject(new DeadlineExceeded('project-load')); }, 60_000); deadlineTimer.unref(); }),
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
- if (!timedOut && !retired) {
296
- if (active)
297
- sendPhase(active.socket, active.request.requestId, 'project-load-end', 60_000);
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 && error.phase === 'project-load') {
347
- projectLoadTimeout();
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
- 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.'));
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 EngineBinaryError || error instanceof TypeScriptBinaryError)
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.');
@@ -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';
@@ -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
- for (const file of replaced)
35
- store.execute(`MATCH (d:Declaration) WHERE d.file = ${literal(file)} DETACH DELETE d`);
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 ?? []);
@@ -1,12 +1,10 @@
1
1
  import type { ConfiguredProjects } from '../ts7/projects.js';
2
- export interface IndexedFileMeta {
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 declare function metadataFor(root: string, projects: ConfiguredProjects, indexedCommit: string, indexDurationMs?: number, tsVersion?: string): StoreMetadata;
24
- /** A persisted store can be rehydrated only when its schema, program shape, and checkpoint still match. */
25
- export declare function canReuse(metadata: StoreMetadata | null, expected: StoreMetadata): boolean;
26
- /** Source changes do not invalidate the graph shape; startup rehydrates then performs the ordinary refresh. */
27
- export declare function hasSourceDrift(metadata: StoreMetadata, expected: StoreMetadata): boolean;
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;
@@ -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
- /** Captures every input whose change can alter the configured-program graph. */
33
- export function metadataFor(root, projects, indexedCommit, indexDurationMs = 0, tsVersion = bundledTsVersion()) {
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
- schemaVersion: STORE_SCHEMA_VERSION,
39
- tsVersion,
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
- /** A persisted store can be rehydrated only when its schema, program shape, and checkpoint still match. */
54
- export function canReuse(metadata, expected) {
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
- /** Source changes do not invalidate the graph shape; startup rehydrates then performs the ordinary refresh. */
58
- export function hasSourceDrift(metadata, expected) {
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 shapeOf(metadata) {
62
- const { files: _files, indexDurationMs: _duration, ...shape } = metadata;
73
+ function driftShape(metadata) {
74
+ const { indexedCommit: _commit, indexDurationMs: _duration, ...shape } = metadata;
63
75
  return shape;
64
76
  }
65
77
  function inheritedConfigs(configs) {
@@ -1,4 +1,4 @@
1
- export declare const STORE_SCHEMA_VERSION = 2;
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)"];
@@ -1,4 +1,4 @@
1
- export const STORE_SCHEMA_VERSION = 2;
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;