tadka-2.0.0.0: CHANGELOG.md
# Changelog for `tadka`
All notable changes are recorded here.
## 2.0.0.0 — miette-parity hardening
### Multi-source diagnostics (Phase 12)
- **Breaking:** `Tadka.Internal.Renderer.Json`'s `LabelDTO` gains a `file`
field, present on every label (`ldFile`/`"file"` in the serialized JSON),
including stale ones. Every existing JSON golden fixture with at least one
label changes shape (`json-single`, `json-cycle`, `json-truncated`);
`json-cause` is unaffected (its diagnostic has no context at all). This
closes a real, pre-existing gap independent of multi-source support: the
JSON DTO never named which file a label belonged to, even for a
single-source diagnostic.
- `Context` now holds a non-empty, ORDERED sequence of source groups
(`SourceGroup`: one `NamedSource` plus its own non-empty, order-preserving
label list) instead of exactly one source. This is a strict generalisation:
a single-source `Context` is now the one-group case of the same
representation, not a different shape. Every existing guarantee —
label count/order preserved, `LabelStale` in place rather than dropped —
now holds pointwise, per group; group order itself is preserved too.
- New construction API, mirroring the single-source functions one level up:
`mkContextMulti` (strict; `Left` on the first out-of-bounds span in
group-then-label order), `mkContextMultiDegrading` (total; degrades per
label within its own group), and `Tadka.Internal`'s `buildContextMulti`
(the multi-source convenience entry point, analogous to `buildContext`\/
`buildContextWith` — a source paired with no labels contributes no group,
mirroring `buildContext`'s empty-list convention per source). `mkContext`,
`mkContextDegrading`, `buildContext`, and `buildContextWith` are now
implemented as the one-group special case of their multi- counterparts —
not a second copy of the resolution logic — and are unchanged in signature
and behaviour (proven byte-identical: every existing graphical and
narratable golden fixture is unchanged).
- All three renderers updated to walk every source group, in order:
- **Graphical** — one `┌─ file:line:col` gutter block per group, separated
by the same lone-rail-line convention `relatedChild` already uses between
a nested diagnostic's snippet and its related forest. The gutter width is
now the maximum needed across every group, so indentation (help/see/
related lines) stays aligned across a multi-file report. A one-group
context renders exactly the single block it always did, with no
separator.
- **Narratable** — one `Location: ...` sentence and its label readouts per
group, in group order.
- **JSON** — every label's DTO carries the name of its own group's source
via the new `file` field.
- The derive macro (`deriveDiagnostic`) and `genericContext` remain
single-source only in v1; `buildContextMulti` is a hand-written-instance
entry point. Deferred, not forgotten, for a future release. §6.
- New `Phase12` property group (6 properties, 100 generated cases each):
`mkContextMulti`/`mkContextMultiDegrading`'s per-group and cross-group
count/order guarantees, the single-source functions' exact equivalence to
their one-group multi- counterparts, and `buildContextMulti`'s empty-group
handling. New `cross-file` / `narr-cross-file` / `json-cross-file` golden
fixtures, exercising a real two-file diagnostic through all three handlers.
Full existing suite (property + golden) re-verified with zero regressions
beyond the three JSON fixtures the `file` field intentionally changes.
### Issue remediation pass
- **Breaking:** `StaleReason` loses its `SourceMismatch` constructor.
`resolveSpan` never produced it — the only real producer path is
out-of-bounds resolution — so it was a representable-but-unreachable state.
Removing it also tightens `Narratable.hs`'s `staleSentence`, which handled
it, to a total single-clause match that will now fail to compile (instead
of silently matching a wildcard) if a future `StaleReason` constructor is
ever added without updating every renderer.
- Fixed: multiple same-kind labels (e.g. two `Secondary` labels on one line)
were visually indistinguishable under `ColorNever`, since `caretGlyph` only
looked at `LabelKind`, not position. The underline glyph is now chosen by a
per-kind rank: the first label of a kind keeps its existing anchor (`^` for
`Primary`, `-` for `Secondary`), and a second-or-later same-kind label
cycles through `~` (then `=`). Colour-mode output is unaffected — the glyph
is always `^` there, exactly as before.
- Fixed: the graphical handler previously discarded `Ann` annotations
(`AnnCode`, `AnnKeyword`, `AnnFilename`, `AnnEmphasis`) on message/label/help
text, rendering plain, unstyled text regardless of author intent. It now
interprets them at its own render boundary — ANSI styling under any colour
mode, a plain-text fallback marker (backtick/quote) under `ColorNever` — the
same way `Narratable.hs`'s `toProseMarker` already did for prose output.
- Fixed: `deriveDiagnostic` had no way to generate `diagnosticCause`. New
`specCause` field on `DiagnosticSpec` names a `Maybe SomeDiagnostic`-typed
field (validated at splice time, same discipline as `specRelated`); the
generated method body is a bare field accessor. New `WrongCauseType`
compile-fail case.
### Collection labels in `deriveDiagnostic`
- New `specLabelCollectionFields`/`specSecondaryLabelCollectionFields` on
`DiagnosticSpec`: each names a `[Span]`-typed field (validated at splice
time, same as `specLabelFields`), and every element of that field's runtime
list becomes its own label sharing the given text — for a variable number of
same-kind occurrences (every prior declaration of a name, every match of a
banned pattern) known only when the diagnostic is built, where
`specLabelFields` needs one field per label fixed at splice time. Rendered
after all fixed-field labels, in field order then list order; an empty
runtime list simply contributes no labels.
- TH-layer only, as intended: `buildContext`/`buildContextWith` are completely
unchanged, since they already accept a plain, arbitrary-length list — the
splice just expands a collection field into that same shape and appends it.
A spec with no collection fields generates byte-identical code to before
this feature existed (the expansion is only spliced in when at least one
collection field is actually declared), so no existing derived instance's
generated code changes shape.
- New `WrongCollectionType` compile-fail case (a `Span`-typed, not
`[Span]`-typed, field must be rejected at the splice site) alongside the
existing ones in `tools/check-compile-fail.sh`. A new `LabelCollection`
property group proves a derived instance with collection fields renders
identically, across all three handlers, to a hand-written
`buildContext`/`buildContextWith` call expanding the same randomly generated
list — for varying list lengths (including empty and a 200-element
totality check), an all-primary-only collection, and a mixed
fixed-primary-plus-secondary-collection instance.
### Terminal hyperlinks: OSC 8 for the `= see:` URL
- The graphical handler now wraps a diagnostic's `url` in an OSC 8 terminal
hyperlink escape when `HyperlinkMode` allows it, so a supporting terminal
renders the `= see:` line as a clickable link instead of plain text a user
must select and copy by hand. New `HyperlinkMode` (`Auto`/`Always`/`Never`,
mirroring `ColorMode`/`UnicodeMode`) and `withHyperlinkMode`; `TerminalCaps`
gains `capNoHyperlink`/`capForceHyperlink` (`NO_HYPERLINK`/`FORCE_HYPERLINK`
— the latter an existing convention from the `supports-hyperlinks` package)
and `resolveHyperlink` resolves `Auto` the same three-tier way `resolveColor`
does. Defaults to `HyperlinkNever`, not `Auto` (unlike colour/Unicode): OSC 8
has no reliable capability query the way TTY-ness does, so defaulting off
keeps every existing caller's output byte-for-byte unchanged until they opt
in. Narratable and JSON output are untouched — this is a graphical-only
affordance. Only ever wraps an already-validated `Url` (never raw `Text`), so
the wrap is injection-safe by construction rather than by an extra runtime
check: `mkUrl`'s absolute-URI grammar has no production admitting a raw
control byte. A new `single-label-hyperlink` golden fixture pins the exact
escape bytes; a dedicated property group proves the resolver mirrors colour's
three-tier law, the wrap touches only the URL (a diagnostic without one
renders identically under `Always` and `Never`), and `Never` output never
contains an escape.
### Security hardening (untrusted-input robustness)
- **Terminal-escape / control-character injection (High).** Raw control
characters (`ESC`, `BEL`, `BS`, DEL, C1) in attacker-controlled source or label
text previously passed through verbatim into graphical and narratable output,
even in `ColorNever` mode — a terminal-injection vector when diagnosing
untrusted code. All three handlers now strip control characters from rendered
text (source lines keep `\t` for tab expansion; the substitution is
width-preserving so caret columns are unmoved). JSON is included because aeson
escapes only `<0x20`, leaving DEL/C1 raw. A property asserts no control
character other than the `\n` line separator survives in any handler's output
over adversarial generated input.
- **Unbounded output from wide spans (Medium, availability).** A span across a
huge line range rendered output proportional to the span (a line-1→5000 span
produced 5002 lines). The planner now caps the contiguous range and falls back
to a bounded context window with elision, so output is proportional to the
diagnostic, not the span (that case now renders ~11 lines); every labelled line
still appears. Small diagnostics are unchanged, so golden fixtures stay
byte-identical. A property bounds output for a 10000-line span.
### Robustness: production edge cases mined from miette's history
- Added a `Production edge cases (from miette)` property group translating
miette's bug-fix history into tadka tests, and fixed the two gaps it exposed:
CRLF (`\r\n`) sources left a stray carriage return in rendered lines
(miette #37) — `SourceCode` now strips a trailing `\r` per line; and a newline
embedded in a label or message could break the caret/gutter layout
(miette #318) — the graphical and narratable handlers now flatten newlines in
rendered text fragments to spaces (JSON keeps them, escaped). tadka was
already robust to the rest: zero-length/point spans (#204/#159/#32), spans
past end-of-line/EOF (#221/#347), empty sources (#183), offset-0 labels,
wide-char + tab alignment (#202), combining marks (#312/#314), nested/
overlapping spans (#316), and multi-line spans not skipping lines (#81), plus
a totality sweep over out-of-range spans on all three targets.
### Snippet renderer rework — Phase III: multi-line span rendering
- Multi-line spans (start line < end line) are no longer clamped to the start
line: they render with a connector gutter between the rail and the source —
`\x256D` opening, `\x2502` continuation, `\x2570` closing (ASCII `/`, `|`,
`\\`) — with the label shown inline on the closing line. The pure core lives
in `Tadka.Internal.Renderer.Layout`: greedy interval-graph lane assignment so
overlapping spans get distinct lanes while disjoint spans reuse one, plus a
`cellAt` classifier. It is glyph-free and Int-only, so it is fully proven:
distinct spans on one lane never share a line (the collision proof), lanes are
contiguous, coverage is order-preserving, and `cellAt` classifies correctly.
End-to-end properties add that every multi-line span draws exactly one opening
and one closing corner and that the label text is shown — the second of which
caught a real lane-reuse bug (a lane hosting two disjoint spans rendered only
the first) before it could ship. Single-line diagnostics are unaffected (the
gutter has zero width when there are no multi-line spans), so all existing
golden fixtures stay byte-identical; a `multi-line` fixture locks the art.
- Note: multiple single-line labels on the same line still render as stacked
caret lines (correct, and unchanged) rather than horizontally packed onto one
line with routed connectors. True same-line packing is deferred as optional
polish; it does not fall out of the multi-line lane engine as cleanly as first
thought, and stacked carets are unambiguous.
### Snippet renderer rework — Phase II: context lines + gap elision
- New pure planner `Tadka.Internal.Renderer.LinePlan` (`PlanEntry`, `planLines`)
decides which source lines to render and where to elide, as an IR between
resolved labels and glyphs. `withContextLines n` shows n lines around each
labelled line and elides the gaps (a `⋮` marker); unset (default) renders the
contiguous labelled range with no elision, so all golden fixtures stay
byte-identical. The graphical window fetch now derives from the plan's shown
lines (via `SourceCode.scLineCount`), not the anchor range. Properties prove
the planner is total, `Nothing` reproduces the contiguous range, and with
context every in-range anchor is shown, line numbers strictly increase and
stay in bounds, and every elision hides at least one line.
### Snippet renderer rework — Phase I: pluggable SourceCode
- New `Tadka.Internal.SourceCode` class: a total, windowed source-reading seam
(`scName`, `scLines (firstLine, lastLine)`). `NamedSource` is the canonical
in-memory instance; the graphical and narratable handlers now fetch only the
line window they render through it, so a lazy/file-backed instance is possible
later. Pure refactor — all golden fixtures byte-identical. Properties prove the
instance is total and its windows equal a filter of the full line enumeration.
### Totality: no partial functions in the library
- Removed every use of a partial function primitive from `src/`. `head`/`!!`,
`maximum`/`minimum`, and `Data.Array.(!)` are gone from all call sites: caret
layout and line lookup now pattern-match `drop`; gutter/line-range use
`foldr max`/`foldr min` with seeds; palette indexing uses total `NonEmpty`
operations; and array indexing is encapsulated in a single total `atMay`
(guarded by `inRange`, returning `Maybe`). Behaviour is unchanged — all 14
golden fixtures remain byte-identical — so this is a pure totality hardening.
### Primary vs secondary labels
- Labels now carry a `LabelKind` (`Primary`/`Secondary`). `buildContext` marks
everything `Primary` (so existing callers and fixtures are unchanged); a new
`buildContextWith` takes explicit kinds, and the derive macro gained
`specSecondaryLabelFields`. All three handlers are kind-aware: the graphical
and narratable reports anchor their location on the first primary label; the
graphical handler draws primary labels with `^` in the severity colour and
secondary labels with `-` in a palette colour; the narratable handler leads
with "The problem is at" vs "Related context is at"; and the JSON DTO gains a
per-label `primary` flag. Properties prove the location anchors on the primary,
the JSON flag tracks the kind, `^`/`-` by kind, and — extending the Phase 8
guarantee — a derived instance with secondary fields renders byte-identically
to a hand-written `buildContextWith` instance across all three handlers.
### Cause chain (diagnostic_source analogue)
- New `diagnosticCause :: e -> Maybe SomeDiagnostic` class method (default
`Nothing`) and a total, depth- and cycle-safe `walkCauses` (cycle detection by
`diagnosticId`, mirroring `walkRelated`). The chain renders as lightweight
linear provenance — graphical `= caused by: …` lines, narratable `Caused by: …`
sentences, and a JSON `causes` array — kept deliberately distinct from the
tree-shaped `related`. The shared generator now emits causes, so the Phase 11
render-totality-over-every-target property covers cause chains too; dedicated
properties prove cyclic chains terminate on every target, an id-cyclic cause's
marker renders at most once, and a real chain produces a "caused by" line.
- Hardened `tools/check-compile-fail.sh` to build the library under the default
configuration first, so `cabal exec` always exposes a consistent `tadka`.
### Terminal detection & ANSI colour (graphical handler)
- `reportDiagnostic` now detects the sink's capabilities and resolves `ColorAuto`
/ `UnicodeAuto` to concrete modes before rendering: `NO_COLOR` disables colour,
`CLICOLOR_FORCE` forces it, otherwise colour follows TTY status; Unicode follows
a UTF-8 locale check (`LC_ALL` > `LC_CTYPE` > `LANG`). So piped/CI output no
longer risks stray escapes, and non-UTF-8 terminals get the ASCII box glyphs
automatically. `selectRenderer` stays pure — resolution is the only new IO.
- New `Tadka.Internal.Terminal`: `detectTerminalCaps` (IO) plus the pure, total
`resolveColor` / `resolveUnicode` / `resolveConfig`. Properties prove explicit
modes pass through untouched; `NO_COLOR` always wins; force beats TTY; `Auto`
otherwise follows TTY/locale; `resolveConfig` eliminates every `Auto`, is
idempotent, and changes only the two mode fields.
- The graphical handler now emits ANSI: the severity/code header is coloured by
severity (bold), and each label's carets + text take their palette colour
(`withLabelPalette`) — delivering the per-label colour deferred in Phase 5.
Under `ColorNever` no ANSI is emitted (proved over generated diagnostics) and
underline glyphs cycle `^`/`~`/`-`, so all golden fixtures stay byte-identical.
A structural property proves colour adds only ANSI and a uniform caret glyph,
never a layout change.
### Tab-stop expansion (graphical handler)
- Source lines are rendered with tabs expanded to the next tab stop, and caret
columns are computed with the same tab-aware `displayColumnAt`, so a caret now
aligns under a span on a tab-indented line instead of drifting. Tab width is
configurable via `withTabWidth` (default 4); character columns reported by the
narratable and JSON handlers are unchanged (a tab is one character).
- New total helpers `Tadka.Internal.Width.displayColumnAt` / `expandTabs`, with
properties proving: expansion leaves no tabs; expanded width equals
`displayColumnAt` of the whole line; `displayColumnAt` is monotonic; a tab
always lands on a tab stop; and — the alignment guarantee — a caret's display
offset equals the width of the tab-expanded source preceding the span. A
`tab-indented` golden fixture locks the visual result.
## 1.0.0.0 — v1
First release. All eleven phases of the implementation spec are complete; the
public API matches vision §8.
### Phase 11 — Consolidation & Release Audit
- Public API surface reconciled to vision §8: `Offset`/`Length` (and their
constructors, accessors, error types, and the `spanOffset`/`spanLength`
accessors that exposed them) are no longer public — spans are the public
position type, and the offset representation lives in "Tadka.Internal.Types"
with no compatibility guarantee. A negative compile check confirms they are
unreachable from `Tadka`.
- Dependency upper bounds added to the library and every interop sub-library;
`cabal check` is clean. Version set to `1.0.0.0`.
- Consolidated golden suite (10 fixtures: the six canonical renderings, the
prose and JSON depth-truncation forms, the JSON cycle form, and the
generated-instance discipline fixture) and property suite (all groups from
Phases 1–9) run together via `make test` / CI.
- New consolidated properties: `render` totality broadened to a single Hedgehog
property over *every* target; a cycle-detection marker property (a repeated
`diagnosticId`'s marker renders at most once, on all three handlers); and the
vision's Success Criterion end-to-end — a misspelled span field is a compile
error (`test/compile-fail/SuccessCriterion.hs`), and a genuinely staled span
renders a clear in-report reason (graphical, narratable, and an explicit JSON
`"stale":true`) rather than a silently shorter report.
### Phase 10 — Interop Helpers
- One-directional adapters turning parser positions into tadka `Span`/`Offset`,
each a plain function against Phase 1/2 types with no new core surface:
- `Tadka.Interop.Megaparsec`: `spanFromError` from a megaparsec
`ParseError`'s stream `errorOffset`.
- `Tadka.Interop.Attoparsec`: `consumedOffset`/`spanFromConsumed` (attoparsec
reports no line/column, so position is recovered as characters consumed).
- `Tadka.Interop.GHC`: `spanFromSrcSpan` converting a GHC `SrcSpan` (with the
source text, to turn 1-based line/column into an offset).
- Each adapter is a **separate cabal sub-library** (`interop-megaparsec`,
`interop-attoparsec`, `interop-ghc`) depending on `tadka`, so the core library
never depends on a parser package and no core module can import interop — the
adapters are one-directional by construction. Minimum upstream versions are
pinned (`megaparsec >=9.0`, `attoparsec >=0.14`, the GHC 9.10 `ghc` library)
and noted in each module's haddock.
- Exit criteria met: a per-library round-trip test (`test-suite interop`)
constructs a known failure, converts its position, resolves against the same
source, and confirms the line/column matches what the library reports
(megaparsec, GHC) or the consumed offset (attoparsec); and an audit confirms
the core library has no parser/ghc dependency and imports no interop module.
### Phase 9 — Generics-Based Label-Wiring Derivation
- `genericContext :: (Generic e, ...) => e -> Context` derives __only__ the
`context` method, via GHC.Generics, for a record with exactly one
`NamedSource` field and one or more `Span` fields — using each span field's
record-selector name as its label text and calling the same `buildContext`.
Used as `context = genericContext` inside an otherwise hand-written instance.
- Deliberately scoped: its type (`e -> Context`) can touch nothing else, so
`code`/`severity`/`help`/`url`/`message`/`diagnosticId` stay hand-written
(each defaultable). This is one method, not "most of the ergonomics" — for the
fuller path use `deriveDiagnostic`.
- The record shape is checked at compile time (a `Nat`-counting type family over
the generic `Rep`): zero or several `NamedSource` fields, or no `Span` field,
is a type error — never a silent guess about which field was meant. A
compile-fail test (`TwoSources`) covers the two-source case.
- Exit criteria met: a property proves a generics-wired instance renders
byte-for-byte identically to a hand-written `buildContext` twin across all
three handlers, and the `e -> Context` signature confirms no other method is
touched.
### Phase 8 — Derive Macro (`deriveDiagnostic`)
- `DiagnosticSpec` (with `specCode`, `specSeverity`, `specHelp`, `specUrl`,
`specSourceField`, `specLabelFields`, `specRelated`, `specId`, `specMessage`)
and `defaultSpec`, plus `deriveDiagnostic :: DiagnosticSpec -> Name -> Q [Dec]`,
an ordinary TH splice (no type-level DSL).
- `reify`-validated at splice time: `specSourceField` must be `NamedSource`,
each `specLabelFields` name `Span`, `specId` `Text` or `DiagnosticId`, and
`specRelated` `[SomeDiagnostic]`; `specCode`/`specUrl` literals run through
`mkDiagnosticCode`/`mkUrl`. Any mismatch is a compile error at the splice site.
The default `message` (`pretty . show`) requires `Show`, checked via `isInstance`.
- Every generated method body is a direct call to a shared function exported
from `Tadka.Internal` (`buildContext`, `unsafeDiagnosticCode`, `unsafeUrl`,
`mkDiagnosticId`) or the field accessor — the derive path and a hand-written
instance are two doors into the same room. `mkDiagnosticId` was added to the
`Tadka.Internal` export list, and that module's haddock now states the
discipline.
- Exit criteria met: compile-fail tests (`test/compile-fail/`, run by
`tools/check-compile-fail.sh`) reject a wrong-typed field, a non-field name,
and an invalid code literal at compile time; a property proves a derived
instance renders byte-for-byte identically to a hand-written twin across all
three handlers; the generated instance source is captured as a golden fixture
and `tools/check-generated.sh` (a **required** CI check) fails the build on any
non-direct-call method body; and a `CONTRIBUTING.md` checklist entry records
the review convention.
### Phase 7 — JSON Report Handler + DTO
- Dedicated `DiagnosticDTO` / `LabelDTO` with hand-written `ToJSON` — never
`deriving ToJSON` on a `Diagnostic`-bearing type. `ToJSON` only in v1;
`FromJSON` is deferred.
- DTO shape per the canonical example: `code`, `severity`, `message`, `labels`
(each `line`, `column`, `length`, `text`, and an explicit `stale`), `help`,
`url`, `related`, `truncated`, `cycleOmitted`. The `stale` flag is derived
from `LabelState` (stale labels carry null `line`/`column`/`length`), never
inferred from absence.
- `related` recurses into nested DTOs; `truncated`/`cycleOmitted` are set from
the Phase 3 walk's `TerminationReason` — the same two values the graphical and
narratable handlers consume, serialized instead of prose-rendered.
- `renderJson` builds the DTO inside the `'TJson` branch and is the only route
to `Output 'TJson = Aeson.Value` (audited: no `Diagnostic` type produces a
`Value` except through this conversion). `render` now dispatches `'TJson`.
- Golden fixtures: `json-single` (byte-for-byte to the vision example, modulo
the same coherent column correction as the other handlers), `json-cycle`
(`cycleOmitted: true`), and `json-truncated` (nested `truncated: true`). The
runner serializes the `Value` with a deterministic ordered pretty-printer to
match the vision's canonical layout. Property suite: ok labels serialize
`stale:false` with positions, stale labels `stale:true` with null positions,
and the handler is total over the shared generated set.
### Phase 6 — Narratable Report Handler
- `renderNarratable` produces the accessibility-first prose form: an
`Error,`/`Warning,`/`Advice,` opener with an optional `code X:` clause (dropped
when there is no code), a `Location: file, line N, column M.` sentence, a
`Source line N: "…".` readout, and a `The problem is at column(s) …, labeled: …`
sentence per label.
- `LabelStale` has a prose equivalent of the graphical degraded marker
(`A labeled position could not be shown because …`, carrying the stale reason
and label) — never silently omitted.
- Related chains render as prose, consuming the Phase 3 walk: `Related: code —
message.`, a cycle sentence, and an `N more related diagnostics … omitted at
the depth limit.` marker (with correct singular/plural).
- `toProseMarker :: Ann -> Text` interprets `Ann` at this handler's boundary
(inline code and file names are surrounded with quotes); rendered via
`renderSimplyDecorated` so annotated content reads naturally.
- `render` now dispatches the `'TNarratable` branch to `renderNarratable`.
- Golden fixtures: `narr-single` (same diagnostic as the graphical single-label
fixture, so the two handlers stay cross-consistent) and `narr-truncated`
(a related chain past the depth limit, exercising the prose truncation marker).
Property suite: `AnnCode` renders quoted, and the handler is total over the
same shared generated set (now in `GenDiag`) used by Phase 5's smoke check.
- Field-coverage cross-check confirmed: code, severity, location, source line,
label text, stale reason, help, url, and related each have a narratable
equivalent — no field is dropped between renderers.
### Phase 5 — Graphical Report Handler
- `renderGraphical` renders a full graphical report: `error[code]: message`
header (no brackets when there is no code; `advice:`/`warning:`/`error:` per
severity), a `┌─ file:line:col` location line, a line-numbered gutter with a
`│` rail, source lines sliced from the single stored `NamedSource`, and
underline carets positioned by display width via `Tadka.Internal.Width`
(combining marks, East-Asian-width, and emoji handled).
- Per-label underline cycling: label index *i* selects palette entry *i mod p*
(`labelStyle`); under `ColorNever` the underline character cycles `^`/`~`/`-`
so labels stay distinguishable in plain text. `UnicodeAscii` degrades the
box-drawing glyphs to `|`/`+`/`-`.
- Stale labels render as `(span unavailable — source no longer matches at this
position)` in place of a source line, in original order.
- Related chains consume the Phase 3 walk: each `CycleOmitted` node renders
`= related: (cycle omitted)`, each `DepthTruncated` node
`= related: (N more related diagnostics omitted)`, and ordinary nodes render
a `= related: code — message` summary plus their own (indented) snippet.
- `render` now dispatches the `'TGraphical` branch to `renderGraphical`.
- Golden suite (byte-for-byte): single-label, multi-label, degraded, and
cycle-omitted fixtures, rendered `ColorNever`/`UnicodeAlways` for determinism.
Property suite: caret layout is non-negative, never collapses, and is
width-aware (doubling as a width-table sync check); palette cycling equals
*i mod p*; and the handler is total over fuelled generated diagnostics
(including stale labels and both id-bearing and `Nothing`-id self-cycles).
Deviations from the vision's hand-drawn examples (which are internally
inconsistent) are deliberate and documented in the fixtures: correct column
numbers, coherent underline-character cycling, full diagnostic codes in
related summaries, and real line numbers rather than fabricated ones.
### Phase 4 — Renderer/Config Scaffolding
- `Tadka.Internal.Config`: `Target` (`TGraphical`/`TNarratable`/`TJson`,
closed), `ColorMode`, `UnicodeMode`, opaque `Config` with `defaultConfig` and
the `withColorMode` / `withUnicodeMode` / `withRelatedDepthLimit` /
`withLabelPalette` / `withTarget` setters, plus the default six-colour palette.
- Three renderer modules with opaque `GraphicalOptions` / `NarratableOptions` /
`JsonOptions` (no public constructor or accessor) and placeholder
`renderGraphical` / `renderNarratable` / `renderJson` bodies (Phases 5–7).
- `Tadka.Internal.Render`: the `Renderer (t :: Target)` GADT (constructors
exported for pattern matching), the closed `Output` type family, `SomeRenderer`,
`render`, and `reportDiagnostic`. `selectRenderer` is the sole constructor of
any `*Options` value and the sole reader of `Config` — the "one path".
- Property suite (Phase 4): an explicit `withTarget` override always yields the
matching renderer constructor (even after other setters), the no-target
default is graphical, and each target's render path runs. Verified by audit
that `selectRenderer` is the only `*Options` construction site, and by
negative compile check that the `*Options`/`Config` constructors are
unreachable from `Tadka`.
### Phase 3 — `Diagnostic` Typeclass & Related/Cycle Walk
- `Tadka.Internal.Diagnostic`: the `Diagnostic` class (only `message`
mandatory, no `Show` superclass; the other seven methods defaulted) and the
`SomeDiagnostic` existential (no `Show` constraint).
- `Tadka.Internal.Related`: the single renderer-agnostic `related`-chain walk
(`walkRelated`) producing a `RelatedTree` of `(SomeDiagnostic,
TerminationReason)` where `TerminationReason` is `NotTerminated` /
`DepthTruncated` / `CycleOmitted`. Cycle detection is by `diagnosticId` along
the current path; termination is always guaranteed by the finite depth
budget (`defaultRelatedDepth = 8`). Factored once here so Phases 5–7 share it
rather than reimplementing.
- Property suite (Phase 3): cycle nodes are visited once and never descended;
a structurally infinite `Nothing`-only chain still terminates by depth (v4
fallback intact); the walk is total and depth-bounded for any fuelled tree.
### Phase 2 — Span Resolution & `Context`
- `Tadka.Internal.Span`: resolution-indexed `SpanF` GADT (`Span` /
`ResolvedSpan`), `mkSpan`, `resolveSpan`, `LineCol`, `StaleReason`,
`SpanError`. `ResolvedSpan` carries positions only — no owned text (v5 fix);
its raw constructors are hidden so a resolved span can only come from
`resolveSpan`.
- `Tadka.Internal.Context`: `Labeled`, `LabelState` (`LabelOk` / `LabelStale`),
`Context` (`NoContext` / `HasLabels`), `mkContext` (strict), and
`mkContextDegrading` (total). The degrading path turns an unresolvable span
into a `LabelStale` marker in its original position, never dropping it — so
label count and ordering are independent of which spans stayed valid.
- `Tadka.Internal.Context.buildContext` — the single plain dispatch both the
derive macro (Phase 8) and manual instances call; re-exported from
`Tadka.Internal`.
- `Tadka.Internal.Ann` (`Ann`) pulled forward from Phase 3, since `Labeled` /
`buildContext` need `Doc Ann`; the renderer-boundary interpreters remain in
Phases 5–7.
- Property suite (Phase 2): `resolveSpan` bounds safety, `mkContext` Left iff a
span is out of bounds, the `mkContextDegrading` count/order guarantee, and
`buildContext` dispatch. Suite reorganised into per-phase modules.
### Phase 1 — Primitive Types, Smart Constructors & the Width Table
- `Tadka.Internal.Types`: validated `Offset`, `Length`, `NamedSource`,
`DiagnosticCode`, `Url`, `Severity`, and `DiagnosticId`, each with a smart
constructor that enforces its invariant and a hidden raw constructor
("illegal states unrepresentable"). Explicit custom error types
(`OffsetError`, `LengthError`, `SourceError`, `CodeError`, `UrlError`).
- Diagnostic-code grammar `^[a-z][a-z0-9_]*::E[0-9]{4,}$` validated without a
regex dependency; URLs validated as absolute URIs via `network-uri`.
- `Severity` display strings centralised in `severityLabels` /
`severityJsonTag` (one source of truth for Phases 5–7).
- Internal `unsafeDiagnosticCode` / `unsafeUrl` exposed only via
`Tadka.Internal` for Phase 8 splice use; verified unreachable from `Tadka`.
- `Tadka.Internal.Width`: `charWidth`, `textWidth`, `graphemeBreakProperty`,
and `isExtendedPictographic`, backed by a generated, checked-in
`Tadka.Internal.Width.Table` (UCD 15.1.0), with binary-searched ranges.
- `tools/gen-width-table.hs` implemented: fetches four pinned UCD 15.1.0 files
and emits the table; UCD version recorded in the generated header.
- Property suite (Hedgehog): smart-constructor rejection (with an independent
grammar oracle), `NamedSource` round-trip, and width point lookups.
- Gated `-Werror` behind a manual `werror` flag (enabled for dev/CI via
`cabal.project`) so `cabal check` passes and released builds stay installable.
### Phase 0 — Repo & Tooling Setup
- Cabal package skeleton with the fixed public/internal module layout
(`Tadka` public; `Tadka.Internal.*` no-compatibility-guarantee).
- Dependency pins: `text`, `prettyprinter`, `prettyprinter-ansi-terminal`,
`ansi-terminal`, `template-haskell`, `aeson`.
- `tools/gen-width-table.hs` scaffold (implemented in Phase 1).
- Two independently-runnable test suites (`golden`, `props`), each passing
trivially on zero tests.
- CI skeleton building the library and running both suites independently.
- Warnings-as-errors (`-Wall -Wcompat -Werror`) from the first commit.