@aglyn/plugins-ai 1.0.0-beta.217 → 1.0.0-beta.219

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 (187) hide show
  1. package/README.md +2 -2
  2. package/package.json +17 -17
  3. package/src/index.d.ts +1 -0
  4. package/src/index.js.map +1 -1
  5. package/src/lib/activity/ai-activity-actions.js +10 -2
  6. package/src/lib/activity/ai-activity-actions.js.map +1 -1
  7. package/src/lib/components/ai-assist-provider.component.js +2 -2
  8. package/src/lib/components/ai-assist-provider.component.js.map +1 -1
  9. package/src/lib/components/ai-brief-dialog.component.js +3 -3
  10. package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
  11. package/src/lib/components/ai-experiment-cards.component.js +1 -1
  12. package/src/lib/components/ai-experiment-cards.component.js.map +1 -1
  13. package/src/lib/components/ai-job-plan.component.d.ts +8 -0
  14. package/src/lib/components/ai-job-plan.component.js +21 -4
  15. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  16. package/src/lib/components/ai-permissions-on-host.component.d.ts +12 -2
  17. package/src/lib/components/ai-permissions-on-host.component.js +12 -3
  18. package/src/lib/components/ai-permissions-on-host.component.js.map +1 -1
  19. package/src/lib/components/ai-product-copy-card.component.d.ts +1 -1
  20. package/src/lib/components/ai-product-copy-card.component.js.map +1 -1
  21. package/src/lib/components/ai-product-import-option.component.d.ts +1 -1
  22. package/src/lib/components/ai-product-import-option.component.js.map +1 -1
  23. package/src/lib/components/ai-product-zones.d.ts +124 -0
  24. package/src/lib/components/ai-product-zones.js +32 -0
  25. package/src/lib/components/ai-product-zones.js.map +1 -0
  26. package/src/lib/components/ai-products-hub-card.component.d.ts +1 -1
  27. package/src/lib/components/ai-products-hub-card.component.js.map +1 -1
  28. package/src/lib/components/ai-products-proposals.component.d.ts +1 -1
  29. package/src/lib/components/ai-products-proposals.component.js.map +1 -1
  30. package/src/lib/components/ai-seo-audit-card.component.js +35 -92
  31. package/src/lib/components/ai-seo-audit-card.component.js.map +1 -1
  32. package/src/lib/components/ai-seo-fields-card.component.js.map +1 -1
  33. package/src/lib/components/ai-theme-proposal-card.component.js.map +1 -1
  34. package/src/lib/components/assist-jobs-drawer.component.js +21 -15
  35. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  36. package/src/lib/components/assist-panel.component.js +2 -2
  37. package/src/lib/components/assist-panel.component.js.map +1 -1
  38. package/src/lib/components/assist-signals-page.component.js +2 -2
  39. package/src/lib/components/assist-signals-page.component.js.map +1 -1
  40. package/src/lib/jobs/ai-email-bindings.d.ts +28 -21
  41. package/src/lib/jobs/ai-email-bindings.js +75 -42
  42. package/src/lib/jobs/ai-email-bindings.js.map +1 -1
  43. package/src/lib/jobs/ai-job-campaign-step.d.ts +0 -2
  44. package/src/lib/jobs/ai-job-campaign-step.js +5 -10
  45. package/src/lib/jobs/ai-job-campaign-step.js.map +1 -1
  46. package/src/lib/jobs/ai-job-component-step.js +9 -3
  47. package/src/lib/jobs/ai-job-component-step.js.map +1 -1
  48. package/src/lib/jobs/ai-job-drafts.d.ts +9 -1
  49. package/src/lib/jobs/ai-job-drafts.js +12 -5
  50. package/src/lib/jobs/ai-job-drafts.js.map +1 -1
  51. package/src/lib/jobs/ai-job-email-step.d.ts +0 -2
  52. package/src/lib/jobs/ai-job-email-step.js +1 -3
  53. package/src/lib/jobs/ai-job-email-step.js.map +1 -1
  54. package/src/lib/jobs/ai-job-experiment-step.d.ts +1 -1
  55. package/src/lib/jobs/ai-job-experiment-step.js +1 -1
  56. package/src/lib/jobs/ai-job-experiment-step.js.map +1 -1
  57. package/src/lib/jobs/ai-job-generation.d.ts +1 -1
  58. package/src/lib/jobs/ai-job-generation.js +10 -2
  59. package/src/lib/jobs/ai-job-generation.js.map +1 -1
  60. package/src/lib/jobs/ai-job-layout-step.js +1 -1
  61. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  62. package/src/lib/jobs/ai-job-page-sections.d.ts +4 -2
  63. package/src/lib/jobs/ai-job-page-sections.js +7 -4
  64. package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
  65. package/src/lib/jobs/ai-job-page-step.js +7 -2
  66. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  67. package/src/lib/jobs/ai-job-plan-conformance.js +33 -0
  68. package/src/lib/jobs/ai-job-plan-conformance.js.map +1 -1
  69. package/src/lib/jobs/ai-job-plan-step.d.ts +3 -0
  70. package/src/lib/jobs/ai-job-plan-step.js +29 -17
  71. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  72. package/src/lib/jobs/ai-job-plugin-drafts.d.ts +14 -6
  73. package/src/lib/jobs/ai-job-plugin-drafts.js +12 -4
  74. package/src/lib/jobs/ai-job-plugin-drafts.js.map +1 -1
  75. package/src/lib/jobs/ai-job-products-step.d.ts +7 -0
  76. package/src/lib/jobs/ai-job-products-step.js +54 -32
  77. package/src/lib/jobs/ai-job-products-step.js.map +1 -1
  78. package/src/lib/jobs/ai-job-seo-step.d.ts +3 -24
  79. package/src/lib/jobs/ai-job-seo-step.js +35 -108
  80. package/src/lib/jobs/ai-job-seo-step.js.map +1 -1
  81. package/src/lib/jobs/ai-job-template-step.js +1 -0
  82. package/src/lib/jobs/ai-job-template-step.js.map +1 -1
  83. package/src/lib/jobs/ai-job-theme-step.d.ts +1 -1
  84. package/src/lib/jobs/ai-job-theme-step.js +1 -1
  85. package/src/lib/jobs/ai-job-theme-step.js.map +1 -1
  86. package/src/lib/jobs/ai-job-workflow-step.js +17 -9
  87. package/src/lib/jobs/ai-job-workflow-step.js.map +1 -1
  88. package/src/lib/jobs/ai-workflow-records.d.ts +19 -11
  89. package/src/lib/jobs/ai-workflow-records.js +60 -27
  90. package/src/lib/jobs/ai-workflow-records.js.map +1 -1
  91. package/src/lib/model/ai-allotments.d.ts +11 -3
  92. package/src/lib/model/ai-allotments.js +21 -4
  93. package/src/lib/model/ai-allotments.js.map +1 -1
  94. package/src/lib/model/ai-automation-outline.d.ts +21 -2
  95. package/src/lib/model/ai-automation-outline.js.map +1 -1
  96. package/src/lib/model/ai-build-plan.d.ts +57 -0
  97. package/src/lib/model/ai-build-plan.js +116 -7
  98. package/src/lib/model/ai-build-plan.js.map +1 -1
  99. package/src/lib/model/ai-seo.d.ts +11 -51
  100. package/src/lib/model/ai-seo.js +0 -17
  101. package/src/lib/model/ai-seo.js.map +1 -1
  102. package/src/lib/model/ai-site-inventory.js +1 -1
  103. package/src/lib/model/ai-site-inventory.js.map +1 -1
  104. package/src/lib/model/ai-template-subjects.js +2 -2
  105. package/src/lib/model/ai-template-subjects.js.map +1 -1
  106. package/src/lib/model/ai-theme-proposal.d.ts +1 -1
  107. package/src/lib/model/ai-theme-proposal.js +1 -1
  108. package/src/lib/model/ai-theme-proposal.js.map +1 -1
  109. package/src/lib/model/ai-workflow-job.d.ts +1 -3
  110. package/src/lib/model/ai-workflow-job.js +1 -2
  111. package/src/lib/model/ai-workflow-job.js.map +1 -1
  112. package/src/lib/plan-entitlements.d.ts +149 -0
  113. package/src/lib/plan-entitlements.js +168 -0
  114. package/src/lib/plan-entitlements.js.map +1 -0
  115. package/src/lib/plugin.js +1 -1
  116. package/src/lib/plugin.js.map +1 -1
  117. package/src/lib/runtime/ai-doctrine-validators.d.ts +25 -4
  118. package/src/lib/runtime/ai-doctrine-validators.js +179 -23
  119. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  120. package/src/lib/runtime/ai-doctrine.js +5 -5
  121. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  122. package/src/lib/runtime/ai-eval-live.js +29 -14
  123. package/src/lib/runtime/ai-eval-live.js.map +1 -1
  124. package/src/lib/runtime/ai-eval.d.ts +8 -1
  125. package/src/lib/runtime/ai-eval.js +29 -2
  126. package/src/lib/runtime/ai-eval.js.map +1 -1
  127. package/src/lib/runtime/ai-node-tree.js +3 -3
  128. package/src/lib/runtime/ai-node-tree.js.map +1 -1
  129. package/src/lib/runtime/ai-palette.d.ts +6 -0
  130. package/src/lib/runtime/ai-palette.generated.js +21 -255
  131. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  132. package/src/lib/runtime/ai-palette.js +7 -1
  133. package/src/lib/runtime/ai-palette.js.map +1 -1
  134. package/src/lib/runtime/seo-audit.d.ts +13 -63
  135. package/src/lib/runtime/seo-audit.js +22 -207
  136. package/src/lib/runtime/seo-audit.js.map +1 -1
  137. package/src/lib/runtime/site-inventory.js +1 -1
  138. package/src/lib/runtime/site-inventory.js.map +1 -1
  139. package/src/lib/server/ai-admin-org.js +4 -25
  140. package/src/lib/server/ai-admin-org.js.map +1 -1
  141. package/src/lib/server/ai-admin-user.js +2 -19
  142. package/src/lib/server/ai-admin-user.js.map +1 -1
  143. package/src/lib/server/ai-assist-prompts.js +3 -3
  144. package/src/lib/server/ai-assist-prompts.js.map +1 -1
  145. package/src/lib/server/ai-insight-digest-route.js +8 -1
  146. package/src/lib/server/ai-insight-digest-route.js.map +1 -1
  147. package/src/lib/server/assist-chat.js +11 -10
  148. package/src/lib/server/assist-chat.js.map +1 -1
  149. package/src/lib/server/assist-view-context.js +23 -23
  150. package/src/lib/server/assist-view-context.js.map +1 -1
  151. package/src/lib/server.d.ts +1 -0
  152. package/src/lib/server.js.map +1 -1
  153. package/src/lib/testing/stand-in-automation-index.d.ts +20 -0
  154. package/src/lib/testing/stand-in-automation-index.js +102 -0
  155. package/src/lib/testing/stand-in-automation-index.js.map +1 -0
  156. package/src/lib/testing/stand-in-list-send-time.d.ts +4 -0
  157. package/src/lib/testing/stand-in-list-send-time.js +77 -0
  158. package/src/lib/testing/stand-in-list-send-time.js.map +1 -0
  159. package/src/lib/testing/stand-in-pipeline-index.d.ts +32 -0
  160. package/src/lib/testing/stand-in-pipeline-index.js +59 -0
  161. package/src/lib/testing/stand-in-pipeline-index.js.map +1 -0
  162. package/src/lib/testing/stand-in-product-index.d.ts +20 -0
  163. package/src/lib/testing/stand-in-product-index.js +105 -0
  164. package/src/lib/testing/stand-in-product-index.js.map +1 -0
  165. package/src/lib/tools/ai-component-tool.js +2 -2
  166. package/src/lib/tools/ai-component-tool.js.map +1 -1
  167. package/src/lib/tools/ai-duplicate-resource-tool.js +1 -1
  168. package/src/lib/tools/ai-duplicate-resource-tool.js.map +1 -1
  169. package/src/lib/tools/ai-seo-tool.d.ts +6 -18
  170. package/src/lib/tools/ai-seo-tool.js +7 -38
  171. package/src/lib/tools/ai-seo-tool.js.map +1 -1
  172. package/src/lib/tools/ai-workflow-tool.d.ts +1 -1
  173. package/src/lib/tools/ai-workflow-tool.js +1 -1
  174. package/src/lib/tools/ai-workflow-tool.js.map +1 -1
  175. package/src/lib/usage/ai-allotment-alerts.d.ts +3 -0
  176. package/src/lib/usage/ai-allotment-alerts.js +20 -8
  177. package/src/lib/usage/ai-allotment-alerts.js.map +1 -1
  178. package/src/lib/usage/assist-free-taste.js +2 -1
  179. package/src/lib/usage/assist-free-taste.js.map +1 -1
  180. package/src/lib/usage/assist-signal-mining.js +1 -1
  181. package/src/lib/usage/assist-signal-mining.js.map +1 -1
  182. package/src/lib/usage-axes.d.ts +39 -0
  183. package/src/lib/usage-axes.js +70 -0
  184. package/src/lib/usage-axes.js.map +1 -0
  185. package/src/lib/runtime/seo-page-facts.d.ts +0 -61
  186. package/src/lib/runtime/seo-page-facts.js +0 -150
  187. package/src/lib/runtime/seo-page-facts.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-experiment-step.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AI_EXPERIMENT_FIGURE_READER,\n AI_EXPERIMENT_MAX_VARIANTS,\n AI_EXPERIMENT_MIN_VARIANTS,\n AI_EXPERIMENT_SUBJECT_MAX_CHARS,\n aiExperimentNextStep,\n aiExperimentTarget,\n aiExperimentTask,\n aiExperimentVerdictNamesWinner,\n checkAiExperimentExplanation,\n checkAiExperimentVariants,\n readAiExperimentArms,\n readAiExperimentResult,\n type AiExperimentArm,\n type AiExperimentExplanation,\n type AiExperimentReading,\n type AiExperimentTarget,\n type AiExperimentVariantsProposal,\n} from '../model/ai-experiment'\nimport type { AiJob, AiJobOutput } from '../model/ai-jobs.types'\nimport { AI_STEP_TIERS } from '../providers/catalog'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport {\n AI_ACCEPTABLE_USE_BLOCK,\n runAiRequest,\n type AiMessage,\n type AiSystemBlock,\n} from '../runtime/ai-runtime'\nimport { AI_GENERATION_MAX_ATTEMPTS } from '../runtime/ai-generation-bounds'\nimport { aiInsightReaders, readAiInsightTables } from '../insights/ai-insight-readers'\nimport {\n registerAiJobAdmission,\n type AiJobAdmissionContext,\n type AiJobAdmissionRefusal,\n} from './ai-job-admission'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport type { AiJobStepContext, AiJobStepOutcome, AiJobStepRunner } from './ai-job-text-step'\nimport { registerAiJobStep } from './ai-jobs'\nimport {\n AI_EXPERIMENT_EXPLAIN_TOOL_NAME,\n AI_EXPERIMENT_VARIANTS_TOOL_NAME,\n aiExperimentExplainTool,\n aiExperimentVariantsTool,\n parseAiExperimentExplanation,\n parseAiExperimentVariants,\n} from '../tools/ai-experiment-tool'\n\n/**\n * The `experiment` step (AGL-2914): A/B tests by AI.\n *\n * A/B testing already exists — the marketing plugin models the experiment,\n * buckets visitors, counts exposures and conversions and compares the arms.\n * This step is the AI layer over it, and it does two things:\n *\n * 1. **Variants.** From the copy under test, it proposes between two and four\n * variants of it. It proposes COPY and nothing else: no weight, no traffic\n * split, no goal, no schedule and no winner. A person puts them into the\n * A/B testing card, which is the only thing that creates or starts a test.\n * 2. **The result.** Code reads the test's figures through the reader\n * marketing registers for them, decides what they support, and only then\n * asks the model for the words. The verdict is never the model's, and an\n * answer that claims more than the verdict is cut back to it.\n *\n * ## Nothing here writes a test\n *\n * Both answers are proposals, like a theme's or a listing's. The experiment\n * document, its variants' screen versions and its traffic are marketing's and\n * the besigner's, and a job that could write them could start an experiment on\n * live visitors without anyone deciding to.\n *\n * ## What reaches the model\n *\n * For variants: the brief, what the test varies, and the copy under test as\n * the surface handed it over. For a result: the test's name, its arms' counts\n * and rates as the results reader published them, and the verdict code\n * reached. No visitor, no session and no assignment is read, because the\n * reader publishes none — it answers in aggregates, which is the whole reason\n * a figure reader is what this reads.\n */\n\n/** The reader whose figures a result is explained from, and the plugin that owns it. */\nexport const AI_EXPERIMENT_READER_ID = AI_EXPERIMENT_FIGURE_READER\n\n/** The window a result is read over: a test's figures are totals since it started. */\nexport const AI_EXPERIMENT_READ_DAYS = 0\n\nexport const AI_EXPERIMENT_NO_TASK_COPY =\n 'Start this from the A/B testing card, which says whether you want variants or an explanation.'\nexport const AI_EXPERIMENT_NO_TARGET_COPY =\n 'Say whether the test varies a screen, a section or an email.'\nexport const AI_EXPERIMENT_NO_SITE_COPY = 'Open the site whose test this is.'\nexport const AI_EXPERIMENT_NO_READER_COPY =\n 'A/B testing is not available on this workspace, so there is no test to work on.'\nexport const AI_EXPERIMENT_NO_SUBJECT_COPY =\n 'Give the copy the test varies, so there is something to write variants of.'\nexport const AI_EXPERIMENT_NO_TEST_COPY =\n 'Name the test to explain, as A/B testing lists it.'\nexport const AI_EXPERIMENT_NO_FIGURES_COPY =\n 'That test’s figures could not be read. Try again in a moment.'\nexport const AI_EXPERIMENT_UNREADABLE_RESULT_COPY =\n 'That test has no first variant and something to compare against it yet, so there is nothing to explain.'\nexport const AI_EXPERIMENT_NO_VARIANTS_COPY =\n 'The AI did not write variants that could be used. Try the brief in different words.'\nexport const AI_EXPERIMENT_NO_EXPLANATION_COPY =\n 'The AI did not explain the result in words that match its figures. Try again.'\n\n/** The step's answer ceiling, from the routing table. */\nexport const AI_JOB_EXPERIMENT_MAX_TOKENS = AI_ROUTING_TABLE['job.experiment'].maxTokens\n\n/**\n * The step's time: at most one re-ask at the routing ceiling, plus the one\n * figure read an explanation makes. Variants read nothing.\n */\nexport const AI_JOB_EXPERIMENT_STEP_BUDGET = aiJobStepBudget({\n tier: AI_STEP_TIERS['job.experiment'],\n maxTokens: AI_JOB_EXPERIMENT_MAX_TOKENS,\n attempts: AI_GENERATION_MAX_ATTEMPTS,\n lookups: 1,\n})\n\nexport const AI_JOB_EXPERIMENT_STEP_MINIMUM_MS = AI_JOB_EXPERIMENT_STEP_BUDGET.minimumMs\n\n/** The rules both tasks are sent: static, byte-identical for every workspace. */\nexport const AI_EXPERIMENT_RULES = [\n 'You work on A/B tests for a website builder. A test shows visitors one of several variants of the same thing and counts which converts best. You never start, stop or change a test: you write variants for a person to put into the A/B testing card, and you put a result someone else measured into plain words.',\n '',\n `When you are given copy under test, call ${AI_EXPERIMENT_VARIANTS_TOOL_NAME} once:`,\n `- Answer with between ${AI_EXPERIMENT_MIN_VARIANTS} and ${AI_EXPERIMENT_MAX_VARIANTS} variants. The first is the copy as it stands, unchanged, so the test has something to measure against.`,\n '- Each later variant changes ONE idea — the promise, the specificity, the length, the call to action — so a result says which idea won.',\n '- No two variants say the same thing in different words.',\n '- Write plain text: no HTML, no markdown, no emoji, no fences.',\n '- Never invent a fact, a price, a name, a statistic or a guarantee the copy under test did not give you.',\n '- Write in the language of the copy under test.',\n '',\n `When you are given a result, call ${AI_EXPERIMENT_EXPLAIN_TOOL_NAME} once:`,\n '- The request states the verdict. It was decided from the counts before you were asked, and it is not yours to revise.',\n '- Where the verdict names no winner, write nothing that says or implies one: no variant is ahead, none is better, nothing is significant, and you recommend nothing. Say what the figures show and why they do not settle it.',\n '- Where the verdict names a winner, name that variant and no other.',\n '- Every number you write appears in the figures you were given, as they show it or rounded. Never compute one.',\n '- Plain sentences: no markdown, no headings, no lists.',\n '- Write in the language of the brief.',\n].join('\\n')\n\nexport const AI_JOB_EXPERIMENT_SYSTEM: AiSystemBlock[] = [\n { text: `${AI_EXPERIMENT_RULES}\\n\\n${AI_ACCEPTABLE_USE_BLOCK}`, cacheBreakpoint: true },\n]\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/** What the test varies, in the words the prompt names it by. */\nconst TARGET_NOUN: Record<AiExperimentTarget, string> = {\n screen: 'a page',\n section: 'one section of a page',\n email: 'an email',\n}\n\n/** The user turn for variants: the brief, what varies, and the copy as it stands. */\nexport function aiExperimentVariantsPrompt(input: {\n brief: string\n target: AiExperimentTarget\n subject: string\n goal: string\n}): string {\n return [\n `Brief: ${input.brief}`,\n `The test varies ${TARGET_NOUN[input.target]}.`,\n ...(input.goal ? [`It is trying to move: ${input.goal}`] : []),\n '',\n 'The copy under test, as it stands:',\n input.subject,\n ].join('\\n')\n}\n\n/** One arm as the explanation prompt shows it. */\nfunction armText(arm: AiExperimentArm, index: number): string {\n const rate = arm.rate === null ? 'no rate' : `${arm.rate}%`\n const against =\n index === 0\n ? 'the first variant, which the others are compared against'\n : [\n arm.lift === null ? 'lift not measurable' : `lift ${arm.lift}%`,\n arm.confidence === null\n ? 'confidence not measurable'\n : `confidence ${arm.confidence}%`,\n ].join(', ')\n return `- ${arm.id}: shown ${arm.exposures}, converted ${arm.conversions}, rate ${rate} (${against})`\n}\n\n/** What the verdict tells the model it may say. */\nexport function aiExperimentVerdictText(reading: AiExperimentReading): string {\n if (reading.verdict === 'winner') {\n return `VERDICT: ${reading.winnerId} is ahead of the first variant by more than chance explains, at the ${reading.threshold}% confidence this test is called at. Name ${reading.winnerId} and no other variant.`\n }\n if (reading.verdict === 'control') {\n return `VERDICT: every other variant is behind the first one, ${reading.winnerId}, by more than chance explains. Name ${reading.winnerId} and no other variant.`\n }\n return [\n `VERDICT: THERE IS NO WINNER — ${reading.reasons.join('; ')}.`,\n 'Say what the figures show and why they do not settle it. Name no winner, say no variant is ahead, better, winning or significant, and recommend no variant.',\n `Leave winner and next empty; what to do next is already decided: ${aiExperimentNextStep(reading.verdict)}`,\n ].join('\\n')\n}\n\n/** The user turn for an explanation: the test, its arms, and the verdict. */\nexport function aiExperimentExplainPrompt(input: {\n brief: string\n reading: AiExperimentReading\n}): string {\n return [\n ...(input.brief ? [`Brief: ${input.brief}`] : []),\n `Test: ${input.reading.test}`,\n 'Figures, totals since the test started:',\n ...input.reading.arms.map(armText),\n '',\n aiExperimentVerdictText(input.reading),\n ].join('\\n')\n}\n\n/** The proposal an answer becomes, as the job's output carries it. */\nfunction experimentOutput(\n job: Pick<AiJob, '$id' | 'hostId' | 'inputs'>,\n label: string,\n proposal: Record<string, unknown>,\n): AiJobOutput {\n return {\n resource: 'experiment',\n // The test the surface named, so a person opens the one they asked about;\n // a variants job for a test that does not exist yet names the job itself.\n id: str(job.inputs?.['experimentId']) || job.$id,\n versionId: null,\n hostId: job.hostId ?? null,\n hostSubdomain: str(job.inputs?.['hostSubdomain']) || null,\n label,\n proposal,\n }\n}\n\n/**\n * The A/B test results for this job's site, or `null` when the workspace has\n * no such reader. The insight path's own resolver is what decides whether a\n * reader may be read here — the plan's entitlement, the owning plugin's\n * release flag, and whether that plugin runs on this workspace and this site\n * — so this asks it rather than restating any of it.\n */\nasync function readExperimentFigures(\n context: Pick<AiJobStepContext, 'firestore' | 'org' | 'now'>,\n place: { orgId: string; hostId: string | null; uid: string | null },\n): Promise<{ table: Parameters<typeof readAiExperimentArms>[0]; refusals: string[] } | null> {\n if (!place.hostId) return null\n const snapshot = await context.firestore.collection('hosts').doc(place.hostId).get()\n const host = snapshot.exists ? ((snapshot.data() ?? {}) as Record<string, unknown>) : null\n if (!host || host['orgId'] !== place.orgId) return null\n const readers = await aiInsightReaders({\n orgId: place.orgId,\n hostId: place.hostId,\n org: context.org ?? null,\n host,\n surface: 'analytics',\n })\n const experiments = readers.filter(({ reader }) => reader.id === AI_EXPERIMENT_READER_ID)\n if (!experiments.length) return null\n const read = await readAiInsightTables(\n experiments,\n [{ reader: AI_EXPERIMENT_READER_ID, days: AI_EXPERIMENT_READ_DAYS, params: {} }],\n { orgId: place.orgId, hostId: place.hostId, uid: place.uid, now: context.now },\n 1,\n )\n return { table: read.tables[0] ?? null, refusals: read.refusals }\n}\n\nexport const runAiJobExperimentStep: AiJobStepRunner = async (\n context: AiJobStepContext,\n) => {\n const { job, signal } = context\n const model = context.modelFor?.('job.experiment') ?? aiModelForStep('job.experiment')\n const maxTokens = AI_JOB_EXPERIMENT_STEP_BUDGET.maxTokens(model)\n const task = aiExperimentTask(job.inputs)\n const spent = (result: { usage: unknown; estCostUsd: number; stopReason: string | null }) => ({\n usage: result.usage as AiJobStepOutcome['usage'],\n estCostUsd: result.estCostUsd,\n model,\n stopReason: result.stopReason,\n effort: null,\n })\n const stop = (failure: string): AiJobStepOutcome => ({\n outputs: [],\n usage: { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 },\n estCostUsd: 0,\n model,\n stopReason: null,\n failure,\n })\n\n if (!task) return stop(AI_EXPERIMENT_NO_TASK_COPY)\n if (!job.hostId) return stop(AI_EXPERIMENT_NO_SITE_COPY)\n\n const ask = (messages: AiMessage[], tool: ReturnType<typeof aiExperimentExplainTool>) =>\n runAiRequest({\n model,\n system: AI_JOB_EXPERIMENT_SYSTEM,\n messages,\n tools: [tool],\n maxTokens,\n ...(AI_ROUTING_TABLE['job.experiment'].thinking\n ? { thinking: AI_ROUTING_TABLE['job.experiment'].thinking }\n : {}),\n stream: false,\n ...(signal ? { signal } : {}),\n })\n\n if (task === 'variants') {\n const target = aiExperimentTarget(job.inputs)\n if (!target) return stop(AI_EXPERIMENT_NO_TARGET_COPY)\n const subject = str(job.inputs?.['subject']).slice(0, AI_EXPERIMENT_SUBJECT_MAX_CHARS)\n if (!subject) return stop(AI_EXPERIMENT_NO_SUBJECT_COPY)\n const tool = aiExperimentVariantsTool(target)\n let messages: AiMessage[] = [\n {\n role: 'user',\n content: aiExperimentVariantsPrompt({\n brief: job.brief,\n target,\n subject,\n goal: str(job.inputs?.['goal']),\n }),\n },\n ]\n let last: AiJobStepOutcome | null = null\n for (let attempt = 0; attempt < AI_GENERATION_MAX_ATTEMPTS; attempt += 1) {\n const result = await ask(messages, tool)\n if (result.kind === 'refusal') {\n return { outputs: [], ...spent(result), refused: true }\n }\n const parsed = parseAiExperimentVariants(\n result.toolUse.find((use) => use.name === AI_EXPERIMENT_VARIANTS_TOOL_NAME)?.input,\n )\n const checked = checkAiExperimentVariants(parsed, target)\n last = { outputs: [], ...spent(result), failure: AI_EXPERIMENT_NO_VARIANTS_COPY }\n if (checked) {\n if (checked.findings.length) {\n console.info('ai experiment variants dropped', {\n jobId: job.$id,\n findings: checked.findings,\n })\n }\n return {\n outputs: [variantsOutput(job, target, checked.proposal)],\n ...spent(result),\n }\n }\n messages = [\n ...messages,\n { role: 'assistant', content: `Answered with ${AI_EXPERIMENT_VARIANTS_TOOL_NAME}.` },\n {\n role: 'user',\n content:\n `Those variants could not be used. Answer again with ${AI_EXPERIMENT_VARIANTS_TOOL_NAME}: ` +\n `at least ${AI_EXPERIMENT_MIN_VARIANTS} variants, each with copy of its own, none repeating ` +\n 'another, and plain text throughout.',\n },\n ]\n }\n return last ?? stop(AI_EXPERIMENT_NO_VARIANTS_COPY)\n }\n\n const test = str(job.inputs?.['test'])\n if (!test) return stop(AI_EXPERIMENT_NO_TEST_COPY)\n const figures = await readExperimentFigures(context, {\n orgId: job.orgId,\n hostId: job.hostId,\n uid: job.createdBy,\n })\n if (!figures) return stop(AI_EXPERIMENT_NO_READER_COPY)\n if (!figures.table) {\n if (figures.refusals.length) {\n console.warn('ai experiment figures refused', { jobId: job.$id, refusals: figures.refusals })\n }\n return stop(AI_EXPERIMENT_NO_FIGURES_COPY)\n }\n const reading = readAiExperimentResult(test, readAiExperimentArms(figures.table, test))\n // Nothing to explain, and nothing spent finding that out: the verdict was\n // reached from the figures alone.\n if (reading.verdict === 'unreadable') return stop(AI_EXPERIMENT_UNREADABLE_RESULT_COPY)\n\n const result = await ask(\n [{ role: 'user', content: aiExperimentExplainPrompt({ brief: job.brief, reading }) }],\n aiExperimentExplainTool(),\n )\n if (result.kind === 'refusal') return { outputs: [], ...spent(result), refused: true }\n const explained = checkAiExperimentExplanation(\n parseAiExperimentExplanation(\n result.toolUse.find((use) => use.name === AI_EXPERIMENT_EXPLAIN_TOOL_NAME)?.input,\n ),\n reading,\n )\n if (!explained) {\n return { outputs: [], ...spent(result), failure: AI_EXPERIMENT_NO_EXPLANATION_COPY }\n }\n if (explained.findings.length) {\n console.info('ai experiment explanation cut back to its reading', {\n jobId: job.$id,\n verdict: reading.verdict,\n findings: explained.findings,\n })\n }\n return { outputs: [explanationOutput(job, reading, explained)], ...spent(result) }\n}\n\n/** A variants answer as the job's output. */\nfunction variantsOutput(\n job: Pick<AiJob, '$id' | 'hostId' | 'inputs'>,\n target: AiExperimentTarget,\n proposal: AiExperimentVariantsProposal,\n): AiJobOutput {\n return {\n ...experimentOutput(job, `${proposal.variants.length} variants to test`, {\n task: 'variants',\n target,\n goal: proposal.goal,\n variants: proposal.variants,\n }),\n note: 'Put these into A/B testing to start the test; nothing has been changed on the site.',\n }\n}\n\n/** An explanation as the job's output, carrying the verdict code reached. */\nfunction explanationOutput(\n job: Pick<AiJob, '$id' | 'hostId' | 'inputs'>,\n reading: AiExperimentReading,\n explained: AiExperimentExplanation,\n): AiJobOutput {\n return experimentOutput(job, explained.test || 'A/B test result', {\n task: 'explain',\n verdict: explained.verdict,\n headline: explained.headline,\n points: explained.points,\n // Only ever the reading's arm: an explanation carries no winner the\n // figures did not support, whatever the answer said.\n winnerId: aiExperimentVerdictNamesWinner(reading.verdict) ? explained.winnerId : null,\n next: explained.next,\n threshold: reading.threshold,\n arms: reading.arms,\n })\n}\n\n/**\n * Whether an experiment job may run here: a site of the job's own org, a task\n * the step runs, and A/B testing available on it — which is the results\n * reader being one this workspace may read, asked of the owning plugin rather\n * than restated.\n */\nexport async function aiExperimentAdmissionRefusal(\n context: AiJobAdmissionContext,\n): Promise<AiJobAdmissionRefusal | null> {\n if (!aiExperimentTask(context.inputs)) {\n return { status: 400, error: AI_EXPERIMENT_NO_TASK_COPY }\n }\n if (!context.hostId) return { status: 400, error: AI_EXPERIMENT_NO_SITE_COPY }\n const snapshot = await context.firestore.collection('hosts').doc(context.hostId).get()\n const host = snapshot.exists ? ((snapshot.data() ?? {}) as Record<string, unknown>) : null\n if (!host || host['orgId'] !== context.orgId) {\n return { status: 404, error: 'Unknown site' }\n }\n const readers = await aiInsightReaders({\n orgId: context.orgId,\n hostId: context.hostId,\n org: context.org,\n host,\n surface: 'analytics',\n })\n return readers.some(({ reader }) => reader.id === AI_EXPERIMENT_READER_ID)\n ? null\n : { status: 403, error: AI_EXPERIMENT_NO_READER_COPY }\n}\n\n/**\n * The experiment kind's registrations, called by the console surface\n * (`registerAiJobKinds` in `server.ts`), never made by importing this module\n * (AGL-3025).\n */\nexport function registerAiExperimentJob(): void {\n registerAiJobStep('experiment', runAiJobExperimentStep, {\n minimumMs: AI_JOB_EXPERIMENT_STEP_MINIMUM_MS,\n })\n registerAiJobAdmission('experiment', aiExperimentAdmissionRefusal)\n}\n"],"names":["AI_EXPERIMENT_FIGURE_READER","AI_EXPERIMENT_MAX_VARIANTS","AI_EXPERIMENT_MIN_VARIANTS","AI_EXPERIMENT_SUBJECT_MAX_CHARS","aiExperimentNextStep","aiExperimentTarget","aiExperimentTask","aiExperimentVerdictNamesWinner","checkAiExperimentExplanation","checkAiExperimentVariants","readAiExperimentArms","readAiExperimentResult","AI_STEP_TIERS","AI_ROUTING_TABLE","aiModelForStep","AI_ACCEPTABLE_USE_BLOCK","runAiRequest","AI_GENERATION_MAX_ATTEMPTS","aiInsightReaders","readAiInsightTables","registerAiJobAdmission","aiJobStepBudget","registerAiJobStep","AI_EXPERIMENT_EXPLAIN_TOOL_NAME","AI_EXPERIMENT_VARIANTS_TOOL_NAME","aiExperimentExplainTool","aiExperimentVariantsTool","parseAiExperimentExplanation","parseAiExperimentVariants","AI_EXPERIMENT_READER_ID","AI_EXPERIMENT_READ_DAYS","AI_EXPERIMENT_NO_TASK_COPY","AI_EXPERIMENT_NO_TARGET_COPY","AI_EXPERIMENT_NO_SITE_COPY","AI_EXPERIMENT_NO_READER_COPY","AI_EXPERIMENT_NO_SUBJECT_COPY","AI_EXPERIMENT_NO_TEST_COPY","AI_EXPERIMENT_NO_FIGURES_COPY","AI_EXPERIMENT_UNREADABLE_RESULT_COPY","AI_EXPERIMENT_NO_VARIANTS_COPY","AI_EXPERIMENT_NO_EXPLANATION_COPY","AI_JOB_EXPERIMENT_MAX_TOKENS","maxTokens","AI_JOB_EXPERIMENT_STEP_BUDGET","tier","attempts","lookups","AI_JOB_EXPERIMENT_STEP_MINIMUM_MS","minimumMs","AI_EXPERIMENT_RULES","join","AI_JOB_EXPERIMENT_SYSTEM","text","cacheBreakpoint","str","value","trim","TARGET_NOUN","screen","section","email","aiExperimentVariantsPrompt","input","brief","target","goal","subject","armText","arm","index","rate","against","lift","confidence","id","exposures","conversions","aiExperimentVerdictText","reading","verdict","winnerId","threshold","reasons","aiExperimentExplainPrompt","test","arms","map","experimentOutput","job","label","proposal","resource","inputs","$id","versionId","hostId","hostSubdomain","readExperimentFigures","context","place","snapshot","read","firestore","collection","doc","get","host","exists","data","orgId","readers","org","surface","experiments","filter","reader","length","days","params","uid","now","table","tables","refusals","runAiJobExperimentStep","result","signal","model","modelFor","task","spent","usage","estCostUsd","stopReason","effort","stop","failure","outputs","inputTokens","outputTokens","cacheReadTokens","cacheWriteTokens","ask","messages","tool","system","tools","thinking","stream","slice","role","content","last","attempt","kind","refused","parsed","toolUse","find","use","name","checked","findings","console","info","jobId","variantsOutput","figures","createdBy","warn","explained","explanationOutput","variants","note","headline","points","next","aiExperimentAdmissionRefusal","status","error","some","registerAiExperimentJob"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-experiment-step.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AI_EXPERIMENT_FIGURE_READER,\n AI_EXPERIMENT_MAX_VARIANTS,\n AI_EXPERIMENT_MIN_VARIANTS,\n AI_EXPERIMENT_SUBJECT_MAX_CHARS,\n aiExperimentNextStep,\n aiExperimentTarget,\n aiExperimentTask,\n aiExperimentVerdictNamesWinner,\n checkAiExperimentExplanation,\n checkAiExperimentVariants,\n readAiExperimentArms,\n readAiExperimentResult,\n type AiExperimentArm,\n type AiExperimentExplanation,\n type AiExperimentReading,\n type AiExperimentTarget,\n type AiExperimentVariantsProposal,\n} from '../model/ai-experiment'\nimport type { AiJob, AiJobOutput } from '../model/ai-jobs.types'\nimport { AI_STEP_TIERS } from '../providers/catalog'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport {\n AI_ACCEPTABLE_USE_BLOCK,\n runAiRequest,\n type AiMessage,\n type AiSystemBlock,\n} from '../runtime/ai-runtime'\nimport { AI_GENERATION_MAX_ATTEMPTS } from '../runtime/ai-generation-bounds'\nimport { aiInsightReaders, readAiInsightTables } from '../insights/ai-insight-readers'\nimport {\n registerAiJobAdmission,\n type AiJobAdmissionContext,\n type AiJobAdmissionRefusal,\n} from './ai-job-admission'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport type { AiJobStepContext, AiJobStepOutcome, AiJobStepRunner } from './ai-job-text-step'\nimport { registerAiJobStep } from './ai-jobs'\nimport {\n AI_EXPERIMENT_EXPLAIN_TOOL_NAME,\n AI_EXPERIMENT_VARIANTS_TOOL_NAME,\n aiExperimentExplainTool,\n aiExperimentVariantsTool,\n parseAiExperimentExplanation,\n parseAiExperimentVariants,\n} from '../tools/ai-experiment-tool'\n\n/**\n * The `experiment` step (AGL-2914): A/B tests by AI.\n *\n * A/B testing already exists — the marketing plugin models the experiment,\n * buckets visitors, counts exposures and conversions and compares the arms.\n * This step is the AI layer over it, and it does two things:\n *\n * 1. **Variants.** From the copy under test, it proposes between two and four\n * variants of it. It proposes COPY and nothing else: no weight, no traffic\n * split, no goal, no schedule and no winner. A person puts them into the\n * A/B testing card, which is the only thing that creates or starts a test.\n * 2. **The result.** Code reads the test's figures through the reader\n * marketing registers for them, decides what they support, and only then\n * asks the model for the words. The verdict is never the model's, and an\n * answer that claims more than the verdict is cut back to it.\n *\n * ## Nothing here writes a test\n *\n * Both answers are proposals, like a theme's or a listing's. The experiment\n * document, its variants' screen versions and its traffic are marketing's and\n * the besigner's, and a job that could write them could start an experiment on\n * live visitors without anyone deciding to.\n *\n * ## What reaches the model\n *\n * For variants: the brief, what the test varies, and the copy under test as\n * the surface handed it over. For a result: the test's name, its arms' counts\n * and rates as the results reader published them, and the verdict code\n * reached. No visitor, no session and no assignment is read, because the\n * reader publishes none — it answers in aggregates, which is the whole reason\n * a figure reader is what this reads.\n */\n\n/** The reader whose figures a result is explained from, and the plugin that owns it. */\nexport const AI_EXPERIMENT_READER_ID = AI_EXPERIMENT_FIGURE_READER\n\n/** The window a result is read over: a test's figures are totals since it started. */\nexport const AI_EXPERIMENT_READ_DAYS = 0\n\nexport const AI_EXPERIMENT_NO_TASK_COPY =\n 'Start this from the A/B testing card, which says whether you want variants or an explanation.'\nexport const AI_EXPERIMENT_NO_TARGET_COPY =\n 'Say whether the test varies a page, a section or an email.'\nexport const AI_EXPERIMENT_NO_SITE_COPY = 'Open the site whose test this is.'\nexport const AI_EXPERIMENT_NO_READER_COPY =\n 'A/B testing is not available on this workspace, so there is no test to work on.'\nexport const AI_EXPERIMENT_NO_SUBJECT_COPY =\n 'Give the copy the test varies, so there is something to write variants of.'\nexport const AI_EXPERIMENT_NO_TEST_COPY =\n 'Name the test to explain, as A/B testing lists it.'\nexport const AI_EXPERIMENT_NO_FIGURES_COPY =\n 'That test’s figures could not be read. Try again in a moment.'\nexport const AI_EXPERIMENT_UNREADABLE_RESULT_COPY =\n 'That test has no first variant and something to compare against it yet, so there is nothing to explain.'\nexport const AI_EXPERIMENT_NO_VARIANTS_COPY =\n 'The AI did not write variants that could be used. Try the brief in different words.'\nexport const AI_EXPERIMENT_NO_EXPLANATION_COPY =\n 'The AI did not explain the result in words that match its figures. Try again.'\n\n/** The step's answer ceiling, from the routing table. */\nexport const AI_JOB_EXPERIMENT_MAX_TOKENS = AI_ROUTING_TABLE['job.experiment'].maxTokens\n\n/**\n * The step's time: at most one re-ask at the routing ceiling, plus the one\n * figure read an explanation makes. Variants read nothing.\n */\nexport const AI_JOB_EXPERIMENT_STEP_BUDGET = aiJobStepBudget({\n tier: AI_STEP_TIERS['job.experiment'],\n maxTokens: AI_JOB_EXPERIMENT_MAX_TOKENS,\n attempts: AI_GENERATION_MAX_ATTEMPTS,\n lookups: 1,\n})\n\nexport const AI_JOB_EXPERIMENT_STEP_MINIMUM_MS = AI_JOB_EXPERIMENT_STEP_BUDGET.minimumMs\n\n/** The rules both tasks are sent: static, byte-identical for every workspace. */\nexport const AI_EXPERIMENT_RULES = [\n 'You work on A/B tests for a website builder. A test shows visitors one of several variants of the same thing and counts which converts best. You never start, stop or change a test: you write variants for a person to put into the A/B testing card, and you put a result someone else measured into plain words.',\n '',\n `When you are given copy under test, call ${AI_EXPERIMENT_VARIANTS_TOOL_NAME} once:`,\n `- Answer with between ${AI_EXPERIMENT_MIN_VARIANTS} and ${AI_EXPERIMENT_MAX_VARIANTS} variants. The first is the copy as it stands, unchanged, so the test has something to measure against.`,\n '- Each later variant changes ONE idea — the promise, the specificity, the length, the call to action — so a result says which idea won.',\n '- No two variants say the same thing in different words.',\n '- Write plain text: no HTML, no markdown, no emoji, no fences.',\n '- Never invent a fact, a price, a name, a statistic or a guarantee the copy under test did not give you.',\n '- Write in the language of the copy under test.',\n '',\n `When you are given a result, call ${AI_EXPERIMENT_EXPLAIN_TOOL_NAME} once:`,\n '- The request states the verdict. It was decided from the counts before you were asked, and it is not yours to revise.',\n '- Where the verdict names no winner, write nothing that says or implies one: no variant is ahead, none is better, nothing is significant, and you recommend nothing. Say what the figures show and why they do not settle it.',\n '- Where the verdict names a winner, name that variant and no other.',\n '- Every number you write appears in the figures you were given, as they show it or rounded. Never compute one.',\n '- Plain sentences: no markdown, no headings, no lists.',\n '- Write in the language of the brief.',\n].join('\\n')\n\nexport const AI_JOB_EXPERIMENT_SYSTEM: AiSystemBlock[] = [\n { text: `${AI_EXPERIMENT_RULES}\\n\\n${AI_ACCEPTABLE_USE_BLOCK}`, cacheBreakpoint: true },\n]\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/** What the test varies, in the words the prompt names it by. */\nconst TARGET_NOUN: Record<AiExperimentTarget, string> = {\n screen: 'a page',\n section: 'one section of a page',\n email: 'an email',\n}\n\n/** The user turn for variants: the brief, what varies, and the copy as it stands. */\nexport function aiExperimentVariantsPrompt(input: {\n brief: string\n target: AiExperimentTarget\n subject: string\n goal: string\n}): string {\n return [\n `Brief: ${input.brief}`,\n `The test varies ${TARGET_NOUN[input.target]}.`,\n ...(input.goal ? [`It is trying to move: ${input.goal}`] : []),\n '',\n 'The copy under test, as it stands:',\n input.subject,\n ].join('\\n')\n}\n\n/** One arm as the explanation prompt shows it. */\nfunction armText(arm: AiExperimentArm, index: number): string {\n const rate = arm.rate === null ? 'no rate' : `${arm.rate}%`\n const against =\n index === 0\n ? 'the first variant, which the others are compared against'\n : [\n arm.lift === null ? 'lift not measurable' : `lift ${arm.lift}%`,\n arm.confidence === null\n ? 'confidence not measurable'\n : `confidence ${arm.confidence}%`,\n ].join(', ')\n return `- ${arm.id}: shown ${arm.exposures}, converted ${arm.conversions}, rate ${rate} (${against})`\n}\n\n/** What the verdict tells the model it may say. */\nexport function aiExperimentVerdictText(reading: AiExperimentReading): string {\n if (reading.verdict === 'winner') {\n return `VERDICT: ${reading.winnerId} is ahead of the first variant by more than chance explains, at the ${reading.threshold}% confidence this test is called at. Name ${reading.winnerId} and no other variant.`\n }\n if (reading.verdict === 'control') {\n return `VERDICT: every other variant is behind the first one, ${reading.winnerId}, by more than chance explains. Name ${reading.winnerId} and no other variant.`\n }\n return [\n `VERDICT: THERE IS NO WINNER — ${reading.reasons.join('; ')}.`,\n 'Say what the figures show and why they do not settle it. Name no winner, say no variant is ahead, better, winning or significant, and recommend no variant.',\n `Leave winner and next empty; what to do next is already decided: ${aiExperimentNextStep(reading.verdict)}`,\n ].join('\\n')\n}\n\n/** The user turn for an explanation: the test, its arms, and the verdict. */\nexport function aiExperimentExplainPrompt(input: {\n brief: string\n reading: AiExperimentReading\n}): string {\n return [\n ...(input.brief ? [`Brief: ${input.brief}`] : []),\n `Test: ${input.reading.test}`,\n 'Figures, totals since the test started:',\n ...input.reading.arms.map(armText),\n '',\n aiExperimentVerdictText(input.reading),\n ].join('\\n')\n}\n\n/** The proposal an answer becomes, as the job's output carries it. */\nfunction experimentOutput(\n job: Pick<AiJob, '$id' | 'hostId' | 'inputs'>,\n label: string,\n proposal: Record<string, unknown>,\n): AiJobOutput {\n return {\n resource: 'experiment',\n // The test the surface named, so a person opens the one they asked about;\n // a variants job for a test that does not exist yet names the job itself.\n id: str(job.inputs?.['experimentId']) || job.$id,\n versionId: null,\n hostId: job.hostId ?? null,\n hostSubdomain: str(job.inputs?.['hostSubdomain']) || null,\n label,\n proposal,\n }\n}\n\n/**\n * The A/B test results for this job's site, or `null` when the workspace has\n * no such reader. The insight path's own resolver is what decides whether a\n * reader may be read here — the plan's entitlement, the owning plugin's\n * release flag, and whether that plugin runs on this workspace and this site\n * — so this asks it rather than restating any of it.\n */\nasync function readExperimentFigures(\n context: Pick<AiJobStepContext, 'firestore' | 'org' | 'now'>,\n place: { orgId: string; hostId: string | null; uid: string | null },\n): Promise<{ table: Parameters<typeof readAiExperimentArms>[0]; refusals: string[] } | null> {\n if (!place.hostId) return null\n const snapshot = await context.firestore.collection('hosts').doc(place.hostId).get()\n const host = snapshot.exists ? ((snapshot.data() ?? {}) as Record<string, unknown>) : null\n if (!host || host['orgId'] !== place.orgId) return null\n const readers = await aiInsightReaders({\n orgId: place.orgId,\n hostId: place.hostId,\n org: context.org ?? null,\n host,\n surface: 'analytics',\n })\n const experiments = readers.filter(({ reader }) => reader.id === AI_EXPERIMENT_READER_ID)\n if (!experiments.length) return null\n const read = await readAiInsightTables(\n experiments,\n [{ reader: AI_EXPERIMENT_READER_ID, days: AI_EXPERIMENT_READ_DAYS, params: {} }],\n { orgId: place.orgId, hostId: place.hostId, uid: place.uid, now: context.now },\n 1,\n )\n return { table: read.tables[0] ?? null, refusals: read.refusals }\n}\n\nexport const runAiJobExperimentStep: AiJobStepRunner = async (\n context: AiJobStepContext,\n) => {\n const { job, signal } = context\n const model = context.modelFor?.('job.experiment') ?? aiModelForStep('job.experiment')\n const maxTokens = AI_JOB_EXPERIMENT_STEP_BUDGET.maxTokens(model)\n const task = aiExperimentTask(job.inputs)\n const spent = (result: { usage: unknown; estCostUsd: number; stopReason: string | null }) => ({\n usage: result.usage as AiJobStepOutcome['usage'],\n estCostUsd: result.estCostUsd,\n model,\n stopReason: result.stopReason,\n effort: null,\n })\n const stop = (failure: string): AiJobStepOutcome => ({\n outputs: [],\n usage: { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 },\n estCostUsd: 0,\n model,\n stopReason: null,\n failure,\n })\n\n if (!task) return stop(AI_EXPERIMENT_NO_TASK_COPY)\n if (!job.hostId) return stop(AI_EXPERIMENT_NO_SITE_COPY)\n\n const ask = (messages: AiMessage[], tool: ReturnType<typeof aiExperimentExplainTool>) =>\n runAiRequest({\n model,\n system: AI_JOB_EXPERIMENT_SYSTEM,\n messages,\n tools: [tool],\n maxTokens,\n ...(AI_ROUTING_TABLE['job.experiment'].thinking\n ? { thinking: AI_ROUTING_TABLE['job.experiment'].thinking }\n : {}),\n stream: false,\n ...(signal ? { signal } : {}),\n })\n\n if (task === 'variants') {\n const target = aiExperimentTarget(job.inputs)\n if (!target) return stop(AI_EXPERIMENT_NO_TARGET_COPY)\n const subject = str(job.inputs?.['subject']).slice(0, AI_EXPERIMENT_SUBJECT_MAX_CHARS)\n if (!subject) return stop(AI_EXPERIMENT_NO_SUBJECT_COPY)\n const tool = aiExperimentVariantsTool(target)\n let messages: AiMessage[] = [\n {\n role: 'user',\n content: aiExperimentVariantsPrompt({\n brief: job.brief,\n target,\n subject,\n goal: str(job.inputs?.['goal']),\n }),\n },\n ]\n let last: AiJobStepOutcome | null = null\n for (let attempt = 0; attempt < AI_GENERATION_MAX_ATTEMPTS; attempt += 1) {\n const result = await ask(messages, tool)\n if (result.kind === 'refusal') {\n return { outputs: [], ...spent(result), refused: true }\n }\n const parsed = parseAiExperimentVariants(\n result.toolUse.find((use) => use.name === AI_EXPERIMENT_VARIANTS_TOOL_NAME)?.input,\n )\n const checked = checkAiExperimentVariants(parsed, target)\n last = { outputs: [], ...spent(result), failure: AI_EXPERIMENT_NO_VARIANTS_COPY }\n if (checked) {\n if (checked.findings.length) {\n console.info('ai experiment variants dropped', {\n jobId: job.$id,\n findings: checked.findings,\n })\n }\n return {\n outputs: [variantsOutput(job, target, checked.proposal)],\n ...spent(result),\n }\n }\n messages = [\n ...messages,\n { role: 'assistant', content: `Answered with ${AI_EXPERIMENT_VARIANTS_TOOL_NAME}.` },\n {\n role: 'user',\n content:\n `Those variants could not be used. Answer again with ${AI_EXPERIMENT_VARIANTS_TOOL_NAME}: ` +\n `at least ${AI_EXPERIMENT_MIN_VARIANTS} variants, each with copy of its own, none repeating ` +\n 'another, and plain text throughout.',\n },\n ]\n }\n return last ?? stop(AI_EXPERIMENT_NO_VARIANTS_COPY)\n }\n\n const test = str(job.inputs?.['test'])\n if (!test) return stop(AI_EXPERIMENT_NO_TEST_COPY)\n const figures = await readExperimentFigures(context, {\n orgId: job.orgId,\n hostId: job.hostId,\n uid: job.createdBy,\n })\n if (!figures) return stop(AI_EXPERIMENT_NO_READER_COPY)\n if (!figures.table) {\n if (figures.refusals.length) {\n console.warn('ai experiment figures refused', { jobId: job.$id, refusals: figures.refusals })\n }\n return stop(AI_EXPERIMENT_NO_FIGURES_COPY)\n }\n const reading = readAiExperimentResult(test, readAiExperimentArms(figures.table, test))\n // Nothing to explain, and nothing spent finding that out: the verdict was\n // reached from the figures alone.\n if (reading.verdict === 'unreadable') return stop(AI_EXPERIMENT_UNREADABLE_RESULT_COPY)\n\n const result = await ask(\n [{ role: 'user', content: aiExperimentExplainPrompt({ brief: job.brief, reading }) }],\n aiExperimentExplainTool(),\n )\n if (result.kind === 'refusal') return { outputs: [], ...spent(result), refused: true }\n const explained = checkAiExperimentExplanation(\n parseAiExperimentExplanation(\n result.toolUse.find((use) => use.name === AI_EXPERIMENT_EXPLAIN_TOOL_NAME)?.input,\n ),\n reading,\n )\n if (!explained) {\n return { outputs: [], ...spent(result), failure: AI_EXPERIMENT_NO_EXPLANATION_COPY }\n }\n if (explained.findings.length) {\n console.info('ai experiment explanation cut back to its reading', {\n jobId: job.$id,\n verdict: reading.verdict,\n findings: explained.findings,\n })\n }\n return { outputs: [explanationOutput(job, reading, explained)], ...spent(result) }\n}\n\n/** A variants answer as the job's output. */\nfunction variantsOutput(\n job: Pick<AiJob, '$id' | 'hostId' | 'inputs'>,\n target: AiExperimentTarget,\n proposal: AiExperimentVariantsProposal,\n): AiJobOutput {\n return {\n ...experimentOutput(job, `${proposal.variants.length} variants to test`, {\n task: 'variants',\n target,\n goal: proposal.goal,\n variants: proposal.variants,\n }),\n note: 'Put these into A/B testing to start the test; nothing has been changed on the site.',\n }\n}\n\n/** An explanation as the job's output, carrying the verdict code reached. */\nfunction explanationOutput(\n job: Pick<AiJob, '$id' | 'hostId' | 'inputs'>,\n reading: AiExperimentReading,\n explained: AiExperimentExplanation,\n): AiJobOutput {\n return experimentOutput(job, explained.test || 'A/B test result', {\n task: 'explain',\n verdict: explained.verdict,\n headline: explained.headline,\n points: explained.points,\n // Only ever the reading's arm: an explanation carries no winner the\n // figures did not support, whatever the answer said.\n winnerId: aiExperimentVerdictNamesWinner(reading.verdict) ? explained.winnerId : null,\n next: explained.next,\n threshold: reading.threshold,\n arms: reading.arms,\n })\n}\n\n/**\n * Whether an experiment job may run here: a site of the job's own org, a task\n * the step runs, and A/B testing available on it — which is the results\n * reader being one this workspace may read, asked of the owning plugin rather\n * than restated.\n */\nexport async function aiExperimentAdmissionRefusal(\n context: AiJobAdmissionContext,\n): Promise<AiJobAdmissionRefusal | null> {\n if (!aiExperimentTask(context.inputs)) {\n return { status: 400, error: AI_EXPERIMENT_NO_TASK_COPY }\n }\n if (!context.hostId) return { status: 400, error: AI_EXPERIMENT_NO_SITE_COPY }\n const snapshot = await context.firestore.collection('hosts').doc(context.hostId).get()\n const host = snapshot.exists ? ((snapshot.data() ?? {}) as Record<string, unknown>) : null\n if (!host || host['orgId'] !== context.orgId) {\n return { status: 404, error: 'Unknown site' }\n }\n const readers = await aiInsightReaders({\n orgId: context.orgId,\n hostId: context.hostId,\n org: context.org,\n host,\n surface: 'analytics',\n })\n return readers.some(({ reader }) => reader.id === AI_EXPERIMENT_READER_ID)\n ? null\n : { status: 403, error: AI_EXPERIMENT_NO_READER_COPY }\n}\n\n/**\n * The experiment kind's registrations, called by the console surface\n * (`registerAiJobKinds` in `server.ts`), never made by importing this module\n * (AGL-3025).\n */\nexport function registerAiExperimentJob(): void {\n registerAiJobStep('experiment', runAiJobExperimentStep, {\n minimumMs: AI_JOB_EXPERIMENT_STEP_MINIMUM_MS,\n })\n registerAiJobAdmission('experiment', aiExperimentAdmissionRefusal)\n}\n"],"names":["AI_EXPERIMENT_FIGURE_READER","AI_EXPERIMENT_MAX_VARIANTS","AI_EXPERIMENT_MIN_VARIANTS","AI_EXPERIMENT_SUBJECT_MAX_CHARS","aiExperimentNextStep","aiExperimentTarget","aiExperimentTask","aiExperimentVerdictNamesWinner","checkAiExperimentExplanation","checkAiExperimentVariants","readAiExperimentArms","readAiExperimentResult","AI_STEP_TIERS","AI_ROUTING_TABLE","aiModelForStep","AI_ACCEPTABLE_USE_BLOCK","runAiRequest","AI_GENERATION_MAX_ATTEMPTS","aiInsightReaders","readAiInsightTables","registerAiJobAdmission","aiJobStepBudget","registerAiJobStep","AI_EXPERIMENT_EXPLAIN_TOOL_NAME","AI_EXPERIMENT_VARIANTS_TOOL_NAME","aiExperimentExplainTool","aiExperimentVariantsTool","parseAiExperimentExplanation","parseAiExperimentVariants","AI_EXPERIMENT_READER_ID","AI_EXPERIMENT_READ_DAYS","AI_EXPERIMENT_NO_TASK_COPY","AI_EXPERIMENT_NO_TARGET_COPY","AI_EXPERIMENT_NO_SITE_COPY","AI_EXPERIMENT_NO_READER_COPY","AI_EXPERIMENT_NO_SUBJECT_COPY","AI_EXPERIMENT_NO_TEST_COPY","AI_EXPERIMENT_NO_FIGURES_COPY","AI_EXPERIMENT_UNREADABLE_RESULT_COPY","AI_EXPERIMENT_NO_VARIANTS_COPY","AI_EXPERIMENT_NO_EXPLANATION_COPY","AI_JOB_EXPERIMENT_MAX_TOKENS","maxTokens","AI_JOB_EXPERIMENT_STEP_BUDGET","tier","attempts","lookups","AI_JOB_EXPERIMENT_STEP_MINIMUM_MS","minimumMs","AI_EXPERIMENT_RULES","join","AI_JOB_EXPERIMENT_SYSTEM","text","cacheBreakpoint","str","value","trim","TARGET_NOUN","screen","section","email","aiExperimentVariantsPrompt","input","brief","target","goal","subject","armText","arm","index","rate","against","lift","confidence","id","exposures","conversions","aiExperimentVerdictText","reading","verdict","winnerId","threshold","reasons","aiExperimentExplainPrompt","test","arms","map","experimentOutput","job","label","proposal","resource","inputs","$id","versionId","hostId","hostSubdomain","readExperimentFigures","context","place","snapshot","read","firestore","collection","doc","get","host","exists","data","orgId","readers","org","surface","experiments","filter","reader","length","days","params","uid","now","table","tables","refusals","runAiJobExperimentStep","result","signal","model","modelFor","task","spent","usage","estCostUsd","stopReason","effort","stop","failure","outputs","inputTokens","outputTokens","cacheReadTokens","cacheWriteTokens","ask","messages","tool","system","tools","thinking","stream","slice","role","content","last","attempt","kind","refused","parsed","toolUse","find","use","name","checked","findings","console","info","jobId","variantsOutput","figures","createdBy","warn","explained","explanationOutput","variants","note","headline","points","next","aiExperimentAdmissionRefusal","status","error","some","registerAiExperimentJob"],"mappings":"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"}
@@ -14,7 +14,7 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
- import type { AiBuildPlanCreate, AiBuildPlanCreateKind } from '../model/ai-build-plan';
17
+ import { type AiBuildPlanCreate, type AiBuildPlanCreateKind } from '../model/ai-build-plan';
18
18
  import type { AiJob, AiJobOutput, AiJobPlan, AiJobReview, AiJobReviewOutlineNode } from '../model/ai-jobs.types';
19
19
  import type { AiSiteInventory } from '../model/ai-site-inventory';
20
20
  import { type AiGenerationSpend, type AiValidatedTree } from '../runtime/ai-doctrine';
@@ -15,6 +15,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
17
  */ import { REUSABLE_INSTANCE_COMPONENT_ID, REUSABLE_INSTANCE_PROP_VALUES_KEY } from "@aglyn/aglyn/app-utils/reusable-component-keys";
18
+ import { AI_BUILD_PLAN_EMBED_HOST_NAMES, AI_PLAN_NEW_REF_PREFIX, isAiPlanNewRef } from "../model/ai-build-plan.js";
18
19
  import { aiAnswerTree } from "../runtime/ai-doctrine.js";
19
20
  import { aiBracketedFacts, aiTreeCopy, walkTree } from "../runtime/ai-doctrine-validators.js";
20
21
  import { AI_INSTANCE_REF_PROP } from "../runtime/ai-node-tree.js";
@@ -51,12 +52,13 @@ import { AI_JOB_BRIEF_MAX_CHARS } from "./ai-job-text-step.js";
51
52
  return `Brief: ${job.brief.slice(0, AI_JOB_BRIEF_MAX_CHARS)}`;
52
53
  }
53
54
  /** The confirmed plan as reference lines; none when there is no confirmed plan. */ export function aiPlanReferenceLines(plan) {
55
+ var _plan_embeds;
54
56
  if (!plan) return [];
55
57
  const lines = [
56
58
  'Confirmed plan:'
57
59
  ];
58
60
  for (const entry of plan.reuse){
59
- lines.push(`- reuse the ${entry.kind} ${referenceOf(plan, entry.id)}: ${entry.purpose}`);
61
+ lines.push(`- reuse the ${entry.kind === 'screen' ? 'page' : entry.kind} ${referenceOf(plan, entry.id)}: ${entry.purpose}`);
60
62
  }
61
63
  for (const entry of plan.create){
62
64
  const from = entry.duplicateOf ? `, from a copy of ${referenceOf(plan, entry.duplicateOf)}` : '';
@@ -65,7 +67,13 @@ import { AI_JOB_BRIEF_MAX_CHARS } from "./ai-job-text-step.js";
65
67
  }
66
68
  for (const screen of plan.screens){
67
69
  const sections = screen.sections.map((section)=>section.name).join(', ');
68
- lines.push(`- the screen "${screen.title}" at ${screen.slug}${sections ? `: ${sections}` : ''}`);
70
+ lines.push(`- the page "${screen.title}" at ${screen.slug}${sections ? `: ${sections}` : ''}`);
71
+ }
72
+ // The one place a Video embed is admitted (AGL-3433): where the plan lists it, at the brief's link or none.
73
+ for (const embed of (_plan_embeds = plan.embeds) != null ? _plan_embeds : []){
74
+ const where = isAiPlanNewRef(embed.where) ? `the component "${embed.where.slice(AI_PLAN_NEW_REF_PREFIX.length)}"` : embed.where;
75
+ const link = embed.url ? `, url ${embed.url}` : ', url left empty for the site owner to paste';
76
+ lines.push(`- embed a ${AI_BUILD_PLAN_EMBED_HOST_NAMES[embed.host]} player (videoEmbed) on ${where}${link}`);
69
77
  }
70
78
  return lines.length > 1 ? lines : [];
71
79
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-generation.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n REUSABLE_INSTANCE_COMPONENT_ID,\n REUSABLE_INSTANCE_PROP_VALUES_KEY,\n} from '@aglyn/aglyn/app-utils/reusable-component-keys'\nimport type { AiBuildPlanCreate, AiBuildPlanCreateKind } from '../model/ai-build-plan'\nimport type {\n AiJob,\n AiJobOutput,\n AiJobPlan,\n AiJobReview,\n AiJobReviewOutlineNode,\n} from '../model/ai-jobs.types'\nimport type { AiSiteInventory } from '../model/ai-site-inventory'\nimport { aiAnswerTree, type AiGenerationSpend, type AiValidatedTree } from '../runtime/ai-doctrine'\nimport {\n aiBracketedFacts,\n aiTreeCopy,\n walkTree,\n type AiDoctrineNode,\n type AiDoctrineTree,\n type AiDoctrineViolation,\n} from '../runtime/ai-doctrine-validators'\nimport { AI_INSTANCE_REF_PROP } from '../runtime/ai-node-tree'\nimport type { AssistTokenUsage } from '../usage/assist-usage'\nimport { AI_JOB_BRIEF_MAX_CHARS, type AiJobStepOutcome } from './ai-job-text-step'\n\n/**\n * What the generation steps share (AGL-2909): reading the plan a member\n * confirmed, stating it to the model as references, and turning what a\n * generation spent or refused into the outcome the machine records.\n *\n * A generation step runs after its job's plan step, once a member confirmed\n * the plan, so the plan is part of what it executes: the records it reuses,\n * and the creation it builds. The plan travels as references — ids, names,\n * reasons — never as a document (AGL-2937).\n */\n\nexport const AI_JOB_ZERO_USAGE: AssistTokenUsage = {\n inputTokens: 0,\n outputTokens: 0,\n cacheReadTokens: 0,\n cacheWriteTokens: 0,\n}\n\n/** The job's plan once a member confirmed it; `null` before that, or for a job that never planned. */\nexport function aiConfirmedPlan(job: Pick<AiJob, 'plan'>): AiJobPlan | null {\n return job.plan?.status === 'confirmed' ? job.plan : null\n}\n\n/** The creation of one kind the plan names; the first when it names several. */\nexport function aiPlanCreation(\n plan: AiJobPlan | null,\n kind: AiBuildPlanCreateKind,\n): AiBuildPlanCreate | null {\n return plan?.create.find((entry) => entry.kind === kind) ?? null\n}\n\n/** A plan reference as the model reads it: the id with the name it was planned under. */\nfunction referenceOf(plan: AiJobPlan, ref: string): string {\n const label = plan.labels?.[ref]\n return label ? `${ref} (\"${label}\")` : ref\n}\n\n/** The user turn's opening: the brief, cut at the ceiling every job step keeps. */\nexport function aiJobBriefLine(job: Pick<AiJob, 'brief'>): string {\n return `Brief: ${job.brief.slice(0, AI_JOB_BRIEF_MAX_CHARS)}`\n}\n\n/** The confirmed plan as reference lines; none when there is no confirmed plan. */\nexport function aiPlanReferenceLines(plan: AiJobPlan | null): string[] {\n if (!plan) return []\n const lines = ['Confirmed plan:']\n for (const entry of plan.reuse) {\n lines.push(`- reuse the ${entry.kind} ${referenceOf(plan, entry.id)}: ${entry.purpose}`)\n }\n for (const entry of plan.create) {\n const from = entry.duplicateOf ? `, from a copy of ${referenceOf(plan, entry.duplicateOf)}` : ''\n const fields = entry.fields.length ? ` Fields: ${entry.fields.join(', ')}.` : ''\n lines.push(`- create the ${entry.kind} \"${entry.name}\"${from}: ${entry.why}${fields}`)\n }\n for (const screen of plan.screens) {\n const sections = screen.sections.map((section) => section.name).join(', ')\n lines.push(`- the screen \"${screen.title}\" at ${screen.slug}${sections ? `: ${sections}` : ''}`)\n }\n return lines.length > 1 ? lines : []\n}\n\n/** The reusable components a tree places, by the id each instance names. */\nexport function aiPlacedComponentIds(tree: Pick<AiValidatedTree, 'rootId' | 'nodes'>): Set<string> {\n const placed = new Set<string>()\n const nodes = tree.nodes as unknown as Record<string, AiDoctrineNode>\n for (const { node } of walkTree({ rootId: tree.rootId, nodes })) {\n const ref = node.props?.[AI_INSTANCE_REF_PROP]\n if (node.componentId === REUSABLE_INSTANCE_COMPONENT_ID && typeof ref === 'string') {\n placed.add(ref)\n }\n }\n return placed\n}\n\n/**\n * Rule 7 on a generated tree: every component the confirmed plan reuses is\n * placed. `noun` names what the tree builds, in the finding.\n */\nexport function aiPlanReuseViolations(\n inventory: AiSiteInventory | null,\n plan: AiJobPlan | null,\n placed: ReadonlySet<string>,\n noun: string,\n): AiDoctrineViolation[] {\n const names = new Map((inventory?.components ?? []).map((component) => [component.id, component.name]))\n const reused = [\n ...new Set(\n (plan?.reuse ?? []).filter((entry) => entry.kind === 'component').map((entry) => entry.id),\n ),\n ]\n const missing = reused.filter((id) => !placed.has(id))\n if (!missing.length) return []\n const listed = missing.map((id) => `\"${names.get(id) ?? id}\"`).join(', ')\n return [\n {\n rule: 7,\n code: 'plan-reuse-not-placed',\n message: `The confirmed plan reuses ${listed}, and the ${noun} does not place ${\n missing.length === 1 ? 'it' : 'them'\n }. Place every component the plan reuses.`,\n },\n ]\n}\n\n/** What a generation spent, as the step's outcome carries it for the meter. */\nexport function aiGenerationSpent(\n spend: AiGenerationSpend,\n): Pick<AiJobStepOutcome, 'outputs' | 'usage' | 'estCostUsd' | 'model' | 'stopReason'> {\n return {\n outputs: [],\n usage: spend.usage,\n estCostUsd: spend.estCostUsd,\n model: spend.model,\n stopReason: spend.stopReason,\n }\n}\n\n/** The most node ids, or plan paths, one finding keeps on its review (AGL-3078). */\nexport const AI_JOB_REVIEW_REFS_MAX = 24\n/** The longest node id or plan path a review keeps, in characters. */\nexport const AI_JOB_REVIEW_REF_MAX_CHARS = 64\n/** How far below a node a finding names its outline goes: the node, its children and theirs. */\nexport const AI_JOB_REVIEW_OUTLINE_MAX_DEPTH = 2\n/** The most nodes a review outlines, across all its findings. */\nexport const AI_JOB_REVIEW_OUTLINE_MAX_NODES = 40\n/** The most names an outlined node lists of its props, of its sx keys, or of its children. */\nexport const AI_JOB_REVIEW_OUTLINE_MAX_NAMES = 16\n/** The most bytes a review's outline takes as JSON, whatever the counts above allow. */\nexport const AI_JOB_REVIEW_OUTLINE_MAX_BYTES = 4_096\n\n/** A Grid's layout props, whose written values an outline keeps. */\nconst AI_JOB_REVIEW_GRID_PROPS = [\n 'container',\n 'size',\n 'offset',\n 'direction',\n 'wrap',\n 'spacing',\n 'rowSpacing',\n 'columnSpacing',\n 'columns',\n] as const\n\n/** A node id or plan path a review keeps: no whitespace, slash or quote, so never copy or an address. */\nconst REVIEW_REF = /^[^\\s/\\\\\"'<>]+$/\n/** An element id an outline names. */\nconst OUTLINE_ELEMENT = /^[\\w.-]{1,64}$/\n/** A prop or sx name an outline lists. */\nconst OUTLINE_NAME = /^[A-Za-z_$][\\w$-]{0,39}$/\n/** The longest layout value an outline keeps as written. */\nconst OUTLINE_VALUE_MAX_CHARS = 40\n/**\n * The words a Grid's layout is written in, besides numbers: breakpoints, span\n * words, switch words, directions and wraps. Nothing else is kept, so no copy is.\n */\nconst OUTLINE_LAYOUT_WORDS = new Set([\n 'xs',\n 'sm',\n 'md',\n 'lg',\n 'xl',\n 'auto',\n 'grow',\n 'true',\n 'false',\n 'yes',\n 'no',\n 'on',\n 'off',\n 'row',\n 'row-reverse',\n 'column',\n 'column-reverse',\n 'wrap',\n 'nowrap',\n 'wrap-reverse',\n])\n/** A number a layout value is written in, with a CSS length unit where it has one. */\nconst OUTLINE_LAYOUT_NUMBER = /^[-+]?(?:\\d+(?:\\.\\d+)?|\\.\\d+)(?:px|rem|em|%)?$/i\n/** What stands between the words of a layout value, as in `xs:12 md:4` or `{ xs: 12, md: 4 }`. */\nconst OUTLINE_LAYOUT_SEPARATORS = /[\\s,:;=/{}()[\\]\"']+/\n/** A binding token, which a component's Grid may carry for a layout value. */\nconst OUTLINE_BINDING = /^\\{\\{\\s*[\\w.-]+\\s*\\}\\}$/\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/** Whether text is a layout value: a binding token, or only layout words and numbers. */\nfunction isLayoutText(value: string): boolean {\n if (value.length > OUTLINE_VALUE_MAX_CHARS) return false\n if (OUTLINE_BINDING.test(value.trim())) return true\n return value\n .split(OUTLINE_LAYOUT_SEPARATORS)\n .every((word) => word === '' || OUTLINE_LAYOUT_WORDS.has(word.toLowerCase()) || OUTLINE_LAYOUT_NUMBER.test(word))\n}\n\n/** An id or a path as a review keeps it: cut to its ceiling, and `?` for one it cannot show. */\nfunction reviewRef(ref: string): string {\n const cut = ref.slice(0, AI_JOB_REVIEW_REF_MAX_CHARS)\n return REVIEW_REF.test(cut) ? cut : '?'\n}\n\nfunction reviewRefs(refs: readonly string[]): string[] {\n return refs.slice(0, AI_JOB_REVIEW_REFS_MAX).map(reviewRef)\n}\n\n/** A written layout value as an outline keeps it; text that is no layout value is kept as a marker. */\nfunction outlineValue(value: unknown): string | number | boolean | null {\n if (value === null) return null\n if (typeof value === 'boolean') return value\n if (typeof value === 'number') return Number.isFinite(value) ? value : null\n if (typeof value === 'string') return isLayoutText(value) ? value : '<text>'\n return Array.isArray(value) ? '<list>' : '<object>'\n}\n\nfunction childIdsOf(node: Record<string, unknown>): string[] {\n return Array.isArray(node['nodes']) ? node['nodes'].filter((child): child is string => typeof child === 'string') : []\n}\n\nfunction elementOf(node: unknown): string {\n const componentId = isRecord(node) ? node['componentId'] : undefined\n return typeof componentId === 'string' && OUTLINE_ELEMENT.test(componentId) ? componentId : '?'\n}\n\n/** One node of an outline: where it sits, what it is and the names of what it sets, never a copy value. */\nfunction outlineNodeOf(\n id: string,\n depth: number,\n node: Record<string, unknown>,\n nodes: Readonly<Record<string, unknown>>,\n): AiJobReviewOutlineNode {\n const props = isRecord(node['props']) ? node['props'] : {}\n const sx = isRecord(node['sx']) ? node['sx'] : isRecord(props['sx']) ? props['sx'] : {}\n const names = (keys: string[]) =>\n keys.filter((key) => OUTLINE_NAME.test(key)).slice(0, AI_JOB_REVIEW_OUTLINE_MAX_NAMES)\n const componentId = elementOf(node)\n const grid =\n componentId === 'muiGrid'\n ? Object.fromEntries(\n AI_JOB_REVIEW_GRID_PROPS.filter((name) => props[name] !== undefined).map((name) => [\n name,\n outlineValue(props[name]),\n ]),\n )\n : {}\n const sxNames = names(Object.keys(sx))\n return {\n id: reviewRef(id),\n depth,\n componentId,\n props: names(Object.keys(props).filter((key) => key !== 'sx')),\n ...(sxNames.length ? { sx: sxNames } : {}),\n ...(Object.keys(grid).length ? { grid } : {}),\n children: childIdsOf(node)\n .filter((child) => isRecord(nodes[child]))\n .slice(0, AI_JOB_REVIEW_OUTLINE_MAX_NAMES)\n .map((child) => elementOf(nodes[child])),\n }\n}\n\n/**\n * The parts of a refused answer its findings name, as a review keeps them\n * (AGL-3078): each node a finding names by id, with the nodes below it to\n * `AI_JOB_REVIEW_OUTLINE_MAX_DEPTH`, in document order and each once. A node\n * keeps its id, its element, the names of its props and sx keys and its\n * children's elements, and a Grid the values of its layout props as written;\n * no copy, and no address, is kept. The outline stops at\n * `AI_JOB_REVIEW_OUTLINE_MAX_NODES` nodes or `AI_JOB_REVIEW_OUTLINE_MAX_BYTES`\n * bytes, whichever comes first. An answer that is no node map, and findings\n * that name no node, outline nothing.\n */\nexport function aiDoctrineReviewOutline(\n answer: Record<string, unknown> | null | undefined,\n violations: readonly AiDoctrineViolation[],\n): AiJobReviewOutlineNode[] {\n const outline: AiJobReviewOutlineNode[] = []\n const tree = answer ? aiAnswerTree(answer) : null\n const nodes = isRecord(tree) && isRecord(tree['nodes']) ? tree['nodes'] : null\n if (!nodes) return outline\n const encoder = new TextEncoder()\n const seen = new Set<string>()\n let bytes = 2\n // False once the outline is full, which ends the walk.\n const add = (id: string, depth: number): boolean => {\n const node = nodes[id]\n if (seen.has(id) || !isRecord(node)) return true\n seen.add(id)\n const entry = outlineNodeOf(id, depth, node, nodes)\n const size = encoder.encode(JSON.stringify(entry)).length + 1\n if (outline.length === AI_JOB_REVIEW_OUTLINE_MAX_NODES || bytes + size > AI_JOB_REVIEW_OUTLINE_MAX_BYTES) {\n return false\n }\n outline.push(entry)\n bytes += size\n return depth === AI_JOB_REVIEW_OUTLINE_MAX_DEPTH || childIdsOf(node).every((child) => add(child, depth + 1))\n }\n const named = violations.flatMap((violation) => (violation.nodeIds ?? []).slice(0, AI_JOB_REVIEW_REFS_MAX))\n for (const id of new Set(named)) {\n if (!add(id, 0)) break\n }\n return outline\n}\n\n/**\n * The review a generation stops its job for when the re-ask still broke a\n * rule: the sentence a person reads, each finding with the nodes or plan\n * entries it names, and an outline of those parts of the answer it was found\n * in (AGL-3078), so why a job stopped can be read back from the job once the\n * answer itself is gone.\n */\nexport function aiDoctrineReview(result: {\n message: string\n violations: readonly AiDoctrineViolation[]\n /** The refused answer, whose nodes the findings name by id; outlined, never kept. */\n answer?: Record<string, unknown> | null\n}): AiJobReview {\n const outline = aiDoctrineReviewOutline(result.answer, result.violations)\n return {\n reason: 'doctrine',\n message: result.message,\n findings: result.violations.map(({ rule, code, message, nodeIds, paths }) => ({\n rule,\n code,\n message,\n ...(nodeIds?.length ? { nodeIds: reviewRefs(nodeIds) } : {}),\n ...(paths?.length ? { paths: reviewRefs(paths) } : {}),\n })),\n ...(outline.length ? { outline } : {}),\n }\n}\n\n/** The review a step stops its job for when the site cannot take the draft. */\nexport function aiLimitReview(message: string): AiJobReview {\n return { reason: 'limit', message, findings: [] }\n}\n\n/**\n * An outcome that reached no model: a draft an earlier run already wrote, a\n * copy the plan asked for, or a person asked before anything was spent.\n */\nexport function aiUnspentOutcome(\n model: string,\n rest: { outputs?: AiJobOutput[]; review?: AiJobReview } = {},\n): AiJobStepOutcome {\n return {\n outputs: rest.outputs ?? [],\n usage: AI_JOB_ZERO_USAGE,\n estCostUsd: 0,\n model,\n stopReason: null,\n ...(rest.review ? { review: rest.review } : {}),\n }\n}\n\n/** Node ids as the MODEL wrote them, from the minted ids a check reads. */\nexport function aiModelNodeIds(\n ids: readonly string[],\n sourceIds: Readonly<Record<string, string>>,\n): string[] {\n return ids.map((id) => sourceIds[id] ?? id)\n}\n\n/** The most facts a note names before it counts the rest. */\nconst AI_NOTE_MAX_FACTS = 8\n\n/** Facts as a sentence lists them: \"a, b and c\", the ones past the cap counted. */\nfunction factList(facts: readonly string[]): string {\n const shown = facts.slice(0, AI_NOTE_MAX_FACTS)\n const items = facts.length > shown.length ? [...shown, `${facts.length - shown.length} more`] : shown\n return items.length <= 1 ? items.join('') : `${items.slice(0, -1).join(', ')} and ${items[items.length - 1]}`\n}\n\n/**\n * What a draft asks the member to fill in before it is published (AGL-3056),\n * in the words the job's output note shows: the facts in square brackets its\n * copy holds, which the brief did not give (rule 14), each once. A page's\n * placed components show their own defaults wherever the page sets no value\n * of its own, so a component whose defaults hold such facts is named once,\n * however often the page places it. `copy` is what the draft shows beside its\n * tree, such as a component's defaults. `null` when there is nothing to fill.\n */\nexport function aiBracketedFactsNote(input: {\n tree: AiDoctrineTree\n inventory: AiSiteInventory | null\n copy?: readonly string[]\n}): string | null {\n const components = input.inventory?.components ?? []\n const componentProps = Object.fromEntries(components.map((component) => [component.id, component.props]))\n const facts = aiBracketedFacts([\n ...aiTreeCopy(input.tree, { componentProps }).map((sample) => sample.text),\n ...(input.copy ?? []),\n ])\n const listed = new Set(facts.map((fact) => fact.toLowerCase()))\n const byComponent = new Map<string, string[]>()\n for (const { node } of walkTree(input.tree)) {\n const component = components.find((row) => row.id === node.props?.[AI_INSTANCE_REF_PROP])\n if (node.componentId !== REUSABLE_INSTANCE_COMPONENT_ID || !component?.bracketedDefaults) continue\n const values = node.props?.[REUSABLE_INSTANCE_PROP_VALUES_KEY]\n const set = (name: string): boolean => {\n const value = values && typeof values === 'object' ? (values as Record<string, unknown>)[name] : undefined\n return value !== undefined && value !== null && String(value).trim() !== ''\n }\n const shown = byComponent.get(component.name) ?? []\n for (const [name, defaults] of Object.entries(component.bracketedDefaults)) {\n if (set(name)) continue\n for (const fact of defaults) {\n if (!listed.has(fact.toLowerCase()) && !shown.some((known) => known.toLowerCase() === fact.toLowerCase())) {\n shown.push(fact)\n }\n }\n }\n if (shown.length) byComponent.set(component.name, shown)\n }\n const sentences = [\n ...(facts.length\n ? [`Before you publish, replace the facts in square brackets, which the brief did not give: ${factList(facts)}.`]\n : []),\n ...[...byComponent.entries()].map(\n ([name, shown]) =>\n `The \"${name}\" component on this page shows ${factList(shown)} until you replace ${shown.length === 1 ? 'it' : 'them'}.`,\n ),\n ]\n return sentences.length ? sentences.join(' ') : null\n}\n"],"names":["REUSABLE_INSTANCE_COMPONENT_ID","REUSABLE_INSTANCE_PROP_VALUES_KEY","aiAnswerTree","aiBracketedFacts","aiTreeCopy","walkTree","AI_INSTANCE_REF_PROP","AI_JOB_BRIEF_MAX_CHARS","AI_JOB_ZERO_USAGE","inputTokens","outputTokens","cacheReadTokens","cacheWriteTokens","aiConfirmedPlan","job","plan","status","aiPlanCreation","kind","create","find","entry","referenceOf","ref","label","labels","aiJobBriefLine","brief","slice","aiPlanReferenceLines","lines","reuse","push","id","purpose","from","duplicateOf","fields","length","join","name","why","screen","screens","sections","map","section","title","slug","aiPlacedComponentIds","tree","placed","Set","nodes","node","rootId","props","componentId","add","aiPlanReuseViolations","inventory","noun","names","Map","components","component","reused","filter","missing","has","listed","get","rule","code","message","aiGenerationSpent","spend","outputs","usage","estCostUsd","model","stopReason","AI_JOB_REVIEW_REFS_MAX","AI_JOB_REVIEW_REF_MAX_CHARS","AI_JOB_REVIEW_OUTLINE_MAX_DEPTH","AI_JOB_REVIEW_OUTLINE_MAX_NODES","AI_JOB_REVIEW_OUTLINE_MAX_NAMES","AI_JOB_REVIEW_OUTLINE_MAX_BYTES","AI_JOB_REVIEW_GRID_PROPS","REVIEW_REF","OUTLINE_ELEMENT","OUTLINE_NAME","OUTLINE_VALUE_MAX_CHARS","OUTLINE_LAYOUT_WORDS","OUTLINE_LAYOUT_NUMBER","OUTLINE_LAYOUT_SEPARATORS","OUTLINE_BINDING","isRecord","value","Array","isArray","isLayoutText","test","trim","split","every","word","toLowerCase","reviewRef","cut","reviewRefs","refs","outlineValue","Number","isFinite","childIdsOf","child","elementOf","undefined","outlineNodeOf","depth","sx","keys","key","grid","Object","fromEntries","sxNames","children","aiDoctrineReviewOutline","answer","violations","outline","encoder","TextEncoder","seen","bytes","size","encode","JSON","stringify","named","flatMap","violation","nodeIds","aiDoctrineReview","result","reason","findings","paths","aiLimitReview","aiUnspentOutcome","rest","review","aiModelNodeIds","ids","sourceIds","AI_NOTE_MAX_FACTS","factList","facts","shown","items","aiBracketedFactsNote","input","componentProps","sample","text","copy","fact","byComponent","row","bracketedDefaults","values","set","String","defaults","entries","some","known","sentences"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-generation.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n REUSABLE_INSTANCE_COMPONENT_ID,\n REUSABLE_INSTANCE_PROP_VALUES_KEY,\n} from '@aglyn/aglyn/app-utils/reusable-component-keys'\nimport {\n AI_BUILD_PLAN_EMBED_HOST_NAMES,\n AI_PLAN_NEW_REF_PREFIX,\n isAiPlanNewRef,\n type AiBuildPlanCreate,\n type AiBuildPlanCreateKind,\n} from '../model/ai-build-plan'\nimport type {\n AiJob,\n AiJobOutput,\n AiJobPlan,\n AiJobReview,\n AiJobReviewOutlineNode,\n} from '../model/ai-jobs.types'\nimport type { AiSiteInventory } from '../model/ai-site-inventory'\nimport { aiAnswerTree, type AiGenerationSpend, type AiValidatedTree } from '../runtime/ai-doctrine'\nimport {\n aiBracketedFacts,\n aiTreeCopy,\n walkTree,\n type AiDoctrineNode,\n type AiDoctrineTree,\n type AiDoctrineViolation,\n} from '../runtime/ai-doctrine-validators'\nimport { AI_INSTANCE_REF_PROP } from '../runtime/ai-node-tree'\nimport type { AssistTokenUsage } from '../usage/assist-usage'\nimport { AI_JOB_BRIEF_MAX_CHARS, type AiJobStepOutcome } from './ai-job-text-step'\n\n/**\n * What the generation steps share (AGL-2909): reading the plan a member\n * confirmed, stating it to the model as references, and turning what a\n * generation spent or refused into the outcome the machine records.\n *\n * A generation step runs after its job's plan step, once a member confirmed\n * the plan, so the plan is part of what it executes: the records it reuses,\n * and the creation it builds. The plan travels as references — ids, names,\n * reasons — never as a document (AGL-2937).\n */\n\nexport const AI_JOB_ZERO_USAGE: AssistTokenUsage = {\n inputTokens: 0,\n outputTokens: 0,\n cacheReadTokens: 0,\n cacheWriteTokens: 0,\n}\n\n/** The job's plan once a member confirmed it; `null` before that, or for a job that never planned. */\nexport function aiConfirmedPlan(job: Pick<AiJob, 'plan'>): AiJobPlan | null {\n return job.plan?.status === 'confirmed' ? job.plan : null\n}\n\n/** The creation of one kind the plan names; the first when it names several. */\nexport function aiPlanCreation(\n plan: AiJobPlan | null,\n kind: AiBuildPlanCreateKind,\n): AiBuildPlanCreate | null {\n return plan?.create.find((entry) => entry.kind === kind) ?? null\n}\n\n/** A plan reference as the model reads it: the id with the name it was planned under. */\nfunction referenceOf(plan: AiJobPlan, ref: string): string {\n const label = plan.labels?.[ref]\n return label ? `${ref} (\"${label}\")` : ref\n}\n\n/** The user turn's opening: the brief, cut at the ceiling every job step keeps. */\nexport function aiJobBriefLine(job: Pick<AiJob, 'brief'>): string {\n return `Brief: ${job.brief.slice(0, AI_JOB_BRIEF_MAX_CHARS)}`\n}\n\n/** The confirmed plan as reference lines; none when there is no confirmed plan. */\nexport function aiPlanReferenceLines(plan: AiJobPlan | null): string[] {\n if (!plan) return []\n const lines = ['Confirmed plan:']\n for (const entry of plan.reuse) {\n lines.push(`- reuse the ${entry.kind === 'screen' ? 'page' : entry.kind} ${referenceOf(plan, entry.id)}: ${entry.purpose}`)\n }\n for (const entry of plan.create) {\n const from = entry.duplicateOf ? `, from a copy of ${referenceOf(plan, entry.duplicateOf)}` : ''\n const fields = entry.fields.length ? ` Fields: ${entry.fields.join(', ')}.` : ''\n lines.push(`- create the ${entry.kind} \"${entry.name}\"${from}: ${entry.why}${fields}`)\n }\n for (const screen of plan.screens) {\n const sections = screen.sections.map((section) => section.name).join(', ')\n lines.push(`- the page \"${screen.title}\" at ${screen.slug}${sections ? `: ${sections}` : ''}`)\n }\n // The one place a Video embed is admitted (AGL-3433): where the plan lists it, at the brief's link or none.\n for (const embed of plan.embeds ?? []) {\n const where = isAiPlanNewRef(embed.where)\n ? `the component \"${embed.where.slice(AI_PLAN_NEW_REF_PREFIX.length)}\"`\n : embed.where\n const link = embed.url ? `, url ${embed.url}` : ', url left empty for the site owner to paste'\n lines.push(`- embed a ${AI_BUILD_PLAN_EMBED_HOST_NAMES[embed.host]} player (videoEmbed) on ${where}${link}`)\n }\n return lines.length > 1 ? lines : []\n}\n\n/** The reusable components a tree places, by the id each instance names. */\nexport function aiPlacedComponentIds(tree: Pick<AiValidatedTree, 'rootId' | 'nodes'>): Set<string> {\n const placed = new Set<string>()\n const nodes = tree.nodes as unknown as Record<string, AiDoctrineNode>\n for (const { node } of walkTree({ rootId: tree.rootId, nodes })) {\n const ref = node.props?.[AI_INSTANCE_REF_PROP]\n if (node.componentId === REUSABLE_INSTANCE_COMPONENT_ID && typeof ref === 'string') {\n placed.add(ref)\n }\n }\n return placed\n}\n\n/**\n * Rule 7 on a generated tree: every component the confirmed plan reuses is\n * placed. `noun` names what the tree builds, in the finding.\n */\nexport function aiPlanReuseViolations(\n inventory: AiSiteInventory | null,\n plan: AiJobPlan | null,\n placed: ReadonlySet<string>,\n noun: string,\n): AiDoctrineViolation[] {\n const names = new Map((inventory?.components ?? []).map((component) => [component.id, component.name]))\n const reused = [\n ...new Set(\n (plan?.reuse ?? []).filter((entry) => entry.kind === 'component').map((entry) => entry.id),\n ),\n ]\n const missing = reused.filter((id) => !placed.has(id))\n if (!missing.length) return []\n const listed = missing.map((id) => `\"${names.get(id) ?? id}\"`).join(', ')\n return [\n {\n rule: 7,\n code: 'plan-reuse-not-placed',\n message: `The confirmed plan reuses ${listed}, and the ${noun} does not place ${\n missing.length === 1 ? 'it' : 'them'\n }. Place every component the plan reuses.`,\n },\n ]\n}\n\n/** What a generation spent, as the step's outcome carries it for the meter. */\nexport function aiGenerationSpent(\n spend: AiGenerationSpend,\n): Pick<AiJobStepOutcome, 'outputs' | 'usage' | 'estCostUsd' | 'model' | 'stopReason'> {\n return {\n outputs: [],\n usage: spend.usage,\n estCostUsd: spend.estCostUsd,\n model: spend.model,\n stopReason: spend.stopReason,\n }\n}\n\n/** The most node ids, or plan paths, one finding keeps on its review (AGL-3078). */\nexport const AI_JOB_REVIEW_REFS_MAX = 24\n/** The longest node id or plan path a review keeps, in characters. */\nexport const AI_JOB_REVIEW_REF_MAX_CHARS = 64\n/** How far below a node a finding names its outline goes: the node, its children and theirs. */\nexport const AI_JOB_REVIEW_OUTLINE_MAX_DEPTH = 2\n/** The most nodes a review outlines, across all its findings. */\nexport const AI_JOB_REVIEW_OUTLINE_MAX_NODES = 40\n/** The most names an outlined node lists of its props, of its sx keys, or of its children. */\nexport const AI_JOB_REVIEW_OUTLINE_MAX_NAMES = 16\n/** The most bytes a review's outline takes as JSON, whatever the counts above allow. */\nexport const AI_JOB_REVIEW_OUTLINE_MAX_BYTES = 4_096\n\n/** A Grid's layout props, whose written values an outline keeps. */\nconst AI_JOB_REVIEW_GRID_PROPS = [\n 'container',\n 'size',\n 'offset',\n 'direction',\n 'wrap',\n 'spacing',\n 'rowSpacing',\n 'columnSpacing',\n 'columns',\n] as const\n\n/** A node id or plan path a review keeps: no whitespace, slash or quote, so never copy or an address. */\nconst REVIEW_REF = /^[^\\s/\\\\\"'<>]+$/\n/** An element id an outline names. */\nconst OUTLINE_ELEMENT = /^[\\w.-]{1,64}$/\n/** A prop or sx name an outline lists. */\nconst OUTLINE_NAME = /^[A-Za-z_$][\\w$-]{0,39}$/\n/** The longest layout value an outline keeps as written. */\nconst OUTLINE_VALUE_MAX_CHARS = 40\n/**\n * The words a Grid's layout is written in, besides numbers: breakpoints, span\n * words, switch words, directions and wraps. Nothing else is kept, so no copy is.\n */\nconst OUTLINE_LAYOUT_WORDS = new Set([\n 'xs',\n 'sm',\n 'md',\n 'lg',\n 'xl',\n 'auto',\n 'grow',\n 'true',\n 'false',\n 'yes',\n 'no',\n 'on',\n 'off',\n 'row',\n 'row-reverse',\n 'column',\n 'column-reverse',\n 'wrap',\n 'nowrap',\n 'wrap-reverse',\n])\n/** A number a layout value is written in, with a CSS length unit where it has one. */\nconst OUTLINE_LAYOUT_NUMBER = /^[-+]?(?:\\d+(?:\\.\\d+)?|\\.\\d+)(?:px|rem|em|%)?$/i\n/** What stands between the words of a layout value, as in `xs:12 md:4` or `{ xs: 12, md: 4 }`. */\nconst OUTLINE_LAYOUT_SEPARATORS = /[\\s,:;=/{}()[\\]\"']+/\n/** A binding token, which a component's Grid may carry for a layout value. */\nconst OUTLINE_BINDING = /^\\{\\{\\s*[\\w.-]+\\s*\\}\\}$/\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/** Whether text is a layout value: a binding token, or only layout words and numbers. */\nfunction isLayoutText(value: string): boolean {\n if (value.length > OUTLINE_VALUE_MAX_CHARS) return false\n if (OUTLINE_BINDING.test(value.trim())) return true\n return value\n .split(OUTLINE_LAYOUT_SEPARATORS)\n .every((word) => word === '' || OUTLINE_LAYOUT_WORDS.has(word.toLowerCase()) || OUTLINE_LAYOUT_NUMBER.test(word))\n}\n\n/** An id or a path as a review keeps it: cut to its ceiling, and `?` for one it cannot show. */\nfunction reviewRef(ref: string): string {\n const cut = ref.slice(0, AI_JOB_REVIEW_REF_MAX_CHARS)\n return REVIEW_REF.test(cut) ? cut : '?'\n}\n\nfunction reviewRefs(refs: readonly string[]): string[] {\n return refs.slice(0, AI_JOB_REVIEW_REFS_MAX).map(reviewRef)\n}\n\n/** A written layout value as an outline keeps it; text that is no layout value is kept as a marker. */\nfunction outlineValue(value: unknown): string | number | boolean | null {\n if (value === null) return null\n if (typeof value === 'boolean') return value\n if (typeof value === 'number') return Number.isFinite(value) ? value : null\n if (typeof value === 'string') return isLayoutText(value) ? value : '<text>'\n return Array.isArray(value) ? '<list>' : '<object>'\n}\n\nfunction childIdsOf(node: Record<string, unknown>): string[] {\n return Array.isArray(node['nodes']) ? node['nodes'].filter((child): child is string => typeof child === 'string') : []\n}\n\nfunction elementOf(node: unknown): string {\n const componentId = isRecord(node) ? node['componentId'] : undefined\n return typeof componentId === 'string' && OUTLINE_ELEMENT.test(componentId) ? componentId : '?'\n}\n\n/** One node of an outline: where it sits, what it is and the names of what it sets, never a copy value. */\nfunction outlineNodeOf(\n id: string,\n depth: number,\n node: Record<string, unknown>,\n nodes: Readonly<Record<string, unknown>>,\n): AiJobReviewOutlineNode {\n const props = isRecord(node['props']) ? node['props'] : {}\n const sx = isRecord(node['sx']) ? node['sx'] : isRecord(props['sx']) ? props['sx'] : {}\n const names = (keys: string[]) =>\n keys.filter((key) => OUTLINE_NAME.test(key)).slice(0, AI_JOB_REVIEW_OUTLINE_MAX_NAMES)\n const componentId = elementOf(node)\n const grid =\n componentId === 'muiGrid'\n ? Object.fromEntries(\n AI_JOB_REVIEW_GRID_PROPS.filter((name) => props[name] !== undefined).map((name) => [\n name,\n outlineValue(props[name]),\n ]),\n )\n : {}\n const sxNames = names(Object.keys(sx))\n return {\n id: reviewRef(id),\n depth,\n componentId,\n props: names(Object.keys(props).filter((key) => key !== 'sx')),\n ...(sxNames.length ? { sx: sxNames } : {}),\n ...(Object.keys(grid).length ? { grid } : {}),\n children: childIdsOf(node)\n .filter((child) => isRecord(nodes[child]))\n .slice(0, AI_JOB_REVIEW_OUTLINE_MAX_NAMES)\n .map((child) => elementOf(nodes[child])),\n }\n}\n\n/**\n * The parts of a refused answer its findings name, as a review keeps them\n * (AGL-3078): each node a finding names by id, with the nodes below it to\n * `AI_JOB_REVIEW_OUTLINE_MAX_DEPTH`, in document order and each once. A node\n * keeps its id, its element, the names of its props and sx keys and its\n * children's elements, and a Grid the values of its layout props as written;\n * no copy, and no address, is kept. The outline stops at\n * `AI_JOB_REVIEW_OUTLINE_MAX_NODES` nodes or `AI_JOB_REVIEW_OUTLINE_MAX_BYTES`\n * bytes, whichever comes first. An answer that is no node map, and findings\n * that name no node, outline nothing.\n */\nexport function aiDoctrineReviewOutline(\n answer: Record<string, unknown> | null | undefined,\n violations: readonly AiDoctrineViolation[],\n): AiJobReviewOutlineNode[] {\n const outline: AiJobReviewOutlineNode[] = []\n const tree = answer ? aiAnswerTree(answer) : null\n const nodes = isRecord(tree) && isRecord(tree['nodes']) ? tree['nodes'] : null\n if (!nodes) return outline\n const encoder = new TextEncoder()\n const seen = new Set<string>()\n let bytes = 2\n // False once the outline is full, which ends the walk.\n const add = (id: string, depth: number): boolean => {\n const node = nodes[id]\n if (seen.has(id) || !isRecord(node)) return true\n seen.add(id)\n const entry = outlineNodeOf(id, depth, node, nodes)\n const size = encoder.encode(JSON.stringify(entry)).length + 1\n if (outline.length === AI_JOB_REVIEW_OUTLINE_MAX_NODES || bytes + size > AI_JOB_REVIEW_OUTLINE_MAX_BYTES) {\n return false\n }\n outline.push(entry)\n bytes += size\n return depth === AI_JOB_REVIEW_OUTLINE_MAX_DEPTH || childIdsOf(node).every((child) => add(child, depth + 1))\n }\n const named = violations.flatMap((violation) => (violation.nodeIds ?? []).slice(0, AI_JOB_REVIEW_REFS_MAX))\n for (const id of new Set(named)) {\n if (!add(id, 0)) break\n }\n return outline\n}\n\n/**\n * The review a generation stops its job for when the re-ask still broke a\n * rule: the sentence a person reads, each finding with the nodes or plan\n * entries it names, and an outline of those parts of the answer it was found\n * in (AGL-3078), so why a job stopped can be read back from the job once the\n * answer itself is gone.\n */\nexport function aiDoctrineReview(result: {\n message: string\n violations: readonly AiDoctrineViolation[]\n /** The refused answer, whose nodes the findings name by id; outlined, never kept. */\n answer?: Record<string, unknown> | null\n}): AiJobReview {\n const outline = aiDoctrineReviewOutline(result.answer, result.violations)\n return {\n reason: 'doctrine',\n message: result.message,\n findings: result.violations.map(({ rule, code, message, nodeIds, paths }) => ({\n rule,\n code,\n message,\n ...(nodeIds?.length ? { nodeIds: reviewRefs(nodeIds) } : {}),\n ...(paths?.length ? { paths: reviewRefs(paths) } : {}),\n })),\n ...(outline.length ? { outline } : {}),\n }\n}\n\n/** The review a step stops its job for when the site cannot take the draft. */\nexport function aiLimitReview(message: string): AiJobReview {\n return { reason: 'limit', message, findings: [] }\n}\n\n/**\n * An outcome that reached no model: a draft an earlier run already wrote, a\n * copy the plan asked for, or a person asked before anything was spent.\n */\nexport function aiUnspentOutcome(\n model: string,\n rest: { outputs?: AiJobOutput[]; review?: AiJobReview } = {},\n): AiJobStepOutcome {\n return {\n outputs: rest.outputs ?? [],\n usage: AI_JOB_ZERO_USAGE,\n estCostUsd: 0,\n model,\n stopReason: null,\n ...(rest.review ? { review: rest.review } : {}),\n }\n}\n\n/** Node ids as the MODEL wrote them, from the minted ids a check reads. */\nexport function aiModelNodeIds(\n ids: readonly string[],\n sourceIds: Readonly<Record<string, string>>,\n): string[] {\n return ids.map((id) => sourceIds[id] ?? id)\n}\n\n/** The most facts a note names before it counts the rest. */\nconst AI_NOTE_MAX_FACTS = 8\n\n/** Facts as a sentence lists them: \"a, b and c\", the ones past the cap counted. */\nfunction factList(facts: readonly string[]): string {\n const shown = facts.slice(0, AI_NOTE_MAX_FACTS)\n const items = facts.length > shown.length ? [...shown, `${facts.length - shown.length} more`] : shown\n return items.length <= 1 ? items.join('') : `${items.slice(0, -1).join(', ')} and ${items[items.length - 1]}`\n}\n\n/**\n * What a draft asks the member to fill in before it is published (AGL-3056),\n * in the words the job's output note shows: the facts in square brackets its\n * copy holds, which the brief did not give (rule 14), each once. A page's\n * placed components show their own defaults wherever the page sets no value\n * of its own, so a component whose defaults hold such facts is named once,\n * however often the page places it. `copy` is what the draft shows beside its\n * tree, such as a component's defaults. `null` when there is nothing to fill.\n */\nexport function aiBracketedFactsNote(input: {\n tree: AiDoctrineTree\n inventory: AiSiteInventory | null\n copy?: readonly string[]\n}): string | null {\n const components = input.inventory?.components ?? []\n const componentProps = Object.fromEntries(components.map((component) => [component.id, component.props]))\n const facts = aiBracketedFacts([\n ...aiTreeCopy(input.tree, { componentProps }).map((sample) => sample.text),\n ...(input.copy ?? []),\n ])\n const listed = new Set(facts.map((fact) => fact.toLowerCase()))\n const byComponent = new Map<string, string[]>()\n for (const { node } of walkTree(input.tree)) {\n const component = components.find((row) => row.id === node.props?.[AI_INSTANCE_REF_PROP])\n if (node.componentId !== REUSABLE_INSTANCE_COMPONENT_ID || !component?.bracketedDefaults) continue\n const values = node.props?.[REUSABLE_INSTANCE_PROP_VALUES_KEY]\n const set = (name: string): boolean => {\n const value = values && typeof values === 'object' ? (values as Record<string, unknown>)[name] : undefined\n return value !== undefined && value !== null && String(value).trim() !== ''\n }\n const shown = byComponent.get(component.name) ?? []\n for (const [name, defaults] of Object.entries(component.bracketedDefaults)) {\n if (set(name)) continue\n for (const fact of defaults) {\n if (!listed.has(fact.toLowerCase()) && !shown.some((known) => known.toLowerCase() === fact.toLowerCase())) {\n shown.push(fact)\n }\n }\n }\n if (shown.length) byComponent.set(component.name, shown)\n }\n const sentences = [\n ...(facts.length\n ? [`Before you publish, replace the facts in square brackets, which the brief did not give: ${factList(facts)}.`]\n : []),\n ...[...byComponent.entries()].map(\n ([name, shown]) =>\n `The \"${name}\" component on this page shows ${factList(shown)} until you replace ${shown.length === 1 ? 'it' : 'them'}.`,\n ),\n ]\n return sentences.length ? sentences.join(' ') : null\n}\n"],"names":["REUSABLE_INSTANCE_COMPONENT_ID","REUSABLE_INSTANCE_PROP_VALUES_KEY","AI_BUILD_PLAN_EMBED_HOST_NAMES","AI_PLAN_NEW_REF_PREFIX","isAiPlanNewRef","aiAnswerTree","aiBracketedFacts","aiTreeCopy","walkTree","AI_INSTANCE_REF_PROP","AI_JOB_BRIEF_MAX_CHARS","AI_JOB_ZERO_USAGE","inputTokens","outputTokens","cacheReadTokens","cacheWriteTokens","aiConfirmedPlan","job","plan","status","aiPlanCreation","kind","create","find","entry","referenceOf","ref","label","labels","aiJobBriefLine","brief","slice","aiPlanReferenceLines","lines","reuse","push","id","purpose","from","duplicateOf","fields","length","join","name","why","screen","screens","sections","map","section","title","slug","embed","embeds","where","link","url","host","aiPlacedComponentIds","tree","placed","Set","nodes","node","rootId","props","componentId","add","aiPlanReuseViolations","inventory","noun","names","Map","components","component","reused","filter","missing","has","listed","get","rule","code","message","aiGenerationSpent","spend","outputs","usage","estCostUsd","model","stopReason","AI_JOB_REVIEW_REFS_MAX","AI_JOB_REVIEW_REF_MAX_CHARS","AI_JOB_REVIEW_OUTLINE_MAX_DEPTH","AI_JOB_REVIEW_OUTLINE_MAX_NODES","AI_JOB_REVIEW_OUTLINE_MAX_NAMES","AI_JOB_REVIEW_OUTLINE_MAX_BYTES","AI_JOB_REVIEW_GRID_PROPS","REVIEW_REF","OUTLINE_ELEMENT","OUTLINE_NAME","OUTLINE_VALUE_MAX_CHARS","OUTLINE_LAYOUT_WORDS","OUTLINE_LAYOUT_NUMBER","OUTLINE_LAYOUT_SEPARATORS","OUTLINE_BINDING","isRecord","value","Array","isArray","isLayoutText","test","trim","split","every","word","toLowerCase","reviewRef","cut","reviewRefs","refs","outlineValue","Number","isFinite","childIdsOf","child","elementOf","undefined","outlineNodeOf","depth","sx","keys","key","grid","Object","fromEntries","sxNames","children","aiDoctrineReviewOutline","answer","violations","outline","encoder","TextEncoder","seen","bytes","size","encode","JSON","stringify","named","flatMap","violation","nodeIds","aiDoctrineReview","result","reason","findings","paths","aiLimitReview","aiUnspentOutcome","rest","review","aiModelNodeIds","ids","sourceIds","AI_NOTE_MAX_FACTS","factList","facts","shown","items","aiBracketedFactsNote","input","componentProps","sample","text","copy","fact","byComponent","row","bracketedDefaults","values","set","String","defaults","entries","some","known","sentences"],"mappings":"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"}
@@ -69,7 +69,7 @@ import { registerAiJobStep } from "./ai-jobs.js";
69
69
  text: [
70
70
  'You build one shared layout for a website: the frame every page of the site renders inside. Answer with submit_layout: the whole layout as one flat node map.',
71
71
  'A layout is a header, one Layout Slot and a footer, top to bottom. The header is an App Bar (muiAppBar) holding a Toolbar Content (muiToolbar) with the site’s name as text and its navigation. The footer is a Section (section) whose element is footer.',
72
- 'Navigation links the site’s own screens by id: a Screen Link (muiScreenLink) whose screenId is a screen id from the site inventory, in the order the brief and the confirmed plan give, seven at most in the header. Never invent a screen id, and never link a screen by its address.',
72
+ 'Navigation links the site’s own pages by id: a Page Link (muiScreenLink) whose screenId is a page id from the site inventory, in the order the brief and the confirmed plan give, seven at most in the header. Never invent a page id, and never link a page by its address.',
73
73
  'When the site inventory lists a component whose name says it is navigation, a menu or a mega menu, place it in the header as an instance instead of building the navigation again. Place every component the confirmed plan reuses.',
74
74
  'The Layout Slot (layoutSlot) is where each page renders and is the page’s main landmark: leave its component unset or main, and put nothing inside it. A layout carries no h1, because every page brings its own.'
75
75
  ].join('\n')
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-layout-step.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { CANVAS_ROOT_ELEMENT_ID } from '@aglyn/aglyn/foundation/constants/canvas'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport { duplicateResource } from '@aglyn/tenant-data-admin/server/duplicate-resource'\nimport type { AiJob, AiJobOutput, AiJobPlan, AiJobReview } from '../model/ai-jobs.types'\nimport type { AiSiteInventory } from '../model/ai-site-inventory'\nimport { AI_STEP_TIERS } from '../providers/catalog'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport {\n aiDoctrineTreeTool,\n runValidatedGeneration,\n type AiValidatedTree,\n} from '../runtime/ai-doctrine'\nimport {\n walkTree,\n type AiDoctrineNode,\n type AiDoctrineTree,\n type AiDoctrineViolation,\n} from '../runtime/ai-doctrine-validators'\nimport type { AiLoadEstimate } from '../runtime/ai-palette'\nimport type { AiSystemBlock } from '../runtime/ai-runtime'\nimport { readSiteInventory } from '../runtime/site-inventory'\nimport { registerAiJobAdmission, type AiJobAdmission } from './ai-job-admission'\nimport { aiJobDraftId } from './ai-job-draft-ids'\nimport {\n aiDraftAdmissionRefusal,\n aiDraftAllowanceRefusal,\n aiSiteSubdomain,\n readAiDraft,\n readAiDraftNodes,\n writeAiDraft,\n type AiDraftRecord,\n} from './ai-job-drafts'\nimport {\n aiBracketedFactsNote,\n aiConfirmedPlan,\n aiDoctrineReview,\n aiGenerationSpent,\n aiJobBriefLine,\n aiLimitReview,\n aiModelNodeIds,\n aiPlacedComponentIds,\n aiPlanCreation,\n aiPlanReferenceLines,\n aiPlanReuseViolations,\n aiUnspentOutcome,\n} from './ai-job-generation'\nimport { aiPlanRegionViolations, aiPlannedLayoutRegions } from './ai-job-plan-conformance'\nimport type { AiJobStepRunner } from './ai-job-text-step'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { registerAiJobStep } from './ai-jobs'\n\n/**\n * The layout step (AGL-2909): the generation step of a `layout` job, run once\n * a member confirmed the job's plan.\n *\n * It builds ONE shared layout — a header with the site's navigation, the\n * Layout Slot each page renders in, a footer — through\n * `runValidatedGeneration('layout', …)`, so every building rule holds on the\n * tree before it is kept, and adds three checks of its own through `extend`: a\n * component the confirmed plan reuses is placed (rule 7), every region the\n * plan gives the layout is built (AGL-3024), and a navigation or menu\n * component the site already has is placed rather than drawn again (rule 1).\n *\n * The layout lands as a new draft (`ai-job-drafts.ts`): no screen uses it and\n * no layout nests it, so nothing on the live site changes until a member\n * assigns it. A plan that starts from a copy of a layout the site has gets\n * that copy, through the platform's duplicate module, and nothing generated —\n * so the copy is read back and held to the plan's regions the same way\n * (`aiCopiedLayoutReview`), because a branch that generates nothing is the one\n * branch where a promise can go unmet with no model to answer for it.\n *\n * A generation step runs on the job beat, never at an inline door: it\n * registers the least time its lookup rounds, its answer and its re-ask need\n * (AGL-3035), runs without extended thinking — the plan was reasoned out and\n * confirmed before it — and asks for no more of a tighter ceiling than the\n * doctrine's default for a layout than fits that time on the model it runs.\n */\n\n/** The longest answer a layout may run to; the tree is held to the layout budget either way. */\nexport const AI_JOB_LAYOUT_MAX_TOKENS = AI_ROUTING_TABLE['job.layout'].maxTokens\n\n/**\n * The layout step's time (AGL-3035, AGL-3036): its two inventory-lookup\n * rounds, its answer and its re-ask at the routing table's ceiling on the tier\n * `job.layout` is served from, fitted to what a beat can give a step.\n */\nexport const AI_JOB_LAYOUT_STEP_BUDGET = aiJobStepBudget({\n tier: AI_STEP_TIERS['job.layout'],\n maxTokens: AI_JOB_LAYOUT_MAX_TOKENS,\n})\n\n/** The least time one layout step needs before it starts. */\nexport const AI_JOB_LAYOUT_STEP_MINIMUM_MS = AI_JOB_LAYOUT_STEP_BUDGET.minimumMs\n\n/** A layout's name when the plan names none. */\nexport const AI_JOB_LAYOUT_DEFAULT_NAME = 'Site layout'\n\n/** The layout step's own instructions, cached after the doctrine. */\nexport const AI_JOB_LAYOUT_INSTRUCTIONS: readonly AiSystemBlock[] = [\n {\n text: [\n 'You build one shared layout for a website: the frame every page of the site renders inside. Answer with submit_layout: the whole layout as one flat node map.',\n 'A layout is a header, one Layout Slot and a footer, top to bottom. The header is an App Bar (muiAppBar) holding a Toolbar Content (muiToolbar) with the site’s name as text and its navigation. The footer is a Section (section) whose element is footer.',\n 'Navigation links the site’s own screens by id: a Screen Link (muiScreenLink) whose screenId is a screen id from the site inventory, in the order the brief and the confirmed plan give, seven at most in the header. Never invent a screen id, and never link a screen by its address.',\n 'When the site inventory lists a component whose name says it is navigation, a menu or a mega menu, place it in the header as an instance instead of building the navigation again. Place every component the confirmed plan reuses.',\n 'The Layout Slot (layoutSlot) is where each page renders and is the page’s main landmark: leave its component unset or main, and put nothing inside it. A layout carries no h1, because every page brings its own.',\n ].join('\\n'),\n },\n]\n\n/** The layout job as the generation's user turn: the name, the brief, the confirmed plan. */\nexport function aiJobLayoutPrompt(\n job: Pick<AiJob, 'brief'>,\n plan: AiJobPlan | null,\n name: string,\n): string {\n return [`Layout name: ${name}`, aiJobBriefLine(job), ...aiPlanReferenceLines(plan)].join('\\n')\n}\n\n/** A component name that says it is the site's navigation; a footer's links are not. */\nconst NAVIGATION_NAME = /\\b(?:nav|navigation|navbar|menu|mega ?menu)\\b/i\nconst FOOTER_NAME = /\\bfooter\\b/i\n\n/** A node that links somewhere: a Screen Link, or a Button pointed at a screen or an address. */\nfunction isLink(node: AiDoctrineNode): boolean {\n if (node.componentId === 'muiScreenLink') return true\n return (\n node.componentId === 'muiButton' &&\n (typeof node.props?.['screenId'] === 'string' || typeof node.props?.['href'] === 'string')\n )\n}\n\n/**\n * The layout door's checks on a tree the doctrine admitted: every component\n * the confirmed plan reuses is placed, every region the confirmed plan gives\n * the layout is built (AGL-3024), and navigation the site already keeps as a\n * component is placed rather than drawn again in the header.\n */\nexport function aiLayoutReuseCheck(\n inventory: AiSiteInventory | null,\n plan: AiJobPlan | null,\n): (tree: AiValidatedTree) => AiDoctrineViolation[] {\n const components = inventory?.components ?? []\n const navigation = components.filter(\n (component) => NAVIGATION_NAME.test(component.name) && !FOOTER_NAME.test(component.name),\n )\n return (tree) => {\n const placed = aiPlacedComponentIds(tree)\n const violations: AiDoctrineViolation[] = [\n ...aiPlanReuseViolations(inventory, plan, placed, 'layout'),\n ...aiPlanRegionViolations(plan, {\n rootId: tree.rootId,\n nodes: tree.nodes as unknown as Record<string, AiDoctrineNode>,\n }),\n ]\n if (navigation.length && !navigation.some((component) => placed.has(component.id))) {\n const visits = walkTree({\n rootId: tree.rootId,\n nodes: tree.nodes as unknown as Record<string, AiDoctrineNode>,\n })\n const slot = visits.findIndex(({ node }) => node.componentId === 'layoutSlot')\n const header = slot === -1 ? visits : visits.slice(0, slot)\n const links = header.filter(({ node }) => isLink(node)).map(({ id }) => id)\n if (links.length >= 2) {\n violations.push({\n rule: 1,\n code: 'navigation-rebuilt',\n message: `The site already has the \"${navigation[0].name}\" component. Place it in the header instead of building the navigation again.`,\n nodeIds: aiModelNodeIds(links, tree.sourceIds),\n })\n }\n }\n return violations\n }\n}\n\n/**\n * The regions the confirmed plan gives the layout, against the layout a COPY\n * actually produced (AGL-3024); `null` when the copy keeps the plan, or when\n * the plan promised no region to keep.\n *\n * The duplicate branch generates nothing, so it is the one place a plan's\n * promise can go unmet without a model ever answering for it. A copy that\n * cannot be read — deleted between the write and this read, or stored under a\n * root this check does not know — settles nothing, and a promise this step\n * cannot settle is reviewed by a person rather than reported as kept.\n */\nexport async function aiCopiedLayoutReview(\n firestore: FirebaseFirestore.Firestore,\n input: { hostId: string; id: string; name: string; plan: AiJobPlan | null },\n): Promise<AiJobReview | null> {\n const planned = aiPlannedLayoutRegions(input.plan)\n if (!planned.regions.length && !planned.unreadable.length) return null\n const stored = await readAiDraftNodes(firestore, {\n kind: 'layout',\n hostId: input.hostId,\n id: input.id,\n })\n const nodes = (stored?.nodes ?? {}) as unknown as Record<string, AiDoctrineNode>\n if (!nodes[CANVAS_ROOT_ELEMENT_ID]) {\n return {\n reason: 'doctrine',\n message: `\"${input.name}\" was copied from the layout the plan names, and this job could not read the copy back to check it has the regions the plan gives it. Open the layout and check it before you put a page in it.`,\n findings: [],\n }\n }\n const violations = aiPlanRegionViolations(input.plan, { rootId: CANVAS_ROOT_ELEMENT_ID, nodes })\n if (!violations.length) return null\n return aiDoctrineReview({\n message: `\"${input.name}\" is a copy of the layout the plan names, and a copy is all it is: ${violations[0].message}`,\n violations,\n })\n}\n\n/** A layout job is admitted for a site of the job's own org with a shared layout to spare. */\nexport const aiLayoutJobAdmission: AiJobAdmission = (context) =>\n aiDraftAdmissionRefusal(context.firestore, {\n orgId: context.orgId,\n hostId: context.hostId,\n kind: 'layout',\n org: context.org,\n })\n\nexport interface AiJobLayoutStepDeps {\n /** The inventory reader; specs hand in a fake. */\n readInventory?: typeof readSiteInventory\n /** The platform's duplicate module, for a plan that starts from a copy. */\n duplicate?: typeof duplicateResource\n}\n\nexport function createAiJobLayoutStep(deps: AiJobLayoutStepDeps = {}): AiJobStepRunner {\n const readInventory = deps.readInventory ?? readSiteInventory\n const duplicate = deps.duplicate ?? duplicateResource\n return async ({ job, now, signal, firestore, modelFor }) => {\n const hostId = job.hostId\n if (!hostId) throw new Error('a layout job names no site, and its admission refuses one')\n const output = (draft: AiDraftRecord, load?: AiLoadEstimate | null, note?: string | null): AiJobOutput => ({\n resource: 'layout',\n id: draft.id,\n versionId: draft.versionId,\n hostId,\n hostSubdomain: draft.hostSubdomain,\n label: draft.name,\n ...(load ? { load } : {}),\n ...(note ? { note } : {}),\n })\n // The switch's answer for this job, else the routing table's (AGL-2942).\n const model = modelFor?.('job.layout') ?? aiModelForStep('job.layout')\n\n // A run cut off after its draft was written reports that draft, found by the id the job recorded.\n const draftId = aiJobDraftId(job, 'layout')\n const written = await readAiDraft(firestore, { kind: 'layout', hostId, id: draftId })\n if (written) return aiUnspentOutcome(model, { outputs: [output(written)] })\n\n const [inventory, orgSnapshot] = await Promise.all([\n readInventory(job.orgId, hostId, { firestore }),\n firestore.collection('orgs').doc(job.orgId).get(),\n ])\n const org = (orgSnapshot.data() ?? null) as Partial<AglynOrgBilling> | null\n const plan = aiConfirmedPlan(job)\n const creation = aiPlanCreation(plan, 'layout')\n const name = creation?.name || AI_JOB_LAYOUT_DEFAULT_NAME\n\n // The plan starts from a copy of a layout the site has (rule 15): the copy\n // is the draft, and nothing is generated. A source gone since the plan\n // was made is built from the brief instead.\n if (creation?.duplicateOf && inventory.layouts.some((layout) => layout.id === creation.duplicateOf)) {\n const copy = await duplicate('layout', {\n orgId: job.orgId,\n hostId,\n sourceId: creation.duplicateOf,\n name,\n uid: job.createdBy,\n org: org as Record<string, unknown> | null,\n // The same claim the page step makes (AGL-3024): a copy core mints is\n // written under core's own id, never `draftId`, so a step that runs\n // again — a pass resumed, a run cut off after the copy — would mint a\n // second. The job's recorded id makes core replay the one it made.\n attemptKey: draftId,\n })\n if (copy.ok) {\n // A copy is not a build (AGL-3024). This branch generates nothing, so\n // whatever the plan promised BEYOND the source is not in the draft\n // unless the source already had it: a plan reading \"from a copy of the\n // base layout, adding a sidebar\" gets the base layout, the sidebar\n // nowhere in it, and until now reported Done. The copy's own regions\n // answer the plan's, and a copy that cannot be read answers nothing —\n // which is a stop for a person, never a Done.\n const review = await aiCopiedLayoutReview(firestore, { hostId, id: copy.id, plan, name })\n if (review) return aiUnspentOutcome(model, { review })\n const hostSubdomain = await aiSiteSubdomain(firestore, hostId)\n return aiUnspentOutcome(model, {\n outputs: [output({ id: copy.id, versionId: copy.versionId, name: copy.name, hostSubdomain })],\n })\n }\n if (copy.ok === false && copy.status === 403) {\n return aiUnspentOutcome(model, { review: aiLimitReview(copy.error) })\n }\n }\n\n const allowance = await aiDraftAllowanceRefusal(firestore, { kind: 'layout', hostId, org })\n if (allowance) return aiUnspentOutcome(model, { review: aiLimitReview(allowance) })\n\n const result = await runValidatedGeneration('layout', {\n step: 'job.layout',\n model,\n instructions: AI_JOB_LAYOUT_INSTRUCTIONS,\n inventory,\n messages: [{ role: 'user', content: aiJobLayoutPrompt(job, plan, name) }],\n tool: aiDoctrineTreeTool('layout'),\n maxTokens: AI_JOB_LAYOUT_STEP_BUDGET.maxTokens(model),\n ...(AI_ROUTING_TABLE['job.layout'].thinking ? { thinking: AI_ROUTING_TABLE['job.layout'].thinking } : {}),\n ...(AI_ROUTING_TABLE['job.layout'].effort ? { effort: AI_ROUTING_TABLE['job.layout'].effort } : {}),\n extend: aiLayoutReuseCheck(inventory, plan),\n ...(signal ? { signal } : {}),\n })\n const spent = aiGenerationSpent(result)\n if (result.status === 'refused') return { ...spent, refused: true }\n if (result.status === 'needs_input') return { ...spent, review: aiDoctrineReview(result) }\n\n const draft = await writeAiDraft(firestore, {\n kind: 'layout',\n hostId,\n id: draftId,\n uid: job.createdBy,\n org,\n name,\n nodes: result.value.nodes,\n now,\n })\n if (draft.ok === false) {\n if (draft.status === 404) throw new Error(`site ${hostId} vanished while its layout was generated`)\n return { ...spent, review: aiLimitReview(draft.error) }\n }\n // The facts the brief did not give, which the member fills before publishing (AGL-3056).\n const note = aiBracketedFactsNote({\n tree: { rootId: result.value.rootId, nodes: result.value.nodes as unknown as AiDoctrineTree['nodes'] },\n inventory,\n })\n return { ...spent, outputs: [output(draft, result.value.load, note)] }\n }\n}\n\nexport const runAiJobLayoutStep = createAiJobLayoutStep()\n\n/** Registers the layout step and the check a layout job passes before it is created or resumed. */\nexport function registerAiLayoutJob(): void {\n registerAiJobStep('layout', runAiJobLayoutStep, { minimumMs: AI_JOB_LAYOUT_STEP_MINIMUM_MS })\n registerAiJobAdmission('layout', aiLayoutJobAdmission)\n}\n"],"names":["CANVAS_ROOT_ELEMENT_ID","duplicateResource","AI_STEP_TIERS","AI_ROUTING_TABLE","aiModelForStep","aiDoctrineTreeTool","runValidatedGeneration","walkTree","readSiteInventory","registerAiJobAdmission","aiJobDraftId","aiDraftAdmissionRefusal","aiDraftAllowanceRefusal","aiSiteSubdomain","readAiDraft","readAiDraftNodes","writeAiDraft","aiBracketedFactsNote","aiConfirmedPlan","aiDoctrineReview","aiGenerationSpent","aiJobBriefLine","aiLimitReview","aiModelNodeIds","aiPlacedComponentIds","aiPlanCreation","aiPlanReferenceLines","aiPlanReuseViolations","aiUnspentOutcome","aiPlanRegionViolations","aiPlannedLayoutRegions","aiJobStepBudget","registerAiJobStep","AI_JOB_LAYOUT_MAX_TOKENS","maxTokens","AI_JOB_LAYOUT_STEP_BUDGET","tier","AI_JOB_LAYOUT_STEP_MINIMUM_MS","minimumMs","AI_JOB_LAYOUT_DEFAULT_NAME","AI_JOB_LAYOUT_INSTRUCTIONS","text","join","aiJobLayoutPrompt","job","plan","name","NAVIGATION_NAME","FOOTER_NAME","isLink","node","componentId","props","aiLayoutReuseCheck","inventory","components","navigation","filter","component","test","tree","placed","violations","rootId","nodes","length","some","has","id","visits","slot","findIndex","header","slice","links","map","push","rule","code","message","nodeIds","sourceIds","aiCopiedLayoutReview","firestore","input","planned","regions","unreadable","stored","kind","hostId","reason","findings","aiLayoutJobAdmission","context","orgId","org","createAiJobLayoutStep","deps","readInventory","duplicate","now","signal","modelFor","orgSnapshot","Error","output","draft","load","note","resource","versionId","hostSubdomain","label","model","draftId","written","outputs","Promise","all","collection","doc","get","data","creation","duplicateOf","layouts","layout","copy","sourceId","uid","createdBy","attemptKey","ok","review","status","error","allowance","result","step","instructions","messages","role","content","tool","thinking","effort","extend","spent","refused","value","runAiJobLayoutStep","registerAiLayoutJob"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-layout-step.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { CANVAS_ROOT_ELEMENT_ID } from '@aglyn/aglyn/foundation/constants/canvas'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport { duplicateResource } from '@aglyn/tenant-data-admin/server/duplicate-resource'\nimport type { AiJob, AiJobOutput, AiJobPlan, AiJobReview } from '../model/ai-jobs.types'\nimport type { AiSiteInventory } from '../model/ai-site-inventory'\nimport { AI_STEP_TIERS } from '../providers/catalog'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport {\n aiDoctrineTreeTool,\n runValidatedGeneration,\n type AiValidatedTree,\n} from '../runtime/ai-doctrine'\nimport {\n walkTree,\n type AiDoctrineNode,\n type AiDoctrineTree,\n type AiDoctrineViolation,\n} from '../runtime/ai-doctrine-validators'\nimport type { AiLoadEstimate } from '../runtime/ai-palette'\nimport type { AiSystemBlock } from '../runtime/ai-runtime'\nimport { readSiteInventory } from '../runtime/site-inventory'\nimport { registerAiJobAdmission, type AiJobAdmission } from './ai-job-admission'\nimport { aiJobDraftId } from './ai-job-draft-ids'\nimport {\n aiDraftAdmissionRefusal,\n aiDraftAllowanceRefusal,\n aiSiteSubdomain,\n readAiDraft,\n readAiDraftNodes,\n writeAiDraft,\n type AiDraftRecord,\n} from './ai-job-drafts'\nimport {\n aiBracketedFactsNote,\n aiConfirmedPlan,\n aiDoctrineReview,\n aiGenerationSpent,\n aiJobBriefLine,\n aiLimitReview,\n aiModelNodeIds,\n aiPlacedComponentIds,\n aiPlanCreation,\n aiPlanReferenceLines,\n aiPlanReuseViolations,\n aiUnspentOutcome,\n} from './ai-job-generation'\nimport { aiPlanRegionViolations, aiPlannedLayoutRegions } from './ai-job-plan-conformance'\nimport type { AiJobStepRunner } from './ai-job-text-step'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { registerAiJobStep } from './ai-jobs'\n\n/**\n * The layout step (AGL-2909): the generation step of a `layout` job, run once\n * a member confirmed the job's plan.\n *\n * It builds ONE shared layout — a header with the site's navigation, the\n * Layout Slot each page renders in, a footer — through\n * `runValidatedGeneration('layout', …)`, so every building rule holds on the\n * tree before it is kept, and adds three checks of its own through `extend`: a\n * component the confirmed plan reuses is placed (rule 7), every region the\n * plan gives the layout is built (AGL-3024), and a navigation or menu\n * component the site already has is placed rather than drawn again (rule 1).\n *\n * The layout lands as a new draft (`ai-job-drafts.ts`): no screen uses it and\n * no layout nests it, so nothing on the live site changes until a member\n * assigns it. A plan that starts from a copy of a layout the site has gets\n * that copy, through the platform's duplicate module, and nothing generated —\n * so the copy is read back and held to the plan's regions the same way\n * (`aiCopiedLayoutReview`), because a branch that generates nothing is the one\n * branch where a promise can go unmet with no model to answer for it.\n *\n * A generation step runs on the job beat, never at an inline door: it\n * registers the least time its lookup rounds, its answer and its re-ask need\n * (AGL-3035), runs without extended thinking — the plan was reasoned out and\n * confirmed before it — and asks for no more of a tighter ceiling than the\n * doctrine's default for a layout than fits that time on the model it runs.\n */\n\n/** The longest answer a layout may run to; the tree is held to the layout budget either way. */\nexport const AI_JOB_LAYOUT_MAX_TOKENS = AI_ROUTING_TABLE['job.layout'].maxTokens\n\n/**\n * The layout step's time (AGL-3035, AGL-3036): its two inventory-lookup\n * rounds, its answer and its re-ask at the routing table's ceiling on the tier\n * `job.layout` is served from, fitted to what a beat can give a step.\n */\nexport const AI_JOB_LAYOUT_STEP_BUDGET = aiJobStepBudget({\n tier: AI_STEP_TIERS['job.layout'],\n maxTokens: AI_JOB_LAYOUT_MAX_TOKENS,\n})\n\n/** The least time one layout step needs before it starts. */\nexport const AI_JOB_LAYOUT_STEP_MINIMUM_MS = AI_JOB_LAYOUT_STEP_BUDGET.minimumMs\n\n/** A layout's name when the plan names none. */\nexport const AI_JOB_LAYOUT_DEFAULT_NAME = 'Site layout'\n\n/** The layout step's own instructions, cached after the doctrine. */\nexport const AI_JOB_LAYOUT_INSTRUCTIONS: readonly AiSystemBlock[] = [\n {\n text: [\n 'You build one shared layout for a website: the frame every page of the site renders inside. Answer with submit_layout: the whole layout as one flat node map.',\n 'A layout is a header, one Layout Slot and a footer, top to bottom. The header is an App Bar (muiAppBar) holding a Toolbar Content (muiToolbar) with the site’s name as text and its navigation. The footer is a Section (section) whose element is footer.',\n 'Navigation links the site’s own pages by id: a Page Link (muiScreenLink) whose screenId is a page id from the site inventory, in the order the brief and the confirmed plan give, seven at most in the header. Never invent a page id, and never link a page by its address.',\n 'When the site inventory lists a component whose name says it is navigation, a menu or a mega menu, place it in the header as an instance instead of building the navigation again. Place every component the confirmed plan reuses.',\n 'The Layout Slot (layoutSlot) is where each page renders and is the page’s main landmark: leave its component unset or main, and put nothing inside it. A layout carries no h1, because every page brings its own.',\n ].join('\\n'),\n },\n]\n\n/** The layout job as the generation's user turn: the name, the brief, the confirmed plan. */\nexport function aiJobLayoutPrompt(\n job: Pick<AiJob, 'brief'>,\n plan: AiJobPlan | null,\n name: string,\n): string {\n return [`Layout name: ${name}`, aiJobBriefLine(job), ...aiPlanReferenceLines(plan)].join('\\n')\n}\n\n/** A component name that says it is the site's navigation; a footer's links are not. */\nconst NAVIGATION_NAME = /\\b(?:nav|navigation|navbar|menu|mega ?menu)\\b/i\nconst FOOTER_NAME = /\\bfooter\\b/i\n\n/** A node that links somewhere: a Screen Link, or a Button pointed at a screen or an address. */\nfunction isLink(node: AiDoctrineNode): boolean {\n if (node.componentId === 'muiScreenLink') return true\n return (\n node.componentId === 'muiButton' &&\n (typeof node.props?.['screenId'] === 'string' || typeof node.props?.['href'] === 'string')\n )\n}\n\n/**\n * The layout door's checks on a tree the doctrine admitted: every component\n * the confirmed plan reuses is placed, every region the confirmed plan gives\n * the layout is built (AGL-3024), and navigation the site already keeps as a\n * component is placed rather than drawn again in the header.\n */\nexport function aiLayoutReuseCheck(\n inventory: AiSiteInventory | null,\n plan: AiJobPlan | null,\n): (tree: AiValidatedTree) => AiDoctrineViolation[] {\n const components = inventory?.components ?? []\n const navigation = components.filter(\n (component) => NAVIGATION_NAME.test(component.name) && !FOOTER_NAME.test(component.name),\n )\n return (tree) => {\n const placed = aiPlacedComponentIds(tree)\n const violations: AiDoctrineViolation[] = [\n ...aiPlanReuseViolations(inventory, plan, placed, 'layout'),\n ...aiPlanRegionViolations(plan, {\n rootId: tree.rootId,\n nodes: tree.nodes as unknown as Record<string, AiDoctrineNode>,\n }),\n ]\n if (navigation.length && !navigation.some((component) => placed.has(component.id))) {\n const visits = walkTree({\n rootId: tree.rootId,\n nodes: tree.nodes as unknown as Record<string, AiDoctrineNode>,\n })\n const slot = visits.findIndex(({ node }) => node.componentId === 'layoutSlot')\n const header = slot === -1 ? visits : visits.slice(0, slot)\n const links = header.filter(({ node }) => isLink(node)).map(({ id }) => id)\n if (links.length >= 2) {\n violations.push({\n rule: 1,\n code: 'navigation-rebuilt',\n message: `The site already has the \"${navigation[0].name}\" component. Place it in the header instead of building the navigation again.`,\n nodeIds: aiModelNodeIds(links, tree.sourceIds),\n })\n }\n }\n return violations\n }\n}\n\n/**\n * The regions the confirmed plan gives the layout, against the layout a COPY\n * actually produced (AGL-3024); `null` when the copy keeps the plan, or when\n * the plan promised no region to keep.\n *\n * The duplicate branch generates nothing, so it is the one place a plan's\n * promise can go unmet without a model ever answering for it. A copy that\n * cannot be read — deleted between the write and this read, or stored under a\n * root this check does not know — settles nothing, and a promise this step\n * cannot settle is reviewed by a person rather than reported as kept.\n */\nexport async function aiCopiedLayoutReview(\n firestore: FirebaseFirestore.Firestore,\n input: { hostId: string; id: string; name: string; plan: AiJobPlan | null },\n): Promise<AiJobReview | null> {\n const planned = aiPlannedLayoutRegions(input.plan)\n if (!planned.regions.length && !planned.unreadable.length) return null\n const stored = await readAiDraftNodes(firestore, {\n kind: 'layout',\n hostId: input.hostId,\n id: input.id,\n })\n const nodes = (stored?.nodes ?? {}) as unknown as Record<string, AiDoctrineNode>\n if (!nodes[CANVAS_ROOT_ELEMENT_ID]) {\n return {\n reason: 'doctrine',\n message: `\"${input.name}\" was copied from the layout the plan names, and this job could not read the copy back to check it has the regions the plan gives it. Open the layout and check it before you put a page in it.`,\n findings: [],\n }\n }\n const violations = aiPlanRegionViolations(input.plan, { rootId: CANVAS_ROOT_ELEMENT_ID, nodes })\n if (!violations.length) return null\n return aiDoctrineReview({\n message: `\"${input.name}\" is a copy of the layout the plan names, and a copy is all it is: ${violations[0].message}`,\n violations,\n })\n}\n\n/** A layout job is admitted for a site of the job's own org with a shared layout to spare. */\nexport const aiLayoutJobAdmission: AiJobAdmission = (context) =>\n aiDraftAdmissionRefusal(context.firestore, {\n orgId: context.orgId,\n hostId: context.hostId,\n kind: 'layout',\n org: context.org,\n })\n\nexport interface AiJobLayoutStepDeps {\n /** The inventory reader; specs hand in a fake. */\n readInventory?: typeof readSiteInventory\n /** The platform's duplicate module, for a plan that starts from a copy. */\n duplicate?: typeof duplicateResource\n}\n\nexport function createAiJobLayoutStep(deps: AiJobLayoutStepDeps = {}): AiJobStepRunner {\n const readInventory = deps.readInventory ?? readSiteInventory\n const duplicate = deps.duplicate ?? duplicateResource\n return async ({ job, now, signal, firestore, modelFor }) => {\n const hostId = job.hostId\n if (!hostId) throw new Error('a layout job names no site, and its admission refuses one')\n const output = (draft: AiDraftRecord, load?: AiLoadEstimate | null, note?: string | null): AiJobOutput => ({\n resource: 'layout',\n id: draft.id,\n versionId: draft.versionId,\n hostId,\n hostSubdomain: draft.hostSubdomain,\n label: draft.name,\n ...(load ? { load } : {}),\n ...(note ? { note } : {}),\n })\n // The switch's answer for this job, else the routing table's (AGL-2942).\n const model = modelFor?.('job.layout') ?? aiModelForStep('job.layout')\n\n // A run cut off after its draft was written reports that draft, found by the id the job recorded.\n const draftId = aiJobDraftId(job, 'layout')\n const written = await readAiDraft(firestore, { kind: 'layout', hostId, id: draftId })\n if (written) return aiUnspentOutcome(model, { outputs: [output(written)] })\n\n const [inventory, orgSnapshot] = await Promise.all([\n readInventory(job.orgId, hostId, { firestore }),\n firestore.collection('orgs').doc(job.orgId).get(),\n ])\n const org = (orgSnapshot.data() ?? null) as Partial<AglynOrgBilling> | null\n const plan = aiConfirmedPlan(job)\n const creation = aiPlanCreation(plan, 'layout')\n const name = creation?.name || AI_JOB_LAYOUT_DEFAULT_NAME\n\n // The plan starts from a copy of a layout the site has (rule 15): the copy\n // is the draft, and nothing is generated. A source gone since the plan\n // was made is built from the brief instead.\n if (creation?.duplicateOf && inventory.layouts.some((layout) => layout.id === creation.duplicateOf)) {\n const copy = await duplicate('layout', {\n orgId: job.orgId,\n hostId,\n sourceId: creation.duplicateOf,\n name,\n uid: job.createdBy,\n org: org as Record<string, unknown> | null,\n // The same claim the page step makes (AGL-3024): a copy core mints is\n // written under core's own id, never `draftId`, so a step that runs\n // again — a pass resumed, a run cut off after the copy — would mint a\n // second. The job's recorded id makes core replay the one it made.\n attemptKey: draftId,\n })\n if (copy.ok) {\n // A copy is not a build (AGL-3024). This branch generates nothing, so\n // whatever the plan promised BEYOND the source is not in the draft\n // unless the source already had it: a plan reading \"from a copy of the\n // base layout, adding a sidebar\" gets the base layout, the sidebar\n // nowhere in it, and until now reported Done. The copy's own regions\n // answer the plan's, and a copy that cannot be read answers nothing —\n // which is a stop for a person, never a Done.\n const review = await aiCopiedLayoutReview(firestore, { hostId, id: copy.id, plan, name })\n if (review) return aiUnspentOutcome(model, { review })\n const hostSubdomain = await aiSiteSubdomain(firestore, hostId)\n return aiUnspentOutcome(model, {\n outputs: [output({ id: copy.id, versionId: copy.versionId, name: copy.name, hostSubdomain })],\n })\n }\n if (copy.ok === false && copy.status === 403) {\n return aiUnspentOutcome(model, { review: aiLimitReview(copy.error) })\n }\n }\n\n const allowance = await aiDraftAllowanceRefusal(firestore, { kind: 'layout', hostId, org })\n if (allowance) return aiUnspentOutcome(model, { review: aiLimitReview(allowance) })\n\n const result = await runValidatedGeneration('layout', {\n step: 'job.layout',\n model,\n instructions: AI_JOB_LAYOUT_INSTRUCTIONS,\n inventory,\n messages: [{ role: 'user', content: aiJobLayoutPrompt(job, plan, name) }],\n tool: aiDoctrineTreeTool('layout'),\n maxTokens: AI_JOB_LAYOUT_STEP_BUDGET.maxTokens(model),\n ...(AI_ROUTING_TABLE['job.layout'].thinking ? { thinking: AI_ROUTING_TABLE['job.layout'].thinking } : {}),\n ...(AI_ROUTING_TABLE['job.layout'].effort ? { effort: AI_ROUTING_TABLE['job.layout'].effort } : {}),\n extend: aiLayoutReuseCheck(inventory, plan),\n ...(signal ? { signal } : {}),\n })\n const spent = aiGenerationSpent(result)\n if (result.status === 'refused') return { ...spent, refused: true }\n if (result.status === 'needs_input') return { ...spent, review: aiDoctrineReview(result) }\n\n const draft = await writeAiDraft(firestore, {\n kind: 'layout',\n hostId,\n id: draftId,\n uid: job.createdBy,\n org,\n name,\n nodes: result.value.nodes,\n now,\n })\n if (draft.ok === false) {\n if (draft.status === 404) throw new Error(`site ${hostId} vanished while its layout was generated`)\n return { ...spent, review: aiLimitReview(draft.error) }\n }\n // The facts the brief did not give, which the member fills before publishing (AGL-3056).\n const note = aiBracketedFactsNote({\n tree: { rootId: result.value.rootId, nodes: result.value.nodes as unknown as AiDoctrineTree['nodes'] },\n inventory,\n })\n return { ...spent, outputs: [output(draft, result.value.load, note)] }\n }\n}\n\nexport const runAiJobLayoutStep = createAiJobLayoutStep()\n\n/** Registers the layout step and the check a layout job passes before it is created or resumed. */\nexport function registerAiLayoutJob(): void {\n registerAiJobStep('layout', runAiJobLayoutStep, { minimumMs: AI_JOB_LAYOUT_STEP_MINIMUM_MS })\n registerAiJobAdmission('layout', aiLayoutJobAdmission)\n}\n"],"names":["CANVAS_ROOT_ELEMENT_ID","duplicateResource","AI_STEP_TIERS","AI_ROUTING_TABLE","aiModelForStep","aiDoctrineTreeTool","runValidatedGeneration","walkTree","readSiteInventory","registerAiJobAdmission","aiJobDraftId","aiDraftAdmissionRefusal","aiDraftAllowanceRefusal","aiSiteSubdomain","readAiDraft","readAiDraftNodes","writeAiDraft","aiBracketedFactsNote","aiConfirmedPlan","aiDoctrineReview","aiGenerationSpent","aiJobBriefLine","aiLimitReview","aiModelNodeIds","aiPlacedComponentIds","aiPlanCreation","aiPlanReferenceLines","aiPlanReuseViolations","aiUnspentOutcome","aiPlanRegionViolations","aiPlannedLayoutRegions","aiJobStepBudget","registerAiJobStep","AI_JOB_LAYOUT_MAX_TOKENS","maxTokens","AI_JOB_LAYOUT_STEP_BUDGET","tier","AI_JOB_LAYOUT_STEP_MINIMUM_MS","minimumMs","AI_JOB_LAYOUT_DEFAULT_NAME","AI_JOB_LAYOUT_INSTRUCTIONS","text","join","aiJobLayoutPrompt","job","plan","name","NAVIGATION_NAME","FOOTER_NAME","isLink","node","componentId","props","aiLayoutReuseCheck","inventory","components","navigation","filter","component","test","tree","placed","violations","rootId","nodes","length","some","has","id","visits","slot","findIndex","header","slice","links","map","push","rule","code","message","nodeIds","sourceIds","aiCopiedLayoutReview","firestore","input","planned","regions","unreadable","stored","kind","hostId","reason","findings","aiLayoutJobAdmission","context","orgId","org","createAiJobLayoutStep","deps","readInventory","duplicate","now","signal","modelFor","orgSnapshot","Error","output","draft","load","note","resource","versionId","hostSubdomain","label","model","draftId","written","outputs","Promise","all","collection","doc","get","data","creation","duplicateOf","layouts","layout","copy","sourceId","uid","createdBy","attemptKey","ok","review","status","error","allowance","result","step","instructions","messages","role","content","tool","thinking","effort","extend","spent","refused","value","runAiJobLayoutStep","registerAiLayoutJob"],"mappings":"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"}
@@ -166,12 +166,14 @@ export declare function aiPageWithSection(page: NodesMap, section: AiPageSection
166
166
  /**
167
167
  * What a tree generated against this inventory may reference, the brand it is
168
168
  * held to, where the workspace keeps no reusable components or saved forms
169
- * (AGL-3030) that the page is built inline, and the sections its plan names,
170
- * which a link may take a visitor to (AGL-3097).
169
+ * (AGL-3030) that the page is built inline, the sections its plan names,
170
+ * which a link may take a visitor to (AGL-3097), and the players its plan
171
+ * lists for this page, the only ones it may embed (AGL-3433).
171
172
  */
172
173
  export declare function aiPageCheckContext(inventory: AiSiteInventory | null, options?: {
173
174
  reusableComponents?: boolean;
174
175
  sections?: readonly string[];
176
+ embeds?: AiDoctrineTreeContext['plannedEmbeds'];
175
177
  }): AiDoctrineTreeContext;
176
178
  export interface AiPageSectionCheckInput {
177
179
  /** The page as stored so far; an empty page on the first pass. */
@@ -82,7 +82,7 @@ import { aiPlanItemCountViolations } from "./ai-job-plan-conformance.js";
82
82
  text: [
83
83
  'You build a web page one section at a time, in the order a confirmed plan gives. Each answer is ONE section: call submit_section with a flat node map whose root is the document wrapper (componentId "div") holding exactly one Section (section).',
84
84
  'The first section is the top of the page and holds the page’s one h1, its title. Every later section opens with an h2, and headings below it step down one level at a time.',
85
- 'Place what the section’s plan line names. A component the site has is placed as an instance, never drawn again: componentId "reusableInstance", props {"refId": "<component id>", "propValues": {…}} filling the props the inventory lists. A form the site has is a Form (form) whose formId is that form’s id. Another screen of the site is linked by its screen id, with a Screen Link (muiScreenLink) or a Button (muiButton) whose screenId is that id.',
85
+ 'Place what the section’s plan line names. A component the site has is placed as an instance, never drawn again: componentId "reusableInstance", props {"refId": "<component id>", "propValues": {…}} filling the props the inventory lists. A form the site has is a Form (form) whose formId is that form’s id. Another page of the site is linked by its page id, with a Page Link (muiScreenLink) or a Button (muiButton) whose screenId is that id.',
86
86
  'Where the request says the site keeps no saved forms or reusable components, draw them in the section instead: a repeated item is written once, and a form is a Form (form) with no formId, a formName saying what it collects and a submitLabel, holding one Form Field (formField) for each answer with its fieldName, label and fieldType.',
87
87
  'Lay the section out with a Container, a Stack or a Grid ("container": true, "spacing": 3) of Grid items sized like "xs:12 md:4", and size nothing with a fixed width. Colors come from the theme’s palette tokens such as primary.main, spacing from the spacing scale, and type from the text variants.',
88
88
  'The page renders inside the site’s layout, so a section is never a header, navigation or footer.',
@@ -256,10 +256,11 @@ function isRecord(value) {
256
256
  /**
257
257
  * What a tree generated against this inventory may reference, the brand it is
258
258
  * held to, where the workspace keeps no reusable components or saved forms
259
- * (AGL-3030) that the page is built inline, and the sections its plan names,
260
- * which a link may take a visitor to (AGL-3097).
259
+ * (AGL-3030) that the page is built inline, the sections its plan names,
260
+ * which a link may take a visitor to (AGL-3097), and the players its plan
261
+ * lists for this page, the only ones it may embed (AGL-3433).
261
262
  */ export function aiPageCheckContext(inventory, options = {}) {
262
- var _options_sections;
263
+ var _options_sections, _options_embeds;
263
264
  return _extends({
264
265
  brand: (inventory == null ? void 0 : inventory.theme) ? {
265
266
  colors: inventory.theme.colors,
@@ -271,6 +272,8 @@ function isRecord(value) {
271
272
  reusableComponents: false
272
273
  } : {}, ((_options_sections = options.sections) == null ? void 0 : _options_sections.length) ? {
273
274
  pageSections: options.sections
275
+ } : {}, ((_options_embeds = options.embeds) == null ? void 0 : _options_embeds.length) ? {
276
+ plannedEmbeds: options.embeds
274
277
  } : {});
275
278
  }
276
279
  /** The parts of an answer the violations name, as the model wrote them, capped as the doctrine caps a re-ask. */ function offendingOf(raw, violations) {