@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
|
@@ -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({
|
package/src/share/agent/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/src/share/env.d.ts
CHANGED
|
@@ -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"]);
|