-
crude-engine program · generated 2026-08-17
+
crude-engine program · generated 2026-08-31
Cycle 0: Honest catalogue
Next task: Count Execute methods from protocol YAML in the generated header
Run: python3 scripts/check_catalogue.py --e-count
diff --git a/local/agents/1.17-clerk/INSTRUCTIONS.md b/local/agents/1.17-clerk/INSTRUCTIONS.md
new file mode 100644
index 0000000..cfcf7e7
--- /dev/null
+++ b/local/agents/1.17-clerk/INSTRUCTIONS.md
@@ -0,0 +1,56 @@
+# 1.17 clerk
+
+Named for napalm-hios v1.17.0. The job is the known-good anchor, wherever
+it lives now (`local/reference/`, WebUI, captured fixtures) — not "compare
+to v1."
+
+## Hole
+
+Answer one question: does a known-good anchor exist for this method, and
+what does it say? Does not fix. Does not decide if a discrepancy is a bug.
+
+## Start
+
+"Is there an anchor for method X?" from any clerk.
+
+## End
+
+Exactly one of three answers:
+
+1. **Yes, captured** — path under `tests/fixtures/` and its `verified_via`.
+2. **Yes, not captured** — a human can check WebUI/CLI now; then test bot
+ captures it. Do not leave a verbal check.
+3. **No** — say so. That is the `NO_HOLE` / no-anchor exit, not a guess.
+
+## Tools (fixed code only)
+
+- `local/reference/CLI/CLI_REFERENCE.md`
+- `local/reference/CLI/cli_ref_hios_merged.json`
+- `local/reference/MIBs/`
+- `local/reference/MOPS/mops_hios.xml`
+- `local/reference/configs/`
+- Device WebUI (human, strongest anchor — shares no decode path with us)
+
+## Bounds
+
+Report what the anchor says. Bootstrap / smash order is
+[`../diagrams/conform.md`](../diagrams/conform.md): MOPS vs WebUI first
+(HITL floor), SNMP against that, SSH last. Never "three protocols agree."
+
+## Decision trail
+
+On the GitHub issue (short lines, no mermaid): flow step, what you
+ruled out, tool run, receipt. End with green / leftover / `NO_HOLE`.
+Glance value — wrong bounce feeds the chart; right bounce is obvious.
+
+## Never
+
+- Fix code or YAML.
+- Call agreement proof.
+- Invent a plausible value.
+
+## NO_HOLE
+
+Answer 3 is already the honest stop. Establishing a new WebUI-manual
+anchor is HITL (or the human sitting at the switch), then test bot
+captures.
diff --git a/local/agents/AGENTS-TODO.md b/local/agents/AGENTS-TODO.md
new file mode 100644
index 0000000..0553a6c
--- /dev/null
+++ b/local/agents/AGENTS-TODO.md
@@ -0,0 +1,9 @@
+# Not standing law
+
+This file was a 2026-09-04 session punch list. It is not the design, not
+the roster, and not something a Bot should load.
+
+Living process: [`README.md`](README.md).
+What changed and what to poke: [`local-agents-refactor.md`](local-agents-refactor.md).
+The 2026-09-04 audit snapshot: [`AUDIT-2026-09-04.md`](AUDIT-2026-09-04.md)
+(historical — also not instructions).
diff --git a/local/agents/AUDIT-2026-09-04.md b/local/agents/AUDIT-2026-09-04.md
new file mode 100644
index 0000000..98d8683
--- /dev/null
+++ b/local/agents/AUDIT-2026-09-04.md
@@ -0,0 +1,290 @@
+# Audit — local/agents/ vs. the real repo, the real GitHub tracker, and outside practice
+
+> **HISTORICAL. 2026-09-04 snapshot. Not standing law. Bots do not read this.**
+> Living process: `README.md` + clerk `INSTRUCTIONS.md` + `flow.md`.
+> Refactor intent: `local-agents-refactor.md`.
+>
+> Written by Claude, 2026-09-04, after Adam asked for a code audit + GitHub issue
+> review, both checked against what `local/agents/` currently claims, plus a look
+> at how other agentic-dev tools structure issue triage. Three independent passes
+> (code, GitHub, external research), synthesized here. Several claims below were
+> already stale relative to a sidecar checkout that may no longer exist on disk.
+>
+> Scope note: the code audit ran against a separate sidecar checkout of
+> `crude-engine` (the git/GitHub-tracked tree), not a personal vault working
+> copy. Dual-tree topology notes are local-only. None of the fixes below had
+> been applied at the time this snapshot was written.
+
+## 1. Stale claims — fix these before they mislead anyone
+
+Things `local/agents/` states as current fact that have already changed, most
+likely because real work happened in the five days between that session and
+this audit:
+
+| Doc | Claim | Reality now |
+|---|---|---|
+| `schema-clerk/INSTRUCTIONS.md` | "40/45 compliant, 6 real errors" | **46 schemas, 45 compliant, 0 errors, 1 warning.** All 6 original errors are already fixed. |
+| `schema-clerk/INSTRUCTIONS.md` | `audit_wire.py` listed as broken (same napalm-hios-v2 path bug as the other three) | **Already fixed.** Docstring: "Retargeted from leftover napalm-hios-v2... to this repo's crude_engine/wire." Runs clean, produces `docs/WIRE_INTEGRITY.md`. It's read-only (duplicate-name / protocol-coverage report) — was never a fixer to begin with. |
+| `schema-clerk/INSTRUCTIONS.md` | `batch_generate_MIB.py` listed as simply broken | It's **deliberately guarded**, not broken — refuses to run without `--isolated --outdir
` (`raise SystemExit(...)`), and is safe/usable today for diff-only comparison against live wire. Different category from the two genuinely dead scripts below. |
+| `test-bot/INSTRUCTIONS.md` | "check_principles.py found 3 live violations: ssh_transport.py:110, :131, snmp_transport.py:265" | **Fixed, now 0 violations.** Confirmed directly — lines 110/131 now bind `except Exception as e:` and log; `snmp_transport.py:265` same. Checker is a clean PASS. |
+| `test-bot/INSTRUCTIONS.md` | "test_replay.py: 280 passed, 61 skipped, 79 failed" | **Doesn't reproduce.** `tests/fixtures/` isn't git-tracked and doesn't exist in a fresh checkout — running it now gives 2 skipped, 0 collected. The 79-failures number was tied to a specific prior session's local fixture state, not a repo-committed fact. Don't carry it forward as "current state" — and note nowhere does test-bot's doc explain how fixtures get created in the first place. |
+| `docs-clerk/INSTRUCTIONS.md` | "`TODO_HITLIST.md` is Generated only, per METHOD.md's authority table" | **Wrong.** The real `docs/program/METHOD.md` table marks only `RELEASE_MATRIX.md`, `API_REFERENCE.md`, `status.html` as generated-only. `TODO_HITLIST.md` is archived/dead — moved to `local/archive/docs-legacy/TODO_HITLIST.md`, superseded by GitHub issues as the live source (`docs/RELEASE_GATE.md` states this explicitly). |
+| `AGENTS-TODO.md` | "File one issue for the 4 broken generator scripts" (open item) | **Already done and merged.** Closed issue #98 named all 11 scripts (not just 4), fixed via merged PR #101. Remove from the punch list — it's the one item that's actually fully complete. |
+| `AGENTS-TODO.md` | "#117 and #133-135 sit unconnected... despite naming the same file" | **Resolved, and the "same file" premise was already off.** All four closed 2026-09-01. They don't share a filename — #117 names `mops_driver.py`/`ssh_driver.py`/`interpreter.py`/`check_principles.py`; #133/134/135 each name a *different* `wire/ssh/*.yaml` overlay. The real link is PR #136, which explicitly named all four in its body (with "do not close #117 on this PR" / "do not close #133/134/135 on this alone" — negated, so not an auto-close keyword) — someone closed them together shortly after merge anyway. |
+
+## 2. A real bug in `DIAGNOSTIC_PROCESS.md` — every clerk citing it inherits it
+
+Step 2 of the diagnostic ladder says to trace with
+`device.method_name(args, debug=True)`. **That's wrong.** The engine's
+per-step pipeline-recording kwarg is `trace`, not `debug` — confirmed
+directly in `interpreter.py`: `tracing = kwargs.pop("trace", False) or
+schema_def.get("debug", False)`. `debug=True` is a *separate* mechanism —
+adapter-level transport logging, not pipeline recording (matches the
+architecture note elsewhere: "Trace/debug separation: `trace=True` (engine,
+pipeline recording), `debug=True` (adapter, transport logging)").
+`release_matrix.py --trace` confirms this by passing `trace=True` to the
+engine, not `debug=True`.
+
+Anyone who follows `DIAGNOSTIC_PROCESS.md` Step 2 literally gets transport
+logs, not the pipeline trace the doc promises them. `engine-clerk`'s
+INSTRUCTIONS.md cites this same step and inherits the same bug. **Fix
+`DIAGNOSTIC_PROCESS.md` directly and every clerk that cites it is fixed for
+free** — this is a one-line doc fix, not a design question, but I haven't
+touched it since I wasn't sure you want me editing the canonical
+(`crude-sidecar`) checkout directly given it's "no longer coded" there. Say
+the word and I'll do it, or route it however you'd rather.
+
+## 3. The wire-YAML question you asked directly — now has a concrete answer
+
+You asked: can wire YAML be hand-edited as a stopgap, proven, documented,
+*then* have the generator reconcile it properly later — and do the clerks
+actually know how?
+
+**Yes, and it's currently the *only* path — no automated generator
+regenerates `crude_engine/wire/*.yaml` from MIBs right now.**
+`docs/WIRE_SPEC.md` claims the live generator is
+`local/generator/batch_generate_webui.py`, but that file has been retired to
+`batch_generate_webui.py.stable`, whose own header reads "Leftover
+v26/monolith one-shot. Not live law. Do not run" — `WIRE_SPEC.md` is stale on
+this exact point.
+
+The real tools, correctly sorted (which none of the clerk docs currently do):
+
+| Script | Real status | Use |
+|---|---|---|
+| `batch_generate_MIB.py --isolated --outdir ` | Guarded, safe, working | Emits to a temp dir for **diff-only** comparison against live wire. Never writes `crude_engine/wire` directly. |
+| `audit_wire.py` | Fixed, working | Read-only coverage/duplicate-name audit → `docs/WIRE_INTEGRITY.md`. |
+| `heal_schemas.py`, `validate_schema_wire.py` | Genuinely dead | Hardcoded pre-rename paths, unguarded, not safe to run. |
+
+`docs/DIAGNOSTIC_PROCESS.md` Step 6 **already states the correct discipline**
+for this exact scenario: hand-fix the wire YAML, then file a GitHub issue for
+the generator leftover ("prove-then-file"), tagged `#generator`. Its own
+tag-cycle table routes that work to **docs clerk** specifically — isolated
+emit-diff only, architect merges on emit-diff + offline CI, sidecar never
+touches it. None of the roster docs currently say this. `schema-clerk`
+treats all four generator scripts as one undifferentiated "broken" bucket;
+the real breakdown is two dead, one guarded-safe, one already-fixed. This is
+the single clearest example of the "limited but complete view" gap you're
+asking about — the discipline exists in the codebase's own docs, it just
+hasn't been pulled into the clerk that needs it.
+
+## 4. Missing standards references — the actual "complete view" gap
+
+`local/generator/README.md` lists five docs as **authored** (hand-written,
+not generated) — meaning they're exactly the standards a clerk should
+validate its own work against, and exactly what drifts silently if nobody's
+watching. None are currently cited by any clerk:
+
+| Doc | What it actually specifies | Should be cited by |
+|---|---|---|
+| `docs/SCHEMA_MODEL.md` | Formal schema-YAML structural spec — "every schema MUST comply with..." | schema-clerk |
+| `docs/SCHEMA_PRIMITIVES.md` | YAML key → pipeline stage → handler reference | schema-clerk, engine-clerk |
+| `docs/WIRE_SPEC.md` | Wire-YAML spec + data lineage (UI capture → MIB resolution → schema enrichment → wire generation) | schema-clerk (wire side), 1.17-clerk |
+| `docs/ENGINE_PRINCIPLES.md` | What belongs in `interpreter.py`/`crude.py` and why — the doc `check_principles.py` actually enforces | engine-clerk (this is the answer to Step 7's "why is this generic" requirement) |
+| `docs/RFC_MAPPING.md` | SYNTAX → transform function mapping | schema-clerk, engine-clerk |
+
+Also uncited anywhere: the root `AGENTS.md` (the canonical repo's own stated
+"single root law" — you already told me this and `local/agents/` are
+deliberately separate documents, which is fine, but architect's triage
+judgment call currently duplicates `AGENTS.md`'s own dispatch table — "Method
+missing in schema → Schema; PDU/encoding wrong → Wire; Need new primitive →
+Prime" — without acknowledging it exists), and the live task-tracking files
+`docs/program/cycles.yaml`, `docs/program/roadmap.yaml`,
+`docs/program/SEED.md`.
+
+## 5. Per-clerk summary
+
+- **Architect** — accurate on what it does claim; missing the cross-reference
+ to `AGENTS.md`'s dispatch table (§4) and `DIAGNOSTIC_PROCESS.md`'s
+ fault-detection table, both of which are machine-checkable aids for exactly
+ the triage judgment call architect owns.
+- **Schema clerk** — see §1 and §3 above; also missing `SCHEMA_MODEL.md`/
+ `SCHEMA_PRIMITIVES.md`/`WIRE_SPEC.md`/`RFC_MAPPING.md` citations (§4).
+- **Engine clerk** — accurate on `audit_getters.py` and `--no-validate`
+ syntax. Missing `ENGINE_PRINCIPLES.md` citation; inherits the
+ `debug=True`/`trace=True` bug from `DIAGNOSTIC_PROCESS.md` (§2). The
+ `_tag_name`/`AGGREGATE_TAGS` hard-gate precedent is asserted as history, not
+ re-derived this pass — fine as narrative, just noting it wasn't
+ re-verified.
+- **Docs clerk** — generator scripts confirmed current as claimed
+ (`generate_docs.py`, `generate_method_ref.py`, `generate_protocols.py` all
+ genuinely path-safe). Missing ownership of the five authored docs (§4) and
+ the wire-reconciliation workflow that `DIAGNOSTIC_PROCESS.md` already
+ assigns here (§3) — this is docs-clerk's biggest actual scope gap, not a
+ documentation nit.
+- **Test bot** — `release_matrix.py --inspect`/`--trace`/`--no-validate` and
+ `audit_getters.py` flags all confirmed exactly as documented. Missing:
+ `tests/test_inspect_result.py`, `test_inspect_reaches_driver.py`,
+ `test_ssh_dns_key_column.py` — the three tests that actually determine
+ `ci_offline.sh`'s exit code today (schema-validate/principles/catalogue are
+ scored but don't gate it until `REQUIRE_RELEASE_PROOFS=1`) — and no mention
+ of how `tests/fixtures/` gets populated before `test_replay.py` can produce
+ signal.
+- **1.17 clerk** — most accurate of the six, nothing wrong found. Could cite
+ `WIRE_SPEC.md`'s data-lineage section (§5 there) as a formal anchor-sourcing
+ reference.
+
+## 6. GitHub issue tracker — ground truth (pulled live via REST API, 92 issues + 49 PRs)
+
+- **The tracker is 5 days old** (2026-08-28 → 09-01), not a mature backlog —
+ there's no real staleness problem yet, and the "weekly pass" cadence
+ hasn't actually been exercised once. What's there is a bootstrap burst
+ followed by 3 days of silence; don't design a cadence around that pattern
+ as if it were steady-state.
+- **The documented `#bucket #short-id` tag scheme (`CLAUDE.md`/
+ `RELEASE_GATE.md`) is used zero times on GitHub.** Three informal,
+ overlapping conventions substitute for it instead: GitHub Labels
+ (`engine`/`schema`/`wire`/`driver`/`test`/`release` all exist and are
+ applied, plus extra ad-hoc ones: `mops`,`snmp`,`ssh`,`offline`,
+ `cycle-0`), a title colon-prefix (`engine: ...`) on 24/92 issues, and a
+ `get_x.read: ` method-prefix convention on the 21 open
+ parity-failure issues. Pick one on paper and it still won't be what's
+ actually happening on GitHub.
+- **23 labels exist, 6 never used** — `duplicate`, `wontfix`, `invalid`,
+ `question`, `help wanted`, `good first issue`. No triage-status label
+ exists at all (nothing like `needs-repro`/`blocked`/`provable`).
+- **PR→issue linkage is genuinely healthy**: 19/27 sampled merged PRs
+ (70%) carry a real `Closes #N`/`Fixes #N`. This is the one part of current
+ practice that's already working and worth keeping, not replacing.
+- **SSH timeout cluster is real, and bigger than `AGENTS-TODO.md` said.**
+ Root ticket #92 explicitly enumerates it: **#40, #42, #47, #48, #56, #59,
+ #61, #62, #74, #92 — 10 issues, all open, all labeled `ssh`.** The doc's
+ subset (#74, #48, #42, #92) is accurate as far as it goes, just incomplete.
+ Linkage is via prose `#N` mentions, not labels or a tracking issue.
+- **Confirmed still-missing GitHub issues** (real findings from the
+ 2026-09-04 session, never filed): `get_mac_address_table` crash under
+ `napalm_compat=True`; `docs/WIRING_GUIDE.md` missing entirely; bare
+ `except Exception:` at the specific file:line level in `ssh_transport.py`/
+ `snmp_transport.py` (broader swallow-pattern checker blindness is tracked
+ via closed #117/open #115, but not these two files by name).
+- **New duplicate/thin-link pairs found, not previously flagged**: #115/#116
+ (checker-blind-to-HiOS-vocab pair, linked one-directionally — #116 → #115,
+ not the reverse); #46/#55 (`get_interface_statistics`/`get_optics` share a
+ 36-key symptom, linked only via a comment, not the issue body/title);
+ #129 (repeatedly self-hedges against folding into closed #69 — a maintainer
+ anticipating a duplicate-shaped review, worth a real link instead of prose
+ disclaimers).
+
+## 7. Proposed issue lifecycle
+
+Adam's sketch (listed → validated → provable? → stuck/HITL) matches real
+prior art closely. What follows adapts that sketch using patterns confirmed
+in outside practice, fitted to this project's specific constraints: one
+full-access agent (Claude) + one constrained sidecar (Grok: fixed
+test-inspection command + git-sync only, no free-form shell) + a hard,
+non-negotiable human sign-off on any engine-layer change.
+
+**States as GitHub labels, one mutually-exclusive `triage:*` label per
+issue** (a second axis from the existing `engine`/`schema`/`wire`/etc.
+domain labels, which stay as-is — state and domain are independent, don't
+conflate them into one flat label set, that's part of why the current tag
+scheme never got adopted):
+
+```
+triage:needs-repro → an agent (Metabase Repro-Bot pattern) attempts
+ reproduction only — no fix, no write access, max 3
+ tries. Produces a receipt (failing test/trace/diff),
+ not a prose claim. On repo's own public issue text —
+ this stage should require a human-applied trigger
+ (comment or label), not fire on every new issue
+ automatically. Untrusted input, same reasoning Repro-
+ Bot uses for its own trigger-label gate.
+
+triage:validated → repro confirmed. Moves to provability check.
+
+triage:provable ─┐ Mechanical test, not a confidence score
+triage:needs-judgment ┤ (Proof-or-Stop framing: "a natural-language report
+ │ from an agent is not evidence" — only a
+ │ deterministic, re-runnable command's pass/fail
+ │ counts):
+ │ - does an existing gate/validator/known-good
+ │ anchor exist that can pass/fail this fix,
+ │ re-runnable by the sidecar's fixed
+ │ test-inspection command?
+ │ - does the fix touch ONLY non-engine files
+ │ (schema/wire YAML, docs, driver registry)?
+ │ BOTH yes → triage:provable
+ │ EITHER no → triage:needs-judgment. Engine-layer
+ │ touch is automatic needs-judgment regardless of
+ │ confidence — hard filter, never bypassable by the
+ │ pipeline, matching your existing engine-clerk gate
+ │ and mirroring GitHub Copilot's own hard four-eyes
+ │ rule on its coding agent.
+
+triage:auto-staged → provable fix drafted, evidence attached (test
+ receipt + known-good-anchor diff — the evidence IS
+ the artifact a human reviews, not something they
+ re-derive). Auto-merge only if ALSO low-blast-radius
+ (docs/schema-only, CI green) — gh-aw's risk-tiered
+ auto-merge pattern. Otherwise → fast-track sign-off
+ (human confirms the receipt, doesn't re-derive it).
+
+triage:stuck-hitl → needs-judgment, OR provable-but-evidence-gate failed
+ 3x (bounded retry, then escalate — Astro
+ triagebot-action's pattern). Routes to you directly.
+```
+
+Two mechanics worth stealing regardless of the label design:
+
+- **1-hour cooldown**: skip any issue/PR touched by a human in the last
+ hour — `gh-aw`'s cheap, concrete defense against an agent stepping on work
+ you're actively doing. Directly relevant given you and Claude work
+ interactively in the same repo agents would be triaging.
+- **`agent-working` claim label**: applied on pickup, released on any
+ terminal state. Prevents two runs (or Claude + Grok) working the same
+ issue concurrently — the standard mitigation for the duplicate-PR failure
+ mode that shows up repeatedly in real-world agent-PR audits.
+
+Sidecar (Grok)'s actual role in this lifecycle, given its real constraints:
+`triage:needs-repro` and the provability check only — read + the one fixed
+test-inspection command + git-sync, never a write/merge action. This mirrors
+`gh-aw`'s own security-motivated split between a no-write analysis job (reads
+untrusted issue text) and a separate, more trusted application job — which
+maps almost exactly onto your existing Claude/Grok split without needing to
+invent anything new.
+
+## 8. Suggested next moves, roughly in order
+
+1. Fix the `debug=True`/`trace=True` bug in `DIAGNOSTIC_PROCESS.md` (§2) —
+ cheap, unambiguous, currently misleading every clerk that reads it. Needs
+ your go-ahead on which checkout to edit.
+2. Correct the six stale claims in §1 across `schema-clerk/INSTRUCTIONS.md`,
+ `test-bot/INSTRUCTIONS.md`, `docs-clerk/INSTRUCTIONS.md`, and
+ `AGENTS-TODO.md`'s punch list.
+3. Add the five missing spec-doc citations (§4) to the relevant clerks —
+ this is the direct fix for "limited but complete view."
+4. Write the wire-YAML hand-edit-then-reconcile workflow (§3) into
+ `docs-clerk/INSTRUCTIONS.md` explicitly — it's currently correct in
+ `DIAGNOSTIC_PROCESS.md` but not surfaced anywhere a clerk would find it
+ without already knowing to look.
+5. File the confirmed-missing GitHub issues (§6): `get_mac_address_table`
+ crash, `WIRING_GUIDE.md` missing, the two bare-except sites by file:line.
+6. Decide on the label taxonomy in §7 and whether to pilot it — the tracker
+ being only 5 days old means there's no backlog to migrate, this is a
+ clean-slate adoption, not a retrofit.
+7. Link #115→#116, #46↔#55, and give #129 a real link instead of prose
+ disclaimers, while the tracker's still small enough that this is a
+ 5-minute pass rather than an archaeology project.
+
+None of the above has been applied — this file is the audit, not the fix.
+Say which of these you want done and I'll do them.
diff --git a/local/agents/README.md b/local/agents/README.md
new file mode 100644
index 0000000..a9193d8
--- /dev/null
+++ b/local/agents/README.md
@@ -0,0 +1,201 @@
+# Agents — sieve, tools, bounded gap
+
+Flowcharts in this folder are the process. They are how a human sees the
+holes, and how a Bot gets its personality (derived, fixed format, not a
+novel). Anything that fits an existing hole runs to done with no human.
+Anything that does not fit is HITL — then the chart gets a new hole, or a
+new fixed-code tool, so the next one falls through.
+
+This folder is the 2.0 of `docs/program/METHOD.md` for bot labour. Destination
+still lives in `docs/program/SEED.md`. Do not treat `SEED.md`'s "loose
+instructions" as a license to narrate. Speech is a receipt or `NO_HOLE`.
+
+## The sieve
+
+Alphabet-shaped work, holes for letters we already know. `a` `b` `c` fall
+through and the team finishes them. A `d` does not squeeze through `c`.
+HITL looks at the object, adds a hole or a tool, and the next `d` is an `a`.
+
+Do not pre-drill holes for letters you have not seen. A fake yes/no is the
+same mistake as skipping the check.
+
+Glance: [`diagrams/base-flow.md`](diagrams/base-flow.md) (does it fit a
+hole?) and [`diagrams/conform.md`](diagrams/conform.md) (conform smash:
+method × transport × CRUDE, MOPS floor then SNMP then SSH). Clerk-specific
+trees sit next to that clerk as `flow.md`. Update the chart when a real
+object does not fit. Then regenerate the Bot instructions from the chart —
+do not patch prose and leave the mermaid lying.
+
+## Two HITL gates
+
+**Fits a hole (schema/wire/docs, no human):** something does not work against a contract that
+already exists.
+
+```
+issue → reproduce with a named tool → propose a small change inside bounds →
+run the same tool → implement when green
+```
+
+Online devices matter. Offline CI is a cheap pre-filter, not a substitute
+for live-device proof. Grok Bot reaches live devices only through the sidecar on the VPS
+(WireGuard, firewalled). If the sidecar cannot run the proof, that is
+`NO_HOLE` (mesh missing), not a prompt to guess.
+
+**HITL (`NO_HOLE` / `LOGIC`):** the object needs logic that is not already
+in the system — a new primitive, a new invariant, a behaviour change, or a
+new test tool so this shape can be proven. That is the work the human wants:
+better testing logic, and decisions that change how things work.
+
+New meaning is one gate. Engine files are a second, separate, absolute one:
+`interpreter.py`, `crude.py`, `drivers/base.py` never merge without a human
+looking, full stop — a proven bugfix against existing declared behaviour
+still needs a HITL glance before it ships. No exception, no matter how
+small the diff or how clean the sweep. This is Adam's rule, not something
+a Bot (Grok included) reasons its way past. Kept cheap on purpose: the
+sign-off is a receipt (sweep result + one line naming why this is existing
+meaning, not new), not a case-file essay.
+
+Every `NO_HOLE` also names the missing hole or missing tool. Otherwise the
+same `d` recurs forever.
+
+## A Bot, fixed format
+
+Personality comes from its `flow.md` (or the base flow). Instructions are
+only: hole, start, end, tools, bounds, never, `NO_HOLE`.
+
+- **Start:** this issue, this exact proof command, currently red (or not
+ yet run).
+- **End:** the same command, green, against the tree that contains the
+ change. Live proofs go through the sidecar and must say which tree ran.
+- **Tools:** the listed fixed-code commands only. No throwaway
+ `device.get_*()` scripts. If the harness cannot do it, add a flag later
+ (HITL / tool work) — do not improvise.
+- **Bounds:** the only AI gap. What YAML/code to try between red and green,
+ inside the hole. Not whether it is proven. Not what the architecture
+ should become.
+- **Never:** essays, new primitives, guessing device behaviour from a
+ code-read, claiming a fix without a receipt from the named tool.
+
+## Architect orchestration
+
+Logic checks for how Architect multiplexes clerks live in
+`architect/INSTRUCTIONS.md` § Orchestration checks (e.g. schema
+hole ⇒ schema→wire ask ⇒ 1.17 differential). HITL-named checks
+get written there — not only in chat memory.
+
+
+### Effort objects (tangible goals)
+
+HITL ideas that span sessions become **in-repo Effort boards** under
+`local/agents/` (primary: [`diagrams/effort-board.md`](diagrams/effort-board.md)),
+not chat memory. Child work is GitHub issues. Architect updates the board when
+bubbles split/green/block (orchestration check 6). Architect-machine-only MD
+is cache only.
+
+## Roster
+
+Six holes. Do not add a seventh until a real object collides two of these.
+
+| Clerk | Hole | Flow | Ships when |
+|---|---|---|---|
+| **Architect** | Route. Does not fix. | [`diagrams/base-flow.md`](diagrams/base-flow.md) | N/A |
+| **Schema clerk** | `schemas/*.yaml` and, for now, `wire/**` | base flow + diagnostic ladder steps 1–6 | named proof green, no new primitive |
+| **Engine clerk** | `engine/interpreter.py`, `engine/crude.py`, `drivers/base.py` | [`engine-clerk/flow.md`](engine-clerk/flow.md) | named proof green, diff uses existing meaning, **and HITL sign-off — always, no exception** |
+| **Docs clerk** | regen generated docs; catch stale hand-written claims | (none yet — add only after a real branch) | docs-only, generators current |
+| **Test bot** | Run the named proof. Never authors. | [`test-bot/flow.md`](test-bot/flow.md) | receipt (pass/fail/blocked), not a merge |
+| **1.17 clerk** | Does a known-good anchor exist, and what does it say? | (three answers only) | an anchor or a plain no |
+
+Wire stays under schema clerk until real PRs show the two roles colliding.
+
+## Fixture confidence (when capturing)
+
+Not all "known good" is equal. Tag `tests/fixtures/` honestly:
+
+- `verified_via: webUI-manual` — human read the device WebUI. Highest.
+- `verified_via: cli-manual` — human ran the CLI by hand.
+- `verified_via: cross-protocol-agreement` — two protocols agreed. The
+ collapse bug had MOPS and SNMP agreeing while both were wrong. Not proof.
+- `verified_via: unverified` — snapshot only. Not an anchor.
+
+Conform smash is [`diagrams/conform.md`](diagrams/conform.md): HITL/1.17
+confirms MOPS, SNMP is fitted to that value (often generated-wire type or
+lookup), then SSH toward the same two known goods. Nested
+`method × transport × CRUDE op`. Reads before writes. Classify an SSH miss
+as wire, driver, or engine/primitive — do not smash all three at once.
+
+## Diagram convention
+
+- Cross-cutting → `diagrams/.md`
+- One clerk's tree → `/flow.md` next to `INSTRUCTIONS.md`
+- Never paste mermaid into instructions. Personality is derived from the
+ chart; instructions stay the fixed format above.
+
+## Sidecar
+
+The VPS sidecar is how Grok Bot proves work on live devices. It is not a
+second Grok and not a substitute for the Bot. Widen sidecar *verbs* when a
+proof cannot be run (HITL / tool work). Until a verb exists, `NO_HOLE`.
+
+Privacy (personal cyber + physical security): do **not** mention a
+restricted site on GitHub or in anything a Bot posts externally. No model
+names, hostnames, pool labels, IPs, inventory size, or counts that imply
+how much hardware exists. Home-office may be named in the abstract. Public
+language is tested/untested and pass/fail. Architect enforces at ingress.
+Device identity stays in gitignored `device_pool` / local sidecar notes.
+
+
+
+## Decision trail (glance value)
+
+Auditable for improvement, not distrust. The GitHub issue is the log
+(on-disk only when there is no ticket). Do **not** paste mermaids onto the
+issue — the charts stay in this folder; the issue holds the decisions.
+
+Fixed shape, short lines:
+
+1. **Architect triage** — hole pick + named proof command
+ e.g. `hole: schema/wire · proof: release_matrix --inspect --method X`
+2. **Clerk steps** — which flow step, what was ruled out, tool run, receipt
+ e.g. `step: ladder-4 · ruled-out: overlay miss · tool: validate_schemas --errors · receipt: …`
+3. **End** — green on that proof, leftover filed, or `NO_HOLE` naming the
+ missing hole/tool
+
+Open the issue → see the logic. Wrong bounce → feedback into the chart.
+Right bounce → obvious. Speech on the issue is a receipt or `NO_HOLE`, not
+an essay.
+
+**Multi-hop is normal.** One issue may bounce Schema → 1.17 → Engine (or
+run two asks in parallel) so clerks scale. One clerk could walk the whole
+path alone; splitting is throughput. Architect multiplexes each ask with a
+named proof. Each hop adds decision-trail lines. Clerk helpers/subagents
+are fine when their work collapses into those same glance lines — no
+silent side channel.
+
+## Living law (how this stays true)
+
+`local/agents/` on `main` is process law for bot labour. Update it in-tree;
+do not let bot profile prose drift ahead of this folder.
+
+1. **Change the chart first** (`diagrams/*.md` or a clerk `flow.md`) when a
+ real object does not fit. Then shorten that clerk's `INSTRUCTIONS.md` to
+ match. Never patch instructions and leave the mermaid lying.
+2. **Ship via PR** into `AdamRickards/crude-engine`. Architect owns GitHub
+ ingress and redaction. Historical snapshots (`AGENTS-TODO.md`,
+ `AUDIT-*.md`) are not law.
+3. **Bot profiles follow the roster.** Each live clerk description is a
+ short leash derived from its `INSTRUCTIONS.md` (hole, tools, never) plus
+ a pointer to this folder. Architect does not invent a seventh clerk.
+4. **Weekday sync.** Architect diffs live bot profiles against the roster
+ here and reports differentials in the Architect chat: missing clerk,
+ extra bot, description drift, or chart/instructions mismatch. Fix by
+ updating the profile or the pack — keep one truth.
+5. **Decision trail on the issue** (see above). Profiles and INSTRUCTIONS
+ require those glance lines; weekday sync flags essays or silent work.
+6. **Claude on the VPS** stays local-only (no push). Architect turns
+ detailed sidecar receipts into GitHub issues without device identity.
+
+## Not standing law
+
+`AGENTS-TODO.md` and `AUDIT-2026-09-04.md` are historical snapshots. Bots
+do not read them. The living spec of this refactor is
+[`local-agents-refactor.md`](local-agents-refactor.md).
diff --git a/local/agents/architect/INSTRUCTIONS.md b/local/agents/architect/INSTRUCTIONS.md
new file mode 100644
index 0000000..c314665
--- /dev/null
+++ b/local/agents/architect/INSTRUCTIONS.md
@@ -0,0 +1,138 @@
+# Architect
+
+Personality from: [`../diagrams/base-flow.md`](../diagrams/base-flow.md)
+
+## Hole
+
+Triage and GitHub ingress. Does not author fixes. Merges only after a named-proof receipt. Decides whether the object fits
+a known hole or is `NO_HOLE`.
+
+## Start
+
+A signal: new issue, sweep finding, or HITL input.
+
+## End
+
+Assigned to one owning clerk with a **named proof command**, or `NO_HOLE`
+handed to HITL with the missing hole or missing tool named.
+
+**Decision trail** (on the GitHub issue, short lines): hole pick + named
+proof. Multi-hop / multi-clerk on one issue is normal — each hop is a
+new trail entry. No mermaid paste. Glance value for HITL feedback into
+the chart.
+
+## Tools (fixed code only)
+
+- GitHub issues: file, label, link. Do not treat Issues as the brain;
+ `cycles.yaml` / this folder wins if they disagree.
+- Sidecar `POST /v1/run` when routing needs a live inspect before the hole
+ is obvious. Prefer `trace:true` (engine pipeline recording) when the miss
+ may be ours — see Orchestration check 2. Not `debug` (foreign library logs).
+- Grep / issue text only for "same file/function/symptom already open."
+ There is no cross-reference tool yet — that gap is `NO_HOLE` for tooling,
+ not a reason to invent a match.
+
+## Bounds
+
+Pick the hole. Write the start (proof command + never-touch). Post that
+as the first decision-trail lines on the issue. That is the whole job.
+
+## Orchestration checks (capture HITL logic here)
+
+When HITL names a multi-hop or pre-assign check, write it in this section
+(or branch `../diagrams/base-flow.md` if the sieve itself changed). Do not
+leave orchestration only in chat memory.
+
+Current checks:
+
+1. **Schema hole ⇒ schema→wire ask ⇒ 1.17 differential** (HITL 2026-09-10,
+ #39). Assigning Schema alone is not enough. On the issue, map
+ method defaults / sub_table field_map → each attr → `wire` + source +
+ which protocols have sources. Then 1.17 clerk diffs **that ask** against
+ what napalm-hios v1.17 actually requested (missing getter, collapsed
+ walk, different keys/columns). 1.17 does not fix. YAML bounce may run
+ with or after that differential; the ask inventory must be on the trail
+ either way.
+
+2. **Named proof includes engine `trace` when locating a pipeline miss**
+ (HITL 2026-09-10). `trace` records *our* steps (intent → wire bind →
+ transform → driver). When parity/empty/defaults look like crude missed
+ the contract, the named proof is inspect/`POST` with `trace:true` (or
+ `trace=True`), and the trail should say which step diverged — not a
+ guess from schema text alone. `debug` is foreign library logs only
+ (#155/#156); do not substitute it for pipeline recording.
+
+3. **Prefer existing schema/wire tools before new primitives** (HITL
+ 2026-09-10, #41/#160). If an existing declaration (e.g. SSH
+ `parser: regex`, value_map, overlay field) meets the contract without
+ a mess, Schema sits with that — no new primitive. Only when the need
+ cannot be met, or the existing tool becomes overly complex / dishonest,
+ hand HITL a `NO_HOLE` naming the missing declarative tool. Do not invent
+ engine parse ports to avoid using a working overlay tool.
+
+4. **Generator align iterates in TEMP** (HITL 2026-09-11, #162). Docs
+ clerk leftover-generator cycle: isolated emit → diff live
+ `crude_engine/wire` → named-TC teach on a **copy** → re-emit. Goal:
+ emit matches live (or the bulk). Archive TODO generator hints are
+ hints only — re-prove vs current live. If emit looks right and a live
+ hand-patch looks wrong, file/split a **wire** leftover (do not teach
+ the bug). Never overlay emit onto live wire until HITL regen.
+
+5. **Wire wrong input → schema looks weird** (HITL 2026-09-11). When
+ engine output is odd because wire fed the wrong type/shape/OID:
+ (1) **Identify** — schema→wire ask + `trace:true` + emit-diff if
+ generator-shaped. (2) **Temp patch** live-shaped wire in a branch/temp
+ only. (3) **Prove** vs known-good (1.17 / fixtures / HITL MOPS or
+ offline XML). (4) **Permanent** — Schema/wire PR after green; if the
+ mistype is generator-shaped, also teach leftover generator (#162 loop)
+ so regen does not lose the fix.
+
+
+6. **HITL discussion → Effort object in-repo** (HITL 2026-09-13). A good
+ conversation about direction is not the work. When HITL names a multi-session
+ goal (floors triad, generator→SNMP, SSH after two floors, …), Architect
+ **mints or updates a durable Effort** that exists on GitHub outside chat —
+ primary glance: `local/agents/diagrams/effort-board.md` (and sibling MD under
+ `local/agents/` as the board grows). Child work stays GitHub issues (split
+ pieces with named proofs). The Effort MD is the visual bubble sheet; update
+ it when a bubble splits, greens, or blocks. Chat and Architect-machine-only
+ caches are not substitutes. Projects V2 is optional whiteboard only (user
+ fine-grained PAT cannot write user-owned Projects). Each Effort names **finished looks like** up front. Subactions (issues,
+ PRs, named proofs) are spawned by poking that Effort until the end is
+ true — or HITL parks it — without HITL restating the idea.
+
+7. **Inspect timeout ⇒ phase ⇒ call ladder** (HITL 2026-09-13, #92/#215).
+ After `#179`, do not stop at "still timeout." Test bot must run the
+ call-timeout resolution in `test-bot/flow.md` /
+ `diagrams/inspect-timeout.md`: `phase=open` vs `phase=call`; on call,
+ declared SSH read commands + CLI.json cross-check → bucket → Schema /
+ `#92` HITL / Engine heartbeat `NO_HOLE` / SSH-parse remainder. Hang
+ receipts often lack `cli` — YAML+CLI.json is the first poke. Do not
+ invent a seventh clerk for transport.
+
+8. **Engine/HITL NO_HOLE ⇒ auto-park, do not wait** (HITL 2026-09-13).
+ When Schema/Docs exhaust tools and the hole is Engine primitive or
+ human eyes: add/update a row in `diagrams/hitl-engine-park.md`, link
+ consumers, keep soft proveable hops (Schema/Docs/Test) moving. Do not
+ send a go/no-go widget that stalls A′/B. Engine inventing meaning still
+ needs HITL sign-off *when the PR is ready* — parking is not a silent
+ Engine kick.
+
+
+## Never
+
+- Author a fix.
+- Merge without a named-proof receipt (or merge engine without HITL sign-off).
+- Guess device behaviour.
+- Mention any restricted/office device identity in anything a Bot
+ will post (GitHub especially): no model names, hostnames, pool
+ labels, IPs, inventory size/counts, or provenance that implies how
+ much hardware exists. Home-office is fine to name in the abstract. Public
+ language is tested/untested and pass/fail. Device identity stays in
+ gitignored pool / local notes.
+
+## NO_HOLE
+
+Novel meaning, no named tool can prove it, or the signal stays ambiguous
+after the listed tools. Hand HITL facts + the receipt you have + options.
+Also name what tool or hole would have made this fall through.
diff --git a/local/agents/diagrams/base-flow.md b/local/agents/diagrams/base-flow.md
new file mode 100644
index 0000000..b64c2b1
--- /dev/null
+++ b/local/agents/diagrams/base-flow.md
@@ -0,0 +1,28 @@
+# Base flow — does this fit a hole?
+
+Owned by no single clerk. Architect routes through this. Branch this chart
+when a real object does not fit — then update the owning clerk's
+`INSTRUCTIONS.md` to match. Do not invent holes speculatively.
+
+```mermaid
+flowchart TD
+ A[Signal: issue, sweep, or HITL] --> B{Fits a known hole?}
+ B -->|Yes: schema/wire, docs regen,\nengine bug vs existing meaning,\nanchor check, named proof| C[Assign owning clerk]
+ B -->|No: new meaning, no tool can prove it,\nambiguous after the named tools| S[NO_HOLE]
+ S --> S1[HITL: new hole, new tool, or park]
+ S --> S2[["Name the missing hole or missing tool.\nThat is how the sieve grows."]]
+ S1 --> C
+ C --> D[Start: named proof command, currently red]
+ D --> E[Owning clerk: one small change inside bounds]
+ E --> F[Test bot: run the SAME proof\nlive via sidecar, or offline if the hole says so]
+ F --> G{Receipt?}
+ G -->|Red, still inside bounds| E
+ G -->|Green, no new meaning| G2{Touches engine/interpreter.py,\nengine/crude.py, or drivers/base.py?}
+ G2 -->|Yes| Gate[["HITL sign-off. Always, no exception —\nreceipt + one-line why-existing-meaning.\nNot a Bot's call, not Grok's to skip."]]
+ G2 -->|No| L[Implement. Done.]
+ Gate --> L
+ G -->|Tool failed to run: tunnel, SHA, infra| I[BLOCKED — mesh.\nNot evidence either way.]
+ G -->|Would need a new primitive / invariant / behaviour| S
+ L --> N[Docs clerk if methods/docs now stale]
+ L --> O[Test bot: capture/update fixture + verified_via]
+```
diff --git a/local/agents/diagrams/conform.md b/local/agents/diagrams/conform.md
new file mode 100644
index 0000000..002bf8a
--- /dev/null
+++ b/local/agents/diagrams/conform.md
@@ -0,0 +1,41 @@
+# Conform — known state → finished state
+
+The smash loop once live devices are reachable. Nested, ordered, always against a
+HITL-agreed value — never three protocols agreeing with each other.
+
+```
+for each method in schema:
+ for each CRUDE op the method declares (read first; C/U/D/E only after read floor):
+ 1. MOPS — HITL baseline (WebUI / 1.17). This is the floor.
+ 2. SNMP — conform to that MOPS value (generated wire: type, lookup, index).
+ 3. SSH — same floor. Classify miss: wire | driver | engine/primitive.
+```
+
+Two known goods after step 1: the MOPS receipt **and** 1.17/WebUI. SNMP and
+SSH steer toward those, not toward each other.
+
+```mermaid
+flowchart TD
+ M[Next method × CRUDE op] --> R{Read floor exists for this method?}
+ R -->|No, and this op is C/U/D/E| Wait[["Skip writes until read floor.\nDo not CRUD against a guess."]]
+ R -->|Yes, or this is a read| Mops[MOPS live inspect]
+ Mops --> H{HITL / 1.17 agrees with MOPS?}
+ H -->|No — MOPS wrong vs WebUI/CLI| FixM[["Schema/wire MOPS until it matches the anchor.\nDo not 'fix' 1.17 to match MOPS."]]
+ FixM --> Mops
+ H -->|No anchor yet| A[["1.17 clerk / HITL. NO_HOLE until a floor exists."]]
+ H -->|Yes — MOPS is the floor| Snmp[SNMP inspect vs MOPS value]
+ Snmp --> Qs{SNMP matches MOPS?}
+ Qs -->|Yes| Ssh[SSH inspect vs MOPS floor]
+ Qs -->|No — type / lookup / index / syntax| Wsnmp[["Wire YAML. Generated SNMP: fix the declaration.\nSchema clerk."]]
+ Wsnmp --> Snmp
+ Qs -->|No — not a wire declaration| Lsnmp[["Ladder: driver vs engine.\nMostly still not engine."]]
+ Lsnmp --> Ssh
+ Ssh --> Qh{SSH matches MOPS floor?}
+ Qh -->|Yes| Done[["Floor for this method × op × all in-scope transports.\nCapture fixture + verified_via."]]
+ Qh -->|Miss| C{Which layer?}
+ C -->|CLI overlay, command, prompt, field map| Wssh[["Wire SSH overlay. Schema clerk."]]
+ C -->|Parse, state machine, transport I/O| Dssh[["Driver function. Still a bug vs existing meaning if the command already works by hand."]]
+ C -->|Cannot declare it in YAML; need a new primitive| E[["LOGIC. Engine clerk + HITL.\nDo not invent meaning in SSH."]]
+ Wssh --> Ssh
+ Dssh --> Ssh
+```
diff --git a/local/agents/diagrams/effort-board.md b/local/agents/diagrams/effort-board.md
new file mode 100644
index 0000000..87c1cff
--- /dev/null
+++ b/local/agents/diagrams/effort-board.md
@@ -0,0 +1,73 @@
+# Effort board — tangible goals (bubbles)
+
+Owned by Architect. Lives **in this repo** under `local/agents/` so HITL can
+open it on GitHub anytime. Chart first: when HITL names a multi-session goal,
+update this board before (or as) clerks spin. Children are GitHub issues.
+Chat is signal, not storage.
+
+
+## What an Effort is
+
+1. **Goal** — multi-session, broadly named (not a single PR).
+2. **Finished looks like** — concrete enough to say yes/no without re-arguing.
+3. **Poke loop** — Architect/clerks keep opening small issues, PRs, and named
+ proofs until finished-looks-like is true (or HITL parks it). Middle may be
+ wrong; start and end stay fixed.
+
+Chat names the Effort once. The board + child issues *are* the Effort after that.
+
+## Live bubbles (update in PRs — not only in chat)
+
+| Bubble | Status | Children |
+| --- | --- | --- |
+| **A** Offline↔gold floor growth | **DONE** floors=**52** | #169 trail; unfloorables → #193 (poe status-null + route_to); merge #257 `3ca9974` (#258 closed duplicate) |
+| **A′** Live MOPS triad (gold / Offline / live) | **IN PROGRESS** | #229 — floored MOPS receipts done (49/49 class); look-intos as filed |
+| **B** Generator → SNMP vs MOPS known-good | **IN PROGRESS** | #162 teach progressed; live 36/43; Schema #233–#235 **closed**; #231→#12 parked; TC-BITS in #205 parked — see `hitl-engine-park.md` |
+| **C** SSH after two good floors | **SOFT** | Timeout detection done; **#92 stays closed**. Open soft: #226 #227 #228 #217 #178 #62 #274; **#272 NO_HOLE→Engine park**. Closed: #44/#54/#264/#46/#55. Prefer A′/B over stalling. |
+
+```mermaid
+flowchart TD
+ HITL["HITL idea / discussion"] --> E["Update this Effort board MD"]
+ E --> Split["Split into child GitHub issues\nnamed proof each"]
+ Split --> A["A Offline↔gold floors\nDONE 52 / #193 look-into"]
+ Split --> Ap["A′ Live MOPS triad\nIN PROGRESS"]
+ Split --> B["B Generator→SNMP\n#162 IN PROGRESS"]
+ Split --> C["C SSH after two floors\nSOFT"]
+ A --> Re["Recombine: update WHERE here\nthen park or open next bubble"]
+ Ap --> Re
+ B --> Re
+ C --> Re
+```
+
+## Rules
+
+- Every open issue hangs on this board, companion #195, or `hitl-engine-park.md` — fold orphans in; do not leave silent backlog.
+- Companion issue [#195](https://github.com/AdamRickards/crude-engine/issues/195) mirrors this table — update **both** in one hop when bubbles change.
+- HITL/Engine park lives in `hitl-engine-park.md` (not duplicated as a fifth bubble).
+- Tangibility: if it is not on GitHub in this folder (or a child issue), it is not the Effort.
+- Architect updates this file when a bubble splits, greens, or blocks.
+- Six clerks only; Architect holds the board and assigns hops.
+- No lab identity on GitHub.
+
+
+## Tracked outside floors Effort (fold-in 2026-09-14)
+
+Every open issue must hang on this board or `hitl-engine-park.md`. These were open but unnamed; reprocessed under current mechanics (no seventh clerk; soft hops vs Engine/HITL park).
+
+| Bucket | Status | Children |
+| --- | --- | --- |
+| **R** Release / RC | **PARKED** — needs HITL `--gate` | #14 setter/CRUD matrix (260 jobs). Not a floors poke; Architect does not close release without hand-back. |
+| **L** Lab / fixture capture | **PARKED** — HITL | #110 multi-device leftover fixture trees (office L3 vs other profiles). No lab identity on GitHub. |
+| **F** Feature gap (webUI tab) | **INVENTORY DONE** | #129 → proposed `get_egress_shaping` (ask map on issue); implement when ordered; not A′/B blocker. |
+| **T** Tooling | **SOFT** | #156 honour `debug=foreign` / `trace=engine` (post #155). Docs/Engine soft when free; not floors-critical. |
+
+Engine cycle-0 that need primitives or checker work live on **`hitl-engine-park.md`**: #30 SNMPHIOS.close tax; #115 `to_bool` false-vocab; #116 `sort:natural` port heuristic — plus existing #12/#68/#106 rows.
+
+## Finished looks like (this board)
+
+| Bubble | Finished looks like |
+| --- | --- |
+| **A** | Offline↔gold floor growth done for floorable methods; unfloorables filed — **met** (52 floors, #193) |
+| **A′** | MOPS + Offline proved against Gold, Config/XML Offline, and Live (named sweeps; look-into list exists) |
+| **B** | Generator emit≈live for teachable residuals; SNMP meets MOPS known-good floor on named proves |
+| **C** | SSH leftovers worked only after A′+B floors hold; timeout *detection* done (#179–#225); #92 stays closed; parse/parity soft backlog includes older fold-ins |
diff --git a/local/agents/diagrams/hitl-engine-park.md b/local/agents/diagrams/hitl-engine-park.md
new file mode 100644
index 0000000..21ed2bd
--- /dev/null
+++ b/local/agents/diagrams/hitl-engine-park.md
@@ -0,0 +1,30 @@
+# HITL / Engine park list
+
+Architect-owned. When a leftover needs **Engine HITL**, a **new primitive**,
+or Schema/Docs `NO_HOLE` that only Engine can fill: **auto-park here** and
+keep soft Schema/Docs/Test proveable hops moving. Do **not** widget-wait
+HITL (Jaysue 2026-09-13).
+
+Glance: open rows = parked for human/Engine. Close rows when the primitive
+lands or HITL kills the need.
+
+| Parked | Why | Consumers | Soft path meanwhile |
+| --- | --- | --- | --- |
+| #12 SNMP compound-index / `key_format` (ascii) | Engine primitive — Schema tools exhausted | #231 `community_access` mops ascii vs snmp 0; trap-dest class | Lane B continues; leave #231 open as NO_HOLE consumer |
+| TC-BITS (~3) prove-before-flip | Generator teach needs live prove first | #205 residual | Parked — not overnight flip |
+| MOPS/SNMP multi-field INDEX / singular `index_field` collapse | Driver `_list_to_dict` / compound INDEX — Schema cannot un-collapse | #68 get_software images; #106 MOPS multi-field INDEX; #12 SNMP compound | Soft hops elsewhere; park Engine |
+| SNMP inspect `last_oid` / walk heartbeat | Hang never returns; snmp call-timeout has no last_command (SSH-only today) | #47 get_interfaces snmp fanout | Schema walk fan-in first; park Engine heartbeat |
+| PR #76 `get_optics` (open since 2026-08-29) | Held for napalm-hios v1.17 compare; outside current A/A′/B/C lanes | silent backlog if ignored | HITL: revive compare+prove **or** close/park explicitly |
+| #30 SNMPHIOS.close() asyncio tax | Engine/transport — ~2s close destroys pending tasks | sequential sidecar / concurrent prove noise | Soft floors continue; Engine park until HITL opens |
+| #115 `to_bool` English/SNMP false-vocab hardcoded | Checker-blind Engine; schema cannot declare | bool matrices / parity | Soft YAML elsewhere; park Engine |
+| #116 `compute sort:natural` HiOS port-name heuristic | Checker-blind Engine encodes vendor sort | port-ordered tables | Soft elsewhere; park Engine |
+| #156 Engine/tools debug↔trace conflation (post Docs #262) | Docs wording cleared; code still maps debug→trace / missing --debug / audit_setters debug=True | napalm-hios _call; tools CLI; interpreter schema debug alias; sidecar/release_matrix; audit_setters | Soft Docs done; park Engine/tools until HITL opens |
+| #272 get_interface_statistics egress physical filter | `index_filter` ingress-only; mops/snmp keep cpu/vlan n=36 vs SSH physical ~28 | Port Statistics–shaped callers | Soft SSH #271 done; park Engine egress filter |
+| Fail-fast / invalid-CLI redefine | Needs HITL eyes on live invalid/missing/bad-attr | was #92 title — **do not reopen #92**; new ticket if poked | Timeout detection done (#179–#225); SSH parse = #226/#227/#228 |
+
+## Rules
+
+1. Engine merge still needs HITL sign-off when it invents meaning / new primitive.
+2. Soft changes (YAML overlay, floors, docs, harness prove) keep the poke loop.
+3. One row per park reason; consumers link in, do not duplicate umbrellas.
+4. No lab identity on this page.
diff --git a/local/agents/diagrams/inspect-timeout.md b/local/agents/diagrams/inspect-timeout.md
new file mode 100644
index 0000000..8a6b9de
--- /dev/null
+++ b/local/agents/diagrams/inspect-timeout.md
@@ -0,0 +1,44 @@
+# Inspect timeout resolution (open vs call)
+
+After `#179`, inspect timeouts carry `phase=open` or `phase=call`.
+Attribution is the **finder**. Resolution is a **Test-bot poke loop**
+(classify + route — never author a fix). Living law:
+[`../test-bot/flow.md`](../test-bot/flow.md) § Call-timeout resolution.
+
+```mermaid
+flowchart TD
+ T[timeout receipt] --> P{phase?}
+ P -->|open| Open[["Open path: login/prompt/budget.\n#92 class until fail-fast HITL.\nDo not Schema-fake overlay."]]
+ P -->|call| Decl[["List declared SSH *read* commands\nfrom wire overlay for this method"]]
+ Decl --> Cli[["CLI.json / CLI_REFERENCE:\nspelling exists? placeholder?"]]
+ Cli --> Bucket{Bucket}
+ Bucket -->|literal placeholder / invalid spelling| Inv[["Invalid-cmd candidate.\nTrail + #92 fail-fast HITL\n(or Schema if overlay invents bad CLI)"]]
+ Bucket -->|N x per-index fanout under call budget| Fan[["Fanout-budget leftover.\nRoute Schema: table show vs\nper-port {index} loop"]]
+ Bucket -->|few valid shows; still call-timeout| Slow[["Slow-or-hang call.\nNote harness: hang returns no cli.\nOptional Engine: last_command heartbeat"]]
+ Bucket -->|call completes; cli listed n=0| Parse[["Not timeout — SSH parse/overlay.\nClose timeout intention; one remainder ticket"]]
+ Inv --> Trail[Decision trail on issue]
+ Fan --> Trail
+ Slow --> Trail
+ Parse --> Trail
+ Trail --> End[["END: leftover / NO_HOLE / green.\nNever invent a seventh clerk."]]
+```
+
+## Why CLI.json before another live poke
+
+On `phase=call` **timeout**, the worker thread never returns — so
+`trace:true` often has **no** `cli` / `last_cli` in the receipt (hang
+never hits `_collect_cli`). Declared commands from
+`crude_engine/wire/ssh/*.yaml` + `local/reference/CLI/cli_ref_hios_merged.json`
+are the first honest poke. Live re-inspect still proves budgets/phase;
+it does not by itself name the stuck `show`.
+
+## Buckets (glance lines for the issue)
+
+| Bucket | Meaning | Route |
+| --- | --- | --- |
+| open-budget | `phase=open` under current `inspect.yaml` | #92 / budget; not overlay |
+| call-budget-fanout | Many `{index}` / per-row reads vs call budget | Schema (collapse to table `show`) |
+| call-invalid-cmd | CLI not in CLI.json, or literal `{index}` in transcript when call completes | #92 fail-fast HITL and/or Schema |
+| call-unknown-hang | Few valid shows; still `phase=call` timeout; no cli | Trail; Engine `NO_HOLE` if need last_command heartbeat |
+| timeout-cleared-parse | `status=ok` but n=0 / wrong shape | SSH remainder ticket; not #92 |
+
diff --git a/local/agents/docs-clerk/INSTRUCTIONS.md b/local/agents/docs-clerk/INSTRUCTIONS.md
new file mode 100644
index 0000000..ae9cde9
--- /dev/null
+++ b/local/agents/docs-clerk/INSTRUCTIONS.md
@@ -0,0 +1,71 @@
+# Docs clerk
+
+Personality from: [`../diagrams/base-flow.md`](../diagrams/base-flow.md).
+No private `flow.md` until a real branch appears.
+
+## Hole
+
+Generated docs stay generated. Hand-written docs stay true. Almost always
+a post-merge reaction, not an originating fix.
+
+Also: GitHub label `generator` — leftover MIB wire generator cycle
+(isolated TEMP emit-diff vs live `crude_engine/wire`; teach leftover
+source so regen does not lose hand-fixes). See Architect orchestration
+check 4 and skill `mib-wire-generator-cycle`. Never write live wire.
+
+## Start
+
+A merge (or a found stale claim) that added/removed/renamed a method,
+schema, protocol, or diagnostic step.
+
+## End
+
+Named generators run. `python3 scripts/generate_status.py --check` and
+`python3 scripts/check_catalogue.py` are the receipts for catalogue/status.
+Hand-written files either still match the tree or got a factual fix.
+
+## Tools (fixed code only)
+
+- `python3 local/generator/generate_docs.py`
+- `python3 local/generator/generate_method_ref.py`
+- `python3 local/generator/generate_protocols.py`
+- `python3 scripts/generate_status.py` and `--check`
+- `python3 scripts/check_catalogue.py`
+- Leftover generator (TEMP only): `python3 local/generator/batch_generate_MIB.py --isolated --outdir ` (isolated venv; never `crude_engine/wire`)
+
+Generated-only (never hand-edit): `docs/API_REFERENCE.md`,
+`docs/RELEASE_MATRIX.md`, `docs/status.html`. If they look wrong, fix the
+generator or the YAML they read.
+
+Hand-written to keep honest: `CLAUDE.md`, `docs/DIAGNOSTIC_PROCESS.md`,
+`docs/ARCHITECTURE.md`, `docs/program/METHOD.md`, `docs/ROADMAP.md`.
+`docs/WIRING_GUIDE.md` is cited and missing — that is a signal for
+architect, not a file to invent in passing.
+
+## Bounds
+
+When `tests/test_crude_matrix.py` lands (#115), document it next to
+the Gate 2 / `crude_matrix` note in `SCHEMA_PRIMITIVES.md` — harness
+modes prove/discover/all. Do not hand-edit generated pages for it.
+
+
+Regen. Or a one-line factual correction in hand-written docs (e.g. the
+trace vs debug mix-up in `DIAGNOSTIC_PROCESS.md` Step 2). No new process.
+
+## Decision trail
+
+On the GitHub issue (short lines, no mermaid): flow step, what you
+ruled out, tool run, receipt. End with green / leftover / `NO_HOLE`.
+Glance value — wrong bounce feeds the chart; right bounce is obvious.
+
+## Never
+
+- Hand-edit generated files.
+- Treat a docs PASS as live-device proof.
+- Describe a step no current tool supports without routing that as
+ `NO_HOLE` (process/tool gap) to architect.
+
+## NO_HOLE
+
+The doc describes a step the tools cannot perform, or regen would require
+a generator that is dead/unguarded. Stop. Architect / HITL.
diff --git a/local/agents/engine-clerk/INSTRUCTIONS.md b/local/agents/engine-clerk/INSTRUCTIONS.md
new file mode 100644
index 0000000..ede80e4
--- /dev/null
+++ b/local/agents/engine-clerk/INSTRUCTIONS.md
@@ -0,0 +1,68 @@
+# Engine clerk
+
+Personality from: [`flow.md`](flow.md)
+
+## Hole
+
+`crude_engine/engine/interpreter.py`, `crude_engine/engine/crude.py`,
+`crude_engine/drivers/base.py`. Last resort. Most symptoms exit this flow
+to schema/wire before a line of engine changes.
+
+## Start
+
+Symptom with a live receipt (trace or inspect). Ladder steps 1–6 did not
+explain it. 1.17 clerk has an anchor, or this is already `NO_HOLE`.
+
+## End
+
+The named proof green on a full getter sweep
+(`audit_getters.py --compare` against last-known-good), the diff uses
+existing engine meaning — a bug vs declared behaviour, not a new rule —
+**and HITL sign-off.** This is the one absolute rule in the whole roster:
+these three files never merge on a bot's own confidence, no exception, no
+matter how small the diff or how clean the sweep. That's Adam's call, not
+a judgment call Grok or any Bot gets to override. Attach the sweep receipt
++ one line naming why this is existing meaning, not new — the human reads
+that, not a case-file essay.
+
+## Tools (fixed code only)
+
+- Sidecar / `python3 tests/release_matrix.py --inspect --method X --device Y --trace`
+ (**Goal:** `trace` = ours — engine pipeline / `device.last_trace`.
+ `debug` = foreign / not-ours logs — netmiko, paramiko, pysnmp, ….
+ Do not confuse them. Ladder Step 2 is `trace=True`.)
+- Same inspect with `--no-validate` (ladder: works without gates? then it
+ is a declaration bug, not engine — exit to schema clerk).
+- `python3 tests/audit_getters.py --compare `
+- Temporary logging on a live run, then remove it before the change ships.
+ The case file keeps the trace output.
+
+## Bounds
+
+Smallest change that makes existing declared behaviour true. Generic
+execution of something YAML already asked for. Not `if/else` for one
+feature. Not a new step, primitive, or invariant.
+
+## Decision trail
+
+On the GitHub issue (short lines, no mermaid): flow step, what you
+ruled out, tool run, receipt. End with green / leftover / `NO_HOLE`.
+Glance value — wrong bounce feeds the chart; right bounce is obvious.
+
+## Never
+
+- Merge without explicit HITL sign-off. Sweep-green is not sign-off.
+- Merge or claim a fix without the sweep receipt.
+- Design a new primitive. That is `LOGIC` / HITL.
+- Skip the ladder and "just read interpreter.py."
+- Fold unrelated sweep findings into this change. File them as new signals
+ for architect.
+
+## NO_HOLE
+
+- No anchor and none quickly establishable.
+- Sweep cannot run (infra) — `BLOCKED`, not a pass and not a fail.
+- The only fix is new meaning (new primitive, new pipeline rule, "YAML
+ cannot express this"). Stop. HITL answers how things should work.
+- Cannot name a second feature the gap affects — declare it in YAML
+ instead (schema clerk), or if YAML cannot, `LOGIC`.
diff --git a/local/agents/engine-clerk/flow.md b/local/agents/engine-clerk/flow.md
new file mode 100644
index 0000000..22cbad0
--- /dev/null
+++ b/local/agents/engine-clerk/flow.md
@@ -0,0 +1,34 @@
+# Engine clerk — decision flow
+
+Same ladder as `docs/DIAGNOSTIC_PROCESS.md`, as a tree. Most symptoms exit
+before step 7. Branch this chart when a real object does not fit.
+
+Pipeline trace is `trace=True`, not `debug=True`.
+
+```mermaid
+flowchart TD
+ Start[Symptom reported] --> S1[Step 1: compare to a passing sibling]
+ S1 --> Q1{Shape mismatch vs sibling\nexplains it?}
+ Q1 -->|Yes| Exit1[["YAML contract. Schema/wire clerk."]]
+ Q1 -->|No| S2[Step 2: inspect with trace=True]
+ S2 --> S3[Step 3: inspect with --no-validate]
+ S3 --> Q3{Works with validate off?}
+ Q3 -->|Yes| Exit3[["Gate declaration wrong. Schema/wire clerk."]]
+ Q3 -->|No| S4[Step 4: wire audit — MIB / MOPS / CLI_REFERENCE]
+ S4 --> S5[Step 5: ask 1.17 clerk — does an anchor exist?]
+ S5 -->|No anchor| Exit5a[["NO_HOLE. Establish an anchor first.\nDo not guess."]]
+ S5 -->|Anchor exists| Q5{Anchor says engine gap,\nnot wire?}
+ Q5 -->|No — wire| Exit5b[["Wire fix. Schema/wire clerk."]]
+ Q5 -->|Yes| S7{Step 7: existing meaning?\n>= 2 features, YAML cannot declare it away}
+ S7 -->|Declare in YAML instead| Exit7[["Schema clerk. Not engine."]]
+ S7 -->|Would be a NEW primitive / rule| ExitL[["LOGIC. HITL.\nDo not invent meaning."]]
+ S7 -->|Bug vs existing meaning| Draft[Smallest engine diff that makes the declared behaviour true]
+ Draft --> Sweep[audit_getters.py --compare vs last-known-good]
+ Sweep --> Q8{Receipt?}
+ Q8 -->|Regression| Draft
+ Q8 -->|Sweep did not run — infra| Infra[["BLOCKED. Not pass, not fail."]]
+ Q8 -->|Green| Gate[["HITL sign-off. Always, no exception —\nsweep receipt + one-line why-existing-meaning,\nnot a case-file essay. Not a Bot's call."]]
+ Gate --> L[Implement. Done.]
+ L --> H1[Test bot: fixture capture/update]
+ L --> H2[Docs clerk: DIAGNOSTIC_PROCESS / ARCHITECTURE now stale?]
+```
diff --git a/local/agents/local-agents-refactor.md b/local/agents/local-agents-refactor.md
new file mode 100644
index 0000000..a335910
--- /dev/null
+++ b/local/agents/local-agents-refactor.md
@@ -0,0 +1,131 @@
+# local/agents refactor — spec for humans (and Claude)
+
+Chiseled 2026-09-05. Target: flowchart personality, fixed-code tools,
+small AI gap between a red named proof and a green one. HITL only when
+the object does not fit a hole (new logic, or a new tool so it can be
+proven).
+
+Claude: poke this file and the standing files it names. Do not restore
+session-stale counts into `INSTRUCTIONS.md`. Do not invent a dashboard,
+check-in protocol, or seventh clerk.
+
+**Amendment, 2026-09-05, Adam, overriding the cut below:** engine never
+merges without HITL sign-off is reinstated, full stop, no exception —
+including for a proven bug against existing meaning. This is Adam's call,
+not Grok's, not Claude's, not a thing a Bot gets to reason its way past.
+Kept cheap: the sign-off is a receipt (sweep result + one line naming why
+this is existing meaning, not new), not a case-file essay — that part of
+the cut stands. What's reinstated is only that the gate exists and is
+unconditional. See `README.md`, `engine-clerk/INSTRUCTIONS.md`,
+`engine-clerk/flow.md`, `diagrams/base-flow.md` — all updated to match.
+
+## Intent
+
+```
+Flowchart = personality + sieve (human glance, Bot derived from it)
+Fixed tools = almost all judgment (inspect, trace, validate, compare)
+AI bounds = what YAML/code to try between red and green, inside the hole
+Start = issue + exact proof command, red
+End = same command, green, on the tree that contains the change
+NO_HOLE = new meaning, or the named tool cannot run → HITL
+```
+
+Schema/wire/docs: proven bug vs existing declared behaviour ships without a
+human. Engine files (`interpreter.py`, `crude.py`, `drivers/base.py`):
+same proof loop, but HITL glance before merge — receipt + one line, not an
+essay. Inventing a primitive, invariant, or pipeline rule is always HITL.
+
+Sidecar on the VPS is how Grok Bot reaches live devices (WireGuard). It is
+not a second Grok. Offline CI is a pre-filter. Live-device proof is the proof.
+
+Do not build HTML5 / leases / sidecar-as-state-machine yet. Get this loop
+boring on real issues first.
+
+## Standing files (Bots may read)
+
+| File | Role |
+|---|---|
+| `README.md` | Sieve, HITL cut, roster, fixture tiers, sidecar, privacy |
+| `diagrams/base-flow.md` | Cross-cutting holes |
+| `diagrams/conform.md` | Conform smash: method × transport × CRUDE, MOPS floor then SNMP then SSH |
+| `*/INSTRUCTIONS.md` | Fixed format: hole, start, end, tools, bounds, never, NO_HOLE |
+| `engine-clerk/flow.md` | Engine ladder as a tree (`trace=True`) |
+| `test-bot/flow.md` | Which proof lane |
+
+## Historical (Bots must not read)
+
+| File | Role |
+|---|---|
+| `AGENTS-TODO.md` | Stub pointing here |
+| `AUDIT-2026-09-04.md` | Bannered snapshot. Useful archaeology. Claims about
+ tool status, issue numbers, and the sidecar checkout may be wrong on
+ *this* tree. |
+
+## What was cut on purpose
+
+- ~~Hard gate "never merge engine without HITL." Wrong cut. New **meaning**
+ is the gate.~~ **Reinstated 2026-09-05 — see amendment above.** New
+ meaning is still *a* gate (on top of the hard one), not a replacement
+ for it.
+- Session numbers as law ("40/45 compliant", "79 replay failures",
+ "3 live principles violations"). Those rot and Bots recite them.
+- Long narrative INSTRUCTIONS (case-file novels, cron sketches, memory
+ porting). Personality is the mermaid; instructions are the leash.
+- Dashboard, check-in/out, IR compiler, GitHub `triage:*` lifecycle.
+ Too much bus before the loop works.
+- Treating sidecar as "Grok is a constrained agent." Grok Bot is the
+ worker; sidecar is the proof plane.
+
+## What was kept
+
+- Narrow clerks (a clerk that can touch anything will).
+- Proof-or-stop. A code-read is not a receipt.
+- Three-way branches already earned: no-anchor vs anchor-says-no;
+ sweep-regression vs sweep-infra.
+- Fixture `verified_via` tiers and MOPS→SNMP→SSH bootstrap.
+- Diagrams separate from instructions (they change on different rhythms).
+- Architect privacy redaction at ingress.
+- Wire still under schema clerk until a real collision.
+
+## Poke list (Claude)
+
+Verify, then either fix in-tree or file as `NO_HOLE` / missing tool — do
+not quietly rewrite the sieve.
+
+1. **`docs/DIAGNOSTIC_PROCESS.md` Step 2** — **done** (#155): Step 2 is
+ `trace=True` (pipeline / `device.last_trace`). `debug` = foreign library
+ logs only. Do not reopen as a docs hole.
+2. **Tool commands in INSTRUCTIONS** — run or read each listed command on
+ *this* checkout. Dead `napalm-hios-v2` paths, missing
+ `WIRING_GUIDE.md`, sidecar only exposing inspect: confirm and leave as
+ `NO_HOLE` (mesh), do not tell Bots to use broken generators.
+3. **`validate_schemas.py --errors` current count** — do not write it into
+ instructions. If it is green, good. If not, that is schema work with
+ that command as the proof, not a roster edit.
+4. **Dual trees** — this folder lives in a Syncthing copy. The audit's
+ the sidecar checkout path on a personal machine may be gone. Do not assume
+ GitHub or the VPS matches until a SHA/receipt says so.
+5. **No new `flow.md`** for architect, schema, docs, 1.17 unless a real
+ issue fails to fall through an existing hole. Then branch the mermaid
+ first, then shorten INSTRUCTIONS to match.
+6. **Do not port Claude `feedback_*` memory** into instructions as essays.
+ If a lesson is load-bearing, it is already a required step (full sweep
+ after engine; content not shape). Anything else waits for a `d`.
+
+## Open (Adam / HITL, not a Bot)
+
+- Widen sidecar verbs (`--trace`, `--no-validate`, `--compare`, replay)
+ so Tester is not `BLOCKED` on mesh.
+- Tree identity on every live receipt (what SHA/generation the VPS ran).
+- Whether schema vs wire needs its own hole — only after they collide.
+- Conform smash loop is now `diagrams/conform.md` (method × transport × CRUDE,
+ MOPS HITL floor → SNMP → SSH, classify layer). Do not "improve" it into
+ three protocols voting.
+- `DIAGNOSTIC_PROCESS.md` Step 2 fix (docs clerk bounds already allow it).
+
+## Done looks like
+
+A Bot given one issue can: read its flowchart, run only listed tools,
+loop inside bounds until the named proof is green, implement, or stop
+with `NO_HOLE`. A human can glance the mermaid and see which hole that
+was. No essay. No new meaning. No dashboard required.
diff --git a/local/agents/schema-clerk/INSTRUCTIONS.md b/local/agents/schema-clerk/INSTRUCTIONS.md
new file mode 100644
index 0000000..bd523bb
--- /dev/null
+++ b/local/agents/schema-clerk/INSTRUCTIONS.md
@@ -0,0 +1,73 @@
+# Schema clerk
+
+Personality from: [`../diagrams/base-flow.md`](../diagrams/base-flow.md),
+[`../diagrams/conform.md`](../diagrams/conform.md), and diagnostic ladder
+steps 1–6 in `docs/DIAGNOSTIC_PROCESS.md`.
+No private `flow.md` until a real schema-vs-wire collision on a PR.
+
+## Hole
+
+`crude_engine/schemas/*.yaml` and, for now, `crude_engine/wire/**/*.yaml`.
+A failure that is a contract mismatch, not new engine meaning.
+
+## Start
+
+Named method × transport × CRUDE op, named proof (sidecar inspect), red
+against the MOPS/HITL floor. Architect has already said this is schema/wire.
+SNMP misses are usually wire type, lookup, or index — fit SNMP to MOPS, do
+not fit MOPS to SNMP.
+
+## End
+
+The same proof green. `validate_schemas.py --errors` introduces no new
+structural errors. No new YAML primitive invented.
+
+## Tools (fixed code only)
+
+- `python3 local/generator/validate_schemas.py --errors`
+ — structural schema law. Weekend/#115: extend this (same script)
+ so schema attr types vs wire `syntax` must be a legal edge in
+ `crude_matrix.yaml` (nonsense pairings = error). Inventory starts
+ from matrix keys + wire syntaxes (`test_crude_matrix.py discover`).
+ Wire (MIB) has more rights — nonsense is usually schema-side. When
+ the allow-list lands, beat existing schemas into shape (shorter
+ standard list OK). Wire PRs: harness before/after with Test bot.
+- Sidecar / `python3 tests/release_matrix.py --inspect --method X --device Y [--trace] [--protocol P] [--no-validate]`
+- Sibling schema/wire YAML (read a passing method, diff declarations)
+- `local/reference/MIBs/`, `local/reference/MOPS/mops_hios.xml`,
+ `local/reference/CLI/CLI_REFERENCE.md` (read-only ground truth)
+- Hand-edit wire YAML as a stopgap, then the proof must pass. Generator
+ leftover is a separate `NO_HOLE` (tool work), not a reason to skip the
+ hand-fix.
+
+Do not run `heal_schemas.py`, `validate_schema_wire.py`, or unguarded
+generators that still hardcode `napalm-hios-v2` paths. If you need them,
+that is a missing-tool `NO_HOLE`.
+
+`docs/WIRING_GUIDE.md` is cited elsewhere and missing. Until it exists,
+ladder + a passing sibling YAML.
+
+## Bounds
+
+Change schema/wire declarations so the existing engine executes the
+contract. Smallest YAML diff that makes the named proof green.
+
+## Decision trail
+
+On the GitHub issue (short lines, no mermaid): flow step, what you
+ruled out, tool run, receipt. End with green / leftover / `NO_HOLE`.
+Glance value — wrong bounce feeds the chart; right bounce is obvious.
+
+## Never
+
+- Touch `engine/interpreter.py`, `engine/crude.py`, `drivers/base.py`
+ (that is engine clerk, and only if the ladder exits there).
+- Invent a new primitive or a new `steps.yaml` key.
+- Treat structural validate-clean as content-correct. Live inspect (or a
+ captured fixture with a real `verified_via`) is the proof.
+- Throwaway Python that calls `device.get_*()`.
+
+## NO_HOLE
+
+Ladder says engine, or the fix only works by adding meaning the engine
+does not have, or the named live tool cannot run. Stop.
diff --git a/local/agents/test-bot/INSTRUCTIONS.md b/local/agents/test-bot/INSTRUCTIONS.md
new file mode 100644
index 0000000..761b130
--- /dev/null
+++ b/local/agents/test-bot/INSTRUCTIONS.md
@@ -0,0 +1,82 @@
+# Test bot
+
+Personality from: [`flow.md`](flow.md) and
+[`../diagrams/conform.md`](../diagrams/conform.md).
+
+## Hole
+
+Run proof. Never author a fix. The autonomous surface is exactly as large
+as what this clerk can prove with listed tools.
+
+## Start
+
+Verification requested: a named proof command, or one cell of
+method × transport × CRUDE op against the MOPS/HITL floor. Engine diffs
+still require a full getter sweep.
+
+## End
+
+A receipt: command, tree/SHA if known, pass / fail / blocked, and the
+actual diff or output — not "looks good." On live work, sidecar ran it.
+
+## Tools (fixed code only)
+
+- `python3 tests/release_matrix.py --inspect --method X --device Y [--trace] [--protocol P] [--no-validate]`
+ — the harness. Never a throwaway `device.get_*()` script.
+- `python3 tests/audit_getters.py --compare `
+ — required after any change to `interpreter.py`, `crude.py`,
+ `drivers/base.py`, or a shared primitive. One method green is not enough.
+- `python3 tests/test_replay.py` — only against fixtures that exist and
+ have a `verified_via` you trust. Empty fixtures ≠ green.
+- `python3 scripts/ci_offline.sh` — cheap pre-filter. Not live-device proof.
+ `check_principles.py` / `check_catalogue.py` are scored inside it;
+ they are not the release gate until `REQUIRE_RELEASE_PROOFS=1`.
+- `python3 scripts/check_principles.py`
+- `python3 scripts/generate_status.py --check` and
+ `python3 scripts/check_catalogue.py` for docs-only holes.
+- `python3 tests/test_crude_matrix.py` — offline wire-syntax ×
+ schema-type transform prove / discover / all (see #115).
+ Until the script exists: missing-tool `NO_HOLE` / `BLOCKED`, do
+ not improvise. On wire changes: run discover/prove **before and
+ after** so the receipt shows schema follow-ups or a datatype fix.
+- `python3 tests/offline_gold_matrix.py [--config XML] [--gold JSON] [--methods …] [--strict]`
+ — CI/CD **gate** candidate for MOPS/Offline gather changes: OfflineHIOS +
+ saved mibconf vs gold floors (MOPS kinship). `config_absent`∩gold and
+ `gold_absent` are feedback, not fail; exit ≠ 0 only on `mismatch`
+ (see #165/#169). Sanitized fixtures in CI; bag prove is local-only.
+
+If sidecar only exposes inspect today, a proof that needs `--compare` or
+replay and cannot be run is `BLOCKED` (mesh), not a skip.
+
+## Bounds
+
+On inspect **timeout**, follow [`flow.md`](flow.md) § Call-timeout
+resolution and [`../diagrams/inspect-timeout.md`](../diagrams/inspect-timeout.md):
+attribute `phase`, then for `phase=call` inventory declared wire CLI +
+CLI.json before parking as "budgets." Classification is proof work;
+fixes stay with Schema/Engine/HITL.
+
+Choose the proof lane the flowchart says. Capture/update
+`tests/fixtures/` with an honest `verified_via` when the hole says so.
+Validate content, not just shape (row count matching is not correctness).
+
+## Decision trail
+
+On the GitHub issue (short lines, no mermaid): flow step, what you
+ruled out, tool run, receipt. End with green / leftover / `NO_HOLE`.
+Glance value — wrong bounce feeds the chart; right bounce is obvious.
+
+## Never
+
+- Author or "just tweak" the fix.
+- Declare verified against a guess or an `unverified` snapshot used as
+ an anchor.
+- Treat a broken sweep (device down, timeout) as evidence for or against
+ the change.
+- Silently drop an unrelated finding from a sweep. That is a new signal
+ for architect.
+
+## NO_HOLE
+
+No anchor for this method (1.17 / HITL first). Named tool not on sidecar
+and not runnable here. Sweep infra failure — `BLOCKED`, not fail.
diff --git a/local/agents/test-bot/flow.md b/local/agents/test-bot/flow.md
new file mode 100644
index 0000000..55a63b2
--- /dev/null
+++ b/local/agents/test-bot/flow.md
@@ -0,0 +1,104 @@
+# Test bot — which proof lane
+
+Branch this chart when a real object does not fit. Do not invent lanes.
+
+```mermaid
+flowchart TD
+ Start[Verification requested] --> Q1{What kind of change?}
+ Q1 -->|Docs-only| Skip[["generate_status.py --check\n+ check_catalogue.py"]]
+ Q1 -->|Schema or wire, one method| Q2{Known-good anchor?}
+ Q1 -->|Engine or shared primitive| Full["audit_getters.py --compare\nfull sweep"]
+ Q1 -->|Matrix / CRUDE type transform| Matrix["test_crude_matrix.py\nprove | discover | all"]
+ Q1 -->|MOPS/Offline kinship / expand floors| OfflineGold["offline_gold_matrix.py\nCI gate: Offline+gold; END mismatch fail"]
+ Q2 -->|No anchor| Anchor[["NO_HOLE for 1.17 / HITL.\nDo not verify against a guess."]]
+ Q2 -->|Fixture with verified_via| Replay["test_replay.py"]
+ Q2 -->|Manual WebUI/CLI, not captured| Inspect["sidecar / release_matrix.py --inspect\nthen capture fixture + verified_via"]
+ Replay --> Q3{Receipt?}
+ Inspect --> Q3
+ Skip --> Q3
+ Matrix --> Q3
+ OfflineGold --> Q3
+ Q3 -->|Fail| Back[["Back to owning clerk with the diff."]]
+ Q3 -->|Pass| Q4{Touches a shared primitive?}
+ Q4 -->|Yes or unsure| Full
+ Q4 -->|No| Done[["Verified. Fixture + verified_via if live."]]
+ Full --> Q5{Sweep receipt?}
+ Q5 -->|Regression| Back
+ Q5 -->|Sweep did not run — infra| Infra[["BLOCKED. Not pass, not fail."]]
+ Q5 -->|Clean| Done
+```
+
+## Offline vs gold (standing) — CI/CD gate
+
+**Purpose / END:** gate MOPS/Offline changes. Offline prove vs saved config +
+gold floors = kinship that MOPS gather shape holds. Gold is the **floor**
+(mismatch catcher), not a second loader. Live sidecar not required for this
+lane. Prove the tool **by using it** (fuller sweeps); sweep feedback →
+look-into list (issues later). If the harness breaks: issue → temp fix →
+prove → PR → merge.
+
+- **START:** schema gather method(s) + Offline + mibconf XML. Named proofs:
+ - CI / sanitized: `PYTHONPATH=. python3 tests/offline_gold_matrix.py`
+ (DEFAULT_METHODS = floored set). Optional `--methods` catalogue-wide
+ for stress (missing floors → `gold_absent` feedback, not fail).
+ - Bag-local (optional, never CI identity): `--config` / `--gold` against
+ a local bag NVM + bag gold. Committed floors stay under
+ `tests/fixtures/offline_gold/`.
+- **WHERE WE ARE:** floored coverage vs catalogue; last sanitized + bag
+ counts; look-into list (`gold_absent`, `config_absent` followups,
+ `offline_empty`, mismatches, blocked). Improve this section when
+ good/bad/ugly hits (what is CI vs bag-only).
+- **END:** receipt — fail only on `mismatch` (exit ≠ 0). `config_absent`∩gold
+ = oper/live followup, not fail. `gold_absent` = missing floor (feedback).
+ Gate-ready for CI when floored DEFAULT_METHODS stay green on sanitized
+ fixtures after MOPS/Offline/FeatureEngine gather changes.
+
+When: after MOPS/Offline client or FeatureEngine gather changes; expanding
+floors; catalogue stress sweeps; before treating Offline as kinship for live
+MOPS.
+
+
+## Call-timeout resolution (standing) — after phase attribution
+
+**Purpose / END:** when inspect reports `status=timeout` with
+`phase=call` (or `phase=open`), **classify why** and route — do not stop
+at "still timeout under budgets." Chart:
+[`../diagrams/inspect-timeout.md`](../diagrams/inspect-timeout.md).
+
+**START (every timeout leftover):**
+
+1. Quote per-protocol `status`, `phase`, `open_ms`, `call_ms`, budgets from
+ `tests/inspect.yaml`. `trace:true` on the named read.
+2. **Split on phase** (finder from `#179`):
+ - `phase=open` → open path (login/prompt/budget). Park as `#92` class
+ until fail-fast HITL. Do not Schema-fake an overlay.
+ - `phase=call` → continue this ladder (call path).
+3. **Declared CLI inventory** (required on call-timeout — hang often has
+ **no** `cli` in the receipt): from `crude_engine/wire/ssh/` for this
+ method, list every **read** `command:`. Count distinct shows vs
+ `{index}` / per-row fanouts.
+4. **CLI.json cross-check** (`local/reference/CLI/cli_ref_hios_merged.json`
+ + `CLI_REFERENCE.md`): does each base `show …` exist? Any invented
+ spelling? Literal placeholder tokens?
+5. **Bucket** (one glance line on the issue — see diagram table):
+ `open-budget` | `call-budget-fanout` | `call-invalid-cmd` |
+ `call-unknown-hang` | `timeout-cleared-parse`.
+6. **Route** (Architect multiplexes; Test bot does not fix):
+ - fanout / wrong CLI spelling → Schema (overlay honesty).
+ - invalid hang / fail-fast redefine → `#92` HITL (user eyes).
+ - unknown hang after inventory → trail + optional Engine `NO_HOLE`
+ for harness `last_command` heartbeat on timeout.
+ - timeout cleared, parse/shape wrong → close timeout intention; one
+ SSH remainder ticket (standing SSH-split law).
+
+**WHERE WE ARE:** learn by repeating this poke on each call-timeout
+leftover; wrong bounce feeds the chart; right bounce is obvious on the
+decision trail.
+
+**END:** decision-trail comment with bucket + declared command list +
+CLI.json hit/miss + route. Green only when live receipt clears *this*
+leftover's intention — not when budgets alone move.
+
+**Never:** author the fix; treat pre-`#179` "overall deadline" null timings
+as the same as phase-attributed call; skip CLI.json because mops/snmp
+were ok (finder, not a vote).
diff --git a/docs/TODO-old.md b/local/archive/docs-legacy/TODO-old.md
similarity index 100%
rename from docs/TODO-old.md
rename to local/archive/docs-legacy/TODO-old.md
diff --git a/docs/TODO.md b/local/archive/docs-legacy/TODO.md
similarity index 100%
rename from docs/TODO.md
rename to local/archive/docs-legacy/TODO.md
diff --git a/docs/TODO_HITLIST.md b/local/archive/docs-legacy/TODO_HITLIST.md
similarity index 100%
rename from docs/TODO_HITLIST.md
rename to local/archive/docs-legacy/TODO_HITLIST.md
diff --git a/local/generator/README.md b/local/generator/README.md
index 889d80a..036a8be 100644
--- a/local/generator/README.md
+++ b/local/generator/README.md
@@ -42,38 +42,48 @@ python3 local/generator/validate_schemas.py --json # machine-readable
| `docs/DIAGNOSTIC_PROCESS.md` | **Authored** — mandatory fault-finding ladder |
| `docs/SCHEMA_PRIMITIVES.md` | **Authored** — YAML key reference |
| `docs/ROADMAP.md` | **Authored** — milestones |
-| `docs/TODO.md` | **Authored** — work items |
+| GitHub issues | Leftover work (prove-then-file or comment-close). Not `docs/TODO.md`. |
-## Wire Generators
+## Optional read-only audits (retargeted at `crude_engine/`)
-Tools that produce wire YAMLs from MIB/MOPS device truth.
+These are leftover v26 walks whose `BASE_DIR` was retargeted at this
+repo's `crude_engine/{wire,schemas}`. They do not mutate YAML. They are
+**not** live doc generators — do not treat their output as catalogue law.
-| Generator | Output | What it reads |
-|-----------|--------|---------------|
-| `batch_generate_MIB.py` | `local/reference/webUI/*.yaml` | MIB XML (`local/reference/MOPS/mops_hios.xml`) + optional WebUI captures |
+| Audit | Output | What it reads |
+|-------|--------|---------------|
+| `validate_v26_all.py` | stdout | Schema→wire broken links + duplicate OIDs |
+| `audit_wire.py` | `docs/WIRE_INTEGRITY.md` (only if you run it) | Wire protocol coverage + duplicate names |
-### Wire Generation Pipeline
+Live schema law remains `validate_schemas.py` (CI).
-```bash
-# Step 1: Generate wire YAMLs from MIB
-python3 local/generator/batch_generate_MIB.py
+## Leftover v26/monolith scripts (not live law)
-# Step 2: Map schemas to generated wires
-python3 local/generator/heal_schemas.py
+Machine-absolute monolith paths are **removed**.
+Leftover scripts are either repo-relative + `--run-archive` stubs, or
+(for MIB emit) isolated temp outdir only. Original absolute-path bodies
+live under `local/archive/generator-monolith-abs/` for archaeology.
+Do not heal/enrich/batch-generate against live YAML.
+`batch_generate_MIB.py` may be invoked **isolated** into a TEMP outdir
+for emit-diff only — never `crude_engine/wire`:
-# Step 3: Audit integrity
-python3 local/generator/validate_v26_all.py
+```bash
+python3 local/generator/batch_generate_MIB.py --isolated --outdir /tmp/crude-mib-emit
```
-See the [WIRE_SPEC.md](../../docs/WIRE_SPEC.md) for format details.
-
-## Other Tools
-
-| Tool | Purpose |
-|------|---------|
-| `heal_schemas.py` | Remap schema `source:` fields to MIB-named wire files |
-| `cross_validate_v26.py` | Validate generated YAMLs against schema contract |
-| `validate_v26_all.py` | Full broken-link + duplicate OID audit |
-| `audit_claims.py` | Audit v2.6 coverage claims against wire reality |
-| `audit_wire.py` | Wire-level integrity checks |
-| `overrides.yaml` | Manual corrections for generator output (create_method, type) |
+| File | Why leftover |
+|------|----------------|
+| `batch_generate_MIB.py` | One-shot MIB→wire generator; not live law. Isolated `--outdir` only |
+| `batch_generate_webui.py.stable` | Same class (sibling one-shot) |
+| `heal_schemas.py` | Mutates schema `source:` against old webUI wires |
+| `enrich_schema_v26.py` | Mutates `docs/napalm-hios-2-6-schema.md` |
+| `cross_validate_v26.py` | v26 master-schema markdown vs webUI |
+| `validate_schema_wire.py` | Post-heal check against `local/reference/webUI` |
+| `audit_v26_coverage.py` | Needs v1 `hios.py` + shim `adapters/napalm.yaml` |
+| `audit_v26_integrity.py` | Same + old webUI wires |
+| `audit_web_coverage.py` | Needs machine-local LocalUI captures |
+| `audit_claims.py` | Needs monolith `hios.py` adapter + v2.6 claim numbers |
+
+`overrides.yaml` is data for the leftover MIB generator, not live law.
+
+See [WIRE_SPEC.md](../../docs/WIRE_SPEC.md) for live wire format.
diff --git a/local/generator/audit_claims.py b/local/generator/audit_claims.py
index 638c64f..da5ba97 100644
--- a/local/generator/audit_claims.py
+++ b/local/generator/audit_claims.py
@@ -1,166 +1,54 @@
-import os
-import yaml
-import re
-from collections import defaultdict
+"""Leftover v26/monolith script. Not live law.
+
+Paths are repo-relative (no machine-absolute hardcodes). Do not treat
+this as a live generator. Live: generate_docs.py, generate_method_ref.py,
+generate_protocols.py, validate_schemas.py. See local/generator/README.md.
+"""
+from __future__ import annotations
+
+import argparse
+import sys
+from pathlib import Path
+
+_REPO_ROOT = Path(__file__).resolve().parents[2]
+_WIRE = _REPO_ROOT / "crude_engine" / "wire"
+_SCHEMAS = _REPO_ROOT / "crude_engine" / "schemas"
+
+
+def _parse_paths(argv: list[str] | None = None):
+ p = argparse.ArgumentParser(description=__doc__)
+ p.add_argument(
+ "--wire-dir",
+ type=Path,
+ default=_WIRE,
+ help="Wire YAML dir (default: repo crude_engine/wire)",
+ )
+ p.add_argument(
+ "--schema-dir",
+ type=Path,
+ default=_SCHEMAS,
+ help="Schema YAML dir (default: repo crude_engine/schemas)",
+ )
+ p.add_argument(
+ "--run-archive",
+ action="store_true",
+ help="Required to actually execute this leftover script",
+ )
+ return p.parse_args(argv)
+
+
+def main(argv: list[str] | None = None) -> None:
+ args = _parse_paths(argv)
+ if not args.run_archive:
+ raise SystemExit(
+ "leftover archive script; pass --run-archive to execute "
+ "(still not live law). Defaults are repo-relative."
+ )
+ raise SystemExit(
+ "archive body not ported to relative paths as a safe mutator; "
+ "use validate_schemas.py / isolated batch_generate_MIB.py instead"
+ )
-# Paths
-BASE_DIR = '/home/adamr/obsidian-vault/Projects/napalm-hios-v2'
-SCHEMAS_DIR = os.path.join(BASE_DIR, 'napalm_hios/schemas')
-WIRE_DIR = os.path.join(BASE_DIR, 'napalm_hios/wire')
-DRIVERS_DIR = os.path.join(BASE_DIR, 'napalm_hios/drivers')
-ENGINE_PY = os.path.join(BASE_DIR, 'napalm_hios/engine/interpreter.py')
-TRANSFORMS_PY = os.path.join(BASE_DIR, 'napalm_hios/engine/transforms.py')
-HIOS_PY = os.path.join(BASE_DIR, 'napalm_hios/hios.py')
-ARCH_MD = os.path.join(BASE_DIR, 'docs/ARCHITECTURE.md')
-RFC_MD = os.path.join(BASE_DIR, 'docs/RFC_MAPPING.md')
-API_REF_MD = os.path.join(BASE_DIR, 'docs/API_REFERENCE.md')
-OUTPUT_FILE = os.path.join(BASE_DIR, 'docs/CLAIMS_AUDIT.md')
-
-def load_yaml(path):
- try:
- with open(path, 'r') as f:
- return yaml.safe_load(f) or {}
- except: return {}
-
-def get_arch_claims():
- """Extract claims from ARCHITECTURE.md"""
- claims = {'primitives': []}
- if not os.path.exists(ARCH_MD): return claims
- with open(ARCH_MD, 'r') as f:
- content = f.read()
-
- m = re.search(r"(\d+) schema YAMLs", content)
- if m: claims['schema_count'] = int(m.group(1))
-
- m = re.search(r"(\d+) methods", content)
- if m: claims['method_total'] = int(m.group(1))
-
- m = re.search(r"(\d+)C (\d+)R (\d+)U (\d+)D (\d+)E", content)
- if m:
- claims['breakdown'] = {'create': int(m.group(1)), 'read': int(m.group(2)), 'upsert': int(m.group(3)), 'delete': int(m.group(4)), 'execute': int(m.group(5))}
-
- m = re.search(r"([\d,]+) attrs", content)
- if m: claims['attr_count'] = int(m.group(1).replace(',', ''))
-
- # Execute methods matrix from doc
- methods = []
- table_started = False
- for line in content.split('\n'):
- if "| Method | MOPS | SNMP | SSH |" in line:
- table_started = True; continue
- if table_started and line.startswith('|'):
- if '---' in line: continue
- parts = line.split('|')
- if len(parts) > 1: methods.append(parts[1].strip('` '))
- elif table_started: break
- claims['execute_methods'] = set([m for m in methods if m])
-
- # Qualitative (Primitives)
- prim_section = False
- for line in content.split('\n'):
- if "**Schema primitives**" in line: prim_section = True; continue
- if prim_section and line.startswith('- `'):
- claims['primitives'].append(line.split('`')[1].replace(':', ''))
- elif prim_section and line.startswith('###'): prim_section = False
-
- return claims
-
-def method_exists_in_file(file_path, method_name):
- if not os.path.exists(file_path): return False
- with open(file_path, 'r') as f:
- content = f.read()
- return f"def {method_name}(" in content
-
-def audit():
- print("Performing 3-Link Integrity Audit (YAML -> Driver -> Adapter)...")
- claims = get_arch_claims()
-
- results = {
- 'schemas': set(),
- 'wire_files': 0,
- 'attrs_total': 0,
- 'methods': defaultdict(set),
- 'engine': {},
- 'leaks': [],
- 'documented': set(),
- 'execute': {}
- }
-
- # 1. Implementation Reality
- for sf in os.listdir(SCHEMAS_DIR):
- if sf.endswith('.yaml'):
- name = sf.replace('.yaml', '')
- results['schemas'].add(name)
- data = load_yaml(os.path.join(SCHEMAS_DIR, sf))
- for m_name, m_def in data.get('methods', {}).items():
- m_type = m_def.get('type', 'read')
- if m_type in ('dict', 'list', 'table'): m_type = 'read'
- results['methods'][m_type].add(m_name)
-
- for wf in os.listdir(WIRE_DIR):
- if wf.endswith('.yaml'):
- results['wire_files'] += 1
- data = load_yaml(os.path.join(WIRE_DIR, wf))
- results['attrs_total'] += len(data.get('attributes', {}))
-
- # 2. Engine Capability
- with open(ENGINE_PY, 'r') as f: engine_code = f.read()
- for p in claims['primitives']:
- results['engine'][p] = p in engine_code
-
- # 3. Execute Integrity Check
- with open(HIOS_PY, 'r') as f: hios_code = f.read()
-
- for proto in ['SSH', 'SNMP', 'MOPS']:
- proto_yaml = load_yaml(os.path.join(DRIVERS_DIR, f"{proto}.yaml"))
- methods = proto_yaml.get('execute_methods', [])
-
- py_file = os.path.join(DRIVERS_DIR, f"{proto.lower()}.py")
- if proto == 'MOPS': py_file = os.path.join(DRIVERS_DIR, "mops_transport.py")
- if proto == 'SNMP': py_file = os.path.join(DRIVERS_DIR, "snmp_transport.py")
-
- results['execute'][proto] = {}
- for m in methods:
- in_driver = method_exists_in_file(py_file, m)
- in_adapter = f"def {m}(" in hios_code
- results['execute'][proto][m] = {'driver': in_driver, 'adapter': in_adapter}
-
- # 4. REPORT
- doc = "# Architecture Integrity Audit (v2.6)\n\n"
- doc += "> Verifying the **3-Link Chain**: YAML Declaration → Driver Implementation → Adapter Exposure.\n\n"
-
- doc += "## 1. Execute Matrix Integrity\n"
- doc += "Verifies if methods claimed in protocol YAMLs are backed by code.\n\n"
-
- for proto in ['SSH', 'SNMP', 'MOPS']:
- doc += f"### {proto} Operations\n"
- doc += "| Method | In YAML | In Driver Code | In hios.py | Status |\n"
- doc += "| :--- | :---: | :---: | :---: | :--- |\n"
-
- for m, status in results['execute'][proto].items():
- s_drv = "✅" if status['driver'] else "❌ Missing"
- s_adp = "✅" if status['adapter'] else "⚠️ Internal Only"
- final = "🟢 Ready" if status['driver'] and status['adapter'] else "🔴 Broken"
- if status['driver'] and not status['adapter']: final = "🟡 Hidden"
-
- doc += f"| `{m}` | ✅ | {s_drv} | {s_adp} | {final} |\n"
- doc += "\n"
-
- doc += "## 2. Metric Variances\n"
- doc += "| Metric | Design (Spec) | Reality (Code) | Status |\n"
- doc += "| :--- | :--- | :--- | :--- |\n"
- doc += f"| Schema YAMLs | {claims.get('schema_count', '?')} | {len(results['schemas'])} | {'✅' if claims.get('schema_count') == len(results['schemas']) else '⚠️ Update Spec'} |\n"
- doc += f"| Wire YAMLs | 134 | {results['wire_files']} | {'✅' if results['wire_files'] >= 134 else '❌ Low'} |\n"
- doc += f"| Total Attributes | {claims.get('attr_count', '?')} | {results['attrs_total']} | {'✅' if results['attrs_total'] >= 4058 else '❌ Low'} |\n"
-
- doc += "\n## 3. Claimed Primitives Verification\n"
- doc += "| Primitive | Engine Support | Status |\n"
- doc += "| :--- | :---: | :---: |\n"
- for p in sorted(claims['primitives']):
- doc += f"| `{p}` | {'✅' if results['engine'].get(p) else '❌'} | {'✅' if results['engine'].get(p) else '❌'} |\n"
-
- with open(OUTPUT_FILE, 'w') as f: f.write(doc)
- print(f"Integrity Audit complete: {OUTPUT_FILE}")
if __name__ == "__main__":
- audit()
+ main()
diff --git a/local/generator/audit_v26_coverage.py b/local/generator/audit_v26_coverage.py
index 05fee88..da5ba97 100644
--- a/local/generator/audit_v26_coverage.py
+++ b/local/generator/audit_v26_coverage.py
@@ -1,96 +1,54 @@
-import os
-import yaml
-import re
+"""Leftover v26/monolith script. Not live law.
-# Paths
-V1_HIOS_PY = "/home/adamr/obsidian-vault/Projects/napalm-hios/napalm_hios/hios.py"
-ADAPTER_YAML = "/home/adamr/obsidian-vault/Projects/napalm-hios-v2/napalm_hios/adapters/napalm.yaml"
-SCHEMA_DIR = "/home/adamr/obsidian-vault/Projects/napalm-hios-v2/napalm_hios/schemas"
+Paths are repo-relative (no machine-absolute hardcodes). Do not treat
+this as a live generator. Live: generate_docs.py, generate_method_ref.py,
+generate_protocols.py, validate_schemas.py. See local/generator/README.md.
+"""
+from __future__ import annotations
-def get_v1_methods():
- methods = set()
- if not os.path.exists(V1_HIOS_PY): return methods
- with open(V1_HIOS_PY, "r") as f:
- for line in f:
- match = re.search(r"def (get_|set_|create_|delete_|add_|remove_)([a-z0-9_]+)\(", line)
- if match:
- methods.add(match.group(1) + match.group(2))
- return methods
+import argparse
+import sys
+from pathlib import Path
-def audit_coverage():
- print("Starting v2.6 Deep Coverage Audit...")
- v1_methods = get_v1_methods()
-
- with open(ADAPTER_YAML, "r") as f:
- adapter = yaml.safe_load(f)
-
- adapter_methods = adapter.get("methods", {})
-
- # 1. Check for v1 methods missing from Adapter
- missing_in_adapter = v1_methods - set(adapter_methods.keys())
-
- # 2. Check for Adapter methods pointing to broken Schemas
- broken_links = []
- missing_schema_methods = []
- missing_crud_attrs = []
-
- schema_cache = {}
-
- for a_method, mapping in adapter_methods.items():
- s_id = mapping.get("feature")
- s_method = mapping.get("schema")
-
- s_path = os.path.join(SCHEMA_DIR, f"{s_id}.yaml")
- if not os.path.exists(s_path):
- broken_links.append(f"{a_method} -> {s_id}.yaml (Missing File)")
- continue
-
- if s_id not in schema_cache:
- with open(s_path, "r") as f:
- schema_cache[s_id] = yaml.safe_load(f)
-
- schema_data = schema_cache[s_id]
- methods_in_schema = schema_data.get("methods", {})
-
- if s_method not in methods_in_schema:
- missing_schema_methods.append(f"{a_method} -> {s_id}.yaml::{s_method} (Missing Method)")
- continue
-
- # 3. CRUD Validation
- m_type = methods_in_schema[s_method].get("type")
- if m_type in ("create", "delete"):
- # Check if any attribute in this schema has 'access: crud'
- has_crud = False
- for attr, attr_def in schema_data.get("attributes", {}).items():
- if attr_def.get("access") == "crud":
- has_crud = True
- break
- if not has_crud:
- missing_crud_attrs.append(f"{a_method} ({m_type}) in {s_id}.yaml (No CRUD attribute found)")
+_REPO_ROOT = Path(__file__).resolve().parents[2]
+_WIRE = _REPO_ROOT / "crude_engine" / "wire"
+_SCHEMAS = _REPO_ROOT / "crude_engine" / "schemas"
- print(f"\n--- AUDIT REPORT ---")
- print(f"Total v1 Methods: {len(v1_methods)}")
- print(f"Total Adapter Methods: {len(adapter_methods)}")
- print(f"Missing in Adapter: {len(missing_in_adapter)}")
- print(f"Broken Schema Files: {len(broken_links)}")
- print(f"Missing Schema Methods: {len(missing_schema_methods)}")
- print(f"Missing CRUD Attributes: {len(missing_crud_attrs)}")
-
- if missing_in_adapter:
- print("\n[!] v1 Methods not in Adapter:")
- for m in sorted(missing_in_adapter): print(f" - {m}")
-
- if broken_links:
- print("\n[!] Broken Schema Links:")
- for m in broken_links: print(f" - {m}")
- if missing_schema_methods:
- print("\n[!] Missing Schema Methods (Defined in Adapter but not in YAML):")
- for m in missing_schema_methods: print(f" - {m}")
+def _parse_paths(argv: list[str] | None = None):
+ p = argparse.ArgumentParser(description=__doc__)
+ p.add_argument(
+ "--wire-dir",
+ type=Path,
+ default=_WIRE,
+ help="Wire YAML dir (default: repo crude_engine/wire)",
+ )
+ p.add_argument(
+ "--schema-dir",
+ type=Path,
+ default=_SCHEMAS,
+ help="Schema YAML dir (default: repo crude_engine/schemas)",
+ )
+ p.add_argument(
+ "--run-archive",
+ action="store_true",
+ help="Required to actually execute this leftover script",
+ )
+ return p.parse_args(argv)
+
+
+def main(argv: list[str] | None = None) -> None:
+ args = _parse_paths(argv)
+ if not args.run_archive:
+ raise SystemExit(
+ "leftover archive script; pass --run-archive to execute "
+ "(still not live law). Defaults are repo-relative."
+ )
+ raise SystemExit(
+ "archive body not ported to relative paths as a safe mutator; "
+ "use validate_schemas.py / isolated batch_generate_MIB.py instead"
+ )
- if missing_crud_attrs:
- print("\n[!] CRUD Mismatch (Methods need a 'crud' access attribute):")
- for m in missing_crud_attrs: print(f" - {m}")
if __name__ == "__main__":
- audit_coverage()
+ main()
diff --git a/local/generator/audit_v26_integrity.py b/local/generator/audit_v26_integrity.py
index 46f54cc..da5ba97 100644
--- a/local/generator/audit_v26_integrity.py
+++ b/local/generator/audit_v26_integrity.py
@@ -1,93 +1,54 @@
-import os
-import yaml
-import re
+"""Leftover v26/monolith script. Not live law.
-# Paths
-V1_HIOS_PY = "/home/adamr/obsidian-vault/Projects/napalm-hios/napalm_hios/hios.py"
-ADAPTER_YAML = "/home/adamr/obsidian-vault/Projects/napalm-hios-v2/napalm_hios/adapters/napalm.yaml"
-SCHEMA_DIR = "/home/adamr/obsidian-vault/Projects/napalm-hios-v2/napalm_hios/schemas"
-WIRE_DIR = "/home/adamr/obsidian-vault/Projects/napalm-hios-v2/local/reference/webUI"
+Paths are repo-relative (no machine-absolute hardcodes). Do not treat
+this as a live generator. Live: generate_docs.py, generate_method_ref.py,
+generate_protocols.py, validate_schemas.py. See local/generator/README.md.
+"""
+from __future__ import annotations
-def get_v1_methods():
- methods = set()
- if not os.path.exists(V1_HIOS_PY): return methods
- with open(V1_HIOS_PY, "r") as f:
- for line in f:
- match = re.search(r"def (get_|set_|create_|delete_|add_|remove_)([a-z0-9_]+)\(", line)
- if match:
- methods.add(match.group(1) + match.group(2))
- return methods
+import argparse
+import sys
+from pathlib import Path
-def audit_coverage():
- print("Starting v2.6 Deep Coverage & Integrity Audit...")
- v1_methods = get_v1_methods()
-
- with open(ADAPTER_YAML, "r") as f:
- adapter = yaml.safe_load(f)
-
- adapter_methods = adapter.get("methods", {})
- missing_in_adapter = v1_methods - set(adapter_methods.keys())
-
- broken_wire_files = []
- missing_wire_attrs = []
- missing_schema_methods = []
-
- schema_cache = {}
- wire_cache = {}
-
- for a_method, mapping in adapter_methods.items():
- s_id = mapping.get("feature")
- s_method = mapping.get("schema")
-
- s_path = os.path.join(SCHEMA_DIR, f"{s_id}.yaml")
- if not os.path.exists(s_path):
- continue # Already caught by adapter-level checks if needed
-
- if s_id not in schema_cache:
- with open(s_path, "r") as f:
- schema_cache[s_id] = yaml.safe_load(f)
-
- schema_data = schema_cache[s_id]
- methods_in_schema = schema_data.get("methods", {})
-
- if s_method not in methods_in_schema:
- missing_schema_methods.append(f"{a_method} -> {s_id}.yaml::{s_method}")
- continue
+_REPO_ROOT = Path(__file__).resolve().parents[2]
+_WIRE = _REPO_ROOT / "crude_engine" / "wire"
+_SCHEMAS = _REPO_ROOT / "crude_engine" / "schemas"
- # Audit Attribute Resolution (Schema -> Wire)
- # We check ALL attributes in the schema file associated with this feature
- for h_attr, a_map in schema_data.get("attributes", {}).items():
- w_id = a_map.get("source")
- w_attr = a_map.get("wire")
-
- w_path = os.path.join(WIRE_DIR, f"{w_id}.yaml")
- if not os.path.exists(w_path):
- link = f"{s_id}.yaml -> {w_id}.yaml (Missing Wire File)"
- if link not in broken_wire_files: broken_wire_files.append(link)
- continue
-
- if w_id not in wire_cache:
- with open(w_path, "r") as f:
- wire_cache[w_id] = yaml.safe_load(f)
-
- wire_data = wire_cache[w_id]
- if w_attr not in wire_data.get("attributes", {}):
- missing_wire_attrs.append(f"{s_id}.yaml::{h_attr} -> {w_id}.yaml::{w_attr} (Missing Attribute)")
- print(f"\n--- INTEGRITY REPORT ---")
- print(f"Missing in Adapter: {len(missing_in_adapter)}")
- print(f"Missing Schema Methods: {len(missing_schema_methods)}")
- print(f"Broken Wire File Links: {len(broken_wire_files)}")
- print(f"Missing Wire Attributes: {len(missing_wire_attrs)}")
-
- if broken_wire_files:
- print("\n[!] Broken Wire File Links:")
- for m in broken_wire_files: print(f" - {m}")
+def _parse_paths(argv: list[str] | None = None):
+ p = argparse.ArgumentParser(description=__doc__)
+ p.add_argument(
+ "--wire-dir",
+ type=Path,
+ default=_WIRE,
+ help="Wire YAML dir (default: repo crude_engine/wire)",
+ )
+ p.add_argument(
+ "--schema-dir",
+ type=Path,
+ default=_SCHEMAS,
+ help="Schema YAML dir (default: repo crude_engine/schemas)",
+ )
+ p.add_argument(
+ "--run-archive",
+ action="store_true",
+ help="Required to actually execute this leftover script",
+ )
+ return p.parse_args(argv)
+
+
+def main(argv: list[str] | None = None) -> None:
+ args = _parse_paths(argv)
+ if not args.run_archive:
+ raise SystemExit(
+ "leftover archive script; pass --run-archive to execute "
+ "(still not live law). Defaults are repo-relative."
+ )
+ raise SystemExit(
+ "archive body not ported to relative paths as a safe mutator; "
+ "use validate_schemas.py / isolated batch_generate_MIB.py instead"
+ )
- if missing_wire_attrs:
- print("\n[!] Missing Wire Attributes (Defined in Schema but missing in Wire YAML):")
- # Deduplicate and sort
- for m in sorted(list(set(missing_wire_attrs))): print(f" - {m}")
if __name__ == "__main__":
- audit_coverage()
+ main()
diff --git a/local/generator/audit_web_coverage.py b/local/generator/audit_web_coverage.py
index cfe10c8..da5ba97 100644
--- a/local/generator/audit_web_coverage.py
+++ b/local/generator/audit_web_coverage.py
@@ -1,147 +1,54 @@
-import os
-import yaml
-import xml.etree.ElementTree as ET
-from collections import defaultdict
+"""Leftover v26/monolith script. Not live law.
+
+Paths are repo-relative (no machine-absolute hardcodes). Do not treat
+this as a live generator. Live: generate_docs.py, generate_method_ref.py,
+generate_protocols.py, validate_schemas.py. See local/generator/README.md.
+"""
+from __future__ import annotations
+
+import argparse
+import sys
+from pathlib import Path
+
+_REPO_ROOT = Path(__file__).resolve().parents[2]
+_WIRE = _REPO_ROOT / "crude_engine" / "wire"
+_SCHEMAS = _REPO_ROOT / "crude_engine" / "schemas"
+
+
+def _parse_paths(argv: list[str] | None = None):
+ p = argparse.ArgumentParser(description=__doc__)
+ p.add_argument(
+ "--wire-dir",
+ type=Path,
+ default=_WIRE,
+ help="Wire YAML dir (default: repo crude_engine/wire)",
+ )
+ p.add_argument(
+ "--schema-dir",
+ type=Path,
+ default=_SCHEMAS,
+ help="Schema YAML dir (default: repo crude_engine/schemas)",
+ )
+ p.add_argument(
+ "--run-archive",
+ action="store_true",
+ help="Required to actually execute this leftover script",
+ )
+ return p.parse_args(argv)
+
+
+def main(argv: list[str] | None = None) -> None:
+ args = _parse_paths(argv)
+ if not args.run_archive:
+ raise SystemExit(
+ "leftover archive script; pass --run-archive to execute "
+ "(still not live law). Defaults are repo-relative."
+ )
+ raise SystemExit(
+ "archive body not ported to relative paths as a safe mutator; "
+ "use validate_schemas.py / isolated batch_generate_MIB.py instead"
+ )
-# Paths
-BASE_DIR = '/home/adamr/obsidian-vault/Projects/napalm-hios-v2'
-LOCAL_UI_DIR = '/home/adamr/obsidian-vault/Projects/LocalUI'
-SCHEMAS_DIR = os.path.join(BASE_DIR, 'napalm_hios/schemas')
-WIRE_DIR = os.path.join(BASE_DIR, 'napalm_hios/wire')
-CAPTURED_DIR = os.path.join(LOCAL_UI_DIR, 'captured')
-OUTPUT_FILE = os.path.join(BASE_DIR, 'local/reference/WEB_VS_DRIVER.md')
-
-def load_yaml(path):
- try:
- with open(path, 'r') as f:
- return yaml.safe_load(f) or {}
- except: return {}
-
-def normalize_mib(mib):
- return mib.replace('_', '-').lower()
-
-def get_exposed_map():
- """Build map: (mib, table, field) -> [schema_feature.attr]"""
- exposed = {}
- wire_db = {}
- for wf in os.listdir(WIRE_DIR):
- if wf.endswith('.yaml'):
- wire_db[wf.replace('.yaml', '')] = load_yaml(os.path.join(WIRE_DIR, wf))
-
- schema_files = [f for f in os.listdir(SCHEMAS_DIR) if f.endswith('.yaml')]
- for sf in schema_files:
- feature = sf.replace('.yaml', '')
- data = load_yaml(os.path.join(SCHEMAS_DIR, sf))
- for attr_name, attr_def in data.get('attributes', {}).items():
- source = attr_def.get('source', feature)
- wire = attr_def.get('wire', attr_name)
-
- w_data = wire_db.get(source, {})
- w_attr = w_data.get('attributes', {}).get(wire, {})
- mops = w_attr.get('sources', {}).get('mops', {}).get('read', {})
-
- if mops and 'mib' in mops and 'table' in mops and 'field' in mops:
- key = (normalize_mib(mops['mib']), mops['table'].lower(), mops['field'].lower())
- if key not in exposed: exposed[key] = []
- exposed[key].append(f"{feature}.{attr_name}")
- return exposed
-
-def parse_mops_xml(file_path):
- """Extract all (mib, table, field) tuples from a MOPS XML."""
- found = set()
- try:
- tree = ET.parse(file_path)
- root = tree.getroot()
- ns = {'m': 'urn:x-mops:1.0'}
- for mib in root.findall('.//m:MIB', ns):
- mib_name = mib.get('name')
- for node in mib.findall('./m:Node', ns):
- table_name = node.get('name')
- for attr in node.findall('.//m:Attribute', ns):
- field_name = attr.get('name')
- if mib_name and table_name and field_name:
- found.add((normalize_mib(mib_name), table_name.lower(), field_name.lower()))
- except: pass
- return found
-
-def audit():
- print("Building exposure map from Schemas...")
- exposed_map = get_exposed_map()
-
- print("Scanning WebUI captures with file-tracing...")
- page_stats = {}
-
- for page_folder in sorted(os.listdir(CAPTURED_DIR)):
- page_path = os.path.join(CAPTURED_DIR, page_folder)
- if not os.path.isdir(page_path): continue
-
- all_attrs_on_page = set()
- attr_to_files = defaultdict(set)
-
- for xml_file in os.listdir(page_path):
- if xml_file.endswith('.xml'):
- attrs = parse_mops_xml(os.path.join(page_path, xml_file))
- for a in attrs:
- all_attrs_on_page.add(a)
- attr_to_files[a].add(xml_file)
-
- if not all_attrs_on_page: continue
-
- covered_attrs = [a for a in all_attrs_on_page if a in exposed_map]
- missing_attrs = [a for a in all_attrs_on_page if a not in exposed_map]
-
- page_stats[page_folder] = {
- 'total': len(all_attrs_on_page),
- 'covered': len(covered_attrs),
- 'percent': (len(covered_attrs) / len(all_attrs_on_page)) * 100,
- 'attr_to_files': attr_to_files,
- 'missing': missing_attrs
- }
-
- doc = "# WebUI vs Driver Deep Trace Audit\n\n"
- doc += "This audit traces every attribute found in **WebUI XML Captures** back to the driver **Schemas**.\n"
- doc += "Includes file-tracing to show exactly which XML file contains the unexposed data.\n\n"
-
- doc += "## Summary Scorecard\n"
- doc += "| | WebUI Page | Coverage | Exposed / Total |\n"
- doc += "| :--- | :--- | :--- | :--- |\n"
-
- for page, stats in sorted(page_stats.items(), key=lambda x: (x[1]['percent'], x[0]), reverse=True):
- status = "✅" if stats['percent'] == 100 else ("⚠️" if stats['percent'] > 0 else "❌")
- doc += f"| {status} | `{page}` | {stats['percent']:.0f}% | {stats['covered']}/{stats['total']} |\n"
-
- doc += "\n\n---\n\n## Unexposed Data Breakdown (with Source Files)\n"
- doc += "Ranked by attribute count. Shows exactly which captured XMLs to inspect for new schema attributes.\n\n"
-
- unexposed = [p for p in page_stats.items() if p[1]['percent'] < 100]
- unexposed.sort(key=lambda x: x[1]['total'] - x[1]['covered'], reverse=True)
-
- for page, stats in unexposed[:30]:
- missing = stats['missing']
- if not missing: continue
-
- doc += f"### {page} ({len(missing)} missing / {stats['total']} total)\n"
-
- # Group by MIB for readability
- mibs = defaultdict(list)
- for attr in missing:
- mib, table, field = attr
- files = ", ".join(sorted(list(stats['attr_to_files'][attr])))
- mibs[mib].append(f"`{table}.{field}` (in {files})")
-
- for mib, entries in list(mibs.items())[:8]:
- doc += f"- **{mib.upper()}**\n"
- for entry in entries[:10]:
- doc += f" - {entry}\n"
- if len(entries) > 10:
- doc += f" - ... and {len(entries)-10} more fields\n"
- doc += "\n"
-
- os.makedirs(os.path.dirname(OUTPUT_FILE), exist_ok=True)
- with open(OUTPUT_FILE, 'w') as f:
- f.write(doc)
-
- print(f"Deep Trace Audit with file tracing complete: {OUTPUT_FILE}")
if __name__ == "__main__":
- audit()
+ main()
diff --git a/local/generator/audit_wire.py b/local/generator/audit_wire.py
index 2c91dbe..07cdc0e 100644
--- a/local/generator/audit_wire.py
+++ b/local/generator/audit_wire.py
@@ -1,11 +1,18 @@
+"""Read-only wire integrity audit (protocol coverage, duplicate names).
+
+Retargeted from leftover napalm-hios-v2 / napalm_hios/wire to this
+repo's crude_engine/wire. Does not mutate YAML. Not a live doc generator
+(those are generate_docs.py / generate_method_ref.py / generate_protocols.py).
+"""
import os
import yaml
from collections import defaultdict
-# Paths
-BASE_DIR = '/home/adamr/obsidian-vault/Projects/napalm-hios-v2'
-WIRE_DIR = os.path.join(BASE_DIR, 'napalm_hios/wire')
-OUTPUT_FILE = os.path.join(BASE_DIR, 'docs/WIRE_INTEGRITY.md')
+# Paths — this repo's crude_engine/ (not napalm-hios-v2)
+BASE_DIR = os.path.dirname(os.path.abspath(__file__))
+PACKAGE_DIR = os.path.join(BASE_DIR, '../../crude_engine')
+WIRE_DIR = os.path.join(PACKAGE_DIR, 'wire')
+OUTPUT_FILE = os.path.join(BASE_DIR, '../../docs/WIRE_INTEGRITY.md')
def load_yaml(path):
try:
diff --git a/local/generator/batch_generate_MIB.py b/local/generator/batch_generate_MIB.py
index fbf6a53..4e679c9 100644
--- a/local/generator/batch_generate_MIB.py
+++ b/local/generator/batch_generate_MIB.py
@@ -1,17 +1,38 @@
-# Version: 2.6.1 - Deep MIB Resolution (restored & perfected)
+# Leftover v26/monolith one-shot. Not live law.
+# Kept (not deleted) as archive. Do not write crude_engine/wire.
+# Isolated temp emit only: python batch_generate_MIB.py --isolated --outdir /tmp/...
+# Live generators: generate_docs.py, generate_method_ref.py, generate_protocols.py.
+# Live schema check: validate_schemas.py. See local/generator/README.md.
+#
+# Version: 2.6.8 - named-TC teach (#162): Timeout→string (defaults via get_default_for_type).
+# Prior 2.6.7: LacpKey→string. Prior 2.6.6: VlanId→string. Prior 2.6.5: AreaID→string.
+# Prior 2.6.4: RouterID→string. Prior 2.6.3: InetAddressType/Version/PrefixLength→string;
+# TC-BITS→string drop bit_map; INTEGER{enabled,disabled}→boolean.
+# Explicitly NOT Metric/DesignatedRouterPriority/BigMetric. Exact s=="Timeout" only.
+# Keep "Timeout" in integer-any list as defensive substring catch so
+# Hm2AgentSwitchAddressAgingTimeoutEntry stays integer (product Entry/Table — do not teach).
+# No blanket integer→string. No LacpKey/VlanId/AreaID/RouterID re-teach.
import os
+import argparse
import xml.etree.ElementTree as ET
import json
import re
import yaml
-# Updated Paths to use /local/reference/
-BASE_DIR = '/home/adamr/obsidian-vault/Projects/napalm-hios-v2'
-captured_dir = os.path.join(BASE_DIR, 'local/reference/captured')
-xml_schema_path = os.path.join(BASE_DIR, 'local/reference/MOPS/mops_hios.xml')
-master_schema_path = os.path.join(BASE_DIR, 'docs/napalm-hios-2-6-schema.md')
-output_dir = os.path.join(BASE_DIR, 'local/reference/webUI')
-overrides_path = os.path.join(BASE_DIR, 'local/generator/overrides.yaml')
+# Repo-relative only (no machine-absolute paths). Isolated --outdir required to write.
+_REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
+BASE_DIR = _REPO_ROOT
+captured_dir = os.path.join(_REPO_ROOT, 'local/reference/captured')
+xml_schema_path = os.path.join(_REPO_ROOT, 'local/reference/MOPS/mops_hios.xml')
+master_schema_path = os.path.join(_REPO_ROOT, 'docs/napalm-hios-2-6-schema.md')
+# Default output stays off live wire; --isolated --outdir overrides.
+output_dir = os.path.join(_REPO_ROOT, 'local/reference/webUI')
+overrides_path = os.path.join(_REPO_ROOT, 'local/generator/overrides.yaml')
+
+def _refuse_live_wire(path):
+ ap = os.path.abspath(path).replace("\\", "/")
+ if "crude_engine/wire" in ap:
+ raise SystemExit("leftover batch_generate_MIB refuses to write live crude_engine/wire")
def load_overrides():
if not os.path.exists(overrides_path): return {}
@@ -57,11 +78,45 @@ def parse_constraints(raw, syntax, mib_range=None):
elif "TruthValue" in s or "HmEnabledStatus" in s: v = {"allowed": [True, False]}
return v
-def syntax_to_type(syntax):
+def syntax_to_type(syntax, enumerations=None, tc_info=None):
s = str(syntax).strip()
+ # Keep TruthValue / HmEnabledStatus / EnabledStatus → boolean (#103 / archive).
+ # Do NOT map TruthValue→integer (falsified teach created 418 new diffs).
if any(x in s for x in ("TruthValue", "HmEnabledStatus", "EnabledStatus")): return "boolean"
- if s == "INTEGER" or any(x in s for x in ("Counter", "Gauge", "Integer", "Unsigned", "RowStatus", "Index", "Percent", "TimeTicks", "Number", "StorageType", "TimeStamp", "TimeInterval", "TimeFilter", "InetAddressPrefixLength", "InetAddressType", "InetPortNumber", "InetVersion", "Timeout", "Metric", "VlanId", "RouterID", "AreaID", "LacpKey", "DesignatedRouterPriority")): return "integer"
+ # INTEGER{enabled(1),disabled(2)} — same semantics as HmEnabledStatus, inline enum
+ if enumerations:
+ pairs = {(e.get("name"), str(e.get("value"))) for e in enumerations}
+ if pairs == {("enabled", "1"), ("disabled", "2")}:
+ return "boolean"
+ # Named Inet TCs only → string (proved vs live). Do not blanket every "Inet*"
+ # (InetZoneIndex overshot). InetPortNumber stays integer below.
+ if any(x in s for x in ("InetAddressType", "InetVersion", "InetAddressPrefixLength")):
+ return "string"
+ # Named RouterID TC → string (already on main #197). Do not re-teach here.
+ if "RouterID" in s:
+ return "string"
+ # Named AreaID TC → string (already on main #199). Do not re-teach here.
+ if "AreaID" in s:
+ return "string"
+ # Named VlanId TC → string (already on main #201). Do not re-teach here.
+ if "VlanId" in s:
+ return "string"
+ # Named LacpKey TC → string (already on main #203). Do not re-teach here.
+ if "LacpKey" in s:
+ return "string"
+ # Named Timeout TC → string (proved vs live #162). Exact match only.
+ # Keep "Timeout" in integer-any below so Hm2AgentSwitchAddressAgingTimeoutEntry
+ # (product Entry/Table) still matches integer via substring — do not teach it.
+ # Explicitly NOT Metric / DesignatedRouterPriority / BigMetric.
+ if s == "Timeout":
+ return "string"
+ if s == "INTEGER" or any(x in s for x in ("Counter", "Gauge", "Integer", "Unsigned", "RowStatus", "Index", "Percent", "TimeTicks", "Number", "StorageType", "TimeStamp", "TimeInterval", "TimeFilter", "InetPortNumber", "Timeout", "Metric", "DesignatedRouterPriority", "SFlowReceiver")): return "integer"
+ # Literal BITS / PortList stay list + bit_map (live already keeps those).
if any(x in s for x in ("BITS", "PortList")): return "list"
+ # TC-BITS (textual-convention whose base syntax is BITS) → string; caller drops
+ # inline bit_map for these. Leave literal BITS path above as list.
+ if tc_info and tc_info.get("syntax") == "BITS":
+ return "string"
# Hm2* BITS types have bit_map in their MIB definition — handled by bit_map detection
# Don't blanket-classify all Hm2* as list — many are integer enums
return "string"
@@ -134,9 +189,47 @@ def build_lookup_tables(root):
if idx_detail:
index_meta[entry_name] = idx_detail
- return obj_by_name, node_by_name, table_for_entry, obj_to_mib, index_fields, index_meta
+ # AUGMENTS entries have no INDEX child — inherit from the augmented Entry
+ # (ifXEntry augments ifEntry, dot1qPortVlanEntry augments dot1dBasePortEntry, …)
+ for obj in root.findall('.//ObjectType'):
+ aug = obj.get('augments')
+ if not aug:
+ continue
+ entry_name = obj.get('name', '')
+ if not entry_name:
+ continue
+ seen = set()
+ cur = aug
+ while cur and cur not in seen:
+ seen.add(cur)
+ if cur in index_fields:
+ index_fields[entry_name] = list(index_fields[cur])
+ if cur in index_meta:
+ index_meta[entry_name] = list(index_meta[cur])
+ break
+ cur_obj = obj_by_name.get(cur)
+ cur = cur_obj.get('augments') if cur_obj is not None else None
+
+ tc_by_name = {}
+ for tc in root.findall('.//TextualConvention'):
+ name = tc.get('name')
+ syn = tc.find('Syntax')
+ if not name or syn is None:
+ continue
+ info = {"syntax": syn.get("name", ""), "bit_map": {}}
+ for enum in syn.findall('Enumeration'):
+ try:
+ info["bit_map"][int(enum.get("value"))] = enum.get("name")
+ except (TypeError, ValueError):
+ pass
+ if not info["bit_map"]:
+ info.pop("bit_map")
+ tc_by_name[name] = info
-def resolve_meta(target_name, master_db, obj_by_name, node_by_name, table_for_entry, obj_to_mib, index_fields, index_meta, root):
+ return obj_by_name, node_by_name, table_for_entry, obj_to_mib, index_fields, index_meta, tc_by_name
+
+def resolve_meta(target_name, master_db, obj_by_name, node_by_name, table_for_entry, obj_to_mib, index_fields, index_meta, root, tc_by_name=None):
+ tc_by_name = tc_by_name or {}
found_meta = {"mib": "Unknown", "table": "Unknown", "oid": "N/A", "syntax": "Unknown", "access": "r", "constraints": "", "is_table": False, "index_field": ""}
target_obj = obj_by_name.get(target_name)
@@ -232,6 +325,9 @@ def resolve_meta(target_name, master_db, obj_by_name, node_by_name, table_for_en
found_meta["index_type"] = "inet_address"
elif any_implied or any(s in ('SnmpAdminString', 'OCTET STRING', 'SnmpEngineID') for s in syntaxes):
found_meta["index_type"] = "implied_string"
+ elif len(idx_detail) > 1:
+ # multi-field INDEX with mixed types (ip_source_guard 4-part, …)
+ found_meta["index_type"] = "composite"
lookup_keys = []
if found_meta["mib"] != "Unknown":
@@ -251,6 +347,13 @@ def resolve_meta(target_name, master_db, obj_by_name, node_by_name, table_for_en
if found_meta["mib"] == "Unknown": found_meta["mib"] = key.split("::")[0]
break
+ if target_obj is not None:
+ syntax_node = target_obj.find('Syntax')
+ if syntax_node is not None:
+ enums = list(syntax_node.findall('Enumeration'))
+ if enums:
+ found_meta["enumerations"] = enums
+
if found_meta["syntax"] == "BITS" and target_obj is not None:
bit_map = {}
syntax_node = target_obj.find('Syntax')
@@ -258,15 +361,23 @@ def resolve_meta(target_name, master_db, obj_by_name, node_by_name, table_for_en
for enum in syntax_node.findall('Enumeration'):
bit_map[int(enum.get('value'))] = enum.get('name')
if bit_map: found_meta["bit_map"] = bit_map
+ else:
+ tc = tc_by_name.get(found_meta["syntax"])
+ if tc and tc.get("syntax") == "BITS" and tc.get("bit_map"):
+ found_meta["bit_map"] = tc["bit_map"]
+ found_meta["tc"] = tc
+ elif tc:
+ found_meta["tc"] = tc
return found_meta
def process_captured_pages():
+ _refuse_live_wire(output_dir)
if not os.path.exists(output_dir): os.makedirs(output_dir)
tree = ET.parse(xml_schema_path); root = tree.getroot()
master_db = load_master_meta()
overrides = load_overrides()
- obj_by_name, node_by_name, table_for_entry, obj_to_mib, index_fields, index_meta = build_lookup_tables(root)
+ obj_by_name, node_by_name, table_for_entry, obj_to_mib, index_fields, index_meta, tc_by_name = build_lookup_tables(root)
mib_features = {}
for name, obj in obj_by_name.items():
@@ -282,10 +393,10 @@ def process_captured_pages():
feature_data = {"version": "2.6.0", "feature": mib_id,
"schemas": {f"read_{mib_id}": {"type": "dict", "defaults": {}}}, "attributes": {}}
for attr_name in sorted(attrs_found.keys()):
- meta = resolve_meta(attr_name, master_db, obj_by_name, node_by_name, table_for_entry, obj_to_mib, index_fields, index_meta, root)
+ meta = resolve_meta(attr_name, master_db, obj_by_name, node_by_name, table_for_entry, obj_to_mib, index_fields, index_meta, root, tc_by_name)
if meta:
access = meta['access'].strip(); syntax = meta['syntax'].strip()
- stype = syntax_to_type(syntax)
+ stype = syntax_to_type(syntax, enumerations=meta.get("enumerations"), tc_info=meta.get("tc"))
validation = parse_constraints(meta['constraints'], syntax, meta.get('mib_range'))
clean_name = attr_name.lower()
feature_data["schemas"][f"read_{mib_id}"]["defaults"][clean_name] = get_default_for_type(stype)
@@ -299,9 +410,20 @@ def process_captured_pages():
mops_read["key_tag"] = "to_hex_decode"
attr_entry = {"syntax": syntax, "type": stype, "access": access, "sources": {"snmp": {"read": snmp_read}, "mops": {"read": mops_read}}}
if validation: attr_entry["validation"] = validation
+ # bit_map: keep for literal BITS / PortList (list). Drop for TC-BITS
+ # where live wants string without inline map (#162 named teach).
if "bit_map" in meta:
- attr_entry["bit_map"] = meta["bit_map"]
- attr_entry["type"] = "list" # BITS fields → list output
+ tc = meta.get("tc") or {}
+ # TC-BITS: syntax_to_type already returned string; skip inline map.
+ if (
+ stype == "string"
+ and tc.get("syntax") == "BITS"
+ and str(meta.get("syntax", "")).strip() != "BITS"
+ ):
+ pass
+ else:
+ attr_entry["bit_map"] = meta["bit_map"]
+ attr_entry["type"] = "list" # literal BITS / PortList → list
if "create_method" in meta: attr_entry["create_method"] = meta["create_method"]
if "index_type" in meta: attr_entry["index_type"] = meta["index_type"]
# Apply overrides from overrides.yaml
@@ -309,6 +431,13 @@ def process_captured_pages():
if cm_override: attr_entry["create_method"] = cm_override
type_override = overrides.get("type", {}).get(clean_name)
if type_override: attr_entry["type"] = type_override
+ src_ov = (overrides.get("sources") or {}).get(clean_name) or {}
+ if src_ov.get("oid"):
+ attr_entry["sources"]["snmp"]["read"]["oid"] = src_ov["oid"]
+ if src_ov.get("table"):
+ attr_entry["sources"]["mops"]["read"]["table"] = src_ov["table"]
+ if src_ov.get("field"):
+ attr_entry["sources"]["mops"]["read"]["field"] = src_ov["field"]
feature_data["attributes"][clean_name] = attr_entry
with open(os.path.join(output_dir, f"{mib_id}.yaml"), 'w') as f:
yaml.dump(feature_data, f, sort_keys=False, default_flow_style=False)
@@ -342,5 +471,28 @@ def process_captured_pages():
print(f"Generated {count} + 1 context-sources v2.6 Wire YAMLs using MIB-based naming.")
-if __name__ == "__main__":
+def _cli(argv=None):
+ global xml_schema_path, overrides_path, master_schema_path, output_dir
+ parser = argparse.ArgumentParser(
+ description="Leftover MIB→wire generator. Not live law. Temp outdir only."
+ )
+ parser.add_argument("--isolated", action="store_true",
+ help="Required. Leftover is dead as live law; isolated temp emit only.")
+ parser.add_argument("--outdir", required=True,
+ help="TEMP directory for YAML emit. Never crude_engine/wire.")
+ parser.add_argument("--xml", default=xml_schema_path,
+ help="mops_hios.xml path (in-tree local/reference/MOPS/)")
+ parser.add_argument("--overrides", default=overrides_path)
+ parser.add_argument("--master", default=master_schema_path)
+ args = parser.parse_args(argv)
+ if not args.isolated:
+ raise SystemExit("leftover batch_generate_MIB is not live law; pass --isolated --outdir /tmp/...")
+ xml_schema_path = args.xml
+ overrides_path = args.overrides
+ master_schema_path = args.master
+ output_dir = args.outdir
+ _refuse_live_wire(output_dir)
process_captured_pages()
+
+if __name__ == "__main__":
+ _cli()
diff --git a/local/generator/batch_generate_webui.py.stable b/local/generator/batch_generate_webui.py.stable
index cf56044..ca14bc0 100644
--- a/local/generator/batch_generate_webui.py.stable
+++ b/local/generator/batch_generate_webui.py.stable
@@ -1,252 +1,8 @@
-import os
-import xml.etree.ElementTree as ET
-import json
-import re
-import yaml
-
-captured_dir = '/home/adamr/obsidian-vault/Projects/LocalUI/captured'
-xml_schema_path = '/home/adamr/obsidian-vault/Projects/MOPS_Emulator/data/mops_schema.xml'
-master_schema_path = '/home/adamr/obsidian-vault/Projects/napalm-hios-v2/docs/napalm-hios-2-6-schema.md'
-output_dir = '/home/adamr/obsidian-vault/Projects/napalm-hios-v2/local/reference/webUI'
-
-def load_master_meta():
- meta_db = {}
- if not os.path.exists(master_schema_path): return {}
- with open(master_schema_path, "r") as f:
- lines = f.readlines()
- for line in lines:
- if "|" in line and "::" in line:
- parts = [p.strip() for p in line.split("|") if p.strip()]
- if len(parts) >= 5:
- full_field = parts[1].lower()
- meta = {
- "syntax": parts[3],
- "access": parts[4],
- "constraints": parts[5] if len(parts) > 5 else "",
- "oid": parts[2]
- }
- meta_db[full_field] = meta
- return meta_db
-
-def parse_constraints(raw, syntax):
- v = {}
- if raw:
- rm = re.search(r"Range: (\d+)\.\.(\d+)", raw)
- if rm: v["min"] = int(rm.group(1)); v["max"] = int(rm.group(2))
- em = re.search(r"Enums: \[(.*)\]", raw)
- if em:
- pairs = em.group(1).split(", "); v["allowed"] = [p.split(":")[1] for p in pairs if ":" in p]
- if not v:
- s = str(syntax)
- if "Unsigned32" in s: v = {"min": 0, "max": 4294967295}
- elif "Integer32" in s or "INTEGER" in s: v = {"min": -2147483648, "max": 2147483647}
- elif "VlanIndex" in s: v = {"min": 1, "max": 4094}
- elif "TruthValue" in s or "HmEnabledStatus" in s: v = {"allowed": [True, False]}
- return v
-
-def syntax_to_type(syntax):
- s = str(syntax).strip()
- if any(x in s for x in ("TruthValue", "HmEnabledStatus", "EnabledStatus")): return "boolean"
- if any(x in s for x in ("Counter", "Gauge", "Integer", "Unsigned", "RowStatus", "Index", "Percent", "TimeTicks", "Number")): return "integer"
- if any(x in s for x in ("BITS", "PortList")) or s.startswith("Hm2"): return "list"
- return "string"
-
-def get_default_for_type(stype):
- if stype == "integer": return 0
- if stype == "boolean": return False
- if stype == "list": return []
- return ""
-
-def build_lookup_tables(root):
- obj_by_name = {}
- for obj in root.findall('.//ObjectType'):
- name = obj.get('name')
- if name: obj_by_name[name] = obj
-
- node_by_name = {}
- for node in root.findall('.//*'):
- if node.tag in ('ObjectIdentifier', 'ObjectType', 'ModuleIdentity'):
- name = node.get('name')
- if name:
- node_by_name[name] = {
- 'oid': node.get('OID', ''),
- 'parent': node.get('Parent', ''),
- 'tag': node.tag,
- 'node': node,
- }
-
- table_for_entry = {}
- for obj in root.findall('.//ObjectType'):
- syntax = obj.find('Syntax')
- if syntax is not None:
- entry_name = syntax.get('name', '')
- if entry_name:
- table_for_entry[entry_name] = {
- 'oid': obj.get('OID', ''),
- 'parent': obj.get('Parent', ''),
- 'name': obj.get('name', ''),
- }
-
- obj_to_mib = {}
- for mib in root.findall('MIB'):
- for definition in mib.findall('Definition'):
- def_name = definition.get('name', '')
- for child in definition:
- child_name = child.get('name', '')
- if child_name: obj_to_mib[child_name] = def_name
-
- index_fields = {}
- for obj in root.findall('.//ObjectType'):
- idx_node = obj.find('Index')
- if idx_node is not None:
- entry_name = obj.get('name', '')
- idx_names = [i.get('name', '') for i in idx_node.findall('Value')]
- if idx_names: index_fields[entry_name] = idx_names
-
- return obj_by_name, node_by_name, table_for_entry, obj_to_mib, index_fields
-
-def resolve_meta(target_name, master_db, obj_by_name, node_by_name, table_for_entry, obj_to_mib, index_fields, root):
- found_meta = {"mib": "Unknown", "table": "Unknown", "oid": "N/A", "syntax": "Unknown", "access": "r", "constraints": "", "is_table": False, "index_field": ""}
-
- target_obj = obj_by_name.get(target_name)
- if target_obj is not None:
- found_meta["mib"] = obj_to_mib.get(target_name, "Unknown")
- parent_name = target_obj.get('Parent', '')
- found_meta["table"] = parent_name
-
- if parent_name and parent_name.endswith('Entry'):
- found_meta["is_table"] = True
- idx_cols = index_fields.get(parent_name, [])
- if idx_cols: found_meta["index_field"] = idx_cols[0]
- elif parent_name and parent_name in node_by_name:
- pnode = node_by_name[parent_name]
- if pnode.get('tag') == 'ObjectType' and pnode['node'].find('Index') is not None:
- found_meta["is_table"] = True
- idx_cols = index_fields.get(parent_name, [])
- if idx_cols: found_meta["index_field"] = idx_cols[0]
-
- oid_parts = []
- curr_oid = target_obj.get('OID', '')
- p = parent_name
-
- if curr_oid.startswith("1.3.6.1"):
- full_oid = curr_oid
- else:
- oid_parts.append(curr_oid)
- seen = {target_name}
- while p and p not in seen:
- seen.add(p)
- info = node_by_name.get(p)
- if not info:
- info = table_for_entry.get(p)
-
- if info:
- p_oid = info['oid']
- if p_oid.startswith("1.3.6.1"):
- oid_parts.insert(0, p_oid)
- p = None
- else:
- oid_parts.insert(0, p_oid)
- p = info['parent']
- else: break
-
- full_oid = ".".join(part for part in oid_parts if part)
-
- # SMART PREFIXING:
- if full_oid.startswith("1.3.6.1"):
- pass
- elif full_oid.startswith("3.6.1.4.1.248.11"):
- full_oid = "1." + full_oid
- elif full_oid.startswith("3.6.1.2.1"):
- full_oid = "1." + full_oid
- else:
- if found_meta["mib"].startswith("HM2"): full_oid = "1.3.6.1.4.1.248.11." + full_oid
- else: full_oid = "1.3.6.1.2.1." + full_oid
-
- found_meta["oid"] = full_oid
- syntax_node = target_obj.find('Syntax')
- found_meta["syntax"] = syntax_node.get('name') if syntax_node is not None else "Unknown"
- mib_access = target_obj.get('access', 'read-only').lower().replace(' ', '-')
- if "read-create" in mib_access or "read-write" in mib_access: found_meta["access"] = "ru"
- if found_meta["syntax"] == "RowStatus": found_meta["access"] = "crud"
-
- lookup_keys = []
- if found_meta["mib"] != "Unknown":
- lookup_keys.append(f"{found_meta['mib']}::{target_name}".lower())
- for k in master_db:
- if k.split("::")[-1] == target_name.lower():
- lookup_keys.append(k)
- break
-
- for key in lookup_keys:
- if key in master_db:
- meta = master_db[key]
- found_meta["syntax"] = meta["syntax"]
- found_meta["access"] = meta["access"]
- found_meta["constraints"] = meta["constraints"]
- if found_meta["oid"] == "N/A": found_meta["oid"] = meta["oid"]
- if found_meta["mib"] == "Unknown": found_meta["mib"] = key.split("::")[0]
- break
-
- if found_meta["syntax"] == "BITS" and target_obj is not None:
- bit_map = {}
- syntax_node = target_obj.find('Syntax')
- if syntax_node is not None:
- for enum in syntax_node.findall('Enumeration'):
- bit_map[int(enum.get('value'))] = enum.get('name')
- if bit_map: found_meta["bit_map"] = bit_map
-
- return found_meta
-
-def process_captured_pages():
- if not os.path.exists(output_dir): os.makedirs(output_dir)
- tree = ET.parse(xml_schema_path); root = tree.getroot()
- master_db = load_master_meta()
- obj_by_name, node_by_name, table_for_entry, obj_to_mib, index_fields = build_lookup_tables(root)
-
- mib_features = {}
- for name, obj in obj_by_name.items():
- mib = obj_to_mib.get(name, "Unknown")
- feature = mib.replace("HM2-", "").replace("-MIB", "").lower()
- if feature not in mib_features: mib_features[feature] = []
- mib_features[feature].append(name)
-
- count = 0
- all_features = set(os.listdir(captured_dir)) | set(mib_features.keys())
- for page_name in sorted(all_features):
- page_path = os.path.join(captured_dir, page_name)
- attrs_found = {}
- if os.path.isdir(page_path):
- for msg in os.listdir(page_path):
- if not msg.endswith('.xml'): continue
- try:
- m_tree = ET.parse(os.path.join(page_path, msg)); m_root = m_tree.getroot()
- for attr in m_root.findall('.//{urn:x-mops:1.0}Attribute'):
- if attr.get('name'): attrs_found[attr.get('name')] = True
- except: continue
- if page_name in mib_features:
- for attr in mib_features[page_name]: attrs_found[attr] = True
- if not attrs_found: continue
-
- feature_data = {"version": "2.6.0", "feature": page_name, "schemas": {f"read_{page_name}": {"type": "dict", "defaults": {}}}, "attributes": {}}
- for attr_name in sorted(attrs_found.keys()):
- meta = resolve_meta(attr_name, master_db, obj_by_name, node_by_name, table_for_entry, obj_to_mib, index_fields, root)
- if meta:
- access = meta['access'].strip(); syntax = meta['syntax'].strip()
- stype = syntax_to_type(syntax); validation = parse_constraints(meta['constraints'], syntax)
- clean_name = attr_name.lower()
- feature_data["schemas"][f"read_{page_name}"]["defaults"][clean_name] = get_default_for_type(stype)
- snmp_read = {"oid": meta['oid']}; snmp_read["method"] = "walk" if meta["is_table"] else "get"
- mops_read = {"mib": meta['mib'], "table": meta['table'], "field": attr_name}
- if meta["is_table"] and meta["index_field"]: mops_read["index_field"] = meta["index_field"]
- attr_entry = {"syntax": syntax, "type": stype, "access": access, "sources": {"snmp": {"read": snmp_read}, "mops": {"read": mops_read}}}
- if validation: attr_entry["validation"] = validation
- if "bit_map" in meta: attr_entry["bit_map"] = meta["bit_map"]
- feature_data["attributes"][clean_name] = attr_entry
- with open(os.path.join(output_dir, f"{page_name}.yaml"), 'w') as f:
- yaml.dump(feature_data, f, sort_keys=False, default_flow_style=False)
- count += 1
- print(f"Generated {count} v2.6 Wire YAMLs with SMART PREFIXING.")
-
-if __name__ == "__main__":
- process_captured_pages()
+# Leftover v26/monolith one-shot. Not live law.
+# Machine-absolute paths removed. Original archived under
+# local/archive/generator-monolith-abs/. Do not run against live wire.
+# Use isolated batch_generate_MIB.py for emit-diff only.
+raise SystemExit(
+ "batch_generate_webui.py.stable is archive-only; "
+ "see local/generator/README.md and local/archive/generator-monolith-abs/"
+)
diff --git a/local/generator/cross_validate_v26.py b/local/generator/cross_validate_v26.py
index 68313d7..da5ba97 100644
--- a/local/generator/cross_validate_v26.py
+++ b/local/generator/cross_validate_v26.py
@@ -1,145 +1,54 @@
-import os
-import yaml
-import re
+"""Leftover v26/monolith script. Not live law.
-SCHEMA_PATH = "/home/adamr/obsidian-vault/Projects/napalm-hios-v2/docs/napalm-hios-2-6-schema.md"
-WEBUI_FEATURES = "/home/adamr/obsidian-vault/Projects/napalm-hios-v2/local/reference/webUI"
+Paths are repo-relative (no machine-absolute hardcodes). Do not treat
+this as a live generator. Live: generate_docs.py, generate_method_ref.py,
+generate_protocols.py, validate_schemas.py. See local/generator/README.md.
+"""
+from __future__ import annotations
-def load_schema_db():
- """Parse the markdown schema into a searchable database."""
- db = {}
- if not os.path.exists(SCHEMA_PATH): return {}
- with open(SCHEMA_PATH, "r") as f:
- content = f.read()
-
- # Extract feature sections
- sections = content.split("## Feature: ")
- for section in sections[1:]:
- lines = section.split("\n")
- feature_line = lines[0].strip()
- feature_name = feature_line.split("(")[0].strip().lower()
- if feature_name not in db: db[feature_name] = {}
-
- for line in lines[1:]:
- if "|" in line and "::" in line:
- parts = [p.strip() for p in line.split("|") if p.strip()]
- if len(parts) >= 5:
- attr_name = parts[0].lower()
- mib_field = parts[1]
- oid = parts[2]
- syntax = parts[3]
- access = parts[4]
-
- db[feature_name][attr_name] = {
- "oid": oid,
- "syntax": syntax,
- "access": access
- }
- return db
+import argparse
+import sys
+from pathlib import Path
-def validate_v26_yaml(path, schema_db, report):
- with open(path, "r") as f:
- try:
- data = yaml.safe_load(f)
- except Exception as e:
- report["errors"].append(f"YAML Parse Error in {os.path.basename(path)}: {str(e)}")
- return
-
- if not data: return
- feature_name = data.get("feature", "").lower()
-
- # 1. Feature check
- if feature_name not in schema_db:
- report["missing_features"].append(feature_name)
- return
-
- # 2. Schema terminology check
- schemas = data.get("schemas", {})
- for s_name in schemas:
- if s_name.startswith("get_"):
- report["terminology_violations"].append(f"{feature_name}: Schema '{s_name}' uses legacy 'get_' prefix")
-
- attrs = data.get("attributes", {})
- for attr_name, definition in attrs.items():
- if not isinstance(definition, dict): continue
-
- clean_attr = attr_name.lower()
- if clean_attr not in schema_db[feature_name]:
- # This is expected for some auto-gen features that haven't been manually curated
- # report["missing_attributes"].append(f"{feature_name}.{attr_name}")
- continue
-
- schema_meta = schema_db[feature_name][clean_attr]
-
- # 3. Syntax consistency
- if definition.get("syntax") != schema_meta["syntax"]:
- report["syntax_mismatches"].append({
- "target": f"{feature_name}.{attr_name}",
- "yaml": definition.get("syntax"),
- "schema": schema_meta["syntax"]
- })
-
- # 4. Access consistency
- yaml_access = definition.get("access", "r").lower()
- schema_access = schema_meta["access"].lower()
- if yaml_access != schema_access:
- report["access_mismatches"].append({
- "target": f"{feature_name}.{attr_name}",
- "yaml": yaml_access,
- "schema": schema_access
- })
-
- # 5. Terminology & Minimalism check in sources
- sources = definition.get("sources", {})
- for proto, src_def in sources.items():
- if not isinstance(src_def, dict): continue
-
- # Terminology: 'get' is forbidden
- if "get" in src_def:
- report["terminology_violations"].append(f"{feature_name}.{attr_name}.{proto}: Source uses legacy 'get' key")
-
- # Minimalism: if ru/crud, 'write' should usually be omitted in Wire YAML
- if yaml_access in ("ru", "crud") and "write" in src_def:
- report["minimalism_warnings"].append(f"{feature_name}.{attr_name}.{proto}: 'write' block exists despite 'access: {yaml_access}'")
+_REPO_ROOT = Path(__file__).resolve().parents[2]
+_WIRE = _REPO_ROOT / "crude_engine" / "wire"
+_SCHEMAS = _REPO_ROOT / "crude_engine" / "schemas"
-def run_validation():
- print(f"Starting v2.6 Compliance Validation...")
- schema_db = load_schema_db()
- print(f"Loaded {len(schema_db)} features from master schema.")
-
- report = {
- "errors": [],
- "missing_features": [],
- "missing_attributes": [],
- "syntax_mismatches": [],
- "access_mismatches": [],
- "terminology_violations": [],
- "minimalism_warnings": []
- }
-
- count = 0
- for fname in os.listdir(WEBUI_FEATURES):
- if not fname.endswith(".yaml"): continue
- validate_v26_yaml(os.path.join(WEBUI_FEATURES, fname), schema_db, report)
- count += 1
-
- print(f"Validated {count} Wire YAMLs.")
- print("\n--- v2.6 COMPLIANCE REPORT ---")
- print(f"Errors (Parse/IO): {len(report['errors'])}")
- print(f"Features Missing in Schema: {len(report['missing_features'])}")
- print(f"Terminology Violations: {len(report['terminology_violations'])}")
- print(f"Minimalism Warnings: {len(report['minimalism_warnings'])}")
- print(f"Syntax Mismatches: {len(report['syntax_mismatches'])}")
- print(f"Access Mismatches: {len(report['access_mismatches'])}")
-
- if report["terminology_violations"]:
- print("\nSample Terminology Violations:")
- for v in report["terminology_violations"][:5]: print(f" {v}")
- if report["syntax_mismatches"]:
- print("\nSample Syntax Mismatches (showing first 5):")
- for m in report["syntax_mismatches"][:5]:
- print(f" {m['target']}: YAML='{m['yaml']}' vs Schema='{m['schema']}'")
+def _parse_paths(argv: list[str] | None = None):
+ p = argparse.ArgumentParser(description=__doc__)
+ p.add_argument(
+ "--wire-dir",
+ type=Path,
+ default=_WIRE,
+ help="Wire YAML dir (default: repo crude_engine/wire)",
+ )
+ p.add_argument(
+ "--schema-dir",
+ type=Path,
+ default=_SCHEMAS,
+ help="Schema YAML dir (default: repo crude_engine/schemas)",
+ )
+ p.add_argument(
+ "--run-archive",
+ action="store_true",
+ help="Required to actually execute this leftover script",
+ )
+ return p.parse_args(argv)
+
+
+def main(argv: list[str] | None = None) -> None:
+ args = _parse_paths(argv)
+ if not args.run_archive:
+ raise SystemExit(
+ "leftover archive script; pass --run-archive to execute "
+ "(still not live law). Defaults are repo-relative."
+ )
+ raise SystemExit(
+ "archive body not ported to relative paths as a safe mutator; "
+ "use validate_schemas.py / isolated batch_generate_MIB.py instead"
+ )
+
if __name__ == "__main__":
- run_validation()
+ main()
diff --git a/local/generator/enrich_schema_v26.py b/local/generator/enrich_schema_v26.py
index 680d334..da5ba97 100644
--- a/local/generator/enrich_schema_v26.py
+++ b/local/generator/enrich_schema_v26.py
@@ -1,87 +1,54 @@
-import os
-import xml.etree.ElementTree as ET
-import re
+"""Leftover v26/monolith script. Not live law.
-SCHEMA_PATH = "/home/adamr/obsidian-vault/Projects/napalm-hios-v2/docs/napalm-hios-2-6-schema.md"
-XML_PATH = "/home/adamr/obsidian-vault/Projects/MOPS_Emulator/data/mops_schema.xml"
+Paths are repo-relative (no machine-absolute hardcodes). Do not treat
+this as a live generator. Live: generate_docs.py, generate_method_ref.py,
+generate_protocols.py, validate_schemas.py. See local/generator/README.md.
+"""
+from __future__ import annotations
-def get_mib_data(root):
- mib_db = {}
- for obj in root.findall(".//ObjectType"):
- name = obj.get("name")
- if not name: continue
- desc_node = obj.find("Description")
- desc = desc_node.get("text").replace("\r", " ").replace("\n", " ").strip() if desc_node is not None else ""
- desc = re.sub(r'\s+', ' ', desc)
- # Extract Constraints (Range or Enums)
- constraints = ""
- syntax_node = obj.find("Syntax")
- syntax_name = "Unknown"
- if syntax_node is not None:
- syntax_name = syntax_node.get("name", "Unknown")
- enums = syntax_node.findall("Enumeration")
- if enums:
- constraints = "Enums: [" + ", ".join([f"{e.get('value')}:{e.get('name')}" for e in enums]) + "]"
- else:
- range_node = syntax_node.find("Range")
- if range_node is not None:
- lv = range_node.get('lowerValue') or range_node.get('value') or "0"
- uv = range_node.get('upperValue')
- if uv: constraints = f"Range: {lv}..{uv}"
- else: constraints = f"Value: {lv}"
+import argparse
+import sys
+from pathlib import Path
- # Fallback: Extract range from description text (e.g. Unsigned32(0..600))
- if not constraints and desc:
- rm = re.search(r"\((\d+)\.\.(\d+)\)", desc)
- if rm:
- constraints = f"Range: {rm.group(1)}..{rm.group(2)}"
+_REPO_ROOT = Path(__file__).resolve().parents[2]
+_WIRE = _REPO_ROOT / "crude_engine" / "wire"
+_SCHEMAS = _REPO_ROOT / "crude_engine" / "schemas"
-
- # Determine high-fidelity access
- mib_access = obj.get("access", "read-only").lower().replace(" ", "-")
- crud_access = "r"
-
- # RowStatus ALWAYS gets CRUD (Forceful override for automation)
- if syntax_name == "RowStatus":
- crud_access = "crud"
- elif "read-create" in mib_access or "read-write" in mib_access:
- crud_access = "ru"
- elif "write-only" in mib_access:
- crud_access = "u"
- mib_db[name] = {"desc": desc, "const": constraints, "access": crud_access, "syntax": syntax_name}
- return mib_db
+def _parse_paths(argv: list[str] | None = None):
+ p = argparse.ArgumentParser(description=__doc__)
+ p.add_argument(
+ "--wire-dir",
+ type=Path,
+ default=_WIRE,
+ help="Wire YAML dir (default: repo crude_engine/wire)",
+ )
+ p.add_argument(
+ "--schema-dir",
+ type=Path,
+ default=_SCHEMAS,
+ help="Schema YAML dir (default: repo crude_engine/schemas)",
+ )
+ p.add_argument(
+ "--run-archive",
+ action="store_true",
+ help="Required to actually execute this leftover script",
+ )
+ return p.parse_args(argv)
+
+
+def main(argv: list[str] | None = None) -> None:
+ args = _parse_paths(argv)
+ if not args.run_archive:
+ raise SystemExit(
+ "leftover archive script; pass --run-archive to execute "
+ "(still not live law). Defaults are repo-relative."
+ )
+ raise SystemExit(
+ "archive body not ported to relative paths as a safe mutator; "
+ "use validate_schemas.py / isolated batch_generate_MIB.py instead"
+ )
-def enrich_schema():
- print("Performing Deep Enrichment of 2.6 Schema...")
- tree = ET.parse(XML_PATH); root = tree.getroot(); mib_db = get_mib_data(root)
- with open(SCHEMA_PATH, "r") as f: lines = f.readlines()
-
- new_lines = []
- enriched_count = 0
- for line in lines:
- if "|" in line and "::" in line:
- parts = [p.strip() for p in line.split("|")]
- if len(parts) >= 9:
- mib_field_raw = parts[2]
- if "::" in mib_field_raw:
- field_name = mib_field_raw.split("::")[-1]
- if field_name in mib_db:
- meta = mib_db[field_name]
- # OVERWRITE with high-fidelity truth
- if meta["const"]: parts[6] = f" {meta['const']} "
- if meta["desc"]: parts[9] = f" {meta['desc'][:150]}... "
- parts[5] = f" {meta['access']} "
- # Update syntax string if it's currently low-fidelity (e.g. 'INTEGER')
- if len(parts[4]) < len(meta["syntax"]) or "INTEGER" in parts[4]:
- parts[4] = f" {meta['syntax']} "
-
- line = "|" + "|".join(parts[1:-1]) + "|\n"
- enriched_count += 1
- new_lines.append(line)
-
- with open(SCHEMA_PATH, "w") as f: f.writelines(new_lines)
- print(f"Enriched {enriched_count} attributes with High-Fidelity truth.")
if __name__ == "__main__":
- enrich_schema()
+ main()
diff --git a/local/generator/heal_schemas.py b/local/generator/heal_schemas.py
index a6ead83..da5ba97 100644
--- a/local/generator/heal_schemas.py
+++ b/local/generator/heal_schemas.py
@@ -1,72 +1,54 @@
-import os
-import yaml
+"""Leftover v26/monolith script. Not live law.
-# Paths
-WIRE_DIR = "/home/adamr/obsidian-vault/Projects/napalm-hios-v2/local/reference/webUI"
-SCHEMA_DIR = "/home/adamr/obsidian-vault/Projects/napalm-hios-v2/napalm_hios/schemas"
+Paths are repo-relative (no machine-absolute hardcodes). Do not treat
+this as a live generator. Live: generate_docs.py, generate_method_ref.py,
+generate_protocols.py, validate_schemas.py. See local/generator/README.md.
+"""
+from __future__ import annotations
-def heal_schemas():
- print("Building Wire Attribute Index...")
- wire_index = {} # attribute_name -> wire_file_base_name
-
- for fname in sorted(os.listdir(WIRE_DIR)):
- if not fname.endswith(".yaml"): continue
- w_id = fname.replace(".yaml", "").lower()
- with open(os.path.join(WIRE_DIR, fname), "r") as f:
- try:
- data = yaml.safe_load(f)
- if not data or "attributes" not in data: continue
- for attr in data["attributes"].keys():
- attr_low = attr.lower()
- # Preference: MIB > everything else
- if "-mib" in w_id:
- wire_index[attr_low] = w_id
- elif attr_low not in wire_index:
- wire_index[attr_low] = w_id
- except: continue
-
- print(f"Indexed {len(wire_index)} unique wire attributes.")
-
- healed_count = 0
-
- for fname in sorted(os.listdir(SCHEMA_DIR)):
- if not fname.endswith(".yaml"): continue
- s_path = os.path.join(SCHEMA_DIR, fname)
- with open(s_path, "r") as f:
- try:
- s_data = yaml.safe_load(f)
- except: continue
-
- if not s_data or "attributes" not in s_data: continue
-
- # Read raw text for surgical source: replacement
- with open(s_path, "r") as f:
- raw = f.read()
+import argparse
+import sys
+from pathlib import Path
- new_raw = raw
- changed = False
- for h_attr, a_map in s_data["attributes"].items():
- old_wire = a_map.get("wire", "").lower()
- old_source = str(a_map.get("source", "")).lower().replace(".yaml", "")
+_REPO_ROOT = Path(__file__).resolve().parents[2]
+_WIRE = _REPO_ROOT / "crude_engine" / "wire"
+_SCHEMAS = _REPO_ROOT / "crude_engine" / "schemas"
- new_source = wire_index.get(old_wire)
- if new_source and new_source != old_source:
- print(f" Fixing {fname}: {h_attr} ({old_wire}) -> {new_source}")
- # Replace only the source: line for this attribute
- new_raw = new_raw.replace(
- f"source: {a_map.get('source', '')}",
- f"source: {new_source}",
- 1 # only first occurrence per iteration
- )
- changed = True
+def _parse_paths(argv: list[str] | None = None):
+ p = argparse.ArgumentParser(description=__doc__)
+ p.add_argument(
+ "--wire-dir",
+ type=Path,
+ default=_WIRE,
+ help="Wire YAML dir (default: repo crude_engine/wire)",
+ )
+ p.add_argument(
+ "--schema-dir",
+ type=Path,
+ default=_SCHEMAS,
+ help="Schema YAML dir (default: repo crude_engine/schemas)",
+ )
+ p.add_argument(
+ "--run-archive",
+ action="store_true",
+ help="Required to actually execute this leftover script",
+ )
+ return p.parse_args(argv)
+
+
+def main(argv: list[str] | None = None) -> None:
+ args = _parse_paths(argv)
+ if not args.run_archive:
+ raise SystemExit(
+ "leftover archive script; pass --run-archive to execute "
+ "(still not live law). Defaults are repo-relative."
+ )
+ raise SystemExit(
+ "archive body not ported to relative paths as a safe mutator; "
+ "use validate_schemas.py / isolated batch_generate_MIB.py instead"
+ )
- if changed:
- with open(s_path, "w") as f:
- f.write(new_raw)
- healed_count += 1
-
- print(f"Healed {healed_count} schema files.")
if __name__ == "__main__":
- heal_schemas()
+ main()
diff --git a/local/generator/overrides.yaml b/local/generator/overrides.yaml
index 850181d..7b8637e 100644
--- a/local/generator/overrides.yaml
+++ b/local/generator/overrides.yaml
@@ -1,6 +1,7 @@
# Generator overrides — human-declared exceptions the MIB can't tell us.
# These take priority over MIB-derived defaults.
# Key: lowercase wire attribute name → override fields
+# Leftover MIB generator data, not live law.
# create_method: device-proven via live SNMP testing (2026-03-23)
# Default is createAndWait. Only list createAndGo exceptions here.
@@ -17,3 +18,13 @@ create_method:
# The MIB has TC definitions with base syntax, but syntax_to_type only sees the TC name
type:
hm2useraccessrole: integer
+
+# sources: leftover NTP names whose device answers SNTP (schema ntp.server_enabled
+# still points at hm2ntpserveradminstate). MIB has both prefixes; #13 is the live
+# lab cell. OID is the SNTP object (not live wire's 1.4.1.3 which collides with
+# hm2PtpProfile).
+sources:
+ hm2ntpserveradminstate:
+ table: hm2SntpServerGroup
+ field: hm2SntpServerAdminState
+ oid: 1.3.6.1.4.1.248.11.50.1.2.1.1
diff --git a/local/generator/validate_schema_wire.py b/local/generator/validate_schema_wire.py
index d3f9575..da5ba97 100644
--- a/local/generator/validate_schema_wire.py
+++ b/local/generator/validate_schema_wire.py
@@ -1,56 +1,54 @@
-"""
-Step 3: Validate schema→wire resolution.
-Run AFTER heal_schemas.py.
+"""Leftover v26/monolith script. Not live law.
-Usage: python3 local/generator/validate_schema_wire.py
+Paths are repo-relative (no machine-absolute hardcodes). Do not treat
+this as a live generator. Live: generate_docs.py, generate_method_ref.py,
+generate_protocols.py, validate_schemas.py. See local/generator/README.md.
"""
-import yaml, os
-
-WIRE_DIR = "/home/adamr/obsidian-vault/Projects/napalm-hios-v2/local/reference/webUI"
-SCHEMA_DIR = "/home/adamr/obsidian-vault/Projects/napalm-hios-v2/napalm_hios/schemas"
-
-# Build wire attr index
-wire_index = {}
-for f in sorted(os.listdir(WIRE_DIR)):
- if not f.endswith('.yaml'): continue
- with open(os.path.join(WIRE_DIR, f)) as fh:
- data = yaml.safe_load(fh)
- for attr in data.get('attributes', {}):
- wire_index[attr] = f[:-5]
-
-# Check each schema
-total = found = 0
-missing = []
-for f in sorted(os.listdir(SCHEMA_DIR)):
- if not f.endswith('.yaml'): continue
- with open(os.path.join(SCHEMA_DIR, f)) as fh:
- s = yaml.safe_load(fh)
- for attr, ref in s.get('attributes', {}).items():
- if not isinstance(ref, dict) or 'wire' not in ref: continue
- total += 1
- src = ref.get('source', '')
- wp = os.path.join(WIRE_DIR, f"{src}.yaml")
- if os.path.exists(wp):
- with open(wp) as wf:
- wd = yaml.safe_load(wf)
- if ref['wire'] in wd.get('attributes', {}):
- found += 1
- continue
- actual = wire_index.get(ref['wire'], 'NOT_IN_ANY_WIRE')
- missing.append((f, attr, ref['wire'], src, actual))
-
-pct = found * 100 // total if total else 0
-print(f"Schema→Wire: {found}/{total} ({pct}%)")
-if missing:
- fixable = [m for m in missing if m[4] != 'NOT_IN_ANY_WIRE']
- gone = [m for m in missing if m[4] == 'NOT_IN_ANY_WIRE']
- if fixable:
- print(f"\nFIXABLE ({len(fixable)}) — wrong source, attr exists elsewhere:")
- for s, a, w, src, actual in fixable:
- print(f" {s}:{a} source={src} → should be {actual}")
- if gone:
- print(f"\nMISSING ({len(gone)}) — attr not in any wire file:")
- for s, a, w, src, actual in gone:
- print(f" {s}:{a} wire={w} source={src}")
-else:
- print("ALL RESOLVED")
+from __future__ import annotations
+
+import argparse
+import sys
+from pathlib import Path
+
+_REPO_ROOT = Path(__file__).resolve().parents[2]
+_WIRE = _REPO_ROOT / "crude_engine" / "wire"
+_SCHEMAS = _REPO_ROOT / "crude_engine" / "schemas"
+
+
+def _parse_paths(argv: list[str] | None = None):
+ p = argparse.ArgumentParser(description=__doc__)
+ p.add_argument(
+ "--wire-dir",
+ type=Path,
+ default=_WIRE,
+ help="Wire YAML dir (default: repo crude_engine/wire)",
+ )
+ p.add_argument(
+ "--schema-dir",
+ type=Path,
+ default=_SCHEMAS,
+ help="Schema YAML dir (default: repo crude_engine/schemas)",
+ )
+ p.add_argument(
+ "--run-archive",
+ action="store_true",
+ help="Required to actually execute this leftover script",
+ )
+ return p.parse_args(argv)
+
+
+def main(argv: list[str] | None = None) -> None:
+ args = _parse_paths(argv)
+ if not args.run_archive:
+ raise SystemExit(
+ "leftover archive script; pass --run-archive to execute "
+ "(still not live law). Defaults are repo-relative."
+ )
+ raise SystemExit(
+ "archive body not ported to relative paths as a safe mutator; "
+ "use validate_schemas.py / isolated batch_generate_MIB.py instead"
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/local/generator/validate_schemas.py b/local/generator/validate_schemas.py
index 90f1797..08e756b 100644
--- a/local/generator/validate_schemas.py
+++ b/local/generator/validate_schemas.py
@@ -27,6 +27,7 @@
VALID_METHOD_KEYS = {
'type', 'defaults', 'primary_key', 'key_map', 'index_fields',
+ 'index_type',
'sub_tables', 'row_status', 'index_key', 'required', 'fields',
'index_filter', 'linked_tables', 'attributes', 'schema',
}
diff --git a/local/generator/validate_v26_all.py b/local/generator/validate_v26_all.py
index 4035f36..dd78627 100644
--- a/local/generator/validate_v26_all.py
+++ b/local/generator/validate_v26_all.py
@@ -1,12 +1,20 @@
+"""Read-only schema→wire integrity audit.
+
+Retargeted from leftover napalm-hios-v2 / napalm_hios paths to this
+repo's crude_engine/{wire,schemas}. Does not mutate YAML.
+Live schema law is validate_schemas.py (CI). This is an extra
+broken-link / duplicate-OID walk, not a live doc generator.
+"""
import os
import yaml
import json
from collections import defaultdict
-# Default Paths
-BASE_DIR = "/home/adamr/obsidian-vault/Projects/napalm-hios-v2"
-WIRE_DIR = os.path.join(BASE_DIR, "napalm_hios/wire")
-SCHEMA_DIR = os.path.join(BASE_DIR, "napalm_hios/schemas")
+# Default paths — this repo's crude_engine/ (not napalm-hios-v2)
+BASE_DIR = os.path.dirname(os.path.abspath(__file__))
+PACKAGE_DIR = os.path.join(BASE_DIR, "../../crude_engine")
+WIRE_DIR = os.path.join(PACKAGE_DIR, "wire")
+SCHEMA_DIR = os.path.join(PACKAGE_DIR, "schemas")
def run_validation(wire_path=WIRE_DIR, schema_path=SCHEMA_DIR):
print(f"Starting v2.6 Multi-Stage Validation...")
diff --git a/scripts/ci_offline.sh b/scripts/ci_offline.sh
index 4a87a87..6eb681d 100644
--- a/scripts/ci_offline.sh
+++ b/scripts/ci_offline.sh
@@ -41,6 +41,9 @@ else
fi
run "program-files" "$PY" scripts/generate_status.py --check
+run "inspect-result" "$PY" tests/test_inspect_result.py
+run "inspect-reaches-driver" "$PY" tests/test_inspect_reaches_driver.py
+run "ssh-dns-key-column" "$PY" tests/test_ssh_dns_key_column.py
run "principles" "$PY" scripts/check_principles.py
# Catalogue proofs are the 2.10 exit. They are expected red in cycle 0
@@ -77,4 +80,7 @@ fi
soft=0
"$PY" -c "from crude_engine import FeatureEngine" || soft=1
"$PY" scripts/generate_status.py --check || soft=1
+"$PY" tests/test_inspect_result.py || soft=1
+"$PY" tests/test_inspect_reaches_driver.py || soft=1
+"$PY" tests/test_ssh_dns_key_column.py || soft=1
exit $soft
diff --git a/scripts/generate_status.py b/scripts/generate_status.py
index 37fd3f1..ba41f7d 100644
--- a/scripts/generate_status.py
+++ b/scripts/generate_status.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
-"""Render docs/status.html from program/*.yaml + TODO/ROADMAP presence.
+"""Render docs/status.html from program/*.yaml + ROADMAP presence.
python3 scripts/generate_status.py
python3 scripts/generate_status.py --check # fail if human files missing
@@ -156,7 +156,7 @@ def task_card(t):
SEED.md ·
METHOD.md ·
ROADMAP.md ·
- TODO.md ·
+ GitHub issues ·
cycles.yaml
@@ -188,7 +188,7 @@ def task_card(t):
def check_human_files():
missing = []
- for rel in ("docs/ROADMAP.md", "docs/TODO.md", "docs/program/SEED.md",
+ for rel in ("docs/ROADMAP.md", "docs/program/SEED.md",
"docs/program/METHOD.md", "docs/program/cycles.yaml",
"docs/program/roadmap.yaml"):
if not os.path.isfile(os.path.join(ROOT, rel)):
diff --git a/sidecar/README.md b/sidecar/README.md
new file mode 100644
index 0000000..e115a35
--- /dev/null
+++ b/sidecar/README.md
@@ -0,0 +1,57 @@
+# Sidecar (read-only named tests)
+
+Thin HTTP over the existing harness. Not a second test suite.
+
+ POST /v1/run {"name": "get_dns.read"}
+
+maps to
+
+ python3 tests/release_matrix.py --inspect --method get_dns --device