# Plugin UI contracts 0.3.1 -- validation report

Evidence record for the commissioned-preset firmware gate (issue \#444; design `.claude/deep-review/2026-10-12-issue444-commissioned-preset-firmware-design.md`, committed on the branch before any acceptance test landed, with its pre-committed acceptance rule §7). The 0.3.0 increment's evidence record remains in `standards/plugin-ui/0.3.0/validation-report.md`, digest-frozen with its tree.


# What moved

plugin-ui 0.3.0 → 0.3.1, PATCH (additive machine errata -- the backwards-compatible row; the mirror image of the \#62 MINOR, which tightened valid→invalid where this loosens invalid→valid with nothing removed or retyped). Copy, never move: 4 new corpus rows citing `standards/plugin-ui/0.3.0/*` as `source`, digests moved only by `benchweave.standards repin`; the predecessor tree is untouched and its rows verified frozen by the same command.

Validator semantics (`src/benchweave/presentation/contracts.py`, `validate_preset`): the firmware gate becomes policy-aware as three rules -- (1) the preset's own `supported_firmware` list always applies to a supplied value; (2) `firmware is None` is admissible only under `firmware_policy: "commissioned"`; (3) a descriptor-DECLARED list is enforced regardless of policy (None is not a member). The unreachable `"any"` arm is removed (`firmware_policy` is `["listed", "commissioned"]` in every OTDP schema version in this tree, and the gate only sees OTDP-validated descriptors). The refusal is byte-identical where it fires: code `incompatible_firmware`, path `preset.supported_firmware`, message "A known firmware compatible with the descriptor and preset is required" -- so every consumer, including the standalone apply path's conflict mapping, is unchanged.

Version sweep: all THREE version-literal sites in the corpus-owned code row move 0.3.0 → 0.3.1 (the path-shaped `SCHEMA_ROOT` literal and the two bare `contract_version` consts); the zero-literal register row's `expected_sites` stays 3, its reason text records the motion.

Range motion (G-3 territory; coordinator ruling referenced in the PR body at landing): the served range slides narrow `>=0.3.0,<0.4.0` → `>=0.3.1,<0.4.0`, forced by the one-live-object rule (the bundle lists `contracts.py` in every carried version's row). 0.3.0 pins re-stamp to 0.3.1. In-tree re-stamps: the sim_scope presentation documents (5 files: envelope, manifest, catalogue, 2 presets) with the digest chain re-derived (preset bytes → manifest asset pins → presentation manifest pin; the descriptor and settings-schema bytes are unchanged), and the SDK fixture plugins (setpoint, binary_frames) plus the scaffold byte-parity fixtures (regenerated through the release-cut regen arm at SDK 0.11.0).

SDK-side riding pieces: the `benchweave_sdk_server` authoring helper (`_validated_preset`) gains the commissioned arm -- an empty firmware candidate list under commissioned policy validates once with `firmware=None` instead of refusing "no firmware to judge the preset against" (that refusal keeps its force for non-commissioned policies); `_preset_firmwares`' docstring records the division. The apply path (`seam.py::_op_preset_apply`) is unchanged by design: it refuses `not_ready` before the gate when no firmware is established, and with an established firmware rule 1 governs.


# Evidence (design §7, pre-committed acceptance)

- **Commissioned-admit arms, RED pre-fix (gateway, junitxml counts).** `tests/unit/test_presentation_presets.py`: tests=22 failures=2 -- `test_commissioned_policy_admits_missing_firmware` and `test_commissioned_policy_admits_preset_listed_firmware` each failed with exactly `Finding(code='incompatible_firmware', path='preset.supported_firmware', message='A known firmware compatible with the descriptor and preset is required')` (the issue's quoted refusal, reproduced synthetically against a commissioned fixture descriptor carrying the corpus's reference-can pattern). Post-fix the same file is tests=22 failures=0.
- **RED, SDK twin arms.** `tests/test_presentation_cli.py` + `tests/server/test_authoring.py`: tests=43 failures=3 pre-fix (the two twin admit arms plus `test_preset_put_commissioned_project_validates_without_a_firmware_source`, which failed at `_preset_put` with "the preset would not validate: 1 finding(s)" -- the same gate finding). Post-fix tests=67 failures=0 over the three files including the apply-path pins.
- **G3 RED-sanity (acceptance ii).** With ONLY the gate restructure reverted (cp backup, version sweep left in place), the gateway preset file returns to tests=22 failures=2 with exactly the two admit arms failing; restored, tests=22 failures=0. The proof is not vacuous.
- **Negative controls (acceptance iii).** C2 (listed-policy mismatch) stays pinned by the untouched `test_requires_known_compatible_firmware` \[None/2.0\]. C3 (commissioned preset-list mismatch), C4 (listed + None), and the two declared-list enforcement arms (rule 3) each refuse with the exact `(code, path, message)` triple, in both repos, before and after.
- **Widening-only proof (acceptance iv).** `test_listed_policy_corpus_admits_with_no_findings[fast-survey/low-noise-pair]` validates the shipped sim_scope corpus (N=2) through the live gate with the corpus action catalogue resolved -- `report.findings == ()` before and after (the tuple was empty under the predecessor gate for exactly these documents and stays empty).
- **Copy-never-move (0.3.0's row set unchanged).** `repin` re-pinned exactly 4 rows, all `plugin-ui/0.3.1/*`; the frozen-row discipline verified the predecessor bytes in the same run. The standards-manifest keeps 0.3.0's migration-note entry beside 0.3.1's (both retained versions carry from-predecessor notes).
- **Gates.** Zero-literal gate green over gateway/plugins/docs/scripts (register row count stays 3); docs baseline refresh diff is exactly the project-index link swap (2 rows). `benchweave.standards check` and the `tests/standards/test_check` mirror/anchor lanes are red on this branch BY THE LANDING SPLIT: they compare a fresh export against the SDK submodule's lock, and the pointer does not move until the landing train's same-commit advance (issue \#408's rule). The same class covers the shipped-package sim_scope lanes in `tests/sdk/` that drive the submodule CLI against the re-stamped presets.
- **Real-consumer evidence.** The design's pre-fix measurement named the owner's commissioned consumer descriptor; it was not available to this build pass, so the mechanism ships on synthetic + corpus evidence per the design's UNDERPOWERED clause -- the issue stays open with the closing claim downgraded to "mechanism verified on fixtures; operator confirmation pending", and the owner's check run is the reopen evidence.


# Honest interim

None on the gate itself. Render-time firmware resolution for commissioned devices stays deferred (design D1): the read-only UI renders all structurally-valid presets and availability is decided at apply, where the established-firmware gate refuses before any write.


# Rider (2026-10-13): issue \#446 slice 2 -- plugin-authored profiles back UI targets

Rides the same 0.3.1 version (no second bump; one-live-object rule keeps a single served 0.3.1). Adds, on top of the \#444 record above:

- Checker (`validate_presentation`/`_target_findings`): a profile id declared by two documents in the served set with divergent rows yields finding `profile_shadow` on each target that binds it (path `targets.<id>`, message names the id). Identical duplicates stay benign last-wins dedup -- byte-identical to 0.3.0 behavior on corpus-only sets (the PATCH class argument holds: no 0.3.0-valid set contains a divergent duplicate; the carried OTDP catalogs' profile rows are identical across versions, re-verified 2026-10-13 for `otdp.dc_psu/1.0.0` between 0.2.0/0.2.2).
- Composition guard (`merge_pinned_documents`): a pinned contract document whose own `$id` or an action-schema `$id` equals a served schema `$id` (corpus or sibling pinned) refuses with `DocumentError` code `pinned_id_shadow`. One shared enforcement point for the SDK helper and the gateway read side (design record fold-in addendum §14.3); the composition points themselves land with slice 1 → slice 2 ordering.
- Prose: this README's target/trust sections name the profile supply rule and both shadow refusals; OTDP `extension-contract.md` §2 names plugin-authored pinned catalogs and the `otdp.*` namespace rule (companion prose, no OTDP bump).

RED-first evidence (junitxml): tests=7 failures=5 errors=0 before the mechanism (the divergent-shadow arm showed `ValidationReport(findings=())` valid=True -- the silent last-wins shadow; 4 merge-guard arms ImportError on the missing symbol), tests=7 failures=0 errors=0 after. Full arms: tests/unit/test_presentation_profiles.py.
