@mysten-incubation/devstack 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard-ui/assets/{grpc-a4usE3Nk.js → grpc-Clz0oOtc.js} +1 -1
- package/dashboard-ui/assets/{index-CRYJ4pru.js → index-Cmqv9kiU.js} +39 -33
- package/dashboard-ui/index.html +1 -1
- package/dist/api/run-stack-internal.mjs +1 -0
- package/dist/api/run-stack-internal.mjs.map +1 -1
- package/dist/build-integrations/playwright/config.d.mts +2 -1
- package/dist/build-integrations/playwright/config.mjs +1 -1
- package/dist/build-integrations/playwright/config.mjs.map +1 -1
- package/dist/build-integrations/vite/index.d.mts +39 -9
- package/dist/build-integrations/vite/index.mjs +47 -11
- package/dist/build-integrations/vite/index.mjs.map +1 -1
- package/dist/build-integrations/vitest/config.mjs +3 -4
- package/dist/build-integrations/vitest/config.mjs.map +1 -1
- package/dist/cli/doctor-probes.mjs +7 -2
- package/dist/cli/doctor-probes.mjs.map +1 -1
- package/dist/cli/main.mjs +2 -2
- package/dist/cli/main.mjs.map +1 -1
- package/dist/cli/wirings/codegen.mjs +35 -35
- package/dist/cli/wirings/codegen.mjs.map +1 -1
- package/dist/cli/wirings/up.mjs +7 -0
- package/dist/cli/wirings/up.mjs.map +1 -1
- package/dist/orchestrators/boot.mjs +12 -2
- package/dist/orchestrators/boot.mjs.map +1 -1
- package/dist/orchestrators/codegen/service.mjs +23 -8
- package/dist/orchestrators/codegen/service.mjs.map +1 -1
- package/dist/plugins/dashboard/domain.mjs +18 -1
- package/dist/plugins/dashboard/domain.mjs.map +1 -1
- package/dist/plugins/dashboard/schema/root.mjs +2 -2
- package/dist/plugins/dashboard/schema/root.mjs.map +1 -1
- package/dist/plugins/dashboard/schema/types.mjs +15 -2
- package/dist/plugins/dashboard/schema/types.mjs.map +1 -1
- package/dist/plugins/deepbook/index.d.mts +7 -3
- package/dist/plugins/deepbook/index.mjs +3 -3
- package/dist/plugins/deepbook/index.mjs.map +1 -1
- package/dist/plugins/sui/index.d.mts +10 -10
- package/dist/plugins/sui/move-summary-runner.mjs +29 -2
- package/dist/plugins/sui/move-summary-runner.mjs.map +1 -1
- package/dist/substrate/runtime/index.mjs +1 -0
- package/dist/substrate/runtime/lifecycle/file-watcher.d.mts +1 -0
- package/dist/substrate/runtime/lifecycle/file-watcher.mjs +54 -0
- package/dist/substrate/runtime/lifecycle/file-watcher.mjs.map +1 -0
- package/dist/substrate/runtime/lifecycle/index.mjs +1 -0
- package/dist/substrate/runtime/lifecycle/watch-attribution.mjs +76 -6
- package/dist/substrate/runtime/lifecycle/watch-attribution.mjs.map +1 -1
- package/dist/substrate/runtime/supervisor/start-supervisor.mjs +2 -6
- package/dist/substrate/runtime/supervisor/start-supervisor.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.mjs","names":[],"sources":["../../../src/orchestrators/codegen/service.ts"],"sourcesContent":["// Codegen orchestrator — main service.\n//\n// Architecture §6 (Codegenable):\n// \"Codegen is a surface (L4) that walks the plugin-emitted\n// `Codegenable` capability decls.\"\n//\n// Hard boundary (distilled-doc §\"Hard boundary\"):\n// \"Apps consume codegen output, NOT devstack.\" Every runtime value\n// the app needs flows through codegen-emitted TS files. The\n// orchestrator owns the only legitimate channel from stack state\n// to app code.\n//\n// Lifecycle (distilled-doc §\"Lifecycle states\"):\n// - at-up: per supervisor cycle, run all emitters serially,\n// with per-file atomic/idempotent writes inside a\n// cycle-level stage-and-swap (rollback on any\n// per-file failure leaves the user-visible tree\n// unchanged).\n// - on-change: re-run as part of the new cycle when the\n// supervisor restarts.\n// - on-demand: same emit pipeline for snapshot resume.\n// - idempotency: per-file no-touch on unchanged content.\n//\n// Pipeline (one cycle):\n// 1. Collect `Codegenable` contributions from the active stack\n// members (the supervisor walks `member.capabilities`).\n// 2. Validate uniqueness: emitter name (literal) is globally\n// unique; output path is globally unique.\n// 3. Run each emitter serially (distilled-doc § \"Serial within a\n// cycle\"). Each emit writes exports through a per-file context.\n// 4. Render each collected file emission to a TS source string.\n// 5. Emit each file with an atomic write + per-file no-touch\n// idempotency.\n// 6. Run the Move-bindings emitter against the collected\n// `package`-emitted contributions.\n// 7. Write the `.gitignore`.\n//\n// What this module does NOT do:\n// - Construct plugin-level resolved blobs (plugins pass them at\n// factory-build time).\n// - Decode the manifest envelope (plugins pass resolved blobs at\n// factory-build time; the orchestrator never re-reads the envelope).\n// - Watch files. Re-emit is driven by the supervisor cycle (and\n// on-demand by the CLI); the app's own toolchain (Vite/HMR)\n// watches the emitted output tree.\n// - Walk the user's Move-source mtimes (see `bindings.ts`).\n\nimport { Context, Effect, FileSystem, Layer, Order, Ref, Scope } from 'effect';\nimport { dirname } from 'node:path';\n\nimport {\n\tisRawExpr,\n\ttype CodegenableDecl,\n\ttype CodegenEmitDone,\n\ttype CodegenEmitContext,\n\ttype OutputLocation,\n} from '../../contracts/codegenable.ts';\nimport { CONFIG_RUNTIME_OUTPUT_PATH, CONFIG_RUNTIME_SOURCE } from './config-runtime.ts';\nimport { acquireStackLock } from '../../substrate/runtime/cross-process/stack-lock.ts';\nimport { mintRandomSuffix } from '../../substrate/runtime/random-suffix.ts';\nimport { stageAndSwap, StageAndSwapError } from '../../substrate/runtime/stage-and-swap/index.ts';\n\nimport {\n\temitBindings,\n\ttype EmitBindingsResult,\n\tisPackageBindings,\n\tMoveCodegenService,\n\tMoveSummaryRunnerService,\n\ttype PackageBindings,\n} from './bindings.ts';\nimport { emitOne } from './emit.ts';\nimport {\n\tCodegenAggregateConflict,\n\tCodegenEmitFailed,\n\tCodegenEmitterCollision,\n\tCodegenPathConflict,\n\tCodegenWriteFailed,\n\ttype CodegenError,\n} from './errors.ts';\nimport { renderFile } from './format.ts';\nimport { writeGitignore } from './gitignore.ts';\nimport { UNRESOLVED_ID } from './id-config.ts';\nimport type { IdConfig, IdConfigNetwork, IdConfigPackage, IdConfigValues } from './id-config.ts';\nimport { CodegenPathsService, type CodegenPaths } from './paths.ts';\nimport { dirModeFor, modeFor, NON_SENSITIVE_DIR_MODE } from './permissions.ts';\n\n// -----------------------------------------------------------------------------\n// Public types\n// -----------------------------------------------------------------------------\n\n/** A type alias the orchestrator uses internally to avoid restating\n * the wide-erased `CodegenableDecl` generic in every signature. */\nexport type Codegenable = CodegenableDecl<string>;\n\nexport interface RunEmitCycleInput {\n\t/** All Codegenable contributions, as collected from the active\n\t * stack members' `capabilities` tuples. The orchestrator does\n\t * not look at plugins — only at the decl set. */\n\treadonly contributions: ReadonlyArray<Codegenable>;\n\t/** Optional: import-extension for bindings emission. Default `.ts`. */\n\treadonly bindingsImportExtension?: '.ts' | '.js' | '';\n\t/** When `true`, this is the COMMITTED projection tree (`src/generated`,\n\t * written by the stack-free `codegen` verb): the `.gitignore` TRACKS\n\t * the stubs (bindings, config, config-runtime) so `tsc`/`vite build`\n\t * work on a fresh clone, ignoring only `sensitivePaths`. When\n\t * `false`/omitted, the ephemeral tree is blanket-ignored. */\n\treadonly trackTree?: boolean;\n}\n\nexport interface RunEmitCycleResult {\n\treadonly filesWritten: ReadonlyArray<string>;\n\treadonly filesUnchanged: ReadonlyArray<string>;\n\treadonly filesChmod: ReadonlyArray<string>;\n\treadonly bindings: EmitBindingsResult | null;\n}\n\n/** Resolve a decl/aggregate's absolute output path against the tree\n * selected by its `outputLocation`. `'generated-extras'` routes\n * through `paths.resolveExtras` (the gitignored dev tree); everything\n * else through `paths.resolve` (the staging-and-swapped runtime tree). */\nconst resolveAt = (\n\tpaths: CodegenPaths,\n\tlocation: OutputLocation,\n\toutputPath: string,\n): Effect.Effect<string, CodegenPathConflict> =>\n\tlocation === 'generated-extras' ? paths.resolveExtras(outputPath) : paths.resolve(outputPath);\n\nconst declLocation = (decl: Pick<Codegenable, 'outputLocation'>): OutputLocation =>\n\tdecl.outputLocation ?? 'generated';\n\n/** A decl belongs to the `emitExtras` flush IFF it writes ONLY into the\n * dev-only `generated-extras` tree. INVARIANT: `emitExtras` must NEVER\n * touch the committed `src/generated` tree (it runs without stage-and-swap),\n * so a decl that would emit any file into `generated` is excluded.\n *\n * - `aggregateOnly` decls write only their aggregate file: include iff the\n * aggregate's location is `generated-extras` (e.g. each account folding\n * into the gitignored `accounts.ts`).\n * - standalone decls write a per-decl file (and possibly an aggregate):\n * include iff that standalone file lands in `generated-extras` (e.g. the\n * wallet's `dev-wallet.ts`). A standalone-in-`generated` decl is excluded\n * even if its aggregate targets `generated-extras`. */\nconst isExtrasDecl = (decl: Codegenable): boolean =>\n\tdecl.aggregateOnly === true\n\t\t? (decl.aggregate?.outputLocation ?? 'generated') === 'generated-extras'\n\t\t: declLocation(decl) === 'generated-extras';\n\nconst buildParentModeResolver = (\n\tpaths: CodegenPaths,\n\tentries: ReadonlyArray<{\n\t\treadonly outputPath: string;\n\t\treadonly location: OutputLocation;\n\t\treadonly sensitive: boolean;\n\t}>,\n): Effect.Effect<(absolutePath: string) => number, CodegenPathConflict> =>\n\tEffect.gen(function* () {\n\t\tconst byParent = new Map<string, Array<{ readonly sensitive?: boolean }>>();\n\t\tfor (const entry of entries) {\n\t\t\tconst parent = dirname(yield* resolveAt(paths, entry.location, entry.outputPath));\n\t\t\tconst current = byParent.get(parent);\n\t\t\tif (current === undefined) {\n\t\t\t\tbyParent.set(parent, [{ sensitive: entry.sensitive }]);\n\t\t\t} else {\n\t\t\t\tcurrent.push({ sensitive: entry.sensitive });\n\t\t\t}\n\t\t}\n\t\tconst modes = new Map<string, number>();\n\t\tfor (const [parent, parentDecls] of byParent) {\n\t\t\tmodes.set(parent, dirModeFor(parentDecls));\n\t\t}\n\t\treturn (absolutePath: string) => modes.get(dirname(absolutePath)) ?? NON_SENSITIVE_DIR_MODE;\n\t});\n\n// -----------------------------------------------------------------------------\n// Main entry — one cycle of the codegen pipeline\n// -----------------------------------------------------------------------------\n\n/**\n * Per-cycle lock acquire timeout. Codegen cycles can be file-system\n * heavy (multi-emitter, Move-bindings compilation), so we allow more\n * than the substrate's default 5s for `stack.lock` — a custom CLI\n * caller that hits a supervisor mid-cycle should wait, not error.\n * Mirrors `MOVE_BUILD_LOCK_TIMEOUT_MS` (5 minutes).\n */\nconst CODEGEN_CYCLE_LOCK_TIMEOUT_MS = 5 * 60_000;\n\nexport const runEmitCycle = (\n\tinput: RunEmitCycleInput,\n): Effect.Effect<\n\tRunEmitCycleResult,\n\tCodegenError,\n\tFileSystem.FileSystem | CodegenPathsService | MoveSummaryRunnerService | MoveCodegenService\n> =>\n\t// Per-process lock. The supervisor's serialized post-acquire path\n\t// is fine for the normal lifecycle, but custom callers (CLI direct\n\t// invocations, future watcher hooks) can call `runEmitCycle`\n\t// concurrently. With `stageAndSwap` writing under shared\n\t// `<outputDir>.staging.<cycleId>` and `<outputDir>.bak.<cycleId>`\n\t// siblings — and with the pre-seed `fs.copy` reading from the\n\t// shared `outputDir` — two overlapping cycles can stage from a\n\t// half-published tree of the other. The lock serializes them.\n\t//\n\t// Dedicated `codegenLockFile` (NOT the substrate `stack.lock`):\n\t// codegen cycles can run for many seconds when Move bindings\n\t// compile, and the substrate's `stack.lock` is reserved for short\n\t// critical sections (roster mutations, the snapshot bounce). A\n\t// dedicated lock isolates codegen contention from those subsystems.\n\tEffect.scoped(\n\t\tEffect.gen(function* () {\n\t\t\tconst paths = yield* CodegenPathsService;\n\t\t\tyield* acquireStackLock(paths.codegenLockFile, CODEGEN_CYCLE_LOCK_TIMEOUT_MS).pipe(\n\t\t\t\tEffect.mapError(\n\t\t\t\t\t(cause) =>\n\t\t\t\t\t\tnew CodegenWriteFailed({\n\t\t\t\t\t\t\toutputPath: paths.codegenLockFile,\n\t\t\t\t\t\t\tstage: 'write',\n\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t}),\n\t\t\t\t),\n\t\t\t);\n\t\t\treturn yield* runEmitCycleLocked(input);\n\t\t}),\n\t);\n\nconst runEmitCycleLocked = (\n\tinput: RunEmitCycleInput,\n): Effect.Effect<\n\tRunEmitCycleResult,\n\tCodegenError,\n\tFileSystem.FileSystem | CodegenPathsService | MoveSummaryRunnerService | MoveCodegenService\n> =>\n\tEffect.gen(function* () {\n\t\tconst paths = yield* CodegenPathsService;\n\t\t// Yield the Move-codegen services here (outside the\n\t\t// stage-and-swap build) so the build's R-channel collapses to\n\t\t// just `FileSystem.FileSystem` — the substrate `stageAndSwap`\n\t\t// primitive constrains `build`'s requirements to\n\t\t// `FileSystem.FileSystem`.\n\t\tconst moveRunner = yield* MoveSummaryRunnerService;\n\t\tconst moveCodegen = yield* MoveCodegenService;\n\n\t\t// Pre-flight contribution-set validation. Detected BEFORE the\n\t\t// stage-and-swap so a programming-bug rejection (duplicate\n\t\t// emitterName / outputPath collision) never opens an empty\n\t\t// staging dir on disk. `package` is the one exception:\n\t\t// multiple instances (one per Package) legitimately share the\n\t\t// `emitterName` literal by design.\n\t\tyield* validateUniqueness(input.contributions);\n\t\tyield* validateAggregatePathAvailability(input.contributions);\n\n\t\t// Cycle-level atomicity: substrate stage-and-swap. The build\n\t\t// populates `<outputDir>.staging.<cycleId>/`; on success the\n\t\t// substrate renames it into place; on any failure the previous\n\t\t// tree (if any) is restored byte-for-byte. Without this wrapper\n\t\t// a mid-cycle emit failure would leave `src/generated/`\n\t\t// half-rewritten — see STYLE_GUIDE §19.\n\t\t//\n\t\t// `preserveOnPreseed: true` — substrate clones the current\n\t\t// target into staging before `build` runs so the per-file\n\t\t// no-touch idempotency (and gitignore user-block preservation)\n\t\t// sees the right baseline. Files this cycle rewrites are\n\t\t// overwritten in staging; files this cycle does NOT touch\n\t\t// survive into the next target verbatim with their original\n\t\t// mtimes (HMR watchers stay quiet for unchanged outputs).\n\t\t//\n\t\t// Cycle id is a random suffix. STYLE_GUIDE §17 mandates 8\n\t\t// hex chars for external-facing identifiers; this is a\n\t\t// deliberate 16-char carve-out because the value only\n\t\t// appears in transient staging-directory names\n\t\t// (`.staging.<id>` / `.bak.<id>`) that the substrate rm's\n\t\t// after publish. The extra entropy is defense-in-depth for\n\t\t// the race-window where two concurrent emit cycles under a\n\t\t// custom-CLI caller could mint overlapping staging dirs\n\t\t// against the same shared `outputDir`; a collision there\n\t\t// would corrupt a half-built tree, not just clash an\n\t\t// operator-visible name.\n\t\tconst cycleId = mintRandomSuffix(16);\n\t\tconst stagingPaths = paths.withRoot(`${paths.outputDir}.staging.${cycleId}`);\n\n\t\treturn yield* stageAndSwap({\n\t\t\ttargetPath: paths.outputDir,\n\t\t\tidSuffix: cycleId,\n\t\t\tpreserveOnPreseed: true,\n\t\t\tbuild: Effect.gen(function* () {\n\t\t\t\tconst inner = yield* runEmitCycleInner(input, stagingPaths).pipe(\n\t\t\t\t\tEffect.provideService(MoveSummaryRunnerService, moveRunner),\n\t\t\t\t\tEffect.provideService(MoveCodegenService, moveCodegen),\n\t\t\t\t);\n\t\t\t\t// Rewrite paths so callers see the final user-visible\n\t\t\t\t// `outputDir` location, not the staging directory that\n\t\t\t\t// only exists for the duration of the build.\n\t\t\t\treturn rewriteResultPaths(inner, stagingPaths.outputDir, paths.outputDir);\n\t\t\t}),\n\t\t}).pipe(\n\t\t\tEffect.mapError((e): CodegenError => {\n\t\t\t\tif (e instanceof StageAndSwapError) {\n\t\t\t\t\treturn new CodegenWriteFailed({\n\t\t\t\t\t\toutputPath: paths.outputDir,\n\t\t\t\t\t\tstage: 'rename',\n\t\t\t\t\t\tcause: e,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\treturn e;\n\t\t\t}),\n\t\t);\n\t});\n\n/**\n * Project a staging-rooted `RunEmitCycleResult` back into the user-\n * visible `outputDir` namespace. The build effect writes through\n * `stagingDir` paths; callers (and tests) expect to see the final\n * post-rename locations.\n */\nconst rewriteResultPaths = (\n\tresult: RunEmitCycleResult,\n\tstagingDir: string,\n\toutputDir: string,\n): RunEmitCycleResult => {\n\tconst stripped = stagingDir.replace(/\\/+$/, '');\n\tconst target = outputDir.replace(/\\/+$/, '');\n\tconst rewrite = (p: string): string =>\n\t\tp.startsWith(stripped) ? `${target}${p.slice(stripped.length)}` : p;\n\treturn {\n\t\tfilesWritten: result.filesWritten.map(rewrite),\n\t\tfilesUnchanged: result.filesUnchanged.map(rewrite),\n\t\tfilesChmod: result.filesChmod.map(rewrite),\n\t\tbindings:\n\t\t\tresult.bindings === null\n\t\t\t\t? null\n\t\t\t\t: {\n\t\t\t\t\t\t...result.bindings,\n\t\t\t\t\t\tfilesWritten: result.bindings.filesWritten.map(rewrite),\n\t\t\t\t\t},\n\t};\n};\n\n/**\n * The body of one emit cycle. Pulled out of `runEmitCycle` so the\n * stage-and-swap wrapper can drive it against a redirected\n * `CodegenPathsService` (the staging tree). Validation runs BEFORE\n * this function so callers know the contribution set is well-formed.\n */\nconst runEmitCycleInner = (\n\tinput: RunEmitCycleInput,\n\tpaths: CodegenPaths,\n): Effect.Effect<\n\tRunEmitCycleResult,\n\tCodegenError,\n\tFileSystem.FileSystem | MoveSummaryRunnerService | MoveCodegenService\n> =>\n\tEffect.gen(function* () {\n\t\tyield* Effect.logInfo(`codegen: emitting projection (trackTree=${input.trackTree === true}).`);\n\t\tconst fileEmitters: Array<Codegenable> = [...input.contributions];\n\n\t\tconst filesWritten: Array<string> = [];\n\t\tconst filesUnchanged: Array<string> = [];\n\t\tconst filesChmod: Array<string> = [];\n\t\t// Aggregate buckets keyed by plugin-supplied bucket name. The\n\t\t// orchestrator treats bucket names as opaque tags chosen by\n\t\t// the contributor; it never branches on plugin identity. See\n\t\t// `CodegenableDecl.aggregate` (contracts/codegenable.ts).\n\t\tconst aggregates = new Map<string, Record<string, unknown>>();\n\t\t// Per-bucket location + sensitivity, read from the first decl\n\t\t// the orchestrator sees contributing to the bucket. Drives where\n\t\t// the synthesized aggregate file lands and its file mode. Every\n\t\t// later contributor to the same bucket MUST agree (enforced\n\t\t// below) — a silent disagreement could misroute a sensitive\n\t\t// aggregate into the committed `generated` tree.\n\t\tconst aggregateMeta = new Map<string, AggregateMeta>();\n\t\tconst packageContribs: Array<PackageBindings> = [];\n\t\tconst sortedDecls = [...fileEmitters].sort(\n\t\t\tOrder.mapInput(Order.String, (d: Codegenable) => d.outputPath),\n\t\t);\n\t\t// Parent-mode resolver must see every path that will be written —\n\t\t// standalone decls AND synthesized aggregates — across BOTH trees.\n\t\t// Aggregate-only decls do not write a standalone file, so they are\n\t\t// excluded from the standalone-path set here.\n\t\tconst parentModeFor = yield* buildParentModeResolver(\n\t\t\tpaths,\n\t\t\tfileEmitters\n\t\t\t\t.filter((d) => d.aggregateOnly !== true)\n\t\t\t\t.map((d) => ({\n\t\t\t\t\toutputPath: d.outputPath,\n\t\t\t\t\tlocation: declLocation(d),\n\t\t\t\t\tsensitive: d.sensitive === true,\n\t\t\t\t})),\n\t\t);\n\t\tfor (const decl of sortedDecls) {\n\t\t\tconst emission = yield* runEmitter(decl);\n\t\t\tconst exported = emission.exports;\n\t\t\tif (decl.aggregate !== undefined) {\n\t\t\t\tconst projected = decl.aggregate.project(exported);\n\t\t\t\tif (projected !== null) {\n\t\t\t\t\tconst bucket = aggregates.get(decl.aggregate.bucket) ?? {};\n\t\t\t\t\tdeepMerge(bucket, projected);\n\t\t\t\t\taggregates.set(decl.aggregate.bucket, bucket);\n\t\t\t\t\t// First-contributor-wins for routing/sensitivity, but a\n\t\t\t\t\t// LATER contributor that disagrees is a hard error — the\n\t\t\t\t\t// `AggregateContribution` contract requires all\n\t\t\t\t\t// contributors to a bucket to agree, and a silent\n\t\t\t\t\t// mismatch could misroute a sensitive aggregate into the\n\t\t\t\t\t// committed `generated` tree (secret leak).\n\t\t\t\t\tconst declLoc = decl.aggregate.outputLocation ?? 'generated';\n\t\t\t\t\tconst declSensitive = decl.aggregate.sensitive === true;\n\t\t\t\t\tconst established = aggregateMeta.get(decl.aggregate.bucket);\n\t\t\t\t\tif (established === undefined) {\n\t\t\t\t\t\taggregateMeta.set(decl.aggregate.bucket, {\n\t\t\t\t\t\t\tlocation: declLoc,\n\t\t\t\t\t\t\tsensitive: declSensitive,\n\t\t\t\t\t\t\testablishedBy: decl.emitterName,\n\t\t\t\t\t\t});\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif (established.location !== declLoc) {\n\t\t\t\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\t\t\t\tnew CodegenAggregateConflict({\n\t\t\t\t\t\t\t\t\tbucket: decl.aggregate.bucket,\n\t\t\t\t\t\t\t\t\tfield: 'outputLocation',\n\t\t\t\t\t\t\t\t\testablished: established.location,\n\t\t\t\t\t\t\t\t\tconflicting: declLoc,\n\t\t\t\t\t\t\t\t\temitters: [established.establishedBy, decl.emitterName],\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (established.sensitive !== declSensitive) {\n\t\t\t\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\t\t\t\tnew CodegenAggregateConflict({\n\t\t\t\t\t\t\t\t\tbucket: decl.aggregate.bucket,\n\t\t\t\t\t\t\t\t\tfield: 'sensitive',\n\t\t\t\t\t\t\t\t\testablished: String(established.sensitive),\n\t\t\t\t\t\t\t\t\tconflicting: String(declSensitive),\n\t\t\t\t\t\t\t\t\temitters: [established.establishedBy, decl.emitterName],\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Move-bindings collection: any export whose shape matches\n\t\t\t// the orchestrator's `PackageBindings` consumer contract is\n\t\t\t// forwarded to `emitBindings`. Runs against the raw `exported`\n\t\t\t// map (not via `aggregate.project`) so direct `codegenable(...)`\n\t\t\t// contributions — which carry no `aggregate` — are picked up\n\t\t\t// too. The orchestrator validates the shape it consumes; it\n\t\t\t// does NOT name the plugin that produced it.\n\t\t\tfor (const value of Object.values(exported)) {\n\t\t\t\tif (isPackageBindings(value)) {\n\t\t\t\t\tpackageContribs.push(value);\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Aggregate-only decls contribute solely to their bucket; the\n\t\t\t// standalone per-decl file is skipped (the combined aggregate\n\t\t\t// is the only app-facing surface).\n\t\t\tif (decl.aggregateOnly === true) continue;\n\t\t\tconst rendered = renderFile({\n\t\t\t\temitterName: decl.emitterName,\n\t\t\t\toutputPath: decl.outputPath,\n\t\t\t\tsensitive: decl.sensitive === true,\n\t\t\t\texports: exported,\n\t\t\t\timports: emission.imports,\n\t\t\t});\n\t\t\tif (!rendered.ok) {\n\t\t\t\treturn yield* Effect.fail(rendered.error);\n\t\t\t}\n\t\t\tconst abs = yield* resolveAt(paths, declLocation(decl), decl.outputPath);\n\t\t\tconst outcome = yield* emitOne({\n\t\t\t\tpath: abs,\n\t\t\t\tcontent: rendered.text,\n\t\t\t\tmode: modeFor(decl),\n\t\t\t\tparentMode: parentModeFor(abs),\n\t\t\t});\n\t\t\tswitch (outcome.outcome) {\n\t\t\t\tcase 'wrote':\n\t\t\t\t\tfilesWritten.push(abs);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'unchanged':\n\t\t\t\t\tfilesUnchanged.push(abs);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'chmod-only':\n\t\t\t\t\tfilesChmod.push(abs);\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tconst aggregateFiles = buildAggregateFiles(aggregates, aggregateMeta);\n\t\t// True once any aggregate references a `config-runtime.ts` resolver\n\t\t// (the committed `config.ts`): then we ALSO emit the fixed\n\t\t// `config-runtime.ts` resolver and import the referenced resolvers into\n\t\t// that file.\n\t\tlet needsConfigRuntime = false;\n\t\tfor (const aggregate of aggregateFiles) {\n\t\t\tconst resolvers = resolversUsedBy(aggregate.exports);\n\t\t\tif (resolvers.length > 0) needsConfigRuntime = true;\n\t\t\tconst rendered = renderFile({\n\t\t\t\temitterName: aggregate.emitterName,\n\t\t\t\toutputPath: aggregate.outputPath,\n\t\t\t\tsensitive: aggregate.sensitive,\n\t\t\t\texports: aggregate.exports,\n\t\t\t\t// The committed `config.ts` resolves ids/network at runtime —\n\t\t\t\t// import exactly the resolvers it references (no unused imports;\n\t\t\t\t// oxlint is pinned and flags them). `.js` specifier (ESM/TS-\n\t\t\t\t// resolved) mirrors the bindings' import style.\n\t\t\t\t...(resolvers.length > 0\n\t\t\t\t\t? { imports: [`import { ${resolvers.join(', ')} } from './config-runtime.js';`] }\n\t\t\t\t\t: {}),\n\t\t\t});\n\t\t\tif (!rendered.ok) {\n\t\t\t\treturn yield* Effect.fail(rendered.error);\n\t\t\t}\n\t\t\tconst abs = yield* resolveAt(paths, aggregate.location, aggregate.outputPath);\n\t\t\tconst outcome = yield* emitOne({\n\t\t\t\tpath: abs,\n\t\t\t\tcontent: rendered.text,\n\t\t\t\tmode: aggregate.sensitive ? 0o600 : 0o644,\n\t\t\t\tparentMode: parentModeFor(abs),\n\t\t\t});\n\t\t\tswitch (outcome.outcome) {\n\t\t\t\tcase 'wrote':\n\t\t\t\t\tfilesWritten.push(abs);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'unchanged':\n\t\t\t\t\tfilesUnchanged.push(abs);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'chmod-only':\n\t\t\t\t\tfilesChmod.push(abs);\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\t// Emit the FIXED `config-runtime.ts` resolver (a constant string, NOT\n\t\t// routed through the literal renderer) when `config.ts` resolves ids\n\t\t// at runtime. It reads the injected `__DEVSTACK_IDS__` global and\n\t\t// THROWS `DevstackConfigMissingError` on an unresolved id.\n\t\tif (needsConfigRuntime) {\n\t\t\tconst abs = yield* paths.resolve(CONFIG_RUNTIME_OUTPUT_PATH);\n\t\t\tconst outcome = yield* emitOne({\n\t\t\t\tpath: abs,\n\t\t\t\tcontent: CONFIG_RUNTIME_SOURCE,\n\t\t\t\tmode: 0o644,\n\t\t\t\tparentMode: parentModeFor(abs),\n\t\t\t});\n\t\t\tswitch (outcome.outcome) {\n\t\t\t\tcase 'wrote':\n\t\t\t\t\tfilesWritten.push(abs);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'unchanged':\n\t\t\t\t\tfilesUnchanged.push(abs);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'chmod-only':\n\t\t\t\t\tfilesChmod.push(abs);\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tlet bindings: EmitBindingsResult | null = null;\n\t\tif (packageContribs.length > 0) {\n\t\t\tbindings = yield* emitBindings({\n\t\t\t\tbindingsDir: paths.bindingsDir,\n\t\t\t\tpackages: packageContribs,\n\t\t\t\timportExtension: input.bindingsImportExtension,\n\t\t\t});\n\t\t} else {\n\t\t\tyield* Effect.logInfo(\n\t\t\t\t'codegen: no package contributions; skipping Move-to-TS bindings step.',\n\t\t\t);\n\t\t}\n\n\t\t// `.gitignore` covers the runtime `generated/` tree only. Decls\n\t\t// routed to `generated-extras` live outside `outputDir` (that\n\t\t// whole tree is gitignored at the `.devstack/` level), and\n\t\t// aggregate-only decls never write a standalone file — both are\n\t\t// excluded here so the managed `.gitignore` only lists real\n\t\t// sensitive files in `outputDir`.\n\t\t//\n\t\t// Synthesized aggregate files are a SEPARATE source of sensitive\n\t\t// runtime paths: a sensitive bucket routed to `generated` writes a\n\t\t// real secret-bearing file in `outputDir` that the standalone-decl\n\t\t// scan above never sees (its contributors may all be\n\t\t// `aggregateOnly`). Without an explicit ignore line such a file\n\t\t// would rely solely on the blanket `*` rule — and a user `!<file>`\n\t\t// override in the preserved user block would then start tracking\n\t\t// the secret. Include them so each gets an explicit re-ignore line.\n\t\tconst sensitivePaths = [\n\t\t\t...fileEmitters\n\t\t\t\t.filter(\n\t\t\t\t\t(d) =>\n\t\t\t\t\t\td.sensitive === true && d.aggregateOnly !== true && declLocation(d) === 'generated',\n\t\t\t\t)\n\t\t\t\t.map((d) => d.outputPath),\n\t\t\t...aggregateFiles\n\t\t\t\t.filter((a) => a.sensitive && a.location === 'generated')\n\t\t\t\t.map((a) => a.outputPath),\n\t\t];\n\t\t// A committed projection (written by the stack-free `codegen` verb\n\t\t// into `src/generated`) is TRACKED: the stubs are committed so\n\t\t// `tsc`/`vite build` work on a fresh clone. Any ephemeral tree keeps\n\t\t// the blanket ignore.\n\t\tyield* writeGitignore({\n\t\t\tpath: paths.gitignoreFile,\n\t\t\tsensitivePaths,\n\t\t\tparentMode: parentModeFor(paths.gitignoreFile),\n\t\t\ttrackTree: input.trackTree === true,\n\t\t});\n\n\t\treturn {\n\t\t\tfilesWritten,\n\t\t\tfilesUnchanged,\n\t\t\tfilesChmod,\n\t\t\tbindings,\n\t\t};\n\t});\n\n// -----------------------------------------------------------------------------\n// Internals\n// -----------------------------------------------------------------------------\n\ninterface CodegenEmission {\n\treadonly exports: { readonly [key: string]: unknown };\n\treadonly imports: ReadonlyArray<string>;\n}\n\nconst runEmitter = (decl: Codegenable): Effect.Effect<CodegenEmission, CodegenEmitFailed> =>\n\tEffect.gen(function* () {\n\t\t// Annotate the emit span with the contributor's plugin-supplied\n\t\t// `kind` tag (declared on the decl's `aggregate`). The orchestrator\n\t\t// stays name-blind — it never reads the tag's VALUE or branches on it\n\t\t// (per the `AggregateContribution` contract); wiring it here makes the\n\t\t// otherwise-dead field observable so a trace attributes each emit to\n\t\t// the plugin family that produced it. `annotateCurrentSpan` only lands\n\t\t// inside an enclosing `withSpan` — the `codegen.emit` span this body is\n\t\t// wrapped in below.\n\t\tconst kind = decl.aggregate?.kind;\n\t\tif (kind !== undefined) {\n\t\t\tyield* Effect.annotateCurrentSpan('codegen.kind', kind);\n\t\t}\n\t\tconst exports: Record<string, unknown> = {};\n\t\tconst imports: Array<string> = [];\n\t\tconst done: CodegenEmitDone = { _tag: 'CodegenEmitDone' };\n\t\tconst ctx: CodegenEmitContext = {\n\t\t\texportConst: (name, value) => {\n\t\t\t\texports[name] = value;\n\t\t\t},\n\t\t\timportStatement: (statement) => {\n\t\t\t\timports.push(statement);\n\t\t\t},\n\t\t\tdone: () => done,\n\t\t};\n\t\tyield* decl.emit(ctx).pipe(\n\t\t\tEffect.mapError(\n\t\t\t\t(cause) =>\n\t\t\t\t\tnew CodegenEmitFailed({\n\t\t\t\t\t\temitterName: decl.emitterName,\n\t\t\t\t\t\toutputPath: decl.outputPath,\n\t\t\t\t\t\tcause,\n\t\t\t\t\t}),\n\t\t\t),\n\t\t);\n\t\treturn { exports, imports };\n\t}).pipe(Effect.withSpan('codegen.emit', { attributes: { 'codegen.emitter': decl.emitterName } }));\n\n/**\n * Uniqueness check: emitter name (literal) must be unique unless\n * the decl opts into repetition via `allowEmitterNameRepetition`\n * (used by per-item plugins like Package, which emit one decl per\n * published package under a shared emitter name). Output paths\n * must be unique across ALL emitters.\n */\nconst validateUniqueness = (\n\tdecls: ReadonlyArray<Codegenable>,\n): Effect.Effect<void, CodegenPathConflict | CodegenEmitterCollision> =>\n\tEffect.gen(function* () {\n\t\tconst byPath = new Map<string, Array<string>>();\n\t\tconst byName = new Map<string, Array<string>>();\n\t\tfor (const d of decls) {\n\t\t\t// Aggregate-only decls write no standalone file, so their\n\t\t\t// `outputPath` is a dead value — exclude them from the\n\t\t\t// path-uniqueness check (many `package` decls legitimately\n\t\t\t// share `config.ts`'s bucket but carry distinct dead\n\t\t\t// `package/<name>.ts` outputPaths that never hit disk).\n\t\t\tif (d.aggregateOnly !== true) {\n\t\t\t\t// Key by (location, path): the same relative path in the\n\t\t\t\t// `generated` vs `generated-extras` trees is two distinct\n\t\t\t\t// files, so `accounts.ts` may exist in both without a\n\t\t\t\t// false collision.\n\t\t\t\tconst pathKey = `${declLocation(d)} ${d.outputPath}`;\n\t\t\t\tconst ps = byPath.get(pathKey) ?? [];\n\t\t\t\tps.push(d.emitterName);\n\t\t\t\tbyPath.set(pathKey, ps);\n\t\t\t}\n\t\t\tif (d.allowEmitterNameRepetition === true) continue;\n\t\t\tconst ns = byName.get(d.emitterName) ?? [];\n\t\t\tns.push(d.outputPath);\n\t\t\tbyName.set(d.emitterName, ns);\n\t\t}\n\t\tfor (const [pathKey, emitters] of byPath) {\n\t\t\tif (emitters.length > 1) {\n\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\tnew CodegenPathConflict({\n\t\t\t\t\t\tkind: 'duplicate',\n\t\t\t\t\t\t// Strip the `<location> ` prefix from the dedup key so\n\t\t\t\t\t\t// the error names the relative path the plugin declared.\n\t\t\t\t\t\toutputPath: pathKey.slice(pathKey.indexOf(' ') + 1),\n\t\t\t\t\t\temitters,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t\tfor (const [name, outputPaths] of byName) {\n\t\t\tif (outputPaths.length > 1) {\n\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\tnew CodegenEmitterCollision({\n\t\t\t\t\t\temitterName: name,\n\t\t\t\t\t\toutputPaths,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t});\n\n/**\n * Reject contribution sets that would have an aggregate bucket\n * collide with a per-decl `outputPath`. The orchestrator only knows\n * bucket names because plugins declared them via `aggregate.bucket`;\n * it does not enumerate or recognize plugin identities here.\n */\nconst validateAggregatePathAvailability = (\n\tdecls: ReadonlyArray<Codegenable>,\n): Effect.Effect<void, CodegenPathConflict> =>\n\tEffect.gen(function* () {\n\t\t// Aggregate buckets keyed by (location, bucket). A standalone\n\t\t// decl in the SAME tree that writes the bucket path would clash\n\t\t// with the synthesized aggregate; a decl in the OTHER tree (or\n\t\t// an aggregate-only decl, which writes no standalone file) does\n\t\t// not.\n\t\tconst aggregatePaths = new Map<string, string>();\n\t\tfor (const decl of decls) {\n\t\t\tif (decl.aggregate !== undefined) {\n\t\t\t\tconst location = decl.aggregate.outputLocation ?? 'generated';\n\t\t\t\taggregatePaths.set(`${location} ${decl.aggregate.bucket}`, decl.aggregate.bucket);\n\t\t\t}\n\t\t}\n\t\tfor (const [key, bucket] of aggregatePaths) {\n\t\t\tconst location = key.slice(0, key.indexOf(' '));\n\t\t\tconst colliding = decls.filter(\n\t\t\t\t(decl) =>\n\t\t\t\t\tdecl.aggregateOnly !== true &&\n\t\t\t\t\tdeclLocation(decl) === location &&\n\t\t\t\t\tdecl.outputPath === bucket,\n\t\t\t);\n\t\t\tif (colliding.length > 0) {\n\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\tnew CodegenPathConflict({\n\t\t\t\t\t\tkind: 'duplicate',\n\t\t\t\t\t\toutputPath: bucket,\n\t\t\t\t\t\temitters: [...colliding.map((decl) => decl.emitterName), `aggregate/${bucket}`],\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t});\n\n/** Per-bucket location + sensitivity, captured from the first decl\n * the orchestrator sees contributing to a bucket. The orchestrator\n * stays name-blind; the plugin owns these on its\n * `AggregateContribution`. `establishedBy` records the emitter name\n * of that first contributor so a later disagreement can quote both\n * sides in `CodegenAggregateConflict`. */\ninterface AggregateMeta {\n\treadonly location: OutputLocation;\n\treadonly sensitive: boolean;\n\treadonly establishedBy: string;\n}\n\ninterface AggregateFile {\n\treadonly emitterName: string;\n\treadonly outputPath: string;\n\treadonly exports: { readonly [key: string]: unknown };\n\treadonly location: OutputLocation;\n\treadonly sensitive: boolean;\n}\n\n/**\n * Recursively merge `source` into `target`. Distinct buckets are\n * shallow records keyed by name, but a single bucket (e.g. `config.ts`)\n * accumulates contributions from MANY plugins into nested sub-records\n * (`networks.localnet` from sui, `packages.<name>` / `objects.<name>`\n * from each package). A shallow `Object.assign` would have the last\n * package's `{packages:{...}}` clobber the prior ones. Deep-merge so\n * sibling keys at every level coexist.\n *\n * Arrays and non-plain values overwrite (no element-wise merge — a\n * plugin that re-emits a bucket key owns its full value). Only plain\n * objects recurse.\n */\nconst isPlainObject = (v: unknown): v is Record<string, unknown> =>\n\ttypeof v === 'object' && v !== null && !Array.isArray(v);\n\nconst deepMerge = (\n\ttarget: Record<string, unknown>,\n\tsource: Readonly<Record<string, unknown>>,\n): void => {\n\tfor (const [key, value] of Object.entries(source)) {\n\t\tconst existing = target[key];\n\t\tif (isPlainObject(existing) && isPlainObject(value)) {\n\t\t\tdeepMerge(existing, value);\n\t\t} else {\n\t\t\t// `value` may be a decl-owned cached projection object (or array).\n\t\t\t// Storing it by reference would let a sibling decl that later\n\t\t\t// recurses into the same nested key mutate this decl's cache in\n\t\t\t// place, leaking state across emit cycles. Deep-clone any plain\n\t\t\t// object / array so the shared bucket never aliases decl state.\n\t\t\ttarget[key] = isPlainObject(value) || Array.isArray(value) ? structuredClone(value) : value;\n\t\t}\n\t}\n};\n\n/**\n * Synthesize one `AggregateFile` per non-empty bucket. The exports\n * map is keyed by the bucket's stem (e.g. `accounts.ts` → `accounts`)\n * so the rendered file exports `export const <stem> = { ... }`. The\n * orchestrator picks the export key from the bucket filename; the\n * stem itself is not a plugin identifier — it is the filename\n * without the `.ts` extension, derived mechanically. The bucket's\n * `location`/`sensitive` (from the first contributing decl) drive\n * which tree the file lands in and its mode.\n */\nconst buildAggregateFiles = (\n\tbuckets: ReadonlyMap<string, Record<string, unknown>>,\n\tmeta: ReadonlyMap<string, AggregateMeta>,\n): ReadonlyArray<AggregateFile> => {\n\tconst files: Array<AggregateFile> = [];\n\tconst sortedEntries = [...buckets.entries()].sort(([a], [b]) => a.localeCompare(b));\n\tfor (const [bucket, contents] of sortedEntries) {\n\t\tif (Object.keys(contents).length === 0) continue;\n\t\tconst stem = bucketStem(bucket);\n\t\tconst bucketMeta = meta.get(bucket) ?? {\n\t\t\tlocation: 'generated' as const,\n\t\t\tsensitive: false,\n\t\t\testablishedBy: `aggregate/${stem}`,\n\t\t};\n\t\tfiles.push({\n\t\t\temitterName: `aggregate/${stem}`,\n\t\t\toutputPath: bucket,\n\t\t\texports: { [stem]: contents },\n\t\t\tlocation: bucketMeta.location,\n\t\t\tsensitive: bucketMeta.sensitive,\n\t\t});\n\t}\n\treturn files;\n};\n\nconst bucketStem = (bucket: string): string => bucket.replace(/\\.ts$/, '').replace(/^.*\\//, '');\n\n/** The config-runtime resolver names a committed `config.ts` aggregate may\n * reference as raw expressions. Each is imported from `./config-runtime.js`\n * only when the aggregate actually calls it (oxlint flags unused imports). */\nconst CONFIG_RUNTIME_RESOLVERS = [\n\t'resolveId',\n\t'resolveNetwork',\n\t'resolveNetworks',\n\t'resolveValue',\n] as const;\ntype ConfigRuntimeResolver = (typeof CONFIG_RUNTIME_RESOLVERS)[number];\n\n/** Recursively collect which `config-runtime.ts` resolvers an exports map\n * references via raw expressions — i.e. the committed `config.ts` needs each\n * imported + the fixed `config-runtime.ts` emitted alongside it. */\nconst collectResolversInValue = (value: unknown, found: Set<ConfigRuntimeResolver>): void => {\n\tif (isRawExpr(value)) {\n\t\tfor (const name of CONFIG_RUNTIME_RESOLVERS) {\n\t\t\tif (value.expr.includes(`${name}(`)) found.add(name);\n\t\t}\n\t\treturn;\n\t}\n\tif (Array.isArray(value)) {\n\t\tfor (const v of value) collectResolversInValue(v, found);\n\t\treturn;\n\t}\n\tif (isPlainObject(value)) {\n\t\tfor (const v of Object.values(value)) collectResolversInValue(v, found);\n\t}\n};\n\n/** The ordered set of config-runtime resolvers an aggregate's exports use\n * (empty when none — the aggregate needs no resolver import). */\nconst resolversUsedBy = (exports: {\n\treadonly [key: string]: unknown;\n}): ReadonlyArray<ConfigRuntimeResolver> => {\n\tconst found = new Set<ConfigRuntimeResolver>();\n\tfor (const v of Object.values(exports)) collectResolversInValue(v, found);\n\treturn CONFIG_RUNTIME_RESOLVERS.filter((name) => found.has(name));\n};\n\n// -----------------------------------------------------------------------------\n// Service surface — registration API + emit-cycle trigger\n// -----------------------------------------------------------------------------\n\n/**\n * The codegen orchestrator's Context-bound service. The substrate's\n * supervisor calls `registerContribution(pluginKey, decl)` once per\n * `CodegenableDecl` on each plugin's `capabilities` tuple, scope-bound\n * to that plugin's acquire scope.\n *\n * Boot no longer runs codegen — the emit pipeline is the stack-free\n * `devstack codegen` verb (which calls `runEmitCycle` directly off the\n * config-derived `staticCodegen` decls). This service keeps the dispatcher\n * seam closed: every `codegenable` contribution still has a handler, so\n * plugins emit decls uniformly even though boot writes the id-config (not\n * the committed tree).\n */\nexport interface CodegenOrchestrator {\n\t/** Register a `CodegenableDecl` from a plugin. Scope-bound — when\n\t * the caller's scope (the plugin's acquire scope) closes, the\n\t * registration is reaped. */\n\treadonly registerContribution: (\n\t\tpluginKey: string,\n\t\tdecl: Codegenable,\n\t) => Effect.Effect<void, never, Scope.Scope>;\n\n\t/** Assemble the id-config from the currently-registered (live-resolved)\n\t * contributions. Boot calls this in its post-acquire hook to WRITE the\n\t * id-config file (the same `networks` / `packages` / `mvrOverrides`\n\t * data that fed `config.ts`, but as loadable JSON the Vite plugin\n\t * injects). `network` is the active network name (`ctx.identity.network`).\n\t * Pure projection over the registered decls — no I/O, no chain. */\n\treadonly assembleIdConfig: (network: string) => Effect.Effect<IdConfig, CodegenEmitFailed>;\n\n\t/** Flush ONLY the `generated-extras` contributions (the dev wallet's\n\t * `dev-wallet.ts` + the account plugin's `accounts.ts`) to the\n\t * gitignored `.devstack/stacks/<stack>/generated-extras` tree. These\n\t * are acquire-resolved (can't be statically derived by the `codegen`\n\t * verb), so boot writes them — but ONLY when the resolved network's\n\t * `devWallet` flag is on. The committed `src/generated` tree is NEVER\n\t * touched (no `generated`-located decl is emitted). Reuses the emit\n\t * renderer + aggregate logic; skips the stage-and-swap of the runtime\n\t * tree (extras live outside it). No-op (empty result) when nothing is\n\t * routed to `generated-extras`. */\n\treadonly emitExtras: () => Effect.Effect<\n\t\tRunEmitCycleResult,\n\t\tCodegenError,\n\t\tFileSystem.FileSystem | CodegenPathsService | MoveSummaryRunnerService | MoveCodegenService\n\t>;\n}\n\nexport class CodegenOrchestratorService extends Context.Service<\n\tCodegenOrchestratorService,\n\tCodegenOrchestrator\n>()('@devstack/orchestrators/Codegen') {}\n\n/**\n * Slice the deep-merged `config.ts` aggregate bucket into the loadable\n * `IdConfig` interchange shape. The bucket is the live codegen\n * accumulation (sui `networks`, per-package `packages`/`objects`/\n * `mvrOverrides`, account `accounts`); this picks the id-bearing fields\n * the Vite plugin injects. Reads are defensive — any missing slice\n * collapses to an empty record so a partial stack still writes a valid\n * (if sparse) id-config.\n */\nconst idConfigFromBucket = (\n\tbucket: Record<string, unknown>,\n\tnetwork: string,\n\tvalues: IdConfigValues,\n): IdConfig => {\n\tconst asRecord = (v: unknown): Record<string, unknown> => (isPlainObject(v) ? v : {});\n\tconst asString = (v: unknown): string | undefined => (typeof v === 'string' ? v : undefined);\n\n\tconst networks: Record<string, IdConfigNetwork> = {};\n\tfor (const [name, raw] of Object.entries(asRecord(bucket['networks']))) {\n\t\tconst entry = asRecord(raw);\n\t\tconst rpc = asString(entry['rpc']);\n\t\tif (rpc === undefined) continue;\n\t\tnetworks[name] = {\n\t\t\trpc,\n\t\t\t...(asString(entry['chainId']) !== undefined ? { chainId: asString(entry['chainId']) } : {}),\n\t\t\t...(entry['faucet'] !== undefined ? { faucet: asString(entry['faucet']) ?? null } : {}),\n\t\t\t...(entry['graphql'] !== undefined ? { graphql: asString(entry['graphql']) ?? null } : {}),\n\t\t};\n\t}\n\n\tconst packages: Record<string, IdConfigPackage> = {};\n\tfor (const [name, raw] of Object.entries(asRecord(bucket['packages']))) {\n\t\tconst entry = asRecord(raw);\n\t\t// The active-network id is `packageId` (convenience field the package\n\t\t// projection sets = `byNetwork[activeNetwork]`). An empty/missing\n\t\t// packageId maps to the UNRESOLVED_ID sentinel — an empty string would\n\t\t// slip past `isUnresolvedId` and ship as a real, resolved id.\n\t\tconst rawId = asString(entry['packageId']);\n\t\tconst id = rawId === undefined || rawId === '' ? UNRESOLVED_ID : rawId;\n\t\tconst objectsRaw = asRecord(entry['objects']);\n\t\tconst objects: Record<string, string> = {};\n\t\tfor (const [k, v] of Object.entries(objectsRaw)) {\n\t\t\tconst s = asString(v);\n\t\t\tif (s !== undefined) objects[k] = s;\n\t\t}\n\t\tpackages[name] = {\n\t\t\tid,\n\t\t\t...(Object.keys(objects).length > 0 ? { objects } : {}),\n\t\t};\n\t}\n\n\tconst accounts: Record<string, string> = {};\n\tfor (const [name, v] of Object.entries(asRecord(bucket['accounts']))) {\n\t\t// Account bindings are an object keyed by name; the injectable id is\n\t\t// the `address`. Tolerate a bare string too (a pinned known-config).\n\t\tconst address = asString(v) ?? asString(asRecord(v)['address']);\n\t\tif (address !== undefined) accounts[name] = address;\n\t}\n\n\tconst mvrOverrides: Record<string, string> = {};\n\tfor (const [mvr, v] of Object.entries(asRecord(bucket['mvrOverrides']))) {\n\t\tconst s = asString(v);\n\t\tif (s !== undefined) mvrOverrides[mvr] = s;\n\t}\n\n\t// The active `network` field MUST be a key present in `networks` — the\n\t// committed `config-runtime.ts` `resolveActiveNetwork()` does\n\t// `resolveNetworks()[network]`, and the Vite dev-wallet injection reads\n\t// `networks[network].rpc`. The `network` PARAM is the identity's network\n\t// name (e.g. `\"testnet-fork\"`), but the sui binding keys the `networks`\n\t// map by what it emitted (`\"localnet\"` for every mode). So PREFER the\n\t// network the binding stamped into the bucket (`bucket['network']`, which\n\t// matches the `networks` key); fall back to the param, then — if neither\n\t// is a known key but exactly one network exists — that sole key. This\n\t// keeps `network` in agreement with `networks` so resolution never\n\t// dereferences `undefined`.\n\tconst networkKeys = Object.keys(networks);\n\tconst bucketNetwork = asString(bucket['network']);\n\tconst activeNetwork =\n\t\tbucketNetwork !== undefined && networkKeys.includes(bucketNetwork)\n\t\t\t? bucketNetwork\n\t\t\t: networkKeys.includes(network)\n\t\t\t\t? network\n\t\t\t\t: networkKeys.length === 1\n\t\t\t\t\t? networkKeys[0]!\n\t\t\t\t\t: (bucketNetwork ?? network);\n\n\treturn {\n\t\tnetwork: activeNetwork,\n\t\tnetworks,\n\t\tpackages,\n\t\taccounts,\n\t\tmvrOverrides,\n\t\t...(Object.keys(values).length > 0 ? { values } : {}),\n\t};\n};\n\ninterface RegisteredCodegenEntry {\n\treadonly pluginKey: string;\n\treadonly decl: Codegenable;\n\treadonly seq: number;\n}\n\n/** Layer-wired codegen orchestrator. No upstream requirements at boot\n * time — the per-cycle effect carries the `FileSystem` + Move-codegen\n * service requirements through to the caller. */\nexport const layerCodegenOrchestrator: Layer.Layer<CodegenOrchestratorService> = Layer.effect(\n\tCodegenOrchestratorService,\n\tEffect.gen(function* () {\n\t\tconst contributionsRef = yield* Ref.make<ReadonlyArray<RegisteredCodegenEntry>>([]);\n\t\tconst seqRef = yield* Ref.make(0);\n\n\t\tconst registerContribution: CodegenOrchestrator['registerContribution'] = (pluginKey, decl) =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst seq = yield* Ref.updateAndGet(seqRef, (n) => n + 1);\n\t\t\t\tconst entry: RegisteredCodegenEntry = { pluginKey, decl, seq };\n\t\t\t\tyield* Ref.update(contributionsRef, (xs) => [...xs, entry]);\n\t\t\t\tyield* Effect.addFinalizer(() =>\n\t\t\t\t\tRef.update(contributionsRef, (xs) => xs.filter((e) => e.seq !== seq)),\n\t\t\t\t);\n\t\t\t}) as Effect.Effect<void, never, Scope.Scope>;\n\n\t\tconst assembleIdConfig: CodegenOrchestrator['assembleIdConfig'] = (network) =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst registered = (yield* Ref.get(contributionsRef)).map((e) => e.decl);\n\t\t\t\t// Deep-merge every contribution's `config.ts` aggregate\n\t\t\t\t// projection into ONE bucket — the SAME accumulation the live\n\t\t\t\t// codegen cycle performed (sui's `networks.<net>` + each\n\t\t\t\t// package's `packages.<name>` / `objects` / `mvrOverrides`,\n\t\t\t\t// the account plugin's `accounts`). The merged bucket carries\n\t\t\t\t// real (live-`acquire`-resolved) ids; we then slice it into the\n\t\t\t\t// loadable `IdConfig` shape.\n\t\t\t\tconst bucket: Record<string, unknown> = {};\n\t\t\t\t// `accounts.ts` is a SEPARATE aggregate bucket (the account\n\t\t\t\t// plugin routes it to `generated-extras`); fold its projection\n\t\t\t\t// under an `accounts` key so the id-config carries account\n\t\t\t\t// addresses alongside the `config.ts`-derived ids.\n\t\t\t\tconst accounts: Record<string, unknown> = {};\n\t\t\t\t// The generic resolver channel — `values[namespace][key]` —\n\t\t\t\t// accumulated from any LIVE config-binding aggregate that\n\t\t\t\t// declared `idConfigValues` (the plugin live JSON the typed\n\t\t\t\t// fields can't carry). Deep-merged so sibling namespaces /\n\t\t\t\t// keys from distinct plugins coexist.\n\t\t\t\tconst values: Record<string, unknown> = {};\n\t\t\t\tfor (const decl of registered) {\n\t\t\t\t\tif (decl.aggregate === undefined) continue;\n\t\t\t\t\t// The generic `values` channel is BUCKET-BLIND: any LIVE\n\t\t\t\t\t// config-binding aggregate (config.ts, coins.ts, deepbook.ts,\n\t\t\t\t\t// walrus.ts, seal.ts, ...) may declare `idConfigValues`. Fold\n\t\t\t\t\t// every contributor's so the committed-tree `resolveValue` calls\n\t\t\t\t\t// those buckets emit resolve at app build/dev time -- not just the\n\t\t\t\t\t// `config.ts` plugins'. Slicing the TYPED id-config fields\n\t\t\t\t\t// (`networks` / `packages` / `mvrOverrides`) stays scoped to the\n\t\t\t\t\t// `config.ts` bucket below.\n\t\t\t\t\tif (decl.aggregate.idConfigValues !== undefined) {\n\t\t\t\t\t\tdeepMerge(values, decl.aggregate.idConfigValues);\n\t\t\t\t\t}\n\t\t\t\t\tif (decl.aggregate.bucket === 'config.ts') {\n\t\t\t\t\t\tconst emission = yield* runEmitter(decl);\n\t\t\t\t\t\tconst projected = decl.aggregate.project(emission.exports);\n\t\t\t\t\t\tif (projected !== null) deepMerge(bucket, projected);\n\t\t\t\t\t} else if (decl.aggregate.bucket === 'accounts.ts') {\n\t\t\t\t\t\tconst emission = yield* runEmitter(decl);\n\t\t\t\t\t\tconst projected = decl.aggregate.project(emission.exports);\n\t\t\t\t\t\tif (projected !== null) deepMerge(accounts, projected);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbucket['accounts'] = accounts;\n\t\t\t\treturn idConfigFromBucket(bucket, network, values as IdConfigValues);\n\t\t\t});\n\n\t\tconst emitExtras: CodegenOrchestrator['emitExtras'] = () =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst registered = (yield* Ref.get(contributionsRef)).map((e) => e.decl);\n\t\t\t\tconst extras = registered.filter(isExtrasDecl);\n\t\t\t\tif (extras.length === 0) {\n\t\t\t\t\t// Nothing routed to the dev tree (no wallet/accounts mounted).\n\t\t\t\t\treturn { filesWritten: [], filesUnchanged: [], filesChmod: [], bindings: null };\n\t\t\t\t}\n\t\t\t\t// Validate the extras-only set up front (mirrors `runEmitCycle`'s\n\t\t\t\t// pre-flight), then emit DIRECTLY against the real paths — no\n\t\t\t\t// stage-and-swap, since the extras tree lives outside the runtime\n\t\t\t\t// `outputDir`. Every `generated-extras` decl routes through\n\t\t\t\t// `paths.resolveExtras`; no `generated`-located decl is present,\n\t\t\t\t// so the committed `src/generated` tree is untouched.\n\t\t\t\tyield* validateUniqueness(extras);\n\t\t\t\tyield* validateAggregatePathAvailability(extras);\n\t\t\t\tconst paths = yield* CodegenPathsService;\n\t\t\t\t// Acquire the dedicated `codegenLockFile` for symmetry with\n\t\t\t\t// `runEmitCycle` — the extras emit writes into the shared stack\n\t\t\t\t// tree (`generated-extras`) and a concurrent codegen cycle could\n\t\t\t\t// otherwise interleave. Scoped so the lock releases when the emit\n\t\t\t\t// completes (or fails).\n\t\t\t\treturn yield* Effect.scoped(\n\t\t\t\t\tEffect.gen(function* () {\n\t\t\t\t\t\tyield* acquireStackLock(paths.codegenLockFile, CODEGEN_CYCLE_LOCK_TIMEOUT_MS).pipe(\n\t\t\t\t\t\t\tEffect.mapError(\n\t\t\t\t\t\t\t\t(cause) =>\n\t\t\t\t\t\t\t\t\tnew CodegenWriteFailed({\n\t\t\t\t\t\t\t\t\t\toutputPath: paths.codegenLockFile,\n\t\t\t\t\t\t\t\t\t\tstage: 'write',\n\t\t\t\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\t\treturn yield* runEmitCycleInner({ contributions: extras, trackTree: false }, paths);\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t});\n\n\t\treturn CodegenOrchestratorService.of({\n\t\t\tregisterContribution,\n\t\t\tassembleIdConfig,\n\t\t\temitExtras,\n\t\t});\n\t}),\n);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"service.mjs","names":[],"sources":["../../../src/orchestrators/codegen/service.ts"],"sourcesContent":["// Codegen orchestrator — main service.\n//\n// Architecture §6 (Codegenable):\n// \"Codegen is a surface (L4) that walks the plugin-emitted\n// `Codegenable` capability decls.\"\n//\n// Hard boundary (distilled-doc §\"Hard boundary\"):\n// \"Apps consume codegen output, NOT devstack.\" Every runtime value\n// the app needs flows through codegen-emitted TS files. The\n// orchestrator owns the only legitimate channel from stack state\n// to app code.\n//\n// Lifecycle (distilled-doc §\"Lifecycle states\"):\n// - at-up: per supervisor cycle, run all emitters serially,\n// with per-file atomic/idempotent writes inside a\n// cycle-level stage-and-swap (rollback on any\n// per-file failure leaves the user-visible tree\n// unchanged).\n// - on-change: re-run as part of the new cycle when the\n// supervisor restarts.\n// - on-demand: same emit pipeline for snapshot resume.\n// - idempotency: per-file no-touch on unchanged content.\n//\n// Pipeline (one cycle):\n// 1. Collect `Codegenable` contributions from the active stack\n// members (the supervisor walks `member.capabilities`).\n// 2. Validate uniqueness: emitter name (literal) is globally\n// unique; output path is globally unique.\n// 3. Run each emitter serially (distilled-doc § \"Serial within a\n// cycle\"). Each emit writes exports through a per-file context.\n// 4. Render each collected file emission to a TS source string.\n// 5. Emit each file with an atomic write + per-file no-touch\n// idempotency.\n// 6. Run the Move-bindings emitter against the collected\n// `package`-emitted contributions.\n// 7. Write the `.gitignore`.\n//\n// What this module does NOT do:\n// - Construct plugin-level resolved blobs (plugins pass them at\n// factory-build time).\n// - Decode the manifest envelope (plugins pass resolved blobs at\n// factory-build time; the orchestrator never re-reads the envelope).\n// - Watch files. Re-emit is driven by the supervisor cycle (and\n// on-demand by the CLI); the app's own toolchain (Vite/HMR)\n// watches the emitted output tree.\n// - Walk the user's Move-source mtimes (see `bindings.ts`).\n\nimport { Context, Effect, FileSystem, Layer, Order, Ref, Scope } from 'effect';\nimport { dirname } from 'node:path';\n\nimport {\n\tisRawExpr,\n\ttype CodegenableDecl,\n\ttype CodegenEmitDone,\n\ttype CodegenEmitContext,\n\ttype OutputLocation,\n} from '../../contracts/codegenable.ts';\nimport { CONFIG_RUNTIME_OUTPUT_PATH, CONFIG_RUNTIME_SOURCE } from './config-runtime.ts';\nimport { acquireStackLock } from '../../substrate/runtime/cross-process/stack-lock.ts';\nimport { mintRandomSuffix } from '../../substrate/runtime/random-suffix.ts';\nimport { stageAndSwap, StageAndSwapError } from '../../substrate/runtime/stage-and-swap/index.ts';\n\nimport {\n\temitBindings,\n\ttype EmitBindingsResult,\n\tisPackageBindings,\n\tMoveCodegenService,\n\tMoveSummaryRunnerService,\n\ttype PackageBindings,\n} from './bindings.ts';\nimport { emitOne } from './emit.ts';\nimport {\n\tCodegenAggregateConflict,\n\tCodegenEmitFailed,\n\tCodegenEmitterCollision,\n\tCodegenPathConflict,\n\tCodegenWriteFailed,\n\ttype CodegenError,\n} from './errors.ts';\nimport { renderFile } from './format.ts';\nimport { writeGitignore } from './gitignore.ts';\nimport { UNRESOLVED_ID } from './id-config.ts';\nimport type { IdConfig, IdConfigNetwork, IdConfigPackage, IdConfigValues } from './id-config.ts';\nimport { CodegenPathsService, type CodegenPaths } from './paths.ts';\nimport { dirModeFor, modeFor, NON_SENSITIVE_DIR_MODE } from './permissions.ts';\n\n// -----------------------------------------------------------------------------\n// Public types\n// -----------------------------------------------------------------------------\n\n/** A type alias the orchestrator uses internally to avoid restating\n * the wide-erased `CodegenableDecl` generic in every signature. */\nexport type Codegenable = CodegenableDecl<string>;\n\nexport interface RunEmitCycleInput {\n\t/** All Codegenable contributions, as collected from the active\n\t * stack members' `capabilities` tuples. The orchestrator does\n\t * not look at plugins — only at the decl set. */\n\treadonly contributions: ReadonlyArray<Codegenable>;\n\t/** Optional: import-extension for bindings emission. Default `.ts`. */\n\treadonly bindingsImportExtension?: '.ts' | '.js' | '';\n\t/** When `true`, this is the COMMITTED projection tree (`src/generated`,\n\t * written by the stack-free `codegen` verb): the `.gitignore` TRACKS\n\t * the stubs (bindings, config, config-runtime) so `tsc`/`vite build`\n\t * work on a fresh clone, ignoring only `sensitivePaths`. When\n\t * `false`/omitted, the ephemeral tree is blanket-ignored. */\n\treadonly trackTree?: boolean;\n}\n\nexport interface RunEmitCycleResult {\n\treadonly filesWritten: ReadonlyArray<string>;\n\treadonly filesUnchanged: ReadonlyArray<string>;\n\treadonly filesChmod: ReadonlyArray<string>;\n\treadonly bindings: EmitBindingsResult | null;\n}\n\n/** Resolve a decl/aggregate's absolute output path against the tree\n * selected by its `outputLocation`. `'generated-extras'` routes\n * through `paths.resolveExtras` (the gitignored dev tree); everything\n * else through `paths.resolve` (the staging-and-swapped runtime tree). */\nconst resolveAt = (\n\tpaths: CodegenPaths,\n\tlocation: OutputLocation,\n\toutputPath: string,\n): Effect.Effect<string, CodegenPathConflict> =>\n\tlocation === 'generated-extras' ? paths.resolveExtras(outputPath) : paths.resolve(outputPath);\n\nconst declLocation = (decl: Pick<Codegenable, 'outputLocation'>): OutputLocation =>\n\tdecl.outputLocation ?? 'generated';\n\n/** A decl belongs to the `emitExtras` flush IFF it writes ONLY into the\n * dev-only `generated-extras` tree. INVARIANT: `emitExtras` must NEVER\n * touch the committed `src/generated` tree (it runs without stage-and-swap),\n * so a decl that would emit any file into `generated` is excluded.\n *\n * - `aggregateOnly` decls write only their aggregate file: include iff the\n * aggregate's location is `generated-extras` (e.g. each account folding\n * into the gitignored `accounts.ts`).\n * - standalone decls write a per-decl file (and possibly an aggregate):\n * include iff that standalone file lands in `generated-extras` (e.g. the\n * wallet's `dev-wallet.ts`). A standalone-in-`generated` decl is excluded\n * even if its aggregate targets `generated-extras`. */\nconst isExtrasDecl = (decl: Codegenable): boolean =>\n\tdecl.aggregateOnly === true\n\t\t? (decl.aggregate?.outputLocation ?? 'generated') === 'generated-extras'\n\t\t: declLocation(decl) === 'generated-extras';\n\nconst buildParentModeResolver = (\n\tpaths: CodegenPaths,\n\tentries: ReadonlyArray<{\n\t\treadonly outputPath: string;\n\t\treadonly location: OutputLocation;\n\t\treadonly sensitive: boolean;\n\t}>,\n): Effect.Effect<(absolutePath: string) => number, CodegenPathConflict> =>\n\tEffect.gen(function* () {\n\t\tconst byParent = new Map<string, Array<{ readonly sensitive?: boolean }>>();\n\t\tfor (const entry of entries) {\n\t\t\tconst parent = dirname(yield* resolveAt(paths, entry.location, entry.outputPath));\n\t\t\tconst current = byParent.get(parent);\n\t\t\tif (current === undefined) {\n\t\t\t\tbyParent.set(parent, [{ sensitive: entry.sensitive }]);\n\t\t\t} else {\n\t\t\t\tcurrent.push({ sensitive: entry.sensitive });\n\t\t\t}\n\t\t}\n\t\tconst modes = new Map<string, number>();\n\t\tfor (const [parent, parentDecls] of byParent) {\n\t\t\tmodes.set(parent, dirModeFor(parentDecls));\n\t\t}\n\t\treturn (absolutePath: string) => modes.get(dirname(absolutePath)) ?? NON_SENSITIVE_DIR_MODE;\n\t});\n\n// -----------------------------------------------------------------------------\n// Main entry — one cycle of the codegen pipeline\n// -----------------------------------------------------------------------------\n\n/**\n * Per-cycle lock acquire timeout. Codegen cycles can be file-system\n * heavy (multi-emitter, Move-bindings compilation), so we allow more\n * than the substrate's default 5s for `stack.lock` — a custom CLI\n * caller that hits a supervisor mid-cycle should wait, not error.\n * Mirrors `MOVE_BUILD_LOCK_TIMEOUT_MS` (5 minutes).\n */\nconst CODEGEN_CYCLE_LOCK_TIMEOUT_MS = 5 * 60_000;\n\nexport const runEmitCycle = (\n\tinput: RunEmitCycleInput,\n): Effect.Effect<\n\tRunEmitCycleResult,\n\tCodegenError,\n\tFileSystem.FileSystem | CodegenPathsService | MoveSummaryRunnerService | MoveCodegenService\n> =>\n\t// Per-process lock. The supervisor's serialized post-acquire path\n\t// is fine for the normal lifecycle, but custom callers (CLI direct\n\t// invocations, future watcher hooks) can call `runEmitCycle`\n\t// concurrently. With `stageAndSwap` writing under shared\n\t// `<outputDir>.staging.<cycleId>` and `<outputDir>.bak.<cycleId>`\n\t// siblings — and with the pre-seed `fs.copy` reading from the\n\t// shared `outputDir` — two overlapping cycles can stage from a\n\t// half-published tree of the other. The lock serializes them.\n\t//\n\t// Dedicated `codegenLockFile` (NOT the substrate `stack.lock`):\n\t// codegen cycles can run for many seconds when Move bindings\n\t// compile, and the substrate's `stack.lock` is reserved for short\n\t// critical sections (roster mutations, the snapshot bounce). A\n\t// dedicated lock isolates codegen contention from those subsystems.\n\tEffect.scoped(\n\t\tEffect.gen(function* () {\n\t\t\tconst paths = yield* CodegenPathsService;\n\t\t\tyield* acquireStackLock(paths.codegenLockFile, CODEGEN_CYCLE_LOCK_TIMEOUT_MS).pipe(\n\t\t\t\tEffect.mapError(\n\t\t\t\t\t(cause) =>\n\t\t\t\t\t\tnew CodegenWriteFailed({\n\t\t\t\t\t\t\toutputPath: paths.codegenLockFile,\n\t\t\t\t\t\t\tstage: 'write',\n\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t}),\n\t\t\t\t),\n\t\t\t);\n\t\t\treturn yield* runEmitCycleLocked(input);\n\t\t}),\n\t);\n\nconst runEmitCycleLocked = (\n\tinput: RunEmitCycleInput,\n): Effect.Effect<\n\tRunEmitCycleResult,\n\tCodegenError,\n\tFileSystem.FileSystem | CodegenPathsService | MoveSummaryRunnerService | MoveCodegenService\n> =>\n\tEffect.gen(function* () {\n\t\tconst paths = yield* CodegenPathsService;\n\t\t// Yield the Move-codegen services here (outside the\n\t\t// stage-and-swap build) so the build's R-channel collapses to\n\t\t// just `FileSystem.FileSystem` — the substrate `stageAndSwap`\n\t\t// primitive constrains `build`'s requirements to\n\t\t// `FileSystem.FileSystem`.\n\t\tconst moveRunner = yield* MoveSummaryRunnerService;\n\t\tconst moveCodegen = yield* MoveCodegenService;\n\n\t\t// Pre-flight contribution-set validation. Detected BEFORE the\n\t\t// stage-and-swap so a programming-bug rejection (duplicate\n\t\t// emitterName / outputPath collision) never opens an empty\n\t\t// staging dir on disk. `package` is the one exception:\n\t\t// multiple instances (one per Package) legitimately share the\n\t\t// `emitterName` literal by design.\n\t\tyield* validateUniqueness(input.contributions);\n\t\tyield* validateAggregatePathAvailability(input.contributions);\n\n\t\t// Cycle-level atomicity: substrate stage-and-swap. The build\n\t\t// populates `<outputDir>.staging.<cycleId>/`; on success the\n\t\t// substrate renames it into place; on any failure the previous\n\t\t// tree (if any) is restored byte-for-byte. Without this wrapper\n\t\t// a mid-cycle emit failure would leave `src/generated/`\n\t\t// half-rewritten — see STYLE_GUIDE §19.\n\t\t//\n\t\t// `preserveOnPreseed: true` — substrate clones the current\n\t\t// target into staging before `build` runs so the per-file\n\t\t// no-touch idempotency (and gitignore user-block preservation)\n\t\t// sees the right baseline. Files this cycle rewrites are\n\t\t// overwritten in staging; files this cycle does NOT touch\n\t\t// survive into the next target verbatim with their original\n\t\t// mtimes (HMR watchers stay quiet for unchanged outputs).\n\t\t//\n\t\t// Cycle id is a random suffix. STYLE_GUIDE §17 mandates 8\n\t\t// hex chars for external-facing identifiers; this is a\n\t\t// deliberate 16-char carve-out because the value only\n\t\t// appears in transient staging-directory names\n\t\t// (`.staging.<id>` / `.bak.<id>`) that the substrate rm's\n\t\t// after publish. The extra entropy is defense-in-depth for\n\t\t// the race-window where two concurrent emit cycles under a\n\t\t// custom-CLI caller could mint overlapping staging dirs\n\t\t// against the same shared `outputDir`; a collision there\n\t\t// would corrupt a half-built tree, not just clash an\n\t\t// operator-visible name.\n\t\tconst cycleId = mintRandomSuffix(16);\n\t\tconst stagingPaths = paths.withRoot(`${paths.outputDir}.staging.${cycleId}`);\n\n\t\treturn yield* stageAndSwap({\n\t\t\ttargetPath: paths.outputDir,\n\t\t\tidSuffix: cycleId,\n\t\t\tpreserveOnPreseed: true,\n\t\t\tbuild: Effect.gen(function* () {\n\t\t\t\tconst inner = yield* runEmitCycleInner(input, stagingPaths).pipe(\n\t\t\t\t\tEffect.provideService(MoveSummaryRunnerService, moveRunner),\n\t\t\t\t\tEffect.provideService(MoveCodegenService, moveCodegen),\n\t\t\t\t);\n\t\t\t\t// Rewrite paths so callers see the final user-visible\n\t\t\t\t// `outputDir` location, not the staging directory that\n\t\t\t\t// only exists for the duration of the build.\n\t\t\t\treturn rewriteResultPaths(inner, stagingPaths.outputDir, paths.outputDir);\n\t\t\t}),\n\t\t}).pipe(\n\t\t\tEffect.mapError((e): CodegenError => {\n\t\t\t\tif (e instanceof StageAndSwapError) {\n\t\t\t\t\treturn new CodegenWriteFailed({\n\t\t\t\t\t\toutputPath: paths.outputDir,\n\t\t\t\t\t\tstage: 'rename',\n\t\t\t\t\t\tcause: e,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\treturn e;\n\t\t\t}),\n\t\t);\n\t});\n\n/**\n * Project a staging-rooted `RunEmitCycleResult` back into the user-\n * visible `outputDir` namespace. The build effect writes through\n * `stagingDir` paths; callers (and tests) expect to see the final\n * post-rename locations.\n */\nconst rewriteResultPaths = (\n\tresult: RunEmitCycleResult,\n\tstagingDir: string,\n\toutputDir: string,\n): RunEmitCycleResult => {\n\tconst stripped = stagingDir.replace(/\\/+$/, '');\n\tconst target = outputDir.replace(/\\/+$/, '');\n\tconst rewrite = (p: string): string =>\n\t\tp.startsWith(stripped) ? `${target}${p.slice(stripped.length)}` : p;\n\treturn {\n\t\tfilesWritten: result.filesWritten.map(rewrite),\n\t\tfilesUnchanged: result.filesUnchanged.map(rewrite),\n\t\tfilesChmod: result.filesChmod.map(rewrite),\n\t\tbindings:\n\t\t\tresult.bindings === null\n\t\t\t\t? null\n\t\t\t\t: {\n\t\t\t\t\t\t...result.bindings,\n\t\t\t\t\t\tfilesWritten: result.bindings.filesWritten.map(rewrite),\n\t\t\t\t\t},\n\t};\n};\n\n/**\n * The body of one emit cycle. Pulled out of `runEmitCycle` so the\n * stage-and-swap wrapper can drive it against a redirected\n * `CodegenPathsService` (the staging tree). Validation runs BEFORE\n * this function so callers know the contribution set is well-formed.\n */\nconst runEmitCycleInner = (\n\tinput: RunEmitCycleInput,\n\tpaths: CodegenPaths,\n): Effect.Effect<\n\tRunEmitCycleResult,\n\tCodegenError,\n\tFileSystem.FileSystem | MoveSummaryRunnerService | MoveCodegenService\n> =>\n\tEffect.gen(function* () {\n\t\tyield* Effect.logInfo(`codegen: emitting projection (trackTree=${input.trackTree === true}).`);\n\t\tconst fileEmitters: Array<Codegenable> = [...input.contributions];\n\n\t\tconst filesWritten: Array<string> = [];\n\t\tconst filesUnchanged: Array<string> = [];\n\t\tconst filesChmod: Array<string> = [];\n\t\t// Aggregate buckets keyed by plugin-supplied bucket name. The\n\t\t// orchestrator treats bucket names as opaque tags chosen by\n\t\t// the contributor; it never branches on plugin identity. See\n\t\t// `CodegenableDecl.aggregate` (contracts/codegenable.ts).\n\t\tconst aggregates = new Map<string, Record<string, unknown>>();\n\t\t// Per-bucket location + sensitivity, read from the first decl\n\t\t// the orchestrator sees contributing to the bucket. Drives where\n\t\t// the synthesized aggregate file lands and its file mode. Every\n\t\t// later contributor to the same bucket MUST agree (enforced\n\t\t// below) — a silent disagreement could misroute a sensitive\n\t\t// aggregate into the committed `generated` tree.\n\t\tconst aggregateMeta = new Map<string, AggregateMeta>();\n\t\tconst packageContribs: Array<PackageBindings> = [];\n\t\tconst sortedDecls = [...fileEmitters].sort(\n\t\t\tOrder.mapInput(Order.String, (d: Codegenable) => d.outputPath),\n\t\t);\n\t\t// Parent-mode resolver must see every path that will be written —\n\t\t// standalone decls AND synthesized aggregates — across BOTH trees.\n\t\t// Aggregate-only decls do not write a standalone file, so they are\n\t\t// excluded from the standalone-path set here.\n\t\tconst parentModeFor = yield* buildParentModeResolver(\n\t\t\tpaths,\n\t\t\tfileEmitters\n\t\t\t\t.filter((d) => d.aggregateOnly !== true)\n\t\t\t\t.map((d) => ({\n\t\t\t\t\toutputPath: d.outputPath,\n\t\t\t\t\tlocation: declLocation(d),\n\t\t\t\t\tsensitive: d.sensitive === true,\n\t\t\t\t})),\n\t\t);\n\t\tfor (const decl of sortedDecls) {\n\t\t\tconst emission = yield* runEmitter(decl);\n\t\t\tconst exported = emission.exports;\n\t\t\tif (decl.aggregate !== undefined) {\n\t\t\t\tconst projected = decl.aggregate.project(exported);\n\t\t\t\tif (projected !== null) {\n\t\t\t\t\tconst bucket = aggregates.get(decl.aggregate.bucket) ?? {};\n\t\t\t\t\tdeepMerge(bucket, projected);\n\t\t\t\t\taggregates.set(decl.aggregate.bucket, bucket);\n\t\t\t\t\t// First-contributor-wins for routing/sensitivity, but a\n\t\t\t\t\t// LATER contributor that disagrees is a hard error — the\n\t\t\t\t\t// `AggregateContribution` contract requires all\n\t\t\t\t\t// contributors to a bucket to agree, and a silent\n\t\t\t\t\t// mismatch could misroute a sensitive aggregate into the\n\t\t\t\t\t// committed `generated` tree (secret leak).\n\t\t\t\t\tconst declLoc = decl.aggregate.outputLocation ?? 'generated';\n\t\t\t\t\tconst declSensitive = decl.aggregate.sensitive === true;\n\t\t\t\t\tconst established = aggregateMeta.get(decl.aggregate.bucket);\n\t\t\t\t\tif (established === undefined) {\n\t\t\t\t\t\taggregateMeta.set(decl.aggregate.bucket, {\n\t\t\t\t\t\t\tlocation: declLoc,\n\t\t\t\t\t\t\tsensitive: declSensitive,\n\t\t\t\t\t\t\testablishedBy: decl.emitterName,\n\t\t\t\t\t\t});\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif (established.location !== declLoc) {\n\t\t\t\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\t\t\t\tnew CodegenAggregateConflict({\n\t\t\t\t\t\t\t\t\tbucket: decl.aggregate.bucket,\n\t\t\t\t\t\t\t\t\tfield: 'outputLocation',\n\t\t\t\t\t\t\t\t\testablished: established.location,\n\t\t\t\t\t\t\t\t\tconflicting: declLoc,\n\t\t\t\t\t\t\t\t\temitters: [established.establishedBy, decl.emitterName],\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (established.sensitive !== declSensitive) {\n\t\t\t\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\t\t\t\tnew CodegenAggregateConflict({\n\t\t\t\t\t\t\t\t\tbucket: decl.aggregate.bucket,\n\t\t\t\t\t\t\t\t\tfield: 'sensitive',\n\t\t\t\t\t\t\t\t\testablished: String(established.sensitive),\n\t\t\t\t\t\t\t\t\tconflicting: String(declSensitive),\n\t\t\t\t\t\t\t\t\temitters: [established.establishedBy, decl.emitterName],\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Move-bindings collection: any export whose shape matches\n\t\t\t// the orchestrator's `PackageBindings` consumer contract is\n\t\t\t// forwarded to `emitBindings`. Runs against the raw `exported`\n\t\t\t// map (not via `aggregate.project`) so direct `codegenable(...)`\n\t\t\t// contributions — which carry no `aggregate` — are picked up\n\t\t\t// too. The orchestrator validates the shape it consumes; it\n\t\t\t// does NOT name the plugin that produced it.\n\t\t\tfor (const value of Object.values(exported)) {\n\t\t\t\tif (isPackageBindings(value)) {\n\t\t\t\t\tpackageContribs.push(value);\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Aggregate-only decls contribute solely to their bucket; the\n\t\t\t// standalone per-decl file is skipped (the combined aggregate\n\t\t\t// is the only app-facing surface).\n\t\t\tif (decl.aggregateOnly === true) continue;\n\t\t\tconst rendered = renderFile({\n\t\t\t\temitterName: decl.emitterName,\n\t\t\t\toutputPath: decl.outputPath,\n\t\t\t\tsensitive: decl.sensitive === true,\n\t\t\t\texports: exported,\n\t\t\t\timports: emission.imports,\n\t\t\t});\n\t\t\tif (!rendered.ok) {\n\t\t\t\treturn yield* Effect.fail(rendered.error);\n\t\t\t}\n\t\t\tconst abs = yield* resolveAt(paths, declLocation(decl), decl.outputPath);\n\t\t\tconst outcome = yield* emitOne({\n\t\t\t\tpath: abs,\n\t\t\t\tcontent: rendered.text,\n\t\t\t\tmode: modeFor(decl),\n\t\t\t\tparentMode: parentModeFor(abs),\n\t\t\t});\n\t\t\tswitch (outcome.outcome) {\n\t\t\t\tcase 'wrote':\n\t\t\t\t\tfilesWritten.push(abs);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'unchanged':\n\t\t\t\t\tfilesUnchanged.push(abs);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'chmod-only':\n\t\t\t\t\tfilesChmod.push(abs);\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tconst aggregateFiles = buildAggregateFiles(aggregates, aggregateMeta);\n\t\t// True once any aggregate references a `config-runtime.ts` resolver\n\t\t// (the committed `config.ts`): then we ALSO emit the fixed\n\t\t// `config-runtime.ts` resolver and import the referenced resolvers into\n\t\t// that file.\n\t\tlet needsConfigRuntime = false;\n\t\tfor (const aggregate of aggregateFiles) {\n\t\t\tconst resolvers = resolversUsedBy(aggregate.exports);\n\t\t\tif (resolvers.length > 0) needsConfigRuntime = true;\n\t\t\tconst rendered = renderFile({\n\t\t\t\temitterName: aggregate.emitterName,\n\t\t\t\toutputPath: aggregate.outputPath,\n\t\t\t\tsensitive: aggregate.sensitive,\n\t\t\t\texports: aggregate.exports,\n\t\t\t\t// The committed `config.ts` resolves ids/network at runtime —\n\t\t\t\t// import exactly the resolvers it references (no unused imports;\n\t\t\t\t// oxlint is pinned and flags them). `.js` specifier (ESM/TS-\n\t\t\t\t// resolved) mirrors the bindings' import style.\n\t\t\t\t...(resolvers.length > 0\n\t\t\t\t\t? { imports: [`import { ${resolvers.join(', ')} } from './config-runtime.js';`] }\n\t\t\t\t\t: {}),\n\t\t\t});\n\t\t\tif (!rendered.ok) {\n\t\t\t\treturn yield* Effect.fail(rendered.error);\n\t\t\t}\n\t\t\tconst abs = yield* resolveAt(paths, aggregate.location, aggregate.outputPath);\n\t\t\tconst outcome = yield* emitOne({\n\t\t\t\tpath: abs,\n\t\t\t\tcontent: rendered.text,\n\t\t\t\tmode: aggregate.sensitive ? 0o600 : 0o644,\n\t\t\t\tparentMode: parentModeFor(abs),\n\t\t\t});\n\t\t\tswitch (outcome.outcome) {\n\t\t\t\tcase 'wrote':\n\t\t\t\t\tfilesWritten.push(abs);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'unchanged':\n\t\t\t\t\tfilesUnchanged.push(abs);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'chmod-only':\n\t\t\t\t\tfilesChmod.push(abs);\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\t// Emit the FIXED `config-runtime.ts` resolver (a constant string, NOT\n\t\t// routed through the literal renderer) when `config.ts` resolves ids\n\t\t// at runtime. It reads the injected `__DEVSTACK_IDS__` global and\n\t\t// THROWS `DevstackConfigMissingError` on an unresolved id.\n\t\tif (needsConfigRuntime) {\n\t\t\tconst abs = yield* paths.resolve(CONFIG_RUNTIME_OUTPUT_PATH);\n\t\t\tconst outcome = yield* emitOne({\n\t\t\t\tpath: abs,\n\t\t\t\tcontent: CONFIG_RUNTIME_SOURCE,\n\t\t\t\tmode: 0o644,\n\t\t\t\tparentMode: parentModeFor(abs),\n\t\t\t});\n\t\t\tswitch (outcome.outcome) {\n\t\t\t\tcase 'wrote':\n\t\t\t\t\tfilesWritten.push(abs);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'unchanged':\n\t\t\t\t\tfilesUnchanged.push(abs);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'chmod-only':\n\t\t\t\t\tfilesChmod.push(abs);\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tlet bindings: EmitBindingsResult | null = null;\n\t\tif (packageContribs.length > 0) {\n\t\t\tbindings = yield* emitBindings({\n\t\t\t\tbindingsDir: paths.bindingsDir,\n\t\t\t\tpackages: packageContribs,\n\t\t\t\timportExtension: input.bindingsImportExtension,\n\t\t\t});\n\t\t} else {\n\t\t\tyield* Effect.logInfo(\n\t\t\t\t'codegen: no package contributions; skipping Move-to-TS bindings step.',\n\t\t\t);\n\t\t}\n\n\t\t// `.gitignore` covers the runtime `generated/` tree only. Decls\n\t\t// routed to `generated-extras` live outside `outputDir` (that\n\t\t// whole tree is gitignored at the `.devstack/` level), and\n\t\t// aggregate-only decls never write a standalone file — both are\n\t\t// excluded here so the managed `.gitignore` only lists real\n\t\t// sensitive files in `outputDir`.\n\t\t//\n\t\t// Synthesized aggregate files are a SEPARATE source of sensitive\n\t\t// runtime paths: a sensitive bucket routed to `generated` writes a\n\t\t// real secret-bearing file in `outputDir` that the standalone-decl\n\t\t// scan above never sees (its contributors may all be\n\t\t// `aggregateOnly`). Without an explicit ignore line such a file\n\t\t// would rely solely on the blanket `*` rule — and a user `!<file>`\n\t\t// override in the preserved user block would then start tracking\n\t\t// the secret. Include them so each gets an explicit re-ignore line.\n\t\t// The managed `.gitignore` belongs to the committed/live `generated`\n\t\t// tree at `<outputDir>/.gitignore` — and only that tree. We write it\n\t\t// ONLY when this emit actually produces `generated` output. An\n\t\t// EXTRAS-ONLY emit (boot's `emitExtras`) writes solely into `extrasDir`,\n\t\t// which lives under the already-gitignored `.devstack/` root, so it\n\t\t// needs no `.gitignore` of its own. Skipping it here is also the fix\n\t\t// for a real bug: at BOOT `outputDir` is only a STAND-IN (boot emits\n\t\t// nothing there — see `paths.ts`), so writing `paths.gitignoreFile` for\n\t\t// an extras-only emit would clobber the committed tree's TRACKED policy\n\t\t// with the blanket ignore-all on every `devstack up`, breaking\n\t\t// `tsc`/`vite build` on a fresh clone.\n\t\tconst hasGeneratedOutput =\n\t\t\tfileEmitters.some((d) => d.aggregateOnly !== true && declLocation(d) === 'generated') ||\n\t\t\taggregateFiles.some((a) => a.location === 'generated');\n\t\tif (hasGeneratedOutput) {\n\t\t\tconst sensitivePaths = [\n\t\t\t\t...fileEmitters\n\t\t\t\t\t.filter(\n\t\t\t\t\t\t(d) =>\n\t\t\t\t\t\t\td.sensitive === true && d.aggregateOnly !== true && declLocation(d) === 'generated',\n\t\t\t\t\t)\n\t\t\t\t\t.map((d) => d.outputPath),\n\t\t\t\t...aggregateFiles\n\t\t\t\t\t.filter((a) => a.sensitive && a.location === 'generated')\n\t\t\t\t\t.map((a) => a.outputPath),\n\t\t\t];\n\t\t\tyield* writeGitignore({\n\t\t\t\tpath: paths.gitignoreFile,\n\t\t\t\tsensitivePaths,\n\t\t\t\tparentMode: parentModeFor(paths.gitignoreFile),\n\t\t\t\ttrackTree: input.trackTree === true,\n\t\t\t});\n\t\t}\n\n\t\treturn {\n\t\t\tfilesWritten,\n\t\t\tfilesUnchanged,\n\t\t\tfilesChmod,\n\t\t\tbindings,\n\t\t};\n\t});\n\n// -----------------------------------------------------------------------------\n// Internals\n// -----------------------------------------------------------------------------\n\ninterface CodegenEmission {\n\treadonly exports: { readonly [key: string]: unknown };\n\treadonly imports: ReadonlyArray<string>;\n}\n\nconst runEmitter = (decl: Codegenable): Effect.Effect<CodegenEmission, CodegenEmitFailed> =>\n\tEffect.gen(function* () {\n\t\t// Annotate the emit span with the contributor's plugin-supplied\n\t\t// `kind` tag (declared on the decl's `aggregate`). The orchestrator\n\t\t// stays name-blind — it never reads the tag's VALUE or branches on it\n\t\t// (per the `AggregateContribution` contract); wiring it here makes the\n\t\t// otherwise-dead field observable so a trace attributes each emit to\n\t\t// the plugin family that produced it. `annotateCurrentSpan` only lands\n\t\t// inside an enclosing `withSpan` — the `codegen.emit` span this body is\n\t\t// wrapped in below.\n\t\tconst kind = decl.aggregate?.kind;\n\t\tif (kind !== undefined) {\n\t\t\tyield* Effect.annotateCurrentSpan('codegen.kind', kind);\n\t\t}\n\t\tconst exports: Record<string, unknown> = {};\n\t\tconst imports: Array<string> = [];\n\t\tconst done: CodegenEmitDone = { _tag: 'CodegenEmitDone' };\n\t\tconst ctx: CodegenEmitContext = {\n\t\t\texportConst: (name, value) => {\n\t\t\t\texports[name] = value;\n\t\t\t},\n\t\t\timportStatement: (statement) => {\n\t\t\t\timports.push(statement);\n\t\t\t},\n\t\t\tdone: () => done,\n\t\t};\n\t\tyield* decl.emit(ctx).pipe(\n\t\t\tEffect.mapError(\n\t\t\t\t(cause) =>\n\t\t\t\t\tnew CodegenEmitFailed({\n\t\t\t\t\t\temitterName: decl.emitterName,\n\t\t\t\t\t\toutputPath: decl.outputPath,\n\t\t\t\t\t\tcause,\n\t\t\t\t\t}),\n\t\t\t),\n\t\t);\n\t\treturn { exports, imports };\n\t}).pipe(Effect.withSpan('codegen.emit', { attributes: { 'codegen.emitter': decl.emitterName } }));\n\n/**\n * Uniqueness check: emitter name (literal) must be unique unless\n * the decl opts into repetition via `allowEmitterNameRepetition`\n * (used by per-item plugins like Package, which emit one decl per\n * published package under a shared emitter name). Output paths\n * must be unique across ALL emitters.\n */\nconst validateUniqueness = (\n\tdecls: ReadonlyArray<Codegenable>,\n): Effect.Effect<void, CodegenPathConflict | CodegenEmitterCollision> =>\n\tEffect.gen(function* () {\n\t\tconst byPath = new Map<string, Array<string>>();\n\t\tconst byName = new Map<string, Array<string>>();\n\t\tfor (const d of decls) {\n\t\t\t// Aggregate-only decls write no standalone file, so their\n\t\t\t// `outputPath` is a dead value — exclude them from the\n\t\t\t// path-uniqueness check (many `package` decls legitimately\n\t\t\t// share `config.ts`'s bucket but carry distinct dead\n\t\t\t// `package/<name>.ts` outputPaths that never hit disk).\n\t\t\tif (d.aggregateOnly !== true) {\n\t\t\t\t// Key by (location, path): the same relative path in the\n\t\t\t\t// `generated` vs `generated-extras` trees is two distinct\n\t\t\t\t// files, so `accounts.ts` may exist in both without a\n\t\t\t\t// false collision.\n\t\t\t\tconst pathKey = `${declLocation(d)} ${d.outputPath}`;\n\t\t\t\tconst ps = byPath.get(pathKey) ?? [];\n\t\t\t\tps.push(d.emitterName);\n\t\t\t\tbyPath.set(pathKey, ps);\n\t\t\t}\n\t\t\tif (d.allowEmitterNameRepetition === true) continue;\n\t\t\tconst ns = byName.get(d.emitterName) ?? [];\n\t\t\tns.push(d.outputPath);\n\t\t\tbyName.set(d.emitterName, ns);\n\t\t}\n\t\tfor (const [pathKey, emitters] of byPath) {\n\t\t\tif (emitters.length > 1) {\n\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\tnew CodegenPathConflict({\n\t\t\t\t\t\tkind: 'duplicate',\n\t\t\t\t\t\t// Strip the `<location> ` prefix from the dedup key so\n\t\t\t\t\t\t// the error names the relative path the plugin declared.\n\t\t\t\t\t\toutputPath: pathKey.slice(pathKey.indexOf(' ') + 1),\n\t\t\t\t\t\temitters,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t\tfor (const [name, outputPaths] of byName) {\n\t\t\tif (outputPaths.length > 1) {\n\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\tnew CodegenEmitterCollision({\n\t\t\t\t\t\temitterName: name,\n\t\t\t\t\t\toutputPaths,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t});\n\n/**\n * Reject contribution sets that would have an aggregate bucket\n * collide with a per-decl `outputPath`. The orchestrator only knows\n * bucket names because plugins declared them via `aggregate.bucket`;\n * it does not enumerate or recognize plugin identities here.\n */\nconst validateAggregatePathAvailability = (\n\tdecls: ReadonlyArray<Codegenable>,\n): Effect.Effect<void, CodegenPathConflict> =>\n\tEffect.gen(function* () {\n\t\t// Aggregate buckets keyed by (location, bucket). A standalone\n\t\t// decl in the SAME tree that writes the bucket path would clash\n\t\t// with the synthesized aggregate; a decl in the OTHER tree (or\n\t\t// an aggregate-only decl, which writes no standalone file) does\n\t\t// not.\n\t\tconst aggregatePaths = new Map<string, string>();\n\t\tfor (const decl of decls) {\n\t\t\tif (decl.aggregate !== undefined) {\n\t\t\t\tconst location = decl.aggregate.outputLocation ?? 'generated';\n\t\t\t\taggregatePaths.set(`${location} ${decl.aggregate.bucket}`, decl.aggregate.bucket);\n\t\t\t}\n\t\t}\n\t\tfor (const [key, bucket] of aggregatePaths) {\n\t\t\tconst location = key.slice(0, key.indexOf(' '));\n\t\t\tconst colliding = decls.filter(\n\t\t\t\t(decl) =>\n\t\t\t\t\tdecl.aggregateOnly !== true &&\n\t\t\t\t\tdeclLocation(decl) === location &&\n\t\t\t\t\tdecl.outputPath === bucket,\n\t\t\t);\n\t\t\tif (colliding.length > 0) {\n\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\tnew CodegenPathConflict({\n\t\t\t\t\t\tkind: 'duplicate',\n\t\t\t\t\t\toutputPath: bucket,\n\t\t\t\t\t\temitters: [...colliding.map((decl) => decl.emitterName), `aggregate/${bucket}`],\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t});\n\n/** Per-bucket location + sensitivity, captured from the first decl\n * the orchestrator sees contributing to a bucket. The orchestrator\n * stays name-blind; the plugin owns these on its\n * `AggregateContribution`. `establishedBy` records the emitter name\n * of that first contributor so a later disagreement can quote both\n * sides in `CodegenAggregateConflict`. */\ninterface AggregateMeta {\n\treadonly location: OutputLocation;\n\treadonly sensitive: boolean;\n\treadonly establishedBy: string;\n}\n\ninterface AggregateFile {\n\treadonly emitterName: string;\n\treadonly outputPath: string;\n\treadonly exports: { readonly [key: string]: unknown };\n\treadonly location: OutputLocation;\n\treadonly sensitive: boolean;\n}\n\n/**\n * Recursively merge `source` into `target`. Distinct buckets are\n * shallow records keyed by name, but a single bucket (e.g. `config.ts`)\n * accumulates contributions from MANY plugins into nested sub-records\n * (`networks.localnet` from sui, `packages.<name>` / `objects.<name>`\n * from each package). A shallow `Object.assign` would have the last\n * package's `{packages:{...}}` clobber the prior ones. Deep-merge so\n * sibling keys at every level coexist.\n *\n * Arrays and non-plain values overwrite (no element-wise merge — a\n * plugin that re-emits a bucket key owns its full value). Only plain\n * objects recurse.\n */\nconst isPlainObject = (v: unknown): v is Record<string, unknown> =>\n\ttypeof v === 'object' && v !== null && !Array.isArray(v);\n\nconst deepMerge = (\n\ttarget: Record<string, unknown>,\n\tsource: Readonly<Record<string, unknown>>,\n): void => {\n\tfor (const [key, value] of Object.entries(source)) {\n\t\tconst existing = target[key];\n\t\tif (isPlainObject(existing) && isPlainObject(value)) {\n\t\t\tdeepMerge(existing, value);\n\t\t} else {\n\t\t\t// `value` may be a decl-owned cached projection object (or array).\n\t\t\t// Storing it by reference would let a sibling decl that later\n\t\t\t// recurses into the same nested key mutate this decl's cache in\n\t\t\t// place, leaking state across emit cycles. Deep-clone any plain\n\t\t\t// object / array so the shared bucket never aliases decl state.\n\t\t\ttarget[key] = isPlainObject(value) || Array.isArray(value) ? structuredClone(value) : value;\n\t\t}\n\t}\n};\n\n/**\n * Synthesize one `AggregateFile` per non-empty bucket. The exports\n * map is keyed by the bucket's stem (e.g. `accounts.ts` → `accounts`)\n * so the rendered file exports `export const <stem> = { ... }`. The\n * orchestrator picks the export key from the bucket filename; the\n * stem itself is not a plugin identifier — it is the filename\n * without the `.ts` extension, derived mechanically. The bucket's\n * `location`/`sensitive` (from the first contributing decl) drive\n * which tree the file lands in and its mode.\n */\nconst buildAggregateFiles = (\n\tbuckets: ReadonlyMap<string, Record<string, unknown>>,\n\tmeta: ReadonlyMap<string, AggregateMeta>,\n): ReadonlyArray<AggregateFile> => {\n\tconst files: Array<AggregateFile> = [];\n\tconst sortedEntries = [...buckets.entries()].sort(([a], [b]) => a.localeCompare(b));\n\tfor (const [bucket, contents] of sortedEntries) {\n\t\tif (Object.keys(contents).length === 0) continue;\n\t\tconst stem = bucketStem(bucket);\n\t\tconst bucketMeta = meta.get(bucket) ?? {\n\t\t\tlocation: 'generated' as const,\n\t\t\tsensitive: false,\n\t\t\testablishedBy: `aggregate/${stem}`,\n\t\t};\n\t\tfiles.push({\n\t\t\temitterName: `aggregate/${stem}`,\n\t\t\toutputPath: bucket,\n\t\t\texports: { [stem]: contents },\n\t\t\tlocation: bucketMeta.location,\n\t\t\tsensitive: bucketMeta.sensitive,\n\t\t});\n\t}\n\treturn files;\n};\n\nconst bucketStem = (bucket: string): string => bucket.replace(/\\.ts$/, '').replace(/^.*\\//, '');\n\n/** The config-runtime resolver names a committed `config.ts` aggregate may\n * reference as raw expressions. Each is imported from `./config-runtime.js`\n * only when the aggregate actually calls it (oxlint flags unused imports). */\nconst CONFIG_RUNTIME_RESOLVERS = [\n\t'resolveId',\n\t'resolveNetwork',\n\t'resolveNetworks',\n\t'resolveValue',\n] as const;\ntype ConfigRuntimeResolver = (typeof CONFIG_RUNTIME_RESOLVERS)[number];\n\n/** Recursively collect which `config-runtime.ts` resolvers an exports map\n * references via raw expressions — i.e. the committed `config.ts` needs each\n * imported + the fixed `config-runtime.ts` emitted alongside it. */\nconst collectResolversInValue = (value: unknown, found: Set<ConfigRuntimeResolver>): void => {\n\tif (isRawExpr(value)) {\n\t\tfor (const name of CONFIG_RUNTIME_RESOLVERS) {\n\t\t\tif (value.expr.includes(`${name}(`)) found.add(name);\n\t\t}\n\t\treturn;\n\t}\n\tif (Array.isArray(value)) {\n\t\tfor (const v of value) collectResolversInValue(v, found);\n\t\treturn;\n\t}\n\tif (isPlainObject(value)) {\n\t\tfor (const v of Object.values(value)) collectResolversInValue(v, found);\n\t}\n};\n\n/** The ordered set of config-runtime resolvers an aggregate's exports use\n * (empty when none — the aggregate needs no resolver import). */\nconst resolversUsedBy = (exports: {\n\treadonly [key: string]: unknown;\n}): ReadonlyArray<ConfigRuntimeResolver> => {\n\tconst found = new Set<ConfigRuntimeResolver>();\n\tfor (const v of Object.values(exports)) collectResolversInValue(v, found);\n\treturn CONFIG_RUNTIME_RESOLVERS.filter((name) => found.has(name));\n};\n\n// -----------------------------------------------------------------------------\n// Service surface — registration API + emit-cycle trigger\n// -----------------------------------------------------------------------------\n\n/**\n * The codegen orchestrator's Context-bound service. The substrate's\n * supervisor calls `registerContribution(pluginKey, decl)` once per\n * `CodegenableDecl` on each plugin's `capabilities` tuple, scope-bound\n * to that plugin's acquire scope.\n *\n * Boot no longer runs codegen — the emit pipeline is the stack-free\n * `devstack codegen` verb (which calls `runEmitCycle` directly off the\n * config-derived `staticCodegen` decls). This service keeps the dispatcher\n * seam closed: every `codegenable` contribution still has a handler, so\n * plugins emit decls uniformly even though boot writes the id-config (not\n * the committed tree).\n */\nexport interface CodegenOrchestrator {\n\t/** Register a `CodegenableDecl` from a plugin. Scope-bound — when\n\t * the caller's scope (the plugin's acquire scope) closes, the\n\t * registration is reaped. */\n\treadonly registerContribution: (\n\t\tpluginKey: string,\n\t\tdecl: Codegenable,\n\t) => Effect.Effect<void, never, Scope.Scope>;\n\n\t/** Assemble the id-config from the currently-registered (live-resolved)\n\t * contributions. Boot calls this in its post-acquire hook to WRITE the\n\t * id-config file (the same `networks` / `packages` / `mvrOverrides`\n\t * data that fed `config.ts`, but as loadable JSON the Vite plugin\n\t * injects). `network` is the active network name (`ctx.identity.network`).\n\t * Pure projection over the registered decls — no I/O, no chain. */\n\treadonly assembleIdConfig: (network: string) => Effect.Effect<IdConfig, CodegenEmitFailed>;\n\n\t/** Flush ONLY the `generated-extras` contributions (the dev wallet's\n\t * `dev-wallet.ts` + the account plugin's `accounts.ts`) to the\n\t * gitignored `.devstack/stacks/<stack>/generated-extras` tree. These\n\t * are acquire-resolved (can't be statically derived by the `codegen`\n\t * verb), so boot writes them — but ONLY when the resolved network's\n\t * `devWallet` flag is on. The committed `src/generated` tree is NEVER\n\t * touched (no `generated`-located decl is emitted). Reuses the emit\n\t * renderer + aggregate logic; skips the stage-and-swap of the runtime\n\t * tree (extras live outside it). No-op (empty result) when nothing is\n\t * routed to `generated-extras`. */\n\treadonly emitExtras: () => Effect.Effect<\n\t\tRunEmitCycleResult,\n\t\tCodegenError,\n\t\tFileSystem.FileSystem | CodegenPathsService | MoveSummaryRunnerService | MoveCodegenService\n\t>;\n\n\t/** Emit the COMMITTED `src/generated` tree (config.ts + Move bindings +\n\t * coins/deepbook/… ) from the caller-supplied STATIC contributions — the\n\t * SAME id-free decls the stack-free `codegen` verb derives via each\n\t * member's `staticCodegen()` hook, NOT the live registered ones. This is\n\t * load-bearing: the live decls bake the resolved on-chain ids into\n\t * `config.ts`, but the committed tree MUST stay id-free (ids resolve at\n\t * app build time via `__DEVSTACK_IDS__`) — baking a live id breaks a fresh\n\t * clone and any OTHER stack reading the tree. Runs the canonical\n\t * {@link runEmitCycle} (per-process lock + content-addressed\n\t * stage-and-swap), so config.ts re-emits to its unchanged id-free form\n\t * (no-touch) while an edited package's Move bindings refresh. Dev-`up`\n\t * only: the post-acquire hook calls this when a Move-source edit\n\t * (re)acquires the package. `apply` / `runStack` never call it (the\n\t * committed tree is the build's input there, not its output). */\n\treadonly emitBindings: (\n\t\tcontributions: ReadonlyArray<CodegenableDecl>,\n\t) => Effect.Effect<\n\t\tRunEmitCycleResult,\n\t\tCodegenError,\n\t\tFileSystem.FileSystem | CodegenPathsService | MoveSummaryRunnerService | MoveCodegenService\n\t>;\n}\n\nexport class CodegenOrchestratorService extends Context.Service<\n\tCodegenOrchestratorService,\n\tCodegenOrchestrator\n>()('@devstack/orchestrators/Codegen') {}\n\n/**\n * Slice the deep-merged `config.ts` aggregate bucket into the loadable\n * `IdConfig` interchange shape. The bucket is the live codegen\n * accumulation (sui `networks`, per-package `packages`/`objects`/\n * `mvrOverrides`, account `accounts`); this picks the id-bearing fields\n * the Vite plugin injects. Reads are defensive — any missing slice\n * collapses to an empty record so a partial stack still writes a valid\n * (if sparse) id-config.\n */\nconst idConfigFromBucket = (\n\tbucket: Record<string, unknown>,\n\tnetwork: string,\n\tvalues: IdConfigValues,\n): IdConfig => {\n\tconst asRecord = (v: unknown): Record<string, unknown> => (isPlainObject(v) ? v : {});\n\tconst asString = (v: unknown): string | undefined => (typeof v === 'string' ? v : undefined);\n\n\tconst networks: Record<string, IdConfigNetwork> = {};\n\tfor (const [name, raw] of Object.entries(asRecord(bucket['networks']))) {\n\t\tconst entry = asRecord(raw);\n\t\tconst rpc = asString(entry['rpc']);\n\t\tif (rpc === undefined) continue;\n\t\tnetworks[name] = {\n\t\t\trpc,\n\t\t\t...(asString(entry['chainId']) !== undefined ? { chainId: asString(entry['chainId']) } : {}),\n\t\t\t...(entry['faucet'] !== undefined ? { faucet: asString(entry['faucet']) ?? null } : {}),\n\t\t\t...(entry['graphql'] !== undefined ? { graphql: asString(entry['graphql']) ?? null } : {}),\n\t\t};\n\t}\n\n\tconst packages: Record<string, IdConfigPackage> = {};\n\tfor (const [name, raw] of Object.entries(asRecord(bucket['packages']))) {\n\t\tconst entry = asRecord(raw);\n\t\t// The active-network id is `packageId` (convenience field the package\n\t\t// projection sets = `byNetwork[activeNetwork]`). An empty/missing\n\t\t// packageId maps to the UNRESOLVED_ID sentinel — an empty string would\n\t\t// slip past `isUnresolvedId` and ship as a real, resolved id.\n\t\tconst rawId = asString(entry['packageId']);\n\t\tconst id = rawId === undefined || rawId === '' ? UNRESOLVED_ID : rawId;\n\t\tconst objectsRaw = asRecord(entry['objects']);\n\t\tconst objects: Record<string, string> = {};\n\t\tfor (const [k, v] of Object.entries(objectsRaw)) {\n\t\t\tconst s = asString(v);\n\t\t\tif (s !== undefined) objects[k] = s;\n\t\t}\n\t\tpackages[name] = {\n\t\t\tid,\n\t\t\t...(Object.keys(objects).length > 0 ? { objects } : {}),\n\t\t};\n\t}\n\n\tconst accounts: Record<string, string> = {};\n\tfor (const [name, v] of Object.entries(asRecord(bucket['accounts']))) {\n\t\t// Account bindings are an object keyed by name; the injectable id is\n\t\t// the `address`. Tolerate a bare string too (a pinned known-config).\n\t\tconst address = asString(v) ?? asString(asRecord(v)['address']);\n\t\tif (address !== undefined) accounts[name] = address;\n\t}\n\n\tconst mvrOverrides: Record<string, string> = {};\n\tfor (const [mvr, v] of Object.entries(asRecord(bucket['mvrOverrides']))) {\n\t\tconst s = asString(v);\n\t\tif (s !== undefined) mvrOverrides[mvr] = s;\n\t}\n\n\t// The active `network` field MUST be a key present in `networks` — the\n\t// committed `config-runtime.ts` `resolveActiveNetwork()` does\n\t// `resolveNetworks()[network]`, and the Vite dev-wallet injection reads\n\t// `networks[network].rpc`. The `network` PARAM is the identity's network\n\t// name (e.g. `\"testnet-fork\"`), but the sui binding keys the `networks`\n\t// map by what it emitted (`\"localnet\"` for every mode). So PREFER the\n\t// network the binding stamped into the bucket (`bucket['network']`, which\n\t// matches the `networks` key); fall back to the param, then — if neither\n\t// is a known key but exactly one network exists — that sole key. This\n\t// keeps `network` in agreement with `networks` so resolution never\n\t// dereferences `undefined`.\n\tconst networkKeys = Object.keys(networks);\n\tconst bucketNetwork = asString(bucket['network']);\n\tconst activeNetwork =\n\t\tbucketNetwork !== undefined && networkKeys.includes(bucketNetwork)\n\t\t\t? bucketNetwork\n\t\t\t: networkKeys.includes(network)\n\t\t\t\t? network\n\t\t\t\t: networkKeys.length === 1\n\t\t\t\t\t? networkKeys[0]!\n\t\t\t\t\t: (bucketNetwork ?? network);\n\n\treturn {\n\t\tnetwork: activeNetwork,\n\t\tnetworks,\n\t\tpackages,\n\t\taccounts,\n\t\tmvrOverrides,\n\t\t...(Object.keys(values).length > 0 ? { values } : {}),\n\t};\n};\n\ninterface RegisteredCodegenEntry {\n\treadonly pluginKey: string;\n\treadonly decl: Codegenable;\n\treadonly seq: number;\n}\n\n/** Layer-wired codegen orchestrator. No upstream requirements at boot\n * time — the per-cycle effect carries the `FileSystem` + Move-codegen\n * service requirements through to the caller. */\nexport const layerCodegenOrchestrator: Layer.Layer<CodegenOrchestratorService> = Layer.effect(\n\tCodegenOrchestratorService,\n\tEffect.gen(function* () {\n\t\tconst contributionsRef = yield* Ref.make<ReadonlyArray<RegisteredCodegenEntry>>([]);\n\t\tconst seqRef = yield* Ref.make(0);\n\n\t\tconst registerContribution: CodegenOrchestrator['registerContribution'] = (pluginKey, decl) =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst seq = yield* Ref.updateAndGet(seqRef, (n) => n + 1);\n\t\t\t\tconst entry: RegisteredCodegenEntry = { pluginKey, decl, seq };\n\t\t\t\tyield* Ref.update(contributionsRef, (xs) => [...xs, entry]);\n\t\t\t\tyield* Effect.addFinalizer(() =>\n\t\t\t\t\tRef.update(contributionsRef, (xs) => xs.filter((e) => e.seq !== seq)),\n\t\t\t\t);\n\t\t\t}) as Effect.Effect<void, never, Scope.Scope>;\n\n\t\tconst assembleIdConfig: CodegenOrchestrator['assembleIdConfig'] = (network) =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst registered = (yield* Ref.get(contributionsRef)).map((e) => e.decl);\n\t\t\t\t// Deep-merge every contribution's `config.ts` aggregate\n\t\t\t\t// projection into ONE bucket — the SAME accumulation the live\n\t\t\t\t// codegen cycle performed (sui's `networks.<net>` + each\n\t\t\t\t// package's `packages.<name>` / `objects` / `mvrOverrides`,\n\t\t\t\t// the account plugin's `accounts`). The merged bucket carries\n\t\t\t\t// real (live-`acquire`-resolved) ids; we then slice it into the\n\t\t\t\t// loadable `IdConfig` shape.\n\t\t\t\tconst bucket: Record<string, unknown> = {};\n\t\t\t\t// `accounts.ts` is a SEPARATE aggregate bucket (the account\n\t\t\t\t// plugin routes it to `generated-extras`); fold its projection\n\t\t\t\t// under an `accounts` key so the id-config carries account\n\t\t\t\t// addresses alongside the `config.ts`-derived ids.\n\t\t\t\tconst accounts: Record<string, unknown> = {};\n\t\t\t\t// The generic resolver channel — `values[namespace][key]` —\n\t\t\t\t// accumulated from any LIVE config-binding aggregate that\n\t\t\t\t// declared `idConfigValues` (the plugin live JSON the typed\n\t\t\t\t// fields can't carry). Deep-merged so sibling namespaces /\n\t\t\t\t// keys from distinct plugins coexist.\n\t\t\t\tconst values: Record<string, unknown> = {};\n\t\t\t\tfor (const decl of registered) {\n\t\t\t\t\tif (decl.aggregate === undefined) continue;\n\t\t\t\t\t// The generic `values` channel is BUCKET-BLIND: any LIVE\n\t\t\t\t\t// config-binding aggregate (config.ts, coins.ts, deepbook.ts,\n\t\t\t\t\t// walrus.ts, seal.ts, ...) may declare `idConfigValues`. Fold\n\t\t\t\t\t// every contributor's so the committed-tree `resolveValue` calls\n\t\t\t\t\t// those buckets emit resolve at app build/dev time -- not just the\n\t\t\t\t\t// `config.ts` plugins'. Slicing the TYPED id-config fields\n\t\t\t\t\t// (`networks` / `packages` / `mvrOverrides`) stays scoped to the\n\t\t\t\t\t// `config.ts` bucket below.\n\t\t\t\t\tif (decl.aggregate.idConfigValues !== undefined) {\n\t\t\t\t\t\tdeepMerge(values, decl.aggregate.idConfigValues);\n\t\t\t\t\t}\n\t\t\t\t\tif (decl.aggregate.bucket === 'config.ts') {\n\t\t\t\t\t\tconst emission = yield* runEmitter(decl);\n\t\t\t\t\t\tconst projected = decl.aggregate.project(emission.exports);\n\t\t\t\t\t\tif (projected !== null) deepMerge(bucket, projected);\n\t\t\t\t\t} else if (decl.aggregate.bucket === 'accounts.ts') {\n\t\t\t\t\t\tconst emission = yield* runEmitter(decl);\n\t\t\t\t\t\tconst projected = decl.aggregate.project(emission.exports);\n\t\t\t\t\t\tif (projected !== null) deepMerge(accounts, projected);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbucket['accounts'] = accounts;\n\t\t\t\treturn idConfigFromBucket(bucket, network, values as IdConfigValues);\n\t\t\t});\n\n\t\tconst emitExtras: CodegenOrchestrator['emitExtras'] = () =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst registered = (yield* Ref.get(contributionsRef)).map((e) => e.decl);\n\t\t\t\tconst extras = registered.filter(isExtrasDecl);\n\t\t\t\tif (extras.length === 0) {\n\t\t\t\t\t// Nothing routed to the dev tree (no wallet/accounts mounted).\n\t\t\t\t\treturn { filesWritten: [], filesUnchanged: [], filesChmod: [], bindings: null };\n\t\t\t\t}\n\t\t\t\t// Validate the extras-only set up front (mirrors `runEmitCycle`'s\n\t\t\t\t// pre-flight), then emit DIRECTLY against the real paths — no\n\t\t\t\t// stage-and-swap, since the extras tree lives outside the runtime\n\t\t\t\t// `outputDir`. Every `generated-extras` decl routes through\n\t\t\t\t// `paths.resolveExtras`; no `generated`-located decl is present,\n\t\t\t\t// so the committed `src/generated` tree is untouched.\n\t\t\t\tyield* validateUniqueness(extras);\n\t\t\t\tyield* validateAggregatePathAvailability(extras);\n\t\t\t\tconst paths = yield* CodegenPathsService;\n\t\t\t\t// Acquire the dedicated `codegenLockFile` for symmetry with\n\t\t\t\t// `runEmitCycle` — the extras emit writes into the shared stack\n\t\t\t\t// tree (`generated-extras`) and a concurrent codegen cycle could\n\t\t\t\t// otherwise interleave. Scoped so the lock releases when the emit\n\t\t\t\t// completes (or fails).\n\t\t\t\treturn yield* Effect.scoped(\n\t\t\t\t\tEffect.gen(function* () {\n\t\t\t\t\t\tyield* acquireStackLock(paths.codegenLockFile, CODEGEN_CYCLE_LOCK_TIMEOUT_MS).pipe(\n\t\t\t\t\t\t\tEffect.mapError(\n\t\t\t\t\t\t\t\t(cause) =>\n\t\t\t\t\t\t\t\t\tnew CodegenWriteFailed({\n\t\t\t\t\t\t\t\t\t\toutputPath: paths.codegenLockFile,\n\t\t\t\t\t\t\t\t\t\tstage: 'write',\n\t\t\t\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\t\treturn yield* runEmitCycleInner({ contributions: extras, trackTree: false }, paths);\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t});\n\n\t\tconst emitBindings: CodegenOrchestrator['emitBindings'] = (contributions) =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\t// Emit from the caller's STATIC (id-free) contributions, NOT the\n\t\t\t\t// live registered ones — the latter bake real on-chain ids into\n\t\t\t\t// `config.ts`, breaking the committed tree's id-free invariant.\n\t\t\t\t// `runEmitCycle` owns the lock + content-addressed stage-and-swap,\n\t\t\t\t// so config.ts re-emits to its unchanged id-free form (no-touch)\n\t\t\t\t// while an edited package's Move bindings refresh (HMR stays quiet).\n\t\t\t\tif (contributions.length === 0) {\n\t\t\t\t\treturn { filesWritten: [], filesUnchanged: [], filesChmod: [], bindings: null };\n\t\t\t\t}\n\t\t\t\treturn yield* runEmitCycle({ contributions, trackTree: true });\n\t\t\t});\n\n\t\treturn CodegenOrchestratorService.of({\n\t\t\tregisterContribution,\n\t\t\tassembleIdConfig,\n\t\t\temitExtras,\n\t\t\temitBindings,\n\t\t});\n\t}),\n);\n"],"mappings":"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"}
|
|
@@ -89,6 +89,17 @@ const narrowEnum = (raw, allowed, field, faults, fallback) => {
|
|
|
89
89
|
faults.push(raw === void 0 || raw === null ? `${field}: missing (expected one of ${allowed.join("|")})` : `${field}: '${String(raw)}' not in ${allowed.join("|")}`);
|
|
90
90
|
return fallback;
|
|
91
91
|
};
|
|
92
|
+
/** Required string-or-numeric coerced to a string. Pyth feed prices arrive as a
|
|
93
|
+
* `bigint` on the resolved value (string in the cached/known shape); accept any
|
|
94
|
+
* of string/number/bigint and record a fault otherwise. Returns '0' on
|
|
95
|
+
* mismatch so the panel still renders a (degraded) cell. */
|
|
96
|
+
const reqNumericStr = (v, field, faults) => {
|
|
97
|
+
if (typeof v === "string") return v;
|
|
98
|
+
if (typeof v === "bigint") return v.toString();
|
|
99
|
+
if (typeof v === "number" && Number.isFinite(v)) return String(v);
|
|
100
|
+
faults.push(typeFault(field, "string|number|bigint", v));
|
|
101
|
+
return "0";
|
|
102
|
+
};
|
|
92
103
|
const optStr = (v) => typeof v === "string" ? v : null;
|
|
93
104
|
const optNum = (v) => typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
94
105
|
const bool = (v) => v === true;
|
|
@@ -132,7 +143,13 @@ const buildDashboardDomain = (deps) => {
|
|
|
132
143
|
baseCoinType: reqStr(p.baseCoinType, `deepbook.pools[${i}].baseCoinType`, faults),
|
|
133
144
|
quoteCoinType: reqStr(p.quoteCoinType, `deepbook.pools[${i}].quoteCoinType`, faults)
|
|
134
145
|
})),
|
|
135
|
-
|
|
146
|
+
pythFeeds: (v.pyth?.feeds ?? []).map((f, i) => ({
|
|
147
|
+
symbol: reqStr(f.symbol, `deepbook.pyth.feeds[${i}].symbol`, faults),
|
|
148
|
+
priceInfoObjectId: reqStr(f.priceInfoObjectId, `deepbook.pyth.feeds[${i}].priceInfoObjectId`, faults),
|
|
149
|
+
price: reqNumericStr(f.price, `deepbook.pyth.feeds[${i}].price`, faults),
|
|
150
|
+
expo: reqNum(f.expo, `deepbook.pyth.feeds[${i}].expo`, faults)
|
|
151
|
+
})),
|
|
152
|
+
hasSeedLiquidity: bool(v.hasSeedLiquidity),
|
|
136
153
|
serverUrl: optStr(v.serverUrl),
|
|
137
154
|
indexerUrl: optStr(v.indexerUrl),
|
|
138
155
|
narrowingFault: faultDetail(faults)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"domain.mjs","names":[],"sources":["../../../src/plugins/dashboard/domain.ts"],"sourcesContent":["// Dashboard plugin-domain shaping.\n//\n// The substrate control-plane is GENERIC + name-blind: it hands out the\n// snapshot catalog, the observability rings, and a single uninterpreted\n// `resolvedValues` accessor (see `substrate/runtime/control-plane/`). This\n// module — which lives in the PLUGIN layer and is allowed to name plugins —\n// owns ALL plugin-name-aware shaping: it matches resolved plugin values by\n// resource id (`deepbook/`-prefix, `seal:`-prefix, `coin:`-prefix, `id ===\n// 'sui'`) and projects them into the app-agnostic shapes the GraphQL schema\n// renders. It also owns the `mode` derivation and the coin `mint` action.\n//\n// Design discipline (mirrors the old substrate seam, one layer up):\n// - We import NO plugin types — we narrow the opaque `unknown` resolved\n// value through shallow structural shapes that mirror the relevant\n// fields. A field missing on the live value collapses to the\n// null/empty default.\n// - Every accessor degrades to empty/`null` rather than failing, so a\n// single missing/uninitialised plugin can't take down a dashboard\n// query (`E = never` on the public surface).\n// - We match plugins by RESOURCE ID — a prefix for the multi-instance\n// kinds (`deepbook/`, `seal:`, `coin:`) and an exact id for the\n// singletons (`id === 'sui'`) — rather than\n// plugin-key substrings: the resource id is the stable identity the\n// plugin factories mint.\n\nimport { Effect } from 'effect';\n\nimport type {\n\tControlPlaneDomain,\n\tControlPlaneResolvedValue,\n} from '../../substrate/runtime/control-plane/service.ts';\nimport type { StrategyRegistry } from '../../contracts/strategy-contributor.ts';\nimport type { FaucetStrategy } from '../../contracts/faucet-strategy.ts';\nimport type { AccountFundingStrategy } from '../../contracts/funding-strategy.ts';\nimport type { AccountValue } from '../account/index.ts';\n// The faucet capability-key prefix is owned by the faucet plugin (single\n// source of truth). The dashboard plugin layer is allowed to name plugins,\n// so importing the key constructor is the same cross-plugin reference the\n// account funding pass (`plugins/account/funding.ts`) uses — NOT a resolved\n// plugin-VALUE import (those stay structurally narrowed below). Imported via\n// the sibling's barrel to satisfy the plugin-boundary invariant.\nimport { FAUCET_CAPABILITY_KEY_PREFIX } from '../faucet/index.ts';\n\n// -----------------------------------------------------------------------------\n// App-agnostic domain shapes the GraphQL schema renders.\n// -----------------------------------------------------------------------------\n\n/** One DeepBook pool object id + coin types (prices are chain-direct). */\nexport interface DashboardDeepbookPool {\n\treadonly name: string;\n\treadonly poolId: string;\n\treadonly baseCoinType: string;\n\treadonly quoteCoinType: string;\n}\n\nexport interface DashboardDeepbookInfo {\n\treadonly pluginKey: string;\n\treadonly name: string;\n\treadonly mode: 'local' | 'override' | 'known';\n\treadonly network: string;\n\treadonly packageId: string;\n\treadonly registryId: string;\n\treadonly adminCapId: string | null;\n\treadonly deepTreasuryId: string | null;\n\treadonly pools: ReadonlyArray<DashboardDeepbookPool>;\n\treadonly marketMakerRunning: boolean;\n\treadonly serverUrl: string | null;\n\treadonly indexerUrl: string | null;\n\t/** Non-null when one or more required fields failed to narrow off the\n\t * opaque resolved value (fail-loud: schema drift surfaced to the caller\n\t * instead of silently degrading). */\n\treadonly narrowingFault: string | null;\n}\n\nexport interface DashboardSealKeyServer {\n\treadonly objectId: string;\n\treadonly weight: number;\n}\n\nexport interface DashboardSealInfo {\n\treadonly pluginKey: string;\n\treadonly mode: 'local-keygen' | 'live' | 'fork-known';\n\treadonly objectId: string;\n\treadonly keyServerUrl: string;\n\treadonly keyServers: ReadonlyArray<DashboardSealKeyServer>;\n\t/** Threshold = number of registered key-server configs. */\n\treadonly threshold: number;\n\t/** Non-null when one or more required fields failed to narrow off the\n\t * opaque resolved value (fail-loud). */\n\treadonly narrowingFault: string | null;\n}\n\n/** A coin's treasury-cap id (drives the Mint action) + addressing facts. */\nexport interface DashboardCoinCap {\n\treadonly pluginKey: string;\n\treadonly symbol: string | null;\n\treadonly fullCoinType: string;\n\treadonly decimals: number;\n\treadonly source: 'registry' | 'on-chain' | 'builtin';\n\treadonly treasuryCapId: string | null;\n\treadonly packageId: string | null;\n\t/** Non-null when one or more required fields failed to narrow off the\n\t * opaque resolved value (fail-loud). */\n\treadonly narrowingFault: string | null;\n}\n\n/** Input for the dashboard mint ACTION. `amountBaseUnits` is the raw\n * integer amount in the coin's smallest unit (decimals already applied\n * by the caller / form) — a string so large u64 values survive the wire\n * without precision loss. */\nexport interface DashboardMintInput {\n\treadonly coinType: string;\n\treadonly recipient: string;\n\treadonly amountBaseUnits: string;\n}\n\n/** Outcome of a dashboard mint ACTION. Mirrors the snapshot\n * restore/delete result shape plus the on-chain tx `digest` on success. */\nexport interface DashboardMintResult {\n\treadonly ok: boolean;\n\treadonly detail: string;\n\treadonly digest: string | null;\n}\n\n/** Input for the dashboard fund ACTION.\n *\n * `coinType` selects the funding strategy: absent or the canonical SUI\n * type routes through the chain's faucet strategy (`faucet:request:<chainId>`,\n * fixed-amount); any other full coin type routes through the coin-specific\n * `coinType:<fullCoinType>` strategy (WAL exchange swap / DEEP pool swap).\n *\n * `amountBaseUnits` is the raw integer amount in the coin's smallest unit\n * (a string so large u64 values survive the wire without precision loss).\n * The SUI faucet is fixed-amount and IGNORES it; WAL/DEEP honor it. */\nexport interface DashboardFundInput {\n\treadonly recipient: string;\n\treadonly coinType?: string | null;\n\treadonly amountBaseUnits?: string | null;\n}\n\n/** Outcome of a dashboard fund ACTION. The in-process funding strategies\n * return `void` (not a digest), so the result carries only `ok`/`detail`.\n * `ok:true` means the strategy's `request(...)` completed (the faucet POST\n * landed / the swap executed on-chain); `detail` carries the reason on\n * failure. */\nexport interface DashboardFundResult {\n\treadonly ok: boolean;\n\treadonly detail: string;\n}\n\n/** One coin the dashboard faucet can actually fund, with whether the\n * underlying strategy honors a caller-supplied amount.\n *\n * SUI is always present (fixed-amount faucet, `honorsAmount: false`).\n * WAL/DEEP appear only when their plugin registered a `coinType:<X>`\n * funding strategy AND (for WAL) carries a swap that spends the\n * recipient account's SUI — so `honorsAmount: true`. */\nexport interface DashboardFundableCoin {\n\treadonly symbol: string;\n\treadonly coinType: string;\n\treadonly honorsAmount: boolean;\n\t/** True when funding requires the recipient to BE a resolved account in\n\t * the stack (the swap spends that account's own SUI via its in-process\n\t * signer). SUI is `false` (any 0x address); WAL/DEEP are `true`. */\n\treadonly requiresAccountSigner: boolean;\n}\n\n/** The dashboard plugin-domain accessor surface. Each member is a\n * self-contained Effect that never fails (`E = never`); they degrade to\n * empty/`null` so a single missing plugin can't take down the dashboard\n * query. */\nexport interface DashboardDomain {\n\t/** Fork-vs-local mode, derived from the resolved sui plugin's chain\n\t * identity. Used for advance-clock gating. `null` when no sui plugin\n\t * is present. */\n\treadonly mode: Effect.Effect<'fork' | 'local' | 'live' | null>;\n\t/** DeepBook deployments (registry/admin/pool ids + MM state). */\n\treadonly deepbook: Effect.Effect<ReadonlyArray<DashboardDeepbookInfo>>;\n\t/** Seal key-server deployments (objectId/threshold/mode). */\n\treadonly seal: Effect.Effect<ReadonlyArray<DashboardSealInfo>>;\n\t/** Coin treasury caps (drives Mint). */\n\treadonly coinCaps: Effect.Effect<ReadonlyArray<DashboardCoinCap>>;\n\t/** Mint ACTION — mints `amountBaseUnits` of `coinType` to `recipient`,\n\t * signed in-process by the treasury-cap-owning publisher signer the\n\t * resolved coin value's self-contained `mintFromCap` closure holds. */\n\treadonly mintCoin: (input: DashboardMintInput) => Effect.Effect<DashboardMintResult>;\n\t/** Coins the faucet can actually fund right now (SUI always; WAL/DEEP\n\t * only when their plugin registered a funding strategy). Drives the\n\t * faucet panel's coin pills + amount-field gating. */\n\treadonly fundableCoins: Effect.Effect<ReadonlyArray<DashboardFundableCoin>>;\n\t/** Fund ACTION — reuses devstack's in-process funding strategies. SUI\n\t * (absent/canonical type) routes through the chain faucet strategy\n\t * (fixed-amount); WAL/DEEP route through the coin-specific funding\n\t * strategy (account-signed exchange/pool swap). Real result — `ok`\n\t * reflects whether the strategy's `request(...)` completed. */\n\treadonly fundAccount: (input: DashboardFundInput) => Effect.Effect<DashboardFundResult>;\n}\n\n// -----------------------------------------------------------------------------\n// Structural projections of plugin-resolved values\n//\n// We narrow the opaque `unknown` resolved value through shallow structural\n// shapes that mirror the relevant fields. A field missing on the live value\n// collapses to the null/empty default.\n// -----------------------------------------------------------------------------\n\ninterface DeepbookShape {\n\treadonly mode?: unknown;\n\treadonly network?: unknown;\n\treadonly packageId?: unknown;\n\treadonly registryId?: unknown;\n\treadonly adminCapId?: unknown;\n\treadonly deepTreasuryId?: unknown;\n\treadonly pools?: ReadonlyArray<{\n\t\treadonly name?: unknown;\n\t\treadonly poolId?: unknown;\n\t\treadonly baseCoinType?: unknown;\n\t\treadonly quoteCoinType?: unknown;\n\t}>;\n\treadonly marketMakerRunning?: unknown;\n\treadonly serverUrl?: unknown;\n\treadonly indexerUrl?: unknown;\n}\n\ninterface SealShape {\n\treadonly mode?: unknown;\n\treadonly objectId?: unknown;\n\treadonly keyServerUrl?: unknown;\n\treadonly serverConfigs?: ReadonlyArray<{\n\t\treadonly objectId?: unknown;\n\t\treadonly weight?: unknown;\n\t}>;\n}\n\ninterface CoinShape {\n\treadonly symbol?: unknown;\n\treadonly fullCoinType?: unknown;\n\treadonly decimals?: unknown;\n\treadonly source?: unknown;\n\treadonly treasuryCapId?: unknown;\n\treadonly packageId?: unknown;\n\t/** Self-contained mint closure on the resolved coin value (present\n\t * for witness-form coins whose publisher still owns the cap). Read\n\t * structurally — we import no coin types. Returns an Effect that\n\t * resolves a `{ digest }`-bearing result or fails with a\n\t * coin/artifact-publisher tagged error. */\n\treadonly mintFromCap?: (opts: {\n\t\treadonly to: string;\n\t\treadonly amount: bigint;\n\t}) => Effect.Effect<{ readonly digest: string }, { readonly message?: unknown }>;\n}\n\ninterface SuiShape {\n\treadonly mode?: unknown;\n\t/** Genesis-digest chain id (`'sui:localnet'`, `'sui:testnet'`,\n\t * `'sui:mainnet-fork@123'`). Composes the faucet capability key\n\t * (`faucet:request:<chainId>`) the SUI funding path resolves. */\n\treadonly chainId?: unknown;\n}\n\n/** Structural shape of the resolved account value (`account/<name>`). The\n * WAL/DEEP funding strategies need this handle as `req.account` — the swap\n * spends the recipient account's own SUI through its in-process signer.\n * Narrowed to the fields the strategy dispatch reads; the full\n * `AccountValue` is assignable to it, so we cast the matched value across\n * the seam without importing the account VALUE shape structurally\n * elsewhere. */\ninterface AccountShape {\n\treadonly address?: unknown;\n\treadonly name?: unknown;\n}\n\n/** A 0x-prefixed Sui address: `0x` + 1..64 hex digits. Mirrors the\n * address validation the mint PTB's `tx.pure.address` ultimately\n * enforces, surfaced up front so the dashboard gets a clean rejection\n * rather than an opaque build failure. */\nconst SUI_ADDRESS_RE = /^0x[0-9a-fA-F]{1,64}$/;\n\n/** The canonical builtin SUI coin type — selects the SUI faucet funding\n * path. Mirrors `SUI_FULL_COIN_TYPE` in `plugins/account/funding.ts`\n * (inlined here so the dashboard plugin does not cross-import the account\n * funding module just for the string). */\nconst SUI_FULL_COIN_TYPE = '0x2::sui::SUI' as const;\n\n/** A positive integer base-unit amount string (no sign, no decimal\n * point, no leading zeros beyond a bare `0` — which is itself rejected\n * as non-positive). */\nconst isPositiveIntegerString = (s: string): boolean => {\n\tif (!/^\\d+$/.test(s)) return false;\n\ttry {\n\t\treturn BigInt(s) > 0n;\n\t} catch {\n\t\treturn false;\n\t}\n};\n\n/** Compose the chain faucet capability key (`faucet:request:<chainId>`)\n * the SUI funding path resolves. Uses the faucet plugin's prefix constant\n * (single source of truth) — same key the boot-time funding pass builds. */\nconst faucetCapabilityKeyFor = (\n\tchain: string,\n): `${typeof FAUCET_CAPABILITY_KEY_PREFIX}:${string}` => `${FAUCET_CAPABILITY_KEY_PREFIX}:${chain}`;\n\n/** Parse a base-unit amount string into a positive bigint, or `null` when\n * absent / non-positive / non-integer. */\nconst parseAmount = (s: string | null | undefined): bigint | null => {\n\tif (s == null) return null;\n\tconst trimmed = s.trim();\n\tif (!isPositiveIntegerString(trimmed)) return null;\n\treturn BigInt(trimmed);\n};\n\n/** Derive a display symbol from a full coin type's module path\n * (`0x…::wal::WAL` → `WAL`, `0x…::deep::DEEP` → `DEEP`). Falls back to the\n * struct name, then the raw type. Keeps the dashboard name-blind on the\n * resolved coin VALUE while still labeling the faucet pill. */\nconst coinSymbolFromType = (coinType: string): string => {\n\tconst parts = coinType.split('::');\n\tconst struct = parts[parts.length - 1];\n\treturn struct !== undefined && struct.length > 0 ? struct : coinType;\n};\n\n/** Extract a human detail from a faucet/funding strategy failure. The\n * strategy error channels are tagged structs carrying `message`/`reason`;\n * read them structurally without importing each plugin's error type. */\nconst causeDetail = (cause: unknown): string => {\n\tif (cause !== null && typeof cause === 'object') {\n\t\tconst c = cause as {\n\t\t\treadonly message?: unknown;\n\t\t\treadonly reason?: unknown;\n\t\t\treadonly _tag?: unknown;\n\t\t};\n\t\tif (typeof c.message === 'string' && c.message.length > 0) return c.message;\n\t\tif (typeof c.reason === 'string' && c.reason.length > 0) return c.reason;\n\t\tif (typeof c._tag === 'string' && c._tag.length > 0) return c._tag;\n\t}\n\treturn String(cause);\n};\nconst faucetCauseDetail = causeDetail;\nconst fundingCauseDetail = causeDetail;\n\n// -----------------------------------------------------------------------------\n// Fail-aware structural narrowing\n//\n// The substrate hands us opaque `unknown` resolved values. We still narrow\n// through shallow structural shapes, but instead of SILENTLY degrading a\n// missing/wrong-typed required field to ''/0 (which hid integration-seam\n// drift), the `req*` readers ACCUMULATE a typed narrowing fault into a\n// per-shaping-call collector. The fault is threaded into the GraphQL-visible\n// result (`narrowingFault` / `detail`) so consumers SEE the drift — WITHOUT\n// ever throwing (the dashboard's `E = never` surface semantic is preserved).\n// `opt*` readers stay quiet: they are for genuinely-optional fields.\n// -----------------------------------------------------------------------------\n\n/** Mutable per-call sink for structural-narrowing faults. */\ntype FaultSink = Array<string>;\n\nconst newFaults = (): FaultSink => [];\n\n/** Collapse accumulated faults into a single nullable detail string for the\n * GraphQL-visible result. `null` when nothing drifted. */\nconst faultDetail = (faults: FaultSink): string | null =>\n\tfaults.length === 0 ? null : faults.join('; ');\n\n/** Describe why a value failed to narrow to the expected type — distinguishing\n * 'absent' from 'present but wrong type'. */\nconst typeFault = (field: string, expected: string, v: unknown): string => {\n\tif (v === undefined || v === null) return `${field}: missing (expected ${expected})`;\n\treturn `${field}: expected ${expected}, got ${typeof v}`;\n};\n\n/** Required string. On mismatch records a fault and returns '' so the panel\n * still renders a (degraded) cell. */\nconst reqStr = (v: unknown, field: string, faults: FaultSink): string => {\n\tif (typeof v === 'string') return v;\n\tfaults.push(typeFault(field, 'string', v));\n\treturn '';\n};\n\n/** Required finite number. On mismatch records a fault and returns the safe\n * display fallback (`fallback`, default 0). */\nconst reqNum = (v: unknown, field: string, faults: FaultSink, fallback = 0): number => {\n\tif (typeof v === 'number' && Number.isFinite(v)) return v;\n\tfaults.push(typeFault(field, 'number', v));\n\treturn fallback;\n};\n\n/** Allow-list enum narrow. Records a fault for an out-of-enum value while\n * still returning a safe display fallback so the panel renders. */\nconst narrowEnum = <T extends string>(\n\traw: unknown,\n\tallowed: ReadonlyArray<T>,\n\tfield: string,\n\tfaults: FaultSink,\n\tfallback: T,\n): T => {\n\tif (typeof raw === 'string' && (allowed as ReadonlyArray<string>).includes(raw)) return raw as T;\n\tfaults.push(\n\t\traw === undefined || raw === null\n\t\t\t? `${field}: missing (expected one of ${allowed.join('|')})`\n\t\t\t: `${field}: '${String(raw)}' not in ${allowed.join('|')}`,\n\t);\n\treturn fallback;\n};\n\n// Genuinely-optional readers — silent by design (absent is a valid state).\nconst optStr = (v: unknown): string | null => (typeof v === 'string' ? v : null);\nconst optNum = (v: unknown): number | null =>\n\ttypeof v === 'number' && Number.isFinite(v) ? v : null;\nconst bool = (v: unknown): boolean => v === true;\n\n/** Filter the generic resolved values down to those whose resource id\n * matches a predicate, in graph order. Returns the `{ pluginKey, value }`\n * the shaping functions consume. */\nconst matching = (\n\tvalues: ReadonlyArray<ControlPlaneResolvedValue>,\n\tmatches: (resourceId: string) => boolean,\n): ReadonlyArray<ControlPlaneResolvedValue> => values.filter((v) => matches(v.id));\n\n// -----------------------------------------------------------------------------\n// Builder\n// -----------------------------------------------------------------------------\n\nexport interface DashboardDomainDeps {\n\t/** The generic, name-blind control-plane domain (resolved values). */\n\treadonly control: ControlPlaneDomain;\n\t/** The scope-local strategy registry — the SAME registry the boot-time\n\t * account funding pass dispatches through (`plugins/account/funding.ts`).\n\t * Drives `fundableCoins` + `fundAccount`: SUI via `faucet:request:<chainId>`,\n\t * WAL/DEEP via `coinType:<fullCoinType>`. `null` in bare test paths; the\n\t * fund accessors degrade to unavailable then. */\n\treadonly strategyRegistry: StrategyRegistry | null;\n}\n\nexport const buildDashboardDomain = (deps: DashboardDomainDeps): DashboardDomain => {\n\tconst { control, strategyRegistry } = deps;\n\n\tconst mode: DashboardDomain['mode'] = control.resolvedValues.pipe(\n\t\tEffect.map((values) => {\n\t\t\tconst sui = matching(values, (id) => id === 'sui')[0];\n\t\t\tif (sui === undefined) return null;\n\t\t\tconst m = (sui.value as SuiShape).mode;\n\t\t\tswitch (m) {\n\t\t\t\tcase 'fork':\n\t\t\t\t\treturn 'fork';\n\t\t\t\tcase 'live':\n\t\t\t\t\treturn 'live';\n\t\t\t\tcase 'local':\n\t\t\t\tcase 'local-rpc':\n\t\t\t\t\treturn 'local';\n\t\t\t\tdefault:\n\t\t\t\t\treturn null;\n\t\t\t}\n\t\t}),\n\t);\n\n\tconst deepbook: DashboardDomain['deepbook'] = control.resolvedValues.pipe(\n\t\tEffect.map((values) =>\n\t\t\tmatching(values, (id) => id.startsWith('deepbook/')).map(\n\t\t\t\t({ pluginKey, value }): DashboardDeepbookInfo => {\n\t\t\t\t\tconst v = value as DeepbookShape;\n\t\t\t\t\tconst faults = newFaults();\n\t\t\t\t\tconst dbMode = narrowEnum(\n\t\t\t\t\t\tv.mode,\n\t\t\t\t\t\t['local', 'override', 'known'] as const,\n\t\t\t\t\t\t'deepbook.mode',\n\t\t\t\t\t\tfaults,\n\t\t\t\t\t\t'local',\n\t\t\t\t\t);\n\t\t\t\t\treturn {\n\t\t\t\t\t\tpluginKey,\n\t\t\t\t\t\tname: pluginKey.replace(/^deepbook:/, ''),\n\t\t\t\t\t\tmode: dbMode,\n\t\t\t\t\t\tnetwork: reqStr(v.network, 'deepbook.network', faults),\n\t\t\t\t\t\tpackageId: reqStr(v.packageId, 'deepbook.packageId', faults),\n\t\t\t\t\t\tregistryId: reqStr(v.registryId, 'deepbook.registryId', faults),\n\t\t\t\t\t\tadminCapId: optStr(v.adminCapId),\n\t\t\t\t\t\tdeepTreasuryId: optStr(v.deepTreasuryId),\n\t\t\t\t\t\tpools: (v.pools ?? []).map((p, i) => ({\n\t\t\t\t\t\t\tname: reqStr(p.name, `deepbook.pools[${i}].name`, faults),\n\t\t\t\t\t\t\tpoolId: reqStr(p.poolId, `deepbook.pools[${i}].poolId`, faults),\n\t\t\t\t\t\t\tbaseCoinType: reqStr(p.baseCoinType, `deepbook.pools[${i}].baseCoinType`, faults),\n\t\t\t\t\t\t\tquoteCoinType: reqStr(p.quoteCoinType, `deepbook.pools[${i}].quoteCoinType`, faults),\n\t\t\t\t\t\t})),\n\t\t\t\t\t\tmarketMakerRunning: bool(v.marketMakerRunning),\n\t\t\t\t\t\tserverUrl: optStr(v.serverUrl),\n\t\t\t\t\t\tindexerUrl: optStr(v.indexerUrl),\n\t\t\t\t\t\tnarrowingFault: faultDetail(faults),\n\t\t\t\t\t};\n\t\t\t\t},\n\t\t\t),\n\t\t),\n\t);\n\n\tconst seal: DashboardDomain['seal'] = control.resolvedValues.pipe(\n\t\tEffect.map((values) =>\n\t\t\tmatching(values, (id) => id.startsWith('seal:')).map(\n\t\t\t\t({ pluginKey, value }): DashboardSealInfo => {\n\t\t\t\t\tconst v = value as SealShape;\n\t\t\t\t\tconst faults = newFaults();\n\t\t\t\t\tconst sealMode = narrowEnum(\n\t\t\t\t\t\tv.mode,\n\t\t\t\t\t\t['local-keygen', 'live', 'fork-known'] as const,\n\t\t\t\t\t\t'seal.mode',\n\t\t\t\t\t\tfaults,\n\t\t\t\t\t\t'local-keygen',\n\t\t\t\t\t);\n\t\t\t\t\tconst keyServers = (v.serverConfigs ?? []).map((c, i) => ({\n\t\t\t\t\t\tobjectId: reqStr(c.objectId, `seal.serverConfigs[${i}].objectId`, faults),\n\t\t\t\t\t\tweight: optNum(c.weight) ?? 1,\n\t\t\t\t\t}));\n\t\t\t\t\treturn {\n\t\t\t\t\t\tpluginKey,\n\t\t\t\t\t\tmode: sealMode,\n\t\t\t\t\t\tobjectId: reqStr(v.objectId, 'seal.objectId', faults),\n\t\t\t\t\t\tkeyServerUrl: reqStr(v.keyServerUrl, 'seal.keyServerUrl', faults),\n\t\t\t\t\t\tkeyServers,\n\t\t\t\t\t\tthreshold: keyServers.length,\n\t\t\t\t\t\tnarrowingFault: faultDetail(faults),\n\t\t\t\t\t};\n\t\t\t\t},\n\t\t\t),\n\t\t),\n\t);\n\n\tconst coinCaps: DashboardDomain['coinCaps'] = control.resolvedValues.pipe(\n\t\tEffect.map((values) =>\n\t\t\tmatching(values, (id) => id.startsWith('coin:')).map(\n\t\t\t\t({ pluginKey, value }): DashboardCoinCap => {\n\t\t\t\t\tconst v = value as CoinShape;\n\t\t\t\t\tconst faults = newFaults();\n\t\t\t\t\tconst source = narrowEnum(\n\t\t\t\t\t\tv.source,\n\t\t\t\t\t\t['registry', 'on-chain', 'builtin'] as const,\n\t\t\t\t\t\t'coin.source',\n\t\t\t\t\t\tfaults,\n\t\t\t\t\t\t'on-chain',\n\t\t\t\t\t);\n\t\t\t\t\treturn {\n\t\t\t\t\t\tpluginKey,\n\t\t\t\t\t\tsymbol: optStr(v.symbol),\n\t\t\t\t\t\tfullCoinType: reqStr(v.fullCoinType, 'coin.fullCoinType', faults),\n\t\t\t\t\t\tdecimals: reqNum(v.decimals, 'coin.decimals', faults, 0),\n\t\t\t\t\t\tsource,\n\t\t\t\t\t\ttreasuryCapId: optStr(v.treasuryCapId),\n\t\t\t\t\t\tpackageId: optStr(v.packageId),\n\t\t\t\t\t\tnarrowingFault: faultDetail(faults),\n\t\t\t\t\t};\n\t\t\t\t},\n\t\t\t),\n\t\t),\n\t);\n\n\t// Mint ACTION — drives the dashboard Coins panel's Mint button.\n\t//\n\t// Signer source: the resolved coin VALUE carries a self-contained\n\t// `mintFromCap` closure (present only for witness-form coins whose\n\t// publisher still owns the TreasuryCap). That closure already captures\n\t// the treasury-cap-owning publisher `MintSigner` + the resolved cap id\n\t// in-process — the same lease-owning path `coin/service.ts`'s\n\t// `fundingStrategy` uses — so we mint WITHOUT threading a signer through\n\t// this seam (we read the resolved value, never plugin internals).\n\t//\n\t// Never fails (`E = never`): every reject path (bad address, non-\n\t// positive amount, no matching coin, cap-not-owned, on-chain failure)\n\t// degrades to `{ ok: false, detail, digest: null }` so the dashboard\n\t// query can't be taken down by a single bad mint.\n\tconst mintCoin: DashboardDomain['mintCoin'] = (input) =>\n\t\tEffect.gen(function* () {\n\t\t\tconst recipient = input.recipient.trim();\n\t\t\tif (!SUI_ADDRESS_RE.test(recipient)) {\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\tdetail: `invalid recipient '${input.recipient}': expected a 0x-prefixed Sui address`,\n\t\t\t\t\tdigest: null,\n\t\t\t\t} satisfies DashboardMintResult;\n\t\t\t}\n\t\t\tif (!isPositiveIntegerString(input.amountBaseUnits)) {\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\tdetail: `invalid amountBaseUnits '${input.amountBaseUnits}': expected a positive integer string`,\n\t\t\t\t\tdigest: null,\n\t\t\t\t} satisfies DashboardMintResult;\n\t\t\t}\n\n\t\t\t// Locate the resolved coin whose fullCoinType matches. Match on the\n\t\t\t// resolved value's `fullCoinType` (the stable on-chain type), not the\n\t\t\t// resource-id prefix, so callers pass the same `coinType` the\n\t\t\t// `coinCaps` query surfaced. Read `fullCoinType` fail-loud so a coin\n\t\t\t// value missing/empty `fullCoinType` names the REAL cause (a drifted\n\t\t\t// resolved shape) rather than collapsing into the generic\n\t\t\t// 'no resolved coin found' reject.\n\t\t\tconst values = yield* control.resolvedValues;\n\t\t\tconst candidates = matching(values, (id) => id.startsWith('coin:')).map(\n\t\t\t\t({ value }) => value as CoinShape,\n\t\t\t);\n\t\t\tconst matchFaults = newFaults();\n\t\t\tconst match = candidates.find(\n\t\t\t\t(v) => reqStr(v.fullCoinType, 'coin.fullCoinType', matchFaults) === input.coinType,\n\t\t\t);\n\n\t\t\tif (match === undefined) {\n\t\t\t\tconst fault = faultDetail(matchFaults);\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\tdetail:\n\t\t\t\t\t\tfault === null\n\t\t\t\t\t\t\t? `no resolved coin found for type '${input.coinType}'`\n\t\t\t\t\t\t\t: `no resolved coin found for type '${input.coinType}' (${fault})`,\n\t\t\t\t\tdigest: null,\n\t\t\t\t} satisfies DashboardMintResult;\n\t\t\t}\n\t\t\tif (typeof match.mintFromCap !== 'function') {\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\tdetail:\n\t\t\t\t\t\t`coin '${input.coinType}' has no in-process treasury cap signer — ` +\n\t\t\t\t\t\t'mint is only available for local-package coins whose publisher still owns the TreasuryCap',\n\t\t\t\t\tdigest: null,\n\t\t\t\t} satisfies DashboardMintResult;\n\t\t\t}\n\n\t\t\treturn yield* match\n\t\t\t\t.mintFromCap({ to: recipient, amount: BigInt(input.amountBaseUnits) })\n\t\t\t\t.pipe(\n\t\t\t\t\tEffect.map(\n\t\t\t\t\t\t(r): DashboardMintResult => ({\n\t\t\t\t\t\t\tok: true,\n\t\t\t\t\t\t\tdetail: `minted ${input.amountBaseUnits} of ${input.coinType} to ${recipient}`,\n\t\t\t\t\t\t\tdigest: r.digest,\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t\t// Typed coin/artifact-publisher failures carry `.message`.\n\t\t\t\t\tEffect.catch((cause) =>\n\t\t\t\t\t\tEffect.succeed<DashboardMintResult>({\n\t\t\t\t\t\t\tok: false,\n\t\t\t\t\t\t\tdetail:\n\t\t\t\t\t\t\t\ttypeof cause?.message === 'string'\n\t\t\t\t\t\t\t\t\t? cause.message\n\t\t\t\t\t\t\t\t\t: `mint failed: ${String(cause)}`,\n\t\t\t\t\t\t\tdigest: null,\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t\t// Residual defects (interrupts, unexpected throws) — degrade\n\t\t\t\t\t// rather than crash the dashboard query.\n\t\t\t\t\tEffect.catchCause((cause) =>\n\t\t\t\t\t\tEffect.succeed<DashboardMintResult>({\n\t\t\t\t\t\t\tok: false,\n\t\t\t\t\t\t\tdetail: `mint crashed: ${String(cause)}`,\n\t\t\t\t\t\t\tdigest: null,\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t});\n\n\t// --- Fund ACTION + fundable-coin derivation ------------------------\n\t//\n\t// Reuses devstack's IN-PROCESS funding strategies — the same strategy\n\t// registry the boot-time account funding pass dispatches through\n\t// (`plugins/account/funding.ts`). We never re-implement signing or the\n\t// exchange/pool swaps:\n\t//\n\t// - SUI (absent / canonical type) → `faucet:request:<chainId>`. The SUI\n\t// faucet strategy takes `{ address, amount }` only (no account handle)\n\t// and is FIXED-AMOUNT (the standard faucet ignores `amount`). Recipient\n\t// may be ANY 0x address.\n\t// - WAL/DEEP → `coinType:<fullCoinType>`. The strategy takes\n\t// `{ address, amount, account }` and runs an account-signed swap, so\n\t// the recipient MUST be a resolved account in the stack (we match the\n\t// recipient address against the resolved `account/<name>` values and\n\t// pass the live `AccountValue` as `req.account`).\n\t//\n\t// Never fails (`E = never`): every reject path degrades to\n\t// `{ ok:false, detail }` so a single bad fund can't take down the query.\n\n\t/** The chain id the SUI funding path keys on, read from the resolved sui\n\t * value (same `chainId` field the sui codegen/snapshot decls stamp). */\n\tconst readChainId = control.resolvedValues.pipe(\n\t\tEffect.map((values) => {\n\t\t\tconst sui = matching(values, (id) => id === 'sui')[0];\n\t\t\treturn sui === undefined ? null : optStr((sui.value as SuiShape).chainId);\n\t\t}),\n\t);\n\n\tconst fundableCoins: DashboardDomain['fundableCoins'] = Effect.gen(function* () {\n\t\t// SUI is always fundable when a faucet strategy is registered for the\n\t\t// active chain; the fixed-amount faucet ignores the requested amount.\n\t\tconst out: DashboardFundableCoin[] = [];\n\t\tif (strategyRegistry === null) return out;\n\n\t\tconst chain = yield* readChainId;\n\t\tconst keys = yield* strategyRegistry.list();\n\t\tif (chain !== null && keys.includes(faucetCapabilityKeyFor(chain))) {\n\t\t\tout.push({\n\t\t\t\tsymbol: 'SUI',\n\t\t\t\tcoinType: SUI_FULL_COIN_TYPE,\n\t\t\t\thonorsAmount: false,\n\t\t\t\trequiresAccountSigner: false,\n\t\t\t});\n\t\t}\n\n\t\t// A coin is fundable iff a `coinType:<fullCoinType>` strategy is actually\n\t\t// registered (the walrus/deepbook/coin plugin contributed one). Derive the\n\t\t// display symbol from the coin type's module path (`::wal::WAL` → WAL,\n\t\t// `::deep::DEEP` → DEEP) so we stay name-blind on the coin VALUE while\n\t\t// still labeling the pill. `requiresAccountSigner` is read from the\n\t\t// strategy's own `requiresRecipientAccount` flag — NOT assumed true for\n\t\t// every coin: WAL/DEEP swaps spend the recipient's SUI (true), but a\n\t\t// managed-coin MINT strategy transfers to a passive recipient (false), so\n\t\t// it can fund any 0x address.\n\t\tfor (const key of keys) {\n\t\t\tif (!key.startsWith('coinType:')) continue;\n\t\t\tconst coinType = key.slice('coinType:'.length);\n\t\t\tif (coinType === SUI_FULL_COIN_TYPE) continue;\n\t\t\tconst strategy = yield* strategyRegistry\n\t\t\t\t.get<typeof key, AccountFundingStrategy<unknown, AccountValue>>(key)\n\t\t\t\t.pipe(Effect.catchTag('StrategyNotFoundError', () => Effect.succeed(null)));\n\t\t\tout.push({\n\t\t\t\tsymbol: coinSymbolFromType(coinType),\n\t\t\t\tcoinType,\n\t\t\t\thonorsAmount: true,\n\t\t\t\trequiresAccountSigner: strategy?.requiresRecipientAccount ?? false,\n\t\t\t});\n\t\t}\n\t\treturn out;\n\t});\n\n\tconst fundAccount: DashboardDomain['fundAccount'] = (input) =>\n\t\tEffect.gen(function* () {\n\t\t\tconst recipient = input.recipient.trim();\n\t\t\tif (!SUI_ADDRESS_RE.test(recipient)) {\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\tdetail: `invalid recipient '${input.recipient}': expected a 0x-prefixed Sui address`,\n\t\t\t\t} satisfies DashboardFundResult;\n\t\t\t}\n\t\t\tif (strategyRegistry === null) {\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\tdetail: 'funding unavailable: no strategy registry wired',\n\t\t\t\t} satisfies DashboardFundResult;\n\t\t\t}\n\n\t\t\tconst coinType = input.coinType?.trim() ?? '';\n\t\t\tconst isSui = coinType === '' || coinType === SUI_FULL_COIN_TYPE;\n\n\t\t\t// SUI path — chain faucet strategy. Fixed-amount: we pass a nominal\n\t\t\t// amount but the standard faucet ignores it. Recipient may be any\n\t\t\t// 0x address (no account handle required).\n\t\t\tif (isSui) {\n\t\t\t\tconst chain = yield* readChainId;\n\t\t\t\tif (chain === null) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tok: false,\n\t\t\t\t\t\tdetail: 'cannot fund SUI: no resolved sui plugin / chain id in this stack',\n\t\t\t\t\t} satisfies DashboardFundResult;\n\t\t\t\t}\n\t\t\t\tconst key = faucetCapabilityKeyFor(chain);\n\t\t\t\tconst strategy = yield* strategyRegistry\n\t\t\t\t\t.get<typeof key, FaucetStrategy>(key)\n\t\t\t\t\t.pipe(Effect.catchTag('StrategyNotFoundError', () => Effect.succeed(null)));\n\t\t\t\tif (strategy === null) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tok: false,\n\t\t\t\t\t\tdetail: `no SUI faucet strategy registered for chain '${chain}'`,\n\t\t\t\t\t} satisfies DashboardFundResult;\n\t\t\t\t}\n\t\t\t\t// `amount` is nominal — the standard faucet grant is fixed and\n\t\t\t\t// ignores it. Default to 1 SUI in MIST so a strategy that DID\n\t\t\t\t// honor it lands a sane value.\n\t\t\t\tconst amount = parseAmount(input.amountBaseUnits) ?? 1_000_000_000n;\n\t\t\t\treturn yield* strategy.request({ address: recipient, amount }).pipe(\n\t\t\t\t\tEffect.map(\n\t\t\t\t\t\t(): DashboardFundResult => ({\n\t\t\t\t\t\t\tok: true,\n\t\t\t\t\t\t\tdetail: `requested SUI for ${recipient} (fixed-amount faucet grant)`,\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t\tEffect.catch((cause) =>\n\t\t\t\t\t\tEffect.succeed<DashboardFundResult>({\n\t\t\t\t\t\t\tok: false,\n\t\t\t\t\t\t\tdetail: `SUI faucet request failed: ${faucetCauseDetail(cause)}`,\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t\tEffect.catchCause((cause) =>\n\t\t\t\t\t\tEffect.succeed<DashboardFundResult>({\n\t\t\t\t\t\t\tok: false,\n\t\t\t\t\t\t\tdetail: `SUI faucet request crashed: ${String(cause)}`,\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// WAL/DEEP path — coin-specific account-signed funding strategy.\n\t\t\tconst amount = parseAmount(input.amountBaseUnits);\n\t\t\tif (amount === null) {\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\tdetail: `invalid amountBaseUnits '${input.amountBaseUnits ?? ''}': expected a positive integer string`,\n\t\t\t\t} satisfies DashboardFundResult;\n\t\t\t}\n\n\t\t\tconst key = `coinType:${coinType}` as const;\n\t\t\tconst strategy = yield* strategyRegistry\n\t\t\t\t.get<typeof key, AccountFundingStrategy<unknown, AccountValue>>(key)\n\t\t\t\t.pipe(Effect.catchTag('StrategyNotFoundError', () => Effect.succeed(null)));\n\t\t\tif (strategy === null) {\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\tdetail:\n\t\t\t\t\t\t`no funding strategy registered for coin '${coinType}' — ` +\n\t\t\t\t\t\t'WAL needs the walrus plugin (with an exchange) and DEEP needs the deepbook plugin',\n\t\t\t\t} satisfies DashboardFundResult;\n\t\t\t}\n\n\t\t\t// Match the recipient to a resolved account, if one holds this address.\n\t\t\tconst values = yield* control.resolvedValues;\n\t\t\tconst account = matching(values, (id) => id.startsWith('account/'))\n\t\t\t\t.map(({ value }) => value as AccountShape)\n\t\t\t\t.find((v) => optStr(v.address) === recipient);\n\n\t\t\t// Account-spending strategies (WAL/DEEP) swap the recipient's OWN SUI,\n\t\t\t// so the recipient must BE a resolved account with a signer. Mint-style\n\t\t\t// strategies (managed coins) transfer to a passive recipient, so any 0x\n\t\t\t// address is fine — gate the rejection on the strategy's flag, not on\n\t\t\t// the coin being non-SUI.\n\t\t\tif (strategy.requiresRecipientAccount === true && account === undefined) {\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\tdetail:\n\t\t\t\t\t\t`coin '${coinType}' is funded by an account-signed swap, but '${recipient}' ` +\n\t\t\t\t\t\t'is not a resolved account in this stack — fund a configured account (the swap spends its SUI)',\n\t\t\t\t} satisfies DashboardFundResult;\n\t\t\t}\n\n\t\t\treturn yield* strategy\n\t\t\t\t.request({\n\t\t\t\t\taddress: recipient,\n\t\t\t\t\tamount,\n\t\t\t\t\t...(account === undefined ? {} : { account: account as unknown as AccountValue }),\n\t\t\t\t})\n\t\t\t\t.pipe(\n\t\t\t\t\tEffect.map(\n\t\t\t\t\t\t(): DashboardFundResult => ({\n\t\t\t\t\t\t\tok: true,\n\t\t\t\t\t\t\tdetail: `funded ${amount} base units of ${coinType} to ${recipient}`,\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t\tEffect.catch((cause) =>\n\t\t\t\t\t\tEffect.succeed<DashboardFundResult>({\n\t\t\t\t\t\t\tok: false,\n\t\t\t\t\t\t\tdetail: `${coinSymbolFromType(coinType)} funding failed: ${fundingCauseDetail(cause)}`,\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t\tEffect.catchCause((cause) =>\n\t\t\t\t\t\tEffect.succeed<DashboardFundResult>({\n\t\t\t\t\t\t\tok: false,\n\t\t\t\t\t\t\tdetail: `${coinSymbolFromType(coinType)} funding crashed: ${String(cause)}`,\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t});\n\n\treturn {\n\t\tmode,\n\t\tdeepbook,\n\t\tseal,\n\t\tcoinCaps,\n\t\tmintCoin,\n\t\tfundableCoins,\n\t\tfundAccount,\n\t};\n};\n\n/** An all-empty dashboard domain. Used by tests that exercise the\n * schema/server without a live registry. Every accessor resolves to\n * empty/`null`. */\nexport const emptyDashboardDomain: DashboardDomain = {\n\tmode: Effect.succeed(null),\n\tdeepbook: Effect.succeed([]),\n\tseal: Effect.succeed([]),\n\tcoinCaps: Effect.succeed([]),\n\tmintCoin: () => Effect.succeed({ ok: false, detail: 'unavailable', digest: null }),\n\tfundableCoins: Effect.succeed([]),\n\tfundAccount: () => Effect.succeed({ ok: false, detail: 'unavailable' }),\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"domain.mjs","names":[],"sources":["../../../src/plugins/dashboard/domain.ts"],"sourcesContent":["// Dashboard plugin-domain shaping.\n//\n// The substrate control-plane is GENERIC + name-blind: it hands out the\n// snapshot catalog, the observability rings, and a single uninterpreted\n// `resolvedValues` accessor (see `substrate/runtime/control-plane/`). This\n// module — which lives in the PLUGIN layer and is allowed to name plugins —\n// owns ALL plugin-name-aware shaping: it matches resolved plugin values by\n// resource id (`deepbook/`-prefix, `seal:`-prefix, `coin:`-prefix, `id ===\n// 'sui'`) and projects them into the app-agnostic shapes the GraphQL schema\n// renders. It also owns the `mode` derivation and the coin `mint` action.\n//\n// Design discipline (mirrors the old substrate seam, one layer up):\n// - We import NO plugin types — we narrow the opaque `unknown` resolved\n// value through shallow structural shapes that mirror the relevant\n// fields. A field missing on the live value collapses to the\n// null/empty default.\n// - Every accessor degrades to empty/`null` rather than failing, so a\n// single missing/uninitialised plugin can't take down a dashboard\n// query (`E = never` on the public surface).\n// - We match plugins by RESOURCE ID — a prefix for the multi-instance\n// kinds (`deepbook/`, `seal:`, `coin:`) and an exact id for the\n// singletons (`id === 'sui'`) — rather than\n// plugin-key substrings: the resource id is the stable identity the\n// plugin factories mint.\n\nimport { Effect } from 'effect';\n\nimport type {\n\tControlPlaneDomain,\n\tControlPlaneResolvedValue,\n} from '../../substrate/runtime/control-plane/service.ts';\nimport type { StrategyRegistry } from '../../contracts/strategy-contributor.ts';\nimport type { FaucetStrategy } from '../../contracts/faucet-strategy.ts';\nimport type { AccountFundingStrategy } from '../../contracts/funding-strategy.ts';\nimport type { AccountValue } from '../account/index.ts';\n// The faucet capability-key prefix is owned by the faucet plugin (single\n// source of truth). The dashboard plugin layer is allowed to name plugins,\n// so importing the key constructor is the same cross-plugin reference the\n// account funding pass (`plugins/account/funding.ts`) uses — NOT a resolved\n// plugin-VALUE import (those stay structurally narrowed below). Imported via\n// the sibling's barrel to satisfy the plugin-boundary invariant.\nimport { FAUCET_CAPABILITY_KEY_PREFIX } from '../faucet/index.ts';\n\n// -----------------------------------------------------------------------------\n// App-agnostic domain shapes the GraphQL schema renders.\n// -----------------------------------------------------------------------------\n\n/** One DeepBook pool object id + coin types (prices are chain-direct). */\nexport interface DashboardDeepbookPool {\n\treadonly name: string;\n\treadonly poolId: string;\n\treadonly baseCoinType: string;\n\treadonly quoteCoinType: string;\n}\n\n/** One Pyth price feed seeded inside DeepBook at boot. `price`/`expo` are the\n * FIXED boot-time mock values (static, not a live quote); `price` is carried\n * as a string so large signed values survive the wire without precision loss. */\nexport interface DashboardPythFeed {\n\treadonly symbol: string;\n\treadonly priceInfoObjectId: string;\n\treadonly price: string;\n\treadonly expo: number;\n}\n\nexport interface DashboardDeepbookInfo {\n\treadonly pluginKey: string;\n\treadonly name: string;\n\treadonly mode: 'local' | 'override' | 'known';\n\treadonly network: string;\n\treadonly packageId: string;\n\treadonly registryId: string;\n\treadonly adminCapId: string | null;\n\treadonly deepTreasuryId: string | null;\n\treadonly pools: ReadonlyArray<DashboardDeepbookPool>;\n\t/** Pyth price feeds seeded inside this DeepBook deployment (boot-time mock\n\t * prices). Empty when no Pyth feeds were configured. */\n\treadonly pythFeeds: ReadonlyArray<DashboardPythFeed>;\n\t/** `true` when one or more pools placed seed orders at boot (NOT a\n\t * market-maker process state). */\n\treadonly hasSeedLiquidity: boolean;\n\treadonly serverUrl: string | null;\n\treadonly indexerUrl: string | null;\n\t/** Non-null when one or more required fields failed to narrow off the\n\t * opaque resolved value (fail-loud: schema drift surfaced to the caller\n\t * instead of silently degrading). */\n\treadonly narrowingFault: string | null;\n}\n\nexport interface DashboardSealKeyServer {\n\treadonly objectId: string;\n\treadonly weight: number;\n}\n\nexport interface DashboardSealInfo {\n\treadonly pluginKey: string;\n\treadonly mode: 'local-keygen' | 'live' | 'fork-known';\n\treadonly objectId: string;\n\treadonly keyServerUrl: string;\n\treadonly keyServers: ReadonlyArray<DashboardSealKeyServer>;\n\t/** Threshold = number of registered key-server configs. */\n\treadonly threshold: number;\n\t/** Non-null when one or more required fields failed to narrow off the\n\t * opaque resolved value (fail-loud). */\n\treadonly narrowingFault: string | null;\n}\n\n/** A coin's treasury-cap id (drives the Mint action) + addressing facts. */\nexport interface DashboardCoinCap {\n\treadonly pluginKey: string;\n\treadonly symbol: string | null;\n\treadonly fullCoinType: string;\n\treadonly decimals: number;\n\treadonly source: 'registry' | 'on-chain' | 'builtin';\n\treadonly treasuryCapId: string | null;\n\treadonly packageId: string | null;\n\t/** Non-null when one or more required fields failed to narrow off the\n\t * opaque resolved value (fail-loud). */\n\treadonly narrowingFault: string | null;\n}\n\n/** Input for the dashboard mint ACTION. `amountBaseUnits` is the raw\n * integer amount in the coin's smallest unit (decimals already applied\n * by the caller / form) — a string so large u64 values survive the wire\n * without precision loss. */\nexport interface DashboardMintInput {\n\treadonly coinType: string;\n\treadonly recipient: string;\n\treadonly amountBaseUnits: string;\n}\n\n/** Outcome of a dashboard mint ACTION. Mirrors the snapshot\n * restore/delete result shape plus the on-chain tx `digest` on success. */\nexport interface DashboardMintResult {\n\treadonly ok: boolean;\n\treadonly detail: string;\n\treadonly digest: string | null;\n}\n\n/** Input for the dashboard fund ACTION.\n *\n * `coinType` selects the funding strategy: absent or the canonical SUI\n * type routes through the chain's faucet strategy (`faucet:request:<chainId>`,\n * fixed-amount); any other full coin type routes through the coin-specific\n * `coinType:<fullCoinType>` strategy (WAL exchange swap / DEEP pool swap).\n *\n * `amountBaseUnits` is the raw integer amount in the coin's smallest unit\n * (a string so large u64 values survive the wire without precision loss).\n * The SUI faucet is fixed-amount and IGNORES it; WAL/DEEP honor it. */\nexport interface DashboardFundInput {\n\treadonly recipient: string;\n\treadonly coinType?: string | null;\n\treadonly amountBaseUnits?: string | null;\n}\n\n/** Outcome of a dashboard fund ACTION. The in-process funding strategies\n * return `void` (not a digest), so the result carries only `ok`/`detail`.\n * `ok:true` means the strategy's `request(...)` completed (the faucet POST\n * landed / the swap executed on-chain); `detail` carries the reason on\n * failure. */\nexport interface DashboardFundResult {\n\treadonly ok: boolean;\n\treadonly detail: string;\n}\n\n/** One coin the dashboard faucet can actually fund, with whether the\n * underlying strategy honors a caller-supplied amount.\n *\n * SUI is always present (fixed-amount faucet, `honorsAmount: false`).\n * WAL/DEEP appear only when their plugin registered a `coinType:<X>`\n * funding strategy AND (for WAL) carries a swap that spends the\n * recipient account's SUI — so `honorsAmount: true`. */\nexport interface DashboardFundableCoin {\n\treadonly symbol: string;\n\treadonly coinType: string;\n\treadonly honorsAmount: boolean;\n\t/** True when funding requires the recipient to BE a resolved account in\n\t * the stack (the swap spends that account's own SUI via its in-process\n\t * signer). SUI is `false` (any 0x address); WAL/DEEP are `true`. */\n\treadonly requiresAccountSigner: boolean;\n}\n\n/** The dashboard plugin-domain accessor surface. Each member is a\n * self-contained Effect that never fails (`E = never`); they degrade to\n * empty/`null` so a single missing plugin can't take down the dashboard\n * query. */\nexport interface DashboardDomain {\n\t/** Fork-vs-local mode, derived from the resolved sui plugin's chain\n\t * identity. Used for advance-clock gating. `null` when no sui plugin\n\t * is present. */\n\treadonly mode: Effect.Effect<'fork' | 'local' | 'live' | null>;\n\t/** DeepBook deployments (registry/admin/pool ids + MM state). */\n\treadonly deepbook: Effect.Effect<ReadonlyArray<DashboardDeepbookInfo>>;\n\t/** Seal key-server deployments (objectId/threshold/mode). */\n\treadonly seal: Effect.Effect<ReadonlyArray<DashboardSealInfo>>;\n\t/** Coin treasury caps (drives Mint). */\n\treadonly coinCaps: Effect.Effect<ReadonlyArray<DashboardCoinCap>>;\n\t/** Mint ACTION — mints `amountBaseUnits` of `coinType` to `recipient`,\n\t * signed in-process by the treasury-cap-owning publisher signer the\n\t * resolved coin value's self-contained `mintFromCap` closure holds. */\n\treadonly mintCoin: (input: DashboardMintInput) => Effect.Effect<DashboardMintResult>;\n\t/** Coins the faucet can actually fund right now (SUI always; WAL/DEEP\n\t * only when their plugin registered a funding strategy). Drives the\n\t * faucet panel's coin pills + amount-field gating. */\n\treadonly fundableCoins: Effect.Effect<ReadonlyArray<DashboardFundableCoin>>;\n\t/** Fund ACTION — reuses devstack's in-process funding strategies. SUI\n\t * (absent/canonical type) routes through the chain faucet strategy\n\t * (fixed-amount); WAL/DEEP route through the coin-specific funding\n\t * strategy (account-signed exchange/pool swap). Real result — `ok`\n\t * reflects whether the strategy's `request(...)` completed. */\n\treadonly fundAccount: (input: DashboardFundInput) => Effect.Effect<DashboardFundResult>;\n}\n\n// -----------------------------------------------------------------------------\n// Structural projections of plugin-resolved values\n//\n// We narrow the opaque `unknown` resolved value through shallow structural\n// shapes that mirror the relevant fields. A field missing on the live value\n// collapses to the null/empty default.\n// -----------------------------------------------------------------------------\n\ninterface DeepbookShape {\n\treadonly mode?: unknown;\n\treadonly network?: unknown;\n\treadonly packageId?: unknown;\n\treadonly registryId?: unknown;\n\treadonly adminCapId?: unknown;\n\treadonly deepTreasuryId?: unknown;\n\treadonly pools?: ReadonlyArray<{\n\t\treadonly name?: unknown;\n\t\treadonly poolId?: unknown;\n\t\treadonly baseCoinType?: unknown;\n\t\treadonly quoteCoinType?: unknown;\n\t}>;\n\t/** Resolved Pyth handle (`PythHandle | null`); we read only `feeds`. */\n\treadonly pyth?: {\n\t\treadonly feeds?: ReadonlyArray<{\n\t\t\treadonly symbol?: unknown;\n\t\t\treadonly priceInfoObjectId?: unknown;\n\t\t\treadonly price?: unknown;\n\t\t\treadonly expo?: unknown;\n\t\t}>;\n\t} | null;\n\treadonly hasSeedLiquidity?: unknown;\n\treadonly serverUrl?: unknown;\n\treadonly indexerUrl?: unknown;\n}\n\ninterface SealShape {\n\treadonly mode?: unknown;\n\treadonly objectId?: unknown;\n\treadonly keyServerUrl?: unknown;\n\treadonly serverConfigs?: ReadonlyArray<{\n\t\treadonly objectId?: unknown;\n\t\treadonly weight?: unknown;\n\t}>;\n}\n\ninterface CoinShape {\n\treadonly symbol?: unknown;\n\treadonly fullCoinType?: unknown;\n\treadonly decimals?: unknown;\n\treadonly source?: unknown;\n\treadonly treasuryCapId?: unknown;\n\treadonly packageId?: unknown;\n\t/** Self-contained mint closure on the resolved coin value (present\n\t * for witness-form coins whose publisher still owns the cap). Read\n\t * structurally — we import no coin types. Returns an Effect that\n\t * resolves a `{ digest }`-bearing result or fails with a\n\t * coin/artifact-publisher tagged error. */\n\treadonly mintFromCap?: (opts: {\n\t\treadonly to: string;\n\t\treadonly amount: bigint;\n\t}) => Effect.Effect<{ readonly digest: string }, { readonly message?: unknown }>;\n}\n\ninterface SuiShape {\n\treadonly mode?: unknown;\n\t/** Genesis-digest chain id (`'sui:localnet'`, `'sui:testnet'`,\n\t * `'sui:mainnet-fork@123'`). Composes the faucet capability key\n\t * (`faucet:request:<chainId>`) the SUI funding path resolves. */\n\treadonly chainId?: unknown;\n}\n\n/** Structural shape of the resolved account value (`account/<name>`). The\n * WAL/DEEP funding strategies need this handle as `req.account` — the swap\n * spends the recipient account's own SUI through its in-process signer.\n * Narrowed to the fields the strategy dispatch reads; the full\n * `AccountValue` is assignable to it, so we cast the matched value across\n * the seam without importing the account VALUE shape structurally\n * elsewhere. */\ninterface AccountShape {\n\treadonly address?: unknown;\n\treadonly name?: unknown;\n}\n\n/** A 0x-prefixed Sui address: `0x` + 1..64 hex digits. Mirrors the\n * address validation the mint PTB's `tx.pure.address` ultimately\n * enforces, surfaced up front so the dashboard gets a clean rejection\n * rather than an opaque build failure. */\nconst SUI_ADDRESS_RE = /^0x[0-9a-fA-F]{1,64}$/;\n\n/** The canonical builtin SUI coin type — selects the SUI faucet funding\n * path. Mirrors `SUI_FULL_COIN_TYPE` in `plugins/account/funding.ts`\n * (inlined here so the dashboard plugin does not cross-import the account\n * funding module just for the string). */\nconst SUI_FULL_COIN_TYPE = '0x2::sui::SUI' as const;\n\n/** A positive integer base-unit amount string (no sign, no decimal\n * point, no leading zeros beyond a bare `0` — which is itself rejected\n * as non-positive). */\nconst isPositiveIntegerString = (s: string): boolean => {\n\tif (!/^\\d+$/.test(s)) return false;\n\ttry {\n\t\treturn BigInt(s) > 0n;\n\t} catch {\n\t\treturn false;\n\t}\n};\n\n/** Compose the chain faucet capability key (`faucet:request:<chainId>`)\n * the SUI funding path resolves. Uses the faucet plugin's prefix constant\n * (single source of truth) — same key the boot-time funding pass builds. */\nconst faucetCapabilityKeyFor = (\n\tchain: string,\n): `${typeof FAUCET_CAPABILITY_KEY_PREFIX}:${string}` => `${FAUCET_CAPABILITY_KEY_PREFIX}:${chain}`;\n\n/** Parse a base-unit amount string into a positive bigint, or `null` when\n * absent / non-positive / non-integer. */\nconst parseAmount = (s: string | null | undefined): bigint | null => {\n\tif (s == null) return null;\n\tconst trimmed = s.trim();\n\tif (!isPositiveIntegerString(trimmed)) return null;\n\treturn BigInt(trimmed);\n};\n\n/** Derive a display symbol from a full coin type's module path\n * (`0x…::wal::WAL` → `WAL`, `0x…::deep::DEEP` → `DEEP`). Falls back to the\n * struct name, then the raw type. Keeps the dashboard name-blind on the\n * resolved coin VALUE while still labeling the faucet pill. */\nconst coinSymbolFromType = (coinType: string): string => {\n\tconst parts = coinType.split('::');\n\tconst struct = parts[parts.length - 1];\n\treturn struct !== undefined && struct.length > 0 ? struct : coinType;\n};\n\n/** Extract a human detail from a faucet/funding strategy failure. The\n * strategy error channels are tagged structs carrying `message`/`reason`;\n * read them structurally without importing each plugin's error type. */\nconst causeDetail = (cause: unknown): string => {\n\tif (cause !== null && typeof cause === 'object') {\n\t\tconst c = cause as {\n\t\t\treadonly message?: unknown;\n\t\t\treadonly reason?: unknown;\n\t\t\treadonly _tag?: unknown;\n\t\t};\n\t\tif (typeof c.message === 'string' && c.message.length > 0) return c.message;\n\t\tif (typeof c.reason === 'string' && c.reason.length > 0) return c.reason;\n\t\tif (typeof c._tag === 'string' && c._tag.length > 0) return c._tag;\n\t}\n\treturn String(cause);\n};\nconst faucetCauseDetail = causeDetail;\nconst fundingCauseDetail = causeDetail;\n\n// -----------------------------------------------------------------------------\n// Fail-aware structural narrowing\n//\n// The substrate hands us opaque `unknown` resolved values. We still narrow\n// through shallow structural shapes, but instead of SILENTLY degrading a\n// missing/wrong-typed required field to ''/0 (which hid integration-seam\n// drift), the `req*` readers ACCUMULATE a typed narrowing fault into a\n// per-shaping-call collector. The fault is threaded into the GraphQL-visible\n// result (`narrowingFault` / `detail`) so consumers SEE the drift — WITHOUT\n// ever throwing (the dashboard's `E = never` surface semantic is preserved).\n// `opt*` readers stay quiet: they are for genuinely-optional fields.\n// -----------------------------------------------------------------------------\n\n/** Mutable per-call sink for structural-narrowing faults. */\ntype FaultSink = Array<string>;\n\nconst newFaults = (): FaultSink => [];\n\n/** Collapse accumulated faults into a single nullable detail string for the\n * GraphQL-visible result. `null` when nothing drifted. */\nconst faultDetail = (faults: FaultSink): string | null =>\n\tfaults.length === 0 ? null : faults.join('; ');\n\n/** Describe why a value failed to narrow to the expected type — distinguishing\n * 'absent' from 'present but wrong type'. */\nconst typeFault = (field: string, expected: string, v: unknown): string => {\n\tif (v === undefined || v === null) return `${field}: missing (expected ${expected})`;\n\treturn `${field}: expected ${expected}, got ${typeof v}`;\n};\n\n/** Required string. On mismatch records a fault and returns '' so the panel\n * still renders a (degraded) cell. */\nconst reqStr = (v: unknown, field: string, faults: FaultSink): string => {\n\tif (typeof v === 'string') return v;\n\tfaults.push(typeFault(field, 'string', v));\n\treturn '';\n};\n\n/** Required finite number. On mismatch records a fault and returns the safe\n * display fallback (`fallback`, default 0). */\nconst reqNum = (v: unknown, field: string, faults: FaultSink, fallback = 0): number => {\n\tif (typeof v === 'number' && Number.isFinite(v)) return v;\n\tfaults.push(typeFault(field, 'number', v));\n\treturn fallback;\n};\n\n/** Allow-list enum narrow. Records a fault for an out-of-enum value while\n * still returning a safe display fallback so the panel renders. */\nconst narrowEnum = <T extends string>(\n\traw: unknown,\n\tallowed: ReadonlyArray<T>,\n\tfield: string,\n\tfaults: FaultSink,\n\tfallback: T,\n): T => {\n\tif (typeof raw === 'string' && (allowed as ReadonlyArray<string>).includes(raw)) return raw as T;\n\tfaults.push(\n\t\traw === undefined || raw === null\n\t\t\t? `${field}: missing (expected one of ${allowed.join('|')})`\n\t\t\t: `${field}: '${String(raw)}' not in ${allowed.join('|')}`,\n\t);\n\treturn fallback;\n};\n\n/** Required string-or-numeric coerced to a string. Pyth feed prices arrive as a\n * `bigint` on the resolved value (string in the cached/known shape); accept any\n * of string/number/bigint and record a fault otherwise. Returns '0' on\n * mismatch so the panel still renders a (degraded) cell. */\nconst reqNumericStr = (v: unknown, field: string, faults: FaultSink): string => {\n\tif (typeof v === 'string') return v;\n\tif (typeof v === 'bigint') return v.toString();\n\tif (typeof v === 'number' && Number.isFinite(v)) return String(v);\n\tfaults.push(typeFault(field, 'string|number|bigint', v));\n\treturn '0';\n};\n\n// Genuinely-optional readers — silent by design (absent is a valid state).\nconst optStr = (v: unknown): string | null => (typeof v === 'string' ? v : null);\nconst optNum = (v: unknown): number | null =>\n\ttypeof v === 'number' && Number.isFinite(v) ? v : null;\nconst bool = (v: unknown): boolean => v === true;\n\n/** Filter the generic resolved values down to those whose resource id\n * matches a predicate, in graph order. Returns the `{ pluginKey, value }`\n * the shaping functions consume. */\nconst matching = (\n\tvalues: ReadonlyArray<ControlPlaneResolvedValue>,\n\tmatches: (resourceId: string) => boolean,\n): ReadonlyArray<ControlPlaneResolvedValue> => values.filter((v) => matches(v.id));\n\n// -----------------------------------------------------------------------------\n// Builder\n// -----------------------------------------------------------------------------\n\nexport interface DashboardDomainDeps {\n\t/** The generic, name-blind control-plane domain (resolved values). */\n\treadonly control: ControlPlaneDomain;\n\t/** The scope-local strategy registry — the SAME registry the boot-time\n\t * account funding pass dispatches through (`plugins/account/funding.ts`).\n\t * Drives `fundableCoins` + `fundAccount`: SUI via `faucet:request:<chainId>`,\n\t * WAL/DEEP via `coinType:<fullCoinType>`. `null` in bare test paths; the\n\t * fund accessors degrade to unavailable then. */\n\treadonly strategyRegistry: StrategyRegistry | null;\n}\n\nexport const buildDashboardDomain = (deps: DashboardDomainDeps): DashboardDomain => {\n\tconst { control, strategyRegistry } = deps;\n\n\tconst mode: DashboardDomain['mode'] = control.resolvedValues.pipe(\n\t\tEffect.map((values) => {\n\t\t\tconst sui = matching(values, (id) => id === 'sui')[0];\n\t\t\tif (sui === undefined) return null;\n\t\t\tconst m = (sui.value as SuiShape).mode;\n\t\t\tswitch (m) {\n\t\t\t\tcase 'fork':\n\t\t\t\t\treturn 'fork';\n\t\t\t\tcase 'live':\n\t\t\t\t\treturn 'live';\n\t\t\t\tcase 'local':\n\t\t\t\tcase 'local-rpc':\n\t\t\t\t\treturn 'local';\n\t\t\t\tdefault:\n\t\t\t\t\treturn null;\n\t\t\t}\n\t\t}),\n\t);\n\n\tconst deepbook: DashboardDomain['deepbook'] = control.resolvedValues.pipe(\n\t\tEffect.map((values) =>\n\t\t\tmatching(values, (id) => id.startsWith('deepbook/')).map(\n\t\t\t\t({ pluginKey, value }): DashboardDeepbookInfo => {\n\t\t\t\t\tconst v = value as DeepbookShape;\n\t\t\t\t\tconst faults = newFaults();\n\t\t\t\t\tconst dbMode = narrowEnum(\n\t\t\t\t\t\tv.mode,\n\t\t\t\t\t\t['local', 'override', 'known'] as const,\n\t\t\t\t\t\t'deepbook.mode',\n\t\t\t\t\t\tfaults,\n\t\t\t\t\t\t'local',\n\t\t\t\t\t);\n\t\t\t\t\treturn {\n\t\t\t\t\t\tpluginKey,\n\t\t\t\t\t\tname: pluginKey.replace(/^deepbook:/, ''),\n\t\t\t\t\t\tmode: dbMode,\n\t\t\t\t\t\tnetwork: reqStr(v.network, 'deepbook.network', faults),\n\t\t\t\t\t\tpackageId: reqStr(v.packageId, 'deepbook.packageId', faults),\n\t\t\t\t\t\tregistryId: reqStr(v.registryId, 'deepbook.registryId', faults),\n\t\t\t\t\t\tadminCapId: optStr(v.adminCapId),\n\t\t\t\t\t\tdeepTreasuryId: optStr(v.deepTreasuryId),\n\t\t\t\t\t\tpools: (v.pools ?? []).map((p, i) => ({\n\t\t\t\t\t\t\tname: reqStr(p.name, `deepbook.pools[${i}].name`, faults),\n\t\t\t\t\t\t\tpoolId: reqStr(p.poolId, `deepbook.pools[${i}].poolId`, faults),\n\t\t\t\t\t\t\tbaseCoinType: reqStr(p.baseCoinType, `deepbook.pools[${i}].baseCoinType`, faults),\n\t\t\t\t\t\t\tquoteCoinType: reqStr(p.quoteCoinType, `deepbook.pools[${i}].quoteCoinType`, faults),\n\t\t\t\t\t\t})),\n\t\t\t\t\t\t// Pyth feeds are seeded inside DeepBook (`pyth.feeds`); absent\n\t\t\t\t\t\t// when no Pyth feeds were configured (`pyth` is null/empty).\n\t\t\t\t\t\tpythFeeds: (v.pyth?.feeds ?? []).map((f, i) => ({\n\t\t\t\t\t\t\tsymbol: reqStr(f.symbol, `deepbook.pyth.feeds[${i}].symbol`, faults),\n\t\t\t\t\t\t\tpriceInfoObjectId: reqStr(\n\t\t\t\t\t\t\t\tf.priceInfoObjectId,\n\t\t\t\t\t\t\t\t`deepbook.pyth.feeds[${i}].priceInfoObjectId`,\n\t\t\t\t\t\t\t\tfaults,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tprice: reqNumericStr(f.price, `deepbook.pyth.feeds[${i}].price`, faults),\n\t\t\t\t\t\t\texpo: reqNum(f.expo, `deepbook.pyth.feeds[${i}].expo`, faults),\n\t\t\t\t\t\t})),\n\t\t\t\t\t\thasSeedLiquidity: bool(v.hasSeedLiquidity),\n\t\t\t\t\t\tserverUrl: optStr(v.serverUrl),\n\t\t\t\t\t\tindexerUrl: optStr(v.indexerUrl),\n\t\t\t\t\t\tnarrowingFault: faultDetail(faults),\n\t\t\t\t\t};\n\t\t\t\t},\n\t\t\t),\n\t\t),\n\t);\n\n\tconst seal: DashboardDomain['seal'] = control.resolvedValues.pipe(\n\t\tEffect.map((values) =>\n\t\t\tmatching(values, (id) => id.startsWith('seal:')).map(\n\t\t\t\t({ pluginKey, value }): DashboardSealInfo => {\n\t\t\t\t\tconst v = value as SealShape;\n\t\t\t\t\tconst faults = newFaults();\n\t\t\t\t\tconst sealMode = narrowEnum(\n\t\t\t\t\t\tv.mode,\n\t\t\t\t\t\t['local-keygen', 'live', 'fork-known'] as const,\n\t\t\t\t\t\t'seal.mode',\n\t\t\t\t\t\tfaults,\n\t\t\t\t\t\t'local-keygen',\n\t\t\t\t\t);\n\t\t\t\t\tconst keyServers = (v.serverConfigs ?? []).map((c, i) => ({\n\t\t\t\t\t\tobjectId: reqStr(c.objectId, `seal.serverConfigs[${i}].objectId`, faults),\n\t\t\t\t\t\tweight: optNum(c.weight) ?? 1,\n\t\t\t\t\t}));\n\t\t\t\t\treturn {\n\t\t\t\t\t\tpluginKey,\n\t\t\t\t\t\tmode: sealMode,\n\t\t\t\t\t\tobjectId: reqStr(v.objectId, 'seal.objectId', faults),\n\t\t\t\t\t\tkeyServerUrl: reqStr(v.keyServerUrl, 'seal.keyServerUrl', faults),\n\t\t\t\t\t\tkeyServers,\n\t\t\t\t\t\tthreshold: keyServers.length,\n\t\t\t\t\t\tnarrowingFault: faultDetail(faults),\n\t\t\t\t\t};\n\t\t\t\t},\n\t\t\t),\n\t\t),\n\t);\n\n\tconst coinCaps: DashboardDomain['coinCaps'] = control.resolvedValues.pipe(\n\t\tEffect.map((values) =>\n\t\t\tmatching(values, (id) => id.startsWith('coin:')).map(\n\t\t\t\t({ pluginKey, value }): DashboardCoinCap => {\n\t\t\t\t\tconst v = value as CoinShape;\n\t\t\t\t\tconst faults = newFaults();\n\t\t\t\t\tconst source = narrowEnum(\n\t\t\t\t\t\tv.source,\n\t\t\t\t\t\t['registry', 'on-chain', 'builtin'] as const,\n\t\t\t\t\t\t'coin.source',\n\t\t\t\t\t\tfaults,\n\t\t\t\t\t\t'on-chain',\n\t\t\t\t\t);\n\t\t\t\t\treturn {\n\t\t\t\t\t\tpluginKey,\n\t\t\t\t\t\tsymbol: optStr(v.symbol),\n\t\t\t\t\t\tfullCoinType: reqStr(v.fullCoinType, 'coin.fullCoinType', faults),\n\t\t\t\t\t\tdecimals: reqNum(v.decimals, 'coin.decimals', faults, 0),\n\t\t\t\t\t\tsource,\n\t\t\t\t\t\ttreasuryCapId: optStr(v.treasuryCapId),\n\t\t\t\t\t\tpackageId: optStr(v.packageId),\n\t\t\t\t\t\tnarrowingFault: faultDetail(faults),\n\t\t\t\t\t};\n\t\t\t\t},\n\t\t\t),\n\t\t),\n\t);\n\n\t// Mint ACTION — drives the dashboard Coins panel's Mint button.\n\t//\n\t// Signer source: the resolved coin VALUE carries a self-contained\n\t// `mintFromCap` closure (present only for witness-form coins whose\n\t// publisher still owns the TreasuryCap). That closure already captures\n\t// the treasury-cap-owning publisher `MintSigner` + the resolved cap id\n\t// in-process — the same lease-owning path `coin/service.ts`'s\n\t// `fundingStrategy` uses — so we mint WITHOUT threading a signer through\n\t// this seam (we read the resolved value, never plugin internals).\n\t//\n\t// Never fails (`E = never`): every reject path (bad address, non-\n\t// positive amount, no matching coin, cap-not-owned, on-chain failure)\n\t// degrades to `{ ok: false, detail, digest: null }` so the dashboard\n\t// query can't be taken down by a single bad mint.\n\tconst mintCoin: DashboardDomain['mintCoin'] = (input) =>\n\t\tEffect.gen(function* () {\n\t\t\tconst recipient = input.recipient.trim();\n\t\t\tif (!SUI_ADDRESS_RE.test(recipient)) {\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\tdetail: `invalid recipient '${input.recipient}': expected a 0x-prefixed Sui address`,\n\t\t\t\t\tdigest: null,\n\t\t\t\t} satisfies DashboardMintResult;\n\t\t\t}\n\t\t\tif (!isPositiveIntegerString(input.amountBaseUnits)) {\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\tdetail: `invalid amountBaseUnits '${input.amountBaseUnits}': expected a positive integer string`,\n\t\t\t\t\tdigest: null,\n\t\t\t\t} satisfies DashboardMintResult;\n\t\t\t}\n\n\t\t\t// Locate the resolved coin whose fullCoinType matches. Match on the\n\t\t\t// resolved value's `fullCoinType` (the stable on-chain type), not the\n\t\t\t// resource-id prefix, so callers pass the same `coinType` the\n\t\t\t// `coinCaps` query surfaced. Read `fullCoinType` fail-loud so a coin\n\t\t\t// value missing/empty `fullCoinType` names the REAL cause (a drifted\n\t\t\t// resolved shape) rather than collapsing into the generic\n\t\t\t// 'no resolved coin found' reject.\n\t\t\tconst values = yield* control.resolvedValues;\n\t\t\tconst candidates = matching(values, (id) => id.startsWith('coin:')).map(\n\t\t\t\t({ value }) => value as CoinShape,\n\t\t\t);\n\t\t\tconst matchFaults = newFaults();\n\t\t\tconst match = candidates.find(\n\t\t\t\t(v) => reqStr(v.fullCoinType, 'coin.fullCoinType', matchFaults) === input.coinType,\n\t\t\t);\n\n\t\t\tif (match === undefined) {\n\t\t\t\tconst fault = faultDetail(matchFaults);\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\tdetail:\n\t\t\t\t\t\tfault === null\n\t\t\t\t\t\t\t? `no resolved coin found for type '${input.coinType}'`\n\t\t\t\t\t\t\t: `no resolved coin found for type '${input.coinType}' (${fault})`,\n\t\t\t\t\tdigest: null,\n\t\t\t\t} satisfies DashboardMintResult;\n\t\t\t}\n\t\t\tif (typeof match.mintFromCap !== 'function') {\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\tdetail:\n\t\t\t\t\t\t`coin '${input.coinType}' has no in-process treasury cap signer — ` +\n\t\t\t\t\t\t'mint is only available for local-package coins whose publisher still owns the TreasuryCap',\n\t\t\t\t\tdigest: null,\n\t\t\t\t} satisfies DashboardMintResult;\n\t\t\t}\n\n\t\t\treturn yield* match\n\t\t\t\t.mintFromCap({ to: recipient, amount: BigInt(input.amountBaseUnits) })\n\t\t\t\t.pipe(\n\t\t\t\t\tEffect.map(\n\t\t\t\t\t\t(r): DashboardMintResult => ({\n\t\t\t\t\t\t\tok: true,\n\t\t\t\t\t\t\tdetail: `minted ${input.amountBaseUnits} of ${input.coinType} to ${recipient}`,\n\t\t\t\t\t\t\tdigest: r.digest,\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t\t// Typed coin/artifact-publisher failures carry `.message`.\n\t\t\t\t\tEffect.catch((cause) =>\n\t\t\t\t\t\tEffect.succeed<DashboardMintResult>({\n\t\t\t\t\t\t\tok: false,\n\t\t\t\t\t\t\tdetail:\n\t\t\t\t\t\t\t\ttypeof cause?.message === 'string'\n\t\t\t\t\t\t\t\t\t? cause.message\n\t\t\t\t\t\t\t\t\t: `mint failed: ${String(cause)}`,\n\t\t\t\t\t\t\tdigest: null,\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t\t// Residual defects (interrupts, unexpected throws) — degrade\n\t\t\t\t\t// rather than crash the dashboard query.\n\t\t\t\t\tEffect.catchCause((cause) =>\n\t\t\t\t\t\tEffect.succeed<DashboardMintResult>({\n\t\t\t\t\t\t\tok: false,\n\t\t\t\t\t\t\tdetail: `mint crashed: ${String(cause)}`,\n\t\t\t\t\t\t\tdigest: null,\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t});\n\n\t// --- Fund ACTION + fundable-coin derivation ------------------------\n\t//\n\t// Reuses devstack's IN-PROCESS funding strategies — the same strategy\n\t// registry the boot-time account funding pass dispatches through\n\t// (`plugins/account/funding.ts`). We never re-implement signing or the\n\t// exchange/pool swaps:\n\t//\n\t// - SUI (absent / canonical type) → `faucet:request:<chainId>`. The SUI\n\t// faucet strategy takes `{ address, amount }` only (no account handle)\n\t// and is FIXED-AMOUNT (the standard faucet ignores `amount`). Recipient\n\t// may be ANY 0x address.\n\t// - WAL/DEEP → `coinType:<fullCoinType>`. The strategy takes\n\t// `{ address, amount, account }` and runs an account-signed swap, so\n\t// the recipient MUST be a resolved account in the stack (we match the\n\t// recipient address against the resolved `account/<name>` values and\n\t// pass the live `AccountValue` as `req.account`).\n\t//\n\t// Never fails (`E = never`): every reject path degrades to\n\t// `{ ok:false, detail }` so a single bad fund can't take down the query.\n\n\t/** The chain id the SUI funding path keys on, read from the resolved sui\n\t * value (same `chainId` field the sui codegen/snapshot decls stamp). */\n\tconst readChainId = control.resolvedValues.pipe(\n\t\tEffect.map((values) => {\n\t\t\tconst sui = matching(values, (id) => id === 'sui')[0];\n\t\t\treturn sui === undefined ? null : optStr((sui.value as SuiShape).chainId);\n\t\t}),\n\t);\n\n\tconst fundableCoins: DashboardDomain['fundableCoins'] = Effect.gen(function* () {\n\t\t// SUI is always fundable when a faucet strategy is registered for the\n\t\t// active chain; the fixed-amount faucet ignores the requested amount.\n\t\tconst out: DashboardFundableCoin[] = [];\n\t\tif (strategyRegistry === null) return out;\n\n\t\tconst chain = yield* readChainId;\n\t\tconst keys = yield* strategyRegistry.list();\n\t\tif (chain !== null && keys.includes(faucetCapabilityKeyFor(chain))) {\n\t\t\tout.push({\n\t\t\t\tsymbol: 'SUI',\n\t\t\t\tcoinType: SUI_FULL_COIN_TYPE,\n\t\t\t\thonorsAmount: false,\n\t\t\t\trequiresAccountSigner: false,\n\t\t\t});\n\t\t}\n\n\t\t// A coin is fundable iff a `coinType:<fullCoinType>` strategy is actually\n\t\t// registered (the walrus/deepbook/coin plugin contributed one). Derive the\n\t\t// display symbol from the coin type's module path (`::wal::WAL` → WAL,\n\t\t// `::deep::DEEP` → DEEP) so we stay name-blind on the coin VALUE while\n\t\t// still labeling the pill. `requiresAccountSigner` is read from the\n\t\t// strategy's own `requiresRecipientAccount` flag — NOT assumed true for\n\t\t// every coin: WAL/DEEP swaps spend the recipient's SUI (true), but a\n\t\t// managed-coin MINT strategy transfers to a passive recipient (false), so\n\t\t// it can fund any 0x address.\n\t\tfor (const key of keys) {\n\t\t\tif (!key.startsWith('coinType:')) continue;\n\t\t\tconst coinType = key.slice('coinType:'.length);\n\t\t\tif (coinType === SUI_FULL_COIN_TYPE) continue;\n\t\t\tconst strategy = yield* strategyRegistry\n\t\t\t\t.get<typeof key, AccountFundingStrategy<unknown, AccountValue>>(key)\n\t\t\t\t.pipe(Effect.catchTag('StrategyNotFoundError', () => Effect.succeed(null)));\n\t\t\tout.push({\n\t\t\t\tsymbol: coinSymbolFromType(coinType),\n\t\t\t\tcoinType,\n\t\t\t\thonorsAmount: true,\n\t\t\t\trequiresAccountSigner: strategy?.requiresRecipientAccount ?? false,\n\t\t\t});\n\t\t}\n\t\treturn out;\n\t});\n\n\tconst fundAccount: DashboardDomain['fundAccount'] = (input) =>\n\t\tEffect.gen(function* () {\n\t\t\tconst recipient = input.recipient.trim();\n\t\t\tif (!SUI_ADDRESS_RE.test(recipient)) {\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\tdetail: `invalid recipient '${input.recipient}': expected a 0x-prefixed Sui address`,\n\t\t\t\t} satisfies DashboardFundResult;\n\t\t\t}\n\t\t\tif (strategyRegistry === null) {\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\tdetail: 'funding unavailable: no strategy registry wired',\n\t\t\t\t} satisfies DashboardFundResult;\n\t\t\t}\n\n\t\t\tconst coinType = input.coinType?.trim() ?? '';\n\t\t\tconst isSui = coinType === '' || coinType === SUI_FULL_COIN_TYPE;\n\n\t\t\t// SUI path — chain faucet strategy. Fixed-amount: we pass a nominal\n\t\t\t// amount but the standard faucet ignores it. Recipient may be any\n\t\t\t// 0x address (no account handle required).\n\t\t\tif (isSui) {\n\t\t\t\tconst chain = yield* readChainId;\n\t\t\t\tif (chain === null) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tok: false,\n\t\t\t\t\t\tdetail: 'cannot fund SUI: no resolved sui plugin / chain id in this stack',\n\t\t\t\t\t} satisfies DashboardFundResult;\n\t\t\t\t}\n\t\t\t\tconst key = faucetCapabilityKeyFor(chain);\n\t\t\t\tconst strategy = yield* strategyRegistry\n\t\t\t\t\t.get<typeof key, FaucetStrategy>(key)\n\t\t\t\t\t.pipe(Effect.catchTag('StrategyNotFoundError', () => Effect.succeed(null)));\n\t\t\t\tif (strategy === null) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tok: false,\n\t\t\t\t\t\tdetail: `no SUI faucet strategy registered for chain '${chain}'`,\n\t\t\t\t\t} satisfies DashboardFundResult;\n\t\t\t\t}\n\t\t\t\t// `amount` is nominal — the standard faucet grant is fixed and\n\t\t\t\t// ignores it. Default to 1 SUI in MIST so a strategy that DID\n\t\t\t\t// honor it lands a sane value.\n\t\t\t\tconst amount = parseAmount(input.amountBaseUnits) ?? 1_000_000_000n;\n\t\t\t\treturn yield* strategy.request({ address: recipient, amount }).pipe(\n\t\t\t\t\tEffect.map(\n\t\t\t\t\t\t(): DashboardFundResult => ({\n\t\t\t\t\t\t\tok: true,\n\t\t\t\t\t\t\tdetail: `requested SUI for ${recipient} (fixed-amount faucet grant)`,\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t\tEffect.catch((cause) =>\n\t\t\t\t\t\tEffect.succeed<DashboardFundResult>({\n\t\t\t\t\t\t\tok: false,\n\t\t\t\t\t\t\tdetail: `SUI faucet request failed: ${faucetCauseDetail(cause)}`,\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t\tEffect.catchCause((cause) =>\n\t\t\t\t\t\tEffect.succeed<DashboardFundResult>({\n\t\t\t\t\t\t\tok: false,\n\t\t\t\t\t\t\tdetail: `SUI faucet request crashed: ${String(cause)}`,\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// WAL/DEEP path — coin-specific account-signed funding strategy.\n\t\t\tconst amount = parseAmount(input.amountBaseUnits);\n\t\t\tif (amount === null) {\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\tdetail: `invalid amountBaseUnits '${input.amountBaseUnits ?? ''}': expected a positive integer string`,\n\t\t\t\t} satisfies DashboardFundResult;\n\t\t\t}\n\n\t\t\tconst key = `coinType:${coinType}` as const;\n\t\t\tconst strategy = yield* strategyRegistry\n\t\t\t\t.get<typeof key, AccountFundingStrategy<unknown, AccountValue>>(key)\n\t\t\t\t.pipe(Effect.catchTag('StrategyNotFoundError', () => Effect.succeed(null)));\n\t\t\tif (strategy === null) {\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\tdetail:\n\t\t\t\t\t\t`no funding strategy registered for coin '${coinType}' — ` +\n\t\t\t\t\t\t'WAL needs the walrus plugin (with an exchange) and DEEP needs the deepbook plugin',\n\t\t\t\t} satisfies DashboardFundResult;\n\t\t\t}\n\n\t\t\t// Match the recipient to a resolved account, if one holds this address.\n\t\t\tconst values = yield* control.resolvedValues;\n\t\t\tconst account = matching(values, (id) => id.startsWith('account/'))\n\t\t\t\t.map(({ value }) => value as AccountShape)\n\t\t\t\t.find((v) => optStr(v.address) === recipient);\n\n\t\t\t// Account-spending strategies (WAL/DEEP) swap the recipient's OWN SUI,\n\t\t\t// so the recipient must BE a resolved account with a signer. Mint-style\n\t\t\t// strategies (managed coins) transfer to a passive recipient, so any 0x\n\t\t\t// address is fine — gate the rejection on the strategy's flag, not on\n\t\t\t// the coin being non-SUI.\n\t\t\tif (strategy.requiresRecipientAccount === true && account === undefined) {\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\tdetail:\n\t\t\t\t\t\t`coin '${coinType}' is funded by an account-signed swap, but '${recipient}' ` +\n\t\t\t\t\t\t'is not a resolved account in this stack — fund a configured account (the swap spends its SUI)',\n\t\t\t\t} satisfies DashboardFundResult;\n\t\t\t}\n\n\t\t\treturn yield* strategy\n\t\t\t\t.request({\n\t\t\t\t\taddress: recipient,\n\t\t\t\t\tamount,\n\t\t\t\t\t...(account === undefined ? {} : { account: account as unknown as AccountValue }),\n\t\t\t\t})\n\t\t\t\t.pipe(\n\t\t\t\t\tEffect.map(\n\t\t\t\t\t\t(): DashboardFundResult => ({\n\t\t\t\t\t\t\tok: true,\n\t\t\t\t\t\t\tdetail: `funded ${amount} base units of ${coinType} to ${recipient}`,\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t\tEffect.catch((cause) =>\n\t\t\t\t\t\tEffect.succeed<DashboardFundResult>({\n\t\t\t\t\t\t\tok: false,\n\t\t\t\t\t\t\tdetail: `${coinSymbolFromType(coinType)} funding failed: ${fundingCauseDetail(cause)}`,\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t\tEffect.catchCause((cause) =>\n\t\t\t\t\t\tEffect.succeed<DashboardFundResult>({\n\t\t\t\t\t\t\tok: false,\n\t\t\t\t\t\t\tdetail: `${coinSymbolFromType(coinType)} funding crashed: ${String(cause)}`,\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t});\n\n\treturn {\n\t\tmode,\n\t\tdeepbook,\n\t\tseal,\n\t\tcoinCaps,\n\t\tmintCoin,\n\t\tfundableCoins,\n\t\tfundAccount,\n\t};\n};\n\n/** An all-empty dashboard domain. Used by tests that exercise the\n * schema/server without a live registry. Every accessor resolves to\n * empty/`null`. */\nexport const emptyDashboardDomain: DashboardDomain = {\n\tmode: Effect.succeed(null),\n\tdeepbook: Effect.succeed([]),\n\tseal: Effect.succeed([]),\n\tcoinCaps: Effect.succeed([]),\n\tmintCoin: () => Effect.succeed({ ok: false, detail: 'unavailable', digest: null }),\n\tfundableCoins: Effect.succeed([]),\n\tfundAccount: () => Effect.succeed({ ok: false, detail: 'unavailable' }),\n};\n"],"mappings":"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"}
|