@aglyn/plugins-ai 1.0.0-beta.260 → 1.0.0-beta.262

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 (81) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-brief-dialog.component.d.ts +21 -58
  3. package/src/lib/components/ai-brief-dialog.component.js +8 -132
  4. package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
  5. package/src/lib/components/ai-job-follow.component.js +2 -35
  6. package/src/lib/components/ai-job-follow.component.js.map +1 -1
  7. package/src/lib/components/ai-job-plan.component.d.ts +2 -8
  8. package/src/lib/components/ai-job-plan.component.js +13 -74
  9. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  10. package/src/lib/components/ai-site-start-card.component.js +43 -34
  11. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  12. package/src/lib/components/ai-template-collection-field.component.d.ts +2 -12
  13. package/src/lib/components/ai-template-collection-field.component.js +13 -10
  14. package/src/lib/components/ai-template-collection-field.component.js.map +1 -1
  15. package/src/lib/components/ai-upsell-dialog.component.d.ts +3 -25
  16. package/src/lib/components/ai-upsell-dialog.component.js +9 -66
  17. package/src/lib/components/ai-upsell-dialog.component.js.map +1 -1
  18. package/src/lib/components/use-ai-job-run.d.ts +3 -8
  19. package/src/lib/components/use-ai-job-run.js +4 -15
  20. package/src/lib/components/use-ai-job-run.js.map +1 -1
  21. package/src/lib/insights/ai-figure-readers.js +15 -5
  22. package/src/lib/insights/ai-figure-readers.js.map +1 -1
  23. package/src/lib/insights/ai-figure-summaries.d.ts +27 -0
  24. package/src/lib/insights/ai-figure-summaries.js +79 -0
  25. package/src/lib/insights/ai-figure-summaries.js.map +1 -0
  26. package/src/lib/insights/ai-insight-readers.d.ts +2 -1
  27. package/src/lib/insights/ai-insight-readers.js +11 -26
  28. package/src/lib/insights/ai-insight-readers.js.map +1 -1
  29. package/src/lib/jobs/ai-job-admission.d.ts +6 -0
  30. package/src/lib/jobs/ai-job-admission.js.map +1 -1
  31. package/src/lib/jobs/ai-job-site-step.js +10 -6
  32. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  33. package/src/lib/jobs/ai-layout-stock-photos.d.ts +43 -11
  34. package/src/lib/jobs/ai-layout-stock-photos.js +329 -32
  35. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  36. package/src/lib/jobs/ai-stock-photo-picker.js +13 -7
  37. package/src/lib/jobs/ai-stock-photo-picker.js.map +1 -1
  38. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-recordings.d.ts +35 -0
  39. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-recordings.js +404 -0
  40. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-recordings.js.map +1 -0
  41. package/src/lib/layout-language/ai-layout-motion.d.ts +9 -3
  42. package/src/lib/layout-language/ai-layout-motion.js +9 -3
  43. package/src/lib/layout-language/ai-layout-motion.js.map +1 -1
  44. package/src/lib/layout-language/ai-layout-pictures.d.ts +10 -1
  45. package/src/lib/layout-language/ai-layout-pictures.js +12 -6
  46. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  47. package/src/lib/model/ai-brief-copy.d.ts +85 -0
  48. package/src/lib/model/ai-brief-copy.js +152 -0
  49. package/src/lib/model/ai-brief-copy.js.map +1 -0
  50. package/src/lib/model/ai-create-view.d.ts +200 -0
  51. package/src/lib/model/ai-create-view.js +263 -0
  52. package/src/lib/model/ai-create-view.js.map +1 -0
  53. package/src/lib/model/ai-insight.d.ts +1 -1
  54. package/src/lib/model/ai-insight.js +4 -39
  55. package/src/lib/model/ai-insight.js.map +1 -1
  56. package/src/lib/model/ai-job-activity.d.ts +24 -0
  57. package/src/lib/model/ai-job-activity.js +52 -1
  58. package/src/lib/model/ai-job-activity.js.map +1 -1
  59. package/src/lib/model/ai-job-plan-lines.d.ts +46 -0
  60. package/src/lib/model/ai-job-plan-lines.js +70 -0
  61. package/src/lib/model/ai-job-plan-lines.js.map +1 -0
  62. package/src/lib/model/ai-template-collections.d.ts +28 -0
  63. package/src/lib/model/ai-template-collections.js +28 -0
  64. package/src/lib/model/ai-template-collections.js.map +1 -0
  65. package/src/lib/model/ai-upsell-copy.d.ts +41 -0
  66. package/src/lib/model/ai-upsell-copy.js +85 -0
  67. package/src/lib/model/ai-upsell-copy.js.map +1 -0
  68. package/src/lib/runtime/ai-doctrine-validators.d.ts +40 -0
  69. package/src/lib/runtime/ai-doctrine-validators.js +148 -3
  70. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  71. package/src/lib/runtime/ai-doctrine.js +6 -2
  72. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  73. package/src/lib/runtime/ai-eval-memory-firestore.js +2 -0
  74. package/src/lib/runtime/ai-eval-memory-firestore.js.map +1 -1
  75. package/src/lib/server/ai-create-route.d.ts +32 -0
  76. package/src/lib/server/ai-create-route.js +295 -0
  77. package/src/lib/server/ai-create-route.js.map +1 -0
  78. package/src/lib/server/ai-jobs-route.js +19 -0
  79. package/src/lib/server/ai-jobs-route.js.map +1 -1
  80. package/src/lib/server.js +14 -0
  81. package/src/lib/server.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/insights/ai-insight-readers.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 { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { isHostPluginEnabled, isPluginEnabled } from '@aglyn/aglyn/plugin-manager/enabled-plugins'\nimport {\n listPluginFigureReaders,\n normalizePluginFigureTable,\n pluginFigureParams,\n pluginFigureReaderPlugin,\n type ResolvedPluginFigureReader,\n} from '@aglyn/aglyn/plugin-manager/plugin-figures'\nimport {\n AI_INSIGHT_MAX_READS,\n AI_INSIGHT_SURFACE_READERS,\n aiInsightCellText,\n type AiInsightSurface,\n type AiInsightTable,\n} from '../model/ai-insight'\nimport { filterEnabledPluginsByReleaseFlags } from '@aglyn/tenant-data-admin/server/release-flags'\nimport type { AiInsightReadRequest } from '../tools/ai-insight-tool'\nimport type { AiDatasetCatalogEntry } from './ai-dataset-catalog'\n\n/**\n * Which figure readers an insight job may read, and reading them (AGL-2915).\n *\n * The caller's half of the `plugin-figures` contract: the seam carries no\n * authorization, so before a reader is offered to the model — let alone read\n * — this establishes that the surface asks about its kind of figures, that\n * the workspace's plan includes the feature the figures are sold under, and\n * that the plugin whose records they are runs here: past its release flag,\n * on for the workspace and not switched off for the site. The site belonging\n * to the job's workspace, and the member's reach, are the job's own checks.\n */\n\n/** Which of a set of plugins are past their release flags for an org; the platform's flags by default. */\nexport type AiReleasedPlugins = (pluginIds: string[], orgId: string) => Promise<string[]>\n\nconst releasedByFlags: AiReleasedPlugins = (pluginIds, orgId) =>\n filterEnabledPluginsByReleaseFlags(pluginIds, { orgId, authorization: null })\n\nexport interface AiInsightReaderContext {\n orgId: string\n /** The site the figures are about; `null` for a workspace-level question. */\n hostId: string | null\n /** The org document the machine or the door read. */\n org: object | null\n /** The site's host document, already checked to belong to the org; `null` with no site. */\n host: Record<string, unknown> | null\n surface: AiInsightSurface\n released?: AiReleasedPlugins\n}\n\n/** The readers a job on this surface may read, ordered by id so a prompt lists them in the same order every time. */\nexport async function aiInsightReaders(context: AiInsightReaderContext): Promise<ResolvedPluginFigureReader[]> {\n const prefixes = AI_INSIGHT_SURFACE_READERS[context.surface]\n const org = context.org as { enabledPlugins?: string[] } | null\n const candidates = listPluginFigureReaders().filter(({ reader }) => {\n if (!prefixes.includes(reader.id.split('.')[0])) return false\n // A site reader needs a site; a workspace reader reads with or without one.\n if (reader.scope === 'site' && !context.hostId) return false\n return !reader.feature || checkEntitlement(org as never, reader.feature)\n })\n const plugins = [...new Set(candidates.map(pluginFigureReaderPlugin).filter((id): id is string => Boolean(id)))]\n const released = new Set(plugins.length ? await (context.released ?? releasedByFlags)(plugins, context.orgId) : [])\n return candidates.filter((resolved) => {\n const plugin = pluginFigureReaderPlugin(resolved)\n if (!plugin) return true\n if (!released.has(plugin)) return false\n return context.hostId\n ? isHostPluginEnabled(org, context.host as { disabledPlugins?: string[] } | null, plugin)\n : isPluginEnabled(org, plugin)\n })\n}\n\n/** The window a reader is read over: the one asked for when it covers it, else the nearest it does. */\nexport function aiInsightReaderDays(windows: readonly number[], asked: number): number {\n if (!windows.length) return 0\n if (windows.includes(asked)) return asked\n return [...windows].sort((a, b) => Math.abs(a - asked) - Math.abs(b - asked) || a - b)[0]\n}\n\n/** The readers as the read prompt lists them: id, what each answers, its windows and its parameters. */\nexport function aiInsightReaderCatalog(readers: readonly ResolvedPluginFigureReader[]): string {\n return readers\n .map(({ reader }) => {\n const windows = reader.windows.length\n ? ` Windows: ${reader.windows.join(', ')} days.`\n : ' Current totals, with no window: days 0.'\n const params = (reader.params ?? []).length\n ? ` Parameters: ${(reader.params ?? [])\n .map((param) => `${param.name} (${param.required ? 'required' : 'optional'}) — ${param.description}`)\n .join('; ')}`\n : ''\n return `- ${reader.id} — ${reader.label}: ${reader.description}${windows}${params}`\n })\n .join('\\n')\n}\n\n/** The datasets a datasets question may name, as the read prompt lists them. */\nexport function aiInsightDatasetCatalog(datasets: readonly AiDatasetCatalogEntry[]): string {\n if (!datasets.length) return 'There are no datasets you may read here.'\n return datasets\n .map(\n (dataset) =>\n `- ${dataset.name} (id ${dataset.id}): ${dataset.fields.map((field) => `${field.name} [${field.type}]`).join(', ') || 'no fields'}`,\n )\n .join('\\n')\n}\n\nexport interface AiInsightReadResult {\n tables: AiInsightTable[]\n /** Why a request that named a real reader was not read, in its owner's words, for the log. */\n refusals: string[]\n}\n\n/**\n * The tables for the reads a job asked for: each request held to the readers\n * this job may read, its window to that reader's, its parameters to those it\n * declares; each table held to the contract by `normalizePluginFigureTable`.\n * A reader asked for twice with the same window and parameters is read once.\n */\nexport async function readAiInsightTables(\n readers: readonly ResolvedPluginFigureReader[],\n requests: readonly AiInsightReadRequest[],\n base: { orgId: string; hostId: string | null; uid: string | null; now: Date },\n maxTables: number = AI_INSIGHT_MAX_READS,\n): Promise<AiInsightReadResult> {\n const tables: AiInsightTable[] = []\n const refusals: string[] = []\n const seen = new Set<string>()\n for (const request of requests) {\n if (tables.length === maxTables) break\n const resolved = readers.find(({ reader }) => reader.id === request.reader)\n if (!resolved) continue\n const { reader } = resolved\n const days = aiInsightReaderDays(reader.windows, request.days)\n const params = pluginFigureParams(reader, request.params)\n if (params.ok === false) {\n refusals.push(`${reader.id}: missing ${params.missing}`)\n continue\n }\n const key = JSON.stringify([reader.id, days, params.params])\n if (seen.has(key)) continue\n seen.add(key)\n try {\n const read = await reader.read({ ...base, days, params: params.params })\n if (read.ok === false) {\n refusals.push(`${reader.id}: ${read.error}`)\n continue\n }\n const table = normalizePluginFigureTable(read.table)\n if (!table) {\n refusals.push(`${reader.id}: not a table`)\n continue\n }\n tables.push({ ...table, ref: `t${tables.length + 1}`, reader: reader.id, days, scope: reader.scope })\n } catch (error) {\n // One reader's fault is not the answer's: the rest are still read.\n console.error('ai insight reader failed', { reader: reader.id, orgId: base.orgId, error })\n refusals.push(`${reader.id}: failed`)\n }\n }\n return { tables, refusals }\n}\n\n/** One table as the answer prompt shows it: a heading, the columns, numbered rows, and its notes. */\nexport function aiInsightTableText(table: AiInsightTable): string {\n const period = table.period ? ` · ${table.period.from} to ${table.period.to} (${table.period.days} days)` : ''\n const lines = [\n `${table.ref} · ${table.title} (${table.source.label})${period}`,\n `Columns: ${table.columns.map((column) => column.label).join(' | ')}`,\n ...table.rows.map(\n (row, index) =>\n `${index}: ${table.columns.map((column) => aiInsightCellText(table, column.key, row[column.key])).join(' | ')}`,\n ),\n ]\n if (!table.rows.length) lines.push('(no rows)')\n if (table.omitted) lines.push(`${table.omitted} more rows were left out of this table.`)\n for (const note of table.notes) lines.push(`Note: ${note}`)\n return lines.join('\\n')\n}\n"],"names":["checkEntitlement","isHostPluginEnabled","isPluginEnabled","listPluginFigureReaders","normalizePluginFigureTable","pluginFigureParams","pluginFigureReaderPlugin","AI_INSIGHT_MAX_READS","AI_INSIGHT_SURFACE_READERS","aiInsightCellText","filterEnabledPluginsByReleaseFlags","releasedByFlags","pluginIds","orgId","authorization","aiInsightReaders","context","prefixes","surface","org","candidates","filter","reader","includes","id","split","scope","hostId","feature","plugins","Set","map","Boolean","released","length","resolved","plugin","has","host","aiInsightReaderDays","windows","asked","sort","a","b","Math","abs","aiInsightReaderCatalog","readers","join","params","param","name","required","description","label","aiInsightDatasetCatalog","datasets","dataset","fields","field","type","readAiInsightTables","requests","base","maxTables","tables","refusals","seen","request","find","days","ok","push","missing","key","JSON","stringify","add","read","error","table","ref","console","aiInsightTableText","period","from","to","lines","title","source","columns","column","rows","row","index","omitted","note","notes"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/insights/ai-insight-readers.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n normalizePluginFigureTable,\n pluginFigureParams,\n type ResolvedPluginFigureReader,\n} from '@aglyn/aglyn/plugin-manager/plugin-figures'\nimport {\n AI_INSIGHT_MAX_READS,\n AI_INSIGHT_SURFACE_READERS,\n aiInsightCellText,\n type AiInsightSurface,\n type AiInsightTable,\n} from '../model/ai-insight'\nimport {\n eligibleFigureReaders,\n type ReleasedFigurePlugins,\n} from '@aglyn/tenant-data-admin/server/figure-readers'\nimport type { AiInsightReadRequest } from '../tools/ai-insight-tool'\nimport type { AiDatasetCatalogEntry } from './ai-dataset-catalog'\n\n/**\n * Which figure readers an insight job may read, and reading them (AGL-2915).\n *\n * The caller's half of the `plugin-figures` contract: the seam carries no\n * authorization, so before a reader is offered to the model — let alone read\n * — this establishes that the surface asks about its kind of figures, that\n * the workspace's plan includes the feature the figures are sold under, and\n * that the plugin whose records they are runs here: past its release flag,\n * on for the workspace and not switched off for the site. The site belonging\n * to the job's workspace, and the member's reach, are the job's own checks.\n */\n\n/** Which of a set of plugins are past their release flags for an org; the platform's flags by default. */\nexport type AiReleasedPlugins = ReleasedFigurePlugins\n\nexport interface AiInsightReaderContext {\n orgId: string\n /** The site the figures are about; `null` for a workspace-level question. */\n hostId: string | null\n /** The org document the machine or the door read. */\n org: object | null\n /** The site's host document, already checked to belong to the org; `null` with no site. */\n host: Record<string, unknown> | null\n surface: AiInsightSurface\n released?: AiReleasedPlugins\n}\n\n/** The readers a job on this surface may read, ordered by id so a prompt lists them in the same order every time. */\nexport async function aiInsightReaders(context: AiInsightReaderContext): Promise<ResolvedPluginFigureReader[]> {\n const prefixes = AI_INSIGHT_SURFACE_READERS[context.surface]\n return eligibleFigureReaders({\n orgId: context.orgId,\n hostId: context.hostId,\n org: context.org,\n host: context.host,\n accept: (reader) => prefixes.includes(reader.id.split('.')[0]),\n ...(context.released ? { released: context.released } : {}),\n })\n}\n\n/** The window a reader is read over: the one asked for when it covers it, else the nearest it does. */\nexport function aiInsightReaderDays(windows: readonly number[], asked: number): number {\n if (!windows.length) return 0\n if (windows.includes(asked)) return asked\n return [...windows].sort((a, b) => Math.abs(a - asked) - Math.abs(b - asked) || a - b)[0]\n}\n\n/** The readers as the read prompt lists them: id, what each answers, its windows and its parameters. */\nexport function aiInsightReaderCatalog(readers: readonly ResolvedPluginFigureReader[]): string {\n return readers\n .map(({ reader }) => {\n const windows = reader.windows.length\n ? ` Windows: ${reader.windows.join(', ')} days.`\n : ' Current totals, with no window: days 0.'\n const params = (reader.params ?? []).length\n ? ` Parameters: ${(reader.params ?? [])\n .map((param) => `${param.name} (${param.required ? 'required' : 'optional'}) — ${param.description}`)\n .join('; ')}`\n : ''\n return `- ${reader.id} — ${reader.label}: ${reader.description}${windows}${params}`\n })\n .join('\\n')\n}\n\n/** The datasets a datasets question may name, as the read prompt lists them. */\nexport function aiInsightDatasetCatalog(datasets: readonly AiDatasetCatalogEntry[]): string {\n if (!datasets.length) return 'There are no datasets you may read here.'\n return datasets\n .map(\n (dataset) =>\n `- ${dataset.name} (id ${dataset.id}): ${dataset.fields.map((field) => `${field.name} [${field.type}]`).join(', ') || 'no fields'}`,\n )\n .join('\\n')\n}\n\nexport interface AiInsightReadResult {\n tables: AiInsightTable[]\n /** Why a request that named a real reader was not read, in its owner's words, for the log. */\n refusals: string[]\n}\n\n/**\n * The tables for the reads a job asked for: each request held to the readers\n * this job may read, its window to that reader's, its parameters to those it\n * declares; each table held to the contract by `normalizePluginFigureTable`.\n * A reader asked for twice with the same window and parameters is read once.\n */\nexport async function readAiInsightTables(\n readers: readonly ResolvedPluginFigureReader[],\n requests: readonly AiInsightReadRequest[],\n base: { orgId: string; hostId: string | null; uid: string | null; now: Date },\n maxTables: number = AI_INSIGHT_MAX_READS,\n): Promise<AiInsightReadResult> {\n const tables: AiInsightTable[] = []\n const refusals: string[] = []\n const seen = new Set<string>()\n for (const request of requests) {\n if (tables.length === maxTables) break\n const resolved = readers.find(({ reader }) => reader.id === request.reader)\n if (!resolved) continue\n const { reader } = resolved\n const days = aiInsightReaderDays(reader.windows, request.days)\n const params = pluginFigureParams(reader, request.params)\n if (params.ok === false) {\n refusals.push(`${reader.id}: missing ${params.missing}`)\n continue\n }\n const key = JSON.stringify([reader.id, days, params.params])\n if (seen.has(key)) continue\n seen.add(key)\n try {\n const read = await reader.read({ ...base, days, params: params.params })\n if (read.ok === false) {\n refusals.push(`${reader.id}: ${read.error}`)\n continue\n }\n const table = normalizePluginFigureTable(read.table)\n if (!table) {\n refusals.push(`${reader.id}: not a table`)\n continue\n }\n tables.push({ ...table, ref: `t${tables.length + 1}`, reader: reader.id, days, scope: reader.scope })\n } catch (error) {\n // One reader's fault is not the answer's: the rest are still read.\n console.error('ai insight reader failed', { reader: reader.id, orgId: base.orgId, error })\n refusals.push(`${reader.id}: failed`)\n }\n }\n return { tables, refusals }\n}\n\n/** One table as the answer prompt shows it: a heading, the columns, numbered rows, and its notes. */\nexport function aiInsightTableText(table: AiInsightTable): string {\n const period = table.period ? ` · ${table.period.from} to ${table.period.to} (${table.period.days} days)` : ''\n const lines = [\n `${table.ref} · ${table.title} (${table.source.label})${period}`,\n `Columns: ${table.columns.map((column) => column.label).join(' | ')}`,\n ...table.rows.map(\n (row, index) =>\n `${index}: ${table.columns.map((column) => aiInsightCellText(table, column.key, row[column.key])).join(' | ')}`,\n ),\n ]\n if (!table.rows.length) lines.push('(no rows)')\n if (table.omitted) lines.push(`${table.omitted} more rows were left out of this table.`)\n for (const note of table.notes) lines.push(`Note: ${note}`)\n return lines.join('\\n')\n}\n"],"names":["normalizePluginFigureTable","pluginFigureParams","AI_INSIGHT_MAX_READS","AI_INSIGHT_SURFACE_READERS","aiInsightCellText","eligibleFigureReaders","aiInsightReaders","context","prefixes","surface","orgId","hostId","org","host","accept","reader","includes","id","split","released","aiInsightReaderDays","windows","asked","length","sort","a","b","Math","abs","aiInsightReaderCatalog","readers","map","join","params","param","name","required","description","label","aiInsightDatasetCatalog","datasets","dataset","fields","field","type","readAiInsightTables","requests","base","maxTables","tables","refusals","seen","Set","request","resolved","find","days","ok","push","missing","key","JSON","stringify","has","add","read","error","table","ref","scope","console","aiInsightTableText","period","from","to","lines","title","source","columns","column","rows","row","index","omitted","note","notes"],"mappings":"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"}
@@ -43,6 +43,12 @@ export interface AiJobAdmissionContext {
43
43
  firestore: FirebaseFirestore.Firestore;
44
44
  orgId: string;
45
45
  hostId: string | null;
46
+ /**
47
+ * The create door's check, asked before "Create site" makes the site the
48
+ * job is for (AGL-3859): `hostId` is null because the site does not exist
49
+ * yet, and a kind that needs one answers for everything but the site.
50
+ */
51
+ siteToCome?: boolean;
46
52
  /** The job's kind-specific inputs, as the door parsed or the job stored them. */
47
53
  inputs: Readonly<Record<string, unknown>>;
48
54
  /** The org document the gate ladder read; a check narrows it to the fields it reads. */
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-admission.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 { AiCreditsPrompt } from '../model/ai-credit-estimate'\nimport type { AiJobKind, AiJobPlan } from '../model/ai-jobs.types'\nimport {\n AI_OFF_FOR_SITE_COPY,\n AI_OFF_FOR_SITE_STATUS,\n isAiOffForSite,\n} from '../model/ai-site-switch'\n\n/**\n * What a job kind checks before a job of it runs (AGL-2909).\n *\n * The create door parses the body every kind shares — the org, the site, the\n * brief, scalar inputs — and nothing the machine knows says whether THIS kind\n * can finish: a layout needs a site to write to, a page template names the\n * collection it renders, and both write a document the site's plan counts.\n * A job that cannot finish should never start, because a planned kind spends\n * on its plan before it writes anything. So a kind registers one check, the\n * create door asks it after the gate ladder admitted the request and before\n * the job exists, and the resume door asks it again before a confirmed plan\n * runs, since an allowance free at creation may be used by now. Either way\n * a refusal hands the reservation back and spends nothing.\n *\n * The resume door also hands the check the plan being confirmed (AGL-2907),\n * so a kind that can build only some plans — a page job builds one screen\n * from what the site has — refuses the others when a member confirms them,\n * not a beat later from inside the step.\n *\n * The registry lives apart from the machine so the doors read it without the\n * generators, and a generator registers here beside its runner.\n */\n\nexport interface AiJobAdmissionContext {\n /** The Admin SDK handle the door runs on. */\n firestore: FirebaseFirestore.Firestore\n orgId: string\n hostId: string | null\n /** The job's kind-specific inputs, as the door parsed or the job stored them. */\n inputs: Readonly<Record<string, unknown>>\n /** The org document the gate ladder read; a check narrows it to the fields it reads. */\n org: object | null\n /**\n * The job's plan when the door is resuming a job that has one; absent at\n * creation, when no plan exists yet.\n */\n plan?: AiJobPlan | null\n /**\n * The member whose request spends: the creator at the create door, the\n * member confirming at the resume door. A kind whose draft another plugin\n * writes asks that plugin whether this member may create one.\n */\n uid?: string | null\n /**\n * The person chose \"Build what fits\" (AGL-3722): a Free job whose p90 is\n * more than what is left is admitted, to build as much as it can and pause\n * with Resume. Unset, such a job is answered with the prompt instead.\n */\n creditsConfirmed?: boolean\n /**\n * Told the prompt a confirmed Free job was admitted past (AGL-3722), so the\n * door records the confirmation on the job: who, when, what was left.\n */\n onCreditsConfirmed?: (prompt: AiCreditsPrompt) => void\n}\n\nexport interface AiJobAdmissionRefusal {\n /**\n * 429: what the month's AI credits have left cannot pay for it (AGL-3660);\n * 409: a Free job past what is left, waiting for the person's go-ahead\n * (AGL-3722) — `code` and `credits` say what to ask.\n */\n status: 400 | 403 | 404 | 409 | 429\n /** Customer-safe: the door answers with it as it stands. */\n error: string\n /** `AI_CREDITS_CONFIRM_CODE` on a 409 that asks for the go-ahead. */\n code?: string\n /** The figures and choices that 409 asks with. */\n credits?: AiCreditsPrompt\n}\n\nexport type AiJobAdmission = (\n context: AiJobAdmissionContext,\n) => Promise<AiJobAdmissionRefusal | null>\n\nconst admissions = new Map<AiJobKind, AiJobAdmission>()\n\n/** Idempotent per kind; the last registration wins, and `null` unregisters. */\nexport function registerAiJobAdmission(kind: AiJobKind, admission: AiJobAdmission | null): void {\n if (admission) admissions.set(kind, admission)\n else admissions.delete(kind)\n}\n\n/** The kind's registered admission, or `null` when it has none. */\nexport function aiJobAdmissionFor(kind: AiJobKind): AiJobAdmission | null {\n return admissions.get(kind) ?? null\n}\n\n/** The kind's refusal, or `null` when it admits the job or has nothing to check. */\nexport async function aiJobAdmissionRefusal(\n kind: AiJobKind,\n context: AiJobAdmissionContext,\n): Promise<AiJobAdmissionRefusal | null> {\n const admission = admissions.get(kind)\n return admission ? admission(context) : null\n}\n\n/**\n * The refusal for a job on a site that switched AI off (AGL-3028), or `null`.\n *\n * Every kind meets it, and every door asks it BEFORE the kind's own check —\n * the create door, each site of an agency batch, and a resume — so no job\n * comes to exist, or runs another step, for a site that said no, and a\n * switched-off site never pays for a kind's reads. A job with no site is\n * workspace-level and always passes.\n */\nexport async function aiJobSiteRefusal(\n context: Pick<AiJobAdmissionContext, 'firestore' | 'org' | 'hostId'>,\n): Promise<AiJobAdmissionRefusal | null> {\n return (await isAiOffForSite(context.firestore, context.org, context.hostId))\n ? { status: AI_OFF_FOR_SITE_STATUS, error: AI_OFF_FOR_SITE_COPY }\n : null\n}\n"],"names":["AI_OFF_FOR_SITE_COPY","AI_OFF_FOR_SITE_STATUS","isAiOffForSite","admissions","Map","registerAiJobAdmission","kind","admission","set","delete","aiJobAdmissionFor","get","aiJobAdmissionRefusal","context","aiJobSiteRefusal","firestore","org","hostId","status","error"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAID,SACEA,oBAAoB,EACpBC,sBAAsB,EACtBC,cAAc,QACT,6BAAyB;AA6EhC,MAAMC,aAAa,IAAIC;AAEvB,6EAA6E,GAC7E,OAAO,SAASC,uBAAuBC,IAAe,EAAEC,SAAgC;IACtF,IAAIA,WAAWJ,WAAWK,GAAG,CAACF,MAAMC;SAC/BJ,WAAWM,MAAM,CAACH;AACzB;AAEA,iEAAiE,GACjE,OAAO,SAASI,kBAAkBJ,IAAe;QACxCH;IAAP,QAAOA,kBAAAA,WAAWQ,GAAG,CAACL,iBAAfH,kBAAwB;AACjC;AAEA,kFAAkF,GAClF,OAAO,eAAeS,sBACpBN,IAAe,EACfO,OAA8B;IAE9B,MAAMN,YAAYJ,WAAWQ,GAAG,CAACL;IACjC,OAAOC,YAAYA,UAAUM,WAAW;AAC1C;AAEA;;;;;;;;CAQC,GACD,OAAO,eAAeC,iBACpBD,OAAoE;IAEpE,OAAO,AAAC,MAAMX,eAAeW,QAAQE,SAAS,EAAEF,QAAQG,GAAG,EAAEH,QAAQI,MAAM,IACvE;QAAEC,QAAQjB;QAAwBkB,OAAOnB;IAAqB,IAC9D;AACN"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-admission.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 { AiCreditsPrompt } from '../model/ai-credit-estimate'\nimport type { AiJobKind, AiJobPlan } from '../model/ai-jobs.types'\nimport {\n AI_OFF_FOR_SITE_COPY,\n AI_OFF_FOR_SITE_STATUS,\n isAiOffForSite,\n} from '../model/ai-site-switch'\n\n/**\n * What a job kind checks before a job of it runs (AGL-2909).\n *\n * The create door parses the body every kind shares — the org, the site, the\n * brief, scalar inputs — and nothing the machine knows says whether THIS kind\n * can finish: a layout needs a site to write to, a page template names the\n * collection it renders, and both write a document the site's plan counts.\n * A job that cannot finish should never start, because a planned kind spends\n * on its plan before it writes anything. So a kind registers one check, the\n * create door asks it after the gate ladder admitted the request and before\n * the job exists, and the resume door asks it again before a confirmed plan\n * runs, since an allowance free at creation may be used by now. Either way\n * a refusal hands the reservation back and spends nothing.\n *\n * The resume door also hands the check the plan being confirmed (AGL-2907),\n * so a kind that can build only some plans — a page job builds one screen\n * from what the site has — refuses the others when a member confirms them,\n * not a beat later from inside the step.\n *\n * The registry lives apart from the machine so the doors read it without the\n * generators, and a generator registers here beside its runner.\n */\n\nexport interface AiJobAdmissionContext {\n /** The Admin SDK handle the door runs on. */\n firestore: FirebaseFirestore.Firestore\n orgId: string\n hostId: string | null\n /**\n * The create door's check, asked before \"Create site\" makes the site the\n * job is for (AGL-3859): `hostId` is null because the site does not exist\n * yet, and a kind that needs one answers for everything but the site.\n */\n siteToCome?: boolean\n /** The job's kind-specific inputs, as the door parsed or the job stored them. */\n inputs: Readonly<Record<string, unknown>>\n /** The org document the gate ladder read; a check narrows it to the fields it reads. */\n org: object | null\n /**\n * The job's plan when the door is resuming a job that has one; absent at\n * creation, when no plan exists yet.\n */\n plan?: AiJobPlan | null\n /**\n * The member whose request spends: the creator at the create door, the\n * member confirming at the resume door. A kind whose draft another plugin\n * writes asks that plugin whether this member may create one.\n */\n uid?: string | null\n /**\n * The person chose \"Build what fits\" (AGL-3722): a Free job whose p90 is\n * more than what is left is admitted, to build as much as it can and pause\n * with Resume. Unset, such a job is answered with the prompt instead.\n */\n creditsConfirmed?: boolean\n /**\n * Told the prompt a confirmed Free job was admitted past (AGL-3722), so the\n * door records the confirmation on the job: who, when, what was left.\n */\n onCreditsConfirmed?: (prompt: AiCreditsPrompt) => void\n}\n\nexport interface AiJobAdmissionRefusal {\n /**\n * 429: what the month's AI credits have left cannot pay for it (AGL-3660);\n * 409: a Free job past what is left, waiting for the person's go-ahead\n * (AGL-3722) — `code` and `credits` say what to ask.\n */\n status: 400 | 403 | 404 | 409 | 429\n /** Customer-safe: the door answers with it as it stands. */\n error: string\n /** `AI_CREDITS_CONFIRM_CODE` on a 409 that asks for the go-ahead. */\n code?: string\n /** The figures and choices that 409 asks with. */\n credits?: AiCreditsPrompt\n}\n\nexport type AiJobAdmission = (\n context: AiJobAdmissionContext,\n) => Promise<AiJobAdmissionRefusal | null>\n\nconst admissions = new Map<AiJobKind, AiJobAdmission>()\n\n/** Idempotent per kind; the last registration wins, and `null` unregisters. */\nexport function registerAiJobAdmission(kind: AiJobKind, admission: AiJobAdmission | null): void {\n if (admission) admissions.set(kind, admission)\n else admissions.delete(kind)\n}\n\n/** The kind's registered admission, or `null` when it has none. */\nexport function aiJobAdmissionFor(kind: AiJobKind): AiJobAdmission | null {\n return admissions.get(kind) ?? null\n}\n\n/** The kind's refusal, or `null` when it admits the job or has nothing to check. */\nexport async function aiJobAdmissionRefusal(\n kind: AiJobKind,\n context: AiJobAdmissionContext,\n): Promise<AiJobAdmissionRefusal | null> {\n const admission = admissions.get(kind)\n return admission ? admission(context) : null\n}\n\n/**\n * The refusal for a job on a site that switched AI off (AGL-3028), or `null`.\n *\n * Every kind meets it, and every door asks it BEFORE the kind's own check —\n * the create door, each site of an agency batch, and a resume — so no job\n * comes to exist, or runs another step, for a site that said no, and a\n * switched-off site never pays for a kind's reads. A job with no site is\n * workspace-level and always passes.\n */\nexport async function aiJobSiteRefusal(\n context: Pick<AiJobAdmissionContext, 'firestore' | 'org' | 'hostId'>,\n): Promise<AiJobAdmissionRefusal | null> {\n return (await isAiOffForSite(context.firestore, context.org, context.hostId))\n ? { status: AI_OFF_FOR_SITE_STATUS, error: AI_OFF_FOR_SITE_COPY }\n : null\n}\n"],"names":["AI_OFF_FOR_SITE_COPY","AI_OFF_FOR_SITE_STATUS","isAiOffForSite","admissions","Map","registerAiJobAdmission","kind","admission","set","delete","aiJobAdmissionFor","get","aiJobAdmissionRefusal","context","aiJobSiteRefusal","firestore","org","hostId","status","error"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAID,SACEA,oBAAoB,EACpBC,sBAAsB,EACtBC,cAAc,QACT,6BAAyB;AAmFhC,MAAMC,aAAa,IAAIC;AAEvB,6EAA6E,GAC7E,OAAO,SAASC,uBAAuBC,IAAe,EAAEC,SAAgC;IACtF,IAAIA,WAAWJ,WAAWK,GAAG,CAACF,MAAMC;SAC/BJ,WAAWM,MAAM,CAACH;AACzB;AAEA,iEAAiE,GACjE,OAAO,SAASI,kBAAkBJ,IAAe;QACxCH;IAAP,QAAOA,kBAAAA,WAAWQ,GAAG,CAACL,iBAAfH,kBAAwB;AACjC;AAEA,kFAAkF,GAClF,OAAO,eAAeS,sBACpBN,IAAe,EACfO,OAA8B;IAE9B,MAAMN,YAAYJ,WAAWQ,GAAG,CAACL;IACjC,OAAOC,YAAYA,UAAUM,WAAW;AAC1C;AAEA;;;;;;;;CAQC,GACD,OAAO,eAAeC,iBACpBD,OAAoE;IAEpE,OAAO,AAAC,MAAMX,eAAeW,QAAQE,SAAS,EAAEF,QAAQG,GAAG,EAAEH,QAAQI,MAAM,IACvE;QAAEC,QAAQjB;QAAwBkB,OAAOnB;IAAqB,IAC9D;AACN"}
@@ -764,17 +764,21 @@ export const AI_SITE_NO_PLAN_COPY = 'This site job has no confirmed plan to buil
764
764
  error: shape
765
765
  };
766
766
  }
767
- if (!context.hostId) {
767
+ if (!context.hostId && !context.siteToCome) {
768
768
  return {
769
769
  status: 400,
770
770
  error: 'Open the site the scaffold is for before starting the job'
771
771
  };
772
772
  }
773
- const owner = await resolveOrgIdForHost(context.hostId);
774
- if (!owner || owner !== context.orgId) return {
775
- status: 404,
776
- error: 'Unknown site'
777
- };
773
+ // A site still to come (AGL-3859) is the one "Create site" makes for
774
+ // this org once everything else here has said yes.
775
+ if (context.hostId) {
776
+ const owner = await resolveOrgIdForHost(context.hostId);
777
+ if (!owner || owner !== context.orgId) return {
778
+ status: 404,
779
+ error: 'Unknown site'
780
+ };
781
+ }
778
782
  if (!aiJobStepRunnerFor('page')) {
779
783
  return {
780
784
  status: 400,