urtext 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +229 -0
  3. package/dist/analyze/blast-radius.d.ts +28 -0
  4. package/dist/analyze/blast-radius.js +163 -0
  5. package/dist/analyze/canonical.d.ts +27 -0
  6. package/dist/analyze/canonical.js +74 -0
  7. package/dist/analyze/citations.d.ts +256 -0
  8. package/dist/analyze/citations.js +945 -0
  9. package/dist/analyze/effects.d.ts +15 -0
  10. package/dist/analyze/effects.js +255 -0
  11. package/dist/analyze/fact.d.ts +42 -0
  12. package/dist/analyze/fact.js +46 -0
  13. package/dist/analyze/guards.d.ts +70 -0
  14. package/dist/analyze/guards.js +211 -0
  15. package/dist/analyze/index.d.ts +26 -0
  16. package/dist/analyze/index.js +52 -0
  17. package/dist/analyze/program.d.ts +15 -0
  18. package/dist/analyze/program.js +229 -0
  19. package/dist/analyze/surface.d.ts +48 -0
  20. package/dist/analyze/surface.js +396 -0
  21. package/dist/bin.d.ts +2 -0
  22. package/dist/bin.js +12 -0
  23. package/dist/cli.d.ts +110 -0
  24. package/dist/cli.js +502 -0
  25. package/dist/extract/diff.d.ts +35 -0
  26. package/dist/extract/diff.js +116 -0
  27. package/dist/extract/git.d.ts +12 -0
  28. package/dist/extract/git.js +247 -0
  29. package/dist/extract/index.d.ts +4 -0
  30. package/dist/extract/index.js +57 -0
  31. package/dist/extract/intent.d.ts +64 -0
  32. package/dist/extract/intent.js +238 -0
  33. package/dist/extract/scope.d.ts +160 -0
  34. package/dist/extract/scope.js +284 -0
  35. package/dist/extract/symbols.d.ts +24 -0
  36. package/dist/extract/symbols.js +230 -0
  37. package/dist/interpret/client.d.ts +27 -0
  38. package/dist/interpret/client.js +80 -0
  39. package/dist/interpret/index.d.ts +41 -0
  40. package/dist/interpret/index.js +86 -0
  41. package/dist/interpret/prompt.d.ts +23 -0
  42. package/dist/interpret/prompt.js +128 -0
  43. package/dist/interpret/schema.d.ts +74 -0
  44. package/dist/interpret/schema.js +103 -0
  45. package/dist/report/conceal.d.ts +63 -0
  46. package/dist/report/conceal.js +129 -0
  47. package/dist/report/coverage.d.ts +43 -0
  48. package/dist/report/coverage.js +56 -0
  49. package/dist/report/html.d.ts +4 -0
  50. package/dist/report/html.js +634 -0
  51. package/dist/report/markdown.d.ts +2 -0
  52. package/dist/report/markdown.js +168 -0
  53. package/dist/report/model.d.ts +303 -0
  54. package/dist/report/model.js +289 -0
  55. package/dist/report/pdf.d.ts +2 -0
  56. package/dist/report/pdf.js +217 -0
  57. package/dist/report/terminal.d.ts +2 -0
  58. package/dist/report/terminal.js +206 -0
  59. package/dist/report/write.d.ts +105 -0
  60. package/dist/report/write.js +160 -0
  61. package/dist/score/index.d.ts +94 -0
  62. package/dist/score/index.js +572 -0
  63. package/dist/score/reach.d.ts +126 -0
  64. package/dist/score/reach.js +320 -0
  65. package/dist/score/reconcile.d.ts +52 -0
  66. package/dist/score/reconcile.js +208 -0
  67. package/dist/types.d.ts +221 -0
  68. package/dist/types.js +10 -0
  69. package/fonts/DejaVuSans-Bold.ttf +0 -0
  70. package/fonts/DejaVuSans-Oblique.ttf +0 -0
  71. package/fonts/DejaVuSans.ttf +0 -0
  72. package/fonts/DejaVuSansMono.ttf +0 -0
  73. package/fonts/LICENSE +187 -0
  74. package/package.json +44 -0
@@ -0,0 +1,160 @@
1
+ import ts from "typescript";
2
+ /**
3
+ * How a name is qualified by the scopes around it. One rule, in one place,
4
+ * because two consumers depend on it agreeing with itself: `mapSymbols`
5
+ * (`./symbols.ts`) groups declarations by qualified name, and `collectGuards`
6
+ * (`../analyze/guards.ts`) attributes a guard to the qualified owner it runs
7
+ * in. `foldReach` then matches facts from different analyzers on that string,
8
+ * so a name qualified two different ways is a fact about one symbol filed
9
+ * under another.
10
+ *
11
+ * "Agreeing with itself" has to mean more than "both call `frameNameOf`". It
12
+ * did not, once: the declaration side grew a rule for unnamed scopes that the
13
+ * guards side never got, and neither framed an object literal, so a guard in
14
+ * `handlers.run` was attributed to a top-level `run` beside it — which stole
15
+ * that export's reference count, and, when the export lost a guard of the same
16
+ * kind in the same change, cancelled it out and reported nothing at all. Both
17
+ * sides now build their frame stack with `framesFor` and mark statement-scope
18
+ * locals with `inStatementScope`, which is where the rule lives;
19
+ * `test/extract/scope.test.ts` asks both of them about the same declaration
20
+ * and compares.
21
+ */
22
+ /**
23
+ * Path for something that belongs to no declaration — code at the top level of
24
+ * a file. Angle brackets because an identifier cannot contain them, so a
25
+ * sentinel cannot collide with a path built out of identifiers. Exported
26
+ * because the renderer has to recognise it to translate it (see
27
+ * `guardOwnerLabel` in `../score/index.ts`).
28
+ *
29
+ * Not collision-proof against every string TypeScript will accept as a frame:
30
+ * `declare module "<module>" { ... }` takes its frame from a *string literal*,
31
+ * and would put this exact text in a path. That is inert here — an ambient
32
+ * module's members are not exports of the file being reviewed, so nothing
33
+ * matches on them — but the guarantee is about identifiers, not about
34
+ * arbitrary module specifiers.
35
+ */
36
+ export declare const MODULE_OWNER = "<module>";
37
+ /**
38
+ * Segment for a function or object with no name to be known by: an arrow
39
+ * function or expression bound to nothing nameable, a method with a computed
40
+ * key, an object literal passed straight to a call. It can appear at any
41
+ * position in a path —
42
+ * `<anonymous>.inner` for a named function declared inside a callback,
43
+ * `Cls.<anonymous>` for a computed-key method — so anything that renders a path
44
+ * has to handle it anywhere, not only at the end.
45
+ */
46
+ export declare const ANONYMOUS_OWNER = "<anonymous>";
47
+ /**
48
+ * Segment for a name declared inside a *statement* scope — a bare block, a
49
+ * `for` header or body, a `catch` clause, a `case` block, a class's static
50
+ * initializer block, or a function's body. Such a name is by construction not
51
+ * a member of anything (`export` is legal only at the top level of a file or a
52
+ * namespace), and without this segment it would be qualified as if it were
53
+ * one: at the top of a file it collided with a top-level export of the same
54
+ * name, and inside a named frame it collided with a real member — a
55
+ * static-block local wore the path of the class's static method, and a
56
+ * function-local object inside `function api` wore the path of a merged
57
+ * `namespace api`'s genuinely exported member. The segment applies wherever
58
+ * the declaration sits, not only when the frame stack is empty — the
59
+ * empty-stack-only version was exactly the second collision.
60
+ *
61
+ * Statement scopes still contribute no frame of their own to the *owner*
62
+ * stack: a guard's own body is a block — `if (x) { throw }` inside `validate`
63
+ * belongs to `validate`, not to a scope between them — and framing every
64
+ * block would bury real paths. The marker enters a path only where a name is
65
+ * introduced: once, before the outermost name declared in statement position.
66
+ */
67
+ export declare const LOCAL_SCOPE = "<local>";
68
+ /**
69
+ * Frame prefixes for accessors: `get value` and `set value` are two distinct
70
+ * runtime symbols — an object can carry either without the other, and a check
71
+ * moved from one to the other genuinely stops running on reads — so they must
72
+ * not share the one path `value` (which is also what a sibling *method* named
73
+ * `value` would wear). The embedded space is what makes the frame
74
+ * collision-free: an identifier cannot contain one, so no method or property
75
+ * name can ever spell `get value`. Like the sentinels, a segment carrying one
76
+ * of these prefixes is not text a reader can find in the source verbatim, so
77
+ * `guardOwnerLabel` in `../score/index.ts` translates it ("the value getter")
78
+ * rather than printing it raw. The same caveat as `MODULE_OWNER` applies:
79
+ * `declare module "get x"` could put this shape in a path, and is inert here
80
+ * for the same reason.
81
+ */
82
+ export declare const GETTER_FRAME_PREFIX = "get ";
83
+ export declare const SETTER_FRAME_PREFIX = "set ";
84
+ /**
85
+ * Every sentinel a qualified path can contain. Anything that renders a path to
86
+ * a reader has to handle all of them, at any position — `guardOwnerLabel` in
87
+ * `../score/index.ts` does, and `test/score/index.test.ts`, "translates every
88
+ * scope sentinel wherever it sits in the path", walks this list rather than a
89
+ * remembered copy of it, so adding a fourth sentinel here fails that test until
90
+ * it has a translation.
91
+ */
92
+ export declare const SCOPE_SENTINELS: readonly string[];
93
+ /**
94
+ * The statically known text of a member name: an identifier, or a private
95
+ * `#name` — which is spelled in full at its declaration and cannot collide
96
+ * with any identifier, since `#` is not an identifier character. Throwing a
97
+ * `#name` away (the old Identifier-only check) framed every private method
98
+ * `<anonymous>`, where it shared a path with any computed-key sibling and a
99
+ * guard moved between the two cancelled to silence. Computed keys and
100
+ * string or numeric names stay undefined: only a computed key's *expression*
101
+ * is known statically, not the name it evaluates to.
102
+ */
103
+ export declare function memberNameOf(name: ts.PropertyName): string | undefined;
104
+ /**
105
+ * The frame this node contributes to the qualified names inside it, or
106
+ * undefined for a node that introduces none.
107
+ *
108
+ * Every function-like node contributes one, falling back to `ANONYMOUS_OWNER`
109
+ * rather than to nothing: a node that opens a scope and contributes no frame
110
+ * would let its contents be qualified as if they sat in the scope outside it.
111
+ * Classes, namespaces, and object literals contribute one so that a method
112
+ * comes out as `Worker.run`, `N.run`, or `handlers.run`. `export default class
113
+ * {}` and `export default function () {}` both use "default", the name they
114
+ * are exported under — a nameless class or function declaration is legal
115
+ * nowhere else, and no identifier can collide with `default` since it is a
116
+ * reserved word.
117
+ *
118
+ * An expression's *internal* name is never used, even when it has one:
119
+ * `const format = function format2() {}` frames `format`, not `format2`.
120
+ * `format2` is in scope only inside the expression, so a path built from it
121
+ * names something no other file can reach — and would collide with a real
122
+ * top-level `format2` declared beside it. The binding is what the rest of the
123
+ * program calls this function, and it is what `mapSymbols` records.
124
+ *
125
+ * Two things this does not frame, deliberately. A class body holds no
126
+ * statements, so a class frame is normally only a prefix for its members —
127
+ * except for a static initializer block, which does hold statements and
128
+ * introduces no frame of its own, so a guard inside one is attributed to the
129
+ * class (`test/analyze/guards.test.ts`, "attributes a static initializer
130
+ * block's guard to the class"). And a plain statement block is not a frame at
131
+ * all — see `LOCAL_SCOPE`.
132
+ */
133
+ export declare function frameNameOf(node: ts.Node): string | undefined;
134
+ /**
135
+ * The segments this node pushes onto a frame stack: none, its frame, or —
136
+ * when the node itself is declared in statement position — `LOCAL_SCOPE` and
137
+ * its frame. Both walkers push through here, which is what makes the local
138
+ * rule one rule rather than two that have to be kept in step.
139
+ */
140
+ export declare function framesFor(node: ts.Node): string[];
141
+ /**
142
+ * A frame stack as one dotted path, or `MODULE_OWNER` for an empty stack —
143
+ * top-level code, which belongs to no declaration. The stack already carries
144
+ * its root marker when it needs one (see `framesFor`), so this is a join and
145
+ * not a second place the rule is decided.
146
+ */
147
+ export declare function qualifyOwner(frames: readonly string[]): string;
148
+ /**
149
+ * A declared name qualified by the frames around it. Unlike `qualifyOwner`
150
+ * there is always a name to fall back on, so an empty stack is not a sentinel —
151
+ * a top-level declaration's qualified name is simply its name, which is what
152
+ * makes `qualifiedName` equal to `name` for everything a module exports.
153
+ *
154
+ * The local rule reappears here for one reason: a declaration is recorded
155
+ * without any frame having been pushed for it, so its own position in a
156
+ * statement scope has to be asked about here — a bare `const` in a top-level
157
+ * block, but equally one in a static block or a function body, where the
158
+ * frame stack is not empty. Same predicate as `framesFor`, so the two agree.
159
+ */
160
+ export declare function qualifyDeclaration(frames: readonly string[], name: string, node: ts.Node): string;
@@ -0,0 +1,284 @@
1
+ import ts from "typescript";
2
+ /**
3
+ * How a name is qualified by the scopes around it. One rule, in one place,
4
+ * because two consumers depend on it agreeing with itself: `mapSymbols`
5
+ * (`./symbols.ts`) groups declarations by qualified name, and `collectGuards`
6
+ * (`../analyze/guards.ts`) attributes a guard to the qualified owner it runs
7
+ * in. `foldReach` then matches facts from different analyzers on that string,
8
+ * so a name qualified two different ways is a fact about one symbol filed
9
+ * under another.
10
+ *
11
+ * "Agreeing with itself" has to mean more than "both call `frameNameOf`". It
12
+ * did not, once: the declaration side grew a rule for unnamed scopes that the
13
+ * guards side never got, and neither framed an object literal, so a guard in
14
+ * `handlers.run` was attributed to a top-level `run` beside it — which stole
15
+ * that export's reference count, and, when the export lost a guard of the same
16
+ * kind in the same change, cancelled it out and reported nothing at all. Both
17
+ * sides now build their frame stack with `framesFor` and mark statement-scope
18
+ * locals with `inStatementScope`, which is where the rule lives;
19
+ * `test/extract/scope.test.ts` asks both of them about the same declaration
20
+ * and compares.
21
+ */
22
+ /**
23
+ * Path for something that belongs to no declaration — code at the top level of
24
+ * a file. Angle brackets because an identifier cannot contain them, so a
25
+ * sentinel cannot collide with a path built out of identifiers. Exported
26
+ * because the renderer has to recognise it to translate it (see
27
+ * `guardOwnerLabel` in `../score/index.ts`).
28
+ *
29
+ * Not collision-proof against every string TypeScript will accept as a frame:
30
+ * `declare module "<module>" { ... }` takes its frame from a *string literal*,
31
+ * and would put this exact text in a path. That is inert here — an ambient
32
+ * module's members are not exports of the file being reviewed, so nothing
33
+ * matches on them — but the guarantee is about identifiers, not about
34
+ * arbitrary module specifiers.
35
+ */
36
+ export const MODULE_OWNER = "<module>";
37
+ /**
38
+ * Segment for a function or object with no name to be known by: an arrow
39
+ * function or expression bound to nothing nameable, a method with a computed
40
+ * key, an object literal passed straight to a call. It can appear at any
41
+ * position in a path —
42
+ * `<anonymous>.inner` for a named function declared inside a callback,
43
+ * `Cls.<anonymous>` for a computed-key method — so anything that renders a path
44
+ * has to handle it anywhere, not only at the end.
45
+ */
46
+ export const ANONYMOUS_OWNER = "<anonymous>";
47
+ /**
48
+ * Segment for a name declared inside a *statement* scope — a bare block, a
49
+ * `for` header or body, a `catch` clause, a `case` block, a class's static
50
+ * initializer block, or a function's body. Such a name is by construction not
51
+ * a member of anything (`export` is legal only at the top level of a file or a
52
+ * namespace), and without this segment it would be qualified as if it were
53
+ * one: at the top of a file it collided with a top-level export of the same
54
+ * name, and inside a named frame it collided with a real member — a
55
+ * static-block local wore the path of the class's static method, and a
56
+ * function-local object inside `function api` wore the path of a merged
57
+ * `namespace api`'s genuinely exported member. The segment applies wherever
58
+ * the declaration sits, not only when the frame stack is empty — the
59
+ * empty-stack-only version was exactly the second collision.
60
+ *
61
+ * Statement scopes still contribute no frame of their own to the *owner*
62
+ * stack: a guard's own body is a block — `if (x) { throw }` inside `validate`
63
+ * belongs to `validate`, not to a scope between them — and framing every
64
+ * block would bury real paths. The marker enters a path only where a name is
65
+ * introduced: once, before the outermost name declared in statement position.
66
+ */
67
+ export const LOCAL_SCOPE = "<local>";
68
+ /**
69
+ * Frame prefixes for accessors: `get value` and `set value` are two distinct
70
+ * runtime symbols — an object can carry either without the other, and a check
71
+ * moved from one to the other genuinely stops running on reads — so they must
72
+ * not share the one path `value` (which is also what a sibling *method* named
73
+ * `value` would wear). The embedded space is what makes the frame
74
+ * collision-free: an identifier cannot contain one, so no method or property
75
+ * name can ever spell `get value`. Like the sentinels, a segment carrying one
76
+ * of these prefixes is not text a reader can find in the source verbatim, so
77
+ * `guardOwnerLabel` in `../score/index.ts` translates it ("the value getter")
78
+ * rather than printing it raw. The same caveat as `MODULE_OWNER` applies:
79
+ * `declare module "get x"` could put this shape in a path, and is inert here
80
+ * for the same reason.
81
+ */
82
+ export const GETTER_FRAME_PREFIX = "get ";
83
+ export const SETTER_FRAME_PREFIX = "set ";
84
+ /**
85
+ * Every sentinel a qualified path can contain. Anything that renders a path to
86
+ * a reader has to handle all of them, at any position — `guardOwnerLabel` in
87
+ * `../score/index.ts` does, and `test/score/index.test.ts`, "translates every
88
+ * scope sentinel wherever it sits in the path", walks this list rather than a
89
+ * remembered copy of it, so adding a fourth sentinel here fails that test until
90
+ * it has a translation.
91
+ */
92
+ export const SCOPE_SENTINELS = [
93
+ MODULE_OWNER,
94
+ ANONYMOUS_OWNER,
95
+ LOCAL_SCOPE,
96
+ ];
97
+ /**
98
+ * The statically known text of a member name: an identifier, or a private
99
+ * `#name` — which is spelled in full at its declaration and cannot collide
100
+ * with any identifier, since `#` is not an identifier character. Throwing a
101
+ * `#name` away (the old Identifier-only check) framed every private method
102
+ * `<anonymous>`, where it shared a path with any computed-key sibling and a
103
+ * guard moved between the two cancelled to silence. Computed keys and
104
+ * string or numeric names stay undefined: only a computed key's *expression*
105
+ * is known statically, not the name it evaluates to.
106
+ */
107
+ export function memberNameOf(name) {
108
+ if (ts.isIdentifier(name) || ts.isPrivateIdentifier(name))
109
+ return name.text;
110
+ return undefined;
111
+ }
112
+ /** The name an arrow, function expression, class expression, or object literal is bound to. */
113
+ function boundName(node) {
114
+ const parent = node.parent;
115
+ if (!parent)
116
+ return undefined;
117
+ if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
118
+ return parent.name.text;
119
+ }
120
+ if ((ts.isPropertyAssignment(parent) || ts.isPropertyDeclaration(parent)) &&
121
+ memberNameOf(parent.name) !== undefined) {
122
+ return memberNameOf(parent.name);
123
+ }
124
+ return undefined;
125
+ }
126
+ /**
127
+ * The frame this node contributes to the qualified names inside it, or
128
+ * undefined for a node that introduces none.
129
+ *
130
+ * Every function-like node contributes one, falling back to `ANONYMOUS_OWNER`
131
+ * rather than to nothing: a node that opens a scope and contributes no frame
132
+ * would let its contents be qualified as if they sat in the scope outside it.
133
+ * Classes, namespaces, and object literals contribute one so that a method
134
+ * comes out as `Worker.run`, `N.run`, or `handlers.run`. `export default class
135
+ * {}` and `export default function () {}` both use "default", the name they
136
+ * are exported under — a nameless class or function declaration is legal
137
+ * nowhere else, and no identifier can collide with `default` since it is a
138
+ * reserved word.
139
+ *
140
+ * An expression's *internal* name is never used, even when it has one:
141
+ * `const format = function format2() {}` frames `format`, not `format2`.
142
+ * `format2` is in scope only inside the expression, so a path built from it
143
+ * names something no other file can reach — and would collide with a real
144
+ * top-level `format2` declared beside it. The binding is what the rest of the
145
+ * program calls this function, and it is what `mapSymbols` records.
146
+ *
147
+ * Two things this does not frame, deliberately. A class body holds no
148
+ * statements, so a class frame is normally only a prefix for its members —
149
+ * except for a static initializer block, which does hold statements and
150
+ * introduces no frame of its own, so a guard inside one is attributed to the
151
+ * class (`test/analyze/guards.test.ts`, "attributes a static initializer
152
+ * block's guard to the class"). And a plain statement block is not a frame at
153
+ * all — see `LOCAL_SCOPE`.
154
+ */
155
+ export function frameNameOf(node) {
156
+ if (ts.isClassDeclaration(node)) {
157
+ return node.name?.text ?? "default";
158
+ }
159
+ if (ts.isClassExpression(node) || ts.isObjectLiteralExpression(node)) {
160
+ return boundName(node) ?? ANONYMOUS_OWNER;
161
+ }
162
+ if (ts.isModuleDeclaration(node)) {
163
+ // `namespace N {}` and `declare module "spec" {}` — an Identifier or a
164
+ // StringLiteral, both of which carry `.text`.
165
+ return node.name.text;
166
+ }
167
+ if (ts.isFunctionDeclaration(node)) {
168
+ return node.name?.text ?? "default";
169
+ }
170
+ if (ts.isMethodDeclaration(node)) {
171
+ return memberNameOf(node.name) ?? ANONYMOUS_OWNER;
172
+ }
173
+ if (ts.isConstructorDeclaration(node)) {
174
+ return "constructor";
175
+ }
176
+ if (ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node)) {
177
+ const name = memberNameOf(node.name);
178
+ if (name === undefined)
179
+ return ANONYMOUS_OWNER;
180
+ // Not the bare name: a getter and a setter are different runtime symbols
181
+ // and must not share a path — see GETTER_FRAME_PREFIX.
182
+ return ts.isGetAccessorDeclaration(node)
183
+ ? GETTER_FRAME_PREFIX + name
184
+ : SETTER_FRAME_PREFIX + name;
185
+ }
186
+ if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
187
+ return boundName(node) ?? ANONYMOUS_OWNER;
188
+ }
189
+ return undefined;
190
+ }
191
+ /**
192
+ * A node that opens a statement scope — the shapes `LOCAL_SCOPE` exists for.
193
+ * Anything carrying a name is already a frame, so this list covers only
194
+ * *statement* scopes. Deliberately not `ts.isModuleBlock`: a namespace's body
195
+ * is where its *members* live — `export` is legal there — so a declaration
196
+ * directly under one is reachable as `N.x` and must not be marked local.
197
+ * `inStatementScope` gives it its own early exit instead.
198
+ *
199
+ * The list has to be complete, and completeness is the whole of its safety:
200
+ * `inStatementScope` walks through any node not listed here, so an unlisted
201
+ * scope qualifies a name declared inside it as if it were a member of the
202
+ * enclosing frame or file — where it can collide with a real declaration of
203
+ * the same name and take that declaration's guard findings and reference
204
+ * count. An earlier version of this comment claimed an omission "can only
205
+ * leave a path shorter", which is not what an omission does; a `for` header
206
+ * was the counterexample, and it was in that state (`for (const f = …)`
207
+ * beside `export const f`, reported as a guard removed from the export, and
208
+ * silent in the other direction). So: every construct that can declare a
209
+ * binding and is not itself a frame belongs here. The three `for` forms are
210
+ * listed for their *headers* — their bodies are already covered when braced,
211
+ * and a braceless body cannot declare anything.
212
+ */
213
+ function isStatementScope(node) {
214
+ return (ts.isBlock(node) ||
215
+ ts.isCaseBlock(node) ||
216
+ ts.isCatchClause(node) ||
217
+ ts.isClassStaticBlockDeclaration(node) ||
218
+ ts.isForStatement(node) ||
219
+ ts.isForInStatement(node) ||
220
+ ts.isForOfStatement(node));
221
+ }
222
+ /**
223
+ * Whether this node's declaration site sits inside a statement scope, rather
224
+ * than directly under the file, a namespace body, or a frame. This is the one
225
+ * predicate behind the `LOCAL_SCOPE` segment, and it is a question about the
226
+ * node's *own* position: the walk stops at the nearest enclosing scope-opener
227
+ * of any kind, so a declaration directly under a frame (a class member, an
228
+ * object-literal method, a namespace member) is not local, while the same
229
+ * declaration one statement scope deeper is — whatever sits above that.
230
+ *
231
+ * A function's body block is a statement scope like any other: a `const`
232
+ * declared in it is a local no other file can reach, not a member of the
233
+ * function. Walking through it to the function — the old behaviour, when the
234
+ * root rule fired only on an empty frame stack — is what let a function-local
235
+ * `handlers` wear a merged namespace's member path.
236
+ */
237
+ function inStatementScope(node) {
238
+ for (let ancestor = node.parent; ancestor; ancestor = ancestor.parent) {
239
+ if (ts.isSourceFile(ancestor) || ts.isModuleBlock(ancestor))
240
+ return false;
241
+ if (isStatementScope(ancestor))
242
+ return true;
243
+ if (frameNameOf(ancestor) !== undefined)
244
+ return false;
245
+ }
246
+ return false;
247
+ }
248
+ /**
249
+ * The segments this node pushes onto a frame stack: none, its frame, or —
250
+ * when the node itself is declared in statement position — `LOCAL_SCOPE` and
251
+ * its frame. Both walkers push through here, which is what makes the local
252
+ * rule one rule rather than two that have to be kept in step.
253
+ */
254
+ export function framesFor(node) {
255
+ const frame = frameNameOf(node);
256
+ if (frame === undefined)
257
+ return [];
258
+ return inStatementScope(node) ? [LOCAL_SCOPE, frame] : [frame];
259
+ }
260
+ /**
261
+ * A frame stack as one dotted path, or `MODULE_OWNER` for an empty stack —
262
+ * top-level code, which belongs to no declaration. The stack already carries
263
+ * its root marker when it needs one (see `framesFor`), so this is a join and
264
+ * not a second place the rule is decided.
265
+ */
266
+ export function qualifyOwner(frames) {
267
+ return frames.length === 0 ? MODULE_OWNER : frames.join(".");
268
+ }
269
+ /**
270
+ * A declared name qualified by the frames around it. Unlike `qualifyOwner`
271
+ * there is always a name to fall back on, so an empty stack is not a sentinel —
272
+ * a top-level declaration's qualified name is simply its name, which is what
273
+ * makes `qualifiedName` equal to `name` for everything a module exports.
274
+ *
275
+ * The local rule reappears here for one reason: a declaration is recorded
276
+ * without any frame having been pushed for it, so its own position in a
277
+ * statement scope has to be asked about here — a bare `const` in a top-level
278
+ * block, but equally one in a static block or a function body, where the
279
+ * frame stack is not empty. Same predicate as `framesFor`, so the two agree.
280
+ */
281
+ export function qualifyDeclaration(frames, name, node) {
282
+ const local = inStatementScope(node);
283
+ return [...frames, ...(local ? [LOCAL_SCOPE] : []), name].join(".");
284
+ }
@@ -0,0 +1,24 @@
1
+ import type { ChangedSymbol, Hunk } from "../types.js";
2
+ /**
3
+ * A TypeScript *implementation* file, in any of the four extensions the
4
+ * language has — `.ts`, `.tsx`, and the module-explicit `.mts`/`.cts` (there
5
+ * is no `.mtsx`/`.ctsx`; JSX never got module-explicit flavours). Declaration
6
+ * files are excluded in every flavour: `.d.ts`, `.d.mts`, `.d.cts`. The
7
+ * `.tsx?`-only version of this test made every `.mts`/`.cts` file invisible
8
+ * to every analyzer, silently — the worst outcome this tool has.
9
+ */
10
+ export declare function isTypeScriptFile(path: string): boolean;
11
+ /**
12
+ * Symbols affected by this change — one entry per symbol, not per declaration.
13
+ * A symbol is reported when it is new, gone, or when a hunk falls inside any
14
+ * of its declarations' line ranges in the after-file.
15
+ *
16
+ * One entry per symbol is what the rest of the pipeline is built on:
17
+ * `blast-radius` derives a fact id from `qualifiedName` alone, so N entries
18
+ * for one overloaded export meant N facts sharing an id — N identical
19
+ * `verified` findings, N identical rows in the report's API-surface table,
20
+ * and a single model claim citing that id attaching to every one of them.
21
+ * See `test/identity.test.ts`, "one changed symbol is one symbol, however
22
+ * many declarations it has".
23
+ */
24
+ export declare function mapSymbols(path: string, before: string | null, after: string | null, hunks: Hunk[]): ChangedSymbol[];