lar1semantic 0.3.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.
@@ -0,0 +1,63 @@
1
+ Metadata-Version: 2.4
2
+ Name: lar1semantic
3
+ Version: 0.3.0
4
+ Summary: Reference Python implementation of LAR-1 semantic overlay
5
+ Project-URL: Homepage, https://github.com/carlsonchik/larone
6
+ Project-URL: Repository, https://github.com/carlsonchik/larone
7
+ Project-URL: Documentation, https://github.com/carlsonchik/larone/blob/main/SPEC.md
8
+ Author: LAR-1 contributors
9
+ License-Expression: MIT
10
+ Keywords: a2a,agent,lar-1,mcp,semantic-overlay
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Software Development :: Libraries
16
+ Requires-Python: >=3.10
17
+ Provides-Extra: dev
18
+ Requires-Dist: langchain-core>=0.3.0; extra == 'dev'
19
+ Requires-Dist: pytest>=8.0; extra == 'dev'
20
+ Description-Content-Type: text/markdown
21
+
22
+ # lar-1 (Python)
23
+
24
+ Reference Python implementation of **LAR-1** v0.2 semantic overlay.
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ pip install lar-1
30
+ # or from monorepo:
31
+ cd packages/lar1-python && pip install -e ".[dev]"
32
+ ```
33
+
34
+ ## API
35
+
36
+ ```python
37
+ from lar1 import parse, validate, compact, serialize, deserialize_fields
38
+
39
+ data = parse("LAR:T=now,C=obs,L=0.9,V=verified_tool")
40
+ validate(data) # True
41
+ compact(data) # canonical LAR: string
42
+ serialize(data) # application/lar+json
43
+ ```
44
+
45
+ ## CLI
46
+
47
+ ```bash
48
+ lar1 validate 'LAR:C=obs,L=0.9'
49
+ lar1 compact message.json
50
+ lar1 json 'LAR:T=now,C=inf,L=0.7'
51
+ ```
52
+
53
+ ## Tests
54
+
55
+ ```bash
56
+ pytest
57
+ ```
58
+
59
+ Runs the same 74+ conformance fixtures as `@lar-1/core`.
60
+
61
+ ## License
62
+
63
+ MIT
@@ -0,0 +1,42 @@
1
+ # lar-1 (Python)
2
+
3
+ Reference Python implementation of **LAR-1** v0.2 semantic overlay.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install lar-1
9
+ # or from monorepo:
10
+ cd packages/lar1-python && pip install -e ".[dev]"
11
+ ```
12
+
13
+ ## API
14
+
15
+ ```python
16
+ from lar1 import parse, validate, compact, serialize, deserialize_fields
17
+
18
+ data = parse("LAR:T=now,C=obs,L=0.9,V=verified_tool")
19
+ validate(data) # True
20
+ compact(data) # canonical LAR: string
21
+ serialize(data) # application/lar+json
22
+ ```
23
+
24
+ ## CLI
25
+
26
+ ```bash
27
+ lar1 validate 'LAR:C=obs,L=0.9'
28
+ lar1 compact message.json
29
+ lar1 json 'LAR:T=now,C=inf,L=0.7'
30
+ ```
31
+
32
+ ## Tests
33
+
34
+ ```bash
35
+ pytest
36
+ ```
37
+
38
+ Runs the same 74+ conformance fixtures as `@lar-1/core`.
39
+
40
+ ## License
41
+
42
+ MIT
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "lar1semantic"
7
+ version = "0.3.0"
8
+ description = "Reference Python implementation of LAR-1 semantic overlay"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "LAR-1 contributors" }]
13
+ keywords = ["lar-1", "agent", "mcp", "a2a", "semantic-overlay"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Topic :: Software Development :: Libraries",
20
+ ]
21
+ dependencies = []
22
+
23
+ [project.optional-dependencies]
24
+ dev = ["pytest>=8.0", "langchain-core>=0.3.0"]
25
+
26
+ [project.scripts]
27
+ lar1 = "lar1.cli:main"
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/carlsonchik/larone"
31
+ Repository = "https://github.com/carlsonchik/larone"
32
+ Documentation = "https://github.com/carlsonchik/larone/blob/main/SPEC.md"
33
+
34
+ [tool.hatch.build.targets.wheel]
35
+ packages = ["src/lar1"]
36
+
37
+ [tool.pytest.ini_options]
38
+ testpaths = ["tests"]
39
+ pythonpath = ["src", "."]
@@ -0,0 +1,21 @@
1
+ """LAR-1 semantic overlay — reference Python SDK."""
2
+
3
+ from lar1.errors import Lar1ParseError
4
+ from lar1.parse import parse, parse_envelope
5
+ from lar1.serialize import compact, deserialize, deserialize_fields, serialize
6
+
7
+ __all__ = [
8
+ "Lar1ParseError",
9
+ "parse",
10
+ "parse_envelope",
11
+ "validate",
12
+ "validate_envelope",
13
+ "serialize",
14
+ "deserialize",
15
+ "deserialize_fields",
16
+ "compact",
17
+ ]
18
+
19
+ from lar1.validate import validate, validate_envelope
20
+
21
+ __version__ = "0.3.0"
@@ -0,0 +1,85 @@
1
+ """LAR-1 command-line interface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+
9
+ from lar1 import compact, deserialize_fields, parse, serialize, validate
10
+ from lar1.errors import Lar1ParseError
11
+
12
+
13
+ def _read_input(path: str | None) -> str:
14
+ if path and path != "-":
15
+ with open(path, encoding="utf-8") as f:
16
+ return f.read()
17
+ return sys.stdin.read()
18
+
19
+
20
+ def cmd_validate(args: argparse.Namespace) -> int:
21
+ raw = _read_input(args.file).strip()
22
+ try:
23
+ if raw.startswith("{"):
24
+ data = deserialize_fields(raw)
25
+ else:
26
+ data = parse(raw)
27
+ if validate(data):
28
+ print("OK")
29
+ return 0
30
+ print("INVALID", file=sys.stderr)
31
+ return 1
32
+ except (Lar1ParseError, ValueError) as exc:
33
+ code = getattr(exc, "code", "INVALID")
34
+ print(code, file=sys.stderr)
35
+ return 1
36
+
37
+
38
+ def cmd_compact(args: argparse.Namespace) -> int:
39
+ raw = _read_input(args.file).strip()
40
+ try:
41
+ if raw.startswith("{"):
42
+ data = deserialize_fields(raw)
43
+ else:
44
+ data = parse(raw)
45
+ print(compact(data))
46
+ return 0
47
+ except (Lar1ParseError, ValueError) as exc:
48
+ code = getattr(exc, "code", str(exc))
49
+ print(code, file=sys.stderr)
50
+ return 1
51
+
52
+
53
+ def cmd_json(args: argparse.Namespace) -> int:
54
+ raw = _read_input(args.file).strip()
55
+ try:
56
+ data = parse(raw)
57
+ print(serialize(data))
58
+ return 0
59
+ except Lar1ParseError as exc:
60
+ print(exc.code, file=sys.stderr)
61
+ return 1
62
+
63
+
64
+ def main() -> None:
65
+ parser = argparse.ArgumentParser(prog="lar1", description="LAR-1 semantic overlay CLI")
66
+ sub = parser.add_subparsers(dest="command", required=True)
67
+
68
+ p_validate = sub.add_parser("validate", help="Validate compact or JSON input")
69
+ p_validate.add_argument("file", nargs="?", default="-", help="File path or - for stdin")
70
+ p_validate.set_defaults(func=cmd_validate)
71
+
72
+ p_compact = sub.add_parser("compact", help="Output canonical compact string")
73
+ p_compact.add_argument("file", nargs="?", default="-")
74
+ p_compact.set_defaults(func=cmd_compact)
75
+
76
+ p_json = sub.add_parser("json", help="Convert compact to application/lar+json")
77
+ p_json.add_argument("file", nargs="?", default="-")
78
+ p_json.set_defaults(func=cmd_json)
79
+
80
+ args = parser.parse_args()
81
+ raise SystemExit(args.func(args))
82
+
83
+
84
+ if __name__ == "__main__":
85
+ main()
@@ -0,0 +1,55 @@
1
+ """LAR-1 v0.2 field enums."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Final, Literal, TypedDict
6
+
7
+ TemporalFrame = Literal["now", "past", "recall", "future"]
8
+ SpatialFrame = Literal["here", "there", "meta"]
9
+ CognitionStance = Literal["obs", "hyp", "mem", "det", "inf", "rev"]
10
+ EvidenceGrounding = Literal["direct", "derived", "aggregated", "reported"]
11
+ VerificationStatus = Literal[
12
+ "unverified", "verified_human", "verified_tool", "verified_crossref"
13
+ ]
14
+
15
+ Lar1ErrorCode = Literal[
16
+ "EMPTY_INPUT",
17
+ "MISSING_PREFIX",
18
+ "NO_PAIRS",
19
+ "UNKNOWN_KEY",
20
+ "INVALID_ENUM",
21
+ "INVALID_LIKELIHOOD",
22
+ "DUPLICATE_KEY",
23
+ "MALFORMED_PAIR",
24
+ "EMPTY_OBJECT",
25
+ ]
26
+
27
+
28
+ class Lar1Fields(TypedDict, total=False):
29
+ T: TemporalFrame
30
+ S: SpatialFrame
31
+ C: CognitionStance
32
+ E: EvidenceGrounding
33
+ L: float
34
+ V: VerificationStatus
35
+
36
+
37
+ Lar1Envelope = TypedDict("Lar1Envelope", {"LAR-1": Lar1Fields})
38
+
39
+
40
+ ENUMS: Final[dict[str, tuple[str, ...]]] = {
41
+ "T": ("now", "past", "recall", "future"),
42
+ "S": ("here", "there", "meta"),
43
+ "C": ("obs", "hyp", "mem", "det", "inf", "rev"),
44
+ "E": ("direct", "derived", "aggregated", "reported"),
45
+ "V": (
46
+ "unverified",
47
+ "verified_human",
48
+ "verified_tool",
49
+ "verified_crossref",
50
+ ),
51
+ }
52
+
53
+ VALID_KEYS: Final[frozenset[str]] = frozenset({"T", "S", "C", "E", "L", "V"})
54
+ FIELD_ORDER: Final[tuple[str, ...]] = ("T", "S", "C", "E", "L", "V")
55
+ PREFIX: Final[str] = "LAR:"
@@ -0,0 +1,7 @@
1
+ """LAR-1 parse errors."""
2
+
3
+
4
+ class Lar1ParseError(Exception):
5
+ def __init__(self, code: str, message: str | None = None) -> None:
6
+ self.code = code
7
+ super().__init__(message or code)
@@ -0,0 +1,60 @@
1
+ """LangGraph / LangChain message integration for LAR-1."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from lar1 import validate
8
+
9
+ LAR1_KEY = "lar-1"
10
+
11
+ # Default cognitive tags by node role name (substring match)
12
+ NODE_COGNITION: dict[str, str] = {
13
+ "research": "obs",
14
+ "retrieve": "obs",
15
+ "critic": "rev",
16
+ "review": "rev",
17
+ "synth": "inf",
18
+ "merge": "inf",
19
+ "plan": "hyp",
20
+ "memory": "mem",
21
+ }
22
+
23
+
24
+ def attach_lar1_message(message: Any, fields: dict[str, Any]) -> Any:
25
+ """Attach LAR-1 to LangChain message additional_kwargs."""
26
+ if not validate(fields):
27
+ raise ValueError("Invalid LAR-1 fields")
28
+ kwargs = dict(getattr(message, "additional_kwargs", {}) or {})
29
+ kwargs[LAR1_KEY] = fields
30
+ return message.model_copy(update={"additional_kwargs": kwargs})
31
+
32
+
33
+ def read_lar1_message(message: Any) -> dict[str, Any] | None:
34
+ block = getattr(message, "additional_kwargs", {}).get(LAR1_KEY)
35
+ if isinstance(block, dict) and validate(block):
36
+ return block
37
+ return None
38
+
39
+
40
+ def lar1_for_node(node_name: str, *, likelihood: float = 0.75) -> dict[str, Any]:
41
+ """Suggest LAR-1 fields from LangGraph node name."""
42
+ name = node_name.lower()
43
+ cognition = "inf"
44
+ for key, c in NODE_COGNITION.items():
45
+ if key in name:
46
+ cognition = c
47
+ break
48
+ return {
49
+ "T": "now",
50
+ "S": "here",
51
+ "C": cognition,
52
+ "E": "derived" if cognition == "inf" else "direct",
53
+ "L": likelihood,
54
+ "V": "unverified",
55
+ }
56
+
57
+
58
+ def middleware_tag_node(node_name: str, message: Any) -> Any:
59
+ """Auto-tag agent message from node name (LangGraph middleware helper)."""
60
+ return attach_lar1_message(message, lar1_for_node(node_name))
@@ -0,0 +1,67 @@
1
+ """Parse LAR-1 compact strings."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from lar1.enums import ENUMS, PREFIX, VALID_KEYS, Lar1Fields
6
+ from lar1.errors import Lar1ParseError
7
+
8
+
9
+ def _assert_enum(key: str, value: str) -> str:
10
+ allowed = ENUMS[key]
11
+ if value not in allowed:
12
+ raise Lar1ParseError("INVALID_ENUM", f"Invalid {key}={value}")
13
+ return value
14
+
15
+
16
+ def _parse_likelihood(raw: str) -> float:
17
+ try:
18
+ n = float(raw)
19
+ except ValueError:
20
+ raise Lar1ParseError("INVALID_LIKELIHOOD", f"Invalid L={raw}") from None
21
+ if n < 0 or n > 1:
22
+ raise Lar1ParseError("INVALID_LIKELIHOOD", f"L out of range: {n}")
23
+ return n
24
+
25
+
26
+ def parse(compact: str) -> Lar1Fields:
27
+ """Parse compact string into inner LAR-1 fields."""
28
+ trimmed = compact.strip()
29
+ if trimmed == "":
30
+ raise Lar1ParseError("EMPTY_INPUT")
31
+ if not trimmed.startswith(PREFIX):
32
+ raise Lar1ParseError("MISSING_PREFIX")
33
+
34
+ body = trimmed[len(PREFIX) :]
35
+ if body == "":
36
+ raise Lar1ParseError("NO_PAIRS")
37
+
38
+ pairs = body.split(",")
39
+ if not pairs or (len(pairs) == 1 and pairs[0] == ""):
40
+ raise Lar1ParseError("NO_PAIRS")
41
+
42
+ seen: set[str] = set()
43
+ data: Lar1Fields = {}
44
+
45
+ for pair in pairs:
46
+ if "=" not in pair:
47
+ raise Lar1ParseError("MALFORMED_PAIR", f"Missing '=': {pair}")
48
+ key, _, value = pair.partition("=")
49
+ key = key.strip()
50
+ value = value.strip()
51
+
52
+ if key not in VALID_KEYS:
53
+ raise Lar1ParseError("UNKNOWN_KEY", f"Unknown key: {key}")
54
+ if key in seen:
55
+ raise Lar1ParseError("DUPLICATE_KEY", f"Duplicate key: {key}")
56
+ seen.add(key)
57
+
58
+ if key == "L":
59
+ data["L"] = _parse_likelihood(value)
60
+ elif key in ENUMS:
61
+ data[key] = _assert_enum(key, value) # type: ignore[literal-required]
62
+
63
+ return data
64
+
65
+
66
+ def parse_envelope(compact: str) -> dict[str, Lar1Fields]:
67
+ return {"LAR-1": parse(compact)}
@@ -0,0 +1,42 @@
1
+ """Serialize and deserialize LAR-1 data."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+ from lar1.enums import FIELD_ORDER, PREFIX, Lar1Fields
8
+ from lar1.validate import validate, validate_envelope
9
+
10
+
11
+ def serialize(data: Lar1Fields) -> str:
12
+ return json.dumps({"LAR-1": data}, separators=(",", ":"))
13
+
14
+
15
+ def deserialize(json_str: str) -> dict[str, Lar1Fields]:
16
+ try:
17
+ parsed = json.loads(json_str)
18
+ except json.JSONDecodeError as exc:
19
+ raise ValueError("Invalid JSON") from exc
20
+ if not validate_envelope(parsed):
21
+ raise ValueError("Invalid LAR-1 envelope")
22
+ return parsed
23
+
24
+
25
+ def deserialize_fields(json_str: str) -> Lar1Fields:
26
+ return deserialize(json_str)["LAR-1"]
27
+
28
+
29
+ def _format_value(key: str, value: object) -> str:
30
+ if key == "L" and isinstance(value, float) and value == int(value):
31
+ return str(int(value))
32
+ return str(value)
33
+
34
+
35
+ def compact(data: Lar1Fields) -> str:
36
+ parts: list[str] = []
37
+ for key in FIELD_ORDER:
38
+ if key in data:
39
+ parts.append(f"{key}={_format_value(key, data[key])}")
40
+ if not parts:
41
+ raise ValueError("Cannot compact empty LAR-1 object")
42
+ return PREFIX + ",".join(parts)
@@ -0,0 +1,34 @@
1
+ """Validate LAR-1 fields."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from lar1.enums import ENUMS, Lar1Fields
8
+
9
+
10
+ def _is_likelihood(value: Any) -> bool:
11
+ return isinstance(value, (int, float)) and not isinstance(value, bool) and 0 <= value <= 1
12
+
13
+
14
+ def validate(data: Lar1Fields | Any) -> bool:
15
+ if not isinstance(data, dict) or not data:
16
+ return False
17
+
18
+ for key, value in data.items():
19
+ if key in ENUMS:
20
+ if value not in ENUMS[key]:
21
+ return False
22
+ elif key == "L":
23
+ if not _is_likelihood(value):
24
+ return False
25
+ else:
26
+ return False
27
+
28
+ return True
29
+
30
+
31
+ def validate_envelope(envelope: Any) -> bool:
32
+ if not isinstance(envelope, dict) or "LAR-1" not in envelope:
33
+ return False
34
+ return validate(envelope["LAR-1"])
File without changes
@@ -0,0 +1,17 @@
1
+ """Conformance fixture loader."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ REPO_ROOT = Path(__file__).resolve().parents[3]
9
+ CONFORMANCE_ROOT = REPO_ROOT / "SPEC" / "conformance"
10
+
11
+
12
+ def load_fixtures(subdir: str) -> list[dict]:
13
+ directory = CONFORMANCE_ROOT / subdir
14
+ return [
15
+ json.loads(path.read_text(encoding="utf-8"))
16
+ for path in sorted(directory.glob("*.json"))
17
+ ]
@@ -0,0 +1,54 @@
1
+ """Conformance tests against SPEC/conformance fixtures."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+
7
+ from lar1 import compact, parse, validate
8
+ from lar1.errors import Lar1ParseError
9
+ from tests.helpers import load_fixtures
10
+
11
+
12
+ @pytest.mark.parametrize(
13
+ "fixture",
14
+ load_fixtures("valid") + load_fixtures("enum"),
15
+ ids=lambda f: f["id"],
16
+ )
17
+ def test_valid_fixtures(fixture: dict) -> None:
18
+ parsed = parse(fixture["input"])
19
+ assert {"LAR-1": parsed} == fixture["expected"]
20
+ assert validate(parsed)
21
+ assert compact(parsed) == fixture["input"]
22
+
23
+
24
+ @pytest.mark.parametrize(
25
+ "fixture",
26
+ load_fixtures("invalid"),
27
+ ids=lambda f: f["id"],
28
+ )
29
+ def test_invalid_fixtures(fixture: dict) -> None:
30
+ with pytest.raises(Lar1ParseError) as exc:
31
+ parse(fixture["input"])
32
+ assert exc.value.code == fixture["error"]
33
+
34
+
35
+ @pytest.mark.parametrize(
36
+ "fixture",
37
+ [f for f in load_fixtures("boundary") if f.get("expected")],
38
+ ids=lambda f: f["id"],
39
+ )
40
+ def test_boundary_valid(fixture: dict) -> None:
41
+ parsed = parse(fixture["input"])
42
+ assert {"LAR-1": parsed} == fixture["expected"]
43
+ assert validate(parsed)
44
+
45
+
46
+ @pytest.mark.parametrize(
47
+ "fixture",
48
+ [f for f in load_fixtures("boundary") if f.get("error")],
49
+ ids=lambda f: f["id"],
50
+ )
51
+ def test_boundary_invalid(fixture: dict) -> None:
52
+ with pytest.raises(Lar1ParseError) as exc:
53
+ parse(fixture["input"])
54
+ assert exc.value.code == fixture["error"]
@@ -0,0 +1,30 @@
1
+ """Tests for LangGraph integration helpers."""
2
+
3
+ from langchain_core.messages import AIMessage
4
+
5
+ from lar1.langgraph import (
6
+ attach_lar1_message,
7
+ lar1_for_node,
8
+ middleware_tag_node,
9
+ read_lar1_message,
10
+ )
11
+
12
+
13
+ def test_attach_and_read():
14
+ msg = AIMessage(content="hi")
15
+ tagged = attach_lar1_message(msg, {"C": "obs", "L": 0.9, "V": "verified_tool"})
16
+ block = read_lar1_message(tagged)
17
+ assert block is not None
18
+ assert block["C"] == "obs"
19
+
20
+
21
+ def test_middleware_tag_node():
22
+ msg = AIMessage(content="research output")
23
+ tagged = middleware_tag_node("researcher", msg)
24
+ block = read_lar1_message(tagged)
25
+ assert block["C"] == "obs"
26
+
27
+
28
+ def test_lar1_for_node_critic():
29
+ fields = lar1_for_node("critic")
30
+ assert fields["C"] == "rev"
@@ -0,0 +1,56 @@
1
+ """Round-trip tests for compact and JSON wire formats."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+
7
+ from lar1 import compact, deserialize_fields, parse, serialize
8
+ from lar1.enums import ENUMS
9
+ from tests.helpers import load_fixtures
10
+
11
+
12
+ @pytest.mark.parametrize("key,values", list(ENUMS.items()))
13
+ def test_compact_roundtrip_enums(key: str, values: tuple[str, ...]) -> None:
14
+ for value in values:
15
+ first = parse(f"LAR:{key}={value}")
16
+ second = parse(compact(first))
17
+ assert second == first
18
+
19
+
20
+ @pytest.mark.parametrize("l_val", [0, 0.5, 1])
21
+ def test_compact_roundtrip_likelihood(l_val: float) -> None:
22
+ first = parse(f"LAR:L={l_val}")
23
+ second = parse(compact(first))
24
+ assert second == first
25
+
26
+
27
+ @pytest.mark.parametrize("key,values", list(ENUMS.items()))
28
+ def test_json_roundtrip_enums(key: str, values: tuple[str, ...]) -> None:
29
+ for value in values:
30
+ first = parse(f"LAR:{key}={value}")
31
+ second = deserialize_fields(serialize(first))
32
+ assert second == first
33
+
34
+
35
+ def test_full_roundtrip_samples() -> None:
36
+ samples = [
37
+ "LAR:T=now,S=here,C=obs,E=direct,L=0.95,V=verified_tool",
38
+ "LAR:C=inf,L=0.72,V=unverified",
39
+ ]
40
+ for input_str in samples:
41
+ a = parse(input_str)
42
+ b = deserialize_fields(serialize(a))
43
+ assert b == a
44
+ c = parse(compact(a))
45
+ assert c == a
46
+
47
+
48
+ @pytest.mark.parametrize(
49
+ "fixture",
50
+ load_fixtures("valid") + load_fixtures("enum"),
51
+ ids=lambda f: f["id"],
52
+ )
53
+ def test_fixture_roundtrip(fixture: dict) -> None:
54
+ first = parse(fixture["input"])
55
+ assert parse(compact(first)) == first
56
+ assert deserialize_fields(serialize(first)) == first