@clossys/writer 0.3.9 → 0.5.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 (136) hide show
  1. package/README.md +647 -6
  2. package/dist/addressability.d.ts +11 -1
  3. package/dist/addressability.d.ts.map +1 -1
  4. package/dist/addressability.js +169 -3
  5. package/dist/addressability.js.map +1 -1
  6. package/dist/approval-bypass.d.ts +120 -0
  7. package/dist/approval-bypass.d.ts.map +1 -0
  8. package/dist/approval-bypass.js +2318 -0
  9. package/dist/approval-bypass.js.map +1 -0
  10. package/dist/approval.d.ts +62 -0
  11. package/dist/approval.d.ts.map +1 -0
  12. package/dist/approval.js +421 -0
  13. package/dist/approval.js.map +1 -0
  14. package/dist/approve-cli.d.ts +82 -0
  15. package/dist/approve-cli.d.ts.map +1 -0
  16. package/dist/approve-cli.js +430 -0
  17. package/dist/approve-cli.js.map +1 -0
  18. package/dist/cli.d.ts +15 -0
  19. package/dist/cli.d.ts.map +1 -1
  20. package/dist/cli.js +264 -22
  21. package/dist/cli.js.map +1 -1
  22. package/dist/field-options.d.ts +59 -0
  23. package/dist/field-options.d.ts.map +1 -0
  24. package/dist/field-options.js +109 -0
  25. package/dist/field-options.js.map +1 -0
  26. package/dist/fold-wallpaper-gate.d.ts +7 -0
  27. package/dist/fold-wallpaper-gate.d.ts.map +1 -0
  28. package/dist/fold-wallpaper-gate.js +29 -0
  29. package/dist/fold-wallpaper-gate.js.map +1 -0
  30. package/dist/front-door.check.d.ts +2 -0
  31. package/dist/front-door.check.d.ts.map +1 -0
  32. package/dist/front-door.check.js +8 -0
  33. package/dist/front-door.check.js.map +1 -0
  34. package/dist/front-door.d.ts +82 -0
  35. package/dist/front-door.d.ts.map +1 -0
  36. package/dist/front-door.js +271 -0
  37. package/dist/front-door.js.map +1 -0
  38. package/dist/generated/advisor-plan-contract.generated.d.ts +3 -0
  39. package/dist/generated/advisor-plan-contract.generated.d.ts.map +1 -0
  40. package/dist/generated/advisor-plan-contract.generated.js +430 -0
  41. package/dist/generated/advisor-plan-contract.generated.js.map +1 -0
  42. package/dist/generated/contract-schema.generated.d.ts +97 -0
  43. package/dist/generated/contract-schema.generated.d.ts.map +1 -0
  44. package/dist/generated/contract-schema.generated.js +496 -0
  45. package/dist/generated/contract-schema.generated.js.map +1 -0
  46. package/dist/index.d.ts +18 -2
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +26 -0
  49. package/dist/index.js.map +1 -1
  50. package/dist/live-copy-gate.d.ts +16 -0
  51. package/dist/live-copy-gate.d.ts.map +1 -0
  52. package/dist/live-copy-gate.js +135 -0
  53. package/dist/live-copy-gate.js.map +1 -0
  54. package/dist/messaging-kit.d.ts +113 -0
  55. package/dist/messaging-kit.d.ts.map +1 -0
  56. package/dist/messaging-kit.js +121 -0
  57. package/dist/messaging-kit.js.map +1 -0
  58. package/dist/passage.d.ts +2 -1
  59. package/dist/passage.d.ts.map +1 -1
  60. package/dist/passage.js +2 -1
  61. package/dist/passage.js.map +1 -1
  62. package/dist/plan-authority.d.ts +73 -0
  63. package/dist/plan-authority.d.ts.map +1 -0
  64. package/dist/plan-authority.js +126 -0
  65. package/dist/plan-authority.js.map +1 -0
  66. package/dist/plan-contract.d.ts +16 -0
  67. package/dist/plan-contract.d.ts.map +1 -0
  68. package/dist/plan-contract.js +33 -0
  69. package/dist/plan-contract.js.map +1 -0
  70. package/dist/plan-digest.d.ts +23 -0
  71. package/dist/plan-digest.d.ts.map +1 -0
  72. package/dist/plan-digest.js +82 -0
  73. package/dist/plan-digest.js.map +1 -0
  74. package/dist/plan-rules.d.ts +54 -0
  75. package/dist/plan-rules.d.ts.map +1 -0
  76. package/dist/plan-rules.js +145 -0
  77. package/dist/plan-rules.js.map +1 -0
  78. package/dist/render-registry-parity.d.ts +20 -0
  79. package/dist/render-registry-parity.d.ts.map +1 -0
  80. package/dist/render-registry-parity.js +45 -0
  81. package/dist/render-registry-parity.js.map +1 -0
  82. package/dist/resolve.d.ts +40 -6
  83. package/dist/resolve.d.ts.map +1 -1
  84. package/dist/resolve.js +144 -18
  85. package/dist/resolve.js.map +1 -1
  86. package/dist/scan.d.ts +32 -0
  87. package/dist/scan.d.ts.map +1 -1
  88. package/dist/scan.js +67 -6
  89. package/dist/scan.js.map +1 -1
  90. package/dist/schema.d.ts +1 -0
  91. package/dist/schema.d.ts.map +1 -1
  92. package/dist/schema.js +231 -5
  93. package/dist/schema.js.map +1 -1
  94. package/dist/site-identity.d.ts +88 -0
  95. package/dist/site-identity.d.ts.map +1 -0
  96. package/dist/site-identity.js +82 -0
  97. package/dist/site-identity.js.map +1 -0
  98. package/dist/treatment-word-budget.d.ts +31 -0
  99. package/dist/treatment-word-budget.d.ts.map +1 -0
  100. package/dist/treatment-word-budget.js +80 -0
  101. package/dist/treatment-word-budget.js.map +1 -0
  102. package/dist/types.d.ts +76 -7
  103. package/dist/types.d.ts.map +1 -1
  104. package/dist/types.js +9 -7
  105. package/dist/types.js.map +1 -1
  106. package/fit-signals.json +11 -0
  107. package/package.json +201 -11
  108. package/skill/SKILL.md +49 -0
  109. package/src/addressability.ts +186 -4
  110. package/src/approval-bypass.ts +2263 -0
  111. package/src/approval.ts +491 -0
  112. package/src/approve-cli.ts +474 -0
  113. package/src/cli.ts +274 -22
  114. package/src/field-options.ts +134 -0
  115. package/src/fold-wallpaper-gate.ts +29 -0
  116. package/src/front-door.check.ts +10 -0
  117. package/src/front-door.ts +312 -0
  118. package/src/generated/advisor-plan-contract.generated.ts +430 -0
  119. package/src/generated/contract-schema.generated.ts +520 -0
  120. package/src/index.ts +107 -1
  121. package/src/live-copy-gate.ts +150 -0
  122. package/src/messaging-kit.ts +195 -0
  123. package/src/passage.ts +2 -1
  124. package/src/plan-authority.ts +160 -0
  125. package/src/plan-contract.ts +45 -0
  126. package/src/plan-digest.ts +81 -0
  127. package/src/plan-rules.ts +179 -0
  128. package/src/render-registry-parity.ts +65 -0
  129. package/src/resolve.ts +185 -19
  130. package/src/scan.ts +97 -6
  131. package/src/schema.ts +247 -5
  132. package/src/site-identity.ts +125 -0
  133. package/src/treatment-word-budget.ts +100 -0
  134. package/src/types.ts +83 -7
  135. package/templates/front-door.en.json +426 -0
  136. package/CHANGELOG.md +0 -330
@@ -0,0 +1,2263 @@
1
+ /**
2
+ * `scanApprovalBypass` and `checkApprovalBypass` — the APPROVAL-BYPASS gate:
3
+ * does consumer code route copy through the registry's approval lifecycle,
4
+ * or around it? Two patterns defeat that lifecycle while every other gate in
5
+ * this package stays green:
6
+ *
7
+ * 1. `approval-set-in-code` — the code itself declares approval: an object
8
+ * property or member assignment named `approvedBy` or
9
+ * `pendingOwnerReview`, or `status` set to the literal `"approved"`
10
+ * (`{ ...entry, status: "approved" }`, `entry.status = "approved"`). An
11
+ * approval typed into a component is an approval no owner ever gave.
12
+ * 2. `copy-read-without-resolver` — the code imports the registry file and
13
+ * reads its content directly (`registry.entries.map(...)`, a spread, any
14
+ * use other than the first argument of `createCopyResolver`,
15
+ * `resolveCopyRef`, or `validateCopyRegistryShape`, or of
16
+ * `parseCopyRegistry` only when its return value is passed to
17
+ * `createCopyResolver` or `resolveCopyRef`).
18
+ * A raw map built from the registry skips every lifecycle, staleness and
19
+ * delegate check the resolver performs.
20
+ *
21
+ * ONLY REGISTRY-COUPLED FILES ARE EXAMINED — a file that imports from
22
+ * `@clossys/writer` (any subpath) or imports the registry file. That keeps a
23
+ * domain model with its own unrelated `status: "approved"` out of scope. It
24
+ * is a STATED LIMIT, not an oversight: a file that copies registry data
25
+ * without importing either (pasted JSON, data fetched at runtime, a registry
26
+ * passed in from an uncoupled module) is never seen by this gate.
27
+ *
28
+ * NO PARSER LIBRARY and no second tokenizer: every comment, string, template,
29
+ * regex and JSX-text boundary comes from `scan.ts`'s `maskNonCode`. The
30
+ * patterns here are matched against its masked `code` (so a comment or a
31
+ * string can never match), and literal values are looked up through its
32
+ * `literals` list. The heuristics that remain are narrow and local; each
33
+ * known gap is listed beside the function that has it.
34
+ *
35
+ * ============================================================================
36
+ * THE TERNARY
37
+ * ============================================================================
38
+ *
39
+ * The same precedence `addressability.ts` uses (issue #407): a definite
40
+ * finding outranks an incomplete picture.
41
+ *
42
+ * - `"violated"` — at least one finding, regardless of unchecked positions.
43
+ * `reasons` still lists any coverage gap.
44
+ * - `"indeterminate"` — no finding, but an unchecked position (a dynamic or
45
+ * otherwise unresolvable registry load, a string naming the registry), a
46
+ * parse failure, or zero files scanned. Never a pass.
47
+ * - `"satisfied"` — at least one file scanned, nothing unchecked, no
48
+ * finding. Zero coupled files among ≥1 scanned file is satisfied.
49
+ */
50
+
51
+ import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
52
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
53
+ import { maskNonCode, type MaskedLiteral } from "./scan.js";
54
+ import type { ParseFailure, SkippedFile } from "./scan.js";
55
+
56
+ export type { ParseFailure, SkippedFile } from "./scan.js";
57
+
58
+ // ------------------------------------------------------------------ types
59
+
60
+ export interface ApprovalBypassScanOptions {
61
+ /** The copy registry file. Absolute, or relative to the scan `root`. Must exist. */
62
+ registryPath: string;
63
+ /** File extensions to read, each including the leading dot. Default: `.ts`, `.tsx`, `.js`, `.jsx`. */
64
+ extensions?: string[];
65
+ /** Directory names never descended into. Default: node_modules, .git, dist, build, coverage. */
66
+ skipDirs?: string[];
67
+ }
68
+
69
+ export type ApprovalBypassRule = "approval-set-in-code" | "copy-read-without-resolver";
70
+
71
+ export interface ApprovalBypassFinding {
72
+ rule: ApprovalBypassRule;
73
+ severity: "error";
74
+ file: string;
75
+ line: number;
76
+ detail: string;
77
+ }
78
+
79
+ export interface ApprovalBypassUncheckedItem {
80
+ file: string;
81
+ line: number;
82
+ /** `"registry-dynamic-import"`, `"dynamic-import-non-literal"`, `"registry-require-unbound"`, `"require-non-literal"`, `"registry-reexport"` or `"registry-path-string"`. */
83
+ kind: string;
84
+ detail: string;
85
+ }
86
+
87
+ export interface ApprovalBypassScanResult {
88
+ /** Files successfully tokenized, coupled or not. Required to be > 0 for a satisfied verdict. */
89
+ filesScanned: number;
90
+ /** Root-relative paths of the files that were examined. */
91
+ coupledFiles: string[];
92
+ findings: ApprovalBypassFinding[];
93
+ unchecked: ApprovalBypassUncheckedItem[];
94
+ skippedByDesign: SkippedFile[];
95
+ parseFailures: ParseFailure[];
96
+ }
97
+
98
+ export type ApprovalBypassVerdict = "violated" | "indeterminate" | "satisfied";
99
+
100
+ export interface ApprovalBypassGateResult {
101
+ verdict: ApprovalBypassVerdict;
102
+ findings: ApprovalBypassFinding[];
103
+ unchecked: ApprovalBypassUncheckedItem[];
104
+ /** Why coverage is incomplete. Empty for `"satisfied"`; may be non-empty for `"violated"`. */
105
+ reasons: string[];
106
+ }
107
+
108
+ export interface ApprovalBypassExtractResult {
109
+ coupled: boolean;
110
+ findings: ApprovalBypassFinding[];
111
+ unchecked: ApprovalBypassUncheckedItem[];
112
+ parseFailure?: string;
113
+ }
114
+
115
+ // --------------------------------------------------------------- walking
116
+
117
+ const DEFAULT_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx"];
118
+ const DEFAULT_SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", "coverage"]);
119
+ /** The same test/spec/check and `.d.ts` pattern `scan.ts` and `addressability.ts` use. */
120
+ const SKIP_FILE_RE = /\.(test|spec|check)\.(ts|tsx|js|jsx)$|\.d\.ts$/;
121
+
122
+ function realOrResolved(path: string): string {
123
+ const resolved = resolve(path);
124
+ if (!existsSync(resolved)) return resolved;
125
+ try {
126
+ return realpathSync(resolved);
127
+ } catch {
128
+ return resolved;
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Walks `root`, examining every registry-coupled source file (see
134
+ * `extractApprovalBypass`). FAILS CLOSED — throws a plain `Error` — on an
135
+ * unreadable directory or file and on a missing registry file: either one
136
+ * means the scan cannot say what it did not see.
137
+ */
138
+ export function scanApprovalBypass(root: string, options: ApprovalBypassScanOptions): ApprovalBypassScanResult {
139
+ const extensions = new Set((options.extensions ?? DEFAULT_EXTENSIONS).map((e) => e.toLowerCase()));
140
+ const skipDirs = new Set(options.skipDirs ?? DEFAULT_SKIP_DIRS);
141
+ const registryAbs = isAbsolute(options.registryPath) ? resolve(options.registryPath) : resolve(root, options.registryPath);
142
+ if (!existsSync(registryAbs)) {
143
+ throw new Error(`scanApprovalBypass: registry file "${options.registryPath}" does not exist (resolved "${registryAbs}")`);
144
+ }
145
+ const registryReal = realOrResolved(registryAbs);
146
+
147
+ const result: ApprovalBypassScanResult = {
148
+ filesScanned: 0,
149
+ coupledFiles: [],
150
+ findings: [],
151
+ unchecked: [],
152
+ skippedByDesign: [],
153
+ parseFailures: [],
154
+ };
155
+
156
+ function walk(dir: string): void {
157
+ let entries: string[];
158
+ try {
159
+ entries = readdirSync(dir);
160
+ } catch (error) {
161
+ throw new Error(`scanApprovalBypass: cannot read directory "${dir}": ${error instanceof Error ? error.message : String(error)}`);
162
+ }
163
+ for (const entry of entries) {
164
+ if (skipDirs.has(entry)) continue;
165
+ const full = join(dir, entry);
166
+ let stat;
167
+ try {
168
+ stat = statSync(full);
169
+ } catch {
170
+ continue; // broken symlink — nothing to read, not a directory-listing failure
171
+ }
172
+ if (stat.isDirectory()) {
173
+ walk(full);
174
+ continue;
175
+ }
176
+ if (!stat.isFile()) continue;
177
+ if (!extensions.has(extname(entry).toLowerCase())) continue;
178
+ if (realOrResolved(full) === registryReal) continue; // the registry itself is data, not consumer code
179
+
180
+ const relPath = relative(root, full).split(sep).join("/");
181
+ if (SKIP_FILE_RE.test(entry)) {
182
+ result.skippedByDesign.push({ file: relPath, reason: "test-or-check-file" });
183
+ continue;
184
+ }
185
+
186
+ let content: string;
187
+ try {
188
+ content = readFileSync(full, "utf8");
189
+ } catch (error) {
190
+ throw new Error(`scanApprovalBypass: cannot read file "${full}": ${error instanceof Error ? error.message : String(error)}`);
191
+ }
192
+
193
+ const extracted = extractApprovalBypass(content, relPath, { registryPath: registryAbs, absoluteFilePath: full });
194
+ if (extracted.parseFailure !== undefined) {
195
+ result.parseFailures.push({ file: relPath, detail: extracted.parseFailure });
196
+ continue;
197
+ }
198
+ result.filesScanned++;
199
+ if (!extracted.coupled) continue;
200
+ result.coupledFiles.push(relPath);
201
+ result.findings.push(...extracted.findings);
202
+ result.unchecked.push(...extracted.unchecked);
203
+ }
204
+ }
205
+
206
+ walk(root);
207
+ return result;
208
+ }
209
+
210
+ // ------------------------------------------------------- code-text helpers
211
+
212
+ const IDENT = "[A-Za-z_$][\\w$]*";
213
+ const ID_CONTINUE_IN_CLASS = "\\p{ID_Continue}$";
214
+ /** Character before an allowed callee must not continue an identifier, `$`, or `.`. */
215
+ const CALLEE_BOUNDARY = `(?<![${ID_CONTINUE_IN_CLASS}.])`;
216
+ const NOT_ID_CONTINUE = `(?![${ID_CONTINUE_IN_CLASS}])`;
217
+ const WRITER_PACKAGE_CALLEES = ["createCopyResolver", "resolveCopyRef", "parseCopyRegistry", "validateCopyRegistryShape"] as const;
218
+ const WRITER_PACKAGE_CALLEE_SET = new Set<string>(WRITER_PACKAGE_CALLEES);
219
+ const APPROVAL_KEYS = new Set(["approvedBy", "pendingOwnerReview"]);
220
+
221
+ function isWs(c: string | undefined): boolean {
222
+ return c === " " || c === "\t" || c === "\n" || c === "\r" || c === "\f" || c === "\v";
223
+ }
224
+
225
+ /**
226
+ * The next position at or after `idx` that is not whitespace and not inside a
227
+ * comment. `code` is masked, so a comment is a run of blanks; this skips it by
228
+ * shape. Never a fixed window.
229
+ */
230
+ function nextSignificant(code: string, idx: number): number {
231
+ let i = idx;
232
+ const n = code.length;
233
+ while (i < n) {
234
+ const c = code[i]!;
235
+ if (isWs(c)) {
236
+ i++;
237
+ continue;
238
+ }
239
+ if (c === "/" && code[i + 1] === "/") {
240
+ const nl = code.indexOf("\n", i);
241
+ i = nl === -1 ? n : nl;
242
+ continue;
243
+ }
244
+ if (c === "/" && code[i + 1] === "*") {
245
+ let end = i + 2;
246
+ while (end < n && !(code[end] === "*" && code[end + 1] === "/")) end++;
247
+ i = end >= n ? n : end + 2;
248
+ continue;
249
+ }
250
+ break;
251
+ }
252
+ return i;
253
+ }
254
+
255
+ /**
256
+ * Reads the assignment operator at `idx` (already advanced to the operator by
257
+ * `nextSignificant`), returning its length, or 0 when it is not one. A
258
+ * `+`/`-`/`*`/`/`/`%`/`&`/`|`/`^` before the `=` is skipped only when it is
259
+ * not itself the first character of a comparison operator (`==`, `!=`, `<=`,
260
+ * `>=`, `&&`, `||`), so `x.approvedBy === y` is a read, never an assignment.
261
+ */
262
+ function assignOpLengthAt(code: string, idx: number): number {
263
+ let i = idx;
264
+ const c = code[i];
265
+ if (c === "?" || c === "|" || c === "&") {
266
+ if (code[i + 1] !== c) return 0;
267
+ i += 2;
268
+ } else if (c === "+" || c === "-" || c === "*" || c === "/" || c === "%" || c === "^") {
269
+ i += 1;
270
+ }
271
+ if (code[i] !== "=") return 0;
272
+ const next = code[i + 1];
273
+ if (next === "=" || next === ">") return 0;
274
+ return i - idx + 1;
275
+ }
276
+
277
+ /**
278
+ * Length of a `:`-only object key separator at `idx`, or 0. A `::` (or a `:`
279
+ * followed by another `:`) is not a key separator.
280
+ */
281
+ function colonLengthAt(code: string, idx: number): number {
282
+ return code[idx] === ":" && code[idx + 1] !== ":" ? 1 : 0;
283
+ }
284
+
285
+ /** Whether the character ending just before `end` is a plain registry binding reference (not a suffix of a longer word or `.`). */
286
+ function isBindingReferenceEnd(code: string, end: number): boolean {
287
+ for (let i = end - 1; i >= 0; i--) {
288
+ const c = code[i]!;
289
+ if (isWs(c)) continue;
290
+ return /[\w$)\]]/.test(c);
291
+ }
292
+ return false;
293
+ }
294
+
295
+ /** Escapes every regular-expression metacharacter in `value`, backslash included. */
296
+ function escapeRegExp(value: string): string {
297
+ return value.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
298
+ }
299
+
300
+ function prevNonWs(code: string, idx: number): number {
301
+ let i = idx - 1;
302
+ while (i >= 0 && isWs(code[i])) i--;
303
+ return i;
304
+ }
305
+
306
+ function nextNonWs(code: string, idx: number): number {
307
+ let i = idx;
308
+ while (i < code.length && isWs(code[i])) i++;
309
+ return i;
310
+ }
311
+
312
+ /**
313
+ * Start index just after the nearest real statement boundary at or before
314
+ * `idx`: file start, a top-level `;`, or a top-level `{` that opens an
315
+ * enclosing block. Parentheses and brackets do NOT bound the segment — a
316
+ * callee (`require(`, `import(`) or a type keyword may precede them — and
317
+ * nested brackets are balanced on the way back, so a `}` closing an import
318
+ * clause or a destructuring pattern never ends the segment early. The length
319
+ * of the segment is arbitrary: there is no fixed character window here.
320
+ */
321
+ function boundaryStartBefore(code: string, idx: number): number {
322
+ let depth = 0;
323
+ for (let i = idx - 1; i >= 0; i--) {
324
+ const c = code[i]!;
325
+ if (c === ")" || c === "]" || c === "}") depth++;
326
+ else if (c === "(" || c === "[" || c === "{") {
327
+ if (depth > 0) {
328
+ depth--;
329
+ continue;
330
+ }
331
+ if (c === "{") return i + 1; // an enclosing block bounds the statement
332
+ } else if (depth === 0 && c === ";") return i + 1;
333
+ }
334
+ return 0;
335
+ }
336
+
337
+ /**
338
+ * End index just past the next real statement boundary at or after `idx`: a
339
+ * top-level `;`, `{` or `}`. An unmatched `)`/`]` (the closer of an enclosing
340
+ * callee or parameter list) does not end the segment, and nested brackets are
341
+ * balanced, so the segment length is arbitrary — no fixed window.
342
+ */
343
+ function forwardSegmentEnd(code: string, idx: number): number {
344
+ let depth = 0;
345
+ for (let i = idx; i < code.length; i++) {
346
+ const c = code[i]!;
347
+ if (c === "(" || c === "[" || c === "{") {
348
+ if (depth === 0 && c === "{") return i + 1;
349
+ depth++;
350
+ } else if (c === ")" || c === "]") {
351
+ if (depth > 0) depth--;
352
+ } else if (c === "}") {
353
+ if (depth === 0) return i + 1;
354
+ depth--;
355
+ } else if (depth === 0 && c === ";") return i + 1;
356
+ }
357
+ return code.length;
358
+ }
359
+
360
+ /** Index of the nearest unclosed `{`, `(` or `[` before `idx`, or -1. `code` is masked, so every bracket seen is real code. */
361
+ function enclosingOpener(code: string, idx: number): number {
362
+ let depth = 0;
363
+ for (let i = idx - 1; i >= 0; i--) {
364
+ const c = code[i];
365
+ if (c === "}" || c === ")" || c === "]") depth++;
366
+ else if (c === "{" || c === "(" || c === "[") {
367
+ if (depth === 0) return i;
368
+ depth--;
369
+ }
370
+ }
371
+ return -1;
372
+ }
373
+
374
+ let matchingCloseTableCode: string | undefined;
375
+ let matchingCloseTable: Int32Array | undefined;
376
+
377
+ function buildMatchingCloseTable(code: string): Int32Array {
378
+ const closeAt = new Int32Array(code.length);
379
+ closeAt.fill(-1);
380
+ const stack: number[] = [];
381
+ for (let i = 0; i < code.length; i++) {
382
+ const c = code[i];
383
+ if (c === "{" || c === "(" || c === "[") stack.push(i);
384
+ else if (c === "}" || c === ")" || c === "]") {
385
+ if (stack.length === 0) continue;
386
+ const open = stack.pop()!;
387
+ closeAt[open] = i;
388
+ }
389
+ }
390
+ return closeAt;
391
+ }
392
+
393
+ function matchingClose(code: string, open: number): number {
394
+ if (matchingCloseTableCode !== code) {
395
+ matchingCloseTableCode = code;
396
+ matchingCloseTable = buildMatchingCloseTable(code);
397
+ }
398
+ const c = code[open];
399
+ if (c !== "{" && c !== "(" && c !== "[") return -1;
400
+ return matchingCloseTable![open] ?? -1;
401
+ }
402
+
403
+ const ID_CONTINUE_RE = /[\p{ID_Continue}$]/u;
404
+ const ID_START_RE = /[\p{ID_Start}$_]/u;
405
+
406
+ function isHighSurrogateUnit(c: string): boolean {
407
+ const u = c.charCodeAt(0);
408
+ return u >= 0xd800 && u <= 0xdbff;
409
+ }
410
+
411
+ function isLowSurrogateUnit(c: string): boolean {
412
+ const u = c.charCodeAt(0);
413
+ return u >= 0xdc00 && u <= 0xdfff;
414
+ }
415
+
416
+ /** Index of the first UTF-16 code unit of the code point ending at `end`. */
417
+ function codePointStartAt(code: string, end: number): number {
418
+ if (end > 0 && isLowSurrogateUnit(code[end]!) && isHighSurrogateUnit(code[end - 1]!)) return end - 1;
419
+ return end;
420
+ }
421
+
422
+ /** Index of the last UTF-16 code unit of the code point starting at `start`. */
423
+ function codePointEndAt(code: string, start: number): number {
424
+ if (isHighSurrogateUnit(code[start]!) && start + 1 < code.length && isLowSurrogateUnit(code[start + 1]!)) return start + 1;
425
+ return start;
426
+ }
427
+
428
+ function codePointBefore(code: string, start: number): string | undefined {
429
+ if (start <= 0) return undefined;
430
+ const prevEnd = start - 1;
431
+ const prevStart = codePointStartAt(code, prevEnd);
432
+ return code.slice(prevStart, codePointEndAt(code, prevStart) + 1);
433
+ }
434
+
435
+ function codePointAt(code: string, start: number): string {
436
+ const end = codePointEndAt(code, start);
437
+ return code.slice(start, end + 1);
438
+ }
439
+
440
+ function isIdContinueCodePoint(cp: string): boolean {
441
+ return ID_CONTINUE_RE.test(cp);
442
+ }
443
+
444
+ function isIdStartCodePoint(cp: string): boolean {
445
+ return ID_START_RE.test(cp);
446
+ }
447
+
448
+ /** Reads the identifier immediately before `(` at `innerOpen`, or `undefined` when it is not a plain callee name. */
449
+ function calleeBeforeOpenParen(code: string, innerOpen: number): string | undefined {
450
+ let i = innerOpen - 1;
451
+ while (i >= 0 && isWs(code[i])) i--;
452
+ if (i < 0) return undefined;
453
+ const identEnd = i;
454
+ let end = i;
455
+ while (end >= 0) {
456
+ const cpStart = codePointStartAt(code, end);
457
+ if (!isIdContinueCodePoint(codePointAt(code, cpStart))) break;
458
+ end = cpStart - 1;
459
+ }
460
+ const start = end + 1;
461
+ const ident = code.slice(start, identEnd + 1);
462
+ if (ident.length === 0 || !isIdStartCodePoint(codePointAt(code, start))) return undefined;
463
+ const before = codePointBefore(code, start);
464
+ if (before !== undefined) {
465
+ if (before === "." || before === "$" || isIdContinueCodePoint(before)) return undefined;
466
+ }
467
+ return ident;
468
+ }
469
+
470
+ /** Innermost `(` call whose argument list contains `idx`. `code` is masked. */
471
+ function callContaining(code: string, idx: number): { open: number; close: number; callee: string } | undefined {
472
+ let depth = 0;
473
+ let innerOpen = -1;
474
+ for (let k = idx; k >= 0; k--) {
475
+ const c = code[k];
476
+ if (c === ")") depth++;
477
+ else if (c === "(") {
478
+ if (depth === 0) {
479
+ innerOpen = k;
480
+ break;
481
+ }
482
+ depth--;
483
+ }
484
+ }
485
+ if (innerOpen === -1) return undefined;
486
+ const close = matchingClose(code, innerOpen);
487
+ if (close === -1 || idx > close) return undefined;
488
+ const callee = calleeBeforeOpenParen(code, innerOpen);
489
+ if (!callee) return undefined;
490
+ return { open: innerOpen, close, callee };
491
+ }
492
+
493
+ function isResolverExport(exportName: string): boolean {
494
+ return exportName === "createCopyResolver" || exportName === "resolveCopyRef";
495
+ }
496
+
497
+ interface WriterCalleeImports {
498
+ localNames: Set<string>;
499
+ localToExport: Map<string, string>;
500
+ }
501
+
502
+ /**
503
+ * When a registry import binding is the argument to `parseCopyRegistry`, the
504
+ * parse result must reach `createCopyResolver`/`resolveCopyRef` — not be
505
+ * read inline (`parseCopyRegistry(registry).entries`) or passed elsewhere.
506
+ */
507
+ function writerCalleeAllowed(
508
+ callee: string,
509
+ writerImports: WriterCalleeImports,
510
+ localShadowedCalleepNames: Set<string>,
511
+ ): boolean {
512
+ return writerImports.localNames.has(callee) && !localShadowedCalleepNames.has(callee);
513
+ }
514
+
515
+ function declaratorListInitEndsAtCall(code: string, declKeywordEnd: number, call: { open: number; close: number }): boolean {
516
+ let i = skipWsCode(code, declKeywordEnd);
517
+ for (;;) {
518
+ const identStart = i;
519
+ i = skipIdentCode(code, i);
520
+ if (i === identStart) return false;
521
+ i = skipWsCode(code, i);
522
+ if (code[i] === ":") {
523
+ i = skipTypeAnnotation(code, i + 1);
524
+ i = skipWsCode(code, i);
525
+ }
526
+ if (code[i] === "=") {
527
+ i = skipWsCode(code, i + 1);
528
+ const initStart = i;
529
+ i = skipIdentCode(code, i);
530
+ i = skipWsCode(code, i);
531
+ if (initStart <= call.open && i === skipWsCode(code, call.open)) return true;
532
+ i = skipExpressionUntilCommaOrSemicolon(code, initStart);
533
+ }
534
+ i = skipWsCode(code, i);
535
+ if (code[i] === ",") {
536
+ i = skipWsCode(code, i + 1);
537
+ continue;
538
+ }
539
+ return false;
540
+ }
541
+ }
542
+
543
+ /** Skips one top-level declarator initializer (through nested calls and brackets). */
544
+ function skipExpressionUntilCommaOrSemicolon(code: string, i: number): number {
545
+ let depthParen = 0;
546
+ let depthBrace = 0;
547
+ let depthBracket = 0;
548
+ while (i < code.length) {
549
+ const c = code[i]!;
550
+ if (depthParen === 0 && depthBrace === 0 && depthBracket === 0 && (c === "," || c === ";")) break;
551
+ if (c === "(") depthParen++;
552
+ else if (c === ")") depthParen = Math.max(0, depthParen - 1);
553
+ else if (c === "{") depthBrace++;
554
+ else if (c === "}") depthBrace = Math.max(0, depthBrace - 1);
555
+ else if (c === "[") depthBracket++;
556
+ else if (c === "]") depthBracket = Math.max(0, depthBracket - 1);
557
+ i++;
558
+ }
559
+ return i;
560
+ }
561
+
562
+ function parseCopyRegistryBindingInitEndsAtCall(code: string, call: { open: number; close: number }): boolean {
563
+ const prefix = code.slice(0, call.open);
564
+ const matches = [...prefix.matchAll(new RegExp(`${CALLEE_BOUNDARY}(?:const|let|var)\\s+`, "gu"))];
565
+ for (let d = matches.length - 1; d >= 0; d--) {
566
+ const decl = matches[d]!;
567
+ const kwEnd = decl.index! + decl[0].length;
568
+ if (declaratorListInitEndsAtCall(code, kwEnd, call)) return true;
569
+ }
570
+ return false;
571
+ }
572
+
573
+ /** Comma in `const a = x, b = y` starts the next declarator — not a continued expression. */
574
+ function commaStartsNextDeclarator(code: string, commaIdx: number): boolean {
575
+ let j = skipWsCode(code, commaIdx + 1);
576
+ const identStart = j;
577
+ j = skipIdentCode(code, j);
578
+ if (j === identStart) return false;
579
+ j = skipWsCode(code, j);
580
+ if (code[j] === ":") {
581
+ j = skipTypeAnnotation(code, j + 1);
582
+ j = skipWsCode(code, j);
583
+ }
584
+ return code[j] === "=";
585
+ }
586
+
587
+ /** A line break before `+`, `(`, `.`, `?.`, etc. continues the initializer — not a finished binding. */
588
+ function postfixContinuesParseResult(code: string, idx: number): boolean {
589
+ const c = code[idx];
590
+ if (c === "," && commaStartsNextDeclarator(code, idx)) return false;
591
+ if (c === "+" || c === "-" || c === "*" || c === "/" || c === "%" || c === "(" || c === ",") return true;
592
+ if (c === "." && code[idx + 1] !== ".") return true;
593
+ if (code.slice(idx, idx + 2) === "?.") return true;
594
+ if (code.slice(idx, idx + 2) === "??") return true;
595
+ if (code.slice(idx, idx + 2) === "||") return true;
596
+ if (code.slice(idx, idx + 2) === "&&") return true;
597
+ if (code.slice(idx, idx + 3) === "===") return true;
598
+ if (code.slice(idx, idx + 3) === "!==") return true;
599
+ if (code.slice(idx, idx + 2) === "!=" && code[idx + 2] !== "=") return true;
600
+ if (code.slice(idx, idx + 2) === "==" && code[idx + 2] !== "=") return true;
601
+ if (code.slice(idx, idx + 2) === "<=" || code.slice(idx, idx + 2) === ">=") return true;
602
+ if (c === "<" && !ltAtIsGenericOpener(code, idx)) return true;
603
+ if (c === ">" && (idx === 0 || code[idx - 1] !== "=") && !ltAtIsGenericOpener(code, idx)) return true;
604
+ return false;
605
+ }
606
+
607
+ function parseCopyRegistryRegistryArgAllowed(
608
+ code: string,
609
+ argIdx: number,
610
+ writerImports: WriterCalleeImports,
611
+ localShadowedCalleepNames: Set<string>,
612
+ ): boolean {
613
+ const call = callContaining(code, argIdx);
614
+ if (!call) return false;
615
+ const parseExport = writerImports.localToExport.get(call.callee);
616
+ if (parseExport !== "parseCopyRegistry") return false;
617
+ if (!writerCalleeAllowed(call.callee, writerImports, localShadowedCalleepNames)) return false;
618
+ const afterClose = nextNonWs(code, call.close + 1);
619
+ const next = code[afterClose];
620
+ if (next === "." || next === "[") return false;
621
+ if (next === "," && commaStartsNextDeclarator(code, afterClose)) return true;
622
+ if (next === "," || next === ")") {
623
+ const outer = callContaining(code, call.close);
624
+ if (!outer) return false;
625
+ const outerExport = writerImports.localToExport.get(outer.callee);
626
+ return outerExport !== undefined && isResolverExport(outerExport) && writerCalleeAllowed(outer.callee, writerImports, localShadowedCalleepNames);
627
+ }
628
+ if (postfixContinuesParseResult(code, afterClose)) return false;
629
+ const gap = code.slice(call.close + 1, afterClose);
630
+ const gapHasNewline = /[\r\n]/.test(gap);
631
+ if (next === ";" || next === undefined || gapHasNewline) {
632
+ return parseCopyRegistryBindingInitEndsAtCall(code, call);
633
+ }
634
+ return false;
635
+ }
636
+
637
+ function bindingNameAtCallArgument(code: string, bindIdx: number, bindNameLen: number): boolean {
638
+ let i = bindIdx + bindNameLen;
639
+ while (i < code.length && isWs(code[i])) i++;
640
+ const c = code[i];
641
+ return c === "," || c === ")";
642
+ }
643
+
644
+ function registryBindingUseAllowed(
645
+ code: string,
646
+ bindIdx: number,
647
+ bindNameLen: number,
648
+ fromParsedRegistry: boolean,
649
+ parseRegistryAssign: boolean,
650
+ writerImports: WriterCalleeImports,
651
+ localShadowedCalleepNames: Set<string>,
652
+ ): boolean {
653
+ if (!bindingNameAtCallArgument(code, bindIdx, bindNameLen)) return false;
654
+ const call = callContaining(code, bindIdx);
655
+ if (!call) return false;
656
+ if (!writerCalleeAllowed(call.callee, writerImports, localShadowedCalleepNames)) return false;
657
+ const exportName = writerImports.localToExport.get(call.callee);
658
+ if (exportName === undefined) return false;
659
+ if (parseRegistryAssign && !fromParsedRegistry) return false;
660
+ if (fromParsedRegistry) return isResolverExport(exportName);
661
+ if (exportName === "parseCopyRegistry") {
662
+ return parseCopyRegistryRegistryArgAllowed(code, bindIdx, writerImports, localShadowedCalleepNames);
663
+ }
664
+ return isResolverExport(exportName) || exportName === "validateCopyRegistryShape";
665
+ }
666
+
667
+ function collectWriterCalleeImports(specifiers: Specifier[], isWriter: (s: string) => boolean, code: string): WriterCalleeImports {
668
+ const localNames = new Set<string>();
669
+ const localToExport = new Map<string, string>();
670
+ const note = (local: string, imported: string): void => {
671
+ localNames.add(local);
672
+ localToExport.set(local, imported);
673
+ };
674
+ for (const spec of specifiers) {
675
+ if (!isWriter(spec.lit.text)) continue;
676
+ if (spec.form === "static") {
677
+ const clause = code.slice(spec.stmtStart + "import".length, spec.lit.start).replace(/\bfrom\s*$/, "").trim();
678
+ if (/^type\s+(?!,)/.test(clause) && clause !== "type") continue;
679
+ const named = /\{([^}]*)\}/.exec(clause);
680
+ if (named) {
681
+ for (const part of named[1]!.split(",")) {
682
+ const m = new RegExp(`^\\s*(type\\s+)?(${IDENT})?\\s*(?:as\\s+(${IDENT}))?\\s*$`, "u").exec(part);
683
+ if (!m || m[1]) continue;
684
+ const imported = m[2];
685
+ const local = m[3] ?? imported;
686
+ if (local === undefined || imported === undefined) continue;
687
+ if (WRITER_PACKAGE_CALLEE_SET.has(imported)) note(local, imported);
688
+ }
689
+ }
690
+ } else if (spec.form === "import-equals") {
691
+ const m = new RegExp(`import\\s+(?:type\\s+)?(${IDENT})\\s*=`, "u").exec(code.slice(spec.stmtStart, spec.lit.start));
692
+ if (m && WRITER_PACKAGE_CALLEE_SET.has(m[1]!)) note(m[1]!, m[1]!);
693
+ } else if (spec.form === "require") {
694
+ const pre = code.slice(boundaryStartBefore(code, spec.stmtStart), spec.stmtStart);
695
+ const decl = new RegExp(`${CALLEE_BOUNDARY}(?:const|let|var)\\s+\\{([^}]*)\\}\\s*=\\s*$`, "u").exec(pre);
696
+ if (decl) {
697
+ for (const part of decl[1]!.split(",")) {
698
+ const trimmed = part.trim();
699
+ if (!trimmed) continue;
700
+ const m = new RegExp(`^(?:(${IDENT})|(${IDENT})\\s*:\\s*(${IDENT}))\\s*$`, "u").exec(trimmed);
701
+ if (!m) continue;
702
+ const local = m[1] ?? m[3];
703
+ const imported = m[2] ?? m[1];
704
+ if (local && imported && WRITER_PACKAGE_CALLEE_SET.has(imported)) note(local, imported);
705
+ }
706
+ }
707
+ }
708
+ }
709
+ return { localNames, localToExport };
710
+ }
711
+
712
+ function writerRequireDestructureSpans(specifiers: Specifier[], isWriter: (s: string) => boolean, code: string): { start: number; end: number }[] {
713
+ const spans: { start: number; end: number }[] = [];
714
+ for (const spec of specifiers) {
715
+ if (spec.form !== "require" || !isWriter(spec.lit.text)) continue;
716
+ const preStart = boundaryStartBefore(code, spec.stmtStart);
717
+ const pre = code.slice(preStart, spec.stmtStart);
718
+ const decl = new RegExp(`${CALLEE_BOUNDARY}(?:const|let|var)\\s+\\{([^}]*)\\}\\s*=\\s*$`, "u").exec(pre);
719
+ if (decl) spans.push({ start: preStart + decl.index!, end: spec.stmtStart });
720
+ }
721
+ return spans;
722
+ }
723
+
724
+ function splitDestructuringParts(inner: string): string[] {
725
+ const parts: string[] = [];
726
+ let depth = 0;
727
+ let start = 0;
728
+ for (let i = 0; i <= inner.length; i++) {
729
+ const c = inner[i];
730
+ if (c === "{" || c === "[" || c === "(") depth++;
731
+ else if (c === "}" || c === "]" || c === ")") depth--;
732
+ else if (c === "," && depth === 0) {
733
+ parts.push(inner.slice(start, i));
734
+ start = i + 1;
735
+ }
736
+ }
737
+ parts.push(inner.slice(start));
738
+ return parts;
739
+ }
740
+
741
+ function destructuringPatternShadowsName(inner: string, name: string): boolean {
742
+ const esc = escapeRegExp(name);
743
+ for (const part of splitDestructuringParts(inner)) {
744
+ const trimmed = part.trim();
745
+ if (!trimmed) continue;
746
+ if (new RegExp(`^${esc}(?:\\s*=.*)?$`, "u").test(trimmed)) return true;
747
+ if (new RegExp(`^${IDENT}\\s*:\\s*${esc}(?:\\s*=.*)?$`, "u").test(trimmed)) return true;
748
+ if (new RegExp(`^\\.\\.\\.\\s*${esc}$`, "u").test(trimmed)) return true;
749
+ if (new RegExp(`^\\[[^\\]]+\\]\\s*:\\s*${esc}(?:\\s*=.*)?$`, "u").test(trimmed)) return true;
750
+ const computedNested = new RegExp(`^\\[[^\\]]+\\]\\s*:\\s*\\{`, "u").exec(trimmed);
751
+ if (computedNested) {
752
+ const open = trimmed.indexOf("{", computedNested.index);
753
+ const close = matchingClose(trimmed, open);
754
+ if (close > open && destructuringPatternShadowsName(trimmed.slice(open + 1, close), name)) return true;
755
+ }
756
+ const nestedObj = new RegExp(`^${IDENT}\\s*:\\s*\\{`, "u").exec(trimmed);
757
+ if (nestedObj) {
758
+ const open = trimmed.indexOf("{", nestedObj.index);
759
+ const close = matchingClose(trimmed, open);
760
+ if (close > open && destructuringPatternShadowsName(trimmed.slice(open + 1, close), name)) return true;
761
+ }
762
+ const nestedArr = new RegExp(`^${IDENT}\\s*:\\s*\\[`, "u").exec(trimmed);
763
+ if (nestedArr) {
764
+ const open = trimmed.indexOf("[", nestedArr.index);
765
+ const close = matchingClose(trimmed, open);
766
+ if (close > open && arrayPatternShadowsName(trimmed.slice(open + 1, close), name)) return true;
767
+ }
768
+ }
769
+ return false;
770
+ }
771
+
772
+ function arrayPatternShadowsName(inner: string, name: string): boolean {
773
+ const esc = escapeRegExp(name);
774
+ for (const part of splitDestructuringParts(inner)) {
775
+ const trimmed = part.trim();
776
+ if (!trimmed) continue;
777
+ if (new RegExp(`^${esc}(?:\\s*=.*)?$`, "u").test(trimmed)) return true;
778
+ if (new RegExp(`^\\.\\.\\.\\s*${esc}(?:\\s*=.*)?$`, "u").test(trimmed)) return true;
779
+ if (trimmed.startsWith("{")) {
780
+ const close = matchingClose(trimmed, 0);
781
+ if (close > 0 && destructuringPatternShadowsName(trimmed.slice(1, close), name)) return true;
782
+ }
783
+ if (trimmed.startsWith("[")) {
784
+ const close = matchingClose(trimmed, 0);
785
+ if (close > 0 && arrayPatternShadowsName(trimmed.slice(1, close), name)) return true;
786
+ }
787
+ }
788
+ return false;
789
+ }
790
+
791
+ /** Statement keywords mistaken for shorthand object methods when `{ if (x)` opens a block. */
792
+ const CONTROL_FLOW_BLOCK_KEYWORDS = new Set(["if", "while", "switch"]);
793
+
794
+ const METHOD_HEADER_MODIFIER_WORDS = new Set([
795
+ "static",
796
+ "async",
797
+ "public",
798
+ "private",
799
+ "protected",
800
+ "readonly",
801
+ "override",
802
+ "abstract",
803
+ ]);
804
+
805
+ /** Modifiers, optional `get`/`set`, and an optional generator `*` before the method name. */
806
+ function skipMethodHeaderLead(code: string, i: number): number {
807
+ for (;;) {
808
+ i = skipWsCode(code, i);
809
+ const kwStart = i;
810
+ i = skipIdentCode(code, i);
811
+ if (i === kwStart) break;
812
+ const word = code.slice(kwStart, i);
813
+ if (!METHOD_HEADER_MODIFIER_WORDS.has(word)) {
814
+ i = kwStart;
815
+ break;
816
+ }
817
+ // A leading word is a modifier only when the real method name follows it.
818
+ // A `(` or `<` right after the word means the word IS the method name and
819
+ // its own parameter list (or type-parameter list) must be read.
820
+ const after = skipWsCode(code, i);
821
+ if (code[after] === "(" || code[after] === "<") {
822
+ i = kwStart;
823
+ break;
824
+ }
825
+ i = after;
826
+ }
827
+ i = skipWsCode(code, i);
828
+ if (isKeywordAt(code, i, "get") || isKeywordAt(code, i, "set")) {
829
+ const after = skipWsCode(code, i + 3);
830
+ // `get`/`set` is an accessor keyword only when the property name follows;
831
+ // a `(` or `<` after it means `get`/`set` is itself the method name.
832
+ if (code[after] !== "(" && code[after] !== "<") i = after;
833
+ }
834
+ if (code[i] === "*") {
835
+ i++;
836
+ i = skipWsCode(code, i);
837
+ }
838
+ return i;
839
+ }
840
+
841
+ /**
842
+ * Whether the `{` at `open` opens a class body or an object-literal member
843
+ * container — the only places a newline-led `name(` is a method header.
844
+ * The class-header test walks back from `{` to the nearest structural
845
+ * boundary (`;`, `}`, `{`, or the start of the file); a header contains none
846
+ * of those, so the length of the name, heritage clause, or spacing before `{`
847
+ * can never matter. There is no fixed look-back window here.
848
+ */
849
+ function braceIsClassOrObjectLiteral(code: string, open: number): boolean {
850
+ if (code[open] !== "{") return false;
851
+ if (objectMethodBraceIsTypeOnly(code, open)) return false;
852
+ if (namespaceBodyBraceKind(code, open) === "plain") return true;
853
+ const segStart = boundaryStartBefore(code, open);
854
+ const header = code.slice(segStart, open);
855
+ if (openBraceIsClassBody(code, open)) return true;
856
+ if (/(?<![\w$.])(?:return|typeof|new|void|delete|await|yield)\s*$/u.test(header)) return true;
857
+ if (/[=,([{:?]\s*$/u.test(header)) return true;
858
+ return false;
859
+ }
860
+
861
+ /** Newline after a field initializer without `;`, inside a class or object literal. */
862
+ function newlineMayStartClassOrObjectMember(code: string, nlIdx: number): boolean {
863
+ let k = nlIdx - 1;
864
+ while (k >= 0 && isWs(code[k]!)) k--;
865
+ if (k < 0) return false;
866
+ const prev = code[k]!;
867
+ if (prev === ";" || prev === "{" || prev === "}" || prev === ",") return false;
868
+ const open = enclosingOpener(code, nlIdx);
869
+ if (open === -1 || code[open] !== "{") return false;
870
+ return braceIsClassOrObjectLiteral(code, open);
871
+ }
872
+
873
+ function isDeclareFunctionPrefix(code: string, fnKeywordIndex: number): boolean {
874
+ let i = fnKeywordIndex - 1;
875
+ while (i >= 0 && isWs(code[i])) i--;
876
+ const wordEnd = i;
877
+ while (i >= 0) {
878
+ const cpStart = codePointStartAt(code, i);
879
+ if (!isIdContinueCodePoint(codePointAt(code, cpStart))) break;
880
+ i = cpStart - 1;
881
+ }
882
+ const wordStart = i + 1;
883
+ if (code.slice(wordStart, wordEnd + 1) !== "declare") return false;
884
+ if (wordStart > 0) {
885
+ const before = codePointBefore(code, wordStart);
886
+ if (before !== undefined && (isIdContinueCodePoint(before) || before === "$" || before === ".")) return false;
887
+ }
888
+ return true;
889
+ }
890
+
891
+ function skipWsCode(code: string, i: number): number {
892
+ while (i < code.length && isWs(code[i])) i++;
893
+ return i;
894
+ }
895
+
896
+ function skipIdentCode(code: string, i: number): number {
897
+ if (i >= code.length || !isIdStartCodePoint(codePointAt(code, i))) return i;
898
+ i = codePointEndAt(code, i) + 1;
899
+ while (i < code.length && isIdContinueCodePoint(codePointAt(code, i))) i = codePointEndAt(code, i) + 1;
900
+ return i;
901
+ }
902
+
903
+ function skipPostfixTypeArrayBrackets(code: string, i: number): number {
904
+ for (;;) {
905
+ i = skipWsCode(code, i);
906
+ if (code[i] !== "[") break;
907
+ const end = matchingClose(code, i);
908
+ i = end === -1 ? code.length : end + 1;
909
+ }
910
+ return i;
911
+ }
912
+
913
+ /** A full type annotation after `:` — unions, intersections, and postfix arrays. */
914
+ function skipTypeAnnotation(code: string, i: number): number {
915
+ for (;;) {
916
+ i = skipWsCode(code, i);
917
+ i = skipTypeOperand(code, i);
918
+ i = skipWsCode(code, i);
919
+ const c = code[i];
920
+ if (c === "|" || c === "&") {
921
+ i++;
922
+ continue;
923
+ }
924
+ break;
925
+ }
926
+ return i;
927
+ }
928
+
929
+ /** One extends/type operand: a name, generic, object type, or parenthesized type. */
930
+ function skipTypeOperand(code: string, i: number): number {
931
+ i = skipWsCode(code, i);
932
+ for (;;) {
933
+ const kwStart = i;
934
+ i = skipIdentCode(code, i);
935
+ if (i === kwStart) break;
936
+ const word = code.slice(kwStart, i);
937
+ if (word === "readonly") continue;
938
+ if (word === "keyof" || word === "typeof" || word === "infer") {
939
+ i = skipWsCode(code, i);
940
+ continue;
941
+ }
942
+ if (word === "unique") {
943
+ i = skipWsCode(code, i);
944
+ i = skipIdentCode(code, i);
945
+ return skipPostfixTypeArrayBrackets(code, i);
946
+ }
947
+ i = kwStart;
948
+ break;
949
+ }
950
+ i = skipWsCode(code, i);
951
+ const c = code[i];
952
+ if (c === "{") {
953
+ const end = matchingClose(code, i);
954
+ i = end === -1 ? code.length : end + 1;
955
+ return skipPostfixTypeArrayBrackets(code, i);
956
+ }
957
+ if (c === "(") {
958
+ const end = matchingClose(code, i);
959
+ i = end === -1 ? code.length : end + 1;
960
+ i = skipWsCode(code, i);
961
+ if (code[i] === "=" && code[i + 1] === ">") {
962
+ i += 2;
963
+ i = skipTypeOperand(code, skipWsCode(code, i));
964
+ }
965
+ return skipPostfixTypeArrayBrackets(code, i);
966
+ }
967
+ if (c === "<") {
968
+ i = indexAfterGenericTypeParamList(code, i);
969
+ return skipPostfixTypeArrayBrackets(code, i);
970
+ }
971
+ if (c === undefined) return i;
972
+ i = skipQualifiedTypeName(code, i);
973
+ return skipPostfixTypeArrayBrackets(code, i);
974
+ }
975
+
976
+ /** Consumes `Foo`, `Foo.Bar`, and `Foo.Bar<Baz>` in type positions. */
977
+ function skipQualifiedTypeName(code: string, i: number): number {
978
+ i = skipIdentCode(code, i);
979
+ for (;;) {
980
+ i = skipWsCode(code, i);
981
+ if (code[i] === "<" && ltAtIsGenericOpener(code, i)) {
982
+ i = indexAfterGenericTypeParamList(code, i);
983
+ i = skipWsCode(code, i);
984
+ }
985
+ if (code[i] !== ".") break;
986
+ i++;
987
+ i = skipIdentCode(code, i);
988
+ }
989
+ return i;
990
+ }
991
+
992
+ function isKeywordAt(code: string, i: number, kw: string): boolean {
993
+ if (code.slice(i, i + kw.length) !== kw) return false;
994
+ const next = code[i + kw.length];
995
+ return next === undefined || !/[\w$]/.test(next);
996
+ }
997
+
998
+ /** Index of `{` opening a `declare` / `declare abstract` class body after the `declare` keyword. */
999
+ function declareClassBodyBraceAfterDeclareKeyword(code: string, declareIdx: number): number {
1000
+ let i = declareIdx + "declare".length;
1001
+ i = skipWsCode(code, i);
1002
+ if (isKeywordAt(code, i, "abstract")) {
1003
+ i += "abstract".length;
1004
+ i = skipWsCode(code, i);
1005
+ }
1006
+ if (!isKeywordAt(code, i, "class")) return -1;
1007
+ return classBodyBraceAfterClassKeyword(code, i);
1008
+ }
1009
+
1010
+ /**
1011
+ * Consumes one heritage-clause operand plus any call-like suffixes it carries:
1012
+ * `B`, `B<C>`, `B<{a:1}>`, `(Foo)`, `mixin(Base)`, `ns.mixin(Base)`. A `{`
1013
+ * inside a generic argument list is consumed with the angle-depth walk
1014
+ * (`skipTypeOperand`/`indexAfterGenericTypeParamList`), never mistaken for the
1015
+ * class body, and the operand's own length never decides anything.
1016
+ */
1017
+ function skipHeritageOperand(code: string, i: number): number {
1018
+ i = skipWsCode(code, i);
1019
+ // An unparenthesized `class` here is a class EXPRESSION operand, not a
1020
+ // qualified type name: `extends class D { x = 1 } { … }`. Walking it as a
1021
+ // type stops at its body brace, so the outer body would be misread. Walk
1022
+ // the whole expression (its name, type parameters, its own
1023
+ // `extends`/`implements` and its own balanced body) and continue after it —
1024
+ // to the outer `implements` or the outer body brace.
1025
+ if (isKeywordAt(code, i, "class")) {
1026
+ const body = classBodyBraceAfterClassKeyword(code, i);
1027
+ if (body !== -1) {
1028
+ const close = matchingClose(code, body);
1029
+ return close === -1 ? code.length : close + 1;
1030
+ }
1031
+ }
1032
+ i = skipTypeOperand(code, i);
1033
+ for (;;) {
1034
+ i = skipWsCode(code, i);
1035
+ // A trailing type-argument list after a heritage operand follows a `)`
1036
+ // (a call or parenthesized operand) or a dotted name, never an
1037
+ // identifier, so `ltAtIsGenericOpener` cannot decide here. In this
1038
+ // context a `<` is a type-argument list: consume the balanced list.
1039
+ if (code[i] === "<") {
1040
+ i = indexAfterGenericTypeParamList(code, i);
1041
+ continue;
1042
+ }
1043
+ if (code[i] === "(" || code[i] === "[") {
1044
+ const close = matchingClose(code, i);
1045
+ i = close === -1 ? code.length : close + 1;
1046
+ continue;
1047
+ }
1048
+ if (code[i] === ".") {
1049
+ i++;
1050
+ i = skipIdentCode(code, i);
1051
+ continue;
1052
+ }
1053
+ break;
1054
+ }
1055
+ return i;
1056
+ }
1057
+
1058
+ /**
1059
+ * Index of the `{` opening the body of the `class` keyword at `classIdx`,
1060
+ * walking the class name, its type-parameter list, and its `extends` /
1061
+ * `implements` clauses. A `{` inside any of those — a type-parameter
1062
+ * constraint (`class C<T extends { a: number }>`), a heritage generic argument
1063
+ * (`class C extends B<{a:1}>`), the same on an `abstract class` — is not the
1064
+ * body: `<`/`>` and bracket depth decide, and the distance from the keyword or
1065
+ * any interior brace never does. Returns -1 when no body follows.
1066
+ */
1067
+ function classBodyBraceAfterClassKeyword(code: string, classIdx: number): number {
1068
+ let i = classIdx + "class".length;
1069
+ i = skipWsCode(code, i);
1070
+ // An anonymous class expression (`class extends Base { … }`) has no name:
1071
+ // the `{` need not follow `class` immediately, so the header walk below
1072
+ // runs whether or not a name is present. `extends`/`implements` are
1073
+ // identifiers but are the clause that an anonymous header continues with,
1074
+ // never the class name.
1075
+ const nameStart = i;
1076
+ const nameEnd = skipIdentCode(code, i);
1077
+ if (nameEnd > nameStart) {
1078
+ const word = code.slice(nameStart, nameEnd);
1079
+ if (word !== "extends" && word !== "implements") i = nameEnd;
1080
+ }
1081
+ // The optional `<…>` type-parameter list is skipped whether or not a name
1082
+ // was read: an anonymous class (`class<T> { … }`) has none, and a
1083
+ // separator (space, tab, newline, comment) before `<` must not hide the
1084
+ // body brace. `ltAtIsGenericOpener` is deliberately not consulted here.
1085
+ i = skipClassHeaderTypeParams(code, i);
1086
+ i = skipWsCode(code, i);
1087
+ if (isExtendsKeywordAt(code, i)) {
1088
+ i += "extends".length;
1089
+ i = skipWsCode(code, i);
1090
+ i = skipHeritageOperand(code, i);
1091
+ }
1092
+ i = skipWsCode(code, i);
1093
+ if (isKeywordAt(code, i, "implements")) {
1094
+ i += "implements".length;
1095
+ for (;;) {
1096
+ i = skipWsCode(code, i);
1097
+ i = skipHeritageOperand(code, i);
1098
+ i = skipWsCode(code, i);
1099
+ if (code[i] === ",") {
1100
+ i++;
1101
+ continue;
1102
+ }
1103
+ break;
1104
+ }
1105
+ }
1106
+ i = skipWsCode(code, i);
1107
+ return code[i] === "{" ? i : -1;
1108
+ }
1109
+
1110
+ /**
1111
+ * Whether `{` at `open` is the body of any `class` keyword in the preceding
1112
+ * header segment, decided by walking each candidate keyword's header
1113
+ * (`classBodyBraceAfterClassKeyword`) rather than by scanning back from `open`
1114
+ * until the first `{` — so a class whose header contains a brace (a
1115
+ * type-parameter constraint or heritage generic argument) is still recognized.
1116
+ *
1117
+ * A heritage clause may itself contain a class expression (`extends (class D)`,
1118
+ * `extends (class {})`, `extends (class D extends E {})`). The LAST `class` in
1119
+ * the segment is then the inner one, whose own header walk expects the inner
1120
+ * body and does not land on the outer brace, so taking only the last keyword
1121
+ * rejects the outer body. The body brace belongs to the class if ANY candidate
1122
+ * keyword's walk reaches it; the outermost (earliest) candidate is tried first
1123
+ * and the earlier ones are tried when a later candidate does not land here.
1124
+ */
1125
+ function openBraceIsClassBody(code: string, open: number): boolean {
1126
+ const segStart = boundaryStartBefore(code, open);
1127
+ const header = code.slice(segStart, open);
1128
+ const matches = [...header.matchAll(/(?<![\w$.])class(?![\w$])/gu)];
1129
+ for (const match of matches) {
1130
+ const classIdx = segStart + match.index!;
1131
+ if (classBodyBraceAfterClassKeyword(code, classIdx) === open) return true;
1132
+ }
1133
+ return false;
1134
+ }
1135
+
1136
+ function eachDeclareClassBodySpan(code: string, visit: (openBrace: number, closeBrace: number) => void): void {
1137
+ for (const m of code.matchAll(new RegExp(`${CALLEE_BOUNDARY}(?:export\\s+)?declare\\s+`, "gu"))) {
1138
+ const declareIdx = code.indexOf("declare", m.index!);
1139
+ if (declareIdx === -1) continue;
1140
+ const open = declareClassBodyBraceAfterDeclareKeyword(code, declareIdx);
1141
+ if (open === -1) continue;
1142
+ const close = matchingClose(code, open);
1143
+ if (close === -1) continue;
1144
+ visit(open, close);
1145
+ }
1146
+ }
1147
+
1148
+ function indexInsideDeclareBlock(code: string, idx: number, kind: "class" | "namespace"): boolean {
1149
+ if (kind === "class") {
1150
+ let inside = false;
1151
+ eachDeclareClassBodySpan(code, (open, close) => {
1152
+ if (idx > open && idx < close) inside = true;
1153
+ });
1154
+ return inside;
1155
+ }
1156
+ let inside = false;
1157
+ eachNamespaceBodySpan(code, (open, close, declare) => {
1158
+ if (declare && idx > open && idx < close) inside = true;
1159
+ });
1160
+ return inside;
1161
+ }
1162
+
1163
+ /** Whether `{` at `openBrace` opens the body of a `declare class` (any class-name length). */
1164
+ function openBraceIsDeclareClassBody(code: string, openBrace: number): boolean {
1165
+ if (code[openBrace] !== "{") return false;
1166
+ let match = false;
1167
+ eachDeclareClassBodySpan(code, (open) => {
1168
+ if (open === openBrace) match = true;
1169
+ });
1170
+ return match;
1171
+ }
1172
+
1173
+ /** `typeof` followed only by whitespace before the registry binding — a type query, not a read. */
1174
+ function bindingFollowsTypeofQuery(code: string, bindIdx: number): boolean {
1175
+ let j = bindIdx - 1;
1176
+ while (j >= 0 && isWs(code[j]!)) j--;
1177
+ if (j < 5) return false;
1178
+ if (code.slice(j - 5, j + 1) !== "typeof") return false;
1179
+ const typeofStart = j - 5;
1180
+ const before = codePointBefore(code, typeofStart);
1181
+ if (before !== undefined && (isIdContinueCodePoint(before) || before === "$" || before === ".")) return false;
1182
+ return true;
1183
+ }
1184
+
1185
+ function functionInDeclareNamespace(code: string, fnKeywordIndex: number): boolean {
1186
+ return indexInsideDeclareBlock(code, fnKeywordIndex, "namespace");
1187
+ }
1188
+
1189
+ function isDeclareNamespacePrefix(code: string, namespaceKeywordIndex: number): boolean {
1190
+ let i = namespaceKeywordIndex - 1;
1191
+ while (i >= 0 && isWs(code[i]!)) i--;
1192
+ const wordEnd = i;
1193
+ while (i >= 0 && isIdContinueCodePoint(codePointAt(code, i))) i = codePointStartAt(code, i) - 1;
1194
+ const wordStart = i + 1;
1195
+ if (wordStart > wordEnd) return false;
1196
+ if (code.slice(wordStart, wordEnd + 1) !== "declare") return false;
1197
+ if (wordStart > 0) {
1198
+ const before = codePointBefore(code, wordStart);
1199
+ if (before !== undefined && (isIdContinueCodePoint(before) || before === "$" || before === ".")) return false;
1200
+ }
1201
+ return true;
1202
+ }
1203
+
1204
+ /**
1205
+ * Visits every `namespace` body in `code`, walking from the keyword through an
1206
+ * arbitrary type-operator header (`Foo.Bar[]`, `typeof Foo.Bar`,
1207
+ * `readonly (string | number)[]`, `unique symbol`, …) to the real `{`. The
1208
+ * header length is never bounded by a character window.
1209
+ */
1210
+ function eachNamespaceBodySpan(
1211
+ code: string,
1212
+ visit: (openBrace: number, closeBrace: number, declare: boolean) => void,
1213
+ ): void {
1214
+ for (const m of code.matchAll(new RegExp(`${CALLEE_BOUNDARY}namespace\\b`, "gu"))) {
1215
+ const kwIdx = m.index!;
1216
+ const declare = isDeclareNamespacePrefix(code, kwIdx);
1217
+ let i = skipWsCode(code, kwIdx + "namespace".length);
1218
+ i = skipTypeOperand(code, i);
1219
+ i = skipWsCode(code, i);
1220
+ if (code[i] !== "{") continue;
1221
+ const close = matchingClose(code, i);
1222
+ if (close === -1) continue;
1223
+ visit(i, close, declare);
1224
+ }
1225
+ }
1226
+
1227
+ function indexInsidePlainNamespaceBlock(code: string, idx: number): boolean {
1228
+ let inside = false;
1229
+ eachNamespaceBodySpan(code, (open, close, declare) => {
1230
+ if (!declare && idx > open && idx < close) inside = true;
1231
+ });
1232
+ return inside;
1233
+ }
1234
+
1235
+ /** Namespace overload/signature declarations (`function f(x: T): R;`) are not value bindings. */
1236
+ function functionSignatureNotFollowedByBlock(code: string, fnKeywordIndex: number): boolean {
1237
+ let i = fnKeywordIndex + "function".length;
1238
+ i = skipWsCode(code, i);
1239
+ if (code[i] === "*") i++;
1240
+ i = skipIdentCode(code, i);
1241
+ i = skipWsCode(code, i);
1242
+ if (code[i] === "<") {
1243
+ i = indexAfterGenericTypeParamList(code, i);
1244
+ i = skipWsCode(code, i);
1245
+ }
1246
+ if (code[i] !== "(") return false;
1247
+ const close = matchingClose(code, i);
1248
+ if (close === -1) return false;
1249
+ i = skipWsCode(code, close + 1);
1250
+ i = skipReturnTypeAfterParamList(code, i);
1251
+ i = skipWsCode(code, i);
1252
+ return code[i] !== "{";
1253
+ }
1254
+
1255
+ function functionInPlainNamespaceWithoutBody(code: string, fnKeywordIndex: number): boolean {
1256
+ return (
1257
+ indexInsidePlainNamespaceBlock(code, fnKeywordIndex) &&
1258
+ functionSignatureNotFollowedByBlock(code, fnKeywordIndex)
1259
+ );
1260
+ }
1261
+
1262
+ function skipFunctionParamValueBinding(code: string, fnKeywordIndex: number): boolean {
1263
+ if (isDeclareFunctionPrefix(code, fnKeywordIndex)) return true;
1264
+ if (functionInDeclareNamespace(code, fnKeywordIndex)) return true;
1265
+ if (functionInPlainNamespaceWithoutBody(code, fnKeywordIndex)) return true;
1266
+ return false;
1267
+ }
1268
+
1269
+ function constructorInDeclareClass(code: string, constructorIdx: number): boolean {
1270
+ return indexInsideDeclareBlock(code, constructorIdx, "class");
1271
+ }
1272
+
1273
+ function isExtendsKeywordAt(code: string, i: number): boolean {
1274
+ return isKeywordAt(code, i, "extends");
1275
+ }
1276
+
1277
+ function skipOptionalGenericTypeParams(code: string, i: number): number {
1278
+ i = skipWsCode(code, i);
1279
+ if (code[i] !== "<" || !ltAtIsGenericOpener(code, i)) return i;
1280
+ return skipWsCode(code, indexAfterGenericTypeParamList(code, i));
1281
+ }
1282
+
1283
+ function interfaceBodyBraceIndex(code: string, openBrace: number): boolean {
1284
+ for (const m of code.matchAll(new RegExp(`\\binterface\\s+${IDENT}`, "gu"))) {
1285
+ let i = m.index! + m[0].length;
1286
+ i = skipWsCode(code, i);
1287
+ i = skipOptionalGenericTypeParams(code, i);
1288
+ i = skipWsCode(code, i);
1289
+ if (isExtendsKeywordAt(code, i)) {
1290
+ i += 7;
1291
+ i = skipWsCode(code, i);
1292
+ for (;;) {
1293
+ i = skipWsCode(code, i);
1294
+ if (code[i] === "{") break;
1295
+ i = skipTypeOperand(code, i);
1296
+ i = skipWsCode(code, i);
1297
+ if (code[i] === ",") {
1298
+ i++;
1299
+ continue;
1300
+ }
1301
+ break;
1302
+ }
1303
+ } else {
1304
+ while (i < code.length && code[i] !== "{" && code[i] !== ";") {
1305
+ const before = i;
1306
+ i = skipIdentCode(code, i);
1307
+ i = skipWsCode(code, i);
1308
+ if (i === before) break;
1309
+ }
1310
+ }
1311
+ i = skipWsCode(code, i);
1312
+ if (i === openBrace && code[i] === "{") return true;
1313
+ }
1314
+ return false;
1315
+ }
1316
+
1317
+ function indexInTypeAliasAssignmentRhs(code: string, idx: number): boolean {
1318
+ for (const m of code.matchAll(new RegExp(`\\btype\\s+${IDENT}`, "gu"))) {
1319
+ let i = m.index! + m[0].length;
1320
+ i = skipWsCode(code, i);
1321
+ i = skipOptionalGenericTypeParams(code, i);
1322
+ i = skipWsCode(code, i);
1323
+ if (code[i] !== "=") continue;
1324
+ i = skipWsCode(code, i + 1);
1325
+ const start = i;
1326
+ for (;;) {
1327
+ i = skipTypeOperand(code, i);
1328
+ i = skipWsCode(code, i);
1329
+ const c = code[i];
1330
+ if (c === "&" || c === "|") {
1331
+ i++;
1332
+ continue;
1333
+ }
1334
+ if (c === ";" || c === undefined) break;
1335
+ break;
1336
+ }
1337
+ if (idx >= start && idx < i) return true;
1338
+ }
1339
+ return false;
1340
+ }
1341
+
1342
+ function typeAliasRhsContainsBrace(code: string, openBrace: number): boolean {
1343
+ return indexInTypeAliasAssignmentRhs(code, openBrace);
1344
+ }
1345
+
1346
+ function openBraceIsTypeMemberContext(code: string, openBrace: number): boolean {
1347
+ return interfaceBodyBraceIndex(code, openBrace) || typeAliasRhsContainsBrace(code, openBrace);
1348
+ }
1349
+
1350
+ /** A `name(params): Return` member inside a type — not a runtime method. */
1351
+ function parenListIsTypeMethodSignature(code: string, close: number): boolean {
1352
+ const i = skipWsCode(code, close + 1);
1353
+ if (code[i] !== ":") return false;
1354
+ const j = skipWsCode(code, i + 1);
1355
+ const next = code[j];
1356
+ return next !== ":" && next !== "=";
1357
+ }
1358
+
1359
+ /** Single `|` or `&` before `(` — union/intersection type syntax, not `||` / `&&`. */
1360
+ function prevCharIsSingleTypeUnionOrIntersection(code: string, openIdx: number): boolean {
1361
+ const p = prevNonWs(code, openIdx);
1362
+ if (p < 0) return false;
1363
+ const c = code[p]!;
1364
+ if (c !== "|" && c !== "&") return false;
1365
+ const q = prevNonWs(code, p);
1366
+ return !(q >= 0 && code[q] === c);
1367
+ }
1368
+
1369
+ /** Flush `<` after an identifier starts a generic type argument list, not a comparison. */
1370
+ function ltAtIsGenericOpener(code: string, ltIdx: number): boolean {
1371
+ const before = codePointBefore(code, ltIdx);
1372
+ return before !== undefined && isIdContinueCodePoint(before);
1373
+ }
1374
+
1375
+ /** A `>` after `openIdx` that closes the `<` at `ltIdx`, ignoring the `>` in `=>`. */
1376
+ function hasMatchingGenericCloserAfter(code: string, ltIdx: number, afterIdx: number): boolean {
1377
+ let depth = 0;
1378
+ for (let j = ltIdx; j < code.length; j++) {
1379
+ const c = code[j]!;
1380
+ if (c === "<") depth++;
1381
+ else if (c === ">") {
1382
+ if (j > 0 && code[j - 1] === "=") continue;
1383
+ depth--;
1384
+ if (depth === 0) return j > afterIdx;
1385
+ }
1386
+ }
1387
+ return false;
1388
+ }
1389
+
1390
+ /** Index after the `>` that closes the type-parameter list opened at `ltIdx`. */
1391
+ function indexAfterGenericTypeParamList(code: string, ltIdx: number): number {
1392
+ let depth = 0;
1393
+ for (let j = ltIdx; j < code.length; j++) {
1394
+ const c = code[j]!;
1395
+ if (c === "<") depth++;
1396
+ else if (c === ">") {
1397
+ if (j > 0 && code[j - 1] === "=") continue;
1398
+ depth--;
1399
+ if (depth === 0) return j + 1;
1400
+ }
1401
+ }
1402
+ return code.length;
1403
+ }
1404
+
1405
+ /**
1406
+ * Skips the optional `<…>` type-parameter list in a CLASS HEADER, after
1407
+ * whitespace and comments, whether or not an identifier precedes the `<`.
1408
+ * `skipOptionalGenericTypeParams` cannot be used here: `ltAtIsGenericOpener`
1409
+ * requires an identifier immediately before the `<`, so a separator (a space,
1410
+ * tab, newline or comment) or an anonymous `class` with no name at all
1411
+ * defeats it. In the class-header context a `<` at the cursor is a
1412
+ * type-parameter list (the header continues only with `extends`,
1413
+ * `implements`, `<…>` or the body brace), so no identifier is needed.
1414
+ */
1415
+ function skipClassHeaderTypeParams(code: string, i: number): number {
1416
+ i = nextSignificant(code, i);
1417
+ if (code[i] !== "<") return i;
1418
+ return nextSignificant(code, indexAfterGenericTypeParamList(code, i));
1419
+ }
1420
+
1421
+ function skipReturnTypeAfterParamList(code: string, i: number): number {
1422
+ if (code[i] !== ":") return i;
1423
+ return skipTypeAnnotation(code, i + 1);
1424
+ }
1425
+
1426
+ /** Comma before `(` or `[` inside a generic or tuple type list — not a call argument comma. */
1427
+ function commaBeforeOpenIsInTypeList(code: string, openIdx: number): boolean {
1428
+ const p = prevNonWs(code, openIdx);
1429
+ if (p < 0 || code[p] !== ",") return false;
1430
+ let angle = 0;
1431
+ let square = 0;
1432
+ let paren = 0;
1433
+ for (let i = p - 1; i >= 0; i--) {
1434
+ const c = code[i]!;
1435
+ if (c === ">") {
1436
+ if (i > 0 && code[i - 1] === "=") continue;
1437
+ angle++;
1438
+ } else if (c === "<") {
1439
+ angle--;
1440
+ if (
1441
+ angle < 0 &&
1442
+ square === 0 &&
1443
+ paren === 0 &&
1444
+ ltAtIsGenericOpener(code, i) &&
1445
+ hasMatchingGenericCloserAfter(code, i, openIdx)
1446
+ )
1447
+ return true;
1448
+ } else if (c === "]") square++;
1449
+ else if (c === "[") {
1450
+ square--;
1451
+ if (square < 0 && angle === 0 && paren === 0) {
1452
+ const prev = prevNonWs(code, i);
1453
+ if (prev >= 0) {
1454
+ const pc = code[prev]!;
1455
+ if (pc === ":" || pc === "|" || pc === "&" || pc === "<") return true;
1456
+ if (pc === "," && commaBeforeOpenIsInTypeList(code, i)) return true;
1457
+ }
1458
+ }
1459
+ } else if (c === ")") paren++;
1460
+ else if (c === "(") paren--;
1461
+ }
1462
+ return false;
1463
+ }
1464
+
1465
+ /** A parenthesized parameter list in a type alias or similar — not a runtime callback. */
1466
+ function openParenIsTypeSyntax(code: string, openIdx: number): boolean {
1467
+ if (indexInsideDeclareBlock(code, openIdx, "class")) {
1468
+ const close = matchingClose(code, openIdx);
1469
+ if (close !== -1 && parenListIsTypeMethodSignature(code, close)) return true;
1470
+ }
1471
+ if (indexInTypeAliasAssignmentRhs(code, openIdx)) return true;
1472
+ const before = code.slice(boundaryStartBefore(code, openIdx), openIdx);
1473
+ if (new RegExp(`(?<![\\w$.])interface\\s+${IDENT}(?:\\s*<[^>]*>)?(?:\\s+extends\\s+[^{;]*)?\\s*$`, "u").test(before))
1474
+ return true;
1475
+ let p = prevNonWs(code, openIdx);
1476
+ if (p >= 0 && code[p] === ":") return true;
1477
+ if (prevCharIsSingleTypeUnionOrIntersection(code, openIdx)) return true;
1478
+ if (commaBeforeOpenIsInTypeList(code, openIdx)) return true;
1479
+ if (p >= 0 && code[p] === "(") {
1480
+ const q = prevNonWs(code, p);
1481
+ if (q >= 0 && code[q] === ":") return true;
1482
+ return openParenIsTypeSyntax(code, p);
1483
+ }
1484
+ if (p >= 0 && /[\w$]/.test(code[p]!)) {
1485
+ let q = p;
1486
+ while (q >= 0 && /[\w$]/.test(code[q]!)) q--;
1487
+ q = prevNonWs(code, q + 1);
1488
+ if (q >= 0 && code[q] === "{") {
1489
+ if (openBraceIsTypeMemberContext(code, q)) return true;
1490
+ }
1491
+ }
1492
+ return false;
1493
+ }
1494
+
1495
+ const PARAM_MODIFIER_RE = /^(?:(?:public|private|protected|readonly)\s+)+/u;
1496
+
1497
+ function singleParamBindingShadows(param: string, name: string): boolean {
1498
+ const esc = escapeRegExp(name);
1499
+ let trimmed = param.trim().replace(PARAM_MODIFIER_RE, "");
1500
+ if (!trimmed) return false;
1501
+ if (new RegExp(`^(?:\\.\\.\\.\\s*)?${esc}(?:\\s*:[^=,)]+|\\s*=[^,)]+|\\s*\\?|\\s*$)`, "u").test(trimmed)) return true;
1502
+ if (trimmed.startsWith("{")) {
1503
+ const close = matchingClose(trimmed, 0);
1504
+ if (close > 0) return destructuringPatternShadowsName(trimmed.slice(1, close), name);
1505
+ }
1506
+ if (trimmed.startsWith("[")) {
1507
+ const close = matchingClose(trimmed, 0);
1508
+ if (close > 0) return arrayPatternShadowsName(trimmed.slice(1, close), name);
1509
+ }
1510
+ return false;
1511
+ }
1512
+
1513
+ function paramListShadowsName(inner: string, name: string): boolean {
1514
+ let depth = 0;
1515
+ let start = 0;
1516
+ for (let i = 0; i <= inner.length; i++) {
1517
+ const c = inner[i];
1518
+ if (c === "(" || c === "{" || c === "[") depth++;
1519
+ else if (c === ")" || c === "}" || c === "]") depth--;
1520
+ else if (c === "," && depth === 0) {
1521
+ if (singleParamBindingShadows(inner.slice(start, i), name)) return true;
1522
+ start = i + 1;
1523
+ }
1524
+ }
1525
+ return singleParamBindingShadows(inner.slice(start), name);
1526
+ }
1527
+
1528
+ /** Parameter list of each `function` declaration matched by `fnDeclPrefix`, after optional type params. */
1529
+ function eachFnDeclParamListAfterName(
1530
+ code: string,
1531
+ fnDeclPrefix: string,
1532
+ onList: (paramInner: string, fnKeywordIndex: number) => boolean,
1533
+ ): boolean {
1534
+ for (const m of code.matchAll(new RegExp(`${fnDeclPrefix}${IDENT}`, "gu"))) {
1535
+ const fnIdx = code.indexOf("function", m.index!);
1536
+ if (fnIdx === -1 || fnIdx > m.index! + m[0].length) continue;
1537
+ let i = m.index! + m[0].length;
1538
+ i = skipWsCode(code, i);
1539
+ if (code[i] === "<") {
1540
+ i = indexAfterGenericTypeParamList(code, i);
1541
+ i = skipWsCode(code, i);
1542
+ }
1543
+ if (code[i] !== "(") continue;
1544
+ const open = i;
1545
+ const close = matchingClose(code, open);
1546
+ if (close === -1) continue;
1547
+ if (onList(code.slice(open + 1, close), fnIdx)) return true;
1548
+ }
1549
+ return false;
1550
+ }
1551
+
1552
+ /** Arrows after an object-property `:` or a ternary `:` are not scanned (e.g. `{ load: (make) => ... }`, `c ? g : (make) => ...`). */
1553
+ /** Whether a `catch (` binding shadows `name`, scanning left-to-right without nested-regex quantifiers. */
1554
+ function anyCatchBindingShadowsName(code: string, name: string): boolean {
1555
+ for (let i = 0; i + 5 <= code.length; i++) {
1556
+ if (code.slice(i, i + 5) !== "catch") continue;
1557
+ if (i > 0 && /[\w$.]/.test(code[i - 1]!)) continue;
1558
+ const afterWord = i + 5;
1559
+ if (afterWord < code.length && /[\w$]/.test(code[afterWord]!)) continue;
1560
+ const open = skipWsCode(code, afterWord);
1561
+ if (code[open] !== "(") continue;
1562
+ const close = matchingClose(code, open);
1563
+ if (close === -1) return false;
1564
+ if (singleParamBindingShadows(code.slice(open + 1, close), name)) return true;
1565
+ }
1566
+ return false;
1567
+ }
1568
+
1569
+ function forEachParenListShadows(code: string, name: string): boolean {
1570
+ for (let i = 0; i < code.length; i++) {
1571
+ if (code[i] !== "(") continue;
1572
+ const open = i;
1573
+ const close = matchingClose(code, open);
1574
+ if (close === -1) continue;
1575
+ const after = skipWsCode(code, close + 1);
1576
+ if (code[after] !== "=" || code[after + 1] !== ">") continue;
1577
+ if (openParenIsTypeSyntax(code, open)) continue;
1578
+ if (paramListShadowsName(code.slice(open + 1, close), name)) return true;
1579
+ }
1580
+ return false;
1581
+ }
1582
+
1583
+ /** Whether `{` at `open` opens a `namespace` body, and whether the `namespace` is ambient (`declare`). */
1584
+ function namespaceBodyBraceKind(code: string, open: number): "plain" | "declare" | undefined {
1585
+ if (code[open] !== "{") return undefined;
1586
+ const segStart = boundaryStartBefore(code, open);
1587
+ const m = new RegExp(`(?<![\\w$.])namespace\\s+[^{};]*$`, "u").exec(code.slice(segStart, open));
1588
+ if (!m) return undefined;
1589
+ return isDeclareNamespacePrefix(code, segStart + m.index) ? "declare" : "plain";
1590
+ }
1591
+
1592
+ /** Each `{|,|;|}`- or newline-led method header through its parameter list, skipping control-flow blocks. */
1593
+ function forEachContainerMethodParamList(
1594
+ code: string,
1595
+ onMethod: (parenOpen: number) => boolean | void,
1596
+ ): boolean {
1597
+ for (let lead = 0; lead < code.length; lead++) {
1598
+ const c = code[lead];
1599
+ if (c !== "{" && c !== "," && c !== ";" && c !== "}") continue;
1600
+ let i = skipWsCode(code, lead + 1);
1601
+ i = skipMethodHeaderLead(code, i);
1602
+ const methodStart = i;
1603
+ i = skipIdentCode(code, i);
1604
+ if (i === methodStart) continue;
1605
+ if (CONTROL_FLOW_BLOCK_KEYWORDS.has(code.slice(methodStart, i))) continue;
1606
+ i = skipWsCode(code, i);
1607
+ if (code[i] === "<") {
1608
+ i = indexAfterGenericTypeParamList(code, i);
1609
+ i = skipWsCode(code, i);
1610
+ }
1611
+ if (code[i] !== "(") continue;
1612
+ if (onMethod(i) === true) return true;
1613
+ }
1614
+ for (let lead = 0; lead < code.length; lead++) {
1615
+ const c = code[lead];
1616
+ if (c !== "\n" && c !== "\r") continue;
1617
+ if (!newlineMayStartClassOrObjectMember(code, lead)) continue;
1618
+ let i = skipWsCode(code, lead + 1);
1619
+ i = skipMethodHeaderLead(code, i);
1620
+ const methodStart = i;
1621
+ i = skipIdentCode(code, i);
1622
+ if (i === methodStart) continue;
1623
+ if (CONTROL_FLOW_BLOCK_KEYWORDS.has(code.slice(methodStart, i))) continue;
1624
+ i = skipWsCode(code, i);
1625
+ if (code[i] === "<") {
1626
+ i = indexAfterGenericTypeParamList(code, i);
1627
+ i = skipWsCode(code, i);
1628
+ }
1629
+ if (code[i] !== "(") continue;
1630
+ if (onMethod(i) === true) return true;
1631
+ }
1632
+ return false;
1633
+ }
1634
+
1635
+ /** Ambient or type-only `{ name(` forms — not class or object-literal methods. */
1636
+ function objectMethodBraceIsTypeOnly(code: string, openBrace: number): boolean {
1637
+ if (openBraceIsDeclareClassBody(code, openBrace)) return true;
1638
+ if (indexInsideDeclareBlock(code, openBrace + 1, "class")) return true;
1639
+ // A plain `namespace N { ... }` body sets approval like any object container.
1640
+ // An ambient `declare namespace` body is type-only. A `namespace` header past
1641
+ // the old 120-character window is still recognised, so `namespace Foo.Bar[]`
1642
+ // cannot hide its members.
1643
+ const nsKind = namespaceBodyBraceKind(code, openBrace);
1644
+ if (nsKind !== undefined) return nsKind === "declare";
1645
+ if (openBraceIsTypeMemberContext(code, openBrace)) return true;
1646
+ const before = code.slice(boundaryStartBefore(code, openBrace), openBrace);
1647
+ if (new RegExp(`(?<![\\w$.])interface\\s+${IDENT}(?:\\s*<[^>]*>)?(?:\\s+extends\\s+[^{;]*)?\\s*$`, "u").test(before))
1648
+ return true;
1649
+ if (new RegExp(`(?<![\\w$.])type\\s+${IDENT}(?:\\s*<[^{};]*)?\\s*=\\s*[^{;]*$`, "u").test(before)) return true;
1650
+ return false;
1651
+ }
1652
+
1653
+ /** Type-only object/class member context when the lead is `{`, `,`, `;`, or `}`. */
1654
+ function objectMethodContextIsTypeOnly(code: string, memberLeadIdx: number): boolean {
1655
+ if (code[memberLeadIdx] === "{") return objectMethodBraceIsTypeOnly(code, memberLeadIdx);
1656
+ const open = enclosingOpener(code, memberLeadIdx);
1657
+ if (open === -1 || code[open] !== "{") return false;
1658
+ return objectMethodBraceIsTypeOnly(code, open);
1659
+ }
1660
+
1661
+ function collectLocalShadowedCalleepNames(
1662
+ code: string,
1663
+ writerLocalNames: Set<string>,
1664
+ writerRequireDestructureDeclSpans: { start: number; end: number }[],
1665
+ ): Set<string> {
1666
+ const shadowed = new Set<string>();
1667
+ const fnDeclPrefix = `${CALLEE_BOUNDARY}(?:export\\s+default\\s+)?(?:async\\s+)?function\\s*(?:\\*\\s*)?`;
1668
+ for (const name of writerLocalNames) {
1669
+ const esc = escapeRegExp(name);
1670
+ for (const m of code.matchAll(new RegExp(`${fnDeclPrefix}${esc}${NOT_ID_CONTINUE}`, "gu"))) {
1671
+ const fnIdx = code.indexOf("function", m.index!);
1672
+ if (fnIdx !== -1 && skipFunctionParamValueBinding(code, fnIdx)) continue;
1673
+ shadowed.add(name);
1674
+ break;
1675
+ }
1676
+ if (new RegExp(`${CALLEE_BOUNDARY}class\\s+${esc}${NOT_ID_CONTINUE}`, "u").test(code)) shadowed.add(name);
1677
+ if (new RegExp(`${CALLEE_BOUNDARY}(?:const|let|var)\\s+${esc}(?:\\s*:[^=;]*)?\\s*=`, "u").test(code)) shadowed.add(name);
1678
+ if (new RegExp(`${CALLEE_BOUNDARY}(?:const|let|var)\\s+${esc}(?:\\s*:[^=;]+)?\\s*;`, "u").test(code)) shadowed.add(name);
1679
+ if (new RegExp(`${CALLEE_BOUNDARY}(?:const|let|var)\\s+[^;]+?,\\s*${esc}(?:\\s*:[^=;]*)?\\s*=`, "u").test(code))
1680
+ shadowed.add(name);
1681
+ if (new RegExp(`${CALLEE_BOUNDARY}using\\s+${esc}(?:\\s*<[^>]*)?\\s*=`, "u").test(code)) shadowed.add(name);
1682
+ if (
1683
+ new RegExp(
1684
+ `${CALLEE_BOUNDARY}for\\s+(?:await\\s+)?\\(\\s*(?:const|let|var)\\s+${esc}(?:\\s*:[^;)]+)?\\s*(?:of|in)\\b`,
1685
+ "u",
1686
+ ).test(code)
1687
+ )
1688
+ shadowed.add(name);
1689
+ for (const m of code.matchAll(new RegExp(`${CALLEE_BOUNDARY}(?:const|let|var)\\s*\\{`, "gu"))) {
1690
+ if (writerRequireDestructureDeclSpans.some((s) => m.index! >= s.start && m.index! < s.end)) continue;
1691
+ const open = m.index! + m[0].length - 1;
1692
+ const close = matchingClose(code, open);
1693
+ if (close === -1) continue;
1694
+ if (destructuringPatternShadowsName(code.slice(open + 1, close), name)) shadowed.add(name);
1695
+ }
1696
+ for (const m of code.matchAll(new RegExp(`${CALLEE_BOUNDARY}(?:const|let|var)\\s*\\[`, "gu"))) {
1697
+ const open = m.index! + m[0].length - 1;
1698
+ const close = matchingClose(code, open);
1699
+ if (close === -1) continue;
1700
+ if (arrayPatternShadowsName(code.slice(open + 1, close), name)) shadowed.add(name);
1701
+ }
1702
+ if (new RegExp(`${fnDeclPrefix}${IDENT}\\s*\\([^)]*\\.\\.\\.\\s*${esc}${NOT_ID_CONTINUE}`, "u").test(code))
1703
+ shadowed.add(name);
1704
+ const boundParam = `(?:\\.\\.\\.\\s*)?${esc}(?:\\?(?:\\s*[,):]|=|:)|(?![\\w$])\\s*(?:[,):]|=|:))`;
1705
+ const firstParamBind = `\\(\\s*${boundParam}`;
1706
+ const laterParamBind = `,\\s*(?:\\.\\.\\.\\s*)?${esc}(?:\\?(?:\\s*[,):]|=|:)|(?![\\w$])\\s*(?:[,):]|=|:))`;
1707
+ const fnNamed = `${IDENT}\\s*(?:<[^>]*>)?\\s*${firstParamBind}`;
1708
+ const fnDeclParamRes = [
1709
+ new RegExp(`${fnDeclPrefix}${fnNamed}`, "gu"),
1710
+ new RegExp(`${fnDeclPrefix}\\s*${firstParamBind}`, "gu"),
1711
+ new RegExp(`${fnDeclPrefix}${IDENT}\\s*(?:<[^>]*>)?\\s*\\([^)]*${laterParamBind}`, "gu"),
1712
+ new RegExp(`${fnDeclPrefix}\\s*\\([^)]*${laterParamBind}`, "gu"),
1713
+ new RegExp(`${fnDeclPrefix}${IDENT}\\s*(?:<[^>]*>)?\\s*\\(\\s*\\{[^}]*${esc}`, "gu"),
1714
+ new RegExp(`${fnDeclPrefix}${IDENT}\\s*(?:<[^>]*>)?\\s*\\(\\s*\\[[^\\]]*${esc}`, "gu"),
1715
+ ];
1716
+ let fnParamShadow = false;
1717
+ for (const re of fnDeclParamRes) {
1718
+ for (const m of code.matchAll(re)) {
1719
+ const fnIdx = code.indexOf("function", m.index!);
1720
+ if (fnIdx === -1 || fnIdx > m.index! + m[0].length) continue;
1721
+ if (skipFunctionParamValueBinding(code, fnIdx)) continue;
1722
+ fnParamShadow = true;
1723
+ break;
1724
+ }
1725
+ if (fnParamShadow) break;
1726
+ }
1727
+ if (fnParamShadow) shadowed.add(name);
1728
+ if (!shadowed.has(name)) {
1729
+ for (const m of code.matchAll(/constructor\s*\(/gu)) {
1730
+ if (constructorInDeclareClass(code, m.index!)) continue;
1731
+ const open = m.index! + m[0].length - 1;
1732
+ const close = matchingClose(code, open);
1733
+ if (close === -1) continue;
1734
+ if (paramListShadowsName(code.slice(open + 1, close), name)) {
1735
+ shadowed.add(name);
1736
+ break;
1737
+ }
1738
+ }
1739
+ }
1740
+ if (
1741
+ !shadowed.has(name) &&
1742
+ eachFnDeclParamListAfterName(code, fnDeclPrefix, (inner, fnIdx) => {
1743
+ if (skipFunctionParamValueBinding(code, fnIdx)) return false;
1744
+ return paramListShadowsName(inner, name);
1745
+ })
1746
+ ) {
1747
+ shadowed.add(name);
1748
+ }
1749
+ if (
1750
+ !shadowed.has(name) &&
1751
+ forEachContainerMethodParamList(code, (parenOpen) => {
1752
+ const container = enclosingOpener(code, parenOpen);
1753
+ if (container >= 0 && objectMethodContextIsTypeOnly(code, container)) return false;
1754
+ if (openParenIsTypeSyntax(code, parenOpen)) return false;
1755
+ const close = matchingClose(code, parenOpen);
1756
+ if (close === -1) return false;
1757
+ if (paramListShadowsName(code.slice(parenOpen + 1, close), name)) return true;
1758
+ return false;
1759
+ })
1760
+ ) {
1761
+ shadowed.add(name);
1762
+ }
1763
+ if (new RegExp(`${CALLEE_BOUNDARY}(?:const|let|var)\\s+${IDENT}\\s*=\\s*(?:async\\s+)?${esc}\\s*=>`, "u").test(code))
1764
+ shadowed.add(name);
1765
+ if (new RegExp(`${CALLEE_BOUNDARY}(?:const|let|var)\\s+${IDENT}\\s*=\\s*(?:async\\s+)?${firstParamBind}\\s*=>`, "u").test(code))
1766
+ shadowed.add(name);
1767
+ if (!shadowed.has(name)) {
1768
+ for (const m of code.matchAll(
1769
+ new RegExp(`${CALLEE_BOUNDARY}(?:const|let|var)\\s+${IDENT}\\s*=\\s*(?:async\\s+)?(?:<[^>]*>\\s*)?\\(\\s*`, "gu"),
1770
+ )) {
1771
+ const open = m.index! + m[0].length - 1;
1772
+ const close = matchingClose(code, open);
1773
+ if (close === -1) continue;
1774
+ if (paramListShadowsName(code.slice(open + 1, close), name)) {
1775
+ shadowed.add(name);
1776
+ break;
1777
+ }
1778
+ }
1779
+ }
1780
+ if (!shadowed.has(name)) {
1781
+ for (const m of code.matchAll(/=>\s*(?:async\s+)?(?:<[^>]*>\s*)?\(\s*/gu)) {
1782
+ const open = m.index! + m[0].length - 1;
1783
+ if (openParenIsTypeSyntax(code, open)) continue;
1784
+ const close = matchingClose(code, open);
1785
+ if (close === -1) continue;
1786
+ if (paramListShadowsName(code.slice(open + 1, close), name)) {
1787
+ shadowed.add(name);
1788
+ break;
1789
+ }
1790
+ }
1791
+ }
1792
+ if (!shadowed.has(name)) {
1793
+ if (new RegExp(`=>\\s*(?:async\\s+)?(?:<[^>]*>\\s*)?${esc}\\s*=>`, "u").test(code)) shadowed.add(name);
1794
+ }
1795
+ if (
1796
+ new RegExp(
1797
+ `${CALLEE_BOUNDARY}(?:const|let|var)\\s+${IDENT}\\s*=\\s*(?:async\\s+)?\\(\\s*${esc}\\s*:\\s*\\([^)]*\\)\\s*=>\\s*[^)]*\\)\\s*=>`,
1798
+ "u",
1799
+ ).test(code)
1800
+ )
1801
+ shadowed.add(name);
1802
+ if (!shadowed.has(name)) {
1803
+ for (const m of code.matchAll(
1804
+ new RegExp(`${CALLEE_BOUNDARY}(?:const|let|var)\\s+${IDENT}\\s*=\\s*(?:async\\s+)?(?:<[^>]*>\\s*)?\\(`, "gu"),
1805
+ )) {
1806
+ const open = m.index! + m[0].length - 1;
1807
+ const close = matchingClose(code, open);
1808
+ if (close === -1) continue;
1809
+ const after = nextNonWs(code, close + 1);
1810
+ if (code.slice(after, after + 2) !== "=>") continue;
1811
+ if (paramListShadowsName(code.slice(open + 1, close), name)) {
1812
+ shadowed.add(name);
1813
+ break;
1814
+ }
1815
+ }
1816
+ }
1817
+ if (new RegExp(`${CALLEE_BOUNDARY}\\(\\s*${esc}(?:\\s*:[^)]*)?\\)\\s*:[^=>]+\\s*=>`, "u").test(code)) shadowed.add(name);
1818
+ if (new RegExp(`${CALLEE_BOUNDARY}export\\s+default\\s+(?:async\\s+)?\\(\\s*${esc}(?:\\s*:[^)]*)?\\)`, "u").test(code))
1819
+ shadowed.add(name);
1820
+ if (!shadowed.has(name) && forEachParenListShadows(code, name)) shadowed.add(name);
1821
+ if (new RegExp(`${CALLEE_BOUNDARY}(?:const|let|var)\\s+${IDENT}\\s*=\\s*(?:async\\s+)?\\(\\s*\\[\\s*${esc}`, "u").test(code))
1822
+ shadowed.add(name);
1823
+ if (new RegExp(`${CALLEE_BOUNDARY}(?:const|let|var)\\s+${IDENT}\\s*=\\s*(?:async\\s+)?\\[\\s*${esc}\\s*=>`, "u").test(code))
1824
+ shadowed.add(name);
1825
+ if (anyCatchBindingShadowsName(code, name)) shadowed.add(name);
1826
+ }
1827
+ return shadowed;
1828
+ }
1829
+
1830
+ /**
1831
+ * Whether the `{` at `open` opens a destructuring pattern or a type rather
1832
+ * than an object literal — so `const { approvedBy } = entry`, a parameter
1833
+ * pattern, an `interface`/`type` body, a type annotation or a class body is
1834
+ * never mistaken for code that SETS approval. Local and approximate: an
1835
+ * annotation that follows a `,`-separated destructured parameter outside
1836
+ * parentheses, or a pattern shape not listed here, is treated as an object
1837
+ * literal (a false positive, never a silent miss).
1838
+ */
1839
+ function braceIsPatternOrType(code: string, open: number): boolean {
1840
+ const before = code.slice(boundaryStartBefore(code, open), open);
1841
+ if (/(?<![\w$.])(?:const|let|var)\s*$/.test(before)) return true;
1842
+ if (new RegExp(`(?<![\\w$.])interface\\s+${IDENT}(?:\\s*<[^>]*>)?(?:\\s+extends\\s+[^{};]*)?\\s*$`).test(before)) return true;
1843
+ if (new RegExp(`(?<![\\w$.])type\\s+${IDENT}(?:\\s*<[^{};]*>)?\\s*=\\s*$`).test(before)) return true;
1844
+ if (new RegExp(`(?<![\\w$.])class(?![\\w$])(?:\\s+${IDENT})?[^{};]*$`).test(before)) return true;
1845
+ if (/(?<![\w$.])(?:as|satisfies)\s*$/.test(before)) return true;
1846
+ if (/\)\s*:\s*$/.test(before)) return true; // a return-type annotation
1847
+ if (new RegExp(`(?<![\\w$.])(?:const|let|var)\\s+${IDENT}\\s*:\\s*$`).test(before)) return true;
1848
+ if (new RegExp(`[(,]\\s*${IDENT}\\s*\\??\\s*:\\s*$`).test(before) && code[enclosingOpener(code, open)] === "(") return true;
1849
+ if (/(?<![\w$.])function\b[^(){};]*\(\s*$/.test(before)) return true;
1850
+ const close = matchingClose(code, open);
1851
+ if (close === -1) return false;
1852
+ const after = code.slice(close + 1, forwardSegmentEnd(code, close + 1));
1853
+ if (/^\s*=(?![=>])/.test(after)) return true;
1854
+ if (/^\s*(?::[^(){};=]*)?\)\s*(?::[^{};=]*)?(?:=>|\{)/.test(after)) return true; // an arrow or method parameter
1855
+ if (/^\s*(?:of|in)\b/.test(after)) return true;
1856
+ return false;
1857
+ }
1858
+
1859
+ /** Whether `idx` sits inside a pattern or type, walking up through nested property values a few levels. */
1860
+ function inPatternOrType(code: string, idx: number): boolean {
1861
+ let pos = idx;
1862
+ for (let level = 0; level < 6; level++) {
1863
+ const open = enclosingOpener(code, pos);
1864
+ if (open === -1 || code[open] !== "{") return false;
1865
+ if (braceIsPatternOrType(code, open)) return true;
1866
+ const prev = prevNonWs(code, open);
1867
+ const c = prev === -1 ? undefined : code[prev];
1868
+ if (c !== ":" && c !== "," && c !== "{") return false;
1869
+ pos = open;
1870
+ }
1871
+ return false;
1872
+ }
1873
+
1874
+ /** Whether position `idx` (a key's first character) is a property key position inside an object literal: preceded by `{` or `,`, directly enclosed by `{`, not a pattern or type. */
1875
+ function isObjectKeyPosition(code: string, idx: number): boolean {
1876
+ const prev = prevNonWs(code, idx);
1877
+ if (prev === -1 || (code[prev] !== "{" && code[prev] !== ",")) return false;
1878
+ if (code[enclosingOpener(code, idx)] !== "{") return false;
1879
+ return !inPatternOrType(code, idx);
1880
+ }
1881
+
1882
+ function isApprovedLiteral(lit: MaskedLiteral | undefined): boolean {
1883
+ if (!lit) return false;
1884
+ if (lit.kind === "template" && lit.raw.includes("${")) return false;
1885
+ return lit.text === "approved";
1886
+ }
1887
+
1888
+ /** A string literal, or a template literal with no interpolation — the only arguments this gate treats as a known specifier. */
1889
+ function isPlainLiteral(lit: MaskedLiteral | undefined): lit is MaskedLiteral {
1890
+ return lit !== undefined && (lit.kind === "string" || !lit.raw.includes("${"));
1891
+ }
1892
+
1893
+ // ------------------------------------------------------------- extraction
1894
+
1895
+ interface Specifier {
1896
+ lit: MaskedLiteral;
1897
+ form: "static" | "bare" | "export-from" | "require" | "import-equals" | "dynamic";
1898
+ /** For `static`/`export-from`: index of the `import`/`export` keyword. For require/dynamic: index of the callee. */
1899
+ stmtStart: number;
1900
+ }
1901
+
1902
+ /**
1903
+ * Pure — no I/O beyond `existsSync`/`realpathSync` for relative-specifier
1904
+ * resolution. Decides whether `content` is registry-coupled and, if so,
1905
+ * reports every approval-bypass finding and unchecked position in it.
1906
+ * `filePath` is the path reported in results (and picks JSX masking for a
1907
+ * `.tsx`/`.jsx` name); `context.absoluteFilePath` is where relative
1908
+ * specifiers resolve from.
1909
+ */
1910
+ export function extractApprovalBypass(
1911
+ content: string,
1912
+ filePath: string,
1913
+ context: { registryPath: string; absoluteFilePath: string },
1914
+ ): ApprovalBypassExtractResult {
1915
+ const mask = maskNonCode(content, filePath);
1916
+ if (mask.failure !== undefined) return { coupled: false, findings: [], unchecked: [], parseFailure: mask.failure };
1917
+ const { code, literals } = mask;
1918
+
1919
+ const registryReal = realOrResolved(context.registryPath);
1920
+ const registryBase = basename(registryReal);
1921
+ const fileDir = dirname(resolve(context.absoluteFilePath));
1922
+
1923
+ const lineStarts = [0];
1924
+ for (let i = 0; i < content.length; i++) if (content[i] === "\n") lineStarts.push(i + 1);
1925
+ const lineOf = (offset: number): number => {
1926
+ let lo = 0;
1927
+ let hi = lineStarts.length - 1;
1928
+ while (lo < hi) {
1929
+ const mid = (lo + hi + 1) >> 1;
1930
+ if (lineStarts[mid]! <= offset) lo = mid;
1931
+ else hi = mid - 1;
1932
+ }
1933
+ return lo + 1;
1934
+ };
1935
+
1936
+ const litAt = new Map<number, MaskedLiteral>();
1937
+ for (const lit of literals) litAt.set(lit.start, lit);
1938
+ /** Next non-blank position in `code`, stopping at a masked literal's start (a literal is blank in `code`, so plain `nextNonWs` would skip it). */
1939
+ const nextTokenStart = (idx: number): number => {
1940
+ let i = idx;
1941
+ while (i < code.length && isWs(code[i]) && !litAt.has(i)) i++;
1942
+ return i;
1943
+ };
1944
+
1945
+ const isRegistry = (s: string): boolean => {
1946
+ if (s.startsWith("./") || s.startsWith("../")) return realOrResolved(resolve(fileDir, s)) === registryReal;
1947
+ return s === registryBase || s.endsWith(`/${registryBase}`);
1948
+ };
1949
+ const isWriter = (s: string): boolean => s === "@clossys/writer" || s.startsWith("@clossys/writer/");
1950
+
1951
+ // ---- 1. classify every literal that is an import/require specifier
1952
+ const specifiers: Specifier[] = [];
1953
+ const specifierStarts = new Set<number>();
1954
+ for (const lit of literals) {
1955
+ if (!isPlainLiteral(lit)) continue;
1956
+ const before = code.slice(boundaryStartBefore(code, lit.start), lit.start);
1957
+ const base = lit.start - before.length;
1958
+ const after = code.slice(lit.end, forwardSegmentEnd(code, lit.end));
1959
+ let spec: Specifier | undefined;
1960
+ if (lit.kind === "string" && /(?<![\w$.])from\s*$/.test(before)) {
1961
+ const kw = [...before.matchAll(/(?<![\w$.])(import|export)(?![\w$])/g)].pop();
1962
+ if (kw) spec = { lit, form: kw[1] === "import" ? "static" : "export-from", stmtStart: base + kw.index! };
1963
+ } else if (lit.kind === "string" && /(?<![\w$.])import\s*$/.test(before)) {
1964
+ spec = { lit, form: "bare", stmtStart: base + before.search(/import\s*$/) };
1965
+ } else if (/(?<![\w$.])require\s*\(\s*$/.test(before) && /^\s*\)/.test(after)) {
1966
+ const callee = base + before.search(/require\s*\(\s*$/);
1967
+ const eqSegmentStart = boundaryStartBefore(code, callee);
1968
+ const eq = new RegExp(`(?<![\\w$.])import\\s+(?:type\\s+)?${IDENT}\\s*=\\s*$`).exec(code.slice(eqSegmentStart, callee));
1969
+ spec = eq
1970
+ ? { lit, form: "import-equals", stmtStart: eqSegmentStart + eq.index }
1971
+ : { lit, form: "require", stmtStart: callee };
1972
+ } else if (/(?<![\w$.])import\s*\(\s*$/.test(before) && /^\s*[),]/.test(after)) {
1973
+ spec = { lit, form: "dynamic", stmtStart: base + before.search(/import\s*\(\s*$/) };
1974
+ }
1975
+ if (spec) {
1976
+ specifiers.push(spec);
1977
+ specifierStarts.add(lit.start);
1978
+ }
1979
+ }
1980
+
1981
+ const coupled = specifiers.some((s) => isWriter(s.lit.text) || isRegistry(s.lit.text));
1982
+ if (!coupled) return { coupled: false, findings: [], unchecked: [] };
1983
+
1984
+ const writerImports = collectWriterCalleeImports(specifiers, isWriter, code);
1985
+ const writerRequireDestructureDeclSpans = writerRequireDestructureSpans(specifiers, isWriter, code);
1986
+ const localShadowedCalleepNames = collectLocalShadowedCalleepNames(
1987
+ code,
1988
+ writerImports.localNames,
1989
+ writerRequireDestructureDeclSpans,
1990
+ );
1991
+
1992
+ const findings: ApprovalBypassFinding[] = [];
1993
+ const unchecked: ApprovalBypassUncheckedItem[] = [];
1994
+ const finding = (rule: ApprovalBypassRule, offset: number, detail: string): void => {
1995
+ findings.push({ rule, severity: "error", file: filePath, line: lineOf(offset), detail });
1996
+ };
1997
+ const uncheckedAt = (kind: string, offset: number, detail: string): void => {
1998
+ unchecked.push({ file: filePath, line: lineOf(offset), kind, detail });
1999
+ };
2000
+
2001
+ // ---- 2. registry loads: bindings to trace, and loads this gate cannot trace
2002
+ const bindings: {
2003
+ name: string;
2004
+ declStart: number;
2005
+ declEnd: number;
2006
+ fromParsedRegistry: boolean;
2007
+ parseRegistryAssign: boolean;
2008
+ }[] = [];
2009
+ for (const spec of specifiers) {
2010
+ if (!isRegistry(spec.lit.text)) continue;
2011
+ const at = spec.lit.start;
2012
+ switch (spec.form) {
2013
+ case "bare":
2014
+ break; // a side-effect import binds nothing
2015
+ case "export-from": {
2016
+ if (/^\s*type\b/.test(code.slice(spec.stmtStart + "export".length, at))) break;
2017
+ uncheckedAt("registry-reexport", at, "registry re-export is not traced by this gate");
2018
+ break;
2019
+ }
2020
+ case "static": {
2021
+ const clause = code.slice(spec.stmtStart + "import".length, at).replace(/\bfrom\s*$/, "").trim();
2022
+ if (/^type\s+(?!,)/.test(clause) && clause !== "type") break; // type-only import: no runtime data
2023
+ const decl = { declStart: spec.stmtStart, declEnd: spec.lit.end };
2024
+ const def = new RegExp(`^(${IDENT})\\s*(?:,|$)`).exec(clause);
2025
+ if (def) bindings.push({ name: def[1]!, ...decl, fromParsedRegistry: false, parseRegistryAssign: false });
2026
+ const ns = new RegExp(`\\*\\s*as\\s+(${IDENT})`).exec(clause);
2027
+ if (ns) bindings.push({ name: ns[1]!, ...decl, fromParsedRegistry: false, parseRegistryAssign: false });
2028
+ const named = /\{([^}]*)\}/.exec(clause);
2029
+ if (named) {
2030
+ for (const part of named[1]!.split(",")) {
2031
+ // A string-literal imported name (`{ "x" as y }`) is masked, leaving only `as y`.
2032
+ const m = new RegExp(`^\\s*(type\\s+)?(${IDENT})?\\s*(?:as\\s+(${IDENT}))?\\s*$`).exec(part);
2033
+ if (!m || m[1]) continue;
2034
+ const imported = m[2];
2035
+ const local = m[3] ?? imported;
2036
+ if (local === undefined) continue;
2037
+ if (imported === "default") {
2038
+ bindings.push({ name: local, ...decl, fromParsedRegistry: false, parseRegistryAssign: false });
2039
+ } else {
2040
+ finding(
2041
+ "copy-read-without-resolver",
2042
+ at,
2043
+ `named import "${local}" reads registry content directly; import the registry whole and pass it to createCopyResolver`,
2044
+ );
2045
+ }
2046
+ }
2047
+ }
2048
+ break;
2049
+ }
2050
+ case "import-equals": {
2051
+ const m = new RegExp(`import\\s+(?:type\\s+)?(${IDENT})\\s*=`).exec(code.slice(spec.stmtStart, at));
2052
+ if (m)
2053
+ bindings.push({
2054
+ name: m[1]!,
2055
+ declStart: spec.stmtStart,
2056
+ declEnd: spec.lit.end,
2057
+ fromParsedRegistry: false,
2058
+ parseRegistryAssign: false,
2059
+ });
2060
+ break;
2061
+ }
2062
+ case "require": {
2063
+ const preStart = boundaryStartBefore(code, spec.stmtStart);
2064
+ const pre = code.slice(preStart, spec.stmtStart);
2065
+ const decl = new RegExp(`(?<![\\w$.])(?:const|let|var)\\s+(${IDENT})\\s*=\\s*$`).exec(pre);
2066
+ const close = code.indexOf(")", spec.lit.end);
2067
+ // Bound only when the call is the whole initializer: `const x = require("...")` followed by `;`, `,`, `}`, a line break or EOF.
2068
+ const bound = decl !== null && /^[ \t]*(?:[;,}\r\n]|$)/.test(code.slice(close + 1));
2069
+ if (decl !== null && bound) {
2070
+ bindings.push({
2071
+ name: decl[1]!,
2072
+ declStart: preStart + decl.index,
2073
+ declEnd: close + 1,
2074
+ fromParsedRegistry: false,
2075
+ parseRegistryAssign: false,
2076
+ });
2077
+ } else {
2078
+ uncheckedAt("registry-require-unbound", at, "require of registry is not bound to a plain identifier, so its use cannot be traced");
2079
+ }
2080
+ break;
2081
+ }
2082
+ case "dynamic":
2083
+ uncheckedAt("registry-dynamic-import", at, "dynamic import of registry — the loaded module's use is not traced by this gate");
2084
+ break;
2085
+ }
2086
+ }
2087
+
2088
+ for (const m of code.matchAll(
2089
+ new RegExp(`${CALLEE_BOUNDARY}(?:const|let|var)\\s+(${IDENT})(?:\\s*:[^=;]+)?\\s*=\\s*${CALLEE_BOUNDARY}parseCopyRegistry\\s*\\(`, "gu"),
2090
+ )) {
2091
+ const name = m[1]!;
2092
+ const declStart = m.index!;
2093
+ const openParen = code.indexOf("(", m.index + m[0].length - 1);
2094
+ const close = matchingClose(code, openParen);
2095
+ if (close === -1) continue;
2096
+ const parseCall = { open: openParen, close };
2097
+ const afterClose = nextNonWs(code, close + 1);
2098
+ const endsAtParse =
2099
+ parseCopyRegistryBindingInitEndsAtCall(code, parseCall) && !postfixContinuesParseResult(code, afterClose);
2100
+ bindings.push({
2101
+ name,
2102
+ declStart,
2103
+ declEnd: close + 1,
2104
+ fromParsedRegistry: endsAtParse,
2105
+ parseRegistryAssign: true,
2106
+ });
2107
+ }
2108
+
2109
+ // ---- 3. import(...) / require(...) calls whose argument is not a plain literal
2110
+ for (const m of code.matchAll(/(?<![\w$.])(import|require)\s*\(/g)) {
2111
+ const argStart = nextTokenStart(m.index + m[0].length);
2112
+ const lit = litAt.get(argStart);
2113
+ const plain = isPlainLiteral(lit) && specifierStarts.has(argStart);
2114
+ if (plain) continue;
2115
+ if (m[1] === "import") {
2116
+ uncheckedAt("dynamic-import-non-literal", m.index, "dynamic import() with a computed argument could load the registry; not traced by this gate");
2117
+ } else {
2118
+ uncheckedAt("require-non-literal", m.index, "require() with a computed argument could load the registry; not traced by this gate");
2119
+ }
2120
+ }
2121
+
2122
+ // ---- 4. copy-read-without-resolver: every use of a registry binding
2123
+ for (const b of bindings) {
2124
+ const re = new RegExp(`(?<![\\w$])${escapeRegExp(b.name)}(?![\\w$])`, "g");
2125
+ for (const m of code.matchAll(re)) {
2126
+ const i = m.index;
2127
+ if (i >= b.declStart && i < b.declEnd) continue;
2128
+ const prev = prevNonWs(code, i);
2129
+ if (prev >= 0 && code[prev] === "." && !(code[prev - 1] === "." && code[prev - 2] === ".")) continue; // `obj.name` — a property, not the binding
2130
+ if (bindingFollowsTypeofQuery(code, i)) continue; // a type query reads no content
2131
+ const afterName = nextSignificant(code, i + b.name.length);
2132
+ if (colonLengthAt(code, afterName) > 0 && isObjectKeyPosition(code, i)) continue; // `{ name: ... }` — a key, not the binding
2133
+ if (
2134
+ registryBindingUseAllowed(
2135
+ code,
2136
+ i,
2137
+ b.name.length,
2138
+ b.fromParsedRegistry,
2139
+ b.parseRegistryAssign,
2140
+ writerImports,
2141
+ localShadowedCalleepNames,
2142
+ )
2143
+ )
2144
+ continue;
2145
+ finding(
2146
+ "copy-read-without-resolver",
2147
+ i,
2148
+ `registry binding "${b.name}" is used outside createCopyResolver/resolveCopyRef/parseCopyRegistry/validateCopyRegistryShape — copy read this way skips the approval lifecycle`,
2149
+ );
2150
+ }
2151
+ }
2152
+
2153
+ // ---- 5. approval-set-in-code
2154
+ const flagged = new Set<number>();
2155
+ const flagApproval = (keyOffset: number, detail: string): void => {
2156
+ if (flagged.has(keyOffset)) return;
2157
+ flagged.add(keyOffset);
2158
+ finding("approval-set-in-code", keyOffset, detail);
2159
+ };
2160
+ const literalAfter = (idx: number): MaskedLiteral | undefined => litAt.get(nextTokenStart(idx));
2161
+
2162
+ // 5a. identifier keys and member assignments
2163
+ for (const m of code.matchAll(/(?<![\w$])(approvedBy|pendingOwnerReview|status)(?![\w$])/g)) {
2164
+ const name = m[1]!;
2165
+ const i = m.index;
2166
+ const end = i + name.length;
2167
+ const prev = prevNonWs(code, i);
2168
+ const isMember = prev >= 0 && code[prev] === "." && code[prev - 1] !== ".";
2169
+ const afterName = nextSignificant(code, end);
2170
+ if (isMember) {
2171
+ if (code[prev - 1] === "?") continue; // `x?.name` can never be assigned
2172
+ const opLen = assignOpLengthAt(code, afterName);
2173
+ if (opLen === 0) continue;
2174
+ if (name === "status") {
2175
+ if (isApprovedLiteral(literalAfter(afterName + opLen))) flagApproval(i, `status is assigned "approved" in code`);
2176
+ } else {
2177
+ flagApproval(i, `${name} is assigned in code`);
2178
+ }
2179
+ continue;
2180
+ }
2181
+ const colonLen = colonLengthAt(code, afterName);
2182
+ if (colonLen > 0) {
2183
+ if (!isObjectKeyPosition(code, i)) continue;
2184
+ if (name === "status") {
2185
+ if (isApprovedLiteral(literalAfter(afterName + colonLen))) flagApproval(i, `status is set to "approved" in an object literal`);
2186
+ } else {
2187
+ flagApproval(i, `${name} is set in an object literal`);
2188
+ }
2189
+ continue;
2190
+ }
2191
+ // shorthand `{ approvedBy }` / `{ a, pendingOwnerReview }`
2192
+ if (name !== "status" && (code[afterName] === "," || code[afterName] === "}") && isObjectKeyPosition(code, i)) {
2193
+ flagApproval(i, `${name} is set in an object literal (shorthand property)`);
2194
+ }
2195
+ }
2196
+
2197
+ // 5b. quoted and computed keys: the key is itself a literal
2198
+ for (const lit of literals) {
2199
+ if (!isPlainLiteral(lit)) continue;
2200
+ if (lit.text !== "status" && !APPROVAL_KEYS.has(lit.text)) continue;
2201
+ if (specifierStarts.has(lit.start)) continue;
2202
+ const name = lit.text;
2203
+ const prev = prevNonWs(code, lit.start);
2204
+ const bracketed = prev >= 0 && code[prev] === "[";
2205
+ let valueFrom: number | undefined;
2206
+ let where = "";
2207
+ if (bracketed) {
2208
+ const close = nextNonWs(code, lit.end);
2209
+ if (code[close] !== "]") continue;
2210
+ const afterBracket = nextSignificant(code, close + 1);
2211
+ const colonLen = colonLengthAt(code, afterBracket);
2212
+ const opLen = assignOpLengthAt(code, afterBracket);
2213
+ if (colonLen > 0 && isObjectKeyPosition(code, prev)) {
2214
+ valueFrom = afterBracket + colonLen;
2215
+ where = "a computed object key";
2216
+ } else if (opLen > 0 && isBindingReferenceEnd(code, prev)) {
2217
+ valueFrom = afterBracket + opLen;
2218
+ where = "a bracket member assignment";
2219
+ } else continue;
2220
+ } else {
2221
+ const colonPos = nextSignificant(code, lit.end);
2222
+ const colonLen = colonLengthAt(code, colonPos);
2223
+ if (colonLen === 0 || !isObjectKeyPosition(code, lit.start)) continue;
2224
+ valueFrom = colonPos + colonLen;
2225
+ where = "a quoted object key";
2226
+ }
2227
+ if (name === "status") {
2228
+ if (isApprovedLiteral(literalAfter(valueFrom))) flagApproval(lit.start, `status is set to "approved" via ${where}`);
2229
+ } else {
2230
+ flagApproval(lit.start, `${name} is set via ${where}`);
2231
+ }
2232
+ }
2233
+
2234
+ // ---- 6. any other string that names the registry file
2235
+ for (const lit of literals) {
2236
+ if (specifierStarts.has(lit.start)) continue;
2237
+ if (!lit.text.includes(registryBase)) continue;
2238
+ uncheckedAt("registry-path-string", lit.start, "registry path string — a load through it is not traced by this gate");
2239
+ }
2240
+
2241
+ findings.sort((a, b) => a.line - b.line);
2242
+ unchecked.sort((a, b) => a.line - b.line);
2243
+ return { coupled: true, findings, unchecked };
2244
+ }
2245
+
2246
+ // ------------------------------------------------------------------- gate
2247
+
2248
+ /** Pure — decides the ternary verdict from an already-computed scan. Never throws. See "THE TERNARY" above. */
2249
+ export function checkApprovalBypass(scan: ApprovalBypassScanResult): ApprovalBypassGateResult {
2250
+ const reasons: string[] = [];
2251
+ if (scan.filesScanned === 0) {
2252
+ reasons.push(scan.parseFailures.length > 0 ? "every matched file failed to parse — nothing was actually scanned" : "no source files were scanned");
2253
+ } else if (scan.parseFailures.length > 0) {
2254
+ reasons.push(`${scan.parseFailures.length} matched file(s) could not be parsed and were never examined`);
2255
+ }
2256
+ if (scan.unchecked.length > 0) {
2257
+ reasons.push(`${scan.unchecked.length} registry load or reference(s) in coupled files could not be traced`);
2258
+ }
2259
+ const base = { findings: scan.findings, unchecked: scan.unchecked };
2260
+ if (scan.findings.length > 0) return { verdict: "violated", reasons, ...base };
2261
+ if (reasons.length > 0) return { verdict: "indeterminate", reasons, ...base };
2262
+ return { verdict: "satisfied", reasons, ...base };
2263
+ }