From 7f067dd3362cf6287cc2b3739a19ebd95975c45a Mon Sep 17 00:00:00 2001 From: Alex Langenfeld Date: Wed, 30 Sep 2026 16:31:59 -0500 Subject: [PATCH 01/11] docs: add a dynamic workflows cookbook recipe backed by e2e fixtures Signed-off-by: Alex Langenfeld --- .changeset/dynamic-workflows-cookbook.md | 4 + .../cookbook/advanced/dynamic-workflows.mdx | 399 ++++++++++++++++++ .../docs/v5/cookbook/advanced/meta.json | 1 + docs/content/docs/v5/cookbook/index.mdx | 1 + docs/lib/cookbook-tree.ts | 9 + .../core/e2e/e2e-dynamic-workflow.test.ts | 161 ++++++- workbench/example/workflows/99_e2e.ts | 215 ++++++++++ 7 files changed, 789 insertions(+), 1 deletion(-) create mode 100644 .changeset/dynamic-workflows-cookbook.md create mode 100644 docs/content/docs/v5/cookbook/advanced/dynamic-workflows.mdx diff --git a/.changeset/dynamic-workflows-cookbook.md b/.changeset/dynamic-workflows-cookbook.md new file mode 100644 index 0000000000..21728a5155 --- /dev/null +++ b/.changeset/dynamic-workflows-cookbook.md @@ -0,0 +1,4 @@ +--- +--- + +Docs: add a dynamic workflows cookbook recipe, backed by new mission-runner e2e fixtures. diff --git a/docs/content/docs/v5/cookbook/advanced/dynamic-workflows.mdx b/docs/content/docs/v5/cookbook/advanced/dynamic-workflows.mdx new file mode 100644 index 0000000000..dcb7c307fd --- /dev/null +++ b/docs/content/docs/v5/cookbook/advanced/dynamic-workflows.mdx @@ -0,0 +1,399 @@ +--- +title: Dynamic Workflows +description: Run reviewed mission templates over a fixed catalog of deployed steps, started from a route handler. +type: guide +summary: Keep a catalog of ordinary deployed steps and a set of reviewed workflow source templates, start them with start() from a route handler, resume their hooks from another route, and fan them out from a static parent. +prerequisites: + - /docs/advanced/dynamic-workflows +related: + - /docs/api-reference/workflow-api/start + - /docs/api-reference/workflow-api/resume-hook + - /docs/api-reference/workflow-api/get-run + - /cookbook/advanced/child-workflows +--- + + + +Use this pattern when a platform runs orchestration whose shape is chosen after deploy, such as a set of "missions" that operators assemble and review, over a fixed catalog of steps that ship with the app. The steps are ordinary deployed code. Each mission is a short, reviewed JavaScript template that [`start()`](/docs/api-reference/workflow-api/start) runs as a [dynamic workflow](/docs/advanced/dynamic-workflows), with no build or deploy per mission. + + +Dynamic workflows are **experimental**. Before this recipe runs: + +- The deployment must set `WORKFLOW_EXPERIMENTAL_DYNAMIC_WORKFLOWS=1`. Without it, `start()` throws. +- A dynamic run can only start on the deployment that calls `start()`. +- On Vercel, the project must have dynamic-workflow storage enabled. Without it, `start()` throws. + +See [Enabling dynamic workflows](/docs/advanced/dynamic-workflows#enabling-dynamic-workflows) and [World support](/docs/advanced/dynamic-workflows#world-support) for the details. + + +## When to use this + +- **Templates are yours.** Your team writes or reviews every mission template, the same way you would review a pull request. +- **The catalog is fixed.** Every step a mission can call is already deployed. Missions only recombine them. +- **Missions change more often than deploys.** New combinations of existing steps should not need a build. + +This is not a way to run code written by end users, or generated by a model from their input. Dynamic source runs with your deployment's full privileges; see [Security](#security). If your workflows are known at build time, write normal workflow functions instead. + +## The step catalog + +The catalog is a workflow file of ordinary step functions, compiled and deployed with the app. Dynamic source cannot define steps, so everything a mission does outside of orchestration happens here. + +```typescript title="workflows/mission-steps.ts" lineNumbers +import { resumeHook } from "workflow/api"; + +export interface BirdDetection { + id: number; + species: string; + confidence: number; +} + +declare function callVisionModel(image: string, maxDetections: number): Promise; // @setup +declare function pageReviewer(detection: BirdDetection, token: string): Promise; // @setup +declare function postToChannel(message: string): Promise; // @setup + +/** Detects birds in an image with a vision model. */ +export async function detectBirds(request: { image: string; count: number }) { + "use step"; + return await callVisionModel(request.image, request.count); +} + +/** Asks an expert to review one detection. */ +export async function requestExpertReview(detection: BirdDetection, token: string) { + "use step"; + await pageReviewer(detection, token); + return { detectionId: detection.id, token }; +} + +/** Posts a status message. */ +export async function notify(message: string) { + "use step"; + await postToChannel(message); + return { delivered: message }; +} + +/** Fans a child mission's result back into its parent's hook. */ +export async function reportToParent(token: string, report: unknown) { + "use step"; + // Throws, and so retries, until the parent has created its hook. + await resumeHook(token, report); +} +``` + +## Mission templates + +A mission template is a constant source string plus the steps it may call. The bird survey mission asks a model for detections, fans out one expert review per detection, and aggregates the verdicts: + +```typescript title="lib/missions.ts" lineNumbers +import { + detectBirds, + notify, + reportToParent, + requestExpertReview, +} from "@/workflows/mission-steps"; + +const BIRD_SURVEY_SOURCE = ` +async function workflow(input) { + "use workflow"; + const detections = await steps.llm({ image: input.image, count: input.count }); + + const reviews = await Promise.all( + detections.map(async (detection) => { + const token = input.reviewTokenPrefix + ":" + detection.id; + const review = createHook({ token }); + await steps.expertReview(detection, token); + const verdict = await review; + return { ...detection, confirmed: verdict.confirmed === true }; + }) + ); + + const confirmed = reviews.filter((review) => review.confirmed).length; + const result = { image: input.image, confirmed, reviews }; + await steps.notify( + input.image + ": " + confirmed + " of " + reviews.length + " confirmed" + ); + if (input.parentToken) { + await steps.reportToParent(input.parentToken, result); + } + return result; +} +`; + +export const MISSIONS = { + birdSurvey: { + source: BIRD_SURVEY_SOURCE, + // Alias the source calls -> imported step function. + steps: { + llm: detectBirds, // [!code highlight] + expertReview: requestExpertReview, // [!code highlight] + notify, + reportToParent, + }, + }, +}; + +export type MissionId = keyof typeof MISSIONS; + +export function reviewTokenPrefix(missionId: string) { + return `mission:${missionId}:review`; +} + +export function reviewToken(missionId: string, detectionId: number) { + return `${reviewTokenPrefix(missionId)}:${detectionId}`; +} +``` + +A few properties of the template are deliberate: + +- **Every parameter arrives as `input`.** The image, the count, and the hook tokens are run input, never interpolated into the source. The source is fixed, so it is exactly the code that was reviewed, and every run of it shares one [workflow ID](/docs/advanced/dynamic-workflows#workflow-ids). +- **The step map holds imported functions.** The build stamps a `stepId` on each imported step function, and that ID binds `steps.llm` in the source to `detectBirds` in the deployment. The source can only call the aliases in this map; see [Security](#security) for what that does and does not guarantee. +- **Hook tokens are deterministic and unique.** Each review token is derived from a mission ID the server generates, so replay recreates the same hook and concurrent missions never share a token. See [custom tokens](/docs/foundations/hooks#custom-tokens-for-deterministic-hooks). +- **It is plain JavaScript.** Only `steps`, `createHook`, `sleep`, and the usual [workflow globals](/docs/api-reference/workflow-globals) are in scope. See [What the source can use](/docs/advanced/dynamic-workflows#what-the-source-can-use). + +## Start a mission from a route handler + +`start()` accepts the source directly, so a route handler can start a mission with no wrapper workflow. The route picks a template by ID; the request never carries source. + +```typescript title="app/api/missions/route.ts" lineNumbers +import { start } from "workflow/api"; +import { z } from "zod"; +import { MISSIONS, reviewTokenPrefix } from "@/lib/missions"; + +const startRequest = z.object({ + mission: z.literal("birdSurvey"), + image: z.string().min(1).max(200), + count: z.number().int().min(1).max(20), +}); + +export async function POST(request: Request) { + // Authenticate the caller first: a mission runs deployed steps. + const parsed = startRequest.safeParse(await request.json()); + if (!parsed.success) { + return Response.json({ error: parsed.error.message }, { status: 400 }); + } + const { mission, image, count } = parsed.data; + + const missionId = crypto.randomUUID(); + const input = { + image, + count, + reviewTokenPrefix: reviewTokenPrefix(missionId), + }; + + const { source, steps } = MISSIONS[mission]; + const run = await start(source, [input], { // [!code highlight] + experimental_dynamic: { steps }, // [!code highlight] + }); // [!code highlight] + + return Response.json({ runId: run.runId, missionId }); +} +``` + +`start()` validates the source before it writes anything: a template with a syntax error, an `import`, or a missing `"use workflow"` directive throws here rather than failing on a queue delivery. See [Rules for the source](/docs/advanced/dynamic-workflows#rules-for-the-source). + +## Resume a review + +Each review waits on its own hook. The review route rebuilds the token from the mission ID and detection ID, confirms the hook belongs to the run, and resumes it: + +```typescript title="app/api/missions/[runId]/reviews/route.ts" lineNumbers +import { getHookByToken, resumeHook } from "workflow/api"; +import { HookNotFoundError } from "workflow/errors"; +import { z } from "zod"; +import { reviewToken } from "@/lib/missions"; + +declare function assertCanReview(request: Request, missionId: string): Promise; // @setup + +const reviewRequest = z.object({ + missionId: z.string().uuid(), + detectionId: z.number().int().min(0), + confirmed: z.boolean(), +}); + +export async function POST( + request: Request, + { params }: { params: Promise<{ runId: string }> } +) { + const { runId } = await params; + const parsed = reviewRequest.safeParse(await request.json()); + if (!parsed.success) { + return Response.json({ error: parsed.error.message }, { status: 400 }); + } + const { missionId, detectionId, confirmed } = parsed.data; + + // Knowing a token does not authorize the caller. + await assertCanReview(request, missionId); // [!code highlight] + + try { + const hook = await getHookByToken(reviewToken(missionId, detectionId)); + if (hook.runId !== runId) { + return Response.json({ error: "No such review" }, { status: 404 }); + } + await resumeHook(hook, { confirmed }); // [!code highlight] + return Response.json({ ok: true }); + } catch (error) { + if (HookNotFoundError.is(error)) { + return Response.json({ error: "No pending review" }, { status: 404 }); + } + throw error; + } +} +``` + +A review that arrives before its hook exists, or after the mission has finished, gets a 404. See [Authorize before calling `resumeHook()`](/docs/foundations/hooks#authorize-before-calling-resumehook). + +## Report mission status + +A status route reads the run with [`getRun()`](/docs/api-reference/workflow-api/get-run). A dynamic run's result type is `unknown` to TypeScript, so pass the type you expect: + +```typescript title="app/api/missions/[runId]/route.ts" lineNumbers +import { getRun } from "workflow/api"; + +interface BirdSurveyResult { + image: string; + confirmed: number; + reviews: Array<{ id: number; species: string; confirmed: boolean }>; +} + +export async function GET( + _request: Request, + { params }: { params: Promise<{ runId: string }> } +) { + const { runId } = await params; + const run = getRun(runId); + if (!(await run.exists)) { + return Response.json({ error: "Mission not found" }, { status: 404 }); + } + + const status = await run.status; + if (status === "completed") { + return Response.json({ status, result: await run.returnValue }); + } + return Response.json({ status }); +} +``` + +## Variant: approval with a timeout + +A mission can race a hook against a durable [`sleep()`](/docs/api-reference/workflow/sleep), so a request that nobody answers still finishes. Add it to `MISSIONS` next to the bird survey, and pass the token and the timeout as input: + +```typescript title="lib/missions.ts" lineNumbers +import { notify } from "@/workflows/mission-steps"; + +const APPROVAL_SOURCE = ` +async function workflow(input) { + "use workflow"; + await steps.notify("Approval requested: " + input.subject); + + const approval = createHook({ token: input.approvalToken }); + const decision = await Promise.race([ + approval.then((payload) => ({ + approved: payload.approved === true, + timedOut: false, + })), + sleep(input.timeout).then(() => ({ approved: false, timedOut: true })), + ]); + + await steps.notify( + input.subject + ": " + (decision.approved ? "approved" : "not approved") + ); + return decision; +} +`; + +declare const BIRD_SURVEY_MISSION: { source: string; steps: Record }; // @setup +export const MISSIONS = { + // birdSurvey: { ... }, + birdSurvey: BIRD_SURVEY_MISSION, // @setup + approval: { + source: APPROVAL_SOURCE, + steps: { notify }, + }, +}; + +export function approvalToken(missionId: string) { + return `mission:${missionId}:approval`; +} +``` + +Start it the same way, with input such as `{ subject, approvalToken: approvalToken(missionId), timeout: "1d" }`. Resuming the token with `{ approved: true }` returns `{ approved: true, timedOut: false }`; if nobody resumes it within the timeout, the run returns `{ approved: false, timedOut: true }`. See [Timeouts](/cookbook/common-patterns/timeouts) for the general pattern. + +## Fan out at scale + +To run one mission per image across a large batch, start the missions from a static parent workflow and fan back in through hooks, as in [Child Workflows](/cookbook/advanced/child-workflows). Each child mission ends by calling `steps.reportToParent(input.parentToken, result)`, which resumes the hook its parent created for it. + +```typescript title="workflows/bird-survey.ts" lineNumbers +import { createHook } from "workflow"; +import { start } from "workflow/api"; +import { MISSIONS, type MissionId } from "@/lib/missions"; + +interface BirdSurveyResult { + image: string; + confirmed: number; +} + +export async function birdSurvey(surveyId: string, images: string[]) { + "use workflow"; + + const hooks = images.map((_, index) => + createHook({ token: `survey:${surveyId}:mission:${index}` }) + ); + await Promise.all( + images.map((image, index) => + startMission("birdSurvey", { + image, + count: 5, + reviewTokenPrefix: `survey:${surveyId}:review:${index}`, + parentToken: `survey:${surveyId}:mission:${index}`, // [!code highlight] + }) + ) + ); + const results = await Promise.all(hooks); // [!code highlight] + + return { + images: results.length, + confirmed: results.reduce((total, result) => total + result.confirmed, 0), + results, + }; +} + +/** Starts one mission as a dynamic child run. */ +async function startMission(mission: MissionId, input: Record) { + "use step"; + const { source, steps } = MISSIONS[mission]; + const run = await start(source, [input], { experimental_dynamic: { steps } }); + return { runId: run.runId }; +} +``` + +`startMission` is a step, not workflow code: `start()` is a side effect, and the `steps` map holds function references that must not cross a step boundary as serialized data. The parent itself is a normal static workflow, so start it from a route with `start(birdSurvey, [surveyId, images])`, and bound the number of images there. + +The child-workflows recipe covers the rest of the fan-out: [chunked spawning](/cookbook/advanced/child-workflows#fan-out-pattern-chunked-spawning) for hundreds of children, [partial failures](/cookbook/advanced/child-workflows#tolerating-partial-failures), and the parent's own event-log growth. + +## Security + +Dynamic source runs with the full privileges of your deployment's functions; the workflow sandbox is for determinism, not isolation. This recipe is safe because of how the source is chosen, not because of where it runs: + +- **Templates are platform code.** They live in your repository and go through review like any other change. The start route selects one by ID and never accepts source in the request. +- **Parameters are input, not source.** Building source from request fields, even with escaping, turns every caller into a code author. Passing them as `input` keeps the executed code identical to the reviewed code. +- **Model output is data.** `detectBirds` returns detections for the template to iterate over. Do not ask a model to write the mission. +- **The step map is not a boundary.** A reviewed template that calls a missing alias fails its run, but the map does not stop source that is trying to reach other steps or the host. Review is the boundary. +- **Authorize every resume.** Hook tokens route a payload to a run; they do not prove who sent it. + +See [Security](/docs/advanced/dynamic-workflows#security) on the concept page for the full model, including what encryption does and does not protect. + +## Tips + +- **Treat a template's bytes as its identity.** The workflow ID hashes the raw source and the step bindings, so reformatting a template, editing a comment, or rebinding an alias creates a new workflow and splits its runs in observability. Keep templates as constants and change them deliberately. +- **Bind only the steps a template calls.** On Vercel, the bindings count against the run's execution-context limit, and long step IDs cost more than long aliases. See [Rules for the source](/docs/advanced/dynamic-workflows#rules-for-the-source). +- **Cap what callers control.** Bound counts, batch sizes, and string lengths in the start route, since they decide how many steps and hooks a mission creates. +- **Test templates locally, then on a preview.** The Local World runs dynamic workflows, so `WORKFLOW_EXPERIMENTAL_DYNAMIC_WORKFLOWS=1 pnpm dev` checks a template's validation and behavior. It does not enforce Vercel's execution-context limit, so check a template with many bindings on a preview deployment too. + +## Key APIs + +- [`start()`](/docs/api-reference/workflow-api/start): Starts a run from workflow source with `experimental_dynamic.steps`. +- [`createHook()`](/docs/api-reference/workflow/create-hook): Waits for an external signal, such as a review, inside the source. +- [`sleep()`](/docs/api-reference/workflow/sleep): Durable timer, raced against a hook for timeouts. +- [`getHookByToken()`](/docs/api-reference/workflow-api/get-hook-by-token): Looks up a pending hook before resuming it. +- [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook): Delivers a review or approval to the waiting run. +- [`getRun()`](/docs/api-reference/workflow-api/get-run): Reads a mission's status and result. diff --git a/docs/content/docs/v5/cookbook/advanced/meta.json b/docs/content/docs/v5/cookbook/advanced/meta.json index 443536ea64..9a1ed183a3 100644 --- a/docs/content/docs/v5/cookbook/advanced/meta.json +++ b/docs/content/docs/v5/cookbook/advanced/meta.json @@ -2,6 +2,7 @@ "title": "Advanced", "pages": [ "child-workflows", + "dynamic-workflows", "upgrading-workflows", "serializable-steps", "publishing-libraries" diff --git a/docs/content/docs/v5/cookbook/index.mdx b/docs/content/docs/v5/cookbook/index.mdx index dc2ed6dae4..d0a1784e86 100644 --- a/docs/content/docs/v5/cookbook/index.mdx +++ b/docs/content/docs/v5/cookbook/index.mdx @@ -33,6 +33,7 @@ Use these workflow patterns and copy-paste code examples to implement common use ## Advanced - [**Child Workflows**](/cookbook/advanced/child-workflows): Spawn and orchestrate child workflows from a parent +- [**Dynamic Workflows**](/cookbook/advanced/dynamic-workflows): Run reviewed mission templates over a fixed catalog of deployed steps, started from a route handler - [**Upgrading Workflows**](/cookbook/advanced/upgrading-workflows): Identify a clean upgrade point in a long-running workflow and spawn a fresh run on the latest deployment carrying state forward - [**Serializable Steps**](/cookbook/advanced/serializable-steps): Wrap non-serializable third-party objects so they cross the workflow boundary - [**Publishing Libraries**](/cookbook/advanced/publishing-libraries): Ship npm packages that export reusable workflow functions diff --git a/docs/lib/cookbook-tree.ts b/docs/lib/cookbook-tree.ts index 833372489b..b4b7e0d55d 100644 --- a/docs/lib/cookbook-tree.ts +++ b/docs/lib/cookbook-tree.ts @@ -58,6 +58,7 @@ export const slugToCategory: Record = { // Advanced 'child-workflows': 'advanced', + 'dynamic-workflows': 'advanced', 'distributed-abort-controller': 'advanced', 'upgrading-workflows': 'advanced', 'serializable-steps': 'advanced', @@ -185,6 +186,14 @@ export const recipes: Record = { 'Spawn and orchestrate child workflows from a parent, waiting for completion via hook resume and handling partial failures.', category: 'advanced', }, + 'dynamic-workflows': { + slug: 'dynamic-workflows', + title: 'Dynamic Workflows', + description: + 'Run reviewed mission templates over a fixed catalog of deployed steps: start them from a route handler, resume their review hooks, and fan them out from a static parent.', + category: 'advanced', + skipVersions: ['v4'], + }, 'distributed-abort-controller': { slug: 'distributed-abort-controller', title: 'Distributed Abort Controller', diff --git a/packages/core/e2e/e2e-dynamic-workflow.test.ts b/packages/core/e2e/e2e-dynamic-workflow.test.ts index 8d61c0a150..5b165dc80e 100644 --- a/packages/core/e2e/e2e-dynamic-workflow.test.ts +++ b/packages/core/e2e/e2e-dynamic-workflow.test.ts @@ -36,7 +36,7 @@ import { setTimeout as sleep } from 'node:timers/promises'; import { getCurrentTest } from '@vitest/runner'; import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; import type { Run, start as rawStart } from '../src/runtime'; -import { getRun, getWorld } from '../src/runtime'; +import { getHookByToken, getRun, getWorld, resumeHook } from '../src/runtime'; import { getCollectedRunIds, getWorkflowMetadata, @@ -403,3 +403,162 @@ describeJs('dynamic workflows e2e', { timeout: 120_000 }, () => { ).rejects.toThrow(/at least one registered step/); }); }); + +/** + * Resume a hook a dynamic mission creates, once it exists. + * + * Missions create their hooks deep inside a dynamic child, so there is no run + * to wait on first: poll the token until it resolves, then resume it once. + * When a `parent` is given (the fan-out case, where the runner never learns + * the children's run IDs up front), a failed parent is checked against the + * unsupported-deployment refusals, so a lane that cannot run dynamic + * workflows skips here instead of timing out on a hook that will never exist. + */ +async function resumeMissionHook( + token: string, + payload: unknown, + { + parent, + timeoutMs = 60_000, + }: { parent?: Run; timeoutMs?: number } = {} +) { + const deadline = Date.now() + timeoutMs; + let lastError: unknown; + while (Date.now() < deadline) { + try { + const hook = await getHookByToken(token); + await resumeHook(hook, payload); + return hook; + } catch (error) { + lastError = error; + } + if (parent && (await parent.status) === 'failed') { + try { + await parent.returnValue; + } catch (error) { + skipIfUnsupportedDeployment(error); + } + } + await sleep(500); + } + throw new Error( + `Timed out after ${timeoutMs}ms waiting for hook ${token}. Last error: ${String(lastError)}` + ); +} + +/** + * The mission runner in the "Dynamic Workflows" cookbook recipe. Its fixtures + * (`dynamicBirdSurveyMission` and friends in `workflows/99_e2e.ts`) hold the + * recipe's step catalog and mission sources. + */ +describeJs( + 'dynamic workflows: mission runner recipe', + { timeout: 120_000 }, + () => { + it('fans a bird survey out to one review hook per detection', async () => { + const prefix = `e2e-dynamic-review:${crypto.randomUUID()}`; + const { childRunId } = await startParent('dynamicBirdSurveyMission', [ + 'backyard.jpg', + 3, + prefix, + ]); + + // Every review waits on its own hook, in parallel; resume them out of + // order to show nothing depends on arrival order. + const verdicts = [true, false, true]; + for (const id of [2, 0, 1]) { + const hook = await resumeMissionHook(`${prefix}:${id}`, { + confirmed: verdicts[id], + }); + expect(hook.runId).toBe(childRunId); + } + + const child = await awaitChildRun(childRunId); + expect(child.output).toEqual({ + image: 'backyard.jpg', + confirmed: 2, + reviews: [ + { + id: 0, + species: 'American robin', + confidence: 0.9, + confirmed: true, + }, + { id: 1, species: 'Blue jay', confidence: 0.89, confirmed: false }, + { + id: 2, + species: 'Northern cardinal', + confidence: 0.88, + confirmed: true, + }, + ], + }); + expect(child.record.workflowName).toMatch( + /^workflow\/\/dynamic\/[0-9a-f]{32}\/\/workflow$/ + ); + }); + + it('resolves an approval mission when the hook is resumed', async () => { + const token = `e2e-dynamic-approval:${crypto.randomUUID()}`; + const { childRunId } = await startParent('dynamicApprovalMission', [ + 'release notes', + token, + '10m', + ]); + + const hook = await resumeMissionHook(token, { approved: true }); + expect(hook.runId).toBe(childRunId); + + const child = await awaitChildRun(childRunId); + expect(child.output).toEqual({ approved: true, timedOut: false }); + }); + + it('times out an approval mission nobody resumes', async () => { + const token = `e2e-dynamic-approval:${crypto.randomUUID()}`; + const { childRunId } = await startParent('dynamicApprovalMission', [ + 'release notes', + token, + '2s', + ]); + + // The durable sleep wins the race, so the run completes with no resume. + const child = await awaitChildRun(childRunId); + expect(child.output).toEqual({ approved: false, timedOut: true }); + }); + + it('fans missions out from a static parent and back in through hooks', async () => { + const surveyKey = `e2e-dynamic-survey:${crypto.randomUUID()}`; + const parent = await start(await e2e('dynamicBirdSurveyFanOut'), [ + surveyKey, + ['north.jpg', 'south.jpg'], + ]); + + // Each child mission makes one detection and waits on its review. + for (const [index, confirmed] of [true, false].entries()) { + const hook = await resumeMissionHook( + `${surveyKey}:review:${index}:0`, + { confirmed }, + { parent } + ); + trackRun(getRun(hook.runId), { + testName: `${getCurrentTest()?.name ?? 'dynamic workflow'} [dynamic run]`, + }); + } + + let output: unknown; + try { + output = await parent.returnValue; + } catch (error) { + skipIfUnsupportedDeployment(error); + } + expect(output).toMatchObject({ + images: 2, + confirmed: 1, + results: [ + { image: 'north.jpg', confirmed: 1 }, + { image: 'south.jpg', confirmed: 0 }, + ], + }); + }); + } +); diff --git a/workbench/example/workflows/99_e2e.ts b/workbench/example/workflows/99_e2e.ts index 3c78756b0a..e1b843a8cf 100644 --- a/workbench/example/workflows/99_e2e.ts +++ b/workbench/example/workflows/99_e2e.ts @@ -4241,3 +4241,218 @@ export async function dynamicWorkflowDisallowedStep(value: number) { const child = await startDisallowedDynamicRun(value); return { childRunId: child.runId }; } + +////////////////////////////////////////////////////////// +// Dynamic workflows: mission runner +// +// Backs the "Dynamic Workflows" cookbook recipe +// (docs/content/docs/v5/cookbook/advanced/dynamic-workflows.mdx). The step +// catalog, the two mission sources, their step bindings, and the fan-out +// parent are the recipe's code; keep the two in sync. The recipe starts +// missions from a route handler; here `startMission` is a step, because the +// e2e runner can only reach the deployment through a static workflow. +////////////////////////////////////////////////////////// + +interface BirdDetection { + id: number; + species: string; + confidence: number; +} + +const BIRD_SPECIES = [ + 'American robin', + 'Blue jay', + 'Northern cardinal', + 'Mourning dove', +] as const; + +/** + * Catalog step standing in for a vision-model call. Deterministic, so the + * runner knows which review hooks the mission will create. + */ +async function detectBirds(request: { + image: string; + count: number; +}): Promise { + 'use step'; + return Array.from({ length: request.count }, (_, id) => ({ + id, + species: BIRD_SPECIES[id % BIRD_SPECIES.length], + confidence: 0.9 - id / 100, + })); +} + +/** Catalog step: asks a reviewer to resume `token`. */ +async function requestExpertReview(detection: BirdDetection, token: string) { + 'use step'; + return { detectionId: detection.id, token }; +} + +/** Catalog step: posts a status message. */ +async function notify(message: string) { + 'use step'; + return { delivered: message }; +} + +/** Catalog step: fans a child mission's result back into its parent. */ +async function reportToParent(token: string, report: unknown) { + 'use step'; + // Throws, and so retries, until the parent has created its hook. + await resumeHook(token, report); +} + +/** + * A reviewed mission template: detect birds in an image, fan out one expert + * review per detection (each waiting on its own hook), then aggregate. Every + * parameter arrives as `input`, so the source never changes and every run of + * it shares one workflow ID. + */ +const BIRD_SURVEY_SOURCE = ` +async function workflow(input) { + "use workflow"; + const detections = await steps.llm({ image: input.image, count: input.count }); + + const reviews = await Promise.all( + detections.map(async (detection) => { + const token = input.reviewTokenPrefix + ":" + detection.id; + const review = createHook({ token }); + await steps.expertReview(detection, token); + const verdict = await review; + return { ...detection, confirmed: verdict.confirmed === true }; + }) + ); + + const confirmed = reviews.filter((review) => review.confirmed).length; + const result = { image: input.image, confirmed, reviews }; + await steps.notify( + input.image + ": " + confirmed + " of " + reviews.length + " confirmed" + ); + if (input.parentToken) { + await steps.reportToParent(input.parentToken, result); + } + return result; +} +`; + +/** A reviewed approval template: a hook raced against a durable timeout. */ +const APPROVAL_SOURCE = ` +async function workflow(input) { + "use workflow"; + await steps.notify("Approval requested: " + input.subject); + + const approval = createHook({ token: input.approvalToken }); + const decision = await Promise.race([ + approval.then((payload) => ({ + approved: payload.approved === true, + timedOut: false, + })), + sleep(input.timeout).then(() => ({ approved: false, timedOut: true })), + ]); + + await steps.notify( + input.subject + ": " + (decision.approved ? "approved" : "not approved") + ); + return decision; +} +`; + +/** Mission templates by ID: reviewed source plus the steps it may call. */ +const MISSIONS = { + birdSurvey: { + source: BIRD_SURVEY_SOURCE, + steps: { + llm: detectBirds, + expertReview: requestExpertReview, + notify, + reportToParent, + }, + }, + approval: { + source: APPROVAL_SOURCE, + steps: { notify }, + }, +}; + +type MissionId = keyof typeof MISSIONS; + +/** + * Starts one mission as a dynamic run. The recipe makes this same `start()` + * call from its route handler; either way it needs the imported step + * functions, which carry the `stepId` the source's aliases bind to. + */ +async function startMission( + mission: MissionId, + input: Record +) { + 'use step'; + const { source, steps } = MISSIONS[mission]; + const run = await start(source, [input], { experimental_dynamic: { steps } }); + return { runId: run.runId }; +} + +/** Starts a bird-survey mission and hands back the child's run ID. */ +export async function dynamicBirdSurveyMission( + image: string, + count: number, + reviewTokenPrefix: string +) { + 'use workflow'; + const child = await startMission('birdSurvey', { + image, + count, + reviewTokenPrefix, + }); + return { childRunId: child.runId }; +} + +/** Starts an approval mission and hands back the child's run ID. */ +export async function dynamicApprovalMission( + subject: string, + approvalToken: string, + timeout: string +) { + 'use workflow'; + const child = await startMission('approval', { + subject, + approvalToken, + timeout, + }); + return { childRunId: child.runId }; +} + +interface BirdSurveyResult { + image: string; + confirmed: number; + reviews: Array; +} + +/** + * Static parent that fans out one dynamic bird-survey mission per image and + * fans back in through hooks: each child resumes the hook the parent created + * for it, as in the child-workflows recipe. + */ +export async function dynamicBirdSurveyFanOut( + surveyKey: string, + images: string[] +) { + 'use workflow'; + const hooks = images.map((_, index) => + createHook({ token: `${surveyKey}:mission:${index}` }) + ); + await Promise.all( + images.map((image, index) => + startMission('birdSurvey', { + image, + count: 1, + reviewTokenPrefix: `${surveyKey}:review:${index}`, + parentToken: `${surveyKey}:mission:${index}`, + }) + ) + ); + const results = await Promise.all(hooks); + return { + images: results.length, + confirmed: results.reduce((total, result) => total + result.confirmed, 0), + results, + }; +} From a8e838e7bc3ce83aadff887b9c740bd580bc0893 Mon Sep 17 00:00:00 2001 From: Alex Langenfeld Date: Wed, 30 Sep 2026 17:29:30 -0500 Subject: [PATCH 02/11] docs: rework the dynamic workflows recipe around missions published after deploy Signed-off-by: Alex Langenfeld --- .changeset/dynamic-workflows-cookbook.md | 2 +- .../cookbook/advanced/dynamic-workflows.mdx | 442 +++++++++++------- docs/content/docs/v5/cookbook/index.mdx | 2 +- docs/lib/cookbook-tree.ts | 2 +- .../core/e2e/e2e-dynamic-workflow.test.ts | 231 +++++++-- workbench/example/workflows/99_e2e.ts | 215 ++++----- 6 files changed, 560 insertions(+), 334 deletions(-) diff --git a/.changeset/dynamic-workflows-cookbook.md b/.changeset/dynamic-workflows-cookbook.md index 21728a5155..1684382723 100644 --- a/.changeset/dynamic-workflows-cookbook.md +++ b/.changeset/dynamic-workflows-cookbook.md @@ -1,4 +1,4 @@ --- --- -Docs: add a dynamic workflows cookbook recipe, backed by new mission-runner e2e fixtures. +Docs: add a dynamic workflows cookbook recipe for publishing reviewed missions after deploy, backed by e2e fixtures. diff --git a/docs/content/docs/v5/cookbook/advanced/dynamic-workflows.mdx b/docs/content/docs/v5/cookbook/advanced/dynamic-workflows.mdx index dcb7c307fd..169ebc76ab 100644 --- a/docs/content/docs/v5/cookbook/advanced/dynamic-workflows.mdx +++ b/docs/content/docs/v5/cookbook/advanced/dynamic-workflows.mdx @@ -1,8 +1,8 @@ --- title: Dynamic Workflows -description: Run reviewed mission templates over a fixed catalog of deployed steps, started from a route handler. +description: Publish reviewed workflow source after deploy and run it on an unchanged deployment, over a fixed catalog of deployed steps. type: guide -summary: Keep a catalog of ordinary deployed steps and a set of reviewed workflow source templates, start them with start() from a route handler, resume their hooks from another route, and fan them out from a static parent. +summary: Deploy a step catalog and a mission runner once, store approved mission revisions, publish a new mission through a privileged operator path, and start it with start() on the same deployment without a rebuild. prerequisites: - /docs/advanced/dynamic-workflows related: @@ -13,10 +13,10 @@ related: --- -Use this pattern when a platform runs orchestration whose shape is chosen after deploy, such as a set of "missions" that operators assemble and review, over a fixed catalog of steps that ship with the app. The steps are ordinary deployed code. Each mission is a short, reviewed JavaScript template that [`start()`](/docs/api-reference/workflow-api/start) runs as a [dynamic workflow](/docs/advanced/dynamic-workflows), with no build or deploy per mission. +Use this pattern when the orchestration a platform runs has to change after deploy, but every side effect it performs does not. The deployment ships a fixed catalog of steps and a small runner. Operators then publish reviewed **missions**, short JavaScript workflows over that catalog, to a store. The runner loads a mission and starts it with [`start()`](/docs/api-reference/workflow-api/start) as a [dynamic workflow](/docs/advanced/dynamic-workflows). Publishing a new mission needs no rebuild or redeploy. Dynamic workflows are **experimental**. Before this recipe runs: @@ -28,17 +28,18 @@ Dynamic workflows are **experimental**. Before this recipe runs: See [Enabling dynamic workflows](/docs/advanced/dynamic-workflows#enabling-dynamic-workflows) and [World support](/docs/advanced/dynamic-workflows#world-support) for the details. -## When to use this +## How it fits together -- **Templates are yours.** Your team writes or reviews every mission template, the same way you would review a pull request. -- **The catalog is fixed.** Every step a mission can call is already deployed. Missions only recombine them. -- **Missions change more often than deploys.** New combinations of existing steps should not need a build. +1. **Deploy once.** The deployment contains the step catalog, the runner, and the publish, start, and review routes. It contains no missions. +2. **Publish mission A.** An operator publishes a reviewed revision of a mission that sends every bird detection to an expert. The store keeps it. +3. **Publish mission B.** Later, an operator publishes a second mission with a different shape: it accepts confident detections on its own and sends only the rest for review. Nothing is rebuilt. +4. **Start either mission** on the same, unchanged deployment. The start route loads the stored revision and runs it. -This is not a way to run code written by end users, or generated by a model from their input. Dynamic source runs with your deployment's full privileges; see [Security](#security). If your workflows are known at build time, write normal workflow functions instead. +If the set of workflows is known at build time, write normal workflow functions and pick one in code instead. They have better types and errors, and no source to store or review separately. ## The step catalog -The catalog is a workflow file of ordinary step functions, compiled and deployed with the app. Dynamic source cannot define steps, so everything a mission does outside of orchestration happens here. +The catalog is a workflow file of ordinary step functions, deployed with the app. Dynamic source cannot define steps, so everything a mission does outside of orchestration happens here. ```typescript title="workflows/mission-steps.ts" lineNumbers import { resumeHook } from "workflow/api"; @@ -53,20 +54,17 @@ declare function callVisionModel(image: string, maxDetections: number): Promise< declare function pageReviewer(detection: BirdDetection, token: string): Promise; // @setup declare function postToChannel(message: string): Promise; // @setup -/** Detects birds in an image with a vision model. */ export async function detectBirds(request: { image: string; count: number }) { "use step"; return await callVisionModel(request.image, request.count); } -/** Asks an expert to review one detection. */ export async function requestExpertReview(detection: BirdDetection, token: string) { "use step"; await pageReviewer(detection, token); return { detectionId: detection.id, token }; } -/** Posts a status message. */ export async function notify(message: string) { "use step"; await postToChannel(message); @@ -81,11 +79,9 @@ export async function reportToParent(token: string, report: unknown) { } ``` -## Mission templates +The runner names the catalog by step name and gives it a version. Bump the version whenever a step changes in a way an existing mission would notice, such as a new argument shape or a different return value. -A mission template is a constant source string plus the steps it may call. The bird survey mission asks a model for detections, fans out one expert review per detection, and aggregates the verdicts: - -```typescript title="lib/missions.ts" lineNumbers +```typescript title="lib/mission-catalog.ts" lineNumbers import { detectBirds, notify, @@ -93,7 +89,62 @@ import { requestExpertReview, } from "@/workflows/mission-steps"; -const BIRD_SURVEY_SOURCE = ` +export const MISSION_CATALOG = { + detectBirds, + requestExpertReview, + notify, + reportToParent, +}; + +export const MISSION_CATALOG_VERSION = "birds@1"; + +export type CatalogStepName = keyof typeof MISSION_CATALOG; +``` + +## Mission revisions + +The store holds **immutable revisions**. A stable, app-level mission ID such as `triage` names the mission; each publish adds a revision under it, and nothing edits a revision in place. A revision records exactly what was approved: + +```typescript title="lib/mission-store.ts" lineNumbers +import type { CatalogStepName } from "@/lib/mission-catalog"; + +export interface MissionRevision { + missionId: string; + revision: number; + /** The exact approved bytes. */ + source: string; + sourceSha256: string; + /** Alias the source calls -> catalog step name. */ + steps: Record; + /** The catalog version the source was reviewed against. */ + catalogVersion: string; + approval: { approvedBy: string; approvedAt: string; reviewUrl: string }; +} + +/** Any durable store works: a database table, a KV namespace, a bucket. */ +export interface MissionStore { + latest(missionId: string): Promise; + get(missionId: string, revision: number): Promise; + /** Appends the next revision; never overwrites one. */ + append(revision: Omit): Promise; +} + +export async function sha256Hex(text: string) { + const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text)); + return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join(""); +} +``` + +Keep the mission ID separate from the workflow ID. The SDK derives the [workflow ID](/docs/advanced/dynamic-workflows#workflow-ids) from a hash of the raw source bytes and the step bindings, so reformatting a mission, editing a comment, or rebinding an alias gives its runs a new workflow ID. The mission ID is what stays stable across revisions. + +## The two missions + +Missions are plain JavaScript: a top-level `async function workflow(input)` that opens with `"use workflow"` and reaches the catalog only through `steps`. `createHook`, `sleep`, and the usual [workflow globals](/docs/api-reference/workflow-globals) are in scope; see [What the source can use](/docs/advanced/dynamic-workflows#what-the-source-can-use). + +Mission A sends every detection to an expert and waits on one hook per detection: + +{/* @skip-typecheck: dynamic workflow source, evaluated with injected steps and createHook */} +```javascript title="missions/review-all.js" lineNumbers async function workflow(input) { "use workflow"; const detections = await steps.llm({ image: input.image, count: input.count }); @@ -104,64 +155,170 @@ async function workflow(input) { const review = createHook({ token }); await steps.expertReview(detection, token); const verdict = await review; - return { ...detection, confirmed: verdict.confirmed === true }; + return { ...detection, confirmed: verdict.confirmed === true, reviewedBy: "expert" }; }) ); const confirmed = reviews.filter((review) => review.confirmed).length; const result = { image: input.image, confirmed, reviews }; - await steps.notify( - input.image + ": " + confirmed + " of " + reviews.length + " confirmed" - ); + await steps.notify(input.image + ": " + confirmed + " of " + reviews.length + " confirmed"); if (input.parentToken) { await steps.reportToParent(input.parentToken, result); } return result; } -`; - -export const MISSIONS = { - birdSurvey: { - source: BIRD_SURVEY_SOURCE, - // Alias the source calls -> imported step function. - steps: { - llm: detectBirds, // [!code highlight] - expertReview: requestExpertReview, // [!code highlight] - notify, - reportToParent, - }, - }, -}; +``` + +Mission B, published later, accepts detections at or above `input.autoAcceptAbove` without a review, and gives each remaining review until `input.reviewTimeout` before counting it as unconfirmed: -export type MissionId = keyof typeof MISSIONS; +{/* @skip-typecheck: dynamic workflow source, evaluated with injected steps, createHook, and sleep */} +```javascript title="missions/triage.js" lineNumbers +async function workflow(input) { + "use workflow"; + const detections = await steps.llm({ image: input.image, count: input.count }); -export function reviewTokenPrefix(missionId: string) { - return `mission:${missionId}:review`; + const reviews = await Promise.all( + detections.map(async (detection) => { + if (detection.confidence >= input.autoAcceptAbove) { // [!code highlight] + return { ...detection, confirmed: true, reviewedBy: "auto" }; // [!code highlight] + } // [!code highlight] + const token = input.reviewTokenPrefix + ":" + detection.id; + const review = createHook({ token }); + await steps.expertReview(detection, token); + const verdict = await Promise.race([ // [!code highlight] + review, // [!code highlight] + sleep(input.reviewTimeout).then(() => null), // [!code highlight] + ]); // [!code highlight] + if (verdict === null) { + return { ...detection, confirmed: false, reviewedBy: "timeout" }; + } + return { ...detection, confirmed: verdict.confirmed === true, reviewedBy: "expert" }; + }) + ); + + const confirmed = reviews.filter((review) => review.confirmed).length; + const result = { image: input.image, confirmed, reviews }; + await steps.notify(input.image + ": " + confirmed + " of " + reviews.length + " confirmed"); + if (input.parentToken) { + await steps.reportToParent(input.parentToken, result); + } + return result; } +``` + +Both missions bind the same aliases: `llm` to `detectBirds`, `expertReview` to `requestExpertReview`, `notify`, and `reportToParent`. Every parameter, including the threshold, the timeout, and the hook tokens, arrives as `input`. The stored source is never templated, so the code that runs is byte for byte the code that was reviewed. Hook tokens are derived from a mission-run ID the server generates, so replay recreates the same hooks and concurrent runs never share a token; see [custom tokens](/docs/foundations/hooks#custom-tokens-for-deterministic-hooks). + +## Publish a revision + +Publishing is a privileged operator action, not part of the public API. A typical setup keeps missions in a reviewed repository and lets its CI job call this route after a change merges: + +```typescript title="app/api/admin/missions/route.ts" lineNumbers +import { z } from "zod"; +import { MISSION_CATALOG, MISSION_CATALOG_VERSION } from "@/lib/mission-catalog"; +import { sha256Hex, type MissionStore } from "@/lib/mission-store"; + +declare const missionStore: MissionStore; // @setup +declare function assertOperator(request: Request): Promise; // @setup + +const publishRequest = z.object({ + missionId: z.string().regex(/^[a-z][a-z0-9-]{0,62}$/), + source: z.string().max(128 * 1024), + steps: z.record(z.string(), z.string()), + catalogVersion: z.string(), + approval: z.object({ + approvedBy: z.string().min(1), + approvedAt: z.string().datetime(), + reviewUrl: z.string().url(), + }), +}); + +export async function POST(request: Request) { + await assertOperator(request); // [!code highlight] + + const parsed = publishRequest.safeParse(await request.json()); + if (!parsed.success) { + return Response.json({ error: parsed.error.message }, { status: 400 }); + } + const { steps, catalogVersion, ...mission } = parsed.data; + + if (catalogVersion !== MISSION_CATALOG_VERSION) { + return Response.json( + { error: `This deployment serves catalog ${MISSION_CATALOG_VERSION}` }, + { status: 409 } + ); + } + for (const name of Object.values(steps)) { + if (!Object.hasOwn(MISSION_CATALOG, name)) { + return Response.json({ error: `Unknown catalog step "${name}"` }, { status: 400 }); + } + } -export function reviewToken(missionId: string, detectionId: number) { - return `${reviewTokenPrefix(missionId)}:${detectionId}`; + const revision = await missionStore.append({ + ...mission, + steps: steps as Record, + catalogVersion, + sourceSha256: await sha256Hex(mission.source), + }); + return Response.json({ missionId: revision.missionId, revision: revision.revision }); } ``` -A few properties of the template are deliberate: +These checks confirm the revision is well formed and fits this deployment's catalog. They are not a review: nothing here can tell whether a mission does what its approver thinks it does. The review is the approval recorded with it. The SDK validates the source shape (the `workflow` declaration, the directive, no imports) when `start()` is called; there is no separate validation API, so a pipeline that wants that check before approval can start the mission once on a preview deployment. + +## Start a mission + +The start route loads the latest revision, checks it again, and starts it. Checking at start as well as at publish catches a record that was edited after approval and a revision published against a catalog this deployment no longer serves. The checks throw [`FatalError`](/docs/api-reference/workflow/fatal-error) so that, when a step calls them (as in [Fan out at scale](#fan-out-at-scale)), a refused revision fails at once instead of retrying. + +```typescript title="lib/start-mission.ts" lineNumbers +import { FatalError } from "workflow"; +import { start } from "workflow/api"; +import { MISSION_CATALOG, MISSION_CATALOG_VERSION } from "@/lib/mission-catalog"; +import { sha256Hex, type MissionRevision } from "@/lib/mission-store"; -- **Every parameter arrives as `input`.** The image, the count, and the hook tokens are run input, never interpolated into the source. The source is fixed, so it is exactly the code that was reviewed, and every run of it shares one [workflow ID](/docs/advanced/dynamic-workflows#workflow-ids). -- **The step map holds imported functions.** The build stamps a `stepId` on each imported step function, and that ID binds `steps.llm` in the source to `detectBirds` in the deployment. The source can only call the aliases in this map; see [Security](#security) for what that does and does not guarantee. -- **Hook tokens are deterministic and unique.** Each review token is derived from a mission ID the server generates, so replay recreates the same hook and concurrent missions never share a token. See [custom tokens](/docs/foundations/hooks#custom-tokens-for-deterministic-hooks). -- **It is plain JavaScript.** Only `steps`, `createHook`, `sleep`, and the usual [workflow globals](/docs/api-reference/workflow-globals) are in scope. See [What the source can use](/docs/advanced/dynamic-workflows#what-the-source-can-use). +export async function bindMissionRevision(revision: MissionRevision) { + const label = `${revision.missionId}@${revision.revision}`; + if (!revision.approval?.approvedBy) { + throw new FatalError(`Mission ${label} has no approval record`); + } + if ((await sha256Hex(revision.source)) !== revision.sourceSha256) { + throw new FatalError(`Mission ${label} does not match its approved source`); + } + if (revision.catalogVersion !== MISSION_CATALOG_VERSION) { + throw new FatalError(`Mission ${label} targets catalog ${revision.catalogVersion}`); + } + const steps: Record = {}; + for (const alias of Object.keys(revision.steps)) { + const name = revision.steps[alias]; + if (!Object.hasOwn(MISSION_CATALOG, name)) { + throw new FatalError(`Mission ${label} binds unknown step "${name}"`); + } + steps[alias] = MISSION_CATALOG[name]; // [!code highlight] + } + return steps; +} -## Start a mission from a route handler +export async function startMissionRevision( + revision: MissionRevision, + input: Record +) { + const steps = await bindMissionRevision(revision); + return await start(revision.source, [input], { // [!code highlight] + experimental_dynamic: { steps }, // [!code highlight] + }); // [!code highlight] +} +``` -`start()` accepts the source directly, so a route handler can start a mission with no wrapper workflow. The route picks a template by ID; the request never carries source. +The step map holds the **imported** step functions. The build stamps a `stepId` on each one, and that ID is what binds `steps.llm` in the source to `detectBirds` in this deployment. ```typescript title="app/api/missions/route.ts" lineNumbers -import { start } from "workflow/api"; import { z } from "zod"; -import { MISSIONS, reviewTokenPrefix } from "@/lib/missions"; +import type { MissionStore } from "@/lib/mission-store"; +import { startMissionRevision } from "@/lib/start-mission"; + +declare const missionStore: MissionStore; // @setup const startRequest = z.object({ - mission: z.literal("birdSurvey"), + missionId: z.string(), image: z.string().min(1).max(200), count: z.number().int().min(1).max(20), }); @@ -172,40 +329,46 @@ export async function POST(request: Request) { if (!parsed.success) { return Response.json({ error: parsed.error.message }, { status: 400 }); } - const { mission, image, count } = parsed.data; + const { missionId, image, count } = parsed.data; + + const revision = await missionStore.latest(missionId); + if (!revision) { + return Response.json({ error: "Unknown mission" }, { status: 404 }); + } - const missionId = crypto.randomUUID(); - const input = { + const missionRunId = crypto.randomUUID(); + const run = await startMissionRevision(revision, { image, count, - reviewTokenPrefix: reviewTokenPrefix(missionId), - }; - - const { source, steps } = MISSIONS[mission]; - const run = await start(source, [input], { // [!code highlight] - experimental_dynamic: { steps }, // [!code highlight] - }); // [!code highlight] - - return Response.json({ runId: run.runId, missionId }); + reviewTokenPrefix: `mission-run:${missionRunId}:review`, + // Read only by missions that use them. + autoAcceptAbove: 0.9, + reviewTimeout: "1d", + }); + + return Response.json({ + runId: run.runId, + missionRunId, + revision: revision.revision, + }); } ``` -`start()` validates the source before it writes anything: a template with a syntax error, an `import`, or a missing `"use workflow"` directive throws here rather than failing on a queue delivery. See [Rules for the source](/docs/advanced/dynamic-workflows#rules-for-the-source). +Record the revision a run started from alongside its run ID, so every result can be traced to the exact approved source. ## Resume a review -Each review waits on its own hook. The review route rebuilds the token from the mission ID and detection ID, confirms the hook belongs to the run, and resumes it: +Each review waits on its own hook. The review route rebuilds the token from the mission-run ID and the detection ID, confirms the hook belongs to the run, and resumes it: ```typescript title="app/api/missions/[runId]/reviews/route.ts" lineNumbers import { getHookByToken, resumeHook } from "workflow/api"; import { HookNotFoundError } from "workflow/errors"; import { z } from "zod"; -import { reviewToken } from "@/lib/missions"; -declare function assertCanReview(request: Request, missionId: string): Promise; // @setup +declare function assertCanReview(request: Request, missionRunId: string): Promise; // @setup const reviewRequest = z.object({ - missionId: z.string().uuid(), + missionRunId: z.string().uuid(), detectionId: z.number().int().min(0), confirmed: z.boolean(), }); @@ -219,13 +382,13 @@ export async function POST( if (!parsed.success) { return Response.json({ error: parsed.error.message }, { status: 400 }); } - const { missionId, detectionId, confirmed } = parsed.data; + const { missionRunId, detectionId, confirmed } = parsed.data; // Knowing a token does not authorize the caller. - await assertCanReview(request, missionId); // [!code highlight] + await assertCanReview(request, missionRunId); // [!code highlight] try { - const hook = await getHookByToken(reviewToken(missionId, detectionId)); + const hook = await getHookByToken(`mission-run:${missionRunId}:review:${detectionId}`); if (hook.runId !== runId) { return Response.json({ error: "No such review" }, { status: 404 }); } @@ -240,107 +403,33 @@ export async function POST( } ``` -A review that arrives before its hook exists, or after the mission has finished, gets a 404. See [Authorize before calling `resumeHook()`](/docs/foundations/hooks#authorize-before-calling-resumehook). - -## Report mission status - -A status route reads the run with [`getRun()`](/docs/api-reference/workflow-api/get-run). A dynamic run's result type is `unknown` to TypeScript, so pass the type you expect: - -```typescript title="app/api/missions/[runId]/route.ts" lineNumbers -import { getRun } from "workflow/api"; +A review for a detection mission B accepted on its own, or one that arrives after its timeout, gets a 404: that hook never existed or is already gone. See [Authorize before calling `resumeHook()`](/docs/foundations/hooks#authorize-before-calling-resumehook). -interface BirdSurveyResult { - image: string; - confirmed: number; - reviews: Array<{ id: number; species: string; confirmed: boolean }>; -} - -export async function GET( - _request: Request, - { params }: { params: Promise<{ runId: string }> } -) { - const { runId } = await params; - const run = getRun(runId); - if (!(await run.exists)) { - return Response.json({ error: "Mission not found" }, { status: 404 }); - } - - const status = await run.status; - if (status === "completed") { - return Response.json({ status, result: await run.returnValue }); - } - return Response.json({ status }); -} -``` - -## Variant: approval with a timeout - -A mission can race a hook against a durable [`sleep()`](/docs/api-reference/workflow/sleep), so a request that nobody answers still finishes. Add it to `MISSIONS` next to the bird survey, and pass the token and the timeout as input: - -```typescript title="lib/missions.ts" lineNumbers -import { notify } from "@/workflows/mission-steps"; - -const APPROVAL_SOURCE = ` -async function workflow(input) { - "use workflow"; - await steps.notify("Approval requested: " + input.subject); - - const approval = createHook({ token: input.approvalToken }); - const decision = await Promise.race([ - approval.then((payload) => ({ - approved: payload.approved === true, - timedOut: false, - })), - sleep(input.timeout).then(() => ({ approved: false, timedOut: true })), - ]); - - await steps.notify( - input.subject + ": " + (decision.approved ? "approved" : "not approved") - ); - return decision; -} -`; - -declare const BIRD_SURVEY_MISSION: { source: string; steps: Record }; // @setup -export const MISSIONS = { - // birdSurvey: { ... }, - birdSurvey: BIRD_SURVEY_MISSION, // @setup - approval: { - source: APPROVAL_SOURCE, - steps: { notify }, - }, -}; - -export function approvalToken(missionId: string) { - return `mission:${missionId}:approval`; -} -``` - -Start it the same way, with input such as `{ subject, approvalToken: approvalToken(missionId), timeout: "1d" }`. Resuming the token with `{ approved: true }` returns `{ approved: true, timedOut: false }`; if nobody resumes it within the timeout, the run returns `{ approved: false, timedOut: true }`. See [Timeouts](/cookbook/common-patterns/timeouts) for the general pattern. +To report status, read the run with [`getRun()`](/docs/api-reference/workflow-api/get-run). A dynamic run's result type is `unknown` to TypeScript, so pass the type you expect, as in `getRun(runId)`. ## Fan out at scale -To run one mission per image across a large batch, start the missions from a static parent workflow and fan back in through hooks, as in [Child Workflows](/cookbook/advanced/child-workflows). Each child mission ends by calling `steps.reportToParent(input.parentToken, result)`, which resumes the hook its parent created for it. +To run a mission once per image across a large batch, start the runs from a static parent workflow and fan back in through hooks, as in [Child Workflows](/cookbook/advanced/child-workflows). Each mission ends with `steps.reportToParent(input.parentToken, result)` when it was given a parent token, which resumes the hook the parent created for it. ```typescript title="workflows/bird-survey.ts" lineNumbers import { createHook } from "workflow"; -import { start } from "workflow/api"; -import { MISSIONS, type MissionId } from "@/lib/missions"; +import type { MissionRevision } from "@/lib/mission-store"; +import { startMissionRevision } from "@/lib/start-mission"; -interface BirdSurveyResult { +interface MissionResult { image: string; confirmed: number; } -export async function birdSurvey(surveyId: string, images: string[]) { +export async function birdSurvey(revision: MissionRevision, surveyId: string, images: string[]) { "use workflow"; const hooks = images.map((_, index) => - createHook({ token: `survey:${surveyId}:mission:${index}` }) + createHook({ token: `survey:${surveyId}:mission:${index}` }) ); await Promise.all( images.map((image, index) => - startMission("birdSurvey", { + startMission(revision, { image, count: 5, reviewTokenPrefix: `survey:${surveyId}:review:${index}`, @@ -357,43 +446,48 @@ export async function birdSurvey(surveyId: string, images: string[]) { }; } -/** Starts one mission as a dynamic child run. */ -async function startMission(mission: MissionId, input: Record) { +async function startMission(revision: MissionRevision, input: Record) { "use step"; - const { source, steps } = MISSIONS[mission]; - const run = await start(source, [input], { experimental_dynamic: { steps } }); + const run = await startMissionRevision(revision, input); return { runId: run.runId }; } ``` -`startMission` is a step, not workflow code: `start()` is a side effect, and the `steps` map holds function references that must not cross a step boundary as serialized data. The parent itself is a normal static workflow, so start it from a route with `start(birdSurvey, [surveyId, images])`, and bound the number of images there. +`startMission` is a step, not workflow code: `start()` is a side effect, and the step map holds function references that must not cross a step boundary as serialized data. Load the revision in the route that starts `birdSurvey`, and pass it in, so every child runs the same revision. The child-workflows recipe covers the rest of the fan-out: [chunked spawning](/cookbook/advanced/child-workflows#fan-out-pattern-chunked-spawning) for hundreds of children, [partial failures](/cookbook/advanced/child-workflows#tolerating-partial-failures), and the parent's own event-log growth. -The child-workflows recipe covers the rest of the fan-out: [chunked spawning](/cookbook/advanced/child-workflows#fan-out-pattern-chunked-spawning) for hundreds of children, [partial failures](/cookbook/advanced/child-workflows#tolerating-partial-failures), and the parent's own event-log growth. +## Runs in flight + +A dynamic run stores its compiled code when it starts, and every replay runs that stored code. Publishing a new revision changes what the *next* start runs: + +- **In-flight runs are neither upgraded nor revoked.** A run of mission A revision 1 finishes on revision 1, even after revision 2 is published or the record is deleted. To stop one, [cancel the run](/docs/api-reference/workflow-api/get-run#cancel-a-run). +- **A run needs its deployment until it finishes.** A dynamic run executes only on the deployment that started it, against that deployment's steps. Keep a deployment available while its long-running missions wait on reviews or timers, and keep its catalog steps compatible with what those missions call. + +## Limits + +- **Source size.** At most 128 KB of source per mission. +- **Execution context.** On Vercel, a run's execution context is limited to **2,048 bytes of JSON**, and the mission's plaintext metadata counts against it: the source hash, the export name, and every alias with its full step ID. Step IDs are derived from the step's file path and function name, so a catalog in a deeply nested file fits fewer aliases than one near the root. Treat any alias count as an estimate; a start that exceeds the limit throws before a run is created. +- **Local and Vercel differ.** The Local World does not enforce the execution-context limit, so start a mission with many bindings on a preview deployment before approving it. + +See [Rules for the source](/docs/advanced/dynamic-workflows#rules-for-the-source) for the full list. ## Security -Dynamic source runs with the full privileges of your deployment's functions; the workflow sandbox is for determinism, not isolation. This recipe is safe because of how the source is chosen, not because of where it runs: +Dynamic workflows execute trusted application code, including code authored by operators or drafted by a model and approved under your application-code review process. Do not execute end-user scripts, or unreviewed model-generated source influenced by untrusted requests, retrieved content, or tool outputs. A fixed step catalog, syntax validation, and a dedicated project do not make that source safe. -- **Templates are platform code.** They live in your repository and go through review like any other change. The start route selects one by ID and never accepts source in the request. -- **Parameters are input, not source.** Building source from request fields, even with escaping, turns every caller into a code author. Passing them as `input` keeps the executed code identical to the reviewed code. -- **Model output is data.** `detectBirds` returns detections for the template to iterate over. Do not ask a model to write the mission. -- **The step map is not a boundary.** A reviewed template that calls a missing alias fails its run, but the map does not stop source that is trying to reach other steps or the host. Review is the boundary. +- **The source has the deployment's privileges.** The workflow sandbox is for determinism, not isolation. Mission source can read `process.env`, reach host globals, call the World with the deployment's credentials (which reach other runs' hooks and streams), and alter state in a warm instance that later requests reuse. +- **The step map is not a boundary.** A mission that calls an alias it was not given fails its run, but the map does not stop source that is trying to reach other steps or the host. Review is the boundary. +- **A dedicated project is defense in depth.** Running missions from their own Vercel project, with only the environment variables and access the catalog needs, limits what a mistake in reviewed code can reach. It does not make unreviewed source safe to run. +- **Guard the publish path like a deploy.** Anyone who can publish a revision can run code with the deployment's privileges. Restrict the publish route to operators, and protect the store's write access the same way. +- **Parameters are input, not source.** Building source from request fields, even with escaping, turns every caller into a code author. - **Authorize every resume.** Hook tokens route a payload to a run; they do not prove who sent it. See [Security](/docs/advanced/dynamic-workflows#security) on the concept page for the full model, including what encryption does and does not protect. -## Tips - -- **Treat a template's bytes as its identity.** The workflow ID hashes the raw source and the step bindings, so reformatting a template, editing a comment, or rebinding an alias creates a new workflow and splits its runs in observability. Keep templates as constants and change them deliberately. -- **Bind only the steps a template calls.** On Vercel, the bindings count against the run's execution-context limit, and long step IDs cost more than long aliases. See [Rules for the source](/docs/advanced/dynamic-workflows#rules-for-the-source). -- **Cap what callers control.** Bound counts, batch sizes, and string lengths in the start route, since they decide how many steps and hooks a mission creates. -- **Test templates locally, then on a preview.** The Local World runs dynamic workflows, so `WORKFLOW_EXPERIMENTAL_DYNAMIC_WORKFLOWS=1 pnpm dev` checks a template's validation and behavior. It does not enforce Vercel's execution-context limit, so check a template with many bindings on a preview deployment too. - ## Key APIs - [`start()`](/docs/api-reference/workflow-api/start): Starts a run from workflow source with `experimental_dynamic.steps`. - [`createHook()`](/docs/api-reference/workflow/create-hook): Waits for an external signal, such as a review, inside the source. - [`sleep()`](/docs/api-reference/workflow/sleep): Durable timer, raced against a hook for timeouts. - [`getHookByToken()`](/docs/api-reference/workflow-api/get-hook-by-token): Looks up a pending hook before resuming it. -- [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook): Delivers a review or approval to the waiting run. -- [`getRun()`](/docs/api-reference/workflow-api/get-run): Reads a mission's status and result. +- [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook): Delivers a review to the waiting run. +- [`getRun()`](/docs/api-reference/workflow-api/get-run): Reads a mission run's status and result. diff --git a/docs/content/docs/v5/cookbook/index.mdx b/docs/content/docs/v5/cookbook/index.mdx index d0a1784e86..2068084184 100644 --- a/docs/content/docs/v5/cookbook/index.mdx +++ b/docs/content/docs/v5/cookbook/index.mdx @@ -33,7 +33,7 @@ Use these workflow patterns and copy-paste code examples to implement common use ## Advanced - [**Child Workflows**](/cookbook/advanced/child-workflows): Spawn and orchestrate child workflows from a parent -- [**Dynamic Workflows**](/cookbook/advanced/dynamic-workflows): Run reviewed mission templates over a fixed catalog of deployed steps, started from a route handler +- [**Dynamic Workflows**](/cookbook/advanced/dynamic-workflows): Publish reviewed workflow source after deploy and run it on an unchanged deployment - [**Upgrading Workflows**](/cookbook/advanced/upgrading-workflows): Identify a clean upgrade point in a long-running workflow and spawn a fresh run on the latest deployment carrying state forward - [**Serializable Steps**](/cookbook/advanced/serializable-steps): Wrap non-serializable third-party objects so they cross the workflow boundary - [**Publishing Libraries**](/cookbook/advanced/publishing-libraries): Ship npm packages that export reusable workflow functions diff --git a/docs/lib/cookbook-tree.ts b/docs/lib/cookbook-tree.ts index b4b7e0d55d..7febaeb755 100644 --- a/docs/lib/cookbook-tree.ts +++ b/docs/lib/cookbook-tree.ts @@ -190,7 +190,7 @@ export const recipes: Record = { slug: 'dynamic-workflows', title: 'Dynamic Workflows', description: - 'Run reviewed mission templates over a fixed catalog of deployed steps: start them from a route handler, resume their review hooks, and fan them out from a static parent.', + 'Publish reviewed workflow source after deploy and run it on an unchanged deployment, over a fixed catalog of deployed steps.', category: 'advanced', skipVersions: ['v4'], }, diff --git a/packages/core/e2e/e2e-dynamic-workflow.test.ts b/packages/core/e2e/e2e-dynamic-workflow.test.ts index 5b165dc80e..8f83bf6fc3 100644 --- a/packages/core/e2e/e2e-dynamic-workflow.test.ts +++ b/packages/core/e2e/e2e-dynamic-workflow.test.ts @@ -30,6 +30,7 @@ * 2. DEPLOYMENT_URL=http://localhost:3000 APP_NAME=nextjs-turbopack \ * pnpm vitest run packages/core/e2e/e2e-dynamic-workflow.test.ts */ +import { createHash } from 'node:crypto'; import fs from 'node:fs'; import path from 'node:path'; import { setTimeout as sleep } from 'node:timers/promises'; @@ -404,6 +405,107 @@ describeJs('dynamic workflows e2e', { timeout: 120_000 }, () => { }); }); +/** + * The mission runner in the "Dynamic Workflows" cookbook recipe. + * + * The deployment holds only the step catalog and the runner + * (`dynamicMissionRun` and friends in `workflows/99_e2e.ts`). The missions + * are published after deploy: this runner plays the operator, building each + * approved revision the way the recipe's publish route does and handing it to + * the unchanged deployment. Keep both sources in sync with the recipe. + */ + +/** Mission A, as the recipe publishes it: every detection gets a review. */ +const REVIEW_ALL_SOURCE = ` +async function workflow(input) { + "use workflow"; + const detections = await steps.llm({ image: input.image, count: input.count }); + + const reviews = await Promise.all( + detections.map(async (detection) => { + const token = input.reviewTokenPrefix + ":" + detection.id; + const review = createHook({ token }); + await steps.expertReview(detection, token); + const verdict = await review; + return { ...detection, confirmed: verdict.confirmed === true, reviewedBy: "expert" }; + }) + ); + + const confirmed = reviews.filter((review) => review.confirmed).length; + const result = { image: input.image, confirmed, reviews }; + await steps.notify(input.image + ": " + confirmed + " of " + reviews.length + " confirmed"); + if (input.parentToken) { + await steps.reportToParent(input.parentToken, result); + } + return result; +} +`; + +/** + * Mission B, published later: confident detections are accepted without a + * review, and a review nobody answers in time counts as unconfirmed. + */ +const TRIAGE_SOURCE = ` +async function workflow(input) { + "use workflow"; + const detections = await steps.llm({ image: input.image, count: input.count }); + + const reviews = await Promise.all( + detections.map(async (detection) => { + if (detection.confidence >= input.autoAcceptAbove) { + return { ...detection, confirmed: true, reviewedBy: "auto" }; + } + const token = input.reviewTokenPrefix + ":" + detection.id; + const review = createHook({ token }); + await steps.expertReview(detection, token); + const verdict = await Promise.race([ + review, + sleep(input.reviewTimeout).then(() => null), + ]); + if (verdict === null) { + return { ...detection, confirmed: false, reviewedBy: "timeout" }; + } + return { ...detection, confirmed: verdict.confirmed === true, reviewedBy: "expert" }; + }) + ); + + const confirmed = reviews.filter((review) => review.confirmed).length; + const result = { image: input.image, confirmed, reviews }; + await steps.notify(input.image + ": " + confirmed + " of " + reviews.length + " confirmed"); + if (input.parentToken) { + await steps.reportToParent(input.parentToken, result); + } + return result; +} +`; + +/** Alias the source calls -> catalog step name, for both missions. */ +const MISSION_STEPS = { + llm: 'detectBirds', + expertReview: 'requestExpertReview', + notify: 'notify', + reportToParent: 'reportToParent', +}; + +/** The operator path: an approved, immutable revision of a mission. */ +function publishRevision(missionId: string, revision: number, source: string) { + return { + missionId, + revision, + source, + sourceSha256: createHash('sha256').update(source).digest('hex'), + steps: MISSION_STEPS, + catalogVersion: 'birds@1', + approval: { + approvedBy: 'e2e-operator', + approvedAt: new Date().toISOString(), + }, + }; +} + +const MISSION_A = publishRevision('review-all', 1, REVIEW_ALL_SOURCE); +const MISSION_B = publishRevision('triage', 1, TRIAGE_SOURCE); + /** * Resume a hook a dynamic mission creates, once it exists. * @@ -446,21 +548,15 @@ async function resumeMissionHook( ); } -/** - * The mission runner in the "Dynamic Workflows" cookbook recipe. Its fixtures - * (`dynamicBirdSurveyMission` and friends in `workflows/99_e2e.ts`) hold the - * recipe's step catalog and mission sources. - */ describeJs( 'dynamic workflows: mission runner recipe', { timeout: 120_000 }, () => { - it('fans a bird survey out to one review hook per detection', async () => { + it('runs a published mission that reviews every detection', async () => { const prefix = `e2e-dynamic-review:${crypto.randomUUID()}`; - const { childRunId } = await startParent('dynamicBirdSurveyMission', [ - 'backyard.jpg', - 3, - prefix, + const { childRunId } = await startParent('dynamicMissionRun', [ + MISSION_A, + { image: 'backyard.jpg', count: 3, reviewTokenPrefix: prefix }, ]); // Every review waits on its own hook, in parallel; resume them out of @@ -481,54 +577,123 @@ describeJs( { id: 0, species: 'American robin', - confidence: 0.9, + confidence: 0.97, confirmed: true, + reviewedBy: 'expert', + }, + { + id: 1, + species: 'Blue jay', + confidence: 0.62, + confirmed: false, + reviewedBy: 'expert', }, - { id: 1, species: 'Blue jay', confidence: 0.89, confirmed: false }, { id: 2, species: 'Northern cardinal', - confidence: 0.88, + confidence: 0.91, confirmed: true, + reviewedBy: 'expert', }, ], }); - expect(child.record.workflowName).toMatch( - /^workflow\/\/dynamic\/[0-9a-f]{32}\/\/workflow$/ - ); }); - it('resolves an approval mission when the hook is resumed', async () => { - const token = `e2e-dynamic-approval:${crypto.randomUUID()}`; - const { childRunId } = await startParent('dynamicApprovalMission', [ - 'release notes', - token, - '10m', + it('runs a mission published after deploy that only reviews uncertain detections', async () => { + const prefix = `e2e-dynamic-triage:${crypto.randomUUID()}`; + const { childRunId } = await startParent('dynamicMissionRun', [ + MISSION_B, + { + image: 'feeder.jpg', + count: 4, + reviewTokenPrefix: prefix, + autoAcceptAbove: 0.9, + reviewTimeout: '10m', + }, ]); - const hook = await resumeMissionHook(token, { approved: true }); - expect(hook.runId).toBe(childRunId); + // Detections 1 and 3 are under the threshold; 0 and 2 never get a hook. + await resumeMissionHook(`${prefix}:1`, { confirmed: true }); + await resumeMissionHook(`${prefix}:3`, { confirmed: false }); const child = await awaitChildRun(childRunId); - expect(child.output).toEqual({ approved: true, timedOut: false }); + expect(child.output).toMatchObject({ + image: 'feeder.jpg', + confirmed: 3, + reviews: [ + { id: 0, confirmed: true, reviewedBy: 'auto' }, + { id: 1, confirmed: true, reviewedBy: 'expert' }, + { id: 2, confirmed: true, reviewedBy: 'auto' }, + { id: 3, confirmed: false, reviewedBy: 'expert' }, + ], + }); + await expect(getHookByToken(`${prefix}:0`)).rejects.toThrow(/not found/i); }); - it('times out an approval mission nobody resumes', async () => { - const token = `e2e-dynamic-approval:${crypto.randomUUID()}`; - const { childRunId } = await startParent('dynamicApprovalMission', [ - 'release notes', - token, - '2s', + it('closes a review nobody answers when its timeout fires', async () => { + const { childRunId } = await startParent('dynamicMissionRun', [ + MISSION_B, + { + image: 'porch.jpg', + count: 2, + reviewTokenPrefix: `e2e-dynamic-triage:${crypto.randomUUID()}`, + autoAcceptAbove: 0.9, + reviewTimeout: '2s', + }, ]); // The durable sleep wins the race, so the run completes with no resume. const child = await awaitChildRun(childRunId); - expect(child.output).toEqual({ approved: false, timedOut: true }); + expect(child.output).toMatchObject({ + confirmed: 1, + reviews: [ + { id: 0, reviewedBy: 'auto' }, + { id: 1, confirmed: false, reviewedBy: 'timeout' }, + ], + }); + }); + + it('gives a revision that differs only in whitespace a new workflow id', async () => { + // One confident detection, so both runs finish without a review. + const input = { + image: 'wire.jpg', + count: 1, + reviewTokenPrefix: `e2e-dynamic-triage:${crypto.randomUUID()}`, + autoAcceptAbove: 0.9, + reviewTimeout: '10m', + }; + const reformatted = publishRevision('triage', 2, `${TRIAGE_SOURCE}\n`); + const [first, second] = await Promise.all([ + startParent('dynamicMissionRun', [MISSION_B, input]), + startParent('dynamicMissionRun', [reformatted, input]), + ]); + + const [a, b] = await Promise.all([ + awaitChildRun(first.childRunId), + awaitChildRun(second.childRunId), + ]); + expect(a.output).toEqual(b.output); + expect(a.record.workflowName).not.toBe(b.record.workflowName); + }); + + it('refuses a stored revision whose source does not match its approval', async () => { + const tampered = { + ...MISSION_B, + source: TRIAGE_SOURCE.replace('>= input.autoAcceptAbove', '>= 0'), + }; + const parent = await start(await e2e('dynamicMissionRun'), [ + tampered, + { image: 'x.jpg', count: 1, reviewTokenPrefix: 'unused' }, + ]); + await expect(parent.returnValue).rejects.toThrow( + /does not match its approved source/ + ); }); it('fans missions out from a static parent and back in through hooks', async () => { const surveyKey = `e2e-dynamic-survey:${crypto.randomUUID()}`; - const parent = await start(await e2e('dynamicBirdSurveyFanOut'), [ + const parent = await start(await e2e('dynamicMissionFanOut'), [ + MISSION_A, surveyKey, ['north.jpg', 'south.jpg'], ]); diff --git a/workbench/example/workflows/99_e2e.ts b/workbench/example/workflows/99_e2e.ts index e1b843a8cf..6f1442a71b 100644 --- a/workbench/example/workflows/99_e2e.ts +++ b/workbench/example/workflows/99_e2e.ts @@ -4246,11 +4246,12 @@ export async function dynamicWorkflowDisallowedStep(value: number) { // Dynamic workflows: mission runner // // Backs the "Dynamic Workflows" cookbook recipe -// (docs/content/docs/v5/cookbook/advanced/dynamic-workflows.mdx). The step -// catalog, the two mission sources, their step bindings, and the fan-out -// parent are the recipe's code; keep the two in sync. The recipe starts -// missions from a route handler; here `startMission` is a step, because the -// e2e runner can only reach the deployment through a static workflow. +// (docs/content/docs/v5/cookbook/advanced/dynamic-workflows.mdx): the step +// catalog, the start-time checks on a mission revision, and the fan-out parent +// are the recipe's code; keep the two in sync. The mission sources themselves +// are NOT here. As in the recipe, they are published after deploy: the e2e +// runner plays the operator and store, and hands each revision to +// `dynamicMissionRun`, which stands in for the recipe's start route. ////////////////////////////////////////////////////////// interface BirdDetection { @@ -4259,17 +4260,15 @@ interface BirdDetection { confidence: number; } -const BIRD_SPECIES = [ - 'American robin', - 'Blue jay', - 'Northern cardinal', - 'Mourning dove', +/** Fixed detections, so the runner knows which reviews a mission will open. */ +const BIRD_DETECTIONS = [ + { species: 'American robin', confidence: 0.97 }, + { species: 'Blue jay', confidence: 0.62 }, + { species: 'Northern cardinal', confidence: 0.91 }, + { species: 'Mourning dove', confidence: 0.55 }, ] as const; -/** - * Catalog step standing in for a vision-model call. Deterministic, so the - * runner knows which review hooks the mission will create. - */ +/** Catalog step standing in for a vision-model call. */ async function detectBirds(request: { image: string; count: number; @@ -4277,8 +4276,7 @@ async function detectBirds(request: { 'use step'; return Array.from({ length: request.count }, (_, id) => ({ id, - species: BIRD_SPECIES[id % BIRD_SPECIES.length], - confidence: 0.9 - id / 100, + ...BIRD_DETECTIONS[id % BIRD_DETECTIONS.length], })); } @@ -4301,147 +4299,116 @@ async function reportToParent(token: string, report: unknown) { await resumeHook(token, report); } -/** - * A reviewed mission template: detect birds in an image, fan out one expert - * review per detection (each waiting on its own hook), then aggregate. Every - * parameter arrives as `input`, so the source never changes and every run of - * it shares one workflow ID. - */ -const BIRD_SURVEY_SOURCE = ` -async function workflow(input) { - "use workflow"; - const detections = await steps.llm({ image: input.image, count: input.count }); - - const reviews = await Promise.all( - detections.map(async (detection) => { - const token = input.reviewTokenPrefix + ":" + detection.id; - const review = createHook({ token }); - await steps.expertReview(detection, token); - const verdict = await review; - return { ...detection, confirmed: verdict.confirmed === true }; - }) - ); +/** Every step a published mission may bind, by catalog name. */ +const MISSION_CATALOG = { + detectBirds, + requestExpertReview, + notify, + reportToParent, +}; - const confirmed = reviews.filter((review) => review.confirmed).length; - const result = { image: input.image, confirmed, reviews }; - await steps.notify( - input.image + ": " + confirmed + " of " + reviews.length + " confirmed" - ); - if (input.parentToken) { - await steps.reportToParent(input.parentToken, result); - } - return result; -} -`; +/** Bumped whenever a catalog step changes in a way old missions would notice. */ +const MISSION_CATALOG_VERSION = 'birds@1'; -/** A reviewed approval template: a hook raced against a durable timeout. */ -const APPROVAL_SOURCE = ` -async function workflow(input) { - "use workflow"; - await steps.notify("Approval requested: " + input.subject); +type CatalogStepName = keyof typeof MISSION_CATALOG; - const approval = createHook({ token: input.approvalToken }); - const decision = await Promise.race([ - approval.then((payload) => ({ - approved: payload.approved === true, - timedOut: false, - })), - sleep(input.timeout).then(() => ({ approved: false, timedOut: true })), - ]); +/** One immutable, approved revision of a mission, as the store holds it. */ +interface MissionRevision { + missionId: string; + revision: number; + source: string; + sourceSha256: string; + steps: Record; + catalogVersion: string; + approval: { approvedBy: string; approvedAt: string }; +} - await steps.notify( - input.subject + ": " + (decision.approved ? "approved" : "not approved") +async function sha256Hex(text: string) { + const digest = await crypto.subtle.digest( + 'SHA-256', + new TextEncoder().encode(text) ); - return decision; + return Array.from(new Uint8Array(digest), (byte) => + byte.toString(16).padStart(2, '0') + ).join(''); } -`; - -/** Mission templates by ID: reviewed source plus the steps it may call. */ -const MISSIONS = { - birdSurvey: { - source: BIRD_SURVEY_SOURCE, - steps: { - llm: detectBirds, - expertReview: requestExpertReview, - notify, - reportToParent, - }, - }, - approval: { - source: APPROVAL_SOURCE, - steps: { notify }, - }, -}; - -type MissionId = keyof typeof MISSIONS; /** - * Starts one mission as a dynamic run. The recipe makes this same `start()` - * call from its route handler; either way it needs the imported step - * functions, which carry the `stepId` the source's aliases bind to. + * Start-time checks on a stored revision: it is approved, its bytes are the + * approved bytes, and it targets the catalog this deployment serves. Returns + * the step map `start()` needs, built from the imported step functions. */ -async function startMission( - mission: MissionId, - input: Record -) { - 'use step'; - const { source, steps } = MISSIONS[mission]; - const run = await start(source, [input], { experimental_dynamic: { steps } }); - return { runId: run.runId }; +async function bindMissionRevision(revision: MissionRevision) { + const label = `${revision.missionId}@${revision.revision}`; + if (!revision.approval?.approvedBy) { + throw new FatalError(`Mission ${label} has no approval record`); + } + if ((await sha256Hex(revision.source)) !== revision.sourceSha256) { + throw new FatalError(`Mission ${label} does not match its approved source`); + } + if (revision.catalogVersion !== MISSION_CATALOG_VERSION) { + throw new FatalError( + `Mission ${label} targets catalog ${revision.catalogVersion}, but this deployment serves ${MISSION_CATALOG_VERSION}` + ); + } + const steps: Record = {}; + for (const [alias, name] of Object.entries(revision.steps)) { + if (!Object.hasOwn(MISSION_CATALOG, name)) { + throw new FatalError(`Mission ${label} binds unknown step "${name}"`); + } + steps[alias] = MISSION_CATALOG[name]; + } + return steps; } -/** Starts a bird-survey mission and hands back the child's run ID. */ -export async function dynamicBirdSurveyMission( - image: string, - count: number, - reviewTokenPrefix: string +/** Starts one stored mission revision as a dynamic run. */ +async function startMissionRevision( + revision: MissionRevision, + input: Record ) { - 'use workflow'; - const child = await startMission('birdSurvey', { - image, - count, - reviewTokenPrefix, + 'use step'; + const steps = await bindMissionRevision(revision); + const run = await start(revision.source, [input], { + experimental_dynamic: { steps }, }); - return { childRunId: child.runId }; + return { runId: run.runId, workflowName: await run.workflowName }; } -/** Starts an approval mission and hands back the child's run ID. */ -export async function dynamicApprovalMission( - subject: string, - approvalToken: string, - timeout: string +/** + * Starts a published mission revision and hands back the child's run ID. The + * recipe's start route makes the same calls. + */ +export async function dynamicMissionRun( + revision: MissionRevision, + input: Record ) { 'use workflow'; - const child = await startMission('approval', { - subject, - approvalToken, - timeout, - }); - return { childRunId: child.runId }; + const child = await startMissionRevision(revision, input); + return { childRunId: child.runId, workflowName: child.workflowName }; } -interface BirdSurveyResult { +interface MissionResult { image: string; confirmed: number; - reviews: Array; } /** - * Static parent that fans out one dynamic bird-survey mission per image and - * fans back in through hooks: each child resumes the hook the parent created - * for it, as in the child-workflows recipe. + * Static parent that fans one mission run out per image and fans back in + * through hooks: each child resumes the hook the parent created for it, as in + * the child-workflows recipe. */ -export async function dynamicBirdSurveyFanOut( +export async function dynamicMissionFanOut( + revision: MissionRevision, surveyKey: string, images: string[] ) { 'use workflow'; const hooks = images.map((_, index) => - createHook({ token: `${surveyKey}:mission:${index}` }) + createHook({ token: `${surveyKey}:mission:${index}` }) ); await Promise.all( images.map((image, index) => - startMission('birdSurvey', { + startMissionRevision(revision, { image, count: 1, reviewTokenPrefix: `${surveyKey}:review:${index}`, From 3fa58e89c25ffe97834caed8e6c0645fb663645a Mon Sep 17 00:00:00 2001 From: Alex Langenfeld Date: Thu, 1 Oct 2026 11:21:25 -0500 Subject: [PATCH 03/11] docs: dispose timed-out review hooks and state what the revision hash proves Signed-off-by: Alex Langenfeld --- .../cookbook/advanced/dynamic-workflows.mdx | 40 +++++++------ .../core/e2e/e2e-dynamic-workflow.test.ts | 60 +++++++++++++++---- workbench/example/workflows/99_e2e.ts | 8 ++- 3 files changed, 76 insertions(+), 32 deletions(-) diff --git a/docs/content/docs/v5/cookbook/advanced/dynamic-workflows.mdx b/docs/content/docs/v5/cookbook/advanced/dynamic-workflows.mdx index 169ebc76ab..bbac05a7db 100644 --- a/docs/content/docs/v5/cookbook/advanced/dynamic-workflows.mdx +++ b/docs/content/docs/v5/cookbook/advanced/dynamic-workflows.mdx @@ -111,7 +111,7 @@ import type { CatalogStepName } from "@/lib/mission-catalog"; export interface MissionRevision { missionId: string; revision: number; - /** The exact approved bytes. */ + /** The exact source that was approved. */ source: string; sourceSha256: string; /** Alias the source calls -> catalog step name. */ @@ -169,7 +169,7 @@ async function workflow(input) { } ``` -Mission B, published later, accepts detections at or above `input.autoAcceptAbove` without a review, and gives each remaining review until `input.reviewTimeout` before counting it as unconfirmed: +Mission B, published later, accepts detections at or above `input.autoAcceptAbove` without a review, and gives each remaining review until `input.reviewTimeout` before counting it as unconfirmed. It disposes each review's hook once the review settles, so a response that arrives after the timeout is refused rather than delivered to a hook nothing reads: {/* @skip-typecheck: dynamic workflow source, evaluated with injected steps, createHook, and sleep */} ```javascript title="missions/triage.js" lineNumbers @@ -184,15 +184,21 @@ async function workflow(input) { } // [!code highlight] const token = input.reviewTokenPrefix + ":" + detection.id; const review = createHook({ token }); - await steps.expertReview(detection, token); - const verdict = await Promise.race([ // [!code highlight] - review, // [!code highlight] - sleep(input.reviewTimeout).then(() => null), // [!code highlight] - ]); // [!code highlight] - if (verdict === null) { - return { ...detection, confirmed: false, reviewedBy: "timeout" }; + try { + await steps.expertReview(detection, token); + const verdict = await Promise.race([ // [!code highlight] + review, // [!code highlight] + sleep(input.reviewTimeout).then(() => null), // [!code highlight] + ]); // [!code highlight] + if (verdict === null) { + return { ...detection, confirmed: false, reviewedBy: "timeout" }; + } + return { ...detection, confirmed: verdict.confirmed === true, reviewedBy: "expert" }; + } finally { + // Release the token, so a response after the timeout is refused + // instead of resolving a hook nothing reads. + review.dispose(); // [!code highlight] } - return { ...detection, confirmed: verdict.confirmed === true, reviewedBy: "expert" }; }) ); @@ -206,7 +212,7 @@ async function workflow(input) { } ``` -Both missions bind the same aliases: `llm` to `detectBirds`, `expertReview` to `requestExpertReview`, `notify`, and `reportToParent`. Every parameter, including the threshold, the timeout, and the hook tokens, arrives as `input`. The stored source is never templated, so the code that runs is byte for byte the code that was reviewed. Hook tokens are derived from a mission-run ID the server generates, so replay recreates the same hooks and concurrent runs never share a token; see [custom tokens](/docs/foundations/hooks#custom-tokens-for-deterministic-hooks). +Both missions bind the same aliases: `llm` to `detectBirds`, `expertReview` to `requestExpertReview`, `notify`, and `reportToParent`. Every parameter, including the threshold, the timeout, and the hook tokens, arrives as `input`. The stored source is never templated, so the code that runs is the stored revision, unchanged. Hook tokens are derived from a mission-run ID the server generates, so replay recreates the same hooks and concurrent runs never share a token; see [custom tokens](/docs/foundations/hooks#custom-tokens-for-deterministic-hooks). ## Publish a revision @@ -263,11 +269,11 @@ export async function POST(request: Request) { } ``` -These checks confirm the revision is well formed and fits this deployment's catalog. They are not a review: nothing here can tell whether a mission does what its approver thinks it does. The review is the approval recorded with it. The SDK validates the source shape (the `workflow` declaration, the directive, no imports) when `start()` is called; there is no separate validation API, so a pipeline that wants that check before approval can start the mission once on a preview deployment. +These checks confirm the revision is well formed and fits this deployment's catalog. They are not a review: nothing here can tell whether a mission does what its approver thinks it does. Nor do they verify the approval: `approvedBy` is a field the caller supplied, and the route trusts it because only operators can reach the route. The review happens in your review process; the publish route and the store's write access are what make the stored record trustworthy. The SDK validates the source shape (the `workflow` declaration, the directive, no imports) when `start()` is called; there is no separate validation API, so a pipeline that wants that check before approval can start the mission once on a preview deployment. ## Start a mission -The start route loads the latest revision, checks it again, and starts it. Checking at start as well as at publish catches a record that was edited after approval and a revision published against a catalog this deployment no longer serves. The checks throw [`FatalError`](/docs/api-reference/workflow/fatal-error) so that, when a step calls them (as in [Fan out at scale](#fan-out-at-scale)), a refused revision fails at once instead of retrying. +The start route loads the latest revision, checks it again, and starts it. Checking at start as well as at publish catches a revision published against a catalog this deployment no longer serves, and a source that changed in the store without its hash, such as a partial write or a hand edit. It does not catch a writer who changes the source and the hash together, or who invents an approval record: the hash is stored next to the source it describes, so it detects accidents, not tampering. Trust in a revision comes from who can publish and who can write to the store. To verify provenance at start independently of the store, sign each revision at publish with a key the store's writers do not hold, and check the signature here. The checks throw [`FatalError`](/docs/api-reference/workflow/fatal-error) so that, when a step calls them (as in [Fan out at scale](#fan-out-at-scale)), a refused revision fails at once instead of retrying. ```typescript title="lib/start-mission.ts" lineNumbers import { FatalError } from "workflow"; @@ -281,7 +287,7 @@ export async function bindMissionRevision(revision: MissionRevision) { throw new FatalError(`Mission ${label} has no approval record`); } if ((await sha256Hex(revision.source)) !== revision.sourceSha256) { - throw new FatalError(`Mission ${label} does not match its approved source`); + throw new FatalError(`Mission ${label} does not match its recorded hash`); } if (revision.catalogVersion !== MISSION_CATALOG_VERSION) { throw new FatalError(`Mission ${label} targets catalog ${revision.catalogVersion}`); @@ -354,7 +360,7 @@ export async function POST(request: Request) { } ``` -Record the revision a run started from alongside its run ID, so every result can be traced to the exact approved source. +Record the revision a run started from alongside its run ID, so every result can be traced to the exact source it ran. ## Resume a review @@ -403,7 +409,7 @@ export async function POST( } ``` -A review for a detection mission B accepted on its own, or one that arrives after its timeout, gets a 404: that hook never existed or is already gone. See [Authorize before calling `resumeHook()`](/docs/foundations/hooks#authorize-before-calling-resumehook). +A review for a detection mission B accepted on its own gets a 404 because that hook never existed. One that arrives after its timeout gets a 404 because mission B disposed the hook when the timeout fired, even though the run may still be finishing its remaining steps. Once a run finishes, all of its hooks are gone. See [Authorize before calling `resumeHook()`](/docs/foundations/hooks#authorize-before-calling-resumehook). To report status, read the run with [`getRun()`](/docs/api-reference/workflow-api/get-run). A dynamic run's result type is `unknown` to TypeScript, so pass the type you expect, as in `getRun(runId)`. @@ -477,7 +483,7 @@ Dynamic workflows execute trusted application code, including code authored by o - **The source has the deployment's privileges.** The workflow sandbox is for determinism, not isolation. Mission source can read `process.env`, reach host globals, call the World with the deployment's credentials (which reach other runs' hooks and streams), and alter state in a warm instance that later requests reuse. - **The step map is not a boundary.** A mission that calls an alias it was not given fails its run, but the map does not stop source that is trying to reach other steps or the host. Review is the boundary. - **A dedicated project is defense in depth.** Running missions from their own Vercel project, with only the environment variables and access the catalog needs, limits what a mistake in reviewed code can reach. It does not make unreviewed source safe to run. -- **Guard the publish path like a deploy.** Anyone who can publish a revision can run code with the deployment's privileges. Restrict the publish route to operators, and protect the store's write access the same way. +- **Guard the publish path like a deploy.** Anyone who can publish a revision, or write to the store directly, can run code with the deployment's privileges. Restrict the publish route to operators and protect the store's write access the same way; the hash and approval fields in a revision do not defend against either. - **Parameters are input, not source.** Building source from request fields, even with escaping, turns every caller into a code author. - **Authorize every resume.** Hook tokens route a payload to a run; they do not prove who sent it. diff --git a/packages/core/e2e/e2e-dynamic-workflow.test.ts b/packages/core/e2e/e2e-dynamic-workflow.test.ts index 8f83bf6fc3..0bd6fad37f 100644 --- a/packages/core/e2e/e2e-dynamic-workflow.test.ts +++ b/packages/core/e2e/e2e-dynamic-workflow.test.ts @@ -457,15 +457,21 @@ async function workflow(input) { } const token = input.reviewTokenPrefix + ":" + detection.id; const review = createHook({ token }); - await steps.expertReview(detection, token); - const verdict = await Promise.race([ - review, - sleep(input.reviewTimeout).then(() => null), - ]); - if (verdict === null) { - return { ...detection, confirmed: false, reviewedBy: "timeout" }; + try { + await steps.expertReview(detection, token); + const verdict = await Promise.race([ + review, + sleep(input.reviewTimeout).then(() => null), + ]); + if (verdict === null) { + return { ...detection, confirmed: false, reviewedBy: "timeout" }; + } + return { ...detection, confirmed: verdict.confirmed === true, reviewedBy: "expert" }; + } finally { + // Release the token, so a response after the timeout is refused + // instead of resolving a hook nothing reads. + review.dispose(); } - return { ...detection, confirmed: verdict.confirmed === true, reviewedBy: "expert" }; }) ); @@ -506,6 +512,23 @@ function publishRevision(missionId: string, revision: number, source: string) { const MISSION_A = publishRevision('review-all', 1, REVIEW_ALL_SOURCE); const MISSION_B = publishRevision('triage', 1, TRIAGE_SOURCE); +/** Every event in a run's log, oldest first. */ +async function allRunEvents(runId: string) { + const world = await getWorld(); + const events: { eventType: string }[] = []; + let cursor: string | undefined; + for (;;) { + const page = await world.events.list({ + runId, + pagination: { limit: 100, cursor, sortOrder: 'asc' }, + }); + events.push(...(page.data as { eventType: string }[])); + if (!page.cursor || page.cursor === cursor) break; + cursor = page.cursor; + } + return events; +} + /** * Resume a hook a dynamic mission creates, once it exists. * @@ -630,13 +653,14 @@ describeJs( await expect(getHookByToken(`${prefix}:0`)).rejects.toThrow(/not found/i); }); - it('closes a review nobody answers when its timeout fires', async () => { + it('closes and disposes a review nobody answers when its timeout fires', async () => { + const prefix = `e2e-dynamic-triage:${crypto.randomUUID()}`; const { childRunId } = await startParent('dynamicMissionRun', [ MISSION_B, { image: 'porch.jpg', count: 2, - reviewTokenPrefix: `e2e-dynamic-triage:${crypto.randomUUID()}`, + reviewTokenPrefix: prefix, autoAcceptAbove: 0.9, reviewTimeout: '2s', }, @@ -651,6 +675,18 @@ describeJs( { id: 1, confirmed: false, reviewedBy: 'timeout' }, ], }); + + // The mission disposed the timed-out review's hook itself, before the + // run completed. Completion removes a run's remaining hooks without + // logging a `hook_disposed`, so this event is the source's dispose(). + const events = await allRunEvents(childRunId); + const types = events.map((event) => event.eventType); + const disposedAt = types.indexOf('hook_disposed'); + expect(disposedAt).toBeGreaterThan(-1); + expect(disposedAt).toBeLessThan(types.indexOf('run_completed')); + + // A late response is refused rather than delivered. + await expect(getHookByToken(`${prefix}:1`)).rejects.toThrow(/not found/i); }); it('gives a revision that differs only in whitespace a new workflow id', async () => { @@ -676,7 +712,7 @@ describeJs( expect(a.record.workflowName).not.toBe(b.record.workflowName); }); - it('refuses a stored revision whose source does not match its approval', async () => { + it('refuses a stored revision whose source does not match its recorded hash', async () => { const tampered = { ...MISSION_B, source: TRIAGE_SOURCE.replace('>= input.autoAcceptAbove', '>= 0'), @@ -686,7 +722,7 @@ describeJs( { image: 'x.jpg', count: 1, reviewTokenPrefix: 'unused' }, ]); await expect(parent.returnValue).rejects.toThrow( - /does not match its approved source/ + /does not match its recorded hash/ ); }); diff --git a/workbench/example/workflows/99_e2e.ts b/workbench/example/workflows/99_e2e.ts index 6f1442a71b..28f92ce23c 100644 --- a/workbench/example/workflows/99_e2e.ts +++ b/workbench/example/workflows/99_e2e.ts @@ -4334,8 +4334,10 @@ async function sha256Hex(text: string) { } /** - * Start-time checks on a stored revision: it is approved, its bytes are the - * approved bytes, and it targets the catalog this deployment serves. Returns + * Start-time checks on a stored revision: it has an approval record, its + * source still matches the hash recorded with it, and it targets the catalog + * this deployment serves. The hash catches a source that changed without it, + * not a writer who changes both; trust comes from who can publish. Returns * the step map `start()` needs, built from the imported step functions. */ async function bindMissionRevision(revision: MissionRevision) { @@ -4344,7 +4346,7 @@ async function bindMissionRevision(revision: MissionRevision) { throw new FatalError(`Mission ${label} has no approval record`); } if ((await sha256Hex(revision.source)) !== revision.sourceSha256) { - throw new FatalError(`Mission ${label} does not match its approved source`); + throw new FatalError(`Mission ${label} does not match its recorded hash`); } if (revision.catalogVersion !== MISSION_CATALOG_VERSION) { throw new FatalError( From 84b5e4c4ade6cc3ed6b0e82738172e117fbfc29f Mon Sep 17 00:00:00 2001 From: Alex Langenfeld Date: Thu, 1 Oct 2026 12:50:19 -0500 Subject: [PATCH 04/11] test(e2e): don't order hook_disposed against run_completed Signed-off-by: Alex Langenfeld --- packages/core/e2e/e2e-dynamic-workflow.test.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/core/e2e/e2e-dynamic-workflow.test.ts b/packages/core/e2e/e2e-dynamic-workflow.test.ts index 0bd6fad37f..aefd9baf4b 100644 --- a/packages/core/e2e/e2e-dynamic-workflow.test.ts +++ b/packages/core/e2e/e2e-dynamic-workflow.test.ts @@ -676,14 +676,12 @@ describeJs( ], }); - // The mission disposed the timed-out review's hook itself, before the - // run completed. Completion removes a run's remaining hooks without - // logging a `hook_disposed`, so this event is the source's dispose(). + // The mission disposed the timed-out review's hook itself. Completion + // removes a run's remaining hooks without logging a `hook_disposed`, so + // this event is the source's dispose(). (Not ordered against + // `run_completed`: under load that event can trail the run's result.) const events = await allRunEvents(childRunId); - const types = events.map((event) => event.eventType); - const disposedAt = types.indexOf('hook_disposed'); - expect(disposedAt).toBeGreaterThan(-1); - expect(disposedAt).toBeLessThan(types.indexOf('run_completed')); + expect(events.map((event) => event.eventType)).toContain('hook_disposed'); // A late response is refused rather than delivered. await expect(getHookByToken(`${prefix}:1`)).rejects.toThrow(/not found/i); From ad03dad5923754f308a3bfe5c2ebd30480e5bca6 Mon Sep 17 00:00:00 2001 From: Alex Langenfeld Date: Thu, 1 Oct 2026 14:34:45 -0500 Subject: [PATCH 05/11] docs: rebuild the dynamic workflows recipe around a customer migration Signed-off-by: Alex Langenfeld --- .changeset/dynamic-workflows-cookbook.md | 2 +- .../cookbook/advanced/dynamic-workflows.mdx | 712 +++++++++--------- docs/content/docs/v5/cookbook/index.mdx | 2 +- docs/lib/cookbook-tree.ts | 2 +- .../core/e2e/e2e-dynamic-workflow.test.ts | 465 +++++++++++- workbench/example/tsconfig.tsbuildinfo | 1 + workbench/example/workflows/99_e2e.ts | 559 +++++++++++++- 7 files changed, 1377 insertions(+), 366 deletions(-) create mode 100644 workbench/example/tsconfig.tsbuildinfo diff --git a/.changeset/dynamic-workflows-cookbook.md b/.changeset/dynamic-workflows-cookbook.md index 1684382723..bc80067b58 100644 --- a/.changeset/dynamic-workflows-cookbook.md +++ b/.changeset/dynamic-workflows-cookbook.md @@ -1,4 +1,4 @@ --- --- -Docs: add a dynamic workflows cookbook recipe for publishing reviewed missions after deploy, backed by e2e fixtures. +Docs: add a dynamic workflows cookbook recipe for running customer-specific migration procedures published after deploy, backed by e2e fixtures. diff --git a/docs/content/docs/v5/cookbook/advanced/dynamic-workflows.mdx b/docs/content/docs/v5/cookbook/advanced/dynamic-workflows.mdx index bbac05a7db..1f260417b0 100644 --- a/docs/content/docs/v5/cookbook/advanced/dynamic-workflows.mdx +++ b/docs/content/docs/v5/cookbook/advanced/dynamic-workflows.mdx @@ -1,8 +1,8 @@ --- title: Dynamic Workflows -description: Publish reviewed workflow source after deploy and run it on an unchanged deployment, over a fixed catalog of deployed steps. +description: Run a customer-specific migration procedure, written and approved after deploy, as a durable workflow over a fixed catalog of deployed steps. type: guide -summary: Deploy a step catalog and a mission runner once, store approved mission revisions, publish a new mission through a privileged operator path, and start it with start() on the same deployment without a rebuild. +summary: Deploy a migration step catalog once, then start reviewed JavaScript procedures for individual customers with start(), resume their approvals, and read back the records they changed, without a rebuild or redeploy. prerequisites: - /docs/advanced/dynamic-workflows related: @@ -13,10 +13,12 @@ related: --- -Use this pattern when the orchestration a platform runs has to change after deploy, but every side effect it performs does not. The deployment ships a fixed catalog of steps and a small runner. Operators then publish reviewed **missions**, short JavaScript workflows over that catalog, to a store. The runner loads a mission and starts it with [`start()`](/docs/api-reference/workflow-api/start) as a [dynamic workflow](/docs/advanced/dynamic-workflows). Publishing a new mission needs no rebuild or redeploy. +A platform that imports customers' legacy CRM data usually needs nothing more than a field mapping. Then a customer arrives whose export only makes sense under their own conventions. Acme groups accounts by contract lineage, so a regional account must be loaded after the account it reports to. The same person appears once per region. Two account rows on one contract are one account, and when their owners disagree, a person has to choose. + +An integration engineer, perhaps with an agent drafting it, writes those rules as a short JavaScript program over the platform's deployed migration steps. A reviewer approves it, and the platform runs it with [`start()`](/docs/api-reference/workflow-api/start) as a [dynamic workflow](/docs/advanced/dynamic-workflows). The program is published after the platform deployed, and running it needs no rebuild or redeploy. It survives pauses for approvals and restarts like any workflow, and it applies the same approved rules to every record. Dynamic workflows are **experimental**. Before this recipe runs: @@ -28,355 +30,303 @@ Dynamic workflows are **experimental**. Before this recipe runs: See [Enabling dynamic workflows](/docs/advanced/dynamic-workflows#enabling-dynamic-workflows) and [World support](/docs/advanced/dynamic-workflows#world-support) for the details. -## How it fits together +## When to use this -1. **Deploy once.** The deployment contains the step catalog, the runner, and the publish, start, and review routes. It contains no missions. -2. **Publish mission A.** An operator publishes a reviewed revision of a mission that sends every bird detection to an expert. The store keeps it. -3. **Publish mission B.** Later, an operator publishes a second mission with a different shape: it accepts confident detections on its own and sends only the rest for review. Nothing is rebuilt. -4. **Start either mission** on the same, unchanged deployment. The start route loads the stored revision and runs it. +- **Use static configuration for routine imports.** If a customer's import is "column `acct_name` goes to `name`", keep a static workflow that reads a mapping. It needs no review of code and has better types and errors. +- **Use a procedure when the rules are an algorithm.** Acme's grouping, identity, and dependency order take loops, maps, and a traversal. You could encode them in JSON, but then the platform owns an interpreter for that JSON. +- **Use an agent to write the procedure, not to run it.** A durable agent would reconsider every record. Here an agent can help draft the procedure, a person approves it, and the workflow executes exactly those rules for every record, every time. -If the set of workflows is known at build time, write normal workflow functions and pick one in code instead. They have better types and errors, and no source to store or review separately. + +A procedure is trusted application code with your deployment's privileges, not a sandboxed customer script. Only run source approved under your application-code review process; a fixed catalog of steps is an integration contract, not a security boundary. See [Security](/docs/advanced/dynamic-workflows#security). + -## The step catalog +## The deployed catalog -The catalog is a workflow file of ordinary step functions, deployed with the app. Dynamic source cannot define steps, so everything a mission does outside of orchestration happens here. +The platform deploys a fixed set of migration steps once. Steps do the I/O: they read the legacy export and write to the destination. The procedure decides what to write and in what order, and reaches the steps only through `steps.()`. -```typescript title="workflows/mission-steps.ts" lineNumbers -import { resumeHook } from "workflow/api"; +```typescript title="workflows/migration-steps.ts" lineNumbers +declare const legacyCrm: { page(dataset: string, cursor?: number): Promise<{ records: unknown[]; nextCursor?: number }> }; // @setup +declare const destinations: { lookup(r: any): Promise; stage(r: any): Promise; apply(r: any): Promise; verify(r: any): Promise }; // @setup +declare function notifyReviewer(r: any): Promise; // @setup +declare function saveReport(r: any): Promise; // @setup -export interface BirdDetection { - id: number; - species: string; - confidence: number; +/** One page of the customer's legacy export. */ +export async function readSourcePage(request: { dataset: string; cursor?: number }) { + "use step"; + return await legacyCrm.page(request.dataset, request.cursor); } -declare function callVisionModel(image: string, maxDetections: number): Promise; // @setup -declare function pageReviewer(detection: BirdDetection, token: string): Promise; // @setup -declare function postToChannel(message: string): Promise; // @setup - -export async function detectBirds(request: { image: string; count: number }) { +/** Which of these keys the destination already has. */ +export async function lookupDestination(request: { destinationId: string; keys: string[] }) { "use step"; - return await callVisionModel(request.image, request.count); + return await destinations.lookup(request); } -export async function requestExpertReview(detection: BirdDetection, token: string) { +/** Validates operations and returns an immutable plan with a content-derived `planId`. */ +export async function stageChanges(request: { + migrationId: string; + destinationId: string; + groupKey: string; + operations: unknown[]; + dependsOn?: unknown[]; +}) { "use step"; - await pageReviewer(detection, token); - return { detectionId: detection.id, token }; + return await destinations.stage(request); } -export async function notify(message: string) { +/** Asks a reviewer to resume `token` with `{ approved, owner? }`. */ +export async function requestApproval(request: { planId: string; token: string; summary: string }) { "use step"; - await postToChannel(message); - return { delivered: message }; + await notifyReviewer(request); + return { planId: request.planId, token: request.token }; } -/** Fans a child mission's result back into its parent's hook. */ -export async function reportToParent(token: string, report: unknown) { +/** Applies a staged plan to the destination named in the plan. */ +export async function applyChanges(request: { plan: unknown; idempotencyKey: string }) { "use step"; - // Throws, and so retries, until the parent has created its hook. - await resumeHook(token, report); + return await destinations.apply(request); } -``` - -The runner names the catalog by step name and gives it a version. Bump the version whenever a step changes in a way an existing mission would notice, such as a new argument shape or a different return value. - -```typescript title="lib/mission-catalog.ts" lineNumbers -import { - detectBirds, - notify, - reportToParent, - requestExpertReview, -} from "@/workflows/mission-steps"; - -export const MISSION_CATALOG = { - detectBirds, - requestExpertReview, - notify, - reportToParent, -}; -export const MISSION_CATALOG_VERSION = "birds@1"; +/** Integrity invariants and the resulting records. */ +export async function verifyChanges(request: { destinationId: string; migrationId: string; results: unknown[] }) { + "use step"; + return await destinations.verify(request); +} -export type CatalogStepName = keyof typeof MISSION_CATALOG; +export async function publishReport(request: { migrationId: string; results: unknown }) { + "use step"; + return { reportId: await saveReport(request) }; +} ``` -## Mission revisions +`legacyCrm` and `destinations` stand for your integration adapters. The destination adapter, not the procedure, owns the contract every procedure relies on: -The store holds **immutable revisions**. A stable, app-level mission ID such as `triage` names the mission; each publish adds a revision under it, and nothing edits a revision in place. A revision records exactly what was approved: - -```typescript title="lib/mission-store.ts" lineNumbers -import type { CatalogStepName } from "@/lib/mission-catalog"; - -export interface MissionRevision { - missionId: string; - revision: number; - /** The exact source that was approved. */ - source: string; - sourceSha256: string; - /** Alias the source calls -> catalog step name. */ - steps: Record; - /** The catalog version the source was reviewed against. */ - catalogVersion: string; - approval: { approvedBy: string; approvedAt: string; reviewUrl: string }; -} +- **Supported operations only:** `createAccount`, `createContact`, `linkContact`, `setParent`, `setOwner`, `markFinal`, `mergeAccount`, and `mergeContact`. There is no step that evaluates arbitrary code. +- **Referential integrity:** every parent and contact link must resolve when the plan is applied, to a record created earlier in the plan or by a plan listed in `dependsOn`. +- **Immutable plans:** `planId` is a hash of the plan's migration, destination, group, operations, and dependencies, and `applyChanges` takes the destination from the plan, so a caller cannot point an approved plan at another destination. +- **Conflicts block writes:** merging two accounts whose owners differ records a conflict on the plan, and `applyChanges` refuses a plan with conflicts. +- **Idempotency:** the key is `"apply:" + planId`. Repeating it returns the first result; reusing it for a different payload is rejected. -/** Any durable store works: a database table, a KV namespace, a bucket. */ -export interface MissionStore { - latest(missionId: string): Promise; - get(missionId: string, revision: number): Promise; - /** Appends the next revision; never overwrites one. */ - append(revision: Omit): Promise; -} +The steps are bound under their own names when a procedure starts: -export async function sha256Hex(text: string) { - const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text)); - return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join(""); -} +```typescript title="lib/migration-catalog.ts" lineNumbers +import { + applyChanges, + lookupDestination, + publishReport, + readSourcePage, + requestApproval, + stageChanges, + verifyChanges, +} from "@/workflows/migration-steps"; + +export const MIGRATION_CATALOG = { + readSourcePage, + lookupDestination, + stageChanges, + requestApproval, + applyChanges, + verifyChanges, + publishReport, +}; ``` -Keep the mission ID separate from the workflow ID. The SDK derives the [workflow ID](/docs/advanced/dynamic-workflows#workflow-ids) from a hash of the raw source bytes and the step bindings, so reformatting a mission, editing a comment, or rebinding an alias gives its runs a new workflow ID. The mission ID is what stays stable across revisions. - -## The two missions +## Procedure A: parent-first -Missions are plain JavaScript: a top-level `async function workflow(input)` that opens with `"use workflow"` and reaches the catalog only through `steps`. `createHook`, `sleep`, and the usual [workflow globals](/docs/api-reference/workflow-globals) are in scope; see [What the source can use](/docs/advanced/dynamic-workflows#what-the-source-can-use). - -Mission A sends every detection to an expert and waits on one hook per detection: +This is Acme's procedure. It is a file in the integration team's repository, not part of the deployment. It migrates each account family as its own plan, all families at once, and only the family whose owners disagree waits for a person: {/* @skip-typecheck: dynamic workflow source, evaluated with injected steps and createHook */} -```javascript title="missions/review-all.js" lineNumbers +```javascript title="procedures/acme-parent-first.js" lineNumbers async function workflow(input) { "use workflow"; - const detections = await steps.llm({ image: input.image, count: input.count }); + const scope = { migrationId: input.migrationId, destinationId: input.destinationId }; + + const rows = []; + let cursor; + do { + const page = await steps.readSourcePage({ dataset: input.dataset, cursor }); + rows.push(...page.records); + cursor = page.nextCursor; + } while (cursor !== undefined); + + // This customer's rules: a family is every account under one contract root, + // accounts on the same contract are one account, and a contact is one person + // per normalized email within a family. + const accounts = rows.filter((row) => row.kind === "account"); + const contacts = rows.filter((row) => row.kind === "contact"); + const families = new Map(); + for (const row of accounts) { + const family = row.contract.split("/")[0]; + families.set(family, [...(families.get(family) || []), row]); + } - const reviews = await Promise.all( - detections.map(async (detection) => { - const token = input.reviewTokenPrefix + ":" + detection.id; - const review = createHook({ token }); - await steps.expertReview(detection, token); - const verdict = await review; - return { ...detection, confirmed: verdict.confirmed === true, reviewedBy: "expert" }; - }) - ); + function familyOperations(members, owner) { + // Parent-first: an account is written only after the account it reports to. + const ordered = []; + const visit = (row) => { + if (ordered.includes(row)) return; + const parent = members.find((other) => other.id === row.parent); + if (parent) visit(parent); + ordered.push(row); + }; + members.forEach(visit); + + const ops = []; + const survivors = new Map(); + const survivorOf = new Map(); + for (const row of ordered) { + ops.push({ op: "createAccount", key: row.id, name: row.name, owner: row.owner, sources: [row.id] }); + if (row.parent) ops.push({ op: "setParent", account: row.id, parent: row.parent }); + const survivor = survivors.get(row.contract); + if (!survivor) { + survivors.set(row.contract, row.id); + survivorOf.set(row.id, row.id); + continue; + } + survivorOf.set(row.id, survivor); + if (owner) { + ops.push({ op: "setOwner", key: survivor, owner }, { op: "setOwner", key: row.id, owner }); + } + ops.push({ op: "mergeAccount", from: row.id, into: survivor }); + } - const confirmed = reviews.filter((review) => review.confirmed).length; - const result = { image: input.image, confirmed, reviews }; - await steps.notify(input.image + ": " + confirmed + " of " + reviews.length + " confirmed"); - if (input.parentToken) { - await steps.reportToParent(input.parentToken, result); + const people = new Map(); + for (const row of contacts) { + if (!survivorOf.has(row.account)) continue; + const email = row.email.trim().toLowerCase(); + const person = people.get(email) || { key: row.id, name: row.name, email, sources: [], accounts: [] }; + person.sources.push(row.id); + person.accounts.push(survivorOf.get(row.account)); + people.set(email, person); + } + for (const person of people.values()) { + ops.push({ op: "createContact", key: person.key, name: person.name, email: person.email, sources: person.sources }); + for (const account of new Set(person.accounts)) { + ops.push({ op: "linkContact", contact: person.key, account }); + } + } + return ops; } - return result; -} -``` - -Mission B, published later, accepts detections at or above `input.autoAcceptAbove` without a review, and gives each remaining review until `input.reviewTimeout` before counting it as unconfirmed. It disposes each review's hook once the review settles, so a response that arrives after the timeout is refused rather than delivered to a hook nothing reads: -{/* @skip-typecheck: dynamic workflow source, evaluated with injected steps, createHook, and sleep */} -```javascript title="missions/triage.js" lineNumbers -async function workflow(input) { - "use workflow"; - const detections = await steps.llm({ image: input.image, count: input.count }); - - const reviews = await Promise.all( - detections.map(async (detection) => { - if (detection.confidence >= input.autoAcceptAbove) { // [!code highlight] - return { ...detection, confirmed: true, reviewedBy: "auto" }; // [!code highlight] - } // [!code highlight] - const token = input.reviewTokenPrefix + ":" + detection.id; + async function migrateFamily(family, members) { + const stage = (owner) => + steps.stageChanges({ ...scope, groupKey: family, operations: familyOperations(members, owner) }); + let plan = await stage(); + if (plan.conflicts.length > 0) { + // Only this family waits for a person; the others carry on. + const token = input.tokenPrefix + ":" + family; const review = createHook({ token }); try { - await steps.expertReview(detection, token); - const verdict = await Promise.race([ // [!code highlight] - review, // [!code highlight] - sleep(input.reviewTimeout).then(() => null), // [!code highlight] - ]); // [!code highlight] - if (verdict === null) { - return { ...detection, confirmed: false, reviewedBy: "timeout" }; - } - return { ...detection, confirmed: verdict.confirmed === true, reviewedBy: "expert" }; + const owners = plan.conflicts.flatMap((conflict) => conflict.values); + await steps.requestApproval({ planId: plan.planId, token, summary: family + ": choose an owner from " + owners.join(", ") }); + const decision = await review; + if (!decision.approved) return { family, status: "skipped" }; + plan = await stage(decision.owner || owners[0]); } finally { - // Release the token, so a response after the timeout is refused - // instead of resolving a hook nothing reads. - review.dispose(); // [!code highlight] + review.dispose(); } - }) - ); - - const confirmed = reviews.filter((review) => review.confirmed).length; - const result = { image: input.image, confirmed, reviews }; - await steps.notify(input.image + ": " + confirmed + " of " + reviews.length + " confirmed"); - if (input.parentToken) { - await steps.reportToParent(input.parentToken, result); - } - return result; -} -``` - -Both missions bind the same aliases: `llm` to `detectBirds`, `expertReview` to `requestExpertReview`, `notify`, and `reportToParent`. Every parameter, including the threshold, the timeout, and the hook tokens, arrives as `input`. The stored source is never templated, so the code that runs is the stored revision, unchanged. Hook tokens are derived from a mission-run ID the server generates, so replay recreates the same hooks and concurrent runs never share a token; see [custom tokens](/docs/foundations/hooks#custom-tokens-for-deterministic-hooks). - -## Publish a revision - -Publishing is a privileged operator action, not part of the public API. A typical setup keeps missions in a reviewed repository and lets its CI job call this route after a change merges: - -```typescript title="app/api/admin/missions/route.ts" lineNumbers -import { z } from "zod"; -import { MISSION_CATALOG, MISSION_CATALOG_VERSION } from "@/lib/mission-catalog"; -import { sha256Hex, type MissionStore } from "@/lib/mission-store"; - -declare const missionStore: MissionStore; // @setup -declare function assertOperator(request: Request): Promise; // @setup - -const publishRequest = z.object({ - missionId: z.string().regex(/^[a-z][a-z0-9-]{0,62}$/), - source: z.string().max(128 * 1024), - steps: z.record(z.string(), z.string()), - catalogVersion: z.string(), - approval: z.object({ - approvedBy: z.string().min(1), - approvedAt: z.string().datetime(), - reviewUrl: z.string().url(), - }), -}); - -export async function POST(request: Request) { - await assertOperator(request); // [!code highlight] - - const parsed = publishRequest.safeParse(await request.json()); - if (!parsed.success) { - return Response.json({ error: parsed.error.message }, { status: 400 }); - } - const { steps, catalogVersion, ...mission } = parsed.data; - - if (catalogVersion !== MISSION_CATALOG_VERSION) { - return Response.json( - { error: `This deployment serves catalog ${MISSION_CATALOG_VERSION}` }, - { status: 409 } - ); - } - for (const name of Object.values(steps)) { - if (!Object.hasOwn(MISSION_CATALOG, name)) { - return Response.json({ error: `Unknown catalog step "${name}"` }, { status: 400 }); } + const result = await steps.applyChanges({ plan, idempotencyKey: "apply:" + plan.planId }); + return { family, status: "applied", result }; } - const revision = await missionStore.append({ - ...mission, - steps: steps as Record, - catalogVersion, - sourceSha256: await sha256Hex(mission.source), + const outcomes = await Promise.all([...families].map(([family, members]) => migrateFamily(family, members))); + const verification = await steps.verifyChanges({ + ...scope, + results: outcomes.filter((outcome) => outcome.result).map((outcome) => outcome.result), }); - return Response.json({ missionId: revision.missionId, revision: revision.revision }); + const summary = outcomes.map(({ family, status }) => ({ family, status })); + const report = await steps.publishReport({ migrationId: input.migrationId, results: { families: summary, verification } }); + return { families: summary, verification, report }; } ``` -These checks confirm the revision is well formed and fits this deployment's catalog. They are not a review: nothing here can tell whether a mission does what its approver thinks it does. Nor do they verify the approval: `approvedBy` is a field the caller supplied, and the route trusts it because only operators can reach the route. The review happens in your review process; the publish route and the store's write access are what make the stored record trustworthy. The SDK validates the source shape (the `workflow` declaration, the directive, no imports) when `start()` is called; there is no separate validation API, so a pipeline that wants that check before approval can start the mission once on a preview deployment. +The procedure owns the customer's policy, and nothing in the catalog knows about it: -## Start a mission +- **Families.** Accounts are grouped by the root of their contract number, so `C-100`, `C-100/E`, and `C-100/W` are one family. Each family becomes its own plan, and `Promise.all` runs them concurrently. +- **Parent-first order.** Within a family, an account is written only after the account it reports to. The destination checks that every parent resolves, so writing a child first would fail the plan. +- **One account per contract.** Two rows on the same contract become one account through `mergeAccount`. If their owners differ, the plan comes back with a conflict, and the procedure asks a person. +- **One person per email within a family.** Ada Park appears once per region with differently cased emails, and becomes one contact linked to both regions. `info@initech.com` appears in two families, so it stays two contacts. +- **Review before waiting.** The approval hook is created before `requestApproval` sends its token, so a fast reviewer can never miss it, and it is disposed once the review settles. A rejected family is skipped; the others are applied either way. -The start route loads the latest revision, checks it again, and starts it. Checking at start as well as at publish catches a revision published against a catalog this deployment no longer serves, and a source that changed in the store without its hash, such as a partial write or a hand edit. It does not catch a writer who changes the source and the hash together, or who invents an approval record: the hash is stored next to the source it describes, so it detects accidents, not tampering. Trust in a revision comes from who can publish and who can write to the store. To verify provenance at start independently of the store, sign each revision at publish with a key the store's writers do not hold, and check the signature here. The checks throw [`FatalError`](/docs/api-reference/workflow/fatal-error) so that, when a step calls them (as in [Fan out at scale](#fan-out-at-scale)), a refused revision fails at once instead of retrying. +Every parameter, including the dataset, the destination, and the token prefix, arrives as `input`. The source is never templated, so what runs is exactly the file that was reviewed. -```typescript title="lib/start-mission.ts" lineNumbers -import { FatalError } from "workflow"; -import { start } from "workflow/api"; -import { MISSION_CATALOG, MISSION_CATALOG_VERSION } from "@/lib/mission-catalog"; -import { sha256Hex, type MissionRevision } from "@/lib/mission-store"; +## Publish and start it -export async function bindMissionRevision(revision: MissionRevision) { - const label = `${revision.missionId}@${revision.revision}`; - if (!revision.approval?.approvedBy) { - throw new FatalError(`Mission ${label} has no approval record`); - } - if ((await sha256Hex(revision.source)) !== revision.sourceSha256) { - throw new FatalError(`Mission ${label} does not match its recorded hash`); - } - if (revision.catalogVersion !== MISSION_CATALOG_VERSION) { - throw new FatalError(`Mission ${label} targets catalog ${revision.catalogVersion}`); - } - const steps: Record = {}; - for (const alias of Object.keys(revision.steps)) { - const name = revision.steps[alias]; - if (!Object.hasOwn(MISSION_CATALOG, name)) { - throw new FatalError(`Mission ${label} binds unknown step "${name}"`); - } - steps[alias] = MISSION_CATALOG[name]; // [!code highlight] - } - return steps; -} +Nothing about this procedure existed when the platform deployed. After the procedure is reviewed and merged in the integration repository, a release job stores the file and its step names wherever the platform keeps procedures, keyed by a stable procedure ID such as `acme-parent-first`. A route then starts it on the running deployment: -export async function startMissionRevision( - revision: MissionRevision, - input: Record -) { - const steps = await bindMissionRevision(revision); - return await start(revision.source, [input], { // [!code highlight] - experimental_dynamic: { steps }, // [!code highlight] - }); // [!code highlight] -} -``` - -The step map holds the **imported** step functions. The build stamps a `stepId` on each one, and that ID is what binds `steps.llm` in the source to `detectBirds` in this deployment. - -```typescript title="app/api/missions/route.ts" lineNumbers +```typescript title="app/api/migrations/route.ts" lineNumbers +import { start } from "workflow/api"; import { z } from "zod"; -import type { MissionStore } from "@/lib/mission-store"; -import { startMissionRevision } from "@/lib/start-mission"; +import { MIGRATION_CATALOG } from "@/lib/migration-catalog"; -declare const missionStore: MissionStore; // @setup +declare const procedures: { latest(id: string): Promise<{ source: string; steps: Record } | null> }; // @setup +declare function assertOperator(request: Request): Promise; // @setup const startRequest = z.object({ - missionId: z.string(), - image: z.string().min(1).max(200), - count: z.number().int().min(1).max(20), + procedureId: z.string(), + dataset: z.string(), + destinationId: z.string(), }); export async function POST(request: Request) { - // Authenticate the caller first: a mission runs deployed steps. + await assertOperator(request); const parsed = startRequest.safeParse(await request.json()); if (!parsed.success) { return Response.json({ error: parsed.error.message }, { status: 400 }); } - const { missionId, image, count } = parsed.data; - - const revision = await missionStore.latest(missionId); - if (!revision) { - return Response.json({ error: "Unknown mission" }, { status: 404 }); + const procedure = await procedures.latest(parsed.data.procedureId); + if (!procedure) { + return Response.json({ error: "Unknown procedure" }, { status: 404 }); } - const missionRunId = crypto.randomUUID(); - const run = await startMissionRevision(revision, { - image, - count, - reviewTokenPrefix: `mission-run:${missionRunId}:review`, - // Read only by missions that use them. - autoAcceptAbove: 0.9, - reviewTimeout: "1d", - }); + // Bind each name the procedure calls to the imported step function. + const steps = Object.fromEntries( + Object.entries(procedure.steps).map(([alias, name]) => [ + alias, + MIGRATION_CATALOG[name as keyof typeof MIGRATION_CATALOG], + ]) + ); - return Response.json({ - runId: run.runId, - missionRunId, - revision: revision.revision, - }); + const id = crypto.randomUUID(); + const migrationId = `migration-${id}`; + const run = await start(procedure.source, [{ // [!code highlight] + dataset: parsed.data.dataset, + destinationId: parsed.data.destinationId, + migrationId, + tokenPrefix: `migration:${id}`, + }], { experimental_dynamic: { steps } }); // [!code highlight] + + return Response.json({ runId: run.runId, migrationId }); } ``` -Record the revision a run started from alongside its run ID, so every result can be traced to the exact source it ran. +The step map holds the **imported** step functions: the build stamps a `stepId` on each one, and that ID is what binds `steps.applyChanges` in the source to this deployment's step. See [Operating it](#operating-it) for what to check before starting a stored procedure. + +The whole sequence is: + +1. Deploy the catalog and these routes once. +2. Write and review a procedure, then publish it to the store. +3. `POST /api/migrations` with the procedure ID, the dataset, and the destination. +4. A reviewer answers any approval it requests. +5. Read the result with [`getRun()`](/docs/api-reference/workflow-api/get-run); the run's return value holds the verification and the report ID. -## Resume a review +## Review an owner conflict -Each review waits on its own hook. The review route rebuilds the token from the mission-run ID and the detection ID, confirms the hook belongs to the run, and resumes it: +Initech's two rows share contract `C-300` but list different owners, so only that family waits. Its token is the run's `tokenPrefix` plus the family. The review route resumes it after authorizing the reviewer: -```typescript title="app/api/missions/[runId]/reviews/route.ts" lineNumbers +```typescript title="app/api/migrations/[runId]/reviews/route.ts" lineNumbers import { getHookByToken, resumeHook } from "workflow/api"; import { HookNotFoundError } from "workflow/errors"; import { z } from "zod"; -declare function assertCanReview(request: Request, missionRunId: string): Promise; // @setup +declare function assertCanReview(request: Request, migrationId: string): Promise; // @setup const reviewRequest = z.object({ - missionRunId: z.string().uuid(), - detectionId: z.number().int().min(0), - confirmed: z.boolean(), + migrationId: z.string().startsWith("migration-"), + family: z.string(), + approved: z.boolean(), + owner: z.string().optional(), }); export async function POST( @@ -388,17 +338,17 @@ export async function POST( if (!parsed.success) { return Response.json({ error: parsed.error.message }, { status: 400 }); } - const { missionRunId, detectionId, confirmed } = parsed.data; - + const { migrationId, family, approved, owner } = parsed.data; // Knowing a token does not authorize the caller. - await assertCanReview(request, missionRunId); // [!code highlight] + await assertCanReview(request, migrationId); // [!code highlight] + const token = `migration:${migrationId.slice("migration-".length)}:${family}`; try { - const hook = await getHookByToken(`mission-run:${missionRunId}:review:${detectionId}`); + const hook = await getHookByToken(token); if (hook.runId !== runId) { return Response.json({ error: "No such review" }, { status: 404 }); } - await resumeHook(hook, { confirmed }); // [!code highlight] + await resumeHook(hook, { approved, owner }); // [!code highlight] return Response.json({ ok: true }); } catch (error) { if (HookNotFoundError.is(error)) { @@ -409,91 +359,173 @@ export async function POST( } ``` -A review for a detection mission B accepted on its own gets a 404 because that hook never existed. One that arrives after its timeout gets a 404 because mission B disposed the hook when the timeout fired, even though the run may still be finishing its remaining steps. Once a run finishes, all of its hooks are gone. See [Authorize before calling `resumeHook()`](/docs/foundations/hooks#authorize-before-calling-resumehook). +A review for a family that never conflicted, or one that arrives after the review settled, gets a 404: that hook never existed, or the procedure already disposed it. See [Authorize before calling `resumeHook()`](/docs/foundations/hooks#authorize-before-calling-resumehook). -To report status, read the run with [`getRun()`](/docs/api-reference/workflow-api/get-run). A dynamic run's result type is `unknown` to TypeScript, so pass the type you expect, as in `getRun(runId)`. +## What survives a pause, and what changed -## Fan out at scale +While Initech waits, the run is suspended on its hook, using no compute, and the other two families are already applied: Acme's three accounts with Ada linked to both regions, and Globex. A restart in between does not matter, and neither does a new deploy: the run stays on the deployment that started it, and when the review arrives it replays its stored procedure and recorded step results instead of re-reading or re-writing anything. -To run a mission once per image across a large batch, start the runs from a static parent workflow and fan back in through hooks, as in [Child Workflows](/cookbook/advanced/child-workflows). Each mission ends with `steps.reportToParent(input.parentToken, result)` when it was given a parent token, which resumes the hook the parent created for it. +After the reviewer picks `sam` as Initech's owner, the destination holds: -```typescript title="workflows/bird-survey.ts" lineNumbers -import { createHook } from "workflow"; -import type { MissionRevision } from "@/lib/mission-store"; -import { startMissionRevision } from "@/lib/start-mission"; +| Account | Parent | Owner | From legacy rows | +| --- | --- | --- | --- | +| Acme Holdings | none | dana | `acct-acme` | +| Acme East | Acme Holdings | dana | `acct-acme-east` | +| Acme West | Acme Holdings | dana | `acct-acme-west` | +| Globex | none | lee | `acct-globex` | +| Initech | none | sam | `acct-initech`, `acct-initech-ltd` | -interface MissionResult { - image: string; - confirmed: number; -} +| Contact | Linked to | From legacy rows | +| --- | --- | --- | +| Ada Park, `ada@acme.com` | Acme East, Acme West | `ct-ada-east`, `ct-ada-west` | +| Initech front desk, `info@initech.com` | Initech | `ct-info-initech` | +| Globex reseller desk, `info@initech.com` | Globex | `ct-info-globex` | -export async function birdSurvey(revision: MissionRevision, surveyId: string, images: string[]) { - "use workflow"; +`verifyChanges` reports no dangling links, no provisional accounts, and no duplicate contacts. A field mapping could not produce this: it would create six accounts and four contacts, merge nothing, and either link the two `info@initech.com` rows together or keep Ada twice. - const hooks = images.map((_, index) => - createHook({ token: `survey:${surveyId}:mission:${index}` }) - ); - await Promise.all( - images.map((image, index) => - startMission(revision, { - image, - count: 5, - reviewTokenPrefix: `survey:${surveyId}:review:${index}`, - parentToken: `survey:${surveyId}:mission:${index}`, // [!code highlight] - }) - ) - ); - const results = await Promise.all(hooks); // [!code highlight] +## Procedure B: provisional first + +Another customer team prefers to see everything land before anything is reconciled. Procedure B runs over the same catalog and is published later, without a deploy. It is structured differently: instead of building each family carefully and pausing only the ambiguous one, it writes every row as it is, then stages one consolidation over all of it, and asks for one review of the whole change: + +{/* @skip-typecheck: excerpt of dynamic workflow source */} +```javascript title="procedures/acme-provisional-first.js (excerpt)" lineNumbers + // 1. Land every row as written: provisional, flat, one record per row. + const load = await steps.stageChanges({ + ...scope, + groupKey: "load", + operations: [ + ...accounts.map((row) => ({ op: "createAccount", key: row.id, name: row.name, owner: row.owner, status: "provisional", sources: [row.id] })), + ...contacts.flatMap((row) => [ + { op: "createContact", key: row.id, name: row.name, email: row.email.trim().toLowerCase(), sources: [row.id] }, + { op: "linkContact", contact: row.id, account: row.account }, + ]), + ], + }); + const loaded = await steps.applyChanges({ plan: load, idempotencyKey: "apply:" + load.planId }); + + // 2. Reconcile the landed records with this customer's rules, as one change: + // setParent for every child, mergeAccount for rows on the same contract, + // mergeContact for one person per email within a family, markFinal for + // every surviving account. + function consolidation(owner) { /* ... */ } + const stage = (owner) => + steps.stageChanges({ ...scope, groupKey: "consolidation", operations: consolidation(owner), dependsOn: [load] }); + let plan = await stage(); + + // 3. One review for the whole consolidation, not one per family. + const token = input.tokenPrefix + ":consolidation"; + const review = createHook({ token }); + let decision; + try { + const owners = plan.conflicts.flatMap((conflict) => conflict.values); + await steps.requestApproval({ planId: plan.planId, token, summary: plan.operations.length + " changes; owners to choose from: " + owners.join(", ") }); + decision = await review; + if (decision.approved && plan.conflicts.length > 0) plan = await stage(decision.owner || owners[0]); + } finally { + review.dispose(); + } +``` + +The two procedures converge on the same records shown above, but they get there differently: - return { - images: results.length, - confirmed: results.reduce((total, result) => total + result.confirmed, 0), - results, - }; +| | Procedure A: parent-first | Procedure B: provisional first | +| --- | --- | --- | +| First writes | Final records, one family at a time | Every row, provisional and flat | +| What waits for a person | Only the family whose owners disagree | The single consolidation of everything | +| Integrity during the run | Parents exist before children | Hierarchy and merges arrive in one plan that depends on the load | +| If the review is rejected | That family is skipped; the others are final | Records stay provisional, and `verifyChanges` reports them | + +Neither is the right migration algorithm in general. They encode two customers' conventions, which is why each is a procedure rather than a platform feature. + +## Many families + +A procedure that runs every family in one run keeps the whole migration in one event log. For an export with thousands of families, start one run per family, or per batch of families, from a static parent workflow, and fan back in through hooks as in [Child Workflows](/cookbook/advanced/child-workflows). Each child is the same procedure with a narrower `input`. + +## Operating it + +The sections above leave out how procedures are stored and checked. None of this changes the API, but a platform that runs procedures for customers needs it. + +### Procedure revisions + +Store procedures as **immutable revisions** under a stable procedure ID: the exact source, the step names it binds, the catalog version it was reviewed against, and the approval record. Publish adds a revision; nothing edits one in place. Record the revision a run started from next to its run ID. + +Keep the procedure ID separate from the [workflow ID](/docs/advanced/dynamic-workflows#workflow-ids). The SDK derives the workflow ID from a hash of the raw source bytes and the step bindings, so reformatting a procedure or editing a comment gives its runs a new workflow ID; the procedure ID stays stable across revisions. + +### Checks at publish and start + +Restrict publishing to operators, and at publish check what your platform owns: the approval record exists, every bound name is in the catalog, the catalog version is one this deployment serves, and the source is under 128 KB. Check again when a run starts, because the store can change in between: + +```typescript title="lib/bind-revision.ts" lineNumbers +import { FatalError } from "workflow"; +import { MIGRATION_CATALOG } from "@/lib/migration-catalog"; + +declare function sha256Hex(text: string): Promise; // @setup + +interface ProcedureRevision { + procedureId: string; + revision: number; + source: string; + sourceSha256: string; + steps: Record; + catalogVersion: string; + approval: { approvedBy: string; approvedAt: string; reviewUrl: string }; } -async function startMission(revision: MissionRevision, input: Record) { - "use step"; - const run = await startMissionRevision(revision, input); - return { runId: run.runId }; +const CATALOG_VERSION = "crm-migration@1"; + +export async function bindRevision(revision: ProcedureRevision) { + const label = `${revision.procedureId}@${revision.revision}`; + if (!revision.approval?.approvedBy) { + throw new FatalError(`Procedure ${label} has no approval record`); + } + if ((await sha256Hex(revision.source)) !== revision.sourceSha256) { + throw new FatalError(`Procedure ${label} does not match its recorded hash`); + } + if (revision.catalogVersion !== CATALOG_VERSION) { + throw new FatalError(`Procedure ${label} targets catalog ${revision.catalogVersion}`); + } + const steps: Record = {}; + for (const alias of Object.keys(revision.steps)) { + const name = revision.steps[alias]; + if (!Object.hasOwn(MIGRATION_CATALOG, name)) { + throw new FatalError(`Procedure ${label} binds unknown step "${name}"`); + } + steps[alias] = MIGRATION_CATALOG[name as keyof typeof MIGRATION_CATALOG]; + } + return steps; } ``` -`startMission` is a step, not workflow code: `start()` is a side effect, and the step map holds function references that must not cross a step boundary as serialized data. Load the revision in the route that starts `birdSurvey`, and pass it in, so every child runs the same revision. The child-workflows recipe covers the rest of the fan-out: [chunked spawning](/cookbook/advanced/child-workflows#fan-out-pattern-chunked-spawning) for hundreds of children, [partial failures](/cookbook/advanced/child-workflows#tolerating-partial-failures), and the parent's own event-log growth. +Be precise about what these checks prove: -## Runs in flight +- **They are not a review.** Nothing here can tell whether a procedure does what its approver thinks it does. Syntax validation in `start()` is not a review either. +- **The hash detects accidents, not tampering.** It is stored next to the source it describes, so it catches a source that changed without it, such as a partial write or a hand edit, but not a writer who changes both or invents an approval record. `approvedBy` is a field the publisher supplied. +- **Trust comes from who can publish and who can write to the store.** To verify provenance at start independently of the store, sign each revision at publish with a key the store's writers do not hold, and check the signature here. -A dynamic run stores its compiled code when it starts, and every replay runs that stored code. Publishing a new revision changes what the *next* start runs: +The checks throw [`FatalError`](/docs/api-reference/workflow/fatal-error) so that a refused revision fails at once instead of retrying when a step calls them. -- **In-flight runs are neither upgraded nor revoked.** A run of mission A revision 1 finishes on revision 1, even after revision 2 is published or the record is deleted. To stop one, [cancel the run](/docs/api-reference/workflow-api/get-run#cancel-a-run). -- **A run needs its deployment until it finishes.** A dynamic run executes only on the deployment that started it, against that deployment's steps. Keep a deployment available while its long-running missions wait on reviews or timers, and keep its catalog steps compatible with what those missions call. +### Runs in flight -## Limits +A dynamic run stores its compiled code when it starts, and every replay runs that stored code: -- **Source size.** At most 128 KB of source per mission. -- **Execution context.** On Vercel, a run's execution context is limited to **2,048 bytes of JSON**, and the mission's plaintext metadata counts against it: the source hash, the export name, and every alias with its full step ID. Step IDs are derived from the step's file path and function name, so a catalog in a deeply nested file fits fewer aliases than one near the root. Treat any alias count as an estimate; a start that exceeds the limit throws before a run is created. -- **Local and Vercel differ.** The Local World does not enforce the execution-context limit, so start a mission with many bindings on a preview deployment before approving it. +- **In-flight runs are neither upgraded nor revoked.** Publishing a new revision changes what the next start runs. To stop a run, [cancel it](/docs/api-reference/workflow-api/get-run#cancel-a-run). +- **A run needs its deployment until it finishes.** A dynamic run executes only on the deployment that started it, against that deployment's steps. Keep a deployment available while its migrations wait on reviews, and bump the catalog version when a step changes in a way an existing procedure would notice. -See [Rules for the source](/docs/advanced/dynamic-workflows#rules-for-the-source) for the full list. +### Limits -## Security +- **Source size.** At most 128 KB of source per procedure. +- **Execution context.** On Vercel, a run's execution context is limited to **2,048 bytes of JSON**, and the procedure's plaintext metadata counts against it: the source hash, the export name, and every bound name with its full step ID. Treat any count of bindings as an estimate; a start that exceeds the limit throws before a run is created. The Local World does not enforce this limit, so try a procedure with many bindings on a preview deployment. -Dynamic workflows execute trusted application code, including code authored by operators or drafted by a model and approved under your application-code review process. Do not execute end-user scripts, or unreviewed model-generated source influenced by untrusted requests, retrieved content, or tool outputs. A fixed step catalog, syntax validation, and a dedicated project do not make that source safe. +See [Rules for the source](/docs/advanced/dynamic-workflows#rules-for-the-source) for the full list. -- **The source has the deployment's privileges.** The workflow sandbox is for determinism, not isolation. Mission source can read `process.env`, reach host globals, call the World with the deployment's credentials (which reach other runs' hooks and streams), and alter state in a warm instance that later requests reuse. -- **The step map is not a boundary.** A mission that calls an alias it was not given fails its run, but the map does not stop source that is trying to reach other steps or the host. Review is the boundary. -- **A dedicated project is defense in depth.** Running missions from their own Vercel project, with only the environment variables and access the catalog needs, limits what a mistake in reviewed code can reach. It does not make unreviewed source safe to run. -- **Guard the publish path like a deploy.** Anyone who can publish a revision, or write to the store directly, can run code with the deployment's privileges. Restrict the publish route to operators and protect the store's write access the same way; the hash and approval fields in a revision do not defend against either. -- **Parameters are input, not source.** Building source from request fields, even with escaping, turns every caller into a code author. -- **Authorize every resume.** Hook tokens route a payload to a run; they do not prove who sent it. +## Testing it -See [Security](/docs/advanced/dynamic-workflows#security) on the concept page for the full model, including what encryption does and does not protect. +The e2e tests behind this recipe run both procedures against a synthetic `acme-legacy` dataset and a **pure simulated destination**: each plan's records are derived from the plan and the plans it depends on, and `verifyChanges` reconciles the results it is given. That shows the workflow's guarantees, including replay of recorded step results across a pause. It does not show destination-side deduplication when a write's outcome is unknown, which needs a destination that stores idempotency keys; test that against your real adapter. ## Key APIs - [`start()`](/docs/api-reference/workflow-api/start): Starts a run from workflow source with `experimental_dynamic.steps`. -- [`createHook()`](/docs/api-reference/workflow/create-hook): Waits for an external signal, such as a review, inside the source. -- [`sleep()`](/docs/api-reference/workflow/sleep): Durable timer, raced against a hook for timeouts. -- [`getHookByToken()`](/docs/api-reference/workflow-api/get-hook-by-token): Looks up a pending hook before resuming it. -- [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook): Delivers a review to the waiting run. -- [`getRun()`](/docs/api-reference/workflow-api/get-run): Reads a mission run's status and result. +- [`createHook()`](/docs/api-reference/workflow/create-hook): Waits for a review inside the procedure. +- [`getHookByToken()`](/docs/api-reference/workflow-api/get-hook-by-token): Looks up a pending review before resuming it. +- [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook): Delivers a reviewer's decision to the waiting run. +- [`getRun()`](/docs/api-reference/workflow-api/get-run): Reads a migration's status and result. diff --git a/docs/content/docs/v5/cookbook/index.mdx b/docs/content/docs/v5/cookbook/index.mdx index 2068084184..29d89d1b81 100644 --- a/docs/content/docs/v5/cookbook/index.mdx +++ b/docs/content/docs/v5/cookbook/index.mdx @@ -33,7 +33,7 @@ Use these workflow patterns and copy-paste code examples to implement common use ## Advanced - [**Child Workflows**](/cookbook/advanced/child-workflows): Spawn and orchestrate child workflows from a parent -- [**Dynamic Workflows**](/cookbook/advanced/dynamic-workflows): Publish reviewed workflow source after deploy and run it on an unchanged deployment +- [**Dynamic Workflows**](/cookbook/advanced/dynamic-workflows): Run a customer-specific migration procedure, written and approved after deploy, over a fixed catalog of deployed steps - [**Upgrading Workflows**](/cookbook/advanced/upgrading-workflows): Identify a clean upgrade point in a long-running workflow and spawn a fresh run on the latest deployment carrying state forward - [**Serializable Steps**](/cookbook/advanced/serializable-steps): Wrap non-serializable third-party objects so they cross the workflow boundary - [**Publishing Libraries**](/cookbook/advanced/publishing-libraries): Ship npm packages that export reusable workflow functions diff --git a/docs/lib/cookbook-tree.ts b/docs/lib/cookbook-tree.ts index 7febaeb755..85c5943340 100644 --- a/docs/lib/cookbook-tree.ts +++ b/docs/lib/cookbook-tree.ts @@ -190,7 +190,7 @@ export const recipes: Record = { slug: 'dynamic-workflows', title: 'Dynamic Workflows', description: - 'Publish reviewed workflow source after deploy and run it on an unchanged deployment, over a fixed catalog of deployed steps.', + 'Run a customer-specific migration procedure, written and approved after deploy, as a durable workflow over a fixed catalog of deployed steps.', category: 'advanced', skipVersions: ['v4'], }, diff --git a/packages/core/e2e/e2e-dynamic-workflow.test.ts b/packages/core/e2e/e2e-dynamic-workflow.test.ts index aefd9baf4b..81ed06fd48 100644 --- a/packages/core/e2e/e2e-dynamic-workflow.test.ts +++ b/packages/core/e2e/e2e-dynamic-workflow.test.ts @@ -406,13 +406,13 @@ describeJs('dynamic workflows e2e', { timeout: 120_000 }, () => { }); /** - * The mission runner in the "Dynamic Workflows" cookbook recipe. + * Published revisions: bird-survey missions kept as regression coverage for + * revision checks, hook disposal, and fan-out. * * The deployment holds only the step catalog and the runner * (`dynamicMissionRun` and friends in `workflows/99_e2e.ts`). The missions * are published after deploy: this runner plays the operator, building each - * approved revision the way the recipe's publish route does and handing it to - * the unchanged deployment. Keep both sources in sync with the recipe. + * approved revision and handing it to the unchanged deployment. */ /** Mission A, as the recipe publishes it: every detection gets a review. */ @@ -494,14 +494,22 @@ const MISSION_STEPS = { }; /** The operator path: an approved, immutable revision of a mission. */ -function publishRevision(missionId: string, revision: number, source: string) { +function publishRevision( + missionId: string, + revision: number, + source: string, + { + steps = MISSION_STEPS, + catalogVersion = 'birds@1', + }: { steps?: Record; catalogVersion?: string } = {} +) { return { missionId, revision, source, sourceSha256: createHash('sha256').update(source).digest('hex'), - steps: MISSION_STEPS, - catalogVersion: 'birds@1', + steps, + catalogVersion, approval: { approvedBy: 'e2e-operator', approvedAt: new Date().toISOString(), @@ -572,7 +580,7 @@ async function resumeMissionHook( } describeJs( - 'dynamic workflows: mission runner recipe', + 'dynamic workflows: published revisions', { timeout: 120_000 }, () => { it('runs a published mission that reviews every detection', async () => { @@ -761,3 +769,446 @@ describeJs( }); } ); + +/** + * The customer-migration procedures in the "Dynamic Workflows" cookbook + * recipe. The deployment holds only the migration catalog (a simulated + * destination; see `99_e2e.ts`). Both procedures exist only here, published + * after deploy, and must converge on the same destination records. Procedure + * A appears in the recipe byte-for-byte, and an excerpt of B; keep them in + * sync. + */ + +/** Procedure A: account families in parallel, parent-first, one review only where owners disagree. */ +const PARENT_FIRST_SOURCE = ` +async function workflow(input) { + "use workflow"; + const scope = { migrationId: input.migrationId, destinationId: input.destinationId }; + + const rows = []; + let cursor; + do { + const page = await steps.readSourcePage({ dataset: input.dataset, cursor }); + rows.push(...page.records); + cursor = page.nextCursor; + } while (cursor !== undefined); + + // This customer's rules: a family is every account under one contract root, + // accounts on the same contract are one account, and a contact is one person + // per normalized email within a family. + const accounts = rows.filter((row) => row.kind === "account"); + const contacts = rows.filter((row) => row.kind === "contact"); + const families = new Map(); + for (const row of accounts) { + const family = row.contract.split("/")[0]; + families.set(family, [...(families.get(family) || []), row]); + } + + function familyOperations(members, owner) { + // Parent-first: an account is written only after the account it reports to. + const ordered = []; + const visit = (row) => { + if (ordered.includes(row)) return; + const parent = members.find((other) => other.id === row.parent); + if (parent) visit(parent); + ordered.push(row); + }; + members.forEach(visit); + + const ops = []; + const survivors = new Map(); + const survivorOf = new Map(); + for (const row of ordered) { + ops.push({ op: "createAccount", key: row.id, name: row.name, owner: row.owner, sources: [row.id] }); + if (row.parent) ops.push({ op: "setParent", account: row.id, parent: row.parent }); + const survivor = survivors.get(row.contract); + if (!survivor) { + survivors.set(row.contract, row.id); + survivorOf.set(row.id, row.id); + continue; + } + survivorOf.set(row.id, survivor); + if (owner) { + ops.push({ op: "setOwner", key: survivor, owner }, { op: "setOwner", key: row.id, owner }); + } + ops.push({ op: "mergeAccount", from: row.id, into: survivor }); + } + + const people = new Map(); + for (const row of contacts) { + if (!survivorOf.has(row.account)) continue; + const email = row.email.trim().toLowerCase(); + const person = people.get(email) || { key: row.id, name: row.name, email, sources: [], accounts: [] }; + person.sources.push(row.id); + person.accounts.push(survivorOf.get(row.account)); + people.set(email, person); + } + for (const person of people.values()) { + ops.push({ op: "createContact", key: person.key, name: person.name, email: person.email, sources: person.sources }); + for (const account of new Set(person.accounts)) { + ops.push({ op: "linkContact", contact: person.key, account }); + } + } + return ops; + } + + async function migrateFamily(family, members) { + const stage = (owner) => + steps.stageChanges({ ...scope, groupKey: family, operations: familyOperations(members, owner) }); + let plan = await stage(); + if (plan.conflicts.length > 0) { + // Only this family waits for a person; the others carry on. + const token = input.tokenPrefix + ":" + family; + const review = createHook({ token }); + try { + const owners = plan.conflicts.flatMap((conflict) => conflict.values); + await steps.requestApproval({ planId: plan.planId, token, summary: family + ": choose an owner from " + owners.join(", ") }); + const decision = await review; + if (!decision.approved) return { family, status: "skipped" }; + plan = await stage(decision.owner || owners[0]); + } finally { + review.dispose(); + } + } + const result = await steps.applyChanges({ plan, idempotencyKey: "apply:" + plan.planId }); + return { family, status: "applied", result }; + } + + const outcomes = await Promise.all([...families].map(([family, members]) => migrateFamily(family, members))); + const verification = await steps.verifyChanges({ + ...scope, + results: outcomes.filter((outcome) => outcome.result).map((outcome) => outcome.result), + }); + const summary = outcomes.map(({ family, status }) => ({ family, status })); + const report = await steps.publishReport({ migrationId: input.migrationId, results: { families: summary, verification } }); + return { families: summary, verification, report }; +} +`; + +/** Procedure B: load everything provisional first, then one reviewed consolidation. */ +const PROVISIONAL_FIRST_SOURCE = ` +async function workflow(input) { + "use workflow"; + const scope = { migrationId: input.migrationId, destinationId: input.destinationId }; + + const rows = []; + let cursor; + do { + const page = await steps.readSourcePage({ dataset: input.dataset, cursor }); + rows.push(...page.records); + cursor = page.nextCursor; + } while (cursor !== undefined); + const accounts = rows.filter((row) => row.kind === "account"); + const contacts = rows.filter((row) => row.kind === "contact"); + + // 1. Land every row as written: provisional, flat, one record per row. + const load = await steps.stageChanges({ + ...scope, + groupKey: "load", + operations: [ + ...accounts.map((row) => ({ op: "createAccount", key: row.id, name: row.name, owner: row.owner, status: "provisional", sources: [row.id] })), + ...contacts.flatMap((row) => [ + { op: "createContact", key: row.id, name: row.name, email: row.email.trim().toLowerCase(), sources: [row.id] }, + { op: "linkContact", contact: row.id, account: row.account }, + ]), + ], + }); + const loaded = await steps.applyChanges({ plan: load, idempotencyKey: "apply:" + load.planId }); + + // 2. Reconcile the landed records with this customer's rules, as one change. + function consolidation(owner) { + const ops = []; + for (const row of accounts) { + if (row.parent) ops.push({ op: "setParent", account: row.id, parent: row.parent }); + } + const survivors = new Map(); + for (const row of accounts) { + const survivor = survivors.get(row.contract); + if (!survivor) { + survivors.set(row.contract, row.id); + continue; + } + if (owner) ops.push({ op: "setOwner", key: survivor, owner }, { op: "setOwner", key: row.id, owner }); + ops.push({ op: "mergeAccount", from: row.id, into: survivor }); + } + const people = new Map(); + for (const row of contacts) { + const holder = accounts.find((account) => account.id === row.account); + const identity = holder.contract.split("/")[0] + ":" + row.email.trim().toLowerCase(); + const person = people.get(identity); + if (person) ops.push({ op: "mergeContact", from: row.id, into: person }); + else people.set(identity, row.id); + } + for (const key of survivors.values()) ops.push({ op: "markFinal", key }); + return ops; + } + const stage = (owner) => + steps.stageChanges({ ...scope, groupKey: "consolidation", operations: consolidation(owner), dependsOn: [load] }); + let plan = await stage(); + + // One review for the whole consolidation, not one per family. + const token = input.tokenPrefix + ":consolidation"; + const review = createHook({ token }); + let decision; + try { + const owners = plan.conflicts.flatMap((conflict) => conflict.values); + await steps.requestApproval({ planId: plan.planId, token, summary: plan.operations.length + " changes; owners to choose from: " + owners.join(", ") }); + decision = await review; + if (decision.approved && plan.conflicts.length > 0) plan = await stage(decision.owner || owners[0]); + } finally { + review.dispose(); + } + + const results = [loaded]; + if (decision.approved) { + results.push(await steps.applyChanges({ plan, idempotencyKey: "apply:" + plan.planId })); + } + const verification = await steps.verifyChanges({ ...scope, results }); + const report = await steps.publishReport({ migrationId: input.migrationId, results: { approved: decision.approved, verification } }); + return { approved: decision.approved, stages: results.map((result) => result.applied), verification, report }; +} +`; + +/** Changes a staged plan before applying it, which the adapter must refuse. */ +const TAMPERED_PLAN_SOURCE = ` +async function workflow(input) { + "use workflow"; + const plan = await steps.stageChanges({ + migrationId: input.migrationId, + destinationId: input.destinationId, + groupKey: "tampered", + operations: [{ op: "createAccount", key: "acct-a", name: "A", owner: "dana", sources: [] }], + }); + plan.operations.push({ op: "createAccount", key: "acct-b", name: "B", owner: "dana", sources: [] }); + return await steps.applyChanges({ plan, idempotencyKey: "apply:" + plan.planId }); +} +`; + +const MIGRATION_CATALOG_STEPS = Object.fromEntries( + [ + 'readSourcePage', + 'lookupDestination', + 'stageChanges', + 'requestApproval', + 'applyChanges', + 'verifyChanges', + 'publishReport', + ].map((name) => [name, name]) +); + +const publishProcedure = (id: string, source: string) => + publishRevision(id, 1, source, { + steps: MIGRATION_CATALOG_STEPS, + catalogVersion: 'crm-migration@1', + }); + +const PROCEDURE_A = publishProcedure('acme-parent-first', PARENT_FIRST_SOURCE); +const PROCEDURE_B = publishProcedure( + 'acme-provisional-first', + PROVISIONAL_FIRST_SOURCE +); + +function migrationInput() { + const id = crypto.randomUUID(); + return { + dataset: 'acme-legacy', + migrationId: `migration-${id}`, + destinationId: `destination-${id}`, + tokenPrefix: `e2e-migration:${id}`, + }; +} + +/** Wait until a hook exists for `token`, without resuming it. */ +async function waitForMissionHook(token: string, timeoutMs = 60_000) { + const deadline = Date.now() + timeoutMs; + let lastError: unknown; + while (Date.now() < deadline) { + try { + return await getHookByToken(token); + } catch (error) { + lastError = error; + } + await sleep(500); + } + throw new Error( + `No hook ${token} after ${timeoutMs}ms: ${String(lastError)}` + ); +} + +/** How many `applyChanges` calls a run has completed so far. */ +async function completedApplies(runId: string) { + const world = await getWorld(); + const page = await world.steps.list({ + runId, + pagination: { limit: 100, sortOrder: 'asc' }, + resolveData: 'none', + }); + return page.data.filter( + (step) => + step.stepName.endsWith('//applyChanges') && step.status === 'completed' + ).length; +} + +/** The destination records both procedures must end with. */ +const MIGRATED_ACCOUNTS = [ + { key: 'acct-acme', parent: null, owner: 'dana', status: 'final' }, + { + key: 'acct-acme-east', + parent: 'acct-acme', + owner: 'dana', + status: 'final', + }, + { + key: 'acct-acme-west', + parent: 'acct-acme', + owner: 'dana', + status: 'final', + }, + { key: 'acct-globex', parent: null, owner: 'lee', status: 'final' }, + { + key: 'acct-initech', + parent: null, + owner: 'sam', + status: 'final', + sources: ['acct-initech', 'acct-initech-ltd'], + }, +]; +const MIGRATED_CONTACTS = [ + { + key: 'ct-ada-east', + email: 'ada@acme.com', + accounts: ['acct-acme-east', 'acct-acme-west'], + sources: ['ct-ada-east', 'ct-ada-west'], + }, + { key: 'ct-info-globex', accounts: ['acct-globex'] }, + { key: 'ct-info-initech', accounts: ['acct-initech'] }, +]; + +describeJs( + 'dynamic workflows: customer migration recipe', + { timeout: 120_000 }, + () => { + it('procedure A finishes independent families while one waits for review', async () => { + const input = migrationInput(); + const { childRunId } = await startParent('dynamicMissionRun', [ + PROCEDURE_A, + input, + ]); + + const hook = await waitForMissionHook(`${input.tokenPrefix}:C-300`); + expect(hook.runId).toBe(childRunId); + // Acme and Globex apply while Initech is still waiting for a person. + const deadline = Date.now() + 30_000; + while ( + (await completedApplies(childRunId)) < 2 && + Date.now() < deadline + ) { + await sleep(250); + } + expect(await completedApplies(childRunId)).toBe(2); + expect((await readRunRecord(childRunId)).status).not.toBe('completed'); + + await resumeHook(hook, { approved: true, owner: 'sam' }); + const child = await awaitChildRun(childRunId); + expect(child.output).toMatchObject({ + families: [ + { family: 'C-100', status: 'applied' }, + { family: 'C-200', status: 'applied' }, + { family: 'C-300', status: 'applied' }, + ], + verification: { + ok: true, + violations: [], + counts: { accounts: 5, contacts: 3, provisional: 0 }, + accounts: MIGRATED_ACCOUNTS, + contacts: MIGRATED_CONTACTS, + }, + }); + }); + + it('procedure A leaves a rejected family out and migrates the rest', async () => { + const input = migrationInput(); + const { childRunId } = await startParent('dynamicMissionRun', [ + PROCEDURE_A, + input, + ]); + await resumeMissionHook(`${input.tokenPrefix}:C-300`, { + approved: false, + }); + + const child = await awaitChildRun(childRunId); + expect(child.output).toMatchObject({ + families: [ + { family: 'C-100', status: 'applied' }, + { family: 'C-200', status: 'applied' }, + { family: 'C-300', status: 'skipped' }, + ], + verification: { + ok: true, + accounts: MIGRATED_ACCOUNTS.slice(0, 4), + contacts: MIGRATED_CONTACTS.slice(0, 2), + }, + }); + }); + + it('procedure B, published later, loads provisionally and converges after one review', async () => { + const input = migrationInput(); + const { childRunId } = await startParent('dynamicMissionRun', [ + PROCEDURE_B, + input, + ]); + + const hook = await waitForMissionHook( + `${input.tokenPrefix}:consolidation` + ); + // Every row has already landed provisionally; nothing is consolidated. + expect(await completedApplies(childRunId)).toBe(1); + + await resumeHook(hook, { approved: true, owner: 'sam' }); + const child = await awaitChildRun(childRunId); + expect(child.output).toMatchObject({ + approved: true, + verification: { + ok: true, + violations: [], + counts: { accounts: 5, contacts: 3, provisional: 0 }, + accounts: MIGRATED_ACCOUNTS, + contacts: MIGRATED_CONTACTS, + }, + }); + }); + + it('procedure B reports provisional records when the consolidation is rejected', async () => { + const input = migrationInput(); + const { childRunId } = await startParent('dynamicMissionRun', [ + PROCEDURE_B, + input, + ]); + await resumeMissionHook(`${input.tokenPrefix}:consolidation`, { + approved: false, + }); + + const child = await awaitChildRun(childRunId); + expect(child.output).toMatchObject({ + approved: false, + verification: { + ok: false, + counts: { accounts: 6, contacts: 4, provisional: 6 }, + }, + }); + }); + + it('refuses a plan changed after it was staged', async () => { + const { childRunId } = await startParent('dynamicMissionRun', [ + publishProcedure('tampered', TAMPERED_PLAN_SOURCE), + migrationInput(), + ]); + const child = trackRun(getRun(childRunId), { + testName: `${getCurrentTest()?.name ?? 'dynamic workflow'} [dynamic run]`, + }); + await expect(child.returnValue).rejects.toThrow( + /changed after it was staged/ + ); + }); + } +); diff --git a/workbench/example/tsconfig.tsbuildinfo b/workbench/example/tsconfig.tsbuildinfo new file mode 100644 index 0000000000..95dd67cb22 --- /dev/null +++ b/workbench/example/tsconfig.tsbuildinfo @@ -0,0 +1 @@ +{"fileNames":["../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es5.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2016.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2017.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2018.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2019.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2020.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2021.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2022.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2023.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2024.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2025.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.esnext.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.dom.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.dom.iterable.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.core.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.collection.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.generator.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.promise.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2016.intl.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2017.date.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2017.object.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2017.string.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2017.intl.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2018.intl.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2018.promise.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2019.array.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2019.object.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2019.string.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2019.intl.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2020.date.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2020.promise.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2020.string.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2020.intl.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2020.number.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2021.promise.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2021.string.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2021.weakref.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2021.intl.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2022.array.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2022.error.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2022.intl.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2022.object.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2022.string.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2022.regexp.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2023.array.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2023.collection.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2023.intl.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2024.collection.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2024.object.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2024.promise.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2024.regexp.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2024.string.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2025.collection.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2025.float16.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2025.intl.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2025.iterator.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2025.promise.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2025.regexp.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.esnext.array.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.esnext.collection.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.esnext.date.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.esnext.decorators.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.esnext.disposable.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.esnext.error.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.esnext.intl.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.esnext.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.esnext.temporal.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.esnext.typedarrays.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.decorators.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.decorators.legacy.d.ts","../../node_modules/.pnpm/@vercel+otel@1.13.0_@opentelemetry+api-logs@0.57.2_@opentelemetry+api@1.9.1_@openteleme_cef2455b7bbd346e216bd42a495369a8/node_modules/@vercel/otel/dist/types/index.d.ts","./instrumentation.ts","./workflows/98_duplicate_case.ts","./api/test-direct-step-call.ts","./workflows/0_workflow_only.ts","../../node_modules/.pnpm/@types+json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts","../../node_modules/.pnpm/@ai-sdk+provider@4.0.9/node_modules/@ai-sdk/provider/dist/index.d.ts","../../node_modules/.pnpm/@standard-schema+spec@1.1.0/node_modules/@standard-schema/spec/dist/index.d.ts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v3/helpers/typealiases.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v3/helpers/util.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v3/zoderror.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v3/locales/en.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v3/errors.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v3/helpers/parseutil.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v3/helpers/enumutil.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v3/helpers/errorutil.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v3/helpers/partialutil.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v3/standard-schema.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v3/types.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v3/external.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v3/index.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/json-schema.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/standard-schema.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/registries.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/to-json-schema.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/util.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/versions.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/schemas.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/checks.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/errors.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/core.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/parse.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/memoizer.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/regexes.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/ar.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/az.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/be.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/bg.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/bn.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/ca.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/ckb.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/cs.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/da.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/de.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/el.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/en.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/eo.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/es.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/fa.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/fi.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/fr.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/fr-ca.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/gu.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/he.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/hi.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/hr.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/hu.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/hy.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/id.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/is.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/it.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/ja.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/ka.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/kh.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/km.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/kn.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/ko.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/lt.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/mk.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/ms.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/ne.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/nl.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/nn.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/no.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/ota.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/ps.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/pl.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/pt.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/pt-br.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/ro.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/ru.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/sk.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/sl.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/sv.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/ta.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/th.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/tk.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/tr.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/ua.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/uk.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/ur.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/uz.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/vi.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/zh-cn.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/zh-tw.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/yo.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/index.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/doc.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/compile.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/api.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/json-schema-processors.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/json-schema-generator.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/index.d.cts","../../node_modules/.pnpm/@workflow+serde@4.1.0/node_modules/@workflow/serde/dist/index.d.ts","../../node_modules/.pnpm/eventsource-parser@3.1.0/node_modules/eventsource-parser/dist/stream.d.ts","../../node_modules/.pnpm/@ai-sdk+provider-utils@5.0.34_zod@4.5.4/node_modules/@ai-sdk/provider-utils/dist/index.d.ts","../../node_modules/.pnpm/@ai-sdk+gateway@4.0.69_zod@4.5.4/node_modules/@ai-sdk/gateway/dist/index.d.ts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/classic/errors.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/classic/parse.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/classic/schemas.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/classic/checks.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/classic/compat.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/classic/from-json-schema.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/classic/deep-partial.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/classic/in-out.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/classic/iso.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/classic/coerce.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/classic/external.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/classic/index.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/index.d.cts","../../node_modules/.pnpm/ai@7.0.85_zod@4.5.4/node_modules/ai/dist/index.d.ts","../../node_modules/.pnpm/@ai-sdk+workflow@2.0.15_workflow@packages+workflow_zod@4.5.4/node_modules/@ai-sdk/workflow/dist/index.d.ts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/index.d.cts","../../packages/world/dist/shared.d.ts","../../packages/world/dist/analytics.d.ts","../../packages/world/dist/serialization.d.ts","../../packages/world/dist/hooks.d.ts","../../packages/world/dist/runs.d.ts","../../packages/world/dist/steps.d.ts","../../packages/world/dist/waits.d.ts","../../packages/world/dist/event-metadata.d.ts","../../packages/world/dist/events.d.ts","../../packages/world/dist/attributes-validation.d.ts","../../packages/world/dist/attributes.d.ts","../../packages/world/dist/env-config.d.ts","../../packages/world/dist/queue.d.ts","../../packages/world/dist/interfaces.d.ts","../../packages/world/dist/invocation.d.ts","../../packages/world/dist/node-http-flag.d.ts","../../packages/world/dist/recovery.d.ts","../../packages/world/dist/slot-identity.d.ts","../../packages/world/dist/spec-version.d.ts","../../packages/world/dist/ulid.d.ts","../../packages/world/dist/index.d.ts","../../packages/core/dist/runtime/world.d.ts","../../packages/core/dist/runtime/world-init.d.ts","../../node_modules/.pnpm/@types+ms@2.1.0/node_modules/@types/ms/index.d.ts","../../packages/errors/dist/error-codes.d.ts","../../packages/errors/dist/index.d.ts","../../packages/serde/dist/index.d.ts","../../packages/core/dist/runtime/runs.d.ts","../../packages/core/dist/runtime/run.d.ts","../../packages/core/dist/schemas.d.ts","../../packages/core/dist/create-hook.d.ts","../../node_modules/.pnpm/@standard-schema+spec@1.0.0/node_modules/@standard-schema/spec/dist/index.d.ts","../../packages/core/dist/define-hook.d.ts","../../packages/core/dist/workflow/set-attributes.d.ts","../../packages/core/dist/set-attributes.d.ts","../../packages/core/dist/sleep.d.ts","../../packages/core/dist/step/get-step-metadata.d.ts","../../packages/core/dist/workflow/get-workflow-metadata.d.ts","../../packages/core/dist/step/get-workflow-metadata.d.ts","../../packages/core/dist/step/writable-stream.d.ts","../../packages/core/dist/index.d.ts","../../packages/workflow/dist/stdlib.d.ts","../../packages/workflow/dist/index.d.ts","../../node_modules/.pnpm/@ai-sdk+provider-utils@5.0.34_zod@4.5.4/node_modules/@ai-sdk/provider-utils/dist/test/index.d.ts","../../node_modules/.pnpm/ai@7.0.85_zod@4.5.4/node_modules/ai/dist/test/index.d.ts","./workflows/ai-sdk-test-provider.ts","./workflows/100_durable_agent_e2e.ts","./workflows/102_plain_module_hook.ts","./workflows/10_single_stmt_control_flow.ts","./workflows/1_simple.ts","./workflows/2_control_flow.ts","./workflows/3_streams.ts","./workflows/4_ai.ts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/internal/builtin-types.d.mts","../../node_modules/.pnpm/buffer@6.0.3/node_modules/buffer/index.d.ts","../../node_modules/.pnpm/node_modules/undici-types/header.d.ts","../../node_modules/.pnpm/node_modules/undici-types/readable.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/compatibility/disposable.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/compatibility/indexable.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/compatibility/iterators.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/compatibility/index.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/globals.typedarray.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/buffer.buffer.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/globals.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/web-globals/abortcontroller.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/web-globals/domexception.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/web-globals/events.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/index.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/web-globals/fetch.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/web-globals/navigator.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/web-globals/storage.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/assert.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/assert/strict.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/async_hooks.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/buffer.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/child_process.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/cluster.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/console.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/constants.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/crypto.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/dgram.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/diagnostics_channel.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/dns.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/dns/promises.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/domain.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/events.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/fs.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/fs/promises.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/http.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/http2.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/https.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/inspector.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/inspector.generated.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/module.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/net.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/os.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/path.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/perf_hooks.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/process.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/punycode.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/querystring.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/readline.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/readline/promises.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/repl.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/sea.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/sqlite.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/stream.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/stream/promises.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/stream/consumers.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/stream/web.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/string_decoder.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/test.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/timers.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/timers/promises.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/tls.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/trace_events.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/tty.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/url.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/util.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/v8.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/vm.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/wasi.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/worker_threads.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/zlib.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/index.d.ts","../../node_modules/.pnpm/node_modules/undici-types/file.d.ts","../../node_modules/.pnpm/node_modules/undici-types/fetch.d.ts","../../node_modules/.pnpm/node_modules/undici-types/formdata.d.ts","../../node_modules/.pnpm/node_modules/undici-types/connector.d.ts","../../node_modules/.pnpm/node_modules/undici-types/client.d.ts","../../node_modules/.pnpm/node_modules/undici-types/errors.d.ts","../../node_modules/.pnpm/node_modules/undici-types/dispatcher.d.ts","../../node_modules/.pnpm/node_modules/undici-types/global-dispatcher.d.ts","../../node_modules/.pnpm/node_modules/undici-types/global-origin.d.ts","../../node_modules/.pnpm/node_modules/undici-types/pool-stats.d.ts","../../node_modules/.pnpm/node_modules/undici-types/pool.d.ts","../../node_modules/.pnpm/node_modules/undici-types/handlers.d.ts","../../node_modules/.pnpm/node_modules/undici-types/balanced-pool.d.ts","../../node_modules/.pnpm/node_modules/undici-types/agent.d.ts","../../node_modules/.pnpm/node_modules/undici-types/mock-interceptor.d.ts","../../node_modules/.pnpm/node_modules/undici-types/mock-agent.d.ts","../../node_modules/.pnpm/node_modules/undici-types/mock-client.d.ts","../../node_modules/.pnpm/node_modules/undici-types/mock-pool.d.ts","../../node_modules/.pnpm/node_modules/undici-types/mock-errors.d.ts","../../node_modules/.pnpm/node_modules/undici-types/proxy-agent.d.ts","../../node_modules/.pnpm/node_modules/undici-types/env-http-proxy-agent.d.ts","../../node_modules/.pnpm/node_modules/undici-types/retry-handler.d.ts","../../node_modules/.pnpm/node_modules/undici-types/retry-agent.d.ts","../../node_modules/.pnpm/node_modules/undici-types/api.d.ts","../../node_modules/.pnpm/node_modules/undici-types/interceptors.d.ts","../../node_modules/.pnpm/node_modules/undici-types/util.d.ts","../../node_modules/.pnpm/node_modules/undici-types/cookies.d.ts","../../node_modules/.pnpm/node_modules/undici-types/patch.d.ts","../../node_modules/.pnpm/node_modules/undici-types/websocket.d.ts","../../node_modules/.pnpm/node_modules/undici-types/eventsource.d.ts","../../node_modules/.pnpm/node_modules/undici-types/filereader.d.ts","../../node_modules/.pnpm/node_modules/undici-types/diagnostics-channel.d.ts","../../node_modules/.pnpm/node_modules/undici-types/content-type.d.ts","../../node_modules/.pnpm/node_modules/undici-types/cache.d.ts","../../node_modules/.pnpm/node_modules/undici-types/index.d.ts","../../node_modules/.pnpm/node_modules/undici/types/utility.d.ts","../../node_modules/.pnpm/node_modules/undici/types/header.d.ts","../../node_modules/.pnpm/node_modules/undici/types/readable.d.ts","../../node_modules/.pnpm/node_modules/undici/types/fetch.d.ts","../../node_modules/.pnpm/node_modules/undici/types/formdata.d.ts","../../node_modules/.pnpm/node_modules/undici/types/connector.d.ts","../../node_modules/.pnpm/node_modules/undici/types/client-stats.d.ts","../../node_modules/.pnpm/node_modules/undici/types/client.d.ts","../../node_modules/.pnpm/node_modules/undici/types/errors.d.ts","../../node_modules/.pnpm/node_modules/undici/types/dispatcher.d.ts","../../node_modules/.pnpm/node_modules/undici/types/global-dispatcher.d.ts","../../node_modules/.pnpm/node_modules/undici/types/global-origin.d.ts","../../node_modules/.pnpm/node_modules/undici/types/pool-stats.d.ts","../../node_modules/.pnpm/node_modules/undici/types/pool.d.ts","../../node_modules/.pnpm/node_modules/undici/types/handlers.d.ts","../../node_modules/.pnpm/node_modules/undici/types/balanced-pool.d.ts","../../node_modules/.pnpm/node_modules/undici/types/round-robin-pool.d.ts","../../node_modules/.pnpm/node_modules/undici/types/h2c-client.d.ts","../../node_modules/.pnpm/node_modules/undici/types/agent.d.ts","../../node_modules/.pnpm/node_modules/undici/types/mock-interceptor.d.ts","../../node_modules/.pnpm/node_modules/undici/types/mock-call-history.d.ts","../../node_modules/.pnpm/node_modules/undici/types/mock-agent.d.ts","../../node_modules/.pnpm/node_modules/undici/types/mock-client.d.ts","../../node_modules/.pnpm/node_modules/undici/types/mock-pool.d.ts","../../node_modules/.pnpm/node_modules/undici/types/snapshot-agent.d.ts","../../node_modules/.pnpm/node_modules/undici/types/mock-errors.d.ts","../../node_modules/.pnpm/node_modules/undici/types/proxy-agent.d.ts","../../node_modules/.pnpm/node_modules/undici/types/socks5-proxy-agent.d.ts","../../node_modules/.pnpm/node_modules/undici/types/env-http-proxy-agent.d.ts","../../node_modules/.pnpm/node_modules/undici/types/retry-handler.d.ts","../../node_modules/.pnpm/node_modules/undici/types/retry-agent.d.ts","../../node_modules/.pnpm/node_modules/undici/types/api.d.ts","../../node_modules/.pnpm/node_modules/undici/types/cache-interceptor.d.ts","../../node_modules/.pnpm/node_modules/undici/types/interceptors.d.ts","../../node_modules/.pnpm/node_modules/undici/types/util.d.ts","../../node_modules/.pnpm/node_modules/undici/types/cookies.d.ts","../../node_modules/.pnpm/node_modules/undici/types/patch.d.ts","../../node_modules/.pnpm/node_modules/undici/types/websocket.d.ts","../../node_modules/.pnpm/node_modules/undici/types/eventsource.d.ts","../../node_modules/.pnpm/node_modules/undici/types/diagnostics-channel.d.ts","../../node_modules/.pnpm/node_modules/undici/types/content-type.d.ts","../../node_modules/.pnpm/node_modules/undici/types/cache.d.ts","../../node_modules/.pnpm/node_modules/undici/types/index.d.ts","../../node_modules/.pnpm/node_modules/undici/index.d.ts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/internal/types.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/internal/headers.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/internal/shim-types.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/core/streaming.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/internal/request-options.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/internal/utils/log.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/core/error.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/pagination.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/internal/parse.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/core/api-promise.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/core/pagination.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/internal/uploads.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/internal/to-file.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/core/uploads.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/core/resource.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/shared.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/completions.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/chat/completions/messages.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/chat/completions/index.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/chat/completions.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/error.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/lib/eventstream.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/lib/abstractchatcompletionrunner.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/lib/chatcompletionstream.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/lib/responsesparser.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/lib/responses/eventtypes.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/lib/responses/responsestream.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/responses/input-items.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/responses/responses.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/lib/parser.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/lib/chatcompletionstreamingrunner.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/lib/jsonschema.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/lib/runnablefunction.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/lib/chatcompletionrunner.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/chat/completions/completions.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/chat/chat.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/chat/index.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/audio/speech.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/audio/transcriptions.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/audio/translations.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/audio/audio.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/batches.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/beta/threads/messages.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/beta/threads/runs/steps.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/lib/assistantstream.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/beta/threads/runs/runs.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/beta/threads/threads.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/beta/assistants.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/beta/realtime/sessions.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/beta/realtime/transcription-sessions.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/beta/realtime/realtime.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/beta/beta.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/containers/files/content.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/containers/files/files.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/containers/containers.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/conversations/items.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/conversations/conversations.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/embeddings.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/graders/grader-models.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/evals/runs/output-items.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/evals/runs/runs.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/evals/evals.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/files.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/fine-tuning/methods.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/fine-tuning/alpha/graders.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/fine-tuning/alpha/alpha.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/fine-tuning/checkpoints/permissions.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/fine-tuning/checkpoints/checkpoints.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/fine-tuning/jobs/checkpoints.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/fine-tuning/jobs/jobs.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/fine-tuning/fine-tuning.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/graders/graders.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/images.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/models.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/moderations.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/realtime/calls.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/realtime/client-secrets.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/realtime/realtime.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/uploads/parts.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/uploads/uploads.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/uploads.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/vector-stores/files.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/vector-stores/file-batches.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/vector-stores/vector-stores.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/webhooks.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/index.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/client.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/azure.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/index.d.mts","./workflows/5_hooks.ts","../../node_modules/.pnpm/@types+lodash@4.17.20/node_modules/@types/lodash/common/common.d.ts","../../node_modules/.pnpm/@types+lodash@4.17.20/node_modules/@types/lodash/common/array.d.ts","../../node_modules/.pnpm/@types+lodash@4.17.20/node_modules/@types/lodash/common/collection.d.ts","../../node_modules/.pnpm/@types+lodash@4.17.20/node_modules/@types/lodash/common/date.d.ts","../../node_modules/.pnpm/@types+lodash@4.17.20/node_modules/@types/lodash/common/function.d.ts","../../node_modules/.pnpm/@types+lodash@4.17.20/node_modules/@types/lodash/common/lang.d.ts","../../node_modules/.pnpm/@types+lodash@4.17.20/node_modules/@types/lodash/common/math.d.ts","../../node_modules/.pnpm/@types+lodash@4.17.20/node_modules/@types/lodash/common/number.d.ts","../../node_modules/.pnpm/@types+lodash@4.17.20/node_modules/@types/lodash/common/object.d.ts","../../node_modules/.pnpm/@types+lodash@4.17.20/node_modules/@types/lodash/common/seq.d.ts","../../node_modules/.pnpm/@types+lodash@4.17.20/node_modules/@types/lodash/common/string.d.ts","../../node_modules/.pnpm/@types+lodash@4.17.20/node_modules/@types/lodash/common/util.d.ts","../../node_modules/.pnpm/@types+lodash@4.17.20/node_modules/@types/lodash/index.d.ts","../../node_modules/.pnpm/@types+lodash.chunk@4.2.9/node_modules/@types/lodash.chunk/index.d.ts","./workflows/6_batching.ts","./workflows/7_full.ts","../../packages/core/dist/global.d.ts","../../packages/core/dist/encryption.d.ts","../../packages/core/dist/sealed-box.d.ts","../../packages/core/dist/serialization/encryption.d.ts","../../packages/core/dist/runtime/helpers.d.ts","../../packages/utils/dist/debug-log.d.ts","../../packages/utils/dist/global-singleton.d.ts","../../packages/utils/dist/parse-name.d.ts","../../packages/utils/dist/pluralize.d.ts","../../packages/utils/dist/promise.d.ts","../../packages/utils/dist/time.d.ts","../../packages/utils/dist/workflow-routes.d.ts","../../packages/utils/dist/world-target.d.ts","../../packages/utils/dist/index.d.ts","../../packages/core/dist/flushable-stream.d.ts","../../packages/core/dist/serialization/compression.d.ts","../../packages/core/dist/serialization/types.d.ts","../../packages/core/dist/serialization/format.d.ts","../../node_modules/.pnpm/devalue@5.9.2/node_modules/devalue/types/index.d.ts","../../packages/core/dist/serialization/hardened.d.ts","../../packages/core/dist/serialization.d.ts","../../packages/core/dist/runtime/resume-hook.d.ts","../../packages/core/dist/runtime/dynamic-workflow.d.ts","../../packages/core/dist/runtime/start.d.ts","../../packages/core/dist/runtime.d.ts","../../packages/core/dist/runtime/lifecycle-hooks.d.ts","../../packages/workflow/dist/api.d.ts","./workflows/97_bench.ts","./workflows/97_bench_cadence.ts","./workflows/97_bench_rtt.ts","../../packages/workflow/dist/internal/errors.d.ts","./workflows/99_e2e.ts","./workflows/_imported_step_only.ts","./workflows/_plain_module_hooks.ts","./workflows/helpers.ts","./workflows/serde-models.ts","./workflows/serde-steps.ts"],"fileIdsList":[[96,195,275,287,304,305],[96,97,110,192,193,194,275,287,304,305],[275,287,304,305],[95,275,287,304,305],[96,195,210,275,287,304,305],[275,287,304,305,519],[275,287,304,305,507,509,510,511,512,513,514,515,516,517,518,519],[275,287,304,305,507,508,510,511,512,513,514,515,516,517,518,519],[275,287,304,305,508,509,510,511,512,513,514,515,516,517,518,519],[275,287,304,305,507,508,509,511,512,513,514,515,516,517,518,519],[275,287,304,305,507,508,509,510,512,513,514,515,516,517,518,519],[275,287,304,305,507,508,509,510,511,513,514,515,516,517,518,519],[275,287,304,305,507,508,509,510,511,512,514,515,516,517,518,519],[275,287,304,305,507,508,509,510,511,512,513,515,516,517,518,519],[275,287,304,305,507,508,509,510,511,512,513,514,516,517,518,519],[275,287,304,305,507,508,509,510,511,512,513,514,515,517,518,519],[275,287,304,305,507,508,509,510,511,512,513,514,515,516,518,519],[275,287,304,305,507,508,509,510,511,512,513,514,515,516,517,519],[275,287,304,305,507,508,509,510,511,512,513,514,515,516,517,518],[275,284,285,287,304,305],[275,286,287,304,305],[287,304,305],[275,287,292,304,305,322],[275,287,288,293,298,304,305,307,319,330,377],[275,287,288,289,298,304,305,307],[270,271,272,275,287,304,305],[275,287,290,304,305,331],[275,287,291,292,299,304,305,308],[275,287,292,304,305,319,327,377],[275,287,293,295,298,304,305,307,377],[275,286,287,294,304,305],[275,287,295,296,304,305],[275,287,297,298,304,305],[275,286,287,298,304,305],[275,287,298,299,300,304,305,319,330,377],[275,287,298,299,300,304,305,314,319,322,377],[275,287,295,298,301,304,305,307,319,330,372,377],[275,287,298,299,301,302,304,305,307,319,327,330,377],[275,287,301,303,304,305,319,327,330,377],[273,274,275,276,277,278,279,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336],[275,287,298,304,305],[275,287,304,305,306,330],[275,287,295,298,304,305,307,319,377],[275,287,304,305,308],[275,287,304,305,309],[275,286,287,304,305,310],[275,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,377],[275,287,304,305,312],[275,287,304,305,313],[275,287,298,304,305,314,315],[275,287,304,305,314,316,331,333],[275,287,299,304,305],[275,287,298,304,305,319,320,322,377],[275,287,304,305,321,322,377],[275,287,304,305,319,320],[275,287,304,305,322],[275,287,304,305,323],[275,284,287,304,305,319,324],[275,287,298,304,305,325,326],[275,287,304,305,325,326],[275,287,292,304,305,307,319,327,377],[275,287,304,305,328],[275,287,304,305,307,329],[275,287,301,304,305,313,330,377],[275,287,292,304,305,331],[275,287,304,305,319,332,377],[275,287,304,305,306,333,377],[275,287,304,305,334],[275,287,304,305,372],[275,287,298,300,304,305,310,319,322,330,332,333,335,372],[275,287,304,305,319,336,377],[96,195,196,209,275,287,301,304,305],[96,195,256,275,287,304,305],[275,287,304,305,330,344,348],[275,287,304,305,319,330,344],[275,287,304,305,339],[275,287,304,305,327,330,341,344],[275,287,304,305,307,327],[275,287,304,305,337],[275,287,304,305,337,339],[275,287,304,305,307,330,341,344],[268,269,275,287,298,304,305,319,330,340,343],[275,287,304,305,344,351],[268,275,287,304,305,342],[275,287,304,305,344,365,366],[275,287,304,305,322,330,337,340,344],[275,287,304,305,337,365],[275,287,304,305,337,338,339],[275,287,304,305,344],[275,287,304,305,338,339,340,341,342,343,344,345,346,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,366,367,368,369,370,371],[275,287,304,305,344,359],[275,287,304,305,344,351,352],[275,287,304,305,342,344,352,353],[275,287,304,305,343],[268,275,287,304,305,339,344],[275,287,304,305,344,348,352,353],[275,287,304,305,348],[275,287,304,305,330,342,344,347],[268,275,287,304,305,341,344,351],[275,287,304,305,319],[275,287,304,305,335,337,339,344,365],[275,287,304,305,415],[275,287,304,305,330,379,382,385,386],[275,287,304,305,319,330,382],[275,287,304,305,330,382,386],[275,287,304,305,376],[275,287,304,305,380],[275,287,304,305,330,378,379,382],[275,287,304,305,337,376],[275,287,304,305,307,330,378,382],[275,287,298,304,305,319,330,373,374,375,377,381],[275,287,304,305,382,391,399],[275,287,304,305,374,380],[275,287,304,305,382,409,410],[275,287,304,305,322,330,337,374,377,382],[275,287,304,305,337,376,377],[275,287,304,305,382],[275,287,304,305,330,378,382],[275,287,304,305,373],[275,287,304,305,376,377,378,380,381,382,383,384,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,410,411,412,413,414],[275,287,295,304,305,382,402,405],[275,287,304,305,382,391,392,393],[275,287,304,305,380,382,392,394],[275,287,304,305,381],[275,287,304,305,374,376,382],[275,287,304,305,382,386,392,394],[275,287,304,305,386],[275,287,304,305,330,380,382,385],[275,287,304,305,374,378,382,391],[275,287,304,305,319,377],[275,287,304,305,382,402],[275,287,304,305,394],[275,287,304,305,374,378,382,386],[275,287,304,305,322,335,337,376,377,382,409],[266,275,287,304,305,418,421,503],[266,275,287,304,305,417,418,421,422,423,426,427,430,433,445,451,452,457,458,468,471,473,474,478,479,487,488,489,490,491,494,496,500,501,502],[275,287,304,305,417,425,503],[275,287,304,305,421,425,426,503],[275,287,304,305,503],[275,287,304,305,419,503],[275,287,304,305,428,429],[275,287,304,305,423],[275,287,304,305,423,426,427,430,503,504],[275,287,304,305,421,424,503],[266,275,287,304,305,417,418,420],[266,275,287,304,305],[275,287,304,305,372,416],[266,275,287,304,305,421,503],[275,287,304,305,421,503],[275,287,304,305,421,433,436,438,447,449,450,505],[275,287,304,305,419,421,438,459,460,462,463,464],[275,287,304,305,436,439,446,449,505],[275,287,304,305,419,421,436,439,451,505],[275,287,304,305,419,436,439,440,446,449,505],[275,287,304,305,437],[275,287,304,305,432,436,445],[275,287,304,305,445],[275,287,304,305,421,438,441,442,445,505],[275,287,304,305,436,445,446],[275,287,304,305,447,448,450],[275,287,304,305,427],[275,287,304,305,431,454,455,456],[275,287,304,305,421,426,431],[275,287,304,305,420,421,426,430,431,455,457],[275,287,304,305,421,426,430,431,455,457],[275,287,304,305,421,426,427,431,432,458],[275,287,304,305,421,426,427,431,432,459,460,461,462,463],[275,287,304,305,431,463,464,467],[275,287,304,305,431,432,465,466,467],[275,287,304,305,421,426,427,431,432,464],[275,287,304,305,420,421,426,427,431,432,459,460,461,462,463,464],[275,287,304,305,421,426,427,431,432,460],[275,287,304,305,420,421,426,431,432,459,461,462,463,464],[275,287,304,305,431,432,451],[275,287,304,305,435],[275,287,304,305,420,421,426,427,431,432,433,434,439,440,446,447,449,450,451],[275,287,304,305,434,451],[275,287,304,305,421,427,431,451],[275,287,304,305,435,452],[275,287,304,305,420,421,426,431,433,451],[275,287,304,305,421,426,427,431,470],[275,287,304,305,421,426,427,430,431,469],[275,287,304,305,421,426,431,432,445,472],[275,287,304,305,421,426,427,431,445,473],[275,287,304,305,421,426,427,431,432,445,475,477],[275,287,304,305,421,426,427,431,477],[275,287,304,305,421,426,427,431,432,445,451,476],[275,287,304,305,421,426,427,430,431],[275,287,304,305,431,481],[275,287,304,305,421,426,431,475],[275,287,304,305,431,483],[275,287,304,305,421,426,427,431],[275,287,304,305,431,480,482,484,486],[275,287,304,305,421,427,431],[275,287,304,305,421,426,427,431,432,480,485],[275,287,304,305,431,475],[275,287,304,305,431,432,445],[275,287,304,305,420,421,426,430,431,489],[275,287,304,305,432,433,445,453,457,458,468,471,473,474,478,479,487,488,489,490,491,494,496,500,501],[275,287,304,305,421,426,431,445,494],[275,287,304,305,421,426,431,445,493,494],[275,287,304,305,431,432,445,492,493,494],[275,287,304,305,421,427,431,445],[275,287,304,305,420,421,426,427,431,432,441,443,444,445],[275,287,304,305,421,426,430,431],[275,287,304,305,421,426,431,479,495],[275,287,304,305,421,426,427,431,497,498,500],[275,287,304,305,421,426,427,431,497,500],[275,287,304,305,421,426,427,431,432,498,499],[275,287,304,305,418,431],[275,287,304,305,430],[207,275,287,304,305],[100,101,275,287,304,305],[98,99,100,102,103,108,275,287,304,305],[99,100,275,287,304,305],[108,275,287,304,305],[109,275,287,304,305],[100,275,287,304,305],[98,99,100,103,104,105,106,107,275,287,304,305],[98,99,110,275,287,304,305],[192,275,287,304,305],[192,199,275,287,304,305],[115,186,190,192,197,198,199,200,201,202,203,204,205,206,275,287,304,305],[111,113,199,275,287,304,305],[192,197,275,287,304,305],[112,118,192,198,275,287,304,305],[113,115,117,118,119,120,275,287,304,305],[115,117,119,120,275,287,304,305],[117,120,275,287,304,305],[115,117,119,275,287,304,305],[112,115,117,118,120,275,287,304,305],[111,113,114,115,116,117,118,119,120,121,122,123,186,187,188,189,190,191,275,287,304,305],[111,113,114,117,275,287,304,305],[113,114,117,275,287,304,305],[117,275,287,304,305],[111,112,114,115,116,118,119,120,275,287,304,305],[111,112,113,117,192,275,287,304,305],[117,118,119,120,275,287,304,305],[207,208,275,287,304,305],[119,275,287,304,305],[124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,275,287,304,305],[236,241,242,275,287,304,305],[233,243,244,275,287,304,305],[275,287,292,304,305],[275,287,304,305,536],[233,242,275,287,304,305],[238,243,245,247,248,249,251,252,275,287,304,305],[233,234,240,241,275,287,304,305,523,527,544,546],[238,275,287,304,305,546],[233,275,287,304,305,526],[238,241,275,287,304,305,526],[233,275,287,304,305,543],[233,239,240,275,287,304,305],[233,275,287,304,305],[233,241,275,287,304,305,527,545],[234,275,287,304,305],[275,287,304,305,524],[238,275,287,304,305,526,537,538,539,540,542],[275,287,304,305,524,525],[275,287,304,305,539],[275,287,304,305,541],[238,275,287,304,305],[246,275,287,304,305],[236,275,287,304,305],[250,275,287,304,305],[236,237,275,287,304,305],[275,287,304,305,528,529,530,531,532,533,534,535],[235,241,275,287,304,305,544,546,547,548],[235,253,254,275,287,304,305],[212,213,275,287,304,305],[221,275,287,304,305],[212,222,275,287,304,305],[212,213,216,217,218,219,220,275,287,304,305],[212,213,215,275,287,304,305],[213,214,215,216,217,218,219,221,223,224,225,226,227,228,229,230,231,232,275,287,304,305],[213,214,216,217,218,221,223,225,275,287,304,305],[209,275,287,304,305],[225,226,275,287,304,305],[212,275,287,304,305],[92,275,287,304,305],[90,275,287,304,305],[209,210,211,255,258,275,287,304,305],[255,275,287,304,305],[209,210,255,275,287,304,305],[255,275,287,304,305,505],[275,287,304,305,520],[255,275,287,304,305,549],[255,275,287,301,304,305,549,553],[257,275,287,304,305]],"fileInfos":[{"version":"bcd24271a113971ba9eb71ff8cb01bc6b0f872a85c23fdbe5d93065b375933cd","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f88bedbeb09c6f5a6645cb24c7c55f1aa22d19ae96c8e6959cbd8b85a707bc6","impliedFormat":1},{"version":"7fe93b39b810eadd916be8db880dd7f0f7012a5cc6ffb62de8f62a2117fa6f1f","impliedFormat":1},{"version":"bb0074cc08b84a2374af33d8bf044b80851ccc9e719a5e202eacf40db2c31600","impliedFormat":1},{"version":"1a7daebe4f45fb03d9ec53d60008fbf9ac45a697fdc89e4ce218bc94b94f94d6","impliedFormat":1},{"version":"f94b133a3cb14a288803be545ac2683e0d0ff6661bcd37e31aaaec54fc382aed","impliedFormat":1},{"version":"f59d0650799f8782fd74cf73c19223730c6d1b9198671b1c5b3a38e1188b5953","impliedFormat":1},{"version":"8a15b4607d9a499e2dbeed9ec0d3c0d7372c850b2d5f1fb259e8f6d41d468a84","impliedFormat":1},{"version":"26e0fe14baee4e127f4365d1ae0b276f400562e45e19e35fd2d4c296684715e6","impliedFormat":1},{"version":"1e9332c23e9a907175e0ffc6a49e236f97b48838cc8aec9ce7e4cec21e544b65","impliedFormat":1},{"version":"3753fbc1113dc511214802a2342280a8b284ab9094f6420e7aa171e868679f91","impliedFormat":1},{"version":"999ca32883495a866aa5737fe1babc764a469e4cde6ee6b136a4b9ae68853e4b","impliedFormat":1},{"version":"17f13ecb98cbc39243f2eee1f16d45cd8ec4706b03ee314f1915f1a8b42f6984","impliedFormat":1},{"version":"d6b1eba8496bdd0eed6fc8a685768fe01b2da4a0388b5fe7df558290bffcf32f","affectsGlobalScope":true,"impliedFormat":1},{"version":"7f57fc4404ff020bc45b9c620aff2b40f700b95fe31164024c453a5e3c163c54","impliedFormat":1},{"version":"eadcffda2aa84802c73938e589b9e58248d74c59cb7fcbca6474e3435ac15504","affectsGlobalScope":true,"impliedFormat":1},{"version":"105ba8ff7ba746404fe1a2e189d1d3d2e0eb29a08c18dded791af02f29fb4711","affectsGlobalScope":true,"impliedFormat":1},{"version":"00343ca5b2e3d48fa5df1db6e32ea2a59afab09590274a6cccb1dbae82e60c7c","affectsGlobalScope":true,"impliedFormat":1},{"version":"ebd9f816d4002697cb2864bea1f0b70a103124e18a8cd9645eeccc09bdf80ab4","affectsGlobalScope":true,"impliedFormat":1},{"version":"2c1afac30a01772cd2a9a298a7ce7706b5892e447bb46bdbeef720f7b5da77ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"7b0225f483e4fa685625ebe43dd584bb7973bbd84e66a6ba7bbe175ee1048b4f","affectsGlobalScope":true,"impliedFormat":1},{"version":"c0a4b8ac6ce74679c1da2b3795296f5896e31c38e888469a8e0f99dc3305de60","affectsGlobalScope":true,"impliedFormat":1},{"version":"3084a7b5f569088e0146533a00830e206565de65cae2239509168b11434cd84f","affectsGlobalScope":true,"impliedFormat":1},{"version":"c5079c53f0f141a0698faa903e76cb41cd664e3efb01cc17a5c46ec2eb0bef42","affectsGlobalScope":true,"impliedFormat":1},{"version":"32cafbc484dea6b0ab62cf8473182bbcb23020d70845b406f80b7526f38ae862","affectsGlobalScope":true,"impliedFormat":1},{"version":"fca4cdcb6d6c5ef18a869003d02c9f0fd95df8cfaf6eb431cd3376bc034cad36","affectsGlobalScope":true,"impliedFormat":1},{"version":"b93ec88115de9a9dc1b602291b85baf825c85666bf25985cc5f698073892b467","affectsGlobalScope":true,"impliedFormat":1},{"version":"f5c06dcc3fe849fcb297c247865a161f995cc29de7aa823afdd75aaaddc1419b","affectsGlobalScope":true,"impliedFormat":1},{"version":"b77e16112127a4b169ef0b8c3a4d730edf459c5f25fe52d5e436a6919206c4d7","affectsGlobalScope":true,"impliedFormat":1},{"version":"fbffd9337146eff822c7c00acbb78b01ea7ea23987f6c961eba689349e744f8c","affectsGlobalScope":true,"impliedFormat":1},{"version":"a995c0e49b721312f74fdfb89e4ba29bd9824c770bbb4021d74d2bf560e4c6bd","affectsGlobalScope":true,"impliedFormat":1},{"version":"c7b3542146734342e440a84b213384bfa188835537ddbda50d30766f0593aff9","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce6180fa19b1cccd07ee7f7dbb9a367ac19c0ed160573e4686425060b6df7f57","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f02e2476bccb9dbe21280d6090f0df17d2f66b74711489415a8aa4df73c9675","affectsGlobalScope":true,"impliedFormat":1},{"version":"45e3ab34c1c013c8ab2dc1ba4c80c780744b13b5676800ae2e3be27ae862c40c","affectsGlobalScope":true,"impliedFormat":1},{"version":"805c86f6cca8d7702a62a844856dbaa2a3fd2abef0536e65d48732441dde5b5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"e42e397f1a5a77994f0185fd1466520691456c772d06bf843e5084ceb879a0ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"f4c2b41f90c95b1c532ecc874bd3c111865793b23aebcc1c3cbbabcd5d76ffb0","affectsGlobalScope":true,"impliedFormat":1},{"version":"ab26191cfad5b66afa11b8bf935ef1cd88fabfcb28d30b2dfa6fad877d050332","affectsGlobalScope":true,"impliedFormat":1},{"version":"2088bc26531e38fb05eedac2951480db5309f6be3fa4a08d2221abb0f5b4200d","affectsGlobalScope":true,"impliedFormat":1},{"version":"cb9d366c425fea79716a8fb3af0d78e6b22ebbab3bd64d25063b42dc9f531c1e","affectsGlobalScope":true,"impliedFormat":1},{"version":"500934a8089c26d57ebdb688fc9757389bb6207a3c8f0674d68efa900d2abb34","affectsGlobalScope":true,"impliedFormat":1},{"version":"689da16f46e647cef0d64b0def88910e818a5877ca5379ede156ca3afb780ac3","affectsGlobalScope":true,"impliedFormat":1},{"version":"bc21cc8b6fee4f4c2440d08035b7ea3c06b3511314c8bab6bef7a92de58a2593","affectsGlobalScope":true,"impliedFormat":1},{"version":"7ca53d13d2957003abb47922a71866ba7cb2068f8d154877c596d63c359fed25","affectsGlobalScope":true,"impliedFormat":1},{"version":"54725f8c4df3d900cb4dac84b64689ce29548da0b4e9b7c2de61d41c79293611","affectsGlobalScope":true,"impliedFormat":1},{"version":"e5594bc3076ac29e6c1ebda77939bc4c8833de72f654b6e376862c0473199323","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f3eb332c2d73e729f3364fcc0c2b375e72a121e8157d25a82d67a138c83a95c","affectsGlobalScope":true,"impliedFormat":1},{"version":"6f4427f9642ce8d500970e4e69d1397f64072ab73b97e476b4002a646ac743b1","affectsGlobalScope":true,"impliedFormat":1},{"version":"48915f327cd1dea4d7bd358d9dc7732f58f9e1626a29cc0c05c8c692419d9bb7","affectsGlobalScope":true,"impliedFormat":1},{"version":"b7bf9377723203b5a6a4b920164df22d56a43f593269ba6ae1fdc97774b68855","affectsGlobalScope":true,"impliedFormat":1},{"version":"db9709688f82c9e5f65a119c64d835f906efe5f559d08b11642d56eb85b79357","affectsGlobalScope":true,"impliedFormat":1},{"version":"4b25b8c874acd1a4cf8444c3617e037d444d19080ac9f634b405583fd10ce1f7","affectsGlobalScope":true,"impliedFormat":1},{"version":"37be57d7c90cf1f8112ee2636a068d8fd181289f82b744160ec56a7dc158a9f5","affectsGlobalScope":true,"impliedFormat":1},{"version":"a917a49ac94cd26b754ab84e113369a75d1a47a710661d7cd25e961cc797065f","affectsGlobalScope":true,"impliedFormat":1},{"version":"6d3261badeb7843d157ef3e6f5d1427d0eeb0af0cf9df84a62cfd29fd47ac86e","affectsGlobalScope":true,"impliedFormat":1},{"version":"195daca651dde22f2167ac0d0a05e215308119a3100f5e6268e8317d05a92526","affectsGlobalScope":true,"impliedFormat":1},{"version":"8b11e4285cd2bb164a4dc09248bdec69e9842517db4ca47c1ba913011e44ff2f","affectsGlobalScope":true,"impliedFormat":1},{"version":"0508571a52475e245b02bc50fa1394065a0a3d05277fbf5120c3784b85651799","affectsGlobalScope":true,"impliedFormat":1},{"version":"8f9af488f510c3015af3cc8c267a9e9d96c4dd38a1fdff0e11dc5a544711415b","affectsGlobalScope":true,"impliedFormat":1},{"version":"fc611fea8d30ea72c6bbfb599c9b4d393ce22e2f5bfef2172534781e7d138104","affectsGlobalScope":true,"impliedFormat":1},{"version":"0bd714129fca875f7d4c477a1a392200b0bcd13fb2e80928cd334b63830ea047","affectsGlobalScope":true,"impliedFormat":1},{"version":"e2c9037ae6cd2c52d80ceef0b3c5ffdb488627d71529cf4f63776daf11161c9a","affectsGlobalScope":true,"impliedFormat":1},{"version":"135d5cf4d345f59f1a9caadfafcd858d3d9cc68290db616cc85797224448cccc","affectsGlobalScope":true,"impliedFormat":1},{"version":"bc238c3f81c2984751932b6aab223cd5b830e0ac6cad76389e5e9d2ffc03287d","affectsGlobalScope":true,"impliedFormat":1},{"version":"4a07f9b76d361f572620927e5735b77d6d2101c23cdd94383eb5b706e7b36357","affectsGlobalScope":true,"impliedFormat":1},{"version":"7c4e8dc6ab834cc6baa0227e030606d29e3e8449a9f67cdf5605ea5493c4db29","affectsGlobalScope":true,"impliedFormat":1},{"version":"de7ba0fd02e06cd9a5bd4ab441ed0e122735786e67dde1e849cced1cd8b46b78","affectsGlobalScope":true,"impliedFormat":1},{"version":"6148e4e88d720a06855071c3db02069434142a8332cf9c182cda551adedf3156","affectsGlobalScope":true,"impliedFormat":1},{"version":"d63dba625b108316a40c95a4425f8d4294e0deeccfd6c7e59d819efa19e23409","affectsGlobalScope":true,"impliedFormat":1},{"version":"0568d6befee03dd435bed4fc25c4e46865b24bdcb8c563fdc21f580a2c301904","affectsGlobalScope":true,"impliedFormat":1},{"version":"30d62269b05b584741f19a5369852d5d34895aa2ac4fd948956f886d15f9cc0d","affectsGlobalScope":true,"impliedFormat":1},{"version":"f128dae7c44d8f35ee42e0a437000a57c9f06cc04f8b4fb42eebf44954d53dc8","affectsGlobalScope":true,"impliedFormat":1},{"version":"ffbe6d7b295306b2ba88030f65b74c107d8d99bdcf596ea99c62a02f606108b0","affectsGlobalScope":true,"impliedFormat":1},{"version":"996fb27b15277369c68a4ba46ed138b4e9e839a02fb4ec756f7997629242fd9f","affectsGlobalScope":true,"impliedFormat":1},{"version":"79b712591b270d4778c89706ca2cfc56ddb8c3f895840e477388f1710dc5eda9","affectsGlobalScope":true,"impliedFormat":1},{"version":"20884846cef428b992b9bd032e70a4ef88e349263f63aeddf04dda837a7dba26","affectsGlobalScope":true,"impliedFormat":1},{"version":"5fcab789c73a97cd43828ee3cc94a61264cf24d4c44472ce64ced0e0f148bdb2","affectsGlobalScope":true,"impliedFormat":1},{"version":"db59a81f070c1880ad645b2c0275022baa6a0c4f0acdc58d29d349c6efcf0903","affectsGlobalScope":true,"impliedFormat":1},{"version":"673294292640f5722b700e7d814e17aaf7d93f83a48a2c9b38f33cbc940ad8b0","affectsGlobalScope":true,"impliedFormat":1},{"version":"d786b48f934cbca483b3c6d0a798cb43bbb4ada283e76fb22c28e53ae05b9e69","affectsGlobalScope":true,"impliedFormat":1},{"version":"1ecb8e347cb6b2a8927c09b86263663289418df375f5e68e11a0ae683776978f","affectsGlobalScope":true,"impliedFormat":1},{"version":"142efd4ce210576f777dc34df121777be89eda476942d6d6663b03dcb53be3ff","affectsGlobalScope":true,"impliedFormat":1},{"version":"379bc41580c2d774f82e828c70308f24a005b490c25ba34d679d84bcf05c3d9d","affectsGlobalScope":true,"impliedFormat":1},{"version":"ed484fb2aa8a1a23d0277056ec3336e0a0b52f9b8d6a961f338a642faf43235d","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ffedae1d1c2d53fdbca1c96d3c7dda544281f7d262f99b6880634f8fd8d9820","affectsGlobalScope":true,"impliedFormat":1},{"version":"83a730b125d477dd264df8ba479afab27a3dae7152b005c214ab94dc7ee44fd3","affectsGlobalScope":true,"impliedFormat":1},{"version":"1ce14b81c5cc821994aa8ec1d42b220dd41b27fcc06373bce3958af7421b77d4","affectsGlobalScope":true,"impliedFormat":1},{"version":"b3a048b3e9302ef9a34ef4ebb9aecfb28b66abb3bce577206a79fee559c230da","affectsGlobalScope":true,"impliedFormat":1},{"version":"14d5ccd6f427b4d1e74a214f59c55740b2079d032a947a706ba0f07cd5599dcd","impliedFormat":99},{"version":"bdfc0dc1b1e449cc8f9380a1408c001cb233677ea972d82a64f4f2ebb1d983e3","impliedFormat":99},{"version":"9b10c9c23741531ffa1fac6edf95c264b3ba541cbe153a22f1b6a2a884311096","impliedFormat":99},{"version":"8a8994aa440f8e76484161d8c63dc8b3539e49bd679241b9d7703e81a20f5375","impliedFormat":99},{"version":"02c012041e0c4f31c7b962ef807ae283635503aa59332f10b3d24d7b6d658431","impliedFormat":99},{"version":"f3d8c757e148ad968f0d98697987db363070abada5f503da3c06aefd9d4248c1","impliedFormat":1},{"version":"de56b7c4893e7457ba7fcae5e772690fa30e34af12b4424a17ceffbc212a0bbf","impliedFormat":99},{"version":"bdd14f07b4eca0b4b5203b85b8dbc4d084c749fa590bee5ea613e1641dcd3b29","impliedFormat":99},{"version":"d3cfde44f8089768ebb08098c96d01ca260b88bccf238d55eee93f1c620ff5a5","impliedFormat":1},{"version":"293eadad9dead44c6fd1db6de552663c33f215c55a1bfa2802a1bceed88ff0ec","impliedFormat":1},{"version":"d5aefcc5d3381ee4fc8b31672dc34ee8c587c95f17666c43e7b74eda31fb8af2","impliedFormat":1},{"version":"fec412ded391a7239ef58f455278154b62939370309c1fed322293d98c8796a6","impliedFormat":1},{"version":"e3498cf5e428e6c6b9e97bd88736f26d6cf147dedbfa5a8ad3ed8e05e059af8a","impliedFormat":1},{"version":"dba3f34531fd9b1b6e072928b6f885aa4d28dd6789cbd0e93563d43f4b62da53","impliedFormat":1},{"version":"f672c876c1a04a223cf2023b3d91e8a52bb1544c576b81bf64a8fec82be9969c","impliedFormat":1},{"version":"e4b03ddcf8563b1c0aee782a185286ed85a255ce8a30df8453aade2188bbc904","impliedFormat":1},{"version":"2329d90062487e1eaca87b5e06abcbbeeecf80a82f65f949fd332cfcf824b87b","impliedFormat":1},{"version":"25b3f581e12ede11e5739f57a86e8668fbc0124f6649506def306cad2c59d262","impliedFormat":1},{"version":"93c3e73824ad57f98fd23b39335dbdae2db0bd98199b0dc0b9ccc60bf3c5134a","impliedFormat":1},{"version":"a9ebb67d6bbead6044b43714b50dcb77b8f7541ffe803046fdec1714c1eba206","impliedFormat":1},{"version":"833e92c058d033cde3f29a6c7603f517001d1ddd8020bc94d2067a3bc69b2a8e","impliedFormat":1},{"version":"d1fc2d317157b161af800631bb01668bf08307bf2a0ff053f743aeca3a368465","impliedFormat":1},{"version":"835fb2909ce458740fb4a49fc61709896c6864f5ce3db7f0a88f06c720d74d02","impliedFormat":1},{"version":"6e5857f38aa297a859cab4ec891408659218a5a2610cd317b6dcbef9979459cc","impliedFormat":1},{"version":"424d9092b2417b1da7f9ba67c15f42def241e29d304e63003a3e171efe04a40a","impliedFormat":1},{"version":"81c5f2e7e6fb72d442e3c08b49c2c71cefd64c590e0a57c2093b3c7d00dbb13b","impliedFormat":1},{"version":"d26b722c6ef39ad70bcfbe4fe1554d2b7f2573677eb87aa03af85f1959eefc8a","impliedFormat":1},{"version":"adca9ceb697fa6f88e4a40bb88626593fccac0b1b5b0c52557887c27394b4cd5","impliedFormat":1},{"version":"21ae7cca76ec2410452737a20d2b55e46dccc5d64a9cef7f042e3a89e4b99db9","impliedFormat":1},{"version":"c140211fba71b3a847b15da4a70776ed904198e7e4dc8c32df22742235072c4c","impliedFormat":1},{"version":"ce51a3572f75b0f8fc07a11828876ac28cf606a780a2d4c6b3bbcee79e48a90b","impliedFormat":1},{"version":"a8fed1298e1a1aeb2b689c56fe4200c6bf48491cabe47f54a1caa22fa9ebcc6e","impliedFormat":1},{"version":"c7d1e897d7751e2f2f7e41ee6db87a567418a0e9d307c580029e8565a76cb1f8","impliedFormat":1},{"version":"880ec5909ddc50fbf8066064fce83094751261a6f8a4962972210c8682899bca","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"71a40e0d3aadfd1998021f706c164195f5d8d043f1551d4ecee8804d77167a5a","impliedFormat":1},{"version":"55fadfc733db7198f557af11bf55ae955db6257e17262351a9cdb8a98af4a9f9","impliedFormat":1},{"version":"293e5cbd76e48e679f3e1c47d9a7a66872136aac96637ea271f4a2c9adb1e6c1","impliedFormat":1},{"version":"b2b2198d1b8c518151f0eafdbbc56c8deee1e428b500f1822352fbcc5993424d","impliedFormat":1},{"version":"2f67911e4bf4e0717dc2ded248ce2d5e4398d945ee13889a6852c1233ea41508","impliedFormat":1},{"version":"14e1da8984e72f3837de4aec8bc6d29e9ecce4dc260ac6ef4ca4e5e4f93add01","impliedFormat":1},{"version":"99003dbd8309d6cfda98e4bacb264ad40bfc197ede6af95370b0f3177076eb6a","impliedFormat":1},{"version":"9eea3e08092b6790361c3f66a11eba28e2f0a4414b89622e1fb9ed70c8f38ef9","impliedFormat":99},{"version":"958002b3dddffde5cd74a187f93508121f7065e361312e16a8aecd7676bf1a83","impliedFormat":99},{"version":"f336533543f32410a2921bad881736d4768e1b858a11f2c88e74136ea23c29f4","impliedFormat":99},{"version":"cbe003be7407ddac990ed386a0e7572e63288ac437eb6dcd4199decebb4bf54a","impliedFormat":99},{"version":"be7e9393805753e479746c81dcfe4287063d83ffa30e0284f7a016775af2c1d2","impliedFormat":1},{"version":"f49bf9fcde399132c5760eefb0714cee3f8e96cd30fcec8c535e2cee57870d63","impliedFormat":1},{"version":"46e55f334e394f80d5be1e4ce45749f77ab99c99917e6e2caf800840da409673","impliedFormat":1},{"version":"5490cbcd192227f9a1f61bdf7000da8b47635fc0cdec9e1879b932586b6d2e8e","impliedFormat":1},{"version":"487178767be2789fa2f765d0e77fc5abad8212f7d5dfbe255011dd8e8bb5306c","impliedFormat":1},{"version":"2316301dd223d31962d917999acf8e543e0119c5d24ec984c9f22cb23247160c","impliedFormat":1},{"version":"a3eb5de45edf334745bb6347979b6f9eb44bab235910afb551d44eb05a84a233","impliedFormat":1},{"version":"bfa8e3563e561377191b49bcfb4cea149286a2b843058c1236dce8c3b541564d","impliedFormat":1},{"version":"b8495ee8dbff077d31e9365c73f170709106d7bc6e17a163c7568bb485c8368c","impliedFormat":1},{"version":"e818471014c77c103330aee11f00a7a00b37b35500b53ea6f337aefacd6174c9","impliedFormat":1},{"version":"85b982d3d1838e3b768353b9afe0cb051bc763077eaf6f2b72141155e1c5a560","impliedFormat":1},{"version":"628f7309657ea46102ae95ad7ebf168fc9c54495da2d6a3c2834e186543c5cc9","impliedFormat":1},{"version":"51a4d8450070d113bb1758ed730a5862e6e676a279a190f5e622d24a8942357a","impliedFormat":1},{"version":"6bdf5675c990953db3b07288a443e083377e331bef60613d08e9def95ab18c79","affectsGlobalScope":true,"impliedFormat":99},{"version":"0e7ab9c6ce896e4a6de1f387664145bc50d5b899dac667ccdb2bd2755f3e1eec","impliedFormat":99},{"version":"31506fd6e72c0d09e082b2f0b3741e6bbe90670361c9560ef998bdb1e56edc3e","impliedFormat":1},{"version":"9190d984b46642b16203d36360260e31f9ba9e423929c8d25730d55710528acc","impliedFormat":99},{"version":"71ca4d5b499bd98537e9d505fa8e3845eb74c1842289b63d12c03a84ebfd237b","impliedFormat":99},{"version":"7314e2d918cc6adf47c94b0f7b0dc230d2b45d982eed1c32ffef54eaaa37950b","impliedFormat":99},{"version":"35d35b2d52697c363e7818ac96e4d9ed174d1c84a8e7954d218fa9594a2b9529","impliedFormat":99},{"version":"c19139650672cefeeef779cefc1cb75ec753796bec415e29b877b504b6f4832a","impliedFormat":99},{"version":"05d896aba8cb582031542db5d6932bbbb6f52ce68f7cd5e62a9493b9f483267b","impliedFormat":99},{"version":"dc50e95f72a1bad73dde789629527d7e4913871b4336b432d297b6d80b58ddaa","impliedFormat":99},{"version":"c9ac63c635f2a388d8cade82b7ad8adf60a2c736c7580a78dad14696cfea49bf","impliedFormat":99},{"version":"9a7e2b6400b2632d9c36d0949940e34dd2c7599b4c061f432d217215d2ed3d87","impliedFormat":99},{"version":"c2d81004ec850eae05ebc8028267b8fe5a46be9aa0142923d6b03d8b397d50d2","impliedFormat":99},{"version":"77e45ec7bae423ca7f5b1f7121be710ffe24905e165c417c5fc72dbf4168e4cb","impliedFormat":99},{"version":"e06d3f14c40ee0bdaabe660e7ee481ea601a08b4d961d7ede248963186a9fd6b","impliedFormat":99},{"version":"33b3171df0516ba4314598c00c38e7959b0f5484bc983226520760e5d3f36729","impliedFormat":99},{"version":"e8fde93049aa349fce243641c9dc94cd74bab77a9f65c8a261eb6253d3adfca7","impliedFormat":99},{"version":"d0d881e35953f0edcb5eaee09b7214ce2ce334fc8de6a7e4df03341272fb0792","impliedFormat":99},{"version":"a75119d0bb1cba189337f878d7055f7be6e38128c8d35e98023853270d42aa8f","impliedFormat":99},{"version":"3f1f910a90d404d6c9814201db146d0c45215c6f50fb02a965aa5458059671ea","impliedFormat":99},{"version":"c3f9a8563a999c2d6b96c2a1b7478e4fa557d923f783c5c120d81309699b6d86","impliedFormat":99},{"version":"6e1c84a02a1891d96bcfaf0624c3807ae3819383d92eb558ef0ffe52703b1107","impliedFormat":99},{"version":"fa152eeaae934e5b7c422d0c4437ec43c427fb34654602991e6cabf834116ea3","impliedFormat":99},{"version":"d715117d2d3883a181f0d07d0ccb629e5177979d6855f094899d2b6c31170de4","impliedFormat":99},{"version":"d9fa9256df8a6c3393bbafca9f3d9e814b89914b15aaf135e1825f980ed7ae7c","impliedFormat":99},{"version":"62d6c1632fe0b57438a4873d9363c00a445b0448fc83ce766735a062ef762ccc","impliedFormat":99},{"version":"fb893a0dfc3c9fb0f9ca93d0648694dd95f33cbad2c0f2c629f842981dfd4e2e","impliedFormat":1},{"version":"509331448d7ccef32318a8d548918878291ed0f2255fca0930e805bf8bf19be6","impliedFormat":99},{"version":"9418aae55aef3291b49b4f10fcec70e6c408ee7e0ffa6dfbc369fabc513ecf8c","impliedFormat":99},{"version":"9eea3e08092b6790361c3f66a11eba28e2f0a4414b89622e1fb9ed70c8f38ef9","impliedFormat":99},{"version":"93832729ccefebc66c64e5c292291ebe09337940aa45b4713bdb11c50cc397a0","impliedFormat":99},{"version":"525ffaabfda919e6f59b87c67760a1530af36ad1c3ad395182470c2fd5afe56f","impliedFormat":99},{"version":"bfec26ac8033dfa8d9844a5c74e09e24c1b33983833493d77236392240ebb2a2","impliedFormat":99},{"version":"756aeb01bcf0223dee4082074a734e5940fdf825e352136b42776ab465fcd4e9","impliedFormat":99},{"version":"76af14c3cce62da183aaf30375e3a4613109d16c7f16d30702f16d625a95e62c","impliedFormat":99},{"version":"bf17c65172c9ed6d07ed66fc2e6d7b20d3d6c70558eead1d162ffc777ee7cb1c","impliedFormat":99},{"version":"1fc872fbaa4cf102142880e930fef3bd15fc4eda64b7051a3970220c1aa0cbf9","impliedFormat":99},{"version":"cee900b02b756607b8c78bc63d60301e20d5b6786f4a59d22f815f8abbf837ed","impliedFormat":99},{"version":"79cb9dbceece4ab437a14e567be9559d85be50e9061c2851fe1dbc1be6834d40","impliedFormat":99},{"version":"71d24f97510ba111ba24afa56e0c8c89df69b41abd7b2f91434ca50207ff1828","impliedFormat":99},{"version":"0a1151651cfdf3f9f007a2f2345193057381d177ecc5ca9fc045e9a4abdf00ad","impliedFormat":99},{"version":"7b7bf440d84a536b64620c7cd6c67f9bac9dfcad00dc611d645aff2eab51cafd","impliedFormat":99},{"version":"3b296653e90ce6f076bccf2ef341ffe6161312dc2628988079497ca9537f19e7","impliedFormat":99},{"version":"b1d83f765b81540213680f712b71c229e641a4c5860091e03cbae6d8d7dad8af","impliedFormat":99},{"version":"4bc58bac748b1e7e17ce375c80493285d64489e0ac56dd0c208bf0b255a4d115","impliedFormat":99},{"version":"fc9c40ea7e4c4c012c287dadb33a412157d41923101bf4981760d9951ba87c9e","impliedFormat":99},{"version":"0dfa84487aff893e6fabbfef9c35111f5e7217c6d25f888d441fa051d6409c10","impliedFormat":99},{"version":"bdb403571bee96186ee438065b5c2d32f3885ba91b62a40de95bbe1d9515c70d","impliedFormat":99},{"version":"3174cf31006e4b14d6eda31dff8d46c27de0dca785c0c84c28b3ce4e5a81d831","impliedFormat":99},{"version":"6c6fb94432c0a0c6bef4a499b82413a4a5d771a3f74f23494ec5aa732fd0eed6","impliedFormat":99},{"version":"3cb17876a9da1e2f64d18c4695e770d3e64e87f21def7275f73a31823ff53a6d","impliedFormat":99},{"version":"d2990059ad207a630e7f8405cdc1d073e57740f56cdd41fbd637846eb9eb6d89","impliedFormat":99},{"version":"51f6001a1dce773d4ca082f2ac3a82b16bf0a0805310329509953ef83e061773","impliedFormat":99},{"version":"c29c5009d43ca5d4d65613c839160be4ee7af336bc0545ca073438fa1f1d5db4","impliedFormat":99},{"version":"5df906533530ba7cac7ccb5fca48169156562aab531db604c5ee8953401e01b2","impliedFormat":99},{"version":"858c9adfbc9b4b2c4ff24f1834a00412eeb34a9fbed81c2e60e0ceabf4e6c149","impliedFormat":99},{"version":"86d4ff8ba66b5ea1df375fe6092d2b167682ccd5dd0d9b003a7d30d95a0cda32","impliedFormat":99},{"version":"4967529644e391115ca5592184d4b63980569adf60ee685f968fd59ab1557188","impliedFormat":1},{"version":"5929864ce17fba74232584d90cb721a89b7ad277220627cc97054ba15a98ea8f","impliedFormat":1},{"version":"763fe0f42b3d79b440a9b6e51e9ba3f3f91352469c1e4b3b67bfa4ff6352f3f4","impliedFormat":1},{"version":"6c7176368037af28cb72f2392010fa1cef295d6d6744bca8cfb54985f3a18c3e","affectsGlobalScope":true,"impliedFormat":1},{"version":"ab41ef1f2cdafb8df48be20cd969d875602483859dc194e9c97c8a576892c052","affectsGlobalScope":true,"impliedFormat":1},{"version":"437e20f2ba32abaeb7985e0afe0002de1917bc74e949ba585e49feba65da6ca1","affectsGlobalScope":true,"impliedFormat":1},{"version":"21d819c173c0cf7cc3ce57c3276e77fd9a8a01d35a06ad87158781515c9a438a","impliedFormat":1},{"version":"98cffbf06d6bab333473c70a893770dbe990783904002c4f1a960447b4b53dca","affectsGlobalScope":true,"impliedFormat":1},{"version":"3af97acf03cc97de58a3a4bc91f8f616408099bc4233f6d0852e72a8ffb91ac9","affectsGlobalScope":true,"impliedFormat":1},{"version":"808069bba06b6768b62fd22429b53362e7af342da4a236ed2d2e1c89fcca3b4a","affectsGlobalScope":true,"impliedFormat":1},{"version":"1db0b7dca579049ca4193d034d835f6bfe73096c73663e5ef9a0b5779939f3d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"9798340ffb0d067d69b1ae5b32faa17ab31b82466a3fc00d8f2f2df0c8554aaa","affectsGlobalScope":true,"impliedFormat":1},{"version":"f26b11d8d8e4b8028f1c7d618b22274c892e4b0ef5b3678a8ccbad85419aef43","affectsGlobalScope":true,"impliedFormat":1},{"version":"bf67d53d168abc1298888693338cb82854bdb2e69ef83f8a0092093c2d562107","impliedFormat":1},{"version":"2cbe0621042e2a68c7cbce5dfed3906a1862a16a7d496010636cdbdb91341c0f","affectsGlobalScope":true,"impliedFormat":1},{"version":"f9501cc13ce624c72b61f12b3963e84fad210fbdf0ffbc4590e08460a3f04eba","affectsGlobalScope":true,"impliedFormat":1},{"version":"e7721c4f69f93c91360c26a0a84ee885997d748237ef78ef665b153e622b36c1","affectsGlobalScope":true,"impliedFormat":1},{"version":"0fa06ada475b910e2106c98c68b10483dc8811d0c14a8a8dd36efb2672485b29","impliedFormat":1},{"version":"33e5e9aba62c3193d10d1d33ae1fa75c46a1171cf76fef750777377d53b0303f","impliedFormat":1},{"version":"2b06b93fd01bcd49d1a6bd1f9b65ddcae6480b9a86e9061634d6f8e354c1468f","impliedFormat":1},{"version":"6a0cd27e5dc2cfbe039e731cf879d12b0e2dded06d1b1dedad07f7712de0d7f4","affectsGlobalScope":true,"impliedFormat":1},{"version":"13f5c844119c43e51ce777c509267f14d6aaf31eafb2c2b002ca35584cd13b29","impliedFormat":1},{"version":"e60477649d6ad21542bd2dc7e3d9ff6853d0797ba9f689ba2f6653818999c264","impliedFormat":1},{"version":"c2510f124c0293ab80b1777c44d80f812b75612f297b9857406468c0f4dafe29","affectsGlobalScope":true,"impliedFormat":1},{"version":"5524481e56c48ff486f42926778c0a3cce1cc85dc46683b92b1271865bcf015a","impliedFormat":1},{"version":"4c829ab315f57c5442c6667b53769975acbf92003a66aef19bce151987675bd1","affectsGlobalScope":true,"impliedFormat":1},{"version":"b2ade7657e2db96d18315694789eff2ddd3d8aea7215b181f8a0b303277cc579","impliedFormat":1},{"version":"9855e02d837744303391e5623a531734443a5f8e6e8755e018c41d63ad797db2","impliedFormat":1},{"version":"4d631b81fa2f07a0e63a9a143d6a82c25c5f051298651a9b69176ba28930756d","impliedFormat":1},{"version":"836a356aae992ff3c28a0212e3eabcb76dd4b0cc06bcb9607aeef560661b860d","impliedFormat":1},{"version":"1e0d1f8b0adfa0b0330e028c7941b5a98c08b600efe7f14d2d2a00854fb2f393","impliedFormat":1},{"version":"41670ee38943d9cbb4924e436f56fc19ee94232bc96108562de1a734af20dc2c","affectsGlobalScope":true,"impliedFormat":1},{"version":"c906fb15bd2aabc9ed1e3f44eb6a8661199d6c320b3aa196b826121552cb3695","impliedFormat":1},{"version":"22295e8103f1d6d8ea4b5d6211e43421fe4564e34d0dd8e09e520e452d89e659","impliedFormat":1},{"version":"bb45cd435da536500f1d9692a9b49d0c570b763ccbf00473248b777f5c1f353b","impliedFormat":1},{"version":"6b4e081d55ac24fc8a4631d5dd77fe249fa25900abd7d046abb87d90e3b45645","impliedFormat":1},{"version":"a10f0e1854f3316d7ee437b79649e5a6ae3ae14ffe6322b02d4987071a95362e","impliedFormat":1},{"version":"e208f73ef6a980104304b0d2ca5f6bf1b85de6009d2c7e404028b875020fa8f2","impliedFormat":1},{"version":"d163b6bc2372b4f07260747cbc6c0a6405ab3fbcea3852305e98ac43ca59f5bc","impliedFormat":1},{"version":"e6fa9ad47c5f71ff733744a029d1dc472c618de53804eae08ffc243b936f87ff","affectsGlobalScope":true,"impliedFormat":1},{"version":"83e63d6ccf8ec004a3bb6d58b9bb0104f60e002754b1e968024b320730cc5311","impliedFormat":1},{"version":"24826ed94a78d5c64bd857570fdbd96229ad41b5cb654c08d75a9845e3ab7dde","impliedFormat":1},{"version":"45875bcae57270aeb3ebc73a5e3fb4c7b9d91d6b045f107c1d8513c28ece71c0","impliedFormat":1},{"version":"928af3d90454bf656a52a48679f199f64c1435247d6189d1caf4c68f2eaf921f","affectsGlobalScope":true,"impliedFormat":1},{"version":"8579647e66a9f713bcba9746f25dcfc7bcfeb8832130a52cd1bca65c4f342bf8","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f16a7e4deafa527ed9995a772bb380eb7d3c2c0fd4ae178c5263ed18394db2c","impliedFormat":1},{"version":"933921f0bb0ec12ef45d1062a1fc0f27635318f4d294e4d99de9a5493e618ca2","impliedFormat":1},{"version":"71a0f3ad612c123b57239a7749770017ecfe6b66411488000aba83e4546fde25","impliedFormat":1},{"version":"77fbe5eecb6fac4b6242bbf6eebfc43e98ce5ccba8fa44e0ef6a95c945ff4d98","impliedFormat":1},{"version":"4f9d8ca0c417b67b69eeb54c7ca1bedd7b56034bb9bfd27c5d4f3bc4692daca7","impliedFormat":1},{"version":"814118df420c4e38fe5ae1b9a3bafb6e9c2aa40838e528cde908381867be6466","impliedFormat":1},{"version":"a3fc63c0d7b031693f665f5494412ba4b551fe644ededccc0ab5922401079c95","impliedFormat":1},{"version":"f27524f4bef4b6519c604bdb23bf4465bddcccbf3f003abb901acbd0d7404d99","impliedFormat":1},{"version":"37ba7b45141a45ce6e80e66f2a96c8a5ab1bcef0fc2d0f56bb58df96ec67e972","impliedFormat":1},{"version":"45650f47bfb376c8a8ed39d4bcda5902ab899a3150029684ee4c10676d9fbaee","impliedFormat":1},{"version":"dba28a419aec76ed864ef43e5f577a5c99a010c32e5949fe4e17a4d57c58dd11","affectsGlobalScope":true,"impliedFormat":1},{"version":"18fd40412d102c5564136f29735e5d1c3b455b8a37f920da79561f1fde068208","impliedFormat":1},{"version":"c959a391a75be9789b43c8468f71e3fa06488b4d691d5729dde1416dcd38225b","impliedFormat":1},{"version":"f0be1b8078cd549d91f37c30c222c2a187ac1cf981d994fb476a1adc61387b14","affectsGlobalScope":true,"impliedFormat":1},{"version":"0aaed1d72199b01234152f7a60046bc947f1f37d78d182e9ae09c4289e06a592","impliedFormat":1},{"version":"5ebe6f4cc3b803cbfc962bae0d954f9c80e5078ca41eb3f1de41d92e7193ef37","impliedFormat":1},{"version":"66ba1b2c3e3a3644a1011cd530fb444a96b1b2dfe2f5e837a002d41a1a799e60","impliedFormat":1},{"version":"7e514f5b852fdbc166b539fdd1f4e9114f29911592a5eb10a94bb3a13ccac3c4","impliedFormat":1},{"version":"5b7aa3c4c1a5d81b411e8cb302b45507fea9358d3569196b27eb1a27ae3a90ef","affectsGlobalScope":true,"impliedFormat":1},{"version":"5987a903da92c7462e0b35704ce7da94d7fdc4b89a984871c0e2b87a8aae9e69","affectsGlobalScope":true,"impliedFormat":1},{"version":"ea08a0345023ade2b47fbff5a76d0d0ed8bff10bc9d22b83f40858a8e941501c","impliedFormat":1},{"version":"47613031a5a31510831304405af561b0ffaedb734437c595256bb61a90f9311b","impliedFormat":1},{"version":"ae062ce7d9510060c5d7e7952ae379224fb3f8f2dd74e88959878af2057c143b","impliedFormat":1},{"version":"8a1a0d0a4a06a8d278947fcb66bf684f117bf147f89b06e50662d79a53be3e9f","affectsGlobalScope":true,"impliedFormat":1},{"version":"9f663c2f91127ef7024e8ca4b3b4383ff2770e5f826696005de382282794b127","impliedFormat":1},{"version":"9f55299850d4f0921e79b6bf344b47c420ce0f507b9dcf593e532b09ea7eeea1","impliedFormat":1},{"version":"25c8056edf4314820382a5fdb4bb7816999acdcb929c8f75e3f39473b87e85bc","impliedFormat":1},{"version":"c464d66b20788266e5353b48dc4aa6bc0dc4a707276df1e7152ab0c9ae21fad8","impliedFormat":1},{"version":"78d0d27c130d35c60b5e5566c9f1e5be77caf39804636bc1a40133919a949f21","impliedFormat":1},{"version":"c6fd2c5a395f2432786c9cb8deb870b9b0e8ff7e22c029954fabdd692bff6195","impliedFormat":1},{"version":"1d6e127068ea8e104a912e42fc0a110e2aa5a66a356a917a163e8cf9a65e4a75","impliedFormat":1},{"version":"5ded6427296cdf3b9542de4471d2aa8d3983671d4cac0f4bf9c637208d1ced43","impliedFormat":1},{"version":"7f182617db458e98fc18dfb272d40aa2fff3a353c44a89b2c0ccb3937709bfb5","impliedFormat":1},{"version":"cadc8aced301244057c4e7e73fbcae534b0f5b12a37b150d80e5a45aa4bebcbd","impliedFormat":1},{"version":"385aab901643aa54e1c36f5ef3107913b10d1b5bb8cbcd933d4263b80a0d7f20","impliedFormat":1},{"version":"9670d44354bab9d9982eca21945686b5c24a3f893db73c0dae0fd74217a4c219","impliedFormat":1},{"version":"0b8a9268adaf4da35e7fa830c8981cfa22adbbe5b3f6f5ab91f6658899e657a7","impliedFormat":1},{"version":"11396ed8a44c02ab9798b7dca436009f866e8dae3c9c25e8c1fbc396880bf1bb","impliedFormat":1},{"version":"ba7bc87d01492633cb5a0e5da8a4a42a1c86270e7b3d2dea5d156828a84e4882","impliedFormat":1},{"version":"4893a895ea92c85345017a04ed427cbd6a1710453338df26881a6019432febdd","impliedFormat":1},{"version":"c21dc52e277bcfc75fac0436ccb75c204f9e1b3fa5e12729670910639f27343e","impliedFormat":1},{"version":"13f6f39e12b1518c6650bbb220c8985999020fe0f21d818e28f512b7771d00f9","impliedFormat":1},{"version":"9b5369969f6e7175740bf51223112ff209f94ba43ecd3bb09eefff9fd675624a","impliedFormat":1},{"version":"4fe9e626e7164748e8769bbf74b538e09607f07ed17c2f20af8d680ee49fc1da","impliedFormat":1},{"version":"24515859bc0b836719105bb6cc3d68255042a9f02a6022b3187948b204946bd2","impliedFormat":1},{"version":"ea0148f897b45a76544ae179784c95af1bd6721b8610af9ffa467a518a086a43","impliedFormat":1},{"version":"24c6a117721e606c9984335f71711877293a9651e44f59f3d21c1ea0856f9cc9","impliedFormat":1},{"version":"dd3273ead9fbde62a72949c97dbec2247ea08e0c6952e701a483d74ef92d6a17","impliedFormat":1},{"version":"405822be75ad3e4d162e07439bac80c6bcc6dbae1929e179cf467ec0b9ee4e2e","impliedFormat":1},{"version":"0db18c6e78ea846316c012478888f33c11ffadab9efd1cc8bcc12daded7a60b6","impliedFormat":1},{"version":"e61be3f894b41b7baa1fbd6a66893f2579bfad01d208b4ff61daef21493ef0a8","impliedFormat":1},{"version":"bd0532fd6556073727d28da0edfd1736417a3f9f394877b6d5ef6ad88fba1d1a","impliedFormat":1},{"version":"89167d696a849fce5ca508032aabfe901c0868f833a8625d5a9c6e861ef935d2","impliedFormat":1},{"version":"615ba88d0128ed16bf83ef8ccbb6aff05c3ee2db1cc0f89ab50a4939bfc1943f","impliedFormat":1},{"version":"a4d551dbf8746780194d550c88f26cf937caf8d56f102969a110cfaed4b06656","impliedFormat":1},{"version":"8bd86b8e8f6a6aa6c49b71e14c4ffe1211a0e97c80f08d2c8cc98838006e4b88","impliedFormat":1},{"version":"317e63deeb21ac07f3992f5b50cdca8338f10acd4fbb7257ebf56735bf52ab00","impliedFormat":1},{"version":"4732aec92b20fb28c5fe9ad99521fb59974289ed1e45aecb282616202184064f","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"bf67d53d168abc1298888693338cb82854bdb2e69ef83f8a0092093c2d562107","impliedFormat":1},{"version":"cdcf9ea426ad970f96ac930cd176d5c69c6c24eebd9fc580e1572d6c6a88f62c","impliedFormat":1},{"version":"23cd712e2ce083d68afe69224587438e5914b457b8acf87073c22494d706a3d0","impliedFormat":1},{"version":"156a859e21ef3244d13afeeba4e49760a6afa035c149dda52f0c45ea8903b338","impliedFormat":1},{"version":"3ac40516c33b87f751f7507346933081a26cdb8a3e11a6b3aa07d23f803c85db","impliedFormat":1},{"version":"4ac80270b6787c2b77a2d98a9714a71f4363c24b5890314f3ba582c94bfbe779","impliedFormat":1},{"version":"14e9acf826baba0ef4b5665704084896e7bcc06f65a9ab13af7e93d27d6b7069","impliedFormat":1},{"version":"68834d631c8838c715f225509cfc3927913b9cc7a4870460b5b60c8dbdb99baf","impliedFormat":1},{"version":"82e8858179f8b8895db6f005f2152d4eda2273721f52be47e22d48bd84590f95","impliedFormat":1},{"version":"eec76bf6b9346f3f95fa402621b889489e96930e72295b0369022f332e9b4a6a","impliedFormat":1},{"version":"171b96f31e3fbdb55fe570f2a29a5ee47223fdca95a84ea2142e4cc4feaf9dfe","impliedFormat":1},{"version":"ea6bc8de8b59f90a7a3960005fd01988f98fd0784e14bc6922dde2e93305ec7d","impliedFormat":1},{"version":"36107995674b29284a115e21a0618c4c2751b32a8766dd4cb3ba740308b16d59","impliedFormat":1},{"version":"914a0ae30d96d71915fc519ccb4efbf2b62c0ddfb3a3fc6129151076bc01dc60","impliedFormat":1},{"version":"9c32412007b5662fd34a8eb04292fb5314ec370d7016d1c2fb8aa193c807fe22","impliedFormat":1},{"version":"d243db6b25788f439e7e2f03c05688e92f46764351673bb0e7b2f3631232e186","impliedFormat":1},{"version":"4d327f7d72ad0918275cea3eee49a6a8dc8114ae1d5b7f3f5d0774de75f7439a","impliedFormat":1},{"version":"6ebe8ebb8659aaa9d1acbf3710d7dae3e923e97610238b9511c25dc39023a166","impliedFormat":1},{"version":"e85d7f8068f6a26710bff0cc8c0fc5e47f71089c3780fbede05857331d2ddec9","impliedFormat":1},{"version":"7befaf0e76b5671be1d47b77fcc65f2b0aad91cc26529df1904f4a7c46d216e9","impliedFormat":1},{"version":"0a60a292b89ca7218b8616f78e5bbd1c96b87e048849469cccb4355e98af959a","impliedFormat":1},{"version":"0b6e25234b4eec6ed96ab138d96eb70b135690d7dd01f3dd8a8ab291c35a683a","impliedFormat":1},{"version":"9666f2f84b985b62400d2e5ab0adae9ff44de9b2a34803c2c5bd3c8325b17dc0","impliedFormat":1},{"version":"40cd35c95e9cf22cfa5bd84e96408b6fcbca55295f4ff822390abb11afbc3dca","impliedFormat":1},{"version":"b1616b8959bf557feb16369c6124a97a0e74ed6f49d1df73bb4b9ddf68acf3f3","impliedFormat":1},{"version":"5b03a034c72146b61573aab280f295b015b9168470f2df05f6080a2122f9b4df","impliedFormat":1},{"version":"40b463c6766ca1b689bfcc46d26b5e295954f32ad43e37ee6953c0a677e4ae2b","impliedFormat":1},{"version":"249b9cab7f5d628b71308c7d9bb0a808b50b091e640ba3ed6e2d0516f4a8d91d","impliedFormat":1},{"version":"d33ce35e3f9cfcc1d94eca415bdd3bde94d5b153ffdd33e6c4455c029986c630","impliedFormat":1},{"version":"80aae6afc67faa5ac0b32b5b8bc8cc9f7fa299cff15cf09cc2e11fd28c6ae29e","impliedFormat":1},{"version":"f473cd2288991ff3221165dcf73cd5d24da30391f87e85b3dd4d0450c787a391","impliedFormat":1},{"version":"499e5b055a5aba1e1998f7311a6c441a369831c70905cc565ceac93c28083d53","impliedFormat":1},{"version":"8aee8b6d4f9f62cf3776cda1305fb18763e2aade7e13cea5bbe699112df85214","impliedFormat":1},{"version":"98498b101803bb3dde9f76a56e65c14b75db1cc8bec5f4db72be541570f74fc5","impliedFormat":1},{"version":"1d5e86e300d9fdeb1e3e88aa4f24d780f045195fcb2dbc24b68bf49c9f816c5e","impliedFormat":1},{"version":"5d0375ca7310efb77e3ef18d068d53784faf62705e0ad04569597ae0e755c401","impliedFormat":1},{"version":"59af37caec41ecf7b2e76059c9672a49e682c1a2aa6f9d7dc78878f53aa284d6","impliedFormat":1},{"version":"addf417b9eb3f938fddf8d81e96393a165e4be0d4a8b6402292f9c634b1cb00d","impliedFormat":1},{"version":"436d7b4543b340b0f3eef4310d524242e41369b9652aa9c70428767c4dcac455","impliedFormat":1},{"version":"adf27937dba6af9f08a68c5b1d3fce0ca7d4b960c57e6d6c844e7d1a8e53adae","impliedFormat":1},{"version":"12950411eeab8563b349cb7959543d92d8d02c289ed893d78499a19becb5a8cc","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"114f493b30f364255290472111b5a4791d5902c308645670cd0401429cbc6930","impliedFormat":1},{"version":"b3fb72492a07a76f7bfa29ecadd029eea081df11512e4dfe6f930a5a9cb1fb75","impliedFormat":1},{"version":"dbab1950ef4bf06f44795b144026a352a7b4a3a68a969bbf32eb55addd0fb95a","impliedFormat":99},{"version":"2b5368217b57528a60433558585186a925d9842fe64c1262adde8eac5cb8de33","impliedFormat":99},{"version":"e22273698b7aad4352f0eb3c981d510b5cf6b17fde2eeaa5c018bb065d15558f","impliedFormat":99},{"version":"ed9680d6573920c3f1588fdb732d2469324e16b4795e2bec5f196a613e66030f","impliedFormat":99},{"version":"804e73c5236db118192cf774837ecf6d37013470832dc0ed9aaecfb4c93fb88b","impliedFormat":99},{"version":"91c093343733c2c2d40bee28dc793eff3071af0cb53897651f8459ad25ad01da","impliedFormat":99},{"version":"dbf1009687760b708258fef934385cf29eada0feb170521f7b03cb874786bcf5","impliedFormat":99},{"version":"e1c58879ba7cfcb2a70f4ec69831f48eef47b7a356f15ab9f4fce03942d9f21a","impliedFormat":99},{"version":"f4fc36916b3eac2ea0180532b46283808604e4b6ff11e5031494d05aa6661cc6","impliedFormat":99},{"version":"82e23a5d9f36ccdac5322227cd970a545b8c23179f2035388a1524f82f96d8d0","impliedFormat":99},{"version":"c52e8203e4cc8ddd3ffa75197673942e80e3ff4b3bffa962588363e872cb9922","impliedFormat":99},{"version":"bfce32506c0d081212ff9d27ec466fa6135a695ba61d5a02738abd2442566231","impliedFormat":99},{"version":"5ad576e13f58a0a2b5d4818dd13c16ec75b43025a14a89a7f09db3fe56c03d30","impliedFormat":99},{"version":"5668033966c8247576fc316629df131d6175d24ccf22940324c19c159671e1c1","impliedFormat":99},{"version":"493c39c5f9e9c050c10930448fda1be8de10a0d9b34dcd24ff17a1713c282162","impliedFormat":99},{"version":"502f64730d7ca8f4b7eb4f16c5b5cdad4e05c2337b85a5b4dbe7c8ad118dcc11","impliedFormat":99},{"version":"fb5a2c398c5d06e25ae7b12ad15a921f1b980a63fa2a7e4fab133b4e2a812016","impliedFormat":99},{"version":"ba3df48971907e524e144d82ed8f02d79729234b659307f8ea6c53b40821c021","impliedFormat":99},{"version":"dbf3d90c21c08217509df631336881a3105740033b0592dcc47036490f95e51c","impliedFormat":99},{"version":"e6ad9376e7d088ce1dc6d3183ba5f0b3fb67ee586aa824cc8519b52f2341307a","impliedFormat":99},{"version":"50cf14b8f0fc2722c11794ca2a06565b1f29e266491da75c745894960ebbce06","impliedFormat":99},{"version":"d62b09cb6f1ceb87ec6c26f3789bc38f8be9fb0ce3126fd0bf89b003d0cba371","impliedFormat":99},{"version":"e9d27f2b7d5171f512053f153cadc303d1b84d00c98e917664ba68eca9b7af6a","impliedFormat":99},{"version":"4899d2cf406cd68748c5d536b736c90339a39f996945126d8a11355eba5f56f3","impliedFormat":99},{"version":"491d5f012b1de793c45e75a930f5cdef1ff0e7875968e743fa6bd5dd7d31cb3b","impliedFormat":99},{"version":"53c86b81daa463deacb0046fee490b6d589438ac71311050b74dcee99afca0f6","impliedFormat":99},{"version":"70587241a4cc2e08ffc30e60c20f3eb38bd5af7e3d99640568ffe2993f933485","impliedFormat":99},{"version":"dd01943d0fe191b3b2020438367709333ff08a69d285e2f715a60711dcf83b61","impliedFormat":99},{"version":"79f3c89a11ffa63d3010bb80908169cf25716115b67695c18f3e5889c1ab02cb","impliedFormat":99},{"version":"b6ff37737d006b86082f2f7176eb0a771001e9dde9152a26ef9ea8fd80e6eba0","impliedFormat":99},{"version":"29c4e9ce50026f15c4e58637d8668ced90f82ce7605ca2fd7b521667caa4a12c","impliedFormat":99},{"version":"e6dd8526d318cce4cb3e83bef3cb4bf3aa08186ddc984c4663cf7dee221d430e","impliedFormat":99},{"version":"3b56bc74e48ec8704af54db1f6ecfee746297ee344b12e990ba5f406431014c1","impliedFormat":99},{"version":"9e4991da8b398fa3ee9b889b272b4fe3c21e898d873916b89c641c0717caed10","impliedFormat":99},{"version":"cf188ffe69f260ffd23b86c4fa2f3ecc01dda5bde2740629be244204581f7a49","impliedFormat":99},{"version":"7630b6a1c0ebaec2ef8e8abff850e1d6c551c47d1c345340a8ab95667460fc95","impliedFormat":99},{"version":"597b0a9ef02a28f5b1195305ec9f20a4f9948bd90ec3291d0343d1e5c0b4bd16","impliedFormat":99},{"version":"0586d346f71f0ec722d384b2569b7284dff554b55f98118f473079405dc8876b","impliedFormat":99},{"version":"09fe9b15282a073c2cd0ef426704e0baea167c2270fc5c46bc932deee440a071","impliedFormat":99},{"version":"ee02719d72e35d2816bd9052ad2a35f148ac54aa4ffb5d2ad2ef0229a17fc3ae","impliedFormat":99},{"version":"eac029dfd99082efdc6854f4f23932fe54be7eb9bb5debd03c2f6ebd1be502f7","impliedFormat":99},{"version":"39dfbe87a0de5cc88287ba79693e0ae8a560e8a1ed2a62c09d6dfc5250b801e1","impliedFormat":99},{"version":"924abf8e5bf12cc08323ce731f7c8215953755d53fdd509886ef321137b1fdf3","impliedFormat":99},{"version":"af12948563d3973b5f4c9a4ceda63c362758edb8c64412410ebd9c145b85611b","impliedFormat":99},{"version":"4a5d9348012a3e46c03888e71b0d318cda7e7db25869731375f90edad8dcea02","impliedFormat":99},{"version":"741208d80a2a3673e3a5216965ddf220003aba217ffe55448e528b766febaef7","impliedFormat":99},{"version":"1c42336e3f0cb6811c70c4b77ea96487a107d159bd23fba5ca69a947a0da37bd","impliedFormat":99},{"version":"69dbd631e44f63d27e20be0a628e9f1d9578e835c7a8ed77653894d7f15441df","impliedFormat":99},{"version":"fc391876e409d362cc43a7468226a9eb83440de09873b284bf09fbfb261ec259","impliedFormat":99},{"version":"d06f5012d5ac1bc25c5033f7e916fe42cc0253d6b523b9747809b71676069370","impliedFormat":99},{"version":"5d35840bd540fad886e21ddaf9b078a44c21a827dec9abc08d2d2c1a3ff27d44","impliedFormat":99},{"version":"bccef2e4035020788934f608255058fc234b3ccc67bf9b888b7eb1ef3285e521","impliedFormat":99},{"version":"4ecb0eb653de7093f2eb589cea5b35fdea6e2bbd62bc3d9fafdc5702850f7714","impliedFormat":99},{"version":"69ed52603ad6430aaffbc9dec25e0d01df733aaa32ab4d57d37987aedc94c349","impliedFormat":99},{"version":"323420ca2dd68ae9922913d7c5ca44f36b1db0e5d58e4a9316d4121d5da88664","impliedFormat":99},{"version":"679c5d237c8e812fd15c9ff547cf5bb17a5a3214af7632da4768efc163bf036a","impliedFormat":99},{"version":"76ab2326e7eeb1a0609423671abd7260a1302ae535bbc45c8bd251da0bdc40bc","impliedFormat":99},{"version":"584cbaebe5928714465942169a1820461276944ac1e97c2062855b14b498b546","impliedFormat":99},{"version":"1f51b0d1ce3e03cb3d56638437a9658888802b639a46792825e6a73c8eb4e060","impliedFormat":99},{"version":"6def25aee4733ebf8625cc8ec6d71c3461507ad47dbd0b8b0b560feaff72e9bb","impliedFormat":99},{"version":"715d90adc185f1cfd1d3cfa8fbc7547f8867fd4ab0b94bd7f69fb6fc0712332f","impliedFormat":99},{"version":"70afc18e984dede6b32e9829a41f0aecdc1f7c2066a900dbaacc4648ca8a5867","impliedFormat":99},{"version":"7bc50a64c4e4d2a40d413b261a07f0e5acdd875ac9d4de6d014f1938f494fcc0","impliedFormat":99},{"version":"953ee863def1b11f321dcb17a7a91686aa582e69dd4ec370e9e33fbad2adcfd3","impliedFormat":99},{"version":"c6fcf55644bb1ee497dbe1debb485d5478abd8e8f9450c3134d1765bff93d141","impliedFormat":99},{"version":"e452b617664fc3d2db96f64ef3addadb8c1ef275eff7946373528b1d6c86a217","impliedFormat":99},{"version":"434a60088d7096cd59e8002f69e87077c620027103d20cd608a240d13881fba7","impliedFormat":99},{"version":"40d9502a7af4ad95d761c849dd6915c9c295b3049faca2728bff940231ca81d3","impliedFormat":99},{"version":"792d1145b644098c0bb411ffb584075eadcfbbd41d72cd9c85c7835212a71079","impliedFormat":99},{"version":"30d0ecf1c23d75cba9e57457703695a25003c4328f6d048171e91b20d1012aa2","impliedFormat":99},{"version":"f216cb46ebeff3f767183626f70d18242307b2c3aab203841ae1d309277aad6b","impliedFormat":99},{"version":"fa9c695ac6e545d4f8a416fb190e4a5e8c5bc2d23388b83f5ae1b765fff5add5","impliedFormat":99},{"version":"fe69ad9a4b9c61fa429e252aaf63ba4bd330bfd169432de7afbd45a8bf2f50a1","impliedFormat":99},{"version":"f294be0ee8508d25d0ea14b5170a056cae0439a6d555a23d7779e3c5c28430ae","impliedFormat":99},{"version":"99b487d1ed8af24e01c427b9837fd7230366ad661d389dc7f142e1c1c8c33b5e","impliedFormat":99},{"version":"1b38ddf6562131f17f58fccf394cced29b37ecf7b80d2a48cc5da424aeb390c4","impliedFormat":99},{"version":"8d2dc7051c83cee0ca7b6d4628dbb86e7726e114d54e0b9932b62cc5ddda7042","impliedFormat":99},{"version":"8192f4b4564ce64404d41c92cdea3c1ad2c2b4986fade78aa0a742a7f1b766d0","impliedFormat":99},{"version":"a384b0ea68d5a8c2ab6ad5fbd3ce1480e752e153dd23feb03d143e7ecc1ac2c7","impliedFormat":99},{"version":"d6a27acb0d9eaf6af0edc8153cd8702301ae561eb990c4b5fdf662ea14a53ce4","impliedFormat":99},{"version":"afad82addd1d9ee6e361606205bbda03e97cb3850f948e53fdbb82f160dc43c7","impliedFormat":99},{"version":"5ee44a60fe09b4c21f71506f6697107f19a01c9842980c7145a4f2938d4dafc4","impliedFormat":99},{"version":"3729454e7f755d54f08bad759e29cc87453323f90ffcbb3f425c4ede7224cfd3","impliedFormat":99},{"version":"da58347ef36f47a7270f743144760adbc97e88b6ff0be2623bc0de73898f66f6","impliedFormat":99},{"version":"852eb3e7189a7c9c6acf431d5d2f8527e590dca54b3f6a099b558e90bb452b08","impliedFormat":99},{"version":"d952a3349aac3cf317856da1db3afc0fb7d3369a310a4c38d268f532929c6d3d","impliedFormat":99},{"version":"5f9449e0561ba44c0f712f1093341e76543627358ed6d8ec8d9ba7f31ba6d2ea","impliedFormat":99},{"version":"c29d1afafb83246b29734f7e0d960bd52842c160994a1cb38db52a738ab52bad","impliedFormat":99},{"version":"7b6261a4407295b1057feba24a1333923dee852f67fe3c329c990ddcfa20adce","impliedFormat":99},{"version":"f20df3a5bd4e29ee7565a24e302d7f9ab7e71ac6e218720717f72f20695e34e8","impliedFormat":99},{"version":"380b919bfa0516118edaf25b99e45f855e7bc3fd75ce4163a1cfe4a666388804","impliedFormat":1},{"version":"0b24a72109c8dd1b41f94abfe1bb296ba01b3734b8ac632db2c48ffc5dccaf01","impliedFormat":1},{"version":"fcf79300e5257a23ed3bacaa6861d7c645139c6f7ece134d15e6669447e5e6db","impliedFormat":1},{"version":"187119ff4f9553676a884e296089e131e8cc01691c546273b1d0089c3533ce42","impliedFormat":1},{"version":"aa2c18a1b5a086bbcaae10a4efba409cc95ba7287d8cf8f2591b53704fea3dea","impliedFormat":1},{"version":"b88749bdb18fc1398370e33aa72bc4f88274118f4960e61ce26605f9b33c5ba2","impliedFormat":1},{"version":"0aaef8cded245bf5036a7a40b65622dd6c4da71f7a35343112edbe112b348a1e","impliedFormat":1},{"version":"00baffbe8a2f2e4875367479489b5d43b5fc1429ecb4a4cc98cfc3009095f52a","impliedFormat":1},{"version":"a873c50d3e47c21aa09fbe1e2023d9a44efb07cc0cb8c72f418bf301b0771fd3","impliedFormat":1},{"version":"7c14ccd2eaa82619fffc1bfa877eb68a012e9fb723d07ee98db451fadb618906","impliedFormat":1},{"version":"49c36529ee09ea9ce19525af5bb84985ea8e782cb7ee8c493d9e36d027a3d019","impliedFormat":1},{"version":"df996e25faa505f85aeb294d15ebe61b399cf1d1e49959cdfaf2cc0815c203f9","impliedFormat":1},{"version":"4f6a12044ee6f458db11964153830abbc499e73d065c51c329ec97407f4b13dd","impliedFormat":1},{"version":"a6a2ed5ba265ec3f2b119ae52b53d3235c4574fcb6f4407220c46814bc2c67e4","impliedFormat":1},{"version":"f67042a1a63935b23f1067ca734f6ac3c7ddf0ee484c5a33fffb0e87d042a235","impliedFormat":99},{"version":"eaf815d73c54d11e3e44fc7b724a5a8a28d70fcd823bf0427ff1d1517006cbf8","impliedFormat":99},{"version":"a089794ce5278f94b496773efccad2cfb51bd51eba55cc2586a1b5ea28e27aef","impliedFormat":99},{"version":"f4d1981b9803c429ae9dc1f8abf555b4024935ae45dc1ae67db8c337c0bcf90f","impliedFormat":99},{"version":"87c9536fabee8c5c5dd06c008eb4a8a487fce8988c89ff853ecb29b93eeef5b8","impliedFormat":99},{"version":"34913745cac2a3d9976abc5cb54bc08e15d9a258d9bd73ffe94fe72f4a6c509a","impliedFormat":99},{"version":"7e61c29172a5ccad8dd15233eb3de2307bdc6e1656958370c2baf89497caae7b","impliedFormat":99},{"version":"2231cee47957cde5f1747fa7a3acd64f87345c87fda0dd5ff6b7e35f7458bb46","impliedFormat":99},{"version":"f210253286d561e6bac9a41f8387133e8f2d62e3e42e074524c16794958e6ae8","impliedFormat":99},{"version":"1680eb0661d5eaa73d79a3cdf51154417cf137c382b034a7241b675496031a08","impliedFormat":99},{"version":"4f7da12ef64d40f273adeb503a3996d2210f8eea3008f655add89383943e9b5c","impliedFormat":99},{"version":"63d742ab455f1aba292073952fa70d636fdd998ea313f0a68164f912d03331be","impliedFormat":99},{"version":"79625168b6860d75b17c2b6037571f26758e89740619b4ebb7331356856330f8","impliedFormat":99},{"version":"94c363dc60940ba8ff678b742c85451cf8a78e4da83af8f0326a1f0c3f6d274e","impliedFormat":99},{"version":"c00e799972c83b2265dbfca0a660659443e67e8610f8b8b4e97be299adfc3add","impliedFormat":99},{"version":"35ca8f074201dd3d142e727ff5baefb65679c4f2d140e62d136c294ef034ca38","impliedFormat":99},{"version":"6a96237a01e444f83d068e3668f6114bf2a32e6725bf96abfdaf01adf10ef6d1","impliedFormat":99},{"version":"ae06faef379813a712d23df97fd4ddebff7c04334b3faf6d8b3ab8599936285e","impliedFormat":99},{"version":"35c85224c37455959ac15cfd2bb2fa6a7c63a41532379dd8cb43e6a341e48d3b","impliedFormat":99},{"version":"52dfaa6b30c53ece0958ed16e6d580a5deffd4d008f5e79b4b374e17e47d0ba9","impliedFormat":99},{"version":"2f914dad1e731faa80fad8c3becab4db3e77d78ab87720a6d009de2e8e7536d8","impliedFormat":99},{"version":"a241bac10bdbf27a8499d0d0c8cd72c9e20bc50e1502d08dfa62fa85e4651cec","impliedFormat":99},{"version":"4450507742c53f51d411ecefbf891527f149bfce177ebbae62b99ddbd7925532","impliedFormat":99},{"version":"46f3315d746e223e8eb5e2c24938adc3dd844111416e9a663f3d2d0d0de48cad","impliedFormat":99},{"version":"b5e9e17d062de12c93b08712759f73e109283bcbd535e967693a14b0c9c95237","impliedFormat":99},{"version":"ab0bfca8c34e7f018158f88c6b05d1b306146a2cf4af064bbfba2f352c883455","impliedFormat":99},{"version":"de2f673742dcca68de3bfe4910549dbfc6ed1322f205de544e5fa337202bead2","impliedFormat":99},{"version":"83145010d8ef1a047bbb1a3401974db304e61af8130de635d93fb5e6b1a94e63","impliedFormat":99},{"version":"06748b0ba63d881a7e9d8a4c233bb4f66466873fdcafea1714a86dd27dee0336","impliedFormat":99},{"version":"c92b432d85b5c569eef906864c7f550aa0219c4795a3a8a0669e097c7cd10229","impliedFormat":99},{"version":"1e4f2f359d1878672b809e2433869997be9e869996fc75244d75adc6abe12370","impliedFormat":99},{"version":"827d7f1c9e70bd39a4e98b9790242e96d4a1e78c2c1a90d0344c3a60ea70c8e3","impliedFormat":99},{"version":"bc28ff904cb0831e032ff539e4d887bd138fae46db74bafef8d98bf09fc6a956","impliedFormat":99},{"version":"f404566ecbd497a5b1ed0a9d73541fac7248fdc7b0de5b866e8a48f3d456235a","impliedFormat":99},{"version":"0cd85d1c5d3348c5299987a23e3af0a70e256b8c0c8d61ce894700e035e8745e","impliedFormat":99},{"version":"ec09e8ef678ad14b6f18cff428fb406f8e20ff6ac530c7c32a06032835c5abcd","impliedFormat":99},{"version":"e414ff213326360728b6a51437f5793778e79c0836b5589b02b0cc03c98110bd","impliedFormat":99},{"version":"5d3840233566489df97ca750fd6051cfec3864c00fcd318746d2c5aaf6a7001b","impliedFormat":99},{"version":"6724ee3a9b90af2e655f48f6084d8016eaeafa790321bafbfc8542fab0c232a7","impliedFormat":99}],"root":[[91,94],[258,265],506,521,522,[550,552],[554,559]],"options":{"allowJs":true,"esModuleInterop":true,"jsx":1,"module":199,"skipLibCheck":true,"strict":true,"target":9},"referencedMap":[[196,1],[195,2],[256,3],[96,4],[211,5],[244,3],[97,3],[95,3],[520,6],[508,7],[509,8],[507,9],[510,10],[511,11],[512,12],[513,13],[514,14],[515,15],[516,16],[517,17],[518,18],[519,19],[236,3],[284,20],[285,20],[286,21],[275,22],[287,23],[288,24],[289,25],[270,3],[273,26],[271,3],[272,3],[290,27],[291,28],[292,29],[293,30],[294,31],[295,32],[296,32],[297,33],[298,34],[299,35],[300,36],[276,3],[274,3],[301,37],[302,38],[303,39],[337,40],[304,41],[305,3],[306,42],[307,43],[308,44],[309,45],[310,46],[311,47],[312,48],[313,49],[314,50],[315,50],[316,51],[317,3],[318,52],[319,53],[321,54],[320,55],[322,56],[323,57],[324,58],[325,59],[326,60],[327,61],[328,62],[329,63],[330,64],[331,65],[332,66],[333,67],[334,68],[277,3],[278,3],[279,3],[281,69],[282,3],[283,3],[335,70],[336,71],[90,3],[193,3],[210,72],[257,73],[267,3],[541,3],[194,3],[351,74],[361,75],[350,74],[371,76],[342,77],[341,78],[370,79],[364,80],[369,81],[344,82],[358,83],[343,84],[367,85],[339,86],[338,79],[368,87],[340,88],[345,89],[346,3],[349,89],[268,3],[372,90],[362,91],[353,92],[354,93],[356,94],[352,95],[355,96],[365,79],[347,97],[348,98],[357,99],[269,100],[360,91],[359,89],[363,3],[366,101],[416,102],[391,103],[404,104],[388,105],[405,100],[414,106],[379,107],[380,108],[378,78],[413,79],[408,109],[412,110],[382,111],[401,112],[381,113],[411,114],[376,115],[377,116],[383,117],[384,3],[390,118],[387,117],[374,119],[415,120],[406,121],[394,122],[393,117],[395,123],[398,124],[392,125],[396,126],[409,79],[385,127],[386,128],[399,129],[375,130],[403,131],[402,117],[389,128],[397,132],[400,133],[407,3],[373,3],[410,134],[504,135],[503,136],[426,137],[423,3],[427,138],[431,139],[420,140],[430,141],[437,142],[505,143],[266,3],[418,3],[425,144],[421,145],[419,56],[429,146],[417,147],[428,148],[422,149],[439,150],[461,151],[450,152],[440,153],[447,154],[438,155],[448,3],[446,156],[442,157],[443,158],[441,159],[449,160],[424,161],[457,162],[454,163],[455,164],[456,165],[458,166],[464,167],[468,168],[467,169],[465,163],[466,163],[459,170],[462,171],[460,172],[463,173],[452,174],[436,175],[451,176],[435,177],[434,178],[453,179],[433,180],[471,181],[469,163],[470,182],[473,183],[472,184],[474,163],[478,185],[476,186],[477,187],[479,188],[482,189],[481,190],[484,191],[483,192],[487,193],[485,194],[486,195],[480,196],[475,197],[488,196],[489,198],[502,199],[490,192],[491,163],[492,200],[493,201],[494,202],[444,203],[445,204],[432,3],[495,205],[496,206],[499,207],[498,208],[500,209],[501,210],[497,211],[88,3],[89,3],[14,3],[15,3],[17,3],[16,3],[2,3],[18,3],[19,3],[20,3],[21,3],[22,3],[23,3],[24,3],[25,3],[3,3],[26,3],[27,3],[4,3],[28,3],[32,3],[29,3],[30,3],[31,3],[33,3],[34,3],[35,3],[5,3],[36,3],[37,3],[38,3],[39,3],[6,3],[43,3],[40,3],[41,3],[42,3],[44,3],[7,3],[45,3],[50,3],[51,3],[46,3],[47,3],[48,3],[49,3],[8,3],[55,3],[52,3],[53,3],[54,3],[56,3],[9,3],[57,3],[58,3],[59,3],[61,3],[60,3],[62,3],[63,3],[10,3],[64,3],[65,3],[66,3],[11,3],[67,3],[68,3],[69,3],[70,3],[71,3],[72,3],[12,3],[73,3],[74,3],[75,3],[76,3],[77,3],[1,3],[78,3],[79,3],[13,3],[80,3],[81,3],[82,3],[83,3],[84,3],[85,3],[86,3],[87,3],[280,90],[212,212],[102,213],[109,214],[104,3],[105,3],[103,215],[106,216],[98,3],[99,3],[110,217],[101,218],[107,3],[108,219],[100,220],[200,221],[206,222],[201,222],[203,222],[197,221],[207,223],[202,224],[204,222],[208,212],[205,222],[198,225],[199,226],[189,227],[118,228],[188,229],[120,230],[187,3],[119,231],[192,232],[191,233],[190,234],[111,3],[122,235],[121,228],[123,3],[113,229],[117,236],[112,3],[114,237],[115,238],[116,3],[209,239],[124,240],[125,240],[126,240],[127,240],[128,240],[129,240],[130,240],[131,240],[132,240],[133,240],[134,240],[135,240],[136,240],[137,240],[138,240],[139,240],[141,240],[140,240],[142,240],[143,240],[144,240],[145,240],[146,240],[147,240],[148,240],[186,241],[149,240],[150,240],[151,240],[152,240],[153,240],[154,240],[155,240],[156,240],[157,240],[158,240],[159,240],[160,240],[161,240],[162,240],[163,240],[164,240],[166,240],[165,240],[168,240],[167,240],[169,240],[170,240],[171,240],[172,240],[173,240],[174,240],[175,240],[176,240],[177,240],[178,240],[179,240],[180,240],[181,240],[182,240],[185,240],[183,240],[184,240],[243,242],[245,243],[524,244],[537,245],[523,246],[253,247],[547,248],[545,249],[527,250],[548,251],[544,252],[241,253],[240,254],[546,255],[235,256],[234,254],[242,3],[525,257],[543,258],[538,3],[526,259],[540,260],[542,261],[539,262],[247,263],[248,264],[249,3],[251,265],[252,3],[250,3],[246,3],[237,3],[238,266],[239,3],[528,3],[529,3],[536,267],[530,3],[531,3],[532,3],[533,264],[534,3],[535,3],[549,268],[255,269],[553,262],[254,3],[214,270],[222,271],[223,272],[224,3],[220,3],[221,273],[216,274],[233,275],[226,276],[227,3],[228,3],[225,277],[229,278],[217,274],[215,279],[213,279],[230,3],[231,3],[218,274],[232,279],[219,279],[93,280],[91,281],[94,3],[259,282],[260,283],[261,3],[262,283],[263,283],[264,3],[265,284],[506,285],[521,286],[522,283],[550,287],[551,3],[552,3],[92,3],[554,288],[555,3],[556,283],[258,289],[557,3],[558,3],[559,3]],"semanticDiagnosticsPerFile":[[260,[{"start":84,"length":23,"messageText":"Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './_plain_module_hooks.js'?","category":1,"code":2835}]],[550,[{"start":3116,"length":20,"messageText":"Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './97_bench_cadence.js'?","category":1,"code":2835},{"start":3420,"length":16,"messageText":"Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './97_bench_rtt.js'?","category":1,"code":2835},{"start":26388,"length":1,"messageText":"Parameter 'v' implicitly has an 'any' type.","category":1,"code":7006}]],[554,[{"start":192,"length":34,"messageText":"Cannot find module '@repo/lib/steps/paths-alias-test' or its corresponding type declarations.","category":1,"code":2307},{"start":627,"length":23,"messageText":"Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './_imported_step_only.js'?","category":1,"code":2835},{"start":706,"length":11,"messageText":"Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './helpers.js'?","category":1,"code":2835},{"start":58966,"length":15,"messageText":"Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './serde-steps.js'?","category":1,"code":2835}]],[559,[{"start":335,"length":16,"messageText":"Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './serde-models.js'?","category":1,"code":2835}]]],"affectedFilesPendingEmit":[93,91,94,259,260,261,262,263,264,265,506,521,522,550,551,552,92,554,555,556,258,557,558,559],"version":"6.0.3"} \ No newline at end of file diff --git a/workbench/example/workflows/99_e2e.ts b/workbench/example/workflows/99_e2e.ts index 28f92ce23c..e087d27ef7 100644 --- a/workbench/example/workflows/99_e2e.ts +++ b/workbench/example/workflows/99_e2e.ts @@ -4243,15 +4243,16 @@ export async function dynamicWorkflowDisallowedStep(value: number) { } ////////////////////////////////////////////////////////// -// Dynamic workflows: mission runner +// Dynamic workflows: published revisions // -// Backs the "Dynamic Workflows" cookbook recipe -// (docs/content/docs/v5/cookbook/advanced/dynamic-workflows.mdx): the step -// catalog, the start-time checks on a mission revision, and the fan-out parent -// are the recipe's code; keep the two in sync. The mission sources themselves -// are NOT here. As in the recipe, they are published after deploy: the e2e -// runner plays the operator and store, and hands each revision to -// `dynamicMissionRun`, which stands in for the recipe's start route. +// A runner for revisions published after deploy, plus a small bird-survey +// catalog kept as regression coverage for revision checks, hook disposal on a +// review timeout, and fan-out from a static parent. The revision checks +// (`bindMissionRevision`) mirror "Operating it" in the "Dynamic Workflows" +// cookbook recipe (docs/content/docs/v5/cookbook/advanced/dynamic-workflows.mdx); +// the recipe's own example is the customer-migration catalog further down. +// No procedure source lives here: the e2e runner plays the operator and store, +// and hands each revision to `dynamicMissionRun`. ////////////////////////////////////////////////////////// interface BirdDetection { @@ -4310,15 +4311,13 @@ const MISSION_CATALOG = { /** Bumped whenever a catalog step changes in a way old missions would notice. */ const MISSION_CATALOG_VERSION = 'birds@1'; -type CatalogStepName = keyof typeof MISSION_CATALOG; - /** One immutable, approved revision of a mission, as the store holds it. */ interface MissionRevision { missionId: string; revision: number; source: string; sourceSha256: string; - steps: Record; + steps: Record; catalogVersion: string; approval: { approvedBy: string; approvedAt: string }; } @@ -4333,6 +4332,533 @@ async function sha256Hex(text: string) { ).join(''); } +////////////////////////////////////////////////////////// +// Dynamic workflows: customer migration catalog +// +// Backs the "Dynamic Workflows" cookbook recipe: these steps are its deployed +// catalog. The procedures that call them (grouping, identity, ordering) are +// published after deploy and live in the e2e test file, byte-for-byte as the +// recipe shows them; keep the three in sync. +// +// The destination is a PURE SIMULATION. There is no shared store on every +// World the e2e suite runs on, so a plan's records are derived from the plan +// itself (and the plans it depends on), and `verifyChanges` reconciles the +// apply results it is handed. It shows SDK replay of recorded step results; +// it does not demonstrate destination-side deduplication after an ambiguous +// write, which needs a real backing store. +////////////////////////////////////////////////////////// + +/** A row of the synthetic legacy CRM export. */ +type LegacyRow = + | { + kind: 'account'; + id: string; + name: string; + contract: string; + owner: string; + parent?: string; + } + | { + kind: 'contact'; + id: string; + name: string; + email: string; + account: string; + }; + +/** Synthetic legacy CRM data: one customer's export, labelled as such. */ +const LEGACY_DATASETS: Record = { + 'acme-legacy': [ + { + kind: 'account', + id: 'acct-acme', + name: 'Acme Holdings', + contract: 'C-100', + owner: 'dana', + }, + { + kind: 'account', + id: 'acct-acme-east', + name: 'Acme East', + contract: 'C-100/E', + owner: 'dana', + parent: 'acct-acme', + }, + { + kind: 'account', + id: 'acct-acme-west', + name: 'Acme West', + contract: 'C-100/W', + owner: 'dana', + parent: 'acct-acme', + }, + { + kind: 'account', + id: 'acct-globex', + name: 'Globex', + contract: 'C-200', + owner: 'lee', + }, + { + kind: 'account', + id: 'acct-initech', + name: 'Initech', + contract: 'C-300', + owner: 'kim', + }, + { + kind: 'account', + id: 'acct-initech-ltd', + name: 'Initech Ltd', + contract: 'C-300', + owner: 'sam', + }, + { + kind: 'contact', + id: 'ct-ada-east', + name: 'Ada Park', + email: 'ada@acme.com', + account: 'acct-acme-east', + }, + { + kind: 'contact', + id: 'ct-ada-west', + name: 'Ada Park', + email: ' ADA@acme.com', + account: 'acct-acme-west', + }, + { + kind: 'contact', + id: 'ct-info-initech', + name: 'Initech front desk', + email: 'info@initech.com', + account: 'acct-initech', + }, + { + kind: 'contact', + id: 'ct-info-globex', + name: 'Globex reseller desk', + email: 'info@initech.com', + account: 'acct-globex', + }, + ], +}; + +const SOURCE_PAGE_SIZE = 4; + +type DestinationOp = + | { + op: 'createAccount'; + key: string; + name: string; + owner: string; + status?: 'provisional' | 'final'; + sources: string[]; + } + | { + op: 'createContact'; + key: string; + name: string; + email: string; + sources: string[]; + } + | { op: 'linkContact'; contact: string; account: string } + | { op: 'setParent'; account: string; parent: string } + | { op: 'setOwner'; key: string; owner: string } + | { op: 'markFinal'; key: string } + | { op: 'mergeAccount'; from: string; into: string } + | { op: 'mergeContact'; from: string; into: string }; + +interface DestinationAccount { + key: string; + name: string; + owner: string; + parent: string | null; + status: 'provisional' | 'final'; + sources: string[]; +} + +interface DestinationContact { + key: string; + name: string; + email: string; + accounts: string[]; + sources: string[]; +} + +interface DestinationRecords { + accounts: Record; + contacts: Record; +} + +/** An adapter-level conflict: the plan can be staged but not applied. */ +interface PlanConflict { + kind: 'owner'; + account: string; + values: [string, string]; +} + +interface StagedPlan { + planId: string; + migrationId: string; + destinationId: string; + groupKey: string; + operations: DestinationOp[]; + dependsOn: StagedPlan[]; + conflicts: PlanConflict[]; +} + +interface ApplyResult { + planId: string; + dependsOn: string[]; + applied: number; + replayed: boolean; + records: DestinationRecords; +} + +function stableJson(value: unknown): string { + if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`; + if (value && typeof value === 'object') { + return `{${Object.keys(value) + .sort() + .filter((key) => (value as Record)[key] !== undefined) + .map( + (key) => + `${JSON.stringify(key)}:${stableJson((value as Record)[key])}` + ) + .join(',')}}`; + } + return JSON.stringify(value); +} + +async function planIdFor(plan: Omit) { + return `plan-${( + await sha256Hex( + stableJson({ + migrationId: plan.migrationId, + destinationId: plan.destinationId, + groupKey: plan.groupKey, + operations: plan.operations, + dependsOn: plan.dependsOn.map((dependency) => dependency.planId), + }) + ) + ).slice(0, 16)}`; +} + +/** + * Applies a plan's operations to the records its dependencies produced, and + * collects its conflicts. Referential integrity lives here, not in the + * procedure: every reference must resolve to a record that exists at that + * point in the plan. + */ +function simulatePlan(plan: Omit): { + records: DestinationRecords; + conflicts: PlanConflict[]; +} { + const base: DestinationRecords = { accounts: {}, contacts: {} }; + for (const dependency of plan.dependsOn) { + const { records } = simulatePlan(dependency); + Object.assign(base.accounts, structuredClone(records.accounts)); + Object.assign(base.contacts, structuredClone(records.contacts)); + } + const conflicts: PlanConflict[] = []; + const { accounts, contacts } = base; + const label = `${plan.groupKey} plan`; + const account = (key: string) => { + const found = accounts[key]; + if (!found) throw new FatalError(`${label}: no account "${key}"`); + return found; + }; + const contact = (key: string) => { + const found = contacts[key]; + if (!found) throw new FatalError(`${label}: no contact "${key}"`); + return found; + }; + for (const op of plan.operations) { + switch (op.op) { + case 'createAccount': + if (accounts[op.key]) { + throw new FatalError(`${label}: account "${op.key}" exists`); + } + accounts[op.key] = { + key: op.key, + name: op.name, + owner: op.owner, + parent: null, + status: op.status ?? 'final', + sources: op.sources, + }; + break; + case 'createContact': + if (contacts[op.key]) { + throw new FatalError(`${label}: contact "${op.key}" exists`); + } + contacts[op.key] = { + key: op.key, + name: op.name, + email: op.email, + accounts: [], + sources: op.sources, + }; + break; + case 'linkContact': { + account(op.account); + const target = contact(op.contact); + if (!target.accounts.includes(op.account)) { + target.accounts.push(op.account); + } + break; + } + case 'setParent': + account(op.parent); + account(op.account).parent = op.parent; + break; + case 'setOwner': + account(op.key).owner = op.owner; + break; + case 'markFinal': + account(op.key).status = 'final'; + break; + case 'mergeAccount': { + const from = account(op.from); + const into = account(op.into); + if (from.owner !== into.owner) { + conflicts.push({ + kind: 'owner', + account: op.into, + values: [into.owner, from.owner], + }); + } + into.sources = [...into.sources, ...from.sources]; + delete accounts[op.from]; + for (const other of Object.values(accounts)) { + if (other.parent === op.from) other.parent = op.into; + } + for (const person of Object.values(contacts)) { + person.accounts = [ + ...new Set( + person.accounts.map((key) => (key === op.from ? op.into : key)) + ), + ]; + } + break; + } + case 'mergeContact': { + const from = contact(op.from); + const into = contact(op.into); + into.accounts = [...new Set([...into.accounts, ...from.accounts])]; + into.sources = [...into.sources, ...from.sources]; + delete contacts[op.from]; + break; + } + default: + throw new FatalError( + `${label}: unsupported operation ${JSON.stringify(op)}` + ); + } + } + return { records: base, conflicts }; +} + +/** Catalog step: one page of the customer's legacy export. */ +async function readSourcePage(request: { dataset: string; cursor?: number }) { + 'use step'; + const rows = LEGACY_DATASETS[request.dataset]; + if (!rows) throw new FatalError(`Unknown dataset "${request.dataset}"`); + const start = request.cursor ?? 0; + const end = start + SOURCE_PAGE_SIZE; + return { + records: rows.slice(start, end), + nextCursor: end < rows.length ? end : undefined, + }; +} + +/** Catalog step: which of these keys the destination already has. */ +async function lookupDestination(_request: { + destinationId: string; + keys: string[]; +}) { + 'use step'; + // The simulated destination starts empty for every migration. + return { matches: [] as string[] }; +} + +/** + * Catalog step: an immutable plan of supported operations. Its ID is a hash + * of its scope and contents, so a different plan never shares an ID. + */ +async function stageChanges(request: { + migrationId: string; + destinationId: string; + groupKey: string; + operations: DestinationOp[]; + dependsOn?: StagedPlan[]; +}): Promise { + 'use step'; + const draft = { + migrationId: request.migrationId, + destinationId: request.destinationId, + groupKey: request.groupKey, + operations: request.operations, + dependsOn: request.dependsOn ?? [], + }; + for (const dependency of draft.dependsOn) { + if (dependency.destinationId !== draft.destinationId) { + throw new FatalError( + 'A plan can only depend on plans for its destination' + ); + } + } + const staged = { ...draft, planId: await planIdFor(draft) }; + // Integrity is checked now, so a broken plan never reaches review. + const { conflicts } = simulatePlan(staged); + return { ...staged, conflicts }; +} + +/** Catalog step: asks a reviewer to resume `token` with a decision. */ +async function requestApproval(request: { + planId: string; + token: string; + summary: string; +}) { + 'use step'; + return { planId: request.planId, token: request.token }; +} + +/** + * Catalog step: applies a staged plan. The destination comes from the plan, + * and the idempotency key must be derived from the plan's ID, so one key can + * never name two payloads. + */ +async function applyChanges(request: { + plan: StagedPlan; + idempotencyKey: string; +}): Promise { + 'use step'; + const { plan } = request; + if ((await planIdFor(plan)) !== plan.planId) { + throw new FatalError(`Plan ${plan.planId} changed after it was staged`); + } + if (request.idempotencyKey !== `apply:${plan.planId}`) { + throw new FatalError( + `Idempotency key ${request.idempotencyKey} does not belong to plan ${plan.planId}` + ); + } + const { records, conflicts } = simulatePlan(plan); + if (conflicts.length > 0) { + throw new FatalError(`Plan ${plan.planId} has unresolved conflicts`); + } + return { + planId: plan.planId, + dependsOn: plan.dependsOn.map((dependency) => dependency.planId), + applied: plan.operations.length, + // The simulation keeps no memory between calls, so it never sees a replay. + replayed: false, + records, + }; +} + +/** Catalog step: integrity invariants over everything a migration applied. */ +async function verifyChanges(request: { + destinationId: string; + migrationId: string; + results: ApplyResult[]; +}) { + 'use step'; + // Reconcile: a result another result builds on is superseded by it, and + // the remaining results cover disjoint partitions. + const superseded = new Set(request.results.flatMap((r) => r.dependsOn)); + const accounts: Record = {}; + const contacts: Record = {}; + const violations: string[] = []; + for (const result of request.results) { + if (superseded.has(result.planId)) continue; + for (const [key, value] of Object.entries(result.records.accounts)) { + if (accounts[key]) violations.push(`account ${key} written by two plans`); + accounts[key] = value; + } + for (const [key, value] of Object.entries(result.records.contacts)) { + if (contacts[key]) violations.push(`contact ${key} written by two plans`); + contacts[key] = value; + } + } + for (const record of Object.values(accounts)) { + if (record.parent && !accounts[record.parent]) { + violations.push(`dangling parent ${record.key} -> ${record.parent}`); + } + if (record.status !== 'final') { + violations.push(`provisional account ${record.key}`); + } + } + // A contact is one person per normalized email within an account family. + const rootOf = (key: string): string => { + const parent = accounts[key]?.parent; + return parent && accounts[parent] ? rootOf(parent) : key; + }; + const people = new Map(); + for (const person of Object.values(contacts)) { + for (const key of person.accounts) { + if (!accounts[key]) { + violations.push(`dangling link ${person.key} -> ${key}`); + } + } + const lineages = new Set(person.accounts.map((key) => rootOf(key))); + for (const lineage of lineages) { + const identity = `${lineage}:${person.email.trim().toLowerCase()}`; + const seen = people.get(identity); + if (seen) violations.push(`duplicate contact ${seen} and ${person.key}`); + people.set(identity, person.key); + } + } + const byKey = (records: Record) => + Object.values(records).sort((a, b) => a.key.localeCompare(b.key)); + return { + ok: violations.length === 0, + violations, + counts: { + accounts: Object.keys(accounts).length, + contacts: Object.keys(contacts).length, + provisional: Object.values(accounts).filter( + (record) => record.status !== 'final' + ).length, + }, + accounts: byKey(accounts), + contacts: byKey(contacts).map((person) => ({ + ...person, + accounts: [...person.accounts].sort(), + })), + }; +} + +/** Catalog step: a human-readable summary of the migration. */ +async function publishReport(request: { + migrationId: string; + results: unknown; +}) { + 'use step'; + return { + reportId: `report-${request.migrationId}`, + summary: JSON.stringify(request.results), + }; +} + +/** Every step a published migration procedure may bind, by catalog name. */ +const MIGRATION_CATALOG = { + readSourcePage, + lookupDestination, + stageChanges, + requestApproval, + applyChanges, + verifyChanges, + publishReport, +}; + +/** Catalogs this deployment serves, by version. */ +const CATALOGS: Record unknown>> = { + [MISSION_CATALOG_VERSION]: MISSION_CATALOG, + 'crm-migration@1': MIGRATION_CATALOG, +}; + /** * Start-time checks on a stored revision: it has an approval record, its * source still matches the hash recorded with it, and it targets the catalog @@ -4348,17 +4874,18 @@ async function bindMissionRevision(revision: MissionRevision) { if ((await sha256Hex(revision.source)) !== revision.sourceSha256) { throw new FatalError(`Mission ${label} does not match its recorded hash`); } - if (revision.catalogVersion !== MISSION_CATALOG_VERSION) { + if (!Object.hasOwn(CATALOGS, revision.catalogVersion)) { throw new FatalError( - `Mission ${label} targets catalog ${revision.catalogVersion}, but this deployment serves ${MISSION_CATALOG_VERSION}` + `Mission ${label} targets catalog ${revision.catalogVersion}, which this deployment does not serve` ); } - const steps: Record = {}; + const catalog = CATALOGS[revision.catalogVersion]; + const steps: Record unknown> = {}; for (const [alias, name] of Object.entries(revision.steps)) { - if (!Object.hasOwn(MISSION_CATALOG, name)) { + if (!Object.hasOwn(catalog, name)) { throw new FatalError(`Mission ${label} binds unknown step "${name}"`); } - steps[alias] = MISSION_CATALOG[name]; + steps[alias] = catalog[name]; } return steps; } From bdb25a9389b48f9d40bd397dab72b9ced579778d Mon Sep 17 00:00:00 2001 From: Alex Langenfeld Date: Thu, 1 Oct 2026 14:45:16 -0500 Subject: [PATCH 06/11] test(e2e): align the migration fixture with the shared synthetic-crm-v1 contract Signed-off-by: Alex Langenfeld --- .../cookbook/advanced/dynamic-workflows.mdx | 32 +++++++------- .../core/e2e/e2e-dynamic-workflow.test.ts | 42 +++++++++---------- workbench/example/workflows/99_e2e.ts | 33 +++++++++------ 3 files changed, 57 insertions(+), 50 deletions(-) diff --git a/docs/content/docs/v5/cookbook/advanced/dynamic-workflows.mdx b/docs/content/docs/v5/cookbook/advanced/dynamic-workflows.mdx index 1f260417b0..ebcb8828c4 100644 --- a/docs/content/docs/v5/cookbook/advanced/dynamic-workflows.mdx +++ b/docs/content/docs/v5/cookbook/advanced/dynamic-workflows.mdx @@ -45,13 +45,13 @@ A procedure is trusted application code with your deployment's privileges, not a The platform deploys a fixed set of migration steps once. Steps do the I/O: they read the legacy export and write to the destination. The procedure decides what to write and in what order, and reaches the steps only through `steps.()`. ```typescript title="workflows/migration-steps.ts" lineNumbers -declare const legacyCrm: { page(dataset: string, cursor?: number): Promise<{ records: unknown[]; nextCursor?: number }> }; // @setup +declare const legacyCrm: { page(dataset: string, cursor: string | null): Promise<{ records: unknown[]; nextCursor: string | null }> }; // @setup declare const destinations: { lookup(r: any): Promise; stage(r: any): Promise; apply(r: any): Promise; verify(r: any): Promise }; // @setup declare function notifyReviewer(r: any): Promise; // @setup declare function saveReport(r: any): Promise; // @setup /** One page of the customer's legacy export. */ -export async function readSourcePage(request: { dataset: string; cursor?: number }) { +export async function readSourcePage(request: { dataset: string; cursor: string | null }) { "use step"; return await legacyCrm.page(request.dataset, request.cursor); } @@ -136,18 +136,18 @@ export const MIGRATION_CATALOG = { This is Acme's procedure. It is a file in the integration team's repository, not part of the deployment. It migrates each account family as its own plan, all families at once, and only the family whose owners disagree waits for a person: {/* @skip-typecheck: dynamic workflow source, evaluated with injected steps and createHook */} -```javascript title="procedures/acme-parent-first.js" lineNumbers +```javascript title="procedures/crm-parent-first.js" lineNumbers async function workflow(input) { "use workflow"; const scope = { migrationId: input.migrationId, destinationId: input.destinationId }; const rows = []; - let cursor; + let cursor = null; do { const page = await steps.readSourcePage({ dataset: input.dataset, cursor }); rows.push(...page.records); cursor = page.nextCursor; - } while (cursor !== undefined); + } while (cursor !== null); // This customer's rules: a family is every account under one contract root, // accounts on the same contract are one account, and a contact is one person @@ -156,7 +156,7 @@ async function workflow(input) { const contacts = rows.filter((row) => row.kind === "contact"); const families = new Map(); for (const row of accounts) { - const family = row.contract.split("/")[0]; + const family = row.contract.split("/")[0].toLowerCase(); families.set(family, [...(families.get(family) || []), row]); } @@ -165,7 +165,7 @@ async function workflow(input) { const ordered = []; const visit = (row) => { if (ordered.includes(row)) return; - const parent = members.find((other) => other.id === row.parent); + const parent = members.find((other) => other.id === row.parentId); if (parent) visit(parent); ordered.push(row); }; @@ -176,7 +176,7 @@ async function workflow(input) { const survivorOf = new Map(); for (const row of ordered) { ops.push({ op: "createAccount", key: row.id, name: row.name, owner: row.owner, sources: [row.id] }); - if (row.parent) ops.push({ op: "setParent", account: row.id, parent: row.parent }); + if (row.parentId) ops.push({ op: "setParent", account: row.id, parent: row.parentId }); const survivor = survivors.get(row.contract); if (!survivor) { survivors.set(row.contract, row.id); @@ -192,11 +192,11 @@ async function workflow(input) { const people = new Map(); for (const row of contacts) { - if (!survivorOf.has(row.account)) continue; + if (!survivorOf.has(row.accountId)) continue; const email = row.email.trim().toLowerCase(); const person = people.get(email) || { key: row.id, name: row.name, email, sources: [], accounts: [] }; person.sources.push(row.id); - person.accounts.push(survivorOf.get(row.account)); + person.accounts.push(survivorOf.get(row.accountId)); people.set(email, person); } for (const person of people.values()) { @@ -243,7 +243,7 @@ async function workflow(input) { The procedure owns the customer's policy, and nothing in the catalog knows about it: -- **Families.** Accounts are grouped by the root of their contract number, so `C-100`, `C-100/E`, and `C-100/W` are one family. Each family becomes its own plan, and `Promise.all` runs them concurrently. +- **Families.** Accounts are grouped by the root of their contract number, so `C-100`, `C-100/E`, and `C-100/W` are one family, keyed `c-100`. Each family becomes its own plan, and `Promise.all` runs them concurrently. - **Parent-first order.** Within a family, an account is written only after the account it reports to. The destination checks that every parent resolves, so writing a child first would fail the plan. - **One account per contract.** Two rows on the same contract become one account through `mergeAccount`. If their owners differ, the plan comes back with a conflict, and the procedure asks a person. - **One person per email within a family.** Ada Park appears once per region with differently cased emails, and becomes one contact linked to both regions. `info@initech.com` appears in two families, so it stays two contacts. @@ -253,7 +253,7 @@ Every parameter, including the dataset, the destination, and the token prefix, a ## Publish and start it -Nothing about this procedure existed when the platform deployed. After the procedure is reviewed and merged in the integration repository, a release job stores the file and its step names wherever the platform keeps procedures, keyed by a stable procedure ID such as `acme-parent-first`. A route then starts it on the running deployment: +Nothing about this procedure existed when the platform deployed. After the procedure is reviewed and merged in the integration repository, a release job stores the file and its step names wherever the platform keeps procedures, keyed by a stable procedure ID such as `crm-parent-first`. A route then starts it on the running deployment: ```typescript title="app/api/migrations/route.ts" lineNumbers import { start } from "workflow/api"; @@ -313,7 +313,7 @@ The whole sequence is: ## Review an owner conflict -Initech's two rows share contract `C-300` but list different owners, so only that family waits. Its token is the run's `tokenPrefix` plus the family. The review route resumes it after authorizing the reviewer: +Initech's two rows share contract `C-300` but list different owners, so only that family waits. Its token is the run's `tokenPrefix` plus the family key, the lowercased contract root (`c-300`). The review route resumes it after authorizing the reviewer: ```typescript title="app/api/migrations/[runId]/reviews/route.ts" lineNumbers import { getHookByToken, resumeHook } from "workflow/api"; @@ -388,7 +388,7 @@ After the reviewer picks `sam` as Initech's owner, the destination holds: Another customer team prefers to see everything land before anything is reconciled. Procedure B runs over the same catalog and is published later, without a deploy. It is structured differently: instead of building each family carefully and pausing only the ambiguous one, it writes every row as it is, then stages one consolidation over all of it, and asks for one review of the whole change: {/* @skip-typecheck: excerpt of dynamic workflow source */} -```javascript title="procedures/acme-provisional-first.js (excerpt)" lineNumbers +```javascript title="procedures/crm-provisional-first.js (excerpt)" lineNumbers // 1. Land every row as written: provisional, flat, one record per row. const load = await steps.stageChanges({ ...scope, @@ -397,7 +397,7 @@ Another customer team prefers to see everything land before anything is reconcil ...accounts.map((row) => ({ op: "createAccount", key: row.id, name: row.name, owner: row.owner, status: "provisional", sources: [row.id] })), ...contacts.flatMap((row) => [ { op: "createContact", key: row.id, name: row.name, email: row.email.trim().toLowerCase(), sources: [row.id] }, - { op: "linkContact", contact: row.id, account: row.account }, + { op: "linkContact", contact: row.id, account: row.accountId }, ]), ], }); @@ -520,7 +520,7 @@ See [Rules for the source](/docs/advanced/dynamic-workflows#rules-for-the-source ## Testing it -The e2e tests behind this recipe run both procedures against a synthetic `acme-legacy` dataset and a **pure simulated destination**: each plan's records are derived from the plan and the plans it depends on, and `verifyChanges` reconciles the results it is given. That shows the workflow's guarantees, including replay of recorded step results across a pause. It does not show destination-side deduplication when a write's outcome is unknown, which needs a destination that stores idempotency keys; test that against your real adapter. +The e2e tests behind this recipe run both procedures against a synthetic `synthetic-crm-v1` dataset and a **pure simulated destination**: each plan's records are derived from the plan and the plans it depends on, and `verifyChanges` reconciles the results it is given. That shows the workflow's guarantees, including replay of recorded step results across a pause. It does not show destination-side deduplication when a write's outcome is unknown, which needs a destination that stores idempotency keys; test that against your real adapter. ## Key APIs diff --git a/packages/core/e2e/e2e-dynamic-workflow.test.ts b/packages/core/e2e/e2e-dynamic-workflow.test.ts index 81ed06fd48..d621a6c36e 100644 --- a/packages/core/e2e/e2e-dynamic-workflow.test.ts +++ b/packages/core/e2e/e2e-dynamic-workflow.test.ts @@ -786,12 +786,12 @@ async function workflow(input) { const scope = { migrationId: input.migrationId, destinationId: input.destinationId }; const rows = []; - let cursor; + let cursor = null; do { const page = await steps.readSourcePage({ dataset: input.dataset, cursor }); rows.push(...page.records); cursor = page.nextCursor; - } while (cursor !== undefined); + } while (cursor !== null); // This customer's rules: a family is every account under one contract root, // accounts on the same contract are one account, and a contact is one person @@ -800,7 +800,7 @@ async function workflow(input) { const contacts = rows.filter((row) => row.kind === "contact"); const families = new Map(); for (const row of accounts) { - const family = row.contract.split("/")[0]; + const family = row.contract.split("/")[0].toLowerCase(); families.set(family, [...(families.get(family) || []), row]); } @@ -809,7 +809,7 @@ async function workflow(input) { const ordered = []; const visit = (row) => { if (ordered.includes(row)) return; - const parent = members.find((other) => other.id === row.parent); + const parent = members.find((other) => other.id === row.parentId); if (parent) visit(parent); ordered.push(row); }; @@ -820,7 +820,7 @@ async function workflow(input) { const survivorOf = new Map(); for (const row of ordered) { ops.push({ op: "createAccount", key: row.id, name: row.name, owner: row.owner, sources: [row.id] }); - if (row.parent) ops.push({ op: "setParent", account: row.id, parent: row.parent }); + if (row.parentId) ops.push({ op: "setParent", account: row.id, parent: row.parentId }); const survivor = survivors.get(row.contract); if (!survivor) { survivors.set(row.contract, row.id); @@ -836,11 +836,11 @@ async function workflow(input) { const people = new Map(); for (const row of contacts) { - if (!survivorOf.has(row.account)) continue; + if (!survivorOf.has(row.accountId)) continue; const email = row.email.trim().toLowerCase(); const person = people.get(email) || { key: row.id, name: row.name, email, sources: [], accounts: [] }; person.sources.push(row.id); - person.accounts.push(survivorOf.get(row.account)); + person.accounts.push(survivorOf.get(row.accountId)); people.set(email, person); } for (const person of people.values()) { @@ -892,12 +892,12 @@ async function workflow(input) { const scope = { migrationId: input.migrationId, destinationId: input.destinationId }; const rows = []; - let cursor; + let cursor = null; do { const page = await steps.readSourcePage({ dataset: input.dataset, cursor }); rows.push(...page.records); cursor = page.nextCursor; - } while (cursor !== undefined); + } while (cursor !== null); const accounts = rows.filter((row) => row.kind === "account"); const contacts = rows.filter((row) => row.kind === "contact"); @@ -909,7 +909,7 @@ async function workflow(input) { ...accounts.map((row) => ({ op: "createAccount", key: row.id, name: row.name, owner: row.owner, status: "provisional", sources: [row.id] })), ...contacts.flatMap((row) => [ { op: "createContact", key: row.id, name: row.name, email: row.email.trim().toLowerCase(), sources: [row.id] }, - { op: "linkContact", contact: row.id, account: row.account }, + { op: "linkContact", contact: row.id, account: row.accountId }, ]), ], }); @@ -919,7 +919,7 @@ async function workflow(input) { function consolidation(owner) { const ops = []; for (const row of accounts) { - if (row.parent) ops.push({ op: "setParent", account: row.id, parent: row.parent }); + if (row.parentId) ops.push({ op: "setParent", account: row.id, parent: row.parentId }); } const survivors = new Map(); for (const row of accounts) { @@ -933,7 +933,7 @@ async function workflow(input) { } const people = new Map(); for (const row of contacts) { - const holder = accounts.find((account) => account.id === row.account); + const holder = accounts.find((account) => account.id === row.accountId); const identity = holder.contract.split("/")[0] + ":" + row.email.trim().toLowerCase(); const person = people.get(identity); if (person) ops.push({ op: "mergeContact", from: row.id, into: person }); @@ -1011,7 +1011,7 @@ const PROCEDURE_B = publishProcedure( function migrationInput() { const id = crypto.randomUUID(); return { - dataset: 'acme-legacy', + dataset: 'synthetic-crm-v1', migrationId: `migration-${id}`, destinationId: `destination-${id}`, tokenPrefix: `e2e-migration:${id}`, @@ -1095,7 +1095,7 @@ describeJs( input, ]); - const hook = await waitForMissionHook(`${input.tokenPrefix}:C-300`); + const hook = await waitForMissionHook(`${input.tokenPrefix}:c-300`); expect(hook.runId).toBe(childRunId); // Acme and Globex apply while Initech is still waiting for a person. const deadline = Date.now() + 30_000; @@ -1112,9 +1112,9 @@ describeJs( const child = await awaitChildRun(childRunId); expect(child.output).toMatchObject({ families: [ - { family: 'C-100', status: 'applied' }, - { family: 'C-200', status: 'applied' }, - { family: 'C-300', status: 'applied' }, + { family: 'c-100', status: 'applied' }, + { family: 'c-200', status: 'applied' }, + { family: 'c-300', status: 'applied' }, ], verification: { ok: true, @@ -1132,16 +1132,16 @@ describeJs( PROCEDURE_A, input, ]); - await resumeMissionHook(`${input.tokenPrefix}:C-300`, { + await resumeMissionHook(`${input.tokenPrefix}:c-300`, { approved: false, }); const child = await awaitChildRun(childRunId); expect(child.output).toMatchObject({ families: [ - { family: 'C-100', status: 'applied' }, - { family: 'C-200', status: 'applied' }, - { family: 'C-300', status: 'skipped' }, + { family: 'c-100', status: 'applied' }, + { family: 'c-200', status: 'applied' }, + { family: 'c-300', status: 'skipped' }, ], verification: { ok: true, diff --git a/workbench/example/workflows/99_e2e.ts b/workbench/example/workflows/99_e2e.ts index e087d27ef7..0da7c43552 100644 --- a/workbench/example/workflows/99_e2e.ts +++ b/workbench/example/workflows/99_e2e.ts @@ -4348,31 +4348,32 @@ async function sha256Hex(text: string) { // write, which needs a real backing store. ////////////////////////////////////////////////////////// -/** A row of the synthetic legacy CRM export. */ +/** A row of the synthetic legacy CRM export. Every name and email is invented. */ type LegacyRow = | { kind: 'account'; id: string; name: string; + parentId: string | null; contract: string; owner: string; - parent?: string; } | { kind: 'contact'; id: string; name: string; email: string; - account: string; + accountId: string; }; /** Synthetic legacy CRM data: one customer's export, labelled as such. */ const LEGACY_DATASETS: Record = { - 'acme-legacy': [ + 'synthetic-crm-v1': [ { kind: 'account', id: 'acct-acme', name: 'Acme Holdings', + parentId: null, contract: 'C-100', owner: 'dana', }, @@ -4380,22 +4381,23 @@ const LEGACY_DATASETS: Record = { kind: 'account', id: 'acct-acme-east', name: 'Acme East', + parentId: 'acct-acme', contract: 'C-100/E', owner: 'dana', - parent: 'acct-acme', }, { kind: 'account', id: 'acct-acme-west', name: 'Acme West', + parentId: 'acct-acme', contract: 'C-100/W', owner: 'dana', - parent: 'acct-acme', }, { kind: 'account', id: 'acct-globex', name: 'Globex', + parentId: null, contract: 'C-200', owner: 'lee', }, @@ -4403,6 +4405,7 @@ const LEGACY_DATASETS: Record = { kind: 'account', id: 'acct-initech', name: 'Initech', + parentId: null, contract: 'C-300', owner: 'kim', }, @@ -4410,6 +4413,7 @@ const LEGACY_DATASETS: Record = { kind: 'account', id: 'acct-initech-ltd', name: 'Initech Ltd', + parentId: null, contract: 'C-300', owner: 'sam', }, @@ -4418,28 +4422,28 @@ const LEGACY_DATASETS: Record = { id: 'ct-ada-east', name: 'Ada Park', email: 'ada@acme.com', - account: 'acct-acme-east', + accountId: 'acct-acme-east', }, { kind: 'contact', id: 'ct-ada-west', name: 'Ada Park', email: ' ADA@acme.com', - account: 'acct-acme-west', + accountId: 'acct-acme-west', }, { kind: 'contact', id: 'ct-info-initech', name: 'Initech front desk', email: 'info@initech.com', - account: 'acct-initech', + accountId: 'acct-initech', }, { kind: 'contact', id: 'ct-info-globex', name: 'Globex reseller desk', email: 'info@initech.com', - account: 'acct-globex', + accountId: 'acct-globex', }, ], }; @@ -4661,15 +4665,18 @@ function simulatePlan(plan: Omit): { } /** Catalog step: one page of the customer's legacy export. */ -async function readSourcePage(request: { dataset: string; cursor?: number }) { +async function readSourcePage(request: { + dataset: string; + cursor: string | null; +}) { 'use step'; const rows = LEGACY_DATASETS[request.dataset]; if (!rows) throw new FatalError(`Unknown dataset "${request.dataset}"`); - const start = request.cursor ?? 0; + const start = request.cursor === null ? 0 : Number(request.cursor); const end = start + SOURCE_PAGE_SIZE; return { records: rows.slice(start, end), - nextCursor: end < rows.length ? end : undefined, + nextCursor: end < rows.length ? String(end) : null, }; } From b69524698c4da42ed9506b9ff872716d1311d887 Mon Sep 17 00:00:00 2001 From: Alex Langenfeld Date: Thu, 1 Oct 2026 14:53:35 -0500 Subject: [PATCH 07/11] chore: drop an accidentally committed tsbuildinfo Signed-off-by: Alex Langenfeld --- workbench/example/tsconfig.tsbuildinfo | 1 - 1 file changed, 1 deletion(-) delete mode 100644 workbench/example/tsconfig.tsbuildinfo diff --git a/workbench/example/tsconfig.tsbuildinfo b/workbench/example/tsconfig.tsbuildinfo deleted file mode 100644 index 95dd67cb22..0000000000 --- a/workbench/example/tsconfig.tsbuildinfo +++ /dev/null @@ -1 +0,0 @@ -{"fileNames":["../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es5.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2016.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2017.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2018.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2019.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2020.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2021.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2022.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2023.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2024.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2025.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.esnext.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.dom.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.dom.iterable.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.core.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.collection.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.generator.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.promise.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2016.intl.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2017.date.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2017.object.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2017.string.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2017.intl.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2018.intl.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2018.promise.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2019.array.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2019.object.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2019.string.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2019.intl.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2020.date.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2020.promise.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2020.string.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2020.intl.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2020.number.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2021.promise.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2021.string.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2021.weakref.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2021.intl.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2022.array.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2022.error.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2022.intl.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2022.object.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2022.string.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2022.regexp.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2023.array.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2023.collection.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2023.intl.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2024.collection.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2024.object.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2024.promise.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2024.regexp.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2024.string.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2025.collection.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2025.float16.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2025.intl.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2025.iterator.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2025.promise.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2025.regexp.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.esnext.array.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.esnext.collection.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.esnext.date.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.esnext.decorators.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.esnext.disposable.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.esnext.error.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.esnext.intl.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.esnext.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.esnext.temporal.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.esnext.typedarrays.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.decorators.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.decorators.legacy.d.ts","../../node_modules/.pnpm/@vercel+otel@1.13.0_@opentelemetry+api-logs@0.57.2_@opentelemetry+api@1.9.1_@openteleme_cef2455b7bbd346e216bd42a495369a8/node_modules/@vercel/otel/dist/types/index.d.ts","./instrumentation.ts","./workflows/98_duplicate_case.ts","./api/test-direct-step-call.ts","./workflows/0_workflow_only.ts","../../node_modules/.pnpm/@types+json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts","../../node_modules/.pnpm/@ai-sdk+provider@4.0.9/node_modules/@ai-sdk/provider/dist/index.d.ts","../../node_modules/.pnpm/@standard-schema+spec@1.1.0/node_modules/@standard-schema/spec/dist/index.d.ts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v3/helpers/typealiases.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v3/helpers/util.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v3/zoderror.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v3/locales/en.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v3/errors.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v3/helpers/parseutil.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v3/helpers/enumutil.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v3/helpers/errorutil.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v3/helpers/partialutil.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v3/standard-schema.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v3/types.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v3/external.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v3/index.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/json-schema.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/standard-schema.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/registries.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/to-json-schema.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/util.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/versions.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/schemas.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/checks.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/errors.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/core.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/parse.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/memoizer.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/regexes.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/ar.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/az.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/be.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/bg.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/bn.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/ca.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/ckb.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/cs.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/da.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/de.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/el.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/en.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/eo.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/es.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/fa.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/fi.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/fr.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/fr-ca.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/gu.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/he.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/hi.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/hr.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/hu.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/hy.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/id.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/is.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/it.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/ja.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/ka.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/kh.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/km.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/kn.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/ko.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/lt.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/mk.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/ms.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/ne.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/nl.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/nn.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/no.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/ota.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/ps.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/pl.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/pt.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/pt-br.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/ro.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/ru.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/sk.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/sl.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/sv.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/ta.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/th.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/tk.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/tr.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/ua.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/uk.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/ur.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/uz.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/vi.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/zh-cn.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/zh-tw.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/yo.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/locales/index.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/doc.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/compile.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/api.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/json-schema-processors.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/json-schema-generator.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/core/index.d.cts","../../node_modules/.pnpm/@workflow+serde@4.1.0/node_modules/@workflow/serde/dist/index.d.ts","../../node_modules/.pnpm/eventsource-parser@3.1.0/node_modules/eventsource-parser/dist/stream.d.ts","../../node_modules/.pnpm/@ai-sdk+provider-utils@5.0.34_zod@4.5.4/node_modules/@ai-sdk/provider-utils/dist/index.d.ts","../../node_modules/.pnpm/@ai-sdk+gateway@4.0.69_zod@4.5.4/node_modules/@ai-sdk/gateway/dist/index.d.ts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/classic/errors.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/classic/parse.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/classic/schemas.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/classic/checks.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/classic/compat.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/classic/from-json-schema.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/classic/deep-partial.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/classic/in-out.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/classic/iso.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/classic/coerce.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/classic/external.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/classic/index.d.cts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/index.d.cts","../../node_modules/.pnpm/ai@7.0.85_zod@4.5.4/node_modules/ai/dist/index.d.ts","../../node_modules/.pnpm/@ai-sdk+workflow@2.0.15_workflow@packages+workflow_zod@4.5.4/node_modules/@ai-sdk/workflow/dist/index.d.ts","../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/index.d.cts","../../packages/world/dist/shared.d.ts","../../packages/world/dist/analytics.d.ts","../../packages/world/dist/serialization.d.ts","../../packages/world/dist/hooks.d.ts","../../packages/world/dist/runs.d.ts","../../packages/world/dist/steps.d.ts","../../packages/world/dist/waits.d.ts","../../packages/world/dist/event-metadata.d.ts","../../packages/world/dist/events.d.ts","../../packages/world/dist/attributes-validation.d.ts","../../packages/world/dist/attributes.d.ts","../../packages/world/dist/env-config.d.ts","../../packages/world/dist/queue.d.ts","../../packages/world/dist/interfaces.d.ts","../../packages/world/dist/invocation.d.ts","../../packages/world/dist/node-http-flag.d.ts","../../packages/world/dist/recovery.d.ts","../../packages/world/dist/slot-identity.d.ts","../../packages/world/dist/spec-version.d.ts","../../packages/world/dist/ulid.d.ts","../../packages/world/dist/index.d.ts","../../packages/core/dist/runtime/world.d.ts","../../packages/core/dist/runtime/world-init.d.ts","../../node_modules/.pnpm/@types+ms@2.1.0/node_modules/@types/ms/index.d.ts","../../packages/errors/dist/error-codes.d.ts","../../packages/errors/dist/index.d.ts","../../packages/serde/dist/index.d.ts","../../packages/core/dist/runtime/runs.d.ts","../../packages/core/dist/runtime/run.d.ts","../../packages/core/dist/schemas.d.ts","../../packages/core/dist/create-hook.d.ts","../../node_modules/.pnpm/@standard-schema+spec@1.0.0/node_modules/@standard-schema/spec/dist/index.d.ts","../../packages/core/dist/define-hook.d.ts","../../packages/core/dist/workflow/set-attributes.d.ts","../../packages/core/dist/set-attributes.d.ts","../../packages/core/dist/sleep.d.ts","../../packages/core/dist/step/get-step-metadata.d.ts","../../packages/core/dist/workflow/get-workflow-metadata.d.ts","../../packages/core/dist/step/get-workflow-metadata.d.ts","../../packages/core/dist/step/writable-stream.d.ts","../../packages/core/dist/index.d.ts","../../packages/workflow/dist/stdlib.d.ts","../../packages/workflow/dist/index.d.ts","../../node_modules/.pnpm/@ai-sdk+provider-utils@5.0.34_zod@4.5.4/node_modules/@ai-sdk/provider-utils/dist/test/index.d.ts","../../node_modules/.pnpm/ai@7.0.85_zod@4.5.4/node_modules/ai/dist/test/index.d.ts","./workflows/ai-sdk-test-provider.ts","./workflows/100_durable_agent_e2e.ts","./workflows/102_plain_module_hook.ts","./workflows/10_single_stmt_control_flow.ts","./workflows/1_simple.ts","./workflows/2_control_flow.ts","./workflows/3_streams.ts","./workflows/4_ai.ts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/internal/builtin-types.d.mts","../../node_modules/.pnpm/buffer@6.0.3/node_modules/buffer/index.d.ts","../../node_modules/.pnpm/node_modules/undici-types/header.d.ts","../../node_modules/.pnpm/node_modules/undici-types/readable.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/compatibility/disposable.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/compatibility/indexable.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/compatibility/iterators.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/compatibility/index.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/globals.typedarray.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/buffer.buffer.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/globals.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/web-globals/abortcontroller.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/web-globals/domexception.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/web-globals/events.d.ts","../../node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/index.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/web-globals/fetch.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/web-globals/navigator.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/web-globals/storage.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/assert.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/assert/strict.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/async_hooks.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/buffer.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/child_process.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/cluster.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/console.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/constants.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/crypto.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/dgram.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/diagnostics_channel.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/dns.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/dns/promises.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/domain.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/events.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/fs.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/fs/promises.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/http.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/http2.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/https.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/inspector.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/inspector.generated.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/module.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/net.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/os.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/path.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/perf_hooks.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/process.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/punycode.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/querystring.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/readline.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/readline/promises.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/repl.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/sea.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/sqlite.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/stream.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/stream/promises.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/stream/consumers.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/stream/web.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/string_decoder.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/test.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/timers.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/timers/promises.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/tls.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/trace_events.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/tty.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/url.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/util.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/v8.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/vm.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/wasi.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/worker_threads.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/zlib.d.ts","../../node_modules/.pnpm/@types+node@22.19.0/node_modules/@types/node/index.d.ts","../../node_modules/.pnpm/node_modules/undici-types/file.d.ts","../../node_modules/.pnpm/node_modules/undici-types/fetch.d.ts","../../node_modules/.pnpm/node_modules/undici-types/formdata.d.ts","../../node_modules/.pnpm/node_modules/undici-types/connector.d.ts","../../node_modules/.pnpm/node_modules/undici-types/client.d.ts","../../node_modules/.pnpm/node_modules/undici-types/errors.d.ts","../../node_modules/.pnpm/node_modules/undici-types/dispatcher.d.ts","../../node_modules/.pnpm/node_modules/undici-types/global-dispatcher.d.ts","../../node_modules/.pnpm/node_modules/undici-types/global-origin.d.ts","../../node_modules/.pnpm/node_modules/undici-types/pool-stats.d.ts","../../node_modules/.pnpm/node_modules/undici-types/pool.d.ts","../../node_modules/.pnpm/node_modules/undici-types/handlers.d.ts","../../node_modules/.pnpm/node_modules/undici-types/balanced-pool.d.ts","../../node_modules/.pnpm/node_modules/undici-types/agent.d.ts","../../node_modules/.pnpm/node_modules/undici-types/mock-interceptor.d.ts","../../node_modules/.pnpm/node_modules/undici-types/mock-agent.d.ts","../../node_modules/.pnpm/node_modules/undici-types/mock-client.d.ts","../../node_modules/.pnpm/node_modules/undici-types/mock-pool.d.ts","../../node_modules/.pnpm/node_modules/undici-types/mock-errors.d.ts","../../node_modules/.pnpm/node_modules/undici-types/proxy-agent.d.ts","../../node_modules/.pnpm/node_modules/undici-types/env-http-proxy-agent.d.ts","../../node_modules/.pnpm/node_modules/undici-types/retry-handler.d.ts","../../node_modules/.pnpm/node_modules/undici-types/retry-agent.d.ts","../../node_modules/.pnpm/node_modules/undici-types/api.d.ts","../../node_modules/.pnpm/node_modules/undici-types/interceptors.d.ts","../../node_modules/.pnpm/node_modules/undici-types/util.d.ts","../../node_modules/.pnpm/node_modules/undici-types/cookies.d.ts","../../node_modules/.pnpm/node_modules/undici-types/patch.d.ts","../../node_modules/.pnpm/node_modules/undici-types/websocket.d.ts","../../node_modules/.pnpm/node_modules/undici-types/eventsource.d.ts","../../node_modules/.pnpm/node_modules/undici-types/filereader.d.ts","../../node_modules/.pnpm/node_modules/undici-types/diagnostics-channel.d.ts","../../node_modules/.pnpm/node_modules/undici-types/content-type.d.ts","../../node_modules/.pnpm/node_modules/undici-types/cache.d.ts","../../node_modules/.pnpm/node_modules/undici-types/index.d.ts","../../node_modules/.pnpm/node_modules/undici/types/utility.d.ts","../../node_modules/.pnpm/node_modules/undici/types/header.d.ts","../../node_modules/.pnpm/node_modules/undici/types/readable.d.ts","../../node_modules/.pnpm/node_modules/undici/types/fetch.d.ts","../../node_modules/.pnpm/node_modules/undici/types/formdata.d.ts","../../node_modules/.pnpm/node_modules/undici/types/connector.d.ts","../../node_modules/.pnpm/node_modules/undici/types/client-stats.d.ts","../../node_modules/.pnpm/node_modules/undici/types/client.d.ts","../../node_modules/.pnpm/node_modules/undici/types/errors.d.ts","../../node_modules/.pnpm/node_modules/undici/types/dispatcher.d.ts","../../node_modules/.pnpm/node_modules/undici/types/global-dispatcher.d.ts","../../node_modules/.pnpm/node_modules/undici/types/global-origin.d.ts","../../node_modules/.pnpm/node_modules/undici/types/pool-stats.d.ts","../../node_modules/.pnpm/node_modules/undici/types/pool.d.ts","../../node_modules/.pnpm/node_modules/undici/types/handlers.d.ts","../../node_modules/.pnpm/node_modules/undici/types/balanced-pool.d.ts","../../node_modules/.pnpm/node_modules/undici/types/round-robin-pool.d.ts","../../node_modules/.pnpm/node_modules/undici/types/h2c-client.d.ts","../../node_modules/.pnpm/node_modules/undici/types/agent.d.ts","../../node_modules/.pnpm/node_modules/undici/types/mock-interceptor.d.ts","../../node_modules/.pnpm/node_modules/undici/types/mock-call-history.d.ts","../../node_modules/.pnpm/node_modules/undici/types/mock-agent.d.ts","../../node_modules/.pnpm/node_modules/undici/types/mock-client.d.ts","../../node_modules/.pnpm/node_modules/undici/types/mock-pool.d.ts","../../node_modules/.pnpm/node_modules/undici/types/snapshot-agent.d.ts","../../node_modules/.pnpm/node_modules/undici/types/mock-errors.d.ts","../../node_modules/.pnpm/node_modules/undici/types/proxy-agent.d.ts","../../node_modules/.pnpm/node_modules/undici/types/socks5-proxy-agent.d.ts","../../node_modules/.pnpm/node_modules/undici/types/env-http-proxy-agent.d.ts","../../node_modules/.pnpm/node_modules/undici/types/retry-handler.d.ts","../../node_modules/.pnpm/node_modules/undici/types/retry-agent.d.ts","../../node_modules/.pnpm/node_modules/undici/types/api.d.ts","../../node_modules/.pnpm/node_modules/undici/types/cache-interceptor.d.ts","../../node_modules/.pnpm/node_modules/undici/types/interceptors.d.ts","../../node_modules/.pnpm/node_modules/undici/types/util.d.ts","../../node_modules/.pnpm/node_modules/undici/types/cookies.d.ts","../../node_modules/.pnpm/node_modules/undici/types/patch.d.ts","../../node_modules/.pnpm/node_modules/undici/types/websocket.d.ts","../../node_modules/.pnpm/node_modules/undici/types/eventsource.d.ts","../../node_modules/.pnpm/node_modules/undici/types/diagnostics-channel.d.ts","../../node_modules/.pnpm/node_modules/undici/types/content-type.d.ts","../../node_modules/.pnpm/node_modules/undici/types/cache.d.ts","../../node_modules/.pnpm/node_modules/undici/types/index.d.ts","../../node_modules/.pnpm/node_modules/undici/index.d.ts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/internal/types.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/internal/headers.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/internal/shim-types.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/core/streaming.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/internal/request-options.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/internal/utils/log.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/core/error.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/pagination.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/internal/parse.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/core/api-promise.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/core/pagination.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/internal/uploads.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/internal/to-file.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/core/uploads.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/core/resource.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/shared.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/completions.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/chat/completions/messages.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/chat/completions/index.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/chat/completions.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/error.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/lib/eventstream.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/lib/abstractchatcompletionrunner.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/lib/chatcompletionstream.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/lib/responsesparser.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/lib/responses/eventtypes.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/lib/responses/responsestream.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/responses/input-items.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/responses/responses.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/lib/parser.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/lib/chatcompletionstreamingrunner.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/lib/jsonschema.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/lib/runnablefunction.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/lib/chatcompletionrunner.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/chat/completions/completions.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/chat/chat.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/chat/index.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/audio/speech.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/audio/transcriptions.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/audio/translations.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/audio/audio.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/batches.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/beta/threads/messages.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/beta/threads/runs/steps.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/lib/assistantstream.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/beta/threads/runs/runs.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/beta/threads/threads.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/beta/assistants.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/beta/realtime/sessions.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/beta/realtime/transcription-sessions.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/beta/realtime/realtime.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/beta/beta.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/containers/files/content.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/containers/files/files.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/containers/containers.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/conversations/items.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/conversations/conversations.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/embeddings.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/graders/grader-models.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/evals/runs/output-items.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/evals/runs/runs.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/evals/evals.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/files.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/fine-tuning/methods.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/fine-tuning/alpha/graders.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/fine-tuning/alpha/alpha.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/fine-tuning/checkpoints/permissions.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/fine-tuning/checkpoints/checkpoints.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/fine-tuning/jobs/checkpoints.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/fine-tuning/jobs/jobs.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/fine-tuning/fine-tuning.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/graders/graders.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/images.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/models.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/moderations.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/realtime/calls.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/realtime/client-secrets.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/realtime/realtime.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/uploads/parts.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/uploads/uploads.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/uploads.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/vector-stores/files.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/vector-stores/file-batches.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/vector-stores/vector-stores.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/webhooks.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/resources/index.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/client.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/azure.d.mts","../../node_modules/.pnpm/openai@6.1.0_ws@8.20.0_zod@4.5.4/node_modules/openai/index.d.mts","./workflows/5_hooks.ts","../../node_modules/.pnpm/@types+lodash@4.17.20/node_modules/@types/lodash/common/common.d.ts","../../node_modules/.pnpm/@types+lodash@4.17.20/node_modules/@types/lodash/common/array.d.ts","../../node_modules/.pnpm/@types+lodash@4.17.20/node_modules/@types/lodash/common/collection.d.ts","../../node_modules/.pnpm/@types+lodash@4.17.20/node_modules/@types/lodash/common/date.d.ts","../../node_modules/.pnpm/@types+lodash@4.17.20/node_modules/@types/lodash/common/function.d.ts","../../node_modules/.pnpm/@types+lodash@4.17.20/node_modules/@types/lodash/common/lang.d.ts","../../node_modules/.pnpm/@types+lodash@4.17.20/node_modules/@types/lodash/common/math.d.ts","../../node_modules/.pnpm/@types+lodash@4.17.20/node_modules/@types/lodash/common/number.d.ts","../../node_modules/.pnpm/@types+lodash@4.17.20/node_modules/@types/lodash/common/object.d.ts","../../node_modules/.pnpm/@types+lodash@4.17.20/node_modules/@types/lodash/common/seq.d.ts","../../node_modules/.pnpm/@types+lodash@4.17.20/node_modules/@types/lodash/common/string.d.ts","../../node_modules/.pnpm/@types+lodash@4.17.20/node_modules/@types/lodash/common/util.d.ts","../../node_modules/.pnpm/@types+lodash@4.17.20/node_modules/@types/lodash/index.d.ts","../../node_modules/.pnpm/@types+lodash.chunk@4.2.9/node_modules/@types/lodash.chunk/index.d.ts","./workflows/6_batching.ts","./workflows/7_full.ts","../../packages/core/dist/global.d.ts","../../packages/core/dist/encryption.d.ts","../../packages/core/dist/sealed-box.d.ts","../../packages/core/dist/serialization/encryption.d.ts","../../packages/core/dist/runtime/helpers.d.ts","../../packages/utils/dist/debug-log.d.ts","../../packages/utils/dist/global-singleton.d.ts","../../packages/utils/dist/parse-name.d.ts","../../packages/utils/dist/pluralize.d.ts","../../packages/utils/dist/promise.d.ts","../../packages/utils/dist/time.d.ts","../../packages/utils/dist/workflow-routes.d.ts","../../packages/utils/dist/world-target.d.ts","../../packages/utils/dist/index.d.ts","../../packages/core/dist/flushable-stream.d.ts","../../packages/core/dist/serialization/compression.d.ts","../../packages/core/dist/serialization/types.d.ts","../../packages/core/dist/serialization/format.d.ts","../../node_modules/.pnpm/devalue@5.9.2/node_modules/devalue/types/index.d.ts","../../packages/core/dist/serialization/hardened.d.ts","../../packages/core/dist/serialization.d.ts","../../packages/core/dist/runtime/resume-hook.d.ts","../../packages/core/dist/runtime/dynamic-workflow.d.ts","../../packages/core/dist/runtime/start.d.ts","../../packages/core/dist/runtime.d.ts","../../packages/core/dist/runtime/lifecycle-hooks.d.ts","../../packages/workflow/dist/api.d.ts","./workflows/97_bench.ts","./workflows/97_bench_cadence.ts","./workflows/97_bench_rtt.ts","../../packages/workflow/dist/internal/errors.d.ts","./workflows/99_e2e.ts","./workflows/_imported_step_only.ts","./workflows/_plain_module_hooks.ts","./workflows/helpers.ts","./workflows/serde-models.ts","./workflows/serde-steps.ts"],"fileIdsList":[[96,195,275,287,304,305],[96,97,110,192,193,194,275,287,304,305],[275,287,304,305],[95,275,287,304,305],[96,195,210,275,287,304,305],[275,287,304,305,519],[275,287,304,305,507,509,510,511,512,513,514,515,516,517,518,519],[275,287,304,305,507,508,510,511,512,513,514,515,516,517,518,519],[275,287,304,305,508,509,510,511,512,513,514,515,516,517,518,519],[275,287,304,305,507,508,509,511,512,513,514,515,516,517,518,519],[275,287,304,305,507,508,509,510,512,513,514,515,516,517,518,519],[275,287,304,305,507,508,509,510,511,513,514,515,516,517,518,519],[275,287,304,305,507,508,509,510,511,512,514,515,516,517,518,519],[275,287,304,305,507,508,509,510,511,512,513,515,516,517,518,519],[275,287,304,305,507,508,509,510,511,512,513,514,516,517,518,519],[275,287,304,305,507,508,509,510,511,512,513,514,515,517,518,519],[275,287,304,305,507,508,509,510,511,512,513,514,515,516,518,519],[275,287,304,305,507,508,509,510,511,512,513,514,515,516,517,519],[275,287,304,305,507,508,509,510,511,512,513,514,515,516,517,518],[275,284,285,287,304,305],[275,286,287,304,305],[287,304,305],[275,287,292,304,305,322],[275,287,288,293,298,304,305,307,319,330,377],[275,287,288,289,298,304,305,307],[270,271,272,275,287,304,305],[275,287,290,304,305,331],[275,287,291,292,299,304,305,308],[275,287,292,304,305,319,327,377],[275,287,293,295,298,304,305,307,377],[275,286,287,294,304,305],[275,287,295,296,304,305],[275,287,297,298,304,305],[275,286,287,298,304,305],[275,287,298,299,300,304,305,319,330,377],[275,287,298,299,300,304,305,314,319,322,377],[275,287,295,298,301,304,305,307,319,330,372,377],[275,287,298,299,301,302,304,305,307,319,327,330,377],[275,287,301,303,304,305,319,327,330,377],[273,274,275,276,277,278,279,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336],[275,287,298,304,305],[275,287,304,305,306,330],[275,287,295,298,304,305,307,319,377],[275,287,304,305,308],[275,287,304,305,309],[275,286,287,304,305,310],[275,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,377],[275,287,304,305,312],[275,287,304,305,313],[275,287,298,304,305,314,315],[275,287,304,305,314,316,331,333],[275,287,299,304,305],[275,287,298,304,305,319,320,322,377],[275,287,304,305,321,322,377],[275,287,304,305,319,320],[275,287,304,305,322],[275,287,304,305,323],[275,284,287,304,305,319,324],[275,287,298,304,305,325,326],[275,287,304,305,325,326],[275,287,292,304,305,307,319,327,377],[275,287,304,305,328],[275,287,304,305,307,329],[275,287,301,304,305,313,330,377],[275,287,292,304,305,331],[275,287,304,305,319,332,377],[275,287,304,305,306,333,377],[275,287,304,305,334],[275,287,304,305,372],[275,287,298,300,304,305,310,319,322,330,332,333,335,372],[275,287,304,305,319,336,377],[96,195,196,209,275,287,301,304,305],[96,195,256,275,287,304,305],[275,287,304,305,330,344,348],[275,287,304,305,319,330,344],[275,287,304,305,339],[275,287,304,305,327,330,341,344],[275,287,304,305,307,327],[275,287,304,305,337],[275,287,304,305,337,339],[275,287,304,305,307,330,341,344],[268,269,275,287,298,304,305,319,330,340,343],[275,287,304,305,344,351],[268,275,287,304,305,342],[275,287,304,305,344,365,366],[275,287,304,305,322,330,337,340,344],[275,287,304,305,337,365],[275,287,304,305,337,338,339],[275,287,304,305,344],[275,287,304,305,338,339,340,341,342,343,344,345,346,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,366,367,368,369,370,371],[275,287,304,305,344,359],[275,287,304,305,344,351,352],[275,287,304,305,342,344,352,353],[275,287,304,305,343],[268,275,287,304,305,339,344],[275,287,304,305,344,348,352,353],[275,287,304,305,348],[275,287,304,305,330,342,344,347],[268,275,287,304,305,341,344,351],[275,287,304,305,319],[275,287,304,305,335,337,339,344,365],[275,287,304,305,415],[275,287,304,305,330,379,382,385,386],[275,287,304,305,319,330,382],[275,287,304,305,330,382,386],[275,287,304,305,376],[275,287,304,305,380],[275,287,304,305,330,378,379,382],[275,287,304,305,337,376],[275,287,304,305,307,330,378,382],[275,287,298,304,305,319,330,373,374,375,377,381],[275,287,304,305,382,391,399],[275,287,304,305,374,380],[275,287,304,305,382,409,410],[275,287,304,305,322,330,337,374,377,382],[275,287,304,305,337,376,377],[275,287,304,305,382],[275,287,304,305,330,378,382],[275,287,304,305,373],[275,287,304,305,376,377,378,380,381,382,383,384,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,410,411,412,413,414],[275,287,295,304,305,382,402,405],[275,287,304,305,382,391,392,393],[275,287,304,305,380,382,392,394],[275,287,304,305,381],[275,287,304,305,374,376,382],[275,287,304,305,382,386,392,394],[275,287,304,305,386],[275,287,304,305,330,380,382,385],[275,287,304,305,374,378,382,391],[275,287,304,305,319,377],[275,287,304,305,382,402],[275,287,304,305,394],[275,287,304,305,374,378,382,386],[275,287,304,305,322,335,337,376,377,382,409],[266,275,287,304,305,418,421,503],[266,275,287,304,305,417,418,421,422,423,426,427,430,433,445,451,452,457,458,468,471,473,474,478,479,487,488,489,490,491,494,496,500,501,502],[275,287,304,305,417,425,503],[275,287,304,305,421,425,426,503],[275,287,304,305,503],[275,287,304,305,419,503],[275,287,304,305,428,429],[275,287,304,305,423],[275,287,304,305,423,426,427,430,503,504],[275,287,304,305,421,424,503],[266,275,287,304,305,417,418,420],[266,275,287,304,305],[275,287,304,305,372,416],[266,275,287,304,305,421,503],[275,287,304,305,421,503],[275,287,304,305,421,433,436,438,447,449,450,505],[275,287,304,305,419,421,438,459,460,462,463,464],[275,287,304,305,436,439,446,449,505],[275,287,304,305,419,421,436,439,451,505],[275,287,304,305,419,436,439,440,446,449,505],[275,287,304,305,437],[275,287,304,305,432,436,445],[275,287,304,305,445],[275,287,304,305,421,438,441,442,445,505],[275,287,304,305,436,445,446],[275,287,304,305,447,448,450],[275,287,304,305,427],[275,287,304,305,431,454,455,456],[275,287,304,305,421,426,431],[275,287,304,305,420,421,426,430,431,455,457],[275,287,304,305,421,426,430,431,455,457],[275,287,304,305,421,426,427,431,432,458],[275,287,304,305,421,426,427,431,432,459,460,461,462,463],[275,287,304,305,431,463,464,467],[275,287,304,305,431,432,465,466,467],[275,287,304,305,421,426,427,431,432,464],[275,287,304,305,420,421,426,427,431,432,459,460,461,462,463,464],[275,287,304,305,421,426,427,431,432,460],[275,287,304,305,420,421,426,431,432,459,461,462,463,464],[275,287,304,305,431,432,451],[275,287,304,305,435],[275,287,304,305,420,421,426,427,431,432,433,434,439,440,446,447,449,450,451],[275,287,304,305,434,451],[275,287,304,305,421,427,431,451],[275,287,304,305,435,452],[275,287,304,305,420,421,426,431,433,451],[275,287,304,305,421,426,427,431,470],[275,287,304,305,421,426,427,430,431,469],[275,287,304,305,421,426,431,432,445,472],[275,287,304,305,421,426,427,431,445,473],[275,287,304,305,421,426,427,431,432,445,475,477],[275,287,304,305,421,426,427,431,477],[275,287,304,305,421,426,427,431,432,445,451,476],[275,287,304,305,421,426,427,430,431],[275,287,304,305,431,481],[275,287,304,305,421,426,431,475],[275,287,304,305,431,483],[275,287,304,305,421,426,427,431],[275,287,304,305,431,480,482,484,486],[275,287,304,305,421,427,431],[275,287,304,305,421,426,427,431,432,480,485],[275,287,304,305,431,475],[275,287,304,305,431,432,445],[275,287,304,305,420,421,426,430,431,489],[275,287,304,305,432,433,445,453,457,458,468,471,473,474,478,479,487,488,489,490,491,494,496,500,501],[275,287,304,305,421,426,431,445,494],[275,287,304,305,421,426,431,445,493,494],[275,287,304,305,431,432,445,492,493,494],[275,287,304,305,421,427,431,445],[275,287,304,305,420,421,426,427,431,432,441,443,444,445],[275,287,304,305,421,426,430,431],[275,287,304,305,421,426,431,479,495],[275,287,304,305,421,426,427,431,497,498,500],[275,287,304,305,421,426,427,431,497,500],[275,287,304,305,421,426,427,431,432,498,499],[275,287,304,305,418,431],[275,287,304,305,430],[207,275,287,304,305],[100,101,275,287,304,305],[98,99,100,102,103,108,275,287,304,305],[99,100,275,287,304,305],[108,275,287,304,305],[109,275,287,304,305],[100,275,287,304,305],[98,99,100,103,104,105,106,107,275,287,304,305],[98,99,110,275,287,304,305],[192,275,287,304,305],[192,199,275,287,304,305],[115,186,190,192,197,198,199,200,201,202,203,204,205,206,275,287,304,305],[111,113,199,275,287,304,305],[192,197,275,287,304,305],[112,118,192,198,275,287,304,305],[113,115,117,118,119,120,275,287,304,305],[115,117,119,120,275,287,304,305],[117,120,275,287,304,305],[115,117,119,275,287,304,305],[112,115,117,118,120,275,287,304,305],[111,113,114,115,116,117,118,119,120,121,122,123,186,187,188,189,190,191,275,287,304,305],[111,113,114,117,275,287,304,305],[113,114,117,275,287,304,305],[117,275,287,304,305],[111,112,114,115,116,118,119,120,275,287,304,305],[111,112,113,117,192,275,287,304,305],[117,118,119,120,275,287,304,305],[207,208,275,287,304,305],[119,275,287,304,305],[124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,275,287,304,305],[236,241,242,275,287,304,305],[233,243,244,275,287,304,305],[275,287,292,304,305],[275,287,304,305,536],[233,242,275,287,304,305],[238,243,245,247,248,249,251,252,275,287,304,305],[233,234,240,241,275,287,304,305,523,527,544,546],[238,275,287,304,305,546],[233,275,287,304,305,526],[238,241,275,287,304,305,526],[233,275,287,304,305,543],[233,239,240,275,287,304,305],[233,275,287,304,305],[233,241,275,287,304,305,527,545],[234,275,287,304,305],[275,287,304,305,524],[238,275,287,304,305,526,537,538,539,540,542],[275,287,304,305,524,525],[275,287,304,305,539],[275,287,304,305,541],[238,275,287,304,305],[246,275,287,304,305],[236,275,287,304,305],[250,275,287,304,305],[236,237,275,287,304,305],[275,287,304,305,528,529,530,531,532,533,534,535],[235,241,275,287,304,305,544,546,547,548],[235,253,254,275,287,304,305],[212,213,275,287,304,305],[221,275,287,304,305],[212,222,275,287,304,305],[212,213,216,217,218,219,220,275,287,304,305],[212,213,215,275,287,304,305],[213,214,215,216,217,218,219,221,223,224,225,226,227,228,229,230,231,232,275,287,304,305],[213,214,216,217,218,221,223,225,275,287,304,305],[209,275,287,304,305],[225,226,275,287,304,305],[212,275,287,304,305],[92,275,287,304,305],[90,275,287,304,305],[209,210,211,255,258,275,287,304,305],[255,275,287,304,305],[209,210,255,275,287,304,305],[255,275,287,304,305,505],[275,287,304,305,520],[255,275,287,304,305,549],[255,275,287,301,304,305,549,553],[257,275,287,304,305]],"fileInfos":[{"version":"bcd24271a113971ba9eb71ff8cb01bc6b0f872a85c23fdbe5d93065b375933cd","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f88bedbeb09c6f5a6645cb24c7c55f1aa22d19ae96c8e6959cbd8b85a707bc6","impliedFormat":1},{"version":"7fe93b39b810eadd916be8db880dd7f0f7012a5cc6ffb62de8f62a2117fa6f1f","impliedFormat":1},{"version":"bb0074cc08b84a2374af33d8bf044b80851ccc9e719a5e202eacf40db2c31600","impliedFormat":1},{"version":"1a7daebe4f45fb03d9ec53d60008fbf9ac45a697fdc89e4ce218bc94b94f94d6","impliedFormat":1},{"version":"f94b133a3cb14a288803be545ac2683e0d0ff6661bcd37e31aaaec54fc382aed","impliedFormat":1},{"version":"f59d0650799f8782fd74cf73c19223730c6d1b9198671b1c5b3a38e1188b5953","impliedFormat":1},{"version":"8a15b4607d9a499e2dbeed9ec0d3c0d7372c850b2d5f1fb259e8f6d41d468a84","impliedFormat":1},{"version":"26e0fe14baee4e127f4365d1ae0b276f400562e45e19e35fd2d4c296684715e6","impliedFormat":1},{"version":"1e9332c23e9a907175e0ffc6a49e236f97b48838cc8aec9ce7e4cec21e544b65","impliedFormat":1},{"version":"3753fbc1113dc511214802a2342280a8b284ab9094f6420e7aa171e868679f91","impliedFormat":1},{"version":"999ca32883495a866aa5737fe1babc764a469e4cde6ee6b136a4b9ae68853e4b","impliedFormat":1},{"version":"17f13ecb98cbc39243f2eee1f16d45cd8ec4706b03ee314f1915f1a8b42f6984","impliedFormat":1},{"version":"d6b1eba8496bdd0eed6fc8a685768fe01b2da4a0388b5fe7df558290bffcf32f","affectsGlobalScope":true,"impliedFormat":1},{"version":"7f57fc4404ff020bc45b9c620aff2b40f700b95fe31164024c453a5e3c163c54","impliedFormat":1},{"version":"eadcffda2aa84802c73938e589b9e58248d74c59cb7fcbca6474e3435ac15504","affectsGlobalScope":true,"impliedFormat":1},{"version":"105ba8ff7ba746404fe1a2e189d1d3d2e0eb29a08c18dded791af02f29fb4711","affectsGlobalScope":true,"impliedFormat":1},{"version":"00343ca5b2e3d48fa5df1db6e32ea2a59afab09590274a6cccb1dbae82e60c7c","affectsGlobalScope":true,"impliedFormat":1},{"version":"ebd9f816d4002697cb2864bea1f0b70a103124e18a8cd9645eeccc09bdf80ab4","affectsGlobalScope":true,"impliedFormat":1},{"version":"2c1afac30a01772cd2a9a298a7ce7706b5892e447bb46bdbeef720f7b5da77ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"7b0225f483e4fa685625ebe43dd584bb7973bbd84e66a6ba7bbe175ee1048b4f","affectsGlobalScope":true,"impliedFormat":1},{"version":"c0a4b8ac6ce74679c1da2b3795296f5896e31c38e888469a8e0f99dc3305de60","affectsGlobalScope":true,"impliedFormat":1},{"version":"3084a7b5f569088e0146533a00830e206565de65cae2239509168b11434cd84f","affectsGlobalScope":true,"impliedFormat":1},{"version":"c5079c53f0f141a0698faa903e76cb41cd664e3efb01cc17a5c46ec2eb0bef42","affectsGlobalScope":true,"impliedFormat":1},{"version":"32cafbc484dea6b0ab62cf8473182bbcb23020d70845b406f80b7526f38ae862","affectsGlobalScope":true,"impliedFormat":1},{"version":"fca4cdcb6d6c5ef18a869003d02c9f0fd95df8cfaf6eb431cd3376bc034cad36","affectsGlobalScope":true,"impliedFormat":1},{"version":"b93ec88115de9a9dc1b602291b85baf825c85666bf25985cc5f698073892b467","affectsGlobalScope":true,"impliedFormat":1},{"version":"f5c06dcc3fe849fcb297c247865a161f995cc29de7aa823afdd75aaaddc1419b","affectsGlobalScope":true,"impliedFormat":1},{"version":"b77e16112127a4b169ef0b8c3a4d730edf459c5f25fe52d5e436a6919206c4d7","affectsGlobalScope":true,"impliedFormat":1},{"version":"fbffd9337146eff822c7c00acbb78b01ea7ea23987f6c961eba689349e744f8c","affectsGlobalScope":true,"impliedFormat":1},{"version":"a995c0e49b721312f74fdfb89e4ba29bd9824c770bbb4021d74d2bf560e4c6bd","affectsGlobalScope":true,"impliedFormat":1},{"version":"c7b3542146734342e440a84b213384bfa188835537ddbda50d30766f0593aff9","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce6180fa19b1cccd07ee7f7dbb9a367ac19c0ed160573e4686425060b6df7f57","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f02e2476bccb9dbe21280d6090f0df17d2f66b74711489415a8aa4df73c9675","affectsGlobalScope":true,"impliedFormat":1},{"version":"45e3ab34c1c013c8ab2dc1ba4c80c780744b13b5676800ae2e3be27ae862c40c","affectsGlobalScope":true,"impliedFormat":1},{"version":"805c86f6cca8d7702a62a844856dbaa2a3fd2abef0536e65d48732441dde5b5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"e42e397f1a5a77994f0185fd1466520691456c772d06bf843e5084ceb879a0ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"f4c2b41f90c95b1c532ecc874bd3c111865793b23aebcc1c3cbbabcd5d76ffb0","affectsGlobalScope":true,"impliedFormat":1},{"version":"ab26191cfad5b66afa11b8bf935ef1cd88fabfcb28d30b2dfa6fad877d050332","affectsGlobalScope":true,"impliedFormat":1},{"version":"2088bc26531e38fb05eedac2951480db5309f6be3fa4a08d2221abb0f5b4200d","affectsGlobalScope":true,"impliedFormat":1},{"version":"cb9d366c425fea79716a8fb3af0d78e6b22ebbab3bd64d25063b42dc9f531c1e","affectsGlobalScope":true,"impliedFormat":1},{"version":"500934a8089c26d57ebdb688fc9757389bb6207a3c8f0674d68efa900d2abb34","affectsGlobalScope":true,"impliedFormat":1},{"version":"689da16f46e647cef0d64b0def88910e818a5877ca5379ede156ca3afb780ac3","affectsGlobalScope":true,"impliedFormat":1},{"version":"bc21cc8b6fee4f4c2440d08035b7ea3c06b3511314c8bab6bef7a92de58a2593","affectsGlobalScope":true,"impliedFormat":1},{"version":"7ca53d13d2957003abb47922a71866ba7cb2068f8d154877c596d63c359fed25","affectsGlobalScope":true,"impliedFormat":1},{"version":"54725f8c4df3d900cb4dac84b64689ce29548da0b4e9b7c2de61d41c79293611","affectsGlobalScope":true,"impliedFormat":1},{"version":"e5594bc3076ac29e6c1ebda77939bc4c8833de72f654b6e376862c0473199323","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f3eb332c2d73e729f3364fcc0c2b375e72a121e8157d25a82d67a138c83a95c","affectsGlobalScope":true,"impliedFormat":1},{"version":"6f4427f9642ce8d500970e4e69d1397f64072ab73b97e476b4002a646ac743b1","affectsGlobalScope":true,"impliedFormat":1},{"version":"48915f327cd1dea4d7bd358d9dc7732f58f9e1626a29cc0c05c8c692419d9bb7","affectsGlobalScope":true,"impliedFormat":1},{"version":"b7bf9377723203b5a6a4b920164df22d56a43f593269ba6ae1fdc97774b68855","affectsGlobalScope":true,"impliedFormat":1},{"version":"db9709688f82c9e5f65a119c64d835f906efe5f559d08b11642d56eb85b79357","affectsGlobalScope":true,"impliedFormat":1},{"version":"4b25b8c874acd1a4cf8444c3617e037d444d19080ac9f634b405583fd10ce1f7","affectsGlobalScope":true,"impliedFormat":1},{"version":"37be57d7c90cf1f8112ee2636a068d8fd181289f82b744160ec56a7dc158a9f5","affectsGlobalScope":true,"impliedFormat":1},{"version":"a917a49ac94cd26b754ab84e113369a75d1a47a710661d7cd25e961cc797065f","affectsGlobalScope":true,"impliedFormat":1},{"version":"6d3261badeb7843d157ef3e6f5d1427d0eeb0af0cf9df84a62cfd29fd47ac86e","affectsGlobalScope":true,"impliedFormat":1},{"version":"195daca651dde22f2167ac0d0a05e215308119a3100f5e6268e8317d05a92526","affectsGlobalScope":true,"impliedFormat":1},{"version":"8b11e4285cd2bb164a4dc09248bdec69e9842517db4ca47c1ba913011e44ff2f","affectsGlobalScope":true,"impliedFormat":1},{"version":"0508571a52475e245b02bc50fa1394065a0a3d05277fbf5120c3784b85651799","affectsGlobalScope":true,"impliedFormat":1},{"version":"8f9af488f510c3015af3cc8c267a9e9d96c4dd38a1fdff0e11dc5a544711415b","affectsGlobalScope":true,"impliedFormat":1},{"version":"fc611fea8d30ea72c6bbfb599c9b4d393ce22e2f5bfef2172534781e7d138104","affectsGlobalScope":true,"impliedFormat":1},{"version":"0bd714129fca875f7d4c477a1a392200b0bcd13fb2e80928cd334b63830ea047","affectsGlobalScope":true,"impliedFormat":1},{"version":"e2c9037ae6cd2c52d80ceef0b3c5ffdb488627d71529cf4f63776daf11161c9a","affectsGlobalScope":true,"impliedFormat":1},{"version":"135d5cf4d345f59f1a9caadfafcd858d3d9cc68290db616cc85797224448cccc","affectsGlobalScope":true,"impliedFormat":1},{"version":"bc238c3f81c2984751932b6aab223cd5b830e0ac6cad76389e5e9d2ffc03287d","affectsGlobalScope":true,"impliedFormat":1},{"version":"4a07f9b76d361f572620927e5735b77d6d2101c23cdd94383eb5b706e7b36357","affectsGlobalScope":true,"impliedFormat":1},{"version":"7c4e8dc6ab834cc6baa0227e030606d29e3e8449a9f67cdf5605ea5493c4db29","affectsGlobalScope":true,"impliedFormat":1},{"version":"de7ba0fd02e06cd9a5bd4ab441ed0e122735786e67dde1e849cced1cd8b46b78","affectsGlobalScope":true,"impliedFormat":1},{"version":"6148e4e88d720a06855071c3db02069434142a8332cf9c182cda551adedf3156","affectsGlobalScope":true,"impliedFormat":1},{"version":"d63dba625b108316a40c95a4425f8d4294e0deeccfd6c7e59d819efa19e23409","affectsGlobalScope":true,"impliedFormat":1},{"version":"0568d6befee03dd435bed4fc25c4e46865b24bdcb8c563fdc21f580a2c301904","affectsGlobalScope":true,"impliedFormat":1},{"version":"30d62269b05b584741f19a5369852d5d34895aa2ac4fd948956f886d15f9cc0d","affectsGlobalScope":true,"impliedFormat":1},{"version":"f128dae7c44d8f35ee42e0a437000a57c9f06cc04f8b4fb42eebf44954d53dc8","affectsGlobalScope":true,"impliedFormat":1},{"version":"ffbe6d7b295306b2ba88030f65b74c107d8d99bdcf596ea99c62a02f606108b0","affectsGlobalScope":true,"impliedFormat":1},{"version":"996fb27b15277369c68a4ba46ed138b4e9e839a02fb4ec756f7997629242fd9f","affectsGlobalScope":true,"impliedFormat":1},{"version":"79b712591b270d4778c89706ca2cfc56ddb8c3f895840e477388f1710dc5eda9","affectsGlobalScope":true,"impliedFormat":1},{"version":"20884846cef428b992b9bd032e70a4ef88e349263f63aeddf04dda837a7dba26","affectsGlobalScope":true,"impliedFormat":1},{"version":"5fcab789c73a97cd43828ee3cc94a61264cf24d4c44472ce64ced0e0f148bdb2","affectsGlobalScope":true,"impliedFormat":1},{"version":"db59a81f070c1880ad645b2c0275022baa6a0c4f0acdc58d29d349c6efcf0903","affectsGlobalScope":true,"impliedFormat":1},{"version":"673294292640f5722b700e7d814e17aaf7d93f83a48a2c9b38f33cbc940ad8b0","affectsGlobalScope":true,"impliedFormat":1},{"version":"d786b48f934cbca483b3c6d0a798cb43bbb4ada283e76fb22c28e53ae05b9e69","affectsGlobalScope":true,"impliedFormat":1},{"version":"1ecb8e347cb6b2a8927c09b86263663289418df375f5e68e11a0ae683776978f","affectsGlobalScope":true,"impliedFormat":1},{"version":"142efd4ce210576f777dc34df121777be89eda476942d6d6663b03dcb53be3ff","affectsGlobalScope":true,"impliedFormat":1},{"version":"379bc41580c2d774f82e828c70308f24a005b490c25ba34d679d84bcf05c3d9d","affectsGlobalScope":true,"impliedFormat":1},{"version":"ed484fb2aa8a1a23d0277056ec3336e0a0b52f9b8d6a961f338a642faf43235d","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ffedae1d1c2d53fdbca1c96d3c7dda544281f7d262f99b6880634f8fd8d9820","affectsGlobalScope":true,"impliedFormat":1},{"version":"83a730b125d477dd264df8ba479afab27a3dae7152b005c214ab94dc7ee44fd3","affectsGlobalScope":true,"impliedFormat":1},{"version":"1ce14b81c5cc821994aa8ec1d42b220dd41b27fcc06373bce3958af7421b77d4","affectsGlobalScope":true,"impliedFormat":1},{"version":"b3a048b3e9302ef9a34ef4ebb9aecfb28b66abb3bce577206a79fee559c230da","affectsGlobalScope":true,"impliedFormat":1},{"version":"14d5ccd6f427b4d1e74a214f59c55740b2079d032a947a706ba0f07cd5599dcd","impliedFormat":99},{"version":"bdfc0dc1b1e449cc8f9380a1408c001cb233677ea972d82a64f4f2ebb1d983e3","impliedFormat":99},{"version":"9b10c9c23741531ffa1fac6edf95c264b3ba541cbe153a22f1b6a2a884311096","impliedFormat":99},{"version":"8a8994aa440f8e76484161d8c63dc8b3539e49bd679241b9d7703e81a20f5375","impliedFormat":99},{"version":"02c012041e0c4f31c7b962ef807ae283635503aa59332f10b3d24d7b6d658431","impliedFormat":99},{"version":"f3d8c757e148ad968f0d98697987db363070abada5f503da3c06aefd9d4248c1","impliedFormat":1},{"version":"de56b7c4893e7457ba7fcae5e772690fa30e34af12b4424a17ceffbc212a0bbf","impliedFormat":99},{"version":"bdd14f07b4eca0b4b5203b85b8dbc4d084c749fa590bee5ea613e1641dcd3b29","impliedFormat":99},{"version":"d3cfde44f8089768ebb08098c96d01ca260b88bccf238d55eee93f1c620ff5a5","impliedFormat":1},{"version":"293eadad9dead44c6fd1db6de552663c33f215c55a1bfa2802a1bceed88ff0ec","impliedFormat":1},{"version":"d5aefcc5d3381ee4fc8b31672dc34ee8c587c95f17666c43e7b74eda31fb8af2","impliedFormat":1},{"version":"fec412ded391a7239ef58f455278154b62939370309c1fed322293d98c8796a6","impliedFormat":1},{"version":"e3498cf5e428e6c6b9e97bd88736f26d6cf147dedbfa5a8ad3ed8e05e059af8a","impliedFormat":1},{"version":"dba3f34531fd9b1b6e072928b6f885aa4d28dd6789cbd0e93563d43f4b62da53","impliedFormat":1},{"version":"f672c876c1a04a223cf2023b3d91e8a52bb1544c576b81bf64a8fec82be9969c","impliedFormat":1},{"version":"e4b03ddcf8563b1c0aee782a185286ed85a255ce8a30df8453aade2188bbc904","impliedFormat":1},{"version":"2329d90062487e1eaca87b5e06abcbbeeecf80a82f65f949fd332cfcf824b87b","impliedFormat":1},{"version":"25b3f581e12ede11e5739f57a86e8668fbc0124f6649506def306cad2c59d262","impliedFormat":1},{"version":"93c3e73824ad57f98fd23b39335dbdae2db0bd98199b0dc0b9ccc60bf3c5134a","impliedFormat":1},{"version":"a9ebb67d6bbead6044b43714b50dcb77b8f7541ffe803046fdec1714c1eba206","impliedFormat":1},{"version":"833e92c058d033cde3f29a6c7603f517001d1ddd8020bc94d2067a3bc69b2a8e","impliedFormat":1},{"version":"d1fc2d317157b161af800631bb01668bf08307bf2a0ff053f743aeca3a368465","impliedFormat":1},{"version":"835fb2909ce458740fb4a49fc61709896c6864f5ce3db7f0a88f06c720d74d02","impliedFormat":1},{"version":"6e5857f38aa297a859cab4ec891408659218a5a2610cd317b6dcbef9979459cc","impliedFormat":1},{"version":"424d9092b2417b1da7f9ba67c15f42def241e29d304e63003a3e171efe04a40a","impliedFormat":1},{"version":"81c5f2e7e6fb72d442e3c08b49c2c71cefd64c590e0a57c2093b3c7d00dbb13b","impliedFormat":1},{"version":"d26b722c6ef39ad70bcfbe4fe1554d2b7f2573677eb87aa03af85f1959eefc8a","impliedFormat":1},{"version":"adca9ceb697fa6f88e4a40bb88626593fccac0b1b5b0c52557887c27394b4cd5","impliedFormat":1},{"version":"21ae7cca76ec2410452737a20d2b55e46dccc5d64a9cef7f042e3a89e4b99db9","impliedFormat":1},{"version":"c140211fba71b3a847b15da4a70776ed904198e7e4dc8c32df22742235072c4c","impliedFormat":1},{"version":"ce51a3572f75b0f8fc07a11828876ac28cf606a780a2d4c6b3bbcee79e48a90b","impliedFormat":1},{"version":"a8fed1298e1a1aeb2b689c56fe4200c6bf48491cabe47f54a1caa22fa9ebcc6e","impliedFormat":1},{"version":"c7d1e897d7751e2f2f7e41ee6db87a567418a0e9d307c580029e8565a76cb1f8","impliedFormat":1},{"version":"880ec5909ddc50fbf8066064fce83094751261a6f8a4962972210c8682899bca","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"71a40e0d3aadfd1998021f706c164195f5d8d043f1551d4ecee8804d77167a5a","impliedFormat":1},{"version":"55fadfc733db7198f557af11bf55ae955db6257e17262351a9cdb8a98af4a9f9","impliedFormat":1},{"version":"293e5cbd76e48e679f3e1c47d9a7a66872136aac96637ea271f4a2c9adb1e6c1","impliedFormat":1},{"version":"b2b2198d1b8c518151f0eafdbbc56c8deee1e428b500f1822352fbcc5993424d","impliedFormat":1},{"version":"2f67911e4bf4e0717dc2ded248ce2d5e4398d945ee13889a6852c1233ea41508","impliedFormat":1},{"version":"14e1da8984e72f3837de4aec8bc6d29e9ecce4dc260ac6ef4ca4e5e4f93add01","impliedFormat":1},{"version":"99003dbd8309d6cfda98e4bacb264ad40bfc197ede6af95370b0f3177076eb6a","impliedFormat":1},{"version":"9eea3e08092b6790361c3f66a11eba28e2f0a4414b89622e1fb9ed70c8f38ef9","impliedFormat":99},{"version":"958002b3dddffde5cd74a187f93508121f7065e361312e16a8aecd7676bf1a83","impliedFormat":99},{"version":"f336533543f32410a2921bad881736d4768e1b858a11f2c88e74136ea23c29f4","impliedFormat":99},{"version":"cbe003be7407ddac990ed386a0e7572e63288ac437eb6dcd4199decebb4bf54a","impliedFormat":99},{"version":"be7e9393805753e479746c81dcfe4287063d83ffa30e0284f7a016775af2c1d2","impliedFormat":1},{"version":"f49bf9fcde399132c5760eefb0714cee3f8e96cd30fcec8c535e2cee57870d63","impliedFormat":1},{"version":"46e55f334e394f80d5be1e4ce45749f77ab99c99917e6e2caf800840da409673","impliedFormat":1},{"version":"5490cbcd192227f9a1f61bdf7000da8b47635fc0cdec9e1879b932586b6d2e8e","impliedFormat":1},{"version":"487178767be2789fa2f765d0e77fc5abad8212f7d5dfbe255011dd8e8bb5306c","impliedFormat":1},{"version":"2316301dd223d31962d917999acf8e543e0119c5d24ec984c9f22cb23247160c","impliedFormat":1},{"version":"a3eb5de45edf334745bb6347979b6f9eb44bab235910afb551d44eb05a84a233","impliedFormat":1},{"version":"bfa8e3563e561377191b49bcfb4cea149286a2b843058c1236dce8c3b541564d","impliedFormat":1},{"version":"b8495ee8dbff077d31e9365c73f170709106d7bc6e17a163c7568bb485c8368c","impliedFormat":1},{"version":"e818471014c77c103330aee11f00a7a00b37b35500b53ea6f337aefacd6174c9","impliedFormat":1},{"version":"85b982d3d1838e3b768353b9afe0cb051bc763077eaf6f2b72141155e1c5a560","impliedFormat":1},{"version":"628f7309657ea46102ae95ad7ebf168fc9c54495da2d6a3c2834e186543c5cc9","impliedFormat":1},{"version":"51a4d8450070d113bb1758ed730a5862e6e676a279a190f5e622d24a8942357a","impliedFormat":1},{"version":"6bdf5675c990953db3b07288a443e083377e331bef60613d08e9def95ab18c79","affectsGlobalScope":true,"impliedFormat":99},{"version":"0e7ab9c6ce896e4a6de1f387664145bc50d5b899dac667ccdb2bd2755f3e1eec","impliedFormat":99},{"version":"31506fd6e72c0d09e082b2f0b3741e6bbe90670361c9560ef998bdb1e56edc3e","impliedFormat":1},{"version":"9190d984b46642b16203d36360260e31f9ba9e423929c8d25730d55710528acc","impliedFormat":99},{"version":"71ca4d5b499bd98537e9d505fa8e3845eb74c1842289b63d12c03a84ebfd237b","impliedFormat":99},{"version":"7314e2d918cc6adf47c94b0f7b0dc230d2b45d982eed1c32ffef54eaaa37950b","impliedFormat":99},{"version":"35d35b2d52697c363e7818ac96e4d9ed174d1c84a8e7954d218fa9594a2b9529","impliedFormat":99},{"version":"c19139650672cefeeef779cefc1cb75ec753796bec415e29b877b504b6f4832a","impliedFormat":99},{"version":"05d896aba8cb582031542db5d6932bbbb6f52ce68f7cd5e62a9493b9f483267b","impliedFormat":99},{"version":"dc50e95f72a1bad73dde789629527d7e4913871b4336b432d297b6d80b58ddaa","impliedFormat":99},{"version":"c9ac63c635f2a388d8cade82b7ad8adf60a2c736c7580a78dad14696cfea49bf","impliedFormat":99},{"version":"9a7e2b6400b2632d9c36d0949940e34dd2c7599b4c061f432d217215d2ed3d87","impliedFormat":99},{"version":"c2d81004ec850eae05ebc8028267b8fe5a46be9aa0142923d6b03d8b397d50d2","impliedFormat":99},{"version":"77e45ec7bae423ca7f5b1f7121be710ffe24905e165c417c5fc72dbf4168e4cb","impliedFormat":99},{"version":"e06d3f14c40ee0bdaabe660e7ee481ea601a08b4d961d7ede248963186a9fd6b","impliedFormat":99},{"version":"33b3171df0516ba4314598c00c38e7959b0f5484bc983226520760e5d3f36729","impliedFormat":99},{"version":"e8fde93049aa349fce243641c9dc94cd74bab77a9f65c8a261eb6253d3adfca7","impliedFormat":99},{"version":"d0d881e35953f0edcb5eaee09b7214ce2ce334fc8de6a7e4df03341272fb0792","impliedFormat":99},{"version":"a75119d0bb1cba189337f878d7055f7be6e38128c8d35e98023853270d42aa8f","impliedFormat":99},{"version":"3f1f910a90d404d6c9814201db146d0c45215c6f50fb02a965aa5458059671ea","impliedFormat":99},{"version":"c3f9a8563a999c2d6b96c2a1b7478e4fa557d923f783c5c120d81309699b6d86","impliedFormat":99},{"version":"6e1c84a02a1891d96bcfaf0624c3807ae3819383d92eb558ef0ffe52703b1107","impliedFormat":99},{"version":"fa152eeaae934e5b7c422d0c4437ec43c427fb34654602991e6cabf834116ea3","impliedFormat":99},{"version":"d715117d2d3883a181f0d07d0ccb629e5177979d6855f094899d2b6c31170de4","impliedFormat":99},{"version":"d9fa9256df8a6c3393bbafca9f3d9e814b89914b15aaf135e1825f980ed7ae7c","impliedFormat":99},{"version":"62d6c1632fe0b57438a4873d9363c00a445b0448fc83ce766735a062ef762ccc","impliedFormat":99},{"version":"fb893a0dfc3c9fb0f9ca93d0648694dd95f33cbad2c0f2c629f842981dfd4e2e","impliedFormat":1},{"version":"509331448d7ccef32318a8d548918878291ed0f2255fca0930e805bf8bf19be6","impliedFormat":99},{"version":"9418aae55aef3291b49b4f10fcec70e6c408ee7e0ffa6dfbc369fabc513ecf8c","impliedFormat":99},{"version":"9eea3e08092b6790361c3f66a11eba28e2f0a4414b89622e1fb9ed70c8f38ef9","impliedFormat":99},{"version":"93832729ccefebc66c64e5c292291ebe09337940aa45b4713bdb11c50cc397a0","impliedFormat":99},{"version":"525ffaabfda919e6f59b87c67760a1530af36ad1c3ad395182470c2fd5afe56f","impliedFormat":99},{"version":"bfec26ac8033dfa8d9844a5c74e09e24c1b33983833493d77236392240ebb2a2","impliedFormat":99},{"version":"756aeb01bcf0223dee4082074a734e5940fdf825e352136b42776ab465fcd4e9","impliedFormat":99},{"version":"76af14c3cce62da183aaf30375e3a4613109d16c7f16d30702f16d625a95e62c","impliedFormat":99},{"version":"bf17c65172c9ed6d07ed66fc2e6d7b20d3d6c70558eead1d162ffc777ee7cb1c","impliedFormat":99},{"version":"1fc872fbaa4cf102142880e930fef3bd15fc4eda64b7051a3970220c1aa0cbf9","impliedFormat":99},{"version":"cee900b02b756607b8c78bc63d60301e20d5b6786f4a59d22f815f8abbf837ed","impliedFormat":99},{"version":"79cb9dbceece4ab437a14e567be9559d85be50e9061c2851fe1dbc1be6834d40","impliedFormat":99},{"version":"71d24f97510ba111ba24afa56e0c8c89df69b41abd7b2f91434ca50207ff1828","impliedFormat":99},{"version":"0a1151651cfdf3f9f007a2f2345193057381d177ecc5ca9fc045e9a4abdf00ad","impliedFormat":99},{"version":"7b7bf440d84a536b64620c7cd6c67f9bac9dfcad00dc611d645aff2eab51cafd","impliedFormat":99},{"version":"3b296653e90ce6f076bccf2ef341ffe6161312dc2628988079497ca9537f19e7","impliedFormat":99},{"version":"b1d83f765b81540213680f712b71c229e641a4c5860091e03cbae6d8d7dad8af","impliedFormat":99},{"version":"4bc58bac748b1e7e17ce375c80493285d64489e0ac56dd0c208bf0b255a4d115","impliedFormat":99},{"version":"fc9c40ea7e4c4c012c287dadb33a412157d41923101bf4981760d9951ba87c9e","impliedFormat":99},{"version":"0dfa84487aff893e6fabbfef9c35111f5e7217c6d25f888d441fa051d6409c10","impliedFormat":99},{"version":"bdb403571bee96186ee438065b5c2d32f3885ba91b62a40de95bbe1d9515c70d","impliedFormat":99},{"version":"3174cf31006e4b14d6eda31dff8d46c27de0dca785c0c84c28b3ce4e5a81d831","impliedFormat":99},{"version":"6c6fb94432c0a0c6bef4a499b82413a4a5d771a3f74f23494ec5aa732fd0eed6","impliedFormat":99},{"version":"3cb17876a9da1e2f64d18c4695e770d3e64e87f21def7275f73a31823ff53a6d","impliedFormat":99},{"version":"d2990059ad207a630e7f8405cdc1d073e57740f56cdd41fbd637846eb9eb6d89","impliedFormat":99},{"version":"51f6001a1dce773d4ca082f2ac3a82b16bf0a0805310329509953ef83e061773","impliedFormat":99},{"version":"c29c5009d43ca5d4d65613c839160be4ee7af336bc0545ca073438fa1f1d5db4","impliedFormat":99},{"version":"5df906533530ba7cac7ccb5fca48169156562aab531db604c5ee8953401e01b2","impliedFormat":99},{"version":"858c9adfbc9b4b2c4ff24f1834a00412eeb34a9fbed81c2e60e0ceabf4e6c149","impliedFormat":99},{"version":"86d4ff8ba66b5ea1df375fe6092d2b167682ccd5dd0d9b003a7d30d95a0cda32","impliedFormat":99},{"version":"4967529644e391115ca5592184d4b63980569adf60ee685f968fd59ab1557188","impliedFormat":1},{"version":"5929864ce17fba74232584d90cb721a89b7ad277220627cc97054ba15a98ea8f","impliedFormat":1},{"version":"763fe0f42b3d79b440a9b6e51e9ba3f3f91352469c1e4b3b67bfa4ff6352f3f4","impliedFormat":1},{"version":"6c7176368037af28cb72f2392010fa1cef295d6d6744bca8cfb54985f3a18c3e","affectsGlobalScope":true,"impliedFormat":1},{"version":"ab41ef1f2cdafb8df48be20cd969d875602483859dc194e9c97c8a576892c052","affectsGlobalScope":true,"impliedFormat":1},{"version":"437e20f2ba32abaeb7985e0afe0002de1917bc74e949ba585e49feba65da6ca1","affectsGlobalScope":true,"impliedFormat":1},{"version":"21d819c173c0cf7cc3ce57c3276e77fd9a8a01d35a06ad87158781515c9a438a","impliedFormat":1},{"version":"98cffbf06d6bab333473c70a893770dbe990783904002c4f1a960447b4b53dca","affectsGlobalScope":true,"impliedFormat":1},{"version":"3af97acf03cc97de58a3a4bc91f8f616408099bc4233f6d0852e72a8ffb91ac9","affectsGlobalScope":true,"impliedFormat":1},{"version":"808069bba06b6768b62fd22429b53362e7af342da4a236ed2d2e1c89fcca3b4a","affectsGlobalScope":true,"impliedFormat":1},{"version":"1db0b7dca579049ca4193d034d835f6bfe73096c73663e5ef9a0b5779939f3d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"9798340ffb0d067d69b1ae5b32faa17ab31b82466a3fc00d8f2f2df0c8554aaa","affectsGlobalScope":true,"impliedFormat":1},{"version":"f26b11d8d8e4b8028f1c7d618b22274c892e4b0ef5b3678a8ccbad85419aef43","affectsGlobalScope":true,"impliedFormat":1},{"version":"bf67d53d168abc1298888693338cb82854bdb2e69ef83f8a0092093c2d562107","impliedFormat":1},{"version":"2cbe0621042e2a68c7cbce5dfed3906a1862a16a7d496010636cdbdb91341c0f","affectsGlobalScope":true,"impliedFormat":1},{"version":"f9501cc13ce624c72b61f12b3963e84fad210fbdf0ffbc4590e08460a3f04eba","affectsGlobalScope":true,"impliedFormat":1},{"version":"e7721c4f69f93c91360c26a0a84ee885997d748237ef78ef665b153e622b36c1","affectsGlobalScope":true,"impliedFormat":1},{"version":"0fa06ada475b910e2106c98c68b10483dc8811d0c14a8a8dd36efb2672485b29","impliedFormat":1},{"version":"33e5e9aba62c3193d10d1d33ae1fa75c46a1171cf76fef750777377d53b0303f","impliedFormat":1},{"version":"2b06b93fd01bcd49d1a6bd1f9b65ddcae6480b9a86e9061634d6f8e354c1468f","impliedFormat":1},{"version":"6a0cd27e5dc2cfbe039e731cf879d12b0e2dded06d1b1dedad07f7712de0d7f4","affectsGlobalScope":true,"impliedFormat":1},{"version":"13f5c844119c43e51ce777c509267f14d6aaf31eafb2c2b002ca35584cd13b29","impliedFormat":1},{"version":"e60477649d6ad21542bd2dc7e3d9ff6853d0797ba9f689ba2f6653818999c264","impliedFormat":1},{"version":"c2510f124c0293ab80b1777c44d80f812b75612f297b9857406468c0f4dafe29","affectsGlobalScope":true,"impliedFormat":1},{"version":"5524481e56c48ff486f42926778c0a3cce1cc85dc46683b92b1271865bcf015a","impliedFormat":1},{"version":"4c829ab315f57c5442c6667b53769975acbf92003a66aef19bce151987675bd1","affectsGlobalScope":true,"impliedFormat":1},{"version":"b2ade7657e2db96d18315694789eff2ddd3d8aea7215b181f8a0b303277cc579","impliedFormat":1},{"version":"9855e02d837744303391e5623a531734443a5f8e6e8755e018c41d63ad797db2","impliedFormat":1},{"version":"4d631b81fa2f07a0e63a9a143d6a82c25c5f051298651a9b69176ba28930756d","impliedFormat":1},{"version":"836a356aae992ff3c28a0212e3eabcb76dd4b0cc06bcb9607aeef560661b860d","impliedFormat":1},{"version":"1e0d1f8b0adfa0b0330e028c7941b5a98c08b600efe7f14d2d2a00854fb2f393","impliedFormat":1},{"version":"41670ee38943d9cbb4924e436f56fc19ee94232bc96108562de1a734af20dc2c","affectsGlobalScope":true,"impliedFormat":1},{"version":"c906fb15bd2aabc9ed1e3f44eb6a8661199d6c320b3aa196b826121552cb3695","impliedFormat":1},{"version":"22295e8103f1d6d8ea4b5d6211e43421fe4564e34d0dd8e09e520e452d89e659","impliedFormat":1},{"version":"bb45cd435da536500f1d9692a9b49d0c570b763ccbf00473248b777f5c1f353b","impliedFormat":1},{"version":"6b4e081d55ac24fc8a4631d5dd77fe249fa25900abd7d046abb87d90e3b45645","impliedFormat":1},{"version":"a10f0e1854f3316d7ee437b79649e5a6ae3ae14ffe6322b02d4987071a95362e","impliedFormat":1},{"version":"e208f73ef6a980104304b0d2ca5f6bf1b85de6009d2c7e404028b875020fa8f2","impliedFormat":1},{"version":"d163b6bc2372b4f07260747cbc6c0a6405ab3fbcea3852305e98ac43ca59f5bc","impliedFormat":1},{"version":"e6fa9ad47c5f71ff733744a029d1dc472c618de53804eae08ffc243b936f87ff","affectsGlobalScope":true,"impliedFormat":1},{"version":"83e63d6ccf8ec004a3bb6d58b9bb0104f60e002754b1e968024b320730cc5311","impliedFormat":1},{"version":"24826ed94a78d5c64bd857570fdbd96229ad41b5cb654c08d75a9845e3ab7dde","impliedFormat":1},{"version":"45875bcae57270aeb3ebc73a5e3fb4c7b9d91d6b045f107c1d8513c28ece71c0","impliedFormat":1},{"version":"928af3d90454bf656a52a48679f199f64c1435247d6189d1caf4c68f2eaf921f","affectsGlobalScope":true,"impliedFormat":1},{"version":"8579647e66a9f713bcba9746f25dcfc7bcfeb8832130a52cd1bca65c4f342bf8","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f16a7e4deafa527ed9995a772bb380eb7d3c2c0fd4ae178c5263ed18394db2c","impliedFormat":1},{"version":"933921f0bb0ec12ef45d1062a1fc0f27635318f4d294e4d99de9a5493e618ca2","impliedFormat":1},{"version":"71a0f3ad612c123b57239a7749770017ecfe6b66411488000aba83e4546fde25","impliedFormat":1},{"version":"77fbe5eecb6fac4b6242bbf6eebfc43e98ce5ccba8fa44e0ef6a95c945ff4d98","impliedFormat":1},{"version":"4f9d8ca0c417b67b69eeb54c7ca1bedd7b56034bb9bfd27c5d4f3bc4692daca7","impliedFormat":1},{"version":"814118df420c4e38fe5ae1b9a3bafb6e9c2aa40838e528cde908381867be6466","impliedFormat":1},{"version":"a3fc63c0d7b031693f665f5494412ba4b551fe644ededccc0ab5922401079c95","impliedFormat":1},{"version":"f27524f4bef4b6519c604bdb23bf4465bddcccbf3f003abb901acbd0d7404d99","impliedFormat":1},{"version":"37ba7b45141a45ce6e80e66f2a96c8a5ab1bcef0fc2d0f56bb58df96ec67e972","impliedFormat":1},{"version":"45650f47bfb376c8a8ed39d4bcda5902ab899a3150029684ee4c10676d9fbaee","impliedFormat":1},{"version":"dba28a419aec76ed864ef43e5f577a5c99a010c32e5949fe4e17a4d57c58dd11","affectsGlobalScope":true,"impliedFormat":1},{"version":"18fd40412d102c5564136f29735e5d1c3b455b8a37f920da79561f1fde068208","impliedFormat":1},{"version":"c959a391a75be9789b43c8468f71e3fa06488b4d691d5729dde1416dcd38225b","impliedFormat":1},{"version":"f0be1b8078cd549d91f37c30c222c2a187ac1cf981d994fb476a1adc61387b14","affectsGlobalScope":true,"impliedFormat":1},{"version":"0aaed1d72199b01234152f7a60046bc947f1f37d78d182e9ae09c4289e06a592","impliedFormat":1},{"version":"5ebe6f4cc3b803cbfc962bae0d954f9c80e5078ca41eb3f1de41d92e7193ef37","impliedFormat":1},{"version":"66ba1b2c3e3a3644a1011cd530fb444a96b1b2dfe2f5e837a002d41a1a799e60","impliedFormat":1},{"version":"7e514f5b852fdbc166b539fdd1f4e9114f29911592a5eb10a94bb3a13ccac3c4","impliedFormat":1},{"version":"5b7aa3c4c1a5d81b411e8cb302b45507fea9358d3569196b27eb1a27ae3a90ef","affectsGlobalScope":true,"impliedFormat":1},{"version":"5987a903da92c7462e0b35704ce7da94d7fdc4b89a984871c0e2b87a8aae9e69","affectsGlobalScope":true,"impliedFormat":1},{"version":"ea08a0345023ade2b47fbff5a76d0d0ed8bff10bc9d22b83f40858a8e941501c","impliedFormat":1},{"version":"47613031a5a31510831304405af561b0ffaedb734437c595256bb61a90f9311b","impliedFormat":1},{"version":"ae062ce7d9510060c5d7e7952ae379224fb3f8f2dd74e88959878af2057c143b","impliedFormat":1},{"version":"8a1a0d0a4a06a8d278947fcb66bf684f117bf147f89b06e50662d79a53be3e9f","affectsGlobalScope":true,"impliedFormat":1},{"version":"9f663c2f91127ef7024e8ca4b3b4383ff2770e5f826696005de382282794b127","impliedFormat":1},{"version":"9f55299850d4f0921e79b6bf344b47c420ce0f507b9dcf593e532b09ea7eeea1","impliedFormat":1},{"version":"25c8056edf4314820382a5fdb4bb7816999acdcb929c8f75e3f39473b87e85bc","impliedFormat":1},{"version":"c464d66b20788266e5353b48dc4aa6bc0dc4a707276df1e7152ab0c9ae21fad8","impliedFormat":1},{"version":"78d0d27c130d35c60b5e5566c9f1e5be77caf39804636bc1a40133919a949f21","impliedFormat":1},{"version":"c6fd2c5a395f2432786c9cb8deb870b9b0e8ff7e22c029954fabdd692bff6195","impliedFormat":1},{"version":"1d6e127068ea8e104a912e42fc0a110e2aa5a66a356a917a163e8cf9a65e4a75","impliedFormat":1},{"version":"5ded6427296cdf3b9542de4471d2aa8d3983671d4cac0f4bf9c637208d1ced43","impliedFormat":1},{"version":"7f182617db458e98fc18dfb272d40aa2fff3a353c44a89b2c0ccb3937709bfb5","impliedFormat":1},{"version":"cadc8aced301244057c4e7e73fbcae534b0f5b12a37b150d80e5a45aa4bebcbd","impliedFormat":1},{"version":"385aab901643aa54e1c36f5ef3107913b10d1b5bb8cbcd933d4263b80a0d7f20","impliedFormat":1},{"version":"9670d44354bab9d9982eca21945686b5c24a3f893db73c0dae0fd74217a4c219","impliedFormat":1},{"version":"0b8a9268adaf4da35e7fa830c8981cfa22adbbe5b3f6f5ab91f6658899e657a7","impliedFormat":1},{"version":"11396ed8a44c02ab9798b7dca436009f866e8dae3c9c25e8c1fbc396880bf1bb","impliedFormat":1},{"version":"ba7bc87d01492633cb5a0e5da8a4a42a1c86270e7b3d2dea5d156828a84e4882","impliedFormat":1},{"version":"4893a895ea92c85345017a04ed427cbd6a1710453338df26881a6019432febdd","impliedFormat":1},{"version":"c21dc52e277bcfc75fac0436ccb75c204f9e1b3fa5e12729670910639f27343e","impliedFormat":1},{"version":"13f6f39e12b1518c6650bbb220c8985999020fe0f21d818e28f512b7771d00f9","impliedFormat":1},{"version":"9b5369969f6e7175740bf51223112ff209f94ba43ecd3bb09eefff9fd675624a","impliedFormat":1},{"version":"4fe9e626e7164748e8769bbf74b538e09607f07ed17c2f20af8d680ee49fc1da","impliedFormat":1},{"version":"24515859bc0b836719105bb6cc3d68255042a9f02a6022b3187948b204946bd2","impliedFormat":1},{"version":"ea0148f897b45a76544ae179784c95af1bd6721b8610af9ffa467a518a086a43","impliedFormat":1},{"version":"24c6a117721e606c9984335f71711877293a9651e44f59f3d21c1ea0856f9cc9","impliedFormat":1},{"version":"dd3273ead9fbde62a72949c97dbec2247ea08e0c6952e701a483d74ef92d6a17","impliedFormat":1},{"version":"405822be75ad3e4d162e07439bac80c6bcc6dbae1929e179cf467ec0b9ee4e2e","impliedFormat":1},{"version":"0db18c6e78ea846316c012478888f33c11ffadab9efd1cc8bcc12daded7a60b6","impliedFormat":1},{"version":"e61be3f894b41b7baa1fbd6a66893f2579bfad01d208b4ff61daef21493ef0a8","impliedFormat":1},{"version":"bd0532fd6556073727d28da0edfd1736417a3f9f394877b6d5ef6ad88fba1d1a","impliedFormat":1},{"version":"89167d696a849fce5ca508032aabfe901c0868f833a8625d5a9c6e861ef935d2","impliedFormat":1},{"version":"615ba88d0128ed16bf83ef8ccbb6aff05c3ee2db1cc0f89ab50a4939bfc1943f","impliedFormat":1},{"version":"a4d551dbf8746780194d550c88f26cf937caf8d56f102969a110cfaed4b06656","impliedFormat":1},{"version":"8bd86b8e8f6a6aa6c49b71e14c4ffe1211a0e97c80f08d2c8cc98838006e4b88","impliedFormat":1},{"version":"317e63deeb21ac07f3992f5b50cdca8338f10acd4fbb7257ebf56735bf52ab00","impliedFormat":1},{"version":"4732aec92b20fb28c5fe9ad99521fb59974289ed1e45aecb282616202184064f","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"bf67d53d168abc1298888693338cb82854bdb2e69ef83f8a0092093c2d562107","impliedFormat":1},{"version":"cdcf9ea426ad970f96ac930cd176d5c69c6c24eebd9fc580e1572d6c6a88f62c","impliedFormat":1},{"version":"23cd712e2ce083d68afe69224587438e5914b457b8acf87073c22494d706a3d0","impliedFormat":1},{"version":"156a859e21ef3244d13afeeba4e49760a6afa035c149dda52f0c45ea8903b338","impliedFormat":1},{"version":"3ac40516c33b87f751f7507346933081a26cdb8a3e11a6b3aa07d23f803c85db","impliedFormat":1},{"version":"4ac80270b6787c2b77a2d98a9714a71f4363c24b5890314f3ba582c94bfbe779","impliedFormat":1},{"version":"14e9acf826baba0ef4b5665704084896e7bcc06f65a9ab13af7e93d27d6b7069","impliedFormat":1},{"version":"68834d631c8838c715f225509cfc3927913b9cc7a4870460b5b60c8dbdb99baf","impliedFormat":1},{"version":"82e8858179f8b8895db6f005f2152d4eda2273721f52be47e22d48bd84590f95","impliedFormat":1},{"version":"eec76bf6b9346f3f95fa402621b889489e96930e72295b0369022f332e9b4a6a","impliedFormat":1},{"version":"171b96f31e3fbdb55fe570f2a29a5ee47223fdca95a84ea2142e4cc4feaf9dfe","impliedFormat":1},{"version":"ea6bc8de8b59f90a7a3960005fd01988f98fd0784e14bc6922dde2e93305ec7d","impliedFormat":1},{"version":"36107995674b29284a115e21a0618c4c2751b32a8766dd4cb3ba740308b16d59","impliedFormat":1},{"version":"914a0ae30d96d71915fc519ccb4efbf2b62c0ddfb3a3fc6129151076bc01dc60","impliedFormat":1},{"version":"9c32412007b5662fd34a8eb04292fb5314ec370d7016d1c2fb8aa193c807fe22","impliedFormat":1},{"version":"d243db6b25788f439e7e2f03c05688e92f46764351673bb0e7b2f3631232e186","impliedFormat":1},{"version":"4d327f7d72ad0918275cea3eee49a6a8dc8114ae1d5b7f3f5d0774de75f7439a","impliedFormat":1},{"version":"6ebe8ebb8659aaa9d1acbf3710d7dae3e923e97610238b9511c25dc39023a166","impliedFormat":1},{"version":"e85d7f8068f6a26710bff0cc8c0fc5e47f71089c3780fbede05857331d2ddec9","impliedFormat":1},{"version":"7befaf0e76b5671be1d47b77fcc65f2b0aad91cc26529df1904f4a7c46d216e9","impliedFormat":1},{"version":"0a60a292b89ca7218b8616f78e5bbd1c96b87e048849469cccb4355e98af959a","impliedFormat":1},{"version":"0b6e25234b4eec6ed96ab138d96eb70b135690d7dd01f3dd8a8ab291c35a683a","impliedFormat":1},{"version":"9666f2f84b985b62400d2e5ab0adae9ff44de9b2a34803c2c5bd3c8325b17dc0","impliedFormat":1},{"version":"40cd35c95e9cf22cfa5bd84e96408b6fcbca55295f4ff822390abb11afbc3dca","impliedFormat":1},{"version":"b1616b8959bf557feb16369c6124a97a0e74ed6f49d1df73bb4b9ddf68acf3f3","impliedFormat":1},{"version":"5b03a034c72146b61573aab280f295b015b9168470f2df05f6080a2122f9b4df","impliedFormat":1},{"version":"40b463c6766ca1b689bfcc46d26b5e295954f32ad43e37ee6953c0a677e4ae2b","impliedFormat":1},{"version":"249b9cab7f5d628b71308c7d9bb0a808b50b091e640ba3ed6e2d0516f4a8d91d","impliedFormat":1},{"version":"d33ce35e3f9cfcc1d94eca415bdd3bde94d5b153ffdd33e6c4455c029986c630","impliedFormat":1},{"version":"80aae6afc67faa5ac0b32b5b8bc8cc9f7fa299cff15cf09cc2e11fd28c6ae29e","impliedFormat":1},{"version":"f473cd2288991ff3221165dcf73cd5d24da30391f87e85b3dd4d0450c787a391","impliedFormat":1},{"version":"499e5b055a5aba1e1998f7311a6c441a369831c70905cc565ceac93c28083d53","impliedFormat":1},{"version":"8aee8b6d4f9f62cf3776cda1305fb18763e2aade7e13cea5bbe699112df85214","impliedFormat":1},{"version":"98498b101803bb3dde9f76a56e65c14b75db1cc8bec5f4db72be541570f74fc5","impliedFormat":1},{"version":"1d5e86e300d9fdeb1e3e88aa4f24d780f045195fcb2dbc24b68bf49c9f816c5e","impliedFormat":1},{"version":"5d0375ca7310efb77e3ef18d068d53784faf62705e0ad04569597ae0e755c401","impliedFormat":1},{"version":"59af37caec41ecf7b2e76059c9672a49e682c1a2aa6f9d7dc78878f53aa284d6","impliedFormat":1},{"version":"addf417b9eb3f938fddf8d81e96393a165e4be0d4a8b6402292f9c634b1cb00d","impliedFormat":1},{"version":"436d7b4543b340b0f3eef4310d524242e41369b9652aa9c70428767c4dcac455","impliedFormat":1},{"version":"adf27937dba6af9f08a68c5b1d3fce0ca7d4b960c57e6d6c844e7d1a8e53adae","impliedFormat":1},{"version":"12950411eeab8563b349cb7959543d92d8d02c289ed893d78499a19becb5a8cc","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"114f493b30f364255290472111b5a4791d5902c308645670cd0401429cbc6930","impliedFormat":1},{"version":"b3fb72492a07a76f7bfa29ecadd029eea081df11512e4dfe6f930a5a9cb1fb75","impliedFormat":1},{"version":"dbab1950ef4bf06f44795b144026a352a7b4a3a68a969bbf32eb55addd0fb95a","impliedFormat":99},{"version":"2b5368217b57528a60433558585186a925d9842fe64c1262adde8eac5cb8de33","impliedFormat":99},{"version":"e22273698b7aad4352f0eb3c981d510b5cf6b17fde2eeaa5c018bb065d15558f","impliedFormat":99},{"version":"ed9680d6573920c3f1588fdb732d2469324e16b4795e2bec5f196a613e66030f","impliedFormat":99},{"version":"804e73c5236db118192cf774837ecf6d37013470832dc0ed9aaecfb4c93fb88b","impliedFormat":99},{"version":"91c093343733c2c2d40bee28dc793eff3071af0cb53897651f8459ad25ad01da","impliedFormat":99},{"version":"dbf1009687760b708258fef934385cf29eada0feb170521f7b03cb874786bcf5","impliedFormat":99},{"version":"e1c58879ba7cfcb2a70f4ec69831f48eef47b7a356f15ab9f4fce03942d9f21a","impliedFormat":99},{"version":"f4fc36916b3eac2ea0180532b46283808604e4b6ff11e5031494d05aa6661cc6","impliedFormat":99},{"version":"82e23a5d9f36ccdac5322227cd970a545b8c23179f2035388a1524f82f96d8d0","impliedFormat":99},{"version":"c52e8203e4cc8ddd3ffa75197673942e80e3ff4b3bffa962588363e872cb9922","impliedFormat":99},{"version":"bfce32506c0d081212ff9d27ec466fa6135a695ba61d5a02738abd2442566231","impliedFormat":99},{"version":"5ad576e13f58a0a2b5d4818dd13c16ec75b43025a14a89a7f09db3fe56c03d30","impliedFormat":99},{"version":"5668033966c8247576fc316629df131d6175d24ccf22940324c19c159671e1c1","impliedFormat":99},{"version":"493c39c5f9e9c050c10930448fda1be8de10a0d9b34dcd24ff17a1713c282162","impliedFormat":99},{"version":"502f64730d7ca8f4b7eb4f16c5b5cdad4e05c2337b85a5b4dbe7c8ad118dcc11","impliedFormat":99},{"version":"fb5a2c398c5d06e25ae7b12ad15a921f1b980a63fa2a7e4fab133b4e2a812016","impliedFormat":99},{"version":"ba3df48971907e524e144d82ed8f02d79729234b659307f8ea6c53b40821c021","impliedFormat":99},{"version":"dbf3d90c21c08217509df631336881a3105740033b0592dcc47036490f95e51c","impliedFormat":99},{"version":"e6ad9376e7d088ce1dc6d3183ba5f0b3fb67ee586aa824cc8519b52f2341307a","impliedFormat":99},{"version":"50cf14b8f0fc2722c11794ca2a06565b1f29e266491da75c745894960ebbce06","impliedFormat":99},{"version":"d62b09cb6f1ceb87ec6c26f3789bc38f8be9fb0ce3126fd0bf89b003d0cba371","impliedFormat":99},{"version":"e9d27f2b7d5171f512053f153cadc303d1b84d00c98e917664ba68eca9b7af6a","impliedFormat":99},{"version":"4899d2cf406cd68748c5d536b736c90339a39f996945126d8a11355eba5f56f3","impliedFormat":99},{"version":"491d5f012b1de793c45e75a930f5cdef1ff0e7875968e743fa6bd5dd7d31cb3b","impliedFormat":99},{"version":"53c86b81daa463deacb0046fee490b6d589438ac71311050b74dcee99afca0f6","impliedFormat":99},{"version":"70587241a4cc2e08ffc30e60c20f3eb38bd5af7e3d99640568ffe2993f933485","impliedFormat":99},{"version":"dd01943d0fe191b3b2020438367709333ff08a69d285e2f715a60711dcf83b61","impliedFormat":99},{"version":"79f3c89a11ffa63d3010bb80908169cf25716115b67695c18f3e5889c1ab02cb","impliedFormat":99},{"version":"b6ff37737d006b86082f2f7176eb0a771001e9dde9152a26ef9ea8fd80e6eba0","impliedFormat":99},{"version":"29c4e9ce50026f15c4e58637d8668ced90f82ce7605ca2fd7b521667caa4a12c","impliedFormat":99},{"version":"e6dd8526d318cce4cb3e83bef3cb4bf3aa08186ddc984c4663cf7dee221d430e","impliedFormat":99},{"version":"3b56bc74e48ec8704af54db1f6ecfee746297ee344b12e990ba5f406431014c1","impliedFormat":99},{"version":"9e4991da8b398fa3ee9b889b272b4fe3c21e898d873916b89c641c0717caed10","impliedFormat":99},{"version":"cf188ffe69f260ffd23b86c4fa2f3ecc01dda5bde2740629be244204581f7a49","impliedFormat":99},{"version":"7630b6a1c0ebaec2ef8e8abff850e1d6c551c47d1c345340a8ab95667460fc95","impliedFormat":99},{"version":"597b0a9ef02a28f5b1195305ec9f20a4f9948bd90ec3291d0343d1e5c0b4bd16","impliedFormat":99},{"version":"0586d346f71f0ec722d384b2569b7284dff554b55f98118f473079405dc8876b","impliedFormat":99},{"version":"09fe9b15282a073c2cd0ef426704e0baea167c2270fc5c46bc932deee440a071","impliedFormat":99},{"version":"ee02719d72e35d2816bd9052ad2a35f148ac54aa4ffb5d2ad2ef0229a17fc3ae","impliedFormat":99},{"version":"eac029dfd99082efdc6854f4f23932fe54be7eb9bb5debd03c2f6ebd1be502f7","impliedFormat":99},{"version":"39dfbe87a0de5cc88287ba79693e0ae8a560e8a1ed2a62c09d6dfc5250b801e1","impliedFormat":99},{"version":"924abf8e5bf12cc08323ce731f7c8215953755d53fdd509886ef321137b1fdf3","impliedFormat":99},{"version":"af12948563d3973b5f4c9a4ceda63c362758edb8c64412410ebd9c145b85611b","impliedFormat":99},{"version":"4a5d9348012a3e46c03888e71b0d318cda7e7db25869731375f90edad8dcea02","impliedFormat":99},{"version":"741208d80a2a3673e3a5216965ddf220003aba217ffe55448e528b766febaef7","impliedFormat":99},{"version":"1c42336e3f0cb6811c70c4b77ea96487a107d159bd23fba5ca69a947a0da37bd","impliedFormat":99},{"version":"69dbd631e44f63d27e20be0a628e9f1d9578e835c7a8ed77653894d7f15441df","impliedFormat":99},{"version":"fc391876e409d362cc43a7468226a9eb83440de09873b284bf09fbfb261ec259","impliedFormat":99},{"version":"d06f5012d5ac1bc25c5033f7e916fe42cc0253d6b523b9747809b71676069370","impliedFormat":99},{"version":"5d35840bd540fad886e21ddaf9b078a44c21a827dec9abc08d2d2c1a3ff27d44","impliedFormat":99},{"version":"bccef2e4035020788934f608255058fc234b3ccc67bf9b888b7eb1ef3285e521","impliedFormat":99},{"version":"4ecb0eb653de7093f2eb589cea5b35fdea6e2bbd62bc3d9fafdc5702850f7714","impliedFormat":99},{"version":"69ed52603ad6430aaffbc9dec25e0d01df733aaa32ab4d57d37987aedc94c349","impliedFormat":99},{"version":"323420ca2dd68ae9922913d7c5ca44f36b1db0e5d58e4a9316d4121d5da88664","impliedFormat":99},{"version":"679c5d237c8e812fd15c9ff547cf5bb17a5a3214af7632da4768efc163bf036a","impliedFormat":99},{"version":"76ab2326e7eeb1a0609423671abd7260a1302ae535bbc45c8bd251da0bdc40bc","impliedFormat":99},{"version":"584cbaebe5928714465942169a1820461276944ac1e97c2062855b14b498b546","impliedFormat":99},{"version":"1f51b0d1ce3e03cb3d56638437a9658888802b639a46792825e6a73c8eb4e060","impliedFormat":99},{"version":"6def25aee4733ebf8625cc8ec6d71c3461507ad47dbd0b8b0b560feaff72e9bb","impliedFormat":99},{"version":"715d90adc185f1cfd1d3cfa8fbc7547f8867fd4ab0b94bd7f69fb6fc0712332f","impliedFormat":99},{"version":"70afc18e984dede6b32e9829a41f0aecdc1f7c2066a900dbaacc4648ca8a5867","impliedFormat":99},{"version":"7bc50a64c4e4d2a40d413b261a07f0e5acdd875ac9d4de6d014f1938f494fcc0","impliedFormat":99},{"version":"953ee863def1b11f321dcb17a7a91686aa582e69dd4ec370e9e33fbad2adcfd3","impliedFormat":99},{"version":"c6fcf55644bb1ee497dbe1debb485d5478abd8e8f9450c3134d1765bff93d141","impliedFormat":99},{"version":"e452b617664fc3d2db96f64ef3addadb8c1ef275eff7946373528b1d6c86a217","impliedFormat":99},{"version":"434a60088d7096cd59e8002f69e87077c620027103d20cd608a240d13881fba7","impliedFormat":99},{"version":"40d9502a7af4ad95d761c849dd6915c9c295b3049faca2728bff940231ca81d3","impliedFormat":99},{"version":"792d1145b644098c0bb411ffb584075eadcfbbd41d72cd9c85c7835212a71079","impliedFormat":99},{"version":"30d0ecf1c23d75cba9e57457703695a25003c4328f6d048171e91b20d1012aa2","impliedFormat":99},{"version":"f216cb46ebeff3f767183626f70d18242307b2c3aab203841ae1d309277aad6b","impliedFormat":99},{"version":"fa9c695ac6e545d4f8a416fb190e4a5e8c5bc2d23388b83f5ae1b765fff5add5","impliedFormat":99},{"version":"fe69ad9a4b9c61fa429e252aaf63ba4bd330bfd169432de7afbd45a8bf2f50a1","impliedFormat":99},{"version":"f294be0ee8508d25d0ea14b5170a056cae0439a6d555a23d7779e3c5c28430ae","impliedFormat":99},{"version":"99b487d1ed8af24e01c427b9837fd7230366ad661d389dc7f142e1c1c8c33b5e","impliedFormat":99},{"version":"1b38ddf6562131f17f58fccf394cced29b37ecf7b80d2a48cc5da424aeb390c4","impliedFormat":99},{"version":"8d2dc7051c83cee0ca7b6d4628dbb86e7726e114d54e0b9932b62cc5ddda7042","impliedFormat":99},{"version":"8192f4b4564ce64404d41c92cdea3c1ad2c2b4986fade78aa0a742a7f1b766d0","impliedFormat":99},{"version":"a384b0ea68d5a8c2ab6ad5fbd3ce1480e752e153dd23feb03d143e7ecc1ac2c7","impliedFormat":99},{"version":"d6a27acb0d9eaf6af0edc8153cd8702301ae561eb990c4b5fdf662ea14a53ce4","impliedFormat":99},{"version":"afad82addd1d9ee6e361606205bbda03e97cb3850f948e53fdbb82f160dc43c7","impliedFormat":99},{"version":"5ee44a60fe09b4c21f71506f6697107f19a01c9842980c7145a4f2938d4dafc4","impliedFormat":99},{"version":"3729454e7f755d54f08bad759e29cc87453323f90ffcbb3f425c4ede7224cfd3","impliedFormat":99},{"version":"da58347ef36f47a7270f743144760adbc97e88b6ff0be2623bc0de73898f66f6","impliedFormat":99},{"version":"852eb3e7189a7c9c6acf431d5d2f8527e590dca54b3f6a099b558e90bb452b08","impliedFormat":99},{"version":"d952a3349aac3cf317856da1db3afc0fb7d3369a310a4c38d268f532929c6d3d","impliedFormat":99},{"version":"5f9449e0561ba44c0f712f1093341e76543627358ed6d8ec8d9ba7f31ba6d2ea","impliedFormat":99},{"version":"c29d1afafb83246b29734f7e0d960bd52842c160994a1cb38db52a738ab52bad","impliedFormat":99},{"version":"7b6261a4407295b1057feba24a1333923dee852f67fe3c329c990ddcfa20adce","impliedFormat":99},{"version":"f20df3a5bd4e29ee7565a24e302d7f9ab7e71ac6e218720717f72f20695e34e8","impliedFormat":99},{"version":"380b919bfa0516118edaf25b99e45f855e7bc3fd75ce4163a1cfe4a666388804","impliedFormat":1},{"version":"0b24a72109c8dd1b41f94abfe1bb296ba01b3734b8ac632db2c48ffc5dccaf01","impliedFormat":1},{"version":"fcf79300e5257a23ed3bacaa6861d7c645139c6f7ece134d15e6669447e5e6db","impliedFormat":1},{"version":"187119ff4f9553676a884e296089e131e8cc01691c546273b1d0089c3533ce42","impliedFormat":1},{"version":"aa2c18a1b5a086bbcaae10a4efba409cc95ba7287d8cf8f2591b53704fea3dea","impliedFormat":1},{"version":"b88749bdb18fc1398370e33aa72bc4f88274118f4960e61ce26605f9b33c5ba2","impliedFormat":1},{"version":"0aaef8cded245bf5036a7a40b65622dd6c4da71f7a35343112edbe112b348a1e","impliedFormat":1},{"version":"00baffbe8a2f2e4875367479489b5d43b5fc1429ecb4a4cc98cfc3009095f52a","impliedFormat":1},{"version":"a873c50d3e47c21aa09fbe1e2023d9a44efb07cc0cb8c72f418bf301b0771fd3","impliedFormat":1},{"version":"7c14ccd2eaa82619fffc1bfa877eb68a012e9fb723d07ee98db451fadb618906","impliedFormat":1},{"version":"49c36529ee09ea9ce19525af5bb84985ea8e782cb7ee8c493d9e36d027a3d019","impliedFormat":1},{"version":"df996e25faa505f85aeb294d15ebe61b399cf1d1e49959cdfaf2cc0815c203f9","impliedFormat":1},{"version":"4f6a12044ee6f458db11964153830abbc499e73d065c51c329ec97407f4b13dd","impliedFormat":1},{"version":"a6a2ed5ba265ec3f2b119ae52b53d3235c4574fcb6f4407220c46814bc2c67e4","impliedFormat":1},{"version":"f67042a1a63935b23f1067ca734f6ac3c7ddf0ee484c5a33fffb0e87d042a235","impliedFormat":99},{"version":"eaf815d73c54d11e3e44fc7b724a5a8a28d70fcd823bf0427ff1d1517006cbf8","impliedFormat":99},{"version":"a089794ce5278f94b496773efccad2cfb51bd51eba55cc2586a1b5ea28e27aef","impliedFormat":99},{"version":"f4d1981b9803c429ae9dc1f8abf555b4024935ae45dc1ae67db8c337c0bcf90f","impliedFormat":99},{"version":"87c9536fabee8c5c5dd06c008eb4a8a487fce8988c89ff853ecb29b93eeef5b8","impliedFormat":99},{"version":"34913745cac2a3d9976abc5cb54bc08e15d9a258d9bd73ffe94fe72f4a6c509a","impliedFormat":99},{"version":"7e61c29172a5ccad8dd15233eb3de2307bdc6e1656958370c2baf89497caae7b","impliedFormat":99},{"version":"2231cee47957cde5f1747fa7a3acd64f87345c87fda0dd5ff6b7e35f7458bb46","impliedFormat":99},{"version":"f210253286d561e6bac9a41f8387133e8f2d62e3e42e074524c16794958e6ae8","impliedFormat":99},{"version":"1680eb0661d5eaa73d79a3cdf51154417cf137c382b034a7241b675496031a08","impliedFormat":99},{"version":"4f7da12ef64d40f273adeb503a3996d2210f8eea3008f655add89383943e9b5c","impliedFormat":99},{"version":"63d742ab455f1aba292073952fa70d636fdd998ea313f0a68164f912d03331be","impliedFormat":99},{"version":"79625168b6860d75b17c2b6037571f26758e89740619b4ebb7331356856330f8","impliedFormat":99},{"version":"94c363dc60940ba8ff678b742c85451cf8a78e4da83af8f0326a1f0c3f6d274e","impliedFormat":99},{"version":"c00e799972c83b2265dbfca0a660659443e67e8610f8b8b4e97be299adfc3add","impliedFormat":99},{"version":"35ca8f074201dd3d142e727ff5baefb65679c4f2d140e62d136c294ef034ca38","impliedFormat":99},{"version":"6a96237a01e444f83d068e3668f6114bf2a32e6725bf96abfdaf01adf10ef6d1","impliedFormat":99},{"version":"ae06faef379813a712d23df97fd4ddebff7c04334b3faf6d8b3ab8599936285e","impliedFormat":99},{"version":"35c85224c37455959ac15cfd2bb2fa6a7c63a41532379dd8cb43e6a341e48d3b","impliedFormat":99},{"version":"52dfaa6b30c53ece0958ed16e6d580a5deffd4d008f5e79b4b374e17e47d0ba9","impliedFormat":99},{"version":"2f914dad1e731faa80fad8c3becab4db3e77d78ab87720a6d009de2e8e7536d8","impliedFormat":99},{"version":"a241bac10bdbf27a8499d0d0c8cd72c9e20bc50e1502d08dfa62fa85e4651cec","impliedFormat":99},{"version":"4450507742c53f51d411ecefbf891527f149bfce177ebbae62b99ddbd7925532","impliedFormat":99},{"version":"46f3315d746e223e8eb5e2c24938adc3dd844111416e9a663f3d2d0d0de48cad","impliedFormat":99},{"version":"b5e9e17d062de12c93b08712759f73e109283bcbd535e967693a14b0c9c95237","impliedFormat":99},{"version":"ab0bfca8c34e7f018158f88c6b05d1b306146a2cf4af064bbfba2f352c883455","impliedFormat":99},{"version":"de2f673742dcca68de3bfe4910549dbfc6ed1322f205de544e5fa337202bead2","impliedFormat":99},{"version":"83145010d8ef1a047bbb1a3401974db304e61af8130de635d93fb5e6b1a94e63","impliedFormat":99},{"version":"06748b0ba63d881a7e9d8a4c233bb4f66466873fdcafea1714a86dd27dee0336","impliedFormat":99},{"version":"c92b432d85b5c569eef906864c7f550aa0219c4795a3a8a0669e097c7cd10229","impliedFormat":99},{"version":"1e4f2f359d1878672b809e2433869997be9e869996fc75244d75adc6abe12370","impliedFormat":99},{"version":"827d7f1c9e70bd39a4e98b9790242e96d4a1e78c2c1a90d0344c3a60ea70c8e3","impliedFormat":99},{"version":"bc28ff904cb0831e032ff539e4d887bd138fae46db74bafef8d98bf09fc6a956","impliedFormat":99},{"version":"f404566ecbd497a5b1ed0a9d73541fac7248fdc7b0de5b866e8a48f3d456235a","impliedFormat":99},{"version":"0cd85d1c5d3348c5299987a23e3af0a70e256b8c0c8d61ce894700e035e8745e","impliedFormat":99},{"version":"ec09e8ef678ad14b6f18cff428fb406f8e20ff6ac530c7c32a06032835c5abcd","impliedFormat":99},{"version":"e414ff213326360728b6a51437f5793778e79c0836b5589b02b0cc03c98110bd","impliedFormat":99},{"version":"5d3840233566489df97ca750fd6051cfec3864c00fcd318746d2c5aaf6a7001b","impliedFormat":99},{"version":"6724ee3a9b90af2e655f48f6084d8016eaeafa790321bafbfc8542fab0c232a7","impliedFormat":99}],"root":[[91,94],[258,265],506,521,522,[550,552],[554,559]],"options":{"allowJs":true,"esModuleInterop":true,"jsx":1,"module":199,"skipLibCheck":true,"strict":true,"target":9},"referencedMap":[[196,1],[195,2],[256,3],[96,4],[211,5],[244,3],[97,3],[95,3],[520,6],[508,7],[509,8],[507,9],[510,10],[511,11],[512,12],[513,13],[514,14],[515,15],[516,16],[517,17],[518,18],[519,19],[236,3],[284,20],[285,20],[286,21],[275,22],[287,23],[288,24],[289,25],[270,3],[273,26],[271,3],[272,3],[290,27],[291,28],[292,29],[293,30],[294,31],[295,32],[296,32],[297,33],[298,34],[299,35],[300,36],[276,3],[274,3],[301,37],[302,38],[303,39],[337,40],[304,41],[305,3],[306,42],[307,43],[308,44],[309,45],[310,46],[311,47],[312,48],[313,49],[314,50],[315,50],[316,51],[317,3],[318,52],[319,53],[321,54],[320,55],[322,56],[323,57],[324,58],[325,59],[326,60],[327,61],[328,62],[329,63],[330,64],[331,65],[332,66],[333,67],[334,68],[277,3],[278,3],[279,3],[281,69],[282,3],[283,3],[335,70],[336,71],[90,3],[193,3],[210,72],[257,73],[267,3],[541,3],[194,3],[351,74],[361,75],[350,74],[371,76],[342,77],[341,78],[370,79],[364,80],[369,81],[344,82],[358,83],[343,84],[367,85],[339,86],[338,79],[368,87],[340,88],[345,89],[346,3],[349,89],[268,3],[372,90],[362,91],[353,92],[354,93],[356,94],[352,95],[355,96],[365,79],[347,97],[348,98],[357,99],[269,100],[360,91],[359,89],[363,3],[366,101],[416,102],[391,103],[404,104],[388,105],[405,100],[414,106],[379,107],[380,108],[378,78],[413,79],[408,109],[412,110],[382,111],[401,112],[381,113],[411,114],[376,115],[377,116],[383,117],[384,3],[390,118],[387,117],[374,119],[415,120],[406,121],[394,122],[393,117],[395,123],[398,124],[392,125],[396,126],[409,79],[385,127],[386,128],[399,129],[375,130],[403,131],[402,117],[389,128],[397,132],[400,133],[407,3],[373,3],[410,134],[504,135],[503,136],[426,137],[423,3],[427,138],[431,139],[420,140],[430,141],[437,142],[505,143],[266,3],[418,3],[425,144],[421,145],[419,56],[429,146],[417,147],[428,148],[422,149],[439,150],[461,151],[450,152],[440,153],[447,154],[438,155],[448,3],[446,156],[442,157],[443,158],[441,159],[449,160],[424,161],[457,162],[454,163],[455,164],[456,165],[458,166],[464,167],[468,168],[467,169],[465,163],[466,163],[459,170],[462,171],[460,172],[463,173],[452,174],[436,175],[451,176],[435,177],[434,178],[453,179],[433,180],[471,181],[469,163],[470,182],[473,183],[472,184],[474,163],[478,185],[476,186],[477,187],[479,188],[482,189],[481,190],[484,191],[483,192],[487,193],[485,194],[486,195],[480,196],[475,197],[488,196],[489,198],[502,199],[490,192],[491,163],[492,200],[493,201],[494,202],[444,203],[445,204],[432,3],[495,205],[496,206],[499,207],[498,208],[500,209],[501,210],[497,211],[88,3],[89,3],[14,3],[15,3],[17,3],[16,3],[2,3],[18,3],[19,3],[20,3],[21,3],[22,3],[23,3],[24,3],[25,3],[3,3],[26,3],[27,3],[4,3],[28,3],[32,3],[29,3],[30,3],[31,3],[33,3],[34,3],[35,3],[5,3],[36,3],[37,3],[38,3],[39,3],[6,3],[43,3],[40,3],[41,3],[42,3],[44,3],[7,3],[45,3],[50,3],[51,3],[46,3],[47,3],[48,3],[49,3],[8,3],[55,3],[52,3],[53,3],[54,3],[56,3],[9,3],[57,3],[58,3],[59,3],[61,3],[60,3],[62,3],[63,3],[10,3],[64,3],[65,3],[66,3],[11,3],[67,3],[68,3],[69,3],[70,3],[71,3],[72,3],[12,3],[73,3],[74,3],[75,3],[76,3],[77,3],[1,3],[78,3],[79,3],[13,3],[80,3],[81,3],[82,3],[83,3],[84,3],[85,3],[86,3],[87,3],[280,90],[212,212],[102,213],[109,214],[104,3],[105,3],[103,215],[106,216],[98,3],[99,3],[110,217],[101,218],[107,3],[108,219],[100,220],[200,221],[206,222],[201,222],[203,222],[197,221],[207,223],[202,224],[204,222],[208,212],[205,222],[198,225],[199,226],[189,227],[118,228],[188,229],[120,230],[187,3],[119,231],[192,232],[191,233],[190,234],[111,3],[122,235],[121,228],[123,3],[113,229],[117,236],[112,3],[114,237],[115,238],[116,3],[209,239],[124,240],[125,240],[126,240],[127,240],[128,240],[129,240],[130,240],[131,240],[132,240],[133,240],[134,240],[135,240],[136,240],[137,240],[138,240],[139,240],[141,240],[140,240],[142,240],[143,240],[144,240],[145,240],[146,240],[147,240],[148,240],[186,241],[149,240],[150,240],[151,240],[152,240],[153,240],[154,240],[155,240],[156,240],[157,240],[158,240],[159,240],[160,240],[161,240],[162,240],[163,240],[164,240],[166,240],[165,240],[168,240],[167,240],[169,240],[170,240],[171,240],[172,240],[173,240],[174,240],[175,240],[176,240],[177,240],[178,240],[179,240],[180,240],[181,240],[182,240],[185,240],[183,240],[184,240],[243,242],[245,243],[524,244],[537,245],[523,246],[253,247],[547,248],[545,249],[527,250],[548,251],[544,252],[241,253],[240,254],[546,255],[235,256],[234,254],[242,3],[525,257],[543,258],[538,3],[526,259],[540,260],[542,261],[539,262],[247,263],[248,264],[249,3],[251,265],[252,3],[250,3],[246,3],[237,3],[238,266],[239,3],[528,3],[529,3],[536,267],[530,3],[531,3],[532,3],[533,264],[534,3],[535,3],[549,268],[255,269],[553,262],[254,3],[214,270],[222,271],[223,272],[224,3],[220,3],[221,273],[216,274],[233,275],[226,276],[227,3],[228,3],[225,277],[229,278],[217,274],[215,279],[213,279],[230,3],[231,3],[218,274],[232,279],[219,279],[93,280],[91,281],[94,3],[259,282],[260,283],[261,3],[262,283],[263,283],[264,3],[265,284],[506,285],[521,286],[522,283],[550,287],[551,3],[552,3],[92,3],[554,288],[555,3],[556,283],[258,289],[557,3],[558,3],[559,3]],"semanticDiagnosticsPerFile":[[260,[{"start":84,"length":23,"messageText":"Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './_plain_module_hooks.js'?","category":1,"code":2835}]],[550,[{"start":3116,"length":20,"messageText":"Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './97_bench_cadence.js'?","category":1,"code":2835},{"start":3420,"length":16,"messageText":"Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './97_bench_rtt.js'?","category":1,"code":2835},{"start":26388,"length":1,"messageText":"Parameter 'v' implicitly has an 'any' type.","category":1,"code":7006}]],[554,[{"start":192,"length":34,"messageText":"Cannot find module '@repo/lib/steps/paths-alias-test' or its corresponding type declarations.","category":1,"code":2307},{"start":627,"length":23,"messageText":"Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './_imported_step_only.js'?","category":1,"code":2835},{"start":706,"length":11,"messageText":"Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './helpers.js'?","category":1,"code":2835},{"start":58966,"length":15,"messageText":"Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './serde-steps.js'?","category":1,"code":2835}]],[559,[{"start":335,"length":16,"messageText":"Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './serde-models.js'?","category":1,"code":2835}]]],"affectedFilesPendingEmit":[93,91,94,259,260,261,262,263,264,265,506,521,522,550,551,552,92,554,555,556,258,557,558,559],"version":"6.0.3"} \ No newline at end of file From d3d2bda930cbec2d907f3b30cd86d22af0163a13 Mon Sep 17 00:00:00 2001 From: Alex Langenfeld Date: Thu, 1 Oct 2026 16:06:14 -0500 Subject: [PATCH 08/11] docs: lead the dynamic workflows recipe with independent publication; reject invalid parent links Signed-off-by: Alex Langenfeld --- .../cookbook/advanced/dynamic-workflows.mdx | 514 +++++++++++------- .../core/e2e/e2e-dynamic-workflow.test.ts | 204 ++++++- workbench/example/workflows/99_e2e.ts | 114 +++- 3 files changed, 609 insertions(+), 223 deletions(-) diff --git a/docs/content/docs/v5/cookbook/advanced/dynamic-workflows.mdx b/docs/content/docs/v5/cookbook/advanced/dynamic-workflows.mdx index ebcb8828c4..4d6d8f4cd6 100644 --- a/docs/content/docs/v5/cookbook/advanced/dynamic-workflows.mdx +++ b/docs/content/docs/v5/cookbook/advanced/dynamic-workflows.mdx @@ -1,8 +1,8 @@ --- title: Dynamic Workflows -description: Run a customer-specific migration procedure, written and approved after deploy, as a durable workflow over a fixed catalog of deployed steps. +description: Publish a reviewed, customer-specific migration procedure after the platform deploys, and run it durably over a fixed catalog of deployed steps. type: guide -summary: Deploy a migration step catalog once, then start reviewed JavaScript procedures for individual customers with start(), resume their approvals, and read back the records they changed, without a rebuild or redeploy. +summary: Deploy a catalog of migration steps once, then start reviewed JavaScript procedures with start(source, [input], { experimental_dynamic: { steps } }), pause them for approvals, and read back the records they changed, without an application release per procedure. prerequisites: - /docs/advanced/dynamic-workflows related: @@ -13,12 +13,19 @@ related: --- -A platform that imports customers' legacy CRM data usually needs nothing more than a field mapping. Then a customer arrives whose export only makes sense under their own conventions. Acme groups accounts by contract lineage, so a regional account must be loaded after the account it reports to. The same person appears once per region. Two account rows on one contract are one account, and when their owners disagree, a person has to choose. +Most CRM imports should be a static workflow with a field mapping, and branching, loops, and variable fan-out do not require dynamic workflows either. This recipe is for a narrower situation: an integration team needs to publish a newly reviewed migration procedure **after the platform has deployed**, so that onboarding a customer does not wait for an application release. -An integration engineer, perhaps with an agent drafting it, writes those rules as a short JavaScript program over the platform's deployed migration steps. A reviewer approves it, and the platform runs it with [`start()`](/docs/api-reference/workflow-api/start) as a [dynamic workflow](/docs/advanced/dynamic-workflows). The program is published after the platform deployed, and running it needs no rebuild or redeploy. It survives pauses for approvals and restarts like any workflow, and it applies the same approved rules to every record. +The platform already knows how to read an export, stage supported destination changes, ask for approval, and apply them. What varies from customer to customer is policy: which rows are the same account, when two contacts are one person, and which records must exist before others can refer to them. A trusted engineer writes that policy as a short JavaScript procedure over the deployed steps. An agent may help draft it, but running it needs no ongoing model judgment: a reviewer approves the procedure, and every run executes exactly those rules, pausing only for the human decisions the procedure asks for. The SDK stores each run's compiled procedure, so the run [replays the code it started with](/docs/advanced/dynamic-workflows#how-the-code-is-stored). + +```text +Deploy once: migration steps + start and review endpoints +Publish later: reviewed JavaScript procedure + the step names it binds +Start a run: start(source, [input], { experimental_dynamic: { steps } }) +Execute: deployed steps -> pause for approval -> remaining work +``` Dynamic workflows are **experimental**. Before this recipe runs: @@ -30,39 +37,278 @@ Dynamic workflows are **experimental**. Before this recipe runs: See [Enabling dynamic workflows](/docs/advanced/dynamic-workflows#enabling-dynamic-workflows) and [World support](/docs/advanced/dynamic-workflows#world-support) for the details. -## When to use this +## When a separate procedure is worth it -- **Use static configuration for routine imports.** If a customer's import is "column `acct_name` goes to `name`", keep a static workflow that reads a mapping. It needs no review of code and has better types and errors. -- **Use a procedure when the rules are an algorithm.** Acme's grouping, identity, and dependency order take loops, maps, and a traversal. You could encode them in JSON, but then the platform owns an interpreter for that JSON. -- **Use an agent to write the procedure, not to run it.** A durable agent would reconsider every record. Here an agent can help draft the procedure, a person approves it, and the workflow executes exactly those rules for every record, every time. +- **Use inputs or configuration** when deployed code already expresses the policy, such as a field mapping or a choice between strategies you already ship. +- **Use a JSON plan** when your existing plan vocabulary still describes the rules clearly. Keep it rather than replacing it with code. +- **Use dynamic source** when trusted authors need to release new orchestration independently of application deployments, and growing a configuration language to cover each customer would cost more than reviewing a short program. +- **Execute an approved procedure** when the rules can be decided up front. Use an agent at runtime when judgment is part of the work itself. A procedure is trusted application code with your deployment's privileges, not a sandboxed customer script. Only run source approved under your application-code review process; a fixed catalog of steps is an integration contract, not a security boundary. See [Security](/docs/advanced/dynamic-workflows#security). -## The deployed catalog +## What the procedure has to do + +The importing customer is a distributor. Its CRM export lists the organizations it sells to: Acme Holdings with two regional offices, Globex, and Initech, which was entered twice. The distributor's conventions turn the export into destination records like this: + +| In the distributor's export | In the destination | +| --- | --- | +| Acme East and Acme West, both reporting to Acme Holdings | Acme Holdings is written first, then its offices point at it | +| Ada Park on Acme East as `ada@acme.com`, and on Acme West as ` ADA@acme.com` | One contact, linked to both offices | +| `info@initech.com` on Initech, and on Globex | Two contacts: the same address is a different desk at each organization | +| Initech (owner kim) and Initech Ltd (owner sam), both on contract `C-300` | One account, with the owner a reviewer chooses | +| An account that reports to itself, or to an account in another organization | Nothing for that organization; the reason is reported | + +A field mapping produces one destination record per row, so it cannot express any of these; they need a program. Whether that program is deployed application code or a procedure published later is the choice described above. The procedure groups accounts into **families**, one per contract root (`C-100`, `C-100/E`, and `C-100/W` are family `c-100`), and each organization in the export becomes one family. -The platform deploys a fixed set of migration steps once. Steps do the I/O: they read the legacy export and write to the destination. The procedure decides what to write and in what order, and reaches the steps only through `steps.()`. +## Deploy once, publish later + +The platform deploys a fixed catalog of migration steps. Steps do the I/O; the procedure decides what to write and in what order, and reaches the steps only through `steps.()`: + +| Step | What it does | +| --- | --- | +| `readSourcePage({ dataset, cursor })` | Returns `{ records, nextCursor }` from the customer's export. | +| `stageChanges({ migrationId, destinationId, groupKey, operations, dependsOn? })` | Validates the operations and returns an immutable plan: `{ planId, …, conflicts }`. | +| `requestApproval({ planId, token, summary })` | Asks a reviewer to resume `token` with `{ approved, owner? }`. | +| `applyChanges({ plan, idempotencyKey })` | Applies a staged plan to the destination named in the plan. | +| `verifyChanges({ destinationId, migrationId, results })` | Returns integrity invariants and the resulting records. | +| `publishReport({ migrationId, results })` | Stores a summary and returns `{ reportId }`. | + +Each step is an ordinary step function that wraps an integration adapter: + +```typescript title="workflows/migration-steps.ts" lineNumbers +declare const destinations: { apply(request: { plan: unknown; idempotencyKey: string }): Promise }; // @setup + +export async function applyChanges(request: { plan: unknown; idempotencyKey: string }) { + "use step"; + return await destinations.apply(request); +} +``` + +The destination adapter, not the procedure, owns the contract every procedure relies on: + +- **Supported operations only:** `createAccount`, `createContact`, `linkContact`, `setParent`, `setOwner`, `markFinal`, `mergeAccount`, and `mergeContact`. No step evaluates code. +- **Referential integrity:** every parent and contact link must resolve to a record created earlier in the plan or by a plan in `dependsOn`, and parent links must not form a cycle. +- **Immutable plans:** `planId` is a hash of the plan's migration, destination, group, operations, and dependencies, and `applyChanges` takes the destination from the plan, so an approved plan cannot be pointed at another destination. +- **Conflicts block writes:** merging two accounts whose owners differ records a conflict, and `applyChanges` refuses a plan that has one. +- **Idempotency:** the key is `"apply:" + planId`. Repeating it returns the first result; reusing it for a different payload is rejected. + + +This is an adapter contract, not a migration service. The tests behind this recipe use a pure simulated destination, so be precise about what they show: + +- A run replays the results of steps that **completed and were recorded**. A write interrupted before its result was recorded runs again, and only destination-side idempotency makes that safe. The simulation keeps no idempotency ledger, so it does not test that. +- The simulated `requestApproval` notifies nobody; the tests resume the hook directly. +- The tests pause runs on hooks and resume them, but do not restart processes or redeploy mid-run. +- The simulated `verifyChanges` checks this recipe's identity policy (one person per email within a family), so the tests do not show that the catalog supports arbitrary identity algorithms. + + +After review, a release job stores the procedure's source, and the step names it binds, wherever the platform keeps procedures. To start one, bind those names to the **imported** step functions and pass the stored source to `start()`: + +```typescript title="lib/start-procedure.ts" lineNumbers +import { start } from "workflow/api"; +import { MIGRATION_CATALOG } from "@/lib/migration-catalog"; + +export async function startProcedure( + procedure: { source: string; steps: Record }, + input: { dataset: string; destinationId: string; migrationId: string; tokenPrefix: string } +) { + const steps = Object.fromEntries( + Object.entries(procedure.steps).map(([alias, name]) => [ + alias, + MIGRATION_CATALOG[name as keyof typeof MIGRATION_CATALOG], + ]) + ); + return await start(procedure.source, [input], { // [!code highlight] + experimental_dynamic: { steps }, // [!code highlight] + }); // [!code highlight] +} +``` + +The build stamps a `stepId` on each imported step function, and that ID is what binds `steps.applyChanges` in the source to this deployment's step. Every parameter, including the dataset, the destination, and the token prefix, arrives as `input`; the source is never templated, so what runs is exactly what was reviewed. The [complete example](#complete-example) has the catalog, the routes, and the checks to make before starting a stored procedure. + +## The approved procedure + +The distributor's procedure, `crm-parent-first`, validates the export's parent links, then migrates each valid family as its own plan, all families at once. Only a family whose owners disagree waits for a person. This excerpt shows the policy and the approval; the [complete source](#procedure-a-source) adds the validation and the operation building: + +{/* @skip-typecheck: excerpt of dynamic workflow source */} +```javascript title="procedures/crm-parent-first.js (excerpt)" lineNumbers + // This customer's rules: a family is every account under one contract root, + // accounts on the same contract are one account, and a contact is one person + // per normalized email within a family. + const families = new Map(); + for (const row of accounts) { + families.set(familyOf(row), [...(families.get(familyOf(row)) || []), row]); + } + + // familyOperations(members, owner) writes each family parent-first: see the complete source. + + async function migrateFamily(family, members) { + const stage = (owner) => + steps.stageChanges({ ...scope, groupKey: family, operations: familyOperations(members, owner) }); + let plan = await stage(); + if (plan.conflicts.length > 0) { + // Only this family waits for a person; the others carry on. + const token = input.tokenPrefix + ":" + family; + const review = createHook({ token }); + try { + const owners = plan.conflicts.flatMap((conflict) => conflict.values); + await steps.requestApproval({ planId: plan.planId, token, summary: family + ": choose an owner from " + owners.join(", ") }); + const decision = await review; + if (!decision.approved) return { family, status: "skipped" }; + plan = await stage(decision.owner || owners[0]); + } finally { + review.dispose(); + } + } + const result = await steps.applyChanges({ plan, idempotencyKey: "apply:" + plan.planId }); + return { family, status: "applied", result }; + } + + const outcomes = await Promise.all([...families].map(([family, members]) => migrateFamily(family, members))); +``` + +- **Families run concurrently.** `Promise.all` migrates every family at once, and each family is its own plan, so one family's pause does not hold back another's writes. +- **Policy stays in the source.** The family rule, the one-account-per-contract rule, and the one-person-per-email rule are all here; the catalog knows none of them. +- **Conflicts come from the adapter, decisions from the source.** Merging Initech and Initech Ltd yields an owner conflict on the plan. The procedure decides that a conflict needs a person, and restages the plan with the reviewer's owner, because a staged plan never changes. +- **Review before waiting.** The hook is created before `requestApproval` sends its token, so a fast reviewer cannot miss it, and it is disposed once the review settles, so a late reply is refused. A rejected family is skipped. +- **Invalid input is rejected, not guessed at.** Before any of this, the procedure walks each account's parent chain. A family with a self-parent, a cycle, a missing parent, or a parent in another family is reported with its reasons and gets no plan at all, while the other families proceed. + +A reviewer resumes the hook from a route that rebuilds the token from the run's `tokenPrefix` and the family key, after authorizing the caller; see [the review route](#review-route). + +## What survives a pause, and what changed + +While Initech waits, its run is suspended on the hook and uses no compute, and the other two families are already applied: Acme's three accounts with Ada linked to both offices, and Globex. When the review arrives, the run replays its stored procedure and the recorded results of the steps that completed, so nothing already read or written is redone. That holds across a restart or a later deploy, because the run stays on the deployment that started it. + +After the reviewer picks `sam` as Initech's owner, the destination holds: + +| Account | Parent | Owner | From export rows | +| --- | --- | --- | --- | +| Acme Holdings | none | dana | `acct-acme` | +| Acme East | Acme Holdings | dana | `acct-acme-east` | +| Acme West | Acme Holdings | dana | `acct-acme-west` | +| Globex | none | lee | `acct-globex` | +| Initech | none | sam | `acct-initech`, `acct-initech-ltd` | + +| Contact | Linked to | From export rows | +| --- | --- | --- | +| Ada Park, `ada@acme.com` | Acme East, Acme West | `ct-ada-east`, `ct-ada-west` | +| Initech front desk, `info@initech.com` | Initech | `ct-info-initech` | +| Globex reseller desk, `info@initech.com` | Globex | `ct-info-globex` | + +`verifyChanges` reports no dangling links, no provisional accounts, and no duplicate contacts. + +## Another procedure over the same catalog + +A second team prefers to see everything land before anything is reconciled. Their procedure, `crm-provisional-first`, binds the same steps and is published later, still without a deploy. It is structured differently: it writes every valid row as it is, stages one consolidation that depends on that load, and asks for a single review of the whole change. + +If these two strategies are the only ones you will ever need, a static workflow with a `mode` option is the simpler choice. B is here to show that a procedure written later fits the catalog the platform already deployed, not that configuration could not express it. This excerpt shows its shape, including how the approved consolidation is applied and the destination is checked; the complete source is in [`e2e-dynamic-workflow.test.ts`](https://github.com/vercel/workflow/blob/main/packages/core/e2e/e2e-dynamic-workflow.test.ts) as `PROVISIONAL_FIRST_SOURCE`: + +{/* @skip-typecheck: excerpt of dynamic workflow source */} +```javascript title="procedures/crm-provisional-first.js (excerpt)" lineNumbers + // 1. Land every valid row as written: provisional, flat, one record per row. + const load = await steps.stageChanges({ + ...scope, + groupKey: "load", + operations: [ + ...accounts.map((row) => ({ op: "createAccount", key: row.id, name: row.name, owner: row.owner, status: "provisional", sources: [row.id] })), + ...contacts.flatMap((row) => [ + { op: "createContact", key: row.id, name: row.name, email: row.email.trim().toLowerCase(), sources: [row.id] }, + { op: "linkContact", contact: row.id, account: row.accountId }, + ]), + ], + }); + const loaded = await steps.applyChanges({ plan: load, idempotencyKey: "apply:" + load.planId }); + + // 2. Reconcile the landed records with this customer's rules, as one change: + // setParent for every child, mergeAccount for rows on the same contract, + // mergeContact for one person per email within a family, markFinal for + // every surviving account. + function consolidation(owner) { /* see the complete source */ } + const stage = (owner) => + steps.stageChanges({ ...scope, groupKey: "consolidation", operations: consolidation(owner), dependsOn: [load] }); + let plan = await stage(); + + // 3. One review for the whole consolidation, not one per family. + const token = input.tokenPrefix + ":consolidation"; + const review = createHook({ token }); + let decision; + try { + const owners = plan.conflicts.flatMap((conflict) => conflict.values); + await steps.requestApproval({ planId: plan.planId, token, summary: plan.operations.length + " changes; owners to choose from: " + owners.join(", ") }); + decision = await review; + if (decision.approved && plan.conflicts.length > 0) plan = await stage(decision.owner || owners[0]); + } finally { + review.dispose(); + } + + // 4. Apply only what was approved, then check the destination either way. + const results = [loaded]; + if (decision.approved) { + results.push(await steps.applyChanges({ plan, idempotencyKey: "apply:" + plan.planId })); + } + const verification = await steps.verifyChanges({ ...scope, results }); +``` + +The two procedures converge on the same records, but get there differently: + +| | `crm-parent-first` | `crm-provisional-first` | +| --- | --- | --- | +| First writes | Final records, one family at a time | Every valid row, provisional and flat | +| What waits for a person | Only the family whose owners disagree | The single consolidation of everything | +| Integrity during the run | Parents exist before children | Hierarchy and merges arrive in one plan that depends on the load | +| If the review is rejected | That family is skipped; the others are final | Records stay provisional, and `verifyChanges` reports `ok: false` | +| Invalid parent links | That family is rejected before staging | The same families are rejected before the load | + +For an export with thousands of families, start one run per family, or per batch of families, from a static parent workflow and fan back in through hooks, as in [Child Workflows](/cookbook/advanced/child-workflows). + +## Production checklist + +### Adapter obligations + +The simulated destination stands in for code you have to write. A real adapter must store idempotency keys so an interrupted `applyChanges` can be retried safely, enforce referential integrity and acyclic parents itself rather than trusting procedures to, check that every plan in `dependsOn` was applied to the same destination, and actually notify reviewers. + +### Procedure revisions + +Store procedures as **immutable revisions** under a stable procedure ID: the exact source, the step names it binds, the catalog version it was reviewed against, and the approval record. Publishing adds a revision; nothing edits one in place. Record the revision each run started from next to its run ID. + +Keep the procedure ID separate from the [workflow ID](/docs/advanced/dynamic-workflows#workflow-ids). The SDK derives the workflow ID from a hash of the raw source bytes and the step bindings, so reformatting a procedure or editing a comment gives its runs a new workflow ID; the procedure ID stays stable across revisions. + +### Checks at publish and start + +Restrict publishing to operators. At publish, check what your platform owns: the approval record exists, every bound name is in the catalog, the catalog version is one this deployment serves, and the source is under 128 KB. Check again when a run starts, because the store can change in between; [Revision checks](#revision-checks) has an implementation. Be precise about what these checks prove: + +- **They are not a review.** Nothing here can tell whether a procedure does what its approver thinks it does, and neither can the syntax validation in `start()`. +- **A hash stored with the source detects accidents, not tampering.** It catches a source that changed without it, such as a partial write or a hand edit, but not a writer who changes both or invents an approval record. `approvedBy` is a field the publisher supplied. +- **Trust comes from who can publish and who can write to the store.** To verify provenance at start independently of the store, sign each revision at publish with a key the store's writers do not hold, and check the signature at start. + +### Runs in flight + +- **In-flight runs are neither upgraded nor revoked.** Publishing a new revision changes what the next start runs. To stop a run, [cancel it](/docs/api-reference/workflow-api/get-run#cancel-a-run). +- **A run needs its deployment until it finishes.** A dynamic run executes only on the deployment that started it, against that deployment's steps. Keep a deployment available while its migrations wait on reviews, and bump the catalog version when a step changes in a way an existing procedure would notice. + +### Limits + +- At most 128 KB of source per procedure. +- On Vercel, a run's execution context is limited to **2,048 bytes of JSON**, and the procedure's plaintext metadata counts against it: the source hash, the export name, and every bound name with its full step ID. Treat any count of bindings as an estimate. The Local World does not enforce this limit, so try a procedure with many bindings on a preview deployment. + +See [Rules for the source](/docs/advanced/dynamic-workflows#rules-for-the-source) for the rest. + +## Complete example + +### Catalog ```typescript title="workflows/migration-steps.ts" lineNumbers declare const legacyCrm: { page(dataset: string, cursor: string | null): Promise<{ records: unknown[]; nextCursor: string | null }> }; // @setup -declare const destinations: { lookup(r: any): Promise; stage(r: any): Promise; apply(r: any): Promise; verify(r: any): Promise }; // @setup +declare const destinations: { stage(r: any): Promise; apply(r: any): Promise; verify(r: any): Promise }; // @setup declare function notifyReviewer(r: any): Promise; // @setup declare function saveReport(r: any): Promise; // @setup -/** One page of the customer's legacy export. */ export async function readSourcePage(request: { dataset: string; cursor: string | null }) { "use step"; return await legacyCrm.page(request.dataset, request.cursor); } -/** Which of these keys the destination already has. */ -export async function lookupDestination(request: { destinationId: string; keys: string[] }) { - "use step"; - return await destinations.lookup(request); -} - -/** Validates operations and returns an immutable plan with a content-derived `planId`. */ export async function stageChanges(request: { migrationId: string; destinationId: string; @@ -74,20 +320,17 @@ export async function stageChanges(request: { return await destinations.stage(request); } -/** Asks a reviewer to resume `token` with `{ approved, owner? }`. */ export async function requestApproval(request: { planId: string; token: string; summary: string }) { "use step"; await notifyReviewer(request); return { planId: request.planId, token: request.token }; } -/** Applies a staged plan to the destination named in the plan. */ export async function applyChanges(request: { plan: unknown; idempotencyKey: string }) { "use step"; return await destinations.apply(request); } -/** Integrity invariants and the resulting records. */ export async function verifyChanges(request: { destinationId: string; migrationId: string; results: unknown[] }) { "use step"; return await destinations.verify(request); @@ -99,20 +342,9 @@ export async function publishReport(request: { migrationId: string; results: unk } ``` -`legacyCrm` and `destinations` stand for your integration adapters. The destination adapter, not the procedure, owns the contract every procedure relies on: - -- **Supported operations only:** `createAccount`, `createContact`, `linkContact`, `setParent`, `setOwner`, `markFinal`, `mergeAccount`, and `mergeContact`. There is no step that evaluates arbitrary code. -- **Referential integrity:** every parent and contact link must resolve when the plan is applied, to a record created earlier in the plan or by a plan listed in `dependsOn`. -- **Immutable plans:** `planId` is a hash of the plan's migration, destination, group, operations, and dependencies, and `applyChanges` takes the destination from the plan, so a caller cannot point an approved plan at another destination. -- **Conflicts block writes:** merging two accounts whose owners differ records a conflict on the plan, and `applyChanges` refuses a plan with conflicts. -- **Idempotency:** the key is `"apply:" + planId`. Repeating it returns the first result; reusing it for a different payload is rejected. - -The steps are bound under their own names when a procedure starts: - ```typescript title="lib/migration-catalog.ts" lineNumbers import { applyChanges, - lookupDestination, publishReport, readSourcePage, requestApproval, @@ -122,7 +354,6 @@ import { export const MIGRATION_CATALOG = { readSourcePage, - lookupDestination, stageChanges, requestApproval, applyChanges, @@ -131,9 +362,7 @@ export const MIGRATION_CATALOG = { }; ``` -## Procedure A: parent-first - -This is Acme's procedure. It is a file in the integration team's repository, not part of the deployment. It migrates each account family as its own plan, all families at once, and only the family whose owners disagree waits for a person: +### Procedure A source {/* @skip-typecheck: dynamic workflow source, evaluated with injected steps and createHook */} ```javascript title="procedures/crm-parent-first.js" lineNumbers @@ -149,24 +378,53 @@ async function workflow(input) { cursor = page.nextCursor; } while (cursor !== null); + // A family whose parent links cannot be ordered is rejected; the rest proceed. + const familyOf = (row) => row.contract.split("/")[0].toLowerCase(); + const byId = new Map(rows.filter((row) => row.kind === "account").map((row) => [row.id, row])); + const reasons = new Map(); + const reject = (row, reason) => reasons.set(familyOf(row), [...(reasons.get(familyOf(row)) || []), reason]); + for (const row of byId.values()) { + if (row.parentId === null) continue; + const parent = byId.get(row.parentId); + if (row.parentId === row.id) reject(row, row.id + " is its own parent"); + else if (!parent) reject(row, row.id + ": parent " + row.parentId + " is not in the export"); + else if (familyOf(parent) !== familyOf(row)) reject(row, row.id + ": parent " + row.parentId + " is in another company"); + else { + const path = [row.id]; + for (let at = parent; at; at = byId.get(at.parentId)) { + if (at.id === row.id) { + reject(row, "cycle: " + [...path, row.id].join(" → ")); + break; + } + if (path.includes(at.id)) break; + path.push(at.id); + } + } + } + const rejectedRecords = rows + .filter((row) => row.kind === "contact" && !byId.has(row.accountId)) + .map((row) => ({ id: row.id, reason: row.id + ": account " + row.accountId + " is not in the export" })); + const accounts = [...byId.values()].filter((row) => !reasons.has(familyOf(row))); + const contacts = rows.filter( + (row) => row.kind === "contact" && byId.has(row.accountId) && !reasons.has(familyOf(byId.get(row.accountId))) + ); + const rejected = [...reasons].map(([family, why]) => ({ family, status: "rejected", reasons: why })); + // This customer's rules: a family is every account under one contract root, // accounts on the same contract are one account, and a contact is one person // per normalized email within a family. - const accounts = rows.filter((row) => row.kind === "account"); - const contacts = rows.filter((row) => row.kind === "contact"); const families = new Map(); for (const row of accounts) { - const family = row.contract.split("/")[0].toLowerCase(); - families.set(family, [...(families.get(family) || []), row]); + families.set(familyOf(row), [...(families.get(familyOf(row)) || []), row]); } function familyOperations(members, owner) { // Parent-first: an account is written only after the account it reports to. + // Validation above guarantees every parent is in the family and acyclic. const ordered = []; const visit = (row) => { if (ordered.includes(row)) return; - const parent = members.find((other) => other.id === row.parentId); - if (parent) visit(parent); + if (row.parentId !== null) visit(byId.get(row.parentId)); ordered.push(row); }; members.forEach(visit); @@ -235,30 +493,17 @@ async function workflow(input) { ...scope, results: outcomes.filter((outcome) => outcome.result).map((outcome) => outcome.result), }); - const summary = outcomes.map(({ family, status }) => ({ family, status })); - const report = await steps.publishReport({ migrationId: input.migrationId, results: { families: summary, verification } }); - return { families: summary, verification, report }; + const summary = [...outcomes.map(({ family, status }) => ({ family, status })), ...rejected]; + const report = await steps.publishReport({ migrationId: input.migrationId, results: { families: summary, rejectedRecords, verification } }); + return { families: summary, rejectedRecords, verification, report }; } ``` -The procedure owns the customer's policy, and nothing in the catalog knows about it: - -- **Families.** Accounts are grouped by the root of their contract number, so `C-100`, `C-100/E`, and `C-100/W` are one family, keyed `c-100`. Each family becomes its own plan, and `Promise.all` runs them concurrently. -- **Parent-first order.** Within a family, an account is written only after the account it reports to. The destination checks that every parent resolves, so writing a child first would fail the plan. -- **One account per contract.** Two rows on the same contract become one account through `mergeAccount`. If their owners differ, the plan comes back with a conflict, and the procedure asks a person. -- **One person per email within a family.** Ada Park appears once per region with differently cased emails, and becomes one contact linked to both regions. `info@initech.com` appears in two families, so it stays two contacts. -- **Review before waiting.** The approval hook is created before `requestApproval` sends its token, so a fast reviewer can never miss it, and it is disposed once the review settles. A rejected family is skipped; the others are applied either way. - -Every parameter, including the dataset, the destination, and the token prefix, arrives as `input`. The source is never templated, so what runs is exactly the file that was reviewed. - -## Publish and start it - -Nothing about this procedure existed when the platform deployed. After the procedure is reviewed and merged in the integration repository, a release job stores the file and its step names wherever the platform keeps procedures, keyed by a stable procedure ID such as `crm-parent-first`. A route then starts it on the running deployment: +### Start route ```typescript title="app/api/migrations/route.ts" lineNumbers -import { start } from "workflow/api"; import { z } from "zod"; -import { MIGRATION_CATALOG } from "@/lib/migration-catalog"; +import { startProcedure } from "@/lib/start-procedure"; declare const procedures: { latest(id: string): Promise<{ source: string; steps: Record } | null> }; // @setup declare function assertOperator(request: Request): Promise; // @setup @@ -280,40 +525,21 @@ export async function POST(request: Request) { return Response.json({ error: "Unknown procedure" }, { status: 404 }); } - // Bind each name the procedure calls to the imported step function. - const steps = Object.fromEntries( - Object.entries(procedure.steps).map(([alias, name]) => [ - alias, - MIGRATION_CATALOG[name as keyof typeof MIGRATION_CATALOG], - ]) - ); - const id = crypto.randomUUID(); const migrationId = `migration-${id}`; - const run = await start(procedure.source, [{ // [!code highlight] + const run = await startProcedure(procedure, { dataset: parsed.data.dataset, destinationId: parsed.data.destinationId, migrationId, tokenPrefix: `migration:${id}`, - }], { experimental_dynamic: { steps } }); // [!code highlight] - + }); return Response.json({ runId: run.runId, migrationId }); } ``` -The step map holds the **imported** step functions: the build stamps a `stepId` on each one, and that ID is what binds `steps.applyChanges` in the source to this deployment's step. See [Operating it](#operating-it) for what to check before starting a stored procedure. +Read a run's status and result with [`getRun()`](/docs/api-reference/workflow-api/get-run); its return value holds the verification and the report ID. -The whole sequence is: - -1. Deploy the catalog and these routes once. -2. Write and review a procedure, then publish it to the store. -3. `POST /api/migrations` with the procedure ID, the dataset, and the destination. -4. A reviewer answers any approval it requests. -5. Read the result with [`getRun()`](/docs/api-reference/workflow-api/get-run); the run's return value holds the verification and the report ID. - -## Review an owner conflict - -Initech's two rows share contract `C-300` but list different owners, so only that family waits. Its token is the run's `tokenPrefix` plus the family key, the lowercased contract root (`c-300`). The review route resumes it after authorizing the reviewer: +### Review route ```typescript title="app/api/migrations/[runId]/reviews/route.ts" lineNumbers import { getHookByToken, resumeHook } from "workflow/api"; @@ -361,99 +587,7 @@ export async function POST( A review for a family that never conflicted, or one that arrives after the review settled, gets a 404: that hook never existed, or the procedure already disposed it. See [Authorize before calling `resumeHook()`](/docs/foundations/hooks#authorize-before-calling-resumehook). -## What survives a pause, and what changed - -While Initech waits, the run is suspended on its hook, using no compute, and the other two families are already applied: Acme's three accounts with Ada linked to both regions, and Globex. A restart in between does not matter, and neither does a new deploy: the run stays on the deployment that started it, and when the review arrives it replays its stored procedure and recorded step results instead of re-reading or re-writing anything. - -After the reviewer picks `sam` as Initech's owner, the destination holds: - -| Account | Parent | Owner | From legacy rows | -| --- | --- | --- | --- | -| Acme Holdings | none | dana | `acct-acme` | -| Acme East | Acme Holdings | dana | `acct-acme-east` | -| Acme West | Acme Holdings | dana | `acct-acme-west` | -| Globex | none | lee | `acct-globex` | -| Initech | none | sam | `acct-initech`, `acct-initech-ltd` | - -| Contact | Linked to | From legacy rows | -| --- | --- | --- | -| Ada Park, `ada@acme.com` | Acme East, Acme West | `ct-ada-east`, `ct-ada-west` | -| Initech front desk, `info@initech.com` | Initech | `ct-info-initech` | -| Globex reseller desk, `info@initech.com` | Globex | `ct-info-globex` | - -`verifyChanges` reports no dangling links, no provisional accounts, and no duplicate contacts. A field mapping could not produce this: it would create six accounts and four contacts, merge nothing, and either link the two `info@initech.com` rows together or keep Ada twice. - -## Procedure B: provisional first - -Another customer team prefers to see everything land before anything is reconciled. Procedure B runs over the same catalog and is published later, without a deploy. It is structured differently: instead of building each family carefully and pausing only the ambiguous one, it writes every row as it is, then stages one consolidation over all of it, and asks for one review of the whole change: - -{/* @skip-typecheck: excerpt of dynamic workflow source */} -```javascript title="procedures/crm-provisional-first.js (excerpt)" lineNumbers - // 1. Land every row as written: provisional, flat, one record per row. - const load = await steps.stageChanges({ - ...scope, - groupKey: "load", - operations: [ - ...accounts.map((row) => ({ op: "createAccount", key: row.id, name: row.name, owner: row.owner, status: "provisional", sources: [row.id] })), - ...contacts.flatMap((row) => [ - { op: "createContact", key: row.id, name: row.name, email: row.email.trim().toLowerCase(), sources: [row.id] }, - { op: "linkContact", contact: row.id, account: row.accountId }, - ]), - ], - }); - const loaded = await steps.applyChanges({ plan: load, idempotencyKey: "apply:" + load.planId }); - - // 2. Reconcile the landed records with this customer's rules, as one change: - // setParent for every child, mergeAccount for rows on the same contract, - // mergeContact for one person per email within a family, markFinal for - // every surviving account. - function consolidation(owner) { /* ... */ } - const stage = (owner) => - steps.stageChanges({ ...scope, groupKey: "consolidation", operations: consolidation(owner), dependsOn: [load] }); - let plan = await stage(); - - // 3. One review for the whole consolidation, not one per family. - const token = input.tokenPrefix + ":consolidation"; - const review = createHook({ token }); - let decision; - try { - const owners = plan.conflicts.flatMap((conflict) => conflict.values); - await steps.requestApproval({ planId: plan.planId, token, summary: plan.operations.length + " changes; owners to choose from: " + owners.join(", ") }); - decision = await review; - if (decision.approved && plan.conflicts.length > 0) plan = await stage(decision.owner || owners[0]); - } finally { - review.dispose(); - } -``` - -The two procedures converge on the same records shown above, but they get there differently: - -| | Procedure A: parent-first | Procedure B: provisional first | -| --- | --- | --- | -| First writes | Final records, one family at a time | Every row, provisional and flat | -| What waits for a person | Only the family whose owners disagree | The single consolidation of everything | -| Integrity during the run | Parents exist before children | Hierarchy and merges arrive in one plan that depends on the load | -| If the review is rejected | That family is skipped; the others are final | Records stay provisional, and `verifyChanges` reports them | - -Neither is the right migration algorithm in general. They encode two customers' conventions, which is why each is a procedure rather than a platform feature. - -## Many families - -A procedure that runs every family in one run keeps the whole migration in one event log. For an export with thousands of families, start one run per family, or per batch of families, from a static parent workflow, and fan back in through hooks as in [Child Workflows](/cookbook/advanced/child-workflows). Each child is the same procedure with a narrower `input`. - -## Operating it - -The sections above leave out how procedures are stored and checked. None of this changes the API, but a platform that runs procedures for customers needs it. - -### Procedure revisions - -Store procedures as **immutable revisions** under a stable procedure ID: the exact source, the step names it binds, the catalog version it was reviewed against, and the approval record. Publish adds a revision; nothing edits one in place. Record the revision a run started from next to its run ID. - -Keep the procedure ID separate from the [workflow ID](/docs/advanced/dynamic-workflows#workflow-ids). The SDK derives the workflow ID from a hash of the raw source bytes and the step bindings, so reformatting a procedure or editing a comment gives its runs a new workflow ID; the procedure ID stays stable across revisions. - -### Checks at publish and start - -Restrict publishing to operators, and at publish check what your platform owns: the approval record exists, every bound name is in the catalog, the catalog version is one this deployment serves, and the source is under 128 KB. Check again when a run starts, because the store can change in between: +### Revision checks ```typescript title="lib/bind-revision.ts" lineNumbers import { FatalError } from "workflow"; @@ -496,32 +630,8 @@ export async function bindRevision(revision: ProcedureRevision) { } ``` -Be precise about what these checks prove: - -- **They are not a review.** Nothing here can tell whether a procedure does what its approver thinks it does. Syntax validation in `start()` is not a review either. -- **The hash detects accidents, not tampering.** It is stored next to the source it describes, so it catches a source that changed without it, such as a partial write or a hand edit, but not a writer who changes both or invents an approval record. `approvedBy` is a field the publisher supplied. -- **Trust comes from who can publish and who can write to the store.** To verify provenance at start independently of the store, sign each revision at publish with a key the store's writers do not hold, and check the signature here. - The checks throw [`FatalError`](/docs/api-reference/workflow/fatal-error) so that a refused revision fails at once instead of retrying when a step calls them. -### Runs in flight - -A dynamic run stores its compiled code when it starts, and every replay runs that stored code: - -- **In-flight runs are neither upgraded nor revoked.** Publishing a new revision changes what the next start runs. To stop a run, [cancel it](/docs/api-reference/workflow-api/get-run#cancel-a-run). -- **A run needs its deployment until it finishes.** A dynamic run executes only on the deployment that started it, against that deployment's steps. Keep a deployment available while its migrations wait on reviews, and bump the catalog version when a step changes in a way an existing procedure would notice. - -### Limits - -- **Source size.** At most 128 KB of source per procedure. -- **Execution context.** On Vercel, a run's execution context is limited to **2,048 bytes of JSON**, and the procedure's plaintext metadata counts against it: the source hash, the export name, and every bound name with its full step ID. Treat any count of bindings as an estimate; a start that exceeds the limit throws before a run is created. The Local World does not enforce this limit, so try a procedure with many bindings on a preview deployment. - -See [Rules for the source](/docs/advanced/dynamic-workflows#rules-for-the-source) for the full list. - -## Testing it - -The e2e tests behind this recipe run both procedures against a synthetic `synthetic-crm-v1` dataset and a **pure simulated destination**: each plan's records are derived from the plan and the plans it depends on, and `verifyChanges` reconciles the results it is given. That shows the workflow's guarantees, including replay of recorded step results across a pause. It does not show destination-side deduplication when a write's outcome is unknown, which needs a destination that stores idempotency keys; test that against your real adapter. - ## Key APIs - [`start()`](/docs/api-reference/workflow-api/start): Starts a run from workflow source with `experimental_dynamic.steps`. diff --git a/packages/core/e2e/e2e-dynamic-workflow.test.ts b/packages/core/e2e/e2e-dynamic-workflow.test.ts index d621a6c36e..5c00839be0 100644 --- a/packages/core/e2e/e2e-dynamic-workflow.test.ts +++ b/packages/core/e2e/e2e-dynamic-workflow.test.ts @@ -793,24 +793,53 @@ async function workflow(input) { cursor = page.nextCursor; } while (cursor !== null); + // A family whose parent links cannot be ordered is rejected; the rest proceed. + const familyOf = (row) => row.contract.split("/")[0].toLowerCase(); + const byId = new Map(rows.filter((row) => row.kind === "account").map((row) => [row.id, row])); + const reasons = new Map(); + const reject = (row, reason) => reasons.set(familyOf(row), [...(reasons.get(familyOf(row)) || []), reason]); + for (const row of byId.values()) { + if (row.parentId === null) continue; + const parent = byId.get(row.parentId); + if (row.parentId === row.id) reject(row, row.id + " is its own parent"); + else if (!parent) reject(row, row.id + ": parent " + row.parentId + " is not in the export"); + else if (familyOf(parent) !== familyOf(row)) reject(row, row.id + ": parent " + row.parentId + " is in another company"); + else { + const path = [row.id]; + for (let at = parent; at; at = byId.get(at.parentId)) { + if (at.id === row.id) { + reject(row, "cycle: " + [...path, row.id].join(" → ")); + break; + } + if (path.includes(at.id)) break; + path.push(at.id); + } + } + } + const rejectedRecords = rows + .filter((row) => row.kind === "contact" && !byId.has(row.accountId)) + .map((row) => ({ id: row.id, reason: row.id + ": account " + row.accountId + " is not in the export" })); + const accounts = [...byId.values()].filter((row) => !reasons.has(familyOf(row))); + const contacts = rows.filter( + (row) => row.kind === "contact" && byId.has(row.accountId) && !reasons.has(familyOf(byId.get(row.accountId))) + ); + const rejected = [...reasons].map(([family, why]) => ({ family, status: "rejected", reasons: why })); + // This customer's rules: a family is every account under one contract root, // accounts on the same contract are one account, and a contact is one person // per normalized email within a family. - const accounts = rows.filter((row) => row.kind === "account"); - const contacts = rows.filter((row) => row.kind === "contact"); const families = new Map(); for (const row of accounts) { - const family = row.contract.split("/")[0].toLowerCase(); - families.set(family, [...(families.get(family) || []), row]); + families.set(familyOf(row), [...(families.get(familyOf(row)) || []), row]); } function familyOperations(members, owner) { // Parent-first: an account is written only after the account it reports to. + // Validation above guarantees every parent is in the family and acyclic. const ordered = []; const visit = (row) => { if (ordered.includes(row)) return; - const parent = members.find((other) => other.id === row.parentId); - if (parent) visit(parent); + if (row.parentId !== null) visit(byId.get(row.parentId)); ordered.push(row); }; members.forEach(visit); @@ -879,9 +908,9 @@ async function workflow(input) { ...scope, results: outcomes.filter((outcome) => outcome.result).map((outcome) => outcome.result), }); - const summary = outcomes.map(({ family, status }) => ({ family, status })); - const report = await steps.publishReport({ migrationId: input.migrationId, results: { families: summary, verification } }); - return { families: summary, verification, report }; + const summary = [...outcomes.map(({ family, status }) => ({ family, status })), ...rejected]; + const report = await steps.publishReport({ migrationId: input.migrationId, results: { families: summary, rejectedRecords, verification } }); + return { families: summary, rejectedRecords, verification, report }; } `; @@ -898,10 +927,40 @@ async function workflow(input) { rows.push(...page.records); cursor = page.nextCursor; } while (cursor !== null); - const accounts = rows.filter((row) => row.kind === "account"); - const contacts = rows.filter((row) => row.kind === "contact"); - // 1. Land every row as written: provisional, flat, one record per row. + // A family whose parent links cannot be ordered is rejected; the rest proceed. + const familyOf = (row) => row.contract.split("/")[0].toLowerCase(); + const byId = new Map(rows.filter((row) => row.kind === "account").map((row) => [row.id, row])); + const reasons = new Map(); + const reject = (row, reason) => reasons.set(familyOf(row), [...(reasons.get(familyOf(row)) || []), reason]); + for (const row of byId.values()) { + if (row.parentId === null) continue; + const parent = byId.get(row.parentId); + if (row.parentId === row.id) reject(row, row.id + " is its own parent"); + else if (!parent) reject(row, row.id + ": parent " + row.parentId + " is not in the export"); + else if (familyOf(parent) !== familyOf(row)) reject(row, row.id + ": parent " + row.parentId + " is in another company"); + else { + const path = [row.id]; + for (let at = parent; at; at = byId.get(at.parentId)) { + if (at.id === row.id) { + reject(row, "cycle: " + [...path, row.id].join(" → ")); + break; + } + if (path.includes(at.id)) break; + path.push(at.id); + } + } + } + const rejectedRecords = rows + .filter((row) => row.kind === "contact" && !byId.has(row.accountId)) + .map((row) => ({ id: row.id, reason: row.id + ": account " + row.accountId + " is not in the export" })); + const accounts = [...byId.values()].filter((row) => !reasons.has(familyOf(row))); + const contacts = rows.filter( + (row) => row.kind === "contact" && byId.has(row.accountId) && !reasons.has(familyOf(byId.get(row.accountId))) + ); + const rejected = [...reasons].map(([family, why]) => ({ family, status: "rejected", reasons: why })); + + // 1. Land every valid row as written: provisional, flat, one record per row. const load = await steps.stageChanges({ ...scope, groupKey: "load", @@ -933,8 +992,7 @@ async function workflow(input) { } const people = new Map(); for (const row of contacts) { - const holder = accounts.find((account) => account.id === row.accountId); - const identity = holder.contract.split("/")[0] + ":" + row.email.trim().toLowerCase(); + const identity = familyOf(byId.get(row.accountId)) + ":" + row.email.trim().toLowerCase(); const person = people.get(identity); if (person) ops.push({ op: "mergeContact", from: row.id, into: person }); else people.set(identity, row.id); @@ -946,7 +1004,7 @@ async function workflow(input) { steps.stageChanges({ ...scope, groupKey: "consolidation", operations: consolidation(owner), dependsOn: [load] }); let plan = await stage(); - // One review for the whole consolidation, not one per family. + // 3. One review for the whole consolidation, not one per family. const token = input.tokenPrefix + ":consolidation"; const review = createHook({ token }); let decision; @@ -959,13 +1017,14 @@ async function workflow(input) { review.dispose(); } + // 4. Apply only what was approved, then check the destination either way. const results = [loaded]; if (decision.approved) { results.push(await steps.applyChanges({ plan, idempotencyKey: "apply:" + plan.planId })); } const verification = await steps.verifyChanges({ ...scope, results }); - const report = await steps.publishReport({ migrationId: input.migrationId, results: { approved: decision.approved, verification } }); - return { approved: decision.approved, stages: results.map((result) => result.applied), verification, report }; + const report = await steps.publishReport({ migrationId: input.migrationId, results: { approved: decision.approved, rejected, rejectedRecords, verification } }); + return { approved: decision.approved, rejected, rejectedRecords, verification, report }; } `; @@ -984,6 +1043,24 @@ async function workflow(input) { } `; +/** Stages a parent cycle directly, which the adapter must refuse on its own. */ +const PARENT_CYCLE_SOURCE = ` +async function workflow(input) { + "use workflow"; + return await steps.stageChanges({ + migrationId: input.migrationId, + destinationId: input.destinationId, + groupKey: "cycle", + operations: [ + { op: "createAccount", key: "acct-a", name: "A", owner: "dana", sources: [] }, + { op: "createAccount", key: "acct-b", name: "B", owner: "dana", sources: [] }, + { op: "setParent", account: "acct-a", parent: "acct-b" }, + { op: "setParent", account: "acct-b", parent: "acct-a" }, + ], + }); +} +`; + const MIGRATION_CATALOG_STEPS = Object.fromEntries( [ 'readSourcePage', @@ -1084,6 +1161,40 @@ const MIGRATED_CONTACTS = [ { key: 'ct-info-initech', accounts: ['acct-initech'] }, ]; +/** Every family in `synthetic-crm-invalid`, in export order. */ +const INVALID_PARENT_FAMILIES = [ + { family: 'c-100', status: 'applied' }, + { + family: 'c-400', + status: 'rejected', + reasons: ['acct-self is its own parent'], + }, + { + family: 'c-500', + status: 'rejected', + reasons: [ + 'cycle: acct-loop-a → acct-loop-b → acct-loop-a', + 'cycle: acct-loop-b → acct-loop-a → acct-loop-b', + ], + }, + { + family: 'c-600', + status: 'rejected', + reasons: ['acct-stray: parent acct-missing is not in the export'], + }, + { + family: 'c-700', + status: 'rejected', + reasons: ['acct-cross: parent acct-acme is in another company'], + }, +]; +const INVALID_PARENT_RECORDS = [ + { + id: 'ct-orphan', + reason: 'ct-orphan: account acct-gone is not in the export', + }, +]; + describeJs( 'dynamic workflows: customer migration recipe', { timeout: 120_000 }, @@ -1198,6 +1309,65 @@ describeJs( }); }); + it('rejects families whose parents cannot be ordered and migrates the rest', async () => { + const input = { ...migrationInput(), dataset: 'synthetic-crm-invalid' }; + const { childRunId } = await startParent('dynamicMissionRun', [ + PROCEDURE_A, + input, + ]); + + // Acme has no owner conflict, so the run finishes without a review. + const child = await awaitChildRun(childRunId); + expect(child.output).toMatchObject({ + families: INVALID_PARENT_FAMILIES, + rejectedRecords: INVALID_PARENT_RECORDS, + verification: { + ok: true, + violations: [], + accounts: MIGRATED_ACCOUNTS.slice(0, 3), + contacts: MIGRATED_CONTACTS.slice(0, 1), + }, + }); + }); + + it('procedure B rejects the same families before it loads anything', async () => { + const input = { ...migrationInput(), dataset: 'synthetic-crm-invalid' }; + const { childRunId } = await startParent('dynamicMissionRun', [ + PROCEDURE_B, + input, + ]); + await resumeMissionHook(`${input.tokenPrefix}:consolidation`, { + approved: true, + }); + + const child = await awaitChildRun(childRunId); + expect(child.output).toMatchObject({ + approved: true, + rejected: INVALID_PARENT_FAMILIES.slice(1), + rejectedRecords: INVALID_PARENT_RECORDS, + verification: { + ok: true, + violations: [], + counts: { accounts: 3, contacts: 1, provisional: 0 }, + accounts: MIGRATED_ACCOUNTS.slice(0, 3), + contacts: MIGRATED_CONTACTS.slice(0, 1), + }, + }); + }); + + it('refuses a plan that would create a parent cycle', async () => { + const { childRunId } = await startParent('dynamicMissionRun', [ + publishProcedure('parent-cycle', PARENT_CYCLE_SOURCE), + migrationInput(), + ]); + const child = trackRun(getRun(childRunId), { + testName: `${getCurrentTest()?.name ?? 'dynamic workflow'} [dynamic run]`, + }); + await expect(child.returnValue).rejects.toThrow( + /acct-b -> acct-a would create a parent cycle/ + ); + }); + it('refuses a plan changed after it was staged', async () => { const { childRunId } = await startParent('dynamicMissionRun', [ publishProcedure('tampered', TAMPERED_PLAN_SOURCE), diff --git a/workbench/example/workflows/99_e2e.ts b/workbench/example/workflows/99_e2e.ts index 0da7c43552..2ab5dc6eee 100644 --- a/workbench/example/workflows/99_e2e.ts +++ b/workbench/example/workflows/99_e2e.ts @@ -4446,6 +4446,102 @@ const LEGACY_DATASETS: Record = { accountId: 'acct-globex', }, ], + // Acme is valid; every other company has a parent link that cannot be + // ordered, and one contact names an account that is not in the export. + 'synthetic-crm-invalid': [ + { + kind: 'account', + id: 'acct-acme', + name: 'Acme Holdings', + parentId: null, + contract: 'C-100', + owner: 'dana', + }, + { + kind: 'account', + id: 'acct-acme-east', + name: 'Acme East', + parentId: 'acct-acme', + contract: 'C-100/E', + owner: 'dana', + }, + { + kind: 'account', + id: 'acct-acme-west', + name: 'Acme West', + parentId: 'acct-acme', + contract: 'C-100/W', + owner: 'dana', + }, + { + kind: 'account', + id: 'acct-self', + name: 'Self Parent Co', + parentId: 'acct-self', + contract: 'C-400', + owner: 'lee', + }, + { + kind: 'account', + id: 'acct-loop-a', + name: 'Loop A', + parentId: 'acct-loop-b', + contract: 'C-500', + owner: 'kim', + }, + { + kind: 'account', + id: 'acct-loop-b', + name: 'Loop B', + parentId: 'acct-loop-a', + contract: 'C-500/B', + owner: 'kim', + }, + { + kind: 'account', + id: 'acct-stray', + name: 'Stray Branch', + parentId: 'acct-missing', + contract: 'C-600', + owner: 'sam', + }, + { + kind: 'account', + id: 'acct-cross', + name: 'Cross Lineage', + parentId: 'acct-acme', + contract: 'C-700', + owner: 'sam', + }, + { + kind: 'contact', + id: 'ct-ada-east', + name: 'Ada Park', + email: 'ada@acme.com', + accountId: 'acct-acme-east', + }, + { + kind: 'contact', + id: 'ct-ada-west', + name: 'Ada Park', + email: ' ADA@acme.com', + accountId: 'acct-acme-west', + }, + { + kind: 'contact', + id: 'ct-loop', + name: 'Loop desk', + email: 'desk@loop.example', + accountId: 'acct-loop-a', + }, + { + kind: 'contact', + id: 'ct-orphan', + name: 'Orphan contact', + email: 'orphan@example.com', + accountId: 'acct-gone', + }, + ], }; const SOURCE_PAGE_SIZE = 4; @@ -4613,10 +4709,18 @@ function simulatePlan(plan: Omit): { } break; } - case 'setParent': - account(op.parent); + case 'setParent': { + // Parent links must stay acyclic, whatever order a plan writes them in. + for (let at: string | null = op.parent; at; at = account(at).parent) { + if (at === op.account) { + throw new FatalError( + `${label}: ${op.account} -> ${op.parent} would create a parent cycle` + ); + } + } account(op.account).parent = op.parent; break; + } case 'setOwner': account(op.key).owner = op.owner; break; @@ -4798,9 +4902,11 @@ async function verifyChanges(request: { } } // A contact is one person per normalized email within an account family. - const rootOf = (key: string): string => { + const rootOf = (key: string, seen = new Set()): string => { const parent = accounts[key]?.parent; - return parent && accounts[parent] ? rootOf(parent) : key; + if (!parent || !accounts[parent] || seen.has(parent)) return key; + seen.add(key); + return rootOf(parent, seen); }; const people = new Map(); for (const person of Object.values(contacts)) { From dd3b9530b1ad14edc06d07b09207a1d7c73be1e0 Mon Sep 17 00:00:00 2001 From: Alex Langenfeld Date: Thu, 1 Oct 2026 16:10:07 -0500 Subject: [PATCH 09/11] test(e2e): report migration groups under the shared result shape Signed-off-by: Alex Langenfeld --- .../cookbook/advanced/dynamic-workflows.mdx | 50 ++++++----- .../core/e2e/e2e-dynamic-workflow.test.ts | 88 +++++++++++-------- 2 files changed, 80 insertions(+), 58 deletions(-) diff --git a/docs/content/docs/v5/cookbook/advanced/dynamic-workflows.mdx b/docs/content/docs/v5/cookbook/advanced/dynamic-workflows.mdx index 4d6d8f4cd6..bb74d8839c 100644 --- a/docs/content/docs/v5/cookbook/advanced/dynamic-workflows.mdx +++ b/docs/content/docs/v5/cookbook/advanced/dynamic-workflows.mdx @@ -60,7 +60,7 @@ The importing customer is a distributor. Its CRM export lists the organizations | Initech (owner kim) and Initech Ltd (owner sam), both on contract `C-300` | One account, with the owner a reviewer chooses | | An account that reports to itself, or to an account in another organization | Nothing for that organization; the reason is reported | -A field mapping produces one destination record per row, so it cannot express any of these; they need a program. Whether that program is deployed application code or a procedure published later is the choice described above. The procedure groups accounts into **families**, one per contract root (`C-100`, `C-100/E`, and `C-100/W` are family `c-100`), and each organization in the export becomes one family. +A field mapping produces one destination record per row, so it cannot express any of these; they need a program. Whether that program is deployed application code or a procedure published later is the choice described above. The procedure groups accounts into **families**, one per contract root (`C-100`, `C-100/E`, and `C-100/W` are family `c-100`), and each organization in the export becomes one family, reported under its group key. ## Deploy once, publish later @@ -137,13 +137,14 @@ The distributor's procedure, `crm-parent-first`, validates the export's parent l // accounts on the same contract are one account, and a contact is one person // per normalized email within a family. const families = new Map(); - for (const row of accounts) { + for (const row of byId.values()) { families.set(familyOf(row), [...(families.get(familyOf(row)) || []), row]); } // familyOperations(members, owner) writes each family parent-first: see the complete source. async function migrateFamily(family, members) { + if (reasons.has(family)) return { group: family, status: "rejected", reasons: reasons.get(family) }; const stage = (owner) => steps.stageChanges({ ...scope, groupKey: family, operations: familyOperations(members, owner) }); let plan = await stage(); @@ -155,14 +156,14 @@ The distributor's procedure, `crm-parent-first`, validates the export's parent l const owners = plan.conflicts.flatMap((conflict) => conflict.values); await steps.requestApproval({ planId: plan.planId, token, summary: family + ": choose an owner from " + owners.join(", ") }); const decision = await review; - if (!decision.approved) return { family, status: "skipped" }; + if (!decision.approved) return { group: family, status: "skipped" }; plan = await stage(decision.owner || owners[0]); } finally { review.dispose(); } } const result = await steps.applyChanges({ plan, idempotencyKey: "apply:" + plan.planId }); - return { family, status: "applied", result }; + return { group: family, status: "applied", result }; } const outcomes = await Promise.all([...families].map(([family, members]) => migrateFamily(family, members))); @@ -172,9 +173,9 @@ The distributor's procedure, `crm-parent-first`, validates the export's parent l - **Policy stays in the source.** The family rule, the one-account-per-contract rule, and the one-person-per-email rule are all here; the catalog knows none of them. - **Conflicts come from the adapter, decisions from the source.** Merging Initech and Initech Ltd yields an owner conflict on the plan. The procedure decides that a conflict needs a person, and restages the plan with the reviewer's owner, because a staged plan never changes. - **Review before waiting.** The hook is created before `requestApproval` sends its token, so a fast reviewer cannot miss it, and it is disposed once the review settles, so a late reply is refused. A rejected family is skipped. -- **Invalid input is rejected, not guessed at.** Before any of this, the procedure walks each account's parent chain. A family with a self-parent, a cycle, a missing parent, or a parent in another family is reported with its reasons and gets no plan at all, while the other families proceed. +- **Invalid input is rejected, not guessed at.** Before any of this, the procedure walks each account's parent chain. A family with a self-parent, a cycle, a missing parent, or a parent in another family gets no plan at all; it appears in the run's `results` as `rejected`, with one reason per problem, while the other families proceed. -A reviewer resumes the hook from a route that rebuilds the token from the run's `tokenPrefix` and the family key, after authorizing the caller; see [the review route](#review-route). +A reviewer resumes the hook from a route that rebuilds the token from the run's `tokenPrefix` and the family's group key, after authorizing the caller; see [the review route](#review-route). ## What survives a pause, and what changed @@ -243,11 +244,16 @@ If these two strategies are the only ones you will ever need, a static workflow } // 4. Apply only what was approved, then check the destination either way. - const results = [loaded]; + const applies = [loaded]; if (decision.approved) { - results.push(await steps.applyChanges({ plan, idempotencyKey: "apply:" + plan.planId })); + applies.push(await steps.applyChanges({ plan, idempotencyKey: "apply:" + plan.planId })); } - const verification = await steps.verifyChanges({ ...scope, results }); + const verification = await steps.verifyChanges({ ...scope, results: applies }); + const results = [ + ...[...reasons].map(([group, why]) => ({ group, status: "rejected", reasons: why })), + { group: "load", status: "applied", planId: load.planId }, + { group: "consolidation", status: decision.approved ? "applied" : "skipped", planId: plan.planId }, + ]; ``` The two procedures converge on the same records, but get there differently: @@ -383,17 +389,19 @@ async function workflow(input) { const byId = new Map(rows.filter((row) => row.kind === "account").map((row) => [row.id, row])); const reasons = new Map(); const reject = (row, reason) => reasons.set(familyOf(row), [...(reasons.get(familyOf(row)) || []), reason]); + const onReportedCycle = new Set(); for (const row of byId.values()) { if (row.parentId === null) continue; const parent = byId.get(row.parentId); if (row.parentId === row.id) reject(row, row.id + " is its own parent"); else if (!parent) reject(row, row.id + ": parent " + row.parentId + " is not in the export"); else if (familyOf(parent) !== familyOf(row)) reject(row, row.id + ": parent " + row.parentId + " is in another company"); - else { + else if (!onReportedCycle.has(row.id)) { const path = [row.id]; for (let at = parent; at; at = byId.get(at.parentId)) { if (at.id === row.id) { reject(row, "cycle: " + [...path, row.id].join(" → ")); + path.forEach((id) => onReportedCycle.add(id)); break; } if (path.includes(at.id)) break; @@ -408,13 +416,12 @@ async function workflow(input) { const contacts = rows.filter( (row) => row.kind === "contact" && byId.has(row.accountId) && !reasons.has(familyOf(byId.get(row.accountId))) ); - const rejected = [...reasons].map(([family, why]) => ({ family, status: "rejected", reasons: why })); // This customer's rules: a family is every account under one contract root, // accounts on the same contract are one account, and a contact is one person // per normalized email within a family. const families = new Map(); - for (const row of accounts) { + for (const row of byId.values()) { families.set(familyOf(row), [...(families.get(familyOf(row)) || []), row]); } @@ -467,6 +474,7 @@ async function workflow(input) { } async function migrateFamily(family, members) { + if (reasons.has(family)) return { group: family, status: "rejected", reasons: reasons.get(family) }; const stage = (owner) => steps.stageChanges({ ...scope, groupKey: family, operations: familyOperations(members, owner) }); let plan = await stage(); @@ -478,14 +486,14 @@ async function workflow(input) { const owners = plan.conflicts.flatMap((conflict) => conflict.values); await steps.requestApproval({ planId: plan.planId, token, summary: family + ": choose an owner from " + owners.join(", ") }); const decision = await review; - if (!decision.approved) return { family, status: "skipped" }; + if (!decision.approved) return { group: family, status: "skipped" }; plan = await stage(decision.owner || owners[0]); } finally { review.dispose(); } } const result = await steps.applyChanges({ plan, idempotencyKey: "apply:" + plan.planId }); - return { family, status: "applied", result }; + return { group: family, status: "applied", result }; } const outcomes = await Promise.all([...families].map(([family, members]) => migrateFamily(family, members))); @@ -493,9 +501,9 @@ async function workflow(input) { ...scope, results: outcomes.filter((outcome) => outcome.result).map((outcome) => outcome.result), }); - const summary = [...outcomes.map(({ family, status }) => ({ family, status })), ...rejected]; - const report = await steps.publishReport({ migrationId: input.migrationId, results: { families: summary, rejectedRecords, verification } }); - return { families: summary, rejectedRecords, verification, report }; + const results = outcomes.map(({ result, ...row }) => row); + const report = await steps.publishReport({ migrationId: input.migrationId, results: { results, rejectedRecords, verification } }); + return { results, rejectedRecords, verification, report }; } ``` @@ -550,7 +558,7 @@ declare function assertCanReview(request: Request, migrationId: string): Promise const reviewRequest = z.object({ migrationId: z.string().startsWith("migration-"), - family: z.string(), + group: z.string(), approved: z.boolean(), owner: z.string().optional(), }); @@ -564,11 +572,11 @@ export async function POST( if (!parsed.success) { return Response.json({ error: parsed.error.message }, { status: 400 }); } - const { migrationId, family, approved, owner } = parsed.data; + const { migrationId, group, approved, owner } = parsed.data; // Knowing a token does not authorize the caller. await assertCanReview(request, migrationId); // [!code highlight] - const token = `migration:${migrationId.slice("migration-".length)}:${family}`; + const token = `migration:${migrationId.slice("migration-".length)}:${group}`; try { const hook = await getHookByToken(token); if (hook.runId !== runId) { @@ -585,7 +593,7 @@ export async function POST( } ``` -A review for a family that never conflicted, or one that arrives after the review settled, gets a 404: that hook never existed, or the procedure already disposed it. See [Authorize before calling `resumeHook()`](/docs/foundations/hooks#authorize-before-calling-resumehook). +A review for a group that never conflicted, or one that arrives after the review settled, gets a 404: that hook never existed, or the procedure already disposed it. See [Authorize before calling `resumeHook()`](/docs/foundations/hooks#authorize-before-calling-resumehook). ### Revision checks diff --git a/packages/core/e2e/e2e-dynamic-workflow.test.ts b/packages/core/e2e/e2e-dynamic-workflow.test.ts index 5c00839be0..e336770bb9 100644 --- a/packages/core/e2e/e2e-dynamic-workflow.test.ts +++ b/packages/core/e2e/e2e-dynamic-workflow.test.ts @@ -798,17 +798,19 @@ async function workflow(input) { const byId = new Map(rows.filter((row) => row.kind === "account").map((row) => [row.id, row])); const reasons = new Map(); const reject = (row, reason) => reasons.set(familyOf(row), [...(reasons.get(familyOf(row)) || []), reason]); + const onReportedCycle = new Set(); for (const row of byId.values()) { if (row.parentId === null) continue; const parent = byId.get(row.parentId); if (row.parentId === row.id) reject(row, row.id + " is its own parent"); else if (!parent) reject(row, row.id + ": parent " + row.parentId + " is not in the export"); else if (familyOf(parent) !== familyOf(row)) reject(row, row.id + ": parent " + row.parentId + " is in another company"); - else { + else if (!onReportedCycle.has(row.id)) { const path = [row.id]; for (let at = parent; at; at = byId.get(at.parentId)) { if (at.id === row.id) { reject(row, "cycle: " + [...path, row.id].join(" → ")); + path.forEach((id) => onReportedCycle.add(id)); break; } if (path.includes(at.id)) break; @@ -823,13 +825,12 @@ async function workflow(input) { const contacts = rows.filter( (row) => row.kind === "contact" && byId.has(row.accountId) && !reasons.has(familyOf(byId.get(row.accountId))) ); - const rejected = [...reasons].map(([family, why]) => ({ family, status: "rejected", reasons: why })); // This customer's rules: a family is every account under one contract root, // accounts on the same contract are one account, and a contact is one person // per normalized email within a family. const families = new Map(); - for (const row of accounts) { + for (const row of byId.values()) { families.set(familyOf(row), [...(families.get(familyOf(row)) || []), row]); } @@ -882,6 +883,7 @@ async function workflow(input) { } async function migrateFamily(family, members) { + if (reasons.has(family)) return { group: family, status: "rejected", reasons: reasons.get(family) }; const stage = (owner) => steps.stageChanges({ ...scope, groupKey: family, operations: familyOperations(members, owner) }); let plan = await stage(); @@ -893,14 +895,14 @@ async function workflow(input) { const owners = plan.conflicts.flatMap((conflict) => conflict.values); await steps.requestApproval({ planId: plan.planId, token, summary: family + ": choose an owner from " + owners.join(", ") }); const decision = await review; - if (!decision.approved) return { family, status: "skipped" }; + if (!decision.approved) return { group: family, status: "skipped" }; plan = await stage(decision.owner || owners[0]); } finally { review.dispose(); } } const result = await steps.applyChanges({ plan, idempotencyKey: "apply:" + plan.planId }); - return { family, status: "applied", result }; + return { group: family, status: "applied", result }; } const outcomes = await Promise.all([...families].map(([family, members]) => migrateFamily(family, members))); @@ -908,9 +910,9 @@ async function workflow(input) { ...scope, results: outcomes.filter((outcome) => outcome.result).map((outcome) => outcome.result), }); - const summary = [...outcomes.map(({ family, status }) => ({ family, status })), ...rejected]; - const report = await steps.publishReport({ migrationId: input.migrationId, results: { families: summary, rejectedRecords, verification } }); - return { families: summary, rejectedRecords, verification, report }; + const results = outcomes.map(({ result, ...row }) => row); + const report = await steps.publishReport({ migrationId: input.migrationId, results: { results, rejectedRecords, verification } }); + return { results, rejectedRecords, verification, report }; } `; @@ -933,17 +935,19 @@ async function workflow(input) { const byId = new Map(rows.filter((row) => row.kind === "account").map((row) => [row.id, row])); const reasons = new Map(); const reject = (row, reason) => reasons.set(familyOf(row), [...(reasons.get(familyOf(row)) || []), reason]); + const onReportedCycle = new Set(); for (const row of byId.values()) { if (row.parentId === null) continue; const parent = byId.get(row.parentId); if (row.parentId === row.id) reject(row, row.id + " is its own parent"); else if (!parent) reject(row, row.id + ": parent " + row.parentId + " is not in the export"); else if (familyOf(parent) !== familyOf(row)) reject(row, row.id + ": parent " + row.parentId + " is in another company"); - else { + else if (!onReportedCycle.has(row.id)) { const path = [row.id]; for (let at = parent; at; at = byId.get(at.parentId)) { if (at.id === row.id) { reject(row, "cycle: " + [...path, row.id].join(" → ")); + path.forEach((id) => onReportedCycle.add(id)); break; } if (path.includes(at.id)) break; @@ -958,7 +962,6 @@ async function workflow(input) { const contacts = rows.filter( (row) => row.kind === "contact" && byId.has(row.accountId) && !reasons.has(familyOf(byId.get(row.accountId))) ); - const rejected = [...reasons].map(([family, why]) => ({ family, status: "rejected", reasons: why })); // 1. Land every valid row as written: provisional, flat, one record per row. const load = await steps.stageChanges({ @@ -1018,13 +1021,18 @@ async function workflow(input) { } // 4. Apply only what was approved, then check the destination either way. - const results = [loaded]; + const applies = [loaded]; if (decision.approved) { - results.push(await steps.applyChanges({ plan, idempotencyKey: "apply:" + plan.planId })); + applies.push(await steps.applyChanges({ plan, idempotencyKey: "apply:" + plan.planId })); } - const verification = await steps.verifyChanges({ ...scope, results }); - const report = await steps.publishReport({ migrationId: input.migrationId, results: { approved: decision.approved, rejected, rejectedRecords, verification } }); - return { approved: decision.approved, rejected, rejectedRecords, verification, report }; + const verification = await steps.verifyChanges({ ...scope, results: applies }); + const results = [ + ...[...reasons].map(([group, why]) => ({ group, status: "rejected", reasons: why })), + { group: "load", status: "applied", planId: load.planId }, + { group: "consolidation", status: decision.approved ? "applied" : "skipped", planId: plan.planId }, + ]; + const report = await steps.publishReport({ migrationId: input.migrationId, results: { results, rejectedRecords, verification } }); + return { results, rejectedRecords, verification, report }; } `; @@ -1163,27 +1171,24 @@ const MIGRATED_CONTACTS = [ /** Every family in `synthetic-crm-invalid`, in export order. */ const INVALID_PARENT_FAMILIES = [ - { family: 'c-100', status: 'applied' }, + { group: 'c-100', status: 'applied' }, { - family: 'c-400', + group: 'c-400', status: 'rejected', reasons: ['acct-self is its own parent'], }, { - family: 'c-500', + group: 'c-500', status: 'rejected', - reasons: [ - 'cycle: acct-loop-a → acct-loop-b → acct-loop-a', - 'cycle: acct-loop-b → acct-loop-a → acct-loop-b', - ], + reasons: ['cycle: acct-loop-a → acct-loop-b → acct-loop-a'], }, { - family: 'c-600', + group: 'c-600', status: 'rejected', reasons: ['acct-stray: parent acct-missing is not in the export'], }, { - family: 'c-700', + group: 'c-700', status: 'rejected', reasons: ['acct-cross: parent acct-acme is in another company'], }, @@ -1222,10 +1227,10 @@ describeJs( await resumeHook(hook, { approved: true, owner: 'sam' }); const child = await awaitChildRun(childRunId); expect(child.output).toMatchObject({ - families: [ - { family: 'c-100', status: 'applied' }, - { family: 'c-200', status: 'applied' }, - { family: 'c-300', status: 'applied' }, + results: [ + { group: 'c-100', status: 'applied' }, + { group: 'c-200', status: 'applied' }, + { group: 'c-300', status: 'applied' }, ], verification: { ok: true, @@ -1249,10 +1254,10 @@ describeJs( const child = await awaitChildRun(childRunId); expect(child.output).toMatchObject({ - families: [ - { family: 'c-100', status: 'applied' }, - { family: 'c-200', status: 'applied' }, - { family: 'c-300', status: 'skipped' }, + results: [ + { group: 'c-100', status: 'applied' }, + { group: 'c-200', status: 'applied' }, + { group: 'c-300', status: 'skipped' }, ], verification: { ok: true, @@ -1278,7 +1283,10 @@ describeJs( await resumeHook(hook, { approved: true, owner: 'sam' }); const child = await awaitChildRun(childRunId); expect(child.output).toMatchObject({ - approved: true, + results: [ + { group: 'load', status: 'applied' }, + { group: 'consolidation', status: 'applied' }, + ], verification: { ok: true, violations: [], @@ -1301,7 +1309,10 @@ describeJs( const child = await awaitChildRun(childRunId); expect(child.output).toMatchObject({ - approved: false, + results: [ + { group: 'load', status: 'applied' }, + { group: 'consolidation', status: 'skipped' }, + ], verification: { ok: false, counts: { accounts: 6, contacts: 4, provisional: 6 }, @@ -1319,7 +1330,7 @@ describeJs( // Acme has no owner conflict, so the run finishes without a review. const child = await awaitChildRun(childRunId); expect(child.output).toMatchObject({ - families: INVALID_PARENT_FAMILIES, + results: INVALID_PARENT_FAMILIES, rejectedRecords: INVALID_PARENT_RECORDS, verification: { ok: true, @@ -1342,8 +1353,11 @@ describeJs( const child = await awaitChildRun(childRunId); expect(child.output).toMatchObject({ - approved: true, - rejected: INVALID_PARENT_FAMILIES.slice(1), + results: [ + ...INVALID_PARENT_FAMILIES.slice(1), + { group: 'load', status: 'applied' }, + { group: 'consolidation', status: 'applied' }, + ], rejectedRecords: INVALID_PARENT_RECORDS, verification: { ok: true, From e88dd6cb8b148d0c09147b71037e2cbb6007d81f Mon Sep 17 00:00:00 2001 From: Alex Langenfeld Date: Thu, 1 Oct 2026 16:13:26 -0500 Subject: [PATCH 10/11] docs: keep braces out of the recipe's frontmatter summary Signed-off-by: Alex Langenfeld --- docs/content/docs/v5/cookbook/advanced/dynamic-workflows.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/docs/v5/cookbook/advanced/dynamic-workflows.mdx b/docs/content/docs/v5/cookbook/advanced/dynamic-workflows.mdx index bb74d8839c..b7661189d6 100644 --- a/docs/content/docs/v5/cookbook/advanced/dynamic-workflows.mdx +++ b/docs/content/docs/v5/cookbook/advanced/dynamic-workflows.mdx @@ -2,7 +2,7 @@ title: Dynamic Workflows description: Publish a reviewed, customer-specific migration procedure after the platform deploys, and run it durably over a fixed catalog of deployed steps. type: guide -summary: Deploy a catalog of migration steps once, then start reviewed JavaScript procedures with start(source, [input], { experimental_dynamic: { steps } }), pause them for approvals, and read back the records they changed, without an application release per procedure. +summary: Deploy a catalog of migration steps once, then start reviewed JavaScript procedures with start() and experimental_dynamic.steps, pause them for approvals, and read back the records they changed, without an application release per procedure. prerequisites: - /docs/advanced/dynamic-workflows related: From 9bc079b9d1fc759c56b46ad1eabbf993e70d31b3 Mon Sep 17 00:00:00 2001 From: Alex Langenfeld Date: Fri, 2 Oct 2026 14:42:46 -0500 Subject: [PATCH 11/11] [docs] Fill gaps in the dynamic workflows concept page (#4550) ## Summary & Motivation Fills gaps the 5.0.0 dogfood surfaced on the dynamic workflows concept page: a table of the per-gate refusals and which of them are fatal, how a Vercel project gets dynamic-source storage, what the 2,048-byte execution-context budget actually buys, that the workflow ID hashes raw source bytes, and that "trusted" means reviewed application code rather than anything a model did not write. Stacked on #4541, which adds the cookbook recipe this page now links to. ## Test Plan Docs only. Links, anchors, quoted error messages, and the fatal/retryable behavior were checked by hand against the runtime source on main. #4534 (open) edits this page's `exportName` rule. This PR leaves that line alone, quotes only the stable prefix of the execution-context error that #4534 extends, and does not document #4534's validation messages. ## Docs Preview | Page | Link | | --- | --- | | Dynamic workflows | [/docs/advanced/dynamic-workflows](https://workflow-docs-rakhde7hj.vercel.sh/docs/advanced/dynamic-workflows) | | Errors for each gate | [#errors-for-each-gate](https://workflow-docs-rakhde7hj.vercel.sh/docs/advanced/dynamic-workflows#errors-for-each-gate) | | Enabling storage on Vercel | [#enabling-storage-on-vercel](https://workflow-docs-rakhde7hj.vercel.sh/docs/advanced/dynamic-workflows#enabling-storage-on-vercel) | --------- Signed-off-by: Alex Langenfeld --- .changeset/dynamic-workflows-concept-docs.md | 4 ++ .../docs/v5/advanced/dynamic-workflows.mdx | 45 +++++++++++++++---- 2 files changed, 40 insertions(+), 9 deletions(-) create mode 100644 .changeset/dynamic-workflows-concept-docs.md diff --git a/.changeset/dynamic-workflows-concept-docs.md b/.changeset/dynamic-workflows-concept-docs.md new file mode 100644 index 0000000000..eff32ba9e4 --- /dev/null +++ b/.changeset/dynamic-workflows-concept-docs.md @@ -0,0 +1,4 @@ +--- +--- + +Docs: fill gaps in the dynamic workflows concept page: per-gate errors and which are fatal, Vercel storage enablement, the execution-context budget, raw-source hashing, and trusted authoring. diff --git a/docs/content/docs/v5/advanced/dynamic-workflows.mdx b/docs/content/docs/v5/advanced/dynamic-workflows.mdx index a829c3cad7..092a8201c5 100644 --- a/docs/content/docs/v5/advanced/dynamic-workflows.mdx +++ b/docs/content/docs/v5/advanced/dynamic-workflows.mdx @@ -52,14 +52,18 @@ console.log(await run.status); // 'running' Only the *orchestration* is dynamic. Every step the source calls was deployed with your app, and `experimental_dynamic.steps` names the ones it calls by alias. That map does not stop source from reaching other steps; see [Security](#security). There is no way to define a new step from source. +`start()` can be called from any server code in the deployment, including a route handler, with no wrapper workflow. Pass the **imported** step functions: the build stamps a `stepId` on each one, and that ID is what binds the alias to a deployed step. Code without a handle on the function, such as a script outside the deployment, has to pass `{ stepId: '...' }` references instead. + +For a complete application of this, see the [Dynamic Workflows recipe](/cookbook/advanced/dynamic-workflows): a migration step catalog deployed once, and customer-specific procedures written, reviewed, and started after deploy. + ## When to use this Reach for dynamic workflows when the **shape** of the orchestration is only known after you deploy, and the source comes from code you trust as much as your own: -- **Orchestration your application assembles** from reviewed templates, over a fixed set of deployed steps. +- **Orchestration published after deploy**, such as reviewed workflows your operators add to a store, over a fixed set of deployed steps. - **Experiments** — try a new composition of existing steps without shipping a build. -Dynamic workflows are not a way to run code written by your end users or generated by a model from their input. That source would run with your deployment's privileges; see [Security](#security). +Dynamic workflows execute trusted application code, including code authored by operators or drafted by a model and approved under your application-code review process. Do not execute end-user scripts, or unreviewed model-generated source influenced by untrusted requests, retrieved content, or tool outputs. That source would run with your deployment's privileges, and a fixed step catalog, syntax validation, and a dedicated project do not make it safe; see [Security](#security). If your workflows are known at build time, use a normal workflow function. It has better types, better errors, no source validation, and no size limits. @@ -73,14 +77,29 @@ WORKFLOW_EXPERIMENTAL_DYNAMIC_WORKFLOWS=1 Only `1` or `true` (case-insensitive) enables them; any other value, or no value, leaves them off. The runtime reads the variable where workflows execute, when it needs it, so set it on the deployment or dev server rather than at build time. It controls three things: -- **Starting.** `start()` with source throws before it contacts the World, creates a run, or enqueues anything unless this process has opted in. -- **Delivery.** When a dynamic run reaches a deployment that has not opted in, the runtime does not execute its stored code. It fails the run with a `RUNTIME_ERROR` rather than retrying it. +- **Starting.** `start()` with source throws before it contacts the World, creates a run, or enqueues anything unless this process has opted in. Source validation runs first, so a malformed source reports its validation error even when the opt-in is off. +- **Delivery.** When a dynamic run reaches a deployment that has not opted in, the runtime does not execute its stored code. It fails the run with a `RUNTIME_ERROR` rather than retrying it. This covers a run that was started while the deployment was opted in and wakes after the variable is removed. - **Health check.** A deployment advertises dynamic support in its [health check](/docs/api-reference/workflow-runtime/health-check) only when it has opted in. ### Same deployment only A dynamic run must execute on the deployment that started it. `start()` rejects a dynamic start whose target differs from the current deployment. That includes an explicit `deploymentId` for another deployment, `deploymentId: 'latest'` when it resolves to a different deployment, and any concrete target when the current deployment cannot be determined. The rejection happens before any capability check, key lookup, upload, run creation, or queue message. +### Errors for each gate + +Each gate fails with a message that names it. Except at delivery, where the run itself fails, `start()` throws: a `WorkflowRuntimeError` for the checks it makes before writing anything, including the World's execution-context limit, and the World's own error when the backend refuses the run. + +| Gate | Where it fails | Message | +| --- | --- | --- | +| Opt-in, at start | `start()` | `Dynamic workflows are disabled on this deployment, so no run was created. Set WORKFLOW_EXPERIMENTAL_DYNAMIC_WORKFLOWS=1 on the deployment to enable them.` | +| Opt-in, at delivery | The run, with `RUNTIME_ERROR` | `Workflow run "" is a dynamic workflow run (source ), but this deployment has not enabled dynamic workflows, so its stored code was not executed. Set WORKFLOW_EXPERIMENTAL_DYNAMIC_WORKFLOWS=1 on the deployment to enable them.` | +| Same deployment | `start()` | `Dynamic workflows can only start on the current deployment. This start targets "" from "", so no run was created.` | +| World capability | `start()` | ``Dynamic workflows require a World that declares `capabilities.dynamicWorkflowCode`. This World does not, so no run was created.`` | +| Execution-context limit (Vercel) | `start()` | `Dynamic workflow execution context is bytes, exceeding the 2048-byte limit, so no run was created.`, followed by guidance on what counts | +| Project storage (Vercel) | `start()`, from the backend | A `WorkflowWorldError` with `status: 400`, `code: 'dynamic-workflow-storage-disabled'`, and the message `dynamic workflow storage is not enabled for this project`. See [World support](#world-support). | + +The `WorkflowRuntimeError` refusals, like the [source validation](#rules-for-the-source) errors, are marked fatal: `FatalError.is()` returns `true` for them, so a step that calls `start()` fails on the first attempt instead of spending its retries. A same-deployment refusal is fatal only when the current deployment is known. When it cannot be determined, the message reads `from an unknown current deployment` and the error stays retryable, because the lookup can succeed on a retry. When workflow code calls `start()` directly, `start()` runs as its own step: that step still fails without retrying, but the error the workflow catches does not carry the fatal marker. + ## What the source can use Dynamic source has no imports. Instead, the generated code predefines a small runtime surface: @@ -146,7 +165,7 @@ Supply a unique, deterministic approval token from the caller. The workflow must - JavaScript only — no TypeScript syntax, no npm dependencies, no bundling. - No inline `"use step"` functions. Steps come from `experimental_dynamic.steps`. - At most 128 KB of source. -- On Vercel, the run's execution context is limited to 2,048 bytes of JSON, and the `dynamicWorkflow` metadata below counts against it. That leaves room for roughly 30 step aliases, depending on how long the aliases and step IDs are. A start that exceeds it fails before anything is written. +- On Vercel, the run's execution context is limited to **2,048 bytes of JSON**, and the `dynamicWorkflow` metadata below counts against it: the source hash, the export name, and every alias together with its full step ID. A step ID comes from the step's file path and function name, so it usually costs more than its alias, and a step in a deeply nested file costs more than one near the root. Treat any alias count as an estimate: in one measurement, 36 aliases bound to 36-character step IDs fit and 37 did not, while with 80-character step IDs about 19 fit. A start that exceeds the limit fails before anything is written. The Local World does not enforce this limit, so check a definition with many bindings on a Vercel preview. Everything a static workflow must obey still applies: the body has to be [deterministic](/docs/foundations/workflows-and-steps), and any side effect belongs in a step. @@ -163,7 +182,7 @@ Two consequences worth knowing: - **The same definition always gets the same ID.** Runs of one generated workflow group together in [observability](/docs/observability) and share a queue topic, even across processes. - **A caller cannot claim an ID.** Because the hash covers the source *and* the step bindings, arbitrary source cannot be made to run under a static workflow's name — or under another definition's. -Changing the source, or pointing an alias at a different step, produces a different workflow. +Changing the source, or pointing an alias at a different step, produces a different workflow. The hash covers the raw source bytes, so whitespace, formatting, and comment changes also produce a new workflow ID, and runs before and after the change group separately. When you need a name that stays stable across revisions of a definition, keep it in your application, alongside the exact source you started; see the [recipe](/cookbook/advanced/dynamic-workflows#procedure-revisions). ## How the code is stored @@ -190,13 +209,19 @@ Dynamic workflows need a World that can store the run's workflow code. | World | Support | | --- | --- | -| [Vercel](/worlds/vercel) | Encrypted, ref-backed storage (small definitions transported inline; large definitions uploaded first). If dynamic-source storage is not enabled for the project, the backend rejects the run's creation and `start()` throws. | +| [Vercel](/worlds/vercel) | Encrypted, ref-backed storage (small definitions transported inline; large definitions uploaded first). Dynamic-source storage must be enabled for the project; see below. | | [Local](/worlds/local) | Stored in plaintext on the run record in the local filesystem store. | | [Postgres](/worlds/postgres) | Stored in plaintext on the run row. | | Others | Supported when the World declares [`capabilities.dynamicWorkflowCode`](/worlds/building-a-world). | After the opt-in and same-deployment checks, `start()` fails a dynamic start on a World that does not declare `capabilities.dynamicWorkflowCode`. On Vercel, `start()` then validates the final execution context against the 2,048-byte limit. All of this happens before serializing or uploading code, creating an event, or publishing a queue message. +A dynamic start then waits for `run_created` and confirms that the backend stored the code before it publishes the run's queue message. Any failure to create the run, retryable or not, reaches the caller with nothing published: unlike a static start, there is no resilient start that queues the run and retries its creation later. A backend that accepts the run without storing its code makes `start()` throw a message beginning `Workflow run was created, but this deployment's Workflow backend did not store its dynamic workflow code`; that run stays `pending` and is never queued. + +### Enabling storage on Vercel + +On Vercel, dynamic-source storage is enabled per project by a server-side allowlist. There is no project setting for it yet; contact the Workflow team to have a project enabled. Until it is, the backend refuses the run's creation and `start()` throws a `WorkflowWorldError` with `status: 400`, `code: 'dynamic-workflow-storage-disabled'`, and the message `dynamic workflow storage is not enabled for this project`. Nothing is queued, so retrying the start does not help until the project is enabled. + ## Security @@ -204,7 +229,9 @@ Dynamic source is **trusted application code** with the full privileges of your - **`steps` is not a boundary.** The `steps` object contains only the aliases you passed and is frozen, so ordinary code that calls `steps.somethingElse()` fails the run instead of dispatching a step it was not given. Code that is trying to reach other steps, or the host, can. -- **Only start source you would merge.** Do not build source from end-user input, and do not run model output generated from untrusted input. Either one gives whoever controls that input your deployment's privileges. +- **The host is reachable.** Source can read `process.env`, reach host globals, call the World with the deployment's credentials (which reach other runs' hooks and streams), and alter state in a warm instance that later requests reuse. +- **Only start reviewed source.** Source authored by operators or drafted by a model is fine once it is approved under your application-code review process. Do not execute end-user scripts, or unreviewed model-generated source influenced by untrusted requests, retrieved content, or tool outputs; either one gives whoever controls that input your deployment's privileges. A fixed step catalog and syntax validation do not change that. +- **A dedicated project is defense in depth, not a boundary.** Running dynamic workflows from their own Vercel project, with only the environment variables and access its steps need, limits what a mistake in reviewed code can reach. It does not make unreviewed source safe to run. - **Opting in is a deployment decision.** A deployment that sets `WORKFLOW_EXPERIMENTAL_DYNAMIC_WORKFLOWS` executes stored code for any dynamic run it receives. Anyone who can start a workflow on it can run code with its privileges. - **Encryption gives confidentiality only.** Where the World encrypts stored code, it cannot be read at rest without the run's key, and a delivery refuses code that is not encrypted with that key. Anyone who can obtain the run's key can still write valid code, so encryption does not replace the opt-in. - **Plaintext Worlds turn storage write access into code execution.** The Local and Postgres Worlds store the code in plaintext. On an opted-in deployment, anyone who can write to the Postgres database or the local data directory can make every worker execute code of their choosing. @@ -223,5 +250,5 @@ Treat dynamic source the way you would treat code in a pull request: written or - No caller-provided workflow IDs. - No **Replay Run** from the observability UI. - Parser-based validation checks JavaScript syntax and the required source/wrapper shape without executing it. It does not validate behavior, determinism, or intent. -- On Vercel, roughly 30 step aliases fit the 2,048-byte execution-context limit. +- On Vercel, the run's execution context is limited to 2,048 bytes of JSON, which bounds how many step aliases fit; the count depends on step-ID length. - Requires a World with dynamic-source storage.