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
7 changes: 5 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -463,9 +463,12 @@ in SQL: the requester always sees their own run; a corporate-entity or
process-unit scope is visible only to affiliated accounts; a
thread-group scope is visible only when the account can already see a
post in that group; `all_visible` is requester-only. Hidden runs 404. Detail also lists ABAC-visible post titles in the
run's scope so a buyer can open a post without seeing hidden rows.
run's scope whose `created_at` is at or before the run
`knowledge_cutoff`, so a later post cannot appear inside a historical
reconstruction (ADR 0016). Detail also returns revision and
configuration digest prefixes.
The home list is clickable: `GET /api/analysis-runs/{id}` fills a
labeled detail (cutoff, requested date, counts, status history)
labeled detail (cutoff, requested date, counts, status history, digests)
without exposing a DSN or raw record. Status history is detail-only
and uses lookup labels plus occurrence times; a failure event keeps
its machine `failure_code` rather than an invented caption. The
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.d/0.83.0-analysis-run-cutoff-posts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# 0.83.0 analysis-run cutoff-scoped posts

Detail lists only posts known at the run knowledge cutoff and shows
revision/config digest prefixes so an operator can confirm the run
they approved. After `make seed`, Demo public post still opens; Late
Demo public post stays hidden.
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.83.0] - 2026-08-16

### Fixed

- Analysis-run detail now lists only posts whose `created_at` is at or
before the run `knowledge_cutoff`. After `make seed`, Demo public post
(2026-01-10) still opens from the January 12 reconstruction; Late Demo
public post (2026-01-13) does not. Open a later run, or ask an
administrator to capture a newer snapshot, when the list is empty.

### Added

- Run detail shows revision and configuration digest prefixes so an
operator can confirm the run matches the code they approved (ADR 0016).
- `AnalysisRunsPanel` is a standalone home-page module with a Storybook
inventory for the next frontend toolchain slice.

## [0.82.0] - 2026-08-16

### Added
Expand Down
8 changes: 8 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# CLAUDE.md

Read [AGENTS.md](AGENTS.md) first. Product architecture lives in
[ARCHITECTURE.md](ARCHITECTURE.md). Active analysis-run decisions are
ADR 0013–0016 under `docs/adr/`.

This repository ships synthetic data only. Do not add fixtures, tests,
or examples derived from a real organization's records.
21 changes: 16 additions & 5 deletions backend/app/analysis_run_ingestion.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

The registry itself is issue #89 / migration 0018. This module is the
product projection: an account sees only runs they requested or whose
scope they already have ABAC authority to walk. Aggregate counts and
lookup labels come back; source SQL, DSNs, raw records, and provider
payloads never do.
scope they already have ABAC authority to walk. Detail post titles are
further limited to posts known at the run knowledge cutoff. Aggregate
counts and lookup labels come back; source SQL, DSNs, raw records, and
provider payloads never do.
"""

from __future__ import annotations
Expand Down Expand Up @@ -226,6 +227,7 @@ async def fetch_visible_analysis_run(
row["process_unit_id"],
row["scope_key"],
affiliated_entity_ids,
row["knowledge_cutoff"],
)
return detail

Expand All @@ -237,33 +239,42 @@ async def fetch_visible_scope_posts(
process_unit_id: Any,
scope_key: str | None,
affiliated_entity_ids: list[str],
knowledge_cutoff: Any,
) -> list[dict[str, str]]:
"""ABAC-visible post titles in the run's scope -- never a hidden body."""
"""ABAC-visible titles known at the run cutoff -- never a hidden body."""
if scope_kind_code == "analysis_scope_corporate_entity" and corporate_entity_id:
rows = await conn.fetch(
"select post_id, post_title, visibility_code, corporate_entity_id "
"from source_post where corporate_entity_id = $1 "
"and created_at <= $2 "
"order by created_at, post_title",
corporate_entity_id,
knowledge_cutoff,
)
elif scope_kind_code == "analysis_scope_process_unit" and process_unit_id:
rows = await conn.fetch(
"select post_id, post_title, visibility_code, corporate_entity_id "
"from source_post where process_unit_id = $1 "
"and created_at <= $2 "
"order by created_at, post_title",
process_unit_id,
knowledge_cutoff,
)
elif scope_kind_code == "analysis_scope_thread_group" and scope_key:
rows = await conn.fetch(
"select post_id, post_title, visibility_code, corporate_entity_id "
"from source_post where thread_group_key = $1 "
"and created_at <= $2 "
"order by created_at, post_title",
scope_key,
knowledge_cutoff,
)
elif scope_kind_code == "analysis_scope_all_visible":
rows = await conn.fetch(
"select post_id, post_title, visibility_code, corporate_entity_id "
"from source_post order by created_at, post_title"
"from source_post where created_at <= $1 "
"order by created_at, post_title",
knowledge_cutoff,
)
else:
return []
Expand Down
25 changes: 21 additions & 4 deletions backend/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,11 +296,18 @@ def _seed_analysis_run(
(account_id, role_id),
)

def _insert_post(title: str, corporate_entity_id, visibility_code: str, body: str = "body") -> str:
def _insert_post(
title: str,
corporate_entity_id,
visibility_code: str,
body: str = "body",
created_at: str = "2026-01-10T09:00:00Z",
) -> str:
cur.execute(
"insert into source_post (author_account_id, corporate_entity_id, post_title, post_body, voc_type_code, visibility_code) "
"values (%s, %s, %s, %s, 'voc', %s) returning post_id",
(account_id, corporate_entity_id, title, body, visibility_code),
"insert into source_post (author_account_id, corporate_entity_id, "
"post_title, post_body, voc_type_code, visibility_code, created_at) "
"values (%s, %s, %s, %s, 'voc', %s, %s) returning post_id",
(account_id, corporate_entity_id, title, body, visibility_code, created_at),
)
return str(cur.fetchone()[0])

Expand All @@ -313,6 +320,13 @@ def _insert_post(title: str, corporate_entity_id, visibility_code: str, body: st
"The weather in Gwangju was irrelevant.",
)
other_private_post_id = _insert_post("Other-corp private post", other_corp_id, "private")
_insert_post(
"Late own-corp private post",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seeded_db now has a fourth own-corp private post. GET /api/posts is not cutoff-scoped, so test_post_list_includes_public_and_own_corp_but_excludes_other_corp still asserting {"Public post", "Own-corp private post"} (line 523, outside this hunk) will fail when the live API suite runs. The fixture docstring still says three rows.

Include Late own-corp private post in that exact set, or insert the late post only inside the analysis-run test. Also GET hidden_all_visible_id and expect 404 — list exclusion alone does not prove the detail path.

own_corp_id,
"private",
"Written after the analysis-run knowledge cutoff.",
created_at="2026-01-13T09:00:00Z",
)

cur.execute(
"insert into cataloged_person (person_name, person_side_code) values "
Expand Down Expand Up @@ -477,7 +491,10 @@ def test_analysis_runs_are_labeled_aggregates_and_hide_other_scopes(
assert all("failure_code" not in event for event in history)
titles = {post["post_title"] for post in body["visible_posts"]}
assert "Own-corp private post" in titles
assert "Late own-corp private post" not in titles
assert "Other-corp private post" not in titles
assert body["code_revision_sha"] == "c" * 40
assert body["configuration_sha256"] == "b" * 64
assert "postgresql://" not in str(body)
assert "visible_posts" not in visible

Expand Down
2 changes: 2 additions & 0 deletions docs/adr/0014-authorized-analysis-run-read.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ LineageWeave owns a fail-closed read projection of the #89 registry:
- `GET /api/analysis-runs/{id}` also returns the append-only labeled
`status_history`. The list does not. A failed event may include the
stored machine `failure_code`; this slice does not invent a label.
- Detail post titles are further limited to
`source_post.created_at <= knowledge_cutoff` (ADR 0016).
- TEPP remains a versioned `AnalysisRunRequest` consumer
(`lineageweave.tepp_client`). This slice does not fork TEPP arithmetic.
- contextual-orchestrator remains the only LLM path. This slice does not
Expand Down
59 changes: 59 additions & 0 deletions docs/adr/0016-analysis-run-knowledge-cutoff-posts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# ADR 0016 — Analysis-run post lists apply the run knowledge cutoff

**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; ADR 0014 authorized analysis-run read
**Refs:** Issue #79 (Milestone 2 parent); PR #89 registry + read projection

## Context

PR #89 stores `knowledge_cutoff` on each `analysis_run` and ADR 0013
requires that a run may use only evidence available at that cutoff.
The v0.82 authorized detail listed every ABAC-visible post in the run
scope. A post written after the cutoff therefore appeared inside a
historical reconstruction. That is the buyer-visible temporal leak:
an operator cannot trust that "this run" is the evidence the run was
allowed to know.

The registry already distinguishes snapshot availability from run
cutoff (Jensen & Snodgrass, 1999; W3C Time Ontology in OWL, 2022).
The read projection must apply the same as-of predicate when it
projects `source_post` titles.

## Decision

`GET /api/analysis-runs/{id}` includes a post title only when:

1. the post is in the run's scope;
2. the caller already has ABAC authority to see that post;
3. `source_post.created_at <= analysis_run.knowledge_cutoff`.

Hidden or later posts never appear. The list payload stays
aggregates-only. Detail also returns `code_revision_sha` and
`configuration_sha256` so an operator can confirm the run matches
the code and configuration they approved. Prefixes are shown in the
home panel; full digests remain on the API.

The home Analysis runs panel lives in `AnalysisRunsPanel` so the
repeating list/detail object can be inventoried for Storybook without
growing `App.tsx`.

## Consequences

Fixture posts that belong in a January 2026 run must carry a
`created_at` at or before that cutoff. `make seed` stamps Demo public
and Demo private posts at 2026-01-10 and inserts Late Demo public post
at 2026-01-13 as the falsifiable own-corp counter-example. Write/rebuild
APIs, TEPP submission, and run-scoped post bodies remain later slices.

## References

Jensen, C. S., & Snodgrass, R. T. (1999). Temporal data management.
*IEEE Transactions on Knowledge and Data Engineering, 11*(1), 36–44.
https://doi.org/10.1109/69.755613

Snodgrass, R. T. (Ed.). (1995). *The TSQL2 temporal query language*.
Springer. https://doi.org/10.1007/978-1-4615-2289-8

World Wide Web Consortium. (2022). *Time ontology in OWL* (W3C
Recommendation). https://www.w3.org/TR/owl-time/
8 changes: 8 additions & 0 deletions docs/doctoring/ANALYSIS_RUN_REGISTRY_REFERENCES.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ provenance, retention, and immutable evidence rather than blanket masking.
|---|---|
| One snapshot supports multiple analyses | Insert two runs over one snapshot with different valid cutoffs. |
| Future evidence is excluded | Reject a run whose cutoff precedes the snapshot's maximum availability time. |
| Later posts are excluded from a historical run | A post with `created_at` after `knowledge_cutoff` is absent from `visible_posts`. |
| Evidence cannot change after derivation | Reject snapshot/count updates and count insert/delete after the first run. |
| Count/run race is serialized | Both paths acquire the snapshot row first; a later concurrency test must prove one legal winner and no lost freeze. |
| Request identity is stable | Reject analysis-run updates; scope and lifecycle live in their own relations. |
Expand All @@ -82,13 +83,20 @@ 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).

Jensen, C. S., & Snodgrass, R. T. (1999). Temporal data management. *IEEE
Transactions on Knowledge and Data Engineering, 11*(1), 36–44.
https://doi.org/10.1109/69.755613

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

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

Snodgrass, R. T. (Ed.). (1995). *The TSQL2 temporal query language*.
Springer. https://doi.org/10.1007/978-1-4615-2289-8

OpenAPI Initiative. (2025). *OpenAPI specification, version 3.2.0*.
https://spec.openapis.org/oas/v3.2.0.html

Expand Down
30 changes: 30 additions & 0 deletions docs/storybook/INVENTORY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Storybook inventory

**Status:** Component inventory for the repeating home-page objects.
Storybook itself is the next frontend toolchain slice; do not add a
second Node package manager while adding it (`frontend/mise.toml`
pins Node 24, Corepack pnpm only).

## Repeating objects

| Object | Module | States a Storybook story must cover |
|---|---|---|
| Analysis run list | `frontend/src/AnalysisRunsPanel.tsx` | empty (`make seed` hint), loading, one succeeded Demo Corp run, hidden-run error |
| Analysis run detail | same | cutoff + requested date, status history, digest prefixes, in-cutoff posts, no posts at cutoff |
| Post list chip | `frontend/src/App.tsx` | public / private badges |
| Calendar commitment | `frontend/src/App.tsx` | dated open ticket |
| Period report row | `frontend/src/App.tsx` | mean θ, CAT item, member click-through |
| Post popup | `frontend/src/App.tsx` | summary, lineage, Keyman, tickets, chat |

## Design tokens

Repeating chips, badges, and list rows must use the CSS custom
properties in `frontend/src/index.css` (`--lw-space-*`,
`--lw-radius-*`, `--lw-color-danger`) rather than one-off hex values
when those objects are next extracted.

## Next action

Add Storybook via `pnpm` in `frontend/` with `@storybook/react-vite`,
then write CSF stories for `AnalysisRunsPanel` first. Keep stories on
synthetic Demo Corp fixtures only.
10 changes: 9 additions & 1 deletion docs/superpowers/plans/2026-08-15-analysis-run-registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,15 @@ for Milestone 2 analysis requests and lifecycle evidence.
8. Obtain independent exact-head review and merge only after the parent PR is on
protected `main` and base-sensitive evidence is regenerated.

## Task 5 — Next bounded vertical slice
## Task 5 — Knowledge-cutoff post projection (v0.83.0)

1. Fail if a post written after `knowledge_cutoff` appears in
`visible_posts`.
2. Apply `created_at <= knowledge_cutoff` in the authorized read.
3. Show revision/config digest prefixes on the home detail.
4. Extract `AnalysisRunsPanel` and inventory Storybook states.

## Task 6 — Next bounded vertical slice

After this registry reaches protected main:

Expand Down
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
"version": "0.82.0",
"version": "0.83.0",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
Loading