---
status: historical
created: 2026-07-07
archived: 2026-08-25
reason: "Executed handoff; the CT-1868/1869/1870 authored-lineage arc shipped."
superseded-by: docs/specs/ts-transformer/ts_transformers_current_behavior_spec.md
---
# A′ Master Handoff — transform-time authored source locations (2026-07-07)
**Read this first.** It is the canonical carry-forward for the **A′ work-line**
(inject the _authored_ source location at transform time so debug `fn.src` is
correct and free, then delete the eager runtime resolution) and its
prerequisite, the **transformer lineage fix**. Every claim in §2–§5 was
re-derived on `main` @ `39afdb62b` (2026-07-07) with the probe rig in §3 — line
numbers will drift; the rig is how you re-pin them. Stage ordinals are
deliberately not used: the registry (`CFC_TRANSFORMER_STAGE_SPECS`,
`src/cf-pipeline.ts`) is canonical and grows; the load-bearing ordering facts
here are relational — LiftLowering → closure strategies → expression-site
lowerings → SchemaInjection → BuilderCallHoisting, the last two adjacent. This
doc supersedes the memory file `project_aprime_lineage_workline.md` (retired)
and the workspace seed `APRIME-LINEAGE-SEED.md` (L-cf-repos, 2026-07-06).
Related merged context: #4458 (lazy/debug-only `fn.src`; production skips
per-primitive resolution; identity re-rooted on content-addressed
`{identity, symbol}` — see `cfc/implementation-identity.ts`), #4436 (identity
re-root). Sibling, NOT part of A′: CT-1819 lazy source-map compose (PR #4560,
merged 2026-07-09) — do not merge scopes.
---
## 0. TL;DR
- **A′ is a debug-correctness line, not a boot-floor line.** The historical
runtime resolver captured one stack and walked source maps per primitive; its
cheap successes could still point at a colliding runtime-factory frame. The
current design records authored sites once in a compiler-generated sidecar and
serves `.src` from a debug-only runtime map.
- **The prerequisite**: transform-time injection needs the transformer pipeline
to know, at the hoisting stage, where each builder call/callback was authored.
Probe-verified on current main: **it doesn't** — all hoisted builder origins
arrive at `BuilderCallHoistingTransformer` with `pos=-1`, no `sourceMapRange`,
no original chain.
- **The law behind the breakage** (cleaner than the inherited three-site
diagnosis): TS `factory.update*` preserves textRange + original-chain
automatically; `factory.create*` yields bare nodes. The pattern-body path uses
`update*`/`preserveNodeSourceMap` throughout and stays healthy (a
probe-observed 6-deep original chain still roots at the authored arrow); the
closure strategies and SchemaInjection use `create*` **with the anchor node in
scope and discarded** at every site. SchemaInjection is a second, independent
strip layer the inherited diagnosis missed: a fix at the strategy sites alone
still arrives at the hoisting stage with nothing.
- **Scope decision: narrow-first** — fix the enumerated builder-path sites (§5)
with a shared `preserveLineage` helper (§6); price "broad" (every replacement
node pipeline-wide) from the sweep in §7 and ticket it separately unless it
turns out nearly free. Decision rationale in §7.
## 1. Value proposition (what A′ buys once lineage is fixed)
1. Debug-time `fn.src` becomes _truthful_ (today's cheap path returns colliding
runtime-factory frames; `INTERNAL_SOURCE_LOCATION_FRAME_PATTERNS` filters
`factory.ts` but not `module.ts`; eval-frames mostly don't canonicalize —
CT-1754 machinery).
2. Kills the dev-build eager resolution cost entirely (annotation becomes a
transform-time constant read).
3. Unblocks seefeld's original directive — "remove the expensive fallback and
error instead" — which was always gated on the cheap path being correct.
4. Carries the empty-`fn.name` debug-name gap (#4458 follow-on): hoisted
`__cfLift_N` names replaced authored names in debug output. The original
chain reaches the authored _node_, not just a position, so the authored
binding name is recoverable at the same injection point.
## 2. Probe findings on main @ 39afdb62b (re-derived 2026-07-07)
Fixture: five authored origins (see §3). What arrives at the hoisting stage
(BuilderCallHoisting), per probe:
| Authored origin | At the hoisting stage | Lineage dies at |
| -------------------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `computed(() => …)` (×2) | hoisted `__cfLift_1/2`: NOTHING | **LiftAppliedStrategy**. Probe in/out: enters with FULL lineage (the LiftLowering stage preserves the triple), exits bare. |
| inline JSX expr `{count * 3}` | hoisted `__cfLift_3`: NOTHING | Born mostly bare in the **expression-rewrite emitters**; the inner call _is_ born with smr via `createHelperCall`, which **SchemaInjection** then strips. |
| `.map((item) =>
…)` in JSX | hoisted `__cfPattern_1`: original chains exist but root in synthetics | **ArrayMethodStrategy**. Probe in/out: enters as the authored node, exits bare. |
| module-scope `handler(…)` (in-place) | call: NOTHING; **callback: authored pos survives** (30:60) | Call wrapper rebuilt by **SchemaInjection**'s handler schema-prepend (`schema-injection.ts:3190` family — the prepended two-schema shape matches the emitted output); callback never rebuilt. |
| `export default pattern(…)` (in-place) | call: depth-1 chain to synthetic; **callback: authored pos + 6-deep chain** | Callback preserved correctly by the pattern-body path (precedent, §6); call wrapper stripped like the handler's. |
Key mechanism notes:
- `LiftLoweringTransformer`'s `finalizeLoweredCall` (`lift/transformer.ts`) does
the full triple — `setTextRange` + `setSourceMapRange` + `setOriginalNode` —
and the probe validates the instrument against it (lineage there is true _by
construction_; if the probe reports nulls there, the probe is broken).
- `CfHelpers.createHelperCall` smr-preserves everything it builds (anchored to
its `originalNode` arg). smr lives on `emitNode`; a later `create*` rebuild
silently drops it — which is exactly what SchemaInjection did. **Correction
(2026-07-08, verified against TS 5.9.2 source during CT-1868; re-verified
unchanged on TS 6.0.3 at the 2026-07 rebase — same
`setOriginalNode`/`mergeEmitNode` shape):** smr is NOT lost through
`update*`/`setOriginalNode` — `setOriginalNode` merges the original's
`emitNode` (including `sourceMapRange`) onto the new node (`typescript.js`
`setOriginalNode`/`mergeEmitNode`). So smr dies only at unanchored `create*`
sites; it rides every `update*` rebuild for free. This refuted the original N8
rationale (§5) and is why smr became the shipped lineage carrier (§6a).
- The hoisting stage itself is lineage-neutral (it relocates the inner call node
and `update*`s the site) and already uses `setOriginalNode` for
checker-identity (`__cfLift_N` identifier → inner call, so
`detectCallKind`/`resolveBuilderExpressionKind` classify the applied hoist).
Inherited-diagnosis scorecard (Opus line, 2026-07-02, secondhand): all three
cited rebuild sites confirmed live (the pattern-builder citation's
`setParentPointers` detail was seed-compression garble — the real point, its two
`createArrowFunction`/`createFunctionExpression` returns, is right and they are
the chokepoint). Missed: the SchemaInjection second layer; the in-place-callback
survival; the update*-vs-create* law.
## 3. The probe rig (re-run recipe)
The instrumentation is temporary and NOT left in the tree; this section is the
complete recipe. (The fix PR should graduate it into a proper regression test —
a hoisting-stage assertion fixture; see §8.)
1. **Fixture** — `packages/patterns/aprime-lineage-probe-fixture.tsx`
(untracked), one authored builder per origin path:
```tsx
import {
computed,
Default,
handler,
NAME,
pattern,
UI,
Writable,
} from "commonfabric";
interface ProbeInput {
count: number | Default<0>;
label: string | Default<"probe">;
items: string[] | Default<[]>;
}
// ORIGIN-C: authored handler (module scope, applied in JSX)
const bump = handler }>((_, state) => {
state.count.set(state.count.get() + 1);
});
export default pattern(({ count, label, items }) => {
// ORIGIN-A: authored computed with capture
const doubled = computed(() => count * 2);
return {
// ORIGIN-N: authored computed template
[NAME]: computed(() => `probe ${label}`),
[UI]: (
{count * 3}
{/* ORIGIN-B: inline reactive expr */}
{doubled}
{items.map((item) => - {item}
)}
{/* ORIGIN-D */}
bump
),
count,
label,
items,
};
});
```
2. **Probe module** — `packages/ts-transformers/src/lineage-probe.ts`
(untracked), env-gated (`CF_LINEAGE_PROBE=1`), reporting per node: kind,
`pos..end`, explicit smr (detect via `ts.getSourceMapRange(n) !== n` — the
API returns the node itself when unset), original-chain depth + terminal, and
`recovered` = what the best-available position points at in authored text
(line:col + snippet). **Content is the ground truth**: a `pos >= 0` pointing
at the wrong text is still broken lineage.
3. **Insertion points** (all one-liners plus imports):
- `transformers/builder-call-hoisting.ts` — in the visit loop right after a
`HOISTABLE_BUILDERS` spec resolves (probe visited/inner/callback, tagged
`hoist:`); in the statement loop for in-place authored builder consts
(inside `collectTopLevelBuilderArtifactNames`'s builder branch, tagged
`authored:`) and for `export default ` (tagged
`authored:export-default`).
- `lift/transformer.ts` `finalizeLoweredCall`, after the triple-set — the
**instrument-validation** probe (`lower:computed`).
- `closures/transformer.ts` strategy dispatch, when `transformed !== node` —
in/out pairs tagged `closure:` (this is what pins stage-8
kill sites).
4. **Run**:
```bash
CF_LINEAGE_PROBE=1 deno task cf check \
packages/patterns/aprime-lineage-probe-fixture.tsx --no-run 2>&1 \
| grep lineage-probe
```
Validate the instrument first: the `lower:computed` lines MUST show `pos>=0`,
explicit smr, `origDepth=1`, `recovered=self→`. Only then
trust downstream `NONE`s. Emitted-output inspection idiom, as ever:
`--show-transformed --no-run | deno task cf view`.
## 4. Acceptance criterion for the lineage fix
Re-run the §3 probe: every `hoist:*` and `authored:*` line — outer/inner call
AND callback — reports `recovered=` pointing at the correct authored snippet
(the origin markers make correctness eyeball-able). Plus the standard full gate
(§8).
## 5. The narrow fix set (all sites probe- or read-verified on 39afdb62b)
Fix-shape column shows **what CT-1868 actually shipped** (2026-07-08) — the
original plan (full `preserveLineage` everywhere + `create→update*` in
SchemaInjection) was revised by the emit-invariance gate and the fixture suite,
which proved textRange/original are observable channels at most sites; §6a has
the shipped channel rules. Anchor = the authored-or-predecessor node, verified
in scope at each site.
| # | Site | Materializes | Anchor in scope | Fix shape |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| N1 | `closures/utils/pattern-builder.ts:170` (`buildCallback`) and `:316` (`buildHandlerCallback`) | every rebuilt closure callback (computed-origin, array-method, inline-handler — all strategies route through here) | `originalCallback` param | `preserveSourceMapRange` (smr-only; the full triple flips arrow-head printing and feeds schema derivation — §6a) |
| N2 | `closures/strategies/lift-applied-strategy.ts:587` | outer lift-applied call | `inputCall` | `preserveLineage` |
| N3 | `transformers/expression-rewrite/rewrite-helpers.ts:160` / `:177` | wrapper arrow + outer applied call | `expression` | `preserveSourceMapRange` (a wrapper is NOT the wrapped expression's semantic predecessor — claiming its identity trips the compute-owned marker invariant; §6a) |
| N4 | `transformers/builtins/lift-applied.ts:224` / `:270` | wrapper arrow + outer applied call | `expression` | `preserveSourceMapRange` (a wrapper is NOT the wrapped expression's semantic predecessor — claiming its identity trips the compute-owned marker invariant; §6a) |
| N5 | `closures/strategies/array-method-transform.ts:297` | `mapWithPattern` call | `methodCall` | `preserveLineage` |
| N6 | `closures/utils/capture-scaffold.ts:54` | outer applied handler call (inline-closure handlers) | `originalNode` | `preserveSourceMapRange` (survives to the printer at its JSX site — a real textRange reflows ternary/JSX layout; sweep-found, spot-verified) |
| N7 | `transformers/schema-injection.ts` builder-call rebuild family: `:1489`, `:2379–:2410` (self-verified) + `:2728`, `:3190`, `:3289`, `:3520`, `:3729`, `:3818`, `:3895`, `:3985`, `:4080` and cluster `:985`, `:3782` (sweep-enumerated; `:2728`/`:3190` spot-verified — re-verify each at implementation) | every builder call SchemaInjection rebuilds to splice/prepend/append schemas — the immediate predecessor of the hoisting stage, so this family alone strips whatever survives stages 8–12 | `node` / `innerLiftCall` | **`create*` + `preserveSourceMapRange`** — NOT the originally-planned `update*`: its "free" textRange/original are precisely the observable channels (kitchensink JSX reflow; typeRegistry resurfacing via `getTypeAtLocationWithFallback` made module-scope-cf-data wrap a hoisted lift). Shipped as **17** smr-carry sites; anchors: inner rebuilds → `innerLiftCall`, outers/direct → `node`, asScope callee → `node.expression`. Classification note: "builder call" here is the `detectCallKind`-builder sense — the set deliberately includes non-hoistable factory/API builders (reactive conditionals, cell factories/`.asSchema`, `wish`, `generateObject`, `sqliteQuery`, `fetchJson`), because the hoisting stage also visits in-place authored builder consts for `__cfReg` registration and A′ needs their `src` equally. The regression test location-pins the hoistable families + the in-place handler; the factory shapes ride the same §6a wrapper without dedicated location tests — add those alongside CT-1870's injection acceptance |
| N8 (**DROPPED**) | `core/cf-helpers.ts` `createHelperCall` | inner helper calls | already smr-anchored | Implemented, then reverted: the premise (smr doesn't ride `update*`) was refuted at TS source level (§2 correction), so N8 added no recovery; and helper-call original chains are exactly the §6 identity hazard. `createHelperCall` at HEAD already suffices |
"Narrow" here means **builder-path-complete**: every site that materializes or
rebuilds what `BuilderCallHoistingTransformer` consumes (builder calls, inner
calls, callbacks — hoisted AND in-place). Shipped as ~24 edit sites; full
`preserveLineage` survives only at N2/N5 (gate-proven inert — N2's channels are
consumed and re-stripped by SchemaInjection before printing; N5's textRange is
why the mapWithPattern outer recovers `self→`), smr-carry everywhere else.
Acceptance verified per §4 (all 16 probe lines recover correct authored
snippets) and guarded by `test/lineage-regression.test.ts` (bite-verified:
reverting schema-injection.ts makes it fail).
Notes:
- N7's smr-carry future-proofs itself: sourceMapRange rides any later `update*`
rebuild automatically (`setOriginalNode` merges emitNode — §2 correction), so
downstream stages need no cooperation. This is what the originally-planned
`update*` switch was for, achieved without its observable textRange/original
channels.
- `setParentPointers` at the existing call sites is orthogonal (tree
navigability, not lineage) and stays as-is.
## 6. `preserveLineage` design (and why `preserveNodeSourceMap` stays)
```ts
// ORIGINAL design sketch (pre-implementation). Superseded on one point by
// the shipped rule (§6a): wrappers NEVER take preserveLineage — a wrapper
// arrow/call that reifies an authored expression is not its semantic
// predecessor and carries position via preserveSourceMapRange only. The
// shipped doc comments in ast/utils.ts are authoritative.
// ast/utils.ts (next to setParentPointers; export via ast/mod.ts)
/**
* Carry full lineage from `origin` onto a synthesized replacement/reification
* of it: textRange (pos/end), sourceMapRange, and the original-node chain.
* `origin` is the node this one replaces in the emitted tree (or reifies,
* e.g. a wrapper arrow whose body is the authored expression). `origin` may
* itself be synthetic mid-chain — the channels compose (original chains
* extend; getSourceMapRange propagates; a -1 textRange copies as a no-op
* while recovery still works through the chain).
*/
export function preserveLineage(
node: T,
origin: ts.Node,
): T {
return ts.setOriginalNode(
ts.setSourceMapRange(
ts.setTextRange(node, origin),
ts.getSourceMapRange(origin),
),
origin,
);
}
```
Precedents in-tree: the full triple at `lift/transformer.ts`
`finalizeLoweredCall` (probe-validated); the healthy pattern-body path
(`pattern-callback-lowering.ts` / `pattern-callback-transform.ts` /
`pattern-body-reactive-root-lowering.ts` — `update*` + `preserveNodeSourceMap`
throughout).
**Naming rationale — two helpers, two concepts, not weak-vs-strong:**
- `preserveLineage(node, origin)` = **provenance**: "in authored text I was born
there, as that node." For replacement-materialization sites, where the
provenance target and the checker-identity target are the same node.
- `preserveNodeSourceMap(node, originalNode, identityNode)` = the resolution
for sites where the two concerns **diverge**: a node has exactly one
`original` pointer, so when position should map to the authored expression but
identity must point elsewhere (e.g. `getHelperExpr` pointing a fresh
`__cfHelpers` identifier's original at the module's helper binding so the
checker resolves it), position goes via smr and original carries identity.
Same move as builder-call-hoisting's `setOriginalNode(name, innerCall)`. The
identity node is required: source-map-only helper names, property accesses,
and calls use `ast/utils.ts`'s
**`preserveSourceMapRange(node, origin)`** instead. The method therefore
represents only genuine identity/position divergence.
**Hazards (why this isn't a mechanical sweep):**
- `setTextRange` on synthetics feeds sourcemaps/diagnostics/`getText()`. Fine
when the anchor is the true semantic predecessor; subtly wrong spans when it
isn't. Anchor discipline is the mitigation.
- `setOriginalNode` is **not inert metadata**: existing code consults original
chains for semantic fallback (`lift-applied-strategy.ts:522` checks the
original's explicit return type; scope-analysis and the builder-kind resolver
walk originals). Giving rebuilt nodes real originals feeds those fallbacks new
— almost certainly more-correct — answers. Mostly a feature; reason for the
full gate per site and for broad needing per-site judgment.
### 6a. Shipped channel rules (CT-1868 implementation findings, 2026-07-08)
The hazards above were not theoretical — the emit-invariance gate and the
fixture suite rejected the full triple at most sites. What shipped:
**sourceMapRange is the lineage carrier; full lineage only where gate-proven
inert.** The mechanisms, each observed and root-caused during implementation
(full detail in the `preserveLineage`/`preserveSourceMapRange` doc comments,
`ast/utils.ts`):
- **`setTextRange` is an observable channel.** A real arrow `pos` alongside
`pos=-1` params flips the printer's `canEmitSimpleArrowHead` (`x =>` →
`(x) =>`); real positions on calls printed inside synthesized containers
reflow JSX/ternary line-breaks.
- **`setOriginalNode` is an observable channel.** Originals feed
`getTypeAtLocationWithFallback` (changes derived schemas: `asCell`, `items`,
`$defs`), resurface `typeRegistry` entries (module-scope-cf-data wrapped a
hoisted lift application it previously left bare), and hit the cross-stage
marker registries (`markSyntheticComputeOwnedSubtree` → `#hasWithOriginal`): a
WRAPPER whose original points into the subtree it wraps reads as compute-owned
and trips the array-method context invariant.
- **`sourceMapRange` is observable by nothing but sourcemaps** — not the
printer's text output, not schema/type resolution, not the markers — and it
rides `update*` rebuilds via `setOriginalNode`'s emitNode merge (§2
correction). Hence: the safe-everywhere carrier, sufficient for A′.
- **Wrapper-vs-replacement matters more than anticipated**: a node that WRAPS
authored content (wrapper arrow, applied wrapper call) is not that content's
semantic predecessor — position yes (smr), identity no. Only
same-semantic-unit replacements may take full `preserveLineage`, and only
after the gate proves the channels inert there (currently: N2, N5).
## 7. Narrow vs broad — decision and pricing
**Decision: narrow-first (the §5 set), then a source-map-range-only broad
sweep.** The narrow set and both broad slices are complete against direct probe
evidence. A wrong source-map anchor is worse than an absent one, so every broad
site is content-bound to its authored semantic predecessor.
**Sweep results (read-only Opus survey, 2026-07-07, on 39afdb62b; three claims
spot-verified exactly — treat the rest as a work list to re-verify at
implementation):**
- **Folded INTO narrow** (builder-path): the schema-injection builder-call
rebuild family (~11 sites, §5 N7) and `capture-scaffold.ts:54` (§5 N6). The
sweep's key structural point: SchemaInjection is the immediate predecessor of
BuilderCallHoisting and rebuilds essentially every builder call bare — it
alone re-strips anything stages 8–12 preserve, which is why the original
three-site diagnosis could never have been sufficient.
- **Straightforward broad slice:** `registerReplacement` carries ranges for its
pattern-body replacement family; UI-helper outer elements, JSX expression
containers, handler attributes and initializers, nested array receiver `key()`
calls, and module-scope `__cf_data` wrappers carry their authored container or
expression range. These sites use `preserveSourceMapRange` exclusively.
- **Judgment broad slice:** `DestructureBinding` retains the binding syntax for
each lowered leaf. A synthesized temporary root anchors to the authored
initializer; local leaf declarations and destructured-parameter prologue
declarations anchor to their binding elements. Assert-diagnostics callback
bodies anchor to the authored body, and each replacement record block and
`return` anchors to the concise expression or authored return it replaces.
`CFHelpers.preserveNodeSourceMap` requires a distinct identity node; its
former source-map-only uses call `preserveSourceMapRange` directly. A focused
review kept the `ifelse.ts` asymmetry: `ifElse` anchors to the whole
conditional, while `when` and `unless` anchor to the left condition.
- **Excluded correctly** (sweep audit list retained in the session record):
expression-rewrite emitters (delegate to known sites), reactive-variable-for
(already fully preserved), type-position construction, additive schema/
coverage/hardening/shadowing machinery, analysis-only stages.
**Final call:** builder-path lineage and the broad replacement sweep are
complete. Both broad slices follow the source-map-range-only safety rule.
End-to-end map probes place the destructuring temporary at its authored
initializer, each leaf at its binding element, a concise assert record return
at its expression, and block assert record returns at their authored `return`
statements. The fixture corpus remains byte-identical because no text range or
original-node identity is added.
The implementation proves the per-site judgment is not just "is the anchor's
span correct" but **"does textRange reach the printer here"** and **"does an
original reach a marker/typeRegistry consumer here."** Default to
`preserveSourceMapRange`; escalate to full `preserveLineage` only with the
emit-invariance check and the full fixture suite green.
## 8. Verification discipline for the fix PR
- Full gate before green: fmt + lint + check + the FULL ts-transformers suite
(not targeted tests) — transformer output is load-bearing for everything
downstream.
- The §3/§4 probe as acceptance; graduate it into a tracked regression test
(hoisting-stage fixture asserting recovered-authored positions for all five
origins) rather than leaving instrumentation ad hoc.
- Emit sanity: `--show-transformed` diff on a corpus pattern (e.g.
`age-category.tsx`) should be **byte-identical** pre/post fix — lineage
channels are emit-map metadata, not output text. Any text diff is a bug.
- Sourcemap spot-check in a dev shell (positions in devtools land on authored
lines for a rebuilt callback).
## 9. The A′ line proper — shipped shape
1. **Lineage fix lands** (§5–§8). Everything below reads positions through it.
2. **Transform-time injection design** (the next design conversation):
- _Where to inject_: at `BuilderCallHoistingTransformer`, which already
visits every hoisted builder AND every in-place/authored builder
(`collectTopLevelBuilderArtifactNames`) AND `export default pattern(…)`;
recovered authored position = the `recoverPosition` walk in
`test/lineage-regression.test.ts` (own range → explicit sourceMapRange →
original-chain terminal) — that function is the A′ prototype. Post-1868,
**smr is the load-bearing channel** (§6a): read positions via
`getSourceMapRange`, do NOT rely on original chains (dropped at most sites
by design), and note the authored binding name for the `fn.name` gap now
comes from position→source-text lookup, not from an original node.
- _What shape_: `BuilderSourceSitesV1`, a versioned per-module compiler
sidecar keyed by the runtime symbol (`__cfReg` name, export alias, or
`default`). It travels beside JavaScript and therefore changes neither
builder signatures nor verifier-visible module bytes.
- _What content_: file + line:col of the authored callback (and the authored
binding name — closes the empty-`fn.name` debug-name gap).
3. **Runtime read side**: `fn.src` and the authored `fn.name` lazy getters read
the sidecar-backed `authored-debug-source` map instead of eval-frame
resolution.
4. **Delete the dev-build eager resolution**: the former eager-source flag and
per-primitive stack/source-map walk are removed; source maps remain lazy for
thrown-error stack mapping.
5. **seefeld's directive**: remove the expensive fallback; error instead. Gated
on 2–3 making the cheap path correct.
6. **Boundaries**: CT-1819 / PR #4560 (lazy source-map compose) is a sibling —
don't re-tread composition here. `~/coding/L-cf-repos/ct1848-repro/` is
another line's preserved repro kit — don't touch.
## 10. Tickets & memory
Filed 2026-07-07:
- **[CT-1868](https://linear.app/common-tools/issue/CT-1868)** — the narrow
lineage fix: `preserveLineage` helper + the §5 builder-path-complete site set
(ticket title's "create→update in SchemaInjection" predates the shipped
`create*`+smr design — §6a). Acceptance = §4, verification = §8.
- **[CT-1869](https://linear.app/common-tools/issue/CT-1869)** — the broad
sweep, carrying the §7 work list. Related to CT-1868; sequenced after A′'s
injection design.
- **[CT-1870](https://linear.app/common-tools/issue/CT-1870)** — A′ proper
(§9.2–9.5 + the empty-`fn.name` gap). Blocked by CT-1868; related to CT-1819
(sibling boundary).
Memory: `project_aprime_lineage_workline.md` in the canonical store is retired
in favor of this doc once tickets exist (pointer line in MEMORY.md updated to
reference this file + ticket IDs).
## 11. Appendix — probe module source (for §3.2)
`packages/ts-transformers/src/lineage-probe.ts`, exactly as used for the §2
findings (env-gated; safe to leave imported only while probing):
```ts
import ts from "typescript";
const ENABLED = typeof Deno !== "undefined" &&
Deno.env.get("CF_LINEAGE_PROBE") === "1";
export function lineageProbeEnabled(): boolean {
return ENABLED;
}
function kindName(n: ts.Node): string {
return ts.SyntaxKind[n.kind] ?? `kind#${n.kind}`;
}
function originalChain(n: ts.Node): ts.Node[] {
const chain: ts.Node[] = [n];
let cur = n;
// deno-lint-ignore no-explicit-any
while ((cur as any).original && chain.length <= 16) {
// deno-lint-ignore no-explicit-any
cur = (cur as any).original as ts.Node;
chain.push(cur);
}
return chain;
}
function fmtPos(sf: ts.SourceFile, pos: number, end: number): string {
try {
const lc = sf.getLineAndCharacterOfPosition(pos);
const snippet = sf.text.slice(pos, Math.min(end, pos + 56)).trim()
.replace(/\s+/g, " ");
return `${lc.line + 1}:${lc.character + 1} «${snippet}»`;
} catch {
return `pos=${pos} (unmappable)`;
}
}
export function probeNode(
tag: string,
role: string,
node: ts.Node,
sf: ts.SourceFile,
): void {
if (!ENABLED) return;
const chain = originalChain(node);
const term = chain[chain.length - 1];
const smrRange = ts.getSourceMapRange(node);
const smrSet = (smrRange as unknown) !== (node as unknown);
const smr = smrSet ? `${smrRange.pos}..${smrRange.end}` : "none";
let recovered = "NONE";
if (node.pos >= 0) {
recovered = `self→${fmtPos(sf, node.pos, node.end)}`;
} else if (smrSet && smrRange.pos >= 0) {
recovered = `smr→${fmtPos(sf, smrRange.pos, smrRange.end)}`;
} else if (term !== node && term.pos >= 0) {
recovered = `orig→${fmtPos(sf, term.pos, term.end)}`;
}
console.error(
`[lineage-probe] ${tag} role=${role} kind=${kindName(node)} ` +
`pos=${node.pos}..${node.end} smr=${smr} origDepth=${chain.length - 1}` +
(chain.length > 1
? ` origTerm=${kindName(term)}@${term.pos}..${term.end}`
: "") +
` recovered=${recovered}`,
);
}
/** Probe a hoistable builder site: the visited (outer/applied) call, the inner
* call being relocated, and the inner call's function-valued argument. */
export function probeBuilderSite(
tag: string,
visited: ts.CallExpression,
innerCall: ts.CallExpression,
sf: ts.SourceFile,
): void {
if (!ENABLED) return;
probeNode(tag, "outer", visited, sf);
if (innerCall !== visited) probeNode(tag, "inner", innerCall, sf);
probeCallback(tag, innerCall, sf);
}
/** Probe an in-place (non-hoisted) authored builder call and its callback. */
export function probeAuthoredBuilder(
tag: string,
call: ts.CallExpression,
sf: ts.SourceFile,
): void {
if (!ENABLED) return;
probeNode(tag, "call", call, sf);
probeCallback(tag, call, sf);
}
/** Probe a rebuild boundary: the node a strategy received vs what it
* returned. Shows whether lineage existed on entry and survived on exit. */
export function probeRebuild(
tag: string,
before: ts.Node,
after: ts.Node,
sf: ts.SourceFile,
): void {
if (!ENABLED) return;
probeNode(tag, "in", before, sf);
probeNode(tag, "out", after, sf);
}
function probeCallback(
tag: string,
call: ts.CallExpression,
sf: ts.SourceFile,
): void {
const cb = call.arguments.find(
(a) => ts.isArrowFunction(a) || ts.isFunctionExpression(a),
);
if (cb) {
probeNode(tag, "callback", cb, sf);
} else {
console.error(`[lineage-probe] ${tag} role=callback MISSING(no fn arg)`);
}
}
```