Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,7 @@ jobs:
- name: Build
working-directory: frontend
run: pnpm run build

- name: Build Storybook
working-directory: frontend
run: pnpm run build-storybook
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,14 @@ floating Node version):
```bash
cd frontend && pnpm install
pnpm run lint && pnpm run test && pnpm run build
# Storybook inventory (ADR 0020 tokens): pnpm run build-storybook
```

A run-bearing analysis-run registry empties only after an unrevoked
`analysis_run_retention_grant` and `GRANT analysis_run_retention_admin`
(ADR 0020 / v0.87.0). The documented phrase is not a secret. Do not
expose purge on a public HTTP route.

## CI gates

`.github/workflows/tests.yml` runs the full suite on every PR to `main`.
Expand Down
6 changes: 6 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,12 @@ Demo Analyst and Demo Admin see "Lineage reconstruction · Succeeded ·
Demo Corp" with "3 documents" and Pending / Running / Succeeded times,
and "TEPP measurement · Failed · Demo Corp" whose detail history ends
in Failed / `tepp_not_available`.
A run-bearing registry is emptied only after an unrevoked
`analysis_run_retention_grant` and `GRANT analysis_run_retention_admin`,
then `purge_analysis_run_registry('approved-retention-purge')`
(ADR 0020); a raw `DELETE` and a runtime role that only knows the
public phrase stay rejected. Repeated chip and close controls use
`frontend/src/styles/tokens.css` and the Storybook inventory.

## Phase 6a: fast-mlsirm dependency + Rust toolchain (infra only)

Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.d/0.87.0-retention-purge-grant-admin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# 0.87.0 analysis-run retention purge

Operators empty a run-bearing analysis-run registry only after an
unrevoked `analysis_run_retention_grant` and
`GRANT analysis_run_retention_admin`. Then
`select purge_analysis_run_registry('approved-retention-purge')`.
Export `analysis_run_retention_event`, delete those rows, then roll
back 0020 and 0018. A raw DELETE, a published token without a grant,
and a runtime role that is not the admin role still fail (ADR 0020).

Designers can change chip and close-button appearance in
`frontend/src/styles/tokens.css` and preview the next click in Storybook
(`cd frontend && pnpm run storybook`).
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,23 @@ All notable changes to this project are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning follows
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.87.0] - 2026-08-16

### Added

- Operators can empty a run-bearing analysis-run registry without a
superuser trigger disable. Insert an unrevoked
`analysis_run_retention_grant` for `session_user`, grant
`analysis_run_retention_admin`, then
`select purge_analysis_run_registry('approved-retention-purge')`
(ADR 0020). Export `analysis_run_retention_event`, delete those
rows, then roll back 0020 and 0018. A raw `DELETE`, a published
token without a grant, and a runtime role that is not the admin
role still fail.
- Repeated citation chips and close buttons use named design tokens
in `frontend/src/styles/tokens.css`. Preview them in Storybook
(`cd frontend && pnpm run storybook`).

## [0.86.2] - 2026-08-16

### Fixed
Expand Down
12 changes: 12 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@
Tool-specific pointer. Policy lives in [AGENTS.md](AGENTS.md) and the
ADRs under `docs/adr/`. Do not fork those rules here.

## Analysis-run retention (v0.87.0)

To empty a run-bearing registry, insert an unrevoked
`analysis_run_retention_grant` for `session_user` and
`GRANT analysis_run_retention_admin` (ADR 0020). Then
`select purge_analysis_run_registry('approved-retention-purge')`,
export `analysis_run_retention_event`, delete those rows, and roll
back 0020 then 0018. The published phrase is not a secret. Do not
`DISABLE TRIGGER` as superuser. Do not grant the admin role or a
retention grant to the application `DATABASE_URL` login. ADR 0019
is the R&R catalog-id bind, not this purge.

## Analysis-run seed (v0.85.0)

`make seed` writes a Demo Corp lineage run and a TEPP run on the same
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,12 @@ FastAPI backend over real `fetch()` with the token Keycloak issued.
make up
make seed
cd frontend && cp .env.example .env.local && pnpm install && pnpm run dev
# Repeated chip/close controls: pnpm run storybook
# (Node 24 via frontend/mise.toml; pnpm only)
# Empty a run-bearing registry: insert analysis_run_retention_grant
# for session_user, GRANT analysis_run_retention_admin, then
# select purge_analysis_run_registry('approved-retention-purge').
# The published token is not a grant (ADR 0020).
# -> http://localhost:5173, click "Log in", redirects through the real
# Keycloak login page for demo.analyst / lineageweave-demo-only
```
Expand Down
2 changes: 2 additions & 0 deletions backend/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
_REALM = "lineageweave-demo"
_MIGRATION_PATH = Path(__file__).resolve().parents[2] / "migrations" / "0001_initial_schema.sql"
_REGISTRY_MIGRATION = Path(__file__).resolve().parents[2] / "migrations" / "0018_analysis_run_registry.sql"
_RETENTION_MIGRATION = Path(__file__).resolve().parents[2] / "migrations" / "0020_analysis_run_retention_purge.sql"


def _postgres_available() -> bool:
Expand Down Expand Up @@ -115,6 +116,7 @@ def seeded_db(demo_analyst_token):
with conn.cursor() as cur:
cur.execute(_MIGRATION_PATH.read_text())
cur.execute(_REGISTRY_MIGRATION.read_text())
cur.execute(_RETENTION_MIGRATION.read_text())
cur.execute(
"insert into common_lookup_value (lookup_category, lookup_code, lookup_label) values "
"('corporate_entity_level', 'group', 'Group'), "
Expand Down
1 change: 1 addition & 0 deletions docker/postgres-init/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ COPY migrations/0016_cross_post_actor_identity.sql /docker-entrypoint-initdb.d/1
COPY migrations/0017_prov_o_standard_relations.sql /docker-entrypoint-initdb.d/18-prov-o-standard-relations.sql
COPY migrations/0018_analysis_run_registry.sql /docker-entrypoint-initdb.d/19-analysis-run-registry.sql
COPY migrations/0019_role_catalog_identity.sql /docker-entrypoint-initdb.d/20-role-catalog-identity.sql
COPY migrations/0020_analysis_run_retention_purge.sql /docker-entrypoint-initdb.d/21-analysis-run-retention-purge.sql
# Official image already drops to this account at runtime; declare it so
# the Dockerfile itself satisfies DS-0002 (explicit non-root USER).
USER postgres
7 changes: 4 additions & 3 deletions docs/adr/0013-normalized-analysis-run-registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,9 +209,10 @@ functional dependency and forces duplicate snapshots.

Migration replay is idempotent and rejects lookup-category collisions. The
rollback refuses to remove non-empty registry relations. Evidence must first be
exported or explicitly deleted under an approved retention procedure. An empty
rollback removes the view, tables, functions, and lookup rows and is itself
replayable.
exported, then emptied with `purge_analysis_run_registry` after an unrevoked
`analysis_run_retention_grant` and `analysis_run_retention_admin` membership
(ADR 0020). An empty rollback removes the view, tables, functions, and lookup
rows and is itself replayable.

## Verification

Expand Down
101 changes: 101 additions & 0 deletions docs/adr/0020-analysis-run-retention-purge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# ADR 0020 — Approved retention purge requires a session grant and admin role

**Decision status:** Accepted on this active PR; not protected-main truth until merge
**Date:** 2026-08-16
**Depends on:** ADR 0013 normalized analysis-run registry

## Context

ADR 0013 and migration `0018` make `analysis_run`, `analysis_run_scope`, and
`analysis_run_status_event` immutable. The 0018 rollback refuses to drop
non-empty registry relations and tells operators to export or delete evidence
under an approved retention procedure.

After the first `analysis_run` insert, a raw `DELETE` is rejected. Snapshot
delete is then blocked by the foreign key. Operators following the documented
procedure cannot satisfy `analysis_run_registry_not_empty` without a superuser
`DISABLE TRIGGER`. That is not a supported product path (ISO 15489-1:2016
disposition; NIST SP 800-92 protected audit records).

A `SECURITY DEFINER` function that `PUBLIC` can execute, or that accepts only
a documented phrase, lets any SQL session wipe analysis-run evidence
(NIST SP 800-53 Rev. 5 AC-3; CWE-250). The phrase is a procedure name, not
an authorization secret. The write API is a separate slice; SQL operators
still need a grant that is independent of application `user_account` rows.

Landed #122 occupies ADR 0018 / package 0.86.0 for the team and organization
related-node walk. ADR 0019 binds `cataloged_team_id` /
`cataloged_corporate_entity_id` on `post_summary_role` and must not be
reused here. This decision is the next free slot.

## Decision

Migration `0020_analysis_run_retention_purge.sql` adds a conjunctive
fail-closed purge:

- `analysis_run_retention_grant` — one unrevoked row per
`database_role_name`; history of revoked grants is allowed;
- `analysis_run_retention_admin` — `NOLOGIN` role that receives
`EXECUTE`; `PUBLIC` does not;
- `purge_analysis_run_registry(approval_token text)` — `SECURITY DEFINER`,
checks the unrevoked grant, then `pg_has_role(..., 'member')` on the
admin role, then accepts only `approved-retention-purge`, disables the
three immutability delete triggers inside that call, deletes in FK
order, re-enables the triggers, and writes one
`analysis_run_retention_event`;
- `analysis_run_retention_event` — purged run/snapshot counts, the SHA-256
of the approval token, `invoking_session_role`, `invoking_current_role`,
and optional `client_network_address`. The raw phrase is never stored.

A session `SET` cannot authorize a raw `DELETE`. A table-DML runtime role
that only knows the public phrase cannot call the function. A member of
the admin role without a grant cannot purge. A grant without admin
membership cannot purge. After purge, export the retention event, delete
those rows, roll back 0020, then roll back 0018.

This migration does not insert a grant or grant the admin role to the
migrator. Production `DATABASE_URL` must not be a superuser and must not
hold either privilege.

## Consequences

- A run-bearing registry can be emptied without superuser trigger disable.
- Retention remains an explicit, audited operator action, not a silent
downgrade.
- 0018 rollback stays fail-closed until the registry tables are empty;
0020 rollback stays fail-closed until retention events are exported
and deleted.
- Repeated citation-chip and close-button appearance lives in
`frontend/src/styles/tokens.css` and the Storybook inventory.

## Follow-up

When the authorized write API exists, bind an administrator
`user_account` to the same grant table. Keep the SQL-role grant for
operators who purge from `psql`. Do not expose purge on a public HTTP
route. Split the application login from the migration owner so the
product role cannot execute the function even as table owner.

## References — APA 7th

American Institute of Certified Public Accountants. (2017). *SOC 2®: SOC
for Service Organizations: Trust Services Criteria*.

International Organization for Standardization. (2016). *ISO 15489-1:2016:
Information and documentation—Records management—Part 1: Concepts and
principles*.

Kent, K., & Souppaya, M. (2006). *Guide to computer security log management*
(NIST Special Publication 800-92). National Institute of Standards and
Technology. https://doi.org/10.6028/NIST.SP.800-92

MITRE. (2026). *CWE-250: Execution with unnecessary privileges*.
https://cwe.mitre.org/data/definitions/250.html

National Institute of Standards and Technology. (2020). *Security and
privacy controls for information systems and organizations* (NIST Special
Publication 800-53 Rev. 5). https://doi.org/10.6028/NIST.SP.800-53r5

PostgreSQL Global Development Group. (2026). *PostgreSQL 18 documentation:
5.8. Privileges*.
https://www.postgresql.org/docs/current/ddl-priv.html
21 changes: 18 additions & 3 deletions docs/doctoring/ANALYSIS_RUN_REGISTRY_REFERENCES.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Analysis-run registry standards and research traceability

**Status:** Active PR evidence; not protected-main truth until merge.
**Scope:** Migration 0018, ADR 0013, rollback, and real-PostgreSQL contract tests.
**Scope:** Migrations 0018 and 0020, ADR 0013 / 0020, rollback, and real-PostgreSQL contract tests.

## Standards mapped to implementation

Expand All @@ -12,7 +12,8 @@
| W3C Accessible Name and Description Computation 1.1 | Do not let `aria-label` replace visible text the operator must hear. | Analysis-run digest prefixes live in a labeled group; the prefixes remain the accessible contents and the full digest is on `title` for hover verification. |
| ISO 8601-1:2019 | Use unambiguous timestamp representation and timezone-aware persistence. | PostgreSQL `timestamptz` for availability, capture, cutoff, request, occurrence, and record clocks; tests use explicit `Z` offsets. |
| PostgreSQL 18 constraints and trigger contracts | Put integrity close to durable truth and use constraints for row shape while triggers enforce cross-row state and serialization. | Digest/check constraints, category allowlists, account-scoped uniqueness, shape constraints, immutable-row triggers, shared snapshot-row locking, and serialized status transitions. |
| NIST SP 800-92 | Treat audit records as bounded, protected operational evidence rather than unstructured application logging. | Append-only status events, machine failure codes, actor identity, occurrence/record clocks, fail-closed rollback, and exclusion of raw source/provider payloads. |
| NIST SP 800-92 | Treat audit records as bounded, protected operational evidence rather than unstructured application logging. | Append-only status events, machine failure codes, actor identity, occurrence/record clocks, fail-closed rollback, `invoking_session_role` on each retention event, and exclusion of raw source/provider payloads. |
| NIST SP 800-53 Rev. 5 AC-3 | Enforce least privilege on privileged procedures; a well-known procedure name is not an authorization secret. | `REVOKE ALL` on `purge_analysis_run_registry` from `PUBLIC`; `GRANT EXECUTE` only to `analysis_run_retention_admin`; unrevoked `analysis_run_retention_grant` required (ADR 0020). |
| OpenAPI 3.2.0 | Define explicit versioned API schemas rather than exposing database rows or implementation-specific payloads. | API intentionally deferred; ADR 0013 requires a source-redacting run list/detail contract before a product surface is claimed. |

## Temporal reasoning
Expand Down Expand Up @@ -75,10 +76,17 @@ provenance, retention, and immutable evidence rather than blanket masking.
| Request identity is stable | Reject analysis-run updates; scope and lifecycle live in their own relations. |
| Idempotency is actor-scoped | Permit identical opaque keys for two accounts and reject reuse by the same account. |
| Lifecycle is ordered | Require pending first, contiguous ordinals, monotonic time, legal transitions, terminal finality, and append-only rows. |
| Rollback does not erase audit data silently | Reject rollback with any registry rows and allow replay after explicit cleanup. |
| Rollback does not erase audit data silently | Reject 0018 rollback with any registry rows. A run-bearing registry empties only through an unrevoked `analysis_run_retention_grant` plus `analysis_run_retention_admin`, then `purge_analysis_run_registry('approved-retention-purge')`; a wrong token, a raw `DELETE`, and a runtime role that only knows the public phrase stay rejected. Export then delete `analysis_run_retention_event` before 0020 rollback. |

## APA 7th references

American Institute of Certified Public Accountants. (2017). *SOC 2®: SOC
for Service Organizations: Trust Services Criteria*.

International Organization for Standardization. (2016). *ISO 15489-1:2016:
Information and documentation—Records management—Part 1: Concepts and
principles*.

International Organization for Standardization. (2019). *ISO 8601-1:2019: Date
and time—Representations for information interchange—Part 1: Basic rules*
(confirmed 2024; Amendment 1:2022).
Expand All @@ -87,6 +95,10 @@ Kent, K., & Souppaya, M. (2006). *Guide to computer security log management*
(NIST Special Publication 800-92). National Institute of Standards and
Technology. https://doi.org/10.6028/NIST.SP.800-92

National Institute of Standards and Technology. (2020). *Security and
privacy controls for information systems and organizations* (NIST Special
Publication 800-53 Rev. 5). https://doi.org/10.6028/NIST.SP.800-53r5

Moreau, L., & Missier, P. (Eds.). (2013). *PROV-DM: The PROV data model*.
World Wide Web Consortium. https://www.w3.org/TR/prov-dm/

Expand All @@ -96,6 +108,9 @@ https://spec.openapis.org/oas/v3.2.0.html
PostgreSQL Global Development Group. (2026). *PostgreSQL 18 documentation:
5.5. Constraints*. https://www.postgresql.org/docs/current/ddl-constraints.html

PostgreSQL Global Development Group. (2026). *PostgreSQL 18 documentation:
5.8. Privileges*. https://www.postgresql.org/docs/current/ddl-priv.html

World Wide Web Consortium. (2013). *PROV-O: The PROV ontology* (W3C
Recommendation). https://www.w3.org/TR/prov-o/

Expand Down
20 changes: 20 additions & 0 deletions docs/doctoring/DESIGN_TOKEN_REFERENCES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Design-token and Storybook traceability

**Status:** Active PR evidence; not protected-main truth until merge.
**Scope:** `frontend/src/styles/tokens.css`, repeated chip/close modules, and
the Storybook inventory.

## Standards mapped to implementation

| Source | Product implication | Implemented evidence |
|---|---|---|
| W3C Design Tokens Format Module 1.0 | Name color, space, type, and radius once; consume those names from repeated objects. | `frontend/src/styles/tokens.css` defines `--color-*`, `--space-*`, `--radius-chip`, and `--font-*`. `CitationChip` and `PopupCloseButton` read those names through `App.css`. |
| Storybook for React & Vite | Catalog repeated controls so a buyer can try the next click without reading `App.tsx`. | `frontend/src/components/*.stories.tsx` and `docs/storybook-inventory.md`. |

## APA 7th references

Design Tokens Community Group. (2025). *Design Tokens Format Module 1.0*
(W3C Community Group Draft Report). https://tr.designtokens.org/format/

Storybook. (2026). *Storybook for React & Vite*.
https://storybook.js.org/docs/get-started/frameworks/react-vite
21 changes: 21 additions & 0 deletions docs/storybook-inventory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Storybook inventory

Open the catalog after `cd frontend && pnpm run storybook`. Each story is a
buyer-facing control you can click before changing product CSS.

| Story | Buyer next action | Token / module |
|---|---|---|
| `Evidence/CitationChip` | Click a cited title to open that source post. | `--color-chip-border`, `--radius-chip`, `CitationChip` |
| `Chrome/PopupCloseButton` | Close the evidence panel or post popup. | `--space-close-inset`, `--font-size-close`, `PopupCloseButton` |

Repeated web objects must use `frontend/src/styles/tokens.css` and a module
under `frontend/src/components/`. Do not add a second Node package manager;
Storybook is installed with the existing pnpm pin on Node 24.

## References — APA 7th

Design Tokens Community Group. (2025). *Design Tokens Format Module 1.0*
(W3C Community Group Draft Report). https://tr.designtokens.org/format/

Storybook. (2026). *Storybook for React & Vite*.
https://storybook.js.org/docs/get-started/frameworks/react-vite
1 change: 1 addition & 0 deletions frontend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ lerna-debug.log*
node_modules
dist
dist-ssr
storybook-static
*.local

# Editor directories and files
Expand Down
12 changes: 12 additions & 0 deletions frontend/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { StorybookConfig } from "@storybook/react-vite";

const config: StorybookConfig = {
stories: ["../src/**/*.stories.@(ts|tsx)"],
addons: [],
framework: {
name: "@storybook/react-vite",
options: {},
},
};

export default config;
11 changes: 11 additions & 0 deletions frontend/.storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { Preview } from "@storybook/react-vite";
import "../src/index.css";
import "../src/App.css";

const preview: Preview = {
parameters: {
controls: { matchers: { color: /(background|color)$/i } },
},
};

export default preview;
Loading
Loading