@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.
- package/LICENSE +202 -0
- package/README.md +132 -0
- package/SKILL.md +63 -0
- package/THIRD-PARTY-NOTICES.md +36 -0
- package/contracts/interface/v2/schema.json +480 -0
- package/dist/answer-forwarder.js +58 -0
- package/dist/attribution-engine.js +164 -0
- package/dist/attribution-producer.js +153 -0
- package/dist/attribution-scoring.js +86 -0
- package/dist/attribution.js +120 -0
- package/dist/blob-source.js +100 -0
- package/dist/boundary-registry.js +47 -0
- package/dist/cas.js +99 -0
- package/dist/change-view.js +201 -0
- package/dist/claude-root-transcript.js +110 -0
- package/dist/cli.js +629 -0
- package/dist/clip-blob-reader.js +104 -0
- package/dist/clip-function-parser.js +83 -0
- package/dist/clip-language.js +15 -0
- package/dist/clip-projection-service.js +240 -0
- package/dist/clip-projection-worker.js +18 -0
- package/dist/clip-projection.js +585 -0
- package/dist/clip-worker-pool.js +118 -0
- package/dist/config.js +154 -0
- package/dist/control-client.js +98 -0
- package/dist/control-protocol.js +72 -0
- package/dist/daemon-location.js +33 -0
- package/dist/daemon.js +951 -0
- package/dist/display-fold.js +332 -0
- package/dist/engine.js +112 -0
- package/dist/entrypoint.js +17 -0
- package/dist/event.js +79 -0
- package/dist/evidence-ingest.js +60 -0
- package/dist/harness-context/claude.js +42 -0
- package/dist/harness-context/codex.js +49 -0
- package/dist/harness-context.js +49 -0
- package/dist/health.js +65 -0
- package/dist/http-reader.js +655 -0
- package/dist/http-security.js +50 -0
- package/dist/interface-projection.js +243 -0
- package/dist/interface-range-resolver.js +230 -0
- package/dist/interface-service.js +509 -0
- package/dist/interface-ts-pool.js +111 -0
- package/dist/interface-ts-worker.js +35 -0
- package/dist/interface-v2-comparison.js +135 -0
- package/dist/interface-v2-core.js +123 -0
- package/dist/interface-v2-typescript.js +448 -0
- package/dist/lock.js +197 -0
- package/dist/log-reader.js +148 -0
- package/dist/log.js +59 -0
- package/dist/maintenance.js +255 -0
- package/dist/mcp-forwarder.js +161 -0
- package/dist/mcp-protocol.js +92 -0
- package/dist/platform.js +21 -0
- package/dist/projection-admission.js +296 -0
- package/dist/projection-trace.js +17 -0
- package/dist/public-events.js +27 -0
- package/dist/question-hook.js +110 -0
- package/dist/questions.js +167 -0
- package/dist/reader-runtime.js +38 -0
- package/dist/reader.js +93 -0
- package/dist/recovery.js +303 -0
- package/dist/schema.js +108 -0
- package/dist/session.js +866 -0
- package/dist/sibling-module.js +10 -0
- package/dist/snapshot.js +15 -0
- package/dist/storage.js +111 -0
- package/dist/store-reader.js +185 -0
- package/dist/swift-grammar.js +268 -0
- package/dist/swift-interface-extract.js +344 -0
- package/dist/swift-interface.js +62 -0
- package/dist/swift-parse-host.js +174 -0
- package/dist/swift-parse-worker.js +34 -0
- package/dist/swift-parse.js +134 -0
- package/dist/swift-spans.js +62 -0
- package/dist/task-forwarder.js +152 -0
- package/dist/transcript/claude.js +199 -0
- package/dist/transcript/codex.js +132 -0
- package/dist/transcript/discovery.js +365 -0
- package/dist/transcript/file-reader.js +128 -0
- package/dist/transcript/fs-io.js +287 -0
- package/dist/transcript/runner.js +62 -0
- package/dist/transcript/steppers.js +30 -0
- package/dist/transcript/types.js +30 -0
- package/dist/transcript/watcher.js +133 -0
- package/dist/tui.js +168 -0
- package/package.json +60 -0
- package/schemas/projections/clip.v1.json +195 -0
- package/schemas/projections/clip.v2.json +212 -0
- package/schemas/projections/clip.v3.json +212 -0
- package/schemas/slipstream.capture.baseline.completed.v1.json +62 -0
- package/schemas/slipstream.capture.gap.v1.json +112 -0
- package/schemas/slipstream.change.attribution.v1.json +115 -0
- package/schemas/slipstream.enrichment.configured.v1.json +77 -0
- package/schemas/slipstream.enrichment.coverage.v1.json +95 -0
- package/schemas/slipstream.file.baselined.v1.json +122 -0
- package/schemas/slipstream.file.changed.v1.json +179 -0
- package/schemas/slipstream.harness.evidence.v1.json +145 -0
- package/schemas/slipstream.question.answered.v1.json +29 -0
- package/schemas/slipstream.question.dispatch_attempted.v1.json +28 -0
- package/schemas/slipstream.question.queued.v1.json +55 -0
- package/schemas/slipstream.session.resumed.v1.json +70 -0
- package/schemas/slipstream.session.started.v1.json +69 -0
- package/schemas/slipstream.task.started.v1.json +72 -0
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
/** TypeScript/TSX written function headers for interface.v2. No body inference. */
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import { Language, Parser } from 'web-tree-sitter';
|
|
7
|
+
import { buildUtf16ToByteTable, utf16RangeToByteRange } from "./swift-spans.js";
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
let initialization;
|
|
10
|
+
const grammars = new Map();
|
|
11
|
+
const GRAMMAR_SHA256 = {
|
|
12
|
+
typescript: '8515404dceed38e1ed86aa34b09fcf3379fff1b4ff9dd3967bcd6d1eb5ac3d8f',
|
|
13
|
+
tsx: '6aa3b2c70e76f5d48eafef1093e9c4de383e13f2fdde2f4e9b98a378f6a8f1b6',
|
|
14
|
+
};
|
|
15
|
+
const RUNTIME_VERSION = '0.25.10';
|
|
16
|
+
const RUNTIME_WASM_SHA256 = 'f38dcc4b43b818f9a0785bc1c6d5611a75ac4cdd428ff3f02757c34ca4e46d7f';
|
|
17
|
+
function verifyParserRuntime() {
|
|
18
|
+
const root = dirname(require.resolve('web-tree-sitter'));
|
|
19
|
+
const version = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).version;
|
|
20
|
+
const bytes = readFileSync(join(root, 'tree-sitter.wasm'));
|
|
21
|
+
const actual = createHash('sha256').update(bytes).digest('hex');
|
|
22
|
+
if (version !== RUNTIME_VERSION || actual !== RUNTIME_WASM_SHA256) {
|
|
23
|
+
throw new Error('typescript.v2 parser runtime version or WASM hash mismatch');
|
|
24
|
+
}
|
|
25
|
+
return bytes;
|
|
26
|
+
}
|
|
27
|
+
/** A language_version must never silently select different grammar bytes. */
|
|
28
|
+
export function verifyTypeScriptGrammarArtifact(language, expectedSha = GRAMMAR_SHA256[language]) {
|
|
29
|
+
const path = require.resolve(`tree-sitter-wasms/out/tree-sitter-${language}.wasm`);
|
|
30
|
+
const bytes = readFileSync(path);
|
|
31
|
+
const actual = createHash('sha256').update(bytes).digest('hex');
|
|
32
|
+
if (actual !== expectedSha)
|
|
33
|
+
throw new Error(`${language}.v2 grammar hash mismatch`);
|
|
34
|
+
return bytes;
|
|
35
|
+
}
|
|
36
|
+
export class TypeScriptLimitError extends Error {
|
|
37
|
+
constructor(limit) { super(`${limit} limit exceeded`); }
|
|
38
|
+
}
|
|
39
|
+
export async function createTypeScriptInterfaceExtractor(language) {
|
|
40
|
+
let pending = grammars.get(language);
|
|
41
|
+
if (!pending) {
|
|
42
|
+
pending = (async () => {
|
|
43
|
+
initialization ??= Promise.resolve().then(() => {
|
|
44
|
+
const runtime = verifyParserRuntime();
|
|
45
|
+
return Parser.init({ wasmBinary: runtime });
|
|
46
|
+
}).catch(error => {
|
|
47
|
+
initialization = undefined;
|
|
48
|
+
throw error;
|
|
49
|
+
});
|
|
50
|
+
await initialization;
|
|
51
|
+
return Language.load(verifyTypeScriptGrammarArtifact(language));
|
|
52
|
+
})();
|
|
53
|
+
grammars.set(language, pending);
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const grammar = await pending;
|
|
57
|
+
return (bytes, limits) => extract(bytes, grammar, limits);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (grammars.get(language) === pending)
|
|
61
|
+
grammars.delete(language);
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function child(node, type) {
|
|
66
|
+
return children(node).find(n => n.type === type);
|
|
67
|
+
}
|
|
68
|
+
function children(node) {
|
|
69
|
+
return node.namedChildren.filter((n) => n !== null && n.type !== 'comment');
|
|
70
|
+
}
|
|
71
|
+
function field(node, name) {
|
|
72
|
+
return node.childForFieldName(name) ?? undefined;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* A wrapped function expression (parenthesized, `as`, `satisfies`, legacy `<T>` angle-bracket
|
|
76
|
+
* assertion) is still an unrepresentable function value. This only unwraps those wrapper shells
|
|
77
|
+
* around a value's own top-level expression — it must not descend into a call's arguments or
|
|
78
|
+
* other nested expressions, or every value that merely contains a callback anywhere inside it
|
|
79
|
+
* would be misclassified as itself an unrepresentable function value.
|
|
80
|
+
*/
|
|
81
|
+
function containsWrappedFunction(node) {
|
|
82
|
+
let current = node;
|
|
83
|
+
while (current) {
|
|
84
|
+
if (['arrow_function', 'function_expression', 'generator_function'].includes(current.type))
|
|
85
|
+
return true;
|
|
86
|
+
if (!['parenthesized_expression', 'as_expression', 'satisfies_expression', 'type_assertion'].includes(current.type))
|
|
87
|
+
return false;
|
|
88
|
+
const kids = children(current);
|
|
89
|
+
// type_assertion is `<T>expr` — the type_arguments come first, so the wrapped expression is last.
|
|
90
|
+
current = current.type === 'type_assertion' ? kids[kids.length - 1] : kids[0];
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The grammar omits literal fragments between template type substitutions as nodes; recover
|
|
96
|
+
* them from source gaps. A template-literal substitution can itself contain another template
|
|
97
|
+
* literal type nested arbitrarily deep, so this schedules each substitution's tokenization on
|
|
98
|
+
* the caller's explicit work stack rather than recursing directly — an unbounded recursive
|
|
99
|
+
* depth here would overflow the native call stack. The whole rendered node is pushed back as a
|
|
100
|
+
* single literal token: template-literal-type text is raw source text, not syntax tokens, so it
|
|
101
|
+
* must never pass back through the outer join's spacing rules.
|
|
102
|
+
*/
|
|
103
|
+
function scheduleTemplateLiteralType(node, stack) {
|
|
104
|
+
const work = [{ kind: 'wrap', raw: true }];
|
|
105
|
+
let out = '';
|
|
106
|
+
let cursor = node.startIndex;
|
|
107
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
108
|
+
const c = node.child(i);
|
|
109
|
+
if (!c)
|
|
110
|
+
continue;
|
|
111
|
+
out += node.text.slice(cursor - node.startIndex, c.startIndex - node.startIndex);
|
|
112
|
+
if (c.type === 'template_type') {
|
|
113
|
+
work.push({ kind: 'literal', text: out }, { kind: 'literal', text: '${' }, { kind: 'wrap', raw: false }, { kind: 'node', node: children(c)[0] }, { kind: 'endWrap' }, { kind: 'literal', text: '}' });
|
|
114
|
+
out = '';
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
out += c.text;
|
|
118
|
+
}
|
|
119
|
+
cursor = c.endIndex;
|
|
120
|
+
}
|
|
121
|
+
out += node.text.slice(cursor - node.startIndex);
|
|
122
|
+
work.push({ kind: 'literal', text: out }, { kind: 'endWrap' });
|
|
123
|
+
for (let i = work.length - 1; i >= 0; i--)
|
|
124
|
+
stack.push(work[i]);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Join syntax leaves, never characters: literals remain intact and identifiers cannot merge.
|
|
128
|
+
* A template-literal substitution's own spacing is resolved eagerly at its `wrap` boundary
|
|
129
|
+
* (see `scheduleTemplateLiteralType`) so it becomes one opaque token before the surrounding
|
|
130
|
+
* join runs — the substitution's internal spacing must never leak into the rules around it.
|
|
131
|
+
*/
|
|
132
|
+
function normalizedTokens(node) {
|
|
133
|
+
const tokens = [];
|
|
134
|
+
const wrapAt = [];
|
|
135
|
+
const stack = [{ kind: 'node', node }];
|
|
136
|
+
while (stack.length) {
|
|
137
|
+
const item = stack.pop();
|
|
138
|
+
if (item.kind === 'wrap') {
|
|
139
|
+
wrapAt.push({ start: tokens.length, raw: item.raw });
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (item.kind === 'endWrap') {
|
|
143
|
+
const { start, raw } = wrapAt.pop();
|
|
144
|
+
const segment = tokens.splice(start, tokens.length - start);
|
|
145
|
+
tokens.push(raw ? segment.join('') : joinTokens(segment));
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (item.kind === 'literal') {
|
|
149
|
+
tokens.push(item.text);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const current = item.node;
|
|
153
|
+
if (current.type === 'comment')
|
|
154
|
+
continue;
|
|
155
|
+
if (current.type === 'template_literal_type') {
|
|
156
|
+
scheduleTemplateLiteralType(current, stack);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (current.childCount === 0) {
|
|
160
|
+
tokens.push(current.text);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
for (let i = current.childCount - 1; i >= 0; i--) {
|
|
164
|
+
const next = current.child(i);
|
|
165
|
+
if (next)
|
|
166
|
+
stack.push({ kind: 'node', node: next });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return joinTokens(tokens);
|
|
170
|
+
}
|
|
171
|
+
/** Join syntax leaves, never characters: literals remain intact and identifiers cannot merge. */
|
|
172
|
+
function joinTokens(tokens) {
|
|
173
|
+
let out = '';
|
|
174
|
+
let previous = '';
|
|
175
|
+
const mergeable = new Set(['++', '--', '&&', '||', '??', '==', '!=', '>=', '<=',
|
|
176
|
+
'>>', '<<', '**', '?.', '/*', '//', '=>', '+=', '-=', '*=', '/=',
|
|
177
|
+
'%=', '&=', '|=', '^=']);
|
|
178
|
+
for (const token of tokens) {
|
|
179
|
+
if (!token)
|
|
180
|
+
continue;
|
|
181
|
+
const word = (value) => /[\p{L}\p{N}_$]$/u.test(value);
|
|
182
|
+
const startsWord = (value) => /^[\p{L}\p{N}_$]/u.test(value);
|
|
183
|
+
const space = previous === '{' && token !== '}'
|
|
184
|
+
|| token === '}' && previous !== '{'
|
|
185
|
+
|| previous === ',' || previous === ':'
|
|
186
|
+
|| previous === '|' || previous === '&' || previous === '=' || previous === '=>'
|
|
187
|
+
|| token === '|' || token === '&' || token === '=' || token === '=>'
|
|
188
|
+
|| word(previous) && startsWord(token)
|
|
189
|
+
|| mergeable.has(previous.slice(-1) + token[0]);
|
|
190
|
+
if (space && out && !out.endsWith(' '))
|
|
191
|
+
out += ' ';
|
|
192
|
+
out += token;
|
|
193
|
+
previous = token;
|
|
194
|
+
}
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
function writtenType(annotation) {
|
|
198
|
+
const type = annotation ? children(annotation)[0] : undefined;
|
|
199
|
+
return type ? { state: 'written', text: normalizedTokens(type) }
|
|
200
|
+
: { state: 'unknown', reason: 'inferred-not-computed' };
|
|
201
|
+
}
|
|
202
|
+
function parseParameters(params) {
|
|
203
|
+
if (!params)
|
|
204
|
+
return null;
|
|
205
|
+
const result = [];
|
|
206
|
+
for (const param of children(params)) {
|
|
207
|
+
if (param.type !== 'required_parameter' && param.type !== 'optional_parameter')
|
|
208
|
+
return null;
|
|
209
|
+
const first = children(param).find(n => n.type !== 'type_annotation' && n.type !== 'accessibility_modifier'
|
|
210
|
+
&& n.type !== 'override_modifier'
|
|
211
|
+
&& n.type !== 'number' && n.type !== 'string');
|
|
212
|
+
if (!first)
|
|
213
|
+
return null;
|
|
214
|
+
const rest = first.type === 'rest_pattern';
|
|
215
|
+
const binding = rest ? children(first)[0] : first;
|
|
216
|
+
if (!binding)
|
|
217
|
+
return null;
|
|
218
|
+
const pattern = binding.type === 'object_pattern' || binding.type === 'array_pattern';
|
|
219
|
+
if (!pattern && binding.type !== 'identifier')
|
|
220
|
+
return null;
|
|
221
|
+
const modifiers = [];
|
|
222
|
+
for (let i = 0; i < param.childCount; i++) {
|
|
223
|
+
const token = param.child(i);
|
|
224
|
+
if (token?.type === 'accessibility_modifier' || token?.type === 'readonly'
|
|
225
|
+
|| token?.type === 'override_modifier') {
|
|
226
|
+
modifiers.push(normalizedTokens(token));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const value = field(param, 'value');
|
|
230
|
+
result.push({
|
|
231
|
+
position: result.length, label: null,
|
|
232
|
+
name: pattern ? normalizedTokens(binding) : binding.text,
|
|
233
|
+
binding: pattern ? 'pattern' : 'identifier',
|
|
234
|
+
type: writtenType(field(param, 'type')),
|
|
235
|
+
optional: param.type === 'optional_parameter', variadic: rest,
|
|
236
|
+
default: value ? normalizedTokens(value) : null,
|
|
237
|
+
modifiers,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
function parseDeclaration(node, spanNode, scope, wrapperModifiers, table, bindingName, bindingType) {
|
|
243
|
+
const nameNode = field(node, 'name');
|
|
244
|
+
if (nameNode?.type === 'computed_property_name')
|
|
245
|
+
return null;
|
|
246
|
+
const name = bindingName ?? nameNode?.text;
|
|
247
|
+
if (!name)
|
|
248
|
+
return null;
|
|
249
|
+
let isStatic = false;
|
|
250
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
251
|
+
if (node.child(i)?.type === 'static')
|
|
252
|
+
isStatic = true;
|
|
253
|
+
}
|
|
254
|
+
const constructor = scope.length > 0 && name === 'constructor' && !isStatic;
|
|
255
|
+
const formal = field(node, 'parameters');
|
|
256
|
+
const single = node.type === 'arrow_function' ? field(node, 'parameter') : undefined;
|
|
257
|
+
const params = formal ? parseParameters(formal) : single?.type === 'identifier' ? [{
|
|
258
|
+
position: 0, label: null, name: single.text, binding: 'identifier',
|
|
259
|
+
type: { state: 'unknown', reason: 'inferred-not-computed' },
|
|
260
|
+
optional: false, variadic: false, default: null, modifiers: [],
|
|
261
|
+
}] : null;
|
|
262
|
+
if (!params)
|
|
263
|
+
return null;
|
|
264
|
+
let returnType = writtenType(field(node, 'return_type'));
|
|
265
|
+
let boundGenerics = [];
|
|
266
|
+
if (bindingType) {
|
|
267
|
+
const functionType = children(bindingType)[0];
|
|
268
|
+
if (!functionType || functionType.type !== 'function_type')
|
|
269
|
+
return null;
|
|
270
|
+
const typeParams = parseParameters(field(functionType, 'parameters'));
|
|
271
|
+
const typeResult = field(functionType, 'return_type');
|
|
272
|
+
if (!typeParams || typeParams.length !== params.length || !typeResult)
|
|
273
|
+
return null;
|
|
274
|
+
for (let i = 0; i < params.length; i++) {
|
|
275
|
+
const written = typeParams[i].type;
|
|
276
|
+
if (params[i].type.state === 'written' && JSON.stringify(params[i].type) !== JSON.stringify(written))
|
|
277
|
+
return null;
|
|
278
|
+
params[i].type = written;
|
|
279
|
+
params[i].optional = typeParams[i].optional;
|
|
280
|
+
params[i].variadic = typeParams[i].variadic;
|
|
281
|
+
}
|
|
282
|
+
const boundResult = { state: 'written', text: normalizedTokens(typeResult) };
|
|
283
|
+
if (returnType.state === 'written' && JSON.stringify(returnType) !== JSON.stringify(boundResult))
|
|
284
|
+
return null;
|
|
285
|
+
returnType = boundResult;
|
|
286
|
+
const boundGenericNode = child(functionType, 'type_parameters');
|
|
287
|
+
boundGenerics = boundGenericNode ? children(boundGenericNode)
|
|
288
|
+
.filter(n => n.type === 'type_parameter').map(normalizedTokens) : [];
|
|
289
|
+
}
|
|
290
|
+
const modifierNodes = [];
|
|
291
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
292
|
+
const c = node.child(i);
|
|
293
|
+
if (!c || c.startIndex >= (field(node, 'parameters')?.startIndex ?? node.endIndex))
|
|
294
|
+
break;
|
|
295
|
+
if (['async', 'static', 'abstract', 'override_modifier', 'readonly', 'accessibility_modifier',
|
|
296
|
+
'declare', 'default', 'generator', '*', '?'].includes(c.type))
|
|
297
|
+
modifierNodes.push(normalizedTokens(c));
|
|
298
|
+
}
|
|
299
|
+
const genericNode = child(node, 'type_parameters');
|
|
300
|
+
const inlineGenerics = genericNode ? children(genericNode).filter(n => n.type === 'type_parameter').map(normalizedTokens) : [];
|
|
301
|
+
if (inlineGenerics.length && boundGenerics.length && JSON.stringify(inlineGenerics) !== JSON.stringify(boundGenerics))
|
|
302
|
+
return null;
|
|
303
|
+
const generics = inlineGenerics.length ? inlineGenerics : boundGenerics;
|
|
304
|
+
const kind = constructor ? 'constructor' : scope.length ? 'method' : 'function';
|
|
305
|
+
return {
|
|
306
|
+
identity: { kind, scope, name, guards: [] },
|
|
307
|
+
displayName: scope.length ? `${scope.map(s => s.name).join('.')}.${name}` : name,
|
|
308
|
+
span: utf16RangeToByteRange(table, spanNode.startIndex, (node.type === 'method_signature' || node.type === 'abstract_method_signature') && spanNode.nextSibling?.type === ';'
|
|
309
|
+
? spanNode.nextSibling.endIndex : spanNode.endIndex),
|
|
310
|
+
parameters: params,
|
|
311
|
+
result: constructor ? null : { kind: 'return', type: returnType },
|
|
312
|
+
throws: { mode: 'notExpressible' },
|
|
313
|
+
header: { modifiers: [...wrapperModifiers, ...modifierNodes], generic_parameters: generics, constraints: [] },
|
|
314
|
+
role: node.type.includes('signature') ? 'signature' : 'implementation',
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function extract(bytes, grammar, limits) {
|
|
318
|
+
if (limits?.inputBytes !== undefined && bytes.byteLength > limits.inputBytes)
|
|
319
|
+
throw new TypeScriptLimitError('inputBytes');
|
|
320
|
+
let source;
|
|
321
|
+
try {
|
|
322
|
+
source = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
return { status: 'incomplete', reason: 'parse-error' };
|
|
326
|
+
}
|
|
327
|
+
const parser = new Parser();
|
|
328
|
+
let tree = null;
|
|
329
|
+
try {
|
|
330
|
+
parser.setLanguage(grammar);
|
|
331
|
+
tree = parser.parse(source);
|
|
332
|
+
if (!tree || tree.rootNode.hasError)
|
|
333
|
+
return { status: 'incomplete', reason: 'parse-error' };
|
|
334
|
+
if (limits?.syntaxVisits !== undefined) {
|
|
335
|
+
let visits = 0;
|
|
336
|
+
const pending = [tree.rootNode];
|
|
337
|
+
while (pending.length) {
|
|
338
|
+
const node = pending.pop();
|
|
339
|
+
if (++visits > limits.syntaxVisits)
|
|
340
|
+
throw new TypeScriptLimitError('syntaxVisits');
|
|
341
|
+
for (let i = node.childCount - 1; i >= 0; i--) {
|
|
342
|
+
const next = node.child(i);
|
|
343
|
+
if (next)
|
|
344
|
+
pending.push(next);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const table = buildUtf16ToByteTable(source);
|
|
349
|
+
const declarations = [];
|
|
350
|
+
const add = (n, span, scope, mods, binding, bindingType) => {
|
|
351
|
+
const parsed = parseDeclaration(n, span, scope, mods, table, binding, bindingType);
|
|
352
|
+
if (!parsed)
|
|
353
|
+
return false;
|
|
354
|
+
declarations.push(parsed);
|
|
355
|
+
if (limits?.declarations !== undefined && declarations.length > limits.declarations)
|
|
356
|
+
throw new TypeScriptLimitError('declarations');
|
|
357
|
+
return true;
|
|
358
|
+
};
|
|
359
|
+
const scan = (node, span = node, mods = []) => {
|
|
360
|
+
if (node.type === 'export_statement') {
|
|
361
|
+
const inner = children(node).find(n => ['function_declaration', 'function_signature',
|
|
362
|
+
'generator_function_declaration', 'class_declaration', 'abstract_class_declaration',
|
|
363
|
+
'lexical_declaration', 'variable_declaration', 'ambient_declaration'].includes(n.type));
|
|
364
|
+
const exportModifiers = [];
|
|
365
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
366
|
+
const token = node.child(i);
|
|
367
|
+
if (token?.type === 'export' || token?.type === 'default')
|
|
368
|
+
exportModifiers.push(token.text);
|
|
369
|
+
}
|
|
370
|
+
if (!inner && children(node).some(n => ['class', 'class_expression', 'module',
|
|
371
|
+
'internal_module', 'statement_block', 'expression_statement'].includes(n.type)))
|
|
372
|
+
return false;
|
|
373
|
+
return !inner || scan(inner, node, [...exportModifiers, ...mods]);
|
|
374
|
+
}
|
|
375
|
+
if (node.type === 'ambient_declaration') {
|
|
376
|
+
const inner = children(node)[0];
|
|
377
|
+
if (inner && ['module', 'internal_module', 'statement_block'].includes(inner.type))
|
|
378
|
+
return false;
|
|
379
|
+
return !inner || scan(inner, span, [...mods, 'declare']);
|
|
380
|
+
}
|
|
381
|
+
if (['function_declaration', 'function_signature', 'generator_function_declaration'].includes(node.type)) {
|
|
382
|
+
return add(node, span, [], mods);
|
|
383
|
+
}
|
|
384
|
+
if (node.type === 'class_declaration' || node.type === 'abstract_class_declaration') {
|
|
385
|
+
const name = field(node, 'name');
|
|
386
|
+
const body = field(node, 'body');
|
|
387
|
+
if (!name || !body)
|
|
388
|
+
return false;
|
|
389
|
+
const scope = [{ kind: 'class', name: name.text }];
|
|
390
|
+
for (const member of children(body)) {
|
|
391
|
+
if (member.type === 'method_definition' || member.type === 'method_signature'
|
|
392
|
+
|| member.type === 'abstract_method_signature') {
|
|
393
|
+
let accessor = false;
|
|
394
|
+
for (let i = 0; i < member.childCount; i++) {
|
|
395
|
+
const token = member.child(i);
|
|
396
|
+
if (token?.type === 'get' || token?.type === 'set')
|
|
397
|
+
accessor = true;
|
|
398
|
+
}
|
|
399
|
+
if (accessor)
|
|
400
|
+
continue;
|
|
401
|
+
if (!add(member, member, scope, []))
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
else if (member.type === 'public_field_definition') {
|
|
405
|
+
const value = field(member, 'value');
|
|
406
|
+
if (value && containsWrappedFunction(value))
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
if (node.type === 'lexical_declaration' || node.type === 'variable_declaration') {
|
|
413
|
+
const declarators = children(node).filter(n => n.type === 'variable_declarator');
|
|
414
|
+
for (const declarator of declarators) {
|
|
415
|
+
const value = field(declarator, 'value');
|
|
416
|
+
if (value?.type === 'class' || value?.type === 'class_expression')
|
|
417
|
+
return false;
|
|
418
|
+
if (value && ['as_expression', 'satisfies_expression', 'parenthesized_expression', 'type_assertion'].includes(value.type)) {
|
|
419
|
+
if (containsWrappedFunction(value))
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
if (!value || !['arrow_function', 'function_expression', 'generator_function'].includes(value.type))
|
|
423
|
+
continue;
|
|
424
|
+
const name = field(declarator, 'name');
|
|
425
|
+
if (!name || name.type !== 'identifier' || declarators.length !== 1)
|
|
426
|
+
return false;
|
|
427
|
+
const keyword = node.child(0)?.text;
|
|
428
|
+
if (!add(value, span, [], [...mods, ...(keyword ? [keyword] : [])], name.text, field(declarator, 'type')))
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
if (node.type === 'module' || node.type === 'internal_module'
|
|
434
|
+
|| node.type === 'expression_statement' && children(node).some(n => n.type === 'internal_module'))
|
|
435
|
+
return false;
|
|
436
|
+
return true;
|
|
437
|
+
};
|
|
438
|
+
for (const node of children(tree.rootNode)) {
|
|
439
|
+
if (!scan(node))
|
|
440
|
+
return { status: 'incomplete', reason: 'unsupported-construct' };
|
|
441
|
+
}
|
|
442
|
+
return { status: 'complete', declarations };
|
|
443
|
+
}
|
|
444
|
+
finally {
|
|
445
|
+
tree?.delete();
|
|
446
|
+
parser.delete();
|
|
447
|
+
}
|
|
448
|
+
}
|
package/dist/lock.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { link, lstat, open, readFile, rename, unlink, utimes } from 'node:fs/promises';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { FILE_MODE } from "./storage.js";
|
|
5
|
+
export class SessionOwnedError extends Error {
|
|
6
|
+
constructor(pid) {
|
|
7
|
+
super(`session is already owned by a live process (pid ${pid})`);
|
|
8
|
+
this.name = 'SessionOwnedError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function isAlive(pid) {
|
|
12
|
+
try {
|
|
13
|
+
process.kill(pid, 0);
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
// ESRCH: no such process (stale). EPERM: exists but not ours (alive).
|
|
18
|
+
return err.code === 'EPERM';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** Parse a lock file. Tolerant of a foreign/legacy bare-integer pid. */
|
|
22
|
+
function parseLock(text) {
|
|
23
|
+
try {
|
|
24
|
+
const obj = JSON.parse(text);
|
|
25
|
+
if (obj && typeof obj === 'object') {
|
|
26
|
+
const pid = typeof obj.pid === 'number' && Number.isInteger(obj.pid) && obj.pid > 0 ? obj.pid : undefined;
|
|
27
|
+
const nonce = typeof obj.nonce === 'string' && obj.nonce.length > 0 ? obj.nonce : undefined;
|
|
28
|
+
return { pid, nonce };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// not JSON: fall through to a bare-pid read
|
|
33
|
+
}
|
|
34
|
+
const pid = Number.parseInt(text, 10);
|
|
35
|
+
return { pid: Number.isInteger(pid) && pid > 0 ? pid : undefined };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Publish `content` at `lockPath` atomically: write+fsync a temp file, then
|
|
39
|
+
* hardlink it into place. `link` fails EEXIST if the lock is held, and — unlike
|
|
40
|
+
* open()+write — the visible lock is never observed empty, so no racing process
|
|
41
|
+
* can mistake a half-created lock for a stale one. Returns false on EEXIST.
|
|
42
|
+
*/
|
|
43
|
+
async function publish(lockPath, content) {
|
|
44
|
+
const tmp = `${lockPath}.${randomUUID()}.tmp`;
|
|
45
|
+
const handle = await open(tmp, 'wx', FILE_MODE);
|
|
46
|
+
try {
|
|
47
|
+
await handle.writeFile(content);
|
|
48
|
+
await handle.sync();
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
await handle.close();
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
await link(tmp, lockPath);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
if (err.code === 'EEXIST')
|
|
59
|
+
return false;
|
|
60
|
+
throw err;
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
await unlink(tmp).catch(() => { });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Refresh cadence and the staleness horizon it must stay comfortably under. */
|
|
67
|
+
const HEARTBEAT_MS = 2_000;
|
|
68
|
+
/**
|
|
69
|
+
* A lock unrefreshed for this long is abandoned. Six heartbeats of slack keeps a
|
|
70
|
+
* live owner from being declared stale by a GC/scheduling pause, while still
|
|
71
|
+
* reclaiming a hung owner promptly enough for a supervisor restart.
|
|
72
|
+
*/
|
|
73
|
+
const STALE_MS = 12_000;
|
|
74
|
+
const MAX_ACQUIRE_ATTEMPTS = 8;
|
|
75
|
+
export async function acquireSessionLock(sessionDir, options = {}) {
|
|
76
|
+
const lockPath = join(sessionDir, 'owner.lock');
|
|
77
|
+
const myNonce = randomUUID();
|
|
78
|
+
const content = JSON.stringify({ pid: process.pid, nonce: myNonce });
|
|
79
|
+
for (let attempt = 0; attempt < MAX_ACQUIRE_ATTEMPTS; attempt++) {
|
|
80
|
+
if (await publish(lockPath, content))
|
|
81
|
+
return makeLock(lockPath, myNonce, content, options.onCompromised);
|
|
82
|
+
let st;
|
|
83
|
+
try {
|
|
84
|
+
st = await lstat(lockPath);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
if (err.code === 'ENOENT')
|
|
88
|
+
continue; // vanished; retry
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
91
|
+
const cur = parseLock(await readFile(lockPath, 'utf8').catch(() => ''));
|
|
92
|
+
const stale = Date.now() - st.mtimeMs > STALE_MS;
|
|
93
|
+
if (cur.pid !== undefined && isAlive(cur.pid) && !stale)
|
|
94
|
+
throw new SessionOwnedError(cur.pid);
|
|
95
|
+
// Reclaim only a provably-abandoned lock: a dead pid on this host, or one no
|
|
96
|
+
// heartbeat has refreshed within STALE_MS. A fresh, unparseable lock is
|
|
97
|
+
// foreign — fail closed (exhaust attempts) rather than delete it.
|
|
98
|
+
const reclaimable = (cur.pid !== undefined && !isAlive(cur.pid)) || stale;
|
|
99
|
+
if (!reclaimable)
|
|
100
|
+
continue;
|
|
101
|
+
// Break it without an ABA hole in the common (no live owner) case: rename it
|
|
102
|
+
// aside — atomic, single-winner — then confirm the file we grabbed is exactly
|
|
103
|
+
// the one we observed (same inode AND mtime; a refresh or a concurrent
|
|
104
|
+
// republish changes one of them). If it changed under us, a live owner has
|
|
105
|
+
// reclaimed it since our stat: restore it without overwriting a newer holder.
|
|
106
|
+
// A live owner whose lock we transiently vacate re-asserts it via its own
|
|
107
|
+
// heartbeat, so this path never permanently unseats a healthy owner.
|
|
108
|
+
const grabbed = `${lockPath}.reclaiming.${randomUUID()}`;
|
|
109
|
+
try {
|
|
110
|
+
await rename(lockPath, grabbed);
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
if (err.code === 'ENOENT')
|
|
114
|
+
continue; // lost the race; retry
|
|
115
|
+
throw err;
|
|
116
|
+
}
|
|
117
|
+
let gst;
|
|
118
|
+
try {
|
|
119
|
+
gst = await lstat(grabbed);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
await unlink(grabbed).catch(() => { });
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (gst.ino === st.ino && gst.mtimeMs === st.mtimeMs) {
|
|
126
|
+
await unlink(grabbed).catch(() => { }); // exactly the observed stale lock: discard it
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
await link(grabbed, lockPath).catch(() => { }); // changed under us: put it back (no overwrite)
|
|
130
|
+
await unlink(grabbed).catch(() => { });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
throw new Error('could not acquire session lock after repeated stale-lock reclaims');
|
|
134
|
+
}
|
|
135
|
+
function makeLock(lockPath, myNonce, content, onCompromised) {
|
|
136
|
+
let done = false; // released or compromised: the heartbeat must stop acting
|
|
137
|
+
let beating = false; // a beat is in flight; ticks do not overlap
|
|
138
|
+
let inFlight = Promise.resolve(); // the in-flight beat, so release can await it
|
|
139
|
+
const surrender = (reason) => {
|
|
140
|
+
if (done)
|
|
141
|
+
return;
|
|
142
|
+
done = true;
|
|
143
|
+
clearInterval(timer);
|
|
144
|
+
onCompromised?.(reason);
|
|
145
|
+
};
|
|
146
|
+
const beat = async () => {
|
|
147
|
+
if (done)
|
|
148
|
+
return;
|
|
149
|
+
let text;
|
|
150
|
+
try {
|
|
151
|
+
text = await readFile(lockPath, 'utf8');
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
if (err.code !== 'ENOENT')
|
|
155
|
+
return; // transient; retry next tick
|
|
156
|
+
// Our path is momentarily vacant: a reclaimer renamed it aside, or it was
|
|
157
|
+
// removed. Re-assert ownership rather than surrender on a blip. If another
|
|
158
|
+
// process now holds it, our re-publish fails EEXIST and we check its nonce.
|
|
159
|
+
try {
|
|
160
|
+
if (await publish(lockPath, content))
|
|
161
|
+
return; // re-established our lock
|
|
162
|
+
text = await readFile(lockPath, 'utf8');
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return; // transient; retry next tick
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (parseLock(text).nonce !== myNonce) {
|
|
169
|
+
return surrender('lock was taken over by another process');
|
|
170
|
+
}
|
|
171
|
+
const now = Date.now() / 1000;
|
|
172
|
+
await utimes(lockPath, now, now).catch(() => { }); // transient; the next tick retries
|
|
173
|
+
};
|
|
174
|
+
const timer = setInterval(() => {
|
|
175
|
+
if (beating || done)
|
|
176
|
+
return;
|
|
177
|
+
beating = true;
|
|
178
|
+
inFlight = beat().finally(() => { beating = false; });
|
|
179
|
+
}, HEARTBEAT_MS);
|
|
180
|
+
timer.unref?.();
|
|
181
|
+
return {
|
|
182
|
+
release: async () => {
|
|
183
|
+
done = true;
|
|
184
|
+
clearInterval(timer);
|
|
185
|
+
// Await any in-flight heartbeat first: one whose read is mid-flight could
|
|
186
|
+
// otherwise re-publish the lock (ENOENT re-assert) *after* we unlink it,
|
|
187
|
+
// leaving an orphan holding our exiting pid. Letting it finish means the
|
|
188
|
+
// nonce check below still owns whatever it left behind.
|
|
189
|
+
await inFlight.catch(() => { });
|
|
190
|
+
// Remove the lock only if it still carries OUR nonce — never delete a
|
|
191
|
+
// successor's lock if we were quietly dispossessed before release.
|
|
192
|
+
const cur = parseLock(await readFile(lockPath, 'utf8').catch(() => ''));
|
|
193
|
+
if (cur.nonce === myNonce)
|
|
194
|
+
await unlink(lockPath).catch(() => { });
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|