ambitgraph 0.3.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 (91) hide show
  1. {ambitgraph-0.3.0/ambitgraph.egg-info → ambitgraph-0.5.0}/PKG-INFO +1 -1
  2. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambit_map/export.py +6 -0
  3. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambit_map/graph.py +73 -0
  4. ambitgraph-0.5.0/ambit_map/langmirror.py +471 -0
  5. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambit_map/library.py +26 -31
  6. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambit_map/orchestrate.py +8 -7
  7. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambit_map/page.py +62 -1
  8. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambit_map/registry.py +229 -7
  9. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambit_map/serve.py +31 -1
  10. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambit_map/theme.py +4 -0
  11. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambit_map/unitpage.py +36 -2
  12. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/__init__.py +1 -1
  13. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/cli.py +15 -12
  14. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/codelib.py +204 -6
  15. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/edges.py +1624 -16
  16. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/langs.py +107 -40
  17. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/portal.py +42 -2
  18. ambitgraph-0.5.0/ambitgraph/prompts.py +88 -0
  19. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/shell.py +145 -28
  20. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/symbols.py +1552 -45
  21. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/urcode.py +11 -20
  22. {ambitgraph-0.3.0 → ambitgraph-0.5.0/ambitgraph.egg-info}/PKG-INFO +1 -1
  23. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph.egg-info/SOURCES.txt +1 -0
  24. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/pyproject.toml +1 -1
  25. ambitgraph-0.3.0/ambit_map/langmirror.py +0 -243
  26. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/LICENSE +0 -0
  27. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/MANIFEST.in +0 -0
  28. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/README.md +0 -0
  29. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambit_map/__init__.py +0 -0
  30. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambit_map/__main__.py +0 -0
  31. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambit_map/ambit-map-enrich.md +0 -0
  32. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambit_map/ambit-mapllm-contract.md +0 -0
  33. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambit_map/cli.py +0 -0
  34. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambit_map/daemon.py +0 -0
  35. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambit_map/dialects/__init__.py +0 -0
  36. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambit_map/dialects/contract.py +0 -0
  37. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambit_map/dialects/express_pack.py +0 -0
  38. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambit_map/dialects/fastapi_pack.py +0 -0
  39. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambit_map/dialects/flask_pack.py +0 -0
  40. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambit_map/dialects/nextjs_pack.py +0 -0
  41. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambit_map/dialects/supabase_pack.py +0 -0
  42. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambit_map/families.py +0 -0
  43. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambit_map/island_layout.py +0 -0
  44. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambit_map/islands.py +0 -0
  45. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambit_map/judged.py +0 -0
  46. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambit_map/kitdemo.py +0 -0
  47. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambit_map/load.py +0 -0
  48. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambit_map/mapllm.py +0 -0
  49. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambit_map/matched.py +0 -0
  50. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambit_map/model.py +0 -0
  51. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambit_map/objects.py +0 -0
  52. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambit_map/paths.py +0 -0
  53. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/WALKTHROUGH.md +0 -0
  54. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/__main__.py +0 -0
  55. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/cockpit.py +0 -0
  56. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/config.py +0 -0
  57. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/console.py +0 -0
  58. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/demo.py +0 -0
  59. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/docs_discovery.py +0 -0
  60. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/doctor.py +0 -0
  61. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/duplication.py +0 -0
  62. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/emit.py +0 -0
  63. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/engine_identity.py +0 -0
  64. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/estimate.py +0 -0
  65. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/eval.py +0 -0
  66. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/feeds.py +0 -0
  67. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/gate.py +0 -0
  68. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/gate_check.py +0 -0
  69. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/gate_server.py +0 -0
  70. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/harvest.py +0 -0
  71. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/inventory.py +0 -0
  72. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/jscalls.py +0 -0
  73. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/mds.py +0 -0
  74. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/merge.py +0 -0
  75. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/merge_jsp.py +0 -0
  76. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/parameterize.py +0 -0
  77. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/pipeline.py +0 -0
  78. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/pycalls.py +0 -0
  79. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/ratify.py +0 -0
  80. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/report.py +0 -0
  81. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/reports.py +0 -0
  82. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/sample.py +0 -0
  83. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/secretary.py +0 -0
  84. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/stages.py +0 -0
  85. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/tables.py +0 -0
  86. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph/tasks.py +0 -0
  87. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph.egg-info/dependency_links.txt +0 -0
  88. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph.egg-info/entry_points.txt +0 -0
  89. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph.egg-info/requires.txt +0 -0
  90. {ambitgraph-0.3.0 → ambitgraph-0.5.0}/ambitgraph.egg-info/top_level.txt +0 -0
  91. {ambitgraph-0.3.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.3.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)
@@ -0,0 +1,471 @@
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
+ # 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
+
130
+ # TYPE_SYMBOL_KINDS (Java W1 repair -- the wall-legal PRODUCT-side
131
+ # mirror of ambitgraph/codelib.py's own _TYPE_SYMBOL_KINDS): the literal
132
+ # kind strings symbols.py's own class alternation emits for a Java type
133
+ # declaration ("class"/"interface"/"enum"/"record") -- "constructor" is
134
+ # DELIBERATELY not a member, the same exclusion codelib.py's own set
135
+ # states (a constructor is a member OF a type, never itself a type
136
+ # declaration). Consumed by ambit_map/unitpage.py's own class-count
137
+ # bucket so a Java interface/enum/record renders in the class(es) count
138
+ # on the unit story, not silently in "other named symbol(s)". Diffed
139
+ # against ambitgraph.codelib._TYPE_SYMBOL_KINDS by
140
+ # tests/test_langs_mirror_diff.py (DIFFED_FACT_NAMES entry
141
+ # "type_symbol_kinds") -- one origin (symbols.py's own kind literals), two
142
+ # independently-declared copies, never a cross-wall import.
143
+ TYPE_SYMBOL_KINDS = ("class", "interface", "enum", "record")
144
+
145
+ # C_TYPE_SYMBOL_KINDS (the wall-legal PRODUCT-side mirror of
146
+ # ambitgraph/codelib.py's own _C_TYPE_SYMBOL_KINDS): the literal kind
147
+ # strings symbols.py's extract_c_symbols emits for a C type declaration --
148
+ # a tagged bodied "struct"/"union"/"enum", and "typedef". "function" is
149
+ # deliberately not a member (it is the callable half, the same boundary
150
+ # TYPE_SYMBOL_KINDS draws against "constructor"). Consumed by
151
+ # ambit_map/unitpage.py's class-count bucket under the C_LANGS gate above:
152
+ # "enum" is a member of BOTH this tuple and TYPE_SYMBOL_KINDS, and the two
153
+ # language gates are disjoint, so a TypeScript enum still renders as
154
+ # "other named symbol(s)" while a C enum counts as a type. Diffed against
155
+ # ambitgraph.codelib._C_TYPE_SYMBOL_KINDS by tests/test_langs_mirror_diff.py
156
+ # (DIFFED_FACT_NAMES entry "c_type_symbol_kinds") -- one origin
157
+ # (symbols.py's own kind literals), two independently-declared copies,
158
+ # never a cross-wall import.
159
+ C_TYPE_SYMBOL_KINDS = ("struct", "union", "enum", "typedef")
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
+
181
+ # NAMESPACE_LANGS (Java W1 repair -- the wall-legal PRODUCT-side mirror of
182
+ # ambitgraph/langs.py's own NAMESPACE_LANGS): the `lang` tags whose units
183
+ # get the java-namespace treatment (the languages whose LangSpec declares
184
+ # namespace=True). Consumed by ambit_map/unitpage.py's class-count bucket
185
+ # as the language gate on TYPE_SYMBOL_KINDS above -- symbols.py emits the
186
+ # SAME literal kinds ("interface"/"enum") for TypeScript declarations,
187
+ # which must keep rendering as "other named symbol(s)", exactly the
188
+ # boundary ambitgraph/codelib.py draws with its own is_java_namespace
189
+ # check at export. Diffed against ambitgraph.langs.NAMESPACE_LANGS by
190
+ # tests/test_langs_mirror_diff.py (DIFFED_FACT_NAMES entry
191
+ # "namespace_langs") -- one origin (the LangSpec namespace flags), two
192
+ # independently-declared copies, never a cross-wall import.
193
+ NAMESPACE_LANGS = frozenset({"java", "jsp"})
194
+
195
+
196
+ # ===========================================================================
197
+ # CENSUS fact 8 -- formerly ambit_map/registry.py's own ROUTEDNESS_LANGS
198
+ # literal. READER-TOLERANCE VOCABULARY, same law as JS_LANGS above: "jspf"
199
+ # can never appear as a real `lang` value today (ambitgraph/symbols.py's
200
+ # build_file_plane folds .jspf into the tag "jsp", never into "jspf"
201
+ # itself) but stays for the identical producer-portability reason.
202
+ # Consumed DIRECTLY by ambit_map/library.py's routedness gates
203
+ # (_routedness_is_silent, _blind_for_run) via `langm.ROUTEDNESS_LANGS` --
204
+ # no production call site reads it through registry.py anymore.
205
+ # ambit_map/registry.py still re-exports the name (`ROUTEDNESS_LANGS =
206
+ # langm.ROUTEDNESS_LANGS`), kept alive for backward compatibility and
207
+ # because tests/test_langs_value_pins.py's own R4.0 locked oracle pin
208
+ # still asserts `reg.ROUTEDNESS_LANGS` by value.
209
+ # ===========================================================================
210
+ ROUTEDNESS_LANGS = ("jsp", "jspf", "java")
211
+
212
+
213
+ # ===========================================================================
214
+ # CENSUS fact 4's product-side mirror -- formerly ambit_map/paths.py's own
215
+ # KIND_READER_LANGS literal (the full reader-coverage matrix: 19
216
+ # ALLOWED_KINDS rows -> the Unit.lang frozenset each kind's reader covers).
217
+ # Java W1 (2026-09-02): the 19th row, include/import.java, is additive --
218
+ # every pre-existing row's value is UNCHANGED. Values UNCHANGED otherwise;
219
+ # the "why" narrative for each individual row (which real reader function
220
+ # backs it, what its own language gate looks like) stays in
221
+ # ambit_map/paths.py's own module comment beside its KIND_READER_LANGS
222
+ # alias -- this module only carries the literal.
223
+ #
224
+ # _kind_applicable's THREE NAMED DIVERGENCES (ambit_map/paths.py, unmoved
225
+ # -- these are about HOW paths.py USES this table, not part of the table's
226
+ # own values, so the overriding logic and its own inputs (TERMINAL_KINDS,
227
+ # MOUNT_CAPABLE_PACKS) stay declared in paths.py, not here):
228
+ # 1. TERMINAL_KINDS (table/read|write|ddl) -- LANGUAGE-AGNOSTIC by the
229
+ # real reader's own construction (ambitgraph/tables.py's build_tables
230
+ # carries no language gate at all): applicable whenever the model
231
+ # holds any unit at all -- this table's own three TERMINAL rows are
232
+ # never actually consulted for that decision.
233
+ # 2. "reference/script" -- unconditionally False: CONFIG-gated on
234
+ # ScanConfig.asset_roots, never language-gated, and this product's own
235
+ # scan config never sets asset_roots -- so this row's frozenset below
236
+ # is never actually consulted either; kept only so this dict's key set
237
+ # still equals paths.ALLOWED_KINDS.
238
+ # 3. "route/mount" -- language overlap against this row IS consulted, but
239
+ # is NECESSARY, not SUFFICIENT: applicability additionally requires at
240
+ # least one of MOUNT_CAPABLE_PACKS (express_pack/fastapi_pack/
241
+ # flask_pack) to have DETECTED this run.
242
+ # ===========================================================================
243
+ KIND_READER_LANGS = {
244
+ # "c" joins this row rather than opening a twentieth: the C
245
+ # preprocessor's own `#include` IS a static include -- the target is a
246
+ # literal read from the source text and never varies at runtime, which
247
+ # is this row's whole claim -- so registry.kind_key maps a C include
248
+ # edge here instead of minting a row whose sentence would repeat
249
+ # include/static's word for word. A C repo's include reader is
250
+ # therefore named as having run, where a missing member would report
251
+ # the include plane unmeasured on every C repo forever.
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"}),
259
+ "include/import.py": frozenset({"py"}),
260
+ "include/import.js": frozenset({"js"}),
261
+ # Java W1 (2026-09-02), additive: langs.py's java LangSpec gains
262
+ # include_lane=True this wave -- the row sits beside its import.py/
263
+ # import.js siblings, same shape, one reader language.
264
+ "include/import.java": frozenset({"java"}),
265
+ "include/require": frozenset({"js"}),
266
+ "include/dynamic": frozenset({"jsp"}),
267
+ "include/reexport": frozenset({"js"}),
268
+ "include/dynamic-import": frozenset({"js"}),
269
+ "include/member-resolved": frozenset({"js"}),
270
+ # The Java exact-evidence call tiers (constructor resolution,
271
+ # static-receiver resolution, static-import binding) record PROVEN
272
+ # rows into this same "calls" kind: no new kind key, no new lang
273
+ # tag, no new symbol kind, and no new blind-prose word crosses the
274
+ # wall for them, so nothing about them enters this mirror or the
275
+ # DIFFED_FACT_NAMES set. Their twelve coverage keys -- the nine
276
+ # refusal/correction counters (java_ctor_target_missing,
277
+ # java_ctor_qualified_unresolved, java_expr_new_refused,
278
+ # java_static_receiver_undeclared,
279
+ # java_static_receiver_multisegment, java_static_import_overrides,
280
+ # java_static_import_target_missing,
281
+ # java_declaration_window_refused, java_annotation_window_refused)
282
+ # and the java_call_counter_samples/_files/_samples_omitted
283
+ # attribution trio -- are engine-side coverage keys with no
284
+ # product-side reader. A matched-marked calls carrier, if one
285
+ # ever lands, arrives as its OWN registry kind row and gains its
286
+ # own entry here beside this one.
287
+ # "c" joins this row with the C call lane (langs.py's c LangSpec
288
+ # declares call_lane=True and edges.py builds C its own definer pool):
289
+ # without the member a C repo would be told the calls reader never
290
+ # heard of C, the exact defect this table's own NAMESPACE_LANGS
291
+ # comment records for java.
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"}),
296
+ "reference/script": frozenset({"jsp"}),
297
+ "route/mount": frozenset({"py", "js"}),
298
+ "ui/component-use": frozenset({"js"}),
299
+ "net/fetch-route": frozenset({"js", "py"}),
300
+ "ui/component-use-matched": frozenset({"js"}),
301
+ "event/emit": frozenset({"js"}),
302
+ "table/read": frozenset({"sql", "js", "py", "jsp"}),
303
+ "table/write": frozenset({"sql", "js", "py", "jsp"}),
304
+ "table/ddl": frozenset({"sql", "js", "py", "jsp"}),
305
+ }
306
+
307
+
308
+ # ===========================================================================
309
+ # CENSUS fact 2's product-side mirror -- formerly ambit_map/paths.py's own
310
+ # _GRAMMAR_EXTS literal, a hand-kept copy of ambitgraph/symbols.py's
311
+ # _LANG_FOR_EXT key set (exactly the extensions with a real comment-
312
+ # blanking grammar). Unlike JS_LANGS/PY_LANGS/ROUTEDNESS_LANGS above, this
313
+ # set carries NO reader-tolerance members: it is a copy of the ENGINE's own
314
+ # key set (every ambitgraph/langs.py LangSpec's naming_exts, unioned), not
315
+ # a copy of a possibly-foreign `lang` value's vocabulary.
316
+ # ===========================================================================
317
+ _GRAMMAR_EXTS = frozenset({
318
+ ".jsp", ".jspf", ".java", ".js", ".mjs", ".cjs", ".jsx", ".ts", ".tsx",
319
+ ".py", ".sh", ".sql", ".c", ".h", ".rb",
320
+ })
321
+
322
+
323
+ # ===========================================================================
324
+ # CENSUS fact 8's disclosure-gate vocabulary -- THE PER-SENTENCE LANGUAGE
325
+ # TAG, product side. Formerly ambit_map/library.py's own
326
+ # _BLIND_SENTENCE_LANGS/_blind_langs pair; it lives here, with every other
327
+ # cross-wall copy, and library.py aliases the reader.
328
+ #
329
+ # The engine declares each blind sentence's own languages AT the sentence
330
+ # (ambitgraph.symbols.BlindSentence's `langs`, empty meaning the sentence
331
+ # holds for every run). That attribute cannot reach this side of the wall:
332
+ # this package reads a run's ARTIFACT FILES, and json.loads hands back
333
+ # plain strings, so the tag is mirrored here -- the same shape
334
+ # ambit_map/registry.py's CALL_BLIND_MATCH already uses, an opening
335
+ # fragment of one engine sentence held as a stable anchor.
336
+ # tests/test_blind_sentence_langs.py diffs the two declarations entry by
337
+ # entry, in both directions, so a sentence tagged on one side and not the
338
+ # other is red, never a page quietly telling a reader a law about a
339
+ # language it never scanned.
340
+ #
341
+ # Each row is (anchor, langs): the anchor is a fragment no other blind
342
+ # sentence contains, and `langs` is that sentence's own `lang` tags. A
343
+ # sentence no row matches is universal and renders on every run.
344
+ #
345
+ # TWO KINDS OF ANCHOR, one shape. The static sentences
346
+ # (ambitgraph.symbols.BLIND_SENTENCES) are anchored on their opening
347
+ # words. The C include lane's four sentences are built per scan and carry
348
+ # their counts at the END, so each is anchored on the FIXED OPENING PREFIX
349
+ # ambitgraph/edges.py declares beside it -- retyped here by value, exactly
350
+ # as this module retypes every other engine literal, because the wall
351
+ # forbids asking the engine for it. The prefix constants and these rows are
352
+ # held equal by the seam tests, never by an import.
353
+ #
354
+ # The prose-word sniffing this field used to carry (BLIND_LANG_PROSE_WORDS
355
+ # plus library.py's compiled _BLIND_LANG_FILTER) is retired: it was an
356
+ # all-or-nothing guess read off a sentence's English, wrong in both
357
+ # directions (Java-only pages rendering C sentences, C-only pages dropping
358
+ # their own call-coverage note), and the tag rows below are the declared
359
+ # answer that replaces it.
360
+ # ===========================================================================
361
+ _JSP_JAVA = frozenset({"jsp", "jspf", "java"})
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"})
366
+ BLIND_SENTENCE_LANGS = (
367
+ ("Java declarations inside JSP scriptlet blocks", _JSP_JAVA),
368
+ ("A Java class whose modifiers are written on their own line", _JSP_JAVA),
369
+ ("A Java method declared inside an ANONYMOUS class body", _JSP_JAVA),
370
+ ("A Java annotation-type declaration", _JSP_JAVA),
371
+ ("A Java declaration with an annotation ANYWHERE on the declaration",
372
+ _JSP_JAVA),
373
+ ("In Java, a constructor or static-receiver call can bind", _JSP_JAVA),
374
+ ("A Java method inherited from a supertype", _JSP_JAVA),
375
+ ("An anonymous class creation over a Java interface or class", _JSP_JAVA),
376
+ ("A Java receiver separated from its method call by a line break",
377
+ _JSP_JAVA),
378
+ ("Java call and type names under four characters", _JSP_JAVA),
379
+ ("For Java, three exact-evidence tiers run before the name-match",
380
+ _JSP_JAVA),
381
+ # The call-coverage sentence names the C lane beside the JSP/Java one:
382
+ # a C-only run draws call edges and must keep the note that says how
383
+ # they were read. Anchor duplicated by value from
384
+ # ambit_map/registry.py's CALL_BLIND_MATCH (this module imports
385
+ # nothing, not even its own package's registry, which imports IT).
386
+ ("call sites are read for the JSP/Java namespace", _JSP_JAVA | _C),
387
+ # The C declaration grammar's own gaps (ambitgraph.symbols.
388
+ # BLIND_SENTENCES), anchored on their opening words.
389
+ ("A C `#define` is never read as a symbol", _C),
390
+ ("A function that a macro DEFINES is not read at all", _C),
391
+ ("Conditional and macro-body text is read exactly as written", _C),
392
+ ("A K&R-style C definition", _C),
393
+ ("A C definition is read at column 0 and brace depth 0 only", _C),
394
+ ("A bodied `struct`/`union`/`enum` with no tag", _C),
395
+ ("Only C definitions are read, never declarations", _C),
396
+ # The C include lane's four per-scan sentences, anchored on
397
+ # ambitgraph/edges.py's C_ANGLE_INCLUDE_SENTENCE_PREFIX,
398
+ # C_QUOTED_UNRESOLVED_SYSTEM_SENTENCE_PREFIX,
399
+ # C_QUOTED_UNRESOLVED_ABSENT_SENTENCE_PREFIX and
400
+ # C_QUOTED_UNRESOLVED_SENTENCE_PREFIX, in that order.
401
+ ("Angle-bracket C includes are never drawn", _C),
402
+ ("A quoted C include naming a C standard library header", _C),
403
+ ("A quoted C include with no candidate anywhere in this scan", _C),
404
+ ("A quoted C include this scan does hold a file for, but position "
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),
454
+ )
455
+
456
+
457
+ def blind_langs(sentence):
458
+ """The `lang` tags one blind sentence is about -- the union of every
459
+ anchor it carries, empty when it names no language at all. Empty is
460
+ the universal answer, not the unknown one: a sentence this vocabulary
461
+ does not describe is a sentence about the instrument, true on every
462
+ run, so it is kept rather than guessed at.
463
+
464
+ The one function in this module: it reads only the table right above
465
+ it and the string handed to it, so it stays as dependency-free as
466
+ every literal here."""
467
+ out = set()
468
+ for anchor, tags in BLIND_SENTENCE_LANGS:
469
+ if anchor in sentence:
470
+ out |= tags
471
+ return frozenset(out)