Status: Active Release Baseline Product:
sc-composer(library) andsc-compose(CLI) Document role: Normative release requirements for both crates
This document supersedes the prior high-level placeholder. It is the normative
release requirements baseline for sc-compose v1.4.1.
This document defines the required behavior of sc-composer and sc-compose.
It is the design authority for release work. If the implementation diverges
from this document, the implementation is wrong unless the document is
explicitly amended.
Teams need one deterministic composition engine for prompt profiles, instruction templates, and composed prompt output across multiple AI runtimes. Without a shared implementation, include handling, variable validation, discovery conventions, and diagnostics drift across callers.
sc-composer exists to provide one reusable implementation for:
- prompt and profile file resolution,
- Jinja2-style template rendering,
- include expansion,
- variable declaration and validation,
- deterministic composition output,
- machine-readable diagnostics.
The product has two deliverables:
- Library crate:
sc-composer - CLI binary crate:
sc-compose
The library is the semantic source of truth. The CLI is a thin interface over the library.
The Phase P generated-Go strategy is governed by ADR-0020: Generated Go Binding Strategy, which is Accepted. The native libraries are distributed as the target-specific release bundles documented by the Go integration guide; a Go module download alone does not include those native libraries.
The repository MUST publish a generated Go module for the canonical sc-sha
hash operations so external consumers can use the shared hash contract without
copying its implementation. The module MUST use the documented submodule path
github.com/randlee/sc-compose/bindings/sc-sha-go, generated UniFFI source, and
the matching packaged native artifact for each supported target (Linux/amd64,
macOS/arm64, and Windows/amd64). Release validation MUST cover generated
source drift, native artifact selection, conformance vectors, and an
independent Go consumer. The adapter MUST depend only on sc-sha and MUST NOT
add ATM, resolver, filesystem, or duplicate hash logic.
This repository is intentionally independent from ATM and any other orchestration runtime.
- No
ATM_HOMEenvironment variable may be referenced anywhere in this repo. - No
agent-team-mail-*crate may appear in anyCargo.tomlin this repo. - No ATM spool, socket, mailbox, or runtime path convention may be assumed.
- No
use atm_*::...oruse agent_team_mail::...imports may appear in the library or CLI crates. - Any ATM integration belongs in adapters outside this repository rather than
in
sc-composerorsc-compose.
sc-composermust remain runtime-agnostic.sc-composermust not depend on mailbox formats, daemon lifecycle behavior, team coordination state, or runtime-specific home-directory conventions.sc-composemust be usable as a standalone tool without any external orchestration runtime.- If an external system needs integration-specific behavior, that adaptation must live outside this repository rather than inside the core composition semantics.
The initial product explicitly does not provide:
- daemon control or process management,
- mailbox handling or message routing,
- team configuration or ATM runtime management,
- network I/O or remote template fetching,
- ATM-specific file path conventions or runtime lookup behavior.
- The engine must support plain text and markup source files, including
.txt,.md, and.xml. - The engine must support template files ending in
.j2, including typed variants such as.md.j2,.txt.j2, and.xml.j2. - Any filename ending in
.j2must be treated as a template. - Files may begin with YAML frontmatter.
- Frontmatter is optional.
Frontmatter must support this schema:
pass: 1
required_variables:
- variable_name
variables:
variable_name:
required: true
defaults:
variable_name: value
input_defaults:
variable_name: fallback
metadata:
key: value
json_escape_mode: autoSchema rules:
required_variablesis optional.passis optional and identifies an explicit pass number for stacked frontmatter blocks.defaultsis optional.input_defaultsis accepted as an alias fordefaultsin frontmatter.- For compatibility with existing template metadata, a frontmatter
variablesmap with{ required: true }declarations is accepted as an equivalent spelling ofrequired_variables. metadatais optional.json_escape_modeis optional for JSON templates and accepts onlyautoorlegacy.autois the default;legacyis an explicit compatibility mode for manually quoted string placeholders and emits a migration warning.- The recognized top-level frontmatter keys are
pass,required_variables,variables,defaults,input_defaults,metadata, andjson_escape_mode. When scanning stacked frontmatter, a later block containing an unrecognized top-level key is treated as template body content. - If a frontmatter block exists and a field is omitted, it defaults to:
required_variables: []defaults: {}metadata: {}
- If no frontmatter block exists at all, the file is treated as having no declarations and no defaults.
required_variablesvalues must be unique variable names.defaultssupplies optional values that become part of the render context unless overridden by environment-derived or explicit input values.- If both
defaultsandinput_defaultsappear in the same frontmatter block,input_defaultswins for overlapping keys and validation emits aWARN_VAL_CONFLICTING_DEFAULT_SECTIONSwarning diagnostic. metadatais descriptive only. It must not directly change render semantics unless a future requirement explicitly assigns meaning to a metadata key.
- JSON templates use complete-value
autointerpolation by default. Bare placeholders own their JSON quoting and preserve scalar, object, array, and null types. - An explicit
legacymode safely escapes string contents without adding surrounding quotes already present in manually quoted source. - CLI mode selection takes precedence over root frontmatter, followed by the
default
automode. validateandvalidate --lintemit the migration-directed warning for legacy mode or quoted placeholders detected in JSON context:Template uses legacy JSON escape mode. Migrate to bare placeholders (auto mode) to avoid double-quoting issues. See docs/migration/json-escape-mode.md- A JSON render must not emit output until the complete body parses successfully.
- The parser gate applies to ordinary
renderas well asrender --check-renderand runs before stdout, file, dry-run preview, or JSON-envelope emission. validateremains static-only and must report that state explicitly. Thevalidate --check-rendervariant renders in memory with the supplied exact context, reports the checked-render state, and emits no body or file.- A malformed rendered JSON body must fail closed with the stable
ERR_RENDER_JSON_MALFORMEDdiagnostic. The diagnostic includes template, line, column, and byte offset, but never echoes rendered values. - Source lint uses the canonical
ERR_JSON_MODE_CONTRACTdiagnostic for an auto-mode quoted scalar placeholder. Ambiguous quoted expressions useWARN_JSON_QUOTED_PLACEHOLDERas a conservative finding instead of being silently treated as safe. The existingWARN_JSON_LEGACY_ESCAPE_MODEmigration warning remains emitted by static validation for legacy mode or detected quoted placeholders.
The render-context value model accepts any finite JSON/YAML-compatible tree
that the existing serde_json::Value and Minijinja context can represent.
The six Phase O.4 repository assignment templates are explicit auto-mode
consumers of this contract. Their scalar interpolation slots and scalar loop
elements are bare; carry_forward_findings_json is the only reviewed raw-JSON
fragment and must contain a validated JSON array. The O.4 semantic fixture
corpus is the acceptance evidence for quotes, backslashes, Unicode, newlines,
empty and optional values, arrays, objects, null branches, and injection-safe
control characters. The migration matrix and legacy exception are maintained
in docs/migration/json-escape-mode.md.
The Phase O.5 release gate extends this contract across consumer repositories:
-
every release-candidate campaign reads a source-of-truth inventory of repository roots and pinned commits, verifies each commit before scanning, and reports the actual JSON-template count and every path;
-
an unavailable or unpinned root blocks an unconditional release claim;
-
a successful JSON render is a valid campaign PASS only after the complete emitted body is parsed as one JSON document; parser failure, partial output, timeout, or success-status/body mismatch is fail-closed evidence;
-
the original 1.4.0 quoted-placeholder shape remains a permanent negative fixture: auto mode rejects it before emission, while explicit legacy mode produces one safely escaped string and one deprecation diagnostic;
-
external findings are owned by the external repository and require a separately merged migration/fix before the release gate can become green.
-
Variables used by template rendering must be one of:
- string
- number
- boolean
- null
- an object/map with string keys, recursively containing supported values
- a sequence recursively containing supported values, including arrays,
objects, scalars,
null, and jagged shapes
-
The top-level
--var-filedocument remains a JSON/YAML object and YAML map keys remain strings; these ingress boundaries are independent of nesting depth. -
metadatamay contain arbitrary YAML values because it is descriptive only and does not participate in rendering semantics.
HTML-Report follow-on design track:
- FR-12 through FR-15 are implemented by Phase HTML-Report.
- The remaining design exploration in
docs/html-sprint-report-plan.md is limited to
H5-and-later work such as multi-panel HTML/XHTML composition, wrapper-level
output viewing behavior, and possible post-render-hook design that stays
outside the core
sc-composecontract unless explicitly accepted later.
- Profile and prompt assets must support both plain files and template files.
- Within a candidate directory, resolver probe order for agent and command files
must be:
<name>.md.j2<name>.md<name>.j2
- Skill probe order must be:
<name>/SKILL.md.j2<name>/SKILL.md<name>/SKILL.j2
- CLI
renderandvalidatemust accept explicit template paths anywhere under the configured root, including nested skill templates.
- Bundled examples and user templates use different on-disk layouts.
- Bundled examples are stored on disk as flat
*.j2files directly under the examples root. - Example names are derived from the template filename by removing the trailing
.j2suffix and then one remaining source extension when present. Examples:hello.md.j2->helloservice-config.yaml.j2->service-config
- Derived bundled example names must be unique. If two flat example files normalize to the same name, the examples root is invalid until the collision is removed.
- User templates are stored as one subdirectory per template under the user templates root.
- A user template directory name is the template name.
- A user template directory may contain one or more files, including one or
more
.j2templates and supporting assets. template.jsonis optional for user template directories. If present, it is user-facing metadata and may contain only:descriptionversioninput_defaults
input_defaultsmay provide default render inputs using supported render-context value types.template.jsonmust not introduce alternate render semantics, hook execution, or manifest-owned entrypoint selection in the initial release.- The CLI treats each normalized bundled example entry as a single-template example pack even though the on-disk layout is a flat file.
- Named render from
sc-compose examples <name>resolves the matching flat example-pack file under the examples root. - Named render from
sc-compose templates <name>is defined only when the template directory contains exactly one root-level*.j2file. - Template directories with zero or multiple root-level
*.j2files remain listable and addable, but they are not implicitly renderable by name in the initial release.
- Final render context precedence must be:
- explicit input variables,
- environment-derived variables,
- built-in render-context variables,
- user-template
input_defaults, - frontmatter defaults.
- Frontmatter-declared
required_variablesmust be evaluated after the merge. - Variables present only in
defaultsare optional by default. - A variable may appear in both
required_variablesanddefaults; in that case the default value satisfies the requirement unless overridden. - An empty sequence value such as
[]is valid input and satisfies a required variable when provided explicitly or by defaults. validateandrender --dry-runmust emit an informational diagnostic when a referenced or required variable is satisfied by a default value rather than explicit caller input.- Explicit CLI
--var key=valueinputs are always strings. - This string-only behavior is intentional: CLI text inputs are not coerced
based on their spelling. Callers that need numeric, boolean, null, object,
or sequence values must use
--var-fileor template-owned defaults. - Variables loaded through
--var-filemay be any supported render-context value type. - Variables loaded through
--env-prefixare always strings. - After the final context merge, every referenced variable path must either have a value binding or be handled by the unbound-variable policy described in FR-2a. A missing binding is distinct from an undeclared token and from an extra caller-provided variable.
ComposePolicy.unbound_variable_policycontrols referenced-but-unbound paths witherror,warn, orignoreseverity. When unset, it inheritsunknown_variable_policyfor compatibility; an explicit value keeps the two policy axes independent.- If frontmatter is absent:
- the engine must discover referenced variables from the template and include graph,
validatemust emit a generated-frontmatter recommendation,- diagnostics must include a direct fix command:
sc-compose frontmatter-init <file>.j2.
- Every render context must inject these built-in variables when the caller
does not supply them:
TEMPLATE_NAMEHOSTNAMEUSERNAMERENDER_DATERENDER_TIMESTAMP
- Built-ins sit below explicit caller inputs and environment-derived values, and above template-owned defaults.
TEMPLATE_NAMEmust reflect the resolved template filename actually rendered, not a caller alias.- Caller-provided values always win:
- explicit input values override built-ins,
- environment-derived values override built-ins,
- template-owned defaults do not override built-ins.
Referenced tokens that are not declared in frontmatter must follow these rules:
- Default behavior:
- they remain preserved in rendered output,
- they do not become implicitly required variables,
- they produce diagnostics in both
renderandvalidate.
- Strict behavior:
- validation fails,
- rendering fails,
- diagnostics identify the undeclared referenced tokens.
This behavior is distinct from missing required variables. A token that is
undeclared is not automatically treated as required unless it is explicitly
listed in required_variables.
Referenced-but-unbound tokens are a separate axis from undeclared tokens:
- A referenced token with no binding after the final context/default merge
emits
ERR_VAL_UNBOUND_VARIABLEaccording tounbound_variable_policy. - A referenced token may be declared in frontmatter and still be unbound; the
unbound-variable policy applies independently of
strict_undeclared_variables. - A referenced token may be undeclared but bound by caller input; the strict undeclared-token policy applies independently and the value is not reported as unbound.
- Loop locals,
{% set %}locals, built-in render-context variables, and nested paths satisfied by a merged object value count as bindings.
- Missing frontmatter-declared required variables must fail rendering.
- Undefined-variable render failures and undeclared-token diagnostics must use distinct stable diagnostic codes.
- Missing-variable diagnostics must include:
- the full set of missing variable names,
- the file in which each variable became required,
- line and column when available,
- the include chain when applicable.
- Extra input variables not declared by the template or frontmatter must be
policy-controlled with
error,warn, orignore.
- The engine must support inline include directives in the form
@<path>. - Include resolution order must be:
- path relative to the containing file,
- path relative to the configured root.
- Nested includes must support:
- cycle detection,
- bounded maximum depth,
- deterministic expansion order.
- Included templates must be evaluated under the same context and validation policy as their parent template.
- Include expansion must be applied consistently whether rendering to stdout or to a file.
- Include failures must produce actionable diagnostics with include-chain context.
- A file's own frontmatter applies to that file.
- Required-variable declarations discovered from included files participate in validation of the overall composition result.
- Defaults declared in included files participate in context construction unless overridden by parent-file defaults, environment-derived variables, or explicit input variables.
- If multiple files declare a default for the same variable, precedence must be:
- explicit input variables,
- environment-derived variables,
- including file defaults,
- included file defaults discovered deeper in the include graph.
metadatafrom included files must be preserved in trace data only if the library exposes include metadata in a future API. Metadata must not affect current render semantics.
- File reads must be confined to a configured root by default.
- Path traversal outside the allowed root set must fail.
- Callers may optionally provide additional allowed roots.
- Template rendering must not execute arbitrary host code.
The resolver must support file mode and profile mode.
In file mode:
- the caller provides an explicit path,
- no precedence search is performed.
In profile mode:
- the caller provides a profile kind and name,
- the caller may provide a runtime or omit it,
- the resolver searches runtime-specific and shared locations according to a configured path policy.
Runtime-specific directories:
.claude/agents/.claude/commands/.claude/skills/.hermes/agents/.hermes/commands/.hermes/skills/.codex/agents/.codex/commands/.codex/skills/.gemini/agents/.gemini/commands/.gemini/skills/.opencode/agents/.opencode/commands/.opencode/skills/
Shared directories:
.agents/agents/.agents/commands/.agents/skills/
Default runtime search order for agents:
claude:.claude/agents/<name>->.agents/agents/<name>hermes:.hermes/agents/<name>->.agents/agents/<name>->.claude/agents/<name>codex:.codex/agents/<name>->.agents/agents/<name>->.claude/agents/<name>gemini:.gemini/agents/<name>->.agents/agents/<name>->.claude/agents/<name>opencode:.opencode/agents/<name>->.agents/agents/<name>->.claude/agents/<name>
Default runtime search order for commands:
claude:.claude/commands/<name>->.agents/commands/<name>hermes:.hermes/commands/<name>->.agents/commands/<name>->.claude/commands/<name>codex:.codex/commands/<name>->.agents/commands/<name>->.claude/commands/<name>gemini:.gemini/commands/<name>->.agents/commands/<name>->.claude/commands/<name>opencode:.opencode/commands/<name>->.agents/commands/<name>->.claude/commands/<name>
Default runtime search order for skills:
claude:.claude/skills/<name>/->.agents/skills/<name>/hermes:.hermes/skills/<name>/->.agents/skills/<name>/->.claude/skills/<name>/codex:.codex/skills/<name>/->.agents/skills/<name>/->.claude/skills/<name>/gemini:.gemini/skills/<name>/->.agents/skills/<name>/->.claude/skills/<name>/opencode:.opencode/skills/<name>/->.agents/skills/<name>/->.claude/skills/<name>/
Ambiguity contract:
- If a runtime is explicitly provided, only that runtime path chain is used.
- If a runtime is omitted, the resolver must evaluate all configured runtime and shared roots.
- If multiple candidates match, resolution must fail with an actionable ambiguity error requiring an explicit runtime selector.
- If exactly one candidate matches, the resolver may select it without an explicit runtime.
There is no flat shared fallback such as .agents/<name>. Shared prompts live
only under .agents/agents/, .agents/commands/, and .agents/skills/.
The resolver path policy must be configurable by callers and must not be hardcoded into downstream integrations.
Final composed output must concatenate blocks in this fixed order:
- resolved profile body,
- guidance block,
- user prompt block.
Each block may be empty. Ordering is never caller-defined.
sc-compose must provide these commands:
renderresolvevalidatebead {render|validate|preview-pour|pour} --request <JSON> --jsonfrontmatter-inittemplate-initinitverifyextractobservability-healthexamplestemplatesreportshelp(see FR-22)
The CLI must support:
--mode <profile|file>--kind <agent|command|skill>--agent <name>--agent-type <name>as an alias for--agent--runtime <claude|hermes|codex|gemini|opencode>as an optional runtime selector--ai <claude|hermes|codex|gemini|opencode>as an alias for--runtime--var key=valuerepeatably--var-file <path|->--env-prefix <PREFIX_>--strict--unknown-var-mode <error|warn|ignore>- controls both extra caller-provided variables and referenced-but-unbound variables; the CLI maps the selected mode to both policy axes.
--root <path>--file <path>--output <path>where applicable--guidance <text>--guidance-file <path|->--prompt <text>--prompt-file <path|->--json--dry-run--forcewhere applicable--pass Nrepeatably where applicable--against <path>where applicable--quietwhere applicable--builtin-var KEY=VALUErepeatably where applicable
Command behavior:
render- renders one resolved template or profile,
- writes to stdout by default,
- may write to a file when requested,
- must honor validation and strictness policy,
- accepts optional guidance and user prompt blocks.
resolve- is defined for
profilemode, - prints the selected profile path,
- reports attempted search paths,
- fails in
filemode.
- is defined for
validate- performs full include expansion and variable analysis,
- does not write output files,
- exits non-zero on validation failure.
frontmatter-init- discovers referenced variables,
- prepends minimal frontmatter,
- fails if frontmatter already exists unless
--forceis provided.
template-init- converts a concrete file into a templated file using one or more pass
groups declared with
--pass N, - accepts
--var key=valuereplacements within each pass group, - accepts
--forceto overwrite an existing frontmatter/template header, - supports
--dry-runwithout writing the rewritten file, - exits with code
3when declared literal values are not found in the source file because that outcome is a usage/configuration failure rather than a successful drift result.
- converts a concrete file into a templated file using one or more pass
groups declared with
init- creates
.prompts/, - ensures
.prompts/is ignored by Git, - scans repository templates,
- validates discovered templates,
- fails if invalid templates are found,
- prints recommendations for missing or weak frontmatter.
- creates
observability-health- reads the current CLI logger health state without mutating composition or log configuration,
- prints a human-readable health summary by default,
- emits the documented JSON schema when
--jsonis provided.
examples- supports:
examples listexamples <name>for implicit named render
- resolves example packs from the bundled examples root,
- uses the same render flags and output semantics as
renderfor implicit named render.
- supports:
templates- supports:
templates listtemplates add <src> [name]templates <name>for implicit named render
- resolves template packs from the user templates root,
- uses the same render flags and output semantics as
renderfor implicit named render, - allows
addfrom either a single file or a directory source, - stores a file source as
<user-template-root>/<pack-name>/<original-file>, - stores a directory source as
<user-template-root>/<pack-name>/....
- supports:
reports- supports:
reports initreports smokereports finalizereports render-specreports indexreports verifyreports publish-manifest
- owns the shared reporting runtime surface rather than repo-specific producer command bodies,
- keeps publish upload and browser-open behavior outside the command family.
- supports:
verify- compares a deployed file against the rendered output of
--against <template>, - supports pass-scoped
--pass Ngroups with per-pass--varand--var-fileinputs when--allis used, - accepts
--builtin-var KEY=VALUEfor deterministic builtin overrides, - accepts
--quietto suppress diff body output while preserving exit status, - exits
0when clean,1when drift is detected, and2or3for genuine validation/render or usage/configuration failures.
- compares a deployed file against the rendered output of
--dry-run behavior:
- For file-writing render operations,
--dry-runmust report:- resolved template path,
- resolved output path,
- whether content would change,
- validation and render diagnostics.
- For
frontmatter-init,--dry-runmust print the exact frontmatter that would be written. - For
init,--dry-runmust print planned filesystem changes, validation results, and recommendations without modifying the workspace.
Guidance and prompt input rules:
--guidanceand--guidance-fileare mutually exclusive.--promptand--prompt-fileare mutually exclusive.--guidance-file -reads guidance content from stdin.--prompt-file -reads prompt content from stdin.- If both guidance and prompt are omitted, only the resolved profile body is composed.
- The CLI must reject attempts to read both guidance and prompt from the same stdin stream in a single invocation.
- CLI-only aliases such as
--agent-typeand--aimust be resolved before library request construction. The library API does not expose alias concepts.
Default output path policy:
- File mode removes the trailing
.j2suffix from the template filename. - Profile mode writes to
.prompts/<name>-<ulid>.mdunless--outputis supplied.
Pack root policy:
examplesresolves example packs from:SC_COMPOSE_DATA_DIR/examples- install-relative
../share/sc-compose/examples/
templatesresolves template packs from:SC_COMPOSE_TEMPLATE_DIR- the platform user-data directory joined with
sc-compose/templates/
templates addmust fail if the destination pack name already exists.examplesis read-only. It must not mutate the bundled examples root.
--var-fileaccepts a JSON or YAML object.- Variable-file keys must be strings.
- Variable-file values must be supported render-context value types.
- Object/map values with string keys are valid per FR-12.
- Sequence values in variable files may contain scalars, objects, arrays, and any finite combination of those values at any nesting depth.
- Historical H2 note (2026-07-29): ADR-E1 supersedes this restriction; the current phase index names the recursive-input implementation Sprint E.1 to avoid colliding with completed Phase D identifiers.
- Arrays of objects are valid per FR-13 when the array is the variable value itself.
CLI exit codes must be:
0for success1reserved exclusively forverifywhen drift is detected after a successful comparison run2for validation or render failure3for usage, configuration, or contract error
All other commands, including template-init, continue to use only 0, 2,
and 3.
The template engine must enable trim_blocks and lstrip_blocks by default.
Authors may opt out for a specific block with the standard Jinja + modifier.
- The same logical inputs must produce byte-identical output.
- Diagnostics must include:
- stable diagnostic code,
- human-readable message,
- source file path,
- line and column when available,
- include stack when applicable,
- severity.
- JSON diagnostics must use a stable, versioned schema suitable for machine consumers.
- Checked rendering must expose a state-shaped result with the states
static_only,contract_invalid,context_required,render_invalid, andrender_checked. Onlyrender_checkedauthorizes a caller to send or cache the exact checked output. render --jsonmust place checked-render parser failures in the standardDiagnosticEnvelope; it must not print a malformed body as a successful payload.
CLI --json output must use the versioned DiagnosticEnvelope as the
canonical transport format:
{
"schema_version": "1",
"payload": {},
"diagnostics": []
}Per-command schemas below describe the shape of the payload field within that
envelope.
render --json
{
"schema_version": "1",
"payload": {
"output_path": "stdout",
"bytes_written": 123,
"template": "path/to/template.md.j2",
"body": "rendered document text"
},
"diagnostics": []
}Schema rules:
-
output_pathis a string and uses"stdout"when no file is written. -
bytes_writtenis the actual byte count written to the selected output target; when writing to stdout it is the UTF-8 byte length emitted to stdout. -
templateis the resolved template path as a string. -
bodyis present only for non-dry-run stdout renders and contains the full rendered document. It is omitted when--output <file>is supplied because the file is the source of truth. -
For JSON templates, ordinary render performs the complete-body parser gate before producing this payload. On parser failure the payload is
{}anddiagnosticscontainsERR_RENDER_JSON_MALFORMED; no body or file is emitted. A successful checked JSON render includes:"render_check": { "state": "render_checked", "template": "path/to/assignment.json.j2", "output_format": "json", "json_escape_mode": "auto", "checked_context": "caller-defined exact context summary", "diagnostics": [] }
-
render --check-renderadds the samerender_checkobject for text output while preserving the existing body/output behavior after a successful gate.
render --dry-run --json
{
"schema_version": "1",
"payload": {
"would_write": ".prompts/example-01HXYZ.md",
"would_change": true,
"template": "path/to/template.md.j2",
"rendered_preview": "preview text"
},
"diagnostics": []
}Schema rules:
would_writeis the derived output target as a string.would_changerecords whether the dry-run output differs from the current file content at the derived output path; missing output files count astrue.rendered_previewis a preview string.
template-init --json
{
"schema_version": "1",
"payload": {
"template_path": "path/to/template.md",
"template_added": true,
"would_change": true,
"vars": ["task"]
},
"diagnostics": []
}Schema rules:
template_pathis the rewritten template path as a string.template_addedistruewhen the command wrote a changed template to disk.would_changerecords whether the generated template differed from the original file content.varsis the ordered list of discovered variable names used in the rewrite.
resolve --json
{
"schema_version": "1",
"payload": {
"resolved_path": ".claude/agents/example.md.j2",
"search_trace": [
".claude/agents/example.md.j2",
".agents/agents/example.md.j2"
],
"found": true
},
"diagnostics": []
}validate --json
{
"schema_version": "1",
"payload": {
"valid": true
},
"diagnostics": [
{
"severity": "info",
"code": "INFO_VAL_DEFAULT_USED",
"message": "variable name not provided, using default: \"world\"",
"location": "templates/example.md.j2"
}
]
}Schema rules:
- Plain
validateis static-only and includes"state": "static_only"alongsidevalid; it does not render. validate --check-renderrenders in memory and returns one of the checked-render states. It never includes a rendered body or output-file field. Onlyrender_checkedhas permission to send or cache the exact context-specific result.validate --lint --check-rendercombines lint and render diagnostics in the same envelope.validate --lintreports source locations and stable mode-lint codes. An auto-mode contract error returns exit code2; warning-only legacy or ambiguous-expression findings preserve exit code0.
sc-compose lint --target template-contracts --jsonuses the same library-owned source scanner asvalidate --lintand reports the effective mode, template path, source location, diagnostic code, migration recommendation, andcontext_backed_renderstate for every finding.- The target is allowlisted through
.sc/sc-lint/targets/template-contracts.tomland remains a local sc-compose capability; it must not duplicate scanner logic in Python or shell. Missing roots, unreadable templates, and include failures are explicit configuration failures, never green passes.
init --json
{
"schema_version": "1",
"payload": {
"workspace_root": "/repo",
"created_files": [
".prompts/",
".gitignore"
]
},
"diagnostics": []
}observability-health --json
{
"schema_version": "1",
"payload": {
"logging": {
"state": "Healthy",
"dropped_events_total": 0,
"flush_errors_total": 0,
"active_log_path": "<log_root>/logs/sc-compose.log.jsonl",
"sink_statuses": [],
"last_error": null,
"query": null
}
},
"diagnostics": []
}Schema rules:
payload.loggingis the JSON serialization ofsc_observability::LoggingHealthReport.LoggingHealthReportis accessed through thesc-observabilityre-export surface for logging-only consumers, per DOC-007 and LOG-038.payload.logging.queryisnullwhen query/follow health is unavailable and otherwise contains aQueryHealthReport.active_log_pathis derived from the configured log root and service name using theLOG-008layout<log_root>/logs/<service>.log.jsonl.- The concrete path is platform-dependent; on Windows it may be drive-qualified.
observability-health --jsonmust not emit console log lines that corrupt the JSON envelope written to stdout.
frontmatter-init --json
{
"schema_version": "1",
"payload": {
"template_path": "templates/example.md.j2",
"frontmatter_added": true,
"would_change": true,
"vars": [
"name",
"role"
]
},
"diagnostics": []
}frontmatter-init --dry-run --json
{
"schema_version": "1",
"payload": {
"action": "frontmatter-init",
"would_affect": [
"templates/example.md.j2"
],
"changed": false,
"would_change": true,
"skipped": false,
"vars": [
"name",
"role"
]
},
"diagnostics": []
}init --dry-run --json
{
"schema_version": "1",
"payload": {
"action": "init",
"would_affect": [
".prompts/",
".gitignore"
],
"changed": false,
"would_change": true,
"skipped": false
},
"diagnostics": []
}Schema rules:
actionnames the command.would_affectlists the filesystem paths or logical targets that would change.changedremainsfalsefor dry-run operations because no write occurs.would_changerecords whether the command would modify its target if writes were enabled.skippedistruewhen the command decides no change is needed.
examples list --json
{
"schema_version": "1",
"payload": {
"packs": [
{
"name": "hello",
"path": "/path/to/share/sc-compose/examples/hello.md.j2"
}
]
},
"diagnostics": []
}templates list --json
{
"schema_version": "1",
"payload": {
"packs": [
{
"name": "pytest-fixture",
"path": "/path/to/user-data/sc-compose/templates/pytest-fixture"
}
]
},
"diagnostics": []
}templates add --json
{
"schema_version": "1",
"payload": {
"name": "pytest-fixture",
"source": "/path/to/source/pytest-fixture.py.j2",
"destination": "/path/to/user-data/sc-compose/templates/pytest-fixture",
"changed": true
},
"diagnostics": []
}Named render through examples <name> and templates <name> must emit the
same command payloads as render and render --dry-run.
sc-composermust not depend directly onsc-observability.sc-composermust not depend onsc-observability-types.sc-composermust define host-injectable observability hooks locally without coupling the library to a concrete logging runtime.- The initial release observability scope is limited to structured logging, health reporting, and downstream extension through the local observer hook model.
sc-composeshall usesc-observabilityas the canonical concrete observability binding for CLI execution.- The current follow-on observability uplift targets
sc-observability1.2.0. - The CLI lifecycle adapter shall prefer
Logger::log(...)for blocking queue admission and may useLogger::try_log(...)only where non-blocking admission is explicitly required. Logger::emit(...)remains a deprecated compatibility path only; any retained use must carry an explicit compatibility rationale indocs/migration-notes.md.- The CLI shutdown path shall adapt to
Logger::shutdown(self) -> Logger<Stopped>while preserving post-shutdown health inspection through the stopped logger typestate. sc-composermust emit composition pipeline events through its local observer/sink hook model.sc-composemust emit command lifecycle events through the same local hook model.- Standalone defaults must keep
sc-composesink paths tool-scoped. - Embedded use must permit host-supplied sink and path configuration.
- If no sink is injected, both crates must remain fully functional with observability reduced to a no-op.
sc-composeshall keep directsc-observabilitylogger construction and sink registration rather than adding thesc-observefacade at the CLI seam.sc-observeandsc-observability-otlpremain out of scope for the initial release.
sc-composershall define its minimal observability hook layer locally insc_composer::observer.- The library hook surface shall remain a local sink/observer abstraction over
ObservationEventrather than importing observability contracts fromsc-observability-types. Renderer::new()shall remain a pure renderer constructor with no observer dependency.compose()shall preserve no-op behavior when the caller does not provide an observer implementation.compose_with_observer(request, &mut dyn CompositionObserver)shall remain the required end-to-end injection surface for host-provided observability.- The local observer hook surface shall remain object-safe and
dyn-compatible so consuming applications can provide their own logging extensions without depending on CLI-specific code. - Injected hooks shall receive structured events for the resolve, include-expand, validate, and render pipeline stages.
- The local observer/sink contracts shall remain usable by embedded hosts that do not use the CLI.
sc-composeshall construct the concretesc-observabilityLoggerduring CLI startup and wire it into thesc-composerinjection point.- The CLI logger wiring shall register both file and console sinks during normal terminal execution.
- The console sink shall be suppressed whenever the active command uses the
--jsonoutput mode so machine-readable command output remains clean. - The CLI shall emit structured command lifecycle events for command start, command completion, and command failure.
- The CLI shall expose logger health through a dedicated
observability-healthcommand so operators can inspect sink state, dropped-event counts, retained-log maintenance state, and the active log path. - The
observability-healthcommand shall initialize logger configuration the same way as a normal CLI process, query health from that process-local logger instance, and must not depend on any daemon or background runtime. - The CLI logger configuration shall keep logger-managed retained-log
maintenance enabled through
RetainedLogPolicy::default()so rotation, pruning, and maintenance cadence stay owned bysc-observabilityrather than duplicated insc-compose. - The CLI shall perform graceful logger shutdown on process exit so pending events flush before termination.
This versioned requirement establishes the Phase G reverse-extraction contract. The contract is defined by ADR-0011.
- Reverse extraction accepts a known template and its rendered output as in-memory text; the core library performs no file I/O, CLI parsing, or network access for this operation.
- The first supported format is XML, and only a documented reversible XML subset is in scope. Extracted values are rendered strings, not reconstructed typed values.
- Reports retain structural occurrence paths, source evidence, confidence in
the closed range
0.0..=1.0, and structured diagnostics. Repeated variable occurrences that cannot be identified unambiguously must produce an ambiguity diagnostic rather than silently overwrite a value. - The initial feature explicitly does not identify unknown templates or reconstruct loops, branches, JSON, Markdown, or source value types.
- Invalid requests, malformed XML, unsupported syntax, and ambiguous structure remain distinct error categories with stable diagnostic codes.
- A dotted expression (for example
{{ user.name }}) is object-field access, not a literal variable identifier. Extraction only supports the scalar (flat) variable subset and has no object/nested-value extraction capability, so any dotted expression is unsupported syntax and must be rejected withERR_EXTRACT_UNSUPPORTEDrather than accepted as a literal variable name (Phase G.7).
The CLI adapter exposes the same known-template contract through the
read-only command sc-compose extract TEMPLATE RENDERED [--format xml|json].
It supports repeatable --include NAME and --exclude NAME filters, uses XML
as the backward-compatible default, and accepts --json for the standard
diagnostics envelope. The command must not identify unknown templates, invoke
the renderer, scan directories, or write output files.
This is a from-scratch product capability informed by prior reverse-extraction research. The committed Rust, Python, and CLI corpus is the regression evidence for the supported known-template/XML-first contract; the earlier research harness is not a product interface or runtime dependency.
Issue #193 records real customer use cases for JSON, YAML, and TOML rendered
output, XML mixed-content blocks, and narrowly defined non-XML prefixes before
rendered XML. Phase H is deliberately limited to the three file-format
extensions: JSON, YAML, and TOML. The XML mixed-content and dirty-prefix
findings remain outside Phase H and are owned by
Phase I.
ADR-0012 and the
Phase-H plan require each in-scope format to receive
explicit semantics, diagnostics, and cross-surface tests before
implementation. H.1 is now accepted: its JSON/YAML/TOML contract, diagnostic
inventory, and shared match_raw_text core are the binding Phase-H design.
The Phase-G XML scalar-only contract and its fail-closed malformed-input
behavior remain authoritative for existing XML behavior, while H.2 through
H.8 remain gated on their own implementation, hardening, and closure criteria
before runtime behavior changes are claimed.
The accepted H.1 design also plans the migration of the format-neutral value-matching logic from the current XML extraction path into one shared internal raw-text matching core. XML structural traversal and format-specific provenance remain owned by XML; delimiter scanning, template-segment parsing, static prefix/suffix matching, capture boundaries, and adjacent-variable ambiguity handling are shared operations. JSON, YAML, and TOML must delegate to that core rather than implement independent text matchers. This is an internal architecture seam in Phase H; Phase I.2 promotes it to a customer-facing raw-text feature without changing the shared matcher.
Phase H did not expose either mode. Phase I.1 now accepts the customer-facing cross-format raw-text mode for known templates, including Markdown, and Phase I.2 owns its implementation. A best-effort/degraded-parse mode that recovers values from structurally modified or partially corrupt documents remains future work beyond Phase I and must not be inferred from the raw-text contract.
The in-scope issue #193 extensions are closed by the committed H.2-H.8
implementation, hardening, cross-surface tests, and bounded campaign evidence in
docs/phase-H/evidence/h-6-cross-format-campaign.json.
The H.6 campaign covers JSON, YAML, and TOML with 36/36 expected outcomes,
including malformed and unsupported inputs as intentional boundaries. This is
bounded local evidence from four workers because Agent Runner was unavailable;
it is not evidence of a distributed adversarial-agent campaign. XML
mixed-content extraction and dirty-prefix stripping remain outside Phase H and
are owned by Phase I; template identification remains future-phase work. They
are not silently treated as H.6 failures.
Phase I.1 accepts the following follow-on requirements. They remain separate from the completed Phase-H delivery and are implemented by the numbered Phase-I sprints.
ExtractFormat::Raw, CLI--format raw, and Pythonformat="raw"shall select one known-template, in-memory raw-text matcher.- Raw mode shall reuse the shared H matcher and generic extraction report; it shall not parse structured formats, identify unknown templates, execute Jinja, reconstruct loops, or infer source types.
- Raw occurrences shall use
RawPathSegmentbyte offsets and one-based line/column evidence, withRawExtractionSource::TextSpanprovenance. - Include/exclude filters shall retain the existing request semantics and filtered variables shall still participate in neighboring capture matching.
- The stable raw diagnostic set is
ERR_EXTRACT_INVALID_REQUEST,ERR_EXTRACT_TEMPLATE_UNSUPPORTED,ERR_EXTRACT_AMBIGUOUS, andWARN_EXTRACT_LOW_CONFIDENCE.
- A known XML element with one full-content placeholder may recover rendered text and approved child markup using deterministic canonical child serialization.
- Multiple placeholders, dynamic names, control-flow reconstruction, unmatched/truncated markup, multiple roots, post-root content, and unknown template identification remain unsupported.
- XML paths, source evidence, ambiguity handling, limits, CLI JSON, and Python reports shall remain consistent with the existing extraction model.
- Rendered XML may contain a leading UTF-8 text/whitespace preamble before one XML document. Complete comments and processing instructions in the retained prolog are allowed; an XML declaration is retained only when first in that prolog.
- Only bytes before the selected root may be removed. The report shall emit
WARN_EXTRACT_DIRTY_PREFIX_STRIPPEDwith the removed span. - Unmatched/truncated prefix markup, malformed suffixes, multiple roots, second documents, post-root content, and DTDs shall remain rejected.
- Strict token discovery shall treat
loop,loop.index,loop.index0,loop.revindex,loop.revindex0,loop.first,loop.last,loop.length,loop.depth,loop.depth0, andloop.cycle(...)as implicit only inside an activeforscope. - Nested scopes shall be independent. A
loopreference outside aforand arbitrary dotted names shall remain subject to normal undeclared-token policy.
- YAML merge keys (
<<) in JSON/YAML var-files shall fail closed withERR_CONFIG_VARFILEbefore tagged-value unwrapping; the diagnostic shall identify the source line and column of the unsupported construct. - The implementation shall not partially expand merge keys or silently lose inherited fields. The diagnostic shall direct callers to expand the mapping explicitly, which is the portable recovery.
- Valid JSON/YAML objects and existing duplicate-key, non-string-key, and value-shape policies shall remain unchanged.
- The CLI shall provide a
sc-compose help [topic]command distinct from clap-generated--help.sc-compose helpwith no argument shall print an index of available topics;sc-compose help --listshall print the same index in a stable, scriptable form. - Each topic shall be a static, versioned manual page shipped inside the
binary (no filesystem lookup, no network access) covering one CLI feature
area: at minimum
render,resolve,validate,verify,extract,template-init,frontmatter-init,init,examples,templates,reports,observability-health, andexit-codes. Theexit-codestopic shall document the FR-7b contract (0/1/2/3) in full, since that contract is otherwise only discoverable by readingdocs/requirements.mdin the repository. - Topic content shall be authored as one Markdown file per topic under
crates/sc-compose/docs/manual/(e.g.crates/sc-compose/docs/manual/render.md), each embedded into the binary at compile time via a single canonical registry (one ordered(topic_name, content)array), not as hand-written Rust string constants and not as separate per-topic Rust modules. This registry is the single seam every topic-content change touches; the dispatch/command-handling code that reads it stays flat and topic-count-independent, so it cannot grow unbounded as topics are added. A scaffolding change must establish this registry mechanism and structure before any per-topic content is added, so that concurrent per-topic contributions only ever add a Markdown file plus one registry entry, never new Rust modules or dispatch branches. - An unknown topic name shall fail closed with a usage-class exit code (per
FR-7b, exit
3) and shall list the valid topic names in its error output. - The root parser shall disable clap's automatically registered
helpsubcommand while retaining the generated--helpflag, then register this conceptualhelpcommand explicitly. This keepssc-compose helpas one unambiguous route to the manual system and prevents clap's generated subcommand from colliding with it. - Topic names are scoped to the explicit
helpcommand, not the root command namespace. A topic may therefore have the same name as a real command (for example,sc-compose help renderdisplays therendermanual whilesc-compose renderruns the renderer); topic lookup must not shadow or reinterpret any root command. helpandhelp --listshall accept--jsonand use the versionedDiagnosticEnvelopetransport. In text mode,help --listis a UTF-8, newline-delimited sequence containing exactly one canonical topic name per line, in registry order, with no labels or indentation; this explicit line-oriented schema is the stable shell-pipeline form. In JSON mode, its payload is{ "topics": ["..."] }in the standard envelope. A topic JSON response uses{ "topic": "...", "manual": "..." }as its payload.- “Versioned” means the Markdown bytes are committed under
crates/sc-compose/docs/manual/, embedded into the same released binary as the CLI, and identified by that binary'ssc-compose --version; there is no independently fetched page or separate page-version field. A behavior change must update the source page and the corresponding CLI release together. - The
help_topicsmodule and its ordered registry are exclusively owned bysc-compose;sc-composerand the Python bindings must not define, import, or mutate manual-topic metadata. - Manual content shall be reviewed for drift against the corresponding command's actual flags/behavior whenever that command's CLI surface changes; this is a documentation-accuracy expectation, not an automated gate.
- The root
sc-compose --helpoutput shall end with a line pointing callers tosc-compose helpfor the full manual/topic index, so the manual system is discoverable without already knowing it exists.
Implemented in Phase HTML-Report.
- Callers may pass structured object/map values as template variables.
- Object keys must be strings.
- Valid object fields must be accessible through normal Jinja field access such
as:
{{ pr.number }}{{ pr.url }}
- Bracket access remains valid when a key is not a valid dotted identifier.
- Structured inputs participate in the same precedence model as existing
inputs:
- explicit input variables,
- environment-derived variables,
- user-template
input_defaults, - frontmatter defaults.
required_variablesmay name nested field paths such aspr.numberonce structured inputs are implemented.- Missing nested required fields must report the full field path, for example
pr.number. - Malformed object input must fail with stable diagnostics using
ERR_VAL_OBJECT_SHAPE. - Duplicate keys in JSON and YAML var-files must fail with
ERR_CONFIG_PARSE; var-files do not use silent last-value-wins semantics. - Nested required-path traversal that encounters a scalar where an object is
required must fail with
ERR_VAL_SHAPE_MISMATCH. --var key=valueremains string-only in this phase. Structured input comes from--var-file, frontmatter defaults, ortemplate.jsoninput_defaults.- JSON and YAML var-file documents remain top-level objects. Structured values are carried in object fields within that top-level object.
Implemented in Phase HTML-Report.
- Callers may pass arrays whose members are objects when the array itself is the variable value.
- Jinja loops such as
{% for item in list %}must support field access within each array member object. - Arrays of objects are valid through:
--var-file,- frontmatter defaults,
- user-template
template.jsoninput_defaults.
- Empty arrays remain valid inputs.
- Arrays of objects may contain nested object fields and arrays, including jagged arrays and arrays nested inside object fields.
- Historical H2 note (2026-07-29): ADR-E1 supersedes this restriction; the current phase index names the recursive-input implementation Sprint E.1 to avoid colliding with completed Phase D identifiers.
- Missing nested fields inside array members must report stable field-path
diagnostics using
ERR_VAL_MISSING_NESTED_FIELD. frontmatter-initmust discover variable references insideforloop bodies. References inside a loop body are attributed to the array variable:{{ sprint.id }}inside{% for sprint in sprints %}meanssprintsis a required variable, notsprintorsprint.id.
Implemented in Phase HTML-Report.
.html.j2templates render like other file-mode templates.- Output path derivation removes only the trailing
.j2suffix and therefore preserves the.htmlextension. - Rendered HTML is treated as a normal template artifact.
AutoEscape::Custom("sc-compose-html")applies to.html.j2,.htm.j2,.xml.j2, and.xhtml.j2templates. It automatically escapes markup and represents XML-illegal control bytes with the legal replacement-character NCR�; template authors do not need to opt in.- Self-contained output, XHTML shape, inline CSS, and browser-viewability are template-author responsibilities rather than core-engine enforcement.
- Dry-run, diagnostics, validation, and output-path rules apply to HTML templates the same way they apply to other file-mode templates.
The shipped renderer's filename-aware AutoEscape::Custom("sc-compose-html")
policy covers .html.j2, .htm.j2, .xml.j2, and .xhtml.j2. The shared
formatter escapes markup and converts XML-illegal C0 control bytes to the legal
replacement-character NCR � while leaving tab, LF, and CR intact.
This is automatic behavior and requires no author opt-in. See
architecture §21.6
for the corresponding boundary statement. This clarification records shipped
behavior; it does not change the existing filename-dispatch mechanism.
Implemented in Phase HTML-Report.
sc-composeshall ship a bundled example namedsprint-report-html.- The example must demonstrate FR-12, FR-13, and FR-14 together using a self-contained HTML sprint status report.
- The H3 example is a single flat file at
examples/sprint-report-html.html.j2. Directory-based example-pack layout is deferred to H4 or a later architecture amendment. - The example must include realistic structured input data showing:
- report metadata,
- sprint entries,
- PR metadata,
- CI status metadata and actionable links,
- actionable links such as PR and CI URLs.
- The example must remain renderable through the standard examples command surface and var-file flow.
- The example must be a credible showcase for
sc-compose, not just a hand-written HTML file stored in the repo.
sc-compose accepts typed TOML semantic report-spec inputs so rendered diagram
formats such as Mermaid become outputs or migration inputs rather than the
long-term source of truth.
Initial implemented report-spec kinds:
state_machinesql_query
state_machine semantic fields:
kindidtitlestatestransitions- optional per-transition fields:
eventguardactoreffect
- optional metadata for ownership, tags, and renderer targets
sql_query semantic fields:
kindidtitlepurposetables_readtables_writtenfiltersorderingcardinalitytransactional_assumptions- optional metadata for ownership, tags, and renderer targets
Input format rule:
- semantic spec input files use TOML
- each spec file defines
[spec] sql_querysemantic fields live under[sql_query]
Transitional Mermaid rule:
- Mermaid may be emitted as an output renderer during migration
- Mermaid may be accepted as a migration input where repos already store it
- Mermaid is not the long-term semantic source model
Semantic QA direction:
- QA should validate structured semantic fields rather than string-compare only the rendered Mermaid output
- renderers may change over time without replacing the typed semantic source contract
Extension rule:
- repos may add new semantic report-spec kinds later without rewriting the shared artifact catalog or producer contracts
Boundary rules:
- the semantic source contract remains format-agnostic
- shared Phase A reporting boundary rules are centralized under
### Report Artifact Contract (Implemented In Sprint B1)
Sprint B1 implements reporting as a generic artifact contract, not as a one-off HTML sprint report feature.
Planned contract shape:
- authored docs remain under
docs/ - report source and catalog inputs live outside
docs/under a report-specific tree such as:reports/catalog/reports/specs/reports/templates/for repo-authored template overrides and other checked-in template inputs
- generated evidence lives outside
docs/under generated-output paths such as:reports/latest/<report-id>/reports/archive/<timestamp>/<report-id>/
- each generated report has one machine-readable metadata sidecar such as
reports/latest/<report-id>/report.json
Planned canonical report catalog fields:
idkindproducerrequiredentrypointmetadata
Requiredness rule:
- each catalog entry declares
required = trueorrequired = false just reportsverification fails only when a report markedrequired = trueis missing- optional reports remain discoverable and publishable without failing shared verification
Planned ownership split:
- producer recipes such as
just lint,just test,just smoke, and repo-specific producer commands own domain data gathering and report generation sc-composeowns rendering semantics where it is used as the report renderer- consumer repos own domain-specific inputs, local producer surfaces, and publish destinations
Shared Phase A reporting boundary rule:
- network publishing remains outside the core engine
- browser-open behavior remains outside the core engine
- the artifact contract is intended to support generic lint, test, smoke, diagram, and custom reports through one shared metadata and filesystem shape
Sprint B3 implements a generic source-driven rendering contract for text assets. This mechanism is not Mermaid-only.
Collection-input contract:
- source collections may be declared by glob or by another stable collection definition
- a collection declares which source files participate in one render-many run
- collection discovery is generic across Mermaid, SVG, Markdown, and other text-based assets
Metadata-extraction contract:
- comment-prefix metadata is supported
- block-comment metadata is supported
- the raw source body remains available to templates without external scripting
- parsed metadata and raw body are exposed together as render inputs
setsmetadata is a collection-local grouping field used to tag one source file into one or more logical sets for selective rendering, filtering, or aggregate groupingsetshas typeOption<Vec<String>>or equivalent optional string-list representation and defaults toNonewhen absent
Render-many contract:
- one generated output is produced per discovered source file
- output derivation is deterministic from collection membership plus source identity
- aggregate templates and review tooling consume a generated manifest rather than ad hoc wrapper state
Generated-manifest contract:
- each source-driven run emits a manifest describing the discovered sources and generated outputs
- the manifest is intended for aggregate templates and review tooling
- browser automation and hosted site behavior remain out of scope for the core engine
Boundary rules for the source-driven line:
- the mechanism remains generic rather than diagram-format-specific
- shared Phase A reporting boundary rules are centralized under
### Phase A Follow-On Reporting Contract (Planning Only) - the scaffold owns creation of
reports/latest/smoke/, and report-smoke execution writes into that prepared path rather than creating the output directory at runtime
Sprint B5 implements how producers write stable latest outputs, how optional
timestamped archive copies are named, and how just reports aggregates and
verifies generated evidence.
Output policy:
- producers overwrite the latest artifact in place at the canonical
reports/latest/<report-id>/...path - producers may also write timestamped archive copies under
reports/archive/<timestamp>/<report-id>/... - archive writes are deterministic and file-system-local
Canonical archive timestamp policy:
- timestamps use a filesystem-safe UTC form such as
2026-05-25T22-10-00Z - one producer run uses one stable timestamp prefix for all archive outputs generated in that run
just reports contract:
- verify required evidence exists
- summarize report status across producers
- build or refresh a combined index when the repo defines one
- print or summarize the latest report entrypoints/paths for the current latest report set
- optional wrapper-owned helpers may open those paths, but browser opening is outside the shared core contract
Verification and failure direction:
just reportsis a shared aggregator and verifier, not a producer that reruns all evidence collection- missing required evidence causes report verification to fail
- required-vs-optional report expectations come from each shared report catalog
entry's
requiredfield - the scaffold owns creation of
reports/latest/smoke/, and report-smoke execution writes into that prepared path rather than creating the output directory at runtime
Archive ownership note:
- archive directories are file-system-local
- archive directories may be consumer-managed
- archive directories may be gitignored
sc-compose emits one machine-readable handoff from generated report artifacts
to CI or wrapper-owned publication steps without moving network or hosting
behavior into the core engine.
Implemented publish-manifest contract:
sc-compose reports publish-manifestwritesreports/latest/publish-manifest.json- the manifest is generated from the current report catalog plus latest sidecars and artifact sets, not from hard-coded per-report paths
- optional reports whose latest artifact sets are absent are skipped
- required reports whose latest artifact sets are absent make manifest generation fail
- artifact roles remain explicit in the manifest rather than inferred by CI
Manifest fields include:
- generated_at
- reports
- per-report report_id
- per-report kind
- per-report entrypoint
- per-report optional archive_root
- per-file role
- per-file path
- per-file intended publish destination
Identity rule:
report_idmust equal the canonical report catalogid
Ownership split:
- producers and renderers create artifacts plus manifest metadata
- CI or wrapper tooling performs upload, copy, or publication steps
Boundary rules:
- the artifact contract supports generic lint, test, smoke, diagram, and custom reports through one shared metadata and filesystem shape
- publish transport and hosting remain outside
sc-composerandsc-compose - machine-readable handoff is in scope; network transport is not
Phase B implements one checked-in proof set in this repo so the shared reporting runtime is exercised by real producer commands instead of docs only.
Implemented proof set:
- this repo ships one checked-in reference report catalog under
reports/catalog/reports.toml - this repo ships one checked-in source fixture tree under:
reports/inputs/reports/specs/reports/smoke/reports/vars/
- this repo ships one reference
Justfileproducer surface for:just lintjust testjust smokejust state-diagramsjust sql-diagramsjust reportsjust reports-verify
- generic producer-owned HTML outputs can be materialized into the shared
report sidecar and archive shape with
sc-compose reports finalize - the repo proves two distinct consumer families through one shared runtime:
sc-lintstyle evidence reportsatm-corestyle diagram reports
atm-coreandsc-lintremain illustrative labels only; the template family key is still the runtime discriminator- producer extension-point typing remains owned by the B1 report artifact runtime
report-evidence-summaryis a new bundled Phase B proof examplesprint-report-htmlremains backward-compatible and stays covered by the shared proof harness
Sprint B2 implements producer recipes as the owners of report generation.
Report generation is not centered on one catch-all just reports command.
Standard producer surface:
just lintjust testjust smoke- repo-specific producer commands such as:
just state-diagramsjust sql-diagrams- schema, migration, or other repo-local evidence producers
Producer contract:
- each producer command is responsible for generating the report artifacts for the report ids it owns
- each producer command writes evidence in the shared report artifact shape
- each producer command updates or emits the catalog/metadata entries for the report ids it owns
- adding a repo-specific producer command must not require changing the shared report aggregation or discovery contract
Boundary rules for the producer line:
- producer recipes own domain data gathering and invocation order
just reportsis reserved for aggregation, verification, combined-index refresh, and latest-entrypoint/path reporting- optional wrapper-owned helpers such as
just reports-openmay exist locally, but they are not part of the shared Phase A command contract - shared Phase A reporting boundary rules are centralized under
### Phase A Follow-On Reporting Contract (Planning Only) - the report ids owned by a producer are declared through the shared report catalog rather than inferred from hard-coded aggregator behavior
- the scaffold owns creation of
reports/latest/smoke/, and report-smoke execution writes into that prepared path rather than creating the output directory at runtime
Sprint B4 implements shared template families and shared panel chrome so report UI behavior does not need to be reimplemented per consumer repo.
Initial template families:
- lint/test/smoke evidence reports
- public API, CLI, and ICD style reports
- diagram, state-machine, and SQL-query reports
Override contract:
The authoritative override contract, shared lookup namespace, consumer
activation config, template block boundary, required template variables, and
include deferral are defined in docs/phase-A/sprint-A5.md.
Repo-authored versus bundled template rule:
- repo-authored template overrides live under the consumer repo's
reports/templates/tree shared:<family>resolves to bundled CLI-owned assets and is not a reference to the consumer repo's authoredreports/templates/tree
Shared panel contract:
- stable panel id
- title
- body content
- required copy-text action
- optional copy-JSON action
- optional fragment or open link
Ownership split:
- shared panel chrome owns panel framing and shared actions
- consumer-specific templates own the panel body content for their report family or repo-local override
Boundary rules:
- per-panel text copy is mandatory
- per-panel JSON copy is optional but first-class
- panel chrome remains part of shared template behavior rather than wrapper-only logic
- shared Phase A reporting boundary rules are centralized under
### Phase A Follow-On Reporting Contract (Planning Only)
- Cross-platform support is required for macOS, Linux, and Windows.
- The product must not rely on shell-specific behavior.
- Single-template
render,resolve, andvalidateoperations must be fast enough for interactive terminal use on local repositories. - The public library API must be stable enough for downstream integration and semver-governed once released.
- The library and CLI must remain separable:
sc-composemay depend onsc-composer, butsc-composermust not depend on the CLI crate. - Observability integration must emit structured events at the resolve, include-expand, validate, and render pipeline stages with stable target, action, and message conventions.
- Observability health state must be queryable without mutating composition behavior so operators and embedded hosts can inspect runtime health safely.
- Process shutdown must flush pending observability output and degrade gracefully when sink flushing reports errors.
- The
sc-composerpublic API is semver-governed. - Before
1.0, breaking API changes require a minor version bump. - After
1.0, patch releases contain backward-compatible bug fixes only. - After
1.0, minor releases contain backward-compatible new features. - After
1.0, major releases contain breaking changes. - Exception: the
1.3.0change toRenderer::with_delimiters(open, close) -> Result<Self, RenderError>ships under the narrow carve-out documented in ADR-0010; this exception is limited to that one constructor change and does not weaken the general major-version rule. - ADR-0009 governs Phase D delivery sequencing and Python parity scope; it is intentionally out of scope for this Section 6 stability policy.
render_template()is a stable convenience API for one-shot rendering.Rendereris the primary stable API for repeated rendering and long-lived library use.
Required unit coverage includes:
- frontmatter parsing,
- frontmatter omission defaults,
- variable precedence,
- required-variable enforcement,
- undeclared-variable behavior in normal and strict modes,
- unknown-variable policy handling,
- include resolution, cycle detection, and depth limits,
- include-driven defaults and required-variable propagation,
- path confinement,
- resolver precedence.
Required integration coverage includes:
- CLI
render, - CLI
resolve, - CLI
validate, - CLI
frontmatter-init, - CLI
init, - CLI
observability-health, - CLI
examples list, - CLI
examples <name>, - CLI
templates list, - CLI
templates add, - CLI
templates <name>, - command lifecycle logging,
- resolve/include-expand/validate/render event emission,
--dry-runno-write guarantees,- JSON diagnostics contract,
- cross-platform path behavior,
- template-pack discovery and add semantics,
- list/array input behavior through frontmatter defaults,
template.jsoninput_defaultsfor user templates, and--var-file.
- Remote includes such as
httporhttps - Arbitrary plugin execution from templates
- Runtime-specific hooks and event integrations inside the core composition engine
prepare-hookandpost-render-hookexecution- Named render for packs with multiple root-level
*.j2entry candidates - Template deletion, update, sync, or remote registry features
- Multi-panel HTML/XHTML report expansion and wrapper-level output viewing behavior remain deferred to the follow-on design track in docs/html-sprint-report-plan.md