Traceability matrix
| Document | CS-VAL-05 |
| Generated by | node scripts/trace.ts from requirements.json and the test sources. Do not edit by hand. |
Each user requirement (UR) is met by one or more functional requirements (FR); each functional requirement is verified by the automated tests or walkthrough steps whose names carry its identifier. Running the tests therefore produces the evidence for this matrix (see OQ protocol).
Requirements: 15 user, 115 functional. Unverified: 0.
UR-01
The system holds records of the types our business needs, with the fields we define, without programming.
| Functional requirement | Risk | Statement | Verified by | In |
|---|---|---|---|---|
| FR-CFG-01 | medium | A configuration document is checked completely before use. | automated test | lib/engine.test.ts |
| FR-CFG-05 | medium | Without a configuration no record can be created. | automated test | lib/engine.test.ts |
| FR-HIER-01 | medium | A record type may nest inside itself and a parent may be optional; other cycles between types stay refused. | automated test | lib/engine.test.ts |
| FR-HIER-02 | high | No record becomes its own ancestor: on create, edit, approval and when an approval is revoked, pending changes included. | automated test | lib/engine.test.ts |
| FR-VAL-01 | medium | Required fields must be filled. | automated test | lib/engine.test.ts |
| FR-VAL-02 | medium | Values must have the field's type. | automated test | lib/engine.test.ts |
| FR-VAL-03 | medium | Numbers stay inside their range and text matches its pattern. | automated test | lib/engine.test.ts |
| FR-VAL-04 | medium | Unique fields are unique among records that are not removed, pending values included. | automated test | lib/engine.test.ts |
| FR-VAL-05 | medium | References must point at live records of the right type. | automated test | lib/engine.test.ts |
| FR-VAL-06 | medium | Fields the configuration does not know are refused. | automated test | lib/engine.test.ts |
| FR-VAL-07 | medium | Data is checked again when it is confirmed and when it is approved. | automated test | lib/engine.test.ts |
UR-02
No change to a record takes effect until a second, authorised person has approved it.
| Functional requirement | Risk | Statement | Verified by | In |
|---|---|---|---|---|
| FR-CFG-08 | high | A proposed configuration takes effect only when a different person with the configure permission approves it. | automated test | lib/engine.test.ts |
| FR-WF-01 | high | A new record starts as version 1, Created, with the next id of its type. | automated test | lib/engine.test.ts |
| FR-WF-02 | high | Only its author can edit a Created version, and the version number stays. | automated test | lib/engine.test.ts |
| FR-WF-03 | high | Only its author can confirm a version. | automated test | lib/engine.test.ts |
| FR-WF-04 | high | Four eyes: nobody approves a version they wrote or confirmed. | automated test | lib/engine.test.ts |
| FR-WF-05 | high | Approval needs the approve permission for the record type. | automated test | lib/engine.test.ts |
| FR-WF-06 | high | Only a Confirmed version can be approved, and the signature states its meaning. | automated test | lib/engine.test.ts |
| FR-WF-07 | high | A Confirmed version is locked against edits. | automated test | lib/engine.test.ts |
| FR-WF-08 | high | Step back moves one stage: Confirmed to Created, Approved to Confirmed. | automated test | lib/engine.test.ts |
| FR-WF-09 | high | Step back, retire, remove and changing an approved record need a reason. | automated test | lib/engine.test.ts |
UR-03
Every version of every record is kept, and the approved version stays in effect until its successor is approved.
| Functional requirement | Risk | Statement | Verified by | In |
|---|---|---|---|---|
| FR-VER-01 | high | Editing an approved record opens a new Confirmed version; the approved one stays in effect. | automated test | lib/engine.test.ts |
| FR-VER-02 | high | Once the new version is approved it is in effect and the old one is kept as superseded. | automated test | lib/engine.test.ts |
| FR-VER-03 | high | Revoking an approval brings the previous approved version back, or leaves none in effect. | automated test | lib/engine.test.ts |
UR-04
Every action is recorded with who did it, when, what changed from what to what, and why; the record of actions cannot be changed or deleted.
| Functional requirement | Risk | Statement | Verified by | In |
|---|---|---|---|---|
| FR-AUD-01 | high | Every command that succeeds writes exactly one complete entry. | automated test | lib/engine.test.ts |
| FR-AUD-02 | high | A refused command writes nothing and changes nothing. | automated test | lib/engine.test.ts |
| FR-AUD-03 | high | Entries are in time order: an action dated before the newest entry is refused. | automated test | lib/engine.test.ts |
| FR-AUD-04 | high | How the person was identified is part of the entry and of its hash. | automated test | lib/engine.test.ts |
| FR-STO-04 | high | The database itself refuses to change or delete entries. | automated test | server/server.test.ts |
UR-05
Any alteration of the record of actions, by anyone including administrators, can be detected.
| Functional requirement | Risk | Statement | Verified by | In |
|---|---|---|---|---|
| FR-CFG-12 | high | The waiting proposal is rebuilt by replay, and a trail written before proposals existed replays to the same hashes. | automated test | lib/engine.test.ts |
| FR-INT-01 | high | Every entry carries the hash of the one before it. | automated test | lib/engine.test.ts |
| FR-INT-02 | high | An entry changed after it was written is found. | automated test | lib/engine.test.ts |
| FR-INT-03 | high | A forged entry with a freshly computed hash is caught when its command is re-run. | automated test | lib/engine.test.ts |
| FR-INT-04 | high | A missing, reordered or inserted entry is found. | automated test | lib/engine.test.ts |
| FR-INT-05 | high | The hash does not depend on the order of keys a store returns. | automated test | lib/engine.test.ts |
| FR-INT-06 | high | Replaying a whole history reproduces the same state, for every demo domain. | automated test | lib/engine.test.ts |
UR-06
People can only do what their role allows, per record type and per level.
| Functional requirement | Risk | Statement | Verified by | In |
|---|---|---|---|---|
| FR-ACC-01 | high | A role writes only the record types and option levels it is given. | automated test | lib/engine.test.ts |
| FR-ACC-02 | high | A person without a role, or with roles the configuration does not know, can change nothing. | automated test | lib/engine.test.ts |
| FR-ACC-03 | high | Importing and configuring are separate permissions that "*" does not include. | automated test | lib/engine.test.ts |
UR-07
People are identified by our identity provider; an approval is an electronic signature that requires the person to have authenticated recently.
| Functional requirement | Risk | Statement | Verified by | In |
|---|---|---|---|---|
| FR-CFG-13 | high | Over HTTP one person proposes a configuration change and another approves it; the approval is a signature and needs a recent sign-in. | automated test | server/server.test.ts |
| FR-SIG-01 | high | A session cookie that was altered, re-signed with another key, or has expired is not accepted. | automated test | server/server.test.ts |
| FR-SIG-02 | high | Sign-in is the OpenID Connect code flow with PKCE, state and nonce, and yields a session. | automated test | server/server.test.ts |
| FR-SIG-03 | high | A sign-in answer that fails any check gives no session. | automated test | server/server.test.ts |
| FR-SIG-04 | high | Roles come from the token's claims, through the role map. | automated test | server/server.test.ts |
| FR-SIG-05 | high | A signature needs a recent sign-in: an approval with an old one is refused until the person signs in again. | automated test | server/server.test.ts |
| FR-SIG-06 | high | Nothing is readable or writable without signing in. | automated test | server/server.test.ts |
| FR-SIG-07 | high | Writes are accepted only from the application's own pages. | automated test | server/server.test.ts |
| FR-SIG-08 | high | The web interface cannot import or set a configuration in one step, and takes the actor only from the session. | automated test | server/server.test.ts |
| FR-SIG-09 | high | Behind an authenticating proxy, identity headers count only with the shared secret. | automated test | server/server.test.ts |
| FR-SIG-10 | high | Demo sign-in exists only when it is switched on explicitly. | automated test | server/server.test.ts |
UR-08
Settings can be defined once and set at any level of the hierarchy; for every record it is clear which value applies and where it comes from.
| Functional requirement | Risk | Statement | Verified by | In |
|---|---|---|---|---|
| FR-HIER-03 | high | Options are inherited down every level of a nested branch: the nearest ancestor wins, then the levels above, then the default. | automated test | lib/engine.test.ts |
| FR-OPT-01 | medium | An option definition must be consistent: type, choices, range, default. | automated test | lib/engine.test.ts |
| FR-OPT-02 | medium | Option names are unique. | automated test | lib/engine.test.ts |
| FR-OPT-03 | medium | An option value fits its definition, is set once per record, and keeps its target. | automated test | lib/engine.test.ts |
| FR-OPT-04 | medium | The effective value is the most specific approved one, with its source. | automated test | lib/engine.test.ts |
| FR-OPT-05 | medium | Only approved, active values count; pending changes are reported beside them. | automated test | lib/engine.test.ts |
| FR-OPT-06 | medium | A definition cannot change in a way that strands values in use. | automated test | lib/engine.test.ts |
| FR-OPT-07 | medium | An approved value that no longer fits its definition is skipped and reported, not applied. | automated test | lib/engine.test.ts |
UR-09
Records can be taken out of use without losing their history.
| Functional requirement | Risk | Statement | Verified by | In |
|---|---|---|---|---|
| FR-HIER-04 | medium | A record cannot be retired or removed while records sit under it, at any level of a nested branch. | automated test | lib/engine.test.ts |
| FR-LIFE-01 | medium | Retiring is a new version that takes effect only when someone else approves it. | automated test | lib/engine.test.ts |
| FR-LIFE-02 | medium | Removing an approved record needs approval; afterwards it accepts no command and keeps its history. | automated test | lib/engine.test.ts |
| FR-LIFE-03 | medium | A record that was never approved is removed at once, by its author or an approver, and cannot be retired. | automated test | lib/engine.test.ts |
| FR-LIFE-04 | medium | A record that others refer to cannot be retired or removed. | automated test | lib/engine.test.ts |
UR-10
The data is kept in a database we operate, safely shared by several application servers.
| Functional requirement | Risk | Statement | Verified by | In |
|---|---|---|---|---|
| FR-STO-01 | high | Entries are appended and read back in order, unchanged. | automated test | server/server.test.ts |
| FR-STO-02 | high | An entry that does not continue the chain is refused. | automated test | server/server.test.ts |
| FR-STO-03 | high | The trail survives a restart. | automated test | server/server.test.ts |
| FR-STO-04 | high | The database itself refuses to change or delete entries. | automated test | server/server.test.ts |
| FR-STO-05 | high | Two servers writing at once cannot fork the trail. | automated test | server/server.test.ts |
UR-11
Records can be brought over from the system we use today, with their origin recorded and the transfer reconciled.
| Functional requirement | Risk | Statement | Verified by | In |
|---|---|---|---|---|
| FR-HIER-05 | medium | A record type that nests inside itself is migrated parents first; a row whose parent comes later in the file is refused, not waited for. | automated test | lib/migrate.test.ts |
| FR-MIG-01 | medium | An imported record arrives approved, marked with where it came from. | automated test | lib/engine.test.ts |
| FR-MIG-02 | medium | The same source record cannot be imported twice. | automated test | lib/engine.test.ts |
| FR-MIG-03 | medium | Imported data must pass the same checks as data entered by hand. | automated test | lib/engine.test.ts |
| FR-MIG-04 | medium | Rows are imported parents first, with references and values converted to the configured types. | automated test | lib/migrate.test.ts |
| FR-MIG-05 | medium | Every source row is accounted for: imported or refused with the reason. | automated test | lib/migrate.test.ts |
| FR-MIG-06 | medium | Running a migration again imports only what is not there yet. | automated test | lib/migrate.test.ts |
| FR-MIG-07 | medium | In review mode migrated records wait as Confirmed for an approver here. | automated test | lib/migrate.test.ts |
| FR-MIG-08 | medium | Source files are read as CSV with quotes, semicolons and line ends handled. | automated test | lib/migrate.test.ts |
| FR-MIG-09 | medium | The data model of the system that is replaced is read from SQL definitions and Prisma schemas, with keys, references and code lists. | automated test | lib/adopt/adopt.test.ts |
| FR-MIG-10 | medium | A drafted configuration and mapping are valid, every source table and column gets a stated fate, and every rule-of-thumb choice is listed as a decision. | automated test | lib/adopt/adopt.test.ts |
| FR-MIG-11 | medium | Capabilities and settings of the old system are reported with where they were seen and how Countersign stands on them; no setting's value is carried. | automated test | lib/adopt/adopt.test.ts |
| FR-MIG-12 | medium | After an import every mapped field of every imported record is compared with its source, and each difference or missing record is reported. | automated test | lib/adopt/adopt.test.ts |
| FR-MIG-13 | medium | Each step of the migration assistant is journaled with the checksums of its inputs and outputs in a chain that reveals later changes. | automated test | lib/adopt/adopt.test.ts |
| FR-MIG-14 | medium | The assistant exports the old data read-only by recorded queries and rehearses import and verification in a scratch store. | automated test | lib/adopt/adopt.test.ts |
UR-12
Changes to the system's own configuration are controlled, attributable and part of the record of actions.
| Functional requirement | Risk | Statement | Verified by | In |
|---|---|---|---|---|
| FR-CFG-02 | medium | The first configuration is audit entry #1 and carries the configuration's hash. | automated test | lib/engine.test.ts |
| FR-CFG-03 | medium | Changing the configuration needs the permission, a reason and a second person. | automated test | lib/engine.test.ts |
| FR-CFG-04 | medium | A configuration change cannot orphan existing records. | automated test | lib/engine.test.ts |
| FR-CFG-06 | medium | A new configuration applies from its entry on, and the history still replays. | automated test | lib/engine.test.ts |
| FR-CFG-07 | high | A person with the configure permission proposes a configuration change with a reason; it is one entry and changes nothing yet. | automated test | lib/engine.test.ts |
| FR-CFG-08 | high | A proposed configuration takes effect only when a different person with the configure permission approves it. | automated test | lib/engine.test.ts |
| FR-CFG-09 | medium | A proposal can be rejected with a reason by a second person or withdrawn by its proposer; neither changes the configuration. | automated test | lib/engine.test.ts |
| FR-CFG-10 | high | A proposal that would strand existing data is refused, naming the records, when it is proposed and again when it is approved. | automated test | lib/engine.test.ts |
| FR-CFG-11 | medium | A field may become required while records lack it: they stay in effect, are reported, and must be completed with their next change. | automated test | lib/engine.test.ts |
| FR-CFG-12 | high | The waiting proposal is rebuilt by replay, and a trail written before proposals existed replays to the same hashes. | automated test | lib/engine.test.ts |
| FR-CFG-13 | high | Over HTTP one person proposes a configuration change and another approves it; the approval is a signature and needs a recent sign-in. | automated test | server/server.test.ts |
| FR-UI-11 | medium | The configuration editor checks the draft as it is edited, shows every problem next to its cause, and shows the changes and their effect on existing records before a change can be proposed. | end-to-end walkthrough | scripts/guide-shots.mjs |
| FR-UI-12 | medium | A proposed configuration change is shown with its changes on the configuration page and in the inbox of those who may decide; its proposer is not offered the approval. | end-to-end walkthrough | scripts/guide-shots.mjs |
UR-13
The record of actions and the records can be exported in open formats.
| Functional requirement | Risk | Statement | Verified by | In |
|---|---|---|---|---|
| FR-EXP-01 | low | The audit trail exports as CSV with one line per entry and no executable cells. | automated test | lib/engine.test.ts |
| FR-UI-06 | medium | The audit trail can be searched and filtered by action, record type and person, and exported as CSV and JSON. | end-to-end walkthrough | scripts/guide-shots.mjs |
UR-14
The system can be monitored and verified by its operators, and stops accepting changes if its record of actions is damaged.
| Functional requirement | Risk | Statement | Verified by | In |
|---|---|---|---|---|
| FR-OPS-01 | medium | An empty store is initialised with the configuration as its first entry, once. | automated test | server/server.test.ts |
| FR-OPS-02 | medium | Entries altered in the store are detected on reading, and from then on every change is refused. | automated test | server/server.test.ts |
| FR-OPS-03 | medium | Entries removed or rewritten while the server runs are detected. | automated test | server/server.test.ts |
| FR-OPS-04 | medium | The server stamps entries with its own clock, never earlier than the newest entry. | automated test | server/server.test.ts |
| FR-OPS-05 | medium | Without storage settings the app is the demo; with them, missing secrets stop the server. | automated test | server/server.test.ts |
| FR-OPS-06 | medium | The health endpoint reports store, head hash, configuration and integrity. | automated test | server/server.test.ts |
| FR-OPS-07 | medium | The engine's refusal reaches the browser as a 422 with the reason. | automated test | server/server.test.ts |
UR-15
The user interface shows the state of every record truthfully and guides people through the workflow.
| Functional requirement | Risk | Statement | Verified by | In |
|---|---|---|---|---|
| FR-UI-01 | medium | Every record shows what is in effect, what is pending, and its state, in the tree and on its page. | end-to-end walkthrough | scripts/guide-shots.mjs |
| FR-UI-02 | medium | The signature panel shows the change, who wrote it, the signer's name and role and the meaning of the signature before it is signed. | end-to-end walkthrough | scripts/guide-shots.mjs |
| FR-UI-03 | medium | Actions a person may not take are not offered, and the reason is shown; the buttons are decided by the same engine that enforces the rules. | end-to-end walkthrough | scripts/guide-shots.mjs |
| FR-UI-04 | medium | Every record shows its complete version history and its own audit trail. | end-to-end walkthrough | scripts/guide-shots.mjs |
| FR-UI-05 | medium | The effective value of every option is shown with its source, what it overrides and what is pending. | end-to-end walkthrough | scripts/guide-shots.mjs |
| FR-UI-06 | medium | The audit trail can be searched and filtered by action, record type and person, and exported as CSV and JSON. | end-to-end walkthrough | scripts/guide-shots.mjs |
| FR-UI-07 | medium | The integrity page verifies the hash chain in the browser and shows the head hash. | end-to-end walkthrough | scripts/guide-shots.mjs |
| FR-UI-08 | medium | Reasons are asked for in a dialog that cannot be submitted empty. | end-to-end walkthrough | scripts/guide-shots.mjs |
| FR-UI-09 | medium | The tree shows nested records to any depth, with records that have no parent at the top; a record's page shows the path above it and the records directly under it. | end-to-end walkthrough | scripts/guide-shots.mjs |
| FR-UI-10 | medium | The parent picker of a nested record never offers the record itself or a record under it. | end-to-end walkthrough | scripts/guide-shots.mjs |
| FR-UI-11 | medium | The configuration editor checks the draft as it is edited, shows every problem next to its cause, and shows the changes and their effect on existing records before a change can be proposed. | end-to-end walkthrough | scripts/guide-shots.mjs |
| FR-UI-12 | medium | A proposed configuration change is shown with its changes on the configuration page and in the inbox of those who may decide; its proposer is not offered the approval. | end-to-end walkthrough | scripts/guide-shots.mjs |
| FR-UI-13 | medium | A record that does not meet the configuration in effect is marked with the reason. | end-to-end walkthrough | scripts/guide-shots.mjs |
| FR-DOC-01 | low | The documentation site builds a search index from the same Markdown files it shows: one entry per section of every document, with the document's title, the section's heading, the heading's address on the page and the section's text. | automated test | server/server.test.ts |
| FR-DOC-02 | low | The documentation search finds the sections that contain every word of the query, ranks a match in a heading or title above a match in the text, links each result to its heading and marks the matched words. | automated test | server/server.test.ts |
| FR-DOC-03 | low | Every document page lists its second- and third-level headings as "On this page", each with an address that exists on the page. | automated test | server/server.test.ts |
| FR-DOC-04 | low | A Mermaid block of a Markdown document is shown as a diagram; its source text is in the page and stays readable where the diagram cannot be drawn. | automated test | server/server.test.ts |