@warpgogol/werkstatt-shared 0.13.0 → 0.14.1

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 (178) hide show
  1. package/README.md +26 -16
  2. package/README.uk.md +24 -14
  3. package/package.json +27 -69
  4. package/src/checks/result-helpers.ts +21 -4
  5. package/src/checks/tests/remediation-auto-populate.test.ts +83 -0
  6. package/src/integration/funnel.ts +78 -1
  7. package/src/integration/index.ts +19 -2
  8. package/src/integration/lagebild-ingress.ts +169 -0
  9. package/src/integration/orchestration.ts +7 -13
  10. package/src/integration/port-barrel.ts +9 -28
  11. package/src/integration/tests/funnel-bridge.test.ts +7 -7
  12. package/src/integration/tests/lagebild-ingress.test.ts +118 -0
  13. package/src/integration/vitest.config.ts +7 -0
  14. package/src/observability/typed-refs.ts +1 -1
  15. package/src/ontology/analytics/matomo-fleet.registry.yaml +1 -1
  16. package/src/ontology/archetypes/index.json +12 -9
  17. package/src/ontology/archetypes/index.yaml +26 -10
  18. package/src/ontology/blueprints/website-local.yaml +11 -11
  19. package/src/ontology/blueprints/website-service.yaml +20 -20
  20. package/src/ontology/capabilities/lead.submit.yaml +2 -2
  21. package/src/ontology/enums.ts +2 -2
  22. package/src/ontology/external-surfaces/jsonld-types.yaml +14 -14
  23. package/src/ontology/external-surfaces/url-schema.yaml +1 -1
  24. package/src/ontology/index.ts +2 -0
  25. package/src/ontology/manifest.ts +19 -0
  26. package/src/ontology/schemas/index.ts +7 -2
  27. package/src/ontology/schemas/system/integrations.ts +1 -1
  28. package/src/ontology/schemas/system/manifest.ts +38 -1
  29. package/src/ontology/schemas/system/tests/growth.test.ts +22 -0
  30. package/src/ontology/schemas/system/tests/integrations.test.ts +12 -0
  31. package/src/ontology/schemas/system/tests/manifest.test.ts +22 -0
  32. package/src/ontology/schemas/system/tests/page-output.test.ts +13 -0
  33. package/src/ontology/schemas/system/tests/release.test.ts +22 -0
  34. package/src/ontology/schemas/system/tests/text.test.ts +18 -0
  35. package/src/ontology/schemas/system/tests/verification.test.ts +12 -0
  36. package/src/ontology/schemas/system.ts +7 -2
  37. package/src/ontology/schemas/tests/biome.test.ts +19 -0
  38. package/src/ontology/schemas/tests/capability.test.ts +22 -0
  39. package/src/ontology/schemas/tests/constellation.test.ts +22 -0
  40. package/src/ontology/schemas/tests/dns-records.test.ts +13 -0
  41. package/src/ontology/schemas/tests/manifest-resolver.test.ts +13 -0
  42. package/src/ontology/schemas/tests/page-entry.test.ts +22 -0
  43. package/src/ontology/schemas/tests/section-archetype.test.ts +12 -0
  44. package/src/ontology/schemas/tests/site-family.test.ts +18 -0
  45. package/src/ontology/schemas/tests/system.test.ts +20 -0
  46. package/src/ontology/shared-section-props/common.ts +11 -0
  47. package/src/ontology/shared-section-props/visual-header.ts +1 -1
  48. package/src/share/agent/api-catalog.ts +5 -0
  49. package/src/share/agent/ard-catalog.ts +14 -0
  50. package/src/share/agent/index.ts +2 -0
  51. package/src/share/agent/manifest.ts +25 -2
  52. package/src/share/agent/mcp-card.ts +4 -0
  53. package/src/share/agent/openapi.ts +78 -0
  54. package/src/share/agent/search.test.ts +188 -0
  55. package/src/share/agent/search.ts +164 -0
  56. package/src/share/agent/tests/api-catalog.test.ts +8 -0
  57. package/src/share/agent/tests/ard-catalog.test.ts +8 -0
  58. package/src/share/agent/tests/capability.test.ts +14 -0
  59. package/src/share/agent/tests/fleet-catalog.test.ts +15 -0
  60. package/src/share/agent/tests/knowledge.test.ts +20 -0
  61. package/src/share/agent/tests/manifest.test.ts +9 -0
  62. package/src/share/agent/tests/mcp-card.test.ts +8 -0
  63. package/src/share/agent/tests/omit-empty.test.ts +33 -0
  64. package/src/share/agent/tests/openapi.test.ts +8 -0
  65. package/src/share/agent/tests/proof.test.ts +15 -0
  66. package/src/share/env.d.ts +0 -4
  67. package/src/share/knowledge/tests/claim.test.ts +11 -0
  68. package/src/share/knowledge/tests/derivation.test.ts +30 -0
  69. package/src/share/knowledge/tests/freshness.test.ts +18 -0
  70. package/src/share/knowledge/tests/ledger.test.ts +35 -0
  71. package/src/share/knowledge/tests/plan.test.ts +19 -0
  72. package/src/share/knowledge/tests/source.test.ts +19 -0
  73. package/src/share/remediation/remediation-catalog.ts +196 -0
  74. package/src/share/schemas/effects.ts +16 -0
  75. package/src/share/schemas/media.ts +4 -0
  76. package/src/share/schemas/section-shell.ts +2 -1
  77. package/src/share/schemas/tests/claim-records.test.ts +52 -0
  78. package/src/share/schemas/tests/claims.test.ts +23 -0
  79. package/src/share/schemas/tests/effects.test.ts +116 -0
  80. package/src/share/schemas/tests/features.test.ts +61 -0
  81. package/src/share/schemas/tests/horizontal-align.test.ts +16 -0
  82. package/src/share/schemas/tests/icon-color.test.ts +16 -0
  83. package/src/share/schemas/tests/live-photo.test.ts +40 -0
  84. package/src/share/schemas/tests/material-credit.test.ts +51 -0
  85. package/src/share/schemas/tests/media.test.ts +26 -0
  86. package/src/share/schemas/tests/navigation.test.ts +58 -0
  87. package/src/share/schemas/tests/page-base.test.ts +19 -0
  88. package/src/share/schemas/tests/participant.test.ts +26 -0
  89. package/src/share/schemas/tests/person.test.ts +12 -0
  90. package/src/share/schemas/tests/print.test.ts +30 -0
  91. package/src/share/schemas/tests/section-background.test.ts +31 -0
  92. package/src/share/schemas/tests/section-body.test.ts +16 -0
  93. package/src/share/schemas/tests/section-cards.test.ts +17 -0
  94. package/src/share/schemas/tests/section-cta.test.ts +41 -0
  95. package/src/share/schemas/tests/section-header.test.ts +31 -0
  96. package/src/share/schemas/tests/section-image.test.ts +24 -0
  97. package/src/share/schemas/tests/section-motion.test.ts +29 -0
  98. package/src/share/schemas/tests/section-shell.test.ts +42 -0
  99. package/src/share/schemas/tests/section-stats.test.ts +17 -0
  100. package/src/share/schemas/tests/site-background.test.ts +33 -0
  101. package/src/share/schemas/tests/standard-list-item.test.ts +16 -0
  102. package/src/share/scripts/external-link-qr.ts +9 -1
  103. package/src/share/scripts/gsap-counter.ts +2 -0
  104. package/src/share/scripts/inline-number-animation.ts +2 -0
  105. package/src/share/scripts/live-photos.ts +2 -0
  106. package/src/share/scripts/tests/external-link-qr.test.ts +8 -0
  107. package/src/share/scripts/tests/external-links.test.ts +25 -0
  108. package/src/share/scripts/tests/gsap-counter.test.ts +8 -0
  109. package/src/share/scripts/tests/gsap-mountain-journey.test.ts +8 -0
  110. package/src/share/scripts/tests/gsap-parallax.test.ts +8 -0
  111. package/src/share/scripts/tests/gsap-reveal.test.ts +8 -0
  112. package/src/share/scripts/tests/gsap-stagger.test.ts +8 -0
  113. package/src/share/scripts/tests/inline-number-animation.test.ts +8 -0
  114. package/src/share/scripts/tests/lenis.test.ts +8 -0
  115. package/src/share/scripts/tests/live-photos.test.ts +8 -0
  116. package/src/share/scripts/tests/lordicon.test.ts +8 -0
  117. package/src/share/scripts/tests/orchestrator.test.ts +8 -0
  118. package/src/share/scripts/tests/scheduler.test.ts +23 -0
  119. package/src/share/scripts/video-player.test.ts +210 -0
  120. package/src/share/scripts/video-player.ts +73 -13
  121. package/src/share/semantic/extract.ts +25 -1
  122. package/src/share/semantic/jsonld/tests/article.test.ts +89 -0
  123. package/src/share/semantic/jsonld/tests/breadcrumb.test.ts +62 -0
  124. package/src/share/semantic/jsonld/tests/collection-list.test.ts +39 -0
  125. package/src/share/semantic/jsonld/tests/faq.test.ts +60 -0
  126. package/src/share/semantic/jsonld/tests/initiative.test.ts +56 -0
  127. package/src/share/semantic/jsonld/tests/organization.test.ts +123 -0
  128. package/src/share/semantic/jsonld/tests/person.test.ts +89 -0
  129. package/src/share/semantic/jsonld/tests/service.test.ts +65 -0
  130. package/src/share/semantic/jsonld/tests/shared.test.ts +41 -0
  131. package/src/share/semantic/jsonld/tests/video.test.ts +84 -0
  132. package/src/share/semantic/jsonld/tests/webpage.test.ts +93 -0
  133. package/src/share/semantic/jsonld/tests/website.test.ts +27 -0
  134. package/src/share/semantic/jsonld/types.ts +1 -0
  135. package/src/share/semantic/models.ts +1 -0
  136. package/src/share/semantic/tests/ai.test.ts +55 -0
  137. package/src/share/semantic/tests/block-extraction.test.ts +34 -0
  138. package/src/share/semantic/tests/breadcrumbs.test.ts +127 -0
  139. package/src/share/semantic/tests/build-page.test.ts +122 -0
  140. package/src/share/semantic/tests/business-projection.test.ts +188 -0
  141. package/src/share/semantic/tests/derived-prices-loader.test.ts +46 -0
  142. package/src/share/semantic/tests/extract.test.ts +126 -0
  143. package/src/share/semantic/tests/feed.test.ts +113 -0
  144. package/src/share/semantic/tests/helpers.ts +32 -0
  145. package/src/share/semantic/tests/ids.test.ts +118 -0
  146. package/src/share/semantic/tests/image-sitemap.test.ts +109 -0
  147. package/src/share/semantic/tests/jsonld.test.ts +61 -0
  148. package/src/share/semantic/tests/llms-policy.test.ts +60 -0
  149. package/src/share/semantic/tests/llms.test.ts +118 -0
  150. package/src/share/semantic/tests/markdown-hygiene.test.ts +72 -0
  151. package/src/share/semantic/tests/markdown-page.test.ts +97 -0
  152. package/src/share/semantic/tests/markdown-twin-provenance.test.ts +152 -0
  153. package/src/share/semantic/tests/organization-profile.test.ts +107 -0
  154. package/src/share/semantic/tests/output-projection.test.ts +77 -0
  155. package/src/share/semantic/tests/page-markdown.test.ts +98 -0
  156. package/src/share/semantic/tests/page-utils.test.ts +88 -0
  157. package/src/share/semantic/tests/price-marker-resolver.test.ts +88 -0
  158. package/src/share/semantic/tests/robots.test.ts +70 -0
  159. package/src/share/semantic/tests/split-sentences.test.ts +57 -0
  160. package/src/share/semantic/tests/update-stamp.test.ts +102 -0
  161. package/src/share/tests/manifest.test.ts +51 -0
  162. package/src/share/tests/openapi.test.ts +30 -0
  163. package/src/share/types/qrcode-browser.d.ts +18 -0
  164. package/src/share/utility-registry.yaml +16 -2
  165. package/src/integration/crm-buffer.ts +0 -491
  166. package/src/integration-adapter-supabase-crm/adapter.ts +0 -437
  167. package/src/integration-adapter-supabase-crm/client.ts +0 -605
  168. package/src/integration-adapter-supabase-crm/index.ts +0 -28
  169. package/src/integration-adapter-supabase-crm/pipedrive-sync-target.ts +0 -482
  170. package/src/integration-adapter-supabase-crm/supabase-rest.ts +0 -89
  171. package/src/integration-adapter-supabase-crm/tenant-registry.ts +0 -334
  172. package/src/integration-adapter-supabase-crm/tests/change-balance.pbt.test.ts +0 -83
  173. package/src/integration-adapter-supabase-crm/tests/funnel-persistence.test.ts +0 -409
  174. package/src/integration-adapter-supabase-crm/tests/lifecycle-sync.test.ts +0 -288
  175. package/src/integration-adapter-supabase-crm/tests/stage-map.test.ts +0 -80
  176. package/src/integration-adapter-supabase-crm/vitest.config.ts +0 -8
  177. package/src/integration-adapter-supabase-crm/worker.ts +0 -223
  178. package/src/ontology/archetypes/components/currency-selector.yaml +0 -19
@@ -0,0 +1,169 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Thin HTTP client for Lagebild API POST /v1/ingress. Werkstatt does NOT
4
+ recreate Lagebild semantics locally — it only calls the public WebsiteIngress v1
5
+ contract. The client builds the payload from form data, sends it with a service
6
+ principal API key, and returns the acceptance result. Replay protection is
7
+ built-in via a stable idempotency_key (derived from submission_id + tenant).</purpose>
8
+ <non-goals>
9
+ <item>Do not implement Lagebild domain logic — only the HTTP call.</item>
10
+ <item>Do not persist anything locally — the API owns canonical state.</item>
11
+ <item>Do not retry on non-2xx — the caller decides retry strategy.</item>
12
+ </non-goals>
13
+ </MODULE_CONTRACT>
14
+ */
15
+
16
+ export const LAGEBILD_INGRESS_CONTRACT_VERSION = 1 as const;
17
+
18
+ export type IngressInteractionKind =
19
+ | "inquiry"
20
+ | "contact_message"
21
+ | "callback_request"
22
+ | "appointment_request"
23
+ | "other";
24
+
25
+ export type IdentityClaimType =
26
+ | "person_name"
27
+ | "organization_name"
28
+ | "email"
29
+ | "phone"
30
+ | "website"
31
+ | "postal_address"
32
+ | "other";
33
+
34
+ export interface IdentityClaim {
35
+ readonly claim_type: IdentityClaimType;
36
+ readonly value: string;
37
+ readonly label?: string | null;
38
+ }
39
+
40
+ export interface IngressOrigin {
41
+ readonly page_uri?: string | null;
42
+ readonly referrer_uri?: string | null;
43
+ readonly locale?: string | null;
44
+ }
45
+
46
+ export type PolicyAssertionKind = "consent" | "objection" | "preference";
47
+
48
+ export interface ExplicitPolicyAssertion {
49
+ readonly kind: PolicyAssertionKind;
50
+ readonly purpose: string;
51
+ readonly channel?: string | null;
52
+ readonly value: boolean;
53
+ readonly statement_ref?: string | null;
54
+ }
55
+
56
+ export interface WebsiteIngressPayload {
57
+ readonly contract_version: typeof LAGEBILD_INGRESS_CONTRACT_VERSION;
58
+ readonly tenant_id: string;
59
+ readonly source_system_id: string;
60
+ readonly submission_id: string;
61
+ readonly idempotency_key: string;
62
+ readonly occurred_at: string;
63
+ readonly interaction_kind: IngressInteractionKind;
64
+ readonly identity_claims: readonly IdentityClaim[];
65
+ readonly message?: string | null;
66
+ readonly origin?: IngressOrigin;
67
+ readonly explicit_policy_assertions?: readonly ExplicitPolicyAssertion[];
68
+ readonly correlation_id?: string | null;
69
+ }
70
+
71
+ export interface IngressResult {
72
+ readonly submission_id: string | null;
73
+ readonly accepted: boolean;
74
+ readonly is_replay: boolean;
75
+ readonly rejection_class?: string | null;
76
+ readonly correlation_id?: string | null;
77
+ }
78
+
79
+ export interface LagebildIngressConfig {
80
+ readonly apiUrl: string;
81
+ readonly apiKey: string;
82
+ readonly tenantId: string;
83
+ readonly sourceSystemId: string;
84
+ }
85
+
86
+ export interface IngressSubmitInput {
87
+ readonly submissionId: string;
88
+ readonly idempotencyKey: string;
89
+ readonly interactionKind: IngressInteractionKind;
90
+ readonly identityClaims: readonly IdentityClaim[];
91
+ readonly message?: string | null;
92
+ readonly origin?: IngressOrigin;
93
+ readonly explicitPolicyAssertions?: readonly ExplicitPolicyAssertion[];
94
+ readonly correlationId?: string | null;
95
+ }
96
+
97
+ /**
98
+ * Submit a website ingress event to the Lagebild API.
99
+ * Returns the acceptance result. Does not throw on non-2xx — returns
100
+ * a result with accepted=false so the caller can decide retry strategy.
101
+ */
102
+ export async function submitIngress(
103
+ config: LagebildIngressConfig,
104
+ input: IngressSubmitInput,
105
+ ): Promise<IngressResult> {
106
+ const payload: WebsiteIngressPayload = {
107
+ contract_version: LAGEBILD_INGRESS_CONTRACT_VERSION,
108
+ tenant_id: config.tenantId,
109
+ source_system_id: config.sourceSystemId,
110
+ submission_id: input.submissionId,
111
+ idempotency_key: input.idempotencyKey,
112
+ occurred_at: new Date().toISOString(),
113
+ interaction_kind: input.interactionKind,
114
+ identity_claims: input.identityClaims,
115
+ ...(input.message != null ? { message: input.message } : {}),
116
+ ...(input.origin != null ? { origin: input.origin } : {}),
117
+ ...(input.explicitPolicyAssertions != null
118
+ ? { explicit_policy_assertions: input.explicitPolicyAssertions }
119
+ : {}),
120
+ ...(input.correlationId != null ? { correlation_id: input.correlationId } : {}),
121
+ };
122
+
123
+ const url = config.apiUrl.replace(/\/$/, "") + "/v1/ingress";
124
+
125
+ let response: Response;
126
+ try {
127
+ response = await fetch(url, {
128
+ method: "POST",
129
+ headers: {
130
+ "Content-Type": "application/json",
131
+ Authorization: "Bearer " + config.apiKey,
132
+ },
133
+ body: JSON.stringify(payload),
134
+ });
135
+ } catch {
136
+ return {
137
+ submission_id: input.submissionId,
138
+ accepted: false,
139
+ is_replay: false,
140
+ rejection_class: "network_error",
141
+ correlation_id: input.correlationId ?? null,
142
+ };
143
+ }
144
+
145
+ let body: unknown;
146
+ try {
147
+ body = await response.json();
148
+ } catch {
149
+ body = {};
150
+ }
151
+
152
+ const result = body as Partial<IngressResult>;
153
+ return {
154
+ submission_id: result.submission_id ?? input.submissionId,
155
+ accepted: result.accepted ?? response.ok,
156
+ is_replay: result.is_replay ?? false,
157
+ rejection_class: result.rejection_class ?? null,
158
+ correlation_id: result.correlation_id ?? input.correlationId ?? null,
159
+ };
160
+ }
161
+
162
+ /**
163
+ * Build a stable idempotency key from tenant + submission ID.
164
+ * This ensures retries within the same submission are deduplicated
165
+ * by the Lagebild API.
166
+ */
167
+ export function buildIdempotencyKey(tenantId: string, submissionId: string): string {
168
+ return tenantId + ":" + submissionId;
169
+ }
@@ -31,8 +31,9 @@ import type {
31
31
  IntegrationSecrets,
32
32
  Lead,
33
33
  LeadMessage,
34
+ EXECUTION_MODES,
34
35
  } from "./port.ts";
35
- import { DESTINATION_KINDS, EXECUTION_MODES, eventToLeadMessage } from "./port.ts";
36
+ import { DESTINATION_KINDS, eventToLeadMessage } from "./port.ts";
36
37
  import { LifecycleEventPayloadSchema } from "./lifecycle.ts";
37
38
  import { z } from "zod";
38
39
 
@@ -171,20 +172,14 @@ export const DESTINATION_ADAPTERS: readonly DestinationAdapter[] = [pipedriveDes
171
172
  * `extraAdapters`; the validator checks vendor + secrets against these catalogs.
172
173
  */
173
174
  export const EXTERNAL_DESTINATION_VENDORS: Readonly<Record<DestinationKind, readonly string[]>> = {
174
- crm: ["supabase-buffer"],
175
+ crm: [],
175
176
  calendar: [],
176
177
  email: [],
177
178
  scheduler: [],
178
179
  };
179
180
 
180
181
  /** Per `(kind, vendor)` → required secret names for externally-implemented adapters. */
181
- export const EXTERNAL_DESTINATION_SECRETS: Readonly<Record<string, readonly string[]>> = {
182
- "crm:supabase-buffer": [
183
- "SUPABASE_BUFFER_URL",
184
- "SUPABASE_BUFFER_SERVICE_KEY",
185
- "SUPABASE_BUFFER_TENANT_ID",
186
- ],
187
- };
182
+ export const EXTERNAL_DESTINATION_SECRETS: Readonly<Record<string, readonly string[]>> = {};
188
183
 
189
184
  /** Closed per-kind vendor catalog, derived from the registry + external vendors (RFC-0176/0186). */
190
185
  export const DESTINATION_VENDORS_BY_KIND: Readonly<Record<DestinationKind, readonly string[]>> =
@@ -318,8 +313,7 @@ export function authenticateInbound(headers: Headers, secret: string | undefined
318
313
  * supplied. `seen` is the optional dedup set (RFC-0176).
319
314
  *
320
315
  * `extraAdapters` lets a specific site's delivery callback inject additional
321
- * DestinationAdapters (e.g. supabaseBufferDestinationAdapter) without modifying
322
- * the closed shared registry. They are self-enabling (skip if secrets absent).
316
+ * DestinationAdapters without modifying the closed shared registry. They are self-enabling (skip if secrets absent).
323
317
  */
324
318
  export function routeEventToReady(
325
319
  event: IntegrationEvent,
@@ -350,8 +344,8 @@ export interface DeliverEventResult {
350
344
  * destinations (CRM), with the client's own tokens. Email is NOT here — it is sent via
351
345
  * Cloudflare Email Routing (send_email binding) in the callback route. `seen` dedups the
352
346
  * destination side on redelivery. Best-effort per sink; the caller decides ack vs retry.
353
- * `extraAdapters` lets a specific site inject additional DestinationAdapters (e.g.
354
- * supabaseBufferDestinationAdapter) without modifying the closed shared registry.
347
+ * `extraAdapters` lets a specific site inject additional DestinationAdapters
348
+ * without modifying the closed shared registry.
355
349
  */
356
350
  export async function deliverEvent(
357
351
  event: IntegrationEvent,
@@ -2,7 +2,7 @@
2
2
  <MODULE_CONTRACT>
3
3
  <purpose>
4
4
  RFC-0168: Integration Port barrel — types and pure contracts only. Re-exports from
5
- port.ts, crm-buffer.ts, funnel.ts, lifecycle.ts, sharding.ts, dispatch.ts, and qstash.ts.
5
+ port.ts, funnel.ts, lifecycle.ts, sharding.ts, dispatch.ts, and qstash.ts.
6
6
  Type-only consumers import from here to avoid transitively pulling in adapter
7
7
  implementations (orchestration.ts → adapters.ts).
8
8
  </purpose>
@@ -22,33 +22,6 @@ export type { IntegrationEvent, DestinationAdapter } from "./port.ts";
22
22
  export { DESTINATION_KINDS, EXECUTION_MODES, eventToLeadMessage, eventToLead } from "./port.ts";
23
23
  export type { DestinationKind, ExecutionMode } from "./port.ts";
24
24
 
25
- export type {
26
- BufferContact,
27
- BufferOrganization,
28
- BufferDeal,
29
- BufferStageTransition,
30
- BufferFunnelEvent,
31
- BufferConsentEvent,
32
- BufferSubscription,
33
- BufferInvoice,
34
- BufferUpsertResult,
35
- OutboxWriteResult,
36
- CrmBufferWriter,
37
- CrmBufferReader,
38
- CrmBufferClient,
39
- DealPipedriveIdPatch,
40
- SyncOutboxRow,
41
- } from "./crm-buffer.ts";
42
- export {
43
- BUFFER_DEAL_STAGES,
44
- FUNNEL_STAGE_TO_BUFFER_STAGE,
45
- bridgeFunnelStage,
46
- isFunnelStage,
47
- SYNC_OUTBOX_STATUSES,
48
- SYNC_OUTBOX_OPS,
49
- } from "./crm-buffer.ts";
50
- export type { BufferDealStage, SyncOutboxStatus, SyncOutboxOp } from "./crm-buffer.ts";
51
-
52
25
  export type {
53
26
  VisitorFunnelEventPayload,
54
27
  FunnelSystemTrigger,
@@ -65,6 +38,11 @@ export {
65
38
  VISITOR_BUYER_TYPES,
66
39
  FUNNEL_TRANSITIONS,
67
40
  isValidFunnelStage,
41
+ BUFFER_DEAL_STAGES,
42
+ FUNNEL_STAGE_TO_BUFFER_STAGE,
43
+ bridgeFunnelStage,
44
+ SYNC_OUTBOX_STATUSES,
45
+ SYNC_OUTBOX_OPS,
68
46
  canTransition,
69
47
  nextStages,
70
48
  reachableStages,
@@ -79,6 +57,9 @@ export type {
79
57
  VisitorFunnelIntent,
80
58
  VisitorFunnelSource,
81
59
  VisitorBuyerType,
60
+ BufferDealStage,
61
+ SyncOutboxStatus,
62
+ SyncOutboxOp,
82
63
  } from "./funnel.ts";
83
64
 
84
65
  export type { LifecycleEventPayload } from "./lifecycle.ts";
@@ -17,9 +17,9 @@ import {
17
17
  BUFFER_DEAL_STAGES,
18
18
  FUNNEL_STAGE_TO_BUFFER_STAGE,
19
19
  bridgeFunnelStage,
20
- isFunnelStage,
21
- } from "../crm-buffer.ts";
22
- import { VISITOR_FUNNEL_STAGES } from "../funnel.ts";
20
+ VISITOR_FUNNEL_STAGES,
21
+ isValidFunnelStage,
22
+ } from "../funnel.ts";
23
23
 
24
24
  test("the bridge maps every canonical funnel stage onto a valid generic stage", () => {
25
25
  const generic = new Set<string>(BUFFER_DEAL_STAGES);
@@ -39,8 +39,8 @@ test("terminal funnel stages bridge to their generic equivalents", () => {
39
39
  expect(bridgeFunnelStage("new_session")).toBe("new");
40
40
  });
41
41
 
42
- test("isFunnelStage guards the canonical catalog", () => {
43
- expect(isFunnelStage("offer_presented")).toBe(true);
44
- expect(isFunnelStage("q_website_tier")).toBe(false); // legacy UChat string
45
- expect(isFunnelStage("new")).toBe(false); // generic buffer stage, not a funnel stage
42
+ test("isValidFunnelStage guards the canonical catalog", () => {
43
+ expect(isValidFunnelStage("offer_presented")).toBe(true);
44
+ expect(isValidFunnelStage("q_website_tier")).toBe(false); // legacy UChat string
45
+ expect(isValidFunnelStage("new")).toBe(false); // generic buffer stage, not a funnel stage
46
46
  });
@@ -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
+ });
@@ -4,5 +4,12 @@ export default defineConfig({
4
4
  test: {
5
5
  environment: "node",
6
6
  include: ["src/**/*.test.ts"],
7
+ coverage: {
8
+ provider: "v8",
9
+ reporter: ["json", "json-summary", "text-summary", "html"],
10
+ reportsDirectory: "../../.coverage/integration",
11
+ include: ["src/integration/**/*.ts"],
12
+ exclude: ["src/integration/**/*.test.ts", "src/integration/**/*.d.ts"],
13
+ },
7
14
  },
8
15
  });
@@ -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