ambitgraph 0.3.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.3.0/ambitgraph.egg-info → ambitgraph-0.4.0}/PKG-INFO +1 -1
  2. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambit_map/langmirror.py +163 -24
  3. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambit_map/library.py +26 -31
  4. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambit_map/orchestrate.py +8 -7
  5. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambit_map/registry.py +128 -7
  6. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambit_map/unitpage.py +20 -2
  7. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/__init__.py +1 -1
  8. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/codelib.py +146 -6
  9. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/edges.py +1079 -16
  10. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/langs.py +61 -29
  11. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/symbols.py +823 -43
  12. {ambitgraph-0.3.0 → ambitgraph-0.4.0/ambitgraph.egg-info}/PKG-INFO +1 -1
  13. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/pyproject.toml +1 -1
  14. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/LICENSE +0 -0
  15. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/MANIFEST.in +0 -0
  16. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/README.md +0 -0
  17. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambit_map/__init__.py +0 -0
  18. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambit_map/__main__.py +0 -0
  19. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambit_map/ambit-map-enrich.md +0 -0
  20. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambit_map/ambit-mapllm-contract.md +0 -0
  21. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambit_map/cli.py +0 -0
  22. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambit_map/daemon.py +0 -0
  23. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambit_map/dialects/__init__.py +0 -0
  24. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambit_map/dialects/contract.py +0 -0
  25. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambit_map/dialects/express_pack.py +0 -0
  26. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambit_map/dialects/fastapi_pack.py +0 -0
  27. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambit_map/dialects/flask_pack.py +0 -0
  28. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambit_map/dialects/nextjs_pack.py +0 -0
  29. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambit_map/dialects/supabase_pack.py +0 -0
  30. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambit_map/export.py +0 -0
  31. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambit_map/families.py +0 -0
  32. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambit_map/graph.py +0 -0
  33. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambit_map/island_layout.py +0 -0
  34. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambit_map/islands.py +0 -0
  35. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambit_map/judged.py +0 -0
  36. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambit_map/kitdemo.py +0 -0
  37. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambit_map/load.py +0 -0
  38. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambit_map/mapllm.py +0 -0
  39. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambit_map/matched.py +0 -0
  40. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambit_map/model.py +0 -0
  41. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambit_map/objects.py +0 -0
  42. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambit_map/page.py +0 -0
  43. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambit_map/paths.py +0 -0
  44. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambit_map/serve.py +0 -0
  45. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambit_map/theme.py +0 -0
  46. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/WALKTHROUGH.md +0 -0
  47. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/__main__.py +0 -0
  48. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/cli.py +0 -0
  49. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/cockpit.py +0 -0
  50. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/config.py +0 -0
  51. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/console.py +0 -0
  52. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/demo.py +0 -0
  53. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/docs_discovery.py +0 -0
  54. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/doctor.py +0 -0
  55. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/duplication.py +0 -0
  56. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/emit.py +0 -0
  57. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/engine_identity.py +0 -0
  58. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/estimate.py +0 -0
  59. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/eval.py +0 -0
  60. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/feeds.py +0 -0
  61. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/gate.py +0 -0
  62. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/gate_check.py +0 -0
  63. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/gate_server.py +0 -0
  64. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/harvest.py +0 -0
  65. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/inventory.py +0 -0
  66. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/jscalls.py +0 -0
  67. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/mds.py +0 -0
  68. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/merge.py +0 -0
  69. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/merge_jsp.py +0 -0
  70. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/parameterize.py +0 -0
  71. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/pipeline.py +0 -0
  72. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/portal.py +0 -0
  73. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/pycalls.py +0 -0
  74. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/ratify.py +0 -0
  75. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/report.py +0 -0
  76. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/reports.py +0 -0
  77. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/sample.py +0 -0
  78. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/secretary.py +0 -0
  79. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/shell.py +0 -0
  80. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/stages.py +0 -0
  81. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/tables.py +0 -0
  82. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/tasks.py +0 -0
  83. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph/urcode.py +0 -0
  84. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph.egg-info/SOURCES.txt +0 -0
  85. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph.egg-info/dependency_links.txt +0 -0
  86. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph.egg-info/entry_points.txt +0 -0
  87. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph.egg-info/requires.txt +0 -0
  88. {ambitgraph-0.3.0 → ambitgraph-0.4.0}/ambitgraph.egg-info/top_level.txt +0 -0
  89. {ambitgraph-0.3.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.3.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
@@ -5,7 +5,7 @@ CENSUS.md).
5
5
  ONE file replacing five scatterings: ambit_map/orchestrate.py's admission
6
6
  copy + MAPLLM_EXTRA_INCLUDE_EXT, ambit_map/registry.py's JS_LANGS/PY_LANGS/
7
7
  ROUTEDNESS_LANGS, ambit_map/paths.py's KIND_READER_LANGS/_GRAMMAR_EXTS, and
8
- ambit_map/library.py's inline blind-filter regex vocabulary. Every value
8
+ ambit_map/library.py's own blind-sentence language vocabulary. Every value
9
9
  below is UNCHANGED from what its scattered original held -- this module
10
10
  only gives them one address; see each field's own comment for its prior
11
11
  location, and the module-docstring note below for R4's "no member
@@ -31,7 +31,9 @@ module's own admission field below continues rather than replaces.
31
31
 
32
32
  Unlike ambitgraph/langs.py (which imports `re` and `dataclasses` for its
33
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). Every
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
35
37
  consumer inside this package (orchestrate.py, paths.py, registry.py,
36
38
  library.py) imports IT; it imports NONE of them, so no existing consumer
37
39
  of registry.py/paths.py/library.py/orchestrate.py risks a cycle by their
@@ -67,16 +69,19 @@ writes.
67
69
  ENGINE_DEFAULT_INCLUDE_EXT = (
68
70
  ".py", ".js", ".ts", ".tsx", ".jsx", ".java", ".jsp", ".jspf",
69
71
  ".css", ".html", ".go", ".rb", ".php", ".cs", ".sql", ".sh",
72
+ ".c", ".h",
70
73
  )
71
74
 
72
75
  # The extra extensions a mapllm run admits beyond the shared engine default
73
- # above -- world-popularity order per the founder's list, the C/C++
76
+ # above -- world-popularity order per the founder's list, the C++
74
77
  # foundation first (formerly ambit_map/orchestrate.py's own
75
78
  # MAPLLM_EXTRA_INCLUDE_EXT literal; this constant is not part of
76
79
  # ambitgraph/langs.py at all -- it is this product's OWN scope extension,
77
80
  # mirrored here only so orchestrate.py can source every admission fact
78
- # from this one file).
79
- MAPLLM_EXTRA_INCLUDE_EXT = (".c", ".h", ".cpp", ".hpp", ".rs")
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")
80
85
 
81
86
 
82
87
  # ===========================================================================
@@ -104,6 +109,14 @@ MAPLLM_EXTRA_INCLUDE_EXT = (".c", ".h", ".cpp", ".hpp", ".rs")
104
109
  JS_LANGS = ("js", "jsx", "mjs", "cjs", "ts", "tsx")
105
110
  PY_LANGS = ("py",)
106
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",)
107
120
 
108
121
  # TYPE_SYMBOL_KINDS (Java W1 repair -- the wall-legal PRODUCT-side
109
122
  # mirror of ambitgraph/codelib.py's own _TYPE_SYMBOL_KINDS): the literal
@@ -120,6 +133,22 @@ JAVA_LANGS = ("java",)
120
133
  # independently-declared copies, never a cross-wall import.
121
134
  TYPE_SYMBOL_KINDS = ("class", "interface", "enum", "record")
122
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
+
123
152
  # NAMESPACE_LANGS (Java W1 repair -- the wall-legal PRODUCT-side mirror of
124
153
  # ambitgraph/langs.py's own NAMESPACE_LANGS): the `lang` tags whose units
125
154
  # get the java-namespace treatment (the languages whose LangSpec declares
@@ -183,7 +212,15 @@ ROUTEDNESS_LANGS = ("jsp", "jspf", "java")
183
212
  # flask_pack) to have DETECTED this run.
184
213
  # ===========================================================================
185
214
  KIND_READER_LANGS = {
186
- "include/static": frozenset({"jsp"}),
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"}),
187
224
  "include/import.py": frozenset({"py"}),
188
225
  "include/import.js": frozenset({"js"}),
189
226
  # Java W1 (2026-09-02), additive: langs.py's java LangSpec gains
@@ -195,7 +232,29 @@ KIND_READER_LANGS = {
195
232
  "include/reexport": frozenset({"js"}),
196
233
  "include/dynamic-import": frozenset({"js"}),
197
234
  "include/member-resolved": frozenset({"js"}),
198
- "calls": frozenset({"jsp", "java", "py", "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"}),
199
258
  "reference/script": frozenset({"jsp"}),
200
259
  "route/mount": frozenset({"py", "js"}),
201
260
  "ui/component-use": frozenset({"js"}),
@@ -219,25 +278,105 @@ KIND_READER_LANGS = {
219
278
  # ===========================================================================
220
279
  _GRAMMAR_EXTS = frozenset({
221
280
  ".jsp", ".jspf", ".java", ".js", ".mjs", ".cjs", ".jsx", ".ts", ".tsx",
222
- ".py", ".sh", ".sql",
281
+ ".py", ".sh", ".sql", ".c", ".h",
223
282
  })
224
283
 
225
284
 
226
285
  # ===========================================================================
227
- # CENSUS fact 8's disclosure-gate vocabulary -- formerly
228
- # ambit_map/library.py's own inline regex literal, r"\bjsp\b|\bjava\b".
229
- # Declared as its OWN field, NEVER derived from a lang-tag grouping above
230
- # (R4-DESIGN.md v2's explicit ruling, JUDGES-DIGEST.txt LENS 1/2 finding on
231
- # the blind filter): this is a vocabulary of ENGLISH WORDS a disclosure
232
- # SENTENCE spells, not a vocabulary of `lang` tag values, and the two
233
- # differ on purpose. Joining it from ROUTEDNESS_LANGS above would either
234
- # resurrect "jspf" into the regex (no disclosure sentence anywhere spells
235
- # the standalone word "jspf" -- ROUTEDNESS_LANGS keeps it only for
236
- # producer-portability tolerance, never because prose says it) or, joined
237
- # from JS_LANGS, mint useless/dangerous words no disclosure sentence uses
238
- # standalone this way ("ts", "cjs") or that collide with ordinary English
239
- # ("js" inside other tokens). ambit_map/library.py's own _BLIND_LANG_FILTER
240
- # compiles its regex from exactly these two words, `\b<word>\b` each,
241
- # joined with `|` -- byte-identical to the pattern it replaces.
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.
242
322
  # ===========================================================================
243
- BLIND_LANG_PROSE_WORDS = ("jsp", "java")
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)
@@ -22,7 +22,6 @@ per (model, dialects) OBJECT IDENTITY, file-local to this module -- see the
22
22
  comment above `_MOUNTS_CACHE_MAXSIZE` below for the full argument.
23
23
  """
24
24
  import collections
25
- import re
26
25
  import threading
27
26
 
28
27
  from . import registry as reg
@@ -890,42 +889,38 @@ def _method_note1(dialects):
890
889
  return ob.note(lead + "".join(clauses))
891
890
 
892
891
 
893
- # Word-boundary, not substring: a bare `"jsp" in s.lower()` also matches
894
- # every sentence that merely contains "jsp" as part of a longer token, and
895
- # a bare "java" substring check would ALSO match "javascript"/"javadoc" --
896
- # neither is the language name this filter exists to drop. `\bjava\b`
897
- # never matches "javascript" (no boundary follows "java" there) or
898
- # "javadoc" (same reason), so only the standalone word is caught.
899
- #
900
- # R4: the two words themselves now live at ambit_map/langmirror.py's own
901
- # BLIND_LANG_PROSE_WORDS, declared as their OWN field rather than derived
902
- # from ROUTEDNESS_LANGS/JS_LANGS above (that module's own comment states
903
- # why: a vocabulary of English disclosure words, not of `lang` tags). The
904
- # compiled pattern is byte-identical to the literal it replaces,
905
- # r"\bjsp\b|\bjava\b".
906
- _BLIND_LANG_FILTER = re.compile(
907
- "|".join(r"\b%s\b" % re.escape(w) for w in langm.BLIND_LANG_PROSE_WORDS),
908
- 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
909
898
 
910
899
 
911
900
  def _blind_for_run(cov, model):
912
901
  """`_combined_blind` (DATA/LOGIC, kept verbatim -- other call sites in
913
902
  this module still want the RAW list) always carries the engine's
914
- static, always-present disclosures, including the JSP/Java-scriptlet
915
- and JSP/Java-namespace sentences -- the comment above
916
- ambitgraph.symbols.BLIND_SENTENCES says so explicitly ("always
917
- present, regardless of what the scanned tree happens to contain"). Review fix (major): on a run with zero jsp/jspf/
918
- java units those two sentences named a language the page never scanned
919
- (chatbot-ui, flask), so THIS caller -- the only live caller of
920
- `_combined_blind` in the new render_library flow -- drops any sentence
921
- naming JSP or Java when the run holds no jsp/jspf/java unit at all. The
922
- RAW list (with those sentences) still reaches `_find_blind`'s other
923
- callers unfiltered, since ZERO_DDL_MATCH/CALL_BLIND_MATCH need the
924
- 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."""
925
916
  blind = _combined_blind(cov)
926
- if any(u.lang in langm.ROUTEDNESS_LANGS for u in model.units.values()):
927
- return blind
928
- 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
929
924
 
930
925
 
931
926
  def _method_note2(model, envelope, grammarless_table=False):
@@ -106,13 +106,14 @@ MEASURED_SECONDS_PER_FILE = 0.080
106
106
  ENGINE_DEFAULT_INCLUDE_EXT = langm.ENGINE_DEFAULT_INCLUDE_EXT
107
107
  # C2 (mapllm wave): the extra extensions a mapllm run admits beyond the
108
108
  # shared engine default above -- world-popularity order per the founder's
109
- # list, the C/C++ foundation first. `map`'s and `scan`'s own defaults stay
110
- # BYTE-UNTOUCHED (the ENGINE_DEFAULT_INCLUDE_EXT equality pin just above
111
- # stays green, unchanged): this list only ever widens a mapllm run's OWN
112
- # generated product config (product_config()/write_product_config()/
113
- # plan()'s own extra_include_ext argument, below), never
114
- # ENGINE_DEFAULT_INCLUDE_EXT itself. R4: sourced from langmirror.py, same
115
- # posture as the admission tuple above.
109
+ # list, the C++ foundation first (C itself moved into the shared engine
110
+ # default above once it gained a real LangSpec). `map`'s and `scan`'s own
111
+ # defaults stay BYTE-UNTOUCHED (the ENGINE_DEFAULT_INCLUDE_EXT equality
112
+ # pin just above stays green, unchanged): this list only ever widens a
113
+ # mapllm run's OWN generated product config (product_config()/
114
+ # write_product_config()/plan()'s own extra_include_ext argument, below),
115
+ # never ENGINE_DEFAULT_INCLUDE_EXT itself. R4: sourced from langmirror.py,
116
+ # same posture as the admission tuple above.
116
117
  MAPLLM_EXTRA_INCLUDE_EXT = langm.MAPLLM_EXTRA_INCLUDE_EXT
117
118
  FAIL_TAIL_CHARS = 1200
118
119
  DIALECT_FILENAME = "product-dialect.json"
@@ -116,10 +116,20 @@ PY_LANGS = langm.PY_LANGS
116
116
  # include/import.py and include/import.js. Same alias-only shape as the
117
117
  # two lines above.
118
118
  JAVA_LANGS = langm.JAVA_LANGS
119
+ # The fourth src_lang bucket, same alias-only shape as the three above:
120
+ # kind_key's include branch reads it to route a C preprocessor include,
121
+ # and unitpage.py's class-count bucket reads it as the gate on
122
+ # C_TYPE_SYMBOL_KINDS below.
123
+ C_LANGS = langm.C_LANGS
119
124
  # Java W1 repair (2026-09-02), additive: the same alias-only
120
125
  # shape, for unitpage.py's own class-count bucket -- see langmirror.py's
121
126
  # own comment on this field for the producer/consumer/diff story.
122
127
  TYPE_SYMBOL_KINDS = langm.TYPE_SYMBOL_KINDS
128
+ # The C half of the same bucket -- the kind strings extract_c_symbols
129
+ # emits for a type declaration; see langmirror.py's own comment on this
130
+ # field for the producer/consumer/diff story, and for why "enum" being a
131
+ # member of both sets is safe (the two language gates are disjoint).
132
+ C_TYPE_SYMBOL_KINDS = langm.C_TYPE_SYMBOL_KINDS
123
133
  # Java W1 repair, additive: the language gate that
124
134
  # TYPE_SYMBOL_KINDS' unitpage consumer needs -- same alias-only shape;
125
135
  # see langmirror.py's own comment on this field.
@@ -263,8 +273,13 @@ _TEMPLATES = {
263
273
  "reference/script": (
264
274
  "{A} loads {B} as a script tag — a browser-side reference, not "
265
275
  "a server-side include."),
276
+ # One sentence for every row on the calls plane, so it names a
277
+ # BINDING rather than a match: Java's exact-evidence tiers bind a
278
+ # site through the caller's own imports and package, while every
279
+ # lane's fallback tier matches a repo-unique name -- both are static
280
+ # reads of source text, and neither traces execution.
266
281
  "calls": (
267
- "{A} calls {B} at {n} site(s) — a static name match, not a "
282
+ "{A} calls {B} at {n} site(s) — a static name binding, not a "
268
283
  "traced call."),
269
284
  "table/read": "{A} reads the {t} table.",
270
285
  "table/write": "{A} writes the {t} table.",
@@ -365,9 +380,17 @@ _EPISTEMIC = {
365
380
  "reference/script": (
366
381
  "this is a <script src> tag read from markup; a tag injected by "
367
382
  "client-side JS is not seen."),
383
+ # ambit_map/matched.py's _definer_name_index docstring cites this
384
+ # string by the exact phrase "unique-name static match" -- that
385
+ # phrase stays, as the fallback tier's own name, whatever else this
386
+ # string carries.
368
387
  "calls": (
369
- "unique-name static match over the JSP/Java namespace only; a name "
370
- "defined in two places is dropped rather than guessed."),
388
+ "every lane ends in the same unique-name static match — a name "
389
+ "defined in two places is dropped rather than guessed; a Java "
390
+ "site may first bind proven on exact evidence: a constructor "
391
+ "resolved through the caller's own imports and package, a static "
392
+ "receiver resolved there whose method the target file declares, "
393
+ "or a static import whose target declares the member."),
371
394
  "table/read": (
372
395
  "a SQL SELECT (or equivalent read) naming a validated table, read "
373
396
  "from the source text."),
@@ -493,6 +516,20 @@ def kind_key(plane, kind="", src_lang=""):
493
516
  if plane == "include":
494
517
  if kind == "static":
495
518
  return "include/static"
519
+ # The C preprocessor's own edge kind, "include". It lands on
520
+ # the include/static row deliberately, not on a row of its
521
+ # own: that row's sentence and epistemic text -- a literal
522
+ # path read from the source text, spliced in at include time,
523
+ # never varying at runtime -- is exactly what a C `#include`
524
+ # is, and a twentieth row would repeat it word for word. The
525
+ # src_lang gate is what keeps the reuse honest: without it,
526
+ # any producer's edge spelled kind "include" would inherit
527
+ # JSP's row by accident. langmirror.KIND_READER_LANGS'
528
+ # include/static row carries "c" beside "jsp" for the same
529
+ # reason, and tests/test_langs_mirror_diff.py's include-rows
530
+ # union check is what holds the two together.
531
+ if kind == "include" and src_lang in C_LANGS:
532
+ return "include/static"
496
533
  if kind == "import":
497
534
  if src_lang in PY_LANGS:
498
535
  return "include/import.py"
@@ -1011,6 +1048,66 @@ def importer_root_underivable_sentence(A, raw):
1011
1048
  {"A": A, "raw": raw})
1012
1049
 
1013
1050
 
1051
+ # The C include lane's four reason tokens, one sentence each -- the same
1052
+ # dispatch the three above ride. EDGE_KINDS["unresolved"]'s own template
1053
+ # ("{A} names a target this scan never read...") is false for all four:
1054
+ # an angle include is HELD, never searched for; a quoted system header is
1055
+ # the toolchain's to find; an absent target may sit in a directory this
1056
+ # scan excludes, which is a scope fact, not an unread file; and the last
1057
+ # names a file this scan DID read and refused to draw for position. Each
1058
+ # sentence says which of those it is, in the same words the run's own
1059
+ # coverage sentence uses, so the chip and the disclosure never disagree.
1060
+ C_ANGLE_INCLUDE_HELD_SENTENCE = (
1061
+ "{A} names {raw} in angle brackets — the toolchain search path decides "
1062
+ "what that reaches, so the connection is held rather than drawn: a name "
1063
+ "matching inside this scan is not evidence the compiler reached that "
1064
+ "file.")
1065
+ _C_ANGLE_INCLUDE_HELD_SLOT_NAMES = ("A", "raw")
1066
+
1067
+
1068
+ def c_angle_include_held_sentence(A, raw):
1069
+ return _fill(C_ANGLE_INCLUDE_HELD_SENTENCE,
1070
+ _C_ANGLE_INCLUDE_HELD_SLOT_NAMES, {"A": A, "raw": raw})
1071
+
1072
+
1073
+ C_QUOTED_INCLUDE_SYSTEM_SENTENCE = (
1074
+ "{A} names {raw} — a C standard library header, resolved by the "
1075
+ "toolchain search path rather than by position. Held, not an error "
1076
+ "row: imported third-party code quotes its own system headers.")
1077
+ _C_QUOTED_INCLUDE_SYSTEM_SLOT_NAMES = ("A", "raw")
1078
+
1079
+
1080
+ def c_quoted_include_system_sentence(A, raw):
1081
+ return _fill(C_QUOTED_INCLUDE_SYSTEM_SENTENCE,
1082
+ _C_QUOTED_INCLUDE_SYSTEM_SLOT_NAMES, {"A": A, "raw": raw})
1083
+
1084
+
1085
+ C_QUOTED_INCLUDE_ABSENT_SENTENCE = (
1086
+ "{A} names {raw} — this scan holds no candidate for it anywhere: the "
1087
+ "target is generated at build time, or it lives in a directory this "
1088
+ "scan excludes, or it is a header from outside this repository that "
1089
+ "the C standard list does not name.")
1090
+ _C_QUOTED_INCLUDE_ABSENT_SLOT_NAMES = ("A", "raw")
1091
+
1092
+
1093
+ def c_quoted_include_absent_sentence(A, raw):
1094
+ return _fill(C_QUOTED_INCLUDE_ABSENT_SENTENCE,
1095
+ _C_QUOTED_INCLUDE_ABSENT_SLOT_NAMES, {"A": A, "raw": raw})
1096
+
1097
+
1098
+ C_QUOTED_INCLUDE_UNRESOLVED_SENTENCE = (
1099
+ "{A} names {raw} — this scan does hold a file for it, but position "
1100
+ "does not license the edge: a one-segment name is licensed only from "
1101
+ "the including directory, and a multi-segment path must end exactly "
1102
+ "one file in the scan.")
1103
+ _C_QUOTED_INCLUDE_UNRESOLVED_SLOT_NAMES = ("A", "raw")
1104
+
1105
+
1106
+ def c_quoted_include_unresolved_sentence(A, raw):
1107
+ return _fill(C_QUOTED_INCLUDE_UNRESOLVED_SENTENCE,
1108
+ _C_QUOTED_INCLUDE_UNRESOLVED_SLOT_NAMES, {"A": A, "raw": raw})
1109
+
1110
+
1014
1111
  def unresolved_reason_sentence(reason, A, raw):
1015
1112
  """The reason-token dispatch every unresolved-ref caption site shares
1016
1113
  (graph.py's _quarantine_note, unitpage.py's connections block): maps
@@ -1025,6 +1122,14 @@ def unresolved_reason_sentence(reason, A, raw):
1025
1122
  return wildcard_import_sentence(A=A, raw=raw)
1026
1123
  if reason == "importer_root_underivable":
1027
1124
  return importer_root_underivable_sentence(A=A, raw=raw)
1125
+ if reason == "c_angle_include_held":
1126
+ return c_angle_include_held_sentence(A=A, raw=raw)
1127
+ if reason == "c_quoted_include_unresolved_system":
1128
+ return c_quoted_include_system_sentence(A=A, raw=raw)
1129
+ if reason == "c_quoted_include_unresolved_absent":
1130
+ return c_quoted_include_absent_sentence(A=A, raw=raw)
1131
+ if reason == "c_quoted_include_unresolved":
1132
+ return c_quoted_include_unresolved_sentence(A=A, raw=raw)
1028
1133
  return ""
1029
1134
 
1030
1135
 
@@ -1142,6 +1247,14 @@ UNIT_STORY_LEAD_NOLANG = "{name} — {lines} lines"
1142
1247
  UNIT_STORY_HOLDS = "the scan read {parts}"
1143
1248
  UNIT_STORY_FNS = "{n} function(s)"
1144
1249
  UNIT_STORY_CLASSES = "{n} class(es)"
1250
+ # The same bucket, worded for a language that has no classes. C's type
1251
+ # declarations are structs, unions, enums and typedefs, and counting them
1252
+ # under a word C does not have is a false statement about the file in the
1253
+ # first sentence of its page -- so the CALLER, which is where the language
1254
+ # gate on this count already lives, names the word and unit_story renders
1255
+ # whichever one it is handed. "type" is registered in _COUNT_NOUNS below,
1256
+ # without which "1 type(s)" would ship its inflection marker to a reader.
1257
+ UNIT_STORY_TYPES = "{n} type(s)"
1145
1258
  # review fix (blocker, 2026-08-26): a unit whose symbols are all a kind
1146
1259
  # other than function/method/class (a SQL table/index/trigger/sql_function,
1147
1260
  # a TS interface/type/const...) used to fall through to UNIT_STORY_NO_SYMS
@@ -1237,8 +1350,16 @@ def _pos(n):
1237
1350
  return isinstance(n, int) and not isinstance(n, bool) and n > 0
1238
1351
 
1239
1352
 
1240
- def unit_story(name, lang, lines, f, c, t, inbound, edges_damaged=False, other=0):
1241
- """One sentence, no language-specific clause ever. Every zero clause is
1353
+ def unit_story(name, lang, lines, f, c, t, inbound, edges_damaged=False, other=0,
1354
+ class_word=UNIT_STORY_CLASSES):
1355
+ """One sentence, no language-specific CLAUSE ever. `class_word` is the
1356
+ one language-shaped choice, and it is the caller's: the type-count
1357
+ bucket is filled under a per-language gate that lives there, and a
1358
+ language with no classes must not have its types counted under that
1359
+ word. The default renders the sentence every non-C page has always
1360
+ rendered, so a caller that never passes it is byte-unchanged.
1361
+
1362
+ Every zero clause is
1242
1363
  OMITTED by the builder -- never a bare 0. `edges_damaged` replaces the
1243
1364
  touches/inbound clauses with one honest "unknown" clause instead of a
1244
1365
  false zero. `other` (review fix, blocker) is every symbol counted
@@ -1252,7 +1373,7 @@ def unit_story(name, lang, lines, f, c, t, inbound, edges_damaged=False, other=0
1252
1373
  if _pos(f):
1253
1374
  holds_parts.append(UNIT_STORY_FNS.format(n=f))
1254
1375
  if _pos(c):
1255
- holds_parts.append(UNIT_STORY_CLASSES.format(n=c))
1376
+ holds_parts.append(class_word.format(n=c))
1256
1377
  if _pos(other):
1257
1378
  holds_parts.append(UNIT_STORY_OTHER.format(n=other))
1258
1379
  segments = []
@@ -1676,7 +1797,7 @@ _COUNT_NOUNS = ("bucket", "class", "column", "day", "directory", "edge",
1676
1797
  "match", "minute", "name", "neighbour", "node", "note",
1677
1798
  "noun", "one", "origin", "page", "path", "reader",
1678
1799
  "reference", "route", "self-reference", "site", "subtree",
1679
- "symbol", "table", "unit")
1800
+ "symbol", "table", "type", "unit")
1680
1801
  # Two inflection markers: "(s)" (plural adds "s") and "(es)" (plural adds
1681
1802
  # "es", e.g. "class(es)" -> "class" / "classes"). One alternation, one
1682
1803
  # capture group naming which suffix a hit's own noun takes.
@@ -834,16 +834,34 @@ def render_unit(model, uid, now_iso, uncap=frozenset(), dialects=None, judged=No
834
834
  # and a TS interface/enum renders "class(es)" again; revert to
835
835
  # the bare "class" check and a Java interface/enum/record falls
836
836
  # into "other" -- both directions pinned through a real render.
837
+ #
838
+ # The C half of the same bucket, gated the identical way: a C unit
839
+ # counts its struct/union/enum/typedef declarations as types
840
+ # (reg.C_TYPE_SYMBOL_KINDS under the reg.C_LANGS gate, both
841
+ # diffed against ambitgraph.codelib's own sets). The gate is what
842
+ # keeps "enum" -- a member of BOTH kind sets -- from re-counting a
843
+ # TypeScript enum: the java and C gates are disjoint and a
844
+ # non-C, non-java unit meets neither, so every page of every other
845
+ # language renders byte-identically to before this branch existed.
837
846
  java_ns = u.lang in reg.NAMESPACE_LANGS
847
+ c_ns = u.lang in reg.C_LANGS
838
848
  c = sum(1 for s in u.symbols
839
849
  if s.kind == "class"
840
- or (java_ns and s.kind in reg.TYPE_SYMBOL_KINDS))
850
+ or (java_ns and s.kind in reg.TYPE_SYMBOL_KINDS)
851
+ or (c_ns and s.kind in reg.C_TYPE_SYMBOL_KINDS))
852
+ # ... and the WORD that bucket is counted under is decided by the
853
+ # SAME gate. C has no classes at all -- what it declares is
854
+ # structs, unions, enums and typedefs -- so a C unit's first
855
+ # sentence would otherwise state a construct the language does not
856
+ # have, about a file that cannot contain one. Every other language
857
+ # keeps reg.UNIT_STORY_CLASSES, the default, byte for byte.
858
+ class_word = reg.UNIT_STORY_TYPES if c_ns else reg.UNIT_STORY_CLASSES
841
859
  other = len(u.symbols) - f - c
842
860
  t = len({op.table for op in u.table_ops})
843
861
  inbound = (len(model.included_by.get(u.uid, ())) + len(model.referenced_by.get(u.uid, ()))
844
862
  + len(model.called_by.get(u.uid, ())))
845
863
  story = reg.unit_story(u.uid, u.lang, u.lines, f, c, t, inbound, model.edges_damaged,
846
- other=other)
864
+ other=other, class_word=class_word)
847
865
 
848
866
  hero = ob.hero(
849
867
  crumbs=[("AmbitGraph", "/library"), ("Library", "/library"),
@@ -3,4 +3,4 @@
3
3
  Phase 1: the scan. Read-only, always.
4
4
  """
5
5
 
6
- __version__ = "0.3.0"
6
+ __version__ = "0.4.0"