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,
},
)
}