# Resolution & enumeration completeness (#40 / #36b) — design

audience: AI coding agents first. Optimize for activation, not prose. Do not prettify into narrative.
SoT for this phase. Prior art (point, do NOT re-inline): `2026-06-17-band2-resolver-generalization-approaches.md`
(#36, the first-party-called-import convention), `2026-06-17-attack-surface-mapping-design.md` (#41, the file-routed
filesystem oracle), `2026-06-17-band2-delivery-shipped.md` (#36 deps-as-context delivery).

## Goal (BLUF)

Make cross-file scope REACH the first-party code it currently can't see, on real pnpm-workspace + barrel repos.
Two gaps, ONE thesis: **prefer a filesystem/convention fact over a content-regex guess.** A file-route is a
LOCATION; a workspace package entry is a `package.json` fact; a barrel re-export is a resolvable edge. Today the
gate guesses all three with regex and misses.

- **Part A (pilot, mapper.py):** enumerate file-routed kinds by the filesystem oracle (location), not the content
  signal. Closes the measured `edge-function` recall miss (0/2) AND its false-positives in ONE move.
- **Part B (gate.py):** auto-discover workspace-package aliases from `pnpm-workspace.yaml` + each `package.json`,
  and FOLLOW barrel re-exports + `exports` subpath maps to the real definition file.

**Headline metric = resolution COVERAGE, not a canonical** (see §Validation). On zync today, `@zync/db` (54 sites),
`@zync/auth` (113), `@zync/ui` (637) and ~17 more first-party packages ALL resolve to `None` → the gate's cross-file
scope is blind to first-party code on every pnpm-workspace repo. That reachability number is the win and needs no
new buggy cell to prove.

## The measured gaps (ground truth, not inferred)

### Gap A — file-routed enumeration uses content-regex, misses real routes

`multideal/functions/api/subscribe.{ts,js}` are CF Pages routes. By LOCATION the filesystem oracle counts them
(`FILE_ROUTE_RULES["edge-function"] = functions/` → 2 routes). The CONTENT signal
`KIND_SIGNALS["edge-function"] = \bonRequest[A-Za-z]*\s*[=(]` detects **0** of them — they use the
`export default async (request) => {…}` idiom, not `onRequest`. Measured within-kind recall: **0/2**. Same signal
also false-positives on Astro middleware (`onRequest` is also Astro's middleware export) + a JSX hit → precision
0/2 too (recorded in `2026-06-17-attack-surface-mapper-shipped.md`). The signal is doubly broken; the LOCATION
oracle is already correct.

### Gap B — cross-file resolve() can't reach workspace packages or barrels

`gate.py:resolve()` (lines 47-67) handles relative (`./x`) + hand-passed `--alias` prefixes ONLY. On the measured
zync tree it fails three ways:

1. **No workspace alias.** `import { tenantQuery } from '@zync/db'` → `resolve` hits the bare-specifier branch →
   `return None` (treated as node_modules). 20+ `@zync/*` packages, hundreds of import sites, all invisible.
2. **Dir-entry assumption is wrong.** Even a hand-passed `--alias '@zync/db=packages/db'` MISSES: `resolve` tries
   `packages/db/index.ts`, but zync's entry is `packages/db/src/index.ts` (declared in `package.json`
   `exports["."] = "./src/index.ts"`). The entry is a `package.json` fact, not a dir convention.
3. **Stops at the barrel.** Resolving to `src/index.ts` returns the BARREL (32 `export {…} from './x'` lines).
   The symbol's real body is one re-export hop away (`tenantQuery` → `./queries` → `src/queries/index.ts`).
   `#36`'s `first_party_value_imports` records `(barrel_file, symbol)` and inlines the barrel — which contains no
   logic, only re-exports. The insecure body is never pulled.

Measured `package.json` shape (`@zync/db`, hand-traced this phase):
```jsonc
"main": "./src/index.ts",                                  // entry → SOURCE (not dist/)
"exports": {
  ".": "./src/index.ts",                                   // bare import → barrel
  "./queries": "./src/queries/index.ts",                   // subpath → PRECISE, no barrel-follow needed
  "./validation/customers": "./src/validation/customers.ts"
}
```

## Scope ladder — tightest constraint first (build in this order)

1. **Part A pilot — file-routed enumeration by location.** Smallest, validation already exists (oracle flips
   0/2→2/2), closes a measured #41 residual. Ship first (pilot-before-fanout).
2. **Part B1 — workspace alias auto-discovery.** The MEASURED blocker (the n=1 dispatch report flagged
   "[low] can't verify defineApi contract without alias"). Without it the gate is blind to all first-party
   `@scope/*` code on a workspace repo.
3. **Part B2 — barrel + `exports`-subpath following.** COMPLETES resolution (it is not a `--depth` hop — a
   transparent re-export resolves DIRECTLY to its target; see Decision D3). Without it B1 resolves to a barrel
   with no logic.
4. **DEFER — tsconfig `paths` parser. YAGNI, measured.** zync's real tree DOES declare `paths`
   (`packages/storage/tsconfig.json`: `"@zync/db": ["../db/src/index.ts"]`) — but it maps to the SAME source
   file B1 already computes from `package.json`. So a `paths` parser is REDUNDANT with workspace discovery for the
   measured repos; it earns its place only for a NON-package alias (`@/components` → `src/components`), and zync
   declares none (every `paths` entry is a `@zync/*` package). multideal declares zero first-party `paths` (its 936
   hits are all `node_modules/.pnpm/` third-party noise). Build it only when a measured repo uses a non-package
   alias. Scaffold as a documented no-op hook, honestly labeled.

## Part A design — enumerate file-routed kinds by location

Rule: **BRANCH enumeration by kind — file-routed kinds enumerate from `FILE_ROUTE_RULES` (location), call-registered
kinds enumerate from `KIND_SIGNALS` (content). Keep `KIND_SIGNALS` INTACT.**

`KIND_SIGNALS` has TWO roles (conventions.py line 2): the cross-kind DENOMINATOR and the enumerator. Do NOT delete a
row — that silently changes what `kind_coverage_by_volume` measures. Instead:

- `enumerate_surface`: branch on kind. File-routed kind (`http-file-route`, `edge-function`) → emit one entry per
  file matching its `FILE_ROUTE_RULES` segments (minus `EXCLUDE_NONROUTE`). Call-registered kind (`http-defn-call`,
  webhook/queue/cron, declared-no-row kinds) → content-match `KIND_SIGNALS[kind]` as today. The `edge-function`
  `onRequest` regex is no longer an enumerator (its false-positives vanish) but its ROW stays for the denominator.
- Make the file-routed kind's cross-kind DENOMINATOR location too (consistent with the within-kind oracle), so
  numerator and denominator share the location fact. After this change, **`kind_coverage_by_volume` means: share of
  detected entry points that fall in a kind the mapper enumerates — call-registered kinds counted by content signal,
  file-routed kinds counted by location.** State this verbatim in the validation doc so the metric's meaning is not
  silently redefined.
- Within-kind recall for file-routed kinds is then 1.0 BY CONSTRUCTION (enumerate == oracle) — no longer a
  *measurement*, it is enumeration-by-the-independent-denominator. Residual risk moves to the `FILE_ROUTE_RULES`
  segments being wrong, which a wrong-location test catches.

Acceptance (Part A): `edge-function` enumerated == 2 (both subscribe files), 0 `onRequest` false-positives.
`http-file-route`: **MEASURE the count before (content-enumeration) and after (location-enumeration) — do NOT assert
unchanged.** Location ⊆ content (the 376/376 within-kind recall proves location is a subset of content hits), so the
count can only stay or DECREASE; any off-location `export const GET/POST` hit is dropped. INSPECT every dropped entry
and confirm each is genuinely not a route `FILE_ROUTE_RULES` should model — a dropped real route is the false-coverage
the project forbids, and a wrong "back to 376" target would invite re-adding bad off-location hits. Record the
measured number, whatever it is. 14 mapper tests still pass + 1 new test: a `functions/api/x.ts` with `export default`
is enumerated, a `src/mw.ts` with `onRequest` is NOT.

## Part B design — workspace-aware cross-file resolver

### B1 — auto-discover the alias map (replaces hand-passed --alias as the DEFAULT)

`build_workspace_aliases(repo_root)`:
1. Read `pnpm-workspace.yaml` `packages:` globs (fallback: root `package.json` `workspaces`). zync = `["apps/*",
   "packages/*"]`.
2. Expand globs → candidate package dirs. For each, read `package.json` `name` (skip if absent/private-without-name).
3. Map `name` → resolved ENTRY for the bare import, and `name + "/" + subpath` → resolved entry for each `exports`
   subpath key. Resolution of an entry value:
   - Prefer source: try the value as-is; if it points under a build dir (`dist/`, `build/`, `.next/`, `out/`) or
     ends in `.js`/`.d.ts`, REMAP to the source twin (`dist/x.js` → `src/x.ts`) before accepting. Rationale: the
     git-driven walk excludes build output, so a `dist/` target would resolve to a file the gate says doesn't exist
     (the #41 dist trap). zync points at source already; the remap is the defensive guard for built packages.
   - Field priority: `exports["."]` → `module` → `main` → conventional `src/index.ts` → `index.ts`.
4. Return the map as the same `[(prefix, root)]` shape `resolve()` already consumes, so `resolve()` is unchanged
   for matching — BUT the values now point at real entry FILES, fixing Gap B2.

`resolve()` gains: accept a discovered map; when a spec is `@scope/pkg/sub`, match the longest `exports` key first
(so `@zync/db/queries` resolves precisely before the bare `@zync/db`). Hand-passed `--alias` still works and
OVERRIDES discovery (escape hatch).

### B2 — follow barrel re-exports + exports subpaths to the definition file

A subpath import (`@zync/db/queries`) resolves DIRECTLY via the `exports` map → no follow needed (precise by
construction). Only the bare `.` import lands on a barrel. So:

`resolve_through_barrel(file, symbol)`: if `file` is a re-export barrel (only `export … from` lines, no value
defns), find the re-export line that carries `symbol` and resolve ITS specifier (one hop), recursing through nested
barrels. Stop at the first file that DEFINES `symbol` (a non-re-export `export const/function/class symbol`), or
after N hops (cap, default 3 — a barrel chain deeper than 3 is pathological; log if hit, never silently truncate).
`export * from './x'` with no named match → follow all `*` targets breadth-first until the symbol is defined.

Wire into `first_party_value_imports`: after `resolve(spec)` returns a barrel, replace `(barrel, symbol)` with
`(definition_file, symbol)` from `resolve_through_barrel`. Transparent re-export resolves DIRECTLY — it is NOT a
`collect_deps` depth hop (Decision D3); the def file is the 1-hop dependency, the barrel was just an indirection.

Acceptance (Part B): on the real zync tree, count first-party `@zync/*` value imports resolving to a real file
BEFORE (0) vs AFTER. Report per-package (`@zync/db`, `@zync/auth`, …) resolved/total. Plus one xfile cell: an
importer pulling a symbol through `@zync/db`'s barrel → the inlined bundle contains the DEFINITION body, not the
barrel line (k=3 the gate still catches its seeded canonical). No new buggy canonical is REQUIRED for the headline
(coverage is the claim); the cell only proves the pulled body is the real one.

**#42 lesson — verify BOTH legs reach the resolved def file, do not assume (BLOCKS the Part B done-claim).**
Barrel-following relocates the in-scope file from barrel → def file; that relocation is exactly the #42 failure mode
(relocating WHERE one leg reads silently shrank ANOTHER leg's reach). The cell above only checks the LLM *bundle*. ALSO
assert the per-in-scope-file oracle (`run_oracle_set`) actually fires on the resolved def file — wiring-verified, not
assumed (e.g. an imported-helper C02/C09 sink in the def file is oracle-only with no LLM backstop, so if the oracle
doesn't follow the resolution it goes silent-false-clean). Test: a barrel-resolved def file carrying an oracle-class
sink is reported by the oracle leg after resolution.

**Value chain — keep it explicit, coverage never stands alone as the win.** Coverage 0→Y is an ENABLER, not a catch:
the chain is `resolution coverage ↑ → oracle/LLM reaches more first-party sinks → catches (demonstrated downstream by
the #24 recall sweep on the newly-reached code)`. Report the coverage number AS the enabler, with the catch evidence
named as the downstream sweep — do not present the coverage delta itself as a recall result.

## Validation — coverage, not a fabricated bug (load-bearing)

This phase is a REACHABILITY fix. Its honest, deterministic gate is **"first-party imports that resolve to None
today vs after, on the real zync tree"** — run static, 0 LLM, fully reproducible. Do NOT manufacture a
barrel-hidden buggy cell to claim a recall win: measured Shape-B (barrel-hidden insecure default) scarcity is n=1/85
(`2026-06-17-shapeB-scarcity-cross-repo.md`), and the project rule forbids building for a synthetic. Claim a recall
improvement ONLY if a real barrel-hidden canonical actually surfaces in the sweep; otherwise the claim is
"resolution coverage X→Y", which is true and sufficient.

Precision guard (the cross-file cost, per #42): more resolved imports = more inlined bodies = more tokens + more
surface for paraphrased-duplicate findings. Measure inlined-bundle size + finding count before/after on the xfile
cell; if dup findings rise, lean on `#17a semantic_merge`, do not widen scope blindly.

## Architecture decisions (deletion / single-adapter / seam)

- **D1 — `build_workspace_aliases` earns its boundary (deletion test PASSES).** Delete it and the per-import alias
  knowledge scatters into every gate invocation as hand-passed `--alias` strings — exactly today's measured
  failure. It hides a real fact source (pnpm-workspace + N package.json files) behind a stable
  `[(prefix, root)]` interface `resolve()` already speaks. DEEP.
- **D2 — Part A COLLAPSES a seam, not adds one.** It drops the `edge-function` content-signal as an ENUMERATOR
  (the `onRequest` regex's false-positives go with it) and routes file-routed enumeration through the existing
  filesystem oracle — while KEEPING the `KIND_SIGNALS` row intact for the cross-kind denominator (fix per advisor:
  the row has two jobs; only the enumerator job moves). Fewer moving parts, both defects gone. No new abstraction.
- **D3 — barrel-following is part of RESOLUTION, not a new depth knob (single-adapter).** Resist a
  `--barrel-depth` flag: a transparent re-export has exactly one correct target; "resolve `symbol` to where it is
  defined" is one operation. A separate depth knob would re-introduce the #36 failure (a real def gated behind a
  hop count). Cap is a pathology backstop, not a feature surface.
- **D4 — tsconfig `paths` parser REJECTED for v1 (single-adapter / YAGNI).** Measured-redundant with B1 on the
  only repos in evidence; the one case it'd serve (non-package alias) has zero measured instances. Documented no-op
  until a repo needs it.
- **Rejected: a generic JS module resolver / ts-morph dependency.** Over-deep for the need (first-party scope only,
  not full TS semantics) + adds a heavy non-Python dep to a deterministic Python orchestrator. The convention-based
  resolver covers the measured shapes; revisit only if a measured repo defeats it.

## Honest scope / NOT covered in v1

- tsconfig `paths` non-package aliases (D4) — deferred no-op.
- 2-hop+ transitive package→package barrels beyond the cap (logged, not silently dropped).
- Yarn/npm `workspaces` globs are read as the fallback but only pnpm-workspace is MEASURED; mark npm/yarn
  best-effort until a real repo exercises it.
- Cross-file PRECISION at n≥3 is the open measurement this phase OPENS (per #40 charter) — delivered as the
  coverage numbers + the xfile cell; broad-precision sweep is its own follow.
