ambitgraph 0.2.0__tar.gz → 0.4.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 (89) hide show
  1. {ambitgraph-0.2.0/ambitgraph.egg-info → ambitgraph-0.4.0}/PKG-INFO +1 -1
  2. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambit_map/graph.py +31 -6
  3. ambitgraph-0.4.0/ambit_map/langmirror.py +382 -0
  4. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambit_map/library.py +37 -26
  5. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambit_map/load.py +29 -6
  6. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambit_map/matched.py +6 -0
  7. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambit_map/model.py +34 -11
  8. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambit_map/objects.py +12 -5
  9. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambit_map/orchestrate.py +26 -18
  10. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambit_map/paths.py +33 -36
  11. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambit_map/registry.py +313 -47
  12. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambit_map/unitpage.py +76 -3
  13. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/__init__.py +1 -1
  14. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/cli.py +15 -0
  15. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/codelib.py +192 -7
  16. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/config.py +7 -4
  17. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/edges.py +1403 -41
  18. ambitgraph-0.4.0/ambitgraph/langs.py +494 -0
  19. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/portal.py +4 -4
  20. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/shell.py +41 -37
  21. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/symbols.py +1786 -224
  22. {ambitgraph-0.2.0 → ambitgraph-0.4.0/ambitgraph.egg-info}/PKG-INFO +1 -1
  23. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph.egg-info/SOURCES.txt +2 -0
  24. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/pyproject.toml +1 -1
  25. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/LICENSE +0 -0
  26. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/MANIFEST.in +0 -0
  27. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/README.md +0 -0
  28. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambit_map/__init__.py +0 -0
  29. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambit_map/__main__.py +0 -0
  30. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambit_map/ambit-map-enrich.md +0 -0
  31. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambit_map/ambit-mapllm-contract.md +0 -0
  32. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambit_map/cli.py +0 -0
  33. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambit_map/daemon.py +0 -0
  34. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambit_map/dialects/__init__.py +0 -0
  35. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambit_map/dialects/contract.py +0 -0
  36. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambit_map/dialects/express_pack.py +0 -0
  37. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambit_map/dialects/fastapi_pack.py +0 -0
  38. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambit_map/dialects/flask_pack.py +0 -0
  39. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambit_map/dialects/nextjs_pack.py +0 -0
  40. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambit_map/dialects/supabase_pack.py +0 -0
  41. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambit_map/export.py +0 -0
  42. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambit_map/families.py +0 -0
  43. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambit_map/island_layout.py +0 -0
  44. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambit_map/islands.py +0 -0
  45. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambit_map/judged.py +0 -0
  46. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambit_map/kitdemo.py +0 -0
  47. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambit_map/mapllm.py +0 -0
  48. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambit_map/page.py +0 -0
  49. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambit_map/serve.py +0 -0
  50. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambit_map/theme.py +0 -0
  51. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/WALKTHROUGH.md +0 -0
  52. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/__main__.py +0 -0
  53. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/cockpit.py +0 -0
  54. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/console.py +0 -0
  55. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/demo.py +0 -0
  56. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/docs_discovery.py +0 -0
  57. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/doctor.py +0 -0
  58. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/duplication.py +0 -0
  59. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/emit.py +0 -0
  60. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/engine_identity.py +0 -0
  61. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/estimate.py +0 -0
  62. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/eval.py +0 -0
  63. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/feeds.py +0 -0
  64. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/gate.py +0 -0
  65. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/gate_check.py +0 -0
  66. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/gate_server.py +0 -0
  67. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/harvest.py +0 -0
  68. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/inventory.py +0 -0
  69. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/jscalls.py +0 -0
  70. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/mds.py +0 -0
  71. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/merge.py +0 -0
  72. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/merge_jsp.py +0 -0
  73. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/parameterize.py +0 -0
  74. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/pipeline.py +0 -0
  75. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/pycalls.py +0 -0
  76. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/ratify.py +0 -0
  77. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/report.py +0 -0
  78. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/reports.py +0 -0
  79. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/sample.py +0 -0
  80. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/secretary.py +0 -0
  81. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/stages.py +0 -0
  82. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/tables.py +0 -0
  83. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/tasks.py +0 -0
  84. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph/urcode.py +0 -0
  85. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph.egg-info/dependency_links.txt +0 -0
  86. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph.egg-info/entry_points.txt +0 -0
  87. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph.egg-info/requires.txt +0 -0
  88. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/ambitgraph.egg-info/top_level.txt +0 -0
  89. {ambitgraph-0.2.0 → ambitgraph-0.4.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ambitgraph
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: Read-only codebase scan: reusable units, families, roles, ownership candidates, duplication.
5
5
  Author: AmbitGraph
6
6
  License: Proprietary
@@ -49,6 +49,19 @@ from . import matched as mtc
49
49
  # 3.1 Constants
50
50
  # ---------------------------------------------------------------------------
51
51
 
52
+ # R4.5: precomputed once, a set UNION that tolerates reg.PY_LANGS/
53
+ # reg.JS_LANGS being either tuples (today's committed type) or frozensets
54
+ # (JUDGES-DIGEST.txt LENS 1, C1's named "declared future shape" -- the
55
+ # adversarial finding that KIND_READER_LANGS is already frozenset-valued,
56
+ # so the natural next step for these two groupings is the same). The
57
+ # branch below used to read `reg.PY_LANGS + reg.JS_LANGS`, tuple
58
+ # CONCATENATION: the instant either grouping becomes a frozenset that
59
+ # expression TypeErrors ("can only concatenate tuple (not frozenset) to
60
+ # tuple") in this exact no-edges unit-view render branch -- one the
61
+ # codelib byte-golden (an engine-side export) can never reach. `in a | b`
62
+ # never cares which collection type either operand is.
63
+ _PY_OR_JS_LANGS = frozenset(reg.PY_LANGS) | frozenset(reg.JS_LANGS)
64
+
52
65
  N_CAP_DEFAULT = 28
53
66
  N_CAP_MIN = 8
54
67
  N_CAP_MAX = 80
@@ -1504,11 +1517,23 @@ def _quarantine_note(drawn):
1504
1517
  # read") is false for a cross-root HELD disclosure -- the scan
1505
1518
  # read the candidate, under a root this file does not sit in.
1506
1519
  # Branch on the ref's own reason, never on a guess.
1507
- if r.reason == "cross_root_import_held":
1508
- out += ob.note(reg.cross_root_import_held_sentence(
1509
- A=drawn["id"], raw=r.raw))
1510
- else:
1511
- out += ob.note(reg.sentence("unresolved", A=drawn["id"], raw=r.raw))
1520
+ #
1521
+ # Java W1 (2026-09-02, DESIGN.md D3): two more machine-token
1522
+ # branches, same law -- "wildcard_import" and "importer_root_
1523
+ # underivable" are both false under the generic "never read"
1524
+ # sentence too (see registry.py's own comment on the two
1525
+ # sentences below for why). cross_root_import_held's own sentence
1526
+ # is reused byte-identical for a java-sourced cross-root hold --
1527
+ # its text names no language ("a matching file exists, but only
1528
+ # under a root this file does not sit in"), so it needs no java
1529
+ # variant. The token-to-sentence dispatch itself lives in
1530
+ # reg.unresolved_reason_sentence, shared with unitpage.py's
1531
+ # connections block -- one origin, so a token can never render
1532
+ # here and fall through to the generic caption there.
1533
+ sent = reg.unresolved_reason_sentence(r.reason, A=drawn["id"], raw=r.raw)
1534
+ if not sent:
1535
+ sent = reg.sentence("unresolved", A=drawn["id"], raw=r.raw)
1536
+ out += ob.note(sent)
1512
1537
  cut_phrase = reg.count_phrase(len(shown), total, reg.NOUN_UNRESOLVED)
1513
1538
  if cut_phrase:
1514
1539
  out += ob.note(reg.esc(cut_phrase))
@@ -1717,7 +1742,7 @@ def _canvas_block(model, drawn, n_cap, pinned, dialects):
1717
1742
  if not merged:
1718
1743
  body = ob.note(_library_view_link(drawn))
1719
1744
  body += ob.note(reg.esc(reg.GRAPH_NO_EDGES.format(seed=_seed_display(drawn))))
1720
- if drawn["state"] == "unit" and drawn["unit"].lang in (reg.PY_LANGS + reg.JS_LANGS):
1745
+ if drawn["state"] == "unit" and drawn["unit"].lang in _PY_OR_JS_LANGS:
1721
1746
  blind = lib._combined_blind(model.coverage)
1722
1747
  found = lib._find_blind(blind, reg.CALL_BLIND_MATCH)
1723
1748
  text = found if found else reg.CALL_BLIND_NO_REASON
@@ -0,0 +1,382 @@
1
+ """ambit_map/langmirror.py -- the product-side ASSERTED MIRROR of
2
+ ambitgraph/langs.py's declared language vocabulary (R4-DESIGN.md, v2;
3
+ CENSUS.md).
4
+
5
+ ONE file replacing five scatterings: ambit_map/orchestrate.py's admission
6
+ copy + MAPLLM_EXTRA_INCLUDE_EXT, ambit_map/registry.py's JS_LANGS/PY_LANGS/
7
+ ROUTEDNESS_LANGS, ambit_map/paths.py's KIND_READER_LANGS/_GRAMMAR_EXTS, and
8
+ ambit_map/library.py's own blind-sentence language vocabulary. Every value
9
+ below is UNCHANGED from what its scattered original held -- this module
10
+ only gives them one address; see each field's own comment for its prior
11
+ location, and the module-docstring note below for R4's "no member
12
+ deletion" ruling.
13
+
14
+ THE WALL (tests/test_import_boundaries.py's violations helper, the
15
+ ambit_map stdlib-only rule inside rule_message, and that file's own
16
+ test_self_test_map_importing_contracts_is_caught test): ambit_map may
17
+ NEVER import ambitgraph, not even ambit_contracts, the house's own
18
+ shared-stdlib-only package -- there was never a choice of a third shared
19
+ package; the wall forecloses it outright (R4-DESIGN.md v2, "The
20
+ architecture ruling"). So this module cannot import ambitgraph/
21
+ langs.py and DERIVE its values the way ambitgraph/symbols.py, edges.py,
22
+ config.py and codelib.py now derive theirs from langs.py -- it is instead
23
+ an ASSERTED COPY: every value below is retyped by hand, checked against
24
+ langs.py's own values ONLY by a TEST (tests/test_langs_mirror_diff.py),
25
+ NEVER by any import at runtime. This is not a new exception -- the
26
+ identical both-sides-import shape is already live at
27
+ tests/test_map_serve.py's own
28
+ test_engine_include_ext_is_duplicated_by_value_and_never_drifts test
29
+ (ENGINE_DEFAULT_INCLUDE_EXT vs ScanConfig().include_ext), which this
30
+ module's own admission field below continues rather than replaces.
31
+
32
+ Unlike ambitgraph/langs.py (which imports `re` and `dataclasses` for its
33
+ own LangSpec declarations), this module is a pure, dependency-free LEAF:
34
+ no imports at all, not even stdlib. Every value below is a literal, and
35
+ the single function here (`blind_langs`) reads only the table declared
36
+ directly above it, so it needs no import either. Every
37
+ consumer inside this package (orchestrate.py, paths.py, registry.py,
38
+ library.py) imports IT; it imports NONE of them, so no existing consumer
39
+ of registry.py/paths.py/library.py/orchestrate.py risks a cycle by their
40
+ now also depending, transitively, on this module.
41
+
42
+ NO MEMBER DELETION (R4-DESIGN.md v2, ruling 1, refuting the v1 draft's
43
+ "delete the dead members" plan twice over -- JUDGES-DIGEST.txt LENS 3,
44
+ finding on ruling 1): tests/test_map_registry.py's own
45
+ test_kind_key_maps_import_by_source_language_not_by_kind test already
46
+ pins kind_key("include", "import", "ts") == "include/import.js" --
47
+ deleting "ts" from JS_LANGS reds that live pin. And ambit_map/load.py's
48
+ load_run reads a unit's `lang` value VERBATIM off of symbols.json with NO
49
+ vocabulary gate at all, under this product's own shipped producer-
50
+ portability contract (ambit-mapllm-contract.md, pyproject.toml) -- an
51
+ older or foreign producer's artifact can carry a `lang` this engine's OWN
52
+ current writer (ambitgraph/symbols.py's build_file_plane) never emits
53
+ today. So every grouping below keeps its READER-TOLERANCE members: they
54
+ document what a FILE may say, not only what this engine currently
55
+ writes.
56
+ """
57
+
58
+ # ===========================================================================
59
+ # Admission -- CENSUS fact 1. Byte-duplicated from ambitgraph/langs.py's
60
+ # ENGINE_DEFAULT_INCLUDE_EXT, IN ORDER: the order is byte-load-bearing
61
+ # through ambitgraph/symbols.py's config_hash and through the cross-import
62
+ # equality pin at tests/test_map_serve.py's own
63
+ # test_engine_include_ext_is_duplicated_by_value_and_never_drifts test
64
+ # (see that engine module's own docstring for the full argument). Formerly
65
+ # hand-duplicated separately inside ambit_map/orchestrate.py;
66
+ # orchestrate.py now aliases this tuple instead of carrying its own
67
+ # literal.
68
+ # ===========================================================================
69
+ ENGINE_DEFAULT_INCLUDE_EXT = (
70
+ ".py", ".js", ".ts", ".tsx", ".jsx", ".java", ".jsp", ".jspf",
71
+ ".css", ".html", ".go", ".rb", ".php", ".cs", ".sql", ".sh",
72
+ ".c", ".h",
73
+ )
74
+
75
+ # The extra extensions a mapllm run admits beyond the shared engine default
76
+ # above -- world-popularity order per the founder's list, the C++
77
+ # foundation first (formerly ambit_map/orchestrate.py's own
78
+ # MAPLLM_EXTRA_INCLUDE_EXT literal; this constant is not part of
79
+ # ambitgraph/langs.py at all -- it is this product's OWN scope extension,
80
+ # mirrored here only so orchestrate.py can source every admission fact
81
+ # from this one file). ".c"/".h" left this tuple once they gained a real
82
+ # LangSpec: they are ENGINE_DEFAULT_INCLUDE_EXT members above now, not a
83
+ # mapllm-only widening.
84
+ MAPLLM_EXTRA_INCLUDE_EXT = (".cpp", ".hpp", ".rs")
85
+
86
+
87
+ # ===========================================================================
88
+ # Naming's product-side grouping (CENSUS fact 2's third spelling) --
89
+ # formerly ambit_map/registry.py's own JS_LANGS/PY_LANGS literals, the pair
90
+ # kind_key's src_lang branch reads. READER-TOLERANCE VOCABULARY (see module
91
+ # docstring): today ambitgraph/symbols.py's build_file_plane (the only
92
+ # writer of a file row's `lang` field) folds .jsx/.mjs/.cjs/.ts/.tsx all
93
+ # into the single tag "js" -- so "jsx"/"mjs"/"cjs"/"ts"/"tsx" can never
94
+ # appear as a REAL `lang` value THIS engine emits today. They stay in
95
+ # JS_LANGS anyway: an older or foreign producer's symbols.json is free to
96
+ # spell it differently under this product's own producer-portability
97
+ # contract, and ambit_map/load.py's load_run reads `lang` with no
98
+ # vocabulary gate at all. PY_LANGS carries no such extra member -- "py" is
99
+ # the only tag ambitgraph/langs.py's "py" LangSpec ever declares.
100
+ #
101
+ # JAVA_LANGS (Java W1, 2026-09-02, additive): the identical one-tag
102
+ # grouping shape as PY_LANGS -- "java" is the only tag ambitgraph/langs.py's
103
+ # "java" LangSpec ever declares, so this carries no reader-tolerance member
104
+ # either. Exists as its own named grouping (never folded into JS_LANGS or
105
+ # PY_LANGS) because kind_key's import branch needs a THIRD disjoint
106
+ # src_lang bucket once include/import.java joins include/import.py and
107
+ # include/import.js.
108
+ # ===========================================================================
109
+ JS_LANGS = ("js", "jsx", "mjs", "cjs", "ts", "tsx")
110
+ PY_LANGS = ("py",)
111
+ JAVA_LANGS = ("java",)
112
+ # C_LANGS: the identical one-tag grouping shape as PY_LANGS/JAVA_LANGS --
113
+ # "c" is the only tag ambitgraph/langs.py's "c" LangSpec ever declares
114
+ # (its naming_exts {".c", ".h"} both fold to that one tag), so this
115
+ # carries no reader-tolerance member either. Read by kind_key's include
116
+ # branch, which maps a C preprocessor include onto the include/static row
117
+ # (see registry.py's own comment there), and by the unit story's type-kind
118
+ # count gate.
119
+ C_LANGS = ("c",)
120
+
121
+ # TYPE_SYMBOL_KINDS (Java W1 repair -- the wall-legal PRODUCT-side
122
+ # mirror of ambitgraph/codelib.py's own _TYPE_SYMBOL_KINDS): the literal
123
+ # kind strings symbols.py's own class alternation emits for a Java type
124
+ # declaration ("class"/"interface"/"enum"/"record") -- "constructor" is
125
+ # DELIBERATELY not a member, the same exclusion codelib.py's own set
126
+ # states (a constructor is a member OF a type, never itself a type
127
+ # declaration). Consumed by ambit_map/unitpage.py's own class-count
128
+ # bucket so a Java interface/enum/record renders in the class(es) count
129
+ # on the unit story, not silently in "other named symbol(s)". Diffed
130
+ # against ambitgraph.codelib._TYPE_SYMBOL_KINDS by
131
+ # tests/test_langs_mirror_diff.py (DIFFED_FACT_NAMES entry
132
+ # "type_symbol_kinds") -- one origin (symbols.py's own kind literals), two
133
+ # independently-declared copies, never a cross-wall import.
134
+ TYPE_SYMBOL_KINDS = ("class", "interface", "enum", "record")
135
+
136
+ # C_TYPE_SYMBOL_KINDS (the wall-legal PRODUCT-side mirror of
137
+ # ambitgraph/codelib.py's own _C_TYPE_SYMBOL_KINDS): the literal kind
138
+ # strings symbols.py's extract_c_symbols emits for a C type declaration --
139
+ # a tagged bodied "struct"/"union"/"enum", and "typedef". "function" is
140
+ # deliberately not a member (it is the callable half, the same boundary
141
+ # TYPE_SYMBOL_KINDS draws against "constructor"). Consumed by
142
+ # ambit_map/unitpage.py's class-count bucket under the C_LANGS gate above:
143
+ # "enum" is a member of BOTH this tuple and TYPE_SYMBOL_KINDS, and the two
144
+ # language gates are disjoint, so a TypeScript enum still renders as
145
+ # "other named symbol(s)" while a C enum counts as a type. Diffed against
146
+ # ambitgraph.codelib._C_TYPE_SYMBOL_KINDS by tests/test_langs_mirror_diff.py
147
+ # (DIFFED_FACT_NAMES entry "c_type_symbol_kinds") -- one origin
148
+ # (symbols.py's own kind literals), two independently-declared copies,
149
+ # never a cross-wall import.
150
+ C_TYPE_SYMBOL_KINDS = ("struct", "union", "enum", "typedef")
151
+
152
+ # NAMESPACE_LANGS (Java W1 repair -- the wall-legal PRODUCT-side mirror of
153
+ # ambitgraph/langs.py's own NAMESPACE_LANGS): the `lang` tags whose units
154
+ # get the java-namespace treatment (the languages whose LangSpec declares
155
+ # namespace=True). Consumed by ambit_map/unitpage.py's class-count bucket
156
+ # as the language gate on TYPE_SYMBOL_KINDS above -- symbols.py emits the
157
+ # SAME literal kinds ("interface"/"enum") for TypeScript declarations,
158
+ # which must keep rendering as "other named symbol(s)", exactly the
159
+ # boundary ambitgraph/codelib.py draws with its own is_java_namespace
160
+ # check at export. Diffed against ambitgraph.langs.NAMESPACE_LANGS by
161
+ # tests/test_langs_mirror_diff.py (DIFFED_FACT_NAMES entry
162
+ # "namespace_langs") -- one origin (the LangSpec namespace flags), two
163
+ # independently-declared copies, never a cross-wall import.
164
+ NAMESPACE_LANGS = frozenset({"java", "jsp"})
165
+
166
+
167
+ # ===========================================================================
168
+ # CENSUS fact 8 -- formerly ambit_map/registry.py's own ROUTEDNESS_LANGS
169
+ # literal. READER-TOLERANCE VOCABULARY, same law as JS_LANGS above: "jspf"
170
+ # can never appear as a real `lang` value today (ambitgraph/symbols.py's
171
+ # build_file_plane folds .jspf into the tag "jsp", never into "jspf"
172
+ # itself) but stays for the identical producer-portability reason.
173
+ # Consumed DIRECTLY by ambit_map/library.py's routedness gates
174
+ # (_routedness_is_silent, _blind_for_run) via `langm.ROUTEDNESS_LANGS` --
175
+ # no production call site reads it through registry.py anymore.
176
+ # ambit_map/registry.py still re-exports the name (`ROUTEDNESS_LANGS =
177
+ # langm.ROUTEDNESS_LANGS`), kept alive for backward compatibility and
178
+ # because tests/test_langs_value_pins.py's own R4.0 locked oracle pin
179
+ # still asserts `reg.ROUTEDNESS_LANGS` by value.
180
+ # ===========================================================================
181
+ ROUTEDNESS_LANGS = ("jsp", "jspf", "java")
182
+
183
+
184
+ # ===========================================================================
185
+ # CENSUS fact 4's product-side mirror -- formerly ambit_map/paths.py's own
186
+ # KIND_READER_LANGS literal (the full reader-coverage matrix: 19
187
+ # ALLOWED_KINDS rows -> the Unit.lang frozenset each kind's reader covers).
188
+ # Java W1 (2026-09-02): the 19th row, include/import.java, is additive --
189
+ # every pre-existing row's value is UNCHANGED. Values UNCHANGED otherwise;
190
+ # the "why" narrative for each individual row (which real reader function
191
+ # backs it, what its own language gate looks like) stays in
192
+ # ambit_map/paths.py's own module comment beside its KIND_READER_LANGS
193
+ # alias -- this module only carries the literal.
194
+ #
195
+ # _kind_applicable's THREE NAMED DIVERGENCES (ambit_map/paths.py, unmoved
196
+ # -- these are about HOW paths.py USES this table, not part of the table's
197
+ # own values, so the overriding logic and its own inputs (TERMINAL_KINDS,
198
+ # MOUNT_CAPABLE_PACKS) stay declared in paths.py, not here):
199
+ # 1. TERMINAL_KINDS (table/read|write|ddl) -- LANGUAGE-AGNOSTIC by the
200
+ # real reader's own construction (ambitgraph/tables.py's build_tables
201
+ # carries no language gate at all): applicable whenever the model
202
+ # holds any unit at all -- this table's own three TERMINAL rows are
203
+ # never actually consulted for that decision.
204
+ # 2. "reference/script" -- unconditionally False: CONFIG-gated on
205
+ # ScanConfig.asset_roots, never language-gated, and this product's own
206
+ # scan config never sets asset_roots -- so this row's frozenset below
207
+ # is never actually consulted either; kept only so this dict's key set
208
+ # still equals paths.ALLOWED_KINDS.
209
+ # 3. "route/mount" -- language overlap against this row IS consulted, but
210
+ # is NECESSARY, not SUFFICIENT: applicability additionally requires at
211
+ # least one of MOUNT_CAPABLE_PACKS (express_pack/fastapi_pack/
212
+ # flask_pack) to have DETECTED this run.
213
+ # ===========================================================================
214
+ KIND_READER_LANGS = {
215
+ # "c" joins this row rather than opening a twentieth: the C
216
+ # preprocessor's own `#include` IS a static include -- the target is a
217
+ # literal read from the source text and never varies at runtime, which
218
+ # is this row's whole claim -- so registry.kind_key maps a C include
219
+ # edge here instead of minting a row whose sentence would repeat
220
+ # include/static's word for word. A C repo's include reader is
221
+ # therefore named as having run, where a missing member would report
222
+ # the include plane unmeasured on every C repo forever.
223
+ "include/static": frozenset({"jsp", "c"}),
224
+ "include/import.py": frozenset({"py"}),
225
+ "include/import.js": frozenset({"js"}),
226
+ # Java W1 (2026-09-02), additive: langs.py's java LangSpec gains
227
+ # include_lane=True this wave -- the row sits beside its import.py/
228
+ # import.js siblings, same shape, one reader language.
229
+ "include/import.java": frozenset({"java"}),
230
+ "include/require": frozenset({"js"}),
231
+ "include/dynamic": frozenset({"jsp"}),
232
+ "include/reexport": frozenset({"js"}),
233
+ "include/dynamic-import": frozenset({"js"}),
234
+ "include/member-resolved": frozenset({"js"}),
235
+ # The Java exact-evidence call tiers (constructor resolution,
236
+ # static-receiver resolution, static-import binding) record PROVEN
237
+ # rows into this same "calls" kind: no new kind key, no new lang
238
+ # tag, no new symbol kind, and no new blind-prose word crosses the
239
+ # wall for them, so nothing about them enters this mirror or the
240
+ # DIFFED_FACT_NAMES set. Their twelve coverage keys -- the nine
241
+ # refusal/correction counters (java_ctor_target_missing,
242
+ # java_ctor_qualified_unresolved, java_expr_new_refused,
243
+ # java_static_receiver_undeclared,
244
+ # java_static_receiver_multisegment, java_static_import_overrides,
245
+ # java_static_import_target_missing,
246
+ # java_declaration_window_refused, java_annotation_window_refused)
247
+ # and the java_call_counter_samples/_files/_samples_omitted
248
+ # attribution trio -- are engine-side coverage keys with no
249
+ # product-side reader. A matched-marked calls carrier, if one
250
+ # ever lands, arrives as its OWN registry kind row and gains its
251
+ # own entry here beside this one.
252
+ # "c" joins this row with the C call lane (langs.py's c LangSpec
253
+ # declares call_lane=True and edges.py builds C its own definer pool):
254
+ # without the member a C repo would be told the calls reader never
255
+ # heard of C, the exact defect this table's own NAMESPACE_LANGS
256
+ # comment records for java.
257
+ "calls": frozenset({"jsp", "java", "py", "js", "c"}),
258
+ "reference/script": frozenset({"jsp"}),
259
+ "route/mount": frozenset({"py", "js"}),
260
+ "ui/component-use": frozenset({"js"}),
261
+ "net/fetch-route": frozenset({"js", "py"}),
262
+ "ui/component-use-matched": frozenset({"js"}),
263
+ "event/emit": frozenset({"js"}),
264
+ "table/read": frozenset({"sql", "js", "py", "jsp"}),
265
+ "table/write": frozenset({"sql", "js", "py", "jsp"}),
266
+ "table/ddl": frozenset({"sql", "js", "py", "jsp"}),
267
+ }
268
+
269
+
270
+ # ===========================================================================
271
+ # CENSUS fact 2's product-side mirror -- formerly ambit_map/paths.py's own
272
+ # _GRAMMAR_EXTS literal, a hand-kept copy of ambitgraph/symbols.py's
273
+ # _LANG_FOR_EXT key set (exactly the extensions with a real comment-
274
+ # blanking grammar). Unlike JS_LANGS/PY_LANGS/ROUTEDNESS_LANGS above, this
275
+ # set carries NO reader-tolerance members: it is a copy of the ENGINE's own
276
+ # key set (every ambitgraph/langs.py LangSpec's naming_exts, unioned), not
277
+ # a copy of a possibly-foreign `lang` value's vocabulary.
278
+ # ===========================================================================
279
+ _GRAMMAR_EXTS = frozenset({
280
+ ".jsp", ".jspf", ".java", ".js", ".mjs", ".cjs", ".jsx", ".ts", ".tsx",
281
+ ".py", ".sh", ".sql", ".c", ".h",
282
+ })
283
+
284
+
285
+ # ===========================================================================
286
+ # CENSUS fact 8's disclosure-gate vocabulary -- THE PER-SENTENCE LANGUAGE
287
+ # TAG, product side. Formerly ambit_map/library.py's own
288
+ # _BLIND_SENTENCE_LANGS/_blind_langs pair; it lives here, with every other
289
+ # cross-wall copy, and library.py aliases the reader.
290
+ #
291
+ # The engine declares each blind sentence's own languages AT the sentence
292
+ # (ambitgraph.symbols.BlindSentence's `langs`, empty meaning the sentence
293
+ # holds for every run). That attribute cannot reach this side of the wall:
294
+ # this package reads a run's ARTIFACT FILES, and json.loads hands back
295
+ # plain strings, so the tag is mirrored here -- the same shape
296
+ # ambit_map/registry.py's CALL_BLIND_MATCH already uses, an opening
297
+ # fragment of one engine sentence held as a stable anchor.
298
+ # tests/test_blind_sentence_langs.py diffs the two declarations entry by
299
+ # entry, in both directions, so a sentence tagged on one side and not the
300
+ # other is red, never a page quietly telling a reader a law about a
301
+ # language it never scanned.
302
+ #
303
+ # Each row is (anchor, langs): the anchor is a fragment no other blind
304
+ # sentence contains, and `langs` is that sentence's own `lang` tags. A
305
+ # sentence no row matches is universal and renders on every run.
306
+ #
307
+ # TWO KINDS OF ANCHOR, one shape. The static sentences
308
+ # (ambitgraph.symbols.BLIND_SENTENCES) are anchored on their opening
309
+ # words. The C include lane's four sentences are built per scan and carry
310
+ # their counts at the END, so each is anchored on the FIXED OPENING PREFIX
311
+ # ambitgraph/edges.py declares beside it -- retyped here by value, exactly
312
+ # as this module retypes every other engine literal, because the wall
313
+ # forbids asking the engine for it. The prefix constants and these rows are
314
+ # held equal by the seam tests, never by an import.
315
+ #
316
+ # The prose-word sniffing this field used to carry (BLIND_LANG_PROSE_WORDS
317
+ # plus library.py's compiled _BLIND_LANG_FILTER) is retired: it was an
318
+ # all-or-nothing guess read off a sentence's English, wrong in both
319
+ # directions (Java-only pages rendering C sentences, C-only pages dropping
320
+ # their own call-coverage note), and the tag rows below are the declared
321
+ # answer that replaces it.
322
+ # ===========================================================================
323
+ _JSP_JAVA = frozenset({"jsp", "jspf", "java"})
324
+ _C = frozenset({"c"})
325
+ BLIND_SENTENCE_LANGS = (
326
+ ("Java declarations inside JSP scriptlet blocks", _JSP_JAVA),
327
+ ("A Java class whose modifiers are written on their own line", _JSP_JAVA),
328
+ ("A Java method declared inside an ANONYMOUS class body", _JSP_JAVA),
329
+ ("A Java annotation-type declaration", _JSP_JAVA),
330
+ ("A Java declaration with an annotation ANYWHERE on the declaration",
331
+ _JSP_JAVA),
332
+ ("In Java, a constructor or static-receiver call can bind", _JSP_JAVA),
333
+ ("A Java method inherited from a supertype", _JSP_JAVA),
334
+ ("An anonymous class creation over a Java interface or class", _JSP_JAVA),
335
+ ("A Java receiver separated from its method call by a line break",
336
+ _JSP_JAVA),
337
+ ("Java call and type names under four characters", _JSP_JAVA),
338
+ ("For Java, three exact-evidence tiers run before the name-match",
339
+ _JSP_JAVA),
340
+ # The call-coverage sentence names the C lane beside the JSP/Java one:
341
+ # a C-only run draws call edges and must keep the note that says how
342
+ # they were read. Anchor duplicated by value from
343
+ # ambit_map/registry.py's CALL_BLIND_MATCH (this module imports
344
+ # nothing, not even its own package's registry, which imports IT).
345
+ ("call sites are read for the JSP/Java namespace", _JSP_JAVA | _C),
346
+ # The C declaration grammar's own gaps (ambitgraph.symbols.
347
+ # BLIND_SENTENCES), anchored on their opening words.
348
+ ("A C `#define` is never read as a symbol", _C),
349
+ ("A function that a macro DEFINES is not read at all", _C),
350
+ ("Conditional and macro-body text is read exactly as written", _C),
351
+ ("A K&R-style C definition", _C),
352
+ ("A C definition is read at column 0 and brace depth 0 only", _C),
353
+ ("A bodied `struct`/`union`/`enum` with no tag", _C),
354
+ ("Only C definitions are read, never declarations", _C),
355
+ # The C include lane's four per-scan sentences, anchored on
356
+ # ambitgraph/edges.py's C_ANGLE_INCLUDE_SENTENCE_PREFIX,
357
+ # C_QUOTED_UNRESOLVED_SYSTEM_SENTENCE_PREFIX,
358
+ # C_QUOTED_UNRESOLVED_ABSENT_SENTENCE_PREFIX and
359
+ # C_QUOTED_UNRESOLVED_SENTENCE_PREFIX, in that order.
360
+ ("Angle-bracket C includes are never drawn", _C),
361
+ ("A quoted C include naming a C standard library header", _C),
362
+ ("A quoted C include with no candidate anywhere in this scan", _C),
363
+ ("A quoted C include this scan does hold a file for, but position "
364
+ "does not license,", _C),
365
+ )
366
+
367
+
368
+ def blind_langs(sentence):
369
+ """The `lang` tags one blind sentence is about -- the union of every
370
+ anchor it carries, empty when it names no language at all. Empty is
371
+ the universal answer, not the unknown one: a sentence this vocabulary
372
+ does not describe is a sentence about the instrument, true on every
373
+ run, so it is kept rather than guessed at.
374
+
375
+ The one function in this module: it reads only the table right above
376
+ it and the string handed to it, so it stays as dependency-free as
377
+ every literal here."""
378
+ out = set()
379
+ for anchor, tags in BLIND_SENTENCE_LANGS:
380
+ if anchor in sentence:
381
+ out |= tags
382
+ return frozenset(out)
@@ -2,8 +2,9 @@
2
2
  screens (U1 rewrite, wave/u1-library-home): the COMPOSITION is now built on
3
3
  ambit_map/objects.py's object kit and ambit_map/families.py's human family
4
4
  taxonomy. No file I/O, no clock, no server. Imports: registry, objects,
5
- families (same package) + collections, threading (stdlib -- W7, the
6
- _resolved_mounts cache below; see that cache's own module comment).
5
+ families, and (R4) langmirror (same package) + collections, threading
6
+ (stdlib -- W7, the _resolved_mounts cache below; see that cache's own
7
+ module comment).
7
8
 
8
9
  U3 (wave/u3-graph, U3-A): the transitional chrome fence that kept the old
9
10
  `_hero`/`_block`/`_note`/... renderers alive for graph.py's own sake is
@@ -21,12 +22,12 @@ per (model, dialects) OBJECT IDENTITY, file-local to this module -- see the
21
22
  comment above `_MOUNTS_CACHE_MAXSIZE` below for the full argument.
22
23
  """
23
24
  import collections
24
- import re
25
25
  import threading
26
26
 
27
27
  from . import registry as reg
28
28
  from . import objects as ob
29
29
  from . import families as fam
30
+ from . import langmirror as langm
30
31
 
31
32
  # ---------------------------------------------------------------------------
32
33
  # Cap constants
@@ -66,7 +67,12 @@ JUDGED_HOME_ROWS_CAP = 24
66
67
  JUDGED_FOLD_ROWS_CAP = 8
67
68
  JUDGED_UNJOINED_CAP = 24
68
69
 
69
- _COUNTED_SYMBOL_KINDS = ("function", "method", "class")
70
+ # R4.4-product: _COUNTED_SYMBOL_KINDS deleted here (was `("function",
71
+ # "method", "class")`) -- grep-verified ZERO references tree-wide before
72
+ # deletion (JUDGES-DIGEST.txt LENS 3 finding: dead code, not a shared
73
+ # vocabulary with this module's own _fn_count or unitpage.py's own
74
+ # per-unit symbol partition -- see those two sites' own comments, both
75
+ # left untouched).
70
76
  _SLUG_OK = set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")
71
77
 
72
78
 
@@ -528,7 +534,7 @@ def _routedness_is_silent(model):
528
534
  servlet container's web.xml is not a thing that could exist)."""
529
535
  units = model.units.values()
530
536
  for u in units:
531
- if u.lang in reg.ROUTEDNESS_LANGS:
537
+ if u.lang in langm.ROUTEDNESS_LANGS:
532
538
  return False
533
539
  for u in units:
534
540
  r = u.routedness
@@ -883,33 +889,38 @@ def _method_note1(dialects):
883
889
  return ob.note(lead + "".join(clauses))
884
890
 
885
891
 
886
- # Word-boundary, not substring: a bare `"jsp" in s.lower()` also matches
887
- # every sentence that merely contains "jsp" as part of a longer token, and
888
- # a bare "java" substring check would ALSO match "javascript"/"javadoc" --
889
- # neither is the language name this filter exists to drop. `\bjava\b`
890
- # never matches "javascript" (no boundary follows "java" there) or
891
- # "javadoc" (same reason), so only the standalone word is caught.
892
- _BLIND_LANG_FILTER = re.compile(r"\bjsp\b|\bjava\b", re.IGNORECASE)
892
+ # THE PER-SENTENCE LANGUAGE TAG's product-side reader. The table it reads
893
+ # (langmirror.BLIND_SENTENCE_LANGS) and this function are declared together
894
+ # in ambit_map/langmirror.py, beside every other copy of an engine fact
895
+ # that crosses the wall; this alias is what `_blind_for_run` below calls,
896
+ # the same alias-only shape registry.py's JS_LANGS/PY_LANGS already use.
897
+ _blind_langs = langm.blind_langs
893
898
 
894
899
 
895
900
  def _blind_for_run(cov, model):
896
901
  """`_combined_blind` (DATA/LOGIC, kept verbatim -- other call sites in
897
902
  this module still want the RAW list) always carries the engine's
898
- static, always-present disclosures, including the JSP/Java-scriptlet
899
- and JSP/Java-namespace sentences -- ambitgraph/symbols.py:43-46 says so
900
- explicitly ("always present, regardless of what the scanned tree
901
- happens to contain"). Review fix (major): on a run with zero jsp/jspf/
902
- java units those two sentences named a language the page never scanned
903
- (chatbot-ui, flask), so THIS caller -- the only live caller of
904
- `_combined_blind` in the new render_library flow -- drops any sentence
905
- naming JSP or Java when the run holds no jsp/jspf/java unit at all. The
906
- RAW list (with those sentences) still reaches `_find_blind`'s other
907
- callers unfiltered, since ZERO_DDL_MATCH/CALL_BLIND_MATCH need the
908
- full list regardless of language mix."""
903
+ static, always-present disclosures, whatever the scanned tree happens
904
+ to contain -- the comment above ambitgraph.symbols.BLIND_SENTENCES
905
+ says so explicitly. A sentence naming a language is a true statement
906
+ only about a run that holds a unit of that language, so this caller
907
+ keeps a sentence when it is universal (no tag) or its tags meet the
908
+ run's own languages, and drops it otherwise.
909
+
910
+ Per sentence, not per run: a run holding one tagged language keeps
911
+ that language's sentences and still drops another language's, which
912
+ an all-or-nothing gate on the run could not express. The RAW list
913
+ still reaches `_find_blind`'s other callers unfiltered -- the graph
914
+ page's no-edges note states the call-graph law on runs of every
915
+ language mix."""
909
916
  blind = _combined_blind(cov)
910
- if any(u.lang in reg.ROUTEDNESS_LANGS for u in model.units.values()):
911
- return blind
912
- return [s for s in blind if not _BLIND_LANG_FILTER.search(s)]
917
+ run_langs = {u.lang for u in model.units.values()}
918
+ out = []
919
+ for s in blind:
920
+ tags = _blind_langs(s)
921
+ if not tags or (tags & run_langs):
922
+ out.append(s)
923
+ return out
913
924
 
914
925
 
915
926
  def _method_note2(model, envelope, grammarless_table=False):
@@ -295,12 +295,35 @@ def load_run(run_dir):
295
295
  line = row.get("line") if isinstance(row.get("line"), int) else 0
296
296
  if not row.get("resolved"):
297
297
  if src in unresolved_acc:
298
- # R3/R9 (W7a, additive): "unresolved_reason" is a
299
- # NEW, optional key ambitgraph/edges.py sets only
300
- # on a Python absolute import HELD on the
301
- # cross-root tier (R9) -- absent (the row.get
302
- # default) on every ordinary unresolved row,
303
- # exactly as before this wave.
298
+ # R3/R9 (W7a, additive) + Java W1 (D3, "machine
299
+ # tokens are interfaces" law; load.py repair):
300
+ # "unresolved_reason" is an OPTIONAL
301
+ # key ambitgraph/edges.py sets on an unresolved
302
+ # row it can name a SPECIFIC held/refused reason
303
+ # for -- absent (the row.get default) on every
304
+ # ORDINARY unresolved row (a target this scan
305
+ # genuinely never read at all), exactly as
306
+ # before R9. THE THREE TOKENS this key actually
307
+ # carries today, this being the one place any of
308
+ # them is parsed off the wire: "cross_root_
309
+ # import_held" (a Python absolute import R9-held
310
+ # on the cross-root tier, OR a Java import
311
+ # HELD the same way -- java's tier reuses this
312
+ # SAME token, never a java-named twin, per D3);
313
+ # "wildcard_import" (a Java plain wildcard import
314
+ # -- `import com.foo.*;` names a package, never a
315
+ # single file, so it is never even tried against
316
+ # the resolver); "importer_root_underivable" (a
317
+ # Java file whose OWN package root could not be
318
+ # derived -- a default-package file, or one whose
319
+ # directory does not match its `package`
320
+ # declaration's suffix -- so none of its imports
321
+ # is tried against either resolution tier at
322
+ # all). registry.py's unresolved_reason_sentence
323
+ # is the dispatch that maps these three literals
324
+ # to their sentences, for every caption site
325
+ # (graph.py's _quarantine_note, unitpage.py's
326
+ # connections block).
304
327
  reason = row.get("unresolved_reason")
305
328
  unresolved_acc[src].append(
306
329
  model.UnresolvedRef(
@@ -24,6 +24,12 @@ from . import library as lib
24
24
  from . import model as model_mod
25
25
 
26
26
  _DEFINER_KINDS = ("function", "class")
27
+ # Java W1 repair (2026-09-02): DELIBERATELY unwidened this wave --
28
+ # symbols.py's D2 closure now also emits "interface"/"enum"/"record", but
29
+ # whether any of those should ever be a call-binding DEFINER is roadmap
30
+ # material (JAVA-W1-DESIGN.md v2's own "out of W1": "call-binding via
31
+ # imports"), not decided here. Left exactly as it was pre-wave; not an
32
+ # oversight.
27
33
 
28
34
 
29
35
  def _collapse_matched_edges(edges):