@aglyn/plugins-ai 1.0.0-beta.219 → 1.0.0-beta.221
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 +18 -18
- package/src/lib/billing/ai-month-meter.d.ts +51 -0
- package/src/lib/billing/ai-month-meter.js +89 -0
- package/src/lib/billing/ai-month-meter.js.map +1 -0
- package/src/lib/billing/ai-overage-close.d.ts +11 -33
- package/src/lib/billing/ai-overage-close.js +4 -3
- package/src/lib/billing/ai-overage-close.js.map +1 -1
- package/src/lib/billing/ai-overage-cutover.d.ts +28 -12
- package/src/lib/billing/ai-overage-cutover.js +1 -28
- package/src/lib/billing/ai-overage-cutover.js.map +1 -1
- package/src/lib/billing/ai-overage-ledger.js +1 -1
- package/src/lib/billing/ai-overage-ledger.js.map +1 -1
- package/src/lib/billing/ai-overage-standing.js +1 -1
- package/src/lib/billing/ai-overage-standing.js.map +1 -1
- package/src/lib/components/ai-allotment-editor.component.js +1 -1
- package/src/lib/components/ai-allotment-editor.component.js.map +1 -1
- package/src/lib/components/ai-assist-provider.component.js +3 -2
- package/src/lib/components/ai-assist-provider.component.js.map +1 -1
- package/src/lib/components/ai-automation-zones.d.ts +63 -0
- package/src/lib/components/ai-automation-zones.js +31 -0
- package/src/lib/components/ai-automation-zones.js.map +1 -0
- package/src/lib/components/ai-describe-automation.component.d.ts +1 -1
- package/src/lib/components/ai-describe-automation.component.js.map +1 -1
- package/src/lib/components/ai-describe-button.component.d.ts +6 -3
- package/src/lib/components/ai-describe-button.component.js +4 -1
- package/src/lib/components/ai-describe-button.component.js.map +1 -1
- package/src/lib/components/ai-explain-automation.component.d.ts +1 -1
- package/src/lib/components/ai-explain-automation.component.js.map +1 -1
- package/src/lib/components/billing-ai-allotments.component.js +1 -1
- package/src/lib/components/billing-ai-allotments.component.js.map +1 -1
- package/src/lib/components/billing-assist-overage-card.component.js +1 -1
- package/src/lib/components/billing-assist-overage-card.component.js.map +1 -1
- package/src/lib/constants.d.ts +5 -0
- package/src/lib/constants.js +4 -0
- package/src/lib/constants.js.map +1 -1
- package/src/lib/declarations.js +2 -0
- package/src/lib/declarations.js.map +1 -1
- package/src/lib/declarations.server.js +24 -10
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/insights/ai-dataset-catalog.d.ts +49 -0
- package/src/lib/insights/ai-dataset-catalog.js +57 -0
- package/src/lib/insights/ai-dataset-catalog.js.map +1 -0
- package/src/lib/insights/ai-figure-readers.d.ts +6 -39
- package/src/lib/insights/ai-figure-readers.js +2 -391
- package/src/lib/insights/ai-figure-readers.js.map +1 -1
- package/src/lib/insights/ai-insight-digest.d.ts +10 -1
- package/src/lib/insights/ai-insight-digest.js +16 -4
- package/src/lib/insights/ai-insight-digest.js.map +1 -1
- package/src/lib/insights/ai-insight-readers.d.ts +1 -1
- package/src/lib/insights/ai-insight-readers.js.map +1 -1
- package/src/lib/jobs/ai-job-component-step.d.ts +6 -1
- package/src/lib/jobs/ai-job-component-step.js +23 -6
- package/src/lib/jobs/ai-job-component-step.js.map +1 -1
- package/src/lib/jobs/ai-job-draft-ids.js +11 -0
- package/src/lib/jobs/ai-job-draft-ids.js.map +1 -1
- package/src/lib/jobs/ai-job-email-step.d.ts +5 -1
- package/src/lib/jobs/ai-job-email-step.js +10 -1
- package/src/lib/jobs/ai-job-email-step.js.map +1 -1
- package/src/lib/jobs/ai-job-form-step.d.ts +17 -1
- package/src/lib/jobs/ai-job-form-step.js +60 -8
- package/src/lib/jobs/ai-job-form-step.js.map +1 -1
- package/src/lib/jobs/ai-job-generation.d.ts +21 -0
- package/src/lib/jobs/ai-job-generation.js +53 -1
- package/src/lib/jobs/ai-job-generation.js.map +1 -1
- package/src/lib/jobs/ai-job-insight-step.js +1 -1
- package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.d.ts +15 -4
- package/src/lib/jobs/ai-job-layout-step.js +45 -12
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-conformance.d.ts +5 -0
- package/src/lib/jobs/ai-job-plan-conformance.js +7 -1
- package/src/lib/jobs/ai-job-plan-conformance.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-creations.d.ts +80 -0
- package/src/lib/jobs/ai-job-plan-creations.js +199 -0
- package/src/lib/jobs/ai-job-plan-creations.js.map +1 -0
- package/src/lib/jobs/ai-job-plan-step.d.ts +8 -2
- package/src/lib/jobs/ai-job-plan-step.js +20 -6
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.d.ts +2 -0
- package/src/lib/jobs/ai-job-site-step.js +6 -6
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-job-template-step.d.ts +9 -6
- package/src/lib/jobs/ai-job-template-step.js +72 -15
- package/src/lib/jobs/ai-job-template-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs.js +1 -1
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/jobs/ai-workflow-records.d.ts +5 -4
- package/src/lib/jobs/ai-workflow-records.js +26 -22
- package/src/lib/jobs/ai-workflow-records.js.map +1 -1
- package/src/lib/model/ai-allotments.d.ts +16 -3
- package/src/lib/model/ai-allotments.js +34 -11
- package/src/lib/model/ai-allotments.js.map +1 -1
- package/src/lib/model/ai-job-creations.d.ts +55 -0
- package/src/lib/model/ai-job-creations.js +85 -0
- package/src/lib/model/ai-job-creations.js.map +1 -0
- package/src/lib/model/ai-page-job.d.ts +3 -2
- package/src/lib/model/ai-page-job.js +4 -3
- package/src/lib/model/ai-page-job.js.map +1 -1
- package/src/lib/model/ai-site-job.d.ts +4 -2
- package/src/lib/model/ai-site-job.js +8 -4
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-template-subjects.d.ts +10 -0
- package/src/lib/model/ai-template-subjects.js +14 -0
- package/src/lib/model/ai-template-subjects.js.map +1 -1
- package/src/lib/model/ai-tokens.js +1 -1
- package/src/lib/model/ai-tokens.js.map +1 -1
- package/src/lib/model/ai-usage-by-user.js +1 -1
- package/src/lib/model/ai-usage-by-user.js.map +1 -1
- package/src/lib/providers/catalog.js +1 -1
- package/src/lib/providers/catalog.js.map +1 -1
- package/src/lib/providers/model-choice.js +1 -1
- package/src/lib/providers/model-choice.js.map +1 -1
- package/src/lib/runtime/ai-device-audit.d.ts +1 -1
- package/src/lib/runtime/ai-device-audit.js +1 -1
- package/src/lib/runtime/ai-device-audit.js.map +1 -1
- package/src/lib/runtime/ai-eval-live.js +1 -1
- package/src/lib/runtime/ai-eval-live.js.map +1 -1
- package/src/lib/runtime/ai-eval.d.ts +1 -1
- package/src/lib/runtime/ai-eval.js.map +1 -1
- package/src/lib/runtime/ai-gate.js +1 -1
- package/src/lib/runtime/ai-gate.js.map +1 -1
- package/src/lib/runtime/ai-palette.d.ts +1 -1
- package/src/lib/runtime/ai-palette.generated.d.ts +1 -1
- package/src/lib/runtime/ai-palette.generated.js +1 -1
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/runtime/ai-palette.js.map +1 -1
- package/src/lib/runtime/site-inventory.js +28 -22
- package/src/lib/runtime/site-inventory.js.map +1 -1
- package/src/lib/server/ai-admin-org.js +1 -1
- package/src/lib/server/ai-admin-org.js.map +1 -1
- package/src/lib/server/ai-admin-orgs-spend.js +1 -1
- package/src/lib/server/ai-admin-orgs-spend.js.map +1 -1
- package/src/lib/server/ai-admin-signals.js +1 -1
- package/src/lib/server/ai-admin-signals.js.map +1 -1
- package/src/lib/server/ai-allotments.js +1 -1
- package/src/lib/server/ai-allotments.js.map +1 -1
- package/src/lib/server/ai-assist.js +2 -2
- package/src/lib/server/ai-assist.js.map +1 -1
- package/src/lib/server/assist-chat.d.ts +1 -1
- package/src/lib/server/assist-chat.js +2 -2
- package/src/lib/server/assist-chat.js.map +1 -1
- package/src/lib/server/billing-credits.d.ts +5 -0
- package/src/lib/server/billing-credits.js +8 -2
- package/src/lib/server/billing-credits.js.map +1 -1
- package/src/lib/server/billing-overage.js +2 -2
- package/src/lib/server/billing-overage.js.map +1 -1
- package/src/lib/testing/stand-in-dataset-index.d.ts +20 -0
- package/src/lib/testing/stand-in-dataset-index.js +80 -0
- package/src/lib/testing/stand-in-dataset-index.js.map +1 -0
- package/src/lib/usage/ai-allotment-alerts.d.ts +10 -7
- package/src/lib/usage/ai-allotment-alerts.js +31 -14
- package/src/lib/usage/ai-allotment-alerts.js.map +1 -1
- package/src/lib/usage/ai-quota-checks.d.ts +37 -0
- package/src/lib/usage/ai-quota-checks.js +69 -0
- package/src/lib/usage/ai-quota-checks.js.map +1 -0
- package/src/lib/usage/ai-usage-alerts.js +22 -10
- package/src/lib/usage/ai-usage-alerts.js.map +1 -1
- package/src/lib/usage/ai-usage-by-user.js +1 -1
- package/src/lib/usage/ai-usage-by-user.js.map +1 -1
- package/src/lib/usage/assist-ceiling.d.ts +2 -2
- package/src/lib/usage/assist-ceiling.js +4 -4
- package/src/lib/usage/assist-ceiling.js.map +1 -1
- package/src/lib/usage/assist-credits.d.ts +494 -0
- package/src/lib/usage/assist-credits.js +524 -0
- package/src/lib/usage/assist-credits.js.map +1 -0
- package/src/lib/usage/assist-free-taste.d.ts +1 -1
- package/src/lib/usage/assist-free-taste.js +1 -1
- package/src/lib/usage/assist-free-taste.js.map +1 -1
- package/src/lib/usage/assist-signal-mining.js +1 -1
- package/src/lib/usage/assist-signal-mining.js.map +1 -1
- package/src/lib/usage/assist-spend-guards.d.ts +153 -0
- package/src/lib/usage/assist-spend-guards.js +182 -0
- package/src/lib/usage/assist-spend-guards.js.map +1 -0
- package/src/lib/usage/assist-usage.d.ts +1 -1
- package/src/lib/usage/assist-usage.js +1 -1
- package/src/lib/usage/assist-usage.js.map +1 -1
- package/src/lib/usage/staff-org-ai.js +1 -1
- package/src/lib/usage/staff-org-ai.js.map +1 -1
- package/src/lib/usage-axes.d.ts +10 -0
- package/src/lib/usage-axes.js +40 -2
- package/src/lib/usage-axes.js.map +1 -1
|
@@ -145,7 +145,12 @@ export interface AiInsightDigestReport {
|
|
|
145
145
|
}
|
|
146
146
|
/** The week a Monday digest covers: the week the run falls in. */
|
|
147
147
|
export declare function aiInsightDigestWeek(now: Date): string;
|
|
148
|
-
/**
|
|
148
|
+
/**
|
|
149
|
+
* A digest email's words: the insights, the site's Analytics page, and where
|
|
150
|
+
* to turn it off. The digest is an AI job metered against the workspace, so
|
|
151
|
+
* it says what it drew, and it names the workspace a member of several asked
|
|
152
|
+
* for it in.
|
|
153
|
+
*/
|
|
149
154
|
export declare function aiInsightDigestEmailText(input: {
|
|
150
155
|
siteName: string;
|
|
151
156
|
productName: string;
|
|
@@ -155,6 +160,10 @@ export declare function aiInsightDigestEmailText(input: {
|
|
|
155
160
|
/** Absolute links, or `null` where no console origin is configured to build one from. */
|
|
156
161
|
analyticsUrl: string | null;
|
|
157
162
|
settingsUrl: string | null;
|
|
163
|
+
/** The workspace the digest was asked for in; `''` falls back to "this workspace". */
|
|
164
|
+
workspaceName?: string;
|
|
165
|
+
/** The AI credits the digest's job drew; 0 or absent says nothing. */
|
|
166
|
+
creditsSpent?: number;
|
|
158
167
|
}): string;
|
|
159
168
|
/**
|
|
160
169
|
* One call of the sweep: delivery for this week and the last on the first
|
|
@@ -44,15 +44,25 @@ const str = (value)=>typeof value === 'string' ? value.trim() : '';
|
|
|
44
44
|
/** The week before, whose late digests a run still delivers. */ function previousWeek(now) {
|
|
45
45
|
return aiInsightIsoWeek(new Date(now.getTime() - 7 * 86400000));
|
|
46
46
|
}
|
|
47
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* A digest email's words: the insights, the site's Analytics page, and where
|
|
49
|
+
* to turn it off. The digest is an AI job metered against the workspace, so
|
|
50
|
+
* it says what it drew, and it names the workspace a member of several asked
|
|
51
|
+
* for it in.
|
|
52
|
+
*/ export function aiInsightDigestEmailText(input) {
|
|
53
|
+
var _input_workspaceName;
|
|
54
|
+
const workspace = ((_input_workspaceName = input.workspaceName) == null ? void 0 : _input_workspaceName.trim()) || '';
|
|
55
|
+
const credits = Math.max(0, Math.round(Number(input.creditsSpent) || 0));
|
|
56
|
+
const drew = credits ? ` Writing it used ${credits.toLocaleString('en-US')} of ` + `${workspace ? `${workspace}’s` : 'the workspace’s'} AI credits.` : '';
|
|
57
|
+
const forWhom = workspace || 'this workspace';
|
|
48
58
|
return [
|
|
49
|
-
`Here is what ${input.siteName}'s figures showed this week
|
|
59
|
+
`Here is what ${input.siteName}'s figures showed this week.${drew}`,
|
|
50
60
|
'',
|
|
51
61
|
...input.insights.map((insight, index)=>`${index + 1}. ${insight.text}`),
|
|
52
62
|
'',
|
|
53
63
|
input.analyticsUrl ? `Each of these is traced to the figures it cites. See them in ${input.productName}: ${input.analyticsUrl}` : `Each of these is traced to the figures it cites. See them on the site's Analytics page in ${input.productName}.`,
|
|
54
64
|
'',
|
|
55
|
-
input.settingsUrl ? `You asked for weekly insights for
|
|
65
|
+
input.settingsUrl ? `You asked for weekly insights for ${forWhom}. Turn them off in Notifications: ${input.settingsUrl}` : `You asked for weekly insights for ${forWhom}. Turn them off in your Notifications settings.`
|
|
56
66
|
].join('\n');
|
|
57
67
|
}
|
|
58
68
|
async function readUsers(firestore, uids) {
|
|
@@ -228,7 +238,9 @@ async function readUsers(firestore, uids) {
|
|
|
228
238
|
productName: branding.productName,
|
|
229
239
|
insights: record.insights,
|
|
230
240
|
analyticsUrl,
|
|
231
|
-
settingsUrl: origin ? `${origin}/manage/notifications` : null
|
|
241
|
+
settingsUrl: origin ? `${origin}/manage/notifications` : null,
|
|
242
|
+
workspaceName: str(org['name']),
|
|
243
|
+
creditsSpent: job.creditsSpent
|
|
232
244
|
}),
|
|
233
245
|
fromName: branding.fromName,
|
|
234
246
|
siteName: site.name,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/insights/ai-insight-digest.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 { FieldPath } from 'firebase-admin/firestore'\nimport { INSIGHT_DIGESTS_FIELD, insightDigestSubscribed, notificationMuted } from '@aglyn/aglyn/app-utils/notifications'\nimport { hostRoleFor } from '@aglyn/aglyn/app-utils/organizations'\nimport { checkEntitlement, resolveBrandingProfile, resolveEffectivePlan } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { isReleaseFlagOnForOrg, parseOrgReleaseFlagOverrides, type ReleaseFlagValue } from '@aglyn/aglyn/app-utils/release-flags'\nimport type { AglynOrgMember } from '@aglyn/aglyn/foundation/definitions/organization.types'\nimport {\n AI_INSIGHTS_COLLECTION,\n aiInsightIsoWeek,\n type AiInsightRecord,\n} from '../model/ai-insight'\nimport { isAiOnForSite } from '../model/ai-site-switch'\nimport { cancelAiJob, createAiJob, getAiJob } from '../jobs/ai-jobs'\n\n/**\n * THE WEEKLY INSIGHTS (AGL-2915): `POST /api/admin/ai-insights-digest`, twice\n * a day on the console's scheduler.\n *\n * ## Who asked for it\n *\n * A person turns weekly insights on for a workspace beside the answers\n * themselves (`users/{uid}.insightDigests.{orgId}`), and off there or in\n * Notifications. It is opt-in, because each digest is a generation the\n * workspace pays for. At the sweep, every rung is asked again rather than\n * trusted from the switch: the workspace's plan includes `aiGenerative`,\n * `release_ai_generative` is on for it, its AI is not paused, the site has AI\n * on, and the person still holds `ai.generate` on the site.\n *\n * ## Monday: one job per site\n *\n * On a Monday run, for each workspace with someone who asked, the sites those\n * people reach — the busiest `AI_INSIGHT_DIGEST_MAX_SITES` by yesterday's page\n * views — each get one `insight` job with the `digest` surface, created by the\n * first of them who may generate there. The job runs on the AI jobs beat like\n * any other: it reserves, meters and records its credits, and a workspace at\n * its band — a Free one that has used its credits — has its job parked by the\n * reservation like any other. `orgs/{orgId}/aiInsightDigests/{week}` records\n * the jobs; `aiInsightDigestQueue/{week}` lists the workspaces still owed a\n * delivery, so a run that delivers reads one document rather than every\n * workspace.\n *\n * ## Every run: deliver what is written\n *\n * A digest whose job is done and kept at least one insight is delivered once:\n * a `content.insightsDigest` notification (unless the person muted the\n * category) and an email through the platform's own sending path, to each\n * person who asked for the workspace's insights and reaches the site. A job\n * that failed, kept nothing, or parked for credits is SKIPPED SILENTLY: a\n * parked job is canceled rather than left to spend later, and nobody is told,\n * because nobody asked about that week in particular. A job still running\n * past `AI_INSIGHT_DIGEST_DEADLINE_MS` is canceled and skipped the same way.\n *\n * The email says only what the answer says: the insights, each traced to the\n * figures it cites, and a link to the site's Analytics page. It is sent by the\n * platform to a person who asked for it, never to a contact.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport const AI_INSIGHTS_DIGEST_CRON_ID = 'ai-insights-digest'\nexport const AI_INSIGHTS_DIGEST_PATH = 'admin/ai-insights-digest'\n\n/** Orgs one call reads; a chunk that times out never advances the cursor, so the safe size is one that finishes. */\nexport const AI_INSIGHT_DIGEST_ORG_CHUNK = 25\n\n/** Sites one workspace's digest covers a week. */\nexport const AI_INSIGHT_DIGEST_MAX_SITES = 5\n\n/** Sites a workspace's sweep reads before it ranks them. */\nexport const AI_INSIGHT_DIGEST_HOST_READ_LIMIT = 50\n\n/** Workspaces one run delivers to, per week. */\nexport const AI_INSIGHT_DIGEST_DELIVER_LIMIT = 100\n\n/** How long a digest's job may take before its week is given up. */\nexport const AI_INSIGHT_DIGEST_DEADLINE_MS = 48 * 60 * 60_000\n\n/**\n * The time one call works before it stops and leaves the rest to the next:\n * inside the plugin dispatcher's 60 s function ceiling, with room for the\n * reads before the first check and the reply after the last. A call killed\n * mid-send could send one person a site's digest twice, because a send is\n * stamped after it goes.\n */\nexport const AI_INSIGHT_DIGEST_RUN_BUDGET_MS = 45_000\n\nexport const AI_INSIGHT_DIGEST_MARKERS = 'aiInsightDigests'\nexport const AI_INSIGHT_DIGEST_QUEUE = 'aiInsightDigestQueue'\n\nexport type AiInsightDigestSiteState = 'pending' | 'delivered' | 'skipped'\n\nexport interface AiInsightDigestSite {\n jobId: string\n name: string\n state: AiInsightDigestSiteState\n /**\n * The people already sent this site's digest, so a run the send rate\n * stopped never sends one twice. Dropped when the site settles, so a\n * settled marker names no person.\n */\n sent?: Record<string, boolean>\n}\n\nexport interface AiInsightDigestMarker {\n week: string\n createdAtMs: number\n sites: Record<string, AiInsightDigestSite>\n}\n\n/** What a send answered: whether it went, and whether the platform's send rate refused it for now. */\nexport interface AiInsightDigestSendResult {\n sent: boolean\n rateLimited: boolean\n}\n\n/** Everything the sweep reaches beyond Firestore, so a spec can hold each one. */\nexport interface AiInsightDigestDeps {\n firestore: Firestore\n now: Date\n flagValue: ReleaseFlagValue\n listMembers: (orgId: string) => Promise<AglynOrgMember[]>\n mayGenerate: (orgId: string, hostId: string, member: AglynOrgMember) => Promise<boolean>\n paused: (orgId: string) => Promise<boolean>\n notify: (uid: string, payload: { title: string; body: string; link: string; orgId: string; hostId: string }) => Promise<void>\n send: (email: {\n to: string\n subject: string\n text: string\n fromName: string\n /** The site the digest is about, and the org whose brand it wears (AGL-3367). */\n siteName: string\n org: Record<string, unknown>\n }) => Promise<AiInsightDigestSendResult>\n /** The console's absolute origin for an email link, or `''` when none is configured. */\n consoleOrigin: string\n /** The call's working time; `AI_INSIGHT_DIGEST_RUN_BUDGET_MS` unless a spec holds it. */\n budgetMs?: number\n}\n\nexport interface AiInsightDigestReport {\n week: string\n swept: number\n created: number\n delivered: number\n skipped: number\n /** The send rate refused or the call's time ran out; the run stops, and the next one delivers the rest. */\n deferred: boolean\n nextCursor: string | null\n done: boolean\n}\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/** The week a Monday digest covers: the week the run falls in. */\nexport function aiInsightDigestWeek(now: Date): string {\n return aiInsightIsoWeek(now)\n}\n\n/** The week before, whose late digests a run still delivers. */\nfunction previousWeek(now: Date): string {\n return aiInsightIsoWeek(new Date(now.getTime() - 7 * 86_400_000))\n}\n\n/** A digest email's words: the insights, the site's Analytics page, and where to turn it off. */\nexport function aiInsightDigestEmailText(input: {\n siteName: string\n productName: string\n insights: ReadonlyArray<{ text: string }>\n /** Absolute links, or `null` where no console origin is configured to build one from. */\n analyticsUrl: string | null\n settingsUrl: string | null\n}): string {\n return [\n `Here is what ${input.siteName}'s figures showed this week.`,\n '',\n ...input.insights.map((insight, index) => `${index + 1}. ${insight.text}`),\n '',\n input.analyticsUrl\n ? `Each of these is traced to the figures it cites. See them in ${input.productName}: ${input.analyticsUrl}`\n : `Each of these is traced to the figures it cites. See them on the site's Analytics page in ${input.productName}.`,\n '',\n input.settingsUrl\n ? `You asked for weekly insights for this workspace. Turn them off in Notifications: ${input.settingsUrl}`\n : `You asked for weekly insights for this workspace. Turn them off in your Notifications settings.`,\n ].join('\\n')\n}\n\nasync function readUsers(firestore: Firestore, uids: readonly string[]) {\n if (!uids.length) return new Map<string, FirebaseFirestore.DocumentSnapshot>()\n const docs = await firestore.getAll(...uids.map((uid) => firestore.collection('users').doc(uid)))\n return new Map(docs.map((doc) => [doc.id, doc]))\n}\n\n/** Creates one workspace's digests for the week; answers how many jobs it created. */\nasync function createOrgDigest(\n deps: AiInsightDigestDeps,\n orgDoc: FirebaseFirestore.QueryDocumentSnapshot,\n week: string,\n): Promise<number> {\n const { firestore, now } = deps\n const orgId = orgDoc.id\n const org = (orgDoc.data() ?? {}) as Record<string, unknown>\n if (!checkEntitlement(org as never, 'aiGenerative')) return 0\n const flagOn = isReleaseFlagOnForOrg(\n 'release_ai_generative',\n deps.flagValue,\n orgId,\n parseOrgReleaseFlagOverrides(org['releaseFlags']),\n resolveEffectivePlan(org as never),\n )\n if (!flagOn || (await deps.paused(orgId))) return 0\n const markerRef = orgDoc.ref.collection(AI_INSIGHT_DIGEST_MARKERS).doc(week)\n if ((await markerRef.get()).exists) return 0\n\n const members = (await deps.listMembers(orgId)).filter((member) => member.orgSuspended !== true)\n const users = await readUsers(firestore, members.map((member) => member.$id).sort())\n const subscribers = members\n .filter((member) => insightDigestSubscribed(users.get(member.$id)?.get(INSIGHT_DIGESTS_FIELD), orgId))\n .sort((a, b) => a.$id.localeCompare(b.$id))\n if (!subscribers.length) return 0\n\n const hosts = await firestore\n .collection('hosts')\n .where('orgId', '==', orgId)\n .limit(AI_INSIGHT_DIGEST_HOST_READ_LIMIT)\n .get()\n const yesterday = new Date(now.getTime() - 86_400_000).toISOString().slice(0, 10)\n const candidates: Array<{ hostId: string; name: string; creator: string; views: number }> = []\n for (const host of hosts.docs) {\n const data = host.data() ?? {}\n if (!isAiOnForSite(org, data)) continue\n let creator: string | null = null\n for (const member of subscribers) {\n if (hostRoleFor(member, host.id) !== null && (await deps.mayGenerate(orgId, host.id, member))) {\n creator = member.$id\n break\n }\n }\n if (!creator) continue\n const day = await host.ref.collection('analytics').doc(yesterday).get()\n candidates.push({\n hostId: host.id,\n name: str(data['displayName']) || str(data['name']) || str(data['subdomain']) || host.id,\n creator,\n views: Number(day.get('total') ?? 0) || 0,\n })\n }\n const chosen = candidates\n .sort((a, b) => b.views - a.views || a.hostId.localeCompare(b.hostId))\n .slice(0, AI_INSIGHT_DIGEST_MAX_SITES)\n if (!chosen.length) return 0\n\n const sites: Record<string, AiInsightDigestSite> = {}\n for (const site of chosen) {\n const job = await createAiJob(\n firestore,\n {\n orgId,\n hostId: site.hostId,\n kind: 'insight',\n brief: `Weekly insights for ${site.name}`,\n inputs: { surface: 'digest', week },\n createdBy: site.creator,\n },\n now,\n )\n sites[site.hostId] = { jobId: job.$id, name: site.name, state: 'pending' }\n }\n const marker: AiInsightDigestMarker = { week, createdAtMs: now.getTime(), sites }\n await markerRef.set(marker)\n await firestore\n .collection(AI_INSIGHT_DIGEST_QUEUE)\n .doc(week)\n .set({ week, pending: { [orgId]: true } }, { merge: true })\n return chosen.length\n}\n\n/** Delivers or settles one workspace's digests for a week; `deferred` when the send rate refused. */\nasync function deliverOrgDigest(\n deps: AiInsightDigestDeps,\n orgId: string,\n week: string,\n overBudget: () => boolean,\n): Promise<{ delivered: number; skipped: number; settled: boolean; deferred: boolean }> {\n const { firestore, now } = deps\n const orgRef = firestore.collection('orgs').doc(orgId)\n const markerRef = orgRef.collection(AI_INSIGHT_DIGEST_MARKERS).doc(week)\n const markerSnapshot = await markerRef.get()\n const result = { delivered: 0, skipped: 0, settled: true, deferred: false }\n if (!markerSnapshot.exists) return result\n const marker = markerSnapshot.data() as AiInsightDigestMarker\n const org = ((await orgRef.get()).data() ?? {}) as Record<string, unknown>\n let members: AglynOrgMember[] | null = null\n const branding = resolveBrandingProfile(org as never)\n\n for (const [hostId, site] of Object.entries(marker.sites ?? {})) {\n if (site.state !== 'pending') continue\n const settle = async (state: AiInsightDigestSiteState) => {\n // Replaced whole rather than merged, which drops `sent`.\n const settled: AiInsightDigestSite = { jobId: site.jobId, name: site.name, state }\n await markerRef.update({ [`sites.${hostId}`]: settled })\n if (state === 'delivered') result.delivered += 1\n else result.skipped += 1\n }\n const job = await getAiJob(firestore, orgId, site.jobId)\n if (!job || job.status === 'failed' || job.status === 'canceled') {\n await settle('skipped')\n continue\n }\n if (job.status !== 'done') {\n const late = now.getTime() - Number(marker.createdAtMs ?? 0) > AI_INSIGHT_DIGEST_DEADLINE_MS\n // A job parked for credits — a Free workspace at its band — is given up\n // rather than left to spend on a week that is already over.\n if (job.status === 'needs_input' || late) {\n await cancelAiJob(firestore, orgId, site.jobId, now)\n await settle('skipped')\n } else {\n result.settled = false\n }\n continue\n }\n const answer = await orgRef.collection(AI_INSIGHTS_COLLECTION).doc(site.jobId).get()\n const record = answer.exists ? (answer.data() as AiInsightRecord) : null\n if (!record?.insights?.length) {\n await settle('skipped')\n continue\n }\n\n members ??= (await deps.listMembers(orgId)).filter((member) => member.orgSuspended !== true)\n const reaching = members.filter((member) => hostRoleFor(member, hostId) !== null)\n const users = await readUsers(firestore, reaching.map((member) => member.$id).sort())\n const host = (await firestore.collection('hosts').doc(hostId).get()).data() ?? {}\n const orgSlug = str(org['slug'])\n const subdomain = str(host['subdomain'])\n const origin = deps.consoleOrigin\n const analyticsUrl = origin && orgSlug && subdomain ? `${origin}/${orgSlug}/hosts/${subdomain}/analytics` : null\n for (const member of reaching.sort((a, b) => a.$id.localeCompare(b.$id))) {\n const user = users.get(member.$id)\n if (!insightDigestSubscribed(user?.get(INSIGHT_DIGESTS_FIELD), orgId)) continue\n if (site.sent?.[member.$id]) continue\n if (overBudget()) {\n // Out of time: what was sent is stamped, and the next run sends the rest.\n result.deferred = true\n result.settled = false\n return result\n }\n const address = str(member.email ?? user?.get('email')).toLowerCase()\n if (address.includes('@')) {\n const sent = await deps.send({\n to: address,\n subject: `Your weekly insights for ${site.name}`,\n text: aiInsightDigestEmailText({\n siteName: site.name,\n productName: branding.productName,\n insights: record.insights,\n analyticsUrl,\n settingsUrl: origin ? `${origin}/manage/notifications` : null,\n }),\n fromName: branding.fromName,\n siteName: site.name,\n org,\n })\n if (sent.rateLimited) {\n // Nothing is marked, so the next run delivers this site whole.\n result.deferred = true\n result.settled = false\n return result\n }\n }\n if (!notificationMuted(user?.get('notificationPrefs'), 'content.insightsDigest')) {\n await deps.notify(member.$id, {\n title: `Weekly insights · ${site.name}`,\n body: record.insights[0].text,\n link: `/${hostId}/analytics`,\n orgId,\n hostId,\n })\n }\n await markerRef.set({ sites: { [hostId]: { sent: { [member.$id]: true } } } }, { merge: true })\n }\n await settle('delivered')\n }\n return result\n}\n\n/**\n * One call of the sweep: delivery for this week and the last on the first\n * call, then — on a Monday — one chunk of workspaces' creation, resumed by\n * `cursor`. The response shape is the console crons' (`nextCursor`, `done`),\n * so the scheduler's loop carries a platform of any size across calls.\n */\nexport async function runAiInsightDigestSweep(\n deps: AiInsightDigestDeps,\n options: { cursor: string | null },\n): Promise<AiInsightDigestReport> {\n const { firestore, now } = deps\n const startedAtMs = Date.now()\n const budgetMs = deps.budgetMs ?? AI_INSIGHT_DIGEST_RUN_BUDGET_MS\n const overBudget = () => Date.now() - startedAtMs > budgetMs\n const week = aiInsightDigestWeek(now)\n const report: AiInsightDigestReport = {\n week,\n swept: 0,\n created: 0,\n delivered: 0,\n skipped: 0,\n deferred: false,\n nextCursor: null,\n done: true,\n }\n\n if (!options.cursor) {\n for (const owed of [previousWeek(now), week]) {\n const queueRef = firestore.collection(AI_INSIGHT_DIGEST_QUEUE).doc(owed)\n const pending = Object.entries(((await queueRef.get()).get('pending') ?? {}) as Record<string, unknown>)\n .filter(([, owed]) => owed === true)\n .map(([orgId]) => orgId)\n .sort()\n .slice(0, AI_INSIGHT_DIGEST_DELIVER_LIMIT)\n for (const orgId of pending) {\n if (overBudget()) {\n report.deferred = true\n return report\n }\n try {\n const delivered = await deliverOrgDigest(deps, orgId, owed, overBudget)\n report.delivered += delivered.delivered\n report.skipped += delivered.skipped\n if (delivered.settled) {\n await queueRef.set({ pending: { [orgId]: false } }, { merge: true })\n }\n if (delivered.deferred) {\n report.deferred = true\n return report\n }\n } catch (error) {\n console.error('ai insight digest delivery failed', { orgId, week: owed, error })\n }\n }\n }\n }\n\n // Monday is the week's first day by ISO 8601, and the day a digest is made.\n if (now.getUTCDay() !== 1) return report\n\n let query = firestore.collection('orgs').orderBy(FieldPath.documentId()).limit(AI_INSIGHT_DIGEST_ORG_CHUNK + 1)\n if (options.cursor) query = query.startAfter(firestore.collection('orgs').doc(options.cursor))\n const page = await query.get()\n const orgs = page.docs.slice(0, AI_INSIGHT_DIGEST_ORG_CHUNK)\n for (const [index, orgDoc] of orgs.entries()) {\n if (overBudget()) {\n // Out of time: the next call resumes after the last workspace read. A\n // call that read none has no cursor to hand on, and the next scheduled\n // run starts the week's workspaces again, which the markers make safe.\n report.nextCursor = index ? orgs[index - 1].id : (options.cursor ?? null)\n report.done = report.nextCursor === null\n return report\n }\n report.swept += 1\n try {\n report.created += await createOrgDigest(deps, orgDoc, week)\n } catch (error) {\n console.error('ai insight digest creation failed', { orgId: orgDoc.id, week, error })\n }\n }\n const more = page.docs.length > AI_INSIGHT_DIGEST_ORG_CHUNK\n report.nextCursor = more ? (orgs[orgs.length - 1]?.id ?? null) : null\n report.done = !more\n return report\n}\n"],"names":["FieldPath","INSIGHT_DIGESTS_FIELD","insightDigestSubscribed","notificationMuted","hostRoleFor","checkEntitlement","resolveBrandingProfile","resolveEffectivePlan","isReleaseFlagOnForOrg","parseOrgReleaseFlagOverrides","AI_INSIGHTS_COLLECTION","aiInsightIsoWeek","isAiOnForSite","cancelAiJob","createAiJob","getAiJob","AI_INSIGHTS_DIGEST_CRON_ID","AI_INSIGHTS_DIGEST_PATH","AI_INSIGHT_DIGEST_ORG_CHUNK","AI_INSIGHT_DIGEST_MAX_SITES","AI_INSIGHT_DIGEST_HOST_READ_LIMIT","AI_INSIGHT_DIGEST_DELIVER_LIMIT","AI_INSIGHT_DIGEST_DEADLINE_MS","AI_INSIGHT_DIGEST_RUN_BUDGET_MS","AI_INSIGHT_DIGEST_MARKERS","AI_INSIGHT_DIGEST_QUEUE","str","value","trim","aiInsightDigestWeek","now","previousWeek","Date","getTime","aiInsightDigestEmailText","input","siteName","insights","map","insight","index","text","analyticsUrl","productName","settingsUrl","join","readUsers","firestore","uids","length","Map","docs","getAll","uid","collection","doc","id","createOrgDigest","deps","orgDoc","week","orgId","org","data","flagOn","flagValue","paused","markerRef","ref","get","exists","members","listMembers","filter","member","orgSuspended","users","$id","sort","subscribers","a","b","localeCompare","hosts","where","limit","yesterday","toISOString","slice","candidates","host","day","creator","mayGenerate","push","hostId","name","views","Number","chosen","sites","site","job","kind","brief","inputs","surface","createdBy","jobId","state","marker","createdAtMs","set","pending","merge","deliverOrgDigest","overBudget","orgRef","markerSnapshot","result","delivered","skipped","settled","deferred","branding","Object","entries","record","settle","update","status","late","answer","reaching","orgSlug","subdomain","origin","consoleOrigin","user","sent","address","email","toLowerCase","includes","send","to","subject","fromName","rateLimited","notify","title","body","link","runAiInsightDigestSweep","options","orgs","startedAtMs","budgetMs","report","swept","created","nextCursor","done","cursor","owed","queueRef","error","console","getUTCDay","query","orderBy","documentId","startAfter","page","more"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/insights/ai-insight-digest.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 { FieldPath } from 'firebase-admin/firestore'\nimport { INSIGHT_DIGESTS_FIELD, insightDigestSubscribed, notificationMuted } from '@aglyn/aglyn/app-utils/notifications'\nimport { hostRoleFor } from '@aglyn/aglyn/app-utils/organizations'\nimport { checkEntitlement, resolveBrandingProfile, resolveEffectivePlan } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { isReleaseFlagOnForOrg, parseOrgReleaseFlagOverrides, type ReleaseFlagValue } from '@aglyn/aglyn/app-utils/release-flags'\nimport type { AglynOrgMember } from '@aglyn/aglyn/foundation/definitions/organization.types'\nimport {\n AI_INSIGHTS_COLLECTION,\n aiInsightIsoWeek,\n type AiInsightRecord,\n} from '../model/ai-insight'\nimport { isAiOnForSite } from '../model/ai-site-switch'\nimport { cancelAiJob, createAiJob, getAiJob } from '../jobs/ai-jobs'\n\n/**\n * THE WEEKLY INSIGHTS (AGL-2915): `POST /api/admin/ai-insights-digest`, twice\n * a day on the console's scheduler.\n *\n * ## Who asked for it\n *\n * A person turns weekly insights on for a workspace beside the answers\n * themselves (`users/{uid}.insightDigests.{orgId}`), and off there or in\n * Notifications. It is opt-in, because each digest is a generation the\n * workspace pays for. At the sweep, every rung is asked again rather than\n * trusted from the switch: the workspace's plan includes `aiGenerative`,\n * `release_ai_generative` is on for it, its AI is not paused, the site has AI\n * on, and the person still holds `ai.generate` on the site.\n *\n * ## Monday: one job per site\n *\n * On a Monday run, for each workspace with someone who asked, the sites those\n * people reach — the busiest `AI_INSIGHT_DIGEST_MAX_SITES` by yesterday's page\n * views — each get one `insight` job with the `digest` surface, created by the\n * first of them who may generate there. The job runs on the AI jobs beat like\n * any other: it reserves, meters and records its credits, and a workspace at\n * its band — a Free one that has used its credits — has its job parked by the\n * reservation like any other. `orgs/{orgId}/aiInsightDigests/{week}` records\n * the jobs; `aiInsightDigestQueue/{week}` lists the workspaces still owed a\n * delivery, so a run that delivers reads one document rather than every\n * workspace.\n *\n * ## Every run: deliver what is written\n *\n * A digest whose job is done and kept at least one insight is delivered once:\n * a `content.insightsDigest` notification (unless the person muted the\n * category) and an email through the platform's own sending path, to each\n * person who asked for the workspace's insights and reaches the site. A job\n * that failed, kept nothing, or parked for credits is SKIPPED SILENTLY: a\n * parked job is canceled rather than left to spend later, and nobody is told,\n * because nobody asked about that week in particular. A job still running\n * past `AI_INSIGHT_DIGEST_DEADLINE_MS` is canceled and skipped the same way.\n *\n * The email says only what the answer says: the insights, each traced to the\n * figures it cites, and a link to the site's Analytics page. It is sent by the\n * platform to a person who asked for it, never to a contact.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport const AI_INSIGHTS_DIGEST_CRON_ID = 'ai-insights-digest'\nexport const AI_INSIGHTS_DIGEST_PATH = 'admin/ai-insights-digest'\n\n/** Orgs one call reads; a chunk that times out never advances the cursor, so the safe size is one that finishes. */\nexport const AI_INSIGHT_DIGEST_ORG_CHUNK = 25\n\n/** Sites one workspace's digest covers a week. */\nexport const AI_INSIGHT_DIGEST_MAX_SITES = 5\n\n/** Sites a workspace's sweep reads before it ranks them. */\nexport const AI_INSIGHT_DIGEST_HOST_READ_LIMIT = 50\n\n/** Workspaces one run delivers to, per week. */\nexport const AI_INSIGHT_DIGEST_DELIVER_LIMIT = 100\n\n/** How long a digest's job may take before its week is given up. */\nexport const AI_INSIGHT_DIGEST_DEADLINE_MS = 48 * 60 * 60_000\n\n/**\n * The time one call works before it stops and leaves the rest to the next:\n * inside the plugin dispatcher's 60 s function ceiling, with room for the\n * reads before the first check and the reply after the last. A call killed\n * mid-send could send one person a site's digest twice, because a send is\n * stamped after it goes.\n */\nexport const AI_INSIGHT_DIGEST_RUN_BUDGET_MS = 45_000\n\nexport const AI_INSIGHT_DIGEST_MARKERS = 'aiInsightDigests'\nexport const AI_INSIGHT_DIGEST_QUEUE = 'aiInsightDigestQueue'\n\nexport type AiInsightDigestSiteState = 'pending' | 'delivered' | 'skipped'\n\nexport interface AiInsightDigestSite {\n jobId: string\n name: string\n state: AiInsightDigestSiteState\n /**\n * The people already sent this site's digest, so a run the send rate\n * stopped never sends one twice. Dropped when the site settles, so a\n * settled marker names no person.\n */\n sent?: Record<string, boolean>\n}\n\nexport interface AiInsightDigestMarker {\n week: string\n createdAtMs: number\n sites: Record<string, AiInsightDigestSite>\n}\n\n/** What a send answered: whether it went, and whether the platform's send rate refused it for now. */\nexport interface AiInsightDigestSendResult {\n sent: boolean\n rateLimited: boolean\n}\n\n/** Everything the sweep reaches beyond Firestore, so a spec can hold each one. */\nexport interface AiInsightDigestDeps {\n firestore: Firestore\n now: Date\n flagValue: ReleaseFlagValue\n listMembers: (orgId: string) => Promise<AglynOrgMember[]>\n mayGenerate: (orgId: string, hostId: string, member: AglynOrgMember) => Promise<boolean>\n paused: (orgId: string) => Promise<boolean>\n notify: (uid: string, payload: { title: string; body: string; link: string; orgId: string; hostId: string }) => Promise<void>\n send: (email: {\n to: string\n subject: string\n text: string\n fromName: string\n /** The site the digest is about, and the org whose brand it wears (AGL-3367). */\n siteName: string\n org: Record<string, unknown>\n }) => Promise<AiInsightDigestSendResult>\n /** The console's absolute origin for an email link, or `''` when none is configured. */\n consoleOrigin: string\n /** The call's working time; `AI_INSIGHT_DIGEST_RUN_BUDGET_MS` unless a spec holds it. */\n budgetMs?: number\n}\n\nexport interface AiInsightDigestReport {\n week: string\n swept: number\n created: number\n delivered: number\n skipped: number\n /** The send rate refused or the call's time ran out; the run stops, and the next one delivers the rest. */\n deferred: boolean\n nextCursor: string | null\n done: boolean\n}\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/** The week a Monday digest covers: the week the run falls in. */\nexport function aiInsightDigestWeek(now: Date): string {\n return aiInsightIsoWeek(now)\n}\n\n/** The week before, whose late digests a run still delivers. */\nfunction previousWeek(now: Date): string {\n return aiInsightIsoWeek(new Date(now.getTime() - 7 * 86_400_000))\n}\n\n/**\n * A digest email's words: the insights, the site's Analytics page, and where\n * to turn it off. The digest is an AI job metered against the workspace, so\n * it says what it drew, and it names the workspace a member of several asked\n * for it in.\n */\nexport function aiInsightDigestEmailText(input: {\n siteName: string\n productName: string\n insights: ReadonlyArray<{ text: string }>\n /** Absolute links, or `null` where no console origin is configured to build one from. */\n analyticsUrl: string | null\n settingsUrl: string | null\n /** The workspace the digest was asked for in; `''` falls back to \"this workspace\". */\n workspaceName?: string\n /** The AI credits the digest's job drew; 0 or absent says nothing. */\n creditsSpent?: number\n}): string {\n const workspace = input.workspaceName?.trim() || ''\n const credits = Math.max(0, Math.round(Number(input.creditsSpent) || 0))\n const drew = credits\n ? ` Writing it used ${credits.toLocaleString('en-US')} of ` +\n `${workspace ? `${workspace}’s` : 'the workspace’s'} AI credits.`\n : ''\n const forWhom = workspace || 'this workspace'\n return [\n `Here is what ${input.siteName}'s figures showed this week.${drew}`,\n '',\n ...input.insights.map((insight, index) => `${index + 1}. ${insight.text}`),\n '',\n input.analyticsUrl\n ? `Each of these is traced to the figures it cites. See them in ${input.productName}: ${input.analyticsUrl}`\n : `Each of these is traced to the figures it cites. See them on the site's Analytics page in ${input.productName}.`,\n '',\n input.settingsUrl\n ? `You asked for weekly insights for ${forWhom}. Turn them off in Notifications: ${input.settingsUrl}`\n : `You asked for weekly insights for ${forWhom}. Turn them off in your Notifications settings.`,\n ].join('\\n')\n}\n\nasync function readUsers(firestore: Firestore, uids: readonly string[]) {\n if (!uids.length) return new Map<string, FirebaseFirestore.DocumentSnapshot>()\n const docs = await firestore.getAll(...uids.map((uid) => firestore.collection('users').doc(uid)))\n return new Map(docs.map((doc) => [doc.id, doc]))\n}\n\n/** Creates one workspace's digests for the week; answers how many jobs it created. */\nasync function createOrgDigest(\n deps: AiInsightDigestDeps,\n orgDoc: FirebaseFirestore.QueryDocumentSnapshot,\n week: string,\n): Promise<number> {\n const { firestore, now } = deps\n const orgId = orgDoc.id\n const org = (orgDoc.data() ?? {}) as Record<string, unknown>\n if (!checkEntitlement(org as never, 'aiGenerative')) return 0\n const flagOn = isReleaseFlagOnForOrg(\n 'release_ai_generative',\n deps.flagValue,\n orgId,\n parseOrgReleaseFlagOverrides(org['releaseFlags']),\n resolveEffectivePlan(org as never),\n )\n if (!flagOn || (await deps.paused(orgId))) return 0\n const markerRef = orgDoc.ref.collection(AI_INSIGHT_DIGEST_MARKERS).doc(week)\n if ((await markerRef.get()).exists) return 0\n\n const members = (await deps.listMembers(orgId)).filter((member) => member.orgSuspended !== true)\n const users = await readUsers(firestore, members.map((member) => member.$id).sort())\n const subscribers = members\n .filter((member) => insightDigestSubscribed(users.get(member.$id)?.get(INSIGHT_DIGESTS_FIELD), orgId))\n .sort((a, b) => a.$id.localeCompare(b.$id))\n if (!subscribers.length) return 0\n\n const hosts = await firestore\n .collection('hosts')\n .where('orgId', '==', orgId)\n .limit(AI_INSIGHT_DIGEST_HOST_READ_LIMIT)\n .get()\n const yesterday = new Date(now.getTime() - 86_400_000).toISOString().slice(0, 10)\n const candidates: Array<{ hostId: string; name: string; creator: string; views: number }> = []\n for (const host of hosts.docs) {\n const data = host.data() ?? {}\n if (!isAiOnForSite(org, data)) continue\n let creator: string | null = null\n for (const member of subscribers) {\n if (hostRoleFor(member, host.id) !== null && (await deps.mayGenerate(orgId, host.id, member))) {\n creator = member.$id\n break\n }\n }\n if (!creator) continue\n const day = await host.ref.collection('analytics').doc(yesterday).get()\n candidates.push({\n hostId: host.id,\n name: str(data['displayName']) || str(data['name']) || str(data['subdomain']) || host.id,\n creator,\n views: Number(day.get('total') ?? 0) || 0,\n })\n }\n const chosen = candidates\n .sort((a, b) => b.views - a.views || a.hostId.localeCompare(b.hostId))\n .slice(0, AI_INSIGHT_DIGEST_MAX_SITES)\n if (!chosen.length) return 0\n\n const sites: Record<string, AiInsightDigestSite> = {}\n for (const site of chosen) {\n const job = await createAiJob(\n firestore,\n {\n orgId,\n hostId: site.hostId,\n kind: 'insight',\n brief: `Weekly insights for ${site.name}`,\n inputs: { surface: 'digest', week },\n createdBy: site.creator,\n },\n now,\n )\n sites[site.hostId] = { jobId: job.$id, name: site.name, state: 'pending' }\n }\n const marker: AiInsightDigestMarker = { week, createdAtMs: now.getTime(), sites }\n await markerRef.set(marker)\n await firestore\n .collection(AI_INSIGHT_DIGEST_QUEUE)\n .doc(week)\n .set({ week, pending: { [orgId]: true } }, { merge: true })\n return chosen.length\n}\n\n/** Delivers or settles one workspace's digests for a week; `deferred` when the send rate refused. */\nasync function deliverOrgDigest(\n deps: AiInsightDigestDeps,\n orgId: string,\n week: string,\n overBudget: () => boolean,\n): Promise<{ delivered: number; skipped: number; settled: boolean; deferred: boolean }> {\n const { firestore, now } = deps\n const orgRef = firestore.collection('orgs').doc(orgId)\n const markerRef = orgRef.collection(AI_INSIGHT_DIGEST_MARKERS).doc(week)\n const markerSnapshot = await markerRef.get()\n const result = { delivered: 0, skipped: 0, settled: true, deferred: false }\n if (!markerSnapshot.exists) return result\n const marker = markerSnapshot.data() as AiInsightDigestMarker\n const org = ((await orgRef.get()).data() ?? {}) as Record<string, unknown>\n let members: AglynOrgMember[] | null = null\n const branding = resolveBrandingProfile(org as never)\n\n for (const [hostId, site] of Object.entries(marker.sites ?? {})) {\n if (site.state !== 'pending') continue\n const settle = async (state: AiInsightDigestSiteState) => {\n // Replaced whole rather than merged, which drops `sent`.\n const settled: AiInsightDigestSite = { jobId: site.jobId, name: site.name, state }\n await markerRef.update({ [`sites.${hostId}`]: settled })\n if (state === 'delivered') result.delivered += 1\n else result.skipped += 1\n }\n const job = await getAiJob(firestore, orgId, site.jobId)\n if (!job || job.status === 'failed' || job.status === 'canceled') {\n await settle('skipped')\n continue\n }\n if (job.status !== 'done') {\n const late = now.getTime() - Number(marker.createdAtMs ?? 0) > AI_INSIGHT_DIGEST_DEADLINE_MS\n // A job parked for credits — a Free workspace at its band — is given up\n // rather than left to spend on a week that is already over.\n if (job.status === 'needs_input' || late) {\n await cancelAiJob(firestore, orgId, site.jobId, now)\n await settle('skipped')\n } else {\n result.settled = false\n }\n continue\n }\n const answer = await orgRef.collection(AI_INSIGHTS_COLLECTION).doc(site.jobId).get()\n const record = answer.exists ? (answer.data() as AiInsightRecord) : null\n if (!record?.insights?.length) {\n await settle('skipped')\n continue\n }\n\n members ??= (await deps.listMembers(orgId)).filter((member) => member.orgSuspended !== true)\n const reaching = members.filter((member) => hostRoleFor(member, hostId) !== null)\n const users = await readUsers(firestore, reaching.map((member) => member.$id).sort())\n const host = (await firestore.collection('hosts').doc(hostId).get()).data() ?? {}\n const orgSlug = str(org['slug'])\n const subdomain = str(host['subdomain'])\n const origin = deps.consoleOrigin\n const analyticsUrl = origin && orgSlug && subdomain ? `${origin}/${orgSlug}/hosts/${subdomain}/analytics` : null\n for (const member of reaching.sort((a, b) => a.$id.localeCompare(b.$id))) {\n const user = users.get(member.$id)\n if (!insightDigestSubscribed(user?.get(INSIGHT_DIGESTS_FIELD), orgId)) continue\n if (site.sent?.[member.$id]) continue\n if (overBudget()) {\n // Out of time: what was sent is stamped, and the next run sends the rest.\n result.deferred = true\n result.settled = false\n return result\n }\n const address = str(member.email ?? user?.get('email')).toLowerCase()\n if (address.includes('@')) {\n const sent = await deps.send({\n to: address,\n subject: `Your weekly insights for ${site.name}`,\n text: aiInsightDigestEmailText({\n siteName: site.name,\n productName: branding.productName,\n insights: record.insights,\n analyticsUrl,\n settingsUrl: origin ? `${origin}/manage/notifications` : null,\n workspaceName: str(org['name']),\n creditsSpent: job.creditsSpent,\n }),\n fromName: branding.fromName,\n siteName: site.name,\n org,\n })\n if (sent.rateLimited) {\n // Nothing is marked, so the next run delivers this site whole.\n result.deferred = true\n result.settled = false\n return result\n }\n }\n if (!notificationMuted(user?.get('notificationPrefs'), 'content.insightsDigest')) {\n await deps.notify(member.$id, {\n title: `Weekly insights · ${site.name}`,\n body: record.insights[0].text,\n link: `/${hostId}/analytics`,\n orgId,\n hostId,\n })\n }\n await markerRef.set({ sites: { [hostId]: { sent: { [member.$id]: true } } } }, { merge: true })\n }\n await settle('delivered')\n }\n return result\n}\n\n/**\n * One call of the sweep: delivery for this week and the last on the first\n * call, then — on a Monday — one chunk of workspaces' creation, resumed by\n * `cursor`. The response shape is the console crons' (`nextCursor`, `done`),\n * so the scheduler's loop carries a platform of any size across calls.\n */\nexport async function runAiInsightDigestSweep(\n deps: AiInsightDigestDeps,\n options: { cursor: string | null },\n): Promise<AiInsightDigestReport> {\n const { firestore, now } = deps\n const startedAtMs = Date.now()\n const budgetMs = deps.budgetMs ?? AI_INSIGHT_DIGEST_RUN_BUDGET_MS\n const overBudget = () => Date.now() - startedAtMs > budgetMs\n const week = aiInsightDigestWeek(now)\n const report: AiInsightDigestReport = {\n week,\n swept: 0,\n created: 0,\n delivered: 0,\n skipped: 0,\n deferred: false,\n nextCursor: null,\n done: true,\n }\n\n if (!options.cursor) {\n for (const owed of [previousWeek(now), week]) {\n const queueRef = firestore.collection(AI_INSIGHT_DIGEST_QUEUE).doc(owed)\n const pending = Object.entries(((await queueRef.get()).get('pending') ?? {}) as Record<string, unknown>)\n .filter(([, owed]) => owed === true)\n .map(([orgId]) => orgId)\n .sort()\n .slice(0, AI_INSIGHT_DIGEST_DELIVER_LIMIT)\n for (const orgId of pending) {\n if (overBudget()) {\n report.deferred = true\n return report\n }\n try {\n const delivered = await deliverOrgDigest(deps, orgId, owed, overBudget)\n report.delivered += delivered.delivered\n report.skipped += delivered.skipped\n if (delivered.settled) {\n await queueRef.set({ pending: { [orgId]: false } }, { merge: true })\n }\n if (delivered.deferred) {\n report.deferred = true\n return report\n }\n } catch (error) {\n console.error('ai insight digest delivery failed', { orgId, week: owed, error })\n }\n }\n }\n }\n\n // Monday is the week's first day by ISO 8601, and the day a digest is made.\n if (now.getUTCDay() !== 1) return report\n\n let query = firestore.collection('orgs').orderBy(FieldPath.documentId()).limit(AI_INSIGHT_DIGEST_ORG_CHUNK + 1)\n if (options.cursor) query = query.startAfter(firestore.collection('orgs').doc(options.cursor))\n const page = await query.get()\n const orgs = page.docs.slice(0, AI_INSIGHT_DIGEST_ORG_CHUNK)\n for (const [index, orgDoc] of orgs.entries()) {\n if (overBudget()) {\n // Out of time: the next call resumes after the last workspace read. A\n // call that read none has no cursor to hand on, and the next scheduled\n // run starts the week's workspaces again, which the markers make safe.\n report.nextCursor = index ? orgs[index - 1].id : (options.cursor ?? null)\n report.done = report.nextCursor === null\n return report\n }\n report.swept += 1\n try {\n report.created += await createOrgDigest(deps, orgDoc, week)\n } catch (error) {\n console.error('ai insight digest creation failed', { orgId: orgDoc.id, week, error })\n }\n }\n const more = page.docs.length > AI_INSIGHT_DIGEST_ORG_CHUNK\n report.nextCursor = more ? (orgs[orgs.length - 1]?.id ?? null) : null\n report.done = !more\n return report\n}\n"],"names":["FieldPath","INSIGHT_DIGESTS_FIELD","insightDigestSubscribed","notificationMuted","hostRoleFor","checkEntitlement","resolveBrandingProfile","resolveEffectivePlan","isReleaseFlagOnForOrg","parseOrgReleaseFlagOverrides","AI_INSIGHTS_COLLECTION","aiInsightIsoWeek","isAiOnForSite","cancelAiJob","createAiJob","getAiJob","AI_INSIGHTS_DIGEST_CRON_ID","AI_INSIGHTS_DIGEST_PATH","AI_INSIGHT_DIGEST_ORG_CHUNK","AI_INSIGHT_DIGEST_MAX_SITES","AI_INSIGHT_DIGEST_HOST_READ_LIMIT","AI_INSIGHT_DIGEST_DELIVER_LIMIT","AI_INSIGHT_DIGEST_DEADLINE_MS","AI_INSIGHT_DIGEST_RUN_BUDGET_MS","AI_INSIGHT_DIGEST_MARKERS","AI_INSIGHT_DIGEST_QUEUE","str","value","trim","aiInsightDigestWeek","now","previousWeek","Date","getTime","aiInsightDigestEmailText","input","workspace","workspaceName","credits","Math","max","round","Number","creditsSpent","drew","toLocaleString","forWhom","siteName","insights","map","insight","index","text","analyticsUrl","productName","settingsUrl","join","readUsers","firestore","uids","length","Map","docs","getAll","uid","collection","doc","id","createOrgDigest","deps","orgDoc","week","orgId","org","data","flagOn","flagValue","paused","markerRef","ref","get","exists","members","listMembers","filter","member","orgSuspended","users","$id","sort","subscribers","a","b","localeCompare","hosts","where","limit","yesterday","toISOString","slice","candidates","host","day","creator","mayGenerate","push","hostId","name","views","chosen","sites","site","job","kind","brief","inputs","surface","createdBy","jobId","state","marker","createdAtMs","set","pending","merge","deliverOrgDigest","overBudget","orgRef","markerSnapshot","result","delivered","skipped","settled","deferred","branding","Object","entries","record","settle","update","status","late","answer","reaching","orgSlug","subdomain","origin","consoleOrigin","user","sent","address","email","toLowerCase","includes","send","to","subject","fromName","rateLimited","notify","title","body","link","runAiInsightDigestSweep","options","orgs","startedAtMs","budgetMs","report","swept","created","nextCursor","done","cursor","owed","queueRef","error","console","getUTCDay","query","orderBy","documentId","startAfter","page","more"],"mappings":"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"}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import { type ResolvedPluginFigureReader } from '@aglyn/aglyn/plugin-manager/plugin-figures';
|
|
18
18
|
import { type AiInsightSurface, type AiInsightTable } from '../model/ai-insight';
|
|
19
19
|
import type { AiInsightReadRequest } from '../tools/ai-insight-tool';
|
|
20
|
-
import type { AiDatasetCatalogEntry } from './ai-
|
|
20
|
+
import type { AiDatasetCatalogEntry } from './ai-dataset-catalog';
|
|
21
21
|
/**
|
|
22
22
|
* Which figure readers an insight job may read, and reading them (AGL-2915).
|
|
23
23
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/insights/ai-insight-readers.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 { isHostPluginEnabled, isPluginEnabled } from '@aglyn/aglyn/plugin-manager/enabled-plugins'\nimport {\n listPluginFigureReaders,\n normalizePluginFigureTable,\n pluginFigureParams,\n pluginFigureReaderPlugin,\n type ResolvedPluginFigureReader,\n} from '@aglyn/aglyn/plugin-manager/plugin-figures'\nimport {\n AI_INSIGHT_MAX_READS,\n AI_INSIGHT_SURFACE_READERS,\n aiInsightCellText,\n type AiInsightSurface,\n type AiInsightTable,\n} from '../model/ai-insight'\nimport { filterEnabledPluginsByReleaseFlags } from '@aglyn/tenant-data-admin/server/release-flags'\nimport type { AiInsightReadRequest } from '../tools/ai-insight-tool'\nimport type { AiDatasetCatalogEntry } from './ai-figure-readers'\n\n/**\n * Which figure readers an insight job may read, and reading them (AGL-2915).\n *\n * The caller's half of the `plugin-figures` contract: the seam carries no\n * authorization, so before a reader is offered to the model — let alone read\n * — this establishes that the surface asks about its kind of figures, that\n * the workspace's plan includes the feature the figures are sold under, and\n * that the plugin whose records they are runs here: past its release flag,\n * on for the workspace and not switched off for the site. The site belonging\n * to the job's workspace, and the member's reach, are the job's own checks.\n */\n\n/** Which of a set of plugins are past their release flags for an org; the platform's flags by default. */\nexport type AiReleasedPlugins = (pluginIds: string[], orgId: string) => Promise<string[]>\n\nconst releasedByFlags: AiReleasedPlugins = (pluginIds, orgId) =>\n filterEnabledPluginsByReleaseFlags(pluginIds, { orgId, authorization: null })\n\nexport interface AiInsightReaderContext {\n orgId: string\n /** The site the figures are about; `null` for a workspace-level question. */\n hostId: string | null\n /** The org document the machine or the door read. */\n org: object | null\n /** The site's host document, already checked to belong to the org; `null` with no site. */\n host: Record<string, unknown> | null\n surface: AiInsightSurface\n released?: AiReleasedPlugins\n}\n\n/** The readers a job on this surface may read, ordered by id so a prompt lists them in the same order every time. */\nexport async function aiInsightReaders(context: AiInsightReaderContext): Promise<ResolvedPluginFigureReader[]> {\n const prefixes = AI_INSIGHT_SURFACE_READERS[context.surface]\n const org = context.org as { enabledPlugins?: string[] } | null\n const candidates = listPluginFigureReaders().filter(({ reader }) => {\n if (!prefixes.includes(reader.id.split('.')[0])) return false\n // A site reader needs a site; a workspace reader reads with or without one.\n if (reader.scope === 'site' && !context.hostId) return false\n return !reader.feature || checkEntitlement(org as never, reader.feature)\n })\n const plugins = [...new Set(candidates.map(pluginFigureReaderPlugin).filter((id): id is string => Boolean(id)))]\n const released = new Set(plugins.length ? await (context.released ?? releasedByFlags)(plugins, context.orgId) : [])\n return candidates.filter((resolved) => {\n const plugin = pluginFigureReaderPlugin(resolved)\n if (!plugin) return true\n if (!released.has(plugin)) return false\n return context.hostId\n ? isHostPluginEnabled(org, context.host as { disabledPlugins?: string[] } | null, plugin)\n : isPluginEnabled(org, plugin)\n })\n}\n\n/** The window a reader is read over: the one asked for when it covers it, else the nearest it does. */\nexport function aiInsightReaderDays(windows: readonly number[], asked: number): number {\n if (!windows.length) return 0\n if (windows.includes(asked)) return asked\n return [...windows].sort((a, b) => Math.abs(a - asked) - Math.abs(b - asked) || a - b)[0]\n}\n\n/** The readers as the read prompt lists them: id, what each answers, its windows and its parameters. */\nexport function aiInsightReaderCatalog(readers: readonly ResolvedPluginFigureReader[]): string {\n return readers\n .map(({ reader }) => {\n const windows = reader.windows.length\n ? ` Windows: ${reader.windows.join(', ')} days.`\n : ' Current totals, with no window: days 0.'\n const params = (reader.params ?? []).length\n ? ` Parameters: ${(reader.params ?? [])\n .map((param) => `${param.name} (${param.required ? 'required' : 'optional'}) — ${param.description}`)\n .join('; ')}`\n : ''\n return `- ${reader.id} — ${reader.label}: ${reader.description}${windows}${params}`\n })\n .join('\\n')\n}\n\n/** The datasets a datasets question may name, as the read prompt lists them. */\nexport function aiInsightDatasetCatalog(datasets: readonly AiDatasetCatalogEntry[]): string {\n if (!datasets.length) return 'There are no datasets you may read here.'\n return datasets\n .map(\n (dataset) =>\n `- ${dataset.name} (id ${dataset.id}): ${dataset.fields.map((field) => `${field.name} [${field.type}]`).join(', ') || 'no fields'}`,\n )\n .join('\\n')\n}\n\nexport interface AiInsightReadResult {\n tables: AiInsightTable[]\n /** Why a request that named a real reader was not read, in its owner's words, for the log. */\n refusals: string[]\n}\n\n/**\n * The tables for the reads a job asked for: each request held to the readers\n * this job may read, its window to that reader's, its parameters to those it\n * declares; each table held to the contract by `normalizePluginFigureTable`.\n * A reader asked for twice with the same window and parameters is read once.\n */\nexport async function readAiInsightTables(\n readers: readonly ResolvedPluginFigureReader[],\n requests: readonly AiInsightReadRequest[],\n base: { orgId: string; hostId: string | null; uid: string | null; now: Date },\n maxTables: number = AI_INSIGHT_MAX_READS,\n): Promise<AiInsightReadResult> {\n const tables: AiInsightTable[] = []\n const refusals: string[] = []\n const seen = new Set<string>()\n for (const request of requests) {\n if (tables.length === maxTables) break\n const resolved = readers.find(({ reader }) => reader.id === request.reader)\n if (!resolved) continue\n const { reader } = resolved\n const days = aiInsightReaderDays(reader.windows, request.days)\n const params = pluginFigureParams(reader, request.params)\n if (params.ok === false) {\n refusals.push(`${reader.id}: missing ${params.missing}`)\n continue\n }\n const key = JSON.stringify([reader.id, days, params.params])\n if (seen.has(key)) continue\n seen.add(key)\n try {\n const read = await reader.read({ ...base, days, params: params.params })\n if (read.ok === false) {\n refusals.push(`${reader.id}: ${read.error}`)\n continue\n }\n const table = normalizePluginFigureTable(read.table)\n if (!table) {\n refusals.push(`${reader.id}: not a table`)\n continue\n }\n tables.push({ ...table, ref: `t${tables.length + 1}`, reader: reader.id, days, scope: reader.scope })\n } catch (error) {\n // One reader's fault is not the answer's: the rest are still read.\n console.error('ai insight reader failed', { reader: reader.id, orgId: base.orgId, error })\n refusals.push(`${reader.id}: failed`)\n }\n }\n return { tables, refusals }\n}\n\n/** One table as the answer prompt shows it: a heading, the columns, numbered rows, and its notes. */\nexport function aiInsightTableText(table: AiInsightTable): string {\n const period = table.period ? ` · ${table.period.from} to ${table.period.to} (${table.period.days} days)` : ''\n const lines = [\n `${table.ref} · ${table.title} (${table.source.label})${period}`,\n `Columns: ${table.columns.map((column) => column.label).join(' | ')}`,\n ...table.rows.map(\n (row, index) =>\n `${index}: ${table.columns.map((column) => aiInsightCellText(table, column.key, row[column.key])).join(' | ')}`,\n ),\n ]\n if (!table.rows.length) lines.push('(no rows)')\n if (table.omitted) lines.push(`${table.omitted} more rows were left out of this table.`)\n for (const note of table.notes) lines.push(`Note: ${note}`)\n return lines.join('\\n')\n}\n"],"names":["checkEntitlement","isHostPluginEnabled","isPluginEnabled","listPluginFigureReaders","normalizePluginFigureTable","pluginFigureParams","pluginFigureReaderPlugin","AI_INSIGHT_MAX_READS","AI_INSIGHT_SURFACE_READERS","aiInsightCellText","filterEnabledPluginsByReleaseFlags","releasedByFlags","pluginIds","orgId","authorization","aiInsightReaders","context","prefixes","surface","org","candidates","filter","reader","includes","id","split","scope","hostId","feature","plugins","Set","map","Boolean","released","length","resolved","plugin","has","host","aiInsightReaderDays","windows","asked","sort","a","b","Math","abs","aiInsightReaderCatalog","readers","join","params","param","name","required","description","label","aiInsightDatasetCatalog","datasets","dataset","fields","field","type","readAiInsightTables","requests","base","maxTables","tables","refusals","seen","request","find","days","ok","push","missing","key","JSON","stringify","add","read","error","table","ref","console","aiInsightTableText","period","from","to","lines","title","source","columns","column","rows","row","index","omitted","note","notes"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/insights/ai-insight-readers.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 { isHostPluginEnabled, isPluginEnabled } from '@aglyn/aglyn/plugin-manager/enabled-plugins'\nimport {\n listPluginFigureReaders,\n normalizePluginFigureTable,\n pluginFigureParams,\n pluginFigureReaderPlugin,\n type ResolvedPluginFigureReader,\n} from '@aglyn/aglyn/plugin-manager/plugin-figures'\nimport {\n AI_INSIGHT_MAX_READS,\n AI_INSIGHT_SURFACE_READERS,\n aiInsightCellText,\n type AiInsightSurface,\n type AiInsightTable,\n} from '../model/ai-insight'\nimport { filterEnabledPluginsByReleaseFlags } from '@aglyn/tenant-data-admin/server/release-flags'\nimport type { AiInsightReadRequest } from '../tools/ai-insight-tool'\nimport type { AiDatasetCatalogEntry } from './ai-dataset-catalog'\n\n/**\n * Which figure readers an insight job may read, and reading them (AGL-2915).\n *\n * The caller's half of the `plugin-figures` contract: the seam carries no\n * authorization, so before a reader is offered to the model — let alone read\n * — this establishes that the surface asks about its kind of figures, that\n * the workspace's plan includes the feature the figures are sold under, and\n * that the plugin whose records they are runs here: past its release flag,\n * on for the workspace and not switched off for the site. The site belonging\n * to the job's workspace, and the member's reach, are the job's own checks.\n */\n\n/** Which of a set of plugins are past their release flags for an org; the platform's flags by default. */\nexport type AiReleasedPlugins = (pluginIds: string[], orgId: string) => Promise<string[]>\n\nconst releasedByFlags: AiReleasedPlugins = (pluginIds, orgId) =>\n filterEnabledPluginsByReleaseFlags(pluginIds, { orgId, authorization: null })\n\nexport interface AiInsightReaderContext {\n orgId: string\n /** The site the figures are about; `null` for a workspace-level question. */\n hostId: string | null\n /** The org document the machine or the door read. */\n org: object | null\n /** The site's host document, already checked to belong to the org; `null` with no site. */\n host: Record<string, unknown> | null\n surface: AiInsightSurface\n released?: AiReleasedPlugins\n}\n\n/** The readers a job on this surface may read, ordered by id so a prompt lists them in the same order every time. */\nexport async function aiInsightReaders(context: AiInsightReaderContext): Promise<ResolvedPluginFigureReader[]> {\n const prefixes = AI_INSIGHT_SURFACE_READERS[context.surface]\n const org = context.org as { enabledPlugins?: string[] } | null\n const candidates = listPluginFigureReaders().filter(({ reader }) => {\n if (!prefixes.includes(reader.id.split('.')[0])) return false\n // A site reader needs a site; a workspace reader reads with or without one.\n if (reader.scope === 'site' && !context.hostId) return false\n return !reader.feature || checkEntitlement(org as never, reader.feature)\n })\n const plugins = [...new Set(candidates.map(pluginFigureReaderPlugin).filter((id): id is string => Boolean(id)))]\n const released = new Set(plugins.length ? await (context.released ?? releasedByFlags)(plugins, context.orgId) : [])\n return candidates.filter((resolved) => {\n const plugin = pluginFigureReaderPlugin(resolved)\n if (!plugin) return true\n if (!released.has(plugin)) return false\n return context.hostId\n ? isHostPluginEnabled(org, context.host as { disabledPlugins?: string[] } | null, plugin)\n : isPluginEnabled(org, plugin)\n })\n}\n\n/** The window a reader is read over: the one asked for when it covers it, else the nearest it does. */\nexport function aiInsightReaderDays(windows: readonly number[], asked: number): number {\n if (!windows.length) return 0\n if (windows.includes(asked)) return asked\n return [...windows].sort((a, b) => Math.abs(a - asked) - Math.abs(b - asked) || a - b)[0]\n}\n\n/** The readers as the read prompt lists them: id, what each answers, its windows and its parameters. */\nexport function aiInsightReaderCatalog(readers: readonly ResolvedPluginFigureReader[]): string {\n return readers\n .map(({ reader }) => {\n const windows = reader.windows.length\n ? ` Windows: ${reader.windows.join(', ')} days.`\n : ' Current totals, with no window: days 0.'\n const params = (reader.params ?? []).length\n ? ` Parameters: ${(reader.params ?? [])\n .map((param) => `${param.name} (${param.required ? 'required' : 'optional'}) — ${param.description}`)\n .join('; ')}`\n : ''\n return `- ${reader.id} — ${reader.label}: ${reader.description}${windows}${params}`\n })\n .join('\\n')\n}\n\n/** The datasets a datasets question may name, as the read prompt lists them. */\nexport function aiInsightDatasetCatalog(datasets: readonly AiDatasetCatalogEntry[]): string {\n if (!datasets.length) return 'There are no datasets you may read here.'\n return datasets\n .map(\n (dataset) =>\n `- ${dataset.name} (id ${dataset.id}): ${dataset.fields.map((field) => `${field.name} [${field.type}]`).join(', ') || 'no fields'}`,\n )\n .join('\\n')\n}\n\nexport interface AiInsightReadResult {\n tables: AiInsightTable[]\n /** Why a request that named a real reader was not read, in its owner's words, for the log. */\n refusals: string[]\n}\n\n/**\n * The tables for the reads a job asked for: each request held to the readers\n * this job may read, its window to that reader's, its parameters to those it\n * declares; each table held to the contract by `normalizePluginFigureTable`.\n * A reader asked for twice with the same window and parameters is read once.\n */\nexport async function readAiInsightTables(\n readers: readonly ResolvedPluginFigureReader[],\n requests: readonly AiInsightReadRequest[],\n base: { orgId: string; hostId: string | null; uid: string | null; now: Date },\n maxTables: number = AI_INSIGHT_MAX_READS,\n): Promise<AiInsightReadResult> {\n const tables: AiInsightTable[] = []\n const refusals: string[] = []\n const seen = new Set<string>()\n for (const request of requests) {\n if (tables.length === maxTables) break\n const resolved = readers.find(({ reader }) => reader.id === request.reader)\n if (!resolved) continue\n const { reader } = resolved\n const days = aiInsightReaderDays(reader.windows, request.days)\n const params = pluginFigureParams(reader, request.params)\n if (params.ok === false) {\n refusals.push(`${reader.id}: missing ${params.missing}`)\n continue\n }\n const key = JSON.stringify([reader.id, days, params.params])\n if (seen.has(key)) continue\n seen.add(key)\n try {\n const read = await reader.read({ ...base, days, params: params.params })\n if (read.ok === false) {\n refusals.push(`${reader.id}: ${read.error}`)\n continue\n }\n const table = normalizePluginFigureTable(read.table)\n if (!table) {\n refusals.push(`${reader.id}: not a table`)\n continue\n }\n tables.push({ ...table, ref: `t${tables.length + 1}`, reader: reader.id, days, scope: reader.scope })\n } catch (error) {\n // One reader's fault is not the answer's: the rest are still read.\n console.error('ai insight reader failed', { reader: reader.id, orgId: base.orgId, error })\n refusals.push(`${reader.id}: failed`)\n }\n }\n return { tables, refusals }\n}\n\n/** One table as the answer prompt shows it: a heading, the columns, numbered rows, and its notes. */\nexport function aiInsightTableText(table: AiInsightTable): string {\n const period = table.period ? ` · ${table.period.from} to ${table.period.to} (${table.period.days} days)` : ''\n const lines = [\n `${table.ref} · ${table.title} (${table.source.label})${period}`,\n `Columns: ${table.columns.map((column) => column.label).join(' | ')}`,\n ...table.rows.map(\n (row, index) =>\n `${index}: ${table.columns.map((column) => aiInsightCellText(table, column.key, row[column.key])).join(' | ')}`,\n ),\n ]\n if (!table.rows.length) lines.push('(no rows)')\n if (table.omitted) lines.push(`${table.omitted} more rows were left out of this table.`)\n for (const note of table.notes) lines.push(`Note: ${note}`)\n return lines.join('\\n')\n}\n"],"names":["checkEntitlement","isHostPluginEnabled","isPluginEnabled","listPluginFigureReaders","normalizePluginFigureTable","pluginFigureParams","pluginFigureReaderPlugin","AI_INSIGHT_MAX_READS","AI_INSIGHT_SURFACE_READERS","aiInsightCellText","filterEnabledPluginsByReleaseFlags","releasedByFlags","pluginIds","orgId","authorization","aiInsightReaders","context","prefixes","surface","org","candidates","filter","reader","includes","id","split","scope","hostId","feature","plugins","Set","map","Boolean","released","length","resolved","plugin","has","host","aiInsightReaderDays","windows","asked","sort","a","b","Math","abs","aiInsightReaderCatalog","readers","join","params","param","name","required","description","label","aiInsightDatasetCatalog","datasets","dataset","fields","field","type","readAiInsightTables","requests","base","maxTables","tables","refusals","seen","request","find","days","ok","push","missing","key","JSON","stringify","add","read","error","table","ref","console","aiInsightTableText","period","from","to","lines","title","source","columns","column","rows","row","index","omitted","note","notes"],"mappings":"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"}
|
|
@@ -87,7 +87,11 @@ export declare function aiCopiedComponentReview(firestore: FirebaseFirestore.Fir
|
|
|
87
87
|
name: string;
|
|
88
88
|
plan: AiJobPlan | null;
|
|
89
89
|
}): Promise<AiJobReview | null>;
|
|
90
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* A component job is admitted for a site of the job's own org whose plan
|
|
92
|
+
* includes reusable components, and confirmed only for a plan whose other
|
|
93
|
+
* creations it builds (AGL-3143 §15).
|
|
94
|
+
*/
|
|
91
95
|
export declare const aiComponentJobAdmission: AiJobAdmission;
|
|
92
96
|
export interface AiJobComponentStepDeps {
|
|
93
97
|
/** The inventory reader; specs hand in a fake. */
|
|
@@ -96,6 +100,7 @@ export interface AiJobComponentStepDeps {
|
|
|
96
100
|
duplicate?: typeof duplicateResource;
|
|
97
101
|
}
|
|
98
102
|
export declare function createAiJobComponentStep(deps?: AiJobComponentStepDeps): AiJobStepRunner;
|
|
103
|
+
/** The component step, building the forms and other components its plan creates first (AGL-3143 §15). */
|
|
99
104
|
export declare const runAiJobComponentStep: AiJobStepRunner;
|
|
100
105
|
/** Registers the component step and the check a component job passes before it is created or resumed. */
|
|
101
106
|
export declare function registerAiComponentJob(): void;
|
|
@@ -22,6 +22,7 @@ import { runValidatedGeneration } from "../runtime/ai-doctrine.js";
|
|
|
22
22
|
import { readSiteInventory } from "../runtime/site-inventory.js";
|
|
23
23
|
import { AI_COMPONENT_TOOL_NAME, aiComponentPropKindWords, aiComponentTool } from "../tools/ai-component-tool.js";
|
|
24
24
|
import { registerAiJobAdmission } from "./ai-job-admission.js";
|
|
25
|
+
import { aiBuildingPlanCreations, aiJobPlanCreationsRefusal, aiPlanCreationsRunMinimumMs } from "./ai-job-plan-creations.js";
|
|
25
26
|
import { aiJobStepBudget } from "./ai-job-budget.js";
|
|
26
27
|
import { aiComponentCheck, aiPlannedComponentProps } from "./ai-job-component-checks.js";
|
|
27
28
|
import { aiJobDraftId } from "./ai-job-draft-ids.js";
|
|
@@ -136,11 +137,16 @@ import { registerAiJobStep } from "./ai-jobs.js";
|
|
|
136
137
|
]
|
|
137
138
|
});
|
|
138
139
|
}
|
|
139
|
-
/**
|
|
140
|
+
/**
|
|
141
|
+
* A component job is admitted for a site of the job's own org whose plan
|
|
142
|
+
* includes reusable components, and confirmed only for a plan whose other
|
|
143
|
+
* creations it builds (AGL-3143 §15).
|
|
144
|
+
*/ export const aiComponentJobAdmission = (context)=>aiDraftAdmissionRefusal(context.firestore, {
|
|
140
145
|
orgId: context.orgId,
|
|
141
146
|
hostId: context.hostId,
|
|
142
147
|
kind: 'component',
|
|
143
|
-
org: context.org
|
|
148
|
+
org: context.org,
|
|
149
|
+
ownCheck: (hostId)=>aiJobPlanCreationsRefusal('component', context, hostId)
|
|
144
150
|
});
|
|
145
151
|
export function createAiJobComponentStep(deps = {}) {
|
|
146
152
|
var _deps_readInventory, _deps_duplicate;
|
|
@@ -188,8 +194,18 @@ export function createAiJobComponentStep(deps = {}) {
|
|
|
188
194
|
const name = (creation == null ? void 0 : creation.name) || AI_JOB_COMPONENT_DEFAULT_NAME;
|
|
189
195
|
// The plan starts from a copy of a component the site has (rule 15): the
|
|
190
196
|
// copy is the draft, and nothing is generated. A source gone since the
|
|
191
|
-
// plan was made is built from the brief instead
|
|
192
|
-
|
|
197
|
+
// plan was made is built from the brief instead, and one that lacks a
|
|
198
|
+
// property the plan gives the component is built from the plan
|
|
199
|
+
// (AGL-3143 §14): copying it could only end in the review below. A source
|
|
200
|
+
// this step cannot read still copies.
|
|
201
|
+
const source = (creation == null ? void 0 : creation.duplicateOf) ? inventory.components.find((component)=>component.id === creation.duplicateOf) : undefined;
|
|
202
|
+
const sourceReview = source ? await aiCopiedComponentReview(firestore, {
|
|
203
|
+
hostId,
|
|
204
|
+
id: source.id,
|
|
205
|
+
name: source.name,
|
|
206
|
+
plan
|
|
207
|
+
}) : null;
|
|
208
|
+
if ((creation == null ? void 0 : creation.duplicateOf) && source && !(sourceReview == null ? void 0 : sourceReview.findings.length)) {
|
|
193
209
|
const copy = await duplicate('component', {
|
|
194
210
|
orgId: job.orgId,
|
|
195
211
|
hostId,
|
|
@@ -322,10 +338,11 @@ export function createAiJobComponentStep(deps = {}) {
|
|
|
322
338
|
});
|
|
323
339
|
};
|
|
324
340
|
}
|
|
325
|
-
export const runAiJobComponentStep = createAiJobComponentStep();
|
|
341
|
+
/** The component step, building the forms and other components its plan creates first (AGL-3143 §15). */ export const runAiJobComponentStep = aiBuildingPlanCreations('component', 'job.component', createAiJobComponentStep());
|
|
326
342
|
/** Registers the component step and the check a component job passes before it is created or resumed. */ export function registerAiComponentJob() {
|
|
327
343
|
registerAiJobStep('component', runAiJobComponentStep, {
|
|
328
|
-
minimumMs: AI_JOB_COMPONENT_STEP_MINIMUM_MS
|
|
344
|
+
minimumMs: AI_JOB_COMPONENT_STEP_MINIMUM_MS,
|
|
345
|
+
minimumMsFor: aiPlanCreationsRunMinimumMs('component')
|
|
329
346
|
});
|
|
330
347
|
registerAiJobAdmission('component', aiComponentJobAdmission);
|
|
331
348
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-component-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 type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport type { ReusableComponentProp } from '@aglyn/aglyn/foundation/definitions/platform.types'\nimport { duplicateResource } from '@aglyn/tenant-data-admin/server/duplicate-resource'\nimport { aiPlanEmbedsFor } from '../model/ai-build-plan'\nimport type { AiJob, AiJobOutput, AiJobPlan, AiJobReview } from '../model/ai-jobs.types'\nimport { AI_STEP_TIERS } from '../providers/catalog'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport { runValidatedGeneration } from '../runtime/ai-doctrine'\nimport type { AiDoctrineTree } from '../runtime/ai-doctrine-validators'\nimport type { AiLoadEstimate } from '../runtime/ai-palette'\nimport type { AiSystemBlock } from '../runtime/ai-runtime'\nimport { readSiteInventory } from '../runtime/site-inventory'\nimport {\n AI_COMPONENT_TOOL_NAME,\n aiComponentPropKindWords,\n aiComponentTool,\n} from '../tools/ai-component-tool'\nimport { registerAiJobAdmission, type AiJobAdmission } from './ai-job-admission'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { aiComponentCheck, aiPlannedComponentProps } from './ai-job-component-checks'\nimport { aiJobDraftId } from './ai-job-draft-ids'\nimport {\n aiDraftAdmissionRefusal,\n aiDraftAllowanceRefusal,\n aiSiteSubdomain,\n readAiDraft,\n readAiDraftComponentProps,\n writeAiDraft,\n type AiDraftRecord,\n} from './ai-job-drafts'\nimport {\n aiBracketedFactsNote,\n aiConfirmedPlan,\n aiDoctrineReview,\n aiGenerationSpent,\n aiJobBriefLine,\n aiLimitReview,\n aiPlanCreation,\n aiPlanReferenceLines,\n aiUnspentOutcome,\n} from './ai-job-generation'\nimport type { AiJobStepRunner } from './ai-job-text-step'\nimport { registerAiJobStep } from './ai-jobs'\n\n/**\n * The component step (AGL-2908): the generation step of a `component` job,\n * run once a member confirmed the job's plan.\n *\n * It builds ONE reusable component from the brief — its tree, and the typed\n * properties each page that places it fills in — through\n * `runValidatedGeneration('component', …)`, so every building rule holds on\n * the tree before it is kept. The tree binds each property with\n * `{{prop.<name>}}`; the palette validator keeps those tokens as written\n * (`definesComponent`), and the step's own check, through `extend`, holds\n * each binding to what the Properties dialog and the Attributes panel allow\n * (`ai-job-component-checks.ts`).\n *\n * The component lands as a new draft (`ai-job-drafts.ts`) that no page\n * places, and gets its first version when a member opens it, as every\n * component created outside the besigner does. A plan that starts from a copy\n * of a component the site has gets that copy, through the platform's\n * duplicate module, and nothing generated.\n *\n * A generation step runs on the job beat, never at an inline door: it\n * registers the least time its lookup rounds, its answer and its re-ask need\n * (AGL-3035), runs without extended thinking, and asks for no more of the\n * ceiling the layout and template steps keep than fits that time on the model\n * it runs. The step's spec measures the request and the answer against it.\n */\n\n/** The longest answer a component may run to; the tree is held to the component budget either way. */\nexport const AI_JOB_COMPONENT_MAX_TOKENS = AI_ROUTING_TABLE['job.component'].maxTokens\n\n/**\n * The component step's time (AGL-3035, AGL-3036): its two inventory-lookup\n * rounds, its answer and its re-ask at the routing table's ceiling on the tier\n * `job.component` is served from, fitted to what a beat can give a step.\n */\nexport const AI_JOB_COMPONENT_STEP_BUDGET = aiJobStepBudget({\n tier: AI_STEP_TIERS['job.component'],\n maxTokens: AI_JOB_COMPONENT_MAX_TOKENS,\n})\n\n/** The least time one component step needs before it starts. */\nexport const AI_JOB_COMPONENT_STEP_MINIMUM_MS = AI_JOB_COMPONENT_STEP_BUDGET.minimumMs\n\n/** A component's name when the plan names none. */\nexport const AI_JOB_COMPONENT_DEFAULT_NAME = 'Component'\n\n/** The component step's own instructions, cached after the doctrine and before the catalog. */\nexport const AI_JOB_COMPONENT_INSTRUCTIONS: readonly AiSystemBlock[] = [\n {\n text: [\n `You build one reusable component for a website: a block the site places on many pages, each placement filling in its own values. Answer with ${AI_COMPONENT_TOOL_NAME}: the component’s tree as one flat node map, and the properties it declares.`,\n 'The root is the document wrapper holding the component. A component is placed inside a page section, which opens with its own h2: it carries no main landmark, its headings are h3 or below, and its colors, spacing and type come from the theme.',\n 'Every value that should differ from one placement to the next is a property, bound in the tree with its token {{prop.<name>}}. Declare every property the tree binds, and bind every property you declare.',\n `Property kinds, as type (the name a page reads): ${aiComponentPropKindWords()}.`,\n 'A property binds only to a field that holds its kind of value:',\n '- text, richText and number: copy, such as a Typography’s text, an Image’s alt or a Button’s label, as the whole value or inside a sentence.',\n '- image: an Image’s src, as the whole value.',\n '- href: a screenId or an href, as the whole value.',\n '- icon: an Icon element’s iconId, as the whole value.',\n '- boolean: a switch setting such as a Button’s fullWidth, or hideIf on the part it hides, as the whole value.',\n '- choice: a setting with fixed options such as a Button’s variant, as the whole value, with every answer’s value one that setting lists.',\n 'An optional part, such as a photo, a badge or a second button, gets a boolean property labeled \"Hide <the part>\" whose default is \"false\", and the part’s own element carries \"hideIf\": \"{{prop.<name>}}\". Never hide the whole component.',\n 'A default is what the component shows until a page sets it: copy in the site’s voice from the brief, with a fact the brief does not give in square brackets; for href a page id from the site inventory, or \"\"; for image \"\", for an upload; for icon \"\", for the site owner to pick; for choice one of its answers’ values.',\n ].join('\\n'),\n },\n]\n\n/** The component job as the generation's user turn: the name, the brief, the confirmed plan. */\nexport function aiJobComponentPrompt(\n job: Pick<AiJob, 'brief'>,\n plan: AiJobPlan | null,\n name: string,\n): string {\n return [`Component name: ${name}`, aiJobBriefLine(job), ...aiPlanReferenceLines(plan)].join('\\n')\n}\n\n/**\n * The props the confirmed plan gives the component, against the component a\n * COPY actually produced (AGL-3024); `null` when the copy keeps the plan, or\n * when the plan promised no prop to keep.\n *\n * The third kind to need this, after the layout (`aiCopiedLayoutReview`) and\n * the page (`aiCopiedPageReview`), and found the same way: a duplicate branch\n * generates nothing, so it is the one place a plan's promise can go unmet\n * with no model to answer for it.\n *\n * MEASURED 2026-09-21. A plan read \"the existing card has no link target; the\n * brief needs one\", and `practice-area-link-card` came out BYTE-IDENTICAL to\n * `practice-area-card` — the same seven nodes under the same ids, the same\n * three props, no link anywhere. Its job reported the copy as built. So did\n * `practice-area-link-card-v2` the following pass. A component job can\n * therefore charge a member for a duplicate of something they already had.\n *\n * A copy that cannot be read back settles nothing, and a promise this step\n * cannot settle is reviewed by a person rather than reported as kept.\n */\nexport async function aiCopiedComponentReview(\n firestore: FirebaseFirestore.Firestore,\n input: { hostId: string; id: string; name: string; plan: AiJobPlan | null },\n): Promise<AiJobReview | null> {\n const promised = aiPlannedComponentProps(input.plan)\n if (!promised.length) return null\n const declared = await readAiDraftComponentProps(firestore, { hostId: input.hostId, id: input.id })\n if (declared === null) {\n return {\n reason: 'doctrine',\n message: `\"${input.name}\" was copied from the component the plan names, and this job could not read the copy back to check it has the properties the plan gives it. Open the component and check it before you place it.`,\n findings: [],\n }\n }\n const missing = promised.filter((name) => !declared.includes(name))\n if (!missing.length) return null\n const one = missing.length === 1\n return aiDoctrineReview({\n message: `\"${input.name}\" is a copy of the component the plan names, and a copy is all it is: the plan gives it ${\n one ? 'a property' : 'properties'\n } the copy does not have.`,\n violations: [\n {\n rule: null,\n code: 'plan-props-missing',\n message: `The confirmed plan says this component has ${missing.join(', ')}, and the copy has ${\n declared.length ? declared.join(', ') : 'none of them'\n }. Add ${one ? 'it' : 'each of them'}, and use ${\n one ? 'it' : 'them'\n } in the component, or the copy is the component it was copied from.`,\n },\n ],\n })\n}\n\n/** A component job is admitted for a site of the job's own org whose plan includes reusable components. */\nexport const aiComponentJobAdmission: AiJobAdmission = (context) =>\n aiDraftAdmissionRefusal(context.firestore, {\n orgId: context.orgId,\n hostId: context.hostId,\n kind: 'component',\n org: context.org,\n })\n\nexport interface AiJobComponentStepDeps {\n /** The inventory reader; specs hand in a fake. */\n readInventory?: typeof readSiteInventory\n /** The platform's duplicate module, for a plan that starts from a copy. */\n duplicate?: typeof duplicateResource\n}\n\nexport function createAiJobComponentStep(deps: AiJobComponentStepDeps = {}): AiJobStepRunner {\n const readInventory = deps.readInventory ?? readSiteInventory\n const duplicate = deps.duplicate ?? duplicateResource\n return async ({ job, now, signal, firestore, modelFor }) => {\n const hostId = job.hostId\n if (!hostId) throw new Error('a component job names no site, and its admission refuses one')\n const output = (draft: AiDraftRecord, load?: AiLoadEstimate | null, note?: string | null): AiJobOutput => ({\n resource: 'reusableComponent',\n id: draft.id,\n versionId: draft.versionId,\n hostId,\n hostSubdomain: draft.hostSubdomain,\n label: draft.name,\n ...(load ? { load } : {}),\n ...(note ? { note } : {}),\n })\n // The switch's answer for this job, else the routing table's (AGL-2942).\n const model = modelFor?.('job.component') ?? aiModelForStep('job.component')\n\n // A run cut off after its draft was written reports that draft, found by the id the job recorded.\n const draftId = aiJobDraftId(job, 'component')\n const written = await readAiDraft(firestore, { kind: 'component', hostId, id: draftId })\n if (written) return aiUnspentOutcome(model, { outputs: [output(written)] })\n\n const [inventory, orgSnapshot] = await Promise.all([\n readInventory(job.orgId, hostId, { firestore }),\n firestore.collection('orgs').doc(job.orgId).get(),\n ])\n const org = (orgSnapshot.data() ?? null) as Partial<AglynOrgBilling> | null\n const plan = aiConfirmedPlan(job)\n const creation = aiPlanCreation(plan, 'component')\n const name = creation?.name || AI_JOB_COMPONENT_DEFAULT_NAME\n\n // The plan starts from a copy of a component the site has (rule 15): the\n // copy is the draft, and nothing is generated. A source gone since the\n // plan was made is built from the brief instead.\n if (\n creation?.duplicateOf &&\n inventory.components.some((component) => component.id === creation.duplicateOf)\n ) {\n const copy = await duplicate('component', {\n orgId: job.orgId,\n hostId,\n sourceId: creation.duplicateOf,\n name,\n uid: job.createdBy,\n org: org as Record<string, unknown> | null,\n // The same claim the page step makes (AGL-3024): a copy core mints is\n // written under core's own id, never `draftId`, so a step that runs\n // again — a pass resumed, a run cut off after the copy — would mint a\n // second. The job's recorded id makes core replay the one it made.\n attemptKey: draftId,\n })\n if (copy.ok) {\n const hostSubdomain = await aiSiteSubdomain(firestore, hostId)\n // The copy is reported with what the plan promised of it read back\n // (AGL-3024): this branch generates nothing, so without this a plan\n // reading \"duplicate it and add a link\" reports Done on a duplicate\n // that added nothing.\n const review = await aiCopiedComponentReview(firestore, {\n hostId,\n id: copy.id,\n name: copy.name,\n plan,\n })\n if (review) return aiUnspentOutcome(model, { review })\n return aiUnspentOutcome(model, {\n outputs: [\n output({ id: copy.id, versionId: copy.versionId ?? null, name: copy.name, hostSubdomain }),\n ],\n })\n }\n if (copy.ok === false && copy.status === 403) {\n return aiUnspentOutcome(model, { review: aiLimitReview(copy.error) })\n }\n }\n\n const allowance = await aiDraftAllowanceRefusal(firestore, { kind: 'component', hostId, org })\n if (allowance) return aiUnspentOutcome(model, { review: aiLimitReview(allowance) })\n\n let props: ReusableComponentProp[] = []\n const result = await runValidatedGeneration('component', {\n step: 'job.component',\n model,\n instructions: AI_JOB_COMPONENT_INSTRUCTIONS,\n inventory,\n messages: [{ role: 'user', content: aiJobComponentPrompt(job, plan, name) }],\n tool: aiComponentTool(),\n maxTokens: AI_JOB_COMPONENT_STEP_BUDGET.maxTokens(model),\n ...(AI_ROUTING_TABLE['job.component'].thinking\n ? { thinking: AI_ROUTING_TABLE['job.component'].thinking }\n : {}),\n ...(AI_ROUTING_TABLE['job.component'].effort\n ? { effort: AI_ROUTING_TABLE['job.component'].effort }\n : {}),\n // A third-party player the confirmed plan lists for this component is the only one it may embed (AGL-3433).\n context: {\n definesComponent: true,\n ...(creation ? { plannedEmbeds: aiPlanEmbedsFor(plan, { create: creation.name }) } : {}),\n },\n extend: aiComponentCheck({\n inventory,\n plan,\n onProps: (declared) => {\n props = declared\n },\n }),\n ...(signal ? { signal } : {}),\n })\n const spent = aiGenerationSpent(result)\n if (result.status === 'refused') return { ...spent, refused: true }\n if (result.status === 'needs_input') return { ...spent, review: aiDoctrineReview(result) }\n\n const draft = await writeAiDraft(firestore, {\n kind: 'component',\n hostId,\n id: draftId,\n uid: job.createdBy,\n org,\n name,\n nodes: result.value.nodes,\n rootId: result.value.rootId,\n props,\n now,\n })\n if (draft.ok === false) {\n if (draft.status === 404) throw new Error(`site ${hostId} vanished while its component was generated`)\n return { ...spent, review: aiLimitReview(draft.error) }\n }\n // The facts the brief did not give, in the tree or the defaults a page shows (AGL-3056).\n const note = aiBracketedFactsNote({\n tree: { rootId: result.value.rootId, nodes: result.value.nodes as unknown as AiDoctrineTree['nodes'] },\n inventory,\n copy: props.map((prop) => (typeof prop.defaultValue === 'string' ? prop.defaultValue : '')),\n })\n return { ...spent, outputs: [output(draft, result.value.load, note)] }\n }\n}\n\nexport const runAiJobComponentStep = createAiJobComponentStep()\n\n/** Registers the component step and the check a component job passes before it is created or resumed. */\nexport function registerAiComponentJob(): void {\n registerAiJobStep('component', runAiJobComponentStep, { minimumMs: AI_JOB_COMPONENT_STEP_MINIMUM_MS })\n registerAiJobAdmission('component', aiComponentJobAdmission)\n}\n"],"names":["duplicateResource","aiPlanEmbedsFor","AI_STEP_TIERS","AI_ROUTING_TABLE","aiModelForStep","runValidatedGeneration","readSiteInventory","AI_COMPONENT_TOOL_NAME","aiComponentPropKindWords","aiComponentTool","registerAiJobAdmission","aiJobStepBudget","aiComponentCheck","aiPlannedComponentProps","aiJobDraftId","aiDraftAdmissionRefusal","aiDraftAllowanceRefusal","aiSiteSubdomain","readAiDraft","readAiDraftComponentProps","writeAiDraft","aiBracketedFactsNote","aiConfirmedPlan","aiDoctrineReview","aiGenerationSpent","aiJobBriefLine","aiLimitReview","aiPlanCreation","aiPlanReferenceLines","aiUnspentOutcome","registerAiJobStep","AI_JOB_COMPONENT_MAX_TOKENS","maxTokens","AI_JOB_COMPONENT_STEP_BUDGET","tier","AI_JOB_COMPONENT_STEP_MINIMUM_MS","minimumMs","AI_JOB_COMPONENT_DEFAULT_NAME","AI_JOB_COMPONENT_INSTRUCTIONS","text","join","aiJobComponentPrompt","job","plan","name","aiCopiedComponentReview","firestore","input","promised","length","declared","hostId","id","reason","message","findings","missing","filter","includes","one","violations","rule","code","aiComponentJobAdmission","context","orgId","kind","org","createAiJobComponentStep","deps","readInventory","duplicate","now","signal","modelFor","orgSnapshot","Error","output","draft","load","note","resource","versionId","hostSubdomain","label","model","draftId","written","outputs","inventory","Promise","all","collection","doc","get","data","creation","duplicateOf","components","some","component","copy","sourceId","uid","createdBy","attemptKey","ok","review","status","error","allowance","props","result","step","instructions","messages","role","content","tool","thinking","effort","definesComponent","plannedEmbeds","create","extend","onProps","spent","refused","nodes","value","rootId","tree","map","prop","defaultValue","runAiJobComponentStep","registerAiComponentJob"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-component-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 type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport type { ReusableComponentProp } from '@aglyn/aglyn/foundation/definitions/platform.types'\nimport { duplicateResource } from '@aglyn/tenant-data-admin/server/duplicate-resource'\nimport { aiPlanEmbedsFor } from '../model/ai-build-plan'\nimport type { AiJob, AiJobOutput, AiJobPlan, AiJobReview } from '../model/ai-jobs.types'\nimport { AI_STEP_TIERS } from '../providers/catalog'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport { runValidatedGeneration } from '../runtime/ai-doctrine'\nimport type { AiDoctrineTree } from '../runtime/ai-doctrine-validators'\nimport type { AiLoadEstimate } from '../runtime/ai-palette'\nimport type { AiSystemBlock } from '../runtime/ai-runtime'\nimport { readSiteInventory } from '../runtime/site-inventory'\nimport {\n AI_COMPONENT_TOOL_NAME,\n aiComponentPropKindWords,\n aiComponentTool,\n} from '../tools/ai-component-tool'\nimport { registerAiJobAdmission, type AiJobAdmission } from './ai-job-admission'\nimport {\n aiBuildingPlanCreations,\n aiJobPlanCreationsRefusal,\n aiPlanCreationsRunMinimumMs,\n} from './ai-job-plan-creations'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { aiComponentCheck, aiPlannedComponentProps } from './ai-job-component-checks'\nimport { aiJobDraftId } from './ai-job-draft-ids'\nimport {\n aiDraftAdmissionRefusal,\n aiDraftAllowanceRefusal,\n aiSiteSubdomain,\n readAiDraft,\n readAiDraftComponentProps,\n writeAiDraft,\n type AiDraftRecord,\n} from './ai-job-drafts'\nimport {\n aiBracketedFactsNote,\n aiConfirmedPlan,\n aiDoctrineReview,\n aiGenerationSpent,\n aiJobBriefLine,\n aiLimitReview,\n aiPlanCreation,\n aiPlanReferenceLines,\n aiUnspentOutcome,\n} from './ai-job-generation'\nimport type { AiJobStepRunner } from './ai-job-text-step'\nimport { registerAiJobStep } from './ai-jobs'\n\n/**\n * The component step (AGL-2908): the generation step of a `component` job,\n * run once a member confirmed the job's plan.\n *\n * It builds ONE reusable component from the brief — its tree, and the typed\n * properties each page that places it fills in — through\n * `runValidatedGeneration('component', …)`, so every building rule holds on\n * the tree before it is kept. The tree binds each property with\n * `{{prop.<name>}}`; the palette validator keeps those tokens as written\n * (`definesComponent`), and the step's own check, through `extend`, holds\n * each binding to what the Properties dialog and the Attributes panel allow\n * (`ai-job-component-checks.ts`).\n *\n * The component lands as a new draft (`ai-job-drafts.ts`) that no page\n * places, and gets its first version when a member opens it, as every\n * component created outside the besigner does. A plan that starts from a copy\n * of a component the site has gets that copy, through the platform's\n * duplicate module, and nothing generated.\n *\n * A generation step runs on the job beat, never at an inline door: it\n * registers the least time its lookup rounds, its answer and its re-ask need\n * (AGL-3035), runs without extended thinking, and asks for no more of the\n * ceiling the layout and template steps keep than fits that time on the model\n * it runs. The step's spec measures the request and the answer against it.\n */\n\n/** The longest answer a component may run to; the tree is held to the component budget either way. */\nexport const AI_JOB_COMPONENT_MAX_TOKENS = AI_ROUTING_TABLE['job.component'].maxTokens\n\n/**\n * The component step's time (AGL-3035, AGL-3036): its two inventory-lookup\n * rounds, its answer and its re-ask at the routing table's ceiling on the tier\n * `job.component` is served from, fitted to what a beat can give a step.\n */\nexport const AI_JOB_COMPONENT_STEP_BUDGET = aiJobStepBudget({\n tier: AI_STEP_TIERS['job.component'],\n maxTokens: AI_JOB_COMPONENT_MAX_TOKENS,\n})\n\n/** The least time one component step needs before it starts. */\nexport const AI_JOB_COMPONENT_STEP_MINIMUM_MS = AI_JOB_COMPONENT_STEP_BUDGET.minimumMs\n\n/** A component's name when the plan names none. */\nexport const AI_JOB_COMPONENT_DEFAULT_NAME = 'Component'\n\n/** The component step's own instructions, cached after the doctrine and before the catalog. */\nexport const AI_JOB_COMPONENT_INSTRUCTIONS: readonly AiSystemBlock[] = [\n {\n text: [\n `You build one reusable component for a website: a block the site places on many pages, each placement filling in its own values. Answer with ${AI_COMPONENT_TOOL_NAME}: the component’s tree as one flat node map, and the properties it declares.`,\n 'The root is the document wrapper holding the component. A component is placed inside a page section, which opens with its own h2: it carries no main landmark, its headings are h3 or below, and its colors, spacing and type come from the theme.',\n 'Every value that should differ from one placement to the next is a property, bound in the tree with its token {{prop.<name>}}. Declare every property the tree binds, and bind every property you declare.',\n `Property kinds, as type (the name a page reads): ${aiComponentPropKindWords()}.`,\n 'A property binds only to a field that holds its kind of value:',\n '- text, richText and number: copy, such as a Typography’s text, an Image’s alt or a Button’s label, as the whole value or inside a sentence.',\n '- image: an Image’s src, as the whole value.',\n '- href: a screenId or an href, as the whole value.',\n '- icon: an Icon element’s iconId, as the whole value.',\n '- boolean: a switch setting such as a Button’s fullWidth, or hideIf on the part it hides, as the whole value.',\n '- choice: a setting with fixed options such as a Button’s variant, as the whole value, with every answer’s value one that setting lists.',\n 'An optional part, such as a photo, a badge or a second button, gets a boolean property labeled \"Hide <the part>\" whose default is \"false\", and the part’s own element carries \"hideIf\": \"{{prop.<name>}}\". Never hide the whole component.',\n 'A default is what the component shows until a page sets it: copy in the site’s voice from the brief, with a fact the brief does not give in square brackets; for href a page id from the site inventory, or \"\"; for image \"\", for an upload; for icon \"\", for the site owner to pick; for choice one of its answers’ values.',\n ].join('\\n'),\n },\n]\n\n/** The component job as the generation's user turn: the name, the brief, the confirmed plan. */\nexport function aiJobComponentPrompt(\n job: Pick<AiJob, 'brief'>,\n plan: AiJobPlan | null,\n name: string,\n): string {\n return [`Component name: ${name}`, aiJobBriefLine(job), ...aiPlanReferenceLines(plan)].join('\\n')\n}\n\n/**\n * The props the confirmed plan gives the component, against the component a\n * COPY actually produced (AGL-3024); `null` when the copy keeps the plan, or\n * when the plan promised no prop to keep.\n *\n * The third kind to need this, after the layout (`aiCopiedLayoutReview`) and\n * the page (`aiCopiedPageReview`), and found the same way: a duplicate branch\n * generates nothing, so it is the one place a plan's promise can go unmet\n * with no model to answer for it.\n *\n * MEASURED 2026-09-21. A plan read \"the existing card has no link target; the\n * brief needs one\", and `practice-area-link-card` came out BYTE-IDENTICAL to\n * `practice-area-card` — the same seven nodes under the same ids, the same\n * three props, no link anywhere. Its job reported the copy as built. So did\n * `practice-area-link-card-v2` the following pass. A component job can\n * therefore charge a member for a duplicate of something they already had.\n *\n * A copy that cannot be read back settles nothing, and a promise this step\n * cannot settle is reviewed by a person rather than reported as kept.\n */\nexport async function aiCopiedComponentReview(\n firestore: FirebaseFirestore.Firestore,\n input: { hostId: string; id: string; name: string; plan: AiJobPlan | null },\n): Promise<AiJobReview | null> {\n const promised = aiPlannedComponentProps(input.plan)\n if (!promised.length) return null\n const declared = await readAiDraftComponentProps(firestore, { hostId: input.hostId, id: input.id })\n if (declared === null) {\n return {\n reason: 'doctrine',\n message: `\"${input.name}\" was copied from the component the plan names, and this job could not read the copy back to check it has the properties the plan gives it. Open the component and check it before you place it.`,\n findings: [],\n }\n }\n const missing = promised.filter((name) => !declared.includes(name))\n if (!missing.length) return null\n const one = missing.length === 1\n return aiDoctrineReview({\n message: `\"${input.name}\" is a copy of the component the plan names, and a copy is all it is: the plan gives it ${\n one ? 'a property' : 'properties'\n } the copy does not have.`,\n violations: [\n {\n rule: null,\n code: 'plan-props-missing',\n message: `The confirmed plan says this component has ${missing.join(', ')}, and the copy has ${\n declared.length ? declared.join(', ') : 'none of them'\n }. Add ${one ? 'it' : 'each of them'}, and use ${\n one ? 'it' : 'them'\n } in the component, or the copy is the component it was copied from.`,\n },\n ],\n })\n}\n\n/**\n * A component job is admitted for a site of the job's own org whose plan\n * includes reusable components, and confirmed only for a plan whose other\n * creations it builds (AGL-3143 §15).\n */\nexport const aiComponentJobAdmission: AiJobAdmission = (context) =>\n aiDraftAdmissionRefusal(context.firestore, {\n orgId: context.orgId,\n hostId: context.hostId,\n kind: 'component',\n org: context.org,\n ownCheck: (hostId) => aiJobPlanCreationsRefusal('component', context, hostId),\n })\n\nexport interface AiJobComponentStepDeps {\n /** The inventory reader; specs hand in a fake. */\n readInventory?: typeof readSiteInventory\n /** The platform's duplicate module, for a plan that starts from a copy. */\n duplicate?: typeof duplicateResource\n}\n\nexport function createAiJobComponentStep(deps: AiJobComponentStepDeps = {}): AiJobStepRunner {\n const readInventory = deps.readInventory ?? readSiteInventory\n const duplicate = deps.duplicate ?? duplicateResource\n return async ({ job, now, signal, firestore, modelFor }) => {\n const hostId = job.hostId\n if (!hostId) throw new Error('a component job names no site, and its admission refuses one')\n const output = (draft: AiDraftRecord, load?: AiLoadEstimate | null, note?: string | null): AiJobOutput => ({\n resource: 'reusableComponent',\n id: draft.id,\n versionId: draft.versionId,\n hostId,\n hostSubdomain: draft.hostSubdomain,\n label: draft.name,\n ...(load ? { load } : {}),\n ...(note ? { note } : {}),\n })\n // The switch's answer for this job, else the routing table's (AGL-2942).\n const model = modelFor?.('job.component') ?? aiModelForStep('job.component')\n\n // A run cut off after its draft was written reports that draft, found by the id the job recorded.\n const draftId = aiJobDraftId(job, 'component')\n const written = await readAiDraft(firestore, { kind: 'component', hostId, id: draftId })\n if (written) return aiUnspentOutcome(model, { outputs: [output(written)] })\n\n const [inventory, orgSnapshot] = await Promise.all([\n readInventory(job.orgId, hostId, { firestore }),\n firestore.collection('orgs').doc(job.orgId).get(),\n ])\n const org = (orgSnapshot.data() ?? null) as Partial<AglynOrgBilling> | null\n const plan = aiConfirmedPlan(job)\n const creation = aiPlanCreation(plan, 'component')\n const name = creation?.name || AI_JOB_COMPONENT_DEFAULT_NAME\n\n // The plan starts from a copy of a component the site has (rule 15): the\n // copy is the draft, and nothing is generated. A source gone since the\n // plan was made is built from the brief instead, and one that lacks a\n // property the plan gives the component is built from the plan\n // (AGL-3143 §14): copying it could only end in the review below. A source\n // this step cannot read still copies.\n const source = creation?.duplicateOf\n ? inventory.components.find((component) => component.id === creation.duplicateOf)\n : undefined\n const sourceReview = source\n ? await aiCopiedComponentReview(firestore, { hostId, id: source.id, name: source.name, plan })\n : null\n if (creation?.duplicateOf && source && !sourceReview?.findings.length) {\n const copy = await duplicate('component', {\n orgId: job.orgId,\n hostId,\n sourceId: creation.duplicateOf,\n name,\n uid: job.createdBy,\n org: org as Record<string, unknown> | null,\n // The same claim the page step makes (AGL-3024): a copy core mints is\n // written under core's own id, never `draftId`, so a step that runs\n // again — a pass resumed, a run cut off after the copy — would mint a\n // second. The job's recorded id makes core replay the one it made.\n attemptKey: draftId,\n })\n if (copy.ok) {\n const hostSubdomain = await aiSiteSubdomain(firestore, hostId)\n // The copy is reported with what the plan promised of it read back\n // (AGL-3024): this branch generates nothing, so without this a plan\n // reading \"duplicate it and add a link\" reports Done on a duplicate\n // that added nothing.\n const review = await aiCopiedComponentReview(firestore, {\n hostId,\n id: copy.id,\n name: copy.name,\n plan,\n })\n if (review) return aiUnspentOutcome(model, { review })\n return aiUnspentOutcome(model, {\n outputs: [\n output({ id: copy.id, versionId: copy.versionId ?? null, name: copy.name, hostSubdomain }),\n ],\n })\n }\n if (copy.ok === false && copy.status === 403) {\n return aiUnspentOutcome(model, { review: aiLimitReview(copy.error) })\n }\n }\n\n const allowance = await aiDraftAllowanceRefusal(firestore, { kind: 'component', hostId, org })\n if (allowance) return aiUnspentOutcome(model, { review: aiLimitReview(allowance) })\n\n let props: ReusableComponentProp[] = []\n const result = await runValidatedGeneration('component', {\n step: 'job.component',\n model,\n instructions: AI_JOB_COMPONENT_INSTRUCTIONS,\n inventory,\n messages: [{ role: 'user', content: aiJobComponentPrompt(job, plan, name) }],\n tool: aiComponentTool(),\n maxTokens: AI_JOB_COMPONENT_STEP_BUDGET.maxTokens(model),\n ...(AI_ROUTING_TABLE['job.component'].thinking\n ? { thinking: AI_ROUTING_TABLE['job.component'].thinking }\n : {}),\n ...(AI_ROUTING_TABLE['job.component'].effort\n ? { effort: AI_ROUTING_TABLE['job.component'].effort }\n : {}),\n // A third-party player the confirmed plan lists for this component is the only one it may embed (AGL-3433).\n context: {\n definesComponent: true,\n ...(creation ? { plannedEmbeds: aiPlanEmbedsFor(plan, { create: creation.name }) } : {}),\n },\n extend: aiComponentCheck({\n inventory,\n plan,\n onProps: (declared) => {\n props = declared\n },\n }),\n ...(signal ? { signal } : {}),\n })\n const spent = aiGenerationSpent(result)\n if (result.status === 'refused') return { ...spent, refused: true }\n if (result.status === 'needs_input') return { ...spent, review: aiDoctrineReview(result) }\n\n const draft = await writeAiDraft(firestore, {\n kind: 'component',\n hostId,\n id: draftId,\n uid: job.createdBy,\n org,\n name,\n nodes: result.value.nodes,\n rootId: result.value.rootId,\n props,\n now,\n })\n if (draft.ok === false) {\n if (draft.status === 404) throw new Error(`site ${hostId} vanished while its component was generated`)\n return { ...spent, review: aiLimitReview(draft.error) }\n }\n // The facts the brief did not give, in the tree or the defaults a page shows (AGL-3056).\n const note = aiBracketedFactsNote({\n tree: { rootId: result.value.rootId, nodes: result.value.nodes as unknown as AiDoctrineTree['nodes'] },\n inventory,\n copy: props.map((prop) => (typeof prop.defaultValue === 'string' ? prop.defaultValue : '')),\n })\n return { ...spent, outputs: [output(draft, result.value.load, note)] }\n }\n}\n\n/** The component step, building the forms and other components its plan creates first (AGL-3143 §15). */\nexport const runAiJobComponentStep = aiBuildingPlanCreations(\n 'component',\n 'job.component',\n createAiJobComponentStep(),\n)\n\n/** Registers the component step and the check a component job passes before it is created or resumed. */\nexport function registerAiComponentJob(): void {\n registerAiJobStep('component', runAiJobComponentStep, {\n minimumMs: AI_JOB_COMPONENT_STEP_MINIMUM_MS,\n minimumMsFor: aiPlanCreationsRunMinimumMs('component'),\n })\n registerAiJobAdmission('component', aiComponentJobAdmission)\n}\n"],"names":["duplicateResource","aiPlanEmbedsFor","AI_STEP_TIERS","AI_ROUTING_TABLE","aiModelForStep","runValidatedGeneration","readSiteInventory","AI_COMPONENT_TOOL_NAME","aiComponentPropKindWords","aiComponentTool","registerAiJobAdmission","aiBuildingPlanCreations","aiJobPlanCreationsRefusal","aiPlanCreationsRunMinimumMs","aiJobStepBudget","aiComponentCheck","aiPlannedComponentProps","aiJobDraftId","aiDraftAdmissionRefusal","aiDraftAllowanceRefusal","aiSiteSubdomain","readAiDraft","readAiDraftComponentProps","writeAiDraft","aiBracketedFactsNote","aiConfirmedPlan","aiDoctrineReview","aiGenerationSpent","aiJobBriefLine","aiLimitReview","aiPlanCreation","aiPlanReferenceLines","aiUnspentOutcome","registerAiJobStep","AI_JOB_COMPONENT_MAX_TOKENS","maxTokens","AI_JOB_COMPONENT_STEP_BUDGET","tier","AI_JOB_COMPONENT_STEP_MINIMUM_MS","minimumMs","AI_JOB_COMPONENT_DEFAULT_NAME","AI_JOB_COMPONENT_INSTRUCTIONS","text","join","aiJobComponentPrompt","job","plan","name","aiCopiedComponentReview","firestore","input","promised","length","declared","hostId","id","reason","message","findings","missing","filter","includes","one","violations","rule","code","aiComponentJobAdmission","context","orgId","kind","org","ownCheck","createAiJobComponentStep","deps","readInventory","duplicate","now","signal","modelFor","orgSnapshot","Error","output","draft","load","note","resource","versionId","hostSubdomain","label","model","draftId","written","outputs","inventory","Promise","all","collection","doc","get","data","creation","source","duplicateOf","components","find","component","undefined","sourceReview","copy","sourceId","uid","createdBy","attemptKey","ok","review","status","error","allowance","props","result","step","instructions","messages","role","content","tool","thinking","effort","definesComponent","plannedEmbeds","create","extend","onProps","spent","refused","nodes","value","rootId","tree","map","prop","defaultValue","runAiJobComponentStep","registerAiComponentJob","minimumMsFor"],"mappings":"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"}
|
|
@@ -108,6 +108,17 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
108
108
|
},
|
|
109
109
|
site: {
|
|
110
110
|
screens: true
|
|
111
|
+
},
|
|
112
|
+
// So are the components and forms a template, layout or component job
|
|
113
|
+
// builds before its own record (AGL-3143 §15).
|
|
114
|
+
template: {
|
|
115
|
+
screens: false
|
|
116
|
+
},
|
|
117
|
+
layout: {
|
|
118
|
+
screens: false
|
|
119
|
+
},
|
|
120
|
+
component: {
|
|
121
|
+
screens: false
|
|
111
122
|
}
|
|
112
123
|
};
|
|
113
124
|
/**
|