First implementation of qbit-qa-capture 0.1.0: optional, provider-pluggable QA bug capture (Jam first), built to the shared qa-capture contract v1.0.0 that qqq-frontend-next implements.
Tables (qa_capture_settings, qa_capture_audience, qa_capture_sanitizer_rule, qa_capture_report) via db/liquibase/qbit-qa-capture/qa_capture.changelog.sql: 50 built-in sanitizer rules seeded insert-only, one guarded FK (audience to settings, cascade), runnable as an app DB user.
Permissionsqa_capture.manage, qa_capture.use_qa_mode, qa_capture.view_reports; app qaCapture nested under the host Admin app through parentAppName.
Enablement (QaCaptureEnablement, unit tested): AUDIENCE / QA_MODE / OFF / NOT_CONFIGURED / NO_ACCESS / QA_MODE_OFF. USER rows match the user id or email (case-insensitive). ROLE rows match withRoleResolver(...), defaulting to the session permission names. The QA mode preference section is qa-capture.
UI API extensionqa-capture 1.0.0: qaCaptureCapability and qaCaptureReportCreate. The fragment is committed at src/main/resources/openapi/qqq-v1/qa-capture.yaml, with a contract test.
Request ids:X-Request-Id on /qqq/* and /qa/*, plus the log4j ThreadContext key requestId and QaCaptureRequestIds.current().
/qa/recorder: Jam connected-domain page with a hash-based CSP and no-store; it answers 404 when capture is off. Tested so that the route wins over the Next dashboard fallback.
Jam webhook/qqq/hooks/qa-capture/jam: Svix verification (constant time, 5-minute tolerance, several signatures), 1 MB cap, idempotent on jamId.
QaCaptureCsp.customize(headers[, qInstance]): adds the dashboard CSP origins only while capture is active.
Secured-data rules: at startup, adds rules for fields marked isSensitive or with a field-level PII/PHI classification. It uses core metadata only and does not depend on qbit-secured-data.
Help bundle with a coverage test. Guardrail tests check that every table is used, seeded or explained, and documented.
Additive notes on the contract
The qa_capture_audience table has an extra source column (ADMIN or QA_MODE). Without a core key-value preference store, QA mode is stored as a QA_MODE row. These rows never grant audience membership.
The nullable providerConfig and sanitizer fields use allOf plus nullable in the fragment, because OpenAPI ignores siblings of $ref.
Tests
./mvnw verify passes: 35 tests, 94% instruction coverage, all classes covered. The suite includes PostgreSQL through Testcontainers (run as a non-superuser) and the JS sanitizers executed in Node.
## Summary
First implementation of `qbit-qa-capture` 0.1.0: optional, provider-pluggable QA bug capture (Jam first), built to the shared `qa-capture` contract v1.0.0 that `qqq-frontend-next` implements.
- **Tables** (`qa_capture_settings`, `qa_capture_audience`, `qa_capture_sanitizer_rule`, `qa_capture_report`) via `db/liquibase/qbit-qa-capture/qa_capture.changelog.sql`: 50 built-in sanitizer rules seeded insert-only, one guarded FK (audience to settings, cascade), runnable as an app DB user.
- **Permissions** `qa_capture.manage`, `qa_capture.use_qa_mode`, `qa_capture.view_reports`; app `qaCapture` nested under the host Admin app through `parentAppName`.
- **Enablement** (`QaCaptureEnablement`, unit tested): AUDIENCE / QA_MODE / OFF / NOT_CONFIGURED / NO_ACCESS / QA_MODE_OFF. USER rows match the user id or email (case-insensitive). ROLE rows match `withRoleResolver(...)`, defaulting to the session permission names. The QA mode preference section is `qa-capture`.
- **UI API extension** `qa-capture` 1.0.0: `qaCaptureCapability` and `qaCaptureReportCreate`. The fragment is committed at `src/main/resources/openapi/qqq-v1/qa-capture.yaml`, with a contract test.
- **Request ids:** `X-Request-Id` on `/qqq/*` and `/qa/*`, plus the log4j ThreadContext key `requestId` and `QaCaptureRequestIds.current()`.
- **`/qa/recorder`:** Jam connected-domain page with a hash-based CSP and `no-store`; it answers 404 when capture is off. Tested so that the route wins over the Next dashboard fallback.
- **Jam webhook** `/qqq/hooks/qa-capture/jam`: Svix verification (constant time, 5-minute tolerance, several signatures), 1 MB cap, idempotent on `jamId`.
- **`QaCaptureCsp.customize(headers[, qInstance])`:** adds the dashboard CSP origins only while capture is active.
- **Secured-data rules:** at startup, adds rules for fields marked `isSensitive` or with a field-level PII/PHI classification. It uses core metadata only and does not depend on qbit-secured-data.
- **Help bundle** with a coverage test. Guardrail tests check that every table is used, seeded or explained, and documented.
## Additive notes on the contract
- The `qa_capture_audience` table has an extra `source` column (ADMIN or QA_MODE). Without a core key-value preference store, QA mode is stored as a QA_MODE row. These rows never grant audience membership.
- The nullable `providerConfig` and `sanitizer` fields use `allOf` plus `nullable` in the fragment, because OpenAPI ignores siblings of `$ref`.
## Tests
`./mvnw verify` passes: 35 tests, 94% instruction coverage, all classes covered. The suite includes PostgreSQL through Testcontainers (run as a non-superuser) and the JS sanitizers executed in Node.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
First implementation of
qbit-qa-capture0.1.0: optional, provider-pluggable QA bug capture (Jam first), built to the sharedqa-capturecontract v1.0.0 thatqqq-frontend-nextimplements.qa_capture_settings,qa_capture_audience,qa_capture_sanitizer_rule,qa_capture_report) viadb/liquibase/qbit-qa-capture/qa_capture.changelog.sql: 50 built-in sanitizer rules seeded insert-only, one guarded FK (audience to settings, cascade), runnable as an app DB user.qa_capture.manage,qa_capture.use_qa_mode,qa_capture.view_reports; appqaCapturenested under the host Admin app throughparentAppName.QaCaptureEnablement, unit tested): AUDIENCE / QA_MODE / OFF / NOT_CONFIGURED / NO_ACCESS / QA_MODE_OFF. USER rows match the user id or email (case-insensitive). ROLE rows matchwithRoleResolver(...), defaulting to the session permission names. The QA mode preference section isqa-capture.qa-capture1.0.0:qaCaptureCapabilityandqaCaptureReportCreate. The fragment is committed atsrc/main/resources/openapi/qqq-v1/qa-capture.yaml, with a contract test.X-Request-Idon/qqq/*and/qa/*, plus the log4j ThreadContext keyrequestIdandQaCaptureRequestIds.current()./qa/recorder: Jam connected-domain page with a hash-based CSP andno-store; it answers 404 when capture is off. Tested so that the route wins over the Next dashboard fallback./qqq/hooks/qa-capture/jam: Svix verification (constant time, 5-minute tolerance, several signatures), 1 MB cap, idempotent onjamId.QaCaptureCsp.customize(headers[, qInstance]): adds the dashboard CSP origins only while capture is active.isSensitiveor with a field-level PII/PHI classification. It uses core metadata only and does not depend on qbit-secured-data.Additive notes on the contract
qa_capture_audiencetable has an extrasourcecolumn (ADMIN or QA_MODE). Without a core key-value preference store, QA mode is stored as a QA_MODE row. These rows never grant audience membership.providerConfigandsanitizerfields useallOfplusnullablein the fragment, because OpenAPI ignores siblings of$ref.Tests
./mvnw verifypasses: 35 tests, 94% instruction coverage, all classes covered. The suite includes PostgreSQL through Testcontainers (run as a non-superuser) and the JS sanitizers executed in Node.🤖 Generated with Claude Code