@salsita-ai/sdk 0.49.0-develop.d4bea2382c → 0.49.0-develop.fc391028c9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/application-spec/generate-application/code/templates/env-example.js +1 -0
  2. package/dist/application-spec/generate-application/code/templates/phoenix-skill.js +1 -1
  3. package/dist/application-spec/parse-application-files/raw-spec/raw-assistant.d.ts +6 -3
  4. package/dist/application-spec/parse-application-files/raw-spec/raw-assistant.js +1 -1
  5. package/dist/application-spec/parse-application-files/spec/sections/assistant/assistant-field-type.js +1 -1
  6. package/dist/application-spec/parse-application-files/spec/sections/assistant/assistant-schema.js +1 -1
  7. package/dist/configurator/application-runtime/extensions/systems/graphql-system/entity-meta.d.ts +32 -32
  8. package/dist/configurator/application-runtime/extensions/systems/graphql-system/entity-meta.js +1 -1
  9. package/dist/configurator/application-runtime/extensions/systems/graphql-system/graphql-system.js +1 -1
  10. package/dist/docs/index.json +1 -1
  11. package/dist/docs/phoenix/csp.mdx +6 -2
  12. package/dist/docs/phoenix/phoenix-cli.mdx +19 -17
  13. package/dist/docs/phoenix/sdk-builder-workflow.mdx +9 -5
  14. package/dist/domain/application/application-config.d.ts +3 -1
  15. package/dist/domain/application/assistant-mutations.d.ts +1 -1
  16. package/dist/domain/application/assistant-mutations.js +1 -1
  17. package/dist/legacy/domain-cli/utils/cache.d.ts +1 -1
  18. package/dist/legacy/domain-cli/utils/cache.js +1 -1
  19. package/dist/legacy/phoenix-cli/commands/dev.d.ts +2 -0
  20. package/dist/legacy/phoenix-cli/commands/dev.js +5 -5
  21. package/dist/legacy/phoenix-cli/project-gen/generators/file-templates/env-example.d.ts +1 -1
  22. package/dist/legacy/phoenix-cli/project-gen/generators/file-templates/env-example.js +1 -0
  23. package/dist/phoenix/application-spec/generate-assistant-prompt.js +1 -1
  24. package/dist/phoenix/application-spec/project-files.js +1 -1
  25. package/dist/phoenix/cli.js +1 -1
  26. package/dist/phoenix/commands/deploy.js +2 -2
  27. package/dist/phoenix/commands/dev.js +4 -4
  28. package/dist/phoenix/commands/init.d.ts +2 -1
  29. package/dist/phoenix/commands/init.js +1 -1
  30. package/dist/phoenix/commands/login.js +2 -2
  31. package/dist/phoenix/commands/sync.js +1 -1
  32. package/dist/phoenix/services/cli-credentials.d.ts +3 -0
  33. package/dist/phoenix/services/cli-credentials.js +1 -1
  34. package/dist/phoenix/utils/index.d.ts +1 -1
  35. package/dist/phoenix/utils/index.js +1 -1
  36. package/dist/phoenix/utils/package-info.d.ts +9 -0
  37. package/dist/phoenix/utils/package-info.js +1 -1
  38. package/package.json +3 -3
@@ -46,6 +46,7 @@ export const generateApplicationSpecEnvExample=()=>`# This file documents develo
46
46
 
47
47
  # deployment:prepare-dev and deployment:prepare-local output and behavior overrides:
48
48
  # OUT_FILE=.env.generated
49
+ # SKIP_UPLOAD also applies to the deployment phoenix dev prepares at startup.
49
50
  # SKIP_UPLOAD=false
50
51
  # OVERRIDE_SCHEMA=true
51
52
 
@@ -1 +1 @@
1
- import{renderAgentSkill as e}from"./common/agent-skill.js";import{sdkGuidanceSections as t}from"./common/sdk-content.js";export const followsCanaryBuilds=e=>!e.useWorkspace&&e.sdkVersion.includes("-develop.");export const generatePhoenixSkill=({checkLatestCanary:n})=>e({name:"phoenix",description:`Phoenix SDK project conventions, covering project structure, architecture principles, key patterns, file and export rules, webhook membership, and how to search the SDK documentation${n?", and the internal-beta check for the latest canary SDK build":""}. Use before writing or changing specs or source code in this Phoenix configurator project.`,sections:[...n?["## Stay on the Latest Canary\nDuring the internal beta this project uses the `canary` build of the SDK, which is published from every change to the platform. Once per conversation, before you change anything, compare the installed version with the newest canary:\n```bash\npnpm list @salsita-ai/sdk --depth 0\nnpm view @salsita-ai/sdk@canary version\n```\nWhen they differ, tell the user which version you are moving to, then update the project and regenerate its files:\n```bash\npnpm add @salsita-ai/sdk@canary\npnpm phoenix sync\n```\n`sync` rewrites the Phoenix skills, so read the ones you use again after it. When the user runs a globally installed `phoenix`, update that too with `npm install -g @salsita-ai/sdk@canary`. If the project no longer builds after the update, stop and tell the user; the `phoenix-feedback` skill covers reporting it."]:[],t.projectStructure,t.architecturePrinciples,t.keyPatterns,t.fileConventions,t.exportRules,t.webhookMembership,t.doNotEdit,"## Code Review\nUse the `phoenix-review` skill to review code changes before committing.",t.sdkDocumentation]});
1
+ import{renderAgentSkill as e}from"./common/agent-skill.js";import{sdkGuidanceSections as t}from"./common/sdk-content.js";export const followsCanaryBuilds=e=>!e.useWorkspace&&e.sdkVersion.includes("-develop.");export const generatePhoenixSkill=({checkLatestCanary:n})=>e({name:"phoenix",description:`Phoenix SDK project conventions, covering project structure, architecture principles, key patterns, file and export rules, webhook membership, showing the user the dev preview and the build of a deploy, and how to search the SDK documentation${n?", and the internal-beta check for the latest canary SDK build":""}. Use before writing or changing specs or source code in this Phoenix configurator project.`,sections:[...n?["## Stay on the Latest Canary\nDuring the internal beta this project uses the `canary` build of the SDK, which is published from every change to the platform. Once per conversation, before you change anything, compare the installed version with the newest canary:\n```bash\npnpm list @salsita-ai/sdk --depth 0\nnpm view @salsita-ai/sdk@canary version\n```\nWhen they differ, tell the user which version you are moving to, then update the project and regenerate its files:\n```bash\npnpm add @salsita-ai/sdk@canary\npnpm phoenix sync\n```\n`sync` rewrites the Phoenix skills, so read the ones you use again after it. When the user runs a globally installed `phoenix`, update that too with `npm install -g @salsita-ai/sdk@canary`. If the project no longer builds after the update, stop and tell the user; the `phoenix-feedback` skill covers reporting it."]:[],t.projectStructure,t.architecturePrinciples,t.keyPatterns,t.fileConventions,t.exportRules,t.webhookMembership,t.doNotEdit,"## Show the Preview While You Work\nThe user should see the configurator change as you build it. Before your first change in a conversation, make sure `pnpm phoenix dev` is running (start it in the background when it is not) and open the address it prints as `Preview: <url>` (also written to `.phoenix/phoenix-dev.log`) in the user's browser: `open '<url>'` on macOS, `xdg-open '<url>'` on Linux, `Start-Process '<url>'` in PowerShell, `start \"\" \"<url>\"` in cmd. Keep the quotes, the shell would otherwise read a `?` in the address as a pattern. The log keeps the lines of earlier runs, so when you have just started dev, wait for the `Preview:` line written after your start and use the last one. Open it once; the preview reloads on every change, so tell the user to keep that tab open instead of opening it again.","## Show the Build After a Deploy\n`pnpm phoenix deploy` starts a build on the server and waits for it to go live. Right after `Build <id> started` it prints `Follow the build in Studio: <url>`. Run the deploy so that you can read its output while it runs, and open that link in the user's browser, quoted as above, as soon as it appears, so they can follow the build they just triggered. When the line is missing, the sign-in is older than the link: tell the user the build is listed under Deployments in Studio, and that `pnpm phoenix login <workspace>` brings the link back.","## Code Review\nUse the `phoenix-review` skill to review code changes before committing.",t.sdkDocumentation]});
@@ -19,23 +19,26 @@ export declare const ASSISTANT_OPTION_COMPONENT_TYPES: readonly ['materialSwatch
19
19
  export type AssistantOptionComponentType = (typeof ASSISTANT_OPTION_COMPONENT_TYPES)[number];
20
20
  /**
21
21
  * What the integrator promises about a list of choices, so the agent can read it once and reuse it:
22
- * `sameForAllInstances` when every entity of the type gets the same list, `stable` when the list
22
+ * `sameForAllInstances` when every entity of the type gets the same list, `constantDuringSession` when the list
23
23
  * does not change during a session. A hint only adds a sentence to a generated description; nothing
24
24
  * verifies it.
25
25
  */
26
- export declare const ASSISTANT_HINTS: readonly ['sameForAllInstances', 'stable'];
26
+ export declare const ASSISTANT_HINTS: readonly ['sameForAllInstances', 'constantDuringSession'];
27
27
  export type AssistantHint = (typeof ASSISTANT_HINTS)[number];
28
28
  /**
29
29
  * The component that holds the choices of a writable field: the instance `instanceName` of the
30
30
  * system component `componentType` on the entity type. It generates the `<field>Options` edge under
31
31
  * `meta`, named by `as` when given, and makes `modify<Type>` reject a value outside the choices
32
- * unless `validate` is `false`. The component is named explicitly and never inferred from a `$bind`.
32
+ * unless `validate` is `false`. `allowDryRuns: false` turns the dry-run of the choices off, for
33
+ * fields with too many of them to try out. The component is named explicitly and never inferred from
34
+ * a `$bind`.
33
35
  */
34
36
  export interface RawAssistantFieldOptions {
35
37
  readonly componentType: AssistantOptionComponentType;
36
38
  readonly instanceName: string;
37
39
  readonly as?: string;
38
40
  readonly validate?: boolean;
41
+ readonly allowDryRuns?: boolean;
39
42
  readonly hints?: readonly AssistantHint[];
40
43
  }
41
44
  /**
@@ -1 +1 @@
1
- import{z as t}from"zod";import{ASSISTANT_AI_PROVIDERS as e,ASSISTANT_MODEL_TIERS as o,ASSISTANT_REASONING_LEVELS as i}from"@salsita-ai/contracts/assistant";export const ASSISTANT_OPTION_COMPONENT_TYPES=["materialSwatch","attributeSelection","distanceDimensionSelect"];export const ASSISTANT_HINTS=["sameForAllInstances","stable"];let n=t.string().regex(/^(?!__)[_A-Za-z][_0-9A-Za-z]*$/,'must be a GraphQL name: letters, digits and "_", not starting with a digit or "__"'),r=e=>t.enum(e,{errorMap:()=>({message:`must be one of: ${e.join(", ")}`})}),s=t.object({description:t.string().optional(),as:n.optional()}).strict(),a=t.array(r(ASSISTANT_HINTS)),c=t.object({options:t.object({componentType:r(ASSISTANT_OPTION_COMPONENT_TYPES),instanceName:t.string().min(1,"must not be empty; name the component instance that holds the choices"),as:n.optional(),validate:t.boolean().optional(),hints:a.optional()}).strict().optional()}).strict(),l=t.object({as:t.string().trim().min(1,"must not be empty; remove the key to use the instance name").optional(),description:t.string().optional(),allowDryRuns:t.boolean().optional()}).strict(),p=e=>t.object({hints:a.optional(),groups:t.record(t.string(),l.nullable(),{required_error:`is required: the recipe groups of the type go under "groups", keyed by the instance name of a "${e}" component`})}).strict(),m=`is required: declare provider (${e.join(", ")}), model (${o.join(", ")}) and reasoning (${i.join(", ")})`,d=t.object({provider:r(e),model:r(o),reasoning:r(i)},{required_error:m,invalid_type_error:m}).strict();export const rawAssistantSchema=t.object({entities:t.record(t.string(),t.record(t.string(),s.nullable())).optional(),deletable:t.record(t.string(),t.object({}).strict().nullable()).optional(),writable:t.record(t.string(),t.record(t.string(),c.nullable())).optional(),childRecipes:t.record(t.string(),p("childEntityRecipes")).optional(),replaceRecipes:t.record(t.string(),p("replaceEntityRecipes")).optional(),instructions:t.string().trim().min(1,"must not be empty; remove the key when there are no instructions").optional(),ai:d}).strict();
1
+ import{z as t}from"zod";import{ASSISTANT_AI_PROVIDERS as e,ASSISTANT_MODEL_TIERS as o,ASSISTANT_REASONING_LEVELS as i}from"@salsita-ai/contracts/assistant";export const ASSISTANT_OPTION_COMPONENT_TYPES=["materialSwatch","attributeSelection","distanceDimensionSelect"];export const ASSISTANT_HINTS=["sameForAllInstances","constantDuringSession"];let n=t.string().regex(/^(?!__)[_A-Za-z][_0-9A-Za-z]*$/,'must be a GraphQL name: letters, digits and "_", not starting with a digit or "__"'),r=e=>t.enum(e,{errorMap:()=>({message:`must be one of: ${e.join(", ")}`})}),s=t.object({description:t.string().optional(),as:n.optional()}).strict(),a=t.array(r(ASSISTANT_HINTS)),c=t.object({options:t.object({componentType:r(ASSISTANT_OPTION_COMPONENT_TYPES),instanceName:t.string().min(1,"must not be empty; name the component instance that holds the choices"),as:n.optional(),validate:t.boolean().optional(),allowDryRuns:t.boolean().optional(),hints:a.optional()}).strict().optional()}).strict(),l=t.object({as:t.string().trim().min(1,"must not be empty; remove the key to use the instance name").optional(),description:t.string().optional(),allowDryRuns:t.boolean().optional()}).strict(),p=e=>t.object({hints:a.optional(),groups:t.record(t.string(),l.nullable(),{required_error:`is required: the recipe groups of the type go under "groups", keyed by the instance name of a "${e}" component`})}).strict(),m=`is required: declare provider (${e.join(", ")}), model (${o.join(", ")}) and reasoning (${i.join(", ")})`,d=t.object({provider:r(e),model:r(o),reasoning:r(i)},{required_error:m,invalid_type_error:m}).strict();export const rawAssistantSchema=t.object({entities:t.record(t.string(),t.record(t.string(),s.nullable())).optional(),deletable:t.record(t.string(),t.object({}).strict().nullable()).optional(),writable:t.record(t.string(),t.record(t.string(),c.nullable())).optional(),childRecipes:t.record(t.string(),p("childEntityRecipes")).optional(),replaceRecipes:t.record(t.string(),p("replaceEntityRecipes")).optional(),instructions:t.string().trim().min(1,"must not be empty; remove the key when there are no instructions").optional(),ai:d}).strict();
@@ -1 +1 @@
1
- import{GraphQLBoolean as e,GraphQLFloat as t,GraphQLID as r,GraphQLInt as n,GraphQLList as i,GraphQLNonNull as l,GraphQLScalarType as a,GraphQLString as o,getNamedType as s}from"graphql";import{isArray as f}from"@salsita-ai/common/utils";import{COMMON_SHORT_PREFIX as u,DEFINITION_REF_PATTERN as p,INTERNAL_REF_PATTERN as d,extractPhxDefName as y}from"../../../../core/index.js";export const GraphQLJSON=new a({name:"JSON",description:"Any JSON value, returned as stored. Always plain serializable data."});let c={number:t,integer:n,string:o,boolean:e},m=new Set(["Localizable","EnhancedLocalizable","MessageDescriptor","EnhancedMessageDescriptor","MultilingualString"]),v={type:GraphQLJSON},h=e=>e.startsWith(u)?e.slice(u.length):d.exec(e)?.[2],g=e=>y(e)??p.exec(e)?.[1];export const createAssistantRefResolver=(e,t)=>r=>{let n=g(r);if(void 0!==n)return t[n]?.schema;let i=h(r);return void 0===i?void 0:e.get(i)?.schema};let S=(e,t,r=new Set)=>{if(e.$ref){if(r.has(e.$ref))return!1;let n=t(e.$ref);return void 0===n||S(n,t,new Set([...r,e.$ref]))}let n=[...e.anyOf??[],...e.oneOf??[]];return"null"===e.type||f(e.type)&&e.type.includes("null")||e.enum?.includes(null)===!0||null===e.const||n.some(e=>S(e,t,r))},O=({anyOf:e,oneOf:t})=>[...e??[],...t??[]].filter(e=>"null"!==e.type),w=(e,t,n)=>{let l;if(void 0!==(l=void 0===e.$ref?void 0:g(e.$ref))&&m.has(l))return{type:o,localized:!0};if(e.tsType)return v;if(e.$ref){var a=e.$ref;if("DomainEntityId"===y(a))return{type:r};let i=h(a),l=void 0===i||n.has(i)?void 0:t(a);return void 0!==i&&l?w(l,t,new Set([...n,i])):v}if(e.anyOf||e.oneOf)return((e,t,r)=>{let n=O(e).map(e=>w(e,t,r)),[i]=n;if(1===n.length&&i)return i;let l=n.flatMap(({type:e,enumValues:t})=>e===o&&t?[t]:[]);if(n.length<2||l.length!==n.length||e.anyOf&&e.oneOf)return v;let a=l.flatMap(e=>[...new Set(e)]);return{type:o,enumValues:e.oneOf?a.filter(e=>a.indexOf(e)===a.lastIndexOf(e)):[...new Set(a)]}})(e,t,n);if(e.enum){let t;return(t=e.enum.filter(e=>null!==e)).every(e=>"string"==typeof e)?{type:o,enumValues:t}:v}if(f(e.type)){let r=e.type.filter(e=>"null"!==e),[i]=r;return 1===r.length&&i?w({...e,type:i},t,n):v}if("array"===e.type){var u=e.items;if(!u||f(u))return v;let r=$(u,t,n);return s(r.type)===GraphQLJSON?v:{type:new i(r.type),enumValues:r.enumValues,localized:r.localized}}let p="string"==typeof e.type?c[e.type]:void 0;return p?{type:p}:v},$=(e,t,r)=>{let n=w(e,t,r);return{...n,type:S(e,t)?n.type:new l(n.type)}},x=(e,t,r)=>{let n=h(e),i=void 0===n||r.has(n)?void 0:t(e);if(void 0!==n&&i)return void 0===i.description&&i.$ref?x(i.$ref,t,new Set([...r,n])):i.description};export const assistantFieldType=(e,t)=>{let r=O(e),n=e.$ref??(1===r.length?r[0]?.$ref:void 0),i=void 0===e.description&&n?x(n,t,new Set):e.description;return{...$(e,t,new Set),...void 0===i?{}:{description:i}}};
1
+ import{GraphQLBoolean as e,GraphQLFloat as t,GraphQLID as r,GraphQLInt as n,GraphQLList as i,GraphQLNonNull as l,GraphQLScalarType as a,GraphQLString as o,getNamedType as s}from"graphql";import{isArray as u}from"@salsita-ai/common/utils";import{COMMON_SHORT_PREFIX as f,DEFINITION_REF_PATTERN as p,INTERNAL_REF_PATTERN as d,extractPhxDefName as y}from"../../../../core/index.js";export const GraphQLJSON=new a({name:"JSON",description:"Any JSON value, returned as stored. Always plain serializable data."});let c={number:t,integer:n,string:o,boolean:e},m=new Set(["Localizable","EnhancedLocalizable","MessageDescriptor","EnhancedMessageDescriptor","MultilingualString"]),v={type:GraphQLJSON},h=e=>e.startsWith(f)?e.slice(f.length):d.exec(e)?.[2],g=e=>y(e)??p.exec(e)?.[1];export const createAssistantRefResolver=(e,t)=>r=>{let n=g(r);if(void 0!==n)return t[n]?.schema;let i=h(r);return void 0===i?void 0:e.get(i)?.schema};let S=(e,t,r=new Set)=>{if(e.$ref){if(r.has(e.$ref))return!1;let n=t(e.$ref);return void 0===n||S(n,t,new Set([...r,e.$ref]))}let n=[...e.anyOf??[],...e.oneOf??[]];return"null"===e.type||u(e.type)&&e.type.includes("null")||e.enum?.includes(null)===!0||null===e.const||n.some(e=>S(e,t,r))},O=({anyOf:e,oneOf:t})=>[...e??[],...t??[]].filter(e=>"null"!==e.type),w=(e,t,n)=>{let l;if(void 0!==(l=void 0===e.$ref?void 0:g(e.$ref))&&m.has(l))return{type:o,localized:!0};if(e.tsType)return v;if(e.$ref){var a=e.$ref;if("DomainEntityId"===y(a))return{type:r};let i=h(a),l=void 0===i||n.has(i)?void 0:t(a);return void 0!==i&&l?w(l,t,new Set([...n,i])):v}if(e.anyOf||e.oneOf)return((e,t,r)=>{let n=O(e).map(e=>w(e,t,r)),[i]=n;if(1===n.length&&i)return i;let l=n.flatMap(({type:e,enumValues:t})=>e===o&&t?[t]:[]);if(n.length<2||l.length!==n.length||e.anyOf&&e.oneOf)return v;let a=l.flatMap(e=>[...new Set(e)]);return{type:o,enumValues:e.oneOf?a.filter(e=>a.indexOf(e)===a.lastIndexOf(e)):[...new Set(a)]}})(e,t,n);if(e.enum){let t;return(t=e.enum.filter(e=>null!==e)).every(e=>"string"==typeof e)?{type:o,enumValues:t}:v}if(u(e.type)){let r=e.type.filter(e=>"null"!==e),[i]=r;return 1===r.length&&i?w({...e,type:i},t,n):v}if("array"===e.type){var f=e.items;if(!f||u(f))return v;let r=$(f,t,n);return s(r.type)===GraphQLJSON?v:{type:new i(r.type),enumValues:r.enumValues,localized:r.localized}}let p="string"==typeof e.type?c[e.type]:void 0;return p?{type:p}:v},$=(e,t,r)=>{let n=w(e,t,r);return{...n,type:S(e,t)?n.type:new l(n.type)}},x=e=>{let t=O(e);return e.$ref??(1===t.length?t[0]?.$ref:void 0)},z=(e,t,r)=>{let n=h(e),i=void 0===n||r.has(n)?void 0:t(e);if(void 0===n||!i)return;let l=x(i);return void 0===i.description&&l?z(l,t,new Set([...r,n])):i.description};export const assistantFieldType=(e,t)=>{let r=x(e),n=void 0===e.description&&r?z(r,t,new Set):e.description;return{...$(e,t,new Set),...void 0===n?{}:{description:n}}};
@@ -1 +1 @@
1
- import{GraphQLBoolean as e,GraphQLID as t,GraphQLInputObjectType as n,GraphQLInterfaceType as i,GraphQLList as r,GraphQLNonNull as o,GraphQLObjectType as s,GraphQLSchema as a,GraphQLString as p,getNullableType as l,isInputType as d,printSchema as c,validateSchema as h}from"graphql";import{DiagnosticCode as f,diagnosticMessages as y}from"../../../../errors/index.js";import{ASSISTANT_HINTS as u}from"../../../raw-spec/index.js";import{GraphQLJSON as g,assistantFieldType as m,createAssistantRefResolver as S}from"./assistant-field-type.js";let T="AnyDomainEntity",w="RootEntity",N="RecipeGroup",E="Recipe",A="RecipeDryRun",_="RecipeDryRunError",I="FieldOption",O={section:"childRecipes",component:"childEntityRecipes",mutation:"addChild"},v={section:"replaceRecipes",component:"replaceEntityRecipes",mutation:"replaceEntity"},b=new Set(["id","type","meta"]),$=new Set(["Query","Mutation","Navigation",N,E,A,_,I,"JSON","String","Int","Float","Boolean","ID"]),R=(e,t)=>{if(!t||0===t.length)return e;let n=`One of: ${t.map(e=>`"${e}"`).join(", ")}.`;return e?/[.!?]$/.test(e)?`${e} ${n}`:`${e}. ${n}`:n},M=new Set(["id","type"]),j="The raw stored value: a string, a message descriptor or a multilingual object.",D=(e,t)=>Object.entries(e).find(([e])=>e===t)?.[1],P=(e,t,{model:n,components:i})=>{let r=new Set([e,...[...n.aliases].filter(([,t])=>t.includes(e)).map(([e])=>e)]);return i.filter(({componentName:e,typeOrAlias:n})=>e===t&&r.has(n)).map(({instanceName:e})=>String(e))},U=(...e)=>u.filter(t=>e.some(e=>e?.includes(t))),C=(e,t,n,i)=>{let{model:r}=n,o=new Map,s=new Map;for(let[a,{groups:p,hints:l}]of Object.entries(e)){let e=r.entityTypes.find(e=>e===a),d=e?[e]:[...r.aliases].find(([e])=>e===a)?.[1];if(!d){i.at(a).error(f.ASSISTANT_UNKNOWN_TYPE,y.ASSISTANT_UNKNOWN_TYPE(t.section,a));continue}for(let[e,r]of(d.forEach(e=>s.set(e,U(s.get(e),l))),Object.entries(p))){let s=i.at(a,"groups",e),p=`${a}.groups.${e}`,l={groupId:r?.as??e,instanceName:e,...r?.description===void 0?{}:{description:r.description},dryRuns:r?.allowDryRuns??!0};for(let i of d){let r=P(i,t.component,n),d=o.get(i)??[],c=d.find(e=>e.group.groupId===l.groupId);r.includes(e)?c?s.error(f.ASSISTANT_DUPLICATE_RECIPE_GROUP,y.ASSISTANT_DUPLICATE_RECIPE_GROUP(t,i,l.groupId,[c.entry,p])):o.set(i,[...d,{group:l,entry:p,entryCtx:s}]):s.error(f.ASSISTANT_UNKNOWN_RECIPE_GROUP,y.ASSISTANT_UNKNOWN_RECIPE_GROUP(t,a,e,i,r))}}}let a=r.entityTypes.flatMap(e=>{let t=o.get(e);return t?[{entityType:e,groups:t}]:[]});return{groups:Object.fromEntries(a.map(({entityType:e,groups:t})=>[e,t.map(({group:e})=>e)])),hints:s,metaEntries:new Map(a.flatMap(({entityType:e,groups:[t]})=>t?[[e,t.entryCtx]]:[]))}},k=e=>`${e}Meta`,W={sameForAllInstances:e=>`All ${e} instances have the same recipe groups and recipes.`,stable:()=>"They do not change during the session."},L={sameForAllInstances:e=>`All ${e} instances have the same options.`,stable:()=>"The options do not change during the session."},F=(e,t,n)=>e.map(e=>` ${t[e](n)}`).join(""),K=({section:e},{groups:t,hints:n},{entityType:i,purpose:s,recipeGroup:a})=>{let p=t.filter(({dryRuns:e})=>!e).map(({groupId:e})=>e);return 0===t.length?{}:{[e]:{type:new o(new r(new o(a))),description:`The recipe groups for ${s} this ${i}: ${t.map(({groupId:e})=>e).join(", ")}. A group is listed only while the configurator offers it on the entity.${p.length>0?` Recipes of these groups cannot be dry-run: ${p.join(", ")}.`:""}${F(n,W,i)}`}}},G=({entityType:e,childRecipes:t,replaceRecipes:n,optionsEdges:i},{recipeGroup:a,fieldOption:p})=>new s({name:k(e),fields:Object.assign(K(O,t,{entityType:e,purpose:"adding children to",recipeGroup:a}),K(v,n,{entityType:e,purpose:"replacing",recipeGroup:a}),...i.map(({options:{edgeName:t,field:n,validate:i},hints:s})=>({[t]:{type:new r(new o(p)),description:`The values "${n}" of this ${e} can take, as the configurator offers them in the current state of the entity. Null when the configurator does not offer a choice on this entity. ${i?`modify${e} rejects a "${n}" value that is not in the list.`:`modify${e} does not check "${n}" against the list.`}${F(s,L,e)}`}})))}),Y=e=>Object.entries(e).map(([e,t])=>`${e} (${t.map(({groupId:e})=>e).join(", ")})`),V=(e,t)=>{let{type:n,enumValues:i,localized:r,description:o}=m(e,t);return{type:{kind:"value",type:n},localized:!0===r,description:o,enumValues:i}},x=(e,t,n,i,r)=>{let o=t.startsWith("$")?((e,t,n,i)=>{let r=((e,t,{expressions:n,entityTypes:i})=>{let r=i.find(t=>t===e);for(let i of void 0===r?[e]:n.entityExpressions.get(r)?.shapeGroups??[]){let e=[...n.expressions.get(i)?.values()??[]].find(e=>e.name===t);if(e?.declaredSchema)return{implementation:e.implementation,declaredSchema:e.declaredSchema}}})(e.name,t,n);if(!r)return void i.error(f.ASSISTANT_UNKNOWN_FIELD,y.ASSISTANT_UNKNOWN_FIELD("entities",e.name,t));let{implementation:o}=r;if("violations"===o.kind)return void i.error(f.ASSISTANT_UNSUPPORTED_EXPRESSION,y.ASSISTANT_UNSUPPORTED_EXPRESSION(e.name,t));if("relation"===o.kind){let[{target:e,cardinality:t}]=o.params;return{type:{kind:"edge",target:e,cardinality:t},localized:!1,description:void 0}}return V(r.declaredSchema,n.resolveRef)})(e,t,i,r):((e,t,{resolveRef:n},i)=>{let r=Object.entries(e.shape.properties).find(([e])=>e===t)?.[1];return r?V(r,n):void i.error(f.ASSISTANT_UNKNOWN_FIELD,y.ASSISTANT_UNKNOWN_FIELD("entities",e.name,t))})(e,t,i,r);if(!o)return;let s=n?.as??(t.startsWith("$")?`_${t.slice(1)}`:t);return b.has(s)?void r.error(f.ASSISTANT_RESERVED_FIELD_NAME,y.ASSISTANT_RESERVED_FIELD_NAME(e.name,t,s)):{name:s,source:t,type:o.type,localized:o.localized,description:R(n?.description??o.description,o.enumValues),describedByManifest:n?.description!==void 0}},B=(e,t,n,i)=>{let r=new Map,o=new Set(n);for(let s of[...t.flat(),...n]){let t=r.get(s.name);t?t.source!==s.source?i.error(f.ASSISTANT_FIELD_NAME_CONFLICT,y.ASSISTANT_FIELD_NAME_CONFLICT(e,s.name,[t.source,s.source])):o.has(s)&&s.describedByManifest&&r.set(s.name,{...t,description:s.description,describedByManifest:!0}):r.set(s.name,s)}return[...r.values()]},q=({target:e,cardinality:t},n)=>{let i=n(e);switch(t){case"1-1":return new o(i);case"0-1":return i;case"0-N":case"1-N":return new o(new r(new o(i)))}},z=(e,n,i)=>{let r={id:{type:new o(t),description:i?`Unique id of the entity, e.g. "${i}-1".`:void 0},type:{type:new o(p),description:i?`Entity type name, e.g. "${i}".`:void 0}};for(let t of e)r[t.name]={type:"edge"===t.type.kind?q(t.type,n):t.type.type,description:t.description};return r},H=e=>Object.fromEntries(e.map(({name:e,source:t,localized:n})=>[e,n?{source:t,localized:!0}:{source:t}]));export const createAssistantSchema=(u,{model:b,states:W,phxDefinitions:L,expressions:F,components:K},V)=>{let q,Q,X,J,Z=V.errors.length,ee=V.at("entities"),et=S(b.commonTypes,L),en=b.rootEntityTypes[0]??b.entityTypes[0];for(let e of b.resolvedShapes.keys())$.has(e)&&V.error(f.ASSISTANT_RESERVED_TYPE_NAME,y.ASSISTANT_RESERVED_TYPE_NAME(e,[...$]));let ei=((e,t,n)=>{let i=new Map;for(let r of e){let e=[];for(let[i,o]of Object.entries(r.entries)){let s=x(r,i,o,t,n.at(r.name,i));s&&e.push(s)}i.set(r.name,e)}return i})(((e,t,n)=>{let i=[];for(let[n,r]of t.resolvedShapes){let t=e[n];t&&i.push({name:n,shape:r,entries:t})}let r=new Set(t.resolvedShapes.keys());for(let t of Object.keys(e))r.has(t)||n.at(t).error(f.ASSISTANT_UNKNOWN_TYPE,y.ASSISTANT_UNKNOWN_TYPE("entities",t));return i})(u.entities??{},b,ee),{resolveRef:et,expressions:F,entityTypes:b.entityTypes},ee),er=B(T,[],ei.get(T)??[],V),eo=new Map,es=C(u.childRecipes??{},O,{model:b,components:K},V.at(O.section)),ea=C(u.replaceRecipes??{},v,{model:b,components:K},V.at(v.section)),ep=es.groups,el=ea.groups,ed=new Map,ec=e=>{let[t,...n]=[...b.aliases].find(([t])=>t===e)?.[1]??[];return(t&&0===n.length?eo.get(t):void 0)??ey.get(e)??ef},eh=e=>eo.get(e)??ec(e),ef=new i({name:T,description:"Every domain entity.",fields:()=>z(er,eh,en)}),ey=new Map([[T,ef]]),eu=new Map([[T,er]]);for(let e of b.aliases.keys()){if(e===T)continue;let t=B(e,[er],ei.get(e)??[],V);eu.set(e,t),ey.set(e,new i({name:e,interfaces:[ef],fields:()=>z(t,eh)}))}let eg={};for(let e of b.entityTypes){let t=[T,...[...b.aliases].filter(([t,n])=>t!==T&&n.includes(e)).map(([e])=>e)],n=B(e,t.map(e=>eu.get(e)??[]),ei.get(e)??[],V);eg[e]=H(n),eo.set(e,new s({name:e,interfaces:t.flatMap(e=>ey.get(e)??[]),fields:()=>{let t=ed.get(e);return{...z(n,eh),...t?{meta:{type:new o(t),description:"What the configurator offers on this entity besides its fields."}}:{}}}}))}let em=((e,t,n)=>{let i=new Set;for(let r of Object.keys(e)){let e=t.entityTypes.find(e=>e===r),o=e?[e]:[...t.aliases].find(([e])=>e===r)?.[1];o?o.forEach(e=>i.add(e)):n.at(r).error(f.ASSISTANT_UNKNOWN_TYPE,y.ASSISTANT_UNKNOWN_TYPE("deletable",r))}return t.entityTypes.filter(e=>i.has(e))})(u.deletable??{},b,V.at("deletable")),eS=((e,t,n)=>{let{model:i}=t,r=((e,t,n)=>{let i=new Map;for(let[r,o]of Object.entries(e)){let e=t.entityTypes.find(e=>e===r),s=e?[e]:[...t.aliases].find(([e])=>e===r)?.[1],a=[...t.resolvedShapes].find(([e])=>e===r)?.[1];if(!s||!a){n.at(r).error(f.ASSISTANT_UNKNOWN_TYPE,y.ASSISTANT_UNKNOWN_TYPE("writable",r));continue}for(let e of Object.keys(o)){let t=n.at(r,e);e.startsWith("$")||M.has(e)?t.error(f.ASSISTANT_NOT_WRITABLE,y.ASSISTANT_NOT_WRITABLE(r,e)):D(a.properties,e)?s.forEach(t=>{let n=i.get(t)??new Map;i.set(t,n.has(e)?n:n.set(e,r))}):t.error(f.ASSISTANT_UNKNOWN_FIELD,y.ASSISTANT_UNKNOWN_FIELD("writable",r,e))}}return i})(e,i,n);return i.entityTypes.flatMap(e=>{let o=[...i.resolvedShapes].find(([t])=>t===e)?.[1],s=[...r.get(e)??[]].flatMap(([i,r])=>{let s=o&&D(o.properties,i);return(s&&((e,{property:t,writableKey:n},i,{resolveRef:r,exposed:o,sources:s},a)=>{let p=Object.entries(D(s,e)??{}),c=o.get(e)?.find(({source:e})=>e===t)?.name??p.find(([,{source:e}])=>e===t)?.[0],h=void 0===c?p.find(([e])=>e===t)?.[1]:void 0;if(h)return void a.at(n,t).error(f.ASSISTANT_FIELD_NAME_CONFLICT,y.ASSISTANT_FIELD_NAME_CONFLICT(e,t,[h.source,t],`${n}.${t}`));let{type:u,enumValues:S,localized:T,description:w}=m(i,r),N=l(u),E=c??t,A=void 0===c?R(w,S):void 0;return T?{name:E,property:t,type:g,description:A?`${A} ${j}`:j}:{name:E,property:t,type:d(N)?N:g,description:A}})(e,{property:i,writableKey:r},s,t,n))??[]});return s.length>0?[{entityType:e,fields:s}]:[]})})(u.writable??{},{model:b,resolveRef:et,exposed:ei,sources:eg},V.at("writable")),eT=((e,t,n,i)=>{let{model:r}=n,o=new Map;for(let[s,a]of Object.entries(e)){let e=r.entityTypes.find(e=>e===s),p=e?[e]:[...r.aliases].find(([e])=>e===s)?.[1]??[];for(let[e,r]of Object.entries(a)){let a=r?.options;if(!a)continue;let l=i.at(s,e,"options"),d=`${s}.${e}`;for(let i of p){let r=t.find(e=>e.entityType===i)?.fields.find(t=>t.property===e);if(!r)continue;let s=a.as??`${r.name}Options`,p=P(i,a.componentType,n),c=o.get(i)??[],h=c.find(e=>e.options.edgeName===s||e.options.field===r.name);if(p.includes(a.instanceName))if(s===O.section||s===v.section)l.error(f.ASSISTANT_RESERVED_OPTIONS_EDGE_NAME,y.ASSISTANT_RESERVED_OPTIONS_EDGE_NAME(d,s,r.name));else if(h)l.error(f.ASSISTANT_DUPLICATE_OPTIONS_EDGE,y.ASSISTANT_DUPLICATE_OPTIONS_EDGE(i,h.options.edgeName===s?{edgeName:s}:{field:r.name},[h.entry,d]));else{let e={field:r.name,edgeName:s,componentType:a.componentType,instanceName:a.instanceName,validate:a.validate??!0};o.set(i,[...c,{options:e,hints:U(a.hints),entry:d,entryCtx:l}])}else l.error(f.ASSISTANT_UNKNOWN_OPTIONS_COMPONENT,y.ASSISTANT_UNKNOWN_OPTIONS_COMPONENT(d,a,i,p))}}}return o})(u.writable??{},eS,{model:b,components:K},V.at("writable"));var ew,eN,eE,eA=[es.metaEntries,ea.metaEntries,new Map([...eT].flatMap(([e,[t]])=>t?[[e,t.entryCtx]]:[]))];for(let e of b.entityTypes){let t=eA.flatMap(t=>t.get(e)??[])[0],n=k(e);t&&[...b.resolvedShapes.keys()].some(e=>e===n)&&t.error(f.ASSISTANT_META_TYPE_NAME_CONFLICT,y.ASSISTANT_META_TYPE_NAME_CONFLICT(e,n))}let e_={recipeGroup:(ew=[...Object.keys(ep).length>0?[O.mutation]:[],...Object.keys(el).length>0?[v.mutation]:[]],eN=()=>ec(ey.has(w)?w:T),eE=()=>ec(T),q=ew.join(" or "),Q=new s({name:_,description:"One reason a recipe cannot be used right now.",fields:{type:{type:new o(p),description:'Kind of the problem, or "uncaught".'},entityId:{type:t,description:"The entity the problem is on, when it is on one."},message:{type:p,description:"The problem as the configurator words it for the user."}}}),X=new s({name:A,description:"What using a recipe would do, tried out on the configuration the query reads. Nothing is changed.",fields:()=>({ok:{type:new o(e),description:`Whether ${q} would succeed with this recipe.`},root:{type:eN(),description:"The root entity of the configuration as it would be after the change; null when ok is false. Everything selected under it reads that configuration."},newEntities:{type:new o(new r(new o(eE()))),description:`The entities ${q} would return, read from that configuration; empty when ok is false.`},errors:{type:new o(new r(new o(Q))),description:"Why the recipe cannot be used; empty when ok is true."}})}),J=new s({name:E,description:"One recipe of a recipe group: something that can be added to the entity, or something the entity can be replaced with, depending on the edge the group is listed under.",fields:{key:{type:new o(p),description:`Identifies the recipe within its group; pass it to ${q} as recipeKey. Keys can change with the configuration and with the language, so read them from the listing rather than remembering them.`},name:{type:new o(p),description:"Name shown to the user."},description:{type:p},imageUrl:{type:p,description:"Thumbnail shown to the user."},entityType:{type:new o(p),description:"Type of the entity the recipe adds under the parent, or of the entity it replaces the current one with."},dryRun:{type:X,description:"Tries the recipe out. Each selected dry-run costs a full recalculation, so select it only on the recipes you are deciding between. Null for a group whose recipes cannot be dry-run."}}}),new s({name:N,description:"A group of recipes of an entity, as the configurator offers them to the user.",fields:{groupId:{type:new o(p),description:`Identifies the group on its entity; pass it to ${q} as recipeGroupId.`},name:{type:new o(p),description:"Name shown to the user."},description:{type:p},recipes:{type:new o(new r(new o(J)))}}})),fieldOption:new s({name:I,description:"One choice of a field, as the configurator offers it to the user.",fields:{value:{type:new o(g),description:"The value to write to the field."},name:{type:p,description:"Name shown to the user."},description:{type:p},imageUrl:{type:p,description:"Image shown to the user."},color:{type:p,description:'Color shown to the user, as "#rrggbb".'}}})};for(let e of b.entityTypes){let t=D(ep,e)??[],n=D(el,e)??[],i=eT.get(e)??[];(t.length>0||n.length>0||i.length>0)&&ed.set(e,G({entityType:e,childRecipes:{groups:t,hints:es.hints.get(e)??[]},replaceRecipes:{groups:n,hints:ea.hints.get(e)??[]},optionsEdges:i},e_))}let eI=Object.fromEntries(b.entityTypes.flatMap(e=>{let t=eT.get(e);return t?[[e,t.map(({options:e})=>e)]]:[]}));if(V.errors.length>Z)return;let eO=((e,t)=>{let n;try{let t=e();if(n=h(t).map(e=>e.message),0===n.length)return c(t)}catch(e){n=[e instanceof Error?e.message:String(e)]}t.error(f.ASSISTANT_INVALID_SCHEMA,y.ASSISTANT_INVALID_SCHEMA(n))})(()=>{var e,i;let l;return new a({query:(e=ec(ey.has(w)?w:T),i=ec(T),l=(e=>{let t={};for(let[n,i,r]of[["navigationState","Active navigation state.",e.navigationStates],["viewState","Active view state.",e.viewStates],["toolState","Active tool state.",e.toolStates]])r.length>1&&(t[n]={type:new o(p),description:R(i,r)});if(0!==Object.keys(t).length)return new s({name:"Navigation",description:"The active UI states of the configurator.",fields:t})})(W),new s({name:"Query",fields:{root:{type:new o(e),description:"The root entity of the configuration."},entities:{type:new o(new r(i)),args:{ids:{type:new r(new o(t))}},description:`Every entity of the configuration, or, when ids are given, those entities in the same order with null for an unknown id. Ids have the form "<Type>-<number>"${en?`, e.g. "${en}-1"`:""}; any other id is an error.`},selectedEntity:{type:new o(i),description:"The selected entity, or the root entity when nothing is selected."},...l?{navigation:{type:new o(l)}}:{}}})),mutation:(({deletableTypes:e,writableTypes:i,childRecipes:a,replaceRecipes:l,objectTypes:d,anyDomainEntity:c})=>{let h,f;if(0!==e.length||0!==i.length||0!==Object.keys(a).length||0!==Object.keys(l).length)return new s({name:"Mutation",fields:Object.assign(0===e.length?{}:{deleteEntity:{type:new o(new r(new o(t))),args:{id:{type:new o(t)}},description:`Deletes the entity with the given id, together with the entities composed into it, and returns the ids of every entity the deletion removed, the given one first. Deletable entity types: ${e.join(", ")}. Nothing can be read from removed entities afterwards, so query what you need from them before deleting.`}},0===(h=Y(a)).length?{}:{addChild:{type:new o(new r(new o(c))),args:{parentId:{type:new o(t)},recipeGroupId:{type:new o(p)},recipeKey:{type:new o(p)}},description:`Adds a child to the entity with the id parentId by one recipe of one of its recipe groups. Entity types with recipe groups: ${h.join("; ")}. Read the groupId and the recipe key from "meta { childRecipes }" of the parent. Returns every entity the change added, the recipe's own first and then whatever the configurator added in response, such as a part the new child needs; the list is empty when the configurator removed them again.`}},0===(f=Y(l)).length?{}:{replaceEntity:{type:new o(new r(new o(c))),args:{entityId:{type:new o(t)},recipeGroupId:{type:new o(p)},recipeKey:{type:new o(p)}},description:`Replaces the entity with the id entityId by one recipe of one of its replace recipe groups. Entity types with replace recipe groups: ${f.join("; ")}. Read the groupId and the recipe key from "meta { replaceRecipes }" of the entity. The replacement is a new entity with a new id: the id passed in no longer exists afterwards, and the replacement has the field values of its recipe, not those of the entity it replaces. Returns every entity the change added, the replacement first and then whatever the configurator added in response, such as a part the replacement needs; the list is empty when the configurator removed them again.`}},...i.flatMap(e=>{let i=d.get(e.entityType);return i?[(({entityType:e,fields:i},r)=>{let s=Object.fromEntries(i.map(({name:e,type:t,description:n})=>[e,{type:t,description:n}]));return{[`modify${e}`]:{type:r,args:{id:{type:new o(t)},delta:{type:new o(new n({name:`${e}Delta`,fields:s}))}},description:`Changes writable fields of the one ${e} with the given id: ${i.map(({name:e})=>e).join(", ")}. The delta sets only the fields it names and needs at least one; values are in stored units, as the field descriptions say. Returns the entity as it is after the change, including what the configurator adjusted in response, or null when the change caused the entity to be removed.`}}})(e,i)]:[]}))})})({deletableTypes:em,writableTypes:eS,childRecipes:ep,replaceRecipes:el,objectTypes:eo,anyDomainEntity:ec(T)}),types:[...ey.values(),...eo.values()]})},V);return void 0===eO?void 0:{sdl:eO,fields:eg,deletableTypes:em,writableFields:Object.fromEntries(eS.map(({entityType:e,fields:t})=>[e,Object.fromEntries(t.map(({name:e,property:t})=>[e,t]))])),childRecipes:ep,replaceRecipes:el,fieldOptions:eI}};
1
+ import{GraphQLBoolean as e,GraphQLID as t,GraphQLInputObjectType as n,GraphQLInterfaceType as i,GraphQLList as r,GraphQLNonNull as o,GraphQLObjectType as s,GraphQLSchema as a,GraphQLString as p,getNullableType as d,isInputType as l,printSchema as c,validateSchema as h}from"graphql";import{DiagnosticCode as f,diagnosticMessages as y}from"../../../../errors/index.js";import{ASSISTANT_HINTS as u}from"../../../raw-spec/index.js";import{GraphQLJSON as g,assistantFieldType as m,createAssistantRefResolver as S}from"./assistant-field-type.js";let w="AnyDomainEntity",T="RootEntity",N="RecipeGroup",E="Recipe",A="DryRun",I="DryRunError",_="FieldOption",v={section:"childRecipes",component:"childEntityRecipes",mutation:"addChild"},O={section:"replaceRecipes",component:"replaceEntityRecipes",mutation:"replaceEntity"},b=new Set(["id","type","meta"]),R=new Set(["Query","Mutation","Navigation",N,E,A,I,_,"JSON","String","Int","Float","Boolean","ID"]),$=(e,t)=>{if(!t||0===t.length)return e;let n=`One of: ${t.map(e=>`"${e}"`).join(", ")}.`;return e?/[.!?]$/.test(e)?`${e} ${n}`:`${e}. ${n}`:n},D=new Set(["id","type"]),M="The raw stored value: a string, a message descriptor or a multilingual object.",j=(e,t)=>Object.entries(e).find(([e])=>e===t)?.[1],P=(e,t,{model:n,components:i})=>{let r=new Set([e,...[...n.aliases].filter(([,t])=>t.includes(e)).map(([e])=>e)]);return i.filter(({componentName:e,typeOrAlias:n})=>e===t&&r.has(n)).map(({instanceName:e})=>String(e))},U=(...e)=>u.filter(t=>e.some(e=>e?.includes(t))),C=(e,t,n,i)=>{let{model:r}=n,o=new Map,s=new Map;for(let[a,{groups:p,hints:d}]of Object.entries(e)){let e=r.entityTypes.find(e=>e===a),l=e?[e]:[...r.aliases].find(([e])=>e===a)?.[1];if(!l){i.at(a).error(f.ASSISTANT_UNKNOWN_TYPE,y.ASSISTANT_UNKNOWN_TYPE(t.section,a));continue}for(let[e,r]of(l.forEach(e=>s.set(e,U(s.get(e),d))),Object.entries(p))){let s=i.at(a,"groups",e),p=`${a}.groups.${e}`,d={groupId:r?.as??e,instanceName:e,...r?.description===void 0?{}:{description:r.description},dryRuns:r?.allowDryRuns??!0};for(let i of l){let r=P(i,t.component,n),l=o.get(i)??[],c=l.find(e=>e.group.groupId===d.groupId);r.includes(e)?c?s.error(f.ASSISTANT_DUPLICATE_RECIPE_GROUP,y.ASSISTANT_DUPLICATE_RECIPE_GROUP(t,i,d.groupId,[c.entry,p])):o.set(i,[...l,{group:d,entry:p,entryCtx:s}]):s.error(f.ASSISTANT_UNKNOWN_RECIPE_GROUP,y.ASSISTANT_UNKNOWN_RECIPE_GROUP(t,a,e,i,r))}}}let a=r.entityTypes.flatMap(e=>{let t=o.get(e);return t?[{entityType:e,groups:t}]:[]});return{groups:Object.fromEntries(a.map(({entityType:e,groups:t})=>[e,t.map(({group:e})=>e)])),hints:s,metaEntries:new Map(a.flatMap(({entityType:e,groups:[t]})=>t?[[e,t.entryCtx]]:[]))}},k=e=>`${e}Meta`,W={sameForAllInstances:e=>`All ${e} instances have the same recipe groups and recipes.`,constantDuringSession:()=>"They do not change during the session."},L={sameForAllInstances:e=>`All ${e} instances have the same options.`,constantDuringSession:()=>"The options do not change during the session."},F=(e,t,n)=>e.map(e=>` ${t[e](n)}`).join(""),K=({section:e},{groups:t,hints:n},{entityType:i,purpose:s,recipeGroup:a})=>{let p=t.filter(({dryRuns:e})=>!e).map(({groupId:e})=>e);return 0===t.length?{}:{[e]:{type:new o(new r(new o(a))),description:`The recipe groups for ${s} this ${i}: ${t.map(({groupId:e})=>e).join(", ")}. A group is listed only while the configurator offers it on the entity.${p.length>0?` Recipes of these groups cannot be dry-run: ${p.join(", ")}.`:""}${F(n,W,i)}`}}},G=({entityType:e,childRecipes:t,replaceRecipes:n,optionsEdges:i},{recipeGroup:a,fieldOption:p})=>new s({name:k(e),fields:Object.assign(K(v,t,{entityType:e,purpose:"adding children to",recipeGroup:a}),K(O,n,{entityType:e,purpose:"replacing",recipeGroup:a}),...i.map(({options:{edgeName:t,field:n,validate:i,dryRuns:s},hints:a})=>({[t]:{type:new r(new o(p)),description:`The values "${n}" of this ${e} can take, as the configurator offers them in the current state of the entity. Null when the configurator does not offer a choice on this entity. ${i?`modify${e} rejects a "${n}" value that is not in the list.`:`modify${e} does not check "${n}" against the list.`}${s?"":" Its options cannot be dry-run."}${F(a,L,e)}`}})))}),V=e=>Object.entries(e).map(([e,t])=>`${e} (${t.map(({groupId:e})=>e).join(", ")})`),Y=(e,t)=>{let{type:n,enumValues:i,localized:r,description:o}=m(e,t);return{type:{kind:"value",type:n},localized:!0===r,description:o,enumValues:i}},x=(e,t,n,i,r)=>{let o=t.startsWith("$")?((e,t,n,i)=>{let r=((e,t,{expressions:n,entityTypes:i})=>{let r=i.find(t=>t===e);for(let i of void 0===r?[e]:n.entityExpressions.get(r)?.shapeGroups??[]){let e=[...n.expressions.get(i)?.values()??[]].find(e=>e.name===t);if(e?.declaredSchema)return{implementation:e.implementation,declaredSchema:e.declaredSchema}}})(e.name,t,n);if(!r)return void i.error(f.ASSISTANT_UNKNOWN_FIELD,y.ASSISTANT_UNKNOWN_FIELD("entities",e.name,t));let{implementation:o}=r;if("violations"===o.kind)return void i.error(f.ASSISTANT_UNSUPPORTED_EXPRESSION,y.ASSISTANT_UNSUPPORTED_EXPRESSION(e.name,t));if("relation"===o.kind){let[{target:e,cardinality:t}]=o.params;return{type:{kind:"edge",target:e,cardinality:t},localized:!1,description:void 0}}return Y(r.declaredSchema,n.resolveRef)})(e,t,i,r):((e,t,{resolveRef:n},i)=>{let r=Object.entries(e.shape.properties).find(([e])=>e===t)?.[1];return r?Y(r,n):void i.error(f.ASSISTANT_UNKNOWN_FIELD,y.ASSISTANT_UNKNOWN_FIELD("entities",e.name,t))})(e,t,i,r);if(!o)return;let s=n?.as??(t.startsWith("$")?`_${t.slice(1)}`:t);return b.has(s)?void r.error(f.ASSISTANT_RESERVED_FIELD_NAME,y.ASSISTANT_RESERVED_FIELD_NAME(e.name,t,s)):{name:s,source:t,type:o.type,localized:o.localized,description:$(n?.description??o.description,o.enumValues),describedByManifest:n?.description!==void 0,enumValues:o.enumValues,valueDescription:n?.description??o.description}},q=(e,t,n,i,r)=>{let o=new Map,s=new Set(n);for(let i of[...t.flat(),...n]){let t=o.get(i.name);t?t.source!==i.source?r.error(f.ASSISTANT_FIELD_NAME_CONFLICT,y.ASSISTANT_FIELD_NAME_CONFLICT(e,i.name,[t.source,i.source])):s.has(i)&&i.describedByManifest&&o.set(i.name,{...t,description:i.description,describedByManifest:!0,valueDescription:i.valueDescription}):o.set(i.name,i)}return[...o.values()].map(e=>{let t=e.enumValues&&i(e.source);return t?{...e,enumValues:t,description:$(e.valueDescription,t)}:e})},B=({target:e,cardinality:t},n)=>{let i=n(e);switch(t){case"1-1":return new o(i);case"0-1":return i;case"0-N":case"1-N":return new o(new r(new o(i)))}},z=(e,n,i)=>{let r={id:{type:new o(t),description:i?`Unique id of the entity, e.g. "${i}-1".`:void 0},type:{type:new o(p),description:i?`Entity type name, e.g. "${i}".`:void 0}};for(let t of e)r[t.name]={type:"edge"===t.type.kind?B(t.type,n):t.type.type,description:t.description};return r},H=e=>Object.fromEntries(e.map(({name:e,source:t,localized:n})=>[e,n?{source:t,localized:!0}:{source:t}]));export const createAssistantSchema=(u,{model:b,states:W,phxDefinitions:L,expressions:F,components:K},Y)=>{let B,Q,X,J=Y.errors.length,Z=Y.at("entities"),ee=S(b.commonTypes,L),et=b.rootEntityTypes[0]??b.entityTypes[0];for(let e of b.resolvedShapes.keys())R.has(e)&&Y.error(f.ASSISTANT_RESERVED_TYPE_NAME,y.ASSISTANT_RESERVED_TYPE_NAME(e,[...R]));let en=((e,t,n)=>{let i=new Map;for(let r of e){let e=[];for(let[i,o]of Object.entries(r.entries)){let s=x(r,i,o,t,n.at(r.name,i));s&&e.push(s)}i.set(r.name,e)}return i})(((e,t,n)=>{let i=[];for(let[n,r]of t.resolvedShapes){let t=e[n];t&&i.push({name:n,shape:r,entries:t})}let r=new Set(t.resolvedShapes.keys());for(let t of Object.keys(e))r.has(t)||n.at(t).error(f.ASSISTANT_UNKNOWN_TYPE,y.ASSISTANT_UNKNOWN_TYPE("entities",t));return i})(u.entities??{},b,Z),{resolveRef:ee,expressions:F,entityTypes:b.entityTypes},Z),ei=q(w,[],en.get(w)??[],()=>void 0,Y),er=new Map,eo=C(u.childRecipes??{},v,{model:b,components:K},Y.at(v.section)),es=C(u.replaceRecipes??{},O,{model:b,components:K},Y.at(O.section)),ea=eo.groups,ep=es.groups,ed=new Map,el=e=>{let[t,...n]=[...b.aliases].find(([t])=>t===e)?.[1]??[];return(t&&0===n.length?er.get(t):void 0)??ef.get(e)??eh},ec=e=>er.get(e)??el(e),eh=new i({name:w,description:"Every domain entity.",fields:()=>z(ei,ec,et)}),ef=new Map([[w,eh]]),ey=new Map([[w,ei]]);for(let e of b.aliases.keys()){if(e===w)continue;let t=q(e,[ei],en.get(e)??[],()=>void 0,Y);ey.set(e,t),ef.set(e,new i({name:e,interfaces:[eh],fields:()=>z(t,ec)}))}let eu={};for(let e of b.entityTypes){let t=[w,...[...b.aliases].filter(([t,n])=>t!==w&&n.includes(e)).map(([e])=>e)],n=[...b.resolvedShapes].find(([t])=>t===e)?.[1],i=q(e,t.map(e=>ey.get(e)??[]),en.get(e)??[],e=>{let t=Object.entries(n?.properties??{}).find(([t])=>t===e)?.[1];return t&&m(t,ee).enumValues},Y);eu[e]=H(i),er.set(e,new s({name:e,interfaces:t.flatMap(e=>ef.get(e)??[]),fields:()=>{let t=ed.get(e);return{...z(i,ec),...t?{meta:{type:new o(t),description:"What the configurator offers on this entity besides its fields."}}:{}}}}))}let eg=((e,t,n)=>{let i=new Set;for(let r of Object.keys(e)){let e=t.entityTypes.find(e=>e===r),o=e?[e]:[...t.aliases].find(([e])=>e===r)?.[1];o?o.forEach(e=>i.add(e)):n.at(r).error(f.ASSISTANT_UNKNOWN_TYPE,y.ASSISTANT_UNKNOWN_TYPE("deletable",r))}return t.entityTypes.filter(e=>i.has(e))})(u.deletable??{},b,Y.at("deletable")),em=((e,t,n)=>{let{model:i}=t,r=((e,t,n)=>{let i=new Map;for(let[r,o]of Object.entries(e)){let e=t.entityTypes.find(e=>e===r),s=e?[e]:[...t.aliases].find(([e])=>e===r)?.[1],a=[...t.resolvedShapes].find(([e])=>e===r)?.[1];if(!s||!a){n.at(r).error(f.ASSISTANT_UNKNOWN_TYPE,y.ASSISTANT_UNKNOWN_TYPE("writable",r));continue}for(let e of Object.keys(o)){let t=n.at(r,e);e.startsWith("$")||D.has(e)?t.error(f.ASSISTANT_NOT_WRITABLE,y.ASSISTANT_NOT_WRITABLE(r,e)):j(a.properties,e)?s.forEach(t=>{let n=i.get(t)??new Map;i.set(t,n.has(e)?n:n.set(e,r))}):t.error(f.ASSISTANT_UNKNOWN_FIELD,y.ASSISTANT_UNKNOWN_FIELD("writable",r,e))}}return i})(e,i,n);return i.entityTypes.flatMap(e=>{let o=[...i.resolvedShapes].find(([t])=>t===e)?.[1],s=[...r.get(e)??[]].flatMap(([i,r])=>{let s=o&&j(o.properties,i);return(s&&((e,{property:t,writableKey:n},i,{resolveRef:r,exposed:o,sources:s},a)=>{let p=Object.entries(j(s,e)??{}),c=o.get(e)?.find(({source:e})=>e===t)?.name??p.find(([,{source:e}])=>e===t)?.[0],h=void 0===c?p.find(([e])=>e===t)?.[1]:void 0;if(h)return void a.at(n,t).error(f.ASSISTANT_FIELD_NAME_CONFLICT,y.ASSISTANT_FIELD_NAME_CONFLICT(e,t,[h.source,t],`${n}.${t}`));let{type:u,enumValues:S,localized:w,description:T}=m(i,r),N=d(u),E=c??t,A=void 0===c?$(T,S):void 0;return w?{name:E,property:t,type:g,description:A?`${A} ${M}`:M}:{name:E,property:t,type:l(N)?N:g,description:A}})(e,{property:i,writableKey:r},s,t,n))??[]});return s.length>0?[{entityType:e,fields:s}]:[]})})(u.writable??{},{model:b,resolveRef:ee,exposed:en,sources:eu},Y.at("writable")),eS=((e,t,n,i)=>{let{model:r}=n,o=new Map;for(let[s,a]of Object.entries(e)){let e=r.entityTypes.find(e=>e===s),p=e?[e]:[...r.aliases].find(([e])=>e===s)?.[1]??[];for(let[e,r]of Object.entries(a)){let a=r?.options;if(!a)continue;let d=i.at(s,e,"options"),l=`${s}.${e}`;for(let i of p){let r=t.find(e=>e.entityType===i)?.fields.find(t=>t.property===e);if(!r)continue;let s=a.as??`${r.name}Options`,p=P(i,a.componentType,n),c=o.get(i)??[],h=c.find(e=>e.options.edgeName===s||e.options.field===r.name);if(p.includes(a.instanceName))if(s===v.section||s===O.section)d.error(f.ASSISTANT_RESERVED_OPTIONS_EDGE_NAME,y.ASSISTANT_RESERVED_OPTIONS_EDGE_NAME(l,s,r.name));else if(h)d.error(f.ASSISTANT_DUPLICATE_OPTIONS_EDGE,y.ASSISTANT_DUPLICATE_OPTIONS_EDGE(i,h.options.edgeName===s?{edgeName:s}:{field:r.name},[h.entry,l]));else{let e={field:r.name,edgeName:s,componentType:a.componentType,instanceName:a.instanceName,validate:a.validate??!0,dryRuns:a.allowDryRuns??!0};o.set(i,[...c,{options:e,hints:U(a.hints),entry:l,entryCtx:d}])}else d.error(f.ASSISTANT_UNKNOWN_OPTIONS_COMPONENT,y.ASSISTANT_UNKNOWN_OPTIONS_COMPONENT(l,a,i,p))}}}return o})(u.writable??{},em,{model:b,components:K},Y.at("writable"));var ew,eT,eN,eE=[eo.metaEntries,es.metaEntries,new Map([...eS].flatMap(([e,[t]])=>t?[[e,t.entryCtx]]:[]))];for(let e of b.entityTypes){let t=eE.flatMap(t=>t.get(e)??[])[0],n=k(e);t&&[...b.resolvedShapes.keys()].some(e=>e===n)&&t.error(f.ASSISTANT_META_TYPE_NAME_CONFLICT,y.ASSISTANT_META_TYPE_NAME_CONFLICT(e,n))}let eA=(ew=()=>el(ef.has(T)?T:w),eT=()=>el(w),B=new s({name:I,description:"One reason a change cannot be made right now.",fields:{type:{type:new o(p),description:'Kind of the problem, or "uncaught".'},entityId:{type:t,description:"The entity the problem is on, when it is on one."},message:{type:p,description:"The problem as the configurator words it for the user."}}}),new s({name:A,description:"What using a recipe or writing an option would do, tried out on the configuration the query reads. Nothing is changed.",fields:()=>({ok:{type:new o(e),description:"Whether the change would succeed on the configuration the query reads."},root:{type:ew(),description:"The root entity of the configuration as it would be after the change; null when ok is false. Everything selected under it reads that configuration."},changedEntities:{type:new o(new r(new o(eT()))),description:"Every entity the change would add or modify, read from that configuration; empty when ok is false. An entity whose id the configuration does not have yet would be added, and its id may differ in the real mutation. An entity that is written to is listed even when it gets the values it already has."},deletedEntityIds:{type:new o(new r(new o(t))),description:"The ids of the entities the change would remove; empty when ok is false."},errors:{type:new o(new r(new o(B))),description:"Why the change cannot be made; empty when ok is true."}})})),eI={recipeGroup:(eN=[...Object.keys(ea).length>0?[v.mutation]:[],...Object.keys(ep).length>0?[O.mutation]:[]],Q=eN.join(" or "),X=new s({name:E,description:"One recipe of a recipe group: something that can be added to the entity, or something the entity can be replaced with, depending on the edge the group is listed under.",fields:{key:{type:new o(p),description:`Identifies the recipe within its group; pass it to ${Q} as recipeKey. Keys can change with the configuration and with the language, so read them from the listing rather than remembering them.`},name:{type:new o(p),description:"Name shown to the user."},description:{type:p},imageUrl:{type:p,description:"Thumbnail shown to the user."},dryRun:{type:eA,description:"Tries the recipe out. Each selected dry-run costs a full recalculation, so select it only on the recipes you are deciding between. Null for a group whose recipes cannot be dry-run."}}}),new s({name:N,description:"A group of recipes of an entity, as the configurator offers them to the user.",fields:{groupId:{type:new o(p),description:`Identifies the group on its entity; pass it to ${Q} as recipeGroupId.`},name:{type:new o(p),description:"Name shown to the user."},description:{type:p},recipes:{type:new o(new r(new o(X)))}}})),fieldOption:new s({name:_,description:"One choice of a field, as the configurator offers it to the user.",fields:{value:{type:new o(g),description:"The value to write to the field."},name:{type:p,description:"Name shown to the user."},description:{type:p},imageUrl:{type:p,description:"Image shown to the user."},color:{type:p,description:'Color shown to the user, as "#rrggbb".'},dryRun:{type:eA,description:"Tries writing the value to the field. Each selected dry-run costs a full recalculation, so select it only on the options you are deciding between. Null for a field whose options cannot be dry-run."}}})};for(let e of b.entityTypes){let t=j(ea,e)??[],n=j(ep,e)??[],i=eS.get(e)??[];(t.length>0||n.length>0||i.length>0)&&ed.set(e,G({entityType:e,childRecipes:{groups:t,hints:eo.hints.get(e)??[]},replaceRecipes:{groups:n,hints:es.hints.get(e)??[]},optionsEdges:i},eI))}let e_=Object.fromEntries(b.entityTypes.flatMap(e=>{let t=eS.get(e);return t?[[e,t.map(({options:e})=>e)]]:[]}));if(Y.errors.length>J)return;let ev=((e,t)=>{let n;try{let t=e();if(n=h(t).map(e=>e.message),0===n.length)return c(t)}catch(e){n=[e instanceof Error?e.message:String(e)]}t.error(f.ASSISTANT_INVALID_SCHEMA,y.ASSISTANT_INVALID_SCHEMA(n))})(()=>{var e,i;let d;return new a({query:(e=el(ef.has(T)?T:w),i=el(w),d=(e=>{let t={};for(let[n,i,r]of[["navigationState","Active navigation state.",e.navigationStates],["viewState","Active view state.",e.viewStates],["toolState","Active tool state.",e.toolStates]])r.length>1&&(t[n]={type:new o(p),description:$(i,r)});if(0!==Object.keys(t).length)return new s({name:"Navigation",description:"The active UI states of the configurator.",fields:t})})(W),new s({name:"Query",fields:{root:{type:new o(e),description:"The root entity of the configuration."},entities:{type:new o(new r(i)),args:{ids:{type:new r(new o(t))}},description:`Every entity of the configuration, or, when ids are given, those entities in the same order with null for an unknown id. Ids have the form "<Type>-<number>"${et?`, e.g. "${et}-1"`:""}; any other id is an error.`},selectedEntity:{type:new o(i),description:"The selected entity, or the root entity when nothing is selected."},...d?{navigation:{type:new o(d)}}:{}}})),mutation:(({deletableTypes:e,writableTypes:i,childRecipes:a,replaceRecipes:d,objectTypes:l,anyDomainEntity:c})=>{let h,f;if(0!==e.length||0!==i.length||0!==Object.keys(a).length||0!==Object.keys(d).length)return new s({name:"Mutation",fields:Object.assign(0===e.length?{}:{deleteEntity:{type:new o(new r(new o(t))),args:{id:{type:new o(t)}},description:`Deletes the entity with the given id, together with the entities composed into it, and returns the ids of every entity the deletion removed, the given one first. Deletable entity types: ${e.join(", ")}. Nothing can be read from removed entities afterwards, so query what you need from them before deleting.`}},0===(h=V(a)).length?{}:{addChild:{type:new o(new r(new o(c))),args:{parentId:{type:new o(t)},recipeGroupId:{type:new o(p)},recipeKey:{type:new o(p)}},description:`Adds a child to the entity with the id parentId by one recipe of one of its recipe groups. Entity types with recipe groups: ${h.join("; ")}. Read the groupId and the recipe key from "meta { childRecipes }" of the parent. Returns every entity the change added, the recipe's own first and then whatever the configurator added in response, such as a part the new child needs; the list is empty when the configurator removed them again.`}},0===(f=V(d)).length?{}:{replaceEntity:{type:new o(new r(new o(c))),args:{entityId:{type:new o(t)},recipeGroupId:{type:new o(p)},recipeKey:{type:new o(p)}},description:`Replaces the entity with the id entityId by one recipe of one of its replace recipe groups. Entity types with replace recipe groups: ${f.join("; ")}. Read the groupId and the recipe key from "meta { replaceRecipes }" of the entity. The replacement is a new entity with a new id: the id passed in no longer exists afterwards, and the replacement has the field values of its recipe, not those of the entity it replaces. Returns every entity the change added, the replacement first and then whatever the configurator added in response, such as a part the replacement needs; the list is empty when the configurator removed them again.`}},...i.flatMap(e=>{let i=l.get(e.entityType);return i?[(({entityType:e,fields:i},r)=>{let s=Object.fromEntries(i.map(({name:e,type:t,description:n})=>[e,{type:t,description:n}]));return{[`modify${e}`]:{type:r,args:{id:{type:new o(t)},delta:{type:new o(new n({name:`${e}Delta`,fields:s}))}},description:`Changes writable fields of the one ${e} with the given id: ${i.map(({name:e})=>e).join(", ")}. The delta sets only the fields it names and needs at least one; values are in stored units, as the field descriptions say. Returns the entity as it is after the change, including what the configurator adjusted in response, or null when the change caused the entity to be removed.`}}})(e,i)]:[]}))})})({deletableTypes:eg,writableTypes:em,childRecipes:ea,replaceRecipes:ep,objectTypes:er,anyDomainEntity:el(w)}),types:[...ef.values(),...er.values()]})},Y);return void 0===ev?void 0:{sdl:ev,fields:eu,deletableTypes:eg,writableFields:Object.fromEntries(em.map(({entityType:e,fields:t})=>[e,Object.fromEntries(t.map(({name:e,property:t})=>[e,t]))])),childRecipes:ea,replaceRecipes:ep,fieldOptions:e_}};
@@ -1,5 +1,6 @@
1
- import { type $DomainEntity, type AssistantManifest, type AssistantRecipeContext, type DomainEntity, type DomainEntityId, type DomainState, type StateUpdate, type StateUpdateEmission } from '#package/domain';
1
+ import { type $DomainEntity, type AssistantManifest, type AssistantRecipeContext, type DomainEntity, type DomainEntityId, type DomainState, type EntityId, type StateUpdate, type StateUpdateEmission } from '#package/domain';
2
2
  type AnyDomainState = DomainState<DomainEntity<string>>;
3
+ type AnyStateUpdate = StateUpdate<DomainEntity<string>, DomainEntity<string>>;
3
4
  /** An entity and the state it was read on, which everything under its `meta` field resolves against. */
4
5
  export interface EntityMetaSource {
5
6
  readonly entity: DomainEntity<string>;
@@ -9,9 +10,22 @@ export interface EntityMetaContext {
9
10
  readonly manifest: AssistantManifest;
10
11
  readonly recipes: AssistantRecipeContext;
11
12
  /** Prepares the emission of a dry-run from the state being read; nothing is staged or committed. */
12
- readonly prepareStateEmission: (updates: ReadonlyArray<StateUpdate<DomainEntity<string>, DomainEntity<string>>>, state: AnyDomainState, kind: 'committed') => StateUpdateEmission<DomainEntity<string>, Record<string, $DomainEntity<DomainEntity<string>>>>;
13
+ readonly prepareStateEmission: (updates: readonly AnyStateUpdate[], state: AnyDomainState, kind: 'committed') => StateUpdateEmission<DomainEntity<string>, Record<string, $DomainEntity<DomainEntity<string>>>>;
13
14
  /** Reads the entities of a dry-run's resulting state; `null` when the entity is not in it. */
14
15
  readonly find$Entity: (id: DomainEntityId<string>, state: AnyDomainState) => unknown;
16
+ /** Checks an entity an option dry-run would write against its shape, which the emission does not do. */
17
+ readonly validateEntity: (entity: DomainEntity<string>) => {
18
+ readonly success: true;
19
+ } | {
20
+ readonly success: false;
21
+ readonly errors: readonly string[];
22
+ };
23
+ }
24
+ /** One reason a dry-run reports for a change that cannot be made; a violation can sit on a virtual entity. */
25
+ interface DryRunError {
26
+ readonly type: string;
27
+ readonly entityId: EntityId<DomainEntity<string>> | null;
28
+ readonly message: string | null;
15
29
  }
16
30
  /**
17
31
  * The value of an entity's `meta` field: its built-ins, resolved lazily against the state the entity
@@ -28,25 +42,18 @@ export declare const createEntityMeta: (source: EntityMetaSource, context: Entit
28
42
  name: string;
29
43
  description: string | null;
30
44
  imageUrl: string | null;
31
- entityType: () => string | undefined;
32
45
  dryRun: () => {
33
46
  ok: boolean;
34
- root: unknown;
35
- newEntities: {}[];
36
- errors: never[];
47
+ root: null;
48
+ changedEntities: never[];
49
+ deletedEntityIds: never[];
50
+ errors: readonly DryRunError[];
37
51
  } | {
38
52
  ok: boolean;
39
- root: null;
40
- newEntities: never[];
41
- errors: {
42
- type: string;
43
- entityId: null;
44
- message: string;
45
- }[] | {
46
- type: never;
47
- entityId: import("#package/domain").EntityId<DomainEntity<string>>;
48
- message: string | null;
49
- }[];
53
+ root: unknown;
54
+ changedEntities: {}[];
55
+ deletedEntityIds: DomainEntityId<string>[];
56
+ errors: never[];
50
57
  } | null;
51
58
  }[];
52
59
  }[];
@@ -59,25 +66,18 @@ export declare const createEntityMeta: (source: EntityMetaSource, context: Entit
59
66
  name: string;
60
67
  description: string | null;
61
68
  imageUrl: string | null;
62
- entityType: () => string | undefined;
63
69
  dryRun: () => {
64
70
  ok: boolean;
65
- root: unknown;
66
- newEntities: {}[];
67
- errors: never[];
71
+ root: null;
72
+ changedEntities: never[];
73
+ deletedEntityIds: never[];
74
+ errors: readonly DryRunError[];
68
75
  } | {
69
76
  ok: boolean;
70
- root: null;
71
- newEntities: never[];
72
- errors: {
73
- type: string;
74
- entityId: null;
75
- message: string;
76
- }[] | {
77
- type: never;
78
- entityId: import("#package/domain").EntityId<DomainEntity<string>>;
79
- message: string | null;
80
- }[];
77
+ root: unknown;
78
+ changedEntities: {}[];
79
+ deletedEntityIds: DomainEntityId<string>[];
80
+ errors: never[];
81
81
  } | null;
82
82
  }[];
83
83
  }[];
@@ -1 +1 @@
1
- import{assistantAddedEntityIds as e,findAssistantFieldOptions as t,findAssistantRecipeGroups as r,prepareAssistantRecipeUpdates as i,readAssistantFieldOptions as n,readAssistantRecipeGroup as a}from"#package/domain";let s=(e,t,{entity:r,state:n},{recipes:a})=>{try{return{updates:i(e,t,r.id,n,a),error:void 0}}catch(e){if("child"===t.target.kind)throw e;return{updates:void 0,error:e instanceof Error?e.message:String(e)}}};export const createEntityMeta=(i,o)=>{let l=t=>()=>r(o.manifest,t,i.entity.type).flatMap(r=>{let n=a(t,r,i.entity.id,i.state,o.recipes);return n?[((t,r,i)=>{let{group:n,label:a,description:o,recipes:l}=t,{resolveLocalizable:p}=i.recipes;return{groupId:n.groupId,name:void 0===a?n.instanceName:p(a),description:n.description??(void 0===o?null:p(o)),recipes:l.map(n=>((t,r,i,n)=>{let{key:a,item:o}=t,{entity:l}=i,{resolveLocalizable:p}=n.recipes;return{key:a,name:p(o.title),description:void 0===o.description?null:p(o.description),imageUrl:void 0===o.thumbnailUrl?null:p(o.thumbnailUrl),entityType:()=>{let e=s(t,r,i,n).updates?.find(e=>"replaceDomainEntity"===e.kind||e.parentId===l.id);return e?"replaceDomainEntity"===e.kind?e.replacementEntity.type:e.entity.type:o.recipes[0]?.type},dryRun:()=>r.group.dryRuns?((t,r,i,n)=>{let{entity:a,state:o}=i,{recipes:l,prepareStateEmission:p,find$Entity:d}=n,{updates:u,error:c}=s(t,r,i,n);if(void 0===u)return{ok:!1,root:null,newEntities:[],errors:[{type:"uncaught",entityId:null,message:c}]};let{validity:y,state:m}=p(u,o,"committed");return y.valid?{ok:!0,root:d(m.rootEntityId,m),newEntities:e(u,a.id,{previousState:o,state:m}).flatMap(e=>d(e,m)??[]),errors:[]}:{ok:!1,root:null,newEntities:[],errors:"uncaught"===y.severity?[{type:"uncaught",entityId:null,message:y.error.message}]:y.entityViolations.filter(({violation:e})=>e.severity===y.severity).map(({entityId:e,violation:t})=>({type:t.type,entityId:e,message:void 0===t.message?null:l.resolveLocalizable(t.message)}))}})(t,r,i,n):null}})(n,t,r,i))}})(n,i,o)]:[]});return{...Object.fromEntries(t(o.manifest,i.entity.type).map(e=>[e.edgeName,()=>n(e,i.entity.id,i.state,o.recipes)])),childRecipes:l("child"),replaceRecipes:l("replace")}};
1
+ import{findAssistantFieldOptions as e,findAssistantRecipeGroups as t,modifyEntityUpdateTag as i,prepareAssistantRecipeUpdates as r,readAssistantFieldOptions as n,readAssistantRecipeGroup as s,systemUpdateFactory as a}from"#package/domain";let o=e=>({ok:!1,root:null,changedEntities:[],deletedEntityIds:[],errors:e}),d=(e,{state:t},{recipes:i,prepareStateEmission:r,find$Entity:n})=>{let{validity:s,state:a}=r(e,t,"committed");if(s.valid){let e=new Map(t.entities.map(e=>[e.id,e])),i=new Set(a.entities.map(e=>e.id));return{ok:!0,root:n(a.rootEntityId,a),changedEntities:a.entities.filter(t=>e.get(t.id)!==t).flatMap(e=>n(e.id,a)??[]),deletedEntityIds:t.entities.map(e=>e.id).filter(e=>!i.has(e)),errors:[]}}return o("uncaught"===s.severity?[{type:"uncaught",entityId:null,message:s.error.message}]:s.entityViolations.filter(({violation:e})=>e.severity===s.severity).map(({entityId:e,violation:t})=>({type:t.type,entityId:e,message:void 0===t.message?null:i.resolveLocalizable(t.message)})))};export const createEntityMeta=(l,c)=>{let p=e=>()=>t(c.manifest,e,l.entity.type).flatMap(t=>{let i=s(e,t,l.entity.id,l.state,c.recipes);return i?[((e,t,i)=>{let{group:n,label:s,description:a,recipes:l}=e,{resolveLocalizable:c}=i.recipes;return{groupId:n.groupId,name:void 0===s?n.instanceName:c(s),description:n.description??(void 0===a?null:c(a)),recipes:l.map(n=>((e,t,i,n)=>{let{key:s,item:a}=e,{resolveLocalizable:l}=n.recipes;return{key:s,name:l(a.title),description:void 0===a.description?null:l(a.description),imageUrl:void 0===a.thumbnailUrl?null:l(a.thumbnailUrl),dryRun:()=>t.group.dryRuns?((e,t,i,n)=>{let{updates:s,error:a}=((e,t,{entity:i,state:n},{recipes:s})=>{try{return{updates:r(e,t,i.id,n,s),error:void 0}}catch(e){if("child"===t.target.kind)throw e;return{updates:void 0,error:e instanceof Error?e.message:String(e)}}})(e,t,i,n);return void 0===s?o([{type:"uncaught",entityId:null,message:a}]):d(s,i,n)})(e,t,i,n):null}})(n,e,t,i))}})(i,l,c)]:[]});return{...Object.fromEntries(e(c.manifest,l.entity.type).map(e=>[e.edgeName,()=>n(e,l.entity.id,l.state,c.recipes)?.map(t=>({...t,dryRun:()=>e.dryRuns?(({field:e},{value:t},r,n)=>{let{entity:s}=r,l=Object.entries(Object.entries(n.manifest.writableFields??{}).find(([e])=>e===s.type)?.[1]??{}).find(([t])=>t===e)?.[1];if(void 0===l)throw Error(`The options of "${e}" on "${s.id}" cannot be dry-run: the schema lists them, but the configurator has no writable field "${e}" on ${s.type}. Regenerate the application so the schema matches the configurator.`);let c=r.state.entities.find(e=>e.id===s.id),p={[l]:t},u=c?n.validateEntity({...c,...p}):{success:!0};return u.success?d([a.DomainEntityModification({entityId:s.id,stateDelta:p,updateTag:i})],r,n):o([{type:"uncaught",entityId:s.id,message:`Writing ${JSON.stringify(t)} to "${e}" would make "${s.id}" an invalid ${s.type}: ${u.errors.join("; ")}.${e===l?"":` The message names the stored property: ${e} is stored as ${l}.`} The option comes from the configurator and cannot be corrected from here, so do not write it.`}])})(e,t,l,c):null}))??null])),childRecipes:p("child"),replaceRecipes:p("replace")}};
@@ -1 +1 @@
1
- import{GraphQLError as e,buildSchema as t,defaultFieldResolver as i,getNamedType as o,graphql as a,validateSchema as n}from"graphql";import{assertExistence as r,createSelector as s,isMessageDescriptor as l,isMultilingualString as c}from"@salsita-ai/common/utils";import{DomainEntityIdSchema as h,findAssistantMutation as d}from"#package/domain";import{createEntityMeta as m}from"./entity-meta.js";let p={errors:[new e('No assistant manifest in the application spec. The GraphQL schema is generated from the "assistant" section. Add an "assistant" section to a spec file to expose a schema.')]},u={errors:[new e("The configuration is not loaded yet, so there is no state to query. Run the query again once the configurator has finished loading.")]},f=new RegExp(h.pattern),y=e=>"object"==typeof e&&null!==e&&"type"in e&&"string"==typeof e.type&&"id"in e&&"string"==typeof e.id,g=(e,t)=>"object"==typeof e&&null!==e?Reflect.get(e,t):void 0,v=e=>e instanceof Error?e.message:String(e),S=e=>/[.!?]$/.test(e)?e:`${e}.`,_=e=>"object"==typeof e&&null!==e&&Object.getPrototypeOf(e)===Object.prototype,w=(e,t)=>{let i=_(e)?e._raw:void 0;if(y(i))return i;if(!Array.isArray(e)&&!_(e))return e;if(t.has(e))throw Error("The value contains itself. Return ids or copies of the values instead of objects that reference each other.");let o=new Set([...t,e]);return Array.isArray(e)?e.map(e=>w(e,o)):Object.fromEntries(Object.entries(e).map(([e,t])=>[e,w(t,o)]))},x=(e,t)=>Array.isArray(e)?e.map(e=>x(e,t)):"string"==typeof e||l(e)||c(e)?t(e):e,$=t=>new e(`The GraphQL schema generated from the "assistant" manifest is invalid: ${S(t)} Regenerate the application so the schema is rebuilt from the current spec.`),E=e=>(e.errors?.length??0)>0,b=t=>({errors:[new e(`Not run: operation ${t+1} failed, so the operations after it were skipped and no change of the list takes effect. Fix the failing operation and send the whole list again.`)]});export class GraphQLSystem{_context;_selectSchema=s([s.leaf(e=>e)],e=>{try{let i=t(e),o=n(i);return o.length>0?$(o.map(e=>e.message).join(" ")):i}catch(e){return $(v(e))}});_selectFieldResolver=s([s.leaf(e=>e)],t=>(a,n,r,s)=>{if(s.parentType===s.schema.getQueryType())return i(a,n,r,s);if(s.parentType===s.schema.getMutationType())return this._resolveMutation(s.fieldName,String(s.path.key),n,r);if(!y(a))return i(a,n,r,s);if("meta"===s.fieldName){let e;return m({entity:a,state:("object"==typeof(e=g(a,"_state"))&&null!==e&&"entities"in e&&"rootEntityId"in e?e:void 0)??r.state},{manifest:r.manifest,recipes:this._recipeContext(),prepareStateEmission:(e,t,i)=>this._context.domainContextSelectors.getStoreSystem().prepareStateEmission(e,t,i),find$Entity:(e,t)=>this._find$Entity(e,t)})}let{source:l,localized:c}=t[s.parentType.name]?.[s.fieldName]??{source:s.fieldName},h=((t,i)=>{try{return g(t,i)}catch(o){throw new e(`Expression "${i}" failed on ${y(t)?t.id:"an entity"}: ${S(v(o))} The expression threw while the query read it; fix the expression or the configuration state it reads.`,{originalError:o instanceof Error?o:void 0})}})(a,l);return c?x(h,this._context.domainServicesSelectors.getResolveLocalizable()):l.startsWith("$")&&"JSON"===o(s.returnType).name?((t,i,o)=>{try{return w(o,new Set)}catch(o){throw new e(`Expression "${i}" returned a value that is not plain serializable data on ${y(t)?t.id:"an entity"}: ${S(v(o))} A JSON field can only return data that serializes to JSON.`,{originalError:o instanceof Error?o:void 0})}})(a,l,h):h});constructor(e){this._context=e}_get$Entity=(e,t)=>this._context.domainContextSelectors.getDomain().get$Entity(e,t);_find$Entity=(e,t)=>this._context.domainContextSelectors.getDomain().getEntity(e,t)?this._get$Entity(e,t):null;_recipeContext=()=>({componentReader:this._context.domainContextSelectors.getComponentReader(),recipeCooker:this._context.domainContextSelectors.getRecipeCookerSystem(),resolveLocalizable:this._context.domainServicesSelectors.getResolveLocalizable()});_rootValue={root:(e,{state:t})=>this._get$Entity(t.rootEntityId,t),entities:({ids:t},{state:i})=>{if(null==t)return i.entities.map(e=>this._get$Entity(e.id,i));var o=i.rootEntityId;let a=t.filter(e=>!f.test(e));if(a.length>0)throw new e(`${a.map(e=>`"${e}"`).join(", ")} ${1===a.length?"is not an entity id":"are not entity ids"}. Entity ids have the form "<Type>-<number>", e.g. "${o}". Use ids as returned by the "id" field.`);return t.map(e=>f.test(e)?this._find$Entity(e,i):null)},selectedEntity:(e,{state:t})=>this._find$Entity(this._context.domainContextSelectors.getNavigationSystem().getActiveSelectionState(t),t),navigation:(e,{state:t})=>{let i=this._context.domainContextSelectors.getNavigationSystem();return{navigationState:i.getActiveNavigationState(t),viewState:i.getActiveViewState(t),toolState:i.getActiveToolState(t)}}};_resolveType=e=>y(e)?e.type:void 0;initialize=()=>{};_resolveMutation=(t,i,o,a)=>{if(void 0!==a.failedMutationField)throw new e(`Not run: the earlier field "${a.failedMutationField}" of this operation failed, so "${i}" was skipped and no change of the operation takes effect. Fix the failing field and send the operation again.`);try{return this._runMutation(t,o,a)}catch(e){throw a.failedMutationField=i,e}};_runMutation=(t,i,o)=>{let a=d(t,o.manifest);if(!a)throw new e(`The schema declares the mutation "${t}", but the configurator has no implementation for it. Regenerate the application so the schema matches the configurator.`);let n=o.state,r=a.implementation(i,n,{manifest:o.manifest,domain:this._context.domainContextSelectors.getDomain(),...this._recipeContext()});if(r.updates.length>0){let i=this._context.domainContextSelectors.getStoreSystem().prepareStateEmission(r.updates,n,"committed");if(!i.validity.valid)throw new e(`The changes of "${t}" were not applied, because the resulting configuration is invalid: ${(e=>{if("uncaught"===e.severity)return`validating it threw: ${S(v(e.error))}`;let t=[...new Set(e.entityViolations.filter(({violation:t})=>t.severity===e.severity).map(({entityId:e})=>e))];return`it has ${e.severity} violations on ${t.join(", ")}.`})(i.validity)} Change the request so the configuration stays valid.`);o.emissions.push(i),o.state=i.state}return r.readResult({previousState:n,state:o.state})};_setup=()=>{let t=this._context.specSelectors.getAssistant();if(!t)return{ready:!1,result:p};let i=this._selectSchema(t.sdl);return i instanceof e?{ready:!1,result:{errors:[i]}}:{ready:!0,assistant:t,schema:i,fieldResolver:this._selectFieldResolver(t.fields)}};prepare=async(e,t)=>{let i,o=this._setup();if(!o.ready)return t.map(()=>({result:o.result,emissions:[]}));let n={manifest:o.assistant,state:e,emissions:[],failedMutationField:void 0},r=[];for(let[e,s]of t.entries())if(void 0===i){let t=n.emissions.length;n.failedMutationField=void 0;let l=await a({schema:o.schema,source:s.document,rootValue:this._rootValue,contextValue:n,variableValues:s.variables,operationName:s.operationName,fieldResolver:o.fieldResolver,typeResolver:this._resolveType});r.push({result:l,emissions:n.emissions.slice(t)}),E(l)&&(i=e)}else r.push({result:b(i),emissions:[]});return r};_prepareAndCommit=async e=>{let t,i,o,a=this._context.domainContextSelectors.getStoreSystem(),n=await this.prepare(a.stateSlices.committed.value.state,e),s=n.map(({result:e})=>e),l=n.flatMap(e=>e.emissions);return s.some(E)||0===l.length?{results:s,outcome:"none"}:{results:s,outcome:a.commitPrepared((t=r(l[0]),i=r(l[l.length-1]),o=l.some(e=>void 0!==e.transformationLog),{kind:"committed",previousState:t.previousState,state:i.state,updates:l.flatMap(e=>e.updates),validity:i.validity,transformationLog:o?l.flatMap(e=>e.transformationLog??[]):void 0}))}};execute=async e=>{if(!this._context.domainContextSelectors.getStoreSystem().isInitialized)return{results:e.map(()=>u),committed:!1};let t=await this._prepareAndCommit(e),{results:i,outcome:o}="conflict"===t.outcome?await this._prepareAndCommit(e):t;return"committed"===o?{results:i,committed:!0}:"held"===o?{results:i,committed:!1,message:"The changes were not committed yet: the application held them, usually to ask the user for approval. They may still be applied once the user approves, and the next turn shows whether they were. Do not send the operations again."}:"conflict"===o?{results:i,committed:!1,message:"The changes were not committed, because the configuration changed while they were being applied. The results describe a state that was discarded. Query the current configuration and send the operations again if they still make sense."}:{results:i,committed:!1}}}
1
+ import{GraphQLError as e,buildSchema as t,defaultFieldResolver as i,getNamedType as o,graphql as a,validateSchema as n}from"graphql";import{assertExistence as r,createSelector as s,isMessageDescriptor as l,isMultilingualString as c}from"@salsita-ai/common/utils";import{DomainEntityIdSchema as h,findAssistantMutation as d}from"#package/domain";import{createEntityMeta as m}from"./entity-meta.js";let p={errors:[new e('No assistant manifest in the application spec. The GraphQL schema is generated from the "assistant" section. Add an "assistant" section to a spec file to expose a schema.')]},u={errors:[new e("The configuration is not loaded yet, so there is no state to query. Run the query again once the configurator has finished loading.")]},f=new RegExp(h.pattern),y=e=>"object"==typeof e&&null!==e&&"type"in e&&"string"==typeof e.type&&"id"in e&&"string"==typeof e.id,g=(e,t)=>"object"==typeof e&&null!==e?Reflect.get(e,t):void 0,v=e=>e instanceof Error?e.message:String(e),S=e=>/[.!?]$/.test(e)?e:`${e}.`,_=e=>"object"==typeof e&&null!==e&&Object.getPrototypeOf(e)===Object.prototype,x=(e,t)=>{let i=_(e)?e._raw:void 0;if(y(i))return i;if(!Array.isArray(e)&&!_(e))return e;if(t.has(e))throw Error("The value contains itself. Return ids or copies of the values instead of objects that reference each other.");let o=new Set([...t,e]);return Array.isArray(e)?e.map(e=>x(e,o)):Object.fromEntries(Object.entries(e).map(([e,t])=>[e,x(t,o)]))},w=(e,t)=>Array.isArray(e)?e.map(e=>w(e,t)):"string"==typeof e||l(e)||c(e)?t(e):e,$=t=>new e(`The GraphQL schema generated from the "assistant" manifest is invalid: ${S(t)} Regenerate the application so the schema is rebuilt from the current spec.`),E=e=>(e.errors?.length??0)>0,b=t=>({errors:[new e(`Not run: operation ${t+1} failed, so the operations after it were skipped and no change of the list takes effect. Fix the failing operation and send the whole list again.`)]});export class GraphQLSystem{_context;_selectSchema=s([s.leaf(e=>e)],e=>{try{let i=t(e),o=n(i);return o.length>0?$(o.map(e=>e.message).join(" ")):i}catch(e){return $(v(e))}});_selectFieldResolver=s([s.leaf(e=>e)],t=>(a,n,r,s)=>{if(s.parentType===s.schema.getQueryType())return i(a,n,r,s);if(s.parentType===s.schema.getMutationType())return this._resolveMutation(s.fieldName,String(s.path.key),n,r);if(!y(a))return i(a,n,r,s);if("meta"===s.fieldName){let e;return m({entity:a,state:("object"==typeof(e=g(a,"_state"))&&null!==e&&"entities"in e&&"rootEntityId"in e?e:void 0)??r.state},{manifest:r.manifest,recipes:this._recipeContext(),prepareStateEmission:(e,t,i)=>this._context.domainContextSelectors.getStoreSystem().prepareStateEmission(e,t,i),find$Entity:(e,t)=>this._find$Entity(e,t),validateEntity:e=>this._context.domainContextSelectors.getDomain().validateEntity(e)})}let{source:l,localized:c}=t[s.parentType.name]?.[s.fieldName]??{source:s.fieldName},h=((t,i)=>{try{return g(t,i)}catch(o){throw new e(`Expression "${i}" failed on ${y(t)?t.id:"an entity"}: ${S(v(o))} The expression threw while the query read it; fix the expression or the configuration state it reads.`,{originalError:o instanceof Error?o:void 0})}})(a,l);return c?w(h,this._context.domainServicesSelectors.getResolveLocalizable()):l.startsWith("$")&&"JSON"===o(s.returnType).name?((t,i,o)=>{try{return x(o,new Set)}catch(o){throw new e(`Expression "${i}" returned a value that is not plain serializable data on ${y(t)?t.id:"an entity"}: ${S(v(o))} A JSON field can only return data that serializes to JSON.`,{originalError:o instanceof Error?o:void 0})}})(a,l,h):h});constructor(e){this._context=e}_get$Entity=(e,t)=>this._context.domainContextSelectors.getDomain().get$Entity(e,t);_find$Entity=(e,t)=>this._context.domainContextSelectors.getDomain().getEntity(e,t)?this._get$Entity(e,t):null;_recipeContext=()=>({componentReader:this._context.domainContextSelectors.getComponentReader(),recipeCooker:this._context.domainContextSelectors.getRecipeCookerSystem(),resolveLocalizable:this._context.domainServicesSelectors.getResolveLocalizable()});_rootValue={root:(e,{state:t})=>this._get$Entity(t.rootEntityId,t),entities:({ids:t},{state:i})=>{if(null==t)return i.entities.map(e=>this._get$Entity(e.id,i));var o=i.rootEntityId;let a=t.filter(e=>!f.test(e));if(a.length>0)throw new e(`${a.map(e=>`"${e}"`).join(", ")} ${1===a.length?"is not an entity id":"are not entity ids"}. Entity ids have the form "<Type>-<number>", e.g. "${o}". Use ids as returned by the "id" field.`);return t.map(e=>f.test(e)?this._find$Entity(e,i):null)},selectedEntity:(e,{state:t})=>this._find$Entity(this._context.domainContextSelectors.getNavigationSystem().getActiveSelectionState(t),t),navigation:(e,{state:t})=>{let i=this._context.domainContextSelectors.getNavigationSystem();return{navigationState:i.getActiveNavigationState(t),viewState:i.getActiveViewState(t),toolState:i.getActiveToolState(t)}}};_resolveType=e=>y(e)?e.type:void 0;initialize=()=>{};_resolveMutation=(t,i,o,a)=>{if(void 0!==a.failedMutationField)throw new e(`Not run: the earlier field "${a.failedMutationField}" of this operation failed, so "${i}" was skipped and no change of the operation takes effect. Fix the failing field and send the operation again.`);try{return this._runMutation(t,o,a)}catch(e){throw a.failedMutationField=i,e}};_runMutation=(t,i,o)=>{let a=d(t,o.manifest);if(!a)throw new e(`The schema declares the mutation "${t}", but the configurator has no implementation for it. Regenerate the application so the schema matches the configurator.`);let n=o.state,r=a.implementation(i,n,{manifest:o.manifest,domain:this._context.domainContextSelectors.getDomain(),...this._recipeContext()});if(r.updates.length>0){let i=this._context.domainContextSelectors.getStoreSystem().prepareStateEmission(r.updates,n,"committed");if(!i.validity.valid)throw new e(`The changes of "${t}" were not applied, because the resulting configuration is invalid: ${(e=>{if("uncaught"===e.severity)return`validating it threw: ${S(v(e.error))}`;let t=[...new Set(e.entityViolations.filter(({violation:t})=>t.severity===e.severity).map(({entityId:e})=>e))];return`it has ${e.severity} violations on ${t.join(", ")}.`})(i.validity)} Change the request so the configuration stays valid.`);o.emissions.push(i),o.state=i.state}return r.readResult({previousState:n,state:o.state})};_setup=()=>{let t=this._context.specSelectors.getAssistant();if(!t)return{ready:!1,result:p};let i=this._selectSchema(t.sdl);return i instanceof e?{ready:!1,result:{errors:[i]}}:{ready:!0,assistant:t,schema:i,fieldResolver:this._selectFieldResolver(t.fields)}};prepare=async(e,t)=>{let i,o=this._setup();if(!o.ready)return t.map(()=>({result:o.result,emissions:[]}));let n={manifest:o.assistant,state:e,emissions:[],failedMutationField:void 0},r=[];for(let[e,s]of t.entries())if(void 0===i){let t=n.emissions.length;n.failedMutationField=void 0;let l=await a({schema:o.schema,source:s.document,rootValue:this._rootValue,contextValue:n,variableValues:s.variables,operationName:s.operationName,fieldResolver:o.fieldResolver,typeResolver:this._resolveType});r.push({result:l,emissions:n.emissions.slice(t)}),E(l)&&(i=e)}else r.push({result:b(i),emissions:[]});return r};_prepareAndCommit=async e=>{let t,i,o,a=this._context.domainContextSelectors.getStoreSystem(),n=await this.prepare(a.stateSlices.committed.value.state,e),s=n.map(({result:e})=>e),l=n.flatMap(e=>e.emissions);return s.some(E)||0===l.length?{results:s,outcome:"none"}:{results:s,outcome:a.commitPrepared((t=r(l[0]),i=r(l[l.length-1]),o=l.some(e=>void 0!==e.transformationLog),{kind:"committed",previousState:t.previousState,state:i.state,updates:l.flatMap(e=>e.updates),validity:i.validity,transformationLog:o?l.flatMap(e=>e.transformationLog??[]):void 0}))}};execute=async e=>{if(!this._context.domainContextSelectors.getStoreSystem().isInitialized)return{results:e.map(()=>u),committed:!1};let t=await this._prepareAndCommit(e),{results:i,outcome:o}="conflict"===t.outcome?await this._prepareAndCommit(e):t;return"committed"===o?{results:i,committed:!0}:"held"===o?{results:i,committed:!1,message:"The changes were not committed yet: the application held them, usually to ask the user for approval. They may still be applied once the user approves, and the next turn shows whether they were. Do not send the operations again."}:"conflict"===o?{results:i,committed:!1,message:"The changes were not committed, because the configuration changed while they were being applied. The results describe a state that was discarded. Query the current configuration and send the operations again if they still make sense."}:{results:i,committed:!1}}}
@@ -1,5 +1,5 @@
1
1
  {
2
- "generatedAt": "2026-10-07T13:21:27.057Z",
2
+ "generatedAt": "2026-10-07T17:54:56.392Z",
3
3
  "categories": [
4
4
  {
5
5
  "name": "Phoenix",
@@ -149,13 +149,17 @@ Host must pass the nonce into the web component:
149
149
  <salsita-configurator nonce="YOUR_REQUEST_NONCE"></salsita-configurator>
150
150
  ```
151
151
 
152
+ ### Starting Policy from Studio
153
+
154
+ The **CSP** tab of an environment in Studio generates a starting `<meta http-equiv="Content-Security-Policy">` tag with that environment's configurator, server and S3 bucket origins filled in. Its `script-src` and `style-src` carry the `'nonce-YOUR_REQUEST_NONCE'` placeholder: the host still has to replace it with a cryptographically random nonce generated for every request and pass the same value to `<salsita-configurator nonce="…">`. Extend the policy with any origins the host page itself needs.
155
+
152
156
  ## What Phoenix Does Not Configure
153
157
 
154
158
  Phoenix options only change SDK runtime internals. They **do not**:
155
159
 
156
160
  - set CSP headers for your hosting platform
157
- - define `script-src`, `style-src`, `connect-src`, etc.
158
- - choose your nonce strategy globally
161
+ - enforce `script-src`, `style-src`, `connect-src`, etc. (the Studio CSP tab only suggests a starting policy)
162
+ - generate nonces or choose your nonce strategy
159
163
 
160
164
  CSP policy itself is owned by the host page / hosting infra.
161
165
 
@@ -30,6 +30,7 @@ pnpm phoenix dev
30
30
  1. Refuses to run when a `spec/` folder already exists, so it never touches an existing project.
31
31
  2. Writes a `package.json` that depends on the CLI's SDK version when the folder has none. An existing `package.json` is kept, as long as it depends on the SDK.
32
32
  3. Writes a starter spec (`spec/application.yaml`, `spec/configurator.yaml`) with one resizable, recolorable box, and switches `package.json` to the application-spec pipeline.
33
+ 4. Writes `pnpm-workspace.yaml` with the pnpm settings the first install needs: the build scripts of the SDK's dependencies are allowed or denied, and `@salsita-ai/*` packages are exempt from pnpm's minimum release age. A project inside a pnpm monorepo gets no file; the workspace root holds these settings.
33
34
 
34
35
  It asks nothing, writes no `.env` and contacts no server. `phoenix sync` then generates every other project file, including `AGENTS.md` and the agent skills described in [Project Structure](?path=/docs/phoenix-project-structure--docs).
35
36
 
@@ -45,7 +46,7 @@ The starter `spec/application.yaml` starts with `project: default`. `project` na
45
46
 
46
47
  ### How `phoenix dev` authenticates
47
48
 
48
- - **Signed in with `phoenix login`** (SDK-builder workspaces): nothing else is needed. On start, `phoenix dev` asks the server for your development environment in the project; the server creates it on the first run. Every user gets their own development environment, so teammates don't overwrite each other's schema or webhook tunnel. The server comes from `--serverUrl`, then `SERVER_URL` in `.env`, then the region (`--region` or `PHOENIX_REGION`, default `eu`).
49
+ - **Signed in with `phoenix login`** (SDK-builder workspaces): nothing else is needed. On start, `phoenix dev` asks the server for your development environment in the project; the server creates it on the first run. Every user gets their own development environment, so teammates don't overwrite each other's schema or webhook tunnel. The server comes from `--serverUrl`, then `SERVER_URL` in `.env`, then the region (`--region` or `PHOENIX_REGION`, default `eu`). `phoenix dev` hands that server to the preview as `SERVER_URL`, so the preview talks to the server the development deployment was prepared on even without a `.env`, and prints the preview address as `Preview: <url>` once the dev server is up (also written to `.phoenix/phoenix-dev.log`).
49
50
  - **With a Studio development environment**: create the project and a development environment in PHX Studio and paste its `.env` block (`SERVER_URL`, `PROJECT_SLUG`, `ENVIRONMENT_SLUG`, `DEVELOPMENT_KEY`) into `.env`. A `DEVELOPMENT_KEY` always takes precedence over the sign-in, and `PROJECT_SLUG` overrides the starter spec's `project: default`, which a development key cannot resolve.
50
51
 
51
52
  ## Local Development Setup
@@ -282,19 +283,20 @@ The Phoenix CLI auto-generates config files that read these variables:
282
283
 
283
284
  Starts the full development environment: Vite dev server, development webhook tunnel, domain-cli watcher, and editor WebSocket server.
284
285
 
285
- | Flag | Env Variable | Default | Description |
286
- | ------------------- | ------------------ | -------------------- | -------------------------------------------------------------------- |
287
- | `--workspace` | — | from `.phoenix.json` | Override workspace mode |
288
- | `--editor` | `ENABLE_EDITOR` | `true` | Enable editor file generation |
289
- | `--editorWsPort` | `EDITOR_WS_PORT` | `3020` | Editor WebSocket port |
290
- | `--debug` | `DEBUG` | `false` | Verbose logging |
291
- | `--projectSlug` | `PROJECT_SLUG` | `local` | Project slug |
292
- | `--environmentSlug` | `ENVIRONMENT_SLUG` | package.json name | Environment slug |
293
- | `--withLingui` | `WITH_LINGUI` | `false` | Enable i18n extraction/compilation and watch i18n sources during dev |
294
- | `--persistence` | `PERSISTENCE_MODE` | `server` | Persistence mode |
295
- | `--force` / `-f` | `FORCE_REBUILD` | `false` | Force full rebuild, skip cache |
296
- | `--serverUrl` | `SERVER_URL` | the region's server | Phoenix server URL |
297
- | `--region` | `PHOENIX_REGION` | `eu` | Region of the signed-in workspace, used when no server URL is set |
286
+ | Flag | Env Variable | Default | Description |
287
+ | ---------------------- | ------------------ | -------------------- | ------------------------------------------------------------------------------- |
288
+ | `--workspace` | — | from `.phoenix.json` | Override workspace mode |
289
+ | `--editor` | `ENABLE_EDITOR` | `true` | Enable editor file generation |
290
+ | `--editorWsPort` | `EDITOR_WS_PORT` | `3020` | Editor WebSocket port |
291
+ | `--debug` | `DEBUG` | `false` | Verbose logging |
292
+ | `--projectSlug` | `PROJECT_SLUG` | `local` | Project slug |
293
+ | `--environmentSlug` | `ENVIRONMENT_SLUG` | package.json name | Environment slug |
294
+ | `--withLingui` | `WITH_LINGUI` | `false` | Enable i18n extraction/compilation and watch i18n sources during dev |
295
+ | `--persistence` | `PERSISTENCE_MODE` | `server` | Persistence mode |
296
+ | `--force` / `-f` | `FORCE_REBUILD` | `false` | Force full rebuild, skip cache |
297
+ | `--serverUrl` | `SERVER_URL` | the region's server | Phoenix server URL |
298
+ | `--region` | `PHOENIX_REGION` | `eu` | Region of the signed-in workspace, used when no server URL is set |
299
+ | `--skipUpload` / `-su` | `SKIP_UPLOAD` | `false` | Skip the S3 asset upload when the development deployment is prepared at startup |
298
300
 
299
301
  In `server` persistence mode the project comes from `--projectSlug`, `PROJECT_SLUG` or `project` in the spec, and the credential from `DEVELOPMENT_KEY` or the `phoenix login` sign-in; see [Starting a New Project](#starting-a-new-project). When signed in, the environment is always your own development environment and `--environmentSlug` is ignored.
300
302
 
@@ -358,7 +360,7 @@ Removes all build artifacts (`.phoenix/`, `node_modules/`), reinstalls dependenc
358
360
 
359
361
  ### `phoenix sync`
360
362
 
361
- Syncs project dependencies and regenerates all project files without building Vite bundles or webhooks. Faster than `clean` — use this for routine updates after changing spec files or upgrading dependencies.
363
+ Syncs project dependencies and regenerates all project files without building Vite bundles or webhooks. Faster than `clean` — use this for routine updates after changing spec files or upgrading dependencies. Before installing, it adds the pnpm settings `phoenix init` writes to a standalone project's `pnpm-workspace.yaml` when they are missing; values you set there are kept.
362
364
 
363
365
  | Flag | Env Variable | Default | Description |
364
366
  | ---------------- | --------------- | ------- | ------------------------------ |
@@ -571,7 +573,7 @@ Signs the CLI in to the SDK-builder workspace `<customer>`:
571
573
  2. Studio sends a one-time code back to the CLI on `127.0.0.1`.
572
574
  3. The CLI exchanges the code for a token.
573
575
 
574
- The token is stored in `~/.phoenix/credentials.json`, readable only by you, per server; `PHOENIX_CREDENTIALS_FILE` moves the file. The token works until you revoke it. Signing in again to the same server revokes the token it replaces; the CLI warns if the server could not revoke it.
576
+ The token is stored in `~/.phoenix/credentials.json`, readable only by you, per server, together with the Studio URL of the sign-in; `PHOENIX_CREDENTIALS_FILE` moves the file. The token works until you revoke it. Signing in again to the same server revokes the token it replaces; the CLI warns if the server could not revoke it.
575
577
 
576
578
  | Flag | Default | Description |
577
579
  | --------------------- | ------------------------------------ | ------------------------------------------------ |
@@ -590,7 +592,7 @@ Revokes the token on the server and removes it from the credentials file. `--reg
590
592
 
591
593
  ### `phoenix deploy`
592
594
 
593
- Validates the spec, packs and uploads the project, starts its build on the server and follows it until the configurator is live. `--region` or `--serverUrl` selects the server, as for `login`; `--no-wait` returns as soon as the build starts.
595
+ Validates the spec, adds the pnpm settings `phoenix init` writes to `pnpm-workspace.yaml` when they are missing, packs and uploads the project, starts its build on the server and follows it until the configurator is live. Once the build starts it prints `Follow the build in Studio: <url>`, the build's page in the Studio you signed in through; a sign-in stored by an older CLI has no Studio URL, so sign in again to get the link. `--region` or `--serverUrl` selects the server, as for `login`; `--no-wait` returns as soon as the build starts.
594
596
 
595
597
  ### `phoenix feedback <message>`
596
598
 
@@ -22,10 +22,12 @@ pnpm install
22
22
  pnpm phoenix sync
23
23
  ```
24
24
 
25
- `phoenix init [folder]` writes the starter project: `package.json` and the starter spec, `spec/application.yaml` and `spec/configurator.yaml`, which describe one box you can resize and recolour. The starter depends on the same SDK version as the CLI that created it. `phoenix sync` then adds the rest of the project: package scripts, webhooks, README and lint config.
25
+ `phoenix init [folder]` writes the starter project: `package.json`, `pnpm-workspace.yaml` and the starter spec, `spec/application.yaml` and `spec/configurator.yaml`, which describe one box you can resize and recolour. The starter depends on the same SDK version as the CLI that created it. `phoenix sync` then adds the rest of the project: package scripts, webhooks, README and lint config.
26
26
 
27
27
  `init` only writes these files: it asks nothing and contacts no server. It refuses a folder that already has a `spec/` folder. A `package.json` already in the folder is kept, as long as it depends on the SDK.
28
28
 
29
+ `pnpm-workspace.yaml` holds the pnpm settings the install needs: it allows the build scripts of the SDK's dependencies and exempts `@salsita-ai/*` packages from pnpm's minimum release age, so a build of the SDK published today installs. The file is yours to edit, for example to allow the build script of a dependency you add. `phoenix sync` adds these entries when they are missing and keeps the values you set. A project inside a pnpm monorepo gets no file of its own; the workspace root holds the settings.
30
+
29
31
  The starter `spec/application.yaml` begins with `project: default`, which means the default project of the workspace you sign in to. Leave it as it is; a workspace has one project.
30
32
 
31
33
  ## 2. Sign in
@@ -55,12 +57,14 @@ The CLI stores the token in `~/.phoenix/credentials.json`, readable only by you,
55
57
  pnpm phoenix dev
56
58
  ```
57
59
 
58
- Signed in, `phoenix dev` needs no `.env` and nothing set up in Studio. When it starts, it asks the server for your development environment in the project the spec names. The server creates that environment the first time and finds it again on every later run. `phoenix dev` prints the workspace, project and environment it works in, prepares a development deployment there and opens the webhook tunnel, as for any Phoenix project (see [Phoenix CLI](?path=/docs/phoenix-phoenix-cli--docs)).
60
+ Signed in, `phoenix dev` needs no `.env` and nothing set up in Studio. When it starts, it asks the server for your development environment in the project the spec names. The server creates that environment the first time and finds it again on every later run. `phoenix dev` prints the workspace, project and environment it works in, prepares a development deployment there and opens the webhook tunnel, as for any Phoenix project (see [Phoenix CLI](?path=/docs/phoenix-phoenix-cli--docs)). It then prints the address of the preview as `Preview: <url>`; the preview talks to the server the deployment was prepared on.
59
61
 
60
62
  Every user of the workspace gets their own development environment, so a teammate who clones the project and signs in doesn't overwrite your schema or take over your webhooks. The development environment is separate from `production`: nothing you do in `phoenix dev` changes the live configurator.
61
63
 
62
64
  `pnpm dev:local` still runs the project without any server, with state kept in the browser.
63
65
 
66
+ Coding agents follow the generated `phoenix` skill: before their first change they start `phoenix dev` and open the preview in your browser, and after `phoenix deploy` starts a build they open its page in Studio.
67
+
64
68
  To build the configurator with your coding agent, give it your product documents, prices, pictures, drawings or models and ask it to create the configurator. The generated `phoenix-create-configurator` skill guides it through five stages: a representative first slice you can try early, a plan of the whole configuration experience, options and features added in small increments, finalization, and a complete review. It studies the SDK documentation before building, keeps a `configurator-spec.md` in the project as the record of what is decided and what remains open, and asks you only for decisions it cannot derive from your materials.
65
69
 
66
70
  ## 4. Deploy
@@ -72,11 +76,11 @@ pnpm phoenix deploy
72
76
  `phoenix deploy` goes through these steps:
73
77
 
74
78
  1. It validates the application spec.
75
- 2. It packs the project files: the `.yaml` files in `spec`, everything in `src`, `mocks`, `assets`, `public` and `templates`, and `package.json`, `pnpm-lock.yaml`, `.gitignore`, `CLAUDE.md` and `.cursor/rules/phoenix.mdc`. Every `node_modules`, `.phoenix` and `__generated` folder, every `.DS_Store` and `._*` file and every `.env` and `.env.*` file are left out, at any depth.
79
+ 2. It packs the project files: the `.yaml` files in `spec`, everything in `src`, `mocks`, `assets`, `public` and `templates`, and `package.json`, `pnpm-lock.yaml`, `pnpm-workspace.yaml`, `.gitignore`, `CLAUDE.md` and `.cursor/rules/phoenix.mdc`. Every `node_modules`, `.phoenix` and `__generated` folder, every `.DS_Store` and `._*` file and every `.env` and `.env.*` file are left out, at any depth.
76
80
  3. It uploads the archive, which may be up to 200 MB.
77
- 4. It starts the build and follows it until your configurator is live, then prints its URL.
81
+ 4. It starts the build, prints a link to the build's page in Studio and follows the build until your configurator is live, then prints its URL.
78
82
 
79
- On the server, the build runs without platform secrets. It sees only your own environment variables and the public platform values, the [build environment](?path=/docs/phoenix-phoenix-cli--docs) that `phoenix deployment:build-envs` writes. The trusted deploy step then publishes the frontend, webhooks and assets.
83
+ On the server, the install follows your `pnpm-lock.yaml` and `pnpm-workspace.yaml`, so it allows the same build scripts and package versions as `pnpm install` on your computer. The build runs without platform secrets. It sees only your own environment variables and the public platform values, the [build environment](?path=/docs/phoenix-phoenix-cli--docs) that `phoenix deployment:build-envs` writes. The trusted deploy step then publishes the frontend, webhooks and assets.
80
84
 
81
85
  If the build fails, is cancelled or takes too long, `phoenix deploy` prints the end of the build log and exits with an error. `--no-wait` returns as soon as the build starts.
82
86
 
@@ -90,7 +90,8 @@ export interface AssistantRecipeGroup {
90
90
  * The options edge of one writable field the assistant manifest links to its component. `field` is
91
91
  * the delta field of `modify<Type>` the choices are for, `edgeName` the field of `<Type>Meta` that
92
92
  * lists them, `componentType` and `instanceName` the component instance they are read from, and
93
- * `validate` whether `modify<Type>` rejects a value that is not among them.
93
+ * `validate` whether `modify<Type>` rejects a value that is not among them, and `dryRuns` whether
94
+ * the choices can be dry-run.
94
95
  */
95
96
  export interface AssistantFieldOptions {
96
97
  readonly field: string;
@@ -98,6 +99,7 @@ export interface AssistantFieldOptions {
98
99
  readonly componentType: 'materialSwatch' | 'attributeSelection' | 'distanceDimensionSelect';
99
100
  readonly instanceName: string;
100
101
  readonly validate: boolean;
102
+ readonly dryRuns: boolean;
101
103
  }
102
104
  /**
103
105
  * Experimental assistant manifest generated from the application spec `assistant` section. `sdl` is
@@ -1,6 +1,6 @@
1
1
  import { type StateUpdate } from '../components/updates.js';
2
2
  import type { Domain } from '../core/domain.js';
3
- import type { $DomainEntity, DomainEntity } from '../core/model.js';
3
+ import { type $DomainEntity, type DomainEntity } from '../core/model.js';
4
4
  import type { DomainState } from '../core/state.js';
5
5
  import type { AssistantManifest } from './application-config.js';
6
6
  import { type AssistantRecipeContext } from './assistant-recipe-groups.js';