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

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 (64) hide show
  1. package/package.json +17 -17
  2. package/src/lib/insights/ai-figure-summaries.d.ts +1 -1
  3. package/src/lib/insights/ai-figure-summaries.js +55 -38
  4. package/src/lib/insights/ai-figure-summaries.js.map +1 -1
  5. package/src/lib/insights/ai-insight-digest.d.ts +2 -0
  6. package/src/lib/insights/ai-insight-digest.js +7 -4
  7. package/src/lib/insights/ai-insight-digest.js.map +1 -1
  8. package/src/lib/jobs/ai-job-insight-step.d.ts +1 -1
  9. package/src/lib/jobs/ai-job-insight-step.js +5 -3
  10. package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
  11. package/src/lib/jobs/ai-job-page-language.d.ts +2 -0
  12. package/src/lib/jobs/ai-job-page-language.js +23 -2
  13. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  14. package/src/lib/jobs/ai-job-page-sections.js +8 -2
  15. package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
  16. package/src/lib/jobs/ai-job-plan-step.d.ts +14 -1
  17. package/src/lib/jobs/ai-job-plan-step.js +44 -9
  18. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  19. package/src/lib/jobs/ai-job-site-datasets.d.ts +11 -1
  20. package/src/lib/jobs/ai-job-site-datasets.js +32 -7
  21. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
  22. package/src/lib/jobs/ai-jobs-notify.d.ts +8 -1
  23. package/src/lib/jobs/ai-jobs-notify.js +44 -2
  24. package/src/lib/jobs/ai-jobs-notify.js.map +1 -1
  25. package/src/lib/jobs/ai-layout-stock-photos.d.ts +91 -11
  26. package/src/lib/jobs/ai-layout-stock-photos.js +1046 -48
  27. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  28. package/src/lib/jobs/fixtures/ai-stock-photo-juniper-records-recordings.d.ts +23 -0
  29. package/src/lib/jobs/fixtures/ai-stock-photo-juniper-records-recordings.js +163 -0
  30. package/src/lib/jobs/fixtures/ai-stock-photo-juniper-records-recordings.js.map +1 -0
  31. package/src/lib/jobs/fixtures/ai-stock-photo-learning-center-recordings.d.ts +23 -0
  32. package/src/lib/jobs/fixtures/ai-stock-photo-learning-center-recordings.js +53 -0
  33. package/src/lib/jobs/fixtures/ai-stock-photo-learning-center-recordings.js.map +1 -0
  34. package/src/lib/jobs/fixtures/ai-stock-photo-synchronicity-recordings.d.ts +21 -0
  35. package/src/lib/jobs/fixtures/ai-stock-photo-synchronicity-recordings.js +119 -0
  36. package/src/lib/jobs/fixtures/ai-stock-photo-synchronicity-recordings.js.map +1 -0
  37. package/src/lib/layout-language/ai-layout-compiler.js +24 -11
  38. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  39. package/src/lib/layout-language/ai-layout-listings.d.ts +7 -0
  40. package/src/lib/layout-language/ai-layout-listings.js +12 -0
  41. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  42. package/src/lib/layout-language/ai-layout-pictures.d.ts +27 -8
  43. package/src/lib/layout-language/ai-layout-pictures.js +85 -4
  44. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  45. package/src/lib/layout-language/ai-layout-promised-copy.d.ts +54 -0
  46. package/src/lib/layout-language/ai-layout-promised-copy.js +104 -0
  47. package/src/lib/layout-language/ai-layout-promised-copy.js.map +1 -0
  48. package/src/lib/model/ai-insight.d.ts +9 -0
  49. package/src/lib/model/ai-insight.js +12 -0
  50. package/src/lib/model/ai-insight.js.map +1 -1
  51. package/src/lib/model/ai-job-notice.d.ts +7 -1
  52. package/src/lib/model/ai-job-notice.js +10 -9
  53. package/src/lib/model/ai-job-notice.js.map +1 -1
  54. package/src/lib/model/ai-plan-said-sections.d.ts +81 -0
  55. package/src/lib/model/ai-plan-said-sections.js +168 -0
  56. package/src/lib/model/ai-plan-said-sections.js.map +1 -0
  57. package/src/lib/model/ai-site-job.d.ts +8 -2
  58. package/src/lib/model/ai-site-job.js +7 -1
  59. package/src/lib/model/ai-site-job.js.map +1 -1
  60. package/src/lib/runtime/ai-doctrine-validators.d.ts +15 -1
  61. package/src/lib/runtime/ai-doctrine-validators.js +80 -1
  62. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  63. package/src/lib/runtime/ai-palette.generated.js +11 -3
  64. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-ai",
3
- "version": "1.0.0-beta.262",
3
+ "version": "1.0.0-beta.264",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,22 +25,22 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.262",
29
- "@aglyn/shared-data-enums": "1.0.0-beta.262",
30
- "@aglyn/shared-data-mdi": "1.0.0-beta.262",
31
- "@aglyn/shared-data-types": "1.0.0-beta.262",
32
- "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.262",
33
- "@aglyn/shared-ui-jsx": "1.0.0-beta.262",
34
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.262",
35
- "@aglyn/shared-ui-theme": "1.0.0-beta.262",
36
- "@aglyn/shared-util-email": "1.0.0-beta.262",
37
- "@aglyn/shared-util-errors": "1.0.0-beta.262",
38
- "@aglyn/shared-util-http": "1.0.0-beta.262",
39
- "@aglyn/shared-util-timestamp": "1.0.0-beta.262",
40
- "@aglyn/shared-util-tools": "1.0.0-beta.262",
41
- "@aglyn/tenant-data-admin": "1.0.0-beta.262",
42
- "@aglyn/tenant-feature-instance": "1.0.0-beta.262",
43
- "@aglyn/tenant-runtime": "1.0.0-beta.262",
28
+ "@aglyn/aglyn": "1.0.0-beta.264",
29
+ "@aglyn/shared-data-enums": "1.0.0-beta.264",
30
+ "@aglyn/shared-data-mdi": "1.0.0-beta.264",
31
+ "@aglyn/shared-data-types": "1.0.0-beta.264",
32
+ "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.264",
33
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.264",
34
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.264",
35
+ "@aglyn/shared-ui-theme": "1.0.0-beta.264",
36
+ "@aglyn/shared-util-email": "1.0.0-beta.264",
37
+ "@aglyn/shared-util-errors": "1.0.0-beta.264",
38
+ "@aglyn/shared-util-http": "1.0.0-beta.264",
39
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.264",
40
+ "@aglyn/shared-util-tools": "1.0.0-beta.264",
41
+ "@aglyn/tenant-data-admin": "1.0.0-beta.264",
42
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.264",
43
+ "@aglyn/tenant-runtime": "1.0.0-beta.264",
44
44
  "@swc/helpers": "0.5.23"
45
45
  },
46
46
  "peerDependencies": {
@@ -17,7 +17,7 @@
17
17
  import type { PluginFigureWeeklySummary } from '@aglyn/aglyn/plugin-manager/plugin-figures';
18
18
  /**
19
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
20
+ * Each answers `null` for a week it has nothing to say about, and for a table
21
21
  * that is not the shape it was written for, so a changed reader drops out of
22
22
  * the email rather than misreporting.
23
23
  */
@@ -13,66 +13,83 @@
13
13
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
- */ import { figureCount, figureHasColumn, figureRanked, figureRow, figureWithChange, weeklySummaryDay } from "@aglyn/aglyn/app-utils/weekly-summary";
16
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
+ import { figureCount, figureHasColumn, figureRanked, figureRow, figureStat, weeklySummaryDay } from "@aglyn/aglyn/app-utils/weekly-summary";
17
18
  /**
18
19
  * 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
+ * Each answers `null` for a week it has nothing to say about, and for a table
20
21
  * that is not the shape it was written for, so a changed reader drops out of
21
22
  * the email rather than misreporting.
22
23
  */ export const TRAFFIC_WEEKLY_SUMMARY = {
23
24
  order: 10,
24
- lines: (table)=>{
25
+ summarize: (table)=>{
25
26
  const visitors = figureRow(table, 'figure', 'Visitors');
26
27
  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;
28
+ if (!figureCount(visitors == null ? void 0 : visitors['current']) && !figureCount(views == null ? void 0 : views['current'])) return null;
29
+ return {
30
+ title: 'Traffic',
31
+ stats: [
32
+ ...visitors ? [
33
+ figureStat(table, visitors, 'current', 'Visitors', {
34
+ change: 'change',
35
+ total: {
36
+ one: 'visitor',
37
+ many: 'visitors'
38
+ }
39
+ })
40
+ ] : [],
41
+ ...views ? [
42
+ figureStat(table, views, 'current', 'Page views', {
43
+ change: 'change'
44
+ })
45
+ ] : []
46
+ ]
47
+ };
43
48
  }
44
49
  };
45
50
  export const TOP_PAGES_WEEKLY_SUMMARY = {
46
51
  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
+ summarize: (table)=>{
53
+ const rows = figureHasColumn(table, 'views') ? figureRanked(table, 'page', 'views') : [];
54
+ return rows.length ? {
55
+ title: 'Top pages',
56
+ rows: rows.map((row)=>_extends({}, row, {
57
+ value: `${row.value} views`
58
+ }))
59
+ } : null;
52
60
  }
53
61
  };
54
62
  export const SOURCES_WEEKLY_SUMMARY = {
55
63
  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
- ] : [];
64
+ summarize: (table)=>{
65
+ const rows = figureHasColumn(table, 'views') ? figureRanked(table, 'source', 'views') : [];
66
+ return rows.length ? {
67
+ title: 'Where visits came from',
68
+ rows: rows.map((row)=>_extends({}, row, {
69
+ value: `${row.value} views`
70
+ }))
71
+ } : null;
61
72
  }
62
73
  };
63
74
  export const FORMS_WEEKLY_SUMMARY = {
64
75
  order: 40,
65
- lines: (table)=>{
66
- if (!figureHasColumn(table, 'submissions')) return [];
76
+ summarize: (table)=>{
77
+ if (!figureHasColumn(table, 'submissions')) return null;
67
78
  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
79
+ if (!total) return null;
80
+ // Form figures are kept by calendar month, so the block says so rather
70
81
  // 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
- ];
82
+ const since = table.period ? weeklySummaryDay(table.period.from) : null;
83
+ return {
84
+ title: 'Forms',
85
+ stats: [
86
+ {
87
+ label: since ? `Submissions since ${since}` : 'Submissions',
88
+ value: total.toLocaleString('en-US')
89
+ }
90
+ ],
91
+ rows: figureRanked(table, 'form', 'submissions')
92
+ };
76
93
  }
77
94
  };
78
95
 
@@ -1 +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"}
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 figureStat,\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 `null` 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 summarize: (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 null\n return {\n title: 'Traffic',\n stats: [\n ...(visitors\n ? [figureStat(table, visitors, 'current', 'Visitors', { change: 'change', total: { one: 'visitor', many: 'visitors' } })]\n : []),\n ...(views ? [figureStat(table, views, 'current', 'Page views', { change: 'change' })] : []),\n ],\n }\n },\n}\n\nexport const TOP_PAGES_WEEKLY_SUMMARY: PluginFigureWeeklySummary = {\n order: 20,\n summarize: (table) => {\n const rows = figureHasColumn(table, 'views') ? figureRanked(table, 'page', 'views') : []\n return rows.length ? { title: 'Top pages', rows: rows.map((row) => ({ ...row, value: `${row.value} views` })) } : null\n },\n}\n\nexport const SOURCES_WEEKLY_SUMMARY: PluginFigureWeeklySummary = {\n order: 30,\n summarize: (table) => {\n const rows = figureHasColumn(table, 'views') ? figureRanked(table, 'source', 'views') : []\n return rows.length ? { title: 'Where visits came from', rows: rows.map((row) => ({ ...row, value: `${row.value} views` })) } : null\n },\n}\n\nexport const FORMS_WEEKLY_SUMMARY: PluginFigureWeeklySummary = {\n order: 40,\n summarize: (table) => {\n if (!figureHasColumn(table, 'submissions')) return null\n const total = table.rows.reduce((sum, row) => sum + figureCount(row['submissions']), 0)\n if (!total) return null\n // Form figures are kept by calendar month, so the block says so rather\n // than calling a month-to-date count \"last week\".\n const since = table.period ? weeklySummaryDay(table.period.from) : null\n return {\n title: 'Forms',\n stats: [\n {\n label: since ? `Submissions since ${since}` : 'Submissions',\n value: total.toLocaleString('en-US'),\n },\n ],\n rows: figureRanked(table, 'form', 'submissions'),\n }\n },\n}\n"],"names":["figureCount","figureHasColumn","figureRanked","figureRow","figureStat","weeklySummaryDay","TRAFFIC_WEEKLY_SUMMARY","order","summarize","table","visitors","views","title","stats","change","total","one","many","TOP_PAGES_WEEKLY_SUMMARY","rows","length","map","row","value","SOURCES_WEEKLY_SUMMARY","FORMS_WEEKLY_SUMMARY","reduce","sum","since","period","from","label","toLocaleString"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAGD,SACEA,WAAW,EACXC,eAAe,EACfC,YAAY,EACZC,SAAS,EACTC,UAAU,EACVC,gBAAgB,QACX,wCAAuC;AAE9C;;;;;CAKC,GAED,OAAO,MAAMC,yBAAoD;IAC/DC,OAAO;IACPC,WAAW,CAACC;QACV,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;QACpF,OAAO;YACLC,OAAO;YACPC,OAAO;mBACDH,WACA;oBAACN,WAAWK,OAAOC,UAAU,WAAW,YAAY;wBAAEI,QAAQ;wBAAUC,OAAO;4BAAEC,KAAK;4BAAWC,MAAM;wBAAW;oBAAE;iBAAG,GACvH,EAAE;mBACFN,QAAQ;oBAACP,WAAWK,OAAOE,OAAO,WAAW,cAAc;wBAAEG,QAAQ;oBAAS;iBAAG,GAAG,EAAE;aAC3F;QACH;IACF;AACF,EAAC;AAED,OAAO,MAAMI,2BAAsD;IACjEX,OAAO;IACPC,WAAW,CAACC;QACV,MAAMU,OAAOlB,gBAAgBQ,OAAO,WAAWP,aAAaO,OAAO,QAAQ,WAAW,EAAE;QACxF,OAAOU,KAAKC,MAAM,GAAG;YAAER,OAAO;YAAaO,MAAMA,KAAKE,GAAG,CAAC,CAACC,MAAS,aAAKA;oBAAKC,OAAO,GAAGD,IAAIC,KAAK,CAAC,MAAM,CAAC;;QAAK,IAAI;IACpH;AACF,EAAC;AAED,OAAO,MAAMC,yBAAoD;IAC/DjB,OAAO;IACPC,WAAW,CAACC;QACV,MAAMU,OAAOlB,gBAAgBQ,OAAO,WAAWP,aAAaO,OAAO,UAAU,WAAW,EAAE;QAC1F,OAAOU,KAAKC,MAAM,GAAG;YAAER,OAAO;YAA0BO,MAAMA,KAAKE,GAAG,CAAC,CAACC,MAAS,aAAKA;oBAAKC,OAAO,GAAGD,IAAIC,KAAK,CAAC,MAAM,CAAC;;QAAK,IAAI;IACjI;AACF,EAAC;AAED,OAAO,MAAME,uBAAkD;IAC7DlB,OAAO;IACPC,WAAW,CAACC;QACV,IAAI,CAACR,gBAAgBQ,OAAO,gBAAgB,OAAO;QACnD,MAAMM,QAAQN,MAAMU,IAAI,CAACO,MAAM,CAAC,CAACC,KAAKL,MAAQK,MAAM3B,YAAYsB,GAAG,CAAC,cAAc,GAAG;QACrF,IAAI,CAACP,OAAO,OAAO;QACnB,uEAAuE;QACvE,kDAAkD;QAClD,MAAMa,QAAQnB,MAAMoB,MAAM,GAAGxB,iBAAiBI,MAAMoB,MAAM,CAACC,IAAI,IAAI;QACnE,OAAO;YACLlB,OAAO;YACPC,OAAO;gBACL;oBACEkB,OAAOH,QAAQ,CAAC,kBAAkB,EAAEA,OAAO,GAAG;oBAC9CL,OAAOR,MAAMiB,cAAc,CAAC;gBAC9B;aACD;YACDb,MAAMjB,aAAaO,OAAO,QAAQ;QACpC;IACF;AACF,EAAC"}
@@ -164,6 +164,8 @@ export declare function aiInsightDigestEmailText(input: {
164
164
  workspaceName?: string;
165
165
  /** The AI credits the digest's job drew; 0 or absent says nothing. */
166
166
  creditsSpent?: number;
167
+ /** The week the figures cover, "Oct 5 – Oct 11"; absent says "this week". */
168
+ periodLabel?: string;
167
169
  }): string;
168
170
  /**
169
171
  * One call of the sweep: delivery for this week and the last on the first
@@ -18,7 +18,8 @@ import { INSIGHT_DIGESTS_FIELD, insightDigestSubscribed, notificationMuted } fro
18
18
  import { hostRoleFor } from "@aglyn/aglyn/app-utils/organizations";
19
19
  import { checkEntitlement, resolveBrandingProfile, resolveEffectivePlan } from "@aglyn/aglyn/app-utils/plan-entitlements";
20
20
  import { isReleaseFlagOnForOrg, parseOrgReleaseFlagOverrides } from "@aglyn/aglyn/app-utils/release-flags";
21
- import { AI_INSIGHTS_COLLECTION, aiInsightIsoWeek } from "../model/ai-insight.js";
21
+ import { weeklySummaryPeriod } from "@aglyn/aglyn/app-utils/weekly-summary";
22
+ import { aiInsightDigestPeriod, AI_INSIGHTS_COLLECTION, aiInsightIsoWeek } from "../model/ai-insight.js";
22
23
  import { isAiOnForSite } from "../model/ai-site-switch.js";
23
24
  import { cancelAiJob, createAiJob, getAiJob } from "../jobs/ai-jobs.js";
24
25
  export const AI_INSIGHTS_DIGEST_CRON_ID = 'ai-insights-digest';
@@ -56,7 +57,7 @@ const str = (value)=>typeof value === 'string' ? value.trim() : '';
56
57
  const drew = credits ? ` Writing it used ${credits.toLocaleString('en-US')} of ` + `${workspace ? `${workspace}’s` : 'the workspace’s'} AI credits.` : '';
57
58
  const forWhom = workspace || 'this workspace';
58
59
  return [
59
- `Here is what ${input.siteName}'s figures showed this week.${drew}`,
60
+ input.periodLabel ? `Here is what ${input.siteName}'s figures showed last week, ${input.periodLabel}.${drew}` : `Here is what ${input.siteName}'s figures showed this week.${drew}`,
60
61
  '',
61
62
  ...input.insights.map((insight, index)=>`${index + 1}. ${insight.text}`),
62
63
  '',
@@ -124,7 +125,8 @@ async function readUsers(firestore, uids) {
124
125
  brief: `Weekly insights for ${site.name}`,
125
126
  inputs: {
126
127
  surface: 'digest',
127
- week
128
+ week,
129
+ through: weeklySummaryPeriod(now).to
128
130
  },
129
131
  createdBy: site.creator
130
132
  }, now);
@@ -240,7 +242,8 @@ async function readUsers(firestore, uids) {
240
242
  analyticsUrl,
241
243
  settingsUrl: origin ? `${origin}/manage/notifications` : null,
242
244
  workspaceName: str(org['name']),
243
- creditsSpent: job.creditsSpent
245
+ creditsSpent: job.creditsSpent,
246
+ periodLabel: aiInsightDigestPeriod(job.inputs, new Date(Number(marker.createdAtMs) || now.getTime())).label
244
247
  }),
245
248
  fromName: branding.fromName,
246
249
  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/**\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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"}
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 { weeklySummaryPeriod } from '@aglyn/aglyn/app-utils/weekly-summary'\nimport {\n aiInsightDigestPeriod,\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 /** The week the figures cover, \"Oct 5 – Oct 11\"; absent says \"this week\". */\n periodLabel?: string\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 input.periodLabel\n ? `Here is what ${input.siteName}'s figures showed last week, ${input.periodLabel}.${drew}`\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, through: weeklySummaryPeriod(now).to },\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 periodLabel: aiInsightDigestPeriod(job.inputs, new Date(Number(marker.createdAtMs) || now.getTime())).label,\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","weeklySummaryPeriod","aiInsightDigestPeriod","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","periodLabel","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","through","to","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","subject","label","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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"}
@@ -26,7 +26,7 @@ export declare const AI_INSIGHT_NO_READERS_COPY = "There are no figures to answe
26
26
  export declare const AI_INSIGHT_NO_TABLES_COPY = "The figures could not be read. Try again in a moment.";
27
27
  export declare const AI_INSIGHT_UNREADABLE_COPY = "The AI did not answer with insights. Try asking in different words.";
28
28
  /** The digest's question: what a week's insights cover, in the order a person reads them. */
29
- export declare const AI_INSIGHT_DIGEST_QUESTION = "What changed on this site this week? Write the week\u2019s insights: the change in traffic, the most viewed page, the form that turned the most views into submissions, a campaign that did better than the rest, and any day that stood out \u2014 only those the tables support.";
29
+ export declare const AI_INSIGHT_DIGEST_QUESTION = "What changed on this site last week, Monday to Sunday? Write the week\u2019s insights: the change in traffic, the most viewed page, the form that turned the most views into submissions, a campaign that did better than the rest, and any day that stood out \u2014 only those the tables support.";
30
30
  /** The rules both calls are sent: static, byte-identical for every workspace. */
31
31
  export declare const AI_INSIGHT_RULES: string;
32
32
  /** The system blocks both calls send: the rules and the platform's acceptable-use rules. */
@@ -14,7 +14,7 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
- import { AI_INSIGHT_ASK_SURFACES, AI_INSIGHT_DIGEST_DAYS, AI_INSIGHT_DIGEST_READERS, AI_INSIGHT_QUESTION_MAX_CHARS, AI_INSIGHTS_COLLECTION, aiInsightDays, aiInsightOutput, aiInsightSurface, aiInsightSurfaceNeedsSite } from "../model/ai-insight.js";
17
+ import { AI_INSIGHT_ASK_SURFACES, AI_INSIGHT_DIGEST_DAYS, AI_INSIGHT_DIGEST_READERS, AI_INSIGHT_QUESTION_MAX_CHARS, AI_INSIGHTS_COLLECTION, aiInsightDays, aiInsightDigestPeriod, aiInsightOutput, aiInsightSurface, aiInsightSurfaceNeedsSite } from "../model/ai-insight.js";
18
18
  import { aiHostPublishContext, aiSiteStatusTable } from "../model/ai-host-publish-context.js";
19
19
  import { aiInstructionsWithSiteContext } from "../model/ai-site-context.js";
20
20
  import { readAiSiteContext } from "../runtime/site-context.js";
@@ -35,7 +35,7 @@ export const AI_INSIGHT_NO_SITE_COPY = 'Open the site whose numbers you are aski
35
35
  export const AI_INSIGHT_NO_READERS_COPY = 'There are no figures to answer from here yet: the features this page reports on are not on for this workspace.';
36
36
  export const AI_INSIGHT_NO_TABLES_COPY = 'The figures could not be read. Try again in a moment.';
37
37
  export const AI_INSIGHT_UNREADABLE_COPY = 'The AI did not answer with insights. Try asking in different words.';
38
- /** The digest's question: what a week's insights cover, in the order a person reads them. */ export const AI_INSIGHT_DIGEST_QUESTION = 'What changed on this site this week? Write the week’s insights: the change in traffic, the most viewed page, the form that turned the most views into submissions, a campaign that did better than the rest, and any day that stood out — only those the tables support.';
38
+ /** The digest's question: what a week's insights cover, in the order a person reads them. */ export const AI_INSIGHT_DIGEST_QUESTION = 'What changed on this site last week, Monday to Sunday? Write the week’s insights: the change in traffic, the most viewed page, the form that turned the most views into submissions, a campaign that did better than the rest, and any day that stood out — only those the tables support.';
39
39
  /** The rules both calls are sent: static, byte-identical for every workspace. */ export const AI_INSIGHT_RULES = [
40
40
  'You answer a workspace owner’s questions about their own figures: page views, orders, bookings, forms, campaigns, A/B tests and datasets. You never see a record, and you never write a query: code reads figures through fixed readers and gives you their tables.',
41
41
  '',
@@ -215,11 +215,13 @@ export const runAiJobInsightStep = async (context)=>{
215
215
  signal
216
216
  } : {}));
217
217
  // 1. The reads.
218
+ // A digest reads last Monday to Sunday, whatever minute the beat runs it.
219
+ const readNow = digest ? aiInsightDigestPeriod(job.inputs, now).now : now;
218
220
  const base = {
219
221
  orgId: job.orgId,
220
222
  hostId,
221
223
  uid: digest ? null : job.createdBy,
222
- now
224
+ now: readNow
223
225
  };
224
226
  let requests;
225
227
  if (digest) {