ambitgraph 0.8.1__tar.gz → 0.8.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/MANIFEST.in +5 -0
  2. {ambitgraph-0.8.1/ambitgraph.egg-info → ambitgraph-0.8.2}/PKG-INFO +1 -1
  3. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambit_map/daemon.py +16 -4
  4. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambit_map/dialects/contract.py +11 -16
  5. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambit_map/families.py +142 -15
  6. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambit_map/langmirror.py +26 -49
  7. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambit_map/load.py +1 -3
  8. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambit_map/model.py +13 -15
  9. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambit_map/orchestrate.py +0 -139
  10. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambit_map/registry.py +92 -1093
  11. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambit_map/serve.py +104 -630
  12. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambit_telegraph/cli.py +29 -1
  13. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/cli.py +23 -0
  14. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/edges.py +79 -74
  15. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/jscalls.py +14 -3
  16. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/langs.py +12 -4
  17. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/pipeline.py +28 -3
  18. ambitgraph-0.8.2/ambitgraph/responsibility.py +140 -0
  19. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/symbols.py +316 -6
  20. {ambitgraph-0.8.1 → ambitgraph-0.8.2/ambitgraph.egg-info}/PKG-INFO +1 -1
  21. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph.egg-info/SOURCES.txt +1 -13
  22. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/pyproject.toml +1 -1
  23. ambitgraph-0.8.1/ambit_map/export.py +0 -656
  24. ambitgraph-0.8.1/ambit_map/graph.py +0 -3091
  25. ambitgraph-0.8.1/ambit_map/island_layout.py +0 -462
  26. ambitgraph-0.8.1/ambit_map/islands.py +0 -536
  27. ambitgraph-0.8.1/ambit_map/judged.py +0 -279
  28. ambitgraph-0.8.1/ambit_map/kitdemo.py +0 -274
  29. ambitgraph-0.8.1/ambit_map/library.py +0 -2038
  30. ambitgraph-0.8.1/ambit_map/matched.py +0 -705
  31. ambitgraph-0.8.1/ambit_map/objects.py +0 -1046
  32. ambitgraph-0.8.1/ambit_map/page.py +0 -213
  33. ambitgraph-0.8.1/ambit_map/paths.py +0 -1434
  34. ambitgraph-0.8.1/ambit_map/theme.py +0 -568
  35. ambitgraph-0.8.1/ambit_map/unitpage.py +0 -1086
  36. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/LICENSE +0 -0
  37. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/README.md +0 -0
  38. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambit_contracts/__init__.py +0 -0
  39. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambit_contracts/edge.py +0 -0
  40. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambit_contracts/envelope.py +0 -0
  41. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambit_contracts/fields.py +0 -0
  42. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambit_contracts/grant.py +0 -0
  43. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambit_contracts/household.py +0 -0
  44. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambit_map/__init__.py +0 -0
  45. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambit_map/__main__.py +0 -0
  46. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambit_map/ambit-map-enrich.md +0 -0
  47. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambit_map/ambit-mapllm-contract.md +0 -0
  48. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambit_map/cli.py +0 -0
  49. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambit_map/dialects/__init__.py +0 -0
  50. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambit_map/dialects/express_pack.py +0 -0
  51. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambit_map/dialects/fastapi_pack.py +0 -0
  52. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambit_map/dialects/flask_pack.py +0 -0
  53. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambit_map/dialects/nextjs_pack.py +0 -0
  54. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambit_map/dialects/supabase_pack.py +0 -0
  55. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambit_map/explain.py +0 -0
  56. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambit_map/mapllm.py +0 -0
  57. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambit_telegraph/__init__.py +0 -0
  58. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambit_telegraph/export.py +0 -0
  59. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambit_telegraph/ledger.py +0 -0
  60. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambit_telegraph/responsibility.py +0 -0
  61. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambit_telegraph/store.py +0 -0
  62. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/WALKTHROUGH.md +0 -0
  63. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/__init__.py +0 -0
  64. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/__main__.py +0 -0
  65. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/_delimited.py +0 -0
  66. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/cockpit.py +0 -0
  67. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/codelib.py +0 -0
  68. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/config.py +0 -0
  69. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/console.py +0 -0
  70. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/demo.py +0 -0
  71. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/design_tokens.py +0 -0
  72. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/docs_discovery.py +0 -0
  73. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/doctor.py +0 -0
  74. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/duplication.py +0 -0
  75. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/emit.py +0 -0
  76. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/engine_identity.py +0 -0
  77. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/estimate.py +0 -0
  78. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/eval.py +0 -0
  79. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/feeds.py +0 -0
  80. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/gate.py +0 -0
  81. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/gate_check.py +0 -0
  82. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/gate_server.py +0 -0
  83. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/harvest.py +0 -0
  84. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/inventory.py +0 -0
  85. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/mds.py +0 -0
  86. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/merge.py +0 -0
  87. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/merge_jsp.py +0 -0
  88. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/omniscient_next/__init__.py +0 -0
  89. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/omniscient_next/_init_js.py +0 -0
  90. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/omniscient_next/filterpanel.js +0 -0
  91. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/omniscient_next/graph.js +0 -0
  92. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/omniscient_next/library.js +0 -0
  93. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/omniscient_next/next_embed.css +0 -0
  94. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/omniscient_next/search.js +0 -0
  95. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/omniscient_next/siderail.js +0 -0
  96. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/omniscient_next/standalone.py +0 -0
  97. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/omniscient_next/wholemap.js +0 -0
  98. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/parameterize.py +0 -0
  99. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/portal.py +0 -0
  100. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/prompts.py +0 -0
  101. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/pycalls.py +0 -0
  102. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/ratify.py +0 -0
  103. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/report.py +0 -0
  104. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/reports.py +0 -0
  105. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/sample.py +0 -0
  106. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/secretary.py +0 -0
  107. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/shell.py +0 -0
  108. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/stages.py +0 -0
  109. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/tables.py +0 -0
  110. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/tasks.py +0 -0
  111. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph/urcode.py +0 -0
  112. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph.egg-info/dependency_links.txt +0 -0
  113. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph.egg-info/entry_points.txt +0 -0
  114. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph.egg-info/requires.txt +0 -0
  115. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/ambitgraph.egg-info/top_level.txt +0 -0
  116. {ambitgraph-0.8.1 → ambitgraph-0.8.2}/setup.cfg +0 -0
@@ -12,6 +12,11 @@ prune dist
12
12
  prune evidence-*
13
13
  prune evidence
14
14
 
15
+ # The repository's own responsibility record (.ambit/responsibility.jsonl):
16
+ # it describes THIS repository's people and travels with the source tree,
17
+ # never with the installed package.
18
+ prune .ambit
19
+
15
20
  # Internal process documents.
16
21
  exclude AGENTS.md
17
22
  exclude CHANGELOG.md
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ambitgraph
3
- Version: 0.8.1
3
+ Version: 0.8.2
4
4
  Summary: Read-only codebase scan: reusable units, families, roles, ownership candidates, duplication.
5
5
  Author: AmbitGraph
6
6
  License: Proprietary
@@ -156,13 +156,12 @@ class DaemonTimeout(Exception):
156
156
 
157
157
 
158
158
  # ---------------------------------------------------------------------------
159
- # 2. reading the serve file -- fail-closed, the read_paths_index posture
159
+ # 2. reading the serve file -- fail-closed
160
160
  # ---------------------------------------------------------------------------
161
161
 
162
162
 
163
163
  def read_serve_file(out_dir):
164
- """Read <out_dir>/serve.json -> dict. FAIL-CLOSED, the exact posture
165
- orchestrate.read_paths_index already takes: absent / unreadable /
164
+ """Read <out_dir>/serve.json -> dict. FAIL-CLOSED: absent / unreadable /
166
165
  non-JSON / not-a-dict / wrong schema version `v` -> None. Likewise,
167
166
  "pid"/"port" that are present but not ints (null, a
168
167
  string, missing) are the SAME fail-closed None -- every caller of this
@@ -1031,6 +1030,19 @@ def launch(args, out_dir, repo, idle_minutes, budget_s=None,
1031
1030
  lines.append(reg.DAEMON_DAMAGED.format(reason=health["damaged"]))
1032
1031
  if cmd is None:
1033
1032
  cmd = invocation_form() + " " + cmd_name
1033
+ # The stop instruction echoes the --out the caller typed, exactly as
1034
+ # {repo} echoes the typed repo: the state file lives under that
1035
+ # directory, so the printed command must carry it to find the map.
1036
+ # A typed value holding a space is double-quoted so the line survives
1037
+ # a shell; without --out the segment is empty and the line is
1038
+ # unchanged.
1039
+ typed_out = getattr(args, "out", None)
1040
+ if typed_out:
1041
+ typed_out = str(typed_out)
1042
+ out_segment = (' --out "{0}"'.format(typed_out) if " " in typed_out
1043
+ else " --out " + typed_out)
1044
+ else:
1045
+ out_segment = ""
1034
1046
  lines.append(reg.DAEMON_OPEN.format(url=url, repo=args.repo, cmd=cmd,
1035
- minutes=idle_minutes))
1047
+ minutes=idle_minutes, out=out_segment))
1036
1048
  return 0, lines, url
@@ -592,9 +592,9 @@ def attribute_head(text):
592
592
  # module) rather than duplicated per Python pack; never resolved to a
593
593
  # real uid here (that join belongs to the renderer -- see
594
594
  # registry.resolve_py_module_spec, a BY-VALUE mirror of
595
- # ambitgraph/edges.py's own `_resolve_python_import`, kept in registry.py
596
- # rather than here because library.py's own import wall does not admit
597
- # this module). Never file I/O; every function here is pure over text
595
+ # ambitgraph/edges.py's own `_resolve_python_import`, kept independently
596
+ # in registry.py since `ambit_map` imports no `ambitgraph`). Never file
597
+ # I/O; every function here is pure over text
598
598
  # already handed to it, matching every other helper in this module.
599
599
  # ---------------------------------------------------------------------------
600
600
 
@@ -770,14 +770,11 @@ def attribute_head_module_spec(head, module_imports, name_imports):
770
770
 
771
771
 
772
772
  # resolve_py_module_spec (the module_spec -> real-uid resolution half)
773
- # lives in registry.py, NOT here -- library.py's own import wall
774
- # (test_library_and_page_import_only_stdlib_and_ambit_map) restricts it to
775
- # {registry, objects, families}, and registry.py already sits BELOW this
773
+ # lives in registry.py, NOT here -- registry.py already sits BELOW this
776
774
  # module in the dependency order (contract.py imports registry, never the
777
- # reverse), so hosting the pure resolver there lets the renderer use it
778
- # without library.py importing dialects.contract at all. See registry.py's
779
- # own import-resolution section for the BY-VALUE mirror of ambitgraph/edges.py's
780
- # `_resolve_python_import` this module's docstring above describes.
775
+ # reverse). See registry.py's own import-resolution section for the
776
+ # BY-VALUE mirror of ambitgraph/edges.py's `_resolve_python_import` this
777
+ # module's docstring above describes.
781
778
 
782
779
 
783
780
  def kwarg_text(parts, name):
@@ -1153,12 +1150,10 @@ def probe_notes(probe):
1153
1150
 
1154
1151
 
1155
1152
  # ---------------------------------------------------------------------------
1156
- # The entry-point glob language + resolver. Lives
1157
- # here (where the pattern language is defined and evaluated) rather than in
1158
- # graph.py -- library.py's own standing rule is that a renderer "imports no
1159
- # pack, only registry"; resolving in one place yields a report field that
1160
- # carries REAL PATHS, so graph.py never pattern-matches anything, only
1161
- # checks set membership against `getattr(dialects, "entry_points", ())`.
1153
+ # The entry-point glob language + resolver. Lives here, where the pattern
1154
+ # language is defined and evaluated, so resolving in one place yields a
1155
+ # report field that carries REAL PATHS rather than raw patterns a
1156
+ # renderer would have to re-match itself.
1162
1157
  # ---------------------------------------------------------------------------
1163
1158
 
1164
1159
  ENTRY_GLOB_DOC = """A pattern is matched against the WHOLE repo-relative posix uid,
@@ -12,22 +12,46 @@ value already built from a sorted walk).
12
12
  """
13
13
 
14
14
  # ---------------------------------------------------------------------------
15
- # 1.1 The eight families -- FIXED order, key . name . letter . pigment
15
+ # 1.1 The nine families -- FIXED order, key . name . letter . light . dark
16
16
  # ---------------------------------------------------------------------------
17
-
17
+ # TWO PIGMENT COLUMNS, ONE PER THEME, the same shape the folder palette carries.
18
+ # A family keeps ONE identity and wears the column its surface calls for; the
19
+ # hue is held across the pair so the green family is green in both.
20
+ #
21
+ # THE LIGHT COLUMN IS THE SHIPPED ONE, unchanged for the eight that existed.
22
+ # Measured rather than assumed: all eight clear the 3:1 non-text floor on the
23
+ # light grounds and only four clear it on the dark ones, which is what says
24
+ # these were authored for a light surface. The dark column is derived from it,
25
+ # hue held and lightness raised until each clears both dark grounds -- four
26
+ # needed no change at all.
27
+ #
28
+ # `modules` KEEPS ITS KEY AND CHANGES ITS NAME to Logic. The key is what every
29
+ # pin, artifact and stored ruling refers to, so renaming the display string
30
+ # moves no file and breaks no reference; renaming the key would do both for no
31
+ # reader-visible gain.
32
+ #
33
+ # `tests` is the one new family. Its hue sits in the widest unused gap -- 89
34
+ # degrees between olive and teal, centred at 131 -- and its separation from
35
+ # every other family was measured, not eyeballed: dE76 27.2 in the light column
36
+ # and 23.1 in the dark, against the tightest pair already shipped at 20.9 and
37
+ # 17.6. It is deliberately dark and unsaturated: it will be the second-largest
38
+ # family on a typical map, and a fifth of every picture wearing a shout is a
39
+ # picture about its tests.
18
40
  FAMILIES = (
19
- ("routes", "Routes", "R", "#0e7490"),
20
- ("pages", "Pages & Components", "P", "#6d28d9"),
21
- ("modules", "Modules", "M", "#4d7c0f"),
22
- ("helpers", "Helpers", "H", "#b45309"),
23
- ("tables", "Models & Tables", "T", "#26418f"),
24
- ("scripts", "Scripts", "S", "#a8324f"),
25
- ("config", "Config", "C", "#475569"),
26
- ("styles", "Styles", "Y", "#0f766e"),
41
+ ("routes", "Routes", "R", "#0e7490", "#0e7490"),
42
+ ("pages", "Pages & Components", "P", "#6d28d9", "#8246de"),
43
+ ("modules", "Logic", "L", "#4d7c0f", "#4d7c0f"),
44
+ ("helpers", "Helpers", "H", "#b45309", "#b45309"),
45
+ ("tables", "Models & Tables", "T", "#26418f", "#3e62cc"),
46
+ ("scripts", "Scripts", "S", "#a8324f", "#bc3858"),
47
+ ("config", "Config", "C", "#475569", "#5a6b84"),
48
+ ("styles", "Styles", "Y", "#0f766e", "#0f766e"),
49
+ ("tests", "Tests", "E", "#1e4825", "#31773d"),
27
50
  )
28
51
 
29
52
  KEYS = tuple(k for k, *_rest in FAMILIES)
30
- _BY_KEY = {k: (name, letter, pig) for k, name, letter, pig in FAMILIES}
53
+ _BY_KEY = {k: (name, letter, light, dark)
54
+ for k, name, letter, light, dark in FAMILIES}
31
55
  _FALLBACK_PIG = "#475569"
32
56
 
33
57
  # ---------------------------------------------------------------------------
@@ -78,6 +102,8 @@ _PAGE_ANCHOR_EXTS = frozenset(("tsx", "jsx", "vue", "svelte", "astro", "mdx", "h
78
102
  _HELPER_SEGMENTS = frozenset(("utils", "util", "lib", "libs", "helpers", "hooks",
79
103
  "includes", "shared", "common"))
80
104
  _HELPER_STEMS = frozenset(("utils", "util", "helpers", "helper"))
105
+ _TEST_SEGMENTS = frozenset(("tests", "test", "__tests__", "spec", "specs"))
106
+ _TEST_BASES = frozenset(("conftest.py",))
81
107
 
82
108
 
83
109
  def family_of(unit, routes_by_uid=None):
@@ -100,6 +126,25 @@ def family_of(unit, routes_by_uid=None):
100
126
  if routes_by_uid and uid in routes_by_uid:
101
127
  return "routes"
102
128
 
129
+ # 1b -- a file that exists to CHECK other files is a test, whatever its
130
+ # extension or its directory. This sits second on purpose. A test's
131
+ # identity as a test dominates both: components/Button.test.tsx is a test,
132
+ # not a page. Placed lower it would catch almost nothing, because the page
133
+ # rule takes .tsx, the helper rule takes anything under lib/, and the table
134
+ # rule takes anything under db/ -- each would claim the file first.
135
+ #
136
+ # It sits AFTER the route fact because that fact is a real mount read from
137
+ # a dialect pack, and a test file cannot hold one.
138
+ #
139
+ # The prefix is `test_` WITH its underscore rather than a bare startswith,
140
+ # so contest.py and latest.py are not swept in. `scripts/run-tests.sh` is
141
+ # likewise untouched: it RUNS tests and is not one.
142
+ if (any(s in _TEST_SEGMENTS for s in segs)
143
+ or base in _TEST_BASES
144
+ or stem.startswith("test_") or stem.endswith("_test")
145
+ or stem.endswith(".test") or stem.endswith(".spec")):
146
+ return "tests"
147
+
103
148
  # 1b -- filename convention, evaluated whether or not any pack fired.
104
149
  if stem in _ROUTE_STEMS:
105
150
  return "routes"
@@ -170,7 +215,7 @@ def families_in_use(units, routes_by_uid=None):
170
215
  key = family_of(u, routes_by_uid)
171
216
  buckets.setdefault(key, []).append(u)
172
217
  out = []
173
- for key, _name, _letter, _pig in FAMILIES:
218
+ for key, _name, _letter, _light, _dark in FAMILIES:
174
219
  if key in buckets:
175
220
  out.append((key, tuple(sorted(buckets[key], key=lambda u: _uid_of(u)))))
176
221
  return tuple(out)
@@ -180,7 +225,19 @@ def families_in_use(units, routes_by_uid=None):
180
225
  # 1.5 wired -- the truth table
181
226
  # ---------------------------------------------------------------------------
182
227
 
183
- ENTRY_FAMILIES = frozenset(("routes", "pages", "scripts", "config", "styles", "tables"))
228
+ # The families nothing needs to point AT. A unit here with no inbound edge is
229
+ # not a loose end -- it is how that kind of file is supposed to look, so it
230
+ # reads "live" rather than joining the unlinked review queue.
231
+ #
232
+ # ⚠ TESTS BELONG HERE AND THE OMISSION IS EXPENSIVE. A test runner DISCOVERS
233
+ # test files; production code never imports them, so every test in a repository
234
+ # has zero inbound edges by design. This list is a POSITIVE one -- anything
235
+ # absent from it falls through to "coming" -- so leaving tests out would have
236
+ # flagged every test file on the map as a possible loose end. On the reference
237
+ # repository that is 55 files, a fifth of the picture, every one of them
238
+ # accused of being unreachable code while being exactly what it should be.
239
+ ENTRY_FAMILIES = frozenset(("routes", "pages", "scripts", "config", "styles",
240
+ "tables", "tests"))
184
241
 
185
242
 
186
243
  def _inbound(model, uid):
@@ -223,9 +280,24 @@ def letter(key):
223
280
  return v[1] if v else "?"
224
281
 
225
282
 
226
- def pigment(key):
283
+ def pigment(key, theme="light"):
284
+ """-> the family's fill for one theme, defaulting to the light column.
285
+
286
+ Two steps: look the family up, then take the column the theme asks for.
287
+ An unknown theme falls back to light rather than raising, because a
288
+ renderer asking for a theme this table has not heard of should still draw
289
+ something.
290
+
291
+ THE DEFAULT IS LIGHT, AND THAT IS NOT A PREFERENCE. Every existing caller
292
+ was written against a single-column table whose colours were authored for
293
+ a light surface, so defaulting to light leaves all of them correct and
294
+ unchanged. A default of dark would have silently repainted every current
295
+ page while every call site still read the same.
296
+ """
227
297
  v = _BY_KEY.get(key)
228
- return v[2] if v else _FALLBACK_PIG
298
+ if not v:
299
+ return _FALLBACK_PIG
300
+ return v[3] if theme == "dark" else v[2]
229
301
 
230
302
 
231
303
  def name(key):
@@ -233,6 +305,61 @@ def name(key):
233
305
  return v[0] if v else key
234
306
 
235
307
 
308
+ # ---------------------------------------------------------------------------
309
+ # 1.7 DEFINITIONS -- one sentence per family, for the reader on its board
310
+ # ---------------------------------------------------------------------------
311
+
312
+ # A SEPARATE TABLE RATHER THAN A SIXTH COLUMN ON `FAMILIES`. Widening that
313
+ # tuple would re-arity two unpackers here and two in the suite -- a
314
+ # restructure riding inside a behaviour change. The completeness this costs
315
+ # (a row physically cannot omit a sixth column) is bought back by the pin
316
+ # that checks these keys against KEYS in BOTH directions.
317
+ #
318
+ # EVERY SENTENCE IS TRUE ONLY IN CASCADE ORDER, because `family_of` is a
319
+ # cascade: the first rule to match wins and later rules never see the unit.
320
+ # So a sentence must never claim a signal an EARLIER rule already took --
321
+ # "pages are .tsx files" is false the moment a .tsx sits under a tests
322
+ # directory. Where that carve-out is the thing a reader would otherwise get
323
+ # wrong, the sentence names it rather than leaving a quiet exception.
324
+ #
325
+ # `modules` IS THE FALLBACK, and its sentence says so instead of naming a
326
+ # purpose. Its members share nothing but having matched no earlier rule: a
327
+ # changelog, an entry point and a data-access module land there together, so
328
+ # any sentence claiming a subject would be false on the first board opened.
329
+ DEFINITIONS = {
330
+ "routes": ("Files whose name or folder marks them as a way in — a router, "
331
+ "a URL table, middleware, or anything under a routes folder."),
332
+ "pages": ("What the app draws — components, templates and views, by file "
333
+ "type or by the folder holding them."),
334
+ "modules": ("Everything no other rule claimed. These files share only that "
335
+ "nothing above matched them — a remainder, not a subject."),
336
+ "helpers": ("Shared odds and ends, by a utils, lib or shared folder or by "
337
+ "name. Data models under those same folders are in Models & "
338
+ "Tables."),
339
+ "tables": ("Where the app's data shape lives — migrations, schemas, and "
340
+ "files named or foldered as models."),
341
+ "scripts": ("Things you run rather than import — shell and batch files, and "
342
+ "whatever sits in a scripts, bin or tools folder."),
343
+ "config": ("Settings and manifests rather than code — declaration files, by "
344
+ "extension or by well-known name."),
345
+ "styles": "How the app looks — stylesheets, by file type.",
346
+ "tests": ("Files that exist to check other files. A test's identity beats "
347
+ "its file type, so a test component sits here rather than in "
348
+ "Pages."),
349
+ }
350
+
351
+
352
+ def definition(key):
353
+ """-> the family's one-sentence definition, or "" for a key this table
354
+ has not heard of.
355
+
356
+ The empty string rather than a raise, and rather than a placeholder: a
357
+ board for an unknown family should render its counts and say nothing,
358
+ not interrupt the page or print a sentence nobody wrote. The same
359
+ fall-back-quietly posture `letter`, `pigment` and `name` already take.
360
+ """
361
+ return DEFINITIONS.get(key, "")
362
+
236
363
  _QUOTE_SAFE = frozenset(
237
364
  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.-~")
238
365
 
@@ -32,11 +32,9 @@ Unlike ambitgraph/langs.py (which imports `re` and `dataclasses` for its
32
32
  own LangSpec declarations), this module is a pure, dependency-free LEAF:
33
33
  no imports at all, not even stdlib. Every value below is a literal, and
34
34
  the single function here (`blind_langs`) reads only the table declared
35
- directly above it, so it needs no import either. Every
36
- consumer inside this package (orchestrate.py, paths.py, registry.py,
37
- library.py) imports IT; it imports NONE of them, so no existing consumer
38
- of registry.py/paths.py/library.py/orchestrate.py risks a cycle by their
39
- now also depending, transitively, on this module.
35
+ directly above it, so it needs no import either. Every consumer inside
36
+ this package imports IT; it imports NONE of them, so no consumer risks
37
+ a cycle by depending, transitively, on this module.
40
38
 
41
39
  NO MEMBER DELETION -- the dead-looking members below are kept deliberately,
42
40
  not by oversight, and deleting one is a real regression:
@@ -180,9 +178,7 @@ GO_LANGS = ("go",)
180
178
  # declaration ("class"/"interface"/"enum"/"record") -- "constructor" is
181
179
  # DELIBERATELY not a member, the same exclusion codelib.py's own set
182
180
  # states (a constructor is a member OF a type, never itself a type
183
- # declaration). Consumed by ambit_map/unitpage.py's own class-count
184
- # bucket so a Java interface/enum/record renders in the class(es) count
185
- # on the unit story, not silently in "other named symbol(s)". Diffed
181
+ # declaration). Diffed
186
182
  # against ambitgraph.codelib._TYPE_SYMBOL_KINDS by
187
183
  # tests/test_langs_mirror_diff.py (DIFFED_FACT_NAMES entry
188
184
  # "type_symbol_kinds") -- one origin (symbols.py's own kind literals), two
@@ -194,11 +190,9 @@ TYPE_SYMBOL_KINDS = ("class", "interface", "enum", "record")
194
190
  # strings symbols.py's extract_c_symbols emits for a C type declaration --
195
191
  # a tagged bodied "struct"/"union"/"enum", and "typedef". "function" is
196
192
  # deliberately not a member (it is the callable half, the same boundary
197
- # TYPE_SYMBOL_KINDS draws against "constructor"). Consumed by
198
- # ambit_map/unitpage.py's class-count bucket under the C_LANGS gate above:
199
- # "enum" is a member of BOTH this tuple and TYPE_SYMBOL_KINDS, and the two
200
- # language gates are disjoint, so a TypeScript enum still renders as
201
- # "other named symbol(s)" while a C enum counts as a type. Diffed against
193
+ # TYPE_SYMBOL_KINDS draws against "constructor"). "enum" is a member of
194
+ # BOTH this tuple and TYPE_SYMBOL_KINDS, and the two language gates are
195
+ # disjoint, under the C_LANGS gate above. Diffed against
202
196
  # ambitgraph.codelib._C_TYPE_SYMBOL_KINDS by tests/test_langs_mirror_diff.py
203
197
  # (DIFFED_FACT_NAMES entry "c_type_symbol_kinds") -- one origin
204
198
  # (symbols.py's own kind literals), two independently-declared copies,
@@ -213,13 +207,7 @@ C_TYPE_SYMBOL_KINDS = ("struct", "union", "enum", "typedef")
213
207
  # "class" itself (Ruby genuinely has classes), so it feeds BOTH halves of
214
208
  # codelib.py's carve identically -- the same shape TYPE_SYMBOL_KINDS takes
215
209
  # for Java, never C's shape (which unions "class" in separately because C
216
- # never emits that literal kind at all). Consumed by
217
- # ambit_map/unitpage.py's class-count bucket under the RB_LANGS gate
218
- # above, WIDENING THE COUNT ONLY -- the word stays reg.UNIT_STORY_CLASSES
219
- # for Ruby (unitpage.py's own class_word ternary is not gated on RB_LANGS
220
- # at all): Ruby genuinely has classes, so mirroring C's word-swap here
221
- # would render "N type(s)" about a language whose classes are exactly what
222
- # the count is naming. Diffed against ambitgraph.codelib._RB_TYPE_SYMBOL_KINDS
210
+ # never emits that literal kind at all). Diffed against ambitgraph.codelib._RB_TYPE_SYMBOL_KINDS
223
211
  # by tests/test_langs_mirror_diff.py (DIFFED_FACT_NAMES entry
224
212
  # "rb_type_symbol_kinds") -- one origin (symbols.py's own kind literals),
225
213
  # two independently-declared copies, never a cross-wall import.
@@ -228,12 +216,11 @@ RB_TYPE_SYMBOL_KINDS = ("class", "module")
228
216
  # NAMESPACE_LANGS (the wall-legal PRODUCT-side mirror of
229
217
  # ambitgraph/langs.py's own NAMESPACE_LANGS): the `lang` tags whose units
230
218
  # get the java-namespace treatment (the languages whose LangSpec declares
231
- # namespace=True). Consumed by ambit_map/unitpage.py's class-count bucket
232
- # as the language gate on TYPE_SYMBOL_KINDS above -- symbols.py emits the
233
- # SAME literal kinds ("interface"/"enum") for TypeScript declarations,
234
- # which must keep rendering as "other named symbol(s)", exactly the
235
- # boundary ambitgraph/codelib.py draws with its own is_java_namespace
236
- # check at export. Diffed against ambitgraph.langs.NAMESPACE_LANGS by
219
+ # namespace=True) -- symbols.py emits the SAME literal kinds
220
+ # ("interface"/"enum") for TypeScript declarations, which must keep
221
+ # rendering as "other named symbol(s)", exactly the boundary
222
+ # ambitgraph/codelib.py draws with its own is_java_namespace check at
223
+ # export. Diffed against ambitgraph.langs.NAMESPACE_LANGS by
237
224
  # tests/test_langs_mirror_diff.py (DIFFED_FACT_NAMES entry
238
225
  # "namespace_langs") -- one origin (the LangSpec namespace flags), two
239
226
  # independently-declared copies, never a cross-wall import.
@@ -246,32 +233,22 @@ NAMESPACE_LANGS = frozenset({"java", "jsp"})
246
233
  # can never appear as a real `lang` value today (ambitgraph/symbols.py's
247
234
  # build_file_plane folds .jspf into the tag "jsp", never into "jspf"
248
235
  # itself) but stays for the identical producer-portability reason.
249
- # Consumed DIRECTLY by ambit_map/library.py's routedness gates
250
- # (_routedness_is_silent, _blind_for_run) via `langm.ROUTEDNESS_LANGS` --
251
- # no production call site reads it through registry.py anymore.
252
- # ambit_map/registry.py still re-exports the name (`ROUTEDNESS_LANGS =
253
- # langm.ROUTEDNESS_LANGS`), kept alive for backward compatibility and
254
- # because tests/test_langs_value_pins.py's own locked oracle pin
255
- # still asserts `reg.ROUTEDNESS_LANGS` by value.
236
+ # ambit_map/registry.py re-exports the name (`ROUTEDNESS_LANGS =
237
+ # langm.ROUTEDNESS_LANGS`), kept alive because
238
+ # tests/test_langs_value_pins.py's own locked oracle pin still asserts
239
+ # `reg.ROUTEDNESS_LANGS` by value; no product call site reads it today.
256
240
  # ===========================================================================
257
241
  ROUTEDNESS_LANGS = ("jsp", "jspf", "java")
258
242
 
259
243
 
260
244
  # ===========================================================================
261
- # Formerly ambit_map/paths.py's own
262
- # KIND_READER_LANGS literal (the full reader-coverage matrix: 19
263
- # ALLOWED_KINDS rows -> the Unit.lang frozenset each kind's reader covers).
264
- # The 19th row, include/import.java, is additive --
265
- # every pre-existing row's value is UNCHANGED. Values UNCHANGED otherwise;
266
- # the "why" narrative for each individual row (which real reader function
267
- # backs it, what its own language gate looks like) stays in
268
- # ambit_map/paths.py's own module comment beside its KIND_READER_LANGS
269
- # alias -- this module only carries the literal.
245
+ # KIND_READER_LANGS: the full reader-coverage matrix, 19 ALLOWED_KINDS
246
+ # rows -> the Unit.lang frozenset each kind's reader covers. The 19th
247
+ # row, include/import.java, is additive -- every pre-existing row's
248
+ # value is UNCHANGED.
270
249
  #
271
- # _kind_applicable's THREE NAMED DIVERGENCES (ambit_map/paths.py, unmoved
272
- # -- these are about HOW paths.py USES this table, not part of the table's
273
- # own values, so the overriding logic and its own inputs (TERMINAL_KINDS,
274
- # MOUNT_CAPABLE_PACKS) stay declared in paths.py, not here):
250
+ # Three named divergences apply this table with an override, rather than
251
+ # a bare language-overlap lookup:
275
252
  # 1. TERMINAL_KINDS (table/read|write|ddl) -- LANGUAGE-AGNOSTIC by the
276
253
  # real reader's own construction (ambitgraph/tables.py's build_tables
277
254
  # carries no language gate at all): applicable whenever the model
@@ -280,8 +257,8 @@ ROUTEDNESS_LANGS = ("jsp", "jspf", "java")
280
257
  # 2. "reference/script" -- unconditionally False: CONFIG-gated on
281
258
  # ScanConfig.asset_roots, never language-gated, and this product's own
282
259
  # scan config never sets asset_roots -- so this row's frozenset below
283
- # is never actually consulted either; kept only so this dict's key set
284
- # still equals paths.ALLOWED_KINDS.
260
+ # is never actually consulted either; kept only so this dict's key
261
+ # set still equals the full 19-kind domain.
285
262
  # 3. "route/mount" -- language overlap against this row IS consulted, but
286
263
  # is NECESSARY, not SUFFICIENT: applicability additionally requires at
287
264
  # least one of MOUNT_CAPABLE_PACKS (express_pack/fastapi_pack/
@@ -395,7 +372,7 @@ _GRAMMAR_EXTS = frozenset({
395
372
  # The disclosure-gate vocabulary -- THE PER-SENTENCE LANGUAGE
396
373
  # TAG, product side. Formerly ambit_map/library.py's own
397
374
  # _BLIND_SENTENCE_LANGS/_blind_langs pair; it lives here, with every other
398
- # cross-wall copy, and library.py aliases the reader.
375
+ # cross-wall copy.
399
376
  #
400
377
  # The engine declares each blind sentence's own languages AT the sentence
401
378
  # (ambitgraph.symbols.BlindSentence's `langs`, empty meaning the sentence
@@ -318,9 +318,7 @@ def load_run(run_dir):
318
318
  # is tried against either resolution tier at
319
319
  # all). registry.py's unresolved_reason_sentence
320
320
  # is the dispatch that maps these three literals
321
- # to their sentences, for every caption site
322
- # (graph.py's _quarantine_note, unitpage.py's
323
- # connections block).
321
+ # to their sentences.
324
322
  reason = row.get("unresolved_reason")
325
323
  unresolved_acc[src].append(
326
324
  model.UnresolvedRef(
@@ -51,11 +51,10 @@ class UnresolvedRef:
51
51
  # ladder for Python -- neither '' nor the importer's own derived
52
52
  # root resolved it, but some OTHER one did; carried on the edges row
53
53
  # as "unresolved_reason"), distinct from an ordinary external/
54
- # never-read import -- ambit_map/load.py sets it, and the caption
55
- # sites (graph.py's _quarantine_note, unitpage.py's connections
56
- # block) read it through registry.py's unresolved_reason_sentence to
57
- # pick the correct sentence (never the false "this scan never read"
58
- # one for a target the scan genuinely read). This token is NOT
54
+ # never-read import -- ambit_map/load.py sets it, and
55
+ # registry.py's unresolved_reason_sentence picks the correct
56
+ # sentence for it (never the false "this scan never read" one for a
57
+ # target the scan genuinely read). This token is NOT
59
58
  # Python-only -- the Java ladder's cross-root tier REUSES this exact
60
59
  # same token, byte-identically, for a java-sourced cross-root hold,
61
60
  # rather than minting a java-named twin (the sentence text itself
@@ -64,8 +63,8 @@ class UnresolvedRef:
64
63
  # before the ladder change renamed it; see edges.py's own docstring
65
64
  # for why the old name no longer names a distinct state.) A machine
66
65
  # token is an interface, so this one keeps the same posture as
67
- # cross_root_import_held -- each ships WITH its own graph.py branch
68
- # and registry.py sentence, never a free-text reason (a free-text
66
+ # cross_root_import_held -- each ships WITH its own registry.py
67
+ # sentence, never a free-text reason (a free-text
69
68
  # reason falls through to the generic "never read" sentence, which is
70
69
  # false for a target this scan genuinely did read):
71
70
  # "wildcard_import" marks a Java wildcard import (`import com.foo.*;`)
@@ -124,8 +123,8 @@ class CallEdge:
124
123
  class MemberResolvedEdge:
125
124
  """A DATA-PLANE row only -- emitted as data, never painted onto the
126
125
  graph: never an IncludeEdge, never folded into `includes`/
127
- `included_by`, never read by graph.py's merge_edges (the quarantine
128
- precedent: present in data, never painted, on purpose). `src` is the
126
+ `included_by` (the quarantine precedent: present in data, never
127
+ painted, on purpose). `src` is the
129
128
  importing unit; `name` is the bound name it imported; `through` is the
130
129
  directly-imported module (already has its own ordinary IncludeEdge);
131
130
  `target` is the module that first DECLARES `name`, reached by tracing
@@ -160,8 +159,8 @@ class PhantomEdge:
160
159
  # ui/component-use). These mirror MemberResolvedEdge's own posture exactly:
161
160
  # DATA-PLANE-shaped value objects, never resolved or joined here -- the
162
161
  # join (composed-route table, event pairing, component-use resolution)
163
- # is ambit_map/matched.py's own job, over these raw facts plus the
164
- # already-loaded model. `src` is the file the site was read in, matching
162
+ # is a separate step, over these raw facts plus the already-loaded
163
+ # model. `src` is the file the site was read in, matching
165
164
  # every other per-unit dict's own key shape (`includes`, `member_resolved`,
166
165
  # ...).
167
166
  # ---------------------------------------------------------------------------
@@ -221,8 +220,8 @@ class ImportBindingTarget:
221
220
 
222
221
 
223
222
  # ---------------------------------------------------------------------------
224
- # The PRODUCT-side output of the four joins in ambit_map/matched.py --
225
- # pure value objects, never constructed by load.py (unlike every dataclass
223
+ # The PRODUCT-side output of a matched-edge join -- pure value objects,
224
+ # never constructed by load.py (unlike every dataclass
226
225
  # above this point, which mirrors an artifact row 1:1). `kind` is always an
227
226
  # ambit_map.registry.EDGE_KINDS key. `src`/`dst` are always real unit uids
228
227
  # (a matched edge with an unresolved endpoint is never constructed -- it is
@@ -405,8 +404,7 @@ class MapModel:
405
404
  # Additive: raw per-unit SITE FACTS, the SAME
406
405
  # uid -> tuple[...] shape `member_resolved` already uses. Defaulted so
407
406
  # every existing MapModel(...) call site (every hand-built test fixture)
408
- # stays byte-unaffected. Never joined/resolved here -- see
409
- # ambit_map/matched.py.
407
+ # stays byte-unaffected. Never joined/resolved here.
410
408
  request_sites: dict = field(default_factory=dict)
411
409
  event_sites: dict = field(default_factory=dict)
412
410
  component_use_sites: dict = field(default_factory=dict)