narrativetrace-glossary 0.1.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 (68) hide show
  1. narrativetrace_glossary-0.1.0/.gitignore +28 -0
  2. narrativetrace_glossary-0.1.0/LICENSE +99 -0
  3. narrativetrace_glossary-0.1.0/PKG-INFO +83 -0
  4. narrativetrace_glossary-0.1.0/README.md +68 -0
  5. narrativetrace_glossary-0.1.0/pyproject.toml +41 -0
  6. narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/__init__.py +114 -0
  7. narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/alias_index.py +53 -0
  8. narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/clarity_issues.py +47 -0
  9. narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/clarity_scan.py +30 -0
  10. narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/class_package_index.py +65 -0
  11. narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/context_resolver.py +40 -0
  12. narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/glossary_loader.py +56 -0
  13. narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/glossary_scan_cli.py +66 -0
  14. narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/harvester.py +213 -0
  15. narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/json_reader.py +263 -0
  16. narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/json_writer.py +121 -0
  17. narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/markdown.py +132 -0
  18. narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/merger.py +155 -0
  19. narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/models.py +288 -0
  20. narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/normalizer.py +254 -0
  21. narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/py.typed +0 -0
  22. narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/rename_suggester.py +65 -0
  23. narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/scaffolding.py +84 -0
  24. narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/static_scan.py +248 -0
  25. narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/suite_harvest.py +103 -0
  26. narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/summary_formatter.py +47 -0
  27. narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/translation_file_sink.py +47 -0
  28. narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/translation_subscriber.py +146 -0
  29. narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/translation_view.py +242 -0
  30. narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/translator.py +97 -0
  31. narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/usage_report.py +86 -0
  32. narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/violations.py +82 -0
  33. narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/vocabulary.py +79 -0
  34. narrativetrace_glossary-0.1.0/tests/conftest.py +91 -0
  35. narrativetrace_glossary-0.1.0/tests/glossary_strategies.py +115 -0
  36. narrativetrace_glossary-0.1.0/tests/test_alias_index.py +94 -0
  37. narrativetrace_glossary-0.1.0/tests/test_clarity_issues.py +65 -0
  38. narrativetrace_glossary-0.1.0/tests/test_clarity_scan.py +61 -0
  39. narrativetrace_glossary-0.1.0/tests/test_class_package_index.py +83 -0
  40. narrativetrace_glossary-0.1.0/tests/test_context_resolver.py +75 -0
  41. narrativetrace_glossary-0.1.0/tests/test_context_resolver_props.py +84 -0
  42. narrativetrace_glossary-0.1.0/tests/test_glossary.py +208 -0
  43. narrativetrace_glossary-0.1.0/tests/test_glossary_loader.py +70 -0
  44. narrativetrace_glossary-0.1.0/tests/test_glossary_props.py +116 -0
  45. narrativetrace_glossary-0.1.0/tests/test_glossary_scan_cli.py +82 -0
  46. narrativetrace_glossary-0.1.0/tests/test_glossary_vocabulary.py +213 -0
  47. narrativetrace_glossary-0.1.0/tests/test_harvester.py +288 -0
  48. narrativetrace_glossary-0.1.0/tests/test_harvester_props.py +129 -0
  49. narrativetrace_glossary-0.1.0/tests/test_json_reader.py +341 -0
  50. narrativetrace_glossary-0.1.0/tests/test_json_writer.py +169 -0
  51. narrativetrace_glossary-0.1.0/tests/test_markdown.py +220 -0
  52. narrativetrace_glossary-0.1.0/tests/test_merger.py +284 -0
  53. narrativetrace_glossary-0.1.0/tests/test_merger_props.py +101 -0
  54. narrativetrace_glossary-0.1.0/tests/test_models.py +174 -0
  55. narrativetrace_glossary-0.1.0/tests/test_normalizer.py +255 -0
  56. narrativetrace_glossary-0.1.0/tests/test_normalizer_props.py +84 -0
  57. narrativetrace_glossary-0.1.0/tests/test_rename_suggester.py +56 -0
  58. narrativetrace_glossary-0.1.0/tests/test_scaffolding.py +58 -0
  59. narrativetrace_glossary-0.1.0/tests/test_static_scan.py +171 -0
  60. narrativetrace_glossary-0.1.0/tests/test_suite_harvest.py +119 -0
  61. narrativetrace_glossary-0.1.0/tests/test_summary_formatter.py +58 -0
  62. narrativetrace_glossary-0.1.0/tests/test_translation_file_sink.py +58 -0
  63. narrativetrace_glossary-0.1.0/tests/test_translation_subscriber.py +216 -0
  64. narrativetrace_glossary-0.1.0/tests/test_translation_view.py +351 -0
  65. narrativetrace_glossary-0.1.0/tests/test_translation_view_props.py +136 -0
  66. narrativetrace_glossary-0.1.0/tests/test_translator.py +129 -0
  67. narrativetrace_glossary-0.1.0/tests/test_usage_report.py +116 -0
  68. narrativetrace_glossary-0.1.0/tests/test_violations.py +123 -0
@@ -0,0 +1,28 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ .pytest_cache/
5
+ .mypy_cache/
6
+ .ruff_cache/
7
+ .hypothesis/
8
+ .mutmut-cache
9
+ mutants/
10
+ # pytest-benchmark autosave (poe bench-gate): per-machine comparison history, never comparable
11
+ # across machines -- stays local to whichever machine runs the nightly, like .venv/.uv-cache.
12
+ .benchmarks/
13
+ htmlcov/
14
+ .coverage
15
+ .coverage.*
16
+ dist/
17
+ build/
18
+ *.egg-info/
19
+
20
+ # Cache for security-tool binaries auto-fetched by scripts/run_security_tool.py
21
+ .tools/
22
+
23
+ # Local tooling state (contains credentials — never commit)
24
+ .devcontainer/claude-home/
25
+ .agent-run*
26
+
27
+ # Local tooling state (holds host-absolute paths)
28
+ .claude/settings.local.json
@@ -0,0 +1,99 @@
1
+ Business Source License 1.1
2
+
3
+ License text copyright © 2017 MariaDB Corporation Ab, All Rights Reserved.
4
+ "Business Source License" is a trademark of MariaDB Corporation Ab.
5
+
6
+ Parameters
7
+
8
+ Licensor: Empower Agile
9
+
10
+ Licensed Work: NarrativeTrace for Python (the narrativetrace PyPI distributions) version {{VERSION}}. The Licensed Work is (c) 2026 Empower Agile.
11
+
12
+ Additional Use Grant: You may make production use of the Licensed Work for any
13
+ purpose, including internal use and use in products and
14
+ services you provide to your own customers, provided that
15
+ such production use does not include offering the
16
+ Licensed Work, or a product or service whose value
17
+ derives substantially from the Licensed Work, to third
18
+ parties as a logging, tracing, or code-narrative product
19
+ or service.
20
+
21
+ Change Date: Four years from the date the Licensed Work is published. ({{CHANGE_DATE}} for this version)
22
+
23
+ Change License: Apache License, Version 2.0
24
+
25
+ For information about alternative licensing arrangements for the Licensed
26
+ Work, please contact Empower Agile at hello@narrativetrace.ai.
27
+
28
+ Notice
29
+
30
+ The Business Source License (this document, or the "License") is not an Open
31
+ Source license. However, the Licensed Work will eventually be made available
32
+ under an Open Source License, as stated in this License.
33
+
34
+ Terms
35
+
36
+ The Licensor hereby grants you the right to copy, modify, create derivative
37
+ works, redistribute, and make non-production use of the Licensed Work. The
38
+ Licensor may make an Additional Use Grant, above, permitting limited
39
+ production use.
40
+
41
+ Effective on the Change Date, or the fourth anniversary of the first publicly
42
+ available distribution of a specific version of the Licensed Work under this
43
+ License, whichever comes first, the Licensor hereby grants you rights under
44
+ the terms of the Change License, and the rights granted in the paragraph
45
+ above terminate.
46
+
47
+ If your use of the Licensed Work does not comply with the requirements
48
+ currently in effect as described in this License, you must purchase a
49
+ commercial license from the Licensor, its affiliated entities, or authorized
50
+ resellers, or you must refrain from using the Licensed Work.
51
+
52
+ All copies of the original and modified Licensed Work, and derivative works
53
+ of the Licensed Work, are subject to this License. This License applies
54
+ separately for each version of the Licensed Work and the Change Date may vary
55
+ for each version of the Licensed Work released by Licensor.
56
+
57
+ You must conspicuously display this License on each original or modified copy
58
+ of the Licensed Work. If you receive the Licensed Work in original or
59
+ modified form from a third party, the terms and conditions set forth in this
60
+ License apply to your use of that work.
61
+
62
+ Any use of the Licensed Work in violation of this License will automatically
63
+ terminate your rights under this License for the current and all other
64
+ versions of the Licensed Work.
65
+
66
+ This License does not grant you any right in any trademark or logo of
67
+ Licensor or its affiliates (provided that you may use a trademark or logo of
68
+ Licensor as expressly required by this License).
69
+
70
+ TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
71
+ AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
72
+ EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
73
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
74
+ TITLE.
75
+
76
+ MariaDB hereby grants you permission to use this License’s text to license
77
+ your works, and to refer to it using the trademark “Business Source License”,
78
+ as long as you comply with the Covenants of Licensor below.
79
+
80
+ Covenants of Licensor
81
+
82
+ In consideration of the right to use this License’s text and the “Business
83
+ Source License” name and trademark, Licensor covenants to MariaDB, and to all
84
+ other recipients of the licensed work to be provided by Licensor:
85
+
86
+ 1. To specify as the Change License the GPL Version 2.0 or any later version,
87
+ or a license that is compatible with GPL Version 2.0 or a later version,
88
+ where “compatible” means that software provided under the Change License can
89
+ be included in a program with software provided under GPL Version 2.0 or a
90
+ later version. Licensor may specify additional Change Licenses without
91
+ limitation.
92
+
93
+ 2. To either: (a) specify an additional grant of rights to use that does not
94
+ impose any additional restriction on the right granted in this License, as
95
+ the Additional Use Grant; or (b) insert the text “None”.
96
+
97
+ 3. To specify a Change Date.
98
+
99
+ 4. Not to modify this License in any other way.
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.5
2
+ Name: narrativetrace-glossary
3
+ Version: 0.1.0
4
+ Summary: Domain glossary: ubiquitous-language harvesting and trace translation for narrativetrace
5
+ Project-URL: Homepage, https://github.com/narrativetrace/narrativetrace-python
6
+ Project-URL: Source, https://github.com/narrativetrace/narrativetrace-python
7
+ Author: narrativetrace contributors
8
+ License-Expression: BUSL-1.1
9
+ License-File: LICENSE
10
+ Classifier: Typing :: Typed
11
+ Requires-Python: >=3.12
12
+ Requires-Dist: narrativetrace
13
+ Requires-Dist: narrativetrace-clarity
14
+ Description-Content-Type: text/markdown
15
+
16
+ # narrativetrace-glossary
17
+
18
+ The domain glossary of a repository: one checked-in `glossary.json` that is simultaneously the
19
+ translation dictionary, the reviewable domain documentation, and the vocabulary norm that clarity
20
+ diagnostics enforce (ADR-012).
21
+
22
+ The Java module `narrativetrace-glossary`. Platform mapping: Java *packages* become **dotted
23
+ module-path prefixes**, so a bounded context owns `acme.billing`, not `com.acme.billing`.
24
+
25
+ ```python
26
+ from narrativetrace_glossary import read_glossary_json, write_glossary_json
27
+
28
+ glossary = read_glossary_json(Path("glossary.json").read_text(encoding="utf-8"))
29
+ Path("glossary.json").write_text(write_glossary_json(glossary), encoding="utf-8")
30
+ ```
31
+
32
+ Identifiers become glossary phrases through the normalizer, and the module path of the traced code
33
+ picks the bounded context that owns them:
34
+
35
+ ```python
36
+ from narrativetrace_glossary import method_candidates, normalize_phrase, resolve_context
37
+
38
+ normalize_phrase("open_account_with_overdraft") # "open account with overdraft"
39
+ method_candidates("openAccountWithOverdraft") # verb phrase + "account with overdraft"
40
+ resolve_context(glossary, "acme.billing.overdraft_service") # "billing"
41
+ ```
42
+
43
+ Every spelling of one concept converges to a single phrase, so matching against terms and aliases
44
+ always happens on the normalized form. Context prefixes match on dot boundaries — `acme.billing`
45
+ owns `acme.billing.overdraft` but never the sibling `acme.billingx` — the longest match wins, and
46
+ an unmatched path falls back to `_unassigned`, so harvesting works with zero configuration.
47
+
48
+ The normalizer rejects anything that is not a single code identifier (blank, underscore-only, or
49
+ whitespace-carrying input raises `ValueError`): a normalized phrase is term identity, so a
50
+ non-canonical one would corrupt a glossary key.
51
+
52
+ The writer is deterministic by construction — contexts sorted by name, terms in `(context, term)`
53
+ order, fixed key order, 2-space indent, trailing newline — so a run that observes no new vocabulary
54
+ leaves the committed file byte-identical.
55
+
56
+ ## Harvesting a run into the glossary
57
+
58
+ `harvest_traces` observes captured trace trees; `merge_harvest` folds those observations into the
59
+ committed glossary. Trace nodes carry a *simple* class name, so the caller supplies `module_of` to
60
+ say which module declares it — production code passes `type(target).__module__`, tests pass a dict
61
+ lookup. A name it cannot answer for resolves to `_unassigned`.
62
+
63
+ ```python
64
+ from narrativetrace_glossary import harvest_traces, merge_harvest, write_glossary_json
65
+
66
+ observed = harvest_traces(trees, glossary=glossary, module_of=modules.get)
67
+ result = merge_harvest(glossary, observed)
68
+
69
+ Path("glossary.json").write_text(write_glossary_json(result.glossary), encoding="utf-8")
70
+ ```
71
+
72
+ Harvest sources are method names (the verb phrase plus the object noun phrase), parameter names,
73
+ class names with a role suffix stripped, and the type name of whatever a node threw. Names that are
74
+ no identifier — synthetic nodes like `<launcher>` or `fire-and-forget` — are skipped silently;
75
+ harvesting is best-effort by design. Narration templates are *never* harvested from a live trace:
76
+ they hold interpolated runtime values there.
77
+
78
+ The merge is **additive-only** and **idempotent**, both property-tested. It never removes or
79
+ rewrites an entry, so human-authored definitions, translations and a `curated` status are safe; a
80
+ phrase that matches a term's deprecated synonym in its own context is suppressed rather than
81
+ re-added, and comes back in `result.suppressed_alias_uses` for violation reporting. New terms are
82
+ `harvested`, record at most three distinct sites, and are dated by an injectable `clock` that
83
+ defaults to today in UTC — so the same run merged twice leaves the file byte-identical.
@@ -0,0 +1,68 @@
1
+ # narrativetrace-glossary
2
+
3
+ The domain glossary of a repository: one checked-in `glossary.json` that is simultaneously the
4
+ translation dictionary, the reviewable domain documentation, and the vocabulary norm that clarity
5
+ diagnostics enforce (ADR-012).
6
+
7
+ The Java module `narrativetrace-glossary`. Platform mapping: Java *packages* become **dotted
8
+ module-path prefixes**, so a bounded context owns `acme.billing`, not `com.acme.billing`.
9
+
10
+ ```python
11
+ from narrativetrace_glossary import read_glossary_json, write_glossary_json
12
+
13
+ glossary = read_glossary_json(Path("glossary.json").read_text(encoding="utf-8"))
14
+ Path("glossary.json").write_text(write_glossary_json(glossary), encoding="utf-8")
15
+ ```
16
+
17
+ Identifiers become glossary phrases through the normalizer, and the module path of the traced code
18
+ picks the bounded context that owns them:
19
+
20
+ ```python
21
+ from narrativetrace_glossary import method_candidates, normalize_phrase, resolve_context
22
+
23
+ normalize_phrase("open_account_with_overdraft") # "open account with overdraft"
24
+ method_candidates("openAccountWithOverdraft") # verb phrase + "account with overdraft"
25
+ resolve_context(glossary, "acme.billing.overdraft_service") # "billing"
26
+ ```
27
+
28
+ Every spelling of one concept converges to a single phrase, so matching against terms and aliases
29
+ always happens on the normalized form. Context prefixes match on dot boundaries — `acme.billing`
30
+ owns `acme.billing.overdraft` but never the sibling `acme.billingx` — the longest match wins, and
31
+ an unmatched path falls back to `_unassigned`, so harvesting works with zero configuration.
32
+
33
+ The normalizer rejects anything that is not a single code identifier (blank, underscore-only, or
34
+ whitespace-carrying input raises `ValueError`): a normalized phrase is term identity, so a
35
+ non-canonical one would corrupt a glossary key.
36
+
37
+ The writer is deterministic by construction — contexts sorted by name, terms in `(context, term)`
38
+ order, fixed key order, 2-space indent, trailing newline — so a run that observes no new vocabulary
39
+ leaves the committed file byte-identical.
40
+
41
+ ## Harvesting a run into the glossary
42
+
43
+ `harvest_traces` observes captured trace trees; `merge_harvest` folds those observations into the
44
+ committed glossary. Trace nodes carry a *simple* class name, so the caller supplies `module_of` to
45
+ say which module declares it — production code passes `type(target).__module__`, tests pass a dict
46
+ lookup. A name it cannot answer for resolves to `_unassigned`.
47
+
48
+ ```python
49
+ from narrativetrace_glossary import harvest_traces, merge_harvest, write_glossary_json
50
+
51
+ observed = harvest_traces(trees, glossary=glossary, module_of=modules.get)
52
+ result = merge_harvest(glossary, observed)
53
+
54
+ Path("glossary.json").write_text(write_glossary_json(result.glossary), encoding="utf-8")
55
+ ```
56
+
57
+ Harvest sources are method names (the verb phrase plus the object noun phrase), parameter names,
58
+ class names with a role suffix stripped, and the type name of whatever a node threw. Names that are
59
+ no identifier — synthetic nodes like `<launcher>` or `fire-and-forget` — are skipped silently;
60
+ harvesting is best-effort by design. Narration templates are *never* harvested from a live trace:
61
+ they hold interpolated runtime values there.
62
+
63
+ The merge is **additive-only** and **idempotent**, both property-tested. It never removes or
64
+ rewrites an entry, so human-authored definitions, translations and a `curated` status are safe; a
65
+ phrase that matches a term's deprecated synonym in its own context is suppressed rather than
66
+ re-added, and comes back in `result.suppressed_alias_uses` for violation reporting. New terms are
67
+ `harvested`, record at most three distinct sites, and are dated by an injectable `clock` that
68
+ defaults to today in UTC — so the same run merged twice leaves the file byte-identical.
@@ -0,0 +1,41 @@
1
+ [project]
2
+ name = "narrativetrace-glossary"
3
+ version = "0.1.0"
4
+ description = "Domain glossary: ubiquitous-language harvesting and trace translation for narrativetrace"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ license = "BUSL-1.1"
8
+ # See packages/narrativetrace/pyproject.toml: one byte-identical copy of the root LICENSE per
9
+ # distribution, because PEP 639 forbids reaching out of the project directory.
10
+ license-files = ["LICENSE"]
11
+ authors = [{ name = "narrativetrace contributors" }]
12
+ classifiers = ["Typing :: Typed"]
13
+ # Term normalization reuses the clarity distribution's tokenizer and lexical tables (ADR-012).
14
+ # The core distribution is a direct dependency because harvesting walks its trace-tree types.
15
+ dependencies = ["narrativetrace", "narrativetrace-clarity"]
16
+
17
+ [project.urls]
18
+ Homepage = "https://github.com/narrativetrace/narrativetrace-python"
19
+ Source = "https://github.com/narrativetrace/narrativetrace-python"
20
+
21
+ [project.scripts]
22
+ # A deliberate, opt-in write tool (item 6's "glossary writes opt-in" prerequisite) — not a CI
23
+ # gate, unlike narrativetrace-clarity's console script. See glossary_scan_cli's module docstring.
24
+ glossary-scan = "narrativetrace_glossary.glossary_scan_cli:main"
25
+
26
+ [build-system]
27
+ requires = ["hatchling"]
28
+ build-backend = "hatchling.build"
29
+
30
+ [tool.hatch.build.targets.wheel]
31
+ packages = ["src/narrativetrace_glossary"]
32
+
33
+ # Mutmut derives mutant keys from the run root, so it must run from this directory
34
+ # (`uv run poe mutate-glossary`), not from the workspace root.
35
+ [tool.mutmut]
36
+ source_paths = ["src/narrativetrace_glossary/"]
37
+ also_copy = ["tests/"]
38
+ pytest_add_cli_args_test_selection = ["tests/"]
39
+ # The pytest plugin distribution imports narrativetrace at plugin-load time, before the mutated
40
+ # copy is on sys.path — loading it would test the installed package instead of the mutant.
41
+ pytest_add_cli_args = ["-p", "no:narrativetrace"]
@@ -0,0 +1,114 @@
1
+ """Domain glossary for narrativetrace: ubiquitous language harvested from traces.
2
+
3
+ One checked-in ``glossary.json`` per repository is at once the translation dictionary, the
4
+ reviewable domain documentation, and the vocabulary norm clarity diagnostics enforce (ADR-012).
5
+ """
6
+
7
+ from narrativetrace_glossary.alias_index import build_alias_index
8
+ from narrativetrace_glossary.clarity_issues import (
9
+ NON_CANONICAL_TERM_CATEGORY,
10
+ non_canonical_term_issues,
11
+ )
12
+ from narrativetrace_glossary.context_resolver import UNASSIGNED_CONTEXT, resolve_context
13
+ from narrativetrace_glossary.glossary_loader import load_glossary
14
+ from narrativetrace_glossary.harvester import HarvestCandidate, harvest_traces
15
+ from narrativetrace_glossary.json_reader import read_glossary_json
16
+ from narrativetrace_glossary.json_writer import write_glossary_json
17
+ from narrativetrace_glossary.markdown import render_glossary_markdown
18
+ from narrativetrace_glossary.merger import MergeResult, merge_harvest
19
+ from narrativetrace_glossary.models import (
20
+ GLOSSARY_ABBREVIATIONS_SCHEMA_VERSION,
21
+ GLOSSARY_SCHEMA_VERSION,
22
+ BoundedContext,
23
+ Glossary,
24
+ GlossaryTerm,
25
+ SynonymAlias,
26
+ TermKey,
27
+ TermKind,
28
+ TermStatus,
29
+ )
30
+ from narrativetrace_glossary.normalizer import (
31
+ TermCandidate,
32
+ class_candidate,
33
+ exception_candidate,
34
+ method_candidates,
35
+ normalize_phrase,
36
+ parameter_candidate,
37
+ )
38
+ from narrativetrace_glossary.rename_suggester import suggest_rename
39
+ from narrativetrace_glossary.scaffolding import SUPPORTED_LOCALES, ScaffoldingBundle
40
+ from narrativetrace_glossary.summary_formatter import (
41
+ format_violation_details,
42
+ format_vocabulary_summary,
43
+ )
44
+ from narrativetrace_glossary.translation_file_sink import TranslationFileSink
45
+ from narrativetrace_glossary.translation_subscriber import (
46
+ DEFAULT_CAPACITY,
47
+ DEFAULT_TTL_SECONDS,
48
+ TranslationSubscriber,
49
+ )
50
+ from narrativetrace_glossary.translation_view import RenderState, TraceTranslationView
51
+ from narrativetrace_glossary.translator import GlossaryTranslator, PhraseTranslation
52
+ from narrativetrace_glossary.usage_report import (
53
+ USAGE_REPORT_FILE,
54
+ usage_report_document,
55
+ write_usage_report,
56
+ )
57
+ from narrativetrace_glossary.violations import VocabularyViolation, aggregate_violations
58
+ from narrativetrace_glossary.vocabulary import (
59
+ GLOSSARY_FILE,
60
+ glossary_vocabulary,
61
+ read_project_vocabulary,
62
+ )
63
+
64
+ __all__ = [
65
+ "DEFAULT_CAPACITY",
66
+ "DEFAULT_TTL_SECONDS",
67
+ "GLOSSARY_ABBREVIATIONS_SCHEMA_VERSION",
68
+ "GLOSSARY_FILE",
69
+ "GLOSSARY_SCHEMA_VERSION",
70
+ "NON_CANONICAL_TERM_CATEGORY",
71
+ "SUPPORTED_LOCALES",
72
+ "UNASSIGNED_CONTEXT",
73
+ "USAGE_REPORT_FILE",
74
+ "BoundedContext",
75
+ "Glossary",
76
+ "GlossaryTerm",
77
+ "GlossaryTranslator",
78
+ "HarvestCandidate",
79
+ "MergeResult",
80
+ "PhraseTranslation",
81
+ "RenderState",
82
+ "ScaffoldingBundle",
83
+ "SynonymAlias",
84
+ "TermCandidate",
85
+ "TermKey",
86
+ "TermKind",
87
+ "TermStatus",
88
+ "TraceTranslationView",
89
+ "TranslationFileSink",
90
+ "TranslationSubscriber",
91
+ "VocabularyViolation",
92
+ "aggregate_violations",
93
+ "build_alias_index",
94
+ "class_candidate",
95
+ "exception_candidate",
96
+ "format_violation_details",
97
+ "format_vocabulary_summary",
98
+ "glossary_vocabulary",
99
+ "harvest_traces",
100
+ "load_glossary",
101
+ "merge_harvest",
102
+ "method_candidates",
103
+ "non_canonical_term_issues",
104
+ "normalize_phrase",
105
+ "parameter_candidate",
106
+ "read_glossary_json",
107
+ "read_project_vocabulary",
108
+ "render_glossary_markdown",
109
+ "resolve_context",
110
+ "suggest_rename",
111
+ "usage_report_document",
112
+ "write_glossary_json",
113
+ "write_usage_report",
114
+ ]
@@ -0,0 +1,53 @@
1
+ """Lookup from ``(context, normalized alias)`` to the canonical term that deprecates it.
2
+
3
+ ``AliasIndex``, whose three methods collapse into one function returning a read-only
4
+ mapping: ``key in index`` answers Java's ``isAlias`` and ``index.get(key)`` answers its
5
+ ``canonicalFor``. INTENT: harvesting and violation reporting both ask the same question, so the
6
+ answer is precomputed once per glossary.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import Mapping
12
+ from types import MappingProxyType
13
+
14
+ from narrativetrace_glossary.models import Glossary, GlossaryTerm, TermKey
15
+
16
+
17
+ def build_alias_index(glossary: Glossary) -> Mapping[TermKey, GlossaryTerm]:
18
+ """Indexes every ``(term context, synonym alias)`` pair the glossary declares.
19
+
20
+ Matching is exact on the whole normalized phrase — neither a substring nor a superstring of an
21
+ alias is one — and scoped per context, so an alias is recognized only where its canonical term
22
+ is defined.
23
+
24
+ Returns:
25
+ A read-only mapping; a phrase absent from it is not a deprecated alias.
26
+
27
+ Raises:
28
+ TypeError: if ``glossary`` is not a :class:`Glossary`.
29
+ """
30
+ if not isinstance(glossary, Glossary):
31
+ raise TypeError("glossary must be a Glossary")
32
+ index = MappingProxyType(
33
+ {
34
+ TermKey(term.context, synonym.alias): term
35
+ for term in glossary.terms
36
+ for synonym in term.synonyms
37
+ }
38
+ )
39
+ assert _invariant(index), "every indexed key must name an alias of the term it maps to"
40
+ return index
41
+
42
+
43
+ def _invariant(index: Mapping[TermKey, GlossaryTerm]) -> bool:
44
+ """Returns whether an index is consistent with the terms it points at.
45
+
46
+ Every key must be filed under its canonical term's own context and name a phrase that term
47
+ actually deprecates — otherwise a lookup would suppress a harvest on someone else's behalf.
48
+ """
49
+ return all(
50
+ key.context == term.context
51
+ and any(synonym.alias == key.normalized for synonym in term.synonyms)
52
+ for key, term in index.items()
53
+ )
@@ -0,0 +1,47 @@
1
+ """Maps vocabulary violations onto the clarity issue model.
2
+
3
+ ``NonCanonicalTermIssues``. INTENT: a deprecated phrasing is a naming-clarity concern like
4
+ any other, so it rides the same ``ClarityIssue``/``Severity`` pipeline the built-in scorers use —
5
+ one issue category, ``non-canonical-term``, contributed by the glossary rather than a dictionary.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Sequence
11
+
12
+ from narrativetrace_clarity import ClarityIssue, Severity
13
+
14
+ from narrativetrace_glossary.violations import VocabularyViolation
15
+
16
+ NON_CANONICAL_TERM_CATEGORY = "non-canonical-term"
17
+ """Clarity issue category for a phrasing the glossary has deprecated."""
18
+
19
+
20
+ def _suggestion(violation: VocabularyViolation) -> str:
21
+ return (
22
+ f"'{violation.alias}' is deprecated in favor of '{violation.canonical_term}' "
23
+ f"(rename to '{violation.suggested_rename}')"
24
+ )
25
+
26
+
27
+ def _issue_of(violation: VocabularyViolation) -> ClarityIssue:
28
+ base = ClarityIssue(
29
+ category=NON_CANONICAL_TERM_CATEGORY,
30
+ element=f"{violation.site}: {violation.identifier}",
31
+ suggestion=_suggestion(violation),
32
+ severity=Severity.MEDIUM,
33
+ )
34
+ return base.with_occurrences(violation.occurrences)
35
+
36
+
37
+ def non_canonical_term_issues(
38
+ violations: Sequence[VocabularyViolation],
39
+ ) -> tuple[ClarityIssue, ...]:
40
+ """Converts vocabulary violations into ``non-canonical-term`` clarity issues, one per violation.
41
+
42
+ Severity is always :attr:`~narrativetrace_clarity.Severity.MEDIUM` — a deprecated phrasing is
43
+ a real naming defect, but never as severe as ``HIGH``-tier structural issues (matches the
44
+ plan's advisory-by-default posture: suite issues are logged, and only fail a build that opts in
45
+ via a hard cap).
46
+ """
47
+ return tuple(_issue_of(violation) for violation in violations)
@@ -0,0 +1,30 @@
1
+ """Glossary-aware entry point for the clarity gate — vocabulary-check defaults (prerequisite 5).
2
+
3
+ ``GlossaryAwareClarityScannerMain``. INTENT: composes ``narrativetrace-clarity``'s CLI
4
+ with :func:`~narrativetrace_glossary.vocabulary.read_project_vocabulary` without either package
5
+ importing the other's internals — ``narrativetrace_clarity.cli.main`` takes the reader as a plain
6
+ callback (see its module docstring). The root ``clarity`` gate task runs *this* module, not
7
+ ``narrativetrace-clarity`` directly, which is what makes glossary-aware scoring the actual default:
8
+ a project with no committed glossary degrades to the built-in dictionaries exactly as before,
9
+ silently and without a separate opt-in flag.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import TYPE_CHECKING
15
+
16
+ from narrativetrace_clarity.cli import main as _clarity_main
17
+
18
+ from narrativetrace_glossary.vocabulary import read_project_vocabulary
19
+
20
+ if TYPE_CHECKING:
21
+ from collections.abc import Sequence
22
+
23
+
24
+ def main(argv: Sequence[str] | None = None) -> int:
25
+ """Runs the clarity scan/gate with the repository's committed glossary as domain vocabulary."""
26
+ return _clarity_main(argv, vocabulary_reader=read_project_vocabulary)
27
+
28
+
29
+ if __name__ == "__main__": # pragma: no cover
30
+ raise SystemExit(main())
@@ -0,0 +1,65 @@
1
+ """Builds a ``class_name -> module path`` lookup from already-captured trace trees.
2
+
3
+ ``ClassPackageIndex`` (Java's compiled-class-directory scan, .NET's loaded-assembly
4
+ scan). INTENT: :func:`~narrativetrace_glossary.harvester.harvest_traces` takes a ``module_of``
5
+ callable but does not build one itself — every runtime resolves it differently. Here, the resolver
6
+ that matters is trivial: :attr:`~narrativetrace.signature.MethodSignature.package_name` is already
7
+ captured on every node (``__module__`` at trace time), so this only needs to aggregate it and
8
+ resolve ambiguity, never scan anything.
9
+
10
+ **Do not default this to "always None".** A prior mirror (dotnet, audit finding G1a) shipped a
11
+ suite reporter that passed a namespace resolver returning ``None`` unconditionally, so every
12
+ harvested term landed in ``_unassigned`` and every bounded context a project declared was
13
+ decoration. Ambiguity is resolved to ``None`` deliberately (a class name seen under two different
14
+ modules names no single context); *absence of any observation* must never be confused with that —
15
+ it is also ``None``, but for the ordinary reason that harvesting only ever sees the classes a run
16
+ actually touched.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from collections.abc import Callable, Sequence
22
+
23
+ from narrativetrace.tree import TraceTree
24
+ from narrativetrace.tree_walk import TreeWalk
25
+
26
+
27
+ def _walk(tree_roots: Sequence[object], observed: dict[str, set[str | None]]) -> None:
28
+ walk = TreeWalk()
29
+ _walk_nodes(tree_roots, observed, walk)
30
+
31
+
32
+ def _walk_nodes(
33
+ nodes: Sequence[object], observed: dict[str, set[str | None]], walk: TreeWalk
34
+ ) -> None:
35
+ for node in nodes:
36
+ signature = node.signature # type: ignore[attr-defined]
37
+ observed.setdefault(signature.class_name, set()).add(signature.package_name)
38
+ if walk.stop_reason(node) is None:
39
+ walk.enter(node)
40
+ try:
41
+ _walk_nodes(node.children, observed, walk) # type: ignore[attr-defined]
42
+ finally:
43
+ walk.exit(node)
44
+
45
+
46
+ def class_package_index(trees: Sequence[TraceTree]) -> Callable[[str], str | None]:
47
+ """Builds a ``module_of`` resolver from the ``package_name`` every node already carries.
48
+
49
+ A class name observed under exactly one non-``None`` module resolves to it; observed under two
50
+ or more distinct modules (or never observed, or only ever observed without a captured module)
51
+ resolves to ``None`` — files that class's terms under ``_unassigned`` rather than guessing.
52
+
53
+ Returns:
54
+ A callable suitable as :func:`~narrativetrace_glossary.harvester.harvest_traces`'s
55
+ ``module_of`` argument.
56
+ """
57
+ observed: dict[str, set[str | None]] = {}
58
+ for tree in trees:
59
+ _walk(tree.roots, observed)
60
+ resolved = {
61
+ class_name: next(iter(modules))
62
+ for class_name, modules in observed.items()
63
+ if len(modules) == 1 and next(iter(modules)) is not None
64
+ }
65
+ return resolved.get
@@ -0,0 +1,40 @@
1
+ """Resolves the dotted module path of traced code to the bounded context that owns it.
2
+
3
+ ``ContextResolver``, whose Java package prefixes become dotted module-path prefixes
4
+ (``acme.billing``, not ``com.acme.billing``). INTENT: contexts declare their prefixes in the
5
+ glossary file itself, so the file stays the single source of truth.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from narrativetrace_glossary.models import Glossary
11
+
12
+ UNASSIGNED_CONTEXT = "_unassigned"
13
+ """Fallback context for module paths no declared context owns."""
14
+
15
+
16
+ def resolve_context(glossary: Glossary, module_path: str) -> str:
17
+ """Resolves a module path to the name of the context that declares a matching prefix.
18
+
19
+ The longest matching prefix wins, so contexts may nest; no match resolves to
20
+ :data:`UNASSIGNED_CONTEXT`, which is what lets harvesting work with zero configuration.
21
+ """
22
+ owners = [
23
+ (len(prefix), name)
24
+ for name in sorted(glossary.contexts)
25
+ for prefix in glossary.contexts[name].packages
26
+ if _owns(prefix, module_path)
27
+ ]
28
+ if not owners:
29
+ return UNASSIGNED_CONTEXT
30
+ # Sorted iteration plus max()'s first-maximal rule makes equal-length ties deterministic:
31
+ # the alphabetically first context name wins.
32
+ return max(owners, key=lambda owner: owner[0])[1]
33
+
34
+
35
+ def _owns(prefix: str, module_path: str) -> bool:
36
+ """Delimiter-aware prefix test: the module itself, or a child module separated by a dot.
37
+
38
+ ``acme.billing`` owns ``acme.billing.overdraft`` but never the sibling ``acme.billingx``.
39
+ """
40
+ return module_path == prefix or module_path.startswith(f"{prefix}.")