Skip to content

A relation carries its own attributes - #598

Merged
WaylandYang merged 3 commits into
devfrom
feat/a-relation-declares-its-qualifiers
Sep 10, 2026
Merged

A relation carries its own attributes#598
WaylandYang merged 3 commits into
devfrom
feat/a-relation-declares-its-qualifiers

Conversation

@WaylandYang

@WaylandYang WaylandYang commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Implements 0037 (new record); its status line and the README index are written.

The problem

Meridian Partners invested $4 billion in Kestrel Dynamics

A fact has one shape — subject, predicate, and either an entity or a literal on the other side — so this sentence has to lose one half. The extractor kept the edge (Meridian invested Kestrel) and the four billion went nowhere. In the next document it kept the other half: Northwind investment_amount $5 billion, on the company, with no way back to which investment it was. Before #586 the amount had a third fate: a node called $5 billion, shared by every company that ever raised that sum.

The amount is not the object of the edge and not an attribute of the company. It is an attribute of the edge, and the ledger had no place for it — while everything else about an edge was already first-class: a fact has an id, evidence / conflicts / derivations hang off it, and the RDF export reifies it as an rdf:Statement. The one thing missing was "carry an attribute".

The change

A relation declares which attributes its edges may carry; the values live beside the fact, not in it.

  • migrations/0049: relation_type_qualifiers (relation → attribute definition) and fact_qualifiers (fact → value). facts does not change. Attribute definitions are reused from relation_types rows with kind = 'attribute' — datatype, unit, normalisation and export IRI all exist; the only new thing is a domain that is a relation instead of a class. An entity_id column is reserved ("A invested in B through C"), one-of-two CHECK; nothing writes it in this cut.
  • Extraction: the relation line in the prompt carries {amount: number $}; new rule 8c — when one sentence gives both the entity and a figure, write object and qualifiers, keyed exactly as listed, never dropping the figure to keep the edge. On write the value is converted by the attribute's datatype and the unit recorded; an unknown key, an unconvertible value, and a disagreement with what is already recorded all go to the drop report, never silently.
  • A qualifier is not part of the edge's identity. The write happens after insert_fact, so it applies when that call reused an existing row — a second mention with the amount adds the amount to the same edge. When two mentions disagree, the first value stays and qualifier_conflict is recorded; the ledger's proper answer is two rows and a fact_conflicts entry, which needs a write path that bypasses dedup on purpose and is the next cut (recorded).
  • Read side: panel JSON, canvas edge payload, MCP fact lines, RDF export (one more triple on the statement node — the RDF 1.2 reifier shape). The FromRow structs gain a #[sqlx(skip)] field filled after loading by fact id; the rows themselves are unchanged.
  • Ontology page: the relation editor gets an "Edge attributes" multi-select over this base's attribute definitions; the panel shows them read-only; en/zh strings.

End to end

Isolated base, six documents. Created class organization, attribute amount (number, $), relation invested_in [event] declaring amount, then ran extraction:

Meridian Partners | invested_in | Kestrel Dynamics   | amount | {"unit":"$","value":4000000000} | 2025-05-09
Vega Capital      | invested_in | Northwind Robotics | amount | {"unit":"$","value":5000000000} | 2025-05-02

Exactly the two sentences that used to lose their number. No qualifier_* drops, zero quantity entities. The three read paths:

  • Panel JSON: "qualifiers":[{"key":"amount","value":{"unit":"$","value":5000000000}}]
  • RDF: <…fact:…> a rdf:Statement ; … <…relation:amount> "5000000000"^^xsd:decimal .
  • MCP: Vega Capital [amount: 5000000000 $] (2025-05-02 → 2025-05-02) [90%]

Tests

  • Store, against a database: a_qualifier_is_not_the_edges_identity — a declaration reads back, a relation cannot be its own qualifier, only an attribute qualifies; writes go Set → Same → Conflict and the first value survives a disagreement.
  • Extraction contract: the relation line keeps the order (signature) [event] {qualifiers}, no braces without qualifiers, qualifiers parses on a fact, older replies are unaffected.
  • fmt / clippy / 232 + 32 unit tests / 18 web tests green.

Out of this cut (open questions in the record)

An amount on a relation that is not adopted yet (predicate_id null) goes to the drop report; the canvas edge label; the bootstrap proposing a relation's qualifiers along with the relation.

🤖 Generated with Claude Code

@WaylandYang
WaylandYang merged commit 0e3123b into dev Sep 10, 2026
4 checks passed
@WaylandYang
WaylandYang deleted the feat/a-relation-declares-its-qualifiers branch September 10, 2026 12:41
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.

1 participant