ambitgraph 0.0.1__tar.gz → 0.2.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 (93) hide show
  1. ambitgraph-0.2.0/LICENSE +25 -0
  2. ambitgraph-0.2.0/MANIFEST.in +25 -0
  3. ambitgraph-0.2.0/PKG-INFO +83 -0
  4. ambitgraph-0.2.0/README.md +55 -0
  5. ambitgraph-0.2.0/ambit_map/__init__.py +7 -0
  6. ambitgraph-0.2.0/ambit_map/__main__.py +6 -0
  7. ambitgraph-0.2.0/ambit_map/ambit-map-enrich.md +54 -0
  8. ambitgraph-0.2.0/ambit_map/ambit-mapllm-contract.md +246 -0
  9. ambitgraph-0.2.0/ambit_map/cli.py +223 -0
  10. ambitgraph-0.2.0/ambit_map/daemon.py +1029 -0
  11. ambitgraph-0.2.0/ambit_map/dialects/__init__.py +5 -0
  12. ambitgraph-0.2.0/ambit_map/dialects/contract.py +1500 -0
  13. ambitgraph-0.2.0/ambit_map/dialects/express_pack.py +220 -0
  14. ambitgraph-0.2.0/ambit_map/dialects/fastapi_pack.py +164 -0
  15. ambitgraph-0.2.0/ambit_map/dialects/flask_pack.py +184 -0
  16. ambitgraph-0.2.0/ambit_map/dialects/nextjs_pack.py +127 -0
  17. ambitgraph-0.2.0/ambit_map/dialects/supabase_pack.py +335 -0
  18. ambitgraph-0.2.0/ambit_map/export.py +321 -0
  19. ambitgraph-0.2.0/ambit_map/families.py +255 -0
  20. ambitgraph-0.2.0/ambit_map/graph.py +1949 -0
  21. ambitgraph-0.2.0/ambit_map/island_layout.py +289 -0
  22. ambitgraph-0.2.0/ambit_map/islands.py +336 -0
  23. ambitgraph-0.2.0/ambit_map/judged.py +279 -0
  24. ambitgraph-0.2.0/ambit_map/kitdemo.py +275 -0
  25. ambitgraph-0.2.0/ambit_map/library.py +1727 -0
  26. ambitgraph-0.2.0/ambit_map/load.py +739 -0
  27. ambitgraph-0.2.0/ambit_map/mapllm.py +594 -0
  28. ambitgraph-0.2.0/ambit_map/matched.py +705 -0
  29. ambitgraph-0.2.0/ambit_map/model.py +419 -0
  30. ambitgraph-0.2.0/ambit_map/objects.py +1000 -0
  31. ambitgraph-0.2.0/ambit_map/orchestrate.py +1230 -0
  32. ambitgraph-0.2.0/ambit_map/page.py +112 -0
  33. ambitgraph-0.2.0/ambit_map/paths.py +1442 -0
  34. ambitgraph-0.2.0/ambit_map/registry.py +3267 -0
  35. ambitgraph-0.2.0/ambit_map/serve.py +1931 -0
  36. ambitgraph-0.2.0/ambit_map/theme.py +515 -0
  37. ambitgraph-0.2.0/ambit_map/unitpage.py +1012 -0
  38. ambitgraph-0.2.0/ambitgraph/WALKTHROUGH.md +188 -0
  39. ambitgraph-0.2.0/ambitgraph/__init__.py +6 -0
  40. ambitgraph-0.2.0/ambitgraph/__main__.py +5 -0
  41. ambitgraph-0.2.0/ambitgraph/cli.py +2784 -0
  42. ambitgraph-0.2.0/ambitgraph/cockpit.py +32 -0
  43. ambitgraph-0.2.0/ambitgraph/codelib.py +782 -0
  44. ambitgraph-0.2.0/ambitgraph/config.py +771 -0
  45. ambitgraph-0.2.0/ambitgraph/console.py +341 -0
  46. ambitgraph-0.2.0/ambitgraph/demo.py +159 -0
  47. ambitgraph-0.2.0/ambitgraph/docs_discovery.py +280 -0
  48. ambitgraph-0.2.0/ambitgraph/doctor.py +178 -0
  49. ambitgraph-0.2.0/ambitgraph/duplication.py +724 -0
  50. ambitgraph-0.2.0/ambitgraph/edges.py +2155 -0
  51. ambitgraph-0.2.0/ambitgraph/emit.py +532 -0
  52. ambitgraph-0.2.0/ambitgraph/engine_identity.py +87 -0
  53. ambitgraph-0.2.0/ambitgraph/estimate.py +315 -0
  54. ambitgraph-0.2.0/ambitgraph/eval.py +126 -0
  55. ambitgraph-0.2.0/ambitgraph/feeds.py +153 -0
  56. ambitgraph-0.2.0/ambitgraph/gate.py +505 -0
  57. ambitgraph-0.2.0/ambitgraph/gate_check.py +552 -0
  58. ambitgraph-0.2.0/ambitgraph/gate_server.py +78 -0
  59. ambitgraph-0.2.0/ambitgraph/harvest.py +513 -0
  60. ambitgraph-0.2.0/ambitgraph/inventory.py +402 -0
  61. ambitgraph-0.2.0/ambitgraph/jscalls.py +1561 -0
  62. ambitgraph-0.2.0/ambitgraph/mds.py +145 -0
  63. ambitgraph-0.2.0/ambitgraph/merge.py +4481 -0
  64. ambitgraph-0.2.0/ambitgraph/merge_jsp.py +533 -0
  65. ambitgraph-0.2.0/ambitgraph/parameterize.py +1778 -0
  66. ambitgraph-0.2.0/ambitgraph/pipeline.py +565 -0
  67. ambitgraph-0.2.0/ambitgraph/portal.py +973 -0
  68. ambitgraph-0.2.0/ambitgraph/pycalls.py +218 -0
  69. ambitgraph-0.2.0/ambitgraph/ratify.py +263 -0
  70. ambitgraph-0.2.0/ambitgraph/report.py +1331 -0
  71. ambitgraph-0.2.0/ambitgraph/reports.py +88 -0
  72. ambitgraph-0.2.0/ambitgraph/sample.py +97 -0
  73. ambitgraph-0.2.0/ambitgraph/secretary.py +229 -0
  74. ambitgraph-0.2.0/ambitgraph/shell.py +7703 -0
  75. ambitgraph-0.2.0/ambitgraph/stages.py +341 -0
  76. ambitgraph-0.2.0/ambitgraph/symbols.py +7073 -0
  77. ambitgraph-0.2.0/ambitgraph/tables.py +286 -0
  78. ambitgraph-0.2.0/ambitgraph/tasks.py +106 -0
  79. ambitgraph-0.2.0/ambitgraph/urcode.py +78 -0
  80. ambitgraph-0.2.0/ambitgraph.egg-info/PKG-INFO +83 -0
  81. ambitgraph-0.2.0/ambitgraph.egg-info/SOURCES.txt +85 -0
  82. ambitgraph-0.2.0/ambitgraph.egg-info/entry_points.txt +2 -0
  83. ambitgraph-0.2.0/ambitgraph.egg-info/requires.txt +12 -0
  84. {ambitgraph-0.0.1 → ambitgraph-0.2.0}/ambitgraph.egg-info/top_level.txt +1 -0
  85. ambitgraph-0.2.0/pyproject.toml +62 -0
  86. ambitgraph-0.0.1/PKG-INFO +0 -23
  87. ambitgraph-0.0.1/README.md +0 -9
  88. ambitgraph-0.0.1/ambitgraph/__init__.py +0 -7
  89. ambitgraph-0.0.1/ambitgraph.egg-info/PKG-INFO +0 -23
  90. ambitgraph-0.0.1/ambitgraph.egg-info/SOURCES.txt +0 -7
  91. ambitgraph-0.0.1/pyproject.toml +0 -24
  92. {ambitgraph-0.0.1 → ambitgraph-0.2.0}/ambitgraph.egg-info/dependency_links.txt +0 -0
  93. {ambitgraph-0.0.1 → ambitgraph-0.2.0}/setup.cfg +0 -0
@@ -0,0 +1,25 @@
1
+ AmbitGraph — Proprietary Software License
2
+ Copyright (c) 2026 AmbitGraph. All rights reserved.
3
+
4
+ This software and its source code (the "Software") are proprietary and
5
+ confidential. The Software is licensed, not sold.
6
+
7
+ 1. GRANT. No license or right to use, copy, modify, merge, publish,
8
+ distribute, sublicense, or sell copies of the Software is granted except
9
+ under a separate written agreement with the copyright holder. Absent such
10
+ an agreement, no rights are conferred by the availability of this package.
11
+
12
+ 2. RESTRICTIONS. You may not reverse engineer, decompile, or disassemble the
13
+ Software, nor remove or alter any proprietary notice, except to the extent
14
+ such restriction is prohibited by applicable law.
15
+
16
+ 3. NO WARRANTY. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
17
+ KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
19
+
20
+ 4. LIMITATION OF LIABILITY. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
21
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN
22
+ AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN
23
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
+
25
+ For licensing inquiries, contact the copyright holder.
@@ -0,0 +1,25 @@
1
+ # The source distribution (.tar.gz on PyPI) ships the packages, the README,
2
+ # pyproject and the LICENSE only. Everything below is development-only.
3
+
4
+ prune tests
5
+ prune tools
6
+ prune docs
7
+ prune build
8
+ prune dist
9
+
10
+ # Internal-only packages -- also excluded from the wheel via the packages list.
11
+ prune ambit_contracts
12
+ prune ambit_telegraph
13
+
14
+ # Development evidence directories (screenshots, transcripts, review notes).
15
+ # Both spellings: the dated evidence-* folders and the single evidence/ folder they moved into.
16
+ prune evidence-*
17
+ prune evidence
18
+
19
+ # Internal process documents.
20
+ exclude AGENTS.md
21
+ exclude CHANGELOG.md
22
+ exclude DATA-FLOW.md
23
+
24
+ global-exclude *.pyc
25
+ global-exclude __pycache__/*
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.4
2
+ Name: ambitgraph
3
+ Version: 0.2.0
4
+ Summary: Read-only codebase scan: reusable units, families, roles, ownership candidates, duplication.
5
+ Author: AmbitGraph
6
+ License: Proprietary
7
+ Keywords: codebase,scan,audit,duplication,refactoring,reuse
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Topic :: Software Development :: Quality Assurance
15
+ Requires-Python: >=3.11
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: pyyaml>=6
19
+ Provides-Extra: ui
20
+ Requires-Dist: playwright>=1.40; extra == "ui"
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=7; extra == "dev"
23
+ Requires-Dist: mcp<2,>=1.2; extra == "dev"
24
+ Requires-Dist: jsonschema>=4; extra == "dev"
25
+ Provides-Extra: gate
26
+ Requires-Dist: mcp<2,>=1.2; extra == "gate"
27
+ Dynamic: license-file
28
+
29
+ # AmbitGraph
30
+
31
+ **You don't understand your own product!**
32
+
33
+ **You can't keep up with the amount of code!**
34
+
35
+ One command, and you do. Deterministic. No tokens.
36
+
37
+ AmbitGraph draws your whole repo as a visual interactive map & library and gives them to you. It needs Python on your machine, never in your project. Free, offline, no account, no API key.
38
+
39
+ Python 3.11 or newer.
40
+
41
+ ## Install and run
42
+
43
+ ```
44
+ pip install ambitgraph
45
+ python -m ambitgraph map <folder>
46
+ ```
47
+
48
+ `ambit map <folder>` does the same when your Python scripts folder is on your PATH.
49
+
50
+ The command gives your terminal back and the map keeps serving. Stop it with `python -m ambitgraph map <folder> --stop`.
51
+
52
+ Your codebase is displayed in your browser as local webpages.
53
+
54
+ It does not make any changes to your codebase.
55
+
56
+ ## A GRAPH
57
+
58
+ Modules, Classes, Imports, Calls, DB Tables, anything in your code mapped on its connections.
59
+
60
+ ## An Indexed and Searchable LIBRARY
61
+
62
+ Understand what you have, what it does, and what it means.
63
+
64
+ Understand your own codebase in a way you never have. Simple, visual, and if you want more you can ask your agent what the hell it just did. A vibe coder can finally see how their code works. A senior can see at a glance when something has got too complicated.
65
+
66
+ ## Your machine
67
+
68
+ The map is served to your own machine, stays on your machine. It and your code belong to you
69
+
70
+ ## No AI
71
+
72
+ No AI is required. The map is plain reading of your code.
73
+
74
+ No model runs, no tokens burn, no key exists. If you want more than the map, your own assistant can work on top of it, on your own subscription, with whatever model you already pay for. Optional, always, and the map is finished without it.
75
+
76
+ ## In this version
77
+
78
+ Search finds functions and tables by name. The Library groups every file the
79
+ scan read into families. The Graph draws one file and everything one hop from
80
+ it, and every row in the Library opens it.
81
+
82
+ Search does not find routes or file names. Drawn connections do not carry
83
+ their meaning beside them. There are no written explanation pages.
@@ -0,0 +1,55 @@
1
+ # AmbitGraph
2
+
3
+ **You don't understand your own product!**
4
+
5
+ **You can't keep up with the amount of code!**
6
+
7
+ One command, and you do. Deterministic. No tokens.
8
+
9
+ AmbitGraph draws your whole repo as a visual interactive map & library and gives them to you. It needs Python on your machine, never in your project. Free, offline, no account, no API key.
10
+
11
+ Python 3.11 or newer.
12
+
13
+ ## Install and run
14
+
15
+ ```
16
+ pip install ambitgraph
17
+ python -m ambitgraph map <folder>
18
+ ```
19
+
20
+ `ambit map <folder>` does the same when your Python scripts folder is on your PATH.
21
+
22
+ The command gives your terminal back and the map keeps serving. Stop it with `python -m ambitgraph map <folder> --stop`.
23
+
24
+ Your codebase is displayed in your browser as local webpages.
25
+
26
+ It does not make any changes to your codebase.
27
+
28
+ ## A GRAPH
29
+
30
+ Modules, Classes, Imports, Calls, DB Tables, anything in your code mapped on its connections.
31
+
32
+ ## An Indexed and Searchable LIBRARY
33
+
34
+ Understand what you have, what it does, and what it means.
35
+
36
+ Understand your own codebase in a way you never have. Simple, visual, and if you want more you can ask your agent what the hell it just did. A vibe coder can finally see how their code works. A senior can see at a glance when something has got too complicated.
37
+
38
+ ## Your machine
39
+
40
+ The map is served to your own machine, stays on your machine. It and your code belong to you
41
+
42
+ ## No AI
43
+
44
+ No AI is required. The map is plain reading of your code.
45
+
46
+ No model runs, no tokens burn, no key exists. If you want more than the map, your own assistant can work on top of it, on your own subscription, with whatever model you already pay for. Optional, always, and the map is finished without it.
47
+
48
+ ## In this version
49
+
50
+ Search finds functions and tables by name. The Library groups every file the
51
+ scan read into families. The Graph draws one file and everything one hop from
52
+ it, and every row in the Library opens it.
53
+
54
+ Search does not find routes or file names. Drawn connections do not carry
55
+ their meaning beside them. There are no written explanation pages.
@@ -0,0 +1,7 @@
1
+ """The AmbitGraph product package — the shipped two-tab map (Graph + Library).
2
+
3
+ Consumes the engine's artifact files (results/symbols.json, results/edges.json)
4
+ and drives the engine only as a subprocess (ambit scan). Imports NO company
5
+ code — not ambitgraph, not ambit_telegraph, not ambit_contracts — and the wall
6
+ is mechanical: tests/test_import_boundaries.py. The engine reaches this package
7
+ only through a sanctioned lazy seam in cli.py (lands with a later wave)."""
@@ -0,0 +1,6 @@
1
+ """`python -m ambit_map <repo>` -- the package's command-line entry point."""
2
+ import sys
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ sys.exit(main(sys.argv[1:]))
@@ -0,0 +1,54 @@
1
+ # Prose judgment — the optional assistant pass
2
+
3
+ AmbitGraph's map is complete without any AI. Everything the Library and the Graph show is
4
+ measured: symbols and edges come from the engine's deterministic scan of your files, with
5
+ no model and no network.
6
+
7
+ What a scan cannot tell you is what a unit is **for** — the sentence a human would write.
8
+ That is *judgment*, not measurement, and this product never invents it. If you want it, you
9
+ run it yourself, with your own assistant, on your own machine, and the map keeps the two
10
+ vocabularies visibly apart: a judged note always renders inside the collapsed
11
+ "Judged by your assistant" block and always carries the `judged` chip. No judged number is
12
+ ever added to a measured number.
13
+
14
+ ## What you need
15
+
16
+ - A completed scan. Its output directory is written as `<run-dir>` below — it is the
17
+ directory `ambit map` printed when it finished, the one holding `results/`,
18
+ `inventory.json` and `pending/`.
19
+ - An assistant that can run a workflow script and write a JSON file. Today's supported
20
+ path is Claude Code; any capable assistant can satisfy the same contract, because your
21
+ artifacts are plain JSON on your disk.
22
+
23
+ ## The loop
24
+
25
+ One harvest advances **one stage only**. The stages run in order: `extract`, then
26
+ `cluster`, then `roles` — and `vocab` or `ownership` when your scan configures them.
27
+ The draft map does not exist until the last stage has run.
28
+
29
+ 1. Open `<run-dir>/pending/` and pick the next `*.workflow.js` script.
30
+ 2. Run that script in an assistant session.
31
+ 3. Harvest what it wrote:
32
+
33
+ ambit harvest <stage> --scan "<run-dir>" --journal <transcriptDir>/journal.jsonl
34
+
35
+ `<stage>` is the script's own stage name. `<transcriptDir>/journal.jsonl` is the
36
+ journal your assistant session wrote; it is not a path this map can know ahead of time.
37
+ Quote `<run-dir>` — an unquoted path containing a space splits into two arguments.
38
+ 4. Re-run the exact `ambit scan` command that produced this run, pointed at this same
39
+ output directory, from your terminal. This page cannot resume a run; it can only start
40
+ a fresh one.
41
+ 5. Repeat 1-4 until `pending/` is empty and the draft map is written.
42
+
43
+ Only then does reloading the Library show judged notes.
44
+
45
+ ## What you get, and what you do not
46
+
47
+ You get, per unit the assistant could pin to a file this scan read: what it is, a role
48
+ with a confidence, a parent, and a structural family name. You do not get any change to a
49
+ measured number, and you do not get a claim about a file the scan never read — a judged
50
+ note that cannot be joined to a real unit is listed separately, as unjoined, and counted
51
+ separately.
52
+
53
+ If the draft cannot be read at all, the map says so by name and shows nothing else from
54
+ that plane. Fail-closed is the rule: a plane that did not load never renders as a zero.
@@ -0,0 +1,246 @@
1
+ # The mapllm ingestion contract — the shapes your assistant's files must carry
2
+
3
+ ambit-map-enrich.md already states the law this document generalises: "Today's supported path
4
+ is Claude Code; ANY capable assistant can satisfy the same contract, because your artifacts are
5
+ plain JSON on your disk." That clause is the whole idea — this document turns it into an actual
6
+ schema, so an assistant that is not Claude Code (or a hand-written script) can write a
7
+ conforming file directly and be read exactly the same way.
8
+
9
+ The law stated plainly, again, because it never changes: everything in this document produces
10
+ *judgment*, not measurement. Nothing here is read, parsed, or counted by the deterministic scan.
11
+ The map keeps the two vocabularies visibly apart — a judged note always renders inside the
12
+ collapsed "Judged by your assistant" block, always carries the `judged` chip, and no judged
13
+ number is ever added to a measured number. A plane built from this contract DECORATES the map;
14
+ it never becomes authoritative over anything the scan itself measured.
15
+
16
+ ## Two ways to produce these files
17
+
18
+ 1. **The workflow-script loop** (ambit-map-enrich.md's own loop): run the script `pending/
19
+ <stage>.workflow.js` in your assistant session, then reduce its journal with `ambit harvest
20
+ <stage> --scan <run-dir> --journal <journal>`. The harvester folds one or many analysts'
21
+ records into the single `results/<stage>.json` file this document specifies below —
22
+ deduplicating, validating against the enumerations each stage declares, and degrading a
23
+ partial or malformed record rather than discarding the run's other work.
24
+ 2. **The direct-results door**: write `results/<stage>.json` yourself, in the exact shape this
25
+ document names for that stage, and skip the workflow-script loop entirely. mapllm's conductor
26
+ accepts a directly-written results file exactly as it accepts one `ambit harvest` produced —
27
+ the file on disk is the whole contract; nothing tracks how it got there.
28
+
29
+ Either way, a stage advances only once its `results/<stage>.json` exists; re-running the same
30
+ scan command against the same output directory picks up from there.
31
+
32
+ ## The stage results files
33
+
34
+ Every stage below is optional except `extract`, `cluster`, and `roles` — `vocab` and `ownership`
35
+ run only when the scan config declares a vocabulary or ownership areas; `suggest` runs only when
36
+ the draft has ambiguous units queued for a human's ruling.
37
+
38
+ ### The `extract` stage — `results/<stage>.json`
39
+
40
+ ```json
41
+ {
42
+ "units": [
43
+ {
44
+ "anchor": "helpers.py::shout",
45
+ "file": "helpers.py",
46
+ "hook": "shout",
47
+ "granularity": "component",
48
+ "role0": "structure_owner",
49
+ "hint": "a small formatting helper",
50
+ "ev": "imported and called by main.py",
51
+ "similar_to": []
52
+ }
53
+ ],
54
+ "uncovered": []
55
+ }
56
+ ```
57
+
58
+ `granularity` is one of `module` (whole-file scope), `component` (a cohesive unit inside a
59
+ file), or `fragment` (a small piece that composes into larger units) — or the empty string when
60
+ unknown. `role0` is one of `structure_owner` (this code DEFINES the shared structure), `filler`
61
+ (this code USES a structure defined elsewhere), or `unclear`. `anchor` is `file::hook` — the
62
+ stable pair every other stage's file joins on. `uncovered` names manifest paths no unit was
63
+ reported for; it is informational, never a refusal.
64
+
65
+ ### The `cluster` stage — `results/<stage>.json`
66
+
67
+ ```json
68
+ {
69
+ "families": [
70
+ {"name": "widgets", "members": ["helpers.py::shout", "main.py::run_report"]}
71
+ ]
72
+ }
73
+ ```
74
+
75
+ Every anchor from `extract.json` appears in exactly one family's `members`. A unit that fits no
76
+ real family belongs in one named `singletons`, never omitted. An optional `warnings` array
77
+ (strings) may accompany a run that had to degrade rather than discard part of the reduction.
78
+
79
+ ### The `roles` stage — `results/<stage>.json`
80
+
81
+ ```json
82
+ {
83
+ "roles": {
84
+ "helpers.py::shout": {"role": "structure_owner", "parent": "", "confidence": "high",
85
+ "family": "widgets"},
86
+ "main.py::run_report": {"role": "filler", "parent": "helpers.py::shout",
87
+ "confidence": "high", "family": "widgets"}
88
+ }
89
+ }
90
+ ```
91
+
92
+ Keyed by anchor. `role` is `structure_owner` | `filler` | `unclear`. `parent` is the anchor a
93
+ `filler` delegates to, written verbatim as anchor text (empty when there is none). `confidence`
94
+ is `high` | `medium` | `low`.
95
+
96
+ ### The `vocab` and `ownership` stages — `results/<stage>.json`
97
+
98
+ Written only when the scan config declares `vocabulary` (for `vocab.json`) or `areas` (for
99
+ `ownership.json`) — each a name-to-definition mapping the config itself states.
100
+
101
+ ```json
102
+ {
103
+ "assignments": {
104
+ "helpers.py::shout": {"family": "widget", "confidence": "high"}
105
+ },
106
+ "missing": [],
107
+ "invalid": []
108
+ }
109
+ ```
110
+
111
+ `vocab.json` uses the key `family`; `ownership.json` uses `area` in its place. The value is one
112
+ of the config's own declared names, or `"none"` (vocab) / `"unknown"` (ownership) — any other
113
+ value is rejected into `invalid`, never silently accepted. `missing` names anchors this file
114
+ assigns nothing to.
115
+
116
+ ### The `suggest` stage — `results/<stage>.json`
117
+
118
+ ```json
119
+ {
120
+ "suggestions": {
121
+ "helpers.py::shout": {"ruling": "keep", "rationale": "a tiny standalone helper, used once."}
122
+ },
123
+ "missing": [],
124
+ "invalid": []
125
+ }
126
+ ```
127
+
128
+ `ruling` is `keep` or `wrong` only — never `defer`; deciding later is the human reviewer's own
129
+ authority, not a machine suggestion. A missing or empty `rationale` is rejected into `invalid`.
130
+
131
+ ## The draft's own version and producer record
132
+
133
+ Once every required stage's results file exists, the scan assembles `map/draft_map.json` — the
134
+ one artifact this whole contract exists to make honest. It carries two fields no prior draft
135
+ ever had:
136
+
137
+ ```json
138
+ {
139
+ "v": 1,
140
+ "producer": {
141
+ "recorded_at": "2026-08-30T00:00:00Z",
142
+ "config_hash": "sha1:0123456789ab",
143
+ "engine_fingerprint": "…64 hex chars…"
144
+ }
145
+ }
146
+ ```
147
+
148
+ `v` is the draft's schema version, checked on load — see "Fail-closed" below for exactly what
149
+ counts as a match. `producer.tool`, when present, names in plain free text what produced the
150
+ stage results this draft was assembled from. Nothing in this contract names a producer for you:
151
+ a `results/<stage>.json` header carries no `producer.tool` unless something put one there — the
152
+ workflow-script loop does not name itself automatically. Today that something is `ambit harvest`'s
153
+ own `--producer` flag:
154
+
155
+ ambit harvest <stage> --scan "<run-dir>" --journal <journal> --producer "your assistant's name and version"
156
+
157
+ `--producer` is optional and free text — name any assistant or tool, or omit it entirely. A
158
+ direct-results-door author writing `results/<stage>.json` by hand carries the identical header
159
+ shape without touching this flag at all: `{"producer": {"tool": "..."}}` sitting on the results
160
+ file is all either path needs to produce. When the `extract` stage's own `results/<stage>.json`
161
+ header never named a producer (the flag was omitted, or a legacy file predates this contract),
162
+ `producer.tool` is **omitted entirely** from the draft, exactly as the example above shows — never
163
+ written as a placeholder string, never guessed. A key that is present is always a real name
164
+ someone gave; a key that is absent means no one gave one. Whitespace-only names (`" "`) count as
165
+ no name at all, on both the writing and the reading side, and are stripped before anything is
166
+ stored — `ambit harvest --producer " "` prints one plain line saying so rather than silently
167
+ writing a blank name. `producer.config_hash` is the scan's own configuration hash (the same
168
+ value `results/symbols.json` and `results/edges.json` already carry); it binds this draft to the
169
+ scan it was produced against, so a draft copied onto a differently-configured scan is caught
170
+ rather than silently misjoined. `producer.engine_fingerprint` mirrors the draft's own top-level
171
+ fingerprint field, absence-disclosed the identical way (an `engine_fingerprint_absent` flag
172
+ stands in when no fingerprint could be computed).
173
+
174
+ ## Fail-closed, on every branch
175
+
176
+ The reader that loads this draft never raises on anything it finds on disk, and never renders a
177
+ guess:
178
+
179
+ - No draft file at all — the normal, unremarkable state before judgment has run. Nothing is
180
+ shown, nothing is claimed.
181
+ - A draft with no `"v"` key — every draft written before this contract existed. It loads exactly
182
+ as it always has; the absence of a version is never treated as a mismatch.
183
+ - A draft whose `"v"` key is present but is anything other than the plain integer `1` — refused
184
+ by name, nothing from the draft is read further. This includes `"v": null` (declared but
185
+ unusable is not the same fact as absent), `"v": true` (a boolean is never accepted as an
186
+ integer, even where it happens to equal `1` by value), and `"v": 1.0` (a float is never accepted
187
+ in place of the declared integer schema version) — every one of these is a real, present value
188
+ that simply is not this reader's version `1`.
189
+ - A draft whose `producer.config_hash` disagrees with the scan actually being served, when both
190
+ values are present — refused by name: the facts were produced against a different scan.
191
+ - Anything else malformed (unparseable JSON, a top level that is not an object, a `units` field
192
+ that is not a list) — refused by name, the same as always.
193
+
194
+ A refused draft never renders as an empty or zero judged plane — a plane that did not load never
195
+ counts as a plane that loaded empty. Every other field this document does not name is tolerated
196
+ additively: an unrecognised key is ignored, never a reason to refuse the whole file.
197
+
198
+ ## What the acceptance of a real repository taught
199
+
200
+ The rules above are the shapes; running a real assistant against a real, unmodified public
201
+ repository end to end found six ways a conforming file — or the run that produces it — can still
202
+ mislead a reader even while it parses cleanly. Each is a rule for the assistant writing these
203
+ files directly, not a change to the shapes themselves.
204
+
205
+ - **`parent` is a verbatim anchor, or the empty string — never prose, and never an external
206
+ framework's name.** It is the exact `file::hook` text some other unit's own `anchor` field
207
+ carries, copied byte for byte, so a reader can follow it with a lookup, not a guess. It is
208
+ never a sentence describing the relationship ("extends the base template"), and never the name
209
+ of a class, component or framework this scan's own unit set does not contain: when a filler's
210
+ true parent lives OUTSIDE what this run analysed — a third-party base class, a library
211
+ component — the correct value is the empty string, the SAME value written when there is no
212
+ parent at all. The external case IS the empty-string case; it is never spelled out in its own
213
+ words instead.
214
+ - **Evidence carries conclusions, never scratch reasoning or self-corrections.** The `ev` field,
215
+ and every free-text evidence field this contract accepts, states what you found — never the
216
+ thinking that got you there, and never a correction written into the record after the fact
217
+ ("actually, on reflection, it might instead..."). A reader who has to debug a train of thought
218
+ to recover the actual claim was handed reasoning, not evidence.
219
+ - **Counts never go inside an anchor.** An anchor names WHICH unit a record is about — it is an
220
+ identity, never a place to carry a measurement. A number belongs in a field built to hold one,
221
+ or is left out; folding it into the anchor text itself (`"helpers.py::shout (12 calls)"`) turns
222
+ an identity into a claim, and the two must never be the same string.
223
+ - **Never write "not in this file set," or any other claim about the file list's horizon.** A
224
+ large repository is read in chunks, and each analyst sees only the subset it was handed — a
225
+ sentence asserting what is or is not present ANYWHERE in the repository is a claim about ground
226
+ this one pass never covered. Worse, the harvester's own reduction keeps every evidence sentence
227
+ it receives VERBATIM: a horizon claim that was true of one chunk's own narrow view is carried
228
+ unchanged into the assembled draft, where it now reads as a claim about the WHOLE repository —
229
+ and is simply false there. State only what this pass itself observed inside the files it was
230
+ actually given.
231
+ - **The number of stages a run produces is repository-dependent — never a fixed count to
232
+ expect.** `vocab` and `ownership` run only when the scan's own config declares them (see "The
233
+ stage results files" above); `suggest` runs only when at least one unit could not be accepted
234
+ in bulk (the next rule names every reason that can queue one). Watch until the conductor's own
235
+ exit; never assume in advance how many stages a given run will produce.
236
+ - **The `suggest` queue is every unit the draft could not accept in bulk, and each entry's own
237
+ `why` field names every reason that queued it, comma-joined, in the engine's own words.** Four
238
+ independent checks fill the queue: `role == "unclear"` writes `"role unclear"`; otherwise a low
239
+ `confidence` writes `"low role confidence"`; no structural family assigned by the `cluster`
240
+ stage writes `"no structural family"`; low vocabulary confidence from the `vocab` stage writes
241
+ `"low vocabulary confidence"` — a unit can carry more than one of these at once, and none of
242
+ them is about duplicate code. Rule on the entry's OWN stated `why`, never a guess at what queued
243
+ it: for `"role unclear"`/`"low role confidence"` the question is "was my own prior uncertainty
244
+ justified" — but the other two reasons ask a genuinely different question, and the `why` string
245
+ is what tells you which one you are actually answering. Read it before ruling; do not assume it
246
+ is always the hedging question.