Skip to content

Propagate DB errors from resolve_reverse_segment instead of silently degrading - #2926

Merged
mstomar125 merged 2 commits into
mainfrom
issue-2885-resolve-reverse-segment
Sep 25, 2026
Merged

mstomar125 merged 2 commits into
mainfrom
issue-2885-resolve-reverse-segment

Conversation

@mstomar125

Copy link
Copy Markdown
Contributor

Summary

resolve_reverse_segment (packages/core/src/playbook/validation.rs) had two silent-degradation points on a DB error:

  • resolve_type_chain(...).unwrap_or_else(|_| vec![node_type]) collapsed the ancestor chain to just the concrete type.
  • get_inbound_relationships's Err case did continue, silently skipping that candidate scope.

Either failure made a legitimate inherited reverse relationship name (e.g. node.blocked_by declared on an ancestor, used on a subtype instance) resolve to None, indistinguishable from a genuine typo. The caller then always reported this as BrokenPath, blaming the play's own expression for what could be a transient DB error.

Changes

  • resolve_reverse_segment now returns Result<Option<String>, PlayValidationError> instead of Option<String>:
    • Ok(Some(source_type)) — a declared reverse name found (unchanged success behavior).
    • Ok(None) — a genuine miss (segment declared nowhere in the chain).
    • Err(PlayValidationError::SchemaResolutionFailed) — a DB error resolving the extends chain or reading a scope's inbound relationships, now propagated instead of silently degrading.
    • This mirrors the SchemaResolutionFailed pattern validate_schema_path already uses for its own forward-resolution tokio::try_join! failure a few lines above — same file, same established convention, not a new mechanism.
  • Updated the one call site in validate_schema_path to match on the three outcomes: continue traversal on Ok(Some), fall through to the existing BrokenPath reporting on Ok(None), and push the propagated error + stop on Err (same as the forward-resolution failure handling immediately above it).
  • Added 3 tests: two directly exercise resolve_reverse_segment's new Result contract (found-via-ancestor-chain → Ok(Some(..)), genuinely-undeclared → Ok(None)), and one locks in that a SchemaResolutionFailed from this call site is structurally distinguishable from BrokenPath (different kind(), correctly flagged as inconclusive via is_genuine_failure()).

Diff is scoped to only this function, its one call site, and new tests — no reformatting or changes elsewhere in the file (a sibling PR is concurrently touching different functions in the same file).

Test plan

  • cargo test -p nodespace-core --lib playbook::validation — 98 passed, including the 3 new tests
  • cargo test -p nodespace-core --lib playbook:: — 462 passed (full playbook module, no regressions)
  • bun run quality:fix — clean (cargo clippy --all-targets -- -D warnings -D clippy::unused-async zero warnings)
  • Pre-push gate (test:all + cargo build --bin nodespaced + test:e2e) passed in full on push

Closes #2885.

🤖 Generated with Claude Code

mstomar125 and others added 2 commits September 25, 2026 09:24
…degrading

resolve_reverse_segment had two silent-degradation points: a
resolve_type_chain error collapsed the ancestor chain to just the concrete
type, and a get_inbound_relationships error on any scope silently skipped
that candidate via `continue`. Either failure made a legitimate inherited
reverse relationship (e.g. node.blocked_by declared on an ancestor) resolve
to None indistinguishable from a genuine typo, and the caller reported it as
BrokenPath — blaming the play's own expression for a transient DB error.

Changes resolve_reverse_segment to return
Result<Option<String>, PlayValidationError>: Ok(Some(source_type)) on a
match, Ok(None) for a genuine miss, and Err(SchemaResolutionFailed) for a DB
error on either call, mirroring the SchemaResolutionFailed pattern
validate_schema_path already uses for its own forward-resolution
tokio::try_join! failure. Updates the one call site in validate_schema_path
to push the propagated error and stop, rather than falling through to
BrokenPath.

Closes #2885.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The check-code-references gate flagged a "core#NNNN"-style reference in a
new test's doc comment, added in the previous commit. Reword to describe
the behavior directly per CLAUDE.md's documentation standard.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@mstomar125

Copy link
Copy Markdown
Contributor Author

/pragmatic-code-review (MEDIUM effort) — result

Ran a review covering all 8 finder angles (line-by-line scan, removed-behavior audit, cross-file tracer, reuse, simplification, efficiency, altitude, conventions) plus an independent verification pass checking:

  • Whether a sibling function (walk_path_against_schema in workflow_state.rs) has the same unfixed bug — it doesn't; separate function, separate degradation-tracking design, out of scope for this diff.
  • Whether ensure_schema_cached plus a later get_inbound_relationships error leaves schema_cache in an inconsistent state — no, the two are documented as independent/uncoupled.
  • Whether the Ok(None) => {} match arm at the call site correctly falls through to the existing BrokenPath push — confirmed, no skip or double-report.
  • Whether resolve_type_chain's error could represent "unknown type" (which would make treating it as SchemaResolutionFailed/inconclusive wrong) — no, it only errors on a genuine DB query failure; an unknown type resolves fine via Ok(vec![node_type]), and the schema's existence is already confirmed by the caller before this runs.
  • CLAUDE.md conventions — no violations.
  • Confirmed via grep that resolve_reverse_segment has exactly one call site, matching the PR's stated scope.

Result: zero findings. No recommendations to address.

Re-Review Decision

Decision: NO RE-REVIEW NEEDED

Rationale: The review returned zero findings after full-angle coverage plus independent verification; no code changes were made in response (there was nothing to change). The PR is unchanged since this review ran.

The PR is ready for merge (pending final approval).

@mstomar125
mstomar125 merged commit 2f9418c into main Sep 25, 2026
@mstomar125
mstomar125 deleted the issue-2885-resolve-reverse-segment branch September 25, 2026 07:39
mstomar125 added a commit that referenced this pull request Sep 25, 2026
…ormat! allocation

- Rebased onto main (PR #2926 for issue #2885 merged first, changing
  resolve_reverse_segment's signature); resolved the one conflicting
  signature line, keeping #2926's Result-returning behavior with this
  PR's SchemaCache type. Verified with a full test run post-rebase.
- The trigger node_type check built a `format!` location string on
  every rule iteration, even when the schema resolved successfully.
  Checks cache membership first now, so the allocation (and the
  cached_schema call that needs it) only runs on the error path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
mstomar125 added a commit that referenced this pull request Sep 25, 2026
…ion (#2928)

* Distinguish schema lookup failure from missing schema in play validation

ensure_schema_cached collapsed a DB/lookup error into the same cache
entry as "no schema for this type", so every consumer reported a
transient failure as UnknownNodeType — wrongly blaming the Play
author's type reference. The schema cache is now a tri-state
(Result<Option<SchemaNode>, String>) so a real lookup failure surfaces
as SchemaResolutionFailed instead, via a new cached_schema helper used
at all four call sites (trigger check, update_node, create_node,
relationship action).

validate_schema_path also silently returned with no error when a
deeper-hop schema (a relationship's target_type reached partway
through a condition path) was missing — the "already reported by
trigger validation" reasoning only holds for the first hop. A Play
could save with an unvalidated tail segment. Now reports
UnknownNodeType/SchemaResolutionFailed for any hop past the first.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Address review: rebase onto merged sibling fix, avoid unconditional format! allocation

- Rebased onto main (PR #2926 for issue #2885 merged first, changing
  resolve_reverse_segment's signature); resolved the one conflicting
  signature line, keeping #2926's Result-returning behavior with this
  PR's SchemaCache type. Verified with a full test run post-rebase.
- The trigger node_type check built a `format!` location string on
  every rule iteration, even when the schema resolved successfully.
  Checks cache membership first now, so the allocation (and the
  cached_schema call that needs it) only runs on the error path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
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.

resolve_reverse_segment degrades silently on two DB errors, producing a false 'broken path' rejection for a legitimate inherited reverse relationship

1 participant