@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,156 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>
|
|
4
|
+
Architecture review 2026-07-10: compose the final IndexDecision from pure gate results.
|
|
5
|
+
Each gate (demand, evidence, substance, freshness, budget) is a small pure function that
|
|
6
|
+
returns a GateResult. The composer applies precedence and produces the final IndexDecision.
|
|
7
|
+
This concentrates all indexability logic in one module instead of scattered inline mutations
|
|
8
|
+
in the consumer's expandBlueprint.
|
|
9
|
+
</purpose>
|
|
10
|
+
<non-goals>
|
|
11
|
+
<item>Do not load data from the filesystem — gate inputs are supplied by the caller.</item>
|
|
12
|
+
<item>Do not mutate VirtualRouteEntry — the caller applies the composed decision.</item>
|
|
13
|
+
</non-goals>
|
|
14
|
+
</MODULE_CONTRACT>
|
|
15
|
+
<CHANGE_SUMMARY>
|
|
16
|
+
<item>Architecture review 2026-07-10: initial implementation of composeIndexDecision and pure gate functions.</item>
|
|
17
|
+
</CHANGE_SUMMARY>
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { IndexDecision, IndexReason } from "./types.ts";
|
|
21
|
+
|
|
22
|
+
/** The outcome of one gate evaluation. */
|
|
23
|
+
export interface GateResult {
|
|
24
|
+
readonly gate: "demand" | "evidence" | "substance" | "freshness" | "budget";
|
|
25
|
+
readonly pass: boolean;
|
|
26
|
+
readonly reason?: IndexReason;
|
|
27
|
+
readonly noindex: boolean;
|
|
28
|
+
/** When true, the entry should be dropped entirely (do-not-emit). */
|
|
29
|
+
readonly suppress?: boolean;
|
|
30
|
+
/** Gate-specific score (e.g. substance score 0..100). */
|
|
31
|
+
readonly score?: number;
|
|
32
|
+
/** Gate-specific metadata (e.g. werkEvidenceCount). */
|
|
33
|
+
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Precedence order: earlier gates take priority for the reason field when multiple fail. */
|
|
37
|
+
const GATE_PRECEDENCE: readonly GateResult["gate"][] = [
|
|
38
|
+
"demand",
|
|
39
|
+
"evidence",
|
|
40
|
+
"freshness",
|
|
41
|
+
"substance",
|
|
42
|
+
"budget",
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Compose the final IndexDecision from a base decision (record gate) and an array of gate results.
|
|
47
|
+
* Applies precedence: the first failing gate in precedence order sets the reason. Any failing gate
|
|
48
|
+
* sets noindex=true. If any gate requests suppress, the entry should be dropped entirely.
|
|
49
|
+
*/
|
|
50
|
+
export function composeIndexDecision(
|
|
51
|
+
base: IndexDecision,
|
|
52
|
+
results: readonly GateResult[],
|
|
53
|
+
): { decision: IndexDecision; suppress: boolean } {
|
|
54
|
+
const decision = { ...base };
|
|
55
|
+
let suppress = false;
|
|
56
|
+
|
|
57
|
+
const byGate = new Map<GateResult["gate"], GateResult>();
|
|
58
|
+
for (const r of results) byGate.set(r.gate, r);
|
|
59
|
+
|
|
60
|
+
for (const r of results) {
|
|
61
|
+
if (r.suppress) suppress = true;
|
|
62
|
+
if (!r.pass && r.noindex) decision.noindex = true;
|
|
63
|
+
if (r.score !== undefined) decision.substanceScore = r.score;
|
|
64
|
+
if (r.metadata?.werkEvidenceCount !== undefined) {
|
|
65
|
+
decision.werkEvidenceCount = r.metadata.werkEvidenceCount as number;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const gate of GATE_PRECEDENCE) {
|
|
70
|
+
const r = byGate.get(gate);
|
|
71
|
+
if (r && !r.pass && r.reason) {
|
|
72
|
+
decision.reason = r.reason;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (byGate.get("demand")) decision.demandGate = byGate.get("demand")!.pass;
|
|
78
|
+
if (byGate.get("evidence")) decision.evidenceGate = byGate.get("evidence")!.pass;
|
|
79
|
+
if (byGate.get("substance")) decision.substanceGate = byGate.get("substance")!.pass;
|
|
80
|
+
if (byGate.get("freshness")) decision.fresh = byGate.get("freshness")!.pass;
|
|
81
|
+
if (byGate.get("budget")) decision.withinBudget = byGate.get("budget")!.pass;
|
|
82
|
+
|
|
83
|
+
return { decision, suppress };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Demand gate: checks whether at least one qualifying demand signal exists for this entry. */
|
|
87
|
+
export function evaluateDemandGate(
|
|
88
|
+
hasMatchingDemand: boolean,
|
|
89
|
+
missingPolicy: "noindex" | "do-not-emit" = "noindex",
|
|
90
|
+
): GateResult {
|
|
91
|
+
if (hasMatchingDemand) {
|
|
92
|
+
return { gate: "demand", pass: true, noindex: false };
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
gate: "demand",
|
|
96
|
+
pass: false,
|
|
97
|
+
reason: "missing-demand",
|
|
98
|
+
noindex: true,
|
|
99
|
+
suppress: missingPolicy === "do-not-emit",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Evidence gate: checks whether enough Werk evidence records exist for this entry. */
|
|
104
|
+
export function evaluateEvidenceGate(
|
|
105
|
+
matchingCount: number,
|
|
106
|
+
minRequired: number | undefined,
|
|
107
|
+
existenceSource: "works" | undefined,
|
|
108
|
+
): GateResult {
|
|
109
|
+
if (typeof minRequired !== "number" && !existenceSource) {
|
|
110
|
+
return { gate: "evidence", pass: true, noindex: false };
|
|
111
|
+
}
|
|
112
|
+
const pass = typeof minRequired !== "number" || matchingCount >= minRequired;
|
|
113
|
+
if (pass) {
|
|
114
|
+
return {
|
|
115
|
+
gate: "evidence",
|
|
116
|
+
pass: true,
|
|
117
|
+
noindex: false,
|
|
118
|
+
metadata: { werkEvidenceCount: matchingCount },
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
gate: "evidence",
|
|
123
|
+
pass: false,
|
|
124
|
+
reason: "missing-werk-evidence",
|
|
125
|
+
noindex: true,
|
|
126
|
+
suppress: existenceSource === "works",
|
|
127
|
+
metadata: { werkEvidenceCount: matchingCount },
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Substance gate: checks whether the page substance score meets the minimum threshold. */
|
|
132
|
+
export function evaluateSubstanceGate(score: number, substanceMin: number): GateResult {
|
|
133
|
+
if (substanceMin === 0 || score >= substanceMin) {
|
|
134
|
+
return { gate: "substance", pass: true, noindex: false, score };
|
|
135
|
+
}
|
|
136
|
+
return { gate: "substance", pass: false, reason: "thin", noindex: true, score };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Freshness gate: checks whether the page's records are fresh within the SLA. */
|
|
140
|
+
export function evaluateFreshnessGate(fresh: boolean, hasInvalidDate: boolean): GateResult {
|
|
141
|
+
if (hasInvalidDate) {
|
|
142
|
+
return { gate: "freshness", pass: false, reason: "invalid-freshness", noindex: true };
|
|
143
|
+
}
|
|
144
|
+
if (fresh) {
|
|
145
|
+
return { gate: "freshness", pass: true, noindex: false };
|
|
146
|
+
}
|
|
147
|
+
return { gate: "freshness", pass: false, reason: "decayed", noindex: true };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Budget gate: checks whether the entry is within the index budget. */
|
|
151
|
+
export function evaluateBudgetGate(withinBudget: boolean): GateResult {
|
|
152
|
+
if (withinBudget) {
|
|
153
|
+
return { gate: "budget", pass: true, noindex: false };
|
|
154
|
+
}
|
|
155
|
+
return { gate: "budget", pass: false, reason: "over-budget", noindex: true };
|
|
156
|
+
}
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>
|
|
4
|
+
[RFC-0192] Axis-generic eligibility engine. The legacy foerderung matrix generalized over an
|
|
5
|
+
arbitrary record set and a declarative axis→field binding: enumerate candidate tuples per depth,
|
|
6
|
+
count matching active records, derive live / noindex / redirect-stub decisions, and expose
|
|
7
|
+
live children / siblings for internal-link grids. Pure — no I/O.
|
|
8
|
+
</purpose>
|
|
9
|
+
<non-goals>
|
|
10
|
+
<item>Do not build URLs or blocks (the Blueprint provider does).</item>
|
|
11
|
+
<item>Do not read content (the kernel command supplies records + universes).</item>
|
|
12
|
+
</non-goals>
|
|
13
|
+
</MODULE_CONTRACT>
|
|
14
|
+
<CHANGE_SUMMARY>
|
|
15
|
+
<item>RFC-0192: initial axis-generic engine.</item>
|
|
16
|
+
</CHANGE_SUMMARY>
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type {
|
|
20
|
+
AxisTuple,
|
|
21
|
+
EligibilityPolicy,
|
|
22
|
+
IndexDecision,
|
|
23
|
+
SurfaceAxis,
|
|
24
|
+
SurfaceRecord,
|
|
25
|
+
} from "./types.ts";
|
|
26
|
+
|
|
27
|
+
/** Axis id → the record field carrying that axis's membership value(s). */
|
|
28
|
+
export type AxisFieldMap = Readonly<Record<string, string>>;
|
|
29
|
+
|
|
30
|
+
/** Pre-built index: axisId → value → Set<record index>. */
|
|
31
|
+
export interface RecordIndex {
|
|
32
|
+
byAxisValue: ReadonlyMap<string, ReadonlyMap<string, ReadonlySet<number>>>;
|
|
33
|
+
records: readonly SurfaceRecord[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A single resolved combination at one depth. */
|
|
37
|
+
export interface MatrixEntry {
|
|
38
|
+
depth: number;
|
|
39
|
+
tuple: AxisTuple;
|
|
40
|
+
recordCount: number;
|
|
41
|
+
indexable: boolean;
|
|
42
|
+
noindex: boolean;
|
|
43
|
+
decision: IndexDecision;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface EligibilityMatrix {
|
|
47
|
+
/** All emitted entries (live + shallow stubs), ordered by depth then enumeration order. */
|
|
48
|
+
entries: MatrixEntry[];
|
|
49
|
+
/** pathKey → entry, for ancestor/child/sibling lookups. */
|
|
50
|
+
byKey: Map<string, MatrixEntry>;
|
|
51
|
+
/** depth → set of live pathKeys. */
|
|
52
|
+
liveKeysByDepth: Map<number, Set<string>>;
|
|
53
|
+
axisOrder: readonly string[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const DEFAULT_MAX_STUB_DEPTH = 1;
|
|
57
|
+
|
|
58
|
+
/** Lowercase, dash-only normalization. Throws on characters outside the policy pattern. */
|
|
59
|
+
export function normalizeSegment(value: string, pattern: RegExp): string {
|
|
60
|
+
const trimmed = value.trim().toLowerCase().replace(/_/g, "-");
|
|
61
|
+
if (!pattern.test(trimmed)) {
|
|
62
|
+
throw new Error(`Invalid surface URL segment: "${value}" (normalized: "${trimmed}")`);
|
|
63
|
+
}
|
|
64
|
+
return trimmed;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** True when `value` (string or array) includes the wanted axis value. */
|
|
68
|
+
function fieldIncludes(value: SurfaceRecord[string], wanted: string): boolean {
|
|
69
|
+
if (Array.isArray(value)) return value.includes(wanted);
|
|
70
|
+
return value === wanted;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Axis-generic membership test: a record matches a partial tuple when, for every constrained axis,
|
|
75
|
+
* the record's bound field carries the chosen value. An unconstrained axis imposes no filter.
|
|
76
|
+
*/
|
|
77
|
+
export function matchesRecord(
|
|
78
|
+
record: SurfaceRecord,
|
|
79
|
+
tuple: AxisTuple,
|
|
80
|
+
axisFieldMap: AxisFieldMap,
|
|
81
|
+
): boolean {
|
|
82
|
+
for (const [axisId, wanted] of Object.entries(tuple)) {
|
|
83
|
+
if (wanted === undefined) continue;
|
|
84
|
+
const field = axisFieldMap[axisId];
|
|
85
|
+
if (!field) return false;
|
|
86
|
+
if (!fieldIncludes(record[field], wanted)) return false;
|
|
87
|
+
}
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Count active records matching the tuple. */
|
|
92
|
+
export function countMatching(
|
|
93
|
+
records: readonly SurfaceRecord[],
|
|
94
|
+
tuple: AxisTuple,
|
|
95
|
+
axisFieldMap: AxisFieldMap,
|
|
96
|
+
): number {
|
|
97
|
+
let count = 0;
|
|
98
|
+
for (const record of records) {
|
|
99
|
+
if (record.status === "archived") continue;
|
|
100
|
+
if (matchesRecord(record, tuple, axisFieldMap)) count += 1;
|
|
101
|
+
}
|
|
102
|
+
return count;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Build a per-axis-value index so countMatching can run in intersection time instead of linear time. */
|
|
106
|
+
export function buildRecordIndex(
|
|
107
|
+
records: readonly SurfaceRecord[],
|
|
108
|
+
axisFieldMap: AxisFieldMap,
|
|
109
|
+
): RecordIndex {
|
|
110
|
+
const byAxisValue = new Map<string, Map<string, Set<number>>>();
|
|
111
|
+
for (let i = 0; i < records.length; i += 1) {
|
|
112
|
+
const record = records[i];
|
|
113
|
+
if (record.status === "archived") continue;
|
|
114
|
+
for (const [axisId, field] of Object.entries(axisFieldMap)) {
|
|
115
|
+
const value = record[field];
|
|
116
|
+
if (value === undefined) continue;
|
|
117
|
+
let axisMap = byAxisValue.get(axisId);
|
|
118
|
+
if (!axisMap) {
|
|
119
|
+
axisMap = new Map<string, Set<number>>();
|
|
120
|
+
byAxisValue.set(axisId, axisMap);
|
|
121
|
+
}
|
|
122
|
+
if (Array.isArray(value)) {
|
|
123
|
+
for (const v of value) {
|
|
124
|
+
let set = axisMap.get(v);
|
|
125
|
+
if (!set) {
|
|
126
|
+
set = new Set<number>();
|
|
127
|
+
axisMap.set(v, set);
|
|
128
|
+
}
|
|
129
|
+
set.add(i);
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
let set = axisMap.get(value);
|
|
133
|
+
if (!set) {
|
|
134
|
+
set = new Set<number>();
|
|
135
|
+
axisMap.set(value, set);
|
|
136
|
+
}
|
|
137
|
+
set.add(i);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Freeze inner structures so callers cannot mutate them.
|
|
142
|
+
const frozenByAxisValue = new Map<string, ReadonlyMap<string, ReadonlySet<number>>>();
|
|
143
|
+
for (const [axisId, axisMap] of byAxisValue) {
|
|
144
|
+
const frozenAxisMap = new Map<string, ReadonlySet<number>>();
|
|
145
|
+
for (const [value, set] of axisMap) {
|
|
146
|
+
frozenAxisMap.set(value, set);
|
|
147
|
+
}
|
|
148
|
+
frozenByAxisValue.set(axisId, frozenAxisMap);
|
|
149
|
+
}
|
|
150
|
+
return { byAxisValue: frozenByAxisValue, records };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** O(k) count using the pre-built index, where k is the number of matching records. */
|
|
154
|
+
export function countMatchingIndexed(
|
|
155
|
+
tuple: AxisTuple,
|
|
156
|
+
axisFieldMap: AxisFieldMap,
|
|
157
|
+
index: RecordIndex,
|
|
158
|
+
): number {
|
|
159
|
+
let candidates: Set<number> | undefined;
|
|
160
|
+
for (const [axisId, wanted] of Object.entries(tuple)) {
|
|
161
|
+
if (wanted === undefined) continue;
|
|
162
|
+
const field = axisFieldMap[axisId];
|
|
163
|
+
if (!field) return 0;
|
|
164
|
+
const axisMap = index.byAxisValue.get(axisId);
|
|
165
|
+
if (!axisMap) return 0;
|
|
166
|
+
const set = axisMap.get(wanted);
|
|
167
|
+
if (!set) return 0;
|
|
168
|
+
if (candidates === undefined) {
|
|
169
|
+
candidates = new Set(set);
|
|
170
|
+
} else {
|
|
171
|
+
for (const idx of candidates) {
|
|
172
|
+
if (!set.has(idx)) candidates.delete(idx);
|
|
173
|
+
}
|
|
174
|
+
if (candidates.size === 0) return 0;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (candidates === undefined) {
|
|
178
|
+
// No axis constrained — count all active records.
|
|
179
|
+
let count = 0;
|
|
180
|
+
for (const record of index.records) {
|
|
181
|
+
if (record.status !== "archived") count += 1;
|
|
182
|
+
}
|
|
183
|
+
return count;
|
|
184
|
+
}
|
|
185
|
+
let count = 0;
|
|
186
|
+
for (const idx of candidates) {
|
|
187
|
+
const record = index.records[idx];
|
|
188
|
+
if (record && record.status !== "archived") count += 1;
|
|
189
|
+
}
|
|
190
|
+
return count;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Stable key for a (depth, tuple) pair. */
|
|
194
|
+
export function pathKey(depth: number, tuple: AxisTuple, axisOrder: readonly string[]): string {
|
|
195
|
+
const parts: string[] = [String(depth)];
|
|
196
|
+
for (let i = 0; i < depth; i += 1) {
|
|
197
|
+
parts.push(tuple[axisOrder[i]!] ?? "");
|
|
198
|
+
}
|
|
199
|
+
return parts.join("|");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Enumerate every candidate tuple at `depth` (cartesian over the first `depth` axes' universes). */
|
|
203
|
+
export function* enumerateCandidateTuples(
|
|
204
|
+
axes: readonly SurfaceAxis[],
|
|
205
|
+
depth: number,
|
|
206
|
+
): Generator<AxisTuple> {
|
|
207
|
+
if (depth === 0) {
|
|
208
|
+
yield {};
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const head = axes.slice(0, depth);
|
|
212
|
+
const indices = new Array(depth).fill(0);
|
|
213
|
+
const universes = head.map((axis) => axis.universe);
|
|
214
|
+
// Guard against an empty universe producing an infinite/empty product.
|
|
215
|
+
if (universes.some((u) => u.length === 0)) return;
|
|
216
|
+
for (;;) {
|
|
217
|
+
const tuple: AxisTuple = {};
|
|
218
|
+
for (let i = 0; i < depth; i += 1) tuple[head[i]!.id] = universes[i]![indices[i]!];
|
|
219
|
+
yield tuple;
|
|
220
|
+
let carry = depth - 1;
|
|
221
|
+
while (carry >= 0) {
|
|
222
|
+
indices[carry] += 1;
|
|
223
|
+
if (indices[carry] < universes[carry]!.length) break;
|
|
224
|
+
indices[carry] = 0;
|
|
225
|
+
carry -= 1;
|
|
226
|
+
}
|
|
227
|
+
if (carry < 0) break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function deriveDecision(
|
|
232
|
+
depth: number,
|
|
233
|
+
recordCount: number,
|
|
234
|
+
policy: EligibilityPolicy,
|
|
235
|
+
forceNonIndexable: boolean,
|
|
236
|
+
): IndexDecision {
|
|
237
|
+
const min = policy.minRecordsPerDepth[depth] ?? 1;
|
|
238
|
+
const recordGate = recordCount >= min;
|
|
239
|
+
const noindexThreshold = policy.noindexBelowPerDepth[depth];
|
|
240
|
+
const noindex = recordGate && noindexThreshold !== undefined && recordCount < noindexThreshold;
|
|
241
|
+
if (recordGate && forceNonIndexable) {
|
|
242
|
+
// RFC-0240: a depth gated behind the regional-hub-or-higher tier — the record gate passed but
|
|
243
|
+
// the resolved entitlement tier has not unlocked this depth (e.g. d3 region hubs on the base tier).
|
|
244
|
+
return { recordGate, indexable: false, noindex: true, reason: "regional-gated" };
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
recordGate,
|
|
248
|
+
indexable: recordGate,
|
|
249
|
+
noindex,
|
|
250
|
+
...(recordGate ? {} : { reason: "too-few-records" as const }),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Build the eligibility matrix. Shallow empty combinations (depth ≤ maxStubDepth) are emitted as
|
|
256
|
+
* non-live stubs (for 301 redirects); deeper empty combinations are dropped so the build graph
|
|
257
|
+
* stays bounded.
|
|
258
|
+
*/
|
|
259
|
+
export function buildEligibilityMatrix(
|
|
260
|
+
axes: readonly SurfaceAxis[],
|
|
261
|
+
axisFieldMap: AxisFieldMap,
|
|
262
|
+
records: readonly SurfaceRecord[],
|
|
263
|
+
policy: EligibilityPolicy,
|
|
264
|
+
options?: {
|
|
265
|
+
maxDepth?: number;
|
|
266
|
+
maxStubDepth?: number;
|
|
267
|
+
/** RFC-0240: depths forced non-indexable (dropped) unless the resolved entitlement tier unlocks them. */
|
|
268
|
+
forceNonIndexableDepths?: readonly number[];
|
|
269
|
+
},
|
|
270
|
+
): EligibilityMatrix {
|
|
271
|
+
const axisOrder = axes.map((axis) => axis.id);
|
|
272
|
+
const maxDepth = options?.maxDepth ?? axes.length;
|
|
273
|
+
const maxStubDepth = options?.maxStubDepth ?? DEFAULT_MAX_STUB_DEPTH;
|
|
274
|
+
const forceNonIndexableDepths = new Set(options?.forceNonIndexableDepths ?? []);
|
|
275
|
+
|
|
276
|
+
const entries: MatrixEntry[] = [];
|
|
277
|
+
const byKey = new Map<string, MatrixEntry>();
|
|
278
|
+
const liveKeysByDepth = new Map<number, Set<string>>();
|
|
279
|
+
const recordIndex = buildRecordIndex(records, axisFieldMap);
|
|
280
|
+
|
|
281
|
+
for (let depth = 0; depth <= maxDepth; depth += 1) {
|
|
282
|
+
const liveSet = new Set<string>();
|
|
283
|
+
liveKeysByDepth.set(depth, liveSet);
|
|
284
|
+
for (const tuple of enumerateCandidateTuples(axes, depth)) {
|
|
285
|
+
const recordCount = countMatchingIndexed(tuple, axisFieldMap, recordIndex);
|
|
286
|
+
const decision = deriveDecision(
|
|
287
|
+
depth,
|
|
288
|
+
recordCount,
|
|
289
|
+
policy,
|
|
290
|
+
forceNonIndexableDepths.has(depth),
|
|
291
|
+
);
|
|
292
|
+
if (!decision.indexable && depth > maxStubDepth) continue;
|
|
293
|
+
const entry: MatrixEntry = {
|
|
294
|
+
depth,
|
|
295
|
+
tuple,
|
|
296
|
+
recordCount,
|
|
297
|
+
indexable: decision.indexable,
|
|
298
|
+
noindex: decision.noindex,
|
|
299
|
+
decision,
|
|
300
|
+
};
|
|
301
|
+
const key = pathKey(depth, tuple, axisOrder);
|
|
302
|
+
entries.push(entry);
|
|
303
|
+
byKey.set(key, entry);
|
|
304
|
+
if (decision.indexable) liveSet.add(key);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return { entries, byKey, liveKeysByDepth, axisOrder };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Walk shallower, dropping one axis at a time, to the nearest live ancestor (or root). */
|
|
312
|
+
export function nearestLiveAncestor(
|
|
313
|
+
entry: MatrixEntry,
|
|
314
|
+
matrix: EligibilityMatrix,
|
|
315
|
+
policy: EligibilityPolicy,
|
|
316
|
+
): MatrixEntry | null {
|
|
317
|
+
if (policy.redirectPolicy === "root") {
|
|
318
|
+
return matrix.byKey.get(pathKey(0, {}, matrix.axisOrder)) ?? null;
|
|
319
|
+
}
|
|
320
|
+
for (let depth = entry.depth - 1; depth >= 0; depth -= 1) {
|
|
321
|
+
const tuple: AxisTuple = {};
|
|
322
|
+
for (let i = 0; i < depth; i += 1) {
|
|
323
|
+
const axisId = matrix.axisOrder[i]!;
|
|
324
|
+
if (entry.tuple[axisId] !== undefined) tuple[axisId] = entry.tuple[axisId];
|
|
325
|
+
}
|
|
326
|
+
const key = pathKey(depth, tuple, matrix.axisOrder);
|
|
327
|
+
if (matrix.liveKeysByDepth.get(depth)?.has(key)) return matrix.byKey.get(key) ?? null;
|
|
328
|
+
}
|
|
329
|
+
return matrix.byKey.get(pathKey(0, {}, matrix.axisOrder)) ?? null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Live entries one level deeper that extend the given parent tuple. */
|
|
333
|
+
export function liveChildrenOf(parent: MatrixEntry, matrix: EligibilityMatrix): MatrixEntry[] {
|
|
334
|
+
const childDepth = parent.depth + 1;
|
|
335
|
+
const liveKeys = matrix.liveKeysByDepth.get(childDepth);
|
|
336
|
+
if (!liveKeys) return [];
|
|
337
|
+
const results: MatrixEntry[] = [];
|
|
338
|
+
for (const key of liveKeys) {
|
|
339
|
+
const entry = matrix.byKey.get(key);
|
|
340
|
+
if (!entry) continue;
|
|
341
|
+
const matchesParent = matrix.axisOrder.slice(0, parent.depth).every((axisId) => {
|
|
342
|
+
const pv = parent.tuple[axisId];
|
|
343
|
+
return pv === undefined || pv === entry.tuple[axisId];
|
|
344
|
+
});
|
|
345
|
+
if (matchesParent) results.push(entry);
|
|
346
|
+
}
|
|
347
|
+
return results;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Live entries at the same depth sharing the same parent tuple. */
|
|
351
|
+
export function liveSiblingsOf(entry: MatrixEntry, matrix: EligibilityMatrix): MatrixEntry[] {
|
|
352
|
+
if (entry.depth === 0) return [];
|
|
353
|
+
const parentDepth = entry.depth - 1;
|
|
354
|
+
const parentTuple: AxisTuple = {};
|
|
355
|
+
for (let i = 0; i < parentDepth; i += 1) {
|
|
356
|
+
const axisId = matrix.axisOrder[i]!;
|
|
357
|
+
if (entry.tuple[axisId] !== undefined) parentTuple[axisId] = entry.tuple[axisId];
|
|
358
|
+
}
|
|
359
|
+
const parentEntry: MatrixEntry = {
|
|
360
|
+
depth: parentDepth,
|
|
361
|
+
tuple: parentTuple,
|
|
362
|
+
recordCount: 0,
|
|
363
|
+
indexable: true,
|
|
364
|
+
noindex: false,
|
|
365
|
+
decision: { recordGate: true, indexable: true, noindex: false },
|
|
366
|
+
};
|
|
367
|
+
return liveChildrenOf(parentEntry, matrix).filter(
|
|
368
|
+
(sibling) =>
|
|
369
|
+
pathKey(sibling.depth, sibling.tuple, matrix.axisOrder) !==
|
|
370
|
+
pathKey(entry.depth, entry.tuple, matrix.axisOrder),
|
|
371
|
+
);
|
|
372
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>
|
|
4
|
+
RFC-0280/RFC-0281 contracts for evidence-backed Programmatic Surface inputs:
|
|
5
|
+
aggregate search-demand signals and consented Werk records.
|
|
6
|
+
</purpose>
|
|
7
|
+
<non-goals>
|
|
8
|
+
<item>Do not read files or call external demand APIs.</item>
|
|
9
|
+
</non-goals>
|
|
10
|
+
</MODULE_CONTRACT>
|
|
11
|
+
<CHANGE_SUMMARY>
|
|
12
|
+
<item>RFC-0280/RFC-0281: first-class demand and Werk evidence contracts.</item>
|
|
13
|
+
</CHANGE_SUMMARY>
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
|
|
18
|
+
export const demandIntentSchema = z.enum([
|
|
19
|
+
"informational",
|
|
20
|
+
"commercial",
|
|
21
|
+
"transactional",
|
|
22
|
+
"navigational",
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
export const demandSignalSourceSchema = z.enum(["gsc", "keyword-planner", "manual", "derived"]);
|
|
26
|
+
|
|
27
|
+
export const demandAxesSchema = z
|
|
28
|
+
.object({
|
|
29
|
+
industry: z.string().min(1).optional(),
|
|
30
|
+
country: z.string().min(1).optional(),
|
|
31
|
+
region: z.string().min(1).optional(),
|
|
32
|
+
city: z.string().min(1).optional(),
|
|
33
|
+
demand: z.string().min(1).optional(),
|
|
34
|
+
})
|
|
35
|
+
.strict();
|
|
36
|
+
|
|
37
|
+
export const demandSignalSchema = z
|
|
38
|
+
.object({
|
|
39
|
+
id: z.string().min(1),
|
|
40
|
+
query: z.string().min(1),
|
|
41
|
+
intent: demandIntentSchema,
|
|
42
|
+
axes: demandAxesSchema,
|
|
43
|
+
volume: z.number().min(0).optional(),
|
|
44
|
+
competition: z.number().min(0).max(1).optional(),
|
|
45
|
+
source: demandSignalSourceSchema,
|
|
46
|
+
observedAt: z.string().datetime({ offset: true }),
|
|
47
|
+
provenance: z.object({
|
|
48
|
+
importId: z.string().min(1),
|
|
49
|
+
sourceRef: z.string().min(1).optional(),
|
|
50
|
+
}),
|
|
51
|
+
})
|
|
52
|
+
.strict();
|
|
53
|
+
|
|
54
|
+
export type DemandIntent = z.infer<typeof demandIntentSchema>;
|
|
55
|
+
export type DemandSignalSource = z.infer<typeof demandSignalSourceSchema>;
|
|
56
|
+
export type DemandAxes = z.infer<typeof demandAxesSchema>;
|
|
57
|
+
export type DemandSignal = z.infer<typeof demandSignalSchema>;
|
|
58
|
+
|
|
59
|
+
export const werkRecordSchema = z
|
|
60
|
+
.object({
|
|
61
|
+
id: z.string().min(1),
|
|
62
|
+
title: z.string().min(1),
|
|
63
|
+
axes: z
|
|
64
|
+
.object({
|
|
65
|
+
industry: z.array(z.string().min(1)).min(1),
|
|
66
|
+
country: z.string().min(1).optional(),
|
|
67
|
+
region: z.string().min(1).optional(),
|
|
68
|
+
city: z.string().min(1),
|
|
69
|
+
demand: z.array(z.string().min(1)).optional(),
|
|
70
|
+
})
|
|
71
|
+
.strict(),
|
|
72
|
+
facts: z
|
|
73
|
+
.object({
|
|
74
|
+
materials: z.array(z.string().min(1)).optional(),
|
|
75
|
+
scope: z.string().min(1).optional(),
|
|
76
|
+
problem: z.string().min(1).optional(),
|
|
77
|
+
outcome: z.string().min(1).optional(),
|
|
78
|
+
completedAt: z.string().datetime({ offset: true }).optional(),
|
|
79
|
+
durationDays: z.number().int().min(0).optional(),
|
|
80
|
+
})
|
|
81
|
+
.strict(),
|
|
82
|
+
media: z.array(z.object({ ref: z.string().min(1), creditRef: z.string().min(1) })).optional(),
|
|
83
|
+
provenance: z.object({
|
|
84
|
+
sourceRef: z.string().min(1),
|
|
85
|
+
anchoredHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
|
|
86
|
+
}),
|
|
87
|
+
consent: z
|
|
88
|
+
.object({
|
|
89
|
+
publishable: z.boolean(),
|
|
90
|
+
clientApproved: z.boolean(),
|
|
91
|
+
redactions: z.array(z.string().min(1)).optional(),
|
|
92
|
+
})
|
|
93
|
+
.strict(),
|
|
94
|
+
})
|
|
95
|
+
.strict();
|
|
96
|
+
|
|
97
|
+
export type WerkRecord = z.infer<typeof werkRecordSchema>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Maintains packages/surface/src/fleet.ts as an authored surface authored module so agents can evolve it without rediscovering local boundaries.</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not replace per-site Bordbuch, autonomy, breaker, or approval state.</item>
|
|
6
|
+
</non-goals>
|
|
7
|
+
</MODULE_CONTRACT>
|
|
8
|
+
<CHANGE_SUMMARY>
|
|
9
|
+
<item>RFC-0284: initial implementation of fleet Leitstand contracts.</item>
|
|
10
|
+
</CHANGE_SUMMARY>
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
|
|
15
|
+
export const fleetBreakerStateSchema = z.enum(["armed", "tripped", "frozen"]);
|
|
16
|
+
|
|
17
|
+
export const fleetSiteStatusSchema = z.object({
|
|
18
|
+
site: z.string().min(1),
|
|
19
|
+
path: z.string().min(1),
|
|
20
|
+
bordbuchHash: z.string().min(1).nullable(),
|
|
21
|
+
autonomy: z.record(z.string(), z.string()),
|
|
22
|
+
openEscalations: z.number().int().nonnegative(),
|
|
23
|
+
breaker: fleetBreakerStateSchema,
|
|
24
|
+
dirtyFlags: z.array(z.string().min(1)),
|
|
25
|
+
lastOutcomeWindow: z.string().datetime().optional(),
|
|
26
|
+
humanMinutesPer1000Pages: z.number().min(0).optional(),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const fleetJobKindSchema = z.enum(["generate", "enrich", "translate", "review", "rollback"]);
|
|
30
|
+
|
|
31
|
+
export const fleetJobSchema = z.object({
|
|
32
|
+
site: z.string().min(1),
|
|
33
|
+
kind: fleetJobKindSchema,
|
|
34
|
+
scope: z.string().min(1).optional(),
|
|
35
|
+
priority: z.number(),
|
|
36
|
+
safety: z.boolean().default(false),
|
|
37
|
+
estimatedCost: z.object({
|
|
38
|
+
llmTokens: z.number().int().nonnegative().optional(),
|
|
39
|
+
reviewMinutes: z.number().nonnegative().optional(),
|
|
40
|
+
ciSeconds: z.number().nonnegative().optional(),
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export const fleetPlanSchema = z.object({
|
|
45
|
+
collectedAt: z.string().datetime().nullable(),
|
|
46
|
+
budgets: z.object({
|
|
47
|
+
llmTokens: z.number().int().nonnegative(),
|
|
48
|
+
reviewMinutes: z.number().nonnegative(),
|
|
49
|
+
ciSeconds: z.number().nonnegative(),
|
|
50
|
+
}),
|
|
51
|
+
jobs: z.array(fleetJobSchema),
|
|
52
|
+
blocked: z.array(z.object({ site: z.string().min(1), reason: z.string().min(1) })),
|
|
53
|
+
humanMinutesPer1000Pages: z.number().min(0),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export type FleetBreakerState = z.infer<typeof fleetBreakerStateSchema>;
|
|
57
|
+
export type FleetSiteStatus = z.infer<typeof fleetSiteStatusSchema>;
|
|
58
|
+
export type FleetJobKind = z.infer<typeof fleetJobKindSchema>;
|
|
59
|
+
export type FleetJob = z.infer<typeof fleetJobSchema>;
|
|
60
|
+
export type FleetPlan = z.infer<typeof fleetPlanSchema>;
|