The implementation snapshot persists the whole Project; Project::Config becomes Project::Snapshot; Implementation#target #792

Closed
opened 2026-09-28 14:17:05 +00:00 by jared · 3 comments
Owner

Problem

implementations.project_config stores one flattened target, from Project#to_config_h. The keys are name, tracker, commit_on_complete, path, test_path, test_command, test_framework, lint_config, and lint_paths. Every other target, the target's name, and map are dropped. The class that parses it is Project::Config, but R2 needs that name for the yaml. Two readers parse the same column: Implementation#project and #commit_on_complete?. No object answers "the target this implementation is bound to".

Reproduce

n/a: new behavior. Failing test: open an implementation with --target b on a yaml with targets a and b, then rewrite the yaml so that b's test_command changes. implementation.target.name is b, its test command is the open-time value, and implementation.project(root).targets.map(&:name) is ["a", "b"]. Today Implementation#target is the String column and targets is not persisted.

Approach

Keep the project_config text column (no SCHEMA_VERSION bump; ADR-0129/0155). Store the whole Project without root: {name, tracker, commit_on_complete, targets: {<name> => {path, test_path, test_command, test_framework, lint_config, lint_paths, tracker}}}. Project::Snapshot is the one JSON codec (dump and parse). Implementation#snapshot parses the column once. #project(root), #commit_on_complete?, and #target ask the snapshot.
Files (8): parser/project/config.rb -> parser/project/snapshot.rb, model/project.rb (to_json/from_json call Snapshot), model/implementation/instance_methods.rb, model/project/target.rb (to_h for the blob), command/implementation/open.rb (no change to the call; confirm), tests/parser/project/config_name_guard_test.rb -> tests/parser/project/snapshot_name_guard_test.rb, tests/model/project_json_round_trip_test.rb, tests/model/implementation/target_test.rb (new).
ADRs cited: ADR-0169 (binds at open, frozen for the life of the implementation, root never stored), ADR-0055, ADR-0139 (flat projects store a null target), ADR-0129/0155 (no bump), ADR-0167, ADR-0227.
ADR to write before open (amends ADR-0169 D1-D2, ADR-0192 items 1/3/4, ADR-0210 rules 1-2, ADR-0088's member-list consequence and its rejected alternative): "Project holds every target; the implementation snapshot persists the whole Project as JSON in implementations.project_config; the column and the schema version do not change; a blob without targets: is legacy and Implementation#target refuses it, so the implementation must be reopened."

Required work:

  1. Rename Project::Config to Project::Snapshot (parser/project/snapshot.rb). Snapshot.dump(project) writes the whole-Project shape above. Snapshot.new(json).project(root) rebuilds targets. The selected-target members are rebuilt from the bound target. — check: ruby plugins/os-sdlc/tests/model/implementation/target_test.rb
  2. Implementation#snapshot = associations[:snapshot] ||= Project::Snapshot.new(project_config). #project(root) and #commit_on_complete? read it. — check: ruby plugins/os-sdlc/tests/model/implementation_empty_project_config_test.rb
  3. Implementation#target = associations[:target] ||= snapshot.project(nil).targets.find { |t| t.name == (self[:target] || snapshot.name) }. When there is no match (a legacy flat blob with a named target), raise HarnessError that names the implementation and says "reopen". — check: same test as item 1
  4. Any code that reads the String column uses self[:target], not #target. — check: grep -rn "implementation\.target\b\|implementation\[:target\]" plugins/os-sdlc/lib
  5. Delete no-longer-used Project::Config references. — check: ! grep -rn "Project::Config\b" plugins/os-sdlc/lib

Rejected: a projects table plus a targets table (option ii). The Project is a 1:1 value snapshot of the implementation with no identity and no query consumer. It needs a schema bump, and ADR-0155 refuses the bump while implementations are live (3 were live on 2026-09-28).
Rejected: Project::Snapshot.find_by_implementation(implementation_id:). It is a query that re-fetches a row that the caller already holds (lens A11/A13). implementation.snapshot is the same fact.
Rejected: rename the column to project. It needs a bump for a name only.

Acceptance criteria

  1. Given an implementation opened with target b on a two-target yaml, when the yaml is then edited, then implementation.target is b with the open-time test command and lint paths.
  2. Given that implementation, when implementation.project(root) is read, then its targets are a and b, and its root is the root that was given.
  3. Given an implementation opened on a flat yaml name: widgets (the target column is null), when implementation.target is read, then it is the target named widgets.
  4. Given a row whose project_config is the pre-change flat shape and whose target column is os-backlog, when implementation.target is read, then a HarnessError says to reopen the implementation.
  5. Given a blob without name, when it is parsed, then the existing HarnessError "project_config blob is missing required key: name" is raised.

Owner sketch

class OsSdlc::Project::Snapshot
  def self.dump(project)        # JSON, no root, every target
  def initialize(json)
  def project(root)             # Project with targets
  def commit_on_complete?
end
module OsSdlc::Implementation::InstanceMethods
  def snapshot = associations[:snapshot] ||= Project::Snapshot.new(project_config)
  def project(root) = snapshot.project(root)
  def target = associations[:target] ||= ...
end

Non-goals

Moving gate, brief, and diff-test callers to implementation.target (T7). The yaml rename (T5).

Retired tests

plugins/os-sdlc/tests/parser/project/config_name_guard_test.rb
plugins/os-sdlc/tests/model/project_json_round_trip_test.rb#test_to_json_omits_root_and_map

Blocking edges

Blocked by: T2

Origin

  • Trigger: user rulings R3/R4 ("make the Project the persisted object")
  • Improvised this session: none
  • Chain: snapshot holds one flattened target ← Project#to_config_h flattens the selected target (DESIGN: ADR-0169 D1, ADR-0192 item 4 "project_config blob stays flat")
  • Where: plugins/os-sdlc/lib/os_sdlc/parser/project/config.rb, model/implementation/instance_methods.rb:45-47
  • Session: bb36e383-017d-486e-809f-7ce25d151e61
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os--claude-worktrees-crc-pass-os-sdlc/bb36e383-017d-486e-809f-7ce25d151e61.jsonl
## Problem `implementations.project_config` stores one flattened target, from `Project#to_config_h`. The keys are name, tracker, commit_on_complete, path, test_path, test_command, test_framework, lint_config, and lint_paths. Every other target, the target's name, and `map` are dropped. The class that parses it is `Project::Config`, but R2 needs that name for the yaml. Two readers parse the same column: `Implementation#project` and `#commit_on_complete?`. No object answers "the target this implementation is bound to". ## Reproduce n/a: new behavior. Failing test: open an implementation with `--target b` on a yaml with targets `a` and `b`, then rewrite the yaml so that `b`'s test_command changes. `implementation.target.name` is `b`, its test command is the open-time value, and `implementation.project(root).targets.map(&:name)` is `["a", "b"]`. Today `Implementation#target` is the String column and `targets` is not persisted. ## Approach Keep the `project_config` text column (no SCHEMA_VERSION bump; ADR-0129/0155). Store the whole Project without root: `{name, tracker, commit_on_complete, targets: {<name> => {path, test_path, test_command, test_framework, lint_config, lint_paths, tracker}}}`. `Project::Snapshot` is the one JSON codec (dump and parse). `Implementation#snapshot` parses the column once. `#project(root)`, `#commit_on_complete?`, and `#target` ask the snapshot. Files (8): parser/project/config.rb -> parser/project/snapshot.rb, model/project.rb (`to_json`/`from_json` call Snapshot), model/implementation/instance_methods.rb, model/project/target.rb (`to_h` for the blob), command/implementation/open.rb (no change to the call; confirm), tests/parser/project/config_name_guard_test.rb -> tests/parser/project/snapshot_name_guard_test.rb, tests/model/project_json_round_trip_test.rb, tests/model/implementation/target_test.rb (new). ADRs cited: ADR-0169 (binds at open, frozen for the life of the implementation, root never stored), ADR-0055, ADR-0139 (flat projects store a null target), ADR-0129/0155 (no bump), ADR-0167, ADR-0227. ADR to write before open (amends ADR-0169 D1-D2, ADR-0192 items 1/3/4, ADR-0210 rules 1-2, ADR-0088's member-list consequence and its rejected alternative): "Project holds every target; the implementation snapshot persists the whole Project as JSON in implementations.project_config; the column and the schema version do not change; a blob without targets: is legacy and Implementation#target refuses it, so the implementation must be reopened." Required work: 1. Rename `Project::Config` to `Project::Snapshot` (parser/project/snapshot.rb). `Snapshot.dump(project)` writes the whole-Project shape above. `Snapshot.new(json).project(root)` rebuilds `targets`. The selected-target members are rebuilt from the bound target. — check: `ruby plugins/os-sdlc/tests/model/implementation/target_test.rb` 2. `Implementation#snapshot = associations[:snapshot] ||= Project::Snapshot.new(project_config)`. `#project(root)` and `#commit_on_complete?` read it. — check: `ruby plugins/os-sdlc/tests/model/implementation_empty_project_config_test.rb` 3. `Implementation#target = associations[:target] ||= snapshot.project(nil).targets.find { |t| t.name == (self[:target] || snapshot.name) }`. When there is no match (a legacy flat blob with a named target), raise HarnessError that names the implementation and says "reopen". — check: same test as item 1 4. Any code that reads the String column uses `self[:target]`, not `#target`. — check: `grep -rn "implementation\.target\b\|implementation\[:target\]" plugins/os-sdlc/lib` 5. Delete no-longer-used `Project::Config` references. — check: `! grep -rn "Project::Config\b" plugins/os-sdlc/lib` Rejected: a `projects` table plus a `targets` table (option ii). The Project is a 1:1 value snapshot of the implementation with no identity and no query consumer. It needs a schema bump, and ADR-0155 refuses the bump while implementations are live (3 were live on 2026-09-28). Rejected: `Project::Snapshot.find_by_implementation(implementation_id:)`. It is a query that re-fetches a row that the caller already holds (lens A11/A13). `implementation.snapshot` is the same fact. Rejected: rename the column to `project`. It needs a bump for a name only. ## Acceptance criteria 1. Given an implementation opened with target `b` on a two-target yaml, when the yaml is then edited, then `implementation.target` is `b` with the open-time test command and lint paths. 2. Given that implementation, when `implementation.project(root)` is read, then its targets are `a` and `b`, and its root is the root that was given. 3. Given an implementation opened on a flat yaml `name: widgets` (the target column is null), when `implementation.target` is read, then it is the target named `widgets`. 4. Given a row whose `project_config` is the pre-change flat shape and whose target column is `os-backlog`, when `implementation.target` is read, then a HarnessError says to reopen the implementation. 5. Given a blob without `name`, when it is parsed, then the existing HarnessError "project_config blob is missing required key: name" is raised. ## Owner sketch ```ruby class OsSdlc::Project::Snapshot def self.dump(project) # JSON, no root, every target def initialize(json) def project(root) # Project with targets def commit_on_complete? end module OsSdlc::Implementation::InstanceMethods def snapshot = associations[:snapshot] ||= Project::Snapshot.new(project_config) def project(root) = snapshot.project(root) def target = associations[:target] ||= ... end ``` ## Non-goals Moving gate, brief, and diff-test callers to `implementation.target` (T7). The yaml rename (T5). ## Retired tests plugins/os-sdlc/tests/parser/project/config_name_guard_test.rb plugins/os-sdlc/tests/model/project_json_round_trip_test.rb#test_to_json_omits_root_and_map ## Blocking edges Blocked by: T2 ## Origin - Trigger: user rulings R3/R4 ("make the Project the persisted object") - Improvised this session: none - Chain: snapshot holds one flattened target ← `Project#to_config_h` flattens the selected target (DESIGN: ADR-0169 D1, ADR-0192 item 4 "project_config blob stays flat") - Where: plugins/os-sdlc/lib/os_sdlc/parser/project/config.rb, model/implementation/instance_methods.rb:45-47 - Session: bb36e383-017d-486e-809f-7ce25d151e61 - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os--claude-worktrees-crc-pass-os-sdlc/bb36e383-017d-486e-809f-7ce25d151e61.jsonl
Author
Owner

Ruling per ADR-0238: the snapshot reader accepts both the new nested blob and the old ADR-0192 flat blob, so the three live implementations survive. Do not refuse old rows. Where the body says old flat rows are refused, read: old flat rows load as a Project with one target.

Ruling per ADR-0238: the snapshot reader accepts both the new nested blob and the old ADR-0192 flat blob, so the three live implementations survive. Do not refuse old rows. Where the body says old flat rows are refused, read: old flat rows load as a Project with one target.
Author
Owner

Work started on branch ticket-792 via /os-sdlc:implement (session bb36e383). #791 merged to main.

Work started on branch ticket-792 via /os-sdlc:implement (session bb36e383). #791 merged to main.
Author
Owner

Resolution

Done: Project::Config renamed Project::Snapshot; the snapshot stores the whole Project (name, tracker, commit_on_complete, target_name, targets) in implementations.project_config with no schema bump; Implementation#snapshot memoizes it, #project(root) rebuilds the Project, #target binds the row's target (or the project name) to one Project::Target; legacy flat blobs load as a one-target Project named by the row column; Implementation#target_name replaces raw row indexing in the commit presenter.

Evidence: os-sdlc implementation 57 on branch ticket-792: 24 dispatches; contract-auditor exhausted its bound on a test conflict (old project_path_test forbade Project#root, new target_test required it); hand-finished per escalation-procedure step 4: refute line retired, auditor corrections 1 and 2 applied, BoundProject deleted; fresh opus review verdict FIX -> legacy rows now load per ADR-0238 item 4; suite 1314 runs 0 failures; eager load clean; inward-dependency count 61 unchanged; rubocop clean on changed files; commit c7cc012a

Follow-ups: Move Target.from_blob into Snapshot, drop the duplicate target_name in the blob, delete Project#to_config_h/#to_flat_h, rename or remove Target#bound_as: all fold into #795 (T7, callers read the bound Target; FlatAttrs deleted). Project#root alias: absorbed by #796 (T8 path->root). Tighten snapshot_name_guard_test message match and fix implementation_project_test alignment: folded into #795 as a comment.

## Resolution **Done:** Project::Config renamed Project::Snapshot; the snapshot stores the whole Project (name, tracker, commit_on_complete, target_name, targets) in implementations.project_config with no schema bump; Implementation#snapshot memoizes it, #project(root) rebuilds the Project, #target binds the row's target (or the project name) to one Project::Target; legacy flat blobs load as a one-target Project named by the row column; Implementation#target_name replaces raw row indexing in the commit presenter. **Evidence:** os-sdlc implementation 57 on branch ticket-792: 24 dispatches; contract-auditor exhausted its bound on a test conflict (old project_path_test forbade Project#root, new target_test required it); hand-finished per escalation-procedure step 4: refute line retired, auditor corrections 1 and 2 applied, BoundProject deleted; fresh opus review verdict FIX -> legacy rows now load per ADR-0238 item 4; suite 1314 runs 0 failures; eager load clean; inward-dependency count 61 unchanged; rubocop clean on changed files; commit c7cc012a **Follow-ups:** Move Target.from_blob into Snapshot, drop the duplicate target_name in the blob, delete Project#to_config_h/#to_flat_h, rename or remove Target#bound_as: all fold into #795 (T7, callers read the bound Target; FlatAttrs deleted). Project#root alias: absorbed by #796 (T8 path->root). Tighten snapshot_name_guard_test message match and fix implementation_project_test alignment: folded into #795 as a comment.
jared closed this issue 2026-09-28 15:39:23 +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#792
No description provided.