From 794deddf21bcd27a3dff2cd3b54a5fac4d35e741 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 27 Sep 2026 02:39:59 +0900 Subject: [PATCH 1/8] fix(ontology): keep searched voice exports bound to visible evidence --- docs/product-technical-gap-baseline.md | 32 ++++++++ docs/storybook-inventory.md | 2 +- .../components/OntologyExplorer.stories.tsx | 27 +++++++ frontend/src/ontologyLayout.test.ts | 73 +++++++++++++++++++ frontend/src/ontologyLayout.ts | 31 +++++--- 5 files changed, 155 insertions(+), 10 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index b5d31877b..f8c3f828a 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,5 +1,37 @@ # Product & Technical Gap Baseline +> Current authority overlay, 2026-09-27 KST. Remote protected `main` is +> `83eba56149eb802cd63642c507c324c9976ec78e` (verified with +> `git ls-remote`). The live open queue contains 168 PRs, all Draft, and +> 42 open issues. None of those PRs is a protected-main product claim or +> eligible for normal merge while Draft. For example, #1128 is at +> `91143146623948dbd26bbfc1c69de3cd77d2ae06`, #1126 at +> `90766d739b980d03d1f552356bcd3905846fc6b1`, and #1123 at +> `fb3dba7e6b8145603389d211a19dbe70280bdea6`; their current-head +> checks include failed CodeQL compatibility jobs and they lack independent +> approval. A successful bot review or an older head's checks do not close +> either gate. Parent PRs must reach protected `main` before stacked children +> are retargeted and rechecked. Older overlays below are dated history. +> +> Authority and gap selected for this slice: the current PRD requires +> authorized evidence for every additional Voice and parity across the graph, +> exact-value CSV, and JSON-LD. ADR 0246 governs the twelve extensible atomic +> Voices; ADR 0256 governs evidence-bearing composition and forbids replacing +> missing evidence with the carrying Post. Current `main` filters graph nodes +> during in-page search but retained a derived Voice's CSV row and JSON-LD +> relationship when that search removed its separate evidence Post. This +> candidate removes that assignment from the searched view and export and +> preserves it when the evidence Post remains visible. The synthetic frontend +> regression is local candidate evidence only. Authenticated PostgreSQL API, +> desktop/mobile render, and protected-main acceptance remain unverified. +> This slice changes no ADR, API, schema, migration ordinal, or release number. +> Its baseline path overlaps draft #1123, and its Storybook inventory path +> overlaps draft #1126; reconcile those exact paths before either later merge. +> Draft #1121 changes the Voice-history ADR and documentation tests, while +> #997 changes the occupational ADR and PRD. Their policy/requirement changes +> are not inherited by this frontend candidate or treated as protected-main +> authority. Each stack still needs a fresh conflict and exact-head audit. + > Exact-head loop overlay: 2026-08-29 13:20 KST. Protected `main` is > `fc13acaa20adca11968238e398d4aafcf62b6cee` (v2.23.0 leftover-map > explained leftover share, #775). Open ready PRs still lack independent diff --git a/docs/storybook-inventory.md b/docs/storybook-inventory.md index f426285a6..6d549f8c3 100644 --- a/docs/storybook-inventory.md +++ b/docs/storybook-inventory.md @@ -13,7 +13,7 @@ operator-facing control you can click before changing product CSS. | `Post/Connect perspective` | Choose one unassigned Voice and an explicit evidence state, then record the open post as its evidence. `Ready`, `Completed`, and `NarrowViewport` cover untouched, successful, and mobile states. | `VoiceAssignmentForm`, `admin-form`, `btn-primary` | | `Evidence/CitationChip` | Click a cited title to open that source post. | `--color-chip-border`, `--radius-chip`, `CitationChip` | | `Evidence/OrganizationAliasChip` | Click a cataloged org; the parenthetical is the unique corroborated SKOS companion. | `--color-chip-border`, `--radius-chip`, `OrganizationAliasChip` | -| `Evidence/OntologyExplorer` | Distinguish Event Lineage from typed ontology facts, inspect Post/Person/Organization/Team/Project shapes, token-backed secondary cues, and truth labels, then open authorized evidence. The named exact-values region supports keyboard scrolling; `LongLabelsAndEvidenceTable` proves complete labels wrap without character-count truncation, while `CombinedVoiceEvidence` covers primary-plus-additional Voice assignments and focuses the evidence action distinct from the carrying-Post action. Desktop, narrow, drawers, legend/filter, empty, truncated, partial, denied, stale, and rejected scenes cover ADR 0184/0222/0251 states. | `OntologyExplorer`, `ontologyLayout`, `--ontology-node-*-fill`, `--color-table-border` | +| `Evidence/OntologyExplorer` | Distinguish Event Lineage from typed ontology facts, inspect Post/Person/Organization/Team/Project shapes, token-backed secondary cues, and truth labels, then open authorized evidence. The named exact-values region supports keyboard scrolling; `LongLabelsAndEvidenceTable` proves complete labels wrap without character-count truncation, while `CombinedVoiceEvidence` covers primary-plus-additional Voice assignments and focuses the evidence action distinct from the carrying-Post action. `SeparateVoiceEvidence` shows a different evidence Post so filtering the scene can verify that its derived Voice disappears from the searched view and export when that Post is absent. Desktop, narrow, drawers, legend/filter, empty, truncated, partial, denied, stale, and rejected scenes cover ADR 0184/0222/0251 states. | `OntologyExplorer`, `ontologyLayout`, `--ontology-node-*-fill`, `--color-table-border` | | `Evidence/OntologyExplorer` | Distinguish Event Lineage from typed ontology facts, inspect Post/Person/Organization/Team/Project/Work-evidence shapes, token-backed secondary cues, and truth labels, then open authorized evidence. The populated scene includes one assertion-backed occupational construct without a person-trait promotion. The named exact-values region supports keyboard scrolling; `LongLabelsAndEvidenceTable` proves complete labels wrap without character-count truncation. Desktop, narrow, drawers, legend/filter, empty, truncated, partial, denied, stale, and rejected scenes cover ADR 0184/0222/0255 states. | `OntologyExplorer`, `ontologyLayout`, `--ontology-node-*-fill`, `--color-table-border` | | `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` | diff --git a/frontend/src/components/OntologyExplorer.stories.tsx b/frontend/src/components/OntologyExplorer.stories.tsx index 5e192ff0f..bc8de76c2 100644 --- a/frontend/src/components/OntologyExplorer.stories.tsx +++ b/frontend/src/components/OntologyExplorer.stories.tsx @@ -3,6 +3,7 @@ import type { OntologyNeighborhoodPayload } from "../api"; import { OntologyExplorer } from "./OntologyExplorer"; const POST_ID = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa1"; +const VOICE_EVIDENCE_POST_ID = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa2"; const PERSON_ID = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbb1"; const CORP_ID = "cccccccc-cccc-cccc-cccc-ccccccccccc1"; const PROJECT_ID = `${POST_ID}/demo-project`; @@ -331,6 +332,32 @@ export const CombinedVoiceEvidence: Story = { }, }; +export const SeparateVoiceEvidence: Story = { + args: { + neighborhood: { + ...combinedVoiceNeighborhood, + nodes: [ + ...combinedVoiceNeighborhood.nodes, + { + ...combinedVoiceNeighborhood.nodes[0], + node_id: VOICE_EVIDENCE_POST_ID, + display_label: "Synthetic evidence post", + }, + ], + voice_assignments: combinedVoiceNeighborhood.voice_assignments?.map((assignment) => + assignment.is_primary + ? assignment + : { ...assignment, evidence_post_id: VOICE_EVIDENCE_POST_ID }, + ), + exact_value_rows: combinedVoiceNeighborhood.exact_value_rows.map((row) => + row.edge_id.endsWith(":vops") + ? { ...row, evidence_post_id: VOICE_EVIDENCE_POST_ID } + : row, + ), + }, + }, +}; + export const LongLabelsAndEvidenceTable: Story = { args: { neighborhood: { diff --git a/frontend/src/ontologyLayout.test.ts b/frontend/src/ontologyLayout.test.ts index b609e7e2b..9e996a954 100644 --- a/frontend/src/ontologyLayout.test.ts +++ b/frontend/src/ontologyLayout.test.ts @@ -159,6 +159,79 @@ describe("ontologyLayout", () => { expect(accumulateNeighborhoodPages(source, withVoice).voice_assignments).toEqual([assignment]); }); + it("omits derived voices when a search hides their evidence post", () => { + const source = payload(); + const evidenceId = "dddddddd-dddd-dddd-dddd-ddddddddddd1"; + const assignmentIri = `${ONTOLOGY_NAMESPACE}voice-assignment/${POST_ID}/vops`; + const primaryIri = `${ONTOLOGY_NAMESPACE}voice-assignment/${POST_ID}/voc`; + const postIri = `${ONTOLOGY_NAMESPACE}node/node_post/${POST_ID}`; + const evidenceIri = `${ONTOLOGY_NAMESPACE}node/node_post/${evidenceId}`; + const relation = `${ONTOLOGY_NAMESPACE}hasVoiceAssignment`; + const assignment = { + post_id: POST_ID, + voice_type_code: "vops", + voice_type_iri: `${ONTOLOGY_NAMESPACE}voiceOfProcessType`, + voice_type_label: "Voice of Process", + is_primary: false, + truth_status_code: "truth_observed", + recorded_at: "2026-01-10T12:00:00+00:00", + provenance_reference: "Evidence-backed additional voice", + evidence_post_id: evidenceId, + }; + const primary = { + ...assignment, + voice_type_code: "voc", + voice_type_iri: `${ONTOLOGY_NAMESPACE}voiceOfCustomerType`, + voice_type_label: "Voice of Customer", + is_primary: true, + evidence_post_id: null, + }; + const row = { + ...source.exact_value_rows[0], + edge_id: `voice-assignment:${POST_ID}:vops`, + property_code: "hasVoiceAssignment", + evidence_post_id: evidenceId, + }; + const withVoice = { + ...source, + nodes: [source.nodes[0], { ...source.nodes[0], node_id: evidenceId, display_label: "Evidence source" }], + edges: [], + exact_value_rows: [row, { ...row, edge_id: `voice-assignment:${POST_ID}:voc`, evidence_post_id: POST_ID }], + voice_assignments: [assignment, primary], + jsonld: { "@graph": [ + { "@id": postIri, "rdfs:label": "Carrying post", [relation]: [ + { "@id": assignmentIri }, { "@id": primaryIri }, + ] }, + { "@id": evidenceIri }, + { "@id": assignmentIri, "prov:wasDerivedFrom": { "@id": evidenceIri } }, + { "@id": primaryIri, "prov:wasDerivedFrom": { "@id": postIri } }, + { "@id": assignment.voice_type_iri }, + { "@id": primary.voice_type_iri }, + ] }, + } satisfies OntologyNeighborhoodPayload; + + const hidden = filterNeighborhood(withVoice, "no match")!; + expect(hidden.voice_assignments).toEqual([primary]); + expect(hidden.exact_value_rows.map((value) => value.edge_id)).toEqual([ + `voice-assignment:${POST_ID}:voc`, + ]); + expect(hidden.jsonld["@graph"]).toEqual([ + { "@id": postIri, "rdfs:label": "Carrying post", [relation]: [{ "@id": primaryIri }] }, + { "@id": primaryIri, "prov:wasDerivedFrom": { "@id": postIri } }, + { "@id": primary.voice_type_iri }, + ]); + + const shown = filterNeighborhood(withVoice, "Evidence source")!; + expect(shown.voice_assignments).toEqual([assignment, primary]); + expect(shown.exact_value_rows.map((value) => value.edge_id)).toEqual([ + row.edge_id, `voice-assignment:${POST_ID}:voc`, + ]); + expect((shown.jsonld["@graph"] as Array>)[0][relation]).toEqual([ + { "@id": assignmentIri }, + { "@id": primaryIri }, + ]); + }); + it("merges JSON-LD properties and multi-value relations for one paged subject", () => { const source = payload(); const postIri = `${ONTOLOGY_NAMESPACE}node/node_post/${POST_ID}`; diff --git a/frontend/src/ontologyLayout.ts b/frontend/src/ontologyLayout.ts index dc49de443..0846db75e 100644 --- a/frontend/src/ontologyLayout.ts +++ b/frontend/src/ontologyLayout.ts @@ -29,6 +29,7 @@ const ROW_GAP = 128; const LEFT = ONTOLOGY_NODE_LABEL_WIDTH / 2 + 20; const TOP = 48; const ONTOLOGY_NAMESPACE = "https://contextualwisdomlab.github.io/LineageWeave/ontology#"; +const HAS_VOICE_ASSIGNMENT = `${ONTOLOGY_NAMESPACE}hasVoiceAssignment`; function nodeKey(nodeTypeCode: string, nodeId: string): string { return `${nodeTypeCode}:${nodeId}`; @@ -202,13 +203,19 @@ export function filterNeighborhood( if (nodeMatch(node)) keep.add(nodeKey(node.node_type_code, node.node_id)); } const nodes = payload.nodes.filter((node) => keep.has(nodeKey(node.node_type_code, node.node_id))); + const visibleVoiceAssignments = (payload.voice_assignments ?? []).filter((assignment) => + keep.has(nodeKey("node_post", assignment.post_id)) && + (assignment.is_primary || + (assignment.evidence_post_id !== null && + assignment.evidence_post_id !== undefined && + keep.has(nodeKey("node_post", assignment.evidence_post_id)))), + ); + const visibleVoiceRowIds = new Set(visibleVoiceAssignments.map( + (assignment) => `voice-assignment:${assignment.post_id}:${assignment.voice_type_code}`, + )); const exact_value_rows = payload.exact_value_rows.filter((row) => edges.some((edge) => edge.edge_id === row.edge_id) || - (payload.voice_assignments ?? []).some( - (assignment) => - row.edge_id === `voice-assignment:${assignment.post_id}:${assignment.voice_type_code}` && - keep.has(nodeKey("node_post", assignment.post_id)), - ), + visibleVoiceRowIds.has(row.edge_id), ); const visibleIds = new Set([ ...nodes.map((node) => `lw:node/${node.node_type_code}/${node.node_id}`), @@ -217,9 +224,6 @@ export function filterNeighborhood( const visibleNodeIds = new Set(nodes.map( (node) => ontologyNodeId(node.node_type_code, node.node_id), )); - const visibleVoiceAssignments = (payload.voice_assignments ?? []).filter((assignment) => - keep.has(nodeKey("node_post", assignment.post_id)), - ); const visibleVoiceIds = new Set( visibleVoiceAssignments.flatMap((assignment) => [ assignment.voice_type_iri, @@ -237,7 +241,16 @@ export function filterNeighborhood( (visibleIds.has(item["@id"]) || visibleNodeIds.has(item["@id"]) || visibleVoiceIds.has(item["@id"])), - ), + ).map((item) => { + if (!Array.isArray(item[HAS_VOICE_ASSIGNMENT])) return item; + const relations = (item[HAS_VOICE_ASSIGNMENT] as unknown[]).filter( + (relation) => typeof relation === "object" && relation !== null && + "@id" in relation && typeof relation["@id"] === "string" && + visibleVoiceIds.has(relation["@id"]), + ); + const { [HAS_VOICE_ASSIGNMENT]: _omitted, ...other } = item; + return relations.length ? { ...other, [HAS_VOICE_ASSIGNMENT]: relations } : other; + }), } : payload.jsonld; return { From 6b301c26cd6e0dbe09f6be7a45a5bcb810e8f02b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 27 Sep 2026 03:52:46 +0900 Subject: [PATCH 2/8] docs(gaps): correct current PR queue status --- docs/product-technical-gap-baseline.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index f8c3f828a..edcf25bb7 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -2,9 +2,13 @@ > Current authority overlay, 2026-09-27 KST. Remote protected `main` is > `83eba56149eb802cd63642c507c324c9976ec78e` (verified with -> `git ls-remote`). The live open queue contains 168 PRs, all Draft, and -> 42 open issues. None of those PRs is a protected-main product claim or -> eligible for normal merge while Draft. For example, #1128 is at +> `git ls-remote`). The live open queue contains 169 PRs and 42 open +> issues. #1129 is ready for review with normal squash auto-merge armed; +> its exact head is `794deddf21bcd27a3dff2cd3b54a5fac4d35e741` before +> this documentation correction. Its required checks are queued and it lacks +> independent approval, so no protected-main delivery is claimed. The other +> 168 PRs are Draft and ineligible for normal merge while Draft. For +> example, #1128 is at > `91143146623948dbd26bbfc1c69de3cd77d2ae06`, #1126 at > `90766d739b980d03d1f552356bcd3905846fc6b1`, and #1123 at > `fb3dba7e6b8145603389d211a19dbe70280bdea6`; their current-head From 4b2637cfbb427b64c1635cd4a1b6dfc6296fa63b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 27 Sep 2026 03:58:40 +0900 Subject: [PATCH 3/8] docs(gaps): record rendered Voice evidence audit --- docs/product-technical-gap-baseline.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index edcf25bb7..b5cdda706 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -3,10 +3,9 @@ > Current authority overlay, 2026-09-27 KST. Remote protected `main` is > `83eba56149eb802cd63642c507c324c9976ec78e` (verified with > `git ls-remote`). The live open queue contains 169 PRs and 42 open -> issues. #1129 is ready for review with normal squash auto-merge armed; -> its exact head is `794deddf21bcd27a3dff2cd3b54a5fac4d35e741` before -> this documentation correction. Its required checks are queued and it lacks -> independent approval, so no protected-main delivery is claimed. The other +> issues. #1129 is ready for review with normal squash auto-merge armed. +> Its current-head required checks are queued and it lacks independent +> approval, so no protected-main delivery is claimed. The other > 168 PRs are Draft and ineligible for normal merge while Draft. For > example, #1128 is at > `91143146623948dbd26bbfc1c69de3cd77d2ae06`, #1126 at @@ -26,8 +25,12 @@ > relationship when that search removed its separate evidence Post. This > candidate removes that assignment from the searched view and export and > preserves it when the evidence Post remains visible. The synthetic frontend -> regression is local candidate evidence only. Authenticated PostgreSQL API, -> desktop/mobile render, and protected-main acceptance remain unverified. +> regression is local candidate evidence only. The `SeparateVoiceEvidence` +> Storybook scene was rendered at 1440×900 and 390×844 CSS pixels on +> 2026-09-27: filtering out its separate evidence Post retained the primary +> row and removed the derived row. At 390 pixels, the document fit the +> viewport and the named exact-values region remained horizontally scrollable. +> Authenticated PostgreSQL API and protected-main acceptance remain unverified. > This slice changes no ADR, API, schema, migration ordinal, or release number. > Its baseline path overlaps draft #1123, and its Storybook inventory path > overlaps draft #1126; reconcile those exact paths before either later merge. From 0eea5f5fe40481ec0555058880087de2fa8a64b8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 27 Sep 2026 03:59:34 +0900 Subject: [PATCH 4/8] test(storybook): include visible Voice JSON-LD evidence --- .../components/OntologyExplorer.stories.tsx | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/frontend/src/components/OntologyExplorer.stories.tsx b/frontend/src/components/OntologyExplorer.stories.tsx index bc8de76c2..6824c67f6 100644 --- a/frontend/src/components/OntologyExplorer.stories.tsx +++ b/frontend/src/components/OntologyExplorer.stories.tsx @@ -8,6 +8,39 @@ const PERSON_ID = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbb1"; const CORP_ID = "cccccccc-cccc-cccc-cccc-ccccccccccc1"; const PROJECT_ID = `${POST_ID}/demo-project`; const CONSTRUCT_ID = "99999999-9999-9999-9999-999999999999"; +const ONTOLOGY_NAMESPACE = "https://contextualwisdomlab.github.io/LineageWeave/ontology#"; +const HAS_VOICE_ASSIGNMENT = `${ONTOLOGY_NAMESPACE}hasVoiceAssignment`; +const PRIMARY_VOICE_IRI = `${ONTOLOGY_NAMESPACE}voice-assignment/${POST_ID}/voc`; +const DERIVED_VOICE_IRI = `${ONTOLOGY_NAMESPACE}voice-assignment/${POST_ID}/vops`; + +function voiceJsonLd(evidencePostId: string): OntologyNeighborhoodPayload["jsonld"] { + const postIri = `${ONTOLOGY_NAMESPACE}node/node_post/${POST_ID}`; + const evidenceIri = `${ONTOLOGY_NAMESPACE}node/node_post/${evidencePostId}`; + + return { + "@context": demoNeighborhood.jsonld["@context"], + "@graph": [ + { + "@id": postIri, + [HAS_VOICE_ASSIGNMENT]: [ + { "@id": PRIMARY_VOICE_IRI }, + { "@id": DERIVED_VOICE_IRI }, + ], + }, + ...(evidencePostId === POST_ID ? [] : [{ "@id": evidenceIri }]), + { + "@id": PRIMARY_VOICE_IRI, + "prov:wasDerivedFrom": { "@id": postIri }, + }, + { + "@id": DERIVED_VOICE_IRI, + "prov:wasDerivedFrom": { "@id": evidenceIri }, + }, + { "@id": `${ONTOLOGY_NAMESPACE}voiceOfCustomerType` }, + { "@id": `${ONTOLOGY_NAMESPACE}voiceOfProcessType` }, + ], + }; +} const demoNeighborhood: OntologyNeighborhoodPayload = { focus_node_id: POST_ID, @@ -303,6 +336,7 @@ const combinedVoiceNeighborhood: OntologyNeighborhoodPayload = { evidence_post_id: POST_ID, })), ], + jsonld: voiceJsonLd(POST_ID), }; const meta = { @@ -354,6 +388,7 @@ export const SeparateVoiceEvidence: Story = { ? { ...row, evidence_post_id: VOICE_EVIDENCE_POST_ID } : row, ), + jsonld: voiceJsonLd(VOICE_EVIDENCE_POST_ID), }, }, }; From e79324be411a39a3cfaa8cbfd8501ed8a9215418 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 27 Sep 2026 04:00:05 +0900 Subject: [PATCH 5/8] docs(design): record Voice fixture and current admission gaps --- docs/product-technical-gap-baseline.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index b5cdda706..af8e5b56e 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -3,10 +3,12 @@ > Current authority overlay, 2026-09-27 KST. Remote protected `main` is > `83eba56149eb802cd63642c507c324c9976ec78e` (verified with > `git ls-remote`). The live open queue contains 169 PRs and 42 open -> issues. #1129 is ready for review with normal squash auto-merge armed. -> Its current-head required checks are queued and it lacks independent -> approval, so no protected-main delivery is claimed. The other -> 168 PRs are Draft and ineligible for normal merge while Draft. For +> issues. #1129 exact `0eea5f5fe40481ec0555058880087de2fa8a64b8` +> is Draft/Proposed after current-head review found that the Storybook fixture +> had an empty JSON-LD graph. The fixture now carries primary and derived Voice +> relationships plus separate evidence identity, but required checks are queued, +> the Tests workflow is skipped, and independent approval is absent. All 169 +> open PRs are therefore Draft and ineligible for normal merge while Draft. For > example, #1128 is at > `91143146623948dbd26bbfc1c69de3cd77d2ae06`, #1126 at > `90766d739b980d03d1f552356bcd3905846fc6b1`, and #1123 at @@ -26,10 +28,11 @@ > candidate removes that assignment from the searched view and export and > preserves it when the evidence Post remains visible. The synthetic frontend > regression is local candidate evidence only. The `SeparateVoiceEvidence` -> Storybook scene was rendered at 1440×900 and 390×844 CSS pixels on -> 2026-09-27: filtering out its separate evidence Post retained the primary -> row and removed the derived row. At 390 pixels, the document fit the -> viewport and the named exact-values region remained horizontally scrollable. +> fixture now includes exportable JSON-LD for the primary and derived Voice +> relationships and binds the derived relation to its distinct evidence Post. +> The prior 1440×900 and 390×844 screenshot statement is not current-head +> executable evidence: no browser artifact, interaction trace, Firefox/WebKit, +> assistive-technology, or eight-locale result is bound to this exact head. > Authenticated PostgreSQL API and protected-main acceptance remain unverified. > This slice changes no ADR, API, schema, migration ordinal, or release number. > Its baseline path overlaps draft #1123, and its Storybook inventory path From 4bcbc9195c81e68fcf15173ffe6975b872b974a4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 27 Sep 2026 05:13:26 +0900 Subject: [PATCH 6/8] docs(gaps): refresh Voice review evidence --- docs/product-technical-gap-baseline.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index af8e5b56e..e6e6d6da9 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -2,13 +2,15 @@ > Current authority overlay, 2026-09-27 KST. Remote protected `main` is > `83eba56149eb802cd63642c507c324c9976ec78e` (verified with -> `git ls-remote`). The live open queue contains 169 PRs and 42 open -> issues. #1129 exact `0eea5f5fe40481ec0555058880087de2fa8a64b8` +> `git ls-remote`). The earlier queue snapshot counted 169 PRs and 42 open +> issues; a fresh total is unavailable after GitHub returned HTTP 403 rate +> limit on paginated reads. #1129 was inspected at exact head +> `e79324be411a39a3cfaa8cbfd8501ed8a9215418`, which > is Draft/Proposed after current-head review found that the Storybook fixture > had an empty JSON-LD graph. The fixture now carries primary and derived Voice > relationships plus separate evidence identity, but required checks are queued, -> the Tests workflow is skipped, and independent approval is absent. All 169 -> open PRs are therefore Draft and ineligible for normal merge while Draft. For +> the Tests workflow is skipped, and independent approval is absent. This PR +> remains ineligible for normal merge while Draft. For > example, #1128 is at > `91143146623948dbd26bbfc1c69de3cd77d2ae06`, #1126 at > `90766d739b980d03d1f552356bcd3905846fc6b1`, and #1123 at @@ -30,8 +32,9 @@ > regression is local candidate evidence only. The `SeparateVoiceEvidence` > fixture now includes exportable JSON-LD for the primary and derived Voice > relationships and binds the derived relation to its distinct evidence Post. -> The prior 1440×900 and 390×844 screenshot statement is not current-head -> executable evidence: no browser artifact, interaction trace, Firefox/WebKit, +> A local Storybook build at this head rendered the separate-evidence scene at +> 1440×900 and 390×844; screenshots were visually checked in the isolated +> worktree. No authenticated browser interaction, Firefox/WebKit, > assistive-technology, or eight-locale result is bound to this exact head. > Authenticated PostgreSQL API and protected-main acceptance remain unverified. > This slice changes no ADR, API, schema, migration ordinal, or release number. From d17b893fa19105a986ea61e6348b43d7ad6e07a8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 27 Sep 2026 06:35:42 +0900 Subject: [PATCH 7/8] docs(gaps): refresh Voice PR exact-head review snapshot --- docs/product-technical-gap-baseline.md | 27 +++++++++++++------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index e6e6d6da9..0ef1e67f2 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -2,21 +2,20 @@ > Current authority overlay, 2026-09-27 KST. Remote protected `main` is > `83eba56149eb802cd63642c507c324c9976ec78e` (verified with -> `git ls-remote`). The earlier queue snapshot counted 169 PRs and 42 open -> issues; a fresh total is unavailable after GitHub returned HTTP 403 rate -> limit on paginated reads. #1129 was inspected at exact head -> `e79324be411a39a3cfaa8cbfd8501ed8a9215418`, which -> is Draft/Proposed after current-head review found that the Storybook fixture -> had an empty JSON-LD graph. The fixture now carries primary and derived Voice -> relationships plus separate evidence identity, but required checks are queued, -> the Tests workflow is skipped, and independent approval is absent. This PR -> remains ineligible for normal merge while Draft. For -> example, #1128 is at +> `git ls-remote`). REST pagination returned 169 open PRs and 28 open issues. +> #1129 was reviewed at exact head +> `4bcbc9195c81e68fcf15173ffe6975b872b974a4`. It is Ready with normal +> squash auto-merge enabled; current-head checks remain queued and no +> independent APPROVE exists. Its Storybook fixture now carries primary and +> derived Voice relationships plus separate evidence identity. The exact head +> of this document update must be re-fetched from the PR before a merge +> decision; this observation names the reviewed parent commit. #1128 is at > `91143146623948dbd26bbfc1c69de3cd77d2ae06`, #1126 at -> `90766d739b980d03d1f552356bcd3905846fc6b1`, and #1123 at -> `fb3dba7e6b8145603389d211a19dbe70280bdea6`; their current-head -> checks include failed CodeQL compatibility jobs and they lack independent -> approval. A successful bot review or an older head's checks do not close +> `c0c5204b702d2d4d24928389db7d04ebe5cb9739`, and #1123 at +> `fb3dba7e6b8145603389d211a19dbe70280bdea6`. #1128 and #1126 are +> Ready with squash auto-merge enabled; #1123 remains Draft. None has a +> current-head independent APPROVE. A successful bot review or an older +> head's checks do not close > either gate. Parent PRs must reach protected `main` before stacked children > are retargeted and rechecked. Older overlays below are dated history. > From 6e624d6178eefbad8c0f3ab1bf5b273138dbee18 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 27 Sep 2026 09:04:45 +0900 Subject: [PATCH 8/8] fix(ontology): discard dangling evidence edges in searched exports --- docs/product-technical-gap-baseline.md | 5 ++++- frontend/src/ontologyLayout.test.ts | 22 ++++++++++++++++++++++ frontend/src/ontologyLayout.ts | 3 ++- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 0ef1e67f2..e97255385 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -28,7 +28,10 @@ > relationship when that search removed its separate evidence Post. This > candidate removes that assignment from the searched view and export and > preserves it when the evidence Post remains visible. The synthetic frontend -> regression is local candidate evidence only. The `SeparateVoiceEvidence` +> regression is local candidate evidence only. A second regression found that +> a matching graph edge could retain an evidence Post identifier absent from +> the authorized node set; the searched view now drops that dangling edge +> before deriving Voice visibility or exports. The `SeparateVoiceEvidence` > fixture now includes exportable JSON-LD for the primary and derived Voice > relationships and binds the derived relation to its distinct evidence Post. > A local Storybook build at this head rendered the separate-evidence scene at diff --git a/frontend/src/ontologyLayout.test.ts b/frontend/src/ontologyLayout.test.ts index 9e996a954..62e59846e 100644 --- a/frontend/src/ontologyLayout.test.ts +++ b/frontend/src/ontologyLayout.test.ts @@ -230,6 +230,28 @@ describe("ontologyLayout", () => { { "@id": assignmentIri }, { "@id": primaryIri }, ]); + + const dangling = filterNeighborhood({ + ...withVoice, + nodes: [source.nodes[0]], + edges: [{ + ...source.edges[0], + edge_id: "dangling-evidence-reference", + source_node_type_code: "node_post", + source_node_id: POST_ID, + target_node_type_code: "node_post", + target_node_id: evidenceId, + property_label: "related evidence", + }], + }, "related evidence")!; + expect(dangling.edges).toEqual([]); + expect(dangling.voice_assignments).toEqual([primary]); + expect(dangling.exact_value_rows.map((value) => value.edge_id)).toEqual([ + `voice-assignment:${POST_ID}:voc`, + ]); + expect((dangling.jsonld["@graph"] as Array>)[0][relation]).toEqual([ + { "@id": primaryIri }, + ]); }); it("merges JSON-LD properties and multi-value relations for one paged subject", () => { diff --git a/frontend/src/ontologyLayout.ts b/frontend/src/ontologyLayout.ts index 0846db75e..8b3235b0b 100644 --- a/frontend/src/ontologyLayout.ts +++ b/frontend/src/ontologyLayout.ts @@ -192,7 +192,8 @@ export function filterNeighborhood( const edges = payload.edges.filter((edge) => { const source = nodesByKey.get(nodeKey(edge.source_node_type_code, edge.source_node_id)); const target = nodesByKey.get(nodeKey(edge.target_node_type_code, edge.target_node_id)); - return edgeMatch(edge) || Boolean(source && nodeMatch(source)) || Boolean(target && nodeMatch(target)); + if (!source || !target) return false; + return edgeMatch(edge) || nodeMatch(source) || nodeMatch(target); }); const keep = new Set([nodeKey(payload.focus_node_type_code, payload.focus_node_id)]); for (const edge of edges) {