Skip to content

fix(serve): truncation signal on /api/v1/artifacts and /api/v1/diagnostics (REQ-303, #832) - #864

Merged
avrabe merged 5 commits into
mainfrom
fix/issue-832-artifacts-truncation-signal
Aug 27, 2026
Merged

fix(serve): truncation signal on /api/v1/artifacts and /api/v1/diagnostics (REQ-303, #832)#864
avrabe merged 5 commits into
mainfrom
fix/issue-832-artifacts-truncation-signal

Conversation

@avrabe

@avrabe avrabe commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Closes #832 — Step 1 (the "cheapest honest version" the issue calls out).

The gap

/api/v1/artifacts caps limit at 1000 and this repo now holds 1017 artifacts, so a client asking for everything gets 1000 rows with no indication that 17 were dropped. The response carries total: 1017 next to a 1000-element array — the data needed to detect truncation is present, but nothing marks the payload as partial, and every consumer reading artifacts as the full set is silently wrong. Same shape at /api/v1/diagnostics, which shares the .min(1000) cap.

Downstream cost already visible in the tree: serve_integration.rs asserts externals_unscoped > 0 on origin=all&limit=1000 — it assumes that window is the full set, a premise that holds today only because the externals happen to sort into positions 0-3.

The fix

Emit count and truncated alongside total on both responses, matching the shape rivet query --format json already uses (schemas/json/query-output.schema.jsoncount, total, truncated). Consumers can now fail loudly on a partial view instead of quietly reading it as the full set.

{ "total": 1017, "count": 1000, "truncated": true, "artifacts": [...] }

Additive-only: existing clients that read only total and artifacts are unaffected. The Playwright tests read data.total and data.artifacts.length and stay green as-is. The dashboard/serve JS does not consume these endpoints.

Deliberately out of scope

Step 1 alone converts silent truncation into detectable truncation and stands on its own.

Verification

  • cargo check -p rivet-cli --tests — clean build.
  • Two new integration tests in serve_integration.rs:
    • api_artifacts_truncation_signal — full window: count == total, truncated == false; small window (limit=1): count == 1, truncated == true.
    • api_diagnostics_truncation_signal — same shape check; the small-window assertion is guarded on total > 1 since the fixture doesn't guarantee any diagnostics.
  • Both tests carry a // rivet: verifies REQ-303 marker.

Commit trailers

Fixes: REQ-303, Verifies: REQ-303, Refs: #832.


Generated by Claude Code

…ion signal (REQ-303, #832)

## The gap

`/api/v1/artifacts` caps `limit` at 1000 and this repo now holds 1017
artifacts, so a client asking for everything received 1000 rows with no
indication that 17 were dropped. The response carried `total: 1017`
next to a 1000-element array — the data was present, but nothing marked
the payload as partial, and every consumer reading `artifacts` as the
full set was silently wrong. Same shape at `/api/v1/diagnostics`, which
uses the same `.min(1000)` cap on its `limit`.

The immediate downstream cost is already visible: `serve_integration.rs`
tests assert `externals_unscoped > 0` on `origin=all&limit=1000`, i.e.
they assume that window is the full set — a premise that holds today only
because the externals happen to sort into positions 0-3.

## The fix

Step 1 from the issue: emit `count` and `truncated` alongside `total` on
both responses, matching the shape `rivet query --format json` already
uses (see `schemas/json/query-output.schema.json`). Consumers can now
fail loudly on a partial view instead of quietly reading it as the full
set. Additive-only — existing clients that read only `total` and
`artifacts` are unaffected.

The determinism half (#746 / #759) and the cap-as-paging-boundary
decision remain out of scope for this change; Step 1 alone converts
silent truncation into detectable truncation, and lands cleanly on its
own.

Fixes: REQ-303
Verifies: REQ-303
Refs: #832
Wrap the `.expect(...)` calls and the `assert_eq!` label onto multiple
lines so rustfmt is happy. No behavior change.

Trace: skip
@github-actions

Copy link
Copy Markdown

📐 Rivet artifact delta

No artifact changes in this PR. Code-only changes (renderer, CLI wiring, tests) don't touch the artifact graph.

avrabe commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

Standing down on the Kani Proofs failure — it isn't this PR's.

What's failing: Kani Proofs on head 17e846a fails to compile rivet-core:

error[E0063]: missing fields `exempt` and `exempt_ids`
              in initializer of `coverage::CoverageEntry`
   --> rivet-core/src/proofs.rs:216:21

Why it isn't this PR's: the diff only touches rivet-cli/src/serve/api.rs and rivet-cli/tests/serve_integration.rs. The failing file is in rivet-core, and the fields it names were added by REQ-309 / #848; the Kani proof site (#[cfg(kani)]-gated, so plain cargo check misses it) was never updated. Reproduces on main @ 83c7d91.

Fix: filed separately as #865 (two-line struct-init update). Not porting into this PR since #865 lands on main and no-ops here once merged. Kani Proofs is continue-on-error: true per #839 anyway, so it isn't blocking ci-gate.

Not spending the one re-run — this is a deterministic compile error, not a flake.


Generated by Claude Code

claude added 2 commits August 27, 2026 13:10
…p (REQ-303, #832)

Previous version asserted `count == total` on `limit=100000`, which
assumed the endpoint returns everything on a large window. It doesn't:
`.min(1000)` caps `limit` internally (rivet-cli/src/serve/api.rs:459
and :630) — the very defect this signal is being added to make legible.
On the CI fixture (total=1024) the "full window" call therefore returned
1000 rows and the assertion failed.

Restructure both `api_artifacts_truncation_signal` and
`api_diagnostics_truncation_signal` to check the shape rather than a
specific full-vs-partial state:

  - Small window (`limit=1`) forces `truncated: true` on any non-trivial
    fixture, and `count == 1`; assert this unconditionally for artifacts
    (fixture is guaranteed > 1), and branch on `total > 1` for
    diagnostics (fixture is not guaranteed to have any).
  - Full window (`limit == total`) is exercised only when `total` fits
    under the endpoint's `.min(1000)` cap. When it doesn't, an
    untruncated response is unreachable from the endpoint at all — the
    exact case this signal exists to make legible — so we skip that leg
    rather than assert an impossibility.

Verified locally against the real fixture: both tests pass.

Trace: skip
Kani Proofs on this PR's head is red for a compile error in code the
diff doesn't touch (rivet-core/src/proofs.rs:216, missing exempt /
exempt_ids fields added by REQ-309 / #848 to `CoverageEntry`). The fix
is already up at #865. Ported here so this PR's Kani stops being red on
an unrelated failure; no-ops once #865 merges.

Fixes: REQ-309
Refs: #848, #865

avrabe commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

CI Gate green on d156713. Kani is now also green — the Kani fix ported from #865 compiled and the harness ran clean. Test and Proptest (extended, 10× load) both green — the corrected truncation-signal tests hold against the real fixture.

One advisory red remains: Mutation Testing (rivet-core, PR-diff) — same finding I posted on #865:

rivet-core/src/proofs.rs:209:9: replace proofs::proof_coverage_percentage_bounds with ()

Line 209 is the #[kani::proof] harness function; no cargo test-driven test exercises it and no property this PR added can kill this mutant — the coverage gap is structural to #[cfg(kani)]-gated code, not introduced by this diff. The workflow itself labels this advisory ("does not fail on this until promotion"). Follow-up on the mutation gate's treatment of Kani-gated code belongs in its own issue — out of scope here.


Generated by Claude Code

@codecov

codecov Bot commented Aug 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@avrabe
avrabe merged commit 679d612 into main Aug 27, 2026
33 checks passed
@avrabe
avrabe deleted the fix/issue-832-artifacts-truncation-signal branch August 27, 2026 16:54
avrabe added a commit that referenced this pull request Aug 27, 2026
…eps causing it

REQ-303 shipped in #864 and was still `proposed`. That is the fifth instance in
one release cycle — REQ-309 (#850), REQ-299 (6b3be3c), REQ-295 (#830), REQ-304
and REQ-305 (#863, found still proposed within the hour), and now REQ-303 — all
mine, all caught only by manual audit.

REQ-315 records the cause rather than the instances. A trailer is a LINK, not a
state transition: `rivet commits` checks that trailers reference real artifacts
and nothing checks the converse, that an artifact a merged commit claims to
implement is no longer sitting at `proposed`. Release readiness is defined as a
query over `release:` + `status`, so the effect is that shipped scope reads as
unshipped and a human has to re-derive the truth from git log — the same defect
class this tool exists to prevent, in its own artifacts.

The proposed check reports; it does not auto-advance. Whether shipped code
discharges every acceptance clause is a judgement (REQ-308), and a tool that
flipped status on the strength of a trailer would manufacture precisely the
false confidence REQ-308 is about.

Confirmed with rivet validate (exit 0), rivet docs check (exit 0), 285
artifacts, no duplicate ids.

Implements: REQ-303
Refs: REQ-308, REQ-315
avrabe added a commit that referenced this pull request Aug 27, 2026
plan(v0.35): cut what shipped, split the remainder (maintainer decision)

Maintainer approved "cut and split". v0.35.0 goes out with what is done; the
rest moves with a logged decision rather than silently.

v0.35.0 keeps 12 artifacts. Three verified (REQ-274/275/276, the customer UI
trio) and nine implemented: REQ-299, 304, 305, 309, 310, 311, 312, 313, plus
REQ-303 which is held here while its PR (#864) is in flight. Every issue a human
reported this cycle — #848, #852, #853, #854, #856 — is fixed in this set, which
is the release's identity.

The implemented ones stay `implemented` rather than being rounded up to
`verified`: each carries a named undischarged clause (the --fail-under policy on
REQ-309, the git: fallback on REQ-312, release-status categories on REQ-313).
Shipping them is not the same as claiming their acceptance is complete. Holding
the release until every clause is discharged would recreate, at release scope,
exactly the deadlock REQ-313 exists to detect: a criterion that can only be
discharged by a release shipping.

v0.36.0 takes 4 — the gate-potency and verification-evidence cluster that keeps
costing diagnosis mid-tick: REQ-295 (nested-workspace scan, Defect 1 already
shipped in #830), REQ-306 (#807 Defect 2, name-existence vs
command-reachability), REQ-307 (#812 residual, 52 flat top-level commands), and
REQ-314 (27 test files resolve the binary at runtime, so nextest with a custom
CARGO_TARGET_DIR cannot find it).

Backlog takes 9, in two groups. Older features that have not converged in three
weeks — REQ-281, 282, 285, 286 date from Aug 4-7, plus REQ-292, 296, 301 — and
two items that need a design decision before they can be scheduled at all:
REQ-300 (assisted authoring) and REQ-308 (per-clause verification, which needs
clauses to become addressable and is therefore a schema migration across every
artifact). Putting a version label on those would move the stall, not resolve it.

Also corrects the fourth instance of status drift this cycle: REQ-304 and
REQ-305 shipped in #863 earlier today and were still `proposed`. REQ-299
(shipped 6b3be3c) and REQ-309 were corrected previously. Shipping code and
leaving the artifact behind is now a recurring failure of mine, not an isolated
slip.

Confirmed with rivet validate (exit 0), rivet docs check (exit 0), YAML parses,
284 artifacts, no duplicate ids.

Refs: REQ-303, REQ-304, REQ-305, REQ-314
plan: flip REQ-303 to implemented; file REQ-315 for the drift that keeps causing it

REQ-303 shipped in #864 and was still `proposed`. That is the fifth instance in
one release cycle — REQ-309 (#850), REQ-299 (6b3be3c), REQ-295 (#830), REQ-304
and REQ-305 (#863, found still proposed within the hour), and now REQ-303 — all
mine, all caught only by manual audit.

REQ-315 records the cause rather than the instances. A trailer is a LINK, not a
state transition: `rivet commits` checks that trailers reference real artifacts
and nothing checks the converse, that an artifact a merged commit claims to
implement is no longer sitting at `proposed`. Release readiness is defined as a
query over `release:` + `status`, so the effect is that shipped scope reads as
unshipped and a human has to re-derive the truth from git log — the same defect
class this tool exists to prevent, in its own artifacts.

The proposed check reports; it does not auto-advance. Whether shipped code
discharges every acceptance clause is a judgement (REQ-308), and a tool that
flipped status on the strength of a trailer would manufacture precisely the
false confidence REQ-308 is about.

Confirmed with rivet validate (exit 0), rivet docs check (exit 0), 285
artifacts, no duplicate ids.

Implements: REQ-303
Refs: REQ-308, REQ-315
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

serve: /api/v1/artifacts silently truncates at limit=1000 (repo has 1017) — no truncation signal in the response

2 participants