Skip to content

DRAFT (do not merge): CI validation — remove component sync/merge engine from #731#741

Closed
wdower wants to merge 601 commits into
masterfrom
chore/731-remove-merge-engine
Closed

DRAFT (do not merge): CI validation — remove component sync/merge engine from #731#741
wdower wants to merge 601 commits into
masterfrom
chore/731-remove-merge-engine

Conversation

@wdower

@wdower wdower commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

⚠️ DO NOT MERGE — CI validation checkpoint only

This draft exists only to run CI on the component sync/merge engine removal
before it lands on #731. It is based on feat/comment-triage-context-panel
(#731), so the diff below shows all of #731 plus the one removal commit
(refactor: Remove component sync/merge engine from #731). Review only that
commit; everything else is #731.

What this validates

The removal extracts epic v2-480 (component sync & merge — the "reupload a
backup to overwrite the DB with 3-way-merged updates" engine) out of #731 so
the rest of #731 can ship. The engine is preserved intact on
feat/component-sync-merge and will be reconstructed off post-#731 master.

What to look at in CI

  • backenddb:schema:load exercises the hand-edited schema.rb (merge
    tables + last_sync_* columns + FKs removed), then full parallel_rspec.
    This is the check we most want green.
  • lint / frontend / docker / ci-gate — merge-only Ruby/RuboCop and OpenAPI
    surface removed; comment-merge (Review.merge_comments!, PATCH /reviews/merge)
    and the spreadsheet round-trip are untouched keepers.

Baseline caveat

#731 has no recent clean CI run, so red here isn't necessarily removal-caused —
merge-spec failures simply vanish (those specs are deleted); any new keeper
failure is what matters.

The removal commit at a glance

  • Deleted: app/services/import/json_archive/merge/ (20 files) + specs,
    MergeJob/ApplicationJob, 3 merge models, sync.rake, 7 merge migrations,
    merge OpenAPI paths/schemas (56 files)
  • Stripped merge-only hunks from 12 shared files (constants, merge_status,
    import_backup?merge, routes, sync: settings, schema, OpenAPI bundle)
  • Kept: ReviewBuilder/backup_serializer import hardening, rule_satisfactions
    FK constraints, backup format 1.1, comment-merge

Tracking: split epic + children (dry-run, classify, remove, validate, rebase)
staged in beads (pending the shared-DB migration).

wdower and others added 30 commits June 5, 2026 10:47
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>
Strategy resolves "what wins when both sides diverge on a field." Seven
verbs: :ours, :theirs, :newer, :conflict, :union, :skip, :manual. Defaults
favor conservatism on rule content (:conflict, force human review) and
additivity on memberships / satisfactions (:union).

- VALID_STRATEGY_RESOLUTIONS constant (frozen) — analyzer + tests share
  the canonical list (F19)
- DEFAULT_STRATEGY (frozen) declares per-entity policy with _default
  sentinel keys for blanket fallbacks
- #locked_field_resolution always returns :conflict; locked fields cannot
  be overridden by any strategy
- ArgumentError on unknown resolution at construction (fail at boundary)
- from_cli_flags intentionally NOT here — belongs in the rake layer (F18)

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

Signed-off-by: Will Dower <will@dower.dev>
Three parallel lists (RuleBuilder DIRECT_COLUMNS, BackupSerializer
EXCLUDED_RULE_COLUMNS by inverse, round-trip RULE_COMPARE_FIELDS) had
drifted apart. Per expert review F19, consolidate on Rule::MERGEABLE_FIELDS
+ Rule::DERIVED_COLUMNS as the canonical content set; everything else
derives.

- Rule::MERGEABLE_FIELDS (19 content fields) and DERIVED_COLUMNS
  (inspec_control_file — regenerated by update_inspec_code after_save)
- RuleBuilder::DIRECT_COLUMNS now = MERGEABLE + identity + lifecycle +
  DERIVED (same 25 columns as before, no drift)
- BackupSerializer EXCLUDED_RULE_COLUMNS gets a cross-reference comment
  pointing at the canonical lists
- RULE_COMPARE_FIELDS in the round-trip spec now derives from
  MERGEABLE_FIELDS + :locked
- New contract spec asserts MERGEABLE ⊆ valid columns, no MERGEABLE/
  DERIVED overlap, DIRECT_COLUMNS is the exact union

Round-trip spec still 24/0 green — no observable behavior change.

Refs commit 5 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 17 commits July 9, 2026 15:09
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>
Extract the component sync & merge engine (epic v2-480) out of PR #731 so
the remaining triage/PAT/upgrade/OpenAPI work can merge without it. The
engine is unfinished and prod-data-risky; it is preserved intact on branch
feat/component-sync-merge and will be reconstructed off post-#731 master.

Deleted (branch-only engine, 56 files):
- app/services/import/json_archive/merge/ (20 files) + specs
- MergeJob + ApplicationJob, ComponentSyncEvent/MergeOperation/MergeQuarantineRecord
- lib/tasks/sync.rake, 7 merge migrations, merge OpenAPI paths/schemas

Stripped merge-only hunks from shared files (kept all #731 work):
- rule.rb: NESTED_MERGEABLE_ASSOCIATIONS (MERGEABLE_FIELDS/DERIVED_COLUMNS kept —
  load-bearing for RuleBuilder + backup round-trip)
- review.rb: MERGEABLE_FIELDS constant
- component.rb: component_sync_events / merge_operations associations
- manifest_validator.rb: merge: param + branch (1.1 format support kept)
- components_controller.rb: merge_status; projects_controller.rb: import_backup?merge + helpers
- routes.rb: merge_status route; vulcan.default.yml: sync: block
- db/schema.rb: 3 merge tables, last_sync_* columns, merge FKs
- OpenAPI: merge params/paths/schemas removed, bundle regenerated

Kept as #731's own work: ReviewBuilder/backup_serializer import hardening,
rule_satisfactions FK constraints, backup format 1.1, and comment-merge
(Review.merge_comments! / PATCH /reviews/merge).

Schema.rb hand-edited (deterministic removal of merge DDL); authoritative
db:migrate regen + full rspec run to follow in the split-validation step.

Signed-off-by: Will Dower <will@dower.dev>
auth = request.headers['Authorization']
return nil unless auth

match = auth.match(/\AToken\s+(.+)\z/i)
@aaronlippold aaronlippold temporarily deployed to vulcan-chore-731-remove-zsllri July 12, 2026 18:44 Inactive
Resolve 2 conflicts from master advancing 2 commits (#736 ironbank, #740
gem bumps): release.yml keeps both the openapi-publish job (#731) and the
Iron Bank release steps (master); Gemfile.lock takes master's security
bumps reconciled with #731's gems via bundle install. Unblocks pull_request
CI on the removal branch (was CONFLICTING with master).

Signed-off-by: Will Dower <will@dower.dev>
@wdower wdower temporarily deployed to vulcan-chore-731-remove-zsllri July 12, 2026 19:09 Inactive
target: production
environment:
RAILS_ENV: production
DATABASE_URL: postgres://postgres:schemathesis@db/vulcan_schemathesis
@@ -39,6 +39,11 @@
default: false,
},
},
data() {
return {
instanceId: Math.random().toString(36).slice(2, 9),
// picks its own seed, so popoverIds don't collide when (e.g.) the navbar
// pack and the project_component pack both render UserBadges on the same
// page. Combined with this._uid for per-instance uniqueness within a pack.
const moduleSeed = Math.random().toString(36).slice(2, 8);
name: Jane Doe
email: jane.doe@example.org
admin: false
password: SecureP@ssw0rd2026!
summary: Set a new password
value:
user:
password: SecureP@ssw0rd2026!

it 'returns true when IP is within an allowed CIDR' do
token = create(:personal_access_token, user: user,
allowed_ips: ['10.0.0.0/8'])

it 'returns true when allowed_ips is empty array (allow all)' do
token = create(:personal_access_token, user: user, allowed_ips: [])
expect(token.ip_allowed?('1.2.3.4')).to be true
describe '#ip_allowed?' do
it 'returns true when allowed_ips is nil (allow all)' do
token = create(:personal_access_token, user: user, allowed_ips: nil)
expect(token.ip_allowed?('1.2.3.4')).to be true

it 'accepts valid CIDR entries' do
token = build(:personal_access_token, user: user,
allowed_ips: ['10.0.0.0/8', '192.168.1.0/24', '2001:db8::/32'])

it 'accepts valid CIDR entries' do
token = build(:personal_access_token, user: user,
allowed_ips: ['10.0.0.0/8', '192.168.1.0/24', '2001:db8::/32'])
Two CI failures on the merge-removal branch, both my own, neither from
the engine removal itself:

1. Gemfile.lock: resolving the master merge with --theirs downgraded
   rack 3.2.6 -> 2.2.23 and concurrent-ruby 1.3.7 -> 1.3.6. #731 is
   ahead of master on gems (Rails 8 / rack 3). The rack 2.x downgrade
   broke :unprocessable_content, a rack 3.1+ status symbol, failing
   every spec asserting it; the concurrent-ruby downgrade re-introduced
   an advisory #731 had already fixed, failing bundle-audit. Restored
   #731's lock (rack 3.2.6, concurrent-ruby 1.3.7).

2. manifest_validator_spec: removing the merge: kwarg left keeper specs
   asserting merge-mode warnings and @merge. Dropped those examples;
   they belong with the merge PR.

Signed-off-by: Will Dower <will@dower.dev>
@wdower wdower temporarily deployed to vulcan-chore-731-remove-zsllri July 12, 2026 22:37 Inactive
@wdower wdower force-pushed the chore/731-remove-merge-engine branch from 040d1a9 to 7ef2830 Compare July 12, 2026 23:40
@wdower wdower temporarily deployed to vulcan-chore-731-remove-zsllri July 12, 2026 23:41 Inactive
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

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

See analysis details on SonarQube Cloud

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

wdower added a commit that referenced this pull request Jul 12, 2026
Removes the component sync & merge engine (epic v2-480) from this PR so
the remaining triage / PAT / upgrade / OpenAPI work can ship without it.
The engine is unfinished (UI, disaster-recovery, integration tests and
manifest v1.1 all still open) and carries production-data risk, so it is
being landed as its own PR rather than holding this one up.

The engine is preserved intact on branch feat/component-sync-merge (also
pushed to origin). After this PR merges to master, that branch rebases
onto the new master and reduces to the engine alone — which is why this
PR should land as a merge commit, NOT a squash: a squash breaks that
rebase.

Removed (56 files): app/services/import/json_archive/merge/ + specs,
MergeJob + ApplicationJob, ComponentSyncEvent / MergeOperation /
MergeQuarantineRecord, lib/tasks/sync.rake, 7 merge migrations, merge
OpenAPI paths and schemas.

Stripped merge-only hunks from 12 shared files, keeping all of this PR's
own work: ReviewBuilder / backup_serializer import hardening,
Rule::MERGEABLE_FIELDS + DERIVED_COLUMNS (load-bearing for RuleBuilder
and the backup round-trip), rule_satisfactions FK constraints, backup
format 1.1, and comment-merge (Review.merge_comments! / PATCH
/reviews/merge — unrelated to the component merge engine).

Also merges origin/master, resolving this PR's conflict with master
(#736 ironbank workflow, #740 gem bumps). release.yml keeps both the
openapi-publish job and the Iron Bank release steps; Gemfile.lock keeps
this PR's rack 3.2.6 / concurrent-ruby 1.3.7, which are ahead of master.

Validated on CI via draft PR #741: the removal itself produced zero
failures (lint, frontend, docker and 4 of 6 backend shards green; the one
remaining failure is a pre-existing OIDC login-button spec that the
removal cannot touch — it changes no auth file and the oidc config is
byte-identical).

Tracking: v2-cwy5 (split epic) — .1 dry-run and .3 classify closed,
.4 removal this commit.

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

wdower commented Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

Superseded: the removal has been merged into #731 (merge commit 43e5921). This draft existed only to CI-validate the merge-engine removal, which it did — the removal produced zero CI failures. The engine itself is preserved on feat/component-sync-merge (pushed to origin) and will become its own PR after #731 lands on master.

@wdower wdower closed this Jul 12, 2026
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.

3 participants