@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/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 +122 -55
- package/dist/server/wire.d.ts +4 -1
- package/dist/store/load.js +2 -2
- package/dist/store/meta.d.ts +13 -11
- package/dist/store/meta.js +16 -20
- 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 +1 -1
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,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
|
-
|
|
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
|
-
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
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
|
356
|
-
|
|
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
|
-
|
|
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
|
|
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.');
|
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,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
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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;
|
package/dist/store/meta.js
CHANGED
|
@@ -20,8 +20,8 @@ export function readMetadata(storePath) {
|
|
|
20
20
|
throw error;
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
-
/** A store
|
|
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
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
|
78
|
-
const {
|
|
73
|
+
function driftShape(metadata) {
|
|
74
|
+
const { indexedCommit: _commit, indexDurationMs: _duration, ...shape } = metadata;
|
|
79
75
|
return shape;
|
|
80
76
|
}
|
|
81
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;
|