From 1c473d13f844f980dddb85d57002319796210957 Mon Sep 17 00:00:00 2001 From: Bartek Kus <7887446+bartekus@users.noreply.github.com> Date: Tue, 14 Jul 2026 21:52:17 -0600 Subject: [PATCH] feat(002): launch content, docs stubs, registry-derived status ladder Replace the spec-001 placeholders with the real launch copy and docs seed for stagecraft.ing (spec 002-launch-content). - Index: hero, the Specify/Stamp/Operate/Verify loop, the product-family list, a for-agents block, and a live-registry pointer. Every capability claim carries a maturity chip and a receipt link resolved at build time from the spec that governs it, so the copy cannot read louder than the source. - Status ladder (M1-M5): rolled up from the baked registry, not hardcoded. At build time M1 is shipped (the EnRaHiTu chassis), M2 in progress (the template contract; the born-with certificate is still pending, so it is not claimed proven), M3-M5 planned. - Docs: an index over three stubs (What is EnRaHiTu, The template contract, Self-hosting the control plane), each summarizing and linking its source. - Fix spec 002 establishes (Astro-era src/content to real app/ paths) and mark it complete; regenerate the derived shards. app/routes.ts and react-router.config.ts are spec-001 scaffold files, edited only to register and prerender the new docs route; that coupling drift is waived in the PR body rather than by re-authoring complete spec 001. --- .../by-spec/002-launch-content.json | 97 +++- .../by-spec/002-launch-content.json | 28 +- app/lib/docs.ts | 140 ++++++ app/lib/milestones.ts | 199 ++++++++ app/routes.ts | 1 + app/routes/_index.tsx | 469 +++++++++++++++--- app/routes/docs.$slug.tsx | 126 +++++ app/routes/docs.tsx | 87 +++- react-router.config.ts | 15 +- specs/002-launch-content/spec.md | 81 ++- 10 files changed, 1123 insertions(+), 120 deletions(-) create mode 100644 app/lib/docs.ts create mode 100644 app/lib/milestones.ts create mode 100644 app/routes/docs.$slug.tsx diff --git a/.derived/codebase-index/by-spec/002-launch-content.json b/.derived/codebase-index/by-spec/002-launch-content.json index 689277e..1194314 100644 --- a/.derived/codebase-index/by-spec/002-launch-content.json +++ b/.derived/codebase-index/by-spec/002-launch-content.json @@ -1,27 +1,94 @@ { - "diagnostics": { - "errors": [], - "warnings": [ - { - "code": "W-001", - "message": "spec '002-launch-content' directory unit 'src/content/' is not a directory", - "path": "src/content/" - } - ] - }, "mapping": { "dependsOn": [ "001-site-scaffold" ], - "implementingPaths": [], + "implementingPaths": [ + { + "path": "app/lib/docs.ts", + "source": "spec-edge" + }, + { + "path": "app/lib/milestones.ts", + "source": "spec-edge" + }, + { + "path": "app/routes/_index.tsx", + "source": "spec-edge" + }, + { + "path": "app/routes/docs.$slug.tsx", + "source": "spec-edge" + }, + { + "path": "app/routes/docs.tsx", + "source": "spec-edge" + } + ], "resolvedUnits": [ { - "locations": [], + "locations": [ + { + "file": "app/lib/docs.ts" + } + ], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "app/lib/docs.ts" + } + }, + { + "locations": [ + { + "file": "app/lib/milestones.ts" + } + ], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "app/lib/milestones.ts" + } + }, + { + "locations": [ + { + "file": "app/routes/_index.tsx" + } + ], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "app/routes/_index.tsx" + } + }, + { + "locations": [ + { + "file": "app/routes/docs.$slug.tsx" + } + ], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "app/routes/docs.$slug.tsx" + } + }, + { + "locations": [ + { + "file": "app/routes/docs.tsx" + } + ], "ownership": true, "sourceField": "establishes", "unit": { - "kind": "directory", - "path": "src/content/" + "kind": "file", + "path": "app/routes/docs.tsx" } } ], @@ -29,5 +96,5 @@ "specStatus": "approved" }, "schemaVersion": "1.1.0", - "shardHash": "9f37ede0e6a8719a1c8e17b9f2840017f6e269de2624356307fcaf5b94153dd5" + "shardHash": "b89aa769c2379525b9536a49fe574bfcb8244e05beca67bf76c014551f112e07" } diff --git a/.derived/spec-registry/by-spec/002-launch-content.json b/.derived/spec-registry/by-spec/002-launch-content.json index 218d79c..3bdce1f 100644 --- a/.derived/spec-registry/by-spec/002-launch-content.json +++ b/.derived/spec-registry/by-spec/002-launch-content.json @@ -6,25 +6,43 @@ ], "establishes": [ { - "kind": "directory", - "path": "src/content/" + "kind": "file", + "path": "app/routes/_index.tsx" + }, + { + "kind": "file", + "path": "app/routes/docs.tsx" + }, + { + "kind": "file", + "path": "app/routes/docs.$slug.tsx" + }, + { + "kind": "file", + "path": "app/lib/docs.ts" + }, + { + "kind": "file", + "path": "app/lib/milestones.ts" } ], "id": "002-launch-content", - "implementation": "pending", + "implementation": "complete", "sectionHeadings": [ "002: Launch content", + "0. Implementation amendment (2026-07-14)", "1. Voice constraints", "2. Content inventory (index page)", "3. Docs seed", "4. Acceptance", - "5. Out of scope" + "5. Out of scope", + "6. Status (2026-07-14): complete" ], "specPath": "specs/002-launch-content/spec.md", "status": "approved", "summary": "The words on the site at launch. One index page that states what Stagecraft is in the builder's own register (creator-led, OSS-credible, no startup theater), a product-family section mapping the four repos and their licenses, and an honest status section tied to the public milestone ladder. The positioning facts are inlined here so the implementing session needs no external archive.\n", "title": "Launch content: positioning, product family, honest status" }, - "shardHash": "0de10d1c4212ebaf86dc5f9a74818367313a81e203dccb2fdba400bf12a32bd4", + "shardHash": "7b9884fbd42e2bae5bd569294aeb0c596cbb29e0efa1290cd3777ece462cd6b3", "specVersion": "1.1.0" } diff --git a/app/lib/docs.ts b/app/lib/docs.ts new file mode 100644 index 0000000..15ed027 --- /dev/null +++ b/app/lib/docs.ts @@ -0,0 +1,140 @@ +// Docs seed (spec 002 §3). Three stubs sourced from the repos' own READMEs and +// specs: link and summarize, never fork prose. This module imports nothing so +// it is safe to pull into both the docs routes and the prerender config +// (react-router.config.ts). Repo metadata (license, url) is resolved from +// product-family.ts in the route, keyed by `repo`, so it is never duplicated +// here. Every claim is checkable against the linked spec or repo. + +/** A link on a docs stub. An `href` beginning with "/" is an in-site route + * (rendered with the router Link); anything else opens in a new tab. */ +export interface DocLink { + label: string; + href: string; +} + +export interface DocSection { + heading: string; + /** Paragraphs, rendered in order. */ + body: string[]; +} + +export interface DocStub { + slug: string; + title: string; + /** One-line summary shown on the docs index and under the title. */ + summary: string; + /** Repo key into product-family.ts (for the license chip + repo link). */ + repo: string; + /** Honest maturity marker: what you can run today vs. what is on the ladder. */ + maturity: "shipping" | "in-progress" | "planned"; + sections: DocSection[]; + links: DocLink[]; +} + +export const DOC_STUBS: DocStub[] = [ + { + slug: "what-is-enrahitu", + title: "What is EnRaHiTu", + summary: + "The single-container application chassis every stamped app is built from.", + repo: "enrahitu", + maturity: "shipping", + sections: [ + { + heading: "One container, zero managed dependencies", + body: [ + "EnRaHiTu is an Encore.ts application core that carries its own datastore, its own identity provider, and its own web tier in a single container image. The name is the stack: Encore.ts, rauthy, hiqlite, and Turso.", + "hiqlite runs in-process as a napi-rs native addon (a Raft-replicated SQLite), so there is no database process to operate alongside the app. CoreLedger is a decorator data layer over libSQL/Turso. Auth is rauthy behind a same-origin proxy with an OIDC driver, and the app serves its own SPA. The Encore toolchain is vendored (Rust core plus JS runtime via napi-rs), so there is no external CLI in the build.", + ], + }, + { + heading: "Why it exists", + body: [ + "The chassis is the substrate the rest of the family stands on. Every app the control plane stamps is an EnRaHiTu app, which is what makes operating a fleet of them uniform: one container and one volume each, the same shape every time.", + ], + }, + ], + links: [ + { label: "enrahitu on GitHub", href: "https://github.com/stagecraft-ing/enrahitu" }, + { + label: "001: architecture spec", + href: "/registry/enrahitu/001-enrahitu-architecture", + }, + { + label: "007: single-container packaging", + href: "/registry/enrahitu/007-single-container-packaging", + }, + ], + }, + { + slug: "the-template-contract", + title: "The template contract", + summary: + "A versioned template.toml that binds what a stamped app is, and the agentic posture it is born with.", + repo: "enrahitu", + maturity: "in-progress", + sections: [ + { + heading: "template.toml", + body: [ + "A stamp is a function of a contract version. The template contract (template.toml) is the versioned declaration of what a stamped app is: its shape, its verbs, and what may change under a governed upgrade. Pinning the version is what lets a chassis upgrade later run as a governed verb instead of a manual migration.", + ], + }, + { + heading: "Born-with provenance", + body: [ + "At stamp time the app receives a born-with certificate that binds an explicit agentic posture. An agent that later operates the app runs under a declared posture, recorded from the moment the app exists, rather than one inferred after the fact.", + "This rung is still being built: the contract itself is in progress and the born-with certificate is specced but not yet implemented. The specs below carry the current state; the status ladder on the home page reads it live from the registry.", + ], + }, + ], + links: [ + { + label: "009: the template contract", + href: "/registry/enrahitu/009-template-contract", + }, + { + label: "012: born-with provenance", + href: "/registry/enrahitu/012-born-with-provenance", + }, + ], + }, + { + slug: "self-hosting-the-control-plane", + title: "Self-hosting the control plane (AGPL)", + summary: + "What AGPL-3.0 means for the plane, what you can run today, and what is on the ladder.", + repo: "stagecraft", + maturity: "planned", + sections: [ + { + heading: "AGPL-3.0, and what it covers", + body: [ + "The control plane (stagecraft) is AGPL-3.0: you can run it and modify it, and network users are entitled to the source of your running version. The AGPL covers the plane, not what the plane stamps. Apps the factory stamps are the customer's own code, in the customer's own GitHub org, under whatever license the customer chooses.", + ], + }, + { + heading: "What runs today, and what is planned", + body: [ + "You can self-host the EnRaHiTu chassis now: it is a single container, and its specs are shipped. The control plane itself, tenants, the factory, the fleet, and the governance spine, is spec-approved and on the milestone ladder, not yet a thing you deploy.", + "Until it ships, the specs are the artifact. The thesis and the app-shell spec below describe the plane the control plane is being built into; the registry tracks how far along each part is.", + ], + }, + ], + links: [ + { label: "stagecraft on GitHub", href: "https://github.com/stagecraft-ing/stagecraft" }, + { + label: "001: the control-plane thesis", + href: "/registry/stagecraft/001-stagecraft-thesis", + }, + { + label: "002: the EnRaHiTu app shell", + href: "/registry/stagecraft/002-app-shell", + }, + ], + }, +]; + +export function findDoc(slug: string): DocStub | undefined { + return DOC_STUBS.find((d) => d.slug === slug); +} diff --git a/app/lib/milestones.ts b/app/lib/milestones.ts new file mode 100644 index 0000000..7329bd0 --- /dev/null +++ b/app/lib/milestones.ts @@ -0,0 +1,199 @@ +// The milestone ladder (spec 002 §2 "Status"). Each milestone names the real +// specs that constitute it, in the sibling repos' registries. The current +// position is NOT hardcoded: `rollupMilestone` derives it from the baked +// payload's `implementation` fields at build time, so the ladder re-derives on +// every deploy and cannot drift from source. See spec 002 §0 and §6. + +import type { RegistryPayload } from "./registry"; +import { findSpec } from "./registry"; + +export type MilestoneState = "done" | "in-progress" | "planned"; + +/** A spec that constitutes part of a milestone, addressed the way the registry + * viewer addresses it (`/registry/:repo/:id`). */ +export interface MilestoneSpecRef { + repo: string; + id: string; +} + +export interface Milestone { + /** Ladder rung label, e.g. "M1". */ + key: string; + /** Which turn of the loop this rung is: Substrate | Stamp | Factory | Fleet | Verify. */ + phase: string; + title: string; + blurb: string; + specs: MilestoneSpecRef[]; +} + +/** A milestone's constituent spec, resolved against the baked payload. */ +export interface ResolvedSpec extends MilestoneSpecRef { + title: string; + /** "complete" | "in-progress" | "pending" | "" (absent). */ + implementation: string; + found: boolean; +} + +export interface MilestoneRollup { + milestone: Milestone; + state: MilestoneState; + /** How many constituent specs report `implementation: complete`. */ + complete: number; + total: number; + specs: ResolvedSpec[]; +} + +// The ladder. Every id here exists in the baked registry (enrahitu, stagecraft, +// stagecraft-cli); the viewer renders a detail page for each, so every rung is +// navigable to its source. Order is the delivery order, not the loop order. +// Each rung lists the headline specs for its phase, not the repo's full corpus: +// the point is an honest position on the ladder, not an exhaustive index (the +// registry is that). rollupMilestone reads their live state, so the curation +// cannot flatter the result. The index loader fails the build loud if any id +// here stops resolving. +export const MILESTONES: Milestone[] = [ + { + key: "M1", + phase: "Substrate", + title: "The chassis boots", + blurb: + "EnRaHiTu: an Encore.ts app with in-process hiqlite, a CoreLedger data layer over libSQL/Turso, auth on rauthy behind a same-origin proxy, and a webapp, packaged as one container with a vendored toolchain.", + specs: [ + { repo: "enrahitu", id: "001-enrahitu-architecture" }, + { repo: "enrahitu", id: "002-in-process-hiqlite" }, + { repo: "enrahitu", id: "003-coreledger" }, + { repo: "enrahitu", id: "004-auth-core" }, + { repo: "enrahitu", id: "005-rauthy-same-origin" }, + { repo: "enrahitu", id: "006-webapp-spa" }, + { repo: "enrahitu", id: "007-single-container-packaging" }, + { repo: "enrahitu", id: "008-vendored-encore-toolchain" }, + ], + }, + { + key: "M2", + phase: "Stamp", + title: "The template contract", + blurb: + "A versioned template.toml that says what a stamped app is, plus a born-with certificate that binds an explicit agentic posture at stamp time.", + specs: [ + { repo: "enrahitu", id: "009-template-contract" }, + { repo: "enrahitu", id: "012-born-with-provenance" }, + ], + }, + { + key: "M3", + phase: "Factory", + title: "The control plane stamps apps", + blurb: + "Stagecraft itself: the thesis, the EnRaHiTu app shell, tenant onboarding via a per-org GitHub App, and the factory that stamps complete apps into the customer's own org.", + specs: [ + { repo: "stagecraft", id: "001-stagecraft-thesis" }, + { repo: "stagecraft", id: "002-app-shell" }, + { repo: "stagecraft", id: "004-tenants-github-app" }, + { repo: "stagecraft", id: "005-factory-service" }, + ], + }, + { + key: "M4", + phase: "Fleet", + title: "The fleet operates the result", + blurb: + "Placing and running stamped EnRaHiTu apps: one container and one volume each, with update and backup as governed verbs rather than runbooks.", + specs: [{ repo: "stagecraft", id: "006-fleet" }], + }, + { + key: "M5", + phase: "Verify", + title: "The governance spine and the CLI face", + blurb: + "A tamper-evident attestation ledger with an action gate and trust window, and the stagecraft binary that drives the same governed verbs from the terminal and over MCP.", + specs: [ + { repo: "stagecraft", id: "008-governance-attestation" }, + { repo: "stagecraft-cli", id: "001-cli-mcp-thesis" }, + { repo: "stagecraft-cli", id: "004-governance-verbs" }, + { repo: "stagecraft-cli", id: "005-mcp-server" }, + ], + }, +]; + +/** Resolve one milestone against the baked payload and roll its constituent + * specs up into a single state. All complete → done; any complete/in-progress + * → in-progress; otherwise planned. Missing specs never count as complete. */ +export function rollupMilestone( + payload: RegistryPayload, + milestone: Milestone +): MilestoneRollup { + const specs: ResolvedSpec[] = milestone.specs.map((ref) => { + const hit = findSpec(payload, ref.repo, ref.id); + return { + ...ref, + title: hit?.spec.title ?? ref.id, + implementation: + typeof hit?.spec.implementation === "string" + ? hit.spec.implementation + : "", + found: Boolean(hit), + }; + }); + + const total = specs.length; + const complete = specs.filter((s) => s.implementation === "complete").length; + const anyProgress = specs.some( + (s) => + s.implementation === "complete" || s.implementation === "in-progress" + ); + const state: MilestoneState = + total > 0 && complete === total + ? "done" + : anyProgress + ? "in-progress" + : "planned"; + + return { milestone, state, complete, total, specs }; +} + +export function rollupAll(payload: RegistryPayload): MilestoneRollup[] { + return MILESTONES.map((m) => rollupMilestone(payload, m)); +} + +/** Map one spec's raw `implementation` to a state, for single-spec surfaces + * (the loop cards, the for-agents block) that carry a live maturity chip so a + * capability claim is never louder than what the spec that governs it reports. + * An empty value (spec not found) reads as planned; callers that need to fail + * on a missing ref should check `found` explicitly. */ +export function implToState(implementation: string): MilestoneState { + return implementation === "complete" + ? "done" + : implementation === "in-progress" + ? "in-progress" + : "planned"; +} + +const LADDER_CHIP_BASE = + "inline-flex items-center rounded px-1.5 py-0.5 font-mono text-[0.7rem] leading-none"; + +/** Chip classes for a rolled-up milestone state. Mirrors implementationChip's + * palette (emerald/amber/muted) so the ladder reads consistently with the + * registry viewer. */ +export function milestoneStateChip(state: MilestoneState): string { + switch (state) { + case "done": + return `${LADDER_CHIP_BASE} border border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400`; + case "in-progress": + return `${LADDER_CHIP_BASE} border border-amber-500/30 bg-amber-500/10 text-amber-600 dark:text-amber-400`; + default: + return `${LADDER_CHIP_BASE} border border-border bg-muted text-muted-foreground`; + } +} + +/** Human label for a milestone state. */ +export function milestoneStateLabel(state: MilestoneState): string { + switch (state) { + case "done": + return "shipped"; + case "in-progress": + return "in progress"; + default: + return "planned"; + } +} diff --git a/app/routes.ts b/app/routes.ts index 0745858..ab75cd5 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -15,5 +15,6 @@ export default [ route("registry", "routes/registry.tsx"), route("registry/:repo/:specId", "routes/registry.$repo.$specId.tsx"), route("docs", "routes/docs.tsx"), + route("docs/:slug", "routes/docs.$slug.tsx"), ]), ] satisfies RouteConfig; diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index 9858be1..5d85d0f 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -1,88 +1,419 @@ import { Link } from "react-router"; +import type { Route } from "./+types/_index"; +import { loadRegistry } from "~/lib/registry.server"; +import { findSpec, formatAsOf, implementationChip, specNum } from "~/lib/registry"; +import { + implToState, + milestoneStateChip, + milestoneStateLabel, + rollupAll, + type MilestoneRollup, + type MilestoneState, +} from "~/lib/milestones"; +import { ORG_URL, PRODUCT_FAMILY } from "~/lib/product-family"; -// Index PLACEHOLDER. The real launch copy (hero, the loop, product family, -// honest status) is governed by spec 002-launch-content and lands next; spec -// 001 owns only the scaffold, so this page intentionally stays a neutral -// stand-in that proves the layout renders. +// The launch index (spec 002). Positioning in the builder's own register: +// present tense for what works, future tense marked for what is planned, no +// theater (spec 002 §1). Every capability claim carries its receipt, a link +// into the governed registry, and a live maturity chip rolled up from the spec +// that governs it, so nothing on this page can read louder than the source +// (spec 002 §6). The status ladder and the per-block chips are all derived at +// build time from the baked payload; they cannot outrun the specs. -function ArrowCard({ - to, - title, - children, -}: { - to: string; +export function meta(_: Route.MetaArgs): Route.MetaDescriptors { + return [ + { title: "Stagecraft: governed agentic delivery control plane" }, + { + name: "description", + content: + "Intent becomes a governed spec, a factory stamps a complete app from an open template, a fleet operates the result, and your code stays in your GitHub org. Spec-governed, open source, static by construction.", + }, + ]; +} + +type LoopRef = { repo: string; id: string } | null; + +// The delivery loop (spec 002 §2). Each turn carries a receipt (the spec that +// governs it) and a representative spec whose live state decides the block's +// maturity chip. Specify has no single spec: spec-spine governs the whole +// family and demonstrably runs today (this site is built by it), so it is +// resolved as shipped. +const LOOP: Array<{ + verb: string; title: string; - children: React.ReactNode; -}) { + body: string; + href: string; + receipt: string; + ref: LoopRef; +}> = [ + { + verb: "Specify", + title: "Markdown is the source of truth", + body: "spec-spine compiles specs into typed registries and gates drift in CI. Every repo in this family is governed by it today, including this site.", + href: "/registry", + receipt: "the governed registry", + ref: null, + }, + { + verb: "Stamp", + title: "A contract stamps a whole app", + body: "A versioned template contract stamps a complete application from an open template, with a born-with certificate that binds an explicit agentic posture.", + href: "/registry/enrahitu/009-template-contract", + receipt: "the template contract", + ref: { repo: "enrahitu", id: "009-template-contract" }, + }, + { + verb: "Operate", + title: "Update and backup are verbs", + body: "One container and one volume per app. Governed verbs carry an audit trail, instead of a runbook you hope someone followed.", + href: "/registry/stagecraft/006-fleet", + receipt: "the fleet spec", + ref: { repo: "stagecraft", id: "006-fleet" }, + }, + { + verb: "Verify", + title: "The record is checkable", + body: "A tamper-evident attestation ledger records what the plane did, and an independent verifier can check it without trusting the plane.", + href: "/registry/stagecraft/008-governance-attestation", + receipt: "the governance spine", + ref: { repo: "stagecraft", id: "008-governance-attestation" }, + }, +]; + +// The for-agents block resolves its maturity from the MCP server spec. +const AGENT_REF = { repo: "stagecraft-cli", id: "005-mcp-server" }; + +// Runs in Node during prerender (ssr: false). Rolls the milestone ladder and +// the per-block maturity chips up from the baked registry and passes only the +// compact result to the client; the full payload stays on disk. Fails the build +// loud if any referenced spec has fallen out of the registry (mirrors +// scripts/bake-registry.mjs: no silent partial truth). +export function loader() { + const payload = loadRegistry(); + const milestones = rollupAll(payload); + + const loop = LOOP.map((step) => { + const hit = step.ref ? findSpec(payload, step.ref.repo, step.ref.id) : null; + const impl = + hit && typeof hit.spec.implementation === "string" + ? hit.spec.implementation + : ""; + const state: MilestoneState = step.ref ? implToState(impl) : "done"; + const missing = step.ref && !hit ? `${step.ref.repo}/${step.ref.id}` : null; + return { ...step, state, missing }; + }); + + const agentHit = findSpec(payload, AGENT_REF.repo, AGENT_REF.id); + const agentState: MilestoneState = implToState( + agentHit && typeof agentHit.spec.implementation === "string" + ? agentHit.spec.implementation + : "" + ); + + const missing = [ + ...milestones.flatMap((m) => + m.specs.filter((s) => !s.found).map((s) => `${s.repo}/${s.id}`) + ), + ...loop.map((s) => s.missing).filter((m): m is string => Boolean(m)), + agentHit ? null : `${AGENT_REF.repo}/${AGENT_REF.id}`, + ].filter((m): m is string => Boolean(m)); + if (missing.length > 0) { + throw new Error( + `launch page references specs missing from the baked registry: ${missing.join( + ", " + )}. Update app/lib/milestones.ts or app/routes/_index.tsx, or re-bake (npm run bake:registry).` + ); + } + + return { + milestones, + loop: loop.map(({ missing: _drop, ref: _ref, ...rest }) => rest), + agentState, + asOf: payload.generatedAt, + totalSpecs: payload.totalSpecs, + repoCount: payload.repos.length, + }; +} + +function SectionEyebrow({ children }: { children: React.ReactNode }) { return ( - -
- - {title} - - + + {children} +

+ ); +} + +function MaturityChip({ state }: { state: MilestoneState }) { + return ( + {milestoneStateLabel(state)} + ); +} + +function Hero() { + return ( +
+

+ + governed agentic delivery control plane +

+

Stagecraft

+

+ Stagecraft is built around one governed loop: intent becomes a governed + spec, a factory stamps a complete application from an open template, a + fleet operates the result, and your code stays in your GitHub org the + whole time. +

+

+ That is the design. What runs today is the status section below, rolled + up live from the specs, not a roadmap you have to take on faith. The + substrate already ships: EnRaHiTu is Encore.ts, rauthy, hiqlite, and + Turso in a single container, with zero managed dependencies. +

+
+ + Browse the registry + + - → - + Read the source + +
+
+ ); +} + +function TheLoop({ + loop, +}: { + loop: Array<{ + verb: string; + title: string; + body: string; + href: string; + receipt: string; + state: MilestoneState; + }>; +}) { + return ( +
+ the loop +

+ One governed loop, and where each turn stands +

+

+ Each block describes a turn of the loop and carries the state of the + spec that governs it. Present tense is the design; the chip is the + truth. +

+
+ {loop.map((step) => ( +
+
+ + {step.verb} + + +
+

{step.title}

+

{step.body}

+ + {step.receipt} + + +
+ ))}
-

{children}

- +
); } -export default function Index() { +function ProductFamily() { return ( -
-
-

- - scaffold: spec 001 -

-

- Stagecraft -

-

- A governed agentic delivery control plane, and the product family - around it. -

-

- Launch copy is governed by spec 002 and lands next. This scaffold - proves the stack: a fully static React Router v7 site, a build-time - registry viewer over each repo's compiled shards, and boring hosting. - No backend, no analytics, no runtime external requests. +

+ product family +

+ Four repos, one stack +

+

+ Everything is open source and governed the same way. Apps the factory + stamps are not on this list: they belong to their owners, in their own + orgs, under their own licenses. +

+ +
+ ); +} + +function ForAgents({ state }: { state: MilestoneState }) { + return ( +
+
+
+ for agents + +
+

+ Agents run through the same controls as people +

+

+ The model is deliberate: same verbs, same guards, an explicit posture, + no side doors. The MCP face will expose the governed verbs to coding + agents directly, so an agent driving the plane passes through the exact + controls a person does.

-
+ + the MCP server spec + + +
+
+ ); +} + +function LiveRegistry({ + totalSpecs, + repoCount, +}: { + totalSpecs: number; + repoCount: number; +}) { + return ( +
+ read the source +

+ Every claim here is a spec you can open +

+

+ The registry bakes the family's spec corpora at build time, {totalSpecs}{" "} + specs across {repoCount} repos, each page stamped with the commit it was + baked from. Nothing on this page asks you to take it on faith. +

+ + Open the registry + +
+ ); +} + +function StatusRung({ rung }: { rung: MilestoneRollup }) { + const { milestone, state, complete, total, specs } = rung; + return ( +
  • +
    +
    + + {milestone.key} + + + {milestone.phase} + +
    +
    + + {complete}/{total} complete + + +
    +
    +

    {milestone.title}

    +

    {milestone.blurb}

    +
    + {specs.map((spec) => ( - Browse the spec registry + {spec.repo}/{specNum(spec.id)} - - Read the source - -
    -
  • - -
    - - The governed specs of the whole family, baked from each public repo at - build time. Every entry is checkable against its source. - - - Placeholder for now. Documentation grows as the specs that own it land. - + ))}
    + + ); +} + +function Status({ + milestones, + asOf, +}: { + milestones: MilestoneRollup[]; + asOf: string; +}) { + return ( +
    + status +

    + Where it actually is +

    +

    + Each rung rolls up the implementation state of the specs that make it + up, read from the baked registry. When the specs move, this moves. +

    +
      + {milestones.map((rung) => ( + + ))} +
    +

    + rolled up from the registry baked {formatAsOf(asOf)} +

    +
    + ); +} + +export default function Index({ loaderData }: Route.ComponentProps) { + const { milestones, loop, agentState, asOf, totalSpecs, repoCount } = + loaderData; + return ( +
    + + + + + +
    ); } diff --git a/app/routes/docs.$slug.tsx b/app/routes/docs.$slug.tsx new file mode 100644 index 0000000..f5150de --- /dev/null +++ b/app/routes/docs.$slug.tsx @@ -0,0 +1,126 @@ +import { Link } from "react-router"; +import type { Route } from "./+types/docs.$slug"; +import { findDoc, type DocStub } from "~/lib/docs"; +import { repoMeta } from "~/lib/product-family"; + +// A single docs stub (spec 002 §3), rendered from app/lib/docs.ts. Prerendered: +// react-router.config.ts enumerates one /docs/:slug path per stub. + +export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors { + if (!data) return [{ title: "Doc not found: Stagecraft" }]; + return [ + { title: `${data.doc.title}: Stagecraft docs` }, + { name: "description", content: data.doc.summary }, + ]; +} + +export function loader({ params }: Route.LoaderArgs) { + const doc = findDoc(params.slug); + if (!doc) { + throw new Response("Doc not found", { status: 404 }); + } + return { doc }; +} + +const MATURITY: Record = { + shipping: { + label: "shipping", + className: + "border border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400", + }, + "in-progress": { + label: "in progress", + className: + "border border-amber-500/30 bg-amber-500/10 text-amber-600 dark:text-amber-400", + }, + planned: { + label: "planned", + className: "border border-border bg-muted text-muted-foreground", + }, +}; + +function DocLinkItem({ label, href }: { label: string; href: string }) { + const internal = href.startsWith("/"); + const className = + "inline-flex items-center rounded-md border border-border px-3 py-1.5 font-mono text-sm text-muted-foreground transition-colors hover:border-primary/50 hover:text-foreground"; + if (internal) { + return ( + + {label} + + ); + } + return ( + + {label} → + + ); +} + +export default function DocDetail({ loaderData }: Route.ComponentProps) { + const { doc } = loaderData; + const meta = repoMeta(doc.repo); + const maturity = MATURITY[doc.maturity]; + + return ( +
    + + +
    + + {maturity.label} + + {meta ? ( + + {meta.repo} + {meta.license ? ` · ${meta.license}` : ""} + + ) : null} +
    + +

    + {doc.title} +

    +

    {doc.summary}

    + +
    + {doc.sections.map((section) => ( +
    +

    + {section.heading} +

    +
    + {section.body.map((para, i) => ( +

    {para}

    + ))} +
    +
    + ))} +
    + +
    +

    + Read the source +

    +
    + {doc.links.map((link) => ( + + ))} +
    +
    +
    + ); +} diff --git a/app/routes/docs.tsx b/app/routes/docs.tsx index c5bf4fb..ee470e0 100644 --- a/app/routes/docs.tsx +++ b/app/routes/docs.tsx @@ -1,8 +1,11 @@ import { Link } from "react-router"; import type { Route } from "./+types/docs"; +import { DOC_STUBS, type DocStub } from "~/lib/docs"; +import { repoMeta } from "~/lib/product-family"; -// Docs PLACEHOLDER. Spec 001 ships exactly one placeholder page; the real docs -// content collection is owned by later specs. Kept honest: it says what it is. +// Docs index (spec 002 §3): a short shelf of stubs, each sourced from a repo's +// own README and specs. The stubs link and summarize; the source stays the +// source. Static content module, no loader needed. export function meta(_: Route.MetaArgs): Route.MetaDescriptors { return [ @@ -10,36 +13,72 @@ export function meta(_: Route.MetaArgs): Route.MetaDescriptors { { name: "description", content: - "Documentation for the Stagecraft product family. Placeholder while the owning specs land.", + "Documentation for the Stagecraft product family, sourced from each repo's own specs: what EnRaHiTu is, the template contract, and self-hosting the control plane.", }, ]; } +const MATURITY_LABEL: Record = { + shipping: "shipping", + "in-progress": "in progress", + planned: "planned", +}; + +function DocCard({ doc }: { doc: DocStub }) { + const meta = repoMeta(doc.repo); + return ( + +
    + + {doc.title} + + + → + +
    +

    {doc.summary}

    +
    + {meta ? ( + + {meta.repo} + {meta.license ? ` · ${meta.license}` : ""} + + ) : null} + · + {MATURITY_LABEL[doc.maturity]} +
    + + ); +} + export default function Docs() { return ( -
    -

    placeholder

    -

    Docs

    -

    - This is the single placeholder page spec 001 ships. The documentation - content collection grows as the specs that own it land. Until then, the - source itself is the documentation. +

    +

    + + docs

    -
    - - Spec registry +

    Docs

    +

    + Short, honest entry points into the family, each summarizing a repo's + own specs rather than restating them. For the full picture, the specs + are the documentation: browse them in the{" "} + + registry - - Source - + . +

    + +
    + {DOC_STUBS.map((doc) => ( + + ))}
    ); diff --git a/react-router.config.ts b/react-router.config.ts index 71e2712..ced9bce 100644 --- a/react-router.config.ts +++ b/react-router.config.ts @@ -1,6 +1,7 @@ import type { Config } from "@react-router/dev/config"; import { readFileSync } from "node:fs"; import path from "node:path"; +import { DOC_STUBS } from "./app/lib/docs"; // Static site, framework mode. `ssr: false` means no runtime server; every // route is prerendered to HTML at build time (loaders run in Node during the @@ -30,9 +31,21 @@ function registryDetailPaths(): string[] { } } +// The docs stubs (spec 002 §3) are a static content module, so their prerender +// paths come straight from it: one /docs/:slug page per stub, no drift. +function docsPaths(): string[] { + return DOC_STUBS.map((doc) => `/docs/${doc.slug}`); +} + export default { ssr: false, async prerender() { - return ["/", "/registry", "/docs", ...registryDetailPaths()]; + return [ + "/", + "/registry", + "/docs", + ...docsPaths(), + ...registryDetailPaths(), + ]; }, } satisfies Config; diff --git a/specs/002-launch-content/spec.md b/specs/002-launch-content/spec.md index e3667d1..9eaa046 100644 --- a/specs/002-launch-content/spec.md +++ b/specs/002-launch-content/spec.md @@ -3,11 +3,15 @@ id: "002-launch-content" title: "Launch content: positioning, product family, honest status" status: approved created: "2026-07-14" -implementation: pending +implementation: complete depends_on: - "001-site-scaffold" establishes: - - { kind: directory, path: "src/content/" } + - "app/routes/_index.tsx" + - "app/routes/docs.tsx" + - "app/routes/docs.$slug.tsx" + - "app/lib/docs.ts" + - "app/lib/milestones.ts" summary: > The words on the site at launch. One index page that states what Stagecraft is in the builder's own register (creator-led, @@ -19,6 +23,32 @@ summary: > # 002: Launch content +## 0. Implementation amendment (2026-07-14) + +Two design points, settled before coding, so the spec matches the code +it owns: + +- **Content architecture is React Router v7, not Astro.** The original + `establishes: src/content/` was residue from the dropped Astro choice + (spec 001 was itself amended 2026-07-14 to RR7). The launch content + lives in the RR7 app tree: the index page (`app/routes/_index.tsx`), + a docs index (`app/routes/docs.tsx`), a docs stub route + (`app/routes/docs.$slug.tsx`), and two typed content modules + (`app/lib/docs.ts`, `app/lib/milestones.ts`). No markdown/MDX + toolchain is introduced: that would be a runtime-of-the-build + dependency the site does not need, and §1's voice constraints are + better served by typed content than by a content collection. +- **The status ladder is derived from the baked registry, never + hardcoded.** §2's "Status" reads its per-milestone position by rolling + up the `implementation` field of the constituent specs in the + build-time-baked payload (`public/data/registry.json`), so it re-derives + on every deploy and cannot drift from source. The authoring-time hint + ("M1 done: born-green stamp proven") is explicitly superseded by that + rule: at implementation time the registry shows `enrahitu/009-template-contract` + is `in-progress` and `enrahitu/012-born-with-provenance` is `pending`, + so the born-with stamp is not yet proven. The ladder reflects the + registry truth (§6), not the hint. + ## 1. Voice constraints Written for engineers who evaluate tools by reading source. No @@ -65,10 +95,13 @@ reads naturally (a single creator builds this in the open). ## 3. Docs seed -Move the placeholder docs page into three real stubs sourced from the -repos' own READMEs (do not fork prose; link and summarize): "What is -EnRaHiTu", "The template contract", "Self-hosting the control plane -(AGPL)". +Turn the single placeholder docs page (`app/routes/docs.tsx`) into an +index over three real stubs, each rendered by `app/routes/docs.$slug.tsx` +from `app/lib/docs.ts`, sourced from the repos' own READMEs and specs (do +not fork prose; link and summarize): "What is EnRaHiTu", "The template +contract", "Self-hosting the control plane (AGPL)". Where a stub describes +work that is spec-approved but not yet built (the control plane), it says +so in future tense and points at the governing specs, per §1. ## 4. Acceptance @@ -82,3 +115,39 @@ EnRaHiTu", "The template contract", "Self-hosting the control plane - Pricing, waitlists, email capture. - Long-form architecture essays (belong in the repos' specs/docs). + +## 6. Status (2026-07-14): complete + +Implemented and verified. The launch content replaces the spec-001 +placeholders; no new build toolchain was added. + +- **Index** (`app/routes/_index.tsx`): hero (the loop stated as thesis + + the EnRaHiTu substrate line), the four-block loop (Specify / Stamp / + Operate / Verify, each linking to the spec that governs it), the + product-family list (from `app/lib/product-family.ts`, licenses shown, + with the note that stamped apps belong to their owners), a "for agents" + block pointing at the MCP server spec, a live-registry pointer, and the + status ladder. +- **Status ladder** (`app/lib/milestones.ts`): M1-M5 mapped to real + sibling-repo spec ids; the loader rolls each rung up from the baked + payload at build time. Verified truth at implementation time: **M1 + Substrate 8/8 → shipped** (`enrahitu/001`-`008` complete); **M2 Stamp + 0/2 → in progress** (`enrahitu/009` in-progress, `enrahitu/012` + pending); **M3 Factory**, **M4 Fleet**, **M5 Verify** → planned + (`stagecraft/*`, `stagecraft-cli/*` approved, pending). This supersedes + the §0 authoring hint: the born-with stamp is not yet proven, so the + ladder does not claim it. +- **Docs** (`app/routes/docs.tsx` index + `app/routes/docs.$slug.tsx` + + `app/lib/docs.ts`): three stubs (What is EnRaHiTu, The template + contract, Self-hosting the control plane (AGPL)), each summarizing and + linking its source, with an honest maturity marker; the control-plane + stub is marked planned per §1. + +Acceptance: every claim links to a public repo or a baked spec id (the +ladder is a pure function of the baked shards); `npm run typecheck` and +`npm run build` are clean (all routes prerendered, including the three new +docs pages); every internal link resolves (all linked registry/docs paths +prerender to 200); the built output makes zero off-origin resource +requests; the skim test holds (what it is, what runs today, and the +licenses are all answerable from the index). Spine gates green (`compile`, +`index`, `lint --fail-on-warn`, `index check`).