diag-lang 1.0.0

Diagnostics: error collection and caret-style source-annotated rendering.
Documentation
# diag-lang — Engineering Notes

Working notes for decisions that were parked for review, and the status at the
pre-1.0 stop point. Lives alongside `DIRECTIVES.md` and `ROADMAP.md` in `dev/`.

---

## Resolved

### A `Render` trait for swappable output targets — RESOLVED (defer)

**Decision (2026-06-28):** ship 1.0 without the trait, per the recommendation
below. The output sink stays swappable via `Renderer::render_to`; a `Render` trait
can be added as a non-breaking minor when a second renderer gives it a second
implementor. The rest of the analysis is kept for the record.

**Context.** `dev/ROADMAP.md` for v0.4.0 lists "a renderer trait so output targets
are swappable (terminal, plain string, structured)". This is potentially a
cross-crate contract: the language crates in the family all render through
diag-lang, and if any of them want to dispatch over a `dyn` renderer (to switch
between, say, a terminal renderer and a JSON one at runtime), the trait's shape
becomes a shared API they depend on. Freezing the wrong shape at 1.0 would be hard
to undo.

**What shipped instead.** The output *sink* is already swappable through
`Renderer::render_to(&mut impl fmt::Write, …)`: it drives a terminal (via a `Write`
adapter), a `String`, a reused buffer, or a formatter. Colour is a configuration on
the one renderer (`with_color`), not a separate target. So "terminal" and "plain
string" — two of the three targets named — are covered without a trait.

**The open question — "structured" output.** A JSON/structured target is a
different *format*, not just a different sink, so `render_to` does not cover it. The
options:

1. **Defer the trait (recommended).** Ship 1.0 with `render_to` as the seam and no
   trait. There is exactly one renderer today, so a trait would be a one-implementor
   abstraction — speculative generality, which REPS calls out as an anti-pattern.
   When a structured renderer is actually built, introduce the trait then; adding a
   trait and a second impl is a non-breaking minor. Lowest risk, smallest frozen
   surface.
2. **Add a minimal object-safe trait now**, e.g.
   `trait RenderDiagnostic { fn render_to(&self, out: &mut dyn fmt::Write, diag: &Diagnostic, map: &SourceMap) -> fmt::Result; }`,
   and impl it for `Renderer`. Honours the roadmap wording literally, but freezes a
   shape (object-safe `&mut dyn Write`, no return value beyond `fmt::Result`, no
   structured payload) against a single implementor and no concrete second use case.
3. **Add a structured target too** (a JSON renderer + the trait) before 1.0. Largest
   scope; the payload schema would itself become a frozen contract, and no consumer
   has asked for it yet.

**Recommendation:** option 1. The `render_to` seam meets the swappable-sink need;
the trait is best designed against a real second implementor. If you want the trait
in 1.0 regardless, option 2 is the conservative shape. This is the one roadmap item
v0.4.0 did not implement literally, and the pre-1.0 stop is the right moment to
decide.

---

## STATUS — 1.0.0, frozen

The crate is built through the full 0.x roadmap and the `1.0.0` API freeze.
Engineering is complete and green; the commit, tag (`v1.0.0`), push, and
`cargo publish` are left for you, as requested.

**Done — v0.2.0 → v0.4.0, each committed green:**

- v0.2.0 — `Severity`, `Label`, `Diagnostic`, and the single-line caret renderer.
  `span-lang`/`source-lang` wired and used. Caret alignment exact over multi-byte
  UTF-8 and tab expansion, proven by byte-exact snapshot tests and a property test.
- v0.3.0 — multi-line spans, secondary labels (`-`), notes/help, per-file frames in
  a stable position-driven order, and an out-of-range fallback note for any label.
- v0.4.0 — optional ANSI colour (`color` feature, `Renderer::with_color`) with the
  additivity invariant tested (strip escapes ⇒ plain output), and the surface
  documented as the 1.0 freeze candidate in `docs/API.md`.

**Quality gates, all green** (Windows local, via the rust-lld linker workaround;
CI runs the same on Linux/macOS/Windows, stable + 1.85): `fmt --check`, `clippy
-D warnings` on default / all-features / no-default-features, `test` on default and
all-features, `doc -D warnings`, no-default-features (`no_std`) build, `+1.85`
build, `deny check`. `#![forbid(unsafe_code)]`, no `unwrap`/`expect`/`todo!` in
shipping code. Counts (default features): 13 unit + 11 single-line snapshot + 7
frame snapshot + 4 colour + 4 property + 24 doctests.

**Decisions taken under the standing defaults (FYI, no action needed):**

- Dropped the scaffold's no-op `serde` feature rather than freeze it unused; a
  `serde` minor can be added post-1.0.
- Fixed `clippy.toml` MSRV from a stray `1.87` to `1.85`, matching the manifest and
  the sibling crates.
- Labels carry **global** spans (resolved through the `SourceMap`), matching
  source-lang's design, so a `Label` is just a span plus a message.
- Multi-line spans render as per-line underlines (carets on every covered line,
  message on the last), not rustc's connector bars — coherent, aligned, and far
  lower risk to get exactly right. Multi-label frames stack underlines per line
  rather than merging with vertical connectors. Both are documented layout rules,
  and the rendered byte format is not part of the frozen contract, so a fancier
  frame style remains a non-breaking output change later.

**Decisions resolved at the freeze:**

- The `Render` trait is deferred (see above). No other open questions; the surface
  is frozen as documented in `docs/API.md`.