@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,502 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>RFC-0208: block text extractor implementations for all known block types. Registers extractors into the global BLOCK_EXTRACTORS registry.</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not handle block types that contain no text-bearing props.</item>
|
|
6
|
+
<item>Do not resolve content references — extraction is shallow and deterministic.</item>
|
|
7
|
+
</non-goals>
|
|
8
|
+
</MODULE_CONTRACT>
|
|
9
|
+
<CHANGE_SUMMARY>
|
|
10
|
+
<item>RFC-0208: registered extractors for warpgogol-com block types.</item>
|
|
11
|
+
<item>RFC-0372: added video-section, donation-card, credits, passport no-op extractors; people extractor now extracts heading; all extractors return heading (possibly empty) for unified SemanticBlock.</item>
|
|
12
|
+
</CHANGE_SUMMARY>
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { BLOCK_EXTRACTORS } from "../block-extraction.ts";
|
|
16
|
+
|
|
17
|
+
function safeGet(obj: unknown, path: string): unknown {
|
|
18
|
+
return path.split(".").reduce((acc: unknown, key: string) => {
|
|
19
|
+
if (acc && typeof acc === "object") return (acc as Record<string, unknown>)[key];
|
|
20
|
+
return undefined;
|
|
21
|
+
}, obj);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function safeString(v: unknown): string | undefined {
|
|
25
|
+
return typeof v === "string" && v.length > 0 ? v : undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function safeArray<T>(v: unknown, mapper: (item: unknown) => T | undefined): T[] | undefined {
|
|
29
|
+
if (!Array.isArray(v)) return undefined;
|
|
30
|
+
const result = v.map(mapper).filter((x): x is T => x !== undefined);
|
|
31
|
+
return result.length > 0 ? result : undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// hero-decision-card
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
BLOCK_EXTRACTORS.register<Record<string, unknown>>({
|
|
38
|
+
blockType: "hero-decision-card",
|
|
39
|
+
extract(props) {
|
|
40
|
+
const heading = safeString(safeGet(props, "header.heading"));
|
|
41
|
+
const lead =
|
|
42
|
+
safeString(safeGet(props, "header.subheading")) ?? safeString(safeGet(props, "tagline"));
|
|
43
|
+
const items = safeArray(safeGet(props, "decisionCard.items"), (item) => {
|
|
44
|
+
const label = safeString(safeGet(item, "label"));
|
|
45
|
+
const value = safeString(safeGet(item, "value"));
|
|
46
|
+
if (!label) return undefined;
|
|
47
|
+
return { title: `${label}: ${value ?? ""}` };
|
|
48
|
+
});
|
|
49
|
+
const ctaItems: Array<{ title: string; description?: string }> = [];
|
|
50
|
+
const primaryLabel = safeString(safeGet(props, "primaryCta.label"));
|
|
51
|
+
const secondaryLabel = safeString(safeGet(props, "secondaryCta.label"));
|
|
52
|
+
if (primaryLabel) ctaItems.push({ title: `CTA: ${primaryLabel}` });
|
|
53
|
+
if (secondaryLabel) ctaItems.push({ title: `CTA: ${secondaryLabel}` });
|
|
54
|
+
return { heading, lead, items: items?.length ? [...items, ...ctaItems] : ctaItems };
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// audience-cards (guarantees, included-features, price-economics, growth-modules, referral-club, growth-timeline, etc.)
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
BLOCK_EXTRACTORS.register<Record<string, unknown>>({
|
|
62
|
+
blockType: "audience-cards",
|
|
63
|
+
extract(props) {
|
|
64
|
+
const heading = safeString(safeGet(props, "header.heading"));
|
|
65
|
+
const lead = safeString(safeGet(props, "header.subheading"));
|
|
66
|
+
const cards = safeArray(safeGet(props, "body.cards"), (card) => {
|
|
67
|
+
const title = safeString(safeGet(card, "title"));
|
|
68
|
+
const description = safeString(safeGet(card, "description"));
|
|
69
|
+
if (!title) return undefined;
|
|
70
|
+
return { title, description };
|
|
71
|
+
});
|
|
72
|
+
return { heading, lead, items: cards };
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// comparison-cards
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
BLOCK_EXTRACTORS.register<Record<string, unknown>>({
|
|
80
|
+
blockType: "comparison-cards",
|
|
81
|
+
extract(props) {
|
|
82
|
+
const heading = safeString(safeGet(props, "header.heading"));
|
|
83
|
+
const _labels = safeGet(props, "labels") as Record<string, string> | undefined;
|
|
84
|
+
const rows = safeArray(safeGet(props, "rows"), (row) => {
|
|
85
|
+
const left = safeString(safeGet(row, "left"));
|
|
86
|
+
const right = safeString(safeGet(row, "right"));
|
|
87
|
+
if (!left || !right) return undefined;
|
|
88
|
+
return { title: `${left} → ${right}` };
|
|
89
|
+
});
|
|
90
|
+
return { heading, items: rows };
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// price-card
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
BLOCK_EXTRACTORS.register<Record<string, unknown>>({
|
|
98
|
+
blockType: "price-card",
|
|
99
|
+
extract(props) {
|
|
100
|
+
const heading = safeString(safeGet(props, "header.heading"));
|
|
101
|
+
const monthly = safeString(safeGet(props, "monthly"));
|
|
102
|
+
const yearly = safeString(safeGet(props, "yearly"));
|
|
103
|
+
const setup = safeString(safeGet(props, "setup"));
|
|
104
|
+
const includes = safeArray(safeGet(props, "includes"), (item) => {
|
|
105
|
+
const text = safeString(safeGet(item, "text"));
|
|
106
|
+
if (!text) return undefined;
|
|
107
|
+
return { title: text };
|
|
108
|
+
});
|
|
109
|
+
const priceItems: Array<{ title: string; description?: string }> = [];
|
|
110
|
+
if (monthly) priceItems.push({ title: `Monatlich: ${monthly}` });
|
|
111
|
+
if (yearly) priceItems.push({ title: `Jährlich: ${yearly}` });
|
|
112
|
+
if (setup) priceItems.push({ title: `Einrichtung: ${setup}` });
|
|
113
|
+
return { heading, items: includes?.length ? [...priceItems, ...includes] : priceItems };
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// ownership-block
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
BLOCK_EXTRACTORS.register<Record<string, unknown>>({
|
|
121
|
+
blockType: "ownership-block",
|
|
122
|
+
extract(props) {
|
|
123
|
+
const heading = safeString(safeGet(props, "header.heading"));
|
|
124
|
+
const items = safeArray(safeGet(props, "body.items"), (item) => {
|
|
125
|
+
const text = safeString(safeGet(item, "text"));
|
|
126
|
+
if (!text) return undefined;
|
|
127
|
+
return { title: text };
|
|
128
|
+
});
|
|
129
|
+
return { heading, items };
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// controlled-responsibility-block
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
BLOCK_EXTRACTORS.register<Record<string, unknown>>({
|
|
137
|
+
blockType: "controlled-responsibility-block",
|
|
138
|
+
extract(props) {
|
|
139
|
+
const heading = safeString(safeGet(props, "header.heading"));
|
|
140
|
+
const lead = safeString(safeGet(props, "header.subheading"));
|
|
141
|
+
const primaryLabel = safeString(safeGet(props, "labels.primary"));
|
|
142
|
+
const secondaryLabel = safeString(safeGet(props, "labels.secondary"));
|
|
143
|
+
const primaryItems = safeArray(safeGet(props, "primaryItems"), (item) => {
|
|
144
|
+
const text = safeString(safeGet(item, "text"));
|
|
145
|
+
if (!text) return undefined;
|
|
146
|
+
return { title: `${primaryLabel ?? ""}: ${text}`.replace(/^:\s*/, "") };
|
|
147
|
+
});
|
|
148
|
+
const secondaryItems = safeArray(safeGet(props, "secondaryItems"), (item) => {
|
|
149
|
+
const text = safeString(safeGet(item, "text"));
|
|
150
|
+
if (!text) return undefined;
|
|
151
|
+
return { title: `${secondaryLabel ?? ""}: ${text}`.replace(/^:\s*/, "") };
|
|
152
|
+
});
|
|
153
|
+
return { heading, lead, items: [...(primaryItems ?? []), ...(secondaryItems ?? [])] };
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// notausgang-block
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
BLOCK_EXTRACTORS.register<Record<string, unknown>>({
|
|
161
|
+
blockType: "notausgang-block",
|
|
162
|
+
extract(props) {
|
|
163
|
+
const heading = safeString(safeGet(props, "header.heading"));
|
|
164
|
+
const items = safeArray(safeGet(props, "body.items"), (item) => {
|
|
165
|
+
const text = safeString(safeGet(item, "text"));
|
|
166
|
+
if (!text) return undefined;
|
|
167
|
+
return { title: text };
|
|
168
|
+
});
|
|
169
|
+
return { heading, items };
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// faq-list — extracts heading only; FAQ entries come from business/faq via reader
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
BLOCK_EXTRACTORS.register<Record<string, unknown>>({
|
|
177
|
+
blockType: "faq-list",
|
|
178
|
+
extract(props) {
|
|
179
|
+
const heading = safeString(safeGet(props, "header.heading"));
|
|
180
|
+
return { heading };
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// final-cta
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
BLOCK_EXTRACTORS.register<Record<string, unknown>>({
|
|
188
|
+
blockType: "final-cta",
|
|
189
|
+
extract(props) {
|
|
190
|
+
const heading = safeString(safeGet(props, "header.heading"));
|
|
191
|
+
const paragraphs = safeArray(safeGet(props, "body.paragraphs"), (p) => {
|
|
192
|
+
const text = safeString(p);
|
|
193
|
+
if (!text) return undefined;
|
|
194
|
+
return { title: text };
|
|
195
|
+
});
|
|
196
|
+
const ctaItems = safeArray(safeGet(props, "ctaGroup.items"), (item) => {
|
|
197
|
+
const label = safeString(safeGet(item, "label"));
|
|
198
|
+
if (!label) return undefined;
|
|
199
|
+
return { title: `CTA: ${label}` };
|
|
200
|
+
});
|
|
201
|
+
const allItems = [...(paragraphs ?? []), ...(ctaItems ?? [])];
|
|
202
|
+
return { heading, items: allItems.length > 0 ? allItems : undefined };
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// trust-strip
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
BLOCK_EXTRACTORS.register<Record<string, unknown>>({
|
|
210
|
+
blockType: "trust-strip",
|
|
211
|
+
extract(props) {
|
|
212
|
+
const items = safeArray(safeGet(props, "body.items"), (item) => {
|
|
213
|
+
const text = safeString(safeGet(item, "text"));
|
|
214
|
+
if (!text) return undefined;
|
|
215
|
+
return { title: text };
|
|
216
|
+
});
|
|
217
|
+
return { items };
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
// send-message
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
BLOCK_EXTRACTORS.register<Record<string, unknown>>({
|
|
225
|
+
blockType: "send-message",
|
|
226
|
+
extract(props) {
|
|
227
|
+
const heading = safeString(safeGet(props, "header.heading"));
|
|
228
|
+
const paragraphs = safeArray(safeGet(props, "body.paragraphs"), (p) => {
|
|
229
|
+
const text = safeString(p);
|
|
230
|
+
if (!text) return undefined;
|
|
231
|
+
return { title: text };
|
|
232
|
+
});
|
|
233
|
+
return { heading, items: paragraphs };
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
// chat-widget
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
BLOCK_EXTRACTORS.register<Record<string, unknown>>({
|
|
241
|
+
blockType: "chat-widget",
|
|
242
|
+
extract(props) {
|
|
243
|
+
const heading = safeString(safeGet(props, "header.heading"));
|
|
244
|
+
return { heading };
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
// markdown (RFC-0325: Programmatic Surface Q&A/prose sections baked as repeated
|
|
250
|
+
// `type: "markdown"` blocks — e.g. the Ratgeber FAQ items). The first markdown
|
|
251
|
+
// block on a page is separately consumed for the page's prose `contentRef`
|
|
252
|
+
// body (build-page.ts `extractMarkdownProps`); this extractor additionally
|
|
253
|
+
// surfaces EVERY markdown block's heading/lead as a content block so repeated
|
|
254
|
+
// Q&A-style sections are not silently dropped from Markdown twins.
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
BLOCK_EXTRACTORS.register<Record<string, unknown>>({
|
|
257
|
+
blockType: "markdown",
|
|
258
|
+
extract(props) {
|
|
259
|
+
const heading = safeString(safeGet(props, "heading"));
|
|
260
|
+
const lead = safeString(safeGet(props, "lead"));
|
|
261
|
+
return { heading, lead };
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
// hero (nicaragua-projekt home page)
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
BLOCK_EXTRACTORS.register<Record<string, unknown>>({
|
|
269
|
+
blockType: "hero",
|
|
270
|
+
extract(props) {
|
|
271
|
+
const heading = safeString(safeGet(props, "header.heading"));
|
|
272
|
+
const lead =
|
|
273
|
+
safeString(safeGet(props, "header.subheading")) ?? safeString(safeGet(props, "tagline"));
|
|
274
|
+
const description = safeString(safeGet(props, "description"));
|
|
275
|
+
const stats = safeArray(safeGet(props, "stats"), (s) => {
|
|
276
|
+
const value = safeString(safeGet(s, "value"));
|
|
277
|
+
const label = safeString(safeGet(s, "label"));
|
|
278
|
+
if (!value || !label) return undefined;
|
|
279
|
+
return { title: `${value} ${label}` };
|
|
280
|
+
});
|
|
281
|
+
const ctaItems: Array<{ title: string }> = [];
|
|
282
|
+
const primary = safeString(safeGet(props, "ctaPrimaryLabel"));
|
|
283
|
+
const secondary = safeString(safeGet(props, "ctaSecondaryLabel"));
|
|
284
|
+
if (primary) ctaItems.push({ title: `CTA: ${primary}` });
|
|
285
|
+
if (secondary) ctaItems.push({ title: `CTA: ${secondary}` });
|
|
286
|
+
const allItems = [...(stats ?? []), ...ctaItems];
|
|
287
|
+
return {
|
|
288
|
+
heading,
|
|
289
|
+
lead: lead ?? description,
|
|
290
|
+
items: allItems.length > 0 ? allItems : undefined,
|
|
291
|
+
};
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
// problem (nicaragua-projekt)
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
BLOCK_EXTRACTORS.register<Record<string, unknown>>({
|
|
299
|
+
blockType: "problem",
|
|
300
|
+
extract(props) {
|
|
301
|
+
const heading = safeString(safeGet(props, "header.heading"));
|
|
302
|
+
const paragraphs = safeArray(safeGet(props, "body.paragraphs"), (p) => {
|
|
303
|
+
const text = safeString(p);
|
|
304
|
+
if (!text) return undefined;
|
|
305
|
+
return { title: text };
|
|
306
|
+
});
|
|
307
|
+
return { heading, items: paragraphs };
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
// approach (nicaragua-projekt)
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
BLOCK_EXTRACTORS.register<Record<string, unknown>>({
|
|
315
|
+
blockType: "approach",
|
|
316
|
+
extract(props) {
|
|
317
|
+
const heading = safeString(safeGet(props, "header.heading"));
|
|
318
|
+
const cards = safeArray(safeGet(props, "body.cards"), (card) => {
|
|
319
|
+
const title = safeString(safeGet(card, "title"));
|
|
320
|
+
const description = safeString(safeGet(card, "description"));
|
|
321
|
+
if (!title) return undefined;
|
|
322
|
+
return { title, description };
|
|
323
|
+
});
|
|
324
|
+
return { heading, items: cards };
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// ---------------------------------------------------------------------------
|
|
329
|
+
// impact (nicaragua-projekt)
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
331
|
+
BLOCK_EXTRACTORS.register<Record<string, unknown>>({
|
|
332
|
+
blockType: "impact",
|
|
333
|
+
extract(props) {
|
|
334
|
+
const heading = safeString(safeGet(props, "header.heading"));
|
|
335
|
+
const stats = safeArray(safeGet(props, "body.stats"), (s) => {
|
|
336
|
+
const value = safeString(safeGet(s, "value"));
|
|
337
|
+
const label = safeString(safeGet(s, "label"));
|
|
338
|
+
if (!value || !label) return undefined;
|
|
339
|
+
return { title: `${value} ${label}` };
|
|
340
|
+
});
|
|
341
|
+
return { heading, items: stats };
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// ---------------------------------------------------------------------------
|
|
346
|
+
// social-proof (nicaragua-projekt)
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
BLOCK_EXTRACTORS.register<Record<string, unknown>>({
|
|
349
|
+
blockType: "social-proof",
|
|
350
|
+
extract(props) {
|
|
351
|
+
const heading = safeString(safeGet(props, "header.heading"));
|
|
352
|
+
const paragraphs = safeArray(safeGet(props, "body.paragraphs"), (p) => {
|
|
353
|
+
const text = safeString(p);
|
|
354
|
+
if (!text) return undefined;
|
|
355
|
+
return { title: text };
|
|
356
|
+
});
|
|
357
|
+
const note = safeString(safeGet(props, "registrationNote"));
|
|
358
|
+
const items = [...(paragraphs ?? [])];
|
|
359
|
+
if (note) items.push({ title: note });
|
|
360
|
+
return { heading, items: items.length > 0 ? items : undefined };
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// ---------------------------------------------------------------------------
|
|
365
|
+
// donation-use (nicaragua-projekt)
|
|
366
|
+
// ---------------------------------------------------------------------------
|
|
367
|
+
BLOCK_EXTRACTORS.register<Record<string, unknown>>({
|
|
368
|
+
blockType: "donation-use",
|
|
369
|
+
extract(props) {
|
|
370
|
+
const heading = safeString(safeGet(props, "header.heading"));
|
|
371
|
+
const cards = safeArray(safeGet(props, "body.cards"), (card) => {
|
|
372
|
+
const number = safeString(safeGet(card, "number"));
|
|
373
|
+
const title = safeString(safeGet(card, "title"));
|
|
374
|
+
if (!title) return undefined;
|
|
375
|
+
return { title: number ? `${number}. ${title}` : title };
|
|
376
|
+
});
|
|
377
|
+
return { heading, items: cards };
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// ---------------------------------------------------------------------------
|
|
382
|
+
// women (nicaragua-projekt)
|
|
383
|
+
// ---------------------------------------------------------------------------
|
|
384
|
+
BLOCK_EXTRACTORS.register<Record<string, unknown>>({
|
|
385
|
+
blockType: "women",
|
|
386
|
+
extract(props) {
|
|
387
|
+
const heading = safeString(safeGet(props, "header.heading"));
|
|
388
|
+
const paragraphs = safeArray(safeGet(props, "body.paragraphs"), (p) => {
|
|
389
|
+
const text = safeString(p);
|
|
390
|
+
if (!text) return undefined;
|
|
391
|
+
return { title: text };
|
|
392
|
+
});
|
|
393
|
+
return { heading, items: paragraphs };
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
398
|
+
// transparency (nicaragua-projekt)
|
|
399
|
+
// ---------------------------------------------------------------------------
|
|
400
|
+
BLOCK_EXTRACTORS.register<Record<string, unknown>>({
|
|
401
|
+
blockType: "transparency",
|
|
402
|
+
extract(props) {
|
|
403
|
+
const rawHeading = safeGet(props, "header.heading");
|
|
404
|
+
let heading: string | undefined;
|
|
405
|
+
if (Array.isArray(rawHeading)) {
|
|
406
|
+
heading = rawHeading
|
|
407
|
+
.map((h: unknown) => {
|
|
408
|
+
if (h && typeof h === "object") {
|
|
409
|
+
const text = (h as Record<string, unknown>)["text"];
|
|
410
|
+
return typeof text === "string" ? text : "";
|
|
411
|
+
}
|
|
412
|
+
return "";
|
|
413
|
+
})
|
|
414
|
+
.filter(Boolean)
|
|
415
|
+
.join(" ");
|
|
416
|
+
} else {
|
|
417
|
+
heading = safeString(rawHeading);
|
|
418
|
+
}
|
|
419
|
+
const note = safeString(safeGet(props, "body.note"));
|
|
420
|
+
const items = safeArray(safeGet(props, "body.items"), (item) => {
|
|
421
|
+
const text = safeString(safeGet(item, "text"));
|
|
422
|
+
if (!text) return undefined;
|
|
423
|
+
return { title: text };
|
|
424
|
+
});
|
|
425
|
+
const reportLink = safeString(safeGet(props, "reportLinkLabel"));
|
|
426
|
+
const allItems = [...(items ?? [])];
|
|
427
|
+
if (note) allItems.unshift({ title: note });
|
|
428
|
+
if (reportLink) allItems.push({ title: reportLink });
|
|
429
|
+
return { heading: heading || undefined, items: allItems.length > 0 ? allItems : undefined };
|
|
430
|
+
},
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
// ---------------------------------------------------------------------------
|
|
434
|
+
// people (nicaragua-projekt, warpgogol-com) — extracts heading from header;
|
|
435
|
+
// people content itself is sourced from business/people enrichment, not block props.
|
|
436
|
+
// ---------------------------------------------------------------------------
|
|
437
|
+
BLOCK_EXTRACTORS.register<Record<string, unknown>>({
|
|
438
|
+
blockType: "people",
|
|
439
|
+
extract(props) {
|
|
440
|
+
const heading = safeString(safeGet(props, "header.heading"));
|
|
441
|
+
return { heading };
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// ---------------------------------------------------------------------------
|
|
446
|
+
// video-section (warpgogol-com) — extracts heading and lead from header.
|
|
447
|
+
// ---------------------------------------------------------------------------
|
|
448
|
+
BLOCK_EXTRACTORS.register<Record<string, unknown>>({
|
|
449
|
+
blockType: "video-section",
|
|
450
|
+
extract(props) {
|
|
451
|
+
const heading = safeString(safeGet(props, "header.heading"));
|
|
452
|
+
const lead = safeString(safeGet(props, "header.subheading"));
|
|
453
|
+
return { heading, lead };
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// ---------------------------------------------------------------------------
|
|
458
|
+
// donation-card (nicaragua-projekt) — extracts heading from labels.title.
|
|
459
|
+
// ---------------------------------------------------------------------------
|
|
460
|
+
BLOCK_EXTRACTORS.register<Record<string, unknown>>({
|
|
461
|
+
blockType: "donation-card",
|
|
462
|
+
extract(props) {
|
|
463
|
+
const heading = safeString(safeGet(props, "labels.title"));
|
|
464
|
+
return { heading };
|
|
465
|
+
},
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
// ---------------------------------------------------------------------------
|
|
469
|
+
// credits (nicaragua-projekt, warpgogol-com) — extracts heading and lead.
|
|
470
|
+
// ---------------------------------------------------------------------------
|
|
471
|
+
BLOCK_EXTRACTORS.register<Record<string, unknown>>({
|
|
472
|
+
blockType: "credits",
|
|
473
|
+
extract(props) {
|
|
474
|
+
const heading = safeString(safeGet(props, "heading"));
|
|
475
|
+
const lead = safeString(safeGet(props, "lead"));
|
|
476
|
+
return { heading, lead };
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// ---------------------------------------------------------------------------
|
|
481
|
+
// RFC-0372: Passport-reserved moon no-op extractors. These block types render
|
|
482
|
+
// visual components (passport-header, pulsar, passport-score-grid,
|
|
483
|
+
// passport-provenance, passport-star-map) with no text content to project into
|
|
484
|
+
// the semantic layer. They must be registered so page.blocks.extract.validate
|
|
485
|
+
// does not fail, but they return empty headings.
|
|
486
|
+
// ---------------------------------------------------------------------------
|
|
487
|
+
const PASSPORT_NOOP_TYPES = [
|
|
488
|
+
"passport-header",
|
|
489
|
+
"pulsar",
|
|
490
|
+
"passport-score-grid",
|
|
491
|
+
"passport-provenance",
|
|
492
|
+
"passport-star-map",
|
|
493
|
+
] as const;
|
|
494
|
+
|
|
495
|
+
for (const blockType of PASSPORT_NOOP_TYPES) {
|
|
496
|
+
BLOCK_EXTRACTORS.register<Record<string, unknown>>({
|
|
497
|
+
blockType,
|
|
498
|
+
extract() {
|
|
499
|
+
return { heading: "" };
|
|
500
|
+
},
|
|
501
|
+
});
|
|
502
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>
|
|
4
|
+
[RFC-0229] The canonical breadcrumb trail builder — the single source of truth for both the
|
|
5
|
+
rendered breadcrumbs section and the BreadcrumbList JSON-LD on every page of every site.
|
|
6
|
+
Framework-agnostic and pure: ancestor discovery is injected via a BreadcrumbAncestorResolver
|
|
7
|
+
seam so each route source (authored pages, Programmatic Surface) contributes its own hierarchy.
|
|
8
|
+
</purpose>
|
|
9
|
+
<non-goals>
|
|
10
|
+
<item>Do not read files, the route registry, or the surface artifact — that is the resolver's job.</item>
|
|
11
|
+
<item>Do not render markup or emit JSON-LD — callers project the trail into both.</item>
|
|
12
|
+
</non-goals>
|
|
13
|
+
</MODULE_CONTRACT>
|
|
14
|
+
<CHANGE_SUMMARY>
|
|
15
|
+
<item>RFC-0229: initial canonical trail builder + resolver seam.</item>
|
|
16
|
+
</CHANGE_SUMMARY>
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { SemanticBreadcrumb } from "./models.ts";
|
|
20
|
+
|
|
21
|
+
/** One crumb in a breadcrumb trail. URLs are absolute so the trail feeds JSON-LD directly. */
|
|
22
|
+
export type BreadcrumbCrumb = SemanticBreadcrumb & { pageId?: string };
|
|
23
|
+
|
|
24
|
+
/** An ordered breadcrumb trail: Home first, the current page last. */
|
|
25
|
+
export type BreadcrumbTrail = BreadcrumbCrumb[];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The injected ancestor-discovery seam. Returns the ordered ancestors that sit *between* Home and
|
|
29
|
+
* the current page (exclusive of both), nearest-root first. Each route source implements this over
|
|
30
|
+
* its own hierarchy (authored: parentPageId chain; Programmatic Surface: the depth/tuple chain).
|
|
31
|
+
* Non-live ancestors (noindex / redirect / missing) MUST be skipped so the trail stays clickable.
|
|
32
|
+
*/
|
|
33
|
+
export interface BreadcrumbAncestorResolver {
|
|
34
|
+
resolveAncestors(input: {
|
|
35
|
+
pageId: string;
|
|
36
|
+
lang: string;
|
|
37
|
+
defaultLang: string;
|
|
38
|
+
}): Promise<BreadcrumbCrumb[]>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Strip a site-name suffix from an SEO title. Pages often use "Page Name | Site Name" for the
|
|
43
|
+
* <title>; a breadcrumb should show only the page-name portion.
|
|
44
|
+
*/
|
|
45
|
+
export function stripSiteNameFromTitle(title: string): string {
|
|
46
|
+
const separatorIndex = title.lastIndexOf(" | ");
|
|
47
|
+
return separatorIndex > 0 ? title.slice(0, separatorIndex).trim() : title.trim();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The ordered ancestor pageIds of a Programmatic Surface page, derived purely from its synthetic
|
|
52
|
+
* pageId, nearest-root first and exclusive of the page itself.
|
|
53
|
+
*
|
|
54
|
+
* A surface pageId encodes its axis tuple as `<surfaceId>:<v1>:<v2>:…` (and the depth-0 landing as
|
|
55
|
+
* `<surfaceId>:_root`), so the hierarchy is recoverable without the Blueprint: the depth-0 landing
|
|
56
|
+
* plus every shallower tuple. Returns `[]` for the root landing and for any non-surface pageId.
|
|
57
|
+
*
|
|
58
|
+
* This is the single source of the surface hierarchy shape — shared by the render-time ancestor
|
|
59
|
+
* resolver (which maps these ids to live crumbs) and `breadcrumb.trail.validate` (which checks the
|
|
60
|
+
* ancestors resolve to routes). Keep both on this one function so they cannot drift.
|
|
61
|
+
*/
|
|
62
|
+
export function surfaceAncestorPageIds(pageId: string): string[] {
|
|
63
|
+
const parts = pageId.split(":");
|
|
64
|
+
if (parts.length < 2) return [];
|
|
65
|
+
const surfaceId = parts[0]!;
|
|
66
|
+
const values = parts.slice(1);
|
|
67
|
+
// The depth-0 landing (`<surfaceId>:_root`) has no ancestors between it and Home.
|
|
68
|
+
if (values.length === 1 && values[0] === "_root") return [];
|
|
69
|
+
|
|
70
|
+
const ids: string[] = [`${surfaceId}:_root`];
|
|
71
|
+
for (let depth = 1; depth < values.length; depth += 1) {
|
|
72
|
+
ids.push(`${surfaceId}:${values.slice(0, depth).join(":")}`);
|
|
73
|
+
}
|
|
74
|
+
return ids;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Normalize a URL for dedupe: drop a trailing slash (but keep a bare-root "/"). */
|
|
78
|
+
function dedupeKey(url: string): string {
|
|
79
|
+
if (url === "/") return "/";
|
|
80
|
+
return url.replace(/\/+$/, "");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Assemble the canonical breadcrumb trail for one page:
|
|
85
|
+
*
|
|
86
|
+
* [ Home, …live ancestors (root-first)…, current page ]
|
|
87
|
+
*
|
|
88
|
+
* The same trail is projected into the visible breadcrumbs section AND the BreadcrumbList JSON-LD,
|
|
89
|
+
* so the two can never drift. The home page itself yields a single-node trail (Home === self), which
|
|
90
|
+
* callers suppress per schema.org guidance.
|
|
91
|
+
*/
|
|
92
|
+
export async function buildBreadcrumbTrail(input: {
|
|
93
|
+
pageId: string;
|
|
94
|
+
pageTitle: string;
|
|
95
|
+
selfUrl: string;
|
|
96
|
+
homeLabel: string;
|
|
97
|
+
homeUrl: string;
|
|
98
|
+
lang: string;
|
|
99
|
+
defaultLang: string;
|
|
100
|
+
resolver: BreadcrumbAncestorResolver;
|
|
101
|
+
}): Promise<BreadcrumbTrail> {
|
|
102
|
+
const { pageId, pageTitle, selfUrl, homeLabel, homeUrl, lang, defaultLang, resolver } = input;
|
|
103
|
+
|
|
104
|
+
const ancestors = await resolver.resolveAncestors({ pageId, lang, defaultLang });
|
|
105
|
+
|
|
106
|
+
const home: BreadcrumbCrumb = { name: homeLabel, url: homeUrl };
|
|
107
|
+
const self: BreadcrumbCrumb = { name: stripSiteNameFromTitle(pageTitle), url: selfUrl, pageId };
|
|
108
|
+
|
|
109
|
+
const trail: BreadcrumbTrail = [home];
|
|
110
|
+
const seen = new Set<string>([dedupeKey(homeUrl), dedupeKey(selfUrl)]);
|
|
111
|
+
for (const crumb of ancestors) {
|
|
112
|
+
const key = dedupeKey(crumb.url);
|
|
113
|
+
// Drop ancestors that collapse onto Home or self, or repeat an earlier ancestor.
|
|
114
|
+
if (seen.has(key) || !crumb.name.trim()) continue;
|
|
115
|
+
seen.add(key);
|
|
116
|
+
trail.push({ ...crumb, name: stripSiteNameFromTitle(crumb.name) });
|
|
117
|
+
}
|
|
118
|
+
trail.push(self);
|
|
119
|
+
return trail;
|
|
120
|
+
}
|