harn-modules 0.7.30

Cross-file module graph and import resolution utilities for Harn
Documentation
// std/text — Text processing utilities for LLM output and code analysis.

// Convert an integer-compatible value into a decimal string with explicit
// stdlib import ergonomics for Harn-authored libraries.
/** int_to_string. */
pub fn int_to_string(value) {
  let parsed = to_int(value)
  require parsed != nil, "int_to_string expects an integer-compatible value"
  return to_string(parsed)
}

// Convert a float-compatible value into a string.
/** float_to_string. */
pub fn float_to_string(value) {
  let parsed = to_float(value)
  require parsed != nil, "float_to_string expects a float-compatible value"
  return to_string(parsed)
}

// Parse an integer-like value, returning fallback when conversion fails.
/** parse_int_or. */
pub fn parse_int_or(value, fallback) {
  return to_int(value) ?? fallback
}

// Parse a float-like value, returning fallback when conversion fails.
/** parse_float_or. */
pub fn parse_float_or(value, fallback) {
  return to_float(value) ?? fallback
}

// Extract file paths from text.
// Splits on newlines, skips comment lines, extracts path-like words,
// validates extensions, and deduplicates.
/** extract_paths. */
pub fn extract_paths(text) {
  let lines = split(text, "\n")
    .filter({ line ->
      let t = trim(line)
      return contains(t, "/")
        && !starts_with(t, "//")
        && !starts_with(t, "#")
    })
  var seen = []
  return lines.flat_map({ line ->
    return split(trim(line), " ").map({ word ->
      var clean = regex_replace("^[\"'`,;:()\\[\\]{}><]+|[\"'`,;:()\\[\\]{}><]+$", "", trim(word))
      while ends_with(clean, ".") || ends_with(clean, ",") || ends_with(clean, ":") || ends_with(clean, ";") {
        clean = substring(clean, 0, len(clean) - 1)
      }
      return clean
    }).filter({ clean ->
      if !contains(clean, "/") && !contains(clean, ".") { return false }
      let ext = extname(clean)
      return ext != "" && len(ext) <= 11
    })
  }).filter({ p ->
    if seen.contains(p) { return false }
    seen = seen + [p]
    return true
  }).sort()
}

// Parse fenced code blocks from LLM response text.
// Returns list of {type: "code"|"call", lang: string|nil, code: string}.
// State machine — not easily expressed as pure map/filter.
/** parse_cells. */
pub fn parse_cells(response) {
  let lines = split(response, "\n")
  var cells = []
  var in_block = false
  var block_type = "code"
  var block_lang = nil
  var current_lines = []

  var trimmed = ""
  var lang_part = ""
  for line in lines {
    trimmed = trim(line)
    if starts_with(trimmed, "```") && in_block {
      cells = cells + [{type: block_type, lang: block_lang, code: join(current_lines, "\n")}]
      in_block = false
      current_lines = []
    } else if starts_with(trimmed, "```") && !in_block {
      lang_part = trim(substring(trimmed, 3))
      if lang_part == "call" || starts_with(lang_part, "call ") {
        block_type = "call"
        if starts_with(lang_part, "call ") {
          block_lang = trim(substring(lang_part, 5))
        } else {
          block_lang = nil
        }
      } else {
        block_type = "code"
        if lang_part == "" {
          block_lang = nil
        } else {
          block_lang = lang_part
        }
      }
      in_block = true
      current_lines = []
    } else if in_block {
      current_lines = current_lines + [line]
    }
  }
  return cells
}

// Filter parsed cells to keep test-relevant ones.
// Keeps type="code" cells and type="call" cells containing "write_file".
// If target_file is provided, only write_file calls mentioning that file are kept.
/** filter_test_cells. */
pub fn filter_test_cells(cells, target_file) {
  return cells.filter({ cell ->
    if cell.type == "call" && contains(cell.code, "write_file") {
      if target_file != nil {
        return contains(cell.code, target_file)
      }
      return true
    }
    return cell.type == "code"
  })
}

// Keep first n and last n lines of text, with an omission marker in between.
/** truncate_head_tail. */
pub fn truncate_head_tail(text, n) {
  let lines = split(text, "\n")
  if len(lines) <= n * 2 {
    return text
  }
  let skipped = len(lines) - n * 2
  return join(lines[:n], "\n")
    + "\n... (" + to_string(skipped) + " lines omitted) ...\n"
    + join(lines[-n:], "\n")
}

// Check if compiler/build output contains compile error indicators.
// Matches Python errors and generic patterns (case-sensitive, matching burin-code behavior).
/** detect_compile_error. */
pub fn detect_compile_error(output) {
  return contains(output, "SyntaxError")
    || contains(output, "IndentationError")
    || contains(output, "ImportError")
    || contains(output, "ModuleNotFoundError")
    || contains(output, "compile error")
    || contains(output, "cannot find")
}

// Check if test output contains both got/actual AND want/expected indicators.
// Returns true only if BOTH sides are present.
/** has_got_want. */
pub fn has_got_want(output) {
  let has_got = contains(output, "got:")
    || contains(output, "Got:")
    || contains(output, "actual:")
    || contains(output, "Actual:")
  let has_want = contains(output, "want:")
    || contains(output, "Want:")
    || contains(output, "expected:")
    || contains(output, "Expected:")
  return has_got && has_want
}

// Extract error-relevant lines from test output.
// Filters for lines containing error keywords, returns first 20 joined with newlines.
/** format_test_errors. */
pub fn format_test_errors(output) {
  let error_lines = split(output, "\n").filter({ line ->
    return contains(line, "FAIL")
      || contains(line, "Error")
      || contains(line, "error")
      || contains(line, "AssertionError")
      || contains(line, "assert")
  })
  if len(error_lines) > 20 {
    return join(error_lines[:20], "\n")
  }
  return join(error_lines, "\n")
}