@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
@@ -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: {
@@ -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
+ }
@@ -0,0 +1,8 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { buildApiCatalog } from "../api-catalog.ts";
3
+
4
+ describe("buildApiCatalog", () => {
5
+ it("is a function", () => {
6
+ expect(typeof buildApiCatalog).toBe("function");
7
+ });
8
+ });
@@ -0,0 +1,8 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { buildArdCatalog } from "../ard-catalog.ts";
3
+
4
+ describe("buildArdCatalog", () => {
5
+ it("is a function", () => {
6
+ expect(typeof buildArdCatalog).toBe("function");
7
+ });
8
+ });
@@ -0,0 +1,14 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { resolveActiveCapabilities, capabilityToActionRef } from "../capability.ts";
3
+
4
+ describe("resolveActiveCapabilities", () => {
5
+ it("is a function", () => {
6
+ expect(typeof resolveActiveCapabilities).toBe("function");
7
+ });
8
+ });
9
+
10
+ describe("capabilityToActionRef", () => {
11
+ it("is a function", () => {
12
+ expect(typeof capabilityToActionRef).toBe("function");
13
+ });
14
+ });
@@ -0,0 +1,15 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { FLEET_AGENT_CATALOG_SCHEMA, buildFleetAgentCatalog } from "../fleet-catalog.ts";
3
+
4
+ describe("FLEET_AGENT_CATALOG_SCHEMA", () => {
5
+ it("is a string constant", () => {
6
+ expect(typeof FLEET_AGENT_CATALOG_SCHEMA).toBe("string");
7
+ expect(FLEET_AGENT_CATALOG_SCHEMA).toContain("fleet.agent-catalog");
8
+ });
9
+ });
10
+
11
+ describe("buildFleetAgentCatalog", () => {
12
+ it("is a function", () => {
13
+ expect(typeof buildFleetAgentCatalog).toBe("function");
14
+ });
15
+ });