Skip to content

Feat/component sync merge#742

Draft
wdower wants to merge 599 commits into
masterfrom
feat/component-sync-merge
Draft

Feat/component sync merge#742
wdower wants to merge 599 commits into
masterfrom
feat/component-sync-merge

Conversation

@wdower

@wdower wdower commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

No description provided.

aaronlippold and others added 30 commits June 5, 2026 00:25
- 61 findings from 8-agent review integrated inline with markers
- Implementation sequence: expert review notes per commit
- Test sequence: 8 new test items added (27 total, was 19)
- File list: pre-Phase-1 blockers, design changes, new files
- Class designs: RuleFieldDiffer hash comparison (not AR Dirty)
- Open questions: #1 resolved by review, 7 new items added
- Effort: recalibrated to ~145 min Claude-pace (+25 min)
- Pickup instructions: blocker-first workflow, key changes

Authored by: Aaron Lippold<lippold@gmail.com>
The backup serializer mixed second and microsecond precision within
one file: review.updated_at and reply.created_at were already
iso8601(6), but the other 8 timestamps (4 Component, 3 Rule, 2 Review:
triage_set_at, adjudicated_at, created_at) emitted iso8601 (second).

Two consequences:

1. Round-trip data loss. base_rules.created_at is a timestamp(6)
   column. Exporting at second precision rounds 10:23:45.123456 to
   10:23:45 — re-importing then loses the microsecond fraction
   permanently. Two cycles, same result.

2. ReviewMatcher composite-key correctness. The merge engine
   (v2-480.1) keys on (rule_id, created_at, comment_digest). Two
   reviews posted within the same second on different instances
   would collide on the first two key components. Microsecond
   distinguishes them.

The policy embodied by this change: any timestamp that flows through
backup serialize → archive zip → JsonArchiveImporter must preserve
full microsecond precision. CSV exports (disposition_matrix_export),
the API serializer (review.rb#serializable_hash), audit fields,
session metadata, and XCCDF dates are deliberately NOT touched — they
are presentation / event surfaces with intentional second-precision
contracts.

Adds a top-level describe block in backup_serializer_spec asserting
the precision contract on every round-tripped timestamp. The existing
'includes timestamps as ISO8601 strings' assertion is updated to the
new contract. Disposition matrix CSV export specs and JsonArchive
importer specs stay green (121 examples, 0 failures across both).

Refs: v2-480.12 (PRE-PHASE-1 BLOCKER for v2-480.1), expert review
finding F28 / F55.

Signed-off-by: Will Dower <will@dower.dev>
rule_satisfactions was created in 20211103190520 as a HABTM join table
with no DB-level foreign key constraints on either rule_id or
satisfied_by_rule_id. Both reference base_rules.id. Without FKs, a
satisfaction row pointing at a non-existent rule on either side persists
as a dangling bigint — silently mis-routed during component sync merge
(expert review finding F2 / PRE-PHASE-1 BLOCKER for v2-480.1).

Applies the codebase's Strong Migrations 2-pass pattern (mirrors
20260502150000 / 20260502150001 for reviews.rule_id):

  Pass 1 (20260605142102): add_foreign_key with validate: false on both
  columns. Constraint exists for new writes; existing rows not scanned
  under ACCESS EXCLUSIVE.

  Pass 2 (20260605142103): disable_ddl_transaction!, delete orphan rows
  (any satisfaction whose rule_id or satisfied_by_rule_id references a
  non-existent base_rule), then validate_foreign_key. Validation does
  not hold the long lock; orphan cleanup is defensive against
  non-Rails operations (manual SQL, prior bug) — no orphans should
  exist on a clean instance because Rule's HABTM cleanup runs as part
  of dependent: :destroy.

on_delete: :cascade (different from reviews — vulcan-cascade-rails-owns
memory does NOT apply here): rule_satisfactions has no Rails callbacks
to preserve (the model is an empty RuleSatisfaction < ApplicationRecord
stub), no audited gem hooks, and the relation is purely binary — when
either parent rule disappears, the row is meaningless and should be
cleaned up at the DB level too. Belt and suspenders alongside HABTM's
own dependent cleanup.

spec/migrations/rule_satisfactions_fk_two_pass_spec.rb encodes the
end-state invariant: both FKs exist, target base_rules, use :cascade,
and are validated in Postgres (convalidated = true). 8/8 green.
Adjacent FK regression spec (responding_to_fk_two_pass) and the
rule_satisfactions request spec stay green (14/14).

Refs: v2-480.13 (PRE-PHASE-1 BLOCKER for v2-480.1), expert review
finding F2.

Signed-off-by: Will Dower <will@dower.dev>
- UserBlueprint :admin view replaces USER_JSON_FIELDS constant + all .as_json
  calls in UsersController (5 sites) and ApplicationController locked_users
- UserBlueprint :profile view for Devise registration pages (6 fields vs 10)
- ProjectBlueprint replaces @project.to_json in rules/index.html.haml
- users/index.html.haml uses UserBlueprint :admin instead of .as_json
- OpenAPI UserSummary schema updated (Blueprinter timestamp format)
- Over-exposure eliminated: uid, unique_session_id, admin flag removed from
  profile page DOM; created_at, updated_at removed from all HAML injections

Authored by: Aaron Lippold<lippold@gmail.com>
- POST /rules/:rule_id/comments route removed (CommentsController doesn't exist)
- 6 Jbuilder templates deleted (components, stigs, srgs index/show) — all
  controllers use Blueprint render body: for JSON, Jbuilder never reached
- Routing spec added to verify dead route is gone

Authored by: Aaron Lippold<lippold@gmail.com>
- Unlink: Devise valid_for_authentication? for lockout (3 attempts → lock),
  resets failed_attempts on successful unlink
- POST /users/initiate_link: session-flag flow for linking external provider
  to local account. OmniAuth callback detects link_in_progress + current_user
  and attaches identity. Edge case: identity already linked → error.
- Settings.respond_to? guard for unconfigured providers (github)
- 8 ADNM rake task tests (dry run, live run, idempotency, filters)
- 3 lockout tests, 5 initiate_link tests

Authored by: Aaron Lippold<lippold@gmail.com>
- Turbolinks gem + npm packages removed
- All 24 packs: turbolinks:load → DOMContentLoaded
- vue-turbolinks adapter removed from createVulcanApp + inline packs
- data-turbolinks attributes removed from 5 Vue components + 2 HAML
- vue-router@3.6.5 installed (hash mode, Vue 2.7 compatible)
- createVulcanApp accepts optional router param
- 3 router configs: componentEditor, triagePage, defaultRoutes
- rules.js, component_triage.js, project_triage.js → createVulcanApp

Authored by: Aaron Lippold<lippold@gmail.com>
- Pinia Setup Store replaces useRuleSelection composable
- store.selectRule() calls router.push (hash mode)
- 8 components migrated: RuleNavigator, RuleSatisfactions,
  NewRuleModalForm, SatisfiedByIndicator, RuleEditorHeader,
  ControlsSidepanels, RulesCodeEditorView, ProjectComponent
- Zero @ruleSelected event emissions remain in editor
- selectedRuleId/openRuleIds prop drilling removed
- SatisfiedByIndicator "Go to parent" wired to store
- routerTestHelper: abstract mode for jsdom compatibility
- getFirstVisibleRule extracted to utils/ruleSelectionUtils.js
- useRuleSelection.js + SelectedRulesMixin.vue deleted

Authored by: Aaron Lippold<lippold@gmail.com>
Authored by: Aaron Lippold<lippold@gmail.com>
- Additive filter model (all-unchecked = show all)
- Guard clause in useRuleFilters for all-unchecked case
- Count indicator "All Rules (X of Y)" + hasActiveFilters
- ActiveFilterPills with dismiss + clear all
- Open Comments Only moved to FilterBar Display section
- Status filters reordered to lifecycle (NYD first)
- Fixed clearFilters mutation bug
- Changed "reset" to "clear" label

Authored by: Aaron Lippold<lippold@gmail.com>
- RuleNavigator 906→293 lines (thin orchestrator)
- Created RuleSearchBar (search + modal + clear)
- Created RuleList (open rules + all rules + nesting)
- Created RuleRowIcons (DRY icon pattern, fixed broken <i>)
- Created AlsoSatisfiesModal (from RulesCodeEditorView)
- Extracted searchHighlight.js utility
- Removed dead ruleStatusCounts computed
- Removed setInterval localStorage polling hack

Authored by: Aaron Lippold<lippold@gmail.com>
- body: vh-100 d-flex flex-column overflow-hidden
- container-fluid: flex-grow-1 overflow-auto (list pages)
- .vulcan-editor-layout utility class in design system
- PanelLayout: b-card no-body wrapper (border + radius)
- ControlsPageLayout: header/body slot pass-through
- HAML wrappers: .vulcan-editor-layout on editor pages
- Empty state: icon + centered text

Authored by: Aaron Lippold<lippold@gmail.com>
- useRuleNavigation composable (filter/search pipeline)
- RulesCodeEditorView: header/body slot split for sidebar
- ProjectComponent: same header/body slot split
- RuleEditor: toolbar flex-shrink-0, content overflow-auto
- Flattened nav composable returns for Vue 2 auto-unwrap

Authored by: Aaron Lippold<lippold@gmail.com>
Replaces the full-width CommentPeriodBanner with a compact button
chip for the command bar. Shows phase status (open/closed), days
remaining, and pending comment count badge. Clickable to open
comments panel. 11 tests.

Authored by: Aaron Lippold<lippold@gmail.com>
Move breadcrumbs from standalone row into command bar. Remove
CommentPeriodBanner from page layout (replaced by CommentStatusChip).
Make filter bar toggleable with localStorage persistence. Add
activeFilterCount to useRuleFilters composable (individual filter
count, not categories). Wire both ProjectComponent and
RulesCodeEditorView. BaseCommandBar gains #above slot.

Authored by: Aaron Lippold<lippold@gmail.com>
Reorganize ControlsCommandBar into 3 semantic zones separated by
border dividers: actions (Edit, Comment), status (CommentStatusChip),
view controls (Filters toggle in panel row). Rare actions (Download,
Upload, Release) move to overflow dropdown with tooltip. Add Clear
Filters button when filters active. Add discoverability tooltips to
all command bar buttons via v-b-tooltip.hover.

Authored by: Aaron Lippold<lippold@gmail.com>
Add v-b-tooltip.hover to all INFO and ACTIONS row buttons:
Related, Satisfies, History, Comment History, Comment, DISA Guide,
Change Review Status, Clone, Delete. Comment button upgraded from
native title to BootstrapVue tooltip for consistent behavior. 9 tests.

Authored by: Aaron Lippold<lippold@gmail.com>
Add buttonTooltip prop to CommentModal for caller-provided tooltip
text. Wire Save, Lock, Unlock tooltips in RuleActionsToolbar. Add
v-b-tooltip.hover to all INFO/ACTIONS buttons (Related, Satisfies,
History, Comment History, DISA Guide, Change Review Status, Clone,
Delete). Change Lock variant from outline-dark to outline-secondary
for better visibility in dark mode.

Authored by: Aaron Lippold<lippold@gmail.com>
Rules.vue had a plain <div> root element that broke the CSS flexbox
chain. Per spec §4.5, overflow:hidden resets min-height:auto to 0
at each chain link — one missing link and all downstream scroll
containers expand to content height. Both sidebar and content area
lost scrolling in edit mode.

Add regression tests on all 3 chain components (Rules.vue,
ProjectComponent.vue, ControlsPageLayout.vue). Update SCSS docs
to mark the chain as MANDATORY.

Authored by: Aaron Lippold<lippold@gmail.com>
Consolidate 6 individual .btn-outline-* blocks and 5 individual
.badge-* blocks into ONE @each loop using $dark-variants map.
Formulas match Bootstrap 5.3: tint-color(40%) for text, shade-color
(60%) for bg. Add regression tests in design_system_audit_spec.rb
that fail if individual variant blocks are reintroduced. Document
the pattern in design-system.md. Update SCSS chain docs.

Authored by: Aaron Lippold<lippold@gmail.com>
Authored by: Aaron Lippold<lippold@gmail.com>
- Remove @ivar aliases from reviews_model_base and components_model_base;
  specs now use let_it_be names directly (reviews_*, components_*)
- Remove dead shared_p2 from reviews_model_base (zero consumers)
- Membership.create → create! to surface silent setup failures
- Project.create → create(:project) for FactoryBot consistency
- Rule.create → create! for explicit error surfacing
- Prefix let_it_be names to prevent collision: reviews_srg vs components_srg
- Rename context 'reviews base setup' → 'reviews request base setup'
- Promote let(:rule) → let_it_be(:rule) in request context
- Keep anchor_admin in request context — absorbs first-user-admin promotion
  (Settings.admin_bootstrap.first_user_admin defaults true), preventing test
  users from accidentally becoming admin and bypassing role checks

399 examples, 0 failures across 29 affected spec files

Authored by: Aaron Lippold<lippold@gmail.com>
- components_counts_spec: Audited.auditing_enabled = false (global)
  replaced with Component.without_auditing { Rule.without_auditing { } }
  (per-model scoped, ensure-based cleanup)
- users.rb factory: before/after callback split replaced with to_create
  wrapping save! in User.without_auditing { } — ensure-based cleanup
  even if save! raises (fragile split pattern could leave auditing
  disabled on factory create failure)

Zero Audited.auditing_enabled mutations remain in spec/

Authored by: Aaron Lippold<lippold@gmail.com>
SatisfiedByBlueprint includes component_prefix (so frontend renders
"Satisfied by PREFIX-RULE_ID" without extra lookups). Test expectation
was written before that field was added.

Authored by: Aaron Lippold<lippold@gmail.com>
- Removed include_context 'components model base setup' (604KB SRG parse
  + ~250 rule import per test group for zero benefit)
- Used :skip_rules trait on create(:component) since comment_phase tests
  only exercise attribute validations, not rules
- 17.43s → 12.58s (28% faster)

Authored by: Aaron Lippold<lippold@gmail.com>
Both specs test ComponentsController actions (POST /components/:id/lock,
PATCH /components/:component_id/lock_sections), not ReviewsController.
Neither included the reviews shared context.

Authored by: Aaron Lippold<lippold@gmail.com>
- :closed_comment_phase trait on components factory (Analyzer precondition)
- JsonArchiveArchiveBuilder helper produces backup-shape hashes from factory
  records (no checked-in zip fixtures)
- Auto-included in spec/services/import/{json_archive/,}merge/ + sync rake specs

Refs v2-480.1 commit 1 of 11 in
docs/superpowers/plans/2026-06-04-v2-480.1-implementation-plan.md.

Signed-off-by: Will Dower <will@dower.dev>
Pure-Ruby matcher: groups two sets of review hashes by
(rule_id, created_at, comment_digest) and partitions matched / only_ours /
only_theirs. Degenerate same-key groups tiebreak by external_id position
and are recorded in a separate collisions array for MergePlan warnings.

- digest: SHA-256[0..15] of NFC-normalized comment text (64 bits)
- v1.0 manifest fallback truncates created_at to second precision for
  legacy archives that pre-date the iso8601(6) BackupSerializer fix
- No composite index migration this commit — Phase 1 matcher is in-memory;
  index would only pay off for the Phase 2 applier (per expert review F21)

Refs commit 2 of 11 in
docs/superpowers/plans/2026-06-04-v2-480.1-implementation-plan.md.

Signed-off-by: Will Dower <will@dower.dev>
Class (not Struct) per expert review F19/F20 — construction validates
format and shape so bad inputs fail at the boundary.

- .from_json_archive pins to the single-component slice and raises
  ArgumentError when component is missing (F20: stop the silent-drop of
  satisfactions.json on flat archives)
- .from_spreadsheet applies SpreadsheetParser.normalize_header_aliases so
  DISA-format headers map to import headers (F18)
- .from_zip_path detects flat vs nested layout; multi-component archives
  must pass component_dir: to disambiguate
- nil arrays coerced to empty; symbol-keyed hashes deep_stringify'd

Refs commit 3 of 11 in
docs/superpowers/plans/2026-06-04-v2-480.1-implementation-plan.md.

Signed-off-by: Will Dower <will@dower.dev>
aaronlippold and others added 28 commits June 14, 2026 20:13
Replace hardcoded :oidc with Devise.omniauth_providers.first in
initiate_link, session_auth_method, and oidc_login_buttons specs.
Update oidc_discovery_spec for renamed fetch_oidc_logout_endpoint_for
method + providers mock. Replace connect_identity_spec with model-layer
tests (OmniAuth session persistence is a test-infra limitation).

Authored by: Aaron Lippold<lippold@gmail.com>
- UnifiedRuleForm: status hint → b-alert above form
- RuleBlueprint :editor: added missing histories field
- OpenAPI RuleEditorResponse updated (39 fields)
- Contract tests updated

Authored by: Aaron Lippold<lippold@gmail.com>
- "Activity" → "Changelog" (project + component levels)
- "History" → "Rule Changelog" (via RULE_TERM)
- "Comment History" → "Rule Discussion" (via RULE_TERM)
- "Revisions" → "Version Comparison" (project level)
- All labels use RULE_TERM/COMPONENT_TERM for customization
- Updated tooltips, sidebar titles, button labels
- 11 test files updated to match

Authored by: Aaron Lippold<lippold@gmail.com>
- CommentRowBlueprint: srg_info object (title, version, is_latest)
- parent_rule_displayed_name added to :project + :user views
- SecurityRequirementsGuide.srg_info_for_components helper
- All 3 consumers wired (CommentQueryService, Project, Users)
- Triage sidebar + comment table show SRG name + version
- OpenAPI CommentRow + UserCommentRow schemas updated

Authored by: Aaron Lippold<lippold@gmail.com>
- VuePropsHelper centralizes 3 shared HAML props (DRY)
- 3 HAML files refactored with **splat pattern
- docs/development/haml-serialization.md documents standard
- VitePress sidebar entry added
- Zero hand-built AR model hashes remain in HAML

Authored by: Aaron Lippold<lippold@gmail.com>
- VersionSortable: latest? + latest_for_family methods
- SRG/STIG/Component Blueprints: is_latest + latest fields
- VersionCurrencyDot.vue shared component (green/yellow)
- BenchmarkTable: dots in version + based_on columns
- OpenAPI schemas updated (SRG, STIG, Component)
- 7 model tests + 10 blueprint tests + 6 Vue tests

Authored by: Aaron Lippold<lippold@gmail.com>
- ADR for DISA catalog service (SolidQueue, one-click import)
- Full v2.x roadmap: 814sp/133 cards/16 epics
- Critical path: 5 cards to SPA spike

Authored by: Aaron Lippold<lippold@gmail.com>
- Tests expected old single-button unlink (data-test=unlink-identity-button)
- Component now uses per-identity table rows with can_unlink flag
- Updated tests to pass identities array + verify identity_id in payload
- 29/29 UserProfile tests green

Authored by: Aaron Lippold<lippold@gmail.com>
- SPA spike: 36 endpoints ready, 19 undocumented, 16 missing
- Roadmap updated: v2-xcy, v2-54tg, spike all DONE
- v2-btu scoped to 14 critical cards (was 39)

Authored by: Aaron Lippold<lippold@gmail.com>
The test-env override forced unlock_strategy to :time, claiming SMTP
dependency — but ActionMailer uses :test delivery in specs, so no SMTP
is needed. The override meant Devise never generated the unlock email
routes in test, making POST /users/unlock untestable.

Authored by: Aaron Lippold<lippold@gmail.com>
Standard Devise controllers returned 500 for JSON requests — the SPA
needs proper JSON on all auth flows. Overrides follow the existing
RegistrationsController respond_to pattern:

- Users::PasswordsController — request reset, execute reset, and
  validate-token (GET edit returns {valid, minimum_password_length})
- Users::ConfirmationsController — resend confirmation instructions
- Users::UnlocksController — resend unlock instructions
- Users::RegistrationsController#edit — profile as CurrentUserBlueprint

Paranoid mode preserved: identical success toast for known and unknown
emails (no enumeration); blank email returns 422. HTML flows unchanged.
14 request specs cover success and failure paths. Authorization
coverage spec skips the new controllers (Devise owns their auth).

Authored by: Aaron Lippold<lippold@gmail.com>
Five new path files documenting the auth flow endpoints the SPA needs:
POST/PUT /users/password, GET /users/password/edit,
POST /users/confirmation, POST /users/unlock, GET /users/edit.
Toast endpoints reference ToastResponse; profile references
CurrentUserResponse; token validation documents its inline shape.

12 contract tests validate real responses against the bundled schema
via openapi_first, including paranoid-mode success responses and 422
error shapes. Live-tested against the running dev server with curl —
all request/response pairs verified with real data.

Authored by: Aaron Lippold<lippold@gmail.com>
bundler-audit flagged patch-level CVEs in transitive dependencies:
concurrent-ruby, crass, faraday, json, msgpack, net-imap, nokogiri,
oauth2, oj, websocket-driver. Conservative update — no version pin
changes in Gemfile. bundler-audit now clean; full suite green
(3851 RSpec / 3357 Vitest).

Authored by: Aaron Lippold<lippold@gmail.com>
helpful_errors rescued StandardError and rendered a generic 500 toast
with no logging — the root cause of any production 500 was invisible
everywhere. Log the exception class, message, and top of the backtrace
before rendering. Specs cover the log line and the admin/non-admin
message split.

Authored by: Aaron Lippold<lippold@gmail.com>
devise-security's before_logout hook calls update_unique_session_id!
during the sign_out that follows account self-deletion; on the
destroyed record it raises NotPersistedError. The account deleted but
the user saw a 500 toast (session limits are enabled by default). The
hook respects skip_session_limitable? — return true for destroyed
records. Covered by the self-delete request specs and live-verified.

Authored by: Aaron Lippold<lippold@gmail.com>
Self-delete previously took one click on a live session — no password,
no continuity check. Per OWASP ASVS 3.7.1 and the GitLab/GitHub account
deletion pattern:

- Local-credential users must re-enter current_password; wrong attempts
  count toward lockout (423 once locked). Provider-managed and
  SSO-created (auto-password) accounts are exempt — their IdP owns
  re-authentication.
- The last system admin cannot self-delete (mirrors the existing
  UsersController guard): 422 with promote-another-admin guidance.
- ConfirmDeleteModal gains an optional requirePassword prop (default
  false, other consumers unaffected); the profile delete modal collects
  the password for local users only.
- DELETE /users documented in OpenAPI (200/422/423) with contract tests
  validating each response shape against the schema.

Live-verified: curl matrix with DB checks + Playwright golden path in
light and dark modes.

Authored by: Aaron Lippold<lippold@gmail.com>
A project could drop to zero admins through the membership API,
orphaning it for its team. Per the GitLab/GitHub last-owner pattern,
the last project-admin membership can no longer be removed or
downgraded — the 422 names the project and says to transfer the admin
role first.

The guard lives in the model as the single source of truth (update
validation + before_destroy abort), so console, importers, and any
future callers hit it too; the existing controller error paths surface
it with no controller changes. Cascades are exempt via
destroyed_by_association (deleting a project legitimately removes its
memberships; the user-deletion path gets its own guard next). Component
memberships are exempt — project admins govern components through
effective_permissions.

The OpenAPI spec had promised 'cannot demote the last admin' since it
was authored; the 422 responses are now documented and the code honors
the design. 13 new specs + schema-validated contract tests; live
verified via curl with the allow-path confirmed after adding a second
admin.

Authored by: Aaron Lippold<lippold@gmail.com>
Deleting a user who held a project's only admin membership silently
orphaned the project for its team — the membership cascade bypasses the
membership-layer guard by design (destroyed_by_association). Apply the
GitLab/GitHub transfer-ownership rule at both deletion endpoints: the
admin user-management path and Devise self-delete return 422 naming the
affected projects until the admin role is transferred.

User#solely_administered_projects is the shared source of truth — one
SQL query (admin-membership subselect anti-joined against other-admin
memberships), no Ruby enumeration; component memberships never count
since project admins govern components through effective_permissions.
The block message comes from a single model method with a contextual
subject ('This user is' / 'You are'), capped at five project names with
an overflow count. System admins get no bypass — continuity, not
privilege.

OpenAPI 422s documented on both delete operations with contract tests
validating each against the schema. Live-verified: both paths blocked,
then allowed after transferring the role to a second admin.

Authored by: Aaron Lippold<lippold@gmail.com>
- create now respond_to json: canonical toast + new request id (200)
  or danger toast on duplicate request (422); HTML flash unchanged
- Fix destroy success toast title: was 'Access request submitted.'
  on deny/cancel — now 'Access request denied.'/'... cancelled.'
- destroy converted from hand-built Toast.new to render_toast helper
- OpenAPI POST path rewritten from '302 HTML-only' to the JSON
  contract; schema renamed AccessRequestToastResponse (POST + DELETE)
- Bundle regenerated (includes the latest-benchmark paths committed
  with their sources in the next commit — hook requires current bundle)
- 4 new request specs + 2 contract tests; live curl verified

Authored by: Aaron Lippold<lippold@gmail.com>
One centralized implementation serves three family-grouped listings
for SPA dropdown population:

- GET /api/srgs/latest + /api/stigs/latest — public DISA reference
  data, one family per row at highest numeric V{major}R{minor}
- GET /api/components/latest — released components only (a component
  is a STIG in progress), auth required, family = prefix, ranked on
  integer version/release with NULLS LAST; drafts never shadow
- LatestBenchmarkListable concern: single #latest action, per-
  controller declarative config, latest_base_scope visibility seam
- BenchmarkSearchable concern: SearchQueryService-backed ?q= across
  all three models (abbreviation expansion, parameterized ILIKE)
- GPOS added to core search abbreviations (helps global search too)
- Lean :latest blueprint views — identity fields only, never XML,
  no per-record severity/currency queries
- 2 OpenAPI schemas + 3 path files + contract tests (incl. 401 shape)
- 27 new examples, all RED-first; authz coverage entries documented

Authored by: Aaron Lippold<lippold@gmail.com>
- GET /api/srgs/:id/stats (authenticated): rule count + severity
  breakdown as SQL aggregates, plus usage — components based on the
  SRG, scoped to caller-visible projects (member/discoverable or
  released); count and list share one scope so hidden usage never
  leaks through the count
- GET /api/stigs/:id/stats (public): rule count + severity breakdown
  only — pure reference data, no usage (components are based on SRGs)
- SeverityCounts#benchmark_stats: one COUNT + one GROUP BY, no Ruby
  enumeration; reusable by Component for the dashboard stats card
- SrgsController skip_before_action narrowed to only: :latest, with
  the cop-documented lexical anchor for the concern-provided action
- OpenAPI BenchmarkStatsResponse schema + 2 path files; 4 contract
  tests incl. 401/404 shapes; authz coverage entries documented
- SRG factory :skip_rules trait (mirrors stig factory) so tests
  hand-craft severities; live-tested incl. non-member data-leak check

Authored by: Aaron Lippold<lippold@gmail.com>
- GET /api/components/:id/summary — lightweight header for SPA
  triage/settings routes: identity, counts, srg info, and the
  caller's effective_permissions; never ships the rules/reviews/
  histories arrays (721 B vs 1.06 MB full blob on real data)
- Comment-phase state machine serialized from the model methods
  (accepting_new_comments, triaging_active, frozen_for_writes,
  comment_period_days_remaining) — single source of truth, clients
  stop reimplementing the write-guard logic to predict rejections
- ComponentBlueprint :summary view — fields live in the view, the
  column-limited default field list is untouched
- authorize_component_access relocated to ApplicationController so
  the API controller shares the released→logged-in / else→viewer
  rule with ComponentsController
- OpenAPI ComponentSummaryResponse schema + path (200/401/403/404,
  ISO 8601 date-time verified against live output); 3 contract tests
- Exhaustive phase/closed_reason matrix in the request spec; live
  curl matrix incl. scratch non-viewer 403 proof

Authored by: Aaron Lippold<lippold@gmail.com>
- api_projects.yaml: all 5 params (q, sort, order, page, per_page)
  with enums matching the controller's apply_sort whitelist and the
  pagy defaults (25/page, max 100); 200/400/401 responses
- PaginationMeta.yaml: reusable {page, per_page, total} schema for
  the canonical {rows, pagination} envelope — pagination rollout
  (v2-btu.8) reuses it instead of inlining per endpoint
- 4 contract tests: default page, q+sort+per_page combined,
  400 out-of-range page, 401 unauthenticated
- Timestamps documented as ISO 8601 (render_as_json path) after
  live output showed the string-render example format was wrong
  for this endpoint
- Zero controller changes — documentation-only card

Authored by: Aaron Lippold<lippold@gmail.com>
Five single-call SQL aggregates powering SPA dashboards:

- GET /api/components/:id/stats — rules by status/severity, completion
  and lock percentages (nil, never fabricated 0.0, on empty components)
- GET /api/components/:id/workflow_state — authoring/lock/review/
  comment/triage/export readiness, reusing the phase-machine booleans
- GET /api/components/:id/triage_summary — top-level comment counts
  per triage status (all 9 keys zero-filled) + adjudication pct
- GET /api/projects/:id/stats — aggregate + per-component breakdown
  from three grouped queries
- GET /api/projects/:id/triage_summary — project-wide triage metrics

Supporting model layer:
- SeverityCounts-style aggregates live on models: Component#
  dashboard_stats/#workflow_state, Project#dashboard_stats,
  Review.triage_summary; Component.status_buckets extracted for reuse
- Review.for_components — canonical commentable-based scope mirroring
  CommentQueryService (rule-attached OR component-attached)
- PercentageMath concern: nil on zero denominators

Bug fix: Component.pending_comment_counts undercounted component-level
comments (commentable=Component, no rule) — the same triage queue.
Rewritten as ONE LEFT JOIN + CASE-grouped query, preserving the
single-query guard (deleted-rule exclusion moved into the join since
raw joins bypass Rule's default_scope).

OpenAPI: 5 paths + 6 schemas (SeverityBuckets, RulesByStatus reusable);
7 contract tests; auth via authorize_component_access /
authorize_viewer_project; live-tested on real data with cross-checks.

Authored by: Aaron Lippold<lippold@gmail.com>
SRG-writing as a first-class document type (v5). Authored SRG
requirements are expanded SrgRules with component linkage; catalog
attachment at release. Records 14 design decisions: Moved terminal
status + tombstone semantics, multi-parent lineage for SRG and STIG
components (core vs derived eligibility), family technology token
with ID minting at release, relocation marker + dual intake,
reference benchmarks, mixed-project sections, full XCCDF export.
Includes 7-phase implementation plan.

Authored by: Aaron Lippold<lippold@gmail.com>
be_within(5.seconds) of a pre-call timestamp fails when the example
runs slowly under load (observed at 6.7s on an unbalanced parallel
run). The applier stamps last_sync_at with Time.current during the
call, so assert it falls between before/after call bounds instead.

Authored by: Aaron Lippold<lippold@gmail.com>
Rack::Attack uses fixed 60-second buckets; without freeze_time the
requests can split across a bucket boundary on a slow run, so the
counter never reaches the limit and the 429 assertion flakes. The
reaction-throttling block already used this pattern — apply it to
the login and comment-action blocks too.

Authored by: Aaron Lippold<lippold@gmail.com>
Two amendments to the SRG Component Authoring ADR (v5 -> v6). Neither
overturns a §0 decision; the v5 storage core (authored SRG requirements
are expanded SrgRules) stands unchanged.

Scoping (§0.1 A) — retitled to "Generalized XCCDF Document Authoring —
SRG as the First New Profile". This is not "adding SRG support"; it is
generalizing Vulcan to author a document off any XCCDF upstream, with
SRG as the primary first-delivered profile. Vulcan's trajectory is to
scope beyond the DoD document space. Scope stays SRG + STIG — the ask is
that the seams be named and shaped for extension so a further profile is
additive config, not a schema migration. Records the five places the
STIG/SRG binary is currently hard-coded (document_type enum, parent
eligibility, the core flag, derived_from_srg_rule_id, the SRG catalog as
sole upstream registry) as naming/widening debt, and §2.2 defines the
per-profile policy seam.

User workflow (§0.1 B, §2.1, R6) — the v5 draft specified storage
exhaustively and the user's path not at all; the creation-time choice had
to be established by phone rather than read off the document. Now
normative: authoring type is chosen at creation and is exclusive (a
component is a STIG or an SRG, never both), and that choice gates the
source picker, the per-requirement status options, the field config,
Satisfied-By, the relocation marker, and the export mode. Adds the
creation walkthrough, the authoring loop, the immutability consequence,
and wires the workflow into the §14 phasing so it is carded rather than
lost between two storage phases.

Also records five review findings as open items (§10.4-8), none blocking
the core: the bare "Applicable" status value is a substring footgun in
the flat shared STATUSES list ('Not Applicable'.include?('Applicable')
is already true) and wants per-profile status vocabularies; Moved
conflates disposition with routing; mis-typed components have no
recovery path; release/catalog attachment risks aliasing published rows
with editable ones; primary-parent selection is undefined for N parents.

Signed-off-by: Will Dower <will@dower.dev>
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
D Reliability Rating on New Code (required ≥ A)
E Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

💡 Need a hand with PR review? Try Gitar by Sonar!

@aaronlippold aaronlippold temporarily deployed to vulcan-feat-component-s-qaxbtk July 13, 2026 00:06 Inactive
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants