chore(scope-resolution): drop unused python/scopes.scm sibling

The file was documentation-only — the authoritative scope query is
the embedded `PYTHON_SCOPE_QUERY` constant in `python/query.ts`.
Nothing loaded the `.scm` at runtime, so it drifted from the code.
Remove it and update the four doc comments that pointed at it:

- language-provider.ts: "scopes.scm query" → "scope query (embedded
  in each language's query.ts)".
- languages/python.ts: capture-vocabulary pointer → query.ts.
- python/query.ts header: drop the "edit both together" note.
- python/receiver-binding.ts: "keeps the .scm declarative" → "keeps
  the embedded scope query declarative".
- scope/walkers.ts: "Python's scopes.scm" → "Python's scope query".

Historical plan docs under docs/plans/ still reference scopes.scm but
are frozen artifacts, not living documentation. C# never had a .scm
sibling, so no action needed there.
This commit is contained in:
Gergo Magyar 2026-04-21 16:55:22 +01:00
parent 8f5f7cf228
commit a58a478c06
6 changed files with 9 additions and 279 deletions

View file

@ -303,8 +303,9 @@ interface LanguageProviderConfig {
/**
* Emit scope captures from raw source, **pre-grouped per tree-sitter
* query match**. Tree-sitter-based providers run a `scopes.scm` query
* and emit one `CaptureMatch` per query match; standalone providers
* query match**. Tree-sitter-based providers run a scope query
* (embedded as a string constant in each language's `query.ts`) and
* emit one `CaptureMatch` per query match; standalone providers
* (COBOL) emit matches from a regex tagger. The return shape is
* parser-agnostic: the central `ScopeExtractor` consumes
* `CaptureMatch[]` without knowing which parser produced them.

View file

@ -92,7 +92,7 @@ export const pythonProvider = defineLanguage({
// ── RFC #909 Ring 3: scope-based resolution hooks (RFC §5) ──────────
// Python is the first migration. See ./python/index.ts for the
// full per-hook rationale and the canonical capture vocabulary in
// ./python/scopes.scm.
// ./python/query.ts (PYTHON_SCOPE_QUERY constant).
emitScopeCaptures: emitPythonScopeCaptures,
interpretImport: interpretPythonImport,
interpretTypeBinding: interpretPythonTypeBinding,

View file

@ -1,12 +1,7 @@
/**
* Tree-sitter query for Python scope captures (RFC §5.1).
*
* The `.scm` sibling file is the human-readable spec; this module
* mirrors it at runtime. **Edit both together** the unit/integration
* tests reference the embedded constant, and the file documents the
* contract.
*
* Also exposes lazy `Parser` and `Query` singletons so callers don't
* Exposes lazy `Parser` and `Query` singletons so callers don't
* pay tree-sitter init cost per file.
*/

View file

@ -4,9 +4,9 @@
*
* Tree-sitter can't easily express "the first parameter of a function
* defined directly inside a class body" via a single static query.
* Doing this in code keeps the `.scm` file declarative and lets us
* encode the `@classmethod` / `@staticmethod` decorator awareness that
* Python's runtime depends on.
* Doing this in code keeps the embedded scope query declarative and
* lets us encode the `@classmethod` / `@staticmethod` decorator
* awareness that Python's runtime depends on.
*/
import type { CaptureMatch } from 'gitnexus-shared';

View file

@ -1,266 +0,0 @@
; Tree-sitter Python query — RFC §5.1 captures for scope-based resolution
; (RFC #909 Ring 3, language: Python — first migration).
;
; Capture vocabulary consumed by the central `ScopeExtractor`:
;
; @scope.module — file root
; @scope.class — class body
; @scope.function — def / async def body (functions, methods, lambdas)
;
; @declaration.class + @declaration.name
; @declaration.function + @declaration.name
; @declaration.method + @declaration.name (functions inside class bodies)
; @declaration.variable + @declaration.name (module/class/function-level assignments)
;
; @import.statement — anchor for `interpretImport`. The hook reads
; the captured source text and tokenizes it into
; a `ParsedImport`. We do NOT decompose
; `import X, Y` here at query time — `interpretImport`
; splits multi-target statements into N matches.
;
; @type-binding.parameter + @type-binding.name + @type-binding.type
;
; @reference.call.free + @reference.name (e.g. `print(x)`)
; @reference.call.member + @reference.name + @reference.receiver
; (e.g. `obj.save()`)
;
; Python has NO block scope: `if`, `for`, `while`, `try`, `with`, `match`
; bodies do NOT introduce a new lexical scope (PEP 8 / language reference).
; We therefore do NOT emit `@scope.block` captures for those constructs;
; their contained declarations land in the enclosing function/class/module
; scope automatically (RFC §5.1 "transparent block" behavior).
;
; `@reference.call.constructor` is intentionally absent: Python has no
; `new` keyword. A call to a class is syntactically identical to a call to
; a free function; the registry decides constructor-vs-call by inspecting
; the resolved `def.type`. Stays out of the parser to avoid duplicating
; that logic in tree-sitter.
; ─── Scopes ────────────────────────────────────────────────────────────────
(module) @scope.module
(class_definition) @scope.class
(function_definition) @scope.function
; ─── Declarations: class / function ────────────────────────────────────────
(class_definition
name: (identifier) @declaration.name) @declaration.class
(function_definition
name: (identifier) @declaration.name) @declaration.function
; ─── Declarations: assignments (module-, class-, function-level variables)
;
; Note: tree-sitter-python parses both annotated and plain assignments as
; `(assignment left: ...)` — typed and untyped both surface here. The
; central extractor de-dupes by `nodeId` (file#line:col:type:name).
(assignment
left: (identifier) @declaration.name) @declaration.variable
; for-loop target — Python `for` does NOT introduce a new scope; the
; loop variable binds in the enclosing function/module scope.
(for_statement
left: (identifier) @declaration.name) @declaration.variable
; ─── Imports ───────────────────────────────────────────────────────────────
;
; The whole statement is the anchor — `interpretImport` decomposes it.
; We tag both shapes so the hook sees a single capture name (`@import.statement`).
(import_statement) @import.statement
(import_from_statement) @import.statement
; ─── Type bindings: parameter annotations ──────────────────────────────────
;
; `def f(x: User)` — `x` is bound to `User` in `f`'s scope.
;
; `interpretTypeBinding` reads `@type-binding.name` (the parameter name)
; and `@type-binding.type` (the annotation source text) to produce a
; `ParsedTypeBinding { boundName, rawTypeName, source: 'parameter-annotation' }`.
(typed_parameter
(identifier) @type-binding.name
type: (type) @type-binding.type) @type-binding.parameter
(typed_default_parameter
name: (identifier) @type-binding.name
type: (type) @type-binding.type) @type-binding.parameter
; ─── Type bindings: constructor-inferred assignments ───────────────────────
;
; `u = User("alice")` — `u`'s type is inferred from the RHS call's target.
; Python has no `new` keyword, so the pattern matches any `assignment`
; whose RHS is a `call` with a bare-identifier function (constructor-
; shaped). The registry resolves the raw name through the scope chain at
; lookup time, so imported classes, local classes, and aliased imports
; all work without query-time knowledge.
;
; Emits `source: 'constructor-inferred'`.
;
; Listed BEFORE the annotation pattern so `u: User = find()` — which
; matches both patterns — has the annotation (later-processed match)
; overwrite the constructor-inferred guess. Explicit user intent wins.
(assignment
left: (identifier) @type-binding.name
right: (call
function: (identifier) @type-binding.type)) @type-binding.constructor
; ─── Type bindings: variable annotations ───────────────────────────────────
;
; `u: User` or `u: User = some_value` — `u` is explicitly annotated. Both
; forms parse under tree-sitter-python as `(assignment left: type:)` with
; an optional `right:`. Module-, class-, and function-scope annotations
; all land here; scope attachment is handled by the central extractor
; via the anchor's innermost-containing scope.
;
; Emits `source: 'annotation'`.
(assignment
left: (identifier) @type-binding.name
type: (type) @type-binding.type) @type-binding.annotation
; For-loop iterable of a free-call result: `for u in get_users()` —
; binds `u → get_users` so the chain post-pass follows it through
; `get_users`'s return-type annotation (cross-file via
; `propagateImportedReturnTypes`).
(for_statement
left: (identifier) @type-binding.name
right: (call
function: (identifier) @type-binding.type)) @type-binding.alias
; for (i, u) in enumerate(X) and for i, u in enumerate(X) — bind the
; second tuple element to X. enumerate yields (int, X-element); the
; chain-follow unwraps X via generic-strip when X is an annotated
; collection.
(for_statement
left: (tuple_pattern
(identifier)
(identifier) @type-binding.name)
right: (call
function: (identifier) @_enum
arguments: (argument_list
(identifier) @type-binding.type))
(#eq? @_enum "enumerate")) @type-binding.alias
(for_statement
left: (pattern_list
(identifier)
(identifier) @type-binding.name)
right: (call
function: (identifier) @_enum
arguments: (argument_list
(identifier) @type-binding.type))
(#eq? @_enum "enumerate")) @type-binding.alias
; for k, v in d.items() — bind v to d. The chain-follow unwraps d's
; dict[K, V] annotation to V via the dict-aware stripGeneric.
(for_statement
left: (pattern_list
(identifier)
(identifier) @type-binding.name)
right: (call
function: (attribute
object: (identifier) @type-binding.type
attribute: (identifier) @_items))
(#eq? @_items "items")) @type-binding.alias
(for_statement
left: (tuple_pattern
(identifier)
(identifier) @type-binding.name)
right: (call
function: (attribute
object: (identifier) @type-binding.type
attribute: (identifier) @_items))
(#eq? @_items "items")) @type-binding.alias
; for i, (k, v) in enumerate(d.items()) — nested tuple destructuring.
(for_statement
left: (pattern_list
(identifier)
(tuple_pattern
(identifier)
(identifier) @type-binding.name))
right: (call
function: (identifier) @_enum
arguments: (argument_list
(call
function: (attribute
object: (identifier) @type-binding.type
attribute: (identifier) @_items))))
(#eq? @_enum "enumerate")
(#eq? @_items "items")) @type-binding.alias
; for i, k, v in enumerate(d.items()) — 3-var flat destructuring.
(for_statement
left: (pattern_list
(identifier)
(identifier)
(identifier) @type-binding.name)
right: (call
function: (identifier) @_enum
arguments: (argument_list
(call
function: (attribute
object: (identifier) @type-binding.type
attribute: (identifier) @_items))))
(#eq? @_enum "enumerate")
(#eq? @_items "items")) @type-binding.alias
; for u in self.X — heuristic: bind u to X (the attribute name).
; The chain-follow resolves X via the enclosing method's parameter
; typeBinding. Supports fixtures that reference `self.X` as a stand-in
; for a parameter X (matches legacy DAG fallback behavior).
(for_statement
left: (identifier) @type-binding.name
right: (attribute
object: (identifier) @_self
attribute: (identifier) @type-binding.type)
(#eq? @_self "self")) @type-binding.alias
; for v in d.values() — bind v to d (dict-strip yields value type).
(for_statement
left: (identifier) @type-binding.name
right: (call
function: (attribute
object: (identifier) @type-binding.type
attribute: (identifier) @_values))
(#eq? @_values "values")) @type-binding.alias
; ─── Type bindings: function return-type annotations ─────────────────────
;
; `def get_user() -> User:` — binds the function's NAME to its return
; type in the enclosing scope. Combined with the constructor-inferred +
; chain-follow path, `u = get_user()` then resolves `u: User` cross-
; call. Python provider hoists the binding via `pythonBindingScopeFor`
; to the function's parent scope so callers in module/class scope see it.
(function_definition
name: (identifier) @type-binding.name
return_type: (type) @type-binding.type) @type-binding.return
; ─── References: calls ─────────────────────────────────────────────────────
;
; Free call: `print(x)` — function is a bare identifier
; Member call: `obj.save()` — function is an attribute access
(call
function: (identifier) @reference.name) @reference.call.free
(call
function: (attribute
object: (_) @reference.receiver
attribute: (identifier) @reference.name)) @reference.call.member
; Attribute write: `obj.name = "x"` — emits ACCESSES (write) edge from
; the enclosing function to the field on obj's class.
(assignment
left: (attribute
object: (_) @reference.receiver
attribute: (identifier) @reference.name)) @reference.write.member

View file

@ -159,7 +159,7 @@ export function populateClassOwnedMembers(parsed: ParsedFile): void {
// when the def sits inside a class. Without this, two classes in the
// same file that share a method name collide at the graph-bridge lookup
// (`node-lookup.ts` keys by (filePath, qualifiedName) and falls back to
// simple name only). Python's `scopes.scm` doesn't emit
// simple name only). Python's scope query doesn't emit
// `@declaration.qualified_name` for nested methods, so the finalized
// defs arrive here with simple names — we stamp the qualifier while
// we're already walking class scopes for ownerId.