loomweave-plugin-python 1.0.0__py3-none-any.whl → 1.2.0__py3-none-any.whl
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/__init__.py +1 -1
- loomweave_plugin_python/extractor.py +199 -29
- loomweave_plugin_python/pyright_session.py +154 -22
- loomweave_plugin_python/qualname.py +3 -3
- loomweave_plugin_python/reference_resolver.py +16 -2
- loomweave_plugin_python/server.py +1 -1
- loomweave_plugin_python/wardline_descriptor.py +24 -9
- {loomweave_plugin_python-1.0.0.data → loomweave_plugin_python-1.2.0.data}/data/share/loomweave/plugins/python/plugin.toml +18 -9
- {loomweave_plugin_python-1.0.0.dist-info → loomweave_plugin_python-1.2.0.dist-info}/METADATA +1 -1
- loomweave_plugin_python-1.2.0.dist-info/RECORD +17 -0
- loomweave_plugin_python-1.0.0.dist-info/RECORD +0 -17
- {loomweave_plugin_python-1.0.0.dist-info → loomweave_plugin_python-1.2.0.dist-info}/WHEEL +0 -0
- {loomweave_plugin_python-1.0.0.dist-info → loomweave_plugin_python-1.2.0.dist-info}/entry_points.txt +0 -0
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
Walks a parsed Python file and emits one ``module`` entity per file plus
|
|
4
4
|
one ``function`` entity per ``FunctionDef`` / ``AsyncFunctionDef`` and one
|
|
5
5
|
``class`` entity per ``ClassDef``. It also emits anchored scan-time
|
|
6
|
-
``imports``, ``calls``,
|
|
6
|
+
``imports``, ``calls``, ``references``, ``inherits_from``, and ``decorates``
|
|
7
|
+
candidate edges. The last three share one resolution pass: base-class and
|
|
8
|
+
decorator expressions become ``base`` / ``decorator`` relation sites in the
|
|
9
|
+
same collector that gathers generic reference sites, and the site kind
|
|
10
|
+
selects the emitted edge kind (see ``reference_resolver.ReferenceSiteKind``).
|
|
7
11
|
|
|
8
12
|
Entity shape matches the Rust host's ``RawEntity`` + ``RawSource``
|
|
9
13
|
contract (``crates/loomweave-core/src/plugin/host.rs:132-154``)::
|
|
@@ -40,6 +44,12 @@ Behaviour (B.2 §3 Q1 supersedes Sprint-1 UQ-WP3-11 for module entities):
|
|
|
40
44
|
with ``parse_status="syntax_error"`` plus one stderr log line
|
|
41
45
|
(UQ-WP3-02). The run continues; WP4-era findings can later attach a
|
|
42
46
|
``LMWV-PY-SYNTAX-ERROR`` annotation.
|
|
47
|
+
- ``RecursionError``/``MemoryError`` from hostile or degenerate nesting
|
|
48
|
+
(deep BinOp/attribute chains, unary prefix runs) — whether raised by
|
|
49
|
+
``ast.parse`` itself or by the post-parse walkers — → one degraded
|
|
50
|
+
module entity with ``parse_status="too_complex"`` plus a
|
|
51
|
+
``LMWV-PY-TOO-COMPLEX`` plugin finding (clarion-f3eb3852d6; Rust
|
|
52
|
+
parse-guard parity, ADR-050). The plugin process survives.
|
|
43
53
|
- Top-level ``__init__.py`` (where the dotted module name resolves to
|
|
44
54
|
``""``) is skipped with stderr; the entity-ID assembler rejects an
|
|
45
55
|
empty ``canonical_qualified_name``.
|
|
@@ -85,6 +95,18 @@ if TYPE_CHECKING:
|
|
|
85
95
|
|
|
86
96
|
_PLUGIN_ID = "python"
|
|
87
97
|
_NOOP_CALL_RESOLVER = NoOpCallResolver()
|
|
98
|
+
|
|
99
|
+
# Deep-nesting degradation (clarion-f3eb3852d6; Rust parse-guard parity,
|
|
100
|
+
# ADR-050). CPython's tokenizer self-limits indentation (100) and paren
|
|
101
|
+
# nesting (~200) as SyntaxError, but indentation-free deep ASTs (BinOp
|
|
102
|
+
# chains, attribute chains, unary prefix runs) pass tokenization and blow
|
|
103
|
+
# up later: ast.parse raises RecursionError ("during ast construction") or
|
|
104
|
+
# MemoryError ("Parser stack overflowed"), and chains as shallow as ~350
|
|
105
|
+
# nodes blow Python-level NodeVisitor recursion in the post-parse walkers
|
|
106
|
+
# AFTER a clean parse. All three degrade to a `too_complex` module entity
|
|
107
|
+
# plus this plugin finding — escaping would fail the whole plugin run at
|
|
108
|
+
# the host's analyze_file boundary.
|
|
109
|
+
FINDING_TOO_COMPLEX = "LMWV-PY-TOO-COMPLEX"
|
|
88
110
|
_NOOP_REFERENCE_RESOLVER = NoOpReferenceResolver()
|
|
89
111
|
|
|
90
112
|
|
|
@@ -180,7 +202,7 @@ class RawEntity(TypedDict):
|
|
|
180
202
|
qualified_name: str
|
|
181
203
|
source: EntitySource
|
|
182
204
|
parent_id: NotRequired[str]
|
|
183
|
-
parse_status: NotRequired[Literal["ok", "syntax_error"]]
|
|
205
|
+
parse_status: NotRequired[Literal["ok", "syntax_error", "too_complex"]]
|
|
184
206
|
# entity_context evidence (clarion-460def6a51). Set on function/class
|
|
185
207
|
# entities; omitted for modules. Rides the host's RawEntity `extra` flatten
|
|
186
208
|
# into `properties_json`, so no host or storage schema change is needed.
|
|
@@ -323,7 +345,7 @@ def _build_module_entity(
|
|
|
323
345
|
source: str,
|
|
324
346
|
dotted_module: str,
|
|
325
347
|
file_path: str,
|
|
326
|
-
parse_status: Literal["ok", "syntax_error"],
|
|
348
|
+
parse_status: Literal["ok", "syntax_error", "too_complex"],
|
|
327
349
|
docstring: str | None = None,
|
|
328
350
|
) -> RawEntity:
|
|
329
351
|
"""Build the per-file module entity (Q1 + Q4 resolutions)."""
|
|
@@ -424,6 +446,17 @@ def extract_with_stats( # noqa: PLR0913 - resolver seams + optional Wardline vo
|
|
|
424
446
|
[],
|
|
425
447
|
ExtractionStats(extractor_parse_latency_ms=parse_latency_ms),
|
|
426
448
|
)
|
|
449
|
+
except (RecursionError, MemoryError) as exc:
|
|
450
|
+
# Deep AST construction (RecursionError) or PEG parser stack guard
|
|
451
|
+
# (MemoryError) — see FINDING_TOO_COMPLEX. Stack is unwound here.
|
|
452
|
+
return _too_complex_result(
|
|
453
|
+
source,
|
|
454
|
+
dotted_module,
|
|
455
|
+
file_path,
|
|
456
|
+
"parse",
|
|
457
|
+
exc,
|
|
458
|
+
_elapsed_ms(parse_started_ns),
|
|
459
|
+
)
|
|
427
460
|
parse_latency_ms = _elapsed_ms(parse_started_ns)
|
|
428
461
|
|
|
429
462
|
module_entity = _build_module_entity(
|
|
@@ -432,35 +465,51 @@ def extract_with_stats( # noqa: PLR0913 - resolver seams + optional Wardline vo
|
|
|
432
465
|
entities: list[RawEntity] = [module_entity]
|
|
433
466
|
edges: list[RawEdge] = []
|
|
434
467
|
function_ids: list[str] = []
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
[tree],
|
|
444
|
-
dotted_module,
|
|
445
|
-
file_path,
|
|
446
|
-
module_entity["id"],
|
|
447
|
-
entities,
|
|
448
|
-
edges,
|
|
449
|
-
function_ids,
|
|
450
|
-
walk_state,
|
|
451
|
-
)
|
|
452
|
-
edges.extend(
|
|
453
|
-
_collect_import_edges(
|
|
454
|
-
source,
|
|
468
|
+
try:
|
|
469
|
+
walk_state = _WalkState(
|
|
470
|
+
seen_ids={module_entity["id"]},
|
|
471
|
+
file_path=file_path,
|
|
472
|
+
exported_names=_module_export_names(tree),
|
|
473
|
+
wardline_vocabulary=wardline_vocabulary,
|
|
474
|
+
)
|
|
475
|
+
_walk(
|
|
455
476
|
tree,
|
|
477
|
+
[tree],
|
|
456
478
|
dotted_module,
|
|
479
|
+
file_path,
|
|
457
480
|
module_entity["id"],
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
481
|
+
entities,
|
|
482
|
+
edges,
|
|
483
|
+
function_ids,
|
|
484
|
+
walk_state,
|
|
485
|
+
)
|
|
486
|
+
edges.extend(
|
|
487
|
+
_collect_import_edges(
|
|
488
|
+
source,
|
|
489
|
+
tree,
|
|
490
|
+
dotted_module,
|
|
491
|
+
module_entity["id"],
|
|
492
|
+
is_package_module=is_package_module,
|
|
493
|
+
),
|
|
494
|
+
)
|
|
495
|
+
reference_sites = _collect_reference_sites(source, tree, dotted_module, module_entity["id"])
|
|
496
|
+
call_stats = call_resolver.resolve_calls(file_path, function_ids)
|
|
497
|
+
reference_stats = reference_resolver.resolve_references(file_path, reference_sites)
|
|
498
|
+
except (RecursionError, MemoryError) as exc:
|
|
499
|
+
# A tree can parse cleanly (the C parser's limit is deeper than the
|
|
500
|
+
# Python recursion limit) and still blow the recursive NodeVisitor
|
|
501
|
+
# walkers / pyright function index at ~350+ nesting. Degrade the
|
|
502
|
+
# whole file: partially-walked entities/edges are discarded so the
|
|
503
|
+
# emission is all-or-nothing per file.
|
|
504
|
+
return _too_complex_result(
|
|
505
|
+
source,
|
|
506
|
+
dotted_module,
|
|
507
|
+
file_path,
|
|
508
|
+
"extract",
|
|
509
|
+
exc,
|
|
510
|
+
parse_latency_ms,
|
|
511
|
+
ast.get_docstring(tree),
|
|
512
|
+
)
|
|
464
513
|
edges.extend(cast("list[RawEdge]", call_stats.edges))
|
|
465
514
|
edges.extend(cast("list[RawEdge]", reference_stats.edges))
|
|
466
515
|
stats = ExtractionStats.from_resolution_results(call_stats, reference_stats)
|
|
@@ -469,6 +518,46 @@ def extract_with_stats( # noqa: PLR0913 - resolver seams + optional Wardline vo
|
|
|
469
518
|
return ExtractResult(entities, edges, stats)
|
|
470
519
|
|
|
471
520
|
|
|
521
|
+
def _too_complex_result( # noqa: PLR0913 - degraded-path builder mirrors the syntax_error arm's inputs.
|
|
522
|
+
source: str,
|
|
523
|
+
dotted_module: str,
|
|
524
|
+
file_path: str,
|
|
525
|
+
phase: Literal["parse", "extract"],
|
|
526
|
+
exc: BaseException,
|
|
527
|
+
parse_latency_ms: int,
|
|
528
|
+
docstring: str | None = None,
|
|
529
|
+
) -> ExtractResult:
|
|
530
|
+
"""Degrade a nesting-bomb file to a `too_complex` module entity + finding.
|
|
531
|
+
|
|
532
|
+
Mirrors the Rust plugin's parse-guard degrade-to-finding pattern
|
|
533
|
+
(ADR-050): the file is ingested as a single degraded module entity, the
|
|
534
|
+
failure is visible as a `LMWV-PY-TOO-COMPLEX` plugin finding riding the
|
|
535
|
+
findings wire, and the plugin (and the host's run) survives.
|
|
536
|
+
"""
|
|
537
|
+
sys.stderr.write(
|
|
538
|
+
f"loomweave-plugin-python: degrading {file_path}: nesting too complex "
|
|
539
|
+
f"during {phase} ({type(exc).__name__})\n",
|
|
540
|
+
)
|
|
541
|
+
stats = ExtractionStats(extractor_parse_latency_ms=parse_latency_ms)
|
|
542
|
+
stats.findings.append(
|
|
543
|
+
Finding(
|
|
544
|
+
subcode=FINDING_TOO_COMPLEX,
|
|
545
|
+
severity="warning",
|
|
546
|
+
message=(
|
|
547
|
+
f"{file_path}: source nesting too complex to extract "
|
|
548
|
+
f"({type(exc).__name__} during {phase}); file ingested as a "
|
|
549
|
+
"degraded module entity"
|
|
550
|
+
),
|
|
551
|
+
metadata={"phase": phase, "exception": type(exc).__name__},
|
|
552
|
+
),
|
|
553
|
+
)
|
|
554
|
+
return ExtractResult(
|
|
555
|
+
[_build_module_entity(source, dotted_module, file_path, "too_complex", docstring)],
|
|
556
|
+
[],
|
|
557
|
+
stats,
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
|
|
472
561
|
def _elapsed_ms(started_ns: int) -> int:
|
|
473
562
|
return max(1, (time.perf_counter_ns() - started_ns + 999_999) // 1_000_000)
|
|
474
563
|
|
|
@@ -710,6 +799,7 @@ class _ReferenceSiteCollector(ast.NodeVisitor):
|
|
|
710
799
|
if _has_overload_decorator(node):
|
|
711
800
|
return
|
|
712
801
|
function_id = self._entity_id_for_scope("function", node)
|
|
802
|
+
self._collect_decorator_sites(node, function_id)
|
|
713
803
|
self.owner_stack.append(function_id)
|
|
714
804
|
self.bound_stack.append(_scope_local_names(node))
|
|
715
805
|
self._visit_function_signature(node)
|
|
@@ -722,6 +812,16 @@ class _ReferenceSiteCollector(ast.NodeVisitor):
|
|
|
722
812
|
|
|
723
813
|
def visit_ClassDef(self, node: ast.ClassDef) -> None:
|
|
724
814
|
class_id = self._entity_id_for_scope("class", node)
|
|
815
|
+
self._collect_decorator_sites(node, class_id)
|
|
816
|
+
# Base expressions become `base` relation sites owned by the subclass
|
|
817
|
+
# (resolved into `inherits_from` edges). Call bases (`class X(make())`)
|
|
818
|
+
# are outside the relation envelope: the call's *result* is the base,
|
|
819
|
+
# so anchoring the callee would assert a false inheritance fact.
|
|
820
|
+
# Keyword arguments (`metaclass=...`) are likewise out of scope.
|
|
821
|
+
for base in node.bases:
|
|
822
|
+
anchor = _relation_anchor(base)
|
|
823
|
+
if anchor is not None:
|
|
824
|
+
self.sites.append(self._site_for_relation(anchor, class_id, "base"))
|
|
725
825
|
self.owner_stack.append(class_id)
|
|
726
826
|
self.bound_stack.append(_scope_local_names(node))
|
|
727
827
|
self.parents.append(node)
|
|
@@ -793,6 +893,59 @@ class _ReferenceSiteCollector(ast.NodeVisitor):
|
|
|
793
893
|
)
|
|
794
894
|
return entity_id(_PLUGIN_ID, kind, qualified_name)
|
|
795
895
|
|
|
896
|
+
def _collect_decorator_sites(
|
|
897
|
+
self,
|
|
898
|
+
node: ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef,
|
|
899
|
+
owner_id: str,
|
|
900
|
+
) -> None:
|
|
901
|
+
"""Emit one ``decorator`` relation site per reducible decorator.
|
|
902
|
+
|
|
903
|
+
``owner_id`` is the *decorated* entity (the site owner); the resolver
|
|
904
|
+
inverts direction at edge construction so the stored edge reads
|
|
905
|
+
``decorator decorates decorated`` (`from_id` = decorator entity).
|
|
906
|
+
Factory decorators (``@app.route("/x")``) reduce to the callee path —
|
|
907
|
+
the factory is the entity that decorates, its arguments are not part
|
|
908
|
+
of the relation token.
|
|
909
|
+
"""
|
|
910
|
+
for decorator in node.decorator_list:
|
|
911
|
+
target = decorator.func if isinstance(decorator, ast.Call) else decorator
|
|
912
|
+
anchor = _relation_anchor(target)
|
|
913
|
+
if anchor is not None:
|
|
914
|
+
self.sites.append(self._site_for_relation(anchor, owner_id, "decorator"))
|
|
915
|
+
|
|
916
|
+
def _site_for_relation(
|
|
917
|
+
self,
|
|
918
|
+
anchor: ast.Name | ast.Attribute,
|
|
919
|
+
from_id: str,
|
|
920
|
+
kind: Literal["base", "decorator"],
|
|
921
|
+
) -> ReferenceSite:
|
|
922
|
+
"""Build a relation site anchored on the full dotted path token.
|
|
923
|
+
|
|
924
|
+
The anchored byte range covers the whole path (Rust parity: the
|
|
925
|
+
implemented-trait PATH's span in ``impl Tr for Foo``), while the
|
|
926
|
+
pyright query position lands on the *final* attribute segment so
|
|
927
|
+
``helpers.Base`` resolves the class, not the module prefix.
|
|
928
|
+
"""
|
|
929
|
+
start_line = anchor.lineno - 1
|
|
930
|
+
end_line = (anchor.end_lineno or anchor.lineno) - 1
|
|
931
|
+
end_col = anchor.end_col_offset if anchor.end_col_offset is not None else anchor.col_offset
|
|
932
|
+
if isinstance(anchor, ast.Attribute):
|
|
933
|
+
query_line = end_line
|
|
934
|
+
query_byte_col = end_col - len(anchor.attr.encode("utf-8"))
|
|
935
|
+
else:
|
|
936
|
+
query_line = start_line
|
|
937
|
+
query_byte_col = anchor.col_offset
|
|
938
|
+
return ReferenceSite(
|
|
939
|
+
from_id=from_id,
|
|
940
|
+
line=query_line,
|
|
941
|
+
character=_byte_col_to_lsp_character(self.source_lines[query_line], query_byte_col),
|
|
942
|
+
end_line=end_line,
|
|
943
|
+
end_character=_byte_col_to_lsp_character(self.source_lines[end_line], end_col),
|
|
944
|
+
source_byte_start=self.line_starts[start_line] + anchor.col_offset,
|
|
945
|
+
source_byte_end=self.line_starts[end_line] + end_col,
|
|
946
|
+
kind=kind,
|
|
947
|
+
)
|
|
948
|
+
|
|
796
949
|
def _site_for_name(self, node: ast.Name) -> ReferenceSite:
|
|
797
950
|
line = node.lineno - 1
|
|
798
951
|
end_line = (node.end_lineno or node.lineno) - 1
|
|
@@ -811,6 +964,23 @@ class _ReferenceSiteCollector(ast.NodeVisitor):
|
|
|
811
964
|
)
|
|
812
965
|
|
|
813
966
|
|
|
967
|
+
def _relation_anchor(expr: ast.expr) -> ast.Name | ast.Attribute | None:
|
|
968
|
+
"""Reduce a base/decorator expression to its resolvable path token.
|
|
969
|
+
|
|
970
|
+
``Name`` and ``Attribute`` are the anchor itself; ``Subscript`` reduces to
|
|
971
|
+
its value (``Generic[T]`` → ``Generic``). Anything else (calls, literals,
|
|
972
|
+
conditional expressions) has no stable path token and yields no relation
|
|
973
|
+
site — the resolution-side precise-entity discipline would drop it anyway.
|
|
974
|
+
"""
|
|
975
|
+
match expr:
|
|
976
|
+
case ast.Name() | ast.Attribute():
|
|
977
|
+
return expr
|
|
978
|
+
case ast.Subscript(value=value):
|
|
979
|
+
return _relation_anchor(value)
|
|
980
|
+
case _:
|
|
981
|
+
return None
|
|
982
|
+
|
|
983
|
+
|
|
814
984
|
def _line_starts(source: str) -> tuple[int, ...]:
|
|
815
985
|
starts = [0]
|
|
816
986
|
total = 0
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import ast
|
|
4
4
|
import ctypes
|
|
5
5
|
import ctypes.util
|
|
6
|
+
import errno
|
|
6
7
|
import json
|
|
7
8
|
import math
|
|
8
9
|
import os
|
|
@@ -41,6 +42,8 @@ FINDING_PYRIGHT_POISON_FRAME = "LMWV-PY-PYRIGHT-POISON-FRAME"
|
|
|
41
42
|
FINDING_PYRIGHT_INIT_TIMEOUT = "LMWV-PY-PYRIGHT-INIT-TIMEOUT"
|
|
42
43
|
FINDING_PYRIGHT_UNAVAILABLE = "LMWV-PY-PYRIGHT-UNAVAILABLE"
|
|
43
44
|
FINDING_PYRIGHT_INSTALL_FAILURE = "LMWV-PY-PYRIGHT-INSTALL-FAILURE"
|
|
45
|
+
FINDING_PYRIGHT_SPAWN_DEFERRED = "LMWV-PY-PYRIGHT-SPAWN-DEFERRED"
|
|
46
|
+
FINDING_PYRIGHT_RESOURCE_EXHAUSTED = "LMWV-PY-PYRIGHT-RESOURCE-EXHAUSTED"
|
|
44
47
|
FINDING_PYRIGHT_CALL_RESOLUTION_TIMEOUT = "LMWV-PY-CALL-RESOLUTION-TIMEOUT"
|
|
45
48
|
FINDING_PYRIGHT_REFERENCE_RESOLUTION_TIMEOUT = "LMWV-PY-REFERENCE-RESOLUTION-TIMEOUT"
|
|
46
49
|
FINDING_PYRIGHT_REFERENCE_SITE_CAP = "LMWV-PY-REFERENCE-SITE-CAP"
|
|
@@ -56,21 +59,44 @@ class PyrightRunState:
|
|
|
56
59
|
consume ``ceil(N/25) * 3`` restarts instead of 3 for an entire analysis
|
|
57
60
|
run. Pass the same ``PyrightRunState`` instance to every successive
|
|
58
61
|
``PyrightSession`` so the budget is enforced across the full run.
|
|
62
|
+
|
|
63
|
+
``consecutive_spawn_deferrals`` tracks transient (resource-pressure) spawn
|
|
64
|
+
failures separately from the ``restart_count`` crash budget: it is reset to
|
|
65
|
+
zero on every successful spawn, so intermittent pressure never poisons the
|
|
66
|
+
run, while a sustained run of deferrals still terminates pyright once it
|
|
67
|
+
exceeds ``MAX_CONSECUTIVE_SPAWN_DEFERRALS``.
|
|
59
68
|
"""
|
|
60
69
|
|
|
61
70
|
restart_count: int = 0
|
|
62
71
|
disabled: bool = False
|
|
72
|
+
consecutive_spawn_deferrals: int = 0
|
|
63
73
|
|
|
64
74
|
|
|
65
75
|
MAX_UNRESOLVED_CALLEE_EXPR_BYTES = 512
|
|
66
76
|
MAX_PYRIGHT_RESTARTS_PER_RUN = 3
|
|
77
|
+
# A spawn that fails with one of these errnos is a *transient* resource-pressure
|
|
78
|
+
# condition (the host is momentarily out of process slots / memory), not a broken
|
|
79
|
+
# install. EAGAIN in particular is what a busy workstation returns from fork(2)
|
|
80
|
+
# when the per-UID RLIMIT_NPROC is hit. These are deferred-and-retried rather
|
|
81
|
+
# than treated as a permanent install failure.
|
|
82
|
+
_TRANSIENT_SPAWN_ERRNOS = frozenset({errno.EAGAIN, errno.ENOMEM, errno.EMFILE, errno.ENFILE})
|
|
83
|
+
# Upper bound on *consecutive* transient spawn deferrals before pyright is
|
|
84
|
+
# disabled for the run. Reset to zero on any successful spawn, so this only
|
|
85
|
+
# fires under sustained pressure, never on an intermittent blip. A failed fork
|
|
86
|
+
# costs microseconds, so retrying once per file across a large run is cheap.
|
|
87
|
+
MAX_CONSECUTIVE_SPAWN_DEFERRALS = 50
|
|
67
88
|
MAX_REFERENCE_SITES_PER_FILE = 2000
|
|
68
89
|
PYRIGHT_INIT_TIMEOUT_SECS = 30.0
|
|
69
90
|
PYRIGHT_CALL_TIMEOUT_SECS = 5.0
|
|
70
|
-
|
|
91
|
+
# Per-file wall-clock budget for reference resolution. Large, heavily-typed
|
|
92
|
+
# files (e.g. numpy/torch-vectorised ML code) can starve a tighter budget and
|
|
93
|
+
# surface LMWV-PY-REFERENCE-RESOLUTION-TIMEOUT findings with edges left
|
|
94
|
+
# unresolved. Such files are rare enough that the extra ceiling is worth the
|
|
95
|
+
# more-complete graph.
|
|
96
|
+
PYRIGHT_FILE_TIMEOUT_SECS = 10.0
|
|
71
97
|
STDERR_TAIL_LIMIT = 65536
|
|
72
98
|
PYRIGHT_EXCLUDE_PATTERNS = [
|
|
73
|
-
"**/.
|
|
99
|
+
"**/.weft/**",
|
|
74
100
|
"**/.git/**",
|
|
75
101
|
"**/.hg/**",
|
|
76
102
|
"**/.svn/**",
|
|
@@ -79,7 +105,7 @@ PYRIGHT_EXCLUDE_PATTERNS = [
|
|
|
79
105
|
"**/__pycache__/**",
|
|
80
106
|
"**/node_modules/**",
|
|
81
107
|
]
|
|
82
|
-
PROJECT_LOCAL_EXTERNAL_DIRS = {".
|
|
108
|
+
PROJECT_LOCAL_EXTERNAL_DIRS = {".weft", ".git", ".hg", ".svn", ".jj", ".venv", "node_modules"}
|
|
83
109
|
|
|
84
110
|
|
|
85
111
|
if TYPE_CHECKING:
|
|
@@ -144,6 +170,7 @@ class _FunctionIndex:
|
|
|
144
170
|
|
|
145
171
|
@dataclass
|
|
146
172
|
class _ReferenceEdgeAccumulator:
|
|
173
|
+
kind: Literal["references", "inherits_from", "decorates"]
|
|
147
174
|
from_id: str
|
|
148
175
|
to_id: str
|
|
149
176
|
source_byte_start: int
|
|
@@ -151,6 +178,17 @@ class _ReferenceEdgeAccumulator:
|
|
|
151
178
|
candidates: set[str]
|
|
152
179
|
|
|
153
180
|
|
|
181
|
+
# Site kind → emitted edge kind (clarion-43416be550). `name`/`annotation`
|
|
182
|
+
# sites keep producing `references`; the two relation kinds map onto the
|
|
183
|
+
# ontology kinds that were previously declared-but-dead for Python.
|
|
184
|
+
_EDGE_KIND_BY_SITE_KIND: dict[str, Literal["references", "inherits_from", "decorates"]] = {
|
|
185
|
+
"name": "references",
|
|
186
|
+
"annotation": "references",
|
|
187
|
+
"base": "inherits_from",
|
|
188
|
+
"decorator": "decorates",
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
154
192
|
class PyrightSession:
|
|
155
193
|
def __init__( # noqa: PLR0913 - knobs are tested lifecycle boundaries.
|
|
156
194
|
self,
|
|
@@ -484,7 +522,7 @@ class PyrightSession:
|
|
|
484
522
|
},
|
|
485
523
|
)
|
|
486
524
|
try:
|
|
487
|
-
accumulators: dict[tuple[str, str], _ReferenceEdgeAccumulator] = {}
|
|
525
|
+
accumulators: dict[tuple[str, str, str], _ReferenceEdgeAccumulator] = {}
|
|
488
526
|
lookup_cache: dict[
|
|
489
527
|
tuple[str, str, str, int, int, int, int], tuple[list[str], bool]
|
|
490
528
|
] = {}
|
|
@@ -518,6 +556,7 @@ class PyrightSession:
|
|
|
518
556
|
deadline=deadline,
|
|
519
557
|
)
|
|
520
558
|
saw_external = saw_external or fallback_external
|
|
559
|
+
candidate_ids = _filter_relation_candidates(site, candidate_ids)
|
|
521
560
|
except LspTimeoutError as exc:
|
|
522
561
|
self._record_finding(
|
|
523
562
|
FINDING_PYRIGHT_REFERENCE_RESOLUTION_TIMEOUT,
|
|
@@ -568,7 +607,13 @@ class PyrightSession:
|
|
|
568
607
|
},
|
|
569
608
|
self._budgeted_timeout(deadline),
|
|
570
609
|
)
|
|
571
|
-
|
|
610
|
+
# Relation sites (base/decorator) resolve to precise entities only:
|
|
611
|
+
# the module-id coarse fallback would mint nonsense facts like
|
|
612
|
+
# "class inherits_from module" for aliased bases.
|
|
613
|
+
return self._target_ids_from_locations(
|
|
614
|
+
result,
|
|
615
|
+
precise_only=site.kind in ("base", "decorator"),
|
|
616
|
+
)
|
|
572
617
|
|
|
573
618
|
def _deadline_for_file(self, path: Path) -> float:
|
|
574
619
|
return self._file_deadlines.setdefault(
|
|
@@ -591,19 +636,32 @@ class PyrightSession:
|
|
|
591
636
|
def _file_budget_expired(self, deadline: float) -> bool:
|
|
592
637
|
return deadline - time.monotonic() <= 0
|
|
593
638
|
|
|
594
|
-
def _target_ids_from_locations(
|
|
639
|
+
def _target_ids_from_locations(
|
|
640
|
+
self,
|
|
641
|
+
result: object,
|
|
642
|
+
*,
|
|
643
|
+
precise_only: bool = False,
|
|
644
|
+
) -> tuple[list[str], bool]:
|
|
595
645
|
locations = result if isinstance(result, list) else [result]
|
|
596
646
|
candidate_ids: set[str] = set()
|
|
597
647
|
saw_external = False
|
|
598
648
|
for location in locations:
|
|
599
|
-
target_id, external = self._target_id_from_location(
|
|
649
|
+
target_id, external = self._target_id_from_location(
|
|
650
|
+
location,
|
|
651
|
+
precise_only=precise_only,
|
|
652
|
+
)
|
|
600
653
|
if external:
|
|
601
654
|
saw_external = True
|
|
602
655
|
if target_id is not None:
|
|
603
656
|
candidate_ids.add(target_id)
|
|
604
657
|
return sorted(candidate_ids), saw_external
|
|
605
658
|
|
|
606
|
-
def _target_id_from_location(
|
|
659
|
+
def _target_id_from_location(
|
|
660
|
+
self,
|
|
661
|
+
location: object,
|
|
662
|
+
*,
|
|
663
|
+
precise_only: bool = False,
|
|
664
|
+
) -> tuple[str | None, bool]:
|
|
607
665
|
if not isinstance(location, dict):
|
|
608
666
|
return None, False
|
|
609
667
|
raw_uri = location.get("uri")
|
|
@@ -625,6 +683,8 @@ class PyrightSession:
|
|
|
625
683
|
key = _range_start_key(raw_range)
|
|
626
684
|
if key is not None and key in target_index.entity_by_name_position:
|
|
627
685
|
return target_index.entity_by_name_position[key], False
|
|
686
|
+
if precise_only:
|
|
687
|
+
return None, False
|
|
628
688
|
return target_index.module_id, False
|
|
629
689
|
|
|
630
690
|
def _ensure_process(self) -> bool:
|
|
@@ -711,14 +771,7 @@ class PyrightSession:
|
|
|
711
771
|
preexec_fn=preexec_fn, # noqa: PLW1509
|
|
712
772
|
)
|
|
713
773
|
except OSError as exc:
|
|
714
|
-
self.
|
|
715
|
-
self._record_finding(
|
|
716
|
-
FINDING_PYRIGHT_INSTALL_FAILURE,
|
|
717
|
-
"pyright-langserver failed to start",
|
|
718
|
-
executable=executable,
|
|
719
|
-
error=str(exc),
|
|
720
|
-
)
|
|
721
|
-
return False
|
|
774
|
+
return self._handle_spawn_oserror(exc, executable)
|
|
722
775
|
|
|
723
776
|
self._process = process
|
|
724
777
|
self._start_stderr_drain(process)
|
|
@@ -745,8 +798,57 @@ class PyrightSession:
|
|
|
745
798
|
process.kill()
|
|
746
799
|
process.wait(timeout=2)
|
|
747
800
|
return False
|
|
801
|
+
# A clean spawn + handshake clears any accumulated transient-deferral
|
|
802
|
+
# pressure: the per-UID resource squeeze that caused earlier EAGAINs has
|
|
803
|
+
# eased, so the run is healthy again.
|
|
804
|
+
self._run_state.consecutive_spawn_deferrals = 0
|
|
748
805
|
return True
|
|
749
806
|
|
|
807
|
+
def _handle_spawn_oserror(self, exc: OSError, executable: str) -> bool:
|
|
808
|
+
"""Triage a ``subprocess.Popen`` failure into transient vs. permanent.
|
|
809
|
+
|
|
810
|
+
``EAGAIN``/``ENOMEM``/``EMFILE``/``ENFILE`` are *transient*
|
|
811
|
+
resource-pressure errors: a busy host momentarily out of process slots,
|
|
812
|
+
memory, or file descriptors. The spawn is deferred — ``self._process``
|
|
813
|
+
stays ``None`` and ``disabled`` is left unset, so the next file retries a
|
|
814
|
+
fresh spawn — and only a sustained run of deferrals
|
|
815
|
+
(``MAX_CONSECUTIVE_SPAWN_DEFERRALS``) gives up. Any other errno (notably
|
|
816
|
+
``ENOENT``/``EACCES``) is a genuine, permanent install defect and
|
|
817
|
+
disables pyright for the rest of the run.
|
|
818
|
+
"""
|
|
819
|
+
if exc.errno in _TRANSIENT_SPAWN_ERRNOS:
|
|
820
|
+
self._run_state.consecutive_spawn_deferrals += 1
|
|
821
|
+
if self._run_state.consecutive_spawn_deferrals > MAX_CONSECUTIVE_SPAWN_DEFERRALS:
|
|
822
|
+
self._run_state.disabled = True
|
|
823
|
+
self._record_finding(
|
|
824
|
+
FINDING_PYRIGHT_RESOURCE_EXHAUSTED,
|
|
825
|
+
"pyright-langserver persistently unavailable under resource "
|
|
826
|
+
"pressure; skipping call resolution",
|
|
827
|
+
executable=executable,
|
|
828
|
+
consecutive_spawn_deferrals=self._run_state.consecutive_spawn_deferrals,
|
|
829
|
+
error=str(exc),
|
|
830
|
+
)
|
|
831
|
+
return False
|
|
832
|
+
# Emit one finding per pressure *episode* (the 0 -> 1 transition),
|
|
833
|
+
# not one per deferred file, so a busy run is not buried in findings.
|
|
834
|
+
if self._run_state.consecutive_spawn_deferrals == 1:
|
|
835
|
+
self._record_finding(
|
|
836
|
+
FINDING_PYRIGHT_SPAWN_DEFERRED,
|
|
837
|
+
"pyright-langserver spawn deferred under resource pressure; "
|
|
838
|
+
"will retry on subsequent files",
|
|
839
|
+
executable=executable,
|
|
840
|
+
error=str(exc),
|
|
841
|
+
)
|
|
842
|
+
return False
|
|
843
|
+
self._run_state.disabled = True
|
|
844
|
+
self._record_finding(
|
|
845
|
+
FINDING_PYRIGHT_INSTALL_FAILURE,
|
|
846
|
+
"pyright-langserver failed to start",
|
|
847
|
+
executable=executable,
|
|
848
|
+
error=str(exc),
|
|
849
|
+
)
|
|
850
|
+
return False
|
|
851
|
+
|
|
750
852
|
def _initialize(self) -> None:
|
|
751
853
|
result = self._request(
|
|
752
854
|
"initialize",
|
|
@@ -1143,17 +1245,31 @@ def _has_overload_decorator(node: ast.FunctionDef | ast.AsyncFunctionDef) -> boo
|
|
|
1143
1245
|
|
|
1144
1246
|
|
|
1145
1247
|
def _merge_reference_site(
|
|
1146
|
-
accumulators: dict[tuple[str, str], _ReferenceEdgeAccumulator],
|
|
1248
|
+
accumulators: dict[tuple[str, str, str], _ReferenceEdgeAccumulator],
|
|
1147
1249
|
site: ReferenceSite,
|
|
1148
1250
|
candidate_ids: Sequence[str],
|
|
1149
1251
|
) -> None:
|
|
1252
|
+
"""Fold one resolved site into the per-file edge accumulators.
|
|
1253
|
+
|
|
1254
|
+
The site kind selects the edge kind (``_EDGE_KIND_BY_SITE_KIND``).
|
|
1255
|
+
``decorator`` sites invert direction: the site owner is the *decorated*
|
|
1256
|
+
entity, but the stored edge reads ``decorator decorates decorated``
|
|
1257
|
+
(ADR-051: from_id = decorator entity, to_id = decorated entity), so the
|
|
1258
|
+
resolved candidate becomes ``from_id``. Ambiguous candidates therefore
|
|
1259
|
+
list alternative decorators (from-side) rather than alternative targets.
|
|
1260
|
+
"""
|
|
1150
1261
|
sorted_candidates = sorted(set(candidate_ids))
|
|
1151
|
-
|
|
1152
|
-
|
|
1262
|
+
edge_kind = _EDGE_KIND_BY_SITE_KIND[site.kind]
|
|
1263
|
+
if site.kind == "decorator":
|
|
1264
|
+
from_id, to_id = sorted_candidates[0], site.from_id
|
|
1265
|
+
else:
|
|
1266
|
+
from_id, to_id = site.from_id, sorted_candidates[0]
|
|
1267
|
+
key = (edge_kind, from_id, to_id)
|
|
1153
1268
|
existing = accumulators.get(key)
|
|
1154
1269
|
if existing is None:
|
|
1155
1270
|
accumulators[key] = _ReferenceEdgeAccumulator(
|
|
1156
|
-
|
|
1271
|
+
kind=edge_kind,
|
|
1272
|
+
from_id=from_id,
|
|
1157
1273
|
to_id=to_id,
|
|
1158
1274
|
source_byte_start=site.source_byte_start,
|
|
1159
1275
|
source_byte_end=site.source_byte_end,
|
|
@@ -1169,6 +1285,22 @@ def _merge_reference_site(
|
|
|
1169
1285
|
existing.source_byte_end = site.source_byte_end
|
|
1170
1286
|
|
|
1171
1287
|
|
|
1288
|
+
def _filter_relation_candidates(site: ReferenceSite, candidate_ids: list[str]) -> list[str]:
|
|
1289
|
+
"""Apply the relation-site target discipline (Rust derives/implements parity).
|
|
1290
|
+
|
|
1291
|
+
``inherits_from`` targets must be class entities — a base name resolving
|
|
1292
|
+
to a function (factory alias, shadowing ``def``) is dropped rather than
|
|
1293
|
+
stored as a class-inherits-function fact, mirroring the Rust resolver's
|
|
1294
|
+
``rust:trait:`` kind filter. Both relation kinds drop self-edges
|
|
1295
|
+
(``class X(X)`` resolving the in-definition name to itself).
|
|
1296
|
+
"""
|
|
1297
|
+
if site.kind == "base":
|
|
1298
|
+
candidate_ids = [cid for cid in candidate_ids if cid.startswith("python:class:")]
|
|
1299
|
+
if site.kind in ("base", "decorator"):
|
|
1300
|
+
candidate_ids = [cid for cid in candidate_ids if cid != site.from_id]
|
|
1301
|
+
return candidate_ids
|
|
1302
|
+
|
|
1303
|
+
|
|
1172
1304
|
def _reference_lookup_cache_key(
|
|
1173
1305
|
site: ReferenceSite,
|
|
1174
1306
|
source_bytes: bytes,
|
|
@@ -1186,7 +1318,7 @@ def _reference_lookup_cache_key(
|
|
|
1186
1318
|
|
|
1187
1319
|
|
|
1188
1320
|
def _sorted_reference_accumulators(
|
|
1189
|
-
accumulators: dict[tuple[str, str], _ReferenceEdgeAccumulator],
|
|
1321
|
+
accumulators: dict[tuple[str, str, str], _ReferenceEdgeAccumulator],
|
|
1190
1322
|
) -> list[_ReferenceEdgeAccumulator]:
|
|
1191
1323
|
return sorted(
|
|
1192
1324
|
accumulators.values(),
|
|
@@ -1204,7 +1336,7 @@ def _reference_accumulator_to_edge(
|
|
|
1204
1336
|
) -> ReferencesRawEdge:
|
|
1205
1337
|
candidates = sorted(accumulator.candidates)
|
|
1206
1338
|
edge: ReferencesRawEdge = {
|
|
1207
|
-
"kind":
|
|
1339
|
+
"kind": accumulator.kind,
|
|
1208
1340
|
"from_id": accumulator.from_id,
|
|
1209
1341
|
"to_id": accumulator.to_id,
|
|
1210
1342
|
"source_byte_start": accumulator.source_byte_start,
|
|
@@ -19,9 +19,9 @@ the same bare Python ``__qualname__`` semantics that Wardline stores in its
|
|
|
19
19
|
dotted module path elsewhere; ADR-018 requires cross-product joins to translate
|
|
20
20
|
between those shapes instead of comparing strings directly.
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
entities;
|
|
24
|
-
|
|
22
|
+
``FunctionDef``, ``AsyncFunctionDef``, and ``ClassDef`` are all emitted
|
|
23
|
+
entities (class entities since WP3); every one of the three also acts as
|
|
24
|
+
a parent scope for qualname reconstruction.
|
|
25
25
|
"""
|
|
26
26
|
|
|
27
27
|
from __future__ import annotations
|
|
@@ -10,7 +10,12 @@ if TYPE_CHECKING:
|
|
|
10
10
|
from loomweave_plugin_python.call_resolver import Finding
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
# ``name``/``annotation`` resolve into ``references`` edges; ``base`` and
|
|
14
|
+
# ``decorator`` are relation sites resolving into ``inherits_from`` /
|
|
15
|
+
# ``decorates`` edges respectively (clarion-43416be550). All four ride the
|
|
16
|
+
# same pyright resolution machinery (external-skip, per-file cap, lookup
|
|
17
|
+
# cache); the site kind selects the emitted edge kind.
|
|
18
|
+
ReferenceSiteKind = Literal["name", "annotation", "base", "decorator"]
|
|
14
19
|
|
|
15
20
|
|
|
16
21
|
@dataclass(frozen=True)
|
|
@@ -30,7 +35,16 @@ class ReferencesEdgeProperties(TypedDict):
|
|
|
30
35
|
|
|
31
36
|
|
|
32
37
|
class ReferencesRawEdge(TypedDict):
|
|
33
|
-
|
|
38
|
+
"""Anchored edge produced by the reference-site resolution pass.
|
|
39
|
+
|
|
40
|
+
Despite the name (historic — B.5* shipped ``references`` alone), this is
|
|
41
|
+
the wire shape for all three site-derived edge kinds. ``candidates`` in
|
|
42
|
+
``properties`` is present only on ambiguous edges; for ``decorates`` the
|
|
43
|
+
candidates are alternative *from*-side decorator entities (direction is
|
|
44
|
+
inverted relative to the site owner).
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
kind: Literal["references", "inherits_from", "decorates"]
|
|
34
48
|
from_id: str
|
|
35
49
|
to_id: str
|
|
36
50
|
source_byte_start: int
|
|
@@ -34,7 +34,7 @@ from loomweave_plugin_python.pyright_session import PyrightRunState, PyrightSess
|
|
|
34
34
|
from loomweave_plugin_python.stdout_guard import install_stdio
|
|
35
35
|
from loomweave_plugin_python.wardline_descriptor import WardlineVocabulary, load_wardline_descriptor
|
|
36
36
|
|
|
37
|
-
ONTOLOGY_VERSION = "0.
|
|
37
|
+
ONTOLOGY_VERSION = "0.8.0"
|
|
38
38
|
|
|
39
39
|
# Plugin-side Content-Length sanity cap. Matches the host's ADR-021 §2b
|
|
40
40
|
# default (8 MiB) so the plugin never emits a frame the host would kill us
|
|
@@ -4,7 +4,7 @@ This module deliberately reads descriptor files without importing Wardline.
|
|
|
4
4
|
Wardline remains authoritative for the vocabulary; Loomweave records only the
|
|
5
5
|
source-observed decorator facts it can derive from that descriptor.
|
|
6
6
|
|
|
7
|
-
Two contract details below (``
|
|
7
|
+
Two contract details below (``PROJECT_DESCRIPTOR_PATHS`` and the descriptor
|
|
8
8
|
``version`` semantics) are Loomweave-side assumptions pending Wardline's
|
|
9
9
|
"Pre-Rust core hardening" Task B, which has not yet published the canonical
|
|
10
10
|
project-local descriptor location or the ``schema: wardline.vocabulary/v1``
|
|
@@ -28,7 +28,18 @@ import yaml
|
|
|
28
28
|
# location and descriptor-version semantics are not yet pinned by Wardline.
|
|
29
29
|
# Tracked: filigree clarion-6ab5668d82.
|
|
30
30
|
EXPECTED_DESCRIPTOR_VERSION = "wardline-generic-2"
|
|
31
|
-
|
|
31
|
+
|
|
32
|
+
# Weft store consolidation (ADR-046): sibling runtime state lives under the
|
|
33
|
+
# shared ``.weft/<member>/`` dotdir, so the Wardline descriptor is read only from
|
|
34
|
+
# the consolidated ``.weft/wardline/`` location. There is no fallback to the
|
|
35
|
+
# pre-consolidation ``.wardline/`` path: after the coordinated cutover every
|
|
36
|
+
# sibling is at ``.weft/`` by construction, so a descriptor found only on the
|
|
37
|
+
# legacy path means a mis-sequenced cutover; resolving it would silently bind a
|
|
38
|
+
# stale dir. Instead the project descriptor reads as absent and the loader falls
|
|
39
|
+
# through to the package descriptor (a loud, visible signal). Loomweave never
|
|
40
|
+
# writes a sibling's subtree — this is read-only.
|
|
41
|
+
WEFT_DESCRIPTOR_PATH = Path(".weft/wardline/vocabulary.yaml")
|
|
42
|
+
PROJECT_DESCRIPTOR_PATHS = (WEFT_DESCRIPTOR_PATH,)
|
|
32
43
|
|
|
33
44
|
DescriptorSource = Literal["project", "package"]
|
|
34
45
|
DescriptorStatus = Literal["enabled", "version_skew", "absent"]
|
|
@@ -97,13 +108,17 @@ def load_wardline_descriptor(project_root: Path | None) -> WardlineDescriptorSta
|
|
|
97
108
|
def _read_project_descriptor(project_root: Path | None) -> str | None:
|
|
98
109
|
if project_root is None:
|
|
99
110
|
return None
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
111
|
+
# Read only the consolidated .weft/wardline/ location (ADR-046); the
|
|
112
|
+
# pre-consolidation .wardline/ path is not consulted.
|
|
113
|
+
for relative in PROJECT_DESCRIPTOR_PATHS:
|
|
114
|
+
path = project_root / relative
|
|
115
|
+
if not path.is_file():
|
|
116
|
+
continue
|
|
117
|
+
try:
|
|
118
|
+
return path.read_text(encoding="utf-8")
|
|
119
|
+
except OSError:
|
|
120
|
+
return None
|
|
121
|
+
return None
|
|
107
122
|
|
|
108
123
|
|
|
109
124
|
def _read_package_descriptor() -> str | None:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[plugin]
|
|
2
2
|
name = "loomweave-plugin-python"
|
|
3
3
|
plugin_id = "python"
|
|
4
|
-
version = "1.
|
|
4
|
+
version = "1.2.0"
|
|
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.
|
|
@@ -25,6 +25,11 @@ wardline_aware = true
|
|
|
25
25
|
# v0.1 rejects `true` at initialize with LMWV-INFRA-MANIFEST-UNSUPPORTED-CAPABILITY.
|
|
26
26
|
reads_outside_project_root = false
|
|
27
27
|
|
|
28
|
+
# Declaring this sub-table marks the plugin as a language-server host: it spawns
|
|
29
|
+
# pyright-langserver (Node). The host detects this and leaves RLIMIT_NPROC
|
|
30
|
+
# uncapped for the plugin — RLIMIT_NPROC is per-UID-global and would otherwise
|
|
31
|
+
# fail pyright's fork() with EAGAIN on a busy workstation (see ADR-021,
|
|
32
|
+
# "process-count control", and host::effective_max_nproc).
|
|
28
33
|
[capabilities.runtime.pyright]
|
|
29
34
|
pin = "1.1.409"
|
|
30
35
|
|
|
@@ -32,19 +37,23 @@ pin = "1.1.409"
|
|
|
32
37
|
# Sprint 2 B.2: classes + modules join the kind set. B.3 (ADR-026) adds
|
|
33
38
|
# the first edge kind, `contains`. B.4* adds scan-time `calls` edges.
|
|
34
39
|
# B.5* adds scan-time `references` edges. Phase 3 Task 3 adds scan-time
|
|
35
|
-
# `imports` candidate edges.
|
|
40
|
+
# `imports` candidate edges. clarion-43416be550 adds scan-time
|
|
41
|
+
# `inherits_from` (subclass → base) + `decorates` (decorator → decorated)
|
|
42
|
+
# relation edges, resolved through the same pyright reference machinery.
|
|
36
43
|
entity_kinds = ["function", "class", "module"]
|
|
37
|
-
edge_kinds = ["contains", "calls", "references", "imports"]
|
|
44
|
+
edge_kinds = ["contains", "calls", "references", "imports", "inherits_from", "decorates"]
|
|
38
45
|
# Per ADR-022: uppercase `LMWV-{PLUGIN_ID_UPPER}-`. Reserved at parse
|
|
39
46
|
# against the LMWV-INFRA-* and LMWV-FACT-* namespaces.
|
|
40
47
|
rule_id_prefix = "LMWV-PY-"
|
|
41
48
|
# Bumps per ADR-027 when the entity/edge/rule set shifts. Phase 3 Task 3 is a
|
|
42
|
-
# MINOR bump (additive `imports` edge kind).
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
|
|
49
|
+
# MINOR bump (additive `imports` edge kind); 0.8.0 is the additive
|
|
50
|
+
# `inherits_from` + `decorates` MINOR bump (clarion-43416be550). NOTE:
|
|
51
|
+
# ADR-007's summary-cache key is the 5-tuple (entity_id, content_hash,
|
|
52
|
+
# prompt_template_id, model_tier, guidance_fingerprint) — ontology_version is
|
|
53
|
+
# handshake-validation, NOT a cache-key component. New edge rows miss the
|
|
54
|
+
# cache by component-1 of the 5-tuple organically (no edges live in the
|
|
55
|
+
# 5-tuple yet anyway).
|
|
56
|
+
ontology_version = "0.8.0"
|
|
48
57
|
|
|
49
58
|
[integrations.wardline]
|
|
50
59
|
expected_descriptor_version = "wardline-generic-2"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
loomweave_plugin_python/__init__.py,sha256=XS_Benpj-Em19GvyBlzkxZN4irB4JyXjbNcDwpaqmxE,95
|
|
2
|
+
loomweave_plugin_python/__main__.py,sha256=oy1S1Ru7WdX6jcAVhsqNI25LxAbQq3jHBlETBt20fyM,376
|
|
3
|
+
loomweave_plugin_python/call_resolver.py,sha256=V6w5AEmWgjkKuWdgUPJTXStOavmmbyvjP0PQOeT3xkU,1692
|
|
4
|
+
loomweave_plugin_python/entity_id.py,sha256=l7_5yyh6WVH9xIzrQVCDb2GmBMhovOVmuQezcZIVf8k,2691
|
|
5
|
+
loomweave_plugin_python/extractor.py,sha256=3S98y0CTjapmNnkU_-E5ySUXDeGyXjzQQsIZGhxB3HM,56261
|
|
6
|
+
loomweave_plugin_python/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
loomweave_plugin_python/pyright_session.py,sha256=QWgXlOSgmN29iZ059sJE6hIbogi2mSCR3aTMFITXoyY,67579
|
|
8
|
+
loomweave_plugin_python/qualname.py,sha256=gk6N2e4tMvufnPgpVZLv--c9_9gW5y9YvJIwUArffFg,2102
|
|
9
|
+
loomweave_plugin_python/reference_resolver.py,sha256=hfcKX3WcDL26vEq7Ahdouq7WBs7mqCNy-rBvl8t97iI,2698
|
|
10
|
+
loomweave_plugin_python/server.py,sha256=_MdaoRE1D-4VMvqLw79JyIvBmyqpU540Grk9FIBXSZg,12329
|
|
11
|
+
loomweave_plugin_python/stdout_guard.py,sha256=tUzcXyvXuy3-dXB81_xCNk8KWE3mqtsWSge81KPDhKg,2007
|
|
12
|
+
loomweave_plugin_python/wardline_descriptor.py,sha256=9rU-DhN1USpsHW9hS6YeWFRgGL4xgBeG41Yr1LfK-M4,8136
|
|
13
|
+
loomweave_plugin_python-1.2.0.data/data/share/loomweave/plugins/python/plugin.toml,sha256=H6rj0AKyM4FaXSHiseFKLpjz86i-yOpWwEsHEE13ios,3580
|
|
14
|
+
loomweave_plugin_python-1.2.0.dist-info/METADATA,sha256=ISJLdfmtVv2iRluZlTWF_M56_-_9jgztQsXRBM9XVOA,2751
|
|
15
|
+
loomweave_plugin_python-1.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
16
|
+
loomweave_plugin_python-1.2.0.dist-info/entry_points.txt,sha256=X1DQOYU5o1z2lBOJoh9JgQR5pg9fd_Yd8KozTHdfZqA,82
|
|
17
|
+
loomweave_plugin_python-1.2.0.dist-info/RECORD,,
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
loomweave_plugin_python/__init__.py,sha256=wmv6wivp3tnzIGHWWG9eO2JiGwxYZW9uqvggUibOLlA,95
|
|
2
|
-
loomweave_plugin_python/__main__.py,sha256=oy1S1Ru7WdX6jcAVhsqNI25LxAbQq3jHBlETBt20fyM,376
|
|
3
|
-
loomweave_plugin_python/call_resolver.py,sha256=V6w5AEmWgjkKuWdgUPJTXStOavmmbyvjP0PQOeT3xkU,1692
|
|
4
|
-
loomweave_plugin_python/entity_id.py,sha256=l7_5yyh6WVH9xIzrQVCDb2GmBMhovOVmuQezcZIVf8k,2691
|
|
5
|
-
loomweave_plugin_python/extractor.py,sha256=niapsUWBbHlQXI17eYY6teJoQB4XxF-Nh2fcqa7dvro,48316
|
|
6
|
-
loomweave_plugin_python/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
-
loomweave_plugin_python/pyright_session.py,sha256=0qXZFTzprefLYEaM7wMP6DjylR_ej9XqDbngQKIkvS4,60871
|
|
8
|
-
loomweave_plugin_python/qualname.py,sha256=7JVETZnQkgU1FxPbyTmzSVZDBm_2SrXGZlqUOjB51W0,2090
|
|
9
|
-
loomweave_plugin_python/reference_resolver.py,sha256=7r2QsM9tRWeYHeRD62-jBvByGkdLC-0AWYhhgKNBgV4,1871
|
|
10
|
-
loomweave_plugin_python/server.py,sha256=UARexfFun59sOpFSMWP7t6baLcOoi98Thbc_Gq6JQ3Q,12329
|
|
11
|
-
loomweave_plugin_python/stdout_guard.py,sha256=tUzcXyvXuy3-dXB81_xCNk8KWE3mqtsWSge81KPDhKg,2007
|
|
12
|
-
loomweave_plugin_python/wardline_descriptor.py,sha256=49yYtIUdUfv4FGKyINql5yEZ2JTOUkl8sJXF3J6fPF8,7197
|
|
13
|
-
loomweave_plugin_python-1.0.0.data/data/share/loomweave/plugins/python/plugin.toml,sha256=0H-_lBqflTkXsm_sqySmS6j0s7pGDQdDntkf1TcCRz0,2908
|
|
14
|
-
loomweave_plugin_python-1.0.0.dist-info/METADATA,sha256=UzoJmnhdF1IOPaIuIBRvwAVxlfkg37zoss3xUBCYem0,2751
|
|
15
|
-
loomweave_plugin_python-1.0.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
16
|
-
loomweave_plugin_python-1.0.0.dist-info/entry_points.txt,sha256=X1DQOYU5o1z2lBOJoh9JgQR5pg9fd_Yd8KozTHdfZqA,82
|
|
17
|
-
loomweave_plugin_python-1.0.0.dist-info/RECORD,,
|
|
File without changes
|
{loomweave_plugin_python-1.0.0.dist-info → loomweave_plugin_python-1.2.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|