harn-stdlib 0.8.39

Embedded Harn standard library source catalog
Documentation
/**
 * `harn routes` ported to .harn — see harn#2311 (W11).
 *
 * **Pragmatic partial port.** The trigger inventory extraction lives in
 * `crates/harn-cli/src/commands/routes.rs` — it walks `harn.toml`,
 * resolves trigger handler URIs against the on-disk manifest cache,
 * parses each handler module, and runs the IR analyser to derive
 * capabilities / vendor lock / framework-overhead tokens. 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 table and the `JsonEnvelope`-shaped `--json` payload. The Rust
 * shim pre-builds a `RoutesReport` and serialises it as JSON into
 * `HARN_ROUTES_VIEW_JSON` so the script just parses + formats.
 *
 * Inputs (from the dispatch shim in crates/harn-cli/src/commands/routes.rs):
 *   HARN_ROUTES_VIEW_JSON — serialised `RoutesReport` with shape:
 *     {
 *       "triggers": [{
 *         "id": "...",
 *         "kind": "...",
 *         "provider": "...",
 *         "path": "..." | absent,
 *         "module": "...",
 *         "handler": "...",
 *         "events": ["..."] | absent (empty Vec is skipped),
 *         "requires_capabilities": ["..."],
 *         "budgets": {...},
 *         "vendor_locked": bool,
 *         "framework_overhead_tokens": int,
 *       }, ...]
 *     }
 *   HARN_OUTPUT_JSON      — "1" iff the host saw `--json`, else
 *                           human-readable text.
 *
 * Error envelopes for the inventory-extraction step stay in the Rust
 * shim — building them on the .harn side would require re-implementing
 * the trigger/manifest/IR analyser. The shim only dispatches when the
 * extraction succeeded; 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 {}
}

fn __safe_bool(value, fallback: bool) -> bool {
  if type_of(value) == "bool" {
    return value
  }
  return fallback
}

fn __safe_int(value, fallback: int) -> int {
  if type_of(value) == "int" {
    return value
  }
  return fallback
}

/**
 * Truncate `value` to at most `width` Unicode scalars. Mirrors the
 * legacy Rust `truncate(&str, width)` — strings ≤ width pass through
 * unchanged, strings > width get an ASCII ellipsis suffix that fits
 * inside the column, and widths ≤ 3 collapse to a row of dots.
 */
fn __truncate(value: string, width: int) -> string {
  if len(value) <= width {
    return value
  }
  if width <= 3 {
    var dots = ""
    var i = 0
    while i < width {
      dots = dots + "."
      i = i + 1
    }
    return dots
  }
  return value[0:width - 3] + "..."
}

/**
 * Right-pad `value` with spaces so the total displayed width is
 * `width`. Matches Rust's `{:<width}` formatter for ASCII inputs (the
 * legacy renderer uses no wide chars in its column headers / payload
 * fields, so the byte/char-count equivalence holds).
 */
fn __pad_right(value: string, width: int) -> string {
  let count = len(value)
  if count >= width {
    return value
  }
  var pad = ""
  var i = 0
  while i < width - count {
    pad = pad + " "
    i = i + 1
  }
  return value + pad
}

/**
 * Left-pad `value` with spaces so the total displayed width is
 * `width`. Matches Rust's `{:>width}` formatter.
 */
fn __pad_left(value: string, width: int) -> string {
  let count = len(value)
  if count >= width {
    return value
  }
  var pad = ""
  var i = 0
  while i < width - count {
    pad = pad + " "
    i = i + 1
  }
  return pad + value
}

/**
 * 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 table. Mirrors `print_text_report` in
 * `crates/harn-cli/src/commands/routes.rs` byte-for-byte (column
 * widths and ordering).
 */
fn __render_text(report: dict) -> string {
  var out = __pad_right("id", 28)
    + " "
    + __pad_right("kind", 10)
    + " "
    + __pad_right("provider", 12)
    + " "
    + __pad_right("path", 24)
    + " "
    + __pad_right("module", 22)
    + " "
    + __pad_right("handler", 18)
    + " "
    + __pad_right("vendor", 7)
    + " "
    + __pad_left("fw_tokens", 8)
    + "  capabilities\n"
  let triggers = __safe_list(report["triggers"])
  for trigger in triggers {
    let t = __safe_dict(trigger)
    let caps = __safe_list(t["requires_capabilities"])
    let capabilities = if len(caps) == 0 {
      "-"
    } else {
      __join(caps, ",")
    }
    let path_value = if type_of(t["path"]) == "string" {
      t["path"]
    } else {
      "-"
    }
    let vendor_locked = __safe_bool(t["vendor_locked"], false)
    let vendor = if vendor_locked {
      "yes"
    } else {
      "no"
    }
    let fw_tokens = __safe_int(t["framework_overhead_tokens"], 0)
    out = out
      + __pad_right(__truncate(__safe_string(t["id"], ""), 28), 28)
      + " "
      + __pad_right(__safe_string(t["kind"], ""), 10)
      + " "
      + __pad_right(__truncate(__safe_string(t["provider"], ""), 12), 12)
      + " "
      + __pad_right(__truncate(path_value, 24), 24)
      + " "
      + __pad_right(__truncate(__safe_string(t["module"], ""), 22), 22)
      + " "
      + __pad_right(__truncate(__safe_string(t["handler"], ""), 18), 18)
      + " "
      + __pad_right(vendor, 7)
      + " "
      + __pad_left(to_string(fw_tokens), 8)
      + "  "
      + capabilities
      + "\n"
  }
  return out
}

/**
 * Build the JSON envelope for `--json`. Matches
 * `JsonEnvelope::ok(ROUTES_SCHEMA_VERSION, report)` in the legacy
 * Rust path. Harn's `json_stringify_pretty` sorts keys alphabetically,
 * so the wire byte order differs from serde's struct-field order — the
 * parity test parses both into serde_json::Value and compares.
 *
 * The `triggers[].path`, `triggers[].events`, and various
 * `budgets.*` keys are absent (not null) on the Rust side when their
 * `Option`/empty `Vec` source values would be skipped by serde
 * (`skip_serializing_if`). Re-stripping them here keeps structural
 * parity with the legacy envelope.
 */
fn __strip_absent(report: dict) -> dict {
  let triggers = __safe_list(report["triggers"])
  var cleaned = []
  for trigger in triggers {
    let t = __safe_dict(trigger)
    var entry = {
      id: __safe_string(t["id"], ""),
      kind: __safe_string(t["kind"], ""),
      provider: __safe_string(t["provider"], ""),
      module: __safe_string(t["module"], ""),
      handler: __safe_string(t["handler"], ""),
      requires_capabilities: __safe_list(t["requires_capabilities"]),
      budgets: __strip_budgets(__safe_dict(t["budgets"])),
      vendor_locked: __safe_bool(t["vendor_locked"], false),
      framework_overhead_tokens: __safe_int(t["framework_overhead_tokens"], 0),
    }
    if type_of(t["path"]) == "string" {
      entry = entry + {path: t["path"]}
    }
    let events = __safe_list(t["events"])
    if len(events) > 0 {
      entry = entry + {events: events}
    }
    cleaned = cleaned.push(entry)
  }
  return {triggers: cleaned}
}

fn __strip_budgets(budgets: dict) -> dict {
  var out = {}
  for key in keys(budgets) {
    let value = budgets[key]
    if value != nil {
      out = out + {[key]: value}
    }
  }
  return out
}

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_ROUTES_VIEW_JSON", "")
  if raw == "" {
    harness.stdio
      .eprintln("internal error: HARN_ROUTES_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_ROUTES_VIEW_JSON: " + to_string(e))
    return 70
  }
  if type_of(report) != "dict" {
    harness.stdio.eprintln("internal error: HARN_ROUTES_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!()` which
  // emits `\n`, so the total 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
}