@aglyn/plugins-ai 1.0.0-beta.261 → 1.0.0-beta.262

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-brief-dialog.component.d.ts +21 -58
  3. package/src/lib/components/ai-brief-dialog.component.js +8 -132
  4. package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
  5. package/src/lib/components/ai-job-follow.component.js +2 -35
  6. package/src/lib/components/ai-job-follow.component.js.map +1 -1
  7. package/src/lib/components/ai-job-plan.component.d.ts +2 -8
  8. package/src/lib/components/ai-job-plan.component.js +13 -74
  9. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  10. package/src/lib/components/ai-template-collection-field.component.d.ts +2 -12
  11. package/src/lib/components/ai-template-collection-field.component.js +13 -10
  12. package/src/lib/components/ai-template-collection-field.component.js.map +1 -1
  13. package/src/lib/components/ai-upsell-dialog.component.d.ts +3 -25
  14. package/src/lib/components/ai-upsell-dialog.component.js +9 -66
  15. package/src/lib/components/ai-upsell-dialog.component.js.map +1 -1
  16. package/src/lib/components/use-ai-job-run.d.ts +3 -8
  17. package/src/lib/components/use-ai-job-run.js +4 -15
  18. package/src/lib/components/use-ai-job-run.js.map +1 -1
  19. package/src/lib/insights/ai-figure-readers.js +15 -5
  20. package/src/lib/insights/ai-figure-readers.js.map +1 -1
  21. package/src/lib/insights/ai-figure-summaries.d.ts +27 -0
  22. package/src/lib/insights/ai-figure-summaries.js +79 -0
  23. package/src/lib/insights/ai-figure-summaries.js.map +1 -0
  24. package/src/lib/insights/ai-insight-readers.d.ts +2 -1
  25. package/src/lib/insights/ai-insight-readers.js +11 -26
  26. package/src/lib/insights/ai-insight-readers.js.map +1 -1
  27. package/src/lib/jobs/ai-layout-stock-photos.js +11 -1
  28. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  29. package/src/lib/model/ai-brief-copy.d.ts +85 -0
  30. package/src/lib/model/ai-brief-copy.js +152 -0
  31. package/src/lib/model/ai-brief-copy.js.map +1 -0
  32. package/src/lib/model/ai-create-view.d.ts +200 -0
  33. package/src/lib/model/ai-create-view.js +263 -0
  34. package/src/lib/model/ai-create-view.js.map +1 -0
  35. package/src/lib/model/ai-insight.d.ts +1 -1
  36. package/src/lib/model/ai-insight.js +4 -39
  37. package/src/lib/model/ai-insight.js.map +1 -1
  38. package/src/lib/model/ai-job-activity.d.ts +24 -0
  39. package/src/lib/model/ai-job-activity.js +52 -1
  40. package/src/lib/model/ai-job-activity.js.map +1 -1
  41. package/src/lib/model/ai-job-plan-lines.d.ts +46 -0
  42. package/src/lib/model/ai-job-plan-lines.js +70 -0
  43. package/src/lib/model/ai-job-plan-lines.js.map +1 -0
  44. package/src/lib/model/ai-template-collections.d.ts +28 -0
  45. package/src/lib/model/ai-template-collections.js +28 -0
  46. package/src/lib/model/ai-template-collections.js.map +1 -0
  47. package/src/lib/model/ai-upsell-copy.d.ts +41 -0
  48. package/src/lib/model/ai-upsell-copy.js +85 -0
  49. package/src/lib/model/ai-upsell-copy.js.map +1 -0
  50. package/src/lib/runtime/ai-doctrine-validators.js +16 -2
  51. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  52. package/src/lib/runtime/ai-eval-memory-firestore.js +2 -0
  53. package/src/lib/runtime/ai-eval-memory-firestore.js.map +1 -1
  54. package/src/lib/server/ai-create-route.d.ts +32 -0
  55. package/src/lib/server/ai-create-route.js +295 -0
  56. package/src/lib/server/ai-create-route.js.map +1 -0
  57. package/src/lib/server.js +14 -0
  58. package/src/lib/server.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/use-ai-job-run.ts"],"sourcesContent":["'use client'\n\n/**\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 { lockdownRefusalText, parseLockdownRefusal } from '@aglyn/aglyn'\nimport {\n authorizedFetch,\n type MaybeTokenSource,\n} from '@aglyn/shared-util-http/authorized-token'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport {\n AI_JOB_TERMINAL_STATUSES,\n type AiJobKind,\n type AiJobSummary,\n} from '../model/ai-jobs.types'\nimport { followAiJobEvents } from './ai-job-events'\nimport { publishAiJob } from './ai-jobs-store'\n\n/**\n * Starting one AI job from a console control, and following it until it\n * settles (AGL-2919): the jobs route's create door, then its events door, the\n * way the SEO card follows its jobs. A control that starts a job the beat\n * runs shows its progress here and its result when it arrives; closing the\n * control stops following, and the job carries on in AI jobs.\n */\n\n/** What a control asks the create door for. */\nexport interface AiJobRequest {\n orgId: string\n /** `null` for a job about the workspace rather than one site. */\n hostId: string | null\n kind: AiJobKind\n brief: string\n inputs: Record<string, string>\n}\n\n/** Whether a job has stopped moving: finished, or waiting on something only a person can change. */\nexport function aiJobSettled(job: AiJobSummary | null): boolean {\n if (!job) return false\n return (\n AI_JOB_TERMINAL_STATUSES.includes(job.status) ||\n job.status === 'needs_input' ||\n job.status === 'needs_review'\n )\n}\n\n/**\n * Why a settled job has no result to show, in words a person can act on, or\n * `null` for a job that finished.\n */\nexport function aiJobProblem(job: AiJobSummary | null, failed: string): string | null {\n if (!job || !aiJobSettled(job) || job.status === 'done') return null\n if (job.status === 'canceled') return 'The job was canceled.'\n if (job.status === 'needs_review') return job.review?.message || job.error || failed\n return job.error || failed\n}\n\nexport interface AiJobRun {\n /** The job as it last arrived; `null` before one is started. */\n job: AiJobSummary | null\n /** The create door is being asked. */\n starting: boolean\n /** The job exists and has not settled. */\n running: boolean\n /** Why the create door refused, or could not be reached. */\n notice: string | null\n /** Starts the job, `true` once it exists, and follows it from there; `false` when the door refused. */\n start: (request: AiJobRequest) => Promise<boolean>\n /** Stops following and forgets the job, for a control asked again. */\n reset: () => void\n}\n\nexport function useAiJobRun(user: MaybeTokenSource, failed: string): AiJobRun {\n const userRef = useRef(user)\n userRef.current = user\n const [job, setJob] = useState<AiJobSummary | null>(null)\n const [starting, setStarting] = useState(false)\n const [notice, setNotice] = useState<string | null>(null)\n const watchRef = useRef<AbortController | null>(null)\n\n useEffect(() => () => watchRef.current?.abort(), [])\n\n /** Follows a job through the events route until it settles or the control goes. */\n const watch = useCallback(async (jobId: string, orgId: string) => {\n watchRef.current?.abort()\n const controller = new AbortController()\n watchRef.current = controller\n await followAiJobEvents(() => userRef.current, orgId, jobId, controller.signal, setJob)\n }, [])\n\n const start = useCallback(\n async (request: AiJobRequest) => {\n watchRef.current?.abort()\n setStarting(true)\n setNotice(null)\n setJob(null)\n let next: AiJobSummary | undefined\n try {\n const response = await authorizedFetch(userRef.current, '/api/ai/jobs', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(request),\n })\n const payload = await response.json().catch(() => null)\n if (!response.ok) {\n const locked = parseLockdownRefusal(response.status, payload)\n setNotice(locked ? lockdownRefusalText(locked) : String(payload?.error ?? failed))\n return false\n }\n next = payload?.job as AiJobSummary | undefined\n if (!next) {\n setNotice(failed)\n return false\n }\n setJob(next)\n // The indicator counts it from the moment it exists (AGL-3593).\n publishAiJob(next)\n } catch {\n setNotice(failed)\n return false\n } finally {\n setStarting(false)\n }\n if (!aiJobSettled(next)) void watch(next.id, request.orgId)\n return true\n },\n [failed, watch],\n )\n\n const reset = useCallback(() => {\n watchRef.current?.abort()\n setJob(null)\n setNotice(null)\n }, [])\n\n return { job, starting, running: Boolean(job && !aiJobSettled(job)), notice, start, reset }\n}\n"],"names":["lockdownRefusalText","parseLockdownRefusal","authorizedFetch","useCallback","useEffect","useRef","useState","AI_JOB_TERMINAL_STATUSES","followAiJobEvents","publishAiJob","aiJobSettled","job","includes","status","aiJobProblem","failed","review","message","error","useAiJobRun","user","userRef","current","setJob","starting","setStarting","notice","setNotice","watchRef","abort","watch","jobId","orgId","controller","AbortController","signal","start","request","next","response","method","headers","body","JSON","stringify","payload","json","catch","ok","locked","String","id","reset","running","Boolean"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/use-ai-job-run.ts"],"sourcesContent":["'use client'\n\n/**\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 { lockdownRefusalText, parseLockdownRefusal } from '@aglyn/aglyn'\nimport {\n authorizedFetch,\n type MaybeTokenSource,\n} from '@aglyn/shared-util-http/authorized-token'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport type { AiJobKind, AiJobSummary } from '../model/ai-jobs.types'\nimport { aiJobProblem, aiJobSettled } from '../model/ai-job-activity'\nimport { followAiJobEvents } from './ai-job-events'\nimport { publishAiJob } from './ai-jobs-store'\n\n/**\n * Starting one AI job from a console control, and following it until it\n * settles (AGL-2919): the jobs route's create door, then its events door, the\n * way the SEO card follows its jobs. A control that starts a job the beat\n * runs shows its progress here and its result when it arrives; closing the\n * control stops following, and the job carries on in AI jobs.\n */\n\n/** What a control asks the create door for. */\nexport interface AiJobRequest {\n orgId: string\n /** `null` for a job about the workspace rather than one site. */\n hostId: string | null\n kind: AiJobKind\n brief: string\n inputs: Record<string, string>\n}\n\n// Whether a job settled, and why it has nothing to show, are the model's\n// (AGL-3854), which the native apps' follow view reads too.\nexport { aiJobProblem, aiJobSettled }\n\nexport interface AiJobRun {\n /** The job as it last arrived; `null` before one is started. */\n job: AiJobSummary | null\n /** The create door is being asked. */\n starting: boolean\n /** The job exists and has not settled. */\n running: boolean\n /** Why the create door refused, or could not be reached. */\n notice: string | null\n /** Starts the job, `true` once it exists, and follows it from there; `false` when the door refused. */\n start: (request: AiJobRequest) => Promise<boolean>\n /** Stops following and forgets the job, for a control asked again. */\n reset: () => void\n}\n\nexport function useAiJobRun(user: MaybeTokenSource, failed: string): AiJobRun {\n const userRef = useRef(user)\n userRef.current = user\n const [job, setJob] = useState<AiJobSummary | null>(null)\n const [starting, setStarting] = useState(false)\n const [notice, setNotice] = useState<string | null>(null)\n const watchRef = useRef<AbortController | null>(null)\n\n useEffect(() => () => watchRef.current?.abort(), [])\n\n /** Follows a job through the events route until it settles or the control goes. */\n const watch = useCallback(async (jobId: string, orgId: string) => {\n watchRef.current?.abort()\n const controller = new AbortController()\n watchRef.current = controller\n await followAiJobEvents(() => userRef.current, orgId, jobId, controller.signal, setJob)\n }, [])\n\n const start = useCallback(\n async (request: AiJobRequest) => {\n watchRef.current?.abort()\n setStarting(true)\n setNotice(null)\n setJob(null)\n let next: AiJobSummary | undefined\n try {\n const response = await authorizedFetch(userRef.current, '/api/ai/jobs', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(request),\n })\n const payload = await response.json().catch(() => null)\n if (!response.ok) {\n const locked = parseLockdownRefusal(response.status, payload)\n setNotice(locked ? lockdownRefusalText(locked) : String(payload?.error ?? failed))\n return false\n }\n next = payload?.job as AiJobSummary | undefined\n if (!next) {\n setNotice(failed)\n return false\n }\n setJob(next)\n // The indicator counts it from the moment it exists (AGL-3593).\n publishAiJob(next)\n } catch {\n setNotice(failed)\n return false\n } finally {\n setStarting(false)\n }\n if (!aiJobSettled(next)) void watch(next.id, request.orgId)\n return true\n },\n [failed, watch],\n )\n\n const reset = useCallback(() => {\n watchRef.current?.abort()\n setJob(null)\n setNotice(null)\n }, [])\n\n return { job, starting, running: Boolean(job && !aiJobSettled(job)), notice, start, reset }\n}\n"],"names":["lockdownRefusalText","parseLockdownRefusal","authorizedFetch","useCallback","useEffect","useRef","useState","aiJobProblem","aiJobSettled","followAiJobEvents","publishAiJob","useAiJobRun","user","failed","userRef","current","job","setJob","starting","setStarting","notice","setNotice","watchRef","abort","watch","jobId","orgId","controller","AbortController","signal","start","request","next","response","method","headers","body","JSON","stringify","payload","json","catch","ok","locked","status","String","error","id","reset","running","Boolean"],"mappings":"AAAA;AAEA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,mBAAmB,EAAEC,oBAAoB,QAAQ,eAAc;AACxE,SACEC,eAAe,QAEV,2CAA0C;AACjD,SAASC,WAAW,EAAEC,SAAS,EAAEC,MAAM,EAAEC,QAAQ,QAAQ,QAAO;AAEhE,SAASC,YAAY,EAAEC,YAAY,QAAQ,8BAA0B;AACrE,SAASC,iBAAiB,QAAQ,qBAAiB;AACnD,SAASC,YAAY,QAAQ,qBAAiB;AAoB9C,yEAAyE;AACzE,4DAA4D;AAC5D,SAASH,YAAY,EAAEC,YAAY,GAAE;AAiBrC,OAAO,SAASG,YAAYC,IAAsB,EAAEC,MAAc;IAChE,MAAMC,UAAUT,OAAOO;IACvBE,QAAQC,OAAO,GAAGH;IAClB,MAAM,CAACI,KAAKC,OAAO,GAAGX,SAA8B;IACpD,MAAM,CAACY,UAAUC,YAAY,GAAGb,SAAS;IACzC,MAAM,CAACc,QAAQC,UAAU,GAAGf,SAAwB;IACpD,MAAMgB,WAAWjB,OAA+B;IAEhDD,UAAU,IAAM;gBAAMkB;oBAAAA,oBAAAA,SAASP,OAAO,qBAAhBO,kBAAkBC,KAAK;WAAI,EAAE;IAEnD,iFAAiF,GACjF,MAAMC,QAAQrB,YAAY,OAAOsB,OAAeC;YAC9CJ;SAAAA,oBAAAA,SAASP,OAAO,qBAAhBO,kBAAkBC,KAAK;QACvB,MAAMI,aAAa,IAAIC;QACvBN,SAASP,OAAO,GAAGY;QACnB,MAAMlB,kBAAkB,IAAMK,QAAQC,OAAO,EAAEW,OAAOD,OAAOE,WAAWE,MAAM,EAAEZ;IAClF,GAAG,EAAE;IAEL,MAAMa,QAAQ3B,YACZ,OAAO4B;YACLT;SAAAA,oBAAAA,SAASP,OAAO,qBAAhBO,kBAAkBC,KAAK;QACvBJ,YAAY;QACZE,UAAU;QACVJ,OAAO;QACP,IAAIe;QACJ,IAAI;YACF,MAAMC,WAAW,MAAM/B,gBAAgBY,QAAQC,OAAO,EAAE,gBAAgB;gBACtEmB,QAAQ;gBACRC,SAAS;oBAAE,gBAAgB;gBAAmB;gBAC9CC,MAAMC,KAAKC,SAAS,CAACP;YACvB;YACA,MAAMQ,UAAU,MAAMN,SAASO,IAAI,GAAGC,KAAK,CAAC,IAAM;YAClD,IAAI,CAACR,SAASS,EAAE,EAAE;;gBAChB,MAAMC,SAAS1C,qBAAqBgC,SAASW,MAAM,EAAEL;gBACrDlB,UAAUsB,SAAS3C,oBAAoB2C,UAAUE,eAAON,2BAAAA,QAASO,KAAK,mBAAIjC;gBAC1E,OAAO;YACT;YACAmB,OAAOO,2BAAAA,QAASvB,GAAG;YACnB,IAAI,CAACgB,MAAM;gBACTX,UAAUR;gBACV,OAAO;YACT;YACAI,OAAOe;YACP,gEAAgE;YAChEtB,aAAasB;QACf,EAAE,eAAM;YACNX,UAAUR;YACV,OAAO;QACT,SAAU;YACRM,YAAY;QACd;QACA,IAAI,CAACX,aAAawB,OAAO,KAAKR,MAAMQ,KAAKe,EAAE,EAAEhB,QAAQL,KAAK;QAC1D,OAAO;IACT,GACA;QAACb;QAAQW;KAAM;IAGjB,MAAMwB,QAAQ7C,YAAY;YACxBmB;SAAAA,oBAAAA,SAASP,OAAO,qBAAhBO,kBAAkBC,KAAK;QACvBN,OAAO;QACPI,UAAU;IACZ,GAAG,EAAE;IAEL,OAAO;QAAEL;QAAKE;QAAU+B,SAASC,QAAQlC,OAAO,CAACR,aAAaQ;QAAOI;QAAQU;QAAOkB;IAAM;AAC5F"}
@@ -17,6 +17,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
17
17
  */ import { formPeriodKey, formStatsTotals } from "@aglyn/aglyn/app-utils/forms";
18
18
  import { PLUGIN_FIGURE_MAX_ROWS, pluginFigureChange, pluginFigureDay, pluginFigureWindows, registerPluginFigureReader } from "@aglyn/aglyn/plugin-manager/plugin-figures";
19
19
  import { AI_PLUGIN_ID } from "../constants.js";
20
+ import { FORMS_WEEKLY_SUMMARY, SOURCES_WEEKLY_SUMMARY, TOP_PAGES_WEEKLY_SUMMARY, TRAFFIC_WEEKLY_SUMMARY } from "./ai-figure-summaries.js";
20
21
  /** The traffic windows: the Traffic card's own ranges. */ export const AI_TRAFFIC_WINDOWS = [
21
22
  7,
22
23
  14,
@@ -129,7 +130,8 @@ export function trafficFigureReaders(firestore) {
129
130
  id: 'traffic.summary',
130
131
  label: 'Traffic',
131
132
  description: 'Page views, visitors and views a day on the site over the window, each beside the window before it and the change.',
132
- windows: AI_TRAFFIC_WINDOWS
133
+ windows: AI_TRAFFIC_WINDOWS,
134
+ weeklySummary: TRAFFIC_WEEKLY_SUMMARY
133
135
  }, async (db, request)=>{
134
136
  const ids = dayIds(request.now, request.days);
135
137
  const [current, previous] = await Promise.all([
@@ -205,7 +207,8 @@ export function trafficFigureReaders(firestore) {
205
207
  id: 'traffic.pages',
206
208
  label: 'Top pages',
207
209
  description: `The site's ${AI_TRAFFIC_TOP_PAGES} most viewed pages over the window, with each page's share of all views and its change from the window before.`,
208
- windows: AI_TRAFFIC_WINDOWS
210
+ windows: AI_TRAFFIC_WINDOWS,
211
+ weeklySummary: TOP_PAGES_WEEKLY_SUMMARY
209
212
  }, async (db, request)=>{
210
213
  const ids = dayIds(request.now, request.days);
211
214
  const [current, previous] = await Promise.all([
@@ -262,11 +265,17 @@ export function trafficFigureReaders(firestore) {
262
265
  id: 'traffic.sources',
263
266
  label: 'Traffic sources',
264
267
  description: 'Where visits came from over the window: the referring sites, and the campaign sources and campaigns links were tagged with, with each one’s views.',
265
- windows: AI_TRAFFIC_WINDOWS
268
+ windows: AI_TRAFFIC_WINDOWS,
269
+ weeklySummary: SOURCES_WEEKLY_SUMMARY
266
270
  }, async (db, request)=>{
267
271
  const days = await readTrafficDays(db, request.hostId, dayIds(request.now, request.days).current);
268
272
  const all = days.reduce((sum, day)=>sum + day.total, 0);
269
- const referrers = ranked(sumMaps(days, (day)=>day.referrers));
273
+ // The collector stores a referring host with its dots as `_`, because a
274
+ // Firestore field path cannot hold one; a host is read back as written.
275
+ const referrers = ranked(sumMaps(days, (day)=>day.referrers)).map(([host, views])=>[
276
+ host.replace(/_/g, '.'),
277
+ views
278
+ ]);
270
279
  const sources = ranked(sumMaps(days, (day)=>{
271
280
  var _day_utm_source;
272
281
  return (_day_utm_source = day.utm['source']) != null ? _day_utm_source : {};
@@ -375,7 +384,8 @@ export function formsFigureReader(firestore) {
375
384
  id: 'forms.performance',
376
385
  label: 'Forms',
377
386
  description: 'Each form’s views, submissions, the share of views that became a submission, and leads, over the calendar months the window falls in.',
378
- windows: AI_TRAFFIC_WINDOWS
387
+ windows: AI_TRAFFIC_WINDOWS,
388
+ weeklySummary: FORMS_WEEKLY_SUMMARY
379
389
  }, async (db, request)=>{
380
390
  const { current } = pluginFigureWindows(request.now, request.days);
381
391
  const from = formPeriodKey(current.startMs);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/insights/ai-figure-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 { formPeriodKey, formStatsTotals, type FormStats } from '@aglyn/aglyn/app-utils/forms'\nimport {\n PLUGIN_FIGURE_MAX_ROWS,\n pluginFigureChange,\n pluginFigureDay,\n pluginFigureWindows,\n registerPluginFigureReader,\n type PluginFigureReader,\n type PluginFigureRead,\n type PluginFigureRequest,\n type PluginFigureRow,\n type PluginFigureTable,\n} from '@aglyn/aglyn/plugin-manager/plugin-figures'\nimport { AI_PLUGIN_ID } from '../constants'\n\n/**\n * The figure readers this plugin registers for records the PLATFORM keeps\n * (AGL-2915): a site's page-view counters and its forms' counters. Every\n * other reader an insight reads is registered by the plugin that owns its\n * records — commerce, bookings, marketing, the workspace's datasets — on the\n * `plugin-figures` seam, and nothing here reads their documents.\n *\n * Each reader answers with one compact table of aggregates, read-only and\n * about nobody: counts, sums and rates, labeled by a page path, a form's name\n * or a referring site — never a visitor or a submission.\n *\n * The readers use the arithmetic their console cards use: the Traffic card's\n * windows and change (`pluginFigureWindows`, `pluginFigureChange`, which are\n * that card's split and delta), and the forms plugin's month counters through\n * `formStatsTotals`, the one function every forms surface windows by.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** How a reader reaches Firestore: resolved when it reads, never when it registers. */\nexport type AiFigureFirestore = () => Firestore\n\n/** The traffic windows: the Traffic card's own ranges. */\nexport const AI_TRAFFIC_WINDOWS: readonly number[] = [7, 14, 30, 90]\n\n/** A day-by-day table fits a table's row bound only over a short window. */\nexport const AI_TRAFFIC_DAILY_WINDOWS: readonly number[] = [7, 14]\n\n/** Pages a pages table lists; the rest are counted in `omitted`. */\nexport const AI_TRAFFIC_TOP_PAGES = 10\n\n/** Referring sites, and campaign sources and campaigns, a sources table lists. */\nexport const AI_TRAFFIC_TOP_REFERRERS = 8\nexport const AI_TRAFFIC_TOP_CAMPAIGNS = 5\n\n/** Forms a forms table reads. */\nexport const AI_FORMS_READ_LIMIT = 100\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\nconst num = (value: unknown): number => {\n const parsed = typeof value === 'number' ? value : Number(value)\n return Number.isFinite(parsed) ? parsed : 0\n}\n\nconst round1 = (value: number): number => Math.round(value * 10) / 10\n\nconst share = (part: number, whole: number): number | null => (whole > 0 ? round1((part / whole) * 100) : null)\n\nfunction refused(status: 400 | 403 | 404, error: string): PluginFigureRead {\n return { ok: false, status, error }\n}\n\n/** A window's days as the table's period, oldest to newest. */\nfunction periodOf(request: PluginFigureRequest): PluginFigureTable['period'] {\n const { current } = pluginFigureWindows(request.now, request.days)\n return { from: current.from, to: current.to, days: request.days }\n}\n\n/** Every day id of the window and the one before it, newest first. */\nfunction dayIds(now: Date, days: number): { current: string[]; previous: string[] } {\n const { current, previous } = pluginFigureWindows(now, days)\n const walk = (startMs: number, endMs: number): string[] => {\n const ids: string[] = []\n for (let at = endMs - 86_400_000; at >= startMs; at -= 86_400_000) ids.push(pluginFigureDay(new Date(at)))\n return ids\n }\n return { current: walk(current.startMs, current.endMs), previous: walk(previous.startMs, previous.endMs) }\n}\n\n// ── Traffic ───────────────────────────────────────────────────────────────\n\ninterface TrafficDay {\n total: number\n visitors: number\n paths: Record<string, number>\n referrers: Record<string, number>\n utm: Record<string, Record<string, number>>\n}\n\nconst counts = (value: unknown): Record<string, number> => {\n const out: Record<string, number> = {}\n if (!value || typeof value !== 'object') return out\n for (const [key, count] of Object.entries(value as Record<string, unknown>)) {\n const n = num(count)\n if (n > 0) out[key] = n\n }\n return out\n}\n\nasync function readTrafficDays(\n firestore: Firestore,\n hostId: string,\n ids: readonly string[],\n): Promise<TrafficDay[]> {\n if (!ids.length) return []\n const collection = firestore.collection('hosts').doc(hostId).collection('analytics')\n const snapshots = await firestore.getAll(...ids.map((id) => collection.doc(id)))\n return snapshots.map((snapshot) => {\n const data = (snapshot.exists ? snapshot.data() : null) ?? {}\n const utm: Record<string, Record<string, number>> = {}\n for (const [param, values] of Object.entries((data['utm'] ?? {}) as Record<string, unknown>)) {\n utm[param] = counts(values)\n }\n return {\n total: num(data['total']),\n visitors: num(data['visitors']),\n paths: counts(data['paths']),\n referrers: counts(data['referrers']),\n utm,\n }\n })\n}\n\nfunction sumMaps(days: readonly TrafficDay[], pick: (day: TrafficDay) => Record<string, number>): Map<string, number> {\n const totals = new Map<string, number>()\n for (const day of days) {\n for (const [key, count] of Object.entries(pick(day))) totals.set(key, (totals.get(key) ?? 0) + count)\n }\n return totals\n}\n\nconst ranked = (totals: Map<string, number>): Array<[string, number]> =>\n [...totals.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))\n\nfunction siteReader(\n base: Omit<PluginFigureReader, 'read' | 'scope' | 'plugin'>,\n read: (firestore: Firestore, request: PluginFigureRequest & { hostId: string }) => Promise<PluginFigureRead>,\n firestore: AiFigureFirestore,\n plugin: string | null,\n): PluginFigureReader {\n return {\n ...base,\n scope: 'site',\n plugin,\n read: async (request) => {\n if (!request.hostId) return refused(400, 'Open a site to read its figures')\n if (!base.windows.includes(request.days)) return refused(400, 'That window is not one this reader covers')\n return read(firestore(), { ...request, hostId: request.hostId })\n },\n }\n}\n\nexport function trafficFigureReaders(firestore: AiFigureFirestore): PluginFigureReader[] {\n const source = { label: 'Analytics', path: 'analytics' }\n const visitorsNote =\n 'Visitors are counted once per browser tab per day, so one person on two days counts twice.'\n return [\n siteReader(\n {\n id: 'traffic.summary',\n label: 'Traffic',\n description:\n 'Page views, visitors and views a day on the site over the window, each beside the window before it and the change.',\n windows: AI_TRAFFIC_WINDOWS,\n },\n async (db, request) => {\n const ids = dayIds(request.now, request.days)\n const [current, previous] = await Promise.all([\n readTrafficDays(db, request.hostId, ids.current),\n readTrafficDays(db, request.hostId, ids.previous),\n ])\n const total = (days: TrafficDay[]) => days.reduce((sum, day) => sum + day.total, 0)\n const visitors = (days: TrafficDay[]) => days.reduce((sum, day) => sum + day.visitors, 0)\n const views = total(current)\n const viewsBefore = total(previous)\n const perDay = Math.round(views / request.days)\n const perDayBefore = Math.round(viewsBefore / request.days)\n const table: PluginFigureTable = {\n title: 'Traffic',\n source,\n period: periodOf(request),\n columns: [\n { key: 'figure', label: 'Figure', kind: 'text' },\n { key: 'current', label: 'This window', kind: 'count' },\n { key: 'previous', label: 'Window before', kind: 'count' },\n { key: 'change', label: 'Change', kind: 'change' },\n ],\n rows: [\n { figure: 'Page views', current: views, previous: viewsBefore, change: pluginFigureChange(views, viewsBefore) },\n {\n figure: 'Visitors',\n current: visitors(current),\n previous: visitors(previous),\n change: pluginFigureChange(visitors(current), visitors(previous)),\n },\n {\n figure: 'Page views a day',\n current: perDay,\n previous: perDayBefore,\n change: pluginFigureChange(perDay, perDayBefore),\n },\n ],\n omitted: 0,\n notes: [\n visitorsNote,\n ...(viewsBefore ? [] : ['The window before this one recorded no page views, so there is no change to compare.']),\n ],\n }\n return { ok: true, table }\n },\n firestore,\n null,\n ),\n siteReader(\n {\n id: 'traffic.pages',\n label: 'Top pages',\n description: `The site's ${AI_TRAFFIC_TOP_PAGES} most viewed pages over the window, with each page's share of all views and its change from the window before.`,\n windows: AI_TRAFFIC_WINDOWS,\n },\n async (db, request) => {\n const ids = dayIds(request.now, request.days)\n const [current, previous] = await Promise.all([\n readTrafficDays(db, request.hostId, ids.current),\n readTrafficDays(db, request.hostId, ids.previous),\n ])\n const now = ranked(sumMaps(current, (day) => day.paths))\n const before = sumMaps(previous, (day) => day.paths)\n const all = now.reduce((sum, [, count]) => sum + count, 0)\n const rows: PluginFigureRow[] = now.slice(0, AI_TRAFFIC_TOP_PAGES).map(([path, views]) => ({\n page: path,\n views,\n share: share(views, all),\n change: pluginFigureChange(views, before.get(path) ?? null),\n }))\n return {\n ok: true,\n table: {\n title: 'Top pages',\n source,\n period: periodOf(request),\n columns: [\n { key: 'page', label: 'Page', kind: 'text' },\n { key: 'views', label: 'Views', kind: 'count' },\n { key: 'share', label: 'Share of views', kind: 'percent' },\n { key: 'change', label: 'Change', kind: 'change' },\n ],\n rows,\n omitted: Math.max(0, now.length - rows.length),\n notes: [],\n },\n }\n },\n firestore,\n null,\n ),\n siteReader(\n {\n id: 'traffic.sources',\n label: 'Traffic sources',\n description:\n 'Where visits came from over the window: the referring sites, and the campaign sources and campaigns links were tagged with, with each one’s views.',\n windows: AI_TRAFFIC_WINDOWS,\n },\n async (db, request) => {\n const days = await readTrafficDays(db, request.hostId, dayIds(request.now, request.days).current)\n const all = days.reduce((sum, day) => sum + day.total, 0)\n const referrers = ranked(sumMaps(days, (day) => day.referrers))\n const sources = ranked(sumMaps(days, (day) => day.utm['source'] ?? {}))\n const campaigns = ranked(sumMaps(days, (day) => day.utm['campaign'] ?? {}))\n const rowsOf = (entries: Array<[string, number]>, limit: number, type: string) =>\n entries.slice(0, limit).map(([name, views]) => ({ source: name, type, views, share: share(views, all) }))\n const rows = [\n ...rowsOf(referrers, AI_TRAFFIC_TOP_REFERRERS, 'Referring site'),\n ...rowsOf(sources, AI_TRAFFIC_TOP_CAMPAIGNS, 'Campaign source'),\n ...rowsOf(campaigns, AI_TRAFFIC_TOP_CAMPAIGNS, 'Campaign'),\n ]\n return {\n ok: true,\n table: {\n title: 'Traffic sources',\n source,\n period: periodOf(request),\n columns: [\n { key: 'source', label: 'Source', kind: 'text' },\n { key: 'type', label: 'Kind', kind: 'text' },\n { key: 'views', label: 'Views', kind: 'count' },\n { key: 'share', label: 'Share of all views', kind: 'percent' },\n ],\n rows,\n omitted:\n Math.max(0, referrers.length - AI_TRAFFIC_TOP_REFERRERS) +\n Math.max(0, sources.length - AI_TRAFFIC_TOP_CAMPAIGNS) +\n Math.max(0, campaigns.length - AI_TRAFFIC_TOP_CAMPAIGNS),\n notes: ['A visit with no referring site, such as a typed address, is in no row.'],\n },\n }\n },\n firestore,\n null,\n ),\n siteReader(\n {\n id: 'traffic.daily',\n label: 'Traffic by day',\n description: 'Page views and visitors on each day of the window, oldest first, for spotting a day that stood out.',\n windows: AI_TRAFFIC_DAILY_WINDOWS,\n },\n async (db, request) => {\n const ids = [...dayIds(request.now, request.days).current].reverse()\n const days = await readTrafficDays(db, request.hostId, ids)\n return {\n ok: true,\n table: {\n title: 'Traffic by day',\n source,\n period: periodOf(request),\n columns: [\n { key: 'day', label: 'Day', kind: 'date' },\n { key: 'views', label: 'Page views', kind: 'count' },\n { key: 'visitors', label: 'Visitors', kind: 'count' },\n ],\n rows: days.map((day, index) => ({ day: ids[index], views: day.total, visitors: day.visitors })),\n omitted: 0,\n notes: [visitorsNote],\n },\n }\n },\n firestore,\n null,\n ),\n ]\n}\n\n// ── Forms ─────────────────────────────────────────────────────────────────\n\nexport function formsFigureReader(firestore: AiFigureFirestore): PluginFigureReader {\n return siteReader(\n {\n id: 'forms.performance',\n label: 'Forms',\n description:\n 'Each form’s views, submissions, the share of views that became a submission, and leads, over the calendar months the window falls in.',\n windows: AI_TRAFFIC_WINDOWS,\n },\n async (db, request) => {\n const { current } = pluginFigureWindows(request.now, request.days)\n const from = formPeriodKey(current.startMs)\n const to = formPeriodKey(current.endMs - 1)\n const snapshot = await db\n .collection('hosts')\n .doc(request.hostId)\n .collection('forms')\n .limit(AI_FORMS_READ_LIMIT)\n .get()\n const forms = snapshot.docs\n .map((doc) => ({ doc, data: (doc.data() ?? {}) as Record<string, unknown> }))\n .filter(({ data }) => !data['deletedAt'])\n .map(({ doc, data }) => {\n const totals = formStatsTotals(data['stats'] as FormStats | undefined, { from, to })\n // Every submission to a form is counted on it, so a form with none\n // recorded in the window had none: 0, not a blank the answer has to\n // hedge over (AGL-3834). Leads are counted only on a form that\n // routes them, so one that does reads 0 the same way, and a form\n // that never has keeps a blank, which the table's note explains.\n const submissions = totals.submissions ?? 0\n const routesLeads = (data['routing'] as { lead?: unknown } | undefined)?.lead === true\n return {\n form: str(data['displayName']) || doc.id,\n views: totals.views,\n submissions,\n completion: totals.views ? share(submissions, totals.views) : null,\n leads: totals.leads ?? (routesLeads ? 0 : null),\n }\n })\n .sort((a, b) => b.submissions - a.submissions || a.form.localeCompare(b.form))\n const monthStart = from ? `${from}-01` : current.from\n return {\n ok: true,\n table: {\n title: 'Forms',\n source: { label: 'Forms', path: 'forms' },\n period: {\n from: monthStart,\n to: current.to,\n days: Math.round((Date.parse(current.to) - Date.parse(monthStart)) / 86_400_000) + 1,\n },\n columns: [\n { key: 'form', label: 'Form', kind: 'text' },\n { key: 'views', label: 'Views', kind: 'count' },\n { key: 'submissions', label: 'Submissions', kind: 'count' },\n { key: 'completion', label: 'Views that became a submission', kind: 'percent' },\n { key: 'leads', label: 'Leads', kind: 'count' },\n ],\n rows: forms.slice(0, PLUGIN_FIGURE_MAX_ROWS),\n omitted: Math.max(0, forms.length - PLUGIN_FIGURE_MAX_ROWS),\n notes: [\n 'Form figures are kept by calendar month, so they cover whole months from the first day of the month the window starts in.',\n 'Views are counted in the visitor’s browser and submissions on the server, so a rate can pass 100%.',\n 'A blank Leads figure means the form does not add its submissions to Leads.',\n ],\n },\n }\n },\n firestore,\n 'forms',\n )\n}\n\n/** Every reader this plugin registers, for the platform's own records. */\nexport function aiFigureReaders(firestore: AiFigureFirestore): PluginFigureReader[] {\n return [...trafficFigureReaders(firestore), formsFigureReader(firestore)]\n}\n\n/**\n * Registers them from the console surface, the one surface that runs insight\n * jobs. By a call, never by an import (AGL-3025).\n */\nexport function registerAiFigureReaders(firestore: AiFigureFirestore): void {\n for (const reader of aiFigureReaders(firestore)) {\n registerPluginFigureReader(reader, { pluginId: AI_PLUGIN_ID })\n }\n}\n"],"names":["formPeriodKey","formStatsTotals","PLUGIN_FIGURE_MAX_ROWS","pluginFigureChange","pluginFigureDay","pluginFigureWindows","registerPluginFigureReader","AI_PLUGIN_ID","AI_TRAFFIC_WINDOWS","AI_TRAFFIC_DAILY_WINDOWS","AI_TRAFFIC_TOP_PAGES","AI_TRAFFIC_TOP_REFERRERS","AI_TRAFFIC_TOP_CAMPAIGNS","AI_FORMS_READ_LIMIT","str","value","trim","num","parsed","Number","isFinite","round1","Math","round","share","part","whole","refused","status","error","ok","periodOf","request","current","now","days","from","to","dayIds","previous","walk","startMs","endMs","ids","at","push","Date","counts","out","key","count","Object","entries","n","readTrafficDays","firestore","hostId","length","collection","doc","snapshots","getAll","map","id","snapshot","data","exists","utm","param","values","total","visitors","paths","referrers","sumMaps","pick","totals","Map","day","set","get","ranked","sort","a","b","localeCompare","siteReader","base","read","plugin","scope","windows","includes","trafficFigureReaders","source","label","path","visitorsNote","description","db","Promise","all","reduce","sum","views","viewsBefore","perDay","perDayBefore","table","title","period","columns","kind","rows","figure","change","omitted","notes","before","slice","page","max","sources","campaigns","rowsOf","limit","type","name","reverse","index","formsFigureReader","forms","docs","filter","submissions","routesLeads","lead","form","completion","leads","monthStart","parse","aiFigureReaders","registerAiFigureReaders","reader","pluginId"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/insights/ai-figure-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 { formPeriodKey, formStatsTotals, type FormStats } from '@aglyn/aglyn/app-utils/forms'\nimport {\n PLUGIN_FIGURE_MAX_ROWS,\n pluginFigureChange,\n pluginFigureDay,\n pluginFigureWindows,\n registerPluginFigureReader,\n type PluginFigureReader,\n type PluginFigureRead,\n type PluginFigureRequest,\n type PluginFigureRow,\n type PluginFigureTable,\n} from '@aglyn/aglyn/plugin-manager/plugin-figures'\nimport { AI_PLUGIN_ID } from '../constants'\nimport {\n FORMS_WEEKLY_SUMMARY,\n SOURCES_WEEKLY_SUMMARY,\n TOP_PAGES_WEEKLY_SUMMARY,\n TRAFFIC_WEEKLY_SUMMARY,\n} from './ai-figure-summaries'\n\n/**\n * The figure readers this plugin registers for records the PLATFORM keeps\n * (AGL-2915): a site's page-view counters and its forms' counters. Every\n * other reader an insight reads is registered by the plugin that owns its\n * records — commerce, bookings, marketing, the workspace's datasets — on the\n * `plugin-figures` seam, and nothing here reads their documents.\n *\n * Each reader answers with one compact table of aggregates, read-only and\n * about nobody: counts, sums and rates, labeled by a page path, a form's name\n * or a referring site — never a visitor or a submission.\n *\n * The readers use the arithmetic their console cards use: the Traffic card's\n * windows and change (`pluginFigureWindows`, `pluginFigureChange`, which are\n * that card's split and delta), and the forms plugin's month counters through\n * `formStatsTotals`, the one function every forms surface windows by.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** How a reader reaches Firestore: resolved when it reads, never when it registers. */\nexport type AiFigureFirestore = () => Firestore\n\n/** The traffic windows: the Traffic card's own ranges. */\nexport const AI_TRAFFIC_WINDOWS: readonly number[] = [7, 14, 30, 90]\n\n/** A day-by-day table fits a table's row bound only over a short window. */\nexport const AI_TRAFFIC_DAILY_WINDOWS: readonly number[] = [7, 14]\n\n/** Pages a pages table lists; the rest are counted in `omitted`. */\nexport const AI_TRAFFIC_TOP_PAGES = 10\n\n/** Referring sites, and campaign sources and campaigns, a sources table lists. */\nexport const AI_TRAFFIC_TOP_REFERRERS = 8\nexport const AI_TRAFFIC_TOP_CAMPAIGNS = 5\n\n/** Forms a forms table reads. */\nexport const AI_FORMS_READ_LIMIT = 100\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\nconst num = (value: unknown): number => {\n const parsed = typeof value === 'number' ? value : Number(value)\n return Number.isFinite(parsed) ? parsed : 0\n}\n\nconst round1 = (value: number): number => Math.round(value * 10) / 10\n\nconst share = (part: number, whole: number): number | null => (whole > 0 ? round1((part / whole) * 100) : null)\n\nfunction refused(status: 400 | 403 | 404, error: string): PluginFigureRead {\n return { ok: false, status, error }\n}\n\n/** A window's days as the table's period, oldest to newest. */\nfunction periodOf(request: PluginFigureRequest): PluginFigureTable['period'] {\n const { current } = pluginFigureWindows(request.now, request.days)\n return { from: current.from, to: current.to, days: request.days }\n}\n\n/** Every day id of the window and the one before it, newest first. */\nfunction dayIds(now: Date, days: number): { current: string[]; previous: string[] } {\n const { current, previous } = pluginFigureWindows(now, days)\n const walk = (startMs: number, endMs: number): string[] => {\n const ids: string[] = []\n for (let at = endMs - 86_400_000; at >= startMs; at -= 86_400_000) ids.push(pluginFigureDay(new Date(at)))\n return ids\n }\n return { current: walk(current.startMs, current.endMs), previous: walk(previous.startMs, previous.endMs) }\n}\n\n// ── Traffic ───────────────────────────────────────────────────────────────\n\ninterface TrafficDay {\n total: number\n visitors: number\n paths: Record<string, number>\n referrers: Record<string, number>\n utm: Record<string, Record<string, number>>\n}\n\nconst counts = (value: unknown): Record<string, number> => {\n const out: Record<string, number> = {}\n if (!value || typeof value !== 'object') return out\n for (const [key, count] of Object.entries(value as Record<string, unknown>)) {\n const n = num(count)\n if (n > 0) out[key] = n\n }\n return out\n}\n\nasync function readTrafficDays(\n firestore: Firestore,\n hostId: string,\n ids: readonly string[],\n): Promise<TrafficDay[]> {\n if (!ids.length) return []\n const collection = firestore.collection('hosts').doc(hostId).collection('analytics')\n const snapshots = await firestore.getAll(...ids.map((id) => collection.doc(id)))\n return snapshots.map((snapshot) => {\n const data = (snapshot.exists ? snapshot.data() : null) ?? {}\n const utm: Record<string, Record<string, number>> = {}\n for (const [param, values] of Object.entries((data['utm'] ?? {}) as Record<string, unknown>)) {\n utm[param] = counts(values)\n }\n return {\n total: num(data['total']),\n visitors: num(data['visitors']),\n paths: counts(data['paths']),\n referrers: counts(data['referrers']),\n utm,\n }\n })\n}\n\nfunction sumMaps(days: readonly TrafficDay[], pick: (day: TrafficDay) => Record<string, number>): Map<string, number> {\n const totals = new Map<string, number>()\n for (const day of days) {\n for (const [key, count] of Object.entries(pick(day))) totals.set(key, (totals.get(key) ?? 0) + count)\n }\n return totals\n}\n\nconst ranked = (totals: Map<string, number>): Array<[string, number]> =>\n [...totals.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))\n\nfunction siteReader(\n base: Omit<PluginFigureReader, 'read' | 'scope' | 'plugin'>,\n read: (firestore: Firestore, request: PluginFigureRequest & { hostId: string }) => Promise<PluginFigureRead>,\n firestore: AiFigureFirestore,\n plugin: string | null,\n): PluginFigureReader {\n return {\n ...base,\n scope: 'site',\n plugin,\n read: async (request) => {\n if (!request.hostId) return refused(400, 'Open a site to read its figures')\n if (!base.windows.includes(request.days)) return refused(400, 'That window is not one this reader covers')\n return read(firestore(), { ...request, hostId: request.hostId })\n },\n }\n}\n\nexport function trafficFigureReaders(firestore: AiFigureFirestore): PluginFigureReader[] {\n const source = { label: 'Analytics', path: 'analytics' }\n const visitorsNote =\n 'Visitors are counted once per browser tab per day, so one person on two days counts twice.'\n return [\n siteReader(\n {\n id: 'traffic.summary',\n label: 'Traffic',\n description:\n 'Page views, visitors and views a day on the site over the window, each beside the window before it and the change.',\n windows: AI_TRAFFIC_WINDOWS,\n weeklySummary: TRAFFIC_WEEKLY_SUMMARY,\n },\n async (db, request) => {\n const ids = dayIds(request.now, request.days)\n const [current, previous] = await Promise.all([\n readTrafficDays(db, request.hostId, ids.current),\n readTrafficDays(db, request.hostId, ids.previous),\n ])\n const total = (days: TrafficDay[]) => days.reduce((sum, day) => sum + day.total, 0)\n const visitors = (days: TrafficDay[]) => days.reduce((sum, day) => sum + day.visitors, 0)\n const views = total(current)\n const viewsBefore = total(previous)\n const perDay = Math.round(views / request.days)\n const perDayBefore = Math.round(viewsBefore / request.days)\n const table: PluginFigureTable = {\n title: 'Traffic',\n source,\n period: periodOf(request),\n columns: [\n { key: 'figure', label: 'Figure', kind: 'text' },\n { key: 'current', label: 'This window', kind: 'count' },\n { key: 'previous', label: 'Window before', kind: 'count' },\n { key: 'change', label: 'Change', kind: 'change' },\n ],\n rows: [\n { figure: 'Page views', current: views, previous: viewsBefore, change: pluginFigureChange(views, viewsBefore) },\n {\n figure: 'Visitors',\n current: visitors(current),\n previous: visitors(previous),\n change: pluginFigureChange(visitors(current), visitors(previous)),\n },\n {\n figure: 'Page views a day',\n current: perDay,\n previous: perDayBefore,\n change: pluginFigureChange(perDay, perDayBefore),\n },\n ],\n omitted: 0,\n notes: [\n visitorsNote,\n ...(viewsBefore ? [] : ['The window before this one recorded no page views, so there is no change to compare.']),\n ],\n }\n return { ok: true, table }\n },\n firestore,\n null,\n ),\n siteReader(\n {\n id: 'traffic.pages',\n label: 'Top pages',\n description: `The site's ${AI_TRAFFIC_TOP_PAGES} most viewed pages over the window, with each page's share of all views and its change from the window before.`,\n windows: AI_TRAFFIC_WINDOWS,\n weeklySummary: TOP_PAGES_WEEKLY_SUMMARY,\n },\n async (db, request) => {\n const ids = dayIds(request.now, request.days)\n const [current, previous] = await Promise.all([\n readTrafficDays(db, request.hostId, ids.current),\n readTrafficDays(db, request.hostId, ids.previous),\n ])\n const now = ranked(sumMaps(current, (day) => day.paths))\n const before = sumMaps(previous, (day) => day.paths)\n const all = now.reduce((sum, [, count]) => sum + count, 0)\n const rows: PluginFigureRow[] = now.slice(0, AI_TRAFFIC_TOP_PAGES).map(([path, views]) => ({\n page: path,\n views,\n share: share(views, all),\n change: pluginFigureChange(views, before.get(path) ?? null),\n }))\n return {\n ok: true,\n table: {\n title: 'Top pages',\n source,\n period: periodOf(request),\n columns: [\n { key: 'page', label: 'Page', kind: 'text' },\n { key: 'views', label: 'Views', kind: 'count' },\n { key: 'share', label: 'Share of views', kind: 'percent' },\n { key: 'change', label: 'Change', kind: 'change' },\n ],\n rows,\n omitted: Math.max(0, now.length - rows.length),\n notes: [],\n },\n }\n },\n firestore,\n null,\n ),\n siteReader(\n {\n id: 'traffic.sources',\n label: 'Traffic sources',\n description:\n 'Where visits came from over the window: the referring sites, and the campaign sources and campaigns links were tagged with, with each one’s views.',\n windows: AI_TRAFFIC_WINDOWS,\n weeklySummary: SOURCES_WEEKLY_SUMMARY,\n },\n async (db, request) => {\n const days = await readTrafficDays(db, request.hostId, dayIds(request.now, request.days).current)\n const all = days.reduce((sum, day) => sum + day.total, 0)\n // The collector stores a referring host with its dots as `_`, because a\n // Firestore field path cannot hold one; a host is read back as written.\n const referrers = ranked(sumMaps(days, (day) => day.referrers)).map(\n ([host, views]): [string, number] => [host.replace(/_/g, '.'), views],\n )\n const sources = ranked(sumMaps(days, (day) => day.utm['source'] ?? {}))\n const campaigns = ranked(sumMaps(days, (day) => day.utm['campaign'] ?? {}))\n const rowsOf = (entries: Array<[string, number]>, limit: number, type: string) =>\n entries.slice(0, limit).map(([name, views]) => ({ source: name, type, views, share: share(views, all) }))\n const rows = [\n ...rowsOf(referrers, AI_TRAFFIC_TOP_REFERRERS, 'Referring site'),\n ...rowsOf(sources, AI_TRAFFIC_TOP_CAMPAIGNS, 'Campaign source'),\n ...rowsOf(campaigns, AI_TRAFFIC_TOP_CAMPAIGNS, 'Campaign'),\n ]\n return {\n ok: true,\n table: {\n title: 'Traffic sources',\n source,\n period: periodOf(request),\n columns: [\n { key: 'source', label: 'Source', kind: 'text' },\n { key: 'type', label: 'Kind', kind: 'text' },\n { key: 'views', label: 'Views', kind: 'count' },\n { key: 'share', label: 'Share of all views', kind: 'percent' },\n ],\n rows,\n omitted:\n Math.max(0, referrers.length - AI_TRAFFIC_TOP_REFERRERS) +\n Math.max(0, sources.length - AI_TRAFFIC_TOP_CAMPAIGNS) +\n Math.max(0, campaigns.length - AI_TRAFFIC_TOP_CAMPAIGNS),\n notes: ['A visit with no referring site, such as a typed address, is in no row.'],\n },\n }\n },\n firestore,\n null,\n ),\n siteReader(\n {\n id: 'traffic.daily',\n label: 'Traffic by day',\n description: 'Page views and visitors on each day of the window, oldest first, for spotting a day that stood out.',\n windows: AI_TRAFFIC_DAILY_WINDOWS,\n },\n async (db, request) => {\n const ids = [...dayIds(request.now, request.days).current].reverse()\n const days = await readTrafficDays(db, request.hostId, ids)\n return {\n ok: true,\n table: {\n title: 'Traffic by day',\n source,\n period: periodOf(request),\n columns: [\n { key: 'day', label: 'Day', kind: 'date' },\n { key: 'views', label: 'Page views', kind: 'count' },\n { key: 'visitors', label: 'Visitors', kind: 'count' },\n ],\n rows: days.map((day, index) => ({ day: ids[index], views: day.total, visitors: day.visitors })),\n omitted: 0,\n notes: [visitorsNote],\n },\n }\n },\n firestore,\n null,\n ),\n ]\n}\n\n// ── Forms ─────────────────────────────────────────────────────────────────\n\nexport function formsFigureReader(firestore: AiFigureFirestore): PluginFigureReader {\n return siteReader(\n {\n id: 'forms.performance',\n label: 'Forms',\n description:\n 'Each form’s views, submissions, the share of views that became a submission, and leads, over the calendar months the window falls in.',\n windows: AI_TRAFFIC_WINDOWS,\n weeklySummary: FORMS_WEEKLY_SUMMARY,\n },\n async (db, request) => {\n const { current } = pluginFigureWindows(request.now, request.days)\n const from = formPeriodKey(current.startMs)\n const to = formPeriodKey(current.endMs - 1)\n const snapshot = await db\n .collection('hosts')\n .doc(request.hostId)\n .collection('forms')\n .limit(AI_FORMS_READ_LIMIT)\n .get()\n const forms = snapshot.docs\n .map((doc) => ({ doc, data: (doc.data() ?? {}) as Record<string, unknown> }))\n .filter(({ data }) => !data['deletedAt'])\n .map(({ doc, data }) => {\n const totals = formStatsTotals(data['stats'] as FormStats | undefined, { from, to })\n // Every submission to a form is counted on it, so a form with none\n // recorded in the window had none: 0, not a blank the answer has to\n // hedge over (AGL-3834). Leads are counted only on a form that\n // routes them, so one that does reads 0 the same way, and a form\n // that never has keeps a blank, which the table's note explains.\n const submissions = totals.submissions ?? 0\n const routesLeads = (data['routing'] as { lead?: unknown } | undefined)?.lead === true\n return {\n form: str(data['displayName']) || doc.id,\n views: totals.views,\n submissions,\n completion: totals.views ? share(submissions, totals.views) : null,\n leads: totals.leads ?? (routesLeads ? 0 : null),\n }\n })\n .sort((a, b) => b.submissions - a.submissions || a.form.localeCompare(b.form))\n const monthStart = from ? `${from}-01` : current.from\n return {\n ok: true,\n table: {\n title: 'Forms',\n source: { label: 'Forms', path: 'forms' },\n period: {\n from: monthStart,\n to: current.to,\n days: Math.round((Date.parse(current.to) - Date.parse(monthStart)) / 86_400_000) + 1,\n },\n columns: [\n { key: 'form', label: 'Form', kind: 'text' },\n { key: 'views', label: 'Views', kind: 'count' },\n { key: 'submissions', label: 'Submissions', kind: 'count' },\n { key: 'completion', label: 'Views that became a submission', kind: 'percent' },\n { key: 'leads', label: 'Leads', kind: 'count' },\n ],\n rows: forms.slice(0, PLUGIN_FIGURE_MAX_ROWS),\n omitted: Math.max(0, forms.length - PLUGIN_FIGURE_MAX_ROWS),\n notes: [\n 'Form figures are kept by calendar month, so they cover whole months from the first day of the month the window starts in.',\n 'Views are counted in the visitor’s browser and submissions on the server, so a rate can pass 100%.',\n 'A blank Leads figure means the form does not add its submissions to Leads.',\n ],\n },\n }\n },\n firestore,\n 'forms',\n )\n}\n\n/** Every reader this plugin registers, for the platform's own records. */\nexport function aiFigureReaders(firestore: AiFigureFirestore): PluginFigureReader[] {\n return [...trafficFigureReaders(firestore), formsFigureReader(firestore)]\n}\n\n/**\n * Registers them from the console surface, the one surface that runs insight\n * jobs. By a call, never by an import (AGL-3025).\n */\nexport function registerAiFigureReaders(firestore: AiFigureFirestore): void {\n for (const reader of aiFigureReaders(firestore)) {\n registerPluginFigureReader(reader, { pluginId: AI_PLUGIN_ID })\n }\n}\n"],"names":["formPeriodKey","formStatsTotals","PLUGIN_FIGURE_MAX_ROWS","pluginFigureChange","pluginFigureDay","pluginFigureWindows","registerPluginFigureReader","AI_PLUGIN_ID","FORMS_WEEKLY_SUMMARY","SOURCES_WEEKLY_SUMMARY","TOP_PAGES_WEEKLY_SUMMARY","TRAFFIC_WEEKLY_SUMMARY","AI_TRAFFIC_WINDOWS","AI_TRAFFIC_DAILY_WINDOWS","AI_TRAFFIC_TOP_PAGES","AI_TRAFFIC_TOP_REFERRERS","AI_TRAFFIC_TOP_CAMPAIGNS","AI_FORMS_READ_LIMIT","str","value","trim","num","parsed","Number","isFinite","round1","Math","round","share","part","whole","refused","status","error","ok","periodOf","request","current","now","days","from","to","dayIds","previous","walk","startMs","endMs","ids","at","push","Date","counts","out","key","count","Object","entries","n","readTrafficDays","firestore","hostId","length","collection","doc","snapshots","getAll","map","id","snapshot","data","exists","utm","param","values","total","visitors","paths","referrers","sumMaps","pick","totals","Map","day","set","get","ranked","sort","a","b","localeCompare","siteReader","base","read","plugin","scope","windows","includes","trafficFigureReaders","source","label","path","visitorsNote","description","weeklySummary","db","Promise","all","reduce","sum","views","viewsBefore","perDay","perDayBefore","table","title","period","columns","kind","rows","figure","change","omitted","notes","before","slice","page","max","host","replace","sources","campaigns","rowsOf","limit","type","name","reverse","index","formsFigureReader","forms","docs","filter","submissions","routesLeads","lead","form","completion","leads","monthStart","parse","aiFigureReaders","registerAiFigureReaders","reader","pluginId"],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { PluginFigureWeeklySummary } from '@aglyn/aglyn/plugin-manager/plugin-figures';
18
+ /**
19
+ * How the traffic and forms readers read in the weekly summary (AGL-3867).
20
+ * Each answers `[]` for a week it has nothing to say about, and for a table
21
+ * that is not the shape it was written for, so a changed reader drops out of
22
+ * the email rather than misreporting.
23
+ */
24
+ export declare const TRAFFIC_WEEKLY_SUMMARY: PluginFigureWeeklySummary;
25
+ export declare const TOP_PAGES_WEEKLY_SUMMARY: PluginFigureWeeklySummary;
26
+ export declare const SOURCES_WEEKLY_SUMMARY: PluginFigureWeeklySummary;
27
+ export declare const FORMS_WEEKLY_SUMMARY: PluginFigureWeeklySummary;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { figureCount, figureHasColumn, figureRanked, figureRow, figureWithChange, weeklySummaryDay } from "@aglyn/aglyn/app-utils/weekly-summary";
17
+ /**
18
+ * How the traffic and forms readers read in the weekly summary (AGL-3867).
19
+ * Each answers `[]` for a week it has nothing to say about, and for a table
20
+ * that is not the shape it was written for, so a changed reader drops out of
21
+ * the email rather than misreporting.
22
+ */ export const TRAFFIC_WEEKLY_SUMMARY = {
23
+ order: 10,
24
+ lines: (table)=>{
25
+ const visitors = figureRow(table, 'figure', 'Visitors');
26
+ const views = figureRow(table, 'figure', 'Page views');
27
+ if (!figureCount(visitors == null ? void 0 : visitors['current']) && !figureCount(views == null ? void 0 : views['current'])) return [];
28
+ return [
29
+ [
30
+ visitors ? `Visitors: ${figureWithChange(table, visitors, 'current', 'change')}` : null,
31
+ views ? `Page views: ${figureWithChange(table, views, 'current', 'change')}` : null
32
+ ].filter(Boolean).join(' · ')
33
+ ];
34
+ },
35
+ headline: (table)=>{
36
+ var _figureRow;
37
+ const count = figureCount((_figureRow = figureRow(table, 'figure', 'Visitors')) == null ? void 0 : _figureRow['current']);
38
+ return count ? {
39
+ count,
40
+ one: 'visitor',
41
+ many: 'visitors'
42
+ } : null;
43
+ }
44
+ };
45
+ export const TOP_PAGES_WEEKLY_SUMMARY = {
46
+ order: 20,
47
+ lines: (table)=>{
48
+ const top = figureHasColumn(table, 'views') ? figureRanked(table, 'page', 'views', 'views') : null;
49
+ return top ? [
50
+ `Top pages: ${top}`
51
+ ] : [];
52
+ }
53
+ };
54
+ export const SOURCES_WEEKLY_SUMMARY = {
55
+ order: 30,
56
+ lines: (table)=>{
57
+ const top = figureHasColumn(table, 'views') ? figureRanked(table, 'source', 'views', 'views') : null;
58
+ return top ? [
59
+ `Top sources: ${top}`
60
+ ] : [];
61
+ }
62
+ };
63
+ export const FORMS_WEEKLY_SUMMARY = {
64
+ order: 40,
65
+ lines: (table)=>{
66
+ if (!figureHasColumn(table, 'submissions')) return [];
67
+ const total = table.rows.reduce((sum, row)=>sum + figureCount(row['submissions']), 0);
68
+ if (!total) return [];
69
+ // Form figures are kept by calendar month, so the line says so rather
70
+ // than calling a month-to-date count "last week".
71
+ const since = table.period ? ` since ${weeklySummaryDay(table.period.from)}` : '';
72
+ const top = figureRanked(table, 'form', 'submissions');
73
+ return [
74
+ `Form submissions${since}: ${total.toLocaleString('en-US')}${top ? ` (${top})` : ''}`
75
+ ];
76
+ }
77
+ };
78
+
79
+ //# sourceMappingURL=ai-figure-summaries.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/insights/ai-figure-summaries.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 { PluginFigureWeeklySummary } from '@aglyn/aglyn/plugin-manager/plugin-figures'\nimport {\n figureCount,\n figureHasColumn,\n figureRanked,\n figureRow,\n figureWithChange,\n weeklySummaryDay,\n} from '@aglyn/aglyn/app-utils/weekly-summary'\n\n/**\n * How the traffic and forms readers read in the weekly summary (AGL-3867).\n * Each answers `[]` for a week it has nothing to say about, and for a table\n * that is not the shape it was written for, so a changed reader drops out of\n * the email rather than misreporting.\n */\n\nexport const TRAFFIC_WEEKLY_SUMMARY: PluginFigureWeeklySummary = {\n order: 10,\n lines: (table) => {\n const visitors = figureRow(table, 'figure', 'Visitors')\n const views = figureRow(table, 'figure', 'Page views')\n if (!figureCount(visitors?.['current']) && !figureCount(views?.['current'])) return []\n return [\n [\n visitors ? `Visitors: ${figureWithChange(table, visitors, 'current', 'change')}` : null,\n views ? `Page views: ${figureWithChange(table, views, 'current', 'change')}` : null,\n ]\n .filter(Boolean)\n .join(' · '),\n ]\n },\n headline: (table) => {\n const count = figureCount(figureRow(table, 'figure', 'Visitors')?.['current'])\n return count ? { count, one: 'visitor', many: 'visitors' } : null\n },\n}\n\nexport const TOP_PAGES_WEEKLY_SUMMARY: PluginFigureWeeklySummary = {\n order: 20,\n lines: (table) => {\n const top = figureHasColumn(table, 'views') ? figureRanked(table, 'page', 'views', 'views') : null\n return top ? [`Top pages: ${top}`] : []\n },\n}\n\nexport const SOURCES_WEEKLY_SUMMARY: PluginFigureWeeklySummary = {\n order: 30,\n lines: (table) => {\n const top = figureHasColumn(table, 'views') ? figureRanked(table, 'source', 'views', 'views') : null\n return top ? [`Top sources: ${top}`] : []\n },\n}\n\nexport const FORMS_WEEKLY_SUMMARY: PluginFigureWeeklySummary = {\n order: 40,\n lines: (table) => {\n if (!figureHasColumn(table, 'submissions')) return []\n const total = table.rows.reduce((sum, row) => sum + figureCount(row['submissions']), 0)\n if (!total) return []\n // Form figures are kept by calendar month, so the line says so rather\n // than calling a month-to-date count \"last week\".\n const since = table.period ? ` since ${weeklySummaryDay(table.period.from)}` : ''\n const top = figureRanked(table, 'form', 'submissions')\n return [`Form submissions${since}: ${total.toLocaleString('en-US')}${top ? ` (${top})` : ''}`]\n },\n}\n"],"names":["figureCount","figureHasColumn","figureRanked","figureRow","figureWithChange","weeklySummaryDay","TRAFFIC_WEEKLY_SUMMARY","order","lines","table","visitors","views","filter","Boolean","join","headline","count","one","many","TOP_PAGES_WEEKLY_SUMMARY","top","SOURCES_WEEKLY_SUMMARY","FORMS_WEEKLY_SUMMARY","total","rows","reduce","sum","row","since","period","from","toLocaleString"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAGD,SACEA,WAAW,EACXC,eAAe,EACfC,YAAY,EACZC,SAAS,EACTC,gBAAgB,EAChBC,gBAAgB,QACX,wCAAuC;AAE9C;;;;;CAKC,GAED,OAAO,MAAMC,yBAAoD;IAC/DC,OAAO;IACPC,OAAO,CAACC;QACN,MAAMC,WAAWP,UAAUM,OAAO,UAAU;QAC5C,MAAME,QAAQR,UAAUM,OAAO,UAAU;QACzC,IAAI,CAACT,YAAYU,4BAAAA,QAAU,CAAC,UAAU,KAAK,CAACV,YAAYW,yBAAAA,KAAO,CAAC,UAAU,GAAG,OAAO,EAAE;QACtF,OAAO;YACL;gBACED,WAAW,CAAC,UAAU,EAAEN,iBAAiBK,OAAOC,UAAU,WAAW,WAAW,GAAG;gBACnFC,QAAQ,CAAC,YAAY,EAAEP,iBAAiBK,OAAOE,OAAO,WAAW,WAAW,GAAG;aAChF,CACEC,MAAM,CAACC,SACPC,IAAI,CAAC;SACT;IACH;IACAC,UAAU,CAACN;YACiBN;QAA1B,MAAMa,QAAQhB,aAAYG,aAAAA,UAAUM,OAAO,UAAU,gCAA3BN,UAAwC,CAAC,UAAU;QAC7E,OAAOa,QAAQ;YAAEA;YAAOC,KAAK;YAAWC,MAAM;QAAW,IAAI;IAC/D;AACF,EAAC;AAED,OAAO,MAAMC,2BAAsD;IACjEZ,OAAO;IACPC,OAAO,CAACC;QACN,MAAMW,MAAMnB,gBAAgBQ,OAAO,WAAWP,aAAaO,OAAO,QAAQ,SAAS,WAAW;QAC9F,OAAOW,MAAM;YAAC,CAAC,WAAW,EAAEA,KAAK;SAAC,GAAG,EAAE;IACzC;AACF,EAAC;AAED,OAAO,MAAMC,yBAAoD;IAC/Dd,OAAO;IACPC,OAAO,CAACC;QACN,MAAMW,MAAMnB,gBAAgBQ,OAAO,WAAWP,aAAaO,OAAO,UAAU,SAAS,WAAW;QAChG,OAAOW,MAAM;YAAC,CAAC,aAAa,EAAEA,KAAK;SAAC,GAAG,EAAE;IAC3C;AACF,EAAC;AAED,OAAO,MAAME,uBAAkD;IAC7Df,OAAO;IACPC,OAAO,CAACC;QACN,IAAI,CAACR,gBAAgBQ,OAAO,gBAAgB,OAAO,EAAE;QACrD,MAAMc,QAAQd,MAAMe,IAAI,CAACC,MAAM,CAAC,CAACC,KAAKC,MAAQD,MAAM1B,YAAY2B,GAAG,CAAC,cAAc,GAAG;QACrF,IAAI,CAACJ,OAAO,OAAO,EAAE;QACrB,sEAAsE;QACtE,kDAAkD;QAClD,MAAMK,QAAQnB,MAAMoB,MAAM,GAAG,CAAC,OAAO,EAAExB,iBAAiBI,MAAMoB,MAAM,CAACC,IAAI,GAAG,GAAG;QAC/E,MAAMV,MAAMlB,aAAaO,OAAO,QAAQ;QACxC,OAAO;YAAC,CAAC,gBAAgB,EAAEmB,MAAM,EAAE,EAAEL,MAAMQ,cAAc,CAAC,WAAWX,MAAM,CAAC,EAAE,EAAEA,IAAI,CAAC,CAAC,GAAG,IAAI;SAAC;IAChG;AACF,EAAC"}
@@ -16,6 +16,7 @@
16
16
  */
17
17
  import { type ResolvedPluginFigureReader } from '@aglyn/aglyn/plugin-manager/plugin-figures';
18
18
  import { type AiInsightSurface, type AiInsightTable } from '../model/ai-insight';
19
+ import { type ReleasedFigurePlugins } from '@aglyn/tenant-data-admin/server/figure-readers';
19
20
  import type { AiInsightReadRequest } from '../tools/ai-insight-tool';
20
21
  import type { AiDatasetCatalogEntry } from './ai-dataset-catalog';
21
22
  /**
@@ -30,7 +31,7 @@ import type { AiDatasetCatalogEntry } from './ai-dataset-catalog';
30
31
  * to the job's workspace, and the member's reach, are the job's own checks.
31
32
  */
32
33
  /** Which of a set of plugins are past their release flags for an org; the platform's flags by default. */
33
- export type AiReleasedPlugins = (pluginIds: string[], orgId: string) => Promise<string[]>;
34
+ export type AiReleasedPlugins = ReleasedFigurePlugins;
34
35
  export interface AiInsightReaderContext {
35
36
  orgId: string;
36
37
  /** The site the figures are about; `null` for a workspace-level question. */
@@ -14,35 +14,20 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
14
14
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
- */ import { checkEntitlement } from "@aglyn/aglyn/app-utils/plan-entitlements";
18
- import { isHostPluginEnabled, isPluginEnabled } from "@aglyn/aglyn/plugin-manager/enabled-plugins";
19
- import { listPluginFigureReaders, normalizePluginFigureTable, pluginFigureParams, pluginFigureReaderPlugin } from "@aglyn/aglyn/plugin-manager/plugin-figures";
17
+ */ import { normalizePluginFigureTable, pluginFigureParams } from "@aglyn/aglyn/plugin-manager/plugin-figures";
20
18
  import { AI_INSIGHT_MAX_READS, AI_INSIGHT_SURFACE_READERS, aiInsightCellText } from "../model/ai-insight.js";
21
- import { filterEnabledPluginsByReleaseFlags } from "@aglyn/tenant-data-admin/server/release-flags";
22
- const releasedByFlags = (pluginIds, orgId)=>filterEnabledPluginsByReleaseFlags(pluginIds, {
23
- orgId,
24
- authorization: null
25
- });
19
+ import { eligibleFigureReaders } from "@aglyn/tenant-data-admin/server/figure-readers";
26
20
  /** The readers a job on this surface may read, ordered by id so a prompt lists them in the same order every time. */ export async function aiInsightReaders(context) {
27
- var _context_released;
28
21
  const prefixes = AI_INSIGHT_SURFACE_READERS[context.surface];
29
- const org = context.org;
30
- const candidates = listPluginFigureReaders().filter(({ reader })=>{
31
- if (!prefixes.includes(reader.id.split('.')[0])) return false;
32
- // A site reader needs a site; a workspace reader reads with or without one.
33
- if (reader.scope === 'site' && !context.hostId) return false;
34
- return !reader.feature || checkEntitlement(org, reader.feature);
35
- });
36
- const plugins = [
37
- ...new Set(candidates.map(pluginFigureReaderPlugin).filter((id)=>Boolean(id)))
38
- ];
39
- const released = new Set(plugins.length ? await ((_context_released = context.released) != null ? _context_released : releasedByFlags)(plugins, context.orgId) : []);
40
- return candidates.filter((resolved)=>{
41
- const plugin = pluginFigureReaderPlugin(resolved);
42
- if (!plugin) return true;
43
- if (!released.has(plugin)) return false;
44
- return context.hostId ? isHostPluginEnabled(org, context.host, plugin) : isPluginEnabled(org, plugin);
45
- });
22
+ return eligibleFigureReaders(_extends({
23
+ orgId: context.orgId,
24
+ hostId: context.hostId,
25
+ org: context.org,
26
+ host: context.host,
27
+ accept: (reader)=>prefixes.includes(reader.id.split('.')[0])
28
+ }, context.released ? {
29
+ released: context.released
30
+ } : {}));
46
31
  }
47
32
  /** The window a reader is read over: the one asked for when it covers it, else the nearest it does. */ export function aiInsightReaderDays(windows, asked) {
48
33
  if (!windows.length) return 0;
@@ -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-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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"}
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 {\n normalizePluginFigureTable,\n pluginFigureParams,\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 {\n eligibleFigureReaders,\n type ReleasedFigurePlugins,\n} from '@aglyn/tenant-data-admin/server/figure-readers'\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 = ReleasedFigurePlugins\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 return eligibleFigureReaders({\n orgId: context.orgId,\n hostId: context.hostId,\n org: context.org,\n host: context.host,\n accept: (reader) => prefixes.includes(reader.id.split('.')[0]),\n ...(context.released ? { released: context.released } : {}),\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":["normalizePluginFigureTable","pluginFigureParams","AI_INSIGHT_MAX_READS","AI_INSIGHT_SURFACE_READERS","aiInsightCellText","eligibleFigureReaders","aiInsightReaders","context","prefixes","surface","orgId","hostId","org","host","accept","reader","includes","id","split","released","aiInsightReaderDays","windows","asked","length","sort","a","b","Math","abs","aiInsightReaderCatalog","readers","map","join","params","param","name","required","description","label","aiInsightDatasetCatalog","datasets","dataset","fields","field","type","readAiInsightTables","requests","base","maxTables","tables","refusals","seen","Set","request","resolved","find","days","ok","push","missing","key","JSON","stringify","has","add","read","error","table","ref","scope","console","aiInsightTableText","period","from","to","lines","title","source","columns","column","rows","row","index","omitted","note","notes"],"mappings":"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"}
@@ -1207,7 +1207,17 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
1207
1207
  'boxing',
1208
1208
  'boxer',
1209
1209
  'beach',
1210
- 'circus'
1210
+ 'circus',
1211
+ // Another sport's ball, never "medicine ball": a basketball court covered a deadlift night (AGL-3864).
1212
+ 'basketball',
1213
+ 'football',
1214
+ 'soccer',
1215
+ 'baseball',
1216
+ 'volleyball',
1217
+ 'tennis',
1218
+ 'golf',
1219
+ 'rugby',
1220
+ 'hockey'
1211
1221
  ]
1212
1222
  },
1213
1223
  {