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::MissingRequiredor thevalidation::missing_requireddiagnostic 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. Seedocs/migrations/0.85-to-0.86.mdand 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:
<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-okis 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.
is now accepted and is equivalent to:
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=andpages=keyword arguments onQuill.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.0schema-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¶
- Open every
Quill.yamlin the bundle and removerequired:from each field schema. For fields that wererequired: falsewithout adefault:, decide whether the field is genuinely Must Fill (drop the line) or Endorsed-with-empty-default (add a type-emptydefault:). - Regenerate any stored
schema.yamlgolden files — therequiredkey will no longer appear, anddescription/defaultordering inside the schema may have shifted. - Regenerate stored blueprint goldens —
; required/; optionalannotations are replaced with<must-fill>values and; skip-okannotations. - Search host code for
validation::missing_requiredand switch tovalidation::must_fill_absent. If you display diagnostics by code, add a branch forvalidation::must_fill_sentinel. - (Optional) Drop explicit
$kind: mainfrom the root block of sample documents and README snippets if you prefer the shorter form.
Updating Python host code¶
- Replace imports of
ParseError/TemplateError/CompilationError/EditErrorwithQuillmarkError. Collapse per-classexceptbranches into a singleexcept QuillmarkErrorthat iterates.diagnostics. - Rename
quill.backend→quill.backend_id, andartifact.output_format/result.output_format→.format. - Replace
quill.platewithquill.blueprint, andquill.name/quill.schema_yaml/quill.defaults/quill.dry_run/quill.print_treewith the equivalents in the table above. - If you rely on render timing, switch from manual wall-clock timing
to
result.render_time_ms.