ambitgraph 0.8.2__tar.gz → 0.8.3__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.
- {ambitgraph-0.8.2/ambitgraph.egg-info → ambitgraph-0.8.3}/PKG-INFO +1 -1
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambit_telegraph/responsibility.py +76 -24
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/cli.py +5 -4
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/responsibility.py +83 -36
- {ambitgraph-0.8.2 → ambitgraph-0.8.3/ambitgraph.egg-info}/PKG-INFO +1 -1
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/pyproject.toml +6 -1
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/LICENSE +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/MANIFEST.in +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/README.md +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambit_contracts/__init__.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambit_contracts/edge.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambit_contracts/envelope.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambit_contracts/fields.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambit_contracts/grant.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambit_contracts/household.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambit_map/__init__.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambit_map/__main__.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambit_map/ambit-map-enrich.md +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambit_map/ambit-mapllm-contract.md +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambit_map/cli.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambit_map/daemon.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambit_map/dialects/__init__.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambit_map/dialects/contract.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambit_map/dialects/express_pack.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambit_map/dialects/fastapi_pack.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambit_map/dialects/flask_pack.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambit_map/dialects/nextjs_pack.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambit_map/dialects/supabase_pack.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambit_map/explain.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambit_map/families.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambit_map/langmirror.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambit_map/load.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambit_map/mapllm.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambit_map/model.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambit_map/orchestrate.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambit_map/registry.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambit_map/serve.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambit_telegraph/__init__.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambit_telegraph/cli.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambit_telegraph/export.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambit_telegraph/ledger.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambit_telegraph/store.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/WALKTHROUGH.md +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/__init__.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/__main__.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/_delimited.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/cockpit.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/codelib.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/config.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/console.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/demo.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/design_tokens.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/docs_discovery.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/doctor.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/duplication.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/edges.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/emit.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/engine_identity.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/estimate.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/eval.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/feeds.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/gate.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/gate_check.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/gate_server.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/harvest.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/inventory.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/jscalls.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/langs.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/mds.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/merge.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/merge_jsp.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/omniscient_next/__init__.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/omniscient_next/_init_js.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/omniscient_next/filterpanel.js +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/omniscient_next/graph.js +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/omniscient_next/library.js +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/omniscient_next/next_embed.css +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/omniscient_next/search.js +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/omniscient_next/siderail.js +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/omniscient_next/standalone.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/omniscient_next/wholemap.js +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/parameterize.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/pipeline.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/portal.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/prompts.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/pycalls.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/ratify.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/report.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/reports.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/sample.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/secretary.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/shell.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/stages.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/symbols.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/tables.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/tasks.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph/urcode.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph.egg-info/SOURCES.txt +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph.egg-info/dependency_links.txt +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph.egg-info/entry_points.txt +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph.egg-info/requires.txt +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/ambitgraph.egg-info/top_level.txt +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.3}/setup.cfg +0 -0
|
@@ -17,6 +17,17 @@ the exact chain, latest line wins, so a transfer is an off record plus an
|
|
|
17
17
|
on record and the file itself is the transfer history. Two different
|
|
18
18
|
chains both on for one node are two owners, not a conflict.
|
|
19
19
|
|
|
20
|
+
A node ending with "/" is a folder node; any other node is a file node
|
|
21
|
+
(queries for a file path never end with "/"). A file's own exact record
|
|
22
|
+
does not replace the nearest covering folder's record -- it ADDS to it:
|
|
23
|
+
a file answers through its own on-records first, then through the on-
|
|
24
|
+
records standing at the nearest folder that covers it. A sub-folder's own
|
|
25
|
+
claim is itself a folder node, so it carves its subtree out of any outer
|
|
26
|
+
folder's reach. Turning a file's exact chain off closes only that
|
|
27
|
+
chain's claim on the file; the folder's owners are untouched. Querying a
|
|
28
|
+
folder node itself is unaffected: the longest matching folder prefix
|
|
29
|
+
still wins alone.
|
|
30
|
+
|
|
20
31
|
Authorship law: a record's `set_by` must already stand in the chain
|
|
21
32
|
strictly before the first element the record changes relative to the
|
|
22
33
|
standing lineage it extends or supersedes; a chain's root is changed only
|
|
@@ -185,36 +196,77 @@ def _covers(rec_node: str, node: str) -> bool:
|
|
|
185
196
|
return rec_node.endswith("/") and node.startswith(rec_node)
|
|
186
197
|
|
|
187
198
|
|
|
199
|
+
def _resolve_one(rec, *, now, gone) -> "Resolution":
|
|
200
|
+
"""One standing on-record turned into its Resolution: the holder is the
|
|
201
|
+
deepest chain element that is neither departed nor (for the acting,
|
|
202
|
+
last element) past its expiry."""
|
|
203
|
+
live = []
|
|
204
|
+
# pair each holder with its root-first depth, so the acting (last)
|
|
205
|
+
# holder can be singled out below for the expiry check
|
|
206
|
+
for position, holder in enumerate(rec.chain):
|
|
207
|
+
if holder in gone:
|
|
208
|
+
break
|
|
209
|
+
# only the chain's last element is the acting holder -- an expiry
|
|
210
|
+
# never applies to a delegator further up the chain
|
|
211
|
+
acting = position == len(rec.chain) - 1
|
|
212
|
+
if acting and rec.expires is not None \
|
|
213
|
+
and now >= _parse_ts(rec.expires, "expires"):
|
|
214
|
+
break
|
|
215
|
+
live.append(holder)
|
|
216
|
+
# the deepest surviving holder answers; an empty walk means nobody does
|
|
217
|
+
return Resolution(record=rec, chain=rec.chain,
|
|
218
|
+
holder=live[-1] if live else None)
|
|
219
|
+
|
|
220
|
+
|
|
188
221
|
def resolve_owners(records, node, *, now, gone=frozenset()) -> tuple:
|
|
189
222
|
"""Who owns `node` right now? Pure: the clock arrives as `now`, the
|
|
190
223
|
departed arrive as `gone`, and nothing here touches the file system.
|
|
191
224
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
225
|
+
A folder query (`node` ends with "/") keeps the plain longest-prefix
|
|
226
|
+
rule: every on-record standing at the longest matching folder node is
|
|
227
|
+
one owner. A file query answers with every on-record standing at the
|
|
228
|
+
file's own exact node, PLUS every on-record standing at the nearest
|
|
229
|
+
covering folder (the longest folder node that reaches it) -- a file
|
|
230
|
+
claim adds an owner beside its folder's, it does not replace it, and a
|
|
231
|
+
sub-folder's own claim still carves that sub-folder out of any outer
|
|
232
|
+
folder. The exact owners come first, then the folder owners, each
|
|
233
|
+
group in its own standing order -- one Resolution per standing
|
|
234
|
+
on-record, always: two records whose chains both resolve to the same
|
|
235
|
+
holder are two owners sharing that holder, never merged into one.
|
|
236
|
+
Each owner's holder is the deepest chain element that is neither in
|
|
237
|
+
`gone` nor past its expiry (the expiry applies to the acting holder
|
|
238
|
+
alone) -- the longest surviving prefix answers, so a departure never
|
|
239
|
+
orphans an area."""
|
|
197
240
|
node = _require_node(node)
|
|
198
241
|
standing = _latest_by_lineage(records)
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
242
|
+
if node.endswith("/"):
|
|
243
|
+
# Folder query: today's rule, unchanged -- the longest covering
|
|
244
|
+
# node wins alone, whatever its depth.
|
|
245
|
+
matches = [rec for rec in standing.values() if _covers(rec.node, node)]
|
|
246
|
+
if not matches:
|
|
247
|
+
return ()
|
|
248
|
+
winning = max(len(rec.node) for rec in matches)
|
|
249
|
+
return tuple(_resolve_one(rec, now=now, gone=gone)
|
|
250
|
+
for rec in matches
|
|
251
|
+
if len(rec.node) == winning and rec.state == "on")
|
|
252
|
+
# File query: every record standing at the exact node...
|
|
253
|
+
exact = [rec for rec in standing.values() if rec.node == node]
|
|
254
|
+
# ...plus every folder node that covers this file, ON OR OFF -- an off
|
|
255
|
+
# record must still be counted here, or a nearer folder that is off
|
|
256
|
+
# would stop masking the outer folder and an outer on-record would
|
|
257
|
+
# wrongly reach past the nearer one
|
|
258
|
+
folders = [rec for rec in standing.values()
|
|
259
|
+
if rec.node.endswith("/") and _covers(rec.node, node)]
|
|
260
|
+
# the exact owners resolve first, in their own standing order, on-only
|
|
261
|
+
out = [_resolve_one(rec, now=now, gone=gone)
|
|
262
|
+
for rec in exact if rec.state == "on"]
|
|
263
|
+
if folders:
|
|
264
|
+
# only the longest (nearest) covering folder node is consulted --
|
|
265
|
+
# an outer folder never reaches past a nearer one
|
|
266
|
+
nearest = max(len(rec.node) for rec in folders)
|
|
267
|
+
out.extend(_resolve_one(rec, now=now, gone=gone)
|
|
268
|
+
for rec in folders
|
|
269
|
+
if len(rec.node) == nearest and rec.state == "on")
|
|
218
270
|
return tuple(out)
|
|
219
271
|
|
|
220
272
|
|
|
@@ -2560,10 +2560,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2560
2560
|
"read by construction, and a derived default would trade "
|
|
2561
2561
|
"that guarantee for a convention")
|
|
2562
2562
|
|
|
2563
|
-
ow = tsub.add_parser("owner", help="
|
|
2564
|
-
"
|
|
2565
|
-
"
|
|
2566
|
-
"
|
|
2563
|
+
ow = tsub.add_parser("owner", help="who answers for this node: every "
|
|
2564
|
+
"owner, the file's own claim first, then its "
|
|
2565
|
+
"folder's, each through its chain -- read from "
|
|
2566
|
+
"the repo's own responsibility record; an unowned "
|
|
2567
|
+
"node is an answer, never an error")
|
|
2567
2568
|
ow.add_argument("node", help="the node whose owner to name (a file or "
|
|
2568
2569
|
"folder path as the map writes it)")
|
|
2569
2570
|
ow.add_argument("--repo", default=".",
|
|
@@ -16,7 +16,16 @@ Absence is not an alarm: a repository with no record yields an empty
|
|
|
16
16
|
overlay and the map renders exactly as it always did. A torn record
|
|
17
17
|
yields only a damaged marker under "__damaged__", never a crash and never
|
|
18
18
|
rows -- a half-read record could mis-assign, so a damaged file assigns
|
|
19
|
-
nothing and says why.
|
|
19
|
+
nothing and says why.
|
|
20
|
+
|
|
21
|
+
A node ending with "/" is a folder node; any other node is a file node.
|
|
22
|
+
A file's own exact record does not replace the nearest covering folder's
|
|
23
|
+
record -- it ADDS to it: the unit's rows carry its own on-records first,
|
|
24
|
+
then the nearest covering folder's on-records. A sub-folder's own claim
|
|
25
|
+
is itself a folder node, so it carves its subtree out of any outer
|
|
26
|
+
folder's reach; turning a file's exact chain off closes only that
|
|
27
|
+
chain's row, leaving the folder's rows untouched. A folder unit query is
|
|
28
|
+
unaffected: the longest matching folder prefix still wins alone."""
|
|
20
29
|
|
|
21
30
|
from __future__ import annotations
|
|
22
31
|
|
|
@@ -76,6 +85,39 @@ def _covers(rec_node: str, unit: str) -> bool:
|
|
|
76
85
|
and unit.startswith(rec_node))
|
|
77
86
|
|
|
78
87
|
|
|
88
|
+
def _row_for(rec, now):
|
|
89
|
+
"""One standing on-record turned into its overlay row: holder is the
|
|
90
|
+
deepest chain element still live against `now`, expiry applying only
|
|
91
|
+
to the acting (last) element."""
|
|
92
|
+
# the record's chain arrives as a plain JSON list; make it a tuple so
|
|
93
|
+
# every row's chain is the same immutable shape the record library uses
|
|
94
|
+
chain = tuple(rec["chain"])
|
|
95
|
+
expires = rec.get("expires")
|
|
96
|
+
# Walk root-first; the expiry applies to the acting holder alone, so a
|
|
97
|
+
# passed expiry ends the walk before the last element and the answer
|
|
98
|
+
# rolls up the surviving prefix.
|
|
99
|
+
live = []
|
|
100
|
+
for position, holder in enumerate(chain):
|
|
101
|
+
# only the chain's last element is the acting holder -- an expiry
|
|
102
|
+
# never applies to a delegator further up the chain
|
|
103
|
+
acting = position == len(chain) - 1
|
|
104
|
+
if acting and expires is not None \
|
|
105
|
+
and now >= _parse_ts(expires):
|
|
106
|
+
break
|
|
107
|
+
live.append(holder)
|
|
108
|
+
# the row this record answers with: the deepest surviving holder (None
|
|
109
|
+
# when the whole walk broke before adding one), the full chain, the
|
|
110
|
+
# reason, the node that covered the query, and the guest-term fields
|
|
111
|
+
return {
|
|
112
|
+
"holder": live[-1] if live else None,
|
|
113
|
+
"chain": chain,
|
|
114
|
+
"why": rec.get("why", ""),
|
|
115
|
+
"via": rec["node"],
|
|
116
|
+
"guest": expires is not None,
|
|
117
|
+
"expires": expires,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
79
121
|
def load_ownership(repo_root, unit_ids, *, now) -> dict:
|
|
80
122
|
"""{unit_id: (owner_row, ...)} for every unit the record covers; units
|
|
81
123
|
the record does not reach are absent, never present-and-empty.
|
|
@@ -84,8 +126,15 @@ def load_ownership(repo_root, unit_ids, *, now) -> dict:
|
|
|
84
126
|
element is gone), chain, why, via (the record node that covered the
|
|
85
127
|
unit -- a folder when ownership is inherited), guest (an expiry is
|
|
86
128
|
set) and expires. The reduction mirrors the record library: latest
|
|
87
|
-
line wins per (node, exact chain)
|
|
88
|
-
|
|
129
|
+
line wins per (node, exact chain). A folder unit (ends with "/") keeps
|
|
130
|
+
the plain longest-prefix rule. A file unit answers with its own
|
|
131
|
+
exact on-records first, then the on-records standing at the nearest
|
|
132
|
+
covering folder (the longest folder node reaching it) -- a file claim
|
|
133
|
+
adds a row beside its folder's, it does not replace it, and a
|
|
134
|
+
sub-folder's own claim carves its subtree out of any outer folder --
|
|
135
|
+
one row per standing on-record, always: two records whose chains both
|
|
136
|
+
resolve to the same holder are two owners sharing that holder, never
|
|
137
|
+
merged into one. Two chains both on are two owners; an expiry on the
|
|
89
138
|
acting holder rolls the answer up the surviving prefix against the
|
|
90
139
|
caller's `now` -- this function reads no clock of its own."""
|
|
91
140
|
path = Path(repo_root) / RESPONSIBILITY_RELPATH
|
|
@@ -101,40 +150,38 @@ def load_ownership(repo_root, unit_ids, *, now) -> dict:
|
|
|
101
150
|
standing[(rec["node"], tuple(rec["chain"]))] = rec
|
|
102
151
|
overlay = {}
|
|
103
152
|
for unit in unit_ids:
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if not matches:
|
|
111
|
-
continue
|
|
112
|
-
# The longest matching node string owns the unit.
|
|
113
|
-
winning = max(len(rec["node"]) for rec in matches)
|
|
114
|
-
rows = []
|
|
115
|
-
for rec in matches:
|
|
116
|
-
if len(rec["node"]) != winning or rec["state"] != "on":
|
|
153
|
+
if unit.endswith("/"):
|
|
154
|
+
# Folder query: today's rule, unchanged -- the longest
|
|
155
|
+
# covering node wins alone, whatever its depth.
|
|
156
|
+
matches = [rec for rec in standing.values()
|
|
157
|
+
if _covers(rec["node"], unit)]
|
|
158
|
+
if not matches:
|
|
117
159
|
continue
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
#
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
160
|
+
winning = max(len(rec["node"]) for rec in matches)
|
|
161
|
+
rows = [_row_for(rec, now) for rec in matches
|
|
162
|
+
if len(rec["node"]) == winning and rec["state"] == "on"]
|
|
163
|
+
else:
|
|
164
|
+
# File query: every record standing at the exact node...
|
|
165
|
+
exact = [rec for rec in standing.values() if rec["node"] == unit]
|
|
166
|
+
# ...plus every folder node that covers this file, ON OR OFF --
|
|
167
|
+
# an off record must still be counted here, or a nearer folder
|
|
168
|
+
# that is off would stop masking the outer folder and an outer
|
|
169
|
+
# on-record would wrongly reach past the nearer one
|
|
170
|
+
folders = [rec for rec in standing.values()
|
|
171
|
+
if rec["node"].endswith("/")
|
|
172
|
+
and _covers(rec["node"], unit)]
|
|
173
|
+
# the exact rows resolve first, in their own standing order,
|
|
174
|
+
# on-only
|
|
175
|
+
rows = [_row_for(rec, now) for rec in exact
|
|
176
|
+
if rec["state"] == "on"]
|
|
177
|
+
if folders:
|
|
178
|
+
# only the longest (nearest) covering folder node is
|
|
179
|
+
# consulted -- an outer folder never reaches past a nearer
|
|
180
|
+
# one
|
|
181
|
+
nearest = max(len(rec["node"]) for rec in folders)
|
|
182
|
+
rows.extend(_row_for(rec, now) for rec in folders
|
|
183
|
+
if len(rec["node"]) == nearest
|
|
184
|
+
and rec["state"] == "on")
|
|
138
185
|
if rows:
|
|
139
186
|
overlay[unit] = tuple(rows)
|
|
140
187
|
return overlay
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ambitgraph"
|
|
7
|
-
version = "0.8.
|
|
7
|
+
version = "0.8.3"
|
|
8
8
|
description = "Read-only codebase scan: reusable units, families, roles, ownership candidates, duplication."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -67,3 +67,8 @@ ambit_map = ["ambit-map-enrich.md", "ambit-mapllm-contract.md"]
|
|
|
67
67
|
[tool.pytest.ini_options]
|
|
68
68
|
testpaths = ["tests"]
|
|
69
69
|
pythonpath = ["."]
|
|
70
|
+
# an unregistered mark refuses instead of silently selecting nothing
|
|
71
|
+
addopts = "--strict-markers"
|
|
72
|
+
markers = [
|
|
73
|
+
"timing: asserts on a measured duration, a deadline wait, or a product timing constant; runs in the timing leg under a machine window",
|
|
74
|
+
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|