Skip to content

The ontology decides a pushed phrase it declares - #1245

Merged
WaylandYang merged 3 commits into
deeplethe:devfrom
gedaiu:feat/ontology-binds-declared-phrases
Oct 16, 2026
Merged

WaylandYang merged 3 commits into
deeplethe:devfrom
gedaiu:feat/ontology-binds-declared-phrases

Conversation

@gedaiu

@gedaiu gedaiu commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Closes #1242.

What happened

A statements source (0054) pushes [Statement] validated_by [Test] into a base whose imported ontology declares validated_by with domain Statement and range Test. The classes come out right with no model once the kind words are bound. The relation never does: a phrase binding is written only by the aligner, which needs a chat model, so in a base without one the statement stays open for good, although the ontology already says what validated_by is.

What changed

  • After a pushed document's extraction, bind_declared looks at the signatures its statements fall in. One whose phrase, normalised the way kind words are, equals the key or label of exactly one declared relation, with the subject's class in the domain and the object's class in the range (subclasses count, forward only), is bound without asking the model and recorded as decided_by = 'ontology'. A literal object, an end without a class, an undeclared domain or range, the reverse direction, or two relations that both fit bind nothing and stay with the aligner.
  • The same extraction enqueues materialization unless one is queued, so typed facts follow the push.
  • A person's decision is never overwritten: the guard in decide_on now protects a person's row from any other decider, not just the agent. The aligner leaves an ontology decision alone while the relation still declares it (settled); without that it would see a row with no basis as stale and ask the model every run. Once the relation stops matching, the aligner re-decides it like an agent row.
  • Migration 0113 admits 'ontology' in phrase_bindings.decided_by (0112 is left to Let a person connect Google Drive and sync the folders they choose #1222). 0044 gains a revision under decision 3 saying why this keeps it (per signature, a reviewable row, a person never replaced) and 0012 (it fires only on equality with a relation someone declared), and what is not done: no typed write and no predicate slot in the push contract (0054 decision 5 stands), near matches still go to the aligner. 0054 and the ontology design note point to it.

It is a little over the 500-line mark: about 165 lines of code and the migration, the rest tests, most of them the setup that pushes a document, modelled on time_resolution_pushed_tests.rs. The binding, the materialize enqueue and the aligner's settled check depend on each other, so splitting them would leave a state where the aligner keeps re-asking about rows the ontology wrote.

Tests

phrase_alignment_declared_tests, against a real database and with no chat model unless stated: a pushed phrase equal to a declared relation binds and materializes into one typed fact; in one base, "validated by tests", a subject outside the domain, the reverse direction and an ambiguous phrase bind nothing while the fitting one does; a person's none decision survives a push; two pushes leave one queued materialization; with an unreachable chat endpoint configured, the aligner makes no model call, keeps the ontology row and does not re-queue itself.

cargo fmt --all --check, cargo clippy --workspace --all-targets -- -D warnings and cargo test --workspace pass with UTOPIA_TEST_REQUIRE_DB=1 (1538 passed, 0 failed). The web build is unaffected.

After a pushed document's extraction, a signature whose phrase equals the
key or label of exactly one declared relation, with the subject's class in
its domain and the object's class in its range, binds without the model,
recorded as decided_by 'ontology'. Materialize is enqueued unless queued,
so the typed graph follows the push in a base with no chat model. A person's
decision is never overwritten, a near match or an ambiguous one goes to the
aligner, and the aligner leaves an ontology decision alone while the
relation still declares it.

Migration 0113 admits 'ontology' in phrase_bindings.decided_by; 0044 gains
a revision under decision 3.

Closes deeplethe#1242

Signed-off-by: Bogdan Szabo <bogdan@re-cinq.com>

@WaylandYang WaylandYang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, this is well bounded: exact equality, both ends declared, forward only, ambiguity falls through, and a person's decision now survives any other decider. The tests read clearly.

One gap before merge: state relations. declared() checks kind == "relation" but not temporal, so a state relation can be bound with no marks. After that, the aligner skips the row (settled short-circuits before awaits_marks, and the marks write only touches decided_by = 'agent' rows), and the review queue doesn't list it (WAITING_PHRASE needs decided_by = 'person' or marks_asked_at). Statements dated at a single moment under it then stay open with no way for a person to find it, so the ADR's "until a person sets one" can't happen. In a base with a model this is a step back, because the aligner would have bound it and asked for marks. Either skip temporal = 'state' in declared() and leave it to the aligner, or add 'ontology' to WAITING_PHRASE so it reaches the queue. Please add a test for whichever you choose.

Two small corrections to the 0044 revision: bindings are per signature, so prose statements that share a pushed signature get bound too (the "not pushed" sentence should say that); and the row carries votes = {"reason": "declared_relation"}, not "no votes". A test for the re-decide once the relation stops matching would back up the claim in the ADR.

A state relation bound by the ontology carried no marks, and neither the
aligner nor the review queue would ever reach it. declared() now skips
temporal = 'state', so the aligner binds it and asks for marks. Tests: a
pushed phrase naming a state relation gets no ontology binding, and an
ontology binding is re-decided once its relation is narrowed or renamed.
0044's revision corrects the votes it records, says a prose statement
sharing a pushed signature is bound with it, and says how a binding whose
relation stops matching is re-decided.

Signed-off-by: Bogdan Szabo <bogdan@re-cinq.com>
@gedaiu

gedaiu commented Oct 16, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, all done in the latest commit.

  • State relations: I took the first option. declared() now skips temporal = 'state', so a state relation is left to the aligner, which binds it and asks for marks; whatever it can't get reaches the queue as before. Test: a_pushed_phrase_equal_to_a_declared_state_relation_is_left_to_the_aligner. It pushes a declared state relation and a plain one; only the plain one is bound by the ontology, and the state one gets no binding row.
  • 0044 revision:
    • It now says "every signature no pushed document reaches", and adds that a binding is per signature, so a prose statement sharing a pushed signature is bound with it.
    • The row is described as carrying votes = {"reason": "declared_relation"}.
    • State relations are listed among the cases left to the aligner. The old sentence about a state binding with no marks is gone.
    • 0054's revision line and design/ontology.md say "not a state" too.
    • Migration 0113's comment is untouched, since sqlx checksums the file.
  • Re-decide: two tests back the claim, and they showed the old wording was too broad, so it is corrected.
    • an_ontology_binding_is_redecided_once_the_relation_stops_declaring_it narrows the domain. The row becomes none/agent/no_candidates without a model call, and the typed row is gone after materialize.
    • a_renamed_relation_sends_the_ontology_binding_back_to_the_aligner renames the relation. The signature goes back to the model, so with an unreachable model it is requeued.
    • The ADR now says that a binding with no fitting relation left is unbound without the model, and that one which still needs the model stays bound in a base with no model until a person decides.

Local runs: fmt and clippy (-D warnings) are clean. declared_tests passes 8/8, and the utopia-server and utopia-store suites pass.

gedaiu added a commit to re-cinq/lore that referenced this pull request Oct 16, 2026
Pins Utopia `0.1.0-re.4`
(`sha256:0697b93d1c976b02409e63f08ed9b1706d4806b31466208d8da61e2464b9e02c`)
in the committed defaults of `infra/terraform/variables.tf`.

`re.4` is `re.3` (re-dev `bc23e69`, which includes #1250) plus the fixes
the maintainer asked for in their review of:
- deeplethe/utopia#1244: a write token creates only `statements` sources
(anything else gets a 403 `token_source_kind`), and the audit rows for
source creation, kind-word decisions and ontology imports record the
token id.
- deeplethe/utopia#1245: a declared state relation is left to the
aligner instead of being bound by the ontology.

Lore pushes only `statements` sources and declares no state relations,
so neither fix changes what the mirror does.

There is no new migration: 113 is still the last one. Before tagging,
the merged re-dev (`26ea10b`) passed fmt, clippy and the `utopia-server`
/ `utopia-store` tests. The image was boot-tested on an empty database:
health returned 200 and migrations ran to 113.

Takes effect with `terraform apply` from main.

@WaylandYang WaylandYang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, this closes the gap: state relations now go to the aligner, which binds them and asks for marks, and the new tests pin down that behaviour and the re-decide after a narrowed domain or a renamed relation. The 0044 revision now matches the code. Merging. Whichever of this and #1222 lands second will need CURRENT_SCHEMA_VERSION bumped to 102.

@WaylandYang
WaylandYang merged commit 962d06b into deeplethe:dev Oct 16, 2026
6 checks passed
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