Skip to content

Commit 42bb65b

Browse files
committed
research notes on quarto-yaml-validation extraction
1 parent e6fcf95 commit 42bb65b

1 file changed

Lines changed: 251 additions & 0 deletions

File tree

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
# Extracting `quarto-yaml-validation` into a standalone repository
2+
3+
**Strand:** bd-egcyeym9
4+
**Date:** 2026-06-17
5+
**Status:** Information gathering / current-state architecture (no code changes this session)
6+
7+
## Goal
8+
9+
Move the YAML-schema-validation infrastructure (`quarto-yaml-validation`) out of
10+
`quarto-dev/q2` so that non-Quarto developers can depend on it as a general-purpose
11+
crate. This session maps where the crate lives in the dependency graph, its inbound
12+
and outbound dependencies, and the design problems an extraction must solve — chiefly
13+
the Quarto-specific error-code catalog.
14+
15+
## TL;DR findings
16+
17+
1. **The outbound closure is small and clean.** To use `quarto-yaml-validation` you
18+
need exactly three other workspace crates: `quarto-yaml`, `quarto-source-map`,
19+
and `quarto-error-reporting`. `quarto-source-map` is a true leaf (only `serde`,
20+
`serde_json`, `smallvec`). None of the four pulls in the heavy Quarto engine
21+
crates (`pampa`, `quarto-core`, etc.).
22+
23+
2. **Inside q2, `quarto-yaml-validation` has essentially no inbound coupling.** The
24+
only in-repo consumer is the `validate-yaml` binary (a thin CLI/test harness). It
25+
is **not** wired into the render pipeline, `quarto-config`, or the WASM client.
26+
This makes extraction low-risk: nothing in the engine breaks if the crate moves.
27+
28+
3. **The three *foundation* crates are heavily used inside q2.** `quarto-source-map`
29+
(~26 dependents), `quarto-error-reporting` (~19), and `quarto-yaml` (8) are core
30+
infrastructure. They **cannot simply be relocated** — q2 still needs them. The
31+
split therefore has to be a *publish-and-consume* arrangement (q2 depends on the
32+
externalized crates), not a move.
33+
34+
4. **The hard design problem is error-code identity.** `quarto-yaml-validation`
35+
hard-codes Quarto error codes (`Q-1-10`, `Q-1-11`, …) and leans on
36+
`quarto-error-reporting`'s centralized `error_catalog.json` (titles, docs URLs at
37+
`https://quarto.org/docs/errors/...`). Outside q2 those codes and URLs are
38+
meaningless. The standalone library needs a *pluggable error-code / catalog
39+
provider*; the q2-embedded build keeps the existing `Q-1-x` codes. This is both a
40+
technical change (abstraction boundary) and an org change (two catalogs, two
41+
"since_version" lineages, a remapping layer).
42+
43+
## Crate dependency graph (relevant slice)
44+
45+
```
46+
quarto-source-map leaf: serde, serde_json, smallvec (NO quarto deps)
47+
48+
49+
quarto-yaml → quarto-source-map, yaml-rust2, serde, thiserror
50+
51+
52+
quarto-error-reporting → quarto-source-map, ariadne, schemars, url, once_cell,
53+
▲ serde, serde_json, thiserror
54+
55+
quarto-yaml-validation → quarto-yaml, quarto-source-map, quarto-error-reporting,
56+
▲ yaml-rust2, regex, serde, serde_json, thiserror
57+
58+
validate-yaml (bin) → the only in-repo consumer of quarto-yaml-validation
59+
```
60+
61+
Note: `quarto-error-reporting` does **not** depend on `quarto-yaml-validation`. (A
62+
grep match is a false positive — it appears only in a code comment that warns against
63+
adding `schemars` for user-YAML validation. There is no dependency cycle.)
64+
65+
### Outbound dependencies (what extraction must carry along)
66+
67+
`quarto-yaml-validation` → (workspace) `quarto-yaml`, `quarto-source-map`,
68+
`quarto-error-reporting`; (crates.io) `yaml-rust2`, `regex`, `serde`, `serde_json`,
69+
`thiserror`, `anyhow`.
70+
71+
The **transitive workspace closure** that must be externalized to make
72+
`quarto-yaml-validation` buildable outside q2 is:
73+
74+
| Crate | Outbound (workspace) deps | crates.io deps | Notes |
75+
|---|---|---|---|
76+
| `quarto-source-map` | *(none)* | serde, serde_json, smallvec | Clean leaf — easiest to externalize |
77+
| `quarto-yaml` | source-map | yaml-rust2, serde, thiserror | Annotated YAML parse; re-exports `SourceInfo` |
78+
| `quarto-error-reporting` | source-map | ariadne, schemars, url, once_cell, serde(_json), thiserror | Holds the **error catalog** — the contentious piece |
79+
| `quarto-yaml-validation` | the above three | yaml-rust2, regex, serde(_json), thiserror, anyhow | The crate we want to ship |
80+
81+
All four share the workspace `[workspace.package]` metadata
82+
(`repository = posit-dev/quarto-markdown-syntax`, `license = MIT`,
83+
`edition = 2024`, version `0.3.0`).
84+
85+
### Inbound dependencies (who would be affected by a move)
86+
87+
| Crate | Direct in-repo dependents (Cargo) | Implication for split |
88+
|---|---|---|
89+
| `quarto-yaml-validation` | `validate-yaml` only | **Trivial** — move freely; only the CLI follows |
90+
| `quarto-yaml` | pampa, quarto-core, quarto-config, quarto-error-reporting, quarto-lsp-core, validate-yaml (8) | q2 still needs it → must depend on externalized crate |
91+
| `quarto-error-reporting` | ~19 crates (pampa, quarto-core, quarto-config, quarto-csl, quarto-citeproc, quarto-doctemplate, quarto-lsp-core, quarto-preview, quarto-publish, quarto, wasm-quarto-hub-client, …) | Deeply embedded → cannot move; must be a shared dependency |
92+
| `quarto-source-map` | ~26 crates (most of the workspace, incl. WASM client) | Core infra → cannot move; must be a shared dependency |
93+
94+
**Consequence:** the only crate that *leaves* q2 cleanly is `quarto-yaml-validation`
95+
itself. The three foundation crates have to become shared/published dependencies that
96+
*both* the new external repo and q2 consume. This is the central structural decision
97+
(see "Extraction strategies" below).
98+
99+
## How `quarto-yaml-validation` couples to source-location infra
100+
101+
`quarto-source-map` provides the annotated-parse machinery the user mentioned. The
102+
validation crate uses it pervasively:
103+
104+
- `quarto-yaml` parses YAML into `YamlWithSourceInfo`, carrying `SourceInfo` (byte
105+
ranges + provenance) for every node. `SourceInfo` is re-exported from
106+
`quarto-source-map`.
107+
- `ValidationError` (in `error.rs`) stores `yaml_node: Option<YamlWithSourceInfo>`
108+
and a resolved `SourceLocation { file, line, column }` obtained via
109+
`SourceInfo::map_offset(0, ctx)` against a `SourceContext`.
110+
- `ValidationDiagnostic` (in `diagnostic.rs`) calls `source_info.map_offset(...)` and
111+
`source_ctx.get_file(...)` to produce a `SourceRange` (filename + offset +
112+
line/col) for machine-readable JSON, and hands the `SourceInfo` to
113+
`quarto-error-reporting`'s ariadne renderer for human text.
114+
115+
The public `quarto-source-map` surface in play:
116+
`SourceContext`, `SourceFile`, `SourceInfo`, `FileId`, `Location`, `MappedLocation`,
117+
`map_offset`, `get_file`, `start_offset`/`end_offset`. This surface is general (it is
118+
not Quarto-specific in any way) — externalizing `quarto-source-map` is conceptually
119+
clean; the only cost is that ~26 q2 crates must now consume it as an external crate.
120+
121+
## How `quarto-yaml-validation` couples to error reporting / the catalog
122+
123+
Two distinct couplings, both via `quarto-error-reporting`:
124+
125+
### (a) Rendering coupling — `DiagnosticMessage` / `DiagnosticMessageBuilder`
126+
127+
`ValidationDiagnostic::build_diagnostic_message` builds a
128+
`DiagnosticMessageBuilder::error("YAML Validation Failed")` and calls `.with_code(...)`,
129+
`.problem(...)`, `.with_location(SourceInfo)`, `.add_detail/.add_info/.add_hint`,
130+
`.build()`. The resulting `DiagnosticMessage` is what renders to ariadne text
131+
(`to_text`) and JSON. This coupling is purely about *presentation* and is
132+
non-Quarto-specific in shape — it could move to the external library as-is.
133+
134+
### (b) Identity coupling — the centralized error-code catalog (the hard part)
135+
136+
- `ValidationErrorKind::error_code()` (in `error.rs`) hard-codes
137+
`&'static str` Quarto codes:
138+
`Q-1-10` (missing required property), `Q-1-11` (type mismatch),
139+
`Q-1-12` (invalid enum), `Q-1-13`, `Q-1-14`, `Q-1-15`, `Q-1-16`, `Q-1-17`,
140+
`Q-1-18`, `Q-1-19`, `Q-1-20`, `Q-1-29`, `Q-1-99` (other).
141+
- These strings are looked up at render time in `quarto-error-reporting`'s
142+
**catalog**: `catalog.rs` loads `error_catalog.json` via `include_str!` into a
143+
`HashMap<String, ErrorCodeInfo>` where `ErrorCodeInfo { subsystem, title,
144+
message_template, docs_url, since_version }`. `DiagnosticMessage::docs_url()` does
145+
`catalog::get_docs_url(code)`.
146+
- The catalog is **global and centralized**: 145 entries spanning many subsystems
147+
(yaml=`Q-1-*`, internal=`Q-0-*`, listing=`Q-12-*`, navigation=`Q-13-*`, …). Codes
148+
follow `Q-<subsystem>-<number>`. The yaml subsystem occupies `Q-1-1..Q-1-29` +
149+
`Q-1-99`. `docs_url`s all point at `https://quarto.org/docs/errors/<subsystem>/<code>`.
150+
- A repo-wide **audit** (`scripts/audit-error-codes.py`, `scripts/quick-error-audit.sh`)
151+
enforces consistency between codes referenced in source and entries in
152+
`error_catalog.json`. Deliberate exceptions use the marker
153+
`// quarto-error-code-audit-ignore` (line) or `...-ignore-file` (file). Any
154+
extraction must keep this audit green for the q2-embedded build.
155+
156+
**Why this blocks a naive extraction:** the codes `Q-1-x`, the central JSON catalog,
157+
the `quarto.org` docs URLs, and the cross-subsystem numbering are all *Quarto policy*.
158+
A standalone library shipped to non-Quarto users must not bake in `Q-1-x`/`quarto.org`,
159+
yet the q2-embedded build must keep them (codes are a stability contract — see
160+
`error.rs`/`builder.rs` doc comments: "codes don't change even if message wording
161+
improves").
162+
163+
## The error-code remapping design problem (statement, not solution)
164+
165+
We need an abstraction where:
166+
167+
- `quarto-yaml-validation` emits *semantic* error kinds (it already has a clean
168+
`ValidationErrorKind` enum — machine-readable, no strings). The mapping from kind →
169+
catalog code becomes a **policy supplied by the embedder**, not hard-coded in the
170+
library.
171+
- `quarto-error-reporting` becomes a reusable error-reporting library whose catalog is
172+
**pluggable / remappable**: a host can register its own code namespace, titles, and
173+
docs-URL scheme. The library ships with no Quarto-specific catalog; q2 supplies the
174+
`Q-*` catalog + `quarto.org` URLs; an external user supplies their own (or none).
175+
- The same `ValidationErrorKind` therefore renders as `Q-1-11` + a `quarto.org` URL
176+
inside q2, and as `<their-scheme>` (or code-less) outside q2.
177+
178+
Candidate shapes to evaluate in a follow-up design session (NOT decided here):
179+
180+
- A trait, e.g. `ErrorCodeProvider`/`Catalog`, injected into the diagnostic builder;
181+
q2 implements it over `error_catalog.json`, the standalone lib provides a no-op or
182+
caller-supplied default.
183+
- Keep `error_code()` returning a *library-local* stable id (namespaced to the
184+
validation crate, e.g. `YV-…` or a typed enum discriminant), and let the embedder
185+
*remap* library ids → its own catalog codes at the reporting boundary. This is the
186+
"remap error codes when errors are defined in different packages" idea the user
187+
raised, and it generalizes beyond yaml-validation (any externalized crate that
188+
defines its own errors).
189+
- Split `quarto-error-reporting` into `error-reporting-core` (catalog-agnostic
190+
builder/render/JSON) + `quarto-error-catalog` (the q2 `Q-*` policy + audit). q2
191+
depends on both; external users depend only on the core.
192+
193+
## Extraction strategies (to weigh in design phase)
194+
195+
Because the three foundation crates must serve *both* repos, the split is really a
196+
question of *who owns the source of truth*:
197+
198+
1. **New repo owns the foundation crates; q2 consumes them as published crates.**
199+
Move `quarto-source-map`, `quarto-yaml`, `quarto-error-reporting`(-core),
200+
`quarto-yaml-validation` to a new repo; publish to crates.io (or git deps); q2
201+
replaces its in-tree copies with version deps. Cleanest long-term, highest
202+
coordination cost (every q2 change to source-map now crosses a repo boundary).
203+
204+
2. **q2 stays the source of truth; new repo vendors via subtree/`git subtree` or a
205+
read-only mirror + crates.io publish from q2.** Lowest disruption to q2 dev flow;
206+
external repo is a packaging view. Risk: the external "non-Quarto" identity is
207+
thin if it is just a mirror.
208+
209+
3. **Cargo workspace inheritance / path-or-version deps.** Publish the foundation
210+
crates from q2 but keep developing them in q2; the external repo pins versions.
211+
Requires decoupling the shared `[workspace.package]` metadata
212+
(`posit-dev/quarto-markdown-syntax` repository field, version `0.3.0`) per-crate.
213+
214+
Each option interacts with the error-code design: option 1/3 force the
215+
catalog-pluggability work (external users get no `Q-*`); option 2 could defer it.
216+
217+
## Open questions for the next session
218+
219+
- Which extraction strategy (own / mirror / publish-from-q2)?
220+
- Does `quarto-error-reporting` split into core + catalog, or gain a provider trait?
221+
- Where do the **JSON wire types** (`json.rs`: `JsonDiagnostic`, shared with
222+
`wasm-quarto-hub-client` and `quarto-preview`) live after the split? They are
223+
catalog-adjacent and have their own q2 consumers.
224+
- `schemars` is scoped into `quarto-error-reporting` deliberately (machine-to-machine
225+
JSON shapes); does the external core keep it?
226+
- Naming/identity of the external project (the YAML validator is "Quarto's dialect"
227+
per the code comments — is the externalized lib still "Quarto-dialect YAML schema"
228+
or rebranded general-purpose?).
229+
- How does the `error-code audit` (`scripts/audit-error-codes.py`) adapt — does it
230+
only police the q2 catalog, with the library carrying its own check?
231+
232+
## Source references
233+
234+
- `crates/quarto-yaml-validation/src/error.rs``ValidationErrorKind::error_code()`
235+
(hard-coded `Q-1-x`), `ValidationError`, `SchemaError`, source-loc usage.
236+
- `crates/quarto-yaml-validation/src/diagnostic.rs``ValidationDiagnostic`,
237+
builder/source-map/catalog coupling, JSON output.
238+
- `crates/quarto-error-reporting/src/catalog.rs` + `error_catalog.json` — centralized
239+
catalog (145 entries), `ErrorCodeInfo`, `get_docs_url`.
240+
- `crates/quarto-error-reporting/src/builder.rs` / `diagnostic.rs``with_code`,
241+
`docs_url()`, ariadne/JSON rendering.
242+
- `crates/quarto-error-reporting/src/lib.rs` — re-exports incl. `json` module.
243+
- `crates/quarto-source-map/src/lib.rs` — public surface (`SourceContext`,
244+
`SourceInfo`, `map_offset`, …).
245+
- `crates/quarto-yaml/src/lib.rs``YamlWithSourceInfo`, re-export of `SourceInfo`.
246+
- `crates/validate-yaml/Cargo.toml` — the sole in-repo consumer.
247+
- `scripts/audit-error-codes.py`, `scripts/quick-error-audit.sh` — catalog audit.
248+
- Prior related notes: `claude-notes/error-id-system-design.md`,
249+
`claude-notes/error-reporting-design-research.md`,
250+
`claude-notes/yaml-validation-rust-design.md`,
251+
`claude-notes/validate-yaml-error-reporting-integration.md`.

0 commit comments

Comments
 (0)