Skip to content

Preserve literal objects when exporting unbound statements - #821

Merged
WaylandYang merged 1 commit into
deeplethe:devfrom
Maya-Kid:codex/preserve-unbound-rdf
Sep 20, 2026
Merged

WaylandYang merged 1 commit into
deeplethe:devfrom
Maya-Kid:codex/preserve-unbound-rdf

Conversation

@Maya-Kid

@Maya-Kid Maya-Kid commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

Problem

An open statement can have a literal object without an ontology predicate. For example, 设备 A —状态→ "待复检" is stored and read by export::facts_page, but its RDF statement has no rdf:object: emit_fact constructed literal objects inside predicate_id.map(...).

Reproduced on dev@91469de through the authenticated HTTP export route against PostgreSQL, as well as the serializer regression.

Change

Construct the literal when object_value exists, using the optional predicate only to look up its datatype. Unbound values use the existing conservative xsd:string representation. Bound datatypes, entity objects, and the current-assertion gate retain their behavior. No predicate or ordinary assertion is invented for an unbound statement.

Validation

Linux, Rust 1.98.1, dedicated PostgreSQL 16/pgvector container, UTOPIA_TEST_REQUIRE_DB=1 and UTOPIA_TEST_REQUIRE_PDFTOTEXT=1:

  • Baseline: cargo test --locked -p utopia-server unbound_literal_objects — both tests fail specifically on the missing object.
  • Fixed: the same 2 tests pass. Removing only the production fix makes both fail again.
  • RDF suite: 16 tests pass, including parsed Turtle/JSON-LD equality, escaping, empty/zero/false/null values, live/retracted statements, absent objects, and bound datatype controls.
  • Authenticated HTTP export reads a statement inserted through insert_open_statement, retains its evidence, and leaves business rows, evidence, and queued jobs unchanged.
  • cargo fmt --all --check, workspace Clippy with -D warnings, workspace tests, and workspace build pass.
  • Workspace tests: 942 passed, 0 failed, 1 explicitly ignored live-public-HTTPS test.
  • Frontend frozen install, 91 tests, and build pass.

Scope and compatibility

Only emit_fact changes in production. No schema, SQL, worker, alignment, or write-path change. The baseline includes #820; its relation-rule design is unaffected. Existing external exports are not rewritten and must be regenerated to recover omitted values. Other RDF omissions remain outside this fix.

Combined regression verification

All five independent fixes merge cleanly onto the same dev baseline. On the combined Linux branch, format, workspace Clippy, build and the full test suite pass (952 passed, 0 failed, 1 explicitly ignored live-public-HTTPS test). A shared authenticated RDF fixture verifies unbound values, subsecond history and explicit ontology links together in Turtle and JSON-LD. Removing each fix individually makes its regression fail again; restoring all fixes returns the full suite to 952 passing tests. No database migration or historical rewrite is included.

Signed-off-by: dada-yan <BinjunYann@gmail.com>
@WaylandYang
WaylandYang merged commit cbf4969 into deeplethe:dev Sep 20, 2026
WaylandYang pushed a commit to rollroyces/utopia that referenced this pull request Sep 24, 2026
… 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>
WaylandYang added a commit that referenced this pull request Sep 24, 2026
… literal can't be parsed (#831) (#865)

Picking up #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 #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 #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>
Co-authored-by: rollroyces <royce@rollroyces.com>
Co-authored-by: WaylandYang <145302500+WaylandYang@users.noreply.github.com>
@Maya-Kid
Maya-Kid deleted the codex/preserve-unbound-rdf branch September 26, 2026 05:45
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.

2 participants