@sonordev/site-kit 6.4.0 → 6.5.0

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 (232) hide show
  1. package/agent-manifest.json +1 -1
  2. package/dist/AnalyticsProvider-44PDLORZ.js +34 -0
  3. package/dist/{AnalyticsProvider-QXZN5O6D.js.map → AnalyticsProvider-44PDLORZ.js.map} +1 -1
  4. package/dist/AnalyticsProvider-F6KHEAOX.mjs +13 -0
  5. package/dist/{AnalyticsProvider-4K5T7OQ7.mjs.map → AnalyticsProvider-F6KHEAOX.mjs.map} +1 -1
  6. package/dist/{ArticleViewTracker-FFMHR7AY.js → ArticleViewTracker-MGNVIHZ4.js} +6 -6
  7. package/dist/{ArticleViewTracker-FFMHR7AY.js.map → ArticleViewTracker-MGNVIHZ4.js.map} +1 -1
  8. package/dist/{ArticleViewTracker-BUG2TOHS.mjs → ArticleViewTracker-NI4UNSY7.mjs} +5 -5
  9. package/dist/{ArticleViewTracker-BUG2TOHS.mjs.map → ArticleViewTracker-NI4UNSY7.mjs.map} +1 -1
  10. package/dist/{ChatWidget-CRQPBDEF.mjs → ChatWidget-XSALHDDW.mjs} +7 -7
  11. package/dist/{ChatWidget-CRQPBDEF.mjs.map → ChatWidget-XSALHDDW.mjs.map} +1 -1
  12. package/dist/{ChatWidget-XFYLVXAX.js → ChatWidget-YBXJRR2Z.js} +10 -10
  13. package/dist/{ChatWidget-XFYLVXAX.js.map → ChatWidget-YBXJRR2Z.js.map} +1 -1
  14. package/dist/EngageWidget-DUEXPVOY.mjs +13 -0
  15. package/dist/{EngageWidget-SQU4T7CI.mjs.map → EngageWidget-DUEXPVOY.mjs.map} +1 -1
  16. package/dist/EngageWidget-MDCRS3BN.js +22 -0
  17. package/dist/{EngageWidget-S234YAT2.js.map → EngageWidget-MDCRS3BN.js.map} +1 -1
  18. package/dist/{FileField-XG4EYAZW.js → FileField-AXU6KBSV.js} +6 -6
  19. package/dist/{FileField-XG4EYAZW.js.map → FileField-AXU6KBSV.js.map} +1 -1
  20. package/dist/{FileField-HNEBEBLB.mjs → FileField-UTLGR5ZN.mjs} +5 -5
  21. package/dist/{FileField-HNEBEBLB.mjs.map → FileField-UTLGR5ZN.mjs.map} +1 -1
  22. package/dist/{FormSpotlight-VKD774PZ.mjs → FormSpotlight-NWLCZMOT.mjs} +3 -3
  23. package/dist/{FormSpotlight-VKD774PZ.mjs.map → FormSpotlight-NWLCZMOT.mjs.map} +1 -1
  24. package/dist/{FormSpotlight-IODREOPN.js → FormSpotlight-RR5KJDIH.js} +9 -9
  25. package/dist/{FormSpotlight-IODREOPN.js.map → FormSpotlight-RR5KJDIH.js.map} +1 -1
  26. package/dist/{FormStage-37KKZQH4.js → FormStage-GFNX4GLU.js} +9 -9
  27. package/dist/{FormStage-37KKZQH4.js.map → FormStage-GFNX4GLU.js.map} +1 -1
  28. package/dist/{FormStage-PRZNRYNC.mjs → FormStage-V4XKLI6J.mjs} +3 -3
  29. package/dist/{FormStage-PRZNRYNC.mjs.map → FormStage-V4XKLI6J.mjs.map} +1 -1
  30. package/dist/ManagedForm-4AD3LWSC.js +25 -0
  31. package/dist/{ManagedForm-JCMEE626.js.map → ManagedForm-4AD3LWSC.js.map} +1 -1
  32. package/dist/ManagedForm-4IWPG6AZ.mjs +16 -0
  33. package/dist/{ManagedForm-4PZOCKCV.mjs.map → ManagedForm-4IWPG6AZ.mjs.map} +1 -1
  34. package/dist/{ManagedNewsletterForm-KMZG24OE.mjs → ManagedNewsletterForm-ARSY2VHC.mjs} +6 -6
  35. package/dist/{ManagedNewsletterForm-KMZG24OE.mjs.map → ManagedNewsletterForm-ARSY2VHC.mjs.map} +1 -1
  36. package/dist/{ManagedNewsletterForm-CTC33B2U.js → ManagedNewsletterForm-HJZNUABY.js} +7 -7
  37. package/dist/{ManagedNewsletterForm-CTC33B2U.js.map → ManagedNewsletterForm-HJZNUABY.js.map} +1 -1
  38. package/dist/{SignalCore-YIPKKIUG.mjs → SignalCore-N2LKG4XN.mjs} +5 -5
  39. package/dist/{SignalCore-YIPKKIUG.mjs.map → SignalCore-N2LKG4XN.mjs.map} +1 -1
  40. package/dist/{SignalCore-YHT7WPXV.js → SignalCore-P2JUY2BO.js} +11 -11
  41. package/dist/{SignalCore-YHT7WPXV.js.map → SignalCore-P2JUY2BO.js.map} +1 -1
  42. package/dist/SitemapSync-KFY4PJ7G.js +19 -0
  43. package/dist/{SitemapSync-GN4OXIM2.js.map → SitemapSync-KFY4PJ7G.js.map} +1 -1
  44. package/dist/SitemapSync-UYDL7T4M.mjs +10 -0
  45. package/dist/{SitemapSync-NG763CGN.mjs.map → SitemapSync-UYDL7T4M.mjs.map} +1 -1
  46. package/dist/analytics/index.js +14 -13
  47. package/dist/analytics/index.mjs +6 -5
  48. package/dist/articles/index.js +1 -1
  49. package/dist/articles/index.mjs +1 -1
  50. package/dist/articles/server-ui.js +1 -1
  51. package/dist/articles/server-ui.mjs +1 -1
  52. package/dist/articles/types.d.ts +6 -0
  53. package/dist/articles/types.d.ts.map +1 -1
  54. package/dist/{chunk-KZBDH5A3.js → chunk-34XL7TVH.js} +4 -4
  55. package/dist/{chunk-KZBDH5A3.js.map → chunk-34XL7TVH.js.map} +1 -1
  56. package/dist/{chunk-5T3J24K4.mjs → chunk-3BLKKF67.mjs} +4 -4
  57. package/dist/{chunk-5T3J24K4.mjs.map → chunk-3BLKKF67.mjs.map} +1 -1
  58. package/dist/{chunk-5W4S7XQD.js → chunk-3EAS4PAT.js} +3 -3
  59. package/dist/{chunk-5W4S7XQD.js.map → chunk-3EAS4PAT.js.map} +1 -1
  60. package/dist/{chunk-LPDXS2B2.mjs → chunk-42LLFAJQ.mjs} +3 -3
  61. package/dist/{chunk-LPDXS2B2.mjs.map → chunk-42LLFAJQ.mjs.map} +1 -1
  62. package/dist/{chunk-TIFVAASU.mjs → chunk-5YA6SI4Z.mjs} +2 -2
  63. package/dist/{chunk-S5QKNXLQ.js → chunk-5ZMTEK7U.js} +10 -10
  64. package/dist/{chunk-S5QKNXLQ.js.map → chunk-5ZMTEK7U.js.map} +1 -1
  65. package/dist/{chunk-YACU35MY.js → chunk-6OQYRVMI.js} +9 -9
  66. package/dist/{chunk-YACU35MY.js.map → chunk-6OQYRVMI.js.map} +1 -1
  67. package/dist/{chunk-4C3INRHE.mjs → chunk-7F66Y4MA.mjs} +5 -5
  68. package/dist/{chunk-4C3INRHE.mjs.map → chunk-7F66Y4MA.mjs.map} +1 -1
  69. package/dist/{chunk-GSU6VTCZ.mjs → chunk-A2HL22BO.mjs} +3 -3
  70. package/dist/{chunk-GSU6VTCZ.mjs.map → chunk-A2HL22BO.mjs.map} +1 -1
  71. package/dist/{chunk-XZHR4E2D.js → chunk-AL4Z6VYF.js} +8 -8
  72. package/dist/{chunk-XZHR4E2D.js.map → chunk-AL4Z6VYF.js.map} +1 -1
  73. package/dist/{chunk-ZEBOOUBP.mjs → chunk-B6QPCAEG.mjs} +3 -3
  74. package/dist/{chunk-ZEBOOUBP.mjs.map → chunk-B6QPCAEG.mjs.map} +1 -1
  75. package/dist/{chunk-HTCNRTN6.js → chunk-BHZHWXER.js} +8 -8
  76. package/dist/{chunk-HTCNRTN6.js.map → chunk-BHZHWXER.js.map} +1 -1
  77. package/dist/{chunk-TPDDSEL3.mjs → chunk-BNKBRLJK.mjs} +3 -3
  78. package/dist/{chunk-TPDDSEL3.mjs.map → chunk-BNKBRLJK.mjs.map} +1 -1
  79. package/dist/{chunk-A6UYV475.mjs → chunk-CABEVWKJ.mjs} +4 -24
  80. package/dist/chunk-CABEVWKJ.mjs.map +1 -0
  81. package/dist/{chunk-VMNPLCZB.js → chunk-CM4AVKXR.js} +14 -14
  82. package/dist/{chunk-VMNPLCZB.js.map → chunk-CM4AVKXR.js.map} +1 -1
  83. package/dist/{chunk-KOPFFKG2.mjs → chunk-D3WJMSTB.mjs} +3 -3
  84. package/dist/{chunk-KOPFFKG2.mjs.map → chunk-D3WJMSTB.mjs.map} +1 -1
  85. package/dist/{chunk-JXRGIE3V.mjs → chunk-DWZGW77V.mjs} +4 -4
  86. package/dist/{chunk-JXRGIE3V.mjs.map → chunk-DWZGW77V.mjs.map} +1 -1
  87. package/dist/{chunk-USQPRLSG.mjs → chunk-FPVYKSXE.mjs} +13 -10
  88. package/dist/chunk-FPVYKSXE.mjs.map +1 -0
  89. package/dist/{chunk-VDEQVXNV.js → chunk-G2KGHIAP.js} +3 -3
  90. package/dist/{chunk-VDEQVXNV.js.map → chunk-G2KGHIAP.js.map} +1 -1
  91. package/dist/chunk-GIGUDKLS.mjs +6 -0
  92. package/dist/{chunk-K3PVUXKB.mjs.map → chunk-GIGUDKLS.mjs.map} +1 -1
  93. package/dist/{chunk-XXSSUCJB.js → chunk-I7AZC5ZQ.js} +2 -2
  94. package/dist/{chunk-C7J4XLBN.js → chunk-IO65ROAO.js} +15 -15
  95. package/dist/{chunk-C7J4XLBN.js.map → chunk-IO65ROAO.js.map} +1 -1
  96. package/dist/chunk-JCDX37BY.js +58 -0
  97. package/dist/chunk-JCDX37BY.js.map +1 -0
  98. package/dist/{chunk-7K6L33BX.mjs → chunk-JOYMIGR2.mjs} +22 -22
  99. package/dist/{chunk-ATPQ6264.js → chunk-JS75ORH7.js} +24 -24
  100. package/dist/{chunk-BI7DH42Z.mjs → chunk-MDQLON5R.mjs} +5 -5
  101. package/dist/{chunk-BI7DH42Z.mjs.map → chunk-MDQLON5R.mjs.map} +1 -1
  102. package/dist/{chunk-GXSSFQ5D.js → chunk-MHKD34B4.js} +15 -15
  103. package/dist/{chunk-GXSSFQ5D.js.map → chunk-MHKD34B4.js.map} +1 -1
  104. package/dist/chunk-MXJOKOFK.mjs +51 -0
  105. package/dist/chunk-MXJOKOFK.mjs.map +1 -0
  106. package/dist/{chunk-5RNAUYWE.js → chunk-NH2P27MA.js} +4 -27
  107. package/dist/chunk-NH2P27MA.js.map +1 -0
  108. package/dist/{chunk-ZUQSVCMD.js → chunk-NN2YE5WT.js} +3 -3
  109. package/dist/{chunk-RH4XEXTK.mjs → chunk-NNZDWY3L.mjs} +3 -3
  110. package/dist/{chunk-SBTA6B6Y.mjs → chunk-OFBEBMXN.mjs} +4 -4
  111. package/dist/{chunk-SBTA6B6Y.mjs.map → chunk-OFBEBMXN.mjs.map} +1 -1
  112. package/dist/{chunk-HSLPN3QK.js → chunk-OY3PDIB3.js} +10 -10
  113. package/dist/{chunk-HSLPN3QK.js.map → chunk-OY3PDIB3.js.map} +1 -1
  114. package/dist/{chunk-ZOBUA65N.js → chunk-PFDRQJKJ.js} +14 -14
  115. package/dist/{chunk-ZOBUA65N.js.map → chunk-PFDRQJKJ.js.map} +1 -1
  116. package/dist/{chunk-YRKWJCML.mjs → chunk-PGYOBEPL.mjs} +3 -3
  117. package/dist/{chunk-YRKWJCML.mjs.map → chunk-PGYOBEPL.mjs.map} +1 -1
  118. package/dist/{chunk-U5NFIHLY.js → chunk-PIPGH7GA.js} +4 -4
  119. package/dist/{chunk-U5NFIHLY.js.map → chunk-PIPGH7GA.js.map} +1 -1
  120. package/dist/chunk-PURI56RE.js +8 -0
  121. package/dist/{chunk-CIB3RHNZ.js.map → chunk-PURI56RE.js.map} +1 -1
  122. package/dist/{chunk-EWDWLY74.js → chunk-QCS7NWMA.js} +15 -12
  123. package/dist/chunk-QCS7NWMA.js.map +1 -0
  124. package/dist/{chunk-W7IAG5YD.js → chunk-RMDH2HMW.js} +3 -3
  125. package/dist/{chunk-W7IAG5YD.js.map → chunk-RMDH2HMW.js.map} +1 -1
  126. package/dist/{chunk-U4CYO4NG.js → chunk-SUGBIEYQ.js} +9 -9
  127. package/dist/{chunk-U4CYO4NG.js.map → chunk-SUGBIEYQ.js.map} +1 -1
  128. package/dist/{chunk-M62L2N5A.mjs → chunk-SVCKJ5KH.mjs} +4 -4
  129. package/dist/{chunk-M62L2N5A.mjs.map → chunk-SVCKJ5KH.mjs.map} +1 -1
  130. package/dist/{chunk-6QPKC6XP.mjs → chunk-THO4NS6Z.mjs} +3 -3
  131. package/dist/{chunk-6QPKC6XP.mjs.map → chunk-THO4NS6Z.mjs.map} +1 -1
  132. package/dist/{chunk-WIWHM47B.mjs → chunk-TPEPAHU5.mjs} +3 -3
  133. package/dist/{chunk-WIWHM47B.mjs.map → chunk-TPEPAHU5.mjs.map} +1 -1
  134. package/dist/{chunk-CUZ3I6CO.js → chunk-U577ITSA.js} +5 -5
  135. package/dist/{chunk-CUZ3I6CO.js.map → chunk-U577ITSA.js.map} +1 -1
  136. package/dist/{chunk-5BARLEAI.mjs → chunk-UCQDE5UH.mjs} +3 -3
  137. package/dist/{chunk-5BARLEAI.mjs.map → chunk-UCQDE5UH.mjs.map} +1 -1
  138. package/dist/{chunk-QOKMH2TD.mjs → chunk-VA2O5OSV.mjs} +3 -3
  139. package/dist/{chunk-QOKMH2TD.mjs.map → chunk-VA2O5OSV.mjs.map} +1 -1
  140. package/dist/{chunk-M3JCHPK3.mjs → chunk-VJTKIKZY.mjs} +5 -5
  141. package/dist/{chunk-M3JCHPK3.mjs.map → chunk-VJTKIKZY.mjs.map} +1 -1
  142. package/dist/{chunk-F2GECCK7.js → chunk-VUAGR6VL.js} +12 -12
  143. package/dist/{chunk-F2GECCK7.js.map → chunk-VUAGR6VL.js.map} +1 -1
  144. package/dist/{chunk-24LFAQEG.mjs → chunk-XEOIGCUE.mjs} +5 -5
  145. package/dist/{chunk-24LFAQEG.mjs.map → chunk-XEOIGCUE.mjs.map} +1 -1
  146. package/dist/{chunk-GGGX54ZE.js → chunk-ZXC56JS3.js} +14 -14
  147. package/dist/{chunk-GGGX54ZE.js.map → chunk-ZXC56JS3.js.map} +1 -1
  148. package/dist/cli/index.js +211 -211
  149. package/dist/cli/index.mjs +151 -151
  150. package/dist/client/index.js +4 -4
  151. package/dist/client/index.mjs +3 -3
  152. package/dist/commerce/index.js +59 -58
  153. package/dist/commerce/index.mjs +7 -6
  154. package/dist/engage/index.js +11 -10
  155. package/dist/engage/index.js.map +1 -1
  156. package/dist/engage/index.mjs +8 -7
  157. package/dist/engage/index.mjs.map +1 -1
  158. package/dist/fleet/index.js +10 -9
  159. package/dist/fleet/index.mjs +6 -5
  160. package/dist/forms/index.js +13 -13
  161. package/dist/forms/index.mjs +7 -7
  162. package/dist/forms/server.js +2 -2
  163. package/dist/forms/server.mjs +1 -1
  164. package/dist/images/index.js +11 -11
  165. package/dist/images/index.mjs +4 -4
  166. package/dist/index.js +65 -64
  167. package/dist/index.js.map +1 -1
  168. package/dist/index.mjs +11 -10
  169. package/dist/index.mjs.map +1 -1
  170. package/dist/layout/SiteKitClientProviders.d.ts.map +1 -1
  171. package/dist/layout/client.d.ts.map +1 -1
  172. package/dist/layout/client.js +10 -9
  173. package/dist/layout/client.mjs +9 -8
  174. package/dist/layout/index.js +12 -11
  175. package/dist/layout/index.js.map +1 -1
  176. package/dist/layout/index.mjs +10 -9
  177. package/dist/layout/index.mjs.map +1 -1
  178. package/dist/layout/types.d.ts +6 -0
  179. package/dist/layout/types.d.ts.map +1 -1
  180. package/dist/{lib-6GWWPDJR.js → lib-6HOOMENJ.js} +7 -7
  181. package/dist/{lib-WFB5RLEQ.js → lib-BGAWCSUH.js} +8 -8
  182. package/dist/{lib-N5PD6JNJ.mjs → lib-R7IQ4ZHE.mjs} +2 -2
  183. package/dist/lib-RSDMEKDQ.mjs +4 -0
  184. package/dist/maps/index.js +5 -5
  185. package/dist/maps/index.mjs +3 -3
  186. package/dist/portfolio/contract.d.ts +93 -0
  187. package/dist/portfolio/contract.d.ts.map +1 -1
  188. package/dist/portfolio/contract.js +94 -1
  189. package/dist/portfolio/contract.js.map +1 -1
  190. package/dist/portfolio/contract.mjs +79 -2
  191. package/dist/portfolio/contract.mjs.map +1 -1
  192. package/dist/search/index.js +4 -4
  193. package/dist/search/index.mjs +3 -3
  194. package/dist/seo/client.js +7 -6
  195. package/dist/seo/client.mjs +6 -5
  196. package/dist/seo/index.js +7 -6
  197. package/dist/seo/index.js.map +1 -1
  198. package/dist/seo/index.mjs +6 -5
  199. package/dist/seo/index.mjs.map +1 -1
  200. package/dist/seo/register-sitemap-cli.js +2 -2
  201. package/dist/seo/register-sitemap-cli.mjs +1 -1
  202. package/dist/server/index.js +3 -3
  203. package/dist/server/index.mjs +2 -2
  204. package/dist/shared/frame.d.ts +15 -0
  205. package/dist/shared/frame.d.ts.map +1 -1
  206. package/dist/shared/version.d.ts +1 -1
  207. package/dist/signal/index.js +11 -11
  208. package/dist/signal/index.mjs +2 -2
  209. package/dist/sync/index.js +17 -17
  210. package/dist/sync/index.mjs +4 -4
  211. package/dist/{web-vitals.attribution-CHXXWO72.mjs → web-vitals.attribution-7LKLZ5GR.mjs} +3 -3
  212. package/dist/web-vitals.attribution-7LKLZ5GR.mjs.map +1 -0
  213. package/dist/{web-vitals.attribution-SQVZNHN3.js → web-vitals.attribution-ONVYQNRB.js} +3 -3
  214. package/dist/web-vitals.attribution-ONVYQNRB.js.map +1 -0
  215. package/package.json +19 -21
  216. package/dist/AnalyticsProvider-4K5T7OQ7.mjs +0 -12
  217. package/dist/AnalyticsProvider-QXZN5O6D.js +0 -33
  218. package/dist/EngageWidget-S234YAT2.js +0 -21
  219. package/dist/EngageWidget-SQU4T7CI.mjs +0 -12
  220. package/dist/ManagedForm-4PZOCKCV.mjs +0 -16
  221. package/dist/ManagedForm-JCMEE626.js +0 -25
  222. package/dist/SitemapSync-GN4OXIM2.js +0 -18
  223. package/dist/SitemapSync-NG763CGN.mjs +0 -9
  224. package/dist/chunk-5RNAUYWE.js.map +0 -1
  225. package/dist/chunk-A6UYV475.mjs.map +0 -1
  226. package/dist/chunk-CIB3RHNZ.js +0 -8
  227. package/dist/chunk-EWDWLY74.js.map +0 -1
  228. package/dist/chunk-K3PVUXKB.mjs +0 -6
  229. package/dist/chunk-USQPRLSG.mjs.map +0 -1
  230. package/dist/lib-BD4KKQBI.mjs +0 -4
  231. package/dist/web-vitals.attribution-CHXXWO72.mjs.map +0 -1
  232. package/dist/web-vitals.attribution-SQVZNHN3.js.map +0 -1
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ var chunkJCDX37BY_js = require('../chunk-JCDX37BY.js');
3
4
  require('../chunk-SNAQBZPT.js');
4
5
 
5
6
  // src/portfolio/contract.ts
@@ -30,14 +31,106 @@ var PORTFOLIO_KPI_JSON_SCHEMA = {
30
31
  properties: {
31
32
  label: { type: "string" },
32
33
  value: { type: "number" },
33
- description: { type: "string" }
34
+ description: { type: "string" },
35
+ source: { enum: ["measured", "reported", "estimated"] },
36
+ reportedBy: {
37
+ type: "object",
38
+ required: ["name", "organization", "date"],
39
+ properties: {
40
+ name: { type: "string", minLength: 1 },
41
+ role: { type: "string" },
42
+ organization: { type: "string", minLength: 1 },
43
+ date: { type: "string", pattern: "^\\d{4}-\\d{2}-\\d{2}$" }
44
+ }
45
+ }
34
46
  },
47
+ // A reported number is only valid with someone named behind it.
48
+ if: { properties: { source: { const: "reported" } }, required: ["source"] },
49
+ then: { required: ["reportedBy"] },
35
50
  additionalProperties: true
36
51
  };
52
+ var METRIC_SOURCES = ["measured", "reported", "estimated"];
53
+ var ISO_DAY_RE = /^\d{4}-\d{2}-\d{2}$/;
54
+ var MONTHS = [
55
+ "January",
56
+ "February",
57
+ "March",
58
+ "April",
59
+ "May",
60
+ "June",
61
+ "July",
62
+ "August",
63
+ "September",
64
+ "October",
65
+ "November",
66
+ "December"
67
+ ];
68
+ function isMetricSource(value) {
69
+ return typeof value === "string" && METRIC_SOURCES.includes(value);
70
+ }
71
+ function metricSourceProblem(metric) {
72
+ if (metric.source === void 0 || metric.source === null) return null;
73
+ if (!isMetricSource(metric.source)) return `source must be one of ${METRIC_SOURCES.join(", ")}`;
74
+ if (metric.source !== "reported") return null;
75
+ const by = metric.reportedBy;
76
+ if (!by || typeof by !== "object") {
77
+ return "a reported number needs reportedBy: who reported it, where they work, and when";
78
+ }
79
+ if (typeof by.name !== "string" || !by.name.trim()) return "reportedBy.name is required";
80
+ if (typeof by.organization !== "string" || !by.organization.trim()) {
81
+ return "reportedBy.organization is required";
82
+ }
83
+ if (typeof by.date !== "string" || !ISO_DAY_RE.test(by.date) || Number.isNaN(Date.parse(by.date))) {
84
+ return "reportedBy.date must be a real day, YYYY-MM-DD";
85
+ }
86
+ if (by.role !== void 0 && typeof by.role !== "string") return "reportedBy.role must be text";
87
+ return null;
88
+ }
89
+ function isHeadlineMetric(metric) {
90
+ if (metric.source === "measured") return true;
91
+ return metric.source === "reported" && metricSourceProblem(metric) === null;
92
+ }
93
+ function metricSourceLabel(source) {
94
+ if (source === "measured") return "Measured";
95
+ if (source === "reported") return "Client-reported";
96
+ if (source === "estimated") return "Estimated";
97
+ return null;
98
+ }
99
+ function formatAttribution(by) {
100
+ const [year, month, day] = by.date.split("-").map(Number);
101
+ const when = MONTHS[month - 1] ? `${MONTHS[month - 1]} ${day}, ${year}` : by.date;
102
+ return [`Reported by ${by.name.trim()}`, by.role?.trim(), by.organization.trim(), when].filter(Boolean).join(", ");
103
+ }
104
+ function shortAttribution(by) {
105
+ return `per ${by.organization.trim()}`;
106
+ }
107
+ function carryReportedMetrics(previous, next, keyOf) {
108
+ const norm = (metric) => String(keyOf(metric) ?? "").trim().toLowerCase();
109
+ const reported = (previous ?? []).filter((m) => m?.source === "reported");
110
+ const taken = new Set(reported.map(norm));
111
+ const generated = (next ?? []).filter((m) => m && m.source !== "reported" && !taken.has(norm(m)));
112
+ return [...reported, ...generated];
113
+ }
37
114
 
115
+ Object.defineProperty(exports, "FRAME_READY_MESSAGE", {
116
+ enumerable: true,
117
+ get: function () { return chunkJCDX37BY_js.FRAME_READY_MESSAGE; }
118
+ });
119
+ Object.defineProperty(exports, "isFrameReadyMessage", {
120
+ enumerable: true,
121
+ get: function () { return chunkJCDX37BY_js.isFrameReadyMessage; }
122
+ });
123
+ exports.METRIC_SOURCES = METRIC_SOURCES;
38
124
  exports.PORTFOLIO_CONTRACT_VERSION = PORTFOLIO_CONTRACT_VERSION;
39
125
  exports.PORTFOLIO_KPI_JSON_SCHEMA = PORTFOLIO_KPI_JSON_SCHEMA;
126
+ exports.carryReportedMetrics = carryReportedMetrics;
40
127
  exports.curateHeroKpis = curateHeroKpis;
128
+ exports.formatAttribution = formatAttribution;
129
+ exports.isHeadlineMetric = isHeadlineMetric;
41
130
  exports.isLighthouseKpi = isLighthouseKpi;
131
+ exports.isMetricSource = isMetricSource;
132
+ exports.metricSourceLabel = metricSourceLabel;
133
+ exports.metricSourceProblem = metricSourceProblem;
134
+ exports.shortAttribution = shortAttribution;
42
135
  //# sourceMappingURL=contract.js.map
43
136
  //# sourceMappingURL=contract.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/portfolio/contract.ts"],"names":[],"mappings":";;;;;AAoBO,IAAM,0BAAA,GAA6B;AAS1C,IAAM,aAAA,GAAgB,aAAA;AAGf,SAAS,gBAAgB,GAAA,EAAsC;AACpE,EAAA,OAAO,aAAA,CAAc,IAAA,CAAK,GAAA,CAAI,KAAA,IAAS,EAAE,CAAA;AAC3C;AAQO,SAAS,eAAkC,IAAA,EAAgB;AAChE,EAAA,MAAM,QAAA,GAAW,KAAK,MAAA,CAAO,CAAC,MAAM,CAAC,eAAA,CAAgB,CAAC,CAAC,CAAA;AACvD,EAAA,MAAM,UAAA,GAAa,IAAA,CAAK,MAAA,CAAO,eAAe,CAAA;AAC9C,EAAA,IAAI,UAAA,CAAW,MAAA,IAAU,CAAA,EAAG,OAAO,IAAA;AAEnC,EAAA,MAAM,UAAA,GAAa,UAAA,CAAW,MAAA,IAAU,CAAA,IAAK,UAAA,CAAW,MAAM,CAAC,CAAA,KAAM,CAAA,CAAE,KAAA,KAAU,GAAG,CAAA;AACpF,EAAA,MAAM,UAAa,UAAA,GACf;AAAA,IACE,GAAG,WAAW,CAAC,CAAA;AAAA,IACf,KAAA,EAAO,8BAAA;AAAA,IACP,WAAA,EAAa;AAAA,GACf,GACC,UAAA,CAAW,IAAA,CAAK,CAAC,CAAA,KAAM,cAAA,CAAe,IAAA,CAAK,CAAA,CAAE,KAAK,CAAC,CAAA,IAAK,UAAA,CAAW,CAAC,CAAA;AAEzE,EAAA,OAAO,CAAC,GAAG,QAAA,EAAU,OAAO,CAAA;AAC9B;AAMO,IAAM,yBAAA,GAA4B;AAAA,EACvC,OAAA,EAAS,yCAAA;AAAA,EACT,GAAA,EAAK,gDAAA;AAAA,EACL,KAAA,EAAO,SAAA;AAAA,EACP,WAAA,EAAa,0EAAA;AAAA,EACb,IAAA,EAAM,QAAA;AAAA,EACN,QAAA,EAAU,CAAC,OAAA,EAAS,OAAO,CAAA;AAAA,EAC3B,UAAA,EAAY;AAAA,IACV,KAAA,EAAO,EAAE,IAAA,EAAM,QAAA,EAAS;AAAA,IACxB,KAAA,EAAO,EAAE,IAAA,EAAM,QAAA,EAAS;AAAA,IACxB,WAAA,EAAa,EAAE,IAAA,EAAM,QAAA;AAAS,GAChC;AAAA,EACA,oBAAA,EAAsB;AACxB","file":"contract.js","sourcesContent":["/**\n * @sonordev/site-kit/portfolio/contract — portfolio proof display policy (v1)\n *\n * Single source of truth for the Lighthouse-KPI collapse policy shared by the\n * content side (signal-api generation) and the render side (agency-site-kit\n * display). Same pattern as the other site-kit contracts:\n * - agency-site-kit imports these directly (it already depends on site-kit)\n * from `src/portfolio/curate-proof.ts`, which keeps the render-only\n * primitives that bind to its section types.\n * - signal-api keeps a byte-identical inline mirror\n * (`src/modules/skills/portfolio/portfolio-kpi-policy.util.ts`) guarded by\n * the shared drift-test vectors in `contract.test.ts`.\n *\n * Why it exists: generated case studies arrived with four \"100/100 Lighthouse X\"\n * KPIs repeated across hero, results cards, and a Performance section. One\n * differentiator repeated a dozen times stops differentiating — collapse the\n * per-category Lighthouse tiles to at most one summary, business outcomes first.\n */\n\n/** Bump on breaking changes to the collapse policy. */\nexport const PORTFOLIO_CONTRACT_VERSION = 1 as const\n\n/** Minimal KPI shape the policy reads — callers pass their own richer tiles. */\nexport interface KpiLike {\n label: string\n value: number\n description?: string\n}\n\nconst LIGHTHOUSE_RE = /lighthouse/i\n\n/** True when a KPI tile is a per-category Lighthouse score. */\nexport function isLighthouseKpi(kpi: Pick<KpiLike, 'label'>): boolean {\n return LIGHTHOUSE_RE.test(kpi.label || '')\n}\n\n/**\n * Collapse per-category Lighthouse KPIs to at most one summary tile, business\n * outcomes first. When every category is present at 100 the summary says so;\n * otherwise the Performance category (the one buyers know) represents the set.\n * Fewer than two Lighthouse tiles → returned unchanged.\n */\nexport function curateHeroKpis<T extends KpiLike>(kpis: T[]): T[] {\n const business = kpis.filter((k) => !isLighthouseKpi(k))\n const lighthouse = kpis.filter(isLighthouseKpi)\n if (lighthouse.length <= 1) return kpis\n\n const allPerfect = lighthouse.length >= 4 && lighthouse.every((k) => k.value === 100)\n const summary: T = allPerfect\n ? {\n ...lighthouse[0],\n label: 'Lighthouse, all 4 categories',\n description: 'Perfect scores across performance, SEO, accessibility, and best practices.',\n }\n : (lighthouse.find((k) => /performance/i.test(k.label)) ?? lighthouse[0])\n\n return [...business, summary]\n}\n\n/**\n * Agent-native contract: JSON Schema for the minimal KPI shape, so services and\n * agents can validate tiles before applying the collapse policy.\n */\nexport const PORTFOLIO_KPI_JSON_SCHEMA = {\n $schema: 'http://json-schema.org/draft-07/schema#',\n $id: 'https://sonor.io/schemas/portfolio-kpi.v1.json',\n title: 'KpiLike',\n description: 'The subset of a portfolio KPI tile the Lighthouse-collapse policy reads.',\n type: 'object',\n required: ['label', 'value'],\n properties: {\n label: { type: 'string' },\n value: { type: 'number' },\n description: { type: 'string' },\n },\n additionalProperties: true,\n} as const\n"]}
1
+ {"version":3,"sources":["../../src/portfolio/contract.ts"],"names":[],"mappings":"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","file":"contract.js","sourcesContent":["/**\n * @sonordev/site-kit/portfolio/contract — portfolio proof display policy (v1)\n *\n * Single source of truth for the Lighthouse-KPI collapse policy shared by the\n * content side (signal-api generation) and the render side (agency-site-kit\n * display). Same pattern as the other site-kit contracts:\n * - agency-site-kit imports these directly (it already depends on site-kit)\n * from `src/portfolio/curate-proof.ts`, which keeps the render-only\n * primitives that bind to its section types.\n * - signal-api keeps a byte-identical inline mirror\n * (`src/modules/skills/portfolio/portfolio-kpi-policy.util.ts`) guarded by\n * the shared drift-test vectors in `contract.test.ts`.\n *\n * Why it exists: generated case studies arrived with four \"100/100 Lighthouse X\"\n * KPIs repeated across hero, results cards, and a Performance section. One\n * differentiator repeated a dozen times stops differentiating — collapse the\n * per-category Lighthouse tiles to at most one summary, business outcomes first.\n *\n * It is also the single source of truth for METRIC PROVENANCE (below): the\n * three sources a portfolio number can have, what a client-reported number\n * must carry, and which numbers may headline. sonor-api and signal-api keep\n * byte-identical mirrors of those functions too, guarded by the same vectors.\n */\n\n// The live-frame handshake a case study's device frames listen for: defined\n// with the frame rules in shared/frame.ts, re-exported here because the\n// portfolio renderers are the ones listening.\nexport { FRAME_READY_MESSAGE, isFrameReadyMessage, type FrameReadyMessage } from '../shared/frame'\n\n/** Bump on breaking changes to the collapse policy. */\nexport const PORTFOLIO_CONTRACT_VERSION = 1 as const\n\n/** Minimal KPI shape the policy reads — callers pass their own richer tiles. */\nexport interface KpiLike {\n label: string\n value: number\n description?: string\n}\n\nconst LIGHTHOUSE_RE = /lighthouse/i\n\n/** True when a KPI tile is a per-category Lighthouse score. */\nexport function isLighthouseKpi(kpi: Pick<KpiLike, 'label'>): boolean {\n return LIGHTHOUSE_RE.test(kpi.label || '')\n}\n\n/**\n * Collapse per-category Lighthouse KPIs to at most one summary tile, business\n * outcomes first. When every category is present at 100 the summary says so;\n * otherwise the Performance category (the one buyers know) represents the set.\n * Fewer than two Lighthouse tiles → returned unchanged.\n */\nexport function curateHeroKpis<T extends KpiLike>(kpis: T[]): T[] {\n const business = kpis.filter((k) => !isLighthouseKpi(k))\n const lighthouse = kpis.filter(isLighthouseKpi)\n if (lighthouse.length <= 1) return kpis\n\n const allPerfect = lighthouse.length >= 4 && lighthouse.every((k) => k.value === 100)\n const summary: T = allPerfect\n ? {\n ...lighthouse[0],\n label: 'Lighthouse, all 4 categories',\n description: 'Perfect scores across performance, SEO, accessibility, and best practices.',\n }\n : (lighthouse.find((k) => /performance/i.test(k.label)) ?? lighthouse[0])\n\n return [...business, summary]\n}\n\n/**\n * Agent-native contract: JSON Schema for the minimal KPI shape, so services and\n * agents can validate tiles before applying the collapse policy.\n */\nexport const PORTFOLIO_KPI_JSON_SCHEMA = {\n $schema: 'http://json-schema.org/draft-07/schema#',\n $id: 'https://sonor.io/schemas/portfolio-kpi.v1.json',\n title: 'KpiLike',\n description: 'The subset of a portfolio KPI tile the Lighthouse-collapse policy reads.',\n type: 'object',\n required: ['label', 'value'],\n properties: {\n label: { type: 'string' },\n value: { type: 'number' },\n description: { type: 'string' },\n source: { enum: ['measured', 'reported', 'estimated'] },\n reportedBy: {\n type: 'object',\n required: ['name', 'organization', 'date'],\n properties: {\n name: { type: 'string', minLength: 1 },\n role: { type: 'string' },\n organization: { type: 'string', minLength: 1 },\n date: { type: 'string', pattern: '^\\\\d{4}-\\\\d{2}-\\\\d{2}$' },\n },\n },\n },\n // A reported number is only valid with someone named behind it.\n if: { properties: { source: { const: 'reported' } }, required: ['source'] },\n then: { required: ['reportedBy'] },\n additionalProperties: true,\n} as const\n\n// ─── Metric provenance ──────────────────────────────────────────────────────\n//\n// Where a published portfolio number came from. The three sources are not\n// grades of confidence, they're three different claims, and no surface may\n// let one pass as another:\n//\n// - measured: we measured it (Lighthouse, analytics, a count anyone can\n// re-run against the live site).\n// - reported: the client told us, and a named person at the client stands\n// behind it. Revenue, headcount and signed work can only ever\n// be this, because we can't see a client's books. It always\n// carries `reportedBy`, and every surface that shows it says\n// whose number it is.\n// - estimated: a reasonable projection. It never headlines anything.\n//\n// Proof is the strongest thing a case study has, and the client's own numbers\n// are usually the strongest proof. Before 'reported' existed they had nowhere\n// honest to go: tagging True Power Systems' \"$1M in signed work from web\n// leads\" as measured would have been false, and as estimated both false and\n// buried (2026-09-25).\n\n/** Every source a portfolio number can have. */\nexport const METRIC_SOURCES = ['measured', 'reported', 'estimated'] as const\nexport type MetricSource = (typeof METRIC_SOURCES)[number]\n\n/** Who stands behind a client-reported number. */\nexport interface MetricAttribution {\n /** The person, e.g. \"Scott Mann\". */\n name: string\n /** Their title, e.g. \"Director of Business Development\". */\n role?: string\n /** Where they work: almost always the client, e.g. \"True Power Systems\". */\n organization: string\n /** The day they reported it, YYYY-MM-DD. */\n date: string\n}\n\n/** The provenance fields a portfolio number (a KPI tile, a results card) carries. */\nexport interface SourcedMetric {\n source?: string | null\n reportedBy?: unknown\n}\n\nconst ISO_DAY_RE = /^\\d{4}-\\d{2}-\\d{2}$/\nconst MONTHS = [\n 'January', 'February', 'March', 'April', 'May', 'June',\n 'July', 'August', 'September', 'October', 'November', 'December',\n]\n\nexport function isMetricSource(value: unknown): value is MetricSource {\n return typeof value === 'string' && (METRIC_SOURCES as readonly string[]).includes(value)\n}\n\n/**\n * What's wrong with a number's provenance, or null when nothing is. A missing\n * source is legacy data and passes (displays show it unlabelled, and it can't\n * headline); an unknown source, or a reported number nobody is named behind,\n * does not.\n */\nexport function metricSourceProblem(metric: SourcedMetric): string | null {\n if (metric.source === undefined || metric.source === null) return null\n if (!isMetricSource(metric.source)) return `source must be one of ${METRIC_SOURCES.join(', ')}`\n if (metric.source !== 'reported') return null\n const by = metric.reportedBy as Partial<MetricAttribution> | null | undefined\n if (!by || typeof by !== 'object') {\n return 'a reported number needs reportedBy: who reported it, where they work, and when'\n }\n if (typeof by.name !== 'string' || !by.name.trim()) return 'reportedBy.name is required'\n if (typeof by.organization !== 'string' || !by.organization.trim()) {\n return 'reportedBy.organization is required'\n }\n if (typeof by.date !== 'string' || !ISO_DAY_RE.test(by.date) || Number.isNaN(Date.parse(by.date))) {\n return 'reportedBy.date must be a real day, YYYY-MM-DD'\n }\n if (by.role !== undefined && typeof by.role !== 'string') return 'reportedBy.role must be text'\n return null\n}\n\n/**\n * May this number lead: a hero tile, a gallery card, a carousel, a social\n * post? Measured, or reported with someone named behind it. Never an\n * estimate, and never an unlabelled legacy number.\n */\nexport function isHeadlineMetric(metric: SourcedMetric): boolean {\n if (metric.source === 'measured') return true\n return metric.source === 'reported' && metricSourceProblem(metric) === null\n}\n\n/** The badge a display shows, or null for a missing or unknown source (show nothing rather than guess). */\nexport function metricSourceLabel(source: unknown): string | null {\n if (source === 'measured') return 'Measured'\n if (source === 'reported') return 'Client-reported'\n if (source === 'estimated') return 'Estimated'\n return null\n}\n\n/**\n * The full credit: \"Reported by Scott Mann, Director of Business Development,\n * True Power Systems, September 25, 2026\". The date is read as a plain day,\n * never through a time zone, so it can't slip to the day before.\n */\nexport function formatAttribution(by: MetricAttribution): string {\n const [year, month, day] = by.date.split('-').map(Number)\n const when = MONTHS[month - 1] ? `${MONTHS[month - 1]} ${day}, ${year}` : by.date\n return [`Reported by ${by.name.trim()}`, by.role?.trim(), by.organization.trim(), when]\n .filter(Boolean)\n .join(', ')\n}\n\n/** The credit for a tight space (a card, a carousel tile): \"per True Power Systems\". */\nexport function shortAttribution(by: Pick<MetricAttribution, 'organization'>): string {\n return `per ${by.organization.trim()}`\n}\n\n/**\n * Carry a person's reported numbers through a regeneration. The generator can\n * only write measured or estimated numbers (it can't see a client's books, and\n * any 'reported' tag it writes is dropped here), so without this a rewrite of\n * the hero or the results would silently erase what a client told us.\n * Reported entries from `previous` lead, in their order; `next` fills in\n * behind them, minus anything sharing a key with a reported entry.\n */\nexport function carryReportedMetrics<T extends SourcedMetric>(\n previous: readonly T[] | null | undefined,\n next: readonly T[] | null | undefined,\n keyOf: (metric: T) => string,\n): T[] {\n const norm = (metric: T) => String(keyOf(metric) ?? '').trim().toLowerCase()\n const reported = (previous ?? []).filter((m) => m?.source === 'reported')\n const taken = new Set(reported.map(norm))\n const generated = (next ?? []).filter((m) => m && m.source !== 'reported' && !taken.has(norm(m)))\n return [...reported, ...generated]\n}\n"]}
@@ -1,3 +1,4 @@
1
+ export { FRAME_READY_MESSAGE, isFrameReadyMessage } from '../chunk-MXJOKOFK.mjs';
1
2
  import '../chunk-BEXTDHO4.mjs';
2
3
 
3
4
  // src/portfolio/contract.ts
@@ -28,11 +29,87 @@ var PORTFOLIO_KPI_JSON_SCHEMA = {
28
29
  properties: {
29
30
  label: { type: "string" },
30
31
  value: { type: "number" },
31
- description: { type: "string" }
32
+ description: { type: "string" },
33
+ source: { enum: ["measured", "reported", "estimated"] },
34
+ reportedBy: {
35
+ type: "object",
36
+ required: ["name", "organization", "date"],
37
+ properties: {
38
+ name: { type: "string", minLength: 1 },
39
+ role: { type: "string" },
40
+ organization: { type: "string", minLength: 1 },
41
+ date: { type: "string", pattern: "^\\d{4}-\\d{2}-\\d{2}$" }
42
+ }
43
+ }
32
44
  },
45
+ // A reported number is only valid with someone named behind it.
46
+ if: { properties: { source: { const: "reported" } }, required: ["source"] },
47
+ then: { required: ["reportedBy"] },
33
48
  additionalProperties: true
34
49
  };
50
+ var METRIC_SOURCES = ["measured", "reported", "estimated"];
51
+ var ISO_DAY_RE = /^\d{4}-\d{2}-\d{2}$/;
52
+ var MONTHS = [
53
+ "January",
54
+ "February",
55
+ "March",
56
+ "April",
57
+ "May",
58
+ "June",
59
+ "July",
60
+ "August",
61
+ "September",
62
+ "October",
63
+ "November",
64
+ "December"
65
+ ];
66
+ function isMetricSource(value) {
67
+ return typeof value === "string" && METRIC_SOURCES.includes(value);
68
+ }
69
+ function metricSourceProblem(metric) {
70
+ if (metric.source === void 0 || metric.source === null) return null;
71
+ if (!isMetricSource(metric.source)) return `source must be one of ${METRIC_SOURCES.join(", ")}`;
72
+ if (metric.source !== "reported") return null;
73
+ const by = metric.reportedBy;
74
+ if (!by || typeof by !== "object") {
75
+ return "a reported number needs reportedBy: who reported it, where they work, and when";
76
+ }
77
+ if (typeof by.name !== "string" || !by.name.trim()) return "reportedBy.name is required";
78
+ if (typeof by.organization !== "string" || !by.organization.trim()) {
79
+ return "reportedBy.organization is required";
80
+ }
81
+ if (typeof by.date !== "string" || !ISO_DAY_RE.test(by.date) || Number.isNaN(Date.parse(by.date))) {
82
+ return "reportedBy.date must be a real day, YYYY-MM-DD";
83
+ }
84
+ if (by.role !== void 0 && typeof by.role !== "string") return "reportedBy.role must be text";
85
+ return null;
86
+ }
87
+ function isHeadlineMetric(metric) {
88
+ if (metric.source === "measured") return true;
89
+ return metric.source === "reported" && metricSourceProblem(metric) === null;
90
+ }
91
+ function metricSourceLabel(source) {
92
+ if (source === "measured") return "Measured";
93
+ if (source === "reported") return "Client-reported";
94
+ if (source === "estimated") return "Estimated";
95
+ return null;
96
+ }
97
+ function formatAttribution(by) {
98
+ const [year, month, day] = by.date.split("-").map(Number);
99
+ const when = MONTHS[month - 1] ? `${MONTHS[month - 1]} ${day}, ${year}` : by.date;
100
+ return [`Reported by ${by.name.trim()}`, by.role?.trim(), by.organization.trim(), when].filter(Boolean).join(", ");
101
+ }
102
+ function shortAttribution(by) {
103
+ return `per ${by.organization.trim()}`;
104
+ }
105
+ function carryReportedMetrics(previous, next, keyOf) {
106
+ const norm = (metric) => String(keyOf(metric) ?? "").trim().toLowerCase();
107
+ const reported = (previous ?? []).filter((m) => m?.source === "reported");
108
+ const taken = new Set(reported.map(norm));
109
+ const generated = (next ?? []).filter((m) => m && m.source !== "reported" && !taken.has(norm(m)));
110
+ return [...reported, ...generated];
111
+ }
35
112
 
36
- export { PORTFOLIO_CONTRACT_VERSION, PORTFOLIO_KPI_JSON_SCHEMA, curateHeroKpis, isLighthouseKpi };
113
+ export { METRIC_SOURCES, PORTFOLIO_CONTRACT_VERSION, PORTFOLIO_KPI_JSON_SCHEMA, carryReportedMetrics, curateHeroKpis, formatAttribution, isHeadlineMetric, isLighthouseKpi, isMetricSource, metricSourceLabel, metricSourceProblem, shortAttribution };
37
114
  //# sourceMappingURL=contract.mjs.map
38
115
  //# sourceMappingURL=contract.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/portfolio/contract.ts"],"names":[],"mappings":";;;AAoBO,IAAM,0BAAA,GAA6B;AAS1C,IAAM,aAAA,GAAgB,aAAA;AAGf,SAAS,gBAAgB,GAAA,EAAsC;AACpE,EAAA,OAAO,aAAA,CAAc,IAAA,CAAK,GAAA,CAAI,KAAA,IAAS,EAAE,CAAA;AAC3C;AAQO,SAAS,eAAkC,IAAA,EAAgB;AAChE,EAAA,MAAM,QAAA,GAAW,KAAK,MAAA,CAAO,CAAC,MAAM,CAAC,eAAA,CAAgB,CAAC,CAAC,CAAA;AACvD,EAAA,MAAM,UAAA,GAAa,IAAA,CAAK,MAAA,CAAO,eAAe,CAAA;AAC9C,EAAA,IAAI,UAAA,CAAW,MAAA,IAAU,CAAA,EAAG,OAAO,IAAA;AAEnC,EAAA,MAAM,UAAA,GAAa,UAAA,CAAW,MAAA,IAAU,CAAA,IAAK,UAAA,CAAW,MAAM,CAAC,CAAA,KAAM,CAAA,CAAE,KAAA,KAAU,GAAG,CAAA;AACpF,EAAA,MAAM,UAAa,UAAA,GACf;AAAA,IACE,GAAG,WAAW,CAAC,CAAA;AAAA,IACf,KAAA,EAAO,8BAAA;AAAA,IACP,WAAA,EAAa;AAAA,GACf,GACC,UAAA,CAAW,IAAA,CAAK,CAAC,CAAA,KAAM,cAAA,CAAe,IAAA,CAAK,CAAA,CAAE,KAAK,CAAC,CAAA,IAAK,UAAA,CAAW,CAAC,CAAA;AAEzE,EAAA,OAAO,CAAC,GAAG,QAAA,EAAU,OAAO,CAAA;AAC9B;AAMO,IAAM,yBAAA,GAA4B;AAAA,EACvC,OAAA,EAAS,yCAAA;AAAA,EACT,GAAA,EAAK,gDAAA;AAAA,EACL,KAAA,EAAO,SAAA;AAAA,EACP,WAAA,EAAa,0EAAA;AAAA,EACb,IAAA,EAAM,QAAA;AAAA,EACN,QAAA,EAAU,CAAC,OAAA,EAAS,OAAO,CAAA;AAAA,EAC3B,UAAA,EAAY;AAAA,IACV,KAAA,EAAO,EAAE,IAAA,EAAM,QAAA,EAAS;AAAA,IACxB,KAAA,EAAO,EAAE,IAAA,EAAM,QAAA,EAAS;AAAA,IACxB,WAAA,EAAa,EAAE,IAAA,EAAM,QAAA;AAAS,GAChC;AAAA,EACA,oBAAA,EAAsB;AACxB","file":"contract.mjs","sourcesContent":["/**\n * @sonordev/site-kit/portfolio/contract — portfolio proof display policy (v1)\n *\n * Single source of truth for the Lighthouse-KPI collapse policy shared by the\n * content side (signal-api generation) and the render side (agency-site-kit\n * display). Same pattern as the other site-kit contracts:\n * - agency-site-kit imports these directly (it already depends on site-kit)\n * from `src/portfolio/curate-proof.ts`, which keeps the render-only\n * primitives that bind to its section types.\n * - signal-api keeps a byte-identical inline mirror\n * (`src/modules/skills/portfolio/portfolio-kpi-policy.util.ts`) guarded by\n * the shared drift-test vectors in `contract.test.ts`.\n *\n * Why it exists: generated case studies arrived with four \"100/100 Lighthouse X\"\n * KPIs repeated across hero, results cards, and a Performance section. One\n * differentiator repeated a dozen times stops differentiating — collapse the\n * per-category Lighthouse tiles to at most one summary, business outcomes first.\n */\n\n/** Bump on breaking changes to the collapse policy. */\nexport const PORTFOLIO_CONTRACT_VERSION = 1 as const\n\n/** Minimal KPI shape the policy reads — callers pass their own richer tiles. */\nexport interface KpiLike {\n label: string\n value: number\n description?: string\n}\n\nconst LIGHTHOUSE_RE = /lighthouse/i\n\n/** True when a KPI tile is a per-category Lighthouse score. */\nexport function isLighthouseKpi(kpi: Pick<KpiLike, 'label'>): boolean {\n return LIGHTHOUSE_RE.test(kpi.label || '')\n}\n\n/**\n * Collapse per-category Lighthouse KPIs to at most one summary tile, business\n * outcomes first. When every category is present at 100 the summary says so;\n * otherwise the Performance category (the one buyers know) represents the set.\n * Fewer than two Lighthouse tiles → returned unchanged.\n */\nexport function curateHeroKpis<T extends KpiLike>(kpis: T[]): T[] {\n const business = kpis.filter((k) => !isLighthouseKpi(k))\n const lighthouse = kpis.filter(isLighthouseKpi)\n if (lighthouse.length <= 1) return kpis\n\n const allPerfect = lighthouse.length >= 4 && lighthouse.every((k) => k.value === 100)\n const summary: T = allPerfect\n ? {\n ...lighthouse[0],\n label: 'Lighthouse, all 4 categories',\n description: 'Perfect scores across performance, SEO, accessibility, and best practices.',\n }\n : (lighthouse.find((k) => /performance/i.test(k.label)) ?? lighthouse[0])\n\n return [...business, summary]\n}\n\n/**\n * Agent-native contract: JSON Schema for the minimal KPI shape, so services and\n * agents can validate tiles before applying the collapse policy.\n */\nexport const PORTFOLIO_KPI_JSON_SCHEMA = {\n $schema: 'http://json-schema.org/draft-07/schema#',\n $id: 'https://sonor.io/schemas/portfolio-kpi.v1.json',\n title: 'KpiLike',\n description: 'The subset of a portfolio KPI tile the Lighthouse-collapse policy reads.',\n type: 'object',\n required: ['label', 'value'],\n properties: {\n label: { type: 'string' },\n value: { type: 'number' },\n description: { type: 'string' },\n },\n additionalProperties: true,\n} as const\n"]}
1
+ {"version":3,"sources":["../../src/portfolio/contract.ts"],"names":[],"mappings":";;;;AA8BO,IAAM,0BAAA,GAA6B;AAS1C,IAAM,aAAA,GAAgB,aAAA;AAGf,SAAS,gBAAgB,GAAA,EAAsC;AACpE,EAAA,OAAO,aAAA,CAAc,IAAA,CAAK,GAAA,CAAI,KAAA,IAAS,EAAE,CAAA;AAC3C;AAQO,SAAS,eAAkC,IAAA,EAAgB;AAChE,EAAA,MAAM,QAAA,GAAW,KAAK,MAAA,CAAO,CAAC,MAAM,CAAC,eAAA,CAAgB,CAAC,CAAC,CAAA;AACvD,EAAA,MAAM,UAAA,GAAa,IAAA,CAAK,MAAA,CAAO,eAAe,CAAA;AAC9C,EAAA,IAAI,UAAA,CAAW,MAAA,IAAU,CAAA,EAAG,OAAO,IAAA;AAEnC,EAAA,MAAM,UAAA,GAAa,UAAA,CAAW,MAAA,IAAU,CAAA,IAAK,UAAA,CAAW,MAAM,CAAC,CAAA,KAAM,CAAA,CAAE,KAAA,KAAU,GAAG,CAAA;AACpF,EAAA,MAAM,UAAa,UAAA,GACf;AAAA,IACE,GAAG,WAAW,CAAC,CAAA;AAAA,IACf,KAAA,EAAO,8BAAA;AAAA,IACP,WAAA,EAAa;AAAA,GACf,GACC,UAAA,CAAW,IAAA,CAAK,CAAC,CAAA,KAAM,cAAA,CAAe,IAAA,CAAK,CAAA,CAAE,KAAK,CAAC,CAAA,IAAK,UAAA,CAAW,CAAC,CAAA;AAEzE,EAAA,OAAO,CAAC,GAAG,QAAA,EAAU,OAAO,CAAA;AAC9B;AAMO,IAAM,yBAAA,GAA4B;AAAA,EACvC,OAAA,EAAS,yCAAA;AAAA,EACT,GAAA,EAAK,gDAAA;AAAA,EACL,KAAA,EAAO,SAAA;AAAA,EACP,WAAA,EAAa,0EAAA;AAAA,EACb,IAAA,EAAM,QAAA;AAAA,EACN,QAAA,EAAU,CAAC,OAAA,EAAS,OAAO,CAAA;AAAA,EAC3B,UAAA,EAAY;AAAA,IACV,KAAA,EAAO,EAAE,IAAA,EAAM,QAAA,EAAS;AAAA,IACxB,KAAA,EAAO,EAAE,IAAA,EAAM,QAAA,EAAS;AAAA,IACxB,WAAA,EAAa,EAAE,IAAA,EAAM,QAAA,EAAS;AAAA,IAC9B,QAAQ,EAAE,IAAA,EAAM,CAAC,UAAA,EAAY,UAAA,EAAY,WAAW,CAAA,EAAE;AAAA,IACtD,UAAA,EAAY;AAAA,MACV,IAAA,EAAM,QAAA;AAAA,MACN,QAAA,EAAU,CAAC,MAAA,EAAQ,cAAA,EAAgB,MAAM,CAAA;AAAA,MACzC,UAAA,EAAY;AAAA,QACV,IAAA,EAAM,EAAE,IAAA,EAAM,QAAA,EAAU,WAAW,CAAA,EAAE;AAAA,QACrC,IAAA,EAAM,EAAE,IAAA,EAAM,QAAA,EAAS;AAAA,QACvB,YAAA,EAAc,EAAE,IAAA,EAAM,QAAA,EAAU,WAAW,CAAA,EAAE;AAAA,QAC7C,IAAA,EAAM,EAAE,IAAA,EAAM,QAAA,EAAU,SAAS,wBAAA;AAAyB;AAC5D;AACF,GACF;AAAA;AAAA,EAEA,EAAA,EAAI,EAAE,UAAA,EAAY,EAAE,MAAA,EAAQ,EAAE,KAAA,EAAO,UAAA,EAAW,EAAE,EAAG,QAAA,EAAU,CAAC,QAAQ,CAAA,EAAE;AAAA,EAC1E,IAAA,EAAM,EAAE,QAAA,EAAU,CAAC,YAAY,CAAA,EAAE;AAAA,EACjC,oBAAA,EAAsB;AACxB;AAwBO,IAAM,cAAA,GAAiB,CAAC,UAAA,EAAY,UAAA,EAAY,WAAW;AAqBlE,IAAM,UAAA,GAAa,qBAAA;AACnB,IAAM,MAAA,GAAS;AAAA,EACb,SAAA;AAAA,EAAW,UAAA;AAAA,EAAY,OAAA;AAAA,EAAS,OAAA;AAAA,EAAS,KAAA;AAAA,EAAO,MAAA;AAAA,EAChD,MAAA;AAAA,EAAQ,QAAA;AAAA,EAAU,WAAA;AAAA,EAAa,SAAA;AAAA,EAAW,UAAA;AAAA,EAAY;AACxD,CAAA;AAEO,SAAS,eAAe,KAAA,EAAuC;AACpE,EAAA,OAAO,OAAO,KAAA,KAAU,QAAA,IAAa,cAAA,CAAqC,SAAS,KAAK,CAAA;AAC1F;AAQO,SAAS,oBAAoB,MAAA,EAAsC;AACxE,EAAA,IAAI,OAAO,MAAA,KAAW,MAAA,IAAa,MAAA,CAAO,MAAA,KAAW,MAAM,OAAO,IAAA;AAClE,EAAA,IAAI,CAAC,cAAA,CAAe,MAAA,CAAO,MAAM,CAAA,SAAU,CAAA,sBAAA,EAAyB,cAAA,CAAe,IAAA,CAAK,IAAI,CAAC,CAAA,CAAA;AAC7F,EAAA,IAAI,MAAA,CAAO,MAAA,KAAW,UAAA,EAAY,OAAO,IAAA;AACzC,EAAA,MAAM,KAAK,MAAA,CAAO,UAAA;AAClB,EAAA,IAAI,CAAC,EAAA,IAAM,OAAO,EAAA,KAAO,QAAA,EAAU;AACjC,IAAA,OAAO,gFAAA;AAAA,EACT;AACA,EAAA,IAAI,OAAO,GAAG,IAAA,KAAS,QAAA,IAAY,CAAC,EAAA,CAAG,IAAA,CAAK,IAAA,EAAK,EAAG,OAAO,6BAAA;AAC3D,EAAA,IAAI,OAAO,GAAG,YAAA,KAAiB,QAAA,IAAY,CAAC,EAAA,CAAG,YAAA,CAAa,MAAK,EAAG;AAClE,IAAA,OAAO,qCAAA;AAAA,EACT;AACA,EAAA,IAAI,OAAO,EAAA,CAAG,IAAA,KAAS,QAAA,IAAY,CAAC,WAAW,IAAA,CAAK,EAAA,CAAG,IAAI,CAAA,IAAK,OAAO,KAAA,CAAM,IAAA,CAAK,MAAM,EAAA,CAAG,IAAI,CAAC,CAAA,EAAG;AACjG,IAAA,OAAO,gDAAA;AAAA,EACT;AACA,EAAA,IAAI,GAAG,IAAA,KAAS,MAAA,IAAa,OAAO,EAAA,CAAG,IAAA,KAAS,UAAU,OAAO,8BAAA;AACjE,EAAA,OAAO,IAAA;AACT;AAOO,SAAS,iBAAiB,MAAA,EAAgC;AAC/D,EAAA,IAAI,MAAA,CAAO,MAAA,KAAW,UAAA,EAAY,OAAO,IAAA;AACzC,EAAA,OAAO,MAAA,CAAO,MAAA,KAAW,UAAA,IAAc,mBAAA,CAAoB,MAAM,CAAA,KAAM,IAAA;AACzE;AAGO,SAAS,kBAAkB,MAAA,EAAgC;AAChE,EAAA,IAAI,MAAA,KAAW,YAAY,OAAO,UAAA;AAClC,EAAA,IAAI,MAAA,KAAW,YAAY,OAAO,iBAAA;AAClC,EAAA,IAAI,MAAA,KAAW,aAAa,OAAO,WAAA;AACnC,EAAA,OAAO,IAAA;AACT;AAOO,SAAS,kBAAkB,EAAA,EAA+B;AAC/D,EAAA,MAAM,CAAC,IAAA,EAAM,KAAA,EAAO,GAAG,CAAA,GAAI,EAAA,CAAG,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA,CAAE,GAAA,CAAI,MAAM,CAAA;AACxD,EAAA,MAAM,IAAA,GAAO,MAAA,CAAO,KAAA,GAAQ,CAAC,IAAI,CAAA,EAAG,MAAA,CAAO,KAAA,GAAQ,CAAC,CAAC,CAAA,CAAA,EAAI,GAAG,CAAA,EAAA,EAAK,IAAI,KAAK,EAAA,CAAG,IAAA;AAC7E,EAAA,OAAO,CAAC,eAAe,EAAA,CAAG,IAAA,CAAK,MAAM,CAAA,CAAA,EAAI,GAAG,IAAA,EAAM,IAAA,IAAQ,EAAA,CAAG,YAAA,CAAa,MAAK,EAAG,IAAI,EACnF,MAAA,CAAO,OAAO,CAAA,CACd,IAAA,CAAK,IAAI,CAAA;AACd;AAGO,SAAS,iBAAiB,EAAA,EAAqD;AACpF,EAAA,OAAO,CAAA,IAAA,EAAO,EAAA,CAAG,YAAA,CAAa,IAAA,EAAM,CAAA,CAAA;AACtC;AAUO,SAAS,oBAAA,CACd,QAAA,EACA,IAAA,EACA,KAAA,EACK;AACL,EAAA,MAAM,IAAA,GAAO,CAAC,MAAA,KAAc,MAAA,CAAO,KAAA,CAAM,MAAM,CAAA,IAAK,EAAE,CAAA,CAAE,IAAA,EAAK,CAAE,WAAA,EAAY;AAC3E,EAAA,MAAM,QAAA,GAAA,CAAY,YAAY,EAAC,EAAG,OAAO,CAAC,CAAA,KAAM,CAAA,EAAG,MAAA,KAAW,UAAU,CAAA;AACxE,EAAA,MAAM,QAAQ,IAAI,GAAA,CAAI,QAAA,CAAS,GAAA,CAAI,IAAI,CAAC,CAAA;AACxC,EAAA,MAAM,aAAa,IAAA,IAAQ,EAAC,EAAG,MAAA,CAAO,CAAC,CAAA,KAAM,CAAA,IAAK,CAAA,CAAE,MAAA,KAAW,cAAc,CAAC,KAAA,CAAM,IAAI,IAAA,CAAK,CAAC,CAAC,CAAC,CAAA;AAChG,EAAA,OAAO,CAAC,GAAG,QAAA,EAAU,GAAG,SAAS,CAAA;AACnC","file":"contract.mjs","sourcesContent":["/**\n * @sonordev/site-kit/portfolio/contract — portfolio proof display policy (v1)\n *\n * Single source of truth for the Lighthouse-KPI collapse policy shared by the\n * content side (signal-api generation) and the render side (agency-site-kit\n * display). Same pattern as the other site-kit contracts:\n * - agency-site-kit imports these directly (it already depends on site-kit)\n * from `src/portfolio/curate-proof.ts`, which keeps the render-only\n * primitives that bind to its section types.\n * - signal-api keeps a byte-identical inline mirror\n * (`src/modules/skills/portfolio/portfolio-kpi-policy.util.ts`) guarded by\n * the shared drift-test vectors in `contract.test.ts`.\n *\n * Why it exists: generated case studies arrived with four \"100/100 Lighthouse X\"\n * KPIs repeated across hero, results cards, and a Performance section. One\n * differentiator repeated a dozen times stops differentiating — collapse the\n * per-category Lighthouse tiles to at most one summary, business outcomes first.\n *\n * It is also the single source of truth for METRIC PROVENANCE (below): the\n * three sources a portfolio number can have, what a client-reported number\n * must carry, and which numbers may headline. sonor-api and signal-api keep\n * byte-identical mirrors of those functions too, guarded by the same vectors.\n */\n\n// The live-frame handshake a case study's device frames listen for: defined\n// with the frame rules in shared/frame.ts, re-exported here because the\n// portfolio renderers are the ones listening.\nexport { FRAME_READY_MESSAGE, isFrameReadyMessage, type FrameReadyMessage } from '../shared/frame'\n\n/** Bump on breaking changes to the collapse policy. */\nexport const PORTFOLIO_CONTRACT_VERSION = 1 as const\n\n/** Minimal KPI shape the policy reads — callers pass their own richer tiles. */\nexport interface KpiLike {\n label: string\n value: number\n description?: string\n}\n\nconst LIGHTHOUSE_RE = /lighthouse/i\n\n/** True when a KPI tile is a per-category Lighthouse score. */\nexport function isLighthouseKpi(kpi: Pick<KpiLike, 'label'>): boolean {\n return LIGHTHOUSE_RE.test(kpi.label || '')\n}\n\n/**\n * Collapse per-category Lighthouse KPIs to at most one summary tile, business\n * outcomes first. When every category is present at 100 the summary says so;\n * otherwise the Performance category (the one buyers know) represents the set.\n * Fewer than two Lighthouse tiles → returned unchanged.\n */\nexport function curateHeroKpis<T extends KpiLike>(kpis: T[]): T[] {\n const business = kpis.filter((k) => !isLighthouseKpi(k))\n const lighthouse = kpis.filter(isLighthouseKpi)\n if (lighthouse.length <= 1) return kpis\n\n const allPerfect = lighthouse.length >= 4 && lighthouse.every((k) => k.value === 100)\n const summary: T = allPerfect\n ? {\n ...lighthouse[0],\n label: 'Lighthouse, all 4 categories',\n description: 'Perfect scores across performance, SEO, accessibility, and best practices.',\n }\n : (lighthouse.find((k) => /performance/i.test(k.label)) ?? lighthouse[0])\n\n return [...business, summary]\n}\n\n/**\n * Agent-native contract: JSON Schema for the minimal KPI shape, so services and\n * agents can validate tiles before applying the collapse policy.\n */\nexport const PORTFOLIO_KPI_JSON_SCHEMA = {\n $schema: 'http://json-schema.org/draft-07/schema#',\n $id: 'https://sonor.io/schemas/portfolio-kpi.v1.json',\n title: 'KpiLike',\n description: 'The subset of a portfolio KPI tile the Lighthouse-collapse policy reads.',\n type: 'object',\n required: ['label', 'value'],\n properties: {\n label: { type: 'string' },\n value: { type: 'number' },\n description: { type: 'string' },\n source: { enum: ['measured', 'reported', 'estimated'] },\n reportedBy: {\n type: 'object',\n required: ['name', 'organization', 'date'],\n properties: {\n name: { type: 'string', minLength: 1 },\n role: { type: 'string' },\n organization: { type: 'string', minLength: 1 },\n date: { type: 'string', pattern: '^\\\\d{4}-\\\\d{2}-\\\\d{2}$' },\n },\n },\n },\n // A reported number is only valid with someone named behind it.\n if: { properties: { source: { const: 'reported' } }, required: ['source'] },\n then: { required: ['reportedBy'] },\n additionalProperties: true,\n} as const\n\n// ─── Metric provenance ──────────────────────────────────────────────────────\n//\n// Where a published portfolio number came from. The three sources are not\n// grades of confidence, they're three different claims, and no surface may\n// let one pass as another:\n//\n// - measured: we measured it (Lighthouse, analytics, a count anyone can\n// re-run against the live site).\n// - reported: the client told us, and a named person at the client stands\n// behind it. Revenue, headcount and signed work can only ever\n// be this, because we can't see a client's books. It always\n// carries `reportedBy`, and every surface that shows it says\n// whose number it is.\n// - estimated: a reasonable projection. It never headlines anything.\n//\n// Proof is the strongest thing a case study has, and the client's own numbers\n// are usually the strongest proof. Before 'reported' existed they had nowhere\n// honest to go: tagging True Power Systems' \"$1M in signed work from web\n// leads\" as measured would have been false, and as estimated both false and\n// buried (2026-09-25).\n\n/** Every source a portfolio number can have. */\nexport const METRIC_SOURCES = ['measured', 'reported', 'estimated'] as const\nexport type MetricSource = (typeof METRIC_SOURCES)[number]\n\n/** Who stands behind a client-reported number. */\nexport interface MetricAttribution {\n /** The person, e.g. \"Scott Mann\". */\n name: string\n /** Their title, e.g. \"Director of Business Development\". */\n role?: string\n /** Where they work: almost always the client, e.g. \"True Power Systems\". */\n organization: string\n /** The day they reported it, YYYY-MM-DD. */\n date: string\n}\n\n/** The provenance fields a portfolio number (a KPI tile, a results card) carries. */\nexport interface SourcedMetric {\n source?: string | null\n reportedBy?: unknown\n}\n\nconst ISO_DAY_RE = /^\\d{4}-\\d{2}-\\d{2}$/\nconst MONTHS = [\n 'January', 'February', 'March', 'April', 'May', 'June',\n 'July', 'August', 'September', 'October', 'November', 'December',\n]\n\nexport function isMetricSource(value: unknown): value is MetricSource {\n return typeof value === 'string' && (METRIC_SOURCES as readonly string[]).includes(value)\n}\n\n/**\n * What's wrong with a number's provenance, or null when nothing is. A missing\n * source is legacy data and passes (displays show it unlabelled, and it can't\n * headline); an unknown source, or a reported number nobody is named behind,\n * does not.\n */\nexport function metricSourceProblem(metric: SourcedMetric): string | null {\n if (metric.source === undefined || metric.source === null) return null\n if (!isMetricSource(metric.source)) return `source must be one of ${METRIC_SOURCES.join(', ')}`\n if (metric.source !== 'reported') return null\n const by = metric.reportedBy as Partial<MetricAttribution> | null | undefined\n if (!by || typeof by !== 'object') {\n return 'a reported number needs reportedBy: who reported it, where they work, and when'\n }\n if (typeof by.name !== 'string' || !by.name.trim()) return 'reportedBy.name is required'\n if (typeof by.organization !== 'string' || !by.organization.trim()) {\n return 'reportedBy.organization is required'\n }\n if (typeof by.date !== 'string' || !ISO_DAY_RE.test(by.date) || Number.isNaN(Date.parse(by.date))) {\n return 'reportedBy.date must be a real day, YYYY-MM-DD'\n }\n if (by.role !== undefined && typeof by.role !== 'string') return 'reportedBy.role must be text'\n return null\n}\n\n/**\n * May this number lead: a hero tile, a gallery card, a carousel, a social\n * post? Measured, or reported with someone named behind it. Never an\n * estimate, and never an unlabelled legacy number.\n */\nexport function isHeadlineMetric(metric: SourcedMetric): boolean {\n if (metric.source === 'measured') return true\n return metric.source === 'reported' && metricSourceProblem(metric) === null\n}\n\n/** The badge a display shows, or null for a missing or unknown source (show nothing rather than guess). */\nexport function metricSourceLabel(source: unknown): string | null {\n if (source === 'measured') return 'Measured'\n if (source === 'reported') return 'Client-reported'\n if (source === 'estimated') return 'Estimated'\n return null\n}\n\n/**\n * The full credit: \"Reported by Scott Mann, Director of Business Development,\n * True Power Systems, September 25, 2026\". The date is read as a plain day,\n * never through a time zone, so it can't slip to the day before.\n */\nexport function formatAttribution(by: MetricAttribution): string {\n const [year, month, day] = by.date.split('-').map(Number)\n const when = MONTHS[month - 1] ? `${MONTHS[month - 1]} ${day}, ${year}` : by.date\n return [`Reported by ${by.name.trim()}`, by.role?.trim(), by.organization.trim(), when]\n .filter(Boolean)\n .join(', ')\n}\n\n/** The credit for a tight space (a card, a carousel tile): \"per True Power Systems\". */\nexport function shortAttribution(by: Pick<MetricAttribution, 'organization'>): string {\n return `per ${by.organization.trim()}`\n}\n\n/**\n * Carry a person's reported numbers through a regeneration. The generator can\n * only write measured or estimated numbers (it can't see a client's books, and\n * any 'reported' tag it writes is dropped here), so without this a rewrite of\n * the hero or the results would silently erase what a client told us.\n * Reported entries from `previous` lead, in their order; `next` fills in\n * behind them, minus anything sharing a key with a reported entry.\n */\nexport function carryReportedMetrics<T extends SourcedMetric>(\n previous: readonly T[] | null | undefined,\n next: readonly T[] | null | undefined,\n keyOf: (metric: T) => string,\n): T[] {\n const norm = (metric: T) => String(keyOf(metric) ?? '').trim().toLowerCase()\n const reported = (previous ?? []).filter((m) => m?.source === 'reported')\n const taken = new Set(reported.map(norm))\n const generated = (next ?? []).filter((m) => m && m.source !== 'reported' && !taken.has(norm(m)))\n return [...reported, ...generated]\n}\n"]}
@@ -2,9 +2,9 @@
2
2
  'use strict';
3
3
 
4
4
  var chunkCLO3K7PF_js = require('../chunk-CLO3K7PF.js');
5
- var chunkC7J4XLBN_js = require('../chunk-C7J4XLBN.js');
6
- require('../chunk-U5NFIHLY.js');
7
- require('../chunk-CIB3RHNZ.js');
5
+ var chunkIO65ROAO_js = require('../chunk-IO65ROAO.js');
6
+ require('../chunk-PIPGH7GA.js');
7
+ require('../chunk-PURI56RE.js');
8
8
  require('../chunk-SNAQBZPT.js');
9
9
  var react = require('react');
10
10
  var jsxRuntime = require('react/jsx-runtime');
@@ -39,7 +39,7 @@ function useSiteSearch(options = {}) {
39
39
  const apiUrl = typeof window !== "undefined" ? window.__SITE_KIT_API_URL__ || "https://api.sonor.io" : "https://api.sonor.io";
40
40
  const apiKey = typeof window !== "undefined" ? window.__SITE_KIT_API_KEY__ : void 0;
41
41
  const resolvedSite = site ?? (typeof window !== "undefined" ? window.__SITE_KIT_SITE__ : void 0);
42
- const res = await chunkC7J4XLBN_js.sonorFetch(
42
+ const res = await chunkIO65ROAO_js.sonorFetch(
43
43
  `${apiUrl}${chunkCLO3K7PF_js.SEARCH_ENDPOINT}`,
44
44
  {
45
45
  method: "POST",
@@ -1,9 +1,9 @@
1
1
  'use client';
2
2
  import { SEARCH_ENDPOINT } from '../chunk-CFRJ2MK5.mjs';
3
3
  export { SEARCH_ENDPOINT } from '../chunk-CFRJ2MK5.mjs';
4
- import { sonorFetch } from '../chunk-5T3J24K4.mjs';
5
- import '../chunk-YRKWJCML.mjs';
6
- import '../chunk-K3PVUXKB.mjs';
4
+ import { sonorFetch } from '../chunk-3BLKKF67.mjs';
5
+ import '../chunk-PGYOBEPL.mjs';
6
+ import '../chunk-GIGUDKLS.mjs';
7
7
  import '../chunk-BEXTDHO4.mjs';
8
8
  import { useState, useRef, useCallback, useEffect } from 'react';
9
9
  import { jsxs, jsx } from 'react/jsx-runtime';
@@ -1,19 +1,20 @@
1
1
  'use client';
2
2
  'use strict';
3
3
 
4
- var chunkU4CYO4NG_js = require('../chunk-U4CYO4NG.js');
4
+ var chunkSUGBIEYQ_js = require('../chunk-SUGBIEYQ.js');
5
5
  require('../chunk-JONWE26J.js');
6
- require('../chunk-5RNAUYWE.js');
7
- require('../chunk-C7J4XLBN.js');
8
- require('../chunk-U5NFIHLY.js');
9
- require('../chunk-CIB3RHNZ.js');
6
+ require('../chunk-NH2P27MA.js');
7
+ require('../chunk-JCDX37BY.js');
8
+ require('../chunk-IO65ROAO.js');
9
+ require('../chunk-PIPGH7GA.js');
10
+ require('../chunk-PURI56RE.js');
10
11
  require('../chunk-SNAQBZPT.js');
11
12
 
12
13
 
13
14
 
14
15
  Object.defineProperty(exports, "SitemapSync", {
15
16
  enumerable: true,
16
- get: function () { return chunkU4CYO4NG_js.SitemapSync; }
17
+ get: function () { return chunkSUGBIEYQ_js.SitemapSync; }
17
18
  });
18
19
  //# sourceMappingURL=client.js.map
19
20
  //# sourceMappingURL=client.js.map
@@ -1,10 +1,11 @@
1
1
  'use client';
2
- export { SitemapSync } from '../chunk-M62L2N5A.mjs';
2
+ export { SitemapSync } from '../chunk-SVCKJ5KH.mjs';
3
3
  import '../chunk-C2CTLLIO.mjs';
4
- import '../chunk-A6UYV475.mjs';
5
- import '../chunk-5T3J24K4.mjs';
6
- import '../chunk-YRKWJCML.mjs';
7
- import '../chunk-K3PVUXKB.mjs';
4
+ import '../chunk-CABEVWKJ.mjs';
5
+ import '../chunk-MXJOKOFK.mjs';
6
+ import '../chunk-3BLKKF67.mjs';
7
+ import '../chunk-PGYOBEPL.mjs';
8
+ import '../chunk-GIGUDKLS.mjs';
8
9
  import '../chunk-BEXTDHO4.mjs';
9
10
  //# sourceMappingURL=client.mjs.map
10
11
  //# sourceMappingURL=client.mjs.map
package/dist/seo/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  var chunkDV2LJYFD_js = require('../chunk-DV2LJYFD.js');
4
4
  var chunkH6AU3WKF_js = require('../chunk-H6AU3WKF.js');
5
5
  var chunkJ7QI3GDX_js = require('../chunk-J7QI3GDX.js');
6
- var chunkU4CYO4NG_js = require('../chunk-U4CYO4NG.js');
6
+ var chunkSUGBIEYQ_js = require('../chunk-SUGBIEYQ.js');
7
7
  var chunkTST2IKYO_js = require('../chunk-TST2IKYO.js');
8
8
  var chunkOSU4QA5M_js = require('../chunk-OSU4QA5M.js');
9
9
  require('../chunk-CXAXZFFW.js');
@@ -13,10 +13,11 @@ require('../chunk-JONWE26J.js');
13
13
  require('../chunk-IBJOGXQI.js');
14
14
  require('../chunk-UZKXOZHR.js');
15
15
  require('../chunk-SWIKKSKL.js');
16
- require('../chunk-5RNAUYWE.js');
17
- require('../chunk-C7J4XLBN.js');
18
- require('../chunk-U5NFIHLY.js');
19
- require('../chunk-CIB3RHNZ.js');
16
+ require('../chunk-NH2P27MA.js');
17
+ require('../chunk-JCDX37BY.js');
18
+ require('../chunk-IO65ROAO.js');
19
+ require('../chunk-PIPGH7GA.js');
20
+ require('../chunk-PURI56RE.js');
20
21
  require('../chunk-SNAQBZPT.js');
21
22
  var react = require('react');
22
23
  var jsxRuntime = require('react/jsx-runtime');
@@ -706,7 +707,7 @@ Object.defineProperty(exports, "ManagedScripts", {
706
707
  });
707
708
  Object.defineProperty(exports, "SitemapSync", {
708
709
  enumerable: true,
709
- get: function () { return chunkU4CYO4NG_js.SitemapSync; }
710
+ get: function () { return chunkSUGBIEYQ_js.SitemapSync; }
710
711
  });
711
712
  Object.defineProperty(exports, "generateSitemap", {
712
713
  enumerable: true,