Skip to content

Fix 4 of 7 edit-protection residuals from PR #11's review - #22

Merged
thorwhalen merged 6 commits into
mainfrom
fix/issue-12-edit-protection-residuals
Sep 8, 2026
Merged

thorwhalen merged 6 commits into
mainfrom
fix/issue-12-edit-protection-residuals

Conversation

@thorwhalen

@thorwhalen thorwhalen commented Sep 8, 2026 •

Copy link
Copy Markdown
Collaborator

Refs #12.

Four of the seven residuals from PR #11's adversarial review, each its own commit:

  1. Lock.by / Lock.at validation — by must be non-empty, at must match the ISO-8601 UTC second-resolution format apply_edits already writes. Previously both accepted any string.
  2. Document-wide float scan — validate_document now flags any raw float reaching the wire outside a schema-typed float field (confidence), closing the Lock.was/attrs hole (both typed Any).
  3. Refuse rename onto an existing id — apply_edits now rejects /steps/<id>/id set to a value another step already uses — document-wide for step ids (matching validate_document's own uniqueness scope), list-local for everything else — instead of accepting it and leaving validate_document to flag the resulting duplicate after the fact.
  4. Merge after rename doesn't resurrect the old id — merge_regenerated now consults a step's own /id lock (which records was = the old id) to match a fresh projection that still emits the pre-rename id, instead of treating it as an unrelated new step. Direct id matches are resolved first and claim their committed step before any rename substitution runs, so one committed step can't be matched twice.

A fifth commit addresses two blocking findings from review: the rename-match race above (fresh emitting both the pre- and post-rename id could produce a duplicate-id document) and a float-scan false positive (a Lock.was legitimately holding the old value of a confidence field was flagged, because the scan's allowlist was keyed on the literal "was" key rather than the field Lock.path actually addresses).

The other three items are declined, one-line reasons on #12 and in docs/07-annotation-model.md §6.6/§7:

  • optional SourceSpan.id — needs a schema change, out of scope here (pairs with Evidence layer: persist analysis into a lacing store #4's evidence layer)
  • Lock.path camelCase vs snake_case — documented as a convention instead of changed (changing the recorded spelling would itself be an on-disk format change for every existing lock)
  • selection-boost scoring gap — already deferred by the issue's own text ("revisit when a fourth segmenter lands")

No schema or on-disk format change. docs/07-annotation-model.md updated (§6.6 is new). No CHANGELOG in this repo. No breaking changes — all fixes tighten previously-permissive validation/merge behavior; existing valid documents and edit sequences are unaffected.

234 passed, 1 skipped (pre-existing skip, unrelated). ruff check/ruff format clean on all touched files.

https://claude.ai/code/session_01A8tPdJ7kZ8FbJpXNhMHgap

Lock.by accepted any string including empty; Lock.at accepted any
string including unparseable junk. Both fields are now validated:
by must be non-empty, at must match the ISO-8601 UTC second-resolution
format apply_edits already writes.

Claude-Session: https://claude.ai/code/session_01A8tPdJ7kZ8FbJpXNhMHgap
Lock.was and every attrs bag are typed Any, so a raw Python float
placed there (e.g. via apply_edits or hand-authored attrs) reached the
document unchanged, violating docs/07 §6.5's no-floats rule with no
signal. validate_document now walks the full document dump and flags
any float outside the schema's own float-typed fields (confidence).

Claude-Session: https://claude.ai/code/session_01A8tPdJ7kZ8FbJpXNhMHgap
apply_edits accepted /steps/<id>/id set to a value another step in the
same list already used; validate_document flagged the resulting
duplicate afterwards, but the invalid document was already committed.
Reject the collision at edit time instead, before any mutation.

Claude-Session: https://claude.ai/code/session_01A8tPdJ7kZ8FbJpXNhMHgap
A committed rename of a step id (apply_edits writes a lock at "/id"
with the old id in `was`) severed the merge key: a regenerated
projection that still emits the analysis-derived old id (because
analysis has no knowledge of the rename) was treated as an unrelated
new step, so it resurfaced alongside the renamed one instead of being
recognised as its predecessor. merge_regenerated now consults each
committed step's own "/id" lock to match a fresh step by its pre-rename
id too.

This does not follow a rename across more than one regeneration cycle
-- only the most recent "/id" lock is consulted -- and is not a schema
change: it reads the same Lock records apply_edits already writes.

Claude-Session: https://claude.ai/code/session_01A8tPdJ7kZ8FbJpXNhMHgap
Adds docs/07 §6.6 summarizing the four fixed residuals and the three
declined ones, and documents the Lock.path snake_case-vs-camelCase
convention (a wire consumer must apply to_camel per segment) instead
of changing what apply_edits records, which would be an on-disk format
change for every already-written lock.

Claude-Session: https://claude.ai/code/session_01A8tPdJ7kZ8FbJpXNhMHgap
Two blocking issues from review of PR #22:

- merge_regenerated could match one committed step to two fresh
  entries when fresh emitted both the pre-rename and post-rename id
  (e.g. analysis caught up mid-flight): the old-id match via
  _renamed_from raced the new-id direct match with no mutual exclusion,
  producing a document with a duplicate step id. Direct id matches are
  now resolved first and claim their committed step before any rename
  substitution runs, so a committed step is matched at most once.
- The float scan's allowlist for schema-typed float fields (confidence)
  was keyed on the literal dict key, so a Lock.was field name itself
  ("was") never matched the allowlist even when the lock's own `path`
  pointed at a confidence field it legitimately snapshotted as a float.
  The scanner now recognises a Lock's dump shape and checks `was`
  against the field its `path` addresses, not the key "was" itself.

Also, per review nits: the rename-collision check is now document-wide
for step ids (matching validate_document's own uniqueness scope, not
just the immediate sibling list), and the float scan addresses steps
by id instead of list index where available.

Claude-Session: https://claude.ai/code/session_01A8tPdJ7kZ8FbJpXNhMHgap
@thorwhalen
thorwhalen merged commit b310851 into main Sep 8, 2026
12 checks passed
@thorwhalen
thorwhalen deleted the fix/issue-12-edit-protection-residuals branch September 8, 2026 15:17
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.

1 participant