Add a concept-cycle lint signal (report only) #949

Closed
opened 2026-10-05 12:28:55 +00:00 by jared · 4 comments
Owner

Problem

Nothing in the repo detects cycles between concepts. Zeitwerk loads them without a warning. A cycle means two concepts change together, so neither can be read or moved alone.
The G4 challenge measured 11 cycle pairs: 1 in os-adr (Record / Repository) and 10 in os-sdlc. It used the script docs/crc/os-adr/scripts/concept-cycles.rb.

Observed

ruby docs/crc/os-adr/scripts/concept-cycles.rb Adr plugins/os-adr/lib
SCC (2): Record, Repository

The script uses Prism to read constants and TSort to find strongly connected components. It takes the root namespace and the lib directory. It lives under docs/, so no crc run finds it.

Reproduce

n/a: missing feature. No existing command reports cycles.

Approach

Promote the script into os-sdlc, next to the inward-dependencies lint. It uses the same layout: a class under lib/os_sdlc/lint/ and a bin/lint-* script. It reports cycles and exits 0. It is a signal for crc-refactor runs. It has no baseline and is not a gate.

The promoted script replaces docs/crc/os-adr/scripts/concept-cycles.rb; delete that file in the same change (no Required-work check can verify a deleted file until #954).
Required work:

  1. Add OsSdlc::ConceptCycles in plugins/os-sdlc/lib/os_sdlc/lint/concept_cycles.rb. It takes a root namespace and a lib dir, and answers the cycles. — check: present ConceptCycles in plugins/os-sdlc/lib/os_sdlc/lint/concept_cycles.rb
  2. Add plugins/os-sdlc/bin/lint-concept-cycles. It prints the cycles and exits 0. — check: present ConceptCycles in plugins/os-sdlc/bin/lint-concept-cycles
  3. Add a Minitest test over a small fixture tree with one cycle and one acyclic pair. — check: present ConceptCycles in plugins/os-sdlc/tests/lint/concept_cycles_test.rb

Rejected: add G4 to the lens — A15 and A2 already decide every measured cycle (docs/crc/os-adr/lens-challenge-g4-concept-cycles.md).
Rejected: a gate or baseline — case 2 made this lint a signal with no baseline.

Acceptance criteria

  1. Given a fixture tree with two concepts that reference each other, when the lint runs, then the output names both concepts as one cycle.
  2. Given a fixture tree with two concepts where only one references the other, when the lint runs, then the output names no cycle.
  3. Given a tree with a cycle, when the lint runs, then it exits 0.

Owner sketch

OsSdlc::ConceptCycles owns cycle detection between concepts, next to OsSdlc::InwardDependencies.

class OsSdlc::ConceptCycles
  def initialize(root_namespace:, lib_dir:)
  def cycles  # array of sorted concept-name arrays, one per strongly connected component of size > 1
end
  • bin/lint-concept-cycles takes the root namespace and the lib dir, calls ConceptCycles.new(...).cycles, prints one line per cycle, and exits 0.
  • plugins/os-sdlc/tests/lint/concept_cycles_test.rb calls cycles over a fixture tree.
  • docs/crc/os-adr/scripts/concept-cycles.rb is deleted.

Non-goals

Fixing the 11 cycles. Moving Adr::Record::Creation (a separate ruling, R-G4-2).

Origin

  • Trigger: lens challenge G4, commit 4b8a1730. The script measured 11 mutual pairs.
  • Improvised this session: none
  • Chain: cycles load silently ← no tool reports concept cycles ← the only detector is a one-off script under docs/ (DESIGN: docs/crc/os-adr/lens-challenge-g4-concept-cycles.md).
  • Root candidate: none
  • Where: docs/crc/os-adr/scripts/concept-cycles.rb
  • Session: ab599e53-dbf5-4953-b8bf-c598a9c5c16e
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os--claude-worktrees-crc-os-adr-2026-10-04/ab599e53-dbf5-4953-b8bf-c598a9c5c16e.jsonl
## Problem Nothing in the repo detects cycles between concepts. Zeitwerk loads them without a warning. A cycle means two concepts change together, so neither can be read or moved alone. The G4 challenge measured 11 cycle pairs: 1 in os-adr (`Record` / `Repository`) and 10 in os-sdlc. It used the script `docs/crc/os-adr/scripts/concept-cycles.rb`. ## Observed ``` ruby docs/crc/os-adr/scripts/concept-cycles.rb Adr plugins/os-adr/lib ``` ``` SCC (2): Record, Repository ``` The script uses Prism to read constants and `TSort` to find strongly connected components. It takes the root namespace and the lib directory. It lives under docs/, so no crc run finds it. ## Reproduce n/a: missing feature. No existing command reports cycles. ## Approach Promote the script into os-sdlc, next to the inward-dependencies lint. It uses the same layout: a class under `lib/os_sdlc/lint/` and a `bin/lint-*` script. It reports cycles and exits 0. It is a signal for crc-refactor runs. It has no baseline and is not a gate. The promoted script replaces `docs/crc/os-adr/scripts/concept-cycles.rb`; delete that file in the same change (no Required-work check can verify a deleted file until #954). Required work: 1. Add `OsSdlc::ConceptCycles` in `plugins/os-sdlc/lib/os_sdlc/lint/concept_cycles.rb`. It takes a root namespace and a lib dir, and answers the cycles. — check: present `ConceptCycles` in `plugins/os-sdlc/lib/os_sdlc/lint/concept_cycles.rb` 2. Add `plugins/os-sdlc/bin/lint-concept-cycles`. It prints the cycles and exits 0. — check: present `ConceptCycles` in `plugins/os-sdlc/bin/lint-concept-cycles` 3. Add a Minitest test over a small fixture tree with one cycle and one acyclic pair. — check: present `ConceptCycles` in `plugins/os-sdlc/tests/lint/concept_cycles_test.rb` Rejected: add G4 to the lens — A15 and A2 already decide every measured cycle (docs/crc/os-adr/lens-challenge-g4-concept-cycles.md). Rejected: a gate or baseline — case 2 made this lint a signal with no baseline. ## Acceptance criteria 1. Given a fixture tree with two concepts that reference each other, when the lint runs, then the output names both concepts as one cycle. 2. Given a fixture tree with two concepts where only one references the other, when the lint runs, then the output names no cycle. 3. Given a tree with a cycle, when the lint runs, then it exits 0. ## Owner sketch `OsSdlc::ConceptCycles` owns cycle detection between concepts, next to `OsSdlc::InwardDependencies`. ```ruby class OsSdlc::ConceptCycles def initialize(root_namespace:, lib_dir:) def cycles # array of sorted concept-name arrays, one per strongly connected component of size > 1 end ``` - `bin/lint-concept-cycles` takes the root namespace and the lib dir, calls `ConceptCycles.new(...).cycles`, prints one line per cycle, and exits 0. - `plugins/os-sdlc/tests/lint/concept_cycles_test.rb` calls `cycles` over a fixture tree. - `docs/crc/os-adr/scripts/concept-cycles.rb` is deleted. ## Non-goals Fixing the 11 cycles. Moving `Adr::Record::Creation` (a separate ruling, R-G4-2). ## Origin - Trigger: lens challenge G4, commit 4b8a1730. The script measured 11 mutual pairs. - Improvised this session: none - Chain: cycles load silently ← no tool reports concept cycles ← the only detector is a one-off script under docs/ (DESIGN: docs/crc/os-adr/lens-challenge-g4-concept-cycles.md). - Root candidate: none - Where: docs/crc/os-adr/scripts/concept-cycles.rb - Session: ab599e53-dbf5-4953-b8bf-c598a9c5c16e - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os--claude-worktrees-crc-os-adr-2026-10-04/ab599e53-dbf5-4953-b8bf-c598a9c5c16e.jsonl
Author
Owner

Work started on branch crc-os-adr-2026-10-04 (persistent worktree) via /os-sdlc:implement.

Work started on branch crc-os-adr-2026-10-04 (persistent worktree) via /os-sdlc:implement.
Author
Owner

Required-work item 4 check removed: an absent check on a glob that the same item's delete leaves empty can never pass (checker reports 'glob matched no file'). Implementation 32 escalated on it; reopening.

Required-work item 4 check removed: an absent check on a glob that the same item's delete leaves empty can never pass (checker reports 'glob matched no file'). Implementation 32 escalated on it; reopening.
Author
Owner

Draft B dropped by the skeptic gate (ticket-caused required-work failure routes to programmer-repair). Reason: it is a symptom of draft A (absent check on an emptied glob can never pass). Fix A and the unfixable failure goes away. A general ticket-defect route is speculative until a second failure of this kind shows up.

Draft B dropped by the skeptic gate (ticket-caused required-work failure routes to programmer-repair). Reason: it is a symptom of draft A (absent check on an emptied glob can never pass). Fix A and the unfixable failure goes away. A general ticket-defect route is speculative until a second failure of this kind shows up.
Author
Owner

Resolution

Done: Added OsSdlc::ConceptCycles (lib/os_sdlc/lint/concept_cycles.rb, References nested under it) and bin/lint-concept-cycles ROOT_NAMESPACE LIB_DIR: prints one 'cycle: A, B' line per strongly connected component of concepts and exits 0 (signal, no baseline). Deleted docs/crc/os-adr/scripts/concept-cycles.rb.

Evidence: Commits 58f9ede0 (test, Runner) and 530c98f6 (production + bin + delete; written by abandoned implementation 32, passed every gate of implementation 34) on branch crc-os-adr-2026-10-04. os-sdlc suite green (1495 runs, 0 failures). Live run: 3 cycle groups in os-sdlc (21-concept group; Daemon/LspSession/Server; Fingerprint/Probe) and Record/Repository in os-adr, matching the G4 challenge.

Follow-ups: #954 absent Required-work check on an emptied glob can never pass (caused implementations 32 and 33 to fail). Reopen after abandon needed a reroute (#951 shape) and the Runner commit omitted files written by the abandoned run; covered by #951. Branch not yet merged to main: waits for the user's decision on the whole crc-os-adr branch.

## Resolution **Done:** Added OsSdlc::ConceptCycles (lib/os_sdlc/lint/concept_cycles.rb, References nested under it) and bin/lint-concept-cycles ROOT_NAMESPACE LIB_DIR: prints one 'cycle: A, B' line per strongly connected component of concepts and exits 0 (signal, no baseline). Deleted docs/crc/os-adr/scripts/concept-cycles.rb. **Evidence:** Commits 58f9ede0 (test, Runner) and 530c98f6 (production + bin + delete; written by abandoned implementation 32, passed every gate of implementation 34) on branch crc-os-adr-2026-10-04. os-sdlc suite green (1495 runs, 0 failures). Live run: 3 cycle groups in os-sdlc (21-concept group; Daemon/LspSession/Server; Fingerprint/Probe) and Record/Repository in os-adr, matching the G4 challenge. **Follow-ups:** #954 absent Required-work check on an emptied glob can never pass (caused implementations 32 and 33 to fail). Reopen after abandon needed a reroute (#951 shape) and the Runner commit omitted files written by the abandoned run; covered by #951. Branch not yet merged to main: waits for the user's decision on the whole crc-os-adr branch.
jared closed this issue 2026-10-05 13:25:41 +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#949
No description provided.