Replace reroutes.draft with applied_at (schema 30) and scope Draft#replace_draft! to its own dispatch #895

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

Problem

ADR-0261 decision 3 replaces the reroutes.draft boolean with applied_at (nil = pending). Reroute::Draft writes the row when map-router proposes and Landing#reroute! applies it later; a pending row that never lands must stay distinguishable, and created_at does not record the landing. Separately, Reroute::Draft#replace_draft! (reroute/draft.rb) runs implementation.reroutes_dataset.drafts.delete, which deletes every pending row of the implementation. ADR-0260 decision 3 says one row per dispatch, so it must delete only the rows of the same dispatch. Note: the row's dispatch_id is the REQUESTING dispatch (@dispatch.received_handoff&.creating_dispatch), not the map-router dispatch, so "same dispatch" is dispatch_id: requester&.id (nil for the orchestrator path). Implementation#draft_reroute_for finds the row by created_at >= dispatch.created_at.

Reproduce

In a test, add a pending row for requester dispatch A, then run Reroute::Draft#write for a map-router dispatch whose requester is B: the row for A is deleted today.

Approach

  1. adapter/db.rb: replace t.TrueClass :draft ... (line 205) with t.DateTime :applied_at (nullable). Bump SCHEMA_VERSION 29 -> 30. No backfill (a stale DB is archived and recreated; reroutes is empty).
  2. model/reroute.rb: applied = exclude(applied_at: nil); pending = where(applied_at: nil) (rename of drafts); apply! is update(applied_at: Time.now); to_report replaces draft: with applied_at (presenter/implementation/summary.rb:50 renders every row, so dropping draft: without a replacement loses pending vs applied).
  3. Update every caller of drafts: model/implementation.rb:77 (draft_reroute_for), command/implementation/landing.rb, query/reroute/check.rb. Reroute::Draft#write stops passing draft: true.
  4. reroute/draft.rb replace_draft!: delete only pending rows whose dispatch_id equals requester&.id.
  5. Update tests and test helpers that build rows with draft:.
  6. Test helper tests/support/reroute_test_case.rb:77 (add_reroute) must pass applied_at: Time.now when it builds an applied row. The old default draft: false meant applied; the new default nil means pending, so tests/model/implementation_current_map_test.rb, tests/command/implementation/transition_test.rb and tests/controller/implementation/change_auditor_reroute_test.rb would silently test pending rows.
  7. tests/controller/implementation/map_update_test.rb: line 144 refute row[:draft], line 154 draft: false, and the test names and message text that say "draft" (lines 25, 35, 89, 97, 101). tests/presenter/implementation/summary_reroutes_test.rb: lines 15 and 17.
  8. Rename Implementation#draft_reroute_for (model/implementation.rb:76-77) to pending_reroute_for and its callers: command/implementation/landing.rb (draft at lines 56, 60, 63) and query/reroute/check.rb (line 11, missing_draft_line, and the message "no draft reroute recorded" at line 19, which becomes "no pending reroute recorded"). agents/map-router.md lines 15 and 36 say "draft reroute": change to "pending reroute".
  9. Rename the column implementations.map_path to map_name (adapter/db.rb:336, add_implementation_binding_columns). The column holds a map NAME, not a path. Do not add a map_name method or any new class on Implementation: Sequel gives the column reader. Callers that need the Map use the existing Implementation#map (model/implementation/targeting.rb:6, Map::Catalog.default.load(map_path), which becomes load(map_name)), and where they need the name, map.name (duck typing). Map::Catalog keeps its name (ADR-0262). Column readers and writers of map_path found in plugins/os-sdlc: model/implementation.rb:56 (create(... map_path: work_order.map_path ...) becomes map_name:), model/implementation/targeting.rb:6, tests/command/implementation/open_map_name_test.rb:22 (row[:map_path]), tests/model/implementation_current_map_test.rb:22 (reloaded.map_path), and the direct implementations inserts or Implementation.create calls with map_path: "map.yml" in tests/model/handoff_test.rb:11 and :80, tests/model/correction_round_test.rb:10 and :86, tests/model/step_result_test.rb:111, tests/model/inverse_associations_test.rb:8, tests/model/implementation/landing/escalation_reason_test.rb:42, tests/command/gate/run_classification_test.rb:35. Not the column, so they may keep their own name: the CLI-side work-order field Dispatch::WorkOrder#map_path (model/dispatch/work_order.rb:5-6), ArgsHolder#map_path! and its use in work_order_fields! (parser/dispatch_commands/args_holder.rb:28,38), command/implementation/open.rb:18, and the roughly 80 WorkOrder.new(... map_path: ...) calls in tests. Do not rename them in this ticket; the CLI positional argument is a map name too, so renaming that field to map_name is a separate follow-up.
  10. Add a nullable map_digest String column to implementations and to reroutes in the same schema bump (for #897, which adds only the behavior: store, compare, raise). Do not add a map_digest method on Implementation or Reroute: Sequel gives the column reader.
  11. One History row in docs/implementation-status/os-sdlc.md; run bin/refresh-plugins.
    Do not bump the schema while a run is live (ADR-0129, ADR-0155); #880 (implementation 14) must be abandoned or finished first.

Required work:

  1. Replace the column — check: present applied_at in plugins/os-sdlc/lib/os_sdlc/adapter/db.rb
  2. Bump the schema version — check: present SCHEMA_VERSION = 30 in plugins/os-sdlc/lib/os_sdlc/adapter/db.rb
  3. Remove the draft flag from the model — check: absent draft: in plugins/os-sdlc/lib/os_sdlc/model/reroute.rb
  4. Scope the replace to one dispatch — check: present dispatch_id in plugins/os-sdlc/lib/os_sdlc/reroute/draft.rb
  5. Emit applied_at in the reroute report — check: present applied_at: applied_at in plugins/os-sdlc/lib/os_sdlc/model/reroute.rb
  6. Test helper builds applied rows with applied_at — check: present applied_at in plugins/os-sdlc/tests/support/reroute_test_case.rb
  7. Update the map-update test — check: absent draft in plugins/os-sdlc/tests/controller/implementation/map_update_test.rb
  8. Update the summary reroutes test — check: absent draft in plugins/os-sdlc/tests/presenter/implementation/summary_reroutes_test.rb
  9. Rename the finder — check: absent draft_reroute_for in plugins/os-sdlc/lib/**/*.rb
  10. Rename the agent wording — check: absent draft reroute in plugins/os-sdlc/agents/map-router.md
  11. Rename the column to map_name — check: absent map_path in plugins/os-sdlc/lib/os_sdlc/adapter/db.rb
  12. Add the nullable digest columns — check: present map_digest in plugins/os-sdlc/lib/os_sdlc/adapter/db.rb
  13. Record the build and refresh the plugin cache (bin/refresh-plugins) — check: present #895 in docs/implementation-status/os-sdlc.md

Acceptance criteria

  1. Given a reroute row with applied_at nil, when Reroute.pending and Reroute.applied are read, then the row is in pending only.
  2. Given a pending row, when apply! runs, then applied_at is set and the row is in applied only; current_map and the cap count it.
  3. Given a pending row for requester dispatch A and a map-router dispatch whose requester is B, when Reroute::Draft#write runs, then A's row remains and B's new row is added.
  4. Given a pending row for requester B, when write runs a second time for the same requester, then one pending row for B remains.
  5. Given the new schema, when a database at schema 29 is opened, then it is archived and recreated, and reroutes has applied_at and no draft column.
  6. Given an applied row and a pending row, when to_report runs on each, then the report has applied_at (a time for the applied row, nil for the pending row) and no draft key, and the run summary shows it.
  7. Given the schema-30 database, when implementations is read, then it has a map_name column and no map_path column.
  8. Given the schema-30 database, when implementations and reroutes are read, then each has a nullable map_digest column.

Owner sketch

class OsSdlc::Reroute < Sequel::Model
  dataset_module { def applied; def pending }
  def apply!
end

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: draft boolean cannot show landing time ← Draft and Landing are two events ← DESIGN (docs/adr/0261)
  • Where: plugins/os-sdlc/lib/os_sdlc/model/reroute.rb

Parent: #859
Blocked by: #899

VERDICT: CREATE (ticket-skeptic 2026-10-02: user-directed ADR-0261 ruling; fixes replace_draft! scope defect)

## Problem ADR-0261 decision 3 replaces the `reroutes.draft` boolean with `applied_at` (nil = pending). `Reroute::Draft` writes the row when map-router proposes and `Landing#reroute!` applies it later; a pending row that never lands must stay distinguishable, and `created_at` does not record the landing. Separately, `Reroute::Draft#replace_draft!` (reroute/draft.rb) runs `implementation.reroutes_dataset.drafts.delete`, which deletes every pending row of the implementation. ADR-0260 decision 3 says one row per dispatch, so it must delete only the rows of the same dispatch. Note: the row's `dispatch_id` is the REQUESTING dispatch (`@dispatch.received_handoff&.creating_dispatch`), not the map-router dispatch, so "same dispatch" is `dispatch_id: requester&.id` (nil for the orchestrator path). `Implementation#draft_reroute_for` finds the row by `created_at >= dispatch.created_at`. ## Reproduce In a test, add a pending row for requester dispatch A, then run `Reroute::Draft#write` for a map-router dispatch whose requester is B: the row for A is deleted today. ## Approach 1. adapter/db.rb: replace `t.TrueClass :draft ...` (line 205) with `t.DateTime :applied_at` (nullable). Bump SCHEMA_VERSION 29 -> 30. No backfill (a stale DB is archived and recreated; reroutes is empty). 2. model/reroute.rb: `applied = exclude(applied_at: nil)`; `pending = where(applied_at: nil)` (rename of `drafts`); `apply!` is `update(applied_at: Time.now)`; `to_report` replaces `draft:` with `applied_at` (presenter/implementation/summary.rb:50 renders every row, so dropping `draft:` without a replacement loses pending vs applied). 3. Update every caller of `drafts`: model/implementation.rb:77 (`draft_reroute_for`), command/implementation/landing.rb, query/reroute/check.rb. `Reroute::Draft#write` stops passing `draft: true`. 4. reroute/draft.rb `replace_draft!`: delete only pending rows whose `dispatch_id` equals `requester&.id`. 5. Update tests and test helpers that build rows with `draft:`. 6. Test helper tests/support/reroute_test_case.rb:77 (`add_reroute`) must pass `applied_at: Time.now` when it builds an applied row. The old default `draft: false` meant applied; the new default nil means pending, so tests/model/implementation_current_map_test.rb, tests/command/implementation/transition_test.rb and tests/controller/implementation/change_auditor_reroute_test.rb would silently test pending rows. 7. tests/controller/implementation/map_update_test.rb: line 144 `refute row[:draft]`, line 154 `draft: false`, and the test names and message text that say "draft" (lines 25, 35, 89, 97, 101). tests/presenter/implementation/summary_reroutes_test.rb: lines 15 and 17. 8. Rename `Implementation#draft_reroute_for` (model/implementation.rb:76-77) to `pending_reroute_for` and its callers: command/implementation/landing.rb (`draft` at lines 56, 60, 63) and query/reroute/check.rb (line 11, `missing_draft_line`, and the message "no draft reroute recorded" at line 19, which becomes "no pending reroute recorded"). agents/map-router.md lines 15 and 36 say "draft reroute": change to "pending reroute". 9. Rename the column `implementations.map_path` to `map_name` (adapter/db.rb:336, `add_implementation_binding_columns`). The column holds a map NAME, not a path. Do not add a `map_name` method or any new class on Implementation: Sequel gives the column reader. Callers that need the Map use the existing `Implementation#map` (model/implementation/targeting.rb:6, `Map::Catalog.default.load(map_path)`, which becomes `load(map_name)`), and where they need the name, `map.name` (duck typing). `Map::Catalog` keeps its name (ADR-0262). Column readers and writers of `map_path` found in plugins/os-sdlc: model/implementation.rb:56 (`create(... map_path: work_order.map_path ...)` becomes `map_name:`), model/implementation/targeting.rb:6, tests/command/implementation/open_map_name_test.rb:22 (`row[:map_path]`), tests/model/implementation_current_map_test.rb:22 (`reloaded.map_path`), and the direct `implementations` inserts or `Implementation.create` calls with `map_path: "map.yml"` in tests/model/handoff_test.rb:11 and :80, tests/model/correction_round_test.rb:10 and :86, tests/model/step_result_test.rb:111, tests/model/inverse_associations_test.rb:8, tests/model/implementation/landing/escalation_reason_test.rb:42, tests/command/gate/run_classification_test.rb:35. Not the column, so they may keep their own name: the CLI-side work-order field `Dispatch::WorkOrder#map_path` (model/dispatch/work_order.rb:5-6), `ArgsHolder#map_path!` and its use in `work_order_fields!` (parser/dispatch_commands/args_holder.rb:28,38), `command/implementation/open.rb:18`, and the roughly 80 `WorkOrder.new(... map_path: ...)` calls in tests. Do not rename them in this ticket; the CLI positional argument is a map name too, so renaming that field to `map_name` is a separate follow-up. 10. Add a nullable `map_digest` String column to `implementations` and to `reroutes` in the same schema bump (for #897, which adds only the behavior: store, compare, raise). Do not add a `map_digest` method on Implementation or Reroute: Sequel gives the column reader. 11. One History row in docs/implementation-status/os-sdlc.md; run `bin/refresh-plugins`. Do not bump the schema while a run is live (ADR-0129, ADR-0155); #880 (implementation 14) must be abandoned or finished first. Required work: 1. Replace the column — check: present `applied_at` in `plugins/os-sdlc/lib/os_sdlc/adapter/db.rb` 2. Bump the schema version — check: present `SCHEMA_VERSION = 30` in `plugins/os-sdlc/lib/os_sdlc/adapter/db.rb` 3. Remove the draft flag from the model — check: absent `draft: ` in `plugins/os-sdlc/lib/os_sdlc/model/reroute.rb` 4. Scope the replace to one dispatch — check: present `dispatch_id` in `plugins/os-sdlc/lib/os_sdlc/reroute/draft.rb` 5. Emit `applied_at` in the reroute report — check: present `applied_at: applied_at` in `plugins/os-sdlc/lib/os_sdlc/model/reroute.rb` 6. Test helper builds applied rows with `applied_at` — check: present `applied_at` in `plugins/os-sdlc/tests/support/reroute_test_case.rb` 7. Update the map-update test — check: absent `draft` in `plugins/os-sdlc/tests/controller/implementation/map_update_test.rb` 8. Update the summary reroutes test — check: absent `draft` in `plugins/os-sdlc/tests/presenter/implementation/summary_reroutes_test.rb` 9. Rename the finder — check: absent `draft_reroute_for` in `plugins/os-sdlc/lib/**/*.rb` 10. Rename the agent wording — check: absent `draft reroute` in `plugins/os-sdlc/agents/map-router.md` 11. Rename the column to `map_name` — check: absent `map_path` in `plugins/os-sdlc/lib/os_sdlc/adapter/db.rb` 12. Add the nullable digest columns — check: present `map_digest` in `plugins/os-sdlc/lib/os_sdlc/adapter/db.rb` 13. Record the build and refresh the plugin cache (`bin/refresh-plugins`) — check: present `#895` in `docs/implementation-status/os-sdlc.md` ## Acceptance criteria 1. Given a reroute row with `applied_at` nil, when `Reroute.pending` and `Reroute.applied` are read, then the row is in pending only. 2. Given a pending row, when `apply!` runs, then `applied_at` is set and the row is in `applied` only; `current_map` and the cap count it. 3. Given a pending row for requester dispatch A and a map-router dispatch whose requester is B, when `Reroute::Draft#write` runs, then A's row remains and B's new row is added. 4. Given a pending row for requester B, when `write` runs a second time for the same requester, then one pending row for B remains. 5. Given the new schema, when a database at schema 29 is opened, then it is archived and recreated, and `reroutes` has `applied_at` and no `draft` column. 6. Given an applied row and a pending row, when `to_report` runs on each, then the report has `applied_at` (a time for the applied row, nil for the pending row) and no `draft` key, and the run summary shows it. 7. Given the schema-30 database, when `implementations` is read, then it has a `map_name` column and no `map_path` column. 8. Given the schema-30 database, when `implementations` and `reroutes` are read, then each has a nullable `map_digest` column. ## Owner sketch ```ruby class OsSdlc::Reroute < Sequel::Model dataset_module { def applied; def pending } def apply! end ``` ## 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: draft boolean cannot show landing time ← Draft and Landing are two events ← DESIGN (docs/adr/0261) - Where: plugins/os-sdlc/lib/os_sdlc/model/reroute.rb Parent: #859 Blocked by: #899 VERDICT: CREATE (ticket-skeptic 2026-10-02: user-directed ADR-0261 ruling; fixes replace_draft! scope defect)
Author
Owner

Work started: /os-sdlc:implement on branch ticket-895 (worktree .claude/worktrees/ticket-895). Implementation 14 was abandoned through #899, so the schema bump is allowed.

Work started: /os-sdlc:implement on branch ticket-895 (worktree .claude/worktrees/ticket-895). Implementation 14 was abandoned through #899, so the schema bump is allowed.
Author
Owner

Resolution

Done: Schema 30: reroutes.draft replaced by nullable applied_at (pending = nil; applied/pending scopes; apply! stamps the time; to_report emits applied_at); replace_draft! scoped to the requester dispatch; draft_reroute_for renamed pending_reroute_for; implementations.map_path column renamed map_name; nullable map_digest on implementations and reroutes; map-router.md says pending reroute.

Evidence: Commit bd16ce63 on ticket-895 (implementation 16 complete, 12 dispatches, test-reviewer pass after 2 rounds, contract-auditor pass first time); suite-check green; new tests tests/adapter/db_schema_30_test.rb, tests/model/reroute_applied_at_test.rb, tests/reroute/draft_test.rb; History row in docs/implementation-status/os-sdlc.md.

Follow-ups: WorkOrder#map_path rename captured as #900 (user-directed). Map digest behavior is #897. Pipeline DB moves to schema 30 on first connect from main after merge.

## Resolution **Done:** Schema 30: reroutes.draft replaced by nullable applied_at (pending = nil; applied/pending scopes; apply! stamps the time; to_report emits applied_at); replace_draft! scoped to the requester dispatch; draft_reroute_for renamed pending_reroute_for; implementations.map_path column renamed map_name; nullable map_digest on implementations and reroutes; map-router.md says pending reroute. **Evidence:** Commit bd16ce63 on ticket-895 (implementation 16 complete, 12 dispatches, test-reviewer pass after 2 rounds, contract-auditor pass first time); suite-check green; new tests tests/adapter/db_schema_30_test.rb, tests/model/reroute_applied_at_test.rb, tests/reroute/draft_test.rb; History row in docs/implementation-status/os-sdlc.md. **Follow-ups:** WorkOrder#map_path rename captured as #900 (user-directed). Map digest behavior is #897. Pipeline DB moves to schema 30 on first connect from main after merge.
jared closed this issue 2026-10-02 13:45:28 +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#895
No description provided.