Skip to content

Fixture Schemas

This page is the authoritative reference for the shape of each fixture file in the conformance suite. Read it alongside the JSON files in conformance/ when writing a runner or adding new fixtures. For the full list of errorCode values used in error expectations, see Error Codes.

Fixture envelope

Every fixture file is a single JSON object:

{
  "schema": "<schema-url>",
  "version": "0.1.0",
  "cases": [ ... ]
}
Field Type Description
schema string $id of the JSON Schema that validates this file (e.g. https://bars.commonsware.com/conformance/schema/render-fixture.schema.json)
version string Suite format version ("0.1.0"), in semver form; asserted by the schema as a pattern
cases array The fixture objects for this file (shapes described per directory below)

Central vs. co-located schemas

kBars keeps one central schema per fixture shape in conformance/schema/, rather than one schema file co-located next to every fixture (as kPointer does). kBars has well over a hundred fixture files across only five distinct fixture shapes; co-locating a schema with each file would mean many near-identical schema copies. This is the one deliberate structural difference from kPointer's co-located schema convention — if you are porting both kPointer's and kBars's suites, expect the schema-discovery step to differ between them.

Schema file Validates directories
render-fixture.schema.json render/, transforms/
grammar-fixture.schema.json grammar/
prepass-fixture.schema.json prepass/
partials-fixture.schema.json partials/
env-property-fixture.schema.json env-properties/

Five further schemas describe generated/CLI artifacts rather than fixtures: manifest.schema.json (validates manifest.json, see the manifest), error-codes.schema.json (validates error-codes.json, see Error Codes), render-report.schema.json (validates kbars render --json output, see the CLI Reference), transform-registry.schema.json (validates transforms/registry.json, see transforms/registry.json below), and env-property-registry.schema.json (validates env-properties/registry.json, see env-properties/registry.json below). All five are embedded and validated the same way the five fixture schemas are.

grammar/*.json

Tests that a template source string parses into the expected AST, or is rejected with the expected error.

{
  "schema": "https://bars.commonsware.com/conformance/schema/grammar-fixture.schema.json",
  "version": "0.1.0",
  "cases": [
    {
      "name": "unique-kebab-case-id",      // required
      "description": "Human-readable note.",  // optional
      "input": "<raw template source>",     // required
      "expect": { … }                       // required; see below
    }
  ]
}

expect shapes

// Parsing succeeded — segments is an array of typed nodes
{ "kind": "ast", "ast": { "segments": [ … ] } }

// Parsing must fail; errorCode is optional
{ "kind": "error", "errorCode": "KB-1001" }

Each element of ast.segments is one of:

  • { "type": "Text", "value": "..." }
  • { "type": "Interpolation", "identifier": "...", "trimOpen": bool, "trimClose": bool }
  • { "type": "UnescapedInterpolation", "identifier": "...", "trimOpen": bool, "trimClose": bool }
  • { "type": "Comment", "body": "..." }
  • { "type": "RawBlock", "body": "..." }
  • { "type": "EscapedDelimiter" }

prepass/*.json

Tests the hand-written pre-pass normalization step that rewrites unterminated opener delimiters before the ANTLR lexer ever sees them.

{
  "schema": "https://bars.commonsware.com/conformance/schema/prepass-fixture.schema.json",
  "version": "0.1.0",
  "cases": [
    {
      "name": "unique-kebab-case-id",
      "description": "Human-readable note.",
      "input": "<raw template source before normalization>",  // required
      "expectedOutput": "<expected string after normalization>"  // required
    }
  ]
}

render/*.json and transforms/*.json

Both directories share the render-fixture shape; transforms/ specifically exercises built-in transforms and is kept separate because the transform roster is the standard library.

{
  "schema": "https://bars.commonsware.com/conformance/schema/render-fixture.schema.json",
  "version": "0.1.0",
  "cases": [
    {
      "name": "unique-kebab-case-id",
      "description": "Human-readable note.",     // optional
      "template": "<raw template source>",       // required — named `template`, not `input`, so it reads clearly next to `context`
      "context": { … },                            // optional — JSON context supplied to the render environment
      "expect": { … },                              // required; see below
      "expectedSoftFails": [ "KB-5005", … ]         // optional; see below
    }
  ]
}

expect shapes

// Render succeeded
{ "kind": "output", "expectedOutput": "…" }

// Render must throw; errorCode, line, and column are each optional and asserted only when present
{ "kind": "error", "errorCode": "KB-NNNN", "line": 1, "column": 4 }

See Source Locations for the line/column counting rule.

expectedSoftFails

When present, asserts that the set of soft-fail codes emitted during rendering matches exactly (order does not matter — the comparison is set-based). Each entry is a KB-5xxx code from Error Codes. When the field is absent, soft-fail events are not asserted at all — a runner should neither require zero soft fails nor any specific set.


partials/*.json

Exercises {{> key }} rendering — partial loading, context arguments, the failure matrix, and the templateStack recorded on soft-fail events and hard-fail exceptions.

{
  "schema": "https://bars.commonsware.com/conformance/schema/partials-fixture.schema.json",
  "version": "0.1.0",
  "cases": [
    {
      "name": "unique-kebab-case-id",
      "description": "Human-readable note.",
      "template": "<root template source>",   // required
      "context": { … },                          // optional
      "partials": { "key": "<source>" },        // optional — see below
      "maxPartialDepth": 8,                      // optional — override for the max partial render depth
      "expect": { … }                             // required; see below
    }
  ]
}

When partials is omitted, no loader is registered, so any {{> … }} tag is a hard failure. When it is present (even {}), a loader is registered that returns each mapped source and null for any unmapped key.

expect shapes

// Render succeeded — softFails is optional and defaults to empty; errorCode, line, and column on
// each soft-fail entry are each optional and asserted only when present
{
  "kind": "output",
  "expectedOutput": "…",
  "softFails": [ { "category": "…", "partialStack": [ … ], "errorCode": "KB-NNNN", "line": 1, "column": 4 } ]
}

// Render must throw — partialStack, messageContains, errorCode, line, and column are each optional
{ "kind": "error", "partialStack": [ … ], "messageContains": "…", "errorCode": "KB-NNNN", "line": 1, "column": 4 }

A soft-fail entry's category is a KbSoftFailEvent.Category name (for example UnresolvedReference or RecursionLimit). A partialStack is the chain of partial keys active at the failure, outermost-first, with the root template left implicit: [] means the root template, and ["outer", "inner"] means the failure occurred in partial inner, reached through partial outer.

A soft-fail entry's errorCode is the stable KB-NNNN code the reported event's category carries (KbSoftFailEvent.Category.code) — the same code family used elsewhere, letting a soft-fail be cross-referenced by code and not just by category name. line/column on both a soft-fail entry and the error expectation follow the same convention as render fixtures — 1-based line, 0-based Unicode-code-point column — and let a fixture pin whether a failure inside a partial reports that partial's own line numbering. See Source Locations for the counting rule.

softFails matching is ordered, not set-based

Unlike render-fixture's expectedSoftFails, which asserts an order-insensitive set of codes, a partials fixture's softFails array is matched positionally and by exact count: entry n must match the n-th soft-fail event emitted during the render, and the number of entries must equal the number of events. A fixture with two or more soft fails therefore lists them in emission order — the order the failing expressions are encountered while rendering the template, left to right, outermost to innermost. See multiple-soft-fails-are-matched-in-emission-order (partials/soft-failures.json) for a fixture that pins this: swapping its two softFails entries would still describe the same set of failures, but fails the fixture, because the reference runner does not treat softFails as an unordered collection.

Static partial keys carry no source location

A partial's source location is captured from its dynamic context expression ({{> (expr) }}) — the parser has no comparable expression node for a static symbol key ({{> key }}), so a failure reached through a static key carries no KbSourceLocation at all, not even an empty partialStack. This affects:

  • KB-4001 (unknown static key) always — a static key is the only way to reach this code, so it is unconditionally location-less.
  • KB-4002 (no onLoadPartial loader registered) only when reached through a static key; the same code reached through a dynamic key does carry the dynamic expression's location. Contrast no-loader-registered-is-hard-failure (static key, no location) with no-loader-registered-via-dynamic-key-reports-its-expression-location (dynamic key, location present) in partials/hard-failures.json.
  • RecursionLimit only when the over-deep partial is reached through a static key, as in recursion-limit-soft-fails-and-stops (partials/soft-failures.json); a self-recursive partial invoked via a dynamic key would carry a location instead.

A runner should not fail a fixture merely because its own implementation happens to produce a location the fixture does not assert — but a fixture that does assert line/column/partialStack on one of these codes is only valid when the underlying key is dynamic, per the above.


env-properties/*.json

Uses the same fields as a render fixture, plus two optional fields for exercising the @env namespace and, in the same fixtures, the {{= scoped-variable tag:

{
  "schema": "https://bars.commonsware.com/conformance/schema/env-property-fixture.schema.json",
  "version": "0.1.0",
  "cases": [
    {
      // ...all render-fixture fields (template, context, expect, expectedSoftFails)...
      "clock": "2026-01-01T00:00:00Z",   // optional — ISO-8601 instant seeding the default `now` property
      "properties": {                     // optional — environment properties registered via overrideProperty
        "site": { "kind": "element", "value": "example.com" },
        "launched": { "kind": "dateTime", "value": "2026-01-01T00:00:00Z" }
      }
    }
  ]
}

Each properties value is a tagged object discriminated by kind:

  • { "kind": "element", "value": <any JSON> } — registered as a KbTransformable.Element.
  • { "kind": "dateTime", "value": "<ISO-8601>" } — registered as a KbTransformable.DateTime.

Fixtures exercising the {{= tag alone (with no clock/properties) use plain template/context/expect fields; their error cases use { "kind": "error", "errorCode": "KB-NNNN" } with a code from Error Codes. The target of {{= must be a bare @env/name or @local/name — a sub-pointer target such as @env/a/b is KB-3002.

Like transforms/, this category is part of the standard library (see the transforms and @env sections below): its property roster is normative for full-tier conformance, and a core-tier port may omit it.


transforms/registry.json

A single generated file cataloging every built-in transform — reusing the fixture envelope shape (schema/version/cases) so existing manifest tooling picks it up without change, though each "case" here is a catalogue entry, not a runnable test case:

{
  "schema": "https://bars.commonsware.com/conformance/schema/transform-registry.schema.json",
  "version": "0.1.0",
  "cases": [
    {
      "name": "abs",                 // required — the name used after a pipe in a template
      "category": "math",            // required — "string" | "math" | "list" | "datetime" | "misc"
      "since": "0.1.0",               // required — suite version this transform was first specified in
      "summary": "Absolute value.",   // required — one line, for the generated index
      "description": "…",             // required — Markdown prose for the generated per-transform page
      "inputCoercion": "number",      // optional — omitted when the transform accepts more than one input kind
      "arguments": [                  // required — may be empty
        { "name": "value", "type": "string", "optional": false }, // "default" is present only when optional
      ],
      "arity": { "min": 0, "max": 0 }, // required — "max": null means "or more", no upper bound
      "fixture": "transforms/transform-abs.json" // required — path this entry's worked examples are drawn from
    }
  ]
}

name, category, since, summary, description, arguments, arity, and fixture are required on every entry; inputCoercion is optional. category drives both the generated spec index's grouping and which author-doc family page (transforms-string.md, transforms-math.md, etc.) cross-links to the transform; misc is for transforms that don't belong to any single family (e.g. default). Arity and argument types are author-maintained to agree with the transform's own imperative argument checks in code — they are not mechanically derived from the implementation.

The registry drives two generated artifacts and one check, described in the Bars specification: the per-transform spec pages under site-spec/docs/spec/0.1.0/transforms/ (kept in sync by checkSpecTransforms), and the requirement, enforced by checkTransformDocLinks, that every registry entry is linked from the author-facing site-kbars/docs/templates/transforms*.md pages. Each entry's worked examples are drawn from its fixture file's cases: a case marked "spotlight": true is preferred, falling back to the first three success cases in file order when a fixture has no spotlighted cases.

transforms/registry.json itself is a full-tier entry in manifest.json, alongside the transforms/ fixture files it describes.


env-properties/registry.json

The @env-property analog of transforms/registry.json: a single generated file cataloging every standard-library @env property, reusing the same fixture envelope shape:

{
  "schema": "https://bars.commonsware.com/conformance/schema/env-property-registry.schema.json",
  "version": "0.1.0",
  "cases": [
    {
      "name": "now",                  // required — the property name after `@env/`
      "since": "0.1.0",                 // required — suite version this property was first specified in
      "summary": "…",                    // required — one line, for the generated index
      "description": "…",                // required — Markdown prose for the generated per-property page
      "valueKind": "dateTime",          // required — "element" | "dateTime" | "empty"
      "sourcing": "…",                   // optional — prose note for an implementation-defined seed
      "fixture": "env-properties/env-now.json" // required — path this entry's worked examples are drawn from
    }
  ]
}

name, since, summary, description, valueKind, and fixture are required on every entry; sourcing is optional and renders as a callout on the generated page — present only for a property whose value is seeded from an implementation-defined source (now's seeding clock, for example). valueKind is the @env analog of the transform registry's inputCoercion: the value-model kind (see Data Model) the property resolves to. Unlike the transform registry, there is no category field — @env properties are a single flat namespace, not grouped into families.

fixture must match ^env-properties/env-[a-z][a-z0-9_]*\.json$, the naming convention that isolates standard-library-property fixtures from the @env namespace-mechanics fixtures (assign-*.json, scalar.json, struct.json, missing.json, list.json) and from datetime-property.json (which documents a user-registered property, not a standard-library one). A bidirectional completeness check enforces that every registry entry has a matching env-<name>.json fixture and vice versa.

The registry drives the same two generated artifacts and one check that the transform registry does, described in the Bars specification: the per-property spec pages under site-spec/docs/spec/0.1.0/env-properties/ (kept in sync by checkSpecEnvProperties), and the requirement, enforced by checkEnvPropertyDocLinks, that every registry entry is linked from the author-facing site-kbars/docs/templates/env-properties.md page. Worked-example selection is identical to the transform registry's ("spotlight": true preferred, falling back to the first three success cases in file order); a selected case that carries a clock also gets a one-line "Given the environment was created at <clock>:" note above its worked example, since an @env/now example is otherwise opaque without its seed.

env-properties/registry.json itself is a full-tier entry in manifest.json, alongside the env-properties/ fixture files it describes.


Transforms and @env are the standard library

Files under transforms/ use the render-fixture schema but exercise built-in transforms (upcase, replace, map, format_date_time, and so on). They are held in their own directory because the transform roster is the standard library — normative for full-tier conformance, absent from core-tier. Keeping the directories separate lets a port adopt the core render/ corpus on its own, then add the standard-library corpus when it targets the full tier. A full-tier port must match the baseline roster faithfully, but may carry additional transforms, and may even widen a baseline transform, provided the baseline behavior is preserved (see Implementation-Defined Behavior). kbars conformance continues to default to ./conformance/render/; running the transform corpus requires passing the directory explicitly (kbars conformance conformance/transforms).

The same applies to env-properties/: the baseline @env property roster (kBars registers now) is standard library, while the seeding clock and any additional properties stay implementation-defined. Each fixture file's tier is recorded in manifest.json, so a runner can tell the core corpus from the standard-library corpus programmatically.