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
3 changes: 3 additions & 0 deletions CHANGELOG.d/0.86.3-requester-thread-cutoff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
A thread-group run you requested stays off the home list unless that
thread has an in-cutoff visible post. Same-named R&R people bind the
earliest catalog row.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ 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.86.3] - 2026-08-16

### Fixed

- A thread-group analysis-run you requested no longer appears on the
home list when that thread has no ABAC-visible post at or before
`knowledge_cutoff` (ADR 0018). Open a thread that has an in-cutoff
visible post, then request again. Detail of the hidden row is 404.
Comment on lines +11 to +14

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.

Important: the recovery sentence overclaims the product.

create_pending_analysis_run still rejects every scope except analysis_scope_corporate_entity ("Other scopes are not available yet."). The home button only POSTs analysis_run_lineage with no thread key. Requesting again cannot attach a January thread-group run, and a new corporate run is a different row.

The honest next action is: that January row stays off the list until the thread already has an ABAC-visible post at or before knowledge_cutoff. Same note is in docs/adr/0018-related-nodes-team-org-walk.md consequences.

Comment on lines +11 to +14

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

“Open a thread … then request again” is not a product path. create_pending_analysis_run still 422s any non-corporate scope; the home button only POSTs analysis_run_lineage. The January row appears when an in-cutoff visible post already exists. #168 corrects this copy.

- An R&R person name that matches two catalog rows now binds the
earliest `created_at`, then `person_id`. A later same-named Keyman
row no longer steals the mention.

## [0.86.2] - 2026-08-16

### Fixed
Expand Down
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,7 @@ Opening a cutoff title shows the live post -- compare it with the
cutoff before treating the body as reconstructed evidence (ADR 0016).
`POST /api/analysis-runs` records Pending on an authorized
cutoff capture (ADR 0017) and does not reconstruct lineage.
A thread-group run lists only when an ABAC-visible post exists at or
before `knowledge_cutoff`, even when the signed-in account requested
the run (ADR 0018). Requesting a January thread that has no in-cutoff
visible post does not put that row on the home list.
55 changes: 33 additions & 22 deletions backend/app/analysis_run_ingestion.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"""Authorized, source-redacting reads of the Milestone 2 analysis-run registry.

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.
product projection: an account sees runs they requested or whose scope
they already have ABAC authority to walk, except a thread-group run
also needs an in-cutoff visible post (ADR 0018). Requester ownership
does not bypass that clock. Aggregate counts and lookup labels come
back; source SQL, DSNs, raw records, and provider payloads never do.

``create_pending_analysis_run`` (ADR 0017) writes snapshot, counts, run,
scope, and the first Pending event atomically. It does not reconstruct
Expand Down Expand Up @@ -33,23 +34,8 @@
"analysis_run_tepp": "tepp-run-v1",
}

_VISIBLE_RUN_SQL = """
run.requested_by_account_id = $1
or (
scope.scope_kind_code = 'analysis_scope_corporate_entity'
and scope.corporate_entity_id = any($2::uuid[])
)
or (
scope.scope_kind_code = 'analysis_scope_process_unit'
and exists (
select 1 from account_affiliation aff
where aff.user_account_id = $1
and aff.process_unit_id = scope.process_unit_id
)
)
or (
scope.scope_kind_code = 'analysis_scope_thread_group'
and exists (
_THREAD_GROUP_IN_CUTOFF_SQL = """
exists (
select 1 from source_post p
where p.thread_group_key = scope.scope_key
and p.created_at <= run.knowledge_cutoff
Expand All @@ -58,6 +44,31 @@
or p.corporate_entity_id = any($2::uuid[])
)
)
"""

_VISIBLE_RUN_SQL = f"""
(
run.requested_by_account_id = $1
or (
scope.scope_kind_code = 'analysis_scope_corporate_entity'
and scope.corporate_entity_id = any($2::uuid[])
)
or (
scope.scope_kind_code = 'analysis_scope_process_unit'
and exists (
select 1 from account_affiliation aff
where aff.user_account_id = $1
and aff.process_unit_id = scope.process_unit_id
)
)
or (
scope.scope_kind_code = 'analysis_scope_thread_group'
and {_THREAD_GROUP_IN_CUTOFF_SQL}
)
)
and (
scope.scope_kind_code <> 'analysis_scope_thread_group'
or {_THREAD_GROUP_IN_CUTOFF_SQL}
)
"""

Expand Down Expand Up @@ -203,7 +214,7 @@ async def fetch_visible_analysis_runs(
account_id: str,
affiliated_entity_ids: list[str],
) -> list[dict[str, Any]]:
"""Runs the account requested or whose scope they may already walk."""
"""Runs the account may walk, with thread-group cutoff still applied."""
rows = await conn.fetch(
_RUN_SELECT.format(where=_VISIBLE_RUN_SQL),
account_id,
Expand Down
6 changes: 4 additions & 2 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1194,8 +1194,10 @@ async def list_analysis_runs(
) -> dict[str, Any]:
"""Authorized analysis-run list: aggregates and labels only.

Hidden scopes 404 at the item path and never appear here. The
payload has no source SQL, DSN, raw record, or provider body.
Hidden scopes 404 at the item path and never appear here. A
thread-group run you requested still needs an in-cutoff visible
post (ADR 0018). The payload has no source SQL, DSN, raw record,
or provider body.
"""
_require_post_read(account)
async with pool.acquire() as conn:
Expand Down
5 changes: 4 additions & 1 deletion backend/app/post_summary_ingestion.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,10 @@ async def _replace_summary_projection(
)
elif role.actor_type_code == ACTOR_TYPE_PERSON:
person_row = await conn.fetchrow(
"select person_id from cataloged_person where person_name = $1 limit 1",
"select person_id from cataloged_person "
"where person_name = $1 "
"order by created_at, person_id "
"limit 1",
role.actor_name,
)
if person_row is not None:
Expand Down
80 changes: 80 additions & 0 deletions backend/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1704,6 +1704,86 @@ def test_thread_group_run_list_honors_knowledge_cutoff(
assert seeded_db["visible_run_id"] in ids


def test_requester_owned_thread_group_run_list_honors_knowledge_cutoff(
client, demo_analyst_token, seeded_db
) -> None:
"""Requesting a January thread-group run does not list it without an in-cutoff post."""

admin_conn = psycopg2.connect(seeded_db["dsn"])
admin_conn.autocommit = True
try:
with admin_conn.cursor() as cur:
cur.execute(
"insert into source_post (author_account_id, corporate_entity_id, post_title, post_body, voc_type_code, visibility_code, thread_group_key, created_at) "
"select author_account_id, corporate_entity_id, %s, %s, 'voc', 'public', %s, %s "
"from source_post where post_id = %s",
(
"Late requester thread-group post",
"Written after the January cutoff.",
"late-requester-thread-group",
"2026-01-20T12:00:00Z",
seeded_db["own_private_post_id"],
),
)
cur.execute(
"""
insert into analysis_source_snapshot
(snapshot_sha256, source_contract_version,
maximum_available_time, captured_at)
values (%s, 'source-contract-v1',
'2026-01-12T00:00:00Z', '2026-01-12T00:05:00Z')
returning analysis_source_snapshot_id
""",
("e" * 64,),
)
snapshot_id = cur.fetchone()[0]
cur.execute(
"""
insert into analysis_run
(analysis_source_snapshot_id, run_kind_code, idempotency_key,
requested_by_account_id, knowledge_cutoff,
configuration_schema_version, configuration_sha256,
code_revision_sha, requested_at)
values (%s, 'analysis_run_lineage', %s,
(select user_account_id from user_account
where email_address = 'test.analyst@example.test'),
'2026-01-12T12:00:00Z', 'lineage-run-v1', %s, %s,
'2026-01-12T12:30:00Z')
returning analysis_run_id
""",
(snapshot_id, "hidden-own-late-thread", "b" * 64, "c" * 40),
)
run_id = str(cur.fetchone()[0])
cur.execute(
"""
insert into analysis_run_scope
(analysis_run_id, scope_kind_code, scope_key)
values (%s, 'analysis_scope_thread_group', 'late-requester-thread-group')
""",
(run_id,),
)
cur.execute(
"""
insert into analysis_run_status_event
(analysis_run_id, status_ordinal, status_code, occurred_at)
values (%s, 1, 'analysis_status_succeeded', '2026-01-12T12:33:00Z')
""",
(run_id,),
)
finally:
admin_conn.close()

headers = {"Authorization": f"Bearer {demo_analyst_token}"}
listed = client.get("/api/analysis-runs", headers=headers)
assert listed.status_code == 200
ids = {run["analysis_run_id"] for run in listed.json()["analysis_runs"]}
assert run_id not in ids
assert seeded_db["visible_run_id"] in ids

hidden = client.get(f"/api/analysis-runs/{run_id}", headers=headers)
assert hidden.status_code == 404


def test_first_mention_of_a_new_counterparty_creates_a_real_corporate_entity(
client, demo_analyst_token, seeded_db, monkeypatch
) -> None:
Expand Down
3 changes: 3 additions & 0 deletions docs/adr/0009-cross-post-actor-identity.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ counterparty and Keyman affiliation already resolves against).
**Person** (an R&R actor, not a Keyman): opportunistically joined to an
*existing* `cataloged_person` row by exact name match, when Keyman
extraction has already cataloged that name on this or another post.
When two rows share `person_name`, the join takes the earliest
`created_at`, then `person_id` — it does not invent a person or pick
an arbitrary `LIMIT 1` row.
R&R does not create a new person identity itself -- `cataloged_person`
requires `person_side_code` (our-side vs. counterparty), which R&R's
prompt does not currently ask for and Keyman's does; inventing one here
Expand Down
6 changes: 6 additions & 0 deletions docs/adr/0018-related-nodes-team-org-walk.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ the id by `corporate_entity.entity_name`.

Thread-group run list visibility requires at least one ABAC-visible
`source_post` whose `created_at` is at or before `knowledge_cutoff`.
Requester ownership (`requested_by_account_id`) does not bypass that
clock: a run you requested on a thread that has no in-cutoff visible
post stays off the home list and returns 404 on detail.

## Consequences

Expand All @@ -49,6 +52,9 @@ Thread-group run list visibility requires at least one ABAC-visible
organization chip.
- A later public post in a thread group no longer lists a January run
that could not have known that post.
- Requesting that January run yourself does not put it on your home
list. Open a thread that has an in-cutoff visible post, then request
again.
Comment on lines +55 to +57

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same dishonest next action. Request still records a corporate-scope lineage run (ADR 0017). The list row appears when the thread already has an in-cutoff visible post.


## References

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.86.2",
"version": "0.86.3",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
2 changes: 1 addition & 1 deletion lineageweave/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,4 @@
"sentence_excerpts",
]

__version__ = "0.86.2"
__version__ = "0.86.3"
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "lineageweave"
version = "0.86.2"
version = "0.86.3"
description = "Reconstructs git-branch-style lineage DAGs from scattered short records using multi-channel score fusion and LLM adjudication."
readme = "README.md"
license = { text = "MIT" }
Expand Down
Loading
Loading