Project::Yaml owns .sdlc/project.yaml: one path, one reader, one guarded writer (supersedes ADR-0088 in part) #567

Closed
opened 2026-09-13 23:18:14 +00:00 by jared · 3 comments
Owner

Map: #568
Blocked by #566

Context

Responsibility audit of project.rb on 2026-09-13. Batch position 4 of 4. Runs last. Project writes .sdlc/project.yaml from three sites, checks the git prerequisite twice, reads the file twice without refusing an empty file, and builds the path by hand in two places.

Observed

plugins/os-sdlc/lib/os_sdlc/project.rb:76    File.write(yaml_path(root), YAML.dump("name" => name))
plugins/os-sdlc/lib/os_sdlc/project.rb:114   File.write(yaml_path(root), YAML.dump(data))
plugins/os-sdlc/lib/os_sdlc/project.rb:149   File.write(path, YAML.dump(data))
plugins/os-sdlc/lib/os_sdlc/project.rb:60    raise_not_a_git_repo!(root) unless git_repo?(root)
plugins/os-sdlc/lib/os_sdlc/project.rb:89    raise_not_a_git_repo!(root) unless git_repo?(root)
plugins/os-sdlc/lib/os_sdlc/project.rb:96    data = YAML.safe_load_file(yaml_path(root))
plugins/os-sdlc/lib/os_sdlc/project.rb:147   data = YAML.safe_load_file(path)
plugins/os-sdlc/lib/os_sdlc/project.rb:156   "in #{File.join(root, ".sdlc", "project.yaml")} " \
plugins/os-sdlc/lib/os_sdlc/project.rb:188   File.join(sdlc_dir(dir), "project.yaml")
plugins/os-sdlc/lib/os_sdlc/project_config_command.rb:101  path = File.join(project.root, ".sdlc", "project.yaml")

save_tracker (lines 147-149) bypasses raise_targets_shaped_write! (line 97) and the existence check. An empty file makes YAML.safe_load_file return nil, then NoMethodError at line 98 and nil.fetch at line 31 instead of HarnessError.

Reproduce

cd plugins/os-sdlc && : > .sdlc/project.yaml && ruby -Ilib -e 'require "os_sdlc"; OsSdlc::Project.load(Dir.pwd)'

Expected

def test_load_raises_harness_error_on_empty_project_yaml
  File.write(Project.yaml_path(dir), "")
  assert_raises(HarnessError) { Project.load(dir) }
end

def test_save_tracker_refuses_targets_shaped_file
  # same refusal Project.upsert applies
end

Illustration

# illustration, not spec
class OsSdlc::Project
  class Yaml                                    # lib/os_sdlc/project/yaml.rb
    def self.containing(dir)                    # upward search, .git/$HOME boundary
    def initialize(root)
    def path                                    # the one expression
    def exist?
    def read                                    # raises HarnessError on empty/unparsable
    def write(data)                             # single File.write; refuses attr keys
    def merge_keys(attrs)                       #   on a targets-shaped file
  end
end

Callers that delegate: Project.load, Project.create, Project.upsert, Project#save_tracker, Project#tracker_required_error, Project.discover_root, ProjectConfigCommand#report and #report_unreadable.

User decisions 2026-09-13:

  • The .sdlc directory name belongs to Project. It is the per-project os-sdlc artifact directory. Only project.yaml belongs to Yaml.
  • Delete Project.discover_root and Project#preflight_summary_line. No senders outside their own tests, verified across lib, bin, hooks, skills, and agents.
  • Keep Project.create as a one-line delegator. The error text at project.rb:48 names it and test support uses it.

Tasks

  • Failing tests: empty file raises HarnessError; save_tracker refuses a targets-shaped file; one yaml path expression
  • Create the superseding ADR with /os-adr:create (supersedes ADR-0088 in part: write refusal moves from Project.upsert and the project-config command into Project::Yaml; consistent with ADR-0055, root derived from file location; .sdlc dirname stays with Project)
  • Build OsSdlc::Project::Yaml at lib/os_sdlc/project/yaml.rb
  • Delegate the callers listed above
  • Delete discover_root and preflight_summary_line and their tests

Acceptance criteria

  • Full suite green; one File.write and one path expression for project.yaml under lib
  • The superseding ADR exists before merge

Origin

  • Trigger: /os-sdlc:responsibility-audit plugins/os-sdlc/lib/os_sdlc/project.rb on 2026-09-13; analysis at .sdlc/tmp/responsibility-audit/2026-09-13-project/analysis.yaml
  • Improvised this session: none
  • Chain: mixed responsibilities in OsSdlc::Project ← DESIGN (plugins/os-sdlc/lib/os_sdlc/project.rb, ADR-0055/0074/0088/0169/0174)
  • Root candidate: this ticket
  • Where: OsSdlc::Project, plugins/os-sdlc/lib/os_sdlc/project.rb
  • Session: c72ac6a8-cb89-4b97-904d-155babd2a7a2
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/c72ac6a8-cb89-4b97-904d-155babd2a7a2.jsonl
Map: #568 Blocked by #566 ## Context Responsibility audit of `project.rb` on 2026-09-13. Batch position 4 of 4. Runs last. `Project` writes `.sdlc/project.yaml` from three sites, checks the git prerequisite twice, reads the file twice without refusing an empty file, and builds the path by hand in two places. ## Observed ``` plugins/os-sdlc/lib/os_sdlc/project.rb:76 File.write(yaml_path(root), YAML.dump("name" => name)) plugins/os-sdlc/lib/os_sdlc/project.rb:114 File.write(yaml_path(root), YAML.dump(data)) plugins/os-sdlc/lib/os_sdlc/project.rb:149 File.write(path, YAML.dump(data)) plugins/os-sdlc/lib/os_sdlc/project.rb:60 raise_not_a_git_repo!(root) unless git_repo?(root) plugins/os-sdlc/lib/os_sdlc/project.rb:89 raise_not_a_git_repo!(root) unless git_repo?(root) plugins/os-sdlc/lib/os_sdlc/project.rb:96 data = YAML.safe_load_file(yaml_path(root)) plugins/os-sdlc/lib/os_sdlc/project.rb:147 data = YAML.safe_load_file(path) plugins/os-sdlc/lib/os_sdlc/project.rb:156 "in #{File.join(root, ".sdlc", "project.yaml")} " \ plugins/os-sdlc/lib/os_sdlc/project.rb:188 File.join(sdlc_dir(dir), "project.yaml") plugins/os-sdlc/lib/os_sdlc/project_config_command.rb:101 path = File.join(project.root, ".sdlc", "project.yaml") ``` `save_tracker` (lines 147-149) bypasses `raise_targets_shaped_write!` (line 97) and the existence check. An empty file makes `YAML.safe_load_file` return nil, then `NoMethodError` at line 98 and `nil.fetch` at line 31 instead of `HarnessError`. ## Reproduce ``` cd plugins/os-sdlc && : > .sdlc/project.yaml && ruby -Ilib -e 'require "os_sdlc"; OsSdlc::Project.load(Dir.pwd)' ``` ## Expected ```ruby def test_load_raises_harness_error_on_empty_project_yaml File.write(Project.yaml_path(dir), "") assert_raises(HarnessError) { Project.load(dir) } end def test_save_tracker_refuses_targets_shaped_file # same refusal Project.upsert applies end ``` ## Illustration ```ruby # illustration, not spec class OsSdlc::Project class Yaml # lib/os_sdlc/project/yaml.rb def self.containing(dir) # upward search, .git/$HOME boundary def initialize(root) def path # the one expression def exist? def read # raises HarnessError on empty/unparsable def write(data) # single File.write; refuses attr keys def merge_keys(attrs) # on a targets-shaped file end end ``` Callers that delegate: `Project.load`, `Project.create`, `Project.upsert`, `Project#save_tracker`, `Project#tracker_required_error`, `Project.discover_root`, `ProjectConfigCommand#report` and `#report_unreadable`. User decisions 2026-09-13: - The `.sdlc` directory name belongs to `Project`. It is the per-project os-sdlc artifact directory. Only `project.yaml` belongs to `Yaml`. - Delete `Project.discover_root` and `Project#preflight_summary_line`. No senders outside their own tests, verified across lib, bin, hooks, skills, and agents. - Keep `Project.create` as a one-line delegator. The error text at `project.rb:48` names it and test support uses it. ## Tasks - [ ] Failing tests: empty file raises HarnessError; save_tracker refuses a targets-shaped file; one yaml path expression - [ ] Create the superseding ADR with `/os-adr:create` (supersedes ADR-0088 in part: write refusal moves from `Project.upsert` and the project-config command into `Project::Yaml`; consistent with ADR-0055, root derived from file location; `.sdlc` dirname stays with `Project`) - [ ] Build `OsSdlc::Project::Yaml` at `lib/os_sdlc/project/yaml.rb` - [ ] Delegate the callers listed above - [ ] Delete `discover_root` and `preflight_summary_line` and their tests ## Acceptance criteria - Full suite green; one `File.write` and one path expression for `project.yaml` under lib - The superseding ADR exists before merge ## Origin - Trigger: `/os-sdlc:responsibility-audit plugins/os-sdlc/lib/os_sdlc/project.rb` on 2026-09-13; analysis at `.sdlc/tmp/responsibility-audit/2026-09-13-project/analysis.yaml` - Improvised this session: none - Chain: mixed responsibilities in `OsSdlc::Project` ← DESIGN (`plugins/os-sdlc/lib/os_sdlc/project.rb`, ADR-0055/0074/0088/0169/0174) - Root candidate: this ticket - Where: `OsSdlc::Project`, `plugins/os-sdlc/lib/os_sdlc/project.rb` - Session: c72ac6a8-cb89-4b97-904d-155babd2a7a2 - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/c72ac6a8-cb89-4b97-904d-155babd2a7a2.jsonl
Author
Owner

Work started via /os-sdlc:implement on branch ticket-567 (worktree). Map: #568.

Work started via /os-sdlc:implement on branch ticket-567 (worktree). Map: #568.
Author
Owner

BLOCKED: pipeline implementation 4 ended implementation_failed, reason bound_exhausted:test-reviewer/fail after 32 dispatches. Failed step: test-writer could not express 'callers delegate to Project::Yaml' as a public-boundary test; its last attempt (test_create_delegates_the_write_to_project_yaml) monkey-patched Yaml#write and was rejected. Done and green on branch ticket-567 (WIP commit, not merged): Project::Yaml class at lib/os_sdlc/project/yaml.rb with read/write/path and targets-shaped refusal plus tests/project/yaml_test.rb; Project.load raises HarnessError on an empty file; save_tracker refuses a targets-shaped file; ADR-0177 written. Outstanding: Project.load/create/upsert/save_tracker/tracker_required_error and ProjectConfigCommand do not yet delegate to Yaml (the guards are duplicated inside Project); Yaml lacks containing/exist?/merge_keys; discover_root and preflight_summary_line not deleted. Resume: hand pass or code-write on the delegation, then suite, then worktree finish. Suite at stop: 1215 runs, 0 failures.

BLOCKED: pipeline implementation 4 ended implementation_failed, reason bound_exhausted:test-reviewer/fail after 32 dispatches. Failed step: test-writer could not express 'callers delegate to Project::Yaml' as a public-boundary test; its last attempt (test_create_delegates_the_write_to_project_yaml) monkey-patched Yaml#write and was rejected. Done and green on branch ticket-567 (WIP commit, not merged): Project::Yaml class at lib/os_sdlc/project/yaml.rb with read/write/path and targets-shaped refusal plus tests/project/yaml_test.rb; Project.load raises HarnessError on an empty file; save_tracker refuses a targets-shaped file; ADR-0177 written. Outstanding: Project.load/create/upsert/save_tracker/tracker_required_error and ProjectConfigCommand do not yet delegate to Yaml (the guards are duplicated inside Project); Yaml lacks containing/exist?/merge_keys; discover_root and preflight_summary_line not deleted. Resume: hand pass or code-write on the delegation, then suite, then worktree finish. Suite at stop: 1215 runs, 0 failures.
Author
Owner

Resolution

Done: Project::Yaml owns .sdlc/project.yaml: containing/path/exist?/read/write/merge_keys; Project.load/create/upsert/save_tracker and Runner::TeaTracker delegate; discover_root and preflight_summary_line deleted; ADR-0177.

Evidence: Merged to main f2639b1 (WIP a9b2838 from the pipeline + hand structural follow-through 9d22487). Under lib: one File.write and one path expression for project.yaml, both in project/yaml.rb. Suite 1215 runs, 0 failures. Pipeline stop root-caused: test-reviewer/fail bound counted ticket-wide; fixed by progress_reset in 4bbea74.

Follow-ups: #569 token columns empty for implementations 2 and 3; ADR-0177 affected-paths lists a cli/ segment that does not exist (typo, dropped as trivial)

## Resolution **Done:** Project::Yaml owns .sdlc/project.yaml: containing/path/exist?/read/write/merge_keys; Project.load/create/upsert/save_tracker and Runner::TeaTracker delegate; discover_root and preflight_summary_line deleted; ADR-0177. **Evidence:** Merged to main f2639b1 (WIP a9b2838 from the pipeline + hand structural follow-through 9d22487). Under lib: one File.write and one path expression for project.yaml, both in project/yaml.rb. Suite 1215 runs, 0 failures. Pipeline stop root-caused: test-reviewer/fail bound counted ticket-wide; fixed by progress_reset in 4bbea74. **Follow-ups:** #569 token columns empty for implementations 2 and 3; ADR-0177 affected-paths lists a cli/ segment that does not exist (typo, dropped as trivial)
jared closed this issue 2026-09-14 12:20:39 +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#567
No description provided.