Skip to content

fix(artifacts): put release: on the plane rivet actually reads (#370) - #373

Merged
avrabe merged 1 commit into
mainfrom
fix/370-release-field-plane
Jul 30, 2026
Merged

fix(artifacts): put release: on the plane rivet actually reads (#370)#373
avrabe merged 1 commit into
mainfrom
fix/370-release-field-plane

Conversation

@avrabe

@avrabe avrabe commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Closes #370.

The defect

rivet has two field planes. release is a core field, read by the CLI query
surface — rivet list --release vX.Y is the release-planning view. Separately,
each artifact may carry a fields: bag for schema-declared custom keys (for
requirement: baseline, category, cited-source, priority,
upstream-ref). Writing release: into the bag is valid YAML, is tolerated by
the schema, and is completely invisible to the query.

- id: REQ-A                - id: REQ-B
  status: proposed           status: proposed
  release: v0.36.0           fields:
                               release: v0.23.0
  ^ rivet sees this                     ^ rivet does not

59 of 85 assignments were in the bag.

Measurement

A/B with one binary (rivet 0.28.0 local — note CI pins v0.4.3), same query,
origin/main @ 2b11af9 vs this branch:

before after
artifacts with a release 26 86
releases populated 18 29

The eleven that went from zero artifacts to non-zero are exactly the eleven
named in the issue — v0.13.0, v0.14.0, v0.15.0, v0.16.0, v0.17.0, v0.18.0,
v0.19.0, v0.20.0, v0.21.0, v0.31.0, v0.32.0. Every one is a shipped release
whose scope query answered "nothing to do".

(86 = 85 promoted/existing + 1 new artifact added by this PR.)

Why it stayed hidden

rivet diagnosed it correctly and 59 times: INFO: field 'release' is not defined in schema, naming the exact ids. Those lines sat under 276 unrelated ERROR:
lines
from the status-vocabulary defect tracked as #371.

A correct diagnostic buried under an unrelated noise floor is not a diagnostic.
So the remedy here is an exit code, not better prose.

The transform

Text-level, so comments and block scalars survive; semantically verified
per-artifact through PyYAML — each artifact's release value must appear on the
new plane, all its other fields must be unchanged, and the id set must be
identical. 58 in artifacts/requirements.yaml, 1 in
safety/stpa/rendering-analysis.yaml.

The gate

tools/check_release_plane.py, wired into the rivet-validate job — which
deliberately has no changes path filter, so it runs on every PR. Behind a
filter it would be skipped by exactly the artifact-light PR that reintroduces
the bug.

It parses rather than greps: release: inside a description: > block
scalar is prose at a deeper indent, so the obvious grep -nE '^ {6,}release: '
would red the build for an English sentence. Stdlib-only and fail-closed, same
rationale as check_human_scoped.py.

The self-test asserts counts, not just the exit code

exit 1 on the violation fixture is necessary for "reported the nested one"
but not sufficient — it is equally consistent with a checker that reports
every release: it sees, i.e. one that does not discriminate on plane at
all. So the self-test asserts (nested, visible).

Mutation-tested. Each fixture kills exactly one guard:

mutation reds
drop plane discrimination violation, block-scalar
drop block-scalar tracking block-scalar
drop "parsed zero artifacts" guard no-artifacts
drop "no sequence entries" guard unsupported

The fourth fixture exists because a mutation survived the first draft.
Deleting the parsed zero artifacts raise left all three original self-tests
green — making that guard an unexercised claim inside a checker whose entire
purpose is to distrust unexercised claims. release-plane-no-artifacts closes
it.

Scope note

This does not fix #371 (the 276 ERROR: lines). rivet validate was A/B'd
before and after at the same error count with identical error classes; the noise
floor is unchanged and tracked separately.

Also note #137: REQ-TSN-SYNTH-QBV-001 carries status: proposed with a stale
release: v0.23.0 that is now visible to rivet for the first time. It needs
re-scoping; that is deliberately not done here.

🤖 Generated with Claude Code

rivet has two field planes. `release` is a CORE field read by the CLI
query surface — `rivet list --release vX.Y` is the release-planning
view. Separately each artifact may carry a `fields:` bag for
schema-declared custom keys. Writing `release:` into the bag is valid
YAML, is tolerated by the schema, and is invisible to the query.

59 of 85 assignments were in the bag. Measured A/B with one binary
(rivet 0.28.0) over the same query:

  artifacts with a release   26 -> 86
  releases populated         18 -> 29

The eleven that went from ZERO artifacts to non-zero are exactly the
eleven named in #370: v0.13.0 through v0.21.0, v0.31.0, v0.32.0. Every
one of those is a SHIPPED release whose scope query answered "nothing to
do". That is the recurring shape — an operation that can produce
"nothing happened" rendering identically to one that worked.

The promotion was done by a text-level transform (so comments and block
scalars survive) with per-artifact semantic verification through PyYAML:
each artifact's release value must appear on the new plane, its other
fields must be unchanged, and the id set must be identical. 58 in
artifacts/requirements.yaml, 1 in safety/stpa/rendering-analysis.yaml.

Add tools/check_release_plane.py as the regression gate, wired into the
rivet-validate job (which deliberately has no `changes` filter, so it
runs on every PR). rivet DID diagnose this — `INFO: field 'release' is
not defined in schema`, 59 times, all correct and naming the exact ids.
They sat under 276 unrelated ERROR lines from #371. A correct diagnostic
under an unrelated noise floor is not a diagnostic, so the remedy is an
exit code rather than better prose.

The checker parses rather than greps: `release:` inside a
`description: >` block scalar is prose at a deeper indent, and the
obvious `grep -nE '^ {6,}release: '` would red the build for an English
sentence. Stdlib-only and fail-closed, same rationale as
check_human_scoped.py.

The self-test asserts finding COUNTS, not just the exit code: `exit 1`
on the violation fixture is equally consistent with a checker that
reports every `release:` it sees — one that does not discriminate on
plane at all. Four mutations (drop plane discrimination, drop
block-scalar tracking, drop either fail-closed guard) each red the
self-test, and each fixture kills exactly one guard.

The fourth fixture exists because a mutation survived the first draft:
deleting the "parsed zero artifacts" raise left all three original
self-tests green, making that guard an unexercised claim inside a
checker whose whole purpose is to distrust unexercised claims.

Closes #370.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Rivet verification gate

20/20 passed

count
Passed 20
Failed 0
Skipped (no steps) 0

Filter: (and (= type "feature") (or (has-tag "v093") (has-tag "v0100")))

Failed artifacts

(none)

Updated automatically by tools/post_verification_comment.py. Source of truth: artifacts/verification.yaml.

@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant