Character creation is composed from versioned recipe capabilities
Context
Section titled “Context”Character creation needs to support materially different journeys without a new frontend and backend branch for every product variation. Examples include a visual character assembled from questionnaire choices, an initial real-identity character created from one photograph, and a subsequent real-identity character improved with optional body and angle references. Future journeys may reorder those steps, add a paywall, reuse a form as an editor, change a workflow, or insert another validation step.
Encoding each journey as application control flow would couple product iteration to
coordinated Brisket, Sirloin, and Brain releases. It would also make labels such as
“VI”, “RI Initial”, and “RI Subsequent” into domain discriminators even though they
describe product recipes rather than different character storage models. The shared
contract carries the durable Real/Virtual identity category that Sirloin stores on
the character (proto/tbone/v1/character_recipe.proto:9-30), while creation-method
provenance remains the pinned recipe key, version, and digest
(proto/sirloin/v5/character_recipe.proto:34-42).
The design must satisfy four forces at once:
- Product authors must be able to compose supported creation flows and configure reviewed management features from data.
- Sirloin must remain authoritative for state, authorization, billing, assets, and transitions.
- Recipes must not inject arbitrary frontend code or make Brain the owner of the character lifecycle.
- Unreleased Characters V2 work must remain invisible to customers who do not have the feature flag, without changing the existing CCV4 route.
The detailed runtime topology, concurrency model, and known production deferrals are documented in the Character Recipe Engine. This ADR records the higher-level capability decision and the rules for extending it.
Decision
Section titled “Decision”Character creation and recipe-backed management use versioned recipe
capabilities, with deliberately different flexibility. Creation recipes arrange
code-owned stages, controls, actions, transitions, and conditions. Management
recipes select and configure code-owned Name, Likeness, and Settings features, or
launch a reviewed named flow. CAPABILITY launches a flow with a server-applied
grant; FLOW launches a standalone non-granting flow and cannot reuse the creation
flow, including through another flow. A standalone flow with an account-visibility
gate cannot transition into another flow; its protected continuation remains in
the same flow so eligibility can be rechecked. Creation flows containing
NSFW_UNSUBSCRIBED_ONLY or KYC_UNVERIFIED_ONLY cannot leave their flow, and no
account-gated capability-granting flow can leave its flow. This keeps the new
adult prerequisites recheckable without changing the existing generic
UNSUBSCRIBED_ONLY creation transition into a granting flow. Both surfaces may
reference bounded fields, asset roles, billing policies, and immutable Brain
workflow pins, but recipes do not define executable code or weaken service-owned
invariants.
The recipe is the product contract
Section titled “The recipe is the product contract”The T-Bone CharacterRecipe envelope contains:
- a stable route key and customer-facing catalog entry;
- the durable Real/Virtual identity category produced by the recipe;
- named presentation flows, one selected by
creationFlowKey, and a bounded management feature/launcher catalog; - workflow contract pins;
- recipe-local billing policies;
- protected policy values; and
- a terminal output schema
(
proto/tbone/v1/character_recipe.proto:21-30).
Sirloin stores canonical immutable versions and routes a recipe key to one active
version. Publication verifies the canonical bytes and validated digest, allocates a
new version, and atomically moves the active route
(apps/sirloin/internal/pkg/storage/characterrecipepublication.go:221-342). A
running session remains tied to its pinned version; publishing a later version does
not silently rewrite that session.
Catalog order and names are presentation data. The current seedable fixtures demonstrate six creation routes across two durable identity types: four RI routes and two VI routes. Route keys do not become additional character types:
| Fixture key | Customer title | Identity type | Demonstrated composition |
|---|---|---|---|
initial-vi-sfw |
Custom AI influencer | Virtual | Female questionnaire, preparation and gallery acceptance; adult enablement uses an 18+ subscription, pubic-hair settings, generated preview, and explicit acceptance. |
initial-vi-male-sfw |
Custom male AI influencer | Virtual | Male questionnaire and the same creation flow; adult enablement adds penis-size and pubic-hair settings. |
initial-ri-sfw |
Initial RI SFW Female | Real | Female initial creation, plus existing-character adult enablement with subscription and account KYC gates. |
digital-twin-sfw |
Subsequent RI SFW Female | Real | Female subsequent creation with required frontal face and optional full-body and angled-body references before preview generation, plus the same adult-enablement management flow. |
initial-ri-male-sfw |
Initial RI SFW Male | Real | Male initial creation, plus the same adult-enablement management flow. |
digital-twin-male-sfw |
Subsequent RI SFW Male | Real | Male subsequent creation with required frontal face and optional full-body and angled-body references before preview generation, plus the same adult-enablement management flow. |
The fixture catalog entries are source evidence for those names and entry stages
(apps/flank/app/fixtures/character-recipes/initial-vi-sfw.json:1-23,
apps/flank/app/fixtures/character-recipes/initial-ri-sfw.json:1-24,
apps/flank/app/fixtures/character-recipes/digital-twin-sfw.json:1-24,
apps/flank/app/fixtures/character-recipes/initial-ri-male-sfw.json:1-24, and
apps/flank/app/fixtures/character-recipes/digital-twin-male-sfw.json:1-24).
Responsibility boundaries
Section titled “Responsibility boundaries”| Layer | Owns | Must not own |
|---|---|---|
| Flank / recipe JSON | Authoring, section editing, validation feedback, immutable publish and rollback requests, workflow-pin bump assistance, and reference fixtures. | Customer session state, billing decisions, or workflow execution. |
| Sirloin | Canonical recipes, publication, account-scoped sessions and characters, action availability, authoritative transitions, uploads and asset provenance, billing reservation/settlement, execution rows, and capability-projected reads. | The full Brain node graph or customer rendering. |
| Brisket | A generic creation runner over reviewed stage/control variants; code-owned management feature layouts; local draft UX, accessibility, subscription presentation, and Characters V2 views. | Authoritative progression, arbitrary recipe-provided components or management journeys, billing authority, or Brain calls. |
| Brain | Published CHARACTER_COMPUTE graphs, allowed-node enforcement, Bull execution, terminal output projection, and completion notification. |
Recipe routes, character/session ownership, entitlement, or billing. |
Flank exposes a dedicated character-recipe editor that works with route versions,
section buffers, publish, rollback, and workflow-pin bumps
(apps/flank/app/routes/character-recipes.tsx:55-112). Brisket accepts only the
registered kinds and variants in its trusted registry
(apps/brisket/src/features/characters-v2/recipe/capabilities.ts:12-82) and rejects
unknown or malformed recipe data instead of treating JSON as executable UI
(parseCharacterRecipe in
apps/brisket/src/features/characters-v2/recipe/parse-character-recipe.ts,
apps/brisket/src/features/characters-v2/recipe/validate-character-recipe.ts:35-67).
flowchart LR A["Flank: author recipe"] -->|"draft / validate / publish"| S["Sirloin: recipe + session authority"] B["Brisket: trusted recipe runner"] -->|"action id + revision + inputs"| S S -->|"pinned workflow UUID + digest + bound inputs"| R["Brain: CHARACTER_COMPUTE"] R -->|"terminal outputs + asset metadata"| S S -->|"authoritative stage, operation and projection"| B
Capability vocabulary
Section titled “Capability vocabulary”The contract intentionally uses a finite vocabulary:
| Capability | What the recipe can choose | Guardrail |
|---|---|---|
| Stage | Form, upload, review, asset picker, confirmation, or status; plus a reviewed variant and presentation copy. | Brisket and Sirloin both reject unsupported kinds or variants. |
| Field | Text, option grid, chips, color, multi-select, or media upload with validation and visibility conditions. | Requiredness, media limits, keys, conditions, and upload shape are validated before publication. |
| Action | Save, upload, execute, select asset, authorize, or local; label, role, bindings, running stage, entitlement, and successful transition. | Sirloin resolves the action from the pinned recipe; the client cannot invent semantics. |
| Transition | Stay, next visible stage, back through history, or explicit stage. | Targets must exist in the same flow and conditional chains require an unconditional fallback. |
| Condition | Exists, equals, all, any, or negation over allowed session paths. | Conditions are declarative and path-validated; they are not expressions or scripts. |
| Workflow pin | Recipe-local workflow id mapped to a published Brain UUID, graph digest, input bindings, output schema, and asset-output cardinality. | Execution uses the immutable UUID and digest, never a workflow name or latest version. |
| Billing policy | Recipe-local credits and included-use count, referenced by catalog display and/or an action. | Declaring a catalog price does not charge; only executing an action that references the policy can reserve credits. |
The wire vocabulary is defined in
proto/tbone/v1/character_recipe.proto:104-148,
proto/tbone/v1/character_recipe.proto:150-221,
proto/tbone/v1/character_recipe.proto:223-324, and
proto/tbone/v1/character_recipe.proto:326-377. Adding a new arrangement of
existing capabilities is a recipe change. Adding a genuinely new stage kind,
control, action semantic, condition operator, or trusted component is a contract and
consumer change and must not be simulated through unvalidated props.
Server-authoritative navigation
Section titled “Server-authoritative navigation”Stage order is useful but not sufficient to determine progression. Sirloin owns the
current stage and visited-stage history. It evaluates action conditions, back
barriers, explicit transitions, and return-to-caller behavior
(apps/sirloin/internal/pkg/storage/characterrecipecapabilities.go:356-383,
apps/sirloin/internal/pkg/storage/characterrecipecapabilities.go:406-451,
apps/sirloin/internal/pkg/storage/charactersessionaction.go:327-369,
apps/sirloin/internal/pkg/storage/charactersessionaction.go:503-544).
The following semantics are load-bearing:
NEXTselects the next visible stage and requires an unconditional fallback.STAGEtargets a named stage in the same flow.BACKfollows server history and respects barriers. The action role controls presentation:BACKis reserved for the header control, whileSECONDARYwith aBACKtransition is a visible body action that returns through the same history.STAYperforms an intentional action without implying navigation.return_to_caller_if_presentlets one local form return to the stage that opened it, while retaining a declaredNEXTfallback when used as the entry flow.- An automatic action is a server-owned
AUTHORIZEfinalization with workflow and billing-policy references. It must require an active subscription, be unique within its stage, and leave the stage. Publication rejects other automatic action shapes and automaticSTAYactions (apps/sirloin/internal/pkg/storage/characterrecipecapabilities.go:183-190,apps/sirloin/internal/app/services/characterrecipecapabilityvalidation.go:444-450,apps/sirloin/internal/app/services/characterrecipecapabilityvalidation.go:532-545,apps/sirloin/internal/app/services/characterrecipecapabilityvalidation.go:661-667).
Brisket may optimistically show a recipe-authored running stage, but it still waits
for the server session to confirm progression. It distinguishes stage-leaving
actions from intentional STAY actions so a successful non-navigating action does
not remain falsely pending
(apps/brisket/src/features/characters-v2/views/creation/recipe-runner/components/action-buttons/index.tsx:14-60,
apps/brisket/src/features/characters-v2/views/creation/recipe-runner/stage-actions/hooks/use-stage-action-controls.ts:147-173).
Local data is presentation state, not authority
Section titled “Local data is presentation state, not authority”presentation.*.local_data supports temporary browser-owned values such as a photo
held before subscription. It cannot participate in server conditions, billing,
workflow inputs, or persisted session state
(proto/tbone/v1/character_recipe.proto:107-112). A LOCAL action is still sent to
Sirloin for the authoritative transition; local values do not turn the browser into
the navigation authority (proto/tbone/v1/character_recipe.proto:219-223).
Initial RI starts at the paywall, which is its only unsubscribed-only stage.
After checkout, the validation upload stage asks the user to select a photo and
pins the reference-validation workflow
(apps/flank/app/fixtures/character-recipes/initial-ri-sfw.json).
Older pinned recipe versions may still begin with a pre-paywall upload and
contain the legacy postPaywallUploadStageId property. Brisket accepts those
recipes only when they otherwise satisfy the current recipe schema. It ignores
the declaration and treats their post-paywall upload as manual. During the
forward-only POC, incompatible versions must be discarded or republished.
Sirloin retains the property allowlist only so compatible legacy versions remain
readable
(apps/sirloin/internal/app/services/characterrecipepropsvalidation.go).
Compute is bound by contract, not by stage name
Section titled “Compute is bound by contract, not by stage name”An execute/upload/authorize action references a recipe-local workflow id. Sirloin
resolves its value and asset bindings into a validated input snapshot, verifies the
pinned Brain workflow UUID and graph digest, and only then reserves billing and
creates the queued execution
(apps/sirloin/internal/pkg/storage/characterexecution.go:319-365). The full node
graph remains in Brain; the recipe stores only the immutable pin and the contract
projection (proto/tbone/v1/character_recipe.proto:293-313).
Brain runs CHARACTER_COMPUTE through its existing workflow execution machinery and
returns terminal outputs and asset metadata through the character-completion
webhook. Sirloin binds those assets against the recipe contract; Brain does not
write character or billing state
(apps/brain/src/modules/domain/workflow/services/sirloin-webhook.service.ts:51-92).
Therefore workflow invocation is attached to an action, not inferred from a particular stage position. An author can make either Continue or Skip start provisioning, provided each action has the required workflow, input bindings, billing policy when applicable, running stage, and successful transition. Moving a stage does not implicitly move a charge or compute call.
Account routing, entitlement, and billing are separate
Section titled “Account routing, entitlement, and billing are separate”Stage-level stage_visibility defines account-owned prerequisites. EVERYONE is
unconditional; UNSUBSCRIBED_ONLY is visible without an active paid subscription;
NSFW_UNSUBSCRIBED_ONLY is visible without an active full-access (18+)
subscription; and KYC_UNVERIFIED_ONLY is visible until the account has a usable
approved identity, including the identification ID and selfie required by
character workflows, or an explicit Strip bypass. Historical age-only approval
does not satisfy this identity gate.
The reviewed confirmation.paywall, confirmation.full-access-paywall, and
confirmation.kyc-verification renderers correspond to those three gates. The
ordinary paywall keeps the existing subscription checkout layout; full-access has
its own stage variant and layout instead of branching the ordinary paywall view.
Sirloin resolves stage visibility on Start, Get, and before and after actions, and prunes
skipped account gates from Back history. The same prerequisite resolution applies
to creation, capability-granting, and standalone management flows, so checkout or
KYC completion can advance the current flow without a client-authored Continue
action, while a later eligibility loss rewinds it to the earliest visible account
gate in that flow.
Each account gate requires a later unconditional EVERYONE successor and cannot
carry a back/history barrier
(apps/sirloin/internal/app/services/characterrecipecapabilityvalidation.go:153-188).
Brisket starts checkout or KYC from the reviewed renderer, then refetches the
session and renders the authoritative stage Sirloin returns.
Adult-enablement KYC uses the account-scoped CHARACTER_ADULT_ENABLEMENT purpose.
It does not persist a legacy pending RI/VI type, and an approved callback does not
create the legacy KYC gift character
(apps/sirloin/internal/app/services/kyc/startkycverification.go:56-70,
apps/sirloin/internal/app/services/characters/ondatowebhook.go:704-725).
An action-level active-subscription entitlement is server authorization for a
protected operation. It neither selects checkout UI nor changes stage routing,
and it is not itself a credit charge. Protected actions may only live on
EVERYONE stages, and every authored path to them must first cross a
subscription gate appropriate to the protected operation; publication rejects a
branch that reaches or jumps to a protected action before that gate. A
billing_policy_id on the action selects the charge policy for that specific
operation
(apps/sirloin/internal/app/services/characterrecipecapabilityvalidation.go:131-190,
proto/tbone/v1/character_recipe.proto:269-291).
For a billed action, Sirloin validates inputs first, then reserves the account’s
first-character allocation or credits before it inserts the execution. Every billed
action that invokes a character.provision workflow can consume that one-time global
allocation, regardless of whether its action kind is AUTHORIZE or EXECUTE.
Insufficient credits reject the action before compute starts
(reserveCharacterActionBilling in
apps/sirloin/internal/pkg/storage/characterbilling.go,
apps/sirloin/internal/pkg/storage/characterexecution.go:319-365). Successful
terminal completion captures the reservation; failure releases it
(settleCharacterBilling in
apps/sirloin/internal/pkg/storage/characterbilling.go). This prevents a recipe
from finalizing expensive work and charging afterward.
The catalog’s billing policy reference exists only to show the matching price before the user starts. It must not be used to infer where charging happens.
Rendering and asynchronous states are recipe-aware but code-owned
Section titled “Rendering and asynchronous states are recipe-aware but code-owned”Brisket’s runner derives the current renderer from the stage kind and reviewed
variant, keeps the server stage authoritative, and shows a stable unsupported state
if the pinned stage is absent
(apps/brisket/src/features/characters-v2/views/creation/recipe-runner/stage-renderer/index.tsx:69-95,
apps/brisket/src/features/characters-v2/views/creation/recipe-runner/index.tsx:92-108).
When a session is running, action controls are locked so repeated clicks cannot
submit the same transition. The processing-status renderer deliberately keeps only
plain text draft fields editable while non-text inputs and actions remain locked
(apps/brisket/src/features/characters-v2/views/creation/recipe-runner/index.tsx:49-50,
apps/brisket/src/features/characters-v2/views/creation/recipe-runner/stages/status/processing/index.tsx:119-154).
Each recipe-runner stage view owns the arrangement of its fields, media, and
controls. Shared presentation primitives remain reusable, but progression stays in
the generic runner and Sirloin. Management uses an explicit feature kind. Name,
Likeness, and Settings each have a code-owned Brisket component, while CAPABILITY
and FLOW launch reviewed named flows in the generic runner. Direct launcher
section URLs canonicalize to the management overview; the launcher has no empty
focused panel. Sirloin publication and Brisket parsing enforce matching
renderer/field/action boundaries, including the bespoke recipe-runner stages and
bounded management shapes
(apps/sirloin/internal/app/services/characterrecipevalidation.go:603-887,
apps/brisket/src/features/characters-v2/recipe/validate-character-recipe.ts:244-748).
COMING_SOON management sections are display-only entries with recipe-owned
title, description, and optional icon. They have no flow, summary, availability
condition, or discard behavior. Sirloin projects them as unavailable with reason
coming_soon; Brisket keeps them visible as disabled buttons with a “Coming soon”
label and returns direct section URLs to the overview. Existing management kinds
retain their behavior (proto/tbone/v1/character_recipe.proto).
Both VI recipes place an adult capability launcher between Name and Enhance me.
Their adult flow reuses the full-access paywall, settings form, processing reveal,
and success renderers. Accepting the preview is a synchronous SAVE to success;
Adjust settings returns to the form for another derivation. Female VI derivation
preserves existing body proportions without a bust-size input. Existing RI graph
pins and identity-verification flows remain unchanged
(apps/flank/app/fixtures/character-recipes/initial-vi-sfw.json,
apps/flank/app/fixtures/character-recipes/initial-vi-male-sfw.json,
apps/brain/src/modules/domain/workflow/poc-fixtures/character-compute.fixtures.ts).
Sirloin checks the stored character type when granting adult capability. Virtual
characters use a character-scoped virtual:<character-id> anchor and instant
verification; this does not occupy a human identity’s RI slot or claim KYC approval.
The VI preview must contain all three NSFW roles generated successfully in the
current session, derived from the corresponding generated SFW references for that
character. RI identity, attestation, and slot checks retain their existing rules
(apps/sirloin/internal/pkg/storage/characteradult.go,
apps/sirloin/internal/pkg/storage/charactersessioncommit.go).
The stage title and description define its ready-state copy. Recipe props may
provide loading, processing, failure, and finalization-failure overrides; renderers
supply conservative fallbacks through stageStateCopy in
apps/brisket/src/features/characters-v2/views/creation/recipe-runner/stage-copy.ts.
Photo validation is one shared stage variant for face, full-body, and alternate-angle
roles. Its passed title and description come from the stage, while
processingTitle, processingDescription, and successLabel are validated recipe
overrides. Older pinned versions fall back to their own stage copy, not renderer-owned
product wording. The supplied photo remains visible during validation, and success
means that it passed requirements—not that a character or dataset is complete
(apps/brisket/src/features/characters-v2/views/creation/recipe-runner/index.tsx:123-163,
apps/brisket/src/features/characters-v2/views/creation/recipe-runner/stage-renderer/index.tsx:150-165,
apps/brisket/src/features/characters-v2/views/creation/recipe-runner/stages/photo-validation/index.tsx:25-142,
and apps/sirloin/internal/app/services/characterrecipepropsvalidation.go:118-126).
Current VI recipes show one generated character gallery. Appearance changes and re-rolls generate a replacement gallery, then return to the mirror reveal. Accepting the gallery commits the character and opens success. The discarded multi-candidate VI recipe and its dedicated compute workflows are not retained; the generic asset-picker component remains reusable.
Authoring, fixtures, and promotion
Section titled “Authoring, fixtures, and promotion”The fixture JSON files are executable examples and contract-test inputs. They are
not a production migration system. The bootstrap seed planner fills only an empty or
draft-only route and deliberately skips a route with a published active version
(apps/flank/app/lib/character-recipe-seed.ts:10-36). The script likewise refuses
to overwrite a published recipe and requires intentional changes to be made through
Flank or after explicitly clearing disposable local data
(apps/flank/scripts/seed-character-recipes.ts:1-8,
apps/flank/scripts/seed-character-recipes.ts:43-68).
POC Brain workflow fixtures are also opt-in. The startup service returns without
touching workflow storage unless
BRAIN_CHARACTER_COMPUTE_FIXTURE_SEED_ENABLED=true; when enabled, it creates and
publishes missing fixtures but skips existing published rows
(apps/brain/src/modules/domain/workflow/poc-fixtures/character-compute-fixture-seed.service.ts:12-70).
Preview and production must leave both recipe and Brain fixture seeding disabled.
The promotion order is:
- Author and publish a Brain workflow with purpose
CHARACTER_COMPUTE. - Record its UUID, graph digest, input binding contract, output schema, and asset outputs in the recipe workflow pin.
- Save the recipe as a draft and resolve every validation issue.
- Publish the Sirloin recipe version to move the route.
- Test through the Characters V2 feature flag before any broader rollout.
Strict typed management clients must ship after every active and rollback-routable recipe route contains the Name, Likeness, and Settings feature catalog. The current forward-only POC may discard old local data; a released compatibility window would instead require retaining the legacy Brisket renderer until route migration ends.
Adult Likeness slots are not part of the direct visual-only Likeness panel in this milestone. The adult-enablement FLOW already supports grouped adult uploads, as described in the recipe engine. Before a recipe exposes adult slots in a direct panel, Sirloin must derive slot visibility from the character’s effective adult capability and omit inaccessible adult slot metadata and signed asset URLs from the customer projection. Brisket must render that server-resolved view rather than infer eligibility or hide already-delivered adult assets.
Publishing Brain alone never changes a live recipe. Publishing the recipe is the explicit ship gate. This store split is governed by Character compute graphs pin by Brain workflow UUID.
Extension rules for authors and agents
Section titled “Extension rules for authors and agents”Use this decision table before changing code:
| Desired change | Recipe-only? | Required work |
|---|---|---|
| Rename a recipe, stage, field, option, button, or state copy | Yes | Edit catalog/stage/action data; publish a new recipe version. |
| Reorder unconditional stages | Usually | Recheck NEXT, conditional fallbacks, running stages, back barriers, and final stage validity. |
| Insert a supported form, upload, confirmation, review, picker, or status stage | Usually | Use a registered kind/variant and valid actions/bindings. |
| Make Continue or Skip start compute | Yes | Put the workflow and optional billing policy on that exact action; define running and success destinations. |
| Reuse a creation form as an editor | Yes | Use a LOCAL action with NEXT plus return_to_caller_if_present; do not duplicate the form. |
| Reorder management cards or change their bounded copy/options | Yes | Keep exactly one Name, Likeness, and Settings feature and satisfy that feature’s validated envelope. |
| Add a new management interaction or lifecycle | No | Add a reviewed feature kind, Sirloin validation/runtime behavior, a dedicated Brisket component, Flank support, and cross-consumer tests. Do not author management stages or transitions to emulate it. |
| Change Brain compute internals without changing its contract | Brain workflow only, then repin | Publish a new workflow UUID and explicitly bump the recipe pin. |
| Change workflow inputs, outputs, or asset roles | No single-file change | Update Brain graph contract and recipe pin together; validate every consumer. |
| Explain to a customer why compute declined their input | No single-file change | Return the verdict and its message in the workflow’s declared output and render that; do not surface the execution error field, which carries unfiltered engine text. |
| Let a compute step reject input as an expected outcome | No single-file change | Complete the workflow successfully with a declining verdict; reserve execution failure for faults. Give the accept-only asset roles minCount: 0. |
| Add a new UI interaction that no registered variant supports | No | Add a reviewed contract capability, Sirloin validation/runtime semantics, Brisket renderer, Flank authoring support, and tests. |
Add arbitrary JavaScript, component names, URLs, or condition expressions through props |
No | Rejected by design; add a bounded typed capability instead. |
| Make a recipe bypass billing, authorization, ownership, or asset validation | No | Forbidden; those are Sirloin invariants. |
Before publishing a changed recipe, verify these invariants:
- Every stage/action/field key is stable and unique in its scope.
- Every transition has an explicit kind and a reachable valid target.
- Conditional stage chains end in an unconditional fallback.
- Automatic actions are unique, entitled, and stage-leaving.
- Workflow and billing behavior is attached to the intended action rather than inferred from layout.
- Required value and asset bindings are satisfiable at the action’s source stage.
- Running stages are unconditional and accurately describe the in-flight work.
- Output schemas and asset roles/cardinality match the pinned Brain workflow.
- A workflow that can decline declares its verdict field as required in
outputSchema, and the asset roles it produces only when accepting useminCount: 0so a decline is not rejected at commit. - Presentation media uses approved first-party paths and alt text.
- Existing session compatibility is considered before deleting or renaming keys.
Production isolation
Section titled “Production isolation”Characters V2 has two Brisket gates. The characters route renders CCV4 unless the
characters-v2 PostHog flag is true
(apps/brisket/src/app/(main-layout)/characters/page.tsx:1-28), and the Characters
V2 tRPC router independently rejects direct access for accounts without that flag
(apps/brisket/src/server/api/routers/characters-v2.ts:25-40). Consequently, the
recipe UI and API are not exposed merely because the code and recipe rows exist.
This isolation does not by itself declare the POC production-ready. Public rollout still depends on the reliability and capacity deferrals in the Character Recipe Engine, including durable Brain completion delivery and dispatch idempotency. Feature-flag removal is a separate release decision.
Consequences
Section titled “Consequences”Benefits
Section titled “Benefits”- Product teams can create materially different onboarding journeys by composing a bounded vocabulary instead of adding recipe-specific service branches.
- VI/RI labels remain presentation concepts; character storage and lifecycle stay uniform.
- Stage order, compute, entitlement, and billing are explicit and independently reviewable.
- Workflow changes are versioned and pinned, so a Brain publish cannot silently alter a customer flow.
- The same forms and reference-update flows can be reused without copying UI code.
- Invalid or unsupported recipe capabilities fail before publication or at a stable customer boundary rather than executing partially.
- The feature can ship dark behind Characters V2 gates while CCV4 remains the default.
Costs and risks
Section titled “Costs and risks”- Recipe publication becomes a real release boundary and needs the same review care as code.
- Flexibility is bounded by the trusted capability registry. A genuinely new interaction still requires coordinated contract, validation, and renderer work.
- Stage/action JSON can be structurally valid yet product-confusing. Automated validation cannot replace flow walkthroughs.
- Changing stable keys can strand pinned sessions. Forward-only POC cleanup is acceptable while unreleased; post-release compatibility requires an explicit migration policy.
- Brain and recipe fixtures are useful for local POC bootstrap but are not an operator-safe production provisioning mechanism.
Rollback
Section titled “Rollback”- Recipe-only regression: route the recipe key back to a previously published version that still passes the current publication and workflow-pin checks.
- Compute regression: republish a recipe version pinned to the previous Brain workflow UUID and digest.
- Customer-surface regression: disable the
characters-v2feature flag; CCV4 remains the route fallback. - Do not roll back by editing immutable published bytes, following latest workflow by name, or mutating sessions in place.
Alternatives Considered
Section titled “Alternatives Considered”- Hardcode one React/backend flow per recipe. Rejected because every product rearrangement would require coordinated releases and duplicate session, billing, and error handling.
- Add VI/RI/subtype columns and switch statements throughout the stack. Rejected because these labels describe recipe provenance, not different persistence or lifecycle semantics.
- Let recipes name arbitrary React components or execute scripts. Rejected because recipe publication would become code deployment and bypass review, security, and compatibility boundaries.
- Make Brain own the entire onboarding and character lifecycle. Rejected because Brain is the compute engine; it should not own customer session, recipe routing, entitlement, billing, or durable character authority.
- Infer compute and billing from stage names or positions. Rejected because reordering a recipe would silently move side effects. Actions explicitly own workflow and billing references.
- Always use the latest Brain workflow by name. Rejected because a workflow publish would change active funnels without a recipe review or rollback pin.
- Automatically overwrite published recipes/workflows from fixtures on startup. Rejected because deployment would become an implicit production migration and could replace operator-authored definitions.