Fail loudly when system-model root has no Ruby files in lib #1515

Closed
opened 2026-11-02 22:36:01 +00:00 by jared · 2 comments
Owner

Problem

system-model with a root that does not exist, or that has no Ruby files under lib/, prints classes: 0 and exits 0.
A mistyped path looks like a valid empty model.

Observed

The chain, with the line of each step:

  1. plugins/os-sdlc/lib/os_sdlc/boot/inputs.rb:70 expands the argument with File.expand_path(argv.first, io.cwd). A relative path resolves against the cwd.
  2. plugins/os-sdlc/lib/os_sdlc/boot.rb:72 calls input.root_or(...). The given root wins, so ProjectBind (boot/project_bind.rb:10-14) never looks up .sdlc/project.yaml.
  3. plugins/os-sdlc/lib/os_sdlc/system_model/extractor.rb:54 globs lib/**/*.rb under the root and finds nothing.
  4. plugins/os-sdlc/lib/os_sdlc/system_model/report.rb:11 prints classes: 0.
    The no-argument form does not use project.yaml today: inputs.rb:68 refuses it with the usage text and exit 2.

Reproduce

cd /tmp && /home/jared/dev/cc-os/plugins/os-sdlc/bin/system-model generate plugins/os-backlog; echo "exit $?"
classes: 0
directory	stereotype	count
kind	card	detail
drift: 0 rows
exit 0

Approach

Choice on relative roots: a relative root keeps resolving against the cwd, as inputs.rb:70 does now. Reason: a path typed in a shell means the cwd, and .sdlc/project.yaml marks the outermost project root (Project::RootSearch.outermost), which differs from a plugin root such as plugins/os-backlog. A project-root rule would move the meaning of every existing call.
The fix is a refusal, not a new lookup. SystemModel::Run#call (system_model/run.rb:17) raises Boot::RefusalError (status 1, the pattern of run.rb:55) when the subcommand is generate, build, coverage or opportunities and the resolved root has no lib/**/*.rb file. The message names the resolved absolute path. A nonexistent root takes the same path: the glob is empty. The guard sits in Run, not in Extractor, because Shape::Run (system_model/shape/run.rb:26) builds one Extractor per root with no per-root rescue, and shapes over plugins with no lib/ (cc-architect, os-context, os-doc-hygiene, os-vault) must keep working. The guard counts lib/**/*.rb only, so a bin-only root is refused too, as the user asked. serve, stop and status do not run the Extractor and keep their behavior.
The no-argument form stays as it is (usage, exit 2). Binding the no-argument form to .sdlc/project.yaml is a separate decision (Non-goals).

Required work:

  1. Refuse an empty lib/ glob in Run — check: present RefusalError in plugins/os-sdlc/lib/os_sdlc/system_model/run.rb
  2. Add tests — check: present no Ruby files in plugins/os-sdlc/tests/system_model_cli_test.rb

Orchestrator work: run bin/refresh-plugins; add one History row to docs/implementation-status/os-sdlc-system-model.md.

Acceptance criteria

  1. Given a root directory that does not exist, when system-model generate <root> runs, then it exits nonzero and the error text contains the resolved absolute path.
  2. Given an existing root with no lib/ directory, when system-model generate <root> runs, then it exits nonzero and the error text contains the resolved absolute path.
  3. Given an existing root whose lib/ has no .rb file, when system-model coverage <root> runs, then it exits nonzero and prints no classes: line.
  4. Given system-model shapes over a plugin root with no lib/ directory, when it runs, then its behavior is unchanged.
  5. Given a relative root and a cwd, when the command runs, then the error path is the root expanded against the cwd.
  6. Given a root with at least one .rb file under lib/, when system-model generate <root> runs, then the output and exit status are unchanged.
  7. Given no argument, when system-model runs, then it prints the usage text and exits 2, as before.

Owner sketch

class OsSdlc::SystemModel::Run
  def call(subcommand = "generate", json: false)
end

Non-goals

Binding the no-argument form to .sdlc/project.yaml: the outermost project root is not a plugin root, so the user must rule on which root applies. system-model shapes roots (Shape::Run) and bin-only roots in shapes.

Origin

  • Trigger: cd /tmp && plugins/os-sdlc/bin/system-model generate plugins/os-backlog printed classes: 0 and exited 0.
  • Improvised this session: none
  • Chain: classes: 0 for a wrong path ← no check that the root has Ruby files ← DESIGN (plugins/os-sdlc/lib/os_sdlc/system_model/extractor.rb:54, plugins/os-sdlc/lib/os_sdlc/boot/inputs.rb:66-71)
  • Root candidate: none found; searched system-model, system_model, classes: 0, no Ruby, nonexistent root, lib/**
    VERDICT: CREATE
  • Where: plugins/os-sdlc/lib/os_sdlc/system_model/run.rb:17
  • Session: d077e084-9195-4803-b761-6ce01eca0249
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/d077e084-9195-4803-b761-6ce01eca0249.jsonl
## Problem `system-model` with a root that does not exist, or that has no Ruby files under `lib/`, prints `classes: 0` and exits 0. A mistyped path looks like a valid empty model. ## Observed The chain, with the line of each step: 1. `plugins/os-sdlc/lib/os_sdlc/boot/inputs.rb:70` expands the argument with `File.expand_path(argv.first, io.cwd)`. A relative path resolves against the cwd. 2. `plugins/os-sdlc/lib/os_sdlc/boot.rb:72` calls `input.root_or(...)`. The given root wins, so `ProjectBind` (`boot/project_bind.rb:10-14`) never looks up `.sdlc/project.yaml`. 3. `plugins/os-sdlc/lib/os_sdlc/system_model/extractor.rb:54` globs `lib/**/*.rb` under the root and finds nothing. 4. `plugins/os-sdlc/lib/os_sdlc/system_model/report.rb:11` prints `classes: 0`. The no-argument form does not use `project.yaml` today: `inputs.rb:68` refuses it with the usage text and exit 2. ## Reproduce ``` cd /tmp && /home/jared/dev/cc-os/plugins/os-sdlc/bin/system-model generate plugins/os-backlog; echo "exit $?" ``` ``` classes: 0 directory stereotype count kind card detail drift: 0 rows exit 0 ``` ## Approach Choice on relative roots: a relative root keeps resolving against the cwd, as `inputs.rb:70` does now. Reason: a path typed in a shell means the cwd, and `.sdlc/project.yaml` marks the outermost project root (`Project::RootSearch.outermost`), which differs from a plugin root such as `plugins/os-backlog`. A project-root rule would move the meaning of every existing call. The fix is a refusal, not a new lookup. `SystemModel::Run#call` (`system_model/run.rb:17`) raises `Boot::RefusalError` (status 1, the pattern of `run.rb:55`) when the subcommand is `generate`, `build`, `coverage` or `opportunities` and the resolved root has no `lib/**/*.rb` file. The message names the resolved absolute path. A nonexistent root takes the same path: the glob is empty. The guard sits in `Run`, not in `Extractor`, because `Shape::Run` (`system_model/shape/run.rb:26`) builds one Extractor per root with no per-root rescue, and `shapes` over plugins with no `lib/` (cc-architect, os-context, os-doc-hygiene, os-vault) must keep working. The guard counts `lib/**/*.rb` only, so a bin-only root is refused too, as the user asked. `serve`, `stop` and `status` do not run the Extractor and keep their behavior. The no-argument form stays as it is (usage, exit 2). Binding the no-argument form to `.sdlc/project.yaml` is a separate decision (Non-goals). Required work: 1. Refuse an empty `lib/` glob in `Run` — check: present `RefusalError` in `plugins/os-sdlc/lib/os_sdlc/system_model/run.rb` 2. Add tests — check: present `no Ruby files` in `plugins/os-sdlc/tests/system_model_cli_test.rb` Orchestrator work: run `bin/refresh-plugins`; add one History row to `docs/implementation-status/os-sdlc-system-model.md`. ## Acceptance criteria 1. Given a root directory that does not exist, when `system-model generate <root>` runs, then it exits nonzero and the error text contains the resolved absolute path. 2. Given an existing root with no `lib/` directory, when `system-model generate <root>` runs, then it exits nonzero and the error text contains the resolved absolute path. 3. Given an existing root whose `lib/` has no `.rb` file, when `system-model coverage <root>` runs, then it exits nonzero and prints no `classes:` line. 4. Given `system-model shapes` over a plugin root with no `lib/` directory, when it runs, then its behavior is unchanged. 5. Given a relative root and a cwd, when the command runs, then the error path is the root expanded against the cwd. 6. Given a root with at least one `.rb` file under `lib/`, when `system-model generate <root>` runs, then the output and exit status are unchanged. 7. Given no argument, when `system-model` runs, then it prints the usage text and exits 2, as before. ## Owner sketch ```ruby class OsSdlc::SystemModel::Run def call(subcommand = "generate", json: false) end ``` ## Non-goals Binding the no-argument form to `.sdlc/project.yaml`: the outermost project root is not a plugin root, so the user must rule on which root applies. `system-model shapes` roots (`Shape::Run`) and bin-only roots in `shapes`. ## Origin - Trigger: `cd /tmp && plugins/os-sdlc/bin/system-model generate plugins/os-backlog` printed `classes: 0` and exited 0. - Improvised this session: none - Chain: `classes: 0` for a wrong path ← no check that the root has Ruby files ← DESIGN (`plugins/os-sdlc/lib/os_sdlc/system_model/extractor.rb:54`, `plugins/os-sdlc/lib/os_sdlc/boot/inputs.rb:66-71`) - Root candidate: none found; searched system-model, system_model, classes: 0, no Ruby, nonexistent root, lib/** VERDICT: CREATE - Where: plugins/os-sdlc/lib/os_sdlc/system_model/run.rb:17 - Session: d077e084-9195-4803-b761-6ce01eca0249 - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/d077e084-9195-4803-b761-6ce01eca0249.jsonl
Author
Owner

Work started on branch ticket-1515 through /os-sdlc:implement.

Work started on branch `ticket-1515` through /os-sdlc:implement.
Author
Owner

Resolution

Done: system-model refuses a root with no Ruby files under lib/: exit 1 and an error that names the resolved root. shapes still accepts a root without lib/. Extractor.sources is the one owner of the lib/**/*.rb glob.

Evidence: Commit 41cdf5fe on ticket-1515 (implementation 222). Five new tests in plugins/os-sdlc/tests/system_model_cli_test.rb. Suite gate green twice (dispatches 2016, 2019). Contract audit pass (2020).

Follow-ups: none

## Resolution **Done:** system-model refuses a root with no Ruby files under lib/: exit 1 and an error that names the resolved root. shapes still accepts a root without lib/. Extractor.sources is the one owner of the lib/**/*.rb glob. **Evidence:** Commit 41cdf5fe on ticket-1515 (implementation 222). Five new tests in plugins/os-sdlc/tests/system_model_cli_test.rb. Suite gate green twice (dispatches 2016, 2019). Contract audit pass (2020). **Follow-ups:** none
jared closed this issue 2026-11-03 13:44:52 +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#1515
No description provided.