@sonordev/contracts 2.12.0 → 2.15.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,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
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.
7
+
8
+ ## 2.15.0
9
+
10
+ - 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
+ - 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
+
13
+ ## 2.14.0
14
+
15
+ - 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.
16
+ - 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.
17
+
18
+ ## 2.13.0
19
+
20
+ - 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.
21
+
3
22
  ## 2.12.0
4
23
 
5
24
  A minor release: `@sonordev/contracts/portfolio` now holds the structure of a case study's story. Every other entry is unchanged, and nothing already exported from `portfolio` changed.
@@ -7,7 +26,7 @@ A minor release: `@sonordev/contracts/portfolio` now holds the structure of a ca
7
26
  - **The case study arc.** `CASE_STUDY_STAGES` names the four beats every case study reads in (`problem`, `build`, `proof`, `outcome`), `CASE_STUDY_STAGE_COPY` gives each a generic name and the question it answers, `PORTFOLIO_SECTION_TYPES` is every section type in canonical reading order, and `SECTION_STAGE` / `sectionStage` say which stage each belongs to. The section list used to be kept separately by the Sonor API and agency-site-kit; this is now the one copy.
8
27
  - **Reading order.** `orderSections` always keeps the arc (hero, problem, build, proof, outcome, CTA) and keeps an agency's stored order only within a stage, so a reordered case study can no longer put its results above its challenges. `groupSectionsByStage` returns the beats a case study actually has, numbered in order. `PORTFOLIO_SECTION_ORDER` is derived from the list. Performance now reads before Results: the proof comes before the outcome.
9
28
  - **Stage status.** `caseStudyStageStatus` reports all four stages, complete only with the stage's core section (`STAGE_CORE_SECTION`), with one sentence on what's missing.
10
- - **Stage names.** An agency can call the four stages what it likes ("Heat", "Shape", "Temper", "Finish"); `CaseStudyStageNames` is that map, `stageName(names, stage)` reads it with the generic name as the fallback, `stageNamesProblems` validates it and `STAGE_NAME_MAX_CHARS` is the length limit. The ids never change; only what people see does.
29
+ - **Stage names.** A site can call the four stages what it likes ("Heat", "Shape", "Temper", "Finish"); `CaseStudyStageNames` is that map, `stageName(names, stage)` reads it with the generic name as the fallback, `stageNamesProblems` validates it and `STAGE_NAME_MAX_CHARS` is the length limit. The ids never change; only what people see does.
11
30
  - **The narrative.** `CaseStudyNarrative` is a case study's thesis and one lede per stage, written for the project rather than borrowed from the house. `narrativeProblems` validates one, `NARRATIVE_MAX_CHARS` is the length limit and `CASE_STUDY_NARRATIVE_JSON_SCHEMA` the same rule as a schema. An absent narrative passes.
12
31
  - **The thread.** A challenge carries an `id` (`challengeId(n)`), a phase `resolves` challenges and a result `proves` them; `caseStudyFlowProblems` names every challenge nothing resolves or proves and every dangling reference, and passes content written before the thread existed.
13
32
 
package/README.md CHANGED
@@ -18,7 +18,7 @@ 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 |
@@ -38,6 +38,17 @@ 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 |
43
+
44
+ ## Focus closing artwork
45
+
46
+ `publication-branding` accepts `editorial.focusClosingBackground` at publication
47
+ and platform levels. `planned_photo` keeps the planned closing image;
48
+ `article_atmosphere` reuses the article's saved atmosphere with its real author
49
+ portrait. Omitting the field keeps existing behavior. A patch value of `null`
50
+ clears the override and restores inheritance. The article-atmosphere mode requires
51
+ both saved sources; it doesn't invent a portrait or substitute unrelated artwork.
41
52
 
42
53
  ## Public URL hosts
43
54
 
@@ -54,6 +65,31 @@ isn't proof a destination is public: the fetcher still validates the protocol,
54
65
  credentials and port, checks the DNS addresses it connects to, and applies its
55
66
  redirect policy on each hop. Those network protections stay in the APIs.
56
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
+
57
93
  ## Schema placeholders
58
94
 
59
95
  `schema-placeholders` finds the parts of a JSON-LD value that a template left
@@ -785,6 +821,14 @@ branding.mergePublicationBrandingPatch(saved, { identity: { logos: { dark: null
785
821
  `{ version: 1 }`, and a change that sets nothing returns the profile as it
786
822
  was. **`mergePublicationBrandingLayers`** is the same leaf-wise merge for
787
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.
788
832
  - **Refusals name the field.** A `PublicationBrandingValidationError` carries
789
833
  the path (`publication_branding.platforms.email.colors.primary`) and says
790
834
  what's allowed. A charter set on a channel override is a plain `Error`: a
@@ -829,10 +873,10 @@ the dashboard that edits it and every site that renders it.
829
873
  only when the stage's core section (`STAGE_CORE_SECTION`: challenges,
830
874
  strategy, performance, results) is present, with one sentence on what's
831
875
  missing. The dashboard shows it beside the editor.
832
- - **Stage names.** The ids are fixed; what people see isn't. An agency
833
- names the four stages once in the dashboard ("Heat", "Shape", "Temper",
834
- "Finish") and `CaseStudyStageNames` carries that map to the editor, the
835
- public portfolio config and the agency's site. `stageName(names, stage)`
876
+ - **Stage names.** The ids are fixed; what people see isn't. A site names
877
+ the four stages in the dashboard ("Heat", "Shape", "Temper", "Finish"),
878
+ per project like the rest of its branding, and `CaseStudyStageNames`
879
+ carries that map to the editor, the public portfolio config and the site. `stageName(names, stage)`
836
880
  reads it with the generic name as the fallback; `stageNamesProblems`
837
881
  validates it and `STAGE_NAME_MAX_CHARS` caps each name.
838
882
  - **`CaseStudyNarrative`** is what the story says above its sections: one
@@ -873,3 +917,15 @@ a shape bumps the major and that contract's own `*_VERSION` constant.
873
917
  ### Editorial media
874
918
 
875
919
  `@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.
920
+
921
+ ## The funnels contract
922
+
923
+ 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.
924
+
925
+ 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.
926
+
927
+ `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.
928
+
929
+ `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.
930
+
931
+ `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,2 @@
1
+ /** Single source of truth for campaign identities across sites, APIs and funnels. */
2
+ export declare function normalizeCampaignSlug(input?: string | null): string | null;