@sonordev/contracts 2.15.0 → 2.17.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/dist/portfolio.js CHANGED
@@ -1,10 +1,43 @@
1
1
  'use strict';
2
2
 
3
- // src/contracts/portfolio.ts
3
+ // src/contracts/public-url-host.ts
4
+ function isPrivateIpv4(a, b) {
5
+ return a === 127 || a === 10 || a === 0 || a === 192 && b === 168 || a === 172 && b >= 16 && b <= 31 || a === 169 && b === 254 || a === 100 && b >= 64 && b <= 127 || a >= 224;
6
+ }
7
+ function ipv6Words(host) {
8
+ if (!host.includes(":") || !/^[a-f\d:.]+$/.test(host)) return null;
9
+ let canonical;
10
+ try {
11
+ canonical = new URL(`http://[${host}]/`).hostname.slice(1, -1);
12
+ } catch {
13
+ return null;
14
+ }
15
+ const sides = canonical.split("::");
16
+ const left = sides[0] ? sides[0].split(":") : [];
17
+ const right = sides.length > 1 && sides[1] ? sides[1].split(":") : [];
18
+ return [...left, ...Array(sides.length > 1 ? 8 - left.length - right.length : 0).fill("0"), ...right].map((word) => parseInt(word, 16));
19
+ }
20
+ function isBlockedPublicUrlHost(rawHost) {
21
+ const host = rawHost.toLowerCase().replace(/^\[|\]$/g, "");
22
+ const blockedName = host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || host.endsWith(".internal");
23
+ if (host.includes(":") && host.includes("%")) return true;
24
+ const ipv4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
25
+ const blockedIp = !!ipv4 && isPrivateIpv4(Number(ipv4[1]), Number(ipv4[2]));
26
+ const words = ipv6Words(host);
27
+ const blockedIpv6 = !!words && // Unspecified/loopback, unique-local, link-local, deprecated site-local
28
+ // and multicast destinations aren't public logo/webhook endpoints.
29
+ (words.every((word) => word === 0) || words.slice(0, 7).every((word) => word === 0) && words[7] === 1 || (words[0] & 65024) === 64512 || (words[0] & 65472) === 65152 || (words[0] & 65472) === 65216 || (words[0] & 65280) === 65280 || // IPv4-mapped and older IPv4-compatible forms carry an IPv4 destination.
30
+ words.slice(0, 5).every((word) => word === 0) && [0, 65535].includes(words[5]) && isPrivateIpv4(words[6] >> 8, words[6] & 255));
31
+ return blockedName || blockedIp || blockedIpv6;
32
+ }
33
+
34
+ // src/contracts/frame-ready.ts
4
35
  var FRAME_READY_MESSAGE = "sonor:frame-ready";
5
36
  function isFrameReadyMessage(data) {
6
37
  return !!data && typeof data === "object" && data.type === FRAME_READY_MESSAGE && data.v === 1;
7
38
  }
39
+
40
+ // src/contracts/portfolio.ts
8
41
  var PORTFOLIO_CONTRACT_VERSION = 1;
9
42
  var CASE_STUDY_STAGES = ["problem", "build", "proof", "outcome"];
10
43
  var CASE_STUDY_STAGE_COPY = {
@@ -47,6 +80,7 @@ var PORTFOLIO_SECTION_TYPES = [
47
80
  // proof
48
81
  "portfolioPerformance",
49
82
  "portfolioSpeedComparison",
83
+ "portfolioVerification",
50
84
  // outcome
51
85
  "portfolioResults",
52
86
  "portfolioMetricsTimeline",
@@ -75,6 +109,7 @@ var SECTION_STAGE = {
75
109
  portfolioTechStack: "build",
76
110
  portfolioPerformance: "proof",
77
111
  portfolioSpeedComparison: "proof",
112
+ portfolioVerification: "proof",
78
113
  portfolioResults: "outcome",
79
114
  portfolioMetricsTimeline: "outcome",
80
115
  portfolioConversionFunnel: "outcome",
@@ -94,6 +129,18 @@ var STAGE_CORE_SECTION = {
94
129
  proof: "portfolioPerformance",
95
130
  outcome: "portfolioResults"
96
131
  };
132
+ var STAGE_CORE_ALTERNATES = {
133
+ proof: ["portfolioVerification"]
134
+ };
135
+ var STAND_IN_HAS_CONTENT = {
136
+ portfolioVerification: (data) => readVerificationChecks(data).length > 0
137
+ };
138
+ var STAND_IN_EMPTY_COPY = {
139
+ portfolioVerification: "The verification is there, but none of its checks can be shown yet. Add a check, and say what it doesn't cover."
140
+ };
141
+ function stageCoreSections(stage) {
142
+ return [STAGE_CORE_SECTION[stage], ...STAGE_CORE_ALTERNATES[stage] ?? []];
143
+ }
97
144
  var PORTFOLIO_SECTION_ORDER = Object.fromEntries(
98
145
  PORTFOLIO_SECTION_TYPES.map((type, i) => [type, i])
99
146
  );
@@ -137,7 +184,7 @@ var STAGE_MISSING_COPY = {
137
184
  noCore: "The build is shown but the plan behind it is missing. Add the strategy."
138
185
  },
139
186
  proof: {
140
- empty: "Nothing measures the finished build. Add a performance measurement.",
187
+ empty: "Nothing measures the finished build. Add a performance measurement, or a verification when the screens are behind a sign-in.",
141
188
  noCore: "The old site is compared but the finished build has no measurement of its own. Add a performance measurement."
142
189
  },
143
190
  outcome: {
@@ -148,15 +195,20 @@ var STAGE_MISSING_COPY = {
148
195
  function caseStudyStageStatus(sections) {
149
196
  const ordered = orderSections(sections);
150
197
  return CASE_STUDY_STAGES.map((stage) => {
151
- const present = ordered.map((s) => s.sectionType).filter((t) => isPortfolioSectionType(t) && SECTION_STAGE[t] === stage);
152
- const complete = present.includes(STAGE_CORE_SECTION[stage]);
198
+ const own = ordered.filter(
199
+ (s) => isPortfolioSectionType(s.sectionType) && SECTION_STAGE[s.sectionType] === stage
200
+ );
201
+ const present = own.map((s) => s.sectionType);
202
+ const standIns = STAGE_CORE_ALTERNATES[stage] ?? [];
203
+ const emptyStandIns = own.filter(
204
+ (s) => standIns.includes(s.sectionType) && STAND_IN_HAS_CONTENT[s.sectionType]?.(s.data) === false
205
+ );
206
+ const complete = own.some(
207
+ (s) => s.sectionType === STAGE_CORE_SECTION[stage] || standIns.includes(s.sectionType) && !emptyStandIns.includes(s)
208
+ );
153
209
  const copy = STAGE_MISSING_COPY[stage];
154
- return {
155
- stage,
156
- present,
157
- complete,
158
- missing: complete ? null : present.length ? copy.noCore : copy.empty
159
- };
210
+ const missing = complete ? null : (emptyStandIns.length ? STAND_IN_EMPTY_COPY[emptyStandIns[0].sectionType] : void 0) ?? (present.length ? copy.noCore : copy.empty);
211
+ return { stage, present, complete, missing };
160
212
  });
161
213
  }
162
214
  var NARRATIVE_MAX_CHARS = 320;
@@ -236,6 +288,350 @@ function caseStudyFlowProblems(input) {
236
288
  }
237
289
  return problems;
238
290
  }
291
+ var ISO_DAY_RE = /^\d{4}-\d{2}-\d{2}$/;
292
+ function isRealDay(value) {
293
+ if (typeof value !== "string" || !ISO_DAY_RE.test(value)) return false;
294
+ const [year, month, day] = value.split("-").map(Number);
295
+ const date = new Date(Date.UTC(year, month - 1, day));
296
+ return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
297
+ }
298
+ var VERIFICATION_MAX_CHECKS = 8;
299
+ var VERIFICATION_CHECK_FIELDS = ["label", "result", "measured", "where", "method", "date", "scope"];
300
+ var VERIFICATION_FIELD_MAX_CHARS = {
301
+ label: 80,
302
+ result: 120,
303
+ measured: 200,
304
+ where: 200,
305
+ method: 240,
306
+ scope: 240
307
+ };
308
+ var VERIFICATION_EARLIEST_DAY = "2000-01-01";
309
+ function latestVerificationDay(now = /* @__PURE__ */ new Date()) {
310
+ const tomorrow = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1));
311
+ return tomorrow.toISOString().slice(0, 10);
312
+ }
313
+ function wordsOf(text) {
314
+ return text.toLowerCase().replace(/['’]/g, "").split(/[^a-z0-9]+/).filter(Boolean);
315
+ }
316
+ var FILLER_WORDS = /* @__PURE__ */ new Set([
317
+ "a",
318
+ "an",
319
+ "the",
320
+ "of",
321
+ "to",
322
+ "at",
323
+ "as",
324
+ "in",
325
+ "is",
326
+ "are",
327
+ "be",
328
+ "it",
329
+ "this",
330
+ "that",
331
+ "any",
332
+ "all"
333
+ ]);
334
+ var PLACEHOLDER_WORDS = /* @__PURE__ */ new Set([
335
+ ...FILLER_WORDS,
336
+ "n",
337
+ "na",
338
+ "nil",
339
+ "null",
340
+ "undefined",
341
+ "none",
342
+ "nothing",
343
+ "else",
344
+ "no",
345
+ "not",
346
+ "yes",
347
+ "ok",
348
+ "okay",
349
+ "x",
350
+ "xx",
351
+ "xxx",
352
+ "tbd",
353
+ "tbc",
354
+ "tba",
355
+ "todo",
356
+ "lorem",
357
+ "ipsum",
358
+ "misc",
359
+ "etc",
360
+ "various",
361
+ "multiple",
362
+ "same",
363
+ "above",
364
+ "see",
365
+ "here",
366
+ "there",
367
+ "applicable",
368
+ "available",
369
+ "relevant",
370
+ "required",
371
+ "needed",
372
+ "specified",
373
+ "unspecified",
374
+ "provided",
375
+ "given",
376
+ "stated",
377
+ "known",
378
+ "unknown",
379
+ "apply",
380
+ "applies",
381
+ "exist",
382
+ "exists",
383
+ "restriction",
384
+ "restrictions",
385
+ "limit",
386
+ "limits",
387
+ "limitation",
388
+ "limitations",
389
+ "caveat",
390
+ "caveats",
391
+ "exclusion",
392
+ "exclusions",
393
+ "exception",
394
+ "exceptions",
395
+ "constraint",
396
+ "constraints",
397
+ "gap",
398
+ "gaps",
399
+ "issue",
400
+ "issues",
401
+ "problem",
402
+ "problems",
403
+ "boundary",
404
+ "boundaries"
405
+ ]);
406
+ var LIMIT_WORDS = /* @__PURE__ */ new Set([
407
+ "only",
408
+ "not",
409
+ "never",
410
+ "cannot",
411
+ "except",
412
+ "beyond",
413
+ "outside",
414
+ "just",
415
+ "solely",
416
+ "merely",
417
+ "unless",
418
+ "besides",
419
+ "untested",
420
+ "unmeasured",
421
+ "unchecked",
422
+ "unverified",
423
+ // negative contractions, apostrophe dropped
424
+ "isnt",
425
+ "arent",
426
+ "wasnt",
427
+ "werent",
428
+ "dont",
429
+ "doesnt",
430
+ "didnt",
431
+ "cant",
432
+ "couldnt",
433
+ "wont",
434
+ "wouldnt",
435
+ "hasnt",
436
+ "havent",
437
+ "hadnt",
438
+ "shouldnt",
439
+ "mustnt",
440
+ "neednt"
441
+ ]);
442
+ var EXCLUSION_WORDS = /* @__PURE__ */ new Set([
443
+ "exclude",
444
+ "excludes",
445
+ "excluded",
446
+ "excluding",
447
+ "omit",
448
+ "omits",
449
+ "omitted",
450
+ "skip",
451
+ "skips",
452
+ "skipped",
453
+ "limited"
454
+ ]);
455
+ var NEGATOR_WORDS = /* @__PURE__ */ new Set(["no", "none", "nothing", "without"]);
456
+ function isPlaceholderText(text) {
457
+ const words = wordsOf(text);
458
+ return words.length === 0 || words.every((word) => PLACEHOLDER_WORDS.has(word));
459
+ }
460
+ function isVacuousScope(scope) {
461
+ const words = wordsOf(scope);
462
+ if (words.length < 2) return true;
463
+ const wordAfter = (i) => {
464
+ for (let j = i + 1; j < words.length; j++) if (!FILLER_WORDS.has(words[j])) return words[j];
465
+ return void 0;
466
+ };
467
+ const wordBefore = (i) => {
468
+ for (let j = i - 1; j >= 0; j--) if (!FILLER_WORDS.has(words[j])) return words[j];
469
+ return void 0;
470
+ };
471
+ const marksLimit = words.map((word, i) => {
472
+ if (NEGATOR_WORDS.has(word)) {
473
+ const next = wordAfter(i);
474
+ return next !== void 0 && !PLACEHOLDER_WORDS.has(next) && !EXCLUSION_WORDS.has(next);
475
+ }
476
+ if (EXCLUSION_WORDS.has(word)) return !NEGATOR_WORDS.has(wordBefore(i) ?? "");
477
+ return LIMIT_WORDS.has(word);
478
+ });
479
+ if (!marksLimit.some(Boolean)) return true;
480
+ return !words.some(
481
+ (word) => !PLACEHOLDER_WORDS.has(word) && !LIMIT_WORDS.has(word) && !EXCLUSION_WORDS.has(word) && !NEGATOR_WORDS.has(word)
482
+ );
483
+ }
484
+ function carriesCredentials(text) {
485
+ for (let at = text.indexOf("://"); at !== -1; at = text.indexOf("://", at + 3)) {
486
+ for (let i = at + 3; i < text.length; i++) {
487
+ const char = text[i];
488
+ if (char === "@") return true;
489
+ if (char === "/" || char === "?" || char === "#" || /\s/.test(char)) break;
490
+ }
491
+ }
492
+ return false;
493
+ }
494
+ var WEB_ADDRESS = /^https?:\/\//i;
495
+ var BARE_PRIVATE_ADDRESS = /^(?:localhost|\d{1,3}(?:\.\d{1,3}){3}|\[[0-9a-f:.]+\])(?::\d+)?(?:[/?#]|$)/i;
496
+ function trimPunctuation(word) {
497
+ let start = 0;
498
+ let end = word.length;
499
+ while (start < end && `("'<`.includes(word[start])) start++;
500
+ while (end > start && `)"'>.,;!?`.includes(word[end - 1])) end--;
501
+ return word.slice(start, end);
502
+ }
503
+ function namesPrivateAddress(text) {
504
+ return text.split(/\s+/).some((word) => {
505
+ const token = trimPunctuation(word);
506
+ const address = WEB_ADDRESS.test(token) ? token : BARE_PRIVATE_ADDRESS.test(token) ? `http://${token}` : null;
507
+ if (!address) return false;
508
+ try {
509
+ return isBlockedPublicUrlHost(new URL(address).hostname);
510
+ } catch {
511
+ return false;
512
+ }
513
+ });
514
+ }
515
+ function looksLikePathOrHost(token) {
516
+ const pieces = token.split(/[/\\.]/).filter(Boolean);
517
+ return pieces.length >= 2 && pieces.every((piece) => /^[\w:-]{2,}$/.test(piece));
518
+ }
519
+ function whereProblem(where) {
520
+ if (carriesCredentials(where)) return "must not carry a username or password";
521
+ if (namesPrivateAddress(where)) return "must be a public address, not localhost or a private network";
522
+ const named = !isPlaceholderText(where) && (verificationLink(where) !== null || where.split(/\s+/).length >= 2 || looksLikePathOrHost(where));
523
+ return named ? null : "must say where the check ran: a public web address, or a screen or file named in words";
524
+ }
525
+ var NOT_PLAIN_LINE = /[\p{Cc}\u2028\u2029\u200B-\u200F\u202A-\u202E\u2066-\u2069\uFEFF]/u;
526
+ function checkProblems(check, at, latestDay, strict) {
527
+ if (!check || typeof check !== "object" || Array.isArray(check)) return [`${at} must be an object`];
528
+ const entry = check;
529
+ const problems = [];
530
+ if (strict) {
531
+ for (const key of Object.keys(entry)) {
532
+ if (!VERIFICATION_CHECK_FIELDS.includes(key)) problems.push(`${at}.${key} is not a field`);
533
+ }
534
+ }
535
+ for (const field of VERIFICATION_CHECK_FIELDS) {
536
+ const value = entry[field];
537
+ if (typeof value !== "string" || !value.trim()) {
538
+ problems.push(
539
+ field === "scope" ? `${at}.scope is required: say what the check does not cover` : `${at}.${field} is required`
540
+ );
541
+ continue;
542
+ }
543
+ if (NOT_PLAIN_LINE.test(value)) problems.push(`${at}.${field} must be one line of plain text`);
544
+ if (field === "date") continue;
545
+ const max = VERIFICATION_FIELD_MAX_CHARS[field];
546
+ if (value.trim().length > max) problems.push(`${at}.${field} is over ${max} characters`);
547
+ }
548
+ const date = typeof entry.date === "string" ? entry.date.trim() : "";
549
+ if (date) {
550
+ if (!isRealDay(date)) problems.push(`${at}.date must be a real day, YYYY-MM-DD`);
551
+ else if (date < VERIFICATION_EARLIEST_DAY) problems.push(`${at}.date must be ${VERIFICATION_EARLIEST_DAY} or later`);
552
+ else if (date > latestDay) problems.push(`${at}.date is in the future`);
553
+ }
554
+ const text = (field) => {
555
+ const value = entry[field];
556
+ return typeof value === "string" && value.trim() && value.trim().length <= VERIFICATION_FIELD_MAX_CHARS[field] ? value.trim() : null;
557
+ };
558
+ const where = text("where");
559
+ const problem = where === null ? null : whereProblem(where);
560
+ if (problem) problems.push(`${at}.where ${problem}`);
561
+ const scope = text("scope");
562
+ if (scope !== null && isVacuousScope(scope)) {
563
+ problems.push(`${at}.scope must state a limit: what the check does not cover, using a word like "only", "not", "except" or "without"`);
564
+ }
565
+ return problems;
566
+ }
567
+ function verificationProblems(data, options = {}) {
568
+ if (!data || typeof data !== "object" || Array.isArray(data)) return ["verification data must be an object"];
569
+ const { checks, ...rest } = data;
570
+ const problems = Object.keys(rest).map((key) => `${key} is not a field of a verification`);
571
+ if (!Array.isArray(checks) || checks.length === 0) return [...problems, "checks must list at least one check"];
572
+ if (checks.length > VERIFICATION_MAX_CHECKS) {
573
+ problems.push(`checks is over ${VERIFICATION_MAX_CHECKS} entries; keep the ones that matter`);
574
+ }
575
+ const latestDay = options.latestDay ?? latestVerificationDay();
576
+ checks.forEach((check, i) => problems.push(...checkProblems(check, `checks[${i}]`, latestDay, true)));
577
+ return problems;
578
+ }
579
+ function readVerificationChecks(data, options = {}) {
580
+ const checks = data?.checks;
581
+ if (!Array.isArray(checks)) return [];
582
+ const latestDay = options.latestDay ?? latestVerificationDay();
583
+ const shown = [];
584
+ checks.forEach((check, i) => {
585
+ if (shown.length >= VERIFICATION_MAX_CHECKS || checkProblems(check, `checks[${i}]`, latestDay, false).length) return;
586
+ const entry = check;
587
+ shown.push(
588
+ Object.fromEntries(VERIFICATION_CHECK_FIELDS.map((field) => [field, entry[field].trim()]))
589
+ );
590
+ });
591
+ return shown;
592
+ }
593
+ function verificationLink(where) {
594
+ const text = where.trim();
595
+ if (!text || /\s/.test(text)) return null;
596
+ try {
597
+ const url = new URL(text);
598
+ if (url.protocol !== "https:" && url.protocol !== "http:") return null;
599
+ if (url.username || url.password || isBlockedPublicUrlHost(url.hostname)) return null;
600
+ return url.toString();
601
+ } catch {
602
+ return null;
603
+ }
604
+ }
605
+ var PORTFOLIO_VERIFICATION_JSON_SCHEMA = {
606
+ $schema: "http://json-schema.org/draft-07/schema#",
607
+ $id: "https://sonor.io/schemas/portfolio-verification.v1.json",
608
+ title: "PortfolioVerificationData",
609
+ 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.",
610
+ type: "object",
611
+ required: ["checks"],
612
+ properties: {
613
+ checks: {
614
+ type: "array",
615
+ minItems: 1,
616
+ maxItems: VERIFICATION_MAX_CHECKS,
617
+ items: {
618
+ type: "object",
619
+ required: [...VERIFICATION_CHECK_FIELDS],
620
+ properties: {
621
+ label: { type: "string", minLength: 1, maxLength: VERIFICATION_FIELD_MAX_CHARS.label },
622
+ result: { type: "string", minLength: 1, maxLength: VERIFICATION_FIELD_MAX_CHARS.result },
623
+ measured: { type: "string", minLength: 1, maxLength: VERIFICATION_FIELD_MAX_CHARS.measured },
624
+ where: { type: "string", minLength: 1, maxLength: VERIFICATION_FIELD_MAX_CHARS.where },
625
+ method: { type: "string", minLength: 1, maxLength: VERIFICATION_FIELD_MAX_CHARS.method },
626
+ date: { type: "string", pattern: "^\\d{4}-\\d{2}-\\d{2}$" },
627
+ scope: { type: "string", minLength: 1, maxLength: VERIFICATION_FIELD_MAX_CHARS.scope }
628
+ },
629
+ additionalProperties: false
630
+ }
631
+ }
632
+ },
633
+ additionalProperties: false
634
+ };
239
635
  var LIGHTHOUSE_RE = /lighthouse/i;
240
636
  function isLighthouseKpi(kpi) {
241
637
  return LIGHTHOUSE_RE.test(kpi.label || "");
@@ -281,7 +677,6 @@ var PORTFOLIO_KPI_JSON_SCHEMA = {
281
677
  additionalProperties: true
282
678
  };
283
679
  var METRIC_SOURCES = ["measured", "reported", "estimated"];
284
- var ISO_DAY_RE = /^\d{4}-\d{2}-\d{2}$/;
285
680
  var MONTHS = [
286
681
  "January",
287
682
  "February",
@@ -311,7 +706,7 @@ function metricSourceProblem(metric) {
311
706
  if (typeof by.organization !== "string" || !by.organization.trim()) {
312
707
  return "reportedBy.organization is required";
313
708
  }
314
- if (typeof by.date !== "string" || !ISO_DAY_RE.test(by.date) || Number.isNaN(Date.parse(by.date))) {
709
+ if (typeof by.date !== "string" || !isRealDay(by.date)) {
315
710
  return "reportedBy.date must be a real day, YYYY-MM-DD";
316
711
  }
317
712
  if (by.role !== void 0 && typeof by.role !== "string") return "reportedBy.role must be text";
@@ -353,9 +748,15 @@ exports.PORTFOLIO_CONTRACT_VERSION = PORTFOLIO_CONTRACT_VERSION;
353
748
  exports.PORTFOLIO_KPI_JSON_SCHEMA = PORTFOLIO_KPI_JSON_SCHEMA;
354
749
  exports.PORTFOLIO_SECTION_ORDER = PORTFOLIO_SECTION_ORDER;
355
750
  exports.PORTFOLIO_SECTION_TYPES = PORTFOLIO_SECTION_TYPES;
751
+ exports.PORTFOLIO_VERIFICATION_JSON_SCHEMA = PORTFOLIO_VERIFICATION_JSON_SCHEMA;
356
752
  exports.SECTION_STAGE = SECTION_STAGE;
753
+ exports.STAGE_CORE_ALTERNATES = STAGE_CORE_ALTERNATES;
357
754
  exports.STAGE_CORE_SECTION = STAGE_CORE_SECTION;
358
755
  exports.STAGE_NAME_MAX_CHARS = STAGE_NAME_MAX_CHARS;
756
+ exports.VERIFICATION_CHECK_FIELDS = VERIFICATION_CHECK_FIELDS;
757
+ exports.VERIFICATION_EARLIEST_DAY = VERIFICATION_EARLIEST_DAY;
758
+ exports.VERIFICATION_FIELD_MAX_CHARS = VERIFICATION_FIELD_MAX_CHARS;
759
+ exports.VERIFICATION_MAX_CHECKS = VERIFICATION_MAX_CHECKS;
359
760
  exports.carryReportedMetrics = carryReportedMetrics;
360
761
  exports.caseStudyFlowProblems = caseStudyFlowProblems;
361
762
  exports.caseStudyStageStatus = caseStudyStageStatus;
@@ -367,12 +768,20 @@ exports.isFrameReadyMessage = isFrameReadyMessage;
367
768
  exports.isHeadlineMetric = isHeadlineMetric;
368
769
  exports.isLighthouseKpi = isLighthouseKpi;
369
770
  exports.isMetricSource = isMetricSource;
771
+ exports.isPlaceholderText = isPlaceholderText;
370
772
  exports.isPortfolioSectionType = isPortfolioSectionType;
773
+ exports.isRealDay = isRealDay;
774
+ exports.isVacuousScope = isVacuousScope;
775
+ exports.latestVerificationDay = latestVerificationDay;
371
776
  exports.metricSourceLabel = metricSourceLabel;
372
777
  exports.metricSourceProblem = metricSourceProblem;
373
778
  exports.narrativeProblems = narrativeProblems;
374
779
  exports.orderSections = orderSections;
780
+ exports.readVerificationChecks = readVerificationChecks;
375
781
  exports.sectionStage = sectionStage;
376
782
  exports.shortAttribution = shortAttribution;
783
+ exports.stageCoreSections = stageCoreSections;
377
784
  exports.stageName = stageName;
378
785
  exports.stageNamesProblems = stageNamesProblems;
786
+ exports.verificationLink = verificationLink;
787
+ exports.verificationProblems = verificationProblems;