You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Cut 1 of 0044 (#710): the open graph lands in the ledger, and extraction can write only that layer. Follows the sequence in #725 (queues come after cuts 1 and 2) and the time record 0045 (#724), whose interpretation columns wait for a later cut.
What the ledger gets
An open statement is what a document says, in its own words: a subject, a relation phrase as written, an object entity or a literal value, qualifiers keyed by the document's role words, the time words it mentions, and provenance to the chunk. Today an unbound relation is representable (0010: predicate_id NULL, phrase on fact_evidence.proposed_predicate), but it dedups badly (NULL-blind key, every observation inserts a row), qualifiers must be keyed by an ontology attribute (0037), a described thing cannot be an entity (canonical_name and the known_as fact are mandatory), time exists only as computed dates, and nothing marks the layer.
Migration 0061_a_statement_keeps_the_documents_words.sql (0060 is taken by #721). Additive, no backfill; there is no released version.
ALTERTABLE knowledge_bases ADD COLUMN open_extraction BOOLEANNOT NULL DEFAULT FALSE;
ALTERTABLE facts
ADD COLUMN layer TEXTNOT NULL DEFAULT 'typed'CHECK (layer IN ('typed', 'open')),
ADD COLUMN phrase TEXT,
ADD CONSTRAINT facts_open_statement_shape
CHECK (layer <>'open'OR (phrase IS NOT NULLAND predicate_id IS NULL));
CREATEINDEXfacts_open_by_subjectON facts (kb_id, subject_id, phrase)
WHERE layer ='open'AND invalidated_at IS NULL;
ALTERTABLE fact_evidence ADD COLUMN quote_start INT, ADD COLUMN quote_end INT; -- char offsets into chunks.text; NULL = not locatedALTERTABLE entities ADD COLUMN description TEXT; -- a described, unnamed thingCREATETABLEstatement_qualifiers (
fact_id UUID NOT NULLREFERENCES facts (id) ON DELETE CASCADE,
role TEXTNOT NULL, -- the document's word
value JSONB,
entity_id UUID REFERENCES entities (id) ON DELETE CASCADE,
PRIMARY KEY (fact_id, role),
CHECK ((value IS NOT NULL) <> (entity_id IS NOT NULL))
);
CREATETABLEtime_mentions (
id UUID PRIMARY KEY,
kb_id UUID NOT NULL,
fact_id UUID NOT NULLREFERENCES facts (id) ON DELETE CASCADE,
chunk_id UUID NOT NULLREFERENCES chunks (id) ON DELETE CASCADE,
textTEXTNOT NULL, -- verbatim words, never a computed date
char_start INTNOT NULL,
recorded_at TIMESTAMPTZNOT NULL DEFAULT now(),
UNIQUE (fact_id, chunk_id, char_start)
);
Open statements are facts rows with layer = 'open', so every read path that already tolerates a NULL predicate (graph, entity detail, paths, export, RDF, tools, tests/no_predicate_still_shows.rs) shows them labelled by the phrase without change. Both clocks: attested_from (record time) is always set; attested_at comes from doc_time only when doc_time_source IN ('content', 'source'), never from upload time (#714); no valid_* is written for an open statement.
Store
graph::insert_open_statement(kb, subject, phrase, object | value, attested_at, confidence): dedup on (kb, subject, phrase, object IS NOT DISTINCT FROM value) among live open rows; writes layer, phrase, and proposed_predicate = phrase on the evidence so present displays stay the same.
graph::add_evidence_located(fact_id, chunk_id, quote, span); add_evidence delegates. Offsets are computed on the server with a text search, never taken from the model.
graph::add_statement_qualifier, statement_qualifiers_for; new module time_mentions with record and for_facts.
resolution::create_described(kb, description, kind_word): an entity with description set and no known_as fact, so it never becomes a recall bridge (0041).
proposed_predicates and adopt are restricted to layer = 'typed', so an open phrase is never auto-adopted as a relation type.
corroborating_candidate, containment_reviews, refresh_disambiguators switch to LEFT JOIN relation_types with COALESCE(key, phrase), so adjudication profiles read open statements.
kbs::update and UpdateKbReq gain open_extraction.
The compact extraction contract
New utopia_extract::open: build_open_messages(filename, known, opening, chunk_text) and parse_open_response. No ontology in the prompt, no document date, no dates asked for. The reply is arrays with short keys and no repeated quotes:
e: id, name or description, the document's kind word, named (1) or described (0). s: subject ref, phrase, object ref or null, literal value or null, qualifiers keyed by the document's role words or null, time-mention ids or null, quote index. t: id, verbatim words, quote index. n: entity ref, other name, quote index.
Server checks, each a drop reason in extraction_drops: every quote is in the chunk (QUOTE_NOT_IN_CHUNK), every name and time text is in its quote (TIME_NOT_IN_QUOTE), every ref exists (UNKNOWN_REF). Offsets come from the located quote.
Write path
New utopia-server::extraction_open::run_open. In extraction::run, after the chat client is built: when kb.open_extraction is on and the document is not a memory document awaiting a nod, run the open path and return; otherwise today's path runs unchanged. The open path reuses the chunk loop, epoch checks, retrying chat, origin_ceiling, mark_chunk_extracted, incomplete_reason, name recording (0041), and the govern and adjudicate_entities enqueues. It skips the ontology lists, the predicate index, record_miss, the temporal reconcile, signature_breaks, resolve_types and bootstrap_ontology. Flag off changes nothing: one if.
Time interpretation (shape, anchor, offset, granularity) and document time context (0045 cuts).
Identity profiles and constrained clustering (0044 decision 6).
A "layer" marker in the UI; open rows render as today's unnamed relation until cut 2 gives them a name.
Retiring the typed extraction path; it stays behind the flag until the open path passes the bench on all corpora.
Tests and measurement
utopia-store/tests/an_open_statement_keeps_the_documents_words.rs: an open statement with a role qualifier and a time mention shows in overview, entity detail, neighborhood, paths and export under its phrase; a second observation reuses the row and adds evidence; proposed_predicates excludes it.
a_described_thing_is_an_entity_without_a_name.rs: create_described writes no known_as fact and name recall does not find it.
a_time_mention_is_words_not_a_date.rs: the words and char_start resolve inside chunks.text; the statement has no valid_from.
Unit tests in open.rs: compact arrays parse, truncated output repairs, a bad quote index is a drop, the prompt carries no relation list.
Bench: scripts/bench/recall.mjs --open flips the flag and scores entity-pair recall on the same corpora as today (SEC, pharma-en, ai-timeline; Re-DocRED via a fetch script, never as prompt examples), two runs per configuration. A judge_open.mjs samples open statements against chunks.text and reports the not-stated rate. Thresholds from 0044: not-stated at most 2%, entity-pair recall no lower than today's, prompt tokens per document reported.
Sequence
Claim 0061 early (0041 cut 2 also waits for the next number). One PR to dev. Cut 2 follows on the same tables: typed rows gain a from_statement_id, and the alignment cache keys on the signature.
Cut 1 of 0044 (#710): the open graph lands in the ledger, and extraction can write only that layer. Follows the sequence in #725 (queues come after cuts 1 and 2) and the time record 0045 (#724), whose interpretation columns wait for a later cut.
What the ledger gets
An open statement is what a document says, in its own words: a subject, a relation phrase as written, an object entity or a literal value, qualifiers keyed by the document's role words, the time words it mentions, and provenance to the chunk. Today an unbound relation is representable (0010:
predicate_idNULL, phrase onfact_evidence.proposed_predicate), but it dedups badly (NULL-blind key, every observation inserts a row), qualifiers must be keyed by an ontology attribute (0037), a described thing cannot be an entity (canonical_nameand theknown_asfact are mandatory), time exists only as computed dates, and nothing marks the layer.Migration
0061_a_statement_keeps_the_documents_words.sql(0060 is taken by #721). Additive, no backfill; there is no released version.Open statements are
factsrows withlayer = 'open', so every read path that already tolerates a NULL predicate (graph, entity detail, paths, export, RDF, tools,tests/no_predicate_still_shows.rs) shows them labelled by the phrase without change. Both clocks:attested_from(record time) is always set;attested_atcomes fromdoc_timeonly whendoc_time_source IN ('content', 'source'), never from upload time (#714); novalid_*is written for an open statement.Store
graph::insert_open_statement(kb, subject, phrase, object | value, attested_at, confidence): dedup on(kb, subject, phrase, object IS NOT DISTINCT FROM value)among live open rows; writeslayer,phrase, andproposed_predicate = phraseon the evidence so present displays stay the same.graph::add_evidence_located(fact_id, chunk_id, quote, span);add_evidencedelegates. Offsets are computed on the server with a text search, never taken from the model.graph::add_statement_qualifier,statement_qualifiers_for; new moduletime_mentionswithrecordandfor_facts.resolution::create_described(kb, description, kind_word): an entity withdescriptionset and noknown_asfact, so it never becomes a recall bridge (0041).proposed_predicatesandadoptare restricted tolayer = 'typed', so an open phrase is never auto-adopted as a relation type.corroborating_candidate,containment_reviews,refresh_disambiguatorsswitch toLEFT JOIN relation_typeswithCOALESCE(key, phrase), so adjudication profiles read open statements.kbs::updateandUpdateKbReqgainopen_extraction.The compact extraction contract
New
utopia_extract::open:build_open_messages(filename, known, opening, chunk_text)andparse_open_response. No ontology in the prompt, no document date, no dates asked for. The reply is arrays with short keys and no repeated quotes:{"q": ["verbatim sentence", "..."], "e": [[0, "name or description", "kind word", 1]], "s": [[0, "relation phrase as written", 1, null, {"role": "words"}, [0], 0]], "t": [[0, "verbatim time words", 0]], "n": [[0, "another name", 0]]}e: id, name or description, the document's kind word, named (1) or described (0).s: subject ref, phrase, object ref or null, literal value or null, qualifiers keyed by the document's role words or null, time-mention ids or null, quote index.t: id, verbatim words, quote index.n: entity ref, other name, quote index.Server checks, each a drop reason in
extraction_drops: every quote is in the chunk (QUOTE_NOT_IN_CHUNK), every name and time text is in its quote (TIME_NOT_IN_QUOTE), every ref exists (UNKNOWN_REF). Offsets come from the located quote.Write path
New
utopia-server::extraction_open::run_open. Inextraction::run, after the chat client is built: whenkb.open_extractionis on and the document is not a memory document awaiting a nod, run the open path and return; otherwise today's path runs unchanged. The open path reuses the chunk loop, epoch checks, retrying chat,origin_ceiling,mark_chunk_extracted,incomplete_reason, name recording (0041), and thegovernandadjudicate_entitiesenqueues. It skips the ontology lists, the predicate index,record_miss, the temporal reconcile,signature_breaks,resolve_typesandbootstrap_ontology. Flag off changes nothing: oneif.Not in this cut
Tests and measurement
utopia-store/tests/an_open_statement_keeps_the_documents_words.rs: an open statement with a role qualifier and a time mention shows in overview, entity detail, neighborhood, paths and export under its phrase; a second observation reuses the row and adds evidence;proposed_predicatesexcludes it.a_described_thing_is_an_entity_without_a_name.rs:create_describedwrites noknown_asfact and name recall does not find it.a_time_mention_is_words_not_a_date.rs: the words andchar_startresolve insidechunks.text; the statement has novalid_from.open.rs: compact arrays parse, truncated output repairs, a bad quote index is a drop, the prompt carries no relation list.scripts/bench/recall.mjs --openflips the flag and scores entity-pair recall on the same corpora as today (SEC, pharma-en, ai-timeline; Re-DocRED via a fetch script, never as prompt examples), two runs per configuration. Ajudge_open.mjssamples open statements againstchunks.textand reports the not-stated rate. Thresholds from 0044: not-stated at most 2%, entity-pair recall no lower than today's, prompt tokens per document reported.Sequence
Claim 0061 early (0041 cut 2 also waits for the next number). One PR to
dev. Cut 2 follows on the same tables: typed rows gain afrom_statement_id, and the alignment cache keys on the signature.