harn-stdlib 0.8.39

Embedded Harn standard library source catalog
Documentation
/**
 * `harn graph` ported to .harn — see harn#2311 (W11).
 *
 * **Pragmatic partial port.** The module-graph extraction itself lives
 * in `crates/harn-cli/src/commands/graph.rs` — it walks the directory
 * tree, runs `crate::commands::check::collect_harn_targets` +
 * `build_module_graph`, parses each module, walks the IR analyser to
 * derive capabilities / effects / host-call surface, and renders public
 * symbols with stdlib-metadata frontmatter. None of that is reachable
 * from script-land today without a new
 * `harness.modules.compile_view(path)` host capability the W11 ticket
 * spec calls out as future scope.
 *
 * What this script owns: the **rendering layer** — the human-readable
 * text tree and the `JsonEnvelope`-shaped `--json` payload. The Rust
 * shim pre-builds a `GraphReport` and serialises it as JSON into
 * `HARN_GRAPH_VIEW_JSON` so the script just parses + formats.
 *
 * Note: the W11 ticket spec mentions a `--mermaid` mode, but the legacy
 * graph handler today exposes only `--json` and the human text tree.
 * Mermaid output is generated by `harn viz` (see `commands/viz.rs`) — a
 * separate subcommand that already has its own renderer. The mermaid
 * acceptance criterion is therefore N/A for the graph port.
 *
 * Inputs (from the dispatch shim in crates/harn-cli/src/commands/graph.rs):
 *   HARN_GRAPH_VIEW_JSON — serialised `GraphReport` with shape:
 *     {
 *       "modules": [{
 *         "path": "...",
 *         "public_symbols": [{
 *           "name": "...",
 *           "kind": "fn" | "tool" | "pipeline" | ...,
 *           "signature": "...",
 *           "metadata": {...} | absent,
 *         }, ...],
 *         "imports": ["..."],
 *         "requires_capabilities": ["..."],
 *         "effects": ["..."],
 *         "host_calls": ["..."],
 *       }, ...],
 *       "graph": {
 *         "nodes": ["..."],
 *         "edges": [{"from": "...", "to": "..."}, ...],
 *       }
 *     }
 *   HARN_OUTPUT_JSON     — "1" iff the host saw `--json`, else
 *                          human-readable text tree.
 *
 * Error envelopes stay in the Rust shim — extraction failures take the
 * legacy direct render path.
 */
fn __safe_string(value, fallback: string) -> string {
  if type_of(value) == "string" {
    return value
  }
  return fallback
}

fn __safe_list(value) -> list {
  if type_of(value) == "list" {
    return value
  }
  return []
}

fn __safe_dict(value) -> dict {
  if type_of(value) == "dict" {
    return value
  }
  return {}
}

/**
 * Join a list of strings with `sep`. Mirrors `join(items, sep)` for
 * homogeneous string lists; non-string entries are coerced via
 * `to_string`.
 */
fn __join(items: list, sep: string) -> string {
  var out = ""
  var first = true
  for item in items {
    let part = if type_of(item) == "string" {
      item
    } else {
      to_string(item)
    }
    if first {
      out = part
      first = false
    } else {
      out = out + sep + part
    }
  }
  return out
}

/**
 * Render the human-readable tree. Mirrors `print_text_report` in
 * `crates/harn-cli/src/commands/graph.rs`:
 *
 *   <module-path>
 *     <kind> <signature>
 *     ...
 *     import <path>
 *     ...
 *     requires <cap1>, <cap2>
 */
fn __render_text(report: dict) -> string {
  var out = ""
  let modules = __safe_list(report["modules"])
  for module in modules {
    let m = __safe_dict(module)
    out = out + __safe_string(m["path"], "") + "\n"
    let symbols = __safe_list(m["public_symbols"])
    for symbol in symbols {
      let s = __safe_dict(symbol)
      out = out + "  " + __safe_string(s["kind"], "")
        + " "
        + __safe_string(s["signature"], "")
        + "\n"
    }
    let imports = __safe_list(m["imports"])
    for import_path in imports {
      out = out + "  import " + __safe_string(import_path, "") + "\n"
    }
    let caps = __safe_list(m["requires_capabilities"])
    if len(caps) > 0 {
      out = out + "  requires " + __join(caps, ", ") + "\n"
    }
  }
  return out
}

/**
 * Re-clean a single `public_symbols` entry: drop the `metadata` key
 * when absent so the JSON envelope structurally matches the Rust
 * `#[serde(skip_serializing_if = "Option::is_none")]` shape, and pass
 * the nested metadata dict through unchanged when present.
 */
fn __clean_symbol(symbol: dict) -> dict {
  var entry = {
    name: __safe_string(symbol["name"], ""),
    kind: __safe_string(symbol["kind"], ""),
    signature: __safe_string(symbol["signature"], ""),
  }
  let metadata = symbol["metadata"]
  if metadata != nil {
    entry = entry + {metadata: metadata}
  }
  return entry
}

/**
 * Re-clean modules + graph for the JSON envelope. Mirrors the
 * `#[serde(skip_serializing_if = ...)]` projections the Rust
 * `GraphReport`/`GraphModule`/`GraphSymbol` types apply on the way out.
 */
fn __strip_absent(report: dict) -> dict {
  let modules = __safe_list(report["modules"])
  var cleaned_modules = []
  for module in modules {
    let m = __safe_dict(module)
    let symbols = __safe_list(m["public_symbols"])
    var cleaned_symbols = []
    for symbol in symbols {
      cleaned_symbols = cleaned_symbols.push(__clean_symbol(__safe_dict(symbol)))
    }
    let entry = {
      path: __safe_string(m["path"], ""),
      public_symbols: cleaned_symbols,
      imports: __safe_list(m["imports"]),
      requires_capabilities: __safe_list(m["requires_capabilities"]),
      effects: __safe_list(m["effects"]),
      host_calls: __safe_list(m["host_calls"]),
    }
    cleaned_modules = cleaned_modules.push(entry)
  }
  let graph = __safe_dict(report["graph"])
  let edges = __safe_list(graph["edges"])
  var cleaned_edges = []
  for edge in edges {
    let e = __safe_dict(edge)
    let edge_entry = {from: __safe_string(e["from"], ""), to: __safe_string(e["to"], "")}
    cleaned_edges = cleaned_edges.push(edge_entry)
  }
  return {modules: cleaned_modules, graph: {nodes: __safe_list(graph["nodes"]), edges: cleaned_edges}}
}

fn __render_envelope(report: dict) -> string {
  let cleaned = __strip_absent(report)
  let envelope = {schemaVersion: 1, ok: true, data: cleaned, error: nil, warnings: []}
  return json_stringify_pretty(envelope)
}

fn main(harness: Harness) -> int {
  let raw = harness.env.get_or("HARN_GRAPH_VIEW_JSON", "")
  if raw == "" {
    harness.stdio
      .eprintln("internal error: HARN_GRAPH_VIEW_JSON not set by dispatch shim")
    return 70
  }
  let report = try {
    json_parse(raw)
  } catch (e) {
    harness.stdio
      .eprintln("internal error: failed to parse HARN_GRAPH_VIEW_JSON: " + to_string(e))
    return 70
  }
  if type_of(report) != "dict" {
    harness.stdio.eprintln("internal error: HARN_GRAPH_VIEW_JSON must be a JSON object")
    return 70
  }
  let json_mode = harness.env.get_or("HARN_OUTPUT_JSON", "0") == "1"
  if json_mode {
    harness.stdio.println(__render_envelope(report))
    return 0
  }
  // Strip the trailing newline so `println` re-adds exactly one — the
  // legacy `print_text_report` ends every row with `println!()` so the
  // stream ends with one terminating `\n`.
  let text = __render_text(report)
  let trimmed = if len(text) > 0 && text[len(text) - 1] == "\n" {
    text[0:len(text) - 1]
  } else {
    text
  }
  harness.stdio.println(trimmed)
  return 0
}