@sonordev/site-kit 8.6.0 → 8.8.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 (161) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/agent-manifest.json +1 -1
  3. package/dist/{AnalyticsProvider-GVJYL6XY.js → AnalyticsProvider-7SBJ27SI.js} +6 -6
  4. package/dist/{ArticleViewTracker-AOJXEBYM.js → ArticleViewTracker-PV2RD3P6.js} +5 -5
  5. package/dist/{BlocksPopup-AJ3CKVQG.js → BlocksPopup-NEEFV2L3.js} +9 -8
  6. package/dist/{ChatWidget-EYBVB5SZ.js → ChatWidget-V46UBDOM.js} +5 -5
  7. package/dist/{FileField-UEQYIFRO.js → FileField-MJ3RCNFJ.js} +3 -3
  8. package/dist/{FormSpotlight-H73ZZ66T.js → FormSpotlight-NQPQFZS7.js} +1 -1
  9. package/dist/{FormStage-2PLWWYBW.js → FormStage-4T5PJR5K.js} +1 -1
  10. package/dist/{ManagedForm-ZQOFXEWS.js → ManagedForm-DDDVMKLJ.js} +6 -6
  11. package/dist/{ManagedNewsletterForm-5TE47YZD.js → ManagedNewsletterForm-TUYGN6O3.js} +9 -9
  12. package/dist/{SignalCore-FEFNMNYN.js → SignalCore-OW3YIH6M.js} +5 -5
  13. package/dist/SiteChat-XMKHBSEK.js +6 -0
  14. package/dist/SiteDesignReporter-BRH7ORQS.js +13 -0
  15. package/dist/{SitePopups-C44YTQ3Q.js → SitePopups-VU2C3Z6B.js} +6 -6
  16. package/dist/SitemapSync-C4H3HF3L.js +9 -0
  17. package/dist/_client/booking-widget.js +5 -5
  18. package/dist/admin-auth/index.d.ts +2 -2
  19. package/dist/admin-auth/session.d.ts +2 -2
  20. package/dist/affiliates/index.js +3 -3
  21. package/dist/analytics/content-text.d.ts +17 -0
  22. package/dist/analytics/index.js +6 -6
  23. package/dist/{article-styles-VASFBYA3.js → article-styles-D7VCDXMG.js} +2 -1
  24. package/dist/article-tables-W45BE353.js +3 -0
  25. package/dist/articles/index.js +7 -20
  26. package/dist/articles/server-ui.js +29 -55
  27. package/dist/articles/server.js +6 -5
  28. package/dist/chat/index.js +8 -8
  29. package/dist/{chunk-3RDJKIYO.js → chunk-2WDWQI7K.js} +1 -1
  30. package/dist/{chunk-KTMHRXDT.js → chunk-2WM4TNDF.js} +1 -1
  31. package/dist/chunk-3MJNBOUO.js +8 -0
  32. package/dist/{chunk-BF7TZYC3.js → chunk-52KO4LKD.js} +1 -1
  33. package/dist/chunk-64XODP7M.js +7 -0
  34. package/dist/{chunk-DSD3XZQQ.js → chunk-6BFQI3ZE.js} +3 -3
  35. package/dist/{chunk-QNOJSSQT.js → chunk-6DXQHNZK.js} +1 -1
  36. package/dist/{chunk-5D7ZISUP.js → chunk-7DYMM6SS.js} +11 -6
  37. package/dist/{chunk-QQT2T6JQ.js → chunk-7HW4TZB5.js} +1 -1
  38. package/dist/{chunk-E4PTMORJ.js → chunk-AL5UCWLI.js} +23 -8
  39. package/dist/chunk-AYQEIMGS.js +390 -0
  40. package/dist/{chunk-L6QTERSH.js → chunk-DYROJGI2.js} +1 -1
  41. package/dist/{chunk-XA7Q2VQA.js → chunk-EK7P62DT.js} +1 -1
  42. package/dist/{chunk-BUE3Z32O.js → chunk-F6WLHVMQ.js} +1 -1
  43. package/dist/{chunk-IYCSDFPN.js → chunk-FLOJUMFR.js} +2 -2
  44. package/dist/{chunk-7R2STOVD.js → chunk-G7BRCSV6.js} +3 -3
  45. package/dist/{chunk-NI6ZTP3D.js → chunk-GQN4BGK6.js} +1 -1
  46. package/dist/{chunk-GFY3E7EQ.js → chunk-GXWM5KNS.js} +15 -7
  47. package/dist/{chunk-U34O6I7W.js → chunk-HN7GVGWC.js} +3 -3
  48. package/dist/{chunk-6KU7STM3.js → chunk-HY7ZEU2C.js} +3 -1
  49. package/dist/chunk-IBHBEUZV.js +27 -0
  50. package/dist/{chunk-ZZSAWXWD.js → chunk-IDC5CVBR.js} +1 -1
  51. package/dist/{chunk-HMTPRBYJ.js → chunk-IWNJCRXW.js} +1 -1
  52. package/dist/chunk-JESLPEST.js +14 -0
  53. package/dist/{chunk-BC7JMY4Z.js → chunk-JG53YJLM.js} +58 -1
  54. package/dist/{chunk-RI2N2EBI.js → chunk-JYITKUUB.js} +13 -5
  55. package/dist/{chunk-V67OD7RW.js → chunk-KG5IEV7I.js} +113 -93
  56. package/dist/chunk-KIYACQW3.js +1 -0
  57. package/dist/{chunk-JSW42BHL.js → chunk-M5JIHQ42.js} +1 -1
  58. package/dist/{chunk-YJH6WE76.js → chunk-NFGAXWCN.js} +9 -9
  59. package/dist/{chunk-Y35BWW2C.js → chunk-ODOV76VW.js} +1 -1
  60. package/dist/{chunk-VA6IJ55K.js → chunk-OPUY4RS6.js} +1 -1
  61. package/dist/{chunk-6NXFA7HE.js → chunk-OWZWRP7Y.js} +2 -2
  62. package/dist/{chunk-JJ4JS3LD.js → chunk-PAYCK7FC.js} +1 -1
  63. package/dist/{chunk-72F44HKL.js → chunk-PSS6M4CG.js} +1 -1
  64. package/dist/{chunk-O3Z5HHFP.js → chunk-SBPUFTMP.js} +2 -2
  65. package/dist/{chunk-OFSV5TZY.js → chunk-SHAUX2MQ.js} +1 -1
  66. package/dist/{chunk-OWO3RFEX.js → chunk-SLFYTELD.js} +1 -1
  67. package/dist/{chunk-CMUD37WV.js → chunk-SVHPILS3.js} +1 -1
  68. package/dist/{chunk-YNBRVVTX.js → chunk-THXELZTQ.js} +1 -1
  69. package/dist/{chunk-IGAIQ2B3.js → chunk-TP32MRXV.js} +2 -2
  70. package/dist/{chunk-NUXXFCIT.js → chunk-URLTBVRX.js} +4 -4
  71. package/dist/chunk-XAZC2TUD.js +1119 -0
  72. package/dist/{chunk-URJN75ZG.js → chunk-XIDWNXZN.js} +3 -3
  73. package/dist/chunk-XLAHRN4B.js +1 -0
  74. package/dist/chunk-XS2ORMUG.js +32 -0
  75. package/dist/{chunk-6AWIUO2Z.js → chunk-Y6SHFILE.js} +1 -1
  76. package/dist/{chunk-FTSEP7Q3.js → chunk-YEX5RAEU.js} +3 -3
  77. package/dist/{chunk-AJG74ZZV.js → chunk-YRQS7VAH.js} +1 -1
  78. package/dist/{chunk-DLM4FFNH.js → chunk-ZQSCMYZO.js} +1 -1
  79. package/dist/client/index.js +4 -4
  80. package/dist/contracts/campaign-normalize.d.ts +2 -0
  81. package/dist/contracts/entries.d.ts +1 -1
  82. package/dist/contracts/frame-ready.d.ts +23 -0
  83. package/dist/contracts/funnels.d.ts +576 -0
  84. package/dist/contracts/inbox.d.ts +42 -0
  85. package/dist/contracts/llms.d.ts +200 -0
  86. package/dist/contracts/portfolio.d.ts +361 -8
  87. package/dist/contracts/publication-branding.d.ts +10 -2
  88. package/dist/contracts/site-cache.d.ts +2 -0
  89. package/dist/fleet/index.js +6 -6
  90. package/dist/forms/index.js +9 -9
  91. package/dist/forms/server.js +2 -2
  92. package/dist/images/index.js +4 -4
  93. package/dist/index.js +1 -1
  94. package/dist/landing/contract.d.ts +2 -13
  95. package/dist/landing/contract.js +2 -0
  96. package/dist/landing/index.js +4 -2
  97. package/dist/landing/managed-server.d.ts +21 -0
  98. package/dist/landing/managed.d.ts +11 -0
  99. package/dist/landing/metadata.js +2 -0
  100. package/dist/landing/server.d.ts +5 -0
  101. package/dist/landing/server.js +20 -2
  102. package/dist/layout/client.js +9 -9
  103. package/dist/layout/index.js +10 -10
  104. package/dist/llms/contract.js +2 -1
  105. package/dist/llms/index.js +9 -7
  106. package/dist/llms/merge.d.ts +29 -0
  107. package/dist/maps/index.js +3 -3
  108. package/dist/mcp/sonor.js +9 -8
  109. package/dist/og/pages.d.ts +3 -3
  110. package/dist/portfolio/contract.js +705 -1
  111. package/dist/press-kit/articles/article-body.d.ts +12 -0
  112. package/dist/press-kit/articles/article-heading-ids.d.ts +7 -0
  113. package/dist/press-kit/articles/article-headings.d.ts +61 -0
  114. package/dist/press-kit/articles/html-tags.d.ts +46 -0
  115. package/dist/press-kit/articles/types.d.ts +2 -0
  116. package/dist/revalidate/index.js +3 -3
  117. package/dist/runtime/index.js +3 -3
  118. package/dist/seo/client.js +6 -6
  119. package/dist/seo/index.js +14 -13
  120. package/dist/seo/llms/contract.js +2 -1
  121. package/dist/seo/llms.js +9 -7
  122. package/dist/seo/meta/contract.js +2 -1
  123. package/dist/seo/register-sitemap-cli.js +1 -1
  124. package/dist/seo/server.js +5 -4
  125. package/dist/seo/sitemap.js +7 -5
  126. package/dist/seo-meta/contract.js +2 -1
  127. package/dist/server/index.js +3 -3
  128. package/dist/{server-api-EGOKC7Q3.js → server-api-XIKDWSNO.js} +2 -2
  129. package/dist/shared/build-entries.d.ts +3 -1
  130. package/dist/shared/frame.d.ts +1 -1
  131. package/dist/shared/markdown.d.ts +7 -6
  132. package/dist/shared/source-graph.d.ts +5 -5
  133. package/dist/shared/version.d.ts +1 -1
  134. package/dist/signal/index.js +2 -2
  135. package/dist/sitemap/index.js +7 -5
  136. package/dist/sitemap/shared.d.ts +1 -2
  137. package/dist/slots/index.js +2 -2
  138. package/dist/sync/index.js +5 -5
  139. package/dist/website/contract.js +2 -1
  140. package/dist/website/images.js +4 -4
  141. package/dist/website/index.js +10 -9
  142. package/dist/website/landing/server.js +20 -2
  143. package/dist/website/landing.js +4 -2
  144. package/dist/website/popups.js +9 -8
  145. package/dist/website/slots.js +2 -2
  146. package/dist/{writeLLMsTxt-KHVHNSDM.js → writeLLMsTxt-4HHUAOA6.js} +6 -4
  147. package/docs.json +5 -0
  148. package/package.json +1 -1
  149. package/src/analytics/README.md +1 -1
  150. package/src/landing/README.md +64 -0
  151. package/src/llms/README.md +52 -20
  152. package/dist/SiteChat-3MKYL2XM.js +0 -6
  153. package/dist/SiteDesignReporter-AJDIBNHI.js +0 -12
  154. package/dist/SitemapSync-UJA6JVPW.js +0 -9
  155. package/dist/article-tables-6XHVULPU.js +0 -2
  156. package/dist/chunk-EKBEOXTH.js +0 -114
  157. package/dist/chunk-NZ4RXN3G.js +0 -145
  158. package/dist/chunk-OSYCOC3K.js +0 -21
  159. /package/dist/{chunk-7ZWO5HDQ.js → chunk-BTHEQDN4.js} +0 -0
  160. /package/dist/{chunk-BQR4IPBW.js → chunk-ISRBSTB7.js} +0 -0
  161. /package/dist/{chunk-IHG36STL.js → chunk-XHYE24HU.js} +0 -0
@@ -1,2 +1,706 @@
1
- export { FRAME_READY_MESSAGE, METRIC_SOURCES, PORTFOLIO_CONTRACT_VERSION, PORTFOLIO_KPI_JSON_SCHEMA, carryReportedMetrics, curateHeroKpis, formatAttribution, isFrameReadyMessage, isHeadlineMetric, isLighthouseKpi, isMetricSource, metricSourceLabel, metricSourceProblem, shortAttribution } from '../chunk-EKBEOXTH.js';
1
+ import { isBlockedPublicUrlHost } from '../chunk-XS2ORMUG.js';
2
+ export { FRAME_READY_MESSAGE, isFrameReadyMessage } from '../chunk-64XODP7M.js';
2
3
  import '../chunk-PKBMQBKP.js';
4
+
5
+ // src/contracts/portfolio.ts
6
+ var PORTFOLIO_CONTRACT_VERSION = 1;
7
+ var CASE_STUDY_STAGES = ["problem", "build", "proof", "outcome"];
8
+ var CASE_STUDY_STAGE_COPY = {
9
+ problem: { name: "The problem", question: "What did the client come in with, and what was in the way?" },
10
+ build: { name: "The build", question: "What did we plan and make, and out of what?" },
11
+ proof: { name: "The proof", question: "How does the finished build hold up when measured?" },
12
+ outcome: { name: "The outcome", question: "What is the work doing for the client now?" }
13
+ };
14
+ var STAGE_NAME_MAX_CHARS = 24;
15
+ function stageNamesProblems(names) {
16
+ if (names === void 0 || names === null) return [];
17
+ if (typeof names !== "object" || Array.isArray(names)) return ["stageNames must be an object"];
18
+ const problems = [];
19
+ for (const [stage, name] of Object.entries(names)) {
20
+ if (!CASE_STUDY_STAGES.includes(stage)) problems.push(`stageNames.${stage} is not a stage`);
21
+ else if (name === void 0 || name === null) continue;
22
+ else if (typeof name !== "string") problems.push(`stageNames.${stage} must be text`);
23
+ else if (name.trim().length > STAGE_NAME_MAX_CHARS)
24
+ problems.push(`stageNames.${stage} is over ${STAGE_NAME_MAX_CHARS} characters`);
25
+ }
26
+ return problems;
27
+ }
28
+ function stageName(names, stage) {
29
+ const own = names?.[stage]?.trim();
30
+ return own || CASE_STUDY_STAGE_COPY[stage].name;
31
+ }
32
+ var PORTFOLIO_SECTION_TYPES = [
33
+ "portfolioHero",
34
+ // problem
35
+ "portfolioChallenges",
36
+ "portfolioBeforeAfter",
37
+ // build
38
+ "portfolioStrategy",
39
+ "portfolioSiteArchitecture",
40
+ "portfolioDesignSystem",
41
+ "portfolioFeatureSpotlight",
42
+ "portfolioGallery",
43
+ "portfolioVideo",
44
+ "portfolioTechStack",
45
+ // proof
46
+ "portfolioPerformance",
47
+ "portfolioSpeedComparison",
48
+ "portfolioVerification",
49
+ // outcome
50
+ "portfolioResults",
51
+ "portfolioMetricsTimeline",
52
+ "portfolioConversionFunnel",
53
+ "portfolioServices",
54
+ "portfolioTeam",
55
+ "portfolioTestimonial",
56
+ "portfolioDetails",
57
+ // outside the story
58
+ "portfolioSeo",
59
+ "portfolioCTA"
60
+ ];
61
+ function isPortfolioSectionType(value) {
62
+ return typeof value === "string" && PORTFOLIO_SECTION_TYPES.includes(value);
63
+ }
64
+ var SECTION_STAGE = {
65
+ portfolioHero: null,
66
+ portfolioChallenges: "problem",
67
+ portfolioBeforeAfter: "problem",
68
+ portfolioStrategy: "build",
69
+ portfolioSiteArchitecture: "build",
70
+ portfolioDesignSystem: "build",
71
+ portfolioFeatureSpotlight: "build",
72
+ portfolioGallery: "build",
73
+ portfolioVideo: "build",
74
+ portfolioTechStack: "build",
75
+ portfolioPerformance: "proof",
76
+ portfolioSpeedComparison: "proof",
77
+ portfolioVerification: "proof",
78
+ portfolioResults: "outcome",
79
+ portfolioMetricsTimeline: "outcome",
80
+ portfolioConversionFunnel: "outcome",
81
+ portfolioServices: "outcome",
82
+ portfolioTeam: "outcome",
83
+ portfolioTestimonial: "outcome",
84
+ portfolioDetails: "outcome",
85
+ portfolioSeo: null,
86
+ portfolioCTA: null
87
+ };
88
+ function sectionStage(type) {
89
+ return isPortfolioSectionType(type) ? SECTION_STAGE[type] : null;
90
+ }
91
+ var STAGE_CORE_SECTION = {
92
+ problem: "portfolioChallenges",
93
+ build: "portfolioStrategy",
94
+ proof: "portfolioPerformance",
95
+ outcome: "portfolioResults"
96
+ };
97
+ var STAGE_CORE_ALTERNATES = {
98
+ proof: ["portfolioVerification"]
99
+ };
100
+ var STAND_IN_HAS_CONTENT = {
101
+ portfolioVerification: (data) => readVerificationChecks(data).length > 0
102
+ };
103
+ var STAND_IN_EMPTY_COPY = {
104
+ portfolioVerification: "The verification is there, but none of its checks can be shown yet. Add a check, and say what it doesn't cover."
105
+ };
106
+ function stageCoreSections(stage) {
107
+ return [STAGE_CORE_SECTION[stage], ...STAGE_CORE_ALTERNATES[stage] ?? []];
108
+ }
109
+ var PORTFOLIO_SECTION_ORDER = Object.fromEntries(
110
+ PORTFOLIO_SECTION_TYPES.map((type, i) => [type, i])
111
+ );
112
+ var UNKNOWN_SECTION_ORDER = PORTFOLIO_SECTION_TYPES.length;
113
+ function sectionOrder(type) {
114
+ return isPortfolioSectionType(type) ? PORTFOLIO_SECTION_ORDER[type] : UNKNOWN_SECTION_ORDER;
115
+ }
116
+ function stageOrder(type) {
117
+ if (type === "portfolioHero") return -1;
118
+ const stage = sectionStage(type);
119
+ return stage ? CASE_STUDY_STAGES.indexOf(stage) : CASE_STUDY_STAGES.length;
120
+ }
121
+ function orderSections(sections, options = {}) {
122
+ const list = [...sections ?? []];
123
+ const byStage = list.map((section, index) => ({ section, index }));
124
+ byStage.sort((a, b) => {
125
+ const stage = stageOrder(a.section.sectionType) - stageOrder(b.section.sectionType);
126
+ if (stage !== 0) return stage;
127
+ if (options.sectionsOrdered) return a.index - b.index;
128
+ const order = sectionOrder(a.section.sectionType) - sectionOrder(b.section.sectionType);
129
+ return order !== 0 ? order : a.index - b.index;
130
+ });
131
+ return byStage.map((s) => s.section);
132
+ }
133
+ function groupSectionsByStage(sections, options = {}) {
134
+ const ordered = orderSections(sections, options);
135
+ const groups = [];
136
+ for (const stage of CASE_STUDY_STAGES) {
137
+ const own = ordered.filter((s) => sectionStage(s.sectionType) === stage);
138
+ if (own.length) groups.push({ stage, index: groups.length + 1, sections: own });
139
+ }
140
+ return groups;
141
+ }
142
+ var STAGE_MISSING_COPY = {
143
+ problem: {
144
+ empty: "Nothing says what the client came in with. Add the challenges.",
145
+ noCore: "The problem is pictured but not stated. Add the challenges."
146
+ },
147
+ build: {
148
+ empty: "Nothing says what was planned or built. Add the strategy.",
149
+ noCore: "The build is shown but the plan behind it is missing. Add the strategy."
150
+ },
151
+ proof: {
152
+ empty: "Nothing measures the finished build. Add a performance measurement, or a verification when the screens are behind a sign-in.",
153
+ noCore: "The old site is compared but the finished build has no measurement of its own. Add a performance measurement."
154
+ },
155
+ outcome: {
156
+ empty: "Nothing says what the work is doing for the client. Add the results.",
157
+ noCore: "The outcome has supporting sections but no results. Add the results."
158
+ }
159
+ };
160
+ function caseStudyStageStatus(sections) {
161
+ const ordered = orderSections(sections);
162
+ return CASE_STUDY_STAGES.map((stage) => {
163
+ const own = ordered.filter(
164
+ (s) => isPortfolioSectionType(s.sectionType) && SECTION_STAGE[s.sectionType] === stage
165
+ );
166
+ const present = own.map((s) => s.sectionType);
167
+ const standIns = STAGE_CORE_ALTERNATES[stage] ?? [];
168
+ const emptyStandIns = own.filter(
169
+ (s) => standIns.includes(s.sectionType) && STAND_IN_HAS_CONTENT[s.sectionType]?.(s.data) === false
170
+ );
171
+ const complete = own.some(
172
+ (s) => s.sectionType === STAGE_CORE_SECTION[stage] || standIns.includes(s.sectionType) && !emptyStandIns.includes(s)
173
+ );
174
+ const copy = STAGE_MISSING_COPY[stage];
175
+ const missing = complete ? null : (emptyStandIns.length ? STAND_IN_EMPTY_COPY[emptyStandIns[0].sectionType] : void 0) ?? (present.length ? copy.noCore : copy.empty);
176
+ return { stage, present, complete, missing };
177
+ });
178
+ }
179
+ var NARRATIVE_MAX_CHARS = 320;
180
+ function narrativeProblems(narrative) {
181
+ if (narrative === void 0 || narrative === null) return [];
182
+ if (typeof narrative !== "object") return ["narrative must be an object"];
183
+ const problems = [];
184
+ const n = narrative;
185
+ if (typeof n.thesis !== "string" || !n.thesis.trim()) problems.push("narrative.thesis is required");
186
+ else if (n.thesis.length > NARRATIVE_MAX_CHARS) problems.push(`narrative.thesis is over ${NARRATIVE_MAX_CHARS} characters`);
187
+ if (n.stages === void 0 || n.stages === null) return problems;
188
+ if (typeof n.stages !== "object") return [...problems, "narrative.stages must be an object"];
189
+ for (const [stage, lede] of Object.entries(n.stages)) {
190
+ if (!CASE_STUDY_STAGES.includes(stage)) {
191
+ problems.push(`narrative.stages.${stage} is not a stage (${CASE_STUDY_STAGES.join(", ")})`);
192
+ continue;
193
+ }
194
+ if (lede === void 0 || lede === null) continue;
195
+ if (typeof lede !== "string") problems.push(`narrative.stages.${stage} must be text`);
196
+ else if (lede.length > NARRATIVE_MAX_CHARS) problems.push(`narrative.stages.${stage} is over ${NARRATIVE_MAX_CHARS} characters`);
197
+ }
198
+ return problems;
199
+ }
200
+ var CASE_STUDY_NARRATIVE_JSON_SCHEMA = {
201
+ $schema: "http://json-schema.org/draft-07/schema#",
202
+ $id: "https://sonor.io/schemas/case-study-narrative.v1.json",
203
+ title: "CaseStudyNarrative",
204
+ description: "The thesis of a case study and the lede that opens each of its four stages.",
205
+ type: "object",
206
+ required: ["thesis"],
207
+ properties: {
208
+ thesis: { type: "string", minLength: 1, maxLength: NARRATIVE_MAX_CHARS },
209
+ stages: {
210
+ type: "object",
211
+ properties: Object.fromEntries(
212
+ CASE_STUDY_STAGES.map((stage) => [stage, { type: "string", maxLength: NARRATIVE_MAX_CHARS }])
213
+ ),
214
+ additionalProperties: false
215
+ }
216
+ },
217
+ additionalProperties: false
218
+ };
219
+ function challengeId(position) {
220
+ return `c${position}`;
221
+ }
222
+ function caseStudyFlowProblems(input) {
223
+ const challenges = input.challenges ?? [];
224
+ const ids = challenges.map((c) => typeof c.id === "string" && c.id.trim() ? c.id.trim() : null);
225
+ if (!ids.some(Boolean)) return [];
226
+ const problems = [];
227
+ const known = new Set(ids.filter((id) => !!id));
228
+ if (known.size !== ids.filter(Boolean).length) problems.push("two challenges share an id");
229
+ ids.forEach((id, i) => {
230
+ if (!id) problems.push(`challenge ${i + 1} has no id while the others do`);
231
+ });
232
+ const resolved = /* @__PURE__ */ new Set();
233
+ const proven = /* @__PURE__ */ new Set();
234
+ for (const phase of input.phases ?? []) {
235
+ for (const ref of phase.resolves ?? []) {
236
+ if (!known.has(ref)) problems.push(`a strategy phase resolves "${ref}", which is not a challenge`);
237
+ else resolved.add(ref);
238
+ }
239
+ }
240
+ for (const result of input.results ?? []) {
241
+ for (const ref of result.proves ?? []) {
242
+ if (!known.has(ref)) problems.push(`a result proves "${ref}", which is not a challenge`);
243
+ else proven.add(ref);
244
+ }
245
+ }
246
+ const nameOf = (id) => {
247
+ const c = challenges.find((x) => x.id === id);
248
+ return c?.title?.trim() ? `"${c.title.trim()}"` : id;
249
+ };
250
+ for (const id of known) {
251
+ if (!resolved.has(id)) problems.push(`no strategy phase resolves challenge ${nameOf(id)}`);
252
+ if (!proven.has(id)) problems.push(`no result proves challenge ${nameOf(id)}`);
253
+ }
254
+ return problems;
255
+ }
256
+ var ISO_DAY_RE = /^\d{4}-\d{2}-\d{2}$/;
257
+ function isRealDay(value) {
258
+ if (typeof value !== "string" || !ISO_DAY_RE.test(value)) return false;
259
+ const [year, month, day] = value.split("-").map(Number);
260
+ const date = new Date(Date.UTC(year, month - 1, day));
261
+ return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
262
+ }
263
+ var VERIFICATION_MAX_CHECKS = 8;
264
+ var VERIFICATION_CHECK_FIELDS = ["label", "result", "measured", "where", "method", "date", "scope"];
265
+ var VERIFICATION_FIELD_MAX_CHARS = {
266
+ label: 80,
267
+ result: 120,
268
+ measured: 200,
269
+ where: 200,
270
+ method: 240,
271
+ scope: 240
272
+ };
273
+ var VERIFICATION_EARLIEST_DAY = "2000-01-01";
274
+ function latestVerificationDay(now = /* @__PURE__ */ new Date()) {
275
+ const tomorrow = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1));
276
+ return tomorrow.toISOString().slice(0, 10);
277
+ }
278
+ function wordsOf(text) {
279
+ return text.toLowerCase().replace(/['’]/g, "").split(/[^a-z0-9]+/).filter(Boolean);
280
+ }
281
+ var FILLER_WORDS = /* @__PURE__ */ new Set([
282
+ "a",
283
+ "an",
284
+ "the",
285
+ "of",
286
+ "to",
287
+ "at",
288
+ "as",
289
+ "in",
290
+ "is",
291
+ "are",
292
+ "be",
293
+ "it",
294
+ "this",
295
+ "that",
296
+ "any",
297
+ "all"
298
+ ]);
299
+ var PLACEHOLDER_WORDS = /* @__PURE__ */ new Set([
300
+ ...FILLER_WORDS,
301
+ "n",
302
+ "na",
303
+ "nil",
304
+ "null",
305
+ "undefined",
306
+ "none",
307
+ "nothing",
308
+ "else",
309
+ "no",
310
+ "not",
311
+ "yes",
312
+ "ok",
313
+ "okay",
314
+ "x",
315
+ "xx",
316
+ "xxx",
317
+ "tbd",
318
+ "tbc",
319
+ "tba",
320
+ "todo",
321
+ "lorem",
322
+ "ipsum",
323
+ "misc",
324
+ "etc",
325
+ "various",
326
+ "multiple",
327
+ "same",
328
+ "above",
329
+ "see",
330
+ "here",
331
+ "there",
332
+ "applicable",
333
+ "available",
334
+ "relevant",
335
+ "required",
336
+ "needed",
337
+ "specified",
338
+ "unspecified",
339
+ "provided",
340
+ "given",
341
+ "stated",
342
+ "known",
343
+ "unknown",
344
+ "apply",
345
+ "applies",
346
+ "exist",
347
+ "exists",
348
+ "restriction",
349
+ "restrictions",
350
+ "limit",
351
+ "limits",
352
+ "limitation",
353
+ "limitations",
354
+ "caveat",
355
+ "caveats",
356
+ "exclusion",
357
+ "exclusions",
358
+ "exception",
359
+ "exceptions",
360
+ "constraint",
361
+ "constraints",
362
+ "gap",
363
+ "gaps",
364
+ "issue",
365
+ "issues",
366
+ "problem",
367
+ "problems",
368
+ "boundary",
369
+ "boundaries"
370
+ ]);
371
+ var LIMIT_WORDS = /* @__PURE__ */ new Set([
372
+ "only",
373
+ "not",
374
+ "never",
375
+ "cannot",
376
+ "except",
377
+ "beyond",
378
+ "outside",
379
+ "just",
380
+ "solely",
381
+ "merely",
382
+ "unless",
383
+ "besides",
384
+ "untested",
385
+ "unmeasured",
386
+ "unchecked",
387
+ "unverified",
388
+ // negative contractions, apostrophe dropped
389
+ "isnt",
390
+ "arent",
391
+ "wasnt",
392
+ "werent",
393
+ "dont",
394
+ "doesnt",
395
+ "didnt",
396
+ "cant",
397
+ "couldnt",
398
+ "wont",
399
+ "wouldnt",
400
+ "hasnt",
401
+ "havent",
402
+ "hadnt",
403
+ "shouldnt",
404
+ "mustnt",
405
+ "neednt"
406
+ ]);
407
+ var EXCLUSION_WORDS = /* @__PURE__ */ new Set([
408
+ "exclude",
409
+ "excludes",
410
+ "excluded",
411
+ "excluding",
412
+ "omit",
413
+ "omits",
414
+ "omitted",
415
+ "skip",
416
+ "skips",
417
+ "skipped",
418
+ "limited"
419
+ ]);
420
+ var NEGATOR_WORDS = /* @__PURE__ */ new Set(["no", "none", "nothing", "without"]);
421
+ function isPlaceholderText(text) {
422
+ const words = wordsOf(text);
423
+ return words.length === 0 || words.every((word) => PLACEHOLDER_WORDS.has(word));
424
+ }
425
+ function isVacuousScope(scope) {
426
+ const words = wordsOf(scope);
427
+ if (words.length < 2) return true;
428
+ const wordAfter = (i) => {
429
+ for (let j = i + 1; j < words.length; j++) if (!FILLER_WORDS.has(words[j])) return words[j];
430
+ return void 0;
431
+ };
432
+ const wordBefore = (i) => {
433
+ for (let j = i - 1; j >= 0; j--) if (!FILLER_WORDS.has(words[j])) return words[j];
434
+ return void 0;
435
+ };
436
+ const marksLimit = words.map((word, i) => {
437
+ if (NEGATOR_WORDS.has(word)) {
438
+ const next = wordAfter(i);
439
+ return next !== void 0 && !PLACEHOLDER_WORDS.has(next) && !EXCLUSION_WORDS.has(next);
440
+ }
441
+ if (EXCLUSION_WORDS.has(word)) return !NEGATOR_WORDS.has(wordBefore(i) ?? "");
442
+ return LIMIT_WORDS.has(word);
443
+ });
444
+ if (!marksLimit.some(Boolean)) return true;
445
+ return !words.some(
446
+ (word) => !PLACEHOLDER_WORDS.has(word) && !LIMIT_WORDS.has(word) && !EXCLUSION_WORDS.has(word) && !NEGATOR_WORDS.has(word)
447
+ );
448
+ }
449
+ function carriesCredentials(text) {
450
+ for (let at = text.indexOf("://"); at !== -1; at = text.indexOf("://", at + 3)) {
451
+ for (let i = at + 3; i < text.length; i++) {
452
+ const char = text[i];
453
+ if (char === "@") return true;
454
+ if (char === "/" || char === "?" || char === "#" || /\s/.test(char)) break;
455
+ }
456
+ }
457
+ return false;
458
+ }
459
+ var WEB_ADDRESS = /^https?:\/\//i;
460
+ var BARE_PRIVATE_ADDRESS = /^(?:localhost|\d{1,3}(?:\.\d{1,3}){3}|\[[0-9a-f:.]+\])(?::\d+)?(?:[/?#]|$)/i;
461
+ function trimPunctuation(word) {
462
+ let start = 0;
463
+ let end = word.length;
464
+ while (start < end && `("'<`.includes(word[start])) start++;
465
+ while (end > start && `)"'>.,;!?`.includes(word[end - 1])) end--;
466
+ return word.slice(start, end);
467
+ }
468
+ function namesPrivateAddress(text) {
469
+ return text.split(/\s+/).some((word) => {
470
+ const token = trimPunctuation(word);
471
+ const address = WEB_ADDRESS.test(token) ? token : BARE_PRIVATE_ADDRESS.test(token) ? `http://${token}` : null;
472
+ if (!address) return false;
473
+ try {
474
+ return isBlockedPublicUrlHost(new URL(address).hostname);
475
+ } catch {
476
+ return false;
477
+ }
478
+ });
479
+ }
480
+ function looksLikePathOrHost(token) {
481
+ const pieces = token.split(/[/\\.]/).filter(Boolean);
482
+ return pieces.length >= 2 && pieces.every((piece) => /^[\w:-]{2,}$/.test(piece));
483
+ }
484
+ function whereProblem(where) {
485
+ if (carriesCredentials(where)) return "must not carry a username or password";
486
+ if (namesPrivateAddress(where)) return "must be a public address, not localhost or a private network";
487
+ const named = !isPlaceholderText(where) && (verificationLink(where) !== null || where.split(/\s+/).length >= 2 || looksLikePathOrHost(where));
488
+ return named ? null : "must say where the check ran: a public web address, or a screen or file named in words";
489
+ }
490
+ var NOT_PLAIN_LINE = /[\p{Cc}\u2028\u2029\u200B-\u200F\u202A-\u202E\u2066-\u2069\uFEFF]/u;
491
+ function checkProblems(check, at, latestDay, strict) {
492
+ if (!check || typeof check !== "object" || Array.isArray(check)) return [`${at} must be an object`];
493
+ const entry = check;
494
+ const problems = [];
495
+ if (strict) {
496
+ for (const key of Object.keys(entry)) {
497
+ if (!VERIFICATION_CHECK_FIELDS.includes(key)) problems.push(`${at}.${key} is not a field`);
498
+ }
499
+ }
500
+ for (const field of VERIFICATION_CHECK_FIELDS) {
501
+ const value = entry[field];
502
+ if (typeof value !== "string" || !value.trim()) {
503
+ problems.push(
504
+ field === "scope" ? `${at}.scope is required: say what the check does not cover` : `${at}.${field} is required`
505
+ );
506
+ continue;
507
+ }
508
+ if (NOT_PLAIN_LINE.test(value)) problems.push(`${at}.${field} must be one line of plain text`);
509
+ if (field === "date") continue;
510
+ const max = VERIFICATION_FIELD_MAX_CHARS[field];
511
+ if (value.trim().length > max) problems.push(`${at}.${field} is over ${max} characters`);
512
+ }
513
+ const date = typeof entry.date === "string" ? entry.date.trim() : "";
514
+ if (date) {
515
+ if (!isRealDay(date)) problems.push(`${at}.date must be a real day, YYYY-MM-DD`);
516
+ else if (date < VERIFICATION_EARLIEST_DAY) problems.push(`${at}.date must be ${VERIFICATION_EARLIEST_DAY} or later`);
517
+ else if (date > latestDay) problems.push(`${at}.date is in the future`);
518
+ }
519
+ const text = (field) => {
520
+ const value = entry[field];
521
+ return typeof value === "string" && value.trim() && value.trim().length <= VERIFICATION_FIELD_MAX_CHARS[field] ? value.trim() : null;
522
+ };
523
+ const where = text("where");
524
+ const problem = where === null ? null : whereProblem(where);
525
+ if (problem) problems.push(`${at}.where ${problem}`);
526
+ const scope = text("scope");
527
+ if (scope !== null && isVacuousScope(scope)) {
528
+ problems.push(`${at}.scope must state a limit: what the check does not cover, using a word like "only", "not", "except" or "without"`);
529
+ }
530
+ return problems;
531
+ }
532
+ function verificationProblems(data, options = {}) {
533
+ if (!data || typeof data !== "object" || Array.isArray(data)) return ["verification data must be an object"];
534
+ const { checks, ...rest } = data;
535
+ const problems = Object.keys(rest).map((key) => `${key} is not a field of a verification`);
536
+ if (!Array.isArray(checks) || checks.length === 0) return [...problems, "checks must list at least one check"];
537
+ if (checks.length > VERIFICATION_MAX_CHECKS) {
538
+ problems.push(`checks is over ${VERIFICATION_MAX_CHECKS} entries; keep the ones that matter`);
539
+ }
540
+ const latestDay = options.latestDay ?? latestVerificationDay();
541
+ checks.forEach((check, i) => problems.push(...checkProblems(check, `checks[${i}]`, latestDay, true)));
542
+ return problems;
543
+ }
544
+ function readVerificationChecks(data, options = {}) {
545
+ const checks = data?.checks;
546
+ if (!Array.isArray(checks)) return [];
547
+ const latestDay = options.latestDay ?? latestVerificationDay();
548
+ const shown = [];
549
+ checks.forEach((check, i) => {
550
+ if (shown.length >= VERIFICATION_MAX_CHECKS || checkProblems(check, `checks[${i}]`, latestDay, false).length) return;
551
+ const entry = check;
552
+ shown.push(
553
+ Object.fromEntries(VERIFICATION_CHECK_FIELDS.map((field) => [field, entry[field].trim()]))
554
+ );
555
+ });
556
+ return shown;
557
+ }
558
+ function verificationLink(where) {
559
+ const text = where.trim();
560
+ if (!text || /\s/.test(text)) return null;
561
+ try {
562
+ const url = new URL(text);
563
+ if (url.protocol !== "https:" && url.protocol !== "http:") return null;
564
+ if (url.username || url.password || isBlockedPublicUrlHost(url.hostname)) return null;
565
+ return url.toString();
566
+ } catch {
567
+ return null;
568
+ }
569
+ }
570
+ var PORTFOLIO_VERIFICATION_JSON_SCHEMA = {
571
+ $schema: "http://json-schema.org/draft-07/schema#",
572
+ $id: "https://sonor.io/schemas/portfolio-verification.v1.json",
573
+ title: "PortfolioVerificationData",
574
+ description: "A short list of checks on the finished build. Each says what was checked, where, how, when, what came out, and what it does not cover.",
575
+ type: "object",
576
+ required: ["checks"],
577
+ properties: {
578
+ checks: {
579
+ type: "array",
580
+ minItems: 1,
581
+ maxItems: VERIFICATION_MAX_CHECKS,
582
+ items: {
583
+ type: "object",
584
+ required: [...VERIFICATION_CHECK_FIELDS],
585
+ properties: {
586
+ label: { type: "string", minLength: 1, maxLength: VERIFICATION_FIELD_MAX_CHARS.label },
587
+ result: { type: "string", minLength: 1, maxLength: VERIFICATION_FIELD_MAX_CHARS.result },
588
+ measured: { type: "string", minLength: 1, maxLength: VERIFICATION_FIELD_MAX_CHARS.measured },
589
+ where: { type: "string", minLength: 1, maxLength: VERIFICATION_FIELD_MAX_CHARS.where },
590
+ method: { type: "string", minLength: 1, maxLength: VERIFICATION_FIELD_MAX_CHARS.method },
591
+ date: { type: "string", pattern: "^\\d{4}-\\d{2}-\\d{2}$" },
592
+ scope: { type: "string", minLength: 1, maxLength: VERIFICATION_FIELD_MAX_CHARS.scope }
593
+ },
594
+ additionalProperties: false
595
+ }
596
+ }
597
+ },
598
+ additionalProperties: false
599
+ };
600
+ var LIGHTHOUSE_RE = /lighthouse/i;
601
+ function isLighthouseKpi(kpi) {
602
+ return LIGHTHOUSE_RE.test(kpi.label || "");
603
+ }
604
+ function curateHeroKpis(kpis) {
605
+ const business = kpis.filter((k) => !isLighthouseKpi(k));
606
+ const lighthouse = kpis.filter(isLighthouseKpi);
607
+ if (lighthouse.length <= 1) return kpis;
608
+ const allPerfect = lighthouse.length >= 4 && lighthouse.every((k) => k.value === 100);
609
+ const summary = allPerfect ? {
610
+ ...lighthouse[0],
611
+ label: "Lighthouse, all 4 categories",
612
+ description: "Perfect scores across performance, SEO, accessibility, and best practices."
613
+ } : lighthouse.find((k) => /performance/i.test(k.label)) ?? lighthouse[0];
614
+ return [...business, summary];
615
+ }
616
+ var PORTFOLIO_KPI_JSON_SCHEMA = {
617
+ $schema: "http://json-schema.org/draft-07/schema#",
618
+ $id: "https://sonor.io/schemas/portfolio-kpi.v1.json",
619
+ title: "KpiLike",
620
+ description: "The subset of a portfolio KPI tile the Lighthouse-collapse policy reads.",
621
+ type: "object",
622
+ required: ["label", "value"],
623
+ properties: {
624
+ label: { type: "string" },
625
+ value: { type: "number" },
626
+ description: { type: "string" },
627
+ source: { enum: ["measured", "reported", "estimated"] },
628
+ reportedBy: {
629
+ type: "object",
630
+ required: ["name", "organization", "date"],
631
+ properties: {
632
+ name: { type: "string", minLength: 1 },
633
+ role: { type: "string" },
634
+ organization: { type: "string", minLength: 1 },
635
+ date: { type: "string", pattern: "^\\d{4}-\\d{2}-\\d{2}$" }
636
+ }
637
+ }
638
+ },
639
+ // A reported number is only valid with someone named behind it.
640
+ if: { properties: { source: { const: "reported" } }, required: ["source"] },
641
+ then: { required: ["reportedBy"] },
642
+ additionalProperties: true
643
+ };
644
+ var METRIC_SOURCES = ["measured", "reported", "estimated"];
645
+ var MONTHS = [
646
+ "January",
647
+ "February",
648
+ "March",
649
+ "April",
650
+ "May",
651
+ "June",
652
+ "July",
653
+ "August",
654
+ "September",
655
+ "October",
656
+ "November",
657
+ "December"
658
+ ];
659
+ function isMetricSource(value) {
660
+ return typeof value === "string" && METRIC_SOURCES.includes(value);
661
+ }
662
+ function metricSourceProblem(metric) {
663
+ if (metric.source === void 0 || metric.source === null) return null;
664
+ if (!isMetricSource(metric.source)) return `source must be one of ${METRIC_SOURCES.join(", ")}`;
665
+ if (metric.source !== "reported") return null;
666
+ const by = metric.reportedBy;
667
+ if (!by || typeof by !== "object") {
668
+ return "a reported number needs reportedBy: who reported it, where they work, and when";
669
+ }
670
+ if (typeof by.name !== "string" || !by.name.trim()) return "reportedBy.name is required";
671
+ if (typeof by.organization !== "string" || !by.organization.trim()) {
672
+ return "reportedBy.organization is required";
673
+ }
674
+ if (typeof by.date !== "string" || !isRealDay(by.date)) {
675
+ return "reportedBy.date must be a real day, YYYY-MM-DD";
676
+ }
677
+ if (by.role !== void 0 && typeof by.role !== "string") return "reportedBy.role must be text";
678
+ return null;
679
+ }
680
+ function isHeadlineMetric(metric) {
681
+ if (metric.source === "measured") return true;
682
+ return metric.source === "reported" && metricSourceProblem(metric) === null;
683
+ }
684
+ function metricSourceLabel(source) {
685
+ if (source === "measured") return "Measured";
686
+ if (source === "reported") return "Client-reported";
687
+ if (source === "estimated") return "Estimated";
688
+ return null;
689
+ }
690
+ function formatAttribution(by) {
691
+ const [year, month, day] = by.date.split("-").map(Number);
692
+ const when = MONTHS[month - 1] ? `${MONTHS[month - 1]} ${day}, ${year}` : by.date;
693
+ return [`Reported by ${by.name.trim()}`, by.role?.trim(), by.organization.trim(), when].filter(Boolean).join(", ");
694
+ }
695
+ function shortAttribution(by) {
696
+ return `per ${by.organization.trim()}`;
697
+ }
698
+ function carryReportedMetrics(previous, next, keyOf) {
699
+ const norm = (metric) => String(keyOf(metric) ?? "").trim().toLowerCase();
700
+ const reported = (previous ?? []).filter((m) => m?.source === "reported");
701
+ const taken = new Set(reported.map(norm));
702
+ const generated = (next ?? []).filter((m) => m && m.source !== "reported" && !taken.has(norm(m)));
703
+ return [...reported, ...generated];
704
+ }
705
+
706
+ export { CASE_STUDY_NARRATIVE_JSON_SCHEMA, CASE_STUDY_STAGES, CASE_STUDY_STAGE_COPY, METRIC_SOURCES, NARRATIVE_MAX_CHARS, PORTFOLIO_CONTRACT_VERSION, PORTFOLIO_KPI_JSON_SCHEMA, PORTFOLIO_SECTION_ORDER, PORTFOLIO_SECTION_TYPES, PORTFOLIO_VERIFICATION_JSON_SCHEMA, SECTION_STAGE, STAGE_CORE_ALTERNATES, STAGE_CORE_SECTION, STAGE_NAME_MAX_CHARS, VERIFICATION_CHECK_FIELDS, VERIFICATION_EARLIEST_DAY, VERIFICATION_FIELD_MAX_CHARS, VERIFICATION_MAX_CHECKS, carryReportedMetrics, caseStudyFlowProblems, caseStudyStageStatus, challengeId, curateHeroKpis, formatAttribution, groupSectionsByStage, isHeadlineMetric, isLighthouseKpi, isMetricSource, isPlaceholderText, isPortfolioSectionType, isRealDay, isVacuousScope, latestVerificationDay, metricSourceLabel, metricSourceProblem, narrativeProblems, orderSections, readVerificationChecks, sectionStage, shortAttribution, stageCoreSections, stageName, stageNamesProblems, verificationLink, verificationProblems };