@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
@@ -0,0 +1,107 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { buildOrganizationProfile } from "../organization-profile.ts";
3
+
4
+ const baseInput = {
5
+ lang: "de",
6
+ siteUrl: "https://example.com",
7
+ brandName: "Warpgogol",
8
+ description: "Digital infrastructure",
9
+ founders: [],
10
+ boardMembers: [],
11
+ };
12
+
13
+ describe("buildOrganizationProfile", () => {
14
+ it("builds organization with canonical root URL", () => {
15
+ const result = buildOrganizationProfile(baseInput);
16
+ expect(result.organization.name).toBe("Warpgogol");
17
+ expect(result.organization.url).toBe("https://example.com/");
18
+ expect(result.organization.description).toBe("Digital infrastructure");
19
+ });
20
+
21
+ it("emits image as og-image.png absolute URL", () => {
22
+ const result = buildOrganizationProfile(baseInput);
23
+ expect(result.organization.image).toBe("https://example.com/og-image.png");
24
+ });
25
+
26
+ it("deduplicates people by name", () => {
27
+ const result = buildOrganizationProfile({
28
+ ...baseInput,
29
+ founders: [{ name: "Jane" }, { name: "John" }],
30
+ boardMembers: [{ name: "Jane" }, { name: "Bob" }],
31
+ });
32
+ expect(result.people.map((p) => p.name).sort()).toEqual(["Bob", "Jane", "John"]);
33
+ });
34
+
35
+ it("prefers team over founders+board when available", () => {
36
+ const result = buildOrganizationProfile({
37
+ ...baseInput,
38
+ founders: [{ name: "Founder" }],
39
+ boardMembers: [{ name: "Board" }],
40
+ team: [{ name: "Team Member" }],
41
+ });
42
+ expect(result.people).toHaveLength(1);
43
+ expect(result.people[0].name).toBe("Team Member");
44
+ });
45
+
46
+ it("builds address from primitives", () => {
47
+ const result = buildOrganizationProfile({
48
+ ...baseInput,
49
+ address: { street: "Test St", streetNumber: "1", zip: "12345", city: "Berlin", country: "DE" },
50
+ });
51
+ expect(result.organization.address).toEqual({
52
+ streetAddress: "Test St 1",
53
+ postalCode: "12345",
54
+ addressLocality: "Berlin",
55
+ addressCountry: "DE",
56
+ });
57
+ });
58
+
59
+ it("emits contactPoints when email or contactType provided", () => {
60
+ const result = buildOrganizationProfile({
61
+ ...baseInput,
62
+ email: "info@example.com",
63
+ contactType: "support",
64
+ });
65
+ expect(result.organization.contactPoints).toEqual([
66
+ { contactType: "support", email: "info@example.com" },
67
+ ]);
68
+ });
69
+
70
+ it("emits donationAccount from bank primitives", () => {
71
+ const result = buildOrganizationProfile({
72
+ ...baseInput,
73
+ bank: { accountHolder: "Org", iban: "DE00", bic: "BIC", bankName: "Bank", konto: "1", blz: "2" },
74
+ });
75
+ expect(result.organization.donationAccount).toEqual({
76
+ accountHolder: "Org",
77
+ iban: "DE00",
78
+ bic: "BIC",
79
+ bankName: "Bank",
80
+ accountNumber: "1",
81
+ bankCode: "2",
82
+ });
83
+ });
84
+
85
+ it("emits sameAs and logo when provided", () => {
86
+ const result = buildOrganizationProfile({
87
+ ...baseInput,
88
+ sameAs: ["https://twitter.com/example"],
89
+ logoUrl: "https://example.com/logo.png",
90
+ });
91
+ expect(result.organization.sameAs).toEqual(["https://twitter.com/example"]);
92
+ expect(result.organization.logo).toBe("https://example.com/logo.png");
93
+ });
94
+
95
+ it("emits schemaType override", () => {
96
+ const result = buildOrganizationProfile({
97
+ ...baseInput,
98
+ schemaType: ["Organization", "ProfessionalService"],
99
+ });
100
+ expect(result.organization.schemaType).toEqual(["Organization", "ProfessionalService"]);
101
+ });
102
+
103
+ it("returns empty initiatives array", () => {
104
+ const result = buildOrganizationProfile(baseInput);
105
+ expect(result.initiatives).toEqual([]);
106
+ });
107
+ });
@@ -0,0 +1,77 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { resolvePageOutput } from "../output-projection.ts";
3
+
4
+ describe("resolvePageOutput", () => {
5
+ it("returns defaults for undefined input", () => {
6
+ const result = resolvePageOutput(undefined);
7
+ expect(result.sitemap.include).toBe(true);
8
+ expect(result.sitemap.category).toBe("content");
9
+ expect(result.sitemap.includeLastmod).toBe(true);
10
+ expect(result.robots.index).toBe(true);
11
+ expect(result.robots.follow).toBe(true);
12
+ expect(result.llms.depth).toBe("full");
13
+ });
14
+
15
+ it("handles sitemap: false boolean shorthand", () => {
16
+ const result = resolvePageOutput({ sitemap: false });
17
+ expect(result.sitemap.include).toBe(false);
18
+ });
19
+
20
+ it("handles sitemap object form", () => {
21
+ const result = resolvePageOutput({
22
+ sitemap: { include: false, category: "custom", lastmod: "2024-01-01", includeLastmod: false },
23
+ });
24
+ expect(result.sitemap.include).toBe(false);
25
+ expect(result.sitemap.category).toBe("custom");
26
+ expect(result.sitemap.lastmod).toBe("2024-01-01");
27
+ expect(result.sitemap.includeLastmod).toBe(false);
28
+ });
29
+
30
+ it("uses legal category for legal semanticType", () => {
31
+ const result = resolvePageOutput({}, { semanticType: "legal" });
32
+ expect(result.sitemap.category).toBe("legal");
33
+ });
34
+
35
+ it("handles robots: false boolean shorthand", () => {
36
+ const result = resolvePageOutput({ robots: false });
37
+ expect(result.robots.index).toBe(false);
38
+ expect(result.robots.follow).toBe(false);
39
+ });
40
+
41
+ it("handles robots object form", () => {
42
+ const result = resolvePageOutput({ robots: { index: false, follow: true } });
43
+ expect(result.robots.index).toBe(false);
44
+ expect(result.robots.follow).toBe(true);
45
+ });
46
+
47
+ it("handles llms string shorthand", () => {
48
+ const result = resolvePageOutput({ llms: "index-only" });
49
+ expect(result.llms.depth).toBe("index-only");
50
+ });
51
+
52
+ it("handles llms object form with sections", () => {
53
+ const result = resolvePageOutput({
54
+ llms: { depth: "summary", sections: { exclude: ["section-1", "section-2"] } },
55
+ });
56
+ expect(result.llms.depth).toBe("summary");
57
+ expect(result.llms.sections?.exclude).toEqual(["section-1", "section-2"]);
58
+ });
59
+
60
+ it("resolves content image", () => {
61
+ const result = resolvePageOutput({
62
+ image: { url: "https://example.com/img.jpg", alt: "Test", width: 800, height: 600 },
63
+ });
64
+ expect(result.image).toEqual({
65
+ url: "https://example.com/img.jpg",
66
+ alt: "Test",
67
+ width: 800,
68
+ height: 600,
69
+ contentImage: true,
70
+ });
71
+ });
72
+
73
+ it("omits image when url is empty", () => {
74
+ const result = resolvePageOutput({ image: { url: "" } });
75
+ expect(result.image).toBeUndefined();
76
+ });
77
+ });
@@ -0,0 +1,98 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { buildPageMarkdown } from "../page-markdown.ts";
3
+ import { makePage } from "./helpers.ts";
4
+
5
+ describe("buildPageMarkdown", () => {
6
+ it("produces markdown with page title as h1", () => {
7
+ const page = makePage({ title: "Test Page" });
8
+ const result = buildPageMarkdown(page);
9
+ expect(result).toContain("# Test Page");
10
+ });
11
+
12
+ it("includes Summary section from lead", () => {
13
+ const page = makePage({ lead: "This is the lead text." });
14
+ const result = buildPageMarkdown(page);
15
+ expect(result).toContain("## Summary");
16
+ expect(result).toContain("This is the lead text.");
17
+ });
18
+
19
+ it("falls back to description for summary", () => {
20
+ const page = makePage({ description: "A test page description" });
21
+ const result = buildPageMarkdown(page);
22
+ expect(result).toContain("## Summary");
23
+ expect(result).toContain("A test page description");
24
+ });
25
+
26
+ it("includes Business context section for generic blocks", () => {
27
+ const page = makePage({
28
+ blocks: [
29
+ {
30
+ id: "block-1",
31
+ blockType: "prose",
32
+ heading: "About Us",
33
+ summary: "We are great.",
34
+ },
35
+ ],
36
+ });
37
+ const result = buildPageMarkdown(page);
38
+ expect(result).toContain("## Business context");
39
+ expect(result).toContain("### About Us");
40
+ expect(result).toContain("We are great.");
41
+ });
42
+
43
+ it("classifies legal blocks as Constraints", () => {
44
+ const page = makePage({
45
+ blocks: [
46
+ {
47
+ id: "block-1",
48
+ blockType: "prose",
49
+ heading: "Legal Notice",
50
+ summary: "Legal info.",
51
+ },
52
+ ],
53
+ });
54
+ const result = buildPageMarkdown(page);
55
+ expect(result).toContain("## Constraints");
56
+ expect(result).toContain("### Legal Notice");
57
+ });
58
+
59
+ it("includes People section when page has people", () => {
60
+ const page = makePage({
61
+ people: [{ name: "Jane Doe", role: "CEO", description: "Leader" }],
62
+ });
63
+ const result = buildPageMarkdown(page);
64
+ expect(result).toContain("## People");
65
+ expect(result).toContain("### Jane Doe");
66
+ expect(result).toContain("**CEO**");
67
+ });
68
+
69
+ it("includes Initiatives section when page has initiatives", () => {
70
+ const page = makePage({
71
+ initiatives: [{ id: "init-1", name: "Project Alpha", summary: "A project" }],
72
+ });
73
+ const result = buildPageMarkdown(page);
74
+ expect(result).toContain("## Initiatives");
75
+ expect(result).toContain("Project Alpha");
76
+ });
77
+
78
+ it("includes FAQ section when page has faqEntries", () => {
79
+ const page = makePage({
80
+ faqEntries: [{ id: "q1", question: "What is this?", answer: "A test." }],
81
+ });
82
+ const result = buildPageMarkdown(page);
83
+ expect(result).toContain("## FAQ");
84
+ expect(result).toContain("### What is this?");
85
+ expect(result).toContain("A test.");
86
+ });
87
+
88
+ it("collapses excessive blank lines", () => {
89
+ const page = makePage({
90
+ blocks: [
91
+ { id: "b1", blockType: "prose", heading: "Block 1", summary: "S1" },
92
+ { id: "b2", blockType: "prose", heading: "Block 2", summary: "S2" },
93
+ ],
94
+ });
95
+ const result = buildPageMarkdown(page);
96
+ expect(result).not.toMatch(/\n{3,}/);
97
+ });
98
+ });
@@ -0,0 +1,88 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ blocksToMarkdown,
4
+ extractAnswerBlocksFromMarkdown,
5
+ toPageEntryId,
6
+ toSemanticAnswerBlocks,
7
+ } from "../page-utils.ts";
8
+
9
+ describe("extractAnswerBlocksFromMarkdown", () => {
10
+ it("extracts h2 sections with content", () => {
11
+ const md = "## Section 1\n\nContent 1\n\n## Section 2\n\nContent 2";
12
+ const blocks = extractAnswerBlocksFromMarkdown(md);
13
+ expect(blocks).toHaveLength(2);
14
+ expect(blocks[0]).toEqual({ heading: "Section 1", content: "Content 1" });
15
+ expect(blocks[1]).toEqual({ heading: "Section 2", content: "Content 2" });
16
+ });
17
+
18
+ it("ignores content before first h2", () => {
19
+ const md = "Intro text\n\n## Section\n\nContent";
20
+ const blocks = extractAnswerBlocksFromMarkdown(md);
21
+ expect(blocks).toHaveLength(1);
22
+ expect(blocks[0].heading).toBe("Section");
23
+ });
24
+
25
+ it("returns empty for no h2", () => {
26
+ expect(extractAnswerBlocksFromMarkdown("Just text")).toEqual([]);
27
+ });
28
+ });
29
+
30
+ describe("blocksToMarkdown", () => {
31
+ it("converts blocks back to markdown", () => {
32
+ const blocks = [
33
+ { heading: "Section 1", content: "Content 1" },
34
+ { heading: "Section 2", content: "Content 2" },
35
+ ];
36
+ expect(blocksToMarkdown(blocks)).toBe("## Section 1\n\nContent 1\n\n## Section 2\n\nContent 2");
37
+ });
38
+ });
39
+
40
+ describe("toPageEntryId", () => {
41
+ it("strips language prefix and .html", () => {
42
+ expect(toPageEntryId(new URL("https://example.com/de/about.html"))).toBe("about");
43
+ });
44
+
45
+ it("handles nested paths", () => {
46
+ expect(toPageEntryId(new URL("https://example.com/de/section/page"))).toBe("section-page");
47
+ });
48
+
49
+ it("returns index for root", () => {
50
+ expect(toPageEntryId(new URL("https://example.com/de/"))).toBe("index");
51
+ });
52
+ });
53
+
54
+ describe("toSemanticAnswerBlocks", () => {
55
+ it("produces SemanticBlock with blockType prose", () => {
56
+ const blocks = [{ heading: "Section", content: "Summary line\n- fact 1\n- fact 2" }];
57
+ const result = toSemanticAnswerBlocks(blocks);
58
+ expect(result).toHaveLength(1);
59
+ expect(result[0].blockType).toBe("prose");
60
+ expect(result[0].heading).toBe("Section");
61
+ expect(result[0].summary).toBe("Summary line");
62
+ expect(result[0].facts).toEqual(["- fact 1", "- fact 2"]);
63
+ });
64
+
65
+ it("preserves table content as summary", () => {
66
+ const blocks = [
67
+ {
68
+ heading: "Pricing",
69
+ content: "| Col1 | Col2 |\n| --- | --- |\n| a | b |",
70
+ },
71
+ ];
72
+ const result = toSemanticAnswerBlocks(blocks);
73
+ expect(result[0].summary).toContain("| Col1 | Col2 |");
74
+ expect(result[0].facts).toBeUndefined();
75
+ });
76
+
77
+ it("preserves multi-paragraph content as summary", () => {
78
+ const blocks = [
79
+ {
80
+ heading: "Section",
81
+ content: "First paragraph.\n\nSecond paragraph.",
82
+ },
83
+ ];
84
+ const result = toSemanticAnswerBlocks(blocks);
85
+ expect(result[0].summary).toBe("First paragraph.\n\nSecond paragraph.");
86
+ expect(result[0].facts).toBeUndefined();
87
+ });
88
+ });
@@ -0,0 +1,88 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ AMOUNT_MARKER_RE,
4
+ OFFERING_URI_PREFIX,
5
+ PRICE_MARKER_RE,
6
+ formatSourcePrice,
7
+ resolveAmountMarkersForSemantic,
8
+ resolvePriceMarkersForSemantic,
9
+ type DerivedPriceEntry,
10
+ } from "../price-marker-resolver.ts";
11
+
12
+ describe("constants", () => {
13
+ it("OFFERING_URI_PREFIX is warpgogol offerings URL", () => {
14
+ expect(OFFERING_URI_PREFIX).toBe("https://warpgogol.com/id/offerings/");
15
+ });
16
+
17
+ it("PRICE_MARKER_RE matches price markers", () => {
18
+ const text = "{price:hosting:monthly}";
19
+ PRICE_MARKER_RE.lastIndex = 0;
20
+ const match = PRICE_MARKER_RE.exec(text);
21
+ expect(match).not.toBeNull();
22
+ expect(match![1]).toBe("hosting");
23
+ expect(match![2]).toBe("monthly");
24
+ });
25
+ });
26
+
27
+ describe("formatSourcePrice", () => {
28
+ it("formats EUR amount with narrowSymbol for de", () => {
29
+ const result = formatSourcePrice("10", "de");
30
+ expect(result).toContain("10");
31
+ expect(result).toContain("€");
32
+ });
33
+
34
+ it("returns 0 € for non-numeric amount", () => {
35
+ expect(formatSourcePrice("not-a-number", "de")).toBe("0\u00A0€");
36
+ });
37
+ });
38
+
39
+ describe("resolvePriceMarkersForSemantic", () => {
40
+ const derivedPrices: Record<string, DerivedPriceEntry[]> = {
41
+ [`${OFFERING_URI_PREFIX}hosting`]: [
42
+ {
43
+ chargeRef: "monthly",
44
+ targetCurrency: "USD",
45
+ amount: { value: "15", currency: "USD" },
46
+ trace: { source: { amount: "10", currency: "EUR" }, rate: { value: "0.9", pair: "EUR/USD" } },
47
+ },
48
+ ],
49
+ };
50
+
51
+ it("resolves price marker to source currency", () => {
52
+ const result = resolvePriceMarkersForSemantic(
53
+ "Price: {price:hosting:monthly}",
54
+ "de",
55
+ derivedPrices,
56
+ );
57
+ expect(result).toContain("10");
58
+ expect(result).toContain("€");
59
+ expect(result).not.toContain("{price:");
60
+ });
61
+
62
+ it("returns 0 € for unknown offering", () => {
63
+ const result = resolvePriceMarkersForSemantic(
64
+ "Price: {price:unknown:charge}",
65
+ "de",
66
+ derivedPrices,
67
+ );
68
+ expect(result).toContain("0");
69
+ expect(result).toContain("€");
70
+ });
71
+
72
+ it("returns text unchanged when no markers", () => {
73
+ expect(resolvePriceMarkersForSemantic("No markers", "de", derivedPrices)).toBe("No markers");
74
+ });
75
+ });
76
+
77
+ describe("resolveAmountMarkersForSemantic", () => {
78
+ it("resolves amount marker to EUR", () => {
79
+ const result = resolveAmountMarkersForSemantic("Threshold: {amount:50}", "de");
80
+ expect(result).toContain("50");
81
+ expect(result).toContain("€");
82
+ expect(result).not.toContain("{amount:");
83
+ });
84
+
85
+ it("returns text unchanged when no markers", () => {
86
+ expect(resolveAmountMarkersForSemantic("No markers", "de")).toBe("No markers");
87
+ });
88
+ });
@@ -0,0 +1,70 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { buildRobotsTxt } from "../robots.ts";
3
+
4
+ describe("buildRobotsTxt", () => {
5
+ it("generates default allow policy", () => {
6
+ const result = buildRobotsTxt({});
7
+ expect(result).toContain("User-agent: *");
8
+ expect(result).not.toContain("Disallow: /");
9
+ expect(result).toContain("Allow: /robots.txt");
10
+ expect(result).toContain("Sitemap: /sitemap.xml");
11
+ });
12
+
13
+ it("generates disallow-all for defaultPolicy: disallow", () => {
14
+ const result = buildRobotsTxt({ defaultPolicy: "disallow" });
15
+ expect(result).toContain("Disallow: /");
16
+ });
17
+
18
+ it("generates specific disallow paths", () => {
19
+ const result = buildRobotsTxt({ disallowedPaths: ["/admin", "/private"] });
20
+ expect(result).toContain("Disallow: /admin");
21
+ expect(result).toContain("Disallow: /private");
22
+ expect(result).not.toContain("Disallow: /\n");
23
+ });
24
+
25
+ it("generates allow paths in allow mode", () => {
26
+ const result = buildRobotsTxt({ allowedPaths: ["/public"] });
27
+ expect(result).toContain("Allow: /public");
28
+ });
29
+
30
+ it("emits Content-Signal directive when provided", () => {
31
+ const result = buildRobotsTxt({ contentSignal: ["ai-train", "search"] });
32
+ expect(result).toContain("Content-Signal: ai-train, search");
33
+ });
34
+
35
+ it("emits crawler blocklist", () => {
36
+ const result = buildRobotsTxt({ crawlerBlocklist: ["BadBot", "ScraperBot"] });
37
+ expect(result).toContain("User-agent: BadBot");
38
+ expect(result).toContain("User-agent: ScraperBot");
39
+ expect(result).toContain("Disallow: /");
40
+ });
41
+
42
+ it("emits crawler allowlist", () => {
43
+ const result = buildRobotsTxt({ crawlerAllowlist: ["FriendlyBot"] });
44
+ expect(result).toContain("User-agent: FriendlyBot");
45
+ expect(result).toContain("Allow: /");
46
+ });
47
+
48
+ it("emits custom per-user-agent rules", () => {
49
+ const result = buildRobotsTxt({
50
+ customRules: [{ userAgent: "GoogleBot", disallow: ["/no-google"], allow: ["/yes-google"] }],
51
+ });
52
+ expect(result).toContain("User-agent: GoogleBot");
53
+ expect(result).toContain("Disallow: /no-google");
54
+ expect(result).toContain("Allow: /yes-google");
55
+ });
56
+
57
+ it("uses custom sitemap URL", () => {
58
+ const result = buildRobotsTxt({ sitemap: "https://example.com/sitemap.xml" });
59
+ expect(result).toContain("Sitemap: https://example.com/sitemap.xml");
60
+ });
61
+
62
+ it("always allows auxiliary discovery files", () => {
63
+ const result = buildRobotsTxt({ defaultPolicy: "disallow" });
64
+ expect(result).toContain("Allow: /robots.txt");
65
+ expect(result).toContain("Allow: /llms.txt");
66
+ expect(result).toContain("Allow: /llms-full.txt");
67
+ expect(result).toContain("Allow: /sitemap.xml");
68
+ expect(result).toContain("Allow: /sitemap-content.xml");
69
+ });
70
+ });
@@ -94,4 +94,61 @@ describe("splitSentences", () => {
94
94
  expect(result[0]).toBe("Das Studio kann nicht wesentliche Prozesse (z. B. Werkzeuge) ändern.");
95
95
  expect(result[1]).toBe("Der wesentliche Leistungsumfang kann nicht einseitig geändert werden.");
96
96
  });
97
+
98
+ it("does not split on German business abbreviation zzgl.", () => {
99
+ const result = splitSentences(
100
+ "€ 49 / Monat (zzgl. USt.) — entspricht 10 Monaten. Zwei Monate gratis.",
101
+ "de",
102
+ );
103
+ expect(result).toHaveLength(2);
104
+ expect(result[0]).toBe("€ 49 / Monat (zzgl. USt.) — entspricht 10 Monaten.");
105
+ expect(result[1]).toBe("Zwei Monate gratis.");
106
+ });
107
+
108
+ it("does not split on German abbreviation inkl.", () => {
109
+ const result = splitSentences(
110
+ "Das Paket enthält Hosting (inkl. SSL-Zertifikat). Das ist ein guter Deal.",
111
+ "de",
112
+ );
113
+ expect(result).toHaveLength(2);
114
+ expect(result[0]).toBe("Das Paket enthält Hosting (inkl. SSL-Zertifikat).");
115
+ expect(result[1]).toBe("Das ist ein guter Deal.");
116
+ });
117
+
118
+ it("does not split on German abbreviation bzw.", () => {
119
+ const result = splitSentences(
120
+ "Rechnungen werden monatlich bzw. jährlich erstellt. Das ist fair.",
121
+ "de",
122
+ );
123
+ expect(result).toHaveLength(2);
124
+ expect(result[0]).toBe("Rechnungen werden monatlich bzw. jährlich erstellt.");
125
+ expect(result[1]).toBe("Das ist fair.");
126
+ });
127
+
128
+ it("does not split on German abbreviation ggf.", () => {
129
+ const result = splitSentences(
130
+ "Der Kunde kann ggf. weitere Module buchen. Das ist optional.",
131
+ "de",
132
+ );
133
+ expect(result).toHaveLength(2);
134
+ expect(result[0]).toBe("Der Kunde kann ggf. weitere Module buchen.");
135
+ expect(result[1]).toBe("Das ist optional.");
136
+ });
137
+
138
+ it("does not split on Ukrainian abbreviation тис.", () => {
139
+ const result = splitSentences(
140
+ "Вартість складає 5 тис. гривень на місяць. Це вигідна пропозиція.",
141
+ "uk",
142
+ );
143
+ expect(result).toHaveLength(2);
144
+ expect(result[0]).toBe("Вартість складає 5 тис. гривень на місяць.");
145
+ expect(result[1]).toBe("Це вигідна пропозиція.");
146
+ });
147
+
148
+ it("does not split on Ukrainian abbreviation грн.", () => {
149
+ const result = splitSentences("Ціна: 1000 грн. за місяць. Друге речення.", "uk");
150
+ expect(result).toHaveLength(2);
151
+ expect(result[0]).toBe("Ціна: 1000 грн. за місяць.");
152
+ expect(result[1]).toBe("Друге речення.");
153
+ });
97
154
  });