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

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 (87) 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-datasets.d.ts +76 -2
  38. package/src/lib/jobs/ai-job-site-datasets.js +206 -13
  39. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
  40. package/src/lib/jobs/ai-job-site-step.d.ts +18 -2
  41. package/src/lib/jobs/ai-job-site-step.js +109 -22
  42. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  43. package/src/lib/jobs/ai-job-site-store-pages.d.ts +75 -0
  44. package/src/lib/jobs/ai-job-site-store-pages.js +195 -0
  45. package/src/lib/jobs/ai-job-site-store-pages.js.map +1 -0
  46. package/src/lib/jobs/ai-job-store-links-layout.d.ts +71 -0
  47. package/src/lib/jobs/ai-job-store-links-layout.js +95 -0
  48. package/src/lib/jobs/ai-job-store-links-layout.js.map +1 -0
  49. package/src/lib/jobs/ai-layout-stock-photos.d.ts +69 -3
  50. package/src/lib/jobs/ai-layout-stock-photos.js +603 -42
  51. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  52. package/src/lib/jobs/ai-site-publish.d.ts +18 -0
  53. package/src/lib/jobs/ai-site-publish.js +76 -15
  54. package/src/lib/jobs/ai-site-publish.js.map +1 -1
  55. package/src/lib/jobs/fixtures/ai-stock-photo-ember-oak-hits.d.ts +43 -0
  56. package/src/lib/jobs/fixtures/ai-stock-photo-ember-oak-hits.js +61 -0
  57. package/src/lib/jobs/fixtures/ai-stock-photo-ember-oak-hits.js.map +1 -0
  58. package/src/lib/layout-language/ai-layout-compiler.js +25 -2
  59. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  60. package/src/lib/layout-language/ai-layout-frame.d.ts +10 -0
  61. package/src/lib/layout-language/ai-layout-frame.js +58 -4
  62. package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
  63. package/src/lib/layout-language/ai-layout-listings.d.ts +6 -0
  64. package/src/lib/layout-language/ai-layout-listings.js +3 -0
  65. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  66. package/src/lib/model/ai-build-job.d.ts +2 -0
  67. package/src/lib/model/ai-build-job.js +23 -3
  68. package/src/lib/model/ai-build-job.js.map +1 -1
  69. package/src/lib/model/ai-build-plan.d.ts +7 -0
  70. package/src/lib/model/ai-build-plan.js.map +1 -1
  71. package/src/lib/model/ai-build-progress.js +3 -1
  72. package/src/lib/model/ai-build-progress.js.map +1 -1
  73. package/src/lib/model/ai-build-store-pages.d.ts +133 -0
  74. package/src/lib/model/ai-build-store-pages.js +209 -0
  75. package/src/lib/model/ai-build-store-pages.js.map +1 -0
  76. package/src/lib/model/ai-layout-store-links.d.ts +64 -0
  77. package/src/lib/model/ai-layout-store-links.js +165 -0
  78. package/src/lib/model/ai-layout-store-links.js.map +1 -0
  79. package/src/lib/model/ai-site-inventory.d.ts +5 -0
  80. package/src/lib/model/ai-site-inventory.js.map +1 -1
  81. package/src/lib/model/ai-site-store-pages.d.ts +192 -0
  82. package/src/lib/model/ai-site-store-pages.js +383 -0
  83. package/src/lib/model/ai-site-store-pages.js.map +1 -0
  84. package/src/lib/runtime/site-inventory.js +8 -3
  85. package/src/lib/runtime/site-inventory.js.map +1 -1
  86. package/src/lib/testing/stand-in-dataset-index.js +8 -4
  87. 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
@@ -103,6 +114,12 @@ export declare const AI_LAYOUT_STOCK_MAX_BYTES: number;
103
114
  * written to, whose photos a page does not place again (AGL-3660).
104
115
  */
105
116
  export declare const AI_STOCK_PHOTO_PAGES_INPUT = "stockPhotoPages";
117
+ /**
118
+ * The unit job input naming library photos (`media:` references) another
119
+ * part of the job already shows outside its pages — a dataset's records
120
+ * (AGL-3616) — which a page does not place again.
121
+ */
122
+ export declare const AI_STOCK_PHOTO_AVOID_INPUT = "stockPhotoAvoid";
106
123
  /**
107
124
  * A word's singular, for matching only: "bowls" and "bowl", "vases" and
108
125
  * "vase", "berries" and "berry", "dishes" and "dish" meet. Both sides of a
@@ -140,6 +157,15 @@ export interface AiStockSiteTerms {
140
157
  domain: string[];
141
158
  /** The searches for the site's world itself, broadest last. */
142
159
  broad: string[];
160
+ /** The searches for its maker at work, for a story of the craft (AGL-3660); empty for a craft not listed. */
161
+ making: string[];
162
+ /** Every stem of the business type or brief: what lets a sensitive topic in ({@link SENSITIVE_TOPICS}). */
163
+ named: string[];
164
+ /** Rival crafts' words, each with the words that must then be named too ({@link CRAFT_KIN}). */
165
+ rivals: Array<{
166
+ words: string[];
167
+ core: string[];
168
+ }>;
143
169
  }
144
170
  /** The site's terms, read from its business type. */
145
171
  export declare function aiStockSiteTerms(businessType: string): AiStockSiteTerms;
@@ -170,7 +196,21 @@ export interface AiStockSearch extends StockPhotoSearchRequest {
170
196
  subject?: string;
171
197
  /** A word every hit must name: a product's photo names the shop's category (AGL-3676). */
172
198
  requires?: string;
199
+ /** The thing every hit must name, where it is not the subject's own: a product's head noun (AGL-3660). */
200
+ object?: string;
201
+ /** Phrases that name the object where its own word is not enough: "wax melt" ({@link OBJECT_KIN}). */
202
+ kin?: readonly string[];
203
+ /** Whether the object must be among the hit's first tags, what the photo is OF (AGL-3660). */
204
+ lead?: boolean;
173
205
  }
206
+ /** 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). */
207
+ export type AiStockSlotPart = 'collection' | 'story';
208
+ /**
209
+ * A slot's part (AGL-3660): a tile of a collections band is product-like,
210
+ * held to the shop's category and its own object; a picture of a story of
211
+ * the craft, or of a thing made by hand, shows the maker at work.
212
+ */
213
+ export declare function aiStockSlotPart(slot: Pick<AiLayoutPictureSlot, 'role' | 'alt'>, sectionName: string): AiStockSlotPart | undefined;
174
214
  /**
175
215
  * A product's searches (AGL-3676), most specific first: each of its subjects
176
216
  * — its name's noun phrase, then its photo's — with the shop's category
@@ -179,10 +219,17 @@ export interface AiStockSearch extends StockPhotoSearchRequest {
179
219
  * hit naming the category, so no product is ever filled with a lifestyle shot
180
220
  * of something else: the beta.237 Willow Wick start put a laptop and roses
181
221
  * under "Candle Wick Trimmer" and an antique tea set under "Candle Gift Set".
222
+ *
223
+ * And every one must find a hit naming the product's own head noun among its
224
+ * first tags (AGL-3660): the beta.239 Ember & Oak start put a bed with a book,
225
+ * a mug and a camera (its candle a last tag) under "Hand-Poured Soy Candle".
226
+ * An object whose own word is not enough ({@link OBJECT_KIN}) is searched by
227
+ * its kin instead of the category alone, and named by them: a single taper
228
+ * candle filled "Wax Melt Gift Set".
182
229
  */
183
- export declare function aiStockProductSearches(slot: Pick<AiLayoutPictureSlot, 'aspect' | 'product'>, craft: string): AiStockSearch[];
230
+ export declare function aiStockProductSearches(slot: Pick<AiLayoutPictureSlot, 'aspect' | 'product'>, craft: string, domain?: readonly string[]): AiStockSearch[];
184
231
  /** 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[];
232
+ export declare function aiStockSearchesFor(slot: Pick<AiLayoutPictureSlot, 'role' | 'alt' | 'aspect'>, terms: Pick<AiStockSiteTerms, 'business' | 'craft' | 'domain' | 'broad'> & Partial<Pick<AiStockSiteTerms, 'making'>>, subject: string, part?: AiStockSlotPart): AiStockSearch[];
186
233
  /** What a hit is judged by. */
187
234
  export interface AiStockJudgement {
188
235
  /** The words the picture is of; empty for a picture of the site's world. */
@@ -196,10 +243,29 @@ export interface AiStockJudgement {
196
243
  strict?: boolean;
197
244
  /** A word every hit must name: a product's photo names the shop's category (AGL-3676). */
198
245
  requires?: string;
246
+ /** The thing a hit must name, where it is not the subject's own (AGL-3660). */
247
+ object?: string;
248
+ /** Phrases naming the object where its word is not enough; one of them is then required, and stands for the category. */
249
+ kin?: readonly string[];
250
+ /** Whether the object must be among the hit's first tags (AGL-3660). */
251
+ lead?: boolean;
252
+ /** The stems of the site's business type or brief, which let a sensitive topic in ({@link AiStockSiteTerms.named}). */
253
+ named?: readonly string[];
254
+ /** Rival crafts: a hit naming one is rejected unless it names the site's core words ({@link AiStockSiteTerms.rivals}). */
255
+ rivals?: ReadonlyArray<{
256
+ words: readonly string[];
257
+ core: readonly string[];
258
+ }>;
199
259
  }
260
+ /**
261
+ * Whether a hit carries a topic its site never named ({@link SENSITIVE_TOPICS}):
262
+ * a cross, a grave, a flag, a gun, a glass of wine, a child, a mannequin.
263
+ */
264
+ export declare function aiStockSensitive(photo: Pick<StockPhoto, 'tags' | 'alt' | 'pageUrl'>, named?: readonly string[]): boolean;
200
265
  /**
201
266
  * 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
267
+ * fill it: a mockup or a blank never; a topic the site never named never
268
+ * ({@link aiStockSensitive}); a picture of a thing only when the hit
203
269
  * names its object AND either the site's world or the subject's own
204
270
  * two-word phrase ("bud vase", "wick trimmer"); anything else only when it
205
271
  * names the site's world (when the site names one). Among the accepted, the