brainclaw 1.9.1 → 1.10.1
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.
- package/README.md +78 -25
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli.js +18 -1
- package/dist/commands/code-map.js +129 -0
- package/dist/commands/codev.js +7 -0
- package/dist/commands/dispatch-watch.js +1 -1
- package/dist/commands/doctor.js +3 -5
- package/dist/commands/loops-handlers.js +4 -1
- package/dist/commands/mcp-read-handlers.js +8 -0
- package/dist/commands/mcp.js +121 -1
- package/dist/commands/metrics.js +0 -1
- package/dist/commands/release-claims.js +1 -1
- package/dist/commands/run-profile.js +3 -2
- package/dist/commands/sequence.js +1 -1
- package/dist/commands/switch.js +100 -89
- package/dist/commands/sync.js +1 -1
- package/dist/commands/upgrade.js +0 -7
- package/dist/core/agent-context.js +1 -1
- package/dist/core/agent-files.js +13 -2
- package/dist/core/agent-integrations.js +3 -3
- package/dist/core/agent-registry.js +2 -2
- package/dist/core/assignments.js +12 -0
- package/dist/core/brainclaw-version.js +2 -2
- package/dist/core/code-map/backend.js +176 -0
- package/dist/core/code-map/core.js +81 -0
- package/dist/core/code-map/drafts.js +2 -0
- package/dist/core/code-map/extractor.js +29 -0
- package/dist/core/code-map/finalizer.js +191 -0
- package/dist/core/code-map/freshness.js +144 -0
- package/dist/core/code-map/ids.js +0 -0
- package/dist/core/code-map/importable.js +35 -0
- package/dist/core/code-map/indexes.js +197 -0
- package/dist/core/code-map/lang/java/imports.scm +17 -0
- package/dist/core/code-map/lang/java/index.js +254 -0
- package/dist/core/code-map/lang/java/tags.scm +48 -0
- package/dist/core/code-map/lang/php/imports.scm +21 -0
- package/dist/core/code-map/lang/php/index.js +251 -0
- package/dist/core/code-map/lang/php/tags.scm +44 -0
- package/dist/core/code-map/lang/provider.js +9 -0
- package/dist/core/code-map/lang/providers.js +24 -0
- package/dist/core/code-map/lang/python/imports.scm +90 -0
- package/dist/core/code-map/lang/python/index.js +364 -0
- package/dist/core/code-map/lang/python/tags.scm +81 -0
- package/dist/core/code-map/lang/query-runtime.js +374 -0
- package/dist/core/code-map/lang/registry.js +125 -0
- package/dist/core/code-map/lang/typescript/imports.scm +90 -0
- package/dist/core/code-map/lang/typescript/index.js +306 -0
- package/dist/core/code-map/lang/typescript/tags.js.scm +106 -0
- package/dist/core/code-map/lang/typescript/tags.scm +151 -0
- package/dist/core/code-map/lock.js +210 -0
- package/dist/core/code-map/materialized.js +51 -0
- package/dist/core/code-map/memory-reader.js +59 -0
- package/dist/core/code-map/paths.js +53 -0
- package/dist/core/code-map/query.js +599 -0
- package/dist/core/code-map/refresh.js +0 -0
- package/dist/core/code-map/resolve.js +177 -0
- package/dist/core/code-map/store.js +206 -0
- package/dist/core/code-map/types.js +293 -0
- package/dist/core/code-map/vocabulary.js +57 -0
- package/dist/core/code-map/wasm-loader.js +294 -0
- package/dist/core/code-map/work-section.js +206 -0
- package/dist/core/codev-rounds.js +4 -0
- package/dist/core/context.js +1 -1
- package/dist/core/cross-project.js +1 -1
- package/dist/core/dispatcher.js +0 -2
- package/dist/core/entity-operations.js +0 -3
- package/dist/core/execution-adapters.js +11 -10
- package/dist/core/execution-profile.js +58 -0
- package/dist/core/facade-schema.js +9 -0
- package/dist/core/ids.js +1 -1
- package/dist/core/instruction-templates.js +2 -0
- package/dist/core/instructions.js +0 -1
- package/dist/core/loops/lock.js +0 -3
- package/dist/core/mcp-command-resolution.js +3 -1
- package/dist/core/protocol-skills.js +5 -3
- package/dist/core/security-detectors.js +2 -2
- package/dist/core/security-extract.js +2 -2
- package/dist/core/store-resolution.js +41 -4
- package/dist/facts.js +9 -5
- package/dist/facts.json +8 -4
- package/dist/vendor/web-tree-sitter/tree-sitter.js +3980 -0
- package/dist/vendor/web-tree-sitter/tree-sitter.wasm +0 -0
- package/dist/wasm/tree-sitter-java.wasm +0 -0
- package/dist/wasm/tree-sitter-javascript.wasm +0 -0
- package/dist/wasm/tree-sitter-php.wasm +0 -0
- package/dist/wasm/tree-sitter-python.wasm +0 -0
- package/dist/wasm/tree-sitter-tsx.wasm +0 -0
- package/dist/wasm/tree-sitter-typescript.wasm +0 -0
- package/dist/wasm/tree-sitter.wasm +0 -0
- package/docs/cli.md +46 -8
- package/docs/code-map.md +209 -0
- package/docs/integrations/mcp.md +13 -6
- package/docs/mcp-schema-changelog.md +7 -3
- package/docs/quickstart.md +1 -1
- package/package.json +11 -6
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
; Brainclaw Code Map — Python IMPORTS query (curated, vendored).
|
|
2
|
+
; Grammar: tree-sitter-python. See ./README.md for the capture-name convention.
|
|
3
|
+
;
|
|
4
|
+
; Python has NO export statement, so this asset emits imports ONLY (no @export.name).
|
|
5
|
+
; Capabilities declare T2.imports = complete (specifiers); resolution is P1c.
|
|
6
|
+
;
|
|
7
|
+
; Capture -> draft mapping performed by the generic query-runtime:
|
|
8
|
+
; @import.source -> ImportDraft.source (the runtime strips surrounding quotes;
|
|
9
|
+
; Python module names are bare identifiers/dotted_names, so
|
|
10
|
+
; there is nothing to strip — the text passes through verbatim,
|
|
11
|
+
; which is exactly what relative-import dots `.` / `..pkg`
|
|
12
|
+
; need). ALSO anchors ImportDraft.span = enclosing
|
|
13
|
+
; import_statement / import_from_statement.
|
|
14
|
+
; @import.named.name -> a source-side imported name (the `from x import NAME` target,
|
|
15
|
+
; NOT the local alias; `*` for a wildcard import)
|
|
16
|
+
; @import.default.name -> (unused for Python — no default-import concept)
|
|
17
|
+
; @import.namespace.name -> (unused for Python — `import x as y` is a source-side
|
|
18
|
+
; module node, not a namespace specifier)
|
|
19
|
+
;
|
|
20
|
+
; MULTI-SOURCE awareness (spec §3.3 / §6): the runtime groups module nodes PER captured
|
|
21
|
+
; @import.source NODE, not per enclosing statement. So `import a, b` — one statement with
|
|
22
|
+
; two `name:` children — yields TWO @import.source captures and therefore TWO module
|
|
23
|
+
; nodes. For a `from x import a, b` statement the single module_name source node is
|
|
24
|
+
; captured once per imported `name:` child across N matches; the runtime accumulates all
|
|
25
|
+
; the names onto the one source node (keyed by node id). The span/ordinal stay anchored
|
|
26
|
+
; on the enclosing statement.
|
|
27
|
+
|
|
28
|
+
; ===========================================================================
|
|
29
|
+
; `import` STATEMENTS (import x / import x.y / import a, b / import x as y)
|
|
30
|
+
; ===========================================================================
|
|
31
|
+
|
|
32
|
+
; import x / import x.y / import a, b
|
|
33
|
+
; Each `name:` child that is a bare dotted_name is one module source. A multi-source
|
|
34
|
+
; statement (`import a, b`) has multiple `name:` children => multiple matches =>
|
|
35
|
+
; multiple @import.source captures => multiple module nodes.
|
|
36
|
+
(import_statement
|
|
37
|
+
name: (dotted_name) @import.source)
|
|
38
|
+
|
|
39
|
+
; import x as y -> source-side module is `x`; the alias `y` is ignored (imported
|
|
40
|
+
; names are source-side). Capture the INNER dotted_name only (NOT the aliased_import).
|
|
41
|
+
(import_statement
|
|
42
|
+
name: (aliased_import
|
|
43
|
+
name: (dotted_name) @import.source))
|
|
44
|
+
|
|
45
|
+
; ===========================================================================
|
|
46
|
+
; `from … import …` STATEMENTS
|
|
47
|
+
; ===========================================================================
|
|
48
|
+
|
|
49
|
+
; from x import a / from x.y import a, b
|
|
50
|
+
; One match per imported `name:` child; all share the single module_name source node,
|
|
51
|
+
; so the runtime accumulates a, b onto module `x` (grouped by source-node id).
|
|
52
|
+
(import_from_statement
|
|
53
|
+
module_name: (dotted_name) @import.source
|
|
54
|
+
name: (dotted_name) @import.named.name)
|
|
55
|
+
|
|
56
|
+
; from x import a as c -> source-side imported name is `a` (alias `c` ignored).
|
|
57
|
+
(import_from_statement
|
|
58
|
+
module_name: (dotted_name) @import.source
|
|
59
|
+
name: (aliased_import
|
|
60
|
+
name: (dotted_name) @import.named.name))
|
|
61
|
+
|
|
62
|
+
; from x import * -> the wildcard_import node's text is `*`, captured as the name.
|
|
63
|
+
(import_from_statement
|
|
64
|
+
module_name: (dotted_name) @import.source
|
|
65
|
+
(wildcard_import) @import.named.name)
|
|
66
|
+
|
|
67
|
+
; ===========================================================================
|
|
68
|
+
; RELATIVE `from … import …` (from . import z / from ..pkg import a, b / *)
|
|
69
|
+
;
|
|
70
|
+
; The relative_import node's TEXT is the verbatim specifier WITH its leading dots
|
|
71
|
+
; (`.`, `..`, `..pkg`) — that text IS how the relative-import level survives without a
|
|
72
|
+
; durable-attribute field (spec §6); P1c resolution consumes the dots. Capturing the
|
|
73
|
+
; relative_import node directly as @import.source preserves it verbatim.
|
|
74
|
+
; ===========================================================================
|
|
75
|
+
|
|
76
|
+
; from . import z / from ..pkg import a, b
|
|
77
|
+
(import_from_statement
|
|
78
|
+
module_name: (relative_import) @import.source
|
|
79
|
+
name: (dotted_name) @import.named.name)
|
|
80
|
+
|
|
81
|
+
; from .pkg import a as c
|
|
82
|
+
(import_from_statement
|
|
83
|
+
module_name: (relative_import) @import.source
|
|
84
|
+
name: (aliased_import
|
|
85
|
+
name: (dotted_name) @import.named.name))
|
|
86
|
+
|
|
87
|
+
; from . import *
|
|
88
|
+
(import_from_statement
|
|
89
|
+
module_name: (relative_import) @import.source
|
|
90
|
+
(wildcard_import) @import.named.name)
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Map P1b — PythonProvider (provider #2; cadrage §5/§6).
|
|
3
|
+
*
|
|
4
|
+
* Owns `.py` (runtime lang `python`). `extractDraft` delegates to the generic
|
|
5
|
+
* query-runtime; the curated `tags.scm`/`imports.scm` (this dir) drive structural
|
|
6
|
+
* extraction. `refine()` carries what the tree-sitter queries CANNOT express
|
|
7
|
+
* syntactically (cadrage §5, all provider-local):
|
|
8
|
+
* - class methods: a `function_definition` directly in a `class_definition` body →
|
|
9
|
+
* subtype `method` (`__init__` included). Identity span stays the
|
|
10
|
+
* `function_definition` (decorators excluded — the query anchors the inner node).
|
|
11
|
+
* - nested defs (a `function_definition` NOT directly owned by a class body) →
|
|
12
|
+
* stay `function`.
|
|
13
|
+
* - decorator-driven: `@property` → `property`; `@staticmethod`/`@classmethod` →
|
|
14
|
+
* `method`. Decorators are NON-emitting (no node, no persisted attribute).
|
|
15
|
+
* - `async def` → `function`/`method` by context (async is classification-only;
|
|
16
|
+
* NOT persisted — there is no durable attribute field).
|
|
17
|
+
* - module-level `UPPER_CASE` simple/annotated assignment → `constant`
|
|
18
|
+
* (else `variable`; class/instance attrs are NOT symbols, never constants).
|
|
19
|
+
*
|
|
20
|
+
* NO exports edges — Python has no export statement (capabilities: T2 = imports).
|
|
21
|
+
*
|
|
22
|
+
* Identity is owned by the CORE finalizer — this provider mints NO ids. The grammar
|
|
23
|
+
* is loaded through the SHARED engine glue (`loadGrammarWasm` → same initialized
|
|
24
|
+
* web-tree-sitter instance as js-ts), NEVER a fresh `web-tree-sitter` import
|
|
25
|
+
* (trp_8df65ab7).
|
|
26
|
+
*/
|
|
27
|
+
import crypto from 'node:crypto';
|
|
28
|
+
import fs from 'node:fs';
|
|
29
|
+
import path from 'node:path';
|
|
30
|
+
import { fileURLToPath } from 'node:url';
|
|
31
|
+
import { loadGrammarWasm, grammarHashForWasm } from '../../wasm-loader.js';
|
|
32
|
+
import { extractWithQueries } from '../query-runtime.js';
|
|
33
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
34
|
+
/** The python grammar .wasm: dist basename + node_modules devDep fallback spec. */
|
|
35
|
+
const PY_WASM_BASENAME = 'tree-sitter-python.wasm';
|
|
36
|
+
const PY_WASM_NODE_MODULES_SPEC = 'tree-sitter-wasms/out/tree-sitter-python.wasm';
|
|
37
|
+
const PY_GRAMMAR_NAME = 'tree-sitter-python';
|
|
38
|
+
/** Resolve a vendored `.scm` next to this module (dist) or from the source tree. */
|
|
39
|
+
function readScm(basename) {
|
|
40
|
+
// Published / dist runtime: this module is dist/core/code-map/lang/python/index.js
|
|
41
|
+
// and the build copies the .scm assets alongside it (copy-code-map-wasm.mjs).
|
|
42
|
+
const local = path.join(HERE, basename);
|
|
43
|
+
if (fs.existsSync(local))
|
|
44
|
+
return fs.readFileSync(local, 'utf-8');
|
|
45
|
+
// From-source / dist-test fallback: tsc emits to dist[-test]/... but does NOT copy
|
|
46
|
+
// .scm, so walk up to the repo root (the dir holding package.json) and read the
|
|
47
|
+
// curated asset from src/core/code-map/lang/python/.
|
|
48
|
+
let dir = HERE;
|
|
49
|
+
for (let i = 0; i < 12; i++) {
|
|
50
|
+
if (fs.existsSync(path.join(dir, 'package.json'))) {
|
|
51
|
+
const fromSrc = path.join(dir, 'src', 'core', 'code-map', 'lang', 'python', basename);
|
|
52
|
+
if (fs.existsSync(fromSrc))
|
|
53
|
+
return fs.readFileSync(fromSrc, 'utf-8');
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
const parent = path.dirname(dir);
|
|
57
|
+
if (parent === dir)
|
|
58
|
+
break;
|
|
59
|
+
dir = parent;
|
|
60
|
+
}
|
|
61
|
+
throw new Error(`code-map: could not locate query asset ${basename} (from ${HERE})`);
|
|
62
|
+
}
|
|
63
|
+
function sha256(s) {
|
|
64
|
+
return `sha256:${crypto.createHash('sha256').update(s, 'utf-8').digest('hex')}`;
|
|
65
|
+
}
|
|
66
|
+
// Load the curated query assets once at module init.
|
|
67
|
+
const TAGS = readScm('tags.scm');
|
|
68
|
+
const IMPORTS = readScm('imports.scm');
|
|
69
|
+
const TAGS_HASH = sha256(TAGS);
|
|
70
|
+
const IMPORTS_HASH = sha256(IMPORTS);
|
|
71
|
+
const parser = {
|
|
72
|
+
grammarForLang: () => loadGrammarWasm(PY_WASM_BASENAME, PY_WASM_NODE_MODULES_SPEC),
|
|
73
|
+
grammarNameForLang: () => PY_GRAMMAR_NAME,
|
|
74
|
+
grammarHashForLang: () => grammarHashForWasm(PY_WASM_BASENAME, PY_WASM_NODE_MODULES_SPEC),
|
|
75
|
+
};
|
|
76
|
+
const queries = {
|
|
77
|
+
tags: {
|
|
78
|
+
name: 'tags',
|
|
79
|
+
sourceForLang: () => TAGS,
|
|
80
|
+
hashForLang: () => TAGS_HASH,
|
|
81
|
+
},
|
|
82
|
+
imports: {
|
|
83
|
+
name: 'imports',
|
|
84
|
+
sourceForLang: () => IMPORTS,
|
|
85
|
+
hashForLang: () => IMPORTS_HASH,
|
|
86
|
+
},
|
|
87
|
+
// Python import statement node types (cadrage §3 / Codex R1): `import a, b` is an
|
|
88
|
+
// `import_statement`; `from x import …` is an `import_from_statement`. Python has
|
|
89
|
+
// no export statement. PROVIDER-LOCAL — runtime gets this per file, no registry.
|
|
90
|
+
// (Same set the old module-global carried for Python → byte-identical output.)
|
|
91
|
+
enclosingStatementNodeTypes: ['import_statement', 'import_from_statement'],
|
|
92
|
+
// P1b §3.4: the runtime drives capture→draft mapping off the HARD-CODED
|
|
93
|
+
// capture-name convention (query-runtime.ts). This captureMap is a declared
|
|
94
|
+
// MIRROR, validated by `assertCaptureMapConforms`. Every entry names a
|
|
95
|
+
// convention-recognized role; Python invents none.
|
|
96
|
+
captureMap: [
|
|
97
|
+
{ capture: 'definition.function.node', field: 'node', subtype: 'function' },
|
|
98
|
+
{ capture: 'definition.function.name', field: 'name' },
|
|
99
|
+
{ capture: 'definition.class.node', field: 'node', subtype: 'class' },
|
|
100
|
+
{ capture: 'definition.class.name', field: 'name' },
|
|
101
|
+
{ capture: 'definition.variable.node', field: 'node', subtype: 'variable' },
|
|
102
|
+
{ capture: 'definition.variable.name', field: 'name' },
|
|
103
|
+
{ capture: 'import.source', field: 'source' },
|
|
104
|
+
{ capture: 'import.named.name', field: 'imported', optional: true },
|
|
105
|
+
],
|
|
106
|
+
};
|
|
107
|
+
const vocabulary = {
|
|
108
|
+
nodeSubtypes: ['function', 'method', 'class', 'variable', 'constant', 'property'],
|
|
109
|
+
edgeKinds: ['contains', 'defines', 'imports'],
|
|
110
|
+
captureMap: queries.captureMap,
|
|
111
|
+
};
|
|
112
|
+
const capabilities = {
|
|
113
|
+
tiers: ['T1.definitions', 'T2.imports', 'T3.import_resolution'],
|
|
114
|
+
proven: {
|
|
115
|
+
'T1.definitions': true,
|
|
116
|
+
'T2.imports': true,
|
|
117
|
+
'T3.import_resolution': true,
|
|
118
|
+
'T4.tests_for': false,
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* P1c file-level import resolution for Python (cadrage v2 §"Python"). Maps an import
|
|
123
|
+
* source string — as the provider emits it on the `module` node `name` — to a single
|
|
124
|
+
* project-internal target FILE path, or `null` (stdlib / site-packages / unresolved).
|
|
125
|
+
*
|
|
126
|
+
* Source forms (verified against the provider's golden):
|
|
127
|
+
* - absolute dotted: `os`, `os.path`, `collections`, `pkg`, `a.b.c`
|
|
128
|
+
* - relative: leading dots count the package level — `.` / `..` (bare, from
|
|
129
|
+
* `from . import x`), `.mod`, `..pkg`, `..pkg.sub`.
|
|
130
|
+
*
|
|
131
|
+
* Resolution (PEP 328 file-level approximation):
|
|
132
|
+
* - relative: N leading dots ⇒ start at the importer's DIRECTORY and walk up (N-1)
|
|
133
|
+
* levels (1 dot = current package). The remaining dotted tail (after the dots) is
|
|
134
|
+
* a sub-path; empty tail (bare `.`/`..`) targets that package's `__init__.py`.
|
|
135
|
+
* - absolute dotted: resolve project-ROOT-relative (`a.b.c` → `a/b/c.py` |
|
|
136
|
+
* `a/b/c/__init__.py`). Stdlib / third-party names simply won't exist as project
|
|
137
|
+
* files → `null` (no edge). v1 does NOT model `src/`-layout sys.path roots — a
|
|
138
|
+
* documented limitation; symbol/submodule precision (`from . import sib` → the
|
|
139
|
+
* `sib` SUBMODULE rather than the package `__init__`) is the P1c-B follow-up.
|
|
140
|
+
*
|
|
141
|
+
* Candidate order is `module.py` before `package/__init__.py` (a regular module
|
|
142
|
+
* shadows a package of the same dotted name in CPython import order). The CORE
|
|
143
|
+
* verifies existence via `ctx.fileExists` and owns id/edge minting — this returns a
|
|
144
|
+
* path only (dec#108/#109).
|
|
145
|
+
*/
|
|
146
|
+
function resolvePyImport(source, fromPath, ctx) {
|
|
147
|
+
const candidates = [];
|
|
148
|
+
if (source.startsWith('.')) {
|
|
149
|
+
let dots = 0;
|
|
150
|
+
while (source[dots] === '.')
|
|
151
|
+
dots++;
|
|
152
|
+
const tail = source.slice(dots); // after the dots: '' | 'mod' | 'pkg.sub'
|
|
153
|
+
let base = path.posix.dirname(toPosixPy(fromPath)); // current package dir (1 dot)
|
|
154
|
+
let escapedRoot = false;
|
|
155
|
+
for (let up = 1; up < dots; up++) {
|
|
156
|
+
if (base === '' || base === '.' || base === '/') {
|
|
157
|
+
escapedRoot = true;
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
base = path.posix.dirname(base); // .. = parent, etc.
|
|
161
|
+
}
|
|
162
|
+
if (escapedRoot)
|
|
163
|
+
return null;
|
|
164
|
+
if (base === '.' || base === '/')
|
|
165
|
+
base = '';
|
|
166
|
+
const prefix = base ? `${base}/` : '';
|
|
167
|
+
if (tail === '') {
|
|
168
|
+
candidates.push(`${prefix}__init__.py`); // the package itself
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
const sub = tail.replace(/\./g, '/');
|
|
172
|
+
candidates.push(`${prefix}${sub}.py`, `${prefix}${sub}/__init__.py`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
// Absolute dotted module, resolved project-root-relative.
|
|
177
|
+
const sub = source.replace(/\./g, '/');
|
|
178
|
+
candidates.push(`${sub}.py`, `${sub}/__init__.py`);
|
|
179
|
+
}
|
|
180
|
+
for (const c of candidates) {
|
|
181
|
+
const norm = c.replace(/^\.\//, '');
|
|
182
|
+
if (ctx.fileExists(norm))
|
|
183
|
+
return norm;
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
function toPosixPy(p) {
|
|
188
|
+
return p.replace(/\\/g, '/');
|
|
189
|
+
}
|
|
190
|
+
function isDefSourceNode(v) {
|
|
191
|
+
return (typeof v === 'object' &&
|
|
192
|
+
v !== null &&
|
|
193
|
+
'node' in v &&
|
|
194
|
+
'nameNode' in v &&
|
|
195
|
+
typeof v.node === 'object');
|
|
196
|
+
}
|
|
197
|
+
const UPPER_RE = /^[A-Z_][A-Z0-9_]*$/;
|
|
198
|
+
/**
|
|
199
|
+
* True iff `defNode` (a `function_definition`) is a method — i.e. directly owned by
|
|
200
|
+
* a `class_definition` body. The grammar nests a def's statements under a `block`
|
|
201
|
+
* whose parent is the `class_definition`; a decorated method is wrapped in a
|
|
202
|
+
* `decorated_definition` (also inside that `block`). So walk: function_definition →
|
|
203
|
+
* (optional decorated_definition) → block → class_definition.
|
|
204
|
+
*/
|
|
205
|
+
function isClassMethod(defNode) {
|
|
206
|
+
let owner = defNode.parent;
|
|
207
|
+
// A decorated def sits inside a `decorated_definition` wrapper.
|
|
208
|
+
if (owner && owner.type === 'decorated_definition')
|
|
209
|
+
owner = owner.parent;
|
|
210
|
+
if (!owner || owner.type !== 'block')
|
|
211
|
+
return false;
|
|
212
|
+
const blockParent = owner.parent;
|
|
213
|
+
return !!blockParent && blockParent.type === 'class_definition';
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Collect decorator names applied to `defNode`. A decorated def's parent is a
|
|
217
|
+
* `decorated_definition` whose `decorator` children carry the applied names (e.g.
|
|
218
|
+
* `@property`, `@staticmethod`, `@app.route(...)`). We read each decorator's leading
|
|
219
|
+
* identifier/attribute text (best-effort) to classify property/staticmethod/classmethod.
|
|
220
|
+
*/
|
|
221
|
+
function decoratorNames(defNode) {
|
|
222
|
+
const wrapper = defNode.parent;
|
|
223
|
+
if (!wrapper || wrapper.type !== 'decorated_definition')
|
|
224
|
+
return [];
|
|
225
|
+
const names = [];
|
|
226
|
+
for (let i = 0; i < wrapper.namedChildCount; i++) {
|
|
227
|
+
const child = wrapper.namedChild(i);
|
|
228
|
+
if (!child || child.type !== 'decorator')
|
|
229
|
+
continue;
|
|
230
|
+
// decorator text is like `@property` / `@staticmethod` / `@app.route("/x")`.
|
|
231
|
+
// Strip the leading `@` and any call/attribute tail to get the head name.
|
|
232
|
+
const text = child.text.replace(/^@/, '').trim();
|
|
233
|
+
const head = text.split(/[(\s]/)[0]; // up to first `(` or whitespace
|
|
234
|
+
names.push(head);
|
|
235
|
+
}
|
|
236
|
+
return names;
|
|
237
|
+
}
|
|
238
|
+
export class PythonProvider {
|
|
239
|
+
id = 'python';
|
|
240
|
+
displayName = 'Python';
|
|
241
|
+
languages = ['python'];
|
|
242
|
+
extensions = ['.py'];
|
|
243
|
+
priority = 0;
|
|
244
|
+
version = '0.1.0';
|
|
245
|
+
parser = parser;
|
|
246
|
+
queries = queries;
|
|
247
|
+
vocabulary = vocabulary;
|
|
248
|
+
capabilities = capabilities;
|
|
249
|
+
/** `.py` → `python`. */
|
|
250
|
+
langForPath(_p) {
|
|
251
|
+
return 'python';
|
|
252
|
+
}
|
|
253
|
+
async extractDraft(input, _services) {
|
|
254
|
+
return extractDraftViaRuntime(this.id, input, this.parser.grammarForLang);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Reclassify subtypes the structural query cannot express (cadrage §5). Drafts-only.
|
|
258
|
+
* - function in a class body → method (decorator @property → property;
|
|
259
|
+
* @staticmethod/@classmethod → method); nested/top-level defs stay function.
|
|
260
|
+
* - module-level UPPER_CASE assignment → constant; else stays variable.
|
|
261
|
+
* `async` is never persisted (no attribute field) — it is classification-only and
|
|
262
|
+
* does not change the subtype beyond function/method by context.
|
|
263
|
+
*/
|
|
264
|
+
refine(draft, _ctx) {
|
|
265
|
+
const definitions = draft.definitions.map((d) => {
|
|
266
|
+
const src = isDefSourceNode(d.sourceNode) ? d.sourceNode : null;
|
|
267
|
+
if (d.subtype === 'function') {
|
|
268
|
+
if (!src)
|
|
269
|
+
return d;
|
|
270
|
+
const decos = decoratorNames(src.node);
|
|
271
|
+
// @property wins as a value-like member; @staticmethod/@classmethod are methods.
|
|
272
|
+
if (decos.includes('property'))
|
|
273
|
+
return setSubtype(d, 'property');
|
|
274
|
+
const inClass = isClassMethod(src.node);
|
|
275
|
+
if (decos.includes('staticmethod') || decos.includes('classmethod')) {
|
|
276
|
+
return setSubtype(d, 'method');
|
|
277
|
+
}
|
|
278
|
+
return inClass ? setSubtype(d, 'method') : d; // top-level / nested stay function
|
|
279
|
+
}
|
|
280
|
+
if (d.subtype === 'variable') {
|
|
281
|
+
// Module-level UPPER_CASE → constant; the tags.scm anchors variables under
|
|
282
|
+
// (module) only, so any captured variable IS module-level.
|
|
283
|
+
return UPPER_RE.test(d.name) ? setSubtype(d, 'constant') : d;
|
|
284
|
+
}
|
|
285
|
+
return d;
|
|
286
|
+
});
|
|
287
|
+
return { ...draft, definitions };
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* P1c file-level import resolution (T3). Returns at most one resolution per import:
|
|
291
|
+
* the resolved project-internal target file path, or `resolvedPath: null` when the
|
|
292
|
+
* specifier points outside the project (stdlib / site-packages / unresolved). The
|
|
293
|
+
* CORE pass filters out `null` paths and mints the `resolves_to` edge.
|
|
294
|
+
*/
|
|
295
|
+
async resolveImport(req, ctx) {
|
|
296
|
+
const resolvedPath = resolvePyImport(req.source, req.fromPath, ctx);
|
|
297
|
+
return [{ source: req.source, resolvedPath, confidence: resolvedPath ? 1.0 : 0 }];
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* P1c-B importability for Python (overrides the `exported`-based default).
|
|
301
|
+
* Python has no export statement — `node.exported` is always false — so a symbol
|
|
302
|
+
* is importable iff it is TOP-LEVEL in its module: no OTHER symbol's span STRICTLY
|
|
303
|
+
* contains it. (A class method / nested function is contained by its class/func and
|
|
304
|
+
* is therefore NOT importable as `from .mod import X`.) Cadrage D2: span containment
|
|
305
|
+
* is the SOUND mechanism — the finalizer's file→contains/defines edges are emitted
|
|
306
|
+
* for EVERY symbol (not just top-level), so they cannot prove top-level status.
|
|
307
|
+
* A symbol lacking a usable span is SKIPPED (can't prove top-level → never guess).
|
|
308
|
+
*/
|
|
309
|
+
isImportableSymbol(node, fileSymbols) {
|
|
310
|
+
if (node.kind !== 'symbol')
|
|
311
|
+
return false;
|
|
312
|
+
if (node.subtype === 'export')
|
|
313
|
+
return false; // defensive (Python never emits it)
|
|
314
|
+
const span = node.span;
|
|
315
|
+
if (!span)
|
|
316
|
+
return false;
|
|
317
|
+
for (const other of fileSymbols) {
|
|
318
|
+
if (other === node || other.id === node.id)
|
|
319
|
+
continue;
|
|
320
|
+
if (other.kind !== 'symbol')
|
|
321
|
+
continue;
|
|
322
|
+
if (other.span && spanStrictlyContains(other.span, span))
|
|
323
|
+
return false; // nested → not top-level
|
|
324
|
+
}
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/** True iff `outer` strictly contains `inner` (covers it on both ends, larger on ≥1). */
|
|
329
|
+
function spanStrictlyContains(outer, inner) {
|
|
330
|
+
const startsAtOrBefore = outer.start_line < inner.start_line ||
|
|
331
|
+
(outer.start_line === inner.start_line && outer.start_col <= inner.start_col);
|
|
332
|
+
const endsAtOrAfter = outer.end_line > inner.end_line ||
|
|
333
|
+
(outer.end_line === inner.end_line && outer.end_col >= inner.end_col);
|
|
334
|
+
if (!startsAtOrBefore || !endsAtOrAfter)
|
|
335
|
+
return false;
|
|
336
|
+
const strictlyLarger = outer.start_line < inner.start_line ||
|
|
337
|
+
outer.start_col < inner.start_col ||
|
|
338
|
+
outer.end_line > inner.end_line ||
|
|
339
|
+
outer.end_col > inner.end_col;
|
|
340
|
+
return strictlyLarger;
|
|
341
|
+
}
|
|
342
|
+
function setSubtype(d, subtype) {
|
|
343
|
+
return d.subtype === subtype ? d : { ...d, subtype };
|
|
344
|
+
}
|
|
345
|
+
async function extractDraftViaRuntime(providerId, input, grammarForLang) {
|
|
346
|
+
return extractWithQueries({
|
|
347
|
+
providerId,
|
|
348
|
+
lang: input.lang,
|
|
349
|
+
source: input.source,
|
|
350
|
+
sizeBytes: input.sizeBytes,
|
|
351
|
+
maxParseFileBytes: input.maxParseFileBytes,
|
|
352
|
+
maxQueryWaitMs: input.maxQueryWaitMs,
|
|
353
|
+
path: input.path,
|
|
354
|
+
grammarForLang,
|
|
355
|
+
tagsSource: TAGS,
|
|
356
|
+
tagsHash: TAGS_HASH,
|
|
357
|
+
importsSource: IMPORTS,
|
|
358
|
+
importsHash: IMPORTS_HASH,
|
|
359
|
+
enclosingStatementNodeTypes: queries.enclosingStatementNodeTypes,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
/** Singleton instance for registry wiring. */
|
|
363
|
+
export const pythonProvider = new PythonProvider();
|
|
364
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
; Brainclaw Code Map — Python DEFINITIONS query (curated, vendored).
|
|
2
|
+
; Grammar: tree-sitter-python (WASM vendored under dist/wasm/). See ./README.md for
|
|
3
|
+
; the capture-name convention, the Python→vocabulary mapping, and provenance.
|
|
4
|
+
;
|
|
5
|
+
; The generic query-runtime (lang/query-runtime.ts) maps each match's captures:
|
|
6
|
+
; @definition.<subtype>.node -> DefinitionDraft.span (the LEGACY IDENTITY span)
|
|
7
|
+
; @definition.<subtype>.name -> DefinitionDraft.name (identifier text + ordinal)
|
|
8
|
+
; @definition.<subtype>.exported -> presence ⇒ exported=true (UNUSED for Python:
|
|
9
|
+
; Python has no export statement, so this asset
|
|
10
|
+
; never emits it — every Python symbol is unexported)
|
|
11
|
+
;
|
|
12
|
+
; Structural subtypes emitted here: function | class | variable.
|
|
13
|
+
; - function_definition is captured UNIFORMLY as @definition.function (whether bare,
|
|
14
|
+
; decorated, top-level, nested, or sitting directly in a class body). The provider's
|
|
15
|
+
; refine() pass reclassifies:
|
|
16
|
+
; * a function_definition whose owner is a class body -> method
|
|
17
|
+
; (@property -> property; @staticmethod/@classmethod -> method)
|
|
18
|
+
; * everything else (top-level + nested defs) -> stays function
|
|
19
|
+
; refine() decides method-vs-function and decorator-driven subtypes because the
|
|
20
|
+
; structural query cannot inspect the enclosing-scope chain or decorator semantics.
|
|
21
|
+
; - module-level assignment is captured as @definition.variable; refine() narrows an
|
|
22
|
+
; all-UPPER_CASE target to `constant`.
|
|
23
|
+
;
|
|
24
|
+
; IDENTITY SPAN: every definition pattern anchors @definition.*.node on the INNER
|
|
25
|
+
; function_definition / class_definition node — NOT on any enclosing decorated_definition
|
|
26
|
+
; wrapper. A decorated def's identity span therefore EXCLUDES its decorators (spec §5),
|
|
27
|
+
; and the same single pattern matches the bare and the decorated forms (the inner node
|
|
28
|
+
; is identical in both trees), so no separate decorated pattern is needed.
|
|
29
|
+
|
|
30
|
+
; ---------------------------------------------------------------------------
|
|
31
|
+
; function / async-function definitions (def f(): ... / async def f(): ...)
|
|
32
|
+
;
|
|
33
|
+
; Matched at ANY depth (top-level, nested, class-body). `async def` is the same
|
|
34
|
+
; function_definition node (the grammar marks async with an `async` keyword child,
|
|
35
|
+
; not a distinct node type), so this one pattern covers sync + async; async-ness is
|
|
36
|
+
; classification-only and is NOT persisted (no durable attribute field — spec §5).
|
|
37
|
+
; refine() promotes class-body functions to `method`.
|
|
38
|
+
; ---------------------------------------------------------------------------
|
|
39
|
+
(function_definition
|
|
40
|
+
name: (identifier) @definition.function.name) @definition.function.node
|
|
41
|
+
|
|
42
|
+
; ---------------------------------------------------------------------------
|
|
43
|
+
; class definitions (class C: ... / class C(Base): ...)
|
|
44
|
+
; ---------------------------------------------------------------------------
|
|
45
|
+
(class_definition
|
|
46
|
+
name: (identifier) @definition.class.name) @definition.class.node
|
|
47
|
+
|
|
48
|
+
; ---------------------------------------------------------------------------
|
|
49
|
+
; module-level variables / constants
|
|
50
|
+
;
|
|
51
|
+
; Anchored under `module` (the root) so ONLY top-level assignments are emitted —
|
|
52
|
+
; class/instance attributes and locals are NOT symbols in P1b (spec §5). Covers both
|
|
53
|
+
; a plain assignment (`X = 1`) and an annotated assignment (`X: int = 1`); both are a
|
|
54
|
+
; single `assignment` node, the annotation living in its `type:` field. Only a simple
|
|
55
|
+
; `identifier` target is captured (tuple/attribute/subscript targets are not symbols).
|
|
56
|
+
; The @definition.variable.node identity span is the `assignment` node. refine()
|
|
57
|
+
; narrows an all-UPPER_CASE name to `constant`; everything else stays `variable`.
|
|
58
|
+
; ---------------------------------------------------------------------------
|
|
59
|
+
(module
|
|
60
|
+
(expression_statement
|
|
61
|
+
(assignment
|
|
62
|
+
left: (identifier) @definition.variable.name) @definition.variable.node))
|
|
63
|
+
|
|
64
|
+
; ---------------------------------------------------------------------------
|
|
65
|
+
; ERROR-root recovery (parity with the TS asset).
|
|
66
|
+
;
|
|
67
|
+
; When tree-sitter cannot parse a file cleanly it may promote a subtree to an `ERROR`
|
|
68
|
+
; node while still recovering well-formed declarations beneath it. Mirror the def/class
|
|
69
|
+
; patterns under an `(ERROR ...)` ancestor so top-level symbols in an error-recovered
|
|
70
|
+
; file are not silently dropped. (Functions/classes match at any depth above already,
|
|
71
|
+
; but anchoring these explicitly keeps content parity for the ERROR-root shape and
|
|
72
|
+
; documents the intent.) Module-level variables are intentionally NOT recovered under
|
|
73
|
+
; ERROR — without the `module` anchor a bare identifier-assignment match is ambiguous.
|
|
74
|
+
; ---------------------------------------------------------------------------
|
|
75
|
+
(ERROR
|
|
76
|
+
(function_definition
|
|
77
|
+
name: (identifier) @definition.function.name) @definition.function.node)
|
|
78
|
+
|
|
79
|
+
(ERROR
|
|
80
|
+
(class_definition
|
|
81
|
+
name: (identifier) @definition.class.name) @definition.class.node)
|