@aglyn/plugins-ai 1.0.0-beta.227 → 1.0.0-beta.228

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 (92) hide show
  1. package/README.md +1 -1
  2. package/package.json +17 -17
  3. package/src/lib/components/ai-brief-dialog.component.js +1 -1
  4. package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
  5. package/src/lib/components/ai-describe-automation.component.js +7 -2
  6. package/src/lib/components/ai-describe-automation.component.js.map +1 -1
  7. package/src/lib/components/ai-describe-button.component.js +6 -1
  8. package/src/lib/components/ai-describe-button.component.js.map +1 -1
  9. package/src/lib/components/ai-describe-page.component.d.ts +1 -1
  10. package/src/lib/components/ai-describe-page.component.js +1 -1
  11. package/src/lib/components/ai-describe-page.component.js.map +1 -1
  12. package/src/lib/components/ai-job-events.d.ts +7 -1
  13. package/src/lib/components/ai-job-events.js +4 -2
  14. package/src/lib/components/ai-job-events.js.map +1 -1
  15. package/src/lib/components/ai-job-site.d.ts +22 -0
  16. package/src/lib/components/ai-job-site.js +73 -0
  17. package/src/lib/components/ai-job-site.js.map +1 -0
  18. package/src/lib/components/ai-jobs-list-page.component.d.ts +23 -0
  19. package/src/lib/components/ai-jobs-list-page.component.js +303 -0
  20. package/src/lib/components/ai-jobs-list-page.component.js.map +1 -0
  21. package/src/lib/components/ai-jobs-page.component.d.ts +7 -0
  22. package/src/lib/components/ai-jobs-page.component.js +30 -0
  23. package/src/lib/components/ai-jobs-page.component.js.map +1 -0
  24. package/src/lib/components/ai-page-brief-dialog.component.d.ts +1 -1
  25. package/src/lib/components/ai-page-brief-dialog.component.js.map +1 -1
  26. package/src/lib/components/ai-site-build-page.component.d.ts +30 -2
  27. package/src/lib/components/ai-site-build-page.component.js +359 -241
  28. package/src/lib/components/ai-site-build-page.component.js.map +1 -1
  29. package/src/lib/components/ai-site-start-card.component.d.ts +8 -0
  30. package/src/lib/components/ai-site-start-card.component.js +20 -11
  31. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  32. package/src/lib/components/assist-panel.component.js +9 -4
  33. package/src/lib/components/assist-panel.component.js.map +1 -1
  34. package/src/lib/jobs/ai-job-draft-ids.d.ts +6 -0
  35. package/src/lib/jobs/ai-job-draft-ids.js +9 -0
  36. package/src/lib/jobs/ai-job-draft-ids.js.map +1 -1
  37. package/src/lib/jobs/ai-job-drafts.d.ts +7 -0
  38. package/src/lib/jobs/ai-job-drafts.js +16 -7
  39. package/src/lib/jobs/ai-job-drafts.js.map +1 -1
  40. package/src/lib/jobs/ai-job-layout-step.js +2 -1
  41. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  42. package/src/lib/jobs/ai-job-page-sections.d.ts +6 -5
  43. package/src/lib/jobs/ai-job-page-sections.js +3 -3
  44. package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
  45. package/src/lib/jobs/ai-job-page-step.d.ts +19 -1
  46. package/src/lib/jobs/ai-job-page-step.js +50 -8
  47. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  48. package/src/lib/jobs/ai-job-plan-step.js +7 -2
  49. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  50. package/src/lib/jobs/ai-job-site-step.d.ts +21 -0
  51. package/src/lib/jobs/ai-job-site-step.js +96 -17
  52. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  53. package/src/lib/jobs/ai-job-text-step.d.ts +7 -1
  54. package/src/lib/jobs/ai-job-text-step.js.map +1 -1
  55. package/src/lib/jobs/ai-jobs.d.ts +9 -1
  56. package/src/lib/jobs/ai-jobs.js +9 -2
  57. package/src/lib/jobs/ai-jobs.js.map +1 -1
  58. package/src/lib/jobs/ai-site-publish.d.ts +98 -0
  59. package/src/lib/jobs/ai-site-publish.js +372 -0
  60. package/src/lib/jobs/ai-site-publish.js.map +1 -0
  61. package/src/lib/jobs/fixtures/ai-page-briefs.d.ts +7 -6
  62. package/src/lib/jobs/fixtures/ai-page-briefs.js +23 -56
  63. package/src/lib/jobs/fixtures/ai-page-briefs.js.map +1 -1
  64. package/src/lib/model/ai-job-notice.d.ts +3 -1
  65. package/src/lib/model/ai-job-notice.js +18 -1
  66. package/src/lib/model/ai-job-notice.js.map +1 -1
  67. package/src/lib/model/ai-jobs.types.d.ts +27 -0
  68. package/src/lib/model/ai-jobs.types.js.map +1 -1
  69. package/src/lib/model/ai-plan-capabilities.d.ts +25 -10
  70. package/src/lib/model/ai-plan-capabilities.js +15 -2
  71. package/src/lib/model/ai-plan-capabilities.js.map +1 -1
  72. package/src/lib/model/ai-site-build-progress.d.ts +17 -1
  73. package/src/lib/model/ai-site-build-progress.js +64 -4
  74. package/src/lib/model/ai-site-build-progress.js.map +1 -1
  75. package/src/lib/model/ai-site-start-seo.d.ts +19 -0
  76. package/src/lib/model/ai-site-start-seo.js +91 -3
  77. package/src/lib/model/ai-site-start-seo.js.map +1 -1
  78. package/src/lib/plugin.js +18 -10
  79. package/src/lib/plugin.js.map +1 -1
  80. package/src/lib/runtime/ai-doctrine-validators.d.ts +16 -12
  81. package/src/lib/runtime/ai-doctrine-validators.js +19 -36
  82. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  83. package/src/lib/runtime/ai-doctrine.js +1 -1
  84. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  85. package/src/lib/runtime/ai-eval-live.d.ts +1 -1
  86. package/src/lib/runtime/ai-eval-live.js +1 -1
  87. package/src/lib/runtime/ai-eval-live.js.map +1 -1
  88. package/src/lib/server/ai-jobs-live-sites.d.ts +26 -0
  89. package/src/lib/server/ai-jobs-live-sites.js +58 -0
  90. package/src/lib/server/ai-jobs-live-sites.js.map +1 -0
  91. package/src/lib/server/ai-jobs-route.js +22 -3
  92. package/src/lib/server/ai-jobs-route.js.map +1 -1
@@ -63,12 +63,93 @@ import { aiSiteWords } from "./ai-site-job.js";
63
63
  /** The kind an `seo` output carries when it is a guided start's site listing. */ export const AI_SITE_SEO_PROPOSAL_KIND = 'site-listing';
64
64
  /** The id the scaffold reports its one site listing under. */ export const AI_SITE_SEO_OUTPUT_ID = 'site:listing';
65
65
  /** What the proposal says about itself wherever it is shown. */ export const AI_SITE_SEO_NOTE = 'Written from what you said the site is and who it is for, not from its pages — read it before you save it.';
66
- /** Held to a length, cut at a word rather than mid-word. */ function clip(value, max) {
66
+ /**
67
+ * Words a phrase never ends on (AGL-3596): a title cut after "for" or "that"
68
+ * reads as broken mid-sentence, which a search result shows to everybody.
69
+ */ const DANGLING = new Set([
70
+ 'a',
71
+ 'an',
72
+ 'and',
73
+ 'as',
74
+ 'at',
75
+ 'by',
76
+ 'for',
77
+ 'from',
78
+ 'in',
79
+ 'into',
80
+ 'of',
81
+ 'on',
82
+ 'or',
83
+ 'so',
84
+ 'that',
85
+ 'the',
86
+ 'to',
87
+ 'which',
88
+ 'who',
89
+ 'with',
90
+ 'where',
91
+ 'whose',
92
+ 'while',
93
+ 'your',
94
+ 'our',
95
+ '—',
96
+ '-',
97
+ '–',
98
+ '&'
99
+ ]);
100
+ /** Without the words a phrase never ends on, or the punctuation before them. */ function withoutDangling(text) {
101
+ const words = text.split(' ');
102
+ while(words.length > 1 && DANGLING.has(words[words.length - 1].toLowerCase()))words.pop();
103
+ return words.join(' ').replace(/[\s,;:—–-]+$/, '').trim();
104
+ }
105
+ /** Held to a length, cut at a word rather than mid-word, and never on a dangling word. */ function clip(value, max) {
67
106
  const text = value.replace(/\s+/g, ' ').trim();
68
107
  if (text.length <= max) return text;
69
108
  const cut = text.slice(0, max + 1);
70
109
  const space = cut.lastIndexOf(' ');
71
- return (space > max / 2 ? cut.slice(0, space) : text.slice(0, max)).trim();
110
+ return withoutDangling((space > max / 2 ? cut.slice(0, space) : text.slice(0, max)).trim());
111
+ }
112
+ /**
113
+ * Where the first clause of an answer ends: "a dog groomer in Austin THAT
114
+ * takes bookings for…" is a subject and then a sentence about it. A title
115
+ * keeps the subject.
116
+ */ const CLAUSE_BOUNDARY = /\s+(?:that|which|who|whose|where|offering|providing|specializing|serving|helping|so|because|with|for|to|by|and|but)\b|[,;:(—–]|\s-\s/i;
117
+ /**
118
+ * A short search title (AGL-3596): the business's name, what it is and where,
119
+ * held to the title cap and never cut mid-phrase. The answer to "what kind of
120
+ * site" is often a whole sentence, and cutting that sentence at sixty
121
+ * characters published "…that takes bookings for" as a site's title.
122
+ *
123
+ * The longest of these that fits wins: the whole answer; its first clause
124
+ * (the subject before "that", "for", a comma…); that clause without its
125
+ * place. The city the person gave is added where the words do not already
126
+ * say it, and the name leads where there is one. Only when even the shortest
127
+ * does not fit is it cut, at a word, never on a dangling one.
128
+ */ export function aiSiteSeoTitle(input) {
129
+ var _input_name, _input_city;
130
+ const max = AI_SITE_SEO_LIMITS.title;
131
+ const name = ((_input_name = input.name) != null ? _input_name : '').replace(/\s+/g, ' ').trim();
132
+ const subject = withoutDangling(input.subject.replace(/\s+/g, ' ').trim());
133
+ const city = ((_input_city = input.city) != null ? _input_city : '').replace(/\s+/g, ' ').trim();
134
+ const placed = (what)=>city && !what.toLowerCase().includes(city.toLowerCase()) ? `${what} in ${city}` : what;
135
+ const boundary = subject.search(CLAUSE_BOUNDARY);
136
+ const clause = withoutDangling(boundary > 0 ? subject.slice(0, boundary) : subject);
137
+ const bare = withoutDangling(clause.replace(/\s+in\s+[^]*$/i, '')) || clause;
138
+ const whats = [
139
+ ...new Set([
140
+ placed(subject),
141
+ subject,
142
+ placed(clause),
143
+ clause,
144
+ bare
145
+ ])
146
+ ];
147
+ const candidates = name ? [
148
+ ...whats.map((what)=>`${name} — ${what}`),
149
+ name
150
+ ] : whats;
151
+ const fits = candidates.find((candidate)=>candidate.length <= max);
152
+ return fits != null ? fits : clip(candidates[candidates.length - 1], max);
72
153
  }
73
154
  /**
74
155
  * The leading article dropped and the first letter raised: "a neighborhood
@@ -98,7 +179,11 @@ import { aiSiteWords } from "./ai-site-job.js";
98
179
  if (!subject) return null;
99
180
  const name = ((_input_siteName = input.siteName) != null ? _input_siteName : '').replace(/\s+/g, ' ').trim();
100
181
  const audience = ((_input_audience = input.audience) != null ? _input_audience : '').replace(/\s+/g, ' ').trim();
101
- const title = clip(name ? `${name} — ${subject}` : subject, AI_SITE_SEO_LIMITS.title);
182
+ const title = aiSiteSeoTitle({
183
+ name,
184
+ subject,
185
+ city: input.city
186
+ });
102
187
  const described = name ? `${name} is ${(_input_about1 = input.about) == null ? void 0 : _input_about1.trim()}` : subject;
103
188
  const description = clip(sentence(audience ? `${described}, for ${audience}` : described), AI_SITE_SEO_LIMITS.description);
104
189
  return {
@@ -115,8 +200,11 @@ import { aiSiteWords } from "./ai-site-job.js";
115
200
  /** The proposal a job's inputs imply, for the step that reports it. */ export function aiSiteSeoProposalForInputs(inputs) {
116
201
  const words = aiSiteWords(inputs);
117
202
  const businessName = inputs == null ? void 0 : inputs['businessName'];
203
+ const city = inputs == null ? void 0 : inputs['city'];
118
204
  return aiSiteSeoProposal(_extends({}, words, typeof businessName === 'string' ? {
119
205
  siteName: businessName
206
+ } : {}, typeof city === 'string' ? {
207
+ city
120
208
  } : {}));
121
209
  }
122
210
  /** Whether a value is a record the reader may look inside. */ function isRecord(value) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-start-seo.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 { SCREEN_SEO_TEXT_GUIDANCE } from '@aglyn/aglyn/app-utils/screen-seo-fields'\nimport type { AiJobOutput } from './ai-jobs.types'\nimport { aiSiteWords, type AiSiteWords } from './ai-site-job'\n\n/**\n * The site's own search listing, from the answers (AGL-2918).\n *\n * A site has two SEO title-and-description pairs. A page's is the page's, and\n * the scaffold's page step writes one for each page it builds. The SITE's is\n * the fallback every page with none of its own publishes, and it is the one\n * nothing was filling: a newly created site carries whatever its creation\n * seeded, which is its subdomain or its display name, and a scaffolded site\n * would publish pages listed under that.\n *\n * ── Derived, not generated ───────────────────────────────────────────────\n *\n * The two values here are arithmetic on what the person typed, not a model's\n * writing, and that is deliberate on three counts. The answers are already\n * the sentence the fallback wants — \"what kind of site\" IS what the site is,\n * and \"who it is for\" IS who it is for. It costs nothing, on a job that is\n * already tens of model calls. And it is available on the scaffold's first\n * pass, so the proposal is waiting while the pages are still building rather\n * than arriving after them.\n *\n * ── A proposal, never a write ────────────────────────────────────────────\n *\n * Nothing here writes the site's settings. The values travel as a job output\n * and land in the site SEO form as unsaved edits through the `hostSeo` zone's\n * own `proposeDraft`; the form's Update is what stores them, and a person who\n * never opens that form is left with exactly what they had.\n *\n * Pure: strings in, strings out, no Firestore and no React.\n */\n\n/**\n * The site SEO form's field names, which are what a proposal is keyed by.\n *\n * The same two strings the host settings SEO section names its Title and\n * Description inputs. They are stated here rather than imported because the\n * form is the console's and this is a plugin: the zone's `proposeDraft`\n * ignores a key the form does not edit, so a name that drifted would stage\n * nothing rather than stage something wrong.\n */\nexport const AI_SITE_SEO_FORM_FIELDS = {\n title: 'seo.title',\n description: 'seo.description',\n} as const\n\nexport type AiSiteSeoFormField =\n (typeof AI_SITE_SEO_FORM_FIELDS)[keyof typeof AI_SITE_SEO_FORM_FIELDS]\n\n/**\n * What each value is held to: the lengths the SEO editors guide a listing to,\n * which are the same two the host settings form counts down from.\n */\nexport const AI_SITE_SEO_LIMITS = SCREEN_SEO_TEXT_GUIDANCE\n\n/** The kind an `seo` output carries when it is a guided start's site listing. */\nexport const AI_SITE_SEO_PROPOSAL_KIND = 'site-listing'\n\n/** The id the scaffold reports its one site listing under. */\nexport const AI_SITE_SEO_OUTPUT_ID = 'site:listing'\n\nexport interface AiSiteSeoProposal {\n kind: typeof AI_SITE_SEO_PROPOSAL_KIND\n /** Values for the site SEO form, by field name. */\n values: Record<AiSiteSeoFormField, string>\n /** Customer-safe lines about where the words came from. */\n notes: string[]\n}\n\n/** What the proposal says about itself wherever it is shown. */\nexport const AI_SITE_SEO_NOTE =\n 'Written from what you said the site is and who it is for, not from its pages — read it before you save it.'\n\n/** Held to a length, cut at a word rather than mid-word. */\nfunction clip(value: string, max: number): string {\n const text = value.replace(/\\s+/g, ' ').trim()\n if (text.length <= max) return text\n const cut = text.slice(0, max + 1)\n const space = cut.lastIndexOf(' ')\n return (space > max / 2 ? cut.slice(0, space) : text.slice(0, max)).trim()\n}\n\n/**\n * The leading article dropped and the first letter raised: \"a neighborhood\n * dog groomer\" is how a person answers the question and is not how a title\n * starts. Anything the person already capitalized is left alone, so a name\n * typed as the site type survives.\n */\nfunction asSubject(about: string): string {\n // `\\b` rather than `\\s+`, so an answer that is ONLY an article leaves\n // nothing behind and the caller proposes nothing: a one-letter title is a\n // worse thing to stage into a required field than no proposal at all.\n const text = about.replace(/\\s+/g, ' ').trim().replace(/^(?:an?|the)\\b\\s*/i, '')\n if (!text) return ''\n return text[0].toUpperCase() + text.slice(1)\n}\n\n/** A sentence closed with one period, and never two. */\nfunction sentence(text: string): string {\n const trimmed = text.trim()\n return /[.!?]$/.test(trimmed) ? trimmed : `${trimmed}.`\n}\n\n/**\n * What the listing is built from: either answer may be missing, because the\n * guided start requires only the first of them.\n */\nexport interface AiSiteSeoInput extends Partial<AiSiteWords> {\n /** The site's own name where the job carries one; empty for a site named on its pages. */\n siteName?: string\n}\n\n/**\n * The site's fallback listing from the answers, or `null` when the answers\n * describe no site at all — a job with no `businessType` has nothing to\n * propose, and an empty proposal is worse than none.\n */\nexport function aiSiteSeoProposal(input: AiSiteSeoInput): AiSiteSeoProposal | null {\n const subject = asSubject(input.about ?? '')\n if (!subject) return null\n const name = (input.siteName ?? '').replace(/\\s+/g, ' ').trim()\n const audience = (input.audience ?? '').replace(/\\s+/g, ' ').trim()\n const title = clip(name ? `${name} — ${subject}` : subject, AI_SITE_SEO_LIMITS.title)\n const described = name ? `${name} is ${input.about?.trim()}` : subject\n const description = clip(\n sentence(audience ? `${described}, for ${audience}` : described),\n AI_SITE_SEO_LIMITS.description,\n )\n return {\n kind: AI_SITE_SEO_PROPOSAL_KIND,\n values: {\n [AI_SITE_SEO_FORM_FIELDS.title]: title,\n [AI_SITE_SEO_FORM_FIELDS.description]: description,\n },\n notes: [AI_SITE_SEO_NOTE],\n }\n}\n\n/** The proposal a job's inputs imply, for the step that reports it. */\nexport function aiSiteSeoProposalForInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiSiteSeoProposal | null {\n const words = aiSiteWords(inputs)\n const businessName = inputs?.['businessName']\n return aiSiteSeoProposal({\n ...words,\n ...(typeof businessName === 'string' ? { siteName: businessName } : {}),\n })\n}\n\n/** Whether a value is a record the reader may look inside. */\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/**\n * The site listing a job's outputs carry, or `null` when they carry none.\n *\n * Read defensively: an output is whatever a past release wrote, so a proposal\n * whose values are not two strings is treated as no proposal rather than\n * staged into somebody's live SEO form.\n */\nexport function aiSiteSeoProposalOf(\n outputs: readonly Pick<AiJobOutput, 'resource' | 'proposal'>[] | null | undefined,\n): AiSiteSeoProposal | null {\n for (const output of outputs ?? []) {\n if (output.resource !== 'seo') continue\n const proposal = output.proposal\n if (!isRecord(proposal) || proposal['kind'] !== AI_SITE_SEO_PROPOSAL_KIND) continue\n const values = isRecord(proposal['values']) ? proposal['values'] : {}\n const title = values[AI_SITE_SEO_FORM_FIELDS.title]\n const description = values[AI_SITE_SEO_FORM_FIELDS.description]\n if (typeof title !== 'string' || typeof description !== 'string') continue\n if (!title.trim() || !description.trim()) continue\n const notes = Array.isArray(proposal['notes'])\n ? proposal['notes'].filter((note): note is string => typeof note === 'string')\n : []\n return {\n kind: AI_SITE_SEO_PROPOSAL_KIND,\n values: {\n [AI_SITE_SEO_FORM_FIELDS.title]: title,\n [AI_SITE_SEO_FORM_FIELDS.description]: description,\n },\n notes,\n }\n }\n return null\n}\n"],"names":["SCREEN_SEO_TEXT_GUIDANCE","aiSiteWords","AI_SITE_SEO_FORM_FIELDS","title","description","AI_SITE_SEO_LIMITS","AI_SITE_SEO_PROPOSAL_KIND","AI_SITE_SEO_OUTPUT_ID","AI_SITE_SEO_NOTE","clip","value","max","text","replace","trim","length","cut","slice","space","lastIndexOf","asSubject","about","toUpperCase","sentence","trimmed","test","aiSiteSeoProposal","input","subject","name","siteName","audience","described","kind","values","notes","aiSiteSeoProposalForInputs","inputs","words","businessName","isRecord","Array","isArray","aiSiteSeoProposalOf","outputs","output","resource","proposal","filter","note"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,wBAAwB,QAAQ,2CAA0C;AAEnF,SAASC,WAAW,QAA0B,mBAAe;AAE7D;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BC,GAED;;;;;;;;CAQC,GACD,OAAO,MAAMC,0BAA0B;IACrCC,OAAO;IACPC,aAAa;AACf,EAAU;AAKV;;;CAGC,GACD,OAAO,MAAMC,qBAAqBL,yBAAwB;AAE1D,+EAA+E,GAC/E,OAAO,MAAMM,4BAA4B,eAAc;AAEvD,4DAA4D,GAC5D,OAAO,MAAMC,wBAAwB,eAAc;AAUnD,8DAA8D,GAC9D,OAAO,MAAMC,mBACX,6GAA4G;AAE9G,0DAA0D,GAC1D,SAASC,KAAKC,KAAa,EAAEC,GAAW;IACtC,MAAMC,OAAOF,MAAMG,OAAO,CAAC,QAAQ,KAAKC,IAAI;IAC5C,IAAIF,KAAKG,MAAM,IAAIJ,KAAK,OAAOC;IAC/B,MAAMI,MAAMJ,KAAKK,KAAK,CAAC,GAAGN,MAAM;IAChC,MAAMO,QAAQF,IAAIG,WAAW,CAAC;IAC9B,OAAO,AAACD,CAAAA,QAAQP,MAAM,IAAIK,IAAIC,KAAK,CAAC,GAAGC,SAASN,KAAKK,KAAK,CAAC,GAAGN,IAAG,EAAGG,IAAI;AAC1E;AAEA;;;;;CAKC,GACD,SAASM,UAAUC,KAAa;IAC9B,sEAAsE;IACtE,0EAA0E;IAC1E,sEAAsE;IACtE,MAAMT,OAAOS,MAAMR,OAAO,CAAC,QAAQ,KAAKC,IAAI,GAAGD,OAAO,CAAC,sBAAsB;IAC7E,IAAI,CAACD,MAAM,OAAO;IAClB,OAAOA,IAAI,CAAC,EAAE,CAACU,WAAW,KAAKV,KAAKK,KAAK,CAAC;AAC5C;AAEA,sDAAsD,GACtD,SAASM,SAASX,IAAY;IAC5B,MAAMY,UAAUZ,KAAKE,IAAI;IACzB,OAAO,SAASW,IAAI,CAACD,WAAWA,UAAU,GAAGA,QAAQ,CAAC,CAAC;AACzD;AAWA;;;;CAIC,GACD,OAAO,SAASE,kBAAkBC,KAAqB;QAC3BA,cAEZA,iBACIA;QAEqBA;IALvC,MAAMC,UAAUR,WAAUO,eAAAA,MAAMN,KAAK,YAAXM,eAAe;IACzC,IAAI,CAACC,SAAS,OAAO;IACrB,MAAMC,OAAO,EAACF,kBAAAA,MAAMG,QAAQ,YAAdH,kBAAkB,IAAId,OAAO,CAAC,QAAQ,KAAKC,IAAI;IAC7D,MAAMiB,WAAW,EAACJ,kBAAAA,MAAMI,QAAQ,YAAdJ,kBAAkB,IAAId,OAAO,CAAC,QAAQ,KAAKC,IAAI;IACjE,MAAMX,QAAQM,KAAKoB,OAAO,GAAGA,KAAK,GAAG,EAAED,SAAS,GAAGA,SAASvB,mBAAmBF,KAAK;IACpF,MAAM6B,YAAYH,OAAO,GAAGA,KAAK,IAAI,GAAEF,gBAAAA,MAAMN,KAAK,qBAAXM,cAAab,IAAI,IAAI,GAAGc;IAC/D,MAAMxB,cAAcK,KAClBc,SAASQ,WAAW,GAAGC,UAAU,MAAM,EAAED,UAAU,GAAGC,YACtD3B,mBAAmBD,WAAW;IAEhC,OAAO;QACL6B,MAAM3B;QACN4B,QAAQ;YACN,CAAChC,wBAAwBC,KAAK,CAAC,EAAEA;YACjC,CAACD,wBAAwBE,WAAW,CAAC,EAAEA;QACzC;QACA+B,OAAO;YAAC3B;SAAiB;IAC3B;AACF;AAEA,qEAAqE,GACrE,OAAO,SAAS4B,2BACdC,MAA4D;IAE5D,MAAMC,QAAQrC,YAAYoC;IAC1B,MAAME,eAAeF,0BAAAA,MAAQ,CAAC,eAAe;IAC7C,OAAOX,kBAAkB,aACpBY,OACC,OAAOC,iBAAiB,WAAW;QAAET,UAAUS;IAAa,IAAI,CAAC;AAEzE;AAEA,4DAA4D,GAC5D,SAASC,SAAS9B,KAAc;IAC9B,OAAO,OAAOA,UAAU,YAAYA,UAAU,QAAQ,CAAC+B,MAAMC,OAAO,CAAChC;AACvE;AAEA;;;;;;CAMC,GACD,OAAO,SAASiC,oBACdC,OAAiF;IAEjF,KAAK,MAAMC,UAAUD,kBAAAA,UAAW,EAAE,CAAE;QAClC,IAAIC,OAAOC,QAAQ,KAAK,OAAO;QAC/B,MAAMC,WAAWF,OAAOE,QAAQ;QAChC,IAAI,CAACP,SAASO,aAAaA,QAAQ,CAAC,OAAO,KAAKzC,2BAA2B;QAC3E,MAAM4B,SAASM,SAASO,QAAQ,CAAC,SAAS,IAAIA,QAAQ,CAAC,SAAS,GAAG,CAAC;QACpE,MAAM5C,QAAQ+B,MAAM,CAAChC,wBAAwBC,KAAK,CAAC;QACnD,MAAMC,cAAc8B,MAAM,CAAChC,wBAAwBE,WAAW,CAAC;QAC/D,IAAI,OAAOD,UAAU,YAAY,OAAOC,gBAAgB,UAAU;QAClE,IAAI,CAACD,MAAMW,IAAI,MAAM,CAACV,YAAYU,IAAI,IAAI;QAC1C,MAAMqB,QAAQM,MAAMC,OAAO,CAACK,QAAQ,CAAC,QAAQ,IACzCA,QAAQ,CAAC,QAAQ,CAACC,MAAM,CAAC,CAACC,OAAyB,OAAOA,SAAS,YACnE,EAAE;QACN,OAAO;YACLhB,MAAM3B;YACN4B,QAAQ;gBACN,CAAChC,wBAAwBC,KAAK,CAAC,EAAEA;gBACjC,CAACD,wBAAwBE,WAAW,CAAC,EAAEA;YACzC;YACA+B;QACF;IACF;IACA,OAAO;AACT"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-start-seo.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 { SCREEN_SEO_TEXT_GUIDANCE } from '@aglyn/aglyn/app-utils/screen-seo-fields'\nimport type { AiJobOutput } from './ai-jobs.types'\nimport { aiSiteWords, type AiSiteWords } from './ai-site-job'\n\n/**\n * The site's own search listing, from the answers (AGL-2918).\n *\n * A site has two SEO title-and-description pairs. A page's is the page's, and\n * the scaffold's page step writes one for each page it builds. The SITE's is\n * the fallback every page with none of its own publishes, and it is the one\n * nothing was filling: a newly created site carries whatever its creation\n * seeded, which is its subdomain or its display name, and a scaffolded site\n * would publish pages listed under that.\n *\n * ── Derived, not generated ───────────────────────────────────────────────\n *\n * The two values here are arithmetic on what the person typed, not a model's\n * writing, and that is deliberate on three counts. The answers are already\n * the sentence the fallback wants — \"what kind of site\" IS what the site is,\n * and \"who it is for\" IS who it is for. It costs nothing, on a job that is\n * already tens of model calls. And it is available on the scaffold's first\n * pass, so the proposal is waiting while the pages are still building rather\n * than arriving after them.\n *\n * ── A proposal, never a write ────────────────────────────────────────────\n *\n * Nothing here writes the site's settings. The values travel as a job output\n * and land in the site SEO form as unsaved edits through the `hostSeo` zone's\n * own `proposeDraft`; the form's Update is what stores them, and a person who\n * never opens that form is left with exactly what they had.\n *\n * Pure: strings in, strings out, no Firestore and no React.\n */\n\n/**\n * The site SEO form's field names, which are what a proposal is keyed by.\n *\n * The same two strings the host settings SEO section names its Title and\n * Description inputs. They are stated here rather than imported because the\n * form is the console's and this is a plugin: the zone's `proposeDraft`\n * ignores a key the form does not edit, so a name that drifted would stage\n * nothing rather than stage something wrong.\n */\nexport const AI_SITE_SEO_FORM_FIELDS = {\n title: 'seo.title',\n description: 'seo.description',\n} as const\n\nexport type AiSiteSeoFormField =\n (typeof AI_SITE_SEO_FORM_FIELDS)[keyof typeof AI_SITE_SEO_FORM_FIELDS]\n\n/**\n * What each value is held to: the lengths the SEO editors guide a listing to,\n * which are the same two the host settings form counts down from.\n */\nexport const AI_SITE_SEO_LIMITS = SCREEN_SEO_TEXT_GUIDANCE\n\n/** The kind an `seo` output carries when it is a guided start's site listing. */\nexport const AI_SITE_SEO_PROPOSAL_KIND = 'site-listing'\n\n/** The id the scaffold reports its one site listing under. */\nexport const AI_SITE_SEO_OUTPUT_ID = 'site:listing'\n\nexport interface AiSiteSeoProposal {\n kind: typeof AI_SITE_SEO_PROPOSAL_KIND\n /** Values for the site SEO form, by field name. */\n values: Record<AiSiteSeoFormField, string>\n /** Customer-safe lines about where the words came from. */\n notes: string[]\n}\n\n/** What the proposal says about itself wherever it is shown. */\nexport const AI_SITE_SEO_NOTE =\n 'Written from what you said the site is and who it is for, not from its pages — read it before you save it.'\n\n/**\n * Words a phrase never ends on (AGL-3596): a title cut after \"for\" or \"that\"\n * reads as broken mid-sentence, which a search result shows to everybody.\n */\nconst DANGLING = new Set([\n 'a', 'an', 'and', 'as', 'at', 'by', 'for', 'from', 'in', 'into', 'of', 'on', 'or', 'so',\n 'that', 'the', 'to', 'which', 'who', 'with', 'where', 'whose', 'while', 'your', 'our', '—', '-', '–', '&',\n])\n\n/** Without the words a phrase never ends on, or the punctuation before them. */\nfunction withoutDangling(text: string): string {\n const words = text.split(' ')\n while (words.length > 1 && DANGLING.has(words[words.length - 1].toLowerCase())) words.pop()\n return words.join(' ').replace(/[\\s,;:—–-]+$/, '').trim()\n}\n\n/** Held to a length, cut at a word rather than mid-word, and never on a dangling word. */\nfunction clip(value: string, max: number): string {\n const text = value.replace(/\\s+/g, ' ').trim()\n if (text.length <= max) return text\n const cut = text.slice(0, max + 1)\n const space = cut.lastIndexOf(' ')\n return withoutDangling((space > max / 2 ? cut.slice(0, space) : text.slice(0, max)).trim())\n}\n\n/**\n * Where the first clause of an answer ends: \"a dog groomer in Austin THAT\n * takes bookings for…\" is a subject and then a sentence about it. A title\n * keeps the subject.\n */\nconst CLAUSE_BOUNDARY =\n /\\s+(?:that|which|who|whose|where|offering|providing|specializing|serving|helping|so|because|with|for|to|by|and|but)\\b|[,;:(—–]|\\s-\\s/i\n\n/**\n * A short search title (AGL-3596): the business's name, what it is and where,\n * held to the title cap and never cut mid-phrase. The answer to \"what kind of\n * site\" is often a whole sentence, and cutting that sentence at sixty\n * characters published \"…that takes bookings for\" as a site's title.\n *\n * The longest of these that fits wins: the whole answer; its first clause\n * (the subject before \"that\", \"for\", a comma…); that clause without its\n * place. The city the person gave is added where the words do not already\n * say it, and the name leads where there is one. Only when even the shortest\n * does not fit is it cut, at a word, never on a dangling one.\n */\nexport function aiSiteSeoTitle(input: { name?: string; subject: string; city?: string }): string {\n const max = AI_SITE_SEO_LIMITS.title\n const name = (input.name ?? '').replace(/\\s+/g, ' ').trim()\n const subject = withoutDangling(input.subject.replace(/\\s+/g, ' ').trim())\n const city = (input.city ?? '').replace(/\\s+/g, ' ').trim()\n const placed = (what: string) =>\n city && !what.toLowerCase().includes(city.toLowerCase()) ? `${what} in ${city}` : what\n const boundary = subject.search(CLAUSE_BOUNDARY)\n const clause = withoutDangling(boundary > 0 ? subject.slice(0, boundary) : subject)\n const bare = withoutDangling(clause.replace(/\\s+in\\s+[^]*$/i, '')) || clause\n const whats = [...new Set([placed(subject), subject, placed(clause), clause, bare])]\n const candidates = name ? [...whats.map((what) => `${name} — ${what}`), name] : whats\n const fits = candidates.find((candidate) => candidate.length <= max)\n return fits ?? clip(candidates[candidates.length - 1], max)\n}\n\n/**\n * The leading article dropped and the first letter raised: \"a neighborhood\n * dog groomer\" is how a person answers the question and is not how a title\n * starts. Anything the person already capitalized is left alone, so a name\n * typed as the site type survives.\n */\nfunction asSubject(about: string): string {\n // `\\b` rather than `\\s+`, so an answer that is ONLY an article leaves\n // nothing behind and the caller proposes nothing: a one-letter title is a\n // worse thing to stage into a required field than no proposal at all.\n const text = about.replace(/\\s+/g, ' ').trim().replace(/^(?:an?|the)\\b\\s*/i, '')\n if (!text) return ''\n return text[0].toUpperCase() + text.slice(1)\n}\n\n/** A sentence closed with one period, and never two. */\nfunction sentence(text: string): string {\n const trimmed = text.trim()\n return /[.!?]$/.test(trimmed) ? trimmed : `${trimmed}.`\n}\n\n/**\n * What the listing is built from: either answer may be missing, because the\n * guided start requires only the first of them.\n */\nexport interface AiSiteSeoInput extends Partial<AiSiteWords> {\n /** The site's own name where the job carries one; empty for a site named on its pages. */\n siteName?: string\n /** The city the person gave, where they gave one. */\n city?: string\n}\n\n/**\n * The site's fallback listing from the answers, or `null` when the answers\n * describe no site at all — a job with no `businessType` has nothing to\n * propose, and an empty proposal is worse than none.\n */\nexport function aiSiteSeoProposal(input: AiSiteSeoInput): AiSiteSeoProposal | null {\n const subject = asSubject(input.about ?? '')\n if (!subject) return null\n const name = (input.siteName ?? '').replace(/\\s+/g, ' ').trim()\n const audience = (input.audience ?? '').replace(/\\s+/g, ' ').trim()\n const title = aiSiteSeoTitle({ name, subject, city: input.city })\n const described = name ? `${name} is ${input.about?.trim()}` : subject\n const description = clip(\n sentence(audience ? `${described}, for ${audience}` : described),\n AI_SITE_SEO_LIMITS.description,\n )\n return {\n kind: AI_SITE_SEO_PROPOSAL_KIND,\n values: {\n [AI_SITE_SEO_FORM_FIELDS.title]: title,\n [AI_SITE_SEO_FORM_FIELDS.description]: description,\n },\n notes: [AI_SITE_SEO_NOTE],\n }\n}\n\n/** The proposal a job's inputs imply, for the step that reports it. */\nexport function aiSiteSeoProposalForInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiSiteSeoProposal | null {\n const words = aiSiteWords(inputs)\n const businessName = inputs?.['businessName']\n const city = inputs?.['city']\n return aiSiteSeoProposal({\n ...words,\n ...(typeof businessName === 'string' ? { siteName: businessName } : {}),\n ...(typeof city === 'string' ? { city } : {}),\n })\n}\n\n/** Whether a value is a record the reader may look inside. */\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/**\n * The site listing a job's outputs carry, or `null` when they carry none.\n *\n * Read defensively: an output is whatever a past release wrote, so a proposal\n * whose values are not two strings is treated as no proposal rather than\n * staged into somebody's live SEO form.\n */\nexport function aiSiteSeoProposalOf(\n outputs: readonly Pick<AiJobOutput, 'resource' | 'proposal'>[] | null | undefined,\n): AiSiteSeoProposal | null {\n for (const output of outputs ?? []) {\n if (output.resource !== 'seo') continue\n const proposal = output.proposal\n if (!isRecord(proposal) || proposal['kind'] !== AI_SITE_SEO_PROPOSAL_KIND) continue\n const values = isRecord(proposal['values']) ? proposal['values'] : {}\n const title = values[AI_SITE_SEO_FORM_FIELDS.title]\n const description = values[AI_SITE_SEO_FORM_FIELDS.description]\n if (typeof title !== 'string' || typeof description !== 'string') continue\n if (!title.trim() || !description.trim()) continue\n const notes = Array.isArray(proposal['notes'])\n ? proposal['notes'].filter((note): note is string => typeof note === 'string')\n : []\n return {\n kind: AI_SITE_SEO_PROPOSAL_KIND,\n values: {\n [AI_SITE_SEO_FORM_FIELDS.title]: title,\n [AI_SITE_SEO_FORM_FIELDS.description]: description,\n },\n notes,\n }\n }\n return null\n}\n"],"names":["SCREEN_SEO_TEXT_GUIDANCE","aiSiteWords","AI_SITE_SEO_FORM_FIELDS","title","description","AI_SITE_SEO_LIMITS","AI_SITE_SEO_PROPOSAL_KIND","AI_SITE_SEO_OUTPUT_ID","AI_SITE_SEO_NOTE","DANGLING","Set","withoutDangling","text","words","split","length","has","toLowerCase","pop","join","replace","trim","clip","value","max","cut","slice","space","lastIndexOf","CLAUSE_BOUNDARY","aiSiteSeoTitle","input","name","subject","city","placed","what","includes","boundary","search","clause","bare","whats","candidates","map","fits","find","candidate","asSubject","about","toUpperCase","sentence","trimmed","test","aiSiteSeoProposal","siteName","audience","described","kind","values","notes","aiSiteSeoProposalForInputs","inputs","businessName","isRecord","Array","isArray","aiSiteSeoProposalOf","outputs","output","resource","proposal","filter","note"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,wBAAwB,QAAQ,2CAA0C;AAEnF,SAASC,WAAW,QAA0B,mBAAe;AAE7D;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BC,GAED;;;;;;;;CAQC,GACD,OAAO,MAAMC,0BAA0B;IACrCC,OAAO;IACPC,aAAa;AACf,EAAU;AAKV;;;CAGC,GACD,OAAO,MAAMC,qBAAqBL,yBAAwB;AAE1D,+EAA+E,GAC/E,OAAO,MAAMM,4BAA4B,eAAc;AAEvD,4DAA4D,GAC5D,OAAO,MAAMC,wBAAwB,eAAc;AAUnD,8DAA8D,GAC9D,OAAO,MAAMC,mBACX,6GAA4G;AAE9G;;;CAGC,GACD,MAAMC,WAAW,IAAIC,IAAI;IACvB;IAAK;IAAM;IAAO;IAAM;IAAM;IAAM;IAAO;IAAQ;IAAM;IAAQ;IAAM;IAAM;IAAM;IACnF;IAAQ;IAAO;IAAM;IAAS;IAAO;IAAQ;IAAS;IAAS;IAAS;IAAQ;IAAO;IAAK;IAAK;IAAK;CACvG;AAED,8EAA8E,GAC9E,SAASC,gBAAgBC,IAAY;IACnC,MAAMC,QAAQD,KAAKE,KAAK,CAAC;IACzB,MAAOD,MAAME,MAAM,GAAG,KAAKN,SAASO,GAAG,CAACH,KAAK,CAACA,MAAME,MAAM,GAAG,EAAE,CAACE,WAAW,IAAKJ,MAAMK,GAAG;IACzF,OAAOL,MAAMM,IAAI,CAAC,KAAKC,OAAO,CAAC,gBAAgB,IAAIC,IAAI;AACzD;AAEA,wFAAwF,GACxF,SAASC,KAAKC,KAAa,EAAEC,GAAW;IACtC,MAAMZ,OAAOW,MAAMH,OAAO,CAAC,QAAQ,KAAKC,IAAI;IAC5C,IAAIT,KAAKG,MAAM,IAAIS,KAAK,OAAOZ;IAC/B,MAAMa,MAAMb,KAAKc,KAAK,CAAC,GAAGF,MAAM;IAChC,MAAMG,QAAQF,IAAIG,WAAW,CAAC;IAC9B,OAAOjB,gBAAgB,AAACgB,CAAAA,QAAQH,MAAM,IAAIC,IAAIC,KAAK,CAAC,GAAGC,SAASf,KAAKc,KAAK,CAAC,GAAGF,IAAG,EAAGH,IAAI;AAC1F;AAEA;;;;CAIC,GACD,MAAMQ,kBACJ;AAEF;;;;;;;;;;;CAWC,GACD,OAAO,SAASC,eAAeC,KAAwD;QAEvEA,aAEAA;IAHd,MAAMP,MAAMnB,mBAAmBF,KAAK;IACpC,MAAM6B,OAAO,EAACD,cAAAA,MAAMC,IAAI,YAAVD,cAAc,IAAIX,OAAO,CAAC,QAAQ,KAAKC,IAAI;IACzD,MAAMY,UAAUtB,gBAAgBoB,MAAME,OAAO,CAACb,OAAO,CAAC,QAAQ,KAAKC,IAAI;IACvE,MAAMa,OAAO,EAACH,cAAAA,MAAMG,IAAI,YAAVH,cAAc,IAAIX,OAAO,CAAC,QAAQ,KAAKC,IAAI;IACzD,MAAMc,SAAS,CAACC,OACdF,QAAQ,CAACE,KAAKnB,WAAW,GAAGoB,QAAQ,CAACH,KAAKjB,WAAW,MAAM,GAAGmB,KAAK,IAAI,EAAEF,MAAM,GAAGE;IACpF,MAAME,WAAWL,QAAQM,MAAM,CAACV;IAChC,MAAMW,SAAS7B,gBAAgB2B,WAAW,IAAIL,QAAQP,KAAK,CAAC,GAAGY,YAAYL;IAC3E,MAAMQ,OAAO9B,gBAAgB6B,OAAOpB,OAAO,CAAC,kBAAkB,QAAQoB;IACtE,MAAME,QAAQ;WAAI,IAAIhC,IAAI;YAACyB,OAAOF;YAAUA;YAASE,OAAOK;YAASA;YAAQC;SAAK;KAAE;IACpF,MAAME,aAAaX,OAAO;WAAIU,MAAME,GAAG,CAAC,CAACR,OAAS,GAAGJ,KAAK,GAAG,EAAEI,MAAM;QAAGJ;KAAK,GAAGU;IAChF,MAAMG,OAAOF,WAAWG,IAAI,CAAC,CAACC,YAAcA,UAAUhC,MAAM,IAAIS;IAChE,OAAOqB,eAAAA,OAAQvB,KAAKqB,UAAU,CAACA,WAAW5B,MAAM,GAAG,EAAE,EAAES;AACzD;AAEA;;;;;CAKC,GACD,SAASwB,UAAUC,KAAa;IAC9B,sEAAsE;IACtE,0EAA0E;IAC1E,sEAAsE;IACtE,MAAMrC,OAAOqC,MAAM7B,OAAO,CAAC,QAAQ,KAAKC,IAAI,GAAGD,OAAO,CAAC,sBAAsB;IAC7E,IAAI,CAACR,MAAM,OAAO;IAClB,OAAOA,IAAI,CAAC,EAAE,CAACsC,WAAW,KAAKtC,KAAKc,KAAK,CAAC;AAC5C;AAEA,sDAAsD,GACtD,SAASyB,SAASvC,IAAY;IAC5B,MAAMwC,UAAUxC,KAAKS,IAAI;IACzB,OAAO,SAASgC,IAAI,CAACD,WAAWA,UAAU,GAAGA,QAAQ,CAAC,CAAC;AACzD;AAaA;;;;CAIC,GACD,OAAO,SAASE,kBAAkBvB,KAAqB;QAC3BA,cAEZA,iBACIA;QAEqBA;IALvC,MAAME,UAAUe,WAAUjB,eAAAA,MAAMkB,KAAK,YAAXlB,eAAe;IACzC,IAAI,CAACE,SAAS,OAAO;IACrB,MAAMD,OAAO,EAACD,kBAAAA,MAAMwB,QAAQ,YAAdxB,kBAAkB,IAAIX,OAAO,CAAC,QAAQ,KAAKC,IAAI;IAC7D,MAAMmC,WAAW,EAACzB,kBAAAA,MAAMyB,QAAQ,YAAdzB,kBAAkB,IAAIX,OAAO,CAAC,QAAQ,KAAKC,IAAI;IACjE,MAAMlB,QAAQ2B,eAAe;QAAEE;QAAMC;QAASC,MAAMH,MAAMG,IAAI;IAAC;IAC/D,MAAMuB,YAAYzB,OAAO,GAAGA,KAAK,IAAI,GAAED,gBAAAA,MAAMkB,KAAK,qBAAXlB,cAAaV,IAAI,IAAI,GAAGY;IAC/D,MAAM7B,cAAckB,KAClB6B,SAASK,WAAW,GAAGC,UAAU,MAAM,EAAED,UAAU,GAAGC,YACtDpD,mBAAmBD,WAAW;IAEhC,OAAO;QACLsD,MAAMpD;QACNqD,QAAQ;YACN,CAACzD,wBAAwBC,KAAK,CAAC,EAAEA;YACjC,CAACD,wBAAwBE,WAAW,CAAC,EAAEA;QACzC;QACAwD,OAAO;YAACpD;SAAiB;IAC3B;AACF;AAEA,qEAAqE,GACrE,OAAO,SAASqD,2BACdC,MAA4D;IAE5D,MAAMjD,QAAQZ,YAAY6D;IAC1B,MAAMC,eAAeD,0BAAAA,MAAQ,CAAC,eAAe;IAC7C,MAAM5B,OAAO4B,0BAAAA,MAAQ,CAAC,OAAO;IAC7B,OAAOR,kBAAkB,aACpBzC,OACC,OAAOkD,iBAAiB,WAAW;QAAER,UAAUQ;IAAa,IAAI,CAAC,GACjE,OAAO7B,SAAS,WAAW;QAAEA;IAAK,IAAI,CAAC;AAE/C;AAEA,4DAA4D,GAC5D,SAAS8B,SAASzC,KAAc;IAC9B,OAAO,OAAOA,UAAU,YAAYA,UAAU,QAAQ,CAAC0C,MAAMC,OAAO,CAAC3C;AACvE;AAEA;;;;;;CAMC,GACD,OAAO,SAAS4C,oBACdC,OAAiF;IAEjF,KAAK,MAAMC,UAAUD,kBAAAA,UAAW,EAAE,CAAE;QAClC,IAAIC,OAAOC,QAAQ,KAAK,OAAO;QAC/B,MAAMC,WAAWF,OAAOE,QAAQ;QAChC,IAAI,CAACP,SAASO,aAAaA,QAAQ,CAAC,OAAO,KAAKjE,2BAA2B;QAC3E,MAAMqD,SAASK,SAASO,QAAQ,CAAC,SAAS,IAAIA,QAAQ,CAAC,SAAS,GAAG,CAAC;QACpE,MAAMpE,QAAQwD,MAAM,CAACzD,wBAAwBC,KAAK,CAAC;QACnD,MAAMC,cAAcuD,MAAM,CAACzD,wBAAwBE,WAAW,CAAC;QAC/D,IAAI,OAAOD,UAAU,YAAY,OAAOC,gBAAgB,UAAU;QAClE,IAAI,CAACD,MAAMkB,IAAI,MAAM,CAACjB,YAAYiB,IAAI,IAAI;QAC1C,MAAMuC,QAAQK,MAAMC,OAAO,CAACK,QAAQ,CAAC,QAAQ,IACzCA,QAAQ,CAAC,QAAQ,CAACC,MAAM,CAAC,CAACC,OAAyB,OAAOA,SAAS,YACnE,EAAE;QACN,OAAO;YACLf,MAAMpD;YACNqD,QAAQ;gBACN,CAACzD,wBAAwBC,KAAK,CAAC,EAAEA;gBACjC,CAACD,wBAAwBE,WAAW,CAAC,EAAEA;YACzC;YACAwD;QACF;IACF;IACA,OAAO;AACT"}
package/src/lib/plugin.js CHANGED
@@ -49,8 +49,7 @@ import { AiExplainAutomation, AiExplainRunFailure } from "./components/ai-explai
49
49
  import AiProductCopyCard from "./components/ai-product-copy-card.component.js";
50
50
  import AiProductImportOption from "./components/ai-product-import-option.component.js";
51
51
  import AiProductsHubCard from "./components/ai-products-hub-card.component.js";
52
- import AiSiteBuildPage from "./components/ai-site-build-page.component.js";
53
- import { AI_SITE_BUILD_HREF } from "./components/ai-job-links.js";
52
+ import AiJobsPage from "./components/ai-jobs-page.component.js";
54
53
  import { AI_PLUGIN_ID } from "./constants.js";
55
54
  import { registerAiDeclarations } from "./declarations.js";
56
55
  /**
@@ -90,17 +89,26 @@ import { registerAiDeclarations } from "./declarations.js";
90
89
  Component: AssistSignalsPage
91
90
  }
92
91
  ],
93
- // "Building your site" (AGL-3594): where the guided start lands a person,
94
- // and where a site job's notification opens. An address under the site
95
- // and no tab — `/ai-jobs/{jobId}`.
92
+ // A site's AI jobs (AGL-3596), an address under the site and no tab:
93
+ // `/ai-jobs` lists them, and `/ai-jobs/{jobId}` is one job's page —
94
+ // "Building your site" (AGL-3594), where the guided start lands a person
95
+ // and where a site job's notification opens. The `href` is a literal,
96
+ // `AI_SITE_BUILD_HREF`'s value, because the tab-title manifest is read
97
+ // from this source (`ai-jobs-page.spec.tsx` holds the two equal).
96
98
  navItems: [
97
99
  {
98
- label: 'Building your site',
99
- href: AI_SITE_BUILD_HREF,
100
+ label: 'AI jobs',
101
+ href: '/ai-jobs',
102
+ recordTitle: 'Building your site',
103
+ header: {
104
+ title: 'AI jobs',
105
+ docsTopic: 'howAglynAiBuilds',
106
+ docsAnchor: '#finding-your-ai-jobs'
107
+ },
100
108
  unlisted: true,
101
109
  ownsSubtree: true,
102
110
  permission: 'ai.generate',
103
- Component: AiSiteBuildPage
111
+ Component: AiJobsPage
104
112
  }
105
113
  ],
106
114
  widgets: [
@@ -342,7 +350,7 @@ import { registerAiDeclarations } from "./declarations.js";
342
350
  permission: 'ai.generate',
343
351
  Component: AiSeoAuditCard
344
352
  },
345
- // A page from a brief (AGL-2907): "Describe it" beside Templates and
353
+ // A page from a brief (AGL-2907): "Create with AI" beside Templates and
346
354
  // Create New Screen. Gated as the other generative widgets are, and it
347
355
  // asks the jobs route about the release flag before it shows anything.
348
356
  {
@@ -393,7 +401,7 @@ import { registerAiDeclarations } from "./declarations.js";
393
401
  Component: AiDescribeComponentButton
394
402
  },
395
403
  // Automations by AI (AGL-2919), in the zones the workflows plugin hosts
396
- // on its Automation page: "Describe it" beside Add action and Recipes,
404
+ // on its Automation page: "Create with AI" beside Add action and Recipes,
397
405
  // "Explain it" in the editor of a saved automation, and "Why did this
398
406
  // fail?" on a failed run. Gated as the other generative widgets are;
399
407
  // each asks the jobs route about the release flag before it shows
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/plugins/ai/src/lib/plugin.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 { PLATFORM_BRAND_NAME, registerConsoleExtension } from '@aglyn/aglyn'\nimport { AiCollaboratorPermissionsCell } from './components/ai-collaborator-permissions-column.component'\nimport { AiSaveAsComponent } from './components/ai-save-as-component.component'\nimport {\n AiGenerateSectionControl,\n AiRewriteControl,\n} from './components/besigner-ai-controls.component'\nimport {\n AiAssistProviderOnHost,\n AssistPanelOnHost,\n} from './components/ai-permissions-on-host.component'\nimport AiCreditsCard from './components/ai-credits-card.component'\nimport AiJobsTopBarIndicator from './components/ai-jobs-indicator.component'\nimport {\n AiCollaboratorCreditsCell,\n AiCollaboratorCreditsHeader,\n AiMemberCreditsCell,\n AiMemberCreditsHeader,\n} from './components/ai-credits-columns.component'\nimport { AssistSignalsPage } from './components/assist-signals-page.component'\nimport BillingAssistOverageCard from './components/billing-assist-overage-card.component'\nimport { AiTopUsersCard } from './components/billing-ai-top-users.component'\nimport { AiAllotmentsCard } from './components/billing-ai-allotments.component'\nimport {\n AiCollaboratorAllotmentCell,\n AiCollaboratorAllotmentHeader,\n AiSiteAllotmentCard,\n} from './components/host-ai-allotments.component'\nimport MemberAiAllotmentCard from './components/member-ai-allotment-card.component'\nimport MemberAiUsageCard from './components/member-ai-usage-card.component'\nimport AiThemeProposalCard from './components/ai-theme-proposal-card.component'\nimport StaffOrgAiCard from './components/staff-org-ai-card.component'\nimport {\n StaffOrgUsageAiCreditsCell,\n StaffOrgUsageAiOverageCell,\n StaffOrgUsageAiPool,\n StaffOrgUsageAssistCell,\n} from './components/staff-org-usage-ai-columns.component'\nimport {\n StaffOrgsAiSpendCell,\n StaffOrgsAiSpendHeader,\n} from './components/staff-orgs-ai-spend-column.component'\nimport StaffUserAiUsageCard from './components/staff-user-ai-usage-card.component'\nimport AiSeoAuditCard from './components/ai-seo-audit-card.component'\nimport AiSiteBatchCard from './components/ai-site-batch-card.component'\nimport AiSiteSeoStartCard from './components/ai-site-seo-start-card.component'\nimport AiSiteStartCard from './components/ai-site-start-card.component'\nimport AiSeoFieldsCard from './components/ai-seo-fields-card.component'\nimport AiDescribeAutomationButton from './components/ai-describe-automation.component'\nimport AiCrmEmailDraft from './components/ai-crm-email-draft.component'\nimport AiCrmImportMapping from './components/ai-crm-import-mapping.component'\nimport AiCrmRecordCard from './components/ai-crm-record-card.component'\nimport AiDescribePageButton from './components/ai-describe-page.component'\nimport {\n AiDescribeComponentButton,\n AiDescribeFormButton,\n AiDescribeLayoutButton,\n AiDescribeTemplateButton,\n} from './components/ai-describe-button.component'\nimport {\n AiExperimentResultCard,\n AiExperimentVariantsCard,\n} from './components/ai-experiment-cards.component'\nimport {\n AiExplainAutomation,\n AiExplainRunFailure,\n} from './components/ai-explain-automation.component'\nimport AiProductCopyCard from './components/ai-product-copy-card.component'\nimport AiProductImportOption from './components/ai-product-import-option.component'\nimport AiProductsHubCard from './components/ai-products-hub-card.component'\nimport AiSiteBuildPage from './components/ai-site-build-page.component'\nimport { AI_SITE_BUILD_HREF } from './components/ai-job-links'\nimport { AI_PLUGIN_ID } from './constants'\nimport { registerAiDeclarations } from './declarations'\n\n/**\n * The Aglyn AI plugin's console half (AGL-2939): the assistant dock, the\n * besigner copy assistant's provider, the billing, member and staff cards,\n * the member tables' credit columns and the Assist signal staff page — each\n * mounted through a shell-owned zone (AGL-2940) or the staff area's generic\n * route, so no console page imports this plugin.\n *\n * No `featureFlag` on the extension: the plugin's doors are gated one by\n * one — the dock reads the `release_assist` verdict the shell hands it, the\n * generative doors answer 404 behind `release_ai_generative`, the billing\n * cards render only where the plan sells a band — and a flag here would\n * switch off the released doors with the unreleased ones.\n */\nexport function registerAiConsole(): void {\n registerAiDeclarations()\n registerConsoleExtension({\n pluginId: AI_PLUGIN_ID,\n displayName: 'AI',\n // The besigner copy assistant (AGL-89/419): mounted by the shell around\n // every console page, and opened by this plugin's besigner controls.\n providers: [AiAssistProviderOnHost],\n // The Assist docs-gap and cost board (AGL-1860, AGL-2252), at the staff\n // URL and under the tab it has always had.\n staffPages: [\n {\n id: 'assist-signals',\n label: 'Assist signal',\n header: {\n // The configured brand, not ours (AGL-2153/2260): a white-label\n // deployment and a self-host operator both read this header.\n title: `${PLATFORM_BRAND_NAME} Assist Signal`,\n docsTopic: 'assistSignals',\n },\n Component: AssistSignalsPage,\n },\n ],\n // \"Building your site\" (AGL-3594): where the guided start lands a person,\n // and where a site job's notification opens. An address under the site\n // and no tab — `/ai-jobs/{jobId}`.\n navItems: [\n {\n label: 'Building your site',\n href: AI_SITE_BUILD_HREF,\n unlisted: true,\n ownsSubtree: true,\n permission: 'ai.generate',\n Component: AiSiteBuildPage,\n },\n ],\n widgets: [\n {\n slot: 'consoleDock',\n widgetId: 'ai-assist-dock',\n title: 'Assistant',\n Component: AssistPanelOnHost,\n },\n // The AI jobs indicator (AGL-3593), beside the notifications bell on\n // every page: what the workspace's jobs are doing, and the way to them.\n // Gated as the other generative widgets are; it asks the shell for the\n // release flags of the jobs and of the panel it opens.\n {\n slot: 'consoleTopBar',\n widgetId: 'ai-jobs-indicator',\n title: 'AI jobs',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiJobsTopBarIndicator,\n },\n {\n slot: 'orgBillingUsage',\n widgetId: 'ai-credits-meter',\n title: 'AI credits',\n Component: AiCreditsCard,\n },\n {\n slot: 'orgBillingUsage',\n widgetId: 'ai-credits-overage',\n title: 'AI credits overage',\n Component: BillingAssistOverageCard,\n },\n // The per-member usage (AGL-2928): who drew the credits the meter\n // counts, on Billing → Usage, on each member's page, on the staff\n // user page, and as a column of both member tables.\n {\n slot: 'orgBillingUsage',\n widgetId: 'ai-usage-by-member',\n title: 'Who is generating what',\n permission: 'billing.view',\n Component: AiTopUsersCard,\n },\n // The collaborators table's AI column (AGL-2927): whether each person\n // may open the AI doors on the site, per site, ahead of the columns\n // that report what they drew.\n {\n slot: 'hostMembers',\n widgetId: 'ai-collaborator-permissions',\n title: 'AI',\n column: { header: 'AI' },\n Component: AiCollaboratorPermissionsCell,\n },\n // The copy assistant's besigner controls (AGL-89, AGL-169): Generate a\n // section on every editor's toolbar, and Rewrite with AI under the\n // selected element's fields. Each opens the provider's dialog, and is\n // drawn only while the reader holds the door's permission.\n {\n slot: 'besignerToolbar',\n widgetId: 'ai-generate-section',\n title: 'Generate a section with AI',\n Component: AiGenerateSectionControl,\n },\n {\n slot: 'besignerInspector',\n widgetId: 'ai-rewrite-copy',\n title: 'Rewrite with AI',\n Component: AiRewriteControl,\n },\n // Save the selection as a reusable component (AGL-2908): the component\n // job's second entry point, which proposes the properties a section\n // should declare and applies them as the manual promote does.\n {\n slot: 'besignerInspector',\n widgetId: 'ai-save-as-component',\n title: 'Make a reusable component with AI',\n permission: 'ai.generate',\n Component: AiSaveAsComponent,\n },\n {\n slot: 'orgMember',\n widgetId: 'ai-member-usage',\n title: 'AI usage',\n Component: MemberAiUsageCard,\n },\n // The allotments (AGL-2942): a member's or a site's monthly share of\n // the pool, set on Billing → Usage, on each member's page and on a\n // site's collaborators card — each card asks the route what its\n // reader may see and change.\n {\n slot: 'orgBillingUsage',\n widgetId: 'ai-allotments',\n title: 'AI allotments',\n permission: 'billing.view',\n Component: AiAllotmentsCard,\n },\n {\n slot: 'orgMember',\n widgetId: 'ai-member-allotment',\n title: 'AI allotment',\n Component: MemberAiAllotmentCard,\n },\n {\n slot: 'hostMembers',\n widgetId: 'ai-collaborator-allotment',\n title: 'AI allotment',\n column: {\n header: 'AI allotment',\n align: 'right',\n Header: AiCollaboratorAllotmentHeader,\n },\n Component: AiCollaboratorAllotmentCell,\n },\n {\n slot: 'hostMembers',\n widgetId: 'ai-site-allotment',\n title: 'Site AI allotment',\n Component: AiSiteAllotmentCard,\n },\n // Themes by AI (AGL-2938): a brief on the site's Theme section, a\n // proposal previewed before and after, and the editor's own Save to\n // keep it. Generation is sold as `aiGenerative` and held by\n // `ai.generate`, so the shell withholds the card from a plan or a\n // member without either; the card itself asks the route about the\n // release flag before it shows anything.\n {\n slot: 'hostTheme',\n widgetId: 'ai-theme-proposal',\n title: 'Theme assistant',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiThemeProposalCard,\n },\n {\n slot: 'staffOrg',\n widgetId: 'ai-org-usage',\n title: 'AI usage',\n Component: StaffOrgAiCard,\n },\n {\n slot: 'staffUser',\n widgetId: 'ai-account-usage',\n title: 'AI usage across organizations',\n Component: StaffUserAiUsageCard,\n },\n // The staff tables' AI figures (AGL-2984): the Organizations list's\n // spend column, and the usage table's three columns with the credit\n // pool line the org page draws above it.\n {\n slot: 'staffOrgsListColumn',\n widgetId: 'ai-orgs-spend',\n title: 'AI spend (month)',\n column: {\n header: 'AI spend (month)',\n align: 'right',\n Header: StaffOrgsAiSpendHeader,\n },\n Component: StaffOrgsAiSpendCell,\n },\n {\n slot: 'staffOrgUsageColumn',\n widgetId: 'ai-usage-assist-cost',\n title: 'Assist',\n column: { header: 'Assist', align: 'right' },\n Component: StaffOrgUsageAssistCell,\n },\n {\n slot: 'staffOrgUsageColumn',\n widgetId: 'ai-usage-credits',\n title: 'AI credits used',\n column: { header: 'AI credits used', align: 'right' },\n Component: StaffOrgUsageAiCreditsCell,\n },\n {\n slot: 'staffOrgUsageColumn',\n widgetId: 'ai-usage-overage',\n title: 'AI overage billed ($)',\n column: { header: 'AI overage billed ($)', align: 'right' },\n Component: StaffOrgUsageAiOverageCell,\n },\n {\n slot: 'staffOrgUsageColumn',\n widgetId: 'ai-usage-pool',\n title: 'AI credit pool',\n Component: StaffOrgUsageAiPool,\n },\n {\n slot: 'orgMembersListColumn',\n widgetId: 'ai-member-credits',\n title: 'AI credits (month)',\n column: { header: 'AI credits (month)', align: 'right', Header: AiMemberCreditsHeader },\n Component: AiMemberCreditsCell,\n },\n {\n slot: 'hostMembers',\n widgetId: 'ai-collaborator-credits',\n title: 'AI credits (site, month)',\n column: {\n header: 'AI credits (site, month)',\n align: 'right',\n Header: AiCollaboratorCreditsHeader,\n },\n Component: AiCollaboratorCreditsCell,\n },\n // SEO by AI (AGL-2910): \"Write SEO\" inside every search listing editor\n // — a page's SEO card and a product's listing — and the site audit on\n // the site's SEO section. Generation is sold as `aiGenerative` and held\n // by `ai.generate`, so the shell withholds both from a plan or a member\n // without either; each card asks the jobs route about the release flag\n // before it shows anything, and neither ever writes what it proposes.\n {\n slot: 'seoFields',\n widgetId: 'ai-seo-fields',\n title: 'SEO assistant',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiSeoFieldsCard,\n },\n {\n slot: 'hostSeo',\n widgetId: 'ai-seo-audit',\n title: 'SEO audit',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiSeoAuditCard,\n },\n // A page from a brief (AGL-2907): \"Describe it\" beside Templates and\n // Create New Screen. Gated as the other generative widgets are, and it\n // asks the jobs route about the release flag before it shows anything.\n {\n slot: 'hostScreens',\n widgetId: 'ai-describe-page',\n title: 'Describe a page',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiDescribePageButton,\n },\n // The same entry for a page template, a layout and a form (AGL-3043),\n // beside the create action of the page that lists each: the same\n // dialog, the same gates, and the same question to the jobs route\n // before it shows anything.\n {\n slot: 'hostTemplates',\n widgetId: 'ai-describe-template',\n title: 'Describe a page template',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiDescribeTemplateButton,\n },\n {\n slot: 'hostLayouts',\n widgetId: 'ai-describe-layout',\n title: 'Describe a layout',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiDescribeLayoutButton,\n },\n {\n slot: 'hostForms',\n widgetId: 'ai-describe-form',\n title: 'Describe a form',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiDescribeFormButton,\n },\n // And a reusable component from a brief (AGL-3051), beside Templates\n // and Create Component: the component job's first entry point, which\n // had none in the console.\n {\n slot: 'hostComponents',\n widgetId: 'ai-describe-component',\n title: 'Describe a reusable component',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiDescribeComponentButton,\n },\n // Automations by AI (AGL-2919), in the zones the workflows plugin hosts\n // on its Automation page: \"Describe it\" beside Add action and Recipes,\n // \"Explain it\" in the editor of a saved automation, and \"Why did this\n // fail?\" on a failed run. Gated as the other generative widgets are;\n // each asks the jobs route about the release flag before it shows\n // anything, and none of them changes an automation.\n {\n slot: 'hostAutomations',\n widgetId: 'ai-describe-automation',\n title: 'Describe an automation',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiDescribeAutomationButton,\n },\n {\n slot: 'automationEditor',\n widgetId: 'ai-explain-automation',\n title: 'Explain this automation',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiExplainAutomation,\n },\n {\n slot: 'automationRun',\n widgetId: 'ai-explain-run-failure',\n title: 'Why did this run fail?',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiExplainRunFailure,\n },\n // CRM by AI (AGL-2917): a record's summary and next step on its page,\n // a draft in the one-to-one composer, and an import's column matches.\n // The CRM hosts each zone, so it is drawn only where the CRM is; the\n // shell holds the plan and `ai.generate`, each card asks the jobs route\n // about the release flag before it shows anything, and none writes what\n // it proposes.\n {\n slot: 'recordInsights',\n widgetId: 'ai-crm-record',\n title: 'AI summary',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiCrmRecordCard,\n },\n {\n slot: 'recordEmail',\n widgetId: 'ai-crm-email',\n title: 'Draft with AI',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiCrmEmailDraft,\n },\n {\n slot: 'importMapping',\n widgetId: 'ai-crm-import-mapping',\n title: 'Match columns with AI',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiCrmImportMapping,\n },\n // The guided start (AGL-2918), on the page a newly created site lands\n // on: a few questions that become a site scaffold. Gated as the other\n // generative widgets are, and it asks the jobs route about the release\n // flag before it shows anything — so a workspace the feature is not\n // released to gets the blank page and nothing else. Skipping is the\n // zone's own `startBlank`, which creates nothing.\n {\n slot: 'hostFirstRun',\n widgetId: 'ai-site-start',\n title: 'Start this site with AI',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiSiteStartCard,\n },\n // The listing those answers describe (AGL-2918), on the site's SEO\n // section: the site-wide search title and description the guided start\n // implies, offered where they are edited. The same gates as every\n // generative widget, and it stages into the SEO form rather than\n // writing it — a person's Update is the write, as it is for the audit\n // card beside it.\n {\n slot: 'hostSeo',\n widgetId: 'ai-site-seo-start',\n title: 'The listing your answers describe',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiSiteSeoStartCard,\n },\n // The agency batch (AGL-2911): one brief across many of the org's\n // sites, from the page that lists them. The card asks the jobs route\n // about the release flag before it shows anything, and its own door\n // holds the plan band and the caller's permission on every site it is\n // pointed at.\n {\n slot: 'orgSites',\n widgetId: 'ai-site-batch',\n title: 'Generate sites with AI',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiSiteBatchCard,\n },\n // Commerce by AI (AGL-2916): product copy in the product editor, the\n // catalog, categories and discounts on the products hub, and copy for\n // an import as it lands. The commerce plugin hosts the zones and makes\n // every write; each card asks the jobs route about the release flag\n // before it shows anything.\n {\n slot: 'productEditor',\n widgetId: 'ai-product-copy',\n title: 'Product copy assistant',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiProductCopyCard,\n },\n {\n slot: 'productsHub',\n widgetId: 'ai-products-hub',\n title: 'Build your catalog with AI',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiProductsHubCard,\n },\n {\n slot: 'productImport',\n widgetId: 'ai-product-import',\n title: 'Write imported product copy with AI',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiProductImportOption,\n },\n // A/B tests by AI (AGL-2914): variants written in the experiment\n // editor, and one test's result read in plain words below its figures.\n // The A/B testing card hosts both zones, so they are drawn only where\n // that card is and only on a site with A/B testing; the shell holds the\n // plan band and `ai.generate`, and each card asks the jobs route about\n // the release flag before it shows anything. Neither writes: the\n // experiment editor's Save is the only write, and an explanation has\n // nothing to apply.\n {\n slot: 'experimentVariants',\n widgetId: 'ai-experiment-variants',\n title: 'Write variants with AI',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiExperimentVariantsCard,\n },\n {\n slot: 'experimentResult',\n widgetId: 'ai-experiment-result',\n title: 'Explain this result with AI',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiExperimentResultCard,\n },\n ],\n })\n}\n"],"names":["PLATFORM_BRAND_NAME","registerConsoleExtension","AiCollaboratorPermissionsCell","AiSaveAsComponent","AiGenerateSectionControl","AiRewriteControl","AiAssistProviderOnHost","AssistPanelOnHost","AiCreditsCard","AiJobsTopBarIndicator","AiCollaboratorCreditsCell","AiCollaboratorCreditsHeader","AiMemberCreditsCell","AiMemberCreditsHeader","AssistSignalsPage","BillingAssistOverageCard","AiTopUsersCard","AiAllotmentsCard","AiCollaboratorAllotmentCell","AiCollaboratorAllotmentHeader","AiSiteAllotmentCard","MemberAiAllotmentCard","MemberAiUsageCard","AiThemeProposalCard","StaffOrgAiCard","StaffOrgUsageAiCreditsCell","StaffOrgUsageAiOverageCell","StaffOrgUsageAiPool","StaffOrgUsageAssistCell","StaffOrgsAiSpendCell","StaffOrgsAiSpendHeader","StaffUserAiUsageCard","AiSeoAuditCard","AiSiteBatchCard","AiSiteSeoStartCard","AiSiteStartCard","AiSeoFieldsCard","AiDescribeAutomationButton","AiCrmEmailDraft","AiCrmImportMapping","AiCrmRecordCard","AiDescribePageButton","AiDescribeComponentButton","AiDescribeFormButton","AiDescribeLayoutButton","AiDescribeTemplateButton","AiExperimentResultCard","AiExperimentVariantsCard","AiExplainAutomation","AiExplainRunFailure","AiProductCopyCard","AiProductImportOption","AiProductsHubCard","AiSiteBuildPage","AI_SITE_BUILD_HREF","AI_PLUGIN_ID","registerAiDeclarations","registerAiConsole","pluginId","displayName","providers","staffPages","id","label","header","title","docsTopic","Component","navItems","href","unlisted","ownsSubtree","permission","widgets","slot","widgetId","featureFlag","column","align","Header"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/plugins/ai/src/lib/plugin.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 { PLATFORM_BRAND_NAME, registerConsoleExtension } from '@aglyn/aglyn'\nimport { AiCollaboratorPermissionsCell } from './components/ai-collaborator-permissions-column.component'\nimport { AiSaveAsComponent } from './components/ai-save-as-component.component'\nimport {\n AiGenerateSectionControl,\n AiRewriteControl,\n} from './components/besigner-ai-controls.component'\nimport {\n AiAssistProviderOnHost,\n AssistPanelOnHost,\n} from './components/ai-permissions-on-host.component'\nimport AiCreditsCard from './components/ai-credits-card.component'\nimport AiJobsTopBarIndicator from './components/ai-jobs-indicator.component'\nimport {\n AiCollaboratorCreditsCell,\n AiCollaboratorCreditsHeader,\n AiMemberCreditsCell,\n AiMemberCreditsHeader,\n} from './components/ai-credits-columns.component'\nimport { AssistSignalsPage } from './components/assist-signals-page.component'\nimport BillingAssistOverageCard from './components/billing-assist-overage-card.component'\nimport { AiTopUsersCard } from './components/billing-ai-top-users.component'\nimport { AiAllotmentsCard } from './components/billing-ai-allotments.component'\nimport {\n AiCollaboratorAllotmentCell,\n AiCollaboratorAllotmentHeader,\n AiSiteAllotmentCard,\n} from './components/host-ai-allotments.component'\nimport MemberAiAllotmentCard from './components/member-ai-allotment-card.component'\nimport MemberAiUsageCard from './components/member-ai-usage-card.component'\nimport AiThemeProposalCard from './components/ai-theme-proposal-card.component'\nimport StaffOrgAiCard from './components/staff-org-ai-card.component'\nimport {\n StaffOrgUsageAiCreditsCell,\n StaffOrgUsageAiOverageCell,\n StaffOrgUsageAiPool,\n StaffOrgUsageAssistCell,\n} from './components/staff-org-usage-ai-columns.component'\nimport {\n StaffOrgsAiSpendCell,\n StaffOrgsAiSpendHeader,\n} from './components/staff-orgs-ai-spend-column.component'\nimport StaffUserAiUsageCard from './components/staff-user-ai-usage-card.component'\nimport AiSeoAuditCard from './components/ai-seo-audit-card.component'\nimport AiSiteBatchCard from './components/ai-site-batch-card.component'\nimport AiSiteSeoStartCard from './components/ai-site-seo-start-card.component'\nimport AiSiteStartCard from './components/ai-site-start-card.component'\nimport AiSeoFieldsCard from './components/ai-seo-fields-card.component'\nimport AiDescribeAutomationButton from './components/ai-describe-automation.component'\nimport AiCrmEmailDraft from './components/ai-crm-email-draft.component'\nimport AiCrmImportMapping from './components/ai-crm-import-mapping.component'\nimport AiCrmRecordCard from './components/ai-crm-record-card.component'\nimport AiDescribePageButton from './components/ai-describe-page.component'\nimport {\n AiDescribeComponentButton,\n AiDescribeFormButton,\n AiDescribeLayoutButton,\n AiDescribeTemplateButton,\n} from './components/ai-describe-button.component'\nimport {\n AiExperimentResultCard,\n AiExperimentVariantsCard,\n} from './components/ai-experiment-cards.component'\nimport {\n AiExplainAutomation,\n AiExplainRunFailure,\n} from './components/ai-explain-automation.component'\nimport AiProductCopyCard from './components/ai-product-copy-card.component'\nimport AiProductImportOption from './components/ai-product-import-option.component'\nimport AiProductsHubCard from './components/ai-products-hub-card.component'\nimport AiJobsPage from './components/ai-jobs-page.component'\nimport { AI_PLUGIN_ID } from './constants'\nimport { registerAiDeclarations } from './declarations'\n\n/**\n * The Aglyn AI plugin's console half (AGL-2939): the assistant dock, the\n * besigner copy assistant's provider, the billing, member and staff cards,\n * the member tables' credit columns and the Assist signal staff page — each\n * mounted through a shell-owned zone (AGL-2940) or the staff area's generic\n * route, so no console page imports this plugin.\n *\n * No `featureFlag` on the extension: the plugin's doors are gated one by\n * one — the dock reads the `release_assist` verdict the shell hands it, the\n * generative doors answer 404 behind `release_ai_generative`, the billing\n * cards render only where the plan sells a band — and a flag here would\n * switch off the released doors with the unreleased ones.\n */\nexport function registerAiConsole(): void {\n registerAiDeclarations()\n registerConsoleExtension({\n pluginId: AI_PLUGIN_ID,\n displayName: 'AI',\n // The besigner copy assistant (AGL-89/419): mounted by the shell around\n // every console page, and opened by this plugin's besigner controls.\n providers: [AiAssistProviderOnHost],\n // The Assist docs-gap and cost board (AGL-1860, AGL-2252), at the staff\n // URL and under the tab it has always had.\n staffPages: [\n {\n id: 'assist-signals',\n label: 'Assist signal',\n header: {\n // The configured brand, not ours (AGL-2153/2260): a white-label\n // deployment and a self-host operator both read this header.\n title: `${PLATFORM_BRAND_NAME} Assist Signal`,\n docsTopic: 'assistSignals',\n },\n Component: AssistSignalsPage,\n },\n ],\n // A site's AI jobs (AGL-3596), an address under the site and no tab:\n // `/ai-jobs` lists them, and `/ai-jobs/{jobId}` is one job's page —\n // \"Building your site\" (AGL-3594), where the guided start lands a person\n // and where a site job's notification opens. The `href` is a literal,\n // `AI_SITE_BUILD_HREF`'s value, because the tab-title manifest is read\n // from this source (`ai-jobs-page.spec.tsx` holds the two equal).\n navItems: [\n {\n label: 'AI jobs',\n href: '/ai-jobs',\n recordTitle: 'Building your site',\n header: { title: 'AI jobs', docsTopic: 'howAglynAiBuilds', docsAnchor: '#finding-your-ai-jobs' },\n unlisted: true,\n ownsSubtree: true,\n permission: 'ai.generate',\n Component: AiJobsPage,\n },\n ],\n widgets: [\n {\n slot: 'consoleDock',\n widgetId: 'ai-assist-dock',\n title: 'Assistant',\n Component: AssistPanelOnHost,\n },\n // The AI jobs indicator (AGL-3593), beside the notifications bell on\n // every page: what the workspace's jobs are doing, and the way to them.\n // Gated as the other generative widgets are; it asks the shell for the\n // release flags of the jobs and of the panel it opens.\n {\n slot: 'consoleTopBar',\n widgetId: 'ai-jobs-indicator',\n title: 'AI jobs',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiJobsTopBarIndicator,\n },\n {\n slot: 'orgBillingUsage',\n widgetId: 'ai-credits-meter',\n title: 'AI credits',\n Component: AiCreditsCard,\n },\n {\n slot: 'orgBillingUsage',\n widgetId: 'ai-credits-overage',\n title: 'AI credits overage',\n Component: BillingAssistOverageCard,\n },\n // The per-member usage (AGL-2928): who drew the credits the meter\n // counts, on Billing → Usage, on each member's page, on the staff\n // user page, and as a column of both member tables.\n {\n slot: 'orgBillingUsage',\n widgetId: 'ai-usage-by-member',\n title: 'Who is generating what',\n permission: 'billing.view',\n Component: AiTopUsersCard,\n },\n // The collaborators table's AI column (AGL-2927): whether each person\n // may open the AI doors on the site, per site, ahead of the columns\n // that report what they drew.\n {\n slot: 'hostMembers',\n widgetId: 'ai-collaborator-permissions',\n title: 'AI',\n column: { header: 'AI' },\n Component: AiCollaboratorPermissionsCell,\n },\n // The copy assistant's besigner controls (AGL-89, AGL-169): Generate a\n // section on every editor's toolbar, and Rewrite with AI under the\n // selected element's fields. Each opens the provider's dialog, and is\n // drawn only while the reader holds the door's permission.\n {\n slot: 'besignerToolbar',\n widgetId: 'ai-generate-section',\n title: 'Generate a section with AI',\n Component: AiGenerateSectionControl,\n },\n {\n slot: 'besignerInspector',\n widgetId: 'ai-rewrite-copy',\n title: 'Rewrite with AI',\n Component: AiRewriteControl,\n },\n // Save the selection as a reusable component (AGL-2908): the component\n // job's second entry point, which proposes the properties a section\n // should declare and applies them as the manual promote does.\n {\n slot: 'besignerInspector',\n widgetId: 'ai-save-as-component',\n title: 'Make a reusable component with AI',\n permission: 'ai.generate',\n Component: AiSaveAsComponent,\n },\n {\n slot: 'orgMember',\n widgetId: 'ai-member-usage',\n title: 'AI usage',\n Component: MemberAiUsageCard,\n },\n // The allotments (AGL-2942): a member's or a site's monthly share of\n // the pool, set on Billing → Usage, on each member's page and on a\n // site's collaborators card — each card asks the route what its\n // reader may see and change.\n {\n slot: 'orgBillingUsage',\n widgetId: 'ai-allotments',\n title: 'AI allotments',\n permission: 'billing.view',\n Component: AiAllotmentsCard,\n },\n {\n slot: 'orgMember',\n widgetId: 'ai-member-allotment',\n title: 'AI allotment',\n Component: MemberAiAllotmentCard,\n },\n {\n slot: 'hostMembers',\n widgetId: 'ai-collaborator-allotment',\n title: 'AI allotment',\n column: {\n header: 'AI allotment',\n align: 'right',\n Header: AiCollaboratorAllotmentHeader,\n },\n Component: AiCollaboratorAllotmentCell,\n },\n {\n slot: 'hostMembers',\n widgetId: 'ai-site-allotment',\n title: 'Site AI allotment',\n Component: AiSiteAllotmentCard,\n },\n // Themes by AI (AGL-2938): a brief on the site's Theme section, a\n // proposal previewed before and after, and the editor's own Save to\n // keep it. Generation is sold as `aiGenerative` and held by\n // `ai.generate`, so the shell withholds the card from a plan or a\n // member without either; the card itself asks the route about the\n // release flag before it shows anything.\n {\n slot: 'hostTheme',\n widgetId: 'ai-theme-proposal',\n title: 'Theme assistant',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiThemeProposalCard,\n },\n {\n slot: 'staffOrg',\n widgetId: 'ai-org-usage',\n title: 'AI usage',\n Component: StaffOrgAiCard,\n },\n {\n slot: 'staffUser',\n widgetId: 'ai-account-usage',\n title: 'AI usage across organizations',\n Component: StaffUserAiUsageCard,\n },\n // The staff tables' AI figures (AGL-2984): the Organizations list's\n // spend column, and the usage table's three columns with the credit\n // pool line the org page draws above it.\n {\n slot: 'staffOrgsListColumn',\n widgetId: 'ai-orgs-spend',\n title: 'AI spend (month)',\n column: {\n header: 'AI spend (month)',\n align: 'right',\n Header: StaffOrgsAiSpendHeader,\n },\n Component: StaffOrgsAiSpendCell,\n },\n {\n slot: 'staffOrgUsageColumn',\n widgetId: 'ai-usage-assist-cost',\n title: 'Assist',\n column: { header: 'Assist', align: 'right' },\n Component: StaffOrgUsageAssistCell,\n },\n {\n slot: 'staffOrgUsageColumn',\n widgetId: 'ai-usage-credits',\n title: 'AI credits used',\n column: { header: 'AI credits used', align: 'right' },\n Component: StaffOrgUsageAiCreditsCell,\n },\n {\n slot: 'staffOrgUsageColumn',\n widgetId: 'ai-usage-overage',\n title: 'AI overage billed ($)',\n column: { header: 'AI overage billed ($)', align: 'right' },\n Component: StaffOrgUsageAiOverageCell,\n },\n {\n slot: 'staffOrgUsageColumn',\n widgetId: 'ai-usage-pool',\n title: 'AI credit pool',\n Component: StaffOrgUsageAiPool,\n },\n {\n slot: 'orgMembersListColumn',\n widgetId: 'ai-member-credits',\n title: 'AI credits (month)',\n column: { header: 'AI credits (month)', align: 'right', Header: AiMemberCreditsHeader },\n Component: AiMemberCreditsCell,\n },\n {\n slot: 'hostMembers',\n widgetId: 'ai-collaborator-credits',\n title: 'AI credits (site, month)',\n column: {\n header: 'AI credits (site, month)',\n align: 'right',\n Header: AiCollaboratorCreditsHeader,\n },\n Component: AiCollaboratorCreditsCell,\n },\n // SEO by AI (AGL-2910): \"Write SEO\" inside every search listing editor\n // — a page's SEO card and a product's listing — and the site audit on\n // the site's SEO section. Generation is sold as `aiGenerative` and held\n // by `ai.generate`, so the shell withholds both from a plan or a member\n // without either; each card asks the jobs route about the release flag\n // before it shows anything, and neither ever writes what it proposes.\n {\n slot: 'seoFields',\n widgetId: 'ai-seo-fields',\n title: 'SEO assistant',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiSeoFieldsCard,\n },\n {\n slot: 'hostSeo',\n widgetId: 'ai-seo-audit',\n title: 'SEO audit',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiSeoAuditCard,\n },\n // A page from a brief (AGL-2907): \"Create with AI\" beside Templates and\n // Create New Screen. Gated as the other generative widgets are, and it\n // asks the jobs route about the release flag before it shows anything.\n {\n slot: 'hostScreens',\n widgetId: 'ai-describe-page',\n title: 'Describe a page',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiDescribePageButton,\n },\n // The same entry for a page template, a layout and a form (AGL-3043),\n // beside the create action of the page that lists each: the same\n // dialog, the same gates, and the same question to the jobs route\n // before it shows anything.\n {\n slot: 'hostTemplates',\n widgetId: 'ai-describe-template',\n title: 'Describe a page template',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiDescribeTemplateButton,\n },\n {\n slot: 'hostLayouts',\n widgetId: 'ai-describe-layout',\n title: 'Describe a layout',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiDescribeLayoutButton,\n },\n {\n slot: 'hostForms',\n widgetId: 'ai-describe-form',\n title: 'Describe a form',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiDescribeFormButton,\n },\n // And a reusable component from a brief (AGL-3051), beside Templates\n // and Create Component: the component job's first entry point, which\n // had none in the console.\n {\n slot: 'hostComponents',\n widgetId: 'ai-describe-component',\n title: 'Describe a reusable component',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiDescribeComponentButton,\n },\n // Automations by AI (AGL-2919), in the zones the workflows plugin hosts\n // on its Automation page: \"Create with AI\" beside Add action and Recipes,\n // \"Explain it\" in the editor of a saved automation, and \"Why did this\n // fail?\" on a failed run. Gated as the other generative widgets are;\n // each asks the jobs route about the release flag before it shows\n // anything, and none of them changes an automation.\n {\n slot: 'hostAutomations',\n widgetId: 'ai-describe-automation',\n title: 'Describe an automation',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiDescribeAutomationButton,\n },\n {\n slot: 'automationEditor',\n widgetId: 'ai-explain-automation',\n title: 'Explain this automation',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiExplainAutomation,\n },\n {\n slot: 'automationRun',\n widgetId: 'ai-explain-run-failure',\n title: 'Why did this run fail?',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiExplainRunFailure,\n },\n // CRM by AI (AGL-2917): a record's summary and next step on its page,\n // a draft in the one-to-one composer, and an import's column matches.\n // The CRM hosts each zone, so it is drawn only where the CRM is; the\n // shell holds the plan and `ai.generate`, each card asks the jobs route\n // about the release flag before it shows anything, and none writes what\n // it proposes.\n {\n slot: 'recordInsights',\n widgetId: 'ai-crm-record',\n title: 'AI summary',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiCrmRecordCard,\n },\n {\n slot: 'recordEmail',\n widgetId: 'ai-crm-email',\n title: 'Draft with AI',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiCrmEmailDraft,\n },\n {\n slot: 'importMapping',\n widgetId: 'ai-crm-import-mapping',\n title: 'Match columns with AI',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiCrmImportMapping,\n },\n // The guided start (AGL-2918), on the page a newly created site lands\n // on: a few questions that become a site scaffold. Gated as the other\n // generative widgets are, and it asks the jobs route about the release\n // flag before it shows anything — so a workspace the feature is not\n // released to gets the blank page and nothing else. Skipping is the\n // zone's own `startBlank`, which creates nothing.\n {\n slot: 'hostFirstRun',\n widgetId: 'ai-site-start',\n title: 'Start this site with AI',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiSiteStartCard,\n },\n // The listing those answers describe (AGL-2918), on the site's SEO\n // section: the site-wide search title and description the guided start\n // implies, offered where they are edited. The same gates as every\n // generative widget, and it stages into the SEO form rather than\n // writing it — a person's Update is the write, as it is for the audit\n // card beside it.\n {\n slot: 'hostSeo',\n widgetId: 'ai-site-seo-start',\n title: 'The listing your answers describe',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiSiteSeoStartCard,\n },\n // The agency batch (AGL-2911): one brief across many of the org's\n // sites, from the page that lists them. The card asks the jobs route\n // about the release flag before it shows anything, and its own door\n // holds the plan band and the caller's permission on every site it is\n // pointed at.\n {\n slot: 'orgSites',\n widgetId: 'ai-site-batch',\n title: 'Generate sites with AI',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiSiteBatchCard,\n },\n // Commerce by AI (AGL-2916): product copy in the product editor, the\n // catalog, categories and discounts on the products hub, and copy for\n // an import as it lands. The commerce plugin hosts the zones and makes\n // every write; each card asks the jobs route about the release flag\n // before it shows anything.\n {\n slot: 'productEditor',\n widgetId: 'ai-product-copy',\n title: 'Product copy assistant',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiProductCopyCard,\n },\n {\n slot: 'productsHub',\n widgetId: 'ai-products-hub',\n title: 'Build your catalog with AI',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiProductsHubCard,\n },\n {\n slot: 'productImport',\n widgetId: 'ai-product-import',\n title: 'Write imported product copy with AI',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiProductImportOption,\n },\n // A/B tests by AI (AGL-2914): variants written in the experiment\n // editor, and one test's result read in plain words below its figures.\n // The A/B testing card hosts both zones, so they are drawn only where\n // that card is and only on a site with A/B testing; the shell holds the\n // plan band and `ai.generate`, and each card asks the jobs route about\n // the release flag before it shows anything. Neither writes: the\n // experiment editor's Save is the only write, and an explanation has\n // nothing to apply.\n {\n slot: 'experimentVariants',\n widgetId: 'ai-experiment-variants',\n title: 'Write variants with AI',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiExperimentVariantsCard,\n },\n {\n slot: 'experimentResult',\n widgetId: 'ai-experiment-result',\n title: 'Explain this result with AI',\n featureFlag: 'aiGenerative',\n permission: 'ai.generate',\n Component: AiExperimentResultCard,\n },\n ],\n })\n}\n"],"names":["PLATFORM_BRAND_NAME","registerConsoleExtension","AiCollaboratorPermissionsCell","AiSaveAsComponent","AiGenerateSectionControl","AiRewriteControl","AiAssistProviderOnHost","AssistPanelOnHost","AiCreditsCard","AiJobsTopBarIndicator","AiCollaboratorCreditsCell","AiCollaboratorCreditsHeader","AiMemberCreditsCell","AiMemberCreditsHeader","AssistSignalsPage","BillingAssistOverageCard","AiTopUsersCard","AiAllotmentsCard","AiCollaboratorAllotmentCell","AiCollaboratorAllotmentHeader","AiSiteAllotmentCard","MemberAiAllotmentCard","MemberAiUsageCard","AiThemeProposalCard","StaffOrgAiCard","StaffOrgUsageAiCreditsCell","StaffOrgUsageAiOverageCell","StaffOrgUsageAiPool","StaffOrgUsageAssistCell","StaffOrgsAiSpendCell","StaffOrgsAiSpendHeader","StaffUserAiUsageCard","AiSeoAuditCard","AiSiteBatchCard","AiSiteSeoStartCard","AiSiteStartCard","AiSeoFieldsCard","AiDescribeAutomationButton","AiCrmEmailDraft","AiCrmImportMapping","AiCrmRecordCard","AiDescribePageButton","AiDescribeComponentButton","AiDescribeFormButton","AiDescribeLayoutButton","AiDescribeTemplateButton","AiExperimentResultCard","AiExperimentVariantsCard","AiExplainAutomation","AiExplainRunFailure","AiProductCopyCard","AiProductImportOption","AiProductsHubCard","AiJobsPage","AI_PLUGIN_ID","registerAiDeclarations","registerAiConsole","pluginId","displayName","providers","staffPages","id","label","header","title","docsTopic","Component","navItems","href","recordTitle","docsAnchor","unlisted","ownsSubtree","permission","widgets","slot","widgetId","featureFlag","column","align","Header"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,mBAAmB,EAAEC,wBAAwB,QAAQ,eAAc;AAC5E,SAASC,6BAA6B,QAAQ,+DAA2D;AACzG,SAASC,iBAAiB,QAAQ,iDAA6C;AAC/E,SACEC,wBAAwB,EACxBC,gBAAgB,QACX,iDAA6C;AACpD,SACEC,sBAAsB,EACtBC,iBAAiB,QACZ,mDAA+C;AACtD,OAAOC,mBAAmB,4CAAwC;AAClE,OAAOC,2BAA2B,8CAA0C;AAC5E,SACEC,yBAAyB,EACzBC,2BAA2B,EAC3BC,mBAAmB,EACnBC,qBAAqB,QAChB,+CAA2C;AAClD,SAASC,iBAAiB,QAAQ,gDAA4C;AAC9E,OAAOC,8BAA8B,wDAAoD;AACzF,SAASC,cAAc,QAAQ,iDAA6C;AAC5E,SAASC,gBAAgB,QAAQ,kDAA8C;AAC/E,SACEC,2BAA2B,EAC3BC,6BAA6B,EAC7BC,mBAAmB,QACd,+CAA2C;AAClD,OAAOC,2BAA2B,qDAAiD;AACnF,OAAOC,uBAAuB,iDAA6C;AAC3E,OAAOC,yBAAyB,mDAA+C;AAC/E,OAAOC,oBAAoB,8CAA0C;AACrE,SACEC,0BAA0B,EAC1BC,0BAA0B,EAC1BC,mBAAmB,EACnBC,uBAAuB,QAClB,uDAAmD;AAC1D,SACEC,oBAAoB,EACpBC,sBAAsB,QACjB,uDAAmD;AAC1D,OAAOC,0BAA0B,qDAAiD;AAClF,OAAOC,oBAAoB,8CAA0C;AACrE,OAAOC,qBAAqB,+CAA2C;AACvE,OAAOC,wBAAwB,mDAA+C;AAC9E,OAAOC,qBAAqB,+CAA2C;AACvE,OAAOC,qBAAqB,+CAA2C;AACvE,OAAOC,gCAAgC,mDAA+C;AACtF,OAAOC,qBAAqB,+CAA2C;AACvE,OAAOC,wBAAwB,kDAA8C;AAC7E,OAAOC,qBAAqB,+CAA2C;AACvE,OAAOC,0BAA0B,6CAAyC;AAC1E,SACEC,yBAAyB,EACzBC,oBAAoB,EACpBC,sBAAsB,EACtBC,wBAAwB,QACnB,+CAA2C;AAClD,SACEC,sBAAsB,EACtBC,wBAAwB,QACnB,gDAA4C;AACnD,SACEC,mBAAmB,EACnBC,mBAAmB,QACd,kDAA8C;AACrD,OAAOC,uBAAuB,iDAA6C;AAC3E,OAAOC,2BAA2B,qDAAiD;AACnF,OAAOC,uBAAuB,iDAA6C;AAC3E,OAAOC,gBAAgB,yCAAqC;AAC5D,SAASC,YAAY,QAAQ,iBAAa;AAC1C,SAASC,sBAAsB,QAAQ,oBAAgB;AAEvD;;;;;;;;;;;;CAYC,GACD,OAAO,SAASC;IACdD;IACAtD,yBAAyB;QACvBwD,UAAUH;QACVI,aAAa;QACb,wEAAwE;QACxE,qEAAqE;QACrEC,WAAW;YAACrD;SAAuB;QACnC,wEAAwE;QACxE,2CAA2C;QAC3CsD,YAAY;YACV;gBACEC,IAAI;gBACJC,OAAO;gBACPC,QAAQ;oBACN,gEAAgE;oBAChE,6DAA6D;oBAC7DC,OAAO,GAAGhE,oBAAoB,cAAc,CAAC;oBAC7CiE,WAAW;gBACb;gBACAC,WAAWpD;YACb;SACD;QACD,qEAAqE;QACrE,oEAAoE;QACpE,yEAAyE;QACzE,sEAAsE;QACtE,uEAAuE;QACvE,kEAAkE;QAClEqD,UAAU;YACR;gBACEL,OAAO;gBACPM,MAAM;gBACNC,aAAa;gBACbN,QAAQ;oBAAEC,OAAO;oBAAWC,WAAW;oBAAoBK,YAAY;gBAAwB;gBAC/FC,UAAU;gBACVC,aAAa;gBACbC,YAAY;gBACZP,WAAWb;YACb;SACD;QACDqB,SAAS;YACP;gBACEC,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPE,WAAW3D;YACb;YACA,qEAAqE;YACrE,wEAAwE;YACxE,uEAAuE;YACvE,uDAAuD;YACvD;gBACEoE,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAWzD;YACb;YACA;gBACEkE,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPE,WAAW1D;YACb;YACA;gBACEmE,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPE,WAAWnD;YACb;YACA,kEAAkE;YAClE,kEAAkE;YAClE,oDAAoD;YACpD;gBACE4D,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPS,YAAY;gBACZP,WAAWlD;YACb;YACA,sEAAsE;YACtE,oEAAoE;YACpE,8BAA8B;YAC9B;gBACE2D,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPc,QAAQ;oBAAEf,QAAQ;gBAAK;gBACvBG,WAAWhE;YACb;YACA,uEAAuE;YACvE,mEAAmE;YACnE,sEAAsE;YACtE,2DAA2D;YAC3D;gBACEyE,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPE,WAAW9D;YACb;YACA;gBACEuE,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPE,WAAW7D;YACb;YACA,uEAAuE;YACvE,oEAAoE;YACpE,8DAA8D;YAC9D;gBACEsE,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPS,YAAY;gBACZP,WAAW/D;YACb;YACA;gBACEwE,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPE,WAAW5C;YACb;YACA,qEAAqE;YACrE,mEAAmE;YACnE,gEAAgE;YAChE,6BAA6B;YAC7B;gBACEqD,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPS,YAAY;gBACZP,WAAWjD;YACb;YACA;gBACE0D,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPE,WAAW7C;YACb;YACA;gBACEsD,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPc,QAAQ;oBACNf,QAAQ;oBACRgB,OAAO;oBACPC,QAAQ7D;gBACV;gBACA+C,WAAWhD;YACb;YACA;gBACEyD,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPE,WAAW9C;YACb;YACA,kEAAkE;YAClE,oEAAoE;YACpE,4DAA4D;YAC5D,kEAAkE;YAClE,kEAAkE;YAClE,yCAAyC;YACzC;gBACEuD,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAW3C;YACb;YACA;gBACEoD,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPE,WAAW1C;YACb;YACA;gBACEmD,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPE,WAAWnC;YACb;YACA,oEAAoE;YACpE,oEAAoE;YACpE,yCAAyC;YACzC;gBACE4C,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPc,QAAQ;oBACNf,QAAQ;oBACRgB,OAAO;oBACPC,QAAQlD;gBACV;gBACAoC,WAAWrC;YACb;YACA;gBACE8C,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPc,QAAQ;oBAAEf,QAAQ;oBAAUgB,OAAO;gBAAQ;gBAC3Cb,WAAWtC;YACb;YACA;gBACE+C,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPc,QAAQ;oBAAEf,QAAQ;oBAAmBgB,OAAO;gBAAQ;gBACpDb,WAAWzC;YACb;YACA;gBACEkD,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPc,QAAQ;oBAAEf,QAAQ;oBAAyBgB,OAAO;gBAAQ;gBAC1Db,WAAWxC;YACb;YACA;gBACEiD,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPE,WAAWvC;YACb;YACA;gBACEgD,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPc,QAAQ;oBAAEf,QAAQ;oBAAsBgB,OAAO;oBAASC,QAAQnE;gBAAsB;gBACtFqD,WAAWtD;YACb;YACA;gBACE+D,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPc,QAAQ;oBACNf,QAAQ;oBACRgB,OAAO;oBACPC,QAAQrE;gBACV;gBACAuD,WAAWxD;YACb;YACA,uEAAuE;YACvE,sEAAsE;YACtE,wEAAwE;YACxE,wEAAwE;YACxE,uEAAuE;YACvE,sEAAsE;YACtE;gBACEiE,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAW9B;YACb;YACA;gBACEuC,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAWlC;YACb;YACA,wEAAwE;YACxE,uEAAuE;YACvE,uEAAuE;YACvE;gBACE2C,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAWzB;YACb;YACA,sEAAsE;YACtE,iEAAiE;YACjE,kEAAkE;YAClE,4BAA4B;YAC5B;gBACEkC,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAWrB;YACb;YACA;gBACE8B,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAWtB;YACb;YACA;gBACE+B,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAWvB;YACb;YACA,qEAAqE;YACrE,qEAAqE;YACrE,2BAA2B;YAC3B;gBACEgC,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAWxB;YACb;YACA,wEAAwE;YACxE,0EAA0E;YAC1E,sEAAsE;YACtE,qEAAqE;YACrE,kEAAkE;YAClE,oDAAoD;YACpD;gBACEiC,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAW7B;YACb;YACA;gBACEsC,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAWlB;YACb;YACA;gBACE2B,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAWjB;YACb;YACA,sEAAsE;YACtE,sEAAsE;YACtE,qEAAqE;YACrE,wEAAwE;YACxE,wEAAwE;YACxE,eAAe;YACf;gBACE0B,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAW1B;YACb;YACA;gBACEmC,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAW5B;YACb;YACA;gBACEqC,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAW3B;YACb;YACA,sEAAsE;YACtE,sEAAsE;YACtE,uEAAuE;YACvE,oEAAoE;YACpE,oEAAoE;YACpE,kDAAkD;YAClD;gBACEoC,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAW/B;YACb;YACA,mEAAmE;YACnE,uEAAuE;YACvE,kEAAkE;YAClE,iEAAiE;YACjE,sEAAsE;YACtE,kBAAkB;YAClB;gBACEwC,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAWhC;YACb;YACA,kEAAkE;YAClE,qEAAqE;YACrE,oEAAoE;YACpE,sEAAsE;YACtE,cAAc;YACd;gBACEyC,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAWjC;YACb;YACA,qEAAqE;YACrE,sEAAsE;YACtE,uEAAuE;YACvE,oEAAoE;YACpE,4BAA4B;YAC5B;gBACE0C,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAWhB;YACb;YACA;gBACEyB,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAWd;YACb;YACA;gBACEuB,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAWf;YACb;YACA,iEAAiE;YACjE,uEAAuE;YACvE,sEAAsE;YACtE,wEAAwE;YACxE,uEAAuE;YACvE,iEAAiE;YACjE,qEAAqE;YACrE,oBAAoB;YACpB;gBACEwB,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAWnB;YACb;YACA;gBACE4B,MAAM;gBACNC,UAAU;gBACVZ,OAAO;gBACPa,aAAa;gBACbJ,YAAY;gBACZP,WAAWpB;YACb;SACD;IACH;AACF"}
@@ -108,10 +108,10 @@ export interface AiDoctrineTreeContext extends AiNodeTreeContext {
108
108
  plannedEmbeds?: readonly Pick<AiBuildPlanEmbed, 'host' | 'url'>[];
109
109
  framing?: AiCopyFraming;
110
110
  /**
111
- * Whether the workspace keeps reusable components and saved forms
112
- * (AGL-3030). `false` builds inline, the one way such a workspace can: a
113
- * form is a Form holding its fields, and a repeated block is drawn where it
114
- * repeats. Absent is `true`, the doctrine whole.
111
+ * Whether the workspace keeps reusable components (AGL-3030). `false`
112
+ * draws a repeated block where it repeats. Absent is `true`, the doctrine
113
+ * whole. Forms do not read it: a form is a saved form placed by its id on
114
+ * every plan, counted against `formsPerHost`, or none (AGL-3596).
115
115
  */
116
116
  reusableComponents?: boolean;
117
117
  /**
@@ -251,12 +251,14 @@ export declare function detectLayoutRegions(tree: AiDoctrineTree, outputKind: Ai
251
251
  * the page never renders: the placed form's own design replaces them) are
252
252
  * refused. A form's own design is the one place fields are drawn.
253
253
  *
254
- * A workspace that keeps no saved forms (AGL-3030) has no Forms page to build
255
- * one on, so there the form IS the page's: an unbound Form holding its Form
256
- * Fields, which the site's submit route collects like any other. Loose fields
257
- * are still refused, and so is an unbound Form with no field to send.
254
+ * On every plan (AGL-3596). A workspace that can save no form once drew an
255
+ * unbound Form holding its fields (AGL-3030): inputs on a page with no saved
256
+ * form behind them, no routing, no consent and nothing in the Forms list —
257
+ * which is what a guided start shipped on its first production run. A page
258
+ * with no saved form to place now places no form, so there is no exception
259
+ * left to read: `reusableComponents` decides repeats only.
258
260
  */
259
- export declare function detectInlineForms(tree: AiDoctrineTree, outputKind: AiOutputKind, context?: Pick<AiDoctrineTreeContext, 'reusableComponents'>): AiDoctrineViolation[];
261
+ export declare function detectInlineForms(tree: AiDoctrineTree, outputKind: AiOutputKind): AiDoctrineViolation[];
260
262
  /**
261
263
  * Rule 5. Every color, spacing, radius and type size is a theme token. A
262
264
  * color the theme lacks is a theme change for the theme door to draft, not a
@@ -629,9 +631,11 @@ export declare function detectPlanSplitLists(plan: AiBuildPlan, inventory: AiSit
629
631
  */
630
632
  export declare function detectPlanLayoutRegions(plan: AiBuildPlan, inventory: AiSiteInventory | null, capabilities?: AiPlanCapabilities | null): AiDoctrineViolation[];
631
633
  /**
632
- * Rule 3 (plan): a section that collects answers places a form from the Forms
633
- * page. A workspace that keeps no saved forms draws the form on the page
634
- * (AGL-3030), and nothing is refused.
634
+ * Rule 3 (plan): a section that collects answers places a saved form — one
635
+ * the site has, or one the plan creates on the Forms page. Where neither is
636
+ * possible (the site has no form and this job may create none), the page
637
+ * places no form, and a section named for one is not refused: there is
638
+ * nothing it could place (AGL-3596). It is never drawn inline instead.
635
639
  */
636
640
  export declare function detectPlanInlineForms(plan: AiBuildPlan, inventory: AiSiteInventory | null, capabilities?: AiPlanCapabilities | null): AiDoctrineViolation[];
637
641
  /** Pages sharing one shape this many times are a template applied. */
@@ -23,7 +23,7 @@ import { REUSABLE_INSTANCE_COMPONENT_ID, REUSABLE_INSTANCE_PROP_VALUES_KEY } fro
23
23
  import { parseBreakpointSpan } from "@aglyn/shared-data-enums/breakpoint-span";
24
24
  import { renderEmailHtml } from "@aglyn/shared-util-email/email-render";
25
25
  import { AI_BUILD_PLAN_CREATION_NOUNS, AI_BUILD_PLAN_EMBED_HOST_NAMES, AI_PLAN_ASKS_FOR_VIDEO, aiEmbedHostOf, aiEmbedVideoKey, aiPlanCreateFor, aiPlanSlugKey, aiPlanUndeclaredRefs, isAiPlanNewRef } from "../model/ai-build-plan.js";
26
- import { aiCreationNoun, aiPlanUncreatable, aiPlanUncreatableKind } from "../model/ai-plan-capabilities.js";
26
+ import { aiCreationNoun, aiPlanCanPlaceForm, aiPlanUncreatable, aiPlanUncreatableKind } from "../model/ai-plan-capabilities.js";
27
27
  import { aiFreeSiteSectionsWithin } from "../model/ai-site-job.js";
28
28
  import { AI_INSTANCE_REF_PROP, validateAiNodeTree } from "./ai-node-tree.js";
29
29
  import { AI_PAGE_SCROLL_TO_PROP, aiPageLinkTarget, aiStoredScrollTargets } from "./ai-page-links.js";
@@ -433,19 +433,18 @@ export const AI_LAYOUT_REGION_NAMES = Object.keys(AI_LAYOUT_REGIONS);
433
433
  * the page never renders: the placed form's own design replaces them) are
434
434
  * refused. A form's own design is the one place fields are drawn.
435
435
  *
436
- * A workspace that keeps no saved forms (AGL-3030) has no Forms page to build
437
- * one on, so there the form IS the page's: an unbound Form holding its Form
438
- * Fields, which the site's submit route collects like any other. Loose fields
439
- * are still refused, and so is an unbound Form with no field to send.
440
- */ export function detectInlineForms(tree, outputKind, context = {}) {
436
+ * On every plan (AGL-3596). A workspace that can save no form once drew an
437
+ * unbound Form holding its fields (AGL-3030): inputs on a page with no saved
438
+ * form behind them, no routing, no consent and nothing in the Forms list —
439
+ * which is what a guided start shipped on its first production run. A page
440
+ * with no saved form to place now places no form, so there is no exception
441
+ * left to read: `reusableComponents` decides repeats only.
442
+ */ export function detectInlineForms(tree, outputKind) {
441
443
  if (outputKind === 'form' || outputKind === 'email') return [];
442
- const inline = context.reusableComponents === false;
443
444
  const loose = [];
444
445
  const unbound = [];
445
446
  const shadowed = [];
446
- const empty = [];
447
- const visits = walkTree(tree);
448
- for (const visit of visits){
447
+ for (const visit of walkTree(tree)){
449
448
  if (visit.node.componentId === 'formField' && !hasAncestor(tree, visit, (node)=>node.componentId === 'form')) {
450
449
  loose.push(visit.id);
451
450
  }
@@ -455,35 +454,17 @@ export const AI_LAYOUT_REGION_NAMES = Object.keys(AI_LAYOUT_REGIONS);
455
454
  if (typeof bound === 'string' && bound) {
456
455
  var _visit_node_nodes;
457
456
  if (((_visit_node_nodes = visit.node.nodes) != null ? _visit_node_nodes : []).length) shadowed.push(visit.id);
458
- } else if (!inline) {
457
+ } else {
459
458
  unbound.push(visit.id);
460
- } else if (!visits.some((inner)=>inner.node.componentId === 'formField' && inner.ancestors.includes(visit.id))) {
461
- empty.push(visit.id);
462
459
  }
463
460
  }
464
461
  }
465
462
  const violations = [];
466
- if (inline && loose.length) {
467
- violations.push({
468
- rule: 3,
469
- code: 'loose-form-field',
470
- message: 'This draws form fields outside a form, where nothing sends them. Put every Form Field inside the Form it belongs to.',
471
- nodeIds: loose
472
- });
473
- }
474
- if (empty.length) {
475
- violations.push({
476
- rule: 3,
477
- code: 'form-without-fields',
478
- message: 'This form has no field to send. Give it the Form Fields it collects.',
479
- nodeIds: empty
480
- });
481
- }
482
- if (!inline && (loose.length || unbound.length)) {
463
+ if (loose.length || unbound.length) {
483
464
  violations.push({
484
465
  rule: 3,
485
466
  code: 'inline-form',
486
- message: 'This draws a form inline. Build the form on the Forms page, with its fields, validation, consent and routing, then place it here by its id.',
467
+ message: 'This draws a form inline. Place a saved form from the Forms page by its id, or, where the site has none to place, leave the form off the page.',
487
468
  nodeIds: [
488
469
  ...unbound,
489
470
  ...loose
@@ -2390,7 +2371,7 @@ const BUDGET_METRICS = [
2390
2371
  ...publish,
2391
2372
  ...detectRepeatedSubtrees(tree, otherPages, context),
2392
2373
  ...detectLayoutRegions(tree, outputKind),
2393
- ...detectInlineForms(tree, outputKind, context),
2374
+ ...detectInlineForms(tree, outputKind),
2394
2375
  ...detectLiteralStyles(tree, outputKind),
2395
2376
  ...detectInvisibleLinks(tree, outputKind),
2396
2377
  ...detectOffBrandEmail(tree, outputKind, context.brand),
@@ -2685,11 +2666,13 @@ const FORM_SECTION_NAME = /\bform\b|\bsign[- ]?up\b|\bsubscribe\b|\bnewsletter\b
2685
2666
  return violations;
2686
2667
  }
2687
2668
  /**
2688
- * Rule 3 (plan): a section that collects answers places a form from the Forms
2689
- * page. A workspace that keeps no saved forms draws the form on the page
2690
- * (AGL-3030), and nothing is refused.
2669
+ * Rule 3 (plan): a section that collects answers places a saved form — one
2670
+ * the site has, or one the plan creates on the Forms page. Where neither is
2671
+ * possible (the site has no form and this job may create none), the page
2672
+ * places no form, and a section named for one is not refused: there is
2673
+ * nothing it could place (AGL-3596). It is never drawn inline instead.
2691
2674
  */ export function detectPlanInlineForms(plan, inventory, capabilities = null) {
2692
- if ((capabilities == null ? void 0 : capabilities.reusableComponents) === false) return [];
2675
+ if (!aiPlanCanPlaceForm(inventory, capabilities)) return [];
2693
2676
  const kinds = inventoryKinds(inventory);
2694
2677
  const unbound = sectionPaths(plan).filter(({ section })=>FORM_SECTION_NAME.test(section.name) && !section.uses.some((ref)=>refKind(ref, plan, kinds) === 'form'));
2695
2678
  return unbound.length ? [