Skip to content

feat(ka-routes)!: remove /api/assertion — unify on /api/knowledge-assets (→ main)#1041

Closed
branarakic wants to merge 59 commits into
mainfrom
feat/ka-routes-main
Closed

feat(ka-routes)!: remove /api/assertion — unify on /api/knowledge-assets (→ main)#1041
branarakic wants to merge 59 commits into
mainfrom
feat/ka-routes-main

Conversation

@branarakic

Copy link
Copy Markdown
Contributor

main-targeted version of #1039. Same migration (remove the legacy /api/assertion/* HTTP surface; unify every assertion-lifecycle op on /api/knowledge-assets/*), reconciled onto main instead of integration/v10-devnet, with the one gap that reconciliation surfaced now fixed.

#1039 targets integration/v10-devnet (where it was developed). Since main is the active line and has caught up on the KA hardening, this PR retargets the work to main. Recommend landing this and closing #1039.

What's here

  • The full migration: −3909-line assertion.ts deleted; /api/kc chain-reads preserved in a new kc-chain-metadata.ts; all assertion-lifecycle ops on /api/knowledge-assets/*; ~80 consumers repointed.
  • The reconciliation onto main — 14 conflicts resolved (consensus files byte-identical between integration and main → zero sync-risk; knowledge-assets.ts = migration + main's Fix oversized SWM promote failures and UI loading hangs #1007 413-handler; assertion.ts stays deleted; main's test-suite splits accepted).
  • fix(node-ui): the last consumer gapui/api.ts (the browser client) still had 4 live /api/assertion calls on main (import-file, create, promote, extraction-status); repointed to the KA surface. Audited: no live /api/assertion callers remain anywhere in source.

Verification

⚠️ Breaking

/api/assertion/* no longer exists. All first-party consumers are repointed in this PR. External callers must move to /api/knowledge-assets/* (mapping in docs/migrations/assertion-to-knowledge-assets.md).

🤖 Generated with Claude Code

Branimir Rakic and others added 30 commits June 3, 2026 23:40
Bring the design source-of-truth into the working tree so it travels
with the code and is linked from PRs (per the v10 build plan). These
mirror dkgv10-spec PR #122.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ny entity count (OT-RFC-44)

Removes the entity-count == KA-count conflation that blocked multi-entity
publishes ("V10 publish requires exactly one root entity per request").

- dkg-publisher.ts: all entities of a file become members of ONE KA
  (shared tokenId), kaCount sent on chain is 1 (matches the contract's
  knowledgeAssetsAmount==1), and the kaCount!==1 guard is removed.
- metadata.ts generateKCMetadata: group entries by KA → emit one
  KnowledgeAsset node with N dkg:rootEntity members + summed triple
  counts (RFC-43 §7 <UAL/1> shape); per-entity private roots attach to
  the single node. Single-entity and distinct-tokenId behavior unchanged.
- storage-ack-handler.ts: the receiver no longer refuses to ACK a
  multi-entity KA — replaced rootSubjects.size==kaCount with kaCount==1
  + entity-count bijection. This was the silent cross-node failure in
  OT-RFC-43 §2.7.
- node-ui: relax all four single-root publish guards (api.ts,
  layer-widgets collectPublishRoots/fetchSwmPublishRoots, MemoryLayerView).
- metadata.test.ts: add a Design B test (N entities, one tokenId -> one
  KA node with N members, summed counts).

Build green across core/storage/chain/query/publisher; publisher unit
(96) + metadata (75) + storage-ack (14) tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e cross-node ACK path (OT-RFC-44)

The cross-node canary (agent/test/e2e-publish-protocol: a 3-node, 3-entity
publish→ACK→finalize) surfaced that the first Design B pass was incomplete:
removing the publisher's kaCount!==1 guard was necessary but not sufficient.
Three more sites assumed entity-count == KA-count and silently blocked a
multi-entity KA from being ACKed by a separate node (OT-RFC-43 §2.7 — the
exact failure single-node tests can't catch):

- dkg-publisher.ts: remove the second guard (MultiRootPublishNotAtomicError)
  in the shared-memory publish path. N roots -> ONE KA in ONE tx is atomic,
  which is what that guard was protecting (V8/V9 mapped N roots -> N txs).
- cli/routes/memory.ts: remove the synchronous-publish 409
  (MULTI_ROOT_PUBLISH_NOT_ATOMIC) — the daemon now publishes N roots as one KA.
- storage-ack-handler.ts (THE deep one): the receiver recomputed
  verifiedKACount = rootSubjects.size (= N) and signed an ACK digest with
  kaCount=N, while the publisher and contract both use kaCount=1. So a
  multi-entity KA's receiver ACKs never validated -> quorum unmet -> publish
  failed cross-node. Fixed to the Design B invariant kaCount=1 (data
  integrity is already proven by the SWM merkle-root check).

Tests updated to Design B (these encoded the old single-root conflation):
- storage-ack-handler.test.ts: 2-entity ACK signs kaCount=1.
- dkg-publisher.test.ts / publisher-evm-e2e.test.ts: multi-entity publish
  now succeeds as ONE KA (inverted from "rejects").
- shared-memory-publish-boundary.test.ts: multi-root selection publishes as
  one KA (inverted; describe renamed; drop MultiRootPublishNotAtomicError import).
- cli/memory-graph-events.test.ts: route publishes multi-root (inverted 409).

New canary: agent/test/e2e-publish-protocol.test.ts — "Design B: multi-entity
file publishes as one KA, ACKed cross-node" (4 tests, all green).

Verified: canary 4/4; publisher metadata+storage-ack+dkg-publisher+boundary
129/129; cli memory-graph-events 23/23; publisher/agent/cli build closures green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…3 §10.5)

A coherent, layer-explicit resource model for a Knowledge Asset — the
working tree is WM, SWM/VM are remote branches, layer is explicit in every
write path. Purely ADDITIVE: the legacy /api/assertion/* + /api/shared-memory/*
routes are untouched (308 redirects from them are a follow-up).

  POST /api/knowledge-assets                   create KA + open WM draft
                                               (atomic: quads auto-write+finalize;
                                                alsoShareSwm / alsoPublishVm tails)
  GET  /api/knowledge-assets/:name             KA lifecycle state
  GET  /api/knowledge-assets/:name/{wm,swm,vm} per-layer status
  POST /api/knowledge-assets/:name/wm/write    append quads
  POST /api/knowledge-assets/:name/wm/finalize seal (git commit)
  POST /api/knowledge-assets/:name/wm/discard  drop the draft
  POST /api/knowledge-assets/:name/swm/share   advance SWM pointer (promote→share)
  POST /api/knowledge-assets/:name/vm/publish  mint/update on chain

These delegate to the SAME agent lifecycle methods the legacy routes use
(agent.assertion.{create,write,finalize,promote,discard,history},
publishFromFinalizedAssertion), so behavior is identical — only the URL shape
changes. Atomic-create returns 201, or 207 Multi-Status when a create+finalize
succeeds but an opt-in also* tail fails (the sealed assertion is a real,
retryable artifact). Registered in handle-request.ts before the assertion routes.

Identifier note (§10.5.7): addressed by lifecycle NAME + contextGraphId for the
v10.0 floor; minter-namespaced (agent, number) addressing layers on with Option 1
on these same routes.

Verified: cli closure builds clean; new route contract test 10/10 (create,
atomic+autofinalize, 207 partial-failure, write, finalize, share, publish,
status read, pull-from 501, path-ignore).

Follow-ups: wm/pull-from impl (returns 501 now — net-new git-checkout primitive,
§10.5.3), 308 redirects from legacy routes, client-SDK migration, per-layer
status enums.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… §10.5.3)

The net-new "git checkout" primitive for the GitHub-shaped KA API. WM is the
only writable surface; to edit content already in SWM or VM you pull it into a
fresh WM draft, edit, then re-share / re-publish.

- publisher: DKGPublisher.assertionPullFrom(cg, name, agent, 'swm'|'vm', opts).
  Reads the file's member entities from the assertion seal (lifecycle URN,
  dual-reading dkg:assertionRootEntity|dkg:assertionEntity), gathers the source
  layer's quads scoped to those entities + their skolemized children (the same
  filter the publish gather / RS prover use), drops trust/ownership bookkeeping,
  and seeds a fresh WM draft. onConflict guards a dirty draft: 'reject' (default,
  throws WM_DRAFT_CONFLICT) or 'replace' (git force-checkout).
- agent: agent.assertion.pullFrom facade.
- cli route: POST /api/knowledge-assets/:name/wm/pull-from now calls it
  (replacing the 501). 400 on bad layer, 409 on WM_DRAFT_CONFLICT.

Verified: store-backed publisher test (4) — entity-scoped gather excludes
co-resident other-file entities + filters workspaceOwner; reject vs replace
conflict; throws when unfinalized (no sealed entity list). Route test now 12/12
(pull-from success / bad-layer 400 / conflict 409). cli closure builds clean.

Note: pull-from reads the entity set from the last finalize's seal; re-pulling a
draft before finalizing it isn't supported (finalize or discard first).

Stacked on #971.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t (OT-RFC-43 §10.5)

Additive client SDK for the new /api/knowledge-assets surface (#971, #972).
Legacy assertion/* + shared-memory/* methods are untouched for back-compat.

New layer-explicit methods on ApiClient:
  createKnowledgeAsset(cg, name, { quads?, authorAgentAddress?, alsoShareSwm?, alsoPublishVm? })
  getKnowledgeAsset(cg, name)            -> GET  /api/knowledge-assets/:name
  knowledgeAssetWrite / Finalize / Discard / PullFrom   -> .../wm/*
  knowledgeAssetShare                    -> .../swm/share
  knowledgeAssetPublish                  -> .../vm/publish

Verified: cli builds clean; 5 fetch-mocked SDK tests assert URL + method + body
(atomic create with quads/alsoShareSwm, URL-encoded name on write, share→swm/share,
publish→vm/publish, pull-from layer+onConflict, GET with contextGraphId query).

Stacked on #972. Migrating the other clients (mcp-dkg, adapters, node-ui) to these
methods + 308 redirects from legacy routes are separate follow-ups.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-suite CI)

The full sharded publisher suite (Tornado: publisher [1/4] & [4/4]) caught two
issues the per-file test runs missed:

1. Over-strict receiver check. The A1 storage-ack change added an entity-count
   *bijection* (rootSubjects.size === rootEntities.length). But `rootEntities`
   is a selection, not a complete enumeration — in the SWM-fallback branch it is
   the entity filter passed to `loadSWMQuads`, so a caller may legitimately
   declare a subset of the subjects present. The check also tripped on an empty
   `rootEntities: []` (length 0 is not nullish, so the `?? rootSubjects.size`
   fallback compared against 0 and pre-empted the Merkle check). Remove the
   bijection; keep the real invariants — `kaCount === 1` (Design B) and the
   per-entity "declared ⊆ actual" presence loop. Payload integrity is guaranteed
   by the flat-KC Merkle root, not by counting subjects.

2. Pre-Design-B test fixtures. Several StorageACKHandler tests still encoded
   `kaCount = entity count = 2`. Under Design B the receiver signs
   verifiedKACount=1, so those fixtures asserted a digest the handler no longer
   produces. Move them to `kaCount: 1` (one KA, N member entities) and the
   expected-digest arg `2n → 1n`. Collector-side peer simulations updated in
   lockstep so each quorum test stays internally consistent.

Full publisher suite: 72 files, 1117 passed, 6 skipped, 0 failed. tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Correct the KnowledgeAssetsLifecycle + DKGKnowledgeAssets NatSpec/comments
that claimed update authority delegates to curators/PCA agents. The live
runtime (_executeUpdateCore) is owner-only: the EIP-712-attested author
MUST equal ownerOf(kaId). Comment-only; zero bytecode change.

Lands before the OT-RFC-43 Option-1 contract audit so the auditor reviews
a contract whose docs match its code (Plan B B1.4 / sequencing step 1).
Also vendors OT-RFC-43/44/45 into docs/rfcs/ so the design travels with
the code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tity (variant 1a)

Replace the global ++_knowledgeAssetsCounter with caller-supplied packed ids
kaId = (uint160(author) << 96) | uint96(number):

- createKnowledgeAsset takes a kaId param; require((kaId >> 96) == uint160(author))
  so a wallet can only mint within its own (attested) namespace, then
  _safeMint(author, kaId). Fail-fast on reuse via _ownerOf AND a slot-empty
  invariant (merkleRoots.length == 0, audit hardening).
- Drop the global counter; retain its storage slot as an unused gap
  (__deprecated_knowledgeAssetsCounter) so layout is safe under both redeploy and
  in-place upgrade (R3). Deprecate getLatestKnowledgeAssetId (now reverts).
- Thread reservedKaId through PublishParams -> _executePublishCore. reservedKaId is
  deliberately NOT added to the ACK digest: the on-chain namespace require() is the
  authority, so a publisher choosing a different number in its own namespace is
  harmless (avoids R5 storage-node coordination).

Namespace binds to the attested AUTHOR (EIP-712 signer / initial owner), a
deliberate divergence from OT-RFC-43 §7's msg.sender diagram (the two coincide in
the dominant self-publish case).

ABI regenerated (+ chain/abi synced). Test fixtures supply a packed reservedKaId;
getLatestKnowledgeAssetId assertions removed; 5 new B5 cases (packed mint,
namespace-mismatch revert, dup-reservation revert, deprecated getter,
owner-only-after-transfer). 16 evm-module tests green.

NOTE: requires the off-chain reservedKaId supply (allocator -> publish
orchestration -> chain adapter) to function; that T0 integration is the immediate
follow-up (Bucket 2). Until it lands, real-adapter publishes revert
KaIdNamespaceMismatch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…B2 core)

Off-chain allocator that mints deterministic packed kaIds:
- KaNumberStore interface (core) + SqliteKaNumberStore on the shared dashDb
  (node-ui), schema v19 -> v20 adding ka_numbers(author_address PK, next_number).
  allocate() is a single atomic INSERT ... ON CONFLICT DO UPDATE ... RETURNING;
  reconcileFloor() raises the floor (never lowers); durable state, not pruned.
- KaNumberAllocator (agent): allocate(author) -> { kaId, number } packing
  kaId = (uint160(author) << 96) | number entirely in bigint (no Number()), a
  reconciled-before-allocate cold-start guard (RFC-43 §4.5), and a static unpack().
- chain-event-poller: a KnowledgeAssetCreated listener decoding
  number = kaId & ((1<<96)-1) for startup reconciliation.
- Construct store + allocator in the daemon lifecycle. The publish-time
  allocate() call site (T0 stamping) is the deferred Bucket-2 integration.

16 allocator unit tests; node-ui schema-v20 migration tests; node-ui 96 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-RFC-43 §9, B4)

Packed kaIds (and uint64 wire ids) exceed 2^53, so Number()/protoToNumber
silently corrupts them. Route the publish gossip path through full-precision
bigint:
- publish.ts: add bigIntToProtoSafe (low/high split + MAX_UINT64 guard, mirroring
  finalization.ts) and apply it to tokenId/startKAId/endKAId in
  encodePublishRequest; widen those proto fields to number|bigint|Long.
- dkg-agent.ts broadcastPublish: pass raw bigints instead of Number(...).
- gossip-publish-handler.ts: decode tokenId via protoToBigInt (not protoToNumber).
- publish-handler.ts: widen the local protoToBigInt to accept bigint (pass-through)
  so the widened proto fields type-check.

blockNumber stays number|Long (a chain block number, never bigint). Full turbo
build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mcp-dkg, openclaw (OT-RFC-43 §10.5, A5)

#973 added the new /api/knowledge-assets surface to the cli ApiClient only —
so the clean KA API was unreachable from the UI, the MCP server, and the
OpenCLAW adapter. This completes Plan A5 across the remaining first-party
clients, mirroring the cli reference verbatim (same routes, bodies, layer
verbs):

  createKnowledgeAsset / getKnowledgeAsset
  knowledgeAssetWrite / Finalize / Discard / PullFrom   (WM)
  knowledgeAssetShare                                   (SWM)
  knowledgeAssetPublish                                 (VM)

- node-ui (packages/node-ui/src/ui/api.ts): positional-arg functional exports
  over the existing post()/get() helpers, matching the file's style.
- mcp-dkg (packages/mcp-dkg/src/client.ts): args-object methods over
  this.request(), normalizing the context-graph id like the sibling methods.
- adapter-openclaw (packages/adapter-openclaw/src/dkg-client.ts): positional
  methods over this.post()/this.get(); + fetch-mocked contract tests pinning
  the route/method/body for create, write (URL-encoded name), pull-from,
  share and publish.

adapter-hermes is intentionally left out — it is a specialised hermes-channel
adapter (connect/send/persist-turn), not a general KA-publishing client.

Routes are identical to the cli surface (already contract-tested in
api-client.test.ts) and validated again end-to-end on the integration devnet.
typecheck clean across all three packages; openclaw suite 70/70.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…T-RFC-43 §10.4)

The function never partitioned into Knowledge Assets — it skolemizes blank
nodes under their parent entity and indexes the result by entity
(Map<entity, Quad[]>). The "autoPartition" name is the misnomer that led
readers (and patches) to treat the entity index as a list of KAs and conclude
"kill autoPartition", re-entrenching the entity-count == KA-count bug Design B
removes. No behavior change.

- auto-partition.ts: rename the export to skolemizeByEntity; fix the
  misleading JSDoc (it indexes by entity; the skolemization is consensus
  load-bearing, the grouping is just an index). Keep `autoPartition` as a
  @deprecated alias for one release so external test/script consumers migrate.
- index.ts: export both names.
- Migrate ~29 internal call sites (publisher, agent, cli, core) to the new
  name. Tests keep working via the alias.

Verified: agent build closure green; publisher tests (dkg-publisher, metadata,
boundary, pure-functions, canonical-publish-payload, publish-lifecycle,
ka-update) all pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The node-ui, mcp-dkg and openclaw knowledge-assets wrappers only accepted
`subGraphName`, so callers could not reach daemon-supported controls that the
cli ApiClient already exposes. This dropped VM-publish behaviour and blocked
external-signer / pre-signed-attestation flows through the new clean KA surface.

Mirror the cli reference verbatim in all three clients:
- KnowledgeAssetFinalizedPublishOptions ({ clearAfter, publishEpochs,
  publisherNodeIdentityIdOverride }) + finalizedPublishOptionsPayload helper,
  serialized under the `options` key on `.../vm/publish` (clearAfter maps to
  the daemon's `clearSharedMemoryAfter`; bigint override -> decimal string).
- knowledgeAssetFinalize forwards `authorAgentAddress`,
  `preSignedAuthorAttestation` and `schemeVersion`.
- createKnowledgeAsset accepts the same finalize fields and an
  `alsoPublishVm` options object (translated to the daemon body shape).

mcp-dkg/node-ui widen `publisherNodeIdentityIdOverride` to bigint|string|number
since JSON callers cannot send a bigint; the wire payload is still a decimal
string identical to the cli.

node-ui carried the same gap the bot flagged on openclaw/mcp-dkg; fixed here too
so the three first-party clients stay byte-for-byte aligned with the cli surface.

Tests: openclaw dkg-client 74/74 (4 new KA contract cases); new mcp-dkg
knowledge-assets-client contract test 5/5; tsc clean across all three packages.

Co-authored-by: Cursor <cursoragent@cursor.com>
…RFC-43 §10.1)

NOT COMPLETE — safe foundation only; do not open a PR from this yet.

- core/entity-predicate.ts (new): the migration toolkit — DKG_ENTITY /
  DKG_ROOT_ENTITY_LEGACY (+ assertion variants), ENTITY_PRED_ALT SPARQL
  alternation, isEntityPredicate(). Documents the de-dup hazard (dual-write
  means alternation reads double-count -> require SELECT DISTINCT /
  COUNT(DISTINCT)).
- metadata.ts: dual-write the KA/share entity-member predicate (6 emit/delete
  sites) under BOTH dkg:rootEntity and dkg:entity via entityMemberQuads().

Verified: core+publisher build clean; publisher metadata/storage-ack/
dkg-publisher tests 125/125 (legacy readers unchanged, still resolve).

REMAINING (focused follow-up, consensus-sensitive):
- Seal dual-write (assertion-seal.ts assertionRootEntity->assertionEntity) —
  needs verification that adding _meta quads doesn't affect any seal-integrity
  check (merkle is over data, signature over the EIP-712 struct, so expected
  safe — but confirm with the RS-proof test).
- finalization-handler emitter dual-write.
- Dual-READ ~20 consumers (SPARQL alternation + DISTINCT/COUNT(DISTINCT);
  code-equality -> isEntityPredicate). The RS-proof reader (ka-extractor) is
  the critical one — must dedup or leaf reconstruction doubles.
- Tests: dual-read-window (legacy-only data read by new code & vice versa) +
  RS proof on a multi-entity KA with dual-written data.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ters (OT-RFC-43 §10.1)

Finishes RFC §10.1 step 1 (dual-write) on top of the #970 foundation: every
emitter of the entity-list predicate now writes BOTH the legacy and the new
honest name. Readers are deliberately left on the legacy predicate (which is
always present during dual-write) — the reader dual-read + legacy-drop are the
RFC's explicit *later-release* steps (§10.1 steps 2-4) and carry the only
consensus risk (the random-sampling proof path), so they are not rushed here.

  dkg:rootEntity          + dkg:entity          (KA/share member list)
  dkg:assertionRootEntity + dkg:assertionEntity (assertion seal)

- core/assertion-seal.ts: add ASSERTION_ENTITY; buildAssertionSealQuads
  dual-writes both per entity. Adding this _meta quad does NOT affect seal
  integrity (merkle is over data; the EIP-712 attestation signs a fixed struct,
  not the entity-list quads). parseAssertionSealQuads still reads the legacy
  name (always present).
- agent/finalization-handler.ts: the op entity-list cleanup deletes BOTH names.
- (metadata.ts emitters were dual-written in the #970 foundation commit.)

Verified: agent build closure green; core seal test (9, incl. new dual-write
assertion) and publisher metadata test (75, incl. dual-write assertion) pass;
dkg-publisher (incl. Design B) unchanged.

NEXT RELEASE (not in this PR, by design): switch the ~28 reader sites to the
dual-read alternation (core's ENTITY_PRED_ALT) with SELECT DISTINCT /
COUNT(DISTINCT) — especially ka-extractor (RS proof) — gated by the dual-read-
window + RS-proof-on-multi-entity tests; then drop the legacy write.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… deferred seam)

Makes OT-RFC-43 Option-1 deterministic identity work end-to-end (single-node
mint verified on hardhat; multi-node gossip round-trips).

Supply chain (allocator → publish → adapter → mint):
- chain: V10PublishParams.reservedKaId + ChainAdapter.getMaxKaNumberForAuthor;
  the real EVM adapter encodes reservedKaId (fail-loud on missing/wrong-namespace
  before gas) and enumerates KnowledgeAssetCreated-by-author for reconciliation;
  mock honors reservedKaId + tracks per-author max.
- publisher: a structural KaIdAllocator option (no agent→publisher cycle);
  ensureReservedKaId lazily reconciles each author's floor against the chain
  (max(local, chainMax)+1 → satisfies the RFC §4.5 cold-start guard) then
  allocates; allocate-at-publish threads reservedKaId into the mint and asserts
  the on-chain id == the reserved id (no UAL/chain split).
- agent/cli: kaNumberAllocator flows DKGAgentConfig → DKGPublisher; the daemon
  lifecycle now wires it (the void placeholder is gone).

256-bit id wire (OT-RFC-43 §9): a packed kaId = (uint160(author)<<96)|number is
~256-bit and overflows uint64, so the gossip protos now carry KA ids
(tokenId/batchId/startKAId/endKAId) as DECIMAL STRINGS, not uint64 — across
publish/finalization/ka-update/verify + their encode (idToProtoString) and decode
(protoToBigInt handles string) sites. blockNumber/timestampMs/txIndex stay uint64.
This supersedes B4's uint64 widening for the id fields with the real 256-bit fix.

Tests: reservedKaId wired across ~38 real-adapter test files via a shared
in-memory allocator helper; 3 new chain Option-1 tests (deterministic-mint,
replay-revert KaIdAlreadyMinted, namespace-guard); gossip/finalization/lifecycle
round-trip green. Full build green (15 turbo tasks).

Deferred (needs Plan A #971 routes): A2 per-layer pointers + API status enums +
VM-pointer create-vs-update + (agent,number) addressing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts:
#	docs/rfcs/OT-RFC-43-deterministic-ka-identity.md
#	packages/agent/src/gossip-publish-handler.ts
branarakic and others added 21 commits June 4, 2026 16:09
…A store (R3 seam) + cover it (#985)

* feat(evm): route submitProof verification through the per-challenge KA store (R3 seam) + cover it

`RandomSampling.submitProof` read the challenged KA's merkle data from the
contract's bound `knowledgeAssetStorage` singleton, while the challenge record
already carries `knowledgeAssetStorageContract` — the store the challenge was
issued against — which was written but NEVER read. This routes the four
verification reads through `DKGKnowledgeAssets(challenge.knowledgeAssetStorageContract)`
instead.

Today there is exactly one KA store, so
`challenge.knowledgeAssetStorageContract == address(knowledgeAssetStorage)` and
the change is strictly behaviour-neutral. It (1) keeps the recorded field LIVE
so an audit/cleanup pass won't prune it as dead code — re-adding it later would
mean a storage-layout change to the reward-critical RandomSamplingStorage; (2)
makes verification immune to a mid-period Hub re-point of the singleton; and (3)
is the seam a future multi-generation RandomSampling needs under the
no-proxy "versioned storage coexistence" recovery model (R3 / OT-RFC-43): a
challenge issued against generation-N's store MUST verify against that store.
The neighbouring `kaToContextGraph` CG-store read stays on the singleton —
cross-generation CG resolution needs the cutover-time generation registry and
is out of scope here.

Adds the first executing coverage for the V10 submitProof success path (the
integration suite's Proof-Submission block is behind `describe.skip(... OBSOLETE:
V8 stake pipeline)`), using the V10-clean direct-storage setup (createProfile +
ShardingTable.insertNode; no stake — proof verification is stake-independent):
  - Test A: seed KA + public CG + node, create challenge, submitProof succeeds
    (solved=true).
  - Test B (the seam): after the challenge is issued (recording store-A),
    re-point the Hub's DKGKnowledgeAssets to an empty store-B and re-initialize
    RandomSampling, then assert the proof still verifies. Verified RED on the
    pre-edit code (MerkleRootMismatchError — reads empty store-B) and GREEN with
    the seam, so it is a true regression test for the field.

Behaviour-neutrality independently confirmed by adversarial review. 110 passing
across the new tests + the RandomSampling unit/curated/storage suites; no
regressions. (Tests use a single-leaf tree so submitProof(root, []) verifies
trivially, side-stepping a kcTools sorted-pair vs on-chain positional-pair
merkle mismatch that is orthogonal to the store-routing under test.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(evm): pin curation classification per-challenge (R3 seam, curated KAs)

Review on #985 flagged that submitProof rerouted only the KA-store reads
through the per-challenge `knowledgeAssetStorageContract`, while `isCurated`
was still re-derived live from the `ContextGraphStorage` singleton
(`kaToContextGraph` + `getIsCurated`). A ContextGraphStorage generation
cutover (or a Hub re-point to a store where the KA's CG binding differs or is
absent) would reclassify an older curated challenge as public mid-period and
verify it against the wrong (merkle vs ciphertext) root/count pair.

Fix symmetrically with the KA-store pin: record `isCurated` on the Challenge
at issuance in `_generateChallenge` (from the eligible cgId the picker
returned) and read `challenge.isCurated` back in submitProof. submitProof now
resolves BOTH the store and the curation branch from the challenge itself and
no longer consults any live singleton for branch/root selection. The new bool
packs into the existing `solved` storage slot.

Adds Test C to the submitProof seam suite: a curated challenge survives a
ContextGraphStorage cutover (re-point to an empty store + re-init) and still
verifies against the ciphertext root — RED on the pre-fix code (re-derived
public branch reads the decoy merkle root → MerkleRootMismatchError).

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Branimir Rakic <aleatoric@Branimirs-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…<ual> KnowledgeAsset node (#989)

Re-authors the two genuine read/metadata gaps from PR #968 onto integration/v10-devnet,
dropping the 8 commits that would REGRESS #980 (the N-root→1-KA publish-boundary guards,
the startKAId+tokenIdx RANGE tokenId / metadataTokenId compatibility-row scheme, and the
uint64 gossip wire encoding — all deliberately superseded by #980's Design-B + string-id work).

Gap 1 — multi-entity resolveKA (query): resolveKA read only `bindings[0].rootEntity` and
filtered the data query to one root, so a Design-B KA with multiple entities returned only
the first entity's triples. Now collects every `?ka <rootEntity>` binding (dedup, ORDER BY
?ka), filters the data graph across ALL member roots, and returns `rootEntities[]`
(`rootEntity` retained = first member, backward-compat). QueryEngine interface updated.

Gap 2 — aggregate <ual> KnowledgeAsset node (metadata): generateKCMetadata typed the bare
<ual> only as dkg:KnowledgeCollection; the per-row <ual>/N subjects were the only
dkg:KnowledgeAsset nodes. Now also emits an aggregate node on the bare UAL (dkg:KnowledgeAsset
+ summed publicTripleCount + every member tokenId + every member entity, dual-written
dkg:rootEntity/dkg:entity per §10.1). Uses ka.tokenId (NOT the dropped metadataTokenId).
Deliberately OMITS the original `<ual> partOf <ual>` self-edge — it would make the bare node
match `?x partOf <ual>` member enumeration (incl. resolveKA) and double-count members; the
aggregate node is purely self-describing.

Verified: query 260/260; publisher 1143 passed / 0 failed (incl. the two existing Design-B
metadata tests updated for the aggregate node). agent + cli show ONLY failures confirmed
PRE-EXISTING on the clean #980 tip (agent: e2e-security, finalization-handler; cli:
auto-update-versioned-e2e — an environmental `git tag` failure), all unrelated to this change.

Co-authored-by: Branimir Rakic <aleatoric@Branimirs-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…fixes (PR #976 F5–F9) onto #980 (#987)

Re-authors the allocator review-fix commits from PR #976 (991a5a2, 45f665c)
onto integration/v10-devnet, WITHOUT the stacked #975 contract commit
(11b480a) — that commit reverses the deliberate R5 decision (reservedKaId out
of the 4-field AuthorAttestation typehash + mid-struct layout) and is
intentionally NOT included here.

These are consensus-adjacent: a missed historical KCCreated leaves the
per-author allocator floor at 0, so the allocator re-issues an already-minted
number → UAL collision.

- F5: remove the dead `event.type === 'KnowledgeAssetCreated'` poller branch
  (the adapter only ever yields `'KCCreated'`) and fan reconcile out off the
  actually-yielded KCCreated event (handleBatchCreated + handleKACreated).
- F6: number→bigint end-to-end (core KaNumberStore, agent allocator, node-ui
  SqliteKaNumberStore + .safeIntegers(true)) — the per-author number can exceed
  2^53; Number() would silently lose precision and re-issue a minted id.
- F7/F8: allocator zero-address rejection + canonical-author key.
- F9: cold-start guard observes full KCCreated history when watching
  KA-created so markReconciled() stays sound.
- New test: chain-event-poller-ka-created.test.ts.

Reconciliation onto #980 (not in the PR — #980-specific consumers the PR's
base didn't have):
- dkg-publisher.ts: KaIdAllocator interface → bigint; reconcile(author,
  chainMax) passes the bigint straight through (drops Number()).
- dkg-agent-publish.ts: same Number(chainMax) → chainMax drop.
- test mocks (publisher + agent test/_helpers/ka-allocator.ts): bigint,
  replacing Math.max (throws "cannot mix BigInt") with a bigint comparison.

Verified: publisher full suite 1147 passed / 0 failed; agent 1214 passed — the
2 remaining failures (e2e-security, finalization-handler) are confirmed
PRE-EXISTING on #980 (identical on the clean tip) and unrelated to this change.
Targeted allocator(23) / db(78) / chain-event-poller-ka-created(4) all green.

Co-authored-by: Branimir Rakic <aleatoric@Branimirs-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tes source before dropping the draft (#992)

assertionPullFrom had a latent bug: it read the file's member entities from
`assertionLifecycleUri` (urn:dkg:assertion:...), but finalize stamps the seal
under `contextGraphAssertionUri` (the assertion-graph URI / wmGraph) — a
DIFFERENT subject. So the entity lookup found nothing and pull-from threw "No
sealed entity list" for EVERY properly-finalized assertion. The existing tests
masked it by seeding entities under the lifecycle URN (matching the buggy read).

- 335e8d8: read the seal from the assertion-graph URI via `parseAssertionSealQuads`
  (the same subject finalize writes); require a real finalized seal.
- 335e8d8: VALIDATE the source has content (`PULL_FROM_EMPTY_SOURCE`) BEFORE
  dropping the WM draft — a drop-before-validate could destroy an open draft when
  the source turned out empty.
- a6740ac: a sub-scoped VM pull reads the SUBGRAPH's data graph (`subGraphUri`),
  not the root data graph; `ensureSubGraphRegistered` instead of a pure name check.

Tests: rewrote `sealEntities` to build a REAL seal under the assertion-graph URI
via `buildAssertionSealQuads` (as finalize does) — the prior helper wrote under
the lifecycle URN, validating the broken read. Added a validate-before-drop test
(empty-source replace pull preserves the dirty draft).

Verified: assertion-pull-from 5/5; publisher full suite 1144 passed / 0 failed.
agent + cli show only failures confirmed PRE-EXISTING/flaky on the clean #980 tip
(e2e-security, finalization-handler; publish-jsonld passes 32/32 isolated — a
chain-backed full-suite flake; cli auto-update-versioned-e2e env `git tag`).

Follow-up: #972's reopen-event refinements (246012a/16006dc — PullFromPreconditionError
409 + suppress the spurious create event on reopen) are smaller observability
deltas, deferred.

Co-authored-by: Branimir Rakic <aleatoric@Branimirs-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ter + VM marker flip (#986)

* fix(node-ui): promote file-imported WM assertions (recognize the lifecycle-URN memoryLayer marker)

The WM "Promote" button no-op'd ("0 triples promoted") for file-imported
assertions in any context graph, even though their content was fully
promotable. Root cause: a marker-form mismatch.

Every create path stamps the canonical `dkg:memoryLayer "WM"` lifecycle
marker on the lifecycle URN (`urn:dkg:assertion:<cg>:<agent>:<name>`,
`assertionLifecycleUri`). Regular create ALSO stamps a data-graph-URI
marker; a FILE IMPORT stamps ONLY the URN form. The UI's
`listAssertions('wm')` parser accepted ONLY the data-graph-URI form
(`startsWith(cgPrefix)` + `/`-split), so it silently dropped every
file-imported WM assertion → the bulk-promote loop iterated zero times.

Fix #1 (packages/node-ui/src/ui/api.ts): the WM `listAssertions` parser
accepts BOTH marker forms (the canonical lifecycle URN + the data-graph
URI), deduping by (subGraph, name). Names may contain ':', so the 0x agent
address is peeled deterministically.

Fix #2 (packages/publisher/src/dkg-publisher.ts): `assertAssertionDataPersisted`
also reads `memoryLayer` via the `dkg:assertionGraph` back-link, so the
"already promoted (SWM/VM) -> harmless no-op" guard recognizes
file-imported assertions (URN-only marker) on a re-promote instead of
misfiring `AssertionNotPersistedError`.

Verified: reproduced the drop on a real file import (old parser -> [], new
-> [name]); 3 new regression tests in ui-api-pure.test.ts; node-ui
ui-api-pure 38/38, publisher draft-lifecycle 45/45, full build 20/20.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(node-ui): seamless WM→SWM→VM publish — auto-register on-chain + finalize-before-share + per-assertion VM publish

"Publish to Verifiable Memory" failed for multi-entity content ("Shared-memory
publish currently requires exactly one root entity"), and even single-root
content couldn't publish from a UI-created project. Three root causes:

1. VM = the on-chain layer, but the UI creates projects OFF-CHAIN by default
   (CreateProjectModal registerOnChain=false) → finalize/publish errored
   "context graph is not registered on-chain".
2. The Publish button used the legacy single-root shared-memory publish
   (collectPublishRoots throws on >1 root) instead of the Design-B per-assertion
   vm/publish (one KA per file, any entity count).
3. vm/publish requires a FINALIZED assertion, but Promote shared without
   finalizing and moved content out of WM — after which finalize is impossible.

Fixes (packages/node-ui):
- api.ts: add registerContextGraph + ensureContextGraphOnChain (fetch status →
  register if off-chain). The UI now auto-registers a CG on-chain transparently
  before any operation that needs it.
- layer-widgets.tsx LayerActionsWidget:
  - Promote (WM→SWM): ensureContextGraphOnChain → finalize each draft → promote.
    Finalizing here (before content leaves WM) is what makes the shared
    assertions publishable to VM later.
  - Publish (SWM→VM): ensureContextGraphOnChain → per-assertion vm/publish
    (Design B, one KA per file, any entity count) — replaces the single-root
    publishSharedMemory.

Verified end-to-end on devnet (fresh off-chain CG): create → import → Promote
(auto-registers onChainId, finalizes, shares) → Publish → status "confirmed",
one KA, multi-entity. + regression test for ensureContextGraphOnChain.

NOTE for review (A2/B3 / Design-B lifecycle owner):
- Promote now registers the CG on-chain + finalizes (spends gas) — a behavior
  change; a production build may want a cost/confirm prompt.
- The same single-root pattern remains in MemoryLayerView + entities.tsx (other
  publish surfaces) and should get the same treatment.
- Assertions promoted WITHOUT a prior finalize (before this fix, or on an
  off-chain CG) are unrecoverable (finalize needs WM content) — re-import.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(node-ui): list SWM assertions from the memoryLayer marker, not async ShareTransition records

"Publish to Verifiable Memory" reported "Nothing to publish — promote assertions
to Shared Memory first" immediately after a successful promote, even though the
SWM layer clearly held entities.

listAssertions('swm') enumerated `dkg:ShareTransition` records in
`<cg>/_shared_memory_meta`. Those are written by the ASYNC SWM share
(sender-key setup + gossip) and therefore LAG the promote, so a publish fired
right after promoting found zero shared assertions. (They also include the
reserved `meta` system assertion, e.g. project-ontology.)

Fix: enumerate SWM from the canonical `dkg:memoryLayer "SWM"` lifecycle marker
in `<cg>/_meta` — written synchronously by promote, the same source the WM
listing already uses — with the identical URN/data-URI-tolerant parser, dedupe,
and `meta` sub-graph exclusion.

Verified on the devnet: a freshly promoted multi-entity assertion is now found
and publishes to VM (status "confirmed", one KA). + regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(node-ui): apply the publish lifecycle to the Assertions tab too + show the UAL per row

The Assertions tab (AssertionsList) had its OWN promote/publish handlers that
still used the legacy single-root path, so "Publish to VM" on a row threw
"Publish requires one root entity" (and, worse, tried to publish EVERY SWM root,
not the clicked one). Only the layer-summary widget was fixed previously.

- entities.tsx: both the per-assertion (handlePromote) and bulk (handlePromoteAll)
  actions now use the correct lifecycle — Promote: ensureContextGraphOnChain ->
  finalize -> promote; Publish: ensureContextGraphOnChain -> per-assertion
  vm/publish (Design B, one KA per file, any entity count).
- api.ts: fetchAssertionUals(cg) -> { name: reservedUal }; the assertions list
  renders the deterministic Option-1 UAL (did:dkg:evm:<chain>/<author>/<n>) in
  gray under each filename.
- layer-widgets.tsx: removed the now-dead collectPublishRoots /
  fetchSwmPublishRoots single-root helpers.

Verified: per-assertion vm/publish confirmed on-chain; typecheck clean;
ui-api-pure 41/41 (incl. fetchAssertionUals).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(node-ui): drop already-published assertions from the SWM Assertions list

A published assertion lingered in the Shared-Memory list. Root cause is a
backend lifecycle gap: vm/publish records a dkg:vmCurrentAssertion pointer
(+ dkg:kaId) but does NOT flip dkg:memoryLayer off "SWM" / state off "promoted".
generateAssertionPublishedMetadata (which would flip them) is wired into
publishFromSharedMemory but its trigger SPARQL gate (dkg-publisher.ts:1523)
joins on dkg:rootEntity/dkg:agent predicates the lifecycle URN never carries
(it has prov:wasAttributedTo), so the flip never fires. The reliable
"is-published" signal is the presence of dkg:vmCurrentAssertion.

listAssertions('swm') now excludes any assertion carrying a vmCurrentAssertion
pointer (keyed by (subGraph,name) so both marker forms drop), so published
assertions leave the Shared-Memory list as expected.

Verified live (dadaa): published BRANCH_SUMMARY/SPEC/TWO-LAPTOP dropped,
un-published MIGRATE_TO_NPM kept. + regression test. ui-api-pure 42/42.

NOTE (A2/B3 lifecycle owner): the proper fix is backend — make vm/publish flip
memoryLayer SWM->VM + state->published (fix the generateAssertionPublishedMetadata
trigger gate at dkg-publisher.ts:1523). This UI filter is the safe interim.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(agent): publish flips the lifecycle marker to VM (proper root fix)

publishFromFinalizedAssertion recorded the dkg:vmCurrentAssertion pointer + kaId
on a confirmed VM publish but did NOT flip dkg:memoryLayer off "SWM" / dkg:state
off "promoted" — the dedicated published-metadata flip
(generateAssertionPublishedMetadata) never fired because its trigger SPARQL gate
(dkg-publisher.ts:1523) joins on dkg:rootEntity/dkg:agent predicates the
lifecycle record never carries (it has prov:wasAttributedTo). So a published
assertion kept reading SWM/promoted and lingered in the Shared-Memory layer.

Now, on a confirmed/tentative publish, flip dkg:memoryLayer -> "VM" and
dkg:state -> "published" on BOTH the lifecycle-URN and data-graph-URI forms
(promote stamps "SWM" on both), right where the vmCurrentAssertion pointer is
stamped. The VM lifecycle record is now equivalent to the WM/SWM records
(a memoryLayer marker + state) with the extra transaction metadata
(vmCurrentAssertion + kaId + on-chain UAL) layered on top.

Verified live (node1 reloaded): create -> finalize -> promote -> vm/publish ->
the assertion's URN AND data-URI both read memoryLayer="VM", state="published",
+ vmCurrentAssertion. Full build 20/20.

The node-ui SWM-list exclusion (by vmCurrentAssertion) remains as a backstop for
assertions published BEFORE this fix (their marker stays "SWM").

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Branimir Rakic <aleatoric@Branimirs-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…pi/knowledge-assets route (#990)

PR #971's /api/knowledge-assets HTTP surface is largely already consolidated on
integration/v10-devnet (the route, B3 addressing via classifyKaIdentifier/resolveByKaId,
the implemented pull-from, MAX_PUBLISH_EPOCHS in sibling routes, validateAssertionName,
hasPendingSharedMemoryWrites are all present). The genuine, confirmed remaining gap this
lands: the `vm/publish` verb passed `parsed.options` straight into
publishFromFinalizedAssertion with NO validation, so malformed epochs / identity overrides
/ flags surfaced as opaque 500s deep in the publisher, and nonsensical fields
(assertionName, author overrides, partial selection) were silently ignored.

- validateFinalizedAssertionPublishRequest: 400s on assertionName / author overrides /
  non-"all" selection (the URL name + seal already select the assertion and encode the author).
- resolveFinalizedPublishOptions: validates + normalizes publishEpochs|epochs (positive,
  safe-integer, <= uint32 MAX_PUBLISH_EPOCHS), publisherNodeIdentityIdOverride (non-negative
  integer), clearAfter/clearSharedMemoryAfter (boolean); returns the normalized options or
  null after writing a 400.
- Wired into the explicit vm/publish verb ONLY — a standalone request where a 400 mutates
  nothing. Deliberately NOT wired into the alsoPublishVm atomic tail: it runs after
  create+finalize succeed, so a 400 there would abort partial success; its bad options
  correctly surface as a 207 phase error instead.

Re-authored onto #980 (not cherry-picked) — the PR's commits conflict (they revert
skolemizeByEntity->autoPartition and drop kaAllocator/createV10UpdateACKProvider on #980).
Dropped the superseded owner-model (dcd6a69#980 uses classifyKaIdentifier/resolveByKaId)
and the 501-pull-from (7bcdd4a#980 implements pull-from).

Verified: knowledge-assets-route 26/26 (5 new vm/publish validation cases); cli full suite
1924 passed, only auto-update-versioned-e2e fails — confirmed PRE-EXISTING on the clean #980
tip (environmental `git tag` failure), unrelated.

Follow-up (separate, assessed next): the #971 seal-field response enrichment (88f628c) and
operation-tracker wiring (F22) are additional observability deltas, tracked separately.

Co-authored-by: Branimir Rakic <aleatoric@Branimirs-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e (200/207/502) (#991)

PR #972's cbb13fb ("gate KA publish side effects") flagged that #980's vm/publish
returned 200 for ANY publish result. On this SYNCHRONOUS route a non-confirmed
publish is not a normal in-flight state ("no silent tentative downgrade"). The
publish result is a 3-state machine (status: 'tentative'|'confirmed'|'failed')
plus an orthogonal contextGraphError. Mapping (owner decision):

  confirmed, no contextGraphError → 200  (fully done)
  confirmed + contextGraphError   → 207  (partial: KA minted on-chain, context-graph binding failed)
  tentative | failed              → 502  (publish did not confirm)

- Explicit vm/publish verb: returns the mapped status; body carries kaId/ual/
  txHash + contextGraphError + a reason on 207/502.
- Atomic create `alsoPublishVm` tail: only a fully-confirmed publish is
  "vm-confirmed"; a partial/non-confirmed outcome is pushed to the tail errors
  so the atomic response is the existing 207 rather than a misleading success.

(#980 has no `emitPublished` in this route, so cbb13fb's event-gating is moot
here — only the HTTP-status contract changes.)

Verified: knowledge-assets-route 24/24 (3 new 207/502 cases + existing); cli
full suite 1922 passed, only auto-update-versioned-e2e fails — confirmed
PRE-EXISTING on the clean #980 tip (environmental `git tag`), unrelated.

NOTE: overlaps PR #990 (#971 vm/publish input validation) on the same handler —
merge #990 first, then this rebases trivially (validation is at the top of the
block, this is the return). The #972 pull-from publisher-internals chain
(335e8d8 subject-mismatch + validate-before-drop, reopen 409s, VM subGraph
routing) is the remaining #972 work, tracked separately.

Co-authored-by: Branimir Rakic <aleatoric@Branimirs-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…onciliation onto #980) (#984)

* fix: validate KA client payloads

* fix: reject invalid KA create payloads

* fix: harden KA client option payloads

* fix: align KA client validation

* fix: reject unknown KA publish options

* fix: validate MCP KA publish options

* docs: narrow entity predicate migration contract

* fix(cli): enforce mutually-exclusive author fields in knowledgeAssetFinalize

Review on #984 flagged that the finalized-publish validation drifted across the
CLI / MCP / OpenClaw / node-ui SDK clients: knowledgeAssetFinalize gained the
self-sign-vs-external-signer mutual-exclusion precheck on every surface EXCEPT
the CLI ApiClient, which forwarded a conflicting
{authorAgentAddress + preSignedAuthorAttestation} body to the daemon instead of
rejecting it client-side like the others.

Reconcile the drift by calling assertExclusiveAuthorFields in the CLI's
knowledgeAssetFinalize too, so all four clients enforce the same contract.
Adds a regression test asserting the conflict throws before any HTTP call.

(The broader DRY suggestion — hoisting these helpers into a shared package —
is a deliberate cross-package refactor better done on its own, not folded into
this land PR.)

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: revert accidentally-committed localhost deployment artifact

The previous commit's `git add -A` swept in a hardhat localhost deployment
JSON regenerated by a local turbo build. It is unrelated to the SDK fix —
restore it to its prior state.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Branimir Rakic <aleatoric@Branimirs-MacBook-Pro.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
…utes (#988)

* feat(agent-tools): migrate assertion lifecycle tools to the v10 KA routes

The v10 KA memory refactor (PR #980) added the /api/knowledge-assets/*
lifecycle and KA client methods, but left every agent-facing tool on the
legacy /api/assertion/* + /api/shared-memory/* routes. This repoints the
assertion lifecycle tools onto the new KA client methods in both agent
surfaces, keeping tool names unchanged so existing agent prompts/skills
keep working.

Mapping (same engine calls underneath; per-name, like the KA routes):
  create   -> createKnowledgeAsset      (POST /api/knowledge-assets)
  write    -> knowledgeAssetWrite       (.../wm/write)
  promote  -> knowledgeAssetShare       (.../swm/share)
  discard  -> knowledgeAssetDiscard     (.../wm/discard)
  history  -> getKnowledgeAsset         (GET /api/knowledge-assets/:name)

Left as-is (no KA equivalent yet, like the import/enrichment aux tools):
  dkg_assertion_query  -> queryAssertion  (the KA GET returns lifecycle
  state, not a quad dump, so the quad-dump tool stays on /api/assertion)

Notes:
- create preserves the legacy idempotent contract by catching the KA
  route's "already exists" error.
- openclaw write keeps the publisher's N-Triples literal escaping
  (normalizeDkgPublisherQuads) before handing off to the daemon.
- history drops the per-author agentAddress filter: the KA GET surface is
  keyed by (contextGraph, name).
- knowledgeAssetWrite's quad `graph` relaxed to optional (the WM-write
  engine derives it; the legacy triples path relied on exactly this).
- publish tools (context-graph-scoped) intentionally untouched — a model
  shift, not a rename — to be handled separately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(agent-tools): align create idempotency with verified KA route behavior

Live-tested the migrated tools against a devnet node (integration branch,
commit 3861487). The KA create route is idempotent AND non-destructive:
re-creating an existing name returns the open WM draft (HTTP 201,
status: draft-open) without clearing its quads — it does NOT error with
"already exists" the way the legacy /api/assertion/create route did.

So the "already exists" catch in both create handlers was dead code.
Removed it; the create tool now just reports "Created" (idempotent,
safe to call repeatedly). Updated the create tool description, the
FakeClient harness (non-throwing get-or-create), and the idempotency
test to assert non-destructive re-create instead of an "already exists"
message.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(agent-tools): address PR review — KA route parity + keep author-scoped history

Resolves the codex review findings on #988.

Claim A/B (error semantics + name validation): the KA daemon routes had only
a blanket 500 catch and no name validation, so user mistakes (bad name,
missing assertion, unsafe/reserved IRI) surfaced as 500s instead of the 400s
the legacy /api/assertion/* routes return. Brought knowledge-assets.ts to
parity: validate the name in the create handler, and route engine errors
through a shared mapper that mirrors assertion.ts (400 for not-found/Invalid/
Unsafe/reserved, 409 for AssertionNotPersistedError, 500 only for the genuinely
unexpected). Added route-level tests covering each mapping.

Claim C (history author scoping): the KA GET surface is keyed by
(contextGraph, name) only and defaults to the local agent, so it cannot fetch
another author's history. Reverted dkg_assertion_history (both mcp-dkg and
openclaw) back to the legacy getAssertionHistory and restored the agentAddress
parameter. Reads now stay on the legacy routes (history + query) until the KA
read surface reaches parity; only the write-side lifecycle migrates to KA.

Create contract: the openclaw create tool documented a duplicate returning
`{ assertionUri: null, alreadyExists: true }`, which the idempotent
non-destructive KA route never produces. Updated the tool description to the
new contract (`{ name, assertionUri, status }`) so the documented behavior
matches reality.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(agent-tools): address 2nd review pass — keep create on legacy, scope KA error mapping

Resolves the follow-up codex findings on #988.

1. Create contract + name validation: the KA create route is an idempotent
   get-or-create that cannot report `alreadyExists` (the engine's
   assertionCreate doesn't surface pre-existence) and lacks the legacy name
   validation. Rather than half-preserve a broken contract, `dkg_assertion_create`
   now stays on the legacy `/api/assertion/create` route in BOTH surfaces —
   keeping the documented `{ assertionUri, alreadyExists }` shape and name
   validation intact. Reverted the speculative `validateAssertionName` addition
   to the KA create route (which also had a non-string-name 500 hazard).

2. Publish error mapping: `respondAssertionError` no longer wraps
   `vm/publish`. On-chain/storage/publisher failures there can carry
   "Invalid"/"Unsafe" text and must stay 500 — publish now has its own
   generic-500 catch, matching the legacy publish path. The 400/409 parity
   mapping applies only to the WM/SWM mutation verbs (write/finalize/discard/
   pull-from/share).

Net migration boundary: write / promote / discard -> KA routes (with 400/409
error parity); create / history / query stay on the legacy routes until the KA
surface reaches parity (alreadyExists reporting, name validation, author-scoped
GET, quad-dump). Added a route test asserting vm/publish stays 500; removed the
now-invalid create-name-validation test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Branimir Rakic <aleatoric@Branimirs-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…y + add wm/quads

First increment of the assertion→knowledge-assets unification (issue: remove
/api/assertion entirely; base integration/v10-devnet; targeting rc.17).

Brings the EXISTING /api/knowledge-assets/* routes to full parity with the
legacy /api/assertion/* handlers, fixing the ambiguity bugs the dual-route
model caused (PR #971/#988 reviews):
- contextGraphId resolution/validation on every mutation (resolveRequiredWrite
  ContextGraphId) — bad/foreign id is a 400, not an opaque 500
- :name decode + validateAssertionName on all verbs + GET
- side-effects restored: emitMemoryGraphChanged + recordAssertionActivity +
  notification SSE on create/write/finalize/discard/share/publish (dashboards
  and the bell pane were going silent under the KA routes)
- full finalize payload (assertionUri/authorAddress/schemeVersion/chainId/
  kav10Address) and full vm/publish payload (assertionUri/authorAddress/
  merkleRoot/kas/blockNumber)
- create now reports `alreadyExists` (idempotent get-or-create); GET accepts
  author-scoped `agentAddress`; strict boolean validation of alsoShareSwm/
  alsoPublishVm; published-activity attributed to the seal author
- NEW route: GET /api/knowledge-assets/:name/wm/quads (replaces the legacy
  POST /api/assertion/:name/query quad dump)

Full migration blueprint (6 missing routes, 30-helper relocation, 35 parity
gaps, ~80 consumers) captured in docs/migrations/assertion-to-knowledge-assets.md.

Typecheck-clean (cli tsc). Route unit-test mocks (listContextGraphs writability)
and the remaining 5 missing routes follow in subsequent Stage-1 increments.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…o shared-assertion-helpers.ts

Moves 30+ helpers (import-artifact resolution, owner-guard, semantic-enrichment
RDF builders, promote-async job views, quad sort) out of assertion.ts into a new
shared module so the new /api/knowledge-assets route ports can reuse them and
assertion.ts can be deleted in Stage 4. assertion.ts re-imports them; behavior
unchanged. Typecheck-clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nt onto KA surface

Adds POST /api/knowledge-assets/import-artifact/{resolve,read-markdown} and
POST /api/knowledge-assets/semantic-enrichment/write as faithful ports of the
legacy /api/assertion/* equivalents (new module knowledge-assets-import.ts,
reusing shared-assertion-helpers). Identical owner-guard (#872 relax on
public+open CGs for reads, strict for enrichment), 400/403/404/409/413 mapping,
response shapes, and the semantic_enrichment_written SSE. Collection-level paths
dispatched before :name parsing. Typecheck-clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n-status onto KA surface

POST /api/knowledge-assets/:name/wm/import-file (multipart) and
GET  /api/knowledge-assets/:name/wm/extraction-status — verbatim ports of the
legacy /api/assertion/:name/{import-file,extraction-status} handlers (same
multipart parsing, MIME inference, per-assertion lock, extractionStatus
lifecycle, two-graph atomic insert, SSE side-effects, 400/409/413/415 mapping).
import-file dispatch is placed BEFORE the JSON safeParseJson preflight so the
multipart stream is read raw. Typecheck-clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ync + share-jobs

Ports all 5 legacy /api/assertion/promote-async* routes onto the KA surface:
  POST   /api/knowledge-assets/:name/swm/share-async         (enqueue)
  GET    /api/knowledge-assets/swm/share-jobs                (list, ?state/limit/contextGraphId)
  GET    /api/knowledge-assets/swm/share-jobs/:jobId         (status)
  DELETE /api/knowledge-assets/swm/share-jobs/:jobId         (cancel)
  POST   /api/knowledge-assets/swm/share-jobs/:jobId/recover (recover)
New module knowledge-assets-async-share.ts; reuses shared promote-job helpers.
Collection job routes dispatched early (incl. new DELETE handling); enqueue
inlined in the SWM section. Faithful 503/400/404/409 mapping + PromoteJobView
shapes. Typecheck-clean.

This completes the 6 missing KA route families — the /api/knowledge-assets
surface is now a full superset of /api/assertion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…w route families)

Updates the route-test harness for the new parity (mock listContextGraphs
writability, ctx side-effect spies, full seal/publish payloads, alreadyExists)
and adds coverage for every new route: wm/quads, import-artifact resolve/
read-markdown, semantic-enrichment/write, wm/import-file (multipart), wm/
extraction-status, swm/share-async, and swm/share-jobs list/status/cancel/
recover (incl. 503 worker-unavailable, agentAddress scoping). 61 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dge-assets

Repoints every /api/assertion/* caller across the codebase to the unified KA
surface (path mapping per docs/migrations/assertion-to-knowledge-assets.md):
- cli api-client.ts (central SDK): all 7 assertion methods → KA; queryAssertion
  changed from POST-body to GET /wm/quads with querystring; promote→swm/share
- mcp-dkg client.ts + adapter-openclaw dkg-client.ts: all assertion methods → KA
  (query POST→GET, history→GET :name, createAssertion reads alreadyExists)
- adapter-hermes Python client.py: all 9 endpoints → KA (py_compile clean)
- node-ui (src/api.ts, ui/lib/ontologyInstall.ts), network-sim (client + the
  server-side sim mock), agent/core doc-strings
- ~14 devnet/test scripts (curl/fetch) repointed; living docs + SKILL.md updated
Historical records (CHANGELOG, RFCs, ADRs, bug reports, async-promote specs) and
unit/e2e test files left for later stages. Every touched TS package typechecks
clean; Python py_compile clean; shell bash -n / node --check clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…client/devnet tests

- cli daemon route-level tests (promote-async-routes, promote-async-daemon-
  lifecycle, promote-route-not-persisted, async-promote-queue-e2e, import-
  artifact-routes, memory-graph-events, context-graph-write-path-validation)
  migrated from handleAssertionRoutes → handleKnowledgeAssetsRoutes at the KA
  paths, preserving every edge case (409 not-persisted, owner-guard 403, 503
  worker-unavailable, validation 400s, SSE operations). Ran green via chain-free
  configs (31+2+4+5+32+23+28).
- adapter-openclaw/hermes + node-ui + agent client tests repointed to KA URLs/
  methods/shapes (openclaw 82+191+58 green); devnet integration tests + bootstrap
  repointed for the final live run.

Also fixes a discard parity gap the migration surfaced: the KA wm/discard handler
now evicts the cached extraction-status record (extractionStatus.delete via
contextGraphAssertionUri) exactly as the legacy discard did, so a re-import after
discard doesn't see a stale "completed".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…/kc reads

Removes packages/cli/src/daemon/routes/assertion.ts (3909 lines) and its dispatch
in handle-request.ts. The /api/assertion/* HTTP surface is gone — everything now
flows through /api/knowledge-assets/*.

Two read-only chain-metadata routes that happened to live in assertion.ts but are
NOT part of the assertion lifecycle (GET /api/kc/:id, GET /api/kc/:id/author) are
extracted verbatim into the new routes/kc-chain-metadata.ts module and dispatched
separately, preserving their exact paths/shapes. kc-author-route.e2e.test.ts
repointed to the new module.

BREAKING: /api/assertion/* routes no longer exist. All first-party clients,
adapters, scripts, docs, and tests were migrated to /api/knowledge-assets/* in
Stages 2-3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rix + rc.17 (#996)

* fix(storage): blank-node-safe delete on SPARQL backends (sparql-http, blazegraph)

`SparqlHttpStore.delete(quads)` (and the identical `BlazegraphStore.delete`)
serialized every quad into a single `DELETE DATA { … }` block. SPARQL forbids
blank nodes in DELETE DATA, so any quad whose subject/object is a blank node
made a spec-compliant endpoint (Oxigraph server, Fuseki, Blazegraph) reject the
whole statement with HTTP 400 ("Variables and blank nodes are not allowed in
DELETE DATA"). This broke "Promote All → Shared" (WM→SWM, assertionPromote →
store.delete of the CONSTRUCT'd source quads) for any entity containing nested/
anonymous RDF — the common case — and exposed every other store.delete(quads)
caller (tentative/finalization cleanup, etc.) on the oxigraph-server backend.

Only the SPARQL-over-HTTP adapters are affected. The embedded OxigraphStore and
oxigraph-worker delete via the native object API (blank-node-safe), which is why
every promote/share unit test (all on OxigraphStore) and the devnet (oxigraph-
worker default) stayed green — while a fresh `dkg init`, which defaults to the
oxigraph-server backend, hit it on real data.

Fix: new `buildBlankNodeSafeDelete()` keeps ground quads on the fast DELETE DATA
path and removes blank-node quads via `DELETE { … } WHERE { … }`, grouping
connected blank-node components (linked by shared labels) into separate
statements and rewriting each blank node to a query variable — the only
spec-legal way to target existing blank-node structure over SPARQL. Per-component
emission avoids cross-product WHERE joins that would silently delete nothing.
Both adapters now share the builder.

Tests (packages/storage/test/sparql-http-blank-nodes.test.ts, 20 cases): a broad
matrix of blank-node shapes (object/subject bnodes, nested entities, RDF lists,
shared bnodes, disjoint components, deep chains, mixed ground+bnode, cross-graph,
the CONSTRUCT→delete promote reproduction) executed against a REAL embedded
Oxigraph engine (same engine oxigraph-server runs) plus the full SparqlHttpStore
HTTP path. A control test asserts the legacy DELETE-DATA-with-bnode form still
throws on that engine. Verified: 20/20 pass on the fix; 16/20 fail on the legacy
behaviour (the suite genuinely gates the regression).

Root-cause of the test gap: the prior sparql-http test used a mock HTTP server
that recorded the update string but never parsed/executed it, so invalid SPARQL
passed. The new suite runs generated SPARQL through a real engine.

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

* chore: bump version to 10.0.0-rc.17

rc.17 ships the blank-node-safe SPARQL delete fix (see previous commit). No
contract changes — the on-chain contracts stay at 10.0.3 and need no redeploy.
Root + all 18 workspace packages 10.0.0-rc.16 → 10.0.0-rc.17. Internal deps are
workspace:* and pnpm-lock.yaml does not pin workspace versions, so frozen-install
CI stays green.

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

* test(storage): run the conformance matrix against the sparql-http backend by default + fix deleteByPattern

Adds the SPARQL-over-HTTP backend (the oxigraph-server default a fresh
`dkg init` gets) to the shared TripleStore conformance matrix, backed by an
in-process endpoint running the real Oxigraph engine — so it executes the
adapter's generated SPARQL (no server binary, runs on every CI run). Also adds
blank-node cases (object/subject bnodes, nested-entity-with-bnode-children =
the WM→SWM promote shape) to the shared suite so EVERY backend is exercised
with blank nodes. This is the matrix entry that would have caught the rc.16
blank-node DELETE-DATA bug.

Wiring sparql-http into the matrix immediately surfaced a SECOND latent bug in
the same class: `deleteByPattern`'s no-graph branch emitted
`DELETE { ?g_ctx { … } }` (missing the GRAPH keyword) — invalid SPARQL,
HTTP 400 on any real endpoint, never executed by a test before. Fixed in both
`sparql-http.ts` and `blazegraph.ts`.

devnet.sh already runs a backend matrix (nodes 1-2 oxigraph-worker, 3-4
blazegraph, 5-6 sparql-http/oxigraph-server) but SILENTLY fell back to the
embedded store when Docker was unavailable — which is how no devnet node ever
ran sparql-http during rc.16 validation. Added a loud coverage summary and an
opt-in `DEVNET_REQUIRE_ALL_BACKENDS=1` strict gate so CI cannot silently shrink
the matrix.

Storage suite: 181 passed / 3 skipped.

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

* fix(pr996): address CodeQL + e2e review feedback

- storage test: stop reflecting the caught exception text in the mock SPARQL
  server's HTTP response (CodeQL: exception-text-as-HTML + stack-trace info
  exposure). The adapter only inspects the 400 status; return a constant
  text/plain body and log the real reason to stderr.
- node-ui: add the `widget-promote-all-btn` / `widget-publish-vm-btn` /
  `layer-action-result` data-testids the WM→SWM UI-cycle spec targets. They
  were missing, so the regression spec's first toBeVisible() would time out and
  the promote flow never ran. Tag only the LayerActionsWidget surface (the one
  the default WM/SWM `items` tab actually renders) to avoid .or() strict-mode
  multi-matches.
- e2e: correct the stale SWM→VM `fixme` rationale — the bulk publish control now
  mints one KA per SWM assertion (Design B), so on the shared seeded CG (~25
  roots) it fires ~25 on-chain mints; single-root publish (PublishPanel) is not
  wired into ProjectView nav. Kept deferred (on-chain mint already covered by the
  API sibling spec) with accurate reasoning.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(pr996): silence CodeQL in the new sparql endpoint helper

The in-process oxigraph SPARQL endpoint added in 25124c2 reintroduced the same
pattern just fixed in sparql-http-blank-nodes.test.ts: its catch block reflected
the caught exception text into the HTTP response, which CodeQL flags as
exception-text-as-HTML (#3984) + stack-trace info exposure (#3985). The
SparqlHttpStore adapter only inspects the 400 status, never the body, so return a
constant text/plain message and log the real reason to stderr.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(e2e): sort WM assertion graphs by name segment, not full URI (PR review)

findWmAssertion sorted candidate graph URIs by the full string. URIs are
.../assertion/<agent>/<name>, so a raw sort orders by the <agent> segment first
and could pick an older assertion from a different agent on a shared devnet,
making the rest of the spec read counts/markers from the wrong graph. Sort by
the trailing assertion-name segment (the stable per-import key) instead.

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

* fix(e2e): poll for CG readiness + isolate adapter-path store per test (PR review)

- wm-swm-vm-ui-cycle.devnet: /api/status answering doesn't mean the seeded
  primary CG has finished registering, so listContextGraphs() can briefly
  return [] on a cold boot and the spec falsely skips. Poll a bounded window
  before the skip-check; a genuinely-empty operator devnet still skips.
- sparql-http-blank-nodes (adapter-path describe): the long-lived endpoint
  served a single shared embedded store across both cases, so leftover data /
  an early failure could leak into the next case. Reset to a fresh store in
  beforeEach for isolation.

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

* test(e2e): drive the WM→SWM→VM UI spec on the production-default oxigraph-server backend (no Docker) (#1014)

* test(e2e): run devnet UI node on the production-default oxigraph-server backend (no Docker)

The devnet's node 1 (the node the UI/e2e suite drives) ran `oxigraph-worker`,
while fresh installs default to `oxigraph-server`. So the UI e2e never exercised
the real backend — and SPARQL-over-HTTP-only bugs like #996 (blank-node
DELETE DATA) could not reproduce in the UI flow. oxigraph-server was only
available via Docker (nodes 5-6), which silently degraded when Docker was
absent — the exact rc.16 trap.

Switch devnet nodes 1-2 to the DAEMON-MANAGED `oxigraph-server` backend: the
daemon auto-downloads + SHA-256-verifies + caches the Oxigraph binary and
spawns it on its own port — NO Docker, cross-platform, production parity (it's
what HariSeldon runs). Keep the Dockerized external Oxigraph on 5-6 as optional
extra coverage; update the backend-matrix summary + DEVNET_REQUIRE_ALL_BACKENDS
gate accordingly.

Add a HARD precondition to wm-swm-vm-ui-cycle.devnet.spec.ts: assert node 1's
/api/status storeBackend === 'oxigraph-server' and FAIL (not skip) otherwise, so
the bug-reproducing backend can never silently regress to a trivial pass.

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

* fix(devnet): pin per-node oxigraph-server ports (7900+n) to avoid the 7878 collision

The daemon-managed oxigraph-server defaults to port 7878; two nodes on one host
(devnet nodes 1-2) collided with 'Address already in use'. Give each its own
port via store.options.port. Caught by booting the e2e devnet on the new
oxigraph-server backend.

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

* fix(e2e): make the WM→SWM→VM UI spec green — stale-state wipe, import-result hooks, publish-policy retry

Three issues surfaced by running the flagship oxigraph-server UI spec end-to-end:

1. devnet.sh: each `start` redeploys a FRESH chain but kept persisted per-node
   store state, so a node's stale `dkg:contextGraphOnChainId` triple made
   registerContextGraph short-circuit ("already registered") and never create
   the CG on the new chain → isContextGraphActive=false, publishPolicy reads 0,
   and every VM publish hit LU-5 "publish access-policy is unknown". Now wipe
   per-node state on a fresh-chain boot (keeping the cached Oxigraph binary), so
   nodes re-register against the new chain. Fixes local repeated-boot flakiness
   (CI was masked by always starting from a clean .devnet).

2. ImportFilesModal: the result element exposed `className="v10-import-result"`
   but none of the `data-testid="import-result"` / `data-import-status` /
   `data-import-triples` hooks the spec asserts on. Added them (the spec was
   written test-first; the component just never exposed the contract).

3. devnet-publish.ts: retry the SPECIFIC LU-5 "policy-not-confirmed" transient
   in publishToVm (≤45s) — defensive hardening for the genuine few-blocks
   registration→confirmation lag (distinct from #1 above), centralized so the
   global-setup seed and every publish caller benefit.

Result: wm-swm-vm-ui-cycle.devnet.spec → import + promote (the #996 blank-node
WM→SWM migration) pass through the REAL UI on the production-default
oxigraph-server backend; VM-publish stays an intentional test.fixme (covered by
the API-driven sibling spec).

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

---------

Co-authored-by: Branimir Rakic <aleatoric@Branimirs-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(e2e): query the _meta marker with a hardcoded GRAPH IRI (PR review)

readMemoryLayerMarker queried the meta partition with `GRAPH ?mg` + a
CONTAINS(/_meta) filter. Under the daemon's scoped-query guard (documented in
this file's header) the `<cg>/_meta` partition is ONLY reachable via a
hardcoded `GRAPH <iri>`; the variable+filter form is for the data /
_shared_memory partitions and 400s on a strict daemon, so the marker read would
fail even after a successful promote. Build the exact meta-graph IRI
(`did:dkg:context-graph:<cg>/_meta`, identical to what the agent's
gossip/finalization handlers use) and query it directly.

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

* fix(e2e): scope dropzone 'supported-format hint' assertions to the modal

import-files + import-data-display asserted page-wide getByText(/\.md/) for the
dropzone's static supported-formats hint. On the shared devnet that also matches
any .md document already imported (e.g. the WM→SWM→VM lifecycle spec's
single-entity-fixture.md), tripping strict-mode with multiple matches. The hint
lives in the import modal, so scope the assertions to importFilesModal.overlay.
Hardens them against any shared .md content (root cause; green on main only
because nothing else imported a .md there).

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

---------

Co-authored-by: Branimir Rakic <aleatoric@Branimirs-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…tegration

# Conflicts:
#	packages/adapter-hermes/test/hermes-adapter.test.ts
#	packages/adapter-openclaw/test/plugin.test.ts
#	packages/agent/src/dkg-agent.ts
#	packages/cli/src/daemon/handle-request.ts
#	packages/cli/src/daemon/routes/assertion.ts
#	packages/cli/src/daemon/routes/knowledge-assets.ts
#	packages/cli/test/import-file-integration.test.ts
#	packages/cli/test/knowledge-assets-route.test.ts
#	packages/cli/test/promote-route-not-persisted.test.ts
#	packages/mcp-dkg/src/client.ts
#	packages/mcp-dkg/src/tools/assertions.ts
#	packages/node-ui/src/ui/api.ts
#	packages/publisher/src/dkg-publisher.ts
#	packages/publisher/src/storage-ack-handler.ts
…utes

Applies the same browser-client repoint as the migration's other ~80 consumers,
to main's copy of ui/api.ts (import-file, create/publishTriples, promote,
extraction-status -> /api/knowledge-assets wm/import-file, POST, swm/share,
wm/extraction-status). Closes the last live /api/assertion caller on the
main-reconciled tree.

Co-Authored-By: Claude Opus 4.8 (1M context) <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 13199 lines (cap: 5,000). Please consider splitting this into smaller PRs for reviewability.

branarakic added a commit that referenced this pull request Jun 7, 2026
D6: demote the workspaceOwner first-writer-wins THROW (dkg-publisher.ts share path) to an advisory warn+skip — co-claims are no longer rejected (OT-RFC-46 §17.4). Under the still-bucket SWM the foreign root is skipped (no clobber); the skip is removed once SWM is per-KA (rc.17b). Receiver-side gate + the layout-entangled substrate/identity/migration work is spec'd, not written blind.

docs: detailed per-file rc.17a/rc.17b implementation + devnet-test plan (the 15 work items, the two sub-trains, the feature-flag/dual-read + 4 hard requirements). PENDING CI — this throwaway clone has no pnpm/build/devnet env, and the work is gated on #1041 landing on main.

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

Copy link
Copy Markdown
Contributor Author

Subsumed into rc17-vm-wip (the rc.17 integration branch → main via #1053) — this work shipped as part of integrated rc.17. Closing as superseded.

@branarakic branarakic closed this Jun 9, 2026
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.

1 participant