@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
|
@@ -20,7 +20,7 @@ import { raiseOperatorAlert } from "@aglyn/tenant-data-admin/server/operator-ale
|
|
|
20
20
|
import { AI_BUILD_PARTLY_FAILED, AI_JOB_FAILED } from "../operator-alerts.js";
|
|
21
21
|
import { liveCustomDomain } from "@aglyn/aglyn/app-utils/host-naming";
|
|
22
22
|
import { TENANT_APEX } from "@aglyn/aglyn/app-utils/tenant-apex";
|
|
23
|
-
import { aiJobNotice } from "../model/ai-job-notice.js";
|
|
23
|
+
import { aiJobNotice, aiJobPlanReminderNotice } from "../model/ai-job-notice.js";
|
|
24
24
|
import { aiBusinessProfilePrefiller } from "./ai-business-profile-prefill.js";
|
|
25
25
|
import { aiJobAutoConfirms } from "./ai-job-auto-confirm.js";
|
|
26
26
|
import { registerAiJobTransitionListener } from "./ai-jobs.js";
|
|
@@ -78,9 +78,9 @@ import { registerAiJobTransitionListener } from "./ai-jobs.js";
|
|
|
78
78
|
* A job's site as its notice names it: the name its managers gave it, else
|
|
79
79
|
* its address, and where it is live — its custom domain once that serves,
|
|
80
80
|
* else its platform subdomain. `null` for a site that is gone.
|
|
81
|
-
*/ export async function readJobSite(hostId) {
|
|
81
|
+
*/ export async function readJobSite(hostId, firestore = firebaseAdmin.app().firestore()) {
|
|
82
82
|
var _host_data;
|
|
83
|
-
const host = await
|
|
83
|
+
const host = await firestore.collection('hosts').doc(hostId).get();
|
|
84
84
|
if (!host.exists) return null;
|
|
85
85
|
const data = (_host_data = host.data()) != null ? _host_data : {};
|
|
86
86
|
const text = (value)=>typeof value === 'string' ? value.trim() : '';
|
|
@@ -91,6 +91,139 @@ import { registerAiJobTransitionListener } from "./ai-jobs.js";
|
|
|
91
91
|
url: domain ? `https://${domain}` : subdomain ? `https://${subdomain}.${TENANT_APEX}` : null
|
|
92
92
|
};
|
|
93
93
|
}
|
|
94
|
+
// ── The one plan-ready reminder (AGL-4076) ───────────────────────────────
|
|
95
|
+
/**
|
|
96
|
+
* How long a plan waits for its confirmation before its one reminder: a day
|
|
97
|
+
* after the job last changed, which for a plan nobody touched is the moment
|
|
98
|
+
* it was ready.
|
|
99
|
+
*/ export const AI_PLAN_REMINDER_AFTER_MS = 24 * 60 * 60000;
|
|
100
|
+
/**
|
|
101
|
+
* How long after it is due a reminder may still go out. A plan that has
|
|
102
|
+
* waited longer than a day and this is never reminded: the person was told
|
|
103
|
+
* when it was ready, and a reminder about a plan left for a week reads as
|
|
104
|
+
* noise, so the plans already waiting when this shipped are not mailed all
|
|
105
|
+
* at once. It is also the beats a lockdown of `ai-generate` may hold the
|
|
106
|
+
* reminders for before one is missed rather than sent late.
|
|
107
|
+
*/ export const AI_PLAN_REMINDER_WINDOW_MS = 6 * 60 * 60000;
|
|
108
|
+
/** The waiting jobs one beat reads, oldest first. */ export const AI_PLAN_REMINDER_SCAN = 25;
|
|
109
|
+
/** The reminders one beat sends; the rest are the next minute's. */ export const AI_PLAN_REMINDER_MAX = 5;
|
|
110
|
+
/** Wall clock one beat gives the reminders before it runs any job's step. */ export const AI_PLAN_REMINDER_BUDGET_MS = 10000;
|
|
111
|
+
/** The org's jobs, as `AI_JOBS_COLLECTION` in `ai-jobs.ts` names them. */ const JOBS_COLLECTION = 'aiJobs';
|
|
112
|
+
function instantMs(value) {
|
|
113
|
+
if (value === null || value === undefined) return null;
|
|
114
|
+
if (value instanceof Date) return value.getTime();
|
|
115
|
+
if (typeof value === 'number') return Number.isFinite(value) ? value : null;
|
|
116
|
+
if (typeof value === 'string') {
|
|
117
|
+
const parsed = Date.parse(value);
|
|
118
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
119
|
+
}
|
|
120
|
+
return typeof value.toMillis === 'function' ? value.toMillis() : null;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Whether a job is owed its plan reminder now (AGL-4076): its plan waits for
|
|
124
|
+
* the person who started it (`needs_review`, reason `plan`), has waited
|
|
125
|
+
* between a day and a day and {@link AI_PLAN_REMINDER_WINDOW_MS}, and the job
|
|
126
|
+
* was never reminded. Never a plan that is confirmed for its person (a guided
|
|
127
|
+
* start's), a job someone asked to cancel, or one whose site was removed: the
|
|
128
|
+
* same jobs the first notice skips, and the ones that moved on.
|
|
129
|
+
*/ export function aiPlanReminderDue(job, nowMs) {
|
|
130
|
+
var _instantMs;
|
|
131
|
+
var _job_review;
|
|
132
|
+
if (job.status !== 'needs_review' || ((_job_review = job.review) == null ? void 0 : _job_review.reason) !== 'plan' || !job.createdBy) return false;
|
|
133
|
+
if (job.remindedAt || job.cancelRequested || job.siteRemoved || aiJobAutoConfirms(job)) return false;
|
|
134
|
+
const waitedMs = nowMs - ((_instantMs = instantMs(job.updatedAt)) != null ? _instantMs : nowMs);
|
|
135
|
+
return waitedMs >= AI_PLAN_REMINDER_AFTER_MS && waitedMs < AI_PLAN_REMINDER_AFTER_MS + AI_PLAN_REMINDER_WINDOW_MS;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* One beat's plan reminders (AGL-4076). A plan that waits for its
|
|
139
|
+
* confirmation is told about once when it is ready (`aiJobTransitionNotifier`)
|
|
140
|
+
* and once more a day later, here, and never again: 13 plans in the 10/10 to
|
|
141
|
+
* 10/28 window waited 4 to 428 hours after their single notice.
|
|
142
|
+
*
|
|
143
|
+
* The waiting jobs are read by the collection-group index the sweep already
|
|
144
|
+
* uses, on (status, updatedAt), inside the window a reminder may go out in,
|
|
145
|
+
* oldest first; every condition is then read again on the job, in the
|
|
146
|
+
* transaction that marks `remindedAt`, so two beats never both send one and a
|
|
147
|
+
* job confirmed, cancelled or deleted in the meantime gets none. The mark is
|
|
148
|
+
* written before the notice: a beat that dies between the two loses a
|
|
149
|
+
* reminder rather than sending a second. `notifyUsers` holds the notice to the
|
|
150
|
+
* person's own settings for AI job needs you, by channel, workspace and site,
|
|
151
|
+
* as it holds the first one; a job whose site is gone is not reminded.
|
|
152
|
+
*
|
|
153
|
+
* The mark leaves `updatedAt` alone, so the job's age in AI jobs stays the
|
|
154
|
+
* age of its plan.
|
|
155
|
+
*/ export async function sweepAiPlanReminders(options) {
|
|
156
|
+
var _options_now, _options_budgetMs, _options_notify, _options_readSite;
|
|
157
|
+
const now = (_options_now = options.now) != null ? _options_now : Date.now;
|
|
158
|
+
const startedAt = now();
|
|
159
|
+
const budgetMs = (_options_budgetMs = options.budgetMs) != null ? _options_budgetMs : AI_PLAN_REMINDER_BUDGET_MS;
|
|
160
|
+
const notify = (_options_notify = options.notify) != null ? _options_notify : notifyUsers;
|
|
161
|
+
const readSite = (_options_readSite = options.readSite) != null ? _options_readSite : (hostId)=>readJobSite(hostId, options.firestore);
|
|
162
|
+
const dueBefore = startedAt - AI_PLAN_REMINDER_AFTER_MS;
|
|
163
|
+
const waiting = await options.firestore.collectionGroup(JOBS_COLLECTION).where('status', '==', 'needs_review').where('updatedAt', '<=', new Date(dueBefore)).where('updatedAt', '>', new Date(dueBefore - AI_PLAN_REMINDER_WINDOW_MS)).orderBy('updatedAt', 'asc').limit(AI_PLAN_REMINDER_SCAN).get();
|
|
164
|
+
const result = {
|
|
165
|
+
scanned: waiting.size,
|
|
166
|
+
reminded: 0
|
|
167
|
+
};
|
|
168
|
+
for (const doc of waiting.docs){
|
|
169
|
+
if (result.reminded >= AI_PLAN_REMINDER_MAX || now() - startedAt >= budgetMs) break;
|
|
170
|
+
const listed = _extends({}, doc.data(), {
|
|
171
|
+
$id: doc.id
|
|
172
|
+
});
|
|
173
|
+
if (!aiPlanReminderDue(listed, now())) continue;
|
|
174
|
+
try {
|
|
175
|
+
// A job's site, read before the mark, so a job whose site is gone is never marked or told.
|
|
176
|
+
const site = listed.hostId ? await readSite(listed.hostId) : null;
|
|
177
|
+
if (listed.hostId && !site) continue;
|
|
178
|
+
const job = await options.firestore.runTransaction(async (tx)=>{
|
|
179
|
+
const fresh = await tx.get(doc.ref);
|
|
180
|
+
if (!fresh.exists) return null;
|
|
181
|
+
const current = _extends({}, fresh.data(), {
|
|
182
|
+
$id: fresh.id
|
|
183
|
+
});
|
|
184
|
+
if (!aiPlanReminderDue(current, now())) return null;
|
|
185
|
+
tx.update(doc.ref, {
|
|
186
|
+
remindedAt: new Date(now())
|
|
187
|
+
});
|
|
188
|
+
return current;
|
|
189
|
+
});
|
|
190
|
+
if (!job) continue;
|
|
191
|
+
const notice = aiJobPlanReminderNotice(job, site);
|
|
192
|
+
if (site) {
|
|
193
|
+
await notify([
|
|
194
|
+
job.createdBy
|
|
195
|
+
], notice, {
|
|
196
|
+
emailDetails: [
|
|
197
|
+
{
|
|
198
|
+
label: 'Site',
|
|
199
|
+
value: site.name
|
|
200
|
+
},
|
|
201
|
+
...site.url ? [
|
|
202
|
+
{
|
|
203
|
+
label: 'Address',
|
|
204
|
+
value: site.url
|
|
205
|
+
}
|
|
206
|
+
] : []
|
|
207
|
+
]
|
|
208
|
+
});
|
|
209
|
+
} else {
|
|
210
|
+
await notify([
|
|
211
|
+
job.createdBy
|
|
212
|
+
], notice);
|
|
213
|
+
}
|
|
214
|
+
result.reminded += 1;
|
|
215
|
+
} catch (error) {
|
|
216
|
+
// One job's fault is isolated; an unmarked job is read again next beat, inside its window.
|
|
217
|
+
console.error('ai plan reminder failed', {
|
|
218
|
+
orgId: listed.orgId,
|
|
219
|
+
jobId: listed.$id,
|
|
220
|
+
error
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (result.reminded) console.info(`ai jobs: ${result.reminded} plan reminder(s) sent`);
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
94
227
|
/** The longest runner error an alert quotes; the log has the rest. */ const ALERT_ERROR_MAX = 300;
|
|
95
228
|
/** At most this many failed parts are named in a partial-build alert. */ const ALERT_ITEMS_MAX = 5;
|
|
96
229
|
const capped = (text)=>text.length > ALERT_ERROR_MAX ? `${text.slice(0, ALERT_ERROR_MAX)}…` : text;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-jobs-notify.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 { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { notifyUsers } from '@aglyn/tenant-data-admin/server/notifications'\nimport { raiseOperatorAlert } from '@aglyn/tenant-data-admin/server/operator-alerts'\nimport { AI_BUILD_PARTLY_FAILED, AI_JOB_FAILED } from '../operator-alerts'\nimport { liveCustomDomain } from '@aglyn/aglyn/app-utils/host-naming'\nimport { TENANT_APEX } from '@aglyn/aglyn/app-utils/tenant-apex'\nimport { aiJobNotice, type AiJobNoticeSite } from '../model/ai-job-notice'\nimport { aiBusinessProfilePrefiller } from './ai-business-profile-prefill'\nimport { aiJobAutoConfirms } from './ai-job-auto-confirm'\nimport { registerAiJobTransitionListener, type AiJobTransitionListener } from './ai-jobs'\n\n/**\n * The person who started an AI job is told when it needs them, when it\n * finishes and when it stops (AGL-3593), through the console's notifications\n * — the bell, and the generic notification email for a person who switched\n * that channel on for the category. No other channel: there is no push.\n *\n * The machine calls this where the job's state changes, once per change\n * (`registerAiJobTransitionListener`): the request that ran a step inline,\n * or the beat that ran it with nobody watching — which is exactly when a\n * person needs telling. The recipient is the job's creator, whose job it is\n * and who alone confirms its plan; a teammate is not told about someone\n * else's draft.\n *\n * `notifyUsers` never throws, and honors the person's per-category,\n * per-workspace and per-site preferences.\n */\nexport function aiJobTransitionNotifier(\n notify: typeof notifyUsers = notifyUsers,\n readSite: (hostId: string) => Promise<AiJobNoticeSite | null> = readJobSite,\n): AiJobTransitionListener {\n return async ({ job, to }) => {\n if (!job.createdBy) return\n // A guided site start's plan is confirmed for it the moment it is kept\n // (AGL-3594): there is nothing for the person to confirm.\n if (to === 'needs-review' && job.review?.reason === 'plan' && aiJobAutoConfirms(job)) return\n // The site a job worked on, by name and address (AGL-3889): the email is\n // read away from the console, often by someone with several sites.\n const site = job.hostId ? await readSite(job.hostId).catch(() => null) : null\n const notice = aiJobNotice(job, to, site)\n if (!site) {\n await notify([job.createdBy], notice)\n return\n }\n await notify([job.createdBy], notice, {\n emailDetails: [\n { label: 'Site', value: site.name },\n ...(site.url ? [{ label: 'Address', value: site.url }] : []),\n ],\n })\n }\n}\n\n/**\n * A job's site as its notice names it: the name its managers gave it, else\n * its address, and where it is live — its custom domain once that serves,\n * else its platform subdomain. `null` for a site that is gone.\n */\nexport async function readJobSite(hostId: string): Promise<AiJobNoticeSite | null> {\n const host = await firebaseAdmin.app().firestore().collection('hosts').doc(hostId).get()\n if (!host.exists) return null\n const data = (host.data() ?? {}) as Record<string, unknown>\n const text = (value: unknown) => (typeof value === 'string' ? value.trim() : '')\n const subdomain = text(data['subdomain'])\n const domain = liveCustomDomain(data as never)\n return {\n name: text(data['displayName']) || text(data['name']) || subdomain || hostId,\n url: domain ? `https://${domain}` : subdomain ? `https://${subdomain}.${TENANT_APEX}` : null,\n }\n}\n\n/** The longest runner error an alert quotes; the log has the rest. */\nconst ALERT_ERROR_MAX = 300\n\n/** At most this many failed parts are named in a partial-build alert. */\nconst ALERT_ITEMS_MAX = 5\n\nconst capped = (text: string): string =>\n text.length > ALERT_ERROR_MAX ? `${text.slice(0, ALERT_ERROR_MAX)}…` : text\n\n/**\n * Staff are told when a job fails on our side (`ai.jobFailed`), and when a\n * build finishes `done` with parts that failed on our side\n * (`ai.buildPartlyFailed`) — each deduped per job kind, so a broken provider\n * or a broken step is heard about the hour it starts. A failure that is the\n * customer's — the model declined the brief or a part of it, the site\n * switched AI off — raises nothing. `raiseOperatorAlert` never throws.\n */\nexport function aiJobFailureAlerter(\n raise: typeof raiseOperatorAlert = raiseOperatorAlert,\n): AiJobTransitionListener {\n return async ({ job, to, failure }) => {\n if (to === 'done') {\n const items = job.items ?? []\n const ours = items.filter((row) => row.status === 'failed' && row.failure?.ours)\n if (!ours.length) return\n const named = ours\n .slice(0, ALERT_ITEMS_MAX)\n .map((row) => `${row.label} (${row.failure?.reason}${row.failure?.detail ? `: ${row.failure.detail}` : ''})`)\n .join('; ')\n await raise(AI_BUILD_PARTLY_FAILED, {\n dedupeKey: job.kind,\n orgId: job.orgId,\n ...(job.hostId ? { hostId: job.hostId } : {}),\n context: {\n kind: job.kind,\n jobId: job.$id,\n orgId: job.orgId,\n failed: ours.length,\n total: items.length,\n items: capped(ours.length > ALERT_ITEMS_MAX ? `${named}; and ${ours.length - ALERT_ITEMS_MAX} more` : named),\n },\n })\n return\n }\n if (to !== 'failed' || !failure?.ours) return\n const error = failure.error ?? job.error ?? 'no error recorded'\n await raise(AI_JOB_FAILED, {\n dedupeKey: job.kind,\n orgId: job.orgId,\n ...(job.hostId ? { hostId: job.hostId } : {}),\n context: {\n kind: job.kind,\n jobId: job.$id,\n orgId: job.orgId,\n step: failure.stepIndex === null ? 'n/a' : job.steps[failure.stepIndex]?.name ?? failure.stepIndex,\n error: capped(error),\n },\n })\n }\n}\n\n/**\n * Registers what the jobs machine tells about each change: the notifier, the\n * staff alert for a failure on our side, and the business profile's prefill\n * from a site job (AGL-3661). One listener, each in turn; the alert and the\n * prefill swallow their own failures, so neither costs the person their notice.\n */\nexport function registerAiJobsNotify(): void {\n const notify = aiJobTransitionNotifier()\n const alert = aiJobFailureAlerter()\n const prefill = aiBusinessProfilePrefiller(() => firebaseAdmin.app().firestore())\n registerAiJobTransitionListener(async (change) => {\n await notify(change)\n await alert(change)\n await prefill(change)\n })\n}\n"],"names":["firebaseAdmin","notifyUsers","raiseOperatorAlert","AI_BUILD_PARTLY_FAILED","AI_JOB_FAILED","liveCustomDomain","TENANT_APEX","aiJobNotice","aiBusinessProfilePrefiller","aiJobAutoConfirms","registerAiJobTransitionListener","aiJobTransitionNotifier","notify","readSite","readJobSite","job","to","createdBy","review","reason","site","hostId","catch","notice","emailDetails","label","value","name","url","host","app","firestore","collection","doc","get","exists","data","text","trim","subdomain","domain","ALERT_ERROR_MAX","ALERT_ITEMS_MAX","capped","length","slice","aiJobFailureAlerter","raise","failure","items","ours","filter","row","status","named","map","detail","join","dedupeKey","kind","orgId","context","jobId","$id","failed","total","error","step","stepIndex","steps","registerAiJobsNotify","alert","prefill","change"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-jobs-notify.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 { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { notifyUsers } from '@aglyn/tenant-data-admin/server/notifications'\nimport { raiseOperatorAlert } from '@aglyn/tenant-data-admin/server/operator-alerts'\nimport { AI_BUILD_PARTLY_FAILED, AI_JOB_FAILED } from '../operator-alerts'\nimport { liveCustomDomain } from '@aglyn/aglyn/app-utils/host-naming'\nimport { TENANT_APEX } from '@aglyn/aglyn/app-utils/tenant-apex'\nimport { aiJobNotice, aiJobPlanReminderNotice, type AiJobNoticeSite } from '../model/ai-job-notice'\nimport type { AiJob } from '../model/ai-jobs.types'\nimport { aiBusinessProfilePrefiller } from './ai-business-profile-prefill'\nimport { aiJobAutoConfirms } from './ai-job-auto-confirm'\nimport { registerAiJobTransitionListener, type AiJobTransitionListener } from './ai-jobs'\n\n/**\n * The person who started an AI job is told when it needs them, when it\n * finishes and when it stops (AGL-3593), through the console's notifications\n * — the bell, and the generic notification email for a person who switched\n * that channel on for the category. No other channel: there is no push.\n *\n * The machine calls this where the job's state changes, once per change\n * (`registerAiJobTransitionListener`): the request that ran a step inline,\n * or the beat that ran it with nobody watching — which is exactly when a\n * person needs telling. The recipient is the job's creator, whose job it is\n * and who alone confirms its plan; a teammate is not told about someone\n * else's draft.\n *\n * `notifyUsers` never throws, and honors the person's per-category,\n * per-workspace and per-site preferences.\n */\nexport function aiJobTransitionNotifier(\n notify: typeof notifyUsers = notifyUsers,\n readSite: (hostId: string) => Promise<AiJobNoticeSite | null> = readJobSite,\n): AiJobTransitionListener {\n return async ({ job, to }) => {\n if (!job.createdBy) return\n // A guided site start's plan is confirmed for it the moment it is kept\n // (AGL-3594): there is nothing for the person to confirm.\n if (to === 'needs-review' && job.review?.reason === 'plan' && aiJobAutoConfirms(job)) return\n // The site a job worked on, by name and address (AGL-3889): the email is\n // read away from the console, often by someone with several sites.\n const site = job.hostId ? await readSite(job.hostId).catch(() => null) : null\n const notice = aiJobNotice(job, to, site)\n if (!site) {\n await notify([job.createdBy], notice)\n return\n }\n await notify([job.createdBy], notice, {\n emailDetails: [\n { label: 'Site', value: site.name },\n ...(site.url ? [{ label: 'Address', value: site.url }] : []),\n ],\n })\n }\n}\n\n/**\n * A job's site as its notice names it: the name its managers gave it, else\n * its address, and where it is live — its custom domain once that serves,\n * else its platform subdomain. `null` for a site that is gone.\n */\nexport async function readJobSite(\n hostId: string,\n firestore: FirebaseFirestore.Firestore = firebaseAdmin.app().firestore(),\n): Promise<AiJobNoticeSite | null> {\n const host = await firestore.collection('hosts').doc(hostId).get()\n if (!host.exists) return null\n const data = (host.data() ?? {}) as Record<string, unknown>\n const text = (value: unknown) => (typeof value === 'string' ? value.trim() : '')\n const subdomain = text(data['subdomain'])\n const domain = liveCustomDomain(data as never)\n return {\n name: text(data['displayName']) || text(data['name']) || subdomain || hostId,\n url: domain ? `https://${domain}` : subdomain ? `https://${subdomain}.${TENANT_APEX}` : null,\n }\n}\n\n// ── The one plan-ready reminder (AGL-4076) ───────────────────────────────\n\n/**\n * How long a plan waits for its confirmation before its one reminder: a day\n * after the job last changed, which for a plan nobody touched is the moment\n * it was ready.\n */\nexport const AI_PLAN_REMINDER_AFTER_MS = 24 * 60 * 60_000\n\n/**\n * How long after it is due a reminder may still go out. A plan that has\n * waited longer than a day and this is never reminded: the person was told\n * when it was ready, and a reminder about a plan left for a week reads as\n * noise, so the plans already waiting when this shipped are not mailed all\n * at once. It is also the beats a lockdown of `ai-generate` may hold the\n * reminders for before one is missed rather than sent late.\n */\nexport const AI_PLAN_REMINDER_WINDOW_MS = 6 * 60 * 60_000\n\n/** The waiting jobs one beat reads, oldest first. */\nexport const AI_PLAN_REMINDER_SCAN = 25\n\n/** The reminders one beat sends; the rest are the next minute's. */\nexport const AI_PLAN_REMINDER_MAX = 5\n\n/** Wall clock one beat gives the reminders before it runs any job's step. */\nexport const AI_PLAN_REMINDER_BUDGET_MS = 10_000\n\n/** The org's jobs, as `AI_JOBS_COLLECTION` in `ai-jobs.ts` names them. */\nconst JOBS_COLLECTION = 'aiJobs'\n\ntype Instant = Date | { toMillis(): number } | number | string | null | undefined\n\nfunction instantMs(value: Instant): number | null {\n if (value === null || value === undefined) return null\n if (value instanceof Date) return value.getTime()\n if (typeof value === 'number') return Number.isFinite(value) ? value : null\n if (typeof value === 'string') {\n const parsed = Date.parse(value)\n return Number.isFinite(parsed) ? parsed : null\n }\n return typeof value.toMillis === 'function' ? value.toMillis() : null\n}\n\n/**\n * Whether a job is owed its plan reminder now (AGL-4076): its plan waits for\n * the person who started it (`needs_review`, reason `plan`), has waited\n * between a day and a day and {@link AI_PLAN_REMINDER_WINDOW_MS}, and the job\n * was never reminded. Never a plan that is confirmed for its person (a guided\n * start's), a job someone asked to cancel, or one whose site was removed: the\n * same jobs the first notice skips, and the ones that moved on.\n */\nexport function aiPlanReminderDue(\n job: Pick<AiJob, 'kind' | 'inputs' | 'status' | 'review' | 'createdBy' | 'updatedAt' | 'remindedAt' | 'cancelRequested' | 'siteRemoved'>,\n nowMs: number,\n): boolean {\n if (job.status !== 'needs_review' || job.review?.reason !== 'plan' || !job.createdBy) return false\n if (job.remindedAt || job.cancelRequested || job.siteRemoved || aiJobAutoConfirms(job)) return false\n const waitedMs = nowMs - (instantMs(job.updatedAt as Instant) ?? nowMs)\n return waitedMs >= AI_PLAN_REMINDER_AFTER_MS && waitedMs < AI_PLAN_REMINDER_AFTER_MS + AI_PLAN_REMINDER_WINDOW_MS\n}\n\nexport interface AiPlanReminderSweepResult {\n /** Jobs read that were waiting in the window, reminded or not. */\n scanned: number\n /** Reminders sent. */\n reminded: number\n}\n\nexport interface SweepAiPlanRemindersOptions {\n firestore: FirebaseFirestore.Firestore\n now?: () => number\n budgetMs?: number\n notify?: typeof notifyUsers\n /** The job's site; `readJobSite` unless a spec replaces it. */\n readSite?: (hostId: string) => Promise<AiJobNoticeSite | null>\n}\n\n/**\n * One beat's plan reminders (AGL-4076). A plan that waits for its\n * confirmation is told about once when it is ready (`aiJobTransitionNotifier`)\n * and once more a day later, here, and never again: 13 plans in the 10/10 to\n * 10/28 window waited 4 to 428 hours after their single notice.\n *\n * The waiting jobs are read by the collection-group index the sweep already\n * uses, on (status, updatedAt), inside the window a reminder may go out in,\n * oldest first; every condition is then read again on the job, in the\n * transaction that marks `remindedAt`, so two beats never both send one and a\n * job confirmed, cancelled or deleted in the meantime gets none. The mark is\n * written before the notice: a beat that dies between the two loses a\n * reminder rather than sending a second. `notifyUsers` holds the notice to the\n * person's own settings for AI job needs you, by channel, workspace and site,\n * as it holds the first one; a job whose site is gone is not reminded.\n *\n * The mark leaves `updatedAt` alone, so the job's age in AI jobs stays the\n * age of its plan.\n */\nexport async function sweepAiPlanReminders(options: SweepAiPlanRemindersOptions): Promise<AiPlanReminderSweepResult> {\n const now = options.now ?? Date.now\n const startedAt = now()\n const budgetMs = options.budgetMs ?? AI_PLAN_REMINDER_BUDGET_MS\n const notify = options.notify ?? notifyUsers\n const readSite = options.readSite ?? ((hostId: string) => readJobSite(hostId, options.firestore))\n const dueBefore = startedAt - AI_PLAN_REMINDER_AFTER_MS\n const waiting = await options.firestore\n .collectionGroup(JOBS_COLLECTION)\n .where('status', '==', 'needs_review')\n .where('updatedAt', '<=', new Date(dueBefore))\n .where('updatedAt', '>', new Date(dueBefore - AI_PLAN_REMINDER_WINDOW_MS))\n .orderBy('updatedAt', 'asc')\n .limit(AI_PLAN_REMINDER_SCAN)\n .get()\n const result: AiPlanReminderSweepResult = { scanned: waiting.size, reminded: 0 }\n for (const doc of waiting.docs) {\n if (result.reminded >= AI_PLAN_REMINDER_MAX || now() - startedAt >= budgetMs) break\n const listed = { ...(doc.data() as Omit<AiJob, '$id'>), $id: doc.id } as AiJob\n if (!aiPlanReminderDue(listed, now())) continue\n try {\n // A job's site, read before the mark, so a job whose site is gone is never marked or told.\n const site = listed.hostId ? await readSite(listed.hostId) : null\n if (listed.hostId && !site) continue\n const job = await options.firestore.runTransaction(async (tx) => {\n const fresh = await tx.get(doc.ref)\n if (!fresh.exists) return null\n const current = { ...(fresh.data() as Omit<AiJob, '$id'>), $id: fresh.id } as AiJob\n if (!aiPlanReminderDue(current, now())) return null\n tx.update(doc.ref, { remindedAt: new Date(now()) })\n return current\n })\n if (!job) continue\n const notice = aiJobPlanReminderNotice(job, site)\n if (site) {\n await notify([job.createdBy], notice, {\n emailDetails: [\n { label: 'Site', value: site.name },\n ...(site.url ? [{ label: 'Address', value: site.url }] : []),\n ],\n })\n } else {\n await notify([job.createdBy], notice)\n }\n result.reminded += 1\n } catch (error) {\n // One job's fault is isolated; an unmarked job is read again next beat, inside its window.\n console.error('ai plan reminder failed', { orgId: listed.orgId, jobId: listed.$id, error })\n }\n }\n if (result.reminded) console.info(`ai jobs: ${result.reminded} plan reminder(s) sent`)\n return result\n}\n\n/** The longest runner error an alert quotes; the log has the rest. */\nconst ALERT_ERROR_MAX = 300\n\n/** At most this many failed parts are named in a partial-build alert. */\nconst ALERT_ITEMS_MAX = 5\n\nconst capped = (text: string): string =>\n text.length > ALERT_ERROR_MAX ? `${text.slice(0, ALERT_ERROR_MAX)}…` : text\n\n/**\n * Staff are told when a job fails on our side (`ai.jobFailed`), and when a\n * build finishes `done` with parts that failed on our side\n * (`ai.buildPartlyFailed`) — each deduped per job kind, so a broken provider\n * or a broken step is heard about the hour it starts. A failure that is the\n * customer's — the model declined the brief or a part of it, the site\n * switched AI off — raises nothing. `raiseOperatorAlert` never throws.\n */\nexport function aiJobFailureAlerter(\n raise: typeof raiseOperatorAlert = raiseOperatorAlert,\n): AiJobTransitionListener {\n return async ({ job, to, failure }) => {\n if (to === 'done') {\n const items = job.items ?? []\n const ours = items.filter((row) => row.status === 'failed' && row.failure?.ours)\n if (!ours.length) return\n const named = ours\n .slice(0, ALERT_ITEMS_MAX)\n .map((row) => `${row.label} (${row.failure?.reason}${row.failure?.detail ? `: ${row.failure.detail}` : ''})`)\n .join('; ')\n await raise(AI_BUILD_PARTLY_FAILED, {\n dedupeKey: job.kind,\n orgId: job.orgId,\n ...(job.hostId ? { hostId: job.hostId } : {}),\n context: {\n kind: job.kind,\n jobId: job.$id,\n orgId: job.orgId,\n failed: ours.length,\n total: items.length,\n items: capped(ours.length > ALERT_ITEMS_MAX ? `${named}; and ${ours.length - ALERT_ITEMS_MAX} more` : named),\n },\n })\n return\n }\n if (to !== 'failed' || !failure?.ours) return\n const error = failure.error ?? job.error ?? 'no error recorded'\n await raise(AI_JOB_FAILED, {\n dedupeKey: job.kind,\n orgId: job.orgId,\n ...(job.hostId ? { hostId: job.hostId } : {}),\n context: {\n kind: job.kind,\n jobId: job.$id,\n orgId: job.orgId,\n step: failure.stepIndex === null ? 'n/a' : job.steps[failure.stepIndex]?.name ?? failure.stepIndex,\n error: capped(error),\n },\n })\n }\n}\n\n/**\n * Registers what the jobs machine tells about each change: the notifier, the\n * staff alert for a failure on our side, and the business profile's prefill\n * from a site job (AGL-3661). One listener, each in turn; the alert and the\n * prefill swallow their own failures, so neither costs the person their notice.\n */\nexport function registerAiJobsNotify(): void {\n const notify = aiJobTransitionNotifier()\n const alert = aiJobFailureAlerter()\n const prefill = aiBusinessProfilePrefiller(() => firebaseAdmin.app().firestore())\n registerAiJobTransitionListener(async (change) => {\n await notify(change)\n await alert(change)\n await prefill(change)\n })\n}\n"],"names":["firebaseAdmin","notifyUsers","raiseOperatorAlert","AI_BUILD_PARTLY_FAILED","AI_JOB_FAILED","liveCustomDomain","TENANT_APEX","aiJobNotice","aiJobPlanReminderNotice","aiBusinessProfilePrefiller","aiJobAutoConfirms","registerAiJobTransitionListener","aiJobTransitionNotifier","notify","readSite","readJobSite","job","to","createdBy","review","reason","site","hostId","catch","notice","emailDetails","label","value","name","url","firestore","app","host","collection","doc","get","exists","data","text","trim","subdomain","domain","AI_PLAN_REMINDER_AFTER_MS","AI_PLAN_REMINDER_WINDOW_MS","AI_PLAN_REMINDER_SCAN","AI_PLAN_REMINDER_MAX","AI_PLAN_REMINDER_BUDGET_MS","JOBS_COLLECTION","instantMs","undefined","Date","getTime","Number","isFinite","parsed","parse","toMillis","aiPlanReminderDue","nowMs","status","remindedAt","cancelRequested","siteRemoved","waitedMs","updatedAt","sweepAiPlanReminders","options","now","startedAt","budgetMs","dueBefore","waiting","collectionGroup","where","orderBy","limit","result","scanned","size","reminded","docs","listed","$id","id","runTransaction","tx","fresh","ref","current","update","error","console","orgId","jobId","info","ALERT_ERROR_MAX","ALERT_ITEMS_MAX","capped","length","slice","aiJobFailureAlerter","raise","failure","items","ours","filter","row","named","map","detail","join","dedupeKey","kind","context","failed","total","step","stepIndex","steps","registerAiJobsNotify","alert","prefill","change"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,aAAa,QAAQ,iDAAgD;AAC9E,SAASC,WAAW,QAAQ,gDAA+C;AAC3E,SAASC,kBAAkB,QAAQ,kDAAiD;AACpF,SAASC,sBAAsB,EAAEC,aAAa,QAAQ,wBAAoB;AAC1E,SAASC,gBAAgB,QAAQ,qCAAoC;AACrE,SAASC,WAAW,QAAQ,qCAAoC;AAChE,SAASC,WAAW,EAAEC,uBAAuB,QAA8B,4BAAwB;AAEnG,SAASC,0BAA0B,QAAQ,mCAA+B;AAC1E,SAASC,iBAAiB,QAAQ,2BAAuB;AACzD,SAASC,+BAA+B,QAAsC,eAAW;AAEzF;;;;;;;;;;;;;;;CAeC,GACD,OAAO,SAASC,wBACdC,SAA6BZ,WAAW,EACxCa,WAAgEC,WAAW;IAE3E,OAAO,OAAO,EAAEC,GAAG,EAAEC,EAAE,EAAE;YAIMD;QAH7B,IAAI,CAACA,IAAIE,SAAS,EAAE;QACpB,uEAAuE;QACvE,0DAA0D;QAC1D,IAAID,OAAO,kBAAkBD,EAAAA,cAAAA,IAAIG,MAAM,qBAAVH,YAAYI,MAAM,MAAK,UAAUV,kBAAkBM,MAAM;QACtF,yEAAyE;QACzE,mEAAmE;QACnE,MAAMK,OAAOL,IAAIM,MAAM,GAAG,MAAMR,SAASE,IAAIM,MAAM,EAAEC,KAAK,CAAC,IAAM,QAAQ;QACzE,MAAMC,SAASjB,YAAYS,KAAKC,IAAII;QACpC,IAAI,CAACA,MAAM;YACT,MAAMR,OAAO;gBAACG,IAAIE,SAAS;aAAC,EAAEM;YAC9B;QACF;QACA,MAAMX,OAAO;YAACG,IAAIE,SAAS;SAAC,EAAEM,QAAQ;YACpCC,cAAc;gBACZ;oBAAEC,OAAO;oBAAQC,OAAON,KAAKO,IAAI;gBAAC;mBAC9BP,KAAKQ,GAAG,GAAG;oBAAC;wBAAEH,OAAO;wBAAWC,OAAON,KAAKQ,GAAG;oBAAC;iBAAE,GAAG,EAAE;aAC5D;QACH;IACF;AACF;AAEA;;;;CAIC,GACD,OAAO,eAAed,YACpBO,MAAc,EACdQ,YAAyC9B,cAAc+B,GAAG,GAAGD,SAAS,EAAE;QAI1DE;IAFd,MAAMA,OAAO,MAAMF,UAAUG,UAAU,CAAC,SAASC,GAAG,CAACZ,QAAQa,GAAG;IAChE,IAAI,CAACH,KAAKI,MAAM,EAAE,OAAO;IACzB,MAAMC,QAAQL,aAAAA,KAAKK,IAAI,cAATL,aAAe,CAAC;IAC9B,MAAMM,OAAO,CAACX,QAAoB,OAAOA,UAAU,WAAWA,MAAMY,IAAI,KAAK;IAC7E,MAAMC,YAAYF,KAAKD,IAAI,CAAC,YAAY;IACxC,MAAMI,SAASpC,iBAAiBgC;IAChC,OAAO;QACLT,MAAMU,KAAKD,IAAI,CAAC,cAAc,KAAKC,KAAKD,IAAI,CAAC,OAAO,KAAKG,aAAalB;QACtEO,KAAKY,SAAS,CAAC,QAAQ,EAAEA,QAAQ,GAAGD,YAAY,CAAC,QAAQ,EAAEA,UAAU,CAAC,EAAElC,aAAa,GAAG;IAC1F;AACF;AAEA,4EAA4E;AAE5E;;;;CAIC,GACD,OAAO,MAAMoC,4BAA4B,KAAK,KAAK,MAAM;AAEzD;;;;;;;CAOC,GACD,OAAO,MAAMC,6BAA6B,IAAI,KAAK,MAAM;AAEzD,mDAAmD,GACnD,OAAO,MAAMC,wBAAwB,GAAE;AAEvC,kEAAkE,GAClE,OAAO,MAAMC,uBAAuB,EAAC;AAErC,2EAA2E,GAC3E,OAAO,MAAMC,6BAA6B,MAAM;AAEhD,wEAAwE,GACxE,MAAMC,kBAAkB;AAIxB,SAASC,UAAUrB,KAAc;IAC/B,IAAIA,UAAU,QAAQA,UAAUsB,WAAW,OAAO;IAClD,IAAItB,iBAAiBuB,MAAM,OAAOvB,MAAMwB,OAAO;IAC/C,IAAI,OAAOxB,UAAU,UAAU,OAAOyB,OAAOC,QAAQ,CAAC1B,SAASA,QAAQ;IACvE,IAAI,OAAOA,UAAU,UAAU;QAC7B,MAAM2B,SAASJ,KAAKK,KAAK,CAAC5B;QAC1B,OAAOyB,OAAOC,QAAQ,CAACC,UAAUA,SAAS;IAC5C;IACA,OAAO,OAAO3B,MAAM6B,QAAQ,KAAK,aAAa7B,MAAM6B,QAAQ,KAAK;AACnE;AAEA;;;;;;;CAOC,GACD,OAAO,SAASC,kBACdzC,GAAwI,EACxI0C,KAAa;QAIaV;QAFWhC;IAArC,IAAIA,IAAI2C,MAAM,KAAK,kBAAkB3C,EAAAA,cAAAA,IAAIG,MAAM,qBAAVH,YAAYI,MAAM,MAAK,UAAU,CAACJ,IAAIE,SAAS,EAAE,OAAO;IAC7F,IAAIF,IAAI4C,UAAU,IAAI5C,IAAI6C,eAAe,IAAI7C,IAAI8C,WAAW,IAAIpD,kBAAkBM,MAAM,OAAO;IAC/F,MAAM+C,WAAWL,UAASV,aAAAA,UAAUhC,IAAIgD,SAAS,aAAvBhB,aAAuCU;IACjE,OAAOK,YAAYrB,6BAA6BqB,WAAWrB,4BAA4BC;AACzF;AAkBA;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,eAAesB,qBAAqBC,OAAoC;QACjEA,cAEKA,mBACFA,iBACEA;IAJjB,MAAMC,OAAMD,eAAAA,QAAQC,GAAG,YAAXD,eAAehB,KAAKiB,GAAG;IACnC,MAAMC,YAAYD;IAClB,MAAME,YAAWH,oBAAAA,QAAQG,QAAQ,YAAhBH,oBAAoBpB;IACrC,MAAMjC,UAASqD,kBAAAA,QAAQrD,MAAM,YAAdqD,kBAAkBjE;IACjC,MAAMa,YAAWoD,oBAAAA,QAAQpD,QAAQ,YAAhBoD,oBAAqB,CAAC5C,SAAmBP,YAAYO,QAAQ4C,QAAQpC,SAAS;IAC/F,MAAMwC,YAAYF,YAAY1B;IAC9B,MAAM6B,UAAU,MAAML,QAAQpC,SAAS,CACpC0C,eAAe,CAACzB,iBAChB0B,KAAK,CAAC,UAAU,MAAM,gBACtBA,KAAK,CAAC,aAAa,MAAM,IAAIvB,KAAKoB,YAClCG,KAAK,CAAC,aAAa,KAAK,IAAIvB,KAAKoB,YAAY3B,6BAC7C+B,OAAO,CAAC,aAAa,OACrBC,KAAK,CAAC/B,uBACNT,GAAG;IACN,MAAMyC,SAAoC;QAAEC,SAASN,QAAQO,IAAI;QAAEC,UAAU;IAAE;IAC/E,KAAK,MAAM7C,OAAOqC,QAAQS,IAAI,CAAE;QAC9B,IAAIJ,OAAOG,QAAQ,IAAIlC,wBAAwBsB,QAAQC,aAAaC,UAAU;QAC9E,MAAMY,SAAS,aAAM/C,IAAIG,IAAI;YAA2B6C,KAAKhD,IAAIiD,EAAE;;QACnE,IAAI,CAAC1B,kBAAkBwB,QAAQd,QAAQ;QACvC,IAAI;YACF,2FAA2F;YAC3F,MAAM9C,OAAO4D,OAAO3D,MAAM,GAAG,MAAMR,SAASmE,OAAO3D,MAAM,IAAI;YAC7D,IAAI2D,OAAO3D,MAAM,IAAI,CAACD,MAAM;YAC5B,MAAML,MAAM,MAAMkD,QAAQpC,SAAS,CAACsD,cAAc,CAAC,OAAOC;gBACxD,MAAMC,QAAQ,MAAMD,GAAGlD,GAAG,CAACD,IAAIqD,GAAG;gBAClC,IAAI,CAACD,MAAMlD,MAAM,EAAE,OAAO;gBAC1B,MAAMoD,UAAU,aAAMF,MAAMjD,IAAI;oBAA2B6C,KAAKI,MAAMH,EAAE;;gBACxE,IAAI,CAAC1B,kBAAkB+B,SAASrB,QAAQ,OAAO;gBAC/CkB,GAAGI,MAAM,CAACvD,IAAIqD,GAAG,EAAE;oBAAE3B,YAAY,IAAIV,KAAKiB;gBAAO;gBACjD,OAAOqB;YACT;YACA,IAAI,CAACxE,KAAK;YACV,MAAMQ,SAAShB,wBAAwBQ,KAAKK;YAC5C,IAAIA,MAAM;gBACR,MAAMR,OAAO;oBAACG,IAAIE,SAAS;iBAAC,EAAEM,QAAQ;oBACpCC,cAAc;wBACZ;4BAAEC,OAAO;4BAAQC,OAAON,KAAKO,IAAI;wBAAC;2BAC9BP,KAAKQ,GAAG,GAAG;4BAAC;gCAAEH,OAAO;gCAAWC,OAAON,KAAKQ,GAAG;4BAAC;yBAAE,GAAG,EAAE;qBAC5D;gBACH;YACF,OAAO;gBACL,MAAMhB,OAAO;oBAACG,IAAIE,SAAS;iBAAC,EAAEM;YAChC;YACAoD,OAAOG,QAAQ,IAAI;QACrB,EAAE,OAAOW,OAAO;YACd,2FAA2F;YAC3FC,QAAQD,KAAK,CAAC,2BAA2B;gBAAEE,OAAOX,OAAOW,KAAK;gBAAEC,OAAOZ,OAAOC,GAAG;gBAAEQ;YAAM;QAC3F;IACF;IACA,IAAId,OAAOG,QAAQ,EAAEY,QAAQG,IAAI,CAAC,CAAC,SAAS,EAAElB,OAAOG,QAAQ,CAAC,sBAAsB,CAAC;IACrF,OAAOH;AACT;AAEA,oEAAoE,GACpE,MAAMmB,kBAAkB;AAExB,uEAAuE,GACvE,MAAMC,kBAAkB;AAExB,MAAMC,SAAS,CAAC3D,OACdA,KAAK4D,MAAM,GAAGH,kBAAkB,GAAGzD,KAAK6D,KAAK,CAAC,GAAGJ,iBAAiB,CAAC,CAAC,GAAGzD;AAEzE;;;;;;;CAOC,GACD,OAAO,SAAS8D,oBACdC,QAAmCnG,kBAAkB;IAErD,OAAO,OAAO,EAAEc,GAAG,EAAEC,EAAE,EAAEqF,OAAO,EAAE;YAyBlBA,MAAAA;YASiCtF;QAjC/C,IAAIC,OAAO,QAAQ;gBACHD;YAAd,MAAMuF,SAAQvF,aAAAA,IAAIuF,KAAK,YAATvF,aAAa,EAAE;YAC7B,MAAMwF,OAAOD,MAAME,MAAM,CAAC,CAACC;oBAAmCA;uBAA3BA,IAAI/C,MAAM,KAAK,cAAY+C,eAAAA,IAAIJ,OAAO,qBAAXI,aAAaF,IAAI;;YAC/E,IAAI,CAACA,KAAKN,MAAM,EAAE;YAClB,MAAMS,QAAQH,KACXL,KAAK,CAAC,GAAGH,iBACTY,GAAG,CAAC,CAACF;oBAAyBA,cAAsBA;uBAAvC,GAAGA,IAAIhF,KAAK,CAAC,EAAE,GAAEgF,eAAAA,IAAIJ,OAAO,qBAAXI,aAAatF,MAAM,GAAGsF,EAAAA,gBAAAA,IAAIJ,OAAO,qBAAXI,cAAaG,MAAM,IAAG,CAAC,EAAE,EAAEH,IAAIJ,OAAO,CAACO,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC;eAC3GC,IAAI,CAAC;YACR,MAAMT,MAAMlG,wBAAwB;gBAClC4G,WAAW/F,IAAIgG,IAAI;gBACnBpB,OAAO5E,IAAI4E,KAAK;eACZ5E,IAAIM,MAAM,GAAG;gBAAEA,QAAQN,IAAIM,MAAM;YAAC,IAAI,CAAC;gBAC3C2F,SAAS;oBACPD,MAAMhG,IAAIgG,IAAI;oBACdnB,OAAO7E,IAAIkE,GAAG;oBACdU,OAAO5E,IAAI4E,KAAK;oBAChBsB,QAAQV,KAAKN,MAAM;oBACnBiB,OAAOZ,MAAML,MAAM;oBACnBK,OAAON,OAAOO,KAAKN,MAAM,GAAGF,kBAAkB,GAAGW,MAAM,MAAM,EAAEH,KAAKN,MAAM,GAAGF,gBAAgB,KAAK,CAAC,GAAGW;gBACxG;;YAEF;QACF;QACA,IAAI1F,OAAO,YAAY,EAACqF,2BAAAA,QAASE,IAAI,GAAE;QACvC,MAAMd,SAAQY,QAAAA,iBAAAA,QAAQZ,KAAK,YAAbY,iBAAiBtF,IAAI0E,KAAK,YAA1BY,OAA8B;QAC5C,MAAMD,MAAMjG,eAAe;YACzB2G,WAAW/F,IAAIgG,IAAI;YACnBpB,OAAO5E,IAAI4E,KAAK;WACZ5E,IAAIM,MAAM,GAAG;YAAEA,QAAQN,IAAIM,MAAM;QAAC,IAAI,CAAC;YAC3C2F,SAAS;gBACPD,MAAMhG,IAAIgG,IAAI;gBACdnB,OAAO7E,IAAIkE,GAAG;gBACdU,OAAO5E,IAAI4E,KAAK;gBAChBwB,MAAMd,QAAQe,SAAS,KAAK,OAAO,kBAAQrG,+BAAAA,IAAIsG,KAAK,CAAChB,QAAQe,SAAS,CAAC,qBAA5BrG,6BAA8BY,IAAI,oBAAI0E,QAAQe,SAAS;gBAClG3B,OAAOO,OAAOP;YAChB;;IAEJ;AACF;AAEA;;;;;CAKC,GACD,OAAO,SAAS6B;IACd,MAAM1G,SAASD;IACf,MAAM4G,QAAQpB;IACd,MAAMqB,UAAUhH,2BAA2B,IAAMT,cAAc+B,GAAG,GAAGD,SAAS;IAC9EnB,gCAAgC,OAAO+G;QACrC,MAAM7G,OAAO6G;QACb,MAAMF,MAAME;QACZ,MAAMD,QAAQC;IAChB;AACF"}
|
|
@@ -138,6 +138,20 @@ import type { AiBuildPlan } from '../model/ai-build-plan';
|
|
|
138
138
|
* where this module lists it ("gym" of "Synchronicity Gym"); and a
|
|
139
139
|
* training word's other senses (a railway's train, an elevator's lift, a
|
|
140
140
|
* newspaper's press, a boat's row) never fill a gym's picture.
|
|
141
|
+
* 10. **The trade, never a word of it** (the 2026-10-25 booth kit, AGL-4065:
|
|
142
|
+
* six trades whose photos were race cars, cyclists, horses, a Honda
|
|
143
|
+
* scooter, a canoe for "Parts for the ranch", hotel buffets for a taco
|
|
144
|
+
* trailer and an office for a landscaper). A business named by two
|
|
145
|
+
* things ("Motorcycle and dirt bike repair shop") is both; the craft
|
|
146
|
+
* named first leads, never one named only in what a customer may ask
|
|
147
|
+
* ("takes catering requests"); a hero's first tag and a broad search's
|
|
148
|
+
* hit name the business's own world ({@link AiStockSiteTerms.lead}),
|
|
149
|
+
* never any word of its type ("race" of "race car"); a two-word world
|
|
150
|
+
* ("dirt bike") is named whole; a maker's name never shows; a sentence to
|
|
151
|
+
* the reader, a job or an order is never a subject, and a part or gear is
|
|
152
|
+
* searched only with its world; a record's cover is searched in its world
|
|
153
|
+
* first; and a listed world widens only where the library knew nothing of
|
|
154
|
+
* it, never an outdoor or a trade world to an office.
|
|
141
155
|
*
|
|
142
156
|
* A slot the library could not fill because it never answered — down, or
|
|
143
157
|
* out of time before its first answer — or because the site's library
|
|
@@ -207,6 +221,9 @@ export declare function aiStockStem(word: string): string;
|
|
|
207
221
|
* of an English noun phrase is at its end). "ceramic artist" from "a
|
|
208
222
|
* portfolio for a ceramic artist who makes stoneware bowls"; "candle shop"
|
|
209
223
|
* from "a small-batch candle shop selling hand-poured soy candles online".
|
|
224
|
+
* A business named by two things joined by "and" is its second noun phrase
|
|
225
|
+
* whole: "dirt bike repair shop" of "Motorcycle and dirt bike repair shop"
|
|
226
|
+
* (AGL-4065).
|
|
210
227
|
*/
|
|
211
228
|
export declare function aiStockBusinessWords(businessType: string): string;
|
|
212
229
|
/**
|
|
@@ -229,6 +246,19 @@ export interface AiStockSiteTerms {
|
|
|
229
246
|
* held to it.
|
|
230
247
|
*/
|
|
231
248
|
domain: string[];
|
|
249
|
+
/**
|
|
250
|
+
* The business's own world (AGL-4065): the listed crafts its own clause
|
|
251
|
+
* names, their names and kin, and its own words where no craft covers
|
|
252
|
+
* them, as stems and stem pairs ("dirt bike"). What a hero's first tag and a
|
|
253
|
+
* broad search's hit name. Its whole domain where no craft is listed.
|
|
254
|
+
*/
|
|
255
|
+
lead?: string[];
|
|
256
|
+
/**
|
|
257
|
+
* The world a trade serves, as its type names it, where the craft is the
|
|
258
|
+
* trade ("dirt bike" for a parts shop's craft "part", AGL-4065): what its
|
|
259
|
+
* pictures are qualified by in place of the craft.
|
|
260
|
+
*/
|
|
261
|
+
leadWord?: string;
|
|
232
262
|
/** The searches for the site's world itself, broadest last. */
|
|
233
263
|
broad: string[];
|
|
234
264
|
/** The searches for its maker at work, for a story of the craft (AGL-3660); empty for a craft not listed. */
|
|
@@ -266,13 +296,30 @@ export interface AiStockSiteTerms {
|
|
|
266
296
|
*/
|
|
267
297
|
work?: AiStockScene[];
|
|
268
298
|
}
|
|
269
|
-
/**
|
|
299
|
+
/**
|
|
300
|
+
* The site's terms, read from its business type.
|
|
301
|
+
*
|
|
302
|
+
* Two worlds are read (AGL-4065). Its DOMAIN is everything its words name
|
|
303
|
+
* of its world, what a picture of a thing is held to. Its LEAD is the
|
|
304
|
+
* business's own world: the listed crafts its own clause names (and the
|
|
305
|
+
* craft's), their names and kin, and the clause's own words where no craft
|
|
306
|
+
* covers them, never a word that names a world only beside another ("race",
|
|
307
|
+
* "team") nor what a customer is asked to do. A hero's first tag and a broad
|
|
308
|
+
* search's hit name the lead: the Rut Runners heroes, held to every word of
|
|
309
|
+
* "Amateur motocross race team: rider profiles, race schedule…", took race
|
|
310
|
+
* cars because "race car" named "race". Of the crafts its words list, the one
|
|
311
|
+
* named first leads (a taco trailer that "takes catering requests" is a taco
|
|
312
|
+
* trailer), and a craft named only in what a customer may ask for gives the
|
|
313
|
+
* site no searches of its own.
|
|
314
|
+
*/
|
|
270
315
|
export declare function aiStockSiteTerms(businessType: string): AiStockSiteTerms;
|
|
271
316
|
/**
|
|
272
317
|
* A picture's subject in a few words (AGL-3660): the noun phrase its alt
|
|
273
318
|
* text opens with — its first clause holding more than filler, cut where a
|
|
274
319
|
* preposition starts the setting, filler dropped, its last three words (the
|
|
275
|
-
* head noun is the last) — else its section's name.
|
|
320
|
+
* head noun is the last) — else its section's name. A word or two joined by
|
|
321
|
+
* "and" to a longer noun phrase only modify its head: the subject of "Potato
|
|
322
|
+
* and egg breakfast taco" is "egg breakfast taco", never "potato" (AGL-4065).
|
|
276
323
|
*/
|
|
277
324
|
export declare function aiStockSubjectWords(alt: string, sectionName: string): string;
|
|
278
325
|
/**
|
|
@@ -490,7 +537,7 @@ export declare function aiStockProductSearches(slot: Pick<AiLayoutPictureSlot, '
|
|
|
490
537
|
* asks first for the role its words or its list's name (`sectionName`) say,
|
|
491
538
|
* in the site's world: "teacher classroom" for a "Math instructor" (AGL-3856).
|
|
492
539
|
*/
|
|
493
|
-
export declare function aiStockSearchesFor(slot: Pick<AiLayoutPictureSlot, 'role' | 'alt' | 'aspect' | 'people' | 'record' | 'product'>, terms: Pick<AiStockSiteTerms, 'business' | 'craft' | 'domain' | 'broad'> & Partial<Pick<AiStockSiteTerms, 'making' | 'core' | 'scenes' | 'setting' | 'maker' | 'work'>>, subject: string, part?: AiStockSlotPart, sectionName?: string): AiStockSearch[];
|
|
540
|
+
export declare function aiStockSearchesFor(slot: Pick<AiLayoutPictureSlot, 'role' | 'alt' | 'aspect' | 'people' | 'record' | 'product'>, terms: Pick<AiStockSiteTerms, 'business' | 'craft' | 'domain' | 'broad'> & Partial<Pick<AiStockSiteTerms, 'making' | 'core' | 'scenes' | 'setting' | 'maker' | 'work' | 'leadWord'>>, subject: string, part?: AiStockSlotPart, sectionName?: string): AiStockSearch[];
|
|
494
541
|
/**
|
|
495
542
|
* Whether a picture of a craft's site whose pictures are people at a moment
|
|
496
543
|
* shows that work wherever a page would show the maker (AGL-3942): an about
|
|
@@ -513,6 +560,12 @@ export interface AiStockJudgement {
|
|
|
513
560
|
subject?: string;
|
|
514
561
|
/** The site's domain stems ({@link AiStockSiteTerms.domain}). */
|
|
515
562
|
domain?: readonly string[];
|
|
563
|
+
/**
|
|
564
|
+
* The business's own world ({@link AiStockSiteTerms.lead}, AGL-4065): what a
|
|
565
|
+
* hero's first tag and a hit of a search that is not of a thing name. The
|
|
566
|
+
* domain where absent.
|
|
567
|
+
*/
|
|
568
|
+
world?: readonly string[];
|
|
516
569
|
/**
|
|
517
570
|
* Whether a hit must name the subject's object. A broad search, a hero
|
|
518
571
|
* and an about picture need only name the domain.
|
|
@@ -841,10 +894,12 @@ export declare function aiStockWorldIsUnlisted(terms: Pick<AiStockSiteTerms, 'bu
|
|
|
841
894
|
* "freelencing", which no library tags, so its cover hero searched it, found
|
|
842
895
|
* nothing, and shipped a flat dark band with no photo.
|
|
843
896
|
*/
|
|
844
|
-
export declare function aiStockWiderTerms(terms: Pick<AiStockSiteTerms, 'business' | 'craft' | 'broad' | 'domain'
|
|
897
|
+
export declare function aiStockWiderTerms(terms: Pick<AiStockSiteTerms, 'business' | 'craft' | 'broad' | 'domain'> & Partial<Pick<AiStockSiteTerms, 'lead'>>, kind: string): Array<{
|
|
845
898
|
rung: 'kind' | 'starter' | 'generic';
|
|
846
899
|
terms: AiStockSiteTerms;
|
|
847
900
|
}>;
|
|
901
|
+
/** Whether a site's own world is outdoors, on a vehicle, with tools or over food ({@link OUTDOOR_AND_TRADE_STEMS}). */
|
|
902
|
+
export declare function aiStockWorldIsOutdoorOrTrade(terms: Pick<AiStockSiteTerms, 'business' | 'craft' | 'domain'> & Partial<Pick<AiStockSiteTerms, 'lead'>>): boolean;
|
|
848
903
|
/**
|
|
849
904
|
* The first word of a text that names a craft {@link CRAFT_KIN} lists, as
|
|
850
905
|
* its table writes it: "gym" of "Synchronicity Gym", "law" of "Hartwell
|