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.
- narrativetrace_glossary-0.1.0/.gitignore +28 -0
- narrativetrace_glossary-0.1.0/LICENSE +99 -0
- narrativetrace_glossary-0.1.0/PKG-INFO +83 -0
- narrativetrace_glossary-0.1.0/README.md +68 -0
- narrativetrace_glossary-0.1.0/pyproject.toml +41 -0
- narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/__init__.py +114 -0
- narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/alias_index.py +53 -0
- narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/clarity_issues.py +47 -0
- narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/clarity_scan.py +30 -0
- narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/class_package_index.py +65 -0
- narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/context_resolver.py +40 -0
- narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/glossary_loader.py +56 -0
- narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/glossary_scan_cli.py +66 -0
- narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/harvester.py +213 -0
- narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/json_reader.py +263 -0
- narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/json_writer.py +121 -0
- narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/markdown.py +132 -0
- narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/merger.py +155 -0
- narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/models.py +288 -0
- narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/normalizer.py +254 -0
- narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/py.typed +0 -0
- narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/rename_suggester.py +65 -0
- narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/scaffolding.py +84 -0
- narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/static_scan.py +248 -0
- narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/suite_harvest.py +103 -0
- narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/summary_formatter.py +47 -0
- narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/translation_file_sink.py +47 -0
- narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/translation_subscriber.py +146 -0
- narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/translation_view.py +242 -0
- narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/translator.py +97 -0
- narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/usage_report.py +86 -0
- narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/violations.py +82 -0
- narrativetrace_glossary-0.1.0/src/narrativetrace_glossary/vocabulary.py +79 -0
- narrativetrace_glossary-0.1.0/tests/conftest.py +91 -0
- narrativetrace_glossary-0.1.0/tests/glossary_strategies.py +115 -0
- narrativetrace_glossary-0.1.0/tests/test_alias_index.py +94 -0
- narrativetrace_glossary-0.1.0/tests/test_clarity_issues.py +65 -0
- narrativetrace_glossary-0.1.0/tests/test_clarity_scan.py +61 -0
- narrativetrace_glossary-0.1.0/tests/test_class_package_index.py +83 -0
- narrativetrace_glossary-0.1.0/tests/test_context_resolver.py +75 -0
- narrativetrace_glossary-0.1.0/tests/test_context_resolver_props.py +84 -0
- narrativetrace_glossary-0.1.0/tests/test_glossary.py +208 -0
- narrativetrace_glossary-0.1.0/tests/test_glossary_loader.py +70 -0
- narrativetrace_glossary-0.1.0/tests/test_glossary_props.py +116 -0
- narrativetrace_glossary-0.1.0/tests/test_glossary_scan_cli.py +82 -0
- narrativetrace_glossary-0.1.0/tests/test_glossary_vocabulary.py +213 -0
- narrativetrace_glossary-0.1.0/tests/test_harvester.py +288 -0
- narrativetrace_glossary-0.1.0/tests/test_harvester_props.py +129 -0
- narrativetrace_glossary-0.1.0/tests/test_json_reader.py +341 -0
- narrativetrace_glossary-0.1.0/tests/test_json_writer.py +169 -0
- narrativetrace_glossary-0.1.0/tests/test_markdown.py +220 -0
- narrativetrace_glossary-0.1.0/tests/test_merger.py +284 -0
- narrativetrace_glossary-0.1.0/tests/test_merger_props.py +101 -0
- narrativetrace_glossary-0.1.0/tests/test_models.py +174 -0
- narrativetrace_glossary-0.1.0/tests/test_normalizer.py +255 -0
- narrativetrace_glossary-0.1.0/tests/test_normalizer_props.py +84 -0
- narrativetrace_glossary-0.1.0/tests/test_rename_suggester.py +56 -0
- narrativetrace_glossary-0.1.0/tests/test_scaffolding.py +58 -0
- narrativetrace_glossary-0.1.0/tests/test_static_scan.py +171 -0
- narrativetrace_glossary-0.1.0/tests/test_suite_harvest.py +119 -0
- narrativetrace_glossary-0.1.0/tests/test_summary_formatter.py +58 -0
- narrativetrace_glossary-0.1.0/tests/test_translation_file_sink.py +58 -0
- narrativetrace_glossary-0.1.0/tests/test_translation_subscriber.py +216 -0
- narrativetrace_glossary-0.1.0/tests/test_translation_view.py +351 -0
- narrativetrace_glossary-0.1.0/tests/test_translation_view_props.py +136 -0
- narrativetrace_glossary-0.1.0/tests/test_translator.py +129 -0
- narrativetrace_glossary-0.1.0/tests/test_usage_report.py +116 -0
- 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}.")
|