harn-stdlib 0.8.52

Embedded Harn standard library source catalog
Documentation
/**
 * `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)
  }
}