os-sdlc: Project becomes an ActiveModel object (no behavior change) #1672

Closed
opened 2026-11-10 17:41:34 +00:00 by jared · 2 comments
Owner

Problem

OsSdlc::Project (plugins/os-sdlc/lib/os_sdlc/project.rb) is Data.define(:name, :root, :tracker, :targets, :selected). The project is persisted as a JSON snapshot in the implementations.project_config column. The next ticket makes the project hold connections (connect(:db) and a lazy repo). A frozen Data object cannot hold them. ADR-0312 accepted activemodel as a direct dependency of os-sdlc.

Approach

Project becomes a Ruby class that includes ActiveModel::Model and ActiveModel::Attributes. The five fields become attributes. gem "activemodel" goes into plugins/os-sdlc/Gemfile, and Gemfile.lock is updated. require "active_model" and ActiveModel.eager_load! are loaded as plugins/os-backlog/lib/backlog.rb:9-12 does it. Project::Target, Project::Suite, and Project::Lint stay Data. The language is Ruby. The suite command is ruby test/all.rb, run from plugins/os-sdlc.

Kept behavior. The existing suite pins these items, so they are not red-round criteria. The new class must keep each one.

  • Value equality: two projects with equal name, root, tracker, targets, and selected are ==, are eql?, and have equal hash (test/project_json_round_trip_test.rb:13, test/project_nested_code_test_lint_test.rb:105; Crc::Census::Settings holds a project inside a Data).
  • Project.new takes the same keywords with the same defaults: targets defaults to [Project::Target.new(name: name)], and root, tracker, and selected default to nil. Project::Unconfigured < Project still loads and constructs.
  • project.with(...) returns a changed copy and leaves the receiver unchanged (call sites project.rb:12, project.rb:23, project/config.rb:17, project/config.rb:71). bound_to returns the same result as before.
  • case project; in Project[root:] matches (lib/os_sdlc/boot/commands.rb:164), so deconstruct_keys must exist.
  • to_json and to_snapshot_h emit exactly the key set {name, tracker, targets: {<target name> => {path, test_path, test_command, test_framework, lint_config, tracker}}} through Project::Snapshot. root and selected never enter the snapshot. A legacy snapshot with no targets key still loads. Do not use the JSON serializer of ActiveModel.
  • The raise messages in project.rb that name .sdlc/project.yaml and --target NAME stay verbatim.

Required work:

  1. present gem "activemodel" in plugins/os-sdlc/Gemfile
  2. present ActiveModel::Attributes in plugins/os-sdlc/lib/os_sdlc/project.rb
  3. absent Data.define(:name, :root, :tracker, :targets, :selected) in plugins/os-sdlc/lib/os_sdlc/project.rb

Acceptance criteria

  1. A project is not frozen: Project.new(name: "x").frozen? is false, and a test sets an instance variable on a Project and reads it back.
  2. Project.new(name: "x", tracker: "t").attributes returns a Hash with exactly the keys "name", "root", "tracker", "targets", and "selected", with the given values.
  3. Project.new(name: "x") answers valid? with true and has an empty errors object.

Owner sketch

OsSdlc::Project owns its five attributes, their defaults, value equality, the changed copy (with), pattern-match keys (deconstruct_keys), and the delegation of its snapshot to Project::Snapshot. This ticket adds no object and moves no responsibility.

Out of scope

connect, repo, the Thor entry, the removal of Boot, and new validations. These are the next tickets of the same change.

Origin

Trigger: n/a: design ticket named by the user, not a run that showed a fault
Improvised this session: none
Chain: the next ticket cannot hold connect(:db) and a lazy repo on Project ← Project is a frozen Data.define ← ADR-0312 (DESIGN) accepted activemodel as a direct dependency
Session: a48658d5-b566-40ce-8257-ebeabf56f7e1
Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os-plugins-os-sdlc/a48658d5-b566-40ce-8257-ebeabf56f7e1.jsonl
Where: plugins/os-sdlc/lib/os_sdlc/project.rb (OsSdlc::Project)
Root candidate: none found; searched Project, ActiveModel, project.rb, Data.define, project_config, connect(:db)
Approved by: Jared named and approved this ticket in session on 2026-11-10.

VERDICT: CREATE
REASON: No open ticket converts OsSdlc::Project from Data to an ActiveModel class. #586 and #1200 name the same file or subsystem but a different fault.

## Problem `OsSdlc::Project` (`plugins/os-sdlc/lib/os_sdlc/project.rb`) is `Data.define(:name, :root, :tracker, :targets, :selected)`. The project is persisted as a JSON snapshot in the `implementations.project_config` column. The next ticket makes the project hold connections (`connect(:db)` and a lazy `repo`). A frozen `Data` object cannot hold them. ADR-0312 accepted `activemodel` as a direct dependency of os-sdlc. ## Approach `Project` becomes a Ruby class that includes `ActiveModel::Model` and `ActiveModel::Attributes`. The five fields become attributes. `gem "activemodel"` goes into `plugins/os-sdlc/Gemfile`, and `Gemfile.lock` is updated. `require "active_model"` and `ActiveModel.eager_load!` are loaded as `plugins/os-backlog/lib/backlog.rb:9-12` does it. `Project::Target`, `Project::Suite`, and `Project::Lint` stay `Data`. The language is Ruby. The suite command is `ruby test/all.rb`, run from `plugins/os-sdlc`. Kept behavior. The existing suite pins these items, so they are not red-round criteria. The new class must keep each one. - Value equality: two projects with equal `name`, `root`, `tracker`, `targets`, and `selected` are `==`, are `eql?`, and have equal `hash` (`test/project_json_round_trip_test.rb:13`, `test/project_nested_code_test_lint_test.rb:105`; `Crc::Census::Settings` holds a project inside a `Data`). - `Project.new` takes the same keywords with the same defaults: `targets` defaults to `[Project::Target.new(name: name)]`, and `root`, `tracker`, and `selected` default to nil. `Project::Unconfigured < Project` still loads and constructs. - `project.with(...)` returns a changed copy and leaves the receiver unchanged (call sites `project.rb:12`, `project.rb:23`, `project/config.rb:17`, `project/config.rb:71`). `bound_to` returns the same result as before. - `case project; in Project[root:]` matches (`lib/os_sdlc/boot/commands.rb:164`), so `deconstruct_keys` must exist. - `to_json` and `to_snapshot_h` emit exactly the key set `{name, tracker, targets: {<target name> => {path, test_path, test_command, test_framework, lint_config, tracker}}}` through `Project::Snapshot`. `root` and `selected` never enter the snapshot. A legacy snapshot with no `targets` key still loads. Do not use the JSON serializer of ActiveModel. - The raise messages in `project.rb` that name `.sdlc/project.yaml` and `--target NAME` stay verbatim. Required work: 1. present `gem "activemodel"` in `plugins/os-sdlc/Gemfile` 2. present `ActiveModel::Attributes` in `plugins/os-sdlc/lib/os_sdlc/project.rb` 3. absent `Data.define(:name, :root, :tracker, :targets, :selected)` in `plugins/os-sdlc/lib/os_sdlc/project.rb` ## Acceptance criteria 1. A project is not frozen: `Project.new(name: "x").frozen?` is false, and a test sets an instance variable on a `Project` and reads it back. 2. `Project.new(name: "x", tracker: "t").attributes` returns a Hash with exactly the keys `"name"`, `"root"`, `"tracker"`, `"targets"`, and `"selected"`, with the given values. 3. `Project.new(name: "x")` answers `valid?` with true and has an empty `errors` object. ## Owner sketch `OsSdlc::Project` owns its five attributes, their defaults, value equality, the changed copy (`with`), pattern-match keys (`deconstruct_keys`), and the delegation of its snapshot to `Project::Snapshot`. This ticket adds no object and moves no responsibility. ## Out of scope `connect`, `repo`, the Thor entry, the removal of `Boot`, and new validations. These are the next tickets of the same change. ## Origin Trigger: n/a: design ticket named by the user, not a run that showed a fault Improvised this session: none Chain: the next ticket cannot hold `connect(:db)` and a lazy `repo` on Project ← Project is a frozen `Data.define` ← ADR-0312 (DESIGN) accepted `activemodel` as a direct dependency Session: a48658d5-b566-40ce-8257-ebeabf56f7e1 Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os-plugins-os-sdlc/a48658d5-b566-40ce-8257-ebeabf56f7e1.jsonl Where: plugins/os-sdlc/lib/os_sdlc/project.rb (OsSdlc::Project) Root candidate: none found; searched Project, ActiveModel, project.rb, Data.define, project_config, connect(:db) Approved by: Jared named and approved this ticket in session on 2026-11-10. VERDICT: CREATE REASON: No open ticket converts OsSdlc::Project from Data to an ActiveModel class. #586 and #1200 name the same file or subsystem but a different fault.
Author
Owner

Work started on branch ticket-1672 through /os-sdlc:implement (map poodr-ticket-implementation).

Work started on branch ticket-1672 through /os-sdlc:implement (map poodr-ticket-implementation).
Author
Owner

Resolution

Done: OsSdlc::Project is an ActiveModel class (ActiveModel::Model + ActiveModel::Attributes) with the five fields as attributes. It is not frozen. Value equality, with, deconstruct_keys, the keyword constructor, and the snapshot key set are kept. activemodel is a direct gem in plugins/os-sdlc/Gemfile (ADR-0312). active_model loads only with Project and with no eager load.

Evidence: Implementation 270 complete on branch ticket-1672: commits 7262854fc and cd1fe70bd. All gates passed (suite, survivors after one survivor-test round, contract-auditor). Full suite after the load fix: 3916 runs, 8055 assertions, 0 failures. Load of lib/os_sdlc plus Project: 0.17 s before, 0.22 s after.

Follow-ups: none: the next pieces of the same change (project.connect and lazy project.repo; Thor entry; removal of Boot) are worked in this session and get their own tickets.

## Resolution **Done:** OsSdlc::Project is an ActiveModel class (ActiveModel::Model + ActiveModel::Attributes) with the five fields as attributes. It is not frozen. Value equality, with, deconstruct_keys, the keyword constructor, and the snapshot key set are kept. activemodel is a direct gem in plugins/os-sdlc/Gemfile (ADR-0312). active_model loads only with Project and with no eager load. **Evidence:** Implementation 270 complete on branch ticket-1672: commits 7262854fc and cd1fe70bd. All gates passed (suite, survivors after one survivor-test round, contract-auditor). Full suite after the load fix: 3916 runs, 8055 assertions, 0 failures. Load of lib/os_sdlc plus Project: 0.17 s before, 0.22 s after. **Follow-ups:** none: the next pieces of the same change (project.connect and lazy project.repo; Thor entry; removal of Boot) are worked in this session and get their own tickets. <!-- os-backlog kind: resolution -->
jared closed this issue 2026-11-10 18:01:42 +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#1672
No description provided.