carrick 0.3.53

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 (114) hide show
  1. package/LICENSE.md +99 -0
  2. package/README.md +79 -0
  3. package/bin/carrick.mjs +168 -0
  4. package/dist/channel.d.ts +16 -0
  5. package/dist/channel.js +37 -0
  6. package/dist/channel.js.map +1 -0
  7. package/dist/cli.d.ts +29 -0
  8. package/dist/cli.js +60 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/contract.d.ts +154 -0
  11. package/dist/contract.js +141 -0
  12. package/dist/contract.js.map +1 -0
  13. package/dist/diagnostics.d.ts +54 -0
  14. package/dist/diagnostics.js +199 -0
  15. package/dist/diagnostics.js.map +1 -0
  16. package/dist/hook/post-edit.d.ts +13 -0
  17. package/dist/hook/post-edit.js +76 -0
  18. package/dist/hook/post-edit.js.map +1 -0
  19. package/dist/hook/session-start.d.ts +2 -0
  20. package/dist/hook/session-start.js +44 -0
  21. package/dist/hook/session-start.js.map +1 -0
  22. package/dist/init/identity.d.ts +20 -0
  23. package/dist/init/identity.js +60 -0
  24. package/dist/init/identity.js.map +1 -0
  25. package/dist/init/repos.d.ts +19 -0
  26. package/dist/init/repos.js +46 -0
  27. package/dist/init/repos.js.map +1 -0
  28. package/dist/init/run.d.ts +11 -0
  29. package/dist/init/run.js +228 -0
  30. package/dist/init/run.js.map +1 -0
  31. package/dist/init/settings.d.ts +50 -0
  32. package/dist/init/settings.js +138 -0
  33. package/dist/init/settings.js.map +1 -0
  34. package/dist/log.d.ts +2 -0
  35. package/dist/log.js +35 -0
  36. package/dist/log.js.map +1 -0
  37. package/dist/native.d.ts +48 -0
  38. package/dist/native.js +127 -0
  39. package/dist/native.js.map +1 -0
  40. package/dist/render.d.ts +65 -0
  41. package/dist/render.js +334 -0
  42. package/dist/render.js.map +1 -0
  43. package/dist/root.d.ts +30 -0
  44. package/dist/root.js +82 -0
  45. package/dist/root.js.map +1 -0
  46. package/dist/server.d.ts +2 -0
  47. package/dist/server.js +255 -0
  48. package/dist/server.js.map +1 -0
  49. package/dist/templates.d.ts +18 -0
  50. package/dist/templates.js +66 -0
  51. package/dist/templates.js.map +1 -0
  52. package/package.json +72 -0
  53. package/plugin/.claude-plugin/plugin.json +6 -0
  54. package/plugin/.lsp.json +14 -0
  55. package/plugin/hooks/hooks.json +27 -0
  56. package/sidecar/dist/src/bundler.d.ts +141 -0
  57. package/sidecar/dist/src/bundler.js +680 -0
  58. package/sidecar/dist/src/capture/anchors.d.ts +61 -0
  59. package/sidecar/dist/src/capture/anchors.js +1132 -0
  60. package/sidecar/dist/src/capture/api.d.ts +378 -0
  61. package/sidecar/dist/src/capture/api.js +10 -0
  62. package/sidecar/dist/src/capture/augmentations.d.ts +20 -0
  63. package/sidecar/dist/src/capture/augmentations.js +60 -0
  64. package/sidecar/dist/src/capture/check-classify.d.ts +58 -0
  65. package/sidecar/dist/src/capture/check-classify.js +189 -0
  66. package/sidecar/dist/src/capture/check-deep.d.ts +32 -0
  67. package/sidecar/dist/src/capture/check-deep.js +91 -0
  68. package/sidecar/dist/src/capture/check-poison.d.ts +40 -0
  69. package/sidecar/dist/src/capture/check-poison.js +155 -0
  70. package/sidecar/dist/src/capture/check-probe.d.ts +70 -0
  71. package/sidecar/dist/src/capture/check-probe.js +147 -0
  72. package/sidecar/dist/src/capture/check-scrub.d.ts +31 -0
  73. package/sidecar/dist/src/capture/check-scrub.js +71 -0
  74. package/sidecar/dist/src/capture/check-workspace.d.ts +52 -0
  75. package/sidecar/dist/src/capture/check-workspace.js +218 -0
  76. package/sidecar/dist/src/capture/check.d.ts +39 -0
  77. package/sidecar/dist/src/capture/check.js +450 -0
  78. package/sidecar/dist/src/capture/deep-walk.d.ts +62 -0
  79. package/sidecar/dist/src/capture/deep-walk.js +243 -0
  80. package/sidecar/dist/src/capture/index.d.ts +36 -0
  81. package/sidecar/dist/src/capture/index.js +477 -0
  82. package/sidecar/dist/src/capture/lockfile.d.ts +48 -0
  83. package/sidecar/dist/src/capture/lockfile.js +490 -0
  84. package/sidecar/dist/src/capture/machinery.d.ts +59 -0
  85. package/sidecar/dist/src/capture/machinery.js +160 -0
  86. package/sidecar/dist/src/capture/node-builder.d.ts +37 -0
  87. package/sidecar/dist/src/capture/node-builder.js +123 -0
  88. package/sidecar/dist/src/capture/paths-rewrite.d.ts +34 -0
  89. package/sidecar/dist/src/capture/paths-rewrite.js +100 -0
  90. package/sidecar/dist/src/capture/self-check.d.ts +38 -0
  91. package/sidecar/dist/src/capture/self-check.js +317 -0
  92. package/sidecar/dist/src/capture/specifiers.d.ts +39 -0
  93. package/sidecar/dist/src/capture/specifiers.js +56 -0
  94. package/sidecar/dist/src/definition-resolver.d.ts +57 -0
  95. package/sidecar/dist/src/definition-resolver.js +153 -0
  96. package/sidecar/dist/src/index.d.ts +14 -0
  97. package/sidecar/dist/src/index.js +564 -0
  98. package/sidecar/dist/src/monorepo-builder.d.ts +129 -0
  99. package/sidecar/dist/src/monorepo-builder.js +584 -0
  100. package/sidecar/dist/src/project-loader.d.ts +130 -0
  101. package/sidecar/dist/src/project-loader.js +399 -0
  102. package/sidecar/dist/src/type-inferrer.d.ts +940 -0
  103. package/sidecar/dist/src/type-inferrer.js +3540 -0
  104. package/sidecar/dist/src/type-structural-expander.d.ts +61 -0
  105. package/sidecar/dist/src/type-structural-expander.js +283 -0
  106. package/sidecar/dist/src/type-text-canonicalizer.d.ts +40 -0
  107. package/sidecar/dist/src/type-text-canonicalizer.js +297 -0
  108. package/sidecar/dist/src/types.d.ts +669 -0
  109. package/sidecar/dist/src/types.js +5 -0
  110. package/sidecar/dist/src/validators.d.ts +2214 -0
  111. package/sidecar/dist/src/validators.js +336 -0
  112. package/sidecar/package.json +6 -0
  113. package/templates/carrick.json +7 -0
  114. package/templates/carrick.yml +45 -0
@@ -0,0 +1,1132 @@
1
+ /**
2
+ * Anchor resolution for the surface entry: guards for addressable anchors
3
+ * (symbol / handler_return) and the locator + node-builder path for
4
+ * anonymous inferred types. Produces the final alias line for each anchor,
5
+ * or a recorded demotion -- never a silently wrong line.
6
+ */
7
+ import ts from 'typescript';
8
+ import * as path from 'node:path';
9
+ import { printTypeForDestination } from './node-builder.js';
10
+ import { typeIsOrContainsMachinery } from './machinery.js';
11
+ /** Repo-root-relative source file -> extensionless specifier from entryDir. */
12
+ export function entryRelativeSpecifier(entryDir, repoRoot, sourceFile) {
13
+ const target = path
14
+ .join(repoRoot, sourceFile)
15
+ .replace(/\.(ts|tsx|mts|cts)$/, '');
16
+ let rel = path.relative(entryDir, target).split(path.sep).join('/');
17
+ if (!rel.startsWith('.'))
18
+ rel = `./${rel}`;
19
+ return rel;
20
+ }
21
+ function moduleExport(checker, sourceFile, name) {
22
+ const moduleSymbol = checker.getSymbolAtLocation(sourceFile);
23
+ if (!moduleSymbol)
24
+ return undefined;
25
+ return checker
26
+ .getExportsOfModule(moduleSymbol)
27
+ .find((s) => s.getName() === name);
28
+ }
29
+ /**
30
+ * Resolve one anchor against the analysis program. `placeholder` is the
31
+ * anchor's own placeholder alias declaration in the entry file -- the
32
+ * destination node the node builder anchors to (banked correction 1).
33
+ */
34
+ export function resolveAnchor(program, request, args) {
35
+ const checker = program.getTypeChecker();
36
+ const demote = (reason) => ({
37
+ request,
38
+ aliasText: 'unknown',
39
+ serialization: 'structural_fallback',
40
+ failureReason: reason,
41
+ });
42
+ if (request.kind === 'literal') {
43
+ const text = request.type_text.trim();
44
+ if (!text) {
45
+ return demote('empty literal type text');
46
+ }
47
+ const bareIdentifier = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(text);
48
+ const siblingSpec = bareIdentifier
49
+ ? args.siblingSymbolSpecs?.get(text)
50
+ : undefined;
51
+ return {
52
+ request,
53
+ aliasText: siblingSpec ? `import('${siblingSpec}').${text}` : text,
54
+ // Literal anchors ARE the legacy-text tier (WP3 wiring of the design's
55
+ // structural_fallback): hand-produced type text riding the surface.
56
+ // The self-check still classifies decay; the fidelity metric counts
57
+ // them at this tier so the legacy dependence stays measurable and
58
+ // ratchetable. Demotions are distinguished by failureReason.
59
+ serialization: 'structural_fallback',
60
+ };
61
+ }
62
+ const sourceAbs = path.join(args.repoRoot, request.source_file);
63
+ const sourceFile = program.getSourceFile(sourceAbs);
64
+ if (!sourceFile) {
65
+ return demote(`source file not in program: ${request.source_file}`);
66
+ }
67
+ const spec = entryRelativeSpecifier(args.entryDir, args.repoRoot, request.source_file);
68
+ if (request.kind === 'symbol') {
69
+ const exported = moduleExport(checker, sourceFile, request.symbol_name);
70
+ if (!exported) {
71
+ return demote(`'${request.symbol_name}' is not an export of ${request.source_file}`);
72
+ }
73
+ // The anchor is the ELEMENT symbol; array_depth restores the use-site's
74
+ // `[]` levels (#248/#306) so an array-vs-scalar mismatch stays visible.
75
+ const arraySuffix = '[]'.repeat(Math.max(0, request.array_depth ?? 0));
76
+ // #438/#439: an LLM symbol anchor may name a schema VALUE const
77
+ // (`export const ZFooSchema = z.object({...})`) whose sibling
78
+ // `export type TFoo = z.infer<typeof ZFooSchema>` was the intended anchor.
79
+ // A value has no type-space meaning, so `import('./m').ZFooSchema` in TYPE
80
+ // position raises TS2694 in the stub tree and (before #438 part 2) poisons
81
+ // every producer pair. Guard structurally on symbol meaning, never on the
82
+ // name: resolve re-exports, then test for any type-space flag.
83
+ const resolvedExport = resolveSymbolAliases(checker, exported);
84
+ if (!symbolHasTypeMeaning(resolvedExport)) {
85
+ // #439 part 2: re-aim at a sibling type alias defined via a `typeof`
86
+ // type-query of this const (covers `z.infer<typeof C>` and equivalents;
87
+ // matched structurally, no library names). Guard A is the fallback.
88
+ const reaimed = reaimValueSymbolToTypeSibling(checker, sourceFile, {
89
+ request,
90
+ valueSymbol: resolvedExport,
91
+ spec,
92
+ arraySuffix,
93
+ });
94
+ if (reaimed)
95
+ return reaimed;
96
+ // #438 part 1: value-only export with no type sibling. Demote so the
97
+ // existing demotion/backfill path engages (honest `unknown`) instead of
98
+ // emitting a surface line that references a value in type position.
99
+ return demote(`'${request.symbol_name}' is a value export with no type-space meaning, ` +
100
+ `and no sibling type alias derives from it (e.g. \`typeof ${request.symbol_name}\`); ` +
101
+ 'demoted so the surface line stays valid');
102
+ }
103
+ return {
104
+ request,
105
+ aliasText: `import('${spec}').${request.symbol_name}${arraySuffix}`,
106
+ serialization: 'emitted',
107
+ };
108
+ }
109
+ if (request.kind === 'handler_return') {
110
+ // Design-doc Capture step 1 guards, all three verified failure modes.
111
+ const exported = moduleExport(checker, sourceFile, request.symbol_name);
112
+ if (!exported) {
113
+ return demote(`handler '${request.symbol_name}' is not an export of ${request.source_file}`);
114
+ }
115
+ const declaration = exported.valueDeclaration ?? exported.declarations?.[0];
116
+ const handlerType = checker.getTypeOfSymbolAtLocation(exported, declaration ?? sourceFile);
117
+ const callSignatures = handlerType.getCallSignatures();
118
+ if (callSignatures.length === 0) {
119
+ return demote(`handler '${request.symbol_name}' has no call signatures`);
120
+ }
121
+ if (callSignatures.length > 1) {
122
+ // ReturnType<> silently resolves the LAST overload only.
123
+ return demote(`handler '${request.symbol_name}' is an overload set (${callSignatures.length} signatures)`);
124
+ }
125
+ if ((callSignatures[0].getTypeParameters() ?? []).length > 0) {
126
+ // Type parameters erase to their constraint/unknown under ReturnType<>.
127
+ return demote(`handler '${request.symbol_name}' is generic`);
128
+ }
129
+ return {
130
+ request,
131
+ aliasText: `Awaited<ReturnType<typeof import('${spec}').${request.symbol_name}>>`,
132
+ serialization: 'emitted',
133
+ };
134
+ }
135
+ // kind === 'infer'
136
+ // #498: a `param_name` anchor targets what a handler RECEIVES. Resolve the
137
+ // parameter before anything else and treat the outcome as final — the
138
+ // expression locator below resolves the enclosing registration call, whose
139
+ // return type (`void`, a subscription handle) is not the payload, and it
140
+ // self-checks clean, so falling through would ship a confidently wrong
141
+ // contract instead of an honest `unknown`.
142
+ if (request.param_name !== undefined) {
143
+ const param = locateHandlerParam(sourceFile, request, request.param_name);
144
+ if (!param) {
145
+ return demote(`no handler parameter '${request.param_name}' resolved in ` +
146
+ `${request.source_file} (${paramLocatorHints(request)})`);
147
+ }
148
+ return finishInferAnchor(program, sourceFile, request, param, args.placeholder, undefined);
149
+ }
150
+ let located = locateNode(sourceFile, request);
151
+ if (!located) {
152
+ return demote(locatorFailureReason(request));
153
+ }
154
+ // #439 part 1: a producer anchor whose locator landed inside a fluent
155
+ // builder chain's config-descriptor argument (an all-literal metadata
156
+ // object) must never capture that descriptor as the request type. Re-aim at
157
+ // the chain's payload/schema argument, or demote when it cannot be picked
158
+ // unambiguously — never leave the descriptor captured (the artifact behind
159
+ // v1's false-incompatible verdicts; kept coupled with #438's containment).
160
+ let reaimNote;
161
+ const builderReaim = reaimBuilderChainPayload(checker, located);
162
+ if (builderReaim.kind === 'demote') {
163
+ return demote(builderReaim.reason);
164
+ }
165
+ if (builderReaim.kind === 'reaim') {
166
+ located = builderReaim.node;
167
+ reaimNote = builderReaim.note;
168
+ }
169
+ return finishInferAnchor(program, sourceFile, request, located, args.placeholder, reaimNote);
170
+ }
171
+ /**
172
+ * Shared tail of the infer paths (locator-resolved node and #498
173
+ * parameter-resolved node alike): read the type at the node, unwrap the
174
+ * transport layer, run the #433 recovery and the carrick#371 machinery guard,
175
+ * and print through the node builder.
176
+ */
177
+ function finishInferAnchor(program, sourceFile, request, located, placeholder, reaimNote) {
178
+ const checker = program.getTypeChecker();
179
+ const demote = (reason) => ({
180
+ request,
181
+ aliasText: 'unknown',
182
+ serialization: 'structural_fallback',
183
+ failureReason: reason,
184
+ });
185
+ let type = checker.getTypeAtLocation(located);
186
+ if ((request.unwrap ?? 'awaited') === 'awaited') {
187
+ type = checker.getAwaitedType(type) ?? type;
188
+ }
189
+ // #433: the checker gave us nothing (whole top type) — try the syntactic
190
+ // recovery for an inline literal type argument of an unresolvable generic
191
+ // annotation. The literal is dependency-free source syntax; only the outer
192
+ // generic needed the missing package.
193
+ if (isTopType(type)) {
194
+ const literalNode = recoverInlineLiteralTypeArgument(checker, sourceFile, request, located);
195
+ if (literalNode) {
196
+ const recovered = checker.getTypeAtLocation(literalNode);
197
+ if (!isTopType(recovered))
198
+ type = recovered;
199
+ }
200
+ }
201
+ // carrick#766: a LINE-ONLY anchor whose type came back a bare top type has
202
+ // no evidence on either side, so it must abstain.
203
+ //
204
+ // The two halves of an infer anchor are the locator and the type at it. A
205
+ // span, an expression text or a parameter name is the scanner NAMING the
206
+ // payload: whatever that node's type turns out to be — including a whole
207
+ // `any` decayed through an uninstalled dependency — is a located fact about
208
+ // the payload, and the deep-any walk, the check's IsAny gate and the literal
209
+ // backfill are all built on it being published. Those anchors are untouched
210
+ // here.
211
+ //
212
+ // A line alone names nothing. `firstExpressionOnLine` picks whatever comes
213
+ // first on the line, which on a re-export statement is the first exported
214
+ // binding. When THAT resolves to a top type the capture holds no payload and
215
+ // no type, and `export type <alias> = any;` states "a type was inferred and
216
+ // it collapsed" — a claim the scan cannot back. The honest word is `unknown`
217
+ // ("no contract stated here"), with the node the line resolved as the reason.
218
+ //
219
+ // Live shape: a route whose handlers are built by a framework factory and
220
+ // re-exported at the bottom of the file. Both of the file's operations
221
+ // anchor at the export statement, the v1 walk abstains there (carrick#771),
222
+ // and the alias falls to this line-only locator, which resolves the first
223
+ // exported binding's identifier.
224
+ if (isTopType(type) && isLineOnly(request)) {
225
+ return {
226
+ request,
227
+ aliasText: 'unknown',
228
+ serialization: 'structural_fallback',
229
+ abstainReason: `line-only locator resolved ` +
230
+ `${describeNode(sourceFile, request.source_file, located)} whose type is a ` +
231
+ `bare top type ('${checker.typeToString(type)}'); the anchor names no ` +
232
+ `payload span, expression or parameter, so it abstains rather than ` +
233
+ `publish a top type as a contract`,
234
+ };
235
+ }
236
+ // carrick#371 fail-closed guard: a producer anchor whose resolved type IS or
237
+ // CONTAINS framework machinery (a raw `Response`/`Request`, a wrapper envelope
238
+ // `{ response: Response; error }`, a wrapper function `(req) => Promise<Response>`)
239
+ // must never be emitted as a comparable contract — that manufactures a false
240
+ // compat mismatch against the consumer's real payload. This is the capture
241
+ // fallback for the case the v1-inferrer machinery guard leaves behind: when v1
242
+ // abstains on the envelope, `derive_capture_anchors` re-routes the alias to
243
+ // this locator-based infer anchor, which then resolves the WRAPPER FUNCTION
244
+ // type. No source locator points at a distinct clean payload here (the payload
245
+ // lives inside the wrapper's response builder), so degrade to `unknown` —
246
+ // abstain, never a concrete verdict off the machinery.
247
+ if (typeIsOrContainsMachinery(checker, type, located)) {
248
+ return demote('resolved type is or contains framework machinery (Response/Request-shaped); ' +
249
+ 'degraded to unknown rather than emit a wrapper envelope as a response contract');
250
+ }
251
+ if (!placeholder) {
252
+ return demote('internal: no placeholder destination for infer anchor');
253
+ }
254
+ const printed = printTypeForDestination(program, type, placeholder);
255
+ if (!printed.text) {
256
+ return demote(printed.failure ?? 'node builder print failed');
257
+ }
258
+ return {
259
+ request,
260
+ aliasText: printed.text,
261
+ serialization: 'node_builder',
262
+ ...(reaimNote ? { reaimNote } : {}),
263
+ };
264
+ }
265
+ /**
266
+ * True when the anchor carries a LINE and nothing else — no payload span, no
267
+ * expression text, no parameter name. Such an anchor states where to look, not
268
+ * what to look at, so a top type resolved from it is not a fact about any
269
+ * payload (carrick#766).
270
+ */
271
+ function isLineOnly(request) {
272
+ return (request.span_start == null &&
273
+ request.span_end == null &&
274
+ !request.expression_text &&
275
+ !request.param_name);
276
+ }
277
+ /**
278
+ * `<kind> at <file>:<line> (`<text>`)` for a node a locator resolved — what a
279
+ * reader needs to see why an alias abstained. The path is the REPO-RELATIVE
280
+ * one off the request, not `sourceFile.fileName`: the reason is published on
281
+ * the capture record, and an absolute path there is the scanner's checkout
282
+ * directory. The text is collapsed to one line and truncated, since a resolved
283
+ * node can be a whole function body.
284
+ */
285
+ function describeNode(sourceFile, sourceFileRel, node) {
286
+ const line = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
287
+ const raw = node.getText(sourceFile).replace(/\s+/g, ' ').trim();
288
+ const text = raw.length > 80 ? `${raw.slice(0, 77)}...` : raw;
289
+ return `${ts.SyntaxKind[node.kind]} at ${sourceFileRel}:${line} (\`${text}\`)`;
290
+ }
291
+ function isTopType(type) {
292
+ return ((type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.Never)) !== 0);
293
+ }
294
+ /**
295
+ * The checker's intrinsic ERROR type: resolution of the reference FAILED
296
+ * (missing package on a bare checkout, unresolvable symbol). Distinct from a
297
+ * legitimate `any`, whose intrinsic name is `any` — an author-written or
298
+ * alias-resolved `any` must never trigger the syntactic recovery.
299
+ * `intrinsicName` is internal but stable since TS 1.x (same standing as the
300
+ * internals node-builder.ts already mirrors).
301
+ */
302
+ function isErrorType(type) {
303
+ if (!(type.flags & ts.TypeFlags.Any))
304
+ return false;
305
+ const name = type.intrinsicName;
306
+ return name === 'error' || name === 'unresolved';
307
+ }
308
+ /**
309
+ * #433 syntactic recovery: an infer anchor whose checker type decayed to a
310
+ * whole top type may sit under a DECLARED annotation of the form
311
+ * `SomeGeneric<{ ...literal... }>` where only the outer generic failed to
312
+ * resolve (error-typed; its package is absent on a bare checkout) while the
313
+ * literal type argument is dependency-free source syntax referencing only
314
+ * locally-resolvable symbols. Trigger is purely structural — no framework
315
+ * names anywhere:
316
+ *
317
+ * 1. Data-flow path: the payload node (the located node, or the anchor's
318
+ * expression_text relocated) is an argument of a call — or IS a send
319
+ * call — whose callee chain roots at an identifier whose declaration
320
+ * carries the annotation (`res.json(payload)` -> `res`).
321
+ * 2. Registration path: the located node is a call registering inline
322
+ * callbacks; the callbacks' parameter annotations are scanned.
323
+ *
324
+ * Either path recovers ONLY when exactly one literal type argument is in
325
+ * scope — ambiguity refuses recovery and the alias keeps decaying honestly.
326
+ * The recovered literal node's own type is printed through the normal
327
+ * node-builder path, so the alias still faces the real self-check.
328
+ */
329
+ function recoverInlineLiteralTypeArgument(checker, sourceFile, request, located) {
330
+ const candidates = [located];
331
+ if (request.expression_text) {
332
+ const byText = nodeByExpressionText(sourceFile, request.expression_text, request.line_number);
333
+ if (byText && byText !== located)
334
+ candidates.push(byText);
335
+ }
336
+ // Data-flow first: the payload's own call names the annotated receiver.
337
+ for (const node of candidates) {
338
+ const call = governingCall(node);
339
+ if (!call)
340
+ continue;
341
+ const root = calleeRootIdentifier(call.expression);
342
+ if (!root)
343
+ continue;
344
+ const annotation = declaredAnnotationOf(checker, root);
345
+ if (!annotation || !isErroredGenericReference(checker, annotation))
346
+ continue;
347
+ // The payload carrier is identified; its annotation's verdict is FINAL —
348
+ // never fall through to the registration scan, which could pick a
349
+ // different parameter's literal for this payload.
350
+ const literals = literalTypeArguments(annotation);
351
+ if (literals.length !== 1)
352
+ return undefined;
353
+ return literalResolvesLocally(checker, literals[0]) ? literals[0] : undefined;
354
+ }
355
+ // Registration shape: the located call passes inline callbacks whose
356
+ // parameters carry the annotations. Ambiguity is counted BEFORE any
357
+ // resolvability filtering: two literal-carrying annotations refuse even
358
+ // when one of them would be rejected later — filtering first could crown
359
+ // the wrong parameter's literal.
360
+ for (const node of candidates) {
361
+ if (!ts.isCallExpression(node))
362
+ continue;
363
+ const literals = [];
364
+ for (const arg of node.arguments) {
365
+ if (!isFunctionArgument(arg))
366
+ continue;
367
+ for (const param of arg.parameters) {
368
+ if (param.type && isErroredGenericReference(checker, param.type)) {
369
+ literals.push(...literalTypeArguments(param.type));
370
+ }
371
+ }
372
+ }
373
+ if (literals.length !== 1)
374
+ continue;
375
+ return literalResolvesLocally(checker, literals[0]) ? literals[0] : undefined;
376
+ }
377
+ return undefined;
378
+ }
379
+ /**
380
+ * The call whose ARGUMENT list carries `node` (payload in argument position),
381
+ * or `node` itself when it is a call (the send call). Calls that register
382
+ * callbacks are excluded: their receiver is the framework app/router value,
383
+ * not a payload carrier — those go through the registration path instead.
384
+ */
385
+ function governingCall(node) {
386
+ if (ts.isCallExpression(node)) {
387
+ return node.arguments.some(isFunctionArgument) ? undefined : node;
388
+ }
389
+ const parent = node.parent;
390
+ if (parent !== undefined &&
391
+ ts.isCallExpression(parent) &&
392
+ node !== parent.expression &&
393
+ parent.arguments.includes(node)) {
394
+ return parent.arguments.some(isFunctionArgument) ? undefined : parent;
395
+ }
396
+ return undefined;
397
+ }
398
+ function isFunctionArgument(arg) {
399
+ return ts.isArrowFunction(arg) || ts.isFunctionExpression(arg);
400
+ }
401
+ /** Root identifier of a callee chain: `res.status(500).json` -> `res`. */
402
+ function calleeRootIdentifier(expr) {
403
+ let current = expr;
404
+ while (ts.isPropertyAccessExpression(current) ||
405
+ ts.isElementAccessExpression(current) ||
406
+ ts.isCallExpression(current) ||
407
+ ts.isNonNullExpression(current) ||
408
+ ts.isParenthesizedExpression(current)) {
409
+ current = current.expression;
410
+ }
411
+ return ts.isIdentifier(current) ? current : undefined;
412
+ }
413
+ /** The explicit type annotation on the identifier's declaration, if any. */
414
+ function declaredAnnotationOf(checker, identifier) {
415
+ const symbol = checker.getSymbolAtLocation(identifier);
416
+ const decl = symbol?.valueDeclaration ?? symbol?.declarations?.[0];
417
+ if (!decl)
418
+ return undefined;
419
+ if (ts.isParameter(decl) ||
420
+ ts.isVariableDeclaration(decl) ||
421
+ ts.isPropertySignature(decl) ||
422
+ ts.isPropertyDeclaration(decl)) {
423
+ return decl.type;
424
+ }
425
+ return undefined;
426
+ }
427
+ /**
428
+ * A generic type reference whose OUTER resolution failed: the annotation is
429
+ * `Name<...args>` and its own type is the checker's error type. A resolvable
430
+ * generic (however it resolves) never triggers recovery; its checker answer
431
+ * is the truth.
432
+ */
433
+ function isErroredGenericReference(checker, annotation) {
434
+ return (ts.isTypeReferenceNode(annotation) &&
435
+ (annotation.typeArguments?.length ?? 0) > 0 &&
436
+ isErrorType(checker.getTypeFromTypeNode(annotation)));
437
+ }
438
+ /** Type-literal type arguments of a generic reference (parens unwrapped). */
439
+ function literalTypeArguments(annotation) {
440
+ const literals = [];
441
+ for (const arg of annotation.typeArguments ?? []) {
442
+ let node = arg;
443
+ while (ts.isParenthesizedTypeNode(node))
444
+ node = node.type;
445
+ if (ts.isTypeLiteralNode(node))
446
+ literals.push(node);
447
+ }
448
+ return literals;
449
+ }
450
+ /**
451
+ * Every type reference inside the literal must itself RESOLVE. A literal
452
+ * leaning on a third-party type (`{ thing: LibThing }` with the lib absent)
453
+ * is not the tractable class — recovering it would bake `any` at a member;
454
+ * it keeps decaying honestly instead. Author-written `any`/`unknown`
455
+ * keywords are allowed through: they are the source's truth, and the
456
+ * self-check's deep walk still owns that verdict.
457
+ */
458
+ function literalResolvesLocally(checker, literal) {
459
+ let resolves = true;
460
+ const visit = (node) => {
461
+ if (!resolves)
462
+ return;
463
+ if ((ts.isTypeReferenceNode(node) ||
464
+ ts.isImportTypeNode(node) ||
465
+ ts.isTypeQueryNode(node) ||
466
+ ts.isExpressionWithTypeArguments(node)) &&
467
+ isErrorType(checker.getTypeFromTypeNode(node))) {
468
+ resolves = false;
469
+ return;
470
+ }
471
+ node.forEachChild(visit);
472
+ };
473
+ visit(literal);
474
+ return resolves;
475
+ }
476
+ // ===========================================================================
477
+ // carrick#498: handler-parameter anchors.
478
+ //
479
+ // A subscriber's contract is what its handler RECEIVES, so its anchor carries
480
+ // a `param_name` locator instead of an expression. Resolving it needs the
481
+ // handler function, and the only locator upstream can supply for an anonymous
482
+ // inline handler is a line. Everything below is STRUCTURAL — argument
483
+ // position and binding shape, never a method name, library, or topic string.
484
+ // ===========================================================================
485
+ /**
486
+ * v1's `findFunctionByLine` tolerance, mirrored so the two paths agree on how
487
+ * far an anchor may be from the function it names — and, since carrick#770, on
488
+ * what a forward match may cross (see [`separatedFromAnchor`]).
489
+ */
490
+ const HANDLER_LINE_TOLERANCE = 2;
491
+ /**
492
+ * The payload parameter named by `paramName` on the handler this anchor
493
+ * points at, or undefined when neither the handler nor the parameter can be
494
+ * picked. The caller demotes on undefined: an unresolved parameter must never
495
+ * fall back to the expression locator.
496
+ */
497
+ function locateHandlerParam(sourceFile, request, paramName) {
498
+ for (const fn of handlerCandidates(sourceFile, request)) {
499
+ const target = resolveParamTarget(fn, paramName);
500
+ if (target)
501
+ return target;
502
+ }
503
+ return undefined;
504
+ }
505
+ /**
506
+ * Handler functions this anchor could be naming, most specific first:
507
+ *
508
+ * 1. the LAST function-typed argument of a call starting on the anchor's
509
+ * line — the handler slot of a registration call, structurally (a
510
+ * registration passes its routing key first and its handler last).
511
+ * Several calls can start on one line (`wrap(bus.subscribe(t, h), g)`),
512
+ * so they are ordered INNERMOST first: the innermost is the registration
513
+ * the anchor's own line most nearly denotes, and it is the same
514
+ * innermost-wins tie-break the v1 inferrer's `findFunctionByLine` uses;
515
+ * 2. any function whose declaration starts within
516
+ * `HANDLER_LINE_TOLERANCE` lines of the anchor, innermost first — the
517
+ * locator may name a binding on the handler's own signature lines rather
518
+ * than the registration line;
519
+ * 3. the innermost function ENCLOSING the node the expression locator
520
+ * resolves — covers a locator that landed in the handler body.
521
+ */
522
+ function handlerCandidates(sourceFile, request) {
523
+ const out = [];
524
+ const push = (fn) => {
525
+ if (fn && !out.includes(fn))
526
+ out.push(fn);
527
+ };
528
+ const line = request.line_number;
529
+ if (line !== undefined) {
530
+ for (const call of callsStartingOnLine(sourceFile, line)) {
531
+ const handlerArgs = call.arguments.filter(isFunctionArgument);
532
+ push(handlerArgs[handlerArgs.length - 1]);
533
+ }
534
+ for (const fn of functionsNearLine(sourceFile, line))
535
+ push(fn);
536
+ }
537
+ push(enclosingFunction(locateNode(sourceFile, request)));
538
+ return out;
539
+ }
540
+ function callsStartingOnLine(sourceFile, line) {
541
+ const calls = [];
542
+ const visit = (node) => {
543
+ if (ts.isCallExpression(node) && startLineOf(sourceFile, node) === line) {
544
+ calls.push(node);
545
+ }
546
+ node.forEachChild(visit);
547
+ };
548
+ visit(sourceFile);
549
+ // Innermost (smallest span) first: a pre-order walk would otherwise hand
550
+ // back the OUTER call of a same-line nest, whose handler is not the one the
551
+ // anchor's line denotes.
552
+ return calls.sort((a, b) => a.getEnd() - a.getStart() - (b.getEnd() - b.getStart()));
553
+ }
554
+ /**
555
+ * Functions declared within the line tolerance of `line`, ordered innermost
556
+ * (smallest span) first so a nested handler wins over its enclosing function.
557
+ *
558
+ * The tolerance is one-directional in effect, the same rule the v1 inferrer's
559
+ * `findFunctionByLine` applies (carrick#766/#770). Looking BACK is free: a
560
+ * function starting before the anchor is one the anchor sits inside or just
561
+ * after. Looking FORWARD reaches a handler that starts a line or two into the
562
+ * registration the anchor names — and, unguarded, also reaches the next
563
+ * declaration in the file when the anchor lands on a statement that declares no
564
+ * function at all. Nothing but trivia may sit between the anchor and the
565
+ * function it is taken to name.
566
+ */
567
+ function functionsNearLine(sourceFile, line) {
568
+ const found = [];
569
+ const visit = (node) => {
570
+ if (isHandlerFunctionLike(node) &&
571
+ Math.abs(startLineOf(sourceFile, node) - line) <= HANDLER_LINE_TOLERANCE) {
572
+ found.push(node);
573
+ }
574
+ node.forEachChild(visit);
575
+ };
576
+ visit(sourceFile);
577
+ const windowStatements = statementsOpeningInWindow(sourceFile, line);
578
+ return found
579
+ .filter((fn) => startLineOf(sourceFile, fn) <= line ||
580
+ !separatedFromAnchor(sourceFile, windowStatements, fn))
581
+ .sort((a, b) => a.getEnd() - a.getStart() - (b.getEnd() - b.getStart()));
582
+ }
583
+ /**
584
+ * Statements opening inside the forward window `[line, line + tolerance]`.
585
+ *
586
+ * Statement LIST members, not every node the checker calls a statement: the
587
+ * members of a source file, a block, a module block or a case clause are the
588
+ * things that can stand between an anchor and a later declaration. Collected
589
+ * from the lists themselves because the raw compiler API has no total
590
+ * "is this a statement" predicate the way ts-morph does.
591
+ */
592
+ function statementsOpeningInWindow(sourceFile, line) {
593
+ const found = [];
594
+ const consider = (statements) => {
595
+ for (const statement of statements) {
596
+ const start = startLineOf(sourceFile, statement);
597
+ if (start >= line && start <= line + HANDLER_LINE_TOLERANCE) {
598
+ found.push(statement);
599
+ }
600
+ }
601
+ };
602
+ const visit = (node) => {
603
+ if (ts.isSourceFile(node) ||
604
+ ts.isBlock(node) ||
605
+ ts.isModuleBlock(node) ||
606
+ ts.isCaseClause(node) ||
607
+ ts.isDefaultClause(node)) {
608
+ consider(node.statements);
609
+ }
610
+ node.forEachChild(visit);
611
+ };
612
+ visit(sourceFile);
613
+ return found;
614
+ }
615
+ /**
616
+ * Whether a statement opening in the forward window stands between the anchor
617
+ * and `fn` — it begins on an earlier line than `fn` and does not contain it.
618
+ *
619
+ * `broker.register('key', (payload) => …)` spread over three lines is one
620
+ * statement that CONTAINS its handler, so the handler is reachable.
621
+ * `export { handler };` followed by `function buildQuery(payload)` is two
622
+ * statements, and the second is not what the first names.
623
+ */
624
+ function separatedFromAnchor(sourceFile, windowStatements, fn) {
625
+ const fnStart = fn.getStart(sourceFile);
626
+ const fnLine = startLineOf(sourceFile, fn);
627
+ return windowStatements.some((statement) => startLineOf(sourceFile, statement) < fnLine &&
628
+ !(statement.getStart(sourceFile) <= fnStart &&
629
+ statement.getEnd() >= fn.getEnd()));
630
+ }
631
+ function startLineOf(sourceFile, node) {
632
+ return (sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1);
633
+ }
634
+ function isHandlerFunctionLike(node) {
635
+ return (ts.isArrowFunction(node) ||
636
+ ts.isFunctionExpression(node) ||
637
+ ts.isFunctionDeclaration(node) ||
638
+ ts.isMethodDeclaration(node));
639
+ }
640
+ function enclosingFunction(node) {
641
+ let current = node;
642
+ while (current) {
643
+ if (isHandlerFunctionLike(current))
644
+ return current;
645
+ current = current.parent;
646
+ }
647
+ return undefined;
648
+ }
649
+ /**
650
+ * The three binding shapes a `function_param` locator can name, mirroring the
651
+ * v1 inferrer's `resolveParamTarget` so both anchor paths answer identically:
652
+ *
653
+ * 1. an exact parameter name (`(payload) => …` <- "payload");
654
+ * 2. a whole destructured binding pattern under whitespace normalisation
655
+ * (`({ id, total }) => …` <- "{ id, total }") — the pattern's own type IS
656
+ * the payload;
657
+ * 3. one named binding element inside a destructured parameter
658
+ * (`({ payload }) => …` <- "payload") — the payload is a property of an
659
+ * envelope parameter, and the checker projects the element's type.
660
+ */
661
+ function resolveParamTarget(fn, paramName) {
662
+ const params = fn.parameters;
663
+ for (const param of params) {
664
+ if (ts.isIdentifier(param.name) && param.name.text === paramName) {
665
+ return param;
666
+ }
667
+ }
668
+ const wanted = paramName.replace(/\s+/g, ' ').trim();
669
+ for (const param of params) {
670
+ if ((ts.isObjectBindingPattern(param.name) ||
671
+ ts.isArrayBindingPattern(param.name)) &&
672
+ param.name.getText().replace(/\s+/g, ' ').trim() === wanted) {
673
+ return param;
674
+ }
675
+ }
676
+ for (const param of params) {
677
+ if (!ts.isObjectBindingPattern(param.name))
678
+ continue;
679
+ for (const element of param.name.elements) {
680
+ if (ts.isIdentifier(element.name) && element.name.text === paramName) {
681
+ return element;
682
+ }
683
+ }
684
+ }
685
+ return undefined;
686
+ }
687
+ function paramLocatorHints(request) {
688
+ return request.line_number !== undefined
689
+ ? `line ${request.line_number}`
690
+ : 'no line hint';
691
+ }
692
+ function locatorFailureReason(request) {
693
+ const hints = [];
694
+ if (request.span_start !== undefined)
695
+ hints.push(`span ${request.span_start}-${request.span_end}`);
696
+ if (request.expression_text)
697
+ hints.push(`expression '${request.expression_text.slice(0, 40)}'`);
698
+ if (request.line_number !== undefined)
699
+ hints.push(`line ${request.line_number}`);
700
+ return `no node located in ${request.source_file} (${hints.join(', ') || 'no locator hints'})`;
701
+ }
702
+ /**
703
+ * Locate the target node: tightest expression covering the byte span when
704
+ * given; else the expression matching expression_text on/after line_number;
705
+ * else the first expression starting on line_number.
706
+ */
707
+ export function locateNode(sourceFile, request) {
708
+ if (request.span_start !== undefined && request.span_end !== undefined) {
709
+ const bySpan = tightestCoveringNode(sourceFile, request.span_start, request.span_end);
710
+ if (bySpan)
711
+ return bySpan;
712
+ }
713
+ if (request.expression_text) {
714
+ const byText = nodeByExpressionText(sourceFile, request.expression_text, request.line_number);
715
+ if (byText)
716
+ return byText;
717
+ }
718
+ if (request.line_number !== undefined) {
719
+ return firstExpressionOnLine(sourceFile, request.line_number);
720
+ }
721
+ return undefined;
722
+ }
723
+ function isPreferredTarget(node) {
724
+ return (ts.isExpression(node) ||
725
+ ts.isVariableDeclaration(node) ||
726
+ ts.isParameter(node) ||
727
+ ts.isPropertyAssignment(node));
728
+ }
729
+ function tightestCoveringNode(sourceFile, start, end) {
730
+ let best;
731
+ const visit = (node) => {
732
+ if (node.getStart(sourceFile) <= start && node.getEnd() >= end) {
733
+ if (isPreferredTarget(node))
734
+ best = node;
735
+ node.forEachChild(visit);
736
+ }
737
+ else {
738
+ node.forEachChild(visit);
739
+ }
740
+ };
741
+ visit(sourceFile);
742
+ return best;
743
+ }
744
+ function nodeByExpressionText(sourceFile, text, fromLine) {
745
+ const wanted = text.replace(/\s+/g, ' ').trim();
746
+ let best;
747
+ const visit = (node) => {
748
+ if (best)
749
+ return;
750
+ if (isPreferredTarget(node)) {
751
+ const nodeText = node.getText(sourceFile).replace(/\s+/g, ' ').trim();
752
+ if (nodeText === wanted) {
753
+ const line = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
754
+ if (fromLine === undefined || line >= fromLine) {
755
+ best = node;
756
+ return;
757
+ }
758
+ }
759
+ }
760
+ node.forEachChild(visit);
761
+ };
762
+ visit(sourceFile);
763
+ return best;
764
+ }
765
+ function firstExpressionOnLine(sourceFile, line) {
766
+ let best;
767
+ const visit = (node) => {
768
+ const nodeLine = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
769
+ if (nodeLine === line && isPreferredTarget(node) && !best) {
770
+ best = node;
771
+ return;
772
+ }
773
+ node.forEachChild(visit);
774
+ };
775
+ visit(sourceFile);
776
+ return best;
777
+ }
778
+ // ===========================================================================
779
+ // #438/#439 symbol-anchor guards: value-only demotion + const->type re-aim.
780
+ // ===========================================================================
781
+ /** Follow re-export aliases to the symbol they ultimately denote. */
782
+ function resolveSymbolAliases(checker, symbol) {
783
+ let current = symbol;
784
+ // Bounded: alias chains are short; the guard is against a pathological cycle.
785
+ for (let i = 0; i < 16 && (current.flags & ts.SymbolFlags.Alias) !== 0; i++) {
786
+ let next;
787
+ try {
788
+ next = checker.getAliasedSymbol(current);
789
+ }
790
+ catch {
791
+ break;
792
+ }
793
+ if (!next || next === current)
794
+ break;
795
+ current = next;
796
+ }
797
+ return current;
798
+ }
799
+ /**
800
+ * True when the symbol can stand in TYPE position (class, interface, enum,
801
+ * type alias, type parameter, ...). A pure value export (const / let / var /
802
+ * function) has no type-space meaning, so referencing it as a type is TS2694.
803
+ * Structural — never a name check. `ts.SymbolFlags.Type` is the compiler's own
804
+ * composite of the type-space meanings.
805
+ */
806
+ function symbolHasTypeMeaning(symbol) {
807
+ return (symbol.flags & ts.SymbolFlags.Type) !== 0;
808
+ }
809
+ /**
810
+ * #439 part 2: when a symbol anchor names a value-only export, look in the
811
+ * SAME module for a single exported type alias whose type node contains a
812
+ * `typeof <const>` type-query (covers `z.infer<typeof C>`, `Infer<typeof C>`,
813
+ * and any equivalent — matched structurally, no library names). Exactly one
814
+ * match re-aims; zero or several fall back to the value-only demotion (never
815
+ * guess which of an input/output pair is the request).
816
+ */
817
+ function reaimValueSymbolToTypeSibling(checker, sourceFile, ctx) {
818
+ const moduleSymbol = checker.getSymbolAtLocation(sourceFile);
819
+ if (!moduleSymbol)
820
+ return undefined;
821
+ const matches = [];
822
+ for (const exported of checker.getExportsOfModule(moduleSymbol)) {
823
+ const resolved = resolveSymbolAliases(checker, exported);
824
+ if ((resolved.flags & ts.SymbolFlags.TypeAlias) === 0)
825
+ continue;
826
+ const decl = resolved.declarations?.find(ts.isTypeAliasDeclaration);
827
+ if (!decl)
828
+ continue;
829
+ if (typeAliasRhsInfersConst(checker, decl.type, ctx.request.symbol_name, ctx.valueSymbol)) {
830
+ matches.push(exported.getName());
831
+ }
832
+ }
833
+ if (matches.length !== 1)
834
+ return undefined;
835
+ return {
836
+ request: ctx.request,
837
+ aliasText: `import('${ctx.spec}').${matches[0]}${ctx.arraySuffix}`,
838
+ serialization: 'emitted',
839
+ reaimNote: `symbol anchor '${ctx.request.symbol_name}' is a value; re-aimed at sibling ` +
840
+ `type alias '${matches[0]}' (derived via \`typeof ${ctx.request.symbol_name}\`)`,
841
+ };
842
+ }
843
+ /**
844
+ * True iff the type alias RHS IS the inferred type derived from a
845
+ * `typeof <const>` query — the ordinary `export type T = z.infer<typeof C>`
846
+ * shape — and NOT a wrapper that merely embeds it. Structurally: the whole
847
+ * type node is a generic type reference (`Infer<typeof C>` /
848
+ * `SomeGeneric<typeof C>`) whose DIRECT type arguments include a `typeof C`
849
+ * query.
850
+ *
851
+ * An object / union / intersection / array that embeds the query as a member
852
+ * or element (`{ data: Infer<typeof C>; meta: string }`) is a WRAPPER: re-aiming
853
+ * at it would capture the wrapper shape, not the payload, and (since the
854
+ * wrapper self-checks concretely) manufacture a false incompatible. Those fall
855
+ * back to the value-only demotion — a guaranteed abstain, never a wrong
856
+ * concrete verdict. A bare `typeof C` alias is also rejected: it denotes the
857
+ * schema wrapper value's type, not the inferred payload. Still no library-name
858
+ * checks: matched purely on shape.
859
+ */
860
+ function typeAliasRhsInfersConst(checker, node, symbolName, valueSymbol) {
861
+ if (!ts.isTypeReferenceNode(node))
862
+ return false;
863
+ for (const arg of node.typeArguments ?? []) {
864
+ let inner = arg;
865
+ while (ts.isParenthesizedTypeNode(inner))
866
+ inner = inner.type;
867
+ if (ts.isTypeQueryNode(inner) &&
868
+ typeQueryMatchesSymbol(checker, inner, symbolName, valueSymbol)) {
869
+ return true;
870
+ }
871
+ }
872
+ return false;
873
+ }
874
+ /**
875
+ * Does a `typeof X` query denote `valueSymbol`? Symbol identity is primary
876
+ * (resolves through re-exports and works on a bare checkout: the value symbol
877
+ * resolves without the schema library present); a textual identifier match on
878
+ * `symbolName` is the fallback.
879
+ */
880
+ function typeQueryMatchesSymbol(checker, query, symbolName, valueSymbol) {
881
+ const entity = query.exprName;
882
+ const idNode = ts.isQualifiedName(entity) ? entity.right : entity;
883
+ const queried = checker.getSymbolAtLocation(entity) ?? checker.getSymbolAtLocation(idNode);
884
+ if (queried && resolveSymbolAliases(checker, queried) === valueSymbol)
885
+ return true;
886
+ return ts.isIdentifier(idNode) && idNode.text === symbolName;
887
+ }
888
+ function reaimBuilderChainPayload(checker, located) {
889
+ const descriptor = enclosingChainDescriptorArgument(checker, located);
890
+ if (!descriptor)
891
+ return { kind: 'none' };
892
+ const chain = fluentChainCalls(descriptor.call);
893
+ if (chain.length < 2)
894
+ return { kind: 'none' };
895
+ const candidates = payloadCandidates(checker, chain, descriptor.argument);
896
+ if (candidates.length === 1) {
897
+ return {
898
+ kind: 'reaim',
899
+ node: candidates[0],
900
+ note: 'producer anchor landed on a builder-chain config descriptor; ' +
901
+ 're-aimed at the chain schema argument',
902
+ };
903
+ }
904
+ // Zero (a descriptor with no schema argument) or several (ambiguous
905
+ // input/output schemas): never keep the descriptor captured.
906
+ return {
907
+ kind: 'demote',
908
+ reason: candidates.length === 0
909
+ ? 'producer anchor landed on a builder-chain config descriptor with no ' +
910
+ 'identifiable schema argument; demoted to keep the request type honest'
911
+ : `producer anchor landed on a builder-chain config descriptor; ` +
912
+ `${candidates.length} schema arguments are present and cannot be ` +
913
+ 'disambiguated structurally; demoted to keep the request type honest',
914
+ };
915
+ }
916
+ /**
917
+ * Walk up from the located node to the enclosing call argument that carries the
918
+ * descriptor signature: all-literal metadata, written inline or referenced by
919
+ * name (#497). Returns the call and the descriptor argument itself, or
920
+ * undefined when the located node is not inside such a descriptor. The argument
921
+ * comes back so the caller can exclude exactly that node from the payload
922
+ * candidates by identity.
923
+ */
924
+ function enclosingChainDescriptorArgument(checker, located) {
925
+ let node = located;
926
+ while (node) {
927
+ const parent = node.parent;
928
+ if (parent &&
929
+ ts.isCallExpression(parent) &&
930
+ parent.arguments.includes(node) &&
931
+ isMetadataDescriptor(checker, node)) {
932
+ return { call: parent, argument: node };
933
+ }
934
+ node = parent;
935
+ }
936
+ return undefined;
937
+ }
938
+ /**
939
+ * A config-descriptor argument: a non-empty, all-literal object literal, either
940
+ * written inline or named by a reference whose declaration initialises it to
941
+ * one (#497 — `.meta(createWidgetMeta)` with the const in a sibling module).
942
+ *
943
+ * The REFERENCE spelling is deliberately STRICTER than the inline one, and the
944
+ * asymmetry is load-bearing. In this file's model an inline all-literal object
945
+ * argument is never a payload, so the inline test can stay loose. A reference is
946
+ * an ordinary identifier that `payloadCandidates` otherwise treats as a payload
947
+ * candidate, so classifying one as a descriptor takes it out of the running: a
948
+ * flat hand-rolled const (`const s = { id: 0, name: "" }` passed to `.input`)
949
+ * would be misread as the descriptor and the capture would re-aim onto the
950
+ * metadata beside it — manufacturing the exact defect #497 fixes. Requiring a
951
+ * nested object-literal property (the `{ openapi: { ... } }` shape both observed
952
+ * variants carry) separates a descriptor from a flat payload const without
953
+ * consulting a single method or library name.
954
+ */
955
+ function isMetadataDescriptor(checker, arg) {
956
+ if (ts.isObjectLiteralExpression(arg))
957
+ return isAllLiteralMetadata(arg);
958
+ const literal = referencedObjectLiteral(checker, arg);
959
+ return (literal !== undefined &&
960
+ isAllLiteralMetadata(literal) &&
961
+ literal.properties.some((p) => ts.isPropertyAssignment(p) && ts.isObjectLiteralExpression(p.initializer)));
962
+ }
963
+ /**
964
+ * The object literal a reference ultimately initialises to, or undefined. Only
965
+ * plain name references are followed (an identifier or a property access);
966
+ * re-export aliases are resolved so an imported const reaches its declaration,
967
+ * and `as const` / `satisfies` / parenthesised initialisers are unwrapped.
968
+ */
969
+ function referencedObjectLiteral(checker, arg) {
970
+ if (!ts.isIdentifier(arg) && !ts.isPropertyAccessExpression(arg))
971
+ return undefined;
972
+ const symbol = checker.getSymbolAtLocation(arg);
973
+ if (!symbol)
974
+ return undefined;
975
+ const declaration = resolveSymbolAliases(checker, symbol).valueDeclaration;
976
+ if (!declaration)
977
+ return undefined;
978
+ let initializer;
979
+ if (ts.isVariableDeclaration(declaration) || ts.isPropertyAssignment(declaration)) {
980
+ initializer = declaration.initializer;
981
+ }
982
+ while (initializer &&
983
+ (ts.isAsExpression(initializer) ||
984
+ ts.isSatisfiesExpression(initializer) ||
985
+ ts.isParenthesizedExpression(initializer) ||
986
+ ts.isTypeAssertionExpression(initializer))) {
987
+ initializer = initializer.expression;
988
+ }
989
+ return initializer && ts.isObjectLiteralExpression(initializer)
990
+ ? initializer
991
+ : undefined;
992
+ }
993
+ /**
994
+ * The full set of calls in the fluent chain containing `anyCall`
995
+ * (`x.a(...).b(...).c(...)` -> the three calls). Climbs to the outermost call,
996
+ * then descends the callee chain.
997
+ */
998
+ function fluentChainCalls(anyCall) {
999
+ let outer = anyCall;
1000
+ for (;;) {
1001
+ const access = outer.parent;
1002
+ if (access &&
1003
+ (ts.isPropertyAccessExpression(access) || ts.isElementAccessExpression(access)) &&
1004
+ access.expression === outer &&
1005
+ access.parent &&
1006
+ ts.isCallExpression(access.parent) &&
1007
+ access.parent.expression === access) {
1008
+ outer = access.parent;
1009
+ continue;
1010
+ }
1011
+ break;
1012
+ }
1013
+ const calls = [];
1014
+ let current = outer;
1015
+ while (ts.isCallExpression(current)) {
1016
+ calls.push(current);
1017
+ const callee = current.expression;
1018
+ if (ts.isPropertyAccessExpression(callee) || ts.isElementAccessExpression(callee)) {
1019
+ current = callee.expression;
1020
+ }
1021
+ else {
1022
+ break;
1023
+ }
1024
+ }
1025
+ return calls;
1026
+ }
1027
+ /**
1028
+ * Chain arguments that carry type-space PAYLOAD meaning: a named or constructed
1029
+ * schema (identifier / member access / call) whose RESOLVED type is an object
1030
+ * shape. Two filters, both load-bearing:
1031
+ * - syntactic: inline object/array literals and inline functions (handlers)
1032
+ * are never candidates;
1033
+ * - semantic: a candidate whose resolved type is a primitive or literal
1034
+ * (`.tag(routeName)` with `routeName: string`), a bare callable (a named
1035
+ * handler), or a top type is REJECTED — capturing a string-literal type as a
1036
+ * request payload manufactures a false incompatible. Only an object-typed
1037
+ * argument is a real schema.
1038
+ * With this filter the single-candidate re-aim fires only on a genuine schema,
1039
+ * and a non-object, ambiguous (>1), or absent candidate abstains via demote.
1040
+ *
1041
+ * `descriptor` is the argument the anchor landed in, excluded by NODE IDENTITY
1042
+ * (#497). Identity, not shape: an inline descriptor was already excluded by the
1043
+ * syntactic filter, but a descriptor named by reference is an identifier like
1044
+ * any other, and excluding it by shape would also strike out a payload schema
1045
+ * that happens to be an all-literal const.
1046
+ */
1047
+ function payloadCandidates(checker, chain, descriptor) {
1048
+ const out = [];
1049
+ for (const call of chain) {
1050
+ for (const arg of call.arguments) {
1051
+ if (arg === descriptor)
1052
+ continue;
1053
+ if ((ts.isIdentifier(arg) ||
1054
+ ts.isPropertyAccessExpression(arg) ||
1055
+ ts.isElementAccessExpression(arg) ||
1056
+ ts.isCallExpression(arg)) &&
1057
+ typeIsObjectPayload(checker.getTypeAtLocation(arg))) {
1058
+ out.push(arg);
1059
+ }
1060
+ }
1061
+ }
1062
+ return out;
1063
+ }
1064
+ /**
1065
+ * A type with object/payload meaning: a real object shape (or a union /
1066
+ * intersection of them), never a primitive, string/number/boolean/bigint/enum
1067
+ * literal, null/undefined/void, a top type (`any`/`unknown`/`never` — cannot be
1068
+ * confirmed a payload), or a bare callable value (a handler function). This is
1069
+ * the gate that keeps the builder-chain re-aim from capturing a non-schema.
1070
+ */
1071
+ function typeIsObjectPayload(type) {
1072
+ const nonObject = ts.TypeFlags.Any |
1073
+ ts.TypeFlags.Unknown |
1074
+ ts.TypeFlags.Never |
1075
+ ts.TypeFlags.Null |
1076
+ ts.TypeFlags.Undefined |
1077
+ ts.TypeFlags.Void |
1078
+ ts.TypeFlags.StringLike |
1079
+ ts.TypeFlags.NumberLike |
1080
+ ts.TypeFlags.BooleanLike |
1081
+ ts.TypeFlags.BigIntLike |
1082
+ ts.TypeFlags.ESSymbolLike |
1083
+ ts.TypeFlags.EnumLike;
1084
+ if (type.flags & nonObject)
1085
+ return false;
1086
+ if (type.flags & (ts.TypeFlags.Union | ts.TypeFlags.Intersection)) {
1087
+ // Every constituent must be an object shape: `string | { a }` is not a
1088
+ // clean payload, while a union of objects stays eligible.
1089
+ return type.types.every(typeIsObjectPayload);
1090
+ }
1091
+ if (type.flags & ts.TypeFlags.Object) {
1092
+ // A bare callable (a named handler passed as a value) is not a payload; a
1093
+ // schema object carries no top-level call/construct signature.
1094
+ if (type.getCallSignatures().length > 0 ||
1095
+ type.getConstructSignatures().length > 0) {
1096
+ return false;
1097
+ }
1098
+ return true;
1099
+ }
1100
+ return false;
1101
+ }
1102
+ /** A non-empty object literal whose every property is a literal-typed value. */
1103
+ function isAllLiteralMetadata(obj) {
1104
+ if (obj.properties.length === 0)
1105
+ return false;
1106
+ for (const prop of obj.properties) {
1107
+ if (!ts.isPropertyAssignment(prop))
1108
+ return false;
1109
+ if (!isLiteralValueExpression(prop.initializer))
1110
+ return false;
1111
+ }
1112
+ return true;
1113
+ }
1114
+ function isLiteralValueExpression(expr) {
1115
+ switch (expr.kind) {
1116
+ case ts.SyntaxKind.StringLiteral:
1117
+ case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
1118
+ case ts.SyntaxKind.NumericLiteral:
1119
+ case ts.SyntaxKind.TrueKeyword:
1120
+ case ts.SyntaxKind.FalseKeyword:
1121
+ case ts.SyntaxKind.NullKeyword:
1122
+ return true;
1123
+ }
1124
+ if (ts.isPrefixUnaryExpression(expr))
1125
+ return isLiteralValueExpression(expr.operand);
1126
+ if (ts.isArrayLiteralExpression(expr)) {
1127
+ return expr.elements.every(isLiteralValueExpression);
1128
+ }
1129
+ if (ts.isObjectLiteralExpression(expr))
1130
+ return isAllLiteralMetadata(expr);
1131
+ return false;
1132
+ }