Skip to content

Latest commit

 

History

History
67 lines (57 loc) · 18.6 KB

File metadata and controls

67 lines (57 loc) · 18.6 KB

CLAUDE.md

Refer to the Contributing and Architecture Highlights sections of README.md for development workflows, release process, and repo conventions.

Non-obvious invariants

These are things that aren't self-evident from reading the code and have bitten us before:

  • Do not upgrade polkadot-api or @polkadot-api/sdk-ink past the current pins without also bumping @polkadot-apps/chain-client. Newer versions break the internal PolkadotClient shape that chain-client still relies on.
  • The mobile app wraps signRaw data with <Bytes>…</Bytes>, which breaks transaction signing. Our src/utils/session-signer-patch.ts exists specifically to route transactions through signPayload instead. Delete this file once @polkadot-apps/terminal ships a fix — nothing else.
  • getSessionSigner() returns an adapter that keeps the Node event loop alive. Every caller must invoke the returned destroy() when done. If you add a new top-level command that signs on behalf of the user, wire up the cleanup or the process will hang after the work is done.
  • dot init auto-runs at the end of install.sh. If the init fails, the exit code is surfaced so CI runs don't silently pass.
  • All chain URLs / contract addresses live in src/config.ts. Never inline a websocket URL or an 0x… address anywhere else — when mainnet launches we'll be flipping one switch, not grepping the tree.
  • Deploy delegates to bulletin-deploy for everything storage-related (chunking, retries, pool accounts, nonce fallback, DAG-PB, DotNS commit-reveal). We intentionally do NOT reimplement any of that here. The one thing we own is registry.publish() — because the contract records env::caller() as app owner and that needs to be the user, not a shared dev key. See src/utils/deploy/playground.ts.
  • Do NOT call bulletin-deploy.deploy() just to store a metadata JSON. deploy() unconditionally runs a DotNS register() + setContenthash() for whatever name you hand it — and for domainName: null it invents a test-domain-<random> label and registers THAT. That second DotNS pass reverts cryptically (Contract execution would revert: 0x…). For plain storage of the playground metadata we use @polkadot-apps/bulletin::upload() → it submits TransactionStorage.store directly and returns the CID. No DotNS side-trip.
  • Build a dedicated Bulletin client with heartbeatTimeout: 300_000 for the metadata upload. The shared client from getConnection() uses @polkadot-apps/chain-client, which calls getWsProvider(rpcs) with no options → polkadot-api's 40 s default heartbeat. A single TransactionStorage.store round-trip can exceed 40 s and the socket tears down as WS halt (3). bulletin-deploy sidesteps this with its own 300 s heartbeat; we mirror that with a one-off client in src/utils/deploy/playground.ts that we destroy immediately after the upload.
  • dot deploy does NOT pass jsMerkle: true to bulletin-deploy right now. bulletin-deploy's pure-JS merkleizer produces CARs that only contain raw leaves — the DAG-PB directory/file blocks are silently dropped by blockstore-core/memory's getAll() under rawLeaves: true + wrapWithDirectory: true. Proof: a real deployed CAR we fetched from paseo-ipfs.polkadot.io contained 157 raw blocks and zero DAG-PB, with the declared root absent → polkadot-desktop parses zero files → sites show {"message":"404: Not found"}. Until the upstream merkleizer is fixed we rely on the Kubo binary path (the default), which is reliable. dot init installs ipfs, so this Just Works for anyone who ran setup. Trade-off: this temporarily breaks the RevX WebContainer story for the main storage upload — flip jsMerkle: true back once bulletin-deploy fixes merkleizeJS to collect all blocks, not just leaves.
  • Signer mode selection lives in one file (src/utils/deploy/signerMode.ts). The mainnet rewrite is a single-file swap; keep that boundary clean.
  • src/utils/deploy/* and src/utils/build/* must not import React or Ink. They form the SDK surface that RevX consumes from a WebContainer. TUI code lives in src/commands/*/.
  • Bun compiled-binary stdin quirk — Ink's useInput silently drops every keystroke (arrows, Enter, Ctrl+C) in bun build --compile binaries unless process.stdin.on('readable', …) is touched before Ink's render(). We install a no-op readable listener at the top of src/index.ts as a warm-up. Do NOT remove it until Bun's compiled-binary TTY stdin behaves like Node's. Symptom if this breaks: TUI renders but nothing responds, including Ctrl+C.
  • bulletin-deploy 0.7.4+ pulls in a transitive dep with a broken publish manifest that pnpm refuses to install. @parity/dotns-cli@0.5.6's published package.json declares "@polkadot-api/descriptors": "file:.papi/descriptors" — a workspace-only path that doesn't exist in the published tarball. npm tolerates the dangling file: reference (creates a broken symlink and continues); pnpm's strict resolver fails with ERR_PNPM_LINKED_PKG_DIR_NOT_FOUND. We work around it with a pnpm.overrides entry in package.json pointing the offending sub-dep at a tiny stub package (stubs/papi-descriptors-stub/) so resolution succeeds. The dep is functionally vestigial — dotns-cli's dist/cli.js is fully-bundled (Bun build, no externals) and never imports @polkadot-api/descriptors at runtime, so the stub exporting {} is correct. Remove the override + stub once @parity/dotns-cli republishes a clean manifest. Tracked upstream against paritytech/dotns-sdk.
  • bulletin-deploy is pinned to an explicit version, not latest. We're on 0.7.6 stable today. The latest npm dist-tag is a moving target and previously pointed at 0.6.8, which has a WebSocket heartbeat bug (default 40s < chunk timeout 60s) that tears down uploads mid-flight as WS halt (3). Keep the pin explicit so we never silently slide onto a broken latest. When upgrading, read the release notes for any public-API changes to deploy(), DotNS methods, or the DeployOptions we rely on (jsMerkle, signer, signerAddress, mnemonic, rpc, attributes). Note: 0.7.0 removed the playground?: boolean DeployOption (registry publishing now lives here in src/utils/deploy/playground.ts), which is a no-op for us since we never passed that flag. 0.7.1 made the memory-report teardown Bun-safe upstream. 0.7.2 bumped the default CHUNK_TIMEOUT_MS 60s → 180s to match Bulletin's new 24s Aura slot duration; BULLETIN_CHUNK_TIMEOUT_MS override still works. 0.7.4 extracted the dotns logic into a separate @parity/dotns-cli subprocess (forked via _require.resolve("@parity/dotns-cli")); see the publish-bug workaround note above. 0.7.4 also moved label classification off the DotNS instance — the previously-instance method dotns.classifyName(label) is now the top-level pure function classifyDotnsLabel(label), and the result field renamed requiredStatusstatus. The function isn't re-exported from the package root, so src/utils/deploy/availability.ts mirrors the (small, stable) logic locally as classifyLabel — same precedent as simulateUserStatus. 0.7.6 added ambient Sentry mode for host apps; keep the CLI-owned privacy gate in src/bootstrap.ts.
  • Throttle TUI info updates — bulletin-deploy logs per-chunk and builds (vite/next) stream thousands of lines/sec. Calling setState on every log event floods React's reconciler with so much backpressure the process can balloon past 20 GB and freeze the OS. RunningStage coalesces "latest info" updates to ≤10/sec via a ref + timer and caps line length at 160 chars. Any new hot-path event sink should do the same; don't hook raw per-line streams directly into Ink state.
  • Process-guard safety net (src/utils/process-guard.ts) — deploy pipelines open several long-lived WebSockets + child processes and any one of them can keep the event loop alive after the TUI visibly finishes, turning dot into a zombie that accumulates retry buffers indefinitely (seen climbing past 25 GB). We defend in depth: (1) installSignalHandlers() catches SIGINT/TERM/HUP + unhandledRejection and forces cleanup + exit within 3 s; (2) scheduleHardExit() installs an unref'd timer that kills the process if the event loop doesn't drain within a grace period; (3) startMemoryWatchdog() aborts if RSS exceeds 4 GB — a generous cap because legit deploys on Bun SEA binaries routinely touch 1–1.5 GB from runtime-metadata decoding + Bun's JSC heap + Ink yoga. Do NOT re-add a per-window growth detector: we tried 300 MB / 3 s and it false-positived on the single-burst metadata-loading spike, aborting deploys that would have succeeded. Set DOT_MEMORY_TRACE=1 to stream per-sample RSS/heap/external stats — useful when diagnosing a real leak report. Telemetry bootstrap (src/bootstrap.ts) is the FIRST import in src/index.ts. It sets BULLETIN_DEPLOY_USE_AMBIENT_SENTRY=1 and BULLETIN_DEPLOY_HOST_APP=playground-cli before bulletin-deploy can evaluate, then maps DOT_TELEMETRY/internal-context detection to BULLETIN_DEPLOY_TELEMETRY. Do not leave BULLETIN_DEPLOY_TELEMETRY unset while setting the host app: bulletin-deploy@0.7.6 treats playground-cli as an internal host, which would enable deploy telemetry for external users. BULLETIN_DEPLOY_MEM_REPORT is not forced off by default anymore because upstream guards the Bun-incompatible memory-report path. Any new long-running command should register a cleanup hook via onProcessShutdown().
  • Parser MUST NOT emit an event per log line. DeployLogParser.feed() is called for every console line bulletin-deploy prints — hundreds per deploy on the happy path, thousands if retries fire. We intentionally emit events ONLY for phase-banner matches and [N/M] chunk progress. Everything else returns null. Adding a catch-all info emit turns the parser into a firehose that allocates ~200 bytes × thousands of lines, and was a measurable contributor to chunk-upload memory pressure.
  • dot mod is GitHub-tarball-only and must stay that way. src/utils/mod/source.ts downloads from codeload.github.com (no auth, no git/gh required for the public-repo case) and extracts via node:zlib + the pure-JS tar package. Do NOT re-introduce git clone or gh repo fork paths — both would re-add a hard tooling requirement and the fork path was specifically removed because GitHub caps you to one fork per source-repo per account, which broke "mod the same starter twice." A non-modable app (no metadata.repository) returns a hard error from dot mod; the interactive picker filters those out so the user never sees an unmoddable option.
  • ensureGhAuthed() does NOT shell out to gh auth login. Even from the interactive deploy, Ink owns stdout + raw-mode stdin and a stdio: "inherit" child would race Ink's useInput for keystrokes — producing garbled output and dropped key events. Both interactive and non-interactive paths that need GitHub repo creation fail with the same actionable message: run gh auth login once outside dot, then retry. Existing origin URLs do not require gh auth. Properly suspending Ink to hand off stdio is possible but requires unmounting + re-rendering with state preservation, and we deemed that complexity not worth it for a one-time-per-machine speedbump. If you ever do implement it, the wiring point is ModablePreflightStage in src/commands/deploy/DeployScreen.tsx.
  • metadata.repository is set ONLY when --modable is opted in. Older code in publishToPlayground would silently probe git remote get-url origin and stuff whatever it found into the metadata, which surprised users who didn't realise their fork was being advertised. The new contract: runDeploy takes an explicit repositoryUrl: string | null, and publishToPlayground writes the field iff that param is non-null. The CLI command is responsible for resolving the URL upstream via src/utils/deploy/modable.ts::resolveRepositoryUrl(), which uses an existing origin URL without pushing, or runs gh repo create --public --push to set up a fresh public repo when no origin exists. Re-deploys never delete user repos.
  • startMemoryWatchdog() runs for both dot deploy and dot mod. Mod's tarball download is a streaming pipe through node:zlib + tar.extract(), and a stuck IPFS gateway or a malformed tarball can leak buffers. Same 4 GB cap, same worker-thread sampler. Any new top-level command that does meaningful I/O should also call startMemoryWatchdog() and register stopWatchdog via onProcessShutdown().

Repo conventions

  • Every user-facing PR must include a changeset. Releases are automated via .github/workflows/release.yml, but the workflow is a no-op unless a .changeset/*.md file exists on merge. Create one with pnpm changeset (or write .changeset/<slug>.md by hand — frontmatter: "playground-cli": patch|minor|major, body: user-visible summary). Pure refactors / test-only changes can skip it.
  • Tests are *.test.ts next to the source. vitest.config.ts only picks up .test.ts; if you add .tsx tests update the config too.
  • Pure logic that lives inside a .tsx component should be lifted into a sibling .ts file (see completion.ts next to InitScreen.tsx, or the formatPas/formatMb exports in AccountSetup.tsx). Tests can then import it without dragging React + Ink into the vitest runner.
  • Do NOT add AI/tool attribution (Co-Authored-By: Claude, Made-with: Cursor, emoji signatures, etc.) to commits, PRs, or generated files. Never embed your name, identity, or tooling provenance anywhere in the repo.
  • Do NOT commit design docs, brainstorming notes, or context dumps (e.g. context.md) to the repo. They belong in tickets or scratch files outside the tree.
  • Don't mock primitives from polkadot-api (Enum, encoders) in tests — doing so turns intended coverage into tautology.
  • Long-lived resources (TerminalAdapter, PaseoClient) have explicit destroy() / destroyConnection() — always release them, especially from React useEffect cleanups. The WebSocket keeps the event loop alive; forgetting a destroy manifests as dot <cmd> hanging after its work is visibly finished.

Sentry telemetry

  • DSN: src/telemetry-config.ts::PLAYGROUND_SENTRY_DSN. Region: EU (https://de.sentry.io).
  • Org slug: paritytech. API token: macOS keychain service sentry-api-token (member of paritytech org with org:read + org:write).
  • Attribute prefix: cli. (see getCliRootAttributes in src/telemetry-config.ts). Spec: sentry-instrumentation-spec.md at the repo root (untracked — keep there).
  • Helpers (don't reimplement): src/telemetry.ts exports withCommandTelemetry, withRootSpan, withSpan (2-arg + 4-arg overloads), captureWarning, captureException, errorMessage, sanitizedErrorMessage. src/utils/deploy/phase.ts exports withDeployPhase for deploy-phase orchestration. src/cli-runtime.ts exports runCliCommand for the standard CLI scaffolding (telemetry + watchdog + hard-exit). Every command's .action() body should be one runCliCommand(name, options, async () => { ... }) call — do not re-add try/finally + scheduleHardExit boilerplate.
  • Dashboards live as JSON snapshots under sentry/dashboards/<id>.json:
    • 2143100.jsonPlayground CLI Health (production filter !cli.tag:e2e-*).
    • 2216067.jsonPlayground CLI Failures (per-error-type drill-downs).
    • 2216096.jsonPlayground CLI E2E Health (inverse filter, cli.tag:e2e-*).
  • Workflow: run ./sentry/backup-dashboards.sh BEFORE any change. Use ./sentry/patch-dashboard.py <id> <patch.json> for surgical edits (supports replace, patch_query, set_description ops) or full widget replacement. Use ./sentry/create-dashboard.py <payload.json> for new dashboards. Per spec §15f, do NOT include a projects field in POST payloads. Per spec §15g, PUT replaces the whole widget list — backup first.
  • E2E tagging: every spawn from e2e/cli/helpers/dot.ts injects DOT_TAG=e2e-local (fallback) and DOT_TELEMETRY=1. tools/e2e-local.sh overrides that to e2e-local-{smoke|pr|nightly} based on the mode argument. CI sets DOT_TAG=e2e-ci-{pr|nightly|dispatch} in .github/workflows/e2e.yml so production health widgets filter cleanly via !cli.tag:e2e-*.
  • SAD% propagation is verified by a regression test in src/telemetry.test.ts ("SAD% propagation through transaction envelope"). It confirms captureWarning flips cli.sad="true" on the root transaction. If that test fails, the SAD% dashboard widget on Dashboard 1 will silently degrade to a duplicate of the unexpected-failure rate.

E2E Tests

  • Local launcher: tools/e2e-local.sh [smoke|pr|nightly] — also callable via pnpm test:e2e:smoke, pnpm test:e2e:pr, pnpm test:e2e:nightly.
  • CI workflow: .github/workflows/e2e.yml — runs on PR / push:main / cron 06:00 UTC / workflow_dispatch.
  • CI matrix: 13 cells across four matrices — test-no-publish (parallel: pr-install, pr-preflight, pr-mod, pr-init-session) + test-publish (max-parallel: 1: pr-deploy-frontend, pr-deploy-foundry) + test-nightly-no-publish (parallel, schedule/dispatch only: nightly-mod-miss, nightly-diagnostic, nightly-rejections, nightly-chaos-sigint) + test-nightly-publish (max-parallel: 1, schedule/dispatch only: nightly-deploy-hardhat, nightly-deploy-multi, nightly-chaos-rpc). Each cell runs a subset via vitest -t "<pattern>".
  • Release smoke: .github/workflows/e2e-release.yml fires on release: prereleased, downloads the dot-linux-x64 SEA asset, and runs e2e/cli/published.test.ts against it. Validates the published binary before stable release.
  • Post-release smoke: .github/workflows/e2e-post-release.yml fires on release: published (stable only — prerelease != true), waits for the SEA asset, runs install.sh (consumer install path via VERSION=<tag> curl … | bash), then runs published.test.ts against the installed ~/.polkadot/bin/dot. Catches install.sh regressions that the prerelease/SEA-download path doesn't.
  • Test files: e2e/cli/*.test.ts (vitest, spawned via bun run src/index.ts).
  • Reports directory: e2e-reports/junit.xml + e2e-reports/dot-runs.log (gitignored).
  • Tag prefix: DOT_TAG=e2e-{ci|local}-{trigger} so Sentry dashboards filter test traffic. The CLI plumbs DOT_TAG into the cli.tag root-span attribute via src/telemetry-config.ts.
  • CI report job name: E2E Report — aggregates per-leg conclusions, posts a sticky PR comment with marker <!-- e2e-pr-report -->, opens an auto-issue on schedule/release fail.
  • Bootstrap: see tools/register-e2e-fixtures.ts for the mod-test fixture; full bootstrap doc TBD in a later phase.
  • Design spec: docs-internal/2026-05-02-e2e-test-suite-design.md.