gitnexus 1.6.3-rc.12 → 1.6.3-rc.14

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.
@@ -0,0 +1,63 @@
1
+ /**
2
+ * `finalizeScopeModel` — turn a workspace's `ParsedFile[]` into a
3
+ * materialized `ScopeResolutionIndexes` (RFC §3.2 Phase 2; Ring 2 PKG #921).
4
+ *
5
+ * Thin integration glue, per issue #884's boundary: all algorithmic logic
6
+ * lives in `gitnexus-shared` (finalize algorithm #915, the four per-file
7
+ * indexes #913, the method-dispatch materialization #914, the scope tree
8
+ * #912). This file does three things only:
9
+ *
10
+ * 1. Map `ParsedFile[]` → `FinalizeInput` and call shared `finalize()`.
11
+ * 2. Build the four workspace-wide indexes from the union of per-file
12
+ * defs/scopes/modules/qualified-names.
13
+ * 3. Bundle the results into `ScopeResolutionIndexes` for
14
+ * `MutableSemanticModel.attachScopeIndexes(...)`.
15
+ *
16
+ * ## What this module is NOT responsible for
17
+ *
18
+ * - Invoking tree-sitter or running AST walks. That's the extractor (#919).
19
+ * - Per-language import-target resolution. Hooks are plumbed through
20
+ * but default to "unresolved" when no provider supplies them — the
21
+ * real adapters land with #922.
22
+ * - Populating `ReferenceIndex`. That's the resolution phase (#925).
23
+ * - Deciding which language uses registry-primary lookup. That's the
24
+ * flag reader (#924).
25
+ *
26
+ * ## Empty-input behavior
27
+ *
28
+ * When `parsedFiles` is empty (the common case today — no language has
29
+ * migrated yet), the orchestrator produces a valid but empty bundle: all
30
+ * indexes are zero-sized, the scope tree is empty, and
31
+ * `finalize.stats.totalFiles === 0`. This lets downstream consumers
32
+ * safely consult `model.scopes` without branching on presence.
33
+ */
34
+ import type { FinalizeHooks, ParsedFile, WorkspaceIndex } from '../../_shared/index.js';
35
+ import type { ScopeResolutionIndexes } from './model/scope-resolution-indexes.js';
36
+ /**
37
+ * Options forwarded to the orchestrator. All fields optional so callers
38
+ * that don't yet have per-language hooks (today) get sensible defaults;
39
+ * #922 will populate `hooks.resolveImportTarget` + friends per language.
40
+ */
41
+ export interface FinalizeOrchestratorOptions {
42
+ /**
43
+ * Hooks forwarded to shared `finalize()`. Any omitted field gets a
44
+ * no-op default: unresolved targets, empty wildcard expansion, append
45
+ * merge for bindings.
46
+ */
47
+ readonly hooks?: Partial<FinalizeHooks>;
48
+ /**
49
+ * Opaque workspace context forwarded to hooks. `undefined` today; Ring
50
+ * 2 PKG #922 populates this with a real cross-file index for the
51
+ * per-language resolvers.
52
+ */
53
+ readonly workspaceIndex?: WorkspaceIndex;
54
+ }
55
+ /**
56
+ * Produce a fully materialized `ScopeResolutionIndexes` from the
57
+ * workspace's per-file artifacts.
58
+ *
59
+ * Pure function (given pure hooks). No I/O, no globals consulted. The
60
+ * pipeline calls this once per ingestion run and hands the result to
61
+ * `MutableSemanticModel.attachScopeIndexes`.
62
+ */
63
+ export declare function finalizeScopeModel(parsedFiles: readonly ParsedFile[], options?: FinalizeOrchestratorOptions): ScopeResolutionIndexes;
@@ -0,0 +1,139 @@
1
+ /**
2
+ * `finalizeScopeModel` — turn a workspace's `ParsedFile[]` into a
3
+ * materialized `ScopeResolutionIndexes` (RFC §3.2 Phase 2; Ring 2 PKG #921).
4
+ *
5
+ * Thin integration glue, per issue #884's boundary: all algorithmic logic
6
+ * lives in `gitnexus-shared` (finalize algorithm #915, the four per-file
7
+ * indexes #913, the method-dispatch materialization #914, the scope tree
8
+ * #912). This file does three things only:
9
+ *
10
+ * 1. Map `ParsedFile[]` → `FinalizeInput` and call shared `finalize()`.
11
+ * 2. Build the four workspace-wide indexes from the union of per-file
12
+ * defs/scopes/modules/qualified-names.
13
+ * 3. Bundle the results into `ScopeResolutionIndexes` for
14
+ * `MutableSemanticModel.attachScopeIndexes(...)`.
15
+ *
16
+ * ## What this module is NOT responsible for
17
+ *
18
+ * - Invoking tree-sitter or running AST walks. That's the extractor (#919).
19
+ * - Per-language import-target resolution. Hooks are plumbed through
20
+ * but default to "unresolved" when no provider supplies them — the
21
+ * real adapters land with #922.
22
+ * - Populating `ReferenceIndex`. That's the resolution phase (#925).
23
+ * - Deciding which language uses registry-primary lookup. That's the
24
+ * flag reader (#924).
25
+ *
26
+ * ## Empty-input behavior
27
+ *
28
+ * When `parsedFiles` is empty (the common case today — no language has
29
+ * migrated yet), the orchestrator produces a valid but empty bundle: all
30
+ * indexes are zero-sized, the scope tree is empty, and
31
+ * `finalize.stats.totalFiles === 0`. This lets downstream consumers
32
+ * safely consult `model.scopes` without branching on presence.
33
+ */
34
+ import { buildDefIndex, buildMethodDispatchIndex, buildModuleScopeIndex, buildQualifiedNameIndex, buildScopeTree, finalize, } from '../../_shared/index.js';
35
+ /**
36
+ * Produce a fully materialized `ScopeResolutionIndexes` from the
37
+ * workspace's per-file artifacts.
38
+ *
39
+ * Pure function (given pure hooks). No I/O, no globals consulted. The
40
+ * pipeline calls this once per ingestion run and hands the result to
41
+ * `MutableSemanticModel.attachScopeIndexes`.
42
+ */
43
+ export function finalizeScopeModel(parsedFiles, options = {}) {
44
+ const hooks = withDefaultHooks(options.hooks ?? {});
45
+ const workspaceIndex = options.workspaceIndex ?? undefined;
46
+ // ── Step 1: Shared finalize — runs SCC-aware cross-file link + binding
47
+ // materialization. Returns linked imports + merged bindings per module
48
+ // scope + SCC condensation + stats.
49
+ const finalizeInput = {
50
+ files: parsedFiles.map(toFinalizeFile),
51
+ workspaceIndex,
52
+ };
53
+ const finalizeOut = finalize(finalizeInput, hooks);
54
+ // ── Step 2: Workspace-wide indexes built from the per-file unions.
55
+ // These are pure aggregations — no algorithm beyond what the builders
56
+ // in gitnexus-shared already encapsulate (first-write-wins, qname
57
+ // collision buckets, etc.).
58
+ const allScopes = [];
59
+ const allDefs = [];
60
+ const moduleEntries = [];
61
+ const allReferenceSites = [];
62
+ for (const file of parsedFiles) {
63
+ for (const s of file.scopes)
64
+ allScopes.push(s);
65
+ for (const d of file.localDefs)
66
+ allDefs.push(d);
67
+ moduleEntries.push({ filePath: file.filePath, moduleScopeId: file.moduleScope });
68
+ }
69
+ // References kept out of the loop above to centralize list-init.
70
+ allReferenceSites.push(...collectReferenceSites(parsedFiles));
71
+ const scopeTree = buildScopeTree(allScopes);
72
+ const defs = buildDefIndex(allDefs);
73
+ const qualifiedNames = buildQualifiedNameIndex(allDefs);
74
+ const moduleScopes = buildModuleScopeIndex(moduleEntries);
75
+ // ── Step 3: MethodDispatchIndex. Today we lack per-language MRO
76
+ // strategies wired into this orchestrator (that belongs with the
77
+ // HeritageMap bridge, a separate piece of work). Ship an EMPTY index
78
+ // so the bundle shape is consistent; the callbacks return `[]` for
79
+ // every owner and `implementsOf` returns `[]`. Populating this
80
+ // properly is tracked alongside the per-language provider hooks.
81
+ const methodDispatch = buildMethodDispatchIndex({
82
+ owners: [], // empty → no MRO entries; `mroFor(x)` returns the frozen empty array
83
+ computeMro: () => [],
84
+ implementsOf: () => [],
85
+ });
86
+ return {
87
+ scopeTree,
88
+ defs,
89
+ qualifiedNames,
90
+ moduleScopes,
91
+ methodDispatch,
92
+ imports: finalizeOut.imports,
93
+ bindings: finalizeOut.bindings,
94
+ referenceSites: Object.freeze([...allReferenceSites]),
95
+ sccs: finalizeOut.sccs,
96
+ stats: finalizeOut.stats,
97
+ };
98
+ }
99
+ // ─── Internal ───────────────────────────────────────────────────────────────
100
+ /** Shape-reduce a `ParsedFile` to the narrower `FinalizeFile` the shared
101
+ * algorithm reads. The subset is stable — `FinalizeFile` is a proper
102
+ * subset of `ParsedFile`. */
103
+ function toFinalizeFile(file) {
104
+ return {
105
+ filePath: file.filePath,
106
+ moduleScope: file.moduleScope,
107
+ parsedImports: file.parsedImports,
108
+ localDefs: file.localDefs,
109
+ };
110
+ }
111
+ /** Flatten every file's reference sites into one list. Order reflects
112
+ * input-file order, then capture order inside each file. Deterministic. */
113
+ function collectReferenceSites(parsedFiles) {
114
+ const out = [];
115
+ for (const file of parsedFiles) {
116
+ for (const site of file.referenceSites)
117
+ out.push(site);
118
+ }
119
+ return out;
120
+ }
121
+ /**
122
+ * Fill in no-op defaults for any omitted hook. Keeps `finalize()`
123
+ * behavior well-defined for the zero-provider case today:
124
+ *
125
+ * - `resolveImportTarget: () => null` — every import edge ends up
126
+ * `linkStatus: 'unresolved'` (or dynamic-unresolved pass-through).
127
+ * - `expandsWildcardTo: () => []` — wildcards don't materialize.
128
+ * - `mergeBindings: (existing, incoming) => [...existing, ...incoming]`
129
+ * — append without precedence; providers override to implement local-
130
+ * shadows-import and similar rules.
131
+ */
132
+ function withDefaultHooks(partial) {
133
+ return {
134
+ resolveImportTarget: partial.resolveImportTarget ?? (() => null),
135
+ expandsWildcardTo: partial.expandsWildcardTo ?? (() => []),
136
+ mergeBindings: partial.mergeBindings ??
137
+ ((existing, incoming) => [...existing, ...incoming]),
138
+ };
139
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Bridge between CLI-package per-language `ImportResolverFn`s and the
3
+ * shared `FinalizeHooks.resolveImportTarget` contract
4
+ * (RFC §5.2; Ring 2 PKG #922).
5
+ *
6
+ * The shared finalize algorithm (#915) asks one question:
7
+ *
8
+ * resolveImportTarget(targetRaw, fromFile, workspaceIndex): string | null
9
+ *
10
+ * The CLI already has 16 language-specific resolvers satisfying a
11
+ * richer signature:
12
+ *
13
+ * ImportResolverFn(rawImportPath, filePath, resolveCtx): ImportResult
14
+ *
15
+ * This module builds a dispatch adapter — one FinalizeHook implementation
16
+ * that looks up the file's language from its path and delegates to the
17
+ * right per-language resolver. Callers package per-language resolvers +
18
+ * a shared `ResolveCtx` into an opaque `ImportTargetWorkspace` and pass
19
+ * it as `workspaceIndex` to `finalizeScopeModel`.
20
+ *
21
+ * ## What's deliberately NOT here
22
+ *
23
+ * - **Re-implementation of any per-language resolver.** We wrap the
24
+ * existing `importResolver` field on each `LanguageProvider` — the
25
+ * same code path the legacy DAG uses today.
26
+ * - **Dynamic-import handling.** The shared finalize algorithm short-
27
+ * circuits `ParsedImport { kind: 'dynamic-unresolved' }` before
28
+ * calling `resolveImportTarget`, so the adapter never sees those.
29
+ * - **`importPathPreprocessor`.** Preprocessing belongs inside the
30
+ * provider's `interpretImport` hook (which writes the final
31
+ * `ParsedImport.targetRaw`). By the time finalize passes a
32
+ * `targetRaw` to this adapter, it is the string the provider wants
33
+ * resolved verbatim.
34
+ */
35
+ import { type SupportedLanguages, type WorkspaceIndex } from '../../_shared/index.js';
36
+ import type { ImportResolverFn, ResolveCtx } from './import-resolvers/types.js';
37
+ import type { LanguageProvider } from './language-provider.js';
38
+ /** A single language's resolver bundled with the context it needs. */
39
+ export interface LanguageResolverEntry {
40
+ readonly resolver: ImportResolverFn;
41
+ readonly ctx: ResolveCtx;
42
+ }
43
+ /**
44
+ * The opaque `workspaceIndex` shape recognized by
45
+ * `resolveImportTargetAcrossLanguages`. Built once per ingestion run via
46
+ * `buildImportTargetWorkspace`, threaded through `finalizeScopeModel`.
47
+ */
48
+ export interface ImportTargetWorkspace {
49
+ readonly perLanguage: ReadonlyMap<SupportedLanguages, LanguageResolverEntry>;
50
+ }
51
+ /**
52
+ * Build the workspace index from a map of language → provider. Providers
53
+ * whose `importResolver` is absent are silently skipped (no language will
54
+ * ever hit that branch at dispatch time).
55
+ *
56
+ * The `resolveCtx` is shared across all languages. Callers assemble it
57
+ * once per run (the existing pipeline already does this for the legacy
58
+ * DAG) and hand it to both the legacy resolution path and this factory.
59
+ */
60
+ export declare function buildImportTargetWorkspace(providers: ReadonlyMap<SupportedLanguages, LanguageProvider>, resolveCtx: ResolveCtx): ImportTargetWorkspace;
61
+ /**
62
+ * The FinalizeHooks-compatible implementation. Dispatches on `fromFile`'s
63
+ * extension → per-language resolver. Returns the first resolved file,
64
+ * or `null` if the resolver returns `null` or doesn't know about the
65
+ * language.
66
+ *
67
+ * Picks the first entry of `files[]` for both `'files'` and `'package'`
68
+ * result kinds — the legacy pipeline uses the whole array, but the
69
+ * shared `finalize()` hook contract is single-file. If the workspace
70
+ * later needs richer semantics (split-target packages), this is the
71
+ * single site to extend.
72
+ */
73
+ export declare function resolveImportTargetAcrossLanguages(targetRaw: string, fromFile: string, workspaceIndex: WorkspaceIndex): string | null;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Bridge between CLI-package per-language `ImportResolverFn`s and the
3
+ * shared `FinalizeHooks.resolveImportTarget` contract
4
+ * (RFC §5.2; Ring 2 PKG #922).
5
+ *
6
+ * The shared finalize algorithm (#915) asks one question:
7
+ *
8
+ * resolveImportTarget(targetRaw, fromFile, workspaceIndex): string | null
9
+ *
10
+ * The CLI already has 16 language-specific resolvers satisfying a
11
+ * richer signature:
12
+ *
13
+ * ImportResolverFn(rawImportPath, filePath, resolveCtx): ImportResult
14
+ *
15
+ * This module builds a dispatch adapter — one FinalizeHook implementation
16
+ * that looks up the file's language from its path and delegates to the
17
+ * right per-language resolver. Callers package per-language resolvers +
18
+ * a shared `ResolveCtx` into an opaque `ImportTargetWorkspace` and pass
19
+ * it as `workspaceIndex` to `finalizeScopeModel`.
20
+ *
21
+ * ## What's deliberately NOT here
22
+ *
23
+ * - **Re-implementation of any per-language resolver.** We wrap the
24
+ * existing `importResolver` field on each `LanguageProvider` — the
25
+ * same code path the legacy DAG uses today.
26
+ * - **Dynamic-import handling.** The shared finalize algorithm short-
27
+ * circuits `ParsedImport { kind: 'dynamic-unresolved' }` before
28
+ * calling `resolveImportTarget`, so the adapter never sees those.
29
+ * - **`importPathPreprocessor`.** Preprocessing belongs inside the
30
+ * provider's `interpretImport` hook (which writes the final
31
+ * `ParsedImport.targetRaw`). By the time finalize passes a
32
+ * `targetRaw` to this adapter, it is the string the provider wants
33
+ * resolved verbatim.
34
+ */
35
+ import { getLanguageFromFilename, } from '../../_shared/index.js';
36
+ /**
37
+ * Build the workspace index from a map of language → provider. Providers
38
+ * whose `importResolver` is absent are silently skipped (no language will
39
+ * ever hit that branch at dispatch time).
40
+ *
41
+ * The `resolveCtx` is shared across all languages. Callers assemble it
42
+ * once per run (the existing pipeline already does this for the legacy
43
+ * DAG) and hand it to both the legacy resolution path and this factory.
44
+ */
45
+ export function buildImportTargetWorkspace(providers, resolveCtx) {
46
+ const perLanguage = new Map();
47
+ for (const [lang, provider] of providers) {
48
+ if (provider.importResolver === undefined)
49
+ continue;
50
+ perLanguage.set(lang, { resolver: provider.importResolver, ctx: resolveCtx });
51
+ }
52
+ return { perLanguage };
53
+ }
54
+ /**
55
+ * The FinalizeHooks-compatible implementation. Dispatches on `fromFile`'s
56
+ * extension → per-language resolver. Returns the first resolved file,
57
+ * or `null` if the resolver returns `null` or doesn't know about the
58
+ * language.
59
+ *
60
+ * Picks the first entry of `files[]` for both `'files'` and `'package'`
61
+ * result kinds — the legacy pipeline uses the whole array, but the
62
+ * shared `finalize()` hook contract is single-file. If the workspace
63
+ * later needs richer semantics (split-target packages), this is the
64
+ * single site to extend.
65
+ */
66
+ export function resolveImportTargetAcrossLanguages(targetRaw, fromFile, workspaceIndex) {
67
+ const workspace = workspaceIndex;
68
+ if (workspace === undefined || workspace.perLanguage === undefined)
69
+ return null;
70
+ const lang = getLanguageFromFilename(fromFile);
71
+ if (lang === null)
72
+ return null;
73
+ const entry = workspace.perLanguage.get(lang);
74
+ if (entry === undefined)
75
+ return null;
76
+ let result;
77
+ try {
78
+ result = entry.resolver(targetRaw, fromFile, entry.ctx);
79
+ }
80
+ catch {
81
+ // Existing resolvers can throw on malformed inputs (e.g., Python
82
+ // relative paths above the workspace root). Swallow — the shared
83
+ // algorithm treats a null here as `linkStatus: 'unresolved'`, which
84
+ // is the right fallback.
85
+ return null;
86
+ }
87
+ if (result === null)
88
+ return null;
89
+ // Both `files` and `package` variants expose a `files` array; the
90
+ // package variant also carries `dirSuffix` which we ignore at the
91
+ // FinalizeHook boundary (single-file contract). Legacy consumers
92
+ // continue to see the full result via `importResolver` directly.
93
+ const first = result.files[0];
94
+ return first ?? null;
95
+ }
@@ -242,9 +242,15 @@ interface LanguageProviderConfig {
242
242
  * Providers that have not yet migrated continue to run through the
243
243
  * legacy DAG path (feature-flagged per `REGISTRY_PRIMARY_<LANG>`).
244
244
  *
245
+ * **Sync return.** Tree-sitter query execution and COBOL's regex
246
+ * tagger are both synchronous; no current or foreseeable provider
247
+ * needs async work inside this hook. The sync signature lets
248
+ * `parse-worker.ts` (#920) invoke it inline in its already-sync
249
+ * per-file loop without cascading `async` through the batch pipeline.
250
+ *
245
251
  * Default: undefined (language continues to use legacy DAG).
246
252
  */
247
- readonly emitScopeCaptures?: (sourceText: string, filePath: string) => Promise<readonly CaptureMatch[]>;
253
+ readonly emitScopeCaptures?: (sourceText: string, filePath: string) => readonly CaptureMatch[];
248
254
  /**
249
255
  * Interpret a raw `@import.statement` capture group into a `ParsedImport`.
250
256
  * The central finalize algorithm resolves `ParsedImport.targetRaw` to a
@@ -0,0 +1,59 @@
1
+ /**
2
+ * `ScopeResolutionIndexes` — the bundle of materialized indexes produced
3
+ * by the finalize-orchestrator (RFC #909 Ring 2 PKG #921) and attached
4
+ * to `MutableSemanticModel`.
5
+ *
6
+ * Produced by `finalizeScopeModel(parsedFiles, hooks)` in
7
+ * `finalize-orchestrator.ts`. Consumed by the resolution phase (future
8
+ * tickets) where `Registry.lookup` / `resolveTypeRef` query this bundle
9
+ * to answer call-resolution questions without re-walking any AST.
10
+ *
11
+ * ## Lifecycle
12
+ *
13
+ * 1. Pipeline collects `ParsedFile[]` from the parsing-processor (#920).
14
+ * 2. Pipeline invokes `finalizeScopeModel(parsedFiles, hooks)` →
15
+ * returns a `ScopeResolutionIndexes` (this interface).
16
+ * 3. Pipeline calls `model.attachScopeIndexes(indexes)` to stamp them
17
+ * onto the `MutableSemanticModel`. This is a **one-shot write**;
18
+ * subsequent calls throw. After attachment, the indexes are frozen
19
+ * at the type level (everything is `readonly`) and at runtime via
20
+ * `Object.freeze` on the bundle.
21
+ * 4. Resolution callers hold a `SemanticModel` reference and read
22
+ * `model.scopes` to query.
23
+ *
24
+ * ## Content
25
+ *
26
+ * - `scopeTree` / `moduleScopes` / `defs` / `qualifiedNames` — the
27
+ * four Ring 2 SHARED indexes built over per-file artifacts.
28
+ * - `methodDispatch` — MRO + implements materialized view (#914).
29
+ * - `imports` — finalized `ImportEdge[]` per module scope (`parsedImports`
30
+ * resolved through cross-file link + wildcard expansion).
31
+ * - `bindings` — merged bindings per module scope (local + import +
32
+ * wildcard + re-export), with the provider's precedence applied.
33
+ * - `referenceSites` — union of every file's pre-resolution usage
34
+ * facts. Consumed by the resolution phase (future) to emit
35
+ * `Reference` records into `ReferenceIndex`.
36
+ * - `stats` — coarse-grained counts from the shared finalize algorithm
37
+ * (total files/edges, linked vs unresolved, SCC topology).
38
+ *
39
+ * `ReferenceIndex` is deliberately NOT here — it is populated in a later
40
+ * phase (RFC §3.2 Phase 4 / Ring 2 PKG #925) and owned separately.
41
+ */
42
+ import type { BindingRef, DefIndex, FinalizedScc, FinalizeStats, ImportEdge, MethodDispatchIndex, ModuleScopeIndex, QualifiedNameIndex, ReferenceSite, ScopeId, ScopeTree } from '../../../_shared/index.js';
43
+ export interface ScopeResolutionIndexes {
44
+ readonly scopeTree: ScopeTree;
45
+ readonly defs: DefIndex;
46
+ readonly qualifiedNames: QualifiedNameIndex;
47
+ readonly moduleScopes: ModuleScopeIndex;
48
+ readonly methodDispatch: MethodDispatchIndex;
49
+ /** Finalized `ImportEdge[]` per module scope. */
50
+ readonly imports: ReadonlyMap<ScopeId, readonly ImportEdge[]>;
51
+ /** Merged bindings (local + imports + wildcards) per module scope. */
52
+ readonly bindings: ReadonlyMap<ScopeId, ReadonlyMap<string, readonly BindingRef[]>>;
53
+ /** Pre-resolution usage facts; consumed by the resolution phase. */
54
+ readonly referenceSites: readonly ReferenceSite[];
55
+ /** SCC condensation of the file-level import graph — callers that want
56
+ * parallel per-SCC processing in the resolution phase read this. */
57
+ readonly sccs: readonly FinalizedScc[];
58
+ readonly stats: FinalizeStats;
59
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * `ScopeResolutionIndexes` — the bundle of materialized indexes produced
3
+ * by the finalize-orchestrator (RFC #909 Ring 2 PKG #921) and attached
4
+ * to `MutableSemanticModel`.
5
+ *
6
+ * Produced by `finalizeScopeModel(parsedFiles, hooks)` in
7
+ * `finalize-orchestrator.ts`. Consumed by the resolution phase (future
8
+ * tickets) where `Registry.lookup` / `resolveTypeRef` query this bundle
9
+ * to answer call-resolution questions without re-walking any AST.
10
+ *
11
+ * ## Lifecycle
12
+ *
13
+ * 1. Pipeline collects `ParsedFile[]` from the parsing-processor (#920).
14
+ * 2. Pipeline invokes `finalizeScopeModel(parsedFiles, hooks)` →
15
+ * returns a `ScopeResolutionIndexes` (this interface).
16
+ * 3. Pipeline calls `model.attachScopeIndexes(indexes)` to stamp them
17
+ * onto the `MutableSemanticModel`. This is a **one-shot write**;
18
+ * subsequent calls throw. After attachment, the indexes are frozen
19
+ * at the type level (everything is `readonly`) and at runtime via
20
+ * `Object.freeze` on the bundle.
21
+ * 4. Resolution callers hold a `SemanticModel` reference and read
22
+ * `model.scopes` to query.
23
+ *
24
+ * ## Content
25
+ *
26
+ * - `scopeTree` / `moduleScopes` / `defs` / `qualifiedNames` — the
27
+ * four Ring 2 SHARED indexes built over per-file artifacts.
28
+ * - `methodDispatch` — MRO + implements materialized view (#914).
29
+ * - `imports` — finalized `ImportEdge[]` per module scope (`parsedImports`
30
+ * resolved through cross-file link + wildcard expansion).
31
+ * - `bindings` — merged bindings per module scope (local + import +
32
+ * wildcard + re-export), with the provider's precedence applied.
33
+ * - `referenceSites` — union of every file's pre-resolution usage
34
+ * facts. Consumed by the resolution phase (future) to emit
35
+ * `Reference` records into `ReferenceIndex`.
36
+ * - `stats` — coarse-grained counts from the shared finalize algorithm
37
+ * (total files/edges, linked vs unresolved, SCC topology).
38
+ *
39
+ * `ReferenceIndex` is deliberately NOT here — it is populated in a later
40
+ * phase (RFC §3.2 Phase 4 / Ring 2 PKG #925) and owned separately.
41
+ */
42
+ export {};
@@ -49,6 +49,7 @@ import type { TypeRegistry, MutableTypeRegistry } from './type-registry.js';
49
49
  import type { MethodRegistry, MutableMethodRegistry } from './method-registry.js';
50
50
  import type { FieldRegistry, MutableFieldRegistry } from './field-registry.js';
51
51
  import type { SymbolTableReader, SymbolTableWriter } from './symbol-table.js';
52
+ import type { ScopeResolutionIndexes } from './scope-resolution-indexes.js';
52
53
  /**
53
54
  * Aggregated read-only view of the semantic registries plus the nested
54
55
  * file/callable SymbolTable.
@@ -70,6 +71,19 @@ export interface SemanticModel {
70
71
  readonly methods: MethodRegistry;
71
72
  readonly fields: FieldRegistry;
72
73
  readonly symbols: SymbolTableReader;
74
+ /**
75
+ * Materialized scope-resolution indexes from RFC #909 Ring 2 PKG #921.
76
+ *
77
+ * `undefined` until the finalize-orchestrator attaches them. While
78
+ * `undefined`, the legacy DAG is the sole resolution surface; once set,
79
+ * resolvers whose language has `REGISTRY_PRIMARY_<LANG>=true` consult
80
+ * these indexes instead.
81
+ *
82
+ * The attach is a one-shot write (see `MutableSemanticModel`). Callers
83
+ * holding a read-only `SemanticModel` handle see either `undefined` or
84
+ * the final frozen bundle — never a half-populated view.
85
+ */
86
+ readonly scopes?: ScopeResolutionIndexes;
73
87
  }
74
88
  /** Mutable variant — exposes the MutableX registries, a Writer-typed
75
89
  * `symbols` facade, and a full-cascade reset. This is the interface
@@ -82,5 +96,16 @@ export interface MutableSemanticModel extends SemanticModel {
82
96
  readonly symbols: SymbolTableWriter;
83
97
  /** Clear all registries AND the nested SymbolTable. */
84
98
  clear(): void;
99
+ /**
100
+ * Stamp the finalize-orchestrator's output onto this model.
101
+ *
102
+ * **One-shot write.** Throws when called a second time — the indexes are
103
+ * meant to be materialized once per ingestion run. `Object.freeze` is
104
+ * applied to the attached bundle so consumers cannot mutate after attach.
105
+ *
106
+ * `clear()` resets the attached bundle back to `undefined`, enabling a
107
+ * fresh re-ingestion to attach a new bundle.
108
+ */
109
+ attachScopeIndexes(indexes: ScopeResolutionIndexes): void;
85
110
  }
86
111
  export declare const createSemanticModel: () => MutableSemanticModel;
@@ -85,6 +85,17 @@ export const createSemanticModel = () => {
85
85
  }
86
86
  return def;
87
87
  };
88
+ // Scope-resolution bundle slot. Starts `undefined`; populated by a
89
+ // one-shot `attachScopeIndexes(...)` from the finalize-orchestrator.
90
+ // Held inside the factory closure so the returned `SemanticModel`
91
+ // surface exposes it as a plain `readonly` property without a setter.
92
+ let attachedScopes;
93
+ const attachScopeIndexes = (indexes) => {
94
+ if (attachedScopes !== undefined) {
95
+ throw new Error('SemanticModel: scope indexes already attached. ' + 'Call `clear()` before re-attaching.');
96
+ }
97
+ attachedScopes = Object.freeze(indexes);
98
+ };
88
99
  // Cascade clear: single source of truth for "reset the entire model".
89
100
  // Wired into both `model.clear()` AND `model.symbols.clear()` so that a
90
101
  // caller holding only a SymbolTable reference can't leave the
@@ -95,6 +106,7 @@ export const createSemanticModel = () => {
95
106
  methods.clear();
96
107
  fields.clear();
97
108
  rawSymbols.clear();
109
+ attachedScopes = undefined;
98
110
  };
99
111
  // Writer-typed facade: exposes reads + add, but NO `clear` field.
100
112
  // Callers holding a `SemanticModel.symbols` reference cannot desync
@@ -115,6 +127,10 @@ export const createSemanticModel = () => {
115
127
  methods,
116
128
  fields,
117
129
  symbols,
130
+ get scopes() {
131
+ return attachedScopes;
132
+ },
118
133
  clear: cascadeClear,
134
+ attachScopeIndexes,
119
135
  };
120
136
  };
@@ -1,6 +1,7 @@
1
1
  import { KnowledgeGraph } from '../graph/types.js';
2
2
  import type { SymbolTableWriter, ExtractedHeritage } from './model/index.js';
3
3
  import { ASTCache } from './ast-cache.js';
4
+ import type { ParsedFile } from '../../_shared/index.js';
4
5
  import { WorkerPool } from './workers/worker-pool.js';
5
6
  import type { ExtractedImport, ExtractedCall, ExtractedAssignment, ExtractedRoute, ExtractedFetchCall, ExtractedDecoratorRoute, ExtractedToolDef, FileConstructorBindings, FileScopeBindings, ExtractedORMQuery } from './workers/parse-worker.js';
6
7
  export type FileProgressCallback = (current: number, total: number, filePath: string) => void;
@@ -16,6 +17,14 @@ export interface WorkerExtractedData {
16
17
  ormQueries: ExtractedORMQuery[];
17
18
  constructorBindings: FileConstructorBindings[];
18
19
  fileScopeBindings: FileScopeBindings[];
20
+ /**
21
+ * Per-file `ParsedFile` artifacts from the new scope-based resolution
22
+ * pipeline (RFC #909 Ring 2). Empty until a provider implements
23
+ * `emitScopeCaptures` — additive to the legacy DAG path. Aggregated
24
+ * from every worker chunk; consumed downstream by #921's
25
+ * finalize-orchestrator.
26
+ */
27
+ parsedFiles: ParsedFile[];
19
28
  }
20
29
  export declare const processParsing: (graph: KnowledgeGraph, files: {
21
30
  path: string;
@@ -34,6 +34,7 @@ const processParsingWithWorkers = async (graph, files, symbolTable, astCache, wo
34
34
  ormQueries: [],
35
35
  constructorBindings: [],
36
36
  fileScopeBindings: [],
37
+ parsedFiles: [],
37
38
  };
38
39
  const total = files.length;
39
40
  // Dispatch to worker pool — pool handles splitting into chunks and sub-batching
@@ -52,6 +53,7 @@ const processParsingWithWorkers = async (graph, files, symbolTable, astCache, wo
52
53
  const allORMQueries = [];
53
54
  const allConstructorBindings = [];
54
55
  const fileScopeBindingsByFile = [];
56
+ const allParsedFiles = [];
55
57
  for (const result of chunkResults) {
56
58
  for (const node of result.nodes) {
57
59
  graph.addNode({
@@ -98,6 +100,13 @@ const processParsingWithWorkers = async (graph, files, symbolTable, astCache, wo
98
100
  if (result.fileScopeBindings)
99
101
  for (const item of result.fileScopeBindings)
100
102
  fileScopeBindingsByFile.push(item);
103
+ // RFC #909 Ring 2: aggregate per-file scope artifacts. Tolerant of
104
+ // workers that don't emit the field yet (older worker builds or
105
+ // partial rollouts), since the additive contract means undefined =
106
+ // "this worker produced no ParsedFiles for this chunk".
107
+ if (result.parsedFiles)
108
+ for (const item of result.parsedFiles)
109
+ allParsedFiles.push(item);
101
110
  }
102
111
  // Merge and log skipped languages from workers
103
112
  const skippedLanguages = new Map();
@@ -126,6 +135,7 @@ const processParsingWithWorkers = async (graph, files, symbolTable, astCache, wo
126
135
  ormQueries: allORMQueries,
127
136
  constructorBindings: allConstructorBindings,
128
137
  fileScopeBindings: fileScopeBindingsByFile,
138
+ parsedFiles: allParsedFiles,
129
139
  };
130
140
  };
131
141
  // ============================================================================
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Bridge between a language provider's `emitScopeCaptures` hook and the
3
+ * `ScopeExtractor` (RFC #909 Ring 2 PKG #920).
4
+ *
5
+ * Extracted into its own module so it can be imported by test code
6
+ * without pulling in `parse-worker.ts` — which has a top-level
7
+ * `parentPort!.on('message', ...)` call that assumes a worker-thread
8
+ * context and throws on direct import.
9
+ *
10
+ * The bridge:
11
+ *
12
+ * 1. Short-circuits when the provider has NOT implemented
13
+ * `emitScopeCaptures`. Returns `undefined`; zero work done. This is
14
+ * the state of every language today — `ParsedFile` production stays
15
+ * dormant until a language migrates.
16
+ * 2. Invokes the hook + feeds its output to `ScopeExtractor.extract`.
17
+ * 3. **Swallows exceptions from either side.** A failure here returns
18
+ * `undefined` and emits a warning via `onWarn`; legacy parsing on
19
+ * the same file continues unaffected by the scope-extraction miss.
20
+ * Scope-based resolution is the new path under construction — it
21
+ * must not destabilize the legacy DAG.
22
+ */
23
+ import type { ParsedFile } from '../../_shared/index.js';
24
+ import type { LanguageProvider } from './language-provider.js';
25
+ /** Callback used to report scope-extraction warnings to the host (worker or direct). */
26
+ export type ScopeBridgeWarn = (message: string) => void;
27
+ /**
28
+ * Produce a `ParsedFile` for the given file, or `undefined` when the
29
+ * provider hasn't migrated / the extractor throws. Never propagates
30
+ * exceptions.
31
+ */
32
+ export declare function extractParsedFile(provider: LanguageProvider, sourceText: string, filePath: string, onWarn?: ScopeBridgeWarn): ParsedFile | undefined;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Bridge between a language provider's `emitScopeCaptures` hook and the
3
+ * `ScopeExtractor` (RFC #909 Ring 2 PKG #920).
4
+ *
5
+ * Extracted into its own module so it can be imported by test code
6
+ * without pulling in `parse-worker.ts` — which has a top-level
7
+ * `parentPort!.on('message', ...)` call that assumes a worker-thread
8
+ * context and throws on direct import.
9
+ *
10
+ * The bridge:
11
+ *
12
+ * 1. Short-circuits when the provider has NOT implemented
13
+ * `emitScopeCaptures`. Returns `undefined`; zero work done. This is
14
+ * the state of every language today — `ParsedFile` production stays
15
+ * dormant until a language migrates.
16
+ * 2. Invokes the hook + feeds its output to `ScopeExtractor.extract`.
17
+ * 3. **Swallows exceptions from either side.** A failure here returns
18
+ * `undefined` and emits a warning via `onWarn`; legacy parsing on
19
+ * the same file continues unaffected by the scope-extraction miss.
20
+ * Scope-based resolution is the new path under construction — it
21
+ * must not destabilize the legacy DAG.
22
+ */
23
+ import { extract as extractScope } from './scope-extractor.js';
24
+ /**
25
+ * Produce a `ParsedFile` for the given file, or `undefined` when the
26
+ * provider hasn't migrated / the extractor throws. Never propagates
27
+ * exceptions.
28
+ */
29
+ export function extractParsedFile(provider, sourceText, filePath, onWarn) {
30
+ if (provider.emitScopeCaptures === undefined)
31
+ return undefined;
32
+ try {
33
+ const captures = provider.emitScopeCaptures(sourceText, filePath);
34
+ return extractScope(captures, filePath, provider);
35
+ }
36
+ catch (err) {
37
+ const message = `scope extraction failed for ${filePath}: ${err instanceof Error ? err.message : String(err)}`;
38
+ if (onWarn !== undefined)
39
+ onWarn(message);
40
+ else
41
+ console.warn(message);
42
+ return undefined;
43
+ }
44
+ }
@@ -4,6 +4,7 @@ import { type MixedChainStep } from '../utils/call-analysis.js';
4
4
  import type { ConstructorBinding } from '../type-env.js';
5
5
  import type { NamedBinding } from '../named-bindings/types.js';
6
6
  import type { NodeLabel } from '../../../_shared/index.js';
7
+ import type { ParsedFile } from '../../../_shared/index.js';
7
8
  interface ParsedNode {
8
9
  id: string;
9
10
  label: string;
@@ -174,6 +175,14 @@ export interface ParseWorkerResult {
174
175
  constructorBindings: FileConstructorBindings[];
175
176
  /** All-scope type bindings from TypeEnv for BindingAccumulator (includes function-local). */
176
177
  fileScopeBindings: FileScopeBindings[];
178
+ /**
179
+ * Per-file `ParsedFile` artifacts from the new scope-based resolution
180
+ * pipeline (RFC #909 Ring 2). Empty unless the file's provider implements
181
+ * `emitScopeCaptures` — default for every language today, so this is
182
+ * additive and leaves the legacy DAG untouched. Consumed by #921's
183
+ * finalize-orchestrator.
184
+ */
185
+ parsedFiles: ParsedFile[];
177
186
  skippedLanguages: Record<string, number>;
178
187
  fileCount: number;
179
188
  }
@@ -43,6 +43,7 @@ import { generateId } from '../../../lib/utils.js';
43
43
  import { preprocessImportPath } from '../import-processor.js';
44
44
  import { extractVueScript, extractTemplateComponents, isVueSetupTopLevel, } from '../vue-sfc-extractor.js';
45
45
  import { buildMethodProps, arityForIdFromInfo, typeTagForId, constTagForId, buildCollisionGroups, } from '../utils/method-props.js';
46
+ import { extractParsedFile } from '../scope-extractor-bridge.js';
46
47
  // ============================================================================
47
48
  // Worker-local parser + language map
48
49
  // ============================================================================
@@ -415,6 +416,7 @@ const processBatch = (files, onProgress) => {
415
416
  ormQueries: [],
416
417
  constructorBindings: [],
417
418
  fileScopeBindings: [],
419
+ parsedFiles: [],
418
420
  skippedLanguages: {},
419
421
  fileCount: 0,
420
422
  };
@@ -1017,11 +1019,25 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
1017
1019
  console.warn(`Query execution failed for ${file.path}: ${err instanceof Error ? err.message : String(err)}`);
1018
1020
  continue;
1019
1021
  }
1022
+ const provider = getProvider(language);
1023
+ // RFC #909 Ring 2: produce a `ParsedFile` for the new scope-based
1024
+ // resolution pipeline. No-op (returns undefined) for every language
1025
+ // today — only fires once a provider implements `emitScopeCaptures`.
1026
+ // Runs BEFORE legacy extraction and its result is independent: a
1027
+ // failure here is caught inside `extractParsedFile` and does NOT
1028
+ // affect the legacy DAG path that follows.
1029
+ const parsedFile = extractParsedFile(provider, parseContent, file.path, (message) => {
1030
+ if (parentPort)
1031
+ parentPort.postMessage({ type: 'warning', message });
1032
+ else
1033
+ console.warn(message);
1034
+ });
1035
+ if (parsedFile !== undefined)
1036
+ result.parsedFiles.push(parsedFile);
1020
1037
  // Pre-pass: extract heritage from query matches to build parentMap for buildTypeEnv.
1021
1038
  // Heritage edges (EXTENDS/IMPLEMENTS) are created by heritage-processor which runs
1022
1039
  // in PARALLEL with call-processor, so the graph edges don't exist when buildTypeEnv
1023
1040
  // runs. This pre-pass makes parent class information available for type resolution.
1024
- const provider = getProvider(language);
1025
1041
  const fileParentMap = new Map();
1026
1042
  if (provider.heritageExtractor) {
1027
1043
  for (const match of matches) {
@@ -1804,6 +1820,7 @@ let accumulated = {
1804
1820
  ormQueries: [],
1805
1821
  constructorBindings: [],
1806
1822
  fileScopeBindings: [],
1823
+ parsedFiles: [],
1807
1824
  skippedLanguages: {},
1808
1825
  fileCount: 0,
1809
1826
  };
@@ -1830,6 +1847,7 @@ const mergeResult = (target, src) => {
1830
1847
  appendAll(target.ormQueries, src.ormQueries);
1831
1848
  appendAll(target.constructorBindings, src.constructorBindings);
1832
1849
  appendAll(target.fileScopeBindings, src.fileScopeBindings);
1850
+ appendAll(target.parsedFiles, src.parsedFiles);
1833
1851
  for (const [lang, count] of Object.entries(src.skippedLanguages)) {
1834
1852
  target.skippedLanguages[lang] = (target.skippedLanguages[lang] || 0) + count;
1835
1853
  }
@@ -1878,6 +1896,7 @@ parentPort.on('message', (msg) => {
1878
1896
  ormQueries: [],
1879
1897
  constructorBindings: [],
1880
1898
  fileScopeBindings: [],
1899
+ parsedFiles: [],
1881
1900
  skippedLanguages: {},
1882
1901
  fileCount: 0,
1883
1902
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.3-rc.12",
3
+ "version": "1.6.3-rc.14",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",