ambitgraph 0.1.0__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.
- {ambitgraph-0.1.0/ambitgraph.egg-info → ambitgraph-0.2.0}/PKG-INFO +1 -1
- ambitgraph-0.2.0/ambit_map/ambit-mapllm-contract.md +246 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambit_map/daemon.py +25 -6
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambit_map/export.py +20 -3
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambit_map/judged.py +86 -3
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambit_map/kitdemo.py +4 -4
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambit_map/library.py +88 -11
- ambitgraph-0.2.0/ambit_map/mapllm.py +594 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambit_map/objects.py +3 -3
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambit_map/orchestrate.py +123 -31
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambit_map/registry.py +162 -27
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambit_map/serve.py +34 -4
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambit_map/unitpage.py +44 -13
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/__init__.py +1 -1
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/cli.py +145 -11
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/edges.py +16 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/pipeline.py +73 -3
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/portal.py +49 -119
- ambitgraph-0.2.0/ambitgraph/reports.py +88 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/shell.py +524 -145
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/symbols.py +188 -9
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/tables.py +30 -6
- ambitgraph-0.2.0/ambitgraph/urcode.py +78 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0/ambitgraph.egg-info}/PKG-INFO +1 -1
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph.egg-info/SOURCES.txt +4 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/pyproject.toml +7 -4
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/LICENSE +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/MANIFEST.in +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/README.md +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambit_map/__init__.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambit_map/__main__.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambit_map/ambit-map-enrich.md +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambit_map/cli.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambit_map/dialects/__init__.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambit_map/dialects/contract.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambit_map/dialects/express_pack.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambit_map/dialects/fastapi_pack.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambit_map/dialects/flask_pack.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambit_map/dialects/nextjs_pack.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambit_map/dialects/supabase_pack.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambit_map/families.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambit_map/graph.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambit_map/island_layout.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambit_map/islands.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambit_map/load.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambit_map/matched.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambit_map/model.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambit_map/page.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambit_map/paths.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambit_map/theme.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/WALKTHROUGH.md +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/__main__.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/cockpit.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/codelib.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/config.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/console.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/demo.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/docs_discovery.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/doctor.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/duplication.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/emit.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/engine_identity.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/estimate.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/eval.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/feeds.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/gate.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/gate_check.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/gate_server.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/harvest.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/inventory.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/jscalls.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/mds.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/merge.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/merge_jsp.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/parameterize.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/pycalls.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/ratify.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/report.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/sample.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/secretary.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/stages.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph/tasks.py +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph.egg-info/dependency_links.txt +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph.egg-info/entry_points.txt +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph.egg-info/requires.txt +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/ambitgraph.egg-info/top_level.txt +0 -0
- {ambitgraph-0.1.0 → ambitgraph-0.2.0}/setup.cfg +0 -0
|
@@ -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.
|
|
@@ -295,7 +295,7 @@ def _http_post_stop(port, token, timeout_s, opener=None):
|
|
|
295
295
|
# ---------------------------------------------------------------------------
|
|
296
296
|
|
|
297
297
|
|
|
298
|
-
def child_argv(sys_executable, args, port, idle_minutes):
|
|
298
|
+
def child_argv(sys_executable, args, port, idle_minutes, cmd_name="map"):
|
|
299
299
|
"""The exact argv/env of the mechanism's step 2: the child is `python
|
|
300
300
|
-P -m ambitgraph map <repo> --foreground --no-open` (the "-P" and the
|
|
301
301
|
env's PYTHONPATH rule are orch.python_child()'s -- see there for what
|
|
@@ -311,8 +311,18 @@ def child_argv(sys_executable, args, port, idle_minutes):
|
|
|
311
311
|
truthy value for them -- read with getattr and a default, the K5
|
|
312
312
|
seam's own rule, so `args` may be a bare six-attribute Namespace built
|
|
313
313
|
by a caller that has never heard of this wave's new flags. Returns
|
|
314
|
-
(argv, env), not argv alone -- see orch.python_child().
|
|
315
|
-
|
|
314
|
+
(argv, env), not argv alone -- see orch.python_child().
|
|
315
|
+
|
|
316
|
+
`cmd_name`, new for the mapllm wave (THE SERVE FLAVOR, PLAN.md section
|
|
317
|
+
2 C1): the child's own subcommand word -- "map" (the default, `ambit
|
|
318
|
+
map`'s own posture, byte-unchanged) or "mapllm". Every existing caller
|
|
319
|
+
of this function never passes it, so every existing argv is
|
|
320
|
+
unaffected; ambit_map.mapllm.cmd_mapllm's own detached-launch path is
|
|
321
|
+
the one caller that passes cmd_name="mapllm", so a mapllm server's
|
|
322
|
+
detached child re-enters the SAME conductor loop that spawned it,
|
|
323
|
+
never a plain `map` child that would silently drop the auto-harvest
|
|
324
|
+
mechanism."""
|
|
325
|
+
tail = [cmd_name, str(args.repo), "--foreground", "--no-open"]
|
|
316
326
|
if port is not None:
|
|
317
327
|
tail += ["--port", str(port)]
|
|
318
328
|
if idle_minutes:
|
|
@@ -865,7 +875,7 @@ def _status_impl(out_dir, repo, reader, prober, pid_checker):
|
|
|
865
875
|
def launch(args, out_dir, repo, idle_minutes, budget_s=None,
|
|
866
876
|
reader=None, prober=None, pid_checker=None, spawner=None,
|
|
867
877
|
child_alive=None, clock=None, platform=None, sys_executable=None,
|
|
868
|
-
poll_interval_s=0.2, cmd=None):
|
|
878
|
+
poll_interval_s=0.2, cmd=None, cmd_name="map"):
|
|
869
879
|
"""The mechanism's steps 1-4, a pure orchestration over injectable
|
|
870
880
|
side effects. `out_dir`/`repo` are ALREADY RESOLVED (the same values
|
|
871
881
|
orch.plan() would compute from args.repo/args.out) -- this function
|
|
@@ -886,6 +896,14 @@ def launch(args, out_dir, repo, idle_minutes, budget_s=None,
|
|
|
886
896
|
this parameter (an existing test, an older caller) still gets a
|
|
887
897
|
real, typeable stop instruction rather than a KeyError.
|
|
888
898
|
|
|
899
|
+
`cmd_name`, new for the mapllm wave (THE SERVE FLAVOR, PLAN.md section
|
|
900
|
+
2 C1): threaded to child_argv() unchanged (see its own docstring), and
|
|
901
|
+
used to resolve `cmd` itself when the caller leaves it None -- "map"
|
|
902
|
+
(the default, byte-unchanged) or "mapllm". Every existing caller
|
|
903
|
+
either passes `cmd` explicitly (cmd_map does) or never heard of
|
|
904
|
+
`cmd_name` at all, so this default resolution changes nothing for
|
|
905
|
+
them.
|
|
906
|
+
|
|
889
907
|
Returns (exit_code, lines_to_print, url_to_open_or_None). cmd_map is a
|
|
890
908
|
thin caller: print each line, then orch.open_browser(url, not
|
|
891
909
|
args.no_open) when url is not None."""
|
|
@@ -933,7 +951,8 @@ def launch(args, out_dir, repo, idle_minutes, budget_s=None,
|
|
|
933
951
|
# DAEMON_PORT_TAKEN comparison below can tell "not given" apart from
|
|
934
952
|
# "given, and it was DEFAULT_PORT".
|
|
935
953
|
requested_port = getattr(args, "port", None)
|
|
936
|
-
argv, env = child_argv(sys_executable, args, requested_port, idle_minutes
|
|
954
|
+
argv, env = child_argv(sys_executable, args, requested_port, idle_minutes,
|
|
955
|
+
cmd_name=cmd_name)
|
|
937
956
|
log_path = log_path_for(out_dir)
|
|
938
957
|
|
|
939
958
|
spawned = {}
|
|
@@ -1005,6 +1024,6 @@ def launch(args, out_dir, repo, idle_minutes, budget_s=None,
|
|
|
1005
1024
|
if health.get("damaged"):
|
|
1006
1025
|
lines.append(reg.DAEMON_DAMAGED.format(reason=health["damaged"]))
|
|
1007
1026
|
if cmd is None:
|
|
1008
|
-
cmd = invocation_form() + "
|
|
1027
|
+
cmd = invocation_form() + " " + cmd_name
|
|
1009
1028
|
lines.append(reg.DAEMON_OPEN.format(url=url, repo=args.repo, cmd=cmd))
|
|
1010
1029
|
return 0, lines, url
|
|
@@ -33,6 +33,10 @@ MANIFEST_NAME = "ambitgraph-map.export.json" # U4 review fix (blocker): the
|
|
|
33
33
|
# fresh off disk at export time (never embedded as a literal), so a wheel
|
|
34
34
|
# install and a repo checkout both ship the SAME bytes setuptools packaged.
|
|
35
35
|
ENRICH_DOC_NAME = "ambit-map-enrich.md"
|
|
36
|
+
# C3 (mapllm wave): the ingestion contract's published schema doc, copied
|
|
37
|
+
# beside every export the SAME way as ENRICH_DOC_NAME just above -- the
|
|
38
|
+
# generalisation of ambit-map-enrich.md's own producer-portability clause.
|
|
39
|
+
CONTRACT_DOC_NAME = "ambit-mapllm-contract.md"
|
|
36
40
|
|
|
37
41
|
Row = collections.namedtuple("Row", "rel route_path raw_query depth")
|
|
38
42
|
|
|
@@ -264,7 +268,12 @@ def write_export(state, out_dir, route, render, now_iso):
|
|
|
264
268
|
|
|
265
269
|
rows, graph_slugs = page_rows(model, dialects)
|
|
266
270
|
total_nodes = len(getattr(model, "units", {})) + len(getattr(model, "tables", {}))
|
|
267
|
-
|
|
271
|
+
# `rows` names every .html page this export writes (the manifest's
|
|
272
|
+
# own "files" list, minus the two .md docs appended after it) --
|
|
273
|
+
# map.html is a real row IN `rows` (page_rows()'s own docstring
|
|
274
|
+
# above, and the manifest comment below, both say so), so the
|
|
275
|
+
# footer's own page count is exactly len(rows), no adjustment.
|
|
276
|
+
foot = _footer_html(len(rows), len(graph_slugs), total_nodes)
|
|
268
277
|
|
|
269
278
|
# Stale-file rule (§2.1), scoped by manifest (U4 review fix, blocker):
|
|
270
279
|
# never shutil.rmtree, never a glob wider than the paths this export
|
|
@@ -295,10 +304,18 @@ def write_export(state, out_dir, route, render, now_iso):
|
|
|
295
304
|
(out_dir / ENRICH_DOC_NAME).write_text(
|
|
296
305
|
enrich_src.read_text(encoding="utf-8"), encoding="utf-8", newline="\n")
|
|
297
306
|
|
|
307
|
+
# C3 -- the SAME copy-beside step for the ingestion contract doc.
|
|
308
|
+
contract_src = Path(__file__).with_name(CONTRACT_DOC_NAME)
|
|
309
|
+
if contract_src.exists():
|
|
310
|
+
(out_dir / CONTRACT_DOC_NAME).write_text(
|
|
311
|
+
contract_src.read_text(encoding="utf-8"), encoding="utf-8", newline="\n")
|
|
312
|
+
|
|
298
313
|
# The manifest names everything THIS export wrote -- map.html included --
|
|
299
314
|
# so the NEXT export's _prune_stale unlinks exactly these paths and
|
|
300
315
|
# nothing a user placed alongside them (U4 review fix, blocker).
|
|
301
316
|
# map.html is a ROW now, so it is already in this list -- appending it
|
|
302
|
-
# again would name it twice in the manifest.
|
|
303
|
-
|
|
317
|
+
# again would name it twice in the manifest. C3: the contract doc joins
|
|
318
|
+
# ENRICH_DOC_NAME here for the identical reason -- an un-manifested copy
|
|
319
|
+
# would be a stray file the NEXT export cannot recognise as its own.
|
|
320
|
+
_write_manifest(out_dir, [row.rel for row in rows] + [ENRICH_DOC_NAME, CONTRACT_DOC_NAME])
|
|
304
321
|
return index_text
|
|
@@ -44,6 +44,12 @@ class JudgedPlane:
|
|
|
44
44
|
unjoined: tuple = () # tuple[JudgedUnit], uid == "", sorted by anchor
|
|
45
45
|
families: tuple = () # tuple[str], sorted -- legend count ONLY in v1
|
|
46
46
|
rows_total: int = 0 # joined + unjoined rows kept
|
|
47
|
+
# the ingestion contract's producer record (a sibling engine module's
|
|
48
|
+
# draft writer stamps this): the free-text tool name, carried onto the
|
|
49
|
+
# plane ONLY when the draft actually named one -- "" otherwise, never
|
|
50
|
+
# guessed. A renderer may swap its generic wording for this when it is
|
|
51
|
+
# non-empty; this module renders nothing itself.
|
|
52
|
+
producer_tool: str = ""
|
|
47
53
|
|
|
48
54
|
|
|
49
55
|
@dataclass(frozen=True)
|
|
@@ -52,6 +58,32 @@ class DamagedJudged:
|
|
|
52
58
|
run_dir: str
|
|
53
59
|
|
|
54
60
|
|
|
61
|
+
# the ingestion contract's version discipline -- duplicated BY VALUE from
|
|
62
|
+
# the engine's own draft-writer module (the SAME number, never imported:
|
|
63
|
+
# this file speaks stdlib only). Bumped only for a BREAKING shape change.
|
|
64
|
+
DRAFT_SCHEMA_VERSION = 1
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class DraftVersionMismatch(ValueError):
|
|
68
|
+
"""Raised only to be caught immediately below; DamagedJudged.reason
|
|
69
|
+
carries just this class's OWN NAME (never a version number, never any
|
|
70
|
+
other byte of the draft). Fires when "v" is PRESENT and != 1 -- a
|
|
71
|
+
schema this reader does not speak. A draft with no "v" key at all is
|
|
72
|
+
NOT this: every draft written before the version key existed predates
|
|
73
|
+
it entirely, and loads exactly as it always has (the absence law)."""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class DraftProducerConfigMismatch(ValueError):
|
|
77
|
+
"""Raised only to be caught immediately below; DamagedJudged.reason
|
|
78
|
+
carries just this class's own name. Fires only when BOTH the draft's
|
|
79
|
+
own producer.config_hash and the caller's run_config_hash are non-
|
|
80
|
+
empty strings AND they differ: the facts in this draft were produced
|
|
81
|
+
against a different scan configuration than the one being served now.
|
|
82
|
+
The F6 absence law, verbatim (a sibling engine-side loader keeps the
|
|
83
|
+
identical guard for its own two artifacts): either side absent never
|
|
84
|
+
fires."""
|
|
85
|
+
|
|
86
|
+
|
|
55
87
|
@dataclass(frozen=True)
|
|
56
88
|
class PendingScript:
|
|
57
89
|
stage: str
|
|
@@ -69,13 +101,20 @@ def _s(row, key):
|
|
|
69
101
|
return v if isinstance(v, str) else ""
|
|
70
102
|
|
|
71
103
|
|
|
72
|
-
def load_judged(run_dir, known_uids=()):
|
|
104
|
+
def load_judged(run_dir, known_uids=(), run_config_hash=""):
|
|
73
105
|
"""-> JudgedPlane | None | DamagedJudged. Fail-closed -- never raises on
|
|
74
106
|
any file-system or content state; callers pass a real path.
|
|
75
107
|
|
|
76
108
|
None is the normal Layer-1 state (no draft yet, or run_dir is not a
|
|
77
109
|
directory at all). DamagedJudged names only the exception CLASS -- no
|
|
78
|
-
byte of the file's content is ever echoed.
|
|
110
|
+
byte of the file's content is ever echoed.
|
|
111
|
+
|
|
112
|
+
`run_config_hash` (the ingestion contract's cross-artifact guard): the
|
|
113
|
+
CURRENT run's own config hash, computed by the caller (this module
|
|
114
|
+
imports no engine code and touches no scan config itself). Compared
|
|
115
|
+
against the draft's own producer.config_hash ONLY when both sides are
|
|
116
|
+
non-empty strings -- the F6 absence law, verbatim: omitted or ""
|
|
117
|
+
never fires a mismatch."""
|
|
79
118
|
path = Path(run_dir).joinpath(*DRAFT_REL)
|
|
80
119
|
try:
|
|
81
120
|
text = path.read_text(encoding="utf-8")
|
|
@@ -94,6 +133,35 @@ def load_judged(run_dir, known_uids=()):
|
|
|
94
133
|
data = json.loads(text)
|
|
95
134
|
if not isinstance(data, dict):
|
|
96
135
|
raise ValueError("draft is not an object")
|
|
136
|
+
# the version gate, BACKWARD HONEST: absent "v" predates this
|
|
137
|
+
# key entirely and is never a mismatch; present and wrong is.
|
|
138
|
+
# PRESENT-BUT-NULL TRAP (adversarial finding): checking
|
|
139
|
+
# `v is not None` treats {"v": null} as absent -- but a
|
|
140
|
+
# DECLARED, unusable value is not the same fact as no value at
|
|
141
|
+
# all ("declared-but-unusable is not absent"), so membership
|
|
142
|
+
# ("v" in data) is the only honest absence test, never `is not`.
|
|
143
|
+
# TYPE TRAP (the other half of the same finding): JSON's
|
|
144
|
+
# {"v": true} and {"v": 1.0} both deserialize to Python values
|
|
145
|
+
# that compare EQUAL to 1 by value (bool is an int subclass;
|
|
146
|
+
# 1.0 == 1) -- a bare `!= DRAFT_SCHEMA_VERSION` would silently
|
|
147
|
+
# accept either as "version 1". Only a real, non-bool int
|
|
148
|
+
# equal to DRAFT_SCHEMA_VERSION passes; every other declared
|
|
149
|
+
# shape (null, bool, float, string, wrong int, ...) mismatches.
|
|
150
|
+
if "v" in data:
|
|
151
|
+
v = data["v"]
|
|
152
|
+
if isinstance(v, bool) or not isinstance(v, int) \
|
|
153
|
+
or v != DRAFT_SCHEMA_VERSION:
|
|
154
|
+
raise DraftVersionMismatch("v")
|
|
155
|
+
# the F6-style config_hash cross-artifact guard: only fires
|
|
156
|
+
# when the draft's own producer.config_hash AND the caller's
|
|
157
|
+
# run_config_hash are both non-empty strings that disagree.
|
|
158
|
+
producer = data.get("producer")
|
|
159
|
+
draft_config_hash = (producer.get("config_hash")
|
|
160
|
+
if isinstance(producer, dict) else None)
|
|
161
|
+
if (isinstance(draft_config_hash, str) and draft_config_hash
|
|
162
|
+
and isinstance(run_config_hash, str) and run_config_hash
|
|
163
|
+
and draft_config_hash != run_config_hash):
|
|
164
|
+
raise DraftProducerConfigMismatch("config_hash")
|
|
97
165
|
rows = data.get("units")
|
|
98
166
|
if not isinstance(rows, list):
|
|
99
167
|
raise ValueError("draft names no units list")
|
|
@@ -146,8 +214,23 @@ def load_judged(run_dir, known_uids=()):
|
|
|
146
214
|
names.add(n)
|
|
147
215
|
families = tuple(sorted(names))
|
|
148
216
|
|
|
217
|
+
# the producer's tool name, carried onto the plane ONLY when the
|
|
218
|
+
# draft actually recorded one -- never guessed. `producer` was
|
|
219
|
+
# already read (and type-checked) by the config_hash gate above.
|
|
220
|
+
# Stripped and re-checked for emptiness (the SAME whitespace rule
|
|
221
|
+
# the writer applies) so a hand-authored draft's " " reads back
|
|
222
|
+
# exactly as absent, never as a blank-looking "named" producer.
|
|
223
|
+
producer_tool = ""
|
|
224
|
+
if isinstance(producer, dict):
|
|
225
|
+
t = producer.get("tool")
|
|
226
|
+
if isinstance(t, str):
|
|
227
|
+
stripped = t.strip()
|
|
228
|
+
if stripped:
|
|
229
|
+
producer_tool = stripped
|
|
230
|
+
|
|
149
231
|
return JudgedPlane(run_dir=str(run_dir), by_uid=by_uid, unjoined=unjoined,
|
|
150
|
-
families=families, rows_total=rows_total
|
|
232
|
+
families=families, rows_total=rows_total,
|
|
233
|
+
producer_tool=producer_tool)
|
|
151
234
|
except Exception as exc:
|
|
152
235
|
return DamagedJudged(reason=type(exc).__name__, run_dir=str(run_dir))
|
|
153
236
|
|
|
@@ -207,9 +207,9 @@ def _table_chips_block():
|
|
|
207
207
|
|
|
208
208
|
def _symtable_block():
|
|
209
209
|
"""U2 (wave/u2-nodes) additive: demonstrates the three new emitters
|
|
210
|
-
(sub_head, sym_table, chip_link) the unit page's "Blocks — what
|
|
211
|
-
|
|
212
|
-
bytes move."""
|
|
210
|
+
(sub_head, sym_table, chip_link) the unit page's "Blocks — what the
|
|
211
|
+
scan read" needs. Appended after every U0/U1 block -- no existing
|
|
212
|
+
block's bytes move."""
|
|
213
213
|
demo = (
|
|
214
214
|
ob.sub_head("Includes — what this file pulls in")
|
|
215
215
|
+ ob.sym_table(
|
|
@@ -219,7 +219,7 @@ def _symtable_block():
|
|
|
219
219
|
+ ob.chip_link("/graph?u=app/main.py", "See it on the Graph →"))
|
|
220
220
|
body = demo + ob.code('objects.sub_head(text) / objects.sym_table(headers, rows) / '
|
|
221
221
|
'objects.chip_link(href, text)')
|
|
222
|
-
return ob.block("kd-symtable", "blocks — what
|
|
222
|
+
return ob.block("kd-symtable", "blocks — what the scan read", body, shown=True)
|
|
223
223
|
|
|
224
224
|
|
|
225
225
|
def _code_block():
|