Ties the Ring 2 pipeline together. Takes the `ParsedFile[]` produced by
#920's parse-worker integration, feeds them to shared `finalize()`
(#915), and bundles every workspace-wide index for attachment onto
`MutableSemanticModel`. Thin integration glue per issue #884's boundary
— all algorithm lives in `gitnexus-shared`.
## Shipped
### `model/scope-resolution-indexes.ts` (new)
```ts
interface ScopeResolutionIndexes {
readonly scopeTree: ScopeTree;
readonly defs: DefIndex;
readonly qualifiedNames: QualifiedNameIndex;
readonly moduleScopes: ModuleScopeIndex;
readonly methodDispatch: MethodDispatchIndex;
readonly imports: ReadonlyMap<ScopeId, readonly ImportEdge[]>;
readonly bindings: ReadonlyMap<ScopeId, ReadonlyMap<string, readonly BindingRef[]>>;
readonly referenceSites: readonly ReferenceSite[];
readonly sccs: readonly FinalizedScc[];
readonly stats: FinalizeStats;
}
```
The bundle produced by the orchestrator, consumed by the resolution
phase. `ReferenceIndex` is deliberately NOT here — it's populated in
the next phase (#925).
### `model/semantic-model.ts` — extended
- `SemanticModel.scopes?: ScopeResolutionIndexes` — undefined until
attached; once attached, frozen.
- `MutableSemanticModel.attachScopeIndexes(indexes)` — one-shot write.
Throws on second call; `Object.freeze`s the bundle on write. `clear()`
resets the slot back to `undefined` so re-ingestion can re-attach.
### `finalize-orchestrator.ts` (new)
```ts
finalizeScopeModel(parsedFiles, options?): ScopeResolutionIndexes
```
Orchestration steps:
1. Map `ParsedFile[]` → `FinalizeInput` (`FinalizeFile` is a structural
subset, so no shape-shifting).
2. Call shared `finalize()` with provider hooks (defaults provided for
the zero-provider case today).
3. Build the four workspace indexes (`DefIndex`, `QualifiedNameIndex`,
`ModuleScopeIndex`, `ScopeTree`) from per-file unions.
4. Build an empty `MethodDispatchIndex` as a placeholder (owners=[],
both callbacks return []). Real MRO wiring lands with the
per-language adapters in #922.
5. Bundle + return.
**Empty-input safety.** Zero parsedFiles → valid but empty bundle with
all zero-sized indexes and `stats.totalFiles === 0`. Downstream code
can consult `model.scopes` without branching on presence — only on
`stats`.
**Hook defaults** (`withDefaultHooks`) for missing provider hooks:
- `resolveImportTarget: () => null` — every import goes `unresolved`
- `expandsWildcardTo: () => []` — wildcards don't materialize
- `mergeBindings: (a, b) => [...a, ...b]` — append without precedence
Providers override these in #922 (per-language import adapters).
## Tests (10, all passing)
- **Empty input** (1): zero parsedFiles → valid empty bundle
- **Single file** (2): all per-file indexes populated · referenceSites
aggregated
- **Cross-file imports** (3): resolveImportTarget threads through +
links · default-null resolver → unresolved · stats reflect graph
- **MutableSemanticModel integration** (4): undefined initially · attach
once · Object.freeze applied · throws on re-attach · clear() resets
## Verification
- `tsc --noEmit` clean in both packages
- `gitnexus-shared` build clean
- 10/10 new tests pass
- Full scope-resolution / shadow / model / flag suite: **321/321 pass**
## What's deferred (not this PR, per RFC #909 scope)
- **Per-language hook adapters** (#922): `resolveImportTarget` +
`expandsWildcardTo` + `mergeBindings` wired per language.
- **MethodDispatchIndex wiring via HeritageMap**: populate MRO + implements
via the existing CLI-package HeritageMap strategies. Likely companion
to #922 or a focused follow-up.
- **Pipeline invocation**: actually calling `finalizeScopeModel` from
the real ingestion pipeline. The orchestrator is callable today; the
ingestion entry point wiring lands with the shadow harness (#923).
- **`ReferenceIndex` population**: RFC §3.2 Phase 4 / #925.
## Closes part of #909. Unblocks
- #923 shadow harness — now has a fully materialized `model.scopes` to
query against the legacy DAG for parity measurement
- #925 ReferenceIndex → LadybugDB emission — consumes `model.scopes`
- Ring 3 language migrations (#926+) — a language flipping to
`REGISTRY_PRIMARY_<LANG>=true` can now expect `model.scopes` to be
populated when the pipeline wires the orchestrator in