Skip to content

feat(validate): carry the validated ledger t on ValidateReport - #1757

Merged
bplatz merged 3 commits into
mainfrom
feat/validate-report-t
Sep 3, 2026
Merged

feat(validate): carry the validated ledger t on ValidateReport#1757
bplatz merged 3 commits into
mainfrom
feat/validate-report-t

Conversation

@bplatz

@bplatz bplatz commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Summary

The full-ledger SHACL validator already pins the snapshot it evaluates (to_t = view.t at the top of validate_view_inner), but the report it returned never said which t that was. A caller that pairs the results with other reads, such as record counts for a violation rate, had to read the handle's t before and after validate_ledger and only trust the pair when they agreed, because a commit landing between the validator's internal snapshot and the caller's own read silently breaks the pin.

This puts the t on the report so consumers stop reconstructing it from a second read.

Changes

  • ValidateReport gains pub t: i64, set from the same to_t at both construction sites (the empty-shapes early return and the final sorted report).
  • The serde summary picks the field up directly. The W3C renderings carry it on the report node as f:t (f = https://ns.flur.ee/db#), since sh:ValidationReport has no slot for a ledger time.
  • Server default JSON envelope adds "t". CLI table summary line now ends with checked at t=N.
  • Docs: docs/cli/validate.md and the /validate section of docs/api/endpoints.md show the field and explain reading any paired measurement at the same t.

Tests

  • Unit: all three rendered forms (summary JSON, JSON-LD, Turtle) carry the value; the Turtle round-trip test still parses.
  • API integration: report.t equals the ledger handle's t through validate_ledger, advances with the head after another commit, and on the cross-ledger shapes path pins the data ledger's t rather than the model's.
  • Server HTTP and CLI integration tests assert the field appears in the envelope and the table line.

Blast radius

Adding a public field is source-breaking only for struct-literal constructions, and all three are in validate.rs. Nothing removed or renamed on the wire; the summary envelope and W3C documents gain one term each.

Note: the excluded testsuite-shacl crate does not compile on main today for an unrelated reason (its runner predates the cancellation / max_fuel fields on ValidateOptions, and CI never builds it). Left alone here — reviving a harness no CI job builds is its own decision.

Follow-up: #1779

Review follow-ups

  • docs/cli/validate.md said file-mode t is "always 1"; it is 2validate_file commits the staging-SHACL-disable config before the data insert. Corrected to say 2 and why, so the number is derivable rather than magic.
  • The CLI file-mode assertion matched checked at t= without pinning the value, which is how the wrong constant reached the docs unchallenged. Pinned to checked at t=2, making the docs claim machine-checked. Verified non-vacuous: the actual output is …(1 shape(s) checked at t=2), and the pinned assertion goes red against t=1.

The full-ledger validator already pins the snapshot it evaluates
(`to_t = view.t` at the top of validate_view_inner), but the report it
returned did not say which t that was. A caller that pairs the results
with other reads (record counts for a violation rate, for instance) had
to read the handle's t before and after the call and bracket the two,
because a commit landing between validate_ledger's internal snapshot and
the caller's own read silently breaks the pin.

Add `ValidateReport.t`, set from the same `to_t` at both construction
sites. The serde summary picks it up directly; the W3C renderings carry
it on the report node as `f:t` (sh:ValidationReport has no slot for a
ledger time), and the server JSON envelope and CLI table line print it.

Integration tests assert report.t equals the ledger handle's t through
validate_ledger, that it advances with the head, and that the
cross-ledger shapes path pins the data ledger's t rather than the
model's.
@bplatz bplatz added the enhancement New feature or request label Sep 2, 2026
@bplatz
bplatz requested review from aaj3f and zonotope September 2, 2026 11:21
@bplatz bplatz added the area:server HTTP surface, routes, error mapping, swagger, timeouts/admission, config graph label Sep 2, 2026

@aaj3f aaj3f left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bplatz this is good and makes sense and I also have some context for understanding its value/utility given @jakep36's comments on fluree/solo#1088. So this is an approve with just some small Claude-found items which I'll leave verbatim below.


This is the right fix in the right place, and I verified the one question that decides whether it's real or cosmetic: the reported t is not a handle read taken around the validate — it's the t of the same immutable LedgerView the validation walks. LedgerView::from_state captures t: state.t() in the same &LedgerState borrow (under the state read lock) as the snapshot/novelty Arcs, and that single to_t variable bounds every GraphDbRef the validator builds and lands on the report at both construction sites. A commit landing mid-validate changes neither what the validator reads nor what the report says — structurally closed, and strictly stronger than the two-handle-read bracket downstream callers (solo#1097) can now delete: the bracket had to refuse when a commit landed mid-validate; report.t stays exact through it.

What I ran and probed, on a warm target with the changed files touched:

  • All 5 new/extended fluree-db-api tests pass by name (--features shacl — worth knowing that a default-features nextest run silently skips the entire validate module; CI's --all-features covers it). Server envelope test and both CLI file-mode tests pass.
  • Mutation check: t: to_tt: 0 at the final construction site turns all three integration tests red — the coverage is real, not decorative.
  • The cross-ledger assertion genuinely discriminates: the fixture leaves the model ledger at t=1 and the data ledger at t=2, so a model-t leak would fail, not coast.
  • Blast radius claim verified: the only ValidateReport struct-literal constructions are in validate.rs; wire changes are additive on all three renderings; f:t reuses the existing ns.flur.ee/db#t predicate rather than minting a term; the @t: addressing the docs teach is real (dataset.rs).
  • testsuite-shacl's pre-existing break confirmed at runner.rs:58 — this PR adds no new one (the runner builds ValidateOptions with no max_fuel/cancellation and no ..Default::default(), so the excluded crate already fails to compile on main; compare.rs only reads ValidateReport fields). Worth its own small issue so it doesn't rot further — reviving a harness CI never builds is a separate decision (does the W3C SHACL suite still pass?), so leaving it alone here is defensible.

The one thing to fix before merge is small: the new CLI doc says file-mode t is "always 1", but it's 2 (the staging-SHACL-disable config commit precedes the data commit) — proven by pinning the test assertion, which currently matches any t value. One word in the doc, one character in the test.

Adherence checklist:

  1. Patterns / abstractions ✔ — extends ValidateReport in place, reuses the existing f:t vocab predicate and the view's own to_t; no parallel construct, no engine change.
  2. Performance ✔ — neutral, verified: no new ledger read on the validate path (to_t already existed), one copied i64 per report, O(1) render additions; no hot crate touched.
  3. Testing ✔ — unit + API + HTTP + CLI integration tests, all wired, all run under CI's --all-features, and mutation-verified red on a wrong t. The CLI file-mode assertion is the one soft spot (unpinned — see nit).
  4. Conventions ✔ — self-describing subject, thorough body with an honest blast-radius and testsuite-shacl disclosure; clippy/fmt green on the exact head; docs updated on both surfaces (one constant to correct).

Comment thread docs/cli/validate.md
`jsonld` and `turtle` emit a W3C-shaped `sh:ValidationReport` with
`sh:focusNode`, `sh:resultPath` (single-predicate paths only — complex paths
are omitted rather than misrepresented), `sh:resultSeverity`,
`sh:sourceShape`, `sh:sourceConstraintComponent`, `sh:resultMessage`, and

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking-adjacent nit (one-word fix — fold in before merge): file-mode t is 2, not "always 1".

The doc says "In file mode t is the ephemeral ledger's commit (always 1)." But validate_file (fluree-db-cli/src/commands/validate.rs:84-125) commits twice before validating — disable_staging_shacl upserts the config graph (commit 1), then the data insert lands as commit 2 — so the ephemeral ledger's head is t=2 when validate_ledger snapshots it. Verified empirically, not just by trace: pinning the CLI integration assertion to checked at t=1 fails with actual output …(1 shape(s) checked at t=2).

Fix: change the doc to 2 (or, better, say why: the loader's staging-SHACL-disable commit plus your data commit), and pin the assertion at integration.rs:3080 to checked at t=2.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 7cc9015

Comment thread fluree-db-cli/tests/integration.rs Outdated
.success()
.stdout(predicate::str::contains("Conforms: true"));
.stdout(predicate::str::contains("Conforms: true"))
.stdout(predicate::str::contains("checked at t="));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional (folds into the docs nit above) — predicate::str::contains("checked at t=") asserts the field exists but not its value. File mode is the one caller with a deterministic t (=2), so pinning it to checked at t=2 makes the docs claim machine-checked — the unpinned matcher is exactly how the wrong constant got past the test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 7cc9015

The CLI doc said file-mode `t` is "always 1". It is 2: `validate_file`
commits the staging-SHACL-disable config graph before the data insert, so
the ephemeral ledger's head is already at 2 when `validate_ledger`
snapshots it.

The assertion that should have caught this matched `checked at t=` without
the value, so the wrong constant went unchallenged. Pin it to `t=2` and say
in the doc why the number is what it is.
@bplatz
bplatz merged commit 12aeb37 into main Sep 3, 2026
16 checks passed
@bplatz
bplatz deleted the feat/validate-report-t branch September 3, 2026 19:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:server HTTP surface, routes, error mapping, swagger, timeouts/admission, config graph enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants