@aglyn/plugins-ai 1.0.0-beta.240 → 1.0.0-beta.242

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/package.json +17 -17
  2. package/src/lib/components/ai-job-links.d.ts +17 -0
  3. package/src/lib/components/ai-job-links.js +63 -0
  4. package/src/lib/components/ai-job-links.js.map +1 -1
  5. package/src/lib/components/ai-job-plan.component.js +8 -1
  6. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  7. package/src/lib/components/ai-site-build-page.component.js +7 -1
  8. package/src/lib/components/ai-site-build-page.component.js.map +1 -1
  9. package/src/lib/components/ai-store-finish-card.component.d.ts +28 -0
  10. package/src/lib/components/ai-store-finish-card.component.js +116 -0
  11. package/src/lib/components/ai-store-finish-card.component.js.map +1 -0
  12. package/src/lib/components/assist-build-card.component.js +12 -1
  13. package/src/lib/components/assist-build-card.component.js.map +1 -1
  14. package/src/lib/jobs/ai-job-build-step.d.ts +45 -1
  15. package/src/lib/jobs/ai-job-build-step.js +283 -7
  16. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  17. package/src/lib/jobs/ai-job-drafts.d.ts +9 -1
  18. package/src/lib/jobs/ai-job-drafts.js +4 -1
  19. package/src/lib/jobs/ai-job-drafts.js.map +1 -1
  20. package/src/lib/jobs/ai-job-layout-language.d.ts +3 -0
  21. package/src/lib/jobs/ai-job-layout-language.js +4 -2
  22. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  23. package/src/lib/jobs/ai-job-layout-step.js +3 -1
  24. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  25. package/src/lib/jobs/ai-job-page-language.d.ts +2 -1
  26. package/src/lib/jobs/ai-job-page-language.js +19 -7
  27. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  28. package/src/lib/jobs/ai-job-page-sections.d.ts +11 -0
  29. package/src/lib/jobs/ai-job-page-sections.js +40 -3
  30. package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
  31. package/src/lib/jobs/ai-job-page-step.d.ts +2 -1
  32. package/src/lib/jobs/ai-job-page-step.js +19 -8
  33. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  34. package/src/lib/jobs/ai-job-plan-step.d.ts +12 -0
  35. package/src/lib/jobs/ai-job-plan-step.js +41 -7
  36. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  37. package/src/lib/jobs/ai-job-site-content.js +3 -2
  38. package/src/lib/jobs/ai-job-site-content.js.map +1 -1
  39. package/src/lib/jobs/ai-job-site-datasets.d.ts +76 -2
  40. package/src/lib/jobs/ai-job-site-datasets.js +206 -13
  41. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
  42. package/src/lib/jobs/ai-job-site-step.d.ts +18 -2
  43. package/src/lib/jobs/ai-job-site-step.js +109 -22
  44. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  45. package/src/lib/jobs/ai-job-site-store-pages.d.ts +75 -0
  46. package/src/lib/jobs/ai-job-site-store-pages.js +195 -0
  47. package/src/lib/jobs/ai-job-site-store-pages.js.map +1 -0
  48. package/src/lib/jobs/ai-job-store-links-layout.d.ts +71 -0
  49. package/src/lib/jobs/ai-job-store-links-layout.js +95 -0
  50. package/src/lib/jobs/ai-job-store-links-layout.js.map +1 -0
  51. package/src/lib/jobs/ai-layout-stock-photos.d.ts +129 -8
  52. package/src/lib/jobs/ai-layout-stock-photos.js +919 -75
  53. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  54. package/src/lib/jobs/ai-site-publish.d.ts +18 -0
  55. package/src/lib/jobs/ai-site-publish.js +76 -15
  56. package/src/lib/jobs/ai-site-publish.js.map +1 -1
  57. package/src/lib/jobs/fixtures/ai-stock-photo-ember-oak-hits.d.ts +43 -0
  58. package/src/lib/jobs/fixtures/ai-stock-photo-ember-oak-hits.js +61 -0
  59. package/src/lib/jobs/fixtures/ai-stock-photo-ember-oak-hits.js.map +1 -0
  60. package/src/lib/jobs/fixtures/ai-stock-photo-saltmarsh-recordings.d.ts +39 -0
  61. package/src/lib/jobs/fixtures/ai-stock-photo-saltmarsh-recordings.js +508 -0
  62. package/src/lib/jobs/fixtures/ai-stock-photo-saltmarsh-recordings.js.map +1 -0
  63. package/src/lib/layout-language/ai-layout-compiler.js +25 -2
  64. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  65. package/src/lib/layout-language/ai-layout-frame.d.ts +10 -0
  66. package/src/lib/layout-language/ai-layout-frame.js +58 -4
  67. package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
  68. package/src/lib/layout-language/ai-layout-listings.d.ts +6 -0
  69. package/src/lib/layout-language/ai-layout-listings.js +3 -0
  70. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  71. package/src/lib/model/ai-build-job.d.ts +2 -0
  72. package/src/lib/model/ai-build-job.js +23 -3
  73. package/src/lib/model/ai-build-job.js.map +1 -1
  74. package/src/lib/model/ai-build-plan.d.ts +7 -0
  75. package/src/lib/model/ai-build-plan.js.map +1 -1
  76. package/src/lib/model/ai-build-progress.js +3 -1
  77. package/src/lib/model/ai-build-progress.js.map +1 -1
  78. package/src/lib/model/ai-build-store-pages.d.ts +133 -0
  79. package/src/lib/model/ai-build-store-pages.js +209 -0
  80. package/src/lib/model/ai-build-store-pages.js.map +1 -0
  81. package/src/lib/model/ai-layout-store-links.d.ts +64 -0
  82. package/src/lib/model/ai-layout-store-links.js +165 -0
  83. package/src/lib/model/ai-layout-store-links.js.map +1 -0
  84. package/src/lib/model/ai-site-inventory.d.ts +5 -0
  85. package/src/lib/model/ai-site-inventory.js.map +1 -1
  86. package/src/lib/model/ai-site-store-pages.d.ts +192 -0
  87. package/src/lib/model/ai-site-store-pages.js +383 -0
  88. package/src/lib/model/ai-site-store-pages.js.map +1 -0
  89. package/src/lib/runtime/site-inventory.js +8 -3
  90. package/src/lib/runtime/site-inventory.js.map +1 -1
  91. package/src/lib/testing/stand-in-dataset-index.js +8 -4
  92. package/src/lib/testing/stand-in-dataset-index.js.map +1 -1
@@ -0,0 +1,195 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { checkEntitlement } from "@aglyn/aglyn/app-utils/plan-entitlements";
18
+ import { hostRoleCanWrite } from "@aglyn/aglyn/app-utils/organizations";
19
+ import { ACCOUNTS_PLUGIN_ID } from "@aglyn/aglyn/plugin-manager/enabled-plugins";
20
+ import { HostScreenVisibility } from "@aglyn/aglyn/foundation/definitions/platform.types";
21
+ import { FieldValue } from "firebase-admin/firestore";
22
+ import { aiStorePageNodes, aiStorePagesToWrite, AI_STORE_PAGES } from "../model/ai-site-store-pages.js";
23
+ import { aiModelForStep } from "../providers/routing.js";
24
+ import { aiOriginJobId } from "./ai-job-draft-ids.js";
25
+ import { writeAiDraft, writeAiDraftScreenSeo } from "./ai-job-drafts.js";
26
+ import { aiUnspentOutcome } from "./ai-job-generation.js";
27
+ import { aiPluginDraftAdmissionRefusal } from "./ai-job-plugin-drafts.js";
28
+ /**
29
+ * The store pages' unit of a site start (AGL-3676): the account, cart and
30
+ * policy pages a paid store gets beside its planned pages
31
+ * (`model/ai-site-store-pages.ts`), written by code in one pass that asks no
32
+ * model and costs no credits.
33
+ *
34
+ * Each page is a draft written through the same writer, under the same pages
35
+ * allowance, as every page the start builds (`writeAiDraft`), under an id
36
+ * derived from the unit's job, so a pass run again finds its own drafts. The
37
+ * guided start's publish then puts them live with the rest.
38
+ *
39
+ * The site's user accounts are turned on here too: the Customer account's
40
+ * sign-in, and the platform's /signin and /signup, answer only where the site
41
+ * serves accounts (`accounts` is off by default on a site that predates
42
+ * 2026-08; every new site is born with it). A site whose owner switched them
43
+ * off keeps them off: an explicit deny is theirs.
44
+ */ /** The unit input a store pages unit carries what it writes in. */ export const AI_SITE_STORE_PAGES_INPUT = 'storePages';
45
+ /** The commerce plugin's name, in the refusal that asks for it. */ const STORE_LABEL = 'Commerce';
46
+ /** What a store pages row says where nothing could be written: our failure. */ export const AI_SITE_STORE_PAGES_NOT_WRITTEN_COPY = 'Your account, cart and policy pages could not be added this time.';
47
+ /** The id of the store page a unit writes for `key`. */ export function aiSiteStorePageId(unitJobId, key) {
48
+ return `${unitJobId}-${key}`;
49
+ }
50
+ const PATH = /^\/[a-z0-9-]*$/;
51
+ function inputOf(job) {
52
+ var _job_inputs;
53
+ const raw = (_job_inputs = job.inputs) == null ? void 0 : _job_inputs[AI_SITE_STORE_PAGES_INPUT];
54
+ if (!raw || !Array.isArray(raw.pages) || !raw.facts) return null;
55
+ const keys = new Set(AI_STORE_PAGES.map((page)=>page.key));
56
+ const pages = raw.pages.filter((page)=>page && keys.has(page.key) && typeof page.slug === 'string');
57
+ const path = (value, fallback)=>typeof value === 'string' && PATH.test(value) ? value : fallback;
58
+ return {
59
+ pages,
60
+ facts: {
61
+ contactPath: raw.facts.contactPath ? path(raw.facts.contactPath, '') || null : null,
62
+ shopPath: path(raw.facts.shopPath, '/shop'),
63
+ shippingPath: path(raw.facts.shippingPath, '/shipping-returns')
64
+ },
65
+ layoutId: typeof raw.layoutId === 'string' && raw.layoutId ? raw.layoutId : null
66
+ };
67
+ }
68
+ /**
69
+ * Whether the store pages may be added for this member on this site, asked
70
+ * before their pass so a refusal spends nothing; `null` admits. The plan must
71
+ * include a store (Starter and up), the commerce plugin must run here, and the
72
+ * member must be able to edit the site's pages.
73
+ */ export async function aiSiteStorePagesRefusal(context, deps = {}) {
74
+ var _context_org, _deps_admission, _context_org1, _host_get;
75
+ const { job, firestore } = context;
76
+ if (!job.hostId) return 'Open the site first.';
77
+ if (!checkEntitlement((_context_org = context.org) != null ? _context_org : null, 'commerce')) return 'Your plan does not include a store.';
78
+ const refused = await ((_deps_admission = deps.admission) != null ? _deps_admission : aiPluginDraftAdmissionRefusal)({
79
+ firestore,
80
+ orgId: job.orgId,
81
+ hostId: job.hostId,
82
+ inputs: {},
83
+ org: (_context_org1 = context.org) != null ? _context_org1 : null
84
+ }, {
85
+ kind: 'site',
86
+ drafts: [
87
+ {
88
+ resource: 'product',
89
+ label: STORE_LABEL
90
+ }
91
+ ],
92
+ now: context.now
93
+ });
94
+ if (refused) return refused.error;
95
+ const host = await firestore.collection('hosts').doc(job.hostId).get();
96
+ return hostRoleCanWrite(((_host_get = host.get('memberRoles')) != null ? _host_get : {})[job.createdBy]) ? null : 'Editing requires the editor role';
97
+ }
98
+ /** The store pages' pass: every page written, user accounts on, one output a page. */ export function createAiSiteStorePagesRunner(deps = {}) {
99
+ var _deps_write, _deps_writeSeo;
100
+ const write = (_deps_write = deps.write) != null ? _deps_write : writeAiDraft;
101
+ const writeSeo = (_deps_writeSeo = deps.writeSeo) != null ? _deps_writeSeo : writeAiDraftScreenSeo;
102
+ return async (context)=>{
103
+ var _ref;
104
+ const { job, firestore, now } = context;
105
+ const model = (_ref = context.modelFor == null ? void 0 : context.modelFor.call(context, 'job.plan')) != null ? _ref : aiModelForStep('job.plan');
106
+ const input = inputOf(job);
107
+ const hostId = job.hostId;
108
+ if (!input || !hostId) return _extends({}, aiUnspentOutcome(model), {
109
+ failure: AI_SITE_STORE_PAGES_NOT_WRITTEN_COPY
110
+ });
111
+ const outputs = [];
112
+ let refusal = null;
113
+ for (const page of aiStorePagesToWrite(input.pages)){
114
+ var _context_org;
115
+ const definition = AI_STORE_PAGES.find((one)=>one.key === page.key);
116
+ if (!definition) continue;
117
+ const id = aiSiteStorePageId(job.$id, page.key);
118
+ const draft = await write(firestore, _extends({
119
+ kind: 'screen',
120
+ hostId,
121
+ id,
122
+ uid: job.createdBy,
123
+ org: (_context_org = context.org) != null ? _context_org : null,
124
+ name: definition.title,
125
+ slug: page.slug,
126
+ nodes: aiStorePageNodes(page.key, input.facts),
127
+ layoutId: input.layoutId
128
+ }, definition.noindex ? {
129
+ visibility: HostScreenVisibility.UNLISTED
130
+ } : {}, {
131
+ aiJobId: aiOriginJobId(job),
132
+ now
133
+ }));
134
+ if (draft.ok === false) {
135
+ // The pages allowance is full, or the site is gone: what was written stays.
136
+ refusal = draft.error;
137
+ break;
138
+ }
139
+ await writeSeo(firestore, {
140
+ hostId,
141
+ id,
142
+ seo: definition.seo,
143
+ now
144
+ });
145
+ outputs.push({
146
+ resource: 'screen',
147
+ id,
148
+ versionId: draft.versionId,
149
+ hostId,
150
+ hostSubdomain: draft.hostSubdomain,
151
+ label: draft.name,
152
+ proposal: {
153
+ storePage: page.key,
154
+ path: page.href
155
+ }
156
+ });
157
+ }
158
+ if (outputs.length) await turnOnSiteAccounts(firestore, hostId, now);
159
+ if (!outputs.length) {
160
+ return refusal ? aiUnspentOutcome(model, {
161
+ review: {
162
+ reason: 'limit',
163
+ message: refusal,
164
+ findings: []
165
+ }
166
+ }) : _extends({}, aiUnspentOutcome(model), {
167
+ failure: AI_SITE_STORE_PAGES_NOT_WRITTEN_COPY
168
+ });
169
+ }
170
+ return aiUnspentOutcome(model, {
171
+ outputs
172
+ });
173
+ };
174
+ }
175
+ export const runAiSiteStorePagesUnit = createAiSiteStorePagesRunner();
176
+ /**
177
+ * Turns on the site's user accounts (`accounts`, default-off per site), so
178
+ * the account page's sign-in and the platform's /signin and /signup answer.
179
+ * An owner's explicit switch-off is left as it is.
180
+ */ export async function turnOnSiteAccounts(firestore, hostId, now) {
181
+ var _host_get, _host_get1;
182
+ const hostRef = firestore.collection('hosts').doc(hostId);
183
+ const host = await hostRef.get();
184
+ if (!host.exists) return false;
185
+ const disabled = (_host_get = host.get('disabledPlugins')) != null ? _host_get : [];
186
+ const enabled = (_host_get1 = host.get('enabledPlugins')) != null ? _host_get1 : [];
187
+ if (disabled.includes(ACCOUNTS_PLUGIN_ID) || enabled.includes(ACCOUNTS_PLUGIN_ID)) return false;
188
+ await hostRef.update({
189
+ enabledPlugins: FieldValue.arrayUnion(ACCOUNTS_PLUGIN_ID),
190
+ updatedAt: now
191
+ });
192
+ return true;
193
+ }
194
+
195
+ //# sourceMappingURL=ai-job-site-store-pages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-site-store-pages.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { hostRoleCanWrite } from '@aglyn/aglyn/app-utils/organizations'\nimport { ACCOUNTS_PLUGIN_ID } from '@aglyn/aglyn/plugin-manager/enabled-plugins'\nimport { HostScreenVisibility } from '@aglyn/aglyn/foundation/definitions/platform.types'\nimport type { NodesMap } from '@aglyn/aglyn/types/nodes'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport type { AiJob, AiJobOutput } from '../model/ai-jobs.types'\nimport {\n aiStorePageNodes,\n aiStorePagesToWrite,\n AI_STORE_PAGES,\n type AiStorePageFacts,\n type AiStorePagePlan,\n} from '../model/ai-site-store-pages'\nimport { aiModelForStep } from '../providers/routing'\nimport { aiOriginJobId } from './ai-job-draft-ids'\nimport { writeAiDraft, writeAiDraftScreenSeo } from './ai-job-drafts'\nimport { aiUnspentOutcome } from './ai-job-generation'\nimport { aiPluginDraftAdmissionRefusal } from './ai-job-plugin-drafts'\nimport type { AiJobStepContext, AiJobStepOutcome, AiJobStepRunner } from './ai-job-text-step'\n\n/**\n * The store pages' unit of a site start (AGL-3676): the account, cart and\n * policy pages a paid store gets beside its planned pages\n * (`model/ai-site-store-pages.ts`), written by code in one pass that asks no\n * model and costs no credits.\n *\n * Each page is a draft written through the same writer, under the same pages\n * allowance, as every page the start builds (`writeAiDraft`), under an id\n * derived from the unit's job, so a pass run again finds its own drafts. The\n * guided start's publish then puts them live with the rest.\n *\n * The site's user accounts are turned on here too: the Customer account's\n * sign-in, and the platform's /signin and /signup, answer only where the site\n * serves accounts (`accounts` is off by default on a site that predates\n * 2026-08; every new site is born with it). A site whose owner switched them\n * off keeps them off: an explicit deny is theirs.\n */\n\n/** The unit input a store pages unit carries what it writes in. */\nexport const AI_SITE_STORE_PAGES_INPUT = 'storePages'\n\n/** What a store pages unit is told: the store pages the plan implies, and what their words link. */\nexport interface AiSiteStorePagesInput {\n pages: AiStorePagePlan[]\n facts: AiStorePageFacts\n /** The layout the start built, which the pages render inside; the site's otherwise. */\n layoutId: string | null\n}\n\n/** The commerce plugin's name, in the refusal that asks for it. */\nconst STORE_LABEL = 'Commerce'\n\n/** What a store pages row says where nothing could be written: our failure. */\nexport const AI_SITE_STORE_PAGES_NOT_WRITTEN_COPY = 'Your account, cart and policy pages could not be added this time.'\n\n/** The id of the store page a unit writes for `key`. */\nexport function aiSiteStorePageId(unitJobId: string, key: string): string {\n return `${unitJobId}-${key}`\n}\n\nconst PATH = /^\\/[a-z0-9-]*$/\n\nfunction inputOf(job: Pick<AiJob, 'inputs'>): AiSiteStorePagesInput | null {\n const raw = job.inputs?.[AI_SITE_STORE_PAGES_INPUT] as Partial<AiSiteStorePagesInput> | undefined\n if (!raw || !Array.isArray(raw.pages) || !raw.facts) return null\n const keys = new Set(AI_STORE_PAGES.map((page) => page.key))\n const pages = raw.pages.filter((page) => page && keys.has(page.key) && typeof page.slug === 'string')\n const path = (value: unknown, fallback: string) => (typeof value === 'string' && PATH.test(value) ? value : fallback)\n return {\n pages,\n facts: {\n contactPath: raw.facts.contactPath ? path(raw.facts.contactPath, '') || null : null,\n shopPath: path(raw.facts.shopPath, '/shop'),\n shippingPath: path(raw.facts.shippingPath, '/shipping-returns'),\n },\n layoutId: typeof raw.layoutId === 'string' && raw.layoutId ? raw.layoutId : null,\n }\n}\n\n/**\n * Whether the store pages may be added for this member on this site, asked\n * before their pass so a refusal spends nothing; `null` admits. The plan must\n * include a store (Starter and up), the commerce plugin must run here, and the\n * member must be able to edit the site's pages.\n */\nexport async function aiSiteStorePagesRefusal(\n context: Pick<AiJobStepContext, 'firestore' | 'org' | 'now'> & { job: Pick<AiJob, 'orgId' | 'hostId' | 'createdBy'> },\n deps: { admission?: typeof aiPluginDraftAdmissionRefusal } = {},\n): Promise<string | null> {\n const { job, firestore } = context\n if (!job.hostId) return 'Open the site first.'\n if (!checkEntitlement((context.org ?? null) as never, 'commerce')) return 'Your plan does not include a store.'\n const refused = await (deps.admission ?? aiPluginDraftAdmissionRefusal)(\n { firestore, orgId: job.orgId, hostId: job.hostId, inputs: {}, org: context.org ?? null },\n { kind: 'site', drafts: [{ resource: 'product', label: STORE_LABEL }], now: context.now },\n )\n if (refused) return refused.error\n const host = await firestore.collection('hosts').doc(job.hostId).get()\n return hostRoleCanWrite((host.get('memberRoles') ?? {})[job.createdBy]) ? null : 'Editing requires the editor role'\n}\n\nexport interface AiSiteStorePagesRunnerDeps {\n write?: typeof writeAiDraft\n writeSeo?: typeof writeAiDraftScreenSeo\n}\n\n/** The store pages' pass: every page written, user accounts on, one output a page. */\nexport function createAiSiteStorePagesRunner(deps: AiSiteStorePagesRunnerDeps = {}): AiJobStepRunner {\n const write = deps.write ?? writeAiDraft\n const writeSeo = deps.writeSeo ?? writeAiDraftScreenSeo\n return async (context): Promise<AiJobStepOutcome> => {\n const { job, firestore, now } = context\n const model = context.modelFor?.('job.plan') ?? aiModelForStep('job.plan')\n const input = inputOf(job)\n const hostId = job.hostId\n if (!input || !hostId) return { ...aiUnspentOutcome(model), failure: AI_SITE_STORE_PAGES_NOT_WRITTEN_COPY }\n const outputs: AiJobOutput[] = []\n let refusal: string | null = null\n for (const page of aiStorePagesToWrite(input.pages)) {\n const definition = AI_STORE_PAGES.find((one) => one.key === page.key)\n if (!definition) continue\n const id = aiSiteStorePageId(job.$id, page.key)\n const draft = await write(firestore, {\n kind: 'screen',\n hostId,\n id,\n uid: job.createdBy,\n org: context.org ?? null,\n name: definition.title,\n slug: page.slug,\n nodes: aiStorePageNodes(page.key, input.facts) as unknown as NodesMap,\n layoutId: input.layoutId,\n // The account and the cart are a shopper's own pages: unlisted, noindex (AGL-3676).\n ...(definition.noindex ? { visibility: HostScreenVisibility.UNLISTED } : {}),\n aiJobId: aiOriginJobId(job),\n now,\n })\n if (draft.ok === false) {\n // The pages allowance is full, or the site is gone: what was written stays.\n refusal = draft.error\n break\n }\n await writeSeo(firestore, { hostId, id, seo: definition.seo, now })\n outputs.push({\n resource: 'screen',\n id,\n versionId: draft.versionId,\n hostId,\n hostSubdomain: draft.hostSubdomain,\n label: draft.name,\n proposal: { storePage: page.key, path: page.href },\n })\n }\n if (outputs.length) await turnOnSiteAccounts(firestore, hostId, now)\n if (!outputs.length) {\n return refusal\n ? aiUnspentOutcome(model, { review: { reason: 'limit', message: refusal, findings: [] } })\n : { ...aiUnspentOutcome(model), failure: AI_SITE_STORE_PAGES_NOT_WRITTEN_COPY }\n }\n return aiUnspentOutcome(model, { outputs })\n }\n}\n\nexport const runAiSiteStorePagesUnit = createAiSiteStorePagesRunner()\n\n/**\n * Turns on the site's user accounts (`accounts`, default-off per site), so\n * the account page's sign-in and the platform's /signin and /signup answer.\n * An owner's explicit switch-off is left as it is.\n */\nexport async function turnOnSiteAccounts(firestore: FirebaseFirestore.Firestore, hostId: string, now: Date): Promise<boolean> {\n const hostRef = firestore.collection('hosts').doc(hostId)\n const host = await hostRef.get()\n if (!host.exists) return false\n const disabled = (host.get('disabledPlugins') ?? []) as unknown[]\n const enabled = (host.get('enabledPlugins') ?? []) as unknown[]\n if (disabled.includes(ACCOUNTS_PLUGIN_ID) || enabled.includes(ACCOUNTS_PLUGIN_ID)) return false\n await hostRef.update({ enabledPlugins: FieldValue.arrayUnion(ACCOUNTS_PLUGIN_ID), updatedAt: now })\n return true\n}\n"],"names":["checkEntitlement","hostRoleCanWrite","ACCOUNTS_PLUGIN_ID","HostScreenVisibility","FieldValue","aiStorePageNodes","aiStorePagesToWrite","AI_STORE_PAGES","aiModelForStep","aiOriginJobId","writeAiDraft","writeAiDraftScreenSeo","aiUnspentOutcome","aiPluginDraftAdmissionRefusal","AI_SITE_STORE_PAGES_INPUT","STORE_LABEL","AI_SITE_STORE_PAGES_NOT_WRITTEN_COPY","aiSiteStorePageId","unitJobId","key","PATH","inputOf","job","raw","inputs","Array","isArray","pages","facts","keys","Set","map","page","filter","has","slug","path","value","fallback","test","contactPath","shopPath","shippingPath","layoutId","aiSiteStorePagesRefusal","context","deps","host","firestore","hostId","org","refused","admission","orgId","kind","drafts","resource","label","now","error","collection","doc","get","createdBy","createAiSiteStorePagesRunner","write","writeSeo","model","modelFor","input","failure","outputs","refusal","definition","find","one","id","$id","draft","uid","name","title","nodes","noindex","visibility","UNLISTED","aiJobId","ok","seo","push","versionId","hostSubdomain","proposal","storePage","href","length","turnOnSiteAccounts","review","reason","message","findings","runAiSiteStorePagesUnit","hostRef","exists","disabled","enabled","includes","update","enabledPlugins","arrayUnion","updatedAt"],"mappings":"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"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { Firestore } from 'firebase-admin/firestore';
18
+ import { type AiStoreLayoutLinks } from '../model/ai-layout-store-links';
19
+ /**
20
+ * A store's links in the site's own layout, as a DRAFT (AGL-3676): a build
21
+ * that makes the site a store and builds no layout writes a new version of
22
+ * the layout its store pages render inside, with Account (and Cart, where
23
+ * the header has none) in the header and the account and policies in the
24
+ * footer (`aiLayoutWithStoreLinks`). The layout's current version — the one
25
+ * visitors see — is never touched: the new version waits beside it, and goes
26
+ * live only when the build publishes (`aiPublishGuidedSite`'s `storeLinks`),
27
+ * as every draft the build makes does.
28
+ */
29
+ /** The field a draft version keeps the version it was made from in, so a publish knows whether it is still current. */
30
+ export declare const AI_STORE_LINKS_SOURCE_FIELD = "aiStoreLinksSourceVersionId";
31
+ /** What a store links draft version is called in the layout's versions. */
32
+ export declare const AI_STORE_LINKS_VERSION_NAME = "Account and policy links";
33
+ /** What writing a store's links into the site's layout came to. */
34
+ export type AiStoreLinksLayoutDraft = {
35
+ status: 'written';
36
+ layoutId: string;
37
+ versionId: string;
38
+ name: string;
39
+ }
40
+ /** Every link was already there. */
41
+ | {
42
+ status: 'present';
43
+ layoutId: string;
44
+ }
45
+ /** The layout has no header list or no footer list to add to: what it had room for, if anything, was written. */
46
+ | {
47
+ status: 'unrecognized';
48
+ layoutId: string;
49
+ versionId: string | null;
50
+ name: string;
51
+ }
52
+ /** The layout is gone, or has no current version to start from. */
53
+ | {
54
+ status: 'gone';
55
+ };
56
+ /**
57
+ * Writes the draft version (idempotent: a version with this id already
58
+ * written is that draft). Reads the layout's current version and writes the
59
+ * new one beside it, a copy of it — its fields, everything the editor stored
60
+ * — with the links added.
61
+ */
62
+ export declare function writeAiStoreLinksLayoutDraft(firestore: Firestore, input: {
63
+ hostId: string;
64
+ layoutId: string;
65
+ versionId: string;
66
+ links: AiStoreLayoutLinks;
67
+ screenPaths: Readonly<Record<string, string>>;
68
+ uid: string;
69
+ aiJobId: string;
70
+ now: Date;
71
+ }): Promise<AiStoreLinksLayoutDraft>;
@@ -0,0 +1,95 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { decodeStoredNodes, encodeStoredNodes } from "@aglyn/aglyn/app-utils/stored-nodes";
18
+ import { aiLayoutWithStoreLinks } from "../model/ai-layout-store-links.js";
19
+ /**
20
+ * A store's links in the site's own layout, as a DRAFT (AGL-3676): a build
21
+ * that makes the site a store and builds no layout writes a new version of
22
+ * the layout its store pages render inside, with Account (and Cart, where
23
+ * the header has none) in the header and the account and policies in the
24
+ * footer (`aiLayoutWithStoreLinks`). The layout's current version — the one
25
+ * visitors see — is never touched: the new version waits beside it, and goes
26
+ * live only when the build publishes (`aiPublishGuidedSite`'s `storeLinks`),
27
+ * as every draft the build makes does.
28
+ */ /** The field a draft version keeps the version it was made from in, so a publish knows whether it is still current. */ export const AI_STORE_LINKS_SOURCE_FIELD = 'aiStoreLinksSourceVersionId';
29
+ /** What a store links draft version is called in the layout's versions. */ export const AI_STORE_LINKS_VERSION_NAME = 'Account and policy links';
30
+ /**
31
+ * Writes the draft version (idempotent: a version with this id already
32
+ * written is that draft). Reads the layout's current version and writes the
33
+ * new one beside it, a copy of it — its fields, everything the editor stored
34
+ * — with the links added.
35
+ */ export async function writeAiStoreLinksLayoutDraft(firestore, input) {
36
+ var _layout_get;
37
+ const layoutRef = firestore.collection('hosts').doc(input.hostId).collection('layouts').doc(input.layoutId);
38
+ const layout = await layoutRef.get();
39
+ const currentVersionId = layout.get('versionId');
40
+ if (!layout.exists || layout.get('deletedAt') != null || typeof currentVersionId !== 'string' || !currentVersionId) {
41
+ return {
42
+ status: 'gone'
43
+ };
44
+ }
45
+ const name = String((_layout_get = layout.get('displayName')) != null ? _layout_get : '') || 'Layout';
46
+ const draftRef = layoutRef.collection('versions').doc(input.versionId);
47
+ const already = await draftRef.get();
48
+ if (already.exists) return {
49
+ status: 'written',
50
+ layoutId: input.layoutId,
51
+ versionId: input.versionId,
52
+ name
53
+ };
54
+ const current = await layoutRef.collection('versions').doc(currentVersionId).get();
55
+ const nodes = current.exists ? decodeStoredNodes(current.get('nodes')) : null;
56
+ if (!nodes) return {
57
+ status: 'gone'
58
+ };
59
+ const result = aiLayoutWithStoreLinks(nodes, input.links, input.screenPaths);
60
+ const recognized = result.header && result.footer;
61
+ const packed = result.nodes ? encodeStoredNodes(result.nodes) : null;
62
+ if (!packed) return recognized ? {
63
+ status: 'present',
64
+ layoutId: input.layoutId
65
+ } : {
66
+ status: 'unrecognized',
67
+ layoutId: input.layoutId,
68
+ versionId: null,
69
+ name
70
+ };
71
+ const copy = _extends({}, current.data());
72
+ delete copy['aiEditJobId'];
73
+ await draftRef.create(_extends({}, copy, {
74
+ nodes: Buffer.from(packed),
75
+ displayName: AI_STORE_LINKS_VERSION_NAME,
76
+ createdAt: input.now,
77
+ updatedAt: input.now,
78
+ createdBy: input.uid,
79
+ aiJobId: input.aiJobId,
80
+ [AI_STORE_LINKS_SOURCE_FIELD]: currentVersionId
81
+ }));
82
+ return recognized ? {
83
+ status: 'written',
84
+ layoutId: input.layoutId,
85
+ versionId: input.versionId,
86
+ name
87
+ } : {
88
+ status: 'unrecognized',
89
+ layoutId: input.layoutId,
90
+ versionId: input.versionId,
91
+ name
92
+ };
93
+ }
94
+
95
+ //# sourceMappingURL=ai-job-store-links-layout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-store-links-layout.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 { decodeStoredNodes, encodeStoredNodes } from '@aglyn/aglyn/app-utils/stored-nodes'\nimport type { Firestore } from 'firebase-admin/firestore'\nimport { aiLayoutWithStoreLinks, type AiStoreLayoutLinks } from '../model/ai-layout-store-links'\n\n/**\n * A store's links in the site's own layout, as a DRAFT (AGL-3676): a build\n * that makes the site a store and builds no layout writes a new version of\n * the layout its store pages render inside, with Account (and Cart, where\n * the header has none) in the header and the account and policies in the\n * footer (`aiLayoutWithStoreLinks`). The layout's current version — the one\n * visitors see — is never touched: the new version waits beside it, and goes\n * live only when the build publishes (`aiPublishGuidedSite`'s `storeLinks`),\n * as every draft the build makes does.\n */\n\n/** The field a draft version keeps the version it was made from in, so a publish knows whether it is still current. */\nexport const AI_STORE_LINKS_SOURCE_FIELD = 'aiStoreLinksSourceVersionId'\n\n/** What a store links draft version is called in the layout's versions. */\nexport const AI_STORE_LINKS_VERSION_NAME = 'Account and policy links'\n\n/** What writing a store's links into the site's layout came to. */\nexport type AiStoreLinksLayoutDraft =\n | { status: 'written'; layoutId: string; versionId: string; name: string }\n /** Every link was already there. */\n | { status: 'present'; layoutId: string }\n /** The layout has no header list or no footer list to add to: what it had room for, if anything, was written. */\n | { status: 'unrecognized'; layoutId: string; versionId: string | null; name: string }\n /** The layout is gone, or has no current version to start from. */\n | { status: 'gone' }\n\n/**\n * Writes the draft version (idempotent: a version with this id already\n * written is that draft). Reads the layout's current version and writes the\n * new one beside it, a copy of it — its fields, everything the editor stored\n * — with the links added.\n */\nexport async function writeAiStoreLinksLayoutDraft(\n firestore: Firestore,\n input: {\n hostId: string\n layoutId: string\n versionId: string\n links: AiStoreLayoutLinks\n screenPaths: Readonly<Record<string, string>>\n uid: string\n aiJobId: string\n now: Date\n },\n): Promise<AiStoreLinksLayoutDraft> {\n const layoutRef = firestore.collection('hosts').doc(input.hostId).collection('layouts').doc(input.layoutId)\n const layout = await layoutRef.get()\n const currentVersionId = layout.get('versionId')\n if (!layout.exists || layout.get('deletedAt') != null || typeof currentVersionId !== 'string' || !currentVersionId) {\n return { status: 'gone' }\n }\n const name = String(layout.get('displayName') ?? '') || 'Layout'\n const draftRef = layoutRef.collection('versions').doc(input.versionId)\n const already = await draftRef.get()\n if (already.exists) return { status: 'written', layoutId: input.layoutId, versionId: input.versionId, name }\n const current = await layoutRef.collection('versions').doc(currentVersionId).get()\n const nodes = current.exists ? decodeStoredNodes<Record<string, unknown>>(current.get('nodes')) : null\n if (!nodes) return { status: 'gone' }\n const result = aiLayoutWithStoreLinks(nodes, input.links, input.screenPaths)\n const recognized = result.header && result.footer\n const packed = result.nodes ? encodeStoredNodes(result.nodes as never) : null\n if (!packed) return recognized ? { status: 'present', layoutId: input.layoutId } : { status: 'unrecognized', layoutId: input.layoutId, versionId: null, name }\n const copy = { ...(current.data() as Record<string, unknown>) }\n delete copy['aiEditJobId']\n await draftRef.create({\n ...copy,\n nodes: Buffer.from(packed),\n displayName: AI_STORE_LINKS_VERSION_NAME,\n createdAt: input.now,\n updatedAt: input.now,\n createdBy: input.uid,\n aiJobId: input.aiJobId,\n [AI_STORE_LINKS_SOURCE_FIELD]: currentVersionId,\n })\n return recognized\n ? { status: 'written', layoutId: input.layoutId, versionId: input.versionId, name }\n : { status: 'unrecognized', layoutId: input.layoutId, versionId: input.versionId, name }\n}\n"],"names":["decodeStoredNodes","encodeStoredNodes","aiLayoutWithStoreLinks","AI_STORE_LINKS_SOURCE_FIELD","AI_STORE_LINKS_VERSION_NAME","writeAiStoreLinksLayoutDraft","firestore","input","layout","layoutRef","collection","doc","hostId","layoutId","get","currentVersionId","exists","status","name","String","draftRef","versionId","already","current","nodes","result","links","screenPaths","recognized","header","footer","packed","copy","data","create","Buffer","from","displayName","createdAt","now","updatedAt","createdBy","uid","aiJobId"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,iBAAiB,EAAEC,iBAAiB,QAAQ,sCAAqC;AAE1F,SAASC,sBAAsB,QAAiC,oCAAgC;AAEhG;;;;;;;;;CASC,GAED,qHAAqH,GACrH,OAAO,MAAMC,8BAA8B,8BAA6B;AAExE,yEAAyE,GACzE,OAAO,MAAMC,8BAA8B,2BAA0B;AAYrE;;;;;CAKC,GACD,OAAO,eAAeC,6BACpBC,SAAoB,EACpBC,KASC;QAQmBC;IANpB,MAAMC,YAAYH,UAAUI,UAAU,CAAC,SAASC,GAAG,CAACJ,MAAMK,MAAM,EAAEF,UAAU,CAAC,WAAWC,GAAG,CAACJ,MAAMM,QAAQ;IAC1G,MAAML,SAAS,MAAMC,UAAUK,GAAG;IAClC,MAAMC,mBAAmBP,OAAOM,GAAG,CAAC;IACpC,IAAI,CAACN,OAAOQ,MAAM,IAAIR,OAAOM,GAAG,CAAC,gBAAgB,QAAQ,OAAOC,qBAAqB,YAAY,CAACA,kBAAkB;QAClH,OAAO;YAAEE,QAAQ;QAAO;IAC1B;IACA,MAAMC,OAAOC,QAAOX,cAAAA,OAAOM,GAAG,CAAC,0BAAXN,cAA6B,OAAO;IACxD,MAAMY,WAAWX,UAAUC,UAAU,CAAC,YAAYC,GAAG,CAACJ,MAAMc,SAAS;IACrE,MAAMC,UAAU,MAAMF,SAASN,GAAG;IAClC,IAAIQ,QAAQN,MAAM,EAAE,OAAO;QAAEC,QAAQ;QAAWJ,UAAUN,MAAMM,QAAQ;QAAEQ,WAAWd,MAAMc,SAAS;QAAEH;IAAK;IAC3G,MAAMK,UAAU,MAAMd,UAAUC,UAAU,CAAC,YAAYC,GAAG,CAACI,kBAAkBD,GAAG;IAChF,MAAMU,QAAQD,QAAQP,MAAM,GAAGhB,kBAA2CuB,QAAQT,GAAG,CAAC,YAAY;IAClG,IAAI,CAACU,OAAO,OAAO;QAAEP,QAAQ;IAAO;IACpC,MAAMQ,SAASvB,uBAAuBsB,OAAOjB,MAAMmB,KAAK,EAAEnB,MAAMoB,WAAW;IAC3E,MAAMC,aAAaH,OAAOI,MAAM,IAAIJ,OAAOK,MAAM;IACjD,MAAMC,SAASN,OAAOD,KAAK,GAAGvB,kBAAkBwB,OAAOD,KAAK,IAAa;IACzE,IAAI,CAACO,QAAQ,OAAOH,aAAa;QAAEX,QAAQ;QAAWJ,UAAUN,MAAMM,QAAQ;IAAC,IAAI;QAAEI,QAAQ;QAAgBJ,UAAUN,MAAMM,QAAQ;QAAEQ,WAAW;QAAMH;IAAK;IAC7J,MAAMc,OAAO,aAAMT,QAAQU,IAAI;IAC/B,OAAOD,IAAI,CAAC,cAAc;IAC1B,MAAMZ,SAASc,MAAM,CAAC,aACjBF;QACHR,OAAOW,OAAOC,IAAI,CAACL;QACnBM,aAAajC;QACbkC,WAAW/B,MAAMgC,GAAG;QACpBC,WAAWjC,MAAMgC,GAAG;QACpBE,WAAWlC,MAAMmC,GAAG;QACpBC,SAASpC,MAAMoC,OAAO;QACtB,CAACxC,4BAA4B,EAAEY;;IAEjC,OAAOa,aACH;QAAEX,QAAQ;QAAWJ,UAAUN,MAAMM,QAAQ;QAAEQ,WAAWd,MAAMc,SAAS;QAAEH;IAAK,IAChF;QAAED,QAAQ;QAAgBJ,UAAUN,MAAMM,QAAQ;QAAEQ,WAAWd,MAAMc,SAAS;QAAEH;IAAK;AAC3F"}
@@ -71,6 +71,17 @@ import { type AiLayoutPictureSlot, type AiLayoutPictureSource } from '../layout-
71
71
  * ranked: the object in the hit's first tags (what the photo is OF) over
72
72
  * a mention in its last, the subject's other words, the domain. Among
73
73
  * equals the job's seed picks, so two sites differ and one job repeats.
74
+ * What the beta.239 Ember & Oak start added: "hand-poured", "handmade"
75
+ * and "by hand" never make "hands" a subject, and a story of the craft
76
+ * asks for the maker at work ("candle making"); a hit carrying a topic
77
+ * the brief never named (a cross, a grave, a flag, a gun, wine, a child,
78
+ * a mannequin) never fills anything; a product, and a collection tile,
79
+ * take only a hit naming its head noun among the first tags, held to the
80
+ * shop's category; and an object whose word is not enough ("melt") is
81
+ * named and searched by its kin ("wax melt", "melt warmer"), never by the
82
+ * category alone. And from the Kiln & Clover portfolio: nudity never fills
83
+ * anything, for any business; a hit naming a rival craft (glass, for a
84
+ * potter) counts only when it names the craft's own words too.
74
85
  * 4. **Never twice.** A photo is placed once per job: not twice on a page,
75
86
  * and not on a page when another page (or another pass, in this process)
76
87
  * of the same job already placed it. The caller hands in what the job's
@@ -84,6 +95,18 @@ import { type AiLayoutPictureSlot, type AiLayoutPictureSource } from '../layout-
84
95
  * to its photographer. A copy that fails tries the slot's next accepted
85
96
  * hit.
86
97
  *
98
+ * 6. **Fall back to the craft, never off it** (the beta.241 Saltmarsh Soap
99
+ * Co start, a store "for handmade cold-process soap": its craft read as
100
+ * nothing, its hero searched "shop" and took a florist's bouquet, and four
101
+ * of five products took the starters' laptop desk and living room). A
102
+ * business that is only a place takes its craft from what it is for
103
+ * ("soap shop"); a hero names the craft itself ({@link AiStockSiteTerms.core});
104
+ * a product, after its own searches, tries its head noun anywhere among
105
+ * the tags, its gift wrapper with the craft and the craft's broad
106
+ * searches, and then — a product or a hero only — reuses an on-topic photo
107
+ * the job already shows rather than take a starter. Every slot of a
108
+ * craft's site tries the craft's broad searches last.
109
+ *
87
110
  * A slot nothing acceptable answered — the library down, refusing, or out
88
111
  * of time — is left `null` for the starter photos. A named object is never
89
112
  * given one while the library answers anything of the site's world: the
@@ -103,6 +126,12 @@ export declare const AI_LAYOUT_STOCK_MAX_BYTES: number;
103
126
  * written to, whose photos a page does not place again (AGL-3660).
104
127
  */
105
128
  export declare const AI_STOCK_PHOTO_PAGES_INPUT = "stockPhotoPages";
129
+ /**
130
+ * The unit job input naming library photos (`media:` references) another
131
+ * part of the job already shows outside its pages — a dataset's records
132
+ * (AGL-3616) — which a page does not place again.
133
+ */
134
+ export declare const AI_STOCK_PHOTO_AVOID_INPUT = "stockPhotoAvoid";
106
135
  /**
107
136
  * A word's singular, for matching only: "bowls" and "bowl", "vases" and
108
137
  * "vase", "berries" and "berry", "dishes" and "dish" meet. Both sides of a
@@ -140,6 +169,22 @@ export interface AiStockSiteTerms {
140
169
  domain: string[];
141
170
  /** The searches for the site's world itself, broadest last. */
142
171
  broad: string[];
172
+ /** The searches for its maker at work, for a story of the craft (AGL-3660); empty for a craft not listed. */
173
+ making: string[];
174
+ /** Every stem of the business type or brief: what lets a sensitive topic in ({@link SENSITIVE_TOPICS}). */
175
+ named: string[];
176
+ /** Rival crafts' words, each with the words that must then be named too ({@link CRAFT_KIN}). */
177
+ rivals: Array<{
178
+ words: string[];
179
+ core: string[];
180
+ }>;
181
+ /**
182
+ * The stems a photo of the craft ITSELF names, one of which a hero and a
183
+ * product's fallbacks must name (AGL-3660): the craft and, for a listed
184
+ * craft, its own names ("soap", "soapmaker", "skincare"). Empty where the
185
+ * site names no craft.
186
+ */
187
+ core: string[];
143
188
  }
144
189
  /** The site's terms, read from its business type. */
145
190
  export declare function aiStockSiteTerms(businessType: string): AiStockSiteTerms;
@@ -170,19 +215,72 @@ export interface AiStockSearch extends StockPhotoSearchRequest {
170
215
  subject?: string;
171
216
  /** A word every hit must name: a product's photo names the shop's category (AGL-3676). */
172
217
  requires?: string;
218
+ /**
219
+ * Words of which a hit must name ONE, `requires` among them (AGL-3660): a
220
+ * shop's category or the slot's own word of its world, so a soap shop's
221
+ * body lotion is held to "lotion", never to "soap".
222
+ */
223
+ category?: readonly string[];
224
+ /** The thing every hit must name, where it is not the subject's own: a product's head noun (AGL-3660). */
225
+ object?: string;
226
+ /** Phrases that name the object where its own word is not enough: "wax melt" ({@link OBJECT_KIN}). */
227
+ kin?: readonly string[];
228
+ /** Whether the object must be among the hit's first tags, what the photo is OF (AGL-3660). */
229
+ lead?: boolean;
230
+ /** Whether the hit's first tags must name the site's world, where the object need not lead (AGL-3660). */
231
+ leadWorld?: boolean;
232
+ }
233
+ /** A slot's part of a page, where it asks for more than its role: a collection tile or a story of the craft (AGL-3660). */
234
+ export type AiStockSlotPart = 'collection' | 'story';
235
+ /**
236
+ * A slot's part (AGL-3660): a tile of a collections band is product-like,
237
+ * held to the shop's category and its own object; a picture of a story of
238
+ * the craft, or of a thing made by hand, shows the maker at work.
239
+ */
240
+ export declare function aiStockSlotPart(slot: Pick<AiLayoutPictureSlot, 'role' | 'alt'>, sectionName: string): AiStockSlotPart | undefined;
241
+ /** What a product falls back to once its own searches find nothing ({@link aiStockProductSearches}). */
242
+ export interface AiStockProductFallback {
243
+ /** The site's broad searches ({@link AiStockSiteTerms.broad}): "handmade soap". */
244
+ broad?: readonly string[];
245
+ /** The words a photo of the craft itself names ({@link AiStockSiteTerms.core}). */
246
+ core?: readonly string[];
173
247
  }
174
248
  /**
175
249
  * A product's searches (AGL-3676), most specific first: each of its subjects
176
250
  * — its name's noun phrase, then its photo's — with the shop's category
177
- * where the phrase does not already say it ("candle gift box"), then the
178
- * category alone, a plain photo of what the shop sells. Every one must find a
179
- * hit naming the category, so no product is ever filled with a lifestyle shot
180
- * of something else: the beta.237 Willow Wick start put a laptop and roses
181
- * under "Candle Wick Trimmer" and an antique tea set under "Candle Gift Set".
251
+ * where the phrase does not already name the shop's world ("candle gift
252
+ * box"), then the category alone, a plain photo of what the shop sells. Every
253
+ * one must find a hit naming the category — or the product's own word of the
254
+ * shop's world, "lotion" in a soap shop — so no product is ever filled with a
255
+ * lifestyle shot of something else: the beta.237 Willow Wick start put a
256
+ * laptop and roses under "Candle Wick Trimmer" and an antique tea set under
257
+ * "Candle Gift Set".
258
+ *
259
+ * And each must find a hit naming the product's own head noun among its
260
+ * first tags (AGL-3660): the beta.239 Ember & Oak start put a bed with a book,
261
+ * a mug and a camera (its candle a last tag) under "Hand-Poured Soy Candle".
262
+ * An object whose own word is not enough ({@link OBJECT_KIN}) is searched by
263
+ * its kin instead of the category alone, and named by them: a single taper
264
+ * candle filled "Wax Melt Gift Set".
265
+ *
266
+ * Then, before a product is ever given a photo of something else, it falls
267
+ * back to its own craft (AGL-3660). The Saltmarsh Soap Co start (2026-10-10)
268
+ * found no hit naming "bar" or "box" among the first tags, and four of its
269
+ * five products took the starter photos — a laptop and roses, a living room
270
+ * of plants. So, in order:
271
+ *
272
+ * 1. The same searches again, the head noun named anywhere among the tags
273
+ * (no extra search: the page remembers its answers), and a gift's own
274
+ * wrapper with the craft ("soap gift box").
275
+ * 2. The site's broad searches ("handmade soap", "natural skincare"), any hit
276
+ * naming the craft itself ({@link AiStockSiteTerms.core}).
277
+ *
278
+ * The source then reuses an on-topic photo another slot of the job already
279
+ * shows before it gives up ({@link aiLayoutStockPhotoSource}).
182
280
  */
183
- export declare function aiStockProductSearches(slot: Pick<AiLayoutPictureSlot, 'aspect' | 'product'>, craft: string): AiStockSearch[];
281
+ export declare function aiStockProductSearches(slot: Pick<AiLayoutPictureSlot, 'aspect' | 'product'>, craft: string, domain?: readonly string[], fallback?: AiStockProductFallback): AiStockSearch[];
184
282
  /** The searches a slot tries, in order, most specific first, each distinct. */
185
- export declare function aiStockSearchesFor(slot: Pick<AiLayoutPictureSlot, 'role' | 'alt' | 'aspect'>, terms: Pick<AiStockSiteTerms, 'business' | 'craft' | 'domain' | 'broad'>, subject: string): AiStockSearch[];
283
+ export declare function aiStockSearchesFor(slot: Pick<AiLayoutPictureSlot, 'role' | 'alt' | 'aspect'>, terms: Pick<AiStockSiteTerms, 'business' | 'craft' | 'domain' | 'broad'> & Partial<Pick<AiStockSiteTerms, 'making' | 'core'>>, subject: string, part?: AiStockSlotPart): AiStockSearch[];
186
284
  /** What a hit is judged by. */
187
285
  export interface AiStockJudgement {
188
286
  /** The words the picture is of; empty for a picture of the site's world. */
@@ -196,10 +294,33 @@ export interface AiStockJudgement {
196
294
  strict?: boolean;
197
295
  /** A word every hit must name: a product's photo names the shop's category (AGL-3676). */
198
296
  requires?: string;
297
+ /** Words of which a hit must name one, `requires` among them ({@link AiStockSearch.category}). */
298
+ category?: readonly string[];
299
+ /** The thing a hit must name, where it is not the subject's own (AGL-3660). */
300
+ object?: string;
301
+ /** Phrases naming the object where its word is not enough; one of them is then required, and stands for the category. */
302
+ kin?: readonly string[];
303
+ /** Whether the object must be among the hit's first tags (AGL-3660). */
304
+ lead?: boolean;
305
+ /** Whether the hit's first tags must name the site's world ({@link AiStockSearch.leadWorld}). */
306
+ leadWorld?: boolean;
307
+ /** The stems of the site's business type or brief, which let a sensitive topic in ({@link AiStockSiteTerms.named}). */
308
+ named?: readonly string[];
309
+ /** Rival crafts: a hit naming one is rejected unless it names the site's core words ({@link AiStockSiteTerms.rivals}). */
310
+ rivals?: ReadonlyArray<{
311
+ words: readonly string[];
312
+ core: readonly string[];
313
+ }>;
199
314
  }
315
+ /**
316
+ * Whether a hit carries a topic its site never named ({@link SENSITIVE_TOPICS}):
317
+ * a cross, a grave, a flag, a gun, a glass of wine, a child, a mannequin.
318
+ */
319
+ export declare function aiStockSensitive(photo: Pick<StockPhoto, 'tags' | 'alt' | 'pageUrl'>, named?: readonly string[]): boolean;
200
320
  /**
201
321
  * How well a hit shows what a slot needs (AGL-3660), or 0 when it must not
202
- * fill it: a mockup or a blank never; a picture of a thing only when the hit
322
+ * fill it: a mockup or a blank never; a topic the site never named never
323
+ * ({@link aiStockSensitive}); a picture of a thing only when the hit
203
324
  * names its object AND either the site's world or the subject's own
204
325
  * two-word phrase ("bud vase", "wick trimmer"); anything else only when it
205
326
  * names the site's world (when the site names one). Among the accepted, the