@playola-radio/slipstream 0.1.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.
Files changed (104) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +132 -0
  3. package/SKILL.md +63 -0
  4. package/THIRD-PARTY-NOTICES.md +36 -0
  5. package/contracts/interface/v2/schema.json +480 -0
  6. package/dist/answer-forwarder.js +58 -0
  7. package/dist/attribution-engine.js +164 -0
  8. package/dist/attribution-producer.js +153 -0
  9. package/dist/attribution-scoring.js +86 -0
  10. package/dist/attribution.js +120 -0
  11. package/dist/blob-source.js +100 -0
  12. package/dist/boundary-registry.js +47 -0
  13. package/dist/cas.js +99 -0
  14. package/dist/change-view.js +201 -0
  15. package/dist/claude-root-transcript.js +110 -0
  16. package/dist/cli.js +629 -0
  17. package/dist/clip-blob-reader.js +104 -0
  18. package/dist/clip-function-parser.js +83 -0
  19. package/dist/clip-language.js +15 -0
  20. package/dist/clip-projection-service.js +240 -0
  21. package/dist/clip-projection-worker.js +18 -0
  22. package/dist/clip-projection.js +585 -0
  23. package/dist/clip-worker-pool.js +118 -0
  24. package/dist/config.js +154 -0
  25. package/dist/control-client.js +98 -0
  26. package/dist/control-protocol.js +72 -0
  27. package/dist/daemon-location.js +33 -0
  28. package/dist/daemon.js +951 -0
  29. package/dist/display-fold.js +332 -0
  30. package/dist/engine.js +112 -0
  31. package/dist/entrypoint.js +17 -0
  32. package/dist/event.js +79 -0
  33. package/dist/evidence-ingest.js +60 -0
  34. package/dist/harness-context/claude.js +42 -0
  35. package/dist/harness-context/codex.js +49 -0
  36. package/dist/harness-context.js +49 -0
  37. package/dist/health.js +65 -0
  38. package/dist/http-reader.js +655 -0
  39. package/dist/http-security.js +50 -0
  40. package/dist/interface-projection.js +243 -0
  41. package/dist/interface-range-resolver.js +230 -0
  42. package/dist/interface-service.js +509 -0
  43. package/dist/interface-ts-pool.js +111 -0
  44. package/dist/interface-ts-worker.js +35 -0
  45. package/dist/interface-v2-comparison.js +135 -0
  46. package/dist/interface-v2-core.js +123 -0
  47. package/dist/interface-v2-typescript.js +448 -0
  48. package/dist/lock.js +197 -0
  49. package/dist/log-reader.js +148 -0
  50. package/dist/log.js +59 -0
  51. package/dist/maintenance.js +255 -0
  52. package/dist/mcp-forwarder.js +161 -0
  53. package/dist/mcp-protocol.js +92 -0
  54. package/dist/platform.js +21 -0
  55. package/dist/projection-admission.js +296 -0
  56. package/dist/projection-trace.js +17 -0
  57. package/dist/public-events.js +27 -0
  58. package/dist/question-hook.js +110 -0
  59. package/dist/questions.js +167 -0
  60. package/dist/reader-runtime.js +38 -0
  61. package/dist/reader.js +93 -0
  62. package/dist/recovery.js +303 -0
  63. package/dist/schema.js +108 -0
  64. package/dist/session.js +866 -0
  65. package/dist/sibling-module.js +10 -0
  66. package/dist/snapshot.js +15 -0
  67. package/dist/storage.js +111 -0
  68. package/dist/store-reader.js +185 -0
  69. package/dist/swift-grammar.js +268 -0
  70. package/dist/swift-interface-extract.js +344 -0
  71. package/dist/swift-interface.js +62 -0
  72. package/dist/swift-parse-host.js +174 -0
  73. package/dist/swift-parse-worker.js +34 -0
  74. package/dist/swift-parse.js +134 -0
  75. package/dist/swift-spans.js +62 -0
  76. package/dist/task-forwarder.js +152 -0
  77. package/dist/transcript/claude.js +199 -0
  78. package/dist/transcript/codex.js +132 -0
  79. package/dist/transcript/discovery.js +365 -0
  80. package/dist/transcript/file-reader.js +128 -0
  81. package/dist/transcript/fs-io.js +287 -0
  82. package/dist/transcript/runner.js +62 -0
  83. package/dist/transcript/steppers.js +30 -0
  84. package/dist/transcript/types.js +30 -0
  85. package/dist/transcript/watcher.js +133 -0
  86. package/dist/tui.js +168 -0
  87. package/package.json +60 -0
  88. package/schemas/projections/clip.v1.json +195 -0
  89. package/schemas/projections/clip.v2.json +212 -0
  90. package/schemas/projections/clip.v3.json +212 -0
  91. package/schemas/slipstream.capture.baseline.completed.v1.json +62 -0
  92. package/schemas/slipstream.capture.gap.v1.json +112 -0
  93. package/schemas/slipstream.change.attribution.v1.json +115 -0
  94. package/schemas/slipstream.enrichment.configured.v1.json +77 -0
  95. package/schemas/slipstream.enrichment.coverage.v1.json +95 -0
  96. package/schemas/slipstream.file.baselined.v1.json +122 -0
  97. package/schemas/slipstream.file.changed.v1.json +179 -0
  98. package/schemas/slipstream.harness.evidence.v1.json +145 -0
  99. package/schemas/slipstream.question.answered.v1.json +29 -0
  100. package/schemas/slipstream.question.dispatch_attempted.v1.json +28 -0
  101. package/schemas/slipstream.question.queued.v1.json +55 -0
  102. package/schemas/slipstream.session.resumed.v1.json +70 -0
  103. package/schemas/slipstream.session.started.v1.json +69 -0
  104. package/schemas/slipstream.task.started.v1.json +72 -0
@@ -0,0 +1,509 @@
1
+ /** Reader-owned, disposable interface.v2 range projection. No events or index. */
2
+ import { webcrypto } from 'node:crypto';
3
+ import { constants } from 'node:fs';
4
+ import { open } from 'node:fs/promises';
5
+ import { blobPath } from "./store-reader.js";
6
+ import { resolveRecordedRange } from "./interface-range-resolver.js";
7
+ import { compareV2 } from "./interface-v2-core.js";
8
+ import { extractSwiftSides, SwiftExtractCancelled, SwiftExtractTimeout } from "./swift-interface.js";
9
+ import { createTypeScriptPool } from "./interface-ts-pool.js";
10
+ import { emitProjectionPhase, emitProjectionTrace } from "./projection-trace.js";
11
+ const FILES_PER_PAGE = 16;
12
+ export const INTERFACE_PAGE_DEADLINE_MS = 10_000;
13
+ const SIDE_BYTES = 1024 * 1024;
14
+ const PAGE_BLOB_BYTES = 8 * 1024 * 1024;
15
+ const FILE_RESULT_BYTES = 512 * 1024;
16
+ const METADATA_BYTES = 64 * 1024;
17
+ export const INTERFACE_SCAN_BUDGET = { records: 100_000, bytes: 16 * 1024 * 1024 };
18
+ const ANALYSIS = { shared_types: 'notAnalyzed', effects: 'notAnalyzed', behavior: 'notAnalyzed' };
19
+ const languageFor = (path) => path.endsWith('.ts') ? { name: 'typescript', version: 'typescript.v2', grammar: 'typescript' }
20
+ : path.endsWith('.tsx') ? { name: 'typescript', version: 'typescript.v2', grammar: 'tsx' }
21
+ : path.endsWith('.swift') ? { name: 'swift', version: 'swift.v1' } : null;
22
+ const tag = (endpoint) => endpoint.kind === 'unknownBoundary' ? 'unknownBoundary'
23
+ : endpoint.snapshot.kind === 'content' ? `content:${endpoint.snapshot.sha256}:${endpoint.snapshot.size}`
24
+ : endpoint.snapshot.kind === 'absent' ? 'absent' : `unavailable:${endpoint.snapshot.reason}`;
25
+ const sideFallback = (endpoint) => {
26
+ if (endpoint.kind === 'unknownBoundary')
27
+ return { state: 'unavailable', reason: 'unknown-boundary' };
28
+ if (endpoint.snapshot.kind === 'absent')
29
+ return { state: 'absent' };
30
+ if (endpoint.snapshot.kind === 'unavailable')
31
+ return { state: 'unavailable', reason: endpoint.snapshot.reason };
32
+ return { state: 'notEvaluated' };
33
+ };
34
+ const statusReason = (coverage, language, compareReason, tooLarge = false) => {
35
+ for (const side of ['before', 'after'])
36
+ if (coverage[side].state === 'incomplete')
37
+ return { status: 'incomplete', fallback_reason: `${side}-${coverage[side].reason}` };
38
+ if (compareReason)
39
+ return { status: 'incomplete', fallback_reason: compareReason };
40
+ for (const side of ['before', 'after'])
41
+ if (coverage[side].state === 'unavailable')
42
+ return { status: 'unavailable', fallback_reason: `${side}-${coverage[side].reason}` };
43
+ if (language === null)
44
+ return { status: 'unsupported', fallback_reason: 'unsupported-language' };
45
+ if (tooLarge)
46
+ return { status: 'skipped', fallback_reason: 'too-large' };
47
+ return { status: 'ready' };
48
+ };
49
+ async function readSide(storeDir, endpoint, limit, trace, traceUnitId, path) {
50
+ const forced = sideFallback(endpoint);
51
+ if (endpoint.kind === 'unknownBoundary' || endpoint.snapshot.kind !== 'content')
52
+ return { coverage: forced, bytes: null, tooLarge: false, readBytes: 0 };
53
+ const readStartedAtNs = trace ? process.hrtime.bigint() : undefined;
54
+ let handle;
55
+ try {
56
+ handle = await open(blobPath(storeDir, endpoint.snapshot.sha256), constants.O_RDONLY | constants.O_NOFOLLOW);
57
+ }
58
+ catch (error) {
59
+ const code = error.code;
60
+ if (code === 'ENOENT' || code === 'ELOOP')
61
+ return { coverage: { state: 'unavailable', reason: 'blob-missing' }, bytes: null, tooLarge: false, readBytes: 0 };
62
+ throw error;
63
+ }
64
+ try {
65
+ const stat = await handle.stat();
66
+ if (!stat.isFile())
67
+ return { coverage: { state: 'unavailable', reason: 'blob-missing' }, bytes: null, tooLarge: false, readBytes: 0 };
68
+ if (stat.size > limit || endpoint.snapshot.size > limit)
69
+ return { coverage: { state: 'notEvaluated' }, bytes: null, tooLarge: true, readBytes: 0 };
70
+ const bytes = await handle.readFile();
71
+ emitProjectionPhase(trace, 'cas-read', readStartedAtNs, { scope: 'interface', unitId: traceUnitId, path });
72
+ const hashStartedAtNs = trace ? process.hrtime.bigint() : undefined;
73
+ const digest = Buffer.from(await webcrypto.subtle.digest('SHA-256', bytes)).toString('hex');
74
+ emitProjectionPhase(trace, 'cas-hash', hashStartedAtNs, { scope: 'interface', unitId: traceUnitId, path });
75
+ if (bytes.length !== endpoint.snapshot.size || digest !== endpoint.snapshot.sha256)
76
+ throw new Error('content-addressed blob does not match recorded snapshot');
77
+ return { coverage: { state: 'notEvaluated' }, bytes, tooLarge: false, readBytes: bytes.length };
78
+ }
79
+ catch (error) {
80
+ const code = error.code;
81
+ if (code === 'ENOENT')
82
+ return { coverage: { state: 'unavailable', reason: 'blob-missing' }, bytes: null, tooLarge: false, readBytes: 0 };
83
+ throw error;
84
+ }
85
+ finally {
86
+ await handle.close();
87
+ }
88
+ }
89
+ async function retainedContent(storeDir, endpoint) {
90
+ if (endpoint.kind !== 'recorded' || endpoint.snapshot.kind !== 'content')
91
+ return true;
92
+ let handle;
93
+ try {
94
+ handle = await open(blobPath(storeDir, endpoint.snapshot.sha256), constants.O_RDONLY | constants.O_NOFOLLOW);
95
+ }
96
+ catch (error) {
97
+ const code = error.code;
98
+ if (code === 'ENOENT' || code === 'ELOOP')
99
+ return false;
100
+ throw error;
101
+ }
102
+ try {
103
+ return (await handle.stat()).isFile();
104
+ }
105
+ finally {
106
+ await handle.close();
107
+ }
108
+ }
109
+ function boundedMetadata(source, max) {
110
+ // Both arrays' brackets and commas are part of their serialized budget.
111
+ let used = 4;
112
+ let gapsComplete = true;
113
+ let scopesComplete = true;
114
+ const gaps = [];
115
+ const scopes = [];
116
+ for (const gap of source.gaps) {
117
+ const size = Buffer.byteLength(JSON.stringify(gap)) + (gaps.length ? 1 : 0);
118
+ if (used + size > max) {
119
+ gapsComplete = false;
120
+ break;
121
+ }
122
+ gaps.push(gap);
123
+ used += size;
124
+ }
125
+ for (const scope of source.inventory.unknownScopes) {
126
+ const size = Buffer.byteLength(JSON.stringify(scope)) + (scopes.length ? 1 : 0);
127
+ if (used + size > max) {
128
+ scopesComplete = false;
129
+ break;
130
+ }
131
+ scopes.push(scope);
132
+ used += size;
133
+ }
134
+ const inventory = { scope: 'observed', baseline_completed_seq: source.inventory.baselineCompletedSeq,
135
+ unknown_scopes: scopes, unknown_scopes_complete: scopesComplete,
136
+ policy_exclusions: source.inventory.policyExclusions };
137
+ return { inventory, gaps, gapsComplete };
138
+ }
139
+ export function createInterfaceService(options) {
140
+ const pool = createTypeScriptPool(options.projectionTrace);
141
+ const cache = new Map();
142
+ let cachedBytes = 0;
143
+ let closed = false;
144
+ const active = new Set();
145
+ const cacheSet = (key, value) => {
146
+ if (!['ready', 'incomplete', 'unsupported'].includes(value.status))
147
+ return;
148
+ if (value.coverage.before.state === 'unavailable' || value.coverage.after.state === 'unavailable')
149
+ return;
150
+ const entry = { value: { status: value.status, fallback_reason: value.fallback_reason,
151
+ coverage: value.coverage, changes: value.changes }, bytes: Buffer.byteLength(JSON.stringify(value)) };
152
+ if (entry.bytes > 16 * 1024 * 1024)
153
+ return;
154
+ const prior = cache.get(key);
155
+ if (prior)
156
+ cachedBytes -= prior.bytes;
157
+ cache.delete(key);
158
+ cache.set(key, entry);
159
+ cachedBytes += entry.bytes;
160
+ while (cache.size > 128 || cachedBytes > 16 * 1024 * 1024) {
161
+ const oldest = cache.keys().next().value;
162
+ if (!oldest)
163
+ break;
164
+ cachedBytes -= cache.get(oldest).bytes;
165
+ cache.delete(oldest);
166
+ }
167
+ };
168
+ const cacheGet = (key) => {
169
+ const found = cache.get(key);
170
+ if (!found)
171
+ return undefined;
172
+ cache.delete(key);
173
+ cache.set(key, found);
174
+ return found.value;
175
+ };
176
+ async function compareFile(file, signal, equalAndRetained, progress, traceUnitId) {
177
+ const language = languageFor(file.path);
178
+ const base = { path: file.path, before: file.before, after: file.after,
179
+ language: language?.name ?? null, language_version: language?.version ?? null,
180
+ status: 'ready', coverage: { before: sideFallback(file.before), after: sideFallback(file.after) }, changes: [] };
181
+ progress.coverage = base.coverage;
182
+ if (equalAndRetained) {
183
+ progress.identical = true;
184
+ base.status = 'identical';
185
+ return { result: base, blobBytes: 0, freshness: 'none' };
186
+ }
187
+ const [before, after] = await Promise.all([
188
+ readSide(options.storeDir, file.before, SIDE_BYTES, options.projectionTrace, traceUnitId, file.path).then(side => {
189
+ base.coverage.before = side.coverage;
190
+ progress.tooLarge ||= side.tooLarge;
191
+ return side;
192
+ }),
193
+ readSide(options.storeDir, file.after, SIDE_BYTES, options.projectionTrace, traceUnitId, file.path).then(side => {
194
+ base.coverage.after = side.coverage;
195
+ progress.tooLarge ||= side.tooLarge;
196
+ return side;
197
+ }),
198
+ ]);
199
+ if (!language) {
200
+ if (base.coverage.before.state === 'notEvaluated')
201
+ base.coverage.before = { state: 'unsupported' };
202
+ if (base.coverage.after.state === 'notEvaluated')
203
+ base.coverage.after = { state: 'unsupported' };
204
+ }
205
+ const blobBytes = before.readBytes + after.readBytes;
206
+ if (signal.aborted)
207
+ throw new Error('interface page cancelled');
208
+ const key = `${tag(file.before)}|${tag(file.after)}|${language?.name ?? 'unsupported'}|${language?.version ?? 'none'}|${language?.grammar ?? 'none'}|interface.v2`;
209
+ if (!before.tooLarge && !after.tooLarge && before.coverage.state !== 'unavailable' && after.coverage.state !== 'unavailable') {
210
+ const hit = cacheGet(key);
211
+ if (hit)
212
+ return { result: { ...base, ...hit }, blobBytes, freshness: 'cache-hit' };
213
+ }
214
+ let freshness = 'none';
215
+ let compareReason;
216
+ let changes = [];
217
+ if (language?.name === 'typescript' && !before.tooLarge && !after.tooLarge &&
218
+ (before.bytes !== null || after.bytes !== null)) {
219
+ if (traceUnitId !== undefined)
220
+ emitProjectionTrace(options.projectionTrace, { kind: 'parser-request', unitId: traceUnitId, atNs: process.hrtime.bigint() });
221
+ const task = pool.run({ language: language.grammar, before: before.bytes, after: after.bytes,
222
+ limits: { inputBytes: SIDE_BYTES, declarations: 4096, syntaxVisits: 100_000 } }, traceUnitId);
223
+ signal.addEventListener('abort', task.cancel, { once: true });
224
+ try {
225
+ const result = await task.promise;
226
+ freshness = result.comparison?.status === 'ready' ? 'fresh' : 'none';
227
+ if (before.bytes !== null)
228
+ base.coverage.before = result.before.status === 'incomplete'
229
+ ? { state: 'incomplete', reason: result.before.reason }
230
+ : result.before.status === 'tooLarge' ? { state: 'notEvaluated' } : { state: 'complete' };
231
+ if (after.bytes !== null)
232
+ base.coverage.after = result.after.status === 'incomplete'
233
+ ? { state: 'incomplete', reason: result.after.reason }
234
+ : result.after.status === 'tooLarge' ? { state: 'notEvaluated' } : { state: 'complete' };
235
+ before.tooLarge ||= result.before.status === 'tooLarge';
236
+ after.tooLarge ||= result.after.status === 'tooLarge';
237
+ progress.tooLarge ||= before.tooLarge || after.tooLarge;
238
+ if (result.comparison?.status === 'incomplete')
239
+ compareReason = result.comparison.fallback_reason;
240
+ else if (result.comparison?.status === 'ready')
241
+ changes = result.comparison.changes;
242
+ }
243
+ finally {
244
+ signal.removeEventListener('abort', task.cancel);
245
+ }
246
+ }
247
+ else if (language?.name === 'swift' && !before.tooLarge && !after.tooLarge &&
248
+ (before.bytes !== null || after.bytes !== null)) {
249
+ const sides = [before.bytes !== null ? { id: 'before', bytes: before.bytes } : null,
250
+ after.bytes !== null ? { id: 'after', bytes: after.bytes } : null].filter((v) => v !== null);
251
+ const results = await (options.extractSwift ?? extractSwiftSides)(sides, { signal,
252
+ limits: { inputBytes: SIDE_BYTES, declarations: 4096, syntaxVisits: 100_000 },
253
+ trace: options.projectionTrace, traceUnitId });
254
+ if (results.size !== sides.length)
255
+ throw new Error('Swift extractor returned unexpected side count');
256
+ for (const side of sides)
257
+ if (!results.has(side.id))
258
+ throw new Error(`Swift extractor omitted ${side.id} side`);
259
+ for (const side of sides) {
260
+ const extracted = results.get(side.id);
261
+ if (extracted.status === 'complete') {
262
+ if (!Array.isArray(extracted.declarations))
263
+ throw new Error('Swift extractor omitted declarations');
264
+ }
265
+ else if (extracted.status === 'incomplete') {
266
+ if (extracted.reason !== 'parse-error' && extracted.reason !== 'unsupported-construct')
267
+ throw new Error('Swift extractor returned unknown incomplete reason');
268
+ }
269
+ else if (extracted.status === 'tooLarge') {
270
+ if (!['inputBytes', 'declarations', 'syntaxVisits'].includes(extracted.limit))
271
+ throw new Error('Swift extractor returned unknown limit');
272
+ }
273
+ else
274
+ throw new Error('Swift extractor returned unknown status');
275
+ }
276
+ for (const side of ['before', 'after']) {
277
+ const extracted = results.get(side);
278
+ if (extracted?.status === 'complete')
279
+ base.coverage[side] = { state: 'complete' };
280
+ else if (extracted?.status === 'incomplete')
281
+ base.coverage[side] = { state: 'incomplete', reason: extracted.reason };
282
+ }
283
+ const left = results.get('before'), right = results.get('after');
284
+ if (left?.status === 'tooLarge' || right?.status === 'tooLarge') {
285
+ before.tooLarge ||= left?.status === 'tooLarge';
286
+ after.tooLarge ||= right?.status === 'tooLarge';
287
+ progress.tooLarge = true;
288
+ }
289
+ else if ((left === undefined || left.status === 'complete') && (right === undefined || right.status === 'complete')) {
290
+ const result = compareV2(left?.status === 'complete' ? left.declarations : [], right?.status === 'complete' ? right.declarations : []);
291
+ freshness = result.status === 'ready' ? 'fresh' : 'none';
292
+ if (result.status === 'incomplete')
293
+ compareReason = result.reason;
294
+ else
295
+ changes = result.changes;
296
+ }
297
+ }
298
+ if (signal.aborted)
299
+ throw new Error('interface page cancelled');
300
+ const disposition = statusReason(base.coverage, base.language, compareReason, before.tooLarge || after.tooLarge);
301
+ Object.assign(base, disposition);
302
+ base.changes = base.status === 'ready' ? changes : [];
303
+ cacheSet(key, base);
304
+ return { result: base, blobBytes, freshness };
305
+ }
306
+ const envelope = (req) => ({
307
+ projection_version: 'interface.v2', session_id: req.sessionId,
308
+ range: { before_seq: req.beforeSeq.toString(), after_seq: req.afterSeq.toString() },
309
+ status: 'ready', inventory: null, analysis: ANALYSIS, gaps: null, gaps_complete: false,
310
+ files: [], page: { complete: false, next_after_path: req.afterPath },
311
+ });
312
+ async function get(req) {
313
+ if (closed)
314
+ return { ...envelope(req), status: 'skipped', fallback_reason: 'cancelled' };
315
+ const abort = new AbortController();
316
+ const onAbort = () => abort.abort();
317
+ req.signal?.addEventListener('abort', onAbort, { once: true });
318
+ if (req.signal?.aborted)
319
+ abort.abort();
320
+ const page = envelope(req);
321
+ let resolved = false;
322
+ let current;
323
+ let currentProgress;
324
+ let sealed = false;
325
+ let failure;
326
+ const interruptPage = (reason) => {
327
+ if (!resolved || page.files.length === 0 && !current) {
328
+ page.status = 'skipped';
329
+ page.fallback_reason = reason;
330
+ page.page = { complete: false, next_after_path: req.afterPath };
331
+ return page;
332
+ }
333
+ if (current) {
334
+ const language = languageFor(current.path);
335
+ const progress = currentProgress;
336
+ const coverage = progress?.coverage ? {
337
+ before: { ...progress.coverage.before }, after: { ...progress.coverage.after },
338
+ } : { before: sideFallback(current.before), after: sideFallback(current.after) };
339
+ const disposition = progress?.identical ? { status: 'identical' }
340
+ : statusReason(coverage, language?.name ?? null, undefined, progress?.tooLarge);
341
+ page.files.push({ path: current.path, before: current.before, after: current.after,
342
+ language: language?.name ?? null, language_version: language?.version ?? null,
343
+ status: disposition.status === 'ready' ? 'skipped' : disposition.status,
344
+ fallback_reason: disposition.status === 'ready' ? reason : disposition.fallback_reason,
345
+ coverage, changes: [] });
346
+ }
347
+ page.status = page.files.some(file => file.status !== 'ready' && file.status !== 'identical') ? 'partial' : 'ready';
348
+ page.page = { complete: false, next_after_path: page.files.at(-1)?.path ?? req.afterPath };
349
+ return page;
350
+ };
351
+ const run = async (traceUnitId) => {
352
+ try {
353
+ const scanStartedAtNs = options.projectionTrace ? process.hrtime.bigint() : undefined;
354
+ const result = await resolveRecordedRange({ logPath: req.logPath, sessionId: req.sessionId,
355
+ durableSeq: req.durableSeq, beforeSeq: req.beforeSeq, afterSeq: req.afterSeq,
356
+ pathPrefix: req.pathPrefix, afterPath: req.afterPath,
357
+ scanBudget: options.scanBudget ?? INTERFACE_SCAN_BUDGET, signal: abort.signal }).finally(() => emitProjectionPhase(options.projectionTrace, 'range-scan', scanStartedAtNs, { scope: 'interface', unitId: traceUnitId, routeKey: req.traceRouteKey }));
358
+ if (sealed)
359
+ return page;
360
+ if (result.kind === 'beyondDurable')
361
+ throw new Error('range exceeded frozen durable boundary');
362
+ if (result.kind === 'scanLimit') {
363
+ page.status = 'skipped';
364
+ page.fallback_reason = 'scan-limit';
365
+ return page;
366
+ }
367
+ if (result.kind === 'aborted')
368
+ return interruptPage('cancelled');
369
+ resolved = true;
370
+ const metadata = boundedMetadata(result, options.metadataBytes ?? METADATA_BYTES);
371
+ page.inventory = metadata.inventory;
372
+ page.gaps = metadata.gaps;
373
+ page.gaps_complete = metadata.gapsComplete;
374
+ let blobBytes = 0;
375
+ let fileBytes = 0;
376
+ let examined = req.afterPath;
377
+ const checkRetention = async (endpoint, path) => {
378
+ const startedAtNs = options.projectionTrace ? process.hrtime.bigint() : undefined;
379
+ try {
380
+ return await retainedContent(options.storeDir, endpoint);
381
+ }
382
+ finally {
383
+ emitProjectionPhase(options.projectionTrace, 'retention-probe', startedAtNs, { scope: 'interface', unitId: traceUnitId, path });
384
+ }
385
+ };
386
+ for (const file of result.files) {
387
+ if (sealed || abort.signal.aborted)
388
+ return sealed ? page : interruptPage('cancelled');
389
+ if (page.files.length >= Math.min(req.limit, FILES_PER_PAGE))
390
+ break;
391
+ const equalAndRetained = file.endpointsEqual && await checkRetention(file.before, file.path);
392
+ if (file.endpointsEqual) {
393
+ const check = options.onRetentionCheck?.(file.path, 'scan');
394
+ if (check)
395
+ await check;
396
+ }
397
+ if (sealed || abort.signal.aborted)
398
+ return sealed ? page : interruptPage('cancelled');
399
+ if (equalAndRetained && !req.includeIdentical) {
400
+ examined = file.path;
401
+ continue;
402
+ }
403
+ const upcoming = equalAndRetained ? 0
404
+ : (file.before.kind === 'recorded' && file.before.snapshot.kind === 'content' ? file.before.snapshot.size : 0)
405
+ + (file.after.snapshot.kind === 'content' ? file.after.snapshot.size : 0);
406
+ if (page.files.length > 0 && blobBytes + upcoming > (options.pageBlobBytes ?? PAGE_BLOB_BYTES))
407
+ break;
408
+ current = file;
409
+ currentProgress = { tooLarge: false, identical: false };
410
+ options.onFileStart?.(file.path);
411
+ const compared = await compareFile(file, abort.signal, equalAndRetained, currentProgress, traceUnitId);
412
+ if (sealed || abort.signal.aborted)
413
+ return sealed ? page : interruptPage('cancelled');
414
+ current = undefined;
415
+ currentProgress = undefined;
416
+ examined = file.path;
417
+ blobBytes += compared.blobBytes;
418
+ if (compared.result.status === 'identical' && !req.includeIdentical)
419
+ continue;
420
+ const budget = options.fileResultBytes ?? FILE_RESULT_BYTES;
421
+ let row = compared.result;
422
+ let size = Buffer.byteLength(JSON.stringify(row));
423
+ if (page.files.length > 0 && fileBytes + size > budget) {
424
+ examined = page.files.at(-1).path;
425
+ break;
426
+ }
427
+ if (page.files.length === 0 && row.status === 'ready' && size > budget) {
428
+ row = { ...row, status: 'skipped', fallback_reason: 'too-large', changes: [],
429
+ coverage: {
430
+ before: row.coverage.before.state === 'complete' ? { state: 'notEvaluated' } : row.coverage.before,
431
+ after: row.coverage.after.state === 'complete' ? { state: 'notEvaluated' } : row.coverage.after,
432
+ } };
433
+ size = Buffer.byteLength(JSON.stringify(row));
434
+ }
435
+ page.files.push(row);
436
+ if (options.projectionTrace && req.traceRouteKey !== undefined) {
437
+ emitProjectionTrace(options.projectionTrace, { kind: 'interface-file', routeKey: req.traceRouteKey,
438
+ path: row.path, atNs: process.hrtime.bigint(), freshness: compared.freshness, resultStatus: row.status });
439
+ }
440
+ fileBytes += size;
441
+ }
442
+ if (sealed || abort.signal.aborted)
443
+ return sealed ? page : interruptPage('cancelled');
444
+ let remaining = false;
445
+ for (const file of result.files) {
446
+ if (file.path <= (examined ?? ''))
447
+ continue;
448
+ if (!file.endpointsEqual || req.includeIdentical) {
449
+ remaining = true;
450
+ break;
451
+ }
452
+ const retained = await checkRetention(file.before, file.path);
453
+ const check = options.onRetentionCheck?.(file.path, 'lookahead');
454
+ if (check)
455
+ await check;
456
+ if (sealed || abort.signal.aborted)
457
+ return sealed ? page : interruptPage('cancelled');
458
+ if (!retained) {
459
+ remaining = true;
460
+ break;
461
+ }
462
+ }
463
+ page.page = { complete: !remaining, next_after_path: remaining ? examined : null };
464
+ page.status = page.files.some(file => file.status !== 'ready' && file.status !== 'identical') ? 'partial' : 'ready';
465
+ return page;
466
+ }
467
+ catch (error) {
468
+ if (sealed)
469
+ return page;
470
+ if (error instanceof SwiftExtractTimeout)
471
+ return interruptPage('timeout');
472
+ if (abort.signal.aborted || error instanceof SwiftExtractCancelled)
473
+ return interruptPage('cancelled');
474
+ throw error;
475
+ }
476
+ };
477
+ // The runner begins only after admission grants the local interface slot.
478
+ // Progress remains outside admission's value, so a timeout retains finished rows.
479
+ const outcome = await options.admission.admit({ workload: 'interface', localConcurrency: 1,
480
+ deadlineMs: options.admissionDeadlineMs ?? INTERFACE_PAGE_DEADLINE_MS,
481
+ signal: req.signal, traceRouteKey: req.traceRouteKey,
482
+ run: traceUnitId => {
483
+ const task = run(traceUnitId).catch(error => { failure = error; throw error; });
484
+ active.add(task);
485
+ void task.finally(() => active.delete(task)).catch(() => { });
486
+ return { promise: task, cancel: () => { sealed = true; abort.abort(); } };
487
+ } });
488
+ req.signal?.removeEventListener('abort', onAbort);
489
+ if (outcome.kind === 'ok')
490
+ return outcome.value;
491
+ sealed = true;
492
+ abort.abort();
493
+ if (outcome.kind === 'error')
494
+ throw failure instanceof Error ? failure : new Error('interface projection failed');
495
+ if (outcome.kind === 'overloaded') {
496
+ page.status = 'skipped';
497
+ page.fallback_reason = 'overloaded';
498
+ return page;
499
+ }
500
+ const reason = outcome.kind === 'timeout' ? 'timeout' : 'cancelled';
501
+ return interruptPage(reason);
502
+ }
503
+ const close = async () => {
504
+ closed = true;
505
+ await pool.close();
506
+ await Promise.allSettled(active);
507
+ };
508
+ return { get, close };
509
+ }
@@ -0,0 +1,111 @@
1
+ import { Worker } from 'node:worker_threads';
2
+ import { siblingModuleUrl } from "./sibling-module.js";
3
+ import { emitProjectionPhase, emitProjectionTrace, traceProcessId } from "./projection-trace.js";
4
+ const WORKER_URL = siblingModuleUrl('interface-ts-worker', import.meta.url);
5
+ /** One persistent parser worker. Cancellation terminates synchronous WASM work. */
6
+ export function createTypeScriptPool(trace) {
7
+ let active = null;
8
+ let serial = 0;
9
+ let closed = false;
10
+ let current = null;
11
+ const retiring = new Set();
12
+ const identities = new WeakMap();
13
+ const retire = (worker, traceUnitId) => {
14
+ const processId = identities.get(worker);
15
+ if (processId !== undefined && traceUnitId !== undefined)
16
+ emitProjectionTrace(trace, { kind: 'process-retire', processId, unitId: traceUnitId, atNs: process.hrtime.bigint() });
17
+ const completion = worker.terminate().catch(() => { });
18
+ retiring.add(completion);
19
+ void completion.finally(() => retiring.delete(completion));
20
+ };
21
+ const spawn = () => {
22
+ const startedAtNs = trace ? process.hrtime.bigint() : undefined;
23
+ const worker = new Worker(WORKER_URL);
24
+ const processId = trace ? traceProcessId() : undefined;
25
+ if (processId !== undefined) {
26
+ identities.set(worker, processId);
27
+ emitProjectionTrace(trace, { kind: 'process-start', processId, process: 'ts-worker', atNs: process.hrtime.bigint() });
28
+ worker.once('online', () => emitProjectionPhase(trace, 'worker-startup', startedAtNs, { scope: 'typescript', processId }));
29
+ }
30
+ worker.on('message', (reply) => {
31
+ const task = active;
32
+ if (!task || task.id !== reply.id)
33
+ return;
34
+ active = null;
35
+ emitProjectionPhase(trace, 'worker-roundtrip', task.startedAtNs, { scope: 'typescript', unitId: task.traceUnitId, processId });
36
+ if (reply.ok) {
37
+ if (reply.result.traceTimings && processId !== undefined) {
38
+ const atNs = process.hrtime.bigint();
39
+ emitProjectionTrace(trace, { kind: 'phase', scope: 'typescript', phase: 'grammar-load', atNs,
40
+ durationNs: reply.result.traceTimings.grammarLoadNs, unitId: task.traceUnitId, processId });
41
+ emitProjectionTrace(trace, { kind: 'phase', scope: 'typescript', phase: 'parse-compare', atNs,
42
+ durationNs: reply.result.traceTimings.parseCompareNs, unitId: task.traceUnitId, processId });
43
+ }
44
+ task.resolve(reply.result);
45
+ }
46
+ else {
47
+ if (worker === current)
48
+ current = null;
49
+ retire(worker, task.traceUnitId);
50
+ task.reject(new Error('TypeScript extraction failed'));
51
+ }
52
+ });
53
+ worker.on('error', (error) => {
54
+ if (worker !== current)
55
+ return; // a retired worker must not fail its replacement's job
56
+ const task = active;
57
+ active = null;
58
+ current = null;
59
+ retire(worker, task?.traceUnitId);
60
+ task?.reject(error);
61
+ });
62
+ worker.on('exit', code => {
63
+ if (processId !== undefined)
64
+ emitProjectionTrace(trace, { kind: 'process-exit', processId, code, atNs: process.hrtime.bigint() });
65
+ if (closed || worker !== current)
66
+ return;
67
+ const task = active;
68
+ active = null;
69
+ current = null;
70
+ task?.reject(new Error(`TypeScript parser worker exited ${code}`));
71
+ });
72
+ return worker;
73
+ };
74
+ const run = (input, traceUnitId) => {
75
+ if (closed || active)
76
+ throw new Error('TypeScript parser worker unavailable');
77
+ current ??= spawn();
78
+ const worker = current;
79
+ const id = ++serial;
80
+ const processId = identities.get(worker);
81
+ if (processId !== undefined && traceUnitId !== undefined)
82
+ emitProjectionTrace(trace, { kind: 'process-use', processId, unitId: traceUnitId, atNs: process.hrtime.bigint() });
83
+ const promise = new Promise((resolve, reject) => {
84
+ active = { id, traceUnitId, startedAtNs: trace ? process.hrtime.bigint() : undefined, resolve, reject };
85
+ worker.postMessage({ ...input, id, ...(trace ? { traceTimings: true } : {}) });
86
+ });
87
+ const cancel = () => {
88
+ const task = active;
89
+ if (!task || task.id !== id)
90
+ return;
91
+ active = null;
92
+ task.reject(new Error('TypeScript extraction cancelled'));
93
+ if (current === worker)
94
+ current = null;
95
+ retire(worker, task.traceUnitId);
96
+ };
97
+ return { promise, cancel };
98
+ };
99
+ const close = async () => {
100
+ if (closed)
101
+ return;
102
+ closed = true;
103
+ const task = active;
104
+ active = null;
105
+ task?.reject(new Error('TypeScript parser worker closed'));
106
+ if (current)
107
+ await current.terminate().catch(() => { });
108
+ await Promise.all(retiring);
109
+ };
110
+ return { run, close };
111
+ }
@@ -0,0 +1,35 @@
1
+ /** CPU-bound TypeScript/TSX extraction runs outside the daemon's capture loop. */
2
+ import { parentPort } from 'node:worker_threads';
3
+ import { createTypeScriptInterfaceExtractor, TypeScriptLimitError } from "./interface-v2-typescript.js";
4
+ import { compareStructuredExtractions } from "./interface-v2-comparison.js";
5
+ if (!parentPort)
6
+ throw new Error('interface-ts-worker must run in a worker thread');
7
+ const port = parentPort;
8
+ port.on('message', (job) => {
9
+ void (async () => {
10
+ const loadStarted = job.traceTimings ? process.hrtime.bigint() : undefined;
11
+ const extract = await createTypeScriptInterfaceExtractor(job.language);
12
+ const parseStarted = job.traceTimings ? process.hrtime.bigint() : undefined;
13
+ const side = (bytes) => {
14
+ if (bytes === null)
15
+ return { status: 'absent' };
16
+ try {
17
+ return extract(bytes, job.limits);
18
+ }
19
+ catch (error) {
20
+ if (error instanceof TypeScriptLimitError)
21
+ return { status: 'tooLarge' };
22
+ throw error;
23
+ }
24
+ };
25
+ const before = side(job.before);
26
+ const after = side(job.after);
27
+ const comparison = before.status === 'tooLarge' || after.status === 'tooLarge'
28
+ ? null : compareStructuredExtractions(before, after);
29
+ return { before, after, comparison,
30
+ ...(loadStarted !== undefined && parseStarted !== undefined ? { traceTimings: {
31
+ grammarLoadNs: parseStarted - loadStarted,
32
+ parseCompareNs: process.hrtime.bigint() - parseStarted
33
+ } } : {}) };
34
+ })().then(result => port.postMessage({ id: job.id, ok: true, result }), () => port.postMessage({ id: job.id, ok: false }));
35
+ });