ambitgraph 0.4.0__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. {ambitgraph-0.4.0/ambitgraph.egg-info → ambitgraph-0.5.0}/PKG-INFO +1 -1
  2. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambit_map/export.py +6 -0
  3. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambit_map/graph.py +73 -0
  4. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambit_map/langmirror.py +92 -3
  5. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambit_map/page.py +62 -1
  6. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambit_map/registry.py +101 -0
  7. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambit_map/serve.py +31 -1
  8. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambit_map/theme.py +4 -0
  9. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambit_map/unitpage.py +21 -5
  10. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/cli.py +15 -12
  11. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/codelib.py +60 -2
  12. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/edges.py +547 -2
  13. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/langs.py +50 -15
  14. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/portal.py +42 -2
  15. ambitgraph-0.5.0/ambitgraph/prompts.py +88 -0
  16. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/shell.py +145 -28
  17. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/symbols.py +729 -2
  18. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/urcode.py +11 -20
  19. {ambitgraph-0.4.0 → ambitgraph-0.5.0/ambitgraph.egg-info}/PKG-INFO +1 -1
  20. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph.egg-info/SOURCES.txt +1 -0
  21. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/pyproject.toml +1 -1
  22. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/LICENSE +0 -0
  23. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/MANIFEST.in +0 -0
  24. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/README.md +0 -0
  25. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambit_map/__init__.py +0 -0
  26. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambit_map/__main__.py +0 -0
  27. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambit_map/ambit-map-enrich.md +0 -0
  28. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambit_map/ambit-mapllm-contract.md +0 -0
  29. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambit_map/cli.py +0 -0
  30. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambit_map/daemon.py +0 -0
  31. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambit_map/dialects/__init__.py +0 -0
  32. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambit_map/dialects/contract.py +0 -0
  33. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambit_map/dialects/express_pack.py +0 -0
  34. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambit_map/dialects/fastapi_pack.py +0 -0
  35. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambit_map/dialects/flask_pack.py +0 -0
  36. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambit_map/dialects/nextjs_pack.py +0 -0
  37. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambit_map/dialects/supabase_pack.py +0 -0
  38. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambit_map/families.py +0 -0
  39. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambit_map/island_layout.py +0 -0
  40. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambit_map/islands.py +0 -0
  41. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambit_map/judged.py +0 -0
  42. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambit_map/kitdemo.py +0 -0
  43. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambit_map/library.py +0 -0
  44. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambit_map/load.py +0 -0
  45. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambit_map/mapllm.py +0 -0
  46. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambit_map/matched.py +0 -0
  47. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambit_map/model.py +0 -0
  48. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambit_map/objects.py +0 -0
  49. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambit_map/orchestrate.py +0 -0
  50. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambit_map/paths.py +0 -0
  51. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/WALKTHROUGH.md +0 -0
  52. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/__init__.py +0 -0
  53. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/__main__.py +0 -0
  54. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/cockpit.py +0 -0
  55. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/config.py +0 -0
  56. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/console.py +0 -0
  57. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/demo.py +0 -0
  58. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/docs_discovery.py +0 -0
  59. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/doctor.py +0 -0
  60. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/duplication.py +0 -0
  61. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/emit.py +0 -0
  62. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/engine_identity.py +0 -0
  63. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/estimate.py +0 -0
  64. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/eval.py +0 -0
  65. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/feeds.py +0 -0
  66. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/gate.py +0 -0
  67. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/gate_check.py +0 -0
  68. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/gate_server.py +0 -0
  69. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/harvest.py +0 -0
  70. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/inventory.py +0 -0
  71. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/jscalls.py +0 -0
  72. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/mds.py +0 -0
  73. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/merge.py +0 -0
  74. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/merge_jsp.py +0 -0
  75. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/parameterize.py +0 -0
  76. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/pipeline.py +0 -0
  77. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/pycalls.py +0 -0
  78. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/ratify.py +0 -0
  79. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/report.py +0 -0
  80. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/reports.py +0 -0
  81. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/sample.py +0 -0
  82. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/secretary.py +0 -0
  83. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/stages.py +0 -0
  84. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/tables.py +0 -0
  85. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph/tasks.py +0 -0
  86. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph.egg-info/dependency_links.txt +0 -0
  87. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph.egg-info/entry_points.txt +0 -0
  88. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph.egg-info/requires.txt +0 -0
  89. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/ambitgraph.egg-info/top_level.txt +0 -0
  90. {ambitgraph-0.4.0 → ambitgraph-0.5.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ambitgraph
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Read-only codebase scan: reusable units, families, roles, ownership candidates, duplication.
5
5
  Author: AmbitGraph
6
6
  License: Proprietary
@@ -23,6 +23,10 @@ GRAPH_EXPORT_CAP = 200 # the brief's cap
23
23
  EXPORT_INDEX = "index.html"
24
24
  EXPORT_MAP = reg.EXPORT_MAP_REL
25
25
  EXPORT_LIBRARY = "library.html"
26
+ # The Whole map tab. NOT map.html -- that is the default GRAPH seed page
27
+ # and every existing link plus the manifest resolve to it; renaming it to
28
+ # free the nicer name would break links to fix an aesthetic.
29
+ EXPORT_OVERVIEW = reg.EXPORT_OVERVIEW_REL
26
30
  MANIFEST_NAME = "ambitgraph-map.export.json" # U4 review fix (blocker): the
27
31
  # scoped-prune manifest -- lists every relative path THIS export wrote, so
28
32
  # the NEXT export deletes only what it itself owns, never a hand-placed file.
@@ -139,6 +143,8 @@ def page_rows(model, dialects):
139
143
  # existing link and the manifest resolve to it. A real row now, not a copy.
140
144
  rows.append(Row(EXPORT_MAP, "/graph", index_query, 0))
141
145
 
146
+ rows.append(Row(EXPORT_OVERVIEW, "/overview", "", 0))
147
+
142
148
  routes_by_uid = lib._routes_by_uid(dialects)
143
149
  units_iter = model.units.values() if hasattr(model, "units") else ()
144
150
  order = fam.families_in_use(units_iter, routes_by_uid)
@@ -1972,3 +1972,76 @@ GRAPH_JS = '''
1972
1972
  box.scrollTop = (H - box.clientHeight) / 2;
1973
1973
  })();
1974
1974
  '''
1975
+
1976
+
1977
+ # ---------------------------------------------------------------------------
1978
+ # 3.16 THE ISLANDS RENDERER (islands-render wave, 2026-09-05) -- the Whole
1979
+ # map tab's drawing half. COMPOSES AND DRAWS ONLY: island_layout.py already
1980
+ # decided every position (its own C7); this function's one piece of
1981
+ # arithmetic is pack_islands's own documented contract -- every island's
1982
+ # placement is normalized into the [0,1] whole-map unit square, so
1983
+ # composing means multiplying by a fixed pixel side, never recomputing a
1984
+ # bounding box or re-deriving a layout.
1985
+ #
1986
+ # REUSES the Graph tab's pan/zoom mechanism byte-for-byte: GRAPH_JS (drag
1987
+ # to pan, the three zoom buttons, SVG width/height-attribute zoom -- never
1988
+ # viewBox alone, the exact trap a design-panel review caught before this
1989
+ # was written) and the SAME .cm-canvas/.cm-canvas-wrap/.cm-zoom-ctrl
1990
+ # classes/button ids, so the script needs no adaptation. An island node
1991
+ # carries no edges (islands.py's own C5), so GRAPH_JS's edge-click/.cm-desc
1992
+ # half simply never fires here -- it is not wired to anything, and nothing
1993
+ # breaks by its absence (every DOM lookup in GRAPH_JS null-checks first).
1994
+ # ---------------------------------------------------------------------------
1995
+
1996
+
1997
+ def render_islands_canvas(layout):
1998
+ """The `<div class="cm-canvas-wrap">...` markup for a DRAWABLE
1999
+ product-layout.json payload (the caller decides drawability -- this
2000
+ function assumes `layout["islands"]` is non-empty and never checks)."""
2001
+ side = reg.ISLANDS_CANVAS_SIDE
2002
+ pad = reg.ISLANDS_CANVAS_PADDING
2003
+ w = side + 2 * pad
2004
+ h = w
2005
+
2006
+ boundary_parts = []
2007
+ node_parts = []
2008
+ for isl in layout.get("islands", ()):
2009
+ bx = isl["x"] * side + pad
2010
+ by = isl["y"] * side + pad
2011
+ bs = isl["scale"] * side
2012
+ # A subtle region under the nodes, never a hard border or a label
2013
+ # (design-panel finding: pack_islands shelves islands with zero
2014
+ # gap, so shared placement alone can read as one cluster when it
2015
+ # is two touching ones).
2016
+ boundary_parts.append(
2017
+ '<rect class="isl-island" x="{x}" y="{y}" width="{s}" height="{s}" '
2018
+ 'rx="6"></rect>'.format(x=_n(bx), y=_n(by), s=_n(bs)))
2019
+ for node in isl.get("nodes", ()):
2020
+ gx = bx + node["x"] * bs
2021
+ gy = by + node["y"] * bs
2022
+ degree = node.get("d", 0)
2023
+ r = min(reg.ISL_NODE_RADIUS_MAX,
2024
+ reg.ISL_NODE_RADIUS_MIN + degree * reg.ISL_NODE_RADIUS_PER_DEGREE)
2025
+ node_parts.append(
2026
+ '<a href="{href}"><circle class="isl-node" cx="{cx}" cy="{cy}" '
2027
+ 'r="{r}"></circle></a>'.format(
2028
+ href=reg.href_unit(node["u"]), cx=_n(gx), cy=_n(gy), r=_n(r)))
2029
+
2030
+ svg = (
2031
+ '<svg class="isl-svg" viewBox="0 0 {w} {h}" width="{w}" height="{h}" '
2032
+ 'data-zoom="1" role="img" aria-label="{aria}" data-amg-object="islands">'
2033
+ '<g class="isl-islands">{boundaries}</g>'
2034
+ '<g class="isl-nodes">{nodes}</g></svg>'
2035
+ ).format(w=_n(w), h=_n(h), aria=reg.esc(reg.OVERVIEW_CANVAS_LABEL),
2036
+ boundaries="".join(boundary_parts), nodes="".join(node_parts))
2037
+
2038
+ return (
2039
+ '<div class="cm-canvas-wrap"><div class="cm-zoom-ctrl">'
2040
+ '<button type="button" id="cmZoomOut" title="{zo}" aria-label="{zo}">{zog}</button>'
2041
+ '<button type="button" id="cmZoomReset" title="{zr}" aria-label="{zr}">{zrg}</button>'
2042
+ '<button type="button" id="cmZoomIn" title="{zi}" aria-label="{zi}">{zig}</button>'
2043
+ '</div><div class="cm-canvas" data-canvas="1">{svg}</div></div>'
2044
+ ).format(zo=reg.esc(reg.GRAPH_ZOOM_OUT_LABEL), zog=reg.esc(reg.GRAPH_ZOOM_OUT_GLYPH),
2045
+ zr=reg.esc(reg.GRAPH_ZOOM_RESET_LABEL), zrg=reg.esc(reg.GRAPH_ZOOM_RESET_GLYPH),
2046
+ zi=reg.esc(reg.GRAPH_ZOOM_IN_LABEL), zig=reg.esc(reg.GRAPH_ZOOM_IN_GLYPH),
2047
+ svg=svg)
@@ -118,6 +118,15 @@ JAVA_LANGS = ("java",)
118
118
  # count gate.
119
119
  C_LANGS = ("c",)
120
120
 
121
+ # RB_LANGS (Ruby wave 1): the identical one-tag grouping shape as
122
+ # PY_LANGS/JAVA_LANGS/C_LANGS -- "rb" is the only tag ambitgraph/langs.py's
123
+ # "rb" LangSpec ever declares (naming_exts {".rb"}), so this carries no
124
+ # reader-tolerance member either. Read by kind_key's include branch, which
125
+ # maps a resolved `require_relative` onto the include/static row (see
126
+ # registry.py's own comment there), and by the unit story's type-kind
127
+ # count gate.
128
+ RB_LANGS = ("rb",)
129
+
121
130
  # TYPE_SYMBOL_KINDS (Java W1 repair -- the wall-legal PRODUCT-side
122
131
  # mirror of ambitgraph/codelib.py's own _TYPE_SYMBOL_KINDS): the literal
123
132
  # kind strings symbols.py's own class alternation emits for a Java type
@@ -149,6 +158,26 @@ TYPE_SYMBOL_KINDS = ("class", "interface", "enum", "record")
149
158
  # never a cross-wall import.
150
159
  C_TYPE_SYMBOL_KINDS = ("struct", "union", "enum", "typedef")
151
160
 
161
+ # RB_TYPE_SYMBOL_KINDS (Ruby wave 1 -- the wall-legal PRODUCT-side mirror
162
+ # of ambitgraph/codelib.py's own _RB_TYPE_SYMBOL_KINDS): the literal kind
163
+ # strings extract_ruby_symbols emits for a Ruby scope-opener declaration --
164
+ # "class" and "module", the two keywords _RB_SCOPE_LINE_RX matches. Unlike
165
+ # TYPE_SYMBOL_KINDS/C_TYPE_SYMBOL_KINDS above, this set already includes
166
+ # "class" itself (Ruby genuinely has classes), so it feeds BOTH halves of
167
+ # codelib.py's carve identically -- the same shape TYPE_SYMBOL_KINDS takes
168
+ # for Java, never C's shape (which unions "class" in separately because C
169
+ # never emits that literal kind at all). Consumed by
170
+ # ambit_map/unitpage.py's class-count bucket under the RB_LANGS gate
171
+ # above, WIDENING THE COUNT ONLY -- the word stays reg.UNIT_STORY_CLASSES
172
+ # for Ruby (unitpage.py's own class_word ternary is not gated on RB_LANGS
173
+ # at all): Ruby genuinely has classes, so mirroring C's word-swap here
174
+ # would render "N type(s)" about a language whose classes are exactly what
175
+ # the count is naming. Diffed against ambitgraph.codelib._RB_TYPE_SYMBOL_KINDS
176
+ # by tests/test_langs_mirror_diff.py (DIFFED_FACT_NAMES entry
177
+ # "rb_type_symbol_kinds") -- one origin (symbols.py's own kind literals),
178
+ # two independently-declared copies, never a cross-wall import.
179
+ RB_TYPE_SYMBOL_KINDS = ("class", "module")
180
+
152
181
  # NAMESPACE_LANGS (Java W1 repair -- the wall-legal PRODUCT-side mirror of
153
182
  # ambitgraph/langs.py's own NAMESPACE_LANGS): the `lang` tags whose units
154
183
  # get the java-namespace treatment (the languages whose LangSpec declares
@@ -220,7 +249,13 @@ KIND_READER_LANGS = {
220
249
  # include/static's word for word. A C repo's include reader is
221
250
  # therefore named as having run, where a missing member would report
222
251
  # the include plane unmeasured on every C repo forever.
223
- "include/static": frozenset({"jsp", "c"}),
252
+ # "rb" joins this row the same way (Ruby wave 1): a resolved
253
+ # `require_relative` IS a static include -- a literal path read from
254
+ # the source text, spliced in at include time, never varying at
255
+ # runtime -- the identical claim this row already makes. Without the
256
+ # member a Ruby repo's 236-edge include lane would report the include
257
+ # plane unmeasured despite having just drawn every one of them.
258
+ "include/static": frozenset({"jsp", "c", "rb"}),
224
259
  "include/import.py": frozenset({"py"}),
225
260
  "include/import.js": frozenset({"js"}),
226
261
  # Java W1 (2026-09-02), additive: langs.py's java LangSpec gains
@@ -254,7 +289,10 @@ KIND_READER_LANGS = {
254
289
  # without the member a C repo would be told the calls reader never
255
290
  # heard of C, the exact defect this table's own NAMESPACE_LANGS
256
291
  # comment records for java.
257
- "calls": frozenset({"jsp", "java", "py", "js", "c"}),
292
+ # "rb" joins this row the same way (Ruby wave 1): langs.py's rb
293
+ # LangSpec declares call_lane=True and edges.py builds Ruby its own
294
+ # definer pool, landed independently of this stage.
295
+ "calls": frozenset({"jsp", "java", "py", "js", "c", "rb"}),
258
296
  "reference/script": frozenset({"jsp"}),
259
297
  "route/mount": frozenset({"py", "js"}),
260
298
  "ui/component-use": frozenset({"js"}),
@@ -278,7 +316,7 @@ KIND_READER_LANGS = {
278
316
  # ===========================================================================
279
317
  _GRAMMAR_EXTS = frozenset({
280
318
  ".jsp", ".jspf", ".java", ".js", ".mjs", ".cjs", ".jsx", ".ts", ".tsx",
281
- ".py", ".sh", ".sql", ".c", ".h",
319
+ ".py", ".sh", ".sql", ".c", ".h", ".rb",
282
320
  })
283
321
 
284
322
 
@@ -322,6 +360,9 @@ _GRAMMAR_EXTS = frozenset({
322
360
  # ===========================================================================
323
361
  _JSP_JAVA = frozenset({"jsp", "jspf", "java"})
324
362
  _C = frozenset({"c"})
363
+ # _RB (Ruby wave 1): the identical one-tag alias shape as _C above -- "rb"
364
+ # is the only tag any Ruby-declared sentence ever carries this wave.
365
+ _RB = frozenset({"rb"})
325
366
  BLIND_SENTENCE_LANGS = (
326
367
  ("Java declarations inside JSP scriptlet blocks", _JSP_JAVA),
327
368
  ("A Java class whose modifiers are written on their own line", _JSP_JAVA),
@@ -362,6 +403,54 @@ BLIND_SENTENCE_LANGS = (
362
403
  ("A quoted C include with no candidate anywhere in this scan", _C),
363
404
  ("A quoted C include this scan does hold a file for, but position "
364
405
  "does not license,", _C),
406
+ # Ruby wave 1's own two STATIC, structural gaps (ambitgraph.symbols.
407
+ # BLIND_SENTENCES), anchored on their opening words -- the same
408
+ # shape as the C declaration grammar's own gaps just above.
409
+ ("A Ruby heredoc body, from its opener line", _RB),
410
+ ("A Ruby percent literal (", _RB),
411
+ # Ruby wave 1's seven PER-SCAN symbol-grammar sentences
412
+ # (ambitgraph.symbols.rb_symbol_blind_sentences), anchored on the
413
+ # fixed prefix constants declared beside that function -- retyped
414
+ # here by value, exactly as the C include lane's four prefixes are
415
+ # above.
416
+ ("A method definition line whose name does not fit any Ruby method "
417
+ "name shape is refused outright", _RB),
418
+ ("A method defined directly on one specific runtime object, or a "
419
+ "singleton class opened on an object other than self, is refused",
420
+ _RB),
421
+ ("A private, protected, or module_function marker changes the "
422
+ "default visibility of every method written after it, a stateful "
423
+ "effect this wave discloses rather than simulates", _RB),
424
+ ("An attr_accessor, attr_reader, or attr_writer call defines one or "
425
+ "more methods by name that this wave does not read as symbols",
426
+ _RB),
427
+ ("A define_method, method_missing, class_eval, instance_eval, "
428
+ "alias, or alias_method call can define or rename a method by a "
429
+ "name this wave cannot predict", _RB),
430
+ ("A class, module, or method opener sharing its physical line with "
431
+ "another statement, separated by a semicolon, is read for its own "
432
+ "name only", _RB),
433
+ ("A method definition found indented with no enclosing class, "
434
+ "module, or singleton scope on the stack sits inside a block "
435
+ "construct this wave does not track", _RB),
436
+ # Ruby wave 1's six PER-SCAN edges.py sentences (the include lane's
437
+ # three, ambitgraph.edges.rb_include_blind_sentences, plus the call
438
+ # ceiling's three, ambitgraph.edges.rb_call_blind_sentences),
439
+ # anchored on their own fixed prefix constants, same discipline.
440
+ ("A require naming a plain load-path string, rather than a "
441
+ "relative file, searches the Ruby runtime load path and is never "
442
+ "drawn as an edge", _RB),
443
+ ("A require_relative naming a file this scan does hold somewhere, "
444
+ "but not at the position this call computes to, draws no edge",
445
+ _RB),
446
+ ("A require_relative naming no file anywhere in this scan draws no "
447
+ "edge", _RB),
448
+ ("Ruby calls need neither a receiver nor parentheses, so an EXACT "
449
+ "count of receiverless, parenless call sites is impossible", _RB),
450
+ ("A method whose name is a setter or an operator can never be "
451
+ "reached by either call site shape this lane recognizes", _RB),
452
+ ("A method name defined in more than one place is dropped from the "
453
+ "call resolution pool entirely rather than guessed at", _RB),
365
454
  )
366
455
 
367
456
 
@@ -14,7 +14,13 @@ from . import objects as ob
14
14
  from . import unitpage as up
15
15
 
16
16
  DEFAULT_SCREEN = "library"
17
- RAIL = (("graph", "/graph", "Graph", "ic-node"),
17
+ # Broad to narrow, and the order the ruling itself listed: the whole shape,
18
+ # then one file's neighbourhood, then the inventory. "ic-grid" is REUSED from
19
+ # the kit (four separated blocks for a view of separate regions) -- it was
20
+ # used only by the internal kit-demo page, so no customer surface is made
21
+ # ambiguous, and the sprite's pinned token order is left alone.
22
+ RAIL = (("overview", "/overview", reg.TAB_WHOLE_MAP, "ic-grid"),
23
+ ("graph", "/graph", "Graph", "ic-node"),
18
24
  ("library", "/library", "Library", "ic-layers"))
19
25
 
20
26
 
@@ -103,6 +109,61 @@ def render_page(model, now_iso, screens=None, active=None, q="",
103
109
  return ob.document("Library", inner, "library", RAIL)
104
110
 
105
111
 
112
+ OVERVIEW_ABSENT = "absent" # no artifact was written for this run
113
+ OVERVIEW_STALE = "stale" # an artifact exists but no longer matches the code
114
+ OVERVIEW_EMPTY = "empty" # a current artifact exists but names zero islands
115
+ OVERVIEW_DRAWABLE = "drawable" # a current artifact exists and has islands to draw
116
+
117
+ _OVERVIEW_SENTENCES = {
118
+ OVERVIEW_ABSENT: reg.OVERVIEW_NO_LAYOUT,
119
+ OVERVIEW_STALE: reg.OVERVIEW_LAYOUT_STALE,
120
+ OVERVIEW_EMPTY: reg.OVERVIEW_LAYOUT_EMPTY,
121
+ }
122
+
123
+
124
+ def render_overview_page(model, now_iso, layout=None, dialects=None, state=OVERVIEW_ABSENT):
125
+ """The Whole map tab -- the islands view (founder ruling, 2026-08-29;
126
+ the drawing half, islands-render wave, 2026-09-05).
127
+
128
+ `state` is the caller's own fail-closed classification of THIS run's
129
+ `product-layout.json` (serve.py's dispatch computes it via
130
+ `islands.read_product_layout` + `islands.layout_is_fresh`) -- one of
131
+ the four module-level constants above. `layout` carries the REAL
132
+ payload dict ONLY when `state == OVERVIEW_DRAWABLE`; every other state
133
+ keeps `layout=None`, because there is nothing to compose and this
134
+ function must never receive position data it isn't about to draw.
135
+
136
+ Four honest states, never conflated: no artifact was written
137
+ (ABSENT); one exists but the code has changed since (STALE -- drawn
138
+ NEVER, a picture from a population that no longer matches the code on
139
+ disk would be exactly the guess-with-a-progress-bar this product
140
+ refuses to ship); one is current but connects nothing (EMPTY -- a
141
+ real, checked state, never a blank canvas a reader could mistake for
142
+ a bug); one is current and has islands (DRAWABLE -- the only state
143
+ that composes and draws `graph.render_islands_canvas(layout)`,
144
+ computing no layout of its own, per island_layout.py's own C7).
145
+ """
146
+ if state == OVERVIEW_DRAWABLE and layout is None:
147
+ raise ValueError("state=drawable requires the real layout payload")
148
+ if state != OVERVIEW_DRAWABLE and layout is not None:
149
+ raise ValueError("layout must be None unless state=drawable")
150
+ # cased=False: ob.hero runs title_case() by default, which rendered the
151
+ # founder's ruled phrase as "Whole Map". His words are not ours to
152
+ # re-case -- the same U1-D hazard the hero's own docstring warns about
153
+ # for a reader's search query. The tests passed while the PAGE was
154
+ # wrong, because they pinned the constant and not the render.
155
+ hero = ob.hero([("AmbitGraph", "/library"), (reg.TAB_WHOLE_MAP, None)],
156
+ reg.KICKER_WHOLE_MAP, reg.TAB_WHOLE_MAP, "", cased=False)
157
+ if state == OVERVIEW_DRAWABLE:
158
+ body = gph.render_islands_canvas(layout)
159
+ else:
160
+ body = ob.note(reg.esc(_OVERVIEW_SENTENCES[state]))
161
+ inner = ob.block("overview", reg.OVERVIEW_CANVAS_LABEL, body,
162
+ shown=True, cased=False)
163
+ return ob.document(reg.TAB_WHOLE_MAP, hero + inner, "overview", RAIL,
164
+ extra_js=(gph.GRAPH_JS if state == OVERVIEW_DRAWABLE else ""))
165
+
166
+
106
167
  def render_damaged_page(damaged, now_iso):
107
168
  """One honest document: no active tab (there is nothing to switch to
108
169
  and a lit tab would be a lie), one hero, one block."""
@@ -121,6 +121,11 @@ JAVA_LANGS = langm.JAVA_LANGS
121
121
  # and unitpage.py's class-count bucket reads it as the gate on
122
122
  # C_TYPE_SYMBOL_KINDS below.
123
123
  C_LANGS = langm.C_LANGS
124
+ # The fifth src_lang bucket, same alias-only shape as the four above:
125
+ # kind_key's include branch reads it to route a resolved Ruby
126
+ # require_relative, and unitpage.py's class-count bucket reads it as the
127
+ # gate on RB_TYPE_SYMBOL_KINDS below.
128
+ RB_LANGS = langm.RB_LANGS
124
129
  # Java W1 repair (2026-09-02), additive: the same alias-only
125
130
  # shape, for unitpage.py's own class-count bucket -- see langmirror.py's
126
131
  # own comment on this field for the producer/consumer/diff story.
@@ -130,6 +135,13 @@ TYPE_SYMBOL_KINDS = langm.TYPE_SYMBOL_KINDS
130
135
  # field for the producer/consumer/diff story, and for why "enum" being a
131
136
  # member of both sets is safe (the two language gates are disjoint).
132
137
  C_TYPE_SYMBOL_KINDS = langm.C_TYPE_SYMBOL_KINDS
138
+ # The Ruby half of the same bucket -- the kind strings extract_ruby_symbols
139
+ # emits for a scope-opener declaration ("class"/"module"); see
140
+ # langmirror.py's own comment on this field for the producer/consumer/diff
141
+ # story. Unlike C_TYPE_SYMBOL_KINDS, this set already includes "class"
142
+ # itself, so unitpage.py's count gate widens with it directly while its
143
+ # word stays on the CLASSES branch -- Ruby genuinely has classes.
144
+ RB_TYPE_SYMBOL_KINDS = langm.RB_TYPE_SYMBOL_KINDS
133
145
  # Java W1 repair, additive: the language gate that
134
146
  # TYPE_SYMBOL_KINDS' unitpage consumer needs -- same alias-only shape;
135
147
  # see langmirror.py's own comment on this field.
@@ -530,6 +542,26 @@ def kind_key(plane, kind="", src_lang=""):
530
542
  # union check is what holds the two together.
531
543
  if kind == "include" and src_lang in C_LANGS:
532
544
  return "include/static"
545
+ # Ruby wave 1: the Ruby include lane's own edge kind is
546
+ # "require_relative", never "include" or "static" -- it is
547
+ # the literal string ambitgraph/edges.py stamps onto a RESOLVED
548
+ # require_relative row (a HELD, never-drawn plain `require`
549
+ # never reaches this function at all: load.py shunts an
550
+ # unresolved row into unresolved_refs before an IncludeEdge is
551
+ # ever built, so "require" can never arrive here as a kind).
552
+ # It lands on the include/static row for the identical reason
553
+ # a C `#include` does: the target is a literal path read from
554
+ # the source text, resolved once at include time, never
555
+ # varying at runtime. The src_lang gate is what keeps the
556
+ # reuse honest, exactly as it does for the C arm above --
557
+ # without it, a foreign producer's edge spelled kind
558
+ # "require_relative" would inherit this row by accident.
559
+ # langmirror.KIND_READER_LANGS' include/static row carries
560
+ # "rb" beside "jsp"/"c" for the same reason, and
561
+ # tests/test_langs_mirror_diff.py's include-rows union check
562
+ # is what holds the two together.
563
+ if kind == "require_relative" and src_lang in RB_LANGS:
564
+ return "include/static"
533
565
  if kind == "import":
534
566
  if src_lang in PY_LANGS:
535
567
  return "include/import.py"
@@ -1663,6 +1695,10 @@ _STATIC = contextvars.ContextVar("ambit_map_static_export", default=None)
1663
1695
  # The default graph seed page's exported filename, spelled ONCE here so the
1664
1696
  # rewriter and the writer cannot disagree about it.
1665
1697
  EXPORT_MAP_REL = "map.html"
1698
+ # The Whole map tab's exported filename, spelled ONCE. export.py reads it
1699
+ # from here rather than keeping a second literal -- two spellings of one
1700
+ # filename is how a rewriter and a writer silently disagree.
1701
+ EXPORT_OVERVIEW_REL = "overview.html"
1666
1702
 
1667
1703
  StaticCtx = namedtuple("StaticCtx", "depth graph_slugs foot_html")
1668
1704
 
@@ -1721,6 +1757,13 @@ def static_href(value, ctx):
1721
1757
  return esc(p + "library/" + _famfile(f))
1722
1758
  if path == "/search":
1723
1759
  return esc(p + "library.html")
1760
+ if path == "/overview":
1761
+ # the Whole map tab. static_href is FAIL-CLOSED, so a rail link
1762
+ # with no mapping here does not become one dead link -- it raises
1763
+ # StaticHrefUnknown and REFUSES THE WHOLE EXPORT. That is the
1764
+ # right severity: the rail is on every page, so an unmappable tab
1765
+ # is a broken artifact, not a broken corner of one.
1766
+ return esc(p + EXPORT_OVERVIEW_REL)
1724
1767
  if path == "/unit" and u is not None:
1725
1768
  return esc(p + "unit/" + export_slug("unit", u) + ".html")
1726
1769
  if path == "/table" and t is not None:
@@ -2839,6 +2882,64 @@ GRAMMARLESS_TABLE_SENTENCE = (
2839
2882
  "statement can still appear here."
2840
2883
  )
2841
2884
 
2885
+ # The Whole map tab (founder ruling, 2026-08-29). His ruled phrase, and it
2886
+ # invents no name: it is panel D's own existing headline wording ("Terrarium,
2887
+ # whole map: 186 connected files"), so the no-new-names law is satisfied by
2888
+ # CITATION rather than by argument. Shortening it to "Map" collides with
2889
+ # map.html's meaning and re-opens a question he already closed.
2890
+ TAB_WHOLE_MAP = "Whole map"
2891
+ # Kicker for the Whole map surface, in the register its siblings use
2892
+ # ("Code map", "Code library").
2893
+ KICKER_WHOLE_MAP = "Code map"
2894
+ # The canvas block's own label. Distinct from the page title on purpose:
2895
+ # "Whole map" was rendering three times on one page (crumb, hero, block).
2896
+ # Mirrors graph.py, whose canvas block keeps a stable label and puts the
2897
+ # no-edges note INSIDE it. "Islands" is the product's own word for the
2898
+ # object -- panel D's "6 separate islands".
2899
+ OVERVIEW_CANVAS_LABEL = "Islands"
2900
+ # The artifact-absent state. ABSENCE CLAIMS NOTHING, BUT A CHECK THAT RAN SAYS
2901
+ # IT CHECKED -- the same law W6's stranger clause runs on. The subject stays on
2902
+ # what this RUN did, never on the reader's codebase, and the way onward points
2903
+ # at a surface that actually exists (an offer whose target is unreachable is
2904
+ # worse than no offer).
2905
+ OVERVIEW_NO_LAYOUT = ("No whole-map layout was written for this run, so there is "
2906
+ "nothing to draw here. The Library lists every unit the "
2907
+ "scan read.")
2908
+ # The artifact-STALE state (islands-render wave, 2026-09-05): a run whose
2909
+ # code changed since its layout was computed is a DIFFERENT fact from one
2910
+ # that wrote nothing at all -- OVERVIEW_NO_LAYOUT is false of it too. A
2911
+ # stale artifact is drawn NEVER (a picture computed from a population that
2912
+ # no longer matches the code on disk would be exactly the guess-with-a-
2913
+ # progress-bar this product refuses to ship); this sentence names why the
2914
+ # tab shows nothing without claiming absence.
2915
+ OVERVIEW_LAYOUT_STALE = ("A whole-map layout exists for this run, but the code has "
2916
+ "changed since it was written, so this view will not draw "
2917
+ "a picture that no longer matches it. The Library lists "
2918
+ "every unit the current scan read.")
2919
+ # The artifact-DRAWABLE-BUT-EMPTY state: a fresh, current layout that
2920
+ # names zero islands (every unit isolated with no unit-to-unit records at
2921
+ # all, or none scanned). Still a real, checked state -- never a blank
2922
+ # <svg> a reader could mistake for a bug.
2923
+ OVERVIEW_LAYOUT_EMPTY = ("The whole-map layout for this run is current, but it "
2924
+ "connects nothing -- every unit the scan read stands "
2925
+ "alone. The Library lists them.")
2926
+ # The whole-map canvas is a fixed-size square (island_layout.pack_islands's
2927
+ # own contract: every island's placement is already normalized into the
2928
+ # [0,1] unit square -- composing means multiplying by this side length,
2929
+ # never recomputing a bounding box). Padding keeps edge-island dots off
2930
+ # the viewBox border.
2931
+ ISLANDS_CANVAS_SIDE = 720
2932
+ ISLANDS_CANVAS_PADDING = 24
2933
+ # Node radius floor and ceiling, in pixels: a degree-0 node (every
2934
+ # singleton island's only member) must never shrink toward invisible --
2935
+ # it is still a real, clickable unit page (islands-render panel finding).
2936
+ ISL_NODE_RADIUS_MIN = 3.0
2937
+ ISL_NODE_RADIUS_MAX = 11.0
2938
+ # Linear pixels-per-degree above the floor, capped at the ceiling -- no
2939
+ # global max-degree pass needed (one run's node sizes stay comparable to
2940
+ # another's, and the renderer stays a pure per-node function).
2941
+ ISL_NODE_RADIUS_PER_DEGREE = 0.6
2942
+
2842
2943
  LABEL_SEARCH = "Find a function"
2843
2944
  LABEL_GLANCE = "The library — at a glance"
2844
2945
  LABEL_TABLES = "Busiest tables — the data the code leans on"
@@ -107,6 +107,7 @@ from pathlib import Path
107
107
  from . import registry as reg
108
108
  from . import library as lib
109
109
  from . import graph as gph
110
+ from . import islands
110
111
  from . import page as pg
111
112
  from . import objects as ob
112
113
  from . import unitpage as up
@@ -129,7 +130,7 @@ SERVER_VERSION = "AmbitGraph/1"
129
130
  # copied by value from ambitgraph/console.py:66 -- ambit_map imports no
130
131
  # company code (the ambit_telegraph/store.py precedent)
131
132
  _ALLOWED_HOSTS = frozenset({"127.0.0.1", "localhost", "[::1]", "::1"})
132
- _ROUTES = ("/", "/library", "/graph", "/search", "/unit", "/table",
133
+ _ROUTES = ("/", "/library", "/graph", "/overview", "/search", "/unit", "/table",
133
134
  "/paths", "/table-closure", "/health", "/ping", "/stop")
134
135
  # H4: named here, beside the route it gates, rather than as a literal
135
136
  # buried in _handle_stop -- daemon.py (the launcher, a different module
@@ -637,6 +638,12 @@ def route(path, raw_query):
637
638
  active = "stop"
638
639
  elif path == "/graph":
639
640
  active = "graph"
641
+ elif path == "/overview":
642
+ # the Whole map tab (founder ruling 2026-08-29). NOT "/map":
643
+ # map.html is already the default graph seed page and the export
644
+ # manifest points at it, so the LABEL carries his ruled phrase
645
+ # while the ROUTE stays plumbing that names nothing.
646
+ active = "overview"
640
647
  elif path == "/search":
641
648
  active = "search"
642
649
  elif path == "/unit":
@@ -701,6 +708,29 @@ def render(state, r, now_iso, snapshot=False) -> str:
701
708
  dialects=state.dialects,
702
709
  stale=(bool(state.stale_reason) or state.pending_rebuild),
703
710
  stale_text=(reg.STALE_TAG_ONCE if snapshot else reg.STALE_TAG_SERVE))
711
+ elif r["active"] == "overview":
712
+ # The four states are pg.render_overview_page's own to classify
713
+ # into a sentence or a drawing (islands-render wave, 2026-09-05).
714
+ # This dispatch's job is ONLY to read the three facts that decide
715
+ # which state applies: does an artifact exist, is it fresh
716
+ # against THIS run's own current population, does it name any
717
+ # islands. state.model/state.dialects/state.run_dir are read here
718
+ # the same unsynchronized way every other route already reads
719
+ # them (docs/ROADMAP.md names the file-wide hardening item; this
720
+ # route does not fix it alone).
721
+ data = islands.read_product_layout(state.run_dir)
722
+ if not data:
723
+ ov_state, ov_layout = pg.OVERVIEW_ABSENT, None
724
+ elif not islands.layout_is_fresh(
725
+ data, pth.assemble_table(state.model, state.dialects), state.model.unit_ids):
726
+ ov_state, ov_layout = pg.OVERVIEW_STALE, None
727
+ elif not data.get("islands"):
728
+ ov_state, ov_layout = pg.OVERVIEW_EMPTY, None
729
+ else:
730
+ ov_state, ov_layout = pg.OVERVIEW_DRAWABLE, data
731
+ doc = pg.render_overview_page(
732
+ state.model, now_iso, layout=ov_layout, dialects=state.dialects,
733
+ state=ov_state)
704
734
  elif r["active"] == "search":
705
735
  raw_q = r["q"] if isinstance(r["q"], str) else ""
706
736
  inner = lib.render_search(state.model, r["q"], now_iso, uncap=r["uncap"],
@@ -324,6 +324,10 @@ p.note{color:var(--muted);font-size:.92rem;margin:0;max-width:80ch;}
324
324
  .cm-zoom-ctrl button:hover{background:#e0e7ff;color:#1e40af;}
325
325
  text.cm-name{font:500 13px ui-monospace,monospace;fill:#1e293b;}
326
326
  text.cm-meta{font:11px system-ui,sans-serif;fill:#64748b;}
327
+ .isl-svg{display:block;}
328
+ .isl-island{fill:var(--soft-bg,#f8fafc);stroke:var(--border,#e2e8f0);stroke-width:1;}
329
+ .isl-node{fill:var(--accent,#2c3e50);}
330
+ .isl-svg a:hover .isl-node,.isl-svg a:focus .isl-node{fill:var(--accent-dark,#1a1a2e);}
327
331
  text.cm-elbl{font:500 11px ui-monospace,monospace;paint-order:stroke;stroke:#ffffff;stroke-width:3px;}
328
332
  .cm-desc{margin-top:10px;padding:11px 14px;border:1px solid var(--line);border-radius:10px;background:#f8fafc;color:var(--ink);font-size:.9rem;min-height:1.4em;}
329
333
  .cm-form{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px;}
@@ -845,15 +845,31 @@ def render_unit(model, uid, now_iso, uncap=frozenset(), dialects=None, judged=No
845
845
  # language renders byte-identically to before this branch existed.
846
846
  java_ns = u.lang in reg.NAMESPACE_LANGS
847
847
  c_ns = u.lang in reg.C_LANGS
848
+ # Ruby wave 1: rb_ns widens the COUNT only, never the word below --
849
+ # see that ternary's own comment for why. reg.RB_TYPE_SYMBOL_KINDS
850
+ # already contains "class" itself (Ruby's own type-kind set, unlike
851
+ # C's), so a Ruby unit's "class" symbols are counted twice over by
852
+ # this `or` (once by the bare `s.kind == "class"` clause, once by
853
+ # this one) -- harmless, `sum` over a boolean-per-symbol expression
854
+ # never double-counts the SAME symbol twice, since each symbol
855
+ # contributes at most one `1` regardless of how many of the three
856
+ # clauses it satisfies.
857
+ rb_ns = u.lang in reg.RB_LANGS
848
858
  c = sum(1 for s in u.symbols
849
859
  if s.kind == "class"
850
860
  or (java_ns and s.kind in reg.TYPE_SYMBOL_KINDS)
851
- or (c_ns and s.kind in reg.C_TYPE_SYMBOL_KINDS))
861
+ or (c_ns and s.kind in reg.C_TYPE_SYMBOL_KINDS)
862
+ or (rb_ns and s.kind in reg.RB_TYPE_SYMBOL_KINDS))
852
863
  # ... and the WORD that bucket is counted under is decided by the
853
- # SAME gate. C has no classes at all -- what it declares is
854
- # structs, unions, enums and typedefs -- so a C unit's first
855
- # sentence would otherwise state a construct the language does not
856
- # have, about a file that cannot contain one. Every other language
864
+ # SAME gate -- for C only. C has no classes at all -- what it
865
+ # declares is structs, unions, enums and typedefs -- so a C unit's
866
+ # first sentence would otherwise state a construct the language
867
+ # does not have, about a file that cannot contain one. Ruby is the
868
+ # opposite case: it genuinely has classes (and modules, counted
869
+ # alongside them just above), so mirroring the C gate onto this
870
+ # word too would render "N type(s)" about a language whose classes
871
+ # are exactly what the count just named -- rb_ns is deliberately
872
+ # ABSENT from this ternary. Every other language, Ruby included,
857
873
  # keeps reg.UNIT_STORY_CLASSES, the default, byte for byte.
858
874
  class_word = reg.UNIT_STORY_TYPES if c_ns else reg.UNIT_STORY_CLASSES
859
875
  other = len(u.symbols) - f - c
@@ -1423,6 +1423,13 @@ def build_cockpit_html(runs_dir, repo_root=None, demo: bool = False,
1423
1423
  # run, no scan.json is simply no tab (#15).
1424
1424
  from . import urcode as uc
1425
1425
  urcode_result = uc.read_urcode(portal)
1426
+ prompts_result = None
1427
+ if portal is not None:
1428
+ # The Manager screen's standing prompts: data the coordinating
1429
+ # seat keeps current; read once per build, never raises — no
1430
+ # declared root is simply no block (#15).
1431
+ from . import prompts as pr
1432
+ prompts_result = pr.read_prompts(portal)
1426
1433
 
1427
1434
  html = build_shell(runs, budget, events, library,
1428
1435
  tasks=shell_tasks, demo=demo, depth=depth,
@@ -1433,7 +1440,8 @@ def build_cockpit_html(runs_dir, repo_root=None, demo: bool = False,
1433
1440
  cockpit_event_available=cockpit_event_available,
1434
1441
  secretary=secretary_result,
1435
1442
  reports=reports_result,
1436
- urcode=urcode_result)
1443
+ urcode=urcode_result,
1444
+ prompts=prompts_result)
1437
1445
  return _CockpitBuild(html, runs, events, portal_error=portal_error)
1438
1446
 
1439
1447
 
@@ -2509,17 +2517,12 @@ def build_parser() -> argparse.ArgumentParser:
2509
2517
  mo.add_argument("--messages", required=True)
2510
2518
  mo.add_argument("--by", required=True)
2511
2519
  mo.add_argument("--reason", default="")
2512
- sp = tsub.add_parser("spend", help="record what a seat spent against a "
2513
- "grant (the seat reports its own spend; there is "
2514
- "no --by)")
2515
- sp.add_argument("--messages", required=True)
2516
- sp.add_argument("--grant", dest="grant_id", required=True)
2517
- sp.add_argument("--seat", required=True,
2518
- help="the seat that spent it; the author IS the seat, "
2519
- "so there is no --by — a seat may only report its own "
2520
- "spend, and only against a grant that names it")
2521
- sp.add_argument("--tokens", type=int, required=True)
2522
- sp.add_argument("--turns", type=int, required=True)
2520
+ # The `spend` verb is GONE (Telegraph T16). It was the only writer of
2521
+ # the meter file kind, and it was never run: no `meter-*.jsonl` has
2522
+ # existed on this machine, and every grant that ever lived expired at
2523
+ # 0 tokens and 0 turns. A grant's BUDGET stays — it is the declared
2524
+ # ceiling, and the ceiling verdicts still test against it — but nothing
2525
+ # counts against that ceiling any more, and nothing ever did.
2523
2526
  ex = tsub.add_parser("export", help="write the Household artifact the "
2524
2527
  "Cockpit reads — one versioned JSON file composed "
2525
2528
  "from the real ledger; the Cockpit reads this FILE "