@component-compass/reference-graph 0.1.18 → 0.1.20

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 (41) hide show
  1. package/dist/builder.d.ts +10 -4
  2. package/dist/builder.js +13 -9
  3. package/dist/builder.js.map +1 -1
  4. package/dist/engine/assert-never.d.ts +1 -1
  5. package/dist/engine/assert-never.js +1 -1
  6. package/dist/engine/component-shape.d.ts +18 -7
  7. package/dist/engine/component-shape.js +48 -18
  8. package/dist/engine/component-shape.js.map +1 -1
  9. package/dist/engine/helper-callers.d.ts +24 -14
  10. package/dist/engine/helper-callers.js +43 -70
  11. package/dist/engine/helper-callers.js.map +1 -1
  12. package/dist/engine/host-element.d.ts +37 -0
  13. package/dist/engine/host-element.js +55 -0
  14. package/dist/engine/host-element.js.map +1 -0
  15. package/dist/engine/index.d.ts +4 -4
  16. package/dist/engine/index.js +181 -91
  17. package/dist/engine/index.js.map +1 -1
  18. package/dist/engine/library-stubs.js +1 -1
  19. package/dist/engine/member-identity.d.ts +9 -10
  20. package/dist/engine/member-identity.js +9 -10
  21. package/dist/engine/member-identity.js.map +1 -1
  22. package/dist/engine/re-export-chain.d.ts +1 -1
  23. package/dist/engine/re-export-chain.js +4 -3
  24. package/dist/engine/re-export-chain.js.map +1 -1
  25. package/dist/engine/registry.d.ts +43 -19
  26. package/dist/engine/registry.js +66 -50
  27. package/dist/engine/registry.js.map +1 -1
  28. package/dist/engine/resolve-reference.js +1 -1
  29. package/dist/engine/resolve-reference.js.map +1 -1
  30. package/dist/engine/resolve-type.d.ts +12 -0
  31. package/dist/engine/resolve-type.js +44 -52
  32. package/dist/engine/resolve-type.js.map +1 -1
  33. package/dist/engine/wrapper-folding.d.ts +22 -3
  34. package/dist/engine/wrapper-folding.js +146 -132
  35. package/dist/engine/wrapper-folding.js.map +1 -1
  36. package/dist/index.d.ts +1 -0
  37. package/dist/index.js +1 -0
  38. package/dist/index.js.map +1 -1
  39. package/dist/types/file-graph.d.ts +29 -13
  40. package/dist/types/inferred-type.d.ts +2 -1
  41. package/package.json +2 -2
@@ -1,4 +1,4 @@
1
- import { resolveType, resolveToFunctions, DYNAMIC_MEMBER_KEY } from "./resolve-type.js";
1
+ import { resolveType, resolveToFunctions, resolveDynamicImportTarget, DYNAMIC_MEMBER_KEY } from "./resolve-type.js";
2
2
  import { argumentRefKey } from "./argument-map.js";
3
3
  import { createCycleGuard } from "./cycle-detection.js";
4
4
  import { fileGraphForRef, resolveReference } from "./resolve-reference.js";
@@ -7,7 +7,7 @@ import { followReExportChain } from "./re-export-chain.js";
7
7
  import { unparsedMemberDefinition, effectiveExportName, residualMemberChain, compoundExportName } from "./member-identity.js";
8
8
  import { evalKind, isImportBackedLeaf } from "./component-shape.js";
9
9
  import { assertNever } from "./assert-never.js";
10
- /** Ruling 19: true when a `ReturnTypeOf`'s callee is a data-method render
10
+ /** True when a `ReturnTypeOf`'s callee is a data-method render
11
11
  * call (`items.map(render)`) rather than a wrapper taking a component as a
12
12
  * prop — the exact structural condition `component-shape.ts`'s rule (ii)
13
13
  * uses to answer `"jsx"` instead of `"component"`. Shared, not re-derived:
@@ -17,7 +17,7 @@ function isDataMethodRenderCall(type, graph, fileGraph, guard) {
17
17
  return type.callee.kind === "MemberOf" && !isImportBackedLeaf(type.callee, graph, fileGraph, guard);
18
18
  }
19
19
  /**
20
- * THE single seam for unreducible-leaf identity synthesis (#135 / #274).
20
+ * THE single seam for unreducible-leaf identity synthesis.
21
21
  *
22
22
  * Maps `resolveType` results into FoldedTerminals. When the algebra preserved
23
23
  * an identity-bearing leaf — an import-backed TypeOf or a DynamicImport that
@@ -40,12 +40,10 @@ function foldTerminals(graph, fileGraph, results, fallback) {
40
40
  }
41
41
  }
42
42
  // A fan-out branch (Union / dynamic-map) is stamped as `source`. Derive
43
- // from it FIRST — a branch that has its own binding must win (#131)
43
+ // from it FIRST — a branch that has its own binding must win —
44
44
  // and only fall back to the resolved binding when the branch carries no
45
45
  // identity of its own (e.g. a bare `{kind:"JSX"}` branch from a lowered
46
- // ternary/logical render body). Committing to `source ?? fallback` before
47
- // deriving discarded the fallback entirely once any fanout occurred, even
48
- // when the branch itself resolved to nothing.
46
+ // ternary/logical render body).
49
47
  const identity = (source && deriveIdentity(source, fileGraph, graph)) ?? deriveIdentity(fallback, fileGraph, graph);
50
48
  return {
51
49
  type: t,
@@ -60,10 +58,11 @@ function foldTerminals(graph, fileGraph, results, fallback) {
60
58
  * a seen-through wrapper and an opaque one attribute identically. Inner
61
59
  * terminals pass through with the wrapper hop prepended. When the inner walk
62
60
  * bottomed out at Unknown but carried an identity hint (imported, or
63
- * local-promoted via #101), synthesise a JSX terminal from it — without this
61
+ * local-promoted through the re-export chain), synthesise a JSX terminal
62
+ * from it — without this
64
63
  * rescue, HOC-wrapped occurrences drop silently (~128 in example-web's
65
- * connect/flow/withTranslators patterns, diagnosed on PR #141). Truly
66
- * nothing → a single Unknown terminal, as before.
64
+ * connect/flow/withTranslators patterns). Truly
65
+ * nothing → a single Unknown terminal.
67
66
  */
68
67
  function foldToWrappedArg(inner, via) {
69
68
  const innerHasNonUnknown = inner.some((t) => t.type.kind !== "Unknown");
@@ -83,7 +82,7 @@ function foldToWrappedArg(inner, via) {
83
82
  * resolution. THE single helper for both via kinds that describe a wrapped
84
83
  * argument (`hoc-wrapper` when the wrapper folds to the argument,
85
84
  * `passed-as-argument` when the holder is its own identity and the argument
86
- * is seeded at its site — spec §4), so the two name a binding identically.
85
+ * is seeded at its site), so the two name a binding identically.
87
86
  * Never re-derive `{ specifier, import }` for an argument elsewhere.
88
87
  */
89
88
  export function argumentProvenance(type, arg, fileGraph) {
@@ -128,10 +127,8 @@ function identityKey(id) {
128
127
  function isPassThroughWrapper(algResult, inner) {
129
128
  const innerKeys = new Set(inner.filter((t) => t.type.kind !== "Unknown" && t.identity !== null).map((t) => identityKey(t.identity)));
130
129
  const algKeys = algResult.filter((t) => t.type.kind !== "Unknown");
131
- // A null identity on either side is "no information", never a match — the
132
- // old null-equals-null comparison folded factory calls onto their
133
- // rehydrate/config argument once the callee unwrap stopped inventing an
134
- // identity for the algebra side.
130
+ // A null identity on either side is "no information", never a match:
131
+ // null-equals-null folds a factory call onto its rehydrate/config argument.
135
132
  return algKeys.length > 0 && algKeys.every((t) => t.identity !== null && innerKeys.has(identityKey(t.identity)));
136
133
  }
137
134
  /**
@@ -143,21 +140,18 @@ function isPassThroughWrapper(algResult, inner) {
143
140
  * For nodes where no folding rule applies, delegates to `resolveType`
144
141
  * (pure algebra) and computes identity from the input type's leaf TypeOf
145
142
  * when one can be confidently identified.
146
- *
147
- * See spec: docs/superpowers/specs/2026-05-19-wrapper-folding-engine-extension-design.md §4.3
148
143
  */
149
144
  export function walkWithFolding(graph, fileGraph, type, argMap, guard = createCycleGuard(),
150
- // Ruling 20: the identity `foldTerminals` falls back to when a terminal
145
+ // The identity `foldTerminals` falls back to when a terminal
151
146
  // carries no identity of its own (no `source`, no unreducible TypeOf/
152
- // DynamicImport leaf). Defaults to `type` (the walked node itself)
153
- // unchanged behaviour for every caller that doesn't pass this. The
147
+ // DynamicImport leaf). Defaults to `type` (the walked node itself). The
154
148
  // `Function` arm below is the ONE place that overrides it: a component's
155
149
  // own return (`return priceElement;`, `return contactModals;` — a bare
156
150
  // JSX-value alias or a hook-return leaf) must keep deriving identity as
157
151
  // "nothing", so the engine's own fallback (the rendered declaration, e.g.
158
152
  // `OfferPrice`) wins — never a same-file local alias like `priceElement`
159
153
  // that happens to sit in `foldTerminals`' fallback slot. Per-branch
160
- // identity from a fanout `source` (#131 — Union([TypeOf A, TypeOf B])) is
154
+ // identity from a fanout `source` (Union([TypeOf A, TypeOf B])) is
161
155
  // untouched: `source` is checked BEFORE this fallback in `foldTerminals`.
162
156
  identityFallback) {
163
157
  const fallback = identityFallback ?? type;
@@ -170,26 +164,25 @@ identityFallback) {
170
164
  }
171
165
  return walkStaticMember(graph, fileGraph, type, argMap, guard, fallback);
172
166
  case "Function":
173
- // Ruling 19: a component's own returns can contain a nested
167
+ // A component's own returns can contain a nested
174
168
  // ReturnTypeOf/MemberOf (`List = ({items}) => items.map(render)`) that
175
169
  // needs wrapper-folding's steps (the opaque-callee / dynamic-map
176
170
  // handling below), not `resolveType`'s plain algebra — `resolveType`'s
177
171
  // own "Function" case recurses via plain `resolveType`, which never
178
172
  // re-enters this walk, so a JSX usage resolving to such a Function
179
- // (e.g. `<List/>`) silently produced zero occurrences before this.
180
- // Mirrors `resolveType`'s "Function" case, one level up. Ruling 20:
181
- // pass THIS Function as the identity fallback for every return — a
173
+ // (e.g. `<List/>`) would silently produce zero occurrences.
174
+ // Mirrors `resolveType`'s "Function" case, one level up. Pass THIS
175
+ // Function as the identity fallback for every return — a
182
176
  // Function is never TypeOf/DynamicImport, so `deriveIdentity` on it
183
- // is always null, matching the pre-ruling-19 semantics this restores.
177
+ // is always null.
184
178
  return type.returns.flatMap((r) => walkWithFolding(graph, fileGraph, r, argMap, guard, type));
185
179
  case "TypeOf":
186
180
  return walkTypeOf(graph, fileGraph, type, argMap, guard, fallback);
187
181
  case "Union":
188
- // Structural re-entry (D1/D3): each branch walks through the walker so
182
+ // Structural re-entry: each branch walks through the walker so
189
183
  // a TypeOf branch reaches `walkTypeOf` and decides its own identity
190
- // (per-branch attribution, #131) while a bare JSX branch — a lowered
191
- // ternary/logical render body — derives from the outer fallback exactly
192
- // as the algebra's `source ?? fallback` did.
184
+ // (per-branch attribution) while a bare JSX branch — a lowered
185
+ // ternary/logical render body — derives from the outer fallback.
193
186
  return type.types.flatMap((branch) => walkWithFolding(graph, fileGraph, branch, argMap, guard, fallback));
194
187
  case "ParameterOf":
195
188
  case "DynamicImport":
@@ -214,14 +207,14 @@ function foldLeaf(graph, fileGraph, type, argMap, guard, fallback) {
214
207
  return foldTerminals(graph, fileGraph, resolveType(graph, fileGraph, type, argMap, guard), fallback);
215
208
  }
216
209
  /**
217
- * THE seam for "walk a Function in its authoring file" (spec D4). A Function
210
+ * THE seam for "walk a Function in its authoring file". A Function
218
211
  * declared in another module must walk in THAT module's graph, because the
219
212
  * walker's file-scoped lookups inside it — a dynamic map's binding
220
213
  * (`findMapBinding`), an import table — only resolve where the code was
221
- * written. Originally `walkDispatcherReturns`' rationale (#136): a
222
- * cross-module `function getMapped(k) { return MAP[k]; }` walked in the
223
- * caller's fileGraph misses `MAP` entirely and loses the `dynamic-map` hop.
224
- * `walkTypeOf` needs the identical switch, so it is one function, not two.
214
+ * written: a cross-module `function getMapped(k) { return MAP[k]; }`
215
+ * walked in the caller's fileGraph misses `MAP` entirely and loses the
216
+ * `dynamic-map` hop. `walkDispatcherReturns` and `walkTypeOf` need the
217
+ * identical switch, so it is one function, not two.
225
218
  * Falls back to the caller's graph when the file is absent or unindexed.
226
219
  */
227
220
  function fileGraphForFunction(graph, fileGraph, fn) {
@@ -230,25 +223,58 @@ function fileGraphForFunction(graph, fileGraph, fn) {
230
223
  : fileGraph;
231
224
  }
232
225
  /**
233
- * Structural re-entry for a reference (spec D1–D4, D6; #521). Resolves ONE
226
+ * The file a DynamicImport's specifier resolves against: the file that
227
+ * contains the `import()`, whichever file the walk is standing in.
228
+ */
229
+ export function fileGraphForDynamicImport(graph, di, fallback) {
230
+ if (!graph || di.originFile === fallback.filePath)
231
+ return fallback;
232
+ return graph.files.get(di.originFile) ?? fallback;
233
+ }
234
+ /**
235
+ * The identity cascade for a lazy target.
236
+ *
237
+ * `resolvedFile` is the specifier canonicalised through the module resolver, so
238
+ * two spellings of one target — `./x` from one file, `../x` from another —
239
+ * carry the same `resolvedFile` and collapse to a single componentId
240
+ * downstream instead of minting a phantom each. A chain that resolves into the
241
+ * graph (workspace sibling, relative import, or unparsed workspace member)
242
+ * promotes to `local`, hashing to the same id as the local-index seed;
243
+ * everything else stays `imported`.
244
+ */
245
+ export function dynamicImportIdentity(graph, fileGraph, di) {
246
+ const origin = fileGraphForDynamicImport(graph, di, fileGraph);
247
+ const importedName = di.projection[0] ?? "default";
248
+ const resolvedFile = graph ? resolveImportedFile(graph, origin, di.specifier, importedName) : null;
249
+ return (workspaceSiblingIdentity(graph, di.specifier, importedName, resolvedFile) ??
250
+ relativeChainLocalIdentity(graph, origin, di.specifier, importedName) ??
251
+ memberLocalIdentity(graph, origin, di.specifier, importedName) ?? {
252
+ kind: "imported",
253
+ specifier: di.specifier,
254
+ imported: importedName,
255
+ resolvedFile,
256
+ });
257
+ }
258
+ /**
259
+ * Structural re-entry for a reference. Resolves ONE
234
260
  * hop and walks the resolved value through `walkWithFolding`, so every walker
235
- * rule — wrapper hops, dynamic maps, Ruling 19's `.map()` bodies — applies
236
- * through an alias exactly as at a direct render site. Before this arm the
237
- * default fold handed the whole subtree to `resolveType`, which never comes
238
- * back, so a map-bodied `List` reached via `MAP[k]` died as Unknown.
261
+ * rule — wrapper hops, dynamic maps, `.map()` bodies — applies
262
+ * through an alias exactly as at a direct render site. Without this arm the
263
+ * default fold hands the whole subtree to `resolveType`, which never comes
264
+ * back, so a map-bodied `List` reached via `MAP[k]` dies as Unknown.
239
265
  *
240
- * Identity (D2): a terminal that comes back with no identity of its own is
266
+ * Identity: a terminal that comes back with no identity of its own is
241
267
  * stamped with THIS reference's identity only when the reference names a
242
268
  * component-shaped value — `evalKind === "component"`, the one component-
243
269
  * shape seam, called not re-derived. A reference to a JSX value
244
270
  * (`return priceElement`) or to a parameter binding stays null so the outer
245
- * fallback wins: Ruling 20 by construction rather than by special case.
271
+ * fallback wins, by construction rather than by special case.
246
272
  *
247
- * An import the graph cannot see (`resolved` is Unknown) is NOT re-entered
248
- * (D6): it is the identity-bearing leaf `foldLeaf` synthesises from
249
- * (#135/#274), and `foldToWrappedArg`'s rescue relies on that shape.
273
+ * An import the graph cannot see (`resolved` is Unknown) is NOT re-entered:
274
+ * it is the identity-bearing leaf `foldLeaf` synthesises from, and
275
+ * `foldToWrappedArg`'s rescue relies on that shape.
250
276
  *
251
- * Cross-file (D4): a Function authored elsewhere walks in ITS file graph
277
+ * Cross-file: a Function authored elsewhere walks in ITS file graph
252
278
  * through the `fileGraphForFunction` seam — the same switch
253
279
  * `walkDispatcherReturns` makes — so file-scoped lookups inside it (a dynamic
254
280
  * map's binding) resolve where they were written.
@@ -286,13 +312,13 @@ function walkTypeOf(graph, fileGraph, type, argMap, guard, fallback) {
286
312
  }
287
313
  }
288
314
  /**
289
- * Static member access (spec D5; #512). Mirrors `walkDynamicMember` for one
315
+ * Static member access. Mirrors `walkDynamicMember` for one
290
316
  * named prop and without the `dynamic-map` hop: resolve the object, take the
291
317
  * member, walk the member's VALUE through `walkWithFolding` — so a wrapper
292
318
  * product stored in a map (`MAP.memo` → `memo(Button)`) reaches
293
319
  * `walkReturnTypeOf` and keeps its `hoc-wrapper` hop. Anything that is not an
294
320
  * in-graph Object (an Array, an import-backed namespace the graph cannot see)
295
- * takes the leaf fold exactly as before.
321
+ * takes the leaf fold.
296
322
  *
297
323
  * Cycle guard: mirrors `walkTypeOf`. `const A = { Item: () => B.Item };
298
324
  * const B = { Item: () => A.Item }` is legal, loadable JS and the walk
@@ -340,8 +366,22 @@ function walkReturnTypeOf(graph, fileGraph, type, argMap, guard) {
340
366
  kind: "DynamicImport",
341
367
  specifier: lazyShape.specifier,
342
368
  projection: lazyShape.projection,
369
+ originFile: lazyShape.originFile,
343
370
  };
344
- const inner = walkWithFolding(graph, fileGraph, syntheticDI, argMap, guard);
371
+ const guardKey = `import()::${syntheticDI.specifier}::${syntheticDI.projection.join(".")}`;
372
+ if (guard.push(syntheticDI.originFile, guardKey) !== "ok") {
373
+ return [{ type: { kind: "Unknown" }, viaTrail: [], identity: null }];
374
+ }
375
+ let inner;
376
+ try {
377
+ const target = resolveDynamicImportTarget(graph, fileGraph, syntheticDI, guard);
378
+ inner = target
379
+ ? walkWithFolding(graph, target.targetFileGraph, target.value, argMap, guard)
380
+ : walkWithFolding(graph, fileGraph, syntheticDI, argMap, guard);
381
+ }
382
+ finally {
383
+ guard.pop(syntheticDI.originFile, guardKey);
384
+ }
345
385
  const wrapperCallee = deriveHocCallee(type.callee);
346
386
  const importedName = lazyShape.projection[0] ?? "default";
347
387
  const via = {
@@ -350,30 +390,9 @@ function walkReturnTypeOf(graph, fileGraph, type, argMap, guard) {
350
390
  specifier: lazyShape.specifier,
351
391
  import: importedName,
352
392
  };
353
- // OVERRIDE identity with the DynamicImport-derived identity. The inner
354
- // walk resolved algebraically through the DynamicImport but its
355
- // identity field defaults to null (resolveType doesn't track which
356
- // module the JSX came from); we know the source from lazyShape.
357
- // Canonicalise via moduleResolver so `lazy(() => import('./x'))` and
358
- // `lazy(() => import('../x'))` from different callers collapse (#109);
359
- // promote to local when the chain resolves into the graph (#101).
360
- // Workspace-sibling bare-package lazy targets promote to local too (#94
361
- // parity with deriveIdentity's DynamicImport branch, #327 — validated
362
- // against a real monorepo where `next/dynamic` member imports split
363
- // identity: a bare-package specifier that resolves to an in-graph file
364
- // via the workspace-exports resolver was falling through to `imported`
365
- // because this chain never called `workspaceSiblingIdentity`).
366
- const resolvedFile = resolveImportedFile(graph, fileGraph, lazyShape.specifier, importedName);
367
- const workspaceLocal = workspaceSiblingIdentity(graph, lazyShape.specifier, importedName, resolvedFile);
368
- const relativeLocal = relativeChainLocalIdentity(graph, fileGraph, lazyShape.specifier, importedName);
369
- const identity = workspaceLocal ??
370
- relativeLocal ??
371
- memberLocalIdentity(graph, fileGraph, lazyShape.specifier, importedName) ?? {
372
- kind: "imported",
373
- specifier: lazyShape.specifier,
374
- imported: importedName,
375
- resolvedFile,
376
- };
393
+ // A terminal keeps the identity its own fold produced; the import-derived
394
+ // identity covers terminals that carry none.
395
+ const identity = dynamicImportIdentity(graph, fileGraph, syntheticDI);
377
396
  const innerHasNonUnknown = inner.some((t) => t.type.kind !== "Unknown");
378
397
  if (innerHasNonUnknown) {
379
398
  return inner
@@ -381,14 +400,17 @@ function walkReturnTypeOf(graph, fileGraph, type, argMap, guard) {
381
400
  .map((t) => ({
382
401
  type: t.type,
383
402
  viaTrail: [via, ...t.viaTrail],
384
- identity,
403
+ identity: t.identity ?? identity,
385
404
  }));
386
405
  }
387
- // External package: target not in graph. We still have a valid specifier+projection
388
- // from lazyShape synthesize a JSX terminal rather than falling through to Unknown.
406
+ // External package: target not in graph. Also reached when the inner walk
407
+ // was cut by the import cycle key above or by the depth budget, in which
408
+ // case the import-derived identity is the answer. We still have a valid
409
+ // specifier+projection from lazyShape — synthesize a JSX terminal rather
410
+ // than falling through to Unknown.
389
411
  return [{ type: { kind: "JSX" }, viaTrail: [via], identity }];
390
412
  }
391
- // (2) Dispatcher detection (#136): if the callee resolves to a Function
413
+ // (2) Dispatcher detection: if the callee resolves to a Function
392
414
  // whose body returns a dynamic-map access (`return MAP[k]`), walk that body
393
415
  // via walkWithFolding in the function's authoring fileGraph. This lets the
394
416
  // walkDynamicMember rule fire and surfaces the dynamic-map via in the chain
@@ -397,7 +419,7 @@ function walkReturnTypeOf(graph, fileGraph, type, argMap, guard) {
397
419
  const dispatcherResult = walkDispatcherReturns(graph, fileGraph, type, argMap, guard);
398
420
  if (dispatcherResult)
399
421
  return dispatcherResult;
400
- // (2b) Structural pass-through (spec D9 arm 1; #328). When every Function
422
+ // (2b) Structural pass-through. When every Function
401
423
  // the callee resolves to returns its own parameter (a bare `ParameterOf`, or a `TypeOf` that resolves to one) bound by THIS call,
402
424
  // the wrapper is identity-preserving by construction — `c => c`; React's
403
425
  // memo/forwardRef reach this arm through a library stub table, regardless
@@ -417,17 +439,17 @@ function walkReturnTypeOf(graph, fileGraph, type, argMap, guard) {
417
439
  // (3) Pure algebra walk. Per-leaf identity uses each terminal's source
418
440
  // (the leaf at the innermost fanout fork that produced it) so dynamic-map /
419
441
  // Union / Array fanouts attribute each branch to its own binding instead
420
- // of collapsing every leaf to the outer wrapper's identity (#131).
442
+ // of collapsing every leaf to the outer wrapper's identity.
421
443
  // foldTerminals also converts unreducible identity-bearing leaves into
422
444
  // synthesized JSX terminals, so a resolvable and an unresolvable imported
423
- // leaf take the same emission path (#274).
445
+ // leaf take the same emission path.
424
446
  const algResult = foldTerminals(graph, fileGraph, resolveType(graph, fileGraph, type, argMap, guard), type);
425
447
  const algHasNonUnknown = algResult.some((r) => r.type.kind !== "Unknown");
426
448
  if (algHasNonUnknown) {
427
449
  // (3b) Pass-through HOC recovery. When the algebra reduces this call to the
428
450
  // identity of its wrapped last arg (an identity-preserving `c => c` wrapper
429
- // — withRouter/connect stubs, and now direct external-import calls the leaf
430
- // synthesis at #274 lets the algebra see through), the reduction flattened
451
+ // — withRouter/connect stubs, plus direct external-import calls the leaf
452
+ // synthesis lets the algebra see through), the reduction flattened
431
453
  // the wrapper breadcrumb away. Recover it so a seen-through wrapper and an
432
454
  // opaque one (step 4) attribute composition identically: via presence
433
455
  // tracks the wrapper, not the algebra's reach. A wrapper whose body returns
@@ -451,14 +473,13 @@ function walkReturnTypeOf(graph, fileGraph, type, argMap, guard) {
451
473
  return [{ type: { kind: "Unknown" }, viaTrail: [], identity: null }];
452
474
  }
453
475
  const inner = walkWithFolding(graph, fileGraph, lastArg, argMap, guard);
454
- // Ruling 19: a data-method render call (`items.map(render)`) is not a
476
+ // A data-method render call (`items.map(render)`) is not a
455
477
  // wrapper — its result IS the callback's rendered JSX, not a component
456
478
  // received as a prop. Return the callback's terminals AS-IS: no
457
479
  // `hoc-wrapper` hop, and identity stays null so the engine's own fallback
458
480
  // (the declaration being walked, e.g. `List`) wins rather than whatever
459
481
  // the callback itself happened to resolve to. Import-backed MemberOf
460
- // callees (`Sentry.withProfiler`) fall through to the existing fold+hop
461
- // below — arm 3 (D9) stands for those.
482
+ // callees (`Sentry.withProfiler`) fall through to the fold+hop below.
462
483
  if (isDataMethodRenderCall(type, graph, fileGraph, guard)) {
463
484
  const nonUnknown = inner.filter((t) => t.type.kind !== "Unknown");
464
485
  if (nonUnknown.length > 0) {
@@ -475,7 +496,7 @@ function walkReturnTypeOf(graph, fileGraph, type, argMap, guard) {
475
496
  * a `TypeOf` resolved one hop) to a `ParameterOf` whose `fn` is the ref this
476
497
  * call binds args to (`bindingRef`), and every return names the same index.
477
498
  * Unions, JSX, nested Functions or an unresolvable callee all yield null so the
478
- * existing algebra / opaque-HOC paths handle them exactly as before.
499
+ * algebra / opaque-HOC paths handle them instead.
479
500
  */
480
501
  function passThroughParameterIndex(graph, fileGraph, type, argMap, guard) {
481
502
  const calleeFns = resolveToFunctions(graph, fileGraph, type.callee, argMap, guard);
@@ -504,7 +525,7 @@ function passThroughParameterIndex(graph, fileGraph, type, argMap, guard) {
504
525
  return index;
505
526
  }
506
527
  /**
507
- * Cross-module dispatcher walk (#136). When the callee of a ReturnTypeOf
528
+ * Cross-module dispatcher walk. When the callee of a ReturnTypeOf
508
529
  * resolves to a Function whose body returns a `MemberOf-DYNAMIC` access
509
530
  * (the canonical `function getMapped(k) { return MAP[k]; }` shape), the
510
531
  * dynamic-map fanout lives across a module boundary from the call site.
@@ -556,13 +577,19 @@ function findLazyImportArg(args, fileGraph) {
556
577
  for (const arg of args) {
557
578
  const di = findFirstDynamicImport(arg, fileGraph, /*depthBudget=*/ 1);
558
579
  if (di)
559
- return { arg, specifier: di.specifier, projection: di.projection };
580
+ return di;
560
581
  }
561
582
  return null;
562
583
  }
584
+ const PROMISE_METHODS = new Set(["then", "catch", "finally"]);
585
+ /** The crediting finder: narrow by design, and the only one that may claim a
586
+ * projection. `firstDynamicImport` (`engine/component-shape.ts`) is the
587
+ * reporting inverse — it descends everything and names a specifier only for
588
+ * the diagnostic. */
563
589
  function findFirstDynamicImport(t, fileGraph, depthBudget) {
564
- if (t.kind === "DynamicImport")
565
- return { specifier: t.specifier, projection: t.projection };
590
+ if (t.kind === "DynamicImport") {
591
+ return { specifier: t.specifier, projection: t.projection, originFile: t.originFile };
592
+ }
566
593
  if (t.kind === "Function") {
567
594
  for (const r of t.returns) {
568
595
  const hit = findFirstDynamicImport(r, fileGraph, depthBudget);
@@ -571,14 +598,21 @@ function findFirstDynamicImport(t, fileGraph, depthBudget) {
571
598
  }
572
599
  return null;
573
600
  }
601
+ if (t.kind === "ReturnTypeOf" && t.callee.kind === "MemberOf" && PROMISE_METHODS.has(t.callee.member)) {
602
+ // `catch`/`finally` keep the import's value; a `then` the parser did not
603
+ // collapse cannot name an export, so the import stays unclaimed.
604
+ return t.callee.member === "then" ? null : findFirstDynamicImport(t.callee.obj, fileGraph, depthBudget);
605
+ }
574
606
  if (t.kind === "MemberOf") {
607
+ if (PROMISE_METHODS.has(t.member))
608
+ return null;
575
609
  // Descend into obj; if the inner result is a bare namespace DynamicImport
576
610
  // (projection=[]), append this member as the named-export projection so
577
611
  // that the async-await form `m.X` where `m = DynamicImport("./foo", [])`
578
612
  // produces projection:["X"] instead of projection:[].
579
613
  const inner = findFirstDynamicImport(t.obj, fileGraph, depthBudget);
580
614
  if (inner && t.member !== "" && inner.projection.length === 0) {
581
- return { specifier: inner.specifier, projection: [t.member] };
615
+ return { specifier: inner.specifier, projection: [t.member], originFile: inner.originFile };
582
616
  }
583
617
  return inner;
584
618
  }
@@ -690,19 +724,19 @@ function resolveAnonymousLastArg(lastArg, fileGraph) {
690
724
  let cur = lastArg;
691
725
  while (cur.kind === "ReturnTypeOf")
692
726
  cur = cur.callee;
693
- // (1) Leaf TypeOf — surface the bound symbol directly (preserves the
694
- // historical hocCallee-adjacent behaviour for chains like `withA(withB(X))`,
695
- // where the inner-call's callee name is the most descriptive via.import).
727
+ // (1) Leaf TypeOf — surface the bound symbol directly: for chains like
728
+ // `withA(withB(X))` the inner call's callee name is the most descriptive
729
+ // via.import.
696
730
  if (cur.kind === "TypeOf") {
697
731
  return { specifier: fileGraph.filePath, import: cur.ref.symbol };
698
732
  }
699
733
  // (2) Function tagged at parser-emit time — works across file boundaries
700
- // because the annotation travels with the value (#130).
734
+ // because the annotation travels with the value.
701
735
  if (cur.kind === "Function" && cur.enclosingBinding) {
702
736
  return { specifier: cur.enclosingBinding.file, import: cur.enclosingBinding.symbol };
703
737
  }
704
738
  // (3) Same-file reference-equality scan — find the declaration whose
705
- // value tree contains lastArg (#122 same-file fallback).
739
+ // value tree contains lastArg (same-file fallback).
706
740
  for (const decl of fileGraph.declarations.values()) {
707
741
  if (decl && typeContainsRef(decl.value, lastArg)) {
708
742
  return { specifier: fileGraph.filePath, import: decl.symbol };
@@ -749,9 +783,9 @@ function typeContainsRef(haystack, needle) {
749
783
  *
750
784
  * When `imported` is provided, follows re-export chains (both
751
785
  * `{kind: "named", from, fromImported}` and `{kind: "star", from}`) to the
752
- * canonical source file via `followReExportChain` which gained star support
753
- * in #158 with first-wins priority. Mirrors what
754
- * `resolveExport` in resolve-reference.ts does for value-flow purposes,
786
+ * canonical source file via `followReExportChain` (star re-exports are
787
+ * first-wins). Mirrors what `resolveExport` in resolve-reference.ts
788
+ * does for value-flow purposes,
755
789
  * applied here at identity-stamping time. So `import { X } from './barrel'`
756
790
  * where `./barrel` re-exports X from `./source` attributes the component's
757
791
  * identity to `./source.tsx`, not `./barrel.tsx`.
@@ -759,7 +793,7 @@ function typeContainsRef(haystack, needle) {
759
793
  * Used by `deriveIdentity` so `TerminalIdentity.resolvedFile` carries the
760
794
  * canonical repo-relative path. Downstream `componentIdFromIdentity` uses
761
795
  * that canonical path so `'./page'` and `'../page'` from different callers
762
- * collapse to the same identity (#109 — Omlet's ModuleResolver shape).
796
+ * collapse to the same identity.
763
797
  */
764
798
  function resolveImportedFile(graph, fileGraph, specifier, imported) {
765
799
  const abs = graph.moduleResolver(fileGraph.filePath, specifier);
@@ -776,9 +810,9 @@ function resolveImportedFile(graph, fileGraph, specifier, imported) {
776
810
  * the exported name, promote the identity to `kind: "local"` directly. This
777
811
  * matches what the local-index seeds emit (keyed on `{filePath, exportName}`)
778
812
  * so the engine occurrence hashes to the same componentId as the seed —
779
- * preventing the barrel-vs-source phantom split (#101).
813
+ * preventing the barrel-vs-source phantom split.
780
814
  *
781
- * Symmetric with `workspaceSiblingIdentity` (#94) but for relative imports.
815
+ * Symmetric with `workspaceSiblingIdentity` but for relative imports.
782
816
  */
783
817
  function relativeChainLocalIdentity(graph, fileGraph, specifier, imported) {
784
818
  if (!graph)
@@ -797,7 +831,7 @@ function relativeChainLocalIdentity(graph, fileGraph, specifier, imported) {
797
831
  return { kind: "local", filePath: chained.file, export: chained.localExport };
798
832
  }
799
833
  /**
800
- * Workspace-sibling detection (#94). A bare-package import (`@org/foo`) that
834
+ * Workspace-sibling detection. A bare-package import (`@org/foo`) that
801
835
  * resolves to a file inside the parsed graph is a workspace sibling — the
802
836
  * package lives in this repo as a sibling workspace, not as a node_modules
803
837
  * dependency. Treat it as a local identity so JSX edges land on the actual
@@ -832,7 +866,7 @@ function workspaceSiblingIdentity(graph, specifier, imported, resolvedFile) {
832
866
  return { kind: "local", filePath: resolvedFile, export: exportName };
833
867
  }
834
868
  /**
835
- * Membership fallback for composition-path identity (#327). When a specifier
869
+ * Membership fallback for composition-path identity. When a specifier
836
870
  * resolves to an absolute target that is NOT in the parsed graph (outside
837
871
  * the scan globs, or reached through an unrealpathed node_modules symlink)
838
872
  * but IS owned by a workspace member, promote to a LOCAL identity pinned via
@@ -857,7 +891,7 @@ function memberLocalIdentity(graph, fileGraph, specifier, imported) {
857
891
  return null;
858
892
  return { kind: "local", filePath: member.absFile, export: member.exportName };
859
893
  }
860
- /** Append a residual member chain (#518) to a derived identity's export name. */
894
+ /** Append a residual member chain to a derived identity's export name. */
861
895
  function withResidual(identity, residual) {
862
896
  if (identity === null || residual.length === 0)
863
897
  return identity;
@@ -876,42 +910,22 @@ function withResidual(identity, residual) {
876
910
  * that match the ref actually live.
877
911
  */
878
912
  export function deriveIdentity(type, fileGraph, graph) {
879
- // A ReturnTypeOf never derives identity from its callee (spec D9 arm 2):
913
+ // A ReturnTypeOf never derives identity from its callee:
880
914
  // a resolvable factory call's identity is the declaration holding the call,
881
915
  // which every engine push site already supplies as its fallback. Returning
882
- // null here is what makes that fallback win. (#271, #328)
916
+ // null here is what makes that fallback win.
883
917
  const cur = type;
884
918
  if (cur.kind === "DynamicImport") {
885
- const importedName = cur.projection[0] ?? "default";
886
- const resolvedFile = graph ? resolveImportedFile(graph, fileGraph, cur.specifier, importedName) : null;
887
- const workspaceLocal = workspaceSiblingIdentity(graph, cur.specifier, importedName, resolvedFile);
888
- if (workspaceLocal)
889
- return workspaceLocal;
890
- // Re-export-chain-followed relative imports promote to local (#101).
891
- const relativeLocal = relativeChainLocalIdentity(graph, fileGraph, cur.specifier, importedName);
892
- if (relativeLocal)
893
- return relativeLocal;
894
- const memberLocal = memberLocalIdentity(graph, fileGraph, cur.specifier, importedName);
895
- if (memberLocal)
896
- return memberLocal;
897
- return {
898
- kind: "imported",
899
- specifier: cur.specifier,
900
- imported: importedName,
901
- // Canonicalise via moduleResolver — `lazy(() => import('./x'))` and
902
- // `lazy(() => import('../x'))` from different callers collapse to one
903
- // identity (#109).
904
- resolvedFile,
905
- };
919
+ return dynamicImportIdentity(graph, fileGraph, cur);
906
920
  }
907
921
  if (cur.kind === "TypeOf") {
908
922
  const symbol = cur.ref.symbol;
909
923
  const lookup = graph ? fileGraphForRef(graph, cur.ref, fileGraph) : fileGraph;
910
924
  const imp = lookup.importsByLocal.get(symbol);
911
925
  if (imp) {
912
- // Namespace-import members canonicalise to the member name (#394),
913
- // matching the direct-import path's `effectiveImported` (engine/index.ts,
914
- // #362) so both paths mint the same identity. The residual chain (#518)
926
+ // Namespace-import members canonicalise to the member name,
927
+ // matching the direct-import path's `effectiveImported`
928
+ // (engine/index.ts) so both paths mint the same identity. The residual chain
915
929
  // is appended AFTER every root lookup, so `Dialog.Popup` on an unparsed
916
930
  // package and `NS.Inline` on a workspace member both mirror the direct
917
931
  // path.
@@ -936,7 +950,7 @@ export function deriveIdentity(type, fileGraph, graph) {
936
950
  }
937
951
  // Check local declarations. A same-file symbol is never a namespace
938
952
  // import, so the whole member chain is residual (mirror of
939
- // `pushLocalOccurrence`'s fallback, #518) — an empty chain is a no-op.
953
+ // `pushLocalOccurrence`'s fallback) — an empty chain is a no-op.
940
954
  for (const [, decl] of lookup.declarations) {
941
955
  if (decl && decl.symbol === symbol) {
942
956
  return withResidual({