@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
@@ -12,6 +12,7 @@ No I/O — the kernel command loads the manifest and passes it here.
12
12
  </MODULE_CONTRACT>
13
13
  <CHANGE_SUMMARY>
14
14
  <item>RFC-0783: initial API Catalog linkset+json projection.</item>
15
+ <item>RFC-0954: add search endpoint link when interfaces.search is non-null.</item>
15
16
  </CHANGE_SUMMARY>
16
17
  */
17
18
 
@@ -71,6 +72,10 @@ export function buildApiCatalog(manifest: AgentSurfaceManifest): ApiCatalog {
71
72
  entry["service"] = [{ href: manifest.interfaces.mcp.url, type: "application/json" }];
72
73
  }
73
74
 
75
+ if (manifest.interfaces.search) {
76
+ entry["search"] = [{ href: manifest.interfaces.search.url, type: "application/json" }];
77
+ }
78
+
74
79
  entry["service-doc"] = [{ href: manifest.interfaces.llms, type: "text/plain" }];
75
80
 
76
81
  return { linkset: [entry] };
@@ -105,6 +105,20 @@ export function buildArdCatalog(
105
105
  ],
106
106
  });
107
107
 
108
+ // Semantic search endpoint entry
109
+ if (manifest.interfaces.search) {
110
+ entries.push({
111
+ identifier: `urn:air:${domain}:api:search`,
112
+ displayName: "Semantic Search",
113
+ type: "application/json",
114
+ url: manifest.interfaces.search.url,
115
+ representativeQueries: [
116
+ "Search this site for relevant content",
117
+ "Find pages matching a natural language query",
118
+ ],
119
+ });
120
+ }
121
+
108
122
  // Knowledge domain entries
109
123
  for (const ref of manifest.knowledge) {
110
124
  entries.push({
@@ -14,6 +14,7 @@ here (knowledge, capability, openapi, proof) and re-export them from this barrel
14
14
  <item>RFC-0292: re-export fleet-catalog module.</item>
15
15
  <item>RFC-0783: re-export api-catalog and mcp-card modules.</item>
16
16
  <item>RFC-0786: re-export dns-aid module.</item>
17
+ <item>RFC-0954: re-export search module.</item>
17
18
  </CHANGE_SUMMARY>
18
19
  */
19
20
 
@@ -28,3 +29,4 @@ export * from "./api-catalog.ts";
28
29
  export * from "./mcp-card.ts";
29
30
  export * from "./dns-aid.ts";
30
31
  export * from "./ard-catalog.ts";
32
+ export * from "./search.ts";
@@ -14,6 +14,7 @@ two runs on unchanged input are byte-identical (AS-7).
14
14
  </MODULE_CONTRACT>
15
15
  <CHANGE_SUMMARY>
16
16
  <item>RFC-0286: initial manifest contract and pure builder.</item>
17
+ <item>RFC-0954: add search interface to AgentSurfaceManifest.interfaces.</item>
17
18
  </CHANGE_SUMMARY>
18
19
  */
19
20
 
@@ -54,6 +55,7 @@ export interface AgentSurfaceManifest {
54
55
  twins: { pattern: string } | null;
55
56
  openapi: string | null;
56
57
  mcp: { url: string; protocolVersion: string } | null;
58
+ search: { url: string; model: string; dimensions: number } | null;
57
59
  };
58
60
  /** Null until agent.surface.sign signs it (RFC-0308) or when no key material exists. */
59
61
  proof: AgentSurfaceProof | null;
@@ -68,6 +70,7 @@ export interface AgentSurfaceManifestInput {
68
70
  hasTwins?: boolean;
69
71
  openapiUrl?: string | null;
70
72
  mcp?: { url: string; protocolVersion: string } | null;
73
+ search?: { url: string; model: string; dimensions: number } | null;
71
74
  }
72
75
 
73
76
  // ---------------------------------------------------------------------------
@@ -91,10 +94,29 @@ export function canonicalJson(value: unknown): string {
91
94
  return JSON.stringify(sortKeysDeep(value));
92
95
  }
93
96
 
97
+ /**
98
+ * sha256 hex over the canonical JSON of `doc` with `contentHash`, `proof`, and any
99
+ * additional `excludeFields` removed. All signed manifest hash functions MUST use
100
+ * this helper to ensure the `proof` field is always excluded — a missing exclusion
101
+ * causes contentHash drift after signing (AGS-SEARCH-07 pattern).
102
+ */
103
+ export function computeSignedContentHash(
104
+ doc: Record<string, unknown>,
105
+ excludeFields: string[] = [],
106
+ ): string {
107
+ const exclusions = new Set(["contentHash", "proof", ...excludeFields]);
108
+ const rest: Record<string, unknown> = {};
109
+ for (const [key, value] of Object.entries(doc)) {
110
+ if (!exclusions.has(key)) {
111
+ rest[key] = value;
112
+ }
113
+ }
114
+ return createHash("sha256").update(canonicalJson(rest), "utf8").digest("hex");
115
+ }
116
+
94
117
  /** sha256 hex over the canonical JSON of `doc` with `contentHash` and `proof` removed. */
95
118
  export function computeAgentManifestContentHash(doc: Record<string, unknown>): string {
96
- const { contentHash: _contentHash, proof: _proof, ...rest } = doc;
97
- return createHash("sha256").update(canonicalJson(rest), "utf8").digest("hex");
119
+ return computeSignedContentHash(doc);
98
120
  }
99
121
 
100
122
  // ---------------------------------------------------------------------------
@@ -118,6 +140,7 @@ export function buildAgentSurfaceManifest(input: AgentSurfaceManifestInput): Age
118
140
  twins: input.hasTwins ? { pattern: "/**.md" } : null,
119
141
  openapi: input.openapiUrl ?? null,
120
142
  mcp: input.mcp ?? null,
143
+ search: input.search ?? null,
121
144
  },
122
145
  proof: null,
123
146
  };
@@ -12,6 +12,7 @@ No I/O — the kernel command loads the manifest and passes it here.
12
12
  </MODULE_CONTRACT>
13
13
  <CHANGE_SUMMARY>
14
14
  <item>RFC-0783: initial MCP Server Card projection.</item>
15
+ <item>RFC-0954: include search tool in serverInfo description when interfaces.search is non-null.</item>
15
16
  </CHANGE_SUMMARY>
16
17
  */
17
18
 
@@ -48,6 +49,9 @@ export function buildMcpServerCard(manifest: AgentSurfaceManifest): McpServerCar
48
49
  serverInfo: {
49
50
  name: `${manifest.site}-agent-gate`,
50
51
  version: manifest.surfaceVersion,
52
+ ...(manifest.interfaces.search
53
+ ? { description: "Agent gate with MCP tools and semantic search" }
54
+ : {}),
51
55
  },
52
56
  transport: {
53
57
  type: "streamable-http",
@@ -15,6 +15,7 @@ copied verbatim (AGO-04 enforces this).
15
15
  </MODULE_CONTRACT>
16
16
  <CHANGE_SUMMARY>
17
17
  <item>RFC-0289: initial OpenAPI projection.</item>
18
+ <item>RFC-0954: add /api/agent/search path with dynamic lang enum from manifest.languages.supported.</item>
18
19
  </CHANGE_SUMMARY>
19
20
  */
20
21
 
@@ -33,6 +34,13 @@ export interface OpenApiOperation {
33
34
  operationId: string;
34
35
  summary?: string;
35
36
  tags: string[];
37
+ parameters?: Array<{
38
+ name: string;
39
+ in: string;
40
+ required?: boolean;
41
+ schema: Record<string, unknown>;
42
+ description?: string;
43
+ }>;
36
44
  requestBody?: {
37
45
  required: true;
38
46
  content: { "application/json": { schema: OpenApiSchemaObject } };
@@ -136,6 +144,76 @@ export function formatAgentOpenApi(
136
144
  };
137
145
  }
138
146
 
147
+ if (manifest.interfaces.search) {
148
+ const searchSchema: OpenApiSchemaObject = {
149
+ type: "object",
150
+ required: ["query", "results", "tookMs"],
151
+ properties: {
152
+ query: { type: "string" },
153
+ results: {
154
+ type: "array",
155
+ items: {
156
+ type: "object",
157
+ properties: {
158
+ chunk: { type: "object" },
159
+ score: { type: "number" },
160
+ },
161
+ },
162
+ },
163
+ tookMs: { type: "number" },
164
+ },
165
+ };
166
+ componentSchemas["search-response"] = searchSchema;
167
+ const langEnum = manifest.languages.supported;
168
+ paths[manifest.interfaces.search.url] = {
169
+ get: {
170
+ operationId: "search.query",
171
+ summary: "Semantic search over site content",
172
+ tags: ["search"],
173
+ responses: {
174
+ "200": {
175
+ description: "Search results",
176
+ content: {
177
+ "application/json": { schema: { $ref: "#/components/schemas/search-response" } },
178
+ },
179
+ },
180
+ "400": { description: "Invalid query" },
181
+ "503": { description: "Search index not available" },
182
+ },
183
+ parameters: [
184
+ {
185
+ name: "q",
186
+ in: "query",
187
+ required: true,
188
+ schema: { type: "string", maxLength: 1000 },
189
+ description: "Natural language search query",
190
+ },
191
+ {
192
+ name: "lang",
193
+ in: "query",
194
+ required: false,
195
+ schema: { type: "string", enum: langEnum },
196
+ description: "Filter by language",
197
+ },
198
+ {
199
+ name: "type",
200
+ in: "query",
201
+ required: false,
202
+ schema: { type: "string", enum: ["page", "knowledge", "faq", "prose"] },
203
+ description: "Filter by content type",
204
+ },
205
+ {
206
+ name: "topK",
207
+ in: "query",
208
+ required: false,
209
+ schema: { type: "integer", default: 5, maximum: 20 },
210
+ description: "Number of results to return",
211
+ },
212
+ ],
213
+ },
214
+ };
215
+ }
216
+
139
217
  const doc: OpenApiDocument = {
140
218
  openapi: "3.1.0",
141
219
  info: {
@@ -0,0 +1,188 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ buildSearchManifest,
4
+ computeSearchManifestContentHash,
5
+ SEARCH_EMBEDDING_MODEL,
6
+ SEARCH_EMBEDDING_DIMENSIONS,
7
+ SEARCH_MANIFEST_SCHEMA_VERSION,
8
+ SEARCH_CHUNK_TEXT_MAX_LENGTH,
9
+ isNonKnowledgeFile,
10
+ NON_KNOWLEDGE_FILES,
11
+ type SearchChunk,
12
+ } from "./search.ts";
13
+
14
+ const baseChunk: SearchChunk = {
15
+ id: "de:/about:block-intro",
16
+ url: "/about",
17
+ lang: "de",
18
+ type: "page",
19
+ blockId: "block-intro",
20
+ heading: "About",
21
+ text: "About the company",
22
+ };
23
+
24
+ describe("buildSearchManifest", () => {
25
+ it("produces a manifest with correct schema version and model", () => {
26
+ const manifest = buildSearchManifest({
27
+ site: "test-site",
28
+ model: SEARCH_EMBEDDING_MODEL,
29
+ dimensions: SEARCH_EMBEDDING_DIMENSIONS,
30
+ generatedAt: "2026-08-27T00:00:00Z",
31
+ chunks: [baseChunk],
32
+ });
33
+ expect(manifest.schema).toBe(SEARCH_MANIFEST_SCHEMA_VERSION);
34
+ expect(manifest.model).toBe(SEARCH_EMBEDDING_MODEL);
35
+ expect(manifest.dimensions).toBe(SEARCH_EMBEDDING_DIMENSIONS);
36
+ });
37
+
38
+ it("sorts chunks by id for deterministic output", () => {
39
+ const chunkB: SearchChunk = { ...baseChunk, id: "de:/about:block-zzz" };
40
+ const chunkA: SearchChunk = { ...baseChunk, id: "de:/about:block-aaa" };
41
+ const manifest = buildSearchManifest({
42
+ site: "test-site",
43
+ model: SEARCH_EMBEDDING_MODEL,
44
+ dimensions: SEARCH_EMBEDDING_DIMENSIONS,
45
+ generatedAt: "2026-08-27T00:00:00Z",
46
+ chunks: [chunkB, chunkA],
47
+ });
48
+ expect(manifest.chunks[0].id).toBe("de:/about:block-aaa");
49
+ expect(manifest.chunks[1].id).toBe("de:/about:block-zzz");
50
+ });
51
+
52
+ it("is deterministic — same input produces same contentHash regardless of chunk order", () => {
53
+ const chunkA: SearchChunk = { ...baseChunk, id: "aaa" };
54
+ const chunkB: SearchChunk = { ...baseChunk, id: "bbb" };
55
+ const m1 = buildSearchManifest({
56
+ site: "test-site",
57
+ model: SEARCH_EMBEDDING_MODEL,
58
+ dimensions: SEARCH_EMBEDDING_DIMENSIONS,
59
+ generatedAt: "2026-08-27T00:00:00Z",
60
+ chunks: [chunkA, chunkB],
61
+ });
62
+ const m2 = buildSearchManifest({
63
+ site: "test-site",
64
+ model: SEARCH_EMBEDDING_MODEL,
65
+ dimensions: SEARCH_EMBEDDING_DIMENSIONS,
66
+ generatedAt: "2026-08-28T00:00:00Z",
67
+ chunks: [chunkB, chunkA],
68
+ });
69
+ expect(m1.contentHash).toBe(m2.contentHash);
70
+ });
71
+
72
+ it("excludes generatedAt from contentHash", () => {
73
+ const m1 = buildSearchManifest({
74
+ site: "test-site",
75
+ model: SEARCH_EMBEDDING_MODEL,
76
+ dimensions: SEARCH_EMBEDDING_DIMENSIONS,
77
+ generatedAt: "2026-08-27T00:00:00Z",
78
+ chunks: [baseChunk],
79
+ });
80
+ const m2 = buildSearchManifest({
81
+ site: "test-site",
82
+ model: SEARCH_EMBEDDING_MODEL,
83
+ dimensions: SEARCH_EMBEDDING_DIMENSIONS,
84
+ generatedAt: "2026-08-28T00:00:00Z",
85
+ chunks: [baseChunk],
86
+ });
87
+ expect(m1.contentHash).toBe(m2.contentHash);
88
+ expect(m1.generatedAt).not.toBe(m2.generatedAt);
89
+ });
90
+
91
+ it("produces different contentHash when chunks change", () => {
92
+ const m1 = buildSearchManifest({
93
+ site: "test-site",
94
+ model: SEARCH_EMBEDDING_MODEL,
95
+ dimensions: SEARCH_EMBEDDING_DIMENSIONS,
96
+ generatedAt: "2026-08-27T00:00:00Z",
97
+ chunks: [baseChunk],
98
+ });
99
+ const m2 = buildSearchManifest({
100
+ site: "test-site",
101
+ model: SEARCH_EMBEDDING_MODEL,
102
+ dimensions: SEARCH_EMBEDDING_DIMENSIONS,
103
+ generatedAt: "2026-08-27T00:00:00Z",
104
+ chunks: [{ ...baseChunk, text: "Different text" }],
105
+ });
106
+ expect(m1.contentHash).not.toBe(m2.contentHash);
107
+ });
108
+
109
+ it("produces different contentHash when site changes", () => {
110
+ const m1 = buildSearchManifest({
111
+ site: "site-a",
112
+ model: SEARCH_EMBEDDING_MODEL,
113
+ dimensions: SEARCH_EMBEDDING_DIMENSIONS,
114
+ generatedAt: "2026-08-27T00:00:00Z",
115
+ chunks: [baseChunk],
116
+ });
117
+ const m2 = buildSearchManifest({
118
+ site: "site-b",
119
+ model: SEARCH_EMBEDDING_MODEL,
120
+ dimensions: SEARCH_EMBEDDING_DIMENSIONS,
121
+ generatedAt: "2026-08-27T00:00:00Z",
122
+ chunks: [baseChunk],
123
+ });
124
+ expect(m1.contentHash).not.toBe(m2.contentHash);
125
+ });
126
+ });
127
+
128
+ describe("computeSearchManifestContentHash", () => {
129
+ it("matches the hash produced by buildSearchManifest", () => {
130
+ const manifest = buildSearchManifest({
131
+ site: "test-site",
132
+ model: SEARCH_EMBEDDING_MODEL,
133
+ dimensions: SEARCH_EMBEDDING_DIMENSIONS,
134
+ generatedAt: "2026-08-27T00:00:00Z",
135
+ chunks: [baseChunk],
136
+ });
137
+ const recomputed = computeSearchManifestContentHash(manifest);
138
+ expect(recomputed).toBe(manifest.contentHash);
139
+ });
140
+
141
+ it("excludes proof from contentHash", () => {
142
+ const manifest = buildSearchManifest({
143
+ site: "test-site",
144
+ model: SEARCH_EMBEDDING_MODEL,
145
+ dimensions: SEARCH_EMBEDDING_DIMENSIONS,
146
+ generatedAt: "2026-08-27T00:00:00Z",
147
+ chunks: [baseChunk],
148
+ });
149
+ const withProof = {
150
+ ...manifest,
151
+ proof: {
152
+ type: "Ed25519Signature2020",
153
+ created: "2026-08-27T00:00:00Z",
154
+ verificationMethod: "did:web:example.com#key-v1",
155
+ proofPurpose: "assertionMethod",
156
+ proofValue: "zFakeProofValue",
157
+ },
158
+ };
159
+ expect(computeSearchManifestContentHash(withProof)).toBe(manifest.contentHash);
160
+ });
161
+ });
162
+
163
+ describe("constants", () => {
164
+ it("SEARCH_CHUNK_TEXT_MAX_LENGTH is 2000", () => {
165
+ expect(SEARCH_CHUNK_TEXT_MAX_LENGTH).toBe(2000);
166
+ });
167
+ });
168
+
169
+ describe("isNonKnowledgeFile", () => {
170
+ it("returns true for search-manifest.json", () => {
171
+ expect(isNonKnowledgeFile("search-manifest.json")).toBe(true);
172
+ });
173
+
174
+ it("returns false for knowledge domain files", () => {
175
+ expect(isNonKnowledgeFile("company.json")).toBe(false);
176
+ expect(isNonKnowledgeFile("offer.json")).toBe(false);
177
+ expect(isNonKnowledgeFile("faq.json")).toBe(false);
178
+ });
179
+
180
+ it("returns false for arbitrary filenames", () => {
181
+ expect(isNonKnowledgeFile("foo.json")).toBe(false);
182
+ expect(isNonKnowledgeFile("readme.md")).toBe(false);
183
+ });
184
+
185
+ it("NON_KNOWLEDGE_FILES contains search-manifest.json", () => {
186
+ expect(NON_KNOWLEDGE_FILES).toContain("search-manifest.json");
187
+ });
188
+ });
@@ -0,0 +1,164 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>
4
+ RFC-0954: shared types and pure helpers for the agent semantic search capability.
5
+ Defines the search manifest shape (build-time output), query/response contracts
6
+ (runtime API), and pinned embedding model constants. Pure, deterministic — no I/O,
7
+ no timestamps in the hash payload.
8
+ </purpose>
9
+ <non-goals>
10
+ <item>Do not read files or embed text — callers (kernel commands) load content and
11
+ call buildSearchManifest with already-extracted chunks.</item>
12
+ <item>Do not call Workers AI or Vectorize — runtime logic lives in agent-gate/astro.ts.</item>
13
+ </non-goals>
14
+ </MODULE_CONTRACT>
15
+ <CHANGE_SUMMARY>
16
+ <item>RFC-0954: initial search manifest contract, query/response types, and pure builder.</item>
17
+ <item>m000101: add NON_KNOWLEDGE_FILES and isNonKnowledgeFile for shared non-knowledge-file exclusion.</item>
18
+ </CHANGE_SUMMARY>
19
+ */
20
+
21
+ import { computeSignedContentHash } from "./manifest.ts";
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Pinned embedding model constants (RFC-0954 §Embedding model)
25
+ // ---------------------------------------------------------------------------
26
+
27
+ /** Cloudflare Workers AI embedding model for semantic search (multilingual, 1024 dims). */
28
+ export const SEARCH_EMBEDDING_MODEL = "@cf/baai/bge-m3";
29
+
30
+ /** Vector dimensions for the pinned embedding model. */
31
+ export const SEARCH_EMBEDDING_DIMENSIONS = 1024;
32
+
33
+ /** Maximum number of results per query. */
34
+ export const SEARCH_MAX_TOP_K = 20;
35
+
36
+ /** Default number of results per query. */
37
+ export const SEARCH_DEFAULT_TOP_K = 5;
38
+
39
+ /** Maximum character length for a chunk's `text` field (truncated before embedding). */
40
+ export const SEARCH_CHUNK_TEXT_MAX_LENGTH = 2000;
41
+
42
+ /** Schema version for the search manifest. */
43
+ export const SEARCH_MANIFEST_SCHEMA_VERSION = "1";
44
+
45
+ /** Static asset path for the generated search manifest. */
46
+ export const SEARCH_MANIFEST_PATH = "public/api/agent/v1/search-manifest.json";
47
+
48
+ /**
49
+ * Files that live in the knowledge directory (`public/api/agent/v1/`) but are NOT
50
+ * knowledge envelopes. Used by `agent.knowledge.validate`, `agent.manifest.generate`,
51
+ * `agent.surface.validate`, and `agent.search.manifest.generate` to skip non-knowledge
52
+ * artifacts when iterating knowledge domain files.
53
+ */
54
+ export const NON_KNOWLEDGE_FILES = ["search-manifest.json"] as const;
55
+
56
+ /** Returns true if `filename` is a non-knowledge file that lives in the knowledge directory. */
57
+ export function isNonKnowledgeFile(filename: string): boolean {
58
+ return (NON_KNOWLEDGE_FILES as readonly string[]).includes(filename);
59
+ }
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Search manifest (build-time output)
63
+ // ---------------------------------------------------------------------------
64
+
65
+ /** Content type of a search chunk. */
66
+ export type SearchChunkType = "page" | "knowledge" | "faq" | "prose";
67
+
68
+ /** A single searchable content unit — one per block, knowledge entry, or FAQ entry. */
69
+ export interface SearchChunk {
70
+ /** Deterministic ID: "{lang}:{urlPath}:{blockId}" or "{lang}:knowledge:{domain}" etc. */
71
+ id: string;
72
+ /** Full URL path to the page or resource (relative to site root). */
73
+ url: string;
74
+ /** ISO 639-1 language code (e.g. "de", "uk"). */
75
+ lang: string;
76
+ /** Content type — determines metadata filtering. */
77
+ type: SearchChunkType;
78
+ /** Block ID within the page (null for knowledge/faq chunks). */
79
+ blockId: string | null;
80
+ /** Heading text extracted from the block (optional). */
81
+ heading: string | null;
82
+ /** Body text extracted from the block, truncated to SEARCH_CHUNK_TEXT_MAX_LENGTH. */
83
+ text: string;
84
+ }
85
+
86
+ /** Static search manifest — build-time output written to public/api/agent/v1/search-manifest.json. */
87
+ export interface SearchManifest {
88
+ schema: string;
89
+ site: string;
90
+ model: string;
91
+ dimensions: number;
92
+ /** sha256 hex over sorted-key JSON of this document minus contentHash + generatedAt. */
93
+ contentHash: string;
94
+ /** ISO timestamp — present in output but excluded from contentHash. */
95
+ generatedAt: string;
96
+ chunks: SearchChunk[];
97
+ }
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // Query / response (runtime API contracts)
101
+ // ---------------------------------------------------------------------------
102
+
103
+ /** Query parameters for the search endpoint. */
104
+ export interface SearchQuery {
105
+ /** Natural language search query (max 1000 chars). */
106
+ q: string;
107
+ /** Filter by language (optional). */
108
+ lang?: string;
109
+ /** Filter by content type (optional). */
110
+ type?: SearchChunkType;
111
+ /** Number of results to return (default 5, max 20). */
112
+ topK?: number;
113
+ }
114
+
115
+ /** A single search result — chunk + similarity score. */
116
+ export interface SearchResult {
117
+ chunk: SearchChunk;
118
+ /** Cosine similarity score (0..1, higher = more similar). */
119
+ score: number;
120
+ }
121
+
122
+ /** Response from the search endpoint. */
123
+ export interface SearchResponse {
124
+ query: string;
125
+ results: SearchResult[];
126
+ /** Server-side processing time in milliseconds. */
127
+ tookMs: number;
128
+ }
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // Pure helpers
132
+ // ---------------------------------------------------------------------------
133
+
134
+ /**
135
+ * Pure: assemble + hash the search manifest. Deterministic — sorts chunks by id,
136
+ * excludes generatedAt from the content hash. Two runs on unchanged input are
137
+ * byte-identical (excluding the timestamp).
138
+ */
139
+ export function buildSearchManifest(input: {
140
+ site: string;
141
+ model: string;
142
+ dimensions: number;
143
+ generatedAt: string;
144
+ chunks: SearchChunk[];
145
+ }): SearchManifest {
146
+ const sortedChunks = [...input.chunks].sort((a, b) => a.id.localeCompare(b.id));
147
+ const base: Omit<SearchManifest, "contentHash"> = {
148
+ schema: SEARCH_MANIFEST_SCHEMA_VERSION,
149
+ site: input.site,
150
+ model: input.model,
151
+ dimensions: input.dimensions,
152
+ generatedAt: input.generatedAt,
153
+ chunks: sortedChunks,
154
+ };
155
+ const contentHash = computeSearchManifestContentHash(base);
156
+ return { ...base, contentHash };
157
+ }
158
+
159
+ /** sha256 hex over the canonical JSON of the manifest minus contentHash + generatedAt + proof. */
160
+ export function computeSearchManifestContentHash(
161
+ doc: Omit<SearchManifest, "contentHash"> | Record<string, unknown>,
162
+ ): string {
163
+ return computeSignedContentHash(doc as Record<string, unknown>, ["generatedAt"]);
164
+ }
@@ -48,10 +48,6 @@ declare module "astro:env/server" {
48
48
  export const UPSTASH_QSTASH_NEXT_SIGNING_KEY: string | undefined;
49
49
  export const UPSTASH_REDIS_REST_URL: string | undefined;
50
50
  export const UPSTASH_REDIS_REST_TOKEN: string | undefined;
51
- // Lagebild MVP: Supabase CRM buffer (self-enabling — adapter skips when absent).
52
- export const SUPABASE_BUFFER_URL: string | undefined;
53
- export const SUPABASE_BUFFER_SERVICE_KEY: string | undefined;
54
- export const SUPABASE_BUFFER_TENANT_ID: string | undefined;
55
51
  // RFC-0191: Stripe billing — the webhook signing secret (stripe-webhook route) and the
56
52
  // server API key (billing client). Per-tenant; self-enabling — handlers fail-closed when absent.
57
53
  export const STRIPE_WEBHOOK_SECRET: string | undefined;
@@ -29,6 +29,7 @@ const NAMED_EFFECT_TARGETS = [
29
29
  "cta",
30
30
  "heading", // RFC-0151 — typographic effects on the section heading
31
31
  "subheading", // typographic effects on the section subheading/tagline
32
+ "background", // lighten overlay on the section background image
32
33
  ] as const;
33
34
 
34
35
  export const namedEffectTargetSchema = z.enum(NAMED_EFFECT_TARGETS);
@@ -106,12 +107,25 @@ export const tiltEffectSchema = z
106
107
  .strict();
107
108
  export type TiltEffect = z.infer<typeof tiltEffectSchema>;
108
109
 
110
+ export const lightenEffectSchema = z
111
+ .object({
112
+ kind: z.literal("lighten"),
113
+ enabled: z.boolean(),
114
+ /** 0..1; opacity of the lightening overlay above the background image. */
115
+ opacity: z.number().min(0).max(1).optional(),
116
+ /** Color alias or raw value for the overlay. Defaults to --ds-color-bg. */
117
+ color: effectColorSchema.optional(),
118
+ })
119
+ .strict();
120
+ export type LightenEffect = z.infer<typeof lightenEffectSchema>;
121
+
109
122
  export const effectSchema = z.discriminatedUnion("kind", [
110
123
  glassEffectSchema,
111
124
  shadowEffectSchema,
112
125
  glowEffectSchema,
113
126
  bulgeEffectSchema,
114
127
  tiltEffectSchema,
128
+ lightenEffectSchema,
115
129
  ]);
116
130
  export type Effect = z.infer<typeof effectSchema>;
117
131
 
@@ -128,6 +142,7 @@ export const EFFECT_KIND_META = {
128
142
  glow: { strategy: "text", maxPerStack: 1 },
129
143
  bulge: { strategy: "text", maxPerStack: 1 },
130
144
  tilt: { strategy: "text", maxPerStack: 1 },
145
+ lighten: { strategy: "surface", maxPerStack: 1 },
131
146
  } as const satisfies Record<EffectKind, { strategy: EffectRenderStrategy; maxPerStack: number }>;
132
147
 
133
148
  /**
@@ -138,6 +153,7 @@ export const EFFECT_KIND_META = {
138
153
  export const TARGET_ALLOWED_KINDS: Record<string, readonly EffectKind[]> = {
139
154
  heading: ["shadow", "glow", "bulge", "tilt"],
140
155
  subheading: ["shadow", "glow", "bulge", "tilt"],
156
+ background: ["lighten"],
141
157
  };
142
158
 
143
159
  const DEFAULT_ALLOWED_KINDS: readonly EffectKind[] = ["glass"];
@@ -19,6 +19,7 @@
19
19
  <item>RFC-0525: added av1 field to VideoManifestSources for AV1 progressive delivery.</item>
20
20
  <item>RFC-0525: added opt-in av1 boolean to mediaSchema (default false) to skip slow AV1 encoding.</item>
21
21
  <item>RFC-0591: added maxSizeMb field to mediaSchema for two-pass bitrate-capped MP4 encoding.</item>
22
+ <item>RFC-0950: added fullscreen field to mediaSchema for content-controlled edge-to-edge layout.</item>
22
23
  </CHANGE_SUMMARY>
23
24
  */
24
25
 
@@ -93,6 +94,9 @@ export const mediaSchema = z
93
94
  loop: z.boolean().optional(),
94
95
  muted: z.boolean().optional(),
95
96
  controls: z.boolean().optional(),
97
+
98
+ /** [RFC-0950] CSS edge-to-edge layout (hero-style), NOT the browser Fullscreen API. */
99
+ fullscreen: z.boolean().optional(),
96
100
  preload: z.enum(["none", "metadata", "auto"]).optional(),
97
101
 
98
102
  /** ambient/background only (RFC-0202): when the loop starts and what a tap does. */
@@ -13,6 +13,7 @@ module owns visual + structural wrapper props only.
13
13
  </MODULE_CONTRACT>
14
14
  <CHANGE_SUMMARY>
15
15
  <item>RFC-0133: backfilled MODULE_MAP and CHANGE_SUMMARY markers for compass.validate compliance.</item>
16
+ <item>Added "flush" density for sections that need zero padding (e.g. fullscreen video flush to header).
16
17
  </CHANGE_SUMMARY>
17
18
  */
18
19
 
@@ -21,7 +22,7 @@ import { sectionBackgroundSchema } from "./section-background.ts";
21
22
  import { effectsSchema } from "./effects.ts";
22
23
  import { sectionMotionConfigSchema } from "./section-motion.ts";
23
24
 
24
- export const sectionDensitySchema = z.enum(["compact", "normal", "spacious"]);
25
+ export const sectionDensitySchema = z.enum(["compact", "normal", "spacious", "flush"]);
25
26
  export type SectionDensity = z.infer<typeof sectionDensitySchema>;
26
27
 
27
28
  export const sectionToneSchema = z.enum(["default", "warning", "success", "muted"]);