loomweave-plugin-python 1.3.1__tar.gz → 1.5.1__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.
Files changed (40) hide show
  1. {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.5.1}/.gitignore +3 -0
  2. {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.5.1}/PKG-INFO +43 -2
  3. loomweave_plugin_python-1.5.1/README.md +90 -0
  4. {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.5.1}/plugin.toml +33 -7
  5. {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.5.1}/pyproject.toml +1 -1
  6. {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.5.1}/src/loomweave_plugin_python/__init__.py +1 -1
  7. loomweave_plugin_python-1.5.1/src/loomweave_plugin_python/call_resolver.py +140 -0
  8. {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.5.1}/src/loomweave_plugin_python/extractor.py +1210 -40
  9. loomweave_plugin_python-1.5.1/src/loomweave_plugin_python/interpreter.py +100 -0
  10. loomweave_plugin_python-1.5.1/src/loomweave_plugin_python/pyright_session.py +2866 -0
  11. {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.5.1}/src/loomweave_plugin_python/reference_resolver.py +3 -0
  12. {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.5.1}/src/loomweave_plugin_python/server.py +35 -3
  13. loomweave_plugin_python-1.5.1/src/loomweave_plugin_python/wardline_descriptor.py +365 -0
  14. loomweave_plugin_python-1.5.1/tests/fixtures/wardline-vocabulary-descriptor.generic-3.preview.yaml +17 -0
  15. loomweave_plugin_python-1.5.1/tests/fixtures/wardline-vocabulary-descriptor.golden.yaml +14 -0
  16. {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.5.1}/tests/test_extractor.py +1740 -88
  17. loomweave_plugin_python-1.5.1/tests/test_interpreter.py +193 -0
  18. {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.5.1}/tests/test_package.py +25 -4
  19. loomweave_plugin_python-1.5.1/tests/test_pyright_session.py +4686 -0
  20. {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.5.1}/tests/test_server.py +243 -4
  21. loomweave_plugin_python-1.5.1/tests/test_wardline_descriptor.py +560 -0
  22. loomweave_plugin_python-1.5.1/tests/test_wardline_vocabulary_descriptor_conformance.py +382 -0
  23. {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.5.1}/uv.lock +4 -4
  24. loomweave_plugin_python-1.3.1/README.md +0 -49
  25. loomweave_plugin_python-1.3.1/src/loomweave_plugin_python/call_resolver.py +0 -65
  26. loomweave_plugin_python-1.3.1/src/loomweave_plugin_python/pyright_session.py +0 -1787
  27. loomweave_plugin_python-1.3.1/src/loomweave_plugin_python/wardline_descriptor.py +0 -212
  28. loomweave_plugin_python-1.3.1/tests/test_pyright_session.py +0 -1620
  29. loomweave_plugin_python-1.3.1/tests/test_wardline_descriptor.py +0 -227
  30. {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.5.1}/src/loomweave_plugin_python/__main__.py +0 -0
  31. {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.5.1}/src/loomweave_plugin_python/entity_id.py +0 -0
  32. {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.5.1}/src/loomweave_plugin_python/py.typed +0 -0
  33. {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.5.1}/src/loomweave_plugin_python/qualname.py +0 -0
  34. {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.5.1}/src/loomweave_plugin_python/stdout_guard.py +0 -0
  35. {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.5.1}/tests/__init__.py +0 -0
  36. {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.5.1}/tests/test_dogfood_uniqueness.py +0 -0
  37. {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.5.1}/tests/test_entity_id.py +0 -0
  38. {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.5.1}/tests/test_qualname.py +0 -0
  39. {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.5.1}/tests/test_round_trip.py +0 -0
  40. {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.5.1}/tests/test_stdout_guard.py +0 -0
@@ -64,7 +64,10 @@ CLAUDE.md
64
64
 
65
65
  # Developer config / local tooling — not part of the solution
66
66
  .agents/
67
+ /.worktrees/
67
68
 
68
69
  # Local tool/federation config — not part of the solution
69
70
  .mcp.json
70
71
  loomweave.yaml
72
+ .wardline
73
+ dist-reconcile/
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: loomweave-plugin-python
3
- Version: 1.3.1
3
+ Version: 1.5.1
4
4
  Summary: Loomweave Python language plugin — v1.0 release
5
5
  Author-email: John Morrissey <qacona@gmail.com>
6
6
  Classifier: Development Status :: 4 - Beta
@@ -59,6 +59,47 @@ pytest plugins/python
59
59
 
60
60
  CI runs the same four gates in the `python-plugin` job.
61
61
 
62
+ ## Interpreter discovery
63
+
64
+ `pyright-langserver` type-checks against whatever `python` it finds — the
65
+ plugin points it at the project's own interpreter deterministically, rather
66
+ than trusting whatever happened to be first on the launching process's
67
+ `PATH` (clarion-5cf9643de9). `discover_project_interpreter`
68
+ (`src/loomweave_plugin_python/interpreter.py`) walks a fixed order and stops
69
+ at the first usable candidate — a **regular** file (`Path.is_file()`) that
70
+ passes `os.access(path, os.X_OK)`:
71
+
72
+ | Rung | Source | Pinned? |
73
+ |---|---|---|
74
+ | 1 | `LOOMWEAVE_PYTHON_INTERPRETER` env var names an executable file | yes |
75
+ | 2 | `<project_root>/.venv/bin/python` | yes |
76
+ | 3 | `$VIRTUAL_ENV/bin/python` | yes |
77
+ | 4 | `$CONDA_PREFIX/bin/python` | yes |
78
+ | 5 | first `python` / `python3` on `PATH` | no |
79
+ | 6 | nothing found | no |
80
+
81
+ An empty env value counts as unset at every rung. The returned path is
82
+ absolute and lexically normalised but **never symlink-resolved** — a venv's
83
+ `bin/python` is typically a symlink to the base interpreter, and pyright
84
+ needs the symlink path to stay inside the venv's `site-packages`.
85
+
86
+ This order is a cross-language contract shared with the Rust host
87
+ (`crates/loomweave-core/src/plugin/interpreter.rs`); the host runs the same
88
+ discovery and, for a `[capabilities.runtime.pyright]` plugin, exports its
89
+ pinned answer to the child as `LOOMWEAVE_PYTHON_INTERPRETER` (unless the
90
+ operator already set it). The plugin's own discovery therefore sees the
91
+ host's choice at rung 1 whenever the host found one.
92
+
93
+ When discovery lands on rung 5 or 6 (no project-owned interpreter), an
94
+ otherwise-`complete` calls/references facet is honestly demoted to
95
+ `degraded` with reason `interpreter_unpinned` — cross-module call/reference
96
+ targets may be missing because pyright resolved against a guessed
97
+ interpreter, even though nothing else went wrong. Fix it by creating
98
+ `.venv` in the project root or setting `LOOMWEAVE_PYTHON_INTERPRETER` to the
99
+ project's interpreter. See
100
+ [ADR-058](../../docs/loomweave/adr/ADR-058-project-interpreter-discovery.md)
101
+ for the full design and the `interpreter_unpinned` coverage semantics.
102
+
62
103
  ## Design references
63
104
 
64
105
  - [WP3 plan](../../docs/implementation/sprint-1/wp3-python-plugin.md) — task
@@ -0,0 +1,90 @@
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
+ ## Interpreter discovery
39
+
40
+ `pyright-langserver` type-checks against whatever `python` it finds — the
41
+ plugin points it at the project's own interpreter deterministically, rather
42
+ than trusting whatever happened to be first on the launching process's
43
+ `PATH` (clarion-5cf9643de9). `discover_project_interpreter`
44
+ (`src/loomweave_plugin_python/interpreter.py`) walks a fixed order and stops
45
+ at the first usable candidate — a **regular** file (`Path.is_file()`) that
46
+ passes `os.access(path, os.X_OK)`:
47
+
48
+ | Rung | Source | Pinned? |
49
+ |---|---|---|
50
+ | 1 | `LOOMWEAVE_PYTHON_INTERPRETER` env var names an executable file | yes |
51
+ | 2 | `<project_root>/.venv/bin/python` | yes |
52
+ | 3 | `$VIRTUAL_ENV/bin/python` | yes |
53
+ | 4 | `$CONDA_PREFIX/bin/python` | yes |
54
+ | 5 | first `python` / `python3` on `PATH` | no |
55
+ | 6 | nothing found | no |
56
+
57
+ An empty env value counts as unset at every rung. The returned path is
58
+ absolute and lexically normalised but **never symlink-resolved** — a venv's
59
+ `bin/python` is typically a symlink to the base interpreter, and pyright
60
+ needs the symlink path to stay inside the venv's `site-packages`.
61
+
62
+ This order is a cross-language contract shared with the Rust host
63
+ (`crates/loomweave-core/src/plugin/interpreter.rs`); the host runs the same
64
+ discovery and, for a `[capabilities.runtime.pyright]` plugin, exports its
65
+ pinned answer to the child as `LOOMWEAVE_PYTHON_INTERPRETER` (unless the
66
+ operator already set it). The plugin's own discovery therefore sees the
67
+ host's choice at rung 1 whenever the host found one.
68
+
69
+ When discovery lands on rung 5 or 6 (no project-owned interpreter), an
70
+ otherwise-`complete` calls/references facet is honestly demoted to
71
+ `degraded` with reason `interpreter_unpinned` — cross-module call/reference
72
+ targets may be missing because pyright resolved against a guessed
73
+ interpreter, even though nothing else went wrong. Fix it by creating
74
+ `.venv` in the project root or setting `LOOMWEAVE_PYTHON_INTERPRETER` to the
75
+ project's interpreter. See
76
+ [ADR-058](../../docs/loomweave/adr/ADR-058-project-interpreter-discovery.md)
77
+ for the full design and the `interpreter_unpinned` coverage semantics.
78
+
79
+ ## Design references
80
+
81
+ - [WP3 plan](../../docs/implementation/sprint-1/wp3-python-plugin.md) — task
82
+ ledger, lock-ins, and UQ resolutions.
83
+ - [ADR-003](../../docs/loomweave/adr/ADR-003-entity-id-scheme.md) — 3-segment
84
+ `EntityId` format this plugin produces.
85
+ - [ADR-018](../../docs/loomweave/adr/ADR-018-identity-reconciliation.md) —
86
+ cross-product identity join with Wardline.
87
+ - [ADR-022](../../docs/loomweave/adr/ADR-022-core-plugin-ontology.md) —
88
+ manifest schema and ontology-boundary enforcement.
89
+ - [ADR-023](../../docs/loomweave/adr/ADR-023-tooling-baseline.md) — the four
90
+ Python gates and the `pre-commit` setup.
@@ -1,7 +1,7 @@
1
1
  [plugin]
2
2
  name = "loomweave-plugin-python"
3
3
  plugin_id = "python"
4
- version = "1.3.1"
4
+ version = "1.5.1"
5
5
  protocol_version = "1.0"
6
6
  # Bare basename per ADR-021 §Layer 1 + WP2 scrub commit eb0a41d — the host
7
7
  # refuses manifests whose `executable` carries any path component.
@@ -10,10 +10,11 @@ language = "python"
10
10
  extensions = ["py"]
11
11
 
12
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.
13
+ # Plugin's declared RSS envelope (MiB). For plugins declaring
14
+ # [capabilities.runtime.pyright] this documents resident memory only: the
15
+ # host applies its 8 GiB language-server RLIMIT_AS ceiling
16
+ # (LANGUAGE_SERVER_MAX_AS_MIB, ADR-021 §2d amendment) because pyright's
17
+ # Node/V8 process reserves virtual space far beyond its RSS.
17
18
  expected_max_rss_mb = 2048
18
19
  # Triggers LMWV-INFRA-PLUGIN-ENTITY-OVERRUN-WARNING well before the 500k
19
20
  # core cap (warning emission itself is deferred to Tier B — Sprint 1
@@ -47,16 +48,41 @@ edge_kinds = ["contains", "calls", "references", "imports", "inherits_from", "de
47
48
  rule_id_prefix = "LMWV-PY-"
48
49
  # Bumps per ADR-027 when the entity/edge/rule set shifts. Phase 3 Task 3 is a
49
50
  # MINOR bump (additive `imports` edge kind); 0.8.0 is the additive
50
- # `inherits_from` + `decorates` MINOR bump (clarion-43416be550). NOTE:
51
+ # `inherits_from` + `decorates` MINOR bump (clarion-43416be550); 0.9.0 is the
52
+ # additive `public-surface` reachability-root tag (clarion-4ec50f3d92) — a
53
+ # no-`__all__` library's public defs/classes are now tagged as roots; 0.10.0
54
+ # adds module-level `exported-api` for explicit `__all__` re-export/imported-name
55
+ # surfaces without double-counting local function/class exports, including
56
+ # literal `__all__ += [...]` extensions; 0.11.0 adds manual main-guard,
57
+ # runner-wrapper, `sys.argv`, and argparse `cli-command` tagging; 0.12.0
58
+ # declares the complete static classifier set so supported-empty results are
59
+ # distinguishable from unsupported classifiers and forces unchanged files to
60
+ # re-dispatch. Classifier declarations do not create entity-tag rows. NOTE:
51
61
  # ADR-007's summary-cache key is the 5-tuple (entity_id, content_hash,
52
62
  # prompt_template_id, model_tier, guidance_fingerprint) — ontology_version is
53
63
  # handshake-validation, NOT a cache-key component. New edge rows miss the
54
64
  # cache by component-1 of the 5-tuple organically (no edges live in the
55
65
  # 5-tuple yet anyway).
56
- ontology_version = "0.8.0"
66
+ ontology_version = "0.12.0"
67
+ classifier_tags = [
68
+ "cli-command",
69
+ "data-model",
70
+ "entry-point",
71
+ "exported-api",
72
+ "framework-handler",
73
+ "http-route",
74
+ "public-surface",
75
+ "test",
76
+ ]
57
77
 
58
78
  [integrations.wardline]
59
79
  expected_descriptor_version = "wardline-generic-2"
80
+ # The dual-accept set, as `schema@version` PAIRS. A version-only list would
81
+ # destroy the schema/version association the gate rests on (re-admitting
82
+ # "version alone unlocks v2 parsing"). A space-separated STRING, never a TOML
83
+ # array: manifest.rs:137 deserialises this table as BTreeMap<String, String>,
84
+ # so an array value is a hard manifest-parse failure.
85
+ accepted_descriptors = "wardline.vocabulary/v1@wardline-generic-2 wardline.vocabulary/v2@wardline-generic-3"
60
86
 
61
87
  [ontology.roles]
62
88
  file_scope = ["module"]
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "loomweave-plugin-python"
7
- version = "1.3.1"
7
+ version = "1.5.1"
8
8
  description = "Loomweave Python language plugin — v1.0 release"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,3 +1,3 @@
1
1
  """loomweave-plugin-python — Python language plugin for Loomweave."""
2
2
 
3
- __version__ = "1.3.1"
3
+ __version__ = "1.5.1"
@@ -0,0 +1,140 @@
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
+ class FacetCoverageWire(TypedDict):
41
+ """Wire shape of one resolution facet's coverage claim (host contract)."""
42
+
43
+ status: Literal["complete", "degraded"]
44
+ reason: NotRequired[str]
45
+ transient: bool
46
+ collateral: bool
47
+
48
+
49
+ class ResolutionCoverageWire(TypedDict):
50
+ calls: FacetCoverageWire
51
+ references: FacetCoverageWire
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class FacetCoverage:
56
+ """How much of one resolution facet (calls / references) a file received.
57
+
58
+ The resolver degrades to EMPTY evidence -- not an error -- when pyright is
59
+ unavailable, times out, or has been poisoned for the rest of the run. The
60
+ host cannot tell that apart from a genuinely call-free file, so it treated
61
+ the empty result as a completed analysis and its incremental skip pinned
62
+ the hole for as long as the file's bytes stayed unchanged
63
+ (clarion-3e517d4aff). This claim rides every ``analyze_file`` result so the
64
+ host can re-dispatch a ``degraded`` + ``transient`` file next run and name
65
+ the hole on its read surface.
66
+
67
+ ``transient`` is True when re-running the unchanged file could plausibly
68
+ recover coverage (resolver timeout, crash, poison, unavailable binary) and
69
+ False for content-determined limits a re-run would hit again (syntax
70
+ error, per-file site cap, nesting too complex).
71
+
72
+ ``collateral`` (clarion-7fc41105ea, ADR-057) is decided by which catch-site
73
+ built the claim, never by message text. Self-inflicted (``False``) means
74
+ ONLY: ``pyright_timeout`` on this file's own budget, or
75
+ ``pyright_transport_failure`` -- pyright died while THIS file's request was
76
+ in flight (``pyright_local_read_error``, a read error on an unrelated
77
+ target while pyright stayed alive, is also this file's gap). Everything
78
+ else is collateral (``True``): ``pyright_restarting`` (found dead on
79
+ arrival), ``pyright_spawn_failed`` (deferred spawn), and the run-disabled
80
+ tokens ``pyright_unavailable`` / ``pyright_poisoned`` /
81
+ ``pyright_restart_cap_exceeded``. The full vocabulary is documented at the
82
+ top of ``pyright_session.py``.
83
+ """
84
+
85
+ status: Literal["complete", "degraded"] = "complete"
86
+ reason: str | None = None
87
+ transient: bool = False
88
+ # The hole predates this file (pyright already dead, spawn deferred, run
89
+ # disabled by an EARLIER file's failure): collateral, not this file's
90
+ # doing. The host dispatches collateral files first so the troublemaker
91
+ # goes last, and keeps a prior self-inflicted mark sticky across a
92
+ # collateral run.
93
+ collateral: bool = False
94
+
95
+ @classmethod
96
+ def degraded(cls, reason: str, *, transient: bool, collateral: bool = False) -> FacetCoverage:
97
+ return cls(status="degraded", reason=reason, transient=transient, collateral=collateral)
98
+
99
+ @property
100
+ def is_degraded(self) -> bool:
101
+ return self.status == "degraded"
102
+
103
+ def to_wire(self) -> FacetCoverageWire:
104
+ wire: FacetCoverageWire = {
105
+ "status": self.status,
106
+ "transient": self.transient,
107
+ "collateral": self.collateral,
108
+ }
109
+ if self.reason is not None:
110
+ wire["reason"] = self.reason
111
+ return wire
112
+
113
+
114
+ @dataclass
115
+ class CallResolutionResult:
116
+ edges: list[CallsRawEdge] = field(default_factory=list)
117
+ unresolved_call_sites_total: int = 0
118
+ unresolved_call_sites: list[UnresolvedCallSite] = field(default_factory=list)
119
+ pyright_query_latency_ms: list[int] = field(default_factory=list)
120
+ pyright_index_parse_latency_ms: list[int] = field(default_factory=list)
121
+ findings: list[Finding] = field(default_factory=list)
122
+ coverage: FacetCoverage = field(default_factory=FacetCoverage)
123
+
124
+
125
+ class CallResolver(Protocol):
126
+ def resolve_calls(
127
+ self,
128
+ file_path: str | Path,
129
+ function_ids: Sequence[str],
130
+ ) -> CallResolutionResult: ...
131
+
132
+
133
+ class NoOpCallResolver:
134
+ def resolve_calls(
135
+ self,
136
+ file_path: str | Path,
137
+ function_ids: Sequence[str],
138
+ ) -> CallResolutionResult:
139
+ _ = (file_path, function_ids)
140
+ return CallResolutionResult()