feat(rules): an edit leaves behind what it replaced (#3241, milestone 323 step 2)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / integration (push) Failing after 33s
CI & Build / TypeScript typecheck (push) Successful in 42s
CI & Build / Python tests (push) Successful in 1m9s
CI & Build / Build & push image (push) Successful in 30s
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / integration (push) Failing after 33s
CI & Build / TypeScript typecheck (push) Successful in 42s
CI & Build / Python tests (push) Successful in 1m9s
CI & Build / Build & push image (push) Successful in 30s
`update_rule` now snapshots the rule's text before it writes. Rescoping rule 79 meant hand-copying the superseded statement into a task log to keep it (#3237); the history is that, done by the write path instead of by somebody remembering. The snapshot is taken BEFORE the field loop, which is the one ordering that matters. `update_rule` drops `verified_at` when `verify_with` changes, and rewriting a check is exactly the edit whose history is worth most — a snapshot taken afterwards would file the NEW check against the OLD wording. Taking it up front also covers `clear`, which is a separate argument from the field loop and is how a rule that stops being a constraint loses its check entirely. Session-bound rather than opening its own like note_versions.create_version: the version and the edit that caused it commit together, so a failed update cannot leave a history entry for an edit that never happened. Two guards from the sibling are deliberately absent, and both are now pinned by tests rather than only by comments — "make it consistent with note_versions" is a plausible-sounding change that would silently start dropping history: - No MIN_VERSION_INTERVAL_SECONDS. That 300-second gate exists because note autosave fires every 60. Every version here comes from a deliberate update_rule, so three edits in one second are three edits. - No MAX_VERSIONS and no pruning. A rule is edited a handful of times in its life; a cap could only ever discard the one edit somebody went looking for. Kept from the sibling: the identical-content skip. Both doors resend every field, so without it a form saved twice would file an identical snapshot. `order_index` is excluded from the snapshot fields for the same reason — reordering a rulebook is not an edit to what any rule says. No delete-time snapshot, against the task's original scope and on the operator's call. A delete goes through trash_svc and is SOFT: the rule row keeps its full text and restores untouched, so there is nothing for a snapshot to preserve. Anything that survived a purge would be data the operator explicitly asked to be gone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,7 @@ from scribe.services.verification import (
|
||||
days_since_verified as _days_since_verified,
|
||||
last_verified_label as _last_verified_label,
|
||||
)
|
||||
from scribe.services import rule_versions
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -748,6 +749,10 @@ async def update_rule(
|
||||
"verify_with", "expires_when",
|
||||
}
|
||||
check_before = rule.verify_with
|
||||
# Captured BEFORE anything is written, and as plain values — this has
|
||||
# to survive the mutation below. A rule's history is the only record
|
||||
# of what it used to say; the edit itself destroys that.
|
||||
text_before = rule_versions.snapshot(rule)
|
||||
for key in clear:
|
||||
if key in allowed and key in NULLABLE_RULE_TEXT:
|
||||
setattr(rule, key, None)
|
||||
@@ -771,6 +776,11 @@ async def update_rule(
|
||||
# wrongly vouched for costs the thing the sweep exists to catch.
|
||||
if rule.verify_with != check_before:
|
||||
rule.verified_at = None
|
||||
# Same session as the edit, so the two commit together. The snapshot
|
||||
# holds the OLD verify_with — the check that was in force when that
|
||||
# wording was written — which is why it is taken before the loop and
|
||||
# not here.
|
||||
rule_versions.record_if_changed(session, rule, user_id, text_before)
|
||||
await session.commit()
|
||||
await session.refresh(rule)
|
||||
_refresh_rule_embedding(rule)
|
||||
|
||||
Reference in New Issue
Block a user