@sonordev/site-kit 6.3.6 → 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.
- package/agent-manifest.json +8 -2
- package/dist/AnalyticsProvider-44PDLORZ.js +34 -0
- package/dist/{AnalyticsProvider-L3GWK5PO.js.map → AnalyticsProvider-44PDLORZ.js.map} +1 -1
- package/dist/AnalyticsProvider-F6KHEAOX.mjs +13 -0
- package/dist/{AnalyticsProvider-XDVJTAPN.mjs.map → AnalyticsProvider-F6KHEAOX.mjs.map} +1 -1
- package/dist/{ArticleViewTracker-YMGW6ZXB.js → ArticleViewTracker-MGNVIHZ4.js} +6 -6
- package/dist/{ArticleViewTracker-YMGW6ZXB.js.map → ArticleViewTracker-MGNVIHZ4.js.map} +1 -1
- package/dist/{ArticleViewTracker-7AJJTGFY.mjs → ArticleViewTracker-NI4UNSY7.mjs} +5 -5
- package/dist/{ArticleViewTracker-7AJJTGFY.mjs.map → ArticleViewTracker-NI4UNSY7.mjs.map} +1 -1
- package/dist/{ChatWidget-MR64ZLOG.mjs → ChatWidget-XSALHDDW.mjs} +7 -7
- package/dist/{ChatWidget-MR64ZLOG.mjs.map → ChatWidget-XSALHDDW.mjs.map} +1 -1
- package/dist/{ChatWidget-IAYEXLNX.js → ChatWidget-YBXJRR2Z.js} +10 -10
- package/dist/{ChatWidget-IAYEXLNX.js.map → ChatWidget-YBXJRR2Z.js.map} +1 -1
- package/dist/EngageWidget-DUEXPVOY.mjs +13 -0
- package/dist/{EngageWidget-RANSIKC3.mjs.map → EngageWidget-DUEXPVOY.mjs.map} +1 -1
- package/dist/EngageWidget-MDCRS3BN.js +22 -0
- package/dist/{EngageWidget-BF7DH4GO.js.map → EngageWidget-MDCRS3BN.js.map} +1 -1
- package/dist/{FileField-VRKATE4G.js → FileField-AXU6KBSV.js} +6 -6
- package/dist/{FileField-VRKATE4G.js.map → FileField-AXU6KBSV.js.map} +1 -1
- package/dist/{FileField-7ZNQ5BID.mjs → FileField-UTLGR5ZN.mjs} +5 -5
- package/dist/{FileField-7ZNQ5BID.mjs.map → FileField-UTLGR5ZN.mjs.map} +1 -1
- package/dist/{FormSpotlight-QNH7XGYS.mjs → FormSpotlight-NWLCZMOT.mjs} +3 -3
- package/dist/{FormSpotlight-QNH7XGYS.mjs.map → FormSpotlight-NWLCZMOT.mjs.map} +1 -1
- package/dist/{FormSpotlight-VQJ2QWIU.js → FormSpotlight-RR5KJDIH.js} +9 -9
- package/dist/{FormSpotlight-VQJ2QWIU.js.map → FormSpotlight-RR5KJDIH.js.map} +1 -1
- package/dist/{FormStage-3JIN6X6O.js → FormStage-GFNX4GLU.js} +9 -9
- package/dist/{FormStage-3JIN6X6O.js.map → FormStage-GFNX4GLU.js.map} +1 -1
- package/dist/{FormStage-XB4JNFCH.mjs → FormStage-V4XKLI6J.mjs} +3 -3
- package/dist/{FormStage-XB4JNFCH.mjs.map → FormStage-V4XKLI6J.mjs.map} +1 -1
- package/dist/ManagedForm-4AD3LWSC.js +25 -0
- package/dist/{ManagedForm-Q5UFMUBE.js.map → ManagedForm-4AD3LWSC.js.map} +1 -1
- package/dist/ManagedForm-4IWPG6AZ.mjs +16 -0
- package/dist/{ManagedForm-E6F4FLVY.mjs.map → ManagedForm-4IWPG6AZ.mjs.map} +1 -1
- package/dist/{ManagedNewsletterForm-OEEC3GVY.mjs → ManagedNewsletterForm-ARSY2VHC.mjs} +6 -6
- package/dist/{ManagedNewsletterForm-OEEC3GVY.mjs.map → ManagedNewsletterForm-ARSY2VHC.mjs.map} +1 -1
- package/dist/{ManagedNewsletterForm-UTNOJ5T4.js → ManagedNewsletterForm-HJZNUABY.js} +7 -7
- package/dist/{ManagedNewsletterForm-UTNOJ5T4.js.map → ManagedNewsletterForm-HJZNUABY.js.map} +1 -1
- package/dist/{SignalCore-PWN5X6FF.mjs → SignalCore-N2LKG4XN.mjs} +5 -5
- package/dist/{SignalCore-PWN5X6FF.mjs.map → SignalCore-N2LKG4XN.mjs.map} +1 -1
- package/dist/{SignalCore-4AUWHMYK.js → SignalCore-P2JUY2BO.js} +11 -11
- package/dist/{SignalCore-4AUWHMYK.js.map → SignalCore-P2JUY2BO.js.map} +1 -1
- package/dist/SitemapSync-KFY4PJ7G.js +19 -0
- package/dist/{SitemapSync-DHJ25PSH.js.map → SitemapSync-KFY4PJ7G.js.map} +1 -1
- package/dist/SitemapSync-UYDL7T4M.mjs +10 -0
- package/dist/{SitemapSync-ZA2MNM63.mjs.map → SitemapSync-UYDL7T4M.mjs.map} +1 -1
- package/dist/analytics/index.js +14 -13
- package/dist/analytics/index.mjs +6 -5
- package/dist/articles/AuthorPage.d.ts +1 -1
- package/dist/articles/AuthorPage.d.ts.map +1 -1
- package/dist/articles/author-schema.d.ts +59 -13
- package/dist/articles/author-schema.d.ts.map +1 -1
- package/dist/articles/index.js +24 -23
- package/dist/articles/index.js.map +1 -1
- package/dist/articles/index.mjs +12 -11
- package/dist/articles/index.mjs.map +1 -1
- package/dist/articles/routes.d.ts +9 -0
- package/dist/articles/routes.d.ts.map +1 -1
- package/dist/articles/server-core.d.ts +5 -2
- package/dist/articles/server-core.d.ts.map +1 -1
- package/dist/articles/server-ui.js +45 -45
- package/dist/articles/server-ui.mjs +8 -8
- package/dist/articles/server.d.ts +5 -2
- package/dist/articles/server.d.ts.map +1 -1
- package/dist/articles/server.js +72 -56
- package/dist/articles/server.mjs +3 -3
- package/dist/articles/types.d.ts +23 -0
- package/dist/articles/types.d.ts.map +1 -1
- package/dist/{chunk-UAOQVHZW.js → chunk-34XL7TVH.js} +4 -4
- package/dist/{chunk-UAOQVHZW.js.map → chunk-34XL7TVH.js.map} +1 -1
- package/dist/chunk-3AOKYAZ2.js +135 -0
- package/dist/chunk-3AOKYAZ2.js.map +1 -0
- package/dist/{chunk-BJKA3XRS.mjs → chunk-3BLKKF67.mjs} +4 -4
- package/dist/{chunk-BJKA3XRS.mjs.map → chunk-3BLKKF67.mjs.map} +1 -1
- package/dist/{chunk-3J5L5DIK.js → chunk-3EAS4PAT.js} +3 -3
- package/dist/{chunk-3J5L5DIK.js.map → chunk-3EAS4PAT.js.map} +1 -1
- package/dist/{chunk-SB4CJX2X.mjs → chunk-42LLFAJQ.mjs} +3 -3
- package/dist/{chunk-SB4CJX2X.mjs.map → chunk-42LLFAJQ.mjs.map} +1 -1
- package/dist/{chunk-ZR43XFUM.js → chunk-55J2KE7D.js} +3 -3
- package/dist/chunk-55J2KE7D.js.map +1 -0
- package/dist/{chunk-VGBUC2JP.js → chunk-5ZMTEK7U.js} +10 -10
- package/dist/{chunk-VGBUC2JP.js.map → chunk-5ZMTEK7U.js.map} +1 -1
- package/dist/{chunk-6DCAMW62.mjs → chunk-6C47333H.mjs} +3 -3
- package/dist/chunk-6C47333H.mjs.map +1 -0
- package/dist/{chunk-E7EV2AHT.js → chunk-6OQYRVMI.js} +9 -9
- package/dist/{chunk-E7EV2AHT.js.map → chunk-6OQYRVMI.js.map} +1 -1
- package/dist/{chunk-UJIKP35G.mjs → chunk-7F66Y4MA.mjs} +4 -4
- package/dist/{chunk-UJIKP35G.mjs.map → chunk-7F66Y4MA.mjs.map} +1 -1
- package/dist/{chunk-6MOGGEY3.mjs → chunk-A2HL22BO.mjs} +3 -3
- package/dist/{chunk-6MOGGEY3.mjs.map → chunk-A2HL22BO.mjs.map} +1 -1
- package/dist/{chunk-7NFSSPGM.js → chunk-AL4Z6VYF.js} +8 -8
- package/dist/{chunk-7NFSSPGM.js.map → chunk-AL4Z6VYF.js.map} +1 -1
- package/dist/{chunk-W46QIVBM.mjs → chunk-B6QPCAEG.mjs} +3 -3
- package/dist/{chunk-W46QIVBM.mjs.map → chunk-B6QPCAEG.mjs.map} +1 -1
- package/dist/{chunk-EB33U2WT.js → chunk-BHZHWXER.js} +131 -71
- package/dist/chunk-BHZHWXER.js.map +1 -0
- package/dist/{chunk-R26FAQWF.mjs → chunk-BNKBRLJK.mjs} +3 -3
- package/dist/{chunk-R26FAQWF.mjs.map → chunk-BNKBRLJK.mjs.map} +1 -1
- package/dist/{chunk-A6UYV475.mjs → chunk-CABEVWKJ.mjs} +4 -24
- package/dist/chunk-CABEVWKJ.mjs.map +1 -0
- package/dist/{chunk-LWCAZDY2.js → chunk-CDRQYJUT.js} +4 -4
- package/dist/{chunk-LWCAZDY2.js.map → chunk-CDRQYJUT.js.map} +1 -1
- package/dist/{chunk-SAOUAIRS.js → chunk-CM4AVKXR.js} +14 -14
- package/dist/{chunk-SAOUAIRS.js.map → chunk-CM4AVKXR.js.map} +1 -1
- package/dist/{chunk-KMONA7CT.mjs → chunk-D3WJMSTB.mjs} +3 -3
- package/dist/{chunk-KMONA7CT.mjs.map → chunk-D3WJMSTB.mjs.map} +1 -1
- package/dist/{chunk-3Q3Y6OQE.mjs → chunk-DWZGW77V.mjs} +4 -4
- package/dist/{chunk-3Q3Y6OQE.mjs.map → chunk-DWZGW77V.mjs.map} +1 -1
- package/dist/{chunk-L6BQIDSZ.mjs → chunk-FPVYKSXE.mjs} +13 -10
- package/dist/chunk-FPVYKSXE.mjs.map +1 -0
- package/dist/{chunk-5XDOBM5S.js → chunk-G2KGHIAP.js} +3 -3
- package/dist/{chunk-5XDOBM5S.js.map → chunk-G2KGHIAP.js.map} +1 -1
- package/dist/chunk-GFSQMK35.mjs +127 -0
- package/dist/chunk-GFSQMK35.mjs.map +1 -0
- package/dist/chunk-GIGUDKLS.mjs +6 -0
- package/dist/{chunk-3V6DIQI7.mjs.map → chunk-GIGUDKLS.mjs.map} +1 -1
- package/dist/{chunk-BJ4T65OK.mjs → chunk-H4TSADY3.mjs} +3 -3
- package/dist/{chunk-BJ4T65OK.mjs.map → chunk-H4TSADY3.mjs.map} +1 -1
- package/dist/{chunk-J4C3JAJF.js → chunk-IO65ROAO.js} +15 -15
- package/dist/{chunk-J4C3JAJF.js.map → chunk-IO65ROAO.js.map} +1 -1
- package/dist/chunk-JCDX37BY.js +58 -0
- package/dist/chunk-JCDX37BY.js.map +1 -0
- package/dist/{chunk-5J4FKRGH.mjs → chunk-MDQLON5R.mjs} +5 -5
- package/dist/{chunk-5J4FKRGH.mjs.map → chunk-MDQLON5R.mjs.map} +1 -1
- package/dist/{chunk-3POB4JKR.js → chunk-MHKD34B4.js} +15 -15
- package/dist/{chunk-3POB4JKR.js.map → chunk-MHKD34B4.js.map} +1 -1
- package/dist/chunk-MXJOKOFK.mjs +51 -0
- package/dist/chunk-MXJOKOFK.mjs.map +1 -0
- package/dist/{chunk-5RNAUYWE.js → chunk-NH2P27MA.js} +4 -27
- package/dist/chunk-NH2P27MA.js.map +1 -0
- package/dist/{chunk-5DZVQW35.mjs → chunk-OFBEBMXN.mjs} +4 -4
- package/dist/{chunk-5DZVQW35.mjs.map → chunk-OFBEBMXN.mjs.map} +1 -1
- package/dist/{chunk-G3L26XQ7.js → chunk-OIPG3EGM.js} +5 -5
- package/dist/{chunk-G3L26XQ7.js.map → chunk-OIPG3EGM.js.map} +1 -1
- package/dist/{chunk-2UZULHI4.js → chunk-OY3PDIB3.js} +10 -10
- package/dist/{chunk-2UZULHI4.js.map → chunk-OY3PDIB3.js.map} +1 -1
- package/dist/{chunk-SNXJXPB6.js → chunk-PFDRQJKJ.js} +13 -13
- package/dist/{chunk-SNXJXPB6.js.map → chunk-PFDRQJKJ.js.map} +1 -1
- package/dist/{chunk-CXYGKK4U.mjs → chunk-PGYOBEPL.mjs} +3 -3
- package/dist/{chunk-CXYGKK4U.mjs.map → chunk-PGYOBEPL.mjs.map} +1 -1
- package/dist/{chunk-TE3YNZVH.js → chunk-PIPGH7GA.js} +4 -4
- package/dist/{chunk-TE3YNZVH.js.map → chunk-PIPGH7GA.js.map} +1 -1
- package/dist/chunk-PURI56RE.js +8 -0
- package/dist/{chunk-6SBRD5KT.js.map → chunk-PURI56RE.js.map} +1 -1
- package/dist/{chunk-X3MY4PL7.mjs → chunk-QAQTDHHT.mjs} +10 -14
- package/dist/chunk-QAQTDHHT.mjs.map +1 -0
- package/dist/{chunk-PMWF5CRL.js → chunk-QCS7NWMA.js} +15 -12
- package/dist/chunk-QCS7NWMA.js.map +1 -0
- package/dist/{chunk-HXHRS3PX.mjs → chunk-RCB6HEHM.mjs} +3 -3
- package/dist/{chunk-HXHRS3PX.mjs.map → chunk-RCB6HEHM.mjs.map} +1 -1
- package/dist/{chunk-ANYCUTS5.js → chunk-RMDH2HMW.js} +3 -3
- package/dist/{chunk-ANYCUTS5.js.map → chunk-RMDH2HMW.js.map} +1 -1
- package/dist/{chunk-NAA5J2JM.js → chunk-RQROZLH3.js} +47 -51
- package/dist/chunk-RQROZLH3.js.map +1 -0
- package/dist/{chunk-X5J3PYZJ.js → chunk-SUGBIEYQ.js} +9 -9
- package/dist/{chunk-X5J3PYZJ.js.map → chunk-SUGBIEYQ.js.map} +1 -1
- package/dist/{chunk-LFF32POH.mjs → chunk-SVCKJ5KH.mjs} +4 -4
- package/dist/{chunk-LFF32POH.mjs.map → chunk-SVCKJ5KH.mjs.map} +1 -1
- package/dist/{chunk-YKZ3EVAQ.mjs → chunk-THO4NS6Z.mjs} +3 -3
- package/dist/{chunk-YKZ3EVAQ.mjs.map → chunk-THO4NS6Z.mjs.map} +1 -1
- package/dist/{chunk-K6ZQVDP4.mjs → chunk-TPEPAHU5.mjs} +3 -3
- package/dist/{chunk-K6ZQVDP4.mjs.map → chunk-TPEPAHU5.mjs.map} +1 -1
- package/dist/{chunk-FRUBUQOH.js → chunk-U577ITSA.js} +5 -5
- package/dist/{chunk-FRUBUQOH.js.map → chunk-U577ITSA.js.map} +1 -1
- package/dist/{chunk-B2NO5QVY.mjs → chunk-UCQDE5UH.mjs} +3 -3
- package/dist/{chunk-B2NO5QVY.mjs.map → chunk-UCQDE5UH.mjs.map} +1 -1
- package/dist/{chunk-KB7SIIVY.mjs → chunk-VA2O5OSV.mjs} +3 -3
- package/dist/{chunk-KB7SIIVY.mjs.map → chunk-VA2O5OSV.mjs.map} +1 -1
- package/dist/{chunk-6I5O5CTL.mjs → chunk-VJTKIKZY.mjs} +128 -68
- package/dist/chunk-VJTKIKZY.mjs.map +1 -0
- package/dist/{chunk-OLVCGOXZ.js → chunk-VUAGR6VL.js} +12 -12
- package/dist/{chunk-OLVCGOXZ.js.map → chunk-VUAGR6VL.js.map} +1 -1
- package/dist/{chunk-PQICWWQW.mjs → chunk-XEOIGCUE.mjs} +5 -5
- package/dist/{chunk-PQICWWQW.mjs.map → chunk-XEOIGCUE.mjs.map} +1 -1
- package/dist/chunk-YMZNEDFP.js +39 -0
- package/dist/chunk-YMZNEDFP.js.map +1 -0
- package/dist/chunk-Z2URIOKG.mjs +36 -0
- package/dist/chunk-Z2URIOKG.mjs.map +1 -0
- package/dist/{chunk-2MEMK5XC.js → chunk-ZXC56JS3.js} +14 -14
- package/dist/{chunk-2MEMK5XC.js.map → chunk-ZXC56JS3.js.map} +1 -1
- package/dist/cli/index.js +3 -3
- package/dist/cli/index.mjs +1 -1
- package/dist/client/index.js +4 -4
- package/dist/client/index.mjs +3 -3
- package/dist/commerce/index.js +59 -58
- package/dist/commerce/index.mjs +7 -6
- package/dist/engage/DesignRenderer.d.ts.map +1 -1
- package/dist/engage/EngageWidget.d.ts.map +1 -1
- package/dist/engage/element-rules.d.ts +39 -0
- package/dist/engage/element-rules.d.ts.map +1 -0
- package/dist/engage/index.js +11 -10
- package/dist/engage/index.js.map +1 -1
- package/dist/engage/index.mjs +8 -7
- package/dist/engage/index.mjs.map +1 -1
- package/dist/fleet/index.js +10 -9
- package/dist/fleet/index.mjs +6 -5
- package/dist/forms/index.js +13 -13
- package/dist/forms/index.mjs +7 -7
- package/dist/forms/server.js +2 -2
- package/dist/forms/server.mjs +1 -1
- package/dist/images/index.js +11 -11
- package/dist/images/index.mjs +4 -4
- package/dist/index.js +65 -64
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +11 -10
- package/dist/index.mjs.map +1 -1
- package/dist/layout/SiteKitClientProviders.d.ts.map +1 -1
- package/dist/layout/client.d.ts.map +1 -1
- package/dist/layout/client.js +10 -9
- package/dist/layout/client.mjs +9 -8
- package/dist/layout/index.js +12 -11
- package/dist/layout/index.js.map +1 -1
- package/dist/layout/index.mjs +10 -9
- package/dist/layout/index.mjs.map +1 -1
- package/dist/layout/types.d.ts +6 -0
- package/dist/layout/types.d.ts.map +1 -1
- package/dist/llms/index.d.ts +2 -0
- package/dist/llms/index.d.ts.map +1 -1
- package/dist/llms/index.js +129 -14
- package/dist/llms/index.js.map +1 -1
- package/dist/llms/index.mjs +115 -5
- package/dist/llms/index.mjs.map +1 -1
- package/dist/llms/revalidate.d.ts +3 -0
- package/dist/llms/revalidate.d.ts.map +1 -1
- package/dist/llms/seo-revalidate.d.ts +105 -0
- package/dist/llms/seo-revalidate.d.ts.map +1 -0
- package/dist/maps/index.js +5 -5
- package/dist/maps/index.mjs +3 -3
- package/dist/mcp/transport.d.ts +166 -0
- package/dist/mcp/transport.d.ts.map +1 -0
- package/dist/mcp/transport.js +137 -0
- package/dist/mcp/transport.js.map +1 -0
- package/dist/mcp/transport.mjs +123 -0
- package/dist/mcp/transport.mjs.map +1 -0
- package/dist/portfolio/contract.d.ts +93 -0
- package/dist/portfolio/contract.d.ts.map +1 -1
- package/dist/portfolio/contract.js +94 -1
- package/dist/portfolio/contract.js.map +1 -1
- package/dist/portfolio/contract.mjs +79 -2
- package/dist/portfolio/contract.mjs.map +1 -1
- package/dist/search/index.js +4 -4
- package/dist/search/index.mjs +3 -3
- package/dist/seo/client.js +7 -6
- package/dist/seo/client.mjs +6 -5
- package/dist/seo/index.js +7 -6
- package/dist/seo/index.js.map +1 -1
- package/dist/seo/index.mjs +6 -5
- package/dist/seo/index.mjs.map +1 -1
- package/dist/seo/register-sitemap-cli.js +1 -1
- package/dist/seo/register-sitemap-cli.mjs +1 -1
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +2 -2
- package/dist/shared/build-entries.d.ts +7 -0
- package/dist/shared/build-entries.d.ts.map +1 -1
- package/dist/shared/frame.d.ts +15 -0
- package/dist/shared/frame.d.ts.map +1 -1
- package/dist/shared/request-guards.d.ts +27 -0
- package/dist/shared/request-guards.d.ts.map +1 -0
- package/dist/shared/version.d.ts +1 -1
- package/dist/signal/index.js +11 -11
- package/dist/signal/index.mjs +2 -2
- package/dist/sitemap/index.js +3 -3
- package/dist/sitemap/index.mjs +2 -2
- package/dist/sync/index.js +17 -17
- package/dist/sync/index.mjs +4 -4
- package/dist/{writeLLMsTxt-LYYC3ZSN.mjs → writeLLMsTxt-IYJILIU3.mjs} +3 -3
- package/dist/{writeLLMsTxt-XHLHJ5XY.js → writeLLMsTxt-SQG4B63X.js} +4 -4
- package/package.json +24 -21
- package/dist/AnalyticsProvider-L3GWK5PO.js +0 -33
- package/dist/AnalyticsProvider-XDVJTAPN.mjs +0 -12
- package/dist/EngageWidget-BF7DH4GO.js +0 -21
- package/dist/EngageWidget-RANSIKC3.mjs +0 -12
- package/dist/ManagedForm-E6F4FLVY.mjs +0 -16
- package/dist/ManagedForm-Q5UFMUBE.js +0 -25
- package/dist/SitemapSync-DHJ25PSH.js +0 -18
- package/dist/SitemapSync-ZA2MNM63.mjs +0 -9
- package/dist/chunk-3V6DIQI7.mjs +0 -6
- package/dist/chunk-5RNAUYWE.js.map +0 -1
- package/dist/chunk-6DCAMW62.mjs.map +0 -1
- package/dist/chunk-6I5O5CTL.mjs.map +0 -1
- package/dist/chunk-6SBRD5KT.js +0 -8
- package/dist/chunk-A6UYV475.mjs.map +0 -1
- package/dist/chunk-EB33U2WT.js.map +0 -1
- package/dist/chunk-L6BQIDSZ.mjs.map +0 -1
- package/dist/chunk-NAA5J2JM.js.map +0 -1
- package/dist/chunk-PMWF5CRL.js.map +0 -1
- package/dist/chunk-X3MY4PL7.mjs.map +0 -1
- package/dist/chunk-YRCR7WY3.js +0 -65
- package/dist/chunk-YRCR7WY3.js.map +0 -1
- package/dist/chunk-ZL35JCDA.mjs +0 -61
- package/dist/chunk-ZL35JCDA.mjs.map +0 -1
- package/dist/chunk-ZR43XFUM.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/mcp/transport.ts"],"names":[],"mappings":"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","file":"transport.mjs","sourcesContent":["/**\n * @sonordev/site-kit/mcp/transport — the rate-limited public MCP relay\n *\n * Netlify rate-limits a native function (declarative `config.rateLimit`), never\n * a Next.js route handler. So a public MCP endpoint on Netlify takes this path:\n *\n * agent ─► /api/mcp netlify/functions/mcp.mjs, rate-limited\n * │ createNetlifyMcpRelay: signs the request\n * │ (HMAC-SHA256 of MCP_TRANSPORT_SECRET over a label)\n * ▼\n * https://<deploy-id>--<site>.netlify.app/api/mcp-internal\n * │ createMcpInternalRoute: 403 unless signed,\n * ▼ then dispatches to the /api/mcp handlers\n * app/api/mcp/route.ts protectMcpHandlers(createMcpHandler(...))\n *\n * In production the function owns /api/mcp, and the Next route behind it\n * refuses unsigned calls too, so no request reaches a tool without passing\n * the rate limit. The relay targets the deploy permalink, so a request is\n * answered by the same immutable deploy that received it, and the base URL\n * comes from the platform (`context`), never from a client header.\n *\n * Single source of truth for the relay. It replaced per-site copies in\n * upforge-nextjs and wirsch-law-nextjs (`lib/mcp/transport-auth.ts`, the\n * `protect()` wrapper, the `/api/mcp-internal` relay and the function body);\n * don't fork it back into a site.\n *\n * Node only (`crypto`), and deliberately not re-exported from the\n * `@sonordev/site-kit/mcp` barrel so that barrel stays runtime-neutral.\n * Nothing here imports `server-only`: the Netlify function runs in plain Node,\n * where `server-only` throws on import.\n *\n * Setup: `MCP_TRANSPORT_SECRET` (`openssl rand -hex 32`) in every deploy\n * context, Functions scope. See the module README for the three files.\n */\n\nimport { createHmac, timingSafeEqual } from 'crypto'\nimport { readCappedBody } from '../shared/request-guards'\n\n// ============================================\n// Constants\n// ============================================\n\n/** Env var holding the relay's HMAC key. */\nexport const MCP_TRANSPORT_SECRET_ENV = 'MCP_TRANSPORT_SECRET'\n\n/** Default request header carrying the signature (and the response marker). */\nexport const DEFAULT_MCP_TRANSPORT_HEADER = 'x-site-mcp-transport'\n\n/** Default HMAC message. Changing it invalidates every signature at once. */\nexport const DEFAULT_MCP_TRANSPORT_LABEL = 'site-mcp-transport-v1'\n\n/** Path of the signed Next route the function relays to. */\nexport const MCP_INTERNAL_PATH = '/api/mcp-internal'\n\n/** Value of the marker header on every relayed response. Release checks assert it. */\nexport const MCP_RELAY_MARKER = 'netlify-rate-limited-v1'\n\n/** Largest POST body the relay forwards (tool arguments), in bytes. */\nexport const MCP_RELAY_MAX_BODY_BYTES = 64 * 1024\n\n/** Upstream timeout. Under Netlify's 60 s synchronous function limit, so the 502 is ours. */\nexport const MCP_RELAY_TIMEOUT_MS = 55_000\n\n/** Methods the MCP wire uses. Anything else gets a 405. */\nexport const MCP_RELAY_METHODS = ['GET', 'POST', 'OPTIONS', 'DELETE'] as const\n\n// ============================================\n// Types\n// ============================================\n\nexport interface McpTransportOptions {\n /**\n * Request header that carries the signature. The relay also stamps it on\n * responses with {@link MCP_RELAY_MARKER}. Must match between the function\n * and the Next routes. Default {@link DEFAULT_MCP_TRANSPORT_HEADER}.\n */\n header?: string\n /**\n * HMAC message. Must match between the function and the Next routes.\n * Default {@link DEFAULT_MCP_TRANSPORT_LABEL}.\n */\n label?: string\n /**\n * The HMAC key, or a getter for it. Default: `MCP_TRANSPORT_SECRET`, read on\n * every request (`Netlify.env` when present, else `process.env`).\n */\n secret?: string | (() => string | undefined)\n}\n\nexport type McpRouteHandler = (request: Request) => Response | Promise<Response>\n\n/** The four method handlers an MCP route exports (what `createMcpHandler` returns). */\nexport interface McpRouteHandlerSet {\n GET: McpRouteHandler\n POST: McpRouteHandler\n DELETE: McpRouteHandler\n OPTIONS: McpRouteHandler\n}\n\nexport interface ProtectMcpHandlersOptions extends McpTransportOptions {\n /**\n * Refuse unsigned requests. Default: `NODE_ENV === 'production'`, read per\n * request, so `next dev` still answers a local MCP client directly.\n */\n enforce?: boolean | (() => boolean)\n}\n\nexport interface NetlifyMcpRelayOptions extends McpTransportOptions {\n /** The signed Next route, on the same deploy. Default {@link MCP_INTERNAL_PATH}. */\n internalPath?: string\n /** Error text for the 502 when the upstream fails or times out. */\n timeoutMessage?: string\n}\n\n/** The parts of Netlify's function `context` the relay reads. */\nexport interface NetlifyRelayContext {\n deploy?: { id?: string }\n site?: { name?: string; url?: string }\n}\n\ninterface ResolvedTransport {\n header: string\n label: string\n secret: McpTransportOptions['secret']\n}\n\n// ============================================\n// Signing\n// ============================================\n\nfunction resolveTransport(options: McpTransportOptions = {}): ResolvedTransport {\n const header = (options.header ?? DEFAULT_MCP_TRANSPORT_HEADER).toLowerCase()\n const label = options.label ?? DEFAULT_MCP_TRANSPORT_LABEL\n if (!/^[a-z0-9-]+$/.test(header)) {\n throw new TypeError(`@sonordev/site-kit/mcp/transport: invalid header name ${JSON.stringify(options.header)}`)\n }\n if (!label) throw new TypeError('@sonordev/site-kit/mcp/transport: label must not be empty')\n return { header, label, secret: options.secret }\n}\n\nfunction readSecret(secret: McpTransportOptions['secret']): string | undefined {\n if (typeof secret === 'function') return secret()\n if (secret !== undefined) return secret\n const netlify = (globalThis as { Netlify?: { env?: { get(name: string): string | undefined } } }).Netlify\n return netlify?.env?.get(MCP_TRANSPORT_SECRET_ENV) || process.env[MCP_TRANSPORT_SECRET_ENV]\n}\n\n/** The signature a relayed request carries: hex HMAC-SHA256 of `label` under `secret`. */\nexport function signMcpTransport(secret: string, label: string = DEFAULT_MCP_TRANSPORT_LABEL): string {\n return createHmac('sha256', secret).update(label).digest('hex')\n}\n\nfunction verify(request: Request, transport: ResolvedTransport): boolean {\n const key = readSecret(transport.secret)\n const supplied = request.headers.get(transport.header) || ''\n if (!key || !/^[a-f0-9]{64}$/.test(supplied)) return false\n const expected = createHmac('sha256', key).update(transport.label).digest()\n return timingSafeEqual(expected, Buffer.from(supplied, 'hex'))\n}\n\n/**\n * True when the request came through the rate-limited relay: its transport\n * header holds the HMAC of `label` under `MCP_TRANSPORT_SECRET`, compared in\n * constant time. False when the secret is unset.\n */\nexport function hasMcpTransportAuthorization(request: Request, options: McpTransportOptions = {}): boolean {\n return verify(request, resolveTransport(options))\n}\n\n// ============================================\n// Next.js routes\n// ============================================\n\nfunction isEnforced(enforce: ProtectMcpHandlersOptions['enforce']): boolean {\n if (typeof enforce === 'function') return enforce()\n if (enforce !== undefined) return enforce\n return process.env.NODE_ENV === 'production'\n}\n\n/**\n * Wrap the `/api/mcp` handlers so, in production, they answer only relayed\n * (signed) requests. The public path is the function's; this closes the Next\n * handler behind it so nothing can skip the rate limit.\n *\n * @example\n * ```ts\n * // app/api/mcp/route.ts\n * export const { POST, GET, DELETE, OPTIONS } = protectMcpHandlers(\n * createMcpHandler({ server: mcpServer, baseUrl: SITE_URL, allowedOrigins: '*' }),\n * )\n * ```\n */\nexport function protectMcpHandlers(\n handlers: McpRouteHandlerSet,\n options: ProtectMcpHandlersOptions = {},\n): McpRouteHandlerSet {\n const transport = resolveTransport(options)\n const protect =\n (handler: McpRouteHandler): McpRouteHandler =>\n (request) => {\n if (isEnforced(options.enforce) && !verify(request, transport)) {\n return Response.json({ error: 'Use the public MCP endpoint' }, { status: 403 })\n }\n return handler(request)\n }\n return {\n GET: protect(handlers.GET),\n POST: protect(handlers.POST),\n DELETE: protect(handlers.DELETE),\n OPTIONS: protect(handlers.OPTIONS),\n }\n}\n\n/**\n * Handlers for `/api/mcp-internal`, the relay's target. 403 without a valid\n * signature in every environment, then dispatch by method to the `/api/mcp`\n * handlers.\n *\n * @example\n * ```ts\n * // app/api/mcp-internal/route.ts\n * import * as mcp from '../mcp/route'\n * export const { POST, GET, DELETE, OPTIONS } = createMcpInternalRoute(mcp)\n * ```\n */\nexport function createMcpInternalRoute(\n handlers: McpRouteHandlerSet,\n options: McpTransportOptions = {},\n): McpRouteHandlerSet {\n const transport = resolveTransport(options)\n const relay: McpRouteHandler = (request) => {\n if (!verify(request, transport)) return Response.json({ error: 'Forbidden' }, { status: 403 })\n switch (request.method) {\n case 'GET':\n return handlers.GET(request)\n case 'POST':\n return handlers.POST(request)\n case 'DELETE':\n return handlers.DELETE(request)\n case 'OPTIONS':\n return handlers.OPTIONS(request)\n default:\n return new Response(null, { status: 405 })\n }\n }\n return { GET: relay, POST: relay, DELETE: relay, OPTIONS: relay }\n}\n\n// ============================================\n// Netlify function\n// ============================================\n\n/**\n * The default export for `netlify/functions/mcp.mjs`. Signs the request and\n * relays it to {@link MCP_INTERNAL_PATH} on the same deploy.\n *\n * - 503 when `MCP_TRANSPORT_SECRET` is unset; 405 for methods MCP doesn't use.\n * - Strips `host` and `content-length`, overwrites any client-sent transport\n * header with the real signature.\n * - POST bodies over 64 KB get a 413 and are never forwarded.\n * - `redirect: 'error'` and a 55 s timeout; any upstream failure is a 502.\n * - Responses drop the encoding/length headers fetch already undid, and gain\n * `Cache-Control: no-store` plus the transport header set to\n * {@link MCP_RELAY_MARKER}.\n *\n * The function file must still declare its own literal `config`: Netlify\n * reads `path` and `rateLimit` statically from that file, so they can't come\n * from an import.\n *\n * @example\n * ```js\n * // netlify/functions/mcp.mjs\n * import { createNetlifyMcpRelay } from '@sonordev/site-kit/mcp/transport'\n *\n * export default createNetlifyMcpRelay()\n *\n * export const config = {\n * path: '/api/mcp',\n * rateLimit: { windowSize: 60, windowLimit: 60, aggregateBy: ['ip', 'domain'] },\n * }\n * ```\n */\nexport function createNetlifyMcpRelay(\n options: NetlifyMcpRelayOptions = {},\n): (request: Request, context: NetlifyRelayContext) => Promise<Response> {\n const transport = resolveTransport(options)\n const internalPath = options.internalPath ?? MCP_INTERNAL_PATH\n const timeoutMessage = options.timeoutMessage ?? 'MCP request timed out. Try again.'\n const unavailable = () => Response.json({ error: 'MCP is temporarily unavailable' }, { status: 503 })\n\n return async function relayMcp(request, context) {\n const key = readSecret(transport.secret)\n if (!key) return unavailable()\n if (!(MCP_RELAY_METHODS as readonly string[]).includes(request.method)) return new Response(null, { status: 405 })\n\n // context is platform-provided, never a client-controlled forwarding URL.\n // The deploy permalink keeps the relay on the deploy that took the call.\n const base =\n context?.deploy?.id && context.site?.name\n ? `https://${context.deploy.id}--${context.site.name}.netlify.app`\n : context?.site?.url\n if (!base) return unavailable()\n const target = new URL(internalPath, base)\n\n const headers = new Headers(request.headers)\n headers.delete('host')\n headers.delete('content-length')\n // set, not append: overwrite anything a client sent under this name.\n headers.set(transport.header, signMcpTransport(key, transport.label))\n\n let body: Uint8Array<ArrayBuffer> | undefined\n if (request.method === 'POST') {\n const bytes = await readCappedBody(request, MCP_RELAY_MAX_BODY_BYTES)\n if (!bytes) return Response.json({ error: 'Tool arguments are too large' }, { status: 413 })\n body = bytes\n }\n\n try {\n const response = await fetch(target, {\n method: request.method,\n headers,\n body,\n redirect: 'error',\n signal: AbortSignal.timeout(MCP_RELAY_TIMEOUT_MS),\n })\n const outgoing = new Headers(response.headers)\n // fetch decodes the upstream body; its compression/length headers no longer apply.\n for (const name of ['content-encoding', 'content-length', 'transfer-encoding']) outgoing.delete(name)\n outgoing.set('Cache-Control', 'no-store')\n outgoing.set(transport.header, MCP_RELAY_MARKER)\n return new Response(response.body, { status: response.status, headers: outgoing })\n } catch {\n return Response.json({ error: timeoutMessage }, { status: 502 })\n }\n }\n}\n"]}
|
|
@@ -15,7 +15,13 @@
|
|
|
15
15
|
* KPIs repeated across hero, results cards, and a Performance section. One
|
|
16
16
|
* differentiator repeated a dozen times stops differentiating — collapse the
|
|
17
17
|
* per-category Lighthouse tiles to at most one summary, business outcomes first.
|
|
18
|
+
*
|
|
19
|
+
* It is also the single source of truth for METRIC PROVENANCE (below): the
|
|
20
|
+
* three sources a portfolio number can have, what a client-reported number
|
|
21
|
+
* must carry, and which numbers may headline. sonor-api and signal-api keep
|
|
22
|
+
* byte-identical mirrors of those functions too, guarded by the same vectors.
|
|
18
23
|
*/
|
|
24
|
+
export { FRAME_READY_MESSAGE, isFrameReadyMessage, type FrameReadyMessage } from '../shared/frame';
|
|
19
25
|
/** Bump on breaking changes to the collapse policy. */
|
|
20
26
|
export declare const PORTFOLIO_CONTRACT_VERSION: 1;
|
|
21
27
|
/** Minimal KPI shape the policy reads — callers pass their own richer tiles. */
|
|
@@ -54,7 +60,94 @@ export declare const PORTFOLIO_KPI_JSON_SCHEMA: {
|
|
|
54
60
|
readonly description: {
|
|
55
61
|
readonly type: 'string';
|
|
56
62
|
};
|
|
63
|
+
readonly source: {
|
|
64
|
+
readonly enum: readonly ['measured', 'reported', 'estimated'];
|
|
65
|
+
};
|
|
66
|
+
readonly reportedBy: {
|
|
67
|
+
readonly type: 'object';
|
|
68
|
+
readonly required: readonly ['name', 'organization', 'date'];
|
|
69
|
+
readonly properties: {
|
|
70
|
+
readonly name: {
|
|
71
|
+
readonly type: 'string';
|
|
72
|
+
readonly minLength: 1;
|
|
73
|
+
};
|
|
74
|
+
readonly role: {
|
|
75
|
+
readonly type: 'string';
|
|
76
|
+
};
|
|
77
|
+
readonly organization: {
|
|
78
|
+
readonly type: 'string';
|
|
79
|
+
readonly minLength: 1;
|
|
80
|
+
};
|
|
81
|
+
readonly date: {
|
|
82
|
+
readonly type: 'string';
|
|
83
|
+
readonly pattern: '^\\d{4}-\\d{2}-\\d{2}$';
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
readonly if: {
|
|
89
|
+
readonly properties: {
|
|
90
|
+
readonly source: {
|
|
91
|
+
readonly const: 'reported';
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
readonly required: readonly ['source'];
|
|
95
|
+
};
|
|
96
|
+
readonly then: {
|
|
97
|
+
readonly required: readonly ['reportedBy'];
|
|
57
98
|
};
|
|
58
99
|
readonly additionalProperties: true;
|
|
59
100
|
};
|
|
101
|
+
/** Every source a portfolio number can have. */
|
|
102
|
+
export declare const METRIC_SOURCES: readonly ['measured', 'reported', 'estimated'];
|
|
103
|
+
export type MetricSource = (typeof METRIC_SOURCES)[number];
|
|
104
|
+
/** Who stands behind a client-reported number. */
|
|
105
|
+
export interface MetricAttribution {
|
|
106
|
+
/** The person, e.g. "Scott Mann". */
|
|
107
|
+
name: string;
|
|
108
|
+
/** Their title, e.g. "Director of Business Development". */
|
|
109
|
+
role?: string;
|
|
110
|
+
/** Where they work: almost always the client, e.g. "True Power Systems". */
|
|
111
|
+
organization: string;
|
|
112
|
+
/** The day they reported it, YYYY-MM-DD. */
|
|
113
|
+
date: string;
|
|
114
|
+
}
|
|
115
|
+
/** The provenance fields a portfolio number (a KPI tile, a results card) carries. */
|
|
116
|
+
export interface SourcedMetric {
|
|
117
|
+
source?: string | null;
|
|
118
|
+
reportedBy?: unknown;
|
|
119
|
+
}
|
|
120
|
+
export declare function isMetricSource(value: unknown): value is MetricSource;
|
|
121
|
+
/**
|
|
122
|
+
* What's wrong with a number's provenance, or null when nothing is. A missing
|
|
123
|
+
* source is legacy data and passes (displays show it unlabelled, and it can't
|
|
124
|
+
* headline); an unknown source, or a reported number nobody is named behind,
|
|
125
|
+
* does not.
|
|
126
|
+
*/
|
|
127
|
+
export declare function metricSourceProblem(metric: SourcedMetric): string | null;
|
|
128
|
+
/**
|
|
129
|
+
* May this number lead: a hero tile, a gallery card, a carousel, a social
|
|
130
|
+
* post? Measured, or reported with someone named behind it. Never an
|
|
131
|
+
* estimate, and never an unlabelled legacy number.
|
|
132
|
+
*/
|
|
133
|
+
export declare function isHeadlineMetric(metric: SourcedMetric): boolean;
|
|
134
|
+
/** The badge a display shows, or null for a missing or unknown source (show nothing rather than guess). */
|
|
135
|
+
export declare function metricSourceLabel(source: unknown): string | null;
|
|
136
|
+
/**
|
|
137
|
+
* The full credit: "Reported by Scott Mann, Director of Business Development,
|
|
138
|
+
* True Power Systems, September 25, 2026". The date is read as a plain day,
|
|
139
|
+
* never through a time zone, so it can't slip to the day before.
|
|
140
|
+
*/
|
|
141
|
+
export declare function formatAttribution(by: MetricAttribution): string;
|
|
142
|
+
/** The credit for a tight space (a card, a carousel tile): "per True Power Systems". */
|
|
143
|
+
export declare function shortAttribution(by: Pick<MetricAttribution, 'organization'>): string;
|
|
144
|
+
/**
|
|
145
|
+
* Carry a person's reported numbers through a regeneration. The generator can
|
|
146
|
+
* only write measured or estimated numbers (it can't see a client's books, and
|
|
147
|
+
* any 'reported' tag it writes is dropped here), so without this a rewrite of
|
|
148
|
+
* the hero or the results would silently erase what a client told us.
|
|
149
|
+
* Reported entries from `previous` lead, in their order; `next` fills in
|
|
150
|
+
* behind them, minus anything sharing a key with a reported entry.
|
|
151
|
+
*/
|
|
152
|
+
export declare function carryReportedMetrics<T extends SourcedMetric>(previous: readonly T[] | null | undefined, next: readonly T[] | null | undefined, keyOf: (metric: T) => string): T[];
|
|
60
153
|
//# sourceMappingURL=contract.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../../src/portfolio/contract.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../../src/portfolio/contract.ts"],"names":[],"mappings":"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"}
|
|
@@ -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"]}
|
package/dist/search/index.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
var chunkCLO3K7PF_js = require('../chunk-CLO3K7PF.js');
|
|
5
|
-
var
|
|
6
|
-
require('../chunk-
|
|
7
|
-
require('../chunk-
|
|
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
|
|
42
|
+
const res = await chunkIO65ROAO_js.sonorFetch(
|
|
43
43
|
`${apiUrl}${chunkCLO3K7PF_js.SEARCH_ENDPOINT}`,
|
|
44
44
|
{
|
|
45
45
|
method: "POST",
|
package/dist/search/index.mjs
CHANGED
|
@@ -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-
|
|
5
|
-
import '../chunk-
|
|
6
|
-
import '../chunk-
|
|
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';
|
package/dist/seo/client.js
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
var
|
|
4
|
+
var chunkSUGBIEYQ_js = require('../chunk-SUGBIEYQ.js');
|
|
5
5
|
require('../chunk-JONWE26J.js');
|
|
6
|
-
require('../chunk-
|
|
7
|
-
require('../chunk-
|
|
8
|
-
require('../chunk-
|
|
9
|
-
require('../chunk-
|
|
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
|
|
17
|
+
get: function () { return chunkSUGBIEYQ_js.SitemapSync; }
|
|
17
18
|
});
|
|
18
19
|
//# sourceMappingURL=client.js.map
|
|
19
20
|
//# sourceMappingURL=client.js.map
|
package/dist/seo/client.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
export { SitemapSync } from '../chunk-
|
|
2
|
+
export { SitemapSync } from '../chunk-SVCKJ5KH.mjs';
|
|
3
3
|
import '../chunk-C2CTLLIO.mjs';
|
|
4
|
-
import '../chunk-
|
|
5
|
-
import '../chunk-
|
|
6
|
-
import '../chunk-
|
|
7
|
-
import '../chunk-
|
|
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
|
|
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-
|
|
17
|
-
require('../chunk-
|
|
18
|
-
require('../chunk-
|
|
19
|
-
require('../chunk-
|
|
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
|
|
710
|
+
get: function () { return chunkSUGBIEYQ_js.SitemapSync; }
|
|
710
711
|
});
|
|
711
712
|
Object.defineProperty(exports, "generateSitemap", {
|
|
712
713
|
enumerable: true,
|