Fix aidd-lint short-circuit that hides offenses after the first failing file #984

Closed
opened 2026-10-10 14:22:40 +00:00 by jared · 2 comments
Owner

Problem

aidd-lint prints offenses for the first failing file only and hides the offenses of every later file.
The line offenses_found ||= emit_offenses(path, offenses) short-circuits: once offenses_found is true, Ruby does not call emit_offenses again.
A full-tree run therefore under-reports.
This defect also masks the symptom of #194 (tmp/ scanning): the tmp/ offense does not print when an earlier file already failed.

Design

/home/jared/dev/cc-os/.claude/worktrees/prism-architect/docs/takedown/984-20261012-094346/final-design.json

Approach

Replace the ||= accumulators in AiddLint::CLI#lint and #tier2_collect with a per-file map that always emits, then reduce with any? for the exit status.

Required work:

  1. Write plugins/os-aidd-lint/lib/aidd_lint/cli.rb
  2. Write test plugins/os-aidd-lint/tests/multi_file_report_test.rb

Acceptance criteria

  • Two failing files: both print, exit 1
  • One failing, one clean: the failing one prints, exit 1
  • Only advisory offenses: all print, exit 0
  • Unknown lint-style: prints, counts as failing, other files still processed
  • Missing manifest: exit 3, unchanged

Owner sketch

Responsibility Owner
Report one lint file and say whether it fails CLI#lint_file
Report one collected subject and say whether it fails CLI#collect_subject
Print offenses, return failing? CLI#emit_offenses (unchanged)
Exit status from the results CLI#lint, CLI#tier2_collect (any?)

plugins/os-aidd-lint/tests/multi_file_report_test.rb

# frozen_string_literal: true

require_relative 'test_helper'
require 'stringio'
require 'tmpdir'
require 'fileutils'

class MultiFileReportTest < Minitest::Test # rubocop:disable Style/Documentation
  BAD_SKILL = <<~MD
    ---
    name: mine-lints
    description: does a thing.
    when_to_use: Use when the user asks for it.
    ---
  MD

  def test_lint_prints_offenses_of_every_failing_file
    Dir.mktmpdir('aidd-lint-multi') do |dir|
      first, second = bad_skills(dir)

      out, _err, status = run_cli(first, second)

      assert_equal 1, status
      assert_match(/#{Regexp.escape(first)}:2: skill-frontmatter-name:/, out)
      assert_match(/#{Regexp.escape(second)}:2: skill-frontmatter-name:/, out)
    end
  end

  def test_collect_prints_tier1_offenses_of_every_failing_subject # rubocop:disable Metrics/MethodLength
    Dir.mktmpdir('aidd-lint-multi') do |dir|
      first, second = bad_skills(dir)

      prepare_out, prepare_err, prepare_status = run_cli('--tier2-prepare', first, second)
      assert_equal 0, prepare_status, prepare_err
      workdir = prepare_out[/^workdir (.+)$/, 1]

      begin
        out, _err, status = run_cli('--tier2-collect', '--tier2-workdir', workdir)

        assert_equal 1, status
        assert_match(/#{Regexp.escape(first)}:2: skill-frontmatter-name:/, out)
        assert_match(/#{Regexp.escape(second)}:2: skill-frontmatter-name:/, out)
      ensure
        FileUtils.rm_rf(workdir)
      end
    end
  end

  private

  def bad_skills(dir)
    %w[a b].map do |name|
      FileUtils.mkdir_p(File.join(dir, name))
      File.join(dir, name, 'SKILL.md').tap { |path| File.write(path, BAD_SKILL) }
    end
  end

  def run_cli(*args)
    stdout = StringIO.new
    stderr = StringIO.new
    invocation = AiddLint::Invocation.new(
      cwd: Dir.pwd, stdin: StringIO.new, stdout: stdout, stderr: stderr, env: {}
    )
    status = AiddLint::CLI.new(invocation).run(args)
    [stdout.string, stderr.string, status]
  end
end
## Problem `aidd-lint` prints offenses for the first failing file only and hides the offenses of every later file. The line `offenses_found ||= emit_offenses(path, offenses)` short-circuits: once `offenses_found` is true, Ruby does not call `emit_offenses` again. A full-tree run therefore under-reports. This defect also masks the symptom of #194 (tmp/ scanning): the tmp/ offense does not print when an earlier file already failed. ## Design /home/jared/dev/cc-os/.claude/worktrees/prism-architect/docs/takedown/984-20261012-094346/final-design.json ## Approach Replace the `||=` accumulators in AiddLint::CLI#lint and #tier2_collect with a per-file map that always emits, then reduce with any? for the exit status. Required work: 1. Write `plugins/os-aidd-lint/lib/aidd_lint/cli.rb` 2. Write test `plugins/os-aidd-lint/tests/multi_file_report_test.rb` ## Acceptance criteria - Two failing files: both print, exit 1 - One failing, one clean: the failing one prints, exit 1 - Only advisory offenses: all print, exit 0 - Unknown lint-style: prints, counts as failing, other files still processed - Missing manifest: exit 3, unchanged ## Owner sketch | Responsibility | Owner | |---|---| | Report one lint file and say whether it fails | CLI#lint_file | | Report one collected subject and say whether it fails | CLI#collect_subject | | Print offenses, return failing? | CLI#emit_offenses (unchanged) | | Exit status from the results | CLI#lint, CLI#tier2_collect (`any?`) | `plugins/os-aidd-lint/tests/multi_file_report_test.rb` ```ruby # frozen_string_literal: true require_relative 'test_helper' require 'stringio' require 'tmpdir' require 'fileutils' class MultiFileReportTest < Minitest::Test # rubocop:disable Style/Documentation BAD_SKILL = <<~MD --- name: mine-lints description: does a thing. when_to_use: Use when the user asks for it. --- MD def test_lint_prints_offenses_of_every_failing_file Dir.mktmpdir('aidd-lint-multi') do |dir| first, second = bad_skills(dir) out, _err, status = run_cli(first, second) assert_equal 1, status assert_match(/#{Regexp.escape(first)}:2: skill-frontmatter-name:/, out) assert_match(/#{Regexp.escape(second)}:2: skill-frontmatter-name:/, out) end end def test_collect_prints_tier1_offenses_of_every_failing_subject # rubocop:disable Metrics/MethodLength Dir.mktmpdir('aidd-lint-multi') do |dir| first, second = bad_skills(dir) prepare_out, prepare_err, prepare_status = run_cli('--tier2-prepare', first, second) assert_equal 0, prepare_status, prepare_err workdir = prepare_out[/^workdir (.+)$/, 1] begin out, _err, status = run_cli('--tier2-collect', '--tier2-workdir', workdir) assert_equal 1, status assert_match(/#{Regexp.escape(first)}:2: skill-frontmatter-name:/, out) assert_match(/#{Regexp.escape(second)}:2: skill-frontmatter-name:/, out) ensure FileUtils.rm_rf(workdir) end end end private def bad_skills(dir) %w[a b].map do |name| FileUtils.mkdir_p(File.join(dir, name)) File.join(dir, name, 'SKILL.md').tap { |path| File.write(path, BAD_SKILL) } end end def run_cli(*args) stdout = StringIO.new stderr = StringIO.new invocation = AiddLint::Invocation.new( cwd: Dir.pwd, stdin: StringIO.new, stdout: stdout, stderr: stderr, env: {} ) status = AiddLint::CLI.new(invocation).run(args) [stdout.string, stderr.string, status] end end ```
Author
Owner

Implementation started on branch prism-architect (persistent worktree) via /os-sdlc:implement 984 takedown-implement. Design: docs/takedown/984-20261012-094346/final-design.json. Session 3f094e92-202f-41fc-8d9c-b591750d9bc1.

Implementation started on branch prism-architect (persistent worktree) via /os-sdlc:implement 984 takedown-implement. Design: docs/takedown/984-20261012-094346/final-design.json. Session 3f094e92-202f-41fc-8d9c-b591750d9bc1.
Author
Owner

Resolution

Done: aidd-lint now prints offenses for every failing file: CLI#lint and #tier2_collect build per-file results that always emit, then reduce with any? for the exit status. Exit codes unchanged.

Evidence: Takedown docs/takedown/984-20261012-094346 (relevance relevant, both new methods red at base); implementation 37 on map takedown-implement: test-writer, programmer, behavior-verifier, suite-check, contract-auditor all pass first try; Runner commit dcf66e4d on prism-architect.

Follow-ups: none; #194 (tmp/ scan) stays open and is now unmasked by this fix

## Resolution **Done:** aidd-lint now prints offenses for every failing file: CLI#lint and #tier2_collect build per-file results that always emit, then reduce with any? for the exit status. Exit codes unchanged. **Evidence:** Takedown docs/takedown/984-20261012-094346 (relevance relevant, both new methods red at base); implementation 37 on map takedown-implement: test-writer, programmer, behavior-verifier, suite-check, contract-auditor all pass first try; Runner commit dcf66e4d on prism-architect. **Follow-ups:** none; #194 (tmp/ scan) stays open and is now unmasked by this fix
jared closed this issue 2026-10-12 13:49:43 +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#984
No description provided.