ambitgraph 0.8.0__tar.gz → 0.8.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/MANIFEST.in +5 -4
  2. {ambitgraph-0.8.0/ambitgraph.egg-info → ambitgraph-0.8.2}/PKG-INFO +1 -1
  3. ambitgraph-0.8.2/ambit_contracts/__init__.py +125 -0
  4. ambitgraph-0.8.2/ambit_contracts/edge.py +200 -0
  5. ambitgraph-0.8.2/ambit_contracts/envelope.py +237 -0
  6. ambitgraph-0.8.2/ambit_contracts/fields.py +211 -0
  7. ambitgraph-0.8.2/ambit_contracts/grant.py +258 -0
  8. ambitgraph-0.8.2/ambit_contracts/household.py +148 -0
  9. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambit_map/cli.py +11 -12
  10. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambit_map/daemon.py +35 -16
  11. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambit_map/dialects/contract.py +11 -16
  12. ambitgraph-0.8.2/ambit_map/explain.py +119 -0
  13. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambit_map/families.py +142 -15
  14. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambit_map/langmirror.py +26 -49
  15. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambit_map/load.py +15 -3
  16. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambit_map/mapllm.py +1 -1
  17. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambit_map/model.py +24 -15
  18. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambit_map/orchestrate.py +84 -145
  19. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambit_map/registry.py +115 -1098
  20. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambit_map/serve.py +230 -649
  21. ambitgraph-0.8.2/ambit_telegraph/__init__.py +31 -0
  22. ambitgraph-0.8.2/ambit_telegraph/cli.py +537 -0
  23. ambitgraph-0.8.2/ambit_telegraph/export.py +395 -0
  24. ambitgraph-0.8.2/ambit_telegraph/ledger.py +921 -0
  25. ambitgraph-0.8.2/ambit_telegraph/responsibility.py +327 -0
  26. ambitgraph-0.8.2/ambit_telegraph/store.py +175 -0
  27. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/cli.py +53 -11
  28. ambitgraph-0.8.2/ambitgraph/cockpit.py +49 -0
  29. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/config.py +5 -2
  30. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/edges.py +260 -93
  31. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/jscalls.py +14 -3
  32. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/langs.py +12 -4
  33. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/omniscient_next/__init__.py +5 -2
  34. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/omniscient_next/_init_js.py +11 -2
  35. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/omniscient_next/graph.js +32 -11
  36. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/omniscient_next/library.js +3 -0
  37. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/omniscient_next/next_embed.css +26 -0
  38. ambitgraph-0.8.2/ambitgraph/omniscient_next/search.js +92 -0
  39. ambitgraph-0.8.2/ambitgraph/omniscient_next/standalone.py +167 -0
  40. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/omniscient_next/wholemap.js +44 -4
  41. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/pipeline.py +28 -3
  42. ambitgraph-0.8.2/ambitgraph/responsibility.py +140 -0
  43. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/shell.py +41 -25
  44. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/symbols.py +330 -8
  45. {ambitgraph-0.8.0 → ambitgraph-0.8.2/ambitgraph.egg-info}/PKG-INFO +1 -1
  46. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph.egg-info/SOURCES.txt +16 -13
  47. ambitgraph-0.8.2/ambitgraph.egg-info/top_level.txt +4 -0
  48. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/pyproject.toml +9 -7
  49. ambitgraph-0.8.0/ambit_map/export.py +0 -656
  50. ambitgraph-0.8.0/ambit_map/graph.py +0 -3091
  51. ambitgraph-0.8.0/ambit_map/island_layout.py +0 -462
  52. ambitgraph-0.8.0/ambit_map/islands.py +0 -474
  53. ambitgraph-0.8.0/ambit_map/judged.py +0 -279
  54. ambitgraph-0.8.0/ambit_map/kitdemo.py +0 -274
  55. ambitgraph-0.8.0/ambit_map/library.py +0 -2038
  56. ambitgraph-0.8.0/ambit_map/matched.py +0 -705
  57. ambitgraph-0.8.0/ambit_map/objects.py +0 -1046
  58. ambitgraph-0.8.0/ambit_map/page.py +0 -213
  59. ambitgraph-0.8.0/ambit_map/paths.py +0 -1434
  60. ambitgraph-0.8.0/ambit_map/theme.py +0 -568
  61. ambitgraph-0.8.0/ambit_map/unitpage.py +0 -1086
  62. ambitgraph-0.8.0/ambitgraph/cockpit.py +0 -32
  63. ambitgraph-0.8.0/ambitgraph.egg-info/top_level.txt +0 -2
  64. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/LICENSE +0 -0
  65. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/README.md +0 -0
  66. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambit_map/__init__.py +0 -0
  67. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambit_map/__main__.py +0 -0
  68. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambit_map/ambit-map-enrich.md +0 -0
  69. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambit_map/ambit-mapllm-contract.md +0 -0
  70. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambit_map/dialects/__init__.py +0 -0
  71. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambit_map/dialects/express_pack.py +0 -0
  72. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambit_map/dialects/fastapi_pack.py +0 -0
  73. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambit_map/dialects/flask_pack.py +0 -0
  74. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambit_map/dialects/nextjs_pack.py +0 -0
  75. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambit_map/dialects/supabase_pack.py +0 -0
  76. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/WALKTHROUGH.md +0 -0
  77. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/__init__.py +0 -0
  78. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/__main__.py +0 -0
  79. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/_delimited.py +0 -0
  80. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/codelib.py +0 -0
  81. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/console.py +0 -0
  82. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/demo.py +0 -0
  83. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/design_tokens.py +0 -0
  84. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/docs_discovery.py +0 -0
  85. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/doctor.py +0 -0
  86. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/duplication.py +0 -0
  87. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/emit.py +0 -0
  88. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/engine_identity.py +0 -0
  89. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/estimate.py +0 -0
  90. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/eval.py +0 -0
  91. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/feeds.py +0 -0
  92. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/gate.py +0 -0
  93. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/gate_check.py +0 -0
  94. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/gate_server.py +0 -0
  95. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/harvest.py +0 -0
  96. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/inventory.py +0 -0
  97. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/mds.py +0 -0
  98. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/merge.py +0 -0
  99. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/merge_jsp.py +0 -0
  100. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/omniscient_next/filterpanel.js +0 -0
  101. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/omniscient_next/siderail.js +0 -0
  102. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/parameterize.py +0 -0
  103. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/portal.py +0 -0
  104. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/prompts.py +0 -0
  105. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/pycalls.py +0 -0
  106. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/ratify.py +0 -0
  107. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/report.py +0 -0
  108. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/reports.py +0 -0
  109. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/sample.py +0 -0
  110. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/secretary.py +0 -0
  111. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/stages.py +0 -0
  112. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/tables.py +0 -0
  113. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/tasks.py +0 -0
  114. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph/urcode.py +0 -0
  115. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph.egg-info/dependency_links.txt +0 -0
  116. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph.egg-info/entry_points.txt +0 -0
  117. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/ambitgraph.egg-info/requires.txt +0 -0
  118. {ambitgraph-0.8.0 → ambitgraph-0.8.2}/setup.cfg +0 -0
@@ -7,15 +7,16 @@ prune docs
7
7
  prune build
8
8
  prune dist
9
9
 
10
- # Internal-only packages -- also excluded from the wheel via the packages list.
11
- prune ambit_contracts
12
- prune ambit_telegraph
13
-
14
10
  # Development evidence directories (screenshots, transcripts, review notes).
15
11
  # Both spellings: the dated evidence-* folders and the single evidence/ folder they moved into.
16
12
  prune evidence-*
17
13
  prune evidence
18
14
 
15
+ # The repository's own responsibility record (.ambit/responsibility.jsonl):
16
+ # it describes THIS repository's people and travels with the source tree,
17
+ # never with the installed package.
18
+ prune .ambit
19
+
19
20
  # Internal process documents.
20
21
  exclude AGENTS.md
21
22
  exclude CHANGELOG.md
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ambitgraph
3
- Version: 0.8.0
3
+ Version: 0.8.2
4
4
  Summary: Read-only codebase scan: reusable units, families, roles, ownership candidates, duplication.
5
5
  Author: AmbitGraph
6
6
  License: Proprietary
@@ -0,0 +1,125 @@
1
+ """ambit_contracts -- dataclasses, JSON-schema dicts, validators and the
2
+ shared text sanitizer for the Telegraph's wire, grant, household and edge
3
+ contracts.
4
+
5
+ Pure functions, stdlib only. NO I/O, NO file locking, NO clock, NO id
6
+ minting: every id and timestamp used here is checked, never generated --
7
+ that is why every validator in this package is testable with fixed inputs
8
+ and no fixtures, and why `grant_is_active` takes `now` as an argument.
9
+
10
+ The asymmetric import wall: `ambitgraph` (the engine) and `ambit_telegraph`
11
+ (the message service) are BOTH permitted to import this package; it imports
12
+ neither of them, nor anything outside the standard library.
13
+ `tests/test_import_boundaries.py` is the mechanical guard and the ground
14
+ truth for what the wall's other directions allow; read it, not this
15
+ docstring, for their specifics.
16
+
17
+ Reject, never repair: `clean` is what a CALLER uses to make a value
18
+ acceptable before building a wire, and what every print boundary uses on
19
+ the way out. The `validate_*` functions never mutate their argument -- an
20
+ invalid value is refused, not silently cleaned up, so a corrupt or hostile
21
+ wire is visible as corrupt rather than quietly repaired.
22
+ """
23
+ from .edge import (
24
+ EDGE_EVENT_SCHEMA,
25
+ EDGE_STATES,
26
+ EdgeEvent,
27
+ edge_key,
28
+ edge_state,
29
+ has_channel,
30
+ validate_channel,
31
+ validate_edge_event,
32
+ )
33
+ from .envelope import (
34
+ EDGE_KINDS,
35
+ WIRE_KINDS,
36
+ WIRE_SCHEMA,
37
+ Wire,
38
+ validate_wire,
39
+ wire_from_dict,
40
+ wire_to_dict,
41
+ )
42
+ from .fields import (
43
+ CAPS,
44
+ CONTRACT_VERSION,
45
+ MAX_SEATS,
46
+ ContractError,
47
+ clean,
48
+ normalize_address,
49
+ validate_address,
50
+ )
51
+ from .grant import (
52
+ GRANT_SCHEMA,
53
+ GRANT_VERDICTS,
54
+ MASTER_OFF_SCHEMA,
55
+ REVOKE_SCHEMA,
56
+ Grant,
57
+ MasterOffEvent,
58
+ RevokeEvent,
59
+ grant_is_active,
60
+ grant_liveness,
61
+ validate_grant,
62
+ validate_master_off,
63
+ validate_revoke,
64
+ )
65
+ from .household import (
66
+ DESIGNATION_SCHEMA,
67
+ ROLES,
68
+ UNDESIGNATION_SCHEMA,
69
+ Designation,
70
+ Undesignation,
71
+ is_guest,
72
+ role_of,
73
+ validate_authorship,
74
+ validate_delivery,
75
+ validate_designation,
76
+ validate_undesignation,
77
+ )
78
+
79
+ __all__ = [
80
+ "CAPS",
81
+ "CONTRACT_VERSION",
82
+ "MAX_SEATS",
83
+ "ContractError",
84
+ "clean",
85
+ "normalize_address",
86
+ "EDGE_KINDS",
87
+ "WIRE_KINDS",
88
+ "WIRE_SCHEMA",
89
+ "Wire",
90
+ "validate_wire",
91
+ "wire_from_dict",
92
+ "wire_to_dict",
93
+ "GRANT_SCHEMA",
94
+ "Grant",
95
+ "validate_grant",
96
+ "grant_is_active",
97
+ "GRANT_VERDICTS",
98
+ "grant_liveness",
99
+ "REVOKE_SCHEMA",
100
+ "RevokeEvent",
101
+ "validate_revoke",
102
+ "MASTER_OFF_SCHEMA",
103
+ "MasterOffEvent",
104
+ "validate_master_off",
105
+ "DESIGNATION_SCHEMA",
106
+ "ROLES",
107
+ "Designation",
108
+ "role_of",
109
+ "is_guest",
110
+ "validate_delivery",
111
+ "validate_authorship",
112
+ "UNDESIGNATION_SCHEMA",
113
+ "Undesignation",
114
+ "validate_designation",
115
+ "validate_undesignation",
116
+ "validate_address",
117
+ "EdgeEvent",
118
+ "EDGE_EVENT_SCHEMA",
119
+ "EDGE_STATES",
120
+ "validate_edge_event",
121
+ "edge_key",
122
+ "edge_state",
123
+ "validate_channel",
124
+ "has_channel",
125
+ ]
@@ -0,0 +1,200 @@
1
+ """ambit_contracts.edge -- the edge model: a pure record and pure rules for
2
+ authority and affinity edges between seats. This module lives BESIDE the
3
+ five founding contracts, not inside ambit_telegraph, for the same reuse
4
+ reason every shared type in this package exists: the CLI and the Household
5
+ surface will both need to read an edge event, and a contract type living
6
+ outside the contract layer would be re-derived by each of them -- reuse,
7
+ never a second copy of a shape.
8
+
9
+ In the local era the Household IS the org graph -- there is no separate
10
+ graph file. An AUTHORITY edge exists IMPLICITLY from the principal to every
11
+ seat whose role is exactly `member`: never written, derived from the
12
+ registry on every call. Every OTHER edge -- authority in either direction,
13
+ affinity between any two peers -- is an EXPLICIT ledger event that only the
14
+ principal may append, switchable on and off by later events, latest-wins in
15
+ append order. `edge_state` returns `None`, not `"off"`, when no event
16
+ exists, which is the whole reason the implicit edge composes cleanly with
17
+ the explicit ones: an explicit event ALWAYS beats the implicit edge, so the
18
+ operator can write `authority FOUNDER->DEV off` and have it actually mean
19
+ something, rather than being silently ignored.
20
+
21
+ Authority edges are DIRECTED (either lexical order is a legal, distinct
22
+ pair). Affinity edges are SYMMETRIC -- affinity is defined as "the two
23
+ seats are related and can see each other's news", a mutual predicate
24
+ with no word for a half-open state -- and `validate_edge_event` makes that
25
+ symmetry MECHANICAL rather than a comparison scattered around every caller:
26
+ an affinity event must name its two seats in canonical (normalized,
27
+ ascending) order, so one relationship can never split across two spellings
28
+ and two different latest-wins histories.
29
+
30
+ Stdlib only (wall (c) of the Telegraph's import wall). No I/O, no clock:
31
+ `edge_state` and `validate_channel` take the events list and the roles they
32
+ need as arguments, the same purity line `role_of` already holds.
33
+ """
34
+ from dataclasses import dataclass
35
+
36
+ from .envelope import EDGE_KINDS
37
+ from .fields import (
38
+ CAPS,
39
+ ContractError,
40
+ _require_address,
41
+ _require_id,
42
+ _require_timestamp,
43
+ _require_version,
44
+ build_schema,
45
+ normalize_address,
46
+ )
47
+
48
+ EDGE_STATES = frozenset({"on", "off"})
49
+
50
+ # Same address shape as envelope.py's _ADDRESS_RE -- duplicated as a literal
51
+ # rather than imported across modules, matching this package's own posture
52
+ # toward small shared regexes (see envelope.py's own comment on caps).
53
+ _ADDRESS_RE = r"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$"
54
+
55
+
56
+ @dataclass
57
+ class EdgeEvent:
58
+ """A dumb, frozen-in-spirit record -- no validation in __post_init__,
59
+ same posture as Wire: the validator, not the dataclass, is the law."""
60
+ v: int
61
+ id: str
62
+ edge_kind: str # authority | affinity (EDGE_KINDS)
63
+ seat_a: str
64
+ seat_b: str
65
+ state: str # "on" | "off" -- a STRING enum, not a bool
66
+ ts: str # informational, the ts law
67
+ set_by: str # the principal's address
68
+
69
+
70
+ EDGE_EVENT_SCHEMA = build_schema(EdgeEvent, "EdgeEvent", {
71
+ "v": {"type": "integer", "const": 1},
72
+ "id": {"type": "string", "pattern": r"^e-[a-z0-9]{8,62}$", "maxLength": CAPS["id"]},
73
+ "edge_kind": {"type": "string", "enum": sorted(EDGE_KINDS), "maxLength": CAPS["enum"]},
74
+ "seat_a": {"type": "string", "pattern": _ADDRESS_RE, "maxLength": CAPS["address"]},
75
+ "seat_b": {"type": "string", "pattern": _ADDRESS_RE, "maxLength": CAPS["address"]},
76
+ "state": {"type": "string", "enum": sorted(EDGE_STATES), "maxLength": CAPS["enum"]},
77
+ "ts": {"type": "string", "maxLength": CAPS["timestamp"]},
78
+ "set_by": {"type": "string", "maxLength": CAPS["address"]},
79
+ })
80
+
81
+
82
+ def validate_edge_event(event: EdgeEvent) -> None:
83
+ """Shape and enums; `seat_a != seat_b` case-insensitively (a self-edge
84
+ is the wire contract's self-address law, twin -- a seat authorising or
85
+ befriending itself is exactly what an edge exists to describe between
86
+ TWO seats); and, for affinity only, the canonical ascending seat order
87
+ that makes symmetry mechanical. `state` is checked against the STRING
88
+ enum only -- `isinstance(True, str)` is False, so a bool is refused by
89
+ the same branch as any other non-string, no special case needed."""
90
+ _require_version(event.v, "v")
91
+ _require_id(event.id, "e", "edge event id")
92
+ if not isinstance(event.edge_kind, str) or event.edge_kind not in EDGE_KINDS:
93
+ raise ContractError("edge event edge_kind is not one of authority or affinity")
94
+ _require_address(event.seat_a, "edge event seat_a")
95
+ _require_address(event.seat_b, "edge event seat_b")
96
+ if normalize_address(event.seat_a) == normalize_address(event.seat_b):
97
+ raise ContractError(
98
+ "edge event seat_a and seat_b are the same address; a seat may "
99
+ "not hold an edge with itself")
100
+ if not isinstance(event.state, str) or event.state not in EDGE_STATES:
101
+ raise ContractError("edge event state is not one of on or off")
102
+ _require_timestamp(event.ts, "edge event ts")
103
+ _require_address(event.set_by, "edge event set_by")
104
+
105
+ if event.edge_kind == "affinity":
106
+ if not normalize_address(event.seat_a) < normalize_address(event.seat_b):
107
+ raise ContractError(
108
+ "an affinity edge event must name seat_a and seat_b in "
109
+ "canonical (normalized, ascending) order; the relationship "
110
+ "is symmetric and must have exactly one spelling")
111
+
112
+
113
+ def edge_key(edge_kind, seat_a, seat_b) -> tuple:
114
+ """authority -> (kind, norm(a), norm(b)), DIRECTED -- order preserved.
115
+ affinity -> (kind, min, max), SYMMETRIC -- the same key regardless of
116
+ argument order, so latest-wins can never split one relationship across
117
+ two orderings of the same pair."""
118
+ a, b = normalize_address(seat_a), normalize_address(seat_b)
119
+ if edge_kind == "affinity":
120
+ return (edge_kind, min(a, b), max(a, b))
121
+ return (edge_kind, a, b)
122
+
123
+
124
+ def edge_state(edge_events, edge_kind, seat_a, seat_b):
125
+ """`None` means NO EVENT EXISTS -- never `"off"`. That distinction is
126
+ the whole reason the implicit edge composes cleanly with the explicit
127
+ ones (see the module docstring). The LAST matching event in LIST order
128
+ wins -- never sorted by `ts`, the same ordering law as every other
129
+ reduction in this contract (`role_of`'s twin)."""
130
+ target = edge_key(edge_kind, seat_a, seat_b)
131
+ result = None
132
+ for ev in edge_events:
133
+ if edge_key(ev.edge_kind, ev.seat_a, ev.seat_b) == target:
134
+ result = ev.state
135
+ return result
136
+
137
+
138
+ _NO_AUTHORITY_CHANNEL = (
139
+ "there is no live authority edge from the sender to the recipient; a "
140
+ "wire needs an edge of its claimed kind, in its direction, and only "
141
+ "the principal may switch one on")
142
+ _NO_AFFINITY_CHANNEL = (
143
+ "there is no live affinity edge between the sender and the recipient; "
144
+ "a wire needs an edge of its claimed kind, in its direction, and only "
145
+ "the principal may switch one on")
146
+
147
+
148
+ def validate_channel(wire, sender_role, to_role, edge_events) -> None:
149
+ """The complete channel law -- the ledger law the wire contract defers
150
+ to: `edge_kind` is a CLAIM the wire makes about itself, and this
151
+ is where the claim is checked against the real edge. Mirrors the
152
+ household contract's `validate_delivery(wire, sender_role, to_role)`:
153
+ the caller looks up both roles (I/O), this function does none. Raises
154
+ `ContractError`, not a new type -- a wire travelling an edge that does
155
+ not exist is a false claim about itself, the same class as the nine
156
+ WIRE LAWS, and this package cannot see `TelegraphError` anyway (wall
157
+ (c): stdlib only).
158
+
159
+ (1) affinity -- live iff an explicit event says "on"; there is no
160
+ implicit affinity edge, ever, so no event at all refuses.
161
+ (2) authority -- a `receipt` travels the edge backwards ("a receipt for
162
+ a binding travels the same authority edge, backwards; edge_kind
163
+ names the edge, not the direction"), every other kind needs it
164
+ forwards. An explicit event on that directed pair always wins;
165
+ only when none exists does the implicit principal->member rule
166
+ apply, read in the direction the wire actually needs.
167
+ (3) not live -> ContractError naming no value.
168
+ """
169
+ if wire.edge_kind == "affinity":
170
+ if edge_state(edge_events, "affinity", wire.sender, wire.to) == "on":
171
+ return
172
+ raise ContractError(_NO_AFFINITY_CHANNEL)
173
+
174
+ if wire.kind == "receipt":
175
+ src, dst = wire.to, wire.sender
176
+ else:
177
+ src, dst = wire.sender, wire.to
178
+
179
+ explicit = edge_state(edge_events, "authority", src, dst)
180
+ if explicit is not None:
181
+ live = explicit == "on"
182
+ else:
183
+ role_src = (sender_role if normalize_address(src) == normalize_address(wire.sender)
184
+ else to_role)
185
+ role_dst = (to_role if normalize_address(dst) == normalize_address(wire.to)
186
+ else sender_role)
187
+ live = role_src == "principal" and role_dst == "member"
188
+
189
+ if not live:
190
+ raise ContractError(_NO_AUTHORITY_CHANNEL)
191
+
192
+
193
+ def has_channel(wire, sender_role, to_role, edge_events) -> bool:
194
+ """The boolean twin of `validate_channel`, for a caller that wants a
195
+ yes/no answer rather than a raised, plain-words reason."""
196
+ try:
197
+ validate_channel(wire, sender_role, to_role, edge_events)
198
+ return True
199
+ except ContractError:
200
+ return False
@@ -0,0 +1,237 @@
1
+ """The Wire -- the CONTRACT is the envelope; the OBJECT is a Wire.
2
+
3
+ Four kinds travel it: receipt | status | ask | binding. A binding wire
4
+ carries an instruction and MUST travel an authority edge with an
5
+ acknowledgment demanded and a grant named; every other kind is news or a
6
+ question and may never demand an ack or name a grant. These are the nine
7
+ WIRE LAWS; `validate_wire`/`wire_from_dict` are the AUTHORITATIVE
8
+ enforcement of all nine (`wire_from_dict` validates before it ever
9
+ constructs a `Wire` that callers keep). `WIRE_SCHEMA` independently encodes
10
+ six of the nine (shape, caps, control chars, and the four kind-conditional
11
+ laws) so a consumer validating a raw dict against the published schema
12
+ alone gets most of the same refusals -- see its `allOf` definition below
13
+ for exactly which one law it cannot express.
14
+
15
+ What this file CANNOT prove, stated rather than faked: `edge_kind` is a
16
+ CLAIM the wire makes about itself. This module checks only that the claim
17
+ is internally coherent (a binding wire claims an authority edge); checking
18
+ the claim against the real graph edge, and matching a receipt to the
19
+ binding it acknowledges, needs the ledger and is a ledger law, not a static one.
20
+ """
21
+ from dataclasses import dataclass
22
+
23
+ from .fields import (
24
+ CAPS,
25
+ ContractError,
26
+ _require_address,
27
+ _require_id,
28
+ _require_text_cap,
29
+ _require_timestamp,
30
+ _require_version,
31
+ build_schema,
32
+ normalize_address,
33
+ )
34
+
35
+ WIRE_KINDS = frozenset({"receipt", "status", "ask", "binding"})
36
+ EDGE_KINDS = frozenset({"authority", "affinity"})
37
+
38
+
39
+ @dataclass
40
+ class Wire:
41
+ """A dumb, frozen-in-spirit record. No validation in `__post_init__` --
42
+ that would make it impossible to construct an invalid one for a test,
43
+ and the validator, not the dataclass, is the law."""
44
+ v: int
45
+ id: str
46
+ ts: str
47
+ sender: str
48
+ to: str
49
+ kind: str
50
+ edge_kind: str
51
+ body: str
52
+ requires_ack: bool
53
+ grant_id: object # str (g-...) or None
54
+ in_reply_to: object # str (w-...) or None
55
+ # OPTIONAL. One sentence a reader with zero context
56
+ # understands -- principle 16's line, as its own field rather than a
57
+ # marker inside the body. Defaults to None because 728 wires predate it
58
+ # and the ledger is append-only: every historical line must still read.
59
+ # Its rules are the body's law at a shorter cap: 200 characters, and
60
+ # control/bidi characters REFUSED, never scrubbed.
61
+ plain: object = None # str (<=200, clean) or None
62
+
63
+
64
+ # Same character-exclusion set as `clean`, spelled as a regex so the
65
+ # published schema can refuse a bidi/control payload too, not only the
66
+ # hand-written validator -- a schema without it once accepted a body
67
+ # carrying a bidi override. Every code point
68
+ # is an \x/\u escape, never a literal glyph -- this file's own bytes must
69
+ # not carry the characters it exists to refuse.
70
+ _NO_CONTROL_OR_BIDI = (
71
+ r"^[^\x00-\x1f\x7f-\x9f\u2028\u2029\u200e\u200f"
72
+ r"\u202a-\u202e\u2066-\u2069]*$")
73
+ _ADDRESS_RE = r"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$"
74
+ # Deliberately loose -- shape only, matching `_require_timestamp`'s own
75
+ # stated scope ("nothing checked but shape, parseability, tz-awareness"):
76
+ # YYYY-MM-DD, a T or space, HH:MM:SS, optional fractional seconds, and
77
+ # either Z or a +HH:MM/-HH:MM offset. Enough to refuse 'pwned' outright.
78
+ _TIMESTAMP_RE = (
79
+ r"^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$")
80
+
81
+ WIRE_SCHEMA = build_schema(Wire, "Wire", {
82
+ "v": {"type": "integer", "const": 1},
83
+ "id": {"type": "string", "pattern": r"^w-[a-z0-9]{8,62}$", "maxLength": CAPS["id"]},
84
+ "ts": {"type": "string", "pattern": _TIMESTAMP_RE, "maxLength": CAPS["timestamp"]},
85
+ "sender": {"type": "string", "pattern": _ADDRESS_RE, "maxLength": CAPS["address"]},
86
+ "to": {"type": "string", "pattern": _ADDRESS_RE, "maxLength": CAPS["address"]},
87
+ "kind": {"type": "string", "enum": sorted(WIRE_KINDS), "maxLength": CAPS["enum"]},
88
+ "edge_kind": {"type": "string", "enum": sorted(EDGE_KINDS), "maxLength": CAPS["enum"]},
89
+ "body": {"type": "string", "pattern": _NO_CONTROL_OR_BIDI, "maxLength": CAPS["body"]},
90
+ "plain": {"type": ["string", "null"], "pattern": _NO_CONTROL_OR_BIDI,
91
+ "minLength": 1, "maxLength": CAPS["plain"]},
92
+ "requires_ack": {"type": "boolean"},
93
+ "grant_id": {"type": ["string", "null"], "pattern": r"^g-[a-z0-9]{8,62}$", "maxLength": CAPS["id"]},
94
+ "in_reply_to": {"type": ["string", "null"], "pattern": r"^w-[a-z0-9]{8,62}$", "maxLength": CAPS["id"]},
95
+ })
96
+
97
+ # The four kind-conditional WIRE LAWS (1/2/3, 4/5, 6), encoded so a consumer
98
+ # validating a raw dict against WIRE_SCHEMA alone -- never having seen
99
+ # `validate_wire` -- gets the same refusals. WHAT THIS CANNOT EXPRESS (same
100
+ # honesty posture as the module docstring above): sender != to (the
101
+ # self-address law) needs cross-field comparison that plain JSON Schema has
102
+ # no keyword for; that law is enforced by `validate_wire` only.
103
+ WIRE_SCHEMA["allOf"] = [
104
+ # LAW 1 -- binding travels authority only.
105
+ {
106
+ "if": {"properties": {"kind": {"const": "binding"}}, "required": ["kind"]},
107
+ "then": {"properties": {"edge_kind": {"const": "authority"}},
108
+ "required": ["edge_kind"]},
109
+ },
110
+ # LAW 2 / 3 -- only binding may demand an ack, and it must.
111
+ {
112
+ "if": {"properties": {"kind": {"const": "binding"}}, "required": ["kind"]},
113
+ "then": {"properties": {"requires_ack": {"const": True}},
114
+ "required": ["requires_ack"]},
115
+ "else": {"properties": {"requires_ack": {"const": False}},
116
+ "required": ["requires_ack"]},
117
+ },
118
+ # LAW 4 / 5 -- only binding names a grant, and it must, well-formed.
119
+ {
120
+ "if": {"properties": {"kind": {"const": "binding"}}, "required": ["kind"]},
121
+ "then": {"properties": {
122
+ "grant_id": {"type": "string", "pattern": r"^g-[a-z0-9]{8,62}$"}}},
123
+ "else": {"properties": {"grant_id": {"type": "null"}}},
124
+ },
125
+ # LAW 6 -- a receipt names what it acknowledges.
126
+ {
127
+ "if": {"properties": {"kind": {"const": "receipt"}}, "required": ["kind"]},
128
+ "then": {"properties": {
129
+ "in_reply_to": {"type": "string", "pattern": r"^w-[a-z0-9]{8,62}$"}},
130
+ "required": ["in_reply_to"]},
131
+ },
132
+ ]
133
+
134
+
135
+ def validate_wire(wire: Wire) -> None:
136
+ """The nine WIRE LAWS, in order: shape first (LAW 7, caps LAW 8,
137
+ control chars LAW 9), then the four semantic laws that read `kind`
138
+ against `edge_kind`/`requires_ack`/`grant_id`/`in_reply_to`."""
139
+ _require_version(wire.v, "v")
140
+ _require_id(wire.id, "w", "wire id")
141
+ _require_timestamp(wire.ts, "ts")
142
+ _require_address(wire.sender, "sender")
143
+ _require_address(wire.to, "to")
144
+ if normalize_address(wire.sender) == normalize_address(wire.to):
145
+ raise ContractError("wire sender and to are the same address; a wire may not address itself")
146
+ if not isinstance(wire.kind, str) or wire.kind not in WIRE_KINDS:
147
+ raise ContractError("wire kind is not one of receipt, status, ask or binding")
148
+ if not isinstance(wire.edge_kind, str) or wire.edge_kind not in EDGE_KINDS:
149
+ raise ContractError("wire edge_kind is not one of authority or affinity")
150
+ _require_text_cap(wire.body, CAPS["body"], "wire body")
151
+ if wire.plain is not None:
152
+ # Same law, shorter cap. An empty line is refused rather than
153
+ # stored: an author who wrote nothing wrote nothing, and absent
154
+ # already means that.
155
+ _require_text_cap(wire.plain, CAPS["plain"], "wire plain line",
156
+ allow_empty=False)
157
+ if wire.requires_ack is not True and wire.requires_ack is not False:
158
+ raise ContractError("wire requires_ack is not a bool")
159
+
160
+ # WIRE LAW 1 -- binding travels authority only.
161
+ if wire.kind == "binding" and wire.edge_kind == "affinity":
162
+ raise ContractError(
163
+ "a binding wire may travel only an authority edge; this one is "
164
+ "addressed along an affinity edge, and affinity edges carry "
165
+ "news, never instructions")
166
+
167
+ # WIRE LAW 2 / 3 -- only binding may demand an ack, and it must.
168
+ if wire.kind == "binding":
169
+ if wire.requires_ack is not True:
170
+ raise ContractError("a binding wire must require an acknowledgment")
171
+ elif wire.requires_ack is not False:
172
+ raise ContractError("only a binding wire may require an acknowledgment")
173
+
174
+ # WIRE LAW 4 / 5 -- only binding names a grant, and it must, well-formed.
175
+ if wire.kind == "binding":
176
+ if wire.grant_id is None:
177
+ raise ContractError(
178
+ "a binding wire must name the grant that authorizes it; "
179
+ "absence of a grant is off")
180
+ _require_id(wire.grant_id, "g", "wire grant_id")
181
+ elif wire.grant_id is not None:
182
+ raise ContractError("only a binding wire may name a grant_id")
183
+
184
+ # WIRE LAW 6 -- a receipt names what it acknowledges; others may.
185
+ if wire.kind == "receipt":
186
+ if wire.in_reply_to is None:
187
+ raise ContractError(
188
+ "a receipt wire must name the wire it acknowledges in in_reply_to")
189
+ _require_id(wire.in_reply_to, "w", "wire in_reply_to")
190
+ elif wire.in_reply_to is not None:
191
+ _require_id(wire.in_reply_to, "w", "wire in_reply_to")
192
+
193
+
194
+ def wire_from_dict(d: dict) -> Wire:
195
+ """Validate BEFORE constructing -- an invalid dict never becomes a Wire
196
+ a caller can hold onto."""
197
+ if not isinstance(d, dict):
198
+ raise ContractError("wire payload is not an object")
199
+ required = set(WIRE_SCHEMA["required"])
200
+ # OPTIONAL fields are known but not demanded: a wire written before the
201
+ # field existed must still read (the append-only law), and a wire carrying
202
+ # it must not be refused as "unknown". Everything outside this union is
203
+ # still refused outright -- one field became optional, the contract did
204
+ # not become permissive.
205
+ optional = set(WIRE_SCHEMA["properties"]) - required
206
+ actual = set(d.keys())
207
+ missing = required - actual
208
+ if missing:
209
+ raise ContractError(
210
+ f"wire is missing required field(s): {', '.join(sorted(missing))}")
211
+ if actual - required - optional:
212
+ # the extra key NAME came from outside the program -- DATA, never
213
+ # interpolated into the message: an error message must never carry
214
+ # a value an outsider chose.
215
+ raise ContractError(
216
+ "wire has an unknown field that is not part of the wire contract")
217
+ wire = Wire(**d)
218
+ validate_wire(wire)
219
+ return wire
220
+
221
+
222
+ def wire_to_dict(wire: Wire) -> dict:
223
+ return {
224
+ "v": wire.v,
225
+ "id": wire.id,
226
+ "ts": wire.ts,
227
+ "sender": wire.sender,
228
+ "to": wire.to,
229
+ "kind": wire.kind,
230
+ "edge_kind": wire.edge_kind,
231
+ "body": wire.body,
232
+ "requires_ack": wire.requires_ack,
233
+ "grant_id": wire.grant_id,
234
+ "in_reply_to": wire.in_reply_to,
235
+ "plain": wire.plain,
236
+ }
237
+