IMPORTANT: Developer documentation for the current development branch. This content is unreleased, may change without notice, and must not be treated as Buildish release documentation.
Site Pipeline API and contract boundaries
For maintainer-facing notes about CLI-layer ownership, shared lower execution paths, and internal report/publication guardrails, see code maintenance.
The intended principle is:
The
site-pipelineexecutable is the only supported invocation API for stable planning, validation, staging, and local source-root discovery operations, specificallyplan,component-source-roots,check,build, andwatch; the staged tree and aggregate metadata are the supported output API for downstream renderers and deployment adapters.
Why this boundary exists
The architecture depends on a clean separation of responsibilities:
- the pipeline owns staging,
- the renderer owns rendering, and
- the consumer owns renderer-specific local development orchestration.
That split keeps the core pipeline portable, container-friendly, and independent of Hugo, Node-based renderers, themes, dev-server behavior, or publishing logic.
Stable invocation API
The stable control-plane API is the site-pipeline CLI.
The long-term stable commands are:
site-pipeline plansite-pipeline component-source-rootssite-pipeline checksite-pipeline buildsite-pipeline watch
Those commands are the supported way to:
- discover required local inputs before staging,
- discover existing effective component source roots for local wrappers or container mounts,
- validate the current workspace and local inputs before staging,
- trigger one-off staging,
- keep staged outputs fresh during local editing,
- run the pipeline in CI,
- invoke the pipeline from container images, and
- integrate the pipeline into editor tasks or consumer wrapper scripts.
The exact CLI surface can evolve, but changes to plan, check, build, and
watch should be treated as compatibility-sensitive API changes rather than
ordinary refactoring.
Stable validation command
site-pipeline check is the stable non-mutating validation command.
It should:
- load effective authored configuration and available local inputs
- run the same core structural, routing, publication, and metadata validation
rules that
builddepends on - aggregate independent diagnostics where practical instead of stopping at the first error
- support machine-readable output for automation
check should reuse the same resolve-and-validate execution path that build
and watch depend on, but stop before any stage-root mutation, file copying,
aggregate output writes, or watch-loop startup.
It must not:
- fetch from SCMs or providers
- mutate caches or local input trees
- write or partially write a staged output tree
By default, validation returns exit code 0 when no error diagnostics are
present, even if warning diagnostics are present.
The stable exit-code contract should be:
0: validation completed and satisfied the active failure threshold1: validation completed but failed the active failure threshold2: invalid invocation, invalid option combination, or unsupported requested report schema version3: internal failure while attempting to evaluate the workspace
site-pipeline check --fail-on warning is a recommended strict mode for callers
that want warning diagnostics to produce exit code 1 without changing the
machine-readable diagnostic severities themselves.
The public contract should stay within these low application-owned exit codes and avoid depending on shell- or signal-reserved ranges.
Recommended check CLI syntax
The stable CLI option names for check should use kebab-case because they are
command-line surface, not input/output model fields.
Recommended forms are:
site-pipeline checksite-pipeline check --fail-on errorsite-pipeline check --fail-on warningsite-pipeline check --workspace-root /workspace --catalog /workspace/buildish/site/catalog.yamlsite-pipeline check --report-format json --report-schema-version 1site-pipeline check --report-format json --report-schema-version 1 --report-output -site-pipeline check --report-format json --report-schema-version 1 --report-output .site-pipeline/check-report.json
Recommended flag meanings are:
--report-formatacceptstextorjson; default istext--report-schema-versionselects the machine-readable report schema version and is required when--report-format jsonis selected--report-outputselects the report destination;-means stdout and is the default--fail-onacceptserrororwarning; default iserror
Shared report flags
The stable machine-readable report flag family should be shared across all report-capable commands:
--report-format--report-schema-version--report-output
When --report-format json is selected, --report-schema-version is required.
Omitting it is an invalid invocation and should return exit code 2.
Path-bearing authored and emitted contract fields such as RepoRelativePath,
StageRelativePath, and PublicPath remain normalized POSIX strings and must
use forward slashes. CLI-local filesystem arguments such as --report-output
are different: they are machine-local paths and should be interpreted using the
host operating system’s native path rules. Relative CLI-local filesystem
arguments are interpreted relative to the process working directory.
That shared flag family should apply to:
site-pipeline plansite-pipeline component-source-rootssite-pipeline checksite-pipeline buildsite-pipeline watch
Failures before a command report exists
After the CLI has successfully parsed and validated a JSON report request, an
input, planning, or internal failure that prevents the normal command report is
written to the selected --report-output sink as a distinct failure envelope:
1{
2 "schemaVersion": 1,
3 "kind": "cliFailure",
4 "command": "check",
5 "exitCode": 1,
6 "error": {
7 "category": "input",
8 "code": "input-validation-failed",
9 "message": "Document does not satisfy its schema",
10 "source": "site/catalog.yaml",
11 "issues": [
12 {
13 "location": "components[0].slug",
14 "code": "required",
15 "message": "Field required"
16 }
17 ]
18 }
19}
kind: cliFailure deliberately distinguishes this envelope from successful or
domain-result planning, check, and stage-run reports. Automation should branch
on kind and stable error.category / error.code values rather than parsing
human message text. Input and expected planning failures use exit code 1,
invalid invocation uses 2, and unexpected internal failure uses 3. Failure
payloads omit raw input objects, tracebacks, and implementation exception names.
The complete machine-readable contract is exported as
cli-failure-report-v1.schema.json.
When an invocation error prevents the CLI from constructing a valid report
request, including an incomplete JSON flag combination, the CLI cannot safely
select that machine sink and writes a concise diagnostic to stderr instead.
Text-mode input failures likewise use concise stderr diagnostics with the
stable code, affected input name, and first actionable validation location.
Command-specific flags such as --fail-on may extend that shared base.
Shared human-log flags
The stable commands also share one human-log verbosity flag family:
--quiet--verbose--debug
These flags control only human-facing diagnostics. They must not rewrite or reformat explicit command outputs such as text reports, JSON reports, or unstable watch-event JSONL.
Recommended meanings are:
- default: write lifecycle/progress logs to
stderr --verbose: add informational diagnostics tostderr--debug: add detailed debugging diagnostics tostderr--quiet: suppress lifecycle/info/debug logs while still allowing warnings/errors onstderr
When callers pass conflicting verbosity flags, the CLI should prefer the most
informative mode: --debug over --verbose, and --verbose over --quiet.
The CLI should configure this logging policy centrally so future diagnostics do
not require each command to manage its own stdout versus stderr rules.
Shared workspace selection flags
The stable commands also share a workspace-selection flag family:
--workspace-root <path>--catalog <path>
Recommended meanings are:
--workspace-rootselects the authored workspace root used to resolve consumer-owned relative inputs such aslocalDir, top-level site content roots, and other repo-relative authored paths--catalogselects the authored catalog document to load- when
--catalogis omitted, the default catalog is<workspace-root>/site/catalog.yaml - the default provider snapshot path and the pipeline-owned
.stageand.site-pipeline-workdirectories are derived from the selected catalog’s parent directory
That split keeps the authored workspace and the catalog location separate. It is meant to support CI and local multi-repository layouts where the shared local checkout root is larger than the repository that holds the site catalog.
For example, this is a supported stable form:
site-pipeline build --workspace-root /workspace --catalog /workspace/buildish/site/catalog.yaml
Future operator-local path mapping is intentionally separate from the shared
catalog contract. --local-overrides <path> is reserved for that future work
but is not implemented yet; see the
maintenance backlog.
Stable component source-root command
site-pipeline component-source-roots is the stable non-mutating command for
enumerating effective component source-root locators.
It should:
- resolve component source roots through the same canonical source-root model the other stable commands use
- emit one normalized source-root locator per line on
stdout - emit authored catalog paths, and any future relative local overrides, relative
to
--workspace-root - emit any future absolute local override as an absolute path
- avoid filtering on current filesystem existence
- deduplicate repeated emitted locators in first-seen order
That command is meant for shell-facing local wrappers and container adapters that need to mount or otherwise pass through the effective component source trees without reimplementing catalog parsing or future operator-local overrides.
Recommended forms are:
site-pipeline component-source-rootssite-pipeline component-source-roots --workspace-root /workspace --catalog /workspace/buildish/site/catalog.yaml
Stable planning command
site-pipeline plan is the stable non-mutating planning command.
It should:
- emit machine-readable planning data such as a resolved materialization report,
- help consumers discover missing or stale local inputs before staging, and
- do not fetch from SCMs or mutate caches themselves.
For automation, callers should be able to request a specific planning-report schema version explicitly.
Recommended behavior is:
- callers select an explicit planning target such as
buildorwatch - callers may request a report schema version explicitly
- the output includes the effective
schemaVersion - unsupported requested versions fail fast
- missing or stale inputs are represented in the report payload rather than in ad hoc exit-code conventions
site-pipeline plan answers what local inputs are needed. It complements
site-pipeline check, which answers whether the current workspace and currently
available local inputs are valid and ready for staging.
That planning universe should match the inputs actually consumed by build and
watch, including consumer-owned top-level site pages, site assets, vendor
asset trees, and component/version-context trees.
That keeps planning visible without turning build or watch into source-sync
commands.
Planning exit codes should be:
0: planning completed and produced a report, even if entries aremissing,stale, orunresolved1: planning completed but found error diagnostics that prevent a usable plan2: invalid invocation, invalid option combination, or unsupported requested report schema version3: internal failure while attempting to evaluate the workspace
Recommended plan CLI syntax
Recommended forms are:
site-pipeline plan --for buildsite-pipeline plan --for watchsite-pipeline plan --for build --workspace-root /workspace --catalog /workspace/buildish/site/catalog.yamlsite-pipeline plan --for build --report-format json --report-schema-version 1site-pipeline plan --for build --report-format json --report-schema-version 1 --report-output -site-pipeline plan --for build --report-format json --report-schema-version 1 --report-output .site-pipeline/materialization-report.json
Recommended flag meanings are:
--foracceptsbuildorwatchand selects the planning target--report-formatacceptstextorjson; default istext--report-schema-versionselects the machine-readable report schema version and is required when--report-format jsonis selected--report-outputselects the report destination;-means stdout and is the default
Stable staging commands
site-pipeline build and site-pipeline watch are the stable stage-producing
commands.
They should:
- use the same resolve-and-validate execution path that
checkuses - consume the same full local-input universe that
planreports, including consumer-owned top-level site pages, site assets, vendor asset trees, and component/version-context trees - emit the staged tree and
manifest.jsonon successful stage production - optionally emit machine-readable run reports when requested via the shared report flags
Recommended behavior is:
buildemits oneStageRunReportfor the completed runwatchrewrites one JSONStageRunReportat the selected report-output path via same-directory temporary write plus atomic replace after the initial cycle and after each subsequent rebuild cycle- on an ordinary watch-cycle failure that does not invalidate the last finalized
stage,
watchkeeps running and reports the failed cycle instead of exiting - if a watch cycle leaves the stage root structurally corrupt, partially
finalized, or otherwise untrustworthy,
watchfails hard and exits rather than serving a dubious stage - if the initial cycle cannot produce a trustworthy stage and there is no prior
trustworthy stage to retain,
watchexits with code3instead of entering steady-state watch mode watch --report-format jsonrequires--report-outputto be a file path, not-, because watch mode is long-running and produces repeated cycle reports over time
Build exit codes should be:
0: build completed and produced a finalized stage, even if warnings were emitted1: build completed evaluation but found error diagnostics or staging failures that prevented a usable finalized stage2: invalid invocation, invalid option combination, or unsupported requested report schema version3: internal failure or unrecoverable stage-integrity failure
Watch exit codes should be:
0: orderly application-controlled shutdown after the watch loop has started2: invalid invocation, invalid option combination, or unsupported requested report schema version3: internal failure or unrecoverable stage-integrity failure, including cases where the process can no longer guarantee that the current stage root is coherent and trustworthy
watch should not normally use application-owned exit code 1; ordinary
per-cycle validation or staging failures should be reported in the refreshed
StageRunReport while the process keeps watching for a future successful cycle.
An initial-cycle failure with no trustworthy retained stage is not an ordinary
per-cycle failure; it is a hard failure and therefore exits with code 3.
If a long-running command is terminated by an external signal, any signal-derived
shell exit status is outside the application-owned 0-through-3 contract.
Recommended build and watch CLI syntax
Recommended forms are:
site-pipeline buildsite-pipeline build --workspace-root /workspace --catalog /workspace/buildish/site/catalog.yamlsite-pipeline build --report-format json --report-schema-version 1site-pipeline build --report-format json --report-schema-version 1 --report-output -site-pipeline build --report-format json --report-schema-version 1 --report-output .site-pipeline/build-report.jsonsite-pipeline watchsite-pipeline watch --quietsite-pipeline watch --workspace-root /workspace --catalog /workspace/buildish/site/catalog.yamlsite-pipeline watch --verbosesite-pipeline watch --report-format json --report-schema-version 1 --report-output .site-pipeline/watch-report.jsonsite-pipeline watch --unstable-events jsonlsite-pipeline watch --unstable-events jsonl --debug --report-format json --report-schema-version 1 --report-output .site-pipeline/watch-report.json
Recommended flag meanings are:
--report-formatacceptstextorjson; default istext--report-schema-versionselects the machine-readable report schema version and is required when--report-format jsonis selected--report-outputselects the report destination; forbuild,-means stdout and is the default; forwatch, JSON report output should be a file path that the command rewrites after each completed cycle. That file path is machine-local and follows host-native path semantics.watchhuman logs follow the shared logging policy: default lifecycle logs, extra info under--verbose, deeper detail under--debug, and reduced chatter under--quiet
Unstable watch event stream
site-pipeline watch also supports one explicitly unstable machine-readable
event stream for local orchestration helpers:
--unstable-events jsonl--unstable-events-output -|<path>(optional; defaults to-for stdout)
This stream is intentionally separate from the stable report contract.
Consumers should treat it as a convenience API for local wrappers such as
make serve, not as a long-term compatibility promise.
Current stream rules are:
- machine-readable events are written as one JSON object per line to the sink
selected by
--unstable-events-output --unstable-events-output -keeps the stream onstdout- when
--unstable-events-outputpoints at a file, that path uses the same host-native path semantics and output-path safety rules as--report-outputand is written directly bysite-pipeline - human-facing watch logs are written through the shared logger to
stderr - non-event output must not be mixed into the JSONL stream when the sink is
stdout; the CLI therefore keeps the final human report onstderrin that mode - when the event sink is
stdout,watchalso performs a best-effort stdout guard by redirecting ordinary Python-level stdout writes tostderrwhile the machine stream ownsstdout; consumers still must tolerate malformed or foreign lines defensively because native code or third-party dependencies can bypass that guard - consumers should discard malformed JSONL lines defensively instead of treating one bad line as a fatal protocol guarantee
Current event types are:
ready: emitted once after the initial watch cycle has produced a consumer-safe stagecycle-succeeded: emitted after each successful watch cyclecycle-failed: emitted after a failed watch cycle when the process retains or reports the last trustworthy stage state
All current events include:
event: event discriminator stringcycle: watch-cycle numberstageRootPath: absolute visible-stage path when knownmanifestPath: absolute stagedmanifest.jsonpath when known
Cycle outcome events also include:
statussucceededwroteStagestageUsableerrorCountwarningCountinfoCount
The current implementation models those payloads internally via one base
WatchEvent type and concrete WatchReadyEvent, WatchCycleSucceededEvent,
and WatchCycleFailedEvent variants so JSON serialization stays centralized and
reviewable.
Stable outputs API
The stable data-plane API is the staged output contract.
That means downstream consumers should integrate against:
manifest.jsonas the staged-tree entry point,- the staged content tree,
- page-local front matter produced by the pipeline,
- aggregate metadata files such as routes, lifecycle, releases, and related indexes, and
- the documented staged directory layout.
Renderers and deployment adapters should treat those staged outputs as the integration boundary rather than reading arbitrary component repositories or calling internal Python code.
What is not a public API
The Python implementation is internal.
This means the following are not public or stable APIs:
- Python modules, classes, and functions in the package
- internal build graph or staging internals
- implementation-specific provider integration code
- internal filesystem or watch-loop helpers
- any future plugin or hook machinery unless documented separately as public
Consumers should not import Site Pipeline internals and should assume they may change at any time.
preview is not part of the stable contract
preview is a debugging convenience, not a stable architectural commitment.
It may remain useful for:
- Site Pipeline development,
- staging-contract debugging, or
- consumer integration debugging when no real renderer dev server is available.
But it is intentionally not the main development story. It is not guaranteed to:
- behave like the real published site,
- remain feature-compatible over time, or
- continue to exist indefinitely.
Consumers should not build their local development architecture around
site-pipeline preview.
Why there is no stable serve command
The Site Pipeline should not define a stable serve command.
A real development server depends on renderer-specific behavior such as:
- how templates are compiled,
- how Markdown, AsciiDoc, Mermaid, or shortcodes are rendered,
- how hot reload works,
- how themes and static assets are resolved, and
- how the dev server watches files and reports errors.
Pulling that into site-pipeline would blur the intended architecture by making
the pipeline responsible for rendering concerns it does not own.
That is why a first-class serve command is intentionally out of scope for the
core pipeline API.
Recommended local development pattern
The preferred local development model is:
- run
site-pipeline watch, - run the consumer’s real renderer dev server, and
- let the renderer react to staged output changes.
This gives developers a realistic preview of the eventual site while preserving the staging/rendering separation.
Consumer-owned wrappers such as make serve are fine when they orchestrate that
workflow, but those wrappers should remain consumer-specific rather than turning
into core Site Pipeline API.
Relationship to distribution
This contract is intentionally friendly to both:
- an installed Python package that exposes the
site-pipelineexecutable, and - a container image whose entrypoint is
site-pipeline.
In both cases, callers use the same invocation API and receive the same staged output contract.
Future integrations and add-ons
Renderer-specific examples, wrapper scripts, or helper tooling may be useful.
But those should be treated as isolated integrations layered on top of the core contracts, not as reasons to widen the core API boundary.
Read next
- architecture overview for the high-level system picture
- staged-output-contract.md for the staged-tree
layout and
manifest.jsoncontract - source resolution and materialization for version selection and materialized content inputs
- validation-and-check.md for non-mutating validation,
diagnostics, and
site-pipeline check - build architecture for the recommended execution
shape of
buildandwatch - code maintenance for maintainer-facing internal boundaries and watch/publication guardrails
- security-and-trust-model.md for path-safety, trust-boundary, and XSS-defense expectations
- flexible-component-publication.md for the publication and lifecycle model