@warpgogol/werkstatt-shared 0.14.1 → 0.15.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/README.md +23 -15
- package/README.uk.md +1 -1
- package/package.json +517 -766
- package/src/{share/agent → agent}/ard-catalog.ts +6 -1
- package/src/{share/agent → agent}/dns-aid.test.ts +2 -2
- package/src/{share/agent → agent}/manifest.ts +15 -1
- package/src/{share/agent → agent}/search.ts +6 -1
- package/src/{share → agent}/tests/api-catalog.test.ts +2 -2
- package/src/{share → agent}/tests/ard-catalog.test.ts +2 -2
- package/src/{share → agent}/tests/capability.test.ts +1 -1
- package/src/{share → agent}/tests/fleet-catalog.test.ts +1 -1
- package/src/{share → agent}/tests/knowledge.test.ts +2 -2
- package/src/{share → agent}/tests/manifest.test.ts +1 -1
- package/src/{share → agent}/tests/mcp-card.test.ts +2 -2
- package/src/{share → agent}/tests/openapi.test.ts +2 -2
- package/src/{share → agent}/tests/proof.test.ts +1 -1
- package/src/{share → attribution}/material-credits.ts +4 -4
- package/src/{share → checks}/check-hints.ts +6 -1
- package/src/checks/index.ts +4 -1
- package/src/checks/lib/astro-site-url.ts +6 -1
- package/src/checks/lib/i18n.ts +14 -0
- package/src/checks/result-helpers.ts +2 -2
- package/src/checks/tests/remediation-auto-populate.test.ts +1 -1
- package/src/{share → client-scripts}/counter-utils.ts +6 -2
- package/src/{share/scripts → client-scripts}/external-link-qr.ts +11 -16
- package/src/{share/scripts → client-scripts}/external-links.ts +12 -1
- package/src/{share/scripts → client-scripts}/gsap-counter.ts +8 -3
- package/src/client-scripts/index.ts +35 -0
- package/src/{share/scripts → client-scripts}/inline-number-animation.ts +1 -1
- package/src/{share/scripts → client-scripts}/lenis.ts +6 -1
- package/src/{share/scripts → client-scripts}/lordicon.ts +6 -1
- package/src/{share/scripts → client-scripts}/orchestrator.ts +10 -1
- package/src/{share/scripts → client-scripts}/scheduler.ts +6 -1
- package/src/client-scripts/scroll-spy.ts +106 -0
- package/src/client-scripts/tests/orchestrator.test.ts +47 -0
- package/src/client-scripts/tests/scroll-spy.test.ts +204 -0
- package/src/{share/scripts → client-scripts/tests}/video-player.test.ts +51 -31
- package/src/component/contracts.ts +206 -0
- package/src/component/index.ts +11 -0
- package/src/{share → content}/content-reference.ts +3 -3
- package/src/{share/content → content}/entity-id.ts +6 -3
- package/src/content/index.ts +3 -0
- package/src/{share/content → content}/merge.ts +6 -1
- package/src/content/system-manifest.test.ts +107 -0
- package/src/content/system-manifest.ts +12 -94
- package/src/{share → content}/tests/resolve-field-path.test.ts +1 -1
- package/src/{share → content}/tests/substitute-deep.test.ts +1 -1
- package/src/content-discipline/index.ts +19 -0
- package/src/{share/content-discipline → content-discipline}/parsers.ts +2 -2
- package/src/{share/env.d.ts → env.d.ts} +6 -3
- package/src/fingerprint/canonical-json.ts +731 -0
- package/src/fingerprint/index.ts +12 -0
- package/src/fingerprint/primitives.ts +72 -0
- package/src/{share/i18n → i18n}/localization.ts +6 -2
- package/src/integration/adapters.ts +10 -71
- package/src/integration/dispatch.ts +1 -1
- package/src/integration/funnel.ts +6 -1
- package/src/integration/index.ts +101 -4
- package/src/integration/lagebild-destination-adapter.ts +108 -0
- package/src/integration/lagebild-ingress.ts +7 -12
- package/src/integration/orchestration.ts +11 -10
- package/src/integration/tests/deliver-event.test.ts +1 -1
- package/src/integration/tests/dispatch.test.ts +4 -4
- package/src/integration/tests/hub-runtime.test.ts +22 -14
- package/src/integration/vitest.config.ts +1 -0
- package/src/kernel/desired-state.ts +227 -0
- package/src/kernel/diagnostic.ts +289 -0
- package/src/kernel/fs-atomic.ts +131 -0
- package/src/kernel/index.ts +15 -0
- package/src/kernel/tests/fs-atomic.test.ts +150 -0
- package/src/kernel/tests/workspace-io.test.ts +187 -0
- package/src/kernel/types.ts +459 -0
- package/src/kernel/workspace-io.ts +248 -0
- package/src/{share → knowledge}/tests/claim.test.ts +1 -1
- package/src/{share → knowledge}/tests/derivation.test.ts +1 -1
- package/src/{share → knowledge}/tests/freshness.test.ts +1 -1
- package/src/{share → knowledge}/tests/ledger.test.ts +1 -1
- package/src/{share → knowledge}/tests/plan.test.ts +1 -1
- package/src/{share → knowledge}/tests/source.test.ts +1 -1
- package/src/legal/index.ts +15 -0
- package/src/{share/middleware → middleware}/access-protection.ts +8 -8
- package/src/{share → node}/dev-props-validator.ts +6 -1
- package/src/{share → node}/import-scan.ts +23 -3
- package/src/{share → node}/semantic/derived-prices-loader.ts +4 -2
- package/src/{share → node}/semantic/markdown-twin-provenance.ts +1 -1
- package/src/{share/semantic → node}/tests/derived-prices-loader.test.ts +14 -1
- package/src/{share/semantic → node}/tests/markdown-twin-provenance.test.ts +1 -1
- package/src/observability/env-resolver.ts +6 -1
- package/src/observability/index.ts +6 -1
- package/src/observability/otlp-converter.ts +6 -1
- package/src/observability/otlp-transport.ts +6 -1
- package/src/observability/pusher.ts +6 -1
- package/src/observability/redact.ts +6 -1
- package/src/observability/typed-refs.ts +6 -2
- package/src/observability/vitest.config.ts +1 -0
- package/src/{share → offers}/formula-eval.ts +3 -3
- package/src/{share → offers}/tests/formula-eval.test.ts +2 -2
- package/src/onboarding/index.ts +1 -0
- package/src/{share → onboarding}/tests/onboarding-yaml.test.ts +1 -1
- package/src/ontology/archetype-registry.ts +6 -1
- package/src/ontology/archetypes/index.json +33 -1
- package/src/ontology/archetypes/index.yaml +27 -1
- package/src/ontology/archetypes/sections/entity-grid.yaml +46 -0
- package/src/ontology/archetypes/sections/exit-diagram.yaml +43 -0
- package/src/ontology/biome-token-projection.ts +6 -1
- package/src/ontology/cosmic/index.ts +6 -1
- package/src/ontology/cosmic/moon-catalog.ts +6 -1
- package/src/ontology/cosmic/planet-catalog.ts +6 -1
- package/src/ontology/cosmic/star-catalog.ts +6 -1
- package/src/ontology/enums.ts +6 -1
- package/src/ontology/external-surfaces/index.ts +1 -1
- package/src/ontology/index.ts +6 -2
- package/src/ontology/manifest.ts +7 -4
- package/src/ontology/operations/artifact-store.ts +45 -0
- package/src/ontology/operations/dht.ts +98 -0
- package/src/ontology/operations/handoff.ts +92 -0
- package/src/ontology/operations/index.ts +187 -1
- package/src/ontology/operations/leitstand.ts +99 -0
- package/src/ontology/operations/materialization.ts +90 -0
- package/src/ontology/operations/mission.ts +111 -0
- package/src/ontology/operations/naming-policy.ts +132 -0
- package/src/ontology/operations/notausgang.ts +64 -0
- package/src/ontology/operations/release.ts +121 -0
- package/src/ontology/operations/sternsystem.ts +192 -0
- package/src/ontology/operations/werkstatt.ts +48 -0
- package/src/ontology/schemas/biome.ts +7 -2
- package/src/ontology/schemas/capability.ts +1 -1
- package/src/ontology/schemas/constellation.ts +6 -1
- package/src/ontology/schemas/index.ts +6 -2
- package/src/ontology/schemas/manifest-resolver.ts +6 -1
- package/src/ontology/schemas/page-entry.ts +9 -5
- package/src/ontology/schemas/system/growth.ts +6 -1
- package/src/ontology/schemas/system/manifest.ts +40 -1
- package/src/ontology/schemas/system/page-output.ts +7 -6
- package/src/ontology/schemas/system/text.ts +15 -0
- package/src/ontology/schemas/system.ts +6 -2
- package/src/ontology/shared-section-props/body-fragments.ts +2 -0
- package/src/ontology/tests/sternsystem-owner.test.ts +1 -1
- package/src/ontology/vitest.config.ts +1 -0
- package/src/{share → page}/page.ts +13 -9
- package/src/{share → page}/tests/dev-props-validator.test.ts +1 -1
- package/src/{share → page}/tests/hero-section-numbering-parity.test.ts +1 -1
- package/src/passport/index.ts +6 -1
- package/src/passport/key-rotate.ts +10 -2
- package/src/passport/schema.ts +6 -1
- package/src/passport/sign.ts +8 -3
- package/src/passport/vitest.config.ts +1 -0
- package/src/pbt/index.test.ts +95 -0
- package/src/pbt/index.ts +251 -0
- package/src/plugin/plugin-contract.ts +1 -1
- package/src/{share → policy}/entitlement.ts +24 -4
- package/src/{share → policy}/feature-policy.ts +8 -3
- package/src/{share → policy}/visibility.ts +7 -2
- package/src/redirects/redirects.ts +117 -0
- package/src/{share → redirects/tests}/redirects.test.ts +1 -1
- package/src/remediation/remediation-catalog.ts +316 -0
- package/src/{share/routes → routes}/template-filter.ts +1 -1
- package/src/{share → runtime}/runtime-context.ts +7 -2
- package/src/{share → runtime}/shared-context.ts +1 -1
- package/src/{share/schemas → schemas}/claims.ts +2 -2
- package/src/{share/schemas → schemas}/effects.ts +6 -1
- package/src/{share/schemas → schemas}/features.ts +1 -1
- package/src/{share/schemas → schemas}/index.ts +3 -3
- package/src/{share/schemas → schemas}/navigation.ts +6 -1
- package/src/{share/schemas → schemas}/page-base.ts +6 -2
- package/src/{share/schemas → schemas}/person.ts +1 -1
- package/src/{share/schemas → schemas}/standard-list-item.ts +1 -1
- package/src/{share → sections}/rfc0042-utils.ts +1 -1
- package/src/{share/semantic → semantic}/block-extractors/index.ts +20 -1
- package/src/{share/semantic → semantic}/business-projection.ts +95 -1
- package/src/semantic/canonical-uri.ts +54 -0
- package/src/{share/semantic → semantic}/extract.ts +100 -1
- package/src/semantic/fact-extraction.test.ts +47 -0
- package/src/semantic/fact-extraction.ts +46 -0
- package/src/semantic/freshness.ts +67 -0
- package/src/{share/semantic → semantic}/ids.ts +6 -1
- package/src/{share/semantic → semantic}/index.ts +9 -2
- package/src/{share/semantic → semantic}/jsonld/breadcrumb.ts +6 -2
- package/src/{share/semantic → semantic}/jsonld/context.ts +6 -2
- package/src/{share/semantic → semantic}/jsonld/faq.ts +6 -2
- package/src/{share/semantic → semantic}/jsonld/initiative.ts +6 -1
- package/src/{share/semantic → semantic}/jsonld/organization.ts +6 -1
- package/src/{share/semantic → semantic}/jsonld/person.ts +6 -2
- package/src/{share/semantic → semantic}/jsonld/service.ts +6 -2
- package/src/semantic/jsonld/shared.ts +36 -0
- package/src/semantic/jsonld/types.ts +24 -0
- package/src/{share/semantic → semantic}/jsonld/webpage.ts +6 -1
- package/src/semantic/jsonld/website.ts +32 -0
- package/src/{share/semantic → semantic}/jsonld.ts +6 -1
- package/src/{share/semantic → semantic}/llms.ts +41 -3
- package/src/{share/semantic → semantic}/models.ts +26 -38
- package/src/{share/semantic → semantic}/organization-profile.ts +3 -0
- package/src/{share/semantic → semantic}/page-builders/markdown-page.ts +6 -3
- package/src/{share/semantic → semantic}/page-utils.ts +6 -3
- package/src/{share/tests/breadcrumbs.test.ts → semantic/tests/breadcrumbs-0229.test.ts} +1 -1
- package/src/{share → semantic}/tests/build-page-price-markers.test.ts +2 -2
- package/src/{share/semantic → semantic}/tests/business-projection.test.ts +179 -0
- package/src/semantic/tests/canonical-uri.test.ts +56 -0
- package/src/{share/semantic → semantic}/tests/extract.test.ts +83 -0
- package/src/semantic/tests/freshness.pbt.test.ts +108 -0
- package/src/{share/tests/image-sitemap.test.ts → semantic/tests/image-sitemap-0150.test.ts} +1 -1
- package/src/{share → semantic}/tests/jsonld-service.test.ts +2 -2
- package/src/{share → semantic}/tests/jsonld-video.test.ts +3 -3
- package/src/{share → semantic}/tests/jsonld-webpage.test.ts +3 -3
- package/src/{share → semantic}/tests/llms-0789.test.ts +2 -2
- package/src/{share/semantic → semantic}/tests/llms.test.ts +46 -0
- package/src/{share → semantic}/tests/organization-jsonld.test.ts +3 -3
- package/src/{share/tests/output-projection.test.ts → semantic/tests/output-projection-0143.test.ts} +1 -1
- package/src/{share/tests/price-marker-resolver.test.ts → semantic/tests/price-marker-resolver-0767.test.ts} +2 -2
- package/src/{share/semantic → semantic}/tests/price-marker-resolver.test.ts +4 -2
- package/src/semantic/tests/project-claims.pbt.test.ts +60 -0
- package/src/{share/semantic → semantic}/tests/split-sentences.test.ts +1 -1
- package/src/{share → semantic}/tests/twin-path.test.ts +1 -1
- package/src/signing/index.ts +13 -0
- package/src/signing/key.ts +139 -0
- package/src/signing/sign.ts +66 -0
- package/src/signing/types.ts +29 -0
- package/src/{share/slug → slug}/slug-url.ts +4 -1
- package/src/stack/run-tool.ts +108 -0
- package/src/stack/stack-checks.ts +124 -0
- package/src/stack/tests/run-tool.test.ts +138 -0
- package/src/stack/tests/stack-checks.test.ts +141 -0
- package/src/stack/tests/walk-files.test.ts +131 -0
- package/src/stack/walk-files.ts +114 -0
- package/src/surface/blueprint.ts +342 -66
- package/src/surface/decision-composer.ts +6 -1
- package/src/surface/geo.ts +6 -1
- package/src/surface/index.ts +11 -7
- package/src/surface/types.ts +2 -2
- package/src/surface/vitest.config.ts +1 -0
- package/src/tests/signing-core.test.ts +129 -0
- package/src/{share → text}/css-value-normalize.ts +6 -1
- package/src/text/string-utils.ts +24 -0
- package/src/{share → text}/tests/text-normalize.test.ts +5 -0
- package/src/{share → text}/text-normalize.ts +4 -3
- package/src/{share → text}/wrap-inline-numbers.ts +1 -1
- package/src/types/axiom-study.d.ts +36 -0
- package/src/types/hls-js-light.d.ts +21 -0
- package/src/types/qrcode-browser.d.ts +23 -0
- package/src/typography/fix.pbt.test.ts +208 -0
- package/src/typography/fix.ts +148 -0
- package/src/typography/index.ts +54 -0
- package/src/typography/locale-defaults.ts +97 -0
- package/src/typography/rules-tier1.test.ts +459 -0
- package/src/typography/rules-tier1.ts +925 -0
- package/src/typography/rules-tier2-locale.pbt.test.ts +190 -0
- package/src/typography/rules-tier2-locale.test.ts +286 -0
- package/src/typography/rules-tier2-locale.ts +399 -0
- package/src/typography/rules-tier2-structure.test.ts +330 -0
- package/src/typography/rules-tier2-structure.ts +378 -0
- package/src/typography/rules-tier3-advisory.test.ts +290 -0
- package/src/typography/rules-tier3-advisory.ts +239 -0
- package/src/typography/text-surface.test.ts +244 -0
- package/src/typography/text-surface.ts +448 -0
- package/src/index.ts +0 -14
- package/src/integration/port-barrel.ts +0 -105
- package/src/share/agent/tests/api-catalog.test.ts +0 -8
- package/src/share/agent/tests/ard-catalog.test.ts +0 -8
- package/src/share/agent/tests/capability.test.ts +0 -14
- package/src/share/agent/tests/fleet-catalog.test.ts +0 -15
- package/src/share/agent/tests/knowledge.test.ts +0 -20
- package/src/share/agent/tests/manifest.test.ts +0 -9
- package/src/share/agent/tests/mcp-card.test.ts +0 -8
- package/src/share/agent/tests/openapi.test.ts +0 -8
- package/src/share/agent/tests/proof.test.ts +0 -15
- package/src/share/content/index.ts +0 -16
- package/src/share/content-discipline/index.ts +0 -14
- package/src/share/index.ts +0 -86
- package/src/share/knowledge/tests/claim.test.ts +0 -11
- package/src/share/knowledge/tests/derivation.test.ts +0 -30
- package/src/share/knowledge/tests/freshness.test.ts +0 -18
- package/src/share/knowledge/tests/ledger.test.ts +0 -35
- package/src/share/knowledge/tests/plan.test.ts +0 -19
- package/src/share/knowledge/tests/source.test.ts +0 -19
- package/src/share/legal/index.ts +0 -1
- package/src/share/redirects.ts +0 -54
- package/src/share/remediation/remediation-catalog.ts +0 -196
- package/src/share/scripts/index.ts +0 -26
- package/src/share/scripts/tests/orchestrator.test.ts +0 -8
- package/src/share/semantic/jsonld/shared.ts +0 -31
- package/src/share/semantic/jsonld/types.ts +0 -19
- package/src/share/semantic/jsonld/website.ts +0 -27
- package/src/share/string-utils.ts +0 -19
- package/src/share/types/axiom-study.d.ts +0 -22
- package/src/share/types/hls-js-light.d.ts +0 -16
- package/src/share/types/qrcode-browser.d.ts +0 -18
- package/src/share/utility-registry.yaml +0 -50
- package/src/share/vitest.config.ts +0 -8
- package/src/surface/blueprint-schema.ts +0 -314
- package/src/surface/blueprint-types.ts +0 -256
- /package/src/{share/agent → agent}/api-catalog.ts +0 -0
- /package/src/{share/agent → agent}/capability.ts +0 -0
- /package/src/{share/agent → agent}/dns-aid.ts +0 -0
- /package/src/{share/agent → agent}/fleet-catalog.ts +0 -0
- /package/src/{share/agent → agent}/index.ts +0 -0
- /package/src/{share/agent → agent}/knowledge.ts +0 -0
- /package/src/{share/agent → agent}/mcp-card.ts +0 -0
- /package/src/{share/agent → agent}/omit-empty.ts +0 -0
- /package/src/{share/agent → agent}/openapi.ts +0 -0
- /package/src/{share/agent → agent}/proof.ts +0 -0
- /package/src/{share/agent → agent}/search.test.ts +0 -0
- /package/src/{share/agent → agent}/tests/omit-empty.test.ts +0 -0
- /package/src/{share → attribution}/attribution-display.ts +0 -0
- /package/src/{share/scripts → client-scripts}/gsap-mountain-journey.ts +0 -0
- /package/src/{share/scripts → client-scripts}/gsap-parallax.ts +0 -0
- /package/src/{share/scripts → client-scripts}/gsap-reveal.ts +0 -0
- /package/src/{share/scripts → client-scripts}/gsap-stagger.ts +0 -0
- /package/src/{share/scripts → client-scripts}/live-photos.ts +0 -0
- /package/src/{share/scripts → client-scripts}/tests/external-link-qr.test.ts +0 -0
- /package/src/{share/scripts → client-scripts}/tests/external-links.test.ts +0 -0
- /package/src/{share/scripts → client-scripts}/tests/gsap-counter.test.ts +0 -0
- /package/src/{share/scripts → client-scripts}/tests/gsap-mountain-journey.test.ts +0 -0
- /package/src/{share/scripts → client-scripts}/tests/gsap-parallax.test.ts +0 -0
- /package/src/{share/scripts → client-scripts}/tests/gsap-reveal.test.ts +0 -0
- /package/src/{share/scripts → client-scripts}/tests/gsap-stagger.test.ts +0 -0
- /package/src/{share/scripts → client-scripts}/tests/inline-number-animation.test.ts +0 -0
- /package/src/{share/scripts → client-scripts}/tests/lenis.test.ts +0 -0
- /package/src/{share/scripts → client-scripts}/tests/live-photos.test.ts +0 -0
- /package/src/{share/scripts → client-scripts}/tests/lordicon.test.ts +0 -0
- /package/src/{share/scripts → client-scripts}/tests/scheduler.test.ts +0 -0
- /package/src/{share/scripts → client-scripts}/video-player.ts +0 -0
- /package/src/{share/content → content}/resolve-field-path.ts +0 -0
- /package/src/{share/content → content}/substitute-deep.ts +0 -0
- /package/src/{share → content}/tests/content-ref-index.test.ts +0 -0
- /package/src/{share/content-discipline → content-discipline}/types.ts +0 -0
- /package/src/{share → image-provider}/image-provider.ts +0 -0
- /package/src/{share/knowledge → knowledge}/claim.ts +0 -0
- /package/src/{share/knowledge → knowledge}/derivation.ts +0 -0
- /package/src/{share/knowledge → knowledge}/freshness.ts +0 -0
- /package/src/{share/knowledge → knowledge}/index.ts +0 -0
- /package/src/{share/knowledge → knowledge}/ledger.ts +0 -0
- /package/src/{share/knowledge → knowledge}/plan.ts +0 -0
- /package/src/{share/knowledge → knowledge}/source.ts +0 -0
- /package/src/{share/legal → legal}/translation-policy.ts +0 -0
- /package/src/{share/middleware → middleware}/language-redirect.ts +0 -0
- /package/src/{share/middleware → middleware}/tests/access-protection.test.ts +0 -0
- /package/src/{share → node}/fs/index.ts +0 -0
- /package/src/{share/tests/markdown-twin-provenance.test.ts → node/tests/markdown-twin-provenance-0613.test.ts} +0 -0
- /package/src/{share → offers}/offer-capacity.ts +0 -0
- /package/src/{share/onboarding-yaml/index.ts → onboarding/yaml.ts} +0 -0
- /package/src/{share → ontology}/tests/ontology-role-by-cosmic-name.test.ts +0 -0
- /package/src/{share/routes → routes}/tests/template-filter.test.ts +0 -0
- /package/src/{share/schemas → schemas}/claim-records.ts +0 -0
- /package/src/{share/schemas → schemas}/horizontal-align.ts +0 -0
- /package/src/{share/schemas → schemas}/icon-color.ts +0 -0
- /package/src/{share/schemas → schemas}/live-photo.ts +0 -0
- /package/src/{share/schemas → schemas}/material-credit.ts +0 -0
- /package/src/{share/schemas → schemas}/media.ts +0 -0
- /package/src/{share/schemas → schemas}/participant.ts +0 -0
- /package/src/{share/schemas → schemas}/print.ts +0 -0
- /package/src/{share/schemas → schemas}/section-background.ts +0 -0
- /package/src/{share/schemas → schemas}/section-body.ts +0 -0
- /package/src/{share/schemas → schemas}/section-cards.ts +0 -0
- /package/src/{share/schemas → schemas}/section-cta.ts +0 -0
- /package/src/{share/schemas → schemas}/section-header.ts +0 -0
- /package/src/{share/schemas → schemas}/section-image.ts +0 -0
- /package/src/{share/schemas → schemas}/section-motion.ts +0 -0
- /package/src/{share/schemas → schemas}/section-shell.ts +0 -0
- /package/src/{share/schemas → schemas}/section-stats.ts +0 -0
- /package/src/{share/schemas → schemas}/site-background.ts +0 -0
- /package/src/{share/schemas → schemas}/tests/claim-records.test.ts +0 -0
- /package/src/{share/schemas → schemas}/tests/claims.test.ts +0 -0
- /package/src/{share/schemas → schemas}/tests/effects.test.ts +0 -0
- /package/src/{share/schemas → schemas}/tests/features.test.ts +0 -0
- /package/src/{share/schemas → schemas}/tests/horizontal-align.test.ts +0 -0
- /package/src/{share/schemas → schemas}/tests/icon-color.test.ts +0 -0
- /package/src/{share/schemas → schemas}/tests/live-photo.test.ts +0 -0
- /package/src/{share/schemas → schemas}/tests/material-credit.test.ts +0 -0
- /package/src/{share/schemas → schemas}/tests/media.test.ts +0 -0
- /package/src/{share/schemas → schemas}/tests/navigation.test.ts +0 -0
- /package/src/{share/schemas → schemas}/tests/page-base.test.ts +0 -0
- /package/src/{share/schemas → schemas}/tests/participant.test.ts +0 -0
- /package/src/{share/schemas → schemas}/tests/person.test.ts +0 -0
- /package/src/{share/schemas → schemas}/tests/print.test.ts +0 -0
- /package/src/{share/schemas → schemas}/tests/section-background.test.ts +0 -0
- /package/src/{share/schemas → schemas}/tests/section-body.test.ts +0 -0
- /package/src/{share/schemas → schemas}/tests/section-cards.test.ts +0 -0
- /package/src/{share/schemas → schemas}/tests/section-cta.test.ts +0 -0
- /package/src/{share/schemas → schemas}/tests/section-header.test.ts +0 -0
- /package/src/{share/schemas → schemas}/tests/section-image.test.ts +0 -0
- /package/src/{share/schemas → schemas}/tests/section-motion.test.ts +0 -0
- /package/src/{share/schemas → schemas}/tests/section-shell.test.ts +0 -0
- /package/src/{share/schemas → schemas}/tests/section-stats.test.ts +0 -0
- /package/src/{share/schemas → schemas}/tests/site-background.test.ts +0 -0
- /package/src/{share/schemas → schemas}/tests/standard-list-item.test.ts +0 -0
- /package/src/{share/semantic → semantic}/ai.ts +0 -0
- /package/src/{share/semantic → semantic}/block-extraction.ts +0 -0
- /package/src/{share/semantic → semantic}/breadcrumbs.ts +0 -0
- /package/src/{share/semantic → semantic}/build-page.ts +0 -0
- /package/src/{share/semantic → semantic}/feed.ts +0 -0
- /package/src/{share/semantic → semantic}/image-sitemap.ts +0 -0
- /package/src/{share/semantic → semantic}/jsonld/article.ts +0 -0
- /package/src/{share/semantic → semantic}/jsonld/collection-list.ts +0 -0
- /package/src/{share/semantic → semantic}/jsonld/tests/article.test.ts +0 -0
- /package/src/{share/semantic → semantic}/jsonld/tests/breadcrumb.test.ts +0 -0
- /package/src/{share/semantic → semantic}/jsonld/tests/collection-list.test.ts +0 -0
- /package/src/{share/semantic → semantic}/jsonld/tests/faq.test.ts +0 -0
- /package/src/{share/semantic → semantic}/jsonld/tests/initiative.test.ts +0 -0
- /package/src/{share/semantic → semantic}/jsonld/tests/organization.test.ts +0 -0
- /package/src/{share/semantic → semantic}/jsonld/tests/person.test.ts +0 -0
- /package/src/{share/semantic → semantic}/jsonld/tests/service.test.ts +0 -0
- /package/src/{share/semantic → semantic}/jsonld/tests/shared.test.ts +0 -0
- /package/src/{share/semantic → semantic}/jsonld/tests/video.test.ts +0 -0
- /package/src/{share/semantic → semantic}/jsonld/tests/webpage.test.ts +0 -0
- /package/src/{share/semantic → semantic}/jsonld/tests/website.test.ts +0 -0
- /package/src/{share/semantic → semantic}/jsonld/video.ts +0 -0
- /package/src/{share/semantic → semantic}/llms-policy.ts +0 -0
- /package/src/{share/semantic → semantic}/markdown-hygiene.ts +0 -0
- /package/src/{share/semantic → semantic}/output-projection.ts +0 -0
- /package/src/{share/semantic → semantic}/page-markdown.ts +0 -0
- /package/src/{share/semantic → semantic}/price-marker-resolver.ts +0 -0
- /package/src/{share/semantic → semantic}/robots.ts +0 -0
- /package/src/{share/semantic → semantic}/tests/ai.test.ts +0 -0
- /package/src/{share/semantic → semantic}/tests/block-extraction.test.ts +0 -0
- /package/src/{share/semantic → semantic}/tests/breadcrumbs.test.ts +0 -0
- /package/src/{share/semantic → semantic}/tests/build-page.test.ts +0 -0
- /package/src/{share/semantic → semantic}/tests/feed.test.ts +0 -0
- /package/src/{share/semantic → semantic}/tests/helpers.ts +0 -0
- /package/src/{share/semantic → semantic}/tests/ids.test.ts +0 -0
- /package/src/{share/semantic → semantic}/tests/image-sitemap.test.ts +0 -0
- /package/src/{share/semantic → semantic}/tests/jsonld.test.ts +0 -0
- /package/src/{share/semantic → semantic}/tests/llms-policy.test.ts +0 -0
- /package/src/{share/semantic → semantic}/tests/markdown-hygiene.test.ts +0 -0
- /package/src/{share/semantic → semantic}/tests/markdown-page.test.ts +0 -0
- /package/src/{share/semantic → semantic}/tests/organization-profile.test.ts +0 -0
- /package/src/{share/semantic → semantic}/tests/output-projection.test.ts +0 -0
- /package/src/{share/semantic → semantic}/tests/page-markdown.test.ts +0 -0
- /package/src/{share/semantic → semantic}/tests/page-utils.test.ts +0 -0
- /package/src/{share/semantic → semantic}/tests/robots.test.ts +0 -0
- /package/src/{share/semantic → semantic}/tests/update-stamp.test.ts +0 -0
- /package/src/{share/semantic → semantic}/update-stamp.ts +0 -0
- /package/src/{share/slug → slug}/heading-slugger.ts +0 -0
- /package/src/{share/slug → slug}/index.ts +0 -0
- /package/src/{share/slug → slug}/slug-id.ts +0 -0
- /package/src/{share/slug → slug}/strategies.ts +0 -0
- /package/src/{share/slug → slug}/tests/slug.test.ts +0 -0
- /package/src/{share → text}/tests/text-normalize.pbt.test.ts +0 -0
- /package/src/{share → text}/text-position.ts +0 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Unit tests for the text-surface extractor (RFC-1068).</purpose>
|
|
4
|
+
</MODULE_CONTRACT>
|
|
5
|
+
<CHANGE_SUMMARY>
|
|
6
|
+
<item>RFC-1068: initial creation.</item>
|
|
7
|
+
</CHANGE_SUMMARY>
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect } from "vitest";
|
|
11
|
+
import { extractTextSurface, deriveFileLocale } from "./text-surface.ts";
|
|
12
|
+
|
|
13
|
+
const LANGS = ["de", "uk", "en"];
|
|
14
|
+
|
|
15
|
+
describe("deriveFileLocale", () => {
|
|
16
|
+
it("derives locale from content path", () => {
|
|
17
|
+
expect(deriveFileLocale("src/content/pages/de/index.md", LANGS, "de")).toBe("de");
|
|
18
|
+
expect(deriveFileLocale("src/content/pages/uk/index.md", LANGS, "de")).toBe("uk");
|
|
19
|
+
expect(deriveFileLocale("src/content/pages/en/index.md", LANGS, "de")).toBe("en");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("derives locale from mission workpiece content path", () => {
|
|
23
|
+
expect(
|
|
24
|
+
deriveFileLocale("missions/wg-m001/workpiece/src/content/pages/de/index.md", LANGS, "de"),
|
|
25
|
+
).toBe("de");
|
|
26
|
+
expect(
|
|
27
|
+
deriveFileLocale(
|
|
28
|
+
"missions/wg-m001/workpiece/src/content/business-profile/uk/offerings/x.md",
|
|
29
|
+
LANGS,
|
|
30
|
+
"de",
|
|
31
|
+
),
|
|
32
|
+
).toBe("uk");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("falls back to default language for unknown locale", () => {
|
|
36
|
+
expect(deriveFileLocale("src/content/pages/fr/index.md", LANGS, "de")).toBe("de");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("falls back to default language for non-content paths", () => {
|
|
40
|
+
expect(deriveFileLocale("src/pages/index.astro", LANGS, "de")).toBe("de");
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("extractTextSurface", () => {
|
|
45
|
+
it("extracts frontmatter strings", () => {
|
|
46
|
+
const source = `---
|
|
47
|
+
title: Hello World
|
|
48
|
+
description: A test page
|
|
49
|
+
---
|
|
50
|
+
Body text here.
|
|
51
|
+
`;
|
|
52
|
+
const result = extractTextSurface("src/content/pages/de/test.md", source, {
|
|
53
|
+
languages: LANGS,
|
|
54
|
+
defaultLanguage: "de",
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
expect(result.yamlError).toBeNull();
|
|
58
|
+
const fmSegments = result.segments.filter((s) => s.source === "frontmatter");
|
|
59
|
+
expect(fmSegments.length).toBeGreaterThanOrEqual(2);
|
|
60
|
+
expect(fmSegments.some((s) => s.text.includes("Hello World"))).toBe(true);
|
|
61
|
+
expect(fmSegments.some((s) => s.text.includes("A test page"))).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("extracts body lines", () => {
|
|
65
|
+
const source = `---
|
|
66
|
+
title: Test
|
|
67
|
+
---
|
|
68
|
+
This is a body paragraph.
|
|
69
|
+
Second line of text.
|
|
70
|
+
`;
|
|
71
|
+
const result = extractTextSurface("src/content/pages/de/test.md", source, {
|
|
72
|
+
languages: LANGS,
|
|
73
|
+
defaultLanguage: "de",
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const bodySegments = result.segments.filter((s) => s.source === "body");
|
|
77
|
+
expect(bodySegments.length).toBe(2);
|
|
78
|
+
expect(bodySegments[0].text).toContain("This is a body paragraph");
|
|
79
|
+
expect(bodySegments[1].text).toContain("Second line of text");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("skips fenced code blocks", () => {
|
|
83
|
+
const source = `---
|
|
84
|
+
title: Test
|
|
85
|
+
---
|
|
86
|
+
Normal text.
|
|
87
|
+
\`\`\`js
|
|
88
|
+
const x = "code block";
|
|
89
|
+
\`\`\`
|
|
90
|
+
After code.
|
|
91
|
+
`;
|
|
92
|
+
const result = extractTextSurface("src/content/pages/de/test.md", source, {
|
|
93
|
+
languages: LANGS,
|
|
94
|
+
defaultLanguage: "de",
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const bodySegments = result.segments.filter((s) => s.source === "body");
|
|
98
|
+
expect(bodySegments.length).toBe(2);
|
|
99
|
+
expect(bodySegments.some((s) => s.text.includes("Normal text"))).toBe(true);
|
|
100
|
+
expect(bodySegments.some((s) => s.text.includes("After code"))).toBe(true);
|
|
101
|
+
expect(bodySegments.some((s) => s.text.includes("code block"))).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("skips technical keys", () => {
|
|
105
|
+
const source = `---
|
|
106
|
+
title: Test
|
|
107
|
+
slug: my-page
|
|
108
|
+
id: abc123
|
|
109
|
+
href: https://example.com
|
|
110
|
+
description: Real text
|
|
111
|
+
---
|
|
112
|
+
Body.
|
|
113
|
+
`;
|
|
114
|
+
const result = extractTextSurface("src/content/pages/de/test.md", source, {
|
|
115
|
+
languages: LANGS,
|
|
116
|
+
defaultLanguage: "de",
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const fmSegments = result.segments.filter((s) => s.source === "frontmatter");
|
|
120
|
+
expect(fmSegments.some((s) => s.text.includes("Real text"))).toBe(true);
|
|
121
|
+
expect(fmSegments.some((s) => s.text.includes("abc123"))).toBe(false);
|
|
122
|
+
expect(fmSegments.some((s) => s.text.includes("my-page"))).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("strips inline code from body lines", () => {
|
|
126
|
+
const source = `---
|
|
127
|
+
title: Test
|
|
128
|
+
---
|
|
129
|
+
Use the \`npm install\` command.
|
|
130
|
+
`;
|
|
131
|
+
const result = extractTextSurface("src/content/pages/de/test.md", source, {
|
|
132
|
+
languages: LANGS,
|
|
133
|
+
defaultLanguage: "de",
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const bodySegments = result.segments.filter((s) => s.source === "body");
|
|
137
|
+
expect(bodySegments.length).toBe(1);
|
|
138
|
+
expect(bodySegments[0].text).toContain("Use the");
|
|
139
|
+
expect(bodySegments[0].text).toContain("command");
|
|
140
|
+
expect(bodySegments[0].text).not.toContain("npm install");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("strips HTML comments from body lines", () => {
|
|
144
|
+
const source = `---
|
|
145
|
+
title: Test
|
|
146
|
+
---
|
|
147
|
+
Text <!-- comment --> here.
|
|
148
|
+
`;
|
|
149
|
+
const result = extractTextSurface("src/content/pages/de/test.md", source, {
|
|
150
|
+
languages: LANGS,
|
|
151
|
+
defaultLanguage: "de",
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const bodySegments = result.segments.filter((s) => s.source === "body");
|
|
155
|
+
expect(bodySegments[0].text).not.toContain("comment");
|
|
156
|
+
expect(bodySegments[0].text).toContain("Text");
|
|
157
|
+
expect(bodySegments[0].text).toContain("here");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("strips URLs from body lines", () => {
|
|
161
|
+
const source = `---
|
|
162
|
+
title: Test
|
|
163
|
+
---
|
|
164
|
+
Visit https://example.com for more.
|
|
165
|
+
`;
|
|
166
|
+
const result = extractTextSurface("src/content/pages/de/test.md", source, {
|
|
167
|
+
languages: LANGS,
|
|
168
|
+
defaultLanguage: "de",
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const bodySegments = result.segments.filter((s) => s.source === "body");
|
|
172
|
+
expect(bodySegments[0].text).not.toContain("https://example.com");
|
|
173
|
+
expect(bodySegments[0].text).toContain("Visit");
|
|
174
|
+
expect(bodySegments[0].text).toContain("for more");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("detects YAML parse errors", () => {
|
|
178
|
+
const source = `---
|
|
179
|
+
title: Test
|
|
180
|
+
foo: [unclosed
|
|
181
|
+
---
|
|
182
|
+
Body.
|
|
183
|
+
`;
|
|
184
|
+
const result = extractTextSurface("src/content/pages/de/test.md", source, {
|
|
185
|
+
languages: LANGS,
|
|
186
|
+
defaultLanguage: "de",
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
expect(result.yamlError).not.toBeNull();
|
|
190
|
+
expect(result.yamlError!.message).toBeTruthy();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("marks table rows correctly", () => {
|
|
194
|
+
const source = `---
|
|
195
|
+
title: Test
|
|
196
|
+
---
|
|
197
|
+
| Col1 | Col2 |
|
|
198
|
+
|------|------|
|
|
199
|
+
| a | b |
|
|
200
|
+
Normal text.
|
|
201
|
+
`;
|
|
202
|
+
const result = extractTextSurface("src/content/pages/de/test.md", source, {
|
|
203
|
+
languages: LANGS,
|
|
204
|
+
defaultLanguage: "de",
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const bodySegments = result.segments.filter((s) => s.source === "body");
|
|
208
|
+
const tableRows = bodySegments.filter((s) => s.isTableRow);
|
|
209
|
+
const normalLines = bodySegments.filter((s) => !s.isTableRow);
|
|
210
|
+
expect(tableRows.length).toBe(3);
|
|
211
|
+
expect(normalLines.length).toBe(1);
|
|
212
|
+
expect(normalLines[0].text).toContain("Normal text");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("handles files without frontmatter", () => {
|
|
216
|
+
const source = "Just body text.\nSecond line.\n";
|
|
217
|
+
const result = extractTextSurface("src/content/pages/de/test.md", source, {
|
|
218
|
+
languages: LANGS,
|
|
219
|
+
defaultLanguage: "de",
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
expect(result.yamlError).toBeNull();
|
|
223
|
+
const bodySegments = result.segments.filter((s) => s.source === "body");
|
|
224
|
+
expect(bodySegments.length).toBe(2);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("skips identifier-like values in frontmatter", () => {
|
|
228
|
+
const source = `---
|
|
229
|
+
title: Test
|
|
230
|
+
path: src/foo/bar.ts
|
|
231
|
+
description: Real text
|
|
232
|
+
---
|
|
233
|
+
Body.
|
|
234
|
+
`;
|
|
235
|
+
const result = extractTextSurface("src/content/pages/de/test.md", source, {
|
|
236
|
+
languages: LANGS,
|
|
237
|
+
defaultLanguage: "de",
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const fmSegments = result.segments.filter((s) => s.source === "frontmatter");
|
|
241
|
+
expect(fmSegments.some((s) => s.text.includes("Real text"))).toBe(true);
|
|
242
|
+
expect(fmSegments.some((s) => s.text.includes("src/foo/bar.ts"))).toBe(false);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Text-surface extractor for the typography rule engine (RFC-1068).
|
|
4
|
+
Extracts scannable text segments from Markdown frontmatter strings and body
|
|
5
|
+
lines, applying a normative stripping order so rules see clean text without
|
|
6
|
+
code blocks, HTML tags, formulas, or URLs. Also catches YAML frontmatter
|
|
7
|
+
parse errors and maps them to a structured yamlError field.</purpose>
|
|
8
|
+
<non-goals>
|
|
9
|
+
<item>Do not implement typography rules — that is rules-tier1.ts.</item>
|
|
10
|
+
<item>Do not read files or perform I/O — the caller passes source text.</item>
|
|
11
|
+
<item>Do not validate content semantics — only extracts text for rule scanning.</item>
|
|
12
|
+
</non-goals>
|
|
13
|
+
</MODULE_CONTRACT>
|
|
14
|
+
<CHANGE_SUMMARY>
|
|
15
|
+
<item>RFC-1068: initial creation.</item>
|
|
16
|
+
</CHANGE_SUMMARY>
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import YAML from "yaml";
|
|
20
|
+
import { parseMarkdownFrontmatter } from "../content/markdown-frontmatter.ts";
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Types
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
export type TextSegmentSource = "frontmatter" | "body";
|
|
27
|
+
|
|
28
|
+
export interface TextSegment {
|
|
29
|
+
/** Repo-relative file path. */
|
|
30
|
+
file: string;
|
|
31
|
+
/** 1-based file line of the segment start (frontmatter offset already applied). */
|
|
32
|
+
line: number;
|
|
33
|
+
/** JSON-path for frontmatter strings (`$.blocks[2].props.items[0].text`), `$body` for body lines. */
|
|
34
|
+
path: string;
|
|
35
|
+
source: TextSegmentSource;
|
|
36
|
+
/** Segment text after stripping; this is what rules see. */
|
|
37
|
+
text: string;
|
|
38
|
+
/** Original text before stripping; used for fix (RFC-1072) and messages. */
|
|
39
|
+
raw: string;
|
|
40
|
+
/** True when the segment is a markdown table row (some rules skip these). */
|
|
41
|
+
isTableRow: boolean;
|
|
42
|
+
locale: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface TextSurfaceOptions {
|
|
46
|
+
/** Keys whose values are never text (default: TECHNICAL_KEYS). */
|
|
47
|
+
skipKeys?: ReadonlySet<string>;
|
|
48
|
+
/** Site i18n languages; used to derive file locale. */
|
|
49
|
+
languages: readonly string[];
|
|
50
|
+
defaultLanguage: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface YamlError {
|
|
54
|
+
line: number;
|
|
55
|
+
column: number;
|
|
56
|
+
message: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ExtractTextSurfaceResult {
|
|
60
|
+
segments: TextSegment[];
|
|
61
|
+
yamlError: YamlError | null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Technical keys — values under these keys are never treated as text
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
export const TECHNICAL_KEYS = new Set([
|
|
69
|
+
"id",
|
|
70
|
+
"slug",
|
|
71
|
+
"href",
|
|
72
|
+
"url",
|
|
73
|
+
"src",
|
|
74
|
+
"path",
|
|
75
|
+
"type",
|
|
76
|
+
"use",
|
|
77
|
+
"kind",
|
|
78
|
+
"variant",
|
|
79
|
+
"layout",
|
|
80
|
+
"anchorId",
|
|
81
|
+
"blockId",
|
|
82
|
+
"ref",
|
|
83
|
+
"key",
|
|
84
|
+
"vendor",
|
|
85
|
+
"collection",
|
|
86
|
+
"lang",
|
|
87
|
+
"locale",
|
|
88
|
+
"currency",
|
|
89
|
+
"email",
|
|
90
|
+
"phone",
|
|
91
|
+
"proseId",
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
// When the parent key is "icon", the child "name" is technical
|
|
95
|
+
const TECHNICAL_PARENT_CHILD: Record<string, Set<string>> = {
|
|
96
|
+
icon: new Set(["name"]),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Identifier heuristic — values without whitespace that contain /, _, or .
|
|
101
|
+
// and no letter-space-letter sequence are treated as identifiers and skipped
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
const IDENTIFIER_REGEX = /^[^\s]+$/;
|
|
105
|
+
const HAS_SLASH_OR_UNDERSCORE_OR_DOT = /[/_\.]/;
|
|
106
|
+
const LETTER_SPACE_LETTER = /\p{L}\s\p{L}/u;
|
|
107
|
+
|
|
108
|
+
function isIdentifierValue(value: string): boolean {
|
|
109
|
+
if (!IDENTIFIER_REGEX.test(value)) return false;
|
|
110
|
+
if (!HAS_SLASH_OR_UNDERSCORE_OR_DOT.test(value)) return false;
|
|
111
|
+
if (LETTER_SPACE_LETTER.test(value)) return false;
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// File locale derivation
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
export function deriveFileLocale(
|
|
120
|
+
file: string,
|
|
121
|
+
languages: readonly string[],
|
|
122
|
+
defaultLanguage: string,
|
|
123
|
+
): string {
|
|
124
|
+
// file is repo-relative: may be src/content/<collection>/<lang>/... or
|
|
125
|
+
// missions/<id>/workpiece/src/content/<collection>/<lang>/...
|
|
126
|
+
const parts = file.split("/");
|
|
127
|
+
const srcIdx = parts.indexOf("src");
|
|
128
|
+
if (srcIdx >= 0 && srcIdx + 3 < parts.length && parts[srcIdx + 1] === "content") {
|
|
129
|
+
const lang = parts[srcIdx + 3];
|
|
130
|
+
if (languages.includes(lang)) return lang;
|
|
131
|
+
}
|
|
132
|
+
return defaultLanguage;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Frontmatter string extraction
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
interface FrontmatterStringEntry {
|
|
140
|
+
path: string;
|
|
141
|
+
value: string;
|
|
142
|
+
/** 1-based line within the frontmatter block (relative to frontmatter start). */
|
|
143
|
+
lineInFrontmatter: number;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function extractFrontmatterStrings(
|
|
147
|
+
data: unknown,
|
|
148
|
+
parentKey: string | null,
|
|
149
|
+
skipKeys: ReadonlySet<string>,
|
|
150
|
+
lineCounter: YAML.LineCounter | null,
|
|
151
|
+
frontmatterText: string,
|
|
152
|
+
): FrontmatterStringEntry[] {
|
|
153
|
+
const results: FrontmatterStringEntry[] = [];
|
|
154
|
+
|
|
155
|
+
function walk(value: unknown, currentPath: string, parent: string | null): void {
|
|
156
|
+
if (typeof value === "string") {
|
|
157
|
+
// Check if this key is technical
|
|
158
|
+
const lastKey = currentPath.split(".").pop() ?? currentPath;
|
|
159
|
+
const isTechnical =
|
|
160
|
+
skipKeys.has(lastKey) ||
|
|
161
|
+
(parent !== null && TECHNICAL_PARENT_CHILD[parent]?.has(lastKey) === true);
|
|
162
|
+
if (isTechnical) return;
|
|
163
|
+
if (isIdentifierValue(value)) return;
|
|
164
|
+
|
|
165
|
+
// Derive line from lineCounter if available
|
|
166
|
+
let lineInFrontmatter = 0;
|
|
167
|
+
if (lineCounter) {
|
|
168
|
+
const range = lineCounter.lineStarts;
|
|
169
|
+
// Find the position of this value in the source — we approximate by
|
|
170
|
+
// searching for the value in the frontmatter text
|
|
171
|
+
const valueIndex = frontmatterText.indexOf(value);
|
|
172
|
+
if (valueIndex >= 0) {
|
|
173
|
+
lineInFrontmatter = range.findIndex((start, i) => {
|
|
174
|
+
const nextStart = range[i + 1] ?? frontmatterText.length + 1;
|
|
175
|
+
return valueIndex >= start && valueIndex < nextStart;
|
|
176
|
+
});
|
|
177
|
+
if (lineInFrontmatter < 0) lineInFrontmatter = 0;
|
|
178
|
+
lineInFrontmatter += 1; // 1-based
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
results.push({ path: currentPath, value, lineInFrontmatter });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (Array.isArray(value)) {
|
|
187
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
188
|
+
walk(value[i], `${currentPath}[${i}]`, parent);
|
|
189
|
+
}
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (value !== null && typeof value === "object") {
|
|
194
|
+
for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
|
|
195
|
+
const newParent = key;
|
|
196
|
+
// Check if this key itself is technical (skip entire subtree)
|
|
197
|
+
if (skipKeys.has(key)) continue;
|
|
198
|
+
if (parent !== null && TECHNICAL_PARENT_CHILD[parent]?.has(key) === true) continue;
|
|
199
|
+
walk(child, currentPath === "$" ? `$.${key}` : `${currentPath}.${key}`, newParent);
|
|
200
|
+
}
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
walk(data, "$", parentKey);
|
|
206
|
+
return results;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
// Body text extraction with normative stripping order
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
// (1) Inline code spans
|
|
214
|
+
const INLINE_CODE = /`[^`]*`/g;
|
|
215
|
+
// (1b) Markdown autolinks <URL> — must be handled before HTML tag stripping
|
|
216
|
+
const AUTOLINK = /<(https?:\/\/[^\s>]+)>/gi;
|
|
217
|
+
// (2) HTML comments
|
|
218
|
+
const HTML_COMMENT = /<!--[\s\S]*?-->/g;
|
|
219
|
+
// (3) Void HTML elements
|
|
220
|
+
const VOID_HTML =
|
|
221
|
+
/<(?:br|hr|img|input|meta|link|source|area|base|col|embed|param|track|wbr)\b[^>]*\/?>/giu;
|
|
222
|
+
// (3) Non-void HTML elements — strip tags, keep text
|
|
223
|
+
const NON_VOID_HTML = /<\/?([a-zA-Z][a-zA-Z0-9]*)\b[^>]*\/?>/gu;
|
|
224
|
+
const VOID_ELEMENTS = new Set([
|
|
225
|
+
"br",
|
|
226
|
+
"hr",
|
|
227
|
+
"img",
|
|
228
|
+
"input",
|
|
229
|
+
"meta",
|
|
230
|
+
"link",
|
|
231
|
+
"source",
|
|
232
|
+
"area",
|
|
233
|
+
"base",
|
|
234
|
+
"col",
|
|
235
|
+
"embed",
|
|
236
|
+
"param",
|
|
237
|
+
"track",
|
|
238
|
+
"wbr",
|
|
239
|
+
]);
|
|
240
|
+
// (4) Formula expressions =(…)
|
|
241
|
+
const FORMULA = /=\([^)]*\)/g;
|
|
242
|
+
// (4b) CMS template expression placeholder (same as rules-tier1.ts CMS_PLACEHOLDER)
|
|
243
|
+
const CMS_PLACEHOLDER = "\uE002";
|
|
244
|
+
// (5) Markdown link/image targets — keep link text, remove ](…)
|
|
245
|
+
const LINK_TARGET = /\]\([^)]*\)/g;
|
|
246
|
+
// (6) URL, e-mail, domain tokens
|
|
247
|
+
const URL_PATTERN = /https?:\/\/[^\s)]+/gi;
|
|
248
|
+
const EMAIL_PATTERN = /[\w.+-]+@[\w.-]+\.\w{2,}/gi;
|
|
249
|
+
const DOMAIN_PATTERN = /\b(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}\b/gi;
|
|
250
|
+
|
|
251
|
+
function stripBodyLine(line: string): string {
|
|
252
|
+
let result = line;
|
|
253
|
+
|
|
254
|
+
// (1) Remove fenced code blocks (they span multiple lines, but for per-line
|
|
255
|
+
// processing we rely on the caller to skip fenced blocks entirely)
|
|
256
|
+
result = result.replace(INLINE_CODE, CMS_PLACEHOLDER);
|
|
257
|
+
|
|
258
|
+
// (1b) Replace markdown autolinks <URL> with URL placeholder
|
|
259
|
+
result = result.replace(AUTOLINK, "\uE001");
|
|
260
|
+
|
|
261
|
+
// (2) Remove HTML comments
|
|
262
|
+
result = result.replace(HTML_COMMENT, "");
|
|
263
|
+
|
|
264
|
+
// (3) Strip inline HTML tags
|
|
265
|
+
result = result.replace(VOID_HTML, "");
|
|
266
|
+
result = result.replace(NON_VOID_HTML, (match, tagName) => {
|
|
267
|
+
if (VOID_ELEMENTS.has(String(tagName).toLowerCase())) return "";
|
|
268
|
+
// Non-void: keep text content (the match is just the tag, not content)
|
|
269
|
+
return "";
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// (4) Replace formulas with placeholder
|
|
273
|
+
result = result.replace(FORMULA, "\uE000");
|
|
274
|
+
|
|
275
|
+
// (4b) Replace CMS template expressions ({price:...}, {t:...}, etc.) with placeholder
|
|
276
|
+
result = result.replace(/\{[^}]*\}/g, CMS_PLACEHOLDER);
|
|
277
|
+
|
|
278
|
+
// (4c) Replace CMS template expressions (=(path.to.value), =(path/to/value)) with placeholder
|
|
279
|
+
result = result.replace(/=\([^)]*\)/g, "\uE002");
|
|
280
|
+
|
|
281
|
+
// (5) Remove markdown link targets, keep link text
|
|
282
|
+
result = result.replace(LINK_TARGET, "");
|
|
283
|
+
|
|
284
|
+
// (6) Replace URLs, e-mails, domains with placeholder
|
|
285
|
+
result = result.replace(URL_PATTERN, "\uE001");
|
|
286
|
+
result = result.replace(EMAIL_PATTERN, "\uE001");
|
|
287
|
+
result = result.replace(DOMAIN_PATTERN, "\uE001");
|
|
288
|
+
|
|
289
|
+
return result;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
// Main extractor
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
export function extractTextSurface(
|
|
297
|
+
file: string,
|
|
298
|
+
source: string,
|
|
299
|
+
options: TextSurfaceOptions,
|
|
300
|
+
): ExtractTextSurfaceResult {
|
|
301
|
+
const skipKeys = options.skipKeys ?? TECHNICAL_KEYS;
|
|
302
|
+
const locale = deriveFileLocale(file, options.languages, options.defaultLanguage);
|
|
303
|
+
|
|
304
|
+
// Parse frontmatter — catch YAML errors
|
|
305
|
+
// Strip BOM if present so the frontmatter regex matches
|
|
306
|
+
const bomStripped = source.replace(/^\uFEFF/, "");
|
|
307
|
+
const frontmatterMatch = bomStripped.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
308
|
+
const frontmatterText = frontmatterMatch?.[1] ?? "";
|
|
309
|
+
const frontmatterLineCount = frontmatterMatch ? frontmatterMatch[0].split(/\r?\n/).length - 1 : 0;
|
|
310
|
+
|
|
311
|
+
let data: Record<string, unknown> = {};
|
|
312
|
+
let yamlError: YamlError | null = null;
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
const parsed = parseMarkdownFrontmatter(source);
|
|
316
|
+
data = parsed.data;
|
|
317
|
+
} catch (err) {
|
|
318
|
+
// YAML parse error — extract line/column from YAMLParseError
|
|
319
|
+
if (err instanceof YAML.YAMLParseError) {
|
|
320
|
+
const linePos = (err as unknown as { linePos?: Array<{ line: number; col: number }> })
|
|
321
|
+
.linePos;
|
|
322
|
+
const line = linePos?.[0]?.line ?? 0;
|
|
323
|
+
const column = linePos?.[0]?.col ?? 0;
|
|
324
|
+
yamlError = { line, column, message: err.message };
|
|
325
|
+
} else {
|
|
326
|
+
yamlError = { line: 0, column: 0, message: err instanceof Error ? err.message : String(err) };
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const segments: TextSegment[] = [];
|
|
331
|
+
|
|
332
|
+
// Extract frontmatter strings
|
|
333
|
+
if (Object.keys(data).length > 0 && !yamlError) {
|
|
334
|
+
let lineCounter: YAML.LineCounter | null = null;
|
|
335
|
+
try {
|
|
336
|
+
lineCounter = new YAML.LineCounter();
|
|
337
|
+
YAML.parseDocument(frontmatterText, { lineCounter });
|
|
338
|
+
} catch {
|
|
339
|
+
lineCounter = null;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const fmStrings = extractFrontmatterStrings(data, null, skipKeys, lineCounter, frontmatterText);
|
|
343
|
+
|
|
344
|
+
for (const entry of fmStrings) {
|
|
345
|
+
const stripped = stripBodyLine(entry.value);
|
|
346
|
+
if (stripped.trim().length === 0) continue;
|
|
347
|
+
segments.push({
|
|
348
|
+
file,
|
|
349
|
+
line: entry.lineInFrontmatter + 1, // +1 because frontmatter starts at line 2 (after ---)
|
|
350
|
+
path: entry.path,
|
|
351
|
+
source: "frontmatter",
|
|
352
|
+
text: stripped,
|
|
353
|
+
raw: entry.value,
|
|
354
|
+
isTableRow: false,
|
|
355
|
+
locale,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Extract body lines
|
|
361
|
+
const body = frontmatterMatch ? bomStripped.slice(frontmatterMatch[0].length) : bomStripped;
|
|
362
|
+
const bodyLines = body.split(/\r?\n/);
|
|
363
|
+
const bodyLineOffset = frontmatterLineCount;
|
|
364
|
+
|
|
365
|
+
let inFencedBlock = false;
|
|
366
|
+
let inHtmlComment = false;
|
|
367
|
+
|
|
368
|
+
for (let i = 0; i < bodyLines.length; i += 1) {
|
|
369
|
+
const rawLine = bodyLines[i];
|
|
370
|
+
const lineNum = i + 1 + bodyLineOffset;
|
|
371
|
+
|
|
372
|
+
// Track fenced code blocks
|
|
373
|
+
if (/^```/.test(rawLine.trim())) {
|
|
374
|
+
inFencedBlock = !inFencedBlock;
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
if (inFencedBlock) continue;
|
|
378
|
+
|
|
379
|
+
// Track multi-line HTML comments
|
|
380
|
+
if (inHtmlComment) {
|
|
381
|
+
if (rawLine.includes("-->")) {
|
|
382
|
+
inHtmlComment = false;
|
|
383
|
+
// Strip everything up to and including -->, keep the rest
|
|
384
|
+
const after = rawLine.slice(rawLine.indexOf("-->") + 3);
|
|
385
|
+
if (after.trim().length === 0) continue;
|
|
386
|
+
const strippedAfter = stripBodyLine(after);
|
|
387
|
+
if (strippedAfter.trim().length === 0) continue;
|
|
388
|
+
segments.push({
|
|
389
|
+
file,
|
|
390
|
+
line: lineNum,
|
|
391
|
+
path: "$body",
|
|
392
|
+
source: "body",
|
|
393
|
+
text: strippedAfter,
|
|
394
|
+
raw: after,
|
|
395
|
+
isTableRow: /^\|.*\|/.test(after.trim()),
|
|
396
|
+
locale,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
// Check if line opens a multi-line HTML comment (<!-- without --> on same line)
|
|
402
|
+
const openIdx = rawLine.indexOf("<!--");
|
|
403
|
+
if (openIdx !== -1) {
|
|
404
|
+
const closeIdx = rawLine.indexOf("-->", openIdx);
|
|
405
|
+
if (closeIdx === -1) {
|
|
406
|
+
// Comment opens but doesn't close on this line — keep content before <!--
|
|
407
|
+
const before = rawLine.slice(0, openIdx);
|
|
408
|
+
inHtmlComment = true;
|
|
409
|
+
if (before.trim().length === 0) continue;
|
|
410
|
+
const strippedBefore = stripBodyLine(before);
|
|
411
|
+
if (strippedBefore.trim().length === 0) continue;
|
|
412
|
+
segments.push({
|
|
413
|
+
file,
|
|
414
|
+
line: lineNum,
|
|
415
|
+
path: "$body",
|
|
416
|
+
source: "body",
|
|
417
|
+
text: strippedBefore,
|
|
418
|
+
raw: before,
|
|
419
|
+
isTableRow: /^\|.*\|/.test(before.trim()),
|
|
420
|
+
locale,
|
|
421
|
+
});
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
// Both on same line — stripBodyLine handles it via regex
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Skip empty lines
|
|
428
|
+
if (rawLine.trim().length === 0) continue;
|
|
429
|
+
|
|
430
|
+
const isTableRow = /^\|.*\|/.test(rawLine.trim());
|
|
431
|
+
const stripped = stripBodyLine(rawLine);
|
|
432
|
+
|
|
433
|
+
if (stripped.trim().length === 0) continue;
|
|
434
|
+
|
|
435
|
+
segments.push({
|
|
436
|
+
file,
|
|
437
|
+
line: lineNum,
|
|
438
|
+
path: "$body",
|
|
439
|
+
source: "body",
|
|
440
|
+
text: stripped,
|
|
441
|
+
raw: rawLine,
|
|
442
|
+
isTableRow,
|
|
443
|
+
locale,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return { segments, yamlError };
|
|
448
|
+
}
|
package/src/index.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
<MODULE_CONTRACT>
|
|
3
|
-
<purpose>Package barrel for @warpgogol/werkstatt-shared — stack-agnostic shared infrastructure extracted from werkstatt-site (RFC-0868).</purpose>
|
|
4
|
-
<non-goals>
|
|
5
|
-
<item>Do not export site-specific validators, Astro components, or stack plugin logic.</item>
|
|
6
|
-
<item>Do not import from @warpgogol/werkstatt-site or any stack plugin.</item>
|
|
7
|
-
</non-goals>
|
|
8
|
-
</MODULE_CONTRACT>
|
|
9
|
-
<CHANGE_SUMMARY>
|
|
10
|
-
<item>RFC-0868: initial extraction from werkstatt-site.</item>
|
|
11
|
-
</CHANGE_SUMMARY>
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
export * from "./checks/index.ts";
|