Repository navigation
nodespace-types: surface SchemaNode field-parse failures via eprintln (closes #2107) - #2128
Conversation
SchemaNode::from_node's `fields` extraction silently defaulted to an empty Vec on a serde_json parse failure, with zero diagnostic signal. This is the sole caller reachable from node_to_typed_value (used by every entry point: Tauri commands, MCP, HTTP), so a schema with malformed/legacy field JSON read back with 0 fields, everywhere, silently. Extract the parse into a private parse_fields helper that returns the field list alongside an optional diagnostic message, and print it via eprintln! in from_node. nodespace-types carries no logging dependency by design, so eprintln! is the closest equivalent to a log line without adding one — it fires the same way regardless of which binary (Tauri app, daemon, CLI) embeds the crate. Deliberately did not thread the failure through from_node's Result: node_to_typed_value feeds nodes_to_typed_values, which collects a Vec<Result<_, _>> into a single Result<Vec<_>, _> across ~10 batch-read call sites in core/desktop-app/daemon — turning this into an Err would fail an entire unrelated batch over one bad schema node. A regression test in convert.rs pins that a malformed schema node no longer risks zeroing out an unrelated batch. Closes #2107 Co-Authored-By: Claude <noreply@anthropic.com>
… gap Review of #2128 found the doc comment's "sole caller" claim was false: from_node also has two direct callers in desktop-app/src-tauri/src/commands/schemas.rs (get_all_schemas/get_schema_definition) that bypass node_to_typed_value entirely. Only the node_to_typed_value -> nodes_to_typed_values path carries the batch-collect blast-radius risk; correct the comment to name all three callers and scope the risk accurately. The fix already covered all three (it lives inside from_node itself), so this is a documentation-only correction, no behavior change. Also note the eprintln!'s Windows-specific visibility gap (daemon stderr piped to null, desktop-app release build has no console) so the comment doesn't overclaim uniform effectiveness across platforms. Tracked as a follow-up (closes #2129 tracks the platform fix; #2130 tracks the analogous relationships-field gap), out of scope here. Co-Authored-By: Claude <noreply@anthropic.com>
|
Spawned an adversarial review sub-agent against this PR. Verdict was "request changes" — two real findings, both addressed in the follow-up commit (
Also filed #2130 for the sibling No functional regression found; review confirmed Re-ran |
|
APPROVE Independently re-verified after addressing the adversarial review's findings:
Approach chosen: a minimal, dependency-free diagnostic hook (issue's approach 2) — Merging (self-approval authorized per repo convention; GitHub blocks a formal self-review on the author's own PR). |
…println (#2130) (#2131) SchemaNode::from_node's `relationships` extraction had the same silent-swallow-to-empty-Vec shape that #2107/#2128 just fixed for the sibling `fields` extraction two lines above it, with zero diagnostic signal on a genuine serde_json parse failure. Extracted the parse into a parse_relationships helper mirroring parse_fields: returns the relationship list alongside an optional diagnostic message, printed via eprintln! in from_node. Same reasoning as the fields fix applies unchanged: from_node's Result is left untouched (a diagnostic-only fix, not a new error path) because node_to_typed_value feeds nodes_to_typed_values, which collects a Vec<Result<_, _>> into a single Result<Vec<_>, _> across ~10 batch-read call sites in core/desktop-app/daemon -- turning this into an Err would fail an entire unrelated batch over one bad schema node. A regression test (mirroring schema_with_malformed_fields_does_not_fail_ an_unrelated_batch_read) pins that a malformed relationships value does not zero out an unrelated batch either. Closes #2130 Co-authored-by: Claude <noreply@anthropic.com>
Closes #2107
Which approach, and why
Chose a minimal, dependency-free diagnostic hook (issue's approach 2) — specifically,
SchemaNode::from_nodenow delegates itsfieldsextraction to a new privateparse_fieldshelper that returns(Vec<SchemaField>, Option<String>): the same (possibly empty) field list as before, plus a ready-to-print diagnostic message when the parse genuinely failed.from_nodeprints that message viaeprintln!before continuing.from_nodehas three production callers:node_to_typed_value, reachable from every entry point (Tauri commands, MCP, HTTP) vianodes_to_typed_values, which.collect()s aVec<Result<_, _>>into a singleResult<Vec<_>, _>.packages/desktop-app/src-tauri/src/commands/schemas.rs(get_all_schemas/get_schema_definition) that callSchemaNode::from_nodedirectly and do not go throughnodes_to_typed_values.Fixing the diagnostic inside
from_nodeitself (rather than at any one caller) covers all three uniformly, without touching any of their signatures.Ruled out the other two:
Result) — rejected, but only for the path that actually carries the risk the issue flagged:nodes_to_typed_valuesis called with?from ~10 batch-read call sites acrosspackages/core/src/ops/{query_ops,node_ops}.rs,packages/desktop-app/src-tauri/src/commands/{nodes,collections}.rs, and the daemon. Propagating the fields-parse failure asfrom_node's ownErrwould make one schema node with stale/malformed field JSON fail an entire unrelated batch read (a task list, a collection view, a genericlist_nodesquery that happens to include one broken schema) on that path. Making that safe would mean changingnodes_to_typed_values's batch-collect semantics (partition into successes + individually-reported failures) — a much larger, riskier change touching every batch-read call site in three crates, for a bug whose actual fix doesn't require it. (The two directschemas.rscallers don't share this risk —get_all_schemasalreadyfilter_maps out bad schemas today, andget_schema_definitionoperates on a single schema — but the fix needed to be uniform across all three callers anyway, so this doesn't change the choice.)nodespace-types) — rejected. A debug-assertion or startup healthcheck would only ever catch this in dev/CI or at daemon boot, not for a schema written or corrupted after boot. The whole point of the bug report is that this fires "silently, everywhere" on a normal read — a healthcheck doesn't close that gap for a schema that goes bad mid-session; it just adds a separate detector that lags behind the read path itself.Approach 2's callback/closure variant was also considered (
from_node(node, on_error: impl FnMut(&str))) but rejected in favor of the current shape: threading a callback through would require touching all three call sites above and would ripple intonode_to_typed_value's own signature (used from dozens of places), for no benefit over an unconditionaleprintln!— the failure is rare (a genuine parse error, not a hot path) andeprintln!requires zero new dependency and zero signature changes anywhere.Known limitation, tracked as a follow-up, not fixed here: the "fires the same way no matter which binary embeds this crate" property doesn't fully hold on Windows — the daemon's child process there is spawned with stderr piped to null, and a release desktop-app build has no console, so this diagnostic is currently invisible on a Windows production build (macOS/Linux both redirect the daemon's stderr to a log file). Filed as #2129, out of scope for this PR (fixing it is a daemon/app stdio-plumbing change, not a
nodespace-typeschange).What changed
packages/nodespace-types/src/schema.rs: extracted thefieldsJSON parse into a new privateparse_fields(properties, node_id) -> (Vec<SchemaField>, Option<String>). On a genuine parse failure it still returns an emptyVec(unchanged behavior — no new error path, no signature change onfrom_node), but now also returns a diagnostic message naming the schema node id and the underlyingserde_jsonerror.from_nodeprints that message viaeprintln!.packages/nodespace-types/src/convert.rs: added a regression test (schema_with_malformed_fields_does_not_fail_an_unrelated_batch_read) pinning that a malformed schema node in anodes_to_typed_valuesbatch does not fail the other, unrelated nodes in the same batch — the invariant approach 1 would have put at risk, explicitly locked down even though this PR didn't touch batch semantics.No dependency was added to
packages/nodespace-types/Cargo.toml— the crate's minimal-dependency-footprint design goal (its ownlib.rsdoc comment: "Intentionally minimal:serde,serde_json,chrono,uuid,thiserror. No database, HTTP, or NLP dependencies.") is unchanged.The
relationshipsextraction two lines belowfieldsin the same function has the identical silent-swallow shape and was left untouched (out of scope for #2107, which named onlyfields) — filed as #2130.Testing
packages/nodespace-types/src/schema.rs:test_parse_fields_malformed_json_surfaces_diagnostic— malformedfieldsJSON now produces a diagnostic naming the schema id and the property.test_parse_fields_valid_json_no_diagnostic/test_parse_fields_absent_key_no_diagnostic— no false-positive diagnostic on a clean parse or an absent key.test_from_node_malformed_fields_still_succeeds_with_empty_fields— end-to-end throughfrom_node: stillOkwith an emptyfieldsVec (unchanged), now with the diagnostic printed along the way. Verified manually withcargo test -p nodespace-types test_from_node_malformed_fields_still_succeeds_with_empty_fields -- --nocapture, which prints:packages/nodespace-types/src/convert.rs:schema_with_malformed_fields_does_not_fail_an_unrelated_batch_read— proves a bad schema node doesn't zero out an unrelated batch (see above).cargo test -p nodespace-types: 50 passed, 0 failed.cargo test -p nodespace-core --lib: 1154 passed, 0 failed.cargo test --workspace --lib --bins: all green (full workspace, including daemon/cli/nlp-engine/agent).cargo clippy -p nodespace-types -p nodespace-core -p nodespace-daemon -p nodespace-cli -p nodespace-app --all-targets -- -D warnings: clean.cargo fmt -p nodespace-types -- --check: clean.Acceptance criteria
nodespace_types::SchemaNode::from_node'sfieldsextraction is now surfaced viaeprintln!(node id + underlyingserde_jsonerror) instead of producing an emptyVecwith zero signal.nodespace-types— the crate's minimal-dependency design goal is unchanged.Review
An adversarial review sub-agent caught two real issues, both addressed:
node_to_typed_valuewas the "sole caller" offrom_node— false, per the two directschemas.rscall sites above. Corrected in both the code comment and this description; the fix already covered all three callers (it lives insidefrom_nodeitself), so this was a documentation-accuracy fix, not a behavior change.A third (nit) finding — a since-removed citation to an unrelated
#[cfg(test)]eprintln!as a "no-logging-dependency precedent" — has been dropped from this description as inaccurate framing; it wasn't load-bearing for the approach choice.