A rule's definition has a history, and a conclusion names the version it was drawn under - #913
Merged
Merged
Conversation
… it was drawn under
A business rule was one row edited in place, and a derivation pointed at
the row. Change a threshold and the invalidated conclusions pointed at a
rule that now said something else: the record axis kept "we concluded
this, then it stopped holding" and lost "under which definition".
Every edit that changes what a rule says now opens a version in
attribute_rule_versions, a full snapshot (subject class, conclusion, join
predicate, conditions) with a record time, and closes the previous one.
Name, description and the enabled switch open nothing; whether the
definition changed is decided by comparing the snapshot JSON, produced by
one SQL expression the migration's backfill and the store share.
A derivation names the version it was drawn under
(derived_facts.attribute_rule_version_id). A conclusion that still stands
after an edit keeps its row and moves to the new version, counted as
`redefined`; the rows the edit invalidates keep pointing at the version
they were drawn under. The proof carries the version number and its
definition, the rules panel shows the version next to the name and opens
the history, and GET /kbs/{id}/rules/{rule_id}/versions reads it for an
integration, with the labels the ids resolve to today. Existing rules
start at version 1 from the migration.
Recorded as 0060. CURRENT_SCHEMA_VERSION is 76.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Wayland Yang <wayland0916@gmail.com>
WaylandYang
added a commit
that referenced
this pull request
Sep 25, 2026
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Wayland Yang <wayland0916@gmail.com>
WaylandYang
added a commit
that referenced
this pull request
Sep 25, 2026
…e date it was pushed with (#911) * Materialization reconciles the rows it writes, and a version keeps the date it was pushed with Two gaps from the #875 replay (#899, #900). Typed materialization wrote rows through the graph's gate but never ran the uniqueness-timeline reconciliation the write path runs after every state fact, so a functional attribute kept two open values and a later observation could not close the earlier one without a manual reconcile. materialize and try_materialize now collect the rows they wrote (new, merged and implied) and reconcile them after the commit, the same reconcile_facts the write path uses; Outcome reports corrected and conflicts. A timeline rewrite (close, rehome) now carries from_statement_id, implied, typed_fact_sources and implied_fact_sources onto the corrected row, so the next round sees it as already computed instead of adding a second one. A same-identity update replaces the document's doc_time, so evidence that stayed on the earlier version dated at the later time and the two values looked simultaneous. document_versions gains doc_time (migration 0090, backfilled for the current version), every version row records the document's date at that moment, and the timeline dates a fact by its evidence's own version, falling back to the document's date for older rows. A pushed statements document now carries the observation's external_id and doc_time ahead of the three arrays, so two observations that saw the same thing are two documents under the one-document-per-content index, and the same payload under a new identity is never a rename. The parser reads only e, s and n. The record and the guide say so. Tests: the two reproductions from #899 and #900 run un-ignored as acceptance; the explicit-reconcile test now expects the materialization to have done the work; a route test pushes one payload under two identities and gets two dated documents. Store and server suites green on a fresh database, clippy clean. Closes #899 Closes #900 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Wayland Yang <wayland0916@gmail.com> * Count migration 0076 into the schema version after #913 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Wayland Yang <wayland0916@gmail.com> --------- Signed-off-by: Wayland Yang <wayland0916@gmail.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This was referenced Sep 25, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #912. Recorded as ADR 0060.
A business rule was one row edited in place, and a derivation pointed at the row. Change a threshold and the invalidated conclusions pointed at a rule that now said something else: the record axis kept "we concluded this, then it stopped holding" and lost "under which definition". This is what stopped #902's second cut.
What changes
attribute_rule_versions(migration 0076): every edit that changes what a rule says opens a version, a full snapshot of subject class, conclusion, join predicate and conditions with a record time, and closes the previous one withsuperseded_at. Name, description and the enabled switch open nothing. Whether the definition changed is decided by comparing the snapshot JSON, produced by one SQL expression that the migration's backfill andbusiness_rules::record_versionshare, so a no-op save opens nothing. Existing rules start at version 1, dated by their last edit, and existing derivations point at it.derived_facts.attribute_rule_version_id: a derivation names the version it was drawn under, written at materialization from the version the run read. A conclusion that still stands after an edit keeps its row (0030) and moves to the new version, counted asredefinedin the report; the rows the edit invalidates keep pointing at the version they were drawn under.DerivedFactView) carriesrule_versionandrule_definition; both derived-view loaders read it.GET /kbs/{id}/rules/{rule_id}/versions: the history, newest first, each version with its record interval, how many conclusions stand on it now, and the labels the ids in the definition resolve to today (a renamed or deleted class falls back to its id).listgainsversion.CURRENT_SCHEMA_VERSION76; design note indocs/design/rules.md; index rows.Not in this cut, said in the record: exporting a version's body (the vocabulary is #902's second cut), a revert button, versions of axiom declarations.
Verified on a fresh database: new store test
a_rule_definition_has_a_history(create is v1; a rename opens nothing; a threshold change opens v2 and the kept row moves to it withredefined == 1; the proof shows v2 and its operand; a conclusion change opens v3, invalidates the old row and inserts one under v3; the history reads back with counts and labels; an unknown rule is 404), the whole store suite, utopia-server 415/415, utopia-cli 13/13 (schema version guard), workspace clippy, web typecheck, build and 135 tests.🤖 Generated with Claude Code