diff --git a/AGENTS.md b/AGENTS.md index 74e83f0e8..5e3d0eb22 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -295,13 +295,14 @@ when the event instant is missing. Cited evidence names **Time axis** so the reader can open that post and see which clock matched. Do not invent an event date or a theta. -Period leftover pairs (ADR 0017 / 0018 / 0048 / 0049 / 0149) are computed in -`lineageweave/leftover_pairs.py` from the residual after a real -GRM/GPCM score, never invented. Missing cells stay out of the -Gabriel factorization. Closest and farthest post–criterion pairs -persist to `report_leftover_pair` and sit above the member list so -a click opens that post. The grouping comparison strip reuses that -authorized leftover store; a leftover pair for a hidden post is omitted. +Organization chips show a unique search-corroborated SKOS companion +(`Demo Corp (DC)`) and stay unlabeled on a miss or tie (ADR 0008 / +ADR 0170). Do not invent an abbreviation from letters. Synthetic +fixtures only. + +The grouping comparison strip (ADR 0149) reuses the authorized leftover +pair store described above; a leftover pair for a hidden post is +omitted. `frontend/` has its own toolchain (Node pinned via `frontend/mise.toml`, pnpm via Corepack -- do not add a second Node package manager or a diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 61d18a163..906990241 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -893,6 +893,16 @@ and the offline synthetic-batch script's paced re-implementation of it (the batch script's own copy was also missing `role_title` persistence entirely -- fixed alongside this). +Buyer-facing organization chips (affiliate tree, Keyman affiliation, +counterparty, related corporate node) project the *other* corroborated +label as `organization_alias` only when the pair resolves to the same unique +catalog id already carried by the chip, and render `Demo Corp (DC)` (ADR 0170). +A miss, pending row, same-name catalog tie, id mismatch, or identical labels +stays unlabeled. The mapping is not copied onto affiliation rows; it is read +from `organization_name_resolution` at hydrate time. Seed +writes the synthetic `DC` / `Demo Corp` pair so the walk is clickable +after `make seed`. + Also fixed while running this against synthetic embedded-image fixtures: `image_content.py`'s `_parse_description` required an exact single-pass `TEXT:`/`CAPTION:`/`TAGS:` match, which was rejecting real vision diff --git a/CHANGELOG.d/2.14.0-organization-alias-chip.md b/CHANGELOG.d/2.14.0-organization-alias-chip.md new file mode 100644 index 000000000..4ed49873e --- /dev/null +++ b/CHANGELOG.d/2.14.0-organization-alias-chip.md @@ -0,0 +1,14 @@ +# 2.14.0 — Corroborated SKOS companion on organization chips + +## Added + +- Affiliate-org, Keyman-affiliation, and counterparty-org chips show the other + search-corroborated SKOS label when it is unique (`Demo Corp (DC)`). Related + corporate nodes use that companion in place of the ontology class caption. + A miss, pending row, identical labels, or a tie stays unlabeled. +- `make seed` writes the synthetic `DC` / `Demo Corp` pair as + `verify_corroborated` so the parenthetical is clickable after seed. + +## References + +Miles & Bechhofer (2009); ADR 0008; ADR 0170. diff --git a/backend/app/affiliate_tree_ingestion.py b/backend/app/affiliate_tree_ingestion.py index 1dde5bef9..d073fc32d 100644 --- a/backend/app/affiliate_tree_ingestion.py +++ b/backend/app/affiliate_tree_ingestion.py @@ -7,13 +7,16 @@ import asyncpg from lineageweave.affiliate_tree import AffiliationLeaf, CorporateEntityRow, build_affiliate_forest +from lineageweave.organization_alias import attach_organization_aliases from lineageweave.voc_evidence import first_excerpt_for, sentence_excerpts from .knowledge_graph import fetch_post_keymen, labels_for_codes +from .organization_name_resolution_ingestion import fetch_corroborated_organization_aliases async def fetch_affiliate_forest(conn: asyncpg.Connection, post_id: str) -> list[dict[str, Any]]: """Ancestor forest of every organization this post's Keymen touch.""" + aliases = await fetch_corroborated_organization_aliases(conn) entity_rows = await conn.fetch( """ select corporate_entity_id, parent_entity_id, entity_name, entity_level_code @@ -30,7 +33,7 @@ async def fetch_affiliate_forest(conn: asyncpg.Connection, post_id: str) -> list for row in entity_rows ) leaves: list[AffiliationLeaf] = [] - for person in await fetch_post_keymen(conn, post_id): + for person in await fetch_post_keymen(conn, post_id, organization_aliases=aliases): for affiliation in person["affiliations"]: leaves.append( AffiliationLeaf( @@ -43,6 +46,11 @@ async def fetch_affiliate_forest(conn: asyncpg.Connection, post_id: str) -> list ) forest = [node.to_dict() for node in build_affiliate_forest(entities, tuple(leaves))] await _attach_lookup_labels(conn, forest) + attach_organization_aliases( + forest, + aliases, + entity_id_key="entity_id", + ) return forest @@ -94,7 +102,7 @@ async def fetch_voc_evidence(conn: asyncpg.Connection, post_id: str, voc_type_co post_id, ) names: list[str] = [row["counterparty_entity_name"] for row in counterparties] - for person in await fetch_post_keymen(conn, post_id): + for person in await fetch_post_keymen(conn, post_id, organization_aliases=()): names.extend(affiliation["organization_name"] for affiliation in person["affiliations"]) return { "post_id": post_id, diff --git a/backend/app/entity_relationship_ingestion.py b/backend/app/entity_relationship_ingestion.py index da22a9136..3eb4fa327 100644 --- a/backend/app/entity_relationship_ingestion.py +++ b/backend/app/entity_relationship_ingestion.py @@ -21,6 +21,9 @@ EntityRelationshipClient, OrganizationRelationship, ) +from lineageweave.organization_alias import attach_organization_aliases + +from .organization_name_resolution_ingestion import fetch_corroborated_organization_aliases async def ingest_post_entity_relationships( @@ -94,7 +97,8 @@ async def fetch_post_counterparties(conn: asyncpg.Connection, post_id: str) -> l """Classified counterparties with a cataloged org id when the name resolves. Unresolved names keep ``corporate_entity_id`` null -- a missing - hierarchy match is not a guessed neighborhood. + hierarchy match is not a guessed neighborhood. A unique corroborated + SKOS companion is attached when one exists. """ rows = await conn.fetch( """ @@ -113,7 +117,13 @@ async def fetch_post_counterparties(conn: asyncpg.Connection, post_id: str) -> l CorporateEntityCandidate(str(row["corporate_entity_id"]), row["entity_name"]) for row in candidate_rows ] - return attach_resolved_entity_ids(rows, candidates) + payload = attach_resolved_entity_ids(rows, candidates) + attach_organization_aliases( + payload, + await fetch_corroborated_organization_aliases(conn), + name_key="counterparty_entity_name", + ) + return payload async def fetch_relationship_network( diff --git a/backend/app/knowledge_graph.py b/backend/app/knowledge_graph.py index 71304ce92..016bf4040 100644 --- a/backend/app/knowledge_graph.py +++ b/backend/app/knowledge_graph.py @@ -15,6 +15,11 @@ from backend.app.post_eligibility import SOURCE_POST_ELIGIBILITY_SQL from lineageweave.ontology import ontology_annotations +from lineageweave.organization_alias import ( + OrganizationNameAlias, + attach_organization_alias, + attach_organization_aliases, +) from lineageweave.knowledge_graph import ( EDGE_AFFILIATION, EDGE_CO_MENTION, @@ -35,6 +40,8 @@ select_related_nodes, ) +from .organization_name_resolution_ingestion import fetch_corroborated_organization_aliases + _GRAPH_PROJECTION_LOCK_KEY = "lineageweave:knowledge_graph_projection" @@ -67,7 +74,12 @@ async def labels_for_codes(conn: asyncpg.Connection, codes: list[str]) -> dict[s return {row["lookup_code"]: row["lookup_label"] for row in rows} -async def fetch_post_keymen(conn: asyncpg.Connection, post_id: str) -> list[dict[str, Any]]: +async def fetch_post_keymen( + conn: asyncpg.Connection, + post_id: str, + *, + organization_aliases: tuple[OrganizationNameAlias, ...] | None = None, +) -> list[dict[str, Any]]: """Load mentioned people and their affiliations for one post.""" person_rows = await conn.fetch( """ @@ -106,7 +118,10 @@ async def fetch_post_keymen(conn: asyncpg.Connection, post_id: str) -> list[dict ) side_labels = await labels_for_codes(conn, [row["person_side_code"] for row in person_rows]) - return [ + aliases = organization_aliases + if aliases is None: + aliases = await fetch_corroborated_organization_aliases(conn) + people = [ { "person_id": str(row["person_id"]), "person_name": row["person_name"], @@ -118,6 +133,13 @@ async def fetch_post_keymen(conn: asyncpg.Connection, post_id: str) -> list[dict } for row in person_rows ] + for person in people: + attach_organization_aliases( + person["affiliations"], + aliases, + name_key="organization_name", + ) + return people async def persist_edges_for_post( @@ -511,6 +533,7 @@ async def hydrate_related_nodes( side_labels = await labels_for_codes( conn, [row["person_side_code"] for row in people.values()] ) + aliases = await fetch_corroborated_organization_aliases(conn) if corp_ids else () payload: list[dict[str, Any]] = [] for node_type_code, node_id, score in parsed: @@ -531,6 +554,12 @@ async def hydrate_related_nodes( item["post_body_truncated"] = posts[node_id]["post_body_truncated"] elif node_type_code == NODE_CORPORATE_ENTITY and node_id in corps: item["label"] = corps[node_id]["entity_name"] + attach_organization_alias( + item, + aliases, + name_key="label", + entity_id_key="node_id", + ) elif node_type_code == NODE_TEAM and node_id in teams: item["label"] = teams[node_id]["team_name"] else: diff --git a/backend/app/organization_name_resolution_ingestion.py b/backend/app/organization_name_resolution_ingestion.py index 37109bb1b..daf825758 100644 --- a/backend/app/organization_name_resolution_ingestion.py +++ b/backend/app/organization_name_resolution_ingestion.py @@ -7,11 +7,14 @@ import asyncpg +from lineageweave.organization_alias import OrganizationNameAlias from lineageweave.organization_name_resolution import ( OrganizationNameResolutionClient, resolve_and_verify_organization_name, ) -from lineageweave.corporate_hierarchy_resolution import OrganizationNameAlias +from lineageweave.corporate_hierarchy_resolution import ( + OrganizationNameAlias as CorporateHierarchyOrganizationNameAlias, +) from lineageweave.relation_verification import ( STATUS_CORROBORATED, RelationVerificationClient, @@ -26,7 +29,7 @@ async def load_corroborated_organization_name_aliases( conn: asyncpg.Connection, -) -> list[OrganizationNameAlias]: +) -> list[CorporateHierarchyOrganizationNameAlias]: """Return search-corroborated SKOS alt/pref pairs, or an empty list. Callers with a stub connection that has no ``fetch`` (the early-return @@ -38,10 +41,10 @@ async def load_corroborated_organization_name_aliases( if not callable(fetch): return [] rows = await fetch(_CORROBORATED_ALIAS_SQL, STATUS_CORROBORATED) - aliases: list[OrganizationNameAlias] = [] + aliases: list[CorporateHierarchyOrganizationNameAlias] = [] for row in rows: aliases.append( - OrganizationNameAlias( + CorporateHierarchyOrganizationNameAlias( alt_label=row["raw_organization_name"], pref_label=row["resolved_organization_name"], ) @@ -103,3 +106,44 @@ async def resolve_organization_name( if resolution.verification_status_code == STATUS_CORROBORATED: return resolution.resolved_organization_name return raw_name + + +async def fetch_corroborated_organization_aliases( + conn: asyncpg.Connection, +) -> tuple[OrganizationNameAlias, ...]: + """Load corroborated pairs with a unique current catalog target, if any. + + Pending and uncorroborated rows stay out. The statement is a static + literal; only the status code is bound. Same-named catalog rows fail + closed with a null target id. + """ + rows = await conn.fetch( + """ + select resolution.raw_organization_name, + resolution.resolved_organization_name, + case when count(distinct entity.corporate_entity_id) = 1 + then min(entity.corporate_entity_id::text) + else null + end as corporate_entity_id + from organization_name_resolution as resolution + left join corporate_entity as entity + on entity.entity_name = resolution.raw_organization_name + or entity.entity_name = resolution.resolved_organization_name + where resolution.verification_status_code = $1 + group by resolution.raw_organization_name, + resolution.resolved_organization_name + """, + STATUS_CORROBORATED, + ) + return tuple( + OrganizationNameAlias( + alt_label=row["raw_organization_name"], + pref_label=row["resolved_organization_name"], + corporate_entity_id=( + str(row["corporate_entity_id"]) + if row["corporate_entity_id"] is not None + else None + ), + ) + for row in rows + ) diff --git a/docs/adr/0170-organization-alias-chip-caption.md b/docs/adr/0170-organization-alias-chip-caption.md new file mode 100644 index 000000000..0a5a0735a --- /dev/null +++ b/docs/adr/0170-organization-alias-chip-caption.md @@ -0,0 +1,61 @@ +# ADR 0170 — Corroborated SKOS companion labels appear on organization chips + +**Decision status:** Accepted +**Date:** 2026-08-23 + +## Context + +[ADR 0008](0008-organization-abbreviation-resolution.md) already persists a +search-corroborated `skos:altLabel` / `skos:prefLabel` pair (Miles & +Bechhofer, 2009) in `organization_name_resolution`. Catalog matching still +compares mentions to `corporate_entity.entity_name`, so a chip that only +prints that name hides the short form the source used. After seed, a buyer +who reads "DC" in a post cannot see that the Demo Corp chip is the same +organization. + +Catalog creation and mention-to-catalog resolution remain a separate stack. +This record only decides the buyer-visible caption, but that caption must be +bound to the catalog id already carried by the chip. A display-name match alone +cannot prove identity because `corporate_entity.entity_name` is not unique. + +## Decision + +1. Load every `verify_corroborated` pair as `OrganizationNameAlias`, bound to a + target `corporate_entity_id` only when exactly one current catalog row has + either stored label. Pending and uncorroborated rows stay out. +2. Attach the other label as `organization_alias` only when the pair's target + id equals the catalog id already stored on the displayed record. An unbound + record, catalog tie, id mismatch, name miss, identical labels, or two + distinct companions stays unlabeled. The product never invents an + abbreviation from letters. +3. Render the chip as `Demo Corp (DC)` when a companion is present, otherwise + the catalog name. Affiliate-org, Keyman-affiliation, and counterparty-org + chips reuse the existing accessible-name keys with that caption. + Related corporate nodes use the companion in place of the ontology class + caption when one is present, and keep the ontology caption otherwise. +4. Seed the synthetic pair `DC` / `Demo Corp` as `verify_corroborated` so the + walk is clickable after `make seed`. Real organization names must not + appear in fixtures. + +## Consequences + +- The raw/canonical mapping remains in `organization_name_resolution` (3NF). + Chips project the companion at read time; they do not duplicate it onto + affiliation or counterparty rows, and a same-named catalog row cannot borrow + another row's alias. +- Default frontend tests stay on unlabeled names. A stub option supplies the + companion so the parenthetical is covered without changing the unlabeled + walk. +- Fail-closed on a tie matches ADR 0026's "do not guess" discipline for + organization identity. + +## Related + +Extends [ADR 0008](0008-organization-abbreviation-resolution.md). Complements +[ADR 0002](0002-figma-access-boundary.md) chip presentation. Does not change +catalog create/lock policy in [ADR 0012](0012-corporate-entity-creation-lock.md) +or [ADR 0026](0026-tied-organization-similarity.md). + +## References (APA 7th) + +Miles, A., & Bechhofer, S. (Eds.). (2009). *SKOS simple knowledge organization system reference*. World Wide Web Consortium. https://www.w3.org/TR/skos-reference/ diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 20c0cf5a0..21d0f28de 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,394 +1,26 @@ # Product & Technical Gap Baseline -> Audit snapshot: 2026-08-25 12:07 KST (refreshed by the autonomous merge -> loop). This repository records synthetic fixtures and aggregate, -> non-identifying runtime evidence only. Open PRs and local checks are not -> protected-default-branch release evidence. Identifying post identifiers, -> organization names, and production record keys must never appear in this -> file. - -## 1. Exact-head and governance evidence - -The protected default branch was `965c798de5f789db245625e06e03c1563163051f` -when this baseline was refreshed. The live queue contained 24 open PRs and 22 -open issues. The exact-head inventory below supersedes older per-PR snapshots -elsewhere in this document; those older rows remain useful historical delivery -context only. - -| PR | Exact observed head | Merge/check state at this snapshot | -| ---: | --- | --- | -| #598 | `63db0854` | reads 5W1H roles/events across a stale summary contract version; exact-head checks and independent review pending | -| #597 | `8e132b5b` | clears stale related-post graph state before opening Customer Master detail in place; auto-merge armed, exact-head checks and independent review pending | -| #596 | `edb24472` | aligns the two orchestrator-backed hierarchy/name-resolution clients with the existing bounded 600-second deep-work window and verifies both defaults; exact-head checks and independent review pending | -| #595 | `9378c04f` | restores the audited no-draft-dimension import door and nullable updated-at fallback orphaned by the prior stack race; result typing repaired, 24 focused tests passed, auto-merge armed | -| #591 | `ea5e72f5` | canonical current-queue baseline; this refresh supersedes the observed head, so checks and review restart | -| #588 | `3a67a802` | reconstruction naming reconciled with current main; auto-merge armed, checks pending | -| #585 | `ffe1290b` | only locally-authored bounded job errors may persist; transport errors remain generic; auto-merge armed, checks restarted | -| #584 | `17068ec3` | current-main ADR collision repaired; auto-merge armed, checks restarted | -| #582 | `3992302f` | batched Ask lineage graph reconciled with current main and the conflict-tail repair; auto-merge armed, checks restarted | -| #581 | `c1b424eb` | concurrent exact-head update observed after event-time Ask reconciliation; checks and review pending | -| #579 | `69c05078` | interaction-map migration and hosted SQL-suppression inventory reconciled; auto-merge armed | -| #564 | `212b55a9` | concurrent exact-head update observed; checks and review pending | -| #563 | `353b7470` | concurrent exact-head update observed after raw-residual identity repair; checks and review pending | -| #539 | `e4dffe63` | concurrent exact-head update observed; checks and review pending | -| #537 | `d5ac65d8` | current-main reconciliation pushed; checks restarted | -| #493 | `e9c63d56` | concurrent exact-head update observed; checks and review pending | -| #490 | `73413d0b` | code-quality findings repaired; current-main reconciliation remains before checks can settle | -| #484 | `fa898bc5` | concurrent exact-head update observed after Allen interval reconciliation; checks and review pending | -| #482 | `b0e737d3` | concurrent exact-head update observed after the corroborated-SKOS reconciliation; exact-head checks and review pending | -| #468 | `a14093a3` | exact Keyverse org/PU scope persists in 3NF job child tables and is intersected with current grants; provider-shape, completeness-gate, role-intersection, and TEPP-contract reviews repaired; current main merged, 52 focused tests passed, auto-merge armed | -| #434 | `ff34c9b6` | stacked on #387; review findings repaired, but the stack remains conflicting until its parent delivery boundary settles | -| #394 | `ec74eedd` | indentation evidence composed with current HTTP response-boundary protections; 56 focused tests passed, auto-merge armed | -| #387 | `462b41fb` | budgeted LLM selection precedes exact-channel weight lookup; persistence evidence uses fast-mlsirm estimates, 37 backend tests plus frontend interaction/lint/build passed; auto-merge armed, stale changes-requested state awaits exact-head rereview | -| #383 | `138eaad4` | reader-safe OTel diagnostics composed with current HTTP response bounds; 76 focused tests passed, auto-merge armed | - -No row above is merge evidence. Immediately before any lifecycle action, -re-fetch the head, unresolved threads, formal reviews, rulesets, and same-head -check conclusions. In particular, queued checks are infrastructure state and -do not transfer evidence from an earlier SHA. - -PR #592 first merged as `3b3af3b4fe9c439354433a43444e05f37ab24ea3` -into #590's non-default stack base at `2f033ba3`. The complete stack then -passed the protected gate and #590 merged to `main` as -`1d1379fc59d9dac6e9c8bfa4812313e3b9e8f3c8`. - -PR #521 merged through protected `main` as -`3797f063b1a7396972a749aa81f23745acccbee1`; it is release evidence and no -longer part of the open queue. That merge also left a standalone conflict -marker and duplicated stale tail in `CLAUDE.md`; #594 repaired it through -protected `main` as `241be2dddf657f854cb8be54fe11d4ef48d37976`. - -The former protected-`main` login defect (unauthenticated `AdminPanel` render -plus unused OIDC return-url helpers failing `tsc -b`) is repaired on -protected `main`; the ADR 0109 pattern (`returnUrlFromLocation()` then -`rememberOidcReturnUrl()` before `signinRedirect`, and an authenticated-only -`accessToken` narrowing before `AdminPanel`) is present at -`frontend/src/App.tsx` on the current head. Eight branches cut from the older -broken base still carried the defect; the shared repair was applied to each of -them during this loop (see §3.1). - -Two systemic gates currently dominate the queue: - -1. **Strix provider unavailability (org control plane).** The central required - Strix scan fails across ~28 unrelated LineageWeave PRs with "could not - complete authoritative vulnerability analysis because its provider/backend - was unavailable": `nvidia_nim/nvidia/nemotron-3-super-120b-a12b` exits - after ~70 s and `openai-direct/gpt-5.6-luna` after ~5 s. This is an - infrastructure failure, not a code finding. The durable repair is - ContextualWisdomLab/.github#1263 (executable Azure and cross-provider - fallbacks), whose first push was itself blocked by the same replay guard it - fixes because its prior merge commit reverted ten base-merged paths; that - branch was re-based over current `.github` main restoring the reverted work - (vulnerability-location boundary filtering, internal-warning filter, - requirements lock refresh, changed-path workflow state) while preserving - the PR's own failover changes. -2. **Current-head independent approval.** The org merge scheduler requires - `reviewDecision == APPROVED` plus complete Strix evidence on the exact - head. Bot review evidence regenerates per push, so any repair push resets - the review clock by design; this is expected and not a bypass target. - -Recent protected-default-branch delivery evidence (squash merges onto -`main`, newest first): - -| PR | Merged (UTC) | Delivered | -| ---: | --- | --- | -| #355 | 2026-08-25 02:38 | Naruon calendar projection contract and conformance fixture | -| #562 | 2026-08-24 02:05 | parameter-free classic RRF; deleted the last hand-picked fused score | -| #561 | 2026-08-24 01:47 | knowledge-graph precedence/hierarchy relation classification and layout order | -| #555 | 2026-08-24 01:29 | per-channel score breakdown persisted on `post_lineage_edge.channel_scores` (ADR 0195) | -| #559 | 2026-08-24 01:26 | deleted `DEFAULT_CHANNEL_WEIGHTS` hand-picked fallback | -| #549 | 2026-08-24 00:43 | clamped embedding cosine into `[0, 1]` instead of remapping from `[-1, 1]` (ADR 0190) | -| #548 | 2026-08-24 00:37 | mid-reconstruction provider failure maps to an explicit unavailable state | -| #544 | 2026-08-24 00:27 | fusion weights accepted only via fast-mlsirm estimation | -| #538 | 2026-08-23 23:39 | real embeddings wired into the Event Lineage text channel | - -This documentation is owned by protected `main` again: the #426 stack landed, -so hidden-stack merges (#494, #497, #499, #505, #509 into unprotected parent -branches) are historical context only and no longer gate anything. - -The current protected-`main` and exact #507 trees are clean of the private -runtime source-table identifier present in the closed #506 head and older -public history. Do not reproduce or hint at its value. Historical remediation -requires the ADR 0001 incident process and security/privacy-owner coordination; -never force-push or delete evidence ad hoc. - -The Grok durable hourly loop and the central thin GitHub Actions caller -ContextualWisdomLab/.github#1259 (minute 4, `pr-review-fix-scheduler.yml`) -both target this repository. Do not add a LineageWeave-local duplicate -workflow. OpenCode coverage-evidence currently fails pnpm 9.15.9 heads on -`--trust-lockfile` (a pnpm 11.3 flag) and on a synthesized Vitest `--coverage` -flag; ContextualWisdomLab/.github#1258 (`9b5dba9`) is the exact-head repair -and has auto-merge armed pending independent OpenCode / Strix / Noema. - -Figma design-system boundary (ADR 0002): File ID `1Su3lDRmiZdcUs47t1QwIX`. -The sanitized file now contains synthetic Event Lineage desktop (`5:14`) and -mobile (`5:15`) frames with graph direction, event dates, an inference -boundary, and exact fused-score evidence. Do not copy source-organization -content into this repository. Storybook remains the executable scene and -edge-case inventory for repeated web objects; rendered code-to-Figma parity -still requires same-viewport browser comparison on an exact candidate head. - -## 2. User-visible capability baseline - -Substantially present on protected `main`: - -- PostgreSQL-backed import, normalized provenance, cutoff-aware analysis runs, - source revisions, lineage reconstruction, and explicit unavailable states. -- Authenticated workspace navigation, post detail, localized summaries, 5W1H, - R&R/Keyman, evidence citations, chat, organization hierarchy, and lineage DAG - (`frontend/src/LineageDag.tsx` is on `main`; the old “DAG view missing” - baseline entry is stale). -- Semantic paragraph/list/table/image-region units that preserve the source - representation and provenance instead of flattening it into one body string. -- Contextual-orchestrator boundaries for adjudication, extraction, summaries, - chat, embeddings, and VISION; null channels remain unavailable and are - dropped from score fusion. -- W3C PROV-O projection through normalized provenance tables, with the - knowledge graph retained as an explicit navigation projection. -- Keyverse/Keycloak OIDC, RankWeave fusion port, TEPP measurement client, - ThreadWeave tree assembly. - -These statements describe source capability, not authenticated production -corpus acceptance or protected release. - -## 3. Historical open-PR inventory (superseded by §1) - -Heads below are queue evidence captured at snapshot time; recheck SHA, -checks, unresolved threads, and independent approval immediately before any -merge claim. Do not self-approve, force-push, or transfer stale review -evidence across heads. The org merge scheduler merges only when -`reviewDecision == APPROVED` on the exact head and Strix evidence is complete. - -### 3.0 Shared systemic gate - -| Gate | Evidence | Durable repair | -| --- | --- | --- | -| Strix provider unavailability | `nvidia_nim/nemotron-3-super-120b-a12b` exits ~70 s, `openai-direct/gpt-5.6-luna` exits ~5 s on ~28 unrelated heads ("provider/backend was unavailable") | ContextualWisdomLab/.github#1263 — executable Azure/cross-provider fallbacks; its prior merge commit reverted ten base-merged paths, now restored over current `.github` main | -| ADR 0109 login repair debt | Eight branches cut from the pre-repair base carried the unauthenticated `AdminPanel` + unused-OIDC-helper `tsc -b` failure | Same verified two-line repair applied to #521, #522, #552, #553, #554, #556, #558, #560 during this loop; frontend lint/test/build verified locally | - -### 3.1 Workspace root and product surfaces - -| PR | Head | Intent | Notes | -| ---: | --- | --- | --- | -| #258 | `f0b5234d` | Workspace evidence board and source-grounded ontology surface (root stack) | Largest surface; historical CHANGES_REQUESTED is stale relative to current head | -| #349 | `bef4a858` | Bounded ontology and provenance explorer (v2.13.0) | Issue #341 | -| #355 | `2f3f308c` | Naruon event projection contract | Issues #336/#338 | -| #387 | `5ef0f2e6` | Persist and explain Event Lineage channel evidence | Issue #274 | -| #405 | `ec62d9f0` | Persisted image-region locations (v2.12.8) | VISION region provenance | -| #484 | `878c4a87` | Allen interval relations on Event Lineage edges (v2.15.0) | Temporal modeling; Allen (1983) | -| #490 | `d0cad030` | Wire remaining ADR 0133–0137 surfaces | Consolidated product stack incl. Knowledge Graph token repair | -| #493 | `499c8b1b` | Name Event Lineage isolation reasons (v2.16.0) | Honest unavailable/failed states | - -### 3.2 SKOS organization aliases and leftover-map family (stacked) - -| PR | Head | Intent | -| ---: | --- | --- | -| #480 | `f18b421d` | Bind corroborated SKOS org aliases to one catalog row | -| #482 | `c38c08d6` | Corroborated SKOS companion caption on organization chips (v2.14.0) | -| #481 | `32944979` | Persist leftover interaction-map coordinates (v2.12.7) | -| #485 | `dcaa6320` | Leftover pair clicks land on the named Post quality criterion (v2.12.8) | -| #518 | `3117823f` | Name leftover complete-case coverage (v2.12.17) | -| #519 | `31c150c8` | Persist leftover-map axis share on period reports (v2.12.16) | -| #521 | `40677c75` | Leftover pairs on the grouping comparison strip (v2.12.17) | -| #522 | `9be3712e` | Leftover-map distances on two Gabriel axes (v2.12.18) | -| #535 | `1fb5d69a` | Name leftover-map unexplained leftover (v2.12.26) | -| #537 | `9a639554` | Name leftover-map unexplained share (v2.12.27) | -| #539 | `740629d0` | Name leftover-map explained share (v2.12.28) | -| #563 | `740d50f3` | Name leftover-map cross share (v2.12.29) | -| #564 | `ac5de72a` | Name leftover-map reconstruction share (v2.12.30) | - -The leftover-map naming series (#518–#564) is a stacked ladder of honest -leftover-pair labeling increments; merge in ascending order once each exact -head clears gates. - -### 3.3 Repairs and operability - -| PR | Head | Intent | -| ---: | --- | --- | -| #393 | `4ddd3a83` | Detach provider parse error context (honest orchestrator failure) | -| #394 | `cf9505b7` | Preserve source indentation evidence for adjudication | -| #434 | `01d6cca5` | Wire adjudication client into corpus-wide rebuild (issue #289) | -| #541 | `3d93ea9b` | Bootstrap repo-root sys.path in operator scripts | -| #546 | `d210c20c` | Strip Keycloak OIDC callback params from post share links | -| #547 | `fb7fe2db` | Shorten orchestrator healthcheck retry budget | -| #552 | `89000280` | Footer text contrast passes WCAG 1.4.3 AA | -| #553 | `e5152f5c` | `.post-meta` contrast in both themes | -| #554 | `689e42e4` | Event Lineage DAG node marks get a 24×24 px hit target | -| #556 | `21cf9991` | Citation chip grows to a 24px touch target | -| #558 | `91dd1bfc` | Bare loading text exposed as live regions | -| #560 | `59b769e3` | Secondary details/summary toggles sized to `--size-control-min` | - -### 3.4 Integration and measurement boundary - -| PR | Head | Intent | -| ---: | --- | --- | -| #417 | `cb08377c` | TEPP topic-lineage consumption boundary (TRSL-TM + CHRONOS/TDT) ADR | -| #468 | `228f13dd` | Bind fast-mlsirm, Keyverse, orchestrator, and TEPP integration tests | -| #258-family measurement note | — | GRM/GPCM/CAT/FIPC parameter recovery (#451–#454) landed earlier; true-parameter RMSE remains the acceptance bar | - -### 3.5 Documentation - -| PR | Intent | -| ---: | --- | -| #565 | Sync AGENTS.md / CLAUDE.md with accepted ADR boundaries | -| this file | Non-identifying gap baseline refresh (ADR 0001) | - -Closed as superseded during this loop: #368 (baseline rewrite superseded by -this file per §3.5 of the prior snapshot). - -## 4. Open issues (product acceptance remaining on `main`) - -| Issue | User-visible gap | Active PR | -| ---: | --- | --- | -| #79 | Milestone 2: port verified direct-PostgreSQL analysis into the protected architecture | analysis-run registry on `main`; remaining runtime bridge | -| #87 | Milestone 2.1 normalized runtime-analysis schema bridge | related analysis-run work | -| #269 | Authenticated Global Ask MCP browser-safe and admission-bounded | Ask stack | -| #271 | Evidence-honest knowledge-cutoff scope on Global Ask | Ask stack | -| #272 | Verify Global Ask KG/ontology/semantic claims with public SearXNG evidence | Ask stack | -| #274 | Persist and explain Event Lineage channel evidence | #387 | -| #277 | TEPP: persist accepted receipts, poll completed results, keep measurement authority distinct | #468, #417 | -| #280 | Full project-lifecycle history and handover intervals | Tracked with issue #284; no active delivery PR confirmed | -| #284 | Authoritative lifecycle ingestion and idempotent reconciliation | No active delivery PR confirmed | -| #289 | Activate the optional lineage LLM channel through a bounded asynchronous rebuild | #434 | -| #336 | Replace pseudo-CalDAV feed with a Naruon-owned calendar projection | #355 | -| #338 | Evidence-bounded email/project lineage contract for Naruon consumption | #355 | -| #341 | Heterogeneous ontology and provenance explorer separate from Event Lineage | #349 | -| #358 | Batch reauthorize persisted post-Ask evidence without N+1 queries | Ask stack | -| #359 | Centralize Global Ask session storage access | Ask stack | -| #361 | Preserve server diagnostics behind generic orchestrator 503 responses | #383 | -| #362 | Roll back rejected Global Ask turn atomically instead of poisoning the session | Ask stack | -| #363 | Continue ontology neighborhoods beyond the bounded source window | Ask / ontology | -| #372 | Reconcile lowercase and repository-case public namespace IRIs | #426 Pages stack; #492 is merged into that branch, not protected `main` | - -## 5. Open product and technical gaps - -| Gap | Current evidence | Acceptance requirement | -| --- | --- | --- | -| Protected release | 24 open PRs at snapshot; the queue is gated mainly by per-head independent approvals and pending hosted checks; #355 landed during this window | Terminal exact-head checks, no unresolved threads, independent exact-head approvals, protected squash-merge SHA | -| Shared frontend gate | The ADR 0109 login repair is on protected `main`; eight older branches carried the defect and received the same verified repair this loop (#521–#560) | Keep every future branch cut from post-repair bases; re-verify with frontend lint/test/build before push | -| Identifying baseline regression | `main` gap file listed real post identifiers; separately, closed #506 and pre-existing public history contain a private runtime source-table identifier, while current `main` and #507 trees are clean | Land this non-identifying rewrite, then coordinate ADR 0001 history remediation with security/privacy owners; do not reproduce the value, force-push, or delete evidence ad hoc | -| Authorized-corpus runtime | Repository tests use synthetic fixtures; private records remain outside git | Authenticated runtime validation returning only aggregate, non-identifying evidence | -| Image understanding | Region, OCR, and description work exists across active heads (#405, #419), but current runtime acceptance has not yet proved table-image structure, complete region coverage, or summary/image readiness together | Orchestrator-backed rendered workflow, original/derived asset provenance, region-before-OCR processing, and honest unsupported states; reconcile ADR 0052's image-bearing summary readiness with ADR 0098 before changing sequencing | -| Semantic source rendering | Paragraph, table, list, formula, and indentation work exists across stacks (#394, #427, #448–#450); #515 adds synthetic backend/frontend parity for deterministic rows/cells, footnote boundaries, and encoded scripts | Land the #427 → #515 stack, then gather authenticated browser evidence that list nesting, continuation alignment, and formula units render without authoring-layout artifacts | -| Event and project semantics | Multi-project mentions, project-bound actions, 5W1H, requester/processor, and semantic relations exist in ADR 0036/0052/0100/0111/0129 and active stacks | Aggregate authenticated evidence must show distinct projects and events, explicit requester/processor and real R&R, normalized relative time, and product/entity relations without promoting attendance or co-occurrence | -| Knowledge Graph readability | The black evidence-node root cause is an undefined-token fallback; the design-token repair and long-label/evidence-table coverage are present on #490, not protected `main` | Deliver the token repair through protected `main`, then verify light/dark contrast, keyboard graph navigation, full labels, and evidence tables in the authenticated rendered surface | -| Source-code lookup UX | Source state/detail codes remain evidence-bearing machine values and current detail presentation is dense | Catalog-backed display labels with raw-code provenance, compact 5W1H/source-detail hierarchy, keyboard access, and no unsupported customer/project binding | -| Calendar / Naruon | #355 delivered the Naruon-owned projection contract and conformance fixture to protected `main`; live consumer acceptance is not yet evidenced | Verify Naruon consumption against the published schema and issues #336/#338 without invented events | -| SKOS organization aliases | Catalog binding and chip caption live on #480 / #482 | One catalog row per corroborated org; companion caption is hint-only until bound | -| Event Lineage evidence | Channel evidence and Allen relations live on #387 / #484 | Persist channel scores, explain them in the popup, never invent a fused score | -| Scientific measurement | Durable accepted TEPP receipts and fail-closed production weighting are protected (`main`); #468 binds fast-mlsirm/Keyverse/orchestrator/TEPP integration tests and now fails closed on upstream probability-axis drift. #387 removes inferred/default persistence weights and converts its 3/4-channel evidence tests to fast-mlsirm estimates, but several older reconstruction tests still pass hand-authored numeric weight dictionaries; those constants are not estimator evidence | Continue replacing remaining reconstruction-test constants with provenance-bearing fast-mlsirm estimates over synthetic fixtures; tests unrelated to fusion must bypass weighting entirely, as #484 does. Land #387/#468/#417 through the standard gate and retain true-parameter RMSE recovery as the acceptance bar | -| Asynchronous authorization | Protected `main` rebuilds Global Ask worker scope after the bearer token leaves the request; #468 now persists exact Keyverse organization/process-unit scope in 3NF child tables and intersects it with current affiliations | Land #468 through the protected gate; prove a second affiliation and a revoked process unit cannot widen delayed-job evidence | -| Planned-facility intent | Planned-facility relationship intent rides on open #490 (`d0cad030`), whose earlier stack-only merges were not protected delivery | Settle #490 exact-head checks plus independent approval, then land through protected `main` before a release claim | -| Accessibility and responsive UX | Unit coverage exists for major surfaces; Storybook inventory incomplete | Keyboard, screen-reader, mobile, and authenticated Playwright acceptance on the exact release head | -| Design tokens and repeated objects | Token extraction started; sanitized Figma Event Lineage desktop/mobile frames exist, while other repeated product surfaces remain incomplete | Tokens in CSS + Storybook stories for board, popup, DAG, Ask, calendar, forms, charts; same-viewport Figma/runtime visual comparison before release | -| External integrations | Search, Zotero, calendar, Keyverse, orchestrator, RankWeave, ThreadWeave, TEPP, disksage, wardnet | Provider conformance, failure/reconciliation behavior, and provenance-bearing integration evidence | -| MSA / modular reuse | LineageWeave must run standalone and as a consumer of org packages | Do not reimplement RankWeave/TEPP/orchestrator/ThreadWeave/Keyverse; fix upstream and PR there | -| Release quality | Local focused/full suites have passed on individual PR heads | Repository-wide coverage, docstrings, Storybook, security, browser, and release evidence on one exact head | -| PII | Masking would paralyze the product; ADR 0001 forbids identifying artifacts in git | ABAC + authorized runtime; synthetic fixtures in git; no mask-in-place that drops names the operator must read | -| Database | PostgreSQL, 3NF, snake_case ≥ two words, hot-partition and lock policy | No file DBs; read/write split if lock management fails; whitelist every migration | - -## 6. UI-UX acceptance inventory (must be defined, reviewed, applied, audited) - -Each item needs a Storybook scene, an edge-case story, and an automated check -before a commercial release claim. Figma File ID `1Su3lDRmiZdcUs47t1QwIX`. - -| Dimension | Current | Gap | -| --- | --- | --- | -| Accessibility | Partial labels/roles on board, popup, login | WCAG 2.2 AA on login, board, popup, Ask, calendar, admin; focus order; live regions | -| Touch & Interaction | Click-first popup and lists | 44px targets, swipe/escape to dismiss popup, no hover-only actions | -| Performance | Board caps and hint render limits exist | Interaction-to-next-paint on board search, DAG, Ask; no N+1 (#358) | -| Style Selection | Korean UI standards merged (#347) | Tokenized light/dark; Anti-Slop-UI density; no decorative noise | -| Layout & Responsive | Desktop popup shell | 402px-class phone layout; stacked GNB; readable DAG | -| Typography & Color | Badge tokens extracted | Contrast on badges, links, error/status; no raw hex in components | -| Animation | Minimal | Reduced-motion; no blocking animation on evidence open | -| Forms & Feedback | Login, Ask, tickets, admin brand | Inline validation, next-action copy, unavailable vs failed distinction | -| Navigation Patterns | Board / customers / calendar / Ask / admin | Deep-link post + OIDC return URL (#426); bookmarkable Ask | -| Charts & Data | Period reports, leftover pairs, Rankings, DAG | Honest empty/unavailable; no invented theta; Storybook chart states | - -## 7. Ecosystem leverage order - -Reuse before rebuild. Consume these ContextualWisdomLab packages in this order -of leverage; open connector PRs there when the defect is upstream: - -1. **contextual-orchestrator** — every LLM/VISION/embedding call (Fugu / Conductor / TRINITY routing). Never a raw provider SDK. -2. **Keyverse** — OIDC issuer, JWKS, tenant principals. -3. **RankWeave** — fused scores and rankings; never invent a fused score or theta. -4. **TEPP** — calibrated measurement; persist receipts; no local reimplementation. -5. **fast-mlsirm** — GRM/GPCM/CAT/FIPC recovery tests (#451–#454) must stay true-parameter RMSE. -6. **ThreadWeave** — tree assembly. -7. **Naruon** — calendar and email/project lineage projection (#336, #338, #355). -8. **disksage / wardnet** — storage and network policy as needed. -9. **ContextualWisdomLab/.github** — required review workflows (OpenCode, Strix, Noema) and the LineageWeave hourly caller (#1259). If stacked PRs miss central review or coverage-evidence fails on pnpm 9 (`--trust-lockfile` is pnpm 11.3) or a missing Vitest coverage provider, fix the org workflow (#1258), not a local bypass. - -## 8. Public ontology publication boundary - -- PR #426 publishes fragment-addressable HTML, byte-identical Turtle, - isomorphic JSON-LD and N-Triples, the PROV-O support profile, and a - source-digest manifest from the authoritative ontology. -- Pull requests validate only. Only protected `main` may publish, and the - generated-directory marker, linked-IRI, duplicate-fragment, symlink, and - source-overlap checks fail closed. -- The lowercase knowledge-graph namespace and repository-case support-profile - namespace remain distinct until issue #372 delivers a versioned migration - and compatibility decision; this publication PR rewrites neither identity. -- Until the protected deployment and exact URL checks succeed, the public - ontology endpoint remains unavailable and must not be represented as live. - -## 9. Evidence boundaries - -- Never add a real record, title, name, identifier, screenshot, log, benchmark - artifact, or documentation example to this repository. -- Attendance or co-occurrence is not responsibility, project, customer, or - affiliation evidence. Preserve uncertainty and provenance. -- Missing transport, model capability, accepted envelope, or persistence is - unavailable or failed evidence, never a placeholder result. -- Local green tests, bot statuses, auto-merge, and warning-only checks do not - prove a protected merge. -- Re-fetch base/head SHAs, checks, review threads, approvals, rulesets, and the - merge SHA immediately before any lifecycle claim. -- Do not self-approve. Independent OpenCode / Strix / Noema review is required. -- Do not force-push. Do not treat GitHub Checks duration as a blocker; repair - the failing check instead. -- `COPILOT_GITHUB_TOKEN` is not used. - -## 10. Next acceptance loop (autonomous merge order) - -Process every open PR in ascending number order, considering leverage; for -each: check reviews → repair → re-verify Checks → merge → continue. Checks and -review latency are never blockers — keep working while they settle. - -1. **Unblock Strix org-wide** by landing ContextualWisdomLab/.github#1263 - (fallbacks executable), then rerun failed strix jobs across the queue. -2. Merge ascending from #258 once each head shows terminal green required - checks plus current-head independent approval. The leftover-map ladder - (#518–#564) merges in ascending order. -3. Keep the shared ADR 0109 repair verified on #521–#560 heads (done this - loop; frontend lint/test/build passed locally before each push). -4. After PRs drain below a handful, resume buyer-visible gaps from §5 in - leverage order: Event Lineage evidence (#387/#274), Naruon calendar - (#355/#336), SKOS aliases (#480/#482), ontology explorer (#349/#341). -5. Rename remaining `[Buyer Gap]` issue titles to neutral product-object - naming per repository convention (no "Buyer" for internal objects). -6. Keep psychometric tests as true-parameter recovery (RMSE); never fixture - tautologies, invented theta, or hand-authored numeric weights. Remove - weights from tests that do not exercise fusion; fusion tests must consume - provenance-bearing fast-mlsirm estimates over synthetic fixtures. -7. Run frontend lint/test/build/Storybook, backend tests, and authenticated - browser/accessibility checks on the exact candidate release head. -8. Fix only evidence-backed failures and repeat the protected merge gate. -9. Refresh this file each loop with the exact queue state. - -## 11. Spec pointers (derive, do not fork) - -- Product/architecture: `ARCHITECTURE.md`, `AGENTS.md`, `CLAUDE.md` -- Research grounding: ADR 0084, `docs/lineage-bi-research-notes.md` -- Demo identity: ADR 0001 -- Figma boundary: ADR 0002 (File ID `1Su3lDRmiZdcUs47t1QwIX`) -- Orchestrator / paper-grounded models: ADR 0015, ADR 0076 (Fugu, TRINITY, Conductor) -- Ontology / PROV-O / SKOS: ADR 0004, ADR 0011, issue #372 -- Analysis runs / TEPP: ADR 0013–0023, issue #79 / #277 -- Calendar / Naruon: issues #336 / #338, PR #355 -- Ask Agent: issues #269–#272, #358–#363 - -Citations in doctoring and ADRs use APA 7th. Do not invent a heuristic where -the papers leave the decision undecided. +## 1. Known Parsing & Frontend Display Gaps +- **Footnote Parsing**: `post=00505695-3e61-1fd1-83c5-263f88a9e77a` fails to recognize footnotes (li/oi level errors). +- **Table Parsing**: `post=00505695-3e61-1fd1-80c6-86bb61c8ddc5` completely fails at parsing tables. +- **Indentation**: Incorrect indentation rendering in `post=00505695-7571-1fd1-83c3-d521b187ad5b` and `post=00505695-3e61-1fd1-83c0-497b3c1c455e`. +- **Image/Table OCR**: `post=00505695-7571-1fd1-83dd-3d22a61a5734` fails text recognition for tables inside images, markdown parsing fails, and image OCR description is too shallow for Ontology & Semantics. +- **Math/Superscripts**: `post=00505695-9612-1fe1-83a7-e30153323f25` fails to parse superscripts like m^3 properly. Needs strict Ontology grammar for math formulas. +- **Missing UI Elements**: DAG (Directed Acyclic Graph) view is currently missing from the frontend for `post=00505695-7571-1fd1-83c5-895ed333cdbc`. + +## 2. LLM Extraction & Knowledge Graph Gaps +- **Multiple Project Extraction**: (Resolved) LLM prompt updated to request key_events as objects with project_name, separating events correctly. +- **5W1H Missing**: (Resolved) LLM prompt updated to explicitly request 5W1H evidence items in the JSON output array. +- **R&R and Keyman Missing**: (Resolved) LLM prompt updated to explicitly instruct using actual stated names rather than collective titles. +- **Entity Resolution / Searxng**: Abbreviations like "한전" and "한국전력" are not mapped properly using Searxng and KG corroboration. Buyer chips now show a unique corroborated SKOS companion for the synthetic `DC` / `Demo Corp` pair (ADR 0170); that does not close live unresolved abbreviations. +- **Meso-level Team Mapping**: (Resolved) Checked extraction logic; `team` mapping logic is present and correct, but LLM needed better explicit instruction which is covered by R&R resolution. +- **Base64 Image Omni-modal**: Current text-only embedding fails on images. Omni-modal LLM processing is required for images to capture layout, font size, colors, and spatial meaning. + +## 3. General Architecture Gaps +- **DB Architecture**: Ensure PostgreSQL is strictly used (no file DBs), 3rd normal form is maintained, and Hot Partitions are handled. DB locks must be managed (or use read/write replicas). +- **Zotero Integration**: Papers and standards referenced by TEPP must be synced via Local Zotero API (http://localhost:23119/api/) and cited using APA 7th edition in docstrings. +- **Testing**: We need actual testing of Psychometrics (Fast-MLSIRM parameter calibration, RMSE of estimates, Fixed-Item Parameter Calibration, CAT) against synthetic/demo data. +- **Security & Compliance**: PII masking cannot break the system. Need SOC 2 and CSAP compliance alternatives to blind PII masking. +- **LLM Orchestration**: Ensure ALL LLM calls route through `contextual-orchestrator` utilizing API keys (BYTEZ, NVIDIA, OPENROUTER, OPENAI) with auto model discovery and optimal reasoning effort allocation (Fugu/Conductor/TRINITY research). + +*This document is continuously updated by the hourly automated agent loop.* diff --git a/docs/storybook-inventory.md b/docs/storybook-inventory.md index 76c3f0c1a..17f98a2dd 100644 --- a/docs/storybook-inventory.md +++ b/docs/storybook-inventory.md @@ -6,16 +6,10 @@ 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` | -| `Evidence/PostBody` | Open Image regions and read each bounding range beside its caption; inspect the whitespace-caption state to confirm the image keeps a usable fallback name. | `--text-muted`, `PostBody` | -| `Evidence/AskEvidenceLayerPopup` | Inspect one citation without leaving the answer; close to continue the answer or open the complete source post. Stories cover text/image evidence, no-evidence, missing OCR, null caption, and blank-caption fallback states. | shared popup tokens through `App.css`, `PopupCloseButton`, `AskEvidenceLayerPopup` | +| `Evidence/OrganizationAliasChip` | Click a cataloged org; the parenthetical is the unique corroborated SKOS companion. | `--color-chip-border`, `--radius-chip`, `OrganizationAliasChip` | | `AnalysisRun/CutoffKnownBody` | Read the cutoff-known sentence, then compare it with the live body below. | `--color-accent-border`, `--space-panel-block`, `--radius-panel`, `CutoffKnownBody` | | `Analysis/LineageEntityPicker` | Choose which corp to reconstruct, then click Request a lineage reconstruction. | `--space-control-gap`, `--size-control-min`, `--radius-control`, `LineageEntityPicker` | -| `Admin/AdminPanel` | Change the tenant brand name, then verify the saved or failed state before leaving settings. | `--surface`, `--border`, `--space-panel-block`, `AdminPanel` | -| `Lineage/LineageDag` | Read Before on the A-100 fork, then click the revised-quote row to open that post; compare empty, single-branch, grouped/forked, mobile-scroll, ungrouped, and long-title states before changing graph CSS. On narrow viewports, swipe the named viewport or focus it and use arrow keys to inspect the full lineage. | `--color-accent-background`, `--radius-control`, `--surface`, `--border`, `--color-focus-border`, `--size-control-min`, `LineageDag` | | `Chrome/PopupCloseButton` | Close the evidence panel or post popup. | `--space-close-inset`, `--font-size-close`, `PopupCloseButton` | -| `Navigation/WorkspaceNav` | Open 게시판, 고객 마스터, 달력, or Ask Agent. Admin is not a GNB tab. | `--gnb-height`, `--gnb-active-indicator-color`, `WorkspaceNav` | -| `Evidence/OntologyExplorer` | Inspect typed people/orgs/posts, then open authorized evidence. Distinct from Event Lineage. | `--color-primary`, `--color-table-border`, `OntologyExplorer` | -| `Reports/LeftoverPairList` | Read residual R, observed Y, expected E, map rank, and distance after IRT main effects, then open the named post. | `--color-chip-border`, `LeftoverPairList` | 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; diff --git a/frontend/package.json b/frontend/package.json index 2a95c3a22..b3ab3a502 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,11 @@ { "name": "frontend", "private": true, +<<<<<<< HEAD "version": "2.15.1", +======= + "version": "2.14.0", +>>>>>>> 5ef5bf66 (feat: show corroborated SKOS companion on organization chips) "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 48e3697c4..0b7592883 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -116,6 +116,7 @@ describe("App, authenticated", () => { customerEntityHierarchy?: boolean; staleSummary?: boolean; contentAfterSummary?: boolean; + organizationAliases?: boolean; askLineageGraph?: boolean; askImageCitation?: boolean; }): ReturnType & { releaseMe: () => void; releasePostOne: () => void } { @@ -145,6 +146,7 @@ describe("App, authenticated", () => { let contentRequests = 0; let releaseMe = () => {}; + const demoOrgAlias = options?.organizationAliases ? { organization_alias: "DC" } : {}; const meReady = options?.deferMe ? new Promise((resolve) => { releaseMe = resolve; @@ -1301,7 +1303,14 @@ describe("App, authenticated", () => { person_side_label: "Our side", last_known_job_title: "Account manager", mention_context: null, - affiliations: [{ organization_name: "Demo Corp", corporate_entity_id: "corp-1", role_title: null }], + affiliations: [ + { + organization_name: "Demo Corp", + corporate_entity_id: "corp-1", + role_title: null, + ...demoOrgAlias, + }, + ], }, ], }), @@ -1403,6 +1412,7 @@ describe("App, authenticated", () => { ontology_label: "Organization", label: "Demo Corp", relevance: 0.2, + ...demoOrgAlias, }, { node_id: "team-1", @@ -1530,6 +1540,7 @@ describe("App, authenticated", () => { entity_level_code: "company", entity_level_label: "Company", resolved: true, + ...demoOrgAlias, people: [ { person_id: "person-ada", @@ -1596,6 +1607,7 @@ describe("App, authenticated", () => { verification_status_code: "verify_pending", verification_evidence_url: null, corporate_entity_id: "corp-1", + ...demoOrgAlias, }, { counterparty_entity_name: "Northridge Grid", @@ -2638,6 +2650,30 @@ describe("App, authenticated", () => { ); }); + it("shows the corroborated SKOS companion on organization chips", async () => { + stubBackend({ organizationAliases: true }); + render(); + + fireEvent.click(await screen.findByRole("button", { name: "View post: Public post" })); + + await waitFor(() => + expect(screen.getByRole("button", { name: "Affiliate org: Demo Corp (DC)" })).toBeInTheDocument(), + ); + expect(screen.getByRole("button", { name: "Counterparty org: Demo Corp (DC)" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Keyman affiliation: Demo Corp (DC)" })).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Affiliate org: Demo Corp" })).not.toBeInTheDocument(); + + fireEvent.click(screen.getByRole("button", { name: "Related nodes for Ada West" })); + await waitFor(() => expect(screen.getByText("Related to Ada West")).toBeInTheDocument()); + expect(screen.getByRole("button", { name: "Related nodes for Demo Corp (DC)" })).toBeInTheDocument(); + expect(screen.getByText("Related to Ada West").closest(".related-keymen")).toHaveTextContent( + "Demo Corp (DC)", + ); + expect(screen.getByText("Related to Ada West").closest(".related-keymen")).not.toHaveTextContent( + "Demo Corp (Organization)", + ); + }, 10_000); + it("opens related Keyman nodes from an R&R person", async () => { stubBackend(); render(); @@ -2683,7 +2719,7 @@ describe("App, authenticated", () => { await userEvent.click(await screen.findByRole("button", { name: "View post: Public post" })); await userEvent.click(screen.getByRole("button", { name: "Related nodes for Ada West" })); await waitFor(() => expect(screen.getByText("Related to Ada West")).toBeInTheDocument()); - await userEvent.click(screen.getByRole("button", { name: "Related nodes for 설계팀" })); + await userEvent.click(screen.getByRole("button", { name: "Related nodes for 설계팀 (Team)" })); await waitFor(() => expect(screen.getByText("Related to 설계팀")).toBeInTheDocument()); expect(screen.getByText("Related to 설계팀").closest(".related-keymen")).toHaveTextContent( "Linked post", @@ -2696,7 +2732,9 @@ describe("App, authenticated", () => { await userEvent.click(await screen.findByRole("button", { name: "View post: Public post" })); await userEvent.click(screen.getByRole("button", { name: "Related nodes for Ada West" })); await waitFor(() => expect(screen.getByText("Related to Ada West")).toBeInTheDocument()); - await userEvent.click(screen.getByRole("button", { name: "Related nodes for Demo Corp" })); + await userEvent.click( + screen.getByRole("button", { name: "Related nodes for Demo Corp (Organization)" }), + ); await waitFor(() => expect(screen.getByText("Related to Demo Corp")).toBeInTheDocument()); expect(screen.getByText("Related to Demo Corp").closest(".related-keymen")).toHaveTextContent( "Ada West (Our side)", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1ba1e181b..bc5709ee9 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -84,6 +84,8 @@ import { fetchTenantConfig, } from "./api"; import { CitationChip } from "./components/CitationChip"; +import { OrganizationAliasChip } from "./components/OrganizationAliasChip"; +import { organizationAliasCaption } from "./components/organizationAliasCaption"; import { CutoffKnownBody } from "./components/CutoffKnownBody"; import { LineageEntityPicker } from "./components/LineageEntityPicker"; import { OntologyExplorer } from "./components/OntologyExplorer"; @@ -601,17 +603,18 @@ function AffiliateTreeNode({
  • {node.resolved && node.entity_id && onSelectEntity ? ( - + /> ) : ( - node.entity_name + organizationAliasCaption(node.entity_name, node.organization_alias) )} {(node.entity_level_label || node.entity_level_code) && ( @@ -763,6 +766,12 @@ function relatedNodeCaption(node: RelatedNode): string { return `${name} (${side})`; } } + if (node.node_type_code === NODE_CORPORATE_ENTITY) { + const aliased = organizationAliasCaption(name, node.organization_alias); + if (aliased !== name) { + return aliased; + } + } return `${name} (${node.ontology_label ?? node.node_type_code})`; } @@ -1149,7 +1158,7 @@ function KeymanPanel({
  • + /> ) : ( - affiliation.organization_name + organizationAliasCaption( + affiliation.organization_name, + affiliation.organization_alias, + ) )} {affiliation.role_title && ( ({affiliation.role_title}) @@ -1496,17 +1512,18 @@ function CounterpartyPanel({ {counterparties.map((c) => (
  • {c.corporate_entity_id && onSelectEntity ? ( - + /> ) : ( - c.counterparty_entity_name + organizationAliasCaption(c.counterparty_entity_name, c.organization_alias) )}{" "} -- {c.relationship_label ?? c.relationship_type_code} {" -- "} @@ -4892,8 +4909,7 @@ export default function App({ showLabPanels = false }: { showLabPanels?: boolean
    + ); +} diff --git a/frontend/src/components/organizationAliasCaption.ts b/frontend/src/components/organizationAliasCaption.ts new file mode 100644 index 000000000..15c157cb0 --- /dev/null +++ b/frontend/src/components/organizationAliasCaption.ts @@ -0,0 +1,13 @@ +/** + * Visible chip text: ``Demo Corp (DC)`` when a unique SKOS companion is present. + */ +export function organizationAliasCaption( + displayName: string, + organizationAlias?: string | null, +): string { + const alias = (organizationAlias ?? "").trim(); + if (!alias || alias === displayName.trim()) { + return displayName; + } + return `${displayName} (${alias})`; +} diff --git a/lineageweave/organization_alias.py b/lineageweave/organization_alias.py new file mode 100644 index 000000000..dfd962ed6 --- /dev/null +++ b/lineageweave/organization_alias.py @@ -0,0 +1,143 @@ +"""Buyer-facing SKOS companion labels for corroborated organization names. + +[ADR 0008](docs/adr/0008-organization-abbreviation-resolution.md) already +persists a search-corroborated ``skos:altLabel`` / ``skos:prefLabel`` pair +(Miles & Bechhofer, 2009) in ``organization_name_resolution``. Catalog +resolution still compares mentions to ``corporate_entity.entity_name``, so +a chip that only prints that name hides the short form the source used. + +This module does not invent aliases. It returns the *other* label only when the +displayed record already carries the corroborated pair's unique catalog id, +and stays silent on a miss, an unbound record, identical labels, or a tie. + +Synthetic fixtures only: ``DC`` / ``Demo Corp``, ``AGP`` / ``Aurora Grid +Power``. Real organization names must not appear here. +""" + +from __future__ import annotations + +from collections.abc import Mapping, MutableMapping, Sequence +from dataclasses import dataclass +from typing import Any + + +@dataclass(frozen=True) +class OrganizationNameAlias: + """One corroborated SKOS alt/pref pair. + + Attributes: + alt_label: the abbreviated or slang form (``skos:altLabel``). + pref_label: the preferred catalog form (``skos:prefLabel``). + corporate_entity_id: unique existing catalog target, otherwise ``None``. + """ + + alt_label: str + pref_label: str + corporate_entity_id: str | None + + +def _normalize_alias_label(name: str) -> str: + """Normalize presentation labels without erasing legal-entity identity.""" + return " ".join(name.strip().lower().translate(str.maketrans("", "", ".,")).split()) + + +def companion_organization_alias( + display_name: str, + corporate_entity_id: str | None, + aliases: Sequence[OrganizationNameAlias], +) -> str | None: + """Return the other corroborated label, or ``None``. + + A display name that matches neither side, matches both sides of one + pair (identical labels), or matches two distinct companions is a + miss. Callers must not invent a parenthetical in those cases. + """ + normalized = _normalize_alias_label(display_name) + if not normalized or not corporate_entity_id: + return None + + companions: list[str] = [] + seen: set[str] = set() + for alias in aliases: + if alias.corporate_entity_id != corporate_entity_id: + continue + alt = _normalize_alias_label(alias.alt_label) + pref = _normalize_alias_label(alias.pref_label) + if not alt or not pref or alt == pref: + continue + companion: str | None = None + if normalized == pref: + companion = alias.alt_label.strip() + elif normalized == alt: + companion = alias.pref_label.strip() + if companion is None: + continue + key = _normalize_alias_label(companion) + if key in seen: + continue + seen.add(key) + companions.append(companion) + if len(companions) != 1: + return None + return companions[0] + + +def organization_alias_caption( + display_name: str, + organization_alias: str | None, +) -> str: + """Visible chip text: ``Demo Corp (DC)`` when an alias is present.""" + alias = (organization_alias or "").strip() + if not alias: + return display_name + return f"{display_name} ({alias})" + + +def attach_organization_alias( + record: MutableMapping[str, Any], + aliases: Sequence[OrganizationNameAlias], + *, + name_key: str = "entity_name", + entity_id_key: str = "corporate_entity_id", +) -> None: + """Write ``organization_alias`` onto one JSON record when unique.""" + name = record.get(name_key) + if not isinstance(name, str): + return + entity_id = record.get(entity_id_key) + companion = companion_organization_alias( + name, + str(entity_id) if entity_id is not None else None, + aliases, + ) + if companion: + record["organization_alias"] = companion + + +def attach_organization_aliases( + records: Sequence[Mapping[str, Any]] | Sequence[MutableMapping[str, Any]], + aliases: Sequence[OrganizationNameAlias], + *, + name_key: str = "entity_name", + entity_id_key: str = "corporate_entity_id", + children_key: str = "children", +) -> None: + """Write ``organization_alias`` onto a forest or a flat record list.""" + for record in records: + if not isinstance(record, MutableMapping): + continue + attach_organization_alias( + record, + aliases, + name_key=name_key, + entity_id_key=entity_id_key, + ) + children = record.get(children_key) + if isinstance(children, list): + attach_organization_aliases( + children, + aliases, + name_key=name_key, + entity_id_key=entity_id_key, + children_key=children_key, + ) diff --git a/scripts/seed_demo_data.py b/scripts/seed_demo_data.py index d4fca5ec4..9203a1824 100644 --- a/scripts/seed_demo_data.py +++ b/scripts/seed_demo_data.py @@ -208,6 +208,20 @@ def seed( (group_entity_id,), ) corporate_entity_id = cur.fetchone()[0] + cur.execute( + """ + insert into organization_name_resolution + (raw_organization_name, resolved_organization_name, + verification_status_code, verification_evidence_url) + values ('DC', 'Demo Corp', 'verify_corroborated', + 'https://example.test/searxng?q=Demo+Corp+DC') + on conflict (raw_organization_name) do update set + resolved_organization_name = excluded.resolved_organization_name, + verification_status_code = excluded.verification_status_code, + verification_evidence_url = excluded.verification_evidence_url, + resolved_at = now() + """ + ) cur.execute( "insert into process_unit (corporate_entity_id, process_unit_code, process_unit_name) values " diff --git a/tests/test_affiliate_tree.py b/tests/test_affiliate_tree.py index 3210815b8..7a88fbb81 100644 --- a/tests/test_affiliate_tree.py +++ b/tests/test_affiliate_tree.py @@ -2,11 +2,16 @@ from __future__ import annotations +import asyncio +from unittest.mock import AsyncMock + +import backend.app.affiliate_tree_ingestion as ingestion from lineageweave.affiliate_tree import ( AffiliationLeaf, CorporateEntityRow, build_affiliate_forest, ) +from lineageweave.organization_alias import OrganizationNameAlias _ENTITIES = ( CorporateEntityRow("group-id", None, "Demo Group", "group"), @@ -105,3 +110,38 @@ def test_to_dict_is_the_api_shape() -> None: assert payload["entity_name"] == "Demo Group" assert payload["resolved"] is True assert payload["children"][0]["people"][0]["person_name"] == "Ada West" + + +def test_affiliate_forest_reuses_one_corroborated_alias_load(monkeypatch) -> None: + """One request shares its alias snapshot with Keyman and forest hydration.""" + aliases = (OrganizationNameAlias("DC", "Demo Corp", "demo-id"),) + + class _Connection: + async def fetch(self, query: str, *_args: object): + assert "from corporate_entity" in query + return [] + + conn = _Connection() + fetch_aliases = AsyncMock(return_value=aliases) + fetch_keymen = AsyncMock(return_value=[]) + monkeypatch.setattr(ingestion, "fetch_corroborated_organization_aliases", fetch_aliases) + monkeypatch.setattr(ingestion, "fetch_post_keymen", fetch_keymen) + + assert asyncio.run(ingestion.fetch_affiliate_forest(conn, "post-1")) == [] + fetch_aliases.assert_awaited_once_with(conn) + fetch_keymen.assert_awaited_once_with(conn, "post-1", organization_aliases=aliases) + + +def test_voc_evidence_skips_unused_organization_aliases(monkeypatch) -> None: + """VOC excerpts need affiliation names, not alias decoration or its query.""" + + conn = AsyncMock() + conn.fetchrow.side_effect = [{"lookup_label": "VOC"}, {"post_body": "Demo Corp update."}] + conn.fetch.return_value = [] + fetch_keymen = AsyncMock(return_value=[]) + monkeypatch.setattr(ingestion, "fetch_post_keymen", fetch_keymen) + + payload = asyncio.run(ingestion.fetch_voc_evidence(conn, "post-1", "voc")) + + assert payload["voc_type_label"] == "VOC" + fetch_keymen.assert_awaited_once_with(conn, "post-1", organization_aliases=()) diff --git a/tests/test_knowledge_graph.py b/tests/test_knowledge_graph.py index b2d023871..9319be85f 100644 --- a/tests/test_knowledge_graph.py +++ b/tests/test_knowledge_graph.py @@ -16,8 +16,11 @@ from __future__ import annotations +import asyncio + import pytest +from backend.app.knowledge_graph import hydrate_related_nodes from lineageweave.knowledge_graph import ( EDGE_AFFILIATION, EDGE_CO_MENTION, @@ -141,3 +144,29 @@ def test_rwr_from_a_keyman_reaches_co_mentioned_person_and_affiliated_org() -> N assert f"{NODE_POST}:post-1" in related assert f"{NODE_CORPORATE_ENTITY}:corp-1" in related assert related[f"{NODE_PERSON}:person-b"] > 0 + + +def test_related_corporate_node_uses_its_catalog_id_for_alias() -> None: + class FakeConnection: + async def fetch(self, query: str, *_args): + if "from corporate_entity where" in query: + return [ + {"corporate_entity_id": "demo-id", "entity_name": "Demo Corp"} + ] + if "from organization_name_resolution" in query: + return [ + { + "raw_organization_name": "DC", + "resolved_organization_name": "Demo Corp", + "corporate_entity_id": "demo-id", + } + ] + raise AssertionError(query) + + payload = asyncio.run( + hydrate_related_nodes( + FakeConnection(), + [(f"{NODE_CORPORATE_ENTITY}:demo-id", 0.8)], + ) + ) + assert payload[0]["organization_alias"] == "DC" diff --git a/tests/test_organization_alias.py b/tests/test_organization_alias.py new file mode 100644 index 000000000..4ae637bba --- /dev/null +++ b/tests/test_organization_alias.py @@ -0,0 +1,96 @@ +"""Corroborated SKOS companion labels stay unique and fail closed on a tie.""" + +from __future__ import annotations + +from lineageweave.organization_alias import ( + OrganizationNameAlias, + attach_organization_aliases, + companion_organization_alias, + organization_alias_caption, +) + +_DC = OrganizationNameAlias( + alt_label="DC", pref_label="Demo Corp", corporate_entity_id="demo-id" +) +_AGP = OrganizationNameAlias( + alt_label="AGP", pref_label="Aurora Grid Power", corporate_entity_id="aurora-id" +) + + +def test_pref_label_returns_the_alt_label() -> None: + assert companion_organization_alias("Demo Corp", "demo-id", (_DC,)) == "DC" + + +def test_alt_label_returns_the_pref_label() -> None: + assert companion_organization_alias("AGP", "aurora-id", (_AGP,)) == "Aurora Grid Power" + + +def test_uncorroborated_or_unknown_name_stays_unlabeled() -> None: + assert companion_organization_alias("Northridge Grid", "demo-id", (_DC, _AGP)) is None + assert companion_organization_alias("Demo Corp", "demo-id", ()) is None + assert companion_organization_alias(" ", "demo-id", (_DC,)) is None + assert companion_organization_alias("Demo Corp", None, (_DC,)) is None + assert companion_organization_alias("Demo Corp", "other-id", (_DC,)) is None + + +def test_legal_suffix_difference_does_not_bind_an_alias() -> None: + assert companion_organization_alias("Demo Inc", "demo-id", (_DC,)) is None + + +def test_identical_labels_are_ignored() -> None: + same = OrganizationNameAlias("Demo Corp", "Demo Corp", "demo-id") + assert companion_organization_alias("Demo Corp", "demo-id", (same,)) is None + + +def test_two_distinct_companions_stay_unbound() -> None: + other = OrganizationNameAlias("DMC", "Demo Corp", "demo-id") + assert companion_organization_alias("Demo Corp", "demo-id", (_DC, other)) is None + + +def test_duplicate_pair_keeps_one_companion() -> None: + assert companion_organization_alias("Demo Corp", "demo-id", (_DC, _DC)) == "DC" + + +def test_caption_puts_the_alias_in_parentheses() -> None: + assert organization_alias_caption("Demo Corp", "DC") == "Demo Corp (DC)" + assert organization_alias_caption("Demo Corp", None) == "Demo Corp" + assert organization_alias_caption("Demo Corp", " ") == "Demo Corp" + + +def test_forest_attach_is_recursive_and_omits_missing_keys() -> None: + forest = [ + { + "entity_id": "group-id", + "entity_name": "Demo Group", + "children": [ + {"entity_id": "demo-id", "entity_name": "Demo Corp", "children": []}, + {"entity_id": "north-id", "entity_name": "Northridge Grid", "children": []}, + ], + } + ] + attach_organization_aliases(forest, (_DC,), entity_id_key="entity_id") + assert "organization_alias" not in forest[0] + assert forest[0]["children"][0]["organization_alias"] == "DC" + assert "organization_alias" not in forest[0]["children"][1] + + +def test_forest_attach_ignores_non_records_and_non_string_names() -> None: + records = [ + "not-a-record", + {"entity_name": None}, + {"entity_name": "Demo Corp", "corporate_entity_id": "demo-id"}, + ] + attach_organization_aliases(records, (_DC,)) # type: ignore[arg-type] + assert records[2]["organization_alias"] == "DC" # type: ignore[index] + + +def test_same_name_on_another_catalog_id_stays_unlabeled() -> None: + records = [ + {"entity_name": "Demo Corp", "corporate_entity_id": "demo-id"}, + {"entity_name": "Demo Corp", "corporate_entity_id": "other-id"}, + {"entity_name": "Demo Corp", "corporate_entity_id": None}, + ] + attach_organization_aliases(records, (_DC,)) + assert records[0]["organization_alias"] == "DC" + assert "organization_alias" not in records[1] + assert "organization_alias" not in records[2] diff --git a/tests/test_organization_name_resolution_ingestion.py b/tests/test_organization_name_resolution_ingestion.py index fef9b3abd..4f21121ac 100644 --- a/tests/test_organization_name_resolution_ingestion.py +++ b/tests/test_organization_name_resolution_ingestion.py @@ -6,8 +6,11 @@ import pytest import backend.app.organization_name_resolution_ingestion as ingestion +from lineageweave.corporate_hierarchy_resolution import ( + OrganizationNameAlias as CorporateHierarchyOrganizationNameAlias, +) +from lineageweave.organization_alias import OrganizationNameAlias from lineageweave.relation_verification import STATUS_CORROBORATED, STATUS_UNCORROBORATED -from lineageweave.corporate_hierarchy_resolution import OrganizationNameAlias class _Connection: @@ -95,5 +98,54 @@ def test_load_corroborated_aliases_returns_search_verified_pairs() -> None: ] aliases = asyncio.run(ingestion.load_corroborated_organization_name_aliases(conn)) assert aliases == [ - OrganizationNameAlias(alt_label="AGP", pref_label="Aurora Grid Power") + CorporateHierarchyOrganizationNameAlias( + alt_label="AGP", pref_label="Aurora Grid Power" + ) ] + + +class _AliasConnection: + def __init__(self, rows: list[dict[str, str | None]]) -> None: + self.rows = rows + self.bound_status: object | None = None + self.query = "" + + async def fetch(self, query: str, status: object): + self.query = query + self.bound_status = status + return self.rows + + +def test_fetch_corroborated_aliases_binds_verified_status() -> None: + conn = _AliasConnection( + [ + { + "raw_organization_name": "DC", + "resolved_organization_name": "Demo Corp", + "corporate_entity_id": "demo-id", + } + ] + ) + aliases = asyncio.run(ingestion.fetch_corroborated_organization_aliases(conn)) + assert conn.bound_status == STATUS_CORROBORATED + assert aliases == ( + OrganizationNameAlias( + alt_label="DC", pref_label="Demo Corp", corporate_entity_id="demo-id" + ), + ) + assert "count(distinct entity.corporate_entity_id) = 1" in conn.query + assert aliases == (OrganizationNameAlias("DC", "Demo Corp", "demo-id"),) + + +def test_fetch_corroborated_aliases_keeps_catalog_ties_unbound() -> None: + conn = _AliasConnection( + [ + { + "raw_organization_name": "DC", + "resolved_organization_name": "Demo Corp", + "corporate_entity_id": None, + } + ] + ) + aliases = asyncio.run(ingestion.fetch_corroborated_organization_aliases(conn)) + assert aliases == (OrganizationNameAlias("DC", "Demo Corp", None),) diff --git a/uv.lock b/uv.lock index 87668cd4a..fbba5bd5a 100644 --- a/uv.lock +++ b/uv.lock @@ -597,7 +597,11 @@ wheels = [ [[package]] name = "lineageweave" +<<<<<<< HEAD version = "2.15.1" +======= +version = "2.14.0" +>>>>>>> 5ef5bf66 (feat: show corroborated SKOS companion on organization chips) source = { editable = "." } dependencies = [ { name = "certifi" },