Map owns the XDG drafts directory (Map::DRAFTS_DIR); Map::Catalog searches the approved and drafts roots #904

Closed
opened 2026-10-02 14:35:47 +00:00 by jared · 5 comments
Owner

Problem

Drafted maps live in maps/drafts/ inside the plugin tree, and the drafts directory name "drafts" has two owners. Map#drafted? (model/map.rb:22) spells the literal. Map::Catalog::DRAFTS_DIR (adapter/map/catalog.rb:7) spells it again. A12 requires one owner for a rule. Only the #880 Required-work check R1 (present `"drafts"` in `plugins/os-sdlc/lib/os_sdlc/adapter/map/catalog.rb` ) keeps the second copy. No code fact needs it.

ADR-0264 moves drafted maps to an XDG data directory. So the one owner, Map::DRAFTS_DIR, holds the XDG drafts path, and Map::Catalog searches two roots: the approved plugin root and that drafts root. This is more than a rename: the drafts location changes, and the catalog no longer derives it from its own directory.

LENS line (verbatim):
LENS: A12 broken at plugins/os-sdlc/lib/os_sdlc/model/map.rb:22 - BREAKS: the drafts directory name is spelled in Map#drafted? and again as Map::Catalog::DRAFTS_DIR (adapter/map/catalog.rb:7); JUSTIFIED: yes, one owner (Map::DRAFTS_DIR used by Catalog) removes the literal that Required-work R1 checks in catalog.rb, so the gate fails (principle 4); PROPOSAL: NONE, the tension comes from this ticket check wording and will not recur; the commit records it.

Parent: #859
Ticket: #904 (owner of this change; found in #880 implementation 5, dispatch 23)

Verdict: code defect found. The heuristic holds; no lens edit.
Evidence:

  • plugins/os-sdlc/lib/os_sdlc/model/map.rb:22 def drafted? = File.basename(File.dirname(path)) == "drafts"
  • plugins/os-sdlc/lib/os_sdlc/adapter/map/catalog.rb:7 DRAFTS_DIR = "drafts"; line 22 drafts_dir = File.join(@dir, DRAFTS_DIR)
  • #880 Approach step 2 prescribes the literal in Map#drafted?; R1 checks the literal in catalog.rb. The ticket wording caused the copy.
  • Checklist Q1: the rebuttal is a condition of this ticket, not of the world. Q2: the cost named is a gate failure, not a design cost. Q8: one move (one constant on Map, used by Map::Catalog) removes the copy. Q10: the obeying design costs one constant and one reference change.

Reproduce

n/a: design defect, nothing to reproduce.

Approach

SUPERSEDED (2026-10-02 second pass, steps 1-7 below replace the Map::DRAFTS_DIR constant that read ENV at load): Define DRAFTS_DIR on OsSdlc::Map (model/map.rb) as the XDG drafts root of ADR-0264: File.join(ENV.fetch("XDG_DATA_HOME", File.expand_path("~/.local/share")), "os-sdlc", "maps", "drafts").

First pass (ec449ee1, kept): Map::Catalog searches two roots, the approved plugin root first and the drafts root, with the same rule as before (raise InvalidMapError when the name is in both or neither), and has no DRAFTS_DIR constant or drafts_dir method of its own. The plugin tree has no maps/drafts/ directory (ADR-0264).

Second pass (user ruling 2026-10-02; fixes lens A6, lens-architecture SKILL.md:59-65 and lens-cases Case 8, and finishes criterion 5):

  1. model/map.rb: replace the constant with def self.drafts_dir(env) = File.join(env.fetch("XDG_DATA_HOME", File.join(env.fetch("HOME"), ".local/share")), "os-sdlc", "maps", "drafts"). No ENV read in lib/ model or adapter code.
  2. controller/boot.rb Boot.run (every bin enters here; ProcessIO#env already holds ENV.to_h, adapter/process_invocation.rb:6, controller/process_io.rb:6): install the default catalog once: Map::Catalog.default ||= Map::Catalog.new(Map::Catalog::PLUGIN_MAPS_DIR, drafts_dir: Map.drafts_dir(io.env)).
  3. Map::Catalog#initialize(dir, drafts_dir:): drafts_dir: is required, with no default.
  4. Map#drafted? compares the parent directory basename with the literal "drafts" (the XDG path and test tmp dirs both end in drafts).
  5. tests/model/map_drafts_dir_test.rb calls Map.drafts_dir(hash) in-process; no subprocess.
  6. Revert the plugins/os-sdlc/Gemfile procman line to File.expand_path("~/dev/ruby-gems/procman") and drop require "etc"; it existed only for the subprocess test.
  7. tests/support/tmpdir_test_case.rb (lines 12 and 23 set Catalog.default = nil): build the test catalog with the plugin maps dir (or a tmp dir) and a tmp drafts_dir. No test reaches the real ~/.local/share.

Required work:

  1. Give the map model the drafts root function — check: present def self.drafts_dir in plugins/os-sdlc/lib/os_sdlc/model/map.rb
  2. Keep ENV out of the map model — check: absent ENV in plugins/os-sdlc/lib/os_sdlc/model/map.rb
  3. Remove the catalog copy and search two roots — check: absent "drafts" in plugins/os-sdlc/lib/os_sdlc/adapter/map/catalog.rb
  4. Drop the Etc require used only by the subprocess test — check: absent Etc in plugins/os-sdlc/Gemfile

Acceptance criteria

  1. Given a valid map x.yaml only in the drafts root, when Catalog#load("x") runs, then it returns the map and drafted? is true.
  2. Given a valid map y.yaml only in the approved root, when Catalog#load("y") runs, then it returns the map and drafted? is false.
  3. Given z.yaml in both roots, or in neither, when Catalog#path_for("z") runs, then it raises InvalidMapError (:ambiguous_map_name).
  4. Given an env hash, Map.drafts_dir(env) returns <XDG_DATA_HOME>/os-sdlc/maps/drafts when XDG_DATA_HOME is set and <HOME>/.local/share/os-sdlc/maps/drafts when it is not; the test calls it in-process.
  5. No file under plugins/os-sdlc/lib/os_sdlc/model or plugins/os-sdlc/lib/os_sdlc/adapter/map reads ENV.
  6. Given Map::Catalog.new(dir) without drafts_dir:, when it runs, then it raises ArgumentError.
  7. Given a bin run through Boot.run, when it starts, then Map::Catalog.default is a catalog whose drafts root is Map.drafts_dir(io.env).
  8. No test constructs a catalog whose drafts root is the real user dir; tests build the catalog with two tmp dirs (the drafts one named drafts) or the plugin maps dir plus a tmp drafts dir.

Owner sketch

module OsSdlc::Map
  def self.drafts_dir(env)
  def drafted?
end

class OsSdlc::Map::Catalog
  def initialize(dir, drafts_dir:)
  def path_for(name)
end

module OsSdlc::Boot
  def self.run(name, io, argv)
end

Origin

  • Trigger: lens challenge of the A12 LENS line in #880 implementation 5, dispatch 23
  • Improvised this session: none
  • Chain: duplicated "drafts" literal ← R1 check requires the literal in catalog.rb ← DESIGN (#880 Approach step 2 and Required work 1)
  • Root candidate: none
  • Where: plugins/os-sdlc/lib/os_sdlc/model/map.rb, plugins/os-sdlc/lib/os_sdlc/adapter/map/catalog.rb
  • Session: 616e6f92-69d9-4a00-bf30-33a77404deb3
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/616e6f92-69d9-4a00-bf30-33a77404deb3.jsonl

VERDICT: CREATE

## Problem Drafted maps live in `maps/drafts/` inside the plugin tree, and the drafts directory name `"drafts"` has two owners. `Map#drafted?` (model/map.rb:22) spells the literal. `Map::Catalog::DRAFTS_DIR` (adapter/map/catalog.rb:7) spells it again. A12 requires one owner for a rule. Only the #880 Required-work check R1 (``present `"drafts"` in `plugins/os-sdlc/lib/os_sdlc/adapter/map/catalog.rb` ``) keeps the second copy. No code fact needs it. ADR-0264 moves drafted maps to an XDG data directory. So the one owner, `Map::DRAFTS_DIR`, holds the XDG drafts path, and `Map::Catalog` searches two roots: the approved plugin root and that drafts root. This is more than a rename: the drafts location changes, and the catalog no longer derives it from its own directory. LENS line (verbatim): LENS: A12 broken at plugins/os-sdlc/lib/os_sdlc/model/map.rb:22 - BREAKS: the drafts directory name is spelled in Map#drafted? and again as Map::Catalog::DRAFTS_DIR (adapter/map/catalog.rb:7); JUSTIFIED: yes, one owner (Map::DRAFTS_DIR used by Catalog) removes the literal that Required-work R1 checks in catalog.rb, so the gate fails (principle 4); PROPOSAL: NONE, the tension comes from this ticket check wording and will not recur; the commit records it. Parent: #859 Ticket: #904 (owner of this change; found in #880 implementation 5, dispatch 23) Verdict: code defect found. The heuristic holds; no lens edit. Evidence: - `plugins/os-sdlc/lib/os_sdlc/model/map.rb:22` `def drafted? = File.basename(File.dirname(path)) == "drafts"` - `plugins/os-sdlc/lib/os_sdlc/adapter/map/catalog.rb:7` `DRAFTS_DIR = "drafts"`; line 22 `drafts_dir = File.join(@dir, DRAFTS_DIR)` - #880 Approach step 2 prescribes the literal in `Map#drafted?`; R1 checks the literal in catalog.rb. The ticket wording caused the copy. - Checklist Q1: the rebuttal is a condition of this ticket, not of the world. Q2: the cost named is a gate failure, not a design cost. Q8: one move (one constant on `Map`, used by `Map::Catalog`) removes the copy. Q10: the obeying design costs one constant and one reference change. ## Reproduce n/a: design defect, nothing to reproduce. ## Approach SUPERSEDED (2026-10-02 second pass, steps 1-7 below replace the `Map::DRAFTS_DIR` constant that read ENV at load): Define `DRAFTS_DIR` on `OsSdlc::Map` (model/map.rb) as the XDG drafts root of ADR-0264: `File.join(ENV.fetch("XDG_DATA_HOME", File.expand_path("~/.local/share")), "os-sdlc", "maps", "drafts")`. First pass (ec449ee1, kept): `Map::Catalog` searches two roots, the approved plugin root first and the drafts root, with the same rule as before (raise `InvalidMapError` when the name is in both or neither), and has no `DRAFTS_DIR` constant or `drafts_dir` method of its own. The plugin tree has no `maps/drafts/` directory (ADR-0264). Second pass (user ruling 2026-10-02; fixes lens A6, lens-architecture SKILL.md:59-65 and lens-cases Case 8, and finishes criterion 5): 1. `model/map.rb`: replace the constant with `def self.drafts_dir(env) = File.join(env.fetch("XDG_DATA_HOME", File.join(env.fetch("HOME"), ".local/share")), "os-sdlc", "maps", "drafts")`. No ENV read in lib/ model or adapter code. 2. `controller/boot.rb` `Boot.run` (every bin enters here; `ProcessIO#env` already holds `ENV.to_h`, `adapter/process_invocation.rb:6`, `controller/process_io.rb:6`): install the default catalog once: `Map::Catalog.default ||= Map::Catalog.new(Map::Catalog::PLUGIN_MAPS_DIR, drafts_dir: Map.drafts_dir(io.env))`. 3. `Map::Catalog#initialize(dir, drafts_dir:)`: `drafts_dir:` is required, with no default. 4. `Map#drafted?` compares the parent directory basename with the literal `"drafts"` (the XDG path and test tmp dirs both end in `drafts`). 5. `tests/model/map_drafts_dir_test.rb` calls `Map.drafts_dir(hash)` in-process; no subprocess. 6. Revert the `plugins/os-sdlc/Gemfile` procman line to `File.expand_path("~/dev/ruby-gems/procman")` and drop `require "etc"`; it existed only for the subprocess test. 7. `tests/support/tmpdir_test_case.rb` (lines 12 and 23 set `Catalog.default = nil`): build the test catalog with the plugin maps dir (or a tmp dir) and a tmp `drafts_dir`. No test reaches the real `~/.local/share`. Required work: 1. Give the map model the drafts root function — check: present `def self.drafts_dir` in `plugins/os-sdlc/lib/os_sdlc/model/map.rb` 2. Keep ENV out of the map model — check: absent `ENV` in `plugins/os-sdlc/lib/os_sdlc/model/map.rb` 3. Remove the catalog copy and search two roots — check: absent `"drafts"` in `plugins/os-sdlc/lib/os_sdlc/adapter/map/catalog.rb` 4. Drop the Etc require used only by the subprocess test — check: absent `Etc` in `plugins/os-sdlc/Gemfile` ## Acceptance criteria 1. Given a valid map `x.yaml` only in the drafts root, when `Catalog#load("x")` runs, then it returns the map and `drafted?` is true. 2. Given a valid map `y.yaml` only in the approved root, when `Catalog#load("y")` runs, then it returns the map and `drafted?` is false. 3. Given `z.yaml` in both roots, or in neither, when `Catalog#path_for("z")` runs, then it raises `InvalidMapError` (`:ambiguous_map_name`). 4. Given an env hash, `Map.drafts_dir(env)` returns `<XDG_DATA_HOME>/os-sdlc/maps/drafts` when `XDG_DATA_HOME` is set and `<HOME>/.local/share/os-sdlc/maps/drafts` when it is not; the test calls it in-process. 5. No file under `plugins/os-sdlc/lib/os_sdlc/model` or `plugins/os-sdlc/lib/os_sdlc/adapter/map` reads ENV. 6. Given `Map::Catalog.new(dir)` without `drafts_dir:`, when it runs, then it raises `ArgumentError`. 7. Given a bin run through `Boot.run`, when it starts, then `Map::Catalog.default` is a catalog whose drafts root is `Map.drafts_dir(io.env)`. 8. No test constructs a catalog whose drafts root is the real user dir; tests build the catalog with two tmp dirs (the drafts one named `drafts`) or the plugin maps dir plus a tmp drafts dir. ## Owner sketch ```ruby module OsSdlc::Map def self.drafts_dir(env) def drafted? end class OsSdlc::Map::Catalog def initialize(dir, drafts_dir:) def path_for(name) end module OsSdlc::Boot def self.run(name, io, argv) end ``` ## Origin - Trigger: lens challenge of the A12 LENS line in #880 implementation 5, dispatch 23 - Improvised this session: none - Chain: duplicated `"drafts"` literal ← R1 check requires the literal in catalog.rb ← DESIGN (#880 Approach step 2 and Required work 1) - Root candidate: none - Where: plugins/os-sdlc/lib/os_sdlc/model/map.rb, plugins/os-sdlc/lib/os_sdlc/adapter/map/catalog.rb - Session: 616e6f92-69d9-4a00-bf30-33a77404deb3 - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/616e6f92-69d9-4a00-bf30-33a77404deb3.jsonl VERDICT: CREATE
Author
Owner

Owner decision: the one owner of the literal "drafts" is the object that owns DRAFTS_ROOT (the drafts root path, File.join(XDG_DATA_HOME or ~/.local/share, "os-sdlc", "maps", "drafts")), per ADR-0264 (user ruling on #898, option B1). Map::Catalog looks up [PLUGIN_MAPS_DIR, DRAFTS_ROOT]. The body names Map::DRAFTS_DIR on Map as owner; reconcile this when the ticket is implemented: Map#drafted? and the catalog must both take the name from that one owner, and the owner is not a second spelling beside DRAFTS_ROOT.

Owner decision: the one owner of the literal "drafts" is the object that owns DRAFTS_ROOT (the drafts root path, File.join(XDG_DATA_HOME or ~/.local/share, "os-sdlc", "maps", "drafts")), per ADR-0264 (user ruling on #898, option B1). Map::Catalog looks up [PLUGIN_MAPS_DIR, DRAFTS_ROOT]. The body names Map::DRAFTS_DIR on Map as owner; reconcile this when the ticket is implemented: Map#drafted? and the catalog must both take the name from that one owner, and the owner is not a second spelling beside DRAFTS_ROOT.
jared changed title from Lens proposal: one owner for the drafts directory name (Map::DRAFTS_DIR) to Map owns the XDG drafts directory (Map::DRAFTS_DIR); Map::Catalog searches the approved and drafts roots 2026-10-02 16:10:38 +00:00
Author
Owner

Work started on branch ticket-904 (os-sdlc pipeline, map poodr-ticket-implementation).

Work started on branch ticket-904 (os-sdlc pipeline, map poodr-ticket-implementation).
Author
Owner

Skeptic DROP on follow-up: TmpdirTestCase#use_plugin_maps (plugins/os-sdlc/tests/support/tmpdir_test_case.rb:23) and teardown (line 12) set Catalog.default to nil, so the catalog falls back to the real Map::DRAFTS_DIR. Acceptance criterion 5 (two tmp dirs) is only partly met. Finish it on #904: build the Catalog with the plugin maps dir and a tmp drafts_dir.

Skeptic DROP on follow-up: TmpdirTestCase#use_plugin_maps (plugins/os-sdlc/tests/support/tmpdir_test_case.rb:23) and teardown (line 12) set Catalog.default to nil, so the catalog falls back to the real Map::DRAFTS_DIR. Acceptance criterion 5 (two tmp dirs) is only partly met. Finish it on #904: build the Catalog with the plugin maps dir and a tmp drafts_dir.
Author
Owner

Second pass started per user ruling 2026-10-02: A6 fix (Opus lens challenge, verdict code defect) + criterion 5 completion. First pass ec449ee1.

Second pass started per user ruling 2026-10-02: A6 fix (Opus lens challenge, verdict code defect) + criterion 5 completion. First pass ec449ee1.
Author
Owner

Resolution

Done: Drafted maps resolve from Map.drafts_dir(env) (XDG_DATA_HOME, else HOME/.local/share) per ADR-0264. Boot#run installs the default catalog from io.env; Map::Catalog requires drafts_dir: and searches [PLUGIN_MAPS_DIR, drafts_dir]. No model reads ENV (lens A6). Test support uses a tmp drafts dir, so no test reads the real user dir (criterion 5). Two pipeline passes: implementation 8 (ec449ee1) and implementation 9 (ff16cc43), History row 182f8825.

Evidence: Branch ticket-904: ec449ee1, ff16cc43, 182f8825. Implementation 9 suite-check dispatch 73 green; behavior-verifier dispatch 72 pass on all 8 criteria; contract-auditor dispatch 74 pass. Gemfile identical to main.

Follow-ups: #907 filed (Runner auto-commit fails on already-staged deletions; skeptic CREATE). Dropped: Catalog.default still falls back to drafts_dir: File::NULL when Boot did not install a catalog; every current bin enters through Boot, so no caller is affected; revisit only if a non-Boot entry point appears.

## Resolution **Done:** Drafted maps resolve from Map.drafts_dir(env) (XDG_DATA_HOME, else HOME/.local/share) per ADR-0264. Boot#run installs the default catalog from io.env; Map::Catalog requires drafts_dir: and searches [PLUGIN_MAPS_DIR, drafts_dir]. No model reads ENV (lens A6). Test support uses a tmp drafts dir, so no test reads the real user dir (criterion 5). Two pipeline passes: implementation 8 (ec449ee1) and implementation 9 (ff16cc43), History row 182f8825. **Evidence:** Branch ticket-904: ec449ee1, ff16cc43, 182f8825. Implementation 9 suite-check dispatch 73 green; behavior-verifier dispatch 72 pass on all 8 criteria; contract-auditor dispatch 74 pass. Gemfile identical to main. **Follow-ups:** #907 filed (Runner auto-commit fails on already-staged deletions; skeptic CREATE). Dropped: Catalog.default still falls back to drafts_dir: File::NULL when Boot did not install a catalog; every current bin enters through Boot, so no caller is affected; revisit only if a non-Boot entry point appears.
jared closed this issue 2026-10-02 16:57:21 +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#904
No description provided.