@warpgogol/werkstatt-shared 0.2.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 +352 -0
- package/src/checks/index.ts +24 -0
- package/src/checks/lib/astro-site-url.ts +30 -0
- package/src/checks/lib/i18n.ts +30 -0
- package/src/checks/result-helpers.ts +102 -0
- package/src/checks/suppressions-config.ts +195 -0
- package/src/index.ts +14 -0
- package/src/integration/README.md +38 -0
- package/src/integration/adapters.ts +143 -0
- package/src/integration/crm-buffer.ts +491 -0
- package/src/integration/dispatch.ts +106 -0
- package/src/integration/funnel.ts +379 -0
- package/src/integration/index.ts +56 -0
- package/src/integration/lifecycle.ts +155 -0
- package/src/integration/orchestration.ts +439 -0
- package/src/integration/port-barrel.ts +124 -0
- package/src/integration/port.ts +143 -0
- package/src/integration/qstash.ts +134 -0
- package/src/integration/sharding.ts +133 -0
- package/src/integration/tests/deliver-event.test.ts +52 -0
- package/src/integration/tests/dispatch.test.ts +88 -0
- package/src/integration/tests/funnel-bridge.test.ts +46 -0
- package/src/integration/tests/hub-runtime.test.ts +128 -0
- package/src/integration/tests/lifecycle.test.ts +69 -0
- package/src/integration/tests/qstash.test.ts +83 -0
- package/src/integration/tests/sharding.test.ts +64 -0
- package/src/integration/vitest.config.ts +8 -0
- package/src/integration-adapter-supabase-crm/adapter.ts +437 -0
- package/src/integration-adapter-supabase-crm/client.ts +605 -0
- package/src/integration-adapter-supabase-crm/index.ts +28 -0
- package/src/integration-adapter-supabase-crm/pipedrive-sync-target.ts +482 -0
- package/src/integration-adapter-supabase-crm/supabase-rest.ts +89 -0
- package/src/integration-adapter-supabase-crm/tenant-registry.ts +334 -0
- package/src/integration-adapter-supabase-crm/tests/change-balance.pbt.test.ts +83 -0
- package/src/integration-adapter-supabase-crm/tests/funnel-persistence.test.ts +409 -0
- package/src/integration-adapter-supabase-crm/tests/lifecycle-sync.test.ts +288 -0
- package/src/integration-adapter-supabase-crm/tests/stage-map.test.ts +80 -0
- package/src/integration-adapter-supabase-crm/vitest.config.ts +8 -0
- package/src/integration-adapter-supabase-crm/worker.ts +223 -0
- package/src/observability/conventions.ts +93 -0
- package/src/observability/env-resolver.ts +57 -0
- package/src/observability/index.ts +57 -0
- package/src/observability/metric-registry.ts +184 -0
- package/src/observability/otlp-converter.ts +125 -0
- package/src/observability/otlp-json.ts +135 -0
- package/src/observability/otlp-transport.ts +68 -0
- package/src/observability/pusher.ts +142 -0
- package/src/observability/redact.ts +27 -0
- package/src/observability/tests/conventions.test.ts +100 -0
- package/src/observability/tests/metric-registry.test.ts +117 -0
- package/src/observability/tests/otlp-converter.pbt.test.ts +77 -0
- package/src/observability/tests/otlp-converter.test.ts +157 -0
- package/src/observability/tests/otlp-json.test.ts +120 -0
- package/src/observability/tests/pusher.test.ts +75 -0
- package/src/observability/tests/redact.pbt.test.ts +71 -0
- package/src/observability/tests/redact.test.ts +37 -0
- package/src/observability/typed-refs.ts +137 -0
- package/src/observability/vitest.config.ts +8 -0
- package/src/ontology/analytics/matomo-binding.yaml +172 -0
- package/src/ontology/analytics/matomo-fleet.registry.yaml +26 -0
- package/src/ontology/analytics/messkanon.yaml +151 -0
- package/src/ontology/archetype-registry.ts +61 -0
- package/src/ontology/archetypes/components/brand-label.yaml +19 -0
- package/src/ontology/archetypes/components/copyright.yaml +19 -0
- package/src/ontology/archetypes/components/currency-aware-price-display.yaml +19 -0
- package/src/ontology/archetypes/components/currency-selector.yaml +19 -0
- package/src/ontology/archetypes/components/footer-promo.yaml +19 -0
- package/src/ontology/archetypes/components/lang-switcher.yaml +19 -0
- package/src/ontology/archetypes/components/layout.yaml +19 -0
- package/src/ontology/archetypes/components/live-photo.yaml +20 -0
- package/src/ontology/archetypes/components/material-credit.yaml +20 -0
- package/src/ontology/archetypes/components/media.yaml +21 -0
- package/src/ontology/archetypes/components/nachweis-card.yaml +34 -0
- package/src/ontology/archetypes/components/nachweis-detail.yaml +43 -0
- package/src/ontology/archetypes/components/nachweis-list.yaml +37 -0
- package/src/ontology/archetypes/components/nachweis-verify.yaml +31 -0
- package/src/ontology/archetypes/components/not-found.yaml +22 -0
- package/src/ontology/archetypes/components/offer-capacity.yaml +21 -0
- package/src/ontology/archetypes/components/passport-header.yaml +19 -0
- package/src/ontology/archetypes/components/passport-provenance.yaml +20 -0
- package/src/ontology/archetypes/components/passport-score-grid.yaml +20 -0
- package/src/ontology/archetypes/components/passport-star-map.yaml +20 -0
- package/src/ontology/archetypes/components/person-profile.yaml +20 -0
- package/src/ontology/archetypes/components/pulsar.yaml +20 -0
- package/src/ontology/archetypes/components/responsive-image.yaml +19 -0
- package/src/ontology/archetypes/components/scroll-to-top.yaml +22 -0
- package/src/ontology/archetypes/components/social-meta.yaml +20 -0
- package/src/ontology/archetypes/components/structured-data.yaml +19 -0
- package/src/ontology/archetypes/index.json +1003 -0
- package/src/ontology/archetypes/index.yaml +821 -0
- package/src/ontology/archetypes/sections/approach.yaml +23 -0
- package/src/ontology/archetypes/sections/article-list.yaml +30 -0
- package/src/ontology/archetypes/sections/audience-cards.yaml +30 -0
- package/src/ontology/archetypes/sections/breadcrumbs.yaml +21 -0
- package/src/ontology/archetypes/sections/chat-widget.yaml +37 -0
- package/src/ontology/archetypes/sections/comparison-cards.yaml +31 -0
- package/src/ontology/archetypes/sections/controlled-responsibility-block.yaml +34 -0
- package/src/ontology/archetypes/sections/credits.yaml +29 -0
- package/src/ontology/archetypes/sections/donation-card.yaml +53 -0
- package/src/ontology/archetypes/sections/donation-use.yaml +23 -0
- package/src/ontology/archetypes/sections/dynamic-status-block.yaml +35 -0
- package/src/ontology/archetypes/sections/faq-list.yaml +26 -0
- package/src/ontology/archetypes/sections/final-cta.yaml +53 -0
- package/src/ontology/archetypes/sections/hero-decision-card.yaml +65 -0
- package/src/ontology/archetypes/sections/hero.yaml +50 -0
- package/src/ontology/archetypes/sections/impact.yaml +23 -0
- package/src/ontology/archetypes/sections/markdown.yaml +43 -0
- package/src/ontology/archetypes/sections/mountain-journey.yaml +43 -0
- package/src/ontology/archetypes/sections/navigation.yaml +20 -0
- package/src/ontology/archetypes/sections/notausgang-block.yaml +41 -0
- package/src/ontology/archetypes/sections/open-source-registry.yaml +26 -0
- package/src/ontology/archetypes/sections/ownership-block.yaml +57 -0
- package/src/ontology/archetypes/sections/people.yaml +48 -0
- package/src/ontology/archetypes/sections/price-card.yaml +50 -0
- package/src/ontology/archetypes/sections/problem.yaml +23 -0
- package/src/ontology/archetypes/sections/send-message.yaml +63 -0
- package/src/ontology/archetypes/sections/service-metadata-block.yaml +40 -0
- package/src/ontology/archetypes/sections/social-proof.yaml +27 -0
- package/src/ontology/archetypes/sections/transparency.yaml +32 -0
- package/src/ontology/archetypes/sections/trust-strip.yaml +42 -0
- package/src/ontology/archetypes/sections/video-section.yaml +27 -0
- package/src/ontology/archetypes/sections/women.yaml +29 -0
- package/src/ontology/biome-token-projection.ts +229 -0
- package/src/ontology/biomes/check-concrete-blueprint.yaml +118 -0
- package/src/ontology/biomes/handwerk-material-warm.yaml +135 -0
- package/src/ontology/biomes/nonprofit-trust.yaml +123 -0
- package/src/ontology/blueprints/offer.yaml +39 -0
- package/src/ontology/blueprints/prompts/local-market-signal.md +13 -0
- package/src/ontology/blueprints/prompts/website-local-narrative.md +24 -0
- package/src/ontology/blueprints/ratgeber.yaml +62 -0
- package/src/ontology/blueprints/website-local.yaml +309 -0
- package/src/ontology/blueprints/website-service.yaml +84 -0
- package/src/ontology/capabilities/lead.submit.yaml +44 -0
- package/src/ontology/cosmic/index.ts +48 -0
- package/src/ontology/cosmic/moon-catalog.ts +173 -0
- package/src/ontology/cosmic/planet-catalog.ts +82 -0
- package/src/ontology/cosmic/star-catalog.ts +337 -0
- package/src/ontology/enums.ts +159 -0
- package/src/ontology/external-surfaces/index.ts +158 -0
- package/src/ontology/external-surfaces/jsonld-types.yaml +135 -0
- package/src/ontology/external-surfaces/sitemap-shape.yaml +6 -0
- package/src/ontology/external-surfaces/url-schema.yaml +95 -0
- package/src/ontology/growth/events/contact-email-click.yaml +21 -0
- package/src/ontology/growth/events/contact-form-submit.yaml +25 -0
- package/src/ontology/growth/events/contact-phone-click.yaml +21 -0
- package/src/ontology/growth/events/contact-route-click.yaml +21 -0
- package/src/ontology/growth/events/contact-submit.yaml +28 -0
- package/src/ontology/growth/events/contact-whatsapp-click.yaml +21 -0
- package/src/ontology/growth/events/cta-click.yaml +30 -0
- package/src/ontology/growth/events/donation-confirmed.yaml +32 -0
- package/src/ontology/growth/events/donation-form-start.yaml +21 -0
- package/src/ontology/growth/events/donation-form-submit.yaml +29 -0
- package/src/ontology/growth/events/donation-intent.yaml +29 -0
- package/src/ontology/growth/events/form-error.yaml +27 -0
- package/src/ontology/growth/events/form-start.yaml +21 -0
- package/src/ontology/growth/events/form-submit.yaml +21 -0
- package/src/ontology/growth/events/outbound-click.yaml +25 -0
- package/src/ontology/growth/events/page-view.yaml +24 -0
- package/src/ontology/growth/events/passport-view.yaml +28 -0
- package/src/ontology/growth/events/star-map-navigate.yaml +30 -0
- package/src/ontology/index.ts +165 -0
- package/src/ontology/manifest.ts +344 -0
- package/src/ontology/operations/index.ts +1 -0
- package/src/ontology/schemas/biome.ts +303 -0
- package/src/ontology/schemas/capability.ts +104 -0
- package/src/ontology/schemas/constellation.ts +114 -0
- package/src/ontology/schemas/dns-records.ts +53 -0
- package/src/ontology/schemas/index.ts +95 -0
- package/src/ontology/schemas/manifest-resolver.ts +123 -0
- package/src/ontology/schemas/page-entry.ts +213 -0
- package/src/ontology/schemas/section-archetype.ts +109 -0
- package/src/ontology/schemas/site-family.ts +98 -0
- package/src/ontology/schemas/system/growth.ts +80 -0
- package/src/ontology/schemas/system/integrations.ts +143 -0
- package/src/ontology/schemas/system/manifest.ts +492 -0
- package/src/ontology/schemas/system/page-output.ts +99 -0
- package/src/ontology/schemas/system/release.ts +58 -0
- package/src/ontology/schemas/system/text.ts +58 -0
- package/src/ontology/schemas/system.ts +53 -0
- package/src/ontology/shared-section-props/body-fragments.ts +182 -0
- package/src/ontology/shared-section-props/catalog.ts +195 -0
- package/src/ontology/shared-section-props/common.ts +178 -0
- package/src/ontology/shared-section-props/index.ts +34 -0
- package/src/ontology/shared-section-props/visual-header.ts +163 -0
- package/src/ontology/site-families/charity-donation-trust/cultural-rules.yaml +6 -0
- package/src/ontology/site-families/charity-donation-trust/family.yaml +44 -0
- package/src/ontology/site-families/charity-donation-trust/linguistic-rules.yaml +6 -0
- package/src/ontology/site-families/check-quality-operator/cultural-rules.yaml +6 -0
- package/src/ontology/site-families/check-quality-operator/family.yaml +40 -0
- package/src/ontology/site-families/check-quality-operator/linguistic-rules.yaml +6 -0
- package/src/ontology/site-families/handwerk-trust-engineering/cultural-rules.yaml +6 -0
- package/src/ontology/site-families/handwerk-trust-engineering/family.yaml +53 -0
- package/src/ontology/site-families/handwerk-trust-engineering/linguistic-rules.yaml +6 -0
- package/src/ontology/tests/catalogs.pbt.test.ts +89 -0
- package/src/ontology/tests/enums.test.ts +203 -0
- package/src/ontology/tests/sternsystem-owner.test.ts +63 -0
- package/src/ontology/vitest.config.ts +8 -0
- package/src/passport/dht-sign.test.ts +99 -0
- package/src/passport/dht-sign.ts +73 -0
- package/src/passport/identity-sign.test.ts +134 -0
- package/src/passport/identity-sign.ts +85 -0
- package/src/passport/index.ts +53 -0
- package/src/passport/key-rotate.ts +133 -0
- package/src/passport/schema.ts +198 -0
- package/src/passport/sign.ts +225 -0
- package/src/passport/tests/schema.pbt.test.ts +93 -0
- package/src/passport/tests/schema.test.ts +222 -0
- package/src/passport/vitest.config.ts +8 -0
- package/src/share/agent/api-catalog.ts +77 -0
- package/src/share/agent/capability.ts +68 -0
- package/src/share/agent/dns-aid.test.ts +74 -0
- package/src/share/agent/dns-aid.ts +53 -0
- package/src/share/agent/fleet-catalog.ts +149 -0
- package/src/share/agent/index.ts +29 -0
- package/src/share/agent/knowledge.ts +96 -0
- package/src/share/agent/manifest.ts +126 -0
- package/src/share/agent/mcp-card.ts +63 -0
- package/src/share/agent/omit-empty.ts +77 -0
- package/src/share/agent/openapi.ts +157 -0
- package/src/share/agent/proof.ts +61 -0
- package/src/share/attribution-display.ts +86 -0
- package/src/share/check-hints.ts +38 -0
- package/src/share/content/entity-id.ts +65 -0
- package/src/share/content/index.ts +16 -0
- package/src/share/content/merge.ts +86 -0
- package/src/share/content/resolve-field-path.ts +39 -0
- package/src/share/content/substitute-deep.ts +47 -0
- package/src/share/content-discipline/index.ts +14 -0
- package/src/share/content-discipline/parsers.ts +66 -0
- package/src/share/content-discipline/types.ts +118 -0
- package/src/share/content-reference.ts +333 -0
- package/src/share/counter-utils.ts +75 -0
- package/src/share/css-value-normalize.ts +48 -0
- package/src/share/dev-props-validator.ts +131 -0
- package/src/share/entitlement.ts +172 -0
- package/src/share/env.d.ts +73 -0
- package/src/share/feature-policy.ts +367 -0
- package/src/share/formula-eval.ts +364 -0
- package/src/share/fs/index.ts +96 -0
- package/src/share/i18n/localization.ts +57 -0
- package/src/share/image-provider.ts +314 -0
- package/src/share/index.ts +86 -0
- package/src/share/knowledge/claim.ts +129 -0
- package/src/share/knowledge/derivation.ts +55 -0
- package/src/share/knowledge/freshness.ts +127 -0
- package/src/share/knowledge/index.ts +23 -0
- package/src/share/knowledge/ledger.ts +222 -0
- package/src/share/knowledge/plan.ts +122 -0
- package/src/share/knowledge/source.ts +170 -0
- package/src/share/legal/translation-policy.ts +133 -0
- package/src/share/material-credits.ts +181 -0
- package/src/share/middleware/language-redirect.ts +49 -0
- package/src/share/offer-capacity.ts +149 -0
- package/src/share/onboarding-yaml/index.ts +162 -0
- package/src/share/page.ts +351 -0
- package/src/share/redirects.test.ts +62 -0
- package/src/share/redirects.ts +54 -0
- package/src/share/rfc0042-utils.ts +95 -0
- package/src/share/runtime-context.ts +70 -0
- package/src/share/schemas/claim-records.ts +69 -0
- package/src/share/schemas/claims.ts +150 -0
- package/src/share/schemas/effects.ts +212 -0
- package/src/share/schemas/features.ts +147 -0
- package/src/share/schemas/horizontal-align.ts +22 -0
- package/src/share/schemas/icon-color.ts +27 -0
- package/src/share/schemas/index.ts +43 -0
- package/src/share/schemas/live-photo.ts +53 -0
- package/src/share/schemas/material-credit.ts +292 -0
- package/src/share/schemas/media.ts +220 -0
- package/src/share/schemas/navigation.ts +74 -0
- package/src/share/schemas/page-base.ts +33 -0
- package/src/share/schemas/participant.ts +247 -0
- package/src/share/schemas/person.ts +24 -0
- package/src/share/schemas/print.ts +93 -0
- package/src/share/schemas/section-background.ts +106 -0
- package/src/share/schemas/section-body.ts +138 -0
- package/src/share/schemas/section-cards.ts +37 -0
- package/src/share/schemas/section-cta.ts +74 -0
- package/src/share/schemas/section-header.ts +77 -0
- package/src/share/schemas/section-image.ts +77 -0
- package/src/share/schemas/section-motion.ts +52 -0
- package/src/share/schemas/section-shell.ts +63 -0
- package/src/share/schemas/section-stats.ts +37 -0
- package/src/share/schemas/site-background.ts +78 -0
- package/src/share/schemas/standard-list-item.ts +38 -0
- package/src/share/scripts/external-links.ts +45 -0
- package/src/share/scripts/gsap-counter.ts +168 -0
- package/src/share/scripts/gsap-mountain-journey.ts +236 -0
- package/src/share/scripts/gsap-parallax.ts +50 -0
- package/src/share/scripts/gsap-reveal.ts +65 -0
- package/src/share/scripts/gsap-stagger.ts +54 -0
- package/src/share/scripts/index.ts +25 -0
- package/src/share/scripts/inline-number-animation.ts +120 -0
- package/src/share/scripts/lenis.ts +109 -0
- package/src/share/scripts/live-photos.ts +250 -0
- package/src/share/scripts/lordicon.ts +94 -0
- package/src/share/scripts/orchestrator.ts +138 -0
- package/src/share/scripts/scheduler.ts +53 -0
- package/src/share/scripts/video-player.ts +116 -0
- package/src/share/semantic/ai.ts +140 -0
- package/src/share/semantic/block-extraction.ts +52 -0
- package/src/share/semantic/block-extractors/index.ts +502 -0
- package/src/share/semantic/breadcrumbs.ts +120 -0
- package/src/share/semantic/build-page.ts +309 -0
- package/src/share/semantic/business-projection.ts +278 -0
- package/src/share/semantic/derived-prices-loader.ts +36 -0
- package/src/share/semantic/extract.ts +118 -0
- package/src/share/semantic/feed.ts +124 -0
- package/src/share/semantic/ids.ts +76 -0
- package/src/share/semantic/image-sitemap.ts +153 -0
- package/src/share/semantic/index.ts +36 -0
- package/src/share/semantic/jsonld/article.ts +47 -0
- package/src/share/semantic/jsonld/breadcrumb.ts +47 -0
- package/src/share/semantic/jsonld/collection-list.ts +30 -0
- package/src/share/semantic/jsonld/context.ts +46 -0
- package/src/share/semantic/jsonld/faq.ts +49 -0
- package/src/share/semantic/jsonld/initiative.ts +53 -0
- package/src/share/semantic/jsonld/organization.ts +101 -0
- package/src/share/semantic/jsonld/person.ts +71 -0
- package/src/share/semantic/jsonld/service.ts +80 -0
- package/src/share/semantic/jsonld/shared.ts +31 -0
- package/src/share/semantic/jsonld/types.ts +18 -0
- package/src/share/semantic/jsonld/webpage.ts +98 -0
- package/src/share/semantic/jsonld/website.ts +27 -0
- package/src/share/semantic/jsonld.ts +75 -0
- package/src/share/semantic/llms-policy.ts +74 -0
- package/src/share/semantic/llms.ts +394 -0
- package/src/share/semantic/markdown-hygiene.ts +68 -0
- package/src/share/semantic/markdown-twin-provenance.ts +242 -0
- package/src/share/semantic/models.ts +402 -0
- package/src/share/semantic/organization-profile.ts +160 -0
- package/src/share/semantic/output-projection.ts +131 -0
- package/src/share/semantic/page-builders/markdown-page.ts +84 -0
- package/src/share/semantic/page-markdown.ts +234 -0
- package/src/share/semantic/page-utils.ts +125 -0
- package/src/share/semantic/price-marker-resolver.ts +92 -0
- package/src/share/semantic/robots.ts +127 -0
- package/src/share/semantic/update-stamp.ts +136 -0
- package/src/share/shared-context.ts +177 -0
- package/src/share/string-utils.ts +19 -0
- package/src/share/tests/api-catalog.test.ts +97 -0
- package/src/share/tests/breadcrumbs.test.ts +126 -0
- package/src/share/tests/build-page-price-markers.test.ts +160 -0
- package/src/share/tests/capability.test.ts +107 -0
- package/src/share/tests/claim.test.ts +90 -0
- package/src/share/tests/content-ref-index.test.ts +328 -0
- package/src/share/tests/create-dev-props-validator.test.ts +38 -0
- package/src/share/tests/derivation.test.ts +39 -0
- package/src/share/tests/dev-props-validator.test.ts +88 -0
- package/src/share/tests/fleet-catalog.test.ts +134 -0
- package/src/share/tests/formula-eval.test.ts +619 -0
- package/src/share/tests/freshness.test.ts +60 -0
- package/src/share/tests/hero-section-numbering-parity.test.ts +51 -0
- package/src/share/tests/image-sitemap.test.ts +101 -0
- package/src/share/tests/jsonld-service.test.ts +282 -0
- package/src/share/tests/knowledge.test.ts +86 -0
- package/src/share/tests/ledger.test.ts +125 -0
- package/src/share/tests/llms-0789.test.ts +63 -0
- package/src/share/tests/manifest.test.ts +100 -0
- package/src/share/tests/markdown-twin-provenance.test.ts +108 -0
- package/src/share/tests/mcp-card.test.ts +57 -0
- package/src/share/tests/onboarding-yaml.test.ts +111 -0
- package/src/share/tests/ontology-role-by-cosmic-name.test.ts +28 -0
- package/src/share/tests/openapi.test.ts +116 -0
- package/src/share/tests/organization-jsonld.test.ts +74 -0
- package/src/share/tests/output-projection.test.ts +61 -0
- package/src/share/tests/plan.test.ts +97 -0
- package/src/share/tests/price-marker-resolver.test.ts +141 -0
- package/src/share/tests/proof.test.ts +72 -0
- package/src/share/tests/resolve-field-path.test.ts +52 -0
- package/src/share/tests/source.test.ts +81 -0
- package/src/share/tests/substitute-deep.test.ts +65 -0
- package/src/share/tests/text-normalize.pbt.test.ts +143 -0
- package/src/share/tests/text-normalize.test.ts +285 -0
- package/src/share/tests/twin-path.test.ts +43 -0
- package/src/share/text-normalize.ts +548 -0
- package/src/share/text-position.ts +19 -0
- package/src/share/types/hls-js-light.d.ts +16 -0
- package/src/share/visibility.ts +167 -0
- package/src/share/vitest.config.ts +8 -0
- package/src/share/wrap-inline-numbers.ts +139 -0
- package/src/surface/blueprint-schema.ts +314 -0
- package/src/surface/blueprint-types.ts +256 -0
- package/src/surface/blueprint.ts +300 -0
- package/src/surface/breaker.ts +57 -0
- package/src/surface/decision-composer.ts +156 -0
- package/src/surface/eligibility.ts +372 -0
- package/src/surface/evidence-records.ts +97 -0
- package/src/surface/fleet.ts +60 -0
- package/src/surface/geo.ts +170 -0
- package/src/surface/governance.ts +146 -0
- package/src/surface/index.ts +216 -0
- package/src/surface/io/index.ts +18 -0
- package/src/surface/io/surface-module-context-io.ts +60 -0
- package/src/surface/io/visibility-outcomes-io.ts +46 -0
- package/src/surface/module-context.ts +120 -0
- package/src/surface/substance.ts +161 -0
- package/src/surface/tests/eligibility.pbt.test.ts +137 -0
- package/src/surface/tests/eligibility.test.ts +302 -0
- package/src/surface/types.ts +289 -0
- package/src/surface/visibility.ts +57 -0
- package/src/surface/vitest.config.ts +8 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { test, expect } from "vitest";
|
|
2
|
+
import { buildPage, type PageEntry } from "../page.ts";
|
|
3
|
+
import { EMPTY_RUNTIME_CONTEXT } from "../runtime-context.ts";
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
<MODULE_CONTRACT>
|
|
7
|
+
<purpose>
|
|
8
|
+
RFC-0263: parity fixture — captures buildPage's hideSectionNumber injection
|
|
9
|
+
behavior for hero-role blocks (Europa, Phobos) and confirms a non-hero block
|
|
10
|
+
never receives the prop. Written against the roleByCosmicName-driven
|
|
11
|
+
implementation (which replaced the UNNUMBERED_HERO_PLANETS literal set) to
|
|
12
|
+
guard byte-identical output going forward.
|
|
13
|
+
</purpose>
|
|
14
|
+
</MODULE_CONTRACT>
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
function fixtureEntry(): PageEntry {
|
|
18
|
+
return {
|
|
19
|
+
kind: "page",
|
|
20
|
+
cosmicStar: "TestStar",
|
|
21
|
+
title: "Test",
|
|
22
|
+
description: "Test page",
|
|
23
|
+
lang: "en",
|
|
24
|
+
blocks: [
|
|
25
|
+
{ id: "hero-block", type: "Europa", props: { title: "Hero" } }, // cosmic-literals-ignore: fixture cosmicName for the hero-role parity assertion
|
|
26
|
+
{ id: "hero-decision-block", type: "Phobos", props: { title: "Decision hero" } }, // cosmic-literals-ignore: fixture cosmicName for the hero-role parity assertion
|
|
27
|
+
{ id: "problem-block", type: "Callisto", props: { title: "Problem" } }, // cosmic-literals-ignore: fixture cosmicName for the non-hero control assertion
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
test("buildPage: hero-role blocks (Europa, Phobos) get hideSectionNumber injected", async () => {
|
|
33
|
+
const entry = fixtureEntry();
|
|
34
|
+
const ctx = EMPTY_RUNTIME_CONTEXT("en");
|
|
35
|
+
|
|
36
|
+
const page = await buildPage(entry, ctx, {
|
|
37
|
+
resolveImportPath: () => "@warpgogol/werkstatt-site/ui/sections/fixture-section.astro",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(page.blocks.length).toBe(3);
|
|
41
|
+
|
|
42
|
+
const hero = page.blocks.find((b) => b.id === "hero-block");
|
|
43
|
+
expect(hero?.props).toEqual({ title: "Hero", hideSectionNumber: true });
|
|
44
|
+
|
|
45
|
+
const heroDecision = page.blocks.find((b) => b.id === "hero-decision-block");
|
|
46
|
+
expect(heroDecision?.props).toEqual({ title: "Decision hero", hideSectionNumber: true });
|
|
47
|
+
|
|
48
|
+
const problem = page.blocks.find((b) => b.id === "problem-block");
|
|
49
|
+
expect(problem?.props).toEqual({ title: "Problem" });
|
|
50
|
+
expect("hideSectionNumber" in (problem?.props ?? {})).toBe(false);
|
|
51
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { test, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
harvestContentImage,
|
|
4
|
+
generateImageSitemapXml,
|
|
5
|
+
isHtmlRedirectPage,
|
|
6
|
+
isSyntheticPreviewUrl,
|
|
7
|
+
type SitemapImageEntry,
|
|
8
|
+
} from "../semantic/image-sitemap.ts";
|
|
9
|
+
|
|
10
|
+
const SITE = "https://example.org";
|
|
11
|
+
|
|
12
|
+
test("harvests a hero data-content-image and absolutizes the /_astro src", () => {
|
|
13
|
+
const html = `
|
|
14
|
+
<link rel="canonical" href="https://example.org/de/about">
|
|
15
|
+
<img src="/_astro/hero-1.BEybXadf.webp" alt="A photo" class="hero__portrait-image" data-content-image>
|
|
16
|
+
`;
|
|
17
|
+
const r = harvestContentImage(html, SITE);
|
|
18
|
+
expect(r.loc).toBe("https://example.org/de/about");
|
|
19
|
+
expect(r.imageUrls).toEqual(["https://example.org/_astro/hero-1.BEybXadf.webp"]);
|
|
20
|
+
expect(r.title).toBe("A photo");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("harvests the authored x-content-image head signal", () => {
|
|
24
|
+
const html = `
|
|
25
|
+
<link rel="canonical" href="https://example.org/">
|
|
26
|
+
<meta name="x-content-image" content="https://cdn.example.org/lead.webp">
|
|
27
|
+
<meta property="og:image:alt" content="Lead alt">
|
|
28
|
+
`;
|
|
29
|
+
const r = harvestContentImage(html, SITE);
|
|
30
|
+
expect(r.imageUrls).toEqual(["https://cdn.example.org/lead.webp"]);
|
|
31
|
+
expect(r.title).toBe("Lead alt");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("excludes RFC-0150 preview screenshots and og-image fallbacks", () => {
|
|
35
|
+
expect(isSyntheticPreviewUrl("https://example.org/preview/de/home.png")).toBe(true);
|
|
36
|
+
expect(isSyntheticPreviewUrl("https://example.org/og-image.png")).toBe(true);
|
|
37
|
+
expect(isSyntheticPreviewUrl("https://example.org/_astro/hero.abc.webp")).toBe(false);
|
|
38
|
+
|
|
39
|
+
const html = `
|
|
40
|
+
<link rel="canonical" href="https://example.org/">
|
|
41
|
+
<meta name="x-content-image" content="/preview/de/home.png">
|
|
42
|
+
<img src="/og-image.png" data-content-image>
|
|
43
|
+
`;
|
|
44
|
+
const r = harvestContentImage(html, SITE);
|
|
45
|
+
expect(r.imageUrls).toEqual([]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("reports multiple distinct content images (uniqueness violation surface)", () => {
|
|
49
|
+
const html = `
|
|
50
|
+
<link rel="canonical" href="https://example.org/x">
|
|
51
|
+
<img src="/_astro/a.1.webp" data-content-image>
|
|
52
|
+
<meta name="x-content-image" content="https://cdn.example.org/b.webp">
|
|
53
|
+
`;
|
|
54
|
+
const r = harvestContentImage(html, SITE);
|
|
55
|
+
expect(r.imageUrls.length).toBe(2);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("deduplicates the same image referenced twice", () => {
|
|
59
|
+
const html = `
|
|
60
|
+
<link rel="canonical" href="https://example.org/x">
|
|
61
|
+
<img src="/_astro/a.1.webp" data-content-image>
|
|
62
|
+
<meta name="x-content-image" content="/_astro/a.1.webp">
|
|
63
|
+
`;
|
|
64
|
+
const r = harvestContentImage(html, SITE);
|
|
65
|
+
expect(r.imageUrls).toEqual(["https://example.org/_astro/a.1.webp"]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("returns null loc when no canonical link present", () => {
|
|
69
|
+
const r = harvestContentImage(`<img src="/_astro/a.webp" data-content-image>`, SITE);
|
|
70
|
+
expect(r.loc).toBe(null);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("detects meta-refresh stubs but not soft window.location redirects on content pages", () => {
|
|
74
|
+
expect(isHtmlRedirectPage(`<meta http-equiv="refresh" content="0; url=/de/">`)).toBe(true);
|
|
75
|
+
// Full content pages carry a soft language redirect (RFC-0159) — NOT a stub.
|
|
76
|
+
expect(
|
|
77
|
+
isHtmlRedirectPage(`<main>real content</main><script>window.location.href="/en/"</script>`),
|
|
78
|
+
).toBe(false);
|
|
79
|
+
expect(isHtmlRedirectPage(`<main>real content</main>`)).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("formats a valid image sitemap with escaping", () => {
|
|
83
|
+
const entries: SitemapImageEntry[] = [
|
|
84
|
+
{
|
|
85
|
+
loc: "https://example.org/a?x=1&y=2",
|
|
86
|
+
imageUrl: "https://example.org/_astro/a.webp",
|
|
87
|
+
title: "T & <b>",
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
const xml = generateImageSitemapXml(entries);
|
|
91
|
+
expect(xml).toMatch(/xmlns:image="http:\/\/www\.google\.com\/schemas\/sitemap-image\/1\.1"/);
|
|
92
|
+
expect(xml).toMatch(/<image:loc>https:\/\/example\.org\/_astro\/a\.webp<\/image:loc>/);
|
|
93
|
+
expect(xml).toMatch(/<loc>https:\/\/example\.org\/a\?x=1&y=2<\/loc>/);
|
|
94
|
+
expect(xml).toMatch(/<image:title>T & <b><\/image:title>/);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("empty entries still yields a valid urlset", () => {
|
|
98
|
+
const xml = generateImageSitemapXml([]);
|
|
99
|
+
expect(xml).toMatch(/<urlset/);
|
|
100
|
+
expect(xml).toMatch(/<\/urlset>/);
|
|
101
|
+
});
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>RFC-0492/RFC-0498: JSON-LD test — surface pages emit industry Service node
|
|
4
|
+
where Service is required (website-local depth-1, website-service depth-1, website-local depth-5)
|
|
5
|
+
and no Service nodes where Service is prohibited (depth-0, 2, 3, 4). Org-level Service nodes
|
|
6
|
+
and services ItemList are suppressed for all surface pages.</purpose>
|
|
7
|
+
</MODULE_CONTRACT>
|
|
8
|
+
<CHANGE_SUMMARY>
|
|
9
|
+
<item>RFC-0492: initial JSON-LD test for industry Service node emission and org-level suppression.</item>
|
|
10
|
+
<item>RFC-0498: extend tests for website-service depth-1, website-local depth-5, and prohibited depths (0, 2, 3, 4).</item>
|
|
11
|
+
</CHANGE_SUMMARY>
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, expect, it } from "vitest";
|
|
15
|
+
import { buildJsonLd } from "../semantic/jsonld.ts";
|
|
16
|
+
import type { SemanticPageModel, SemanticOrganization } from "../semantic/models.ts";
|
|
17
|
+
|
|
18
|
+
function makeOrg(): SemanticOrganization {
|
|
19
|
+
return {
|
|
20
|
+
name: "Test Org",
|
|
21
|
+
description: "Test organization",
|
|
22
|
+
url: "https://example.com",
|
|
23
|
+
services: [
|
|
24
|
+
{ id: "webdesign", name: "Webdesign", description: "Webdesign service" },
|
|
25
|
+
{ id: "seo", name: "SEO", description: "SEO service" },
|
|
26
|
+
],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function makeModel(overrides: Partial<SemanticPageModel> = {}): SemanticPageModel {
|
|
31
|
+
return {
|
|
32
|
+
type: "home",
|
|
33
|
+
lang: "de",
|
|
34
|
+
url: "https://example.com/de/website/elektriker",
|
|
35
|
+
title: "Website für Elektriker",
|
|
36
|
+
description: "Professionelle Websites für Elektriker",
|
|
37
|
+
breadcrumbs: [],
|
|
38
|
+
blocks: [],
|
|
39
|
+
organization: makeOrg(),
|
|
40
|
+
...overrides,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe("RFC-0492/RFC-0498: JSON-LD Service node for surface pages", () => {
|
|
45
|
+
it("emits industry-specific Service node for depth-1 website-local page", () => {
|
|
46
|
+
const model = makeModel({
|
|
47
|
+
surfaceId: "website-local",
|
|
48
|
+
depth: 1,
|
|
49
|
+
industryService: {
|
|
50
|
+
serviceType: "Website für Elektriker",
|
|
51
|
+
description: "Professionelle Websites für Elektriker",
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const doc = buildJsonLd(model);
|
|
56
|
+
const serviceNodes = doc["@graph"].filter(
|
|
57
|
+
(n) => (n as { "@type"?: string | string[] })["@type"] === "Service",
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
expect(serviceNodes.length).toBe(1);
|
|
61
|
+
const node = serviceNodes[0] as Record<string, unknown>;
|
|
62
|
+
expect(node["serviceType"]).toBe("Website für Elektriker");
|
|
63
|
+
expect(node["name"]).toBe("Website für Elektriker");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("suppresses org-level Service nodes for depth-1 website-local page", () => {
|
|
67
|
+
const model = makeModel({
|
|
68
|
+
surfaceId: "website-local",
|
|
69
|
+
depth: 1,
|
|
70
|
+
industryService: {
|
|
71
|
+
serviceType: "Website für Elektriker",
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const doc = buildJsonLd(model);
|
|
76
|
+
const serviceNodes = doc["@graph"].filter(
|
|
77
|
+
(n) => (n as { "@type"?: string | string[] })["@type"] === "Service",
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// Should have exactly 1 (the industry node), not 2 (org-level services)
|
|
81
|
+
expect(serviceNodes.length).toBe(1);
|
|
82
|
+
const node = serviceNodes[0] as Record<string, unknown>;
|
|
83
|
+
expect(node["serviceType"]).toBe("Website für Elektriker");
|
|
84
|
+
// Should NOT have org-level service names
|
|
85
|
+
expect(node["name"]).not.toBe("Webdesign");
|
|
86
|
+
expect(node["name"]).not.toBe("SEO");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("emits org-level Service nodes for non-surface pages", () => {
|
|
90
|
+
const model = makeModel({
|
|
91
|
+
surfaceId: "other-surface",
|
|
92
|
+
depth: 1,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const doc = buildJsonLd(model);
|
|
96
|
+
const serviceNodes = doc["@graph"].filter(
|
|
97
|
+
(n) => (n as { "@type"?: string | string[] })["@type"] === "Service",
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Should have org-level services (Webdesign, SEO)
|
|
101
|
+
expect(serviceNodes.length).toBe(2);
|
|
102
|
+
const names = serviceNodes.map((n) => (n as Record<string, unknown>)["name"]);
|
|
103
|
+
expect(names).toContain("Webdesign");
|
|
104
|
+
expect(names).toContain("SEO");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("emits industry-specific Service node for website-service depth-1 page", () => {
|
|
108
|
+
const model = makeModel({
|
|
109
|
+
surfaceId: "website-service",
|
|
110
|
+
depth: 1,
|
|
111
|
+
industryService: {
|
|
112
|
+
serviceType: "Website für Elektriker — SEO",
|
|
113
|
+
description: "Professionelle SEO für Elektriker",
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const doc = buildJsonLd(model);
|
|
118
|
+
const serviceNodes = doc["@graph"].filter(
|
|
119
|
+
(n) => (n as { "@type"?: string | string[] })["@type"] === "Service",
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
expect(serviceNodes.length).toBe(1);
|
|
123
|
+
const node = serviceNodes[0] as Record<string, unknown>;
|
|
124
|
+
expect(node["serviceType"]).toBe("Website für Elektriker — SEO");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("emits industry-specific Service node with areaServed for website-local depth-5 page", () => {
|
|
128
|
+
const model = makeModel({
|
|
129
|
+
surfaceId: "website-local",
|
|
130
|
+
depth: 5,
|
|
131
|
+
industryService: {
|
|
132
|
+
serviceType: "Website für Elektriker — SEO",
|
|
133
|
+
description: "Professionelle SEO für Elektriker",
|
|
134
|
+
areaServed: "Stuttgart",
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const doc = buildJsonLd(model);
|
|
139
|
+
const serviceNodes = doc["@graph"].filter(
|
|
140
|
+
(n) => (n as { "@type"?: string | string[] })["@type"] === "Service",
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
expect(serviceNodes.length).toBe(1);
|
|
144
|
+
const node = serviceNodes[0] as Record<string, unknown>;
|
|
145
|
+
expect(node["serviceType"]).toBe("Website für Elektriker — SEO");
|
|
146
|
+
const areaServed = node["areaServed"] as Record<string, unknown>;
|
|
147
|
+
expect(areaServed).toBeDefined();
|
|
148
|
+
expect(areaServed["@type"]).toBe("City");
|
|
149
|
+
expect(areaServed["name"]).toBe("Stuttgart");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("emits no Service nodes for depth-0 website-local page (Service prohibited)", () => {
|
|
153
|
+
const model = makeModel({
|
|
154
|
+
surfaceId: "website-local",
|
|
155
|
+
depth: 0,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const doc = buildJsonLd(model);
|
|
159
|
+
const serviceNodes = doc["@graph"].filter(
|
|
160
|
+
(n) => (n as { "@type"?: string | string[] })["@type"] === "Service",
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
expect(serviceNodes.length).toBe(0);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("emits no Service nodes for depth-4 website-local page (Service prohibited)", () => {
|
|
167
|
+
const model = makeModel({
|
|
168
|
+
surfaceId: "website-local",
|
|
169
|
+
depth: 4,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const doc = buildJsonLd(model);
|
|
173
|
+
const serviceNodes = doc["@graph"].filter(
|
|
174
|
+
(n) => (n as { "@type"?: string | string[] })["@type"] === "Service",
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// RFC-0498: Service is prohibited on depth-4 — no Service nodes at all
|
|
178
|
+
expect(serviceNodes.length).toBe(0);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("suppresses org-level services ItemList for all surface pages", () => {
|
|
182
|
+
const model = makeModel({
|
|
183
|
+
surfaceId: "website-local",
|
|
184
|
+
depth: 1,
|
|
185
|
+
industryService: {
|
|
186
|
+
serviceType: "Website für Elektriker",
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const doc = buildJsonLd(model);
|
|
191
|
+
const itemListNodes = doc["@graph"].filter(
|
|
192
|
+
(n) => (n as { "@type"?: string | string[] })["@type"] === "ItemList",
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
// The org-level services ItemList should NOT be present
|
|
196
|
+
const servicesList = itemListNodes.find((n) => {
|
|
197
|
+
const node = n as Record<string, unknown>;
|
|
198
|
+
const id = node["@id"] as string | undefined;
|
|
199
|
+
return id?.includes("services");
|
|
200
|
+
});
|
|
201
|
+
expect(servicesList).toBeUndefined();
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe("ProfilePage mainEntity (Google Search Console fix)", () => {
|
|
206
|
+
it("sets mainEntity to extraGraphNodes[0] @id for AI-agent profile pages (page.people empty)", () => {
|
|
207
|
+
const model = makeModel({
|
|
208
|
+
type: "person",
|
|
209
|
+
url: "https://example.com/team/ki-agenten/test-bot/",
|
|
210
|
+
title: "Test Bot",
|
|
211
|
+
people: [],
|
|
212
|
+
extraGraphNodes: [
|
|
213
|
+
{
|
|
214
|
+
"@type": "SoftwareApplication",
|
|
215
|
+
"@id": "https://example.com/team/ki-agenten/test-bot/#software",
|
|
216
|
+
name: "Test Bot",
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const doc = buildJsonLd(model);
|
|
222
|
+
const webPageNode = doc["@graph"].find((n) => {
|
|
223
|
+
const types = (n as Record<string, unknown>)["@type"];
|
|
224
|
+
if (Array.isArray(types)) return types.includes("ProfilePage");
|
|
225
|
+
return types === "ProfilePage";
|
|
226
|
+
}) as Record<string, unknown> | undefined;
|
|
227
|
+
|
|
228
|
+
expect(webPageNode).toBeDefined();
|
|
229
|
+
const mainEntity = webPageNode!["mainEntity"] as Record<string, unknown>;
|
|
230
|
+
expect(mainEntity).toBeDefined();
|
|
231
|
+
expect(mainEntity["@id"]).toBe("https://example.com/team/ki-agenten/test-bot/#software");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("sets mainEntity to extraGraphNodes[0] @id for human profile pages with extended Person node", () => {
|
|
235
|
+
const model = makeModel({
|
|
236
|
+
type: "person",
|
|
237
|
+
url: "https://example.com/team/jane-doe/",
|
|
238
|
+
title: "Jane Doe",
|
|
239
|
+
people: [{ name: "Jane Doe" }],
|
|
240
|
+
extraGraphNodes: [
|
|
241
|
+
{
|
|
242
|
+
"@type": "Person",
|
|
243
|
+
"@id": "https://example.com/team/jane-doe/#person",
|
|
244
|
+
name: "Jane Doe",
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const doc = buildJsonLd(model);
|
|
250
|
+
const webPageNode = doc["@graph"].find((n) => {
|
|
251
|
+
const types = (n as Record<string, unknown>)["@type"];
|
|
252
|
+
if (Array.isArray(types)) return types.includes("ProfilePage");
|
|
253
|
+
return types === "ProfilePage";
|
|
254
|
+
}) as Record<string, unknown> | undefined;
|
|
255
|
+
|
|
256
|
+
expect(webPageNode).toBeDefined();
|
|
257
|
+
const mainEntity = webPageNode!["mainEntity"] as Record<string, unknown>;
|
|
258
|
+
expect(mainEntity).toBeDefined();
|
|
259
|
+
expect(mainEntity["@id"]).toBe("https://example.com/team/jane-doe/#person");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("falls back to ids.person when no extraGraphNodes", () => {
|
|
263
|
+
const model = makeModel({
|
|
264
|
+
type: "person",
|
|
265
|
+
url: "https://example.com/team/jane-doe/",
|
|
266
|
+
title: "Jane Doe",
|
|
267
|
+
people: [{ name: "Jane Doe" }],
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const doc = buildJsonLd(model);
|
|
271
|
+
const webPageNode = doc["@graph"].find((n) => {
|
|
272
|
+
const types = (n as Record<string, unknown>)["@type"];
|
|
273
|
+
if (Array.isArray(types)) return types.includes("ProfilePage");
|
|
274
|
+
return types === "ProfilePage";
|
|
275
|
+
}) as Record<string, unknown> | undefined;
|
|
276
|
+
|
|
277
|
+
expect(webPageNode).toBeDefined();
|
|
278
|
+
const mainEntity = webPageNode!["mainEntity"] as Record<string, unknown>;
|
|
279
|
+
expect(mainEntity).toBeDefined();
|
|
280
|
+
expect(mainEntity["@id"]).toBe("https://example.com/#/schema/person/jane-doe");
|
|
281
|
+
});
|
|
282
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>RFC-0287: shape + determinism tests for the knowledge envelope formatter.</purpose>
|
|
4
|
+
<keywords>RFC-0287, agent surface, knowledge, test</keywords>
|
|
5
|
+
</MODULE_CONTRACT>
|
|
6
|
+
<MODULE_MAP>
|
|
7
|
+
<entry key="tests">domain list, envelope shape, determinism, hash stability.</entry>
|
|
8
|
+
</MODULE_MAP>
|
|
9
|
+
<CHANGE_SUMMARY>
|
|
10
|
+
<item>RFC-0287: initial knowledge formatter tests.</item>
|
|
11
|
+
</CHANGE_SUMMARY>
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { test, expect } from "vitest";
|
|
15
|
+
import {
|
|
16
|
+
AGENT_KNOWLEDGE_DOMAINS,
|
|
17
|
+
isAgentKnowledgeDomain,
|
|
18
|
+
formatAgentKnowledge,
|
|
19
|
+
} from "../agent/knowledge.ts";
|
|
20
|
+
import { BUSINESS_DOMAIN_VISIBILITY } from "../semantic/business-projection.ts";
|
|
21
|
+
|
|
22
|
+
test("AGENT_KNOWLEDGE_DOMAINS: matches exactly the `public` BUSINESS_DOMAIN_VISIBILITY keys", () => {
|
|
23
|
+
const publicDomains = Object.entries(BUSINESS_DOMAIN_VISIBILITY)
|
|
24
|
+
.filter(([, visibility]) => visibility === "public")
|
|
25
|
+
.map(([domain]) => domain)
|
|
26
|
+
.sort();
|
|
27
|
+
expect([...AGENT_KNOWLEDGE_DOMAINS].sort()).toEqual(publicDomains);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("isAgentKnowledgeDomain: rejects non-public domains", () => {
|
|
31
|
+
expect(isAgentKnowledgeDomain("offer")).toBe(true);
|
|
32
|
+
expect(isAgentKnowledgeDomain("compliance")).toBe(false);
|
|
33
|
+
expect(isAgentKnowledgeDomain("externalServices")).toBe(false);
|
|
34
|
+
expect(isAgentKnowledgeDomain("meta")).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("formatAgentKnowledge: sets the schema tag with @1 version and strips trailing slash", () => {
|
|
38
|
+
const env = formatAgentKnowledge({
|
|
39
|
+
domain: "offer",
|
|
40
|
+
site: "warpgogol-com",
|
|
41
|
+
baseUrl: "https://warpgogol.com/",
|
|
42
|
+
languages: { default: "de", supported: ["de", "uk"] },
|
|
43
|
+
data: { de: { prices: [] } },
|
|
44
|
+
});
|
|
45
|
+
expect(env.schema).toBe("gogol.agent.knowledge/offer@1");
|
|
46
|
+
expect(env.baseUrl).toBe("https://warpgogol.com");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("formatAgentKnowledge: omits freshness when not supplied, includes it when supplied", () => {
|
|
50
|
+
const withoutFreshness = formatAgentKnowledge({
|
|
51
|
+
domain: "company",
|
|
52
|
+
site: "s",
|
|
53
|
+
baseUrl: "https://s.example",
|
|
54
|
+
languages: { default: "de", supported: ["de"] },
|
|
55
|
+
data: { de: {} },
|
|
56
|
+
});
|
|
57
|
+
expect("freshness" in withoutFreshness).toBe(false);
|
|
58
|
+
|
|
59
|
+
const withFreshness = formatAgentKnowledge({
|
|
60
|
+
domain: "company",
|
|
61
|
+
site: "s",
|
|
62
|
+
baseUrl: "https://s.example",
|
|
63
|
+
languages: { default: "de", supported: ["de"] },
|
|
64
|
+
data: { de: {} },
|
|
65
|
+
freshness: { lastVerified: "2026-07-01", source: "ckl-claim-ledger", coverage: "domain" },
|
|
66
|
+
});
|
|
67
|
+
expect(withFreshness.freshness).toEqual({
|
|
68
|
+
lastVerified: "2026-07-01",
|
|
69
|
+
source: "ckl-claim-ledger",
|
|
70
|
+
coverage: "domain",
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("formatAgentKnowledge: deterministic across repeated calls", () => {
|
|
75
|
+
const input = {
|
|
76
|
+
domain: "faq" as const,
|
|
77
|
+
site: "s",
|
|
78
|
+
baseUrl: "https://s.example",
|
|
79
|
+
languages: { default: "de", supported: ["de", "en"] },
|
|
80
|
+
data: { de: [{ question: "Q", answer: "A" }] },
|
|
81
|
+
};
|
|
82
|
+
const a = formatAgentKnowledge(input);
|
|
83
|
+
const b = formatAgentKnowledge(input);
|
|
84
|
+
expect(JSON.stringify(a)).toBe(JSON.stringify(b));
|
|
85
|
+
expect(a.contentHash).toBe(b.contentHash);
|
|
86
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>RFC-0217: tests for the claim ledger — event-id identity (incl. value),
|
|
4
|
+
as-of query, lineage assembly, and the two projections.</purpose>
|
|
5
|
+
<keywords>RFC-0217, CKL, ledger, event, lineage, projection, test</keywords>
|
|
6
|
+
</MODULE_CONTRACT>
|
|
7
|
+
<MODULE_MAP><entry key="tests">stableEventId, queryAsOf, buildLineage, projectGraph, projectTemporalSeo, ndjson round-trip.</entry></MODULE_MAP>
|
|
8
|
+
<CHANGE_SUMMARY><item>RFC-0217: initial ledger tests (added in self-review pass).</item></CHANGE_SUMMARY>
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { test, expect } from "vitest";
|
|
12
|
+
import {
|
|
13
|
+
stableEventId,
|
|
14
|
+
parseNdjson,
|
|
15
|
+
serializeEvent,
|
|
16
|
+
buildLineage,
|
|
17
|
+
queryAsOf,
|
|
18
|
+
projectGraph,
|
|
19
|
+
projectTemporalSeo,
|
|
20
|
+
type ClaimEvent,
|
|
21
|
+
} from "../knowledge/ledger.ts";
|
|
22
|
+
|
|
23
|
+
function evt(partial: Partial<ClaimEvent>): ClaimEvent {
|
|
24
|
+
const subject = partial.subject ?? "business/de/location#residents";
|
|
25
|
+
const asOf = partial.asOf ?? "2026-01-01";
|
|
26
|
+
const event = partial.event ?? "genesis";
|
|
27
|
+
const actor = partial.actor ?? "agent:test";
|
|
28
|
+
return {
|
|
29
|
+
id: stableEventId(subject, asOf, event, actor, partial.value),
|
|
30
|
+
ts: partial.ts ?? `${asOf}T00:00:00Z`,
|
|
31
|
+
subject,
|
|
32
|
+
value: partial.value,
|
|
33
|
+
provenance: partial.provenance ?? "external",
|
|
34
|
+
asOf,
|
|
35
|
+
actor,
|
|
36
|
+
event,
|
|
37
|
+
...partial,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
test("stableEventId: identical payload is idempotent", () => {
|
|
42
|
+
const a = stableEventId("s#f", "2026-01-01", "genesis", "agent:a", "39120");
|
|
43
|
+
const b = stableEventId("s#f", "2026-01-01", "genesis", "agent:a", "39120");
|
|
44
|
+
expect(a).toBe(b);
|
|
45
|
+
expect(a).toMatch(/^evt_[0-9a-f]{16}$/);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("stableEventId: a different value on the same day/actor/event is a DISTINCT event", () => {
|
|
49
|
+
const first = stableEventId("s#f", "2026-01-01", "verify-update", "agent:a", "39120");
|
|
50
|
+
const corrected = stableEventId("s#f", "2026-01-01", "verify-update", "agent:a", "39200");
|
|
51
|
+
expect(first).not.toBe(corrected);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("ndjson round-trip preserves a compact event", () => {
|
|
55
|
+
const e = evt({ value: "39120" });
|
|
56
|
+
const [parsed] = parseNdjson(`${serializeEvent(e)}\n`);
|
|
57
|
+
expect(parsed).toEqual(e);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("parseNdjson ignores blank lines", () => {
|
|
61
|
+
const e = evt({ value: "1" });
|
|
62
|
+
expect(parseNdjson(`\n${serializeEvent(e)}\n\n`).length).toBe(1);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("queryAsOf: returns the most recent event whose asOf precedes the query date", () => {
|
|
66
|
+
const events = [
|
|
67
|
+
evt({ asOf: "2025-01-15", value: "38500", event: "genesis" }),
|
|
68
|
+
evt({ asOf: "2026-06-18", value: "39120", event: "verify-update" }),
|
|
69
|
+
];
|
|
70
|
+
expect(queryAsOf(events, "business/de/location#residents", "2025-06-01")?.value).toBe("38500");
|
|
71
|
+
expect(queryAsOf(events, "business/de/location#residents", "2026-12-01")?.value).toBe("39120");
|
|
72
|
+
expect(queryAsOf(events, "business/de/location#residents", "2024-01-01")).toBe(null);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("buildLineage: history is newest → oldest with the latest as current", () => {
|
|
76
|
+
const events = [
|
|
77
|
+
evt({ asOf: "2025-01-15", ts: "2025-01-15T00:00:00Z", value: "38500" }),
|
|
78
|
+
evt({ asOf: "2026-06-18", ts: "2026-06-18T00:00:00Z", value: "39120" }),
|
|
79
|
+
];
|
|
80
|
+
const lin = buildLineage(events, "business/de/location#residents");
|
|
81
|
+
expect(lin).toBeTruthy();
|
|
82
|
+
expect(lin!.current.value).toBe("39120");
|
|
83
|
+
expect(lin!.history[0].value).toBe("39120");
|
|
84
|
+
expect(lin!.history[1].value).toBe("38500");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("buildLineage: null for an unknown subject", () => {
|
|
88
|
+
expect(buildLineage([evt({ value: "1" })], "business/de/nope#x")).toBe(null);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("projectGraph: one node per subject, newest event wins, history counted", () => {
|
|
92
|
+
const events = [
|
|
93
|
+
evt({ asOf: "2025-01-15", ts: "2025-01-15T00:00:00Z", value: "38500" }),
|
|
94
|
+
evt({ asOf: "2026-06-18", ts: "2026-06-18T00:00:00Z", value: "39120" }),
|
|
95
|
+
evt({ subject: "business/de/company#foundingYear", value: "2026", ts: "2026-02-01T00:00:00Z" }),
|
|
96
|
+
];
|
|
97
|
+
const graph = projectGraph(events, "warpgogol-com");
|
|
98
|
+
expect(graph.nodes.length).toBe(2);
|
|
99
|
+
const residents = graph.nodes.find((n) => n.subject === "business/de/location#residents")!;
|
|
100
|
+
expect(residents.currentValue).toBe("39120");
|
|
101
|
+
expect(residents.historyCount).toBe(2);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("projectTemporalSeo: datePublished is earliest genesis, dateModified is latest update", () => {
|
|
105
|
+
const subject = "business/de/location#residents";
|
|
106
|
+
const events = [
|
|
107
|
+
evt({ subject, asOf: "2025-01-15", event: "genesis", value: "38500" }),
|
|
108
|
+
evt({ subject, asOf: "2026-06-18", event: "verify-update", value: "39120" }),
|
|
109
|
+
];
|
|
110
|
+
const map = new Map<string, string[]>([["de/location", [subject]]]);
|
|
111
|
+
const [seo] = projectTemporalSeo(events, map);
|
|
112
|
+
expect(seo.datePublished).toBe("2025-01-15");
|
|
113
|
+
expect(seo.dateModified).toBe("2026-06-18");
|
|
114
|
+
expect(seo.temporalCoverage).toBe("2025-01-15/2026-06-18");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("projectTemporalSeo: no temporalCoverage when published equals modified", () => {
|
|
118
|
+
const subject = "business/de/company#foundingYear";
|
|
119
|
+
const events = [evt({ subject, asOf: "2026-02-01", event: "genesis", value: "2026" })];
|
|
120
|
+
const map = new Map<string, string[]>([["de/company", [subject]]]);
|
|
121
|
+
const [seo] = projectTemporalSeo(events, map);
|
|
122
|
+
expect(seo.datePublished).toBe("2026-02-01");
|
|
123
|
+
expect(seo.dateModified).toBe("2026-02-01");
|
|
124
|
+
expect(seo.temporalCoverage).toBe(undefined);
|
|
125
|
+
});
|