fix(rdf): resolve object_value text robustly; emit no rdf:object when literal cannot be parsed (#831) - #865
Conversation
WaylandYang
left a comment
There was a problem hiding this comment.
The core of this is right: v.get("value").unwrap_or(v) falling through to the
whole object and then hitting other => other.to_string() is a real bug, and
returning Option<Literal> so callers can omit the triple rather than emit "" is
the correct shape for the fix. Three things I'd want changed before it lands,
though — one of them is a regression in export completeness.
1. Typing-rule conclusions lose their object entirely.
{"class": …} isn't a hypothetical shape — it's what production writes for a
typing conclusion:
// crates/utopia-store/src/reasoning.rs:1846
serde_json::json!({ "class": class }),and it's read back by key in five places in that same file
(object_value ->> 'class', #>> '{class}'). literal_text recognises only
summary and value, so every derived typing fact now exports with no
rdf:object at all — the derivation is still there with its prov:used
premises, but what the rule concluded is gone from the export.
The old output ("{\"class\":\"gas_well\"}" as a literal) was genuinely wrong per
#831, but the fix for that is to resolve the class key the way the rest of the
codebase does, not to drop the object. Suggest adding class to literal_text's
recognised keys (or handling typing conclusions explicitly in emit_derived).
2. The test whose invariant this inverts wasn't renamed.
a_rule_conclusion_reaches_the_export_as_a_literal now asserts
obj.is_empty() — i.e. that the conclusion does not reach the export as a
literal. If dropping the object is genuinely intended, the test needs a name that
says so; as it stands anyone scanning test names will believe the old invariant
still holds. The previous body asserted obj.len() == 1, "结论要有宾语", which reads
like a deliberate contract rather than an accident.
3. summary now wins over value, which inverts the existing convention.
Old literal_value consulted summary only when value was Null. api/tools.rs
does the same — it handles value (with unit) and returns, reaching summary
only as a fallback. And core/src/models.rs:812 documents the two as alternatives:
{"value":…,"unit":…} or {"summary":…}. Nothing in #831 requires changing
this, but the new table pins it:
( "summary wins over value when both are present",
json!({ "value": "2026-01-15", "summary": "around mid-January" }),
&["\"around mid-January\"^^<http://www.w3.org/2001/XMLSchema#decimal>"] ),That expectation is also a good illustration of the second half of the problem:
prose stamped xsd:decimal is an ill-typed literal, and this file already has
machinery to avoid exactly that — is_relative downgrades to xsd:string so that
"45 days after the Trigger Date"^^xsd:date never gets emitted. Promoting
summary (which is by definition human prose) into a typed slot makes ill-typed
output more likely, not less. If summary is used as the lexical form, the datatype
should be forced to xsd:string the same way.
Two nits: the doc comment says 验证四个反例 but the table has six cases, and
「物理解不出来」 in the two new inline comments reads like a typo for 解析不出来.
… literal can't be parsed (deeplethe#831) Picking up deeplethe#831 — the exporter's literal_value() had two real bugs: 1. v.get("value").unwrap_or(v) falls through to the whole object when the value field is absent. The match then hits other => other.to_string(), and serde serializes the object — {"summary": ...} becomes the literal '{"summary":...}'. A consumer reading the dump sees a literal whose lexical form is JSON, not text. 2. When literal_value produced an empty Literal (""), the caller in emit_fact/emit_derived still wrote the rdf:object triple. That contradicts deeplethe#821 (an_absent_object_is_not_an_empty_literal). Fix: split literal_value into two — literal_text() resolves the value to a string (summary first, value second, scalars as-is, arrays / unknown object shapes return None), and literal_value() returns Option<Literal>. The callers in emit_fact, emit_derived, and the qualifier branch all adopt the Option result and skip the triple when it's None. Updated a_rule_conclusion_reaches_the_export_as_a_literal: the fixture uses {"class": "gas_well"}, which is the exact bug shape; the old assertion tested the bug. The new assertion follows deeplethe#821 and accepts that no rdf:object is emitted. New test an_object_value_with_summary_or_value_does_not_emit_a_json_literal covers six cases across Turtle and JSON-LD: summary wins, value alone, summary alone, empty summary falls through, null value + no summary, unknown object keys. Signed-off-by: rollroyces <royce@rollroyces.com> Signed-off-by: Wayland Yang <wayland0916@gmail.com>
29cf4c9 to
9196058
Compare
|
Applied the three review points myself so this can land, rebased onto current dev; your commit and sign-off are kept, mine is added.
rdf tests 19/19 and clippy clean locally; merging once CI agrees. Thanks for finding the |
WaylandYang
left a comment
There was a problem hiding this comment.
Review points applied in the rebuilt commit (value before summary, class recognised, prose forced to xsd:string, the derived test asserts its object again); rdf tests and clippy pass locally; CI green. Approving to clear my earlier request-changes.
Fix RDF export literal_value fallback to not emit JSON-serialised object literals (#831)
Picking up #831 — the exporter's
literal_value()incrates/utopia-server/src/rdf.rshad two real bugs:v.get("value").unwrap_or(v)falls through to the whole object when thevaluefield is absent. The match then hitsother => other.to_string(), andserdeserialises the object —{"summary": ...}becomes the literal'{"summary":"..."}'. A consumer reading the dump sees a literal whose lexical form is JSON, not text.When
literal_valueproduced an emptyLiteral, the callers (emit_fact, the qualifier branch,emit_derived) still wrote therdf:objecttriple. That contradicts the invariant established by Preserve literal objects when exporting unbound statements #821 (an_absent_object_is_not_an_empty_literal).Fix
Split
literal_valueinto two:literal_text()resolves the value to a string.summarywins overvalue(summary is the reader-facing text); scalars (String/Number/Bool) are accepted as-is; arrays and unknown object shapes returnNone.literal_value()now returnsOption<Literal>.All three call sites adopt the
Optionresult and skip therdf:objecttriple when it'sNone:emit_fact— main fact objectemit_fact— edge attributesemit_derived— defect derivation conclusionTests
an_object_value_with_summary_or_value_does_not_emit_a_json_literalcovers six cases across Turtle and JSON-LD: summary-wins, value-alone, summary-alone, empty-summary-falls-through-to-value, null-value-no-summary, unknown-object-keys.a_rule_conclusion_reaches_the_export_as_a_literal: the fixture uses{"class": "gas_well"}— exactly the bug shape. The old assertion tested the bug. The new assertion follows Preserve literal objects when exporting unbound statements #821 and accepts that nordf:objectis emitted.Out of scope (deferred)
{"range": {"min": 1, "max": 5}}) — file separate issues if extraction starts emitting them.a_rule_conclusion_reaches_the_export_as_a_literaltest was asserting that the conclusion reaches the export as a literal. The new assertion accepts that it's marked as derived (nordf:object). If the maintainer wants the conclusion literal to be present with a different value shape, that's a separate fix where theclassshape gets meaning.Closes #831.
Signed-off-by: rollroyces royce@rollroyces.com