mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-05 02:43:32 +00:00
docs(scope-resolution): document I1-I8 invariants, source-of-truth, and same-graph guarantee
Promote contract knowledge that was implicit in code into the canonical docs
so future migrations and the next reviewer don't have to reverse-engineer it.
contract/scope-resolver.ts:
* Migration cookbook lists every optional hook (was: only the two
booleans), with one-line guidance per hook including when to enable
`hoistTypeBindingsToModule`.
* Contract Invariants I1-I7 are now spelled out in full (was: only
I1/I3/I5 summarized with a pointer to a plan file). Added new I8
"post-finalize hooks may mutate Scope.typeBindings and indexes.bindings;
consumers must not freeze or snapshot before all post-finalize hooks
have run".
* New "Semantic-model source of truth" section: ParsedFile is the
single semantic model; passes that need AST-level facts must reuse
the orchestrator's treeCache rather than re-parse.
* New "Same-graph guarantee" section: legacy DAG and scope-resolution
emit indistinguishable edges (node identity, edge vocabulary,
confidence). CI parity workflow enforces this.
gitnexus-shared/src/scope-resolution/parsed-file.ts:
* Added "Source-of-truth invariant" pointer paragraph.
ARCHITECTURE.md (Coexistence section):
* Updated migrated-language list (Python + C#).
* Added "Same-graph guarantee" subsection.
* Added "Semantic-model source of truth" subsection.
* Filled in the ScopeResolver hook table with the five optional hooks
that landed in this branch (unwrapCollectionAccessor,
collapseMemberCallsByCallerTarget, populateNamespaceSiblings,
hoistTypeBindingsToModule, fieldFallbackOnMethodLookup).
* Added C# rows to the code-references table.
Verified:
- npx tsc --noEmit clean
- C# + Python integration 393/393 passing
This commit is contained in:
parent
f9956d89e2
commit
84a89a42b7
3 changed files with 146 additions and 13 deletions
|
|
@ -215,10 +215,24 @@ Both hooks are optional on `LanguageProvider`. Ruby is the only current implemen
|
|||
The Call-Resolution DAG is the **legacy path**. RFC #909 Ring 3 introduces a parallel **scope-resolution pipeline** (next section) that replaces stages 1–6 with a scope-indexed registry lookup. Both paths ship side-by-side and are gated per-language via `MIGRATED_LANGUAGES` + the `REGISTRY_PRIMARY_<LANG>` env var.
|
||||
|
||||
- **Unmigrated language** → Call-Resolution DAG runs; scope-resolution phase is a no-op.
|
||||
- **Migrated language** (currently: Python) → scope-resolution owns CALLS/ACCESSES/USES emission; the legacy DAG gates off for that language via `isRegistryPrimary(lang)` checks in `call-processor.ts` and `import-processor.ts`.
|
||||
- **Migrated language** (currently: Python, C#) → scope-resolution owns CALLS/ACCESSES/USES emission; the legacy DAG gates off for that language via `isRegistryPrimary(lang)` checks in `call-processor.ts` and `import-processor.ts`.
|
||||
- `import-processor` still populates `importMap` for migrated languages — heritage's `ctx.resolve` reads it to disambiguate parent classes. Only edge emission is gated.
|
||||
- CI runs BOTH paths for every migrated language on every PR (`.github/workflows/ci-scope-parity.yml`); both must pass.
|
||||
|
||||
#### Same-graph guarantee
|
||||
|
||||
Edges emitted by the scope-resolution pipeline and edges emitted by the legacy DAG are indistinguishable to downstream consumers (MCP tools, HTTP API, embeddings, group bridge):
|
||||
|
||||
- **Node identity** — both paths use `generateId(...)` from `lib/utils.ts`, the same qualified-name keyspace, and the same node labels (`File`, `Folder`, `Class`, `Method`, `Function`, …). Overload disambiguation suffixes `parameterTypes` into the id consistently — see `scope-resolution/graph-bridge/ids.ts` and the legacy emitter in `call-processor.ts`.
|
||||
- **Edge vocabulary** — both paths emit the same reasons: `'import-resolved' | 'global' | 'local-call' | 'same-file' | 'interface-dispatch' | 'read' | 'write'`. Migrating a language must not change which reasons consumers see for previously-resolved edges.
|
||||
- **Confidence tier** — both paths attach a numeric `confidence` to each edge using the same scale.
|
||||
|
||||
The CI parity workflow (`.github/workflows/ci-scope-parity.yml`) runs both paths against every migrated language's fixture corpus and fails on any divergence.
|
||||
|
||||
#### Semantic-model source of truth
|
||||
|
||||
`ParsedFile` (`gitnexus-shared/src/scope-resolution/parsed-file.ts`) is the single semantic model both paths consume. Scope-resolution passes MUST NOT build a parallel parse representation. If a per-language hook needs AST-level facts that `ParsedFile` doesn't expose, it should reuse the orchestrator's `treeCache` (`RunScopeResolutionInput.treeCache`) rather than re-invoking `parser.parse(...)` on its own — the C# `populateNamespaceSiblings` hook is the reference implementation of this pattern.
|
||||
|
||||
---
|
||||
|
||||
## Scope-Resolution Pipeline (RFC #909 Ring 3)
|
||||
|
|
@ -260,6 +274,11 @@ Single interface a language implements to plug into the pipeline. Contract fully
|
|||
| `arityCompatibility` | Provider consumed by registry during `MethodRegistry.lookup` Step 2 |
|
||||
| `importEdgeReason` | Confidence-tier string for IMPORTS edge reason field |
|
||||
| `propagatesReturnTypesAcrossImports?` | Opt out of cross-file return-type propagation (default on) |
|
||||
| `fieldFallbackOnMethodLookup?` | Statically-typed languages turn this OFF — the heuristic over-connects (default on) |
|
||||
| `unwrapCollectionAccessor?` | Property-style collection views (`data.Values` on Dictionary-like receivers) — default off |
|
||||
| `collapseMemberCallsByCallerTarget?` | One CALLS edge per (caller, target) instead of per-site — default off |
|
||||
| `populateNamespaceSiblings?` | Cross-file implicit visibility (compiler-implicit namespace sharing) — default off; ctx carries `treeCache` |
|
||||
| `hoistTypeBindingsToModule?` | Walk up to Module scope when looking up a method's return-type typeBinding — default off; enable only when bindings are stored at module level |
|
||||
|
||||
### Per-language registration
|
||||
|
||||
|
|
@ -284,6 +303,9 @@ CI auto-discovers the set via `tsx`. No workflow edit required.
|
|||
| `registry-primary-flag.ts` | `MIGRATED_LANGUAGES` set + `isRegistryPrimary(lang)` |
|
||||
| `languages/python/index.ts` | Python `ScopeResolver` hooks + known-limitation docs |
|
||||
| `languages/python/captures.ts` | `emitPythonScopeCaptures` (honors cross-phase Tree cache) |
|
||||
| `languages/csharp/index.ts` | C# `ScopeResolver` hooks + known-limitation docs |
|
||||
| `languages/csharp/captures.ts` | `emitCsharpScopeCaptures` (honors cross-phase Tree cache) |
|
||||
| `languages/csharp/namespace-siblings.ts` | Cross-file implicit-namespace visibility hook (reads `treeCache`) |
|
||||
|
||||
### Performance notes
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,18 @@
|
|||
* `localDefs`. A `ParsedFile` is trivially convertible to a `FinalizeFile`
|
||||
* by picking those four fields, so the finalize orchestrator threads
|
||||
* ParsedFile through to the shared algorithm without shape-shifting.
|
||||
*
|
||||
* ## Source-of-truth invariant
|
||||
*
|
||||
* `ParsedFile` is the single semantic model consumed by both the legacy
|
||||
* DAG (`gitnexus/src/core/ingestion/` outside `scope-resolution/`) and
|
||||
* the scope-resolution pipeline (`gitnexus/src/core/ingestion/scope-resolution/`).
|
||||
* Downstream passes MUST NOT build a parallel parse representation; if
|
||||
* a pass needs AST-level facts that `ParsedFile` doesn't expose, it
|
||||
* should reuse the orchestrator's `treeCache` rather than re-invoke
|
||||
* `parser.parse(...)` on its own. See the
|
||||
* `ScopeResolver` contract (`gitnexus/src/core/ingestion/scope-resolution/contract/scope-resolver.ts`)
|
||||
* for the full list of invariants downstream consumers rely on.
|
||||
*/
|
||||
|
||||
import type { Scope, ScopeId } from './types.js';
|
||||
|
|
|
|||
|
|
@ -8,11 +8,19 @@
|
|||
*
|
||||
* 1. Implement `ScopeResolver` in
|
||||
* `gitnexus/src/core/ingestion/languages/<lang>/scope-resolver.ts`.
|
||||
* Six required fields (language, languageProvider,
|
||||
* Nine required fields (language, languageProvider,
|
||||
* importEdgeReason, resolveImportTarget, mergeBindings,
|
||||
* arityCompatibility, buildMro, populateOwners, isSuperReceiver)
|
||||
* plus two optional booleans (propagatesReturnTypesAcrossImports,
|
||||
* fieldFallbackOnMethodLookup).
|
||||
* plus optional toggles / hooks:
|
||||
* - propagatesReturnTypesAcrossImports (default true)
|
||||
* - fieldFallbackOnMethodLookup (default true — turn OFF for
|
||||
* statically-typed languages; the heuristic over-connects)
|
||||
* - unwrapCollectionAccessor — property-style collection views
|
||||
* - collapseMemberCallsByCallerTarget — one edge per caller/target
|
||||
* - populateNamespaceSiblings — cross-file implicit visibility
|
||||
* - hoistTypeBindingsToModule — enable ONLY when method return
|
||||
* types are stored on the enclosing Module scope; most
|
||||
* languages attach them to the class scope and leave this off
|
||||
* 2. Export a thin entry point:
|
||||
* `runYourLangScopeResolution(input) = runScopeResolution(input, yourScopeResolver)`.
|
||||
* 3. Register the provider in
|
||||
|
|
@ -59,18 +67,109 @@
|
|||
*
|
||||
* ## Contract Invariants the orchestrator depends on
|
||||
*
|
||||
* (See the plan for the full list. Highlights only here.)
|
||||
* These are non-obvious behaviors that the orchestrator and the
|
||||
* existing Python + C# resolvers depend on. Future implementers will
|
||||
* break them silently if not documented.
|
||||
*
|
||||
* - **I1 — Phase 4 emission order is load-bearing.** `emitReceiverBoundCalls`
|
||||
* runs FIRST (populates `handledSites`), then `emitFreeCallFallback`,
|
||||
* then `emitReferencesViaLookup` (consumes `handledSites` as a skip
|
||||
* set), then `emitImportEdges`. Reordering breaks same-name collision
|
||||
* resolution: the shared lookup can mis-resolve `app_metrics.get_metrics()`
|
||||
* to a same-named local function, and only the precise per-receiver
|
||||
* pass running first prevents the wrong edge.
|
||||
*
|
||||
* - **I2 — `handledSites` semantics.** A site is added to
|
||||
* `handledSites` IFF a `tryEmitEdge` call returned `true` for it.
|
||||
* Sites a pass touched but couldn't resolve do NOT get marked —
|
||||
* they still get a chance from the shared resolver. Exception:
|
||||
* the free-call fallback marks the site unconditionally after
|
||||
* attempting emission (even on dedup-collapse), because the
|
||||
* per-(caller, target) collapse semantics require multiple call
|
||||
* sites in the same caller body not produce multiple edges.
|
||||
*
|
||||
* - **I3 — `propagateImportedReturnTypes` mutation timing.** The
|
||||
* pass mutates `Scope.typeBindings` (a plain `new Map(...)` from
|
||||
* `draftToScope`, NOT frozen). It MUST run AFTER `finalizeScopeModel`
|
||||
* (so `indexes.bindings` is populated) and BEFORE
|
||||
* `resolveReferenceSites` (so resolution sees the propagated types).
|
||||
* The pass also re-runs `followChainPostFinalize` on every scope's
|
||||
* typeBindings because scope-extractor's pass-4 already ran and
|
||||
* missed any chain whose terminal lives in a foreign file.
|
||||
*
|
||||
* - **I4 — `emitReceiverBoundCalls` case order.** Cases are evaluated
|
||||
* in this order; the FIRST that emits an edge wins:
|
||||
* 1. super branch (`provider.isSuperReceiver(receiverName)`)
|
||||
* 2. Case 0 compound (`receiverName` has `.` or `(`)
|
||||
* 3. Case 1 namespace-receiver
|
||||
* 4. Case 2 class-name receiver
|
||||
* 5. Case 3 dotted typeBinding for namespace prefix
|
||||
* 6. Case 3b chain-typebinding (compound resolver)
|
||||
* 7. Case 4 simple typeBinding (MRO walk + findOwnedMember)
|
||||
* Reordering or merging cases changes resolution semantics. The
|
||||
* numbering is part of the contract — keep the comments.
|
||||
*
|
||||
* - **I1 — Phase 4 emission order is load-bearing.** Receiver-bound
|
||||
* pass FIRST, then free-call fallback, then `emitReferencesViaLookup`.
|
||||
* - **I3 — `propagateImportedReturnTypes` runs after finalize and
|
||||
* before `resolveReferenceSites`.** It mutates non-frozen
|
||||
* `Scope.typeBindings`. Do not freeze typeBindings in any
|
||||
* downstream refactor.
|
||||
* - **I5 — Pre-seeding `seen` from `referenceIndex` is forbidden.**
|
||||
* The receiver-bound pass relies on this never happening.
|
||||
* Earlier versions of the receiver-bound pass pre-populated `seen`
|
||||
* to avoid double-emit. After Phase 4 was reordered, pre-seeding
|
||||
* became actively harmful: it suppresses correct emissions for
|
||||
* sites the shared resolver happened to resolve to a wrong target.
|
||||
* The orchestrator MUST NOT pre-seed.
|
||||
*
|
||||
* Plan: `docs/plans/2026-04-20-001-refactor-emit-pipeline-generalization-plan.md`.
|
||||
* - **I6 — `Scope.typeBindings` is mutable post-finalize.** `draftToScope`
|
||||
* (in `scope-extractor.ts`) builds `typeBindings` as a plain
|
||||
* `new Map(...)` — not frozen, intentionally. Passes below rely on
|
||||
* this. Do NOT freeze `typeBindings` in any downstream refactor.
|
||||
*
|
||||
* - **I7 — `ScopeResolver` and `LanguageProvider` are distinct contracts.**
|
||||
* Python and C# pass the SAME function reference through both
|
||||
* interfaces where they share a hook name — no second copy of the
|
||||
* logic. Rationale for not collapsing them: lifecycles differ
|
||||
* (parsing-side runs once per file at extract time, emit-side runs
|
||||
* once per workspace at resolve time), and merging would create a
|
||||
* god-interface that complicates future migrations.
|
||||
*
|
||||
* - **I8 — Post-finalize hooks may mutate `Scope.typeBindings` and
|
||||
* `indexes.bindings`.** `propagateImportedReturnTypes` and
|
||||
* `populateNamespaceSiblings` both write to these structures via
|
||||
* `as Map<...>` casts through `ReadonlyMap` facades. Downstream
|
||||
* consumers MUST NOT freeze or snapshot these maps before all
|
||||
* post-finalize hooks have run. The `ReadonlyMap<...>` type on
|
||||
* `ScopeResolutionIndexes` is a read-guidance surface for
|
||||
* consumers, NOT an immutability promise during the resolve phase.
|
||||
*
|
||||
* ## Semantic-model source of truth
|
||||
*
|
||||
* `ParsedFile` (from `gitnexus-shared/src/scope-resolution/parsed-file.ts`)
|
||||
* is the single semantic model consumed by both the legacy DAG and the
|
||||
* scope-resolution pipeline. Scope-resolution passes MUST NOT build a
|
||||
* parallel parse representation; if a pass needs AST-level facts that
|
||||
* `ParsedFile` doesn't expose, it should reuse the orchestrator's
|
||||
* `treeCache` (see `RunScopeResolutionInput.treeCache`) rather than
|
||||
* re-invoke `parser.parse(...)` on its own.
|
||||
*
|
||||
* ## Same-graph guarantee
|
||||
*
|
||||
* Edges emitted by `runScopeResolution` and edges emitted by the legacy
|
||||
* DAG are indistinguishable to downstream consumers:
|
||||
* - Node identity: same `generateId(...)` helper, same qualified-name
|
||||
* keyspace, same File/Folder/Method/Class node labels.
|
||||
* - Edge vocabulary: `'import-resolved' | 'global' | 'local-call' |
|
||||
* 'same-file' | 'interface-dispatch' | 'read' | 'write'` — both
|
||||
* paths emit the same reasons (see
|
||||
* `gitnexus/src/core/ingestion/call-processor.ts` for the legacy
|
||||
* emitter and `passes/receiver-bound-calls.ts` /
|
||||
* `passes/free-call-fallback.ts` for the scope-resolution emitters).
|
||||
* - Overload disambiguation: both paths use
|
||||
* `generateId('Method', ...)` suffixed with `parameterTypes` when a
|
||||
* method has overloads — see `graph-bridge/ids.ts`.
|
||||
*
|
||||
* The CI parity workflow (`.github/workflows/ci-scope-parity.yml`)
|
||||
* runs both paths on every migrated language's fixture corpus and
|
||||
* fails if the graph outputs diverge.
|
||||
*
|
||||
* Plan that introduced most of these invariants:
|
||||
* `docs/plans/2026-04-20-001-refactor-emit-pipeline-generalization-plan.md`.
|
||||
*/
|
||||
|
||||
import type {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue