@haystackeditor/cli 0.25.1 → 0.26.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 (97) hide show
  1. package/README.md +39 -455
  2. package/dist/capture/app-config.js +25 -1
  3. package/dist/commands/capture-brief.js +41 -32
  4. package/dist/commands/feedback.js +66 -0
  5. package/dist/commands/init-telemetry.js +53 -7
  6. package/dist/commands/init.js +7 -2
  7. package/dist/commands/lockfile-pin.js +307 -0
  8. package/dist/commands/verify.js +68 -13
  9. package/dist/index.js +27 -923
  10. package/dist/schema.js +4 -10
  11. package/dist/utils/haystack-api.js +0 -36
  12. package/package.json +1 -5
  13. package/schemas/feedback.v1.json +13 -0
  14. package/schemas/pre-verify.v2.json +240 -0
  15. package/schemas/verify-raw.v1.json +1132 -0
  16. package/schemas/verify.v2.json +655 -0
  17. package/dist/assets/hooks/agent-context/detect.ts +0 -316
  18. package/dist/assets/hooks/agent-context/format.ts +0 -100
  19. package/dist/assets/hooks/agent-context/index.ts +0 -41
  20. package/dist/assets/hooks/agent-context/parsers/claude.ts +0 -262
  21. package/dist/assets/hooks/agent-context/parsers/codex.ts +0 -416
  22. package/dist/assets/hooks/agent-context/parsers/gemini.ts +0 -155
  23. package/dist/assets/hooks/agent-context/parsers/opencode.ts +0 -174
  24. package/dist/assets/hooks/agent-context/tsconfig.json +0 -14
  25. package/dist/assets/hooks/agent-context/types.ts +0 -58
  26. package/dist/assets/hooks/llm-rules-template.md +0 -59
  27. package/dist/assets/hooks/package-lock.json +0 -598
  28. package/dist/assets/hooks/package.json +0 -12
  29. package/dist/assets/hooks/scripts/commit-msg.sh +0 -5
  30. package/dist/assets/hooks/scripts/post-commit.sh +0 -5
  31. package/dist/assets/hooks/scripts/pre-commit.sh +0 -175
  32. package/dist/assets/hooks/scripts/pre-push.sh +0 -25
  33. package/dist/assets/hooks/scripts/prepare-commit-msg.sh +0 -5
  34. package/dist/assets/hooks/truncation-checker/ast-analyzer.ts +0 -528
  35. package/dist/assets/hooks/truncation-checker/index.ts +0 -595
  36. package/dist/assets/hooks/truncation-checker/tsconfig.json +0 -13
  37. package/dist/assets/skills/map-cloud-verifier-universe/SKILL.md +0 -2051
  38. package/dist/assets/skills/map-cloud-verifier-universe/agents/openai.yaml +0 -4
  39. package/dist/assets/skills/map-cloud-verifier-universe/references/output-contract.md +0 -3411
  40. package/dist/assets/skills/map-your-system.md +0 -143
  41. package/dist/assets/skills/submit.md +0 -200
  42. package/dist/commands/ask.js +0 -20
  43. package/dist/commands/cloud-verifier-behaviors.js +0 -218
  44. package/dist/commands/cloud-verifier-data-store-census.js +0 -539
  45. package/dist/commands/cloud-verifier-data-store-drift.js +0 -158
  46. package/dist/commands/cloud-verifier-identity-census.js +0 -4060
  47. package/dist/commands/cloud-verifier-materialization.js +0 -704
  48. package/dist/commands/cloud-verifier-pascal-selector-census.js +0 -1382
  49. package/dist/commands/cloud-verifier-python-manifest-selector-census.js +0 -2015
  50. package/dist/commands/cloud-verifier-specialized-operational-census.js +0 -11432
  51. package/dist/commands/cloud-verifier-universe.js +0 -10178
  52. package/dist/commands/config.js +0 -549
  53. package/dist/commands/design-verify.js +0 -311
  54. package/dist/commands/dismiss.js +0 -159
  55. package/dist/commands/hooks.js +0 -226
  56. package/dist/commands/inbox.js +0 -137
  57. package/dist/commands/mcp.js +0 -201
  58. package/dist/commands/policy.js +0 -371
  59. package/dist/commands/pr-status.js +0 -207
  60. package/dist/commands/pr.js +0 -105
  61. package/dist/commands/prepare-universe-review.js +0 -1092
  62. package/dist/commands/production-source-deny-policy.js +0 -100
  63. package/dist/commands/request-review.js +0 -74
  64. package/dist/commands/review.js +0 -191
  65. package/dist/commands/rules.js +0 -98
  66. package/dist/commands/scaffold-provisional-universe.js +0 -806
  67. package/dist/commands/setup.js +0 -1170
  68. package/dist/commands/skills.js +0 -447
  69. package/dist/commands/submit.js +0 -745
  70. package/dist/commands/system-map.js +0 -228
  71. package/dist/commands/triage.js +0 -598
  72. package/dist/commands/webhooks.js +0 -241
  73. package/dist/states.js +0 -46
  74. package/dist/tools/detect.js +0 -832
  75. package/dist/triage/astra.js +0 -202
  76. package/dist/triage/prompts.js +0 -188
  77. package/dist/triage/runner.js +0 -200
  78. package/dist/triage/types.js +0 -7
  79. package/dist/utils/action-output.js +0 -26
  80. package/dist/utils/analysis-api.js +0 -416
  81. package/dist/utils/design-verifier-api.js +0 -294
  82. package/dist/utils/design-verifier-history.js +0 -79
  83. package/dist/utils/design-verifier-result.js +0 -424
  84. package/dist/utils/github-api.js +0 -324
  85. package/dist/utils/pending-state.js +0 -86
  86. package/dist/utils/pr-ref.js +0 -56
  87. package/dist/utils/prompter.js +0 -328
  88. package/schemas/action.v1.json +0 -22
  89. package/schemas/ask.v1.json +0 -40
  90. package/schemas/inbox.v1.json +0 -27
  91. package/schemas/pr-status.v1.json +0 -61
  92. package/schemas/pr.v1.json +0 -97
  93. package/schemas/pr.v3.json +0 -45
  94. package/schemas/setup.v1.json +0 -75
  95. package/schemas/submit.v1.json +0 -90
  96. package/schemas/triage.v1.json +0 -103
  97. package/schemas/triage.v2.json +0 -64
@@ -1,1382 +0,0 @@
1
- import { createHash } from 'node:crypto';
2
- import { readFileSync, statSync } from 'node:fs';
3
- import { basename, extname, join, posix } from 'node:path';
4
- const MAX_PASCAL_SOURCE_BYTES = 4 * 1024 * 1024;
5
- const PASCAL_EXTENSIONS = new Set(['.pas', '.pp', '.inc']);
6
- const TEST_COMPONENTS = new Set([
7
- '__fixtures__', '__tests__', 'benchmark', 'benchmarks', 'example', 'examples',
8
- 'fixture', 'fixtures', 'sample', 'samples', 'spec', 'specs', 'test', 'testapp',
9
- 'testapps', 'tests',
10
- ]);
11
- const ROUTINE_KEYWORDS = new Set(['constructor', 'destructor', 'function', 'operator', 'procedure']);
12
- const END_BLOCK_KEYWORDS = new Set(['asm', 'begin', 'case', 'try']);
13
- function sha256(value) {
14
- return createHash('sha256').update(value).digest('hex');
15
- }
16
- function stableId(kind, identityKey) {
17
- const slug = kind.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
18
- return `census-${slug}-${sha256(`${kind}\0${identityKey}`).slice(0, 20)}`;
19
- }
20
- function recordStableId(kind, path, owner, identity) {
21
- return stableId(kind, `${path}#${owner}:${identity}`);
22
- }
23
- function normalizePath(path) {
24
- return path.split(posix.sep).join('/').replace(/^\.\//, '');
25
- }
26
- function isTestRoot(path) {
27
- const normalized = normalizePath(path);
28
- const components = normalized.toLowerCase().split('/');
29
- const name = basename(normalized).toLowerCase();
30
- return components.slice(0, -1).some((component) => TEST_COMPONENTS.has(component))
31
- || /(?:^|[._-])(test|tests|spec|fixture|mock)(?:[._-]|$)/.test(name)
32
- || /^(?:test_|conftest\.)/.test(name);
33
- }
34
- /** Strip Pascal comments and compiler directives without moving any source. */
35
- function stripPascalComments(source) {
36
- const output = [...source];
37
- let index = 0;
38
- let mode = 'code';
39
- while (index < source.length) {
40
- const char = source[index];
41
- const next = source[index + 1] ?? '';
42
- if (mode === 'code') {
43
- if (char === "'") {
44
- mode = 'string';
45
- index += 1;
46
- continue;
47
- }
48
- if (char === '{')
49
- mode = 'brace';
50
- else if (char === '(' && next === '*')
51
- mode = 'paren-star';
52
- else if (char === '/' && next === '/')
53
- mode = 'line';
54
- else {
55
- index += 1;
56
- continue;
57
- }
58
- output[index] = ' ';
59
- if (mode === 'paren-star' || mode === 'line')
60
- output[index + 1] = ' ';
61
- index += mode === 'brace' ? 1 : 2;
62
- continue;
63
- }
64
- if (mode === 'string') {
65
- if (char === "'" && next === "'") {
66
- index += 2;
67
- continue;
68
- }
69
- if (char === "'")
70
- mode = 'code';
71
- index += 1;
72
- continue;
73
- }
74
- if (char !== '\n' && char !== '\r')
75
- output[index] = ' ';
76
- if (mode === 'brace' && char === '}')
77
- mode = 'code';
78
- else if (mode === 'paren-star' && char === '*' && next === ')') {
79
- output[index + 1] = ' ';
80
- mode = 'code';
81
- index += 2;
82
- continue;
83
- }
84
- else if (mode === 'line' && (char === '\n' || char === '\r'))
85
- mode = 'code';
86
- index += 1;
87
- }
88
- return output.join('');
89
- }
90
- function tokenizePascal(source) {
91
- const clean = stripPascalComments(source);
92
- const tokens = [];
93
- let line = 1;
94
- let index = 0;
95
- while (index < clean.length) {
96
- const char = clean[index];
97
- if (/\s/.test(char)) {
98
- if (char === '\n')
99
- line += 1;
100
- index += 1;
101
- continue;
102
- }
103
- const start = index;
104
- const tokenLine = line;
105
- if (/[A-Za-z_]/.test(char)) {
106
- index += 1;
107
- while (index < clean.length && /[A-Za-z0-9_]/.test(clean[index]))
108
- index += 1;
109
- const text = source.slice(start, index);
110
- tokens.push({ text, lower: text.toLowerCase(), kind: 'identifier', start, end: index, line: tokenLine });
111
- continue;
112
- }
113
- if (/\d/.test(char) || (char === '$' && /[0-9A-Fa-f]/.test(clean[index + 1] ?? ''))) {
114
- index += 1;
115
- while (index < clean.length && /[A-Za-z0-9_.]/.test(clean[index])) {
116
- if (clean[index] === '.' && clean[index + 1] === '.')
117
- break;
118
- index += 1;
119
- }
120
- const text = source.slice(start, index);
121
- tokens.push({ text, lower: text.toLowerCase(), kind: 'number', start, end: index, line: tokenLine });
122
- continue;
123
- }
124
- if (char === "'") {
125
- index += 1;
126
- while (index < clean.length) {
127
- if (clean[index] === "'" && clean[index + 1] === "'") {
128
- index += 2;
129
- continue;
130
- }
131
- if (clean[index] === "'") {
132
- index += 1;
133
- break;
134
- }
135
- if (clean[index] === '\n')
136
- line += 1;
137
- index += 1;
138
- }
139
- const text = source.slice(start, index);
140
- tokens.push({ text, lower: text, kind: 'string', start, end: index, line: tokenLine });
141
- continue;
142
- }
143
- const pair = clean.slice(index, index + 2);
144
- const text = ['..', ':=', '<=', '>=', '<>', '+=', '-=', '*=', '/='].includes(pair) ? pair : char;
145
- index += text.length;
146
- tokens.push({ text, lower: text.toLowerCase(), kind: 'symbol', start, end: index, line: tokenLine });
147
- }
148
- return tokens;
149
- }
150
- function canonicalTokens(tokens) {
151
- return tokens.map((token) => `${token.kind}:${token.text.length}:${token.text}`).join('|');
152
- }
153
- function compactTokens(tokens) {
154
- return tokens.map((token) => token.text).join('');
155
- }
156
- function trimWrappingParens(tokens) {
157
- let current = tokens;
158
- while (current.length >= 2 && current[0].text === '(' && current.at(-1)?.text === ')') {
159
- let depth = 0;
160
- let wraps = true;
161
- for (let index = 0; index < current.length; index += 1) {
162
- if (current[index].text === '(')
163
- depth += 1;
164
- else if (current[index].text === ')')
165
- depth -= 1;
166
- if (depth === 0 && index < current.length - 1) {
167
- wraps = false;
168
- break;
169
- }
170
- }
171
- if (!wraps)
172
- break;
173
- current = current.slice(1, -1);
174
- }
175
- return current;
176
- }
177
- function selectorText(tokens) {
178
- const trimmed = trimWrappingParens(tokens);
179
- if (trimmed.length === 1 && trimmed[0].kind === 'string') {
180
- return trimmed[0].text.slice(1, -1).replace(/''/g, "'");
181
- }
182
- return compactTokens(trimmed);
183
- }
184
- function sourceLine(lines, line) {
185
- return lines[line - 1] ?? '';
186
- }
187
- function matchingEnd(tokens, start) {
188
- const opening = tokens[start]?.lower;
189
- if (!END_BLOCK_KEYWORDS.has(opening) && opening !== 'repeat')
190
- return null;
191
- const stack = [opening];
192
- for (let index = start + 1; index < tokens.length; index += 1) {
193
- const token = tokens[index].lower;
194
- if (token === 'repeat')
195
- stack.push(token);
196
- else if (END_BLOCK_KEYWORDS.has(token))
197
- stack.push(token);
198
- else if (token === 'until') {
199
- const repeat = stack.lastIndexOf('repeat');
200
- if (repeat >= 0)
201
- stack.splice(repeat, 1);
202
- }
203
- else if (token === 'end') {
204
- for (let cursor = stack.length - 1; cursor >= 0; cursor -= 1) {
205
- if (stack[cursor] !== 'repeat') {
206
- stack.splice(cursor, 1);
207
- break;
208
- }
209
- }
210
- }
211
- if (stack.length === 0)
212
- return index;
213
- }
214
- return null;
215
- }
216
- function routineDeclarations(tokens) {
217
- const declarations = [];
218
- for (let index = 0; index < tokens.length; index += 1) {
219
- if (!ROUTINE_KEYWORDS.has(tokens[index].lower))
220
- continue;
221
- let nameStart = index + 1;
222
- if (tokens[nameStart]?.lower === 'generic')
223
- nameStart += 1;
224
- const nameTokens = [];
225
- for (let cursor = nameStart; cursor < tokens.length; cursor += 1) {
226
- const token = tokens[cursor];
227
- if (token.text === '(' || token.text === ':' || token.text === ';')
228
- break;
229
- if (token.kind === 'identifier' || token.text === '.')
230
- nameTokens.push(token);
231
- else
232
- break;
233
- }
234
- const name = compactTokens(nameTokens);
235
- if (!name || !/[A-Za-z_]/.test(name[0]))
236
- continue;
237
- let paren = 0;
238
- let headerEnd = -1;
239
- for (let cursor = nameStart; cursor < tokens.length; cursor += 1) {
240
- if (tokens[cursor].text === '(')
241
- paren += 1;
242
- else if (tokens[cursor].text === ')')
243
- paren -= 1;
244
- else if (tokens[cursor].text === ';' && paren === 0) {
245
- headerEnd = cursor;
246
- break;
247
- }
248
- }
249
- if (headerEnd >= 0)
250
- declarations.push({ name, token: index, headerEnd });
251
- }
252
- return declarations;
253
- }
254
- function routineRanges(tokens) {
255
- const declarations = routineDeclarations(tokens);
256
- const ranges = [];
257
- for (let declarationIndex = declarations.length - 1; declarationIndex >= 0; declarationIndex -= 1) {
258
- const declaration = declarations[declarationIndex];
259
- for (let cursor = declaration.headerEnd + 1; cursor < tokens.length; cursor += 1) {
260
- if (tokens[cursor].lower !== 'begin' && tokens[cursor].lower !== 'asm')
261
- continue;
262
- if (ranges.some((range) => cursor >= range.start && cursor <= range.end))
263
- continue;
264
- const end = matchingEnd(tokens, cursor);
265
- if (end === null)
266
- break;
267
- ranges.push({ name: declaration.name, declaration: declaration.token, start: cursor, end });
268
- break;
269
- }
270
- }
271
- return ranges;
272
- }
273
- function ownerAt(tokenIndex, routines) {
274
- const containing = routines
275
- .filter((routine) => tokenIndex >= routine.start && tokenIndex <= routine.end)
276
- .sort((left, right) => (left.end - left.start) - (right.end - right.start)
277
- || right.declaration - left.declaration);
278
- return containing[0]?.name ?? '<unit-scope>';
279
- }
280
- function isInsideDeclarationRecord(tokens, tokenIndex) {
281
- // A variant declaration lives between its `record`/`object` opener and any
282
- // executable `begin`. Looking only to the nearest such opener avoids trying
283
- // to balance conditional-compilation alternatives that are intentionally
284
- // retained by the lexical pass.
285
- for (let index = tokenIndex - 1; index >= 0; index -= 1) {
286
- if (tokens[index].lower === 'begin' || tokens[index].lower === 'asm')
287
- return false;
288
- if (tokens[index].lower === 'record' || tokens[index].lower === 'object')
289
- return true;
290
- }
291
- return false;
292
- }
293
- function topLevelOf(tokens, caseIndex) {
294
- let paren = 0;
295
- let bracket = 0;
296
- for (let index = caseIndex + 1; index < tokens.length; index += 1) {
297
- const text = tokens[index].text;
298
- if (text === '(')
299
- paren += 1;
300
- else if (text === ')')
301
- paren -= 1;
302
- else if (text === '[')
303
- bracket += 1;
304
- else if (text === ']')
305
- bracket -= 1;
306
- else if (paren === 0 && bracket === 0 && tokens[index].lower === 'of')
307
- return index;
308
- else if (paren === 0 && bracket === 0 && (text === ';' || tokens[index].lower === 'end'))
309
- return null;
310
- }
311
- return null;
312
- }
313
- function splitLabels(tokens) {
314
- const result = [];
315
- let start = 0;
316
- let paren = 0;
317
- let bracket = 0;
318
- for (let index = 0; index <= tokens.length; index += 1) {
319
- const token = tokens[index];
320
- if (token?.text === '(')
321
- paren += 1;
322
- else if (token?.text === ')')
323
- paren -= 1;
324
- else if (token?.text === '[')
325
- bracket += 1;
326
- else if (token?.text === ']')
327
- bracket -= 1;
328
- if (index === tokens.length || (token?.text === ',' && paren === 0 && bracket === 0)) {
329
- const part = tokens.slice(start, index);
330
- if (part.length === 0)
331
- return null;
332
- result.push(part);
333
- start = index + 1;
334
- }
335
- }
336
- return result;
337
- }
338
- function plausibleCaseLabels(tokens) {
339
- if (tokens.length === 0 || tokens.some((token) => token.text === ':=' || token.text === ';'))
340
- return false;
341
- const forbidden = new Set([
342
- 'begin', 'case', 'do', 'else', 'end', 'for', 'if', 'repeat', 'then', 'try', 'until', 'while', 'with',
343
- ]);
344
- return !tokens.some((token) => token.kind === 'identifier' && forbidden.has(token.lower));
345
- }
346
- function caseBranches(source, lines, tokens, caseIndex, ofIndex, endIndex) {
347
- const candidates = [];
348
- const stack = ['case'];
349
- let segmentStart = ofIndex + 1;
350
- let caseElseIndex = null;
351
- let pendingTopLevelIf = 0;
352
- for (let index = ofIndex + 1; index < endIndex; index += 1) {
353
- const token = tokens[index];
354
- const lower = token.lower;
355
- if (lower === 'repeat')
356
- stack.push(lower);
357
- else if (END_BLOCK_KEYWORDS.has(lower))
358
- stack.push(lower);
359
- else if (lower === 'until') {
360
- const repeat = stack.lastIndexOf('repeat');
361
- if (repeat >= 0)
362
- stack.splice(repeat, 1);
363
- }
364
- else if (lower === 'end') {
365
- for (let cursor = stack.length - 1; cursor > 0; cursor -= 1) {
366
- if (stack[cursor] !== 'repeat') {
367
- stack.splice(cursor, 1);
368
- break;
369
- }
370
- }
371
- }
372
- if (stack.length !== 1)
373
- continue;
374
- if (lower === 'if') {
375
- pendingTopLevelIf += 1;
376
- continue;
377
- }
378
- if (token.text === ';') {
379
- segmentStart = index + 1;
380
- pendingTopLevelIf = 0;
381
- continue;
382
- }
383
- if (lower === 'else' && pendingTopLevelIf > 0) {
384
- pendingTopLevelIf -= 1;
385
- continue;
386
- }
387
- if (lower === 'else') {
388
- caseElseIndex = index;
389
- break;
390
- }
391
- if (token.text !== ':')
392
- continue;
393
- const labelTokens = tokens.slice(segmentStart, index);
394
- if (!plausibleCaseLabels(labelTokens))
395
- continue;
396
- const labels = splitLabels(labelTokens);
397
- if (!labels)
398
- continue;
399
- candidates.push({ colon: index, start: segmentStart, labels });
400
- }
401
- const branches = [];
402
- candidates.forEach((candidate, index) => {
403
- const bodyStart = candidate.colon + 1;
404
- let bodyEnd = index + 1 < candidates.length
405
- ? candidates[index + 1].start
406
- : caseElseIndex ?? endIndex;
407
- for (let cursor = bodyEnd - 1; cursor >= bodyStart; cursor -= 1) {
408
- if (tokens[cursor].lower === 'else' && cursor < endIndex)
409
- bodyEnd = cursor;
410
- if (tokens[cursor].text !== ';')
411
- break;
412
- bodyEnd = cursor;
413
- }
414
- const body = tokens.slice(bodyStart, bodyEnd);
415
- if (body.length === 0)
416
- return;
417
- const implementationHash = sha256(canonicalTokens(body)).slice(0, 24);
418
- branches.push({
419
- labelTokens: candidate.labels,
420
- bodyStart,
421
- bodyEnd,
422
- line: tokens[candidate.start]?.line ?? tokens[caseIndex].line,
423
- anchor: sourceLine(lines, tokens[candidate.start]?.line ?? tokens[caseIndex].line),
424
- implementationKey: `pascal-branch:${implementationHash}`,
425
- entryIdentities: [],
426
- });
427
- });
428
- return {
429
- branches,
430
- hasElse: caseElseIndex !== null && caseElseIndex + 1 < endIndex,
431
- elseBodyStart: caseElseIndex === null ? null : caseElseIndex + 1,
432
- elseBodyEnd: caseElseIndex === null ? null : endIndex,
433
- };
434
- }
435
- function closedDomains(tokens) {
436
- const domains = new Map([
437
- ['boolean', new Set(['false', 'true'])],
438
- ['bool', new Set(['false', 'true'])],
439
- ]);
440
- for (let index = 0; index < tokens.length - 4; index += 1) {
441
- if (tokens[index].kind !== 'identifier' || tokens[index + 1].text !== '=' || tokens[index + 2].text !== '(')
442
- continue;
443
- const values = [];
444
- let valid = true;
445
- let cursor = index + 3;
446
- while (cursor < tokens.length && tokens[cursor].text !== ')') {
447
- if (tokens[cursor].kind !== 'identifier') {
448
- valid = false;
449
- break;
450
- }
451
- values.push(tokens[cursor].lower);
452
- cursor += 1;
453
- if (tokens[cursor]?.text === '=') {
454
- cursor += 1;
455
- if (tokens[cursor]?.text === '+' || tokens[cursor]?.text === '-')
456
- cursor += 1;
457
- if (tokens[cursor]?.kind !== 'number') {
458
- valid = false;
459
- break;
460
- }
461
- cursor += 1;
462
- }
463
- if (tokens[cursor]?.text === ',')
464
- cursor += 1;
465
- else if (tokens[cursor]?.text !== ')') {
466
- valid = false;
467
- break;
468
- }
469
- }
470
- if (valid && values.length > 0 && tokens[cursor]?.text === ')' && tokens[cursor + 1]?.text === ';') {
471
- domains.set(tokens[index].lower, new Set(values));
472
- }
473
- }
474
- return domains;
475
- }
476
- function containingRoutine(tokenIndex, routines) {
477
- return routines
478
- .filter((routine) => tokenIndex >= routine.start && tokenIndex <= routine.end)
479
- .sort((left, right) => (left.end - left.start) - (right.end - right.start)
480
- || right.declaration - left.declaration)[0] ?? null;
481
- }
482
- function closedDomainForCase(subjectTokens, caseIndex, tokens, routines, domains) {
483
- const subject = trimWrappingParens(subjectTokens);
484
- if (subject.length >= 4 && subject[0].kind === 'identifier' && subject[1].text === '('
485
- && subject.at(-1)?.text === ')' && domains.has(subject[0].lower))
486
- return { typeName: subject[0].text, values: domains.get(subject[0].lower) };
487
- if (subject.length !== 1 || subject[0].kind !== 'identifier')
488
- return null;
489
- const routine = containingRoutine(caseIndex, routines);
490
- const searchStart = routine?.declaration ?? 0;
491
- for (let colon = caseIndex - 2; colon >= searchStart; colon -= 1) {
492
- if (tokens[colon].text !== ':' || tokens[colon + 1]?.kind !== 'identifier')
493
- continue;
494
- const values = domains.get(tokens[colon + 1].lower);
495
- if (!values || ![';', ')', '=', ','].includes(tokens[colon + 2]?.text ?? ''))
496
- continue;
497
- const names = [];
498
- for (let cursor = colon - 1; cursor >= searchStart; cursor -= 1) {
499
- const token = tokens[cursor];
500
- if (token.text === ';' || token.text === '(' || ['var', 'const', 'out', 'type'].includes(token.lower))
501
- break;
502
- if (token.kind === 'identifier')
503
- names.push(token);
504
- else if (token.text !== ',')
505
- break;
506
- }
507
- if (names.some((name) => name.lower === subject[0].lower)) {
508
- return { typeName: tokens[colon + 1].text, values };
509
- }
510
- }
511
- return null;
512
- }
513
- function branchLabelsCoverDomain(branches, domain) {
514
- const labels = branches.flatMap((branch) => branch.labelTokens).map((label) => {
515
- const value = selectorText(label).toLowerCase();
516
- return value.includes('.') ? value.split('.').at(-1) : value;
517
- });
518
- return labels.every((label) => domain.has(label)) && domain.size === new Set(labels).size;
519
- }
520
- function pureBooleanOutcome(tokens, start, end) {
521
- let body = tokens.slice(start, end).filter((token) => token.text !== ';');
522
- while (body[0]?.lower === 'begin' && body.at(-1)?.lower === 'end') {
523
- body = body.slice(1, -1).filter((token) => token.text !== ';');
524
- }
525
- if (body.length === 3
526
- && body[0].kind === 'identifier'
527
- && body[1].text === ':='
528
- && (body[2].lower === 'false' || body[2].lower === 'true'))
529
- return body[2].lower;
530
- if (body.length === 4
531
- && body[0].lower === 'exit'
532
- && body[1].text === '('
533
- && (body[2].lower === 'false' || body[2].lower === 'true')
534
- && body[3].text === ')')
535
- return body[2].lower;
536
- return null;
537
- }
538
- function isBooleanMembershipCase(parsed, tokens) {
539
- if (parsed.branches.length === 0
540
- || parsed.elseBodyStart === null
541
- || parsed.elseBodyEnd === null)
542
- return false;
543
- const branchOutcomes = parsed.branches.map((branch) => pureBooleanOutcome(tokens, branch.bodyStart, branch.bodyEnd));
544
- const fallback = pureBooleanOutcome(tokens, parsed.elseBodyStart, parsed.elseBodyEnd);
545
- return fallback !== null
546
- && branchOutcomes.every((outcome) => outcome !== null && outcome !== fallback);
547
- }
548
- function scanCaseFamilies(path, source, lines, tokens, routines) {
549
- const partial = [];
550
- const domains = closedDomains(tokens);
551
- for (let index = 0; index < tokens.length; index += 1) {
552
- if (tokens[index].lower !== 'case' || isInsideDeclarationRecord(tokens, index))
553
- continue;
554
- const ofIndex = topLevelOf(tokens, index);
555
- if (ofIndex === null)
556
- continue;
557
- const subjectTokens = tokens.slice(index + 1, ofIndex);
558
- if (subjectTokens.length === 0 || subjectTokens.some((token) => token.text === ':'))
559
- continue;
560
- const end = matchingEnd(tokens, index);
561
- if (end === null)
562
- continue;
563
- const parsed = caseBranches(source, lines, tokens, index, ofIndex, end);
564
- const domain = parsed.branches.length === 1
565
- ? closedDomainForCase(subjectTokens, index, tokens, routines, domains)
566
- : null;
567
- const exactOneArm = domain !== null
568
- && (parsed.hasElse || branchLabelsCoverDomain(parsed.branches, domain.values));
569
- const exhaustiveFallback = parsed.branches.length === 1
570
- && parsed.elseBodyStart !== null && parsed.elseBodyEnd !== null
571
- && sha256(canonicalTokens(tokens.slice(parsed.elseBodyStart, parsed.elseBodyEnd)))
572
- !== sha256(canonicalTokens(tokens.slice(parsed.branches[0].bodyStart, parsed.branches[0].bodyEnd)));
573
- const booleanMembership = isBooleanMembershipCase(parsed, tokens);
574
- const discoveryLead = booleanMembership
575
- || (parsed.branches.length === 1 && !exactOneArm && !exhaustiveFallback);
576
- if (parsed.branches.length === 0)
577
- continue;
578
- const owner = ownerAt(index, routines);
579
- partial.push({
580
- kind: discoveryLead ? 'pascal-case-membership-lead-family' : 'pascal-typed-case-family',
581
- entryKind: discoveryLead ? 'pascal-case-membership-lead-entry' : 'pascal-typed-case-entry',
582
- path,
583
- owner,
584
- selectorSubject: compactTokens(subjectTokens),
585
- ...(domain === null ? {} : {
586
- selectorDomainIdentity: `closed-domain:${domain.typeName}:${compactTokens(subjectTokens)}`,
587
- }),
588
- proofStatus: discoveryLead ? 'discovery-lead' : 'proof-complete',
589
- line: tokens[index].line,
590
- anchor: sourceLine(lines, tokens[index].line),
591
- start: index,
592
- end,
593
- branches: parsed.branches,
594
- });
595
- }
596
- const occurrences = new Map();
597
- return partial.map((family) => {
598
- const signature = sha256(canonicalTokens(tokens.slice(family.start, family.end + 1))).slice(0, 24);
599
- const base = `${family.owner}\0${family.selectorSubject}\0${signature}`;
600
- const occurrence = (occurrences.get(base) ?? 0) + 1;
601
- occurrences.set(base, occurrence);
602
- return { ...family, identity: `pascal-case:${family.selectorSubject}:${signature}:${occurrence}` };
603
- });
604
- }
605
- function statementEnd(tokens, start) {
606
- if (start >= tokens.length)
607
- return start;
608
- const blockEnd = matchingEnd(tokens, start);
609
- if (blockEnd !== null)
610
- return blockEnd + (tokens[blockEnd + 1]?.text === ';' ? 2 : 1);
611
- let paren = 0;
612
- let bracket = 0;
613
- for (let index = start; index < tokens.length; index += 1) {
614
- if (tokens[index].text === '(')
615
- paren += 1;
616
- else if (tokens[index].text === ')')
617
- paren -= 1;
618
- else if (tokens[index].text === '[')
619
- bracket += 1;
620
- else if (tokens[index].text === ']')
621
- bracket -= 1;
622
- else if (tokens[index].lower === 'else' && paren === 0 && bracket === 0)
623
- return index;
624
- else if (tokens[index].text === ';' && paren === 0 && bracket === 0)
625
- return index + 1;
626
- }
627
- return tokens.length;
628
- }
629
- function topLevelThen(tokens, ifIndex) {
630
- let paren = 0;
631
- let bracket = 0;
632
- for (let index = ifIndex + 1; index < tokens.length; index += 1) {
633
- if (tokens[index].text === '(')
634
- paren += 1;
635
- else if (tokens[index].text === ')')
636
- paren -= 1;
637
- else if (tokens[index].text === '[')
638
- bracket += 1;
639
- else if (tokens[index].text === ']')
640
- bracket -= 1;
641
- else if (tokens[index].lower === 'then' && paren === 0 && bracket === 0)
642
- return index;
643
- else if (tokens[index].text === ';' && paren === 0 && bracket === 0)
644
- return null;
645
- }
646
- return null;
647
- }
648
- function simpleQualifiedReference(tokens, allowAddress = false) {
649
- const trimmed = trimWrappingParens(tokens);
650
- let index = allowAddress && trimmed[0]?.text === '@' ? 1 : 0;
651
- if (trimmed[index]?.kind !== 'identifier')
652
- return false;
653
- index += 1;
654
- while (index < trimmed.length) {
655
- if (trimmed[index].text !== '.' || trimmed[index + 1]?.kind !== 'identifier')
656
- return false;
657
- index += 2;
658
- }
659
- return true;
660
- }
661
- function exactLiteral(tokens) {
662
- const trimmed = trimWrappingParens(tokens);
663
- if (trimmed.length === 1) {
664
- return trimmed[0].kind === 'string' || trimmed[0].kind === 'number'
665
- || ['false', 'nil', 'true'].includes(trimmed[0].lower);
666
- }
667
- return trimmed.length >= 2 && trimmed.every((token) => token.kind === 'number' || token.text === '#' || token.text === '+' || token.text === '-');
668
- }
669
- function splitTopLevelOr(tokens) {
670
- const condition = trimWrappingParens(tokens);
671
- const clauses = [];
672
- let start = 0;
673
- let paren = 0;
674
- let bracket = 0;
675
- for (let index = 0; index <= condition.length; index += 1) {
676
- const token = condition[index];
677
- if (token?.text === '(')
678
- paren += 1;
679
- else if (token?.text === ')')
680
- paren -= 1;
681
- else if (token?.text === '[')
682
- bracket += 1;
683
- else if (token?.text === ']')
684
- bracket -= 1;
685
- if (index === condition.length || (token?.lower === 'or' && paren === 0 && bracket === 0)) {
686
- clauses.push(condition.slice(start, index));
687
- start = index + 1;
688
- }
689
- }
690
- return clauses;
691
- }
692
- function sliceCallArguments(tokens, open) {
693
- const result = [];
694
- let start = open + 1;
695
- let paren = 1;
696
- let bracket = 0;
697
- for (let index = open + 1; index < tokens.length; index += 1) {
698
- if (tokens[index].text === '(')
699
- paren += 1;
700
- else if (tokens[index].text === ')') {
701
- paren -= 1;
702
- if (paren === 0) {
703
- if (index !== tokens.length - 1)
704
- return null;
705
- result.push(tokens.slice(start, index));
706
- return result;
707
- }
708
- }
709
- else if (tokens[index].text === '[')
710
- bracket += 1;
711
- else if (tokens[index].text === ']')
712
- bracket -= 1;
713
- else if (tokens[index].text === ',' && paren === 1 && bracket === 0) {
714
- result.push(tokens.slice(start, index));
715
- start = index + 1;
716
- }
717
- }
718
- return null;
719
- }
720
- function exactEqualityClause(tokens) {
721
- const clause = trimWrappingParens(tokens);
722
- let paren = 0;
723
- let bracket = 0;
724
- let operator = -1;
725
- for (let index = 0; index < clause.length; index += 1) {
726
- if (clause[index].text === '(')
727
- paren += 1;
728
- else if (clause[index].text === ')')
729
- paren -= 1;
730
- else if (clause[index].text === '[')
731
- bracket += 1;
732
- else if (clause[index].text === ']')
733
- bracket -= 1;
734
- else if (clause[index].text === '=' && paren === 0 && bracket === 0) {
735
- if (operator >= 0)
736
- return null;
737
- operator = index;
738
- }
739
- }
740
- if (operator <= 0 || operator >= clause.length - 1)
741
- return null;
742
- const subject = clause.slice(0, operator);
743
- const selector = clause.slice(operator + 1);
744
- if (!simpleQualifiedReference(subject) || !exactLiteral(selector))
745
- return null;
746
- return { subject: compactTokens(subject), selectors: [selectorText(selector)] };
747
- }
748
- const SUBJECT_FIRST_PREFIX_PREDICATES = new Set([
749
- 'beginswith', 'sbeginswith', 'sstartswith', 'startswith',
750
- ]);
751
- const PREFIX_FIRST_PREDICATES = new Set([
752
- 'ansistartsstr', 'ansistartstext', 'startsstr', 'startstext',
753
- ]);
754
- const RECEIVER_PREFIX_PREDICATES = new Set(['beginswith', 'startswith']);
755
- function exactPrefixClause(tokens) {
756
- const clause = trimWrappingParens(tokens);
757
- const open = clause.findIndex((token) => token.text === '(');
758
- if (open < 1 || clause.at(-1)?.text !== ')')
759
- return null;
760
- const args = sliceCallArguments(clause, open);
761
- if (!args)
762
- return null;
763
- const callee = clause.slice(0, open);
764
- if (callee.length >= 3 && callee.at(-2)?.text === '.') {
765
- const method = callee.at(-1);
766
- const receiver = callee.slice(0, -2);
767
- if (method.kind === 'identifier' && RECEIVER_PREFIX_PREDICATES.has(method.lower)
768
- && simpleQualifiedReference(receiver) && args.length === 1 && exactLiteral(args[0]))
769
- return { subject: compactTokens(receiver), selectors: [`${selectorText(args[0])}*`] };
770
- return null;
771
- }
772
- if (callee.length !== 1 || callee[0].kind !== 'identifier' || args.length !== 2)
773
- return null;
774
- if (SUBJECT_FIRST_PREFIX_PREDICATES.has(callee[0].lower)
775
- && simpleQualifiedReference(args[0]) && exactLiteral(args[1]))
776
- return { subject: compactTokens(trimWrappingParens(args[0])), selectors: [`${selectorText(args[1])}*`] };
777
- if (PREFIX_FIRST_PREDICATES.has(callee[0].lower)
778
- && exactLiteral(args[0]) && simpleQualifiedReference(args[1]))
779
- return { subject: compactTokens(trimWrappingParens(args[1])), selectors: [`${selectorText(args[0])}*`] };
780
- return null;
781
- }
782
- function equalityCondition(tokens) {
783
- const clauses = splitTopLevelOr(tokens);
784
- const parsed = clauses.map((clause) => exactEqualityClause(clause) ?? exactPrefixClause(clause));
785
- if (parsed.some((clause) => clause === null))
786
- return null;
787
- const exact = parsed;
788
- if (exact.length === 0 || exact.some((clause) => clause.subject !== exact[0].subject))
789
- return null;
790
- return { subject: exact[0].subject, selectors: [...new Set(exact.flatMap((clause) => clause.selectors))] };
791
- }
792
- function runtimeTypeCondition(tokens) {
793
- const condition = trimWrappingParens(tokens);
794
- const operator = condition.findIndex((token) => token.lower === 'is');
795
- if (operator <= 0 || operator >= condition.length - 1)
796
- return null;
797
- if (condition.slice(operator + 1).some((token) => token.lower === 'is'))
798
- return null;
799
- const subject = condition.slice(0, operator);
800
- const selector = condition.slice(operator + 1);
801
- if (!simpleQualifiedReference(subject) || !simpleQualifiedReference(selector))
802
- return null;
803
- return { subject: compactTokens(subject), selectors: [compactTokens(selector)] };
804
- }
805
- function conditionalOccurrences(tokens, routines) {
806
- const result = [];
807
- for (let index = 0; index < tokens.length; index += 1) {
808
- if (tokens[index].lower !== 'if' || isInsideDeclarationRecord(tokens, index))
809
- continue;
810
- const then = topLevelThen(tokens, index);
811
- if (then === null)
812
- continue;
813
- const bodyStart = then + 1;
814
- const bodyEnd = statementEnd(tokens, bodyStart);
815
- if (bodyEnd <= bodyStart)
816
- continue;
817
- const elseToken = tokens[bodyEnd]?.lower === 'else' ? bodyEnd : null;
818
- const elseStart = elseToken === null ? null : elseToken + 1;
819
- const elseEnd = elseStart === null ? null : statementEnd(tokens, elseStart);
820
- result.push({
821
- start: index,
822
- bodyStart,
823
- bodyEnd,
824
- elseStart,
825
- elseEnd,
826
- fullEnd: elseEnd ?? bodyEnd,
827
- owner: ownerAt(index, routines),
828
- typeCondition: runtimeTypeCondition(tokens.slice(index + 1, then)),
829
- equalityCondition: equalityCondition(tokens.slice(index + 1, then)),
830
- });
831
- }
832
- return result;
833
- }
834
- function rawCaseRegions(source, lines, tokens) {
835
- const result = [];
836
- for (let index = 0; index < tokens.length; index += 1) {
837
- if (tokens[index].lower !== 'case' || isInsideDeclarationRecord(tokens, index))
838
- continue;
839
- const of = topLevelOf(tokens, index);
840
- const end = of === null ? null : matchingEnd(tokens, index);
841
- if (of === null || end === null)
842
- continue;
843
- const parsed = caseBranches(source, lines, tokens, index, of, end);
844
- parsed.branches.forEach((branch) => result.push({ start: branch.bodyStart, end: branch.bodyEnd }));
845
- }
846
- return result;
847
- }
848
- function enclosingRegion(occurrence, subject, mode, occurrences, caseRegions, routines, tokenCount) {
849
- const candidates = [...caseRegions.filter((region) => occurrence.start > region.start && occurrence.start < region.end)];
850
- occurrences.forEach((parent) => {
851
- if (parent === occurrence)
852
- return;
853
- const sameDispatch = mode === 'type'
854
- ? parent.typeCondition?.subject === subject
855
- : parent.equalityCondition?.subject === subject;
856
- if (sameDispatch)
857
- return;
858
- const regions = [
859
- { start: parent.bodyStart, end: parent.bodyEnd },
860
- ...(parent.elseStart === null || parent.elseEnd === null
861
- ? [] : [{ start: parent.elseStart, end: parent.elseEnd }]),
862
- ];
863
- regions.forEach((region) => {
864
- if (occurrence.start > region.start && occurrence.start < region.end)
865
- candidates.push(region);
866
- });
867
- });
868
- const routine = containingRoutine(occurrence.start, routines);
869
- candidates.push(routine ? { start: routine.start, end: routine.end } : { start: -1, end: tokenCount });
870
- return candidates.sort((left, right) => (left.end - left.start) - (right.end - right.start))[0];
871
- }
872
- function onlySeparators(tokens, start, end) {
873
- return tokens.slice(start, end).every((token) => token.text === ';');
874
- }
875
- function clusterOccurrences(entries, tokens) {
876
- const sorted = [...entries].sort((left, right) => left.start - right.start);
877
- const result = [];
878
- let current = [];
879
- let coveredUntil = -1;
880
- sorted.forEach((entry) => {
881
- if (current.length === 0 || entry.start <= coveredUntil || onlySeparators(tokens, coveredUntil, entry.start)) {
882
- current.push(entry);
883
- coveredUntil = Math.max(coveredUntil, entry.fullEnd);
884
- return;
885
- }
886
- result.push(current);
887
- current = [entry];
888
- coveredUntil = entry.fullEnd;
889
- });
890
- if (current.length > 0)
891
- result.push(current);
892
- return result;
893
- }
894
- function statementTerminates(tokens, start, end) {
895
- const terminal = new Set(['abort', 'break', 'continue', 'exit', 'halt', 'raise']);
896
- while (start < end && tokens[start]?.text === ';')
897
- start += 1;
898
- while (end > start && tokens[end - 1]?.text === ';')
899
- end -= 1;
900
- if (start >= end)
901
- return false;
902
- if (terminal.has(tokens[start].lower))
903
- return true;
904
- if (tokens[start].lower === 'begin') {
905
- const blockEnd = matchingEnd(tokens, start);
906
- if (blockEnd === null || blockEnd >= end)
907
- return false;
908
- const statements = [];
909
- const stack = ['begin'];
910
- let statementStart = start + 1;
911
- for (let index = start + 1; index < blockEnd; index += 1) {
912
- const lower = tokens[index].lower;
913
- if (lower === 'repeat')
914
- stack.push(lower);
915
- else if (END_BLOCK_KEYWORDS.has(lower))
916
- stack.push(lower);
917
- else if (lower === 'until') {
918
- const repeat = stack.lastIndexOf('repeat');
919
- if (repeat >= 0)
920
- stack.splice(repeat, 1);
921
- }
922
- else if (lower === 'end') {
923
- for (let cursor = stack.length - 1; cursor > 0; cursor -= 1) {
924
- if (stack[cursor] !== 'repeat') {
925
- stack.splice(cursor, 1);
926
- break;
927
- }
928
- }
929
- }
930
- if (stack.length === 1 && tokens[index].text === ';') {
931
- if (statementStart < index)
932
- statements.push({ start: statementStart, end: index });
933
- statementStart = index + 1;
934
- }
935
- }
936
- if (statementStart < blockEnd)
937
- statements.push({ start: statementStart, end: blockEnd });
938
- const last = statements.at(-1);
939
- return last !== undefined && statementTerminates(tokens, last.start, last.end);
940
- }
941
- if (tokens[start].lower === 'if') {
942
- const then = topLevelThen(tokens, start);
943
- if (then === null || then >= end)
944
- return false;
945
- const thenStart = then + 1;
946
- const thenEnd = statementEnd(tokens, thenStart);
947
- if (thenEnd >= end || tokens[thenEnd]?.lower !== 'else')
948
- return false;
949
- const elseStart = thenEnd + 1;
950
- const elseEnd = statementEnd(tokens, elseStart);
951
- return elseEnd <= end
952
- && statementTerminates(tokens, thenStart, thenEnd)
953
- && statementTerminates(tokens, elseStart, elseEnd);
954
- }
955
- return false;
956
- }
957
- function terminalBehavior(tokens, start, end) {
958
- return statementTerminates(tokens, start, end);
959
- }
960
- function safeLinearFallthrough(tokens, start, end) {
961
- const control = new Set([
962
- 'break', 'case', 'continue', 'exit', 'for', 'goto', 'halt', 'if', 'raise',
963
- 'repeat', 'try', 'while', 'with',
964
- ]);
965
- return !tokens.slice(start, end).some((token) => control.has(token.lower));
966
- }
967
- function structuralRegionBoundary(tokens, start, end) {
968
- return tokens.slice(start, end).every((token) => ['.', ')', ';'].includes(token.text) || ['else', 'end'].includes(token.lower));
969
- }
970
- function terminalPostdominatorCluster(entries, region, tokens) {
971
- const sorted = [...entries].sort((left, right) => left.start - right.start);
972
- if (sorted.length < 2 || !sorted.every((entry) => terminalBehavior(tokens, entry.bodyStart, entry.bodyEnd))) {
973
- return null;
974
- }
975
- for (let index = 1; index < sorted.length; index += 1) {
976
- if (!safeLinearFallthrough(tokens, sorted[index - 1].fullEnd, sorted[index].start))
977
- return null;
978
- }
979
- const last = sorted.at(-1);
980
- const exactFallback = structuralRegionBoundary(tokens, last.fullEnd, region.end)
981
- || statementTerminates(tokens, last.fullEnd, region.end);
982
- return exactFallback ? sorted : null;
983
- }
984
- function elseConnected(entries) {
985
- return entries.some((parent) => parent.elseStart !== null && parent.elseEnd !== null && entries.some((child) => child !== parent && child.start >= parent.elseStart && child.start < parent.elseEnd));
986
- }
987
- function scanConditionalFamilies(path, source, lines, tokens, routines) {
988
- const result = [];
989
- const allOccurrences = conditionalOccurrences(tokens, routines);
990
- const caseRegions = rawCaseRegions(source, lines, tokens);
991
- for (const mode of ['type', 'equality']) {
992
- const occurrences = new Map();
993
- const groups = new Map();
994
- const regions = new Map();
995
- allOccurrences.forEach((entry) => {
996
- const condition = mode === 'type' ? entry.typeCondition : entry.equalityCondition;
997
- if (!condition)
998
- return;
999
- const region = enclosingRegion(entry, condition.subject, mode, allOccurrences, caseRegions, routines, tokens.length);
1000
- const key = `${entry.owner}\0${region.start}:${region.end}\0${condition.subject}`;
1001
- const values = groups.get(key) ?? [];
1002
- values.push(entry);
1003
- groups.set(key, values);
1004
- regions.set(key, region);
1005
- });
1006
- groups.forEach((group, groupKey) => {
1007
- const adjacency = clusterOccurrences(group, tokens);
1008
- const terminalCluster = mode === 'type'
1009
- ? terminalPostdominatorCluster(group, regions.get(groupKey), tokens)
1010
- : null;
1011
- const candidateGroups = mode === 'equality'
1012
- ? adjacency
1013
- : [
1014
- ...adjacency.filter((entries) => elseConnected(entries)),
1015
- ...(terminalCluster === null ? [] : [terminalCluster]),
1016
- ];
1017
- const uniqueGroups = [...new Map(candidateGroups.map((entries) => [
1018
- entries.map((entry) => entry.start).join(','), entries,
1019
- ])).values()];
1020
- uniqueGroups.forEach((entries) => {
1021
- const conditions = entries.map((entry) => mode === 'type' ? entry.typeCondition : entry.equalityCondition);
1022
- const selectorCount = new Set(conditions.flatMap((condition) => condition.selectors)).size;
1023
- const implementations = entries.map((entry) => sha256(canonicalTokens(tokens.slice(entry.bodyStart, entry.bodyEnd))).slice(0, 24));
1024
- if (selectorCount < 2 || new Set(implementations).size < 2)
1025
- return;
1026
- if (mode === 'type' && !elseConnected(entries) && !entries.every((entry) => terminalBehavior(tokens, entry.bodyStart, entry.bodyEnd)))
1027
- return;
1028
- const owner = entries[0].owner;
1029
- const subject = conditions[0].subject;
1030
- const booleanMembership = mode === 'equality'
1031
- && entries.every((entry) => pureBooleanOutcome(tokens, entry.bodyStart, entry.bodyEnd) !== null);
1032
- const kind = booleanMembership
1033
- ? 'pascal-conditional-membership-lead-family'
1034
- : mode === 'type' ? 'pascal-typed-runtime-type-family' : 'pascal-typed-equality-family';
1035
- const entryKind = booleanMembership
1036
- ? 'pascal-conditional-membership-lead-entry'
1037
- : mode === 'type' ? 'pascal-typed-runtime-type-entry' : 'pascal-typed-equality-entry';
1038
- const signature = sha256(entries.map((entry) => canonicalTokens(tokens.slice(entry.start, entry.bodyEnd))).join('\0')).slice(0, 24);
1039
- const occurrenceBase = `${owner}\0${subject}\0${signature}`;
1040
- const occurrence = (occurrences.get(occurrenceBase) ?? 0) + 1;
1041
- occurrences.set(occurrenceBase, occurrence);
1042
- result.push({
1043
- kind,
1044
- entryKind,
1045
- path,
1046
- owner,
1047
- identity: `${mode === 'type' ? 'pascal-is' : 'pascal-equality'}:${subject}:${signature}:${occurrence}`,
1048
- selectorSubject: subject,
1049
- proofStatus: booleanMembership ? 'discovery-lead' : 'proof-complete',
1050
- line: tokens[entries[0].start].line,
1051
- anchor: sourceLine(lines, tokens[entries[0].start].line),
1052
- start: entries[0].start,
1053
- end: entries.at(-1).bodyEnd,
1054
- branches: entries.map((entry, entryIndex) => ({
1055
- labelTokens: [],
1056
- bodyStart: entry.bodyStart,
1057
- bodyEnd: entry.bodyEnd,
1058
- line: tokens[entry.start].line,
1059
- anchor: sourceLine(lines, tokens[entry.start].line),
1060
- implementationKey: `pascal-branch:${implementations[entryIndex]}`,
1061
- entryIdentities: conditions[entryIndex].selectors,
1062
- })),
1063
- });
1064
- });
1065
- });
1066
- }
1067
- return result;
1068
- }
1069
- function callArguments(tokens, open) {
1070
- const args = [];
1071
- let start = open + 1;
1072
- let paren = 1;
1073
- let bracket = 0;
1074
- for (let index = open + 1; index < tokens.length; index += 1) {
1075
- if (tokens[index].text === '(')
1076
- paren += 1;
1077
- else if (tokens[index].text === ')') {
1078
- paren -= 1;
1079
- if (paren === 0) {
1080
- args.push(tokens.slice(start, index));
1081
- return { args, end: index };
1082
- }
1083
- }
1084
- else if (tokens[index].text === '[')
1085
- bracket += 1;
1086
- else if (tokens[index].text === ']')
1087
- bracket -= 1;
1088
- else if (tokens[index].text === ',' && paren === 1 && bracket === 0) {
1089
- args.push(tokens.slice(start, index));
1090
- start = index + 1;
1091
- }
1092
- }
1093
- return null;
1094
- }
1095
- function topLevelDo(tokens, withIndex) {
1096
- let paren = 0;
1097
- let bracket = 0;
1098
- for (let index = withIndex + 1; index < tokens.length; index += 1) {
1099
- if (tokens[index].text === '(')
1100
- paren += 1;
1101
- else if (tokens[index].text === ')')
1102
- paren -= 1;
1103
- else if (tokens[index].text === '[')
1104
- bracket += 1;
1105
- else if (tokens[index].text === ']')
1106
- bracket -= 1;
1107
- else if (tokens[index].lower === 'do' && paren === 0 && bracket === 0)
1108
- return index;
1109
- else if (tokens[index].text === ';' && paren === 0 && bracket === 0)
1110
- return null;
1111
- }
1112
- return null;
1113
- }
1114
- function exactWithReceiver(tokens) {
1115
- let paren = 0;
1116
- let bracket = 0;
1117
- let asIndex = -1;
1118
- for (let index = 0; index < tokens.length; index += 1) {
1119
- if (tokens[index].text === '(')
1120
- paren += 1;
1121
- else if (tokens[index].text === ')')
1122
- paren -= 1;
1123
- else if (tokens[index].text === '[')
1124
- bracket += 1;
1125
- else if (tokens[index].text === ']')
1126
- bracket -= 1;
1127
- else if (tokens[index].lower === 'as' && paren === 0 && bracket === 0)
1128
- asIndex = index;
1129
- }
1130
- if (asIndex <= 0 || asIndex >= tokens.length - 1)
1131
- return null;
1132
- const expression = tokens.slice(0, asIndex);
1133
- const type = tokens.slice(asIndex + 1);
1134
- if (!simpleQualifiedReference(type) || expression.some((token) => [',', ';'].includes(token.text)))
1135
- return null;
1136
- return `${compactTokens(expression)} as ${compactTokens(type)}`;
1137
- }
1138
- function withScopes(tokens, routines) {
1139
- const result = [];
1140
- for (let index = 0; index < tokens.length; index += 1) {
1141
- if (tokens[index].lower !== 'with')
1142
- continue;
1143
- const doIndex = topLevelDo(tokens, index);
1144
- if (doIndex === null)
1145
- continue;
1146
- const receiver = exactWithReceiver(tokens.slice(index + 1, doIndex));
1147
- if (!receiver)
1148
- continue;
1149
- const bodyStart = doIndex + 1;
1150
- const bodyEnd = statementEnd(tokens, bodyStart);
1151
- if (bodyEnd <= bodyStart)
1152
- continue;
1153
- result.push({ owner: ownerAt(index, routines), receiver, start: index, bodyStart, bodyEnd });
1154
- }
1155
- return result;
1156
- }
1157
- function scanAddMethodFamilies(path, source, lines, tokens, routines) {
1158
- const scopes = withScopes(tokens, routines);
1159
- const groups = new Map();
1160
- for (let index = 0; index < tokens.length - 2; index += 1) {
1161
- if (tokens[index].lower !== 'addmethod' || tokens[index + 1].text !== '(')
1162
- continue;
1163
- let receiverStart = index;
1164
- let receiverIdentity = null;
1165
- let gateIdentity = '';
1166
- if (tokens[index - 1]?.text === '.') {
1167
- receiverStart = index - 2;
1168
- while (receiverStart >= 2 && tokens[receiverStart - 1].text === '.' && tokens[receiverStart - 2].kind === 'identifier')
1169
- receiverStart -= 2;
1170
- const receiver = tokens.slice(receiverStart, index - 1);
1171
- if (!simpleQualifiedReference(receiver))
1172
- continue;
1173
- receiverIdentity = compactTokens(receiver);
1174
- }
1175
- else {
1176
- const scope = scopes
1177
- .filter((candidate) => index >= candidate.bodyStart && index < candidate.bodyEnd)
1178
- .sort((left, right) => (left.bodyEnd - left.bodyStart) - (right.bodyEnd - right.bodyStart))[0];
1179
- if (!scope)
1180
- continue;
1181
- receiverStart = index;
1182
- receiverIdentity = scope.receiver;
1183
- gateIdentity = `with:${scope.start}`;
1184
- }
1185
- const call = callArguments(tokens, index + 1);
1186
- if (!call || call.args.length < 2 || !exactLiteral(call.args[0]) || !simpleQualifiedReference(call.args[1], true))
1187
- continue;
1188
- const owner = ownerAt(index, routines);
1189
- const groupKey = `${owner}\0${receiverIdentity}\0${gateIdentity}`;
1190
- const group = groups.get(groupKey) ?? { owner, receiver: receiverIdentity, registrations: [] };
1191
- group.registrations.push({
1192
- selector: selectorText(call.args[0]),
1193
- handler: compactTokens(trimWrappingParens(call.args[1])),
1194
- line: tokens[index].line,
1195
- anchor: sourceLine(lines, tokens[index].line),
1196
- start: receiverStart,
1197
- end: call.end + 1,
1198
- });
1199
- groups.set(groupKey, group);
1200
- }
1201
- const result = [];
1202
- const occurrences = new Map();
1203
- groups.forEach(({ owner, receiver, registrations }) => {
1204
- if (registrations.length < 2)
1205
- return;
1206
- const signature = sha256(registrations.map((entry) => `${entry.selector}\0${entry.handler}`).join('\0')).slice(0, 24);
1207
- const occurrenceBase = `${owner}\0${receiver}\0${signature}`;
1208
- const occurrence = (occurrences.get(occurrenceBase) ?? 0) + 1;
1209
- occurrences.set(occurrenceBase, occurrence);
1210
- result.push({
1211
- kind: 'pascal-typed-method-registration-family',
1212
- entryKind: 'pascal-typed-method-registration-entry',
1213
- path,
1214
- owner,
1215
- identity: `pascal-add-method:${receiver}:${signature}:${occurrence}`,
1216
- selectorSubject: receiver,
1217
- line: registrations[0].line,
1218
- anchor: registrations[0].anchor,
1219
- start: registrations[0].start,
1220
- end: registrations.at(-1).end,
1221
- branches: registrations.map((registration) => ({
1222
- labelTokens: [],
1223
- bodyStart: registration.start,
1224
- bodyEnd: registration.end,
1225
- line: registration.line,
1226
- anchor: registration.anchor,
1227
- implementationKey: `${path}#${owner}.${registration.handler.replace(/^@/, '')}`,
1228
- entryIdentities: [registration.selector],
1229
- })),
1230
- });
1231
- });
1232
- return result;
1233
- }
1234
- function emitFamilies(families) {
1235
- const records = [];
1236
- const familyIds = new Map();
1237
- const branchEntries = new Map();
1238
- for (const family of families) {
1239
- const familyId = recordStableId(family.kind, family.path, family.owner, family.identity);
1240
- const selectorDomain = family.selectorDomainIdentity ?? family.selectorSubject;
1241
- const proofStatus = family.proofStatus ?? 'proof-complete';
1242
- const inventoryMarker = proofStatus === 'proof-complete'
1243
- ? 'proof-complete:pascal-typed-registry'
1244
- : 'discovery-lead:pascal-open-membership';
1245
- familyIds.set(family, familyId);
1246
- records.push({
1247
- kind: family.kind,
1248
- projections: ['registry_families'],
1249
- presumptiveInventory: proofStatus === 'proof-complete',
1250
- proofStatus,
1251
- path: family.path,
1252
- line: family.line,
1253
- anchor: family.anchor,
1254
- owner: family.owner,
1255
- identity: family.identity,
1256
- tokens: [family.selectorSubject, selectorDomain, inventoryMarker],
1257
- semanticIdentityKey: `pascal-registry-family\0${family.path}#${family.owner}\0${family.identity}`,
1258
- identityComponents: {
1259
- identity_kind: 'pascal-registry-family',
1260
- owner_identity: `${family.path}#${family.owner}`,
1261
- selector_domain: selectorDomain,
1262
- },
1263
- });
1264
- for (const branch of family.branches) {
1265
- const selectors = branch.entryIdentities.length > 0
1266
- ? branch.entryIdentities
1267
- : branch.labelTokens.map(selectorText);
1268
- const entries = [];
1269
- selectors.forEach((selector, selectorIndex) => {
1270
- const implementationKey = branch.implementationKey.startsWith(`${family.path}#`)
1271
- ? branch.implementationKey
1272
- : `${family.path}#${family.owner}:${branch.implementationKey}`;
1273
- const identity = `${family.identity}:selector:${selector}:implementation:${implementationKey}:${selectorIndex}`;
1274
- const entryId = recordStableId(family.entryKind, family.path, family.owner, identity);
1275
- entries.push(entryId);
1276
- records.push({
1277
- kind: family.entryKind,
1278
- projections: ['registry_entries'],
1279
- presumptiveInventory: proofStatus === 'proof-complete',
1280
- proofStatus,
1281
- path: family.path,
1282
- line: branch.line,
1283
- anchor: branch.anchor,
1284
- owner: family.owner,
1285
- identity,
1286
- tokens: [selector, implementationKey, inventoryMarker],
1287
- selectorKey: selector,
1288
- implementationKey,
1289
- selectorFamilyRecordId: familyId,
1290
- semanticIdentityKey: `pascal-registry-entry\0${family.path}#${family.owner}\0${family.identity}\0${selector}\0${implementationKey}`,
1291
- identityComponents: {
1292
- identity_kind: 'pascal-registry-entry',
1293
- owner_identity: `${family.path}#${family.owner}`,
1294
- selector_domain: selectorDomain,
1295
- selector_key: selector,
1296
- implementation_identity: implementationKey,
1297
- },
1298
- });
1299
- });
1300
- branchEntries.set(branch, entries);
1301
- }
1302
- }
1303
- for (const child of families.filter((family) => family.kind === 'pascal-typed-case-family')) {
1304
- const owners = families
1305
- .filter((family) => family !== child && family.proofStatus !== 'discovery-lead'
1306
- && child.start > family.start && child.end < family.end)
1307
- .map((family) => ({
1308
- family,
1309
- branch: family.branches.find((candidate) => child.start >= candidate.bodyStart && child.end <= candidate.bodyEnd),
1310
- }))
1311
- .filter((owner) => owner.branch !== undefined)
1312
- .sort((left, right) => (left.family.end - left.family.start) - (right.family.end - right.family.start));
1313
- const owner = owners[0];
1314
- if (!owner)
1315
- continue;
1316
- const { family: parent, branch } = owner;
1317
- const childFamilyId = familyIds.get(child);
1318
- for (const parentEntryId of branchEntries.get(branch) ?? []) {
1319
- const identity = `edge:${parentEntryId}:${childFamilyId}`;
1320
- records.push({
1321
- kind: 'pascal-typed-selector-edge',
1322
- projections: ['registry_edges'],
1323
- presumptiveInventory: true,
1324
- proofStatus: 'proof-complete',
1325
- path: parent.path,
1326
- line: child.line,
1327
- anchor: child.anchor,
1328
- owner: parent.owner,
1329
- identity,
1330
- tokens: [parentEntryId, childFamilyId, 'proof-complete:pascal-typed-registry'],
1331
- parentSelectorEntryRecordId: parentEntryId,
1332
- childSelectorFamilyRecordId: childFamilyId,
1333
- relationKind: 'parent-key-partitions-selector',
1334
- semanticIdentityKey: `pascal-registry-edge\0${parentEntryId}\0${childFamilyId}\0parent-key-partitions-selector`,
1335
- identityComponents: {
1336
- identity_kind: 'pascal-registry-edge',
1337
- owner_identity: `${parent.path}#${parent.owner}`,
1338
- selector_domain: child.selectorDomainIdentity ?? child.selectorSubject,
1339
- parent_entry_identity: parentEntryId,
1340
- child_family_identity: childFamilyId,
1341
- relation_kind: 'parent-key-partitions-selector',
1342
- },
1343
- });
1344
- }
1345
- }
1346
- return records;
1347
- }
1348
- export function scanPascalSelectorIdentities(repositoryRoot, trackedPaths) {
1349
- const records = [];
1350
- const paths = [...new Set(trackedPaths.map(normalizePath))].sort();
1351
- for (const path of paths) {
1352
- if (!PASCAL_EXTENSIONS.has(extname(path).toLowerCase()) || isTestRoot(path))
1353
- continue;
1354
- const absolute = join(repositoryRoot, path);
1355
- let source;
1356
- try {
1357
- const stat = statSync(absolute);
1358
- if (!stat.isFile() || stat.size > MAX_PASCAL_SOURCE_BYTES)
1359
- continue;
1360
- const bytes = readFileSync(absolute);
1361
- if (bytes.includes(0))
1362
- continue;
1363
- source = bytes.toString('utf8');
1364
- }
1365
- catch {
1366
- continue;
1367
- }
1368
- const lines = source.split(/\r?\n/);
1369
- const tokens = tokenizePascal(source);
1370
- const routines = routineRanges(tokens);
1371
- const families = [
1372
- ...scanCaseFamilies(path, source, lines, tokens, routines),
1373
- ...scanConditionalFamilies(path, source, lines, tokens, routines),
1374
- ...scanAddMethodFamilies(path, source, lines, tokens, routines),
1375
- ].sort((left, right) => left.start - right.start || left.kind.localeCompare(right.kind));
1376
- records.push(...emitFamilies(families));
1377
- }
1378
- return records.sort((left, right) => left.path.localeCompare(right.path)
1379
- || left.line - right.line
1380
- || left.kind.localeCompare(right.kind)
1381
- || left.identity.localeCompare(right.identity));
1382
- }