@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-plan-conformance.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 type { AiBuildPlanScreen, AiBuildPlanSection } from '../model/ai-build-plan'\nimport type { AiJobPlan } from '../model/ai-jobs.types'\nimport {\n AI_LAYOUT_REGION_NAMES,\n aiBindsRepeatedItems,\n aiLayoutRegionOf,\n aiRepeatedItemCount,\n aiTreeLayoutRegions,\n walkTree,\n type AiDoctrineNode,\n type AiDoctrineTree,\n type AiDoctrineViolation,\n type AiLayoutRegion,\n} from '../runtime/ai-doctrine-validators'\nimport { aiBindingTokensIn, type AiTemplateSubjectDefinition } from '../model/ai-template-subjects'\nimport { aiPlanCreation } from './ai-job-generation'\n\n/**\n * The build held to the plan the member confirmed (AGL-3024).\n *\n * The doctrine validators ask whether an artifact was built the way a careful\n * author builds one. They do not ask whether it is the artifact the plan\n * PROMISED, and a build that quietly delivers less still reports `Done` —\n * which is the dangerous part, because `Done` is what tells the customer they\n * have no reason to re-read it. Measured on beta.139: a plan promising four\n * practice areas built three cards; a plan promising a layout with a sidebar\n * built a verbatim copy of the base layout, the word \"sidebar\" nowhere in it.\n *\n * ── Structured commitments, never prose ──────────────────────────────────\n *\n * A plan's rationale (`why`) is prose a model wrote, and matching English\n * against a node tree after the fact would be a second guess dressed as a\n * check. Only the plan's STRUCTURE is read here, and only the two shapes that\n * are already numbers or names in `ai-build-plan.ts`:\n *\n * - **A count**: `screens[].sections[].items`, which the plan already\n * carries and the section prompt already states as \"It shows N items\".\n * Nothing read it back.\n * - **A region**: `create[].fields` on a layout, which is that kind's own\n * field for the regions it has (`AI_LAYOUT_REGIONS`). A layout has no\n * properties, so the planner is asked there for the one thing about a\n * layout worth promising and worth checking — a checkable assertion the\n * planner emits beside its prose, rather than English parsed out of it.\n * - **A token**: `create[].fields` on a template, from the subject's own\n * closed catalog (`AI_TEMPLATE_SUBJECT_DEFINITIONS[subject].tokens`), which\n * is the template's analogue of a layout's regions — the one thing about a\n * template that differs per record, and therefore the one worth promising.\n *\n * The third was added after a measured miss (AGL-3024, 2026-09-21). A brief\n * asked for an attorney's bar admissions; the plan's `why` promised \"a\n * bar-admissions block\"; the built template had no such thing and reported\n * `Done`. Nothing had gone wrong in the build: `author` has six tokens —\n * name, bio, image, jobTitle, worksFor, url — and no catalog anywhere fills a\n * bar admission, so the promise was unkeepable when it was written. Read as a\n * token list the promise cannot even be MADE, which is a better place to stop\n * than after the document is paid for.\n *\n * ── It fails closed ──────────────────────────────────────────────────────\n *\n * Every function here answers \"the plan is kept\" only when it could look. A\n * layout the step never read is not a layout whose regions are present; the\n * layout door turns that into a stop for review rather than a `Done`. What it\n * will not do is guess: a count it cannot settle from the document — items a\n * collection fills at render — is not reported as short.\n *\n * The findings are ordinary `AiDoctrineViolation`s, so they ride the\n * mechanism every other rule rides: `runValidatedGeneration` re-asks the\n * model once with them, and a second answer that still breaks one stops the\n * job for a person with `aiDoctrineReview`. They carry `rule: null` on\n * purpose. The seventeen rules are how a document is BUILT; keeping the plan\n * is not one of them, and numbering these as rule 7 would print \"Rule 7\n * (Reuse before creating)\" over a finding about a count.\n */\n\n/** A count below this is not a repeat, so a plan promising one promises nothing to count. */\nexport const AI_PLAN_ITEMS_MIN = 2\n\n/**\n * The regions the confirmed plan's layout creation names, and the words it\n * named that no region answers to. A plan with no layout creation names none.\n */\nexport function aiPlannedLayoutRegions(plan: AiJobPlan | null): {\n regions: AiLayoutRegion[]\n unreadable: string[]\n} {\n const regions = new Set<AiLayoutRegion>()\n const unreadable: string[] = []\n for (const field of aiPlanCreation(plan, 'layout')?.fields ?? []) {\n const region = aiLayoutRegionOf(field)\n if (region) regions.add(region)\n else unreadable.push(field)\n }\n return { regions: [...regions], unreadable }\n}\n\n/**\n * The plan's promised regions against a built layout: one finding naming\n * every region the plan lists that the layout does not carry, and one naming\n * a word the vocabulary could not read, which is a promise this check cannot\n * settle either way.\n */\nexport function aiPlanRegionViolations(\n plan: AiJobPlan | null,\n tree: AiDoctrineTree,\n): AiDoctrineViolation[] {\n const { regions, unreadable } = aiPlannedLayoutRegions(plan)\n const built = aiTreeLayoutRegions(tree)\n const violations: AiDoctrineViolation[] = []\n const missing = regions.filter((region) => !built.has(region))\n if (missing.length) {\n const one = missing.length === 1\n violations.push({\n rule: null,\n code: 'plan-region-missing',\n message: `The confirmed plan gives this layout ${listed(\n missing,\n )}, and the layout has no ${one ? 'such region' : 'such regions'}. Build ${\n one ? 'it' : 'each of them'\n }: a sidebar is a Section whose element is aside, a header a Section whose element is header or an App Bar, a nav a Section whose element is nav, a footer a Section whose element is footer, and the main region the Layout Slot.`,\n })\n }\n if (unreadable.length) {\n violations.push({\n rule: null,\n code: 'plan-region-unreadable',\n message: `The confirmed plan gives this layout ${listed(\n unreadable,\n )}, which is no region this platform builds, so nothing can check the layout has ${\n unreadable.length === 1 ? 'it' : 'them'\n }. A layout's regions are ${AI_LAYOUT_REGION_NAMES.join(', ')}.`,\n })\n }\n return violations\n}\n\n/**\n * The plan's promised item count against a built section. Silent where there\n * is nothing to settle: a section promising fewer than `AI_PLAN_ITEMS_MIN`\n * promises no repeat, and one whose items a collection fills draws them from\n * data at render, where the document cannot be counted and rule 8 asked for\n * exactly that. Silent, too, when the section shows MORE than it promised:\n * the count is read generously (`aiRepeatedItemCount`), so a number above the\n * promise is as likely to be the reading as the section.\n */\nexport function aiPlanItemCountViolations(\n section: Pick<AiBuildPlanSection, 'name' | 'items'>,\n tree: AiDoctrineTree,\n): AiDoctrineViolation[] {\n if (section.items < AI_PLAN_ITEMS_MIN) return []\n if (aiBindsRepeatedItems(tree)) return []\n const shown = aiRepeatedItemCount(tree)\n if (shown >= section.items) return []\n return [\n {\n rule: null,\n code: 'plan-items-short',\n message: `The confirmed plan says the \"${section.name}\" section shows ${section.items} items, and this section shows ${shown}. Build all ${section.items}.`,\n },\n ]\n}\n\n/**\n * The counts the plan's sections promised against a page a COPY produced\n * (AGL-3024), read over the whole page because a copy has no section of the\n * plan's in it to read one at a time.\n *\n * A copied screen carries the SOURCE's nodes under the source's ids: nothing\n * maps \"the practice areas section\" onto a subtree of it, so the per-section\n * reading `aiPlanItemCountViolations` does at a generation pass has nothing\n * to stand on here. What can still be settled is the weaker claim, and it is\n * the one that catches the measured shape: a plan promising a section of four\n * against a copy in which NO group of four repeated things exists anywhere is\n * a promise the copy did not keep, whichever section was meant to keep it.\n *\n * Weaker on purpose, and so quieter: a copy holding six of something else\n * reads as six and is let through. A screen carries no app bar and no footer\n * — those are its layout's — so what is counted is the page's own content.\n */\nexport function aiPlanCopiedPageViolations(\n screen: Pick<AiBuildPlanScreen, 'sections'>,\n tree: AiDoctrineTree,\n): AiDoctrineViolation[] {\n const promised = screen.sections\n .filter((section) => section.items >= AI_PLAN_ITEMS_MIN)\n .sort((a, b) => b.items - a.items)[0]\n if (!promised) return []\n if (aiBindsRepeatedItems(tree)) return []\n const shown = aiRepeatedItemCount(tree)\n if (shown >= promised.items) return []\n return [\n {\n rule: null,\n code: 'plan-items-short',\n message: `The confirmed plan says the \"${promised.name}\" section shows ${promised.items} items, and this page is a copy that shows at most ${shown} of anything. Build the ${promised.items}.`,\n },\n ]\n}\n\n/** A token as the catalog spells it, for matching what a plan wrote against it. */\nfunction tokenKey(value: string): string {\n return value.replace(/[{}\\s]/g, '').toLowerCase()\n}\n\n/** Every binding token a built tree holds, spelled as the catalog spells them. */\nexport function aiTreeBoundTokens(tree: AiDoctrineTree): Set<string> {\n const bound = new Set<string>()\n const nodes = tree.nodes as unknown as Record<string, AiDoctrineNode>\n const strings = (value: unknown, out: string[]): string[] => {\n if (typeof value === 'string') out.push(value)\n else if (Array.isArray(value)) for (const inner of value) strings(inner, out)\n else if (value && typeof value === 'object') {\n for (const inner of Object.values(value as Record<string, unknown>)) strings(inner, out)\n }\n return out\n }\n for (const { node } of walkTree({ rootId: tree.rootId, nodes })) {\n for (const token of strings(node.props, []).flatMap(aiBindingTokensIn)) {\n bound.add(tokenKey(token))\n }\n }\n return bound\n}\n\n/**\n * The subject tokens the confirmed plan's template creation names, and the\n * words it named that the subject's catalog does not answer to. A plan with\n * no template creation names none.\n */\nexport function aiPlannedTemplateTokens(\n plan: AiJobPlan | null,\n definition: AiTemplateSubjectDefinition,\n): { tokens: string[]; unreadable: string[] } {\n const catalog = new Map(definition.tokens.map((entry) => [tokenKey(entry.token), entry.token]))\n const tokens = new Set<string>()\n const unreadable: string[] = []\n for (const field of aiPlanCreation(plan, 'template')?.fields ?? []) {\n const token = catalog.get(tokenKey(field))\n if (token) tokens.add(token)\n else unreadable.push(field)\n }\n return { tokens: [...tokens], unreadable }\n}\n\n/**\n * The plan's promised tokens against a built template: one finding naming\n * every token the plan lists that the template does not bind, and one naming\n * a word the subject's catalog could not read — which is a promise this check\n * cannot settle either way, and, more usefully, one the page could never have\n * filled.\n */\nexport function aiPlanTemplateTokenViolations(\n plan: AiJobPlan | null,\n definition: AiTemplateSubjectDefinition,\n tree: AiDoctrineTree,\n): AiDoctrineViolation[] {\n const { tokens, unreadable } = aiPlannedTemplateTokens(plan, definition)\n const violations: AiDoctrineViolation[] = []\n if (tokens.length) {\n const bound = aiTreeBoundTokens(tree)\n const missing = tokens.filter((token) => !bound.has(tokenKey(token)))\n if (missing.length) {\n const one = missing.length === 1\n violations.push({\n rule: null,\n code: 'plan-token-missing',\n message: `The confirmed plan says this template shows ${listed(missing)}, and it binds ${\n one ? 'no such token' : 'no such tokens'\n }. Put ${one ? 'it' : 'each of them'} on the page, so every ${\n definition.noun.replace(/^an? /, '')\n } shows their own.`,\n })\n }\n }\n if (unreadable.length) {\n violations.push({\n rule: null,\n code: 'plan-token-unreadable',\n message: `The confirmed plan says this template shows ${listed(\n unreadable,\n )}, which ${definition.noun}'s page does not fill, so nothing can check the template shows ${\n unreadable.length === 1 ? 'it' : 'them'\n }. Promise only what the page fills: ${definition.tokens\n .map((entry) => entry.token)\n .join(', ')}.`,\n })\n }\n return violations\n}\n\n/** `\"a\"`, `\"a\" and \"b\"`, `\"a\", \"b\" and \"c\"`. */\nfunction listed(values: readonly string[]): string {\n const quoted = values.map((value) => `\"${value}\"`)\n return quoted.length <= 1\n ? quoted.join('')\n : `${quoted.slice(0, -1).join(', ')} and ${quoted[quoted.length - 1]}`\n}\n"],"names":["AI_LAYOUT_REGION_NAMES","aiBindsRepeatedItems","aiLayoutRegionOf","aiRepeatedItemCount","aiTreeLayoutRegions","walkTree","aiBindingTokensIn","aiPlanCreation","AI_PLAN_ITEMS_MIN","aiPlannedLayoutRegions","plan","regions","Set","unreadable","field","fields","region","add","push","aiPlanRegionViolations","tree","built","violations","missing","filter","has","length","one","rule","code","message","listed","join","aiPlanItemCountViolations","section","items","shown","name","aiPlanCopiedPageViolations","screen","promised","sections","sort","a","b","tokenKey","value","replace","toLowerCase","aiTreeBoundTokens","bound","nodes","strings","out","Array","isArray","inner","Object","values","node","rootId","token","props","flatMap","aiPlannedTemplateTokens","definition","catalog","Map","tokens","map","entry","get","aiPlanTemplateTokenViolations","noun","quoted","slice"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-plan-conformance.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 type { AiBuildPlanScreen, AiBuildPlanSection } from '../model/ai-build-plan'\nimport type { AiJobPlan } from '../model/ai-jobs.types'\nimport {\n AI_LAYOUT_REGION_NAMES,\n aiBindsRepeatedItems,\n aiLayoutRegionOf,\n aiRepeatedItemCount,\n aiTreeLayoutRegions,\n walkTree,\n type AiDoctrineNode,\n type AiDoctrineTree,\n type AiDoctrineViolation,\n type AiLayoutRegion,\n} from '../runtime/ai-doctrine-validators'\nimport { aiBindingTokensIn, type AiTemplateSubjectDefinition } from '../model/ai-template-subjects'\nimport { aiPlanCreation } from './ai-job-generation'\n\n/**\n * The build held to the plan the member confirmed (AGL-3024).\n *\n * The doctrine validators ask whether an artifact was built the way a careful\n * author builds one. They do not ask whether it is the artifact the plan\n * PROMISED, and a build that quietly delivers less still reports `Done` —\n * which is the dangerous part, because `Done` is what tells the customer they\n * have no reason to re-read it. Measured on beta.139: a plan promising four\n * practice areas built three cards; a plan promising a layout with a sidebar\n * built a verbatim copy of the base layout, the word \"sidebar\" nowhere in it.\n *\n * ── Structured commitments, never prose ──────────────────────────────────\n *\n * A plan's rationale (`why`) is prose a model wrote, and matching English\n * against a node tree after the fact would be a second guess dressed as a\n * check. Only the plan's STRUCTURE is read here, and only the two shapes that\n * are already numbers or names in `ai-build-plan.ts`:\n *\n * - **A count**: `screens[].sections[].items`, which the plan already\n * carries and the section prompt already states as \"It shows N items\".\n * Nothing read it back.\n * - **A region**: `create[].fields` on a layout, which is that kind's own\n * field for the regions it has (`AI_LAYOUT_REGIONS`). A layout has no\n * properties, so the planner is asked there for the one thing about a\n * layout worth promising and worth checking — a checkable assertion the\n * planner emits beside its prose, rather than English parsed out of it.\n * - **A token**: `create[].fields` on a template, from the subject's own\n * closed catalog (`AI_TEMPLATE_SUBJECT_DEFINITIONS[subject].tokens`), which\n * is the template's analogue of a layout's regions — the one thing about a\n * template that differs per record, and therefore the one worth promising.\n *\n * The third was added after a measured miss (AGL-3024, 2026-09-21). A brief\n * asked for an attorney's bar admissions; the plan's `why` promised \"a\n * bar-admissions block\"; the built template had no such thing and reported\n * `Done`. Nothing had gone wrong in the build: `author` has six tokens —\n * name, bio, image, jobTitle, worksFor, url — and no catalog anywhere fills a\n * bar admission, so the promise was unkeepable when it was written. Read as a\n * token list the promise cannot even be MADE, which is a better place to stop\n * than after the document is paid for.\n *\n * ── It fails closed ──────────────────────────────────────────────────────\n *\n * Every function here answers \"the plan is kept\" only when it could look. A\n * layout the step never read is not a layout whose regions are present; the\n * layout door turns that into a stop for review rather than a `Done`. What it\n * will not do is guess: a count it cannot settle from the document — items a\n * collection fills at render — is not reported as short.\n *\n * The findings are ordinary `AiDoctrineViolation`s, so they ride the\n * mechanism every other rule rides: `runValidatedGeneration` re-asks the\n * model once with them, and a second answer that still breaks one stops the\n * job for a person with `aiDoctrineReview`. They carry `rule: null` on\n * purpose. The seventeen rules are how a document is BUILT; keeping the plan\n * is not one of them, and numbering these as rule 7 would print \"Rule 7\n * (Reuse before creating)\" over a finding about a count.\n */\n\n/** A count below this is not a repeat, so a plan promising one promises nothing to count. */\nexport const AI_PLAN_ITEMS_MIN = 2\n\n/**\n * The regions the confirmed plan's layout creation names, and the words it\n * named that no region answers to. A plan with no layout creation names none.\n */\nexport function aiPlannedLayoutRegions(plan: AiJobPlan | null): {\n regions: AiLayoutRegion[]\n unreadable: string[]\n} {\n const regions = new Set<AiLayoutRegion>()\n const unreadable: string[] = []\n for (const field of aiPlanCreation(plan, 'layout')?.fields ?? []) {\n const region = aiLayoutRegionOf(field)\n if (region) regions.add(region)\n else unreadable.push(field)\n }\n return { regions: [...regions], unreadable }\n}\n\n/**\n * The plan's promised regions against a built layout: one finding naming\n * every region the plan lists that the layout does not carry, and one naming\n * a word the vocabulary could not read, which is a promise this check cannot\n * settle either way.\n */\nexport function aiPlanRegionViolations(\n plan: AiJobPlan | null,\n tree: AiDoctrineTree,\n): AiDoctrineViolation[] {\n const { regions, unreadable } = aiPlannedLayoutRegions(plan)\n const built = aiTreeLayoutRegions(tree)\n const violations: AiDoctrineViolation[] = []\n const missing = regions.filter((region) => !built.has(region))\n if (missing.length) {\n const one = missing.length === 1\n violations.push({\n rule: null,\n code: 'plan-region-missing',\n message: `The confirmed plan gives this layout ${listed(\n missing,\n )}, and the layout has no ${one ? 'such region' : 'such regions'}. Build ${\n one ? 'it' : 'each of them'\n }: a sidebar is a Section whose element is aside, a header a Section whose element is header or an App Bar, a nav a Section whose element is nav, a footer a Section whose element is footer, and the main region the Layout Slot.`,\n })\n }\n if (unreadable.length) {\n violations.push({\n rule: null,\n code: 'plan-region-unreadable',\n message: `The confirmed plan gives this layout ${listed(\n unreadable,\n )}, which is no region this platform builds, so nothing can check the layout has ${\n unreadable.length === 1 ? 'it' : 'them'\n }. A layout's regions are ${AI_LAYOUT_REGION_NAMES.join(', ')}.`,\n })\n }\n return violations\n}\n\n/**\n * The plan's promised item count against a built section. Silent where there\n * is nothing to settle: a section promising fewer than `AI_PLAN_ITEMS_MIN`\n * promises no repeat, and one whose items a collection fills draws them from\n * data at render, where the document cannot be counted and rule 8 asked for\n * exactly that. Silent, too, when the section shows MORE than it promised:\n * the count is read generously (`aiRepeatedItemCount`), so a number above the\n * promise is as likely to be the reading as the section.\n */\nexport function aiPlanItemCountViolations(\n section: Pick<AiBuildPlanSection, 'name' | 'items'>,\n tree: AiDoctrineTree,\n): AiDoctrineViolation[] {\n if (section.items < AI_PLAN_ITEMS_MIN) return []\n if (aiBindsRepeatedItems(tree)) return []\n const shown = aiRepeatedItemCount(tree)\n if (shown >= section.items) return []\n return [\n {\n rule: null,\n code: 'plan-items-short',\n message: `The confirmed plan says the \"${section.name}\" section shows ${section.items} items, and this section shows ${shown}. Build all ${section.items}.`,\n },\n ]\n}\n\n/**\n * The counts the plan's sections promised against a page a COPY produced\n * (AGL-3024), read over the whole page because a copy has no section of the\n * plan's in it to read one at a time.\n *\n * A copied screen carries the SOURCE's nodes under the source's ids: nothing\n * maps \"the practice areas section\" onto a subtree of it, so the per-section\n * reading `aiPlanItemCountViolations` does at a generation pass has nothing\n * to stand on here. What can still be settled is the weaker claim, and it is\n * the one that catches the measured shape: a plan promising a section of four\n * against a copy in which NO group of four repeated things exists anywhere is\n * a promise the copy did not keep, whichever section was meant to keep it.\n *\n * Weaker on purpose, and so quieter: a copy holding six of something else\n * reads as six and is let through. A screen carries no app bar and no footer\n * — those are its layout's — so what is counted is the page's own content.\n */\nexport function aiPlanCopiedPageViolations(\n screen: Pick<AiBuildPlanScreen, 'sections'>,\n tree: AiDoctrineTree,\n): AiDoctrineViolation[] {\n const promised = screen.sections\n .filter((section) => section.items >= AI_PLAN_ITEMS_MIN)\n .sort((a, b) => b.items - a.items)[0]\n if (!promised) return []\n if (aiBindsRepeatedItems(tree)) return []\n const shown = aiRepeatedItemCount(tree)\n if (shown >= promised.items) return []\n return [\n {\n rule: null,\n code: 'plan-items-short',\n message: `The confirmed plan says the \"${promised.name}\" section shows ${promised.items} items, and this page is a copy that shows at most ${shown} of anything. Build the ${promised.items}.`,\n },\n ]\n}\n\n/** A token as the catalog spells it, for matching what a plan wrote against it. */\nfunction tokenKey(value: string): string {\n return value.replace(/[{}\\s]/g, '').toLowerCase()\n}\n\n/** Every binding token a built tree holds, spelled as the catalog spells them. */\nexport function aiTreeBoundTokens(tree: AiDoctrineTree): Set<string> {\n const bound = new Set<string>()\n const nodes = tree.nodes as unknown as Record<string, AiDoctrineNode>\n const strings = (value: unknown, out: string[]): string[] => {\n if (typeof value === 'string') out.push(value)\n else if (Array.isArray(value)) for (const inner of value) strings(inner, out)\n else if (value && typeof value === 'object') {\n for (const inner of Object.values(value as Record<string, unknown>)) strings(inner, out)\n }\n return out\n }\n for (const { node } of walkTree({ rootId: tree.rootId, nodes })) {\n for (const token of strings(node.props, []).flatMap(aiBindingTokensIn)) {\n bound.add(tokenKey(token))\n }\n }\n return bound\n}\n\n/**\n * The subject tokens the confirmed plan's template creation names, and the\n * words it named that the subject's catalog does not answer to. A plan with\n * no template creation names none.\n */\nexport function aiPlannedTemplateTokens(\n plan: AiJobPlan | null,\n definition: AiTemplateSubjectDefinition,\n): { tokens: string[]; unreadable: string[] } {\n const catalog = new Map(definition.tokens.map((entry) => [tokenKey(entry.token), entry.token]))\n const tokens = new Set<string>()\n const unreadable: string[] = []\n for (const field of aiPlanCreation(plan, 'template')?.fields ?? []) {\n const token = catalog.get(tokenKey(field))\n if (token) tokens.add(token)\n else unreadable.push(field)\n }\n return { tokens: [...tokens], unreadable }\n}\n\n/**\n * The plan's promised tokens against a built template: one finding naming\n * every token the plan lists that the template does not bind, and one naming\n * a word the subject's catalog could not read — which is a promise this check\n * cannot settle either way, and, more usefully, one the page could never have\n * filled.\n */\nexport function aiPlanTemplateTokenViolations(\n plan: AiJobPlan | null,\n definition: AiTemplateSubjectDefinition,\n tree: AiDoctrineTree,\n): AiDoctrineViolation[] {\n const { tokens, unreadable } = aiPlannedTemplateTokens(plan, definition)\n const violations: AiDoctrineViolation[] = []\n if (tokens.length) {\n const bound = aiTreeBoundTokens(tree)\n const missing = tokens.filter((token) => !bound.has(tokenKey(token)))\n if (missing.length) {\n const one = missing.length === 1\n violations.push({\n rule: null,\n code: 'plan-token-missing',\n message: `The confirmed plan says this template shows ${listed(missing)}, and it binds ${\n one ? 'no such token' : 'no such tokens'\n }. Put ${one ? 'it' : 'each of them'} on the page, so every ${\n definition.noun.replace(/^an? /, '')\n } shows their own.`,\n })\n }\n }\n // A template renders for every record it serves, so a video it plays that\n // the member never confirmed loads on every page of the collection (AGL-3433).\n const promised = new Set(tokens.map(tokenKey))\n const videos = new Set(\n definition.tokens\n .map((entry) => tokenKey(entry.token))\n .filter((token) => token.endsWith('video') && !promised.has(token)),\n )\n const playing = new Map<string, string[]>()\n const nodes = tree.nodes as unknown as Record<string, AiDoctrineNode>\n for (const { id, node } of walkTree({ rootId: tree.rootId, nodes })) {\n for (const token of aiBindingTokensIn(JSON.stringify(node.props ?? {}))) {\n if (videos.has(tokenKey(token))) playing.set(token, [...(playing.get(token) ?? []), id])\n }\n }\n if (playing.size) {\n violations.push({\n rule: 16,\n code: 'plan-video-unasked',\n message: `This template plays ${listed([...playing.keys()])}, which the confirmed plan does not list, so every ${definition.noun.replace(\n /^an? /,\n '',\n )} page would load a player nobody asked for. Remove it.`,\n nodeIds: [...new Set([...playing.values()].flat())],\n })\n }\n if (unreadable.length) {\n violations.push({\n rule: null,\n code: 'plan-token-unreadable',\n message: `The confirmed plan says this template shows ${listed(\n unreadable,\n )}, which ${definition.noun}'s page does not fill, so nothing can check the template shows ${\n unreadable.length === 1 ? 'it' : 'them'\n }. Promise only what the page fills: ${definition.tokens\n .map((entry) => entry.token)\n .join(', ')}.`,\n })\n }\n return violations\n}\n\n/** `\"a\"`, `\"a\" and \"b\"`, `\"a\", \"b\" and \"c\"`. */\nfunction listed(values: readonly string[]): string {\n const quoted = values.map((value) => `\"${value}\"`)\n return quoted.length <= 1\n ? quoted.join('')\n : `${quoted.slice(0, -1).join(', ')} and ${quoted[quoted.length - 1]}`\n}\n"],"names":["AI_LAYOUT_REGION_NAMES","aiBindsRepeatedItems","aiLayoutRegionOf","aiRepeatedItemCount","aiTreeLayoutRegions","walkTree","aiBindingTokensIn","aiPlanCreation","AI_PLAN_ITEMS_MIN","aiPlannedLayoutRegions","plan","regions","Set","unreadable","field","fields","region","add","push","aiPlanRegionViolations","tree","built","violations","missing","filter","has","length","one","rule","code","message","listed","join","aiPlanItemCountViolations","section","items","shown","name","aiPlanCopiedPageViolations","screen","promised","sections","sort","a","b","tokenKey","value","replace","toLowerCase","aiTreeBoundTokens","bound","nodes","strings","out","Array","isArray","inner","Object","values","node","rootId","token","props","flatMap","aiPlannedTemplateTokens","definition","catalog","Map","tokens","map","entry","get","aiPlanTemplateTokenViolations","noun","videos","endsWith","playing","id","JSON","stringify","set","size","keys","nodeIds","flat","quoted","slice"],"mappings":"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"}
@@ -19,6 +19,7 @@ import { type AiBuildPlan } from '../model/ai-build-plan';
19
19
  import type { AiJob, AiJobKind, AiJobPlan, AiJobStatus } from '../model/ai-jobs.types';
20
20
  import { type AiPlanCapabilities, type AiPlanJobScope } from '../model/ai-plan-capabilities';
21
21
  import type { AiSiteInventory } from '../model/ai-site-inventory';
22
+ import type { AiTool } from '../providers/contract';
22
23
  import type { AiSystemBlock } from '../runtime/ai-runtime';
23
24
  import { readSiteInventory } from '../runtime/site-inventory';
24
25
  import { aiJobAdmissionRefusal } from './ai-job-admission';
@@ -158,6 +159,8 @@ export declare function aiJobPlanKey(input: {
158
159
  prompt: string;
159
160
  model: string;
160
161
  system: readonly AiSystemBlock[];
162
+ /** The plan tool the request offers (AGL-3433); the plain one when absent. */
163
+ tool?: AiTool;
161
164
  }): string;
162
165
  /**
163
166
  * The newest plan worth reusing out of what the finder returned: another
@@ -15,13 +15,14 @@ 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 { createHash } from "node:crypto";
18
- import { AI_BUILD_PLAN_TOOL, isAiPlanNewRef } from "../model/ai-build-plan.js";
18
+ import { AI_BUILD_PLAN_TOOL, aiBuildPlanToolFor, isAiPlanNewRef } from "../model/ai-build-plan.js";
19
19
  import { AI_TEMPLATE_SUBJECT_DEFINITIONS } from "../model/ai-template-subjects.js";
20
20
  import { AI_PAGE_CREATE_KINDS, aiPagePlanShapeRefusal } from "../model/ai-page-job.js";
21
21
  import { aiPlanCapabilitiesForJob, aiPlanCapabilityLines } from "../model/ai-plan-capabilities.js";
22
22
  import { AI_SITE_CREATE_KINDS, aiSitePlanShapeRefusal } from "../model/ai-site-job.js";
23
23
  import { aiDoctrineSystemBlocks, runValidatedGeneration } from "../runtime/ai-doctrine.js";
24
24
  import { AI_STEP_TIERS } from "../providers/catalog.js";
25
+ import { aiPlanEmbedBriefViolations } from "../runtime/ai-doctrine-validators.js";
25
26
  import { AI_ROUTING_TABLE, aiModelForStep } from "../providers/routing.js";
26
27
  import { readSiteInventory } from "../runtime/site-inventory.js";
27
28
  import { aiJobAdmissionRefusal } from "./ai-job-admission.js";
@@ -58,7 +59,7 @@ import { AI_JOBS_COLLECTION, registerAiJobPlanStep } from "./ai-jobs.js";
58
59
  * its plan is not kept, and it holds nothing.
59
60
  */ /** The plan step's own instructions, cached after the doctrine. */ export const AI_JOB_PLAN_INSTRUCTIONS = [
60
61
  {
61
- text: 'You plan what a website builder job will build, before anything is built. You are given the kind of job and the brief from the person who owns the site. ' + 'Answer with submit_build_plan: what the site inventory already has that the job reuses, what it must create and why nothing listed will do, and each screen it builds with its layout, template, slug, search title, search description and sections from top to bottom. ' + 'Refer to inventory records by id and to what the plan creates as new:<name>. Plan only what the brief asks for: a component, layout, template, form or email job plans no screens unless the brief asks for pages, and a page job plans exactly one screen.'
62
+ text: 'You plan what a website builder job will build, before anything is built. You are given the kind of job and the brief from the person who owns the site. ' + 'Answer with submit_build_plan: what the site inventory already has that the job reuses, what it must create and why nothing listed will do, and each page it builds (the screens list) with its layout, template, slug, search title, search description and sections from top to bottom. ' + 'Refer to inventory records by id and to what the plan creates as new:<name>. Plan only what the brief asks for: a component, layout, template, form or email job plans no pages unless the brief asks for them, and a page job plans exactly one page.'
62
63
  }
63
64
  ];
64
65
  /**
@@ -147,17 +148,25 @@ import { AI_JOBS_COLLECTION, registerAiJobPlanStep } from "./ai-jobs.js";
147
148
  shapeRefusal: aiSitePlanShapeRefusal
148
149
  }
149
150
  };
150
- /** A kind's shape refusal as the violation the plan's one re-ask names. */ function shapeViolations(scope) {
151
- if (!scope) return undefined;
151
+ /**
152
+ * What the plan step holds beyond the doctrine's plan rules, as the
153
+ * violations the plan's one re-ask names: the players the plan lists against
154
+ * the brief it answers (AGL-3433), which only this step reads beside the plan,
155
+ * and a kind's shape refusal.
156
+ */ function planViolations(scope, brief) {
152
157
  return (plan)=>{
153
- const message = scope.shapeRefusal(plan);
154
- return message ? [
155
- {
156
- rule: null,
157
- code: 'plan-job-shape',
158
- message
159
- }
160
- ] : [];
158
+ var _ref;
159
+ const message = (_ref = scope == null ? void 0 : scope.shapeRefusal(plan)) != null ? _ref : null;
160
+ return [
161
+ ...aiPlanEmbedBriefViolations(plan, brief),
162
+ ...message ? [
163
+ {
164
+ rule: null,
165
+ code: 'plan-job-shape',
166
+ message
167
+ }
168
+ ] : []
169
+ ];
161
170
  };
162
171
  }
163
172
  /**
@@ -226,7 +235,7 @@ import { AI_JOBS_COLLECTION, registerAiJobPlanStep } from "./ai-jobs.js";
226
235
  * MEANS, rather than what it covers, bumps it and strands every old key
227
236
  * harmlessly, since a key nothing matches simply asks the model.
228
237
  */ export function aiJobPlanKey(input) {
229
- var _input_job_hostId;
238
+ var _input_job_hostId, _input_tool;
230
239
  const digest = createHash('sha256');
231
240
  for (const part of [
232
241
  'plan.v1',
@@ -235,7 +244,7 @@ import { AI_JOBS_COLLECTION, registerAiJobPlanStep } from "./ai-jobs.js";
235
244
  input.model,
236
245
  input.prompt,
237
246
  ...input.system.map((block)=>block.text),
238
- JSON.stringify(AI_BUILD_PLAN_TOOL)
247
+ JSON.stringify((_input_tool = input.tool) != null ? _input_tool : AI_BUILD_PLAN_TOOL)
239
248
  ]){
240
249
  // Length-prefixed, so two different splits of the same characters cannot
241
250
  // collide by running into one another.
@@ -373,13 +382,16 @@ export function createAiJobPlanStep(deps = {}) {
373
382
  // — this job's plan is proposed to this member, whatever the last one did
374
383
  // with theirs.
375
384
  const resolved = model != null ? model : aiModelForStep('job.plan');
385
+ // A brief that asks for a video is offered the plan's list of players (AGL-3433).
386
+ const tool = aiBuildPlanToolFor(job.brief);
376
387
  const key = aiJobPlanKey({
377
388
  job,
378
389
  prompt,
379
390
  model: resolved,
380
391
  system: aiDoctrineSystemBlocks(inventory, {
381
392
  instructions: AI_JOB_PLAN_INSTRUCTIONS
382
- })
393
+ }),
394
+ tool
383
395
  });
384
396
  const reused = findPlansByKey ? aiReusablePlan(await findPlansByKey(job.orgId, key, firestore), {
385
397
  jobId: job.$id,
@@ -420,7 +432,7 @@ export function createAiJobPlanStep(deps = {}) {
420
432
  content: prompt
421
433
  }
422
434
  ],
423
- tool: AI_BUILD_PLAN_TOOL,
435
+ tool,
424
436
  // The routing ceiling, lowered on a tier too slow to look records up,
425
437
  // answer and ask again inside the least time the step registered.
426
438
  maxTokens: aiJobPlanMaxTokens(resolved)
@@ -432,7 +444,7 @@ export function createAiJobPlanStep(deps = {}) {
432
444
  signal
433
445
  } : {}, {
434
446
  capabilities,
435
- extend: shapeViolations(scope)
447
+ extend: planViolations(scope, job.brief)
436
448
  }));
437
449
  const spent = _extends({
438
450
  outputs: [],
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-plan-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 { createHash } from 'node:crypto'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport {\n AI_BUILD_PLAN_TOOL,\n isAiPlanNewRef,\n type AiBuildPlan,\n} from '../model/ai-build-plan'\nimport type { AiJob, AiJobKind, AiJobPlan, AiJobStatus } from '../model/ai-jobs.types'\nimport {\n AI_TEMPLATE_SUBJECT_DEFINITIONS,\n type AiTemplateSubject,\n} from '../model/ai-template-subjects'\nimport { AI_PAGE_CREATE_KINDS, aiPagePlanShapeRefusal } from '../model/ai-page-job'\nimport {\n aiPlanCapabilitiesForJob,\n aiPlanCapabilityLines,\n type AiPlanCapabilities,\n type AiPlanJobScope,\n} from '../model/ai-plan-capabilities'\nimport type { AiSiteInventory } from '../model/ai-site-inventory'\nimport { AI_SITE_CREATE_KINDS, aiSitePlanShapeRefusal } from '../model/ai-site-job'\nimport { aiDoctrineSystemBlocks, runValidatedGeneration } from '../runtime/ai-doctrine'\nimport { AI_STEP_TIERS } from '../providers/catalog'\nimport type { AiDoctrineViolation } from '../runtime/ai-doctrine-validators'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport type { AiSystemBlock } from '../runtime/ai-runtime'\nimport { readSiteInventory } from '../runtime/site-inventory'\nimport { aiJobAdmissionRefusal } from './ai-job-admission'\nimport { readAiPlanCapabilities } from './ai-job-drafts'\nimport {\n AI_JOB_BRIEF_MAX_CHARS,\n type AiJobStepOutcome,\n type AiJobStepRunner,\n} from './ai-job-text-step'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { aiDoctrineReview, aiUnspentOutcome } from './ai-job-generation'\nimport { aiPlanWithDraftIds } from './ai-job-draft-ids'\nimport { AI_JOBS_COLLECTION, registerAiJobPlanStep } from './ai-jobs'\n\n/**\n * The plan step (AGL-2935): the first step of every job that builds site\n * structure. Before a node is generated, the model answers with a typed plan\n * — what the site already has that the job reuses, what it creates and why,\n * and the screens it builds from them — held to the doctrine's plan rules\n * against the site's inventory and re-asked once when it breaks one.\n *\n * A plan that passes is kept on the job, and the job stops for review: the\n * member reads what it will build before a credit is spent building it, and\n * confirms through the resume door. A plan that still breaks a rule on its\n * re-ask stops the job the same way, with the rules named. Either way the\n * step writes nothing itself; the machine records the plan, the spend and\n * the stop, as it records every step.\n *\n * ── Told what it may create, refused what it cannot build (AGL-3030) ─────\n *\n * Before it asks, the step reads what the job may create on its site: the\n * workspace's plan, the site's counts against its limits, and — for a kind\n * that builds only some plans — what the job builds itself. The user turn\n * states it, the plan rules refuse a creation outside it with the one re-ask,\n * and where the workspace keeps no reusable components they accept the page\n * built inline. A plan the confirm door would still refuse — the kind's own\n * admission, handed the plan, as the resume door hands it — is refused HERE,\n * before a member is shown a Confirm: the job fails with the door's sentence,\n * its plan is not kept, and it holds nothing.\n */\n\n/** The plan step's own instructions, cached after the doctrine. */\nexport const AI_JOB_PLAN_INSTRUCTIONS: readonly AiSystemBlock[] = [\n {\n text:\n 'You plan what a website builder job will build, before anything is built. You are given the kind of job and the brief from the person who owns the site. ' +\n 'Answer with submit_build_plan: what the site inventory already has that the job reuses, what it must create and why nothing listed will do, and each screen it builds with its layout, template, slug, search title, search description and sections from top to bottom. ' +\n 'Refer to inventory records by id and to what the plan creates as new:<name>. Plan only what the brief asks for: a component, layout, template, form or email job plans no screens unless the brief asks for pages, and a page job plans exactly one screen.',\n },\n]\n\n/**\n * The plan step's time (AGL-3026, AGL-3036): its two inventory-lookup rounds,\n * its answer and its re-ask at the routing table's ceiling for `job.plan`, on\n * the tier that step kind is served from, with the step's reads and writes,\n * at the rates `ai-job-budget.ts` assumes — fitted to what a beat can give a\n * step, so the served tier asks a little under the routing ceiling.\n */\nexport const AI_JOB_PLAN_STEP_BUDGET = aiJobStepBudget({\n tier: AI_STEP_TIERS['job.plan'],\n maxTokens: AI_ROUTING_TABLE['job.plan'].maxTokens,\n})\n\n/**\n * The least time a plan needs before it starts. Registered with the step, so\n * an inline door never starts a plan. A plan thinks before it answers and\n * routinely runs past an inline door's budget, and a provider call that\n * budget cuts off is still generated and billed upstream while the meter\n * records nothing. The beat starts a plan only with this much of its own\n * budget left, and a spec holds it inside that budget.\n */\nexport const AI_JOB_PLAN_STEP_MINIMUM_MS = AI_JOB_PLAN_STEP_BUDGET.minimumMs\n\n/**\n * The plan's answer ceiling on the model a job runs: the most whose worst\n * case fits the least time the step registered, and never more than the\n * routing table's. A model the catalog does not know is planned at the\n * slowest.\n */\nexport function aiJobPlanMaxTokens(model: string): number {\n return AI_JOB_PLAN_STEP_BUDGET.maxTokens(model)\n}\n\n/** What the member reads while a plan waits for them. */\nexport const AI_JOB_PLAN_REVIEW_COPY =\n 'The plan is ready. Review what the job will reuse and create, then confirm it to build.'\n\n/**\n * The job as the plan step's user turn: its kind, its brief, its scalar\n * inputs and, where the job read them, what it may create on its site\n * (AGL-3030). Per workspace and per site, which is why they ride here and\n * never in a system block the platform's cache entry is keyed on.\n */\nexport function aiJobPlanPrompt(\n job: Pick<AiJob, 'kind' | 'brief' | 'inputs'>,\n capabilities: AiPlanCapabilities | null = null,\n): string {\n const lines = [`Job kind: ${job.kind}`, `Brief: ${job.brief.slice(0, AI_JOB_BRIEF_MAX_CHARS)}`]\n for (const [key, value] of Object.entries(job.inputs ?? {})) {\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {\n lines.push(`${key}: ${String(value)}`)\n }\n }\n if (capabilities) lines.push(...aiPlanCapabilityLines(capabilities))\n lines.push(...aiPlanTemplateTokenLines(job))\n return lines.join('\\n')\n}\n\n/**\n * What a TEMPLATE job's plan is told its creation's `fields` are (AGL-3143\n * §11): the subject's binding tokens, listed so the planner promises only\n * what the page can fill and the build can be held to it\n * (`aiPlanTemplateTokenViolations`).\n *\n * ⛔ It rides the job's own turn and NOT the `fields` description in the plan\n * tool, which every kind's request caches. Written there it read better and\n * cost 40 tokens of the shared prefix — 2,980 to 3,020 — which is one credit\n * of the Free page's 300-credit wall, taking the room it keeps for a re-asked\n * section from 45 credits to 44 and breaking `ai-job-free-page.spec.ts`. A\n * page job must not pay for a sentence about templates.\n */\nexport function aiPlanTemplateTokenLines(job: Pick<AiJob, 'kind' | 'inputs'>): string[] {\n if (job.kind !== 'template') return []\n const subject = (job.inputs ?? {})['subject']\n const definition =\n typeof subject === 'string'\n ? AI_TEMPLATE_SUBJECT_DEFINITIONS[subject as AiTemplateSubject]\n : undefined\n if (!definition) return []\n return [\n `The template's fields are the binding tokens its page shows, each one of: ${definition.tokens\n .map((entry) => entry.token)\n .join(', ')}. Promise only what the page fills.`,\n ]\n}\n\n/** A kind that builds only some plans: the creations it makes, and the shapes it refuses. */\nexport interface AiJobPlanScope extends AiPlanJobScope {\n /** Why a job of the kind cannot build a plan of this shape; `null` when it can. Pure. */\n shapeRefusal: (plan: AiBuildPlan) => string | null\n}\n\n/**\n * The kinds whose confirm door builds only some plans (AGL-3030). A page job\n * builds one screen and a scaffold four to eight; each builds only the\n * creations its list names. Every other planned kind builds its own one\n * output, and its plan is held to the workspace alone.\n */\nexport const AI_JOB_PLAN_SCOPES: Readonly<Partial<Record<AiJobKind, AiJobPlanScope>>> = {\n page: { noun: 'a page job', creates: AI_PAGE_CREATE_KINDS, shapeRefusal: aiPagePlanShapeRefusal },\n site: { noun: 'a site scaffold', creates: AI_SITE_CREATE_KINDS, shapeRefusal: aiSitePlanShapeRefusal },\n}\n\n/** A kind's shape refusal as the violation the plan's one re-ask names. */\nfunction shapeViolations(scope: AiJobPlanScope | null): ((plan: AiBuildPlan) => AiDoctrineViolation[]) | undefined {\n if (!scope) return undefined\n return (plan) => {\n const message = scope.shapeRefusal(plan)\n return message ? [{ rule: null, code: 'plan-job-shape', message }] : []\n }\n}\n\n/**\n * The inventory names of every id the plan references, so the proposal reads\n * \"the Services layout\" rather than an id — kept on the plan because the\n * inventory is not read again when the member opens it.\n */\nexport function aiPlanLabels(\n plan: AiBuildPlan,\n inventory: AiSiteInventory | null,\n): Record<string, string> {\n const names = new Map<string, string>()\n for (const rows of [\n inventory?.components,\n inventory?.layouts,\n inventory?.templates,\n inventory?.forms,\n inventory?.datasets,\n inventory?.collections,\n inventory?.screens,\n ]) {\n for (const row of rows ?? []) names.set(row.id, row.name)\n }\n const refs = [\n ...plan.reuse.map((entry) => entry.id),\n ...plan.create.map((entry) => entry.duplicateOf),\n ...plan.screens.flatMap((screen) => [\n screen.layout,\n screen.template,\n screen.duplicateOf,\n ...screen.sections.flatMap((section) => section.uses),\n ]),\n ]\n const labels: Record<string, string> = {}\n for (const ref of refs) {\n if (!ref || isAiPlanNewRef(ref)) continue\n const name = names.get(ref)\n if (name) labels[ref] = name\n }\n return labels\n}\n\n/* ------------------------------------------------------------------------ *\n * Reusing an identical brief's plan\n * ------------------------------------------------------------------------ */\n\n/**\n * How long a plan may be reused for (AGL-2937).\n *\n * The window is what makes reuse safe rather than merely cheap. The key\n * already covers everything the request said — the brief, the inputs, the\n * model and the prompt as rendered, site inventory included — so a plan is\n * reused only when nothing the model was shown has changed. What the key\n * cannot see is the world outside the request: a page published, a component\n * renamed after the inventory was read, a member who meant something\n * different the second time. Fifteen minutes is short enough that the answer\n * is still the one the site would give, and long enough to cover what reuse\n * is actually for — the same brief run twice in a sitting, a job resubmitted\n * after a refused reservation, two members starting the same work.\n */\nexport const AI_PLAN_REUSE_WINDOW_MS = 15 * 60 * 1_000\n\n/** Jobs one lookup reads. More than one can share a key; the newest plan wins. */\nexport const AI_PLAN_REUSE_CANDIDATES = 5\n\n/** A job whose plan might be reused, as the finder reports it. */\nexport interface AiJobPlanCandidate {\n jobId: string\n status: AiJobStatus\n plan: AiJobPlan\n}\n\n/**\n * The digest a plan is reused by: the whole request, never the answer.\n *\n * It hashes what the model was asked and what it was shown — the job's kind\n * and site, the user turn (which carries the trimmed brief and every scalar\n * input), the model that would answer, every rendered system block including\n * the site inventory, and the tool's schema. Two requests that hash the same\n * would have been sent the same bytes to the same model, so the second can\n * keep the first's answer.\n *\n * The version tag is the escape hatch: anything that changes what a key\n * MEANS, rather than what it covers, bumps it and strands every old key\n * harmlessly, since a key nothing matches simply asks the model.\n */\nexport function aiJobPlanKey(input: {\n job: Pick<AiJob, 'kind' | 'hostId'>\n prompt: string\n model: string\n system: readonly AiSystemBlock[]\n}): string {\n const digest = createHash('sha256')\n for (const part of [\n 'plan.v1',\n input.job.kind,\n input.job.hostId ?? '',\n input.model,\n input.prompt,\n ...input.system.map((block) => block.text),\n JSON.stringify(AI_BUILD_PLAN_TOOL),\n ]) {\n // Length-prefixed, so two different splits of the same characters cannot\n // collide by running into one another.\n digest.update(`${part.length}:${part}\\u0000`)\n }\n return digest.digest('hex')\n}\n\n/** A job that could not have produced a reusable plan, whatever its key says. */\nconst UNREUSABLE: readonly AiJobStatus[] = ['canceled', 'failed']\n\nfunction planMillis(plan: AiJobPlan): number | null {\n const at = plan.proposedAt as unknown\n if (at instanceof Date) return at.getTime()\n if (at && typeof (at as { toMillis?: unknown }).toMillis === 'function') {\n return (at as { toMillis(): number }).toMillis()\n }\n return null\n}\n\n/**\n * The newest plan worth reusing out of what the finder returned: another\n * job's, still proposed or already confirmed, on a job that was neither\n * canceled nor failed, proposed inside the window.\n *\n * A canceled or failed job is excluded even though its plan may be perfectly\n * good: those are the jobs a member walked away from, and handing their plan\n * to the next one would make a rejected answer look like a fresh one.\n */\nexport function aiReusablePlan(\n candidates: readonly AiJobPlanCandidate[],\n context: { jobId: string; now: Date },\n): AiJobPlanCandidate | null {\n const floor = context.now.getTime() - AI_PLAN_REUSE_WINDOW_MS\n let best: AiJobPlanCandidate | null = null\n let bestAt = -1\n for (const candidate of candidates) {\n if (candidate.jobId === context.jobId) continue\n if (UNREUSABLE.includes(candidate.status)) continue\n if (candidate.plan.status !== 'proposed' && candidate.plan.status !== 'confirmed') continue\n const at = planMillis(candidate.plan)\n if (at === null || at < floor || at > context.now.getTime()) continue\n if (at > bestAt) {\n best = candidate\n bestAt = at\n }\n }\n return best\n}\n\nexport type AiJobPlanFinder = (\n orgId: string,\n key: string,\n firestore?: FirebaseFirestore.Firestore,\n) => Promise<AiJobPlanCandidate[]>\n\n/**\n * The finder the step uses in production: an equality on `plan.key` inside\n * the org's own jobs.\n *\n * One equality on one field, with no ordering beside it, so Firestore's\n * automatic single-field index answers it and no composite index is deployed.\n * The window and the statuses are applied in memory instead, which is what\n * keeps it that way.\n */\nexport const findAiJobsByPlanKey: AiJobPlanFinder = async (orgId, key, firestore) => {\n if (!firestore) return []\n const snapshot = await firestore\n .collection('orgs')\n .doc(orgId)\n .collection(AI_JOBS_COLLECTION)\n .where('plan.key', '==', key)\n .limit(AI_PLAN_REUSE_CANDIDATES)\n .get()\n return snapshot.docs.flatMap((doc) => {\n const data = doc.data() as { status?: AiJobStatus; plan?: AiJobPlan | null }\n return data.plan\n ? [{ jobId: doc.id, status: data.status ?? 'queued', plan: data.plan }]\n : []\n })\n}\n\n/** What a job may create on its site, read for the plan step; `null` for no site. */\nexport type AiPlanCapabilitiesReader = (input: {\n job: AiJob\n org: Partial<AglynOrgBilling> | null\n firestore: FirebaseFirestore.Firestore\n}) => Promise<AiPlanCapabilities | null>\n\n/** The reader the step uses in production: the draft bands' own arithmetic, for the job's site. */\nexport const readAiJobPlanCapabilities: AiPlanCapabilitiesReader = async ({ job, org, firestore }) =>\n job.hostId ? readAiPlanCapabilities(firestore, { hostId: job.hostId, org }) : null\n\nexport interface AiJobPlanStepDeps {\n /** The inventory reader; specs hand in a fake. */\n readInventory?: typeof readSiteInventory\n /** The reuse lookup; specs hand in a fake, and `null` turns reuse off. */\n findPlansByKey?: AiJobPlanFinder | null\n /** What the job may create on its site (AGL-3030); specs and the eval recorder hand in their own. */\n readCapabilities?: AiPlanCapabilitiesReader\n /** The kind's admission, asked of a plan before it is kept; the registry's otherwise. */\n admissionRefusal?: typeof aiJobAdmissionRefusal\n}\n\nexport function createAiJobPlanStep(deps: AiJobPlanStepDeps = {}): AiJobStepRunner {\n const readInventory = deps.readInventory ?? readSiteInventory\n const findPlansByKey =\n deps.findPlansByKey === undefined ? findAiJobsByPlanKey : deps.findPlansByKey\n const readCapabilities = deps.readCapabilities ?? readAiJobPlanCapabilities\n const admissionRefusal = deps.admissionRefusal ?? aiJobAdmissionRefusal\n return async ({ job, now, signal, firestore, modelFor, org: orgDocument }) => {\n const org = (orgDocument ?? null) as Partial<AglynOrgBilling> | null\n const [inventory, workspace] = await Promise.all([\n job.hostId ? readInventory(job.orgId, job.hostId, { firestore }) : Promise.resolve(null),\n readCapabilities({ job, org, firestore }),\n ])\n const scope = AI_JOB_PLAN_SCOPES[job.kind] ?? null\n const capabilities = workspace ? aiPlanCapabilitiesForJob(workspace, scope) : null\n // The model switch's answer for this job (AGL-2942): the creator's pick\n // where the plan, the org restriction and the allotment allowlists allow\n // it, and Auto held to those same lists otherwise. Without a resolver the\n // doctrine asks the routing table itself.\n const model = modelFor?.('job.plan')\n const route = AI_ROUTING_TABLE['job.plan']\n const prompt = aiJobPlanPrompt(job, capabilities)\n\n /**\n * The confirm door's answer for this plan, asked before the plan is kept\n * (AGL-3030): the kind's own admission, handed the plan as the resume\n * door hands it. A refusal fails the job with the door's sentence; a door\n * that could not answer keeps the plan, since the resume door asks again\n * before anything is built.\n */\n const refusalOnKeep = async (plan: AiJobPlan, outcome: AiJobStepOutcome): Promise<AiJobStepOutcome | null> => {\n let refusal: Awaited<ReturnType<typeof aiJobAdmissionRefusal>> = null\n try {\n refusal = await admissionRefusal(job.kind, {\n firestore,\n orgId: job.orgId,\n hostId: job.hostId ?? null,\n inputs: job.inputs ?? {},\n org,\n plan,\n uid: job.createdBy,\n })\n } catch (error) {\n console.error('ai plan admission failed', { orgId: job.orgId, jobId: job.$id, error })\n }\n return refusal ? { ...outcome, failure: refusal.error } : null\n }\n\n // Reuse before asking (AGL-2937). The key covers the whole request, so a\n // hit is a request that would have been sent the same bytes to the same\n // model; the window covers what the key cannot see. A reused plan spends\n // nothing, so the machine's spent-nothing branch releases the reservation\n // and meters no credit, and the job still stops for the member to confirm\n // — this job's plan is proposed to this member, whatever the last one did\n // with theirs.\n const resolved = model ?? aiModelForStep('job.plan')\n const key = aiJobPlanKey({\n job,\n prompt,\n model: resolved,\n system: aiDoctrineSystemBlocks(inventory, { instructions: AI_JOB_PLAN_INSTRUCTIONS }),\n })\n const reused = findPlansByKey\n ? aiReusablePlan(await findPlansByKey(job.orgId, key, firestore), {\n jobId: job.$id,\n now,\n })\n : null\n if (reused) {\n // The reused plan's draft ids name the other job's drafts; this job's are its own.\n const plan: AiJobPlan = aiPlanWithDraftIds(job.kind, {\n ...reused.plan,\n status: 'proposed',\n labels: aiPlanLabels(reused.plan, inventory),\n proposedAt: now as unknown as AiJobPlan['proposedAt'],\n confirmedAt: null,\n confirmedBy: null,\n key,\n reusedFrom: reused.jobId,\n })\n const unspent = aiUnspentOutcome(resolved)\n return (\n (await refusalOnKeep(plan, unspent)) ?? {\n ...unspent,\n plan,\n review: { reason: 'plan', message: AI_JOB_PLAN_REVIEW_COPY, findings: [] },\n }\n )\n }\n\n const result = await runValidatedGeneration('plan', {\n step: 'job.plan',\n ...(model ? { model } : {}),\n instructions: AI_JOB_PLAN_INSTRUCTIONS,\n inventory,\n messages: [{ role: 'user', content: prompt }],\n tool: AI_BUILD_PLAN_TOOL,\n // The routing ceiling, lowered on a tier too slow to look records up,\n // answer and ask again inside the least time the step registered.\n maxTokens: aiJobPlanMaxTokens(resolved),\n ...(route.thinking ? { thinking: route.thinking } : {}),\n ...(route.effort ? { effort: route.effort } : {}),\n ...(signal ? { signal } : {}),\n capabilities,\n extend: shapeViolations(scope),\n })\n const spent: AiJobStepOutcome = {\n outputs: [],\n usage: result.usage,\n estCostUsd: result.estCostUsd,\n model: result.model,\n stopReason: result.stopReason,\n ...(result.effort ? { effort: result.effort } : {}),\n }\n if (result.status === 'refused') return { ...spent, refused: true }\n if (result.status === 'needs_input') return { ...spent, review: aiDoctrineReview(result) }\n // Each draft the plan decides is named as the plan is kept, before anything is built (AGL-3079).\n const plan: AiJobPlan = aiPlanWithDraftIds(job.kind, {\n ...result.value,\n status: 'proposed',\n labels: aiPlanLabels(result.value, inventory),\n // A Date the Admin SDK stores as a timestamp, like every instant the machine writes.\n proposedAt: now as unknown as AiJobPlan['proposedAt'],\n confirmedAt: null,\n confirmedBy: null,\n key,\n })\n return (\n (await refusalOnKeep(plan, spent)) ?? {\n ...spent,\n plan,\n review: { reason: 'plan', message: AI_JOB_PLAN_REVIEW_COPY, findings: [] },\n }\n )\n }\n}\n\nexport const runAiJobPlanStep = createAiJobPlanStep()\n\n/**\n * Registers the plan step every planned kind runs first, with the least time\n * a plan needs; the plugin's console surface calls it.\n */\nexport function registerAiJobPlan(): void {\n registerAiJobPlanStep(runAiJobPlanStep, { minimumMs: AI_JOB_PLAN_STEP_MINIMUM_MS })\n}\n"],"names":["createHash","AI_BUILD_PLAN_TOOL","isAiPlanNewRef","AI_TEMPLATE_SUBJECT_DEFINITIONS","AI_PAGE_CREATE_KINDS","aiPagePlanShapeRefusal","aiPlanCapabilitiesForJob","aiPlanCapabilityLines","AI_SITE_CREATE_KINDS","aiSitePlanShapeRefusal","aiDoctrineSystemBlocks","runValidatedGeneration","AI_STEP_TIERS","AI_ROUTING_TABLE","aiModelForStep","readSiteInventory","aiJobAdmissionRefusal","readAiPlanCapabilities","AI_JOB_BRIEF_MAX_CHARS","aiJobStepBudget","aiDoctrineReview","aiUnspentOutcome","aiPlanWithDraftIds","AI_JOBS_COLLECTION","registerAiJobPlanStep","AI_JOB_PLAN_INSTRUCTIONS","text","AI_JOB_PLAN_STEP_BUDGET","tier","maxTokens","AI_JOB_PLAN_STEP_MINIMUM_MS","minimumMs","aiJobPlanMaxTokens","model","AI_JOB_PLAN_REVIEW_COPY","aiJobPlanPrompt","job","capabilities","lines","kind","brief","slice","key","value","Object","entries","inputs","push","String","aiPlanTemplateTokenLines","join","subject","definition","undefined","tokens","map","entry","token","AI_JOB_PLAN_SCOPES","page","noun","creates","shapeRefusal","site","shapeViolations","scope","plan","message","rule","code","aiPlanLabels","inventory","names","Map","rows","components","layouts","templates","forms","datasets","collections","screens","row","set","id","name","refs","reuse","create","duplicateOf","flatMap","screen","layout","template","sections","section","uses","labels","ref","get","AI_PLAN_REUSE_WINDOW_MS","AI_PLAN_REUSE_CANDIDATES","aiJobPlanKey","input","digest","part","hostId","prompt","system","block","JSON","stringify","update","length","UNREUSABLE","planMillis","at","proposedAt","Date","getTime","toMillis","aiReusablePlan","candidates","context","floor","now","best","bestAt","candidate","jobId","includes","status","findAiJobsByPlanKey","orgId","firestore","snapshot","collection","doc","where","limit","docs","data","readAiJobPlanCapabilities","org","createAiJobPlanStep","deps","readInventory","findPlansByKey","readCapabilities","admissionRefusal","signal","modelFor","orgDocument","workspace","Promise","all","resolve","route","refusalOnKeep","outcome","refusal","uid","createdBy","error","console","$id","failure","resolved","instructions","reused","confirmedAt","confirmedBy","reusedFrom","unspent","review","reason","findings","result","step","messages","role","content","tool","thinking","effort","extend","spent","outputs","usage","estCostUsd","stopReason","refused","runAiJobPlanStep","registerAiJobPlan"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-plan-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 { createHash } from 'node:crypto'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport {\n AI_BUILD_PLAN_TOOL,\n aiBuildPlanToolFor,\n isAiPlanNewRef,\n type AiBuildPlan,\n} from '../model/ai-build-plan'\nimport type { AiJob, AiJobKind, AiJobPlan, AiJobStatus } from '../model/ai-jobs.types'\nimport {\n AI_TEMPLATE_SUBJECT_DEFINITIONS,\n type AiTemplateSubject,\n} from '../model/ai-template-subjects'\nimport { AI_PAGE_CREATE_KINDS, aiPagePlanShapeRefusal } from '../model/ai-page-job'\nimport {\n aiPlanCapabilitiesForJob,\n aiPlanCapabilityLines,\n type AiPlanCapabilities,\n type AiPlanJobScope,\n} from '../model/ai-plan-capabilities'\nimport type { AiSiteInventory } from '../model/ai-site-inventory'\nimport { AI_SITE_CREATE_KINDS, aiSitePlanShapeRefusal } from '../model/ai-site-job'\nimport { aiDoctrineSystemBlocks, runValidatedGeneration } from '../runtime/ai-doctrine'\nimport { AI_STEP_TIERS } from '../providers/catalog'\nimport {\n aiPlanEmbedBriefViolations,\n type AiDoctrineViolation,\n} from '../runtime/ai-doctrine-validators'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport type { AiTool } from '../providers/contract'\nimport type { AiSystemBlock } from '../runtime/ai-runtime'\nimport { readSiteInventory } from '../runtime/site-inventory'\nimport { aiJobAdmissionRefusal } from './ai-job-admission'\nimport { readAiPlanCapabilities } from './ai-job-drafts'\nimport {\n AI_JOB_BRIEF_MAX_CHARS,\n type AiJobStepOutcome,\n type AiJobStepRunner,\n} from './ai-job-text-step'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { aiDoctrineReview, aiUnspentOutcome } from './ai-job-generation'\nimport { aiPlanWithDraftIds } from './ai-job-draft-ids'\nimport { AI_JOBS_COLLECTION, registerAiJobPlanStep } from './ai-jobs'\n\n/**\n * The plan step (AGL-2935): the first step of every job that builds site\n * structure. Before a node is generated, the model answers with a typed plan\n * — what the site already has that the job reuses, what it creates and why,\n * and the screens it builds from them — held to the doctrine's plan rules\n * against the site's inventory and re-asked once when it breaks one.\n *\n * A plan that passes is kept on the job, and the job stops for review: the\n * member reads what it will build before a credit is spent building it, and\n * confirms through the resume door. A plan that still breaks a rule on its\n * re-ask stops the job the same way, with the rules named. Either way the\n * step writes nothing itself; the machine records the plan, the spend and\n * the stop, as it records every step.\n *\n * ── Told what it may create, refused what it cannot build (AGL-3030) ─────\n *\n * Before it asks, the step reads what the job may create on its site: the\n * workspace's plan, the site's counts against its limits, and — for a kind\n * that builds only some plans — what the job builds itself. The user turn\n * states it, the plan rules refuse a creation outside it with the one re-ask,\n * and where the workspace keeps no reusable components they accept the page\n * built inline. A plan the confirm door would still refuse — the kind's own\n * admission, handed the plan, as the resume door hands it — is refused HERE,\n * before a member is shown a Confirm: the job fails with the door's sentence,\n * its plan is not kept, and it holds nothing.\n */\n\n/** The plan step's own instructions, cached after the doctrine. */\nexport const AI_JOB_PLAN_INSTRUCTIONS: readonly AiSystemBlock[] = [\n {\n text:\n 'You plan what a website builder job will build, before anything is built. You are given the kind of job and the brief from the person who owns the site. ' +\n 'Answer with submit_build_plan: what the site inventory already has that the job reuses, what it must create and why nothing listed will do, and each page it builds (the screens list) with its layout, template, slug, search title, search description and sections from top to bottom. ' +\n 'Refer to inventory records by id and to what the plan creates as new:<name>. Plan only what the brief asks for: a component, layout, template, form or email job plans no pages unless the brief asks for them, and a page job plans exactly one page.',\n },\n]\n\n/**\n * The plan step's time (AGL-3026, AGL-3036): its two inventory-lookup rounds,\n * its answer and its re-ask at the routing table's ceiling for `job.plan`, on\n * the tier that step kind is served from, with the step's reads and writes,\n * at the rates `ai-job-budget.ts` assumes — fitted to what a beat can give a\n * step, so the served tier asks a little under the routing ceiling.\n */\nexport const AI_JOB_PLAN_STEP_BUDGET = aiJobStepBudget({\n tier: AI_STEP_TIERS['job.plan'],\n maxTokens: AI_ROUTING_TABLE['job.plan'].maxTokens,\n})\n\n/**\n * The least time a plan needs before it starts. Registered with the step, so\n * an inline door never starts a plan. A plan thinks before it answers and\n * routinely runs past an inline door's budget, and a provider call that\n * budget cuts off is still generated and billed upstream while the meter\n * records nothing. The beat starts a plan only with this much of its own\n * budget left, and a spec holds it inside that budget.\n */\nexport const AI_JOB_PLAN_STEP_MINIMUM_MS = AI_JOB_PLAN_STEP_BUDGET.minimumMs\n\n/**\n * The plan's answer ceiling on the model a job runs: the most whose worst\n * case fits the least time the step registered, and never more than the\n * routing table's. A model the catalog does not know is planned at the\n * slowest.\n */\nexport function aiJobPlanMaxTokens(model: string): number {\n return AI_JOB_PLAN_STEP_BUDGET.maxTokens(model)\n}\n\n/** What the member reads while a plan waits for them. */\nexport const AI_JOB_PLAN_REVIEW_COPY =\n 'The plan is ready. Review what the job will reuse and create, then confirm it to build.'\n\n/**\n * The job as the plan step's user turn: its kind, its brief, its scalar\n * inputs and, where the job read them, what it may create on its site\n * (AGL-3030). Per workspace and per site, which is why they ride here and\n * never in a system block the platform's cache entry is keyed on.\n */\nexport function aiJobPlanPrompt(\n job: Pick<AiJob, 'kind' | 'brief' | 'inputs'>,\n capabilities: AiPlanCapabilities | null = null,\n): string {\n const lines = [`Job kind: ${job.kind}`, `Brief: ${job.brief.slice(0, AI_JOB_BRIEF_MAX_CHARS)}`]\n for (const [key, value] of Object.entries(job.inputs ?? {})) {\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {\n lines.push(`${key}: ${String(value)}`)\n }\n }\n if (capabilities) lines.push(...aiPlanCapabilityLines(capabilities))\n lines.push(...aiPlanTemplateTokenLines(job))\n return lines.join('\\n')\n}\n\n/**\n * What a TEMPLATE job's plan is told its creation's `fields` are (AGL-3143\n * §11): the subject's binding tokens, listed so the planner promises only\n * what the page can fill and the build can be held to it\n * (`aiPlanTemplateTokenViolations`).\n *\n * ⛔ It rides the job's own turn and NOT the `fields` description in the plan\n * tool, which every kind's request caches. Written there it read better and\n * cost 40 tokens of the shared prefix — 2,980 to 3,020 — which is one credit\n * of the Free page's 300-credit wall, taking the room it keeps for a re-asked\n * section from 45 credits to 44 and breaking `ai-job-free-page.spec.ts`. A\n * page job must not pay for a sentence about templates.\n */\nexport function aiPlanTemplateTokenLines(job: Pick<AiJob, 'kind' | 'inputs'>): string[] {\n if (job.kind !== 'template') return []\n const subject = (job.inputs ?? {})['subject']\n const definition =\n typeof subject === 'string'\n ? AI_TEMPLATE_SUBJECT_DEFINITIONS[subject as AiTemplateSubject]\n : undefined\n if (!definition) return []\n return [\n `The template's fields are the binding tokens its page shows, each one of: ${definition.tokens\n .map((entry) => entry.token)\n .join(', ')}. Promise only what the page fills.`,\n ]\n}\n\n/** A kind that builds only some plans: the creations it makes, and the shapes it refuses. */\nexport interface AiJobPlanScope extends AiPlanJobScope {\n /** Why a job of the kind cannot build a plan of this shape; `null` when it can. Pure. */\n shapeRefusal: (plan: AiBuildPlan) => string | null\n}\n\n/**\n * The kinds whose confirm door builds only some plans (AGL-3030). A page job\n * builds one screen and a scaffold four to eight; each builds only the\n * creations its list names. Every other planned kind builds its own one\n * output, and its plan is held to the workspace alone.\n */\nexport const AI_JOB_PLAN_SCOPES: Readonly<Partial<Record<AiJobKind, AiJobPlanScope>>> = {\n page: { noun: 'a page job', creates: AI_PAGE_CREATE_KINDS, shapeRefusal: aiPagePlanShapeRefusal },\n site: { noun: 'a site scaffold', creates: AI_SITE_CREATE_KINDS, shapeRefusal: aiSitePlanShapeRefusal },\n}\n\n/**\n * What the plan step holds beyond the doctrine's plan rules, as the\n * violations the plan's one re-ask names: the players the plan lists against\n * the brief it answers (AGL-3433), which only this step reads beside the plan,\n * and a kind's shape refusal.\n */\nfunction planViolations(\n scope: AiJobPlanScope | null,\n brief: string,\n): (plan: AiBuildPlan) => AiDoctrineViolation[] {\n return (plan) => {\n const message = scope?.shapeRefusal(plan) ?? null\n return [\n ...aiPlanEmbedBriefViolations(plan, brief),\n ...(message ? [{ rule: null, code: 'plan-job-shape', message }] : []),\n ]\n }\n}\n\n/**\n * The inventory names of every id the plan references, so the proposal reads\n * \"the Services layout\" rather than an id — kept on the plan because the\n * inventory is not read again when the member opens it.\n */\nexport function aiPlanLabels(\n plan: AiBuildPlan,\n inventory: AiSiteInventory | null,\n): Record<string, string> {\n const names = new Map<string, string>()\n for (const rows of [\n inventory?.components,\n inventory?.layouts,\n inventory?.templates,\n inventory?.forms,\n inventory?.datasets,\n inventory?.collections,\n inventory?.screens,\n ]) {\n for (const row of rows ?? []) names.set(row.id, row.name)\n }\n const refs = [\n ...plan.reuse.map((entry) => entry.id),\n ...plan.create.map((entry) => entry.duplicateOf),\n ...plan.screens.flatMap((screen) => [\n screen.layout,\n screen.template,\n screen.duplicateOf,\n ...screen.sections.flatMap((section) => section.uses),\n ]),\n ]\n const labels: Record<string, string> = {}\n for (const ref of refs) {\n if (!ref || isAiPlanNewRef(ref)) continue\n const name = names.get(ref)\n if (name) labels[ref] = name\n }\n return labels\n}\n\n/* ------------------------------------------------------------------------ *\n * Reusing an identical brief's plan\n * ------------------------------------------------------------------------ */\n\n/**\n * How long a plan may be reused for (AGL-2937).\n *\n * The window is what makes reuse safe rather than merely cheap. The key\n * already covers everything the request said — the brief, the inputs, the\n * model and the prompt as rendered, site inventory included — so a plan is\n * reused only when nothing the model was shown has changed. What the key\n * cannot see is the world outside the request: a page published, a component\n * renamed after the inventory was read, a member who meant something\n * different the second time. Fifteen minutes is short enough that the answer\n * is still the one the site would give, and long enough to cover what reuse\n * is actually for — the same brief run twice in a sitting, a job resubmitted\n * after a refused reservation, two members starting the same work.\n */\nexport const AI_PLAN_REUSE_WINDOW_MS = 15 * 60 * 1_000\n\n/** Jobs one lookup reads. More than one can share a key; the newest plan wins. */\nexport const AI_PLAN_REUSE_CANDIDATES = 5\n\n/** A job whose plan might be reused, as the finder reports it. */\nexport interface AiJobPlanCandidate {\n jobId: string\n status: AiJobStatus\n plan: AiJobPlan\n}\n\n/**\n * The digest a plan is reused by: the whole request, never the answer.\n *\n * It hashes what the model was asked and what it was shown — the job's kind\n * and site, the user turn (which carries the trimmed brief and every scalar\n * input), the model that would answer, every rendered system block including\n * the site inventory, and the tool's schema. Two requests that hash the same\n * would have been sent the same bytes to the same model, so the second can\n * keep the first's answer.\n *\n * The version tag is the escape hatch: anything that changes what a key\n * MEANS, rather than what it covers, bumps it and strands every old key\n * harmlessly, since a key nothing matches simply asks the model.\n */\nexport function aiJobPlanKey(input: {\n job: Pick<AiJob, 'kind' | 'hostId'>\n prompt: string\n model: string\n system: readonly AiSystemBlock[]\n /** The plan tool the request offers (AGL-3433); the plain one when absent. */\n tool?: AiTool\n}): string {\n const digest = createHash('sha256')\n for (const part of [\n 'plan.v1',\n input.job.kind,\n input.job.hostId ?? '',\n input.model,\n input.prompt,\n ...input.system.map((block) => block.text),\n JSON.stringify(input.tool ?? AI_BUILD_PLAN_TOOL),\n ]) {\n // Length-prefixed, so two different splits of the same characters cannot\n // collide by running into one another.\n digest.update(`${part.length}:${part}\\u0000`)\n }\n return digest.digest('hex')\n}\n\n/** A job that could not have produced a reusable plan, whatever its key says. */\nconst UNREUSABLE: readonly AiJobStatus[] = ['canceled', 'failed']\n\nfunction planMillis(plan: AiJobPlan): number | null {\n const at = plan.proposedAt as unknown\n if (at instanceof Date) return at.getTime()\n if (at && typeof (at as { toMillis?: unknown }).toMillis === 'function') {\n return (at as { toMillis(): number }).toMillis()\n }\n return null\n}\n\n/**\n * The newest plan worth reusing out of what the finder returned: another\n * job's, still proposed or already confirmed, on a job that was neither\n * canceled nor failed, proposed inside the window.\n *\n * A canceled or failed job is excluded even though its plan may be perfectly\n * good: those are the jobs a member walked away from, and handing their plan\n * to the next one would make a rejected answer look like a fresh one.\n */\nexport function aiReusablePlan(\n candidates: readonly AiJobPlanCandidate[],\n context: { jobId: string; now: Date },\n): AiJobPlanCandidate | null {\n const floor = context.now.getTime() - AI_PLAN_REUSE_WINDOW_MS\n let best: AiJobPlanCandidate | null = null\n let bestAt = -1\n for (const candidate of candidates) {\n if (candidate.jobId === context.jobId) continue\n if (UNREUSABLE.includes(candidate.status)) continue\n if (candidate.plan.status !== 'proposed' && candidate.plan.status !== 'confirmed') continue\n const at = planMillis(candidate.plan)\n if (at === null || at < floor || at > context.now.getTime()) continue\n if (at > bestAt) {\n best = candidate\n bestAt = at\n }\n }\n return best\n}\n\nexport type AiJobPlanFinder = (\n orgId: string,\n key: string,\n firestore?: FirebaseFirestore.Firestore,\n) => Promise<AiJobPlanCandidate[]>\n\n/**\n * The finder the step uses in production: an equality on `plan.key` inside\n * the org's own jobs.\n *\n * One equality on one field, with no ordering beside it, so Firestore's\n * automatic single-field index answers it and no composite index is deployed.\n * The window and the statuses are applied in memory instead, which is what\n * keeps it that way.\n */\nexport const findAiJobsByPlanKey: AiJobPlanFinder = async (orgId, key, firestore) => {\n if (!firestore) return []\n const snapshot = await firestore\n .collection('orgs')\n .doc(orgId)\n .collection(AI_JOBS_COLLECTION)\n .where('plan.key', '==', key)\n .limit(AI_PLAN_REUSE_CANDIDATES)\n .get()\n return snapshot.docs.flatMap((doc) => {\n const data = doc.data() as { status?: AiJobStatus; plan?: AiJobPlan | null }\n return data.plan\n ? [{ jobId: doc.id, status: data.status ?? 'queued', plan: data.plan }]\n : []\n })\n}\n\n/** What a job may create on its site, read for the plan step; `null` for no site. */\nexport type AiPlanCapabilitiesReader = (input: {\n job: AiJob\n org: Partial<AglynOrgBilling> | null\n firestore: FirebaseFirestore.Firestore\n}) => Promise<AiPlanCapabilities | null>\n\n/** The reader the step uses in production: the draft bands' own arithmetic, for the job's site. */\nexport const readAiJobPlanCapabilities: AiPlanCapabilitiesReader = async ({ job, org, firestore }) =>\n job.hostId ? readAiPlanCapabilities(firestore, { hostId: job.hostId, org }) : null\n\nexport interface AiJobPlanStepDeps {\n /** The inventory reader; specs hand in a fake. */\n readInventory?: typeof readSiteInventory\n /** The reuse lookup; specs hand in a fake, and `null` turns reuse off. */\n findPlansByKey?: AiJobPlanFinder | null\n /** What the job may create on its site (AGL-3030); specs and the eval recorder hand in their own. */\n readCapabilities?: AiPlanCapabilitiesReader\n /** The kind's admission, asked of a plan before it is kept; the registry's otherwise. */\n admissionRefusal?: typeof aiJobAdmissionRefusal\n}\n\nexport function createAiJobPlanStep(deps: AiJobPlanStepDeps = {}): AiJobStepRunner {\n const readInventory = deps.readInventory ?? readSiteInventory\n const findPlansByKey =\n deps.findPlansByKey === undefined ? findAiJobsByPlanKey : deps.findPlansByKey\n const readCapabilities = deps.readCapabilities ?? readAiJobPlanCapabilities\n const admissionRefusal = deps.admissionRefusal ?? aiJobAdmissionRefusal\n return async ({ job, now, signal, firestore, modelFor, org: orgDocument }) => {\n const org = (orgDocument ?? null) as Partial<AglynOrgBilling> | null\n const [inventory, workspace] = await Promise.all([\n job.hostId ? readInventory(job.orgId, job.hostId, { firestore }) : Promise.resolve(null),\n readCapabilities({ job, org, firestore }),\n ])\n const scope = AI_JOB_PLAN_SCOPES[job.kind] ?? null\n const capabilities = workspace ? aiPlanCapabilitiesForJob(workspace, scope) : null\n // The model switch's answer for this job (AGL-2942): the creator's pick\n // where the plan, the org restriction and the allotment allowlists allow\n // it, and Auto held to those same lists otherwise. Without a resolver the\n // doctrine asks the routing table itself.\n const model = modelFor?.('job.plan')\n const route = AI_ROUTING_TABLE['job.plan']\n const prompt = aiJobPlanPrompt(job, capabilities)\n\n /**\n * The confirm door's answer for this plan, asked before the plan is kept\n * (AGL-3030): the kind's own admission, handed the plan as the resume\n * door hands it. A refusal fails the job with the door's sentence; a door\n * that could not answer keeps the plan, since the resume door asks again\n * before anything is built.\n */\n const refusalOnKeep = async (plan: AiJobPlan, outcome: AiJobStepOutcome): Promise<AiJobStepOutcome | null> => {\n let refusal: Awaited<ReturnType<typeof aiJobAdmissionRefusal>> = null\n try {\n refusal = await admissionRefusal(job.kind, {\n firestore,\n orgId: job.orgId,\n hostId: job.hostId ?? null,\n inputs: job.inputs ?? {},\n org,\n plan,\n uid: job.createdBy,\n })\n } catch (error) {\n console.error('ai plan admission failed', { orgId: job.orgId, jobId: job.$id, error })\n }\n return refusal ? { ...outcome, failure: refusal.error } : null\n }\n\n // Reuse before asking (AGL-2937). The key covers the whole request, so a\n // hit is a request that would have been sent the same bytes to the same\n // model; the window covers what the key cannot see. A reused plan spends\n // nothing, so the machine's spent-nothing branch releases the reservation\n // and meters no credit, and the job still stops for the member to confirm\n // — this job's plan is proposed to this member, whatever the last one did\n // with theirs.\n const resolved = model ?? aiModelForStep('job.plan')\n // A brief that asks for a video is offered the plan's list of players (AGL-3433).\n const tool = aiBuildPlanToolFor(job.brief)\n const key = aiJobPlanKey({\n job,\n prompt,\n model: resolved,\n system: aiDoctrineSystemBlocks(inventory, { instructions: AI_JOB_PLAN_INSTRUCTIONS }),\n tool,\n })\n const reused = findPlansByKey\n ? aiReusablePlan(await findPlansByKey(job.orgId, key, firestore), {\n jobId: job.$id,\n now,\n })\n : null\n if (reused) {\n // The reused plan's draft ids name the other job's drafts; this job's are its own.\n const plan: AiJobPlan = aiPlanWithDraftIds(job.kind, {\n ...reused.plan,\n status: 'proposed',\n labels: aiPlanLabels(reused.plan, inventory),\n proposedAt: now as unknown as AiJobPlan['proposedAt'],\n confirmedAt: null,\n confirmedBy: null,\n key,\n reusedFrom: reused.jobId,\n })\n const unspent = aiUnspentOutcome(resolved)\n return (\n (await refusalOnKeep(plan, unspent)) ?? {\n ...unspent,\n plan,\n review: { reason: 'plan', message: AI_JOB_PLAN_REVIEW_COPY, findings: [] },\n }\n )\n }\n\n const result = await runValidatedGeneration('plan', {\n step: 'job.plan',\n ...(model ? { model } : {}),\n instructions: AI_JOB_PLAN_INSTRUCTIONS,\n inventory,\n messages: [{ role: 'user', content: prompt }],\n tool,\n // The routing ceiling, lowered on a tier too slow to look records up,\n // answer and ask again inside the least time the step registered.\n maxTokens: aiJobPlanMaxTokens(resolved),\n ...(route.thinking ? { thinking: route.thinking } : {}),\n ...(route.effort ? { effort: route.effort } : {}),\n ...(signal ? { signal } : {}),\n capabilities,\n extend: planViolations(scope, job.brief),\n })\n const spent: AiJobStepOutcome = {\n outputs: [],\n usage: result.usage,\n estCostUsd: result.estCostUsd,\n model: result.model,\n stopReason: result.stopReason,\n ...(result.effort ? { effort: result.effort } : {}),\n }\n if (result.status === 'refused') return { ...spent, refused: true }\n if (result.status === 'needs_input') return { ...spent, review: aiDoctrineReview(result) }\n // Each draft the plan decides is named as the plan is kept, before anything is built (AGL-3079).\n const plan: AiJobPlan = aiPlanWithDraftIds(job.kind, {\n ...result.value,\n status: 'proposed',\n labels: aiPlanLabels(result.value, inventory),\n // A Date the Admin SDK stores as a timestamp, like every instant the machine writes.\n proposedAt: now as unknown as AiJobPlan['proposedAt'],\n confirmedAt: null,\n confirmedBy: null,\n key,\n })\n return (\n (await refusalOnKeep(plan, spent)) ?? {\n ...spent,\n plan,\n review: { reason: 'plan', message: AI_JOB_PLAN_REVIEW_COPY, findings: [] },\n }\n )\n }\n}\n\nexport const runAiJobPlanStep = createAiJobPlanStep()\n\n/**\n * Registers the plan step every planned kind runs first, with the least time\n * a plan needs; the plugin's console surface calls it.\n */\nexport function registerAiJobPlan(): void {\n registerAiJobPlanStep(runAiJobPlanStep, { minimumMs: AI_JOB_PLAN_STEP_MINIMUM_MS })\n}\n"],"names":["createHash","AI_BUILD_PLAN_TOOL","aiBuildPlanToolFor","isAiPlanNewRef","AI_TEMPLATE_SUBJECT_DEFINITIONS","AI_PAGE_CREATE_KINDS","aiPagePlanShapeRefusal","aiPlanCapabilitiesForJob","aiPlanCapabilityLines","AI_SITE_CREATE_KINDS","aiSitePlanShapeRefusal","aiDoctrineSystemBlocks","runValidatedGeneration","AI_STEP_TIERS","aiPlanEmbedBriefViolations","AI_ROUTING_TABLE","aiModelForStep","readSiteInventory","aiJobAdmissionRefusal","readAiPlanCapabilities","AI_JOB_BRIEF_MAX_CHARS","aiJobStepBudget","aiDoctrineReview","aiUnspentOutcome","aiPlanWithDraftIds","AI_JOBS_COLLECTION","registerAiJobPlanStep","AI_JOB_PLAN_INSTRUCTIONS","text","AI_JOB_PLAN_STEP_BUDGET","tier","maxTokens","AI_JOB_PLAN_STEP_MINIMUM_MS","minimumMs","aiJobPlanMaxTokens","model","AI_JOB_PLAN_REVIEW_COPY","aiJobPlanPrompt","job","capabilities","lines","kind","brief","slice","key","value","Object","entries","inputs","push","String","aiPlanTemplateTokenLines","join","subject","definition","undefined","tokens","map","entry","token","AI_JOB_PLAN_SCOPES","page","noun","creates","shapeRefusal","site","planViolations","scope","plan","message","rule","code","aiPlanLabels","inventory","names","Map","rows","components","layouts","templates","forms","datasets","collections","screens","row","set","id","name","refs","reuse","create","duplicateOf","flatMap","screen","layout","template","sections","section","uses","labels","ref","get","AI_PLAN_REUSE_WINDOW_MS","AI_PLAN_REUSE_CANDIDATES","aiJobPlanKey","input","digest","part","hostId","prompt","system","block","JSON","stringify","tool","update","length","UNREUSABLE","planMillis","at","proposedAt","Date","getTime","toMillis","aiReusablePlan","candidates","context","floor","now","best","bestAt","candidate","jobId","includes","status","findAiJobsByPlanKey","orgId","firestore","snapshot","collection","doc","where","limit","docs","data","readAiJobPlanCapabilities","org","createAiJobPlanStep","deps","readInventory","findPlansByKey","readCapabilities","admissionRefusal","signal","modelFor","orgDocument","workspace","Promise","all","resolve","route","refusalOnKeep","outcome","refusal","uid","createdBy","error","console","$id","failure","resolved","instructions","reused","confirmedAt","confirmedBy","reusedFrom","unspent","review","reason","findings","result","step","messages","role","content","thinking","effort","extend","spent","outputs","usage","estCostUsd","stopReason","refused","runAiJobPlanStep","registerAiJobPlan"],"mappings":"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"}
@@ -29,9 +29,9 @@ import type { AiJobAdmissionContext, AiJobAdmissionRefusal } from './ai-job-admi
29
29
  * caller, so a caller establishes, before it asks for anything:
30
30
  *
31
31
  * 1. a site is named, and it is the job's own org's;
32
- * 2. the owning plugin runs on that site — switched on for the workspace,
33
- * not switched off for the site, and past its release flag — and has
34
- * actually registered its writer in this process;
32
+ * 2. the owning plugin — whichever registered the writer; this plugin names
33
+ * none — runs on that site, switched on for the workspace, not switched
34
+ * off for the site, and past its release flag;
35
35
  * 3. whatever the kind checks of its own, such as the plan a campaign needs;
36
36
  * 4. that the member the drafts are for may create one of each, which only
37
37
  * the owner can answer, and does, through `refusal`.
@@ -46,12 +46,18 @@ import type { AiJobAdmissionContext, AiJobAdmissionRefusal } from './ai-job-admi
46
46
  export type AiPluginDraftWriterLookup = (resource: string) => PluginResourceDraftWriter | null;
47
47
  /** The registered writer for a resource, or `null` when no plugin writes it here. */
48
48
  export declare const aiPluginDraftWriter: AiPluginDraftWriterLookup;
49
- /** One draft a job kind needs written, and who owns it. */
49
+ /**
50
+ * The plugin that registered the writer for a resource, or `null` when none
51
+ * did in this process. The owner is whoever writes the resource — the
52
+ * registry knows it, so this plugin names none (AGL-3080).
53
+ */
54
+ export type AiPluginDraftOwnerLookup = (resource: string) => string | null;
55
+ /** The registered writer's plugin, from the core's registry. */
56
+ export declare const aiPluginDraftOwner: AiPluginDraftOwnerLookup;
57
+ /** One draft a job kind needs written. */
50
58
  export interface AiPluginDraftNeed {
51
59
  /** The resource name the owner registered its writer under. */
52
60
  resource: string;
53
- /** The owning plugin, as `plugins.config.json` names it. */
54
- pluginId: string;
55
61
  /** What the site's plugin list calls it, for the sentence a person reads. */
56
62
  label: string;
57
63
  }
@@ -64,6 +70,8 @@ export interface AiPluginDraftAdmissionInput {
64
70
  ownCheck?: (hostId: string) => Promise<AiJobAdmissionRefusal | null>;
65
71
  /** How a writer is found; the registry otherwise. */
66
72
  writerFor?: AiPluginDraftWriterLookup;
73
+ /** Which plugin owns a resource's writer; the registry otherwise. */
74
+ ownerFor?: AiPluginDraftOwnerLookup;
67
75
  /** The instant the owner's allowance is read against; now otherwise. */
68
76
  now?: Date;
69
77
  }
@@ -22,6 +22,11 @@ import { resolveOrgIdForHost } from "@aglyn/tenant-data-admin/server/organizatio
22
22
  var _pluginResourceDraftWriter;
23
23
  return (_ref = (_pluginResourceDraftWriter = pluginResourceDraftWriter(resource)) == null ? void 0 : _pluginResourceDraftWriter.writer) != null ? _ref : null;
24
24
  };
25
+ /** The registered writer's plugin, from the core's registry. */ export const aiPluginDraftOwner = (resource)=>{
26
+ var _ref;
27
+ var _pluginResourceDraftWriter;
28
+ return (_ref = (_pluginResourceDraftWriter = pluginResourceDraftWriter(resource)) == null ? void 0 : _pluginResourceDraftWriter.pluginId) != null ? _ref : null;
29
+ };
25
30
  /** What a person is told when the plugin that owns a draft does not run here. */ export function aiPluginDraftUnavailable(label) {
26
31
  return `Turn on ${label} for this site before starting the job.`;
27
32
  }
@@ -29,7 +34,7 @@ import { resolveOrgIdForHost } from "@aglyn/tenant-data-admin/server/organizatio
29
34
  * Whether a job of this kind may start on this site for this member; `null`
30
35
  * admits it. Nothing here writes, and nothing here spends.
31
36
  */ export async function aiPluginDraftAdmissionRefusal(context, input) {
32
- var _input_writerFor, _data, _input_now;
37
+ var _input_writerFor, _input_ownerFor, _data, _input_now;
33
38
  const { hostId } = context;
34
39
  if (!hostId) {
35
40
  return {
@@ -43,18 +48,21 @@ import { resolveOrgIdForHost } from "@aglyn/tenant-data-admin/server/organizatio
43
48
  error: 'Unknown site'
44
49
  };
45
50
  const writerFor = (_input_writerFor = input.writerFor) != null ? _input_writerFor : aiPluginDraftWriter;
51
+ const ownerFor = (_input_ownerFor = input.ownerFor) != null ? _input_ownerFor : aiPluginDraftOwner;
46
52
  const host = (_data = (await context.firestore.collection('hosts').doc(hostId).get()).data()) != null ? _data : null;
47
53
  const org = context.org;
54
+ const owners = input.drafts.map((draft)=>ownerFor(draft.resource));
48
55
  // One release-flag read for every owner at once: the answer is per org, and
49
56
  // a plugin subtracted here is one the site cannot reach at all.
50
- const released = new Set(await filterEnabledPluginsByReleaseFlags(input.drafts.map((draft)=>draft.pluginId), {
57
+ const released = new Set(await filterEnabledPluginsByReleaseFlags(owners.filter((owner)=>Boolean(owner)), {
51
58
  orgId: context.orgId,
52
59
  authorization: null
53
60
  }));
54
61
  const writers = [];
55
- for (const draft of input.drafts){
62
+ for (const [index, draft] of input.drafts.entries()){
56
63
  const writer = writerFor(draft.resource);
57
- if (!writer || !released.has(draft.pluginId) || !isHostPluginEnabled(org, host, draft.pluginId)) {
64
+ const owner = owners[index];
65
+ if (!writer || !owner || !released.has(owner) || !isHostPluginEnabled(org, host, owner)) {
58
66
  return {
59
67
  status: 403,
60
68
  error: aiPluginDraftUnavailable(draft.label)
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-plugin-drafts.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 { isHostPluginEnabled } from '@aglyn/aglyn/plugin-manager/enabled-plugins'\nimport {\n pluginResourceDraftWriter,\n type PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport { filterEnabledPluginsByReleaseFlags } from '@aglyn/tenant-data-admin/server/release-flags'\nimport { resolveOrgIdForHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport type { AiJobKind } from '../model/ai-jobs.types'\nimport type { AiJobAdmissionContext, AiJobAdmissionRefusal } from './ai-job-admission'\n\n/**\n * Admitting a job whose drafts ANOTHER plugin writes (AGL-2912).\n *\n * `aiDraftAdmissionRefusal` admits the kinds this plugin writes itself, where\n * the allowance is one it can read. An email job writes no document of its\n * own: the email plugin owns the email design and the marketing plugin owns\n * the campaign, and each registers a writer on the `plugin-resource-drafts`\n * seam. So the question this answers is different, and it is the caller's\n * half of that seam's contract — the seam carries no authorization of the\n * caller, so a caller establishes, before it asks for anything:\n *\n * 1. a site is named, and it is the job's own org's;\n * 2. the owning plugin runs on that site — switched on for the workspace,\n * not switched off for the site, and past its release flag — and has\n * actually registered its writer in this process;\n * 3. whatever the kind checks of its own, such as the plan a campaign needs;\n * 4. that the member the drafts are for may create one of each, which only\n * the owner can answer, and does, through `refusal`.\n *\n * Asked at the create door before the job exists and again at the resume door\n * before a confirmed plan runs, so a plugin switched off, or an allowance used\n * up, in between is caught before the job spends. A refusal is the owner's own\n * sentence wherever there is one, so a person reads what the console would\n * have told them.\n */\n\n/** How a resource's draft writer is found; the core's registry by default. */\nexport type AiPluginDraftWriterLookup = (resource: string) => PluginResourceDraftWriter | null\n\n/** The registered writer for a resource, or `null` when no plugin writes it here. */\nexport const aiPluginDraftWriter: AiPluginDraftWriterLookup = (resource) =>\n pluginResourceDraftWriter(resource)?.writer ?? null\n\n/** One draft a job kind needs written, and who owns it. */\nexport interface AiPluginDraftNeed {\n /** The resource name the owner registered its writer under. */\n resource: string\n /** The owning plugin, as `plugins.config.json` names it. */\n pluginId: string\n /** What the site's plugin list calls it, for the sentence a person reads. */\n label: string\n}\n\nexport interface AiPluginDraftAdmissionInput {\n /** The job kind, named in the refusal that asks for a site. */\n kind: AiJobKind\n /** Every draft the kind writes, each checked in turn. */\n drafts: readonly AiPluginDraftNeed[]\n /** The kind's own check, asked once the site and its plugins are known good. */\n ownCheck?: (hostId: string) => Promise<AiJobAdmissionRefusal | null>\n /** How a writer is found; the registry otherwise. */\n writerFor?: AiPluginDraftWriterLookup\n /** The instant the owner's allowance is read against; now otherwise. */\n now?: Date\n}\n\n/** What a person is told when the plugin that owns a draft does not run here. */\nexport function aiPluginDraftUnavailable(label: string): string {\n return `Turn on ${label} for this site before starting the job.`\n}\n\n/**\n * Whether a job of this kind may start on this site for this member; `null`\n * admits it. Nothing here writes, and nothing here spends.\n */\nexport async function aiPluginDraftAdmissionRefusal(\n context: AiJobAdmissionContext,\n input: AiPluginDraftAdmissionInput,\n): Promise<AiJobAdmissionRefusal | null> {\n const { hostId } = context\n if (!hostId) {\n return { status: 400, error: `Open the site the ${input.kind} is for before starting the job` }\n }\n const owner = await resolveOrgIdForHost(hostId)\n if (!owner || owner !== context.orgId) return { status: 404, error: 'Unknown site' }\n\n const writerFor = input.writerFor ?? aiPluginDraftWriter\n const host = (await context.firestore.collection('hosts').doc(hostId).get()).data() ?? null\n const org = context.org as { enabledPlugins?: string[] } | null\n // One release-flag read for every owner at once: the answer is per org, and\n // a plugin subtracted here is one the site cannot reach at all.\n const released = new Set(\n await filterEnabledPluginsByReleaseFlags(\n input.drafts.map((draft) => draft.pluginId),\n { orgId: context.orgId, authorization: null },\n ),\n )\n const writers: PluginResourceDraftWriter[] = []\n for (const draft of input.drafts) {\n const writer = writerFor(draft.resource)\n if (\n !writer ||\n !released.has(draft.pluginId) ||\n !isHostPluginEnabled(org, host, draft.pluginId)\n ) {\n return { status: 403, error: aiPluginDraftUnavailable(draft.label) }\n }\n writers.push(writer)\n }\n\n const own = await input.ownCheck?.(hostId)\n if (own) return own\n\n // Whether this member may create one of each, in the owner's own words. A\n // door that names no member — the machine re-checking a job it stored —\n // asks nothing here, because there is nobody to ask about.\n const uid = context.uid\n if (!uid) return null\n const now = input.now ?? new Date()\n for (const writer of writers) {\n const refusal = await writer.refusal({ orgId: context.orgId, hostId, uid, org, now })\n // The seam's 409 is \"one already exists\", which for an admission is a\n // room-to-create answer rather than a fault the door can act on.\n if (refusal) return { status: refusal.status === 409 ? 403 : refusal.status, error: refusal.error }\n }\n return null\n}\n"],"names":["isHostPluginEnabled","pluginResourceDraftWriter","filterEnabledPluginsByReleaseFlags","resolveOrgIdForHost","aiPluginDraftWriter","resource","writer","aiPluginDraftUnavailable","label","aiPluginDraftAdmissionRefusal","context","input","hostId","status","error","kind","owner","orgId","writerFor","host","firestore","collection","doc","get","data","org","released","Set","drafts","map","draft","pluginId","authorization","writers","has","push","own","ownCheck","uid","now","Date","refusal"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,mBAAmB,QAAQ,8CAA6C;AACjF,SACEC,yBAAyB,QAEpB,qDAAoD;AAC3D,SAASC,kCAAkC,QAAQ,gDAA+C;AAClG,SAASC,mBAAmB,QAAQ,gDAA+C;AAiCnF,mFAAmF,GACnF,OAAO,MAAMC,sBAAiD,CAACC;;QAC7DJ;oBAAAA,6BAAAA,0BAA0BI,8BAA1BJ,2BAAqCK,MAAM,mBAAI;EAAI;AAyBrD,+EAA+E,GAC/E,OAAO,SAASC,yBAAyBC,KAAa;IACpD,OAAO,CAAC,QAAQ,EAAEA,MAAM,uCAAuC,CAAC;AAClE;AAEA;;;CAGC,GACD,OAAO,eAAeC,8BACpBC,OAA8B,EAC9BC,KAAkC;QAShBA,kBACL,OA+BDA;IAvCZ,MAAM,EAAEC,MAAM,EAAE,GAAGF;IACnB,IAAI,CAACE,QAAQ;QACX,OAAO;YAAEC,QAAQ;YAAKC,OAAO,CAAC,kBAAkB,EAAEH,MAAMI,IAAI,CAAC,+BAA+B,CAAC;QAAC;IAChG;IACA,MAAMC,QAAQ,MAAMb,oBAAoBS;IACxC,IAAI,CAACI,SAASA,UAAUN,QAAQO,KAAK,EAAE,OAAO;QAAEJ,QAAQ;QAAKC,OAAO;IAAe;IAEnF,MAAMI,aAAYP,mBAAAA,MAAMO,SAAS,YAAfP,mBAAmBP;IACrC,MAAMe,QAAO,QAAA,AAAC,CAAA,MAAMT,QAAQU,SAAS,CAACC,UAAU,CAAC,SAASC,GAAG,CAACV,QAAQW,GAAG,EAAC,EAAGC,IAAI,cAApE,QAA0E;IACvF,MAAMC,MAAMf,QAAQe,GAAG;IACvB,4EAA4E;IAC5E,gEAAgE;IAChE,MAAMC,WAAW,IAAIC,IACnB,MAAMzB,mCACJS,MAAMiB,MAAM,CAACC,GAAG,CAAC,CAACC,QAAUA,MAAMC,QAAQ,GAC1C;QAAEd,OAAOP,QAAQO,KAAK;QAAEe,eAAe;IAAK;IAGhD,MAAMC,UAAuC,EAAE;IAC/C,KAAK,MAAMH,SAASnB,MAAMiB,MAAM,CAAE;QAChC,MAAMtB,SAASY,UAAUY,MAAMzB,QAAQ;QACvC,IACE,CAACC,UACD,CAACoB,SAASQ,GAAG,CAACJ,MAAMC,QAAQ,KAC5B,CAAC/B,oBAAoByB,KAAKN,MAAMW,MAAMC,QAAQ,GAC9C;YACA,OAAO;gBAAElB,QAAQ;gBAAKC,OAAOP,yBAAyBuB,MAAMtB,KAAK;YAAE;QACrE;QACAyB,QAAQE,IAAI,CAAC7B;IACf;IAEA,MAAM8B,MAAM,OAAMzB,MAAM0B,QAAQ,oBAAd1B,MAAM0B,QAAQ,MAAd1B,OAAiBC;IACnC,IAAIwB,KAAK,OAAOA;IAEhB,0EAA0E;IAC1E,wEAAwE;IACxE,2DAA2D;IAC3D,MAAME,MAAM5B,QAAQ4B,GAAG;IACvB,IAAI,CAACA,KAAK,OAAO;IACjB,MAAMC,OAAM5B,aAAAA,MAAM4B,GAAG,YAAT5B,aAAa,IAAI6B;IAC7B,KAAK,MAAMlC,UAAU2B,QAAS;QAC5B,MAAMQ,UAAU,MAAMnC,OAAOmC,OAAO,CAAC;YAAExB,OAAOP,QAAQO,KAAK;YAAEL;YAAQ0B;YAAKb;YAAKc;QAAI;QACnF,sEAAsE;QACtE,iEAAiE;QACjE,IAAIE,SAAS,OAAO;YAAE5B,QAAQ4B,QAAQ5B,MAAM,KAAK,MAAM,MAAM4B,QAAQ5B,MAAM;YAAEC,OAAO2B,QAAQ3B,KAAK;QAAC;IACpG;IACA,OAAO;AACT"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-plugin-drafts.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 { isHostPluginEnabled } from '@aglyn/aglyn/plugin-manager/enabled-plugins'\nimport {\n pluginResourceDraftWriter,\n type PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport { filterEnabledPluginsByReleaseFlags } from '@aglyn/tenant-data-admin/server/release-flags'\nimport { resolveOrgIdForHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport type { AiJobKind } from '../model/ai-jobs.types'\nimport type { AiJobAdmissionContext, AiJobAdmissionRefusal } from './ai-job-admission'\n\n/**\n * Admitting a job whose drafts ANOTHER plugin writes (AGL-2912).\n *\n * `aiDraftAdmissionRefusal` admits the kinds this plugin writes itself, where\n * the allowance is one it can read. An email job writes no document of its\n * own: the email plugin owns the email design and the marketing plugin owns\n * the campaign, and each registers a writer on the `plugin-resource-drafts`\n * seam. So the question this answers is different, and it is the caller's\n * half of that seam's contract — the seam carries no authorization of the\n * caller, so a caller establishes, before it asks for anything:\n *\n * 1. a site is named, and it is the job's own org's;\n * 2. the owning plugin — whichever registered the writer; this plugin names\n * none — runs on that site, switched on for the workspace, not switched\n * off for the site, and past its release flag;\n * 3. whatever the kind checks of its own, such as the plan a campaign needs;\n * 4. that the member the drafts are for may create one of each, which only\n * the owner can answer, and does, through `refusal`.\n *\n * Asked at the create door before the job exists and again at the resume door\n * before a confirmed plan runs, so a plugin switched off, or an allowance used\n * up, in between is caught before the job spends. A refusal is the owner's own\n * sentence wherever there is one, so a person reads what the console would\n * have told them.\n */\n\n/** How a resource's draft writer is found; the core's registry by default. */\nexport type AiPluginDraftWriterLookup = (resource: string) => PluginResourceDraftWriter | null\n\n/** The registered writer for a resource, or `null` when no plugin writes it here. */\nexport const aiPluginDraftWriter: AiPluginDraftWriterLookup = (resource) =>\n pluginResourceDraftWriter(resource)?.writer ?? null\n\n/**\n * The plugin that registered the writer for a resource, or `null` when none\n * did in this process. The owner is whoever writes the resource — the\n * registry knows it, so this plugin names none (AGL-3080).\n */\nexport type AiPluginDraftOwnerLookup = (resource: string) => string | null\n\n/** The registered writer's plugin, from the core's registry. */\nexport const aiPluginDraftOwner: AiPluginDraftOwnerLookup = (resource) =>\n pluginResourceDraftWriter(resource)?.pluginId ?? null\n\n/** One draft a job kind needs written. */\nexport interface AiPluginDraftNeed {\n /** The resource name the owner registered its writer under. */\n resource: string\n /** What the site's plugin list calls it, for the sentence a person reads. */\n label: string\n}\n\nexport interface AiPluginDraftAdmissionInput {\n /** The job kind, named in the refusal that asks for a site. */\n kind: AiJobKind\n /** Every draft the kind writes, each checked in turn. */\n drafts: readonly AiPluginDraftNeed[]\n /** The kind's own check, asked once the site and its plugins are known good. */\n ownCheck?: (hostId: string) => Promise<AiJobAdmissionRefusal | null>\n /** How a writer is found; the registry otherwise. */\n writerFor?: AiPluginDraftWriterLookup\n /** Which plugin owns a resource's writer; the registry otherwise. */\n ownerFor?: AiPluginDraftOwnerLookup\n /** The instant the owner's allowance is read against; now otherwise. */\n now?: Date\n}\n\n/** What a person is told when the plugin that owns a draft does not run here. */\nexport function aiPluginDraftUnavailable(label: string): string {\n return `Turn on ${label} for this site before starting the job.`\n}\n\n/**\n * Whether a job of this kind may start on this site for this member; `null`\n * admits it. Nothing here writes, and nothing here spends.\n */\nexport async function aiPluginDraftAdmissionRefusal(\n context: AiJobAdmissionContext,\n input: AiPluginDraftAdmissionInput,\n): Promise<AiJobAdmissionRefusal | null> {\n const { hostId } = context\n if (!hostId) {\n return { status: 400, error: `Open the site the ${input.kind} is for before starting the job` }\n }\n const owner = await resolveOrgIdForHost(hostId)\n if (!owner || owner !== context.orgId) return { status: 404, error: 'Unknown site' }\n\n const writerFor = input.writerFor ?? aiPluginDraftWriter\n const ownerFor = input.ownerFor ?? aiPluginDraftOwner\n const host = (await context.firestore.collection('hosts').doc(hostId).get()).data() ?? null\n const org = context.org as { enabledPlugins?: string[] } | null\n const owners = input.drafts.map((draft) => ownerFor(draft.resource))\n // One release-flag read for every owner at once: the answer is per org, and\n // a plugin subtracted here is one the site cannot reach at all.\n const released = new Set(\n await filterEnabledPluginsByReleaseFlags(\n owners.filter((owner): owner is string => Boolean(owner)),\n { orgId: context.orgId, authorization: null },\n ),\n )\n const writers: PluginResourceDraftWriter[] = []\n for (const [index, draft] of input.drafts.entries()) {\n const writer = writerFor(draft.resource)\n const owner = owners[index]\n if (\n !writer ||\n !owner ||\n !released.has(owner) ||\n !isHostPluginEnabled(org, host, owner)\n ) {\n return { status: 403, error: aiPluginDraftUnavailable(draft.label) }\n }\n writers.push(writer)\n }\n\n const own = await input.ownCheck?.(hostId)\n if (own) return own\n\n // Whether this member may create one of each, in the owner's own words. A\n // door that names no member — the machine re-checking a job it stored —\n // asks nothing here, because there is nobody to ask about.\n const uid = context.uid\n if (!uid) return null\n const now = input.now ?? new Date()\n for (const writer of writers) {\n const refusal = await writer.refusal({ orgId: context.orgId, hostId, uid, org, now })\n // The seam's 409 is \"one already exists\", which for an admission is a\n // room-to-create answer rather than a fault the door can act on.\n if (refusal) return { status: refusal.status === 409 ? 403 : refusal.status, error: refusal.error }\n }\n return null\n}\n"],"names":["isHostPluginEnabled","pluginResourceDraftWriter","filterEnabledPluginsByReleaseFlags","resolveOrgIdForHost","aiPluginDraftWriter","resource","writer","aiPluginDraftOwner","pluginId","aiPluginDraftUnavailable","label","aiPluginDraftAdmissionRefusal","context","input","hostId","status","error","kind","owner","orgId","writerFor","ownerFor","host","firestore","collection","doc","get","data","org","owners","drafts","map","draft","released","Set","filter","Boolean","authorization","writers","index","entries","has","push","own","ownCheck","uid","now","Date","refusal"],"mappings":"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"}
@@ -14,6 +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 PluginRecordIndex } from '@aglyn/aglyn/plugin-manager/plugin-record-index';
17
18
  import type { AiJob } from '../model/ai-jobs.types';
18
19
  import { type AiProductPhotoRead } from '../model/ai-products';
19
20
  import { type AiJobAdmission } from './ai-job-admission';
@@ -88,6 +89,12 @@ export interface AiJobProductsStepDeps {
88
89
  image?: AiProductImageSeams;
89
90
  /** Whether a model reads pictures; the provider's own descriptor otherwise. */
90
91
  readsImages?: (model: string) => boolean;
92
+ /**
93
+ * Where the site's categories are read; the `productCategory` index the
94
+ * plugin that keeps them publishes otherwise. The eval harness hands its
95
+ * own, over the categories a case names.
96
+ */
97
+ categoryIndex?: PluginRecordIndex | null;
91
98
  }
92
99
  /** The step, with its reads replaceable where a spec needs them. */
93
100
  export declare function createAiJobProductsStep(deps?: AiJobProductsStepDeps): AiJobStepRunner;