fix(node-ui): WM Assertions tab surfaces sub-graph assertions (#706)#710
Merged
Conversation
The WM Assertions tab silently dropped sub-graph-scoped assertions
because the client-side filter only matched the root URI shape:
`did:dkg:context-graph:<cg>/assertion/<agent>/<name>`. Sub-graph
WM assertions live at
`did:dkg:context-graph:<cg>/<sg>/assertion/<agent>/<name>` (sub-graph
segment between `<cg>/` and `/assertion/`), and `startsWith('…/assertion/')`
rejected them outright.
Sibling fix to PR #675 (SWM Assertions tab) shipped earlier; this
brings WM to parity. Both tabs now uniformly surface root + sub-graph
assertions with an inline `subGraph` field on each `AssertionInfo`.
api.ts:
- `AssertionInfo.subGraph?: string` — undefined for root, slug for
sub-graph-scoped. Uniformly populated on both WM and SWM branches.
- WM filter switched to `startsWith(cgPrefix) + includes('/assertion/')`
to mirror the daemon's `wmSparql` discriminator. Sub-graph slug
recovered via `subGraphFromAssertionGraphUri` (the same parser
the lifecycle hook uses; defensive on malformed input).
- SWM branch surfaces its already-computed `subGraphName` on the
returned `AssertionInfo` — no behaviour change, just uniformity.
components.tsx (`AssertionsList`):
- Inline sub-graph chip per row, locked spec: glyph + slug, mirror
`.v10-item-count` styling exactly (10px / tertiary / mono / no
border / no background), middle-ellipsis truncation at 18 chars,
full slug in tooltip. Position: after `.v10-item-count`, separated
by the existing meta-row gap.
- Glyph chosen: `🗂` (reads as containment — right semantic for
sub-graph membership; `📑` would read as "multiple pages" which
misleads).
- Chip is non-clickable in this PR per the locked plan; tooltip
reads "In sub-graph: <full-slug>". Click-to-navigate lands with S3.
- `truncateMiddle(s, max)` helper added near the component; the
prefix-weighted split preserves common namespace prefixes
(`epcis-*`, `github-*`) in full on typical slug widths.
Tests:
- `context-graph-empty-stat-components.test.ts` — new case pinning
the three states: root row → no chip; short slug → verbatim
chip text + tooltip with full slug; long slug → middle-ellipsis
applied, full slug preserved in tooltip. All three rows render
(no silent drop on sub-graph rows).
URI parsing pinned by `subGraphFromAssertionGraphUri`'s existing
cases in `use-assertion-lifecycle-events.test.ts`; not duplicated.
User visual review on `84a2fdbab` flagged the `🗂` (FOLDER) glyph rendering full-colour on Windows despite `--text-tertiary` on the chip — colour-emoji fonts override the CSS colour, so the chip shouted vs the rest of the meta-row. ux-lead pick: `›` (U+203A, single right-pointing angle quotation mark). Monochrome Unicode (dodges the color-emoji caveat), inherits `--text-tertiary` cleanly, reads as "scope into / drill in" — semantically right for "this assertion is inside that sub-graph". Considered + rejected by ux-lead: - `▸` reads as expand/play (state action, not containment) - `└` implies tree hierarchy that doesn't exist here - `◫` sparse monospace coverage Single-character edit in `components.tsx` at the chip render site. No layout / CSS / helper / conditional change. Tests: `context-graph-empty-stat-components.test.ts` asserts on slug text + ellipsis + tooltip, never on the glyph itself, so they need no update. Confirmed by grep — only one `🗂` literal existed in the codebase (the swap site). CSS-style verification: - `tsc --noEmit` clean. - Focused suite: 27/27 (13 lifecycle + 14 empty-stat). - `pnpm build:ui` produced `index-C5vosJX4.js` (content hash flipped from prior bundle). Binary check: `e2 80 ba` (U+203A) present, zero `🗂` occurrences in the bundle.
…er test Fix A — promote handlers thread `subGraph` to the daemon. Sub-graph assertions surfaced in the WM/SWM list (PR #706) became clickable, but the four `promoteAssertion()` call sites only sent `(contextGraphId, name)`. Daemon's lookup key is `(contextGraphId, name, subGraphName)`, so promoting a sub-graph assertion either 404'd or silently hit a same-named root one. Wire shape stays root-bucket-compatible — `subGraphName` is only spread when supplied. api.ts: - `promoteAssertion(contextGraphId, assertionName, entities='all', subGraphName?)`. Daemon route already accepts `subGraphName` in the body (`packages/cli/src/daemon/routes/assertion.ts:820-823`); no daemon change. components.tsx — four call sites + one closure-shape touch: - `LayerWidgetStrip` bulk-promote loop (line ~1564) → passes `a.subGraph`. - `AssertionsList.handlePromote(name, subGraph?)` per-row handler (line ~2689) + the per-row button (line ~2812) now passes `a.subGraph` through. - `AssertionsList.handlePromoteAll` bulk loop (line ~2722) → passes `a.subGraph`. - `EntityPromoteButton` per-entity promote (line ~3100) → uses `sgBinding!.subGraph`. `sgBinding` reshaped from `binding | null` to `{ binding, subGraph } | null` so the matched slug travels with the binding (it was already known in the lookup loop — just discarded). All `sgBinding.sourceAssertion` references updated to `sgBinding.binding.sourceAssertion`. Fix B — module cycle broken by relocating the parser. `api.ts → useAssertionLifecycleEvents.ts → api.ts` worked under ESM hoisting but was brittle, and conceptually wrong (pure string parser pulled into the transport layer via a React hook module). - New `packages/node-ui/src/ui/lib/sub-graph-uri.ts` — `subGraphFromAssertionGraphUri` moved verbatim (JSDoc preserved, plus a note explaining why it lives in `lib/`). - `api.ts` import path updated. - `useAssertionLifecycleEvents.ts` — local definition deleted, helper now imported from `lib/`. No re-export shim per dkg-v9 no-backwards-compat rule. - Tests for `subGraphFromAssertionGraphUri` moved out of `use-assertion-lifecycle-events.test.ts` into a new focused `test/sub-graph-uri.test.ts` (4 cases, contents identical). Fix C — direct test for `listAssertions` URI parsing. The existing `context-graph-empty-stat-components.test.ts` mocks `listAssertions` itself, so a regression in the api.ts parser wouldn't be caught. - New `packages/node-ui/test/list-assertions.test.ts` (5 cases): root-bucket parse + subGraph=undefined; sub-graph + root in one response (#706 regression guard); malformed graph URIs silently dropped; `cgPrefix` scoping (other-CG bindings AND `cg-A-suffix` shape both rejected by the trailing-slash prefix); empty-result path. - Mocks `globalThis.fetch` rather than `vi.mock`ing the api module — `listAssertions` calls `executeQuery` intra-module, and ES intra-module refs don't pick up the mocked export. Same pattern as `use-swm-attributions.test.ts` from PR #694. SWM-branch test deferred (the brief listed it as optional — `dkg:ShareTransition` binding shape needs more fixture setup); the `subGraph` field on SWM is already covered by the existing component test via the mocked `listAssertions` shape. Verification: - `tsc --noEmit` clean. - Focused suite: 32/32 (context-graph-empty-stat 14 + use-assertion-lifecycle 9 [4 fewer — helper tests moved] + sub-graph-uri 4 [new] + list-assertions 5 [new]). - `pnpm build:ui` green; bundle picks up the new promote handler wiring.
…710 follow-up) Second consumer of `listAssertions('wm')` missed by the prior Fix A pass — `MemoryLayerView.tsx`'s `AssertionList` has the same single-row + bulk-loop shape as `AssertionsList` in components.tsx but lives in its own file, so it didn't surface in the prior grep sweep. Same data path → same daemon `(cg, name, subGraph)` lookup bug if a sub-graph-scoped assertion is promoted from this view. Three-touch fix mirrors the AssertionsList pattern verbatim: - `handlePromote(name, subGraph?)` — adds the optional 4th arg and threads it to `promoteAssertion`. - Per-row button onClick — passes `a.subGraph` through. - `handlePromoteAll` bulk loop — passes `a.subGraph` per row. No tests changed; the data contract is pinned by `list-assertions.test.ts` and the callsite-correctness is mechanically identical to the now-tested AssertionsList path. Verification: - `tsc --noEmit` clean. - Focused suite: 32/32 (context-graph-empty-stat 14 + use-assertion-lifecycle 9 + sub-graph-uri 4 + list-assertions 5). - `pnpm build:ui` green.
Two more identity-by-name leaks in the same root-cause family as
the prior promote fixes: `name` is no longer a unique identifier
once root + sub-graph partitions can share names.
Fix D — busy state + React keys must use `graphUri` (🔴):
`AssertionsList` (`components.tsx`) and `AssertionList`
(`MemoryLayerView.tsx`) both keyed transient UI state on `name`.
A root + sub-graph pair with the same name (e.g. both partitions
have a `draft`) would render duplicate React keys, highlight both
rows as busy on a single click, and surface the wrong result
message. Switched both to `graphUri` (daemon-produced, unique).
`handlePromote` reshaped from `(name, subGraph?)` to
`(assertion: AssertionInfo)` — pulls all three fields (name,
subGraph, graphUri) from one source rather than threading
positional args that drift apart. `AssertionInfo` type import
added to `components.tsx` (was inferential through
`listAssertions` return); already imported in MemoryLayerView.
Touch list:
- `components.tsx` lines ~2691 (handler), ~2792 (key), ~2812
(onClick), ~2813/2815 (busy comparisons).
- `MemoryLayerView.tsx` lines ~399 (handler), ~457 (key), ~473
(onClick), ~476 (busy comparison).
- `'__all__'` sentinel kept — not collidable with any graphUri.
Fix E — file preview / extraction-status by name only (🔴):
`previewName: string | null` + `fetchExtractionStatus(name, cg)`
lost the sub-graph context. Clicking a sub-graph-scoped imported
assertion's filename queried the root-bucket assertion → 404 or
wrong file. Daemon route already accepts `subGraphName` query
param (`packages/cli/src/daemon/routes/assertion.ts:3364`).
`api.ts:fetchExtractionStatus` extended to accept `subGraphName?`
4th arg, built with `URLSearchParams` (per the brief's "cleaner
than hand-rolling `&subGraphName=…`"); only set when supplied.
`FilePreviewModal` props gained `subGraphName?`; the useEffect
forwards it to `fetchExtractionStatus` and lists `subGraphName`
in its deps array so a preview change re-triggers the fetch.
`MemoryLayerView` `previewName: string | null` reshaped to
`preview: { name; subGraph? } | null`; all setters and the
`<FilePreviewModal …>` mount updated to carry the slug.
`FilePreviewModal` is mounted at only one site (grep confirmed)
so no other consumers to update.
Test fixtures: the prior #706 + SWM fixtures in
`context-graph-empty-stat-components.test.ts` were missing
`graphUri` — pre-Fix D the React key fell back to `name` (always
defined); post-Fix D the key is `graphUri` (would have been
undefined). Updated three fixture lines to include `graphUri`
with the canonical daemon shape; re-run confirms no React
key-warning in stderr. Per brief: no net-new test cases — D/E
are identity-shape changes, not new parsing rules; existing
`list-assertions.test.ts` pins the upstream data contract.
Verification:
- `tsc --noEmit` clean.
- Focused suite: 32/32 (context-graph-empty-stat 14 +
use-assertion-lifecycle 9 + sub-graph-uri 4 + list-assertions 5).
React key-warning gone from stderr.
- `pnpm build:ui` green; bundle picks up the threaded
`subGraphName` on `fetchExtractionStatus` calls.
The prior `g.indexOf('/assertion/')` searched the full URI. Because
`validateContextGraphId` permits slashes, a cgId can literally
contain `/assertion/` as a substring, and the indexOf would have
hit the cgId's internal `/assertion/` rather than the delimiter
after the cg prefix — mis-parsing both the name and the sub-graph.
Tail-scope the parse: take everything after `cgPrefix`, then
discriminate inside the tail. Branches:
- tail starts with `assertion/` → root-bucket
- tail contains `/assertion/` → sub-graph-scoped
- otherwise → drop
This subsumes the prior outer `.includes('/assertion/')` filter and
removes the cgId-substring failure mode. Same family as the earlier
wmSparql asymmetry — `subGraphFromAssertionGraphUri` was already
tail-scoped; only this site and the outer filter still operated on
the full URI.
Test pins the regression: a cgId containing `/assertion/` should
still parse a binding whose tail is `assertion/<agent>/<name>` to
`{ name: 'foo', subGraph: undefined }`.
The prior `tail.indexOf('/assertion/')` branch admitted any tail with
`/assertion/` anywhere inside, including shapes like
`foo/bar/assertion/<agent>/<name>` (3+ segments before `assertion/`).
`subGraphFromAssertionGraphUri` returned `undefined` for those —
`segments[1]` isn't `'assertion'` — so the row got admitted as a
root assertion with a mis-derived name. Promote/preview lookups
key on `name` and would silently miss.
Replace the indexOf-based branches with a strict segment-count parse
on the tail (post-cgPrefix):
['assertion', <agent>, <name>] → root-bucket (3 segs)
[<sg>, 'assertion', <agent>, <name>] → sub-graph (4 segs)
anything else → drop
Drop the call to `subGraphFromAssertionGraphUri` from this site (and
the now-unused import) — the strict segment parse derives the slug
in one pass and the helper stays in `lib/` for the lifecycle hook.
Tests pin both edges of the strict contract: a 3+-segments-before-
`assertion/` binding is rejected, and a sub-graph binding with an
extra segment after `<name>` is rejected.
…moryLayerView After #706 surfaced both root + sub-graph assertions in the same WM list, two rows literally labeled `draft` are indistinguishable in the `MemoryLayerView` `AssertionList` row, even though the promote and preview handlers already key on `graphUri` and route correctly. The `› <slug>` chip closed the same gap for `AssertionsList` (components.tsx) in the original #706; we missed the second consumer. Three small changes: 1. Lift `truncateMiddle` from `components.tsx` into `lib/truncate.ts` so both consumers share one definition — same `lib/` pattern as `sub-graph-uri.ts` from the prior PR #710 fix. 2. Render the chip in the `MemoryLayerView` row using the identical JSX pattern from `AssertionsList` (same class, same `›` glyph, same 18-char mid-truncation, same tooltip). 3. Disambiguate the success message by carrying `subGraph` on `promoteResult` and rendering `" (in <slug>)"` after the assertion name when set. Root rows keep the prior wording.
3 tasks
This was referenced May 27, 2026
3 tasks
4 tasks
Jurij89
pushed a commit
that referenced
this pull request
Jun 1, 2026
…877 c1) Drafts created before #710 carry `dkg:assertionGraph` in `_meta` but no `dkg:subGraphName`, so the _meta-derived WM listing would surface them as root-bucket and mis-scope promote/preview lookups for that migration window. Re-bind `?assertionGraph` in the WM SELECT and derive the sub-graph scope as: explicit `dkg:subGraphName` literal when present (authoritative, since #710), else parse the sub-graph segment out of the assertion-graph URI via the existing `subGraphFromAssertionGraphUri` helper (the same migration fallback the lifecycle hook uses). `graphUri` stays the lifecycle URN (?assertion); no data-graph/triple filter is added (that would reintroduce the promoted- assertion-leak the _meta approach fixes). Tests: list-assertions.test.ts adds pre-#710 compat (subGraph from URI when subGraphName absent; root-bucket legacy URI stays root) + literal-wins-over- URI precedence — 9 tests. node-ui tsc --noEmit clean; full node-ui suite 87 files / 1246 passed / 38 skipped / 0 fail (--no-file-parallelism), modulo the known openclaw-bridge flake. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Jurij89
added a commit
that referenced
this pull request
Jun 1, 2026
… _meta) (#877) * fix(node-ui): derive WM assertions list from _meta (fix #844 empty-state regression) Merge #844 added an opt-in includeContextGraphPartitions flag to the query engine; by default a `GRAPH ?g { ?s ?p ?o }` enumeration is clamped to a meta-only allow-list (<cg>/_meta, <cg>/_shared_memory_meta), so per-assertion data graphs are no longer enumerated. useMemoryEntities.ts was migrated (the count badges work), but the WM branch of listAssertions was missed: its `GRAPH ?g` enumeration returned only meta graphs, every assertion row was filtered out, and the Assertions subtab rendered "No Working Memory assertions yet." despite WM having data. Fix is frontend-only (the gate is intentional and security-reviewed): derive WM assertions from the <cg>/_meta lifecycle graph (which IS in the default allow-list, so no opt-in needed), filtering dkg:Assertion + dkg:memoryLayer "WM" + dkg:assertionName. memoryLayer is mutable and flips to "SWM" on promote, so promoted assertions correctly drop out of the WM list. graphUri is the lifecycle URN (?assertion) — SWM-consistent and stable across promote; subGraph comes from dkg:subGraphName (#710). Dedup on the lifecycle URN. Mirrors the proven SWM branch; reuses bv(). No triple-count chip on WM rows (SWM-parity; renderer guards tripleCount != null). Rewrites list-assertions.test.ts WM fixtures around the _meta contract (?assertion ?name ?sg): root-bucket, sub-graph-scoped, skip-incomplete, DISTINCT dedup, no-tripleCount, empty — 7 tests. node-ui tsc clean; list-assertions.test.ts 7/7; full node-ui suite 87 files / 1244 passed / 38 skipped / 0 fail (--no-file-parallelism), modulo the known pre-existing openclaw-bridge flake. Live node: new query returns 6 WM rows for ui-refresh / 1 for medical-data; negative control (old GRAPH ?g) returns only the two meta graphs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(node-ui): pre-#710 sub-graph fallback in WM listAssertions (Codex #877 c1) Drafts created before #710 carry `dkg:assertionGraph` in `_meta` but no `dkg:subGraphName`, so the _meta-derived WM listing would surface them as root-bucket and mis-scope promote/preview lookups for that migration window. Re-bind `?assertionGraph` in the WM SELECT and derive the sub-graph scope as: explicit `dkg:subGraphName` literal when present (authoritative, since #710), else parse the sub-graph segment out of the assertion-graph URI via the existing `subGraphFromAssertionGraphUri` helper (the same migration fallback the lifecycle hook uses). `graphUri` stays the lifecycle URN (?assertion); no data-graph/triple filter is added (that would reintroduce the promoted- assertion-leak the _meta approach fixes). Tests: list-assertions.test.ts adds pre-#710 compat (subGraph from URI when subGraphName absent; root-bucket legacy URI stays root) + literal-wins-over- URI precedence — 9 tests. node-ui tsc --noEmit clean; full node-ui suite 87 files / 1246 passed / 38 skipped / 0 fail (--no-file-parallelism), modulo the known openclaw-bridge flake. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Jurij Skornik <jurij.skornik@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
matic031
pushed a commit
to KilianTrunk/dkg
that referenced
this pull request
Jun 2, 2026
Brings forward 3 node-ui UI bug fixes that landed on main but hadn't yet flowed into the release branch: - 902a618 fix(node-ui): entity tripleCount matches Triples tab (count type + multi-value + incoming) (OriginTrail#728) - 76b86b9 fix(node-ui): SubGraphBar "All" pill now matches layer tab badge (OriginTrail#725) - 387b0e0 fix(node-ui): WM Assertions tab surfaces sub-graph assertions (OriginTrail#706) (OriginTrail#710) All three touch only node-ui files (UI + UI tests); no runtime / chain / publisher / agent surface area touched. Auto-merge with rc.12 was clean — one file (`packages/node-ui/src/ui/api.ts`) merged automatically with no conflicts.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
listAssertions(cg, 'wm')inpackages/node-ui/src/ui/api.tsran a permissiveSELECT DISTINCT ?gSPARQL and then client-side-filtered with a hardcoded prefixdid:dkg:context-graph:<cgId>/assertion/— sub-graph assertions live at<cgId>/<subGraphName>/assertion/…and silently failed thestartsWithcheck.wmSparqlfilter pattern (useMemoryEntities.ts:171) and adds a quiet sub-graph indicator to each affected row so the user can see which sub-graph each assertion belongs to.Related
dkg_query view: "working-memory"agent-tool half — same root-only-filter family, different code path; intentionally deferred)Files changed
packages/node-ui/src/ui/api.tsstartsWith(<cgPrefix>/) && includes('/assertion/'); addedsubGraph?: stringtoAssertionInfo; sub-graph slug populated via the existingsubGraphFromAssertionGraphUri()helper fromuseAssertionLifecycleEvents.ts. SWM branch also surfaces the field for uniformity.packages/node-ui/src/ui/views/project/components.tsxAssertionsListrow renders an inline🗂 <slug>chip in.v10-item-meta-rowwhenassertion.subGraphis set. Reuses.v10-item-countstyling exactly (low-contrast, no fill, no border) so the chip reads as quiet structural metadata, not a state-change pill. 18-char middle-ellipsis via localtruncateMiddle; full slug intitle="In sub-graph: <slug>". Non-clickable in this PR — click-to-navigate lands with S3.packages/node-ui/test/context-graph-empty-stat-components.test.tsTest plan
pnpm --filter @origintrail-official/dkg-node-ui exec tsc --noEmitcleanpnpm --filter @origintrail-official/dkg-node-ui exec vitest run test/context-graph-empty-stat-components.test.ts test/use-assertion-lifecycle-events.test.ts— 27/27pnpm build:uigreen--text-tertiary, position after.v10-item-count, threshold = 18, tooltip wording)🗂 <slug>chip; root rows don't; hover tooltip readsIn sub-graph: <full-slug>Notes
dkg_query view: "working-memory"agent-tool half of dkg_query view: "working-memory" does not include data from sub-graphs #675 (separate code path inpackages/mcp-dkg; lay-down decision deferred to its own cycle); the SubGraphBar chip-scope behaviour (S3 territory); grouping assertions by sub-graph in the list (badge-only treatment per user direction).coloron the 🗂 glyph, so it may render more saturated than the chip's--text-tertiarytext. CSS-only mitigation exists (font-variant-emoji: text;) if needed after live review.