Skip to content

0.83.0 → 0.84.0 — Must Fill / Endorsed schema model, Python ↔ WASM parity

0.84.0 retires the required: axis on field schemas, replacing it with a default-driven Must Fill / Endorsed cell model. The same release aligns the Python binding contract with @quillmark/wasm and relaxes the parser to accept root blocks that omit $kind: main.

This is a breaking release for:

  • Quill authors who used required: on field schemas.
  • Backends and host code that matched on ValidationError::MissingRequired or the validation::missing_required diagnostic code.
  • Python consumers of quillmark (the binding surface was reshaped to mirror WASM).

Schema model: required: → Must Fill / Endorsed

A field's cell is now inferred from default::

Cell When Blueprint rendering Validation behaviour
Endorsed default: is present Renders the default value plus ; skip-ok Shippable as-is; absent ⇒ default applied
Must Fill default: is absent Renders <must-fill> sentinel Absent ⇒ validation::must_fill_absent; sentinel survives to render ⇒ validation::must_fill_sentinel

Note (0.86+): The validation behaviour above describes 0.84. As of 0.86, an absent Must Fill field is non-fatal — the render path zero-fills it silently. Only a surviving <must-fill> sentinel is still a render error. See docs/migrations/0.85-to-0.86.md and the CHANGELOG.

The required: key is gone from FieldSchema. Treat the migration as:

  main:
    fields:
      subject:
        type: string
-       required: true
        description: Be brief and clear.

      tone:
        type: string
-       required: false
+       default: neutral
        enum: [neutral, formal, friendly]

A schema that previously declared required: true with no default: is already a Must Fill field — drop the line. A schema that declared required: false without a default: is the new "skippable Endorsed" case: add a type-empty default ("", [], {}, 0, false) so the blueprint can ship as-is.

Blueprint output

Two new inline tokens appear in the rendered blueprint:

# <type>[<format>][; skip-ok]
  • <must-fill> is the literal sentinel embedded in the value cell of a Must Fill field. For markdown fields, it appears inside a block scalar wrapper so the surrounding shape stays parseable.
  • ; skip-ok is appended to the annotation of an Endorsed field to signal that the rendered default is safe to keep.

The legacy ; required / ; optional role tags are gone from blueprint output.

Validation codes

Code (0.83) Code (0.84) Notes
validation::missing_required validation::must_fill_absent Fires when a Must Fill field is absent at validate time.
(new) validation::must_fill_sentinel Fires when the <must-fill> sentinel survives into the rendered document.

ValidationError::MissingRequired is replaced by ValidationError::MustFillUnset { source: MustFillSource::Absent } and ValidationError::MustFillUnset { source: MustFillSource::Sentinel }. Host code that matched the old variant should switch on MustFillSource.

  match err {
-     ValidationError::MissingRequired { path, .. } => handle_absent(path),
+     ValidationError::MustFillUnset { path, source: MustFillSource::Absent, .. } => handle_absent(path),
+     ValidationError::MustFillUnset { path, source: MustFillSource::Sentinel, .. } => handle_sentinel(path),
      ...
  }

Uniform validation-message format

Type and presence errors now emit a single canonical message shape: field path, verbatim YAML source token, schema declaration, and — when applicable — both exits (provide a value of type X, or omit the line to accept the default). ValidationError::TypeMismatch gains source_token and default fields, and Display is hand-rolled to dispatch on (expected, actual, has_default). Diagnostic codes themselves (validation::type_mismatch, etc.) are unchanged for non-Must-Fill variants.

Markdown parser: $kind: main is now optional on the root block

The root block's $kind is main by virtue of position. The parser accepts root blocks that omit the $kind: line and splices a synthetic $kind: main at the canonical position, so the in-memory model stays uniform.

~~~card-yaml
$quill: my_quill@0.1.0
title: Hello
~~~

Body goes here.

is now accepted and is equivalent to:

~~~card-yaml
$quill: my_quill@0.1.0
$kind: main
title: Hello
~~~

Body goes here.

Canonical emission still writes the explicit line, so source with the line round-trips byte-equal and source without it converges to the canonical form on first emit. A non-main $kind on the root is still a parse error, and no composable (non-root) block may declare $kind: main.

No migration is required for existing documents — the change is additive.

Python binding: align with WASM

The Python binding has been reshaped so its patterns match @quillmark/wasm exactly. There is no per-binding error hierarchy or naming convention to track separately anymore.

Error contract — one exception, structured diagnostics

Every failure raises a single QuillmarkError carrying a non-empty .diagnostics: list[Diagnostic]. The legacy exception classes (ParseError, TemplateError, CompilationError, EditError) are gone, as is the singular .diagnostic shim.

- from quillmark import ParseError, TemplateError, EditError
+ from quillmark import QuillmarkError

  try:
      quill.render(doc)
- except ParseError as e:
-     print(e.diagnostic)
- except TemplateError as e:
-     ...
+ except QuillmarkError as e:
+     for diag in e.diagnostics:
+         print(diag.severity, diag.code, diag.message)

EditError::<Variant> survives as a message prefix on the QuillmarkError (matching how WASM emits it), so substring assertions that targeted variant names should pivot from the exception type to the message prefix.

Renames

0.83 0.84
Quill.backend Quill.backend_id
Artifact.output_format Artifact.format
RenderResult.output_format RenderResult.format

Quill.metadata now mirrors WASM

Quill.metadata returns a dict shaped like the WASM identity snapshot: name, version, backend, author, description, supportedFormats. Any extra keys declared under quill: in Quill.yaml are forwarded as-is.

Removed surface

Removed Replacement
Quill.plate Quill.blueprint (it was always an alias)
Quill.name quill.metadata["name"]
Quill.schema_yaml quill.schema (structured dict), serialise with yaml.safe_dump
Quill.defaults Inspect quill.schema field-by-field
Quill.dry_run quill.form(doc) returns the same form view
Quill.print_tree Build the rendering from quill.schema if you need it

Added surface

  • RenderResult.render_time_ms — wall-clock render time in milliseconds, matching the WASM field.
  • ppi= and pages= keyword arguments on Quill.render, matching WASM.

Test rot fixed in passing

Two unrelated pieces of stale syntax were repaired alongside the binding rework:

  • #@quill: frontmatter in fixtures (retired in #629) is now $quill: everywhere — see 0.82 → 0.83.
  • A literal 0.81.0 schema-version string in a test was updated to the current version.

If your local test suite still asserts on either, update the expectation.

Prescan: multibyte-safe bullet stripping

A &str[2..] byte-slice in the markdown prescan path could panic on a multibyte bullet (en-dash, em-dash, smart quote, emoji). The site was already guarded by starts_with("- ") so no real input triggered it, but the slice has been replaced with strip_prefix("- ") to remove the panic class entirely. No host action required.

Updating a quill bundle

  1. Open every Quill.yaml in the bundle and remove required: from each field schema. For fields that were required: false without a default:, decide whether the field is genuinely Must Fill (drop the line) or Endorsed-with-empty-default (add a type-empty default:).
  2. Regenerate any stored schema.yaml golden files — the required key will no longer appear, and description/default ordering inside the schema may have shifted.
  3. Regenerate stored blueprint goldens — ; required / ; optional annotations are replaced with <must-fill> values and ; skip-ok annotations.
  4. Search host code for validation::missing_required and switch to validation::must_fill_absent. If you display diagnostics by code, add a branch for validation::must_fill_sentinel.
  5. (Optional) Drop explicit $kind: main from the root block of sample documents and README snippets if you prefer the shorter form.

Updating Python host code

  1. Replace imports of ParseError / TemplateError / CompilationError / EditError with QuillmarkError. Collapse per-class except branches into a single except QuillmarkError that iterates .diagnostics.
  2. Rename quill.backendquill.backend_id, and artifact.output_format / result.output_format.format.
  3. Replace quill.plate with quill.blueprint, and quill.name / quill.schema_yaml / quill.defaults / quill.dry_run / quill.print_tree with the equivalents in the table above.
  4. If you rely on render timing, switch from manual wall-clock timing to result.render_time_ms.