@aglyn/plugins-ai 1.0.0-beta.285 → 1.0.0-beta.287
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.
- package/package.json +17 -17
- package/src/lib/assets/site-styles/nonprofit.webp +0 -0
- package/src/lib/assets/site-styles/professional.webp +0 -0
- package/src/lib/assets/site-styles/store.webp +0 -0
- package/src/lib/assets/site-styles/trades.webp +0 -0
- package/src/lib/components/ai-theme-proposal-card.component.js +12 -3
- package/src/lib/components/ai-theme-proposal-card.component.js.map +1 -1
- package/src/lib/declarations.js +5 -2
- package/src/lib/declarations.js.map +1 -1
- package/src/lib/declarations.server.js +12 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/jobs/ai-build-capabilities.d.ts +3 -1
- package/src/lib/jobs/ai-build-capabilities.js +27 -2
- package/src/lib/jobs/ai-build-capabilities.js.map +1 -1
- package/src/lib/jobs/ai-image-reads.d.ts +59 -0
- package/src/lib/jobs/ai-image-reads.js +138 -0
- package/src/lib/jobs/ai-image-reads.js.map +1 -0
- package/src/lib/jobs/ai-job-build-step.d.ts +29 -0
- package/src/lib/jobs/ai-job-build-step.js +83 -12
- package/src/lib/jobs/ai-job-build-step.js.map +1 -1
- package/src/lib/jobs/ai-job-duplicate-step.d.ts +2 -1
- package/src/lib/jobs/ai-job-duplicate-step.js +6 -6
- package/src/lib/jobs/ai-job-duplicate-step.js.map +1 -1
- package/src/lib/jobs/ai-job-import-step.d.ts +4 -2
- package/src/lib/jobs/ai-job-import-step.js +15 -11
- package/src/lib/jobs/ai-job-import-step.js.map +1 -1
- package/src/lib/jobs/ai-job-insight-step.d.ts +10 -0
- package/src/lib/jobs/ai-job-insight-step.js +23 -5
- package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-language.js +2 -1
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-media-step.d.ts +2 -1
- package/src/lib/jobs/ai-job-media-step.js +6 -8
- package/src/lib/jobs/ai-job-media-step.js.map +1 -1
- package/src/lib/jobs/ai-job-navigation-step.d.ts +2 -1
- package/src/lib/jobs/ai-job-navigation-step.js +7 -9
- package/src/lib/jobs/ai-job-navigation-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.js +7 -1
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-sections.js +16 -3
- package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.js +9 -1
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +30 -8
- package/src/lib/jobs/ai-job-plan-step.js +78 -13
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-privacy-step.d.ts +4 -2
- package/src/lib/jobs/ai-job-privacy-step.js +14 -15
- package/src/lib/jobs/ai-job-privacy-step.js.map +1 -1
- package/src/lib/jobs/ai-job-products-step.js +5 -3
- package/src/lib/jobs/ai-job-products-step.js.map +1 -1
- package/src/lib/jobs/ai-job-settings-step.d.ts +2 -1
- package/src/lib/jobs/ai-job-settings-step.js +7 -8
- package/src/lib/jobs/ai-job-settings-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-content.d.ts +34 -1
- package/src/lib/jobs/ai-job-site-content.js +67 -8
- package/src/lib/jobs/ai-job-site-content.js.map +1 -1
- package/src/lib/jobs/ai-job-site-datasets.d.ts +53 -3
- package/src/lib/jobs/ai-job-site-datasets.js +298 -30
- package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.js +6 -3
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-store-pages.js +3 -3
- package/src/lib/jobs/ai-job-site-store-pages.js.map +1 -1
- package/src/lib/jobs/ai-job-tracking-step.d.ts +2 -1
- package/src/lib/jobs/ai-job-tracking-step.js +7 -8
- package/src/lib/jobs/ai-job-tracking-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs-beat.d.ts +11 -7
- package/src/lib/jobs/ai-jobs-beat.js +18 -8
- package/src/lib/jobs/ai-jobs-beat.js.map +1 -1
- package/src/lib/jobs/ai-jobs-notify.d.ts +66 -1
- package/src/lib/jobs/ai-jobs-notify.js +136 -3
- package/src/lib/jobs/ai-jobs-notify.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +59 -4
- package/src/lib/jobs/ai-layout-stock-photos.js +897 -81
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/ai-product-image.d.ts +14 -0
- package/src/lib/jobs/ai-product-image.js +73 -21
- package/src/lib/jobs/ai-product-image.js.map +1 -1
- package/src/lib/jobs/ai-theme-brand-inputs.d.ts +9 -1
- package/src/lib/jobs/ai-theme-brand-inputs.js +28 -2
- package/src/lib/jobs/ai-theme-brand-inputs.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-site-agl4065-datasets.d.ts +175 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-datasets.js +601 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-datasets.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-pages.d.ts +46 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-pages.js +2862 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-pages.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-plans.d.ts +74 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-plans.js +888 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-plans.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-judge.d.ts +29 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-judge.js +81 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-judge.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-recordings.d.ts +87 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-recordings.js +225 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-recordings.js.map +1 -0
- package/src/lib/layout-language/ai-layout-compiler.d.ts +11 -0
- package/src/lib/layout-language/ai-layout-compiler.js +194 -18
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-copy.d.ts +37 -0
- package/src/lib/layout-language/ai-layout-copy.js +383 -3
- package/src/lib/layout-language/ai-layout-copy.js.map +1 -1
- package/src/lib/layout-language/ai-layout-design.d.ts +19 -0
- package/src/lib/layout-language/ai-layout-design.js +31 -11
- package/src/lib/layout-language/ai-layout-design.js.map +1 -1
- package/src/lib/layout-language/ai-layout-listings.d.ts +7 -1
- package/src/lib/layout-language/ai-layout-listings.js +9 -1
- package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
- package/src/lib/layout-language/ai-layout-pictures.d.ts +30 -0
- package/src/lib/layout-language/ai-layout-pictures.js +131 -3
- package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
- package/src/lib/layout-language/ai-layout-storefront.d.ts +18 -4
- package/src/lib/layout-language/ai-layout-storefront.js +100 -20
- package/src/lib/layout-language/ai-layout-storefront.js.map +1 -1
- package/src/lib/model/ai-build-job.d.ts +7 -0
- package/src/lib/model/ai-build-job.js +36 -7
- package/src/lib/model/ai-build-job.js.map +1 -1
- package/src/lib/model/ai-build-plan.d.ts +28 -4
- package/src/lib/model/ai-build-plan.js +49 -9
- package/src/lib/model/ai-build-plan.js.map +1 -1
- package/src/lib/model/ai-build-progress.js +3 -1
- package/src/lib/model/ai-build-progress.js.map +1 -1
- package/src/lib/model/ai-job-notice.d.ts +8 -0
- package/src/lib/model/ai-job-notice.js +33 -0
- package/src/lib/model/ai-job-notice.js.map +1 -1
- package/src/lib/model/ai-job-plan-lines.d.ts +2 -1
- package/src/lib/model/ai-job-plan-lines.js +6 -1
- package/src/lib/model/ai-job-plan-lines.js.map +1 -1
- package/src/lib/model/ai-jobs.types.d.ts +8 -0
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-permissions.d.ts +13 -0
- package/src/lib/model/ai-permissions.js +29 -1
- package/src/lib/model/ai-permissions.js.map +1 -1
- package/src/lib/model/ai-products.d.ts +18 -0
- package/src/lib/model/ai-products.js +32 -0
- package/src/lib/model/ai-products.js.map +1 -1
- package/src/lib/model/ai-site-inspiration.d.ts +3 -2
- package/src/lib/model/ai-site-inspiration.js +3 -2
- package/src/lib/model/ai-site-inspiration.js.map +1 -1
- package/src/lib/plugin.js +4 -13
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/providers/anthropic.js +9 -2
- package/src/lib/providers/anthropic.js.map +1 -1
- package/src/lib/providers/catalog.js +14 -5
- package/src/lib/providers/catalog.js.map +1 -1
- package/src/lib/providers/contract.d.ts +24 -1
- package/src/lib/providers/contract.js.map +1 -1
- package/src/lib/providers/routing.js +5 -2
- package/src/lib/providers/routing.js.map +1 -1
- package/src/lib/runtime/ai-dataset-generation.d.ts +99 -1
- package/src/lib/runtime/ai-dataset-generation.js +251 -3
- package/src/lib/runtime/ai-dataset-generation.js.map +1 -1
- package/src/lib/runtime/ai-dev-replay.d.ts +1 -0
- package/src/lib/runtime/ai-dev-replay.js +4 -2
- package/src/lib/runtime/ai-dev-replay.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +47 -3
- package/src/lib/runtime/ai-doctrine-validators.js +343 -27
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +8 -2
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-icon-library.d.ts +27 -1
- package/src/lib/runtime/ai-icon-library.js +30 -3
- package/src/lib/runtime/ai-icon-library.js.map +1 -1
- package/src/lib/runtime/ai-products-generation.d.ts +11 -1
- package/src/lib/runtime/ai-products-generation.js +51 -4
- package/src/lib/runtime/ai-products-generation.js.map +1 -1
- package/src/lib/runtime/assist-answer-cache.d.ts +2 -0
- package/src/lib/runtime/assist-answer-cache.js +5 -2
- package/src/lib/runtime/assist-answer-cache.js.map +1 -1
- package/src/lib/server/ai-allotments.js +7 -2
- package/src/lib/server/ai-allotments.js.map +1 -1
- package/src/lib/server/ai-experiment-versions.js +5 -5
- package/src/lib/server/ai-experiment-versions.js.map +1 -1
- package/src/lib/server/ai-host-permissions-roster.d.ts +25 -0
- package/src/lib/server/ai-host-permissions-roster.js +33 -0
- package/src/lib/server/ai-host-permissions-roster.js.map +1 -0
- package/src/lib/server/ai-host-permissions.js +8 -6
- package/src/lib/server/ai-host-permissions.js.map +1 -1
- package/src/lib/server/ai-image-reads-route.d.ts +17 -0
- package/src/lib/server/ai-image-reads-route.js +106 -0
- package/src/lib/server/ai-image-reads-route.js.map +1 -0
- package/src/lib/server/ai-seo-apply.js +5 -5
- package/src/lib/server/ai-seo-apply.js.map +1 -1
- package/src/lib/server/ai-site-import-route.js +7 -4
- package/src/lib/server/ai-site-import-route.js.map +1 -1
- package/src/lib/server/assist-build.js +1 -0
- package/src/lib/server/assist-build.js.map +1 -1
- package/src/lib/server/assist-chat.d.ts +11 -7
- package/src/lib/server/assist-chat.js +185 -48
- package/src/lib/server/assist-chat.js.map +1 -1
- package/src/lib/server/assist-docs-query.d.ts +51 -0
- package/src/lib/server/assist-docs-query.js +57 -0
- package/src/lib/server/assist-docs-query.js.map +1 -0
- package/src/lib/server/assist-jobs-context.d.ts +102 -0
- package/src/lib/server/assist-jobs-context.js +227 -0
- package/src/lib/server/assist-jobs-context.js.map +1 -0
- package/src/lib/server/assist-view-context.d.ts +8 -4
- package/src/lib/server/assist-view-context.js +12 -7
- package/src/lib/server/assist-view-context.js.map +1 -1
- package/src/lib/server.js +12 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/tools/ai-dataset-tool.d.ts +11 -1
- package/src/lib/tools/ai-dataset-tool.js +24 -7
- package/src/lib/tools/ai-dataset-tool.js.map +1 -1
- package/src/lib/tools/ai-duplicate-resource-tool.js +5 -6
- package/src/lib/tools/ai-duplicate-resource-tool.js.map +1 -1
- package/src/lib/components/ai-collaborator-permissions-column.component.d.ts +0 -32
- package/src/lib/components/ai-collaborator-permissions-column.component.js +0 -132
- package/src/lib/components/ai-collaborator-permissions-column.component.js.map +0 -1
|
@@ -15,7 +15,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/ import { checkEntitlement } from "@aglyn/aglyn/app-utils/plan-entitlements";
|
|
18
|
-
import {
|
|
18
|
+
import { holdsSiteKey, siteKeyRefusal } from "@aglyn/aglyn/app-utils/host-permissions";
|
|
19
19
|
import { ACCOUNTS_PLUGIN_ID } from "@aglyn/aglyn/plugin-manager/enabled-plugins";
|
|
20
20
|
import { HostScreenVisibility } from "@aglyn/aglyn/foundation/definitions/platform.types";
|
|
21
21
|
import { FieldValue } from "firebase-admin/firestore";
|
|
@@ -71,7 +71,7 @@ function inputOf(job) {
|
|
|
71
71
|
* include a store (Starter and up), the commerce plugin must run here, and the
|
|
72
72
|
* member must be able to edit the site's pages.
|
|
73
73
|
*/ export async function aiSiteStorePagesRefusal(context, deps = {}) {
|
|
74
|
-
var _context_org, _deps_admission, _context_org1
|
|
74
|
+
var _context_org, _deps_admission, _context_org1;
|
|
75
75
|
const { job, firestore } = context;
|
|
76
76
|
if (!job.hostId) return 'Open the site first.';
|
|
77
77
|
if (!checkEntitlement((_context_org = context.org) != null ? _context_org : null, 'commerce')) return 'Your plan does not include a store.';
|
|
@@ -93,7 +93,7 @@ function inputOf(job) {
|
|
|
93
93
|
});
|
|
94
94
|
if (refused) return refused.error;
|
|
95
95
|
const host = await firestore.collection('hosts').doc(job.hostId).get();
|
|
96
|
-
return
|
|
96
|
+
return holdsSiteKey(host, job.createdBy, 'site.edit') ? null : siteKeyRefusal('site.edit');
|
|
97
97
|
}
|
|
98
98
|
/** The store pages' pass: every page written, user accounts on, one output a page. */ export function createAiSiteStorePagesRunner(deps = {}) {
|
|
99
99
|
var _deps_write, _deps_writeSeo;
|
|
@@ -1 +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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"}
|
|
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 { holdsSiteKey, siteKeyRefusal } from '@aglyn/aglyn/app-utils/host-permissions'\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 holdsSiteKey(host, job.createdBy, 'site.edit') ? null : siteKeyRefusal('site.edit')\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","holdsSiteKey","siteKeyRefusal","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","firestore","hostId","org","refused","admission","orgId","kind","drafts","resource","label","now","error","host","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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"}
|
|
@@ -63,7 +63,8 @@ export declare const AI_TRACKING_KIND: "tracking";
|
|
|
63
63
|
/** ASSUMED: the least time the step needs — the site, the org and one transaction. */
|
|
64
64
|
export declare const AI_JOB_TRACKING_STEP_MINIMUM_MS = 5000;
|
|
65
65
|
export declare const AI_TRACKING_NO_SITE_COPY = "Open the site whose tracking should change.";
|
|
66
|
-
|
|
66
|
+
/** Names "Edit site content" (`site.edit`, AGL-4047), never a role. */
|
|
67
|
+
export declare const AI_TRACKING_ROLE_COPY: string;
|
|
67
68
|
export declare const AI_TRACKING_LOCKED_COPY = "This site is not accepting changes right now.";
|
|
68
69
|
export declare const AI_TRACKING_NOTHING_COPY = "The request named no tracking id this could set or remove.";
|
|
69
70
|
/** Where a person finds what the op changed: the card that edits it. */
|
|
@@ -15,7 +15,8 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/ import { createResourceUid } from "@aglyn/aglyn/app-utils/create-resource-uid";
|
|
18
|
-
import {
|
|
18
|
+
import { holdsSiteKey } from "@aglyn/aglyn/app-utils/host-permissions";
|
|
19
|
+
import { orgPermissionLabel } from "@aglyn/aglyn/app-utils/org-permissions";
|
|
19
20
|
import { SCREEN_ROOT_PATH } from "@aglyn/aglyn/app-utils/screen-route";
|
|
20
21
|
import { GA_MEASUREMENT_ID_PATTERN, GOOGLE_ADS_ID_PATTERN, GTM_CONTAINER_ID_PATTERN, LINKEDIN_PARTNER_ID_PATTERN, META_PIXEL_ID_PATTERN, PINTEREST_TAG_ID_PATTERN, TIKTOK_PIXEL_ID_PATTERN } from "@aglyn/aglyn/app-utils/visitor-consent";
|
|
21
22
|
import { dropPluginSiteCache } from "@aglyn/aglyn/plugin-manager/plugin-site-cache";
|
|
@@ -66,7 +67,7 @@ import { registerAiJobStep } from "./ai-jobs.js";
|
|
|
66
67
|
*/ export const AI_TRACKING_KIND = 'tracking';
|
|
67
68
|
/** ASSUMED: the least time the step needs — the site, the org and one transaction. */ export const AI_JOB_TRACKING_STEP_MINIMUM_MS = 5000;
|
|
68
69
|
export const AI_TRACKING_NO_SITE_COPY = 'Open the site whose tracking should change.';
|
|
69
|
-
export const AI_TRACKING_ROLE_COPY =
|
|
70
|
+
/** Names "Edit site content" (`site.edit`, AGL-4047), never a role. */ export const AI_TRACKING_ROLE_COPY = `Changing the site’s tracking needs the "${orgPermissionLabel('site.edit')}" permission on this site — ask an organization admin.`;
|
|
70
71
|
export const AI_TRACKING_LOCKED_COPY = 'This site is not accepting changes right now.';
|
|
71
72
|
export const AI_TRACKING_NOTHING_COPY = 'The request named no tracking id this could set or remove.';
|
|
72
73
|
/** Where a person finds what the op changed: the card that edits it. */ export const AI_TRACKING_WHERE = 'Setup → Tracking';
|
|
@@ -316,14 +317,13 @@ const getPath = (data, path)=>path.split('.').reduce((at, key)=>at && typeof at
|
|
|
316
317
|
*/ export async function writeAiSiteTracking(firestore, input) {
|
|
317
318
|
const hostRef = firestore.collection('hosts').doc(input.hostId);
|
|
318
319
|
return firestore.runTransaction(async (tx)=>{
|
|
319
|
-
var
|
|
320
|
+
var _host_data, _data_analytics, _data_consent;
|
|
320
321
|
const host = await tx.get(hostRef);
|
|
321
322
|
if (!host.exists) return {
|
|
322
323
|
ok: false,
|
|
323
324
|
error: 'Unknown site'
|
|
324
325
|
};
|
|
325
|
-
|
|
326
|
-
if (!hostRoleCanWrite(roles[input.uid])) return {
|
|
326
|
+
if (!holdsSiteKey(host, input.uid, 'site.edit')) return {
|
|
327
327
|
ok: false,
|
|
328
328
|
error: AI_TRACKING_ROLE_COPY
|
|
329
329
|
};
|
|
@@ -386,10 +386,9 @@ const getPath = (data, path)=>path.split('.').reduce((at, key)=>at && typeof at
|
|
|
386
386
|
}
|
|
387
387
|
// ── The step ──────────────────────────────────────────────────────────────
|
|
388
388
|
/** Why this member may not change this site's tracking now, or `null`. */ export async function aiTrackingAccessRefusal(input, deps = {}) {
|
|
389
|
-
var
|
|
389
|
+
var _deps_lockdown;
|
|
390
390
|
if (!input.host.exists) return 'Unknown site';
|
|
391
|
-
|
|
392
|
-
if (!hostRoleCanWrite(roles[input.uid])) return AI_TRACKING_ROLE_COPY;
|
|
391
|
+
if (!holdsSiteKey(input.host, input.uid, 'site.edit')) return AI_TRACKING_ROLE_COPY;
|
|
393
392
|
const locked = await ((_deps_lockdown = deps.lockdown) != null ? _deps_lockdown : getLockdownVerdict)({
|
|
394
393
|
staff: false,
|
|
395
394
|
uid: input.uid,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-tracking-step.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport { hostRoleCanWrite } from '@aglyn/aglyn/app-utils/organizations'\nimport { SCREEN_ROOT_PATH } from '@aglyn/aglyn/app-utils/screen-route'\nimport {\n GA_MEASUREMENT_ID_PATTERN,\n GOOGLE_ADS_ID_PATTERN,\n GTM_CONTAINER_ID_PATTERN,\n LINKEDIN_PARTNER_ID_PATTERN,\n META_PIXEL_ID_PATTERN,\n PINTEREST_TAG_ID_PATTERN,\n TIKTOK_PIXEL_ID_PATTERN,\n} from '@aglyn/aglyn/app-utils/visitor-consent'\nimport { dropPluginSiteCache } from '@aglyn/aglyn/plugin-manager/plugin-site-cache'\nimport type { PluginAiCapabilityArgsSchema, PluginAiCapabilityInputField } from '@aglyn/aglyn/plugin-manager/plugin-ai-capabilities'\nimport { getLockdownVerdict } from '@aglyn/tenant-data-admin/server/lockdown'\nimport { resolveOrgIdForHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport { FieldValue, type Firestore } from 'firebase-admin/firestore'\nimport type { AiJobOutput } from '../model/ai-jobs.types'\nimport { registerAiJobAdmission, type AiJobAdmission, type AiJobAdmissionRefusal } from './ai-job-admission'\nimport { aiJobEditModel } from './ai-job-edit-step'\nimport { aiLimitReview, aiUnspentOutcome } from './ai-job-generation'\nimport type { AiJobStepOutcome, AiJobStepRunner } from './ai-job-text-step'\nimport { registerAiJobStep } from './ai-jobs'\n\n/**\n * The `tracking` step (AGL-3751): the site's analytics and advertising tags,\n * as the console's Setup → Tracking card sets them — Google Analytics, Google\n * Tag Manager, and the Meta, Google Ads, LinkedIn, TikTok and Pinterest tags —\n * as a first-class build operation, so \"connect Google Analytics G-4R7KX2PQ9Z\"\n * is one item on Assist's plan card.\n *\n * NO MODEL. The planner already wrote the ids as the item's arguments; this\n * holds each to the pattern the card's field (and the tenant's loader) holds\n * it to and writes it, so it costs no credits.\n *\n * ── Only the card's own fields, and only public ids ──────────────────────\n *\n * Every field written is one the Tracking card writes, under the same name:\n * `analytics.gaMeasurementId`, `analytics.gtmContainerId` and\n * `analytics.adTags.{meta,google-ads,linkedin,tiktok,pinterest}`, each tested\n * by the same expression (`GA_MEASUREMENT_ID_PATTERN` and its siblings in\n * `visitor-consent.ts`). Each is an id that is printed in every page's HTML\n * anyway, never a credential. Removing one deletes the field, as the card's\n * cleared box does (`CLEARABLE_TRACKING_PATHS`).\n *\n * NEVER: the consent banner (`consent.*` — turning it off, its mode, or the\n * advertising question), search indexing, verification tokens, and anything\n * that needs a secret the person pastes — the Ad conversions card's access\n * tokens, Zapier, order webhooks' signing secrets. Those are not arguments.\n * The tags load only where the consent banner lets them, exactly as a tag the\n * card saved does: the tenant gates on the stored ids, not on who wrote them.\n *\n * ── Live on confirm, who may, idempotent ─────────────────────────────────\n *\n * As the `settings` step (AGL-3748): a tag has no draft, so the plan card's\n * confirm is the agreement and the write is live. One transaction re-reads\n * the host, re-checks the role, writes only what differs with a site-wide\n * publish outbox entry, and the site's cached pages are dropped after. The\n * gate is the Firestore rules' for the host document (`canWriteHostContent`,\n * `hostWritesFrozen`): an admin, editor or author, while no lockdown freezes\n * the site, its org or the platform.\n */\n\nexport const AI_TRACKING_KIND = 'tracking' as const\n\n/** ASSUMED: the least time the step needs — the site, the org and one transaction. */\nexport const AI_JOB_TRACKING_STEP_MINIMUM_MS = 5_000\n\nexport const AI_TRACKING_NO_SITE_COPY = 'Open the site whose tracking should change.'\nexport const AI_TRACKING_ROLE_COPY = 'Changing the site’s tracking needs the admin, editor or author role on this site.'\nexport const AI_TRACKING_LOCKED_COPY = 'This site is not accepting changes right now.'\nexport const AI_TRACKING_NOTHING_COPY = 'The request named no tracking id this could set or remove.'\n\n/** Where a person finds what the op changed: the card that edits it. */\nexport const AI_TRACKING_WHERE = 'Setup → Tracking'\n\n/** The output's fixed id: one site's tracking, whatever changed. */\nexport const AI_TRACKING_OUTPUT_ID = 'tracking'\n\n/** One tag the card sets: its argument, its stored field, its name, and its pattern. */\nexport interface AiTrackingTag {\n arg: string\n path: string\n label: string\n pattern: RegExp\n /** What the card's field says the id looks like. */\n shape: string\n /** The id's name, as a field asking for it is labeled (AGL-3978): it names the provider. */\n field: string\n /** Asked when a request names this tag and no usable id, from the card's helper text. */\n ask: string\n /** Words that name this tag in an item's name. */\n named: RegExp\n}\n\n/** The Tracking card's fields, in its order (`trackingSchema`, host-settings-scope.tsx). */\nexport const AI_TRACKING_TAGS: readonly AiTrackingTag[] = [\n {\n arg: 'googleAnalytics',\n path: 'analytics.gaMeasurementId',\n label: 'Google Analytics',\n pattern: GA_MEASUREMENT_ID_PATTERN,\n shape: 'G-XXXXXXXXXX',\n field: 'Google Analytics measurement ID',\n ask: 'What’s your Google Analytics measurement ID? It looks like G-XXXXXXXXXX.',\n named: /google analytics|\\bga4?\\b|\\banalytics\\b/i,\n },\n {\n arg: 'googleTagManager',\n path: 'analytics.gtmContainerId',\n label: 'Google Tag Manager',\n pattern: GTM_CONTAINER_ID_PATTERN,\n shape: 'GTM-XXXXXXX',\n field: 'Google Tag Manager container ID',\n ask: 'What’s your Google Tag Manager container ID? It looks like GTM-XXXXXXX.',\n named: /tag manager|\\bgtm\\b/i,\n },\n {\n arg: 'metaPixel',\n path: 'analytics.adTags.meta',\n label: 'the Meta pixel',\n pattern: META_PIXEL_ID_PATTERN,\n shape: '8–20 digits',\n field: 'Meta pixel ID',\n ask: 'What’s your Meta pixel ID? It’s the number (8–20 digits) shown for your pixel in Meta Events Manager.',\n named: /\\bmeta\\b|facebook|instagram/i,\n },\n {\n arg: 'googleAds',\n path: 'analytics.adTags.google-ads',\n label: 'the Google Ads tag',\n pattern: GOOGLE_ADS_ID_PATTERN,\n shape: 'AW-123456789',\n field: 'Google Ads conversion ID',\n ask: 'What’s your Google Ads conversion ID? It looks like AW-123456789, from Google Ads.',\n named: /google ads|adwords/i,\n },\n {\n arg: 'linkedinInsight',\n path: 'analytics.adTags.linkedin',\n label: 'the LinkedIn Insight Tag',\n pattern: LINKEDIN_PARTNER_ID_PATTERN,\n shape: '4–10 digits',\n field: 'LinkedIn partner ID',\n ask: 'What’s your LinkedIn partner ID? It’s the number (4–10 digits) in LinkedIn Campaign Manager.',\n named: /linkedin/i,\n },\n {\n arg: 'tiktokPixel',\n path: 'analytics.adTags.tiktok',\n label: 'the TikTok pixel',\n pattern: TIKTOK_PIXEL_ID_PATTERN,\n shape: '16–24 capital letters and digits',\n field: 'TikTok pixel code',\n ask: 'What’s your TikTok pixel code? It’s in TikTok Ads Manager → Events, like C4ABCDEFGH1234567890.',\n named: /tiktok/i,\n },\n {\n arg: 'pinterestTag',\n path: 'analytics.adTags.pinterest',\n label: 'the Pinterest tag',\n pattern: PINTEREST_TAG_ID_PATTERN,\n shape: '10–16 digits',\n field: 'Pinterest tag ID',\n ask: 'What’s your Pinterest tag ID? It’s the number (10–16 digits) in Pinterest Ads → Conversions.',\n named: /pinterest/i,\n },\n]\n\n/** Asked when an item names no tag at all. */\nexport const AI_TRACKING_WHICH_COPY =\n 'Which tag should be connected, and what is its ID? Google Analytics, Google Tag Manager, the Meta pixel, Google Ads, LinkedIn, TikTok or Pinterest.'\n\nconst TAG_ARGS = AI_TRACKING_TAGS.map((tag) => tag.arg)\n\nconst ID_RULE = 'Exactly as the person gave it; leave it out unless the request states the id — never make one up.'\n\n/** The item's arguments, as the planner fills them in. Every one optional. */\nexport const AI_TRACKING_ARGS_SCHEMA: PluginAiCapabilityArgsSchema = {\n type: 'object',\n properties: {\n googleAnalytics: { type: 'string', description: `The Google Analytics 4 measurement ID, like G-XXXXXXXXXX. ${ID_RULE}`, maxLength: 24 },\n googleTagManager: { type: 'string', description: `The Google Tag Manager container ID, like GTM-XXXXXXX. ${ID_RULE}`, maxLength: 24 },\n metaPixel: { type: 'string', description: `The Meta (Facebook) pixel ID, 8–20 digits. ${ID_RULE}`, maxLength: 24 },\n googleAds: { type: 'string', description: `The Google Ads conversion ID, like AW-123456789. ${ID_RULE}`, maxLength: 24 },\n linkedinInsight: { type: 'string', description: `The LinkedIn Insight Tag partner ID, 4–10 digits. ${ID_RULE}`, maxLength: 24 },\n tiktokPixel: { type: 'string', description: `The TikTok pixel code, like C4ABCDEFGH1234567890. ${ID_RULE}`, maxLength: 32 },\n pinterestTag: { type: 'string', description: `The Pinterest tag ID, 10–16 digits. ${ID_RULE}`, maxLength: 24 },\n remove: {\n type: 'array',\n description: 'The tags the person asked to take off the site.',\n items: { type: 'string', enum: TAG_ARGS },\n maxItems: AI_TRACKING_TAGS.length,\n },\n },\n additionalProperties: false,\n}\n\n/** What an item asks: ids to set by field, and fields to clear. */\nexport interface AiTrackingRequest {\n set: Record<string, string>\n remove: string[]\n}\n\nexport interface AiTrackingRead {\n request: AiTrackingRequest\n /** A sentence per value that could not be used, so the rest still are. */\n problems: string[]\n}\n\nconst text = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/** The item's arguments read: what may be written, and a sentence for each that may not. */\nexport function aiTrackingRequestOf(inputs: Readonly<Record<string, unknown>> | null | undefined): AiTrackingRead {\n const set: Record<string, string> = {}\n const problems: string[] = []\n const removing = new Set(Array.isArray(inputs?.['remove']) ? (inputs?.['remove'] as unknown[]).map((one) => String(one)) : [])\n const remove: string[] = []\n for (const tag of AI_TRACKING_TAGS) {\n const value = text(inputs?.[tag.arg])\n if (value && removing.has(tag.arg)) {\n problems.push(`${capitalized(tag.label)} was both set and removed, so it was left as it is.`)\n continue\n }\n if (removing.has(tag.arg)) {\n remove.push(tag.path)\n continue\n }\n if (!value) continue\n // The card's own test, and the loader's: an id of any other shape is\n // saved by nobody, since it would never load.\n if (tag.pattern.test(value)) set[tag.path] = value\n else problems.push(`“${value.slice(0, 40)}” is not an ID for ${tag.label}, which looks like ${tag.shape}.`)\n }\n return { request: { set, remove }, problems }\n}\n\nfunction capitalized(label: string): string {\n return label.charAt(0).toUpperCase() + label.slice(1)\n}\n\n/**\n * The question a planned tracking change asks the person, or `null` when it\n * sets or removes something (AGL-3751). An item with no usable id — the\n * planner kept \"add my Meta pixel\" with no id, or an id of the wrong shape —\n * is never built: it asks for the id of the tag its arguments or its name\n * name, in the card's own words, and says why an id given was not one.\n */\nexport function aiTrackingMissingInput(item: AiTrackingItem): string | null {\n const read = aiTrackingRequestOf(item.args)\n if (!aiTrackingRequestEmpty(read.request)) return null\n const named = aiTrackingTagsAsked(item)\n const asks = named.length ? named.map((tag) => tag.ask) : [AI_TRACKING_WHICH_COPY]\n return [...read.problems, ...asks].join(' ')\n}\n\ntype AiTrackingItem = { name?: string; why?: string; args: Readonly<Record<string, unknown>> }\n\n/** The tags an item asks about: those its arguments tried to set, else the ones its name or reason names. */\nfunction aiTrackingTagsAsked(item: AiTrackingItem): AiTrackingTag[] {\n const tried = AI_TRACKING_TAGS.filter((tag) => text(item.args[tag.arg]))\n const naming = (words: string | undefined) => AI_TRACKING_TAGS.filter((tag) => tag.named.test(words ?? ''))\n const byName = naming(item.name)\n return tried.length ? tried : byName.length ? byName : naming(item.why)\n}\n\n/**\n * The fields a planned tracking change with no usable id waits for (AGL-3978):\n * one per tag it asks about, labeled with the provider's id and held to the\n * card's pattern, so the build pauses in `needs_input` for the person to give\n * it. Empty when the item can be built, or names no tag at all.\n */\nexport function aiTrackingInputFields(item: AiTrackingItem): PluginAiCapabilityInputField[] {\n if (!aiTrackingMissingInput(item)) return []\n return aiTrackingTagsAsked(item).map((tag) => ({ arg: tag.arg, label: tag.field, shape: tag.shape, pattern: tag.pattern }))\n}\n\n/** Whether a read request asks for anything at all. */\nexport function aiTrackingRequestEmpty(request: AiTrackingRequest): boolean {\n return Object.keys(request.set).length === 0 && request.remove.length === 0\n}\n\n// ── The write ─────────────────────────────────────────────────────────────\n\nconst getPath = (data: Record<string, unknown>, path: string): unknown =>\n path.split('.').reduce<unknown>((at, key) => (at && typeof at === 'object' ? (at as Record<string, unknown>)[key] : undefined), data)\n\n/** One change to the host document, with the sentence that says it was made. */\nexport interface AiTrackingChange {\n path: string\n /** The id to store, or `null` to delete the field. */\n value: string | null\n said: string\n}\n\n/** Every change the request asks, in the card's order, before comparing with what is stored. */\nexport function aiTrackingChanges(request: AiTrackingRequest): AiTrackingChange[] {\n const changes: AiTrackingChange[] = []\n for (const tag of AI_TRACKING_TAGS) {\n const value = request.set[tag.path]\n if (value) changes.push({ path: tag.path, value, said: `${capitalized(tag.label)} is connected (${value}).` })\n else if (request.remove.includes(tag.path)) changes.push({ path: tag.path, value: null, said: `${capitalized(tag.label)} is removed.` })\n }\n return changes\n}\n\n/**\n * What the person should know about the tags the site now carries, in the\n * card's own words: where the consent banner is off, nothing of ours gates\n * them; and a container beside a direct Google tag counts everything twice.\n */\nexport function aiTrackingWarnings(host: Record<string, unknown>): string[] {\n const warnings: string[] = []\n const has = (path: string) => {\n const value = getPath(host, path)\n return typeof value === 'string' && value !== ''\n }\n const any = AI_TRACKING_TAGS.some((tag) => has(tag.path))\n if (any && getPath(host, 'consent.disabled') === true) {\n warnings.push('This site’s cookie banner is turned off because it uses its own consent tool, so that tool decides when these load.')\n }\n if (has('analytics.gtmContainerId') && (has('analytics.gaMeasurementId') || has('analytics.adTags.google-ads'))) {\n warnings.push('Do not also put an Analytics or Google Ads tag in the Tag Manager container: that loads the same measurement twice and counts every visit and conversion twice.')\n }\n return warnings\n}\n\nexport type AiTrackingWrite =\n | { ok: true; done: string[]; changed: string[]; warnings: string[]; outboxId: string | null }\n | { ok: false; error: string }\n\n/**\n * The tags written, in one transaction that reads the host as it is stored\n * NOW: the member still holds a write role, and only the values that differ\n * from what is stored are written, with a site-wide publish outbox entry\n * beside them so the live site's cached pages are dropped.\n */\nexport async function writeAiSiteTracking(\n firestore: Firestore,\n input: { hostId: string; uid: string; request: AiTrackingRequest; now: Date },\n): Promise<AiTrackingWrite> {\n const hostRef = firestore.collection('hosts').doc(input.hostId)\n return firestore.runTransaction(async (tx): Promise<AiTrackingWrite> => {\n const host = await tx.get(hostRef)\n if (!host.exists) return { ok: false, error: 'Unknown site' }\n const roles = (host.get('memberRoles') ?? {}) as Record<string, unknown>\n if (!hostRoleCanWrite(roles[input.uid])) return { ok: false, error: AI_TRACKING_ROLE_COPY }\n const data = (host.data() ?? {}) as Record<string, unknown>\n const changes = aiTrackingChanges(input.request)\n const changed = changes.filter((change) => {\n const stored = getPath(data, change.path)\n return change.value === null ? stored != null && stored !== '' : stored !== change.value\n })\n // What the site carries once this lands, for the warnings.\n // Only the two maps the warnings read; both are plain strings and flags.\n const after: Record<string, unknown> = JSON.parse(JSON.stringify({ analytics: data['analytics'] ?? {}, consent: data['consent'] ?? {} }))\n for (const change of changed) {\n const keys = change.path.split('.')\n const leaf = keys.pop() as string\n let node = after as Record<string, unknown>\n for (const key of keys) {\n if (!node[key] || typeof node[key] !== 'object') node[key] = {}\n node = node[key] as Record<string, unknown>\n }\n if (change.value === null) delete node[leaf]\n else node[leaf] = change.value\n }\n const done = changes.map((change) => change.said)\n const warnings = aiTrackingWarnings(after)\n if (!changed.length) return { ok: true, done, changed: [], warnings, outboxId: null }\n const updates: Record<string, unknown> = { updatedAt: input.now }\n for (const change of changed) updates[change.path] = change.value === null ? FieldValue.delete() : change.value\n tx.update(hostRef, updates)\n // The tags are in every page's head, so every cached page is stale.\n const outboxRef = firestore.collection('publishOutbox').doc(createResourceUid())\n tx.set(outboxRef, {\n hostId: input.hostId,\n paths: [SCREEN_ROOT_PATH],\n entireHost: true,\n createdAt: FieldValue.serverTimestamp(),\n attempts: 0,\n })\n return { ok: true, done, changed: changed.map((change) => change.path), warnings, outboxId: outboxRef.id }\n })\n}\n\n// ── The step ──────────────────────────────────────────────────────────────\n\n/** Why this member may not change this site's tracking now, or `null`. */\nexport async function aiTrackingAccessRefusal(\n input: { host: FirebaseFirestore.DocumentSnapshot; uid: string; org: object | null },\n deps: { lockdown?: typeof getLockdownVerdict } = {},\n): Promise<string | null> {\n if (!input.host.exists) return 'Unknown site'\n const roles = (input.host.get('memberRoles') ?? {}) as Record<string, unknown>\n if (!hostRoleCanWrite(roles[input.uid])) return AI_TRACKING_ROLE_COPY\n const locked = await (deps.lockdown ?? getLockdownVerdict)({\n staff: false,\n uid: input.uid,\n org: input.org as never,\n host: input.host.data() as never,\n intent: 'write',\n })\n return locked ? AI_TRACKING_LOCKED_COPY : null\n}\n\n/** The output's note: what changed, that it is live, how consent gates it, and what was not done. */\nexport function aiTrackingOutputNote(done: readonly string[], warnings: readonly string[], notDone: readonly string[], changedAny: boolean): string {\n const did = done.length\n ? `${done.join(' ')} Live now, in ${AI_TRACKING_WHERE}.${changedAny ? '' : ' It already was, so nothing was written.'}`\n : 'Nothing needed changing.'\n // The card's own promise, true of a tag whoever saved it.\n const consent = done.length ? ' Tags load only where a visitor’s cookie consent allows them.' : ''\n const warn = warnings.length ? ` ${warnings.join(' ')}` : ''\n const not = notDone.length ? ` Not done: ${notDone.join(' ')}` : ''\n return `${did}${consent}${warn}${not}`.slice(0, 1_000)\n}\n\nexport interface AiJobTrackingStepDeps {\n write?: typeof writeAiSiteTracking\n lockdown?: typeof getLockdownVerdict\n dropCache?: typeof dropPluginSiteCache\n}\n\nexport function createAiJobTrackingStep(deps: AiJobTrackingStepDeps = {}): AiJobStepRunner {\n const write = deps.write ?? writeAiSiteTracking\n const dropCache = deps.dropCache ?? dropPluginSiteCache\n return async ({ job, now, firestore, org, modelFor }): Promise<AiJobStepOutcome> => {\n // No model is asked: the outcome records the edit step's at no cost.\n const model = modelFor?.('job.edit') ?? aiJobEditModel()\n const unspent = (failure: string): AiJobStepOutcome => ({ ...aiUnspentOutcome(model), failure })\n const hostId = job.hostId\n if (!hostId) return unspent(AI_TRACKING_NO_SITE_COPY)\n const read = aiTrackingRequestOf(job.inputs)\n if (aiTrackingRequestEmpty(read.request)) return unspent([AI_TRACKING_NOTHING_COPY, ...read.problems].join(' '))\n const orgData = (org ?? ((await firestore.collection('orgs').doc(job.orgId).get()).data() ?? null)) as Record<string, unknown> | null\n const host = await firestore.collection('hosts').doc(hostId).get()\n const refused = await aiTrackingAccessRefusal({ host, uid: job.createdBy, org: orgData }, { lockdown: deps.lockdown })\n if (refused) return aiUnspentOutcome(model, { review: aiLimitReview(refused) })\n const written = await write(firestore, { hostId, uid: job.createdBy, request: read.request, now })\n if (written.ok === false) return unspent(written.error)\n if (written.outboxId) {\n // The live site's cached pages go now; the outbox entry stays for the\n // drain unless the drop certainly happened.\n const dropped = await dropCache({ hostIds: [hostId], reason: 'AI build changed site tracking' }).catch(() => ({ complete: false }))\n if (dropped.complete) await firestore.collection('publishOutbox').doc(written.outboxId).delete().catch(() => undefined)\n }\n const output: AiJobOutput = {\n resource: 'tracking',\n id: AI_TRACKING_OUTPUT_ID,\n hostId,\n hostSubdomain: (typeof host.get('subdomain') === 'string' && host.get('subdomain')) || null,\n label: 'Site tracking',\n note: aiTrackingOutputNote(written.done, written.warnings, read.problems, written.changed.length > 0),\n }\n return aiUnspentOutcome(model, { outputs: [output] })\n }\n}\n\nexport const runAiJobTrackingStep = createAiJobTrackingStep()\n\n// ── Admission ─────────────────────────────────────────────────────────────\n\nexport interface AiTrackingJobAdmissionDeps {\n ownerOf?: typeof resolveOrgIdForHost\n lockdown?: typeof getLockdownVerdict\n}\n\n/**\n * A tracking change is admitted on a site of the job's own org, for a member\n * who may write its settings now, where the request names at least one tag\n * this can set or remove. Refused in those words before anything is written.\n */\nexport function createAiTrackingJobAdmission(deps: AiTrackingJobAdmissionDeps = {}): AiJobAdmission {\n const ownerOf = deps.ownerOf ?? resolveOrgIdForHost\n return async (context): Promise<AiJobAdmissionRefusal | null> => {\n if (!context.hostId) return { status: 400, error: AI_TRACKING_NO_SITE_COPY }\n const owner = await ownerOf(context.hostId)\n if (!owner || owner !== context.orgId) return { status: 404, error: 'Unknown site' }\n const host = await context.firestore.collection('hosts').doc(context.hostId).get()\n const refused = await aiTrackingAccessRefusal({ host, uid: context.uid ?? '', org: context.org }, { lockdown: deps.lockdown })\n if (refused) return { status: 403, error: refused }\n const read = aiTrackingRequestOf(context.inputs as Record<string, unknown>)\n return aiTrackingRequestEmpty(read.request) ? { status: 400, error: [AI_TRACKING_NOTHING_COPY, ...read.problems].join(' ') } : null\n }\n}\n\nexport const aiTrackingJobAdmission = createAiTrackingJobAdmission()\n\n/** Registers the tracking step and its door (AGL-3751). */\nexport function registerAiTrackingJob(): void {\n registerAiJobStep(AI_TRACKING_KIND, runAiJobTrackingStep, { minimumMs: AI_JOB_TRACKING_STEP_MINIMUM_MS })\n registerAiJobAdmission(AI_TRACKING_KIND, aiTrackingJobAdmission)\n}\n"],"names":["createResourceUid","hostRoleCanWrite","SCREEN_ROOT_PATH","GA_MEASUREMENT_ID_PATTERN","GOOGLE_ADS_ID_PATTERN","GTM_CONTAINER_ID_PATTERN","LINKEDIN_PARTNER_ID_PATTERN","META_PIXEL_ID_PATTERN","PINTEREST_TAG_ID_PATTERN","TIKTOK_PIXEL_ID_PATTERN","dropPluginSiteCache","getLockdownVerdict","resolveOrgIdForHost","FieldValue","registerAiJobAdmission","aiJobEditModel","aiLimitReview","aiUnspentOutcome","registerAiJobStep","AI_TRACKING_KIND","AI_JOB_TRACKING_STEP_MINIMUM_MS","AI_TRACKING_NO_SITE_COPY","AI_TRACKING_ROLE_COPY","AI_TRACKING_LOCKED_COPY","AI_TRACKING_NOTHING_COPY","AI_TRACKING_WHERE","AI_TRACKING_OUTPUT_ID","AI_TRACKING_TAGS","arg","path","label","pattern","shape","field","ask","named","AI_TRACKING_WHICH_COPY","TAG_ARGS","map","tag","ID_RULE","AI_TRACKING_ARGS_SCHEMA","type","properties","googleAnalytics","description","maxLength","googleTagManager","metaPixel","googleAds","linkedinInsight","tiktokPixel","pinterestTag","remove","items","enum","maxItems","length","additionalProperties","text","value","trim","aiTrackingRequestOf","inputs","set","problems","removing","Set","Array","isArray","one","String","has","push","capitalized","test","slice","request","charAt","toUpperCase","aiTrackingMissingInput","item","read","args","aiTrackingRequestEmpty","aiTrackingTagsAsked","asks","join","tried","filter","naming","words","byName","name","why","aiTrackingInputFields","Object","keys","getPath","data","split","reduce","at","key","undefined","aiTrackingChanges","changes","said","includes","aiTrackingWarnings","host","warnings","any","some","writeAiSiteTracking","firestore","input","hostRef","collection","doc","hostId","runTransaction","tx","get","exists","ok","error","roles","uid","changed","change","stored","after","JSON","parse","stringify","analytics","consent","leaf","pop","node","done","outboxId","updates","updatedAt","now","delete","update","outboxRef","paths","entireHost","createdAt","serverTimestamp","attempts","id","aiTrackingAccessRefusal","deps","locked","lockdown","staff","org","intent","aiTrackingOutputNote","notDone","changedAny","did","warn","not","createAiJobTrackingStep","write","dropCache","job","modelFor","model","unspent","failure","orgData","orgId","refused","createdBy","review","written","dropped","hostIds","reason","catch","complete","output","resource","hostSubdomain","note","outputs","runAiJobTrackingStep","createAiTrackingJobAdmission","ownerOf","context","status","owner","aiTrackingJobAdmission","registerAiTrackingJob","minimumMs"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-tracking-step.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport { holdsSiteKey } from '@aglyn/aglyn/app-utils/host-permissions'\nimport { orgPermissionLabel } from '@aglyn/aglyn/app-utils/org-permissions'\nimport { SCREEN_ROOT_PATH } from '@aglyn/aglyn/app-utils/screen-route'\nimport {\n GA_MEASUREMENT_ID_PATTERN,\n GOOGLE_ADS_ID_PATTERN,\n GTM_CONTAINER_ID_PATTERN,\n LINKEDIN_PARTNER_ID_PATTERN,\n META_PIXEL_ID_PATTERN,\n PINTEREST_TAG_ID_PATTERN,\n TIKTOK_PIXEL_ID_PATTERN,\n} from '@aglyn/aglyn/app-utils/visitor-consent'\nimport { dropPluginSiteCache } from '@aglyn/aglyn/plugin-manager/plugin-site-cache'\nimport type { PluginAiCapabilityArgsSchema, PluginAiCapabilityInputField } from '@aglyn/aglyn/plugin-manager/plugin-ai-capabilities'\nimport { getLockdownVerdict } from '@aglyn/tenant-data-admin/server/lockdown'\nimport { resolveOrgIdForHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport { FieldValue, type Firestore } from 'firebase-admin/firestore'\nimport type { AiJobOutput } from '../model/ai-jobs.types'\nimport { registerAiJobAdmission, type AiJobAdmission, type AiJobAdmissionRefusal } from './ai-job-admission'\nimport { aiJobEditModel } from './ai-job-edit-step'\nimport { aiLimitReview, aiUnspentOutcome } from './ai-job-generation'\nimport type { AiJobStepOutcome, AiJobStepRunner } from './ai-job-text-step'\nimport { registerAiJobStep } from './ai-jobs'\n\n/**\n * The `tracking` step (AGL-3751): the site's analytics and advertising tags,\n * as the console's Setup → Tracking card sets them — Google Analytics, Google\n * Tag Manager, and the Meta, Google Ads, LinkedIn, TikTok and Pinterest tags —\n * as a first-class build operation, so \"connect Google Analytics G-4R7KX2PQ9Z\"\n * is one item on Assist's plan card.\n *\n * NO MODEL. The planner already wrote the ids as the item's arguments; this\n * holds each to the pattern the card's field (and the tenant's loader) holds\n * it to and writes it, so it costs no credits.\n *\n * ── Only the card's own fields, and only public ids ──────────────────────\n *\n * Every field written is one the Tracking card writes, under the same name:\n * `analytics.gaMeasurementId`, `analytics.gtmContainerId` and\n * `analytics.adTags.{meta,google-ads,linkedin,tiktok,pinterest}`, each tested\n * by the same expression (`GA_MEASUREMENT_ID_PATTERN` and its siblings in\n * `visitor-consent.ts`). Each is an id that is printed in every page's HTML\n * anyway, never a credential. Removing one deletes the field, as the card's\n * cleared box does (`CLEARABLE_TRACKING_PATHS`).\n *\n * NEVER: the consent banner (`consent.*` — turning it off, its mode, or the\n * advertising question), search indexing, verification tokens, and anything\n * that needs a secret the person pastes — the Ad conversions card's access\n * tokens, Zapier, order webhooks' signing secrets. Those are not arguments.\n * The tags load only where the consent banner lets them, exactly as a tag the\n * card saved does: the tenant gates on the stored ids, not on who wrote them.\n *\n * ── Live on confirm, who may, idempotent ─────────────────────────────────\n *\n * As the `settings` step (AGL-3748): a tag has no draft, so the plan card's\n * confirm is the agreement and the write is live. One transaction re-reads\n * the host, re-checks the role, writes only what differs with a site-wide\n * publish outbox entry, and the site's cached pages are dropped after. The\n * gate is the Firestore rules' for the host document (`canWriteHostContent`,\n * `hostWritesFrozen`): an admin, editor or author, while no lockdown freezes\n * the site, its org or the platform.\n */\n\nexport const AI_TRACKING_KIND = 'tracking' as const\n\n/** ASSUMED: the least time the step needs — the site, the org and one transaction. */\nexport const AI_JOB_TRACKING_STEP_MINIMUM_MS = 5_000\n\nexport const AI_TRACKING_NO_SITE_COPY = 'Open the site whose tracking should change.'\n/** Names \"Edit site content\" (`site.edit`, AGL-4047), never a role. */\nexport const AI_TRACKING_ROLE_COPY = `Changing the site’s tracking needs the \"${orgPermissionLabel('site.edit')}\" permission on this site — ask an organization admin.`\nexport const AI_TRACKING_LOCKED_COPY = 'This site is not accepting changes right now.'\nexport const AI_TRACKING_NOTHING_COPY = 'The request named no tracking id this could set or remove.'\n\n/** Where a person finds what the op changed: the card that edits it. */\nexport const AI_TRACKING_WHERE = 'Setup → Tracking'\n\n/** The output's fixed id: one site's tracking, whatever changed. */\nexport const AI_TRACKING_OUTPUT_ID = 'tracking'\n\n/** One tag the card sets: its argument, its stored field, its name, and its pattern. */\nexport interface AiTrackingTag {\n arg: string\n path: string\n label: string\n pattern: RegExp\n /** What the card's field says the id looks like. */\n shape: string\n /** The id's name, as a field asking for it is labeled (AGL-3978): it names the provider. */\n field: string\n /** Asked when a request names this tag and no usable id, from the card's helper text. */\n ask: string\n /** Words that name this tag in an item's name. */\n named: RegExp\n}\n\n/** The Tracking card's fields, in its order (`trackingSchema`, host-settings-scope.tsx). */\nexport const AI_TRACKING_TAGS: readonly AiTrackingTag[] = [\n {\n arg: 'googleAnalytics',\n path: 'analytics.gaMeasurementId',\n label: 'Google Analytics',\n pattern: GA_MEASUREMENT_ID_PATTERN,\n shape: 'G-XXXXXXXXXX',\n field: 'Google Analytics measurement ID',\n ask: 'What’s your Google Analytics measurement ID? It looks like G-XXXXXXXXXX.',\n named: /google analytics|\\bga4?\\b|\\banalytics\\b/i,\n },\n {\n arg: 'googleTagManager',\n path: 'analytics.gtmContainerId',\n label: 'Google Tag Manager',\n pattern: GTM_CONTAINER_ID_PATTERN,\n shape: 'GTM-XXXXXXX',\n field: 'Google Tag Manager container ID',\n ask: 'What’s your Google Tag Manager container ID? It looks like GTM-XXXXXXX.',\n named: /tag manager|\\bgtm\\b/i,\n },\n {\n arg: 'metaPixel',\n path: 'analytics.adTags.meta',\n label: 'the Meta pixel',\n pattern: META_PIXEL_ID_PATTERN,\n shape: '8–20 digits',\n field: 'Meta pixel ID',\n ask: 'What’s your Meta pixel ID? It’s the number (8–20 digits) shown for your pixel in Meta Events Manager.',\n named: /\\bmeta\\b|facebook|instagram/i,\n },\n {\n arg: 'googleAds',\n path: 'analytics.adTags.google-ads',\n label: 'the Google Ads tag',\n pattern: GOOGLE_ADS_ID_PATTERN,\n shape: 'AW-123456789',\n field: 'Google Ads conversion ID',\n ask: 'What’s your Google Ads conversion ID? It looks like AW-123456789, from Google Ads.',\n named: /google ads|adwords/i,\n },\n {\n arg: 'linkedinInsight',\n path: 'analytics.adTags.linkedin',\n label: 'the LinkedIn Insight Tag',\n pattern: LINKEDIN_PARTNER_ID_PATTERN,\n shape: '4–10 digits',\n field: 'LinkedIn partner ID',\n ask: 'What’s your LinkedIn partner ID? It’s the number (4–10 digits) in LinkedIn Campaign Manager.',\n named: /linkedin/i,\n },\n {\n arg: 'tiktokPixel',\n path: 'analytics.adTags.tiktok',\n label: 'the TikTok pixel',\n pattern: TIKTOK_PIXEL_ID_PATTERN,\n shape: '16–24 capital letters and digits',\n field: 'TikTok pixel code',\n ask: 'What’s your TikTok pixel code? It’s in TikTok Ads Manager → Events, like C4ABCDEFGH1234567890.',\n named: /tiktok/i,\n },\n {\n arg: 'pinterestTag',\n path: 'analytics.adTags.pinterest',\n label: 'the Pinterest tag',\n pattern: PINTEREST_TAG_ID_PATTERN,\n shape: '10–16 digits',\n field: 'Pinterest tag ID',\n ask: 'What’s your Pinterest tag ID? It’s the number (10–16 digits) in Pinterest Ads → Conversions.',\n named: /pinterest/i,\n },\n]\n\n/** Asked when an item names no tag at all. */\nexport const AI_TRACKING_WHICH_COPY =\n 'Which tag should be connected, and what is its ID? Google Analytics, Google Tag Manager, the Meta pixel, Google Ads, LinkedIn, TikTok or Pinterest.'\n\nconst TAG_ARGS = AI_TRACKING_TAGS.map((tag) => tag.arg)\n\nconst ID_RULE = 'Exactly as the person gave it; leave it out unless the request states the id — never make one up.'\n\n/** The item's arguments, as the planner fills them in. Every one optional. */\nexport const AI_TRACKING_ARGS_SCHEMA: PluginAiCapabilityArgsSchema = {\n type: 'object',\n properties: {\n googleAnalytics: { type: 'string', description: `The Google Analytics 4 measurement ID, like G-XXXXXXXXXX. ${ID_RULE}`, maxLength: 24 },\n googleTagManager: { type: 'string', description: `The Google Tag Manager container ID, like GTM-XXXXXXX. ${ID_RULE}`, maxLength: 24 },\n metaPixel: { type: 'string', description: `The Meta (Facebook) pixel ID, 8–20 digits. ${ID_RULE}`, maxLength: 24 },\n googleAds: { type: 'string', description: `The Google Ads conversion ID, like AW-123456789. ${ID_RULE}`, maxLength: 24 },\n linkedinInsight: { type: 'string', description: `The LinkedIn Insight Tag partner ID, 4–10 digits. ${ID_RULE}`, maxLength: 24 },\n tiktokPixel: { type: 'string', description: `The TikTok pixel code, like C4ABCDEFGH1234567890. ${ID_RULE}`, maxLength: 32 },\n pinterestTag: { type: 'string', description: `The Pinterest tag ID, 10–16 digits. ${ID_RULE}`, maxLength: 24 },\n remove: {\n type: 'array',\n description: 'The tags the person asked to take off the site.',\n items: { type: 'string', enum: TAG_ARGS },\n maxItems: AI_TRACKING_TAGS.length,\n },\n },\n additionalProperties: false,\n}\n\n/** What an item asks: ids to set by field, and fields to clear. */\nexport interface AiTrackingRequest {\n set: Record<string, string>\n remove: string[]\n}\n\nexport interface AiTrackingRead {\n request: AiTrackingRequest\n /** A sentence per value that could not be used, so the rest still are. */\n problems: string[]\n}\n\nconst text = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/** The item's arguments read: what may be written, and a sentence for each that may not. */\nexport function aiTrackingRequestOf(inputs: Readonly<Record<string, unknown>> | null | undefined): AiTrackingRead {\n const set: Record<string, string> = {}\n const problems: string[] = []\n const removing = new Set(Array.isArray(inputs?.['remove']) ? (inputs?.['remove'] as unknown[]).map((one) => String(one)) : [])\n const remove: string[] = []\n for (const tag of AI_TRACKING_TAGS) {\n const value = text(inputs?.[tag.arg])\n if (value && removing.has(tag.arg)) {\n problems.push(`${capitalized(tag.label)} was both set and removed, so it was left as it is.`)\n continue\n }\n if (removing.has(tag.arg)) {\n remove.push(tag.path)\n continue\n }\n if (!value) continue\n // The card's own test, and the loader's: an id of any other shape is\n // saved by nobody, since it would never load.\n if (tag.pattern.test(value)) set[tag.path] = value\n else problems.push(`“${value.slice(0, 40)}” is not an ID for ${tag.label}, which looks like ${tag.shape}.`)\n }\n return { request: { set, remove }, problems }\n}\n\nfunction capitalized(label: string): string {\n return label.charAt(0).toUpperCase() + label.slice(1)\n}\n\n/**\n * The question a planned tracking change asks the person, or `null` when it\n * sets or removes something (AGL-3751). An item with no usable id — the\n * planner kept \"add my Meta pixel\" with no id, or an id of the wrong shape —\n * is never built: it asks for the id of the tag its arguments or its name\n * name, in the card's own words, and says why an id given was not one.\n */\nexport function aiTrackingMissingInput(item: AiTrackingItem): string | null {\n const read = aiTrackingRequestOf(item.args)\n if (!aiTrackingRequestEmpty(read.request)) return null\n const named = aiTrackingTagsAsked(item)\n const asks = named.length ? named.map((tag) => tag.ask) : [AI_TRACKING_WHICH_COPY]\n return [...read.problems, ...asks].join(' ')\n}\n\ntype AiTrackingItem = { name?: string; why?: string; args: Readonly<Record<string, unknown>> }\n\n/** The tags an item asks about: those its arguments tried to set, else the ones its name or reason names. */\nfunction aiTrackingTagsAsked(item: AiTrackingItem): AiTrackingTag[] {\n const tried = AI_TRACKING_TAGS.filter((tag) => text(item.args[tag.arg]))\n const naming = (words: string | undefined) => AI_TRACKING_TAGS.filter((tag) => tag.named.test(words ?? ''))\n const byName = naming(item.name)\n return tried.length ? tried : byName.length ? byName : naming(item.why)\n}\n\n/**\n * The fields a planned tracking change with no usable id waits for (AGL-3978):\n * one per tag it asks about, labeled with the provider's id and held to the\n * card's pattern, so the build pauses in `needs_input` for the person to give\n * it. Empty when the item can be built, or names no tag at all.\n */\nexport function aiTrackingInputFields(item: AiTrackingItem): PluginAiCapabilityInputField[] {\n if (!aiTrackingMissingInput(item)) return []\n return aiTrackingTagsAsked(item).map((tag) => ({ arg: tag.arg, label: tag.field, shape: tag.shape, pattern: tag.pattern }))\n}\n\n/** Whether a read request asks for anything at all. */\nexport function aiTrackingRequestEmpty(request: AiTrackingRequest): boolean {\n return Object.keys(request.set).length === 0 && request.remove.length === 0\n}\n\n// ── The write ─────────────────────────────────────────────────────────────\n\nconst getPath = (data: Record<string, unknown>, path: string): unknown =>\n path.split('.').reduce<unknown>((at, key) => (at && typeof at === 'object' ? (at as Record<string, unknown>)[key] : undefined), data)\n\n/** One change to the host document, with the sentence that says it was made. */\nexport interface AiTrackingChange {\n path: string\n /** The id to store, or `null` to delete the field. */\n value: string | null\n said: string\n}\n\n/** Every change the request asks, in the card's order, before comparing with what is stored. */\nexport function aiTrackingChanges(request: AiTrackingRequest): AiTrackingChange[] {\n const changes: AiTrackingChange[] = []\n for (const tag of AI_TRACKING_TAGS) {\n const value = request.set[tag.path]\n if (value) changes.push({ path: tag.path, value, said: `${capitalized(tag.label)} is connected (${value}).` })\n else if (request.remove.includes(tag.path)) changes.push({ path: tag.path, value: null, said: `${capitalized(tag.label)} is removed.` })\n }\n return changes\n}\n\n/**\n * What the person should know about the tags the site now carries, in the\n * card's own words: where the consent banner is off, nothing of ours gates\n * them; and a container beside a direct Google tag counts everything twice.\n */\nexport function aiTrackingWarnings(host: Record<string, unknown>): string[] {\n const warnings: string[] = []\n const has = (path: string) => {\n const value = getPath(host, path)\n return typeof value === 'string' && value !== ''\n }\n const any = AI_TRACKING_TAGS.some((tag) => has(tag.path))\n if (any && getPath(host, 'consent.disabled') === true) {\n warnings.push('This site’s cookie banner is turned off because it uses its own consent tool, so that tool decides when these load.')\n }\n if (has('analytics.gtmContainerId') && (has('analytics.gaMeasurementId') || has('analytics.adTags.google-ads'))) {\n warnings.push('Do not also put an Analytics or Google Ads tag in the Tag Manager container: that loads the same measurement twice and counts every visit and conversion twice.')\n }\n return warnings\n}\n\nexport type AiTrackingWrite =\n | { ok: true; done: string[]; changed: string[]; warnings: string[]; outboxId: string | null }\n | { ok: false; error: string }\n\n/**\n * The tags written, in one transaction that reads the host as it is stored\n * NOW: the member still holds a write role, and only the values that differ\n * from what is stored are written, with a site-wide publish outbox entry\n * beside them so the live site's cached pages are dropped.\n */\nexport async function writeAiSiteTracking(\n firestore: Firestore,\n input: { hostId: string; uid: string; request: AiTrackingRequest; now: Date },\n): Promise<AiTrackingWrite> {\n const hostRef = firestore.collection('hosts').doc(input.hostId)\n return firestore.runTransaction(async (tx): Promise<AiTrackingWrite> => {\n const host = await tx.get(hostRef)\n if (!host.exists) return { ok: false, error: 'Unknown site' }\n if (!holdsSiteKey(host, input.uid, 'site.edit')) return { ok: false, error: AI_TRACKING_ROLE_COPY }\n const data = (host.data() ?? {}) as Record<string, unknown>\n const changes = aiTrackingChanges(input.request)\n const changed = changes.filter((change) => {\n const stored = getPath(data, change.path)\n return change.value === null ? stored != null && stored !== '' : stored !== change.value\n })\n // What the site carries once this lands, for the warnings.\n // Only the two maps the warnings read; both are plain strings and flags.\n const after: Record<string, unknown> = JSON.parse(JSON.stringify({ analytics: data['analytics'] ?? {}, consent: data['consent'] ?? {} }))\n for (const change of changed) {\n const keys = change.path.split('.')\n const leaf = keys.pop() as string\n let node = after as Record<string, unknown>\n for (const key of keys) {\n if (!node[key] || typeof node[key] !== 'object') node[key] = {}\n node = node[key] as Record<string, unknown>\n }\n if (change.value === null) delete node[leaf]\n else node[leaf] = change.value\n }\n const done = changes.map((change) => change.said)\n const warnings = aiTrackingWarnings(after)\n if (!changed.length) return { ok: true, done, changed: [], warnings, outboxId: null }\n const updates: Record<string, unknown> = { updatedAt: input.now }\n for (const change of changed) updates[change.path] = change.value === null ? FieldValue.delete() : change.value\n tx.update(hostRef, updates)\n // The tags are in every page's head, so every cached page is stale.\n const outboxRef = firestore.collection('publishOutbox').doc(createResourceUid())\n tx.set(outboxRef, {\n hostId: input.hostId,\n paths: [SCREEN_ROOT_PATH],\n entireHost: true,\n createdAt: FieldValue.serverTimestamp(),\n attempts: 0,\n })\n return { ok: true, done, changed: changed.map((change) => change.path), warnings, outboxId: outboxRef.id }\n })\n}\n\n// ── The step ──────────────────────────────────────────────────────────────\n\n/** Why this member may not change this site's tracking now, or `null`. */\nexport async function aiTrackingAccessRefusal(\n input: { host: FirebaseFirestore.DocumentSnapshot; uid: string; org: object | null },\n deps: { lockdown?: typeof getLockdownVerdict } = {},\n): Promise<string | null> {\n if (!input.host.exists) return 'Unknown site'\n if (!holdsSiteKey(input.host, input.uid, 'site.edit')) return AI_TRACKING_ROLE_COPY\n const locked = await (deps.lockdown ?? getLockdownVerdict)({\n staff: false,\n uid: input.uid,\n org: input.org as never,\n host: input.host.data() as never,\n intent: 'write',\n })\n return locked ? AI_TRACKING_LOCKED_COPY : null\n}\n\n/** The output's note: what changed, that it is live, how consent gates it, and what was not done. */\nexport function aiTrackingOutputNote(done: readonly string[], warnings: readonly string[], notDone: readonly string[], changedAny: boolean): string {\n const did = done.length\n ? `${done.join(' ')} Live now, in ${AI_TRACKING_WHERE}.${changedAny ? '' : ' It already was, so nothing was written.'}`\n : 'Nothing needed changing.'\n // The card's own promise, true of a tag whoever saved it.\n const consent = done.length ? ' Tags load only where a visitor’s cookie consent allows them.' : ''\n const warn = warnings.length ? ` ${warnings.join(' ')}` : ''\n const not = notDone.length ? ` Not done: ${notDone.join(' ')}` : ''\n return `${did}${consent}${warn}${not}`.slice(0, 1_000)\n}\n\nexport interface AiJobTrackingStepDeps {\n write?: typeof writeAiSiteTracking\n lockdown?: typeof getLockdownVerdict\n dropCache?: typeof dropPluginSiteCache\n}\n\nexport function createAiJobTrackingStep(deps: AiJobTrackingStepDeps = {}): AiJobStepRunner {\n const write = deps.write ?? writeAiSiteTracking\n const dropCache = deps.dropCache ?? dropPluginSiteCache\n return async ({ job, now, firestore, org, modelFor }): Promise<AiJobStepOutcome> => {\n // No model is asked: the outcome records the edit step's at no cost.\n const model = modelFor?.('job.edit') ?? aiJobEditModel()\n const unspent = (failure: string): AiJobStepOutcome => ({ ...aiUnspentOutcome(model), failure })\n const hostId = job.hostId\n if (!hostId) return unspent(AI_TRACKING_NO_SITE_COPY)\n const read = aiTrackingRequestOf(job.inputs)\n if (aiTrackingRequestEmpty(read.request)) return unspent([AI_TRACKING_NOTHING_COPY, ...read.problems].join(' '))\n const orgData = (org ?? ((await firestore.collection('orgs').doc(job.orgId).get()).data() ?? null)) as Record<string, unknown> | null\n const host = await firestore.collection('hosts').doc(hostId).get()\n const refused = await aiTrackingAccessRefusal({ host, uid: job.createdBy, org: orgData }, { lockdown: deps.lockdown })\n if (refused) return aiUnspentOutcome(model, { review: aiLimitReview(refused) })\n const written = await write(firestore, { hostId, uid: job.createdBy, request: read.request, now })\n if (written.ok === false) return unspent(written.error)\n if (written.outboxId) {\n // The live site's cached pages go now; the outbox entry stays for the\n // drain unless the drop certainly happened.\n const dropped = await dropCache({ hostIds: [hostId], reason: 'AI build changed site tracking' }).catch(() => ({ complete: false }))\n if (dropped.complete) await firestore.collection('publishOutbox').doc(written.outboxId).delete().catch(() => undefined)\n }\n const output: AiJobOutput = {\n resource: 'tracking',\n id: AI_TRACKING_OUTPUT_ID,\n hostId,\n hostSubdomain: (typeof host.get('subdomain') === 'string' && host.get('subdomain')) || null,\n label: 'Site tracking',\n note: aiTrackingOutputNote(written.done, written.warnings, read.problems, written.changed.length > 0),\n }\n return aiUnspentOutcome(model, { outputs: [output] })\n }\n}\n\nexport const runAiJobTrackingStep = createAiJobTrackingStep()\n\n// ── Admission ─────────────────────────────────────────────────────────────\n\nexport interface AiTrackingJobAdmissionDeps {\n ownerOf?: typeof resolveOrgIdForHost\n lockdown?: typeof getLockdownVerdict\n}\n\n/**\n * A tracking change is admitted on a site of the job's own org, for a member\n * who may write its settings now, where the request names at least one tag\n * this can set or remove. Refused in those words before anything is written.\n */\nexport function createAiTrackingJobAdmission(deps: AiTrackingJobAdmissionDeps = {}): AiJobAdmission {\n const ownerOf = deps.ownerOf ?? resolveOrgIdForHost\n return async (context): Promise<AiJobAdmissionRefusal | null> => {\n if (!context.hostId) return { status: 400, error: AI_TRACKING_NO_SITE_COPY }\n const owner = await ownerOf(context.hostId)\n if (!owner || owner !== context.orgId) return { status: 404, error: 'Unknown site' }\n const host = await context.firestore.collection('hosts').doc(context.hostId).get()\n const refused = await aiTrackingAccessRefusal({ host, uid: context.uid ?? '', org: context.org }, { lockdown: deps.lockdown })\n if (refused) return { status: 403, error: refused }\n const read = aiTrackingRequestOf(context.inputs as Record<string, unknown>)\n return aiTrackingRequestEmpty(read.request) ? { status: 400, error: [AI_TRACKING_NOTHING_COPY, ...read.problems].join(' ') } : null\n }\n}\n\nexport const aiTrackingJobAdmission = createAiTrackingJobAdmission()\n\n/** Registers the tracking step and its door (AGL-3751). */\nexport function registerAiTrackingJob(): void {\n registerAiJobStep(AI_TRACKING_KIND, runAiJobTrackingStep, { minimumMs: AI_JOB_TRACKING_STEP_MINIMUM_MS })\n registerAiJobAdmission(AI_TRACKING_KIND, aiTrackingJobAdmission)\n}\n"],"names":["createResourceUid","holdsSiteKey","orgPermissionLabel","SCREEN_ROOT_PATH","GA_MEASUREMENT_ID_PATTERN","GOOGLE_ADS_ID_PATTERN","GTM_CONTAINER_ID_PATTERN","LINKEDIN_PARTNER_ID_PATTERN","META_PIXEL_ID_PATTERN","PINTEREST_TAG_ID_PATTERN","TIKTOK_PIXEL_ID_PATTERN","dropPluginSiteCache","getLockdownVerdict","resolveOrgIdForHost","FieldValue","registerAiJobAdmission","aiJobEditModel","aiLimitReview","aiUnspentOutcome","registerAiJobStep","AI_TRACKING_KIND","AI_JOB_TRACKING_STEP_MINIMUM_MS","AI_TRACKING_NO_SITE_COPY","AI_TRACKING_ROLE_COPY","AI_TRACKING_LOCKED_COPY","AI_TRACKING_NOTHING_COPY","AI_TRACKING_WHERE","AI_TRACKING_OUTPUT_ID","AI_TRACKING_TAGS","arg","path","label","pattern","shape","field","ask","named","AI_TRACKING_WHICH_COPY","TAG_ARGS","map","tag","ID_RULE","AI_TRACKING_ARGS_SCHEMA","type","properties","googleAnalytics","description","maxLength","googleTagManager","metaPixel","googleAds","linkedinInsight","tiktokPixel","pinterestTag","remove","items","enum","maxItems","length","additionalProperties","text","value","trim","aiTrackingRequestOf","inputs","set","problems","removing","Set","Array","isArray","one","String","has","push","capitalized","test","slice","request","charAt","toUpperCase","aiTrackingMissingInput","item","read","args","aiTrackingRequestEmpty","aiTrackingTagsAsked","asks","join","tried","filter","naming","words","byName","name","why","aiTrackingInputFields","Object","keys","getPath","data","split","reduce","at","key","undefined","aiTrackingChanges","changes","said","includes","aiTrackingWarnings","host","warnings","any","some","writeAiSiteTracking","firestore","input","hostRef","collection","doc","hostId","runTransaction","tx","get","exists","ok","error","uid","changed","change","stored","after","JSON","parse","stringify","analytics","consent","leaf","pop","node","done","outboxId","updates","updatedAt","now","delete","update","outboxRef","paths","entireHost","createdAt","serverTimestamp","attempts","id","aiTrackingAccessRefusal","deps","locked","lockdown","staff","org","intent","aiTrackingOutputNote","notDone","changedAny","did","warn","not","createAiJobTrackingStep","write","dropCache","job","modelFor","model","unspent","failure","orgData","orgId","refused","createdBy","review","written","dropped","hostIds","reason","catch","complete","output","resource","hostSubdomain","note","outputs","runAiJobTrackingStep","createAiTrackingJobAdmission","ownerOf","context","status","owner","aiTrackingJobAdmission","registerAiTrackingJob","minimumMs"],"mappings":"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"}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { type SweepAiJobsResult } from './ai-jobs';
|
|
18
18
|
import { type AiEmptySiteSweepResult } from './ai-empty-site-sweep';
|
|
19
|
+
import { type AiPlanReminderSweepResult } from './ai-jobs-notify';
|
|
19
20
|
/**
|
|
20
21
|
* Resume AI generation jobs on the AI jobs beat (AGL-2904, AGL-3026).
|
|
21
22
|
*
|
|
@@ -71,16 +72,19 @@ export type AiJobsBeatResult = {
|
|
|
71
72
|
} | ({
|
|
72
73
|
held: false;
|
|
73
74
|
emptySites: AiEmptySiteSweepResult | null;
|
|
75
|
+
planReminders: AiPlanReminderSweepResult | null;
|
|
74
76
|
} & SweepAiJobsResult);
|
|
75
77
|
/**
|
|
76
78
|
* One beat: the switch, then the empty sites a guided start left that are
|
|
77
|
-
* due for removal (AGL-3936), then one
|
|
78
|
-
*
|
|
79
|
-
*
|
|
79
|
+
* due for removal (AGL-3936), then the one reminder a plan waiting a day
|
|
80
|
+
* for its confirmation is owed (AGL-4076), then one sweep under its own
|
|
81
|
+
* lease owner. The owner is fresh per beat and fixed for the whole sweep, so
|
|
82
|
+
* two beats overlapping inside a lease never read each other's lease as
|
|
83
|
+
* their own.
|
|
80
84
|
*
|
|
81
|
-
* The removals come first, a few a beat inside
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
+
* The removals and the reminders come first, each a few a beat inside its
|
|
86
|
+
* own budget, and whatever they took comes off the sweep's budget, so the
|
|
87
|
+
* route's arithmetic inside `maxDuration` holds. A removal or a reminder that
|
|
88
|
+
* fails never holds up a job's step.
|
|
85
89
|
*/
|
|
86
90
|
export declare function runAiJobsBeat(): Promise<AiJobsBeatResult>;
|
|
@@ -18,6 +18,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
18
18
|
import { featureLockdownRefusal, firebaseAdmin } from "@aglyn/tenant-data-admin";
|
|
19
19
|
import { AI_JOB_SWEEP_BUDGET_MS, sweepAiJobs } from "./ai-jobs.js";
|
|
20
20
|
import { sweepAiEmptySites } from "./ai-empty-site-sweep.js";
|
|
21
|
+
import { sweepAiPlanReminders } from "./ai-jobs-notify.js";
|
|
21
22
|
/**
|
|
22
23
|
* Resume AI generation jobs on the AI jobs beat (AGL-2904, AGL-3026).
|
|
23
24
|
*
|
|
@@ -66,14 +67,16 @@ import { sweepAiEmptySites } from "./ai-empty-site-sweep.js";
|
|
|
66
67
|
*/ export const AI_JOBS_BEAT_PATH = 'admin/ai-jobs-beat';
|
|
67
68
|
/**
|
|
68
69
|
* One beat: the switch, then the empty sites a guided start left that are
|
|
69
|
-
* due for removal (AGL-3936), then one
|
|
70
|
-
*
|
|
71
|
-
*
|
|
70
|
+
* due for removal (AGL-3936), then the one reminder a plan waiting a day
|
|
71
|
+
* for its confirmation is owed (AGL-4076), then one sweep under its own
|
|
72
|
+
* lease owner. The owner is fresh per beat and fixed for the whole sweep, so
|
|
73
|
+
* two beats overlapping inside a lease never read each other's lease as
|
|
74
|
+
* their own.
|
|
72
75
|
*
|
|
73
|
-
* The removals come first, a few a beat inside
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
76
|
+
* The removals and the reminders come first, each a few a beat inside its
|
|
77
|
+
* own budget, and whatever they took comes off the sweep's budget, so the
|
|
78
|
+
* route's arithmetic inside `maxDuration` holds. A removal or a reminder that
|
|
79
|
+
* fails never holds up a job's step.
|
|
77
80
|
*/ export async function runAiJobsBeat() {
|
|
78
81
|
const locked = await featureLockdownRefusal({
|
|
79
82
|
feature: 'ai-generate'
|
|
@@ -93,6 +96,12 @@ import { sweepAiEmptySites } from "./ai-empty-site-sweep.js";
|
|
|
93
96
|
return null;
|
|
94
97
|
});
|
|
95
98
|
if (emptySites == null ? void 0 : emptySites.removed) console.info(`ai jobs: ${emptySites.removed} empty site(s) removed`);
|
|
99
|
+
const planReminders = await sweepAiPlanReminders({
|
|
100
|
+
firestore
|
|
101
|
+
}).catch((error)=>{
|
|
102
|
+
console.error('ai jobs beat: the plan reminders failed', error);
|
|
103
|
+
return null;
|
|
104
|
+
});
|
|
96
105
|
const result = await sweepAiJobs({
|
|
97
106
|
firestore,
|
|
98
107
|
owner: `beat:${randomUUID()}`,
|
|
@@ -103,7 +112,8 @@ import { sweepAiEmptySites } from "./ai-empty-site-sweep.js";
|
|
|
103
112
|
}
|
|
104
113
|
return _extends({
|
|
105
114
|
held: false,
|
|
106
|
-
emptySites
|
|
115
|
+
emptySites,
|
|
116
|
+
planReminders
|
|
107
117
|
}, result);
|
|
108
118
|
}
|
|
109
119
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-jobs-beat.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 { randomUUID } from 'crypto'\nimport { featureLockdownRefusal, firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { AI_JOB_SWEEP_BUDGET_MS, sweepAiJobs, type SweepAiJobsResult } from './ai-jobs'\nimport { sweepAiEmptySites, type AiEmptySiteSweepResult } from './ai-empty-site-sweep'\n\n/**\n * Resume AI generation jobs on the AI jobs beat (AGL-2904, AGL-3026).\n *\n * A job's first step runs inline in the console route when it can; every\n * step after it, every step too long for an inline door, and any step a\n * budget cut short, runs here. The sweep bounds itself at\n * `AI_JOB_SWEEP_BUDGET_MS` of wall clock and hands the rest to the next beat\n * — the ordering of the queue is the cursor, so nothing is stored between\n * beats and nothing can be skipped.\n *\n * ## Why the console runs it\n *\n * A step calls the AI provider, and the provider's key is held by the\n * console alone: the tenant app serves every published site, and a\n * credential it holds is one request-handling bug away from a visitor. So\n * the beat is a console route (`server/ai-jobs-beat-route.ts`, registered\n * by the console surface), driven every minute by its own Cloud Scheduler\n * job in `cloud/functions` on the console's cron secret — the reason the\n * sending-domain sweep is a console route and not a job on the tenant's\n * platform beat. A beat must resolve to the minute: a step handed back by\n * a budget, or a page pass waiting on the next, should resume on the next\n * minute, not the next quarter hour.\n *\n * ## The kill switch\n *\n * `ai-generate` on the staff lockdown page is the lever for a provider\n * incident or a spend runaway on the generative doors, and this beat is the\n * one generative caller with no request to refuse. So it asks the same\n * feature verdict a door would — the platform scope and then the feature\n * document — and does nothing while either is locked. A job it leaves\n * queued stays queued: its lease is not taken, so nothing expires and\n * nothing is lost, and the first beat after the lock lifts picks it up.\n *\n * The same switch paused for ONE workspace (AGL-3037) holds that workspace's\n * jobs and no other's: `runAiJobStep` asks the verdict with the job's org\n * before it claims a step, and leaves a paused workspace's job queued.\n */\n\n/** The beat's row in `SCHEDULED_JOBS`, and the mark it leaves for `/api/health/crons`. */\nexport const AI_JOBS_BEAT_CRON_ID = 'ai-jobs-beat'\n\n/**\n * The console path the beat is registered at: the one path this plugin\n * registers outside its own prefixes. `/api/admin/` is where the console's\n * scheduled sweeps live, and the console's edge lets a request past its bot\n * challenge there when it carries the cron secret's header — a route\n * anywhere else would need a firewall rule of its own before a scheduler\n * could reach it. The console app serves this exact path from a named route\n * with the sweep's function time, so the plugin dispatcher never serves it.\n */\nexport const AI_JOBS_BEAT_PATH = 'admin/ai-jobs-beat'\n\n/** What one beat did: nothing while `ai-generate` is locked, else the sweep's account. */\nexport type AiJobsBeatResult =\n | { held: true }\n | ({
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-jobs-beat.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 { randomUUID } from 'crypto'\nimport { featureLockdownRefusal, firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { AI_JOB_SWEEP_BUDGET_MS, sweepAiJobs, type SweepAiJobsResult } from './ai-jobs'\nimport { sweepAiEmptySites, type AiEmptySiteSweepResult } from './ai-empty-site-sweep'\nimport { sweepAiPlanReminders, type AiPlanReminderSweepResult } from './ai-jobs-notify'\n\n/**\n * Resume AI generation jobs on the AI jobs beat (AGL-2904, AGL-3026).\n *\n * A job's first step runs inline in the console route when it can; every\n * step after it, every step too long for an inline door, and any step a\n * budget cut short, runs here. The sweep bounds itself at\n * `AI_JOB_SWEEP_BUDGET_MS` of wall clock and hands the rest to the next beat\n * — the ordering of the queue is the cursor, so nothing is stored between\n * beats and nothing can be skipped.\n *\n * ## Why the console runs it\n *\n * A step calls the AI provider, and the provider's key is held by the\n * console alone: the tenant app serves every published site, and a\n * credential it holds is one request-handling bug away from a visitor. So\n * the beat is a console route (`server/ai-jobs-beat-route.ts`, registered\n * by the console surface), driven every minute by its own Cloud Scheduler\n * job in `cloud/functions` on the console's cron secret — the reason the\n * sending-domain sweep is a console route and not a job on the tenant's\n * platform beat. A beat must resolve to the minute: a step handed back by\n * a budget, or a page pass waiting on the next, should resume on the next\n * minute, not the next quarter hour.\n *\n * ## The kill switch\n *\n * `ai-generate` on the staff lockdown page is the lever for a provider\n * incident or a spend runaway on the generative doors, and this beat is the\n * one generative caller with no request to refuse. So it asks the same\n * feature verdict a door would — the platform scope and then the feature\n * document — and does nothing while either is locked. A job it leaves\n * queued stays queued: its lease is not taken, so nothing expires and\n * nothing is lost, and the first beat after the lock lifts picks it up.\n *\n * The same switch paused for ONE workspace (AGL-3037) holds that workspace's\n * jobs and no other's: `runAiJobStep` asks the verdict with the job's org\n * before it claims a step, and leaves a paused workspace's job queued.\n */\n\n/** The beat's row in `SCHEDULED_JOBS`, and the mark it leaves for `/api/health/crons`. */\nexport const AI_JOBS_BEAT_CRON_ID = 'ai-jobs-beat'\n\n/**\n * The console path the beat is registered at: the one path this plugin\n * registers outside its own prefixes. `/api/admin/` is where the console's\n * scheduled sweeps live, and the console's edge lets a request past its bot\n * challenge there when it carries the cron secret's header — a route\n * anywhere else would need a firewall rule of its own before a scheduler\n * could reach it. The console app serves this exact path from a named route\n * with the sweep's function time, so the plugin dispatcher never serves it.\n */\nexport const AI_JOBS_BEAT_PATH = 'admin/ai-jobs-beat'\n\n/** What one beat did: nothing while `ai-generate` is locked, else the sweep's account. */\nexport type AiJobsBeatResult =\n | { held: true }\n | ({\n held: false\n emptySites: AiEmptySiteSweepResult | null\n planReminders: AiPlanReminderSweepResult | null\n } & SweepAiJobsResult)\n\n/**\n * One beat: the switch, then the empty sites a guided start left that are\n * due for removal (AGL-3936), then the one reminder a plan waiting a day\n * for its confirmation is owed (AGL-4076), then one sweep under its own\n * lease owner. The owner is fresh per beat and fixed for the whole sweep, so\n * two beats overlapping inside a lease never read each other's lease as\n * their own.\n *\n * The removals and the reminders come first, each a few a beat inside its\n * own budget, and whatever they took comes off the sweep's budget, so the\n * route's arithmetic inside `maxDuration` holds. A removal or a reminder that\n * fails never holds up a job's step.\n */\nexport async function runAiJobsBeat(): Promise<AiJobsBeatResult> {\n const locked = await featureLockdownRefusal({ feature: 'ai-generate' })\n if (locked) {\n console.warn('ai jobs beat held: ai-generate is locked')\n return { held: true }\n }\n const firestore = firebaseAdmin.app().firestore()\n const startedAt = Date.now()\n const emptySites = await sweepAiEmptySites({ firestore }).catch((error: unknown) => {\n console.error('ai jobs beat: the empty site sweep failed', error)\n return null\n })\n if (emptySites?.removed) console.info(`ai jobs: ${emptySites.removed} empty site(s) removed`)\n const planReminders = await sweepAiPlanReminders({ firestore }).catch((error: unknown) => {\n console.error('ai jobs beat: the plan reminders failed', error)\n return null\n })\n const result = await sweepAiJobs({\n firestore,\n owner: `beat:${randomUUID()}`,\n budgetMs: Math.max(0, AI_JOB_SWEEP_BUDGET_MS - (Date.now() - startedAt)),\n })\n if (result.due) {\n console.info(\n `ai jobs: ${result.ran} step(s) run, ${result.skipped} skipped, ` +\n `${result.paused} held by a workspace's AI pause, ` +\n `${result.remaining} left for the next beat` +\n (result.budgetExhausted ? ' (budget exhausted)' : ''),\n )\n }\n return { held: false, emptySites, planReminders, ...result }\n}\n"],"names":["randomUUID","featureLockdownRefusal","firebaseAdmin","AI_JOB_SWEEP_BUDGET_MS","sweepAiJobs","sweepAiEmptySites","sweepAiPlanReminders","AI_JOBS_BEAT_CRON_ID","AI_JOBS_BEAT_PATH","runAiJobsBeat","locked","feature","console","warn","held","firestore","app","startedAt","Date","now","emptySites","catch","error","removed","info","planReminders","result","owner","budgetMs","Math","max","due","ran","skipped","paused","remaining","budgetExhausted"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,UAAU,QAAQ,SAAQ;AACnC,SAASC,sBAAsB,EAAEC,aAAa,QAAQ,2BAA0B;AAChF,SAASC,sBAAsB,EAAEC,WAAW,QAAgC,eAAW;AACvF,SAASC,iBAAiB,QAAqC,2BAAuB;AACtF,SAASC,oBAAoB,QAAwC,sBAAkB;AAEvF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoCC,GAED,wFAAwF,GACxF,OAAO,MAAMC,uBAAuB,eAAc;AAElD;;;;;;;;CAQC,GACD,OAAO,MAAMC,oBAAoB,qBAAoB;AAWrD;;;;;;;;;;;;CAYC,GACD,OAAO,eAAeC;IACpB,MAAMC,SAAS,MAAMT,uBAAuB;QAAEU,SAAS;IAAc;IACrE,IAAID,QAAQ;QACVE,QAAQC,IAAI,CAAC;QACb,OAAO;YAAEC,MAAM;QAAK;IACtB;IACA,MAAMC,YAAYb,cAAcc,GAAG,GAAGD,SAAS;IAC/C,MAAME,YAAYC,KAAKC,GAAG;IAC1B,MAAMC,aAAa,MAAMf,kBAAkB;QAAEU;IAAU,GAAGM,KAAK,CAAC,CAACC;QAC/DV,QAAQU,KAAK,CAAC,6CAA6CA;QAC3D,OAAO;IACT;IACA,IAAIF,8BAAAA,WAAYG,OAAO,EAAEX,QAAQY,IAAI,CAAC,CAAC,SAAS,EAAEJ,WAAWG,OAAO,CAAC,sBAAsB,CAAC;IAC5F,MAAME,gBAAgB,MAAMnB,qBAAqB;QAAES;IAAU,GAAGM,KAAK,CAAC,CAACC;QACrEV,QAAQU,KAAK,CAAC,2CAA2CA;QACzD,OAAO;IACT;IACA,MAAMI,SAAS,MAAMtB,YAAY;QAC/BW;QACAY,OAAO,CAAC,KAAK,EAAE3B,cAAc;QAC7B4B,UAAUC,KAAKC,GAAG,CAAC,GAAG3B,yBAA0Be,CAAAA,KAAKC,GAAG,KAAKF,SAAQ;IACvE;IACA,IAAIS,OAAOK,GAAG,EAAE;QACdnB,QAAQY,IAAI,CACV,CAAC,SAAS,EAAEE,OAAOM,GAAG,CAAC,cAAc,EAAEN,OAAOO,OAAO,CAAC,UAAU,CAAC,GAC/D,GAAGP,OAAOQ,MAAM,CAAC,iCAAiC,CAAC,GACnD,GAAGR,OAAOS,SAAS,CAAC,uBAAuB,CAAC,GAC3CT,CAAAA,OAAOU,eAAe,GAAG,wBAAwB,EAAC;IAEzD;IACA,OAAO;QAAEtB,MAAM;QAAOM;QAAYK;OAAkBC;AACtD"}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { notifyUsers } from '@aglyn/tenant-data-admin/server/notifications';
|
|
18
18
|
import { raiseOperatorAlert } from '@aglyn/tenant-data-admin/server/operator-alerts';
|
|
19
19
|
import { type AiJobNoticeSite } from '../model/ai-job-notice';
|
|
20
|
+
import type { AiJob } from '../model/ai-jobs.types';
|
|
20
21
|
import { type AiJobTransitionListener } from './ai-jobs';
|
|
21
22
|
/**
|
|
22
23
|
* The person who started an AI job is told when it needs them, when it
|
|
@@ -40,7 +41,71 @@ export declare function aiJobTransitionNotifier(notify?: typeof notifyUsers, rea
|
|
|
40
41
|
* its address, and where it is live — its custom domain once that serves,
|
|
41
42
|
* else its platform subdomain. `null` for a site that is gone.
|
|
42
43
|
*/
|
|
43
|
-
export declare function readJobSite(hostId: string): Promise<AiJobNoticeSite | null>;
|
|
44
|
+
export declare function readJobSite(hostId: string, firestore?: FirebaseFirestore.Firestore): Promise<AiJobNoticeSite | null>;
|
|
45
|
+
/**
|
|
46
|
+
* How long a plan waits for its confirmation before its one reminder: a day
|
|
47
|
+
* after the job last changed, which for a plan nobody touched is the moment
|
|
48
|
+
* it was ready.
|
|
49
|
+
*/
|
|
50
|
+
export declare const AI_PLAN_REMINDER_AFTER_MS: number;
|
|
51
|
+
/**
|
|
52
|
+
* How long after it is due a reminder may still go out. A plan that has
|
|
53
|
+
* waited longer than a day and this is never reminded: the person was told
|
|
54
|
+
* when it was ready, and a reminder about a plan left for a week reads as
|
|
55
|
+
* noise, so the plans already waiting when this shipped are not mailed all
|
|
56
|
+
* at once. It is also the beats a lockdown of `ai-generate` may hold the
|
|
57
|
+
* reminders for before one is missed rather than sent late.
|
|
58
|
+
*/
|
|
59
|
+
export declare const AI_PLAN_REMINDER_WINDOW_MS: number;
|
|
60
|
+
/** The waiting jobs one beat reads, oldest first. */
|
|
61
|
+
export declare const AI_PLAN_REMINDER_SCAN = 25;
|
|
62
|
+
/** The reminders one beat sends; the rest are the next minute's. */
|
|
63
|
+
export declare const AI_PLAN_REMINDER_MAX = 5;
|
|
64
|
+
/** Wall clock one beat gives the reminders before it runs any job's step. */
|
|
65
|
+
export declare const AI_PLAN_REMINDER_BUDGET_MS = 10000;
|
|
66
|
+
/**
|
|
67
|
+
* Whether a job is owed its plan reminder now (AGL-4076): its plan waits for
|
|
68
|
+
* the person who started it (`needs_review`, reason `plan`), has waited
|
|
69
|
+
* between a day and a day and {@link AI_PLAN_REMINDER_WINDOW_MS}, and the job
|
|
70
|
+
* was never reminded. Never a plan that is confirmed for its person (a guided
|
|
71
|
+
* start's), a job someone asked to cancel, or one whose site was removed: the
|
|
72
|
+
* same jobs the first notice skips, and the ones that moved on.
|
|
73
|
+
*/
|
|
74
|
+
export declare function aiPlanReminderDue(job: Pick<AiJob, 'kind' | 'inputs' | 'status' | 'review' | 'createdBy' | 'updatedAt' | 'remindedAt' | 'cancelRequested' | 'siteRemoved'>, nowMs: number): boolean;
|
|
75
|
+
export interface AiPlanReminderSweepResult {
|
|
76
|
+
/** Jobs read that were waiting in the window, reminded or not. */
|
|
77
|
+
scanned: number;
|
|
78
|
+
/** Reminders sent. */
|
|
79
|
+
reminded: number;
|
|
80
|
+
}
|
|
81
|
+
export interface SweepAiPlanRemindersOptions {
|
|
82
|
+
firestore: FirebaseFirestore.Firestore;
|
|
83
|
+
now?: () => number;
|
|
84
|
+
budgetMs?: number;
|
|
85
|
+
notify?: typeof notifyUsers;
|
|
86
|
+
/** The job's site; `readJobSite` unless a spec replaces it. */
|
|
87
|
+
readSite?: (hostId: string) => Promise<AiJobNoticeSite | null>;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* One beat's plan reminders (AGL-4076). A plan that waits for its
|
|
91
|
+
* confirmation is told about once when it is ready (`aiJobTransitionNotifier`)
|
|
92
|
+
* and once more a day later, here, and never again: 13 plans in the 10/10 to
|
|
93
|
+
* 10/28 window waited 4 to 428 hours after their single notice.
|
|
94
|
+
*
|
|
95
|
+
* The waiting jobs are read by the collection-group index the sweep already
|
|
96
|
+
* uses, on (status, updatedAt), inside the window a reminder may go out in,
|
|
97
|
+
* oldest first; every condition is then read again on the job, in the
|
|
98
|
+
* transaction that marks `remindedAt`, so two beats never both send one and a
|
|
99
|
+
* job confirmed, cancelled or deleted in the meantime gets none. The mark is
|
|
100
|
+
* written before the notice: a beat that dies between the two loses a
|
|
101
|
+
* reminder rather than sending a second. `notifyUsers` holds the notice to the
|
|
102
|
+
* person's own settings for AI job needs you, by channel, workspace and site,
|
|
103
|
+
* as it holds the first one; a job whose site is gone is not reminded.
|
|
104
|
+
*
|
|
105
|
+
* The mark leaves `updatedAt` alone, so the job's age in AI jobs stays the
|
|
106
|
+
* age of its plan.
|
|
107
|
+
*/
|
|
108
|
+
export declare function sweepAiPlanReminders(options: SweepAiPlanRemindersOptions): Promise<AiPlanReminderSweepResult>;
|
|
44
109
|
/**
|
|
45
110
|
* Staff are told when a job fails on our side (`ai.jobFailed`), and when a
|
|
46
111
|
* build finishes `done` with parts that failed on our side
|