Pre-release Harn is pre-1.0 — the language, standard library, and CLI may change between releases. See the release notes

Structured refactorings

The std/edit module ships compound, language-aware refactorings built on top of the AST-precise edit primitives (edit_apply_node, edit_insert_at_anchor, edit_safe_text_patch, edit_dry_run). They are the "burin-like" edits an agent loop should reach for instead of regenerating files or hand-patching call sites: each one resolves structure with tree-sitter, previews as a unified diff, and commits atomically through the staged-fs overlay (#1722).

FunctionWhat it does
edit_extract_variableLift a single-line expression into a named local.
edit_extract_functionLift a statement range into a new function; free variables become parameters.
edit_change_signatureReplace a function's whole parameter list.
edit_add_parameterInsert one parameter; fill the argument at every call site.
edit_reorder_parametersPermute parameters and every call's arguments together.
edit_change_return_typeRewrite a function's declared return type.
edit_inlineInline a zero-parameter, single-return function and delete it.
edit_move_declMove a named top-level declaration or Harn binding into another file.

Shared contract

Every refactoring returns the same shape:

{
  ok, applied, result,        // result ∈ applied | no_op | conflict
                              //        | unsupported | invalid_params
  operation, language,
  dry_run,
  touched_files,              // files that actually changed
  unified_diff,               // [{path, diff, lines_added, lines_removed}]
  summary: {files_touched, lines_added, lines_removed},
  conflicts,                  // B.3-style [{code, message, path?}]
  errors, warnings, provenance
}

Three knobs are common to all of them:

  • dry_run: true — stage the edit into a throw-away overlay and return the per-file unified_diff without writing a byte. Always preview first.
  • session_id — stage into a caller-owned staged-fs session (the caller commits). Omit it and the refactoring opens its own transient session and commits atomically — all files flip together, or none do on the first conflict.
  • Capability matrix — when a language lacks the structure a refactoring needs, the call returns result: "unsupported" with a reason instead of guessing. These all require the tools:deterministic capability.

Recipe — move a Harn setting into a config module

Use edit_move_decl when a top-level Harn const, let, or var belongs in another module. The move is structural: Harn selects the named binding from the syntax tree, stages both file changes, and then commits them together. Local bindings and destructuring patterns are not selected by name.

import { edit_move_decl } from "std/edit"

pipeline default(harness: Harness) {
  const preview = edit_move_decl(
    harness.fs,
    harness.random,
    harness.ast,
    {
      path: "src/github.harn",
      symbol: { name: "GITHUB_API_URL" },
      target_file: "src/config.harn",
      dry_run: true,
    },
  )

  harness.stdio.log(preview.unified_diff[0].diff)
}

Review the preview, then repeat the call without dry_run to commit the move. See the std/edit structured-refactoring reference for parameters, result fields, and language coverage.

Recipe — extract a function

Pull a contiguous range of statements out of a top-level function. Free variables of the block (computed from the AST) become parameters; names that resolve to the module level (other functions, imports) stay referenced rather than parameterized.

import { edit_extract_function } from "std/edit"

pipeline default(harness: Harness) {
  // def report(base, qty):
  //     subtotal = base * qty   <- line 1
  //     audit(subtotal)         <- line 2
  //     ...
  const preview = edit_extract_function(
    harness.fs,
    harness.random,
    harness.ast,
    {
      path: "billing.py",
      range: { start_line: 1, end_line: 2 },
      new_name: "compute_subtotal",
      dry_run: true,
    },
  )
  harness.stdio.log(preview.unified_diff[0].diff)
  // def compute_subtotal(base, qty):     <- `base`/`qty` captured,
  //     subtotal = base * qty            <-  `audit` left as a free call
  //     audit(subtotal)
}

Supported: python, javascript, jsx, typescript, tsx, ruby. The generated function is void; if the block produces a value used afterward, thread it back by hand.

Recipe — change a signature across every caller

This is where structured edits earn their keep: add a parameter to a function and fill the argument at all of its call sites in one atomic transaction.

import { edit_add_parameter } from "std/edit"

pipeline default(harness: Harness) {
  // fn scale(value: i64, factor: i64) -> i64 { ... }
  // called as scale(2, 3), scale(4, 5), scale(6, 7)
  const result = edit_add_parameter(
    harness.fs,
    harness.random,
    harness.ast,
    {
      path: "src/lib.rs",
      symbol: { name: "scale" },
      param: "offset: i64",
      default: "0",            // default_fill: inserted at each call site
    },
  )
  if !result.ok {
    harness.stdio.log("refused: " + result.result + " — " + (result.details ?? ""))
    return
  }
  harness.stdio.log("updated " + to_string(result.summary.files_touched) + " file(s)")
  // fn scale(value: i64, factor: i64, offset: i64) -> i64 { ... }
  // scale(2, 3, 0), scale(4, 5, 0), scale(6, 7, 0)
}

callsite_strategy controls how callers are handled:

  • default_fill (default for add_parameter) inserts default at each call.
  • strict refuses with result: "conflict" when callers exist, so you can rewrite the definition only when it is safe.

edit_change_signature takes the full new_params text for arbitrary changes; edit_reorder_parameters permutes parameters and every call's arguments together (refusing on an argument-count mismatch).

Verify the result

Refactorings re-parse the rewritten file with tree-sitter before committing, so a syntactically broken edit surfaces as result: "conflict" rather than landing on disk. To verify behavior after an apply, run the project's own checks — for the scale example above:

const check = run_command({ cmd: ["cargo", "check"], cwd: "." })
harness.stdio.log(check.exit_code == 0 ? "callers still compile" : check.stderr)

Supported languages for the signature family and return-type rewrites: rust, python, typescript, tsx, javascript, jsx, go (JavaScript/JSX have no return-type slot, so edit_change_return_type reports unsupported there).