diff --git a/Cargo.lock b/Cargo.lock index d8a352c0c..9100bfd1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3418,6 +3418,7 @@ dependencies = [ "pollster", "quarto-core", "quarto-doctemplate", + "quarto-error-catalog", "quarto-error-reporting", "quarto-hub", "quarto-lsp", @@ -3534,6 +3535,7 @@ dependencies = [ "quarto-ast-reconcile", "quarto-config", "quarto-doctemplate", + "quarto-error-catalog", "quarto-error-reporting", "quarto-highlight", "quarto-navigation", @@ -3593,6 +3595,16 @@ dependencies = [ "tree-sitter-doctemplate", ] +[[package]] +name = "quarto-error-catalog" +version = "0.7.0" +dependencies = [ + "once_cell", + "quarto-error-reporting", + "serde", + "serde_json", +] + [[package]] name = "quarto-error-message-macros" version = "0.1.0" @@ -3609,7 +3621,6 @@ name = "quarto-error-reporting" version = "0.7.0" dependencies = [ "ariadne", - "once_cell", "quarto-source-map", "schemars", "serde", diff --git a/Cargo.toml b/Cargo.toml index 625c1d47f..402df1d58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -115,6 +115,9 @@ path = "./crates/quarto-yaml-validation" [workspace.dependencies.quarto-error-reporting] path = "./crates/quarto-error-reporting" +[workspace.dependencies.quarto-error-catalog] +path = "./crates/quarto-error-catalog" + [workspace.dependencies.quarto-source-map] version = "0.1.0" diff --git a/claude-notes/plans/2026-06-26-extract-error-reporting-foundation.md b/claude-notes/plans/2026-06-26-extract-error-reporting-foundation.md index 84a725217..439bd2af0 100644 --- a/claude-notes/plans/2026-06-26-extract-error-reporting-foundation.md +++ b/claude-notes/plans/2026-06-26-extract-error-reporting-foundation.md @@ -181,8 +181,10 @@ the **WASM cutover** — on the easiest crate, before the harder error-reporting +serde), added `README.md` + `.gitignore`. Builds on **stable** rustc 1.95 (no nightly needed). - [x] **1b.** Local validation green: `cargo build`, `cargo test` (104 unit + 4 - doctests pass), `cargo publish --dry-run` clean. *(Gap: no GitHub Actions CI - workflow committed to the new repo yet — tests were run locally. Add one.)* + doctests pass), `cargo publish --dry-run` clean. **CI workflow added** + (`.github/workflows/ci.yml`, commit `ee3780d`): stable Rust, test matrix + Linux/macOS/**Windows** + a fmt/clippy(`-D warnings`) job. First run green on + all 4 jobs — Windows build confirmed (first time this crate has built there). - [x] **1c.** Published `quarto-source-map 0.1.0` to crates.io (Carlos's personal account; `cargo owner --add github:posit-dev:` deferred — weekend). - [x] **1d.** q2 cutover (branch `braid/bd-egcyeym9-source-map-extraction`): @@ -214,33 +216,106 @@ engineering and is valuable even if the repo move slipped. Does **not** depend o Phase 1. There is **no façade** — `quarto-error-reporting` keeps its name and its public surface (minus the moved catalog *data*). -- [ ] **2a (test first).** Pin the behaviour the carve-out must preserve, against - the *current* code: - - `get_docs_url("Q-0-1")` returns the quarto.org URL (with the q2 catalog - active); - - a new test asserting that with **no** catalog installed, `get_docs_url` - returns `None` (passes only after the registry exists — write it now, - expect red). -- [ ] **2b.** Introduce `CatalogProvider` + the `OnceLock` registry + delegating - free-functions in `quarto-error-reporting`; keep `ErrorCodeInfo` here. - Repoint `diagnostic.rs:290` at `catalog()`. Green. -- [ ] **2c.** Create `quarto-error-catalog` crate: move `error_catalog.json` + the - `ERROR_CATALOG` static + the `QuartoCatalog` provider + `install()` there. - Move the catalog-data tests with it. Wire `install()` into q2 binary/WASM/test - entry points. -- [ ] **2d.** Put `json.rs` behind a default-off `json` feature in - `quarto-error-reporting`; have q2's dependents that use it enable - `features = ["json"]`. Confirm the 9 dependents + the 2 `quarto-core` catalog - callers + all `json`/`coalesce` consumers compile **unchanged** (only feature - flags and the `ERROR_CATALOG` import path may move). -- [ ] **2e.** Audit manifests: `schemars` becomes `json`-feature-only; drop - `once_cell` if the registry's `OnceLock` makes it unused; `url` stays. -- [ ] **2f.** `cargo xtask verify` green (touches `quarto-core` → hub-client/WASM; - do NOT `--skip-hub-build`). +- [x] **2a (test first).** Behaviour pinned: `empty_catalog_returns_none` + (direct, global-free) + `installed_catalog_is_used_by_lookups` (the single + global-mutating test) in `quarto-error-reporting`; the positive + `get_docs_url("Q-0-1") → quarto.org` case relocated to `quarto-error-catalog` + integration tests (`install_makes_get_docs_url_resolve`, + `diagnostic_docs_url_resolves_after_install`). +- [x] **2b.** `quarto-error-reporting` now catalog-agnostic: `CatalogProvider` + trait + `EmptyCatalog` + `OnceLock` registry + `install_catalog` in + `catalog.rs`; `get_docs_url`/`get_error_info`/`get_subsystem` keep their + signatures but delegate to the installed provider (the `&'static` lifetime + survives via `catalog(): &'static dyn CatalogProvider`). `ERROR_CATALOG` + static + `include_str!` removed; `lib.rs` re-exports updated. `diagnostic.rs` + `docs_url()` unchanged (still calls the local delegating fn); its positive + doctest/test relaxed/relocated. +- [x] **2c.** New `quarto-error-catalog` crate: `error_catalog.json` (git-moved) + + `ERROR_CATALOG` (Lazy) + `QuartoCatalog: CatalogProvider` + `install()`; the + example moved here; the 10 data-presence tests ported (direct map access). + `install()` wired into the `q2` binary `main`. **WASM deliberately does + NOT install** (see the 2f note): the WASM bridge never surfaces docs URLs + (`JsonDiagnostic` has no `docs_url` field), and installing would + `include_str!` the 46 KB catalog into the bundle, pushing the WASM past + hub-client's 35 MiB PWA precache limit. A legitimate "embedder installs + nothing → EmptyCatalog" choice. The 2 `quarto-core` data-presence `#[test]`s + now query + `quarto_error_catalog::ERROR_CATALOG` directly (dev-dep added). **Audit + script + ~25 path references updated** to `crates/quarto-error-catalog/…`; + `scripts/audit-error-codes.py` passes (exit 0). Full workspace nextest: + **10240 passed**. +- [x] **2d.** `json.rs` now behind a **default-off `json` feature** (`lib.rs` + `#[cfg(feature = "json")]` on the module + re-export; `tests/schema_drift.rs` + gated with `#![cfg(feature = "json")]`). Only **4** crates use the wire + symbols (`quarto`, `quarto-core`, `quarto-preview`, `wasm-quarto-hub-client`); + each now declares `features = ["json"]`. `to_json` (uses `serde_json::json!`, + not the module) and `coalesce.rs` stay unconditional. Verified by `cargo + tree`: `schemars` **absent** by default, present with `--features json`. +- [x] **2e.** `schemars` made `optional = true` + `[features] json = + ["dep:schemars"]`. `once_cell` **dropped** (registry uses `std::sync::OnceLock`; + confirmed unused via `cargo tree`). `url` stays. Two clippy fixes in the new + code (`map().unwrap_or` → `match`; needless doctest `fn main`). +- [x] **2f.** `cargo xtask verify` **GREEN — all 14 steps** (incl. WASM build + + hub-client tests). Two failures found + fixed en route: (1) two clippy lints + in the new code (Step 1); (2) the WASM build (Step 7) broke hub-client's vite + PWA step — wiring `install()` into the WASM bootstrap `include_str!`'d the + 46 KB catalog and forced it past the 35 MiB precache limit (`vite.config.ts` + `maximumFileSizeToCacheInBytes`). Fixed *soundly* (not by raising the limit): + removed the WASM `install()` + `quarto-error-catalog` dep, since the WASM + surfaces no docs URLs — pure dead weight there. WASM now 36,684,365 B + (15.8 KB **under** the limit). Workspace nextest **10240 passed**. + +**Phase 2 is COMPLETE.** `quarto-error-reporting` is now catalog-agnostic (a +`CatalogProvider` seam + `EmptyCatalog` default, no embedded `Q-*` data); the q2 +policy lives in the new `quarto-error-catalog`; `json` is a default-off feature. +The crate is ready to carve out (Phase 3) once the Phase-1-style repo/publish +machinery is pointed at it. Uncommitted on `braid/bd-egcyeym9-error-reporting-split`. > At the end of Phase 2, q2 still builds `quarto-error-reporting` from a path dep; > it is now catalog-agnostic and cleanly carve-able. +### Phase 2 — implementation notes (investigation 2026-06-27) + +**Blast-radius finding (much smaller than feared).** A full workspace audit shows +the catalog is **completely decoupled from the production render path**: +- `DiagnosticMessage` does *not* consult the catalog to build its title/message + (verified: no `get_error_info`/`message_template` use in `builder.rs`/`diagnostic.rs`). +- `to_text` (ariadne) and the JSON wire shape **never call `docs_url()`**; `json.rs` + has no `docs_url` field. **Zero `.snap` files contain a `quarto.org/docs/errors` + URL.** So the carve-out cannot change any rendered output or snapshot. +- `docs_url()` has **zero** consumers anywhere in the workspace. `ErrorCodeInfo` has + no external users. +- The *only* catalog uses outside `quarto-error-reporting`: two `quarto-core` + `#[test]`s (`project_resources.rs:1447`, `theme_diagnostic.rs:271`) asserting + their codes are registered, plus the `examples/with_error_code.rs` example. + +**Consequence:** `install()` is **behaviour-neutral today** (nothing reads the +catalog in production); it matters only for the data-verification tests and for +future features that surface docs URLs. This removes the test-fragility risk that +would otherwise come from an uninitialised global. + +**Finalised design.** +- *`quarto-error-reporting` (catalog-agnostic):* keep `ErrorCodeInfo`; add + `CatalogProvider` trait + `OnceLock>` registry + + `install_catalog()` + `EmptyCatalog` default. `get_docs_url`/`get_error_info`/ + `get_subsystem` keep their **exact signatures** (the `&'static` lifetime survives + because `catalog()` returns `&'static dyn CatalogProvider`) but delegate to the + installed provider. `diagnostic.rs:290` is unchanged (still calls the local + `get_docs_url`). Remove `error_catalog.json` + the `ERROR_CATALOG` static + the + data tests + the example. Drop the `ERROR_CATALOG` re-export from `lib.rs`. +- *`quarto-error-catalog` (new q2 crate):* `error_catalog.json` + the loader + + `QuartoCatalog: CatalogProvider` + `install()`. Houses the moved data-presence + tests, the moved doctests (e.g. `get_subsystem("Q-0-1") == Some("internal")`), + and the moved example. Deps: `quarto-error-reporting`, `serde`, `serde_json`, + `once_cell`. +- *Test seams:* test providers/`EmptyCatalog` are exercised **directly** (no + global) wherever possible; exactly one test asserts the global-empty default and + one asserts global-install delegation — safe under nextest's process-per-test + (and per-process doctests). The two `quarto-core` tests gain a + `quarto-error-catalog` dev-dependency and call `quarto_error_catalog::install()`. +- *Binaries/WASM:* wire `quarto_error_catalog::install()` into the `q2` binary + `main` and the WASM bootstrap (future-proofing; behaviour-neutral now). + ## Phase 3 — Extract `quarto-error-reporting` into a *separate* repo and cut q2 over Requires Phase 1 (source-map published) **and** Phase 2 (carve-out done). diff --git a/crates/qmd-syntax-helper/src/conversions/q_2_11.rs b/crates/qmd-syntax-helper/src/conversions/q_2_11.rs index 830b0a5e7..5ec052a9e 100644 --- a/crates/qmd-syntax-helper/src/conversions/q_2_11.rs +++ b/crates/qmd-syntax-helper/src/conversions/q_2_11.rs @@ -3,7 +3,7 @@ // This conversion rule fixes Q-2-11 errors by adding closing double quotes // where they are missing at the end of blocks. // -// Error catalog entry: crates/quarto-error-reporting/error_catalog.json +// Error catalog entry: crates/quarto-error-catalog/error_catalog.json // Error code: Q-2-11 // Title: "Unclosed Double Quote" // Message: "I reached the end of the block before finding a closing '\"' for the quote." diff --git a/crates/qmd-syntax-helper/src/conversions/q_2_12.rs b/crates/qmd-syntax-helper/src/conversions/q_2_12.rs index 38d10af04..105bfd803 100644 --- a/crates/qmd-syntax-helper/src/conversions/q_2_12.rs +++ b/crates/qmd-syntax-helper/src/conversions/q_2_12.rs @@ -3,7 +3,7 @@ // This conversion rule fixes Q-2-12 errors by adding closing star marks // where they are missing at the end of blocks. // -// Error catalog entry: crates/quarto-error-reporting/error_catalog.json +// Error catalog entry: crates/quarto-error-catalog/error_catalog.json // Error code: Q-2-12 // Title: "Unclosed Star Emphasis" // Message: "I reached the end of the block before finding a closing '*' for the emphasis." diff --git a/crates/qmd-syntax-helper/src/conversions/q_2_13.rs b/crates/qmd-syntax-helper/src/conversions/q_2_13.rs index b3ef019b7..7b2352a29 100644 --- a/crates/qmd-syntax-helper/src/conversions/q_2_13.rs +++ b/crates/qmd-syntax-helper/src/conversions/q_2_13.rs @@ -3,7 +3,7 @@ // This conversion rule fixes Q-2-13 errors by adding closing '**' marks // where they are missing at the end of blocks. // -// Error catalog entry: crates/quarto-error-reporting/error_catalog.json +// Error catalog entry: crates/quarto-error-catalog/error_catalog.json // Error code: Q-2-13 // Title: "Unclosed Strong Star Emphasis" // Message: "I reached the end of the block before finding a closing '**' for the strong emphasis." diff --git a/crates/qmd-syntax-helper/src/conversions/q_2_15.rs b/crates/qmd-syntax-helper/src/conversions/q_2_15.rs index 8ce09b4d5..78fe6c531 100644 --- a/crates/qmd-syntax-helper/src/conversions/q_2_15.rs +++ b/crates/qmd-syntax-helper/src/conversions/q_2_15.rs @@ -1,6 +1,6 @@ // Q-2-15: Unclosed Strong Underscore Emphasis // -// Error catalog entry: crates/quarto-error-reporting/error_catalog.json +// Error catalog entry: crates/quarto-error-catalog/error_catalog.json // Error code: Q-2-15 // Title: "Unclosed Strong Underscore Emphasis" // Message: "I reached the end of the block before finding a closing '__' for the strong emphasis." diff --git a/crates/qmd-syntax-helper/src/conversions/q_2_16.rs b/crates/qmd-syntax-helper/src/conversions/q_2_16.rs index 88fb07a4b..444549500 100644 --- a/crates/qmd-syntax-helper/src/conversions/q_2_16.rs +++ b/crates/qmd-syntax-helper/src/conversions/q_2_16.rs @@ -3,7 +3,7 @@ // This conversion rule fixes Q-2-16 errors by adding closing superscript marks // where they are missing at the end of blocks. // -// Error catalog entry: crates/quarto-error-reporting/error_catalog.json +// Error catalog entry: crates/quarto-error-catalog/error_catalog.json // Error code: Q-2-16 // Title: "Unclosed Superscript" // Message: "I reached the end of the block before finding a closing '^' for the superscript." diff --git a/crates/qmd-syntax-helper/src/conversions/q_2_17.rs b/crates/qmd-syntax-helper/src/conversions/q_2_17.rs index f75a49344..b5bcc7c84 100644 --- a/crates/qmd-syntax-helper/src/conversions/q_2_17.rs +++ b/crates/qmd-syntax-helper/src/conversions/q_2_17.rs @@ -3,7 +3,7 @@ // This conversion rule fixes Q-2-17 errors by adding closing subscript marks // where they are missing at the end of blocks. // -// Error catalog entry: crates/quarto-error-reporting/error_catalog.json +// Error catalog entry: crates/quarto-error-catalog/error_catalog.json // Error code: Q-2-17 // Title: "Unclosed Subscript" // Message: "I reached the end of the block before finding a closing '~' for the subscript." diff --git a/crates/qmd-syntax-helper/src/conversions/q_2_18.rs b/crates/qmd-syntax-helper/src/conversions/q_2_18.rs index 2ab744971..ad8ff9960 100644 --- a/crates/qmd-syntax-helper/src/conversions/q_2_18.rs +++ b/crates/qmd-syntax-helper/src/conversions/q_2_18.rs @@ -3,7 +3,7 @@ // This conversion rule fixes Q-2-18 errors by adding closing strikeout marks // where they are missing at the end of blocks. // -// Error catalog entry: crates/quarto-error-reporting/error_catalog.json +// Error catalog entry: crates/quarto-error-catalog/error_catalog.json // Error code: Q-2-18 // Title: "Unclosed Strikeout" // Message: "I reached the end of the block before finding a closing '~~' for the strikeout." diff --git a/crates/qmd-syntax-helper/src/conversions/q_2_19.rs b/crates/qmd-syntax-helper/src/conversions/q_2_19.rs index d34c74e27..b14b94779 100644 --- a/crates/qmd-syntax-helper/src/conversions/q_2_19.rs +++ b/crates/qmd-syntax-helper/src/conversions/q_2_19.rs @@ -3,7 +3,7 @@ // This conversion rule fixes Q-2-19 errors by adding closing '++]' marks // where they are missing at the end of blocks. // -// Error catalog entry: crates/quarto-error-reporting/error_catalog.json +// Error catalog entry: crates/quarto-error-catalog/error_catalog.json // Error code: Q-2-19 // Title: "Unclosed Editorial Insert" // Message: "I reached the end of the block before finding a closing '++]' for the editorial insert." diff --git a/crates/qmd-syntax-helper/src/conversions/q_2_20.rs b/crates/qmd-syntax-helper/src/conversions/q_2_20.rs index ea5cdab77..c05665084 100644 --- a/crates/qmd-syntax-helper/src/conversions/q_2_20.rs +++ b/crates/qmd-syntax-helper/src/conversions/q_2_20.rs @@ -3,7 +3,7 @@ // This conversion rule fixes Q-2-20 errors by adding closing '--]' marks // where they are missing at the end of blocks. // -// Error catalog entry: crates/quarto-error-reporting/error_catalog.json +// Error catalog entry: crates/quarto-error-catalog/error_catalog.json // Error code: Q-2-20 // Title: "Unclosed Editorial Delete" // Message: "I reached the end of the block before finding a closing '--]' for the editorial delete." diff --git a/crates/qmd-syntax-helper/src/conversions/q_2_21.rs b/crates/qmd-syntax-helper/src/conversions/q_2_21.rs index 634927143..56d6058f1 100644 --- a/crates/qmd-syntax-helper/src/conversions/q_2_21.rs +++ b/crates/qmd-syntax-helper/src/conversions/q_2_21.rs @@ -3,7 +3,7 @@ // This conversion rule fixes Q-2-21 errors by adding closing ']' marks // where they are missing at the end of blocks. // -// Error catalog entry: crates/quarto-error-reporting/error_catalog.json +// Error catalog entry: crates/quarto-error-catalog/error_catalog.json // Error code: Q-2-21 // Title: "Unclosed Editorial Comment" // Message: "I reached the end of the block before finding a closing ']' for the editorial comment." diff --git a/crates/qmd-syntax-helper/src/conversions/q_2_22.rs b/crates/qmd-syntax-helper/src/conversions/q_2_22.rs index 2ebd9916f..96bf211d1 100644 --- a/crates/qmd-syntax-helper/src/conversions/q_2_22.rs +++ b/crates/qmd-syntax-helper/src/conversions/q_2_22.rs @@ -3,7 +3,7 @@ // This conversion rule fixes Q-2-22 errors by adding closing ']' marks // where they are missing at the end of blocks. // -// Error catalog entry: crates/quarto-error-reporting/error_catalog.json +// Error catalog entry: crates/quarto-error-catalog/error_catalog.json // Error code: Q-2-22 // Title: "Unclosed Editorial Highlight" // Message: "I reached the end of the block before finding a closing ']' for the editorial highlight." diff --git a/crates/qmd-syntax-helper/src/conversions/q_2_23.rs b/crates/qmd-syntax-helper/src/conversions/q_2_23.rs index c12cae69f..157df0102 100644 --- a/crates/qmd-syntax-helper/src/conversions/q_2_23.rs +++ b/crates/qmd-syntax-helper/src/conversions/q_2_23.rs @@ -3,7 +3,7 @@ // This conversion rule fixes Q-2-23 errors by adding closing '$' marks // where they are missing at the end of blocks. // -// Error catalog entry: crates/quarto-error-reporting/error_catalog.json +// Error catalog entry: crates/quarto-error-catalog/error_catalog.json // Error code: Q-2-23 // Title: "Unclosed Inline Math" // Message: "I reached the end of the block before finding a closing '$' for the inline math." diff --git a/crates/qmd-syntax-helper/src/conversions/q_2_24.rs b/crates/qmd-syntax-helper/src/conversions/q_2_24.rs index 792e79dbd..827f30447 100644 --- a/crates/qmd-syntax-helper/src/conversions/q_2_24.rs +++ b/crates/qmd-syntax-helper/src/conversions/q_2_24.rs @@ -3,7 +3,7 @@ // This conversion rule fixes Q-2-24 errors by adding closing '`' marks // where they are missing at the end of blocks. // -// Error catalog entry: crates/quarto-error-reporting/error_catalog.json +// Error catalog entry: crates/quarto-error-catalog/error_catalog.json // Error code: Q-2-24 // Title: "Unclosed Code Span" // Message: "I reached the end of the block before finding a closing '`' for the code span." diff --git a/crates/qmd-syntax-helper/src/conversions/q_2_25.rs b/crates/qmd-syntax-helper/src/conversions/q_2_25.rs index bea51c473..8516d7e8d 100644 --- a/crates/qmd-syntax-helper/src/conversions/q_2_25.rs +++ b/crates/qmd-syntax-helper/src/conversions/q_2_25.rs @@ -3,7 +3,7 @@ // This conversion rule fixes Q-2-25 errors by adding closing '](url)' marks // where they are missing at the end of blocks. // -// Error catalog entry: crates/quarto-error-reporting/error_catalog.json +// Error catalog entry: crates/quarto-error-catalog/error_catalog.json // Error code: Q-2-25 // Title: "Unclosed Image" // Message: "I reached the end of the block before finding a closing '](url)' for the image." diff --git a/crates/qmd-syntax-helper/src/conversions/q_2_26.rs b/crates/qmd-syntax-helper/src/conversions/q_2_26.rs index ee039314f..5f490e964 100644 --- a/crates/qmd-syntax-helper/src/conversions/q_2_26.rs +++ b/crates/qmd-syntax-helper/src/conversions/q_2_26.rs @@ -3,7 +3,7 @@ // This conversion rule fixes Q-2-26 errors by adding closing ']' marks // where they are missing at the end of blocks. // -// Error catalog entry: crates/quarto-error-reporting/error_catalog.json +// Error catalog entry: crates/quarto-error-catalog/error_catalog.json // Error code: Q-2-26 // Title: "Unclosed Inline Footnote" // Message: "I reached the end of the block before finding a closing ']' for the inline footnote." diff --git a/crates/qmd-syntax-helper/src/conversions/q_2_28.rs b/crates/qmd-syntax-helper/src/conversions/q_2_28.rs index 1df0c7330..5d8e0cc19 100644 --- a/crates/qmd-syntax-helper/src/conversions/q_2_28.rs +++ b/crates/qmd-syntax-helper/src/conversions/q_2_28.rs @@ -3,7 +3,7 @@ // This conversion rule fixes Q-2-28 errors by removing line breaks // immediately before the escaped shortcode closing delimiter >}}} // -// Error catalog entry: crates/quarto-error-reporting/error_catalog.json +// Error catalog entry: crates/quarto-error-catalog/error_catalog.json // Error code: Q-2-28 // Title: "Line Break Before Escaped Shortcode Close" // Message: "Line breaks are not allowed immediately before the escaped shortcode closing delimiter `>}}}`." diff --git a/crates/qmd-syntax-helper/src/conversions/q_2_33.rs b/crates/qmd-syntax-helper/src/conversions/q_2_33.rs index 7b2f9836a..a14aa359a 100644 --- a/crates/qmd-syntax-helper/src/conversions/q_2_33.rs +++ b/crates/qmd-syntax-helper/src/conversions/q_2_33.rs @@ -3,7 +3,7 @@ // This conversion rule fixes Q-2-33 errors by replacing spaces with %20 // in link and image targets. // -// Error catalog entry: crates/quarto-error-reporting/error_catalog.json +// Error catalog entry: crates/quarto-error-catalog/error_catalog.json // Error code: Q-2-33 // Title: "Spaces in Link Targets" // Message: "Link targets cannot contain spaces. Replace spaces with %20." diff --git a/crates/qmd-syntax-helper/src/conversions/q_2_5.rs b/crates/qmd-syntax-helper/src/conversions/q_2_5.rs index 15c6750c1..e4bcd3a31 100644 --- a/crates/qmd-syntax-helper/src/conversions/q_2_5.rs +++ b/crates/qmd-syntax-helper/src/conversions/q_2_5.rs @@ -3,7 +3,7 @@ // This conversion rule fixes Q-2-5 errors by adding closing '_' marks // where they are missing at the end of blocks. // -// Error catalog entry: crates/quarto-error-reporting/error_catalog.json +// Error catalog entry: crates/quarto-error-catalog/error_catalog.json // Error code: Q-2-5 // Title: "Unclosed Underscore Emphasis" // Message: "I reached the end of the block before finding a closing '_' for the emphasis." diff --git a/crates/qmd-syntax-helper/src/diagnostics/q_2_30.rs b/crates/qmd-syntax-helper/src/diagnostics/q_2_30.rs index 700fdf461..70bfe93b0 100644 --- a/crates/qmd-syntax-helper/src/diagnostics/q_2_30.rs +++ b/crates/qmd-syntax-helper/src/diagnostics/q_2_30.rs @@ -7,7 +7,7 @@ // This is a LINTING diagnostic - the document parses successfully but // likely has a semantic error (the indented paragraph is NOT part of the footnote). // -// Error catalog entry: crates/quarto-error-reporting/error_catalog.json +// Error catalog entry: crates/quarto-error-catalog/error_catalog.json // Error code: Q-2-30 // Title: "Multi-Paragraph Footnote Indentation Not Supported" // diff --git a/crates/quarto-core/Cargo.toml b/crates/quarto-core/Cargo.toml index 0a4f3d0a2..4de14cda2 100644 --- a/crates/quarto-core/Cargo.toml +++ b/crates/quarto-core/Cargo.toml @@ -38,7 +38,7 @@ quarto-system-runtime.workspace = true quarto-pandoc-types.workspace = true quarto-source-map.workspace = true quarto-doctemplate.workspace = true -quarto-error-reporting.workspace = true +quarto-error-reporting = { workspace = true, features = ["json"] } quarto-config.workspace = true quarto-yaml.workspace = true quarto-ast-reconcile.workspace = true @@ -110,6 +110,7 @@ uuid.workspace = true [dev-dependencies] flate2.workspace = true insta.workspace = true +quarto-error-catalog = { workspace = true } tempfile = "3" walkdir.workspace = true tokio = { version = "1", features = ["rt", "macros"] } diff --git a/crates/quarto-core/src/project/listing/config.rs b/crates/quarto-core/src/project/listing/config.rs index 6baf749c9..4892d6dc2 100644 --- a/crates/quarto-core/src/project/listing/config.rs +++ b/crates/quarto-core/src/project/listing/config.rs @@ -14,7 +14,7 @@ //! The shape is the L2 reference document //! (`claude-notes/plans/2026-05-06-listings-L2-data-model.md`). //! Diagnostic codes for invalid shapes are `Q-12-N`, registered in -//! `crates/quarto-error-reporting/error_catalog.json`. +//! `crates/quarto-error-catalog/error_catalog.json`. use std::collections::BTreeMap; use std::path::PathBuf; diff --git a/crates/quarto-core/src/project_resources.rs b/crates/quarto-core/src/project_resources.rs index 8a81e8351..a1197eab9 100644 --- a/crates/quarto-core/src/project_resources.rs +++ b/crates/quarto-core/src/project_resources.rs @@ -1449,15 +1449,18 @@ mod tests { // `resource_error_to_parse_error` should be findable in the // shared error catalog. If a future change adds a new variant // and forgets to register a code, this test fails loudly. + // Query the catalog data directly (the codes live in + // `quarto-error-catalog` now, not in `quarto-error-reporting`). for code in ["Q-5-1", "Q-5-2", "Q-5-3"] { + let info = quarto_error_catalog::ERROR_CATALOG.get(code); assert!( - quarto_error_reporting::catalog::get_error_info(code).is_some(), + info.is_some(), "code {} is not registered in error_catalog.json", code ); assert_eq!( - quarto_error_reporting::catalog::get_subsystem(code), - Some("project"), + info.unwrap().subsystem, + "project", "code {} should be under the 'project' subsystem", code ); diff --git a/crates/quarto-core/src/theme_diagnostic.rs b/crates/quarto-core/src/theme_diagnostic.rs index fd317ea52..bb07f85eb 100644 --- a/crates/quarto-core/src/theme_diagnostic.rs +++ b/crates/quarto-core/src/theme_diagnostic.rs @@ -272,15 +272,18 @@ mod tests { // Belt-and-braces: every code emitted by // sass_error_to_parse_error must exist in the shared // catalog, under the 'theme' subsystem. + // Query the catalog data directly (the codes live in + // `quarto-error-catalog` now, not in `quarto-error-reporting`). for code in ["Q-14-1", "Q-14-2"] { + let info = quarto_error_catalog::ERROR_CATALOG.get(code); assert!( - quarto_error_reporting::catalog::get_error_info(code).is_some(), + info.is_some(), "{} is not registered in error_catalog.json", code, ); assert_eq!( - quarto_error_reporting::catalog::get_subsystem(code), - Some("theme"), + info.unwrap().subsystem, + "theme", "{} should live under the 'theme' subsystem", code, ); diff --git a/crates/quarto-error-catalog/Cargo.toml b/crates/quarto-error-catalog/Cargo.toml new file mode 100644 index 000000000..2eae0b65e --- /dev/null +++ b/crates/quarto-error-catalog/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "quarto-error-catalog" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +description = "Quarto's centralized Q-* error-code catalog: the data plus a CatalogProvider for quarto-error-reporting." + +[dependencies] +# The catalog-agnostic host: provides ErrorCodeInfo, CatalogProvider, install_catalog. +quarto-error-reporting = { workspace = true } + +once_cell = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } + +[lints] +workspace = true diff --git a/crates/quarto-error-reporting/error_catalog.json b/crates/quarto-error-catalog/error_catalog.json similarity index 100% rename from crates/quarto-error-reporting/error_catalog.json rename to crates/quarto-error-catalog/error_catalog.json diff --git a/crates/quarto-error-reporting/examples/with_error_code.rs b/crates/quarto-error-catalog/examples/with_error_code.rs similarity index 71% rename from crates/quarto-error-reporting/examples/with_error_code.rs rename to crates/quarto-error-catalog/examples/with_error_code.rs index ac236a4b3..4be132c6d 100644 --- a/crates/quarto-error-reporting/examples/with_error_code.rs +++ b/crates/quarto-error-catalog/examples/with_error_code.rs @@ -1,10 +1,16 @@ //! Error code example. //! -//! This example shows how to use error codes from the catalog and get documentation URLs. +//! Shows how to use Quarto's `Q-*` error catalog: install it, build a +//! diagnostic with a code, resolve its documentation URL, and browse the +//! catalog. Run with: `cargo run -p quarto-error-catalog --example with_error_code`. -use quarto_error_reporting::{DiagnosticMessageBuilder, catalog}; +use quarto_error_catalog::ERROR_CATALOG; +use quarto_error_reporting::{DiagnosticMessageBuilder, get_error_info}; fn main() { + // Install the Q-* catalog so docs URLs and catalog lookups resolve. + quarto_error_catalog::install(); + println!("=== Example 1: Using an error code ===\n"); let error = DiagnosticMessageBuilder::error("Internal Error") @@ -16,14 +22,14 @@ fn main() { println!("{}", error.to_text(None)); - // Get docs URL + // Get docs URL (resolves because the catalog is installed). if let Some(url) = error.docs_url() { println!("\nDocumentation: {}", url); } println!("\n=== Example 2: Looking up error info from catalog ===\n"); - if let Some(info) = catalog::get_error_info("Q-0-1") { + if let Some(info) = get_error_info("Q-0-1") { println!("Error code: Q-0-1"); println!("Subsystem: {}", info.subsystem); println!("Title: {}", info.title); @@ -48,16 +54,16 @@ fn main() { println!("\n=== Example 4: Browsing the catalog ===\n"); - // List all markdown parsing errors (Q-2-*) + // List all markdown parsing errors (Q-2-*) directly from the data. println!("Markdown parsing errors in catalog:"); - let mut markdown_codes: Vec<_> = catalog::ERROR_CATALOG + let mut markdown_codes: Vec<_> = ERROR_CATALOG .keys() .filter(|k| k.starts_with("Q-2-")) .collect(); markdown_codes.sort(); for code in markdown_codes.iter().take(5) { - if let Some(info) = catalog::ERROR_CATALOG.get(*code) { + if let Some(info) = ERROR_CATALOG.get(*code) { println!(" {} - {}", code, info.title); } } diff --git a/crates/quarto-error-catalog/src/lib.rs b/crates/quarto-error-catalog/src/lib.rs new file mode 100644 index 000000000..fd6c840ab --- /dev/null +++ b/crates/quarto-error-catalog/src/lib.rs @@ -0,0 +1,306 @@ +//! Quarto's centralized `Q-*` error-code catalog. +//! +//! `quarto-error-reporting` is catalog-agnostic — it defines the catalog +//! *shape* and the [`CatalogProvider`] seam but ships no data. This crate +//! carries Quarto's *policy*: the `Q--` codes, their titles and +//! `quarto.org` documentation URLs (`error_catalog.json`), and a +//! [`CatalogProvider`] implementation over them. +//! +//! An embedding binary installs this catalog once, early (e.g. at the top of +//! `main`), via [`install`]: +//! +//! ```no_run +//! quarto_error_catalog::install(); +//! // From here, quarto_error_reporting::get_docs_url("Q-0-1") resolves. +//! ``` +//! +//! See `claude-notes/designs/cross-package-error-codes.md` for the discipline +//! this implements (this crate is the q2 *presentation*-code policy). + +use once_cell::sync::Lazy; +use quarto_error_reporting::{CatalogProvider, ErrorCodeInfo, install_catalog}; +use std::collections::HashMap; + +/// The Quarto `Q-*` error catalog, loaded once from the embedded +/// `error_catalog.json`. +/// +/// The JSON is embedded at compile time via `include_str!`, so there is no +/// runtime file I/O. +/// +/// # Panics +/// +/// Panics if the embedded JSON is invalid — only possible if someone edits the +/// catalog incorrectly during development. +pub static ERROR_CATALOG: Lazy> = Lazy::new(|| { + let json_data = include_str!("../error_catalog.json"); + serde_json::from_str(json_data).expect("Invalid error catalog JSON - this is a bug in Quarto") +}); + +/// A [`CatalogProvider`] backed by Quarto's embedded `Q-*` catalog. +pub struct QuartoCatalog; + +impl CatalogProvider for QuartoCatalog { + fn lookup(&self, code: &str) -> Option<&ErrorCodeInfo> { + // `ERROR_CATALOG` is `'static`; the returned reference outlives `&self`. + ERROR_CATALOG.get(code) + } +} + +/// Install Quarto's `Q-*` catalog as the process-wide +/// [`CatalogProvider`](quarto_error_reporting::CatalogProvider). +/// +/// Idempotent (first install wins). Call once, as early as possible, from a +/// binary's `main` / the WASM bootstrap, before any diagnostic's docs URL is +/// resolved. Behaviour is unaffected if called multiple times. +pub fn install() { + install_catalog(Box::new(QuartoCatalog)); +} + +#[cfg(test)] +mod tests { + use super::*; + + // ─── Catalog data presence (ported from quarto-error-reporting) ────────── + // + // These assert the *data* in `error_catalog.json` directly via the embedded + // map, so they do not depend on the installed-global state. + + #[test] + fn catalog_loads_and_is_nonempty() { + assert!(!ERROR_CATALOG.is_empty()); + } + + #[test] + fn internal_error_q_0_1_exists() { + let info = ERROR_CATALOG.get("Q-0-1").expect("Q-0-1 must exist"); + assert_eq!(info.subsystem, "internal"); + assert_eq!(info.title, "Internal Error"); + assert!(info.docs_url.is_some()); + assert!( + info.docs_url + .as_deref() + .unwrap() + .starts_with("https://quarto.org/docs/errors/") + ); + } + + #[test] + fn nonexistent_code_is_absent() { + assert!(ERROR_CATALOG.get("Q-999-999").is_none()); // quarto-error-code-audit-ignore + } + + // L8 / bd-rqgx: Q-12-14 catalog presence. + #[test] + fn error_catalog_has_q_12_14() { + let info = ERROR_CATALOG + .get("Q-12-14") + .expect("Q-12-14 must be in the catalog"); + assert_eq!(info.subsystem, "listing"); + assert_eq!(info.title, "Listing Type custom Without template Path"); + assert!( + info.message_template.contains("type: custom"), + "Q-12-14 message must mention `type: custom`; got: {}", + info.message_template + ); + assert!( + info.message_template.contains("default"), + "Q-12-14 message must mention the default fallback; got: {}", + info.message_template + ); + } + + // bd-8d6rk: Q-13-1..7 navigation subsystem catalog presence. + #[test] + fn error_catalog_has_q_13_navigation_codes() { + let cases: &[(&str, &str, &str)] = &[ + ("Q-13-1", "Sidebar", "missing document"), + ("Q-13-2", "Navbar", "missing document"), + ("Q-13-3", "Page footer", "missing document"), + ("Q-13-4", "Body link", "missing document"), + ("Q-13-5", "auto:", "project index"), + ("Q-13-6", "auto:", "no documents"), + ("Q-13-7", "Page navigation", "missing document"), + ]; + for (code, title_substr, message_substr) in cases { + let info = ERROR_CATALOG + .get(*code) + .unwrap_or_else(|| panic!("{} must be in the catalog", code)); + assert_eq!( + info.subsystem, "navigation", + "{} should be in the navigation subsystem; got: {}", + code, info.subsystem + ); + assert!( + info.title.contains(title_substr), + "{} title must mention `{}`; got: {}", + code, + title_substr, + info.title + ); + assert!( + info.message_template.contains(message_substr), + "{} message must mention `{}`; got: {}", + code, + message_substr, + info.message_template + ); + assert!( + info.docs_url.as_deref().is_some_and(|u| u.ends_with(code)), + "{} docs_url must end with {}; got: {:?}", + code, + code, + info.docs_url + ); + } + } + + // L9 / bd-o90m: Q-12-15 + Q-12-16 catalog presence. + #[test] + fn error_catalog_has_q_12_15_and_q_12_16() { + let q15 = ERROR_CATALOG + .get("Q-12-15") + .expect("Q-12-15 must be in the catalog"); + assert_eq!(q15.subsystem, "listing"); + assert!( + q15.title.to_lowercase().contains("feed"), + "Q-12-15 title must mention feed; got: {}", + q15.title + ); + assert!( + q15.message_template.contains("site-url"), + "Q-12-15 message must mention `site-url`; got: {}", + q15.message_template + ); + + let q16 = ERROR_CATALOG + .get("Q-12-16") + .expect("Q-12-16 must be in the catalog"); + assert_eq!(q16.subsystem, "listing"); + assert!( + q16.title.to_lowercase().contains("feed"), + "Q-12-16 title must mention feed; got: {}", + q16.title + ); + assert!( + q16.message_template.contains("description"), + "Q-12-16 message must mention the empty description fallback; got: {}", + q16.message_template + ); + } + + // bd-bxrkxblx: Q-5-6 / Q-5-7 resource-copy diagnostics. + #[test] + fn error_catalog_has_q_5_6_and_q_5_7() { + let q6 = ERROR_CATALOG + .get("Q-5-6") + .expect("Q-5-6 must be in the catalog"); + assert_eq!(q6.subsystem, "project"); + assert!( + q6.title.to_lowercase().contains("resource"), + "Q-5-6 title must mention `resource`; got: {}", + q6.title + ); + assert!( + q6.message_template.to_lowercase().contains("not exist") + || q6.message_template.to_lowercase().contains("missing") + || q6.message_template.to_lowercase().contains("not found"), + "Q-5-6 message must describe the missing source; got: {}", + q6.message_template + ); + assert!( + q6.docs_url.as_deref().is_some_and(|u| u.ends_with("Q-5-6")), + "Q-5-6 docs_url must end with the code; got: {:?}", + q6.docs_url + ); + + let q7 = ERROR_CATALOG + .get("Q-5-7") + .expect("Q-5-7 must be in the catalog"); + assert_eq!(q7.subsystem, "project"); + assert!( + q7.title.to_lowercase().contains("copy") + || q7.title.to_lowercase().contains("resource"), + "Q-5-7 title must mention copy/resource; got: {}", + q7.title + ); + assert!( + q7.message_template.to_lowercase().contains("copy"), + "Q-5-7 message must mention copying; got: {}", + q7.message_template + ); + assert!( + q7.docs_url.as_deref().is_some_and(|u| u.ends_with("Q-5-7")), + "Q-5-7 docs_url must end with the code; got: {:?}", + q7.docs_url + ); + } + + // bd-rr6qzcvu: Q-15-1 — the crossref subsystem's first code. + #[test] + fn error_catalog_has_q_15_1_crossref() { + let info = ERROR_CATALOG + .get("Q-15-1") + .expect("Q-15-1 must be in the catalog"); + assert_eq!(info.subsystem, "crossref"); + assert!( + info.title.to_lowercase().contains("duplicate") + && info.title.to_lowercase().contains("crossref"), + "Q-15-1 title must mention a duplicate crossref; got: {}", + info.title + ); + assert!( + info.message_template.to_lowercase().contains("unique") + || info + .message_template + .to_lowercase() + .contains("more than once"), + "Q-15-1 message must explain the uniqueness requirement; got: {}", + info.message_template + ); + assert!( + info.docs_url + .as_deref() + .is_some_and(|u| u.ends_with("Q-15-1")), + "Q-15-1 docs_url must end with the code; got: {:?}", + info.docs_url + ); + } + + // ─── Integration: install() wires this catalog into quarto-error-reporting ─ + // + // These exercise the installed-global delegation path end-to-end. Each calls + // `install()` (idempotent); under nextest every test is its own process, and + // they all install the *same* QuartoCatalog, so there is no conflict. + + #[test] + fn install_makes_get_docs_url_resolve() { + install(); + let url = quarto_error_reporting::get_docs_url("Q-0-1") + .expect("Q-0-1 should resolve after install()"); + assert!(url.starts_with("https://quarto.org/docs/errors/")); + assert!(url.contains("Q-0-1")); + } + + #[test] + fn install_makes_get_subsystem_resolve() { + install(); + assert_eq!( + quarto_error_reporting::get_subsystem("Q-0-1"), + Some("internal") + ); + assert!(quarto_error_reporting::get_subsystem("Q-999-999").is_none()); // quarto-error-code-audit-ignore + } + + // Relocated from quarto-error-reporting's `diagnostic.rs::test_docs_url`: + // a DiagnosticMessage's `docs_url()` resolves once the Q-* catalog is installed. + #[test] + fn diagnostic_docs_url_resolves_after_install() { + install(); + let msg = + quarto_error_reporting::DiagnosticMessage::error("Internal Error").with_code("Q-0-1"); + let url = msg + .docs_url() + .expect("docs_url should resolve after install()"); + assert!(url.contains("Q-0-1")); + } +} diff --git a/crates/quarto-error-reporting/Cargo.toml b/crates/quarto-error-reporting/Cargo.toml index 780a8b151..90af34f0d 100644 --- a/crates/quarto-error-reporting/Cargo.toml +++ b/crates/quarto-error-reporting/Cargo.toml @@ -14,24 +14,31 @@ quarto-source-map = { workspace = true } # Error reporting ariadne = { workspace = true } thiserror = { workspace = true } -once_cell = { workspace = true } # Serialization serde = { workspace = true } serde_json = { workspace = true } # JSON Schema generation for the wire format (bd-iey8o). Scoped to -# this crate: `schemars` documents machine-to-machine JSON shapes -# emitted by Quarto. Do NOT add it to other crates for the purpose -# of validating user-authored YAML config — that's the job of -# `quarto-yaml-validation`, which speaks Quarto's own dialect. See -# `claude-notes/plans/2026-05-22-q2-render-json-errors.md` § -# "Coexistence with quarto-yaml-validation". -schemars = { workspace = true } +# this crate AND to the `json` feature: `schemars` documents the +# machine-to-machine JSON shapes in `json.rs`. Do NOT add it to other +# crates for the purpose of validating user-authored YAML config — +# that's the job of `quarto-yaml-validation`, which speaks Quarto's own +# dialect. See `claude-notes/plans/2026-05-22-q2-render-json-errors.md` +# § "Coexistence with quarto-yaml-validation". +schemars = { workspace = true, optional = true } # URL handling for file:// hyperlinks url = "2.5" +[features] +# The `json.rs` wire shape (JsonDiagnostic etc.) and its `schemars` +# dependency are opt-in: the q2 consumers (quarto, quarto-core, +# quarto-preview, wasm-quarto-hub-client) enable `json`, while a +# standalone non-Quarto consumer gets a leaner build with no schemars. +# Default-off so the published crate stays minimal. +json = ["dep:schemars"] + [dev-dependencies] # No dev dependencies yet diff --git a/crates/quarto-error-reporting/src/catalog.rs b/crates/quarto-error-reporting/src/catalog.rs index 4aed037e3..6256839cc 100644 --- a/crates/quarto-error-reporting/src/catalog.rs +++ b/crates/quarto-error-reporting/src/catalog.rs @@ -1,16 +1,23 @@ -//! Error code catalog and lookup. +//! Pluggable error-code catalog. //! -//! This module provides access to the centralized error catalog, which maps -//! error codes (like "Q-1-1") to their metadata (title, message template, docs URL, etc.). +//! `quarto-error-reporting` is **catalog-agnostic**: it defines the catalog +//! *shape* ([`ErrorCodeInfo`]) and a [`CatalogProvider`] seam, but ships no +//! catalog *data*. An embedding product installs its own catalog once, early, +//! via [`install_catalog`]; in Quarto this is done by the `quarto-error-catalog` +//! crate (`quarto_error_catalog::install()`), which carries the `Q-*` +//! `error_catalog.json`. With nothing installed, every lookup returns `None` +//! (see [`EmptyCatalog`]). +//! +//! This is the host side of the cross-package error-code discipline; see +//! `claude-notes/designs/cross-package-error-codes.md`. -use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use std::sync::OnceLock; /// Metadata for an error code. /// -/// Each entry in the error catalog describes a specific error code, -/// including its subsystem, title, default message, and documentation URL. +/// Each catalog entry describes a specific error code, including its +/// subsystem, title, default message, and documentation URL. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct ErrorCodeInfo { /// Subsystem name (e.g., "yaml", "markdown", "engine") @@ -30,288 +37,161 @@ pub struct ErrorCodeInfo { pub since_version: String, } -/// Global error catalog, loaded lazily from JSON at compile time. +/// A source of error-code metadata, supplied by the embedding product. /// -/// The catalog is loaded from `error_catalog.json` using `include_str!()`, -/// which embeds the JSON at compile time. This means no runtime file I/O. +/// Implementors return metadata for a given code, or `None` if the code is not +/// in their catalog. The returned reference is tied to `&self`, which lets the +/// installed-global path (see [`install_catalog`]) hand out `&'static` +/// references — the installed provider lives for the rest of the process. /// -/// # Panics +/// `Send + Sync` is required so the provider can live in a process-wide +/// [`OnceLock`]; it costs nothing for the data-only providers in practice. +pub trait CatalogProvider: Send + Sync { + /// Look up an error code's metadata, or `None` if it is not in this catalog. + fn lookup(&self, code: &str) -> Option<&ErrorCodeInfo>; +} + +/// The default provider used when none has been installed: every lookup is +/// `None`. This is what makes the crate usable standalone with zero config — a +/// non-Quarto consumer that installs nothing simply gets code-less, URL-less +/// diagnostics (tier-2 "passthrough" in the discipline's terms). +pub struct EmptyCatalog; + +impl CatalogProvider for EmptyCatalog { + fn lookup(&self, _code: &str) -> Option<&ErrorCodeInfo> { + None + } +} + +/// The process-wide installed catalog. Written at most once, by the embedder. +static CATALOG: OnceLock> = OnceLock::new(); + +/// Install the process-wide catalog provider. /// -/// Panics if the embedded JSON is invalid. This should only happen during -/// development if someone manually edits the catalog incorrectly. -pub static ERROR_CATALOG: Lazy> = Lazy::new(|| { - let json_data = include_str!("../error_catalog.json"); - serde_json::from_str(json_data).expect("Invalid error catalog JSON - this is a bug in Quarto") -}); +/// The **first** call wins; later calls are no-ops (so a double install — e.g. +/// a binary's `main` plus a test helper — is harmless). Embedders should call +/// this once, as early as possible, at binary / WASM startup, *before* any +/// diagnostic's docs URL is resolved. +pub fn install_catalog(provider: Box) { + let _ = CATALOG.set(provider); +} + +/// The installed provider, or a shared [`EmptyCatalog`] if none was installed. +fn catalog() -> &'static dyn CatalogProvider { + static EMPTY: EmptyCatalog = EmptyCatalog; + match CATALOG.get() { + Some(provider) => &**provider, + None => &EMPTY, + } +} -/// Look up error code information. +/// Look up full metadata for an error code via the installed catalog. /// -/// Returns `None` if the error code is not found in the catalog. +/// Returns `None` if no catalog is installed, or the code is not in it. /// /// # Example /// /// ``` /// use quarto_error_reporting::catalog::get_error_info; /// -/// if let Some(info) = get_error_info("Q-0-1") { -/// println!("Error: {} - {}", info.title, info.message_template); -/// } +/// // With a `CatalogProvider` installed (e.g. via `quarto-error-catalog`), +/// // this resolves to the code's metadata; with none installed it is `None`. +/// let _ = get_error_info("Q-0-1"); /// ``` -pub fn get_error_info(code: &str) -> Option<&ErrorCodeInfo> { - ERROR_CATALOG.get(code) +pub fn get_error_info(code: &str) -> Option<&'static ErrorCodeInfo> { + catalog().lookup(code) } -/// Get documentation URL for an error code. +/// Get the documentation URL for an error code, if the installed catalog has one. /// -/// Returns `None` if the error code is not found or has no documentation URL. +/// Returns `None` if no catalog is installed, the code is unknown, or the entry +/// has no documentation URL. /// /// # Example /// /// ``` /// use quarto_error_reporting::catalog::get_docs_url; /// -/// if let Some(url) = get_docs_url("Q-0-1") { -/// println!("See {} for more information", url); -/// } +/// // `Some(url)` iff a catalog mapping this code (with a docs URL) is installed. +/// let _ = get_docs_url("Q-0-1"); /// ``` -pub fn get_docs_url(code: &str) -> Option<&str> { - ERROR_CATALOG - .get(code) +pub fn get_docs_url(code: &str) -> Option<&'static str> { + catalog() + .lookup(code) .and_then(|info| info.docs_url.as_deref()) } -/// Get the subsystem name for an error code. +/// Get the subsystem name for an error code, if the installed catalog knows it. /// -/// Returns `None` if the error code is not found. +/// Returns `None` if no catalog is installed or the code is unknown. /// /// # Example /// /// ``` /// use quarto_error_reporting::catalog::get_subsystem; /// -/// assert_eq!(get_subsystem("Q-0-1"), Some("internal")); +/// // With a catalog installed this returns e.g. `Some("internal")` for "Q-0-1". +/// let _ = get_subsystem("Q-0-1"); /// ``` -pub fn get_subsystem(code: &str) -> Option<&str> { - ERROR_CATALOG.get(code).map(|info| info.subsystem.as_str()) +pub fn get_subsystem(code: &str) -> Option<&'static str> { + catalog().lookup(code).map(|info| info.subsystem.as_str()) } #[cfg(test)] mod tests { use super::*; - #[test] - fn test_catalog_loads() { - // Just accessing ERROR_CATALOG will trigger loading - // If the JSON is invalid, this will panic - assert!(!ERROR_CATALOG.is_empty()); - } - - #[test] - fn test_internal_error_exists() { - let info = get_error_info("Q-0-1"); - assert!(info.is_some()); - - let info = info.unwrap(); - assert_eq!(info.subsystem, "internal"); - assert_eq!(info.title, "Internal Error"); - assert!(info.docs_url.is_some()); - } - - #[test] - fn test_get_docs_url() { - let url = get_docs_url("Q-0-1"); - assert!(url.is_some()); - assert!(url.unwrap().starts_with("https://quarto.org/docs/errors/")); + fn sample_info(subsystem: &str, docs_url: Option<&str>) -> ErrorCodeInfo { + ErrorCodeInfo { + subsystem: subsystem.to_string(), + title: "Sample".to_string(), + message_template: "sample".to_string(), + docs_url: docs_url.map(str::to_string), + since_version: "0.0.0".to_string(), + } } + /// The default provider returns `None` for everything. Tested *directly* + /// (no global state) so it is robust regardless of test runner — this is + /// the canonical "no catalog installed → None" behaviour. #[test] - fn test_get_subsystem() { - assert_eq!(get_subsystem("Q-0-1"), Some("internal")); - assert_eq!(get_subsystem("Q-999-999"), None); // quarto-error-code-audit-ignore + fn empty_catalog_returns_none() { + let empty = EmptyCatalog; + assert!(empty.lookup("Q-0-1").is_none()); + assert!(empty.lookup("anything").is_none()); } - #[test] - fn test_nonexistent_code() { - assert!(get_error_info("Q-999-999").is_none()); // quarto-error-code-audit-ignore - assert!(get_docs_url("Q-999-999").is_none()); // quarto-error-code-audit-ignore + /// A trivial mock provider implements the trait and is found by lookup. + struct MockCatalog { + entry: ErrorCodeInfo, } - - // L8 / bd-rqgx test #1: Q-12-14 catalog presence. - #[test] - fn error_catalog_has_q_12_14() { - let info = get_error_info("Q-12-14").expect("Q-12-14 must be in the catalog"); - assert_eq!(info.subsystem, "listing"); - assert_eq!(info.title, "Listing Type custom Without template Path"); - assert!( - info.message_template.contains("type: custom"), - "Q-12-14 message must mention `type: custom`; got: {}", - info.message_template - ); - assert!( - info.message_template.contains("default"), - "Q-12-14 message must mention the default fallback; got: {}", - info.message_template - ); - } - - // bd-8d6rk: Q-13-1..6 navigation subsystem catalog presence. - // - // These six codes back the structured-diagnostic migration of the - // sidebar / navbar / page-footer / body-link / sidebar `auto:` - // warnings (previously plain `DiagnosticMessage::warning(format!())` - // strings without codes). See - // `claude-notes/plans/2026-05-20-bd-8d6rk-navigation-diagnostics.md` - // for the per-code title/problem/hint table. - #[test] - fn error_catalog_has_q_13_navigation_codes() { - let cases: &[(&str, &str, &str)] = &[ - ("Q-13-1", "Sidebar", "missing document"), - ("Q-13-2", "Navbar", "missing document"), - ("Q-13-3", "Page footer", "missing document"), - ("Q-13-4", "Body link", "missing document"), - ("Q-13-5", "auto:", "project index"), - ("Q-13-6", "auto:", "no documents"), - ("Q-13-7", "Page navigation", "missing document"), - ]; - for (code, title_substr, message_substr) in cases { - let info = - get_error_info(code).unwrap_or_else(|| panic!("{} must be in the catalog", code)); - assert_eq!( - info.subsystem, "navigation", - "{} should be in the navigation subsystem; got: {}", - code, info.subsystem - ); - assert!( - info.title.contains(title_substr), - "{} title must mention `{}`; got: {}", - code, - title_substr, - info.title - ); - assert!( - info.message_template.contains(message_substr), - "{} message must mention `{}`; got: {}", - code, - message_substr, - info.message_template - ); - assert!( - info.docs_url.as_deref().is_some_and(|u| u.ends_with(code)), - "{} docs_url must end with {}; got: {:?}", - code, - code, - info.docs_url - ); + impl CatalogProvider for MockCatalog { + fn lookup(&self, code: &str) -> Option<&ErrorCodeInfo> { + (code == "Q-0-1").then_some(&self.entry) } } - // L9 / bd-o90m test #1: Q-12-15 + Q-12-16 catalog presence. - #[test] - fn error_catalog_has_q_12_15_and_q_12_16() { - let q15 = get_error_info("Q-12-15").expect("Q-12-15 must be in the catalog"); - assert_eq!(q15.subsystem, "listing"); - assert!( - q15.title.to_lowercase().contains("feed"), - "Q-12-15 title must mention feed; got: {}", - q15.title - ); - assert!( - q15.message_template.contains("site-url"), - "Q-12-15 message must mention `site-url`; got: {}", - q15.message_template - ); - - let q16 = get_error_info("Q-12-16").expect("Q-12-16 must be in the catalog"); - assert_eq!(q16.subsystem, "listing"); - assert!( - q16.title.to_lowercase().contains("feed"), - "Q-12-16 title must mention feed; got: {}", - q16.title - ); - assert!( - q16.message_template.contains("description"), - "Q-12-16 message must mention the empty description fallback; got: {}", - q16.message_template - ); - } - - // bd-bxrkxblx: Q-5-6 / Q-5-7 resource-copy diagnostics. - // - // Q-5-6 is the span-aware *warning* for a referenced resource - // whose source file is missing (detected before the copy is - // attempted). Q-5-7 is the hard *error* for a copy/write that - // fails at flush for an environment reason (permission denied, - // disk full). See - // `claude-notes/plans/2026-06-19-resource-copy-error-diagnostic.md`. + /// The **single** test in this crate that mutates the process-global + /// catalog: install a mock and assert the free functions delegate to it for + /// a known code, and return `None` for an unknown one. Keeping this the only + /// global-mutating test means there is no intra-process install conflict, + /// even under `cargo test` (threads) rather than nextest (process-per-test). #[test] - fn error_catalog_has_q_5_6_and_q_5_7() { - let q6 = get_error_info("Q-5-6").expect("Q-5-6 must be in the catalog"); - assert_eq!(q6.subsystem, "project"); - assert!( - q6.title.to_lowercase().contains("resource"), - "Q-5-6 title must mention `resource`; got: {}", - q6.title - ); - assert!( - q6.message_template.to_lowercase().contains("not exist") - || q6.message_template.to_lowercase().contains("missing") - || q6.message_template.to_lowercase().contains("not found"), - "Q-5-6 message must describe the missing source; got: {}", - q6.message_template - ); - assert!( - q6.docs_url.as_deref().is_some_and(|u| u.ends_with("Q-5-6")), - "Q-5-6 docs_url must end with the code; got: {:?}", - q6.docs_url - ); + fn installed_catalog_is_used_by_lookups() { + install_catalog(Box::new(MockCatalog { + entry: sample_info("internal", Some("https://example.test/docs/Q-0-1")), + })); - let q7 = get_error_info("Q-5-7").expect("Q-5-7 must be in the catalog"); - assert_eq!(q7.subsystem, "project"); - assert!( - q7.title.to_lowercase().contains("copy") - || q7.title.to_lowercase().contains("resource"), - "Q-5-7 title must mention copy/resource; got: {}", - q7.title - ); - assert!( - q7.message_template.to_lowercase().contains("copy"), - "Q-5-7 message must mention copying; got: {}", - q7.message_template - ); - assert!( - q7.docs_url.as_deref().is_some_and(|u| u.ends_with("Q-5-7")), - "Q-5-7 docs_url must end with the code; got: {:?}", - q7.docs_url + assert_eq!(get_subsystem("Q-0-1"), Some("internal")); + assert_eq!( + get_docs_url("Q-0-1"), + Some("https://example.test/docs/Q-0-1") ); - } + assert!(get_error_info("Q-0-1").is_some()); - // bd-rr6qzcvu: Q-15-1 — the new `crossref` subsystem's first code, - // for a duplicate crossref identifier. - #[test] - fn error_catalog_has_q_15_1_crossref() { - let info = get_error_info("Q-15-1").expect("Q-15-1 must be in the catalog"); - assert_eq!(info.subsystem, "crossref"); - assert!( - info.title.to_lowercase().contains("duplicate") - && info.title.to_lowercase().contains("crossref"), - "Q-15-1 title must mention a duplicate crossref; got: {}", - info.title - ); - assert!( - info.message_template.to_lowercase().contains("unique") - || info - .message_template - .to_lowercase() - .contains("more than once"), - "Q-15-1 message must explain the uniqueness requirement; got: {}", - info.message_template - ); - assert!( - info.docs_url - .as_deref() - .is_some_and(|u| u.ends_with("Q-15-1")), - "Q-15-1 docs_url must end with the code; got: {:?}", - info.docs_url - ); + // Unknown code, even with a catalog installed, is `None`. + assert!(get_subsystem("Q-9-9").is_none()); + assert!(get_docs_url("Q-9-9").is_none()); + assert!(get_error_info("Q-9-9").is_none()); } } diff --git a/crates/quarto-error-reporting/src/diagnostic.rs b/crates/quarto-error-reporting/src/diagnostic.rs index d86e503dd..a402084d3 100644 --- a/crates/quarto-error-reporting/src/diagnostic.rs +++ b/crates/quarto-error-reporting/src/diagnostic.rs @@ -276,13 +276,18 @@ impl DiagnosticMessage { /// /// # Example /// + /// Resolves the code against the installed [`CatalogProvider`] + /// (`crate::catalog`); returns `None` when no catalog is installed, the + /// code is unknown, or the entry has no docs URL. + /// /// ``` /// use quarto_error_reporting::DiagnosticMessage; /// /// let msg = DiagnosticMessage::error("Internal Error") /// .with_code("Q-0-1"); /// - /// assert!(msg.docs_url().is_some()); + /// // `Some(url)` iff a catalog mapping "Q-0-1" (with a docs URL) is installed. + /// let _ = msg.docs_url(); /// ``` pub fn docs_url(&self) -> Option<&str> { self.code @@ -928,12 +933,11 @@ mod tests { assert_eq!(msg.code, Some("Q-1-1".to_string())); } - #[test] - fn test_docs_url() { - let msg = DiagnosticMessage::error("Internal Error").with_code("Q-0-1"); - assert!(msg.docs_url().is_some()); - assert!(msg.docs_url().unwrap().contains("Q-0-1")); - } + // The positive case — `docs_url()` for a real code resolves to the + // quarto.org URL — moved to `quarto-error-catalog`'s integration tests, + // where the `Q-*` catalog is installed. Here we only cover the + // catalog-free cases (no code / unknown code → `None`), which hold + // regardless of whether a catalog is installed. #[test] fn test_docs_url_without_code() { diff --git a/crates/quarto-error-reporting/src/lib.rs b/crates/quarto-error-reporting/src/lib.rs index 51af77b9f..66bd92630 100644 --- a/crates/quarto-error-reporting/src/lib.rs +++ b/crates/quarto-error-reporting/src/lib.rs @@ -62,7 +62,10 @@ pub mod builder; // (WASM render bridge) and quarto-preview (server-side diagnostics // endpoint). Lifted from wasm-quarto-hub-client under bd-b9kzg so // the q2-preview SPA can consume both feeds without a translation -// layer. +// layer. Behind the default-off `json` feature (carries `schemars` +// and Quarto's `quarto.org` schema URLs) so the published crate stays +// minimal for non-Quarto consumers. +#[cfg(feature = "json")] pub mod json; // Macros for convenient error creation @@ -77,11 +80,15 @@ pub mod coalesce; // Re-export main types for convenience pub use builder::DiagnosticMessageBuilder; -pub use catalog::{ERROR_CATALOG, ErrorCodeInfo, get_docs_url, get_error_info, get_subsystem}; +pub use catalog::{ + CatalogProvider, EmptyCatalog, ErrorCodeInfo, get_docs_url, get_error_info, get_subsystem, + install_catalog, +}; pub use coalesce::{CoalescedDiagnostic, coalesce_by_source}; pub use diagnostic::{ DetailItem, DetailKind, DiagnosticKind, DiagnosticMessage, MessageContent, TextRenderOptions, }; +#[cfg(feature = "json")] pub use json::{ JsonDiagnostic, JsonDiagnosticDetail, JsonPass1Failure, diagnostic_to_json, with_source_file, }; diff --git a/crates/quarto-error-reporting/tests/schema_drift.rs b/crates/quarto-error-reporting/tests/schema_drift.rs index 5a04a373e..6eab0b955 100644 --- a/crates/quarto-error-reporting/tests/schema_drift.rs +++ b/crates/quarto-error-reporting/tests/schema_drift.rs @@ -25,6 +25,11 @@ //! # Regenerate after editing JsonDiagnostic / JsonPass1Failure: //! QUARTO_REGEN_SCHEMAS=1 cargo nextest run -p quarto-error-reporting --test schema_drift //! ``` +//! +//! Gated on the `json` feature (the wire shapes live behind it); compiles to an +//! empty test binary when `json` is off. In the q2 workspace the feature is on +//! (enabled by `quarto`/`quarto-core`/… via unification). +#![cfg(feature = "json")] use std::path::PathBuf; diff --git a/crates/quarto-preview/Cargo.toml b/crates/quarto-preview/Cargo.toml index c52ddbc2c..8f9a91a25 100644 --- a/crates/quarto-preview/Cargo.toml +++ b/crates/quarto-preview/Cargo.toml @@ -26,7 +26,7 @@ automerge = { version = "0.8.0", features = ["utf16-indexing"] } samod = { version = "0.9.0", git = "https://github.com/quarto-dev/samod.git", branch = "q2", features = ["tokio"] } quarto-core.workspace = true -quarto-error-reporting.workspace = true +quarto-error-reporting = { workspace = true, features = ["json"] } quarto-hub.workspace = true quarto-pandoc-types.workspace = true quarto-source-map.workspace = true diff --git a/crates/quarto/Cargo.toml b/crates/quarto/Cargo.toml index 5ab0e44c7..fed706f29 100644 --- a/crates/quarto/Cargo.toml +++ b/crates/quarto/Cargo.toml @@ -23,7 +23,8 @@ pollster.workspace = true tokio.workspace = true quarto-core = { workspace = true, features = ["clap"] } -quarto-error-reporting.workspace = true +quarto-error-catalog.workspace = true +quarto-error-reporting = { workspace = true, features = ["json"] } quarto-source-map.workspace = true quarto-trace.workspace = true quarto-trace-server.workspace = true diff --git a/crates/quarto/src/commands/render.rs b/crates/quarto/src/commands/render.rs index ff188d1da..922b50663 100644 --- a/crates/quarto/src/commands/render.rs +++ b/crates/quarto/src/commands/render.rs @@ -1096,7 +1096,7 @@ fn emit_parse_error_json(parse_error: &quarto_core::ParseError, input: Option<&P /// `Q-7-N` code, then emit on stderr as one JSON line. /// /// Codes are catalog entries in -/// `crates/quarto-error-reporting/error_catalog.json` — keep them +/// `crates/quarto-error-catalog/error_catalog.json` — keep them /// in sync. fn emit_dispatch_error_json(e: &DispatchError) { emit_json_line(&diagnostic_to_json( diff --git a/crates/quarto/src/main.rs b/crates/quarto/src/main.rs index f2274fed5..a08f9f912 100644 --- a/crates/quarto/src/main.rs +++ b/crates/quarto/src/main.rs @@ -609,6 +609,11 @@ enum TraceCommand { } fn main() -> Result<()> { + // Install Quarto's `Q-*` error catalog into the catalog-agnostic + // `quarto-error-reporting` host, so diagnostics can resolve docs URLs and + // catalog metadata. Must run before any diagnostic is rendered; idempotent. + quarto_error_catalog::install(); + let cli = Cli::parse(); // Initialize logging. The `-v` flag chooses a default filter diff --git a/crates/wasm-quarto-hub-client/Cargo.lock b/crates/wasm-quarto-hub-client/Cargo.lock index d5804cf97..a5635cb17 100644 --- a/crates/wasm-quarto-hub-client/Cargo.lock +++ b/crates/wasm-quarto-hub-client/Cargo.lock @@ -2224,7 +2224,6 @@ name = "quarto-error-reporting" version = "0.7.0" dependencies = [ "ariadne", - "once_cell", "quarto-source-map", "schemars", "serde", diff --git a/crates/wasm-quarto-hub-client/Cargo.toml b/crates/wasm-quarto-hub-client/Cargo.toml index a4228c2f0..53e7a7bab 100644 --- a/crates/wasm-quarto-hub-client/Cargo.toml +++ b/crates/wasm-quarto-hub-client/Cargo.toml @@ -13,7 +13,10 @@ crate-type = ["cdylib"] pampa = { path = "../pampa", default-features = false, features = ["lua-filter"] } quarto-ast-reconcile = { path = "../quarto-ast-reconcile" } quarto-core = { path = "../quarto-core" } -quarto-error-reporting = { path = "../quarto-error-reporting" } +# NOTE: `quarto-error-catalog` is intentionally NOT a dependency here — the WASM +# bridge never surfaces docs URLs, so installing the catalog would only bloat the +# bundle past the PWA precache limit. See the comment in `init()` in src/lib.rs. +quarto-error-reporting = { path = "../quarto-error-reporting", features = ["json"] } # Direct dep so the test-only `quarto_highlight_for_test` export can # call the Registry-backed highlight path. Same crate quarto-core uses # via CodeHighlightStage — ensuring WASM tests exercise the same code diff --git a/crates/wasm-quarto-hub-client/src/lib.rs b/crates/wasm-quarto-hub-client/src/lib.rs index 2cbedd016..4a76ca22c 100644 --- a/crates/wasm-quarto-hub-client/src/lib.rs +++ b/crates/wasm-quarto-hub-client/src/lib.rs @@ -108,6 +108,19 @@ fn populate_dir_recursive(runtime: &WasmRuntime, dir: &include_dir::Dir<'_>, pre #[wasm_bindgen(start)] pub fn init() { + // NOTE: we deliberately do NOT install `quarto-error-catalog` here. The + // catalog only supplies docs URLs / titles, and the WASM render bridge + // never surfaces them — the `JsonDiagnostic` wire shape has no `docs_url` + // field, so diagnostics flow out code-only regardless. Installing it would + // `include_str!` the 46 KB `error_catalog.json` (plus provider code) into + // the bundle, pushing the WASM past hub-client's 35 MiB PWA precache limit + // (`vite.config.ts` `maximumFileSizeToCacheInBytes`) for zero functional + // gain. This is a legitimate "embedder installs nothing → EmptyCatalog" + // choice in the cross-package error-code discipline. The native `q2` + // binary, which has no bundle constraint, does install it. If the preview + // ever needs docs URLs, add a `docs_url` field to `JsonDiagnostic`, install + // here, and raise the precache limit deliberately. + // Install console_error_panic_hook as the base, then wrap it to // filter out expected Lua control-flow panics (see `LuaThrow` above). // Without this wrapper, every pcall-caught Lua error would leave a diff --git a/docs/errors/README.md b/docs/errors/README.md index 4757998bd..662da9f2e 100644 --- a/docs/errors/README.md +++ b/docs/errors/README.md @@ -5,7 +5,7 @@ Each page expands a terminal-format error message into a fuller explanation: what the error means in plain language, why it typically fires, and how a user can fix it. The codes themselves live in the catalog at -[`crates/quarto-error-reporting/error_catalog.json`](../../crates/quarto-error-reporting/error_catalog.json), +[`crates/quarto-error-catalog/error_catalog.json`](../../crates/quarto-error-catalog/error_catalog.json), which defines which codes exist; the pages in this directory explain them. diff --git a/docs/errors/index.qmd b/docs/errors/index.qmd index 7592e4aaa..87b91cb42 100644 --- a/docs/errors/index.qmd +++ b/docs/errors/index.qmd @@ -29,7 +29,7 @@ the per-code reference page. Use the filter above to narrow by subsystem or status. The catalog of codes lives in -[`crates/quarto-error-reporting/error_catalog.json`](../../crates/quarto-error-reporting/error_catalog.json). +[`crates/quarto-error-catalog/error_catalog.json`](../../crates/quarto-error-catalog/error_catalog.json). The pages here are the prose layer over that catalog: where the catalog says *which* codes exist, the pages explain them. diff --git a/scripts/audit-error-codes.py b/scripts/audit-error-codes.py index c8f81b218..5b189bfbe 100755 --- a/scripts/audit-error-codes.py +++ b/scripts/audit-error-codes.py @@ -241,7 +241,7 @@ class ErrorCodeAuditor: def __init__(self, repo_root: Path): self.repo_root = repo_root - self.catalog_path = repo_root / "crates/quarto-error-reporting/error_catalog.json" + self.catalog_path = repo_root / "crates/quarto-error-catalog/error_catalog.json" def run(self) -> AuditResults: """Run the complete audit.""" diff --git a/scripts/quick-error-audit.sh b/scripts/quick-error-audit.sh index 49f3f9914..4fc21ed00 100755 --- a/scripts/quick-error-audit.sh +++ b/scripts/quick-error-audit.sh @@ -19,12 +19,12 @@ echo # Extract catalog codes echo "📚 Extracting catalog codes..." -if [ ! -f "crates/quarto-error-reporting/error_catalog.json" ]; then +if [ ! -f "crates/quarto-error-catalog/error_catalog.json" ]; then echo "❌ Error: Cannot find error_catalog.json" exit 1 fi -jq -r 'keys[]' crates/quarto-error-reporting/error_catalog.json | sort > /tmp/catalog-codes.txt +jq -r 'keys[]' crates/quarto-error-catalog/error_catalog.json | sort > /tmp/catalog-codes.txt catalog_count=$(wc -l < /tmp/catalog-codes.txt | tr -d ' ') echo " Found $catalog_count codes in catalog"