diff --git a/CHANGELOG.d/ontology-node-readability.md b/CHANGELOG.d/ontology-node-readability.md new file mode 100644 index 000000000..50ca29ad4 --- /dev/null +++ b/CHANGELOG.d/ontology-node-readability.md @@ -0,0 +1,5 @@ +### Changed + +- Ontology neighborhood nodes now reuse light/dark design tokens for distinct + Post, Person, Organization, and Team surfaces while retaining shape, text, + keyboard controls, and the exact-value table as the authoritative cues. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index aa1bcf250..974bef3ac 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,6 +1,6 @@ # Product & Technical Gap Baseline -> Dashboard delivery snapshot: 2026-08-26 05:01 KST. Protected `main` was +> Dashboard delivery snapshot: 2026-08-26 06:20 KST. Protected `main` was > `04e6b610655d0db91d5f7ba9486bdda1440e0b19`. This local branch is not > protected-main release evidence. @@ -60,14 +60,14 @@ only aggregate, non-identifying evidence to this repository. ### Exact open-PR boundary -At this snapshot there were 16 open PRs and 10 open issues. The exact-head +At this snapshot there were 11 open PRs and 10 open issues. The exact-head inventory in section 1 is authoritative for this snapshot. Every open head remained blocked on hosted gates and/or independent review. These observations are not merge readiness. Re-fetch exact heads, unresolved threads, checks, approvals, rulesets, and merge SHA before any lifecycle claim. -> Audit snapshot: 2026-08-26 05:01 KST (refreshed by the autonomous merge +> Audit snapshot: 2026-08-26 06:20 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, @@ -77,29 +77,25 @@ lifecycle claim. ## 1. Exact-head and governance evidence The protected default branch was `04e6b610655d0db91d5f7ba9486bdda1440e0b19` -when this baseline was refreshed. The live queue contained 16 open PRs and 10 +when this baseline was refreshed. The live queue contained 11 open PRs and 10 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 | | ---: | --- | --- | -| #663 | `be361f10` | project ontology traversal plus cutoff/snapshot-frozen project focus and labels | -| #662 | `92534118` | fail-closed injectable TEPP status/read boundary; the owning executable HTTP route remains unavailable | -| #661 | `11206c58` | Rust-estimated reconstruction fixture weights replace hand-authored reconstruction-test dictionaries | -| #660 | `c10cae01` (observed parent) | backend runtime repair plus the #664 semantic-unit stack; this documentation commit advances the head after capture | -| #659 | `e948bd27` | ontology node readability and tokenized UI fills | -| #658 | `fe830b0a` | evidence-honest Global Ask cutoff with revision-interval live-after semantics | -| #657 | `64f48679` | Dashboard case-metric contract work | -| #644 | `d9ff9980` | native-surface code splitting with modal-focus regression coverage | -| #643 | `0a1f8ec1` | accessible status-notice surfaces | -| #640 | `2d50fa01` | operations-dashboard contract alignment | -| #639 | `aee02dca` | terminal checks observed; exact-head independent approval still required | -| #636 | `f7b9a65f` | terminal checks observed; exact-head independent approval still required | -| #632 | `32c7d359` | active semantic provenance repair head; hosted checks and independent review required | -| #631 | `c0022c97` | terminal checks observed; exact-head independent approval still required | -| #629 | `4b4d6707` | terminal checks observed; exact-head independent approval still required | -| #579 | `689a21b6` | leftover interaction-map coordinate persistence; exact-head independent approval still required | +| #663 | `ab330a0f` | project ontology traversal plus cutoff/snapshot-frozen project focus and labels; exact-head checks/review required | +| #660 | `24fda085` | backend runtime repair plus semantic-unit stack; exact-head checks/review required | +| #659 | `0739b9d7` | ontology node readability and tokenized UI fills; branch is DIRTY against current main and requires conflict repair | +| #658 | `fe830b0a` | evidence-honest Global Ask cutoff with revision-interval live-after semantics; exact-head checks/review required | +| #657 | `64f48679` | Dashboard case-metric contract work; exact-head checks/review required | +| #644 | `d9ff9980` | native-surface code splitting with modal-focus regression coverage; exact-head checks/review required | +| #643 | `0a1f8ec1` | accessible status-notice surfaces; exact-head checks/review required | +| #640 | `2d50fa01` | operations-dashboard contract alignment; exact-head checks/review required | +| #639 | `aee02dca` | exact-head checks/review required | +| #632 | `a4059113` | active semantic provenance repair head; exact-head checks/review required | +| #631 | `1d9ac825` | documentation decomposition; branch is DIRTY against current main and requires conflict repair | +| #629 | `4b4d6707` | exact-head checks/review required | No row above is merge evidence. Immediately before any lifecycle action, re-fetch the head, unresolved threads, formal reviews, rulesets, and same-head diff --git a/docs/storybook-inventory.md b/docs/storybook-inventory.md index 3716656cd..7ec497477 100644 --- a/docs/storybook-inventory.md +++ b/docs/storybook-inventory.md @@ -15,6 +15,7 @@ operator-facing control you can click before changing product CSS. | `Lineage/LineageDag` | Open a reconstructed connection to read its inferred channel scores and Allen interval relation, or open the current branch node; 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` | | `Workspace/WorkspaceCalendar` | Read observed Naruon events, or open a commitment to land on that post. Fail-closed copy stays `이 범위의 일정을 아직 받을 수 없습니다`. | `--color-chip-border`, `WorkspaceCalendar`, `EvidenceStatusMark` | +| `Evidence/OntologyExplorer` | Distinguish Post, Person, Organization, and Team by shape and text, use the token-backed surface as a secondary cue, then open the exact-value table or cited evidence. Compare desktop, narrow, drawer, empty, truncated, denied, stale, and rejected states. | `--ontology-node-*-fill`, `OntologyExplorer` | 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/src/App.css b/frontend/src/App.css index eeaa61299..b4b65dfc8 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1148,11 +1148,30 @@ .ontology-node { cursor: pointer; - fill: var(--color-background); stroke: var(--color-text-heading); stroke-width: 1.5; } +.ontology-node-post { + fill: var(--ontology-node-post-fill); +} + +.ontology-node-person { + fill: var(--ontology-node-person-fill); +} + +.ontology-node-organization { + fill: var(--ontology-node-organization-fill); +} + +.ontology-node-team { + fill: var(--ontology-node-team-fill); +} + +.ontology-node-generic { + fill: var(--ontology-node-generic-fill); +} + .ontology-node text { fill: var(--color-text-heading); stroke: var(--color-background); @@ -1161,7 +1180,7 @@ font-size: 0.8rem; } -.ontology-node-type { +.ontology-node text.ontology-node-type { font-size: 0.7rem; fill: var(--color-text); } @@ -1207,7 +1226,17 @@ .ontology-exact-values { margin-top: 1rem; + max-width: 100%; overflow-x: auto; + overscroll-behavior-inline: contain; + scrollbar-gutter: stable; + -webkit-overflow-scrolling: touch; +} + +.ontology-exact-values:focus-visible { + outline: 2px solid var(--color-focus-border); + outline-offset: 2px; + border-radius: var(--radius-control); } .ontology-exact-values table { diff --git a/frontend/src/components/OntologyExplorer.test.tsx b/frontend/src/components/OntologyExplorer.test.tsx index 6c6dbb8e4..4ca22ac4f 100644 --- a/frontend/src/components/OntologyExplorer.test.tsx +++ b/frontend/src/components/OntologyExplorer.test.tsx @@ -236,6 +236,7 @@ describe("OntologyExplorer", () => { expect( screen.getByText(/This is an ontology neighborhood, not Event Lineage/), ).toBeInTheDocument(); + expect(screen.getByRole("region", { name: "Exact values" })).toHaveAttribute("tabindex", "0"); await userEvent.click(screen.getByRole("button", { name: "Select node: Post Demo public post" })); expect(screen.getByRole("heading", { name: "Demo public post" })).toBeInTheDocument(); await userEvent.click(screen.getByRole("button", { name: "Open evidence post" })); @@ -248,6 +249,23 @@ describe("OntologyExplorer", () => { expect(screen.queryByRole("img")).not.toBeInTheDocument(); }); + it("maps known ontology node types to token-backed visual classes", () => { + render( + , + ); + + expect(screen.getByRole("button", { name: "Select node: Post Demo public post" })) + .toHaveClass("ontology-node-post"); + expect(screen.getByRole("button", { name: "Select node: Person Test Person" })) + .toHaveClass("ontology-node-person"); + expect(screen.getByRole("button", { name: "Select node: Organization Demo Corp" })) + .toHaveClass("ontology-node-organization"); + }); + it("names empty, truncated, denied, and rejected next actions", () => { const { rerender } = render( = { node_team: "Team", }; +const NODE_TYPE_CLASS: Record = { + node_post: "ontology-node-post", + node_person: "ontology-node-person", + node_corporate_entity: "ontology-node-organization", + node_team: "ontology-node-team", +}; + const TRUTH_LABEL: Record = { truth_authoritative: "Authoritative", truth_observed: "Observed", @@ -418,7 +425,11 @@ function OntologyGraph({ {layout.nodes.map((node) => ( void; }) { return ( -
+

{t("Exact values")}

{payload.exact_value_rows.length === 0 ? (

{t("No visible ontology relations for this focus. Open a Keyman or affiliated organization next.")}

diff --git a/frontend/src/ontologyExplorerCss.test.ts b/frontend/src/ontologyExplorerCss.test.ts index ad35b8868..612df09a3 100644 --- a/frontend/src/ontologyExplorerCss.test.ts +++ b/frontend/src/ontologyExplorerCss.test.ts @@ -11,4 +11,8 @@ describe("Ontology Explorer CSS contracts", () => { it("defines the ontology node focus-visible rule exactly once", () => { expect(appCss.match(/\.ontology-node:focus-visible\s*\{/g)).toHaveLength(1); }); + + it("lets the node-type label override the shared node text color", () => { + expect(appCss).toContain(".ontology-node text.ontology-node-type {"); + }); }); diff --git a/frontend/src/styles/tokens.css b/frontend/src/styles/tokens.css index 400c686da..ef152eb51 100644 --- a/frontend/src/styles/tokens.css +++ b/frontend/src/styles/tokens.css @@ -104,6 +104,13 @@ --color-dashboard-positive: #426b45; --color-dashboard-surface: #f4f6fa; + /* Ontology node surfaces (ADR 0184: shape and text remain primary) */ + --ontology-node-post-fill: #e3f2fd; + --ontology-node-person-fill: #ede7f6; + --ontology-node-organization-fill: #fff3e0; + --ontology-node-team-fill: #e0f2f1; + --ontology-node-generic-fill: var(--color-background); + /* Layout & Breakpoint Tokens (§2.1 – 화면 해상도 / 반응형) */ --breakpoint-phone: 768px; --breakpoint-tablet: 1024px; @@ -209,6 +216,12 @@ --badge-status-prediction-bg: rgba(251, 146, 60, 0.2); --badge-status-prediction-text: #fdba74; + --ontology-node-post-fill: #173653; + --ontology-node-person-fill: #30234d; + --ontology-node-organization-fill: #4b2e1d; + --ontology-node-team-fill: #123f3b; + --ontology-node-generic-fill: var(--color-background); + --color-drawer-bg: #16171d; --color-drawer-border: #2e303a; --color-drawer-overlay: rgba(0, 0, 0, 0.65); diff --git a/frontend/src/styles/tokens.test.ts b/frontend/src/styles/tokens.test.ts index 74d987ccb..435c973ad 100644 --- a/frontend/src/styles/tokens.test.ts +++ b/frontend/src/styles/tokens.test.ts @@ -36,6 +36,14 @@ const BADGE_AND_ACCENT_TOKENS = [ "--badge-status-prediction-text", ]; +const ONTOLOGY_NODE_TOKENS = [ + "--ontology-node-post-fill", + "--ontology-node-person-fill", + "--ontology-node-organization-fill", + "--ontology-node-team-fill", + "--ontology-node-generic-fill", +]; + // Colors this file's dark-mode block replaced -- a regression here would // silently make dark mode as bright as light mode again (ADR 0099). const RETIRED_LIGHT_ONLY_HEX = [ @@ -55,7 +63,10 @@ const RETIRED_LIGHT_ONLY_HEX = [ // WCAG 2.x relative luminance / contrast ratio (SC 1.4.3). function relativeLuminance(hex: string): number { - const rgb = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255); + const normalized = hex.length === 4 + ? `#${[...hex.slice(1)].map((digit) => `${digit}${digit}`).join("")}` + : hex; + const rgb = [1, 3, 5].map((i) => parseInt(normalized.slice(i, i + 2), 16) / 255); const [r, g, b] = rgb.map((c) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4)); return 0.2126 * r + 0.7152 * g + 0.0722 * b; } @@ -76,7 +87,7 @@ function readToken(block: string, token: string): string { // literal passed by the call sites below (e.g. "--color-footer-bg"), and // escapeRegExp() neutralizes any regex metacharacters before // interpolation, so no caller-controlled input reaches RegExp unescaped. - const match = block.match(new RegExp(`${escapeRegExp(token)}:\\s*(#[0-9a-fA-F]{6})`)); // nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp + const match = block.match(new RegExp(`${escapeRegExp(token)}:\\s*(#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3})(?![0-9a-fA-F])`)); // nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp if (!match) throw new Error(`${token} not found as a hex value`); return match[1]; } @@ -125,6 +136,26 @@ describe("design tokens", () => { } }); + it("defines and consumes ontology node fills in both color schemes", () => { + for (const token of ONTOLOGY_NODE_TOKENS) { + expect(lightBlock, `${token} missing from the light :root block`).toContain(`${token}:`); + expect(darkBlock, `${token} missing from the dark prefers-color-scheme block`).toContain( + `${token}:`, + ); + expect(appCss, `App.css never references var(${token})`).toContain(`var(${token})`); + } + }); + + it("keeps ontology labels and their halo at WCAG AA contrast in both color schemes", () => { + for (const block of [lightBlock, darkBlock]) { + const background = readToken(block, "--color-background"); + for (const token of ["--color-text-heading", "--color-text"]) { + expect(contrastRatio(readToken(block, token), background), `${token} label contrast`) + .toBeGreaterThanOrEqual(4.5); + } + } + }); + it("does not dim .post-meta with opacity -- it fails WCAG AA in both themes and carries role=\"status\" next-action text", () => { const match = appCss.match(/\.post-meta\s*\{([^}]*)\}/); expect(match, ".post-meta rule not found in App.css").not.toBeNull();