harn-stdlib 0.10.53

Embedded Harn standard library source catalog
Documentation
import { diff_artifact } from "std/diff"
import { edit_refactor_content_hash } from "std/edit/internal"
import { edit_apply_node, edit_insert_at_anchor } from "std/edit/patch"

/**
 * Test whether an item is present in a refactor planning list.
 *
 * @effects: []
 * @errors: []
 */
pub fn __refactor_member(list, item) -> bool {
  for x in list ?? [] {
    if x == item {
      return true
    }
  }
  return false
}

fn __refactor_touched_paths(ops) {
  let seen = []
  for op in ops ?? [] {
    const path = op?.path
    if path != nil && !__refactor_member(seen, path) {
      seen = seen + [path]
    }
  }
  return seen
}

fn __refactor_common_root(paths) {
  if len(paths) == 0 {
    return "."
  }
  const first = split(paths[0], "/")
  let common = if len(first) > 0 {
    first[:len(first) - 1]
  } else {
    first
  }
  for p in paths {
    const segs = split(p, "/")
    const dir = if len(segs) > 0 {
      segs[:len(segs) - 1]
    } else {
      segs
    }
    let prefix = []
    let idx = 0
    while idx < len(common) && idx < len(dir) && common[idx] == dir[idx] {
      prefix = prefix + [common[idx]]
      idx = idx + 1
    }
    common = prefix
  }
  const root = join(common, "/")
  if root == "" {
    return "."
  }
  return root
}

fn __refactor_new_session(random: HarnessRandom, operation) {
  return "harn-refactor-" + operation + "-" + random.uuid_v7()
}

/**
 * Build the stable result envelope shared by structured refactorings.
 *
 * @effects: []
 * @errors: []
 */
pub fn __refactor_result(operation, language, fields) -> dict {
  const base = {
    ok: false,
    applied: false,
    result: "unsupported",
    operation: operation,
    language: language,
    dry_run: false,
    touched_files: [],
    unified_diff: [],
    summary: {files_touched: 0, lines_added: 0, lines_removed: 0},
    conflicts: [],
    details: nil,
    errors: [],
    warnings: [],
    provenance: {
      module: "std/edit",
      helper: "edit_" + operation,
      operation: operation,
      language: language,
    },
  }
  return base.merging(fields ?? {})
}

/**
 * Build an unsupported-operation refactor result.
 *
 * @effects: []
 * @errors: []
 */
pub fn __refactor_unsupported(operation, language, reason) -> dict {
  return __refactor_result(
    operation,
    language,
    {result: "unsupported", details: reason, errors: [{code: "unsupported", message: reason}]},
  )
}

/**
 * Build an invalid-parameters refactor result.
 *
 * @effects: []
 * @errors: []
 */
pub fn __refactor_invalid(operation, language, reason) -> dict {
  return __refactor_result(
    operation,
    language,
    {
      result: "invalid_params",
      details: reason,
      errors: [{code: "invalid_params", message: reason}],
    },
  )
}

/**
 * Build a conflict refactor result from structured conflicts.
 *
 * @effects: []
 * @errors: []
 */
pub fn __refactor_conflict(operation, language, conflicts) -> dict {
  return __refactor_result(
    operation,
    language,
    {
      result: "conflict",
      conflicts: conflicts,
      details: if len(conflicts) > 0 {
        conflicts[0].message
      } else {
        nil
      },
      errors: conflicts.map({ c -> {code: c.code, message: c.message} }),
    },
  )
}

fn __refactor_exec_op(fs: HarnessFs, ast: HarnessAst, op, session_id, before_map) {
  const kind = op?.kind
  if kind == "content" {
    const before = before_map.get(op.path, "")
    const res = fs.safe_text_patch(
      {
        path: op.path,
        content: op.after,
        expected_hash: edit_refactor_content_hash(before),
        session_id: session_id,
        create_parents: true,
        overwrite: true,
      },
    )
    const tag = res?.result ?? "applied"
    return {
      ok: tag == "applied",
      result: tag,
      conflict: {code: tag, message: "content write `" + tag + "` at " + op.path, path: op.path},
    }
  }
  if kind == "node_apply" {
    const res = edit_apply_node(ast, (op?.params ?? {}).merging({session_id: session_id}))
    return {
      ok: res.result == "applied",
      result: res.result,
      conflict: {
        code: res.result,
        message: res.details ?? ("apply_node " + res.result),
        path: op?.path,
      },
    }
  }
  if kind == "node_insert" {
    const res = edit_insert_at_anchor(ast, (op?.params ?? {}).merging({session_id: session_id}))
    return {
      ok: res.result == "applied",
      result: res.result,
      conflict: {
        code: res.result,
        message: res.details ?? ("insert_at_anchor " + res.result),
        path: op?.path,
      },
    }
  }
  return {
    ok: false,
    result: "invalid_op",
    conflict: {code: "invalid_op", message: "unknown refactor op kind"},
  }
}

/**
 * Lower structured refactor operations into staged writes.
 *
 * A dry run previews and discards one overlay. An apply either commits every
 * operation atomically or discards the overlay after the first conflict.
 * Callers that provide `session_id` retain ownership of the final commit.
 *
 * @effects: [host, fs]
 * @errors: [backend]
 */
pub fn __refactor_run(
  fs: HarnessFs,
  random: HarnessRandom,
  ast: HarnessAst,
  operation,
  language,
  ops,
  params,
  warnings,
) -> dict {
  const p = params ?? {}
  const dry_run = p?.dry_run ?? false
  const warns = warnings ?? []
  if len(ops) == 0 {
    return __refactor_result(
      operation,
      language,
      {ok: true, applied: true, result: "no_op", dry_run: dry_run, warnings: warns},
    )
  }
  const touched = __refactor_touched_paths(ops)
  const caller_session = p?.session_id
  const manage_session = dry_run || caller_session == nil
  const session_id = if manage_session {
    __refactor_new_session(random, operation)
  } else {
    caller_session
  }
  if manage_session {
    if dry_run {
      // A preview never commits, so leave the overlay state at the host
      // default (cwd-relative) rather than scattering `.harn/state` next
      // to the target files — same as `edit_dry_run`.
      fs.set_mode({session_id: session_id, mode: "staged"})
    } else {
      // An apply must commit to disk, so root the overlay at the tightest
      // directory enclosing every touched path.
      fs.set_mode(
        {session_id: session_id, mode: "staged", root: p?.root ?? __refactor_common_root(touched)},
      )
    }
  }
  let before_map = {}
  for path in touched {
    const r = fs.staged_read_text({path: path, session_id: session_id})
    before_map = before_map.merging({[path]: r?.content ?? ""})
  }
  let conflicts = []
  let ok_all = true
  for op in ops {
    const outcome = __refactor_exec_op(fs, ast, op, session_id, before_map)
    if !outcome.ok {
      conflicts = conflicts + [outcome.conflict]
      ok_all = false
      break
    }
  }
  let diffs = []
  let changed = []
  let added = 0
  let removed = 0
  if ok_all {
    for path in touched {
      const before = before_map.get(path, "")
      const after_read = fs.staged_read_text({path: path, session_id: session_id})
      const after = after_read?.content ?? before
      if after != before {
        const artifact = diff_artifact(before, after, {path: path})
        changed = changed + [path]
        added = added + artifact.insertions
        removed = removed + artifact.deletions
        diffs = diffs
          + [
          {
            path: path,
            diff: artifact.diff,
            lines_added: artifact.insertions,
            lines_removed: artifact.deletions,
          },
        ]
      }
    }
  }
  if manage_session {
    if dry_run || !ok_all {
      fs.discard_staged({session_id: session_id})
    } else {
      fs.commit_staged({session_id: session_id})
    }
  }
  const result_tag = if !ok_all {
    "conflict"
  } else if len(changed) == 0 {
    "no_op"
  } else {
    "applied"
  }
  return __refactor_result(
    operation,
    language,
    {
      ok: result_tag == "applied",
      applied: result_tag == "applied" || result_tag == "no_op",
      result: result_tag,
      dry_run: dry_run,
      touched_files: changed,
      unified_diff: diffs,
      summary: {files_touched: len(changed), lines_added: added, lines_removed: removed},
      conflicts: conflicts,
      details: if !ok_all {
        conflicts[0].message
      } else {
        nil
      },
      errors: conflicts,
      warnings: warns,
    },
  )
}