@sonordev/contracts 2.15.0 → 2.16.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/CHANGELOG.md CHANGED
@@ -1,12 +1,20 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 2.16.0
4
4
 
5
- - `llms` holds the rules for what llms.txt says about each page, so the Sonor API and site-kit's local generator build page lines the same way. New: `pickLlmsPageDescription` and `resolveLlmsPageDescription` (summary, curated meta, served meta, schema description, then a cleaned scrape), `llmsPageListNote`, `llmsPageTitle`, `isLlmsListablePage`, `selectLlmsPages`, `groupLlmsPages`, `llmsBlockquoteSummary`, `llmsContactLines`, `llmsAbsoluteUrl`, `cleanScrapedPageText`, `stripContactDetails`, `stripPageFiller` and `LLMS_DESCRIPTION_MAX_LENGTH`.
6
- - `sanitizeLlmsPublicSummary` removes a phone number or email address, with the "call" or "email us at" in front of it, and a "This page covers" lead-in, and keeps the rest. It used to return `null` for any summary that mentioned a phone or email, and a page with no summary is described by its scrape.
5
+ A minor release: `@sonordev/contracts/portfolio` gains the verification, a proof-stage section for builds a Lighthouse run can't honestly score. Every other entry is unchanged. Nothing was removed or renamed, but three things behave differently. `PortfolioSectionType` has a new member, so a `Record<PortfolioSectionType, ...>` you keep needs an entry for `portfolioVerification` before it type-checks. `caseStudyStageStatus` reads each section's `data`. A reported number's date has to be a real day.
6
+
7
+ - **A new section type, `portfolioVerification`.** An app behind a sign-in has a public door and little else a browser can reach, and a Lighthouse score for the door reads as a score for the app. A verification is a short list of checks instead (one to eight). Each says what it's called (`label`), what came out (`result`), what was measured (`measured`), where (`where`: a URL, a screen or a file), with which tool and settings (`method`), on what day (`date`, YYYY-MM-DD) and, required, what it doesn't cover (`scope`). A check that can't state its limits isn't a check. Every check is something you ran yourself; a figure a client gave you stays a reported number in Results.
8
+ - **It sits in the proof stage**, after `portfolioPerformance` and `portfolioSpeedComparison`. `PORTFOLIO_SECTION_TYPES` grows by one entry, so the `PORTFOLIO_SECTION_ORDER` value of each outcome section and the CTA rises by one. The reading order itself is unchanged.
9
+ - **It completes the proof stage on its own, once it has something to show.** `caseStudyStageStatus` counts a verification toward the proof stage only when its `data` holds at least one check a page may show (`readVerificationChecks`). An empty verification, which is what an editor's Add button creates, leaves the stage incomplete and says so, instead of reading as a finished proof stage over an empty one. `SectionLike` gains an optional `data`; pass each section's `data`, because a caller that passes only `sectionType` sees a verification as empty. `STAGE_CORE_SECTION` still names Performance as the section to offer when a stage is empty; `STAGE_CORE_ALTERNATES` lists the sections that stand in for a stage's core and `stageCoreSections(stage)` returns the core and its stand-ins together. Nothing is gated on the kind of project, because the status doesn't know it. An empty proof stage now says a verification is the other way to fill it.
10
+ - **One rule for what a day is.** `isRealDay(value)` says whether a string is a real calendar day written YYYY-MM-DD. A verification's `date` and a reported number's `reportedBy.date` both ask it. The reported number's date used to go through `Date.parse`, which rolls `2026-02-31` into March and let it pass; it's now refused, so a number can't be attributed to a day that doesn't exist.
11
+ - **Validating and reading one.** `PortfolioVerificationData` and `VerificationCheck` are the shapes. `verificationProblems(data, { latestDay? })` lists what's wrong, one sentence each, with the check's position: a missing field, a field over its limit (`VERIFICATION_FIELD_MAX_CHARS`) or not one line of plain text (a control character, a zero-width or direction mark), more than `VERIFICATION_MAX_CHECKS` checks, a day that doesn't exist, a day before `VERIFICATION_EARLIEST_DAY` or after `latestVerificationDay()` (tomorrow in UTC, so a check run today in any time zone passes and one that hasn't happened doesn't; `readVerificationChecks` applies the same bound), a `where` that says nothing ("n/a", one plain word), carries a username or password, or names localhost or a private network, a field the check doesn't have, and a scope that doesn't state a limit (`isVacuousScope`): it has to be a sentence with a word that marks where coverage stops, such as "only", "not" or "without", so placeholders ("N/A", "None at all"), coverage claims ("All screens") and denied limits ("No restrictions") are refused. It reads English, and it can see that a limit was written, not that it's true, so a person still reads the scope. `isPlaceholderText(text)` is the test for a line with nothing in it ("n/a", "see above"), shared by `where` and `scope`. `readVerificationChecks(data)` returns the checks that can be shown, trimmed and in order, and leaves out any that fail, so a check with no scope line, or a placeholder for one, doesn't print. `verificationLink(where)` returns a plain `http` or `https` address with a public host as a link and everything else, localhost included, as null. `PORTFOLIO_VERIFICATION_JSON_SCHEMA` is the same shape as a schema.
7
12
 
8
13
  ## 2.15.0
9
14
 
15
+ - `llms` holds the rules for what llms.txt says about each page, so the Sonor API and site-kit's local generator build page lines the same way. New: `pickLlmsPageDescription` and `resolveLlmsPageDescription` (summary, curated meta, served meta, schema description, then a cleaned scrape), `llmsPageListNote`, `llmsPageTitle`, `isLlmsListablePage`, `selectLlmsPages`, `groupLlmsPages`, `llmsBlockquoteSummary`, `llmsContactLines`, `llmsAbsoluteUrl`, `cleanScrapedPageText`, `stripContactDetails`, `stripPageFiller` and `LLMS_DESCRIPTION_MAX_LENGTH`.
16
+ - `sanitizeLlmsPublicSummary` removes a phone number or email address, with the "call" or "email us at" in front of it, and a "This page covers" lead-in, and keeps the rest. It used to return `null` for any summary that mentioned a phone or email, and a page with no summary is described by its scrape.
17
+
10
18
  - Add `@sonordev/contracts/funnels` with bounded incomplete drafts, launch-ready definitions, command envelopes, brand presentation overrides, readiness and public page validation. Runtime parsers and JSON schemas share the same shapes.
11
19
  - Add the canonical funnel content cache tag and campaign normalization used by managed landing pages. Existing publication logo roles and inbox behavior remain available.
12
20
 
package/README.md CHANGED
@@ -25,7 +25,7 @@ pnpm add @sonordev/contracts
25
25
  | `@sonordev/contracts/slot-content` | What a slot of each kind may hold (`normalizeSlotValue`), safe links, the rich-text parser. No `crypto`, so browsers can import it | the Sonor API, site-kit, the dashboard editor |
26
26
  | `@sonordev/contracts/site-cache` | The cache tags a site's Sonor fetches carry and the revalidation route's path, payload and ping | site-kit tags and serves the route, the Sonor API calls it |
27
27
  | `@sonordev/contracts/site-edit` | Edit on page: the messages between a framed site and the dashboard, trusted origins, how a slot is found on the page | site-kit's overlay, the dashboard's visual editor |
28
- | `@sonordev/contracts/portfolio` | The case study arc: every section type, the stage each belongs to, the reading order, the narrative that opens each stage and the thread from a challenge to the phase that cleared it and the result that proves it. Also where a portfolio number comes from and which may headline, and the live-frame message | the Sonor API, Signal's case-study writer, the dashboard's editor, agency-site-kit |
28
+ | `@sonordev/contracts/portfolio` | The case study arc: every section type, the stage each belongs to, the reading order, the narrative that opens each stage and the thread from a challenge to the phase that cleared it and the result that proves it. The verification, the proof for a build a Lighthouse run can't score: checks that each state what they don't cover. Also where a portfolio number comes from and which may headline, and the live-frame message | the Sonor API, Signal's case-study writer, the dashboard's editor, agency-site-kit |
29
29
  | `@sonordev/contracts/proposal-sitemap` | How a proposal's site plan counts its pages, when its before-and-after comparison may show, and whether the page counts stated elsewhere in the proposal add up | the Sonor API, Signal's proposal writer, the dashboard's proposal pages |
30
30
  | `@sonordev/contracts/voice` | The house voice's contraction rule. `findUncontracted` and `contractText` find and apply full forms that should be contractions; `findContracted` finds the contractions a question-form heading should expand | anything that checks or rewrites copy: Sonor's APIs and dashboard |
31
31
  | `@sonordev/contracts/error-page` | Whether stored text is a rendered error page rather than a page's content, and whether generated copy says the page itself is missing | anything that stores page text or writes page copy from it: the Sonor API's page-view and sitemap writers, Signal's metadata generators |
@@ -871,8 +871,12 @@ the dashboard that edits it and every site that renders it.
871
871
  with its sections; a stage with nothing in it is left out.
872
872
  - **`caseStudyStageStatus(sections)`** reports all four stages, complete
873
873
  only when the stage's core section (`STAGE_CORE_SECTION`: challenges,
874
- strategy, performance, results) is present, with one sentence on what's
875
- missing. The dashboard shows it beside the editor.
874
+ strategy, performance, results) or a stand-in for it
875
+ (`STAGE_CORE_ALTERNATES`: a verification completes the proof stage) is
876
+ present, with one sentence on what's missing. A stand-in counts only when
877
+ its `data` holds something a page can show, so pass each section's `data`
878
+ along with its `sectionType`. `stageCoreSections(stage)` lists the core and
879
+ its stand-ins. The dashboard shows the status beside the editor.
876
880
  - **Stage names.** The ids are fixed; what people see isn't. A site names
877
881
  the four stages in the dashboard ("Heat", "Shape", "Temper", "Finish"),
878
882
  per project like the rest of its branding, and `CaseStudyStageNames`
@@ -891,6 +895,100 @@ the dashboard that edits it and every site that renders it.
891
895
  phases, results })` names every challenge nothing resolves or proves and
892
896
  every dangling reference. It judges links only where ids were written, so
893
897
  content from before the thread passes.
898
+ - **`isRealDay(value)`** is the one rule for whether a string is a day: a real
899
+ calendar day written YYYY-MM-DD. `2026-02-31` isn't one, and neither is a
900
+ year under 100. A verification's `date` and a reported number's
901
+ `reportedBy.date` both ask it, and so can anything else that stores a day.
902
+
903
+ ### The verification
904
+
905
+ A Lighthouse run can only score what a browser reaches without signing in. For
906
+ an app whose screens sit behind a login, that's the sign-in page, and a score
907
+ for the door reads as a score for the app. The `portfolioVerification` section
908
+ is the proof stage for that build: a short list of checks (one to
909
+ `VERIFICATION_MAX_CHECKS`, eight), each stating what it covers and, required,
910
+ what it doesn't.
911
+
912
+ ```ts
913
+ import {
914
+ readVerificationChecks,
915
+ verificationLink,
916
+ verificationProblems,
917
+ type PortfolioVerificationData,
918
+ } from '@sonordev/contracts/portfolio'
919
+
920
+ const data: PortfolioVerificationData = {
921
+ checks: [
922
+ {
923
+ label: 'Sign-in page, mobile speed',
924
+ result: 'Performance 89',
925
+ measured: 'The Lighthouse performance score of the page a visitor sees before signing in',
926
+ where: 'https://example.com/login',
927
+ method: 'Lighthouse 13, mobile preset, median of 3 runs',
928
+ date: '2026-10-20',
929
+ scope: 'The sign-in page only; the screens behind it were not measured.',
930
+ },
931
+ ],
932
+ }
933
+
934
+ verificationProblems(data) // []
935
+ readVerificationChecks(data) // the checks a page may print
936
+ verificationLink(data.checks[0].where) // 'https://example.com/login'
937
+ ```
938
+
939
+ - **Every field is required**, and each is one line of plain text within its
940
+ limit (`VERIFICATION_FIELD_MAX_CHARS`): no line break, tab or other control
941
+ character, and no zero-width or direction mark. `date` is a real day, YYYY-MM-DD, no
942
+ earlier than `VERIFICATION_EARLIEST_DAY` (2000-01-01) and no later than
943
+ `latestVerificationDay()`, which is tomorrow in UTC. That lets a check run
944
+ today in a time zone ahead of your server through and refuses one that hasn't
945
+ happened. `verificationProblems` and `readVerificationChecks` both apply it
946
+ without being asked; pass `{ latestDay }` to fix the clock.
947
+ - **`where` points somewhere a reader can go or look.** A public web address,
948
+ or a screen or file named in words ("the migrations in the repository") or as
949
+ a path. A placeholder ("n/a", "here"; `isPlaceholderText` is the test), one
950
+ plain word, a username or password
951
+ in an address, and localhost or a private network are refused. One thing no
952
+ rule can catch is a view link with a token in its path or query: if the link
953
+ works for whoever holds it, don't put it here.
954
+ - **`scope` states a limit.** It's what the check does _not_ cover, written as a
955
+ sentence with a word that marks where coverage stops: "only", "not", "except",
956
+ "without", or a contraction like "weren't". A placeholder ("N/A", "none",
957
+ "TBD"), a coverage claim ("All screens", "Fully tested") and a denied limit
958
+ ("No restrictions") are refused (`isVacuousScope`). "Desktop only" and
959
+ "Counted, not exercised" pass. Write what the check leaves out: "Counted from
960
+ the migrations; no query ran as a signed-in user." The test reads English, and
961
+ it can see that a limit was written, not that it's true, so a person still
962
+ reads the scope before a case study goes live.
963
+ - **`verificationProblems(data, options)`** names every problem with the check's
964
+ position (`checks[1].scope is required: say what the check does not cover`),
965
+ and refuses a field a check doesn't have. Whatever stores a verification
966
+ should run it first. Nothing calls it for you, and until a write path does,
967
+ stored data can hold anything, which is why `readVerificationChecks` checks
968
+ again before a page prints.
969
+ - **`readVerificationChecks(data)`** is what a page prints: the checks that pass,
970
+ trimmed and in order, up to the limit. A check that fails is left out, so one
971
+ with no scope line, or a placeholder for one, doesn't print, and a field a
972
+ newer writer added is ignored.
973
+ - **`verificationLink(where)`** returns the address when `where` is exactly one
974
+ `http` or `https` URL with no credentials in it and a public host, and null
975
+ for a screen name, a file path, localhost or anything else, so only a real
976
+ public address becomes a link.
977
+ - **Every check is something you ran.** There's no `source` on a check. A figure
978
+ the client gave you stays in Results as a reported number, with the person
979
+ behind it.
980
+ - **`PORTFOLIO_VERIFICATION_JSON_SCHEMA`** is the same shape for agents. What a
981
+ `scope` or a `where` says, and whether a `date` is a real day in range, are
982
+ `verificationProblems`' job; a schema can't express them.
983
+
984
+ It completes the proof stage on its own (`STAGE_CORE_ALTERNATES`), once it has
985
+ at least one check `readVerificationChecks` would show. An empty verification,
986
+ which is what an editor's Add button creates, leaves the stage incomplete and
987
+ says why; `caseStudyStageStatus` reads that from each section's `data`, and a
988
+ caller that passes only `sectionType` sees a verification as empty. Any project
989
+ can use it. The status doesn't know the kind of project, so nothing is gated on
990
+ it; a site that wants a Lighthouse score for a public site keeps writing a
991
+ Performance section.
894
992
 
895
993
  ## Why it exists
896
994
 
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The live-frame handshake, on its own so it stays small.
3
+ *
4
+ * A site-kit page framed by another origin posts FRAME_READY_MESSAGE to its
5
+ * embedder once it has painted (shared/frame.ts, announceFrameReady); a case
6
+ * study's device frames (the portfolio renderers) are the ones listening.
7
+ *
8
+ * This is a leaf module on purpose. shared/frame.ts runs in every page's
9
+ * analytics and layout graph, and `./portfolio` is a large module of case
10
+ * study rules: importing the message from there put all of it in every
11
+ * page's eager bundle. Single source of truth for the message: `./portfolio`
12
+ * re-exports it (so `@sonordev/contracts/portfolio` and
13
+ * `@sonordev/site-kit/portfolio/contract` still carry it) and shared/frame.ts
14
+ * imports it from here. Keep it free of other imports.
15
+ */
16
+ /** The message a cross-origin-framed site-kit page posts to its embedder once it has painted. */
17
+ export declare const FRAME_READY_MESSAGE: 'sonor:frame-ready';
18
+ export interface FrameReadyMessage {
19
+ type: typeof FRAME_READY_MESSAGE;
20
+ v: 1;
21
+ }
22
+ /** True for a {@link FrameReadyMessage}, for the embedder's message handler. */
23
+ export declare function isFrameReadyMessage(data: unknown): data is FrameReadyMessage;
@@ -16,14 +16,7 @@
16
16
  * must carry, and which numbers may headline. The Sonor API applies the same
17
17
  * rules.
18
18
  */
19
- /** The message a cross-origin-framed site-kit page posts to its embedder once it has painted. */
20
- export declare const FRAME_READY_MESSAGE: 'sonor:frame-ready';
21
- export interface FrameReadyMessage {
22
- type: typeof FRAME_READY_MESSAGE;
23
- v: 1;
24
- }
25
- /** True for a {@link FrameReadyMessage}, for the embedder's message handler. */
26
- export declare function isFrameReadyMessage(data: unknown): data is FrameReadyMessage;
19
+ export { FRAME_READY_MESSAGE, isFrameReadyMessage, type FrameReadyMessage } from './frame-ready';
27
20
  /** Bump on breaking changes to the collapse policy. */
28
21
  export declare const PORTFOLIO_CONTRACT_VERSION: 1;
29
22
  /** The four beats, in reading order. */
@@ -57,7 +50,7 @@ export declare function stageName(names: CaseStudyStageNames | null | undefined,
57
50
  * sections of the story (SEO metadata, the closing CTA). This is THE list;
58
51
  * the Sonor API and agency-site-kit import it rather than keeping their own.
59
52
  */
60
- export declare const PORTFOLIO_SECTION_TYPES: readonly ['portfolioHero', 'portfolioChallenges', 'portfolioBeforeAfter', 'portfolioStrategy', 'portfolioSiteArchitecture', 'portfolioDesignSystem', 'portfolioFeatureSpotlight', 'portfolioGallery', 'portfolioVideo', 'portfolioTechStack', 'portfolioPerformance', 'portfolioSpeedComparison', 'portfolioResults', 'portfolioMetricsTimeline', 'portfolioConversionFunnel', 'portfolioServices', 'portfolioTeam', 'portfolioTestimonial', 'portfolioDetails', 'portfolioSeo', 'portfolioCTA'];
53
+ export declare const PORTFOLIO_SECTION_TYPES: readonly ['portfolioHero', 'portfolioChallenges', 'portfolioBeforeAfter', 'portfolioStrategy', 'portfolioSiteArchitecture', 'portfolioDesignSystem', 'portfolioFeatureSpotlight', 'portfolioGallery', 'portfolioVideo', 'portfolioTechStack', 'portfolioPerformance', 'portfolioSpeedComparison', 'portfolioVerification', 'portfolioResults', 'portfolioMetricsTimeline', 'portfolioConversionFunnel', 'portfolioServices', 'portfolioTeam', 'portfolioTestimonial', 'portfolioDetails', 'portfolioSeo', 'portfolioCTA'];
61
54
  export type PortfolioSectionType = (typeof PORTFOLIO_SECTION_TYPES)[number];
62
55
  export declare function isPortfolioSectionType(value: unknown): value is PortfolioSectionType;
63
56
  /**
@@ -70,20 +63,42 @@ export declare const SECTION_STAGE: Record<PortfolioSectionType, CaseStudyStage
70
63
  /** The stage a section type belongs to, or null when it sits outside the story. Unknown types are null. */
71
64
  export declare function sectionStage(type: string): CaseStudyStage | null;
72
65
  /**
73
- * The one section each stage can't do without. A stage with other sections
74
- * but not this one is told so by {@link caseStudyStageStatus}: a proof stage
75
- * that's only a speed comparison has no measurement of the finished build.
66
+ * The one section each stage can't do without, and the one an editor offers
67
+ * to add when the stage has nothing. A stage with other sections but not its
68
+ * core (or a stand-in, {@link STAGE_CORE_ALTERNATES}) is told so by
69
+ * {@link caseStudyStageStatus}: a proof stage that's only a speed comparison
70
+ * has no measurement of the finished build.
76
71
  */
77
72
  export declare const STAGE_CORE_SECTION: Record<CaseStudyStage, PortfolioSectionType>;
73
+ /**
74
+ * Sections that stand in for a stage's core. A Lighthouse run can only score
75
+ * what a browser can reach without signing in, so an app whose screens sit
76
+ * behind a login has no honest Performance section; its proof is a
77
+ * verification: checks that each say what they cover and what they don't.
78
+ * Either one completes the proof stage. This is deliberately not gated on
79
+ * the kind of project: the status below only sees section types, and the
80
+ * rule for "is this an app" lives with each caller's own data. A verification
81
+ * is a measurement of the finished build with its limits stated, which is
82
+ * what the stage asks for. Single source of truth for what completes a stage.
83
+ */
84
+ export declare const STAGE_CORE_ALTERNATES: Partial<Record<CaseStudyStage, readonly PortfolioSectionType[]>>;
85
+ /** Every section type that completes a stage: its core first, then its stand-ins. */
86
+ export declare function stageCoreSections(stage: CaseStudyStage): PortfolioSectionType[];
78
87
  /**
79
88
  * Canonical display order, derived from the list above so it can't disagree
80
89
  * with the stages. Renderers sort by this when nobody has reordered; the
81
90
  * dashboard's stored order only ever moves a section within its stage.
82
91
  */
83
92
  export declare const PORTFOLIO_SECTION_ORDER: Record<PortfolioSectionType, number>;
84
- /** The least a section needs for the ordering helpers: its type. Anything else rides along. */
93
+ /**
94
+ * The least a section needs for the ordering helpers: its type. Anything else
95
+ * rides along. `data` is optional for them and needed by
96
+ * {@link caseStudyStageStatus}, which won't count a stand-in section for a
97
+ * stage's core (a verification) unless its data shows something.
98
+ */
85
99
  export interface SectionLike {
86
100
  sectionType: string;
101
+ data?: unknown;
87
102
  }
88
103
  /**
89
104
  * The sections in the order the case study reads.
@@ -120,7 +135,11 @@ export interface CaseStudyStageStatus {
120
135
  stage: CaseStudyStage;
121
136
  /** The section types present, in reading order. */
122
137
  present: PortfolioSectionType[];
123
- /** True when the stage's core section ({@link STAGE_CORE_SECTION}) is present. */
138
+ /**
139
+ * True when the stage's core section ({@link STAGE_CORE_SECTION}) is present, or a stand-in for it
140
+ * ({@link STAGE_CORE_ALTERNATES}) is present with something a page can show. Pass each section's `data`:
141
+ * a stand-in without it doesn't count.
142
+ */
124
143
  complete: boolean;
125
144
  /** Null when complete; otherwise one sentence on what the stage is missing. */
126
145
  missing: string | null;
@@ -129,6 +148,10 @@ export interface CaseStudyStageStatus {
129
148
  * Each stage's status, all four, in order, whether or not the case study has
130
149
  * sections in it. The dashboard shows these beside the editor so an empty
131
150
  * proof stage is seen before the case study is published, not after.
151
+ *
152
+ * A stage's core section completes it by type. A stand-in for the core (a
153
+ * verification) completes it only when its `data` holds something a page can
154
+ * show, so pass `data`; without it a stand-in reads as empty, never as done.
132
155
  */
133
156
  export declare function caseStudyStageStatus(sections: readonly SectionLike[] | null | undefined): CaseStudyStageStatus[];
134
157
  export interface CaseStudyNarrative {
@@ -196,6 +219,166 @@ export declare function caseStudyFlowProblems(input: {
196
219
  phases?: readonly PhaseLike[] | null;
197
220
  results?: readonly ResultLike[] | null;
198
221
  }): string[];
222
+ /**
223
+ * True for a real calendar day written YYYY-MM-DD. "2026-02-31" is not (Date.parse
224
+ * rolls it into March), and neither is a year under 100 (Date.UTC maps those into
225
+ * the 1900s). Single source of truth for what counts as a day.
226
+ */
227
+ export declare function isRealDay(value: string): boolean;
228
+ /** The most checks one verification lists. It's a short list; the case study isn't an audit report. */
229
+ export declare const VERIFICATION_MAX_CHECKS = 8;
230
+ /** Every field of a check, in the order a renderer reads them. */
231
+ export declare const VERIFICATION_CHECK_FIELDS: readonly ['label', 'result', 'measured', 'where', 'method', 'date', 'scope'];
232
+ export type VerificationCheckField = (typeof VERIFICATION_CHECK_FIELDS)[number];
233
+ /** The longest each text field may run. Each is a line, not a paragraph. `date` is a day, not text. */
234
+ export declare const VERIFICATION_FIELD_MAX_CHARS: {
235
+ readonly label: 80;
236
+ readonly result: 120;
237
+ readonly measured: 200;
238
+ readonly where: 200;
239
+ readonly method: 240;
240
+ readonly scope: 240;
241
+ };
242
+ /** One thing we checked on the finished build. Every field is required. */
243
+ export interface VerificationCheck {
244
+ /** What the check is called: "Sign-in page, mobile speed". */
245
+ label: string;
246
+ /** What came out: "Performance 89", "42 of 42 tables have row-level security". */
247
+ result: string;
248
+ /** What was measured: "Lighthouse performance score of the page a visitor sees before signing in". */
249
+ measured: string;
250
+ /** Where: a URL, or the screen or file. "https://example.com/login", "the migrations in the repository". */
251
+ where: string;
252
+ /** The tool and settings: "Lighthouse 13, mobile preset, median of 3 runs". */
253
+ method: string;
254
+ /** The day it was checked, YYYY-MM-DD. */
255
+ date: string;
256
+ /** What the check does NOT cover, as a limit: "The sign-in page only; the screens behind it weren't measured." */
257
+ scope: string;
258
+ }
259
+ /** The `data` of a `portfolioVerification` section. */
260
+ export interface PortfolioVerificationData {
261
+ checks: VerificationCheck[];
262
+ }
263
+ /** The earliest day a check may carry. An earlier date is a typo, not a check of this build. */
264
+ export declare const VERIFICATION_EARLIEST_DAY = "2000-01-01";
265
+ /**
266
+ * The latest day a check may carry: tomorrow in UTC. A check run today anywhere
267
+ * on earth is dated no later than that (the furthest-ahead zone is UTC+14), so a
268
+ * server's clock can't drop a check dated "today" where the person ran it, and a
269
+ * date further out is a check that hasn't happened. Single source of truth for
270
+ * the allowance: writers and readers both use it, so no one keeps a second one.
271
+ * `now` is for tests and for callers with a clock of their own.
272
+ */
273
+ export declare function latestVerificationDay(now?: Date): string;
274
+ export interface VerificationOptions {
275
+ /**
276
+ * The latest day a check may carry, YYYY-MM-DD; a later one is a problem. Leave it out to use
277
+ * {@link latestVerificationDay}, which is what a page and a write path both want. Pass one for a fixed clock.
278
+ */
279
+ latestDay?: string;
280
+ }
281
+ /**
282
+ * True when a line says nothing: it has no words, or every word is a
283
+ * placeholder or filler ("N/A", "n/a n/a", "TBD", "see above", "none at all").
284
+ */
285
+ export declare function isPlaceholderText(text: string): boolean;
286
+ /**
287
+ * True when a scope line doesn't state a limit. A scope is "what this check
288
+ * does NOT cover", so it has to be a sentence that uses a word marking where
289
+ * coverage stops ("only", "not", "except", "without", "n't") and says
290
+ * something beyond that word. A coverage claim ("All screens", "Fully
291
+ * tested"), a placeholder ("N/A", "None at all", "Not applicable") and a
292
+ * denied limit ("No restrictions", "Nothing excluded") fail. "Desktop only",
293
+ * "Counted, not exercised" and "No queries ran as a signed-in user" pass.
294
+ *
295
+ * It reads English. It can't judge whether a limit is true or useful, only
296
+ * that one was written; a person still reads the scope.
297
+ */
298
+ export declare function isVacuousScope(scope: string): boolean;
299
+ /**
300
+ * What's wrong with a verification section's data, or an empty list. The
301
+ * section needs between one and {@link VERIFICATION_MAX_CHECKS} checks, and
302
+ * every field of every check is required, the scope most of all. Whatever
303
+ * stores a verification should run this first. The contract can't make it:
304
+ * until a write path calls it, stored data can hold anything, and
305
+ * {@link readVerificationChecks} is what keeps a bad check off the page.
306
+ */
307
+ export declare function verificationProblems(data: unknown, options?: VerificationOptions): string[];
308
+ /**
309
+ * The checks a renderer may show: the ones with every field present, within
310
+ * the limits, with a real day and a scope that states a limit, trimmed, in their
311
+ * stored order and no more than {@link VERIFICATION_MAX_CHECKS}. A check that
312
+ * fails is left out, so one with no scope line (or a placeholder for one) never
313
+ * prints. That's all it can promise: whether a scope is true is a person's call.
314
+ * Empty when the data is anything else. Single source of truth for what a case
315
+ * study prints from this section.
316
+ */
317
+ export declare function readVerificationChecks(data: unknown, options?: VerificationOptions): VerificationCheck[];
318
+ /**
319
+ * The link for a check's `where`: the address when it is exactly one http or
320
+ * https URL with no credentials in it and a public host, else null. A screen
321
+ * name or a file path is printed as text; nothing else is ever made into a link.
322
+ */
323
+ export declare function verificationLink(where: string): string | null;
324
+ /** Agent-native contract: JSON Schema for {@link PortfolioVerificationData}. What a scope or a where says, and whether a date is a real day in range, are checked by {@link verificationProblems}. */
325
+ export declare const PORTFOLIO_VERIFICATION_JSON_SCHEMA: {
326
+ readonly $schema: 'http://json-schema.org/draft-07/schema#';
327
+ readonly $id: 'https://sonor.io/schemas/portfolio-verification.v1.json';
328
+ readonly title: 'PortfolioVerificationData';
329
+ readonly 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.';
330
+ readonly type: 'object';
331
+ readonly required: readonly ['checks'];
332
+ readonly properties: {
333
+ readonly checks: {
334
+ readonly type: 'array';
335
+ readonly minItems: 1;
336
+ readonly maxItems: 8;
337
+ readonly items: {
338
+ readonly type: 'object';
339
+ readonly required: readonly ["label", "result", "measured", "where", "method", "date", "scope"];
340
+ readonly properties: {
341
+ readonly label: {
342
+ readonly type: 'string';
343
+ readonly minLength: 1;
344
+ readonly maxLength: 80;
345
+ };
346
+ readonly result: {
347
+ readonly type: 'string';
348
+ readonly minLength: 1;
349
+ readonly maxLength: 120;
350
+ };
351
+ readonly measured: {
352
+ readonly type: 'string';
353
+ readonly minLength: 1;
354
+ readonly maxLength: 200;
355
+ };
356
+ readonly where: {
357
+ readonly type: 'string';
358
+ readonly minLength: 1;
359
+ readonly maxLength: 200;
360
+ };
361
+ readonly method: {
362
+ readonly type: 'string';
363
+ readonly minLength: 1;
364
+ readonly maxLength: 240;
365
+ };
366
+ readonly date: {
367
+ readonly type: 'string';
368
+ readonly pattern: '^\\d{4}-\\d{2}-\\d{2}$';
369
+ };
370
+ readonly scope: {
371
+ readonly type: 'string';
372
+ readonly minLength: 1;
373
+ readonly maxLength: 240;
374
+ };
375
+ };
376
+ readonly additionalProperties: false;
377
+ };
378
+ };
379
+ };
380
+ readonly additionalProperties: false;
381
+ };
199
382
  /** Minimal KPI shape the policy reads — callers pass their own richer tiles. */
200
383
  export interface KpiLike {
201
384
  label: string;