Skip to content

A rule's definition has a history, and a conclusion names the version it was drawn under - #913

Merged
WaylandYang merged 1 commit into
devfrom
feat/rule-versions
Sep 25, 2026
Merged

WaylandYang merged 1 commit into
devfrom
feat/rule-versions

Conversation

@WaylandYang

Copy link
Copy Markdown
Contributor

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 with superseded_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 and business_rules::record_version share, 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 as redefined in the report; the rows the edit invalidates keep pointing at the version they were drawn under.
  • The proof (DerivedFactView) carries rule_version and rule_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). list gains version.
  • Rules panel: the version next to the rule's name opens the history, each version rendered with the same criterion and conclusion renderers as the current one. Strings in both packs.
  • CURRENT_SCHEMA_VERSION 76; design note in docs/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 with redefined == 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

… 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
WaylandYang merged commit a656cb0 into dev Sep 25, 2026
7 checks passed
@WaylandYang
WaylandYang deleted the feat/rule-versions branch September 25, 2026 02:44
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>
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.

A rule's definition has no history, so a conclusion cannot say which definition it was drawn under

1 participant