@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,344 @@
1
+ /** Swift syntax extraction. Call only in the pinned --liftoff-only child. */
2
+ import { Parser } from 'web-tree-sitter';
3
+ import { buildUtf16ToByteTable, utf16RangeToByteRange } from "./swift-spans.js";
4
+ export const SWIFT_V1 = {
5
+ language: 'swift', languageVersion: 'swift.v1', extensions: ['.swift'],
6
+ exclusions: [
7
+ 'anonymous closures', 'local functions', 'nested functions', 'accessors', 'subscripts',
8
+ 'macro-generated declarations', 'deinitializers', 'shared type propagation', 'effects', 'behavior',
9
+ 'typed throws (pinned grammar reports ERROR)',
10
+ ],
11
+ };
12
+ class Unsupported extends Error {
13
+ }
14
+ class TooLarge extends Error {
15
+ limit;
16
+ constructor(limit) { super(limit); this.limit = limit; }
17
+ }
18
+ function children(node) { return node.children.filter((child) => child !== null); }
19
+ function named(node) { return node.namedChildren.filter((child) => child !== null); }
20
+ function field(node, child) {
21
+ for (let i = 0; i < node.childCount; i++)
22
+ if (node.child(i)?.id === child.id)
23
+ return node.fieldNameForChild(i);
24
+ return null;
25
+ }
26
+ function direct(node, type) { return children(node).find((child) => child.type === type); }
27
+ function tokens(node, source) {
28
+ const result = [];
29
+ const pending = [node];
30
+ while (pending.length) {
31
+ const current = pending.pop();
32
+ if (current.type === 'comment' || current.type === 'multiline_comment')
33
+ continue;
34
+ if (current.type.includes('string_literal') || current.type === 'regex_literal' || current.childCount === 0) {
35
+ result.push(source.slice(current.startIndex, current.endIndex));
36
+ continue;
37
+ }
38
+ const parts = children(current);
39
+ for (let i = parts.length - 1; i >= 0; i--)
40
+ pending.push(parts[i]);
41
+ }
42
+ return result;
43
+ }
44
+ /** Canonical token rendering: trivia is discarded, literal contents are atomic. */
45
+ function renderTokens(ts) {
46
+ let result = '';
47
+ for (let i = 0; i < ts.length; i++) {
48
+ const t = ts[i];
49
+ const previous = ts[i - 1];
50
+ const word = (s) => /[\p{L}\p{N}_$]$/u.test(s);
51
+ const startsWord = (s) => /^[\p{L}\p{N}_$]/u.test(s);
52
+ const operator = (s) => /^[&|+*/%=~^-]+$/.test(s);
53
+ const punctuation = (s) => /^[!$%&*+./:<=>?@^|~-]+$/.test(s);
54
+ const gap = previous !== undefined && ((word(previous) && startsWord(t)) || previous === ':' || previous === ',' ||
55
+ previous === '->' || t === '->' || operator(previous) || operator(t) ||
56
+ (punctuation(previous) && punctuation(t)));
57
+ if (gap)
58
+ result += ' ';
59
+ result += t;
60
+ }
61
+ return result;
62
+ }
63
+ function normalized(node, source) { return renderTokens(tokens(node, source)); }
64
+ function identifier(node, source) {
65
+ const written = normalized(node, source);
66
+ return written.startsWith('`') && written.endsWith('`') ? written.slice(1, -1) : written;
67
+ }
68
+ function writtenType(parts, source, parameter) {
69
+ const syntax = parts.filter((c) => c.type !== 'comment' && c.type !== 'multiline_comment' &&
70
+ !(parameter && (c.type === 'parameter_modifiers' || c.type === '...')));
71
+ const types = syntax.filter((c) => c.isNamed);
72
+ if (types.length < 1 || types.length > 2 ||
73
+ (types.length === 2 && types[0].type !== 'type_modifiers') ||
74
+ types.at(-1).type === 'type_modifiers')
75
+ throw new Unsupported('type without representable syntax');
76
+ const suffix = syntax.filter((c) => !c.isNamed);
77
+ if (suffix.length > 1 || (suffix.length === 1 &&
78
+ (suffix[0].type !== '!' || syntax.at(-1) !== suffix[0])))
79
+ throw new Unsupported('unrepresented type suffix');
80
+ return types.map((c) => normalized(c, source)).join(' ') + (suffix.length ? '!' : '');
81
+ }
82
+ /** The pinned grammar exposes a #if directive as one opaque leaf, so scan its
83
+ * condition into tokens without treating comments or spacing as identity. */
84
+ function guardCondition(raw) {
85
+ const result = [];
86
+ let i = 0;
87
+ while (i < raw.length) {
88
+ if (/\s/u.test(raw[i])) {
89
+ i++;
90
+ continue;
91
+ }
92
+ if (raw.startsWith('//', i))
93
+ break;
94
+ if (raw.startsWith('/*', i)) {
95
+ let depth = 1;
96
+ i += 2;
97
+ while (i < raw.length && depth) {
98
+ if (raw.startsWith('/*', i)) {
99
+ depth++;
100
+ i += 2;
101
+ }
102
+ else if (raw.startsWith('*/', i)) {
103
+ depth--;
104
+ i += 2;
105
+ }
106
+ else
107
+ i++;
108
+ }
109
+ if (depth)
110
+ throw new Unsupported('unclosed directive comment');
111
+ continue;
112
+ }
113
+ const start = i;
114
+ const c = raw[i];
115
+ if (c === '"') {
116
+ i++;
117
+ while (i < raw.length) {
118
+ if (raw[i] === '\\') {
119
+ i += 2;
120
+ continue;
121
+ }
122
+ if (raw[i++] === '"')
123
+ break;
124
+ }
125
+ }
126
+ else if (/[\p{L}\p{N}_]/u.test(c)) {
127
+ i++;
128
+ while (i < raw.length && /[\p{L}\p{N}_]/u.test(raw[i]))
129
+ i++;
130
+ }
131
+ else if (/[!$%&*+./:<=>?@^|~-]/.test(c)) {
132
+ i++;
133
+ while (i < raw.length && /[!$%&*+./:<=>?@^|~-]/.test(raw[i]))
134
+ i++;
135
+ }
136
+ else
137
+ i++;
138
+ result.push(raw.slice(start, i));
139
+ }
140
+ return renderTokens(result);
141
+ }
142
+ function parameter(node, source, position, attributes, defaultNode) {
143
+ const parts = children(node);
144
+ const names = parts.filter((c) => c.type === 'simple_identifier');
145
+ const external = parts.find((c) => field(node, c) === 'external_name');
146
+ const local = names.find((c) => c !== external);
147
+ if (!local)
148
+ throw new Unsupported('parameter without local name');
149
+ const colon = parts.findIndex((c) => c.type === ':');
150
+ if (colon < 0)
151
+ throw new Unsupported('parameter without type');
152
+ const type = writtenType(parts.slice(colon + 1), source, true);
153
+ const modifierNode = direct(node, 'parameter_modifiers');
154
+ const modifiers = [
155
+ ...attributes.map((attribute) => normalized(attribute, source)),
156
+ ...(modifierNode ? named(modifierNode).map((c) => normalized(c, source)) : []),
157
+ ];
158
+ const name = identifier(local, source);
159
+ return {
160
+ position, label: external ? identifier(external, source) : null, name,
161
+ binding: name === '_' ? 'wildcard' : 'identifier',
162
+ type: { state: 'written', text: type },
163
+ optional: false, variadic: Boolean(direct(node, '...')),
164
+ default: defaultNode ? normalized(defaultNode, source) : null,
165
+ modifiers,
166
+ };
167
+ }
168
+ function scopeKind(node) {
169
+ if (node.type === 'protocol_declaration')
170
+ return 'protocol';
171
+ const kind = children(node).find((c) => ['class', 'struct', 'enum', 'extension', 'actor'].includes(c.type));
172
+ if (!kind)
173
+ throw new Unsupported('unknown type scope');
174
+ return kind.type;
175
+ }
176
+ function declaration(node, source, table, scope, guards) {
177
+ const parts = children(node);
178
+ const isInit = node.type === 'init_declaration';
179
+ const nameNode = parts.find((c) => field(node, c) === 'name' &&
180
+ (c.type === 'simple_identifier' || c.type === 'init'));
181
+ if (!nameNode && !isInit)
182
+ throw new Unsupported('function without representable name');
183
+ const name = isInit ? 'init' : identifier(nameNode, source);
184
+ const params = [];
185
+ const pendingAttributes = [];
186
+ let withinParameters = false;
187
+ for (let i = 0; i < parts.length; i++) {
188
+ const child = parts[i];
189
+ if (child.type === '(' && !withinParameters) {
190
+ withinParameters = true;
191
+ continue;
192
+ }
193
+ if (child.type === ')' && withinParameters) {
194
+ if (pendingAttributes.length)
195
+ throw new Unsupported('parameter attribute without parameter');
196
+ withinParameters = false;
197
+ continue;
198
+ }
199
+ if (child.type === 'attribute' && withinParameters) {
200
+ pendingAttributes.push(child);
201
+ continue;
202
+ }
203
+ if (child.type === 'parameter') {
204
+ const next = parts[i + 1];
205
+ const defaultNode = next?.type === '=' ? parts[i + 2] : undefined;
206
+ params.push(parameter(child, source, params.length, pendingAttributes, defaultNode));
207
+ pendingAttributes.length = 0;
208
+ }
209
+ }
210
+ const arrow = parts.findIndex((c) => c.type === '->');
211
+ const endOfReturn = arrow < 0 ? -1 : parts.findIndex((c, i) => i > arrow &&
212
+ (c.type === 'type_constraints' || c.type === 'function_body'));
213
+ const returnType = arrow < 0 ? null : writtenType(parts.slice(arrow + 1, endOfReturn < 0 ? undefined : endOfReturn), source, false);
214
+ const failableNode = isInit ? parts.find((c) => c.type === '?' || c.type === '!' || c.type === 'bang') : undefined;
215
+ const failable = failableNode?.type === 'bang' ? '!' : failableNode?.type;
216
+ const result = isInit ? { kind: 'initializer', failable: failable ?? null } :
217
+ { kind: 'return', type: returnType !== null ? { state: 'written', text: returnType } :
218
+ { state: 'implicit', text: 'Void' } };
219
+ const throwsNode = direct(node, 'throws');
220
+ const throws = { mode: throwsNode?.text === 'rethrows' ? 'rethrows' : throwsNode ? 'throws' : 'none' };
221
+ const modifierNode = direct(node, 'modifiers');
222
+ const genericNode = direct(node, 'type_parameters');
223
+ const constraintNode = direct(node, 'type_constraints');
224
+ const header = {
225
+ modifiers: [
226
+ ...(modifierNode ? children(modifierNode).map((c) => normalized(c, source)) : []),
227
+ ...parts.filter((c) => c.type === 'class' || c.type === 'static').map((c) => normalized(c, source)),
228
+ ...parts.filter((c) => c.type === 'async').map((c) => normalized(c, source)),
229
+ ],
230
+ generic_parameters: genericNode ? named(genericNode).filter((c) => c.type === 'type_parameter').map((c) => normalized(c, source)) : [],
231
+ constraints: constraintNode ? named(constraintNode).filter((c) => c.type === 'type_constraint').map((c) => normalized(c, source)) : [],
232
+ };
233
+ const labels = params.map((p) => `${p.label ?? p.name}:`).join('');
234
+ const displayName = `${scope.length ? `${scope.map((s) => s.name).join('.')}.` : ''}${name}(${labels})`;
235
+ const identity = { kind: isInit ? 'initializer' : scope.length ? 'method' : 'function', scope, name, guards };
236
+ // The grammar omits an optional protocol requirement semicolon from the node.
237
+ // It is still part of the written declaration's source reference.
238
+ const trailing = /^[ \t]*;/.exec(source.slice(node.endIndex));
239
+ const span = utf16RangeToByteRange(table, node.startIndex, node.endIndex + (trailing?.[0].length ?? 0));
240
+ return {
241
+ identity, displayName, span,
242
+ signature: JSON.stringify([params.map(({ position: _position, ...p }) => p), result, throws, header]),
243
+ parameters: params, result, throws, header,
244
+ };
245
+ }
246
+ /** One whole-file parse; any ERROR/MISSING or eligible unsupported syntax refuses all rows. */
247
+ export function extractSwiftSource(language, source, limits = {}) {
248
+ const table = buildUtf16ToByteTable(source);
249
+ if (limits.inputBytes !== undefined && table.bytes[table.length] > limits.inputBytes)
250
+ return { status: 'tooLarge', limit: 'inputBytes' };
251
+ const parser = new Parser();
252
+ let tree = null;
253
+ try {
254
+ parser.setLanguage(language);
255
+ tree = parser.parse(source);
256
+ if (!tree)
257
+ throw new Error('Swift parser returned no tree');
258
+ if (tree.rootNode.hasError)
259
+ return { status: 'incomplete', reason: 'parse-error' };
260
+ let syntaxVisits = 0;
261
+ const stack = [tree.rootNode];
262
+ let malformed = false;
263
+ while (stack.length) {
264
+ const node = stack.pop();
265
+ syntaxVisits++;
266
+ if (limits.syntaxVisits !== undefined && syntaxVisits > limits.syntaxVisits)
267
+ throw new TooLarge('syntaxVisits');
268
+ if (node.isError || node.isMissing)
269
+ malformed = true;
270
+ for (let i = node.childCount - 1; i >= 0; i--) {
271
+ const child = node.child(i);
272
+ if (child)
273
+ stack.push(child);
274
+ }
275
+ }
276
+ if (malformed || tree.rootNode.hasError)
277
+ return { status: 'incomplete', reason: 'parse-error' };
278
+ const declarations = [];
279
+ const visit = (container, scope, guards) => {
280
+ const frames = [];
281
+ for (const child of named(container)) {
282
+ if (child.type === 'directive') {
283
+ const text = child.text.trim();
284
+ const match = /^#(if|elseif|else|endif)\b(?:\s+([\s\S]*))?$/.exec(text);
285
+ if (!match)
286
+ continue; // Nonconditional directives do not change identity.
287
+ const condition = match[2] === undefined ? undefined : guardCondition(match[2]);
288
+ if (match[1] === 'if') {
289
+ if (!condition)
290
+ throw new Unsupported('empty if');
291
+ frames.push({ prior: [condition], active: condition });
292
+ }
293
+ else if (match[1] === 'endif') {
294
+ if (!frames.pop())
295
+ throw new Unsupported('unmatched endif');
296
+ }
297
+ else {
298
+ const frame = frames.at(-1);
299
+ if (!frame)
300
+ throw new Unsupported('unmatched conditional');
301
+ const prior = frame.prior.map((c) => `!(${c})`).join(' && ');
302
+ if (match[1] === 'elseif' && !condition)
303
+ throw new Unsupported('empty elseif');
304
+ frame.active = match[1] === 'else' ? prior : `${prior} && (${condition})`;
305
+ if (match[1] === 'elseif')
306
+ frame.prior.push(condition);
307
+ }
308
+ continue;
309
+ }
310
+ const activeGuards = [...guards, ...frames.map((f) => f.active)];
311
+ if (['function_declaration', 'protocol_function_declaration', 'init_declaration'].includes(child.type)) {
312
+ declarations.push(declaration(child, source, table, scope, activeGuards));
313
+ if (limits.declarations !== undefined && declarations.length > limits.declarations)
314
+ throw new TooLarge('declarations');
315
+ }
316
+ else if (child.type === 'class_declaration' || child.type === 'protocol_declaration') {
317
+ const kind = scopeKind(child);
318
+ if (kind === 'extension' && direct(child, 'type_constraints'))
319
+ throw new Unsupported('constrained extension scope');
320
+ const nameNode = children(child).find((c) => field(child, c) === 'name');
321
+ const body = children(child).find((c) => field(child, c) === 'body');
322
+ if (!nameNode || !body)
323
+ throw new Unsupported('type scope without name or body');
324
+ visit(body, [...scope, { kind, name: identifier(nameNode, source) }], activeGuards);
325
+ }
326
+ }
327
+ if (frames.length)
328
+ throw new Unsupported('unclosed if');
329
+ };
330
+ visit(tree.rootNode, [], []);
331
+ return { status: 'complete', declarations };
332
+ }
333
+ catch (error) {
334
+ if (error instanceof TooLarge)
335
+ return { status: 'tooLarge', limit: error.limit };
336
+ if (error instanceof Unsupported)
337
+ return { status: 'incomplete', reason: 'unsupported-construct' };
338
+ throw error;
339
+ }
340
+ finally {
341
+ tree?.delete();
342
+ parser.delete();
343
+ }
344
+ }
@@ -0,0 +1,62 @@
1
+ /** FD2 parent API. Grammar loading/parsing stays in the liftoff-only child. */
2
+ import { runSwiftParseChild, SwiftChildError } from "./swift-parse.js";
3
+ import { SWIFT_V1 } from "./swift-interface-extract.js";
4
+ import { emitProjectionTrace } from "./projection-trace.js";
5
+ export { SWIFT_V1 };
6
+ export class SwiftExtractCancelled extends Error {
7
+ }
8
+ export class SwiftExtractTimeout extends Error {
9
+ }
10
+ /** Batch sides into one isolated child invocation. Callers may pass absent sides as no entry. */
11
+ export async function extractSwiftSides(sides, options = {}) {
12
+ if (options.signal?.aborted)
13
+ throw new SwiftExtractCancelled('Swift extraction aborted before start');
14
+ const ids = new Set();
15
+ const decoded = [];
16
+ const failures = new Map();
17
+ for (const { id, bytes } of sides) {
18
+ if (ids.has(id))
19
+ throw new Error(`duplicate Swift side id: ${id}`);
20
+ ids.add(id);
21
+ if (options.limits?.inputBytes !== undefined && bytes.byteLength > options.limits.inputBytes) {
22
+ failures.set(id, { status: 'tooLarge', limit: 'inputBytes' });
23
+ continue;
24
+ }
25
+ try {
26
+ const source = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
27
+ decoded.push({ id, source });
28
+ }
29
+ catch {
30
+ failures.set(id, { status: 'incomplete', reason: 'parse-error' });
31
+ }
32
+ }
33
+ if (!decoded.length)
34
+ return failures;
35
+ if (options.trace && options.traceUnitId !== undefined)
36
+ emitProjectionTrace(options.trace, { kind: 'parser-request', unitId: options.traceUnitId, atNs: process.hrtime.bigint() });
37
+ try {
38
+ const result = await runSwiftParseChild({ op: 'extract', sides: decoded, limits: options.limits,
39
+ ...(options.trace ? { traceTimings: true } : {}) }, { signal: options.signal, deadlineMs: options.deadlineMs, trace: options.trace, traceUnitId: options.traceUnitId });
40
+ if (result.op !== 'extract')
41
+ throw new Error('Swift child returned unexpected operation');
42
+ if (options.trace && result.traceTimings
43
+ && Number.isSafeInteger(result.traceTimings.grammarLoadNs) && result.traceTimings.grammarLoadNs >= 0
44
+ && Number.isSafeInteger(result.traceTimings.parseCompareNs) && result.traceTimings.parseCompareNs >= 0) {
45
+ const atNs = process.hrtime.bigint();
46
+ emitProjectionTrace(options.trace, { kind: 'phase', scope: 'swift', phase: 'grammar-load', atNs,
47
+ durationNs: BigInt(result.traceTimings.grammarLoadNs), unitId: options.traceUnitId });
48
+ emitProjectionTrace(options.trace, { kind: 'phase', scope: 'swift', phase: 'parse-compare', atNs,
49
+ durationNs: BigInt(result.traceTimings.parseCompareNs), unitId: options.traceUnitId });
50
+ }
51
+ for (const { id, side } of result.results)
52
+ failures.set(id, side);
53
+ return failures;
54
+ }
55
+ catch (error) {
56
+ if (error instanceof SwiftChildError && /aborted/.test(error.message))
57
+ throw new SwiftExtractCancelled(error.message);
58
+ if (error instanceof SwiftChildError && /deadline/.test(error.message))
59
+ throw new SwiftExtractTimeout(error.message);
60
+ throw error;
61
+ }
62
+ }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * The isolated feasibility child: the ONLY process that loads and parses the
3
+ * Swift grammar. It must be launched with `node --liftoff-only` (see
4
+ * src/swift-grammar.ts for why); the runner (src/swift-parse.ts) does that.
5
+ * It reads one JSON request on stdin, performs one operation, prints one JSON
6
+ * result line on stdout, and exits. Keeping every real Swift load behind this
7
+ * boundary means a stray default-launch parse can never abort the checker, the
8
+ * test runner, or the daemon.
9
+ */
10
+ import { Worker } from 'node:worker_threads';
11
+ import { siblingModuleUrl } from "./sibling-module.js";
12
+ import { readFile } from 'node:fs/promises';
13
+ import { isMainModule } from "./entrypoint.js";
14
+ import { loadSwiftLanguage, parseSwiftSource } from "./swift-grammar.js";
15
+ import { extractSwiftSource } from "./swift-interface-extract.js";
16
+ const WORKER_URL = siblingModuleUrl('swift-parse-worker', import.meta.url);
17
+ async function readStdin() {
18
+ const chunks = [];
19
+ for await (const chunk of process.stdin)
20
+ chunks.push(chunk);
21
+ return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(Buffer.concat(chunks));
22
+ }
23
+ async function readRequest() {
24
+ const [mode, path] = process.argv.slice(2).filter(arg => arg !== '--fd5-trace-startup');
25
+ if (mode === '--parse-file') {
26
+ if (path === undefined)
27
+ throw new Error('--parse-file requires a path');
28
+ return { op: 'parse', source: new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(await readFile(path)) };
29
+ }
30
+ if (mode === '--parse-stdin')
31
+ return { op: 'parse', source: await readStdin() };
32
+ return JSON.parse(await readStdin());
33
+ }
34
+ /** Run one Swift parse in a fresh terminable worker, resolving when the parse
35
+ * finishes (`done`) — or rejecting if the worker errors. Returns a `cancel`
36
+ * that hard-terminates the worker (a synchronous parse cannot be stopped from
37
+ * inside), a `started` promise that resolves when the worker is about to block
38
+ * in the parse, and `finished()`, which reads a shared flag the worker sets the
39
+ * instant the parse returns. Because the parse is synchronous, `started` can
40
+ * only fire immediately before it; the caller proves mid-flight cancellation by
41
+ * reading `finished()` (shared memory, immune to this process's event-loop
42
+ * scheduling), not by trusting `started` alone or by racing a `done` message. */
43
+ export function runInWorker(source, workerUrl = WORKER_URL) {
44
+ const worker = new Worker(workerUrl);
45
+ const progress = new Int32Array(new SharedArrayBuffer(4));
46
+ let onStarted;
47
+ let rejectStarted;
48
+ const started = new Promise((res, reject) => { onStarted = res; rejectStarted = reject; });
49
+ let settled = false;
50
+ const fail = (err) => {
51
+ if (settled)
52
+ return;
53
+ settled = true;
54
+ rejectStarted(err);
55
+ rejectDone(err);
56
+ };
57
+ let rejectDone;
58
+ const done = new Promise((resolve, reject) => {
59
+ rejectDone = reject;
60
+ worker.on('message', (msg) => {
61
+ if (msg.type === 'started')
62
+ onStarted();
63
+ else if (msg.type === 'done') {
64
+ settled = true;
65
+ resolve(msg.result);
66
+ }
67
+ else
68
+ fail(new Error(msg.message));
69
+ });
70
+ worker.on('error', fail);
71
+ worker.on('exit', (code) => {
72
+ if (code !== 0)
73
+ fail(new Error(`Swift parse worker exited before completing (code=${code})`));
74
+ });
75
+ });
76
+ // The cancellation path intentionally does not await `done`; mark its
77
+ // rejection observed while preserving the promise for normal callers.
78
+ void done.catch(() => { });
79
+ worker.postMessage({ source, progress });
80
+ return {
81
+ done,
82
+ started,
83
+ finished: () => Atomics.load(progress, 0) === 1,
84
+ cancel: () => worker.terminate(),
85
+ worker,
86
+ };
87
+ }
88
+ async function main() {
89
+ if (process.argv.includes('--fd5-trace-startup'))
90
+ process.stdout.write('FD5_CHILD_READY\n');
91
+ const request = await readRequest();
92
+ if (request.op === 'cancel-demo') {
93
+ const first = runInWorker(request.pathologicalSource);
94
+ await first.started;
95
+ // Completion guard: a synchronous parse can only be terminated from outside,
96
+ // so `started` is posted just before the parse blocks the worker. Give it a
97
+ // beat to get into the parse, then hard-terminate and read the worker's
98
+ // shared progress flag AFTER teardown completes. The worker stores the flag
99
+ // the instant the parse returns, before any teardown, so once the thread has
100
+ // exited: flag set => the parse finished before it was killed; flag unset =>
101
+ // it was still parsing when terminated. Sampling at the teardown boundary
102
+ // (not before terminate) closes the check-to-terminate window — a parse that
103
+ // completes while the host is descheduled is observed as finished, never
104
+ // mis-reported as interrupted. `inProgressAtCancel:false` means the
105
+ // pathological input finished before the kill landed; the acceptance check
106
+ // treats that as a failure, never a pass. (One irreducible sub-instruction
107
+ // window remains: a parse that returns but is killed before its very next
108
+ // store executes — documented in SWIFT-GRAMMAR.md, inherent to a
109
+ // non-interruptible synchronous parse.)
110
+ await new Promise((res) => setTimeout(res, 150));
111
+ const t = performance.now();
112
+ await first.cancel();
113
+ const terminateMs = performance.now() - t;
114
+ const inProgressAtCancel = !first.finished();
115
+ // The runtime recovers: a replacement worker parses clean input to completion.
116
+ const second = runInWorker(request.cleanSource);
117
+ const result = await second.done;
118
+ await second.worker.terminate();
119
+ print({
120
+ op: 'cancel-demo',
121
+ startedBeforeCancel: true,
122
+ inProgressAtCancel,
123
+ terminateMs,
124
+ replacement: { clean: result.clean, rootType: result.rootType },
125
+ });
126
+ return 0;
127
+ }
128
+ const t0 = performance.now();
129
+ const loaded = await loadSwiftLanguage();
130
+ const initAndLoadMs = performance.now() - t0;
131
+ if (request.op === 'parse') {
132
+ const p0 = performance.now();
133
+ const result = parseSwiftSource(loaded.language, request.source);
134
+ const firstParseMs = performance.now() - p0;
135
+ print({ op: 'parse', provenance: loaded.provenance, result, timings: { initAndLoadMs, firstParseMs } });
136
+ return 0;
137
+ }
138
+ if (request.op === 'extract') {
139
+ const parseStarted = request.traceTimings ? performance.now() : undefined;
140
+ const results = request.sides.map(({ id, source }) => ({ id, side: extractSwiftSource(loaded.language, source, request.limits) }));
141
+ print({ op: 'extract', results,
142
+ ...(parseStarted === undefined ? {} : { traceTimings: {
143
+ grammarLoadNs: Math.round(initAndLoadMs * 1e6),
144
+ parseCompareNs: Math.round((performance.now() - parseStarted) * 1e6)
145
+ } }) });
146
+ return 0;
147
+ }
148
+ if (request.op === 'measure') {
149
+ const parses = request.sources.map(({ label, source }) => {
150
+ const f0 = performance.now();
151
+ const first = parseSwiftSource(loaded.language, source);
152
+ const firstParseMs = performance.now() - f0;
153
+ const w0 = performance.now();
154
+ parseSwiftSource(loaded.language, source);
155
+ const warmParseMs = performance.now() - w0;
156
+ return { label, byteLength: first.byteLength, clean: first.clean, firstParseMs, warmParseMs };
157
+ });
158
+ print({ op: 'measure', provenance: loaded.provenance, initAndLoadMs, parses });
159
+ return 0;
160
+ }
161
+ // op: 'survive' — parse, then stay alive past the observed OOM window and exit
162
+ // 0. A default-launch process aborts during this hold; surviving it is the
163
+ // load-bearing proof that --liftoff-only actually prevents the delayed crash.
164
+ const result = parseSwiftSource(loaded.language, request.source);
165
+ await new Promise((res) => setTimeout(res, request.holdMs));
166
+ print({ op: 'survive', result, heldMs: request.holdMs });
167
+ return 0;
168
+ }
169
+ function print(result) {
170
+ process.stdout.write(JSON.stringify(result) + '\n');
171
+ }
172
+ if (isMainModule(import.meta.url, process.argv[1] ?? '')) {
173
+ main().then((code) => process.stdout.write('', () => process.exit(code)), (err) => { process.stderr.write(`swift-parse-host: ${err.stack ?? err}\n`); process.exit(2); });
174
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * A terminable Swift-parse worker, used only to demonstrate cancellation
3
+ * (STAGE-T-PREREQS 3.4). It inherits `--liftoff-only` from the host that spawns
4
+ * it (src/swift-parse-host.ts), so it can load the grammar without aborting.
5
+ *
6
+ * Protocol: the parent posts `{ source, progress? }`; the worker posts
7
+ * `{ type: 'started' }` immediately before the (synchronous, blocking) parse,
8
+ * then — after the parse returns — writes the shared `progress` flag to 1 and
9
+ * posts `{ type: 'done', result }` (or `{ type: 'error', message }`). A
10
+ * synchronous parse cannot post mid-flight, so `started` fires just before the
11
+ * worker blocks; the parent proves mid-flight cancellation by reading the shared
12
+ * flag (which the worker sets only once the parse has actually finished, immune
13
+ * to the parent's event-loop scheduling), then calling `worker.terminate()` —
14
+ * the clip worker precedent (src/clip-worker-pool.ts).
15
+ */
16
+ import { parentPort } from 'node:worker_threads';
17
+ import { loadSwiftLanguage, parseSwiftSource } from "./swift-grammar.js";
18
+ const port = parentPort;
19
+ if (!port)
20
+ throw new Error('swift-parse-worker must run as a worker thread');
21
+ let loaded;
22
+ port.on('message', async (req) => {
23
+ try {
24
+ loaded ??= await loadSwiftLanguage();
25
+ port.postMessage({ type: 'started' });
26
+ const result = parseSwiftSource(loaded.language, req.source);
27
+ if (req.progress)
28
+ Atomics.store(req.progress, 0, 1);
29
+ port.postMessage({ type: 'done', result });
30
+ }
31
+ catch (err) {
32
+ port.postMessage({ type: 'error', message: err.message });
33
+ }
34
+ });