1232 lines
51 KiB
Markdown
1232 lines
51 KiB
Markdown
# Change 0001: Repository-driven theme rendering
|
|
|
|
- Status: Implemented
|
|
- Owners: Website engine maintainers
|
|
- Target: `labyricorn-theme/v1`
|
|
- Last updated: 2026-07-23
|
|
|
|
## Summary
|
|
|
|
Replace the built-in production HTML and CSS renderer with a repository-controlled
|
|
theme package rooted at `.theme/`. A build must resolve every input to an exact Git
|
|
commit, validate the theme without executing repository code, render with a
|
|
strict Liquid-compatible engine, copy only declared static assets, and publish a
|
|
single immutable release artifact.
|
|
|
|
Themes are trusted presentation data, not executable build programs. They may
|
|
contain templates and manifest-declared browser assets, but may not run Node,
|
|
shell commands, package-manager scripts, Liquid extensions, or arbitrary
|
|
build-time callbacks.
|
|
|
|
The first implementation is deliberately read-only. The control plane displays
|
|
configuration and theme data from the resolved Git snapshot, but does not claim
|
|
to persist edits until a separate reviewed commit or pull-request workflow is
|
|
implemented.
|
|
|
|
## Decision record
|
|
|
|
| Area | Decision |
|
|
| --- | --- |
|
|
| Template engine | Liquid-compatible, implemented with LiquidJS in strict mode |
|
|
| Theme location | `.theme/` in the repository named by `site.yml` |
|
|
| Theme execution | Templates only; no repository-provided build-time code |
|
|
| Template selection | Explicit manifest keys selected by content and project configuration |
|
|
| Project publication | Configuration-first; repository content cannot create a project page by itself |
|
|
| Standalone files | Explicit source-to-route mappings in project configuration |
|
|
| CSS and browser JavaScript | Static files explicitly declared by `theme.yml` |
|
|
| Fonts | Git or checksum-pinned HTTPS sources, copied into the release |
|
|
| Missing or invalid theme | Fail validation and create no release |
|
|
| Configuration source | Fresh immutable snapshots at exact Git commits for every build |
|
|
| Control-plane editing | Read-only in v1 |
|
|
| Content safety | Escaped values plus explicitly typed, sanitized content fields |
|
|
| Promotion | Staging and live point to the same immutable release directory |
|
|
| Determinism | Input commits and source-file checksums are recorded in the manifest |
|
|
| Production fallback | None after migration |
|
|
|
|
## Current state and problem statement
|
|
|
|
The current implementation has a real deterministic artifact writer, but its
|
|
presentation and provenance do not yet match what the control plane reports.
|
|
|
|
| Current behavior | Evidence | Required change |
|
|
| --- | --- | --- |
|
|
| Production layout and CSS are string literals. | `src/backend/buildEngine.ts` defines `layout()` and embeds a `<style>` block. | Render every route from `.theme/` templates and copy declared assets. |
|
|
| Theme metadata is synthetic process state. | `src/backend/store.ts` constructs `themeConfig` during startup. | Load the manifest and files from a pinned repository snapshot. |
|
|
| Theme validation always succeeds without reading files. | `POST /api/theme/validate` in `server.ts` sets `isValidated = true`. | Run the same loader, schema, path, parse, and policy checks used by builds. |
|
|
| Site configuration is loaded from the application checkout at startup. | `src/backend/store.ts` reads `packages/site-definition/*.yml` in its constructor. | Load configuration from a pinned site-definition commit for each build. |
|
|
| The configuration editor only mutates memory. | `PUT /api/site-config` in `server.ts` changes `store.siteConfig`. | Make the API and UI explicitly read-only in v1. |
|
|
| The UI says that saving created a Git commit. | `src/components/ConfigAndAuditTab.tsx` displays “Created Git commit”. | Remove the editor and false persistence message. |
|
|
| Build provenance uses a placeholder. | `src/backend/store.ts` writes `siteDefCommit: "local"`. | Record the resolved repository and full commit SHA. |
|
|
| Live preview has a second hard-coded renderer. | `/api/live-site/html` in `server.ts` synthesizes HTML if no release file exists. | Serve a release file or return an explicit unavailable/404 response. |
|
|
| Staging is represented by mutable status only. | `stagingReleaseId` is updated, while the configured staging root is separate. | Point a staging link to the built release; promote that same directory. |
|
|
|
|
These mismatches are operationally significant. An operator cannot prove which
|
|
repository state produced a release, theme validation does not establish that a
|
|
theme exists, a restart discards edited configuration, and preview output can
|
|
differ from the generated artifact.
|
|
|
|
## Goals
|
|
|
|
- All production presentation comes from a validated `.theme/` package.
|
|
- Builds use immutable Git snapshots and remain restart-safe.
|
|
- A missing, malformed, unsafe, or incomplete theme fails before a release
|
|
directory is created.
|
|
- The manifest identifies the exact site-definition, content, and theme inputs.
|
|
- Builds from identical input snapshots are byte-identical.
|
|
- Staging and live serve the identical immutable artifact.
|
|
- The control plane reports only capabilities the backend actually implements.
|
|
- Content cannot acquire script execution by crossing the Markdown-to-template
|
|
boundary.
|
|
- Composite project pages are generated only from declared project
|
|
configuration and may include configured metadata, relationships, and
|
|
standalone file publications.
|
|
- Content models select explicit theme templates instead of relying on
|
|
conditional logic in one generic content template.
|
|
|
|
## Non-goals
|
|
|
|
- Arbitrary Node, shell, WASM, package-manager, or generator execution from a
|
|
theme repository.
|
|
- A visual page builder.
|
|
- A runtime database-backed CMS.
|
|
- Automatic mutation of Git repositories in v1.
|
|
- Installing dependencies declared by a theme.
|
|
- General-purpose or undeclared repository file hosting.
|
|
- Server-side theme plug-ins, custom Liquid tags, or custom Liquid filters.
|
|
- Silent production fallback to a built-in renderer.
|
|
- Backward-compatible rendering of the synthetic `ThemeConfig`.
|
|
- Guaranteeing identical artifacts across different builder versions.
|
|
|
|
## Terminology and invariants
|
|
|
|
- **Build run ID** is the control-plane identifier such as `build-000123`. It is
|
|
operational state and must not enter rendered bytes.
|
|
- **Input snapshot** is the canonical descriptor of resolved repositories,
|
|
commits, builder version, and source-file checksums.
|
|
- **Artifact build ID** is `sha256-<hex>` over the canonical input snapshot. It
|
|
is deterministic and is the value exposed to templates as `build.id`.
|
|
- **Release directory** is the immutable directory created after a successful
|
|
build.
|
|
- **Theme root** is the validated `.theme/` directory inside the checked-out
|
|
theme repository.
|
|
- **Declared project** is a project entry in the Git-owned project configuration.
|
|
Repository discovery alone never creates one.
|
|
- **Published file** is a regular repository file copied unchanged to one exact
|
|
configured artifact route.
|
|
- **Promotion** changes a staging or live pointer; it never rerenders or copies
|
|
a release.
|
|
|
|
The following invariants are mandatory:
|
|
|
|
1. A ref is resolved once per build and all reads use the resulting full commit
|
|
SHA.
|
|
2. No build reads configuration, content, templates, or assets from mutable
|
|
process state after snapshots are created.
|
|
3. No release directory is visible until all validation and rendering steps
|
|
succeed.
|
|
4. A published release is never modified.
|
|
5. The build run ID, wall-clock start time, temporary paths, process ID, and
|
|
repository checkout paths never affect artifact bytes.
|
|
6. Staging and live reference a release by directory identity and verified
|
|
artifact checksum.
|
|
|
|
## Repository and snapshot contract
|
|
|
|
`site.yml` continues to select the theme:
|
|
|
|
```yaml
|
|
theme:
|
|
source: site-definition
|
|
path: .theme
|
|
```
|
|
|
|
`source` must equal a configured Git source ID. A leading slash in the existing
|
|
`/.theme` spelling is accepted during migration and canonicalized to `.theme`;
|
|
new configuration must use the repository-relative spelling.
|
|
|
|
For each build, a new `BuildInputLoader` performs these steps:
|
|
|
|
1. Resolve the site-definition ref with Git to a full 40-character commit SHA.
|
|
2. Create a detached, read-only checkout for that SHA beneath the build work
|
|
root.
|
|
3. Parse and validate `site.yml` from that checkout.
|
|
4. Resolve every required source ref to a full SHA and create detached
|
|
checkouts. A required source that is unreachable or unresolved fails the
|
|
build.
|
|
5. Resolve `site.theme.source` to one of those snapshots. In v1 this will
|
|
normally be `site-definition`, but the contract permits a separate configured
|
|
theme source.
|
|
6. Load navigation, content models, source definitions, style configuration,
|
|
content, and `.theme/` only from these snapshots.
|
|
7. Create a canonical input descriptor and derive the artifact build ID.
|
|
8. Pass an immutable `ResolvedBuildInput` into validation and rendering.
|
|
|
|
Resolving a ref and checking out files are part of one build operation. Cached
|
|
Git object databases may be reused, but working trees and previously parsed
|
|
objects may not be reused as authoritative input. A cache hit must still verify
|
|
the requested commit and materialize or address that exact tree.
|
|
|
|
Production builds reject dirty working trees and symbolic refs after resolution.
|
|
An explicitly configured local working-tree mode may remain for development, but
|
|
it must be labeled non-reproducible, must not be promotable, and is outside the
|
|
v1 production path.
|
|
|
|
The new backend boundary is:
|
|
|
|
```ts
|
|
interface RepositorySnapshot {
|
|
sourceId: string;
|
|
repository: string;
|
|
commit: string; // full SHA
|
|
checkoutRoot: string; // internal only; never serialized into an artifact
|
|
}
|
|
|
|
interface ResolvedBuildInput {
|
|
runId: string;
|
|
artifactBuildId: string;
|
|
generatedAt: string;
|
|
siteDefinition: RepositorySnapshot;
|
|
sources: ReadonlyMap<string, RepositorySnapshot>;
|
|
siteConfig: SiteConfig;
|
|
theme: LoadedTheme;
|
|
projects: readonly ResolvedProject[];
|
|
contentItems: readonly ContentItem[];
|
|
mediaAssets: readonly MediaAsset[];
|
|
publishedFiles: readonly ResolvedPublishedFile[];
|
|
}
|
|
```
|
|
|
|
`generatedAt` is deterministic: it is the latest Git committer timestamp among
|
|
the pinned input commits, normalized to UTC with whole-second precision. It is
|
|
not the build execution time. The execution time remains control-plane metadata
|
|
outside the artifact.
|
|
|
|
## Configuration-first project publication
|
|
|
|
Project pages are allowlisted by Git-owned configuration. Discovery of a
|
|
repository, README, devlog, or project-shaped front matter never creates a
|
|
public project or route by itself.
|
|
|
|
`site.yml` references a project declaration file:
|
|
|
|
```yaml
|
|
projectsFile: ./projects.yml
|
|
```
|
|
|
|
`projects.yml` declares each public project and all material that may be joined
|
|
into its composite page:
|
|
|
|
```yaml
|
|
projects:
|
|
- id: demo-project
|
|
source: demo-project
|
|
route: /project/demo-project.html
|
|
title: Demo Project
|
|
summary: A configured Git-native project.
|
|
version: 1.0.0
|
|
license: MIT
|
|
homepage: /project/demo-project/demo.html
|
|
tags: [demo, test]
|
|
stack: [Python, HTML, CSS, Git]
|
|
|
|
presentation:
|
|
detailTemplate: project
|
|
|
|
relationships:
|
|
devlogs:
|
|
contentModel: devlog
|
|
matchField: metadata.projectId
|
|
required: false
|
|
articles:
|
|
contentModel: article
|
|
matchField: metadata.projectId
|
|
required: false
|
|
|
|
publishedFiles:
|
|
- source: demo.html
|
|
route: /project/demo-project/demo.html
|
|
mediaType: text/html
|
|
```
|
|
|
|
`source` must resolve to a configured pinned repository snapshot. `id`,
|
|
`source`, `route`, and `presentation.detailTemplate` are required. Project
|
|
metadata is schema-validated and remains inert escaped data in the render
|
|
context.
|
|
|
|
The project projection obeys these rules:
|
|
|
|
1. No declaration means no project page, even when matching repository content
|
|
exists.
|
|
2. A duplicate project ID or route is a configuration error.
|
|
3. A declared project whose source or detail template cannot be resolved fails
|
|
the build. A relationship with no matches fails only when its declaration
|
|
sets `required: true`.
|
|
4. `matchField` is a schema-defined dotted field name, not an expression. Its
|
|
value must equal the declaring project's `id`. Arbitrary queries, Liquid,
|
|
regular expressions, and executable predicates are forbidden.
|
|
5. Relationship items are filtered to published, valid content and sorted by a
|
|
configured stable order or the content model's canonical order.
|
|
6. Git provenance shown on the project page comes from the pinned source
|
|
snapshot and discovered commit metadata, never from theme-supplied values.
|
|
7. The engine generates deterministic heading IDs and a table of contents from
|
|
sanitized Markdown headings when that capability is enabled by the content
|
|
model.
|
|
|
|
Navigation and content-model configuration select section templates by key:
|
|
|
|
```yaml
|
|
navigation:
|
|
- id: projects
|
|
route: /projects
|
|
contentModel: project
|
|
presentation:
|
|
sectionTemplate: projects
|
|
```
|
|
|
|
The keys `project` and `projects` resolve through `theme.yml`; configuration
|
|
never supplies a template filesystem path.
|
|
|
|
### Declared standalone files
|
|
|
|
`publishedFiles` is the configuration term for exact source-to-route
|
|
publication. Each entry maps one regular file in the project's pinned source
|
|
repository to one exact artifact path. The source path is repository-relative;
|
|
the route is an absolute public path and may be file-style, such as
|
|
`/project/demo-project/demo.html`.
|
|
|
|
Published files are copied byte-for-byte. They are not Liquid-rendered or
|
|
Markdown-rendered. They are nevertheless part of the release security boundary:
|
|
|
|
- source paths receive the same traversal, symlink, special-file, and size
|
|
checks as theme assets;
|
|
- every dependent local stylesheet, script, image, font, or other file must
|
|
have its own declaration or resolve to an already declared release asset;
|
|
- HTML and CSS are parsed after copying and must satisfy the same script,
|
|
external-origin, URL-scheme, and local-reference policy as rendered pages;
|
|
- executable server files and build scripts are never run;
|
|
- route and output-file collisions fail the build; and
|
|
- each source checksum, output route, media type, and output checksum is
|
|
recorded in the build manifest.
|
|
|
|
The route writer must support both directory routes that emit `index.html` and
|
|
literal configured file routes. These are distinct route kinds in the internal
|
|
model and share one collision map. A published file is unreachable unless its
|
|
declaration and owning project both validate.
|
|
|
|
## Theme package contract
|
|
|
|
The required layout is:
|
|
|
|
```text
|
|
.theme/
|
|
├── theme.yml
|
|
├── templates/
|
|
│ ├── layout.liquid
|
|
│ ├── home.liquid
|
|
│ ├── 404.liquid
|
|
│ ├── sections/
|
|
│ │ ├── projects.liquid
|
|
│ │ ├── articles.liquid
|
|
│ │ └── blog.liquid
|
|
│ └── content/
|
|
│ ├── project.liquid
|
|
│ ├── article.liquid
|
|
│ └── blog.liquid
|
|
├── partials/
|
|
└── assets/
|
|
├── styles/
|
|
├── scripts/
|
|
├── images/
|
|
└── fonts/
|
|
```
|
|
|
|
The tree is illustrative. `layout`, `home`, and `notFound` are required. Every
|
|
section or content template key referenced by site, navigation, content-model,
|
|
or project configuration is also required. Unreferenced templates are allowed
|
|
but do not create routes.
|
|
|
|
### `theme.yml`
|
|
|
|
```yaml
|
|
protocol: labyricorn-theme/v1
|
|
id: labyricorn-default
|
|
version: 2.0.0
|
|
engine: liquid
|
|
|
|
templates:
|
|
layout: templates/layout.liquid
|
|
home: templates/home.liquid
|
|
notFound: templates/404.liquid
|
|
sections:
|
|
projects: templates/sections/projects.liquid
|
|
articles: templates/sections/articles.liquid
|
|
blog: templates/sections/blog.liquid
|
|
content:
|
|
project: templates/content/project.liquid
|
|
article: templates/content/article.liquid
|
|
blog: templates/content/blog.liquid
|
|
|
|
assets:
|
|
styles:
|
|
- assets/styles/theme.css
|
|
scripts:
|
|
- assets/scripts/theme.js
|
|
files:
|
|
- assets/images/logo.svg
|
|
fonts:
|
|
- id: playfair-display-500-italic
|
|
family: Playfair Display
|
|
style: italic
|
|
weight: 500
|
|
source:
|
|
kind: https
|
|
url: https://fonts.gstatic.com/example/playfair-display.woff2
|
|
checksum: sha256-0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
|
|
format: woff2
|
|
- id: ibm-plex-mono-400
|
|
family: IBM Plex Mono
|
|
style: normal
|
|
weight: 400
|
|
source:
|
|
kind: git
|
|
sourceId: site-definition
|
|
path: .theme/assets/fonts/ibm-plex-mono-400.woff2
|
|
format: woff2
|
|
|
|
security:
|
|
allowScripts: true
|
|
allowExternalAssets: false
|
|
allowedExternalOrigins: []
|
|
```
|
|
|
|
The schema is closed: unknown keys fail validation. `protocol` and `engine` are
|
|
exact enums. `id` and `version` are required non-empty strings; `version` must be
|
|
SemVer. Template keys must match the configured presentation keys exactly. A
|
|
missing key is a build error; the renderer does not fall back to another content
|
|
template.
|
|
|
|
Content-model and project configuration selects keys, not paths. For example,
|
|
`detailTemplate: project` selects `templates.content.project`; it cannot name
|
|
`templates/content/project.liquid` directly. This keeps filesystem ownership in
|
|
the theme manifest while making presentation selection explicit.
|
|
|
|
Asset lists are ordered because their order controls emitted `<link>` and
|
|
`<script>` tags. Duplicate paths are invalid. Only regular files below
|
|
`.theme/assets/` may be declared. Directories, globs, absolute paths, URL values,
|
|
and undeclared files are not copied, except for the explicit font-source union
|
|
described below.
|
|
|
|
### Font sources
|
|
|
|
`assets.fonts` may contain as many font-face declarations as the theme needs,
|
|
subject to the aggregate theme size and build resource limits. Each declaration
|
|
describes one exact face, including family, style, numeric weight, format, and a
|
|
source.
|
|
|
|
A `git` font source identifies a configured source snapshot and a
|
|
repository-relative regular file. Its bytes come from that source's pinned
|
|
commit. An `https` font source identifies one exact HTTPS font-file URL and must
|
|
include its expected SHA-256 checksum. The builder:
|
|
|
|
1. resolves Git font files from the pinned snapshot or downloads HTTPS font
|
|
files with a versioned fixed request profile;
|
|
2. rejects redirects to a different origin, credentials in URLs, non-HTTPS
|
|
remote URLs, private/link-local/loopback destinations, MIME or format
|
|
mismatches, and checksum mismatches;
|
|
3. caches remote bytes only by verified checksum;
|
|
4. copies every verified face to a deterministic local path under
|
|
`/assets/fonts/`;
|
|
5. emits deterministic `/assets/styles/fonts.generated.css` containing the
|
|
corresponding `@font-face` declarations; and
|
|
6. exposes that local stylesheet through `theme.fontStylesheet`.
|
|
|
|
The generated site never depends on a live Google or other font host at runtime.
|
|
A Google-hosted face is therefore represented by its direct HTTPS font-file URL
|
|
and pinned checksum, not by an unpinned runtime stylesheet. All font source URLs,
|
|
source commits, input checksums, and release paths are recorded in the manifest.
|
|
|
|
If `allowScripts` is false, `assets.scripts` must be empty and generated HTML
|
|
must not contain script elements or inline event-handler attributes. If true,
|
|
only the declared local script files may be referenced; inline scripts and
|
|
inline event handlers remain forbidden.
|
|
|
|
All author-supplied CSS must be a declared static stylesheet. Inline `<style>`
|
|
elements and `style` attributes are rejected. The engine-generated font
|
|
stylesheet is the only generated CSS exception. CSS files are parsed so every
|
|
local `url()` resolves to a declared release file and every external URL obeys
|
|
the external-asset policy.
|
|
|
|
If `allowExternalAssets` is false, `allowedExternalOrigins` must be empty and
|
|
runtime network-bearing `src`, `srcset`, stylesheet, object, embed, and iframe
|
|
reference is rejected. Build-time HTTPS font sources are copied locally and do
|
|
not count as runtime external assets. If external assets are enabled, each
|
|
external origin must be an exact
|
|
lower-case HTTPS origin in `allowedExternalOrigins`; wildcards, credentials,
|
|
paths, queries, fragments, HTTP, protocol-relative URLs, and non-default port
|
|
ambiguity are forbidden. Ordinary navigation links are link-checked but are not
|
|
assets.
|
|
|
|
External navigation links are checked syntactically without making a network
|
|
request; HTTPS and configured `mailto:` links are permitted.
|
|
|
|
Content-derived embeds, including YouTube embeds, are subject to the same
|
|
external-origin allowlist. For example, a theme that renders the engine's
|
|
YouTube directive must explicitly allow
|
|
`https://www.youtube-nocookie.com`.
|
|
|
|
### Liquid profile
|
|
|
|
The renderer uses LiquidJS with:
|
|
|
|
- `strictVariables: true`;
|
|
- `strictFilters: true`;
|
|
- file-system access disabled except through an engine-owned partial resolver;
|
|
- dynamic partial names disabled;
|
|
- include/render roots fixed to `.theme/partials/`;
|
|
- an allowlist of standard non-executing tags and filters;
|
|
- the `raw` tag and any unescaped-output aliases disabled;
|
|
- maximum template size, render-output size, include depth, loop iterations, and
|
|
render duration enforced; and
|
|
- no theme-provided extensions.
|
|
|
|
The exact allowlists and limits live in versioned engine code and are covered by
|
|
contract tests. The initial limits are:
|
|
|
|
| Limit | Value |
|
|
| --- | ---: |
|
|
| Individual manifest, template, or partial | 1 MiB |
|
|
| Total theme source plus declared assets | 50 MiB |
|
|
| Include depth | 20 |
|
|
| Loop iterations per rendered page | 10,000 |
|
|
| Generated HTML per route | 5 MiB |
|
|
| Render time per route | 2 seconds |
|
|
|
|
Limit failures are validation/build errors, not warnings.
|
|
|
|
`layout.liquid` is the outer document template. Page templates render their page
|
|
body first; the engine then invokes `layout.liquid` with that result in
|
|
`page.body`. The engine chooses the page template from the validated
|
|
configuration key and manifest mapping before rendering. Liquid cannot compute,
|
|
override, or select a template path.
|
|
|
|
## Stable render context
|
|
|
|
Templates receive the following documented, JSON-serializable shape:
|
|
|
|
```yaml
|
|
site:
|
|
id:
|
|
title:
|
|
language:
|
|
baseUrl:
|
|
|
|
navigation:
|
|
- id:
|
|
label:
|
|
route:
|
|
contentModel:
|
|
templateKey:
|
|
|
|
page:
|
|
type: home | section | content | project | notFound
|
|
contentModel:
|
|
templateKey:
|
|
id:
|
|
title:
|
|
route:
|
|
summary:
|
|
published:
|
|
updated:
|
|
author:
|
|
tags: []
|
|
metadata:
|
|
version:
|
|
license:
|
|
homepage:
|
|
stack: []
|
|
provenance:
|
|
sourceId:
|
|
repository:
|
|
commit:
|
|
renderedContent:
|
|
body:
|
|
tableOfContents:
|
|
- id:
|
|
text:
|
|
level:
|
|
relationships:
|
|
devlogs:
|
|
- id:
|
|
title:
|
|
route:
|
|
summary:
|
|
published:
|
|
metadata: {}
|
|
renderedContent:
|
|
articles:
|
|
- id:
|
|
title:
|
|
route:
|
|
summary:
|
|
published:
|
|
metadata: {}
|
|
renderedContent:
|
|
publications:
|
|
- source:
|
|
route:
|
|
mediaType:
|
|
|
|
collection:
|
|
items:
|
|
- id:
|
|
contentModel:
|
|
templateKey:
|
|
title:
|
|
route:
|
|
summary:
|
|
published:
|
|
tags: []
|
|
metadata: {}
|
|
|
|
theme:
|
|
id:
|
|
version:
|
|
assetBaseUrl: /assets/
|
|
styles: []
|
|
scripts: []
|
|
fontStylesheet: /assets/styles/fonts.generated.css
|
|
|
|
build:
|
|
id:
|
|
generatedAt:
|
|
sourceCommits: {}
|
|
```
|
|
|
|
Keys are always present. Inapplicable scalar values are `null`, and collections
|
|
are empty arrays or objects. Items and object keys are sorted in engine-defined
|
|
canonical order before rendering. New optional keys may be added within the
|
|
same protocol, but existing keys may not change meaning or type.
|
|
|
|
`page.relationships` contains only relationship names declared by the owning
|
|
project configuration and only the fields allowed by each content model's
|
|
projection schema. A project template can therefore render a composite page
|
|
with repository metadata, a generated contents panel, embedded devlogs, related
|
|
articles, and links to declared standalone publications without accessing raw
|
|
repository objects.
|
|
|
|
`page.metadata` and collection-item metadata use closed schemas selected by the
|
|
content model. They are not unrestricted front-matter bags. Project version,
|
|
license, homepage, and stack come from the project declaration; source commit,
|
|
updated time, and author come from verified discovery provenance.
|
|
|
|
The context contains no filesystem paths, environment variables, credentials,
|
|
repository transport credentials, mutable store objects, functions, prototypes,
|
|
or executable callbacks.
|
|
|
|
### Content-safety boundary
|
|
|
|
All ordinary scalar values are HTML-escaped by default at Liquid output time.
|
|
Markdown is converted by the engine and sanitized against a versioned HTML
|
|
allowlist before it becomes `page.renderedContent`. The engine represents that
|
|
field internally as a `SafeHtml` value; a theme can emit it only with the
|
|
engine-owned `safe_content` filter:
|
|
|
|
```liquid
|
|
<article class="content">
|
|
{{ page.renderedContent | safe_content }}
|
|
</article>
|
|
```
|
|
|
|
`safe_content` rejects every value except the exact engine-created safe value.
|
|
It cannot be applied to titles, summaries, front matter, repository values, or
|
|
theme variables. `page.body` is likewise created only by rendering a validated
|
|
page template and is emitted by the layout with `safe_page_body`.
|
|
|
|
Relationship `renderedContent` fields pass through the same sanitizer and
|
|
receive the same internal safe-value type. A composite project template may
|
|
therefore render configured devlogs without granting raw front matter access:
|
|
|
|
```liquid
|
|
{% for entry in page.relationships.devlogs %}
|
|
{{ entry.renderedContent | safe_content }}
|
|
{% endfor %}
|
|
```
|
|
|
|
The versioned Markdown profile required for the reference theme includes
|
|
headings with deterministic IDs, paragraphs, emphasis, links, lists, fenced and
|
|
inline code, blockquotes, and configured alert/callout syntax. The table of
|
|
contents is derived from the same sanitized heading model used to render HTML,
|
|
so its IDs cannot diverge from the document.
|
|
|
|
Raw Markdown and unsanitized HTML are not included in the template context.
|
|
When `rawHtmlPolicy` is `disabled`, source HTML is escaped as today. When it is
|
|
`sanitized`, source HTML passes through the same allowlist and all scriptable
|
|
elements, attributes, URL schemes, and CSS are removed. Sanitization happens
|
|
before the safe value is constructed.
|
|
|
|
## Theme loading and validation pipeline
|
|
|
|
`ThemeLoader.load(snapshot, themePath)` is a pure input-loading operation with
|
|
structured errors. `BuildEngine.build()` must not accept a caller-supplied
|
|
`ThemeConfig` claiming prior validation.
|
|
|
|
Validation runs in this order:
|
|
|
|
1. Parse and schema-validate the site, navigation, content-model, and project
|
|
configuration from the pinned site-definition snapshot.
|
|
2. Resolve each declared project's configured source snapshot. Reject content
|
|
that attempts to create a project without a declaration.
|
|
3. Validate project IDs, routes, metadata, relationship selectors, presentation
|
|
keys, and published-file mappings.
|
|
4. Canonicalize the configured theme path as a repository-relative POSIX path.
|
|
5. Reject empty paths, NUL bytes, drive prefixes, UNC paths, absolute paths,
|
|
backslashes, `.` segments, and `..` segments.
|
|
6. Resolve the theme root and verify its real path remains inside the repository
|
|
checkout.
|
|
7. Read `.theme/theme.yml` with size and UTF-8 checks.
|
|
8. Parse YAML with aliases disabled and reject duplicate mapping keys.
|
|
9. Validate against the closed `labyricorn-theme/v1` JSON Schema.
|
|
10. Resolve every configured section and detail template key through the
|
|
manifest. Missing mappings fail; no fallback key is selected.
|
|
11. Canonicalize every template, partial, local asset, Git font, and published
|
|
source path.
|
|
12. Walk the referenced trees with `lstat`. For symlinks, resolve the final real path
|
|
and reject links escaping the theme root or repository snapshot. Reject
|
|
devices, sockets, named pipes, and other non-regular files.
|
|
13. Confirm every required template, declared asset, Git font, and published
|
|
file exists and has the expected file type.
|
|
14. Resolve every HTTPS font declaration, verify its format, size, MIME type,
|
|
and pinned checksum, then assign its deterministic local release path.
|
|
15. Parse all templates and partials before rendering. Reject syntax errors,
|
|
unknown tags or filters, forbidden constructs, dynamic includes, and
|
|
includes outside `.theme/partials/`.
|
|
16. Validate the script, CSS, and external-asset policy against manifest
|
|
declarations.
|
|
17. Calculate SHA-256 checksums for configuration, the manifest, every template
|
|
and partial, every declared asset and font, and every published source file.
|
|
18. Copy assets, resolved fonts, and declared standalone files into a temporary
|
|
release directory. Generate the local font stylesheet.
|
|
19. Build config-authorized project, content, section, home, and 404 contexts and
|
|
render their explicitly selected templates.
|
|
20. Parse all rendered and copied HTML and CSS. Reject policy violations,
|
|
duplicate output paths, broken internal links, missing local assets, and
|
|
routes that escape the release root.
|
|
21. Calculate output checksums and the artifact checksum.
|
|
22. Rename the completed temporary directory to its immutable release path.
|
|
|
|
The Theme Management validation endpoint uses the same pinned site configuration
|
|
and runs steps 1 through 17 so it can verify configured template keys, fonts,
|
|
projects, and publications rather than validating an isolated manifest.
|
|
Validation never mutates `ThemeConfig` to manufacture success.
|
|
|
|
### Error model
|
|
|
|
Build-input and theme errors use stable codes in `ValidationReport.errors`:
|
|
|
|
| Code | Meaning |
|
|
| --- | --- |
|
|
| `E_PROJECT_CONFIG_INVALID` | Project declaration or metadata is missing or invalid |
|
|
| `E_PROJECT_SOURCE_UNRESOLVED` | A declared project source cannot be resolved |
|
|
| `E_PROJECT_TEMPLATE_UNRESOLVED` | A configured presentation key is absent from the theme manifest |
|
|
| `E_PROJECT_RELATION_INVALID` | A relationship selector or required relationship is invalid |
|
|
| `E_PUBLISHED_FILE_INVALID` | A published source file or mapping is missing, unsafe, or invalid |
|
|
| `E_PUBLISHED_ROUTE_COLLISION` | A published file collides with another output route |
|
|
| `E_THEME_SOURCE_UNRESOLVED` | Theme source or commit cannot be resolved |
|
|
| `E_THEME_ROOT_INVALID` | Configured theme root is unsafe or missing |
|
|
| `E_THEME_MANIFEST_INVALID` | YAML or schema validation failed |
|
|
| `E_THEME_PATH_ESCAPE` | A path or symlink escapes an allowed root |
|
|
| `E_THEME_FILE_MISSING` | A required template, partial, or asset is missing |
|
|
| `E_THEME_FONT_SOURCE` | A font source is unsafe, unavailable, malformed, or fails checksum validation |
|
|
| `E_THEME_TEMPLATE_INVALID` | Liquid parsing or strict-profile validation failed |
|
|
| `E_THEME_RENDER_FAILED` | Strict rendering or a resource limit failed |
|
|
| `E_THEME_SCRIPT_POLICY` | Script declaration or generated HTML violates policy |
|
|
| `E_THEME_STYLE_POLICY` | CSS declaration, inline style, or CSS URL violates policy |
|
|
| `E_THEME_EXTERNAL_ASSET` | An external asset is undeclared or disallowed |
|
|
| `E_THEME_OUTPUT_INVALID` | Generated routes, links, or local asset references are invalid |
|
|
|
|
Errors include the repository-relative file path when one is safe to disclose.
|
|
They never include checkout roots, credentials, environment data, or template
|
|
context values.
|
|
|
|
## Artifact layout and manifest
|
|
|
|
Declared local assets are copied without transformation to `/assets/`,
|
|
preserving their path relative to `.theme/assets/`. Resolved fonts use
|
|
deterministic generated paths, and project publications use their exact
|
|
configured routes:
|
|
|
|
```text
|
|
<release>/
|
|
├── index.html
|
|
├── articles/.../index.html
|
|
├── 404.html
|
|
├── assets/
|
|
│ └── ...
|
|
├── build-manifest.json
|
|
└── checksums.json
|
|
```
|
|
|
|
The builder does not minify, transpile, bundle, fingerprint, or normalize theme
|
|
assets or published-file contents in v1. The generated font stylesheet and
|
|
template output are deterministic builder products. This keeps the artifact a
|
|
transparent function of configured source bytes and builder version.
|
|
|
|
The illustrative tree also permits literal configured paths such as
|
|
`project/demo-project.html` and `project/demo-project/demo.html`, plus generated
|
|
`assets/styles/fonts.generated.css` and copied files below `assets/fonts/`.
|
|
|
|
`build-manifest.json` adds complete provenance:
|
|
|
|
```json
|
|
{
|
|
"protocol": "labyricorn-build/v1",
|
|
"builderVersion": "2.0.0-labyricorn",
|
|
"artifactBuildId": "sha256-...",
|
|
"generatedAt": "2026-07-23T12:00:00Z",
|
|
"site": {
|
|
"id": "labyricorn",
|
|
"repository": "ssh://git.example/site-definition.git",
|
|
"commit": "0123456789abcdef0123456789abcdef01234567",
|
|
"configChecksum": "sha256-..."
|
|
},
|
|
"sourceCommits": {
|
|
"content": "89abcdef0123456789abcdef0123456789abcdef"
|
|
},
|
|
"theme": {
|
|
"sourceId": "site-definition",
|
|
"repository": "ssh://git.example/site-definition.git",
|
|
"commit": "0123456789abcdef0123456789abcdef01234567",
|
|
"id": "labyricorn-default",
|
|
"version": "2.0.0",
|
|
"manifestChecksum": "sha256-...",
|
|
"templateChecksums": {
|
|
"templates/layout.liquid": "sha256-..."
|
|
},
|
|
"assetChecksums": {
|
|
"assets/styles/theme.css": "sha256-..."
|
|
},
|
|
"fonts": {
|
|
"playfair-display-500-italic": {
|
|
"sourceKind": "https",
|
|
"source": "https://fonts.gstatic.com/example/playfair-display.woff2",
|
|
"inputChecksum": "sha256-...",
|
|
"output": "assets/fonts/playfair-display-500-italic.woff2",
|
|
"outputChecksum": "sha256-..."
|
|
}
|
|
}
|
|
},
|
|
"projects": {
|
|
"demo-project": {
|
|
"sourceId": "demo-project",
|
|
"commit": "fedcba9876543210fedcba9876543210fedcba98",
|
|
"templateKey": "project",
|
|
"route": "/project/demo-project.html"
|
|
}
|
|
},
|
|
"publishedFiles": [
|
|
{
|
|
"projectId": "demo-project",
|
|
"source": "demo.html",
|
|
"route": "/project/demo-project/demo.html",
|
|
"mediaType": "text/html",
|
|
"inputChecksum": "sha256-...",
|
|
"outputChecksum": "sha256-..."
|
|
}
|
|
],
|
|
"routes": ["/", "/project/demo-project.html", "/project/demo-project/demo.html"],
|
|
"media": []
|
|
}
|
|
```
|
|
|
|
Repository URLs are normalized identifiers with user information and
|
|
credentials removed. Maps and route lists are serialized in lexical order with
|
|
the existing stable JSON writer. Checksums use lowercase SHA-256 hex prefixed
|
|
with `sha256-`.
|
|
|
|
`checksums.json` contains the checksum of every generated HTML file, copied
|
|
asset, and `build-manifest.json`. It does not checksum itself. The artifact
|
|
checksum remains SHA-256 over the canonical bytes of `checksums.json`, avoiding
|
|
a checksum cycle.
|
|
|
|
Template and asset checksums in the manifest are checksums of repository source
|
|
bytes. Output checksums in `checksums.json` are checksums of artifact bytes.
|
|
|
|
## Determinism requirements
|
|
|
|
Two builds are byte-identical when all of these values match:
|
|
|
|
- builder version;
|
|
- site-definition repository and commit;
|
|
- all required source repositories and commits;
|
|
- theme repository and commit;
|
|
- every configured Git and HTTPS font input checksum;
|
|
- rendering protocol versions; and
|
|
- explicitly versioned sanitizer and Liquid profiles.
|
|
|
|
To preserve that property:
|
|
|
|
- templates use the deterministic artifact build ID and `generatedAt`;
|
|
- files are read and written as bytes with explicit UTF-8 for text;
|
|
- emitted text uses LF line endings;
|
|
- directory enumeration, maps, routes, navigation, content, and checksums are
|
|
canonically sorted where semantic order is not configured;
|
|
- configured list order is preserved where order is semantic;
|
|
- locale-sensitive, host-sensitive, and timezone-sensitive formatting is
|
|
forbidden;
|
|
- generated files receive no timestamps or random identifiers in their content;
|
|
and
|
|
- the build run ID is never exposed to templates or written into artifacts.
|
|
|
|
A builder-version change is allowed to change output and must be visible in the
|
|
manifest.
|
|
|
|
## Configuration ownership and control-plane behavior
|
|
|
|
`site.yml`, navigation, project declarations, content models, source
|
|
definitions, style configuration, font declarations, and `.theme/` are
|
|
Git-owned. The backend loads them for each build from resolved pinned snapshots.
|
|
|
|
The v1 API behavior is:
|
|
|
|
- `GET /api/site-config` returns parsed configuration plus
|
|
`{ sourceId, repository, commit, readOnly: true }`.
|
|
- `PUT /api/site-config` returns HTTP 405 with
|
|
`{ code: "E_CONFIG_READ_ONLY" }`.
|
|
- `GET /api/theme` returns the latest resolved manifest, source repository,
|
|
commit, checksums, validation state, and validation errors. It must return an
|
|
explicit `unresolved` state when no snapshot has been loaded.
|
|
- `POST /api/theme/validate` resolves or accepts an already resolved commit and
|
|
runs validation steps 1 through 17, including configured template, font,
|
|
project, and publication references. Its response identifies every exact
|
|
commit validated.
|
|
|
|
The control plane:
|
|
|
|
- displays the site-definition source and full commit;
|
|
- displays the theme source, full commit, manifest checksum, and validation
|
|
result;
|
|
- removes the editable configuration textarea and “Save & Commit to Git”
|
|
control, or disables them with a clear read-only explanation;
|
|
- never says a Git commit was created unless a future backend workflow actually
|
|
creates and verifies one; and
|
|
- distinguishes “not resolved”, “invalid”, and “valid at commit”.
|
|
|
|
An actual commit or pull-request editing workflow requires a later change
|
|
proposal with authorization, conflict handling, review, audit, and credential
|
|
design.
|
|
|
|
## Staging, promotion, and recovery
|
|
|
|
A successful build creates:
|
|
|
|
```text
|
|
<build-root>/releases/<artifact-build-id>/
|
|
```
|
|
|
|
The directory is made read-only after its manifest and checksums are finalized.
|
|
The control plane may associate multiple build run IDs with the same artifact.
|
|
|
|
Staging atomically points `<build-root>/staging` to the release directory.
|
|
Promotion verifies `checksums.json`, then atomically points
|
|
`<build-root>/current` to that exact directory. It does not call the renderer,
|
|
recopy assets, change bytes, or create a second release.
|
|
|
|
Before either pointer update, the backend verifies:
|
|
|
|
1. the target resolves below `<build-root>/releases/`;
|
|
2. `build-manifest.json` and `checksums.json` exist;
|
|
3. all recorded output checksums match; and
|
|
4. the computed artifact checksum equals the build record.
|
|
|
|
On restart, the backend reconstructs staged and live state from the two pointers
|
|
and manifests rather than relying on in-memory booleans. A missing or invalid
|
|
target is reported as unhealthy and is never silently replaced.
|
|
|
|
The preview endpoint serves only files from the staging or live release. If no
|
|
valid release is selected, it returns HTTP 503 with
|
|
`E_RELEASE_UNAVAILABLE`. An unknown route returns the artifact's `404.html` with
|
|
HTTP 404. There is no synthesized HTML fallback.
|
|
|
|
## Implementation plan and migration gates
|
|
|
|
Each phase is independently mergeable. The production fallback is removed only
|
|
after the reference theme passes the full contract.
|
|
|
|
### Phase 1: Schemas and reference fixture
|
|
|
|
- Add `src/backend/theme/theme.schema.json`.
|
|
- Add a closed project-declaration schema and reference `projects.yml`.
|
|
- Add TypeScript types for `ThemeManifestV1`, `LoadedTheme`,
|
|
`RepositorySnapshot`, `ProjectDeclaration`, `PublishedFile`, `FontSource`, and
|
|
`ResolvedBuildInput`.
|
|
- Add `.theme/` beneath `packages/site-definition/` as the reference theme.
|
|
- Model the inspected project, article, blog, composite project, and standalone
|
|
demo routes in the reference fixtures.
|
|
- Add fixture themes under `src/backend/fixtures/themes/` for valid and invalid
|
|
contracts.
|
|
- Add LiquidJS and the chosen JSON Schema validator as pinned dependencies.
|
|
|
|
Gate: schema and fixture tests pass; no production rendering changes.
|
|
|
|
### Phase 2: Snapshot-based input loading
|
|
|
|
- Add `src/backend/repositories/repositorySnapshot.ts`.
|
|
- Add `src/backend/buildInputLoader.ts`.
|
|
- Resolve refs once, checkout exact commits, and load all Git-owned
|
|
configuration, including project declarations, relationships, publications,
|
|
and Git font sources, from snapshots.
|
|
- Separate build run state from immutable resolved input.
|
|
- Replace `"local"` provenance with the full site-definition commit.
|
|
|
|
Gate: tests prove a ref moving after resolution cannot change an in-progress
|
|
build, required source failures stop the build, and a restart can rediscover
|
|
release provenance.
|
|
|
|
### Phase 3: Theme loading and validation
|
|
|
|
- Add `src/backend/theme/themeLoader.ts`.
|
|
- Add `src/backend/theme/fontResolver.ts` with pinned Git and HTTPS resolution.
|
|
- Implement schema, explicit-template, project, relationship, published-file,
|
|
path, symlink, file, policy, font, and checksum validation.
|
|
- Return structured `ValidationReport` errors.
|
|
- Make `/api/theme/validate` call the real loader.
|
|
|
|
Gate: all configuration, path, manifest, and font security tests pass and
|
|
validation identifies every exact source commit and remote checksum.
|
|
|
|
### Phase 4: Strict Liquid rendering
|
|
|
|
- Add `src/backend/theme/liquidRenderer.ts`.
|
|
- Define and freeze the Liquid tag/filter allowlists and resource limits.
|
|
- Add `SafeHtml`, `safe_content`, and `safe_page_body`.
|
|
- Build the stable render context for home, explicit section and content views,
|
|
composite projects, relationships, table-of-contents data, and 404 routes.
|
|
- Resolve every page through its configured manifest key without a template
|
|
fallback.
|
|
- Use a whole-build migration feature flag; production keeps its current default
|
|
until the reference theme has parity.
|
|
|
|
Gate: reference-theme output passes route, safety, determinism, and snapshot
|
|
tests.
|
|
|
|
### Phase 5: Assets, output validation, and manifest v1
|
|
|
|
- Copy only declared assets, resolved fonts, and config-authorized standalone
|
|
files.
|
|
- Generate the local font stylesheet and expose it to the layout context.
|
|
- Add literal file-route output alongside directory-index routes.
|
|
- Parse rendered and copied HTML plus CSS and enforce script, style,
|
|
external-origin, local-asset, link, and route policies.
|
|
- Add theme and configuration provenance to `build-manifest.json`.
|
|
- Include copied assets, fonts, generated font CSS, and published files in
|
|
`checksums.json`.
|
|
|
|
Gate: changing any asset, font, or published source changes the artifact
|
|
checksum; undeclared or unsafe references fail before publication.
|
|
|
|
### Phase 6: Truthful control plane
|
|
|
|
- Replace synthetic `themeConfig` with loader results.
|
|
- Update Theme Management to show actual manifest files, source, commit,
|
|
checksums, and errors.
|
|
- Make site configuration read-only in the backend and UI.
|
|
- Distinguish operational build run IDs from artifact build IDs.
|
|
|
|
Gate: every status shown in Theme Management and Configuration is backed by the
|
|
resolved snapshot or an explicit unavailable state.
|
|
|
|
### Phase 7: Immutable staging and promotion
|
|
|
|
- Store releases by artifact build ID.
|
|
- Implement verified atomic staging and current pointers.
|
|
- Reconstruct active state on restart.
|
|
- Make live preview serve only the selected artifact.
|
|
|
|
Gate: staging and live resolve to the same release directory and checksum before
|
|
and after restart.
|
|
|
|
### Phase 8: Remove production fallbacks
|
|
|
|
- Remove the `layout()` HTML/CSS strings from `src/backend/buildEngine.ts`.
|
|
- Remove synthesized preview HTML from `server.ts`.
|
|
- Remove the synthetic startup theme from `src/backend/store.ts`.
|
|
- Remove the production feature flag for the old renderer.
|
|
- Update `README.md` and `docs/OPERATIONS.md`.
|
|
|
|
Gate: a repository without a valid `.theme/` fails; no production code path can
|
|
emit a built-in layout.
|
|
|
|
## Acceptance tests
|
|
|
|
The implementation is complete only when the following automated tests pass.
|
|
Unless otherwise stated, a failed build must leave no release directory and
|
|
must not change staging or live pointers.
|
|
|
|
### Reference-site parity
|
|
|
|
The reference fixture must demonstrate that a theme with the inspected site's
|
|
information architecture and presentation is possible without special renderer
|
|
code:
|
|
|
|
- `RP-01`: the fixture renders a dark editorial home feed, explicitly templated
|
|
project/article/blog sections, article and blog details, a composite project
|
|
detail, a declared standalone demo page, and a themed 404 page;
|
|
- `RP-02`: the project page contains configured version, license, tags, homepage,
|
|
stack, source commit, updated time, author, generated table of contents,
|
|
embedded devlogs, and related articles;
|
|
- `RP-03`: removing the project declaration removes the project page,
|
|
relationships, and standalone publication even when the repository still
|
|
contains matching files;
|
|
- `RP-04`: adding repository content without a matching declaration creates no
|
|
public project route;
|
|
- `RP-05`: each project, article, blog, and section route uses its exact
|
|
configuration-selected manifest template key;
|
|
- `RP-06`: the reference Markdown renders headings, code, lists, links, and
|
|
alert/callout blocks while producing matching deterministic contents links;
|
|
- `RP-07`: declared Git and HTTPS font faces are copied into the release, the
|
|
generated local font stylesheet contains the requested families, weights, and
|
|
styles, and rendered HTML contains no runtime Google Fonts dependency;
|
|
- `RP-08`: any number of declared font faces works within aggregate resource
|
|
limits and each face appears in manifest provenance;
|
|
- `RP-09`: the published `demo.html` bytes equal the pinned source bytes and are
|
|
served at `/project/demo-project/demo.html`;
|
|
- `RP-10`: desktop verification at 1440 CSS pixels shows the project metadata
|
|
and contents beside the main project content;
|
|
- `RP-11`: mobile verification at 390 CSS pixels shows usable navigation, moves
|
|
project metadata into a single-column flow, prevents horizontal overflow, and
|
|
preserves readable code blocks and relationship cards; and
|
|
- `RP-12`: the reference appearance is produced by theme templates, declared
|
|
CSS, local release fonts, and configured data only—no application renderer
|
|
contains reference-theme markup, colors, typography, or breakpoints.
|
|
|
|
### Contract and rendering
|
|
|
|
1. A valid reference theme renders home, every configured section and content
|
|
template, composite projects, declared files, and the 404 template.
|
|
2. Changing only declared theme CSS changes the copied CSS bytes, artifact
|
|
checksum, and browser-rendered appearance.
|
|
3. Changing one template changes the output routes that use that template and
|
|
leaves unrelated routes byte-identical.
|
|
4. Two builds from identical repository commits and builder version have
|
|
identical file lists, file bytes, `checksums.json`, and artifact checksum,
|
|
even when their build run IDs and execution times differ.
|
|
5. The manifest identifies the exact site-definition, content, and theme
|
|
repository commits, project declarations, published files, fonts, template
|
|
mappings, and all source checksums.
|
|
6. Stylesheet, generated font stylesheet, and script order in generated HTML
|
|
matches manifest order.
|
|
7. Undeclared files under `.theme/assets/` are not copied.
|
|
|
|
### Failure and filesystem safety
|
|
|
|
8. A missing `theme.yml` fails with `E_THEME_ROOT_INVALID` or
|
|
`E_THEME_FILE_MISSING`.
|
|
9. Each missing required template fails with `E_THEME_FILE_MISSING`; a missing
|
|
configured template key fails with `E_PROJECT_TEMPLATE_UNRESOLVED`.
|
|
10. Invalid YAML, an unknown manifest key, unsupported protocol, unsupported
|
|
engine, and invalid SemVer each fail with `E_THEME_MANIFEST_INVALID`.
|
|
11. Absolute paths, Windows drive paths, UNC paths, backslashes, NUL bytes, and
|
|
`..` traversal fail.
|
|
12. A symlink escaping `.theme/` or the repository snapshot fails with
|
|
`E_THEME_PATH_ESCAPE`; an internal symlink resolves only if its target
|
|
remains allowed.
|
|
13. A template include outside `partials/`, dynamic include, include cycle, or
|
|
excessive include depth fails.
|
|
14. Missing declared assets, fonts, published files, and duplicate declarations
|
|
fail.
|
|
15. A template exceeding resource limits fails without leaving a partial
|
|
release.
|
|
15a. An HTTPS font with a missing or wrong checksum, unsafe destination,
|
|
cross-origin redirect, incorrect MIME type, or format mismatch fails with
|
|
`E_THEME_FONT_SOURCE`.
|
|
15b. Published source traversal, symlink escape, special files, unsafe media
|
|
types, undeclared dependencies, and output collisions fail before copying a
|
|
release.
|
|
|
|
### Template and content security
|
|
|
|
16. An unknown Liquid variable and unknown filter fail in strict mode.
|
|
17. Templates cannot read environment variables, credentials, process state,
|
|
or files outside the partial resolver.
|
|
18. Theme-provided plug-ins, custom filters, and executable build files are
|
|
ignored and never executed.
|
|
19. Titles, summaries, tags, navigation labels, and front matter containing HTML
|
|
or Liquid syntax render as escaped text.
|
|
20. `safe_content` rejects any value other than the engine-created sanitized
|
|
content value.
|
|
21. Markdown content containing script elements, event handlers, `javascript:`
|
|
URLs, unsafe SVG, or CSS cannot inject script into output.
|
|
22. Scripts are rejected when `allowScripts` is false. When true, undeclared
|
|
local scripts, inline scripts, and event handlers are still rejected.
|
|
22a. Inline `<style>` elements and `style` attributes are rejected; CSS local
|
|
URLs must resolve to declared output files.
|
|
22b. Copied HTML and CSS receive the same script, style, URL, and external-asset
|
|
validation as rendered output.
|
|
23. External assets are rejected when `allowExternalAssets` is false.
|
|
24. When external assets are enabled, an origin absent from
|
|
`allowedExternalOrigins` is rejected.
|
|
|
|
### Provenance, UI, and promotion
|
|
|
|
25. A build reads configuration and theme files from the resolved commit even if
|
|
the branch advances before rendering.
|
|
26. A required repository that cannot resolve to a commit fails the build.
|
|
26a. Project pages and relationships use only the pinned project declaration and
|
|
source commits, and remain unchanged if refs advance during rendering.
|
|
26b. A verified remote font cache entry is addressed by checksum and produces
|
|
the same release bytes when its origin is unavailable.
|
|
27. `PUT /api/site-config` returns 405 and does not change subsequent builds.
|
|
28. Theme Management reports validation of actual files at the displayed
|
|
commits, configured template keys, fonts, projects, and publications; it
|
|
cannot turn invalid state valid by button click alone.
|
|
29. Staging and live point to the identical release directory and artifact
|
|
checksum after promotion.
|
|
30. Promotion detects any post-build byte modification and refuses activation.
|
|
31. Restarting the service reconstructs site, theme, staged, and live provenance
|
|
without relying on prior in-memory state.
|
|
32. A missing theme never invokes a production fallback renderer.
|
|
33. No production layout HTML or CSS remains embedded in
|
|
`src/backend/buildEngine.ts` or the live preview route in `server.ts`.
|
|
|
|
## Required verification commands
|
|
|
|
The final phase must expose these checks through the normal project scripts:
|
|
|
|
```bash
|
|
npm test
|
|
npm run lint
|
|
npm run build
|
|
```
|
|
|
|
`npm test` must include contract, security, determinism, snapshot, and promotion
|
|
tests. Integration fixtures must use local temporary Git repositories and must
|
|
not require network access or credentials.
|
|
|
|
## Rollout and rollback
|
|
|
|
Rollout uses a builder feature flag only during phases 4 through 7:
|
|
|
|
```text
|
|
LABYRICORN_THEME_RENDERER=repository
|
|
```
|
|
|
|
The flag selects the renderer for a whole build; mixed built-in/theme rendering
|
|
within one artifact is forbidden. Repository rendering is exercised in CI and
|
|
staging against the reference theme before it becomes the default.
|
|
|
|
After Phase 8, the flag and built-in renderer are deleted. Operational rollback
|
|
then means activating a previously verified immutable release or deploying the
|
|
previous application version. It never means silently rendering a missing theme
|
|
with embedded production HTML.
|
|
|
|
## Open follow-up work
|
|
|
|
The following require separate proposals and do not block this change:
|
|
|
|
- reviewed Git commit or pull-request editing from the control plane;
|
|
- additional theme protocol versions;
|
|
- asset compilation or image transformation;
|
|
- a richer component/slot contract;
|
|
- runtime content APIs; and
|
|
- cryptographic signing or attestations for build manifests.
|