@haystackeditor/cli 0.25.1 → 0.27.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 (109) hide show
  1. package/README.md +39 -463
  2. package/dist/capture/app-config.js +30 -6
  3. package/dist/commands/capture-brief.js +45 -35
  4. package/dist/commands/capture-contract.js +4 -4
  5. package/dist/commands/crawl-report.js +196 -0
  6. package/dist/commands/db-profile-upload.js +3 -3
  7. package/dist/commands/feedback.js +66 -0
  8. package/dist/commands/init-telemetry.js +55 -9
  9. package/dist/commands/init.js +8 -3
  10. package/dist/commands/lockfile-pin.js +307 -0
  11. package/dist/commands/telemetry-token.js +3 -3
  12. package/dist/commands/tokens.js +4 -4
  13. package/dist/commands/verify-explore.js +3 -3
  14. package/dist/commands/verify-history.js +2 -2
  15. package/dist/commands/verify-onboarding.js +13 -16
  16. package/dist/commands/verify-precompute.js +3 -4
  17. package/dist/commands/verify.js +31 -118
  18. package/dist/index.js +55 -968
  19. package/dist/schema.js +4 -10
  20. package/dist/utils/haystack-api.js +8 -36
  21. package/package.json +1 -5
  22. package/schemas/feedback.v1.json +13 -0
  23. package/schemas/pre-verify.v2.json +240 -0
  24. package/schemas/verify-raw.v1.json +1132 -0
  25. package/schemas/verify.v2.json +655 -0
  26. package/dist/assets/hooks/agent-context/detect.ts +0 -316
  27. package/dist/assets/hooks/agent-context/format.ts +0 -100
  28. package/dist/assets/hooks/agent-context/index.ts +0 -41
  29. package/dist/assets/hooks/agent-context/parsers/claude.ts +0 -262
  30. package/dist/assets/hooks/agent-context/parsers/codex.ts +0 -416
  31. package/dist/assets/hooks/agent-context/parsers/gemini.ts +0 -155
  32. package/dist/assets/hooks/agent-context/parsers/opencode.ts +0 -174
  33. package/dist/assets/hooks/agent-context/tsconfig.json +0 -14
  34. package/dist/assets/hooks/agent-context/types.ts +0 -58
  35. package/dist/assets/hooks/llm-rules-template.md +0 -59
  36. package/dist/assets/hooks/package-lock.json +0 -598
  37. package/dist/assets/hooks/package.json +0 -12
  38. package/dist/assets/hooks/scripts/commit-msg.sh +0 -5
  39. package/dist/assets/hooks/scripts/post-commit.sh +0 -5
  40. package/dist/assets/hooks/scripts/pre-commit.sh +0 -175
  41. package/dist/assets/hooks/scripts/pre-push.sh +0 -25
  42. package/dist/assets/hooks/scripts/prepare-commit-msg.sh +0 -5
  43. package/dist/assets/hooks/truncation-checker/ast-analyzer.ts +0 -528
  44. package/dist/assets/hooks/truncation-checker/index.ts +0 -595
  45. package/dist/assets/hooks/truncation-checker/tsconfig.json +0 -13
  46. package/dist/assets/skills/map-cloud-verifier-universe/SKILL.md +0 -2051
  47. package/dist/assets/skills/map-cloud-verifier-universe/agents/openai.yaml +0 -4
  48. package/dist/assets/skills/map-cloud-verifier-universe/references/output-contract.md +0 -3411
  49. package/dist/assets/skills/map-your-system.md +0 -143
  50. package/dist/assets/skills/submit.md +0 -200
  51. package/dist/commands/ask.js +0 -20
  52. package/dist/commands/cloud-verifier-behaviors.js +0 -218
  53. package/dist/commands/cloud-verifier-data-store-census.js +0 -539
  54. package/dist/commands/cloud-verifier-data-store-drift.js +0 -158
  55. package/dist/commands/cloud-verifier-identity-census.js +0 -4060
  56. package/dist/commands/cloud-verifier-materialization.js +0 -704
  57. package/dist/commands/cloud-verifier-pascal-selector-census.js +0 -1382
  58. package/dist/commands/cloud-verifier-python-manifest-selector-census.js +0 -2015
  59. package/dist/commands/cloud-verifier-specialized-operational-census.js +0 -11432
  60. package/dist/commands/cloud-verifier-universe.js +0 -10178
  61. package/dist/commands/config.js +0 -549
  62. package/dist/commands/design-verify.js +0 -311
  63. package/dist/commands/dismiss.js +0 -159
  64. package/dist/commands/hooks.js +0 -226
  65. package/dist/commands/inbox.js +0 -137
  66. package/dist/commands/mcp.js +0 -201
  67. package/dist/commands/policy.js +0 -371
  68. package/dist/commands/pr-status.js +0 -207
  69. package/dist/commands/pr.js +0 -105
  70. package/dist/commands/prepare-universe-review.js +0 -1092
  71. package/dist/commands/production-source-deny-policy.js +0 -100
  72. package/dist/commands/request-review.js +0 -74
  73. package/dist/commands/review.js +0 -191
  74. package/dist/commands/rules.js +0 -98
  75. package/dist/commands/scaffold-provisional-universe.js +0 -806
  76. package/dist/commands/setup.js +0 -1170
  77. package/dist/commands/skills.js +0 -447
  78. package/dist/commands/status.js +0 -35
  79. package/dist/commands/submit.js +0 -745
  80. package/dist/commands/system-map.js +0 -228
  81. package/dist/commands/triage.js +0 -598
  82. package/dist/commands/webhooks.js +0 -241
  83. package/dist/states.js +0 -46
  84. package/dist/tools/detect.js +0 -832
  85. package/dist/triage/astra.js +0 -202
  86. package/dist/triage/prompts.js +0 -188
  87. package/dist/triage/runner.js +0 -200
  88. package/dist/triage/types.js +0 -7
  89. package/dist/types.js +0 -326
  90. package/dist/utils/action-output.js +0 -26
  91. package/dist/utils/analysis-api.js +0 -416
  92. package/dist/utils/config.js +0 -54
  93. package/dist/utils/design-verifier-api.js +0 -294
  94. package/dist/utils/design-verifier-history.js +0 -79
  95. package/dist/utils/design-verifier-result.js +0 -424
  96. package/dist/utils/github-api.js +0 -324
  97. package/dist/utils/pending-state.js +0 -86
  98. package/dist/utils/pr-ref.js +0 -56
  99. package/dist/utils/prompter.js +0 -328
  100. package/schemas/action.v1.json +0 -22
  101. package/schemas/ask.v1.json +0 -40
  102. package/schemas/inbox.v1.json +0 -27
  103. package/schemas/pr-status.v1.json +0 -61
  104. package/schemas/pr.v1.json +0 -97
  105. package/schemas/pr.v3.json +0 -45
  106. package/schemas/setup.v1.json +0 -75
  107. package/schemas/submit.v1.json +0 -90
  108. package/schemas/triage.v1.json +0 -103
  109. 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
- }