diff --git a/AGENTS.md b/AGENTS.md index 86665233c..e26c146d2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -113,6 +113,9 @@ v0.88.0). Do not invent a theta. Opening a cutoff-rewritten title shows **Body this run knew** from `source_post_revision` beside the live rewrite (ADR 0025 / v2.1.0). Do not invent the earlier sentence when no revision covers the cutoff. +After `make seed`, the January 12 Demo Corp lineage and TEPP runs list +Demo public post and do not list Late Demo public post (2026-01-13). +The live post list still shows Late Demo (ADR 0016). A corporate-entity similarity result has three outcomes: unique, miss, or tie (ADR 0026). A tie is not a miss. Keep the organization name diff --git a/CHANGELOG.d/2.12.3-late-demo-cutoff-post.md b/CHANGELOG.d/2.12.3-late-demo-cutoff-post.md new file mode 100644 index 000000000..d442279e0 --- /dev/null +++ b/CHANGELOG.d/2.12.3-late-demo-cutoff-post.md @@ -0,0 +1,6 @@ +# 2.12.3 Late Demo public post + +Seed inserts Late Demo public post after the January 12 knowledge +cutoff so the ADR 0016 list filter has a falsifiable own-corp +counter-example. No second cutoff implementation. TEPP honesty +unchanged. diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f0862b8b..521f4dcb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). +## [2.12.3] - 2026-08-18 + +### Added + +- `make seed` inserts Late Demo public post (2026-01-13) so the + January 12 Demo Corp lineage and TEPP runs' knowledge cutoff is + falsifiable: Demo public post still opens; Late Demo does not. + The live post list still shows Late Demo. The cutoff filter + itself already lives on this stack (ADR 0016). TEPP honesty is + unchanged: accepted acks stay Failed transport evidence, not + Succeeded. Never invent a theta. + ## [2.12.2] - 2026-08-18 ### Fixed diff --git a/CLAUDE.md b/CLAUDE.md index 2ffd5241a..08e3682a6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,6 +44,9 @@ after cutoff were rewritten after the run; the opened body names both clocks and shows **Body this run knew** beside the live rewrite. Compare those two texts before treating the live body as reconstructed evidence (ADR 0016 / 0025). +The January 12 Demo Corp lineage and TEPP runs list Demo public post +and do not list Late Demo public post (2026-01-13). The live post +list still shows Late Demo. `POST /api/analysis-runs` records Pending lineage only on an authorized cutoff capture (ADR 0017). TEPP and period-report kinds are 422. The Request button waits until affiliated corps load; choose diff --git a/docs/adr/0016-analysis-run-knowledge-cutoff-posts.md b/docs/adr/0016-analysis-run-knowledge-cutoff-posts.md index 373c783ac..553d549b5 100644 --- a/docs/adr/0016-analysis-run-knowledge-cutoff-posts.md +++ b/docs/adr/0016-analysis-run-knowledge-cutoff-posts.md @@ -45,8 +45,9 @@ run. ## Consequences - After `make seed`, the Demo Corp lineage run lists Demo public post - and other in-cutoff Demo Corp titles. The later fixture account-review - post (2026-02-10) does not appear. + and other in-cutoff Demo Corp titles. Late Demo public post + (2026-01-13) and the later fixture account-review post (2026-02-10) + do not appear. The live post list still shows Late Demo. - Open the run: Demo public post is marked updated after cutoff (`updated_at` 2026-01-13). Demo private post is not. - Open a marked title: the popup shows **Body this run knew** from @@ -62,6 +63,13 @@ run. (ADR 0018). A later public post cannot surface a previously hidden thread-group run. +## Follow-up — v2.12.3 Late Demo own-corp counter-example + +v2.12.3 seeds Late Demo public post on 2026-01-13 so the January 12 +Demo Corp lineage and TEPP runs can prove the existing +`created_at <= knowledge_cutoff` filter. This is not a second cutoff +and does not change TEPP honesty. + ## References International Organization for Standardization. (2019). *ISO 8601-1:2019: diff --git a/frontend/package.json b/frontend/package.json index 72e864e31..967e529e3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "2.12.2", + "version": "2.12.3", "type": "module", "scripts": { "dev": "vite", diff --git a/lineageweave/__init__.py b/lineageweave/__init__.py index d1fff5078..fd0c4e2e0 100644 --- a/lineageweave/__init__.py +++ b/lineageweave/__init__.py @@ -55,4 +55,4 @@ "sentence_excerpts", ] -__version__ = "2.12.2" +__version__ = "2.12.3" diff --git a/pyproject.toml b/pyproject.toml index ade603e14..808919f4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lineageweave" -version = "2.12.2" +version = "2.12.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" } diff --git a/scripts/seed_demo_data.py b/scripts/seed_demo_data.py index aa6d2850d..60b9771d0 100644 --- a/scripts/seed_demo_data.py +++ b/scripts/seed_demo_data.py @@ -74,6 +74,17 @@ ) CALENDAR_TICKET_TITLE = "Send Riverbend the revised delivery schedule." +# ADR 0016: Demo Corp lineage/TEPP runs use this analysis clock. Late Demo +# public post is the own-corp counter-example dated after that clock. +DEMO_PUBLIC_POST_TITLE = "Demo public post" +DEMO_PUBLIC_POST_CREATED_AT = "2026-01-10T12:00:00Z" +DEMO_ANALYSIS_RUN_KNOWLEDGE_CUTOFF = "2026-01-12T12:00:00Z" +LATE_DEMO_PUBLIC_POST_TITLE = "Late Demo public post" +LATE_DEMO_PUBLIC_POST_CREATED_AT = "2026-01-13T09:00:00Z" +LATE_DEMO_PUBLIC_POST_BODY = ( + "Written after the Demo Corp lineage-run knowledge cutoff." +) + def _fetch_demo_user_subjects(base_url: str, admin_user: str, admin_password: str) -> dict[str, str]: """Return {username: Keycloak subject id} for the two synthetic demo users.""" @@ -278,12 +289,15 @@ def seed( "the January cutoff: Priya Nair at Northridge Grid now expects " "a later delivery window." ) - cur.execute("select post_id, post_body from source_post where post_title = 'Demo public post'") + cur.execute( + "select post_id, post_body from source_post where post_title = %s", + (DEMO_PUBLIC_POST_TITLE,), + ) demo_public_row = cur.fetchone() if demo_public_row is None: cur.execute( "insert into source_post (author_account_id, corporate_entity_id, process_unit_id, post_title, post_body, voc_type_code, visibility_code, created_at, updated_at) " - "values (%s, %s, %s, 'Demo public post', " + "values (%s, %s, %s, %s, " "%s, " "'voc', 'public', '2026-01-10T12:00:00Z', '2026-01-10T12:00:00Z') " "returning post_id", @@ -291,6 +305,7 @@ def seed( account_ids["demo.analyst"], corporate_entity_id, process_units["DEMO-PU-A"], + DEMO_PUBLIC_POST_TITLE, demo_public_cutoff_body, ), ) @@ -341,6 +356,12 @@ def seed( "updated_at = '2026-01-10T12:00:00Z' " "where post_title = 'Demo private post'" ) + seed_late_demo_public_post( + cur, + account_ids["demo.analyst"], + corporate_entity_id, + process_units["DEMO-PU-A"], + ) cur.execute( "insert into post_counterparty_entity (post_id, counterparty_entity_name, relationship_type_code) " "values (%s, 'Northridge Grid', 'rel_voc'), (%s, 'Demo Corp', 'rel_voc') " @@ -446,6 +467,45 @@ def seed( conn.close() +def seed_late_demo_public_post( + cur, + author_account_id, + corporate_entity_id, + process_unit_id, +) -> None: + """Insert Late Demo public post after the January 12 knowledge cutoff. + + ADR 0016 already filters ``source_post.created_at <= knowledge_cutoff``. + This own-corp public post is the falsifiable counter-example: Demo + public post stays on the January 12 Demo Corp lineage and TEPP run + lists; Late Demo does not. The live post list still shows Late Demo. + Does not implement a second cutoff, invent a theta, or stamp TEPP + Succeeded. + """ + cur.execute( + "select post_id from source_post where post_title = %s", + (LATE_DEMO_PUBLIC_POST_TITLE,), + ) + if cur.fetchone() is not None: + return + cur.execute( + "insert into source_post (" + "author_account_id, corporate_entity_id, process_unit_id, " + "post_title, post_body, voc_type_code, visibility_code, " + "created_at, updated_at" + ") values (%s, %s, %s, %s, %s, 'voc', 'public', %s, %s)", + ( + author_account_id, + corporate_entity_id, + process_unit_id, + LATE_DEMO_PUBLIC_POST_TITLE, + LATE_DEMO_PUBLIC_POST_BODY, + LATE_DEMO_PUBLIC_POST_CREATED_AT, + LATE_DEMO_PUBLIC_POST_CREATED_AT, + ), + ) + + def insert_fixture_source_posts(cur, author_account_id, corporate_entity_id, process_unit_id): """Insert ``sample_records()`` as ``source_post`` rows seed and rebuild share. @@ -707,7 +767,7 @@ def _seed_fixture_evaluations(cur) -> None: from lineageweave.fixtures import ambiguous_commitment_post, sample_records from lineageweave.post_evaluation import RUBRIC_VERSION - titles = ["Demo public post", ambiguous_commitment_post()[0]] + titles = [DEMO_PUBLIC_POST_TITLE, ambiguous_commitment_post()[0]] titles.extend(rec.label for rec in sample_records()) for title in titles: cur.execute("select post_id from source_post where post_title = %s", (title,)) @@ -1451,7 +1511,7 @@ def _seed_demo_analysis_run(cur, requested_by_account_id, corporate_entity_id) - configuration_schema_version, configuration_sha256, code_revision_sha, requested_at) values (%s, 'analysis_run_lineage', %s, - %s, '2026-01-12T12:00:00Z', 'lineage-run-v1', %s, %s, + %s, %s, 'lineage-run-v1', %s, %s, '2026-01-12T12:30:00Z') returning analysis_run_id """, @@ -1459,6 +1519,7 @@ def _seed_demo_analysis_run(cur, requested_by_account_id, corporate_entity_id) - snapshot_id, DEMO_LINEAGE_IDEMPOTENCY_KEY, requested_by_account_id, + DEMO_ANALYSIS_RUN_KNOWLEDGE_CUTOFF, "b" * 64, "c" * 40, ), @@ -1528,7 +1589,7 @@ def _seed_demo_run_reconstruction(cur, analysis_run_id, corporate_entity_id) -> and created_at <= %s order by created_at, post_title """, - (corporate_entity_id, datetime(2026, 1, 12, 12, 0, tzinfo=timezone.utc)), + (corporate_entity_id, DEMO_ANALYSIS_RUN_KNOWLEDGE_CUTOFF), ) columns = [desc[0] for desc in cur.description] rows = [dict(zip(columns, row)) for row in cur.fetchall()] @@ -1564,7 +1625,7 @@ def tepp_seed_request() -> AnalysisRunRequest: idempotency_key=DEMO_TEPP_IDEMPOTENCY_KEY, tenant_workspace_id="demo-workspace", snapshot_id=demo_source_snapshot_sha256(), - knowledge_cutoff="2026-01-12T12:00:00Z", + knowledge_cutoff=DEMO_ANALYSIS_RUN_KNOWLEDGE_CUTOFF, model_contract_version="tepp-analysis-run-v1", output_profile="calibrated_event_measurement", ) @@ -1637,7 +1698,7 @@ def _seed_demo_tepp_run(cur, requested_by_account_id, corporate_entity_id) -> No configuration_schema_version, configuration_sha256, code_revision_sha, requested_at) values (%s, 'analysis_run_tepp', %s, - %s, '2026-01-12T12:00:00Z', 'tepp-run-v1', %s, %s, + %s, %s, 'tepp-run-v1', %s, %s, '2026-01-12T12:34:00Z') returning analysis_run_id """, @@ -1645,6 +1706,7 @@ def _seed_demo_tepp_run(cur, requested_by_account_id, corporate_entity_id) -> No snapshot_id, DEMO_TEPP_IDEMPOTENCY_KEY, requested_by_account_id, + DEMO_ANALYSIS_RUN_KNOWLEDGE_CUTOFF, "d" * 64, "e" * 40, ), @@ -1686,7 +1748,7 @@ def tepp_accepted_seed_request() -> AnalysisRunRequest: idempotency_key=DEMO_TEPP_SUCCEEDED_IDEMPOTENCY_KEY, tenant_workspace_id="demo-workspace", snapshot_id=demo_source_snapshot_sha256(), - knowledge_cutoff="2026-01-12T12:00:00Z", + knowledge_cutoff=DEMO_ANALYSIS_RUN_KNOWLEDGE_CUTOFF, model_contract_version="tepp-analysis-run-v1", output_profile="calibrated_event_measurement", ) @@ -1722,7 +1784,7 @@ def _seed_demo_accepted_tepp_run(cur, requested_by_account_id, corporate_entity_ configuration_schema_version, configuration_sha256, code_revision_sha, requested_at) values (%s, 'analysis_run_tepp', %s, - %s, '2026-01-12T12:00:00Z', 'tepp-run-v1', %s, %s, + %s, %s, 'tepp-run-v1', %s, %s, '2026-01-12T12:42:00Z') returning analysis_run_id """, @@ -1730,6 +1792,7 @@ def _seed_demo_accepted_tepp_run(cur, requested_by_account_id, corporate_entity_ snapshot_id, DEMO_TEPP_SUCCEEDED_IDEMPOTENCY_KEY, requested_by_account_id, + DEMO_ANALYSIS_RUN_KNOWLEDGE_CUTOFF, "c" * 64, "b" * 40, ), @@ -1823,7 +1886,7 @@ def _seed_demo_report_run(cur, requested_by_account_id, corporate_entity_id) -> configuration_schema_version, configuration_sha256, code_revision_sha, requested_at) values (%s, 'analysis_run_report', %s, - %s, '2026-01-12T12:00:00Z', 'report-run-v1', %s, %s, + %s, %s, 'report-run-v1', %s, %s, '2026-01-12T12:38:00Z') returning analysis_run_id """, @@ -1831,6 +1894,7 @@ def _seed_demo_report_run(cur, requested_by_account_id, corporate_entity_id) -> snapshot_id, DEMO_REPORT_IDEMPOTENCY_KEY, requested_by_account_id, + DEMO_ANALYSIS_RUN_KNOWLEDGE_CUTOFF, "f" * 64, "a" * 40, ), diff --git a/tests/test_seed_late_demo_post.py b/tests/test_seed_late_demo_post.py new file mode 100644 index 000000000..14688350a --- /dev/null +++ b/tests/test_seed_late_demo_post.py @@ -0,0 +1,147 @@ +"""Late Demo public post is the ADR 0016 own-corp cutoff counter-example.""" + +from datetime import datetime, timezone +from inspect import getsource +from pathlib import Path + +from scripts.seed_demo_data import ( + DEMO_ANALYSIS_RUN_KNOWLEDGE_CUTOFF, + DEMO_PUBLIC_POST_CREATED_AT, + DEMO_PUBLIC_POST_TITLE, + LATE_DEMO_PUBLIC_POST_BODY, + LATE_DEMO_PUBLIC_POST_CREATED_AT, + LATE_DEMO_PUBLIC_POST_TITLE, + seed, + seed_late_demo_public_post, + tepp_accepted_seed_request, + tepp_seed_outcome, + tepp_seed_request, + _ensure_demo_source_snapshot_members, + _seed_demo_run_reconstruction, +) + + +def _parse_seed_clock(value: str) -> datetime: + """Parse a seeded ISO-8601 Z clock as UTC.""" + return datetime.fromisoformat(value.replace("Z", "+00:00")) + + +class _LateDemoCursor: + """Drive ``seed_late_demo_public_post`` without a live database.""" + + def __init__(self, existing: bool = False) -> None: + self.existing = existing + self.statements: list[str] = [] + self.params: list[object] = [] + + def execute(self, sql: str, params=None) -> None: + self.statements.append(" ".join(sql.split())) + self.params.append(params) + + def fetchone(self): + last = self.statements[-1] + if last.lstrip().startswith("select") and "from source_post" in last: + return ("late-demo-id",) if self.existing else None + return None + + +def test_january_12_run_lists_demo_public_not_late_demo() -> None: + """ADR 0016 ``created_at <= knowledge_cutoff`` keeps Demo public, drops Late Demo.""" + cutoff = _parse_seed_clock(DEMO_ANALYSIS_RUN_KNOWLEDGE_CUTOFF) + listed = { + title: _parse_seed_clock(created_at) <= cutoff + for title, created_at in ( + (DEMO_PUBLIC_POST_TITLE, DEMO_PUBLIC_POST_CREATED_AT), + (LATE_DEMO_PUBLIC_POST_TITLE, LATE_DEMO_PUBLIC_POST_CREATED_AT), + ) + } + assert cutoff == datetime(2026, 1, 12, 12, 0, tzinfo=timezone.utc) + assert listed[DEMO_PUBLIC_POST_TITLE] is True + assert listed[LATE_DEMO_PUBLIC_POST_TITLE] is False + + +def test_listing_still_uses_created_at_not_a_second_cutoff() -> None: + """Visible-post SQL stays the ADR 0016 created_at gate on every scope.""" + listing = ( + Path(__file__).resolve().parents[1] + / "backend" + / "app" + / "analysis_run_ingestion.py" + ).read_text(encoding="utf-8") + start = listing.index("async def fetch_visible_scope_posts") + end = listing.index("\nclass AnalysisRunCreateError") + listing_fn = listing[start:end] + assert listing_fn.count("created_at <= $") == 4 + assert "LATE_DEMO" not in listing_fn + + +def test_reconstruction_seed_uses_the_january_12_cutoff() -> None: + """Run reconstruction persists only posts known at the same analysis clock.""" + source = getsource(_seed_demo_run_reconstruction) + assert "created_at <= %s" in source + assert "DEMO_ANALYSIS_RUN_KNOWLEDGE_CUTOFF" in source + assert LATE_DEMO_PUBLIC_POST_TITLE not in source + + +def test_snapshot_members_exclude_late_demo_created_at() -> None: + """Frozen snapshot membership uses created_at before Late Demo exists.""" + source = getsource(_ensure_demo_source_snapshot_members) + assert "created_at <= '2026-01-12T00:00:00Z'" in source + late = _parse_seed_clock(LATE_DEMO_PUBLIC_POST_CREATED_AT) + snapshot_max = datetime(2026, 1, 12, tzinfo=timezone.utc) + assert late > snapshot_max + + +def test_seed_late_demo_public_post_inserts_after_cutoff() -> None: + """Persist writes the own-corp public counter-example dated 2026-01-13.""" + cursor = _LateDemoCursor() + seed_late_demo_public_post(cursor, "account-1", "corp-1", "pu-1") + inserts = [ + (sql, params) + for sql, params in zip(cursor.statements, cursor.params, strict=True) + if "insert into source_post" in sql + ] + assert inserts, "missing Late Demo must be inserted" + sql, params = inserts[0] + assert params is not None + assert LATE_DEMO_PUBLIC_POST_TITLE in params + assert LATE_DEMO_PUBLIC_POST_BODY in params + assert LATE_DEMO_PUBLIC_POST_CREATED_AT in params + assert params.count(LATE_DEMO_PUBLIC_POST_CREATED_AT) == 2 + assert "public" in sql + assert "theta" not in sql.lower() + assert not any( + isinstance(value, str) and ("theta" in value.lower() or "θ" in value) + for value in params + ) + + +def test_seed_late_demo_public_post_skips_when_already_present() -> None: + """Re-seed must not invent a second Late Demo row.""" + cursor = _LateDemoCursor(existing=True) + seed_late_demo_public_post(cursor, "account-1", "corp-1", "pu-1") + assert not any("insert into source_post" in sql for sql in cursor.statements) + + +def test_seed_calls_late_demo_before_analysis_runs() -> None: + """``seed()`` writes Late Demo, then lineage/TEPP runs on the January 12 clock.""" + source = getsource(seed) + late_at = source.index("seed_late_demo_public_post(") + lineage_at = source.index("_seed_demo_analysis_run(") + tepp_at = source.index("_seed_demo_tepp_run(") + assert late_at < lineage_at < tepp_at + helper = getsource(seed_late_demo_public_post) + insert_sql = helper[helper.index("insert into source_post") :] + assert "created_at <= " not in insert_sql + assert "theta" not in insert_sql.lower() + + +def test_tepp_seed_keeps_the_same_january_12_cutoff() -> None: + """Late Demo does not fork TEPP arithmetic or stamp Succeeded.""" + request = tepp_seed_request() + accepted = tepp_accepted_seed_request() + assert request.knowledge_cutoff == DEMO_ANALYSIS_RUN_KNOWLEDGE_CUTOFF + assert accepted.knowledge_cutoff == DEMO_ANALYSIS_RUN_KNOWLEDGE_CUTOFF + status, failure = tepp_seed_outcome() + assert status == "analysis_status_failed" + assert failure == "tepp_not_available" diff --git a/uv.lock b/uv.lock index 59ca5eb54..1575f180a 100644 --- a/uv.lock +++ b/uv.lock @@ -454,7 +454,7 @@ wheels = [ [[package]] name = "lineageweave" -version = "2.12.1" +version = "2.12.3" source = { virtual = "." } dependencies = [ { name = "certifi" },