Skip to content
Closed
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 ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,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 with
`purge_analysis_run_registry('approved-retention-purge')` after an
unrevoked `analysis_run_retention_grant` (ADR 0018 / 0019); a raw
`DELETE` stays rejected. The retention event names
`invoking_session_role`. 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
10 changes: 10 additions & 0 deletions CHANGELOG.d/0.86.0-retention-purge-and-tokens.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# 0.86.0 retention purge and design tokens

Operators can empty a run-bearing analysis-run registry without a superuser
trigger disable: `select purge_analysis_run_registry('approved-retention-purge')`,
export `analysis_run_retention_event`, delete those rows, then roll back 0019
and 0018. A raw DELETE still fails.

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`).
7 changes: 7 additions & 0 deletions CHANGELOG.d/0.87.0-retention-grant.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# 0.87.0 retention grant and purge actor

A published token does not empty the registry. Insert an unrevoked
`analysis_run_retention_grant` for the operator `session_user`, then run
`select purge_analysis_run_registry('approved-retention-purge')`. Export
`analysis_run_retention_event` to see `invoking_session_role`. `PUBLIC`
cannot execute the function.
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,31 @@ 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

- A published purge token is not enough. Insert an unrevoked
`analysis_run_retention_grant` for the operator `session_user`, then
run `select purge_analysis_run_registry('approved-retention-purge')`.
The exportable `analysis_run_retention_event` names
`invoking_session_role`. `PUBLIC` cannot execute the function
(ADR 0019).

## [0.86.0] - 2026-08-16

### Added

- Operators can empty a run-bearing analysis-run registry without a
superuser trigger disable. After export, run
`select purge_analysis_run_registry('approved-retention-purge')`,
export `analysis_run_retention_event`, delete those rows, then roll
back 0019 and 0018. A raw `DELETE` of a run still fails (ADR 0018).
- Repeated chip and close-button styles live in
`frontend/src/styles/tokens.css`. Open
`cd frontend && pnpm run storybook` and click a citation chip or
close control before changing product CSS.

## [0.84.0] - 2026-08-16

### Added
Expand Down
9 changes: 9 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
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)

Confirm `session_user` has an unrevoked `analysis_run_retention_grant`
row (insert one if you are not the migrator). Then run
`select purge_analysis_run_registry('approved-retention-purge')`,
export `analysis_run_retention_event` (it names `invoking_session_role`),
delete those rows, then roll back 0019 and 0018. Do not `DISABLE TRIGGER`
as superuser. The published token is not a grant.

## Analysis-run seed (v0.84.0)

`make seed` writes a Demo Corp lineage run and a TEPP run on the same
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,11 @@ 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, then select purge_analysis_run_registry(
# 'approved-retention-purge'). The published token is not a grant.
# -> 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 @@ -31,6 +31,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" / "0019_analysis_run_retention_purge.sql"


def _postgres_available() -> bool:
Expand Down Expand Up @@ -114,6 +115,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 @@ -24,6 +24,7 @@ COPY migrations/0015_organization_name_resolution.sql /docker-entrypoint-initdb.
COPY migrations/0016_cross_post_actor_identity.sql /docker-entrypoint-initdb.d/17-cross-post-actor-identity.sql
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_analysis_run_retention_purge.sql /docker-entrypoint-initdb.d/20-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
8 changes: 5 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,11 @@ 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('approved-retention-purge')`
after an unrevoked `analysis_run_retention_grant` (ADR 0018 / 0019). A raw
`DELETE` of a run, scope, or status event stays rejected. After purge, export
and delete `analysis_run_retention_event`, roll back 0019, then roll back 0018. An empty 0018 rollback removes the view,
tables, functions, and lookup rows and is itself replayable.

## Verification

Expand Down
56 changes: 56 additions & 0 deletions docs/adr/0018-analysis-run-retention-purge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# ADR 0018 — Approved retention purge empties an immutable analysis-run registry

**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).

## Decision

Migration `0019_analysis_run_retention_purge.sql` adds:

- `purge_analysis_run_registry(approval_token text)` — `SECURITY DEFINER`,
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 and the SHA-256
of the approval token. The raw phrase is never stored.

A session `SET` cannot authorize a raw `DELETE`. After purge, export the
retention event, delete those rows, roll back 0019, then roll back 0018.

## 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.

## Follow-up

ADR 0019 binds purge to an unrevoked `analysis_run_retention_grant` on
`session_user`, records `invoking_session_role`, and revokes `PUBLIC`
execute. Bind an administrator `user_account` to that grant table when
the write API exists. Do not expose purge on a public HTTP route.

## References — APA 7th

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
63 changes: 63 additions & 0 deletions docs/adr/0019-analysis-run-retention-grant.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# ADR 0019 — Retention purge requires an unrevoked database-role grant

**Decision status:** Accepted on this active PR; not protected-main truth until merge
**Date:** 2026-08-16
**Depends on:** ADR 0018 approved retention purge

## Context

ADR 0018 publishes `purge_analysis_run_registry('approved-retention-purge')`
as the only supported way to empty a run-bearing registry. The approval
phrase is documentation, not a secret. A `SECURITY DEFINER` function that
`PUBLIC` can execute therefore lets any SQL session wipe analysis-run
evidence (NIST SP 800-53 Rev. 5 AC-3; CWE-250). The retention event also
omitted the caller, so a compliance officer cannot answer who purged
(NIST SP 800-53 Rev. 5 AU-3; NIST SP 800-92).

The write API is a separate slice. SQL operators still need a grant that
is independent of application `user_account` rows.

## Decision

Migration `0019_analysis_run_retention_purge.sql` (this successor) adds:

- `analysis_run_retention_grant` — one unrevoked row per
`database_role_name`; history of revoked grants is allowed;
- a grant check on `session_user` before the token check
(`analysis_run_retention_not_granted`);
- `invoking_session_role` on `analysis_run_retention_event` — the
caller's `session_user`, never the `SECURITY DEFINER` owner;
- `REVOKE ALL ON FUNCTION purge_analysis_run_registry(text) FROM PUBLIC`.

The migrator's `session_user` receives the first grant so `make seed`
and the live contract tests keep working. Insert another grant before
delegating purge to a second operator role. Do not expose purge on a
public HTTP route.

## Consequences

- A published token without a grant cannot empty the registry.
- The exportable retention event names the database role that purged.
- 0018 rollback stays fail-closed until registry tables are empty;
0019 rollback stays fail-closed until retention events are exported
and deleted.

## 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`.

## References — APA 7th

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

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
16 changes: 14 additions & 2 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–0019, ADR 0013 / 0018 / 0019, rollback, and real-PostgreSQL contract tests.

## Standards mapped to implementation

Expand Down Expand Up @@ -74,10 +74,22 @@ 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 `purge_analysis_run_registry('approved-retention-purge')`; a missing grant, a wrong token, and a raw `DELETE` stay rejected. The retention event names `invoking_session_role`. Export then delete `analysis_run_retention_event` before 0019 rollback. |

## APA 7th references

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

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

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

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 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