Plugin UI contracts 0.2.0 — validation report
Evidence record for the SDK preset-validation scope increment (issue #62; design .claude/deep-review/2026-09-19-issue62-sdk-validation-scope-design.md, committed on the branch before any implementation, with its pre-committed acceptance rule §7). The 0.1.1 increment’s evidence record remains in standards/plugin-ui/0.1.1/validation-report.md, digest-frozen with its tree.
What moved
plugin-ui 0.1.1 → 0.2.0 (MINOR: a tightening — packages with unreferenced preset assets, valid under 0.1.1, are refused; the batch also carries the additive lane-1 errata, and a batch’s bump follows its highest change class). Bump mechanics per standards/GOVERNANCE.md: copy-never-move, new corpus rows cite standards/plugin-ui/0.1.1/<file> as source, digests moved only by benchweave.standards repin (4 rows re-pinned), 0.1.1 tree untouched. Schema shapes are unchanged — only $id roots, contract_version consts and titles moved (verified: byte-diff against 0.1.1 shows version strings only). The behavior rides the validator (src/benchweave/presentation/contracts.py, vendored byte-for-byte) plus SDK CLI surface.
Two gaps closed (issue #62):
- Lane-1 descriptor envelopes.
validate_presetgains keyword-onlyaction_id: an explicit id wins (one absent from the descriptor is refused asunresolved_referenceonpreset.action, never silently skipped); the default resolves the action from the preset’s settings-schema identity via the exportedresolve_preset_action— exact, not heuristic, because lane 2 requires the settings-schema$idto equal the bound action’s corpus input-schema$id(identity_mismatchotherwise), so every preset that could pass lane 2 resolves to its own action, and corpus input-schema$ids are urn-per-action. The shared_preset_envelope_findingsapplies, AND-wise and with codeinvalid_settings, the canonical corpus action input schema and the descriptor action’sinput_constraints; lane 2’s former inline copy of those checks is deleted in favor of the shared helper. A custom-$idsettings schema applies no envelope by design (no action inference) — the check-preset success message prints the loud negative (no descriptor envelope applied … pass --action to force one), and--actionforces a named envelope.benchweave_sdk.presentationexportsresolve_preset_action(the same resolver the enforcement uses, so the CLI note cannot disagree with what was applied). - Unreferenced / declared-unlisted presets.
validate_presentationvalidates every preset a configuration target declares (the author wired it by declaring it; binding exposure is a UI choice; a preset two bindings list is validated once — finding de-duplication on multi-binding packages), and refuses a preset-shaped asset no target declares with the newunreferenced_presetfinding rather than guessing its wiring. The probe is bounded: assets are already read and digested,parse_documentcaps at 256 KiB, and only documents carryingcontract_versionplussettingsandsettings_schemacount as preset-shaped. The binding loop keeps only membership and kind checks (a binding naming a preset the target does not declare still yieldsunresolved_reference, existing path).
Honest residual (stated up front in the design §0): lane 1 still applies no descriptor envelope for plugins whose authored settings schema deliberately carries a non-corpus $id — for those the default invocation degrades loudly and full envelope coverage remains lane 2’s job (or the explicit flag).
sim_scope re-versions to 0.2.0 (ui/manifest.json, presentation.json, binding-catalogue.json, both presets; settings-schema bytes unchanged — OTDP-side — so its digest pin is stable; descriptor untouched). Docs: the device-developer guide’s lane text re-scoped (both lanes enforce the envelope; identity resolution; the orphan rule), the sim_scope README’s authored-envelopes paragraph moved to both-lane enforcement, the project index and the compatibility matrix follow the version. The SDK repo (branch feat/issue62-plugin-ui-020, commits f923179 + 67dcb69) vendors the tree single-active-version, adds --action + the notes, loads plugin-ui at 0.2.0, and keeps benchweave-sdk’s own version at 0.0.2 (the behavior rides the next release train; precedent f5e05fa).
RED (pre-implementation, commit d6b171f; submodule at the pre-change pointer)
All lane behavior tests main-side, per design §5. 35 collected / 11 failed / 0 errors (junitxml):
- the six
should_refuse=Truecensus rows (range_v25.0 / 0.0005 / 10.001 / 0.0009,offset_v100.0 / −10.001) each failassert lane1_exit != 0— lane 1 exited 0 on all six; test_l2_descriptor_constraints_tighter_than_corpus_still_refuse— the added narrowed-descriptor lane-1 assertion fails (assert 0 != 0);test_l1_default_resolves_action_by_schema_identity—assert 0 != 0;test_l1_explicit_action_flag_forces_envelope—TypeError: validate_preset() got an unexpected keyword argument 'action_id';test_l2e_unreferenced_preset_asset_refused—assert 0 != 0(check-ui exited 0 on the envelope-violating orphan);test_l2f_target_declared_unlisted_preset_validated—assert 0 != 0(the violating declared-unlisted preset was never validated).
The optionality control (test_l1_custom_settings_schema_applies_no_envelope_loudly) is green in both worlds by design — it pins the no-over-tightening requirement (design metric C); no RED exists for it by construction and none was faked.
GREEN
- Census + lanes (
tests/sdk/test_sim_scope_presets.py): 35/0 at the synced submodule. Metric A: 6/6 True rows refuse; 3/3 endpoint False rows (range_v10.0 / 0.001,offset_v−10.0) and 2/2 shipped presets still exit 0. Metric B: the envelope-violating orphan refuses withunreferenced_presetand WITHOUTinvalid_settings(refused, not settings-validated); the unmodified package and the valid extra target-declared unlisted preset (metric B2) both exit 0. Metric C: the custom-$idrow exits 0 with no envelope finding, before and after. - Unit presentation + architecture suites: 67/0.
tests/standards+tests/contract/test_baseline.py: green after the matrix regeneration and the SDK sync (the pre-sync run’stest_real_tree_is_cleansdk_version_mismatchand the two stale-matrix failures were the expected mid-sequence window, matching the OTDP 0.2.0 train’s disclosed shape).- Full cold suite: 1290 collected / 1 failed — the single failure is
test_submodule_head_matches_the_recorded_gitlink, which compares the submodule HEAD against the COMMITTED gitlink and therefore closes exactly when the pointer commit lands (this commit’s successor), the same closure the 0.1.1 and OTDP 0.2.0 trains recorded.
Gates
uv run ruff check . clean; bare config-driven uv run mypy clean (177 source files, includes packages/sdk/src); make check-sdk-standards clean (sync check + matrix --check at the advanced pointer); benchweave.standards versions: standard plugin-ui@0.2.0 = sdk lock plugin-ui@0.2.0. Falsifier sweep (design §4): no live plugin-ui/0.1.1 path or 0.1.1 contract literal remains outside the frozen trees, the frozen corpus rows, and the design records; the SDK repo’s one live deep link was swept with 67dcb69. SDK repo: ruff clean, strict mypy clean, pytest 10/0, sync-standards --check agrees. Fixture-key note: the registry suites need the gitignored signing keys (fixtures/registry/keys/*.pem) copied into a worktree; without them 8 registry tests fail environmentally (verified the same tests pass in the maintainer’s main checkout at the base commit cc501b2).