@sonordev/contracts 2.13.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 +23 -0
- package/README.md +149 -4
- package/dist/campaign-normalize.d.ts +2 -0
- package/dist/frame-ready.d.ts +23 -0
- package/dist/funnels.d.ts +576 -0
- package/dist/funnels.js +617 -0
- package/dist/funnels.mjs +560 -0
- package/dist/inbox.d.ts +42 -0
- package/dist/inbox.js +14 -0
- package/dist/inbox.mjs +9 -0
- package/dist/llms.d.ts +200 -0
- package/dist/llms.js +1002 -8
- package/dist/llms.mjs +989 -9
- package/dist/portfolio.d.ts +197 -14
- package/dist/portfolio.js +421 -12
- package/dist/portfolio.mjs +408 -13
- package/dist/publication-branding.d.ts +6 -1
- package/dist/publication-branding.js +1 -1
- package/dist/publication-branding.mjs +1 -1
- package/dist/site-cache.d.ts +2 -0
- package/dist/site-cache.js +3 -1
- package/dist/site-cache.mjs +3 -1
- package/package.json +13 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.16.0
|
|
4
|
+
|
|
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.
|
|
12
|
+
|
|
13
|
+
## 2.15.0
|
|
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
|
+
|
|
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.
|
|
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.
|
|
20
|
+
|
|
21
|
+
## 2.14.0
|
|
22
|
+
|
|
23
|
+
- Publication branding adds optional `identity.squareLogoUrl` and `identity.fullLogoUrl` for a square publication mark and its full/display logo. Both use the existing URL validation and support `null` in patches to clear an override. Theme variants in `identity.logos`, the text `identity.wordmark`, and publisher attribution remain separate. Existing saved profiles keep their shape and version.
|
|
24
|
+
- Add `@sonordev/contracts/inbox`: the one rule for which inbox cards are decisions that "Mark all read" and "Dismiss all" leave alone. `isDecisionCard(card)` counts an approval, the ledger's unconfirmed-action card and any card carrying a proposal; `DECISION_INSIGHT_TYPES`, `APPROVAL_NEEDED_INSIGHT_TYPE` and `LEDGER_UNCONFIRMED_INSIGHT_TYPE` are the types behind it. The Signal API and the dashboard read the same rule, so a bulk action's preview matches what it writes.
|
|
25
|
+
|
|
3
26
|
## 2.13.0
|
|
4
27
|
|
|
5
28
|
- Publication branding adds `editorial.focusClosingBackground`, with `planned_photo` and `article_atmosphere` choices. The setting inherits through publication and platform layers, and supports clearing an override. Article atmosphere closing frames reuse saved artwork and the real author portrait; other publications retain their existing behavior.
|
package/README.md
CHANGED
|
@@ -18,14 +18,14 @@ pnpm add @sonordev/contracts
|
|
|
18
18
|
| `@sonordev/contracts/sites` | The canonical shape of a site host (the multi-site `site` dimension) | every ingest and read that filters by site |
|
|
19
19
|
| `@sonordev/contracts/seo-pages` | Which `seo_pages` row a path resolves to on a multi-site project | the Sonor API, Signal, site-kit's sitemap sync |
|
|
20
20
|
| `@sonordev/contracts/seo-meta` | Whether a title or description is fit to ship | site-kit, Signal's SEO gate |
|
|
21
|
-
| `@sonordev/contracts/llms` | llms.txt sanitizers, the word-safe cut they use (`clipAtWordBoundary`), and the contract version | site-kit, the Sonor API, Signal's schema validation |
|
|
21
|
+
| `@sonordev/contracts/llms` | llms.txt sanitizers, the word-safe cut they use (`clipAtWordBoundary`), what each page line says (`pickLlmsPageDescription`: summary, curated meta, served meta, schema description, then a cleaned scrape; `llmsPageTitle`, `isLlmsListablePage`, `groupLlmsPages`, `selectLlmsPages`) and the contract version | site-kit, the Sonor API, Signal's schema validation |
|
|
22
22
|
| `@sonordev/contracts/forms` | The honeypot policy | site-kit forms, the Sonor API's form intake |
|
|
23
23
|
| `@sonordev/contracts/fleet` | The fleet heartbeat's wire shape | site-kit, the Sonor API |
|
|
24
24
|
| `@sonordev/contracts/slots` | Slot payload signing, contract v2 (HMAC; Node only). Signs and verifies v1 for older site-kit releases | the Sonor API, site-kit on the server |
|
|
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 |
|
|
@@ -38,6 +38,8 @@ pnpm add @sonordev/contracts
|
|
|
38
38
|
| `@sonordev/contracts/broadcast-layouts` | Which Broadcast card layouts are drawn (centered `headline`, low `anchor`), which retired IDs still read and what each draws as (`normalizeArticleCoverLayout`), the Broadcast styles, and the review compositions with the frozen draw that gives each review its own (`reviewCardVariationFromDigest`) | the Sonor API's card renderers and settings, Signal's agent tools, the dashboard's pickers |
|
|
39
39
|
| `@sonordev/contracts/publication-branding` | What a publication's branding may hold and what a change may set (`createPublicationBrandingValidator`), how a change merges, the editorial charter, and safe font stacks | the Sonor API stores and resolves it, Signal checks an agent's change before filing it, the dashboard edits it |
|
|
40
40
|
| `@sonordev/contracts/public-url-host` | Which literal hosts are refused as public fetch destinations (`isBlockedPublicUrlHost`) | the Sonor API and Signal's URL validators and network fetchers |
|
|
41
|
+
| `@sonordev/contracts/funnels` | Incomplete drafts, launch definitions, command/readiness/receipt schemas, bounded public pages and presentation-only brand overrides | Sites, APIs, UI and agent adapters share the same rules |
|
|
42
|
+
| `@sonordev/contracts/inbox` | Which inbox cards are decisions a bulk action leaves alone (`isDecisionCard`, `DECISION_INSIGHT_TYPES`) | Signal's inbox decides what "Mark all read" and "Dismiss all" may touch, the dashboard previews the same result before the write |
|
|
41
43
|
|
|
42
44
|
## Focus closing artwork
|
|
43
45
|
|
|
@@ -63,6 +65,31 @@ isn't proof a destination is public: the fetcher still validates the protocol,
|
|
|
63
65
|
credentials and port, checks the DNS addresses it connects to, and applies its
|
|
64
66
|
redirect policy on each hop. Those network protections stay in the APIs.
|
|
65
67
|
|
|
68
|
+
## Inbox decisions
|
|
69
|
+
|
|
70
|
+
"Mark all read" and "Dismiss all" never touch a decision, so clearing a backlog
|
|
71
|
+
of notices can't bury an approval waiting on a yes. `isDecisionCard(card)` is
|
|
72
|
+
that rule: the Signal API follows it when it writes, and the dashboard follows
|
|
73
|
+
it to show the result before the write, so the two can't drift apart.
|
|
74
|
+
|
|
75
|
+
A card is a decision when it is:
|
|
76
|
+
|
|
77
|
+
- an approval (insight type `approval_needed`, `APPROVAL_NEEDED_INSIGHT_TYPE`);
|
|
78
|
+
- the ledger's unconfirmed-action card (`ledger_unconfirmed_action`,
|
|
79
|
+
`LEDGER_UNCONFIRMED_INSIGHT_TYPE`): Signal started something it never saw
|
|
80
|
+
finish, so a person has to check it. It's raised once per claim, so a bulk
|
|
81
|
+
dismissal that took it would leave nobody told. It carries no proposal, so
|
|
82
|
+
there's nothing to approve or reject on it; or
|
|
83
|
+
- any card carrying a ledger proposal (`data.proposal_id` set).
|
|
84
|
+
|
|
85
|
+
`DECISION_INSIGHT_TYPES` is the first two, for the callers that need them as a
|
|
86
|
+
list (a database filter that leaves them out of a bulk write, for one).
|
|
87
|
+
`isDecisionCard` takes any object with an `insight_type` string and an optional
|
|
88
|
+
`data` object, so a stored row and the dashboard's inbox item both fit. It reads
|
|
89
|
+
`data.proposal_id` the way the database reads `data->>proposal_id`: a missing
|
|
90
|
+
key, a JSON `null` and a `null` `data` mean no proposal, and any other value,
|
|
91
|
+
an empty string included, means one.
|
|
92
|
+
|
|
66
93
|
## Schema placeholders
|
|
67
94
|
|
|
68
95
|
`schema-placeholders` finds the parts of a JSON-LD value that a template left
|
|
@@ -794,6 +821,14 @@ branding.mergePublicationBrandingPatch(saved, { identity: { logos: { dark: null
|
|
|
794
821
|
`{ version: 1 }`, and a change that sets nothing returns the profile as it
|
|
795
822
|
was. **`mergePublicationBrandingLayers`** is the same leaf-wise merge for
|
|
796
823
|
resolving layers.
|
|
824
|
+
- **Publication logo roles** are optional `identity.squareLogoUrl` and
|
|
825
|
+
`identity.fullLogoUrl`. Use the first for a square publication mark and the
|
|
826
|
+
second for its full/display logo. Both follow the same URL policy as other
|
|
827
|
+
logo fields. Set either leaf to `null` in a patch to clear that override
|
|
828
|
+
and restore inheritance. `identity.logos.default/light/dark` remain theme
|
|
829
|
+
variants, `identity.wordmark` stays text, and `publisherLogoUrl` is publisher
|
|
830
|
+
attribution. Omitting the new leaves preserves existing profiles; the
|
|
831
|
+
validator doesn't infer a square mark or select an effective display logo.
|
|
797
832
|
- **Refusals name the field.** A `PublicationBrandingValidationError` carries
|
|
798
833
|
the path (`publication_branding.platforms.email.colors.primary`) and says
|
|
799
834
|
what's allowed. A charter set on a channel override is a plain `Error`: a
|
|
@@ -836,8 +871,12 @@ the dashboard that edits it and every site that renders it.
|
|
|
836
871
|
with its sections; a stage with nothing in it is left out.
|
|
837
872
|
- **`caseStudyStageStatus(sections)`** reports all four stages, complete
|
|
838
873
|
only when the stage's core section (`STAGE_CORE_SECTION`: challenges,
|
|
839
|
-
strategy, performance, results)
|
|
840
|
-
|
|
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.
|
|
841
880
|
- **Stage names.** The ids are fixed; what people see isn't. A site names
|
|
842
881
|
the four stages in the dashboard ("Heat", "Shape", "Temper", "Finish"),
|
|
843
882
|
per project like the rest of its branding, and `CaseStudyStageNames`
|
|
@@ -856,6 +895,100 @@ the dashboard that edits it and every site that renders it.
|
|
|
856
895
|
phases, results })` names every challenge nothing resolves or proves and
|
|
857
896
|
every dangling reference. It judges links only where ids were written, so
|
|
858
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.
|
|
859
992
|
|
|
860
993
|
## Why it exists
|
|
861
994
|
|
|
@@ -882,3 +1015,15 @@ a shape bumps the major and that contract's own `*_VERSION` constant.
|
|
|
882
1015
|
### Editorial media
|
|
883
1016
|
|
|
884
1017
|
`@sonordev/contracts/editorial-media` exports `resolveArticlePhotoPlacements(html, photos)`, returning section targets and a status/offset for each explicit placement. Both the editor and article renderer use this contract; a heading with no eligible prose is reported as `no_paragraph`, and missing/ambiguous anchors never drift to another section. `editorialTextId` creates content identities for explicit takeaway assignments. Source-image format, size and completed-art exclusions are shared by the picker and resolver.
|
|
1018
|
+
|
|
1019
|
+
## The funnels contract
|
|
1020
|
+
|
|
1021
|
+
Import from `@sonordev/contracts/funnels`. `createFunnelDraft(template)` provides neutral defaults for `newsletter`, `requested_guide` and `inquiry`. `parseFunnelDraftDefinition` accepts bounded incomplete drafts so each setup step can save. `parseFunnelDefinition` requires a launch-ready structural definition. Resource ownership, current permission, route installation and launch authorization remain server checks.
|
|
1022
|
+
|
|
1023
|
+
Use `parseFunnelCreateArgs`, `parseFunnelUpdateArgs`, `parseFunnelRevisionArgs`, `parseFunnelLaunchArgs`, `parseFunnelLifecycleArgs` and `parseFunnelRetryArgs` at command boundaries. `parseFunnelDraftArgs(value, mode)` requires an explicit create/update mode. Rollback uses `parseFunnelLifecycleArgs(value, 'rollback')`. Matching exported JSON schemas describe the closed structural shapes; runtime parsers also enforce semantic checks such as matching form references and managed paths.
|
|
1024
|
+
|
|
1025
|
+
`applyFunnelBrandOverride` takes an already resolved brand, an allowlisted presentation patch and an API-authorized asset map. Null leaves restore inheritance. It returns the resulting brand, overridden field paths and asset references. It doesn't fetch assets, resolve business precedence or calculate authoritative hashes. `parseFunnelBrandSnapshot` validates the captured presentation and provenance.
|
|
1026
|
+
|
|
1027
|
+
`parsePublishedFunnelPage` refuses private definition fields and unsafe asset URLs. It supports bounded text paragraphs and bullet lists, with a managed form slug and canonical campaign identity. `managedFunnelPath` reserves `/f/:slug`; a consuming site must opt in and verify its actual routes.
|
|
1028
|
+
|
|
1029
|
+
`parseFunnelReadiness`, `parseFunnelOperationReceipt` and the detail/catalog/preview parsers validate bounded response envelopes. Operation receipts bind the exact command target and approved action, and always report zero messages sent by the configuration command. Preview and published-version detail parsers preserve the exact launch or rollback evidence.
|
|
@@ -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;
|