diff --git a/AGENTS.md b/AGENTS.md index 06999f370..686ca7322 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,7 +12,7 @@ normative research-grounding policy, and [`docs/lineage-bi-research-notes.md`](docs/lineage-bi-research-notes.md) for supporting literature and aggregate evidence. Event Lineage (reconstructed post-to-post parents) is distinct from the typed ontology neighborhood -(ADR 0168); source-window continuation is ADR 0124. Do not mix those graphs. +(ADR 0184); source-window continuation is ADR 0124. Do not mix those graphs. ## Hard rule: no real data in repository artifacts diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 235f2a216..88d645c90 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -79,7 +79,7 @@ flowchart LR | `post_chat.py` | Pluggable in-popup chat's reason-and-cite step (retrieve step lives in `backend/app/post_chat_ingestion.py`) | | `commitment_extraction.py` | Pluggable LLM derivation of a customer commitment (promise + deadline) from a post; `Null` default, `ContextualOrchestrator` real impl | | `ontology.py` | Loads `docs/ontology/lineageweave-kg.ttl`, the formal OWL 2/RDFS/SKOS vocabulary for the Knowledge Graph's node/edge types (ADR 0004) | -| `ontology_neighborhood.py` | Bounded typed ontology/provenance neighborhood (ADR 0168); PostgreSQL stays authoritative, OWL subclass is not an instance edge | +| `ontology_neighborhood.py` | Bounded typed ontology/provenance neighborhood (ADR 0184); PostgreSQL stays authoritative, OWL subclass is not an instance edge | | `ontology_source_cursor.py` | Opaque HMAC source-window continuation (ADR 0124); keyset pagination, never OFFSET | | `period_report.py` | Fit GRM/GPCM on persisted IRT rows, FIPC-select, EAP-score a period (ADR 0003 slice 3; Bock & Mislevy, 1982) | | `fixtures.py` | Synthetic demo dataset -- no real data ships in this repo | diff --git a/CHANGELOG.d/2.13.0-ontology-explorer.md b/CHANGELOG.d/2.13.0-ontology-explorer.md index 7893733a1..f57c47f8b 100644 --- a/CHANGELOG.d/2.13.0-ontology-explorer.md +++ b/CHANGELOG.d/2.13.0-ontology-explorer.md @@ -1,7 +1,7 @@ # 2.13.0 — Ontology provenance explorer - Added `GET /api/ontology/neighborhood` as a bounded typed instance graph - (ADR 0168 / issue #341), distinct from Event Lineage. + (ADR 0184 / issue #341), distinct from Event Lineage. - PostgreSQL stays authoritative; OWL/RDF/JSON-LD is a projection. - SKOS broader comes from `corporate_entity.parent_entity_id`; OWL subclass is rejected as an instance edge. diff --git a/CHANGELOG.d/2.15.0-gnb-four-korean-chrome.md b/CHANGELOG.d/2.15.0-gnb-four-korean-chrome.md new file mode 100644 index 000000000..fbefdc718 --- /dev/null +++ b/CHANGELOG.d/2.15.0-gnb-four-korean-chrome.md @@ -0,0 +1,7 @@ +# 2.15.0 — Four-destination Korean analyst GNB + +- Analyst Global Navigation is exactly 게시판, 고객 마스터, 달력, and Ask Agent. +- Buyer/Cubee copy is off the chrome; `BuyerNav` is now `WorkspaceNav`. +- Operator Admin stays a non-GNB destination. +- Unwired CalendarWeave consume fail-closes with + `이 범위의 일정을 아직 받을 수 없습니다` (ADR 0183). diff --git a/CHANGELOG.md b/CHANGELOG.md index b2040d64c..87383d244 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ All notable changes to this project are documented here. Format follows - Bounded ontology/provenance neighborhood (`GET /api/ontology/neighborhood`) with typed Post/Person/CorporateEntity/Team nodes, SKOS broader distinct from OWL subclass, truth-status vocabulary, knowledge-cutoff binding, and - a Keyman-panel explorer that is not Event Lineage (ADR 0168 / #341). + a Keyman-panel explorer that is not Event Lineage (ADR 0184 / #341). - Corroborated SKOS `altLabel` / `prefLabel` pairs now expand corporate catalog candidates so a synthetic short form (`AGP`) and full form (`Aurora Grid Power`) bind one `corporate_entity` row instead of @@ -84,6 +84,22 @@ All notable changes to this project are documented here. Format follows and looks up corporate-parent visibility independently of the child. OWL-Time is cited as a W3C Candidate Recommendation Draft. +## [2.15.0] - 2026-08-25 + +### Changed + +- Analyst Global Navigation is exactly four Korean destinations: 게시판, + 고객 마스터, 달력, and Ask Agent. `BuyerNav` is now `WorkspaceNav`; Buyer + and Cubee are not product names on the chrome (ADR 0183). +- Operator Admin remains available as a non-GNB destination and is not a + fifth analyst tab. + +### Fixed + +- The 달력 destination fail-closes when CalendarWeave / Naruon CalDAV consume + is unwired, with `이 범위의 일정을 아직 받을 수 없습니다`. Weekly VOC and + newspaper stay on the board. + ## [2.12.26] - 2026-08-24 ### Added diff --git a/backend/app/ontology_neighborhood_ingestion.py b/backend/app/ontology_neighborhood_ingestion.py index 921bda972..b9d6d4900 100644 --- a/backend/app/ontology_neighborhood_ingestion.py +++ b/backend/app/ontology_neighborhood_ingestion.py @@ -1,4 +1,4 @@ -"""Load an ABAC-visible ontology neighborhood from PostgreSQL (ADR 0168).""" +"""Load an ABAC-visible ontology neighborhood from PostgreSQL (ADR 0184).""" from __future__ import annotations diff --git a/docs/adr/0124-ontology-source-window-cursor.md b/docs/adr/0124-ontology-source-window-cursor.md index a081c4c7a..ce808dd6b 100644 --- a/docs/adr/0124-ontology-source-window-cursor.md +++ b/docs/adr/0124-ontology-source-window-cursor.md @@ -3,7 +3,7 @@ **Status:** Accepted **Date:** 2026-08-21 **Issue:** [#363](https://github.com/ContextualWisdomLab/LineageWeave/issues/363) -**Predecessor:** [ADR 0168](0168-ontology-provenance-explorer.md) +**Predecessor:** [ADR 0184](0184-ontology-provenance-explorer.md) **Context:** PR #349 withholds a continuation token when the authorization-safe recursive SQL window is exhausted. The in-memory `after:` cursor can only page facts already loaded, so a buyer cannot inspect every authorized relation in a large neighborhood without fabricating completeness. Issue #363 requires a real database continuation contract that keeps bounds, ABAC, cutoff, and deterministic ordering. diff --git a/docs/adr/0183-gnb-four-korean-chrome.md b/docs/adr/0183-gnb-four-korean-chrome.md new file mode 100644 index 000000000..8d004137a --- /dev/null +++ b/docs/adr/0183-gnb-four-korean-chrome.md @@ -0,0 +1,49 @@ +# ADR 0183: Four-destination Korean analyst GNB chrome + +- Status: Accepted +- Date: 2026-08-25 +- Supersedes (labels only): [0037](0037-buyer-gnb-and-product-surface.md) English GNB labels +- Folds: closed unmerged #474 WorkspaceNav rename + +## Context + +Main still shipped `BuyerNav` with English analyst tabs (Board / Customer +master / Calendar / Ask Agent / Admin). LineageWeave is an analyst workspace, +not a storefront, so "Buyer" and "Cubee" are not product names. Calendar is +CalendarWeave / Naruon CalDAV consume only (issue #336); this slice must not +add a calendar kernel. + +## Decision + +1. Analyst Global Navigation is exactly four destinations, in this order, with + these Korean labels regardless of locale: + + 1. 게시판 (board) + 2. 고객 마스터 (customers) + 3. 달력 (calendar) + 4. Ask Agent (ask) + +2. `BuyerNav` / `BuyerDestination` become `WorkspaceNav` / + `WorkspaceDestination`. GNB CSS uses `.workspace-gnb*`. The accessible name + is `Workspace navigation`. +3. Operator Admin remains a non-GNB destination. It is not a fifth analyst tab. +4. Weekly VOC remains a board filter. Weekly/monthly newspaper remains a + scheduled board post. Neither is a GNB item. +5. The 달력 destination fail-closes when CalendarWeave / Naruon consume is + unwired or missing, with the exact copy + `이 범위의 일정을 아직 받을 수 없습니다`. Existing advanced-review + commitment projection is not a calendar kernel and is not this GNB surface. + +## Consequences + +- Analyst chrome no longer shows Buyer, Cubee, Board, or Customer master. +- CalendarWeave wiring stays a later consume-only slice. This PR does not + import naruon mailbox, ThreadWeave, or Keyverse dumps. +- Historical ADRs keep the wording of their time. + +## References + +- ADR 0037 (original four-destination GNB) +- ADR 0038 (CalDAV consume vs internal commitments) +- Issue #336 (Naruon calendar projection) +- Closed unmerged #474 (WorkspaceNav rename) diff --git a/docs/adr/0168-ontology-provenance-explorer.md b/docs/adr/0184-ontology-provenance-explorer.md similarity index 98% rename from docs/adr/0168-ontology-provenance-explorer.md rename to docs/adr/0184-ontology-provenance-explorer.md index aed4ef0b7..530238687 100644 --- a/docs/adr/0168-ontology-provenance-explorer.md +++ b/docs/adr/0184-ontology-provenance-explorer.md @@ -1,4 +1,4 @@ -# ADR 0168: Heterogeneous ontology and provenance explorer +# ADR 0184: Heterogeneous ontology and provenance explorer **Status:** Accepted **Date:** 2026-08-21 diff --git a/docs/adr/README.md b/docs/adr/README.md index 64927cbe0..737a54bca 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -15,7 +15,7 @@ decision from them. | [`PROV_O_IMPLEMENTATION_MATRIX.md`](../PROV_O_IMPLEMENTATION_MATRIX.md) | [0065](0065-prov-o-provenance-boundary.md) | | [`ONTOLOGY_NAMESPACE_INVENTORY.md`](../doctoring/ONTOLOGY_NAMESPACE_INVENTORY.md) | [0157](0157-public-ontology-namespace-identity.md) | | [`image-content-schema.md`](../image-content-schema.md) | [0066](0066-position-preserving-image-content.md) | -| [`storybook-inventory.md`](../storybook-inventory.md) | [0118](0118-uiux-standard-guide-v3-design-overhaul.md), [0168](0168-ontology-provenance-explorer.md) | +| [`storybook-inventory.md`](../storybook-inventory.md) | [0118](0118-uiux-standard-guide-v3-design-overhaul.md), [0184](0184-ontology-provenance-explorer.md) | Files under `docs/doctoring/` remain non-normative supporting evidence even when this map links them to an ADR. Runtime-evidence files record observed diff --git a/docs/doctoring/ONTOLOGY_EXPLORER_REFERENCES.md b/docs/doctoring/ONTOLOGY_EXPLORER_REFERENCES.md index 88e4a4ac8..bce474846 100644 --- a/docs/doctoring/ONTOLOGY_EXPLORER_REFERENCES.md +++ b/docs/doctoring/ONTOLOGY_EXPLORER_REFERENCES.md @@ -1,6 +1,6 @@ # Ontology explorer — doctoring -These are the standards that ground ADR 0168 / issue #341. Cite them in +These are the standards that ground ADR 0184 / issue #341. Cite them in APA 7th when you extend the neighborhood, projection, or Buyer graph. Cyganiak, R., Wood, D., & Lanthaler, M. (Eds.). (2014). *RDF 1.1 concepts diff --git a/docs/storybook-inventory.md b/docs/storybook-inventory.md index 86974dd97..b4c9f3e20 100644 --- a/docs/storybook-inventory.md +++ b/docs/storybook-inventory.md @@ -13,6 +13,7 @@ buyer-facing control you can click before changing product CSS. | `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` | Open the current branch node; compare empty, 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. | `--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` | diff --git a/frontend/package.json b/frontend/package.json index 33d6c7d6e..5e9cc95d5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "2.14.0", + "version": "2.15.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.css b/frontend/src/App.css index b8e62ffa9..01e491796 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -142,7 +142,7 @@ } /* GNB Navigation (§2.3.1) */ -.buyer-gnb { +.workspace-gnb { display: flex; align-items: center; height: var(--gnb-height); @@ -155,7 +155,7 @@ z-index: var(--z-gnb-pulldown); } -.buyer-gnb-item { +.workspace-gnb-item { display: flex; align-items: center; height: 100%; @@ -170,15 +170,15 @@ transition: color 0.15s ease; } -.buyer-gnb-item:hover { +.workspace-gnb-item:hover { color: var(--color-text-heading); } -.buyer-gnb-item[aria-current="page"] { +.workspace-gnb-item[aria-current="page"] { color: var(--color-primary); } -.buyer-gnb-item[aria-current="page"]::after { +.workspace-gnb-item[aria-current="page"]::after { content: ""; position: absolute; bottom: 0; @@ -188,7 +188,7 @@ background-color: var(--gnb-active-indicator-color); } -.buyer-gnb-tools { +.workspace-gnb-tools { margin-left: auto; display: flex; align-items: center; @@ -906,7 +906,7 @@ .app-header { padding: 0 1rem; } - .buyer-gnb { + .workspace-gnb { padding: 0 1rem; } } @@ -914,7 +914,7 @@ @media (max-width: 768px) { /* Phone Breakpoint (<768px) */ - .buyer-gnb { + .workspace-gnb { display: none; /* Replaced by drawer on mobile */ } diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 7a2faf0ef..d0b0bce1d 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -1944,7 +1944,7 @@ describe("App, authenticated", () => { stubBackend(); render(); expect(await screen.findByRole("button", { name: "View post: Public post" })).toBeInTheDocument(); - await userEvent.click(screen.getByRole("button", { name: "Customer master" })); + await userEvent.click(screen.getByRole("button", { name: "고객 마스터" })); expect(await screen.findByText("Demo Corp")).toBeInTheDocument(); expect(screen.getByText("DEMO-CORP-01 · Company")).toBeInTheDocument(); @@ -1964,7 +1964,7 @@ describe("App, authenticated", () => { stubBackend({ customerEntityHierarchy: true }); render(); expect(await screen.findByRole("button", { name: "View post: Public post" })).toBeInTheDocument(); - await userEvent.click(screen.getByRole("button", { name: "Customer master" })); + await userEvent.click(screen.getByRole("button", { name: "고객 마스터" })); expect(await screen.findByText("Demo Group")).toBeInTheDocument(); const subsidiaryRow = screen.getByText("Demo Corp").closest("li"); @@ -1984,7 +1984,7 @@ describe("App, authenticated", () => { stubBackend(); render(); expect(await screen.findByRole("button", { name: "View post: Public post" })).toBeInTheDocument(); - await userEvent.click(screen.getByRole("button", { name: "Customer master" })); + await userEvent.click(screen.getByRole("button", { name: "고객 마스터" })); expect(await screen.findByText("Northridge Grid")).toBeInTheDocument(); expect(screen.getByText("Voice of Customer (1), Voice of Competitor (1)")).toBeInTheDocument(); @@ -2006,7 +2006,7 @@ describe("App, authenticated", () => { stubBackend({ admin: true, manyCustomerHints: 1 }); render(); expect(await screen.findByRole("button", { name: "View post: Public post" })).toBeInTheDocument(); - await userEvent.click(screen.getByRole("button", { name: "Customer master" })); + await userEvent.click(screen.getByRole("button", { name: "고객 마스터" })); expect(await screen.findByText("CUST-0")).toBeInTheDocument(); await userEvent.click(screen.getByRole("button", { name: "Resolve" })); @@ -2018,7 +2018,7 @@ describe("App, authenticated", () => { stubBackend({ manyCustomerHints: 1 }); render(); expect(await screen.findByRole("button", { name: "View post: Public post" })).toBeInTheDocument(); - await userEvent.click(screen.getByRole("button", { name: "Customer master" })); + await userEvent.click(screen.getByRole("button", { name: "고객 마스터" })); expect(await screen.findByText("CUST-0")).toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Resolve" })).not.toBeInTheDocument(); @@ -2034,7 +2034,7 @@ describe("App, authenticated", () => { stubBackend({ hintRelatedPosts: true }); render(); expect(await screen.findByRole("button", { name: "View post: Public post" })).toBeInTheDocument(); - await userEvent.click(screen.getByRole("button", { name: "Customer master" })); + await userEvent.click(screen.getByRole("button", { name: "고객 마스터" })); const customerSection = await screen.findByRole("region", { name: "Observed customer evidence" }); expect(within(customerSection).getByText("Related posts (1)").closest("details")).toHaveClass( @@ -2060,7 +2060,7 @@ describe("App, authenticated", () => { stubBackend({ manyCustomerHints: 45 }); render(); expect(await screen.findByRole("button", { name: "View post: Public post" })).toBeInTheDocument(); - await userEvent.click(screen.getByRole("button", { name: "Customer master" })); + await userEvent.click(screen.getByRole("button", { name: "고객 마스터" })); expect(await screen.findByText("CUST-0")).toBeInTheDocument(); expect(screen.getByText(/Showing the first 30 of 45 observed customer identifiers/)).toBeInTheDocument(); @@ -3868,12 +3868,31 @@ describe("App, authenticated", () => { ); }); - it("keeps advanced review tools out of the buyer board", async () => { + it("keeps advanced review tools out of the analyst board", async () => { stubBackend(); render(); - expect(await screen.findByRole("navigation", { name: "Buyer navigation" })).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Board" })).toHaveAttribute("aria-current", "page"); + const nav = await screen.findByRole("navigation", { name: "Workspace navigation" }); + expect(nav).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "게시판" })).toHaveAttribute("aria-current", "page"); + expect(within(nav).getAllByRole("button").map((button) => button.textContent)).toEqual([ + "게시판", + "고객 마스터", + "달력", + "Ask Agent", + ]); + expect(nav.textContent).not.toMatch(/Buyer|Cubee|Board|Customer master/i); + expect(within(nav).queryByRole("button", { name: /Admin|관리자/i })).not.toBeInTheDocument(); expect(screen.queryByText("Advanced review tools")).not.toBeInTheDocument(); }); + + it("fails closed on the calendar destination when CalendarWeave consume is unwired", async () => { + stubBackend(); + render(); + + await userEvent.click(await screen.findByRole("button", { name: "달력" })); + expect(screen.getByRole("heading", { name: "달력" })).toBeInTheDocument(); + expect(screen.getByText("이 범위의 일정을 아직 받을 수 없습니다")).toBeInTheDocument(); + expect(screen.queryByText(/Buyer|Cubee/i)).not.toBeInTheDocument(); + }); }); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8c1d3b27e..22f002775 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -90,7 +90,8 @@ import { OntologyExplorer } from "./components/OntologyExplorer"; import { AskEvidenceLayerPopup } from "./components/AskEvidenceLayerPopup"; import { PopupCloseButton } from "./components/PopupCloseButton"; import { chatEvidenceKindLabel } from "./evidenceKindLabels"; -import { BuyerNav, type BuyerDestination } from "./components/BuyerNav"; +import { WorkspaceNav, type WorkspaceDestination } from "./components/WorkspaceNav"; +import { CALENDAR_CONSUME_UNAVAILABLE } from "./gnbChrome"; import { LineageDag } from "./LineageDag"; import { PostBody } from "./PostBody"; import { decodeHtmlEntities } from "./postBodyDisplay"; @@ -4360,10 +4361,10 @@ function CustomerMasterPanel({ } return ( -
+

{t("Authorized customer scope")}

{t("Customer master")}

-

{t("Customer entities available to this account.")}

+

{t("Customer entities available to this account.")}

{error ?

{error}

: null} {master === null && !error ?

{t("Loading customer master...")}

: null} {master?.corporate_entities.length === 0 ? ( @@ -4388,7 +4389,7 @@ function CustomerMasterPanel({ {master && (master.relationship_network ?? []).length > 0 ? (

{t("Relationship network")}

-

+

{t("A counterparty can hold more than one role over time -- a customer in one post can be a competitor, supplier, or partner in another. Every role observed for a name is listed, not just the most frequent.")}

    @@ -4411,7 +4412,7 @@ function CustomerMasterPanel({ {master && master.source_customer_hints.length > 0 ? (

    {t("Observed customer evidence")}

    -

    +

    {t("Source identifiers are hints only; ontology and semantic evidence must resolve them before binding a customer.")}

    {master.source_customer_hints.length > HINT_RENDER_LIMIT && ( @@ -4562,10 +4563,10 @@ function AskAgentPanel({ } return ( -
    +

    {t("Evidence-grounded questions")}

    {t("Ask Agent")}

    -

    {t("Questions use authorized posts and their evidence.")}

    +

    {t("Questions use authorized posts and their evidence.")}

    {error ?

    {error}

    : null}