loomweave-plugin-python 1.3.1__tar.gz → 1.4.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.3.1 → loomweave_plugin_python-1.4.0}/.gitignore +1 -0
- {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.4.0}/PKG-INFO +1 -1
- {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.4.0}/plugin.toml +5 -3
- {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.4.0}/pyproject.toml +1 -1
- {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.4.0}/src/loomweave_plugin_python/__init__.py +1 -1
- {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.4.0}/src/loomweave_plugin_python/extractor.py +74 -19
- {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.4.0}/src/loomweave_plugin_python/server.py +1 -1
- loomweave_plugin_python-1.4.0/tests/fixtures/wardline-vocabulary-descriptor.golden.yaml +14 -0
- {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.4.0}/tests/test_extractor.py +219 -0
- {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.4.0}/tests/test_package.py +3 -3
- {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.4.0}/tests/test_server.py +2 -2
- loomweave_plugin_python-1.4.0/tests/test_wardline_vocabulary_descriptor_conformance.py +309 -0
- {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.4.0}/uv.lock +1 -1
- {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.4.0}/README.md +0 -0
- {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.4.0}/src/loomweave_plugin_python/__main__.py +0 -0
- {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.4.0}/src/loomweave_plugin_python/call_resolver.py +0 -0
- {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.4.0}/src/loomweave_plugin_python/entity_id.py +0 -0
- {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.4.0}/src/loomweave_plugin_python/py.typed +0 -0
- {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.4.0}/src/loomweave_plugin_python/pyright_session.py +0 -0
- {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.4.0}/src/loomweave_plugin_python/qualname.py +0 -0
- {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.4.0}/src/loomweave_plugin_python/reference_resolver.py +0 -0
- {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.4.0}/src/loomweave_plugin_python/stdout_guard.py +0 -0
- {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.4.0}/src/loomweave_plugin_python/wardline_descriptor.py +0 -0
- {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.4.0}/tests/__init__.py +0 -0
- {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.4.0}/tests/test_dogfood_uniqueness.py +0 -0
- {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.4.0}/tests/test_entity_id.py +0 -0
- {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.4.0}/tests/test_pyright_session.py +0 -0
- {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.4.0}/tests/test_qualname.py +0 -0
- {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.4.0}/tests/test_round_trip.py +0 -0
- {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.4.0}/tests/test_stdout_guard.py +0 -0
- {loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.4.0}/tests/test_wardline_descriptor.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[plugin]
|
|
2
2
|
name = "loomweave-plugin-python"
|
|
3
3
|
plugin_id = "python"
|
|
4
|
-
version = "1.
|
|
4
|
+
version = "1.4.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.
|
|
@@ -47,13 +47,15 @@ edge_kinds = ["contains", "calls", "references", "imports", "inherits_from", "de
|
|
|
47
47
|
rule_id_prefix = "LMWV-PY-"
|
|
48
48
|
# Bumps per ADR-027 when the entity/edge/rule set shifts. Phase 3 Task 3 is a
|
|
49
49
|
# MINOR bump (additive `imports` edge kind); 0.8.0 is the additive
|
|
50
|
-
# `inherits_from` + `decorates` MINOR bump (clarion-43416be550).
|
|
50
|
+
# `inherits_from` + `decorates` MINOR bump (clarion-43416be550); 0.9.0 is the
|
|
51
|
+
# additive `public-surface` reachability-root tag (clarion-4ec50f3d92) — a
|
|
52
|
+
# no-`__all__` library's public defs/classes are now tagged as roots. NOTE:
|
|
51
53
|
# ADR-007's summary-cache key is the 5-tuple (entity_id, content_hash,
|
|
52
54
|
# prompt_template_id, model_tier, guidance_fingerprint) — ontology_version is
|
|
53
55
|
# handshake-validation, NOT a cache-key component. New edge rows miss the
|
|
54
56
|
# cache by component-1 of the 5-tuple organically (no edges live in the
|
|
55
57
|
# 5-tuple yet anyway).
|
|
56
|
-
ontology_version = "0.
|
|
58
|
+
ontology_version = "0.9.0"
|
|
57
59
|
|
|
58
60
|
[integrations.wardline]
|
|
59
61
|
expected_descriptor_version = "wardline-generic-2"
|
|
@@ -1058,7 +1058,10 @@ class _WalkState:
|
|
|
1058
1058
|
seen_ids: set[str]
|
|
1059
1059
|
file_path: str
|
|
1060
1060
|
wardline_vocabulary: WardlineVocabulary | None = None
|
|
1061
|
-
|
|
1061
|
+
# ``None`` means no ``__all__`` was declared (triggers the public-surface
|
|
1062
|
+
# heuristic); a set (possibly empty) means ``__all__`` was declared
|
|
1063
|
+
# explicitly (clarion-4ec50f3d92).
|
|
1064
|
+
exported_names: set[str] | None = None
|
|
1062
1065
|
duplicate_entities_dropped: int = 0
|
|
1063
1066
|
|
|
1064
1067
|
|
|
@@ -1274,16 +1277,39 @@ def _attach_wardline_entity_metadata(
|
|
|
1274
1277
|
}
|
|
1275
1278
|
|
|
1276
1279
|
|
|
1277
|
-
def _module_export_names(tree: ast.Module) -> set[str]:
|
|
1278
|
-
|
|
1280
|
+
def _module_export_names(tree: ast.Module) -> set[str] | None:
|
|
1281
|
+
"""Names declared in a module-level ``__all__``, or ``None`` when no
|
|
1282
|
+
``__all__`` is declared at all (clarion-4ec50f3d92).
|
|
1283
|
+
|
|
1284
|
+
The ``None`` vs empty-set distinction is load-bearing for the public-surface
|
|
1285
|
+
fallback: an explicit ``__all__ = []`` declares an *empty* public surface
|
|
1286
|
+
(no roots), whereas the *absence* of ``__all__`` triggers the PEP 8
|
|
1287
|
+
public-surface heuristic in :func:`_function_tags` / :func:`_class_tags`.
|
|
1288
|
+
Both plain (``__all__ = [...]``) and annotated (``__all__: list[str] = [...]``)
|
|
1289
|
+
assignments count as a declaration so an annotated ``__all__`` is never
|
|
1290
|
+
mistaken for an absent one. A non-literal value (``__all__ = a + b``, an
|
|
1291
|
+
alias, a comprehension) or an annotation-only stub (``__all__: list[str]``
|
|
1292
|
+
with no value) counts as a declaration but contributes no names — i.e. it is
|
|
1293
|
+
treated as an explicit *empty* surface, which suppresses ``public-surface``;
|
|
1294
|
+
this matches the prior behaviour for non-literal ``__all__`` (no names were
|
|
1295
|
+
ever extracted from it). Bare ``__all__ += [...]`` (``ast.AugAssign`` without a
|
|
1296
|
+
prior ``=`` assignment) is intentionally not handled: it is a runtime
|
|
1297
|
+
``NameError`` and so unreachable in valid Python."""
|
|
1298
|
+
exported: set[str] | None = None
|
|
1279
1299
|
for statement in tree.body:
|
|
1280
|
-
if
|
|
1300
|
+
if isinstance(statement, ast.Assign):
|
|
1301
|
+
targets: list[ast.expr] = statement.targets
|
|
1302
|
+
value: ast.expr | None = statement.value
|
|
1303
|
+
elif isinstance(statement, ast.AnnAssign):
|
|
1304
|
+
targets = [statement.target]
|
|
1305
|
+
value = statement.value
|
|
1306
|
+
else:
|
|
1281
1307
|
continue
|
|
1282
|
-
if not any(
|
|
1283
|
-
isinstance(target, ast.Name) and target.id == "__all__" for target in statement.targets
|
|
1284
|
-
):
|
|
1308
|
+
if not any(isinstance(target, ast.Name) and target.id == "__all__" for target in targets):
|
|
1285
1309
|
continue
|
|
1286
|
-
|
|
1310
|
+
if exported is None:
|
|
1311
|
+
exported = set()
|
|
1312
|
+
match value:
|
|
1287
1313
|
case ast.List(elts=elts) | ast.Tuple(elts=elts) | ast.Set(elts=elts):
|
|
1288
1314
|
for elt in elts:
|
|
1289
1315
|
if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
|
|
@@ -1321,16 +1347,18 @@ def _is_module_level(parents: list[ast.AST]) -> bool:
|
|
|
1321
1347
|
def _function_tags(
|
|
1322
1348
|
node: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
1323
1349
|
parents: list[ast.AST],
|
|
1324
|
-
exported_names: set[str],
|
|
1350
|
+
exported_names: set[str] | None,
|
|
1325
1351
|
) -> set[str]:
|
|
1326
1352
|
tags: set[str] = set()
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
if _is_module_level(parents) and node.name in exported_names:
|
|
1330
|
-
tags.add("exported-api")
|
|
1331
|
-
if node.name.startswith("test_") or any(
|
|
1353
|
+
module_level = _is_module_level(parents)
|
|
1354
|
+
is_test = node.name.startswith("test_") or any(
|
|
1332
1355
|
isinstance(parent, ast.ClassDef) and parent.name.startswith("Test") for parent in parents
|
|
1333
|
-
)
|
|
1356
|
+
)
|
|
1357
|
+
if module_level and node.name == "main":
|
|
1358
|
+
tags.add("entry-point")
|
|
1359
|
+
if module_level:
|
|
1360
|
+
tags.update(_module_surface_tag(node.name, exported_names, is_test=is_test))
|
|
1361
|
+
if is_test:
|
|
1334
1362
|
tags.add("test")
|
|
1335
1363
|
decorator_names = _decorator_names(node)
|
|
1336
1364
|
if any(_last_name(name) in _HTTP_ROUTE_DECORATOR_NAMES for name in decorator_names):
|
|
@@ -1340,11 +1368,38 @@ def _function_tags(
|
|
|
1340
1368
|
return tags
|
|
1341
1369
|
|
|
1342
1370
|
|
|
1343
|
-
def
|
|
1371
|
+
def _module_surface_tag(name: str, exported_names: set[str] | None, *, is_test: bool) -> set[str]:
|
|
1372
|
+
"""Root tag for a module-level public entity (clarion-4ec50f3d92).
|
|
1373
|
+
|
|
1374
|
+
With a declared ``__all__`` (``exported_names is not None``) only listed
|
|
1375
|
+
names are ``exported-api`` — the declaration is **authoritative**: a name the
|
|
1376
|
+
author explicitly exported is an exported root regardless of its spelling
|
|
1377
|
+
(so a ``Test``/``test_``-named or underscore-prefixed name listed in
|
|
1378
|
+
``__all__`` still gets ``exported-api``). With no ``__all__`` at all
|
|
1379
|
+
(``None``) every non-underscore, non-test module-level def/class is the
|
|
1380
|
+
module's public surface by PEP 8 convention, tagged ``public-surface``: a
|
|
1381
|
+
reachability root of lower confidence than a declared export. It is a
|
|
1382
|
+
fail-toward-live posture for public code invoked from outside the static call
|
|
1383
|
+
graph (a library's consumers, or an app's framework-dispatched / DI / CLI
|
|
1384
|
+
handlers), so a codebase that does not exhaustively declare ``__all__`` is not
|
|
1385
|
+
read as mostly dead. ``is_test`` gates only this inferred fallback — test
|
|
1386
|
+
entities are already roots via the ``test`` tag and are not a module's public
|
|
1387
|
+
API; it never suppresses a declared export."""
|
|
1388
|
+
if exported_names is None:
|
|
1389
|
+
if is_test or name.startswith("_"):
|
|
1390
|
+
return set()
|
|
1391
|
+
return {"public-surface"}
|
|
1392
|
+
return {"exported-api"} if name in exported_names else set()
|
|
1393
|
+
|
|
1394
|
+
|
|
1395
|
+
def _class_tags(
|
|
1396
|
+
node: ast.ClassDef, parents: list[ast.AST], exported_names: set[str] | None
|
|
1397
|
+
) -> set[str]:
|
|
1344
1398
|
tags: set[str] = set()
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1399
|
+
is_test = node.name.startswith("Test")
|
|
1400
|
+
if _is_module_level(parents):
|
|
1401
|
+
tags.update(_module_surface_tag(node.name, exported_names, is_test=is_test))
|
|
1402
|
+
if is_test:
|
|
1348
1403
|
tags.add("test")
|
|
1349
1404
|
decorator_names = _decorator_names(node)
|
|
1350
1405
|
base_names = [_expr_qualified_name(base) for base in node.bases]
|
|
@@ -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.9.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
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
schema: wardline.vocabulary/v1
|
|
2
|
+
version: wardline-generic-2
|
|
3
|
+
entries:
|
|
4
|
+
- canonical_name: external_boundary
|
|
5
|
+
group: 1
|
|
6
|
+
attrs: {}
|
|
7
|
+
- canonical_name: trust_boundary
|
|
8
|
+
group: 1
|
|
9
|
+
attrs:
|
|
10
|
+
_wardline_to_level: TaintState
|
|
11
|
+
- canonical_name: trusted
|
|
12
|
+
group: 1
|
|
13
|
+
attrs:
|
|
14
|
+
_wardline_level: TaintState
|
|
@@ -1262,6 +1262,225 @@ class Config:
|
|
|
1262
1262
|
assert "data-model" in config["tags"]
|
|
1263
1263
|
|
|
1264
1264
|
|
|
1265
|
+
def test_no_dunder_all_tags_public_module_surface_as_public_surface() -> None:
|
|
1266
|
+
"""clarion-4ec50f3d92: a module with no ``__all__`` falls back to the PEP 8
|
|
1267
|
+
public-surface heuristic — non-underscore module-level defs/classes become
|
|
1268
|
+
``public-surface`` reachability roots (a lower-confidence root than a
|
|
1269
|
+
declared ``exported-api``), while private, nested, and test entities do not."""
|
|
1270
|
+
|
|
1271
|
+
source = """\
|
|
1272
|
+
def public_fn():
|
|
1273
|
+
pass
|
|
1274
|
+
|
|
1275
|
+
|
|
1276
|
+
def _private_fn():
|
|
1277
|
+
pass
|
|
1278
|
+
|
|
1279
|
+
|
|
1280
|
+
def main():
|
|
1281
|
+
pass
|
|
1282
|
+
|
|
1283
|
+
|
|
1284
|
+
def test_thing():
|
|
1285
|
+
assert True
|
|
1286
|
+
|
|
1287
|
+
|
|
1288
|
+
class PublicClass:
|
|
1289
|
+
def method(self):
|
|
1290
|
+
pass
|
|
1291
|
+
|
|
1292
|
+
|
|
1293
|
+
class _PrivateClass:
|
|
1294
|
+
pass
|
|
1295
|
+
|
|
1296
|
+
|
|
1297
|
+
class TestThing:
|
|
1298
|
+
pass
|
|
1299
|
+
"""
|
|
1300
|
+
entities, _ = extract(source, "lib.py")
|
|
1301
|
+
by_id = {e["id"]: e for e in entities}
|
|
1302
|
+
|
|
1303
|
+
def tags(entity_id: str) -> list[str]:
|
|
1304
|
+
return by_id[entity_id].get("tags", [])
|
|
1305
|
+
|
|
1306
|
+
# Public module-level def/class → public-surface, never exported-api
|
|
1307
|
+
# (nothing was *declared*).
|
|
1308
|
+
assert "public-surface" in tags("python:function:lib.public_fn")
|
|
1309
|
+
assert "exported-api" not in tags("python:function:lib.public_fn")
|
|
1310
|
+
assert "public-surface" in tags("python:class:lib.PublicClass")
|
|
1311
|
+
|
|
1312
|
+
# A no-__all__ module-level `main` is both an entry-point and public surface;
|
|
1313
|
+
# the two roots coexist (pinned so a future reader does not assume exclusivity).
|
|
1314
|
+
assert "entry-point" in tags("python:function:lib.main")
|
|
1315
|
+
assert "public-surface" in tags("python:function:lib.main")
|
|
1316
|
+
|
|
1317
|
+
# Underscore-prefixed names are non-public by convention → no root.
|
|
1318
|
+
assert "public-surface" not in tags("python:function:lib._private_fn")
|
|
1319
|
+
assert "public-surface" not in tags("python:class:lib._PrivateClass")
|
|
1320
|
+
|
|
1321
|
+
# Nested defs are not module-level public surface.
|
|
1322
|
+
assert "public-surface" not in tags("python:function:lib.PublicClass.method")
|
|
1323
|
+
|
|
1324
|
+
# Test entities are already roots via the ``test`` tag; they are not the
|
|
1325
|
+
# library's public API and must not be double-tagged public-surface.
|
|
1326
|
+
assert "test" in tags("python:function:lib.test_thing")
|
|
1327
|
+
assert "public-surface" not in tags("python:function:lib.test_thing")
|
|
1328
|
+
assert "test" in tags("python:class:lib.TestThing")
|
|
1329
|
+
assert "public-surface" not in tags("python:class:lib.TestThing")
|
|
1330
|
+
|
|
1331
|
+
|
|
1332
|
+
def test_dunder_all_present_suppresses_public_surface_heuristic() -> None:
|
|
1333
|
+
"""clarion-4ec50f3d92: when ``__all__`` is declared it is authoritative —
|
|
1334
|
+
only listed names are ``exported-api`` and the public-surface heuristic does
|
|
1335
|
+
not fire for unlisted public names."""
|
|
1336
|
+
|
|
1337
|
+
source = """\
|
|
1338
|
+
__all__ = ["exported_fn", "ExportedClass"]
|
|
1339
|
+
|
|
1340
|
+
|
|
1341
|
+
def exported_fn():
|
|
1342
|
+
pass
|
|
1343
|
+
|
|
1344
|
+
|
|
1345
|
+
def public_unlisted_fn():
|
|
1346
|
+
pass
|
|
1347
|
+
|
|
1348
|
+
|
|
1349
|
+
class ExportedClass:
|
|
1350
|
+
pass
|
|
1351
|
+
|
|
1352
|
+
|
|
1353
|
+
class UnlistedClass:
|
|
1354
|
+
pass
|
|
1355
|
+
"""
|
|
1356
|
+
entities, _ = extract(source, "lib.py")
|
|
1357
|
+
by_id = {e["id"]: e for e in entities}
|
|
1358
|
+
|
|
1359
|
+
def tags(entity_id: str) -> list[str]:
|
|
1360
|
+
return by_id[entity_id].get("tags", [])
|
|
1361
|
+
|
|
1362
|
+
assert "exported-api" in tags("python:function:lib.exported_fn")
|
|
1363
|
+
assert "public-surface" not in tags("python:function:lib.exported_fn")
|
|
1364
|
+
assert "exported-api" in tags("python:class:lib.ExportedClass")
|
|
1365
|
+
# exported-api and public-surface are mutually exclusive for a listed name.
|
|
1366
|
+
assert "public-surface" not in tags("python:function:lib.exported_fn")
|
|
1367
|
+
assert "public-surface" not in tags("python:class:lib.ExportedClass")
|
|
1368
|
+
|
|
1369
|
+
# A declared __all__ means unlisted public names are deliberately not API.
|
|
1370
|
+
assert "exported-api" not in tags("python:function:lib.public_unlisted_fn")
|
|
1371
|
+
assert "public-surface" not in tags("python:function:lib.public_unlisted_fn")
|
|
1372
|
+
assert "exported-api" not in tags("python:class:lib.UnlistedClass")
|
|
1373
|
+
assert "public-surface" not in tags("python:class:lib.UnlistedClass")
|
|
1374
|
+
|
|
1375
|
+
|
|
1376
|
+
def test_annotated_dunder_all_is_a_declaration() -> None:
|
|
1377
|
+
"""clarion-4ec50f3d92 (review follow-up): an annotated ``__all__: list[str] =
|
|
1378
|
+
[...]`` is a declaration like a plain assignment — listed names get
|
|
1379
|
+
``exported-api`` and the public-surface heuristic does not fire for unlisted
|
|
1380
|
+
public names. Pins the ``ast.AnnAssign`` branch of ``_module_export_names``."""
|
|
1381
|
+
|
|
1382
|
+
source = """\
|
|
1383
|
+
__all__: list[str] = ["exported_fn"]
|
|
1384
|
+
|
|
1385
|
+
|
|
1386
|
+
def exported_fn():
|
|
1387
|
+
pass
|
|
1388
|
+
|
|
1389
|
+
|
|
1390
|
+
def public_unlisted_fn():
|
|
1391
|
+
pass
|
|
1392
|
+
"""
|
|
1393
|
+
entities, _ = extract(source, "lib.py")
|
|
1394
|
+
by_id = {e["id"]: e for e in entities}
|
|
1395
|
+
|
|
1396
|
+
def tags(entity_id: str) -> list[str]:
|
|
1397
|
+
return by_id[entity_id].get("tags", [])
|
|
1398
|
+
|
|
1399
|
+
assert "exported-api" in tags("python:function:lib.exported_fn")
|
|
1400
|
+
assert "public-surface" not in tags("python:function:lib.exported_fn")
|
|
1401
|
+
# __all__ is declared (just annotated) → heuristic suppressed for unlisted.
|
|
1402
|
+
assert "exported-api" not in tags("python:function:lib.public_unlisted_fn")
|
|
1403
|
+
assert "public-surface" not in tags("python:function:lib.public_unlisted_fn")
|
|
1404
|
+
|
|
1405
|
+
|
|
1406
|
+
def test_annotated_empty_dunder_all_declares_empty_surface() -> None:
|
|
1407
|
+
"""clarion-4ec50f3d92 (review follow-up): annotated ``__all__: list[str] = []``
|
|
1408
|
+
is an explicit empty surface, identical to the plain empty case — no
|
|
1409
|
+
public-surface."""
|
|
1410
|
+
|
|
1411
|
+
source = """\
|
|
1412
|
+
__all__: list[str] = []
|
|
1413
|
+
|
|
1414
|
+
|
|
1415
|
+
def public_fn():
|
|
1416
|
+
pass
|
|
1417
|
+
"""
|
|
1418
|
+
entities, _ = extract(source, "lib.py")
|
|
1419
|
+
by_id = {e["id"]: e for e in entities}
|
|
1420
|
+
fn_tags = by_id["python:function:lib.public_fn"].get("tags", [])
|
|
1421
|
+
assert "public-surface" not in fn_tags
|
|
1422
|
+
assert "exported-api" not in fn_tags
|
|
1423
|
+
|
|
1424
|
+
|
|
1425
|
+
def test_empty_dunder_all_declares_empty_public_surface() -> None:
|
|
1426
|
+
"""clarion-4ec50f3d92: ``__all__ = []`` is an *explicit* empty public
|
|
1427
|
+
surface — distinct from the absence of ``__all__`` — so the public-surface
|
|
1428
|
+
heuristic must not fire."""
|
|
1429
|
+
|
|
1430
|
+
source = """\
|
|
1431
|
+
__all__ = []
|
|
1432
|
+
|
|
1433
|
+
|
|
1434
|
+
def public_fn():
|
|
1435
|
+
pass
|
|
1436
|
+
"""
|
|
1437
|
+
entities, _ = extract(source, "lib.py")
|
|
1438
|
+
by_id = {e["id"]: e for e in entities}
|
|
1439
|
+
fn_tags = by_id["python:function:lib.public_fn"].get("tags", [])
|
|
1440
|
+
|
|
1441
|
+
assert "public-surface" not in fn_tags
|
|
1442
|
+
assert "exported-api" not in fn_tags
|
|
1443
|
+
|
|
1444
|
+
|
|
1445
|
+
def test_dunder_all_membership_is_authoritative_over_test_name() -> None:
|
|
1446
|
+
"""clarion-4ec50f3d92 (review follow-up): a declared ``__all__`` is
|
|
1447
|
+
authoritative. A ``Test``/``test_``-named name explicitly listed in
|
|
1448
|
+
``__all__`` keeps ``exported-api`` — the test-name heuristic gates only the
|
|
1449
|
+
*inferred* ``public-surface`` fallback, never a declared export (mirrors
|
|
1450
|
+
ADR-053's 'the declaration is authoritative' principle)."""
|
|
1451
|
+
|
|
1452
|
+
source = """\
|
|
1453
|
+
__all__ = ["TestableThing", "test_vector_exported"]
|
|
1454
|
+
|
|
1455
|
+
|
|
1456
|
+
class TestableThing:
|
|
1457
|
+
pass
|
|
1458
|
+
|
|
1459
|
+
|
|
1460
|
+
def test_vector_exported():
|
|
1461
|
+
pass
|
|
1462
|
+
|
|
1463
|
+
|
|
1464
|
+
class TestUnlisted:
|
|
1465
|
+
pass
|
|
1466
|
+
"""
|
|
1467
|
+
entities, _ = extract(source, "lib.py")
|
|
1468
|
+
by_id = {e["id"]: e for e in entities}
|
|
1469
|
+
|
|
1470
|
+
def tags(entity_id: str) -> list[str]:
|
|
1471
|
+
return by_id[entity_id].get("tags", [])
|
|
1472
|
+
|
|
1473
|
+
# Declared in __all__ → exported-api, even though test-named.
|
|
1474
|
+
assert "exported-api" in tags("python:class:lib.TestableThing")
|
|
1475
|
+
assert "exported-api" in tags("python:function:lib.test_vector_exported")
|
|
1476
|
+
# Still also classified as tests (both are reachability roots, no conflict).
|
|
1477
|
+
assert "test" in tags("python:class:lib.TestableThing")
|
|
1478
|
+
assert "test" in tags("python:function:lib.test_vector_exported")
|
|
1479
|
+
# A test-named class NOT in __all__ gets neither exported-api nor public-surface.
|
|
1480
|
+
assert "exported-api" not in tags("python:class:lib.TestUnlisted")
|
|
1481
|
+
assert "public-surface" not in tags("python:class:lib.TestUnlisted")
|
|
1482
|
+
|
|
1483
|
+
|
|
1265
1484
|
def _wardline_vocabulary(
|
|
1266
1485
|
*,
|
|
1267
1486
|
confidence_basis: Literal["descriptor", "descriptor_version_skew"] = "descriptor",
|
|
@@ -17,7 +17,7 @@ def _read_toml(path: Path) -> dict[str, Any]:
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def test_package_version_matches_pyproject() -> None:
|
|
20
|
-
assert loomweave_plugin_python.__version__ == "1.
|
|
20
|
+
assert loomweave_plugin_python.__version__ == "1.4.0"
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def test_plugin_version_lockstep_across_pyproject_manifest_and_module() -> None:
|
|
@@ -42,12 +42,12 @@ def test_plugin_version_lockstep_across_pyproject_manifest_and_module() -> None:
|
|
|
42
42
|
def test_manifest_declares_current_v1_ontology_only() -> None:
|
|
43
43
|
manifest = _read_toml(_PLUGIN_ROOT / "plugin.toml")
|
|
44
44
|
|
|
45
|
-
assert manifest["plugin"]["version"] == "1.
|
|
45
|
+
assert manifest["plugin"]["version"] == "1.4.0"
|
|
46
46
|
assert manifest["capabilities"]["runtime"]["wardline_aware"] is True
|
|
47
47
|
assert manifest["integrations"]["wardline"]["expected_descriptor_version"] == (
|
|
48
48
|
EXPECTED_DESCRIPTOR_VERSION
|
|
49
49
|
)
|
|
50
|
-
assert manifest["ontology"]["ontology_version"] == "0.
|
|
50
|
+
assert manifest["ontology"]["ontology_version"] == "0.9.0"
|
|
51
51
|
assert manifest["ontology"]["entity_kinds"] == ["function", "class", "module"]
|
|
52
52
|
assert manifest["ontology"]["edge_kinds"] == [
|
|
53
53
|
"contains",
|
|
@@ -86,8 +86,8 @@ def test_initialize_roundtrip() -> None:
|
|
|
86
86
|
assert response["id"] == 1
|
|
87
87
|
result = response["result"]
|
|
88
88
|
assert result["name"] == "loomweave-plugin-python"
|
|
89
|
-
assert result["version"] == "1.
|
|
90
|
-
assert result["ontology_version"] == "0.
|
|
89
|
+
assert result["version"] == "1.4.0"
|
|
90
|
+
assert result["ontology_version"] == "0.9.0"
|
|
91
91
|
assert set(result["capabilities"]) == {"wardline"}
|
|
92
92
|
assert result["capabilities"]["wardline"]["status"] in {
|
|
93
93
|
"absent",
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""Wardline → Loomweave trust-vocabulary descriptor wire conformance oracle.
|
|
2
|
+
|
|
3
|
+
The CONSUMER side of the cross-repo "Vocabulary descriptor (trust-vocab)" seam.
|
|
4
|
+
Wardline AUTHORS the NG-25 trust-vocabulary descriptor — it OWNS the vocabulary
|
|
5
|
+
via ``wardline.core.registry.REGISTRY`` and serialises it through
|
|
6
|
+
``wardline.core.descriptor.build_vocabulary_descriptor`` / ``descriptor_to_yaml``
|
|
7
|
+
to ``.weft/wardline/vocabulary.yaml`` (``{schema: wardline.vocabulary/v1,
|
|
8
|
+
version, entries:[{canonical_name, group, attrs}]}``). Loomweave's Python plugin
|
|
9
|
+
is the real CONSUMER: ``loomweave_plugin_python.wardline_descriptor
|
|
10
|
+
.load_wardline_descriptor`` byte-reads that file (it NEVER imports wardline),
|
|
11
|
+
version-gates on ``EXPECTED_DESCRIPTOR_VERSION == "wardline-generic-2"``, and
|
|
12
|
+
parses ``entries`` into a ``WardlineVocabulary`` that the extractor threads into
|
|
13
|
+
``wardline:external_boundary`` / ``wardline:trusted`` entity tags.
|
|
14
|
+
|
|
15
|
+
This oracle pins that the bytes Wardline produces are accepted + correctly
|
|
16
|
+
interpreted by Loomweave's REAL consumer code path. It mirrors the layering of
|
|
17
|
+
the taint-fact storage oracle
|
|
18
|
+
(``crates/loomweave-storage/tests/wardline_taint_fact_conformance_oracle.rs``):
|
|
19
|
+
|
|
20
|
+
* Layer 1 — a byte-pin (``test_vendored_golden_matches_byte_pin``): the
|
|
21
|
+
git-blob SHA-1 of the vendored golden, asserted against a const. If the
|
|
22
|
+
vendored fixture drifts by a single byte the pin reds. The SHA mirrors
|
|
23
|
+
wardline's own ``UPSTREAM_BLOB_SHA`` byte-pin idiom
|
|
24
|
+
(``wardline/tests/conformance/test_vocabulary_descriptor_wire_golden.py``)
|
|
25
|
+
so the two repos pin the SAME 40 hex chars. On its OWN this is circular
|
|
26
|
+
(it pins the vendored bytes against themselves); the non-circular breaks
|
|
27
|
+
are the consumer oracle below + the Layer-2 drift recheck.
|
|
28
|
+
|
|
29
|
+
* A NON-CIRCULAR consumer oracle: the vendored golden bytes are written to a
|
|
30
|
+
project descriptor location and fed through Loomweave's REAL
|
|
31
|
+
``load_wardline_descriptor`` (resolve → read → ``yaml.safe_load`` → parse →
|
|
32
|
+
version-gate), asserting Loomweave ACCEPTS them (version accepted →
|
|
33
|
+
``enabled``; entries → ``WardlineVocabulary`` with the expected
|
|
34
|
+
canonical_names / groups; the ``external_boundary`` / ``trusted`` decorator
|
|
35
|
+
attrs derive correctly). It then drives the REAL extractor
|
|
36
|
+
(``extractor.extract``) with that parsed-from-golden vocabulary to prove the
|
|
37
|
+
``wardline:external_boundary`` / ``wardline:trusted`` TAGS derive correctly
|
|
38
|
+
end to end — not just that the lookup table parses. A version-skew copy
|
|
39
|
+
(only the ``version`` string bumped, every other byte the golden's) is fed
|
|
40
|
+
through the SAME real loader to prove the version gate is real, not
|
|
41
|
+
cosmetic: ``enabled`` flips to ``version_skew``. All assertions are driven
|
|
42
|
+
off the consumer's returned state, NOT off the golden restated against
|
|
43
|
+
itself.
|
|
44
|
+
|
|
45
|
+
* Layer 2 — a drift recheck (``test_vendored_golden_matches_wardline_authority``):
|
|
46
|
+
the vendored fixture bytes are compared against the authority golden in the
|
|
47
|
+
Wardline repo
|
|
48
|
+
(``$WARDLINE_REPO/tests/conformance/fixtures/wardline-vocabulary-descriptor.golden.yaml``,
|
|
49
|
+
``WARDLINE_REPO`` defaulting to ``/home/john/wardline``). Skip-clean when the
|
|
50
|
+
sibling repo is absent (CI / detached checkout); fail-closed on any
|
|
51
|
+
divergence.
|
|
52
|
+
|
|
53
|
+
── Scope / honesty caveat ──
|
|
54
|
+
This drives the consumer parse + gate (``load_wardline_descriptor``) and the
|
|
55
|
+
consumer tag-derivation (``extractor.extract`` with the parsed vocabulary) — the
|
|
56
|
+
same two code paths the plugin server runs: ``server.handle_initialize`` calls
|
|
57
|
+
``load_wardline_descriptor`` and stashes ``.vocabulary`` on its state, and
|
|
58
|
+
``server.handle_analyze_file`` threads that vocabulary into ``extract`` as
|
|
59
|
+
``wardline_vocabulary=``. It does NOT spin up the JSON-RPC server loop itself;
|
|
60
|
+
the server's own tests cover that wiring. The non-circular guarantee comes from
|
|
61
|
+
driving the REAL parse/gate/derive on the producer-authored bytes, never the
|
|
62
|
+
golden against itself.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
from __future__ import annotations
|
|
66
|
+
|
|
67
|
+
import hashlib
|
|
68
|
+
import os
|
|
69
|
+
from pathlib import Path
|
|
70
|
+
|
|
71
|
+
import pytest
|
|
72
|
+
|
|
73
|
+
from loomweave_plugin_python.extractor import extract
|
|
74
|
+
from loomweave_plugin_python.wardline_descriptor import (
|
|
75
|
+
EXPECTED_DESCRIPTOR_VERSION,
|
|
76
|
+
WardlineVocabulary,
|
|
77
|
+
load_wardline_descriptor,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# The vendored copy of wardline's authority golden, BYTE-IDENTICAL to
|
|
81
|
+
# wardline/tests/conformance/fixtures/wardline-vocabulary-descriptor.golden.yaml
|
|
82
|
+
# (confirmed via `cmp`). Read as bytes so the byte-pin sees the exact on-disk
|
|
83
|
+
# bytes wardline ships, not a yaml.safe_load round-trip.
|
|
84
|
+
GOLDEN_PATH = Path(__file__).parent / "fixtures" / "wardline-vocabulary-descriptor.golden.yaml"
|
|
85
|
+
|
|
86
|
+
# Layer-1 byte-pin: the git-blob SHA-1 of the vendored golden. This is the SAME
|
|
87
|
+
# 40 hex chars wardline pins as UPSTREAM_BLOB_SHA on the producer side
|
|
88
|
+
# (wardline/tests/conformance/test_vocabulary_descriptor_wire_golden.py) — the
|
|
89
|
+
# two repos pin identical bytes, so a one-sided re-vendor reds both suites.
|
|
90
|
+
# Recomputed below as sha1(b"blob %d\0" % len(data) + data). Any edit to the
|
|
91
|
+
# vendored golden without a matching re-pin reds this test.
|
|
92
|
+
UPSTREAM_BLOB_SHA = "f5ad8d2346ffb6ea75aa469e423c6c7cfd16d40a"
|
|
93
|
+
|
|
94
|
+
# The canonical decorator names the producer authors (group 1) and the attrs the
|
|
95
|
+
# consumer must surface for the trust-tier markers. Asserting on these proves the
|
|
96
|
+
# consumer parses the real authored format — including the trust-tier `attrs`
|
|
97
|
+
# that the existing inline-string unit tests cover but which here come straight
|
|
98
|
+
# off the producer's own bytes.
|
|
99
|
+
EXPECTED_CANONICAL_NAMES = ("external_boundary", "trust_boundary", "trusted")
|
|
100
|
+
EXPECTED_ATTRS = {
|
|
101
|
+
"external_boundary": {},
|
|
102
|
+
"trust_boundary": {"_wardline_to_level": "TaintState"},
|
|
103
|
+
"trusted": {"_wardline_level": "TaintState"},
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _write_project_descriptor(project_root: Path, text: str) -> None:
|
|
108
|
+
"""Place descriptor bytes at the real .weft/wardline/ project location the
|
|
109
|
+
consumer reads (ADR-046)."""
|
|
110
|
+
descriptor = project_root / ".weft" / "wardline" / "vocabulary.yaml"
|
|
111
|
+
descriptor.parent.mkdir(parents=True, exist_ok=True)
|
|
112
|
+
descriptor.write_text(text, encoding="utf-8")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ── Layer 1 — byte-pin ───────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_vendored_golden_matches_byte_pin() -> None:
|
|
119
|
+
"""Layer-1: the vendored wardline-authored descriptor golden byte-pins to its
|
|
120
|
+
git-blob SHA-1. ANY edit to the vendored fixture without a matching re-pin
|
|
121
|
+
reds here. On its OWN this is circular (vendored bytes pinned against
|
|
122
|
+
themselves); the non-circular protection is the consumer oracle + the Layer-2
|
|
123
|
+
drift recheck below.
|
|
124
|
+
|
|
125
|
+
Tamper proof (verified out-of-band): a one-byte-tampered copy of the fixture
|
|
126
|
+
hashes to a DIFFERENT git-blob SHA-1, so this assert reds — the pin is
|
|
127
|
+
load-bearing, not decorative.
|
|
128
|
+
"""
|
|
129
|
+
assert len(UPSTREAM_BLOB_SHA) == 40, (
|
|
130
|
+
f"UPSTREAM_BLOB_SHA must be a 40-char git blob SHA-1: {UPSTREAM_BLOB_SHA!r}"
|
|
131
|
+
)
|
|
132
|
+
assert set(UPSTREAM_BLOB_SHA) <= set("0123456789abcdef"), (
|
|
133
|
+
f"UPSTREAM_BLOB_SHA must be lowercase hex (a git blob SHA-1): {UPSTREAM_BLOB_SHA!r}"
|
|
134
|
+
)
|
|
135
|
+
data = GOLDEN_PATH.read_bytes()
|
|
136
|
+
actual = hashlib.sha1(b"blob %d\x00" % len(data) + data).hexdigest() # noqa: S324 - git blob id, not a security hash
|
|
137
|
+
assert actual == UPSTREAM_BLOB_SHA, (
|
|
138
|
+
f"the vendored vocabulary-descriptor golden changed (git blob {actual}, "
|
|
139
|
+
f"pinned {UPSTREAM_BLOB_SHA}) — if this was a deliberate re-vendor, re-copy "
|
|
140
|
+
"BYTE-IDENTICAL from wardline "
|
|
141
|
+
"(tests/conformance/fixtures/wardline-vocabulary-descriptor.golden.yaml), "
|
|
142
|
+
"confirm with `cmp`, and update UPSTREAM_BLOB_SHA in the SAME commit; if not, "
|
|
143
|
+
"revert the edit."
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# ── NON-CIRCULAR consumer oracle ─────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_consumer_accepts_golden_and_parses_vocabulary(tmp_path: Path) -> None:
|
|
151
|
+
"""The vendored golden bytes (schema line included) fed through the REAL
|
|
152
|
+
``load_wardline_descriptor`` are ACCEPTED: version-gated to ``enabled`` and
|
|
153
|
+
parsed into a ``WardlineVocabulary`` whose canonical_names / groups / attrs
|
|
154
|
+
match the producer-authored format. Driven off the consumer's returned state,
|
|
155
|
+
never the golden against itself."""
|
|
156
|
+
golden_text = GOLDEN_PATH.read_text("utf-8")
|
|
157
|
+
_write_project_descriptor(tmp_path, golden_text)
|
|
158
|
+
|
|
159
|
+
state = load_wardline_descriptor(tmp_path)
|
|
160
|
+
|
|
161
|
+
# Version accepted — the gate passed, so the descriptor is enabled.
|
|
162
|
+
assert state.status == "enabled", (
|
|
163
|
+
f"consumer rejected the producer-authored golden: status={state.status!r} "
|
|
164
|
+
f"reason={state.reason!r}"
|
|
165
|
+
)
|
|
166
|
+
assert state.source == "project"
|
|
167
|
+
assert state.descriptor_version == EXPECTED_DESCRIPTOR_VERSION
|
|
168
|
+
|
|
169
|
+
vocab = state.vocabulary
|
|
170
|
+
assert isinstance(vocab, WardlineVocabulary)
|
|
171
|
+
assert vocab.version == EXPECTED_DESCRIPTOR_VERSION
|
|
172
|
+
assert vocab.confidence_basis == "descriptor"
|
|
173
|
+
|
|
174
|
+
# entries → WardlineVocabulary with the expected canonical_names.
|
|
175
|
+
assert tuple(sorted(vocab.entries_by_name)) == tuple(sorted(EXPECTED_CANONICAL_NAMES))
|
|
176
|
+
|
|
177
|
+
# Each entry's group + attrs survive parse exactly. The trust-tier attrs
|
|
178
|
+
# (_wardline_to_level / _wardline_level) are the real cross-tool delta the
|
|
179
|
+
# inline-string unit tests duplicate but which here come off producer bytes.
|
|
180
|
+
for name in EXPECTED_CANONICAL_NAMES:
|
|
181
|
+
entry = vocab.entries_by_name[name]
|
|
182
|
+
assert entry.canonical_name == name
|
|
183
|
+
assert entry.group == 1, f"{name} must be a group-1 marker, got {entry.group}"
|
|
184
|
+
assert entry.attrs == EXPECTED_ATTRS[name], (
|
|
185
|
+
f"{name} attrs drifted: {entry.attrs!r} != {EXPECTED_ATTRS[name]!r}"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# entry_for_decorator (the real lookup the extractor calls) resolves the
|
|
189
|
+
# last dotted segment to the right entry — the external_boundary / trusted
|
|
190
|
+
# markers derive from the parsed table, not a restated f-string.
|
|
191
|
+
eb = vocab.entry_for_decorator("weft_markers.external_boundary")
|
|
192
|
+
assert eb is not None
|
|
193
|
+
assert eb.canonical_name == "external_boundary"
|
|
194
|
+
tr = vocab.entry_for_decorator("trusted")
|
|
195
|
+
assert tr is not None
|
|
196
|
+
assert tr.canonical_name == "trusted"
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def test_consumer_derives_external_boundary_and_trusted_tags_through_extractor(
|
|
200
|
+
tmp_path: Path,
|
|
201
|
+
) -> None:
|
|
202
|
+
"""End-to-end consumer derivation: parse the golden through the REAL
|
|
203
|
+
``load_wardline_descriptor``, then thread the parsed vocabulary into the REAL
|
|
204
|
+
``extractor.extract``. This proves the ``wardline:external_boundary`` /
|
|
205
|
+
``wardline:trusted`` TAGS derive correctly through the full consumer path —
|
|
206
|
+
not merely that the lookup table parsed."""
|
|
207
|
+
golden_text = GOLDEN_PATH.read_text("utf-8")
|
|
208
|
+
_write_project_descriptor(tmp_path, golden_text)
|
|
209
|
+
|
|
210
|
+
state = load_wardline_descriptor(tmp_path)
|
|
211
|
+
assert state.status == "enabled"
|
|
212
|
+
vocabulary = state.vocabulary
|
|
213
|
+
assert vocabulary is not None
|
|
214
|
+
|
|
215
|
+
source = """\
|
|
216
|
+
from weft_markers import external_boundary, trust_boundary, trusted
|
|
217
|
+
|
|
218
|
+
@external_boundary
|
|
219
|
+
def read_body():
|
|
220
|
+
return ""
|
|
221
|
+
|
|
222
|
+
@weft_markers.trust_boundary(to_level="ASSURED")
|
|
223
|
+
@trusted(level="INTEGRAL")
|
|
224
|
+
class Sanitizer:
|
|
225
|
+
pass
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
entities, _edges = extract(source, "service.py", wardline_vocabulary=vocabulary)
|
|
229
|
+
|
|
230
|
+
read_body = next(e for e in entities if e["id"] == "python:function:service.read_body")
|
|
231
|
+
sanitizer = next(e for e in entities if e["id"] == "python:class:service.Sanitizer")
|
|
232
|
+
|
|
233
|
+
# external_boundary tag derives from the golden-parsed vocabulary.
|
|
234
|
+
assert "wardline" in read_body["tags"]
|
|
235
|
+
assert "wardline:external_boundary" in read_body["tags"]
|
|
236
|
+
assert read_body["wardline"]["descriptor_version"] == EXPECTED_DESCRIPTOR_VERSION
|
|
237
|
+
assert read_body["wardline"]["confidence_basis"] == "descriptor"
|
|
238
|
+
eb_decorators = read_body["wardline"]["decorators"]
|
|
239
|
+
assert [d["canonical_name"] for d in eb_decorators] == ["external_boundary"]
|
|
240
|
+
assert eb_decorators[0]["attrs"] == {}
|
|
241
|
+
|
|
242
|
+
# trust_boundary + trusted tags derive, carrying the trust-tier attrs.
|
|
243
|
+
assert "wardline:trust_boundary" in sanitizer["tags"]
|
|
244
|
+
assert "wardline:trusted" in sanitizer["tags"]
|
|
245
|
+
san_attrs = {d["canonical_name"]: d["attrs"] for d in sanitizer["wardline"]["decorators"]}
|
|
246
|
+
assert san_attrs["trust_boundary"] == {"_wardline_to_level": "TaintState"}
|
|
247
|
+
assert san_attrs["trusted"] == {"_wardline_level": "TaintState"}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def test_consumer_version_gate_rejects_skew_copy(tmp_path: Path) -> None:
|
|
251
|
+
"""The version gate is REAL, not cosmetic: the SAME golden bytes with ONLY
|
|
252
|
+
the ``version`` string bumped flip the consumer from ``enabled`` to
|
|
253
|
+
``version_skew`` through the REAL ``load_wardline_descriptor``. The contrast
|
|
254
|
+
with ``test_consumer_accepts_golden_and_parses_vocabulary`` (identical bytes
|
|
255
|
+
bar the version) is the proof that the gate fires on version alone."""
|
|
256
|
+
golden_text = GOLDEN_PATH.read_text("utf-8")
|
|
257
|
+
assert EXPECTED_DESCRIPTOR_VERSION in golden_text, (
|
|
258
|
+
"the golden must carry the expected version for the skew derivation to be a "
|
|
259
|
+
"single-field perturbation"
|
|
260
|
+
)
|
|
261
|
+
skewed = golden_text.replace(EXPECTED_DESCRIPTOR_VERSION, "wardline-generic-3")
|
|
262
|
+
assert skewed != golden_text, "version-skew copy must differ from the golden"
|
|
263
|
+
_write_project_descriptor(tmp_path, skewed)
|
|
264
|
+
|
|
265
|
+
state = load_wardline_descriptor(tmp_path)
|
|
266
|
+
|
|
267
|
+
# The gate fired: a one-field version bump is REJECTED as skew.
|
|
268
|
+
assert state.status == "version_skew", (
|
|
269
|
+
f"version gate failed to fire on a skewed descriptor: status={state.status!r}"
|
|
270
|
+
)
|
|
271
|
+
assert state.descriptor_version == "wardline-generic-3"
|
|
272
|
+
assert state.expected_version == EXPECTED_DESCRIPTOR_VERSION
|
|
273
|
+
# The vocabulary still parses (entries are valid) but is flagged degraded —
|
|
274
|
+
# proving the gate keys on version, not on a parse failure.
|
|
275
|
+
assert state.vocabulary is not None
|
|
276
|
+
assert state.vocabulary.confidence_basis == "descriptor_version_skew"
|
|
277
|
+
assert tuple(sorted(state.vocabulary.entries_by_name)) == tuple(
|
|
278
|
+
sorted(EXPECTED_CANONICAL_NAMES)
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
# ── Layer 2 — drift recheck vs the Wardline source of truth ──────────────────
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def test_vendored_golden_matches_wardline_authority() -> None:
|
|
286
|
+
"""Layer-2: the vendored fixture bytes must equal the authority golden in the
|
|
287
|
+
Wardline repo. Skip-clean when the sibling repo is absent (CI / detached
|
|
288
|
+
checkout) — the vendored copy + Layer-1 pin still hold; fail-closed on any
|
|
289
|
+
divergence."""
|
|
290
|
+
repo = os.environ.get("WARDLINE_REPO", "/home/john/wardline")
|
|
291
|
+
authority = (
|
|
292
|
+
Path(repo)
|
|
293
|
+
/ "tests"
|
|
294
|
+
/ "conformance"
|
|
295
|
+
/ "fixtures"
|
|
296
|
+
/ "wardline-vocabulary-descriptor.golden.yaml"
|
|
297
|
+
)
|
|
298
|
+
if not authority.exists():
|
|
299
|
+
pytest.skip(
|
|
300
|
+
f"wardline authority golden not found at {authority} — skipping Layer-2 drift "
|
|
301
|
+
"recheck (set WARDLINE_REPO to enable)"
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
authority_bytes = authority.read_bytes()
|
|
305
|
+
vendored_bytes = GOLDEN_PATH.read_bytes()
|
|
306
|
+
assert authority_bytes == vendored_bytes, (
|
|
307
|
+
f"the vendored golden has DRIFTED from the Wardline authority at {authority}; "
|
|
308
|
+
"re-vendor BYTE-IDENTICAL (cmp must show no difference) and re-pin UPSTREAM_BLOB_SHA"
|
|
309
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.4.0}/src/loomweave_plugin_python/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.4.0}/tests/test_dogfood_uniqueness.py
RENAMED
|
File without changes
|
|
File without changes
|
{loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.4.0}/tests/test_pyright_session.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{loomweave_plugin_python-1.3.1 → loomweave_plugin_python-1.4.0}/tests/test_wardline_descriptor.py
RENAMED
|
File without changes
|