From dfa43d8db41e11717bd99f24a0f49d568c8a3e4b Mon Sep 17 00:00:00 2001 From: ottovlotto <142217647+ottovlotto@users.noreply.github.com> Date: Wed, 3 Jun 2026 23:55:35 +0200 Subject: [PATCH] docs(claude.md): align with spec v0.31 Brings CLAUDE.md up to date with playground-dot Spec v0.31 (June 2026). Key changes: - Spec ref bumped v0.18 -> v0.31 - `dot` -> `playground` rename throughout (binary `playground`, short alias `pg`) - CR6 reframed: tutorial-flag (gameable) -> per-account `deploy_count` counter (deploy #1 = 100 XP, deploy #2 = 50 XP, subsequent = 0) - Network: PreviewNet -> Paseo Next v2 migration referenced - Gav's KPI (quality of human interactions > total app count) - Closed open question: CLI command naming settled as `playground`/`pg` --- CLAUDE.md | 145 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 83 insertions(+), 62 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index fbeadf6..ca9bc97 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,53 +22,52 @@ These aren't self-evident from reading the code and have bitten us before. Treat - **Import from `@parity/product-sdk-*`, never `@polkadot-apps/*`.** The CLI runtime is fully on product-sdk. `@polkadot-apps/*` is gone from the lockfile and CI's `Format` job runs `grep -rnE "['\"]@polkadot-apps/" src/ e2e/ scripts/ tools/` as a guard. Product-sdk uses caret ranges (`^0.x.y`); on a 0.x line `^` only widens patches, so a true breaking change still needs an explicit `package.json` bump. - **`@dotdm/contracts` tracks the `^3.x` line.** The legacy `1.1.1` stable still depends on `@polkadot-apps/*` + PAPI 1.x — do NOT downgrade. -- **`@novasamatech/*` is force-pinned to `0.7.9` via `pnpm.overrides` — a deliberate mobile-compat pin, NOT tree hygiene.** `@parity/product-sdk-terminal@0.3.0` declares `^0.8.1`, but host-papp 0.8 emits a **V2 pairing QR** (leading SCALE byte `0x01`, `VersionedHandshakeProposal.V2`) that the Polkadot mobile app cannot decode: its native pairing codec (`feature/sso/impl/.../HandshakeOffer.kt`) accepts ONLY `@EnumIndex(0) V1`, and no V2 support exists anywhere in the Android repo (verified at build 1227 and HEAD, 2026-06-03) — scanning a 0.8 QR shows "Invalid QR code". host-papp `0.7.9` final keeps the V1 QR AND has everything terminal 0.3.0 needs: `UserSession.createTransaction` (RFC-0020) with a wire codec that is byte-identical to 0.8.4's, `requestResourceAllocation`, no `verifiablejs` (so no WASM patch). **Drop the pin only when the mobile app ships V2 pairing** — upstream guidance (host-papp maintainers, 2026-06-03) is to stay on 0.7.9 for now since the V2 handshake is part of multi-device support, which is not merged yet. Re-verify pairing on a real phone before merging the un-pin. The pin travels with a `pnpm` patch on `@parity/product-sdk-terminal` (`patches/`) restoring the `metadataUrl` adapter option: the V1 QR embeds a metadata URL (`src/config.ts::TERMINAL_METADATA_URL`) that the phone fetches to render the Sign-In screen — without it the V1 `metadata` field encodes as an empty string and pairing dies with "Failed to load pairing request". Remove the patch + `TERMINAL_METADATA_URL` together with the pin. Upstream RENAMED `@novasamatech/product-sdk` → `@novasamatech/host-api-wrapper` (triangle-js-sdks #169, no compat re-export) — nothing in our code may depend on the old name. The `host-api-wrapper@0.8.4` copy in the lockfile hangs off the `@dotdm/contracts@^3.x` peer subtree (dormant, signer-less paths). `auth.ts::loadSessions` still translates session decode failures into a `STALE_SESSION_MESSAGE` "playground logout / playground init" hint — defensive for future wire bumps; on the 0.7.9 pin existing sessions decode fine. -- **`@polkadot-api/json-rpc-provider: ^0.2.0` override is load-bearing.** Removing it splits the lockfile across three versions of `json-rpc-provider` (`0.0.1`/`0.0.4`/`0.2.0`) — different PAPI 2.x transitive consumers ask for different versions. Forcing everyone onto `0.2.0` avoids subtle wire-shape divergence and reduces bundle/process memory. Relatedly, `polkadot-api` itself is deduped to a single `2.1.5` resolution in the runtime — if a future bump ever re-splits it, `pnpm dedupe polkadot-api` collapses it back (the bundled dotns-cli `1.23.3` copy is separate and unaffected, per the PAPI bullet below). +- **`@novasamatech/*` packages are forced to `0.7.9-4` via `pnpm.overrides`.** They're transitive (via `@parity/product-sdk-terminal`'s `^0.7.7` ranges) and pnpm won't bump transitives across patches. The override aligns the tree on the latest published Novasama line including RFC-0010 `requestResourceAllocation`. Drop the override once product-sdk-terminal bumps its caret natively. +- **`@polkadot-api/json-rpc-provider: ^0.2.0` override is load-bearing.** Removing it splits the lockfile across three versions of `json-rpc-provider` (`0.0.1`/`0.0.4`/`0.2.0`) — different PAPI 2.x transitive consumers ask for different versions. Forcing everyone onto `0.2.0` avoids subtle wire-shape divergence and reduces bundle/process memory. - **`@parity/dotns-cli@0.6.1` ships a broken publish manifest** declaring `"@polkadot-api/descriptors": "file:.papi/descriptors"` — a workspace path missing from the tarball. pnpm refuses; we redirect that sub-dep to `stubs/papi-descriptors-stub/` (an empty `{}` export). dotns-cli's `dist/cli.js` is a fully-bundled Bun build, so the stub is functionally correct. Remove the override + stub when `@parity/dotns-cli` republishes a clean manifest. -- **`bulletin-deploy` is pinned to an explicit version (`0.8.1`), not `latest`.** A previous `latest` (0.6.8) had a WebSocket-heartbeat bug that tore chunk uploads down mid-flight. The pin avoids ever silently sliding onto a broken `latest`. The 0.7.29 → 0.8.1 bump was verified additive for everything we touch: the `deploy()` signature, the `DeployOptions` we use (`jsMerkle`, `signer`, `signerAddress`, `mnemonic`, `rpc`, `attributes`), the DotNS methods, and the env helpers (`loadEnvironments`, `resolveEndpoints`) are all unchanged. New in 0.8.1: an optional `DeployOptions.contracts` (unused by us) and a `verifiablejs@1.3.0-beta.4` pin. The merkleizer is functionally unchanged, so the `jsMerkle: false` invariant below still stands. When bumping again, re-read the release notes for changes to those same surfaces; we don't consume the env helpers — our env table lives in `src/config.ts::CONFIGS`. -- **`polkadot-api` resolves to a single `2.1.5`** and is effectively the only PAPI version in the runtime: the lockfile contains `polkadot-api@1.23.3` only because `@parity/dotns-cli` declares it, and dotns-cli ships as a single fully-bundled `dist/cli.js` with all deps inlined — never resolved at runtime. +- **`bulletin-deploy` is pinned to an explicit version (`0.7.24`), not `latest`.** A previous `latest` (0.6.8) had a WebSocket-heartbeat bug that tore chunk uploads down mid-flight. The pin avoids ever silently sliding onto a broken `latest`. When bumping, read release notes for changes to `deploy()`, DotNS methods, or the `DeployOptions` we use (`jsMerkle`, `signer`, `signerAddress`, `mnemonic`, `rpc`, `attributes`). Newer releases now also export environment helpers (`loadEnvironments`, `resolveEndpoints`, etc.); we don't consume them — our env table lives in `src/config.ts::CONFIGS`. +- **`polkadot-api` is `^2.1.3`** and effectively the only PAPI version in the runtime: the lockfile contains `polkadot-api@1.x` only because `@parity/dotns-cli` declares it, and dotns-cli ships as a single fully-bundled `dist/cli.js` with all deps inlined — never resolved at runtime. ### Network / env -- **`ACTIVE_TESTNET_ENV = "paseo-next-v2"`** (`src/config.ts`). It's the only env wired up; the others throw "not supported" from `getChainConfig()`. The deploy `--env` flag accepts both the new ids and the legacy `testnet|mainnet` aliases (mapped via `resolveLegacyEnv`). When adding an env, populate `CONFIGS` and verify descriptors exist in `@parity/product-sdk-descriptors`. The `paseo-*` descriptor exports we use today are generated against paseo-next-v2 endpoints despite the unversioned names. +- **`ACTIVE_TESTNET_ENV = "paseo-next-v2"`** (`src/config.ts`). It's the only env wired up; the others throw "not supported" from `getChainConfig()`. The deploy `--env` flag accepts both the new ids and the legacy `testnet|mainnet` aliases (mapped via `resolveLegacyEnv`). When adding an env, populate `CONFIGS` and verify descriptors exist in `@parity/product-sdk-descriptors`. The `paseo-*` descriptor exports we use today are generated against paseo-next-v2 endpoints despite the unversioned names. The Summit devnet (planned, ~week of Summit) will be a separate `CONFIGS` entry — don't pin `paseo-next-v2` as the permanent network. - **All chain URLs / contract addresses live in `src/config.ts`.** Never inline a websocket URL or `0x…` address anywhere else — at mainnet launch we'll flip one switch, not grep the tree. -- **Live contract addresses resolve through `ContractManager.fromLiveClient`, not a hand-rolled patcher.** With `@parity/product-sdk-contracts@0.7` the `cdm.json` is FLAT (`{ registry, dependencies, contracts }`, no target-hash buckets). `src/utils/registry.ts` calls `ContractManager.fromLiveClient(cdmJson, client, descriptor, { libraries, defaultOrigin, defaultSigner? })` wrapped in a `MetaRegistryFailure:` error. The old hand-rolled `resolveLiveContractAddresses` / `withRequiredLiveContractAddresses` / `patchContractAddresses` in `contractManifest.ts` are DELETED — that file now only holds `PLAYGROUND_REGISTRY_CONTRACT` + Revive trace-noise suppression. The `registry` address (`0xf62c2ece29cd8df2e10040ecfa5a894a5c5d9cb0`) and the `"@w3s/playground-registry": "latest"` dependency in `cdm.json` MUST stay byte-identical to playground-app's `cdm.json`: runtime convergence comes from same-registry + `latest` live resolution, NOT from snapshotting addresses. Don't pin the dep or fork the registry address. - **Username lookup hits `Resources.Consumers` on the People parachain** (`src/utils/username.ts`). Mirrors `@novasamatech/host-papp`'s `createIdentityRpcAdapter`. Pass the SS58 string directly to `getValues([[ss58]])` — do NOT round-trip through `AccountId().dec(ss58)`. The upstream code only does that because its callers pass `0x…` hex; on an SS58 string `Bytes(32).dec` silently corrupts it into a different 32-byte sequence and the lookup fails opaquely as `(lookup failed)`. ### Deploy / Bulletin - **Deploy delegates to `bulletin-deploy` for everything storage-related** — chunking, retries, pool accounts, nonce fallback, DAG-PB, DotNS commit-reveal. Don't reimplement. The one thing we own is `registry.publish()`. The contract takes an `Option
owner` parameter — when None, it falls back to `env::caller()`; when Some, that H160 is recorded as the app owner regardless of who signed. Phone mode passes None (caller IS the user). Dev mode with an active session passes the session's `productH160` so Alice can sign the tx while the user still appears in MyApps. The `publisher` field on `AppInfo` always stores `env::caller()`, so `is_authorized_to_republish` lets the original signer iterate without rewriting ownership. See `src/utils/deploy/playground.ts` and `src/utils/deploy/signerMode.ts::resolveSignerSetup`. -- **Do NOT call `bulletin-deploy.deploy()` just to store a metadata JSON.** `deploy()` unconditionally runs a DotNS `register()` + `setContenthash()`, and for `domainName: null` invents a `test-domain-` label and registers THAT — the side-trip reverts cryptically. For metadata storage we submit `TransactionStorage.store` directly via PAPI using `calculateCid` from `@parity/product-sdk-cloud-storage`. The metadata `store` is signed with the SDK-cached Bulletin slot account from `@parity/product-sdk-terminal/host` (`~/.polkadot-apps/_AllowanceKeys.json`, not Alice, not the product account). Asset Hub `registry.publish` is signed with the user's product account in phone mode, and with a dev signer in dev mode (claimed-owner H160 carries the user identity, per the bullet above). See `src/utils/deploy/playground.ts::publishToPlayground`. +- **Do NOT call `bulletin-deploy.deploy()` just to store a metadata JSON.** `deploy()` unconditionally runs a DotNS `register()` + `setContenthash()`, and for `domainName: null` invents a `test-domain-` label and registers THAT — the side-trip reverts cryptically. For metadata storage we submit `TransactionStorage.store` directly via PAPI using `calculateCid` from `@parity/product-sdk-bulletin`. The metadata `store` is signed with the product-scoped RFC-0010 Bulletin allowance account cached in `allowance-keys.json` (not Alice, not the product account). Asset Hub `registry.publish` is signed with the user's product account in phone mode, and with a dev signer in dev mode (claimed-owner H160 carries the user identity, per the bullet above). See `src/utils/deploy/playground.ts::publishToPlayground`. - **The "dev signer" used in dev mode is bulletin-deploy's `DEFAULT_MNEMONIC` bare-root account, not Substrate's `//Alice`.** The bare-root SS58 (`5DfhGyQd…`) is what bulletin-deploy uses internally for its DEFAULT_MNEMONIC storage + DotNS signing, so the CLI's `createAliceSignerForDevPublish` derives from the same `(mnemonic, path="")` pair via `seedToAccount`. Storage, DotNS, and registry publish all sign as one identity. Substrate's `//Alice` (`5Grwva…`) is a DIFFERENT account — `createDevSigner("Alice")` from `@parity/product-sdk-tx` returns that one. Don't mix them; the `signerModeAlice.test.ts` snapshot guards against regression. - **Dev-mode re-publish only works on apps that were first published from dev mode.** `is_authorized_to_republish` accepts `caller == owner OR caller == publisher`. In dev mode the publisher is always Alice (`5DfhGyQd…`), so dev-mode re-deploys of a dev-published app succeed. But an app first published from phone mode has `caller == publisher == user H160`; Alice is neither, so a dev-mode re-deploy reverts `Unauthorized`. To iterate on a phone-published app in dev mode the user must unpublish it from phone mode first. Intentional asymmetry: once a user "owns" an app from their phone, a shared dev key can't touch it. - **Build a dedicated Bulletin client with `heartbeatTimeout: 300_000` for the metadata upload.** The shared client from `getConnection()` uses `@parity/product-sdk-chain-client`'s default 40 s heartbeat; a single `TransactionStorage.store` round-trip can exceed that and the socket tears down as `WS halt (3)`. We mirror bulletin-deploy's 300 s heartbeat with a one-off client that gets destroyed immediately after the upload. -- **`dot deploy` does NOT pass `jsMerkle: true` today.** bulletin-deploy's pure-JS merkleizer produces CARs containing only raw leaves (DAG-PB blocks are silently dropped by `blockstore-core/memory`'s `getAll()` under `rawLeaves: true` + `wrapWithDirectory: true`) → polkadot-desktop parses zero files → sites return 404. We rely on the Kubo binary path until the upstream merkleizer collects all blocks, not just leaves. `dot init` installs `ipfs`. Trade-off: this temporarily breaks the RevX WebContainer story for the main storage upload — flip `jsMerkle: true` back once `merkleizeJS` is fixed. -- **Tx signing routes through host-papp `createTransaction`, NOT through `signRaw`.** `@parity/product-sdk-terminal@0.3.0`'s `createSessionSignerForAccount` hands the extrinsic to the wallet, which builds AND signs it — so every signed extension the chain declares (paseo-next-v2's `AsPgas`, `AuthorizeValueTransfer`, all of them) survives verbatim, with no PJS bridge, no relaxed-extensions wrapper, and no `` envelope ever touching a tx payload. `signBytes` still routes `signRaw({ tag: "Bytes" })` — that path keeps the `` anti-phishing envelope and is for arbitrary raw USER data only; never reach for it to sign extrinsic payloads. The CLI MUST pass the derived product-account publicKey to the SDK signer (`src/utils/sessionSigner.ts` always supplies `derivePlaygroundProductPublicKey(...)`); the SDK's fallback is the wallet's currently-selected account, which would produce signatures the chain rejects. +- **`playground deploy` does NOT pass `jsMerkle: true` today.** bulletin-deploy's pure-JS merkleizer produces CARs containing only raw leaves (DAG-PB blocks are silently dropped by `blockstore-core/memory`'s `getAll()` under `rawLeaves: true` + `wrapWithDirectory: true`) → polkadot-desktop parses zero files → sites return 404. We rely on the Kubo binary path until the upstream merkleizer collects all blocks, not just leaves. `playground init` installs `ipfs`. Trade-off: this temporarily breaks the RevX WebContainer story for the main storage upload — flip `jsMerkle: true` back once `merkleizeJS` is fixed. +- **The mobile app wraps `signRaw` data with ``** (anti-phishing envelope). On paseo-next-v2 this doesn't matter for tx signing: `@parity/product-sdk-terminal@0.2.1+`'s `createSessionSignerForAccount` routes tx payloads through `session.signRaw({ data: { tag: "Payload", value: hex(toSign) } })` — opaque bytes, no `` envelope — so every signed extension declared by the chain (including paseo-next-v2's `AsPgas`) survives end-to-end. Don't reach for `signRaw` to sign extrinsic payloads from anywhere outside the signer; raw-message signing keeps the `Bytes` tag for arbitrary user data. The pre-0.2.1 PJS path failed on v2 with `PJS does not support this signed-extension: AsPgas`. - **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/account/bulletinTopUp.ts` mirrors bulletin-deploy's internal `attemptTestnetTopUp`** so `dot init` front-loads the dev-funder top-up at setup time rather than waiting for the just-in-time call inside `deploy()`. Both flows no-op once the recipient is ≥ 0.1 PAS, so running them back-to-back doesn't double-transfer. Delete the local mirror only once bulletin-deploy surfaces `attemptTestnetTopUp` at the package root — today it's an internal `DotNS` method. +- **`src/utils/account/bulletinTopUp.ts` mirrors bulletin-deploy's internal `attemptTestnetTopUp`** so `playground init` front-loads the dev-funder top-up at setup time rather than waiting for the just-in-time call inside `deploy()`. Both flows no-op once the recipient is ≥ 0.1 PAS, so running them back-to-back doesn't double-transfer. Delete the local mirror only once bulletin-deploy surfaces `attemptTestnetTopUp` at the package root — today it's an internal `DotNS` method. ### Accounts: root, product, and what the mobile app shows - **`session.rootAccountId` is whatever the mobile app published as `rootUserAccountId` in the SSO handshake.** On current mobile builds (`polkadot-app-android-v2`, see `feature/sso/impl/.../RealSsoHandshakeUseCase.kt:34` → `deriveRootAccount() = derivationPath = null`) it's the bare-mnemonic sr25519 root with no junction. The host-papp SDK does not derive it — it just decodes the 32 bytes from `HandshakeResponseSensitiveData.rootUserAccountId` (`triangle-js-sdks/packages/host-papp/src/sso/auth/scale/handshake.ts:23-27`) and forwards them. If a future mobile release changes the path, our display will silently change with it — the source of truth is the phone, not the CLI. - **The mobile's "Wallet account address" and "Candidate account address" debug rows are NOT reachable from the host.** They're sr25519 of mnemonic + `//wallet` and mnemonic + `//candidate` respectively (`feature/account/impl/.../RealAccountRepository.kt:166-173`, hard junctions). Hard derivations can't be reproduced from a public key, so the CLI never sees those SS58s. Don't try to surface a "wallet address that matches mobile" — it isn't possible without the mnemonic. -- **The playground product account is derived by exactly one function** (`src/utils/sessionSigner.ts::derivePlaygroundProductPublicKey`), called by both `createPlaygroundSessionSigner` (which feeds the result as the SDK signer's `publicKey` — terminal 0.3.0 `createSessionSignerForAccount`, see the tx-signing bullet) and `auth.ts::deriveSessionAddresses` (display triple). The math is `deriveProductAccountPublicKey(rootAccountId, "playground.dot", 0)` from `@parity/product-sdk-keys`. Do NOT call `deriveProductAccountPublicKey` (or any helper that wraps it) on an already-product-derived SS58 — that yields a doubly-derived ghost account. The `productAccountDisplay` / `productAccountAddresses` helpers that used to live in `src/commands/init/identityLine.ts` had exactly this bug and were deleted; resist re-introducing them. A frozen-vector regression test in `src/utils/auth.test.ts` (`deriveSessionAddresses` block) locks the pubkey/H160 the playground-app expects. +- **The playground product account is derived by exactly one function** (`src/utils/sessionSigner.ts::derivePlaygroundProductPublicKey`), called by both `createPlaygroundSessionSigner` (signer construction) and `auth.ts::deriveSessionAddresses` (display triple). The math is `deriveProductAccountPublicKey(rootAccountId, "playground.dot", 0)` from `@parity/product-sdk-keys`. Do NOT call `deriveProductAccountPublicKey` (or any helper that wraps it) on an already-product-derived SS58 — that yields a doubly-derived ghost account. The `productAccountDisplay` / `productAccountAddresses` helpers that used to live in `src/commands/init/identityLine.ts` had exactly this bug and were deleted; resist re-introducing them. A frozen-vector regression test in `src/utils/auth.test.ts` (`deriveSessionAddresses` block) locks the pubkey/H160 the playground-app expects. - **Username storage is keyed on `session.rootAccountId`, not on the product account.** `Resources.Consumers[]` on the People parachain is populated by mobile's `Resources.register_person` call (signed by `//wallet`-derived key, but the storage key is the root). `lookupUsername` MUST be called with `addresses.rootAddress`, not the product SS58. Polkadot-desktop's `useSessionIdentity(session)` does the same — both read off the SSO `rootAccountId`. -- **`SessionAddresses` triples are computed once in `auth.ts` and threaded through.** `ConnectResult`, `LoginStatus.success`, and `SessionHandle` all carry the `{ rootAddress, productAddress, productH160 }` bundle. `SessionHandle.address` is kept as a back-compat alias for `addresses.productAddress` because `signer.ts::resolveSigner` spreads the handle into `ResolvedSigner` and downstream deploy code (`signerMode.ts`, `playground.ts`, `registry.ts`, `DeployScreen.tsx`) reads `.address` for the signing key. UI code should prefer `addresses` so the root vs product distinction stays explicit. `SessionHandle` now also exposes `adapter` (the `TerminalAdapter`) because the SDK `./host` allowance calls need it alongside the session; `ResolvedSigner` carries an optional `adapter`, present iff `source === "session"`. +- **`SessionAddresses` triples are computed once in `auth.ts` and threaded through.** `ConnectResult`, `LoginStatus.success`, and `SessionHandle` all carry the `{ rootAddress, productAddress, productH160 }` bundle. `SessionHandle.address` is kept as a back-compat alias for `addresses.productAddress` because `signer.ts::resolveSigner` spreads the handle into `ResolvedSigner` and downstream deploy code (`signerMode.ts`, `playground.ts`, `registry.ts`, `DeployScreen.tsx`) reads `.address` for the signing key. UI code should prefer `addresses` so the root vs product distinction stays explicit. ### Allowances / session -- **`getSessionSigner()` returns an adapter that keeps the Node event loop alive.** Every caller must invoke the returned `destroy()` when done. Forgetting it manifests as `dot ` hanging after the work visibly finishes. -- **RFC-0010 allowances come from `@parity/product-sdk-terminal/host` — there is no CLI-local shim.** The old `src/utils/allowances/host.ts` shim is DELETED; do NOT re-add it. The SDK `./host` module exports `requestResourceAllocation(session, adapter, resources, opts)`, `getCachedAllocation`, `ensureSlotAccountSigner`, and `createSlotAccountSigner`; these need both the session and its adapter (hence `SessionHandle.adapter`). `@parity/product-sdk-host`'s `requestResourceAllocation` is the in-container variant (browser globals required) and still won't work from the CLI — use the terminal `./host` one. CLI-local glue that remains: `src/utils/allowances/resources.ts` (`PLAYGROUND_RESOURCES`, `summarizeOutcomes`, `describeResource`) and `bulletin.ts`'s quota loop. -- **The SDK now owns both the grant marker and the slot keys** — one file, `~/.polkadot-apps/_AllowanceKeys.json` (0600, atomic write), managed entirely by `@parity/product-sdk-terminal/host`. A cache entry IS the grant marker: the SDK writes it only after the wallet returns `Allocated`, so a present key doubles as proof the grant happened. The old CLI-local `~/.polkadot/allowances.json` (`marker.ts`) and `~/.polkadot/allowance-keys.json` (`slotKeys.ts`) files are DEAD and both source files are DELETED — do NOT resurrect `marker.ts` / `slotKeys.ts` or any `{ env: { ss58: { resourceTag } } }` marker scheme. Gotcha: the SDK cache is NOT env-keyed. When `ACTIVE_TESTNET_ENV` changes, stale Bulletin slots surface as "not authorized on-chain" and `playground init` re-grants — don't try to read the cache as authoritative across envs. -- **`playground init` requests all three resources in ONE mobile dialog.** `PLAYGROUND_RESOURCES` (`src/utils/allowances/resources.ts`) = Bulletin + Statement Store + SmartContract(gas, 0) passed together to the SDK `requestResourceAllocation`, so the user sees a single approval dialog, not three. Usability is checked via `cachedBulletinSlotAuthorization` (verifies on-chain authorization through cloud-storage `checkAuthorization` AND remaining quota) — NEVER infer usability from "a key is cached". When Bulletin quota is exhausted, `getBulletinAllowanceSigner` (`bulletin.ts`) makes a single `Increase` retry on the phone, then re-checks; still-unusable throws a plain-English error. -- **`dot init --yes` auto-runs at the end of `install.sh`** to skip the interactive QR-scan so non-interactive installers don't block. It installs prerequisites and prints "setup complete", then `install.sh` prints a hint to run `dot init` for the full mobile login. Dep-setup failures surface their exit code so CI runs don't silently pass. +- **`getSessionSigner()` returns an adapter that keeps the Node event loop alive.** Every caller must invoke the returned `destroy()` when done. Forgetting it manifests as `playground ` hanging after the work visibly finishes. +- **`requestResourceAllocation` lives in a CLI-local shim** (`src/utils/allowances/host.ts`). `@parity/product-sdk-terminal@0.2.1` does NOT yet re-export the RFC-0010 host call at the package root, but the underlying `UserSession` (from `@novasamatech/host-papp`) does — we call it directly via the raw session on `SessionHandle.userSession`. `@parity/product-sdk-host`'s `requestResourceAllocation` is the in-container variant (browser globals required) and won't work from the CLI. Replace the shim when product-sdk-terminal surfaces it externally. +- **Allowance grant markers live at `~/.polkadot/allowances.json`** (`src/utils/allowances/marker.ts`), mode 0600, sibling to `accounts.json`. RFC-0010 has no on-chain query for allowance status, so we persist `{ env: { ss58Address: { resourceTag: { grantedAt, source } } } }` after a successful host grant. Slot-account private keys for Bulletin / Statement Store live separately in `~/.polkadot/allowance-keys.json` (`src/utils/allowances/slotKeys.ts`), also mode 0600. A marker alone isn't enough to skip `playground init` for slot resources — confirm the matching key exists too. Markers and keys are isolated per env. Keep `source: "host"` as the only value emitted from production code. +- **Bulletin is not requested through mobile resource allocation in `playground init`.** Until product-sdk exposes the proper terminal host/preimage path, the CLI creates or reuses a locally cached Bulletin slot key and surfaces `bulletinAuthorizationHelp(slot)` against the env's `bulletinAuthorizationUrl`. Always check usability via `hasUsableBulletinSlotAuthorization`, never just `hasSlotAccountKey`. +- **`playground init --yes` auto-runs at the end of `install.sh`** to skip the interactive QR-scan so non-interactive installers don't block. It installs prerequisites and prints "setup complete", then `install.sh` prints a hint to run `playground init` for the full mobile login. Dep-setup failures surface their exit code so CI runs don't silently pass. ### CLI surface boundaries - **`src/utils/deploy/*` and `src/utils/build/*` must not import React or Ink.** They form the SDK surface RevX consumes from a WebContainer. TUI code lives in `src/commands/*/`. -- **`dot mod` runs signer-less.** `runModCommand` does not call `resolveSigner` — it uses `getReadOnlyRegistryContract(rawClient)` (origin = Alice's SS58) for browse + metadata-uri lookup. The `--suri` flag is a deprecated no-op. Users browse + clone moddable apps without `dot init` / mapping their account. The signed `getRegistryContract(rawClient, signer)` is used only for `registry.publish.tx(...)` in `src/utils/deploy/playground.ts`. Don't drag a user signer back into `dot mod`. -- **`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` for public repos) and extracts via `node:zlib` + the pure-JS `tar` package. Do NOT re-introduce `git clone` or `gh repo fork` — both re-add a hard tooling dep, and the fork path was specifically removed because GitHub caps you to one fork per source-repo per account. The interactive picker filters out non-moddable apps. The picker does NOT pre-probe each app's repo visibility (would burn the 60 req/hr anonymous GitHub quota); instead `runModCommand` lazy-probes the picked app once via `assertPublicGitHubRepo()` between picker dismount and `SetupScreen` mount. -- **`dot` never invokes `gh`.** `dot deploy --moddable` reads an existing `origin`, validates it's a public GitHub URL via `HEAD https://github.com/{o}/{r}`, and records it in metadata. No auto-create path. Missing `origin`, private repos, and non-GitHub URLs all hard-fail with actionable messages from `src/utils/deploy/moddable.ts::resolveRepositoryUrl()`. We deliberately do NOT add an interactive `gh auth login` handoff — Ink owns stdout + raw-mode stdin and a `stdio: "inherit"` child would race `useInput` for keystrokes. +- **`playground mod` runs signer-less.** `runModCommand` does not call `resolveSigner` — it uses `getReadOnlyRegistryContract(rawClient)` (origin = Alice's SS58) for browse + metadata-uri lookup. The `--suri` flag is a deprecated no-op. Users browse + clone moddable apps without `playground init` / mapping their account. The signed `getRegistryContract(rawClient, signer)` is used only for `registry.publish.tx(...)` in `src/utils/deploy/playground.ts`. Don't drag a user signer back into `playground mod`. +- **`playground 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` for public repos) and extracts via `node:zlib` + the pure-JS `tar` package. Do NOT re-introduce `git clone` or `gh repo fork` — both re-add a hard tooling dep, and the fork path was specifically removed because GitHub caps you to one fork per source-repo per account. The interactive picker filters out non-moddable apps. The picker does NOT pre-probe each app's repo visibility (would burn the 60 req/hr anonymous GitHub quota); instead `runModCommand` lazy-probes the picked app once via `assertPublicGitHubRepo()` between picker dismount and `SetupScreen` mount. +- **`playground` never invokes `gh`.** `playground deploy --moddable` reads an existing `origin`, validates it's a public GitHub URL via `HEAD https://github.com/{o}/{r}`, and records it in metadata. No auto-create path. Missing `origin`, private repos, and non-GitHub URLs all hard-fail with actionable messages from `src/utils/deploy/moddable.ts::resolveRepositoryUrl()`. We deliberately do NOT add an interactive `gh auth login` handoff — Ink owns stdout + raw-mode stdin and a `stdio: "inherit"` child would race `useInput` for keystrokes. - **`metadata.repository` is set ONLY when `--moddable` is opted in.** `runDeploy` takes an explicit `repositoryUrl: string | null` and `publishToPlayground` writes the field iff that param is non-null. Earlier code silently probed `git remote get-url origin` and surprised users — don't reintroduce that behaviour. ### Runtime / memory @@ -78,8 +77,8 @@ These aren't self-evident from reading the code and have bitten us before. Treat - **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` evaluates, then maps `DOT_TELEMETRY`/internal-context detection to `BULLETIN_DEPLOY_TELEMETRY`. Don't leave `BULLETIN_DEPLOY_TELEMETRY` unset while setting the host app: `bulletin-deploy` treats `playground-cli` as an internal host, which would enable deploy telemetry for external users. - **Throttle TUI info updates.** bulletin-deploy logs per-chunk, builds stream thousands of lines/sec. `setState`-per-event floods React's reconciler with backpressure (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. Don't hook raw per-line streams directly into Ink state. - **`DeployLogParser.feed()` MUST NOT emit an event per log line.** It's called for every console line bulletin-deploy prints. We emit only for phase-banner matches and `[N/M]` chunk progress; everything else returns `null`. A catch-all `info` emit allocates ~200 bytes × thousands of lines and was a measurable contributor to chunk-upload memory pressure. -- **`startMemoryWatchdog()` runs for both `dot deploy` and `dot mod`.** Mod's tarball download is a streaming pipe through `node:zlib` + `tar.extract()`; a stuck IPFS gateway or malformed tarball can leak buffers. Any new top-level command doing meaningful I/O should also call `startMemoryWatchdog()` and register `stopWatchdog` via `onProcessShutdown()`. -- **`QueryResult` from `@parity/product-sdk-contracts@0.7` is a discriminated union.** Narrow on `.success` before reading `.value`. On the failure branch `.value` is the runtime's dispatch-error payload (`unknown`). On the success branch `gasRequired` is non-optional. We apply this in `src/commands/mod/AppBrowser.tsx` and `src/commands/mod/SetupScreen.tsx`. +- **`startMemoryWatchdog()` runs for both `playground deploy` and `playground mod`.** Mod's tarball download is a streaming pipe through `node:zlib` + `tar.extract()`; a stuck IPFS gateway or malformed tarball can leak buffers. Any new top-level command doing meaningful I/O should also call `startMemoryWatchdog()` and register `stopWatchdog` via `onProcessShutdown()`. +- **`QueryResult` from `@parity/product-sdk-contracts@0.5+` is a discriminated union.** Narrow on `.success` before reading `.value`. On the failure branch `.value` is the runtime's dispatch-error payload (`unknown`). On the success branch `gasRequired` is non-optional. We apply this in `src/utils/contractManifest.ts::resolveLiveContractAddresses`, `src/commands/mod/AppBrowser.tsx`, and `src/commands/mod/SetupScreen.tsx`. ## Repo conventions @@ -89,7 +88,7 @@ These aren't self-evident from reading the code and have bitten us before. Treat - Do NOT add AI/tool attribution (`Co-Authored-By: Claude`, "Made with Cursor", emoji signatures) 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 — 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 ` hanging after the work is visibly finished. +- 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 `playground ` hanging after the work is visibly finished. ## Sentry telemetry @@ -113,7 +112,7 @@ These aren't self-evident from reading the code and have bitten us before. Treat # Product context: playground.dot -Source: Playground Full Spec v0.18, May 2026. Team: Ionut (TL), Rebecca (PM), Charles, Utkarsh, Todor, Reinhard, Sveta (Design), Karim (Dept), RevX team (parallel). Kanban: https://github.com/orgs/paritytech/projects/278. +Source: Playground Full Spec v0.31, June 2026. Team: Ionut (TL), Rebecca (PM), Charles, Utkarsh, Todor, Reinhard, Sveta (Design), Karim (Dept), RevX team (parallel). Kanban: https://github.com/orgs/paritytech/projects/278. ## What it is @@ -121,6 +120,8 @@ playground.dot is a mobile-first quest platform for the Web3 Summit Developer La **V1 is the only active build target.** V2+ are directional ideas — do not implement unless an issue or PR explicitly requests it. +**North-star KPI (Gav):** quality of human interactions and engagement. Fewer apps is fine — total app count is a secondary metric. A smaller registry of quality apps beats a large registry padded with spam. + ## App structure Three tabs in the playground-app (not a single "registry browser"). All three are V1 scope: @@ -138,7 +139,7 @@ Three tabs in the playground-app (not a single "registry browser"). All three ar | Repo | Role | |---|---| | `paritytech/playground-app` | Registry + Playground tab + Profile | -| `paritytech/playground-cli` (this repo) | `dot` CLI | +| `paritytech/playground-cli` (this repo) | playground CLI (binary: `playground`, short alias `pg`) | | `paritytech/Rock-Paper-Scissors` | Rock Paper Scissors tutorial (4 levels) | | `paritytech/playground-app-template` | Blank-canvas starter | | `paritytech/product-sdk` | Publishes `@parity/product-sdk-*` | @@ -150,7 +151,7 @@ Three tabs in the playground-app (not a single "registry browser"). All three ar | Component | Owned by | Role | |---|---|---| | **playground-app** | Frontend/contract team | Three tabs, App Detail Page, publish pipeline | -| **`dot` CLI** (this repo) | CLI team | Local IDE path: `dot init`, `dot mod`, `dot build`, `dot deploy --playground`, `dot logout`, `dot update` | +| **playground CLI** (this repo) | CLI team | Local IDE path: `playground init`, `playground mod`, `playground build`, `playground deploy --playground`, `playground decentralise`, `playground logout`, `playground update`. `pg` is interchangeable everywhere (`pg init` == `playground init`). | | **RevX** | Talles / RevX | Browser IDE; opens via `revx.dev/editor?mod=&quest=` (`quest=` for tutorial only) | | **Tutorial** | Todor | Rock Paper Scissors (4 levels, ~30 min) | | **Sample apps** (~4 for V1, ≥10 for V2) | Various | Each is its own repo with `setup.sh` + `.claude/skills/`. Quest ideas live in the README — no `quests.json`. Feedback Board (Todor) is built; The Ballot, Kudos, Countdown, Pact are candidates. | @@ -161,7 +162,7 @@ Three tabs in the playground-app (not a single "registry browser"). All three ar ## Network -**Current:** Paseo Next v2 (`ACTIVE_TESTNET_ENV = "paseo-next-v2"` in `src/config.ts`). +**Current:** Paseo Next v2 — migrated from PreviewNet (`ACTIVE_TESTNET_ENV = "paseo-next-v2"` in `src/config.ts`). Bulletin Chain went live on Polkadot mainnet 7 May 2026. **Summit network:** the event itself runs on a **Summit-specific closed devnet** operated by Parity. All participants get pre-allocated allowances — **no storage or PGAS constraints during the event**. The devnet switches off at the closing ceremony and apps cease to exist. "Save your repo to GitHub" is the consistent message. **Don't hardcode "Paseo Next v2" as the permanent network** — the Summit devnet is a separate deployment, gated by `src/config.ts::CONFIGS`. @@ -176,7 +177,7 @@ Sign-in is **never** described as "wallet" in the product — it's an **account* 3. The session key is pre-loaded via a single `host_request_resource_allocation([BulletinAllowance, StatementStoreAllowance, SmartContractAllowance])` call: one authorisation dialog, then the session flows. 4. From then until logout, publish + on-chain interactions are signed by the session key. The user is never asked to top up, fund, or acquire tokens. -`dot logout` signs out, notifies the mobile app, and clears the local session. +`playground logout` signs out, notifies the mobile app, and clears the local session. **The CLI must not present fee-acquisition UX.** If you find yourself designing a "buy tokens" or "top up" flow, something has gone wrong upstream. Session keys are confirmed kept for Summit — without them every action needs phone approval and batching breaks PGAS. @@ -190,7 +191,7 @@ Sign-in is **never** described as "wallet" in the product — it's an **account* Budget is sufficient for ~180–200 transactions across 2 days. PGAS claim path is **v5 extrinsic only** (mobile-only); spending PGAS is v4 and works everywhere. **Batching transactions breaks PGAS fee payment** — the publish flow must remain sequential individual transactions. -Summit devnet allowances are pre-allocated. Vouchers, soft-limit messaging, Bulletin expiry countdown UI, and `dot voucher` are all **removed from V1** — don't reintroduce. +Summit devnet allowances are pre-allocated. Vouchers, soft-limit messaging, Bulletin expiry countdown UI, and `playground voucher` are all **removed from V1** — don't reintroduce. ## The publish flow (5 steps, all paid by the session key) @@ -199,11 +200,15 @@ Summit devnet allowances are pre-allocated. Vouchers, soft-limit messaging, Bull | 1 | Upload frontend assets + metadata to Bulletin | "Uploading to Bulletin..." | | 2 | Reserve `.dot` domain on Polkadot Hub | "Registering your .dot domain..." | | 3 | Register on the playground registry | "Publishing to playground registry..." | -| 4 | Link app to user account | "Linking to your account..." | -| 5 | Generate a shareable link | "Your app is live!" | +| 4 | Live URL ready | "Your app is live at `yourapp.dot.li`." | +| 5 | Share link ready | "Share: `playground.dot/app/yourapp.dot`" (copyable) | + +Steps 4 and 5 are the two links the user copies/pastes to share — the live `.dot.li` Bulletin URL (to *open* the app) and the playground detail page deep link (to let others *mod* it). Internally Bulletin upload and registry publish run in parallel; the user-facing pipeline preserves the 5-tick mental model. Plain-English error messages — never hex revert codes. Retries are safe: Bulletin uploads dedupe by content, DotNS skips if already owned, registry updates existing entry. Re-deploys show "Updating myapp.dot", not "Publishing myapp.dot". Account switch mid-publish aborts with `Account changed mid-publish — please re-run from the new account`. +**Current state vs spec.** Pipeline runs end-to-end, but only 4 statuses surface in the UI today (`preparing` / `uploading` / `publishing` / `done`); the spec's 5 named steps aren't all distinctly labelled yet. Treat the table above as target, not current implementation. + **Publish validation (V1):** domain uniqueness (DotNS contract, first on-chain tx wins) and required fields. Image format/size limits deferred to V2. **Post-deploy CLI output target:** live URL (`yourapp.dot.li`) + playground detail link + share CTA ("Share your app — let others mod it") + sovereignty line ("Your app is live on Bulletin Chain, registered on Polkadot Hub, accessible at yourapp.dot.li. Nobody controls this but you.") + name reveal ("You're live as [current display name]. To set a different username for playground.dot, go to My Profile in playground.dot.") + moddable nudge + docs link. @@ -221,9 +226,9 @@ Three tiers share the same contract; the frontend differentiates via pinning + A | 3 | The Leaderboard | Deploy leaderboard smart contract | ❌ CLI + laptop only (RevX dropped Solidity/Rust support) | | 4 | Multiplayer | P2P via Statement Store. Challenge via link/QR | ❌ CLI + laptop only | -**XP:** 100 XP **flat** on tutorial deploy — one award for completing the tutorial track, not per level. Requires the tutorial flag (CR6 — see XP section). IslandPortal popup currently shows 400 XP / 90 min — should be 100 XP / ~30 min. Align with Todor. +**XP:** 100 XP awarded on the user's first-ever deploy (deploy #1 on the account), 50 XP on the second. In practice the first deploy will almost always be the tutorial deploy. After that, deploys earn 0 XP — reward shifts to social signal (stars + mods received). See CR6 in the XP section. IslandPortal popup framing should say "your first deploy earns 100 XP" (not "tutorial = 100 XP") — the award is on deploy #1 regardless of source. -**Tier 2 — Sample apps** (~4 V1, ≥10 V2). Each is its own repo, pinned. **No `quests.json`** — quest ideas live in the README. Sample app deploys earn 0 XP under v0.18 scoring (no XP for the act of deploying — see XP section). **Feedback Board** (Todor) is built. The Ballot, Kudos, Countdown, Pact are candidates — 3 more sample apps need commissioning + builders before 31 May. +**Tier 2 — Sample apps** (~4 V1, ≥10 V2). Each is its own repo, pinned. **No `quests.json`** — quest ideas live in the README. Once the user has used their two first-deploy bonuses, sample app deploys earn 0 XP (see XP section). **Feedback Board** (Todor) is built. The Ballot, Kudos, Countdown, Pact are candidates — 3 more sample apps need commissioning + builders. Sample app spec: start from `playground-app-template`, ship a README (quest ideas + SDK packages + key files), idempotent `setup.sh`, `.claude/skills/app-context.md` (~10 lines). Must be moddable (public GitHub). Size limit: one Bulletin chunk (~10 MB, TBC). Naming `sample--app`. @@ -239,23 +244,30 @@ Points are referred to as **XP** throughout V1. | Action | XP displayed | Raw contract | Notes | |---|---|---|---| -| Tutorial completed | 100 | 10 | Flat on tutorial deploy. Requires tutorial flag (CR6 — pending). | -| New app deployed | 0 | 0 | Deploying with AI is low-skill; XP rewards what others think of your app, not the act. | +| First-ever deploy | 100 | 10 | Awarded once per account when `deploy_count == 1`. In practice the user's tutorial deploy (the structured first step). Tracked via per-account `deploy_count` counter on the registry — see CR6. | +| Second-ever deploy | 50 | 5 | Awarded once per account when `deploy_count == 2`. In practice the user's first solo / non-tutorial app. | +| Subsequent deploys | 0 | 0 | Reward shifts entirely to social signal (stars + mods received). Keeps the Apps tab usable as a discovery surface. | | Moddable deploy bonus | 0 | 0 | No bonus; the incentive to be moddable is the much larger "your app is modded" payout. | | Star received | 10 | 1 | Per star awarded to your app. | | Someone mods your app | 50 | 5 | Strongest single-signal award. Dedupe per `(modder, source_domain)`. | +**Why first-N-deploys instead of a tutorial flag.** Earlier scoring used a per-deploy `is_tutorial: bool` flag to award 100 XP for tutorial completion. The flag was gameable — any caller hitting the contract directly could set it `true` and farm 100 XP per deploy. The new counter approach is ungameable on the contract side (no flag to lie about) and lands almost the same outcome: deploy #1 is in practice the tutorial deploy, deploy #2 is the user's first solo app. Anything in the code/docs referring to a "tutorial flag" or `is_tutorial` field is stale — the replacement is a per-account `deploy_count` on the registry. + The contract stores raw values; UI applies a uniform 10× multiplier on display. CLI output that surfaces XP should match — multiply contract reads by 10 before showing the user. -**Stars = what users award.** Binary, one-way, permanent. Cumulative count displayed (never average X.X / 5). Self-starring forbidden at the contract level. Unlimited per user. Each star earns the app owner 10 XP. **No `unstar` method** (CR2 — code change pending): stars are an XP transfer, and an unstar method would be a points-removal griefing vector. +**Contract scoring constants — pending v14 redeploy.** CR1 drops `launch_delta` to 0 (currently `2 + if moddable { 1 } else { 0 }`) and bumps your-app-is-modded from raw 1 to raw 5. CR6 adds the per-account `deploy_count` for the first-N bonuses. Without these landing in the v14 redeploy, deploys still earn 20 XP each and modded apps earn 10 XP per mod — the prize behaviour won't match what the spec promises. -**`modded_from` is off-chain metadata, not contract storage.** At publish time the CLI passes `modded_from` as a transient `publish()` parameter — the contract uses it to award the "your app is modded" XP to the source owner and update `mod_credited`, then discards it. The "Modded from: domain01.dot" lineage rendered on the App Detail Page reads from the off-chain Bulletin metadata blob. +**Stars = what users award.** Binary, one-way, permanent. Cumulative count displayed (never average X.X / 5). Self-starring forbidden at the contract level. Unlimited per user. Each star earns the app owner 10 XP. -**Leaderboard is V1.** Top Builders reads `get_top_builders` and applies the 10× display multiplier. "Most starred" and "most modded" sort options on the Apps grid are V2. +**CR2 status: `unstar` is still in the contract source AND the UI still has `handleUnstar`.** Spec position: stars should be one-way / permanent — the unstar path is a points-removal griefing vector (much easier to organise removing points from a competitor than awarding them). Both contract and UI changes pending. + +**`modded_from` is a transient `publish()` parameter, BUT lineage IS recorded on-chain.** v11 of the registry added `get_lineage(start, count)` / `get_lineage_count()`: each `(child, source)` edge is recorded in `lineage_at` with `lineage_recorded` per-domain dedupe. The CLI passes `modded_from` to award the "your app is modded" XP to the source owner and update `mod_credited`, then the contract also writes the lineage edge. The "Modded from: domain01.dot" string rendered on the App Detail Page still reads from the off-chain Bulletin metadata blob — pick whichever fits the call site. + +**Leaderboard is V1.** Top Builders reads `get_top_builders` and applies the 10× display multiplier. "Most starred" and "most modded" sort options on the Apps grid are V2 (in progress, on-chain sort indexes — Reinhard). ## Prize logistics -~$2,000 prize pool, split four ways at $500 each: +~$2,000 main pool, split four ways at $500 each, plus a small narrative "Spark" prize: | Prize | Awarded for | Determined by | |---|---|---| @@ -263,8 +275,9 @@ The contract stores raw values; UI applies a uniform 10× multiplier on display. | Most Modded App | The single app with highest `mod_count` | On-chain per-app counter | | Most Starred App | The single app with highest `star_count` | On-chain per-app counter | | Wildcards | Judges' picks for innovative or noteworthy apps | Judges at venue, off-chain | +| **Spark** (narrative) | Source app at the root of the longest mod chain at closing | Off-chain walk of `modded_from` metadata, or on-chain via `get_lineage`. Small prize (~$50 / merch / recognition); stacks with other prizes. | -Ties on per-app prizes are split equally. Tutorial completion is verifiable from the registry via the tutorial flag — no manual verification needed. +Ties on per-app prizes are split equally. No "tutorial completion" verification step — prize eligibility reads directly from on-chain XP and per-app `mod_count` / `star_count`. The first-N-deploys bonuses (CR6) replace the old tutorial-flag mechanic. ## Display names @@ -276,29 +289,33 @@ Precedence (implemented in playground-app via `displayNameForAccount`): CLI output that surfaces the user's display name should match the precedence — read the registry first, fall back to wallet name, fall back to truncated H160. The "You're live as [current display name]" line in post-deploy output uses the same lookup. -**Out of scope:** adjective-noun name generation, Bulletin storage for names, first-encounter ceremony reveal moment. The wallet-name fallback handles the common case; the modal handles the upgrade path. +**V1 prompts to upgrade** (in the playground-app, not CLI): first-star auto-prompt and leaderboard banner. V2 adds polish (#22a-d): auto-prompt SetUsernameModal, leaderboard banner copy, adjective-adjective-noun handle fallback for users with no registry username AND no wallet name (`quiet-curious-otter`-style, stored on Bulletin off-chain), and first-encounter name reveal. + +**Out of scope for V1:** adjective-noun name generation (V2 #22c), Bulletin storage for names (V2), first-encounter ceremony reveal moment (V2 #22d). The wallet-name fallback handles the common case; the modal handles the upgrade path. ## RevX deep-link contract `revx.dev/editor?mod=&quest=` — `mod=` required; `quest=` only for the tutorial (RevX reads `quests.json`, checks out the right branch, loads the per-level AI skill). Single "Open in RevX" button per app, same for tutorial / sample / participant apps. -RevX downloads source as HTTPS tarball (same as the CLI). After load: PoP auth (QR on desktop, direct on mobile), AI chat pre-loaded with the template's `CLAUDE.md` + Product SDK skills, CLI bridge maps RevX UI actions to `dot build`, `dot deploy --playground`. RevX should default to working RPC config so testers don't need to manually switch network. +RevX downloads source as HTTPS tarball (same as the CLI). After load: PoP auth (QR on desktop, direct on mobile), AI chat pre-loaded with the template's `CLAUDE.md` + Product SDK skills, CLI bridge maps RevX UI actions to `playground build`, `playground deploy --playground`. RevX should default to working RPC config so testers don't need to manually switch network. + +**RevX deep-link prepopulated prompts (built June 2026, V2 #86).** RevX accepts `?prompt=` (clears project, loads starter Rust template, opens `src/starter.rs`, activates `polkavm` skill, auto-submits the prompt). Companion params: `?import=` (load by Bulletin CID), `?example=`, `?fresh=1`, plus the original `?mod=`. So an IslandPortal CTA can open RevX with `?prompt=start%20tutorial` and the user is immediately in a building-with-AI state. ⚠️ **Web container constraint:** the RevX browser web container is Node/TS/JS only — cannot run the IPFS binary. The CLI's Kubo-binary path (see `jsMerkle: false` invariant) blocks RevX's main storage upload until bulletin-deploy's pure-JS merkleizer is fixed. -## CLI deep-link contract (`dot mod`) +## CLI deep-link contract (`playground mod`) -`dot mod` downloads source as an HTTPS tarball via `codeload.github.com` — no git, no `gh`, no clone. Forms: `dot mod` (interactive picker over moddable apps), `dot mod ` (direct). After download, `setup.sh` runs and stays visible/logged. `dot mod` writes the source domain into deploy metadata; at publish time the CLI passes it as the transient `modded_from` parameter to the registry's `publish()`, which awards the source owner the "your app is modded" XP and updates `mod_credited`. +`playground mod` downloads source as an HTTPS tarball via `codeload.github.com` — no git, no `gh`, no clone. Forms: `playground mod` (interactive picker over moddable apps), `playground mod ` (direct). After download, `setup.sh` runs and stays visible/logged. `playground mod` writes the source domain into deploy metadata; at publish time the CLI passes it as the transient `modded_from` parameter to the registry's `publish()`, which awards the source owner the "your app is modded" XP and updates `mod_credited`. The contract also records the lineage edge in `lineage_at` (v11+). -Subsequent commands: `dot build` (auto-detects Rust/Solidity/EVM + frontend, picks the package manager), `dot deploy --playground` (full 5-step pipeline). The moddable-by-default fix (#24) is V1 P0 — current code defaults non-moddable and Session 02 testers (Will, others) hit `--moddable requires a GitHub origin` and were stopped from deploying. +Subsequent commands: `playground build` (auto-detects Rust/Solidity/EVM + frontend, picks the package manager), `playground deploy --playground` (full 5-step pipeline). The moddable-by-default fix (#24) is V1 P0 — current code defaults non-moddable and Session 02 testers (Will, others) hit `--moddable requires a GitHub origin` and were stopped from deploying. ## Moddable default flow -`dot deploy --playground` should default to moddable. Current code defaults non-moddable — Session 02 testers hit `--moddable requires a GitHub origin` and were blocked from deploying. The spec-level intent is to read an existing public GitHub origin, deploy moddable automatically, and prompt only if missing. **The CLI itself never invokes `gh`** (see invariants above) — that's the playground-app's job, not the CLI's. Non-moddable apps still get DotNS + Bulletin links; they just can't be cloned. +`playground deploy --playground` should default to moddable. Current code defaults non-moddable — Session 02 testers hit `--moddable requires a GitHub origin` and were blocked from deploying. The spec-level intent is to read an existing public GitHub origin, deploy moddable automatically, and prompt only if missing. **The CLI itself never invokes `gh`** (see invariants above) — that's the playground-app's job, not the CLI's. Non-moddable apps still get DotNS + Bulletin links; they just can't be cloned. ## quests.json (tutorial only) -Only the tutorial ships a `quests.json` — it's the manifest RevX reads to check out per-level branches and load per-level AI skill files (`.claude/skills/level-N-*.md`). Sample apps do NOT have a `quests.json` — quest ideas in their README are plain text inspiration. The CLI `--quest` flag was removed because the picker happens inside the editor (RevX's QuestPickerDialog or `dot mod`'s SetupScreen), not because quests are gone. +Only the tutorial ships a `quests.json` — it's the manifest RevX reads to check out per-level branches and load per-level AI skill files (`.claude/skills/level-N-*.md`). Sample apps do NOT have a `quests.json` — quest ideas in their README are plain text inspiration. The CLI `--quest` flag was removed because the picker happens inside the editor (RevX's QuestPickerDialog or `playground mod`'s SetupScreen), not because quests are gone. ```json { @@ -327,23 +344,25 @@ The tutorial repo also ships `setup.sh` + `.claude/skills/`. Generic Product SDK ## V1 CLI feature scope -- `dot init` — first-time setup, QR auth, session key, dependency install (login + toolchain run concurrently), funding, account mapping, Bulletin allowance, optional playground username claim. Alice grants 1000 tx / 100MB. Alice sends 10 PAS if balance < 1 PAS. `Revive.map_account` signed by user. -- `dot mod` — HTTPS tarball via `codeload.github.com`, interactive picker over moddable apps, source-domain capture, moddable preflight check. -- `dot build` — auto-detect Rust/Solidity/EVM + frontend, picks the package manager. -- `dot deploy --playground` — full 5-step pipeline. Flags: `--signer dev|phone`, `--domain`, `--buildDir`, `--no-build`, `--playground`, `--private`, `--moddable`/`--no-moddable`, `--suri`, `--env` (defaults to `paseo-next-v2`). -- `dot contract` — contract install + deploy. -- `dot decentralize` — point at a live static site URL (e.g. a GitHub Pages page), get back a `.dot` URL hosted on Bulletin. Interactive TUI by default; headless with `--site=`. Optional `--playground` flag also publishes to the playground registry. The spec lists this as V2.5 `dot import`; the CLI ships it earlier under a different name. -- `dot logout`, `dot update` (self-update from GitHub releases). +- `playground init` — first-time setup, QR auth, session key, dependency install (login + toolchain run concurrently), funding, account mapping, Bulletin allowance, optional playground username claim. Alice grants 1000 tx / 100MB. Alice sends 10 PAS if balance < 1 PAS. `Revive.map_account` signed by user. +- `playground mod` — HTTPS tarball via `codeload.github.com`, interactive picker over moddable apps, source-domain capture, moddable preflight check. +- `playground build` — auto-detect Rust/Solidity/EVM + frontend, picks the package manager. +- `playground deploy --playground` — full 5-step pipeline. Flags: `--signer dev|phone`, `--domain`, `--buildDir`, `--no-build`, `--playground`, `--private`, `--moddable`/`--no-moddable`, `--suri`, `--env` (defaults to `paseo-next-v2`). +- `playground contract` — contract install + deploy. +- `playground decentralise ` (CLI v0.26.0+) — point at a live static site URL (e.g. a GitHub Pages page), get back a `.dot` URL hosted on Bulletin. Interactive TUI: URL → signer → name → publish to Bulletin + .dot. Optional `--playground` flag also publishes to the playground registry. Powers the IslandPortal "Launch your first .dot site" quest on the Playground tab. Note: spelling is `decentralise` (British, matches spec); some earlier code references `decentralize`. +- `playground logout`, `playground update` (self-update from GitHub releases). +- **Signer behaviour:** `--signer dev` requires 0-1 phone approvals (Alice signs); `--signer phone` requires 3-4. Interactive prompt if omitted. +- **`--env` flag** defaults to `paseo-next-v2`. Other envs throw "not supported" until wired into `CONFIGS`. - Plain-English error messages for all common on-chain failures. Session 02 raw-error blockers: PoP/DotNS ~100-word unrecoverable error, chunk-verification `Missing CIDs: bafkrei...` mid-deploy, raw npm EEXIST and JSON Parse EOF errors. -- Mobile signing hang detection — inline fallback prompt if mobile signing has no response after N seconds: "Mobile signing hasn't responded — retry, or use a dev signer? [y/N]". -- Up-front phone approval count on `dot deploy`: "This will need 3 approvals on your phone — keep it ready." -- `dot mod` post-clone UX — auto-`cd` into cloned dir or surface a clear copyable `cd ` line; detect when `dot deploy` is run from outside a project and print a helpful message. +- Mobile signing hang detection — inline fallback prompt if mobile signing has no response after N seconds: "Mobile signing hasn't responded — retry, or use a dev signer? [y/N]". (V2 — #21b) +- Up-front phone approval count on `playground deploy`: "This will need 3 approvals on your phone — keep it ready." (V2 — #21c) +- `playground mod` post-clone UX — auto-`cd` into cloned dir or surface a clear copyable `cd ` line; detect when `playground deploy` is run from outside a project and print a helpful message. (V2 — #21a) -**Removed from V1, do not reintroduce:** `dot voucher`, conditional voucher prompt at `dot init`, soft-limit communication, Bulletin expiry countdown / two-week expiry narrative. +**Removed from V1, do not reintroduce:** `playground voucher`, conditional voucher prompt at `playground init`, soft-limit communication, Bulletin expiry countdown / two-week expiry narrative. -## CLI command rename (open) +## CLI binary name (closed) -`dot` is being renamed — it collides with too many existing tools and with Polkadot's own product family. Candidates so far: `play-dot`, `playdot`, `dotdeploy`. Affects binaries, `install.sh`, README, IslandPortal copy, on-site materials, tutorial scripts. Decision needed before on-site materials are printed. +Closed in spec v0.25. Binary is **`playground`** with short alias **`pg`** — both interchangeable (`playground init` == `pg init` for every subcommand). Old `dot` binary no longer installed. CLI v0.27.0 (1 June 2026). Old code paths, comments, file/dir names referring to `dot` should be renamed opportunistically; user-facing strings ARE renamed in current code per the Session 02 sweep. ## Vocabulary the product uses @@ -354,7 +373,7 @@ CLI output, error messages, and command names should follow: | Taking on a challenge | accept a quest / join a quest | try / attempt / do | | Modifying an app | mod (verb and noun) | remix / fork / clone | | The modified version | your mod / your app | your fork / your remix | -| Full deploy + publish | `dot deploy --playground` | dot ship | +| Full deploy + publish | `playground deploy --playground` | dot ship | | Publishing to the registry | deploy / publish | submit / upload / release | | The structured tutorial | Rock Paper Scissors tutorial / the tutorial | The Stadium / Polkadot Games Tutorial | | Open-ended modding challenge | quest idea | hackathon / challenge | @@ -364,6 +383,8 @@ CLI output, error messages, and command names should follow: | Host ↔ product transport layer | TrUAPI | TruAPI / Host API / triangle-js-sdk / host-api | | App others can mod | **moddable** (two d's) | modable (one d) | | Leaderboard score | **XP** | points (legacy term) | +| The registry browser tab | **Apps** | dAppStore / store / dApp store | +| The command-line tool | playground CLI (binary `playground`, alias `pg`) | DOT CLI / Polkadot CLI / the dot CLI | ## Out of scope (per spec) @@ -374,7 +395,7 @@ CLI output, error messages, and command names should follow: - Account creation outside the Polkadot app / PoP flow. - Contract-modding on mobile (Level 1 / UI-only quests on phone). - Chat Extensions sharing. -- Vouchers / `dot voucher` / soft-limit messaging / Bulletin expiry countdown UI. +- Vouchers / `playground voucher` / soft-limit messaging / Bulletin expiry countdown UI. - Account status component — parked, intentional given the devnet. - DOT airdrop as a W3S mechanism. - Display name generation — no adjective-noun generator, no Bulletin storage for names, no first-encounter ceremony.