/** * ctx.state — the scope's infrastructure state, persisted in the scope's * non-rendered `state` attribute (declare it in your schema as a readOnly, * NOT-required object so it never reaches the form). * * Best-effort BY DESIGN: state is a convenience mirror for humans and tooling; * your infrastructure's own tags/labels stay the source of truth. A failed * save reports through onSkip (the SDK wires it to ctx.log.detail) and moves * on — it never fails the action. */ import type { PlatformClient } from "../platform"; import type { ScopeState } from "./context"; export function createScopeState( scopeId: string, platform: PlatformClient, onSkip: (reason: string) => void ): ScopeState { const guard = (fallback: T, fn: () => Promise): Promise => { if (!scopeId) { onSkip("no scope in this action"); return Promise.resolve(fallback); } return fn().catch((error: unknown) => { onSkip(error instanceof Error ? error.message : String(error)); return fallback; }); }; return { read: () => guard({}, async () => { const caps = await platform.getScopeCapabilities(scopeId); return (caps.state as Record) ?? {}; }), merge: (patch) => guard(undefined, async () => { const caps = await platform.getScopeCapabilities(scopeId); const current = (caps.state as Record) ?? {}; const state = { ...current, ...patch, updatedAt: new Date().toISOString(), }; await platform.patchScopeCapabilities(scopeId, { ...caps, state }); }), }; }