/**
* `harn explain <CODE>` ported to .harn — see harn#2304 (W4).
*
* Pragmatic split. The `.harn` port owns the *single-code* render path
* (`harn explain HARN-TYP-014` and `harn explain HARN-TYP-014 --json`),
* which is the interactive path agents and humans actually hit.
*
* The two complementary modes stay in Rust by design:
*
* * `--catalog [--format markdown|json|text]` is a code-generation
* tool consumed by `make sync-diagnostics-catalog` and the drift
* gate in `make check-diagnostics-catalog`. Re-porting the 300-LOC
* markdown renderer to .harn for zero functional gain is not worth
* the byte-for-byte risk.
* * `--invariant` is the legacy control-flow path explainer; it
* reaches into `harn_ir::explain_handler_invariant` and is
* explicitly out of scope per #2304.
*
* Inputs (from the dispatch shim in crates/harn-cli/src/commands/explain.rs):
* HARN_EXPLAIN_ENTRY_JSON — serialized diagnostic entry with:
* {
* "code": "HARN-TYP-014",
* "category": "TYP",
* "summary": "...",
* "explanation": "...full markdown body...",
* "repair": {"id": "...", "safety": "...", "summary": "..."} | null,
* "related": [{"code": "HARN-TYP-013", "summary": "..."}, ...]
* }
* HARN_OUTPUT_JSON — "1" for the JSON envelope, otherwise human text.
*/
fn trim_trailing_newlines(s: string) -> string {
var out = s
while len(out) > 0 && (out[len(out) - 1] == "\n" || out[len(out) - 1] == "\r") {
out = out[0:len(out) - 1]
}
return out
}
fn render_human(entry: dict) -> string {
var out = entry["code"] + " — " + entry["summary"] + "\n"
out = out + "\n"
out = out + trim_trailing_newlines(entry["explanation"]) + "\n"
let repair = entry["repair"]
if repair != nil {
out = out + "\n"
out = out + "Repair: " + repair["id"] + " [" + repair["safety"] + "] — "
+ repair["summary"]
+ "\n"
}
let related = entry["related"] ?? []
if len(related) > 0 {
out = out + "\n"
out = out + "See also:\n"
for other in related {
out = out + " - " + other["code"] + " — " + other["summary"] + "\n"
}
}
return out
}
fn render_envelope_json(entry: dict) -> string {
// Build the same envelope shape the legacy Rust impl emits. Harn's
// json_stringify sorts dict keys alphabetically, so the wire-order
// differs from serde's struct-field order — the parity test parses
// both sides into serde_json::Value and compares structurally.
let related_codes = []
var related_strs = related_codes
let related = entry["related"] ?? []
for other in related {
related_strs = related_strs.push(other["code"])
}
var repairs = []
let repair = entry["repair"]
if repair != nil {
repairs = repairs
.push({id: repair["id"], safety: repair["safety"], summary: repair["summary"]})
}
let envelope = {
schemaVersion: 1,
code: entry["code"],
category: entry["category"],
summary: entry["summary"],
body: entry["explanation"],
repairs: repairs,
related: related_strs,
apiStability: "stable",
}
return json_stringify_pretty(envelope)
}
fn main(harness: Harness) {
let raw = harness.env.get_or("HARN_EXPLAIN_ENTRY_JSON", "")
if raw == "" {
harness.stdio
.eprintln("internal error: HARN_EXPLAIN_ENTRY_JSON not set by dispatch shim")
exit(70)
}
let entry = json_parse(raw)
let json_mode = harness.env.get_or("HARN_OUTPUT_JSON", "0") == "1"
if json_mode {
harness.stdio.println(render_envelope_json(entry))
} else {
// print! (no trailing newline) — `render_human` already emits its
// own newlines so the output matches the legacy Rust println chain
// byte-for-byte. Drop the last newline that would be doubled.
let text = render_human(entry)
let trimmed = if len(text) > 0 && text[len(text) - 1] == "\n" {
text[0:len(text) - 1]
} else {
text
}
harness.stdio.println(trimmed)
}
}