Plugin UI contracts 0.3.0 — validation report
Evidence record for the digital_lanes train’s slice 1 (issue #244; design .claude/deep-review/2026-09-29-issue244-digital-lanes-design.md, committed on the branch before any acceptance test landed, with its pre-committed acceptance rules §6 — amended before the record’s first commit per the governor ruling of 2026-09-29). The 0.2.0 increment’s evidence record remains in standards/plugin-ui/0.2.0/validation-report.md, digest-frozen with its tree. The same train bumps plugin-ui-preview 0.1.1 → 0.2.0 (one increment, one merge, per GOVERNANCE).
What moved
plugin-ui 0.2.0 → 0.3.0 and plugin-ui-preview 0.1.1 → 0.2.0, both MINOR: a new enum member plus new per-kind grammar plus normative renderer behaviour (slice 2’s edge-preserving decimation) is new normative capability. Copy, never move: the new corpus rows cite the predecessor paths as source, digests moved only by benchweave.standards repin (2 rows re-pinned after the grammar edits; 6 new rows total), predecessor trees untouched.
Grammar (ui-manifest.schema.json $defs/plot + the preview wire’s plot_view): kind gains digital_lanes; per-kind caps via allOf if/then — 64 y/channel_hints/channels for the lanes kind, 16 for the analog kinds (the caps live ONLY in the kind branches: a base maxItems would intersect, not relax, so a 17-lane lanes plot would have been refused — caught by the A1.1 boundary vector); channel_hints items for lanes carry visible only (color_role refused); lane_groups (≤16, members 2..64, radix hex default / decimal opt-in) and decoder_lanes (≤8, sources 1..4, binding_id naming a dataset binding). The analog kinds cannot declare the lanes fields.
Validator semantics (src/benchweave/presentation/contracts.py): the digital_lanes branch of _plot_findings refuses non-dataset targets and non-string/scalar y axes (invalid_plot), resolves group members, decoder sources and decoder bindings against the plot’s y and the page’s bindings (unresolved_reference), enforces one identifier space across y ∪ group ids ∪ decoder lane ids (invalid_document) and one group claim per channel (invalid_plot). The ≥2-member floor is a SCHEMA shape rule (member_ids minItems: 2 → invalid_document): item counts are the schema’s job in the corpus, and _checked_document fails closed on schema findings, so a semantic floor would be unreachable — the design’s §1.2/A1.1 named invalid_plot there; corrected per the prose-defers-to- machine-source rule (see the design record’s §1.2 resolution note).
Version sweep: all THREE version-literal sites in the corpus-owned code row move 0.2.0 → 0.3.0 (the path-shaped SCHEMA_ROOT literal and the two bare contract_version consts); the zero-literal register row’s expected_sites stays 3 (three sites move, count unchanged), its reason text records the motion and the fired D2 trigger.
Range motion (coordinator ruling VR-43; OBLIGATION-ONLY reference): the governor split the design’s “slide narrow, both” claim. plugin-ui slides narrow >=0.2.0,<0.3.0 → >=0.3.0,<0.4.0 — FORCED by the one-live-object rule (the normative bundle row lists contracts.py in every carried version’s row, so carrying 0.2.0 beside 0.3.0 would serve 0.3.0 code bytes under a 0.2.0 pin) plus the manifest’s F1/D2 policy note; below the two-release floor by design. plugin-ui-preview moves floor-compliant >=0.1.0,<0.2.0 → >=0.1.1,<0.3.0 (carries 0.1.1 + 0.2.0, the two most recent released versions; a narrow slide would drop 0.1.1 and violate the support-window floor). 0.1.0 leaves the window; 0.2.0-pinned plugin-ui packages re-stamp to 0.3.0.
Migration notes (SM-5; grandfathering ended, both bumps judged): one per standard, from-predecessor, repo-relative pointers in the dependency-policy block. The D2 reopen trigger named in the zero-literal register (“the first plugin-ui bump after the arc”) FIRED with this bump; D2 stays open with narrower reopen conditions (the design record’s D-1).
Evidence (design §6, slice 1)
- A1.1 vectors. Exactly 12 valid vectors (minimal 2-lane; the 64-lane boundary exactly; groups hex/decimal/collapsed; decoder lanes with and without settings; visible-only hints; integer x; two groups; combined) and 14 invalid vectors naming their codes (
tests/unit/test_presentation_digital_lanes.py). The reproduced refusal delta: the frozen 0.2.0 schema refuses a digital_lanes plot at the kind enum while 0.3.0 admits the same plot — pinned as a permanent test (#62 evidence shape). RED arm (PR body): with the kind enum neutralized to the predecessor’s, 22 of 28 vector tests fail with kind-enuminvalid_document. - A1.2 semantic RED. With the two
elif lanesbranches dead (the basenot lanesguard kept), 9 semantic vectors pass wrongly — non-string type, group-outside-y and decoder-unresolved among them; restored, all 28 pass. - Const-sweep RED. With the two bare consts reverted to “0.2.0” (one-site-sweep simulation), 26 of 28 fail with
unsupported_version. - A1.3 copy-never-move. 0.2.0’s and 0.1.1’s corpus rows and directory bytes unchanged (frozen-row tests + repin verify); the new rows’ digests verified against file bytes after the repin.
- A1.4 gates. SM-5 RED arm: removing the plugin-ui migration-note row fires
migration_note_missing:. Zero-literal gate green over the edited register row. Train-window clear (last version-dir creations 2026-09-19 / 2026-09-21 vs landing 2026-09-29). Served set after the motions: plugin-ui {0.3.0}, plugin-ui-preview {0.1.1, 0.2.0}.
Honest interim
Slice 2 landed the renderer: DigitalLanesPlot draws the kind (identity is lane position; the four states are structurally distinct geometry), and the preview serves and draws lanes views over a labelled synthetic pattern. The remaining interim is decoder-lane annotations (slice 3) — declared decoder lanes render a visible awaiting-render note naming the decoders, never a silent blank. The wire grammar is final in 0.3.0 / 0.2.0: no second bump results from this issue.