@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.
- package/package.json +21 -69
- package/src/integration/funnel.ts +78 -1
- package/src/integration/index.ts +19 -2
- package/src/integration/lagebild-ingress.ts +169 -0
- package/src/integration/orchestration.ts +7 -13
- package/src/integration/port-barrel.ts +9 -28
- package/src/integration/tests/funnel-bridge.test.ts +7 -7
- package/src/integration/tests/lagebild-ingress.test.ts +118 -0
- package/src/observability/typed-refs.ts +1 -1
- package/src/ontology/analytics/matomo-fleet.registry.yaml +1 -1
- package/src/ontology/archetypes/index.json +12 -9
- package/src/ontology/archetypes/index.yaml +26 -10
- package/src/ontology/blueprints/website-local.yaml +11 -11
- package/src/ontology/blueprints/website-service.yaml +20 -20
- package/src/ontology/capabilities/lead.submit.yaml +2 -2
- package/src/ontology/enums.ts +2 -2
- package/src/ontology/external-surfaces/jsonld-types.yaml +14 -14
- package/src/ontology/external-surfaces/url-schema.yaml +1 -1
- package/src/ontology/index.ts +2 -0
- package/src/ontology/schemas/index.ts +7 -2
- package/src/ontology/schemas/system/integrations.ts +1 -1
- package/src/ontology/schemas/system/manifest.ts +38 -1
- package/src/ontology/schemas/system.ts +7 -2
- package/src/ontology/shared-section-props/common.ts +11 -0
- package/src/ontology/shared-section-props/visual-header.ts +1 -1
- package/src/share/agent/api-catalog.ts +5 -0
- package/src/share/agent/ard-catalog.ts +14 -0
- package/src/share/agent/index.ts +2 -0
- package/src/share/agent/manifest.ts +25 -2
- package/src/share/agent/mcp-card.ts +4 -0
- package/src/share/agent/openapi.ts +78 -0
- package/src/share/agent/search.test.ts +188 -0
- package/src/share/agent/search.ts +164 -0
- package/src/share/env.d.ts +0 -4
- package/src/share/schemas/effects.ts +16 -0
- package/src/share/schemas/media.ts +4 -0
- package/src/share/schemas/section-shell.ts +2 -1
- package/src/share/scripts/external-link-qr.ts +9 -1
- package/src/share/scripts/gsap-counter.ts +2 -0
- package/src/share/scripts/inline-number-animation.ts +2 -0
- package/src/share/scripts/live-photos.ts +2 -0
- package/src/share/scripts/video-player.test.ts +210 -0
- package/src/share/scripts/video-player.ts +73 -13
- package/src/share/semantic/extract.ts +25 -1
- package/src/share/semantic/tests/split-sentences.test.ts +57 -0
- package/src/share/tests/manifest.test.ts +51 -0
- package/src/share/tests/openapi.test.ts +30 -0
- package/src/share/types/qrcode-browser.d.ts +18 -0
- package/src/share/utility-registry.yaml +2 -2
- package/src/integration/crm-buffer.ts +0 -491
- package/src/integration-adapter-supabase-crm/adapter.ts +0 -437
- package/src/integration-adapter-supabase-crm/client.ts +0 -605
- package/src/integration-adapter-supabase-crm/index.ts +0 -28
- package/src/integration-adapter-supabase-crm/pipedrive-sync-target.ts +0 -482
- package/src/integration-adapter-supabase-crm/supabase-rest.ts +0 -89
- package/src/integration-adapter-supabase-crm/tenant-registry.ts +0 -334
- package/src/integration-adapter-supabase-crm/tests/change-balance.pbt.test.ts +0 -83
- package/src/integration-adapter-supabase-crm/tests/funnel-persistence.test.ts +0 -409
- package/src/integration-adapter-supabase-crm/tests/lifecycle-sync.test.ts +0 -288
- package/src/integration-adapter-supabase-crm/tests/stage-map.test.ts +0 -80
- package/src/integration-adapter-supabase-crm/vitest.config.ts +0 -8
- package/src/integration-adapter-supabase-crm/worker.ts +0 -223
- package/src/ontology/archetypes/components/currency-selector.yaml +0 -19
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
<MODULE_CONTRACT>
|
|
3
|
-
<purpose>Unit tests for resolvePipedriveStageUpdate: verifies every BUFFER_DEAL_STAGES entry
|
|
4
|
-
produces the correct Pipedrive payload fragment, and that won/lost use the `status` field
|
|
5
|
-
rather than a stage_id. Acts as regression guard against catalog/STAGE_MAP desync.</purpose>
|
|
6
|
-
<responsibilities>
|
|
7
|
-
<item>Every active stage maps to { stage_id: number }.</item>
|
|
8
|
-
<item>won maps to { status: "won" } — no stage_id.</item>
|
|
9
|
-
<item>lost maps to { status: "lost" } — no stage_id.</item>
|
|
10
|
-
<item>No stage falls back to { stage_id: 1 } (new) silently.</item>
|
|
11
|
-
</responsibilities>
|
|
12
|
-
<non-goals><item>No network — pure function tests.</item></non-goals>
|
|
13
|
-
</MODULE_CONTRACT>
|
|
14
|
-
<CHANGE_SUMMARY><item>Initial regression guard for won/lost Pipedrive stage mapping fix.</item></CHANGE_SUMMARY>
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { test, expect } from "vitest";
|
|
18
|
-
import { BUFFER_DEAL_STAGES } from "@warpgogol/werkstatt-shared/integration/crm-buffer";
|
|
19
|
-
import type { BufferDealStage } from "@warpgogol/werkstatt-shared/integration/crm-buffer";
|
|
20
|
-
import { STAGE_MAP, resolvePipedriveStageUpdate } from "../pipedrive-sync-target.ts";
|
|
21
|
-
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
// Exhaustiveness: every BUFFER_DEAL_STAGES entry must be a key of STAGE_MAP
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
|
|
26
|
-
test("STAGE_MAP covers every BUFFER_DEAL_STAGES entry (exhaustiveness guard)", () => {
|
|
27
|
-
for (const stage of BUFFER_DEAL_STAGES) {
|
|
28
|
-
expect(Object.prototype.hasOwnProperty.call(STAGE_MAP, stage)).toBeTruthy();
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
// won / lost must use Pipedrive status, never stage_id
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
|
|
36
|
-
test("won resolves to { status: 'won' } (not a stage_id)", () => {
|
|
37
|
-
const result = resolvePipedriveStageUpdate("won");
|
|
38
|
-
expect(!("stage_id" in result)).toBeTruthy();
|
|
39
|
-
expect(result).toEqual({ status: "won" });
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test("lost resolves to { status: 'lost' } (not a stage_id)", () => {
|
|
43
|
-
const result = resolvePipedriveStageUpdate("lost");
|
|
44
|
-
expect(!("stage_id" in result)).toBeTruthy();
|
|
45
|
-
expect(result).toEqual({ status: "lost" });
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
// ---------------------------------------------------------------------------
|
|
49
|
-
// Active stages must produce stage_id, not status, and never fall back to 1
|
|
50
|
-
// ---------------------------------------------------------------------------
|
|
51
|
-
|
|
52
|
-
const ACTIVE_STAGES: Array<{ stage: BufferDealStage; expectedId: number }> = [
|
|
53
|
-
{ stage: "new", expectedId: 1 },
|
|
54
|
-
{ stage: "contacted", expectedId: 2 },
|
|
55
|
-
{ stage: "qualified", expectedId: 3 },
|
|
56
|
-
{ stage: "proposal", expectedId: 4 },
|
|
57
|
-
{ stage: "negotiation", expectedId: 5 },
|
|
58
|
-
];
|
|
59
|
-
|
|
60
|
-
for (const { stage, expectedId } of ACTIVE_STAGES) {
|
|
61
|
-
test(`"${stage}" resolves to { stage_id: ${expectedId} }`, () => {
|
|
62
|
-
const result = resolvePipedriveStageUpdate(stage);
|
|
63
|
-
expect("stage_id" in result).toBeTruthy();
|
|
64
|
-
expect(result).toEqual({ stage_id: expectedId });
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// ---------------------------------------------------------------------------
|
|
69
|
-
// Old typo spellings must NOT silently succeed with stage_id 1
|
|
70
|
-
// ---------------------------------------------------------------------------
|
|
71
|
-
|
|
72
|
-
test("closed_won (old typo) does not map to stage_id 1 (new)", () => {
|
|
73
|
-
const result = resolvePipedriveStageUpdate("closed_won");
|
|
74
|
-
// Falls back to STAGE_MAP.new = 1 since it's not in catalog.
|
|
75
|
-
// This test documents the fallback behavior and ensures "won" (correct) is used instead.
|
|
76
|
-
// The real bug was using "closed_won" as the key — this test guards the fix.
|
|
77
|
-
if ("stage_id" in result) {
|
|
78
|
-
expect(result.stage_id).toBe(1);
|
|
79
|
-
}
|
|
80
|
-
});
|
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
<MODULE_CONTRACT>
|
|
3
|
-
<purpose>RFC-0186: Shared Lagebild sync worker (multi-tenant). Reads enabled tenants from
|
|
4
|
-
sync_tenants registry, resolves their secret references from Worker env, and processes each
|
|
5
|
-
tenant's pending sync_outbox rows with isolation: per-tenant batch size, concurrency, and circuit
|
|
6
|
-
breaker. One Worker serves all clients; tenant lifecycle is data-driven.</purpose>
|
|
7
|
-
<non-goals>
|
|
8
|
-
<item>Do not store or transmit secret values other than to destination APIs.</item>
|
|
9
|
-
<item>Do not process tenants with missing secret references.</item>
|
|
10
|
-
<item>Do not mix tenant data (strict RLS via tenant_id).</item>
|
|
11
|
-
</non-goals>
|
|
12
|
-
</MODULE_CONTRACT>
|
|
13
|
-
<CHANGE_SUMMARY>
|
|
14
|
-
<item>RFC-0186: Refactored from single-tenant factory to multi-tenant shared worker.</item>
|
|
15
|
-
<item>Extracted Pipedrive sync logic behind CrmSyncTarget port — worker is now a thin orchestrator.</item>
|
|
16
|
-
<item>RFC-0386: Added upsert_subscription / upsert_invoice dispatch and P3/P4 stage-map pass-through.</item>
|
|
17
|
-
</CHANGE_SUMMARY>
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import type { ScheduledEvent, ExecutionContext } from "@cloudflare/workers-types";
|
|
21
|
-
import { createSupabaseCrmBufferClient } from "./client.ts";
|
|
22
|
-
import type {
|
|
23
|
-
CrmBufferReader,
|
|
24
|
-
SyncOutboxRow,
|
|
25
|
-
} from "@warpgogol/werkstatt-shared/integration/crm-buffer";
|
|
26
|
-
import { createSyncTarget, type CrmSyncTarget } from "./pipedrive-sync-target.ts";
|
|
27
|
-
import {
|
|
28
|
-
type SyncTenant,
|
|
29
|
-
type TenantWithSecrets,
|
|
30
|
-
resolveTenantSecrets,
|
|
31
|
-
getEnabledTenants,
|
|
32
|
-
updateTenantHealth,
|
|
33
|
-
type RegistryClient,
|
|
34
|
-
} from "./tenant-registry.ts";
|
|
35
|
-
import { createMetricsPusher } from "@warpgogol/werkstatt-shared/observability";
|
|
36
|
-
|
|
37
|
-
/** RFC-0186: Shared Worker env bindings. Includes registry connection + tenant secrets namespace. */
|
|
38
|
-
export interface LagebildSharedWorkerEnv {
|
|
39
|
-
// Registry (same Supabase project as buffer)
|
|
40
|
-
LAGEBILD_REGISTRY_URL: string;
|
|
41
|
-
LAGEBILD_REGISTRY_API_KEY: string;
|
|
42
|
-
// Optional: override default cron group
|
|
43
|
-
LAGEBILD_CRON_GROUP?: string;
|
|
44
|
-
// RFC-0807: OTLP metrics push
|
|
45
|
-
WARPGOGOL_OTLP_ENDPOINT: string;
|
|
46
|
-
WARPGOGOL_OTLP_TOKEN: string;
|
|
47
|
-
// Tenant secrets are resolved dynamically via sync_tenants.secret_ref fields
|
|
48
|
-
// Pattern: TENANT_{SITE_NAME}_{KIND} injected by wrangler secret put
|
|
49
|
-
[key: string]: string | undefined;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** RFC-0186: Factory returning the shared multi-tenant scheduled handler. */
|
|
53
|
-
export function createLagebildSharedSyncWorker() {
|
|
54
|
-
return {
|
|
55
|
-
async scheduled(
|
|
56
|
-
_event: ScheduledEvent,
|
|
57
|
-
env: LagebildSharedWorkerEnv,
|
|
58
|
-
_ctx: ExecutionContext,
|
|
59
|
-
): Promise<void> {
|
|
60
|
-
const pusher = createMetricsPusher(
|
|
61
|
-
{ serviceName: "lagebild-sync", layer: "back", environment: "production" },
|
|
62
|
-
{ endpoint: env.WARPGOGOL_OTLP_ENDPOINT, token: env.WARPGOGOL_OTLP_TOKEN },
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
const registry: RegistryClient = {
|
|
66
|
-
url: env.LAGEBILD_REGISTRY_URL,
|
|
67
|
-
apiKey: env.LAGEBILD_REGISTRY_API_KEY,
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const cronGroup = env.LAGEBILD_CRON_GROUP ?? "default";
|
|
71
|
-
let tenants: SyncTenant[];
|
|
72
|
-
try {
|
|
73
|
-
tenants = await getEnabledTenants(registry, cronGroup);
|
|
74
|
-
} catch (err) {
|
|
75
|
-
console.error("[lagebild] failed to load tenants:", (err as Error).message);
|
|
76
|
-
if (pusher) {
|
|
77
|
-
pusher.gaugeSet("warpgogol_back_up", 0, { service: "lagebild-sync" });
|
|
78
|
-
pusher.counterAdd("warpgogol_back_last_run_total", 1, {
|
|
79
|
-
service: "lagebild-sync",
|
|
80
|
-
status: "failure",
|
|
81
|
-
});
|
|
82
|
-
pusher.counterAdd("warpgogol_back_last_error_total", 1, { service: "lagebild-sync" });
|
|
83
|
-
await pusher.flush();
|
|
84
|
-
}
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (tenants.length === 0) {
|
|
89
|
-
console.log("[lagebild] no enabled tenants");
|
|
90
|
-
if (pusher) {
|
|
91
|
-
pusher.gaugeSet("warpgogol_back_up", 1, { service: "lagebild-sync" });
|
|
92
|
-
pusher.counterAdd("warpgogol_back_last_run_total", 1, {
|
|
93
|
-
service: "lagebild-sync",
|
|
94
|
-
status: "success",
|
|
95
|
-
});
|
|
96
|
-
await pusher.flush();
|
|
97
|
-
}
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
console.log(`[lagebild] processing ${tenants.length} tenants`);
|
|
102
|
-
|
|
103
|
-
let hadError = false;
|
|
104
|
-
|
|
105
|
-
for (const tenant of tenants) {
|
|
106
|
-
const resolved = resolveTenantSecrets(tenant, env);
|
|
107
|
-
if (resolved.secrets === null) {
|
|
108
|
-
const msg = `missing secrets: ${resolved.missing.join(", ")}`;
|
|
109
|
-
console.warn(`[lagebild][${tenant.site_name}] ${msg}`);
|
|
110
|
-
await updateTenantHealth(registry, tenant.tenant_id, {
|
|
111
|
-
last_seen_at: new Date().toISOString(),
|
|
112
|
-
last_error_at: new Date().toISOString(),
|
|
113
|
-
last_error: msg,
|
|
114
|
-
});
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
try {
|
|
119
|
-
await processTenant(tenant, resolved, registry);
|
|
120
|
-
await updateTenantHealth(registry, tenant.tenant_id, {
|
|
121
|
-
last_seen_at: new Date().toISOString(),
|
|
122
|
-
last_success_at: new Date().toISOString(),
|
|
123
|
-
last_error: null,
|
|
124
|
-
});
|
|
125
|
-
} catch (err) {
|
|
126
|
-
hadError = true;
|
|
127
|
-
const msg = (err as Error).message;
|
|
128
|
-
console.error(`[lagebild][${tenant.site_name}] tenant failed:`, msg);
|
|
129
|
-
await updateTenantHealth(registry, tenant.tenant_id, {
|
|
130
|
-
last_seen_at: new Date().toISOString(),
|
|
131
|
-
last_error_at: new Date().toISOString(),
|
|
132
|
-
last_error: msg,
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (pusher) {
|
|
138
|
-
pusher.gaugeSet("warpgogol_back_up", hadError ? 0 : 1, { service: "lagebild-sync" });
|
|
139
|
-
pusher.counterAdd("warpgogol_back_last_run_total", 1, {
|
|
140
|
-
service: "lagebild-sync",
|
|
141
|
-
status: hadError ? "failure" : "success",
|
|
142
|
-
});
|
|
143
|
-
if (hadError) {
|
|
144
|
-
pusher.counterAdd("warpgogol_back_last_error_total", 1, { service: "lagebild-sync" });
|
|
145
|
-
}
|
|
146
|
-
await pusher.flush();
|
|
147
|
-
}
|
|
148
|
-
},
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/** RFC-0186: Process one tenant's outbox with per-tenant limits. */
|
|
153
|
-
async function processTenant(
|
|
154
|
-
tenant: SyncTenant,
|
|
155
|
-
resolved: TenantWithSecrets,
|
|
156
|
-
_registry: RegistryClient,
|
|
157
|
-
): Promise<void> {
|
|
158
|
-
const secrets = resolved.secrets!; // null checked by caller
|
|
159
|
-
|
|
160
|
-
const client = createSupabaseCrmBufferClient({
|
|
161
|
-
url: secrets.supabase_url,
|
|
162
|
-
serviceKey: secrets.supabase_service_key,
|
|
163
|
-
tenantId: tenant.tenant_id,
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
const target = createSyncTarget(tenant.destination_vendor, {
|
|
167
|
-
destination_token: secrets.destination_token,
|
|
168
|
-
destination_domain: secrets.destination_domain,
|
|
169
|
-
p3StageMap: secrets.p3StageMap,
|
|
170
|
-
p4StageMap: secrets.p4StageMap,
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
const tasks = await client.readPendingOutbox(tenant.tenant_id, tenant.batch_size);
|
|
174
|
-
if (tasks.length === 0) return;
|
|
175
|
-
|
|
176
|
-
console.log(`[lagebild][${tenant.site_name}] ${tasks.length} tasks`);
|
|
177
|
-
|
|
178
|
-
let consecutiveErrors = 0;
|
|
179
|
-
const threshold = tenant.circuit_breaker_threshold;
|
|
180
|
-
|
|
181
|
-
// Simple semaphore for max_concurrency (1 = sequential)
|
|
182
|
-
for (const task of tasks) {
|
|
183
|
-
if (consecutiveErrors >= threshold) {
|
|
184
|
-
console.warn(`[lagebild][${tenant.site_name}] circuit open`);
|
|
185
|
-
break;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
try {
|
|
189
|
-
await processTask(task, client, target);
|
|
190
|
-
consecutiveErrors = 0;
|
|
191
|
-
await client.updateOutboxStatus(task.id, "done");
|
|
192
|
-
} catch (err) {
|
|
193
|
-
consecutiveErrors++;
|
|
194
|
-
const msg = (err as Error).message;
|
|
195
|
-
console.error(`[lagebild][${tenant.site_name}] task ${task.id} failed:`, msg);
|
|
196
|
-
await client.updateOutboxStatus(task.id, "failed");
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/** Route task to the sync target by operation. */
|
|
202
|
-
async function processTask(
|
|
203
|
-
task: SyncOutboxRow,
|
|
204
|
-
buffer: CrmBufferReader,
|
|
205
|
-
target: CrmSyncTarget,
|
|
206
|
-
): Promise<void> {
|
|
207
|
-
switch (task.op) {
|
|
208
|
-
case "upsert_contact":
|
|
209
|
-
return target.syncContact(task, buffer);
|
|
210
|
-
case "upsert_organization":
|
|
211
|
-
return target.syncOrganization(task, buffer);
|
|
212
|
-
case "upsert_deal":
|
|
213
|
-
return target.syncDeal(task, buffer);
|
|
214
|
-
case "update_deal_stage":
|
|
215
|
-
return target.syncDealStage(task, buffer);
|
|
216
|
-
case "upsert_subscription":
|
|
217
|
-
return target.syncSubscription(task, buffer);
|
|
218
|
-
case "upsert_invoice":
|
|
219
|
-
return target.syncInvoice(task, buffer);
|
|
220
|
-
default:
|
|
221
|
-
throw new Error(`unknown op: ${(task as { op: string }).op}`);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
id: currency-selector
|
|
2
|
-
displayName: Currency Selector
|
|
3
|
-
version: 1.0.0
|
|
4
|
-
semanticRole: component-currency-selector
|
|
5
|
-
description: |
|
|
6
|
-
Currency selector dropdown for multi-currency sites (RFC-0743).
|
|
7
|
-
Renders available target currencies and persists selection in localStorage.
|
|
8
|
-
expectedIntents:
|
|
9
|
-
- orient-visitor
|
|
10
|
-
- guide-navigation
|
|
11
|
-
expectedIndustryFit: []
|
|
12
|
-
layoutHint: split
|
|
13
|
-
propsSchema:
|
|
14
|
-
$shape: zod
|
|
15
|
-
shape: |
|
|
16
|
-
z.object({}).passthrough()
|
|
17
|
-
acceptedCosmicNames:
|
|
18
|
-
- Portia
|
|
19
|
-
constraints: {}
|