Resolve drafted maps from maps/drafts/ through Map::Catalog #880

Closed
opened 2026-10-01 15:26:46 +00:00 by jared · 8 comments
Owner

Problem

ADR-0258 decision 9 lets map-router draft a map at maps/drafts/<name>.yaml and the run continues on it. ADR-0261 makes the file location the draft status: there is no draft column on reroutes (it becomes applied_at, #895) and no draft: keyword on load. Nothing resolves a drafted name today. Implementation#current_map looks for maps/<name>.yaml and fails. presenter/brief.rb:6 resolves brief: paths from the map's directory; a draft in maps/drafts/ must still resolve them from the plugin maps directory (ADR-0258 decision 10). The A/B memo on this ticket is resolved by ADR-0261. Where drafts are written is open (#898); this ticket resolves names under the catalog directory only.

Reproduce

n/a: design change, nothing to reproduce.

Approach

  1. Add plugins/os-sdlc/maps/drafts/ (with .gitkeep).
  2. Map.new takes path: (Map::Loader#build_map passes the @path it holds). Add Map#path and Map#drafted?: File.basename(File.dirname(path)) == "drafts". Map#name returns File.basename(path, ".yaml").
  3. Map::Catalog (adapter/map/catalog.rb) resolves a name to exactly one file: maps/<name>.yaml first, then maps/drafts/<name>.yaml. It raises InvalidMapError when the name is in both and when it is in neither (Reroute::Draft#loaded_map, reroute/draft.rb:56, rescues only InvalidMapError and SystemCallError). load(name) takes no draft: keyword. Implementation#current_map calls catalog.load(reroutes.first.map) with no draft flag.
  4. Brief.for keeps Map::Catalog.default.dir as map_dir for drafts, so brief: paths resolve from maps/, not maps/drafts/.
  5. The approved_maps fact (query/brief/facts.rb) globs maps/*.yaml only, so drafts never reach a change-auditor reroute brief (ADR-0258 decision 9). Add a test that pins this. This test and the AC 4 and AC 5 tests pin existing behavior, so they are green from the start.
  6. Delete the name: line from plugins/os-sdlc/maps/*.yaml and every fixture map under plugins/os-sdlc/tests/fixtures/maps/, and remove :name from Map::Headers and data["name"] from the loader. Update the authoring prose in skills/map-making/SKILL.md. ADR-0263 (amends ADR-0261 decisions 2 and 5).
  7. Fixture maps whose name: header differs from their file basename are objections_loop.yaml (header objections-loop) and role_mismatch.yaml (header role-mismatch). Tests that assert map.name on those fixtures will see the basename after this change and must be checked.

Required work:

  1. Resolve drafted names under maps/drafts/ — check: present "drafts" in plugins/os-sdlc/lib/os_sdlc/adapter/map/catalog.rb
  2. Expose file location on the map — check: present def drafted? in plugins/os-sdlc/lib/os_sdlc/model/map.rb
  3. Pass the path from the loader — check: present path: in plugins/os-sdlc/lib/os_sdlc/adapter/map/loader.rb
  4. Derive the name from the path — check: present File.basename(path, ".yaml") in plugins/os-sdlc/lib/os_sdlc/model/map.rb
  5. Remove the name from the headers — check: absent :name in plugins/os-sdlc/lib/os_sdlc/model/map/headers.rb
  6. Stop reading the name header in the loader — check: absent data["name"] in plugins/os-sdlc/lib/os_sdlc/adapter/map/loader.rb
  7. Delete the name header from the plugin maps — check: absent name: in plugins/os-sdlc/maps/*.yaml
  8. Delete the name header from the fixture maps — check: absent name: in plugins/os-sdlc/tests/fixtures/maps/*.yaml
  9. Delete the name from the test map builder — check: absent "name" => "built" in plugins/os-sdlc/tests/support/entry_map_builder.rb
  10. Stop asking authors for the header — check: absent the map name in plugins/os-sdlc/skills/map-making/SKILL.md

Acceptance criteria

  1. Given a file maps/drafts/x.yaml and no maps/x.yaml, when Map::Catalog#load("x") runs, then it returns a map with drafted? true and path ending in maps/drafts/x.yaml.
  2. Given a file maps/x.yaml and no draft of that name, when Map::Catalog#load("x") runs, then it returns a map with drafted? false.
  3. Given a name in both maps/ and maps/drafts/, or in neither, when Map::Catalog#load runs, then it raises an error that names the name and the directories searched.
  4. Given a drafted map whose node has brief: briefs/start.md, when that node's brief is composed, then the file resolves from the catalog maps directory, not from drafts/.
  5. Given a file in maps/drafts/, when the approved_maps fact is resolved, then the joined document has no Map: line for it.
  6. Given maps/drafts/x.yaml, when it is loaded, then map.name is "x" and map.drafted? is true.

Owner sketch

class OsSdlc::Map::Catalog
  def load(name)
  def path_for(name) # raises when in both or neither
end

class OsSdlc::Map
  attr_reader :path
  def drafted?
end

Origin

  • Trigger: user ruling on the map reroute design, 2026-10-01 (ADR-0258 decisions 9-10; ADR-0259 A6)
  • Improvised this session: none
  • Chain: an unmapped ticket shape is hand-finished ← map-router can only select approved maps ← DESIGN (docs/adr/0258)
  • Root candidate: #859
  • Where: plugins/os-sdlc/lib/os_sdlc/presenter/brief.rb
  • Improvised this session: none
  • Root candidate: #859
  • Session: 55a56279-83ee-47d5-a21e-ee67f9e28abd
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/55a56279-83ee-47d5-a21e-ee67f9e28abd.jsonl

Parent: #859
Blocked by: #868, #871, #885, #895

## Problem ADR-0258 decision 9 lets map-router draft a map at `maps/drafts/<name>.yaml` and the run continues on it. ADR-0261 makes the file location the draft status: there is no `draft` column on `reroutes` (it becomes `applied_at`, #895) and no `draft:` keyword on load. Nothing resolves a drafted name today. `Implementation#current_map` looks for `maps/<name>.yaml` and fails. `presenter/brief.rb:6` resolves `brief:` paths from the map's directory; a draft in `maps/drafts/` must still resolve them from the plugin maps directory (ADR-0258 decision 10). The A/B memo on this ticket is resolved by ADR-0261. Where drafts are written is open (#898); this ticket resolves names under the catalog directory only. ## Reproduce n/a: design change, nothing to reproduce. ## Approach 1. Add `plugins/os-sdlc/maps/drafts/` (with `.gitkeep`). 2. `Map.new` takes `path:` (`Map::Loader#build_map` passes the `@path` it holds). Add `Map#path` and `Map#drafted?`: `File.basename(File.dirname(path)) == "drafts"`. `Map#name` returns `File.basename(path, ".yaml")`. 3. `Map::Catalog` (adapter/map/catalog.rb) resolves a name to exactly one file: `maps/<name>.yaml` first, then `maps/drafts/<name>.yaml`. It raises `InvalidMapError` when the name is in both and when it is in neither (`Reroute::Draft#loaded_map`, reroute/draft.rb:56, rescues only `InvalidMapError` and `SystemCallError`). `load(name)` takes no `draft:` keyword. `Implementation#current_map` calls `catalog.load(reroutes.first.map)` with no draft flag. 4. `Brief.for` keeps `Map::Catalog.default.dir` as `map_dir` for drafts, so `brief:` paths resolve from `maps/`, not `maps/drafts/`. 5. The `approved_maps` fact (query/brief/facts.rb) globs `maps/*.yaml` only, so drafts never reach a change-auditor reroute brief (ADR-0258 decision 9). Add a test that pins this. This test and the AC 4 and AC 5 tests pin existing behavior, so they are green from the start. 6. Delete the `name:` line from `plugins/os-sdlc/maps/*.yaml` and every fixture map under `plugins/os-sdlc/tests/fixtures/maps/`, and remove `:name` from `Map::Headers` and `data["name"]` from the loader. Update the authoring prose in `skills/map-making/SKILL.md`. ADR-0263 (amends ADR-0261 decisions 2 and 5). 7. Fixture maps whose `name:` header differs from their file basename are `objections_loop.yaml` (header `objections-loop`) and `role_mismatch.yaml` (header `role-mismatch`). Tests that assert `map.name` on those fixtures will see the basename after this change and must be checked. Required work: 1. Resolve drafted names under `maps/drafts/` — check: present `"drafts"` in `plugins/os-sdlc/lib/os_sdlc/adapter/map/catalog.rb` 2. Expose file location on the map — check: present `def drafted?` in `plugins/os-sdlc/lib/os_sdlc/model/map.rb` 3. Pass the path from the loader — check: present `path:` in `plugins/os-sdlc/lib/os_sdlc/adapter/map/loader.rb` 4. Derive the name from the path — check: present `File.basename(path, ".yaml")` in `plugins/os-sdlc/lib/os_sdlc/model/map.rb` 5. Remove the name from the headers — check: absent `:name` in `plugins/os-sdlc/lib/os_sdlc/model/map/headers.rb` 6. Stop reading the name header in the loader — check: absent `data["name"]` in `plugins/os-sdlc/lib/os_sdlc/adapter/map/loader.rb` 7. Delete the name header from the plugin maps — check: absent `name:` in `plugins/os-sdlc/maps/*.yaml` 8. Delete the name header from the fixture maps — check: absent `name:` in `plugins/os-sdlc/tests/fixtures/maps/*.yaml` 9. Delete the name from the test map builder — check: absent `"name" => "built"` in `plugins/os-sdlc/tests/support/entry_map_builder.rb` 10. Stop asking authors for the header — check: absent `the map name` in `plugins/os-sdlc/skills/map-making/SKILL.md` ## Acceptance criteria 1. Given a file `maps/drafts/x.yaml` and no `maps/x.yaml`, when `Map::Catalog#load("x")` runs, then it returns a map with `drafted?` true and `path` ending in `maps/drafts/x.yaml`. 2. Given a file `maps/x.yaml` and no draft of that name, when `Map::Catalog#load("x")` runs, then it returns a map with `drafted?` false. 3. Given a name in both `maps/` and `maps/drafts/`, or in neither, when `Map::Catalog#load` runs, then it raises an error that names the name and the directories searched. 4. Given a drafted map whose node has `brief: briefs/start.md`, when that node's brief is composed, then the file resolves from the catalog maps directory, not from `drafts/`. 5. Given a file in `maps/drafts/`, when the `approved_maps` fact is resolved, then the joined document has no `Map:` line for it. 6. Given `maps/drafts/x.yaml`, when it is loaded, then `map.name` is "x" and `map.drafted?` is true. ## Owner sketch ```ruby class OsSdlc::Map::Catalog def load(name) def path_for(name) # raises when in both or neither end class OsSdlc::Map attr_reader :path def drafted? end ``` ## Origin - Trigger: user ruling on the map reroute design, 2026-10-01 (ADR-0258 decisions 9-10; ADR-0259 A6) - Improvised this session: none - Chain: an unmapped ticket shape is hand-finished ← map-router can only select approved maps ← DESIGN (docs/adr/0258) - Root candidate: #859 - Where: plugins/os-sdlc/lib/os_sdlc/presenter/brief.rb - Improvised this session: none - Root candidate: #859 - Session: 55a56279-83ee-47d5-a21e-ee67f9e28abd - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/55a56279-83ee-47d5-a21e-ee67f9e28abd.jsonl Parent: #859 Blocked by: #868, #871, #885, #895
Author
Owner

Work started: os-sdlc pipeline on branch ticket-880 (worktree .claude/worktrees/ticket-880).

Work started: os-sdlc pipeline on branch ticket-880 (worktree .claude/worktrees/ticket-880).
Author
Owner

Ruling needed: what does reroutes.draft mean?

Implementation 14 escalated at change-auditor (dispatch 88, verdict: fail, escalation 6). The run waits at needs-orchestrator. #880, #881 and #882 wait for this ruling.

Conflict. Two Accepted ADRs give the one column reroutes.draft two meanings:

  • ADR-0258 (schema, line 53): draft true means "resolve maps/drafts/<name>.yaml" (a drafted map). #880 AC 1 depends on this meaning.
  • ADR-0260 (Negative): "the Reroute table needs a draft marker" for a pending reroute. #873 implemented that marker with the same column:
# model/reroute.rb
def applied = where(draft: false)
def drafts = where(draft: true)
def apply! = update(draft: false)
# reroute/draft.rb:28
implementation.add_reroute(**@proposal.to_h, draft: true, dispatch_id: requester&.id)
# model/implementation.rb
def current_map
  reroutes_dataset.applied.first&.then { |reroute| Map::Catalog.default.load(reroute.map) } || map
end

Landing sets draft: false, so after apply! no row can say "this map came from maps/drafts/". #880 AC 1 ("a reroutes row with draft: true ... current_map loads maps/drafts/x.yaml") cannot hold.

Option A: a timestamp records application; draft keeps its ADR-0258 meaning

# migration: add applied_at (DateTime, null), backfill from draft: false rows; draft = drafted map
def applied = exclude(applied_at: nil)
def pending = where(applied_at: nil)
def apply! = update(applied_at: Time.now)
def current_map
  reroutes_dataset.applied.first&.then { |r| Map::Catalog.default.load(r.map, draft: r.draft) } || map
end

Amends the ADR-0260 wording ("draft marker" becomes applied_at). Follows the architecture rule "Record transitions as timestamps. Never add a state or status column" (.claude/rules/os-sdlc-architecture.md).

Option B: draft keeps the pending meaning; a new column marks a drafted map

# migration: add drafted_map (TrueClass, default false)
def current_map
  reroutes_dataset.applied.first&.then { |r| Map::Catalog.default.load(r.map, draft: r.drafted_map) } || map
end

Amends the ADR-0258 schema (the drafted-map flag gets a new name). The draft boolean stays a persisted state flag.

Recommendation: A, confidence 80%. The deciding reason: pending-then-applied is a transition, and the repo rule records transitions as timestamps; that also restores ADR-0258's literal schema, so #880 and #881 need no text change.

Ask and default. Reply A or B (or another shape). With A, I file one ticket for the applied_at change (schema bump, amending ADR to ADR-0260), run it before #880, and then run #880 -> #881 -> #882. If you say nothing, #880, #881 and #882 stay open and implementation 14 stays waiting; no code changes.

## Ruling needed: what does `reroutes.draft` mean? Implementation 14 escalated at change-auditor (dispatch 88, `verdict: fail`, escalation 6). The run waits at `needs-orchestrator`. #880, #881 and #882 wait for this ruling. **Conflict.** Two Accepted ADRs give the one column `reroutes.draft` two meanings: - ADR-0258 (schema, line 53): `draft` true means "resolve `maps/drafts/<name>.yaml`" (a drafted map). #880 AC 1 depends on this meaning. - ADR-0260 (Negative): "the Reroute table needs a `draft` marker" for a pending reroute. #873 implemented that marker with the same column: ```ruby # model/reroute.rb def applied = where(draft: false) def drafts = where(draft: true) def apply! = update(draft: false) # reroute/draft.rb:28 implementation.add_reroute(**@proposal.to_h, draft: true, dispatch_id: requester&.id) # model/implementation.rb def current_map reroutes_dataset.applied.first&.then { |reroute| Map::Catalog.default.load(reroute.map) } || map end ``` Landing sets `draft: false`, so after `apply!` no row can say "this map came from `maps/drafts/`". #880 AC 1 ("a reroutes row with `draft: true` ... `current_map` loads `maps/drafts/x.yaml`") cannot hold. **Option A: a timestamp records application; `draft` keeps its ADR-0258 meaning** ```ruby # migration: add applied_at (DateTime, null), backfill from draft: false rows; draft = drafted map def applied = exclude(applied_at: nil) def pending = where(applied_at: nil) def apply! = update(applied_at: Time.now) def current_map reroutes_dataset.applied.first&.then { |r| Map::Catalog.default.load(r.map, draft: r.draft) } || map end ``` Amends the ADR-0260 wording ("draft marker" becomes `applied_at`). Follows the architecture rule "Record transitions as timestamps. Never add a state or status column" (.claude/rules/os-sdlc-architecture.md). **Option B: `draft` keeps the pending meaning; a new column marks a drafted map** ```ruby # migration: add drafted_map (TrueClass, default false) def current_map reroutes_dataset.applied.first&.then { |r| Map::Catalog.default.load(r.map, draft: r.drafted_map) } || map end ``` Amends the ADR-0258 schema (the drafted-map flag gets a new name). The `draft` boolean stays a persisted state flag. **Recommendation: A, confidence 80%.** The deciding reason: pending-then-applied is a transition, and the repo rule records transitions as timestamps; that also restores ADR-0258's literal schema, so #880 and #881 need no text change. **Ask and default.** Reply A or B (or another shape). With A, I file one ticket for the `applied_at` change (schema bump, amending ADR to ADR-0260), run it before #880, and then run #880 -> #881 -> #882. If you say nothing, #880, #881 and #882 stay open and implementation 14 stays waiting; no code changes.
jared changed title from Resolve drafted maps from maps/drafts/ through Map::Catalog to Resolve drafted maps from maps/drafts/ through MapCatalog 2026-10-02 12:45:06 +00:00
Author
Owner

The A/B memo on this ticket is resolved by ADR-0261 (commit 93202771). The YAML file is the source of truth; the file location (maps/ or maps/drafts/) is the draft status; MapCatalog (#896) resolves a name to exactly one file. Body rewritten. Blocked by #895 and #896; drafts location is open in #898.

The A/B memo on this ticket is resolved by ADR-0261 (commit 93202771). The YAML file is the source of truth; the file location (maps/ or maps/drafts/) is the draft status; MapCatalog (#896) resolves a name to exactly one file. Body rewritten. Blocked by #895 and #896; drafts location is open in #898.
jared changed title from Resolve drafted maps from maps/drafts/ through MapCatalog to Resolve drafted maps from maps/drafts/ through Map::Catalog 2026-10-02 13:04:25 +00:00
Author
Owner

Implementation 14 abandoned with os-sdlc-implementation-abandon (#899) on 2026-10-02. #880 re-runs with a fresh implementation-open after #895 lands.

Implementation 14 abandoned with os-sdlc-implementation-abandon (#899) on 2026-10-02. #880 re-runs with a fresh implementation-open after #895 lands.
Author
Owner

Work started: /os-sdlc:implement on branch ticket-880 (worktree .claude/worktrees/ticket-880), after #895 (schema 30) and ADR-0261/0262/0263.

Work started: /os-sdlc:implement on branch ticket-880 (worktree .claude/worktrees/ticket-880), after #895 (schema 30) and ADR-0261/0262/0263.
Author
Owner

Blocked 2026-10-02. Implementation 4 failed at change-auditor (dispatch 15): verdict:error. The agent's real pass handoff was refused by the harness as too complex three times and once failed JSON parsing; it then ran os-sdlc-subagent-stop with a stub verdict: error to test the submission, and the Runner accepted it as the dispatch's verdict and closed the run. Earlier today: implementation 1 failed on a stale Required-work path (fixed in the body); implementation 2 escalated on a test-writer stub defect (hand-fixed c585e62c); implementation 3 hit unexpected_pass because implementation 2's uncommitted production changes were still in the worktree (set aside to the session scratchpad as a patch). Recovery: a fresh implementation-open. Defect ticket: #901.

Blocked 2026-10-02. Implementation 4 failed at change-auditor (dispatch 15): verdict:error. The agent's real pass handoff was refused by the harness as too complex three times and once failed JSON parsing; it then ran os-sdlc-subagent-stop with a stub verdict: error to test the submission, and the Runner accepted it as the dispatch's verdict and closed the run. Earlier today: implementation 1 failed on a stale Required-work path (fixed in the body); implementation 2 escalated on a test-writer stub defect (hand-fixed c585e62c); implementation 3 hit unexpected_pass because implementation 2's uncommitted production changes were still in the worktree (set aside to the session scratchpad as a patch). Recovery: a fresh implementation-open. Defect ticket: #901.
Author
Owner

Resumed 2026-10-02: fresh implementation-open (run 5) per user decision; #901 tracks the run-4 defect.

Resumed 2026-10-02: fresh implementation-open (run 5) per user decision; #901 tracks the run-4 defect.
Author
Owner

Resolution

Done: Map gains path and drafted?; Map#name is File.basename(path, '.yaml') and the name: header is removed from Map::Headers, the loader, plugin maps and all fixture maps; Map::Catalog resolves maps/ then maps/drafts/ and raises InvalidMapError naming both candidate files when a name is in both or neither; load takes no draft: keyword; maps/drafts/ exists; map-making prose updated.

Evidence: Commits bd38b788 (Runner, implementation 5: 8 dispatches, 0 retries, suite-check green, contract-auditor pass) and 21876554 (fixture maps + History row); test hand-fix c585e62c from implementation 2. ADR-0261, ADR-0262, ADR-0263.

Follow-ups: #901 (Runner accepts a test submission as verdict), #903 (test-reviewer stub gap; diff-test stops at first red), #897 and #900 now unblocked; lens proposal on the drafts directory literal is being challenged and, if justified, captured as a ready-for-human Lens proposal ticket.

## Resolution **Done:** Map gains path and drafted?; Map#name is File.basename(path, '.yaml') and the name: header is removed from Map::Headers, the loader, plugin maps and all fixture maps; Map::Catalog resolves maps/ then maps/drafts/ and raises InvalidMapError naming both candidate files when a name is in both or neither; load takes no draft: keyword; maps/drafts/ exists; map-making prose updated. **Evidence:** Commits bd38b788 (Runner, implementation 5: 8 dispatches, 0 retries, suite-check green, contract-auditor pass) and 21876554 (fixture maps + History row); test hand-fix c585e62c from implementation 2. ADR-0261, ADR-0262, ADR-0263. **Follow-ups:** #901 (Runner accepts a test submission as verdict), #903 (test-reviewer stub gap; diff-test stops at first red), #897 and #900 now unblocked; lens proposal on the drafts directory literal is being challenged and, if justified, captured as a ready-for-human Lens proposal ticket.
jared closed this issue 2026-10-02 14:34:55 +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.

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