@crouton-kit/tsym 0.1.1 → 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, discardIncompatibleStore, 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,55 +245,91 @@ 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
- if (discardIncompatibleStore(paths.store))
259
- append(paths, 'discarded a store written by another tsym schema version');
260
- store = new Store(paths.store);
261
- if (active)
262
- sendPhase(active.socket, active.request.requestId, 'index-build-start', remaining(loadDeadlineAt));
263
- // The watcher starts before the build, not after it: buildIndex takes tens of seconds on a large workspace,
264
- // and an edit landing in that window would otherwise never be recorded, publishing a stale index as current.
265
- // buildIndex is synchronous, so no watch callback can run until index is assigned below.
266
- watcher = watchIndex(root, projects.configured(), refresh, (error) => append(paths, `index refresh failure: ${safe(error)}`), (flush) => index?.markStale(flush));
267
273
  const configured = projects.configured();
268
- const expected = metadataFor(root, configured, headCommit(root));
269
- const metadata = readMetadata(store.path);
270
- let built;
271
- 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) {
272
307
  const reused = reuseIndex(store);
273
- built = hasSourceDrift(metadata, expected)
274
- ? refreshIndex(root, program, configured, store, reused, { fileChanges: { invalidateAll: true } }).built
275
- : 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;
276
315
  }
277
316
  else {
278
- built = buildIndex(root, program, configured, store);
317
+ built = buildIndex(root, program, configured, store, identity);
279
318
  }
280
- index = new IndexStateController({ indexedAt: indexedAt(root), state: 'current', built }, (snapshot) => {
281
- semantic.index = snapshot;
282
- publishState(paths, publishedState(snapshot));
283
- });
284
- semantic.store = store;
285
- if (active)
286
- sendPhase(active.socket, active.request.requestId, 'index-build-end', remaining(loadDeadlineAt));
287
319
  })(),
288
- new Promise((_, reject) => { deadlineTimer = setTimeout(() => { timedOut = true; reject(new DeadlineExceeded('project-load')); }, 60_000); deadlineTimer.unref(); }),
320
+ deadline('index-build', 180_000),
289
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);
290
333
  bootstrapped = true;
291
334
  }
292
335
  finally {
@@ -301,14 +344,10 @@ async function main() {
301
344
  }
302
345
  loading = false;
303
346
  loadDeadlineAt = undefined;
304
- if (!timedOut && !retired) {
305
- if (active)
306
- sendPhase(active.socket, active.request.requestId, 'project-load-end', 60_000);
307
- for (const queued of queue) {
308
- sendPhase(queued.socket, queued.request.requestId, 'project-load-end', 60_000);
347
+ indexBuildDeadlineAt = undefined;
348
+ if (bootstrapped && !retired)
349
+ for (const queued of queue)
309
350
  armQueueDeadline(queued);
310
- }
311
- }
312
351
  }
313
352
  };
314
353
  const processQueue = async () => {
@@ -352,8 +391,8 @@ async function main() {
352
391
  respond(queued.socket, queued.request.requestId, completion);
353
392
  }
354
393
  catch (error) {
355
- if (error instanceof DeadlineExceeded && error.phase === 'project-load') {
356
- projectLoadTimeout();
394
+ if (error instanceof DeadlineExceeded) {
395
+ startupTimeout(error.phase);
357
396
  return;
358
397
  }
359
398
  if (error instanceof Ts7ServerDiedError) {
@@ -366,8 +405,16 @@ async function main() {
366
405
  respond(queued.socket, queued.request.requestId, { category: 'tool', code: error.code, attributes: { platform: error.platform }, message: error.message });
367
406
  return;
368
407
  }
369
- if (!retired)
370
- 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
+ }
371
418
  }
372
419
  finally {
373
420
  if (activeTimer)
@@ -424,12 +471,32 @@ async function main() {
424
471
  finishSocket(socket);
425
472
  return;
426
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
+ }
427
492
  if (request.operation === 'status' && forceRebuild && !bootstrapped) {
428
493
  try {
429
- await loadProjects();
494
+ await loadProjects({ socket, requestId: request.requestId });
430
495
  }
431
496
  catch (error) {
432
- 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)
433
500
  respond(socket, request.requestId, { category: 'tool', code: error.code, attributes: { platform: error.platform }, message: error.message });
434
501
  else
435
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.'));
@@ -451,7 +518,7 @@ async function main() {
451
518
  const snapshot = index?.current();
452
519
  const receipt = snapshot?.built.receipt;
453
520
  const indexedCommit = readMetadata(paths.store)?.indexedCommit;
454
- 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 });
455
522
  return;
456
523
  }
457
524
  if (request.operation === 'stop') {
@@ -462,7 +529,7 @@ async function main() {
462
529
  clearTimeout(idleTimer);
463
530
  publishState(paths, publishedState());
464
531
  for (const [invokeSocket, requestId] of invokeSockets) {
465
- const deadlineAt = active?.socket === invokeSocket ? (loading ? loadDeadlineAt : activeDeadlineAt) : undefined;
532
+ const deadlineAt = active?.socket === invokeSocket ? (loading ? indexBuildDeadlineAt ?? loadDeadlineAt : activeDeadlineAt) : undefined;
466
533
  sendPhase(invokeSocket, requestId, 'stopping', deadlineAt === undefined ? undefined : remaining(deadlineAt));
467
534
  }
468
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,13 +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;
21
- /** A store written by another schema version, or without readable metadata, cannot be read by this build; it is deleted so the next build starts from nothing. */
22
- export declare function discardIncompatibleStore(storePath: string): boolean;
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;
23
21
  export declare function writeMetadata(storePath: string, metadata: StoreMetadata): void;
24
22
  /** Captures every input whose change can alter the configured-program graph. */
25
- export declare function metadataFor(root: string, projects: ConfiguredProjects, indexedCommit: string, indexDurationMs?: number, tsVersion?: string): StoreMetadata;
26
- /** A persisted store can be rehydrated only when its schema, program shape, and checkpoint still match. */
27
- export declare function canReuse(metadata: StoreMetadata | null, expected: StoreMetadata): boolean;
28
- /** Source changes do not invalidate the graph shape; startup rehydrates then performs the ordinary refresh. */
29
- 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;
30
32
  export declare function hashTsymBuild(dist?: string): string;
@@ -20,8 +20,8 @@ export function readMetadata(storePath) {
20
20
  throw error;
21
21
  }
22
22
  }
23
- /** A store written by another schema version, or without readable metadata, cannot be read by this build; it is deleted so the next build starts from nothing. */
24
- export function discardIncompatibleStore(storePath) {
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
25
  if (!existsSync(storePath))
26
26
  return false;
27
27
  let metadata = null;
@@ -31,7 +31,7 @@ export function discardIncompatibleStore(storePath) {
31
31
  catch {
32
32
  metadata = null;
33
33
  }
34
- if (metadata?.schemaVersion === STORE_SCHEMA_VERSION)
34
+ if (metadata !== null && sameFormat(metadata, identity))
35
35
  return false;
36
36
  rmSync(storePath, { recursive: true, force: true });
37
37
  return true;
@@ -45,37 +45,33 @@ export function writeMetadata(storePath, metadata) {
45
45
  writeFileSync(temporary, `${JSON.stringify(metadata)}\n`, { mode: 0o600 });
46
46
  renameSync(temporary, target);
47
47
  }
48
- /** Captures every input whose change can alter the configured-program graph. */
49
- 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()) {
50
52
  const configs = [...new Set(projects.records().map((project) => path.resolve(project.config)))].sort();
51
53
  const shapeConfigs = inheritedConfigs(configs);
52
54
  const files = [...new Set(projects.records().flatMap((project) => [...project.files]))].sort();
53
55
  return {
54
- schemaVersion: STORE_SCHEMA_VERSION,
55
- tsVersion,
56
- tsymBuild: hashTsymBuild(),
56
+ ...identity,
57
+ root,
57
58
  indexedCommit,
58
59
  configSet: configs.map((file) => relative(root, file)),
59
60
  configHashes: Object.fromEntries(shapeConfigs.map((file) => [relative(root, file), hashFile(file)])),
60
61
  packageHash: hashOptional(path.join(root, 'package.json')),
61
62
  lockfileHash: lockfiles.map((name) => `${name}:${hashOptional(path.join(root, name))}`).join('|'),
62
63
  indexDurationMs,
63
- files: Object.fromEntries(files.map((file) => {
64
- const stat = statSync(file);
65
- return [relative(root, file), { size: stat.size, mtimeMs: stat.mtimeMs }];
66
- })),
64
+ files: Object.fromEntries(files.map((file) => [relative(root, file), hashFile(file)])),
67
65
  };
68
66
  }
69
- /** A persisted store can be rehydrated only when its schema, program shape, and checkpoint still match. */
70
- export function canReuse(metadata, expected) {
71
- 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));
72
69
  }
73
- /** Source changes do not invalidate the graph shape; startup rehydrates then performs the ordinary refresh. */
74
- export function hasSourceDrift(metadata, expected) {
75
- 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;
76
72
  }
77
- function shapeOf(metadata) {
78
- const { files: _files, indexDurationMs: _duration, ...shape } = metadata;
73
+ function driftShape(metadata) {
74
+ const { indexedCommit: _commit, indexDurationMs: _duration, ...shape } = metadata;
79
75
  return shape;
80
76
  }
81
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;