feat: add the schema-check module - #5
Merged
Merged
Conversation
Every extension that checks its `_schema.yml` at render time needs the same wiring. It reads the schema once, checks the document configuration, checks a shortcode call, and reports through `logging`. The module holds that wiring, so an extension does not copy it. The validator arrives as an argument rather than through `require`. A vendored copy of this module then knows nothing about where the validator was vendored, and the two sources stay independent. The level of each finding lives in one table. Nothing the module reports stops a render.
`options` returned the schema defaults alone. An extension needs more than that: a default is always present once declared, so the defaults cannot tell a value the document wrote from a key it never set. It now returns the defaults first, and a table holding `provided`, `merged` and `defaults` second. A caller that wants the defaults alone is unchanged.
…trip Two levels in the severity table had no test that reached them. `option_error` needed an options pass the schema rejects, and `call_error` needed a call that gives its required argument, so that the missing argument path does not take over the reporting. The two once per render checks passed whether or not the guard was there, because the stub they used reported nothing. The stub now reports a finding on every pass, so a second check shows as a second message. `_report` no longer reports an unmapped kind as a warning. It says which kind it could not report, at error level, and the module now checks at load that every level has a reporting function.
The validator comes from an independent source, so `options` no longer assumes the schema it returns carries an `options` key. `call` already made the same allowance for `shortcodes`, and the two methods now agree on how much they trust the injected table. `options` returned the checker's own defaults table. One caller writing into what it received changed the defaults for the whole render, and for every later reader. It now returns a copy on every call. The second return still holds the checker's own tables, and the docstring says they must not be written to.
The module discards the first return of `validate_shortcode`, so a `valid` on the call stub said nothing while inviting the next reader to believe it gated the errors loop. The stub now returns true there, and a comment says why, next to the options pass where `valid` does gate the reporting.
A default is not always a scalar. One extension declares `default: []` for an array option, and another declares a mapping default, so a copy one level deep handed two callers the same inner table and left the aliasing one level down. The copy is now recursive. A table already copied on the walk is reused, which keeps shared structure shared and makes a cycle terminate.
The docstring justified the recursive copy with the two schemas that need it today. That is the weaker claim: the vocabulary allows `type: array` and `type: object`, and the validator returns a declared default untouched once it matches its type, so any option with a collection type and a literal default resolves to a table. The docstring now says that instead.
Both nested defaults in the previous check sit one level below the top container and hold scalars, so they pass against a copy that recurses exactly one level further and stops. The new check nests a default three levels down, as an array of objects whose properties are objects, and writes through the returned table at each level. Nothing in the fleet declares that shape today, so it covers the format rather than a schema in play, which is the reasoning that set the depth.
mcanouil
marked this pull request as ready for review
September 6, 2026 15:32
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
schema-checkholds the wiring an extension needs to check a document and a shortcode call against its own_schema.yml, so that thirty-eight extensions can validate without each carrying sixty lines of the same code.The validator is injected rather than required:
newreads the schema once.optionsvalidates the document configuration and returns the resolved defaults, and second a table holdingprovided,mergedanddefaults. The second return is what tells a value an author wrote from a key they never set, which is the only wayinline: falsediffers from an absentinline.callvalidates one shortcode call, and only an extension declaring ashortcodes:block needs it.Severity lives in one table. Warnings, except an unreadable schema and a missing required argument, which are errors. Nothing here stops a render, raises, or exits, and a test asserts that no path calls
os.exit. The single exception is a load-time assertion that every severity level has a reporter, whose condition depends only on constants in the file, so no document or schema can reach it and a mistyped level cannot reach a release.The behaviour is
quarto-iconify's, moved rather than rewritten. Message strings are byte-identical, and the levels were cross-checked against the lines they came from, so an extension adopting this module reports exactly what iconify reports today.The defaults are copied on the way out, all the way down. A caller that writes into what it received cannot change what a later reader sees, and the copy is recursive because the schema vocabulary allows a collection default at any depth:
quarto-rememberdeclares an array default andquarto-revealjs-a11yan object one.122 checks pass. Each of them was checked against a mutation of the thing it covers, so a test that could not fail was found and fixed rather than counted.