From f5c093145885525aa42c0248fcd6c8d1391b1235 Mon Sep 17 00:00:00 2001 From: NeuroKoder3 Date: Sat, 1 Aug 2026 19:00:30 -0500 Subject: [PATCH 1/2] docs(validation): resolve duplicate requirement ids, complete v1.2.1 package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The chart filing requirements added for 1.2.1 were numbered TT-R137–R142, which collides with the pre-existing cross-cutting TT-R140–R142. Two different requirements shared each of three ids, so any trace through them was ambiguous. The collision survived review because neither document is wrong when read on its own. Renumbered to TT-R150–R155, which is safe to do now: no site has executed an OQ against these ids yet. Section 4 of the SRS also had the new requirements inserted mid-table, leaving TT-R079 orphaned after TT-R142. Restored to numeric order and split the new work into §4.1 (IOTA pipeline) and §4.2 (chart filing). Fills in the rest of the package for the 1.2.1 functionality: - SDS §11–§16 covering migration safety, diagnostics and PHI redaction, the IOTA notification pipeline, chart filing, dependency vulnerability exceptions, and renderer bridge integrity. Appended rather than interleaved so existing §-references in the matrix stay valid. Each section records why the design is as it is — why free text is withheld rather than filtered, why the idempotency key excludes the content hash, why the chart-filing transport is injected rather than resolved internally. - 32 OQ test cases, written as steps a site can actually execute in a non-PHI environment. The one that matters most for R-023: plant a recognisable name in free text, export a default bundle, and search the file for it. - Risk register R-020 to R-027. R-013's mitigation was revised because transactional rollback does not cover a multi-migration sequence that fails after an earlier migration has already committed; the pre-migration copy does. - Two requirements for controls that existed without one: TT-R144 (dependency exception gate) and TT-R145 (bridge and packaging integrity). Co-authored-by: Cursor --- docs/compliance/RISK_REGISTER.md | 20 +- .../SOFTWARE_DESIGN_SPECIFICATION.md | 219 +++++++++++++++++- .../SYSTEM_REQUIREMENTS_SPECIFICATION.md | 36 ++- docs/compliance/TRACEABILITY_MATRIX.md | 97 +++++--- .../templates/OQ_PROTOCOL_TEMPLATE.md | 59 +++++ 5 files changed, 386 insertions(+), 45 deletions(-) diff --git a/docs/compliance/RISK_REGISTER.md b/docs/compliance/RISK_REGISTER.md index 971b3e5..fbd3b9c 100644 --- a/docs/compliance/RISK_REGISTER.md +++ b/docs/compliance/RISK_REGISTER.md @@ -3,8 +3,16 @@ | Document control | | |---|---| | Document ID | TT-RISK-001 | -| Version | 1.0 | +| Version | 1.1 | | Status | Baseline — to be extended by deploying organization | +| Applies to software version | 1.2.1 | + +## Revision history + +| Ver | Change | Rationale | +|---|---|---| +| 1.0 | Baseline. | Initial issue. | +| 1.1 | Added R-020 to R-027. Revised the mitigation for R-013. | New hazards arising from the IOTA notification pipeline, chart filing, and diagnostics export added in software version 1.2.1. R-013 was revised because its original mitigation — transactional rollback — does not cover a multi-migration sequence that fails partway, which is now addressed by a verified pre-migration copy. | ## Severity scale @@ -53,10 +61,18 @@ Mitigations move risk to **Acceptable** when residual risk is **Low** or | R-010 | Vulnerable bundled component (Electron, SQLite, Node) | 2 | A | Quarterly dependency scan; security-advisory monitoring; release notes call out CVE remediations. | C | Med (Acceptable) | Engineering | | R-011 | Insider exfiltration via export | 2 | B | Every export logs file path, user, request_id; admin can disable exports per role. | D | Low | Engineering + Customer | | R-012 | Power loss during write corrupts database | 2 | C | SQLite WAL + synchronous=FULL; integrity check at startup; automatic backup. | D | Low | Engineering | -| R-013 | Migration fails mid-way leaving DB in inconsistent state | 2 | D | Migrations wrapped in transactions; failed migration rolled back atomically. | E | Low | Engineering | +| R-013 | Migration fails mid-way leaving DB in inconsistent state | 2 | D | Migrations wrapped in transactions; failed migration rolled back atomically. A verified pre-migration copy is taken before any pending migration runs, and migration is refused outright if that copy cannot be written, so a sequence that fails after an earlier migration has already committed is still recoverable. The failure reports the schema version reached and the copy's path. | E | Low | Engineering | | R-014 | Cross-org data leak in multi-tenant deployment | 1 | D | All queries scoped by `org_id`; UNIQUE constraints include `org_id`. | E | Low | Engineering | | R-015 | Brute-force online password attack | 2 | B | Account lockout after 5 failed attempts × 15 min; rate-limit middleware on auth IPC. | D | Low | Engineering | | R-016 | Phishing of MFA TOTP code | 2 | B | TOTP step skew limited to ±1; backup codes single-use; admin notified on backup-code use. | D | Med (Acceptable) | Customer training | | R-017 | OPTN-style export mistakenly submitted to UNet | 2 | C | Export filename and CSV header carry "DO_NOT_SUBMIT" watermark; in-app modal warns. | D | Low | Product | | R-018 | Living donor follow-up windows missed (OPTN Policy 14) | 2 | B | Follow-up tasks auto-generated at 6 / 12 / 24 months; overdue tasks escalate. | D | Low | Engineering | | R-019 | TransTrack mistakenly classified by FDA as a device | 2 | C | `FDA_DEVICE_RATIONALE.md` documents non-device positioning; UI labels and disclaimers reinforce. | D | Med (Acceptable) | Product + Legal | +| R-020 | Statutory IOTA notification deadline missed, leaving a patient unaware they cannot receive organ offers | 2 | B | The obligation is created in the same operation as the status transition, so it cannot be forgotten; the due date derives from the transition's effective timestamp rather than the generation time; overdue obligations are surfaced on the compliance summary. Where configuration is incomplete the transition is still recorded and reported as unmet. | D | Med (Acceptable) | Customer Admin + Engineering | +| R-021 | Duplicate copy of the same notice filed into the patient's chart, causing clinician confusion about which is current | 3 | C | Idempotency key identifies the obligation (transition + notice kind + revision), not the rendered content, so a retry cannot produce a second document; `UNIQUE(org_id, idempotency_key)` enforces it at the database. Superseding requires an explicit revision increment. A notice already filed is not filed again. | E | Low | Engineering | +| R-022 | Notice filed to the wrong patient's chart, disclosing PHI into another patient's record | 1 | D | The DocumentReference subject is derived from the notification's own patient reference rather than from UI selection state; filing re-verifies the notice body against its recorded content hash before transmission; dry-run mode allows the resource to be inspected before any live filing is enabled at a site. | E | Low | Engineering | +| R-023 | Support bundle carries PHI out of the safeguarded environment via a support ticket | 2 | B | Free-text values are withheld rather than filtered, because a name in prose cannot be reliably detected; structured PHI is redacted by key and by pattern; the no-PHI claim is tested adversarially against deliberately PHI-laden input. Including free text requires an explicit request, is recorded inside the bundle, and relabels it as requiring PHI handling. Export is admin-only and audit-logged. | D | Low | Engineering + Customer | +| R-024 | Notice content altered after filing, so the record no longer matches what the patient received | 2 | D | The rendered body and its content hash are frozen at generation by database trigger; reprint verifies body against hash and reports any mismatch. | E | Low | Engineering | +| R-025 | Hospital-authored notice template omits a content element required by § 512.442(d) | 2 | B | Templates are validated at configuration time against all five required elements and rejected if any is missing or an unrecognised placeholder is used. The organ-offer-eligibility statement is system-supplied and not editable through template configuration. | D | Low | Engineering | +| R-026 | A documented dependency-vulnerability exception becomes a permanent, unreviewed suppression | 2 | C | Exceptions carry a `reviewBy` date and the release gate fails once it passes; the gate also fails on an undocumented finding, on a severity increase beyond what the exception assessed, and on an exception that no longer matches any real finding. | D | Med (Acceptable) | Engineering | +| R-027 | A feature works in development but is unwired in the packaged build, failing first in front of a clinician | 3 | B | Every `api..()` call in the renderer is checked against the real preload surface by automated test; the source entry point is guarded against being overwritten by a build artifact; the release gate verifies the installer version matches the source version. | D | Low | Engineering | diff --git a/docs/compliance/SOFTWARE_DESIGN_SPECIFICATION.md b/docs/compliance/SOFTWARE_DESIGN_SPECIFICATION.md index 6c1b0a0..dac7be7 100644 --- a/docs/compliance/SOFTWARE_DESIGN_SPECIFICATION.md +++ b/docs/compliance/SOFTWARE_DESIGN_SPECIFICATION.md @@ -3,8 +3,16 @@ | Document control | | |---|---| | Document ID | TT-SDS-001 | -| Version | 1.0 | +| Version | 1.1 | | Status | Baseline | +| Applies to software version | 1.2.1 | + +## Revision history + +| Ver | Change | Rationale | +|---|---|---| +| 1.0 | Baseline. | Initial issue. | +| 1.1 | Added §11 migration safety, §12 diagnostics and PHI redaction, §13 IOTA notification pipeline, §14 chart filing, §15 dependency vulnerability exceptions, §16 renderer bridge integrity. Extended §4 with the new tables. | Design record for the functionality added in software version 1.2.1. Sections are appended rather than interleaved so that existing §-references in the traceability matrix remain valid. | ## 1. Architecture overview @@ -67,6 +75,7 @@ See `electron/database/schema.cjs` for the authoritative DDL. Core entities: * `audit_logs` (immutable), `access_justification_logs` * `user_mfa`, `user_password_history`, `siem_destinations` * `inactivation_predictions`, `outcomes_snapshots`, `srtr_metrics`, `tasks` +* `waitlist_status_transitions` (append-only), `iota_notifications` (see §13) All PHI tables are scoped by `org_id`. Indexes enforce the lookup pattern. @@ -135,3 +144,211 @@ Authenticator, Authy, and 1Password. * React components in `.jsx` (ESM). * All side-effecting handlers route through `shared.validateSession()` first. * Migrations are forward-only and idempotent. + +## 11. Schema migration safety + +`electron/database/migrationSafety.cjs` wraps `runMigrations()` so that an +upgrade always has a restore point. + +``` +init.cjs + └─ runMigrationsSafely(db, { dbPath, backupDir, logger }) + ├─ ensureMigrationsTable(db) // fresh installs have no table yet + ├─ getPendingMigrations(db) + │ └─ none pending ──────────────► run nothing, take no copy + ├─ createPreMigrationBackup() // SQLCipher backup API, then verify + │ └─ copy or verification fails ─► ABORT, database untouched + ├─ runMigrations(db) + │ └─ throws ────────────────────► rethrow with { reachedVersion, backupPath } + └─ pruneOldBackups() // keep 5, secure-delete the rest +``` + +Design decisions: + +* **The copy is conditional.** Taking it only when migrations are pending keeps + ordinary startup fast and avoids accumulating identical copies of the database + on every launch. +* **Failure to back up is failure to migrate.** `createPreMigrationBackup()` + verifies the written file before migrations are allowed to proceed. A migration + that cannot be undone is a worse outcome than a deferred upgrade, so the + design fails closed (TT-R085). +* **The error carries the remedy.** A migration failure rethrows with the schema + version actually reached and the absolute path of the copy, so the operator is + not left to guess which of several backups predates the failure (TT-R086). +* **Retention is bounded and erasure is secure.** `MAX_PRE_MIGRATION_BACKUPS = 5`, + files named `transtrack-premigration-*`, older ones removed through + `secureDelete.cjs` because they are full copies of PHI (TT-R087). + +## 12. Diagnostics: PHI redaction and support bundles + +Two modules cooperate. `electron/services/phiRedaction.cjs` is the single source +of truth for scrubbing; `electron/services/logger.cjs` delegates to it so that a +log file and a support bundle cannot drift apart in what they consider sensitive. + +| Concern | Mechanism | +|---|---| +| Structured PHI | `redactValue()` walks objects to arbitrary depth and replaces values whose key matches `PHI_KEYS`. | +| Patterned PHI in text | `redactText()` matches SSN, MRN, e-mail, phone, and date-of-birth shapes. | +| Free text | Withheld entirely — see below. | + +**Free text is withheld, not filtered.** A patient name embedded in prose ("call +Zephyrina about her labs") cannot be reliably detected by pattern or key, and a +redactor that mostly works is worse than none because it invites the reader to +trust the output. `supportBundle.cjs` therefore replaces free-text values — +log `message` bodies, notes, descriptions — with `[FREE_TEXT_OMITTED]` plus a +character count, so support can see that content existed and how much +(TT-R126). `includeFreeText: true` overrides this, and the bundle then records +that choice in its own `redactionPolicy` and is labelled as requiring PHI +handling; the bundle never claims to be PHI-free when it is not (TT-R127). + +`PHI_KEYS` deliberately excludes the bare key `name`. Over-redaction has its own +failure mode: a bundle that has scrubbed migration names, component names and +version strings is useless for diagnosis, and useless diagnostics stop being +collected. Specific identifiers (`first_name`, `last_name`, `patient_name`) +remain redacted. + +`collectBundle()` gathers each section behind its own error boundary, so a +failing subsystem yields a bundle with one section marked in error rather than no +bundle at all — the case where diagnostics matter most is precisely when +something is broken. `readLogTail()` opens the log once and measures the +descriptor with `fstat` rather than stat-ing the path and then opening it, +because `logger.cjs` rotates those same files and a check-then-open sequence can +lose that race. + +Export is admin-only and audit-logged, including whether free text was included +(TT-R128). `src/pages/SystemHealth.jsx` presents component health, schema +version, and the export control. + +## 13. CMS IOTA notification pipeline + +Implements the patient-notification duty in CMS IOTA Model § 512.442(d). The +design separates an immutable record of *what happened* from a mutable record of +*what we did about it*. + +``` +waitlist status change + └─ iotaNoticeService.recordTransition() + ├─ INSERT waitlist_status_transitions (append-only; DB triggers) + └─ offer-eligibility impact blocks offers? + └─ yes ─► create notification obligation in the same operation + └─ iotaNoticeGenerator.generateNotice() (pure) + └─ INSERT iota_notifications +``` + +**Layering.** `iotaNoticeGenerator.cjs` is a pure function: it takes a +transition, a patient, a centre and a template, and returns rendered content plus +derived metadata. It performs no I/O, which is what makes determinism testable +(TT-R079). `iotaNoticeService.cjs` owns persistence, delivery state and +reporting. + +**Templates are the hospital's, not the vendor's.** The transplant hospital +supplies the notice language; `EXAMPLE_TEMPLATE` is a starting point that is +never applied implicitly (TT-R077). `validateTemplate()` rejects a template +missing any of the five statutory content elements or referencing an unknown +placeholder, and it does so at configuration time rather than when a patient's +notice is due (TT-R131). The statement that organ offers cannot be received +while inactive is system-supplied via `OFFER_ELIGIBILITY_STATEMENT` and cannot be +edited through template configuration, because it is the one sentence the rule +prescribes in substance. + +**Deadlines derive from the record, not the clock.** `NOTICE_DUE_DAYS = 10` and +`ANNUAL_DUE_DAYS = 365` are applied to the transition's effective timestamp, so +regenerating a notice later cannot move a deadline. + +**Idempotency identifies the obligation, not the document.** The key is +`transitionId:noticeKind:r{revision}` and deliberately excludes both +`generatedAt` and the content hash. Keying on content would let a retry whose +letterhead date has rolled over hash differently and file a *second* copy of the +same notice into the patient's chart — the exact outcome the +`UNIQUE(org_id, idempotency_key)` constraint exists to prevent. Superseding a +filed notice is therefore an explicit act: increment `options.revision` +(TT-R075). + +**The transition survives a configuration gap.** If no usable template exists, +the transition is still recorded and the obligation is reported as unmet. The +transition establishes when the statutory clock started, so discarding it +because a notice could not be produced would destroy the only evidence of the +deadline (TT-R130). + +**Partial discharge is visible.** A notice delivered to the patient but not +copied to a required dialysis facility, or delivered but not filed to the chart, +is reported as incompletely addressed rather than done. `decorate()` sets +`secondaryRecipientUnknown` where the duty exists but no recipient is on record +(TT-R134). + +Immutability is enforced at the database: `iota_notifications` permits lifecycle +columns (delivery, secondary notification, chart filing) to change while a +trigger rejects any change to the transition reference, notice kind, content +hash, generator version, due date, generation timestamp, or idempotency key +(TT-R074). Migration 18 added `content` and `template_sha256` so a filed notice +can be reproduced and verified against its frozen hash (TT-R135). + +## 14. Chart filing (FHIR R4 DocumentReference) + +`electron/services/chartFiling.cjs` turns a generated notice into a FHIR R4 +`DocumentReference` for the patient's chart. Three modes: + +| Mode | Behaviour | +|---|---| +| `dry_run` | Build and validate the resource; transmit nothing; return it for inspection. | +| `fhir_documentreference` | Build, validate, and submit through a caller-supplied transport. | +| `manual` | Record that the copy was filed by another route (interface engine, HL7 `MDM^T02`, or by hand). | + +**The transport is injected, never resolved internally.** `fileNotice()` takes a +`submit` function as an argument. There is no configuration value that turns on +outbound transmission by itself (TT-R153). This keeps the offline-first claim +verifiable by inspection: a reviewer reading `chartFiling.cjs` can see that the +module cannot reach the network on its own, and the sole caller that supplies a +real transport is visible in the IPC layer. + +**Filing refuses to proceed on a content mismatch.** `buildDocumentReference()` +re-hashes the stored notice body and compares it to the recorded hash, so a +tampered or truncated notice is never filed (TT-R151). + +`dry_run` exists because a site's Epic organisation must complete four +enablement steps before `DocumentReference.Create` will succeed. Dry run lets a +pilot demonstrate the whole path — generation, validation, resource shape — +before that work lands, rather than blocking pilot readiness on a third party +(TT-R152). `manual` covers sites that will never get FHIR write access and file +through an interface engine instead (TT-R155). + +Failures are recorded with their cause and remain retryable; a notice already +filed is not filed again (TT-R154). The document type defaults to LOINC +`74213-0` (`SUGGESTED_TYPE_CODING`) and is overridable per site, because document +type catalogues are configured per Epic organisation. + +Client-side write support (`fhirPost`, `createDocumentReference`) lives in +`server/src/integrations/epic/client.js`. `fhirPost` does not retry: +re-POSTing a document whose response was lost risks duplicating it in the chart, +so retry is a decision for the caller with the idempotency key in hand, not a +default of the transport. `system/DocumentReference.write` is excluded from the +default scope set and must be added deliberately. + +## 15. Dependency vulnerability exceptions + +`scripts/audit-with-exceptions.mjs` runs `npm audit` and reconciles its findings +against `security/vulnerability-exceptions.json`, which follows the CycloneDX VEX +vocabulary. The gate fails when a finding is undocumented, when an exception has +passed its `reviewBy` date, when a finding's severity has risen above what the +exception assessed, or when an exception no longer matches any real finding. + +The last two conditions matter as much as the first. An exception that has gone +stale is a claim nobody has checked, and an expiry date that does not fail the +build is not an expiry date. The mechanism is designed to make "we assessed this +and it does not affect us" an auditable, decaying statement rather than a +permanent suppression. + +## 16. Renderer bridge integrity + +The renderer↔main seam is the one place where a mistake compiles cleanly, passes +unit tests, and fails only in a packaged build — a call to `api.x.y()` that no +preload method backs is a runtime error the developer never sees in `dev` mode. +`tests/rendererBridgeCoverage.test.mjs` statically collects every +`api..()` in the renderer and checks it against the *real* +surface of `electron/preload.cjs`, loaded by injecting a fake `electron` module. +Verifying against the genuine preload rather than a hand-maintained list is the +point: a stub would drift and re-open the gap it exists to close. + +`tests/buildEntryIntegrity.test.mjs` asserts that the source `index.html` still +loads `/src/main.jsx` and carries no hashed build-output references, guarding +against a build artifact overwriting the source entry point. diff --git a/docs/compliance/SYSTEM_REQUIREMENTS_SPECIFICATION.md b/docs/compliance/SYSTEM_REQUIREMENTS_SPECIFICATION.md index 04c0b9e..72d319c 100644 --- a/docs/compliance/SYSTEM_REQUIREMENTS_SPECIFICATION.md +++ b/docs/compliance/SYSTEM_REQUIREMENTS_SPECIFICATION.md @@ -3,8 +3,16 @@ | Document control | | |---|---| | Document ID | TT-SRS-001 | -| Version | 1.0 | +| Version | 1.1 | | Status | Baseline | +| Applies to software version | 1.2.1 | + +## Revision history + +| Ver | Change | Rationale | +|---|---|---| +| 1.0 | Baseline. | Initial issue. | +| 1.1 | Added §4.1 IOTA notification pipeline (TT-R129–R136), §4.2 chart filing (TT-R150–R155), migration safety (TT-R084–R087), and system health and support bundles (TT-R124–R128). Chart filing requirements renumbered from TT-R137–R142 to TT-R150–R155. | New functionality in software version 1.2.1. The renumbering removes a collision with the pre-existing cross-cutting requirements TT-R140–R142, which is corrected before any site executes an OQ against these IDs. | Each requirement has a unique ID `TT-Rxxx`, a category, a priority (M=Mandatory, S=Should, C=Could), and a verification method (R=Review, T=Test, I=Inspection, @@ -70,6 +78,12 @@ D=Demonstration). All `M` requirements must trace to at least one OQ test case. | TT-R076 | M | The system shall identify notification records that are past their due date and not yet delivered. | T | | TT-R077 | M | Notice content shall be generated from a per-organisation template supplied by the transplant hospital. The system shall not apply vendor-authored notice language implicitly: no template shall be applied unless it has been explicitly configured for the organisation. | T | | TT-R078 | M | The system shall refuse to render a notice template that omits any of the five content elements required by CMS IOTA Model § 512.442(d) — inactive-since date, reason for the change, statement that organ offers cannot be received while inactive, reactivation instructions, and hospital contact information — or that references an unrecognised placeholder. The statement that organ offers cannot be received shall be system-supplied and shall not be alterable through template configuration. | T | +| TT-R079 | M | Notice generation shall be deterministic — identical inputs shall yield identical content and content hash — and every derived value shall follow from the patient record rather than the generating environment: the 10-day and annual due dates from the transition's effective timestamp, and the required secondary recipient from the patient's ESRD status (dialysis facility for ESRD, referring provider otherwise), refusing generation when that status is unknown. | T | + +### 4.1 IOTA notification pipeline (OP-IOTA) + +| ID | Pri | Requirement | Verify | +|---|---|---|---| | TT-R129 | M | Recording a waitlist status transition whose offer-eligibility impact blocks organ offers shall create the corresponding notification obligation in the same operation, so that the duty cannot be created without a tracked deadline. | T | | TT-R130 | M | Where notice configuration is incomplete, the system shall nevertheless record the status transition and report the notification obligation as unmet. The transition record establishes when the statutory clock started and shall never be discarded because a notice could not be produced. | T | | TT-R131 | M | The system shall reject a notice template at configuration time if it omits a required content element, so that the defect is surfaced when it can be corrected rather than when a patient's notice is due. | T | @@ -78,13 +92,17 @@ D=Demonstration). All `M` requirements must trace to at least one OQ test case. | TT-R134 | M | Where a notice carries a duty to copy a dialysis facility or referring provider but no recipient is recorded for the patient, the system shall flag the obligation as incompletely addressed rather than presenting it as discharged. | T | | TT-R135 | M | The rendered notice body shall be retained and shall remain verifiable against its frozen content hash, so that a filed notice can be reproduced for the patient or a surveyor and any post-hoc alteration is detectable. | T | | TT-R136 | M | All IOTA notification channels shall be scoped to the authenticated user's organisation, restricted by role (configuration to administrators; obligation and delivery writes to administrators and coordinators; read access additionally to physicians and regulators), and every write shall be audit-logged. | T | -| TT-R137 | M | The system shall record a copy of each notice in the patient's medical record via a FHIR R4 DocumentReference, and shall report a notice that has been delivered but not filed as a partially met obligation. | T | -| TT-R138 | M | The system shall refuse to file a notice whose stored body does not match its recorded content hash. | T | -| TT-R139 | M | The system shall support filing in dry-run mode, in which the DocumentReference is constructed and validated but not transmitted, so that readiness can be demonstrated before a site's Epic organisation has enabled document creation. | T | -| TT-R140 | M | The system shall not transmit to an external endpoint unless a transport is explicitly supplied by the caller; no configuration value alone shall enable outbound clinical document transmission. | T | -| TT-R141 | M | A failed chart filing shall be recorded with its cause and shall remain retryable; a notice already filed shall not be filed again. | T | -| TT-R142 | M | The system shall support recording a filing performed by another route (manual or interface engine) so that a site without FHIR write access can evidence a discharged obligation. | T | -| TT-R079 | M | Notice generation shall be deterministic — identical inputs shall yield identical content and content hash — and every derived value shall follow from the patient record rather than the generating environment: the 10-day and annual due dates from the transition's effective timestamp, and the required secondary recipient from the patient's ESRD status (dialysis facility for ESRD, referring provider otherwise), refusing generation when that status is unknown. | T | + +### 4.2 Chart filing (OP-CF) + +| ID | Pri | Requirement | Verify | +|---|---|---|---| +| TT-R150 | M | The system shall record a copy of each notice in the patient's medical record via a FHIR R4 DocumentReference, and shall report a notice that has been delivered but not filed as a partially met obligation. | T | +| TT-R151 | M | The system shall refuse to file a notice whose stored body does not match its recorded content hash. | T | +| TT-R152 | M | The system shall support filing in dry-run mode, in which the DocumentReference is constructed and validated but not transmitted, so that readiness can be demonstrated before a site's Epic organisation has enabled document creation. | T | +| TT-R153 | M | The system shall not transmit to an external endpoint unless a transport is explicitly supplied by the caller; no configuration value alone shall enable outbound clinical document transmission. | T | +| TT-R154 | M | A failed chart filing shall be recorded with its cause and shall remain retryable; a notice already filed shall not be filed again. | T | +| TT-R155 | M | The system shall support recording a filing performed by another route (manual or interface engine) so that a site without FHIR write access can evidence a discharged obligation. | T | ## 5. Performance and reliability (PR) @@ -128,3 +146,5 @@ D=Demonstration). All `M` requirements must trace to at least one OQ test case. | TT-R141 | M | The system shall not transmit PHI to any external host unless explicitly enabled in settings. | T,I | | TT-R142 | M | The system shall log a unique request_id for every IPC call and propagate it into audit and SIEM events. | T | | TT-R143 | M | The system shall provide an "About" dialog stating the regulatory design alignment (not certification). | I | +| TT-R144 | M | The release gate shall fail on any dependency vulnerability that is not covered by a documented exception, whose severity exceeds what its exception assessed, or whose exception has passed its review date. An exception that no longer matches a real finding shall also fail the gate, so that the exception set cannot silently diverge from the dependency tree. | T | +| TT-R145 | M | Every renderer call to an inter-process API shall be verified against the actual bridge surface exposed by the main process, and the release gate shall fail if the packaged application's version does not match the source version. | T | diff --git a/docs/compliance/TRACEABILITY_MATRIX.md b/docs/compliance/TRACEABILITY_MATRIX.md index 9c5fe78..6f6dc57 100644 --- a/docs/compliance/TRACEABILITY_MATRIX.md +++ b/docs/compliance/TRACEABILITY_MATRIX.md @@ -1,8 +1,14 @@ # TransTrack Traceability Matrix -Maps every Mandatory requirement from `SYSTEM_REQUIREMENTS_SPECIFICATION.md` to its +Maps every requirement from `SYSTEM_REQUIREMENTS_SPECIFICATION.md` to its design (`SOFTWARE_DESIGN_SPECIFICATION.md`), the implementing module(s), and the -verification artifact (test or OQ test case). +verification artifact (test or OQ test case). Requirements not implemented in the +current software version are listed with their status rather than omitted, so +that the gap is visible rather than inferred from an absence. + +`scripts/check-compliance-docs.mjs` enforces the cross-references below: unique +requirement ids, a matrix row for every requirement, a verification artifact for +every Mandatory requirement, and resolvable SDS, OQ and risk references. | Req ID | Design § | Implementation | Verification | |---|---|---|---| @@ -15,6 +21,7 @@ verification artifact (test or OQ test case). | TT-R007 | §9 | `electron/services/passwordPolicy.cjs` | `tests/passwordPolicy.test.cjs` | | TT-R008 | §2 | `src/components/session/IdleTimeoutManager.jsx` | OQ-08 | | TT-R009 | §2, §4 | `electron/database/schema.cjs` (users.role) | OQ-09 | +| TT-R010 | §3 | Not implemented — deferred beyond 1.2.1. The customer IdP is trusted for primary authentication only where SSO is deployed; TOTP remains the TransTrack-issued factor. | Deferred; no verification artifact in this version. | | TT-R020 | §7 | `electron/ipc/shared.cjs` (logAudit) | `tests/services.test.cjs` | | TT-R021 | §7 | `electron/ipc/shared.cjs` | `tests/services.test.cjs` | | TT-R022 | §7 | `electron/database/schema.cjs` (triggers) | `tests/auditImmutability.test.cjs` | @@ -38,46 +45,68 @@ verification artifact (test or OQ test case). | TT-R068 | §4 | `electron/services/livingDonor.cjs`, `electron/ipc/handlers/livingDonor.cjs` | `tests/livingDonor.test.cjs` | | TT-R069 | §4 | `electron/services/hl7v2.cjs` | `tests/hl7v2.test.cjs` | | TT-R070 | §4 | `electron/services/optnExport.cjs` | `tests/optnExport.test.cjs` | -| TT-R071 | §4 | `electron/database/schema.cjs` (waitlist_status_transitions) | `tests/iotaNotifications.test.cjs` | -| TT-R072 | §7 | `electron/database/schema.cjs` (createWaitlistTransitionTriggers) | `tests/iotaNotifications.test.cjs` | -| TT-R073 | §4 | `electron/database/schema.cjs` (iota_notifications) | `tests/iotaNotifications.test.cjs` | -| TT-R074 | §7 | `electron/database/schema.cjs` (iota_notifications_frozen_fields) | `tests/iotaNotifications.test.cjs` | -| TT-R075 | §4 | `electron/database/schema.cjs` (iota_notifications.idempotency_key), `electron/services/iotaNoticeGenerator.cjs` (idempotencyKey) | `tests/iotaNotifications.test.cjs`, `tests/iotaNoticeGenerator.test.cjs` | -| TT-R076 | §4 | `electron/database/schema.cjs` (idx_iota_notif_org_due) | `tests/iotaNotifications.test.cjs` | -| TT-R077 | §4 | `electron/services/iotaNoticeGenerator.cjs` (generateNotice, EXAMPLE_TEMPLATE) | `tests/iotaNoticeGenerator.test.cjs` | -| TT-R078 | §4 | `electron/services/iotaNoticeGenerator.cjs` (validateTemplate, REQUIRED_TOKENS, OFFER_ELIGIBILITY_STATEMENT) | `tests/iotaNoticeGenerator.test.cjs` | -| TT-R079 | §4 | `electron/services/iotaNoticeGenerator.cjs` (render, resolveSecondaryRecipient) | `tests/iotaNoticeGenerator.test.cjs` | -| TT-R129 | §4 | `electron/services/iotaNoticeService.cjs` (recordTransition) | `tests/iotaNoticeService.test.cjs` | -| TT-R130 | §4 | `electron/services/iotaNoticeService.cjs` (recordTransition, getComplianceSummary) | `tests/iotaNoticeService.test.cjs` | -| TT-R131 | §4 | `electron/services/iotaNoticeService.cjs` (saveConfig) | `tests/iotaNoticeService.test.cjs` | -| TT-R132 | §4 | `electron/services/iotaNoticeService.cjs` (markDelivered, decorate) | `tests/iotaNoticeService.test.cjs` | -| TT-R133 | §4 | `electron/services/iotaNoticeService.cjs` (getComplianceSummary); `src/pages/IotaCompliance.jsx` | `tests/iotaNoticeService.test.cjs` | -| TT-R134 | §4 | `electron/services/iotaNoticeService.cjs` (decorate.secondaryRecipientUnknown) | `tests/iotaNoticeService.test.cjs` | -| TT-R135 | §4 | `electron/database/migrations.cjs` (v18); `electron/services/iotaNoticeService.cjs` (contentIntegrityOk) | `tests/iotaNoticeService.test.cjs` | -| TT-R136 | §4 | `electron/ipc/handlers/iota.cjs` (requireRole, logAudit) | `tests/iotaNoticeService.test.cjs`; `tests/rendererBridgeCoverage.test.mjs` | -| TT-R137 | §4 | `electron/services/chartFiling.cjs` (buildDocumentReference); `electron/services/iotaNoticeService.cjs` (fileToChart) | `tests/chartFiling.test.cjs` | -| TT-R138 | §4 | `electron/services/chartFiling.cjs` (hash check in buildDocumentReference) | `tests/chartFiling.test.cjs` | -| TT-R139 | §4 | `electron/services/chartFiling.cjs` (prepareFiling, mode `dry_run`) | `tests/chartFiling.test.cjs` | -| TT-R140 | §4 | `electron/services/chartFiling.cjs` (injected `submit`); `electron/ipc/handlers/iota.cjs` | `tests/chartFiling.test.cjs` | -| TT-R141 | §4 | `electron/services/chartFiling.cjs` (fileNotice error path); `iotaNoticeService.fileToChart` | `tests/chartFiling.test.cjs` | -| TT-R142 | §4 | `electron/services/chartFiling.cjs` (mode `manual`) | `tests/chartFiling.test.cjs` | +| TT-R071 | §4, §13 | `electron/database/schema.cjs` (waitlist_status_transitions) | `tests/iotaNotifications.test.cjs`; OQ-71 | +| TT-R072 | §7, §13 | `electron/database/schema.cjs` (createWaitlistTransitionTriggers) | `tests/iotaNotifications.test.cjs`; OQ-72 | +| TT-R073 | §4, §13 | `electron/database/schema.cjs` (iota_notifications) | `tests/iotaNotifications.test.cjs`; OQ-73 | +| TT-R074 | §7, §13 | `electron/database/schema.cjs` (iota_notifications_frozen_fields) | `tests/iotaNotifications.test.cjs`; OQ-74 | +| TT-R075 | §13 | `electron/database/schema.cjs` (iota_notifications.idempotency_key), `electron/services/iotaNoticeGenerator.cjs` (idempotencyKey) | `tests/iotaNotifications.test.cjs`, `tests/iotaNoticeGenerator.test.cjs`; OQ-75 | +| TT-R076 | §13 | `electron/database/schema.cjs` (idx_iota_notif_org_due) | `tests/iotaNotifications.test.cjs`; OQ-76 | +| TT-R077 | §13 | `electron/services/iotaNoticeGenerator.cjs` (generateNotice, EXAMPLE_TEMPLATE) | `tests/iotaNoticeGenerator.test.cjs`; OQ-77 | +| TT-R078 | §13 | `electron/services/iotaNoticeGenerator.cjs` (validateTemplate, REQUIRED_TOKENS, OFFER_ELIGIBILITY_STATEMENT) | `tests/iotaNoticeGenerator.test.cjs`; OQ-78 | +| TT-R079 | §13 | `electron/services/iotaNoticeGenerator.cjs` (render, resolveSecondaryRecipient) | `tests/iotaNoticeGenerator.test.cjs`; OQ-79 | +| TT-R129 | §13 | `electron/services/iotaNoticeService.cjs` (recordTransition) | `tests/iotaNoticeService.test.cjs`; OQ-129 | +| TT-R130 | §13 | `electron/services/iotaNoticeService.cjs` (recordTransition, getComplianceSummary) | `tests/iotaNoticeService.test.cjs`; OQ-130 | +| TT-R131 | §13 | `electron/services/iotaNoticeService.cjs` (saveConfig) | `tests/iotaNoticeService.test.cjs`; OQ-131 | +| TT-R132 | §13 | `electron/services/iotaNoticeService.cjs` (markDelivered, decorate) | `tests/iotaNoticeService.test.cjs`; OQ-132 | +| TT-R133 | §13 | `electron/services/iotaNoticeService.cjs` (getComplianceSummary); `src/pages/IotaCompliance.jsx` | `tests/iotaNoticeService.test.cjs`; OQ-133 | +| TT-R134 | §13 | `electron/services/iotaNoticeService.cjs` (decorate.secondaryRecipientUnknown) | `tests/iotaNoticeService.test.cjs`; OQ-134 | +| TT-R135 | §13 | `electron/database/migrations.cjs` (v18); `electron/services/iotaNoticeService.cjs` (contentIntegrityOk) | `tests/iotaNoticeService.test.cjs`; OQ-135 | +| TT-R136 | §13 | `electron/ipc/handlers/iota.cjs` (requireRole, logAudit) | `tests/iotaNoticeService.test.cjs`; `tests/rendererBridgeCoverage.test.mjs`; OQ-136 | +| TT-R150 | §14 | `electron/services/chartFiling.cjs` (buildDocumentReference); `electron/services/iotaNoticeService.cjs` (fileToChart) | `tests/chartFiling.test.cjs`; OQ-150 | +| TT-R151 | §14 | `electron/services/chartFiling.cjs` (hash check in buildDocumentReference) | `tests/chartFiling.test.cjs`; OQ-151 | +| TT-R152 | §14 | `electron/services/chartFiling.cjs` (prepareFiling, mode `dry_run`) | `tests/chartFiling.test.cjs`; OQ-152 | +| TT-R153 | §14 | `electron/services/chartFiling.cjs` (injected `submit`); `electron/ipc/handlers/iota.cjs` | `tests/chartFiling.test.cjs`; OQ-153 | +| TT-R154 | §14 | `electron/services/chartFiling.cjs` (fileNotice error path); `iotaNoticeService.fileToChart` | `tests/chartFiling.test.cjs`; OQ-154 | +| TT-R155 | §14 | `electron/services/chartFiling.cjs` (mode `manual`) | `tests/chartFiling.test.cjs`; OQ-155 | | TT-R080 | §4 | `electron/database/schema.cjs` (indexes) | PQ-80 | | TT-R081 | §2 | `electron/database/init.cjs` (WAL) | PQ-81 | | TT-R082 | §2 | `electron/services/disasterRecovery.cjs` | PQ-82 | | TT-R083 | §2 | `electron/services/disasterRecovery.cjs` | PQ-83 | -| TT-R084 | §2 | `electron/database/migrationSafety.cjs` (createPreMigrationBackup, runMigrationsSafely), `electron/database/init.cjs` | `tests/migrationSafety.test.cjs` | -| TT-R085 | §2 | `electron/database/migrationSafety.cjs` (runMigrationsSafely fail-closed path) | `tests/migrationSafety.test.cjs` | -| TT-R086 | §2 | `electron/database/migrationSafety.cjs` (error backupPath/reachedVersion) | `tests/migrationSafety.test.cjs` | -| TT-R087 | §2 | `electron/database/migrationSafety.cjs` (pruneOldBackups), `electron/services/secureDelete.cjs` | `tests/migrationSafety.test.cjs` | +| TT-R084 | §2, §11 | `electron/database/migrationSafety.cjs` (createPreMigrationBackup, runMigrationsSafely), `electron/database/init.cjs` | `tests/migrationSafety.test.cjs`; OQ-84 | +| TT-R085 | §11 | `electron/database/migrationSafety.cjs` (runMigrationsSafely fail-closed path) | `tests/migrationSafety.test.cjs`; OQ-85 | +| TT-R086 | §11 | `electron/database/migrationSafety.cjs` (error backupPath/reachedVersion) | `tests/migrationSafety.test.cjs`; OQ-86 | +| TT-R087 | §11 | `electron/database/migrationSafety.cjs` (pruneOldBackups), `electron/services/secureDelete.cjs` | `tests/migrationSafety.test.cjs`; OQ-87 | | TT-R120 | §7 | `electron/ipc/auditReportHandler.cjs` | OQ-120 | -| TT-R124 | §7 | `src/pages/SystemHealth.jsx`, `electron/services/healthCheck.cjs` | `tests/healthCheck.test.cjs`, `tests/rendererBridgeCoverage.test.mjs` | -| TT-R125 | §7 | `electron/services/supportBundle.cjs` (collectBundle, writeBundle), `electron/ipc/handlers/operations.cjs` (support:exportBundle) | `tests/supportBundle.test.cjs` | -| TT-R126 | §3 | `electron/services/supportBundle.cjs` (withholdFreeText, skeletonLogLine), `electron/services/phiRedaction.cjs` | `tests/supportBundle.test.cjs` | -| TT-R127 | §3 | `electron/services/supportBundle.cjs` (redactionPolicy, includeFreeText), `src/pages/SystemHealth.jsx` | `tests/supportBundle.test.cjs` | -| TT-R128 | §7 | `electron/ipc/handlers/operations.cjs` (support:exportBundle RBAC + logAudit) | `tests/supportBundle.test.cjs`, `tests/rbacMatrix.test.cjs` | +| TT-R124 | §7, §12 | `src/pages/SystemHealth.jsx`, `electron/services/healthCheck.cjs` | `tests/healthCheck.test.cjs`, `tests/rendererBridgeCoverage.test.mjs`; OQ-124 | +| TT-R125 | §12 | `electron/services/supportBundle.cjs` (collectBundle, writeBundle), `electron/ipc/handlers/operations.cjs` (support:exportBundle) | `tests/supportBundle.test.cjs`; OQ-125 | +| TT-R126 | §3, §12 | `electron/services/supportBundle.cjs` (withholdFreeText, skeletonLogLine), `electron/services/phiRedaction.cjs` | `tests/supportBundle.test.cjs`; OQ-126 | +| TT-R127 | §3, §12 | `electron/services/supportBundle.cjs` (redactionPolicy, includeFreeText), `src/pages/SystemHealth.jsx` | `tests/supportBundle.test.cjs`; OQ-127 | +| TT-R128 | §7, §12 | `electron/ipc/handlers/operations.cjs` (support:exportBundle RBAC + logAudit) | `tests/supportBundle.test.cjs`, `tests/rbacMatrix.test.cjs`; OQ-128 | | TT-R121 | §10 | `electron/database/migrations.cjs` | OQ-121 | | TT-R122 | §2 | `electron/services/encryptionKeyManagement.cjs` | OQ-122 | +| TT-R123 | §4 | `electron/services/optnExport.cjs`, `electron/ipc/handlers/optnExport.cjs` | `tests/optnExport.test.cjs`; OQ-70 | +| TT-R100 | §4 | `electron/functions/validateFHIRData.cjs`, `server/src/routes/fhir.js` | `server/test/integration/fhir.test.mjs`, `server/test/unit/fhirCapability.test.mjs` | +| TT-R101 | §4 | `electron/ipc/handlers/hl7.cjs`, `electron/services/hl7Ingest.cjs` | `tests/hl7Ingest.test.cjs`; OQ-69 | | TT-R140 | §10 | input validators in `electron/ipc/handlers/entities.cjs` | OQ-140 | | TT-R141 | §3 | `electron/main.cjs` (CSP, no remote) | OQ-141 (network capture) | | TT-R142 | §10 | `electron/ipc/handlers.cjs` (request_id) | OQ-142 | | TT-R143 | §2 | `electron/main.cjs` About menu | OQ-143 | +| TT-R144 | §15 | `scripts/audit-with-exceptions.mjs`, `security/vulnerability-exceptions.json` | `tests/auditExceptions.test.mjs`; OQ-144 | +| TT-R145 | §16 | `tests/rendererBridgeCoverage.test.mjs`, `tests/buildEntryIntegrity.test.mjs`, `scripts/release-readiness-check.mjs` (installer version check) | `tests/rendererBridgeCoverage.test.mjs`, `tests/buildEntryIntegrity.test.mjs`; OQ-145 | + +## Risk linkage + +Maps the hazards in `RISK_REGISTER.md` to the requirements that control them. +Requirement groups added in software version 1.2.1: + +| Risk | Controlled by | +|---|---| +| R-013 Migration fails mid-way | TT-R084, TT-R085, TT-R086, TT-R087 | +| R-020 Missed IOTA notification deadline | TT-R073, TT-R076, TT-R079, TT-R129, TT-R130, TT-R133 | +| R-021 Duplicate notice filed to chart | TT-R075, TT-R154 | +| R-022 Notice filed to the wrong chart | TT-R151, TT-R152 | +| R-023 Support bundle carries PHI out | TT-R126, TT-R127, TT-R128 | +| R-024 Notice altered after filing | TT-R074, TT-R135, TT-R151 | +| R-025 Template omits a statutory element | TT-R077, TT-R078, TT-R131 | +| R-026 Vulnerability exception becomes permanent | TT-R144 | +| R-027 Feature unwired in packaged build | TT-R145 | diff --git a/docs/compliance/templates/OQ_PROTOCOL_TEMPLATE.md b/docs/compliance/templates/OQ_PROTOCOL_TEMPLATE.md index 0675419..ede80a0 100644 --- a/docs/compliance/templates/OQ_PROTOCOL_TEMPLATE.md +++ b/docs/compliance/templates/OQ_PROTOCOL_TEMPLATE.md @@ -65,6 +65,63 @@ against the running build. OQ is executed in a **non-PHI** test environment. | OQ-69 | Ingest sample HL7 v2 ADT^A01 message. | Patient created or updated. | | | | OQ-70 | Generate OPTN-style export. | CSV produced; filename and header carry "DO_NOT_SUBMIT" watermark. | | | +## Waitlist status transitions and CMS IOTA notices + +Executed against test patients only. Where a step requires a past date, set the +transition's effective date rather than changing the workstation clock. + +| ID | Step | Expected | Pass/Fail | Evidence | +|---|---|---|---|---| +| OQ-71 | Change a test patient's waitlist status from Active to Inactive with a reason code. | Transition recorded with prior status, new status, reason, effective timestamp, offer-eligibility impact, and acting user. | | | +| OQ-72 | Attempt `UPDATE waitlist_status_transitions SET reason_code='x'` and `DELETE FROM waitlist_status_transitions` via direct SQL. | Both rejected by database trigger. | | | +| OQ-73 | Inspect the notification created by OQ-71. | Record exists with content hash, generator version, and a due date exactly 10 days after the effective timestamp. | | | +| OQ-74 | Attempt via direct SQL to alter the notification's content hash, due date, notice kind, or idempotency key. | Each rejected by trigger. Repeat for a delivery column; that update succeeds. | | | +| OQ-75 | Re-run notice generation for the same transition without incrementing the revision. | No second notification is created. Then increment the revision and repeat: a new notification is created with key suffix `r1`. | | | +| OQ-76 | Set a notification's due date in the past by recording a transition dated 15 days ago; open the IOTA Notices page. | Obligation is listed as overdue. | | | +| OQ-77 | Attempt to generate a notice for an organisation with no template configured. | Generation is refused; no vendor default language is substituted. | | | +| OQ-78 | Configure a template omitting the reactivation instructions; save. | Rejected at save time, naming the missing element. Repeat with an unknown `{{placeholder}}`: also rejected. | | | +| OQ-79 | Generate a notice twice for the same transition and compare the rendered bodies and content hashes. | Identical. Confirm the notice states that organ offers cannot be received while inactive, in system-supplied wording not present in the configured template. | | | +| OQ-129 | Record an offer-blocking status change. | The transition and its notification obligation are created in the same operation; no transition exists without a tracked deadline. | | | +| OQ-130 | With notice configuration deliberately incomplete, record an offer-blocking status change. | Transition is still recorded; the obligation is reported as unmet rather than discarded. | | | +| OQ-131 | Attempt to save a template missing the hospital contact block. | Rejected at configuration time. | | | +| OQ-132 | Mark a notice delivered within its due date; mark a second notice delivered after its due date. | Channel and timestamp recorded for both; the first reports on time, the second late. Attempt to mark the first delivered again: rejected. | | | +| OQ-133 | Open the IOTA compliance summary. | Counts shown for open, overdue, delivered on time, and delivered late, plus any obligating transition with no notice. | | | +| OQ-134 | Generate a notice for an ESRD test patient with no dialysis facility recorded. | Obligation flagged as incompletely addressed; not presented as discharged. | | | +| OQ-135 | Reprint a delivered notice. | Stored body is reproduced and verifies against its recorded content hash. Alter the stored body via direct SQL and reprint: mismatch is reported. | | | +| OQ-136 | Attempt each IOTA action as `viewer`, `physician`, `coordinator`, and `admin`. | Configuration admin-only; obligation and delivery writes admin and coordinator; read additionally physician and regulator. Every write produces an audit row. | | | + +## Chart filing + +| ID | Step | Expected | Pass/Fail | Evidence | +|---|---|---|---|---| +| OQ-150 | File a delivered notice to the chart. | A FHIR R4 DocumentReference is produced for the patient. Before filing, confirm the compliance summary reports the delivered-but-unfiled notice as partially met. | | | +| OQ-151 | Alter a notice's stored body via direct SQL, then attempt to file it. | Filing refused on content-hash mismatch. | | | +| OQ-152 | File in dry-run mode. | DocumentReference is built, validated and displayed; capture network traffic during the step and confirm no outbound request is made. | | | +| OQ-153 | Inspect the deployed configuration for any setting that enables transmission. | None exists; transmission requires a transport supplied by the caller. Confirm by network capture across a full session of normal use. | | | +| OQ-154 | File to chart with the Epic endpoint unreachable. | Failure recorded with its cause; the obligation remains retryable. Restore the endpoint and retry: succeeds. Attempt to file the same notice again: refused. | | | +| OQ-155 | Record a manual filing for a notice sent via interface engine. | Obligation shows as filed with the manual route recorded. | | | + +## Backup and migration safety + +Executed on a copy of a populated non-PHI test database. + +| ID | Step | Expected | Pass/Fail | Evidence | +|---|---|---|---|---| +| OQ-84 | Install a build carrying a pending migration and start it. | A verified pre-migration copy is written before the migration runs. Restart with no migrations pending: no new copy is taken. | | | +| OQ-85 | Make the backup directory read-only, then start a build carrying a pending migration. | Startup refuses to migrate; the database is left at its previous schema version and opens normally once the build is reverted. | | | +| OQ-86 | Introduce a deliberately failing migration in a test build and start it. | The reported error names the schema version reached and the full path of the pre-migration copy. Restore from that path and confirm the database opens. | | | +| OQ-87 | Apply six successive migrations. | At most five pre-migration copies are retained; older ones are removed via secure delete. | | | + +## System health and support bundles + +| ID | Step | Expected | Pass/Fail | Evidence | +|---|---|---|---|---| +| OQ-124 | Open System Health as an administrator. | Per-component status, overall status, and current schema version are shown. | | | +| OQ-125 | Export a support bundle. | A single file is produced containing health status, schema version, aggregate record counts, backup history, and recent log activity. | | | +| OQ-126 | Enter a recognisable test-patient name into a free-text note and trigger a log entry containing it; export a default bundle and search the file for that name, an MRN, a date of birth, and an e-mail address. | None appear. Free-text values are present as `[FREE_TEXT_OMITTED]` with a length hint rather than filtered text. | | | +| OQ-127 | Export a bundle with free text explicitly included. | The bundle records that choice in its `redactionPolicy`, is labelled as requiring PHI handling, and does not claim to be PHI-free. | | | +| OQ-128 | Attempt bundle export as a non-administrator; then export as an administrator. | Non-admin refused. Admin export produces an audit row recording the export and whether free text was included. | | | + ## Reporting | ID | Step | Expected | Pass/Fail | Evidence | @@ -81,6 +138,8 @@ against the running build. OQ is executed in a **non-PHI** test environment. | OQ-141 | Capture egress with PCAP for 30 min normal use. | Only whitelisted hosts. | | | | OQ-142 | Inspect audit log row for `request_id`. | Present and unique. | | | | OQ-143 | Open About dialog. | Design alignment statement present (not "certified"). | | | +| OQ-144 | Review the release evidence for the build under test: the dependency audit output and the exception file. | Every finding is either resolved or covered by an unexpired documented exception. Confirm by back-dating one exception's review date in a scratch copy that the gate then fails. | | | +| OQ-145 | Confirm the installed application's version matches the version in the release record; exercise each administrative screen including Disaster Recovery and System Health. | Versions match; every control performs its action rather than failing at the bridge. | | | ## Acceptance From a847414c430381d517ebac5fcdf47647ababd6eb Mon Sep 17 00:00:00 2001 From: NeuroKoder3 Date: Sat, 1 Aug 2026 19:00:53 -0500 Subject: [PATCH 2/2] test(validation): machine-verify the validation package cross-references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A validation package is only worth what its cross-references are worth, and those decay silently: the documents still render, the build still passes, and the defect surfaces when an auditor walks a trace. The duplicate requirement ids fixed in the previous commit are the case in point — TT-R140 meant two different things across two documents, and reading either document alone gave no hint of it. check-compliance-docs.mjs resolves what the documents claim about each other: requirement ids are unique, every requirement has exactly one matrix row and every row a real requirement, every Mandatory requirement names a verification artifact, and every cited SDS section, OQ case and risk exists. It reports the counts it parsed so a format change that breaks parsing cannot pass vacuously with zero of everything — the test asserts on those counts for the same reason. On its first run it found four requirements with no matrix row (TT-R010, TT-R100, TT-R101, TT-R123). Three are implemented and are now traced; SSO remains deferred and is listed with that status rather than omitted, so the gap is visible on the page instead of inferred from an absence. The matrix preamble previously claimed to map only Mandatory requirements, which is now no longer a caveat it needs. Runs in the standard test group, so the package is checked on the same cadence as the code it describes. Co-authored-by: Cursor --- CHANGELOG.md | 43 +++++++ docs/compliance/README.md | 9 ++ scripts/check-compliance-docs.mjs | 181 ++++++++++++++++++++++++++++++ scripts/run-test-suites.cjs | 3 + tests/complianceDocs.test.mjs | 79 +++++++++++++ 5 files changed, 315 insertions(+) create mode 100644 scripts/check-compliance-docs.mjs create mode 100644 tests/complianceDocs.test.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index c70e80e..f0dabe2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,49 @@ All notable changes to TransTrack are documented in this file. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Fixed + +- **Support bundle log tail no longer loses a race with log rotation.** + `readLogTail()` checked a path for existence and size and then opened it. + `logger.cjs` rotates those same files, so the log could be renamed in + between — costing a section of the diagnostics, or pairing one file's size + with another's bytes. It now opens once and measures the descriptor. +- **`supportBundle.test.cjs` no longer requires an installed Electron + binary.** It loaded `logger.cjs`, which destructures `app` at module scope, + so the suite passed locally and failed in CI, where the binary download is + skipped. It now stubs `electron` the way the neighbouring suites do. + +### Changed — validation package + +- **Chart filing requirements renumbered TT-R137–R142 → TT-R150–R155.** The + original numbering collided with the pre-existing cross-cutting + requirements TT-R140–R142. Corrected before any site executes an OQ against + these ids. +- **SDS extended** with §11 migration safety, §12 diagnostics and PHI + redaction, §13 the IOTA notification pipeline, §14 chart filing, §15 + dependency vulnerability exceptions, and §16 renderer bridge integrity. + Sections are appended rather than interleaved so existing §-references stay + valid. +- **OQ protocol extended** with 32 executable test cases covering the IOTA + pipeline, chart filing, migration safety, and support bundles — including + the adversarial one that matters: plant a patient name in free text, export + a default bundle, and search the file for it. +- **Risk register extended** with R-020 to R-027 (missed notification + deadline, duplicate or misfiled chart document, bundle PHI leakage, notice + altered after filing, template missing a statutory element, stale + vulnerability exception, feature unwired in the packaged build). R-013's + mitigation was revised: transactional rollback does not cover a + multi-migration sequence that fails partway, which the pre-migration copy + now does. +- **`scripts/check-compliance-docs.mjs`** — the cross-references between + these documents are now machine-verified and run in the standard test + suite. The requirement-id collision above survived review of both documents + because neither is wrong when read alone; this is the check that catches + that class of defect. It also found four requirements with no matrix row, + now traced. + ## [1.2.1] - 2026-08-01 Pilot-readiness release. Apart from the IOTA notification pipeline below, diff --git a/docs/compliance/README.md b/docs/compliance/README.md index 80028c6..951e96b 100644 --- a/docs/compliance/README.md +++ b/docs/compliance/README.md @@ -20,6 +20,7 @@ requirements. | [`SOFTWARE_DESIGN_SPECIFICATION.md`](SOFTWARE_DESIGN_SPECIFICATION.md) | High-level design and architecture mapped to requirements. | | [`TRACEABILITY_MATRIX.md`](TRACEABILITY_MATRIX.md) | Requirement → design → test traceability. | | [`RISK_REGISTER.md`](RISK_REGISTER.md) | ISO 14971-style risk register and mitigations. | +| `scripts/check-compliance-docs.mjs` | Automated consistency gate over the documents above: unique requirement ids, a matrix row per requirement, a verification artifact for every Mandatory requirement, and resolvable SDS, OQ and risk references. Runs in the standard test suite. | | [`VALIDATION_SUMMARY_REPORT_TEMPLATE.md`](VALIDATION_SUMMARY_REPORT_TEMPLATE.md) | Template for the deploying organization to sign after IQ/OQ/PQ are executed. | ### Qualification protocols (templates to execute on the customer site) @@ -72,3 +73,11 @@ The presence and quality of these artifacts is itself a buying signal. A reviewe should expect to find: numbered requirements traced to tests, a risk register with mitigations, executable IQ/OQ/PQ templates, and explicit policy documents that map to HIPAA Administrative Safeguards. All of those exist here. + +Two things are worth checking, because they are where validation packages usually +decay. First, the traceability is machine-verified rather than asserted: +`scripts/check-compliance-docs.mjs` runs in the standard test suite and fails the +build on a duplicate requirement id, an untraced requirement, or a dangling OQ or +risk reference. Second, requirements that are *not* implemented are listed in the +matrix with their status rather than omitted, so the gaps are visible on the page +instead of having to be inferred from an absence. diff --git a/scripts/check-compliance-docs.mjs b/scripts/check-compliance-docs.mjs new file mode 100644 index 0000000..81ce0f8 --- /dev/null +++ b/scripts/check-compliance-docs.mjs @@ -0,0 +1,181 @@ +#!/usr/bin/env node +/** + * TransTrack — consistency gate for the validation package. + * + * The validation package is only useful if its cross-references hold. A + * duplicate requirement ID, a matrix row pointing at a requirement that was + * renamed, or a Mandatory requirement with no OQ case are all defects an + * auditor will find, and all of them are mechanically detectable. This script + * checks what the documents claim about themselves: + * + * 1. Requirement IDs in the SRS are unique. + * 2. Every requirement in the SRS appears in the traceability matrix, and + * every matrix row refers to a requirement that exists. + * 3. Every Mandatory requirement traces to at least one verification + * artifact, and every OQ id cited by the matrix exists in the protocol. + * 4. Every SDS section referenced by the matrix exists. + * 5. Every risk cited in the matrix's risk linkage exists in the register. + * + * Run: node scripts/check-compliance-docs.mjs + */ + +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); +const DOCS = join(ROOT, 'docs', 'compliance'); + +const read = (p) => readFileSync(join(DOCS, p), 'utf8'); + +const srs = read('SYSTEM_REQUIREMENTS_SPECIFICATION.md'); +const matrix = read('TRACEABILITY_MATRIX.md'); +const sds = read('SOFTWARE_DESIGN_SPECIFICATION.md'); +const risks = read('RISK_REGISTER.md'); +const oq = read(join('templates', 'OQ_PROTOCOL_TEMPLATE.md')); + +const problems = []; +const fail = (msg) => problems.push(msg); + +/** Rows of a markdown table, as arrays of trimmed cells. */ +function tableRows(text) { + return text + .split('\n') + .filter((l) => l.trimStart().startsWith('|')) + .map((l) => l.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => c.trim())) + .filter((cells) => !cells.every((c) => /^:?-+:?$/.test(c) || c === '')); +} + +// ---------------------------------------------------------------- 1. SRS IDs + +const srsReqs = new Map(); // id -> priority +for (const cells of tableRows(srs)) { + const m = /^TT-R(\d+)$/.exec(cells[0]); + if (!m) continue; + const id = cells[0]; + if (srsReqs.has(id)) { + fail(`SRS: duplicate requirement id ${id}. Requirement ids must be unique — a duplicate breaks every downstream trace.`); + } + srsReqs.set(id, cells[1]); +} + +if (srsReqs.size === 0) fail('SRS: parsed no requirements. The table format probably changed.'); + +// --------------------------------------------------------- 2 & 3. The matrix + +const matrixRows = new Map(); // id -> { design, impl, verification } +let inRiskLinkage = false; +const riskLinks = []; + +for (const line of matrix.split('\n')) { + if (/^##\s+Risk linkage/i.test(line)) { inRiskLinkage = true; continue; } + if (/^##\s/.test(line) && inRiskLinkage) inRiskLinkage = false; + if (!line.trimStart().startsWith('|')) continue; + + const cells = line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => c.trim()); + if (cells.every((c) => /^:?-+:?$/.test(c) || c === '')) continue; + + if (inRiskLinkage) { + const rm = /^(R-\d+)/.exec(cells[0]); + if (rm) riskLinks.push({ risk: rm[1], reqs: (cells[1] || '').match(/TT-R\d+/g) || [] }); + continue; + } + + const m = /^TT-R\d+$/.exec(cells[0]); + if (!m) continue; + if (matrixRows.has(cells[0])) { + fail(`Traceability matrix: duplicate row for ${cells[0]}.`); + } + matrixRows.set(cells[0], { design: cells[1] || '', impl: cells[2] || '', verification: cells[3] || '' }); +} + +for (const [id, pri] of srsReqs) { + if (!matrixRows.has(id)) { + fail(`Traceability matrix: ${id} (priority ${pri}) is specified in the SRS but has no matrix row.`); + } +} +for (const id of matrixRows.keys()) { + if (!srsReqs.has(id)) { + fail(`Traceability matrix: row ${id} refers to a requirement that does not exist in the SRS.`); + } +} + +// Mandatory requirements need a verification artifact. +for (const [id, pri] of srsReqs) { + if (pri !== 'M') continue; + const row = matrixRows.get(id); + if (!row) continue; + if (row.verification === '') { + fail(`Traceability matrix: ${id} is Mandatory but names no verification artifact.`); + } +} + +// ------------------------------------------------------------ 3b. OQ ids + +const oqIds = new Set(); +for (const cells of tableRows(oq)) { + const m = /^(OQ-\d+)$/.exec(cells[0]); + if (m) { + if (oqIds.has(m[1])) fail(`OQ protocol: duplicate test case id ${m[1]}.`); + oqIds.add(m[1]); + } +} + +for (const [id, row] of matrixRows) { + for (const cited of row.verification.match(/OQ-\d+/g) || []) { + if (!oqIds.has(cited)) { + fail(`Traceability matrix: ${id} cites ${cited}, which does not exist in the OQ protocol template.`); + } + } +} + +// ------------------------------------------------------------ 4. SDS sections + +const sdsSections = new Set(); +for (const m of sds.matchAll(/^##+\s+(\d+)(?:\.\d+)?\.\s/gm)) sdsSections.add(m[1]); + +for (const [id, row] of matrixRows) { + for (const cited of row.design.match(/§(\d+)/g) || []) { + const n = cited.slice(1); + if (!sdsSections.has(n)) { + fail(`Traceability matrix: ${id} points at SDS §${n}, which does not exist.`); + } + } +} + +// ------------------------------------------------------------ 5. Risk linkage + +const riskIds = new Set(); +for (const cells of tableRows(risks)) { + const m = /^(R-\d+)$/.exec(cells[0]); + if (m) { + if (riskIds.has(m[1])) fail(`Risk register: duplicate risk id ${m[1]}.`); + riskIds.add(m[1]); + } +} + +for (const { risk, reqs } of riskLinks) { + if (!riskIds.has(risk)) { + fail(`Traceability matrix: risk linkage cites ${risk}, which is not in the risk register.`); + } + if (reqs.length === 0) { + fail(`Traceability matrix: risk linkage for ${risk} names no controlling requirement.`); + } + for (const r of reqs) { + if (!srsReqs.has(r)) { + fail(`Traceability matrix: risk linkage for ${risk} cites ${r}, which does not exist in the SRS.`); + } + } +} + +// ---------------------------------------------------------------- Report + +if (problems.length > 0) { + console.error('Validation package consistency: FAIL\n'); + for (const p of problems) console.error(` - ${p}`); + console.error(`\n${problems.length} problem(s).`); + process.exit(1); +} + +console.log('Validation package consistency: PASS'); +console.log(` ${srsReqs.size} requirements, ${matrixRows.size} matrix rows, ${oqIds.size} OQ cases, ${riskIds.size} risks.`); diff --git a/scripts/run-test-suites.cjs b/scripts/run-test-suites.cjs index 30f1676..84e5dea 100644 --- a/scripts/run-test-suites.cjs +++ b/scripts/run-test-suites.cjs @@ -97,6 +97,9 @@ const FUNCTIONAL_SUITES = [ 'auditExceptions.test.mjs', 'migrationSafety.test.cjs', 'supportBundle.test.cjs', + // The validation package is a deliverable; its cross-references are checked + // on the same cadence as the code they describe. + 'complianceDocs.test.mjs', ]; const GROUPS = { diff --git a/tests/complianceDocs.test.mjs b/tests/complianceDocs.test.mjs new file mode 100644 index 0000000..9f66d83 --- /dev/null +++ b/tests/complianceDocs.test.mjs @@ -0,0 +1,79 @@ +/** + * TransTrack — validation package consistency. + * + * The SRS, SDS, traceability matrix, OQ protocol and risk register only have + * value as a set. A requirement id used twice, a matrix row pointing at a + * requirement that was renumbered, or an OQ case cited but never written are + * exactly the defects an auditor finds first, and they appear silently: the + * documents still render, and nothing in the build notices. + * + * That happened here. Six chart-filing requirements were numbered TT-R137–R142, + * colliding with the pre-existing cross-cutting TT-R140–R142, and the collision + * survived review of both documents because neither is wrong when read alone. + * + * This suite runs `scripts/check-compliance-docs.mjs`, which resolves every + * cross-reference between the documents. Keeping it in the normal test group + * means the validation package is checked on the same cadence as the code it + * describes. + * + * Run standalone: node tests/complianceDocs.test.mjs + */ + +import assert from 'node:assert'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..'); +const checker = join(repoRoot, 'scripts', 'check-compliance-docs.mjs'); + +let PASS = 0, FAIL = 0; +const failures = []; +function test(name, fn) { + try { fn(); PASS++; console.log(` ok ${name}`); } + catch (e) { FAIL++; failures.push({ name, error: e }); console.log(` FAIL ${name}: ${e.message}`); } +} + +console.log('\nValidation package consistency'); + +// No `shell: true`: process.execPath contains spaces on a default Windows +// install, and the shell splits it into a bogus command. +const run = spawnSync(process.execPath, [checker], { cwd: repoRoot, encoding: 'utf8' }); + +test('the consistency checker runs', () => { + assert.strictEqual(run.error, undefined, `could not spawn the checker: ${run.error?.message}`); +}); + +test('every cross-reference in the validation package resolves', () => { + assert.strictEqual( + run.status, + 0, + `the validation package is inconsistent:\n${run.stderr || run.stdout}`, + ); +}); + +test('the checker reports what it inspected', () => { + assert.match( + run.stdout, + /\d+ requirements, \d+ matrix rows, \d+ OQ cases, \d+ risks/, + 'the checker should report its counts so a silently empty parse is visible', + ); +}); + +test('the checker is not silently parsing nothing', () => { + const m = /(\d+) requirements, (\d+) matrix rows, (\d+) OQ cases, (\d+) risks/.exec(run.stdout); + assert.ok(m, 'counts not found'); + const [, reqs, rows, oq, risks] = m.map(Number); + // A format change that broke parsing would report zero and pass every other + // check vacuously. + assert.ok(reqs > 50, `expected the SRS to yield >50 requirements, got ${reqs}`); + assert.strictEqual(rows, reqs, 'every requirement must have exactly one matrix row'); + assert.ok(oq > 50, `expected >50 OQ cases, got ${oq}`); + assert.ok(risks > 20, `expected >20 risks, got ${risks}`); +}); + +console.log(`\n${PASS} passed, ${FAIL} failed\n`); +if (FAIL > 0) { + for (const f of failures) console.error(`${f.name}\n${f.error.message}\n`); + process.exit(1); +}