os-backlog validator: optional sections list for ## Retired tests and ## Approach #612

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

os-backlog validator: optional sections list for ## Retired tests and ## Approach [capture]

Context

#610 (commit 7462a0d2) added ## Retired tests to capture.md as optional, one path#test_method per line.
TicketBodyChecker#check_headings only visits @required_sections + @conditional_sections (plugins/os-backlog/lib/backlog/ticket_body_checker.rb:26).
Backlog::TemplateValidator only knows REQUIRED_SECTIONS and AGENT_READY_SECTIONS (plugins/os-backlog/lib/backlog/template_validator.rb:13,15).
An unlisted heading is never visited, so a malformed Retired tests line passes capture and fails later at implementation-open.

Change Effect
Add Retired tests to AGENT_READY_SECTIONS Wrong: that list means required-and-non-empty for ready-for-agent; rejects every agent-ready ticket with no tests to retire
Add a third, optional-sections list Checked only when present; empty-when-absent is allowed

Observed

ruby -e '
require_relative "plugins/os-backlog/lib/backlog/ticket_body_checker"
require_relative "plugins/os-backlog/lib/backlog/issue_body"
require_relative "plugins/os-backlog/lib/backlog/headings_holder"
require_relative "plugins/os-backlog/lib/backlog/validation_report"
body = Backlog::IssueBody.new("## Slice\nx\n## Acceptance criteria\nx\n## Non-goals\nx\n## Retired tests\nnot a test line\n")
checker = Backlog::TicketBodyChecker.new(body: body, tracker: nil, required_depth: 2, sections: { required: ["Slice", "Acceptance criteria", "Non-goals"], conditional: [] })
p checker.call
'
[]

Reproduce

ruby -e '
require_relative "plugins/os-backlog/lib/backlog/ticket_body_checker"
require_relative "plugins/os-backlog/lib/backlog/issue_body"
require_relative "plugins/os-backlog/lib/backlog/headings_holder"
require_relative "plugins/os-backlog/lib/backlog/validation_report"
body = Backlog::IssueBody.new("## Slice\nx\n## Acceptance criteria\nx\n## Non-goals\nx\n## Retired tests\nnot a test line\n")
checker = Backlog::TicketBodyChecker.new(body: body, tracker: nil, required_depth: 2, sections: { required: ["Slice", "Acceptance criteria", "Non-goals"], conditional: [] })
p checker.call
'

Expected

def test_retired_tests_line_not_matching_path_hash_test_method_reports_error
  body = Backlog::IssueBody.new("## Slice\nx\n## Acceptance criteria\nx\n## Non-goals\nx\n## Retired tests\nnot a test line\n")
  checker = Backlog::TicketBodyChecker.new(
    body: body, tracker: nil, required_depth: 2,
    sections: { required: ["Slice", "Acceptance criteria", "Non-goals"], conditional: [], optional: ["Retired tests"] }
  )
  assert_includes checker.call, "Retired tests: invalid line: not a test line"
end

Slice

  • Add OPTIONAL_SECTIONS = ["Retired tests", "Approach"].freeze to Backlog::TemplateValidator.
  • Pass optional: OPTIONAL_SECTIONS into the sections hash built by TemplateValidator#build_checker.
  • TicketBodyChecker accepts sections[:optional] (default []) and stores it as @optional_sections.
  • check_headings iterates @required_sections + @conditional_sections + present_optional_sections, where present_optional_sections filters @optional_sections to those headings_holder.select_by_name(name) is non-empty for.
  • Depth, duplicate, and empty checks reuse check_heading_section unchanged for the optional sections that are present.
  • Add RETIRED_TEST_LINE = /\A\S+#\S+\z/ to TicketBodyChecker and a check_retired_test_lines(match) step, called only when @name == "Retired tests", that reports each non-blank line failing the regex by name.
  • Document ## Approach in plugins/os-backlog/references/templates/capture.md as optional, under the ## Retired tests line in the agent-ready section.

Acceptance criteria

  • The seven cases under ## Cases are green as Minitest tests.
  • plugins/os-backlog test suite is green.
  • capture.md documents ## Approach as optional with its content rule.

Non-goals

  • No change to os-sdlc.
  • No change to REQUIRED_SECTIONS or AGENT_READY_SECTIONS.
  • No line-format rule for ## Approach.

Owner sketch

module Backlog
  class TemplateValidator
    OPTIONAL_SECTIONS = ["Retired tests", "Approach"].freeze

    def build_checker(body, labels)
    end
  end

  class TicketBodyChecker
    RETIRED_TEST_LINE = /\A\S+#\S+\z/

    def initialize(body:, tracker:, required_depth:, sections:)
    end

    def check_headings
    end

    def present_optional_sections
    end

    def check_retired_test_lines(match)
    end
  end
end

Cases

  1. Given a body without ## Retired tests or ## Approach, when validated, then it passes unchanged.
  2. Given ## Retired tests present with valid path#test_method lines, when validated, then it passes.
  3. Given ## Retired tests with a line not matching path#test_method, when validated, then it reports an error naming the line.
  4. Given ## Retired tests present but empty, when validated, then it reports an empty section.
  5. Given ## Approach present and non-empty, when validated, then it passes; given ## Approach present and empty, then it reports an empty section.
  6. Given an agent-ready ticket without ## Retired tests or ## Approach, when validated, then it passes.
  7. Given capture.md, when read, then it documents ## Approach as optional with its content rule.

Approach

Rejected: adding Retired tests to AGENT_READY_SECTIONS. Reason: that list means required-and-non-empty for ready-for-agent; it would reject every agent-ready ticket with no tests to retire.
Chosen: a third OPTIONAL_SECTIONS list, checked only when the heading is present.
Depends on: HeadingsHolder#select_by_name already returning [] for an absent heading, so "present" is a plain emptiness check.
## Approach motivation: design intent today has only ## Owner sketch (code shape) and ## Cases (one-line items; prose there caused an escalation, #593), so approach prose has no home. #611 will be the first ticket to use it.

Origin

  • Trigger: review of the #610 final summary, 2026-09-18, user asked for the ticket.
  • Improvised this session: none.
  • Chain: malformed Retired tests passes capture ← validator visits listed headings only ← no optional-sections concept ← DESIGN (plugins/os-backlog/lib/backlog/template_validator.rb).
  • Root candidate: this ticket.
  • Where: plugins/os-backlog/lib/backlog/template_validator.rb, plugins/os-backlog/lib/backlog/ticket_body_checker.rb.
  • Session: edb23374-6b44-4410-b666-a485a1923501
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/edb23374-6b44-4410-b666-a485a1923501.jsonl
  • Related: #610, #611
# os-backlog validator: optional sections list for `## Retired tests` and `## Approach` [capture] ## Context #610 (commit 7462a0d2) added `## Retired tests` to capture.md as optional, one `path#test_method` per line. `TicketBodyChecker#check_headings` only visits `@required_sections + @conditional_sections` (`plugins/os-backlog/lib/backlog/ticket_body_checker.rb:26`). `Backlog::TemplateValidator` only knows `REQUIRED_SECTIONS` and `AGENT_READY_SECTIONS` (`plugins/os-backlog/lib/backlog/template_validator.rb:13,15`). An unlisted heading is never visited, so a malformed `Retired tests` line passes capture and fails later at `implementation-open`. | Change | Effect | | --- | --- | | Add `Retired tests` to `AGENT_READY_SECTIONS` | Wrong: that list means required-and-non-empty for `ready-for-agent`; rejects every agent-ready ticket with no tests to retire | | Add a third, optional-sections list | Checked only when present; empty-when-absent is allowed | ## Observed ``` ruby -e ' require_relative "plugins/os-backlog/lib/backlog/ticket_body_checker" require_relative "plugins/os-backlog/lib/backlog/issue_body" require_relative "plugins/os-backlog/lib/backlog/headings_holder" require_relative "plugins/os-backlog/lib/backlog/validation_report" body = Backlog::IssueBody.new("## Slice\nx\n## Acceptance criteria\nx\n## Non-goals\nx\n## Retired tests\nnot a test line\n") checker = Backlog::TicketBodyChecker.new(body: body, tracker: nil, required_depth: 2, sections: { required: ["Slice", "Acceptance criteria", "Non-goals"], conditional: [] }) p checker.call ' ``` ``` [] ``` ## Reproduce ``` ruby -e ' require_relative "plugins/os-backlog/lib/backlog/ticket_body_checker" require_relative "plugins/os-backlog/lib/backlog/issue_body" require_relative "plugins/os-backlog/lib/backlog/headings_holder" require_relative "plugins/os-backlog/lib/backlog/validation_report" body = Backlog::IssueBody.new("## Slice\nx\n## Acceptance criteria\nx\n## Non-goals\nx\n## Retired tests\nnot a test line\n") checker = Backlog::TicketBodyChecker.new(body: body, tracker: nil, required_depth: 2, sections: { required: ["Slice", "Acceptance criteria", "Non-goals"], conditional: [] }) p checker.call ' ``` ## Expected ```ruby def test_retired_tests_line_not_matching_path_hash_test_method_reports_error body = Backlog::IssueBody.new("## Slice\nx\n## Acceptance criteria\nx\n## Non-goals\nx\n## Retired tests\nnot a test line\n") checker = Backlog::TicketBodyChecker.new( body: body, tracker: nil, required_depth: 2, sections: { required: ["Slice", "Acceptance criteria", "Non-goals"], conditional: [], optional: ["Retired tests"] } ) assert_includes checker.call, "Retired tests: invalid line: not a test line" end ``` ## Slice - [ ] Add `OPTIONAL_SECTIONS = ["Retired tests", "Approach"].freeze` to `Backlog::TemplateValidator`. - [ ] Pass `optional: OPTIONAL_SECTIONS` into the `sections` hash built by `TemplateValidator#build_checker`. - [ ] `TicketBodyChecker` accepts `sections[:optional]` (default `[]`) and stores it as `@optional_sections`. - [ ] `check_headings` iterates `@required_sections + @conditional_sections + present_optional_sections`, where `present_optional_sections` filters `@optional_sections` to those `headings_holder.select_by_name(name)` is non-empty for. - [ ] Depth, duplicate, and empty checks reuse `check_heading_section` unchanged for the optional sections that are present. - [ ] Add `RETIRED_TEST_LINE = /\A\S+#\S+\z/` to `TicketBodyChecker` and a `check_retired_test_lines(match)` step, called only when `@name == "Retired tests"`, that reports each non-blank line failing the regex by name. - [ ] Document `## Approach` in `plugins/os-backlog/references/templates/capture.md` as optional, under the `## Retired tests` line in the agent-ready section. ## Acceptance criteria - [ ] The seven cases under `## Cases` are green as Minitest tests. - [ ] `plugins/os-backlog` test suite is green. - [ ] `capture.md` documents `## Approach` as optional with its content rule. ## Non-goals - [ ] No change to `os-sdlc`. - [ ] No change to `REQUIRED_SECTIONS` or `AGENT_READY_SECTIONS`. - [ ] No line-format rule for `## Approach`. ## Owner sketch ```ruby module Backlog class TemplateValidator OPTIONAL_SECTIONS = ["Retired tests", "Approach"].freeze def build_checker(body, labels) end end class TicketBodyChecker RETIRED_TEST_LINE = /\A\S+#\S+\z/ def initialize(body:, tracker:, required_depth:, sections:) end def check_headings end def present_optional_sections end def check_retired_test_lines(match) end end end ``` ## Cases 1. Given a body without `## Retired tests` or `## Approach`, when validated, then it passes unchanged. 2. Given `## Retired tests` present with valid `path#test_method` lines, when validated, then it passes. 3. Given `## Retired tests` with a line not matching `path#test_method`, when validated, then it reports an error naming the line. 4. Given `## Retired tests` present but empty, when validated, then it reports an empty section. 5. Given `## Approach` present and non-empty, when validated, then it passes; given `## Approach` present and empty, then it reports an empty section. 6. Given an agent-ready ticket without `## Retired tests` or `## Approach`, when validated, then it passes. 7. Given `capture.md`, when read, then it documents `## Approach` as optional with its content rule. ## Approach Rejected: adding `Retired tests` to `AGENT_READY_SECTIONS`. Reason: that list means required-and-non-empty for `ready-for-agent`; it would reject every agent-ready ticket with no tests to retire. Chosen: a third `OPTIONAL_SECTIONS` list, checked only when the heading is present. Depends on: `HeadingsHolder#select_by_name` already returning `[]` for an absent heading, so "present" is a plain emptiness check. `## Approach` motivation: design intent today has only `## Owner sketch` (code shape) and `## Cases` (one-line items; prose there caused an escalation, #593), so approach prose has no home. #611 will be the first ticket to use it. ## Origin - Trigger: review of the #610 final summary, 2026-09-18, user asked for the ticket. - Improvised this session: none. - Chain: malformed Retired tests passes capture ← validator visits listed headings only ← no optional-sections concept ← `DESIGN` (`plugins/os-backlog/lib/backlog/template_validator.rb`). - Root candidate: this ticket. - Where: `plugins/os-backlog/lib/backlog/template_validator.rb`, `plugins/os-backlog/lib/backlog/ticket_body_checker.rb`. - Session: edb23374-6b44-4410-b666-a485a1923501 - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/edb23374-6b44-4410-b666-a485a1923501.jsonl - Related: #610, #611 </content>
Author
Owner

Work started on branch ticket-612 via the os-sdlc pipeline (worktree .claude/worktrees/ticket-612). Also adds plugins/os-backlog/.sdlc/project.yaml, the plugin's first pipeline config.

Work started on branch ticket-612 via the os-sdlc pipeline (worktree .claude/worktrees/ticket-612). Also adds plugins/os-backlog/.sdlc/project.yaml, the plugin's first pipeline config.
Author
Owner

Implementation 1 escalated at test-writer with unexpected_pass:test-writer/diff-test. Cause: environment defect. The new .sdlc/project.yaml set no test_path, the Runner default is test/, this plugin uses tests/, so the diff-test gate selected no files and auto-passed (exit 0, empty output, 0 ms on every attempt). The written tests are red for the right reason when run directly. Fix: test_path: tests added. Re-opening as implementation 2 with the red-round files kept on disk.

Implementation 1 escalated at test-writer with unexpected_pass:test-writer/diff-test. Cause: environment defect. The new .sdlc/project.yaml set no test_path, the Runner default is test/, this plugin uses tests/, so the diff-test gate selected no files and auto-passed (exit 0, empty output, 0 ms on every attempt). The written tests are red for the right reason when run directly. Fix: test_path: tests added. Re-opening as implementation 2 with the red-round files kept on disk.
Author
Owner

Resolution

Done: OPTIONAL_SECTIONS added to TemplateValidator; TicketBodyChecker checks Retired tests and Approach only when present (depth, unique, non-empty); Retired tests lines must be a bare path or path#test_method; capture.md documents Approach. Also adds plugins/os-backlog/.sdlc/project.yaml (first pipeline config for this plugin).

Evidence: Branch ticket-612 commit ffea5110. os-backlog suite 234 runs, 0 failures. rubocop clean on the 5 changed Ruby files. validate-template 611 and 612 exit 0 (both carry ## Approach). Pipeline impl 2 reached bound_exhausted at programmer-repair because the lint gate doubled the plugin path (gate defect, not code); diff-test was green every round; hand-finished after reading the diff.

Follow-ups: #615 (P1, ready-for-agent): Git::Repo#changed_paths mixes repo-relative and cwd-relative paths. #616 (P2): test_path default test/ vs tests/ undetected. Both carry skeptic CREATE verdicts.

## Resolution **Done:** OPTIONAL_SECTIONS added to TemplateValidator; TicketBodyChecker checks Retired tests and Approach only when present (depth, unique, non-empty); Retired tests lines must be a bare path or path#test_method; capture.md documents Approach. Also adds plugins/os-backlog/.sdlc/project.yaml (first pipeline config for this plugin). **Evidence:** Branch ticket-612 commit ffea5110. os-backlog suite 234 runs, 0 failures. rubocop clean on the 5 changed Ruby files. validate-template 611 and 612 exit 0 (both carry ## Approach). Pipeline impl 2 reached bound_exhausted at programmer-repair because the lint gate doubled the plugin path (gate defect, not code); diff-test was green every round; hand-finished after reading the diff. **Follow-ups:** #615 (P1, ready-for-agent): Git::Repo#changed_paths mixes repo-relative and cwd-relative paths. #616 (P2): test_path default test/ vs tests/ undetected. Both carry skeptic CREATE verdicts.
jared closed this issue 2026-09-18 14:19:36 +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#612
No description provided.