Skip to content

Feat/intuition onchain integration#12

Open
Wieedze wants to merge 47 commits into
intuition-box:mainfrom
Wieedze:feat/intuition-onchain-integration
Open

Feat/intuition onchain integration#12
Wieedze wants to merge 47 commits into
intuition-box:mainfrom
Wieedze:feat/intuition-onchain-integration

Conversation

@Wieedze
Copy link
Copy Markdown

@Wieedze Wieedze commented Apr 30, 2026

Mission

Integrate the Intuition Protocol into the Ontology app so users can
publish their claims onchain (atoms + triples) and explore real
indexer-driven data

  • Network: Intuition L3 mainnet (chain 1155, MultiVault
    0x6E35cF57A41fA15eA0EaE9C33e751b01A784Fe7e), tested on chain 13579.
  • Native token: $TRUST.

What this PR ships

1. Wallet connection

  • wagmi v2 + RainbowKit on a custom chain definition for Intuition L3
    (mainnet + testnet).
  • Custom WalletButton matching the Ontology aesthetic (deterministic
    gradient avatar, balance via useBalance, clickable network row).
  • Single source of truth for env via zod-validated config/env.ts.

2. Onchain claim submission

  • ClaimSubmissionService.submit and .submitBatch orchestrate the full
    flow: pin (IPFS via GraphQL pinThing/pinPerson/pinOrganization)
    → resolve canonical predicate → existence check → ONE createAtoms
    ONE createTriples.
  • Batch publishes N claims in exactly 2 transactions regardless of N.
  • Pre-checks isTermCreated for both atoms and triples to avoid the
    "unknown gas fees" revert when a term already exists onchain.

3. Live indexer-driven views

  • LiveInstanceGraph: D3 force-directed instance graph from the indexer,
    pre-warmed for instant convergence, camera locked on selected atom.
  • RecentLiveClaimsCard: side-by-side with the graph, click-sync
    selection in both directions.
  • claim-matrix: per-row live triple count for every (subject, object)
    pair.
  • relationship-graph, atom-tree: counts + accent edges from live data.
  • claim-builder examples: split static templates from a read-only
    Related onchain panel so users see what already exists and craft a
    complementary claim.

4. Post-publish UX

  • Per-triple summary list after a confirmed batch with See on graph
    buttons that pivot the live graph to the published subject.
  • Form auto-clears on confirmation; submission status panel keeps the tx
    links + the "See on the graph" affordance.
  • Dedup: addToBatch rejects identical (subject, predicate, object)
    triples with an inline feedback (1.8s).
  • Dismiss and Clear batch clean both the staging list and the
    confirmation card.

Architecture

  • services/intuition/: pure TS, no React. Class-based with constructor
    DI (IndexerService, PinningService, MultiVaultReadService,
    MultiVaultWriteService, ClaimSubmissionService).
  • hooks/intuition/: pure orchestration over services
    (useSubmitClaim, useSubmitBatch, useIntuitionSession,
    useLiveExamples, useLiveTripleCounts, useLiveAtoms,
    useLiveTriples).
  • Branded types (AtomId, TripleId, CurveId) so opaque bytes32 IDs
    can't be confused at call sites.
  • Discriminated unions for every state machine (SubmissionState,
    BatchSubmissionState).
  • Single env loader (config/env.ts) with zod validation; no
    import.meta.env outside it.

See docs/architecture.md, docs/decisions.md,
docs/env.md, docs/plan.md.

Onchain proofs (testnet, chain 13579)

  • Single claim — atoms tx: <0x…>
  • Single claim — triple tx: <0x…>
  • Batch (2 claims) — atoms tx: <0x…>
  • Batch (2 claims) — triples tx: <0x…>
  • Re-publish existing triple (short-circuit, no popup): <tripleId 0x…>

Explorer: https://testnet.explorer.intuition.systems

Test plan

  • bun install && bun run build && bun run lint — clean.
  • Set .env from .env.example (chain id, RPC, GraphQL, MultiVault
    address, WalletConnect project id).
  • Connect wallet on Intuition L3.
  • Publish a single claim — verify tx hashes resolve on the explorer
    and the live graph picks up the new triple within ~12s.
  • Add 2 different claims to the batch, publish — exactly 2 wallet
    popups, per-triple summary appears, "See on graph" focuses the
    subject atom.
  • Try to add the same triple twice — the second attempt is rejected
    with "This claim is already in the batch."
  • Republish an existing triple — short-circuits without a wallet
    popup.
  • Click any subject in RecentLiveClaimsCard — graph centers on it.

What's intentionally out of scope

  • The Glossary page is left untouched (different intent: educational
    content, not onchain claims).
  • No counter-triples / staking UI in this PR (could be a follow-up).

Wieedze and others added 30 commits April 29, 2026 18:41
- CLAUDE.md: layering rules, typing rules, commit rules, phase gates
- .claude/agents/: architecture / typing / intuition / docs-writer reviewers
- .claude/settings.json: bun-only permissions, deny npm/npx/yarn/pnpm
- docs/: architecture, ADRs (ADR-001 layering, ADR-002 bun), env inventory, plan
- .env.example: testnet defaults (chain 13579, MultiVault, GraphQL, RPC)
- wagmi + viem: wallet + contract interactions
- @rainbow-me/rainbowkit: wallet UI
- @tanstack/react-query: GraphQL caching
- graphql + graphql-request: Intuition indexer client
- remove package-lock.json (bun is the canonical package manager per ADR-002)
- docs/architecture: layering rules (pages -> components -> hooks -> services -> lib)
- docs/decisions: ADRs (ADR-001 layering, ADR-002 bun-only)
- docs/env: env var inventory
- docs/plan: bounty phase plan with commit-level breakdown
- .env.example: testnet defaults (chain 13579, MultiVault, GraphQL, RPC)
- .gitignore: env files and personal tooling
Single source of truth for environment variables. Validates at module load
with developer-friendly diagnostics so a misconfigured .env crashes at boot
rather than producing late opaque failures during contract calls or queries.

- supports chain 1155 (mainnet) and 13579 (testnet)
- enforces 0x-prefixed 40-char hex on VITE_MULTIVAULT_ADDRESS
- exposes derived networkName for UI use
- src/config/env.ts is the only file allowed to read import.meta.env
The Intuition L3 (mainnet 1155, testnet 13579) is not in viem's built-in
chain list, so each chain is declared via defineChain. Canonical RPC and
explorer URLs are baked in as defaults; the wagmi config will override
the RPC at provider construction using the env-loaded URL.

- intuitionMainnet / intuitionTestnet exports
- getIntuitionChain(chainId) helper
- IntuitionChainId / IntuitionChain types
Sourced from the canonical Intuition V2 contracts. The L3 is not indexed
by Etherscan, so the ABI must be declared explicitly via parseAbi.

Split into read- and write-only subsets so query-only services don't
inadvertently hold a payable function reference. A merged multivaultAbi
is exported for the rare consumer that needs both directions on the
same contract instance.

Covers: cost queries, existence checks, atom/triple data + ID derivation,
vault state, preview functions (mandatory before any write), fee queries,
config getters, atom-wallet helper, plus the full write surface
(createAtoms, createTriples, deposit/redeem with batch variants, approve,
claimAtomWalletDepositFees).
Atom IDs and triple IDs share the same EVM representation (bytes32) but
are not interchangeable — depositing into a triple vault expecting an
atom vault, or vice versa, corrupts protocol state silently. The branded
types make the mismatch a compile-time error; the asAtomId / asTripleId
constructors validate the bytes32 shape at the I/O boundary.

CurveId is branded to prevent mixing with other bigint values (assets,
shares, fees). The active curve is fetched once per session via
getBondingCurveConfig() and cached.

assertNever is the exhaustiveness helper for switches on discriminated
unions — adding a new variant without a matching case becomes a
compile-time error at the call site.
Stateless typed read layer over the Intuition indexer. The client is
built lazily on first call and kept as a module-level singleton for
connection reuse. Caching, retries, and React state belong to the
consumer hooks (TanStack Query); the service surfaces raw network
errors and trusts the indexer schema contract.

Initial query surface:
- findPredicateAtomsByLabel: locate canonical predicate atoms ordered
  by usage count (skip TextObject legacy entries per skill convention)
- getAtomByTermId: read a single atom by its bytes32 ID, returns null
  on miss
- listTriplesByPredicate: enumerate triples for a predicate, paginated
  by the caller

Additional queries (positions by user, triples by subject, etc.) will
land in subsequent commits as the consuming hooks come online.
Wraps the configured pinning endpoint (VITE_IPFS_PIN_ENDPOINT) in a
generic POST/JSON-LD adapter. The deployment platform supplies the
endpoint URL — Pinata, Web3.Storage, self-hosted Helia, or the
Intuition team's service all conform to the same { uri } response
contract.

Per the skill convention, every atom except CAIP-10 blockchain
addresses is pinned first; the resulting ipfs:// URI becomes the
atom's bytes payload via stringToHex(). Plain-string fallbacks
produce legacy TextObject atoms and are intentionally unsupported
here — the on-chain write path must treat a pinning failure as
fatal and abort before issuing any contract call.

Initial schema set covers the three most common atom-type categories:
- pinThing for generic concepts
- pinPerson for people
- pinOrganization for companies / DAOs / projects

Additional schemas (CreativeWork, SoftwareApplication, etc.) land in
follow-up commits as the atom-types map grows.
Reads atomCost, tripleCost, and defaultCurveId from the MultiVault in a
single multicall via wagmi's useReadContracts. These constants change
only on governance actions, so we cache them indefinitely with
staleTime: Infinity and refetch only on explicit invalidation.

The return shape is a discriminated union over loading / error / success
— consumers must handle each variant before submitting, otherwise the
write would either revert (MultiVault_InsufficientAssets) or hit a
stale curve.

The defaultCurveId is wrapped in the branded CurveId at the I/O
boundary so downstream services and hooks cannot accidentally mix it
with assets or shares.

The hook itself is part of the foundation module: it's defined here
but consumed only after the bounty PR introduces the WagmiProvider in
App.tsx.
The Intuition indexer exposes pinning as GraphQL mutations on the same
endpoint as the read queries — there is no separate IPFS REST endpoint
to configure. Verified against the canonical sofia-core implementation
and the indexer's generated GraphQL schema (PinThingInput, PinPersonInput,
PinOrganizationInput all returning PinOutput { uri }).

Removes VITE_IPFS_PIN_ENDPOINT from the env loader and .env.example —
deployments now only need the GraphQL endpoint, which is already
configured for queries.

The service surface stays the same (pinThing / pinPerson / pinOrganization
returning a branded ipfs:// URI), so downstream callers are unaffected.
Both mainnet and testnet are registered so the user can switch chains
via the wallet UI. The http transport for the active chain (per env)
uses the env-configured RPC URL; the inactive chain falls back to its
built-in default.

No connectors are declared in this config — the RainbowKit provider
added by the bounty PR wraps the config and supplies its own connector
list, keeping the foundation module agnostic to wallet-UI choices.
The Intuition indexer's Hasura request-transformation template
references every pin-mutation field; sending an undefined optional
field triggers a transformation error at the gateway. Coerce undefined
to '' before issuing the mutation so the wire payload always carries
the full shape — same pattern as the canonical sofia-core AtomService.

Affects pinThing, pinPerson, pinOrganization.
…tion

Per the Intuition skill, TextObject atoms are legacy plain-string
predicates and must not be reused — when only TextObject candidates
exist, callers should pin a structured replacement instead.

The raw findPredicateAtomsByLabel stays for debug/analytics access,
but the new pickCanonicalPredicate helper enforces the rule, and
resolveCanonicalPredicateByLabel wraps both calls so consumer hooks
cannot fall through to a TextObject atom by accident.
The Number(value) as 1155 | 13579 cast is safe because the preceding
z.enum guarantees the runtime value is one of the two literals, but
the project rule requires every type assertion outside parsing/boundary
code to carry a // boundary: comment. Add it inline.
…aultConfig

The bounty integration requires a wallet-selection modal. RainbowKit's
getDefaultConfig is the canonical wagmi v2 setup that supplies a
curated connector list for injected, browser-extension, and mobile
wallets out of the box.

It requires a WalletConnect Cloud project ID — registered free at
https://cloud.walletconnect.com — added to env.ts as a required var
and surfaced through env.walletConnectProjectId.

The transport policy is unchanged: env-configured RPC for the active
chain, default RPC for the inactive chain. ssr: false because Ontology
is a client-only Vite SPA.
main.tsx wraps the app with WagmiProvider > QueryClientProvider so
wagmi hooks resolve at any depth. RainbowKit's stylesheet is imported
once at the entry to keep modal styles loaded before any consumer
renders.

The RainbowKitProvider itself stays inside App.tsx because the modal
theme tracks the user's dark/light preference — kept close to the
theme state.
…hetic

Replaces the default RainbowKit ConnectButton with a custom wallet
manager modeled on intuition-fee-proxy-template's pattern, adapted to
Ontology's CSS tokens and h-8 header rhythm.

States:
- disconnected: outlined Connect pill with wallet icon
- wrong network: destructive-tinted pill
- connected: round avatar (deterministic HSL gradient from address,
  or ENS image) opening an inline dropdown with wallet identity,
  network info, and copy / explorer / disconnect actions

App.tsx wraps the existing tree with RainbowKitProvider so the modal
theme tracks dark/light mode, and surfaces WalletButton in the header
next to the tutorial and theme toggles.
Pure function that classifies an Ontology atom-type ID into the
on-chain materialization strategy:
- caip10: Ethereum accounts/contracts/tokens (no IPFS pinning;
  the CAIP-10 URI is the atom data itself)
- person: Person, AIAgent (pinned via pinPerson)
- organization: Organization, LocalBusiness, Brand (pinned via
  pinOrganization)
- thing: catch-all for everything else (pinned via pinThing)

Atom-type IDs not in the explicit sets fall through to 'thing' by
default — surface this file in code review when adding new types
to src/data/atom-types.ts to opt into a more specific classification.

Also exposes encodeAtomData(uri) which centralizes the stringToHex
encoding so consumer hooks never reach into viem primitives directly.
Full on-chain submission flow for a single claim, exposed as a hook
that returns a discriminated state machine:
idle -> preparing -> creating-atoms -> creating-triple -> confirmed
(or -> error from any phase).

Steps:
1. Pin subject + object atoms in parallel via the IPFS service (or
   encode CAIP-10 URI directly for blockchain atom types).
2. Resolve the canonical predicate atom by label via
   resolveCanonicalPredicateByLabel; pin a Thing if no canonical
   exists yet.
3. Compute atom IDs with calculateAtomId and check existence with
   isTermCreated. Skip already-created atoms.
4. Batch-create missing atoms in a single createAtoms tx; await
   confirmation.
5. Create the triple with createTriples (single-element arrays);
   await confirmation.
6. Compute and surface the resulting tripleId via calculateTripleId
   so the UI can link to the explorer or seed local history.

isReady gates the submit button on (wallet connected) AND (Intuition
session loaded), so consumers never submit before atomCost / tripleCost
are known.
Convert the service layer to injectable classes so business logic is
fully decoupled from React and from infrastructure clients. Module-level
mutable singletons (`let cachedClient`) are gone — each service now
receives its dependencies through the constructor and is testable in
isolation without monkey-patching globals.

New surface:
- IndexerService wraps the GraphQL read queries (predicate resolution,
  atom lookup, triple listing). Canonical predicate resolution is the
  only public path to predicate IDs, enforcing the no-TextObject rule
  at the API boundary instead of by convention.
- PinningService wraps the pinThing/Person/Organization mutations on
  the same GraphQL endpoint, sharing the underlying client with the
  indexer.
- MultiVaultReadService and MultiVaultWriteService split the contract
  surface so a read-only consumer never accidentally holds a wallet-
  bound instance. Both services own their respective ABI subset, and
  the write service awaits transaction receipts internally — callers
  treat a resolved promise as "mined".
- ClaimSubmissionService is the new orchestrator: it owns the seven-
  step submission pipeline (pin atoms, resolve canonical predicate,
  compute IDs, existence-check, batch-create missing atoms, create
  triple, derive triple ID). All the business decisions that used to
  leak into the hook now live here behind a single `submit(...)` call
  that emits phase events for UI progress and returns a typed result.
- factory.ts is the composition root: the only place in the app that
  knows how to wire the service tree from raw clients. Tests construct
  services directly with mocks; production code calls the factory.

The codebase's `erasableSyntaxOnly: true` setting forbids parameter
properties, so each class declares its fields explicitly and assigns
them in the body of the constructor.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The previous implementation owned the full submission pipeline inline:
GraphQL queries, IPFS pinning, contract reads/writes, ID derivation,
existence checks, batch math. That made the hook 280 lines, mixed
business logic with React state, and forced anyone writing a related
flow (batch builder, retry, edit) to duplicate the same plumbing.

Move all of that into ClaimSubmissionService and have the hook simply
forward to it. The hook now:
- Reads connection state and protocol session from wagmi/TanStack.
- Memoizes a service tree built from the current public/wallet client.
- Calls services.claimSubmission.submit(draft, context, onPhase) and
  maps the phase callbacks + terminal result onto the existing
  SubmissionState discriminated union.

The public API (submit, reset, state, isReady) is unchanged, so
ClaimBuilder and any future consumer keep working without edits. No
more `walletClient.writeContract` or contract ABI imports in the hook
file — those moved to MultiVaultWriteService.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
… shape

wagmi v2's useBalance returns { decimals, symbol, value: bigint } and
no longer exposes a pre-formatted `formatted` string. The dropdown
relied on `balance.formatted`, which crashes the type-check and would
render `undefined` at runtime. Format on the fly with viem's
formatUnits before applying the four-decimal display rule.

Bundles a small UX addition that surfaces the chain switcher inside
the dropdown via openChainModal, so a user on the wrong network can
recover without leaving the connected state.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…onfig

The original chain definitions used a /http suffix on RPC URLs and
'tTRUST' as the testnet symbol — taken from an older skill revision.
This caused two issues:
- wagmi useBalance returned an unparseable balance for users on either
  network (NaN display in the wallet UI)
- the wallet-installed chain disagreed with our config (wallets canonically
  show TRUST as the symbol on both networks)

Cross-checked against intuition-fee-proxy-template/packages/sdk/src/chains.ts
and aligned:
- Drop /http suffix on RPC URLs (mainnet and testnet)
- Use TRUST as native-currency symbol on both networks
- Register Multicall3 deployment (0xcA11...CA11) so wagmi can batch
  read calls efficiently
- Add public RPC URL alongside default for viem fallback

Wagmi config simplified accordingly: no manual transports override —
getDefaultConfig now uses each chain's rpcUrls.default.http[0] which
matches the env value. Wallets that already added the chain with the
old config need to delete-and-re-add for the symbol/RPC to refresh.
The session-snapshot shape (atomCost, tripleCost, defaultCurveId) is
consumed by both the hook (which fetches it via wagmi) and the
ClaimSubmissionService (which uses the values to compute fees). Moving
the interface to types.ts lets the service layer reference the canonical
shape without depending on a hook — keeps services React-free and
unblocks the orchestrator's constructor injection.

The hook re-exports the type so existing imports are not broken.
Adds the read-side primitives the visualization views need to start
consuming on-chain data with a clean fallback path:

- IndexerService.listAtoms({ limit, offset }) — recent atoms paginated
- IndexerService.listRecentTriples({ limit, offset }) — recent triples
  with subject/predicate/object atoms joined inline (single GraphQL
  round-trip yields labeled edges)
- JoinedTripleRecord type for the joined triple shape
- useIndexer — lightweight read-only React-side wiring (no wallet
  client; useable in any context that just needs to query the indexer)
- useLiveAtoms / useLiveTriples — TanStack Query wrappers exposing
  the raw query handle so consumers can branch on isLoading/error and
  fall back to static seed data when the indexer is unreachable or
  returns an empty page

These hooks are the foundation for B3b/c/d (graph/tree/matrix live
data); each consuming view will merge the live records with its
existing static fallback so the UI stays usable offline.
- useLiveTriples fetches the 200 most recent triples from the indexer
- Counts aggregated by (subject.type, object.type) pair, with a
  null-guard skipping triples whose joined atom rows have been pruned
- Edges with live triples get an accent-tinted stroke and a 2x
  thickness boost so they stand out in the dense schema graph
- Status pill in the header surfaces loading / indexer error / live
  count, hidden behind the type-filter highlight when active

JoinedTripleRecord updated to mark subject/predicate/object as
nullable, matching the indexer schema.
- useLiveAtoms fetches the 200 most recent atoms from the indexer
- Counts aggregated per atom type, surfaced as a 'Type · N' suffix
  on the radial tree labels — preserves the schema view while making
  on-chain volume visible per type at a glance
- Status pill in the header shows loading / indexer error / total
  live atoms count
A new D3 force-directed graph that complements the schema relationship
graph by visualizing actual on-chain atoms as nodes and triples as
directed edges:

- Nodes: each unique atom from the 100 most recent triples, colored
  by its atom-type category (Person/Thing/Organization/etc.) with a
  neutral fallback for types outside the static palette (e.g. legacy
  TextObject atoms)
- Edges: each triple as a directed link from subject to object with
  an accent-tinted stroke and arrowhead; hovering an edge surfaces
  the predicate label as a tooltip so the canvas stays uncluttered
- Same UX primitives as the schema graph: force simulation, zoom,
  pan, fullscreen, drag-to-reposition, hover-to-highlight neighbors

Self-contained: no props, pulls live data via TanStack Query and
gracefully shows an empty-state hint when the indexer has no triples
to render. Mounted on the Explorer page below the schema views so
reviewers see the instance-level knowledge graph without leaving the
landing route.
Wieedze and others added 16 commits April 29, 2026 23:55
ClaimSubmissionService.submitBatch() handles N drafts in exactly two
transactions regardless of size — the design createAtoms and
createTriples were built for. Atoms are deduplicated across drafts so
a label appearing in three claims gets pinned and registered once.

Flow:
1. Dedupe (value, type) atom specs across all drafts
2. Pin all unique atoms in parallel (or build CAIP-10 URI)
3. Resolve unique predicates (canonical from indexer, or pin a Thing)
4. Compute atom IDs and run existence checks in parallel
5. ONE createAtoms tx for every previously-uncreated atom
6. ONE createTriples tx for every triple via parallel arrays

useSubmitBatch React binding mirrors useSubmitClaim's state machine
so the BatchBuilder UI reuses the same status-panel pattern. The
BatchBuilder gains a 'Publish all onchain (N)' outlined accent button
with a tooltip explaining the 2-tx contract when disabled.

Both submission hooks invalidate the 'intuition' query family on
confirmed so the live graph and tree views pick up the freshly
published atoms/triples without waiting for staleTime to expire.

Verified onchain testnet tx:
- Atoms:   0xb3d427...463740
- Triples: 0x617272...82b171  (2 claims: sofia employs sam + sofia employs max)
Splits the live data section into a 2/3 + 1/3 grid with both cards at
a fixed height so they match exactly and the graph never inherits
runaway height from the recent-claims sibling.

- LiveInstanceGraph accepts selectedAtomId / onSelectAtom props and
  surfaces a persistent highlight (enlarged node, accent ring, dimmed
  neighborhood) driven externally
- RecentLiveClaimsCard mirrors the selection: each row's subject and
  object labels are buttons that toggle selection, with an accent
  background on the active row
- Selection state lifted to home.tsx so clicking an atom node in the
  graph highlights its row in the list, and tapping a label in the
  list focuses its node in the graph

Stacks vertically below the lg breakpoint.
Surfaces the most recent on-chain claims at the top of the matrix
listed by their resolved atom labels (subject — predicate — object)
so users see published claims instance-level rather than only the
schema combinations beneath.

- useLiveTriples({ limit: 50 }) + null-guard on the joined atom
  relations (the indexer marks them nullable when an atom row has
  been pruned)
- Recent claims section: top 5 triples with type-colored labels,
  hidden when the indexer is empty so the schema layout stays clean
- LiveTriplesPill header status mirrors the graph and tree patterns
- liveAtomColor falls back to a neutral shade for types outside our
  static palette (e.g. legacy TextObject rows)
When a sibling component (the recent-claims card) drives a selection,
the graph now animates an affine transform that lands the highlighted
node at the center of the canvas at 1.6x zoom — drives the
click-from-recent-claims pivot UX.

Node positions are read from a ref so the recentre effect doesn't
re-fire on every simulation tick. The 'data-edge-id' attribute is
kept as  to satisfy noUnusedParameters.
Surfaces real on-chain claims for the selected subject type as
inspiration in the claim-builder examples list. Replaces direct
imports of the static EXAMPLE_CLAIMS seed with a useLiveExamples
hook that:

- Queries IndexerService.listTriplesBySubjectType for recent triples
  matching the selected atom type
- Filters out triples whose predicate label maps to no known static
  predicate (so the form's validation rules and selector still apply
  on click)
- Deduplicates by (subject, predicate, object) tuple and caps at the
  caller-supplied limit (3 by default)
- Falls back to the curated EXAMPLE_CLAIMS seed when live data is
  unavailable, still loading, or empty for the selected type — keeps
  the UI usable offline and meaningful for atom types without
  on-chain activity yet

The static seed stays in /src/data/example-claims.ts as the offline
fallback; consumers no longer reach into it directly.
…ergence

Every nodes update used to bring up to a few seconds of visible
'collision dance' as the layout settled — newly-spawned nodes start
at the origin and the force simulation needs ~300 ticks to converge.
A 'See on the graph' pivot landing during that window read stale
coordinates, so the camera focused on the wrong position.

Switch the simulation to a stop+manual-tick model: build the forces,
then run 300 ticks in memory before letting D3 attach to the DOM.
Nodes appear at their converged coordinates on first render — no
animated settling. A small alpha=0.05 restart at the end keeps drag
and hover interactions feeling alive without re-energizing the whole
graph.

The zoom-to-selected effect can now apply immediately (no settle
delay) because target coordinates are stable as soon as the node
shows up in the nodes array.
…data refreshes

The simulation rebuilds on every nodes change (data refetch), and
each rebuild places nodes at slightly shifted converged coordinates
once a freshly-published triple enters the dataset. The previously
saved camera transform pointed at the old coordinates, so the
selected atom drifted off-center after each invalidation cycle.

The recenter effect now re-applies the zoom on every (selection,
nodes) change while a selection is active. The first focus animates
so the user sees the camera pivot; subsequent re-applies snap
silently to the new converged coordinates, so the lock feels
perfectly stable across refetches.

The camera releases as soon as the user clears the selection
(clicking the active row toggles it off).
After a successful publish, the claim preview was lingering with
its three action buttons (Save / Add to Batch / Publish onchain),
visually inviting a duplicate publish. The submission status panel
below already carries the success message + tx links + 'See on the
graph' affordance, so the form values are no longer useful — clear
them once onchain.state.status flips to 'confirmed' so the next
claim starts on a fresh canvas.
…imit ceiling

The status pills on the four schema/live cards (Entity Hierarchy,
Entity Relationship, Live Knowledge Graph, Recent claims, Entity
Matrix) advertised numbers like '200 ONCHAIN' that were really just
our hardcoded query window — not the actual on-chain total. They
suggested authoritative counts the app couldn't deliver, so they
read as noise.

- Remove LiveAtomsPill, LiveStatusPill, LiveTriplesPill, and the
  two StatusBadge variants. The card titles already carry the
  context; further numbers belong only where they're accurate.
- Drop the now-dead total/count helper variables that fed those
  badges.
- Raise every useLive* limit (atoms, triples, triple counts) from
  50/100/200 to 5000 — high enough that the indexer's current data
  fits unpaginated, so the visualizations reflect everything it has
  rather than an arbitrary head.
Backs the matrix's per-row live badge with a single GraphQL fetch over
listRecentTriples, aggregated client-side by the (subjectType, objectType)
key. Matches the consumer wiring already merged in claim-matrix.tsx so
the build no longer references a missing module.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
… panel

The single 'Examples' list mixed authored prompts (Billy, Alice Johnson)
with on-chain triples, which was confusing and made it easy to publish a
duplicate by clicking what looked like a template. Restructure into:

  - 'Examples' — always the curated static seed, clickable to fill the form.
  - 'Related onchain' — read-only list of real triples for the selected
    subject type, surfaced so the user can see what already exists and
    author a complementary claim instead of duplicating one.

Hardens the indexer query at the same time: switching from the unreliable
nested atom_type filter to a wider listRecentTriples + client-side filter
so the live list isn't silently empty for atom types the indexer's nested
filter rejects.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…sult

Re-publishing a claim that was already onchain reverts with
MultiVault_TripleExists, which MetaMask renders as 'unknown gas fees'
because gas estimation fails before signing — confusing for both the
single-claim and batch flows.

Compute the triple ID up-front and skip createTriples for any triple
that already exists. For batches, filter the parallel arrays so the
single createTriples call only carries brand-new triples (no-op when
every entry duplicates an existing one).

Surface subjectAtomId / predicateAtomId / objectAtomId on the submission
result so the UI can drive a graph selection or pivot once a claim
confirms — used by the upcoming 'See on graph' affordance.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
A successful batch confirmation showed two opaque tx hashes and nothing
about which claims were actually published. Render a numbered list of
the submitted drafts (subject — predicate — object, with the same color
coding as the staging list) and a 'See on graph' button per row that
selects the subject in the live graph and scrolls the section into view.

Snapshot the batch entries at submit time so the labels persist even if
the user empties or edits the staging list afterwards. On Dismiss after a
confirmed batch — and on Clear batch — also wipe the submission state
machine and the snapshot so the next composition starts from a clean
slate instead of inheriting a stale confirmation card.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
A batch with two identical (subject, predicate, object) tuples writes to
the same onchain triple slot twice — the second createTriples call
reverts. Reject duplicates upstream in addToBatch (returns false on
match) and surface a 1.8s inline notice in the claim builder so the user
understands why their click had no effect.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Replace the '0x' sentinel for skipped createTriples with `Hex | undefined`
on `ClaimSubmissionResult.tripleTxHash` so consumers can distinguish a
real hash from a no-op. Fix `submitBatch` to populate `subjectAtomId`,
`predicateAtomId`, and `objectAtomId` per draft (was missing required
fields, hidden by the length-1 shortcut workaround which is now removed).
Drop the dead defensive guard in `useSubmitBatch` and propagate the
nullable `tripleTxHash` through the UI status panels.

Remove unused exports: `multivaultAbi` (merged), `getIntuitionChain` /
`intuitionChains` / `IntuitionChain` / `IntuitionChainId`, indexer
methods `getAtomByTermId` / `listTriplesByPredicate` /
`listTriplesBySubjectType`, the public `findPredicateAtomsByLabel`
(inlined into its sole caller), and `assertNever`.

Simplify the null-guard ladders in `useLiveExamples` and
`useLiveTripleCounts`, and move the orphan Hasura-coercion comment in
`PinningService` into the class JSDoc.
feat(data-migration): live indexer-driven views + post-publish UX
@host-intuition-box
Copy link
Copy Markdown

host-intuition-box Bot commented Apr 30, 2026

The preview deployment for Ontology is ready. 🟢

Open Preview | Open Build Logs | Open Application Logs

Last updated at: 2026-04-30 13:02:01 CET

When the pre-flight isTermCreated check finds the triple already lives
on-chain, the service short-circuits without calling createTriples — but
the UI still rendered the generic 'published onchain' confirmation with
no way to tell that nothing actually happened in this transaction. Make
the case explicit:

  - Single submit: panel title flips to 'Claim already onchain' and a
    short note explains no new transaction was needed. The 'See on the
    graph' affordance still works because the triple is genuinely
    on-chain.
  - Batch: the per-draft result list marks each pre-existing triple
    with an amber 'already onchain' badge alongside its 'See on graph'
    button. When the entire batch was duplicates the panel title flips
    to 'All claims were already onchain'; when only some drafts were
    duplicates a '(some were already onchain)' suffix is appended.

The service now returns a per-draft tripleTxHash on the batch path
(undefined for already-existed entries, the shared createTriples hash
otherwise) so the UI can render that distinction without a second
existence query. The hook picks the shared hash via the first
non-undefined entry rather than results[0], which would have been
undefined whenever the first draft happened to be a duplicate.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
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