Files
stack/docs/plans/reviews/2026-09-06_foundation-inspector-rocko-feasibility.md
jason.woltje 8ebddd6f93 feat(foundation): offline synthetic scope/permission inspector (FI-FILBERT-8 APPROVED r6)
Rocko-authored, Filbert-reviewed inspector (r6 manifest
a4a44930...) with full review/build/verdict evidence under
docs/plans/reviews. 43/0 selftests, oracle zero-disagreement,
foundation checker PASS. Owner A9 acceptance recorded separately.
2026-09-07 14:06:35 -05:00

28 KiB
Raw Permalink Blame History

FI-ROCKO-1 — Foundation synthetic inspector: measured feasibility note

Author: rocko (Claude Fable 5.1, Archify lane author seat). Requested by: darkwing (FI-ROCKO-1, 2026-09-06). Issue: #53. Status: planning evidence only. This note proposes; it does not approve, implement, or bind. Every choice below is a recommendation for darkwing to accept, alter, or reject in the frozen charter.

0. Admission

FI-ROCKO-1 is admitted. No blocker.

  • Compatibility: my current owner goal (Archify C1 rev 3, HELD for Jason's T2/T3 rulings) is a waiting state with no lane action due; this note displaces nothing. I take no supervisory authority over darkwing, filbert, or Jason's queue; Jason may veto or discard this note at any time.
  • Authority basis observed, not assumed: docs/plans/CURRENT.md (working tree) records Jason's acceptance of the map and "continue with filbert/rocko"; the charter draft restates it. I did not verify this with Jason directly.
  • Scope honoured: this file is the only repository write. No commit, push, checkout, reset, live/runtime test, credential read, or ~/.mosaic investigation was performed. Dewey's extensions/**, .pi/**, and scripts were not touched. Filbert's verdict files were not opened for editing.

1. Pinned inputs

Input Identity How read
Accepted map docs/plans/2026-09-06_agent-project-workspace-foundation.md commit 7345f330fc6bfae5aa1d896c78cfb7cbe62efbae git show 7345f33:…
Source/plan baseline (candidate schema, semantic-model.py, check.py, README, REVIEW, RUNTIME, phase-2 contract, ROADMAP, foundation plan) commit d4696d09eb1b5dcf1028f30db2cd63735f51cb16 git show d4696d09:…
Charter draft 1 docs/plans/2026-09-06_foundation-inspector-charter.md untracked; SHA-256 fdcdf4df8bf30b9fbfff960f9b1d4160ce7e6bbc4b8bb601a93cda13b56c8996 at write time working tree
Filbert map verdict (FM-FILBERT-1) SHA-256 6b08c6fac0718d3db527cf9ffbfab49407e7b289d09782f5d1d0e26493eaabb3 (as cited in the charter) read only

The charter is a moving draft: an earlier read in this session hashed to 9c065ef1…. Recommendations below target the fdcdf4df… text. If the frozen charter differs materially, this note must be re-checked, not assumed valid.

Citations use commit:path:line. cand/ abbreviates docs/plans/foundation-v1-candidate/.

2. Repository facts that constrain the design

  • Host toolchain: Node v26.8.1 with node:test available; Python 3.12.8 with jsonschema 4.26.0 importable. package.json declares one dependency (pi 0.84.4); no node_modules is present in the tree. RELEASE = 0.0.12.
  • scripts/ is flat; no packages/ tree exists yet. ROADMAP places the package layout under M20 (d4696d09:docs/plans/ROADMAP.md:114) with the layout doctrine and phase-1 sequencing at :142-145 and the target tree at :152-159 (packages/config at :159).
  • The map's recommended increment forbids npm or Turbo changes (7345f33:docs/plans/2026-09-06_agent-project-workspace-foundation.md:278) and proposes exit classes 0/2/3/4 (:275).
  • Existing validation idiom is hand-rolled and closed: scripts/mosaic-task.mjs (rejectUnknownKeys, fail(2, …)). scripts/mosaic-config.mjs reads $MOSAIC_CONFIG (:42-43) and refuses symlinks (:54); importing it runs config resolution, which the charter forbids for the inspector.
  • Existing suite style: scripts/test-config.sh uses a mktemp sandbox, check/expect_exit helpers, and NO_COLOR.
  • cand/FINDINGS.md records an ajv/fast-uri audit concern; the candidate's own checker uses Python jsonschema (cand/check.py).
  • The candidate's fingerprint rule is restricted-domain canonicalization, not full RFC 8785 (d4696d09:cand/README.md:265-288, ASCII-only vectors :281-282; check.py sorts keys, uses compact separators, ASCII-only, sorts skills by id, digest = "sha256:" + hex(SHA-256(UTF-8))).

3. Language and dependency pin (recommendation)

Recommend: Node ESM (.mjs), zero new dependencies, hand-rolled closed validation. Pin: the repository's existing Node engine (v26.8.1 on host); no package.json change; no ajv, no jsonschema at runtime.

Why:

  • Matches the map (:278, no npm/Turbo change) and the repo's validation idiom.
  • Avoids the ajv/fast-uri audit issue and avoids adding a runtime dependency for a preview tool that must be reproducible offline.
  • Node gives TextDecoder('utf-8', { fatal: true }) for malformed-UTF-8 refusal, node:crypto for SHA-256, and node:test for units, all built in.
  • A hand-rolled closed validator is smaller than the candidate schema but must be derived from cand/records.schema.json field-by-field. To keep it honest, the test suite should run the Python candidate checker over every fixture bundle's records as a cross-check when python3 -c "import jsonschema" succeeds, and skip with a visible notice otherwise (see §9).

Alternative (viable, not preferred): Python 3.12 stdlib only, reusing check.py's canonicalizer verbatim. Rejected because the CLI surface of the stack is Node/bash and because the charter says not to promote the author-only Python model into the inspector. This is unresolved decision D-A.

Explicitly not recommended: JSON Schema at runtime via ajv (audit issue, dependency) or via Python jsonschema in a Node CLI (two runtimes).

4. Physical source paths (recommendation, M20-consistent)

No packages/ scaffolding in this increment. Files land in scripts/, which is the existing flat layout, with a comment header naming the logical owner so M20 phase 1 can move them without rewriting.

Path Logical owner (M20 target) Contents
scripts/foundation-inspect.mjs packages/mosaic (CLI presentation) argv parsing, file read, output rendering, exit classes
scripts/foundation/strict-json.mjs packages/config strict JSON reader: fatal UTF-8, duplicate-key refusal, depth/size limits, byte-offset-only errors
scripts/foundation/canonical.mjs packages/config restricted-domain canonicalizer + sha256: digest, out-of-domain refusal
scripts/foundation/resolve.mjs packages/config bundle validation, reference resolution, graph checks, permission intersection, proposal evaluation. Pure: no I/O, no env, no clock
scripts/foundation/fixtures/*.json test data demo bundle plus negative fixtures
scripts/test-foundation.sh suite sandboxed CLI/privacy/non-effect suite, invokes node --test scripts/foundation/*.test.mjs
scripts/foundation/*.test.mjs unit tests node:test units for the three pure modules

Rules:

  • resolve.mjs, canonical.mjs, strict-json.mjs import only each other and node:crypto. Never mosaic-config.mjs, never process.env, never Date.
  • foundation-inspect.mjs is the only file that touches process.argv, fs, stdout, stderr, and process.exit. It reads process.env only to honour NO_COLOR; it never reads MOSAIC_CONFIG, HOME, or the data root.
  • Root stays bootstrap-only (AGENTS.md invariant 1): nothing is added to root.
  • The command name mosaic foundation inspect <bundle.json> from REVIEW (d4696d09:cand/REVIEW.md:123) is the eventual packages/mosaic surface. In this increment the wrapper is scripts/foundation-inspect.mjs; a foundation-inspect.sh shim is unnecessary. Naming is decision D-H.

5. Bundle contract (exact fields, proposed to freeze)

One file, one closed object. Every object is closed: unknown keys refuse. Duplicate keys refuse at parse time. All strings must be ASCII printable when they participate in digests (§6); identifiers are ^[a-z0-9][a-z0-9-]{0,63}$.

{
  "bundleVersion": 1,
  "kind": "foundation-inspector-bundle",
  "simulation": true,                     // must be literally true; anything else refuses
  "records": [ <record>, ... ],           // candidate envelope records, ≤256
  "registries": [ <registryEntry>, ... ], // ≤256
  "selection": {
    "agentId": "<id>",
    "projectId": "<id>",
    "workspaceId": "<id>",
    "assignmentRef": <recordRef> | null,
    "launchRestrictions": <restrictions> | null   // simulated execution layer, D-F
  },
  "operation": { "name": "<one of the 29 operation names>", "target": <target> | null },
  "proposal": null | {
    "kind": "assignment-change",
    "requesterAgentId": "<id>",
    "subjectAssignmentRef": <recordRef>,
    "targetTaskRef": <recordRef>,
    "delegationRef": <recordRef> | null,       // decision, decisionKind delegation
    "changeDecisionRef": <recordRef> | null,   // decision, decisionKind assignment-change
    "message": "<string>" | absent,            // informational only; never authority
    "runtimeObservations": {
      "originalExecutionStopped": true|false|null,
      "effectsReconciled": true|false|null
    }
  }
}

<record> is exactly the candidate envelope (d4696d09:cand/records.schema.json): {schemaVersion:1, kind, id, scope, revision, createdAt, createdBy, supersedes, authorizationRef, payload}, closed. createdAt is carried and validated as a string but never used in calculations (charter: no timestamps in results).

<recordRef> = {kind, id, scope, revision}; <registryRef> = {registry, id, revision, digest}; <restrictions> = {operations[], readPaths[], writePaths[], network, endpointRefs[]} with pathGrant {root:"workspace", path|null}; all as defined in the candidate schema.

<target> for file operations: {root:"workspace", path:"<relative>"}. For record operations: a <recordRef>. For operations that take no target: null. An operation whose target shape does not match its name refuses invalid-request.

<registryEntry> has two shapes:

  • Declared: {registry, id, revision, digest} for harness, settings, context-content, endpoint. Existence and exact-match only.
  • Content-bearing: {registry, id, revision, digest, content} for scope-role, agent-policy, project-policy. content is {restrictions: <restrictions>}; digest must equal the canonical digest of content (§6) or the bundle refuses invalid-request. These are the "separately identified mock policy inputs" the charter requires. The candidate does not define registry content shapes; this is decision D-E.

Bounds (refuse invalid-request before any calculation):

Bound Value
File size ≤ 1 MiB (aligns with RUNTIME frame limit d4696d09:cand/RUNTIME.md:41 and contract …phase2-contract.md:116)
records ≤ 256
registries ≤ 256
Any array ≤ 1024 items (contract :116)
Nesting depth ≤ 32
Any string ≤ 4096 bytes; path strings ≤ 4096 bytes
Input file regular file only; symlink, directory, FIFO, device refuse invalid-request (matches mosaic-config.mjs:54 doctrine)
Path argument exactly one positional bundle path; relative paths resolved against cwd but never searched; no defaults

Explicitly absent by design: no $ref, no includes, no URLs, no env, no credentials, no authenticated/authorized booleans anywhere. Presence of any of those keys is an unknown-key refusal, not a warning.

6. Reference and digest algorithm

6.1 Canonical digest (restricted domain)

Implement canonical.mjs as the candidate's rule, not JCS:

  1. Domain: objects, arrays, strings, booleans, null, and integers within Number.isSafeInteger. Floats, -0, non-ASCII strings, control characters other than those JSON must escape, and values outside the domain refuse with unsupported-capability. This is stricter than JCS and is the documented restricted domain (d4696d09:cand/README.md:265-288).
  2. Object keys sorted by UTF-16 code unit order (same as sort_keys=True on ASCII keys), compact separators, no whitespace.
  3. Set-valued arrays are sorted by a stable key before serialization: skillRefs by id, instructionRefs by id, operations lexically, readPaths/writePaths by path with null first, endpointRefs by id. Sequence-valued arrays (dependencies, subjectRefs, evidenceRefs) keep order. The list of set-valued fields is enumerated in code and in the frozen charter; anything not listed is a sequence.
  4. Digest = "sha256:" + lowercase hex SHA-256 of the UTF-8 bytes.
  5. Test vectors: reproduce the README's vectors (:281-282) and add one out-of-domain vector per rejected class.

No claim of RFC 8785 conformance is made in code, docs, or output.

6.2 Reference resolution (bounded resolver, resolve.mjs)

Index build:

  • Key (kind, id, scope) → list of revisions. Two records with the same (kind, id, scope, revision) refuse invalid-request (duplicate identity, A3).
  • supersedes chains must be contiguous from revision 1: revision n must supersede exactly (kind, id, scope, n-1); gaps, forks (two records superseding the same predecessor), or a supersedes pointing outside the identity refuse invalid-request. The bundle may carry only the head revision if it carries no history at all for that identity (D-J).
  • id must agree with scope per README :64; mismatch refuses.

Reference check (every recordRef and registryRef in every payload):

  • Exact match on all fields; digest compared byte-for-byte after §6.1 recomputation for content-bearing entries. A ref to a superseded revision refuses stale-revision. A ref to an absent identity refuses missing-state. Kind mismatch (e.g. policyRef not pointing at agent-policy) refuses invalid-request.

Graph rules (all refuse without repair; the violated rule name is emitted):

  • workspace.payload must reference exactly one active project (README :65; charter "exactly one project owns a workspace").
  • A workspace registration for agent X must reference, via projectRegistrationRef, an active project registration for the same agent X in the owning project (README :65). Missing or revoked → refuse.
  • mission.parentMissionRef must resolve to a mission in the same project; a task's missionRef must resolve to a mission in the selected project.
  • assignment must bind one task and one agent; intentRef must resolve to a decision with decisionKind ∈ {plan-approval, delegation, assignment-change} whose subjectRefs include the task. Otherwise not-authorized.
  • Cycle checks (A3, "document which edges participate"): DFS over exactly three edge sets, evaluated independently: (i) task.dependencies, (ii) mission.parentMissionRef, (iii) supersedes. A cycle in any of them refuses invalid-request. Edges deliberately excluded from cycle checks: intentRef, authorizationRef, evidenceRefs, subjectRefs, all registryRefs (they point outside the record graph).
  • Cross-project execution selection refuses (README :71).

7. Permission algorithm

Inputs: the resolved graph, selection, operation. Output: allowed or refused + reason code + violated rule + the identities consulted. Never the payload bodies.

Taskless set: {work.read, file.read} (from semantic-model.py). Every other operation requires selection.assignmentRef to be non-null (D-D asks whether work.propose belongs in the taskless set; recommendation: no, keep the candidate's set).

Layers, in fixed order, each producing a <restrictions> view or a refusal:

# Layer Source Missing →
L1 agent policy agent-definition.policyRef → registry agent-policy.content.restrictions refuse missing-state
L2 project registration active registration for agent in projectId, restrictions refuse not-authorized (A2)
L3 workspace registration active registration for agent in workspaceId whose projectRegistrationRef is L2's record refuse not-authorized (A2)
L4 project policy project.policyRef → registry project-policy refuse missing-state
L5 workspace policy workspace.policyRef if present in candidate payload; else scope-role of L3 refuse missing-state
L6 assignment selection.assignmentRef; must be status: selected, agent = agentId, task in projectId non-taskless op → refuse not-authorized
L7 task task.restrictions null = no narrowing
L8 mission chain each mission up the parentMissionRef chain, restrictions null = no narrowing
L9 simulated execution layer selection.launchRestrictions null = no narrowing; when present, narrows only (D-F)

Intersection semantics (README :96-142, contract :158-193):

  • operations: set intersection. An explicit empty array is none (:101). A null restrictions object at L7L9 means "no narrowing"; at L1L5 a null refuses (required layer, :133).
  • readPaths/writePaths: a grant {root:"workspace", path:null} is the whole root (:102). Intersection of two grant lists = the set of pairs where one is a component-prefix of the other, keeping the narrower. Component-prefix means path segments, not string prefix (a/b covers a/b/c, not a/bc). Target paths must be relative, normalized, contain no .., no empty segment, no leading /, no \, no NUL; otherwise refuse invalid-request before any permission result (A5).
  • network: none ∧ anything = none; approved-endpointsapproved-endpoints = intersection of endpointRefs by exact ref.
  • Never union across assignments (contract :172); exactly one assignment is consulted (README :134). A second selected assignment for the same agent+task in the bundle is not an error, but only the selected one is used.
  • Result for the operation: allowed iff operation.name ∈ operations and, for file operations, the target path is covered by the corresponding readPaths/writePaths after intersection, and for network operations the endpoint is covered.

Refusal precedence (deterministic): parse/shape errors (exit 2) → graph incoherence (exit 2, D-C) → missing required layer (exit 3, missing-state) → registration absent (exit 3, not-authorized) → operation not in intersection (exit 3, not-authorized). The first failing check ends evaluation; the output names only that rule.

8. Proposal mode (cross-lane reassignment, map :290-314)

Evaluated only when proposal !== null, after the base permission result:

  1. requesterAgentId must hold an active registration in the target task's project (authority in target scope) — else refuse not-authorized.
  2. delegationRef must resolve to a decision with decisionKind: delegation, outcome approved, delegatedOperations including assignment.change, and subjectRefs covering the subject assignment or its task — else refuse not-authorized. A message with no delegation refuses with the same code and the violated rule "message-is-not-authority" (A7).
  3. changeDecisionRef must resolve to a decision with decisionKind: assignment-change whose subjectRefs include both the subject assignment and the target task and whose basisRef is the delegation decision — else refuse not-authorized.
  4. runtimeObservations: if either field is null, result is unresolved with reason unknown-effects (charter: report reconciliation-required rather than simulate success). If originalExecutionStopped is false or effectsReconciled is false, refuse unsafe-replacement.
  5. On any outcome, the output's selection.assignmentRef is echoed unchanged and the bundle bytes are never written (A6, A7).

Exit class for unresolved: 3 (it is a refusal to affirm), with result: "unresolved" in JSON and text so it cannot be read as a permission refusal. Requester model in increment 1 is agent-only (D-G).

9. Output and exit classes

  • Text (default) and --json must be generated from the same result object; the text renderer is a pure function of the JSON. First line of both, always: SYNTHETIC PREVIEW — NO LIVE EFFECTS (text) / "disclaimer": "SYNTHETIC PREVIEW — NO LIVE EFFECTS" (JSON), including on every refusal and on exit 2/4. REVIEW's phrasing "preview: no live registrations or permission grants" (d4696d09:cand/REVIEW.md:126) is emitted as the second line.
  • JSON result shape (closed): {disclaimer, bundleVersion, result: allowed | refused | unresolved, reason: <code>, rule: <rule-name>, selection: {agentId, projectId, workspaceId, assignmentRef}, operation, consulted: [<recordRef | registryRef>], proposal: null | {result, reason, rule, selectedAssignmentRef}}. No payload bodies, no paths other than the requested target, no input snippets, no timestamps, no random ids, keys sorted.
  • Reason codes: the RUNTIME closed set (d4696d09:cand/RUNTIME.md:97: allowed, invalid-request, not-authorized, stale-revision, missing-state, already-active, control-conflict, retired, unsupported-capability, audit-unavailable, unknown-effects, unsafe-replacement, request-id-conflict, expired) plus inspector-only codes unsupported-kind, unresolved, io-failure. Inspector-only codes are documented as not being runtime API codes (charter).
  • Exit classes: 0 allowed; 2 malformed input (parse, shape, bounds, graph incoherence, unsupported kind/operation); 3 simulated refusal or unresolved; 4 I/O failure (missing file, unreadable, not a regular file). Exit 4 output names the path as given on argv and nothing else; no bootstrap, no config read, no directory creation (A8).
  • Errors from strict-json.mjs report byte offset and error class only, never the surrounding bytes.

10. Supported record kinds and operations

Supported kinds in increment 1: agent-definition, project, workspace, registration, mission, task, assignment, decision. The remaining candidate kinds refuse unsupported-kind (exit 2) when present in records, rather than being ignored. Operation vocabulary is the candidate's 29-name enum (d4696d09:cand/RUNTIME.md:55); a name outside it refuses invalid-request. Operations whose semantics need runtime state the inspector cannot model (stop, durability, audit) are accepted as names for permission intersection only; the output states "scope": "permission-only" for them so a 0 is not read as an executability claim.

11. Fixtures and the first demonstration

scripts/foundation/fixtures/demo.bundle.json implements the charter's scenario: project P1 with workspaces W1 and W2; project P2; agent A registered in P1 and W1 only; one mission, one task in W1, one selected assignment for A; agent B registered in P2 with a delegation decision that does not cover assignment.change. Derived bundles (same records, different selection or proposal) cover:

Fixture Expectation
demo-read-w1 exit 0, allowed, file.read on docs/notes.md
demo-read-w2 exit 3, not-authorized, rule workspace-registration-missing, no W2 payloads in output
demo-cross-project-exec exit 3, not-authorized, cross-project selection
demo-reassign-message-only exit 3, not-authorized, message-is-not-authority, selection unchanged
demo-reassign-no-observations exit 3, unresolved, unknown-effects
neg-duplicate-identity, neg-stale-ref, neg-missing-parent, neg-cycle-task, neg-cycle-mission, neg-fork-supersedes exit 2, named rule
neg-unknown-key, neg-dup-json-key, neg-bad-utf8.bin, neg-bad-path exit 2, before any permission text
neg-empty-ops-layer exit 3, explicit empty = none
neg-union-two-assignments exit 3, second assignment does not widen

Fixture digests are generated by a checked-in helper (node scripts/foundation/canonical.mjs --digest <file>) and the suite fails if a stored digest disagrees with a recomputed one, so fixtures cannot drift silently.

12. Test harness (isolated, non-effect, privacy)

scripts/test-foundation.sh, same shape as scripts/test-config.sh:

  • Runs under env -i PATH=… HOME=<mktemp> NO_COLOR=1 node …; MOSAIC_CONFIG unset; no ~/.config/mosaic-dev or data root exists in the sandbox. A canary HOME/.config/mosaic-dev/config.json is absent and the test asserts it stays absent (no bootstrap on missing input, A8).
  • Non-effect: before and after every CLI invocation, find <sandbox> -newer <stamp> must be empty and sha256sum of each bundle must be unchanged (A6). Also asserts no .pi, roles, or state directory appears under the sandbox.
  • Privacy: fixture payloads contain a canary string (CANARY-DO-NOT-PRINT-<n>) in every unrelated record body and in W2's payload; every stdout/stderr capture is grepped for it and must be clean (A2, charter diagnostics rule). Exit-2 parse errors are checked for absence of any input bytes beyond the byte offset.
  • Determinism: each fixture runs twice; outputs must be byte-identical; --json and text must agree on result/reason/rule (A8).
  • Golden files: scripts/foundation/fixtures/expected/<name>.{txt,json}, compared exactly.
  • Units (node --test scripts/foundation/*.test.mjs): canonicalizer vectors and out-of-domain refusals; strict-json duplicate key, depth, size, fatal UTF-8; resolver graph rules one per test; permission intersection tables (empty=none, null=whole root, component-prefix, no union); proposal rules.
  • Cross-check (optional, skipped visibly when Python jsonschema is absent): python3 cand/check.py over each fixture's records to confirm the hand-rolled validator has not drifted from the candidate schema.
  • The suite does not touch Dewey's extension installation, ~/.mosaic, the network, or any engine/process launch. It states this in its header. The existing required suites remain untouched and must stay green.

13. Unresolved decisions for the frozen charter

Id Decision Recommendation
D-A Language: Node ESM vs Python stdlib Node ESM, zero deps (§3)
D-B Directory layout before M20 scripts/foundation/ with owner headers, no packages/ (§4)
D-C Exit class for graph incoherence (cycle, stale, duplicate) 2 (malformed input), not 3, so it cannot be mistaken for a policy refusal
D-D Taskless operation set {work.read, file.read} exactly; work.propose stays assigned-only
D-E Registry content shapes for scope-role / agent-policy / project-policy {restrictions} only, declared simulation-only in the charter; candidate schema does not define these
D-F Simulated execution layer selection.launchRestrictions, narrowing-only, default null
D-G Requester model in proposal mode agent-only (agentId + registration); no human/operator principal in increment 1
D-H Wrapper naming scripts/foundation-inspect.mjs, no .sh shim
D-I Delegation enforcement depth require delegatedOperations ∋ assignment.change and subject coverage; do not model delegation chains
D-J Whether bundles carry revision history permit either head-only or contiguous full history per identity; forbid partial history

One further point the charter should settle explicitly: whether a 0 result for stop/durability/audit-class operations is acceptable at all in increment 1, or whether those names should refuse unsupported-capability outright. My recommendation is the latter for execution.stop-class names, to keep A9's "understands the limitation" honest.

14. Feasibility verdict

Feasible as a bounded increment with no new dependencies, no package migration, and no runtime coupling. Estimated size: three pure modules of a few hundred lines each, one CLI wrapper, roughly twenty fixtures, one bash suite, and unit tests. The main risk is drift between the hand-rolled validator and the candidate schema; the Python cross-check in §12 and the enumerated set-valued field list in §6.1 are the mitigations. Nothing here weakens the accepted semantics; where the candidate is silent (D-E, D-F), the proposal is narrowing-only and simulation-labelled.

This note is evidence for darkwing's charter. It is not approval, and it authorizes no implementation.