Feat/component sync merge#742
Draft
wdower wants to merge 599 commits into
Draft
Conversation
- 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>
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>
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


No description provided.