Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions packages/node-ui/src/ui/hooks/useSwmAttributions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,11 @@ function subGraphFromMetaGraphUri(gUri: string, cgId: string): string | undefine
}

export function buildAttributionsQuery(cgId: string): string {
const cgUri = `did:dkg:context-graph:${cgId}`;
// Trailing slash makes this an EXACT CG prefix: every partition graph is
// `did:dkg:context-graph:<cg>/[<sg>/]_shared_memory_meta`, so "<cgUri>/"
// matches all of this CG's partitions while excluding sibling CGs whose id
// merely shares the prefix (cg-1 must not capture cg-10 / cg-1-foo).
const cgPrefix = `did:dkg:context-graph:${cgId}/`;
return `PREFIX dkg: <http://dkg.io/ontology/>
PREFIX prov: <http://www.w3.org/ns/prov#>
SELECT ?op ?root ?agent ?publishedAt ?g WHERE {
Expand All @@ -191,7 +195,7 @@ SELECT ?op ?root ?agent ?publishedAt ?g WHERE {
prov:wasAttributedTo ?agent .
}
FILTER(
STRSTARTS(STR(?g), "${cgUri}") &&
STRSTARTS(STR(?g), "${cgPrefix}") &&
CONTAINS(STR(?g), "_shared_memory_meta")
)
} ORDER BY DESC(?publishedAt) LIMIT 5000`;
Expand Down Expand Up @@ -251,8 +255,14 @@ export function useSwmAttributions(contextGraphId: string | undefined): SwmAttri
headers: { 'Content-Type': 'application/json', ...authHeaders() },
signal: controller.signal,
body: JSON.stringify({
// Do NOT send contextGraphId here. The query already scopes to the
Comment thread
Jurij89 marked this conversation as resolved.
Comment thread
Jurij89 marked this conversation as resolved.
// CG via its exact STRSTARTS(?g, "<cgUri>/") filter and must read EVERY
// sub-graph's <cg>/<sg>/_shared_memory_meta partition. Passing
// contextGraphId makes the engine constrain GRAPH ?g to CG-direct
// graphs only, dropping per-sub-graph attribution so the legend
// under-counts agents (B2). See dkg-query-engine.ts graph-variable
// allow-list.
sparql: buildAttributionsQuery(contextGraphId),
Comment thread
Jurij89 marked this conversation as resolved.
Comment thread
Jurij89 marked this conversation as resolved.
contextGraphId,
}),
});
if (!res.ok) throw new Error(`SPARQL query failed: ${res.status}`);
Expand Down
15 changes: 12 additions & 3 deletions packages/node-ui/src/ui/hooks/useVerifiedMemoryAnchors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,11 @@ function agentLabel(agentId: string): string {
/** Build the SPARQL that enumerates every publish-batch WorkspaceOperation
* across every sub-graph's `_shared_memory_meta`. */
function buildAnchorsQuery(cgId: string): string {
const cgUri = `did:dkg:context-graph:${cgId}`;
// Trailing slash makes this an EXACT CG prefix: every partition graph is
// `did:dkg:context-graph:<cg>/[<sg>/]_shared_memory_meta`, so "<cgUri>/"
// matches all of this CG's partitions while excluding sibling CGs whose id
// merely shares the prefix (cg-1 must not capture cg-10 / cg-1-foo).
const cgPrefix = `did:dkg:context-graph:${cgId}/`;
return `PREFIX dkg: <http://dkg.io/ontology/>
PREFIX prov: <http://www.w3.org/ns/prov#>
SELECT ?op ?root ?agent ?publishedAt ?g WHERE {
Expand All @@ -122,7 +126,7 @@ SELECT ?op ?root ?agent ?publishedAt ?g WHERE {
prov:wasAttributedTo ?agent .
}
FILTER(
STRSTARTS(STR(?g), "${cgUri}") &&
STRSTARTS(STR(?g), "${cgPrefix}") &&
CONTAINS(STR(?g), "_shared_memory_meta")
)
} ORDER BY ?publishedAt LIMIT 2000`;
Expand Down Expand Up @@ -234,8 +238,13 @@ export function useVerifiedMemoryAnchors(
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders() },
body: JSON.stringify({
// Do NOT send contextGraphId here. buildAnchorsQuery already scopes to
// the CG via its exact STRSTARTS(?g, "<cgUri>/") and enumerates EVERY sub-graph's
// <cg>/<sg>/_shared_memory_meta partition. Passing contextGraphId makes
// the engine constrain GRAPH ?g to CG-direct graphs only, dropping
// per-sub-graph anchors/attribution (same bug class as B2). See
// dkg-query-engine.ts graph-variable allow-list.
sparql: buildAnchorsQuery(contextGraphId),
contextGraphId,
}),
});
if (!res.ok) throw new Error(`SPARQL query failed: ${res.status}`);
Expand Down
64 changes: 63 additions & 1 deletion packages/node-ui/test/use-swm-attributions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ describe('useSwmAttributions — SPARQL query shape', () => {
// the new DESC and the old `ORDER BY ?publishedAt` both present.
expect(q).not.toMatch(/ORDER BY \?publishedAt\s+LIMIT/);
});

// Codex review (PR #1055) — with contextGraphId removed from the request
// (B2), scoping rests entirely on this STRSTARTS prefix, so it MUST end in
// "/" to be exact. A bare "did:dkg:context-graph:cg-1" prefix would also
// match a sibling CG like cg-10 / cg-1-foo and merge its _shared_memory_meta
// attribution rows into the legend.
it('scopes via an exact "<cgUri>/" STRSTARTS prefix so sibling CGs do not leak in', () => {
const q = buildAttributionsQuery('cg-1');
expect(q).toContain('STRSTARTS(STR(?g), "did:dkg:context-graph:cg-1/")');
// The slash-less prefix would over-match cg-10 / cg-1-foo.
expect(q).not.toMatch(/STRSTARTS\(STR\(\?g\), "did:dkg:context-graph:cg-1"\)/);
});
});

// Codex Code7 (PR #656) — the hook returns its previous-graph result
Expand All @@ -49,7 +61,11 @@ describe('useSwmAttributions — stale-on-switch protection', () => {
originalFetch = globalThis.fetch;
globalThis.fetch = vi.fn(async (_url: any, init?: any) => {
const body = init?.body ? JSON.parse(String(init.body)) : {};
const cgId: string = body.contextGraphId;
// B2: the POST body no longer carries contextGraphId (it scoped the
// engine to CG-direct graphs and dropped per-sub-graph attribution).
// Recover the cgId from the SPARQL's STRSTARTS filter to route the
// deferred promise, matching how the daemon now scopes the query.
const cgId: string = String(body.sparql).match(/context-graph:([^"/]+)/)?.[1] ?? '';
Comment thread
Jurij89 marked this conversation as resolved.
Outdated
const p = new Promise<any[]>((resolve) => {
pending.set(cgId, { resolve });
});
Expand Down Expand Up @@ -125,3 +141,49 @@ describe('useSwmAttributions — stale-on-switch protection', () => {
expect(latest!.events[0].rootUri).toBe('urn:e:cg-B');
});
});

// B2 (DKG-NODE-ISSUES-FOR-RC17) — the attribution fetch MUST NOT send
// contextGraphId. The SPARQL scopes itself to the CG via STRSTARTS(?g, …)
// and must read every sub-graph's <cg>/<sg>/_shared_memory_meta partition;
// sending contextGraphId makes the daemon constrain GRAPH ?g to CG-direct
// graphs only, so the legend under-counted agents (showed 1 of 5 agents live).
describe('useSwmAttributions — B2 POST body scoping', () => {
let root: Root;
let container: HTMLDivElement;
let originalFetch: typeof globalThis.fetch | undefined;

beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
originalFetch = globalThis.fetch;
globalThis.fetch = vi.fn(async () => ({
ok: true,
status: 200,
json: async () => ({ result: { bindings: [] } }),
} as any)) as any;
});

afterEach(async () => {
await act(async () => { root.unmount(); });
container.remove();
if (originalFetch) globalThis.fetch = originalFetch;
});

it('omits contextGraphId from the /api/query POST body so all sub-graph partitions are reached', async () => {
function Probe({ id }: { id: string }) {
useSwmAttributions(id);
return null;
}
await act(async () => {
root.render(React.createElement(Probe, { id: 'cg-1' }));
});
await act(async () => { await new Promise((r) => setTimeout(r, 0)); });

const calls = vi.mocked(fetch).mock.calls;
expect(calls.length).toBeGreaterThan(0);
const body = JSON.parse(String((calls[0][1] as any)?.body ?? '{}'));
expect(body.sparql).toContain('_shared_memory_meta');
expect(body).not.toHaveProperty('contextGraphId');
});
});
57 changes: 57 additions & 0 deletions packages/node-ui/test/use-verified-memory-anchors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// @vitest-environment happy-dom

import React, { act } from 'react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { createRoot, type Root } from 'react-dom/client';
import { useVerifiedMemoryAnchors } from '../src/ui/hooks/useVerifiedMemoryAnchors.js';

(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;

// Same bug class as B2 (DKG-NODE-ISSUES-FOR-RC17): buildAnchorsQuery already
// scopes to the CG via STRSTARTS(?g, …) and enumerates EVERY sub-graph's
// <cg>/<sg>/_shared_memory_meta partition, so the fetch MUST NOT also send
// contextGraphId — that makes the daemon constrain GRAPH ?g to CG-direct
// graphs only, dropping per-sub-graph anchors/attribution.
describe('useVerifiedMemoryAnchors — POST body scoping', () => {
let root: Root;
let container: HTMLDivElement;
let originalFetch: typeof globalThis.fetch | undefined;

beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
originalFetch = globalThis.fetch;
globalThis.fetch = vi.fn(async () => ({
ok: true,
status: 200,
json: async () => ({ result: { bindings: [] } }),
} as any)) as any;
});

afterEach(async () => {
await act(async () => { root.unmount(); });
container.remove();
if (originalFetch) globalThis.fetch = originalFetch;
});

it('omits contextGraphId from the /api/query POST body so all sub-graph partitions are reached', async () => {
function Probe({ id }: { id: string }) {
useVerifiedMemoryAnchors(id);
return null;
}
await act(async () => {
root.render(React.createElement(Probe, { id: 'cg-1' }));
});
await act(async () => { await new Promise((r) => setTimeout(r, 0)); });

const calls = vi.mocked(fetch).mock.calls;
expect(calls.length).toBeGreaterThan(0);
const body = JSON.parse(String((calls[0][1] as any)?.body ?? '{}'));
expect(body.sparql).toContain('_shared_memory_meta');
expect(body).not.toHaveProperty('contextGraphId');
// Codex review (PR #1055) — exact "<cgUri>/" prefix so a sibling CG
// (cg-10 / cg-1-foo) can't leak its anchors in once contextGraphId is gone.
expect(body.sparql).toContain('STRSTARTS(STR(?g), "did:dkg:context-graph:cg-1/")');
});
});
Loading