Skip to content

Per-KA metadata trim: ~134 → ~50 quads/KA (RFC + Phases 0–3 implementation)#1155

Merged
branarakic merged 5 commits into
mainfrom
feat/ka-metadata-trim
Jun 13, 2026
Merged

Per-KA metadata trim: ~134 → ~50 quads/KA (RFC + Phases 0–3 implementation)#1155
branarakic merged 5 commits into
mainfrom
feat/ka-metadata-trim

Conversation

@branarakic

Copy link
Copy Markdown
Contributor

What

Implements RFC: per-KA metadata trim + indexed graph addressing (in this PR), Phases 0–3.

Publishing one 1-triple KA currently leaves ~134 resident quads in the local store (live-measured on a Base Sepolia rc.17 node) — ~97% publish bookkeeping, ~30 quads repeated copies of five values. That volume (× network sync) feeds RocksDB compaction and makes every recurring reconciler query more expensive — the mechanism behind the rc.17 idle-node CPU saturation. After this PR: ~50 quads/KA, ~40 with metadata.provenanceEvents: false (lite mode).

How

Every removed/relocated triple was justified by a writer + reader audit (grep both IRI forms over all packages incl. node-ui/mcp-dkg; verdicts cited per-row in the RFC). Every migrated reader reads both old and new shapes (replicas hold old-shape rows synced from older nodes).

  • Phase 0 — dead code: the never-firing AssertionPublished writer+gate, orphan kcUal read, dangling partition bnode.
  • Phase 1 — zero-reader drops: kaCount, blockTimestamp, publisherAddress, chainId, blockNumber, tokenId×2, publicTripleCount×2, AuthorshipProof block, Publication node, lifecycle-URN type/contextGraph/wasGeneratedBy rows.
  • Phase 2 — dedupe: KC/KA rdf:type rows → predicate-based counters; one rootEntity (the dkg:entity dual-write dropped outside the signed seal); wm/swm pointers written only on divergence from vm; fromLayer/toLayer derived from event class; wasAssociatedWith optional; publicSnapshotRef collapsed into the digest.
  • Phase 3 — structural: UAL + <ual>/<n> collapsed into one node (post-rc.17 invariant: 1 publish = 1 KA = 1 UAL); metadata.provenanceEvents config (default true) gating all four lifecycle event writers; ShareTransition removed (node-ui receipt reads the seal subject, legacy fallback retained); partition copy → documented minimal shape (REMAP keeps the wholesale move — it's the only meta home there).

Deliberately deferred (with worked plan)

Lifecycle-URN → seal-subject merge: the implementation audit surfaced (a) subject collision with author-signed seal/receipt rows under subject-scoped wipes, and (b) exact-subject kaId/reservedUal reads risking identity double-allocation on upgraded stores. Plan at TODO(rfc-ka-trim) in packages/core/src/constants.ts.

Verification

  • Suites green: core 1039 · query 261 · random-sampling 62 · publisher 1150 · agent 1226 · cli 2031 · kafka-plugin 169 · node-ui 1418 (+ tsc/builds for all touched packages).
  • Adversarial review round produced 4 blockers + 1 reader violation — all fixed with regression tests:
    1. kafka-plugin discovery read partOf+publishedAt (audit miss — publishedAt is now KEPT, RFC corrected) → read-both queries; the kafka e2e reproduced the bug live pre-fix (red→green).
    2. provenanceEvents=false didn't gate discard/update event writers → gated, prov:wasInvalidatedBy gated with its event.
    3. Multi-root private KA could serve a mismatched (rootEntity, privateMerkleRoot) pair under the collapsed shape → handler now computes the private root from the actually-served triples when ambiguous.
    4. WM-marker deletion turned a stale re-promote no-op into a thrown 409 → marker updated to "VM" instead.
    5. isAlreadyConfirmed ASK vs the minimal partition shape → read-both against label _meta.
  • Interop audit across three store states (fresh / upgraded-with-old-rows / replica-syncing-old-peers) for resolveKA, access control, RS prover, history, sync responder, node-ui receipt.

Reviewer focus, please

  1. The UAL-collapse read-both sites (resolveKA, access-handler incl. the <ual>/<n>→bare-UAL fallback, RS prover, sync delta, kafka) — highest blast radius.
  2. The dkg:partOf write removal (RFC "Invariant exception" section) — sanctioned, all readers read-both, but it's a model change worth a deliberate ACK.
  3. metadata.provenanceEvents default (currently true = zero behavior change; flipping to false maximizes savings network-wide).
  4. RFC Part 2 (graph registry + event-driven reconcilers) is proposed but NOT implemented here — follow-up scope.

🤖 Generated with Claude Code

Branimir Rakic and others added 2 commits June 12, 2026 12:48
One 1-triple KA publish leaves ~134 resident quads (live-measured); ~97% is
bookkeeping, ~30 quads are copies of five values. Combined with hot-path
graph-name scans (STRSTARTS(STR(?g)), SELECT DISTINCT ?g — the adapter's own
"dominant idle-node CPU cost"), this drives the rc.17 idle-CPU saturation.

The RFC specifies: Phase 0 dead code, Phase 1 zero-reader drops (~-24/KA),
Phase 2 dedupe via small reader migrations (~-25), Phase 3 aggressive decision
points (UAL/token collapse, URN merge, provenance-events flag, ~45-50/KA), and
the query-side fixes (graph registry to kill name scans, event-driven
reconcilers). Every verdict cites its writers and readers by file:line.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements docs/rfcs/ka-metadata-trim.md: one 1-triple KA publish drops from
~134 resident quads to ~50 (~40 with metadata.provenanceEvents=false), with
every removed/relocated triple justified by a writer+reader audit and every
migrated reader reading both old and new shapes.

- Phase 0: dead AssertionPublished writer+gate; orphan kcUal read; dangling
  partition bnode.
- Phase 1: zero-reader drops (kaCount, blockTimestamp, publisherAddress,
  chainId, blockNumber, tokenId, publicTripleCount, AuthorshipProof block,
  Publication node, URN type/contextGraph/wasGeneratedBy rows). publishedAt
  KEPT after adversarial review found the kafka-plugin discovery reader.
- Phase 2: KC/KA type rows -> predicate-based counters; single rootEntity
  (entity alias dropped outside the signed seal); wm/swm pointers written
  only on divergence; fromLayer/toLayer + wasAssociatedWith derived/optional;
  publicSnapshotRef collapsed; WM marker updated (not deleted) at VM flip.
- Phase 3: UAL+<ual>/<n> collapsed to one node (read-both in resolveKA,
  access-handler incl. <ual>/<n> fallback for old clients, RS prover, sync,
  kafka discovery, async-lift, EPCIS, counters); metadata.provenanceEvents
  config (default true) gating all four lifecycle event writers; ShareTransition
  dropped (node-ui receipt reads the seal subject, legacy fallback retained);
  partition CONSTRUCT-copy -> documented minimal shape (REMAP keeps wholesale
  move). Lifecycle-URN->seal merge DEFERRED with a worked plan
  (TODO(rfc-ka-trim) at assertionLifecycleUri) after the audit surfaced
  signed-material collision + identity double-allocation hazards.
- Adversarial-review fixes: kafka read-both queries; provenanceEvents gating
  on discard/update; multi-root private access attestation always matches the
  served triples (computePrivateRoot fallback); stale re-promote stays a no-op;
  isAlreadyConfirmed read-both vs the minimal partition shape.

Author-signed seal material untouched. Suites green: core 1039, query 261,
random-sampling 62, publisher 1150, agent 1226, cli 2031, kafka-plugin 169,
node-ui 1456 (+tsc/builds).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
if (q.predicate === `${DKG_NS}merkleRoot`) kcMerkleRoots.set(q.subject, stripLiteral(q.object));
}

// TODO(rfc-ka-trim) P3.1: collapsed-shape rows (rootEntity on the UAL, no

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Bug: this leaves collapsed-shape KAs (rootEntity on the bare UAL, which this PR now writes by default) on the existing "accept on trust" path, so sync no longer Merkle-verifies newly published metadata at all. That is a silent data-integrity downgrade for replicated stores. Either teach the verifier to self-map bare UAL subjects before enabling P3.1 writes, or keep emitting the legacy partOf rows until the verifier is upgraded.

)
OPTIONAL { ?asrt dkg:publishedAtKaId ?kaId . }
OPTIONAL { ?asrt dkg:assertionFinalizedAt ?finalizedAt . }
OPTIONAL { ?lc dkg:rootEntity ?asrt ; dkg:reservedUal ?ual . }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Bug: ?lc dkg:rootEntity ?asrt does not match the current lifecycle shape — those dkg:rootEntity rows point at member entities, not the assertion URI. After this PR stops writing publishedAtKaId, that means ?ual/?batchId never bind for new publishes, so the hook will return status: 'verified' with ual and kaId still null. Join through a real assertion→lifecycle edge, or keep a direct id field until that link exists.

// old-client requests keep resolving against new-shape stores.
const legacyToken = /^(.+)\/\d+$/.exec(kaUal);
if (legacyToken && legacyToken[1]) {
return this.queryKAMeta(legacyToken[1]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Bug: collapsing every legacy <ual>/<n> request to the bare UAL loses per-root addressability for multi-root KAs. Once queryKAMeta() runs on the collapsed shape, both /1 and /2 resolve to whichever rootEntity happens to be first, so non-first private bags become unreachable and callers can receive the wrong member's data. This needs an explicit root-selection mapping (or retained token metadata) before falling back to the bare UAL.

Comment thread packages/publisher/src/metadata.ts Outdated
mq(subject, `${RDF}type`, `${DKG}Assertion`, metaGraph),
// Assertion entity (DKG identity). RFC ka-metadata-trim Phase 1: the
// `a prov:Entity` / `a dkg:Assertion` type rows, `dkg:contextGraph` and
// `prov:wasGeneratedBy` were dropped (zero readers — history joins the

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: prov:wasGeneratedBy still has a live in-repo consumer: packages/graph-viz/src/core/provenance-resolver.ts reads it to populate generatedBy / generatedByName. Dropping the triple here will make newly created assertions lose provenance in graph-viz. Either update that consumer in this PR or defer removing the edge.

- sync-verify: collapsed-shape KCs now Merkle-verified on sync (self-map in
  both duplicated impls, multi-map roots, dual-shape dedupe guard) — a wrong
  merkle root on a collapsed KC is now rejected instead of accepted on trust.
- node-ui receipt: replaced the never-binding lifecycle join with the
  member-entity join pinned by the URN/URI tail correspondence; the identical
  latent bug in the legacy hop fixed too; real executed-query tests added.
- multi-root access: conditional collapse — multi-root KAs re-emit per-token
  pairing rows (manifest order) alongside the collapsed shape, so legacy
  <ual>/<n> resolves exactly root N with matching attestation; single-root
  publishes (dominant case) keep the full collapse. Handler serves the first
  root that has a private bag; F3 recompute guard retained.
- graph-viz wasGeneratedBy: documentation-only correction (generic matcher,
  no stranded feature) — RFC + inline comment amended.

Tests: agent unit + 9 sync-path hardhat suites green; publisher full 1232;
node-ui full 1423 (+ new executed-query tests); graph-viz 140; builds green.
Full agent lane delegated to CI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@branarakic

Copy link
Copy Markdown
Contributor Author

All four review comments verified independently against the code, then fixed in 09fc5ec8:

  1. sync-verify (valid — the important one): collapsed-shape KCs are now Merkle-verified on sync. Both duplicated implementations (sync-verify-worker-impl.ts + dkg-agent-utils.ts) self-map bare-UAL subjects (keyed on merkleRoot-bearing subjects so lifecycle/SWM rows can't mint bogus KCs), kaRootEntity is a multi-map, and the dual multi-root shape is deduped so good data isn't double-counted. A collapsed KC with a wrong root is now rejected (was: accepted on trust). New 9-test suite runs both impls against shared fixtures and asserts they agree.

  2. node-ui receipt (valid): the ?lc dkg:rootEntity ?asrt join indeed never binds — replaced with the member-entity join pinned to the assertion via the URN :{addr}:{name} ⟷ URI /assertion/{addr}/{name} tail correspondence. The same never-binding pattern existed in the legacy hop-2 query and is fixed identically. New tests execute the real query builders against a real store populated by the real writers — closing the gap that let this ship.

  3. multi-root access (valid): adopted conditional collapse — when distinct member roots > 1, the per-token pairing rows (rootEntity/partOf/privateTripleCount/privateMerkleRoot, manifest order) are re-emitted alongside the collapsed shape in all four writers, so legacy <ual>/<n> resolves exactly root N with root N's attestation. Single-root publishes (the measured dominant case) keep the full collapse. The handler also now serves the first root that actually has a private bag rather than denying on an engine-arbitrary binding, and the recompute-attestation guard stays.

  4. graph-viz wasGeneratedBy (partially valid): the matcher at provenance-resolver.ts:79 is a generic predicate display, not a feature dependency — nothing strands. Recorded as a documentation correction in the RFC (same treatment as the kafka publishedAt correction) rather than restoring the write.

Suites: publisher full 1232 ✅, node-ui full 1423 ✅ (incl. new executed-query tests), agent unit + all 9 sync-path hardhat suites ✅, graph-viz 140 ✅. Full agent lane delegated to CI on this push.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex review skipped: filtered diff is 5723 lines (cap: 5,000). Please consider splitting this into smaller PRs for reviewability.

?ka <${PART_OF}> <${ual}> .
{ ?ka <${PART_OF}> <${ual}> . ?ka <${ROOT_ENTITY}> ?root . }
UNION
{ <${ual}> <${ROOT_ENTITY}> ?root . }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This single-UAL query needs the same dedupe as the list path. Pre-trim rows can satisfy both UNION arms because the bare UAL carried aggregate dkg:rootEntity rows while <ual>/<n> also carried dkg:partOf + rootEntity. buildListQuery hides that with SELECT DISTINCT ?ual ?root ?receivedAt, but here every data triple is returned twice, and bindingsToKa() turns scalar properties into duplicate arrays (e.g. schema:name: ["demo", "demo"]) after handleGetSingle adds the UAL back. Please make this SELECT DISTINCT ?root ?p ?o or dedupe before bindingsToKa().

OPTIONAL {
?lc dkg:rootEntity <${entityIri}> ;
dkg:reservedUal ?ual .
FILTER(STRENDS(STR(?lc), CONCAT(":", REPLACE(STRAFTER(STR(?asrt), "/assertion/"), "/", ":"))))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only pins the lifecycle row by {addr,name}. For a sub-graph assertion such as did:dkg:context-graph:cg/demo/assertion/0x.../name, STRAFTER(..., "/assertion/") drops the demo segment, so this filter also matches urn:dkg:assertion:cg:0x...:name or urn:dkg:assertion:cg:other:0x...:name whenever those lifecycle rows stamp the same member entity. That gives the UI the wrong reservedUal/agent/batchId for same-named root/sub-graph assertions. The new tests cover a different assertion name, but not the same-name/different-subgraph case. Please derive the expected lifecycle URI from the full assertion scope before /assertion/ (including optional subGraphName) before binding ?ual.

@branarakic

Copy link
Copy Markdown
Contributor Author

Coordination note: this PR is the data-volume half of the #1138 scalability wave

For reviewers / whoever sequences these into main: #1155 (per-KA _meta trim) and the integration/issue-1138 wave (Jurij's A/B/C tracks) share one diagnosis — the SELECT DISTINCT ?g / STRSTARTS(STR(?g)) full-store scans that dominate idle-node CPU. We reduce quads-per-graph; they reduce how often/how broadly graphs are enumerated. They compose cleanly — and #1150 (GraphSetIndexStore) + #1151 (CG-meta projection) effectively build out this PR's RFC Part 2, so #1155 stays scoped to the shape trim.

A semantic cross-check across the wave confirmed nothing reads a _meta predicate or graph shape this trim changed, and nothing re-adds bookkeeping we removed. Two items need active coordination when these land together (whoever rebases second on the shared file owns the fix):

  1. A1 — sync responder: bound-graph page serving (the ~95% win) #1141 (A1 sync) — sync-handler.ts / graph-plan.ts. This PR adds a third UNION arm for the collapsed KA shape (?ual dkg:rootEntity ?re ; dkg:batchId ?bid, no partOf) to the durable delta query. A1 re-homes that query into durableDeltaWhereClauseForGraphs carrying only the legacy partOf arm, so a collapsed single-root KA stops binding in delta-sync. Benign today (delta is flag-off via A5 — delta sync activation (flagged off) #1149/A5), but the arm must be re-ported before DKG_SYNC_DELTA defaults on. Detailed on A1 — sync responder: bound-graph page serving (the ~95% win) #1141.

  2. fix: resolve all 14 issues from rc.17 fresh-main QA validation (#1093–#1106) #1107 (pre-mainnet QA) — finalization-handler.ts + the SWM path. This PR's minimal per-cgId partition moves dkg:status to the label _meta graph (isAlreadyConfirmed reads both), and adds a self-map so sync still Merkle-verifies the collapsed bare-UAL shape (sync-verify-worker-impl.ts + dkg-agent-utils.ts). fix: resolve all 14 issues from rc.17 fresh-main QA validation (#1093–#1106) #1107's bug: already-subscribed core node never receives a freshly published KA (VM divergence) and its random-sampling loop spams kc-not-synced/KCNotFoundError every 5s #1098/bug: SWM clear-after-publish does not propagate — peers that catch up afterwards resurrect SWM content the publisher cleared #1099 SWM-drain assume the pre-trim partition shape — reconcile the status/SWM graph location and re-run the SWM merkle-verify + late-joiner convergence checks against the collapsed self-map after rebase. Detailed on fix: resolve all 14 issues from rc.17 fresh-main QA validation (#1093–#1106) #1107.

Everything else in the wave is clean rebase friction or complementary build-out.

@Jurij89

Jurij89 commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Agreed with the coordination framing — #1155 (quads-per-graph) and the integration/issue-1138 A/B/C tracks (how often/broadly graphs are enumerated) are complementary halves of one diagnosis, and C1 (#1150) / C2 (#1151) build out this PR's RFC Part 2.

Proposed sequence (so the wave rebases once, against the final data shape):

  1. Land Per-KA metadata trim: ~134 → ~50 quads/KA (RFC + Phases 0–3 implementation) #1155 to main first — it's the data foundation everything else scans, and it's ready.
  2. Rebase integration/issue-1138 onto the new main.
  3. A1's outstanding durable-meta + SWM query rewrites then target the trimmed shape with read-both.

One scoping clarification so nothing falls between the two efforts: validating A1 (#1141) against the production oxigraph-server backend (not the in-memory embedded store its tests use) showed the durable-meta page is still O(meta×lifecycle) on the server — the per-row 3-arm UNION EXISTS in readDurableMetaRowsPage (graph-plan.ts), >60 s/page on a 1,000-lifecycle _meta. This PR's trim reduces the rows that query scans (~2.7×) but doesn't change the query shape, so that rewrite stays A1-owned (read-once + code-side admission filter + snapshot cache, same pattern as durable-data). Flagging it so it isn't assumed covered by the trim.

Coordination points confirmed (whoever rebases second owns): (1) re-port the collapsed-shape arm into A1's graph-plan delta query before DKG_SYNC_DELTA defaults on; (2) #1107 reconcile the status/SWM graph location + re-run the collapsed self-map merkle checks.

The two ka-extractor regression-lock tests asserted a FULL-collapse shape
(zero <ual>/N token rows) that #1155 abandoned. Root cause is a two-commit
contradiction within #1155: 369042c rewrote source+tests to full collapse,
then the Codex-review follow-up 09fc5ec added the CONDITIONAL collapse back
to the source (metadata.ts:900-919 — multi-root re-emits per-token rows in
manifest order alongside the collapsed UAL-subject set) but left these two
tests at the interim expectation.

The source is correct and load-bearing (required by the green
multi-root-token-rows suite + AccessHandler per-root private-bag resolution);
only the tests were stale. Restored the GENUINE invariants against the actual
conditional-collapse shape — not weakened:

- bug 2 (manifest order, N=12): assert <ual>/i dkg:rootEntity urn:new:i AND
  partOf <ual> for i in 1..12 (one binding each) — catches a lex-sort
  regression (<ual>/10 -> urn:new:2). Kept the collapsed UAL-subject set check.
- bug 3 (surplus deletion, 5->2 shrink): assert exactly {<ual>/1,<ual>/2}
  survive as partOf tokens, <ual>/3..5 are fully gone (the real surplus lock),
  tokens bind urn:new:a/b in manifest order, UAL subject = {urn:new:a,urn:new:b}.

Verified: both restored asserts proven to FAIL under source mutation
(lex-sort -> bug2 red; drop surplus-delete -> bug3 red). Single-root collapse
and merkle leaves unaffected (leaves key on distinct root URI, not token).
No source changed. random-sampling 62/62 green (clean rebuild).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex review skipped: filtered diff is 5740 lines (cap: 5,000). Please consider splitting this into smaller PRs for reviewability.

@branarakic

Copy link
Copy Markdown
Contributor Author

CI blocker fixed: random-sampling ka-extractor (Codex bug 2/3) — 51ef2cae8

The Kosava: random-sampling + kafka-plugin (real Hardhat) lane was red on 2 ka-extractor tests:

  • preserves manifest root order for ≥10 batches (Codex bug 2)
  • deletes surplus kaSubjects when an update shrinks the root count (Codex bug 3)

Root cause — a two-commit contradiction inside this PR, not a source bug. 369042c68 (Phases 0-3) rewrote source and these tests to a full-collapse shape (zero <ual>/N token rows). The Codex-review follow-up 09fc5ec81 then added the conditional collapse back to the source (metadata.ts:900-919 — a multi-root update re-emits per-token rows in manifest order alongside the collapsed UAL-subject set, needed for AccessHandler per-root private-bag resolution), but left these two tests at the abandoned interim expectation. So the source is correct and load-bearing (the green multi-root-token-rows suite + AccessHandler depend on it); only the tests were stale.

Fix is test-only — invariants restored, not weakened. The two tests now assert the genuine regression locks against the actual conditional-collapse shape:

  • bug 2: each re-emitted <ual>/i dkg:rootEntity urn:new:i (+ partOf <ual>) for i in 1..12, one binding each — catches a lex-sort regression (<ual>/10 → urn:new:2).
  • bug 3 (5→2 shrink): exactly {<ual>/1,<ual>/2} survive, <ual>/3..5 fully gone (the real surplus-deletion lock), tokens bound in manifest order, UAL subject = {urn:new:a,urn:new:b}.

Both restored assertions were proven to fail under source mutation (inject lex-sort → bug 2 red; drop the surplus delete → bug 3 red), so they're real locks, not vacuous passes. Single-root collapse and merkle leaves are unaffected (leaves key on distinct root URI, not token subject). No source files changed. random-sampling 62/62 green on a clean rebuild.

Note for whoever sequences the merge: the PR body's "random-sampling 62 ✅" predated 09fc5ec81 — that commit's conditional re-emit is what diverged the two interim tests. With this push the lane should go green; #1155 is then clear to land as the data foundation for the integration/issue-1138 wave to rebase onto.

🤖 Generated with Claude Code

…h scope

Addresses the two open review threads on PR #1155 (both read-both /
collapsed-shape correctness):

1. kafka-plugin discovery single-UAL query (discovery.ts): on a pre-trim /
   mixed-version store both meta-block UNION arms (legacy <ual>/<n> partOf token
   + collapsed bare-UAL rootEntity) bind the same ?root, returning every data
   triple twice -> bindingsToKa produced duplicate-array scalars
   (schema:name: ["demo","demo"]). Add SELECT DISTINCT, mirroring buildListQuery.

2. node-ui receipt lifecycle filter (useEntityOnChainReceipt.ts seal query):
   pinned the lifecycle URN by only :{addr}:{name}, so a same-named assertion in
   a sibling sub-graph leaked the wrong reservedUal/agent/batchId. Pin to the
   FULL assertion scope. cgId is a build-time constant, so anchor on it as a
   literal (preserving slashes — wallet-scoped cgIds like <addr>/<name> embed RAW
   in the URN via assertionLifecycleUri) and only '/'->':' convert the
   [/sub]/{addr}/{name} tail derived from ?asrt.

Regression tests (proven red-without-fix):
- kafka: single-UAL builder asserts SELECT DISTINCT + bindingsToKa scalar lock.
- node-ui: real-store receipt-subgraph-scope.test.ts — cross-subgraph (red under
  :{addr}:{name}-only) AND wallet-scoped slash-cgId (red under a blanket '/'->':'
  that rewrites the cgId's own slash); root + sub-graph, no-slash + slash.

kafka discovery 49/49, node-ui receipt 9/9 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex review skipped: filtered diff is 6108 lines (cap: 5,000). Please consider splitting this into smaller PRs for reviewability.

@branarakic

Copy link
Copy Markdown
Contributor Author

Both open review threads addressed — 8ed33eb7c

  1. kafka-plugin/src/discovery.ts — single-UAL dedupe. Added SELECT DISTINCT ?root ?p ?o to the single-UAL query (mirrors buildListQuery's inner SELECT DISTINCT). On a pre-trim/mixed-version store both meta-block UNION arms (legacy <ual>/<n> partOf token + collapsed bare-UAL rootEntity) bind the same ?root, so each data triple returned twice → bindingsToKa made scalar props into duplicate arrays. Regression test asserts the projection is DISTINCT + a bindingsToKa scalar lock.

  2. useEntityOnChainReceipt.ts — receipt sub-graph scope. The seal query now pins the lifecycle URN to the full assertion scope (cg + optional subGraphName), not just :{addr}:{name}, so a same-named assertion in a sibling sub-graph no longer leaks the wrong reservedUal/agent/batchId.

    ⚠️ One subtlety worth flagging: the first pass derived the cg from ?asrt with a blanket REPLACE('/'→':'), which broke wallet-scoped cgIds (<addr>/<name>) — assertionLifecycleUri embeds the cgId raw, so the slash must stay a slash, and the over-conversion made the join bind nothing (empty UAL/agent for every wallet-scoped CG). An adversarial check caught it. The committed fix anchors on the literal cgId (a build-time constant, slashes preserved) and only /: converts the [/sub]/{addr}/{name} tail from ?asrt.

    The new receipt-subgraph-scope.test.ts drives the real buildSealReceiptQuery against a real OxigraphStore seeded by the real writers, covering cross-subgraph and wallet-scoped slash-cgId, root + sub-graph — proven red without the fix (cross-subgraph red under :{addr}:{name}-only; slash-cg red under the blanket replace).

kafka discovery 49/49, node-ui receipt 9/9 green. Both threads can be resolved.

🤖 Generated with Claude Code

@branarakic branarakic merged commit 6731f43 into main Jun 13, 2026
42 checks passed
Jurij89 pushed a commit that referenced this pull request Jun 13, 2026
…1138

Brings the data-volume half (#1155) under the A1+A2 access-pattern work.
Single conflict: sync-handler.ts durable-data else branch — took A1's
structure (inline delta query was re-homed to graph-plan.readDurableDataPage);
#1155's read-both collapsed-KA arm is already present in A1's
durableDeltaWhereClauseForGraphs (legacy partOf arm + collapsed
rootEntity+batchId arm), so the coordination re-port is satisfied.
#1155 preserves the sync _meta predicates (memoryLayer/assertionGraph),
so A1 durable-meta admission + D-SEC are unaffected. Builds green (tsc 8/8).
Jurij89 pushed a commit that referenced this pull request Jun 14, 2026
…hygiene (#1143)

Squash-merge of codex/issue-1138-a3-sync-scheduler-progress (15 commits)
onto integration/issue-1138 (A1+A2+#1155), plus one stale-test fix.

A3 makes the sync requester/scheduler honest about failure:
 - separates peer-reachability failures (failedPeers) from phase failures
   (failedPhases); the lifecycle success-gate requires both zero, so a
   failed/timed-out/denied round no longer stamps success → backoff engages
 - per-(peer, contextGraph) progress accounting (defines a 'clean round')
 - freshness-aware durable checkpoints; metadata-only freshness is not
   counted as data progress
 - flap hygiene: cooldowns/backoff survive connection:close

Validation (worktree dkg-a1-verify, integration is not CI-gated):
 - build + tsc clean (agent/cli/node-ui)
 - agent suite 1361 passed / 0 failed; node-ui 1431/0
 - cli 82 fails are pre-existing Windows-host env (identical on base) —
   A3's own new cli tests pass
 - server-backed responder perf guard 3/3 (A3 doesn't touch the responder)
 - 4-lens adversarial review: 0 confirmed issues

Stale-test fix: swm-snapshot-sync 'remote snapshots unavailable' is a phase
failure, not a peer failure → assert failedPeers=0 + failedPhases=1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants