Store a digest of the map file on implementations at open and fail loudly on mismatch #897

Closed
opened 2026-10-02 12:44:26 +00:00 by jared · 3 comments
Owner

Problem

Every process re-loads the map by name: Implementation#current_map (model/implementation.rb:71) and Implementation::Targeting#map (model/implementation/targeting.rb:6). If the map file is edited while a run is open, Map#node raises on a node that no longer exists, deep inside a gate or hook. The run cannot tell that the map changed. Related to #859 (map reroute), not slice 4.

Reproduce

Open an implementation, rename a node in its map YAML, call os-sdlc-next: the error is a KeyError from Map#node, not a message about the map changing.

Approach

  1. The columns implementations.map_digest and reroutes.map_digest (nullable String) come from the #895 schema bump (29 -> 30). This ticket adds no schema change, only the behavior.
  2. At implementation-open, compute the SHA-256 digest of the map file bytes that the Loader parsed (the same bytes, no separate read, to avoid a race with an edit) and store it in implementations.map_digest. When a reroute is applied (Reroute#apply! / Landing#reroute!), store the rerouted map's digest in reroutes.map_digest the same way.
  3. Compare behind Map::Catalog#load (adapter/map/catalog.rb:23), the single choke point, rather than at each caller: load takes the stored digest, parses, and compares with the digest of the bytes it parsed. On mismatch it raises Map::ChangedError (*Error in *_error.rb, a HarnessError subclass like Project::TargetSelectionError, so model/map/changed_error.rb) whose message names the map and says the map changed since the implementation was opened and the implementation must be reopened. A NULL stored digest means no check (rows from before the bump).
  4. A bin/refresh-plugins that ships a map edit fails every live run on that map, loudly. That is intended: ADR-0169 says map contents are frozen, ADR-0160 treats them as configuration, and this ticket enforces "frozen for the life of a run" (ADR-0261 is context).
    Do not add recovery, a map snapshot, an auto-reopen, a heuristic or an ENV override. Failing loudly is the whole behavior. Depends on #895 (columns) and #880 (Map#path).

Required work:

  1. Digest the parsed bytes — check: present SHA256 in plugins/os-sdlc/lib/os_sdlc/adapter/map/loader.rb
  2. Store the digest on open — check: present map_digest in plugins/os-sdlc/lib/os_sdlc/command/implementation/open.rb
  3. Store the digest on reroute apply — check: present map_digest in plugins/os-sdlc/lib/os_sdlc/command/implementation/landing.rb
  4. Compare in the catalog — check: present ChangedError in plugins/os-sdlc/lib/os_sdlc/adapter/map/catalog.rb
  5. Name the error — check: present ChangedError in plugins/os-sdlc/lib/os_sdlc/model/map/changed_error.rb

Acceptance criteria

  1. Given an open implementation and an unchanged map file, when the map is loaded in another process, then it loads.
  2. Given an open implementation and a map file edited after open, when the map is loaded, then Map::ChangedError names the map x and says it changed since the implementation opened.
  3. Given a rerouted implementation, when the rerouted map file is edited after landing, then the same error is raised for that map.
  4. Given an implementation whose stored digest is NULL, when the map file is edited, then the map loads and no error is raised.

Owner sketch

class OsSdlc::Map::Catalog
  def load(name, digest: nil)
end

class OsSdlc::Map::ChangedError < OsSdlc::HarnessError
end

No map_digest method on Implementation: it would shadow the Sequel column reader.

Origin

  • Trigger: user ruling on the map reroute design, 2026-10-02 (ADR-0261)
  • Improvised this session: none
  • Root candidate: #859
  • Session: 616e6f92-69d9-4a00-bf30-33a77404deb3
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/616e6f92-69d9-4a00-bf30-33a77404deb3.jsonl
  • Chain: a map edit mid-run raises KeyError in Map#node ← maps are re-loaded by name in every process ← no content identity on the implementations row
  • Where: plugins/os-sdlc/lib/os_sdlc/adapter/map/catalog.rb

Parent: #859 (mention only; not a slice 4 blocker)
Blocked by: #895, #880

VERDICT: CREATE (ticket-skeptic 2026-10-02: user-approved; reproducible KeyError on mid-run map edit)

## Problem Every process re-loads the map by name: `Implementation#current_map` (model/implementation.rb:71) and `Implementation::Targeting#map` (model/implementation/targeting.rb:6). If the map file is edited while a run is open, `Map#node` raises on a node that no longer exists, deep inside a gate or hook. The run cannot tell that the map changed. Related to #859 (map reroute), not slice 4. ## Reproduce Open an implementation, rename a node in its map YAML, call `os-sdlc-next`: the error is a `KeyError` from `Map#node`, not a message about the map changing. ## Approach 1. The columns `implementations.map_digest` and `reroutes.map_digest` (nullable String) come from the #895 schema bump (29 -> 30). This ticket adds no schema change, only the behavior. 2. At `implementation-open`, compute the SHA-256 digest of the map file bytes that the Loader parsed (the same bytes, no separate read, to avoid a race with an edit) and store it in `implementations.map_digest`. When a reroute is applied (`Reroute#apply!` / `Landing#reroute!`), store the rerouted map's digest in `reroutes.map_digest` the same way. 3. Compare behind `Map::Catalog#load` (adapter/map/catalog.rb:23), the single choke point, rather than at each caller: `load` takes the stored digest, parses, and compares with the digest of the bytes it parsed. On mismatch it raises `Map::ChangedError` (`*Error` in `*_error.rb`, a `HarnessError` subclass like `Project::TargetSelectionError`, so model/map/changed_error.rb) whose message names the map and says the map changed since the implementation was opened and the implementation must be reopened. A NULL stored digest means no check (rows from before the bump). 4. A `bin/refresh-plugins` that ships a map edit fails every live run on that map, loudly. That is intended: ADR-0169 says map contents are frozen, ADR-0160 treats them as configuration, and this ticket enforces "frozen for the life of a run" (ADR-0261 is context). Do not add recovery, a map snapshot, an auto-reopen, a heuristic or an ENV override. Failing loudly is the whole behavior. Depends on #895 (columns) and #880 (`Map#path`). Required work: 1. Digest the parsed bytes — check: present `SHA256` in `plugins/os-sdlc/lib/os_sdlc/adapter/map/loader.rb` 2. Store the digest on open — check: present `map_digest` in `plugins/os-sdlc/lib/os_sdlc/command/implementation/open.rb` 3. Store the digest on reroute apply — check: present `map_digest` in `plugins/os-sdlc/lib/os_sdlc/command/implementation/landing.rb` 4. Compare in the catalog — check: present `ChangedError` in `plugins/os-sdlc/lib/os_sdlc/adapter/map/catalog.rb` 5. Name the error — check: present `ChangedError` in `plugins/os-sdlc/lib/os_sdlc/model/map/changed_error.rb` ## Acceptance criteria 1. Given an open implementation and an unchanged map file, when the map is loaded in another process, then it loads. 2. Given an open implementation and a map file edited after open, when the map is loaded, then `Map::ChangedError` names the map `x` and says it changed since the implementation opened. 3. Given a rerouted implementation, when the rerouted map file is edited after landing, then the same error is raised for that map. 4. Given an implementation whose stored digest is NULL, when the map file is edited, then the map loads and no error is raised. ## Owner sketch ```ruby class OsSdlc::Map::Catalog def load(name, digest: nil) end class OsSdlc::Map::ChangedError < OsSdlc::HarnessError end ``` No `map_digest` method on `Implementation`: it would shadow the Sequel column reader. ## Origin - Trigger: user ruling on the map reroute design, 2026-10-02 (ADR-0261) - Improvised this session: none - Root candidate: #859 - Session: 616e6f92-69d9-4a00-bf30-33a77404deb3 - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/616e6f92-69d9-4a00-bf30-33a77404deb3.jsonl - Chain: a map edit mid-run raises KeyError in Map#node ← maps are re-loaded by name in every process ← no content identity on the implementations row - Where: plugins/os-sdlc/lib/os_sdlc/adapter/map/catalog.rb Parent: #859 (mention only; not a slice 4 blocker) Blocked by: #895, #880 VERDICT: CREATE (ticket-skeptic 2026-10-02: user-approved; reproducible KeyError on mid-run map edit)
jared changed title from Store a hash of the map file on the run row at open to Store a digest of the map file on implementations at open and fail loudly on mismatch 2026-10-02 13:13:45 +00:00
Author
Owner

Work started on branch ticket-897 through /os-sdlc:implement (poodr-ticket-implementation, target os-sdlc).

Work started on branch ticket-897 through /os-sdlc:implement (poodr-ticket-implementation, target os-sdlc).
Author
Owner

Stale-handoff replay (implementation 6, 2026-10-02): the test-writer submitted the change-auditor's leftover scratchpad/h.json; subagent-stop replayed completed dispatch 24 and dispatch 26 stayed open. Skeptic verdict DROP: the root is the brief rule naming no file. Fix applied in briefs: each agent writes handoff-<dispatch_id>-.json and checks the reply's dispatch_id.

Stale-handoff replay (implementation 6, 2026-10-02): the test-writer submitted the change-auditor's leftover scratchpad/h.json; subagent-stop replayed completed dispatch 24 and dispatch 26 stayed open. Skeptic verdict DROP: the root is the brief rule naming no file. Fix applied in briefs: each agent writes handoff-<dispatch_id>-<agent>.json and checks the reply's dispatch_id.
Author
Owner

Resolution

Done: Map digest, option A (fail loudly only): Map::Source holds the map file path and the SHA-256 digest of its bytes. implementations.map_digest is stored at open, and reroutes.map_digest at reroute apply. Map::Catalog#load(name, digest:) raises Map::ChangedError ('reopen the implementation') when the file changed since then. No recovery and no snapshot.

Evidence: Branch ticket-897: d93e4600 (Runner commit, implementation 6, 16 dispatches, 21 min 24 s), e43b1864 (adds model/map/source.rb, which the Runner commit path list omitted), f3a9943c (History row). Full os-sdlc suite on the clean tree: 1425 runs, 0 failures, 0 errors. New test: plugins/os-sdlc/tests/model/implementation_map_digest_test.rb.

Follow-ups: #905 (Lens proposal: Implementation.open takes **columns, so a caller can override fixed columns; filed with skeptic VERDICT: CREATE). Stale-handoff replay during this run: skeptic DROP, reason posted on #897 and #901. The Runner commit omitted a new file created by the refactorer: noted in this close; I captured no separate ticket.

## Resolution **Done:** Map digest, option A (fail loudly only): Map::Source holds the map file path and the SHA-256 digest of its bytes. implementations.map_digest is stored at open, and reroutes.map_digest at reroute apply. Map::Catalog#load(name, digest:) raises Map::ChangedError ('reopen the implementation') when the file changed since then. No recovery and no snapshot. **Evidence:** Branch ticket-897: d93e4600 (Runner commit, implementation 6, 16 dispatches, 21 min 24 s), e43b1864 (adds model/map/source.rb, which the Runner commit path list omitted), f3a9943c (History row). Full os-sdlc suite on the clean tree: 1425 runs, 0 failures, 0 errors. New test: plugins/os-sdlc/tests/model/implementation_map_digest_test.rb. **Follow-ups:** #905 (Lens proposal: Implementation.open takes **columns, so a caller can override fixed columns; filed with skeptic VERDICT: CREATE). Stale-handoff replay during this run: skeptic DROP, reason posted on #897 and #901. The Runner commit omitted a new file created by the refactorer: noted in this close; I captured no separate ticket.
jared closed this issue 2026-10-02 15:09:53 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jared/cc-os#897
No description provided.