plainweave 1.0.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.
plainweave/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Plainweave - local-first requirements and verification authority for Weft."""
2
+
3
+ from plainweave._version import __version__
4
+
5
+ __all__ = ["__version__"]
plainweave/__main__.py ADDED
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+
5
+ from plainweave.cli import main
6
+
7
+ if __name__ == "__main__":
8
+ sys.exit(main())
plainweave/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"
plainweave/bindings.py ADDED
@@ -0,0 +1,98 @@
1
+ """Plainweave ↔ Loomweave SEI bindings via the ADR-029 entity-association contract.
2
+
3
+ Code leaves are keyed by **Loomweave SEI** (``loomweave:eid:...``), so a binding
4
+ survives rename/move (design §3). Bindings **reuse the ADR-029 entity-association
5
+ contract** — SEI-keyed, with ``content_hash_at_attach`` drift detection, the same
6
+ pattern Filigree uses to bind issues to code — **not** a new link store
7
+ (design §4).
8
+
9
+ The write path is **authoring-time** ("speak SEI at entry," extended to intent,
10
+ design §5): when an agent creates or commits a module / public entity, Plainweave
11
+ offers an inline bind — link this SEI to a requirement (existing or a freshly
12
+ minted shell) and optionally ladder that requirement to a goal. One call,
13
+ attributed (who bound it, when). Code that skips the bind is exactly what
14
+ surfaces as an orphan via :mod:`plainweave.intent_graph`.
15
+
16
+ The ``loomweave:eid:`` SEI scheme is **FROZEN** — Plainweave consumes it, never
17
+ mints or reinterprets it. Persistence lives in :mod:`plainweave.service`; this
18
+ module holds the public value object and small drift helpers used by CLI/MCP
19
+ serializers.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from dataclasses import dataclass
25
+ from datetime import UTC, datetime
26
+ from typing import Any
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class SeiBinding:
31
+ """A code-leaf ↔ requirement binding, recorded via the ADR-029
32
+ entity-association contract.
33
+
34
+ ``sei`` is an opaque ``loomweave:eid:...`` identifier (frozen scheme;
35
+ consumed, never minted). ``content_hash_at_attach`` is stored so the consumer
36
+ read path can detect drift between the bound entity's content and its content
37
+ when the binding was made (ADR-029).
38
+ """
39
+
40
+ entity_id: str
41
+ entity_kind: str
42
+ requirement_id: str
43
+ content_hash_at_attach: str | None
44
+ drift_status: str
45
+ freshness: str
46
+ bound_by: str
47
+ bound_at: str
48
+ provenance: dict[str, Any]
49
+
50
+ @property
51
+ def sei(self) -> str:
52
+ """Backward-compatible alias for the opaque Loomweave entity ID."""
53
+ return self.entity_id
54
+
55
+
56
+ def bind_sei_to_requirement(
57
+ sei: str,
58
+ requirement_id: str,
59
+ *,
60
+ bound_by: str,
61
+ content_hash_at_attach: str | None = None,
62
+ entity_kind: str = "loomweave_entity",
63
+ bound_at: str | None = None,
64
+ provenance: dict[str, Any] | None = None,
65
+ ) -> SeiBinding:
66
+ """Construct a SEI binding value object for the ADR-029 contract.
67
+
68
+ The service method with the same semantic name persists this object. This
69
+ module-level helper stays storage-free so callers can validate envelope
70
+ shapes without opening a project database.
71
+ """
72
+ if not sei:
73
+ raise ValueError("sei is required")
74
+ if not requirement_id:
75
+ raise ValueError("requirement_id is required")
76
+ if not bound_by:
77
+ raise ValueError("bound_by is required")
78
+ return SeiBinding(
79
+ entity_id=sei,
80
+ entity_kind=entity_kind,
81
+ requirement_id=requirement_id,
82
+ content_hash_at_attach=content_hash_at_attach,
83
+ drift_status="unknown" if content_hash_at_attach is None else "attached",
84
+ freshness="unknown" if content_hash_at_attach is None else "current",
85
+ bound_by=bound_by,
86
+ bound_at=bound_at or datetime.now(UTC).isoformat(),
87
+ provenance=dict(provenance or {}),
88
+ )
89
+
90
+
91
+ def is_drifted(binding: SeiBinding, current_content_hash: str) -> bool:
92
+ """Whether the bound entity's content has changed since the binding was made
93
+ (``content_hash_at_attach`` vs. ``current_content_hash``; ADR-029 drift).
94
+ """
95
+
96
+ if binding.content_hash_at_attach is None:
97
+ return False
98
+ return binding.content_hash_at_attach != current_content_hash
plainweave/cli.py ADDED
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from collections.abc import Callable, Sequence
5
+ from typing import cast
6
+
7
+ from plainweave import __version__
8
+ from plainweave.cli_commands import register_commands
9
+
10
+ DESCRIPTION = "Plainweave requirements and verification authority."
11
+ EPILOG = "Local-core commands are available for requirements, criteria, trace, init, and diagnostics."
12
+
13
+
14
+ def build_parser() -> argparse.ArgumentParser:
15
+ parser = argparse.ArgumentParser(prog="plainweave", description=DESCRIPTION, epilog=EPILOG)
16
+ parser.add_argument("--version", action="store_true", help="Print the Plainweave version and exit.")
17
+ subparsers = parser.add_subparsers(dest="command")
18
+ register_commands(subparsers)
19
+ return parser
20
+
21
+
22
+ def main(argv: Sequence[str] | None = None) -> int:
23
+ parser = build_parser()
24
+ if argv is not None and list(argv) in (["-h"], ["--help"]):
25
+ parser.print_help()
26
+ return 0
27
+ args = parser.parse_args(argv)
28
+ if args.version:
29
+ print(f"plainweave {__version__}")
30
+ return 0
31
+ handler = getattr(args, "handler", None)
32
+ if handler is not None:
33
+ return cast(Callable[[argparse.Namespace], int], handler)(args)
34
+ parser.print_help()
35
+ return 0