@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,548 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>
|
|
4
|
+
RFC-0235 egress text normalizer. A server-only adapter that strips AI-authorship
|
|
5
|
+
typographic signals (special dashes, curly/guillemet quotes, special + zero-width
|
|
6
|
+
spaces, typographic HTML entities, single-char ellipsis) from text on its way to
|
|
7
|
+
public output. Pure string transforms — no DOM, no heavy parser — safe in SSR/SSG
|
|
8
|
+
frontmatter, kernel build steps, and the OG-image generator. Authored sources are
|
|
9
|
+
never touched; this only transforms output strings.
|
|
10
|
+
</purpose>
|
|
11
|
+
<non-goals>
|
|
12
|
+
<item>Do not import in browser scripts — server-only (entity decode + Node usage assumptions).</item>
|
|
13
|
+
<item>Do not touch structural HTML/JSON/XML syntax or the structural entities & < > " '.</item>
|
|
14
|
+
<item>Do not normalize text inside code/pre/script/style or fenced/inline Markdown code.</item>
|
|
15
|
+
<item>Do not apply heuristics that distinguish "intentional" from "AI" typography — blanket per signal.</item>
|
|
16
|
+
</non-goals>
|
|
17
|
+
</MODULE_CONTRACT>
|
|
18
|
+
<CHANGE_SUMMARY>
|
|
19
|
+
<item>RFC-0235: initial egress text normalizer.</item>
|
|
20
|
+
<item>RFC-0569: add createDevNormalizeMiddleware for dev/prod egress parity.</item>
|
|
21
|
+
</CHANGE_SUMMARY>
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// @ai-invariant: RFC-0235. Server-only. Never import in browser scripts.
|
|
25
|
+
// All special-character sets are expressed as \u escapes — never literal glyphs —
|
|
26
|
+
// so the source contains no invisible characters and the matchers are auditable.
|
|
27
|
+
// RFC-0569: createDevNormalizeMiddleware adds a dev-only Astro middleware factory.
|
|
28
|
+
|
|
29
|
+
import type { MiddlewareHandler } from "astro";
|
|
30
|
+
|
|
31
|
+
export type SignalId = "dashes" | "quotes" | "spaces" | "zeroWidth" | "htmlEntities" | "ellipsis";
|
|
32
|
+
|
|
33
|
+
export const SIGNAL_IDS: readonly SignalId[] = [
|
|
34
|
+
"dashes",
|
|
35
|
+
"quotes",
|
|
36
|
+
"spaces",
|
|
37
|
+
"zeroWidth",
|
|
38
|
+
"htmlEntities",
|
|
39
|
+
"ellipsis",
|
|
40
|
+
] as const;
|
|
41
|
+
|
|
42
|
+
export interface NormalizeConfig {
|
|
43
|
+
/** Master switch. When false, every normalizer is a no-op. */
|
|
44
|
+
enabled: boolean;
|
|
45
|
+
/** Per-signal toggle. An absent signal defaults to ON. */
|
|
46
|
+
signals: Record<SignalId, boolean>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface SignalSpec {
|
|
50
|
+
id: SignalId;
|
|
51
|
+
title: string;
|
|
52
|
+
/** Human-readable summary of the matched Unicode set. */
|
|
53
|
+
unicode: string;
|
|
54
|
+
/** Human-readable replacement description. */
|
|
55
|
+
replacement: string;
|
|
56
|
+
/** Default gating — always true (everything on by default, RFC-0235). */
|
|
57
|
+
default: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The single source of truth for the signal taxonomy. Consumed by
|
|
62
|
+
* `text.normalize.rules.list`, the validators, and the transforms.
|
|
63
|
+
*/
|
|
64
|
+
export const SIGNAL_REGISTRY: readonly SignalSpec[] = [
|
|
65
|
+
{
|
|
66
|
+
id: "dashes",
|
|
67
|
+
title: "Special dashes",
|
|
68
|
+
unicode: "U+2010 U+2011 U+2012 U+2013 U+2014 U+2015 U+2212",
|
|
69
|
+
replacement: "hyphen-minus '-' (surrounding spaces preserved)",
|
|
70
|
+
default: true,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: "quotes",
|
|
74
|
+
title: "Typographic / smart quotes",
|
|
75
|
+
unicode: "U+00AB U+00BB U+2018-U+201F",
|
|
76
|
+
replacement: "straight double / single quote",
|
|
77
|
+
default: true,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: "spaces",
|
|
81
|
+
title: "Special spaces",
|
|
82
|
+
unicode: "U+00A0 U+1680 U+2000-U+200A U+202F U+205F U+3000",
|
|
83
|
+
replacement: "regular space U+0020 (1:1, runs not collapsed)",
|
|
84
|
+
default: true,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: "zeroWidth",
|
|
88
|
+
title: "Zero-width / invisible characters",
|
|
89
|
+
unicode: "U+00AD U+061C U+180E U+200B U+200C U+200D* U+2060 U+2061-U+2064 U+FEFF",
|
|
90
|
+
replacement: "removed (ZWJ inside emoji sequences preserved)",
|
|
91
|
+
default: true,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: "htmlEntities",
|
|
95
|
+
title: "Typographic HTML entities",
|
|
96
|
+
unicode:
|
|
97
|
+
" — – … « » “ ” ‘ ’   —",
|
|
98
|
+
replacement: "decoded, then routed through the matching char signal",
|
|
99
|
+
default: true,
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: "ellipsis",
|
|
103
|
+
title: "Single-character ellipsis",
|
|
104
|
+
unicode: "U+2026",
|
|
105
|
+
replacement: "three dots '...'",
|
|
106
|
+
default: true,
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
/** All-on config (the default, applied when no `text.normalize` block is present). */
|
|
111
|
+
export const DEFAULT_NORMALIZE_CONFIG: NormalizeConfig = {
|
|
112
|
+
enabled: true,
|
|
113
|
+
signals: {
|
|
114
|
+
dashes: true,
|
|
115
|
+
quotes: true,
|
|
116
|
+
spaces: true,
|
|
117
|
+
zeroWidth: true,
|
|
118
|
+
htmlEntities: true,
|
|
119
|
+
ellipsis: true,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Resolve the effective normalize config from a parsed system manifest.
|
|
125
|
+
* Absent block ⇒ all signals on. Absent individual signal ⇒ on.
|
|
126
|
+
*/
|
|
127
|
+
export function resolveNormalizeConfig(manifest: unknown): NormalizeConfig {
|
|
128
|
+
const block = (manifest as { text?: { normalize?: unknown } } | null | undefined)?.text
|
|
129
|
+
?.normalize as { enabled?: unknown; signals?: Record<string, unknown> } | undefined;
|
|
130
|
+
if (!block || typeof block !== "object") {
|
|
131
|
+
return { ...DEFAULT_NORMALIZE_CONFIG, signals: { ...DEFAULT_NORMALIZE_CONFIG.signals } };
|
|
132
|
+
}
|
|
133
|
+
const enabled = block.enabled === undefined ? true : block.enabled !== false;
|
|
134
|
+
const sig = block.signals ?? {};
|
|
135
|
+
const signals = {} as Record<SignalId, boolean>;
|
|
136
|
+
for (const id of SIGNAL_IDS) {
|
|
137
|
+
signals[id] = sig[id] === undefined ? true : sig[id] !== false;
|
|
138
|
+
}
|
|
139
|
+
return { enabled, signals };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Character-level transforms (sets are \u escapes — no literal glyphs)
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
// U+2010 hyphen, U+2011 non-breaking hyphen, U+2012 figure dash, U+2013 en dash,
|
|
147
|
+
// U+2014 em dash, U+2015 horizontal bar, U+2212 minus sign.
|
|
148
|
+
const RE_DASHES = /[‐‑‒–—―−]/g;
|
|
149
|
+
// U+00AB U+00BB U+201C U+201D U+201E U+201F
|
|
150
|
+
const RE_QUOTES_DOUBLE = /[«»“”„‟]/g;
|
|
151
|
+
// U+2018 U+2019 U+201A U+201B
|
|
152
|
+
const RE_QUOTES_SINGLE = /[‘’‚‛]/g;
|
|
153
|
+
const RE_ELLIPSIS = /…/g;
|
|
154
|
+
// U+00A0 nbsp, U+1680 ogham, U+2000-U+200A, U+202F narrow nbsp, U+205F medium math,
|
|
155
|
+
// U+3000 ideographic space.
|
|
156
|
+
const RE_SPACES = /[ - ]/g;
|
|
157
|
+
// Zero-width / invisible cruft: U+00AD soft hyphen, U+061C arabic letter mark,
|
|
158
|
+
// U+180E Mongolian vowel sep, U+200B ZWSP, U+200C ZWNJ, U+2060 word joiner,
|
|
159
|
+
// U+2061-U+2064 invisible math ops, U+FEFF BOM/ZWNBSP. NOTE: U+200D (ZWJ) is
|
|
160
|
+
// handled separately to preserve emoji ZWJ sequences. Directional marks
|
|
161
|
+
// (U+200E/U+200F) are intentionally excluded — meaningful for RTL content.
|
|
162
|
+
const RE_ZERO_WIDTH = /[-]/g;
|
|
163
|
+
// Standalone ZWJ: only when NOT bridging two emoji components.
|
|
164
|
+
const EMOJI_PART = "[\\p{Extended_Pictographic}\\uFE0F\\u{1F3FB}-\\u{1F3FF}]";
|
|
165
|
+
const RE_STANDALONE_ZWJ = new RegExp(`(?<!${EMOJI_PART})\\u200D|\\u200D(?!${EMOJI_PART})`, "gu");
|
|
166
|
+
|
|
167
|
+
function applyCharSignals(input: string, cfg: NormalizeConfig): string {
|
|
168
|
+
let s = input;
|
|
169
|
+
if (cfg.signals.dashes) s = s.replace(RE_DASHES, "-");
|
|
170
|
+
if (cfg.signals.ellipsis) s = s.replace(RE_ELLIPSIS, "...");
|
|
171
|
+
if (cfg.signals.quotes) {
|
|
172
|
+
s = s.replace(RE_QUOTES_DOUBLE, '"').replace(RE_QUOTES_SINGLE, "'");
|
|
173
|
+
}
|
|
174
|
+
if (cfg.signals.spaces) s = s.replace(RE_SPACES, " ");
|
|
175
|
+
if (cfg.signals.zeroWidth) {
|
|
176
|
+
s = s.replace(RE_ZERO_WIDTH, "").replace(RE_STANDALONE_ZWJ, "");
|
|
177
|
+
}
|
|
178
|
+
return s;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// Typographic HTML entity decoding (the `htmlEntities` lens)
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
const NAMED_ENTITIES: Record<string, string> = {
|
|
186
|
+
nbsp: " ",
|
|
187
|
+
ensp: " ",
|
|
188
|
+
emsp: " ",
|
|
189
|
+
emsp13: " ",
|
|
190
|
+
emsp14: " ",
|
|
191
|
+
numsp: " ",
|
|
192
|
+
puncsp: " ",
|
|
193
|
+
thinsp: " ",
|
|
194
|
+
hairsp: " ",
|
|
195
|
+
mdash: "—",
|
|
196
|
+
ndash: "–",
|
|
197
|
+
dash: "‐",
|
|
198
|
+
horbar: "―",
|
|
199
|
+
minus: "−",
|
|
200
|
+
hellip: "…",
|
|
201
|
+
mldr: "…",
|
|
202
|
+
laquo: "«",
|
|
203
|
+
raquo: "»",
|
|
204
|
+
ldquo: "“",
|
|
205
|
+
rdquo: "”",
|
|
206
|
+
lsquo: "‘",
|
|
207
|
+
rsquo: "’",
|
|
208
|
+
bdquo: "„",
|
|
209
|
+
sbquo: "‚",
|
|
210
|
+
shy: "",
|
|
211
|
+
zwnj: "",
|
|
212
|
+
zwj: "",
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// Every codepoint any char signal can match — gates numeric-entity decoding.
|
|
216
|
+
const TYPOGRAPHIC_CODEPOINTS = new Set<number>([
|
|
217
|
+
0x2010,
|
|
218
|
+
0x2011,
|
|
219
|
+
0x2012,
|
|
220
|
+
0x2013,
|
|
221
|
+
0x2014,
|
|
222
|
+
0x2015,
|
|
223
|
+
0x2212, // dashes
|
|
224
|
+
0x00ab,
|
|
225
|
+
0x00bb,
|
|
226
|
+
0x2018,
|
|
227
|
+
0x2019,
|
|
228
|
+
0x201a,
|
|
229
|
+
0x201b,
|
|
230
|
+
0x201c,
|
|
231
|
+
0x201d,
|
|
232
|
+
0x201e,
|
|
233
|
+
0x201f, // quotes
|
|
234
|
+
0x00a0,
|
|
235
|
+
0x1680,
|
|
236
|
+
0x202f,
|
|
237
|
+
0x205f,
|
|
238
|
+
0x3000, // spaces (U+2000-U+200A range added below)
|
|
239
|
+
0x00ad,
|
|
240
|
+
0x061c,
|
|
241
|
+
0x180e,
|
|
242
|
+
0x200b,
|
|
243
|
+
0x200c,
|
|
244
|
+
0x200d,
|
|
245
|
+
0x2060,
|
|
246
|
+
0x2061,
|
|
247
|
+
0x2062,
|
|
248
|
+
0x2063,
|
|
249
|
+
0x2064,
|
|
250
|
+
0xfeff, // zero-width
|
|
251
|
+
0x2026, // ellipsis
|
|
252
|
+
]);
|
|
253
|
+
for (let cp = 0x2000; cp <= 0x200a; cp++) TYPOGRAPHIC_CODEPOINTS.add(cp);
|
|
254
|
+
|
|
255
|
+
const RE_NAMED_ENTITY = /&([a-z][a-z0-9]*);/gi;
|
|
256
|
+
const RE_NUMERIC_ENTITY = /&#(x?)([0-9a-f]+);/gi;
|
|
257
|
+
|
|
258
|
+
/** Decode only the typographic entity subset; leave structural entities intact. */
|
|
259
|
+
function decodeTypographicEntities(input: string): string {
|
|
260
|
+
let s = input.replace(RE_NAMED_ENTITY, (m, name: string) => {
|
|
261
|
+
const decoded = NAMED_ENTITIES[name.toLowerCase()];
|
|
262
|
+
return decoded ?? m;
|
|
263
|
+
});
|
|
264
|
+
s = s.replace(RE_NUMERIC_ENTITY, (m, hex: string, digits: string) => {
|
|
265
|
+
const cp = parseInt(digits, hex ? 16 : 10);
|
|
266
|
+
return TYPOGRAPHIC_CODEPOINTS.has(cp) ? String.fromCodePoint(cp) : m;
|
|
267
|
+
});
|
|
268
|
+
return s;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
// normalizeText — plain text / char level
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
/** Normalize a plain-text string (also used for HTML text nodes and attr values). */
|
|
276
|
+
export function normalizeText(input: string, cfg: NormalizeConfig): string {
|
|
277
|
+
if (!cfg.enabled || !input) return input;
|
|
278
|
+
let s = input;
|
|
279
|
+
if (cfg.signals.htmlEntities) s = decodeTypographicEntities(s);
|
|
280
|
+
s = applyCharSignals(s, cfg);
|
|
281
|
+
return s;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
// normalizeHtml — tokenizer-based, escaping-safe
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
const HTML_SKIP_TAGS = new Set(["script", "style", "code", "pre", "kbd", "samp"]);
|
|
289
|
+
const ATTR_WHITELIST = ["alt", "title", "content", "aria-label", "aria-description", "placeholder"];
|
|
290
|
+
const RE_JSONLD_BLOCK =
|
|
291
|
+
/(<script\b[^>]*type\s*=\s*["']application\/ld\+json["'][^>]*>)([\s\S]*?)(<\/script>)/gi;
|
|
292
|
+
|
|
293
|
+
function tagName(token: string): string {
|
|
294
|
+
const m = token.match(/^<\/?([a-zA-Z][a-zA-Z0-9]*)/);
|
|
295
|
+
return m ? m[1].toLowerCase() : "";
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Escape a normalized attribute value for the given quote delimiter. */
|
|
299
|
+
function escapeAttr(value: string, quote: '"' | "'"): string {
|
|
300
|
+
// Only the delimiter needs escaping; we never introduce < or &.
|
|
301
|
+
return quote === '"' ? value.replace(/"/g, """) : value.replace(/'/g, "'");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function normalizeOpeningTagAttrs(tag: string, cfg: NormalizeConfig): string {
|
|
305
|
+
const re = new RegExp(`\\b(${ATTR_WHITELIST.join("|")})\\s*=\\s*("([^"]*)"|'([^']*)')`, "gi");
|
|
306
|
+
return tag.replace(re, (_full, name: string, _q: string, dq?: string, sq?: string) => {
|
|
307
|
+
const quote: '"' | "'" = dq !== undefined ? '"' : "'";
|
|
308
|
+
const raw = dq !== undefined ? dq : (sq ?? "");
|
|
309
|
+
const normalized = escapeAttr(normalizeText(raw, cfg), quote);
|
|
310
|
+
return `${name}=${quote}${normalized}${quote}`;
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Normalize an HTML string: text nodes, whitelisted attributes, and inline JSON-LD. */
|
|
315
|
+
export function normalizeHtml(input: string, cfg: NormalizeConfig): string {
|
|
316
|
+
if (!cfg.enabled || !input) return input;
|
|
317
|
+
|
|
318
|
+
// 1. Inline JSON-LD: normalize string values via the JSON path (re-escapes safely).
|
|
319
|
+
const html = input.replace(
|
|
320
|
+
RE_JSONLD_BLOCK,
|
|
321
|
+
(_full, open: string, body: string, close: string) => {
|
|
322
|
+
return `${open}${normalizeJson(body, cfg)}${close}`;
|
|
323
|
+
},
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
// 2. Tokenize on tags; normalize text segments + whitelisted attributes.
|
|
327
|
+
// Only match real HTML tags (starting with a letter, /, or !) — not stray <
|
|
328
|
+
// in CSS like @media (width<=768px) which would swallow the </style> closer
|
|
329
|
+
// and permanently trap the skip stack.
|
|
330
|
+
const tokens = html.split(/(<[a-zA-Z/!][^>]*>)/);
|
|
331
|
+
const skipStack: string[] = [];
|
|
332
|
+
const out: string[] = [];
|
|
333
|
+
|
|
334
|
+
for (const token of tokens) {
|
|
335
|
+
if (token.startsWith("<")) {
|
|
336
|
+
const isComment = token.startsWith("<!--");
|
|
337
|
+
const isClosing = token.startsWith("</");
|
|
338
|
+
const name = tagName(token);
|
|
339
|
+
if (!isComment && !isClosing) {
|
|
340
|
+
const inSkip = skipStack.length > 0;
|
|
341
|
+
if (HTML_SKIP_TAGS.has(name)) skipStack.push(name);
|
|
342
|
+
out.push(inSkip ? token : normalizeOpeningTagAttrs(token, cfg));
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (isClosing && skipStack[skipStack.length - 1] === name) skipStack.pop();
|
|
346
|
+
out.push(token);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
out.push(skipStack.length > 0 ? token : normalizeText(token, cfg));
|
|
350
|
+
}
|
|
351
|
+
return out.join("");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
// normalizeJson — deep string-value normalize
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
|
|
358
|
+
function normalizeJsonValue(
|
|
359
|
+
value: unknown,
|
|
360
|
+
cfg: NormalizeConfig,
|
|
361
|
+
state: { changed: boolean },
|
|
362
|
+
): unknown {
|
|
363
|
+
if (typeof value === "string") {
|
|
364
|
+
const next = normalizeText(value, cfg);
|
|
365
|
+
if (next !== value) state.changed = true;
|
|
366
|
+
return next;
|
|
367
|
+
}
|
|
368
|
+
if (Array.isArray(value)) {
|
|
369
|
+
return value.map((v) => normalizeJsonValue(v, cfg, state));
|
|
370
|
+
}
|
|
371
|
+
if (value && typeof value === "object") {
|
|
372
|
+
const out: Record<string, unknown> = {};
|
|
373
|
+
for (const [k, v] of Object.entries(value)) {
|
|
374
|
+
out[k] = normalizeJsonValue(v, cfg, state); // keys left untouched
|
|
375
|
+
}
|
|
376
|
+
return out;
|
|
377
|
+
}
|
|
378
|
+
return value;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Normalize string VALUES in a JSON document. Returns the original text if nothing changed. */
|
|
382
|
+
export function normalizeJson(input: string, cfg: NormalizeConfig): string {
|
|
383
|
+
if (!cfg.enabled || !input.trim()) return input;
|
|
384
|
+
let parsed: unknown;
|
|
385
|
+
try {
|
|
386
|
+
parsed = JSON.parse(input);
|
|
387
|
+
} catch {
|
|
388
|
+
// Not parseable as JSON — fall back to a text-safe normalize.
|
|
389
|
+
return normalizeText(input, cfg);
|
|
390
|
+
}
|
|
391
|
+
const state = { changed: false };
|
|
392
|
+
const next = normalizeJsonValue(parsed, cfg, state);
|
|
393
|
+
if (!state.changed) return input; // preserve original formatting when clean
|
|
394
|
+
return JSON.stringify(next, null, 2);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
398
|
+
// normalizeXml — text content + CDATA (RSS, sitemap, SVG)
|
|
399
|
+
// ---------------------------------------------------------------------------
|
|
400
|
+
|
|
401
|
+
const RE_CDATA = /(<!\[CDATA\[)([\s\S]*?)(\]\]>)/g;
|
|
402
|
+
|
|
403
|
+
/** Normalize XML text content and CDATA. CDATA bodies are treated as HTML. */
|
|
404
|
+
export function normalizeXml(input: string, cfg: NormalizeConfig): string {
|
|
405
|
+
if (!cfg.enabled || !input) return input;
|
|
406
|
+
|
|
407
|
+
// 1. CDATA blocks (RSS descriptions hold HTML) — normalize via the HTML path.
|
|
408
|
+
const cdataParts: string[] = [];
|
|
409
|
+
let xml = input.replace(RE_CDATA, (_full, open: string, body: string, close: string) => {
|
|
410
|
+
cdataParts.push(`${open}${normalizeHtml(body, cfg)}${close}`);
|
|
411
|
+
return ` CDATA${cdataParts.length - 1} `;
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// 2. Element text content (between tags). XML attributes rarely carry prose; skip them.
|
|
415
|
+
// Only match real XML tags (starting with letter, /, or !) — not stray < in content.
|
|
416
|
+
const tokens = xml.split(/(<[a-zA-Z/!][^>]*>)/);
|
|
417
|
+
xml = tokens.map((t) => (t.startsWith("<") ? t : normalizeText(t, cfg))).join("");
|
|
418
|
+
|
|
419
|
+
// 3. Restore CDATA placeholders.
|
|
420
|
+
xml = xml.replace(/ CDATA(\d+) /g, (_m, i: string) => cdataParts[Number(i)]);
|
|
421
|
+
return xml;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ---------------------------------------------------------------------------
|
|
425
|
+
// normalizeMarkdown — protect fenced + inline code
|
|
426
|
+
// ---------------------------------------------------------------------------
|
|
427
|
+
|
|
428
|
+
const RE_MD_CODE = /(```[\s\S]*?```|~~~[\s\S]*?~~~|`[^`\n]*`)/g;
|
|
429
|
+
|
|
430
|
+
/** Normalize Markdown prose while leaving fenced and inline code untouched. */
|
|
431
|
+
export function normalizeMarkdown(input: string, cfg: NormalizeConfig): string {
|
|
432
|
+
if (!cfg.enabled || !input) return input;
|
|
433
|
+
// Capturing split keeps code spans at odd indices.
|
|
434
|
+
const parts = input.split(RE_MD_CODE);
|
|
435
|
+
return parts.map((part, i) => (i % 2 === 1 ? part : normalizeText(part, cfg))).join("");
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ---------------------------------------------------------------------------
|
|
439
|
+
// Dispatch by kind (dist adapter) + residual detection (backstop)
|
|
440
|
+
// ---------------------------------------------------------------------------
|
|
441
|
+
|
|
442
|
+
export type NormalizableKind = "html" | "json" | "xml" | "svg" | "md" | "txt";
|
|
443
|
+
|
|
444
|
+
/** Map a file path to a normalizer kind, or null if it must not be touched. */
|
|
445
|
+
export function normalizeKindForPath(filePath: string): NormalizableKind | null {
|
|
446
|
+
const lower = filePath.toLowerCase();
|
|
447
|
+
if (lower.endsWith(".html") || lower.endsWith(".htm")) return "html";
|
|
448
|
+
if (lower.endsWith(".json")) return "json";
|
|
449
|
+
if (lower.endsWith(".xml")) return "xml";
|
|
450
|
+
if (lower.endsWith(".svg")) return "svg";
|
|
451
|
+
if (lower.endsWith(".md")) return "md";
|
|
452
|
+
if (lower.endsWith(".txt")) return "txt";
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** Normalize a string given a normalizer kind. */
|
|
457
|
+
export function normalizeByKind(
|
|
458
|
+
input: string,
|
|
459
|
+
kind: NormalizableKind,
|
|
460
|
+
cfg: NormalizeConfig,
|
|
461
|
+
): string {
|
|
462
|
+
switch (kind) {
|
|
463
|
+
case "html":
|
|
464
|
+
return normalizeHtml(input, cfg);
|
|
465
|
+
case "json":
|
|
466
|
+
return normalizeJson(input, cfg);
|
|
467
|
+
case "xml":
|
|
468
|
+
case "svg":
|
|
469
|
+
return normalizeXml(input, cfg);
|
|
470
|
+
case "md":
|
|
471
|
+
return normalizeMarkdown(input, cfg);
|
|
472
|
+
case "txt":
|
|
473
|
+
return normalizeText(input, cfg);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export interface ResidualFinding {
|
|
478
|
+
/** 1-based line number of the first residual occurrence. */
|
|
479
|
+
line: number;
|
|
480
|
+
/** Signal ids that still fire on the content. */
|
|
481
|
+
signals: SignalId[];
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Backstop detection: would normalization still change this content? Reuses the
|
|
486
|
+
* transforms so it inherits their skip rules (code/script/structural). Returns
|
|
487
|
+
* null when clean. The per-signal set is computed by single-signal diffing.
|
|
488
|
+
*/
|
|
489
|
+
export function detectResidual(
|
|
490
|
+
input: string,
|
|
491
|
+
kind: NormalizableKind,
|
|
492
|
+
cfg: NormalizeConfig,
|
|
493
|
+
): ResidualFinding | null {
|
|
494
|
+
if (!cfg.enabled) return null;
|
|
495
|
+
const normalized = normalizeByKind(input, kind, cfg);
|
|
496
|
+
if (normalized === input) return null;
|
|
497
|
+
|
|
498
|
+
const signals: SignalId[] = [];
|
|
499
|
+
for (const id of SIGNAL_IDS) {
|
|
500
|
+
if (!cfg.signals[id]) continue;
|
|
501
|
+
const single: NormalizeConfig = { enabled: true, signals: {} as Record<SignalId, boolean> };
|
|
502
|
+
for (const other of SIGNAL_IDS) single.signals[other] = other === id;
|
|
503
|
+
if (normalizeByKind(input, kind, single) !== input) signals.push(id);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const a = input.split("\n");
|
|
507
|
+
const b = normalized.split("\n");
|
|
508
|
+
let line = 1;
|
|
509
|
+
for (let i = 0; i < a.length; i++) {
|
|
510
|
+
if (a[i] !== b[i]) {
|
|
511
|
+
line = i + 1;
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return { line, signals };
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* RFC-0569: Dev-only Astro middleware factory that applies egress text normalization
|
|
520
|
+
* to HTML responses in dev mode. Gated by `import.meta.env.DEV` at the call site —
|
|
521
|
+
* never executes in production builds. Reuses `normalizeHtml()` from this module,
|
|
522
|
+
* ensuring dev/prod parity with the post-build dist sweep.
|
|
523
|
+
*
|
|
524
|
+
* Server-only. The caller loads the NormalizeConfig from `system.md` via
|
|
525
|
+
* `loadSystemManifest()` from `@warpgogol/werkstatt-site/content` and passes it in.
|
|
526
|
+
*/
|
|
527
|
+
export function createDevNormalizeMiddleware(config: NormalizeConfig): MiddlewareHandler {
|
|
528
|
+
return async (_context, next) => {
|
|
529
|
+
const response = await next();
|
|
530
|
+
if (!config.enabled) return response;
|
|
531
|
+
const contentType = response.headers.get("content-type");
|
|
532
|
+
if (!contentType?.includes("text/html")) return response;
|
|
533
|
+
try {
|
|
534
|
+
const body = await response.text();
|
|
535
|
+
const normalized = normalizeHtml(body, config);
|
|
536
|
+
return new Response(normalized, {
|
|
537
|
+
status: response.status,
|
|
538
|
+
statusText: response.statusText,
|
|
539
|
+
headers: response.headers,
|
|
540
|
+
});
|
|
541
|
+
} catch (err) {
|
|
542
|
+
if (process.env.NODE_ENV !== "production") {
|
|
543
|
+
console.debug("[dev-normalize] normalizeHtml failed, returning original response:", err);
|
|
544
|
+
}
|
|
545
|
+
return response;
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Canonical pure text-position math shared by lint/validator diagnostics: convert a string offset into a 1-based line/column pair.</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not touch the filesystem — this module is pure and browser-safe.</item>
|
|
6
|
+
</non-goals>
|
|
7
|
+
</MODULE_CONTRACT>
|
|
8
|
+
<CHANGE_SUMMARY>
|
|
9
|
+
<item>RFC-0303: extracted as the single canonical home, previously duplicated across several validators.</item>
|
|
10
|
+
</CHANGE_SUMMARY>
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export function getLineColumn(text: string, index: number): { line: number; column: number } {
|
|
14
|
+
const before = text.slice(0, index);
|
|
15
|
+
const lines = before.split("\n");
|
|
16
|
+
const line = lines.length;
|
|
17
|
+
const column = lines[lines.length - 1]?.length ?? 0;
|
|
18
|
+
return { line, column: column + 1 };
|
|
19
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Type shim for the official `hls.js/light` export used by the lazy feature-video runtime.</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not change runtime resolution; this file only fills the missing package declaration.</item>
|
|
6
|
+
</non-goals>
|
|
7
|
+
</MODULE_CONTRACT>
|
|
8
|
+
<CHANGE_SUMMARY>
|
|
9
|
+
<item>Use the hls.js light runtime without losing TypeScript strictness.</item>
|
|
10
|
+
</CHANGE_SUMMARY>
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
declare module "hls.js/light" {
|
|
14
|
+
export { default } from "hls.js";
|
|
15
|
+
export * from "hls.js";
|
|
16
|
+
}
|