@warpgogol/werkstatt-shared 0.7.0 → 0.8.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.
- package/package.json +30 -14
- package/src/checks/index.ts +1 -0
- package/src/checks/result-helpers.ts +60 -6
- package/src/checks/suppressions-config.ts +26 -1
- package/src/content/system-manifest.ts +8 -0
- package/src/ontology/archetypes/index.json +17 -1
- package/src/ontology/schemas/page-entry.ts +5 -7
- package/src/ontology/schemas/system/manifest.ts +8 -0
- package/src/ontology/schemas/system/verification.ts +40 -0
- package/src/passport/sign.ts +20 -14
- package/src/share/agent/ard-catalog.ts +133 -0
- package/src/share/agent/index.ts +1 -0
- package/src/share/content-reference.ts +7 -1
- package/src/share/middleware/access-protection.ts +174 -0
- package/src/share/middleware/tests/access-protection.test.ts +123 -0
- package/src/share/page.ts +4 -2
- package/src/share/routes/template-filter.ts +36 -0
- package/src/share/routes/tests/template-filter.test.ts +40 -0
- package/src/share/scripts/lenis.ts +1 -0
- package/src/share/semantic/block-extractors/index.ts +24 -0
- package/src/share/semantic/build-page.ts +39 -1
- package/src/share/semantic/extract.ts +85 -11
- package/src/share/semantic/ids.ts +13 -0
- package/src/share/semantic/jsonld/organization.ts +20 -0
- package/src/share/semantic/jsonld/video.ts +56 -0
- package/src/share/semantic/jsonld/webpage.ts +1 -1
- package/src/share/semantic/jsonld.ts +5 -0
- package/src/share/semantic/models.ts +9 -0
- package/src/share/semantic/organization-profile.ts +3 -2
- package/src/share/semantic/page-utils.ts +4 -10
- package/src/share/semantic/tests/split-sentences.test.ts +97 -0
- package/src/share/slug/heading-slugger.ts +26 -0
- package/src/share/slug/index.ts +15 -0
- package/src/share/slug/slug-id.ts +22 -0
- package/src/share/slug/slug-url.ts +24 -0
- package/src/share/slug/strategies.ts +68 -0
- package/src/share/slug/tests/slug.test.ts +84 -0
- package/src/share/tests/ard-catalog.test.ts +129 -0
- package/src/share/tests/build-page-price-markers.test.ts +3 -0
- package/src/share/tests/jsonld-video.test.ts +143 -0
- package/src/share/tests/jsonld-webpage.test.ts +69 -0
- package/src/share/tests/organization-jsonld.test.ts +52 -0
- package/src/share/types/axiom-study.d.ts +22 -0
- package/src/share/utility-registry.yaml +36 -0
- package/src/surface/blueprint-schema.ts +1 -1
- package/src/surface/blueprint-types.ts +1 -1
- package/tsconfig.json +11 -8
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>ARD ai-catalog.json projection tests: determinism, shape, entry coverage.</purpose>
|
|
4
|
+
<keywords>ARD, ai-catalog, agent surface, test</keywords>
|
|
5
|
+
</MODULE_CONTRACT>
|
|
6
|
+
<CHANGE_SUMMARY>
|
|
7
|
+
<item>Initial ARD ai-catalog.json projection tests.</item>
|
|
8
|
+
</CHANGE_SUMMARY>
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { test, expect } from "vitest";
|
|
12
|
+
import { buildArdCatalog } from "../agent/ard-catalog.ts";
|
|
13
|
+
import { buildAgentSurfaceManifest } from "../agent/manifest.ts";
|
|
14
|
+
|
|
15
|
+
test("buildArdCatalog: produces specVersion, host, and entries array", () => {
|
|
16
|
+
const manifest = buildAgentSurfaceManifest({
|
|
17
|
+
site: "s",
|
|
18
|
+
baseUrl: "https://s.example",
|
|
19
|
+
languages: { default: "de", supported: ["de"] },
|
|
20
|
+
});
|
|
21
|
+
const catalog = buildArdCatalog(manifest, "My Site");
|
|
22
|
+
expect(catalog.specVersion).toBe("1.0");
|
|
23
|
+
expect(catalog.host.displayName).toBe("My Site");
|
|
24
|
+
expect(catalog.host.identifier).toBe("did:web:s.example");
|
|
25
|
+
expect(catalog.entries.length).toBeGreaterThan(0);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("buildArdCatalog: each entry has identifier, displayName, type, url, representativeQueries", () => {
|
|
29
|
+
const manifest = buildAgentSurfaceManifest({
|
|
30
|
+
site: "s",
|
|
31
|
+
baseUrl: "https://s.example",
|
|
32
|
+
languages: { default: "de", supported: ["de"] },
|
|
33
|
+
mcp: { url: "/api/agent/mcp", protocolVersion: "2025-06-18" },
|
|
34
|
+
knowledge: [
|
|
35
|
+
{ domain: "offer", url: "/api/agent/v1/offer.json", schema: "gogol.agent.knowledge/offer@1" },
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
const catalog = buildArdCatalog(manifest);
|
|
39
|
+
for (const entry of catalog.entries) {
|
|
40
|
+
expect(entry.identifier).toMatch(/^urn:air:s\.example:/);
|
|
41
|
+
expect(entry.displayName).toBeTruthy();
|
|
42
|
+
expect(entry.type).toBeTruthy();
|
|
43
|
+
expect(entry.url).toBeTruthy();
|
|
44
|
+
expect(entry.representativeQueries.length).toBeGreaterThanOrEqual(2);
|
|
45
|
+
expect(entry.representativeQueries.length).toBeLessThanOrEqual(5);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("buildArdCatalog: MCP interface adds MCP server entry", () => {
|
|
50
|
+
const manifest = buildAgentSurfaceManifest({
|
|
51
|
+
site: "s",
|
|
52
|
+
baseUrl: "https://s.example",
|
|
53
|
+
languages: { default: "de", supported: ["de"] },
|
|
54
|
+
mcp: { url: "/api/agent/mcp", protocolVersion: "2025-06-18" },
|
|
55
|
+
});
|
|
56
|
+
const catalog = buildArdCatalog(manifest);
|
|
57
|
+
const mcpEntry = catalog.entries.find((e) => e.identifier.includes(":server:mcp"));
|
|
58
|
+
expect(mcpEntry).toBeDefined();
|
|
59
|
+
expect(mcpEntry?.type).toBe("application/mcp-server-card+json");
|
|
60
|
+
expect(mcpEntry?.url).toBe("/.well-known/mcp/server-card.json");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("buildArdCatalog: knowledge refs produce entries", () => {
|
|
64
|
+
const manifest = buildAgentSurfaceManifest({
|
|
65
|
+
site: "s",
|
|
66
|
+
baseUrl: "https://s.example",
|
|
67
|
+
languages: { default: "de", supported: ["de"] },
|
|
68
|
+
knowledge: [
|
|
69
|
+
{ domain: "offer", url: "/api/agent/v1/offer.json", schema: "gogol.agent.knowledge/offer@1" },
|
|
70
|
+
{ domain: "team", url: "/api/agent/v1/team.json", schema: "gogol.agent.knowledge/team@1" },
|
|
71
|
+
],
|
|
72
|
+
});
|
|
73
|
+
const catalog = buildArdCatalog(manifest);
|
|
74
|
+
const knowledgeEntries = catalog.entries.filter((e) => e.identifier.includes(":knowledge:"));
|
|
75
|
+
expect(knowledgeEntries).toHaveLength(2);
|
|
76
|
+
expect(knowledgeEntries[0].url).toBe("/api/agent/v1/offer.json");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("buildArdCatalog: entries are sorted by identifier", () => {
|
|
80
|
+
const manifest = buildAgentSurfaceManifest({
|
|
81
|
+
site: "s",
|
|
82
|
+
baseUrl: "https://s.example",
|
|
83
|
+
languages: { default: "de", supported: ["de"] },
|
|
84
|
+
knowledge: [
|
|
85
|
+
{ domain: "zeta", url: "/api/agent/v1/zeta.json", schema: "gogol.agent.knowledge/zeta@1" },
|
|
86
|
+
{ domain: "alpha", url: "/api/agent/v1/alpha.json", schema: "gogol.agent.knowledge/alpha@1" },
|
|
87
|
+
],
|
|
88
|
+
});
|
|
89
|
+
const catalog = buildArdCatalog(manifest);
|
|
90
|
+
const identifiers = catalog.entries.map((e) => e.identifier);
|
|
91
|
+
const sorted = [...identifiers].sort();
|
|
92
|
+
expect(identifiers).toEqual(sorted);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("buildArdCatalog: determinism — same input produces identical output", () => {
|
|
96
|
+
const input = {
|
|
97
|
+
site: "s",
|
|
98
|
+
baseUrl: "https://s.example",
|
|
99
|
+
languages: { default: "de", supported: ["de"] },
|
|
100
|
+
knowledge: [
|
|
101
|
+
{ domain: "offer", url: "/api/agent/v1/offer.json", schema: "gogol.agent.knowledge/offer@1" },
|
|
102
|
+
],
|
|
103
|
+
mcp: { url: "/api/agent/mcp", protocolVersion: "2025-06-18" },
|
|
104
|
+
};
|
|
105
|
+
const a = buildArdCatalog(buildAgentSurfaceManifest(input), "My Site");
|
|
106
|
+
const b = buildArdCatalog(buildAgentSurfaceManifest(input), "My Site");
|
|
107
|
+
expect(JSON.stringify(a)).toBe(JSON.stringify(b));
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("buildArdCatalog: host identifier uses did:web format with domain", () => {
|
|
111
|
+
const manifest = buildAgentSurfaceManifest({
|
|
112
|
+
site: "warpgogol-com",
|
|
113
|
+
baseUrl: "https://warpgogol.com",
|
|
114
|
+
languages: { default: "de", supported: ["de"] },
|
|
115
|
+
});
|
|
116
|
+
const catalog = buildArdCatalog(manifest, "Warpgogol");
|
|
117
|
+
expect(catalog.host.identifier).toBe("did:web:warpgogol.com");
|
|
118
|
+
expect(catalog.host.displayName).toBe("Warpgogol");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("buildArdCatalog: default hostDisplayName falls back to site name", () => {
|
|
122
|
+
const manifest = buildAgentSurfaceManifest({
|
|
123
|
+
site: "my-site",
|
|
124
|
+
baseUrl: "https://example.com",
|
|
125
|
+
languages: { default: "de", supported: ["de"] },
|
|
126
|
+
});
|
|
127
|
+
const catalog = buildArdCatalog(manifest);
|
|
128
|
+
expect(catalog.host.displayName).toBe("my-site");
|
|
129
|
+
});
|
|
@@ -69,6 +69,7 @@ describe("buildSemanticPageModelWith with price markers", () => {
|
|
|
69
69
|
description: "Test description",
|
|
70
70
|
blocks: [
|
|
71
71
|
{
|
|
72
|
+
id: "hero",
|
|
72
73
|
type: "hero",
|
|
73
74
|
props: {
|
|
74
75
|
header: {
|
|
@@ -102,6 +103,7 @@ describe("buildSemanticPageModelWith with price markers", () => {
|
|
|
102
103
|
description: "Test description",
|
|
103
104
|
blocks: [
|
|
104
105
|
{
|
|
106
|
+
id: "hero",
|
|
105
107
|
type: "hero",
|
|
106
108
|
props: {
|
|
107
109
|
header: {
|
|
@@ -134,6 +136,7 @@ describe("buildSemanticPageModelWith with price markers", () => {
|
|
|
134
136
|
description: "A page without price markers",
|
|
135
137
|
blocks: [
|
|
136
138
|
{
|
|
139
|
+
id: "hero",
|
|
137
140
|
type: "hero",
|
|
138
141
|
props: {
|
|
139
142
|
header: {
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { buildVideoObjectNodes } from "../semantic/jsonld/video.ts";
|
|
3
|
+
import { createJsonLdContext } from "../semantic/jsonld/context.ts";
|
|
4
|
+
import type { SemanticPageModel, VideoSeoData } from "../semantic/models.ts";
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
<MODULE_CONTRACT>
|
|
8
|
+
<purpose>
|
|
9
|
+
RFC-0912: Tests for buildVideoObjectNodes — the JSON-LD builder that emits
|
|
10
|
+
VideoObject nodes from SemanticBlock.video data on opted-in content video blocks.
|
|
11
|
+
</purpose>
|
|
12
|
+
</MODULE_CONTRACT>
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
function makeMinimalPage(overrides: Partial<SemanticPageModel> = {}): SemanticPageModel {
|
|
16
|
+
return {
|
|
17
|
+
url: "https://example.com/uk/demo",
|
|
18
|
+
type: "article",
|
|
19
|
+
lang: "uk",
|
|
20
|
+
title: "Demo Page",
|
|
21
|
+
description: "A demo page",
|
|
22
|
+
blocks: [],
|
|
23
|
+
organization: { name: "Test Org", description: "Test", url: "https://example.com" },
|
|
24
|
+
...overrides,
|
|
25
|
+
} as unknown as SemanticPageModel;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const sampleVideoSeo: VideoSeoData = {
|
|
29
|
+
seo: {
|
|
30
|
+
name: "Demo Video",
|
|
31
|
+
description: "A demonstration of the platform",
|
|
32
|
+
uploadDate: "2026-01-15T00:00:00Z",
|
|
33
|
+
},
|
|
34
|
+
manifest: {
|
|
35
|
+
posterUrl: "https://example.com/_video/uk/demo/poster.webp",
|
|
36
|
+
contentUrl: "https://example.com/_video/uk/demo/progressive.h264.mp4",
|
|
37
|
+
durationSec: 120,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
describe("buildVideoObjectNodes", () => {
|
|
42
|
+
it("returns empty array when no blocks have video data", () => {
|
|
43
|
+
const page = makeMinimalPage({
|
|
44
|
+
blocks: [{ id: "block-1", heading: "Intro" }],
|
|
45
|
+
});
|
|
46
|
+
const ctx = createJsonLdContext(page);
|
|
47
|
+
const nodes = buildVideoObjectNodes(ctx);
|
|
48
|
+
expect(nodes).toHaveLength(0);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("emits a VideoObject node for a block with video data", () => {
|
|
52
|
+
const page = makeMinimalPage({
|
|
53
|
+
blocks: [{ id: "video-section", heading: "Demo", video: sampleVideoSeo }],
|
|
54
|
+
});
|
|
55
|
+
const ctx = createJsonLdContext(page);
|
|
56
|
+
const nodes = buildVideoObjectNodes(ctx);
|
|
57
|
+
|
|
58
|
+
expect(nodes).toHaveLength(1);
|
|
59
|
+
const node = nodes[0]!;
|
|
60
|
+
expect(node["@type"]).toBe("VideoObject");
|
|
61
|
+
expect(node.name).toBe("Demo Video");
|
|
62
|
+
expect(node.description).toBe("A demonstration of the platform");
|
|
63
|
+
expect(node.uploadDate).toBe("2026-01-15T00:00:00Z");
|
|
64
|
+
expect(node.thumbnailUrl).toBe("https://example.com/_video/uk/demo/poster.webp");
|
|
65
|
+
expect(node.contentUrl).toBe("https://example.com/_video/uk/demo/progressive.h264.mp4");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("includes ISO 8601 duration when durationSec is present", () => {
|
|
69
|
+
const page = makeMinimalPage({
|
|
70
|
+
blocks: [{ id: "v1", heading: "Demo", video: sampleVideoSeo }],
|
|
71
|
+
});
|
|
72
|
+
const ctx = createJsonLdContext(page);
|
|
73
|
+
const nodes = buildVideoObjectNodes(ctx);
|
|
74
|
+
|
|
75
|
+
expect(nodes[0]!.duration).toBe("PT2M0S");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("omits duration when durationSec is absent", () => {
|
|
79
|
+
const videoWithoutDuration: VideoSeoData = {
|
|
80
|
+
seo: sampleVideoSeo.seo,
|
|
81
|
+
manifest: {
|
|
82
|
+
posterUrl: sampleVideoSeo.manifest.posterUrl,
|
|
83
|
+
contentUrl: sampleVideoSeo.manifest.contentUrl,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
const page = makeMinimalPage({
|
|
87
|
+
blocks: [{ id: "v1", heading: "Demo", video: videoWithoutDuration }],
|
|
88
|
+
});
|
|
89
|
+
const ctx = createJsonLdContext(page);
|
|
90
|
+
const nodes = buildVideoObjectNodes(ctx);
|
|
91
|
+
|
|
92
|
+
expect(nodes[0]!.duration).toBeUndefined();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("emits multiple nodes for multiple video blocks", () => {
|
|
96
|
+
const page = makeMinimalPage({
|
|
97
|
+
blocks: [
|
|
98
|
+
{ id: "v1", heading: "Demo 1", video: sampleVideoSeo },
|
|
99
|
+
{
|
|
100
|
+
id: "v2",
|
|
101
|
+
heading: "Demo 2",
|
|
102
|
+
video: {
|
|
103
|
+
seo: { name: "Second", description: "Second video", uploadDate: "2026-02-01" },
|
|
104
|
+
manifest: {
|
|
105
|
+
posterUrl: "https://example.com/_video/uk/demo2/poster.webp",
|
|
106
|
+
contentUrl: "https://example.com/_video/uk/demo2/progressive.h264.mp4",
|
|
107
|
+
durationSec: 3661,
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
});
|
|
113
|
+
const ctx = createJsonLdContext(page);
|
|
114
|
+
const nodes = buildVideoObjectNodes(ctx);
|
|
115
|
+
|
|
116
|
+
expect(nodes).toHaveLength(2);
|
|
117
|
+
expect(nodes[0]!.name).toBe("Demo Video");
|
|
118
|
+
expect(nodes[1]!.name).toBe("Second");
|
|
119
|
+
expect(nodes[1]!.duration).toBe("PT1H1M1S");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("generates unique @id per video block", () => {
|
|
123
|
+
const page = makeMinimalPage({
|
|
124
|
+
blocks: [
|
|
125
|
+
{ id: "v1", heading: "Demo 1", video: sampleVideoSeo },
|
|
126
|
+
{
|
|
127
|
+
id: "v2",
|
|
128
|
+
heading: "Demo 2",
|
|
129
|
+
video: {
|
|
130
|
+
...sampleVideoSeo,
|
|
131
|
+
seo: { name: "Second", description: "d", uploadDate: "2026-02-01" },
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
});
|
|
136
|
+
const ctx = createJsonLdContext(page);
|
|
137
|
+
const nodes = buildVideoObjectNodes(ctx);
|
|
138
|
+
|
|
139
|
+
expect(nodes[0]!["@id"]).not.toBe(nodes[1]!["@id"]);
|
|
140
|
+
expect(nodes[0]!["@id"]).toContain("v1");
|
|
141
|
+
expect(nodes[1]!["@id"]).toContain("v2");
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Test that buildWebPageNode emits correct speakable cssSelector matching the rendered section-header__subheading class.</purpose>
|
|
4
|
+
</MODULE_CONTRACT>
|
|
5
|
+
<CHANGE_SUMMARY>
|
|
6
|
+
<item>Initial test for speakable cssSelector referencing section-header__subheading (not section-header__lead).</item>
|
|
7
|
+
</CHANGE_SUMMARY>
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it } from "vitest";
|
|
11
|
+
import { buildWebPageNode } from "../semantic/jsonld/webpage.ts";
|
|
12
|
+
import { createJsonLdContext } from "../semantic/jsonld/context.ts";
|
|
13
|
+
import type { SemanticPageModel, SemanticOrganization } from "../semantic/models.ts";
|
|
14
|
+
|
|
15
|
+
function makeModel(overrides: Partial<SemanticPageModel> = {}): SemanticPageModel {
|
|
16
|
+
const org: SemanticOrganization = {
|
|
17
|
+
name: "Test Org",
|
|
18
|
+
description: "Test organization",
|
|
19
|
+
url: "https://example.com",
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
type: "home",
|
|
23
|
+
lang: "de",
|
|
24
|
+
url: "https://example.com/de",
|
|
25
|
+
title: "Test Page",
|
|
26
|
+
description: "Test description",
|
|
27
|
+
breadcrumbs: [],
|
|
28
|
+
blocks: [],
|
|
29
|
+
organization: org,
|
|
30
|
+
...overrides,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe("buildWebPageNode speakable cssSelector", () => {
|
|
35
|
+
it("references section-header__subheading (not section-header__lead) when page has lead", () => {
|
|
36
|
+
const model = makeModel({
|
|
37
|
+
lead: "Test lead text",
|
|
38
|
+
blocks: [{ id: "block-1", heading: "Section heading" }],
|
|
39
|
+
});
|
|
40
|
+
const context = createJsonLdContext(model);
|
|
41
|
+
const node = buildWebPageNode(context);
|
|
42
|
+
const speakable = node.speakable as { cssSelector: string[] };
|
|
43
|
+
expect(speakable).toBeDefined();
|
|
44
|
+
expect(speakable.cssSelector).toContain("h1");
|
|
45
|
+
expect(speakable.cssSelector).toContain(".section-header__subheading");
|
|
46
|
+
expect(speakable.cssSelector).not.toContain(".section-header__lead");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("emits only h1 selector when page has no lead", () => {
|
|
50
|
+
const model = makeModel({
|
|
51
|
+
blocks: [{ id: "block-1", heading: "Section heading" }],
|
|
52
|
+
});
|
|
53
|
+
const context = createJsonLdContext(model);
|
|
54
|
+
const node = buildWebPageNode(context);
|
|
55
|
+
const speakable = node.speakable as { cssSelector: string[] };
|
|
56
|
+
expect(speakable).toBeDefined();
|
|
57
|
+
expect(speakable.cssSelector).toEqual(["h1"]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("omits speakable when no blocks have headings", () => {
|
|
61
|
+
const model = makeModel({
|
|
62
|
+
lead: "Test lead text",
|
|
63
|
+
blocks: [{ id: "block-1", heading: "" }],
|
|
64
|
+
});
|
|
65
|
+
const context = createJsonLdContext(model);
|
|
66
|
+
const node = buildWebPageNode(context);
|
|
67
|
+
expect(node.speakable).toBeUndefined();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -72,3 +72,55 @@ describe("RFC-0745: buildOrganizationNode makesOffer priceCurrency", () => {
|
|
|
72
72
|
expect(makesOffer[1].priceCurrency).toBe("EUR");
|
|
73
73
|
});
|
|
74
74
|
});
|
|
75
|
+
|
|
76
|
+
describe("buildOrganizationNode priceRange", () => {
|
|
77
|
+
it("emits priceRange as min–max with currency when multiple prices exist", () => {
|
|
78
|
+
const org = makeOrgWithPrices([
|
|
79
|
+
{ id: "monthly", label: "Monthly", amount: "70.00", currency: "EUR" },
|
|
80
|
+
{ id: "yearly", label: "Yearly", amount: "700.00", currency: "EUR" },
|
|
81
|
+
{ id: "setup", label: "Setup", amount: "200.00", currency: "EUR" },
|
|
82
|
+
]);
|
|
83
|
+
const context = createJsonLdContext(makeModel(org));
|
|
84
|
+
const node = buildOrganizationNode(context);
|
|
85
|
+
expect(node.priceRange).toBe("70–700 EUR");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("emits single-amount priceRange when all prices are equal", () => {
|
|
89
|
+
const org = makeOrgWithPrices([
|
|
90
|
+
{ id: "monthly", label: "Monthly", amount: "70.00", currency: "EUR" },
|
|
91
|
+
]);
|
|
92
|
+
const context = createJsonLdContext(makeModel(org));
|
|
93
|
+
const node = buildOrganizationNode(context);
|
|
94
|
+
expect(node.priceRange).toBe("70 EUR");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("omits priceRange when no prices exist", () => {
|
|
98
|
+
const org: SemanticOrganization = {
|
|
99
|
+
name: "Test Org",
|
|
100
|
+
description: "Test organization",
|
|
101
|
+
url: "https://example.com",
|
|
102
|
+
};
|
|
103
|
+
const context = createJsonLdContext(makeModel(org));
|
|
104
|
+
const node = buildOrganizationNode(context);
|
|
105
|
+
expect(node.priceRange).toBeUndefined();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("omits priceRange when price amounts are non-numeric", () => {
|
|
109
|
+
const org = makeOrgWithPrices([
|
|
110
|
+
{ id: "custom", label: "Custom", amount: "auf Anfrage", currency: "EUR" },
|
|
111
|
+
]);
|
|
112
|
+
const context = createJsonLdContext(makeModel(org));
|
|
113
|
+
const node = buildOrganizationNode(context);
|
|
114
|
+
expect(node.priceRange).toBe("");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("emits priceRange without currency when prices lack currency", () => {
|
|
118
|
+
const org = makeOrgWithPrices([
|
|
119
|
+
{ id: "monthly", label: "Monthly", amount: "70.00" },
|
|
120
|
+
{ id: "setup", label: "Setup", amount: "200.00" },
|
|
121
|
+
]);
|
|
122
|
+
const context = createJsonLdContext(makeModel(org));
|
|
123
|
+
const node = buildOrganizationNode(context);
|
|
124
|
+
expect(node.priceRange).toBe("70–200");
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
declare module "@syrokomskyi/axiom-study" {
|
|
2
|
+
export interface Finding {
|
|
3
|
+
findingId: string;
|
|
4
|
+
semanticFingerprint: { algorithm: "sha256"; digest: string; size: number; mediaType: string };
|
|
5
|
+
methodologyId: string;
|
|
6
|
+
ruleId: string;
|
|
7
|
+
affectedSubjectId: string;
|
|
8
|
+
title: string;
|
|
9
|
+
severity: "info" | "low" | "medium" | "high" | "critical";
|
|
10
|
+
evidence: Array<{
|
|
11
|
+
evidenceRef: {
|
|
12
|
+
artifactId: string;
|
|
13
|
+
rootDigest: { algorithm: "sha256"; digest: string; size: number; mediaType: string };
|
|
14
|
+
schema: string;
|
|
15
|
+
};
|
|
16
|
+
selector: string;
|
|
17
|
+
evidenceClass: string;
|
|
18
|
+
}>;
|
|
19
|
+
uncertainty: unknown[];
|
|
20
|
+
extension: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
utilities:
|
|
2
|
+
- id: slug
|
|
3
|
+
canonicalPath: packages/werkstatt-shared/src/share/slug/
|
|
4
|
+
forbiddenImports:
|
|
5
|
+
- "@sindresorhus/slugify"
|
|
6
|
+
- "cyrillic-to-translit-js"
|
|
7
|
+
- "github-slugger"
|
|
8
|
+
functionNames:
|
|
9
|
+
- slugify
|
|
10
|
+
- toSlug
|
|
11
|
+
- makeSlug
|
|
12
|
+
- createSlug
|
|
13
|
+
- slugUrl
|
|
14
|
+
- slugId
|
|
15
|
+
patterns:
|
|
16
|
+
- id: nkfd-diacritic-strip
|
|
17
|
+
regex: "\\.normalize\\(.{0,5}NFKD.{0,5}\\)\\.replace"
|
|
18
|
+
description: NFKD normalize + diacritic strip + non-alphanumeric replace (legacy custom slugify pattern)
|
|
19
|
+
allowlist:
|
|
20
|
+
- path: packages/werkstatt-shared/src/share/slug/
|
|
21
|
+
reason: Canonical slug module — owns all slug generation logic (RFC-0915, DNA-88)
|
|
22
|
+
- path: packages/werkstatt-shared/src/share/semantic/extract.ts
|
|
23
|
+
reason: CHANGE_SUMMARY comment references slugify removal — no function definition remains
|
|
24
|
+
- path: packages/werkstatt-site/src/checks/tests/utility-provenance.test.ts
|
|
25
|
+
reason: Test file intentionally contains forbidden patterns to validate the provenance validator (RFC-0916)
|
|
26
|
+
- path: packages/werkstatt-site/src/checks/utility-provenance.ts
|
|
27
|
+
reason: Validator implementation references forbidden imports/function names in string literals for detection logic (RFC-0916)
|
|
28
|
+
- id: template-filter
|
|
29
|
+
canonicalPath: packages/werkstatt-shared/src/share/routes/
|
|
30
|
+
forbiddenImports: []
|
|
31
|
+
functionNames:
|
|
32
|
+
- hasPlaceholderRoutes
|
|
33
|
+
patterns: []
|
|
34
|
+
allowlist:
|
|
35
|
+
- path: packages/werkstatt-shared/src/share/routes/
|
|
36
|
+
reason: Canonical placeholder route filter — owns all template detection logic (RFC-0917)
|
|
@@ -53,7 +53,7 @@ const pillarAdaptationSchema = z.object({
|
|
|
53
53
|
const pillarProductPriceSchema = z.object({
|
|
54
54
|
heading: localizedString,
|
|
55
55
|
body: localizedString,
|
|
56
|
-
priceRef: z.string().min(1),
|
|
56
|
+
priceRef: z.string().min(1).optional(),
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
const pillarFinalCtaSchema = z.object({
|
package/tsconfig.json
CHANGED
|
@@ -2,19 +2,22 @@
|
|
|
2
2
|
"compilerOptions": {
|
|
3
3
|
"target": "ES2022",
|
|
4
4
|
"module": "preserve",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"isolatedModules": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
5
12
|
"noEmit": true,
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
6
14
|
"declaration": true,
|
|
7
15
|
"rewriteRelativeImportExtensions": true,
|
|
8
|
-
"types": [
|
|
9
|
-
"node"
|
|
10
|
-
],
|
|
16
|
+
"types": ["node"],
|
|
11
17
|
"rootDir": ".",
|
|
12
|
-
"outDir": "dist"
|
|
13
|
-
"allowImportingTsExtensions": true
|
|
18
|
+
"outDir": "dist"
|
|
14
19
|
},
|
|
15
|
-
"include": [
|
|
16
|
-
"src/**/*.ts"
|
|
17
|
-
],
|
|
20
|
+
"include": ["src/**/*.ts"],
|
|
18
21
|
"exclude": [
|
|
19
22
|
"src/**/*.template.*",
|
|
20
23
|
"src/**/*.test.ts",
|