loomweave-plugin-python 1.0.0__tar.gz
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.
- loomweave_plugin_python-1.0.0/.gitignore +48 -0
- loomweave_plugin_python-1.0.0/PKG-INFO +73 -0
- loomweave_plugin_python-1.0.0/README.md +49 -0
- loomweave_plugin_python-1.0.0/plugin.toml +71 -0
- loomweave_plugin_python-1.0.0/pyproject.toml +104 -0
- loomweave_plugin_python-1.0.0/src/loomweave_plugin_python/__init__.py +3 -0
- loomweave_plugin_python-1.0.0/src/loomweave_plugin_python/__main__.py +15 -0
- loomweave_plugin_python-1.0.0/src/loomweave_plugin_python/call_resolver.py +65 -0
- loomweave_plugin_python-1.0.0/src/loomweave_plugin_python/entity_id.py +75 -0
- loomweave_plugin_python-1.0.0/src/loomweave_plugin_python/extractor.py +1312 -0
- loomweave_plugin_python-1.0.0/src/loomweave_plugin_python/py.typed +0 -0
- loomweave_plugin_python-1.0.0/src/loomweave_plugin_python/pyright_session.py +1655 -0
- loomweave_plugin_python-1.0.0/src/loomweave_plugin_python/qualname.py +48 -0
- loomweave_plugin_python-1.0.0/src/loomweave_plugin_python/reference_resolver.py +70 -0
- loomweave_plugin_python-1.0.0/src/loomweave_plugin_python/server.py +310 -0
- loomweave_plugin_python-1.0.0/src/loomweave_plugin_python/stdout_guard.py +62 -0
- loomweave_plugin_python-1.0.0/src/loomweave_plugin_python/wardline_descriptor.py +197 -0
- loomweave_plugin_python-1.0.0/tests/__init__.py +0 -0
- loomweave_plugin_python-1.0.0/tests/test_entity_id.py +212 -0
- loomweave_plugin_python-1.0.0/tests/test_extractor.py +1600 -0
- loomweave_plugin_python-1.0.0/tests/test_package.py +56 -0
- loomweave_plugin_python-1.0.0/tests/test_pyright_session.py +1093 -0
- loomweave_plugin_python-1.0.0/tests/test_qualname.py +138 -0
- loomweave_plugin_python-1.0.0/tests/test_round_trip.py +219 -0
- loomweave_plugin_python-1.0.0/tests/test_server.py +751 -0
- loomweave_plugin_python-1.0.0/tests/test_stdout_guard.py +54 -0
- loomweave_plugin_python-1.0.0/tests/test_wardline_descriptor.py +167 -0
- loomweave_plugin_python-1.0.0/uv.lock +1077 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
.filigree/
|
|
2
|
+
/target
|
|
3
|
+
**/*.rs.bk
|
|
4
|
+
Cargo.lock.bak
|
|
5
|
+
|
|
6
|
+
# SQLite working files (project-level .loomweave/ is tracked per ADR-005)
|
|
7
|
+
*.db-journal
|
|
8
|
+
*.db-wal
|
|
9
|
+
|
|
10
|
+
# Rust-analyzer / IDE caches
|
|
11
|
+
/.idea
|
|
12
|
+
/.vscode
|
|
13
|
+
|
|
14
|
+
# Python (plugins/python/)
|
|
15
|
+
.venv/
|
|
16
|
+
__pycache__/
|
|
17
|
+
*.egg-info/
|
|
18
|
+
.mypy_cache/
|
|
19
|
+
.pytest_cache/
|
|
20
|
+
.ruff_cache/
|
|
21
|
+
.coverage
|
|
22
|
+
htmlcov/
|
|
23
|
+
.claude/
|
|
24
|
+
|
|
25
|
+
.env
|
|
26
|
+
|
|
27
|
+
# Smoke-test result artifacts (per-run; archived separately at tag-cut)
|
|
28
|
+
tests/e2e/external-operator-smoke-results-*.md
|
|
29
|
+
|
|
30
|
+
# Generated skill fingerprint (rewritten by `loomweave install --skills`).
|
|
31
|
+
.agents/skills/loomweave-workflow/.fingerprint
|
|
32
|
+
|
|
33
|
+
# Loomweave runtime artifacts — the index DB, per-project instance fingerprint,
|
|
34
|
+
# and analyze lock change on every run, so they are not tracked
|
|
35
|
+
# (see .loomweave/.gitignore).
|
|
36
|
+
.loomweave/loomweave.db
|
|
37
|
+
.loomweave/instance_id
|
|
38
|
+
.loomweave/loomweave.lock
|
|
39
|
+
|
|
40
|
+
# Documentation site build output (mkdocs `site_dir`, web/mkdocs.yml).
|
|
41
|
+
/site-build/
|
|
42
|
+
|
|
43
|
+
# Filigree-managed docs — a running filigree process rewrites its managed
|
|
44
|
+
# instruction blocks in these every session; untracked to avoid diff churn
|
|
45
|
+
# (filigree regenerates them on demand).
|
|
46
|
+
AGENTS.md
|
|
47
|
+
CLAUDE.md
|
|
48
|
+
.agents/skills/filigree-workflow/SKILL.md
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: loomweave-plugin-python
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Loomweave Python language plugin — v1.0 release
|
|
5
|
+
Author-email: John Morrissey <qacona@gmail.com>
|
|
6
|
+
Classifier: Development Status :: 4 - Beta
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
|
+
Requires-Dist: packaging>=24
|
|
12
|
+
Requires-Dist: pyright==1.1.409
|
|
13
|
+
Requires-Dist: pyyaml>=6.0
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
16
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
17
|
+
Requires-Dist: pip-audit>=2.9; extra == 'dev'
|
|
18
|
+
Requires-Dist: pre-commit>=3.8; extra == 'dev'
|
|
19
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
21
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
22
|
+
Requires-Dist: types-pyyaml>=6.0; extra == 'dev'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# loomweave-plugin-python
|
|
26
|
+
|
|
27
|
+
The Python language plugin for [Loomweave](../../README.md). Extracts Python
|
|
28
|
+
entities from source files and serves them to the Loomweave core over the
|
|
29
|
+
JSON-RPC protocol defined in [WP2 L4](../../docs/implementation/sprint-1/wp2-plugin-host.md#l4--json-rpc-method-set--content-length-framing).
|
|
30
|
+
|
|
31
|
+
**Status**: Python structural extractor. It emits modules, classes, functions,
|
|
32
|
+
`contains`, `calls`, `references`, `imports`, and versioned entity signatures
|
|
33
|
+
for Stable Entity Identity (SEI) matching. It also reads Wardline's NG-25
|
|
34
|
+
trust-vocabulary descriptor without importing Wardline and emits source-observed
|
|
35
|
+
Wardline decorator metadata/tags on decorated entities when a descriptor is
|
|
36
|
+
available.
|
|
37
|
+
|
|
38
|
+
## Install (development)
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
python -m venv .venv
|
|
42
|
+
source .venv/bin/activate
|
|
43
|
+
pip install -e '.[dev]'
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
This places `loomweave-plugin-python` on your `$PATH` and installs the
|
|
47
|
+
dev-time toolchain (`ruff`, `mypy`, `pytest`, `pytest-cov`, `pre-commit`).
|
|
48
|
+
|
|
49
|
+
## ADR-023 tooling gates
|
|
50
|
+
|
|
51
|
+
Every commit must pass all four:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
ruff check plugins/python
|
|
55
|
+
ruff format --check plugins/python
|
|
56
|
+
mypy --strict plugins/python
|
|
57
|
+
pytest plugins/python
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
CI runs the same four gates in the `python-plugin` job.
|
|
61
|
+
|
|
62
|
+
## Design references
|
|
63
|
+
|
|
64
|
+
- [WP3 plan](../../docs/implementation/sprint-1/wp3-python-plugin.md) — task
|
|
65
|
+
ledger, lock-ins, and UQ resolutions.
|
|
66
|
+
- [ADR-003](../../docs/loomweave/adr/ADR-003-entity-id-scheme.md) — 3-segment
|
|
67
|
+
`EntityId` format this plugin produces.
|
|
68
|
+
- [ADR-018](../../docs/loomweave/adr/ADR-018-identity-reconciliation.md) —
|
|
69
|
+
cross-product identity join with Wardline.
|
|
70
|
+
- [ADR-022](../../docs/loomweave/adr/ADR-022-core-plugin-ontology.md) —
|
|
71
|
+
manifest schema and ontology-boundary enforcement.
|
|
72
|
+
- [ADR-023](../../docs/loomweave/adr/ADR-023-tooling-baseline.md) — the four
|
|
73
|
+
Python gates and the `pre-commit` setup.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# loomweave-plugin-python
|
|
2
|
+
|
|
3
|
+
The Python language plugin for [Loomweave](../../README.md). Extracts Python
|
|
4
|
+
entities from source files and serves them to the Loomweave core over the
|
|
5
|
+
JSON-RPC protocol defined in [WP2 L4](../../docs/implementation/sprint-1/wp2-plugin-host.md#l4--json-rpc-method-set--content-length-framing).
|
|
6
|
+
|
|
7
|
+
**Status**: Python structural extractor. It emits modules, classes, functions,
|
|
8
|
+
`contains`, `calls`, `references`, `imports`, and versioned entity signatures
|
|
9
|
+
for Stable Entity Identity (SEI) matching. It also reads Wardline's NG-25
|
|
10
|
+
trust-vocabulary descriptor without importing Wardline and emits source-observed
|
|
11
|
+
Wardline decorator metadata/tags on decorated entities when a descriptor is
|
|
12
|
+
available.
|
|
13
|
+
|
|
14
|
+
## Install (development)
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
python -m venv .venv
|
|
18
|
+
source .venv/bin/activate
|
|
19
|
+
pip install -e '.[dev]'
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
This places `loomweave-plugin-python` on your `$PATH` and installs the
|
|
23
|
+
dev-time toolchain (`ruff`, `mypy`, `pytest`, `pytest-cov`, `pre-commit`).
|
|
24
|
+
|
|
25
|
+
## ADR-023 tooling gates
|
|
26
|
+
|
|
27
|
+
Every commit must pass all four:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
ruff check plugins/python
|
|
31
|
+
ruff format --check plugins/python
|
|
32
|
+
mypy --strict plugins/python
|
|
33
|
+
pytest plugins/python
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
CI runs the same four gates in the `python-plugin` job.
|
|
37
|
+
|
|
38
|
+
## Design references
|
|
39
|
+
|
|
40
|
+
- [WP3 plan](../../docs/implementation/sprint-1/wp3-python-plugin.md) — task
|
|
41
|
+
ledger, lock-ins, and UQ resolutions.
|
|
42
|
+
- [ADR-003](../../docs/loomweave/adr/ADR-003-entity-id-scheme.md) — 3-segment
|
|
43
|
+
`EntityId` format this plugin produces.
|
|
44
|
+
- [ADR-018](../../docs/loomweave/adr/ADR-018-identity-reconciliation.md) —
|
|
45
|
+
cross-product identity join with Wardline.
|
|
46
|
+
- [ADR-022](../../docs/loomweave/adr/ADR-022-core-plugin-ontology.md) —
|
|
47
|
+
manifest schema and ontology-boundary enforcement.
|
|
48
|
+
- [ADR-023](../../docs/loomweave/adr/ADR-023-tooling-baseline.md) — the four
|
|
49
|
+
Python gates and the `pre-commit` setup.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
[plugin]
|
|
2
|
+
name = "loomweave-plugin-python"
|
|
3
|
+
plugin_id = "python"
|
|
4
|
+
version = "1.0.0"
|
|
5
|
+
protocol_version = "1.0"
|
|
6
|
+
# Bare basename per ADR-021 §Layer 1 + WP2 scrub commit eb0a41d — the host
|
|
7
|
+
# refuses manifests whose `executable` carries any path component.
|
|
8
|
+
executable = "loomweave-plugin-python"
|
|
9
|
+
language = "python"
|
|
10
|
+
extensions = ["py"]
|
|
11
|
+
|
|
12
|
+
[capabilities.runtime]
|
|
13
|
+
# Plugin's declared RSS envelope (MiB). Effective prlimit is
|
|
14
|
+
# min(this, core default 2 GiB). Pyright's Node-based language server
|
|
15
|
+
# inherits this limit from the plugin process, so B.4* declares the core
|
|
16
|
+
# ceiling rather than the Sprint-1 CPython-only working set.
|
|
17
|
+
expected_max_rss_mb = 2048
|
|
18
|
+
# Triggers LMWV-INFRA-PLUGIN-ENTITY-OVERRUN-WARNING well before the 500k
|
|
19
|
+
# core cap (warning emission itself is deferred to Tier B — Sprint 1
|
|
20
|
+
# only lands the declaration).
|
|
21
|
+
expected_entities_per_file = 5000
|
|
22
|
+
# Wardline semantic extraction reads the NG-25 vocabulary descriptor without
|
|
23
|
+
# importing Wardline, then emits source-observed decorator metadata on entities.
|
|
24
|
+
wardline_aware = true
|
|
25
|
+
# v0.1 rejects `true` at initialize with LMWV-INFRA-MANIFEST-UNSUPPORTED-CAPABILITY.
|
|
26
|
+
reads_outside_project_root = false
|
|
27
|
+
|
|
28
|
+
[capabilities.runtime.pyright]
|
|
29
|
+
pin = "1.1.409"
|
|
30
|
+
|
|
31
|
+
[ontology]
|
|
32
|
+
# Sprint 2 B.2: classes + modules join the kind set. B.3 (ADR-026) adds
|
|
33
|
+
# the first edge kind, `contains`. B.4* adds scan-time `calls` edges.
|
|
34
|
+
# B.5* adds scan-time `references` edges. Phase 3 Task 3 adds scan-time
|
|
35
|
+
# `imports` candidate edges.
|
|
36
|
+
entity_kinds = ["function", "class", "module"]
|
|
37
|
+
edge_kinds = ["contains", "calls", "references", "imports"]
|
|
38
|
+
# Per ADR-022: uppercase `LMWV-{PLUGIN_ID_UPPER}-`. Reserved at parse
|
|
39
|
+
# against the LMWV-INFRA-* and LMWV-FACT-* namespaces.
|
|
40
|
+
rule_id_prefix = "LMWV-PY-"
|
|
41
|
+
# Bumps per ADR-027 when the entity/edge/rule set shifts. Phase 3 Task 3 is a
|
|
42
|
+
# MINOR bump (additive `imports` edge kind). NOTE: ADR-007's summary-cache key
|
|
43
|
+
# is the 5-tuple (entity_id, content_hash, prompt_template_id, model_tier,
|
|
44
|
+
# guidance_fingerprint) — ontology_version is handshake-validation, NOT a
|
|
45
|
+
# cache-key component. New edge rows miss the cache by component-1 of
|
|
46
|
+
# the 5-tuple organically (no edges live in the 5-tuple yet anyway).
|
|
47
|
+
ontology_version = "0.7.0"
|
|
48
|
+
|
|
49
|
+
[integrations.wardline]
|
|
50
|
+
expected_descriptor_version = "wardline-generic-2"
|
|
51
|
+
|
|
52
|
+
[ontology.roles]
|
|
53
|
+
file_scope = ["module"]
|
|
54
|
+
callable = ["function"]
|
|
55
|
+
syntax_degraded_module = ["module"]
|
|
56
|
+
|
|
57
|
+
# SEI signature declaration (ADR-038 REQ-C-01 / Wave 1). The plugin emits a
|
|
58
|
+
# versioned `signature` object per function/class entity, stored verbatim by the
|
|
59
|
+
# core and compared by string equality as the matcher's move-case input. Modules
|
|
60
|
+
# and packages declare no signature (the move case abstains for them — fail
|
|
61
|
+
# closed). A `schema_version` bump voids cached signature equality.
|
|
62
|
+
[signature]
|
|
63
|
+
schema_version = 1
|
|
64
|
+
|
|
65
|
+
[signature.schemas.function]
|
|
66
|
+
v = 1
|
|
67
|
+
fields = ["params", "return_ann"]
|
|
68
|
+
|
|
69
|
+
[signature.schemas.class]
|
|
70
|
+
v = 1
|
|
71
|
+
fields = ["bases"]
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "loomweave-plugin-python"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Loomweave Python language plugin — v1.0 release"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
authors = [{ name = "John Morrissey", email = "qacona@gmail.com" }]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 4 - Beta",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.11",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
]
|
|
18
|
+
dependencies = [
|
|
19
|
+
"packaging>=24",
|
|
20
|
+
"pyright==1.1.409",
|
|
21
|
+
"pyyaml>=6.0",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
dev = [
|
|
26
|
+
"pytest>=8.0",
|
|
27
|
+
"pytest-cov>=5.0",
|
|
28
|
+
"ruff>=0.6",
|
|
29
|
+
"mypy>=1.11",
|
|
30
|
+
"pre-commit>=3.8",
|
|
31
|
+
"pip-audit>=2.9",
|
|
32
|
+
"build>=1.2",
|
|
33
|
+
"types-PyYAML>=6.0",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
loomweave-plugin-python = "loomweave_plugin_python.__main__:main"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["src/loomweave_plugin_python"]
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.wheel.shared-data]
|
|
43
|
+
# Route plugin.toml into <install-prefix>/share/loomweave/plugins/<suffix>/ so
|
|
44
|
+
# WP2's L9 install-prefix fallback finds it. The <suffix> is produced by
|
|
45
|
+
# `discovery.rs::strip_prefix("loomweave-plugin-")` on the binary name, so for
|
|
46
|
+
# loomweave-plugin-python the suffix is "python" — the target directory must
|
|
47
|
+
# match that basename exactly.
|
|
48
|
+
"plugin.toml" = "share/loomweave/plugins/python/plugin.toml"
|
|
49
|
+
|
|
50
|
+
[tool.ruff]
|
|
51
|
+
target-version = "py311"
|
|
52
|
+
line-length = 100
|
|
53
|
+
src = ["src", "tests"]
|
|
54
|
+
|
|
55
|
+
[tool.ruff.lint]
|
|
56
|
+
select = ["ALL"]
|
|
57
|
+
ignore = [
|
|
58
|
+
"D", # pydocstyle relaxed per ADR-023
|
|
59
|
+
"COM812", # conflicts with ruff format
|
|
60
|
+
"ISC001", # conflicts with ruff format
|
|
61
|
+
"CPY", # copyright headers are not our convention
|
|
62
|
+
"ANN401", # Any is legitimate for JSON-RPC payload fields (shape is open)
|
|
63
|
+
"TRY003", # short composed exception messages are fine
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
[tool.ruff.lint.per-file-ignores]
|
|
67
|
+
"tests/**" = [
|
|
68
|
+
"S101", # assert is the whole point of tests
|
|
69
|
+
"PLR2004", # magic numbers are fine in test expectations
|
|
70
|
+
"ANN", # type annotations optional in tests
|
|
71
|
+
"INP001", # tests/ is a namespace package
|
|
72
|
+
"S108", # hard-coded /tmp paths are test fixtures, not real I/O
|
|
73
|
+
"E501", # long assert messages in test failures are fine
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
[tool.ruff.lint.mccabe]
|
|
77
|
+
# Dispatch loops naturally exceed the default 10; 15 matches our Rust clippy.toml.
|
|
78
|
+
max-complexity = 15
|
|
79
|
+
|
|
80
|
+
[tool.ruff.lint.pylint]
|
|
81
|
+
max-returns = 10
|
|
82
|
+
max-branches = 15
|
|
83
|
+
|
|
84
|
+
[tool.ruff.format]
|
|
85
|
+
# defaults: double quotes, space indent, trailing commas where appropriate
|
|
86
|
+
|
|
87
|
+
[tool.mypy]
|
|
88
|
+
python_version = "3.11"
|
|
89
|
+
strict = true
|
|
90
|
+
warn_unused_configs = true
|
|
91
|
+
files = ["src", "tests"]
|
|
92
|
+
|
|
93
|
+
[[tool.mypy.overrides]]
|
|
94
|
+
module = "yaml"
|
|
95
|
+
ignore_missing_imports = true
|
|
96
|
+
|
|
97
|
+
[tool.pytest.ini_options]
|
|
98
|
+
testpaths = ["tests"]
|
|
99
|
+
addopts = "--strict-markers --cov=loomweave_plugin_python --cov-report=term-missing --cov-fail-under=85"
|
|
100
|
+
pythonpath = ["src"]
|
|
101
|
+
markers = [
|
|
102
|
+
"pyright: requires pyright-langserver on PATH or in the active virtualenv",
|
|
103
|
+
"slow: exercises subprocess or scale behavior that is slower than unit tests",
|
|
104
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Entry point for the ``loomweave-plugin-python`` executable.
|
|
2
|
+
|
|
3
|
+
Installs stdout discipline (``stdout_guard``) and hands control to the
|
|
4
|
+
JSON-RPC server loop. ``sys.exit`` threads the server's exit code out to
|
|
5
|
+
the host process.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
from loomweave_plugin_python.server import main
|
|
13
|
+
|
|
14
|
+
if __name__ == "__main__":
|
|
15
|
+
sys.exit(main())
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import TYPE_CHECKING, Literal, NotRequired, Protocol, TypedDict
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CallsEdgeProperties(TypedDict):
|
|
12
|
+
candidates: list[str]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CallsRawEdge(TypedDict):
|
|
16
|
+
kind: Literal["calls"]
|
|
17
|
+
from_id: str
|
|
18
|
+
to_id: str
|
|
19
|
+
source_byte_start: int
|
|
20
|
+
source_byte_end: int
|
|
21
|
+
confidence: Literal["resolved", "ambiguous"]
|
|
22
|
+
properties: NotRequired[CallsEdgeProperties]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Finding(TypedDict):
|
|
26
|
+
subcode: str
|
|
27
|
+
severity: Literal["info", "warning", "error"]
|
|
28
|
+
message: str
|
|
29
|
+
metadata: dict[str, object]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class UnresolvedCallSite(TypedDict):
|
|
33
|
+
caller_entity_id: str
|
|
34
|
+
site_ordinal: int
|
|
35
|
+
source_byte_start: int
|
|
36
|
+
source_byte_end: int
|
|
37
|
+
callee_expr: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class CallResolutionResult:
|
|
42
|
+
edges: list[CallsRawEdge] = field(default_factory=list)
|
|
43
|
+
unresolved_call_sites_total: int = 0
|
|
44
|
+
unresolved_call_sites: list[UnresolvedCallSite] = field(default_factory=list)
|
|
45
|
+
pyright_query_latency_ms: list[int] = field(default_factory=list)
|
|
46
|
+
pyright_index_parse_latency_ms: list[int] = field(default_factory=list)
|
|
47
|
+
findings: list[Finding] = field(default_factory=list)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class CallResolver(Protocol):
|
|
51
|
+
def resolve_calls(
|
|
52
|
+
self,
|
|
53
|
+
file_path: str | Path,
|
|
54
|
+
function_ids: Sequence[str],
|
|
55
|
+
) -> CallResolutionResult: ...
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class NoOpCallResolver:
|
|
59
|
+
def resolve_calls(
|
|
60
|
+
self,
|
|
61
|
+
file_path: str | Path,
|
|
62
|
+
function_ids: Sequence[str],
|
|
63
|
+
) -> CallResolutionResult:
|
|
64
|
+
_ = (file_path, function_ids)
|
|
65
|
+
return CallResolutionResult()
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""L2 3-segment EntityId assembler matching WP1's Rust ``entity_id()`` byte-for-byte.
|
|
2
|
+
|
|
3
|
+
Per ADR-003 + ADR-022, every Loomweave entity has a 3-segment ID of the
|
|
4
|
+
form ``{plugin_id}:{kind}:{canonical_qualified_name}``.
|
|
5
|
+
|
|
6
|
+
Validation (mirrors ``crates/loomweave-core/src/entity_id.rs``):
|
|
7
|
+
|
|
8
|
+
- ``plugin_id`` and ``kind`` must match the identifier grammar
|
|
9
|
+
``[a-z][a-z0-9_]*`` (ADR-022).
|
|
10
|
+
- No segment may contain a literal ``:`` (reserved separator).
|
|
11
|
+
- No segment may be empty.
|
|
12
|
+
|
|
13
|
+
The shared fixture ``fixtures/entity_id.json`` (Task 5) drives the
|
|
14
|
+
cross-language parity check: both the Rust assembler and this Python
|
|
15
|
+
assembler consume the same fixture rows and must produce identical
|
|
16
|
+
strings byte-for-byte.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import re
|
|
22
|
+
|
|
23
|
+
_GRAMMAR = re.compile(r"^[a-z][a-z0-9_]*$")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class EntityIdError(ValueError):
|
|
27
|
+
"""Base class for all ``entity_id()`` validation failures."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class EmptySegmentError(EntityIdError):
|
|
31
|
+
"""A segment (``plugin_id``, ``kind``, or ``canonical_qualified_name``) was empty."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, field: str) -> None:
|
|
34
|
+
super().__init__(f"segment {field} empty")
|
|
35
|
+
self.field = field
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class GrammarViolationError(EntityIdError):
|
|
39
|
+
"""A segment did not match the ADR-022 grammar ``[a-z][a-z0-9_]*``."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, field: str, value: str) -> None:
|
|
42
|
+
super().__init__(f"segment {field} violates ADR-022 grammar [a-z][a-z0-9_]*: {value!r}")
|
|
43
|
+
self.field = field
|
|
44
|
+
self.value = value
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class SegmentContainsColonError(EntityIdError):
|
|
48
|
+
"""A segment contained the reserved ``:`` separator (UQ-WP1-07)."""
|
|
49
|
+
|
|
50
|
+
def __init__(self, field: str, value: str) -> None:
|
|
51
|
+
super().__init__(f"segment {field} contains reserved ':' separator: {value!r}")
|
|
52
|
+
self.field = field
|
|
53
|
+
self.value = value
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _validate_grammar(field: str, value: str) -> None:
|
|
57
|
+
"""Mirror ``validate_grammar`` in the Rust side — empty, colon, then regex."""
|
|
58
|
+
if not value:
|
|
59
|
+
raise EmptySegmentError(field)
|
|
60
|
+
if ":" in value:
|
|
61
|
+
raise SegmentContainsColonError(field, value)
|
|
62
|
+
if not _GRAMMAR.fullmatch(value):
|
|
63
|
+
raise GrammarViolationError(field, value)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def entity_id(plugin_id: str, kind: str, canonical_qualified_name: str) -> str:
|
|
67
|
+
"""Assemble the 3-segment EntityId string with full validation."""
|
|
68
|
+
_validate_grammar("plugin_id", plugin_id)
|
|
69
|
+
_validate_grammar("kind", kind)
|
|
70
|
+
qn_field = "canonical_qualified_name"
|
|
71
|
+
if not canonical_qualified_name:
|
|
72
|
+
raise EmptySegmentError(qn_field)
|
|
73
|
+
if ":" in canonical_qualified_name:
|
|
74
|
+
raise SegmentContainsColonError(qn_field, canonical_qualified_name)
|
|
75
|
+
return f"{plugin_id}:{kind}:{canonical_qualified_name}"
|