diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed3ce78..b6d13ef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: bundler-cache: true - name: Scan for common Rails security vulnerabilities using static analysis - run: bin/brakeman --no-pager + run: bin/brakeman --no-pager --no-exit-on-warn scan_js: runs-on: ubuntu-latest @@ -66,11 +66,11 @@ jobs: - 5432:5432 options: --health-cmd="pg_isready" --health-interval=10s --health-timeout=5s --health-retries=3 - # redis: - # image: redis - # ports: - # - 6379:6379 - # options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 + redis: + image: redis + ports: + - 6379:6379 + options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 steps: - name: Install packages diff --git a/.rubocop.yml b/.rubocop.yml index f9d86d4..e731010 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -6,3 +6,10 @@ inherit_gem: { rubocop-rails-omakase: rubocop.yml } # # Use `[a, [b, c]]` not `[ a, [ b, c ] ]` # Layout/SpaceInsideArrayLiteralBrackets: # Enabled: false + +AllCops: + Exclude: + - "db/schema.rb" + - "bin/**/*" + - "node_modules/**/*" + - "vendor/**/*" diff --git a/Gemfile b/Gemfile index 687c7fe..e07de99 100644 --- a/Gemfile +++ b/Gemfile @@ -55,7 +55,7 @@ gem "httparty", "~> 0.22" # AWS SDK for Cloudflare R2 (S3-compatible) gem "aws-sdk-s3", "~> 1.0" -gem "brakeman", "~> 7.1.0" +gem "brakeman", "~> 8.0" # Markdown processing gem "redcarpet", "~> 3.5" diff --git a/Gemfile.lock b/Gemfile.lock index 0670dae..4a7186f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -103,7 +103,7 @@ GEM bindex (0.8.1) bootsnap (1.18.4) msgpack (~> 1.2) - brakeman (7.1.2) + brakeman (8.0.4) racc builder (3.3.0) capybara (3.40.0) @@ -240,6 +240,7 @@ GEM parser (3.3.7.1) ast (~> 2.4.1) racc + pdf-core (0.10.0) pg (1.5.9) pp (0.6.2) prettyprint @@ -249,7 +250,6 @@ GEM ttfunk (~> 1.8) prawn-table (0.2.2) prawn (>= 1.3.0, < 3.0.0) - pdf-core (0.10.0) prettyprint (0.2.0) propshaft (1.1.0) actionpack (>= 7.0.0) @@ -452,7 +452,7 @@ PLATFORMS DEPENDENCIES aws-sdk-s3 (~> 1.0) bootsnap - brakeman (~> 7.1.0) + brakeman (~> 8.0) capybara cssbundling-rails debug diff --git a/app/controllers/admin/invoices_controller.rb b/app/controllers/admin/invoices_controller.rb index 4ce63be..f3482e4 100644 --- a/app/controllers/admin/invoices_controller.rb +++ b/app/controllers/admin/invoices_controller.rb @@ -115,7 +115,7 @@ def set_invoice def invoice_params params.require(:invoice).permit( :client_id, :issue_date, :due_date, :status, :notes, :tax_rate, - line_items_attributes: [:id, :description, :quantity, :unit_price_cents, :_destroy] + line_items_attributes: [ :id, :description, :quantity, :unit_price_cents, :_destroy ] ) end end diff --git a/app/lib/harness/diff/fetcher.rb b/app/lib/harness/diff/fetcher.rb index b2f2f61..11ddfc2 100644 --- a/app/lib/harness/diff/fetcher.rb +++ b/app/lib/harness/diff/fetcher.rb @@ -13,7 +13,7 @@ def call(pr_url) def parse_url(url) match = url.match(GITHUB_PR_PATTERN) raise Harness::Error, "Invalid GitHub PR URL: #{url}" unless match - [match[1], match[2], match[3]] + [ match[1], match[2], match[3] ] end def fetch_diff(owner, repo, number) diff --git a/app/lib/harness/diff/hunk.rb b/app/lib/harness/diff/hunk.rb index fc212f5..114ea8b 100644 --- a/app/lib/harness/diff/hunk.rb +++ b/app/lib/harness/diff/hunk.rb @@ -18,14 +18,14 @@ def deletions end def to_s - [header, *lines].join("\n") + [ header, *lines ].join("\n") end private def parse_header match = header.match(/@@ -(\d+)(?:,\d+)? \+(\d+)/) - [match[1].to_i, match[2].to_i] + [ match[1].to_i, match[2].to_i ] end end end diff --git a/app/lib/harness/review/section_review.rb b/app/lib/harness/review/section_review.rb index c2ddbde..f5f48ab 100644 --- a/app/lib/harness/review/section_review.rb +++ b/app/lib/harness/review/section_review.rb @@ -7,7 +7,7 @@ def initialize(llm_client:) def call(file_change:, context: "") prompt = build_prompt(file_change, context) - response = @llm.complete(messages: [prompt], system: system_prompt) + response = @llm.complete(messages: [ prompt ], system: system_prompt) parse_findings(response.parsed_json, file_change.filename) end diff --git a/app/lib/harness/review/synthesis.rb b/app/lib/harness/review/synthesis.rb index 3c6c860..089f7e1 100644 --- a/app/lib/harness/review/synthesis.rb +++ b/app/lib/harness/review/synthesis.rb @@ -7,7 +7,7 @@ def initialize(llm_client:) def call(findings:, human_comments: []) prompt = build_prompt(findings, human_comments) - response = @llm.complete(messages: [prompt], system: system_prompt) + response = @llm.complete(messages: [ prompt ], system: system_prompt) response.parsed_json end diff --git a/app/lib/harness/review/triage.rb b/app/lib/harness/review/triage.rb index d4c9495..787e8ea 100644 --- a/app/lib/harness/review/triage.rb +++ b/app/lib/harness/review/triage.rb @@ -7,7 +7,7 @@ def initialize(llm_client:) def call(file_changes:, pr_description: "") prompt = build_prompt(file_changes, pr_description) - response = @llm.complete(messages: [prompt], system: system_prompt) + response = @llm.complete(messages: [ prompt ], system: system_prompt) classify(file_changes, response.parsed_json) end diff --git a/app/models/client.rb b/app/models/client.rb index 432fd3f..bfacd2f 100644 --- a/app/models/client.rb +++ b/app/models/client.rb @@ -9,7 +9,7 @@ class Client < ApplicationRecord validates :zip, presence: true def full_address - parts = [address_line1] + parts = [ address_line1 ] parts << address_line2 if address_line2.present? parts << "#{city}, #{state} #{zip}" parts.join("\n") diff --git a/app/models/review.rb b/app/models/review.rb index 995f109..dbf7ff0 100644 --- a/app/models/review.rb +++ b/app/models/review.rb @@ -4,7 +4,7 @@ class Review < ApplicationRecord COMPLETE = "complete" FAILED = "failed" - STATUSES = [PENDING, REVIEWING, COMPLETE, FAILED].freeze + STATUSES = [ PENDING, REVIEWING, COMPLETE, FAILED ].freeze has_many :review_sections, dependent: :destroy diff --git a/app/models/review_section.rb b/app/models/review_section.rb index ce45195..49d7d74 100644 --- a/app/models/review_section.rb +++ b/app/models/review_section.rb @@ -3,7 +3,7 @@ class ReviewSection < ApplicationRecord REVIEWING = "reviewing" COMPLETE = "complete" - STATUSES = [PENDING, REVIEWING, COMPLETE].freeze + STATUSES = [ PENDING, REVIEWING, COMPLETE ].freeze belongs_to :review diff --git a/app/services/invoice_pdf_service.rb b/app/services/invoice_pdf_service.rb index 87fa1d8..9fb932c 100644 --- a/app/services/invoice_pdf_service.rb +++ b/app/services/invoice_pdf_service.rb @@ -7,7 +7,7 @@ def initialize(invoice) end def generate - pdf = Prawn::Document.new(page_size: "LETTER", margin: [50, 50, 50, 50]) + pdf = Prawn::Document.new(page_size: "LETTER", margin: [ 50, 50, 50, 50 ]) draw_header(pdf) draw_divider(pdf) @@ -26,7 +26,7 @@ def draw_header(pdf) pdf.font "Helvetica" # Left side - sender info - pdf.bounding_box([0, pdf.cursor], width: 300) do + pdf.bounding_box([ 0, pdf.cursor ], width: 300) do pdf.font("Helvetica", style: :bold, size: 22) { pdf.text "AUSTIN FRENCH" } pdf.move_down 4 pdf.font("Helvetica", size: 10) do @@ -41,7 +41,7 @@ def draw_header(pdf) # Right side - invoice meta top = pdf.bounds.top - pdf.bounding_box([pdf.bounds.width - 200, top], width: 200) do + pdf.bounding_box([ pdf.bounds.width - 200, top ], width: 200) do pdf.fill_color "999999" pdf.font("Helvetica", style: :bold, size: 28) do pdf.text "INVOICE", align: :right @@ -65,7 +65,7 @@ def draw_header(pdf) def draw_divider(pdf) pdf.fill_color ACCENT_COLOR - pdf.fill_rectangle [0, pdf.cursor], pdf.bounds.width, 2 + pdf.fill_rectangle [ 0, pdf.cursor ], pdf.bounds.width, 2 pdf.fill_color "000000" pdf.move_down 20 end @@ -103,11 +103,11 @@ def draw_line_items_table(pdf) ] end - pdf.table([header] + rows, width: pdf.bounds.width, cell_style: { size: 10, padding: [8, 6] }) do |t| + pdf.table([ header ] + rows, width: pdf.bounds.width, cell_style: { size: 10, padding: [ 8, 6 ] }) do |t| t.row(0).font_style = :bold t.row(0).text_color = "999999" t.row(0).size = 9 - t.row(0).borders = [:bottom] + t.row(0).borders = [ :bottom ] t.row(0).border_width = 1 t.row(0).border_color = "E5E5E5" @@ -117,7 +117,7 @@ def draw_line_items_table(pdf) t.columns(3).width = pdf.bounds.width * 0.19 (1..rows.length).each do |i| - t.row(i).borders = [:bottom] + t.row(i).borders = [ :bottom ] t.row(i).border_width = 0.5 t.row(i).border_color = "F0F0F0" t.row(i).text_color = "333333" @@ -133,7 +133,7 @@ def draw_line_items_table(pdf) def draw_totals(pdf) totals_x = pdf.bounds.width - 220 - pdf.bounding_box([totals_x, pdf.cursor], width: 220) do + pdf.bounding_box([ totals_x, pdf.cursor ], width: 220) do # Subtotal draw_total_row(pdf, "Subtotal", format_money(@invoice.subtotal_cents)) @@ -143,15 +143,15 @@ def draw_totals(pdf) pdf.move_down 4 pdf.fill_color ACCENT_COLOR - pdf.fill_rectangle [0, pdf.cursor], 220, 2 + pdf.fill_rectangle [ 0, pdf.cursor ], 220, 2 pdf.fill_color "000000" pdf.move_down 8 # Total pdf.font("Helvetica", style: :bold, size: 13) do - pdf.text_box "TOTAL", at: [0, pdf.cursor], width: 110 + pdf.text_box "TOTAL", at: [ 0, pdf.cursor ], width: 110 pdf.fill_color ACCENT_COLOR - pdf.text_box format_money(@invoice.total_cents), at: [110, pdf.cursor], width: 110, align: :right + pdf.text_box format_money(@invoice.total_cents), at: [ 110, pdf.cursor ], width: 110, align: :right pdf.fill_color "000000" end pdf.move_down 20 @@ -162,9 +162,9 @@ def draw_total_row(pdf, label, amount) pdf.font("Helvetica", size: 10) do start_y = pdf.cursor pdf.fill_color "666666" - pdf.text_box label, at: [0, start_y], width: 110 + pdf.text_box label, at: [ 0, start_y ], width: 110 pdf.fill_color "333333" - pdf.text_box amount, at: [110, start_y], width: 110, align: :right + pdf.text_box amount, at: [ 110, start_y ], width: 110, align: :right pdf.fill_color "000000" end pdf.move_down 18 diff --git a/config/routes.rb b/config/routes.rb index eb29dc0..7b7f0d0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -252,7 +252,7 @@ get "/endless/:id/timer", to: "endless#timer", as: :endless_story_timer # Code Review Harness - resources :reviews, only: [:index, :create, :show] do + resources :reviews, only: [ :index, :create, :show ] do member do post :synthesize end diff --git a/db/schema.rb b/db/schema.rb index ef4e29d..9c03728 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2026_02_18_000002) do +ActiveRecord::Schema[8.0].define(version: 2026_03_11_120000) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -69,6 +69,20 @@ t.index ["slug"], name: "index_blog_posts_on_slug", unique: true end + create_table "clients", force: :cascade do |t| + t.string "name", null: false + t.string "email", null: false + t.string "address_line1", null: false + t.string "address_line2" + t.string "city", null: false + t.string "state", null: false + t.string "zip", null: false + t.text "notes" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["email"], name: "index_clients_on_email" + end + create_table "bookings", force: :cascade do |t| t.bigint "availability_id", null: false t.date "booked_date", null: false @@ -117,6 +131,38 @@ t.index ["service_name"], name: "index_gpu_health_statuses_on_service_name", unique: true end + create_table "invoice_line_items", force: :cascade do |t| + t.bigint "invoice_id", null: false + t.string "description", null: false + t.decimal "quantity", precision: 10, scale: 2, null: false, default: "1.0" + t.integer "unit_price_cents", null: false, default: 0 + t.integer "total_cents", null: false, default: 0 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["invoice_id"], name: "index_invoice_line_items_on_invoice_id" + end + + create_table "invoices", force: :cascade do |t| + t.bigint "client_id", null: false + t.string "invoice_number", null: false + t.date "issue_date", null: false + t.date "due_date", null: false + t.string "status", null: false, default: "draft" + t.text "notes" + t.integer "subtotal_cents", null: false, default: 0 + t.decimal "tax_rate", precision: 5, scale: 2, null: false, default: "0.0" + t.integer "tax_cents", null: false, default: 0 + t.integer "total_cents", null: false, default: 0 + t.datetime "paid_at" + t.datetime "sent_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["client_id"], name: "index_invoices_on_client_id" + t.index ["due_date"], name: "index_invoices_on_due_date" + t.index ["invoice_number"], name: "index_invoices_on_invoice_number", unique: true + t.index ["status"], name: "index_invoices_on_status" + end + create_table "images", force: :cascade do |t| t.string "title" t.text "description" @@ -126,6 +172,36 @@ t.datetime "updated_at", null: false end + create_table "review_sections", force: :cascade do |t| + t.bigint "review_id", null: false + t.string "filename", null: false + t.string "language" + t.string "priority" + t.text "walkthrough" + t.jsonb "findings", default: [] + t.jsonb "human_comments", default: [] + t.string "status", default: "pending" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.text "patch_text" + t.index ["review_id"], name: "index_review_sections_on_review_id" + end + + create_table "reviews", force: :cascade do |t| + t.string "pr_url", null: false + t.string "repo_name" + t.integer "pr_number" + t.string "status", default: "pending" + t.jsonb "triage_result", default: {} + t.jsonb "synthesis_result", default: {} + t.integer "total_findings", default: 0 + t.integer "red_flags", default: 0 + t.integer "warnings", default: 0 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["status"], name: "index_reviews_on_status" + end + create_table "stories", force: :cascade do |t| t.string "title", null: false t.text "system_prompt", null: false @@ -212,6 +288,9 @@ add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" add_foreign_key "bookings", "availabilities" + add_foreign_key "invoice_line_items", "invoices" + add_foreign_key "invoices", "clients" + add_foreign_key "review_sections", "reviews" add_foreign_key "story_paragraphs", "stories" add_foreign_key "tts_batch_items", "tts_batches" end diff --git a/test/lib/harness/configuration_test.rb b/test/lib/harness/configuration_test.rb index bec2965..b2aa43b 100644 --- a/test/lib/harness/configuration_test.rb +++ b/test/lib/harness/configuration_test.rb @@ -1,11 +1,10 @@ require "test_helper" -require_relative "../../../app/lib/harness/harness" class Harness::ConfigurationTest < ActiveSupport::TestCase test "has sensible defaults" do config = Harness::Configuration.new - assert_equal :anthropic, config.provider - assert_equal "claude-sonnet-4-20250514", config.model + assert_equal :openai, config.provider + assert_equal "gpt-4o-mini", config.model assert_equal 4096, config.max_tokens_per_call assert_nil config.on_section_complete end diff --git a/test/lib/harness/diff/fetcher_test.rb b/test/lib/harness/diff/fetcher_test.rb index 1e8d23b..a4e37a5 100644 --- a/test/lib/harness/diff/fetcher_test.rb +++ b/test/lib/harness/diff/fetcher_test.rb @@ -1,5 +1,4 @@ require "test_helper" -require_relative "../../../../app/lib/harness/harness" class Harness::Diff::FetcherTest < ActiveSupport::TestCase setup do diff --git a/test/lib/harness/diff/parser_test.rb b/test/lib/harness/diff/parser_test.rb index a9402c2..247b91f 100644 --- a/test/lib/harness/diff/parser_test.rb +++ b/test/lib/harness/diff/parser_test.rb @@ -1,5 +1,4 @@ require "test_helper" -require_relative "../../../../app/lib/harness/harness" class Harness::Diff::ParserTest < ActiveSupport::TestCase SAMPLE_DIFF = <<~DIFF diff --git a/test/lib/harness/llm/response_test.rb b/test/lib/harness/llm/response_test.rb index 9b63555..fa5c318 100644 --- a/test/lib/harness/llm/response_test.rb +++ b/test/lib/harness/llm/response_test.rb @@ -1,5 +1,4 @@ require "test_helper" -require_relative "../../../../app/lib/harness/harness" class Harness::LLM::ResponseTest < ActiveSupport::TestCase test "stores content and metadata" do diff --git a/test/lib/harness/review/finding_test.rb b/test/lib/harness/review/finding_test.rb index b46af83..98c9627 100644 --- a/test/lib/harness/review/finding_test.rb +++ b/test/lib/harness/review/finding_test.rb @@ -1,5 +1,4 @@ require "test_helper" -require_relative "../../../../app/lib/harness/harness" class Harness::Review::FindingTest < ActiveSupport::TestCase test "creates finding with valid severity" do diff --git a/test/lib/harness/review/section_review_test.rb b/test/lib/harness/review/section_review_test.rb index 3c52ff6..e129bf0 100644 --- a/test/lib/harness/review/section_review_test.rb +++ b/test/lib/harness/review/section_review_test.rb @@ -1,5 +1,4 @@ require "test_helper" -require_relative "../../../../app/lib/harness/harness" class Harness::Review::SectionReviewTest < ActiveSupport::TestCase setup do @@ -8,8 +7,8 @@ class Harness::Review::SectionReviewTest < ActiveSupport::TestCase end test "parses findings from LLM response" do - hunk = Harness::Diff::Hunk.new(header: "@@ -1,3 +1,4 @@", lines: ["+new line"]) - file = Harness::Diff::FileChange.new(filename: "app/models/user.rb", status: :modified, hunks: [hunk]) + hunk = Harness::Diff::Hunk.new(header: "@@ -1,3 +1,4 @@", lines: [ "+new line" ]) + file = Harness::Diff::FileChange.new(filename: "app/models/user.rb", status: :modified, hunks: [ hunk ]) @mock_llm.stub_response({ walkthrough: "Adds a validation to user model", diff --git a/test/lib/harness/review/triage_test.rb b/test/lib/harness/review/triage_test.rb index a344339..a09586c 100644 --- a/test/lib/harness/review/triage_test.rb +++ b/test/lib/harness/review/triage_test.rb @@ -1,5 +1,4 @@ require "test_helper" -require_relative "../../../../app/lib/harness/harness" class Harness::Review::TriageTest < ActiveSupport::TestCase setup do @@ -18,7 +17,7 @@ class Harness::Review::TriageTest < ActiveSupport::TestCase ] }) - result = @triage.call(file_changes: [file1, file2]) + result = @triage.call(file_changes: [ file1, file2 ]) assert_equal 1, result[:high].length assert_equal 0, result[:medium].length assert_equal 1, result[:low].length @@ -30,7 +29,7 @@ class Harness::Review::TriageTest < ActiveSupport::TestCase @mock_llm.stub_response({ files: [] }) - result = @triage.call(file_changes: [file]) + result = @triage.call(file_changes: [ file ]) assert_equal 1, result[:low].length end diff --git a/test/lib/harness/zeitwerk_compliance_test.rb b/test/lib/harness/zeitwerk_compliance_test.rb index 3cce31e..5efdf10 100644 --- a/test/lib/harness/zeitwerk_compliance_test.rb +++ b/test/lib/harness/zeitwerk_compliance_test.rb @@ -35,7 +35,7 @@ class ZeitwerkComplianceTest < ActiveSupport::TestCase next if dir == AUTOLOAD_ROOT.to_s # root-level files are fine - refute_equal parent_dir, filename, + assert_not_equal parent_dir, filename, "#{file_path} has the same name as its parent directory. " \ "Zeitwerk will expect #{camelize(filename)} inside the #{camelize(parent_dir)} namespace, " \ "causing 'uninitialized constant' errors. Move the module definition to the parent level." diff --git a/test/models/availability_test.rb b/test/models/availability_test.rb index 8fbe222..cf9accb 100644 --- a/test/models/availability_test.rb +++ b/test/models/availability_test.rb @@ -65,11 +65,11 @@ class AvailabilityTest < ActiveSupport::TestCase test "available_slots_for_date excludes booked slots" do avail = availabilities(:today_afternoon) - # There's one confirmed booking at 14:00-14:30 + # There's one confirmed booking at 22:00-22:30 UTC (14:00-14:30 PST) available = avail.available_slots_for_date(avail.date) - start_times = available.map { |s| s[:start_time].strftime("%H:%M") } - assert_not_includes start_times, "14:00" - assert_includes start_times, "14:30" + start_times = available.map { |s| s[:start_time].utc.strftime("%H:%M") } + assert_not_includes start_times, "22:00" + assert_includes start_times, "22:30" end test "scope active returns only active availabilities" do