Skip to content

feat: triage workspace + PAT auth + upgrade infrastructure + seed dataset + OpenAPI hardening - #731

Open
aaronlippold wants to merge 714 commits into
masterfrom
feat/comment-triage-context-panel
Open

feat: triage workspace + PAT auth + upgrade infrastructure + seed dataset + OpenAPI hardening#731
aaronlippold wants to merge 714 commits into
masterfrom
feat/comment-triage-context-panel

Conversation

@aaronlippold

@aaronlippold aaronlippold commented May 20, 2026

Copy link
Copy Markdown
Member

Summary

Major feature branch: 247 commits, 668 files changed, 53K insertions. Builds on the triage split-pane workspace (PR #717 follow-up) and adds infrastructure standardization, API authentication, upgrade system, seed data, and documentation.

Core Features (earlier commits on this branch)

  • Three-column split-pane triage — rule sidebar + content + triage form
  • Triage progress bar — status pills, stacked bar, click-to-filter
  • Public comment review system — full DISA workflow (PR Feat/viewer comments #717)
  • Admin actions — merge comments, force-withdraw, restore, move-to-rule, hard-delete

New in this push

  • Personal Access Tokens — SHA-256 digest, scoped (read/write/admin), IP allowlist, dual-mode auth (session + token)
  • Toast value object — canonical {title, message: Array, variant} across all mutation endpoints
  • Upgrade path infrastructure — version manifest (GitLab pattern), preflight/fix/verify/auto rake tasks, standalone Ruby DB renamer for Docker
  • Database standardizationvulcan_development/test/production naming, test DB name hardcoded to prevent collision, port registry (5435)
  • CommentRowBlueprint — replaces 3 hand-built comment row serializers with one Blueprint + 4 views
  • Container SRG Test seed — 264 rules, 228 reviews (91 addressed_by, 99 replies), PII-clean, RBAC-distributed attribution across 8 community personas, imported via production JsonArchiveImporter
  • Custom linting rules — RuboCop cop + ESLint rule permanently block tracker IDs in source comments
  • OpenAPI hardening — strict Redocly lint (operation/parameter/tag descriptions required), 17 operation descriptions added, tokenAuth global security, 107 contract tests
  • VitePress documentation — 7 new pages (design system, toast contract, OpenAPI testing, upgrade guide, public comment review, etc.), 10 updated pages, sharp dependency fix
  • Importer FK fixdrop_invalid_reviews now deletes children before parents (RESTRICT safety)

Bug fixes

  • API 401→302 regression (Devise warden throw)
  • ComponentBlueprint :index missing fields (Error when duplicating components #734)
  • DATABASE_NAME dev/test collision
  • Docker entrypoint swallowing upgrade errors
  • db-rename-legacy silent no-op in Docker (no psql binary)
  • admin_bootstrap parallel test flake
  • 24 test failures from PAT/Toast session
  • Broken auto-corrected comments from tracker cleanup

Test plan

  • bin/parallel_rspec spec/ — full backend suite
  • yarn test:unit — Vue component tests
  • yarn lint:ci — 0 warnings
  • bundle exec rubocop — 0 offenses
  • yarn openapi:bundle && yarn openapi:lint — 0 errors
  • bundle exec rspec spec/contracts/ — 107 contract tests
  • yarn docs:build — VitePress builds clean
  • bundle exec brakeman — 0 warnings
  • Manual: login, create project, import Container SRG, verify 228 reviews, triage split-pane, PAT creation

Authored by: Aaron Lippoldlippold@gmail.com

Copilot AI review requested due to automatic review settings May 20, 2026 03:43
@gitguardian

gitguardian Bot commented May 20, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 7 secrets following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secrets in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
33453143 Triggered Generic Password 51d7583 doc/openapi/paths/users.yaml View secret
34693140 Triggered Generic Password 7582af0 spec/contracts/devise_auth_flows_contract_spec.rb View secret
33788689 Triggered Generic Password a80edde spec/requests/users/passwords_spec.rb View secret
34701656 Triggered Generic Password 5092b87 spec/javascript/components/users/UserProfile.spec.js View secret
34701655 Triggered Generic Password 5092b87 spec/contracts/devise_auth_flows_contract_spec.rb View secret
34701657 Triggered Generic Password df9e0ef spec/contracts/devise_auth_flows_contract_spec.rb View secret
34701658 Triggered Generic Password 5092b87 spec/javascript/components/shared/ConfirmDeleteModal.spec.js View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secrets safely. Learn here the best practices.
  3. Revoke and rotate these secrets.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR modernizes the comment/triage workflow and development seeding by introducing a split-pane triage UI (replacing the modal flow), modular/idempotent seed helpers + dev rake tasks, and a DRY’d comment composer state model (ReplyComposerMixin) with supporting factory/test improvements.

Changes:

  • Add split-pane triage experience (queue navigation + rule context panel + extracted triage form) and related API support (include_rule_content, HTML redirect to triage).
  • Modularize and validate seed pipeline (SeedHelpers, numbered seed files, dev:* rake tasks, seed verification specs).
  • Consolidate composer/triage vocabulary utilities and standardize factories/spec fixtures around new traits.

Reviewed changes

Copilot reviewed 83 out of 83 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
spec/services/import/json_archive_importer_spec.rb Switch review setup to factories/traits for import lifecycle fixtures.
spec/services/import/integration/backup_round_trip_spec.rb Use factory-created reviews in backup round-trip integration spec.
spec/services/export/serializers/backup_serializer_spec.rb Use factory traits for review lifecycle/duplicate-of serialization coverage.
spec/services/export/base_spec.rb Use factory traits for disposition-related export setup.
spec/seeds/seed_pipeline_spec.rb Add end-to-end seed pipeline spec (tagged, truncation strategy).
spec/requests/users_spec.rb Use review factory traits for “My Comments” request fixtures.
spec/requests/rules_spec.rb Use review factory traits for component JSON sidebar/history tests.
spec/requests/reactions_spec.rb Use factory-created reviews/replies for reaction request specs.
spec/requests/rack_attack_spec.rb Use factory-created review for rack-attack reaction target fixture.
spec/requests/projects_import_backup_spec.rb Use factory-created review for project import backup tests.
spec/requests/projects_disposition_matrix_export_spec.rb Use factory traits for triage-status filtered disposition export tests.
spec/requests/projects_create_from_backup_spec.rb Use factory-created review for include_reviews param coverage.
spec/requests/project_comments_aggregate_spec.rb Use factory traits to build aggregate comment fixtures.
spec/requests/components_spec.rb Add HTML redirect expectation for /components/:id/comments; use review factory.
spec/requests/components_disposition_matrix_export_spec.rb Use review factory traits for component disposition export fixtures.
spec/requests/component_reviews_spec.rb Use :component_comment factory trait for component-scoped review tests.
spec/rails_helper.rb Exclude :seed_pipeline specs by default to avoid parallel DB corruption.
spec/models/user_spec.rb Use review factory traits for imported attribution behavior tests.
spec/models/reviews_spec.rb Standardize review creation on factories/traits across invariants + scopes.
spec/models/review_polymorphic_commentable_spec.rb Use :comment/:component_comment traits for polymorphic coverage.
spec/models/review_cross_scope_validations_spec.rb Use review factory traits for cross-scope validation fixtures.
spec/models/reaction_spec.rb Use review factory traits for reaction invariants and summary tests.
spec/models/query_performance_spec.rb Use factory-created reviews to drive component review query tests.
spec/models/project_pending_comment_counts_spec.rb Use factory traits for pending/total count fixtures.
spec/models/paginated_comments_pii_spec.rb Use review factory traits for paginated comment response/reaction counts.
spec/models/components_spec.rb Add assertions for include_rule_content payload shape + updated_at presence.
spec/models/component_pending_comment_counts_spec.rb Use review factory traits for per-component pending count fixtures.
spec/lib/seed_helpers_spec.rb Add unit specs for SeedHelpers API (seed_xccdf/component/review/reply/status/verify).
spec/lib/disposition_matrix_export_spec.rb Switch to review factory traits for export and CSV defang coverage.
spec/javascript/mixins/ReplyComposerMixin.spec.js Add vitest coverage for unified composer state mixin behaviors.
spec/javascript/constants/triageVocabulary.spec.js Add tests for buildStatusFilterOptions().
spec/javascript/components/triage/TriageQueueNav.spec.js Add tests for 2D triage queue navigation component.
spec/javascript/components/triage/CommentTriageForm.spec.js Add tests for extracted triage form component validation/events.
spec/javascript/components/components/ComponentComments.spec.js Update ComponentComments tests for split-mode behavior and sorting/filter visibility.
spec/javascript/components/components/CommentTriageModal.spec.js Update modal tests to validate delegation to CommentTriageForm and new doTriage API.
spec/factory_specs/stig_deadlock_spec.rb Add regression spec preventing STIG factory deadlock/import side effects.
spec/factory_specs/factory_traits_spec.rb Add spec verifying new FactoryBot traits across multiple models.
spec/factories/stigs.rb Add :skip_rules trait to disable rule import on STIG factory.
spec/factories/stig_rules.rb Ensure stig_rule factory uses a skip-import STIG by default.
spec/factories/rules.rb Add rule status/locked traits used by tests/seeds.
spec/factories/reviews.rb Add review comment/triage/lifecycle traits + auto-membership wiring.
spec/factories/projects.rb Add project membership traits (:with_admin, :with_members).
spec/factories/memberships.rb Add explicit :viewer trait.
spec/factories/components.rb Add component traits for comment period, PoC, released/locked rules.
spec/config/seed_idempotency_spec.rb Update idempotency checks to match modular seed loader + SeedHelpers.
spec/blueprints/rule_blueprint_spec.rb Use review factory traits for comment summary blueprint tests.
spec/blueprints/review_membership_blueprints_spec.rb Use review factory traits for blueprint output coverage.
lib/tasks/dev.rake Add dev:prime/status/verify/reset seed tasks.
lib/seed_helpers.rb Introduce SeedHelpers module (XCCDF import, component seeding, comment seeding, verify/status).
docs/superpowers/plans/2026-05-19-comment-interaction-dry.md Document DRY plan for composer state/event standardization.
docs/development/testing.md Document available FactoryBot traits and usage patterns.
docs/development/seed-system.md Document seed architecture, commands, and SeedHelpers API.
db/seeds/data/00_users.rb Modular seed: demo admin + role-tier + filler users.
db/seeds/data/01_projects.rb Modular seed: demo projects.
db/seeds/data/02_srgs.rb Modular seed: SRG imports via SeedHelpers.
db/seeds/data/03_stigs.rb Modular seed: STIG imports via SeedHelpers.
db/seeds/data/04_components.rb Modular seed: demo components, overlays, dummy stress components, PoC backfill.
db/seeds/data/05_memberships.rb Modular seed: demo RBAC wiring and counter cache reset.
db/seeds/data/06_rule_statuses.rb Modular seed: vary rule statuses for demo coverage.
db/seeds/data/10_comments.rb Modular seed: demo comment threads + triage states.
db/seeds/data/11_cross_project.rb Modular seed: cross-project comment fixtures + triage examples.
app/models/review.rb Improve sync_commentable_from_rule to backfill rule_id from commentable when needed.
app/models/component.rb Add include_rule_content option to paginated_comments, include updated_at, and serialize rule content.
app/controllers/components_controller.rb Redirect HTML /components/:id/comments to triage; add include_rule_content param handling for JSON.
app/javascript/mixins/ReplyComposerMixin.vue Add unified comment composer state + open/close/post hooks.
app/javascript/constants/triageVocabulary.js Add terminal/single-button status sets + buildStatusFilterOptions().
app/javascript/composables/ruleFieldConfig.js Add canonical FIELD_LABELS mapping for rule context display.
app/javascript/components/users/UserComments.vue Adopt ReplyComposerMixin; switch truncation to CSS; use shared status options builder.
app/javascript/components/shared/CommentThread.vue Switch date formatting to DateFormatMixin helper and remove ad-hoc formatter.
app/javascript/components/rules/RulesCodeEditorView.vue Adopt ReplyComposerMixin and unify composer open/post handling.
app/javascript/components/components/ProjectComponent.vue Adopt ReplyComposerMixin and unify composer open/post handling.
app/javascript/components/components/ComponentTriagePage.vue Add command bar controls for split-mode context/admin panel; wire split-mode events.
app/javascript/components/components/ComponentComments.vue Add split-mode triage integration; default sort by ID; hide filters in split mode; use status options builder; adopt ReplyComposerMixin.
app/javascript/components/components/CommentTriageModal.vue Embed CommentTriageForm; refactor to doTriage API; use DateFormatMixin.
app/javascript/components/components/CommentDedupBanner.vue Switch date formatting to DateFormatMixin helper and remove ad-hoc formatter.
app/javascript/components/triage/TriageQueueNav.vue Add queue navigation UI (prev/next comment + prev/next rule + jump-to dropdown).
app/javascript/components/triage/RuleContextPanel.vue Add rule context side panel with collapsible sections and related comments list.
app/javascript/components/triage/CommentTriageForm.vue Extract triage form component used by modal + split view.
app/javascript/components/triage/TriageSplitView.vue Add split-pane triage view with optimistic-lock support and admin sidebar actions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/seed_helpers.rb
Comment on lines +39 to +47
elsif is_stig
record = Stig.from_mapping(parsed)
existing = Stig.find_by(stig_id: record.stig_id)
if existing
puts " Already exists: #{existing.name} (Stig)"
return existing
end
record.xml = Nokogiri::XML(xml)
else
Comment thread spec/factories/reviews.rb
Comment on lines +29 to +40
trait :reply do
action { 'comment' }
comment { 'Reply to parent comment' }
triage_status { nil }

after(:build) do |review, _evaluator|
unless review.responding_to_review_id
parent = create(:review, :comment, user: review.user, rule: review.rule)
review.responding_to_review_id = parent.id
review.section = parent.section
end
end
Comment thread spec/factories/reviews.rb
Comment on lines +43 to +60
trait :component_comment do
action { 'comment' }
rule { nil }
section { nil }
comment { 'Component-level comment' }

after(:build) do |review|
unless review.commentable_type == 'Component'
component = create(:component, :skip_rules)
review.commentable = component
review.commentable_type = 'Component'

if review.user
project = component.project
create(:membership, user: review.user, membership: project, role: 'viewer') unless Membership.exists?(user: review.user, membership: project)
end
end
end
Comment thread spec/factories/reviews.rb
Comment on lines +119 to +129
trait :duplicate do
triage_status { 'duplicate' }

after(:build) do |review|
review.triage_set_by ||= create(:user)
review.triage_set_at ||= Time.current
unless review.duplicate_of_review_id
target = create(:review, :comment, rule: review.rule, user: review.user)
review.duplicate_of_review_id = target.id
end
end
Comment on lines +103 to +135
comments: { type: Array, required: true },
currentId: { type: [Number, String], default: null },
},
computed: {
ruleGroups() {
const groups = [];
const seen = new Map();
for (const c of this.comments) {
const key = c.rule_id || `component-${c.id}`;
if (!seen.has(key)) {
const group = {
ruleId: key,
ruleName: c.rule_displayed_name || "(component)",
comments: [],
};
seen.set(key, group);
groups.push(group);
}
seen.get(key).comments.push(c);
}
return groups;
},
currentPosition() {
for (let gi = 0; gi < this.ruleGroups.length; gi++) {
const group = this.ruleGroups[gi];
for (let ci = 0; ci < group.comments.length; ci++) {
if (group.comments[ci].id === this.currentId) {
return { ruleIndex: gi, commentIndex: ci };
}
}
}
return { ruleIndex: -1, commentIndex: -1 };
},
Comment on lines +218 to +245
props: {
rows: { type: Array, required: true },
initialCommentId: { type: [Number, String], required: true },
componentId: { type: [Number, String], required: true },
effectivePermissions: { type: String, default: null },
adminPanelOpen: { type: Boolean, default: false },
contextMode: { type: String, default: "commented" },
},
data() {
return {
activeCommentId: this.initialCommentId,
isDirty: false,
saving: false,
conflictAlert: null,
adminAction: null,
adminAuditComment: "",
adminConfirmationId: "",
adminTargetRuleId: null,
};
},
computed: {
sortedRows() {
return [...this.rows].sort((a, b) => a.id - b.id);
},
activeComment() {
return this.sortedRows.find((r) => r.id === this.activeCommentId) || null;
},
canTriage() {
Comment thread spec/lib/seed_helpers_spec.rb Outdated
Comment on lines +59 to +67
it 'creates a review comment using FactoryBot' do
review = described_class.find_or_seed_review(
rule: rule, user: user, section: 'check_content',
comment: 'Test comment for seed helper'
)
expect(review).to be_persisted
expect(review.action).to eq('comment')
expect(review.triage_status).to eq('pending')
end
Comment on lines +338 to +374
async onTriageSave(decision) {
await this.doSave(decision, false);
},
async onTriageSaveAndNext(decision) {
await this.doSave(decision, true);
},
async doSave(decision, advance) {
if (!this.activeComment) return;
this.saving = true;
this.conflictAlert = null;
try {
const payload = {
triage_status: decision.triage_status,
expected_updated_at: this.activeComment.updated_at,
};
if (decision.response_comment) {
payload.response_comment = decision.response_comment;
}
if (decision.triage_status === "duplicate") {
payload.duplicate_of_review_id = decision.duplicate_of_review_id;
}

const triageRes = await axios.patch(`/reviews/${this.activeComment.id}/triage`, payload);
this.$emit("triaged", triageRes.data.review);

if (triageRes.data.response_review) {
this.$emit("response-posted", {
parentId: this.activeComment.id,
responseReview: triageRes.data.response_review,
});
}

if (!SINGLE_BUTTON_STATUSES.has(decision.triage_status)) {
const adjRes = await axios.patch(`/reviews/${this.activeComment.id}/adjudicate`, {});
this.$emit("adjudicated", adjRes.data.review);
}

@wdower

wdower commented May 20, 2026

Copy link
Copy Markdown
Contributor

Some notes after review:

  • Does the new Triage Queue modal show the version of the field as it was when the comment was left, or the field as it is right now? i.e. if someone comments on the status of a requirement, and then an author changes that status, does the user see the old version or the new version of the status in the triage queue?
  • Does the Triage Queue's requirement fields show if they are in locked status? In general does the requirement shown in the Triage Queue show the same information as is seen in the regular component view?
  • The "Commented/All Fields" buttons in the top right of Triage Queue should be a single toggle that is closer to the requirement that it changes.
  • The admin buttons don't really need their own pullout modal. They should be a series of buttons right under the comment box that ONLY appear for an admin user.
  • The "Jump To. . ." button doesn't make it obvious what we're jumping to. It jumps to other comments in the triage queue. Not sure how yet but we want to rethink how we approach this feature because it looks messy when the dropdown opens up.
  • Can we consider changing the name of Triage Table to just be Comments Table (and making its URL the component's /comments endpoint?) Since the actual "Triage" part of Triage Table now only happens in the Triage Queue.

Some side notes about locked fields:

  • Can we make sure all roles can comment on locked fields? A comment allows people to argue about whether a field is set correctly but still restricts people's ability to change it.
  • Can we ensure that the Reviewer role is capable of locking fields, and not just Admin?

@aaronlippold
aaronlippold temporarily deployed to vulcan-feat-comment-tri-92jytj May 20, 2026 03:57 Inactive
@aaronlippold
aaronlippold temporarily deployed to vulcan-feat-comment-tri-92jytj May 20, 2026 04:17 Inactive
@wdower

wdower commented May 20, 2026

Copy link
Copy Markdown
Contributor

That 3NF doc design is a big enough change to warrant its own PR. Going to cherry pick it and make a new PR off main.

@aaronlippold
aaronlippold temporarily deployed to vulcan-feat-comment-tri-92jytj May 20, 2026 05:29 Inactive
@aaronlippold
aaronlippold temporarily deployed to vulcan-feat-comment-tri-92jytj May 20, 2026 05:36 Inactive
@aaronlippold
aaronlippold temporarily deployed to vulcan-feat-comment-tri-92jytj May 20, 2026 05:46 Inactive
@aaronlippold
aaronlippold temporarily deployed to vulcan-feat-comment-tri-92jytj May 20, 2026 06:01 Inactive
@aaronlippold
aaronlippold temporarily deployed to vulcan-feat-comment-tri-92jytj May 20, 2026 06:04 Inactive
@aaronlippold
aaronlippold temporarily deployed to vulcan-feat-comment-tri-92jytj May 20, 2026 06:06 Inactive
@aaronlippold
aaronlippold temporarily deployed to vulcan-feat-comment-tri-92jytj May 20, 2026 06:08 Inactive
@aaronlippold

Copy link
Copy Markdown
Member Author

@wdower — heads up, this PR is still in progress. All your review feedback items from the first round are addressed (admin inline, toggle moved, rename, locked comments, staleness badge, etc.) but I still have a few cards to finish:

  • BenchmarkViewer three-column migration (ec3)
  • SRG sidebar highlight fix (nzv)
  • Show-resolved toggle (ngm)
  • CHANGELOG

Will push these in the next session. Don't need a full re-review yet — just wanted you to know the status.

— Aaron

@aaronlippold
aaronlippold temporarily deployed to vulcan-feat-comment-tri-92jytj May 20, 2026 12:34 Inactive
@aaronlippold
aaronlippold temporarily deployed to vulcan-feat-comment-tri-92jytj May 20, 2026 12:50 Inactive
@aaronlippold
aaronlippold temporarily deployed to vulcan-feat-comment-tri-92jytj May 20, 2026 13:07 Inactive
@aaronlippold
aaronlippold temporarily deployed to vulcan-feat-comment-tri-92jytj May 20, 2026 13:20 Inactive
@aaronlippold
aaronlippold temporarily deployed to vulcan-feat-comment-tri-92jytj May 20, 2026 13:37 Inactive
@aaronlippold
aaronlippold temporarily deployed to vulcan-feat-comment-tri-92jytj May 20, 2026 13:45 Inactive
@aaronlippold
aaronlippold temporarily deployed to vulcan-feat-comment-tri-92jytj May 20, 2026 13:49 Inactive
@aaronlippold
aaronlippold temporarily deployed to vulcan-feat-comment-tri-92jytj May 20, 2026 13:56 Inactive
@aaronlippold
aaronlippold temporarily deployed to vulcan-feat-comment-tri-92jytj May 20, 2026 13:57 Inactive
@aaronlippold
aaronlippold temporarily deployed to vulcan-feat-comment-tri-92jytj May 20, 2026 14:01 Inactive
@aaronlippold aaronlippold changed the title feat: split-pane triage context panel + seed modernization + DRY centralization feat: three-column triage workspace + progress bar + DRY color palette + accessibility May 20, 2026
aaronlippold added a commit that referenced this pull request May 20, 2026
Added/Changed/Fixed sections covering three-column layout,
progress bar, DRY color palette, ARIA accessibility,
InfoTooltip/InfoNotice adoption, and bug fixes

Authored by: Aaron Lippold<lippold@gmail.com>
@aaronlippold
aaronlippold temporarily deployed to vulcan-feat-comment-tri-92jytj May 20, 2026 19:01 Inactive
The stored XCCDF document is authoritative and backup restore
re-derives rows from it — a row whose columns disagree with its own
header would silently change identity across a backup cycle (the
package-registry lesson: derive the index from the artifact, and
check it).

- The three header composition formulas (V{v}R{r} version string,
  display name, benchmark-id underscoring) each get ONE shared class
  method; SRG and STIG from_mapping and the XCCDF exporter call them.
  Fixes a latent bug in BOTH from_mapping copies: single-character
  reads truncated multi-digit versions (V10R12 displayed as
  "Ver 1, Rel 2")
- Xccdf::BenchmarkHeader: SAX header reader that stops at the first
  Profile or Group — identity fields without the rule-bulk parse
- Consistency validation on create/xml change: srg_id, title, and
  version columns must match the stored header; mismatch is a
  rejection naming both values
- skip_rule_import intent flag on the after_create import — the
  release path populates catalog rows from the release copy, never
  the XML import; the factory :skip_rules trait now uses the real
  flag and stamps its header to match its attributes (raising loudly
  if the fixture ever drifts)
- Spec support cleanup the invariant forced into the open: the
  global skip_callback hacks in the search spec leaked across
  parallel workers (54 apparent failures from one bug) — replaced
  with per-instance factory mechanics; legacy odd-version rows are
  now simulated via update_column; the deletable-SRG contract row is
  factory-built instead of hand-edited onto another document's XML

Authored by: Aaron Lippold<lippold@gmail.com>
In this domain the swept word means NIST 800-53 control groupings
and nothing else — the remaining uses in export spec prose, one
formatter comment, and the mailer spec requirements block are
reworded. Behavior untouched; the enforcement assertions and the
NIST control usage are the only remaining occurrences.

Authored by: Aaron Lippold<lippold@gmail.com>
The authored generator carried the catalog row's own document id onto
generated requirement rows (a dup artifact — set_rule_id skips
non-blank ids), defeating the local numbering design: largest_rule_id's
TO_NUMBER, working display names, and relocation renumbering all
assume six-digit ordinals.

Numbers are assigned explicitly in the generation loop — the bulk
import validates every row before any insert, so the per-row callback
would hand them all the same next number. The catalog rows are read in
canonical order so numbering is deterministic and follows the source
document's requirement order, the same convention the stig import path
uses; a reverse-insertion spec pins that (it fails under insertion- or
database-order numbering).

Authored by: Aaron Lippold<lippold@gmail.com>
The V-/SV-/F-/C- compositions for draft-published requirements lived
inline in the XCCDF formatter and again in the InSpec control tags.
PublishedIdentifiers is now the one home; both emitters call it. The
byte corpus proves the XCCDF emission identical; the InSpec specs pin
the tag values.

Why one home matters: the basing import joins a document's Rule
element ids against catalog rule_id columns — emission and the release
catalog copy must agree byte-for-byte, so the formula may exist
exactly once.

Authored by: Aaron Lippold<lippold@gmail.com>
ReleaseAttachmentService lands the release the registry way, in one
transaction: mint identifiers, generate the published XCCDF, create
the catalog SecurityRequirementsGuide with columns derived FROM that
artifact (the stored document is authoritative; the header-consistency
validation enforces the match), then populate rules through the
release copy. The released entry is shaped exactly like an uploaded
SRG — basing and is-latest work unchanged, proven by round-trip specs.

- Component integer version/release surface as catalog V{v}R{r}
  through compose-then-extract; a component missing either is refused,
  never defaulted
- The catalog copy now stamps each row's rule_id with the published
  Rule element id — the join key the basing import matches against
  the parsed document (uploaded shape all the way down)
- ReleaseCopyService exposes publishable_rows and
  component_validation_errors: one selection and one component-side
  check shared by minting, export, and the copy
- Release changelog: structured removals data from executed relocation
  records (identifier, destination abbreviation, executed date) plus a
  plain-text renderer; storage and serving belong to the endpoint

Authored by: Aaron Lippold<lippold@gmail.com>
- useRelocations now owns the labelled destination list: endpoint rows
  named by SRG (with next-release suffix) merged with outside-Vulcan
  marker abbreviations, deduped and abbreviation-sorted
- proposalDestinationOptions excludes the component's own SRG for the
  propose picker; the backlog keeps it (it is the receiver's inbox)
- consumers render the shared options instead of deriving their own:
  the backlog panel's local tokenOptions and both editor pages'
  own-token filters are deleted
- FilterDropdown renders disabled with its placeholder when it has
  zero options instead of opening an empty menu
- both editor pages eagerly fetch destinations at created so the
  vocabulary is present before any panel opens
- specs pin the merged labelling, the sort order, the propose subset,
  the zero-options guard, and the view-page fetch wiring

Authored by: Aaron Lippold<lippold@gmail.com>
- POST /components/:id/release (author-authorized) runs the whole
  release in one transaction: preflight blockers, mint identifiers,
  generate the published XCCDF, attach the catalog entry, copy rows,
  then flip released — a failure anywhere rolls everything back
- the response toast carries the new catalog SRG and the structured
  changelog (removals from executed relocations); blockers return a
  422 toast listing every unmet requirement
- Component gains the via_release_flow intent flag and a model guard:
  an srg-kind component can no longer be released by a plain
  released:true update, which would have stranded it outside the
  catalog; the flag is not mass-assignable
- useConfirmRelease kind-routes the existing Release button: srg goes
  through the new endpoint, stig keeps the PATCH path unchanged
- OpenAPI path + ComponentReleaseResponse schema documented and
  bundled, with a contract test validating the live response shape
- request specs cover success, blockers, authz, and the guard; the
  two existing specs that built post-release fixtures via plain
  updates now set the intent flag explicitly

Authored by: Aaron Lippold<lippold@gmail.com>
- BackupSerializer emitted attribution only from the user associations,
  so a review whose author was already on the imported columns (prior
  import without a local match, or a destroyed user) exported blank —
  and the importer then skipped the row entirely; attribution died on
  the second export/import round trip
- serialize_review now derives each of the three attribution pairs
  (commenter, triager, adjudicator) through one fallback: the linked
  user wins; the *_imported_email/_name snapshot fills in when the FK
  is nil; archive keys unchanged
- specs pin all three fallbacks with exact values, the linked-user
  precedence, and a full export-then-import round trip proving the
  carried attribution lands back on the imported columns

Authored by: Aaron Lippold<lippold@gmail.com>
- the core SRGs are defined — exactly three, DISA-published (Operating
  System Core, Network Core, Application Core); core-ness is recognition
  of those documents, never a user designation, and nothing in the
  product could set the flag before this
- SecurityRequirementsGuide gains CORE_SRG_IDS (the three benchmark ids
  read from the real documents) and one-directional recognition on
  create: a matching id flags the record core, a non-matching id never
  unsets an explicitly-set core — the model is the one seam, so upload,
  seeds, and every other creation path recognize alike
- the three documents land as standard seeds; the seed-file inventory
  guard's pinned list grows to match
- an idempotent data backfill flags rows that predate recognition, on
  any release of the three documents (benchmark ids are stable across
  releases); migration spec pins the flip, the derived-row guard, the
  idempotent re-run, and the intentionally irreversible down

Authored by: Aaron Lippold<lippold@gmail.com>
- One-off, plan-driven pure carrier: replays every planned review
  comment onto the redo component's rows with imported attribution,
  provenance markers, and original-commentable lineage; writes no
  requirement states
- Rows insert through Review.insert! and re-validate against the
  import-integrity contract; threads carry in two passes so replies
  land under their carried parents; replies ride with their thread
  regardless of plan listing (the thread is the carry unit)
- Labeled research notes ride alongside team comments so reviewers can
  tell the transition context apart from the original commentary
- Re-run refusal via the provenance guard; rake entry point takes the
  plan path and target component id and reports source -> target per
  row with counts and skip reasons
- 8 service specs cover attribution, threading, zero-state writes,
  planned-ids-only, re-run refusal, full report shape including the
  target-not-found branch, and the srg-kind guard; independently
  review-verified

Authored by: Aaron Lippold<lippold@gmail.com>
The sidebar SRG ID column blanked for authored SRG requirements: the
serializer never exposed srg_id, so the identifier reached the editor
header (which reads version) but never the requirement list.

Serve srg_id from the row's own version so requirement lists stay
kind-agnostic across document kinds. Schema, bundle, and the contract
example move with it.

Authored by: Aaron Lippold<lippold@gmail.com>
A not-applicable SRG requirement showed only the status and its
justification editor, so the author had to switch the requirement back
to applicable to read what was being excluded.

Title, severity, the reference identifiers, and the vulnerability
discussion now render read-only beside the editable fields, keeping the
spirit of the requirement in view while the justification is written.

Authored by: Aaron Lippold<lippold@gmail.com>
The sidebar comment indicator becomes a button that opens the full
comment list for that requirement. Adding a comment or replying hands
off to the page's existing composer, so both editor surfaces keep one
composer instance and one posting path.

Each sidebar row also carries a status dot. Colors come from a new
rule-status design-system layer that mirrors the triage tints: one
data-attribute block per status, core tokens only, neutral fallback for
unrecognized values. Nested rows resolve their status from the loaded
requirements, since satisfaction references carry none.

Authored by: Aaron Lippold<lippold@gmail.com>
The question "which SRG requirement does this correspond to?" was being
answered by callers reaching through the association themselves, so the
same derivation was written out in nine places across models, blueprints
and the export serializer. Each copy had to know that a STIG rule keeps
that answer on its source requirement while an authored SRG requirement
holds it directly, which is exactly the sort of knowledge that ends up
diverging.

The hierarchy answers it once instead: BaseRule declares srg_identifier
and refuses a default, and each subclass supplies its own. The callers
just ask. A blueprint that had already drifted from the others now gets
the same answer as the rest, which is what let the sidebar sort on a
field the serializer actually serves.

Authored by: Aaron Lippold<lippold@gmail.com>
Each toggle was declared in four unsynchronized places: a defaults
object, a restorable-keys list for persistence, the active-filter count,
and the bar's own markup. Adding a toggle meant editing all four, and
when requirements gained a second document kind the toggles that do not
apply to it were left switched on and silently inert, because nothing
described which kind a toggle belongs to.

One registry now describes each toggle once, in three groups, with the
status group generated from the runtime vocabulary rather than restated.
The bar renders whatever the registry declares, persistence and the
active count read it, and a toggle that does not apply to the current
kind renders disabled with a tooltip saying why instead of pretending
to work. A second filter pipeline that had gone dead, with search
semantics that no longer matched the live one, is removed rather than
left to be found later.

Also folds in the sidebar sort, which shares this file: it keys on the
identifier the serializer actually sends instead of one it never did,
so the SRG sort applies to the rows the user sees.

Authored by: Aaron Lippold<lippold@gmail.com>
The sidebar took a share of a twelve-column grid, so its width tracked
the viewport while its content did not: a requirement identifier is a
fixed string, and at the widths people actually work at the column was
too narrow for it. The list had been compensating by shrinking its own
font, which is a workaround for the sizing being wrong.

Panels are now sized by role. A sidebar holds content of known width and
gets a bounded token from the design system; everything else divides
what is left. That is the same mechanism Bootstrap uses for its own
columns, with a measured token instead of a percentage, so the font
compensation is gone and no consumer passes a column count any more.

The floor is measured against the worst row the feature can produce, not
the common one: a nested parent carrying a chevron, status dot,
identifier, child-count badge and the icon strip. It also covers the
panel's own chrome, including the scroll gutter -- a browser drawing
overlay scrollbars takes no width, but a classic scrollbar takes about
seventeen pixels out of the same panel and the list always scrolls. A
floor measured only under overlay scrollbars cut six of thirteen
identifiers once a classic one was forced. The trailing icon strip is
also pinned against shrinking, so it can no longer be squeezed into
wrapping and doubling the row height.

Authored by: Aaron Lippold<lippold@gmail.com>
The test was named for all three places a requirement row renders its
trailing icons, but its fixture built only two of them and asserted the
count was greater than zero. One site had no coverage while the name
claimed otherwise, and a lower bound would have passed even if a row
rendered a shrinkable strip — which is the row that wrapped and caused
the height regression in the first place.

It now builds an open rule, an all-rules parent and a nested child in
one wrapper and pins the count at exactly three, reusing the setup an
adjacent test in the same file already established.

Authored by: Aaron Lippold<lippold@gmail.com>
Opening a component asked the database for thousands of rows one at a
time. The editor issued 6045 queries and took ten seconds; a component
twice the size cost roughly twice as much, so the pages got slower as
teams did more work in them.

Nothing described what a serializer reads. Each field was free to reach
through an association while it rendered, and because the output is
identical either way, no test could see it happening. The eager-loading
that was meant to prevent this could only act on dependencies that were
declared, and almost none were.

Every field that reaches for data now says so where it is defined, and
the fetch plan is derived from the serializer itself rather than
restated by callers, so the two cannot drift apart. Three sources
accounted for nearly all of it: reply counts and comment authors,
resolved per comment; the source requirement and owning component
behind each satisfaction link, resolved per link; and the audit trail.

The audit trail is different in kind. It is assembled per record rather
than read from an association, so it cannot be fetched for a collection
in one query no matter how it is declared — and it is only ever shown
for the single requirement a user has open. It is now sent when asked
for: the per-requirement endpoint asks, a component's rules list does
not. The sidebar already fetched a requirement's detail when selecting
one that lacked it; the component view page emitted that request but
had nobody listening, so it gained a handler.

  /components/:id/edit    6045 queries / 9934 ms -> 62 / 1636 ms
  /components/122         2196 queries / 3826 ms -> 38 /  348 ms
  /components/:id/triage  1619 queries           -> 62 / 1017 ms

Counts no longer grow with requirements, comments or satisfaction
links; a test pins each of those axes by holding the other two equal.
Serialized output is byte-identical for both document kinds and both
views, so nothing consuming the API sees a change — except the audit
trail leaving the collection, which the contract test now asserts is
absent so it cannot quietly return.

Authored by: Aaron Lippold<lippold@gmail.com>
The component copy path built its id-mapping CTE by interpolating each
id pair into a VALUES list. The values were integer primary keys, so the
statement was never actually injectable, but the SQL text changed with
every row count and the scanner could not tell the difference — which is
why four suppressions had accumulated around it.

Pass the ids as two bigint[] parameters and let unnest zip them back
into rows. Every statement in the copy path is now a fixed string with
no interpolation, so it also stops defeating statement caching.

Drops the four suppressions the old construct required; the remaining
one covers a separate file still using the older pattern.

Authored by: Aaron Lippold<lippold@gmail.com>
auth = request.headers['Authorization']
return nil unless auth

match = auth.match(/\AToken\s+(.+)\z/i)
Brings in the image CVE refresh (#744): activestorage 8.0.5.1, loofah
2.25.2, rails-html-sanitizer 1.7.1, oj 3.17.5, Ruby 3.4.10, plus the
Dockerfile and CI changes that came with it. bundler-audit is clean
again, which unblocks the branch's pipeline.

Conflicts resolved by taking the better side of each, not one side
throughout:

.tool-versions — Ruby 3.4.10 from master, but Node 24 from this branch.
Master's file said 22.13.1 while its own Dockerfile builds 24.18.0; the
Dockerfile is what actually runs, so the branch value is the correct one.
Worth fixing on master too.

Gemfile — both sides kept: this branch's climate_control, has_scope and
pagy alongside master's faraday.

Gemfile.lock — based on this branch's lock, with only the four gems
bundler-audit flagged updated on top. Basing on master's lock instead
looks equivalent but silently reverts rack 3.2.6 to 2.2.23, and this
branch relies on the 3.1 status-code names in 72 places; that route
fails 162 examples.

yarn.lock — regenerated from this branch's package.json. Master's axios
pins do not carry over because this branch replaced axios with ky.

Full backend suite 4109 green, JS 3613 green, build, lint, Brakeman and
bundler-audit all clean.

Authored by: Aaron Lippold<lippold@gmail.com>

- name: Publish OpenAPI spec to Scalar registry
run: |
npx @scalar/cli auth login --token "$SCALAR_TOKEN"

- name: Publish OpenAPI spec to Scalar registry
run: |
npx @scalar/cli auth login --token "$SCALAR_TOKEN"
# the Rails app's Vue 2). Install only docs deps in CI.
cd docs
yarn install
yarn install --frozen-lockfile
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0

- name: Install root dependencies (for redocly CLI)
run: yarn install --frozen-lockfile
A dual-lineage component lost its secondary source on restore — the
archive itself was incomplete, so no later restore could recover it.
The backup now serializes the full declared parent set the same
portable way based_on travels (srg_id plus version, never a database
id), and the restore rebuilds the join rows through the parent-set
seam so the membership validations see them on first save.

The manifest was part of the same defect: pre-flight validation and
include_srg bundling both read only the singular based_on, so a
missing secondary surfaced after the component was already built and
a bundled archive was not self-contained for its secondary. Both now
cover every source. A wholly-absent secondary warns instead of
failing the restore — unlike based_on, its absence costs one lineage
link while everything else restores intact.

The key is additive with no format-version bump: deployed validators
hard-reject unknown versions, and readers branch on key presence, not
version — the same compatibility signal document_type uses. Archives
from before the key restore unchanged, and a malformed value falls
back to based_on alone.

Authored by: Aaron Lippold<lippold@gmail.com>
The disposition matrix queried requirements through the Rule STI
subclass at three sites, so every SRG requirement's comments were
silently missing from the export — and a fourth site surfaced when the
end-to-end test ran: the row builder resolved its requirement through
the Rule-typed association, which is nil for authored requirements,
and crashed on the first SRG comment. All four now go through the
kind-agnostic seams: the live-requirements scope for the queries, and
Review#requirement — the model's own preferred resolution — for the
row builder.

A regression guard pins the invariant across the four
triage/comment/disposition files: any reintroduced STI-subclass query
fails with the offending file and line. Only live code is matched, so
prose documenting the anti-pattern by name stays legal.

Comments on soft-deleted requirements now leave the export — the
seam's documented semantics, pinned by test so the exclusion stays a
decision rather than an accident.

Authored by: Aaron Lippold<lippold@gmail.com>
Settings is a process-wide singleton, and these specs mutated four
consent keys while restoring only one — the leaked String version
failed the Settings-defaults type pins whenever example randomization
ran them first in a worker. The banner contexts had the same shape,
silently leaking colors and text.

Every mutation now captures its originals and restores them all, so a
yml default change can never diverge from the restore. The only other
spec mutating Settings directly already reloads after each example.

Authored by: Aaron Lippold<lippold@gmail.com>
Find scoped its search through the Rule STI association, so it
returned nothing for SRG components. Only the base scope was actually
kind-specific — every searched column lives on base_rules and both
child tables key on base_rule, so one field list serves both kinds —
and it now routes through the requirements seam. The action also
re-queried the component its before_action had already loaded; the
dead lookup is gone.

Blueprint selection moves to ComponentBlueprint.requirement_blueprint,
the one seam both full-collection serialization and find pick their
blueprint through, so matches serialize in the shape their kind's
editor consumes. The find path's response schema becomes the same
oneOf the component editor response already uses, and the searched
requirements now preload their blueprint plan.

Authored by: Aaron Lippold<lippold@gmail.com>
The compare endpoint diffs exactly one field — inspec_control_file —
and SRG requirements carry no InSpec. An SRG or mixed-kind pair now
guards with a clear 422 instead of diffing two empty sets and
reporting every requirement unchanged. The existing 4XX ErrorResponse
wildcard already covers the response shape.

The revision-history diff walked the Rule-typed collection, so SRG
component versions compared as two empty sets and every history entry
showed no changes. It now traverses the kind-agnostic requirement set
via a shared requirement_diff_fields helper; basic_fields is hoisted
to BaseRule (row-local check and fix text) and Rule keeps its
satisfaction-aware override. The satisfied_by eager-load stays on the
STIG branch only, where the association exists.

- Request specs cover SRG-vs-SRG and mixed-kind compare 422s
- Request spec pins the SRG history diff: added and updated
  requirements surface with base/diff titles

Authored by: Aaron Lippold<lippold@gmail.com>
Component destroy collected rule ids with a Rule-typed query, so an
SRG component's authored SrgRules never entered the bulk cleanup and
deletion fell to the per-row cascade. The SQL log showed the STIG
path was no better off: destroy loaded the full eager graph and
destroy! re-walked the stale in-memory rules association per row
after the bulk deletes — and that accidental re-walk was the only
thing cleaning references, which has no database foreign key.

- Collect ids via BaseRule.unscoped so both kinds (and tombstoned
  rows) take the same fast path
- Load destroy with the basic loader — nothing about deleting needs
  the eager graph, and the stale association was the re-walk's cause
- Add RequirementRelocation source-side rows (their foreign key
  restricts deletes) and Reference rows (no foreign key at all) to
  the bulk cleanup list
- Pin both kinds with query-count invariance specs: the destroy query
  count must not scale with the requirement count, the only honest
  observable separating bulk from cascade

Authored by: Aaron Lippold<lippold@gmail.com>
Brakeman's 60-day EOL window for Rails 8.0 (support ends 2026-10-07)
opened today and fails every CI run and push. The Rails 8.1 upgrade
is planned master-first; this acknowledged-warning entry bridges the
gap and comes out when that upgrade merges into this branch.

Authored by: Aaron Lippold<lippold@gmail.com>
@sonarqubecloud

sonarqubecloud Bot commented Aug 8, 2026

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

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement Pull requests that add a new feature urgent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants