/*
[RFC-0192] Closed contracts for the Programmatic Surface route-source port: the axis/record
model, the eligibility policy, the materialized virtual route entry, and the PageSurfaceProvider
interface. Framework-free — no Astro, no Node I/O. These types are the seam shared by the
build-time generator (kernel command) and the runtime registry merge.
- Do not read content or the filesystem — all I/O lives in the kernel command wrapper.
- Do not define the Blueprint schema (see blueprint.ts) or substance/budget/freshness (later RFCs).
- RFC-0192: initial contracts.
- RFC-0207: SurfaceNarrative (bespoke per-page hero/section prose) + SurfaceRecordImage (per-record image token + alt).
- warpgogol-m000185: VirtualRouteEntry.noindexLocales — per-locale index suppression on generated surface entries.
*/
/**
* RFC-0207: the bespoke per-page narrative that replaces template-glued headings for an indexable
* page. Produced once per live tuple (or per record) by surface.enrich, stored frozen + approved as
* a content entry, and read by the baker. The h1/lead/tagline land in the hero; bridges become
* connective section prose between the field-driven blocks. Transcreated natively per language.
*/
export interface SurfaceNarrative {
/** Grammatically complete page H1 (not a token-glued title). */
h1: string;
/** Hero lead — distinct from any record field and from the tagline. */
lead: string;
/** Hero tagline — distinct from the lead. */
tagline?: string;
/** Connective section prose woven between the field-driven blocks. */
bridges?: Array<{ heading: string; body: string }>;
}
/**
* RFC-0207: a surface dataset record's image. `image` is a content-asset token resolved exactly like
* a hero `leadImage.src` / `portraitImage` (bare filename under the app's content assets, RFC-0167);
* `imageAlt` is the meaningful alt text (per-language record carries the per-language alt). Mixed into
* SurfaceRecord so the baker can read it without a separate load.
*/
export interface SurfaceRecordImage {
image?: string;
imageAlt?: string;
}
/**
* RFC-0192: a block in a baked page. Structurally compatible with `@warpgogol/werkstatt-shared/page` `BlockEntry`, but
* declared here so `@warpgogol/werkstatt-shared/surface` stays the lower-level package with no dependency on share
* (avoids a workspace cycle — share consumes surface's route types).
*/
export interface SurfaceBlock {
id?: string;
/** CMS-facing archetype slug (e.g. "hero", "markdown", "final-cta"). */
type?: string;
/** PlanetName/MoonName (normalized from `type` for internal resolution). */
use?: string;
props: Record;
layer?: "shell" | "section";
}
/** RFC-0192: a baked block-declarative page. Structurally compatible with `@warpgogol/werkstatt-shared/page` `PageEntry`. */
export interface PageEntry {
kind: "page";
cosmicStar: string;
title: string;
description: string;
lang: string;
blocks: SurfaceBlock[];
}
/** A single combinatorial axis and the closed universe of value slugs it ranges over. */
export interface SurfaceAxis {
id: string;
/** Value slugs, sourced from a content-source collection by the generator. */
universe: readonly string[];
}
/** A partial assignment of axis ids → chosen value (undefined ⇒ axis not yet constrained). */
export type AxisTuple = Record;
/**
* RFC-0199: a value's slug, optionally localized per language. The `neutral` slug is the
* language-neutral identity key (filename stem / `slug` field) and the fallback; `byLang`
* supplies a per-language URL segment. Languages absent from `byLang` fall back to `neutral`.
*/
export interface LocalizedSlug {
neutral: string;
byLang?: Record;
}
/**
* RFC-0199: per-axis localized slug map — axisId → (neutral value slug → its LocalizedSlug).
* An empty/absent universe makes slug rendering fall back to the neutral tuple value, so the
* generator output is byte-identical to pre-RFC-0199 behavior when no localized slugs exist.
*/
export type LocalizedUniverse = Record>;
/**
* A dataset row projected into axis-addressable fields. Each axis declares which record field
* carries its membership (a value or an array of values). `status` gates live vs archived rows.
*/
export type SurfaceRecord = Record & {
slug?: string;
status?: "active" | "archived";
};
export type RedirectPolicy = "nearest-ancestor" | "root";
/**
* The eligibility policy: how many records a combination needs to go live at each depth, when a
* live page is demoted to noindex, what empty combinations redirect to, and URL-segment rules.
* Axis-generic generalization of the legacy foerderung policy.
*/
export interface EligibilityPolicy {
/** depth → minimum active records required to emit a live page. */
minRecordsPerDepth: Readonly>;
/** depth → live-but-`noindex` threshold (live page tagged noindex below this count). */
noindexBelowPerDepth: Readonly>;
redirectPolicy: RedirectPolicy;
trailingSlash: boolean;
/** Allowed URL-segment shape (single lowercase dash segment by default). */
segmentPattern: RegExp;
}
/**
* The reason a generated page is not indexable. Extended by later RFCs (substance/budget/freshness);
* RFC-0192 emits only the record-count reasons.
*/
export type IndexReason =
| "too-few-records"
| "thin" // RFC-0194
| "over-budget" // RFC-0196
| "decayed" // RFC-0196
| "regional-gated" // RFC-0240
| "invalid-freshness" // RFC-0274
| "missing-evidence" // RFC-0274
| "missing-demand" // RFC-0280
| "missing-werk-evidence" // RFC-0281
| "navigation-noindex" // RFC-0324
| "redirect-stub"
| "duplicate"; // RFC-0274
/** The composed indexability decision for a generated page. Later RFCs fill the optional gates. */
export interface IndexDecision {
recordGate: boolean;
substanceGate?: boolean;
evidenceGate?: boolean;
demandGate?: boolean;
duplicateGate?: boolean;
withinBudget?: boolean;
fresh?: boolean;
indexable: boolean;
noindex: boolean;
reason?: IndexReason;
/** RFC-0194: the page substance score (0..100) that informed the substance gate. */
substanceScore?: number;
/** RFC-0281: count of consented Werk records backing this tuple. */
werkEvidenceCount?: number;
}
/** One materialized generated route. The single unit the registry merge consumes. */
export interface VirtualRouteEntry {
/** Owning Blueprint id (e.g. "website-local"). */
surfaceId: string;
/** Synthetic stable page id, e.g. "website-local:elektriker:berlin". */
pageId: string;
/** lang → localized slug. */
routes: Record;
axes: AxisTuple;
depth: number;
recordCount: number;
indexable: boolean;
noindex: boolean;
/** For non-live (redirect-stub) entries: the live ancestor pageId to 301 to. */
redirectToPageId?: string;
/** For live noindex navigation pages: pageId whose URL should be rendered as canonical. */
canonicalPageId?: string;
/** Semantic page type for JSON-LD / OG meta (defaults to "content"). */
semanticType?: string;
/**
* RFC-0325: dated-editorial article metadata, present when `semanticType` is "article". Mirrors
* the system.md `pages[].article` shape exactly so the RFC-0317 update-stamp resolver
* (`resolveAuthoredStamp`) and the Astro render layer's Article/BlogPosting JSON-LD wiring work
* for a Programmatic Surface page without any source-specific branching.
*/
article?: {
publishedAt: string;
updatedAt?: string;
author?: string;
tags?: string[];
};
/** RFC-0195: per-depth GEO contribution — "full" (llms.txt + twin), "twin-only", "off". */
geo?: "full" | "twin-only" | "off";
/**
* Optional page-level site-background image name (resolved via the app's content assets, same as
* an authored page's `shell.background` imageName in system.md). When set, the page handler renders
* a shell-layer background instead of the generic `home-bg` default. Omit for the default.
*/
backgroundImage?: string;
/**
* RFC-0192: the resolved page. In "inline" bake mode it carries the full blocks. In "lazy" mode it
* carries only metadata (title/description, empty blocks) and the full page lives in its own file
* (`.surface-cache/.yaml`), loaded on demand at render.
*/
page?: PageEntry;
/**
* RFC-0193: per-language baked pages (multilingual surfaces). Keyed by lang. `page` mirrors the
* default-language entry here. Language-neutral consumers (substance, counts) use `page`; per-lang
* consumers (render, Markdown twins, llms.txt) use `pages[lang] ?? page`.
*/
pages?: Record;
/** RFC-0192: true when the full page blocks live in a per-page file (lazy bake). */
lazy?: boolean;
/**
* RFC-0207: non-default languages whose localized page was omitted because its baked content fell
* back to the default language (no native record fields and no approved narrative). Their `routes`
* + `pages` entries are dropped, so the de-fallback page never renders under a localized URL, is
* never indexed, and never enters the sitemap/hreflang/twins. surface.validate reports each as an
* `untranslated-route` error. Reversible: supply native content (or an approved narrative) and the
* route reappears on the next generate.
*/
untranslatedLangs?: string[];
/**
* warpgogol-m000185: locales whose rendered page stays live but carries robots
* noindex and drops out of sitemap/llms (e.g. operator-language copies of
* generated city/service pages). Set by surface.generate from the owning
* module context's `noindexLocales`; the tuple-level `indexable`/`noindex`
* stay untouched so sibling locales remain indexable.
*/
noindexLocales?: string[];
/** Decision provenance for the manifest (RFC-0194/0196 enrich this). */
decision?: IndexDecision;
}
/**
* The closed route-source port. One implementation per Blueprint (RFC-0193).
* `enumerate` runs at build time (Node); `resolve` may run at build time (baked) or render time.
*/
export interface PageSurfaceProvider {
readonly id: string;
enumerate(lang: string): Promise;
resolve(entry: VirtualRouteEntry, lang: string): Promise;
}
/** Per-surface generation counts (written into pseo-manifest.json). */
export interface SurfaceCounts {
surfaceId: string;
generated: number;
indexable: number;
noindex: number;
redirected: number;
/** RFC-0194: live pages suppressed by the substance gate. */
thin?: number;
/** RFC-0194: median Page Substance Score across live pages. */
medianSubstance?: number;
/** RFC-0324: legacy composite score retained for diagnostics while medianSubstance is unique-share. */
legacyMedianSubstanceScore?: number;
/** RFC-0324: distribution of unique token share across non-redirect generated pages. */
uniqueTokenShareBands?: {
lt030: number;
gte030lt050: number;
gte050: number;
};
/** RFC-0195: Markdown twins emitted (GEO corpus). */
twins?: number;
/** RFC-0195: pages contributing to llms.txt (GEO depth "full"). */
inLlms?: number;
}
/** The pseo-manifest.json payload. */
export interface SurfaceManifest {
generatedAt: string | null;
surfaces: SurfaceCounts[];
}
/** The src/surface.generated.yaml payload consumed by the runtime registry merge. */
export interface SurfaceArtifact {
generatedAt: string | null;
entries: VirtualRouteEntry[];
}
/** RFC-0492: structured dossier fields for depth-1 industry records. */
export interface IndustryDossierFields {
customerQuestions?: string[];
customerJourneys?: Array<{ title: string; stages: string[] }>;
serviceTaxonomy?: Array<{ category: string; description: string }>;
trustSignals?: string[];
evidenceRequirements?: string[];
contactModes?: string[];
serviceAreaModel?: string;
recommendedArchitecture?: Array<{ page: string; description: string }>;
suitableModules?: Array<{ module: string; applicability: string }>;
notdienst?: boolean;
industryFaq?: Array<{ question: string; answer: string }>;
}