ambitgraph 0.8.2__tar.gz → 0.8.4__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.4}/PKG-INFO +1 -1
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambit_telegraph/responsibility.py +76 -24
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/cli.py +5 -4
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/omniscient_next/__init__.py +7 -1
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/omniscient_next/graph.js +8 -2
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/omniscient_next/library.js +7 -1
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/omniscient_next/search.js +6 -1
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/omniscient_next/siderail.js +16 -5
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/omniscient_next/wholemap.js +13 -2
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/responsibility.py +83 -36
- {ambitgraph-0.8.2 → ambitgraph-0.8.4/ambitgraph.egg-info}/PKG-INFO +1 -1
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/pyproject.toml +6 -1
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/LICENSE +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/MANIFEST.in +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/README.md +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambit_contracts/__init__.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambit_contracts/edge.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambit_contracts/envelope.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambit_contracts/fields.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambit_contracts/grant.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambit_contracts/household.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambit_map/__init__.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambit_map/__main__.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambit_map/ambit-map-enrich.md +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambit_map/ambit-mapllm-contract.md +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambit_map/cli.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambit_map/daemon.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambit_map/dialects/__init__.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambit_map/dialects/contract.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambit_map/dialects/express_pack.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambit_map/dialects/fastapi_pack.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambit_map/dialects/flask_pack.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambit_map/dialects/nextjs_pack.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambit_map/dialects/supabase_pack.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambit_map/explain.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambit_map/families.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambit_map/langmirror.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambit_map/load.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambit_map/mapllm.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambit_map/model.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambit_map/orchestrate.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambit_map/registry.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambit_map/serve.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambit_telegraph/__init__.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambit_telegraph/cli.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambit_telegraph/export.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambit_telegraph/ledger.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambit_telegraph/store.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/WALKTHROUGH.md +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/__init__.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/__main__.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/_delimited.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/cockpit.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/codelib.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/config.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/console.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/demo.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/design_tokens.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/docs_discovery.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/doctor.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/duplication.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/edges.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/emit.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/engine_identity.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/estimate.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/eval.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/feeds.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/gate.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/gate_check.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/gate_server.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/harvest.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/inventory.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/jscalls.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/langs.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/mds.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/merge.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/merge_jsp.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/omniscient_next/_init_js.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/omniscient_next/filterpanel.js +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/omniscient_next/next_embed.css +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/omniscient_next/standalone.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/parameterize.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/pipeline.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/portal.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/prompts.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/pycalls.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/ratify.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/report.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/reports.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/sample.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/secretary.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/shell.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/stages.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/symbols.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/tables.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/tasks.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph/urcode.py +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph.egg-info/SOURCES.txt +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph.egg-info/dependency_links.txt +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph.egg-info/entry_points.txt +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph.egg-info/requires.txt +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/ambitgraph.egg-info/top_level.txt +0 -0
- {ambitgraph-0.8.2 → ambitgraph-0.8.4}/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=".",
|
|
@@ -140,7 +140,13 @@ def render_screen(run_path: str) -> str:
|
|
|
140
140
|
scripts (modules + payload + wiring). The caller (shell.py) owns the
|
|
141
141
|
absence branch; calling this without data is a build error."""
|
|
142
142
|
data = build_data(run_path)
|
|
143
|
-
|
|
143
|
+
# EVERY `<` becomes an escaped unicode point, not just the ones that open a
|
|
144
|
+
# closing tag. Replacing `</` alone defends against `</script>` and against
|
|
145
|
+
# nothing else: a scanned name containing `<!--` opens a comment inside the
|
|
146
|
+
# script block and swallows the payload after it, and `<script` opens a
|
|
147
|
+
# second one. The escape is invisible to JSON.parse, so the data a reader
|
|
148
|
+
# gets is unchanged -- this is the cockpit's own pattern, not a new one.
|
|
149
|
+
payload = json.dumps(data, separators=(",", ":")).replace("<", "\\u003c")
|
|
144
150
|
return (
|
|
145
151
|
'<div class="cp-next-shell">'
|
|
146
152
|
'<div class="sh-top">'
|
|
@@ -153,7 +153,13 @@ class SingleGraph {
|
|
|
153
153
|
destroy(){ this.container.innerHTML=""; }
|
|
154
154
|
|
|
155
155
|
/* ---------------- internals ---------------- */
|
|
156
|
-
|
|
156
|
+
/* All five characters that change how markup is read. This pane writes a
|
|
157
|
+
folder's name into data-expand="...", so an unescaped quote in a directory
|
|
158
|
+
name ends that attribute early and the rest of the name is read as markup.
|
|
159
|
+
The ampersand is replaced FIRST, or a later substitution's own output is
|
|
160
|
+
escaped a second time. */
|
|
161
|
+
_esc(s){ return String(s).replace(/&/g,"&").replace(/</g,"<")
|
|
162
|
+
.replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'"); }
|
|
157
163
|
|
|
158
164
|
_neigh(applyFilter=true){
|
|
159
165
|
// inbound/outbound entries, deduped per file, weights summed
|
|
@@ -214,7 +220,7 @@ class SingleGraph {
|
|
|
214
220
|
'<div class="sg-e-name"><span class="sg-e-dot" style="background:'+
|
|
215
221
|
(FC[n.f]||"#8b96a8")+'"></span>'+esc(n.id.split("/").pop())+'</div>'+
|
|
216
222
|
'<div class="sg-e-path">'+esc(n.id)+'</div>'+
|
|
217
|
-
'<div class="sg-e-meta">'+n.d+' links · '+n.in+' inbound · '+n.f+'/</div></div>'; });
|
|
223
|
+
'<div class="sg-e-meta">'+n.d+' links · '+n.in+' inbound · '+esc(n.f)+'/</div></div>'; });
|
|
218
224
|
h+='</div></div>';
|
|
219
225
|
this._canvas.innerHTML=h;
|
|
220
226
|
}
|
|
@@ -74,7 +74,13 @@ class LibraryView {
|
|
|
74
74
|
if (i >= 0) { this._state = {...this._state, level:"file", file:i}; this._render(); } }
|
|
75
75
|
home() { this._state = {...this._state, level:"home"}; this._render(); }
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
/* All five characters that change how markup is read. Two are enough for
|
|
78
|
+
text between tags and are no defence at all for a value placed inside a
|
|
79
|
+
quoted attribute, where one unescaped quote ends the attribute early and
|
|
80
|
+
everything after it is read as markup. The ampersand is replaced FIRST,
|
|
81
|
+
or a later substitution's own output is escaped a second time. */
|
|
82
|
+
_esc(s){ return String(s).replace(/&/g,"&").replace(/</g,"<")
|
|
83
|
+
.replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'"); }
|
|
78
84
|
_cat(key){ return this._CATS.find(c => c[0] === key); }
|
|
79
85
|
|
|
80
86
|
_click(e) {
|
|
@@ -61,7 +61,12 @@ class SearchDrop {
|
|
|
61
61
|
if (m.syms.length) { return this._data.nodes[m.syms[0][1]].id; }
|
|
62
62
|
return null;
|
|
63
63
|
}
|
|
64
|
-
|
|
64
|
+
/* All five characters that change how markup is read. Two are enough for
|
|
65
|
+
text between tags and are no defence at all for a value placed inside a
|
|
66
|
+
quoted attribute. The ampersand is replaced FIRST, or a later
|
|
67
|
+
substitution's own output is escaped a second time. */
|
|
68
|
+
_esc(s) { return String(s).replace(/&/g, "&").replace(/</g, "<")
|
|
69
|
+
.replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'"); }
|
|
65
70
|
_render() {
|
|
66
71
|
const SEARCH_ROW_CAP = 12;
|
|
67
72
|
const m = this.matches(this._input.value);
|
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
navigation lands (a test pins that retirement). Architecture constraints:
|
|
4
4
|
one view, one class, all CSS class names prefixed to avoid collisions,
|
|
5
5
|
and only the documented public surface is used. */
|
|
6
|
+
/* All five characters that change how markup is read. This module had none:
|
|
7
|
+
its labels and notes are file and folder names from the scanned repository
|
|
8
|
+
and they were written straight into innerHTML, so a name decided markup on
|
|
9
|
+
a page that opens by default. The ampersand is replaced FIRST, or a later
|
|
10
|
+
substitution's own output is escaped a second time.
|
|
11
|
+
The name is module-local because every module is concatenated into one
|
|
12
|
+
script and a bare `esc` would collide; one shared escaper replacing all of
|
|
13
|
+
these is the next release's work, not this patch's. */
|
|
14
|
+
function _srEsc(s){ return String(s).replace(/&/g,"&").replace(/</g,"<")
|
|
15
|
+
.replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'"); }
|
|
16
|
+
|
|
6
17
|
/* ============================================================
|
|
7
18
|
SideRail — the left rail with NESTED quick-nav: entry points
|
|
8
19
|
nested under Graph, categories under Library, each group
|
|
@@ -36,8 +47,8 @@ class SideRail {
|
|
|
36
47
|
b.className = "sr-item"; b.tabIndex = 0; b.setAttribute("role", "button");
|
|
37
48
|
b.innerHTML =
|
|
38
49
|
(it.children ? "<span class='sr-caret'>▸</span>" : "<span class='sr-caret sr-blank'></span>") +
|
|
39
|
-
"<span class='sr-label'>" + it.label + "</span>" +
|
|
40
|
-
(it.note ? "<span class='sr-note'>" + it.note + "</span>" : "");
|
|
50
|
+
"<span class='sr-label'>" + _srEsc(it.label) + "</span>" +
|
|
51
|
+
(it.note ? "<span class='sr-note'>" + _srEsc(it.note) + "</span>" : "");
|
|
41
52
|
const switchTo = () => { if (typeof opts.onSwitch === "function") opts.onSwitch(it.key); };
|
|
42
53
|
b.querySelector(".sr-label").onclick = switchTo;
|
|
43
54
|
if (it.note) b.querySelector(".sr-note").onclick = switchTo;
|
|
@@ -58,9 +69,9 @@ class SideRail {
|
|
|
58
69
|
it.children.forEach(c => {
|
|
59
70
|
const k = document.createElement("div");
|
|
60
71
|
k.className = "sr-kid"; k.tabIndex = 0; k.setAttribute("role", "button");
|
|
61
|
-
k.innerHTML = (c.color ? "<span class='sr-dot' style='background:"+c.color+"'></span>" : "") +
|
|
62
|
-
"<span class='sr-klabel'>" + c.label + "</span>" +
|
|
63
|
-
(c.note ? "<span class='sr-note'>" + c.note + "</span>" : "");
|
|
72
|
+
k.innerHTML = (c.color ? "<span class='sr-dot' style='background:"+_srEsc(c.color)+"'></span>" : "") +
|
|
73
|
+
"<span class='sr-klabel'>" + _srEsc(c.label) + "</span>" +
|
|
74
|
+
(c.note ? "<span class='sr-note'>" + _srEsc(c.note) + "</span>" : "");
|
|
64
75
|
k.onclick = () => c.onClick && c.onClick();
|
|
65
76
|
k.addEventListener("keydown", e => { if (e.key === "Enter") k.onclick(); });
|
|
66
77
|
kids.appendChild(k);
|
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
implementation and is expected to be retired once the map product view
|
|
3
3
|
ships (a test pins this until then). Architecture constraints: one view,
|
|
4
4
|
one class, CSS prefixed to this module, public surface only. */
|
|
5
|
+
/* All five characters that change how markup is read. This module had none:
|
|
6
|
+
the folder legend wrote a scanned folder's name straight into innerHTML, on
|
|
7
|
+
the pane the page opens on, so the name decided markup with no click at all.
|
|
8
|
+
The ampersand is replaced FIRST, or a later substitution's own output is
|
|
9
|
+
escaped a second time.
|
|
10
|
+
The name is module-local because every module is concatenated into one
|
|
11
|
+
script and a bare `esc` would collide; one shared escaper replacing all of
|
|
12
|
+
these is the next release's work, not this patch's. */
|
|
13
|
+
function _wmEsc(s){ return String(s).replace(/&/g,"&").replace(/</g,"<")
|
|
14
|
+
.replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'"); }
|
|
15
|
+
|
|
5
16
|
/* ============================================================
|
|
6
17
|
WholeMap — the whole-graph view, as its own module.
|
|
7
18
|
|
|
@@ -471,8 +482,8 @@ class WholeMap {
|
|
|
471
482
|
/* chips — computed from THIS repo's data, never assumed */
|
|
472
483
|
data.folders.forEach(f=>{ const [name,count]=f;
|
|
473
484
|
const el=document.createElement("span"); el.className="wm-chip"; el.tabIndex=0;
|
|
474
|
-
el.innerHTML="<span class='wm-sw' style='background:"+folderColor[name]+"'></span>"
|
|
475
|
-
+name+" "+count;
|
|
485
|
+
el.innerHTML="<span class='wm-sw' style='background:"+_wmEsc(folderColor[name])+"'></span>"
|
|
486
|
+
+_wmEsc(name)+" "+count;
|
|
476
487
|
el.onclick=()=>{ traced = traced===name? null : name;
|
|
477
488
|
chipbar.querySelectorAll(".wm-chip").forEach(c=>c.classList.remove("wm-on"));
|
|
478
489
|
if(traced) el.classList.add("wm-on");
|
|
@@ -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.4"
|
|
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
|