@warpgogol/werkstatt-shared 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/package.json +21 -69
  2. package/src/integration/funnel.ts +78 -1
  3. package/src/integration/index.ts +19 -2
  4. package/src/integration/lagebild-ingress.ts +169 -0
  5. package/src/integration/orchestration.ts +7 -13
  6. package/src/integration/port-barrel.ts +9 -28
  7. package/src/integration/tests/funnel-bridge.test.ts +7 -7
  8. package/src/integration/tests/lagebild-ingress.test.ts +118 -0
  9. package/src/observability/typed-refs.ts +1 -1
  10. package/src/ontology/analytics/matomo-fleet.registry.yaml +1 -1
  11. package/src/ontology/archetypes/index.json +12 -9
  12. package/src/ontology/archetypes/index.yaml +26 -10
  13. package/src/ontology/blueprints/website-local.yaml +11 -11
  14. package/src/ontology/blueprints/website-service.yaml +20 -20
  15. package/src/ontology/capabilities/lead.submit.yaml +2 -2
  16. package/src/ontology/enums.ts +2 -2
  17. package/src/ontology/external-surfaces/jsonld-types.yaml +14 -14
  18. package/src/ontology/external-surfaces/url-schema.yaml +1 -1
  19. package/src/ontology/index.ts +2 -0
  20. package/src/ontology/schemas/index.ts +7 -2
  21. package/src/ontology/schemas/system/integrations.ts +1 -1
  22. package/src/ontology/schemas/system/manifest.ts +38 -1
  23. package/src/ontology/schemas/system.ts +7 -2
  24. package/src/ontology/shared-section-props/common.ts +11 -0
  25. package/src/ontology/shared-section-props/visual-header.ts +1 -1
  26. package/src/share/agent/api-catalog.ts +5 -0
  27. package/src/share/agent/ard-catalog.ts +14 -0
  28. package/src/share/agent/index.ts +2 -0
  29. package/src/share/agent/manifest.ts +25 -2
  30. package/src/share/agent/mcp-card.ts +4 -0
  31. package/src/share/agent/openapi.ts +78 -0
  32. package/src/share/agent/search.test.ts +188 -0
  33. package/src/share/agent/search.ts +164 -0
  34. package/src/share/env.d.ts +0 -4
  35. package/src/share/schemas/effects.ts +16 -0
  36. package/src/share/schemas/media.ts +4 -0
  37. package/src/share/schemas/section-shell.ts +2 -1
  38. package/src/share/scripts/external-link-qr.ts +9 -1
  39. package/src/share/scripts/gsap-counter.ts +2 -0
  40. package/src/share/scripts/inline-number-animation.ts +2 -0
  41. package/src/share/scripts/live-photos.ts +2 -0
  42. package/src/share/scripts/video-player.test.ts +210 -0
  43. package/src/share/scripts/video-player.ts +73 -13
  44. package/src/share/semantic/extract.ts +25 -1
  45. package/src/share/semantic/tests/split-sentences.test.ts +57 -0
  46. package/src/share/tests/manifest.test.ts +51 -0
  47. package/src/share/tests/openapi.test.ts +30 -0
  48. package/src/share/types/qrcode-browser.d.ts +18 -0
  49. package/src/share/utility-registry.yaml +2 -2
  50. package/src/integration/crm-buffer.ts +0 -491
  51. package/src/integration-adapter-supabase-crm/adapter.ts +0 -437
  52. package/src/integration-adapter-supabase-crm/client.ts +0 -605
  53. package/src/integration-adapter-supabase-crm/index.ts +0 -28
  54. package/src/integration-adapter-supabase-crm/pipedrive-sync-target.ts +0 -482
  55. package/src/integration-adapter-supabase-crm/supabase-rest.ts +0 -89
  56. package/src/integration-adapter-supabase-crm/tenant-registry.ts +0 -334
  57. package/src/integration-adapter-supabase-crm/tests/change-balance.pbt.test.ts +0 -83
  58. package/src/integration-adapter-supabase-crm/tests/funnel-persistence.test.ts +0 -409
  59. package/src/integration-adapter-supabase-crm/tests/lifecycle-sync.test.ts +0 -288
  60. package/src/integration-adapter-supabase-crm/tests/stage-map.test.ts +0 -80
  61. package/src/integration-adapter-supabase-crm/vitest.config.ts +0 -8
  62. package/src/integration-adapter-supabase-crm/worker.ts +0 -223
  63. package/src/ontology/archetypes/components/currency-selector.yaml +0 -19
@@ -0,0 +1,118 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Unit tests for the Lagebild ingress HTTP client. Verifies payload
4
+ construction, idempotency key generation, and network error handling.</purpose>
5
+ <non-goals>
6
+ <item>Do not make real HTTP calls — fetch is mocked.</item>
7
+ </non-goals>
8
+ </MODULE_CONTRACT>
9
+ */
10
+
11
+ import { test, expect, vi } from "vitest";
12
+ import {
13
+ submitIngress,
14
+ buildIdempotencyKey,
15
+ LAGEBILD_INGRESS_CONTRACT_VERSION,
16
+ type LagebildIngressConfig,
17
+ } from "../lagebild-ingress.ts";
18
+
19
+ const baseConfig: LagebildIngressConfig = {
20
+ apiUrl: "https://lagebild-api.example.workers.dev",
21
+ apiKey: "lbk_test_key_0001",
22
+ tenantId: "ten_warpgogol000001",
23
+ sourceSystemId: "site_warpgogol_com",
24
+ };
25
+
26
+ test("buildIdempotencyKey is stable and combines tenant + submission", () => {
27
+ const key = buildIdempotencyKey("ten_abc", "sub_123");
28
+ expect(key).toBe("ten_abc:sub_123");
29
+ expect(buildIdempotencyKey("ten_abc", "sub_123")).toBe(key);
30
+ });
31
+
32
+ test("submitIngress sends POST with correct headers and payload", async () => {
33
+ const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
34
+ new Response(
35
+ JSON.stringify({
36
+ submission_id: "sub_test00000000000001",
37
+ accepted: true,
38
+ is_replay: false,
39
+ correlation_id: null,
40
+ }),
41
+ { status: 200, headers: { "Content-Type": "application/json" } },
42
+ ),
43
+ );
44
+
45
+ const result = await submitIngress(baseConfig, {
46
+ submissionId: "sub_test00000000000001",
47
+ idempotencyKey: "ten_warpgogol000001:sub_test00000000000001",
48
+ interactionKind: "contact_message",
49
+ identityClaims: [
50
+ { claim_type: "email", value: "test@example.com" },
51
+ { claim_type: "person_name", value: "Test User" },
52
+ ],
53
+ message: "Hello from the test",
54
+ origin: { page_uri: "https://example.com/contact", locale: "de" },
55
+ });
56
+
57
+ expect(result.accepted).toBe(true);
58
+ expect(result.is_replay).toBe(false);
59
+
60
+ expect(fetchSpy).toHaveBeenCalledOnce();
61
+ const [url, init] = fetchSpy.mock.calls[0] as [string, RequestInit];
62
+ expect(url).toBe("https://lagebild-api.example.workers.dev/v1/ingress");
63
+ expect(init.method).toBe("POST");
64
+ expect((init.headers as Record<string, string>)["Authorization"]).toBe(
65
+ "Bearer lbk_test_key_0001",
66
+ );
67
+
68
+ const body = JSON.parse(init.body as string) as Record<string, unknown>;
69
+ expect(body["contract_version"]).toBe(LAGEBILD_INGRESS_CONTRACT_VERSION);
70
+ expect(body["tenant_id"]).toBe("ten_warpgogol000001");
71
+ expect(body["source_system_id"]).toBe("site_warpgogol_com");
72
+ expect(body["interaction_kind"]).toBe("contact_message");
73
+
74
+ fetchSpy.mockRestore();
75
+ });
76
+
77
+ test("submitIngress returns accepted=false on network error", async () => {
78
+ vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("connection refused"));
79
+
80
+ const result = await submitIngress(baseConfig, {
81
+ submissionId: "sub_test00000000000002",
82
+ idempotencyKey: "ten:test2",
83
+ interactionKind: "inquiry",
84
+ identityClaims: [{ claim_type: "email", value: "test@example.com" }],
85
+ });
86
+
87
+ expect(result.accepted).toBe(false);
88
+ expect(result.rejection_class).toBe("network_error");
89
+
90
+ vi.restoreAllMocks();
91
+ });
92
+
93
+ test("submitIngress returns accepted=false on 400 response", async () => {
94
+ vi.spyOn(globalThis, "fetch").mockResolvedValue(
95
+ new Response(
96
+ JSON.stringify({
97
+ submission_id: "sub_test00000000000003",
98
+ accepted: false,
99
+ is_replay: false,
100
+ rejection_class: "schema_invalid",
101
+ correlation_id: null,
102
+ }),
103
+ { status: 400, headers: { "Content-Type": "application/json" } },
104
+ ),
105
+ );
106
+
107
+ const result = await submitIngress(baseConfig, {
108
+ submissionId: "sub_test00000000000003",
109
+ idempotencyKey: "ten:test3",
110
+ interactionKind: "inquiry",
111
+ identityClaims: [{ claim_type: "email", value: "bad" }],
112
+ });
113
+
114
+ expect(result.accepted).toBe(false);
115
+ expect(result.rejection_class).toBe("schema_invalid");
116
+
117
+ vi.restoreAllMocks();
118
+ });
@@ -15,7 +15,7 @@
15
15
  */
16
16
 
17
17
  import type { MetricsPusher } from "./pusher.ts";
18
- import { WARPGOGOL_METRIC_REGISTRY } from "./metric-registry.ts";
18
+ import type { WARPGOGOL_METRIC_REGISTRY } from "./metric-registry.ts";
19
19
 
20
20
  type LabelKeys<L extends readonly string[]> = L[number];
21
21
  type LabelMap<L extends readonly string[]> = Partial<Record<LabelKeys<L>, string>>;
@@ -15,7 +15,7 @@ sites:
15
15
  siteId: "1"
16
16
  matomoCloudHost: warpgogol.matomo.cloud
17
17
  status: active
18
- provisionedAt: "2026-08-08"
18
+ provisionedAt: 2026-08-08
19
19
  dimensions:
20
20
  visit:
21
21
  client_id: 1
@@ -135,15 +135,15 @@
135
135
  "layer": "component"
136
136
  },
137
137
  {
138
- "id": "currency-selector",
139
- "displayName": "Currency Selector",
140
- "semanticRole": "component-currency-selector",
138
+ "id": "cycle-toggle",
139
+ "displayName": "Cycle Toggle",
140
+ "semanticRole": "component-cycle-toggle",
141
141
  "version": "1.0.0",
142
142
  "layoutHint": "split",
143
143
  "acceptedCosmicNames": [
144
- "Portia"
144
+ "Cupid"
145
145
  ],
146
- "sourceFile": "packages/werkstatt-site/src/domain/ontology/archetypes/components/currency-selector.yaml",
146
+ "sourceFile": "packages/werkstatt-site/src/domain/ontology/archetypes/components/cycle-toggle.yaml",
147
147
  "layer": "component"
148
148
  },
149
149
  {
@@ -750,7 +750,7 @@
750
750
  "component-brand-label",
751
751
  "component-copyright",
752
752
  "component-currency-aware-price-display",
753
- "component-currency-selector",
753
+ "component-cycle-toggle",
754
754
  "component-footer-promo",
755
755
  "component-lang-switcher",
756
756
  "component-layout",
@@ -814,9 +814,10 @@
814
814
  "Proteus": "@warpgogol/werkstatt-site/ui/components/brand-label",
815
815
  "Charon": "@warpgogol/werkstatt-site/ui/components/copyright",
816
816
  "Rosalind": "@warpgogol/werkstatt-site/ui/components/currency-aware-price-display",
817
- "Portia": "@warpgogol/werkstatt-site/ui/components/currency-selector",
817
+ "Cupid": "@warpgogol/werkstatt-site/ui/components/cycle-toggle",
818
818
  "Galatea": "@warpgogol/werkstatt-site/ui/components/donation-card",
819
819
  "Puck": "@warpgogol/werkstatt-site/ui/components/effects",
820
+ "Prospero": "@warpgogol/werkstatt-site/ui/components/external-link-qr-modal",
820
821
  "Nereid": "@warpgogol/werkstatt-site/ui/components/footer-promo",
821
822
  "Ophelia": "@warpgogol/werkstatt-site/ui/components/lang-switcher",
822
823
  "Umbriel": "@warpgogol/werkstatt-site/ui/components/layout",
@@ -892,8 +893,9 @@
892
893
  "brand-label": "Proteus",
893
894
  "copyright": "Charon",
894
895
  "currency-aware-price-display": "Rosalind",
895
- "currency-selector": "Portia",
896
+ "cycle-toggle": "Cupid",
896
897
  "section-shell": "Puck",
898
+ "external-link-qr-modal": "Prospero",
897
899
  "footer-promo": "Nereid",
898
900
  "lang-switcher": "Ophelia",
899
901
  "layout": "Umbriel",
@@ -975,9 +977,10 @@
975
977
  "Ariel": "breadcrumbs",
976
978
  "Charon": "copyright",
977
979
  "Rosalind": "price-display",
978
- "Portia": "currency-selector",
980
+ "Cupid": "cycle-toggle",
979
981
  "Galatea": "person-profile",
980
982
  "Puck": "section-shell",
983
+ "Prospero": "layout-shell",
981
984
  "Titania": "footer",
982
985
  "Nereid": "footer-promo",
983
986
  "Oberon": "header",
@@ -1,5 +1,5 @@
1
1
  schemaVersion: 1.0.0
2
- totalCount: 58
2
+ totalCount: 59
3
3
  entries:
4
4
  - id: approach
5
5
  displayName: Approach
@@ -100,14 +100,14 @@ entries:
100
100
  - Rosalind
101
101
  sourceFile: packages/werkstatt-site/src/domain/ontology/archetypes/components/currency-aware-price-display.yaml
102
102
  layer: component
103
- - id: currency-selector
104
- displayName: Currency Selector
105
- semanticRole: component-currency-selector
103
+ - id: cycle-toggle
104
+ displayName: Cycle Toggle
105
+ semanticRole: component-cycle-toggle
106
106
  version: 1.0.0
107
107
  layoutHint: split
108
108
  acceptedCosmicNames:
109
- - Portia
110
- sourceFile: packages/werkstatt-site/src/domain/ontology/archetypes/components/currency-selector.yaml
109
+ - Cupid
110
+ sourceFile: packages/werkstatt-site/src/domain/ontology/archetypes/components/cycle-toggle.yaml
111
111
  layer: component
112
112
  - id: donation-card
113
113
  displayName: Donation Card
@@ -163,6 +163,15 @@ entries:
163
163
  - Nereid
164
164
  sourceFile: packages/werkstatt-site/src/domain/ontology/archetypes/components/footer-promo.yaml
165
165
  layer: component
166
+ - id: gratitude
167
+ displayName: Gratitude
168
+ semanticRole: client-gratitude
169
+ version: 1.0.0
170
+ layoutHint: card-grid
171
+ acceptedCosmicNames:
172
+ - Gonggong
173
+ sourceFile: packages/werkstatt-site/src/domain/ontology/archetypes/sections/gratitude.yaml
174
+ layer: section
166
175
  - id: hero
167
176
  displayName: Hero
168
177
  semanticRole: hero
@@ -528,6 +537,7 @@ sectionRoles:
528
537
  - article-index
529
538
  - audience-self-identification
530
539
  - breadcrumbs
540
+ - client-gratitude
531
541
  - client-ownership-disclosure
532
542
  - consent-gated-chat-launcher
533
543
  - credits-gallery
@@ -560,7 +570,7 @@ componentRoles:
560
570
  - component-brand-label
561
571
  - component-copyright
562
572
  - component-currency-aware-price-display
563
- - component-currency-selector
573
+ - component-cycle-toggle
564
574
  - component-footer-promo
565
575
  - component-lang-switcher
566
576
  - component-layout
@@ -599,6 +609,7 @@ planetImportPaths:
599
609
  Elara: "@warpgogol/werkstatt-site/ui/sections/dynamic-status-block"
600
610
  Atlas: "@warpgogol/werkstatt-site/ui/sections/faq-list"
601
611
  Dione: "@warpgogol/werkstatt-site/ui/sections/final-cta"
612
+ Gonggong: "@warpgogol/werkstatt-site/ui/sections/gratitude"
602
613
  Europa: "@warpgogol/werkstatt-site/ui/sections/hero"
603
614
  Phobos: "@warpgogol/werkstatt-site/ui/sections/hero-decision-card"
604
615
  Ganymede: "@warpgogol/werkstatt-site/ui/sections/impact"
@@ -622,9 +633,10 @@ planetImportPaths:
622
633
  Proteus: "@warpgogol/werkstatt-site/ui/components/brand-label"
623
634
  Charon: "@warpgogol/werkstatt-site/ui/components/copyright"
624
635
  Rosalind: "@warpgogol/werkstatt-site/ui/components/currency-aware-price-display"
625
- Portia: "@warpgogol/werkstatt-site/ui/components/currency-selector"
636
+ Cupid: "@warpgogol/werkstatt-site/ui/components/cycle-toggle"
626
637
  Galatea: "@warpgogol/werkstatt-site/ui/components/donation-card"
627
638
  Puck: "@warpgogol/werkstatt-site/ui/components/effects"
639
+ Prospero: "@warpgogol/werkstatt-site/ui/components/external-link-qr-modal"
628
640
  Nereid: "@warpgogol/werkstatt-site/ui/components/footer-promo"
629
641
  Ophelia: "@warpgogol/werkstatt-site/ui/components/lang-switcher"
630
642
  Umbriel: "@warpgogol/werkstatt-site/ui/components/layout"
@@ -675,6 +687,7 @@ blockTypeToCosmicName:
675
687
  dynamic-status-block: Elara
676
688
  faq-list: Atlas
677
689
  final-cta: Dione
690
+ gratitude: Gonggong
678
691
  hero: Europa
679
692
  hero-decision-card: Phobos
680
693
  impact: Ganymede
@@ -698,8 +711,9 @@ blockTypeToCosmicName:
698
711
  brand-label: Proteus
699
712
  copyright: Charon
700
713
  currency-aware-price-display: Rosalind
701
- currency-selector: Portia
714
+ cycle-toggle: Cupid
702
715
  section-shell: Puck
716
+ external-link-qr-modal: Prospero
703
717
  footer-promo: Nereid
704
718
  lang-switcher: Ophelia
705
719
  layout: Umbriel
@@ -754,6 +768,7 @@ roleByCosmicName:
754
768
  Elara: data-driven-status-indicator
755
769
  Atlas: approach
756
770
  Dione: final-cta
771
+ Gonggong: client-gratitude
757
772
  Europa: hero
758
773
  Phobos: hero
759
774
  Ganymede: impact
@@ -778,9 +793,10 @@ roleByCosmicName:
778
793
  Ariel: breadcrumbs
779
794
  Charon: copyright
780
795
  Rosalind: price-display
781
- Portia: currency-selector
796
+ Cupid: cycle-toggle
782
797
  Galatea: person-profile
783
798
  Puck: section-shell
799
+ Prospero: layout-shell
784
800
  Titania: footer
785
801
  Nereid: footer-promo
786
802
  Oberon: header
@@ -95,8 +95,8 @@ levels:
95
95
  de: "Welche Angaben helfen, den Betrieb einzuschätzen: Qualifikationen, Meisterbetrieb, Fotos, Bewertungen, Öffnungszeiten."
96
96
  uk: "Які відомості допомагають оцінити бізнес: кваліфікації, майстерність, фото, відгуки, години роботи."
97
97
  catalogHeading:
98
- de: "Gewerbe auswählen"
99
- uk: "Обрати галузь"
98
+ de: Gewerbe auswählen
99
+ uk: Обрати галузь
100
100
  productPrice:
101
101
  heading:
102
102
  de: "Gleiche Grundlage, angepasste Struktur"
@@ -104,7 +104,7 @@ levels:
104
104
  body:
105
105
  de: "Die Branche verändert nicht automatisch den Basispreis. Zusätzliche Seiten, Sprachen, Buchungsfunktionen oder Integrationen werden gesondert vereinbart."
106
106
  uk: "Галузь не змінює автоматично базову ціну. Додаткові сторінки, мови, функції бронювання чи інтеграції узгоджуються окремо."
107
- priceRef: "business-profile.offerings/digital-foundation.presentation.price"
107
+ priceRef: business-profile.offerings/digital-foundation.presentation.price
108
108
  finalCta:
109
109
  heading:
110
110
  de: Ihr Gewerbe ist noch nicht aufgeführt?
@@ -141,14 +141,14 @@ levels:
141
141
  minModuleMappings: 3
142
142
  minUniqueFaq: 5
143
143
  claimRestrictions:
144
- - "mehr Anfragen"
145
- - "mehr Termin-Anfragen"
146
- - "echte Aufträge"
147
- - "steigt die Wahrscheinlichkeit"
148
- - "weniger Streuverluste"
149
- - "höhere Conversion"
150
- - "besser gefunden"
151
- - "stärkstes Conversion-Signal"
144
+ - mehr Anfragen
145
+ - mehr Termin-Anfragen
146
+ - echte Aufträge
147
+ - steigt die Wahrscheinlichkeit
148
+ - weniger Streuverluste
149
+ - höhere Conversion
150
+ - besser gefunden
151
+ - stärkstes Conversion-Signal
152
152
  doorwayMaxFlaggedShare: 0.30
153
153
  duplicateMaxSimilarity: 0.70
154
154
  mode: warn
@@ -22,7 +22,7 @@ axes:
22
22
 
23
23
  levels:
24
24
  - depth: 0
25
- slug: { de: "leistungen/website-service", uk: "posluhy/sait-service" }
25
+ slug: { de: leistungen/website-service, uk: posluhy/sait-service }
26
26
  constellation: website-service-root
27
27
  geo: twin-only
28
28
  semanticType: collection
@@ -51,25 +51,25 @@ levels:
51
51
  minFaq: 5
52
52
  minPageStructure: 1
53
53
  claimRestrictions:
54
- - "mehr Anfragen"
55
- - "mehr Termin-Anfragen"
56
- - "mehr Buchungen"
57
- - "echte Aufträge"
58
- - "steigt die Wahrscheinlichkeit"
59
- - "weniger Streuverluste"
60
- - "höhere Conversion"
61
- - "besser gefunden"
62
- - "stärkstes Conversion-Signal"
63
- - "garantierte Ergebnisse"
64
- - "guaranteed results"
65
- - "no. 1"
66
- - "number one"
67
- - "leading provider"
68
- - "top rated"
69
- - "beste Preis"
70
- - "best price"
71
- - "cheapest"
72
- - "günstigste"
54
+ - mehr Anfragen
55
+ - mehr Termin-Anfragen
56
+ - mehr Buchungen
57
+ - echte Aufträge
58
+ - steigt die Wahrscheinlichkeit
59
+ - weniger Streuverluste
60
+ - höhere Conversion
61
+ - besser gefunden
62
+ - stärkstes Conversion-Signal
63
+ - garantierte Ergebnisse
64
+ - guaranteed results
65
+ - no. 1
66
+ - number one
67
+ - leading provider
68
+ - top rated
69
+ - beste Preis
70
+ - best price
71
+ - cheapest
72
+ - günstigste
73
73
  mode: warn
74
74
 
75
75
  policy:
@@ -2,8 +2,8 @@ id: lead.submit
2
2
  version: 1
3
3
  kind: action
4
4
  title:
5
- de: "Anfrage senden"
6
- en: "Submit a lead"
5
+ de: Anfrage senden
6
+ en: Submit a lead
7
7
  description:
8
8
  de: "Reicht eine Kontaktanfrage (Name, E-Mail, Nachricht) beim Gewerbe ein. Die Anfrage wird asynchron zugestellt — die Bestätigung signalisiert Annahme, nicht Zustellung."
9
9
  en: "Submit a contact request (name, email, message) to the business. The request is delivered asynchronously — the confirmation signals acceptance, not delivery."
@@ -87,7 +87,7 @@ export type SemanticRole = string;
87
87
  * section-cta | [RFC-0104] Single CTA / CTA group primitive
88
88
  * section-image | [RFC-0104] Authored image primitive with fade masks
89
89
  * site-background | [RFC-0105] Full-viewport background shell-layer component
90
- * currency-selector | [RFC-0743] Currency selector dropdown for multi-currency sites
90
+ * cycle-toggle | [RFC-0946] Generic cycle toggle button with localStorage persistence
91
91
  * price-display | [RFC-0743] Currency-aware price display with pre-rendered variants
92
92
  * scroll-to-top | [RFC-0768] Floating scroll-to-top button with Lenis smooth scrolling
93
93
  */
@@ -107,7 +107,7 @@ export const ComponentRoleValues = [
107
107
  "section-cta",
108
108
  "section-image",
109
109
  "site-background",
110
- "currency-selector",
110
+ "cycle-toggle",
111
111
  "price-display",
112
112
  "scroll-to-top",
113
113
  ] as const;
@@ -105,31 +105,31 @@ surfacePolicy:
105
105
  # Enforced by surface.media-leakage.validate and surface.contract.validate.
106
106
  mediaLeakagePolicy:
107
107
  prohibitedStrings:
108
- - pattern: "Gemini"
108
+ - pattern: Gemini
109
109
  matchingStrategy: context-aware
110
110
  contextSelector: "[data-credit-context], figcaption, details, dl"
111
111
  reason: "AI model name is internal metadata, not visitor-facing"
112
- - pattern: "AIPlatform"
112
+ - pattern: AIPlatform
113
113
  matchingStrategy: whole-word
114
- reason: "Internal enum value (CreditPartyKind)"
115
- - pattern: "Organization"
114
+ reason: Internal enum value (CreditPartyKind)
115
+ - pattern: Organization
116
116
  matchingStrategy: context-aware
117
117
  contextSelector: "[data-credit-context], figcaption, details, dl"
118
- reason: "Internal enum value (CreditPartyKind) — only as media author label"
119
- - pattern: "commissioned-warpgogol-material"
118
+ reason: Internal enum value (CreditPartyKind) — only as media author label
119
+ - pattern: commissioned-warpgogol-material
120
120
  matchingStrategy: whole-word
121
- reason: "Internal enum value (MaterialSourceType)"
122
- - pattern: "linked-public-source"
121
+ reason: Internal enum value (MaterialSourceType)
122
+ - pattern: linked-public-source
123
123
  matchingStrategy: whole-word
124
- reason: "Internal enum value"
125
- - pattern: "Copyright © 2026 Warpgogol"
124
+ reason: Internal enum value
125
+ - pattern: Copyright © 2026 Warpgogol
126
126
  matchingStrategy: exact
127
127
  contextSelector: ":not(footer):not([data-footer])"
128
- reason: "Repetitive copyright boilerplate — per-material usage status is on /bildnachweise/"
128
+ reason: Repetitive copyright boilerplate — per-material usage status is on /bildnachweise/
129
129
  requiredLabels:
130
- - label: "Konzeptillustration"
130
+ - label: Konzeptillustration
131
131
  lang: de
132
- - label: "Концептуальна ілюстрація"
132
+ - label: Концептуальна ілюстрація
133
133
  lang: uk
134
134
  requiredLinkPattern: "/bildnachweise/#"
135
- aiImageAttribute: "data-ai-generated"
135
+ aiImageAttribute: data-ai-generated
@@ -77,7 +77,7 @@ routePatterns:
77
77
  type: string
78
78
  generated: true
79
79
  # RFC-0512: Team JSON endpoints (machine-readable profiles)
80
- - pattern: "/team/profiles.json"
80
+ - pattern: /team/profiles.json
81
81
  params: {}
82
82
  generated: true
83
83
  - pattern: "/team/:slug/profile.json"
@@ -81,6 +81,7 @@ export {
81
81
  SiteFamilyContract,
82
82
  sectionArchetypeSchema,
83
83
  systemManifestSchema,
84
+ systemCollectionSchema,
84
85
  } from "./schemas/index.ts";
85
86
  export type {
86
87
  Constellation,
@@ -106,6 +107,7 @@ export type {
106
107
  SystemManifest,
107
108
  SystemPagePin,
108
109
  SystemPlanetPin,
110
+ SystemCollectionData,
109
111
  } from "./schemas/index.ts";
110
112
 
111
113
  // ---------------------------------------------------------------------------
@@ -63,8 +63,13 @@ export type {
63
63
  SectionArchetypeLayoutHint,
64
64
  } from "./section-archetype.ts";
65
65
 
66
- export { systemManifestSchema } from "./system.ts";
67
- export type { SystemManifest, SystemPagePin, SystemPlanetPin } from "./system.ts";
66
+ export { systemManifestSchema, systemCollectionSchema } from "./system.ts";
67
+ export type {
68
+ SystemManifest,
69
+ SystemPagePin,
70
+ SystemPlanetPin,
71
+ SystemCollectionData,
72
+ } from "./system.ts";
68
73
 
69
74
  // Architecture review 2026-07-10: Re-export growthVendorSchema for @warpgogol/werkstatt-site/growth/config.ts.
70
75
  export { growthVendorSchema } from "./system.ts";
@@ -128,7 +128,7 @@ export const systemIntegrationsSchema = z.object({
128
128
  * (e.g. uchat, stripe, operator), and whether the pilot is `enabled`. UChat is the
129
129
  * conversation runtime — it renders the funnel and requests transitions; it never
130
130
  * owns the graph, pricing, or canonical state. Make.com is never a source. Validated
131
- * by `funnel.contract.validate` / `funnel.stage.validate` / `funnel.lagebild.validate`.
131
+ * by `funnel.contract.validate` / `funnel.stage.validate` / `funnel.crm.validate`.
132
132
  * Absent ⇒ the funnel validators are a no-op pass (funnel not yet enabled for the app).
133
133
  */
134
134
  funnel: z
@@ -8,6 +8,7 @@
8
8
  <CHANGE_SUMMARY>
9
9
  <item>RFC-0303 Phase 3: extracted from schemas/system.ts as part of the domain split.</item>
10
10
  <item>RFC-0377: added optional `audience` field to the per-page pin schema.</item>
11
+ <item>ADR-0062: added `ctaTarget` to identity, `sectionNav` + `ctaTarget` to page pins, created `systemCollectionSchema` for content collection typing.</item>
11
12
  </CHANGE_SUMMARY>
12
13
  */
13
14
 
@@ -79,6 +80,13 @@ export const systemManifestSchema = z.object({
79
80
  * brief.validate can cross-check the brief against the running app identity.
80
81
  */
81
82
  domain: z.string().min(1).optional(),
83
+
84
+ /**
85
+ * ADR-0062: App-level default CTA target — the pageId of the contact/CTA page.
86
+ * Per-page `ctaTarget` in pages[] overrides this app-level default.
87
+ * Used by resolve-route.ts to populate `ctaTarget` in PageRouteData.
88
+ */
89
+ ctaTarget: z.string().min(1).optional(),
82
90
  }),
83
91
 
84
92
  /**
@@ -222,7 +230,7 @@ export const systemManifestSchema = z.object({
222
230
  enabled: z.boolean(),
223
231
  cosmicMoon: z.string().min(1),
224
232
  pin: z.string().min(1),
225
- props: z.record(z.string(), z.any()).optional(),
233
+ props: z.record(z.string(), z.unknown()).optional(),
226
234
  })
227
235
  .optional(),
228
236
  })
@@ -247,6 +255,19 @@ export const systemManifestSchema = z.object({
247
255
  )
248
256
  .optional()
249
257
  .default([]),
258
+
259
+ /**
260
+ * RFC-0922 / ADR-0062: per-page section nav toggle. When false, the
261
+ * page template skips extractSectionNavItems and the Header secondary
262
+ * nav strip is omitted. Defaults to true when absent.
263
+ */
264
+ sectionNav: z.boolean().optional(),
265
+
266
+ /**
267
+ * ADR-0062: per-page CTA target override. When present, overrides the
268
+ * app-level `identity.ctaTarget` for this page.
269
+ */
270
+ ctaTarget: z.string().min(1).optional(),
250
271
  }),
251
272
  )
252
273
  .optional()
@@ -498,3 +519,19 @@ export type SystemManifest = z.infer<typeof systemManifestSchema>;
498
519
  export type SystemPagePin = NonNullable<SystemManifest["pages"]>[number];
499
520
  /** A single planet pin within a page-route entry. */
500
521
  export type SystemPlanetPin = NonNullable<SystemManifest["pages"][number]["planets"]>[number];
522
+
523
+ /**
524
+ * ADR-0062: Lenient schema for the `system` Astro content collection.
525
+ *
526
+ * `systemManifestSchema` is the strict validation schema used by `system.manifest.validate`.
527
+ * The content collection schema needs to be more lenient:
528
+ * - `.partial()` — all top-level fields are optional (system.md content varies per site)
529
+ * - `.loose()` — unknown top-level keys pass through (e.g. `policy`, `seo`)
530
+ *
531
+ * This schema is used in `src/content/config.ts` via `defineCollection({ schema: systemCollectionSchema })`.
532
+ * It provides compile-time typing for `getCollection("system")` consumers, eliminating `as any` casts.
533
+ */
534
+ export const systemCollectionSchema = systemManifestSchema.partial().loose();
535
+
536
+ /** Inferred type for the `system` content collection data. */
537
+ export type SystemCollectionData = z.infer<typeof systemCollectionSchema>;
@@ -49,5 +49,10 @@ export {
49
49
  pageOutputSchema,
50
50
  } from "./system/page-output.ts";
51
51
 
52
- export { systemManifestSchema } from "./system/manifest.ts";
53
- export type { SystemManifest, SystemPagePin, SystemPlanetPin } from "./system/manifest.ts";
52
+ export { systemManifestSchema, systemCollectionSchema } from "./system/manifest.ts";
53
+ export type {
54
+ SystemManifest,
55
+ SystemPagePin,
56
+ SystemPlanetPin,
57
+ SystemCollectionData,
58
+ } from "./system/manifest.ts";
@@ -155,6 +155,17 @@ export const EFFECT_STACK_ITEM_SCHEMA = {
155
155
  skewX: { type: "number", minimum: -15, maximum: 15 },
156
156
  },
157
157
  },
158
+ {
159
+ type: "object",
160
+ additionalProperties: false,
161
+ required: ["kind", "enabled"],
162
+ properties: {
163
+ kind: { const: "lighten" },
164
+ enabled: { type: "boolean" },
165
+ opacity: { type: "number", minimum: 0, maximum: 1 },
166
+ color: EFFECT_COLOR_SCHEMA,
167
+ },
168
+ },
158
169
  ],
159
170
  };
160
171
 
@@ -88,7 +88,7 @@ export const SECTION_VISUAL_FRAGMENT: JsonSchemaFragment = {
88
88
  },
89
89
  },
90
90
  },
91
- density: { enum: ["compact", "normal", "spacious"] },
91
+ density: { enum: ["compact", "normal", "spacious", "flush"] },
92
92
  tone: { enum: ["default", "warning", "success", "muted"] },
93
93
  containerVariant: { enum: ["default", "narrow", "full"] },
94
94
  motion: {