feat(store): a record's source can be corrected without touching its words (P5)

Accounting reason: a convention, a decision or an open item carries who
decided it, when and on what authority. When that is recorded wrongly the
honest fix is to say so on the record, not to restate the policy: a policy
revision bumps the convention's revision and reads as a change of the rule,
and a new decision supersedes the old one. A source correction changes only
the authority and says why, so the audit trail shows the words were never
altered.

New event types convention.source_revised, decision.source_revised and
open_item.source_revised {id, source, reason} under the journal feature
source_revisions: an older version would refuse the types, or show the old
source as the record's authority, so it stops at the declaration instead
(rules.md B-27) and books that have not had the owner's upgrade refuse the
write with format_upgrade_pending, journaling nothing. Replay sets the
source and nothing else (the text, the revision and the journal line stay);
the projection updates the rows' existing source columns, so no schema
changed. A correction must be one: other words, the same source, no reason
or an unknown record is refused.

books.set_convention takes reason (the text exactly as it stands),
books.record_decision and books.raise_item take revises with reason. Without
reason the old refusal stands and now says how to correct only the source.

8.0.6: the hand-written lines of the three shapes in
books_written_under_older_rules_still_open; fixture old-books/p5_before,
written by b97234ac (a revised convention, two decisions, two open items, a
draft) through old-books-writer/src/bin/p5_before.rs, opens to that
version's figures, so no figure moved; books_p2_served_cut_over_whole
refuses the correction before consent and takes it after; the fuzzer and the
viewer's journal know the types. The live books refuse the new write until
the owner gives the go-ahead; merging does not need it.

Tests: a_source_only_revision_is_journaled,
a_source_revision_must_change_only_the_source,
source_revisions_wait_for_the_upgrade, source_revisions_is_its_own_feature.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
This commit is contained in:
swaitsandClaude Fable 5.1 committed 2026-10-29 15:48:27 -06:00
1 parent cf73d0bda5
commit 4236f03744
19 files changed
+1188 -13

No files matched your search

+18 -2
View File
@@ -173,6 +173,16 @@ as it is made.
`policy`, `close_step`, `calendar`, `mapping`, `note`, in words). An id is
a slug, revised in place (same kind), never reused once retired:
`books.retire_convention { id, reason, decided_by, ... }` (destructive).
**Correcting a source** (P5, journal feature `source_revisions`):
`books.set_convention` with `reason` and the text exactly as it stands
changes only who decided it, when and on what (`decided_by`,
`decided_on`, `citation`, `note`), journaled as `convention.source_revised
{id, source, reason}`; the revision number and the words do not move.
Without `reason` the same call is refused ("already says exactly that;
to correct only who decided it, when or on what, add reason"), and with
`reason` and different words it is refused ("changes more than its
source"). Books that have not had the owner's upgrade refuse it with
`format_upgrade_pending`.
- `books.draft_from_template { template_id, date, amount, accounts?,
description?, party?, no_party_reason? }` → `{ entry_id, description,
lines }`: a draft whose lines split the amount (at most a line's) by the
@@ -190,8 +200,14 @@ as it is made.
decision without them journals the bytes it always did, and one with
them needs the journal feature `projects`. `books.decisions` gives each
decision's `projects: [{ id, name }]` (left out when none); `project`
filters to the decisions naming one.
- `books.raise_item { title, detail_md?, due?, decided_by, ... }` → `{ id }`,
filters to the decisions naming one. `books.record_decision { revises:
ID, reason, decided_by: [..], decided_on, citation? }` corrects only
that decision's source (who decided it, when, on what; its words stay,
and `subject` and `decision_md`, if given, must be the ones it has):
`decision.source_revised {id, source, reason}`, returning `{ id }`.
- `books.raise_item { title, detail_md?, due?, decided_by, ... }` → `{ id }`
(`books.raise_item { revises: ID, reason, decided_by, ... }` corrects
only that item's source: `open_item.source_revised`),
`books.resolve_item { id, resolution, decided_by, ... }`,
`books.open_items { include_resolved? }`.
- `books.approve_entry { entry_id, approver, note?, approved_on? }`: the
+8 -1
View File
@@ -64,7 +64,14 @@ apply is safe — on startup the applier replays every journal line with
`attachment.annotated`, `entry.settles` (P6), `entry.draft_state_set`
(a draft building or awaiting its evidence: a note on the draft, no
figure) and `entry.draft_discarded` (a draft thrown away, with why; the
draft's earlier lines stay in the journal) (P4, feature `drafts_v2`)
draft's earlier lines stay in the journal) (P4, feature `drafts_v2`),
and `convention.source_revised {id, source, reason}`,
`decision.source_revised {id, source: {decided_by: [..], decided_on,
citation?}, reason}` and `open_item.source_revised {id, source, reason}`
(P5, feature `source_revisions`: who decided a convention, a decision or
an open item, when and on what, corrected without touching its words;
replay sets the source and nothing else, and the projection updates the
row's existing source columns, so no schema changed)
(plus the A/R, A/P, asset,
period, payroll, and class events listed on `JournalLine`).
- `payload`: the full domain facts (account fields, entry with lines, blob
+4 -1
View File
@@ -1761,7 +1761,10 @@ saying what it duplicates, B-44). P4's is `drafts_v2` (an
`entry.draft_state_set` or `entry.draft_discarded`, both new event types:
an older version would refuse them, or, reading around them, keep a
discarded draft that holds a period open and call a waiting draft one
being built, B-43). A command asks for
being built, B-43). P5's is `source_revisions` (a
`convention.source_revised`, `decision.source_revised` or
`open_item.source_revised`, new event types: an older version would
refuse them, or show the old source as the record's authority). A command asks for
the feature it needs (`Store::ensure_feature`) before its first journaled
line, and only when the line it is about to write uses it, so a write
refused for waiting on the upgrade journals nothing, never an entry