// tools/hako_check/cli.hako - HakoAnalyzerBox (MVP)
using selfhost.shared.common.string_helpers as Str
using tools.hako_check.analysis_consumer as HakoAnalysisBuilderBox
using tools.hako_check.rules.rule_include_forbidden as RuleIncludeForbiddenBox
using tools.hako_check.rules.rule_using_quoted as RuleUsingQuotedBox
using tools.hako_check.rules.rule_static_top_assign as RuleStaticTopAssignBox
using tools.hako_check.rules.rule_global_assign as RuleGlobalAssignBox
using tools.hako_check.rules.rule_dead_methods as RuleDeadMethodsBox
using tools.hako_check.rules.rule_jsonfrag_usage as RuleJsonfragUsageBox
using tools.hako_check.rules.rule_unused_alias as RuleUnusedAliasBox
using tools.hako_check.rules.rule_non_ascii_quotes as RuleNonAsciiQuotesBox
using tools.hako_check.rules.rule_dead_static_box as RuleDeadStaticBoxBox
using tools.hako_check.rules.rule_duplicate_method as RuleDuplicateMethodBox
using tools.hako_check.rules.rule_missing_entrypoint as RuleMissingEntrypointBox
using tools.hako_check.rules.rule_top_level_local as RuleTopLevelLocalBox
using tools.hako_check.rules.rule_arity_mismatch as RuleArityMismatchBox
using tools.hako_check.rules.rule_stage3_gate as RuleStage3GateBox
using tools.hako_check.rules.rule_brace_heuristics as RuleBraceHeuristicsBox
using tools.hako_check.rules.rule_analyzer_io_safety as RuleAnalyzerIoSafetyBox
using tools.hako_check.rules.rule_dead_code as DeadCodeAnalyzerBox
using tools.hako_check.rules.rule_dead_blocks as DeadBlockAnalyzerBox
using tools.hako_check.render.graphviz as GraphvizRenderBox
using tools.hako_parser.parser_core as HakoParserCoreBox

static box HakoAnalyzerBox {
  run(args) {
    if args == null || args.size() < 1 { print("[lint/error] missing paths"); return 2 }
    // options: --format {text|dot|json} (accept anywhere)
    local fmt = "text"
    local debug = 0
    // Default: AST path OFF (enable with --force-ast)
    local no_ast = 1
    // QuickFix options (Phase 21.8)
    local fix_dry_run = 0
    local fix_scope = "text" // reserved for future
    local fix_plan = 0
    // AST rename options
    local ren_boxes = new ArrayBox()      // Array<Map{old,new}>
    local ren_methods = new ArrayBox()    // Array<Map{box,old,new}>
    // single-pass parse: handle options in-place and collect sources
    local i = 0
    local fail = 0
    local irs = new ArrayBox()
    local diags = new ArrayBox()
    // optional filters
    local rules_only = null  // ArrayBox of keys
    local rules_skip = null  // ArrayBox of keys
    local dead_code_mode = 0  // Phase 153: --dead-code flag
    local dead_blocks_mode = 0  // Phase 154: --dead-blocks flag
    local mir_json_content = null  // Phase 156: MIR JSON content for CFG integration
    // Support inline sources: --source-file <path> <text>. Also accept --debug and --format anywhere.
    while i < args.size() {
      local p = args.get(i)
      // handle options
      if p == "--debug" { debug = 1; i = i + 1; continue }
      if p == "--dead-code" { dead_code_mode = 1; i = i + 1; continue }
      if p == "--dead-blocks" { dead_blocks_mode = 1; i = i + 1; continue }
      if p == "--mir-json-content" {
        if i + 1 >= args.size() { print("[lint/error] --mir-json-content requires content"); return 2 }
        mir_json_content = args.get(i+1); i = i + 2; continue
      }
      if p == "--no-ast" { no_ast = 1; i = i + 1; continue }
      if p == "--force-ast" { no_ast = 0; i = i + 1; continue }
      if p == "--format" {
        if i + 1 >= args.size() { print("[lint/error] --format requires value"); return 2 }
        fmt = args.get(i+1); i = i + 2; continue
      }
      if p == "--fix-dry-run" { fix_dry_run = 1; i = i + 1; continue }
      if p == "--fix-scope" {
        if i + 1 >= args.size() { print("[lint/error] --fix-scope requires value"); return 2 }
        fix_scope = args.get(i+1); i = i + 2; continue
      }
      if p == "--fix-plan" { fix_plan = 1; i = i + 1; continue }
      if p == "--rename-box" {
        if i + 2 >= args.size() { print("[lint/error] --rename-box requires <Old> <New>"); return 2 }
        local rec = new MapBox(); rec.set("old", args.get(i+1)); rec.set("new", args.get(i+2)); ren_boxes.push(rec); i = i + 3; continue
      }
      if p == "--rename-method" {
        if i + 3 >= args.size() { print("[lint/error] --rename-method requires <Box> <Old> <New>"); return 2 }
        local recm = new MapBox(); recm.set("box", args.get(i+1)); recm.set("old", args.get(i+2)); recm.set("new", args.get(i+3)); ren_methods.push(recm); i = i + 4; continue
      }
      if p == "--rules" {
        if i + 1 >= args.size() { print("[lint/error] --rules requires CSV"); return 2 }
        rules_only = me._parse_csv(args.get(i+1)); i = i + 2; continue
      }
      if p == "--skip-rules" {
        if i + 1 >= args.size() { print("[lint/error] --skip-rules requires CSV"); return 2 }
        rules_skip = me._parse_csv(args.get(i+1)); i = i + 2; continue
      }
      // source handling
      local text = null
      if p == "--source-file" {
        if i + 2 < args.size() { p = args.get(i+1); text = args.get(i+2); i = i + 3 } else { print("[lint/error] --source-file requires <path> <text>"); return 2 }
      } else {
        // Read from filesystem via FileBox (plugin must be available)
        local f = new FileBox(); if f.open(p) == 0 { print("[lint/error] cannot open: " + p); fail = fail + 1; i = i + 1; continue }
        text = f.read(); f.close(); i = i + 1
      }
      // keep a copy before sanitize for rules that must see original bytes (HC017, etc.)
      local text_raw = text
      // pre-sanitize (ASCII quotes, normalize newlines) - minimal & reversible
      text = me._sanitize(text)

      // Phase 21.8: QuickFix (--fix-dry-run)
      if fix_dry_run == 1 && (ren_boxes.size() == 0 && ren_methods.size() == 0) {
        // Apply text-scope quick fixes in a safe subset and print unified diff
        local patched = me._fix_text_quick(text)
        if patched != null && patched != text {
          me._print_unified_diff(p, text, patched)
        }
        // In dry-run mode, we do not run analyzers further
        continue
      }

      // AST-scope rename (dry-run or plan)
      if (ren_boxes.size() > 0 || ren_methods.size() > 0) {
        if fix_plan == 1 {
          me._print_refactor_plan_json(p, ren_boxes, ren_methods)
          continue
        }
        if fix_dry_run == 1 {
          local patched_ast = me._fix_ast_rename(text, p, ren_boxes, ren_methods)
          if patched_ast != null && patched_ast != text { me._print_unified_diff(p, text, patched_ast) }
          continue
        }
      }
      // analysis - only build IR if needed by active rules
      local ir = null
      if me._needs_ir(rules_only, rules_skip) == 1 {
        // Enable AST inside IR builder only when the active rules explicitly need it
        local want_ast = me._needs_ast(rules_only, rules_skip)
        local no_ast_eff = no_ast
        if want_ast == 1 { no_ast_eff = 0 }
        ir = HakoAnalysisBuilderBox.build_from_source_flags(text, p, no_ast_eff)
        // Fail-Fast: Guard against void return from builder
        if ir == null || ir == void {
          ir = new MapBox()
          ir.set("path", p)
          ir.set("methods", new ArrayBox())
          ir.set("calls", new ArrayBox())
          ir.set("boxes", new ArrayBox())
          ir.set("entrypoints", new ArrayBox())
        }
        // Phase 156: Integrate MIR CFG if available
        if mir_json_content != null { ir.set("_mir_json_text", mir_json_content) }
      } else {
        // Minimal IR stub for rules that don't need it
        ir = new MapBox()
        ir.set("path", p)
        ir.set("methods", new ArrayBox())
        ir.set("calls", new ArrayBox())
        ir.set("boxes", new ArrayBox())
        ir.set("entrypoints", new ArrayBox())
      }
      // parse AST only when explicitly needed by active rules (include_forbidden fallback allowed)
      local ast = null
      if no_ast == 0 && me._needs_ast(rules_only, rules_skip) == 1 { ast = HakoParserCoreBox.parse(text) }
      if debug == 1 {
        local mc = (ir.get("methods")!=null)?ir.get("methods").size():0
        local cc = (ir.get("calls")!=null)?ir.get("calls").size():0
        local ec = (ir.get("entrypoints")!=null)?ir.get("entrypoints").size():0
        print("[hako_check/IR] file=" + p + " methods=" + me._itoa(mc) + " calls=" + me._itoa(cc) + " eps=" + me._itoa(ec))
      }
      irs.push(ir)
      // rules that work on raw source
      local out = new ArrayBox()
      if ast != null {
        local before = out.size()
        if me._rule_enabled(rules_only, rules_skip, "include_forbidden") == 1 { RuleIncludeForbiddenBox.apply_ast(ast, p, out) }
        // Fallback to text scan if AST did not detect any include
        if out.size() == before && me._rule_enabled(rules_only, rules_skip, "include_forbidden") == 1 { RuleIncludeForbiddenBox.apply(text, p, out) }
      } else {
        if me._rule_enabled(rules_only, rules_skip, "include_forbidden") == 1 { RuleIncludeForbiddenBox.apply(text, p, out) }
      }
      if me._rule_enabled(rules_only, rules_skip, "using_quoted") == 1 { RuleUsingQuotedBox.apply(text, p, out) }
      if me._rule_enabled(rules_only, rules_skip, "unused_alias") == 1 { RuleUnusedAliasBox.apply(text, p, out) }
      if me._rule_enabled(rules_only, rules_skip, "static_top_assign") == 1 { RuleStaticTopAssignBox.apply(text, p, out) }
      if me._rule_enabled(rules_only, rules_skip, "global_assign") == 1 { RuleGlobalAssignBox.apply(text, p, out) }
      // HC017 must inspect original text prior to sanitize
      if me._rule_enabled(rules_only, rules_skip, "non_ascii_quotes") == 1 { RuleNonAsciiQuotesBox.apply(text_raw, p, out) }
      if me._rule_enabled(rules_only, rules_skip, "jsonfrag_usage") == 1 { RuleJsonfragUsageBox.apply(text, p, out) }
      if me._rule_enabled(rules_only, rules_skip, "top_level_local") == 1 { RuleTopLevelLocalBox.apply(text, p, out) }
      if me._rule_enabled(rules_only, rules_skip, "stage3_gate") == 1 { RuleStage3GateBox.apply(text, p, out) }
      // HC031 must inspect original text prior to sanitize (like HC017)
      local before_hc031 = out.size()
      if me._rule_enabled(rules_only, rules_skip, "brace_heuristics") == 1 { RuleBraceHeuristicsBox.apply(text_raw, p, out) }
      if debug == 1 {
        local added_hc031 = out.size() - before_hc031
        print("[hako_check/HC031] file=" + p + " added=" + me._itoa(added_hc031) + " total_out=" + me._itoa(out.size()))
      }
      if me._rule_enabled(rules_only, rules_skip, "analyzer_io_safety") == 1 { RuleAnalyzerIoSafetyBox.apply(text, p, out) }
      // rules that need IR (enable dead code detection)
      local before_n = out.size()
      if me._rule_enabled(rules_only, rules_skip, "dead_methods") == 1 {
        me._log_stderr("[rule/exec] HC011 (dead_methods) " + p)
        RuleDeadMethodsBox.apply_ir(ir, p, out)
      }
      if debug == 1 {
        local after_n = out.size()
        local added = after_n - before_n
        print("[hako_check/HC011] file=" + p + " added=" + me._itoa(added) + " total_out=" + me._itoa(after_n))
      }
      before_n = out.size()
      if me._rule_enabled(rules_only, rules_skip, "dead_static_box") == 1 {
        me._log_stderr("[rule/exec] HC012 (dead_static_box) " + p)
        RuleDeadStaticBoxBox.apply_ir(ir, p, out)
      }
      if debug == 1 {
        local after_n = out.size()
        local added = after_n - before_n
        local boxes_count = (ir.get("boxes")!=null)?ir.get("boxes").size():0
        print("[hako_check/HC012] file=" + p + " boxes=" + me._itoa(boxes_count) + " added=" + me._itoa(added) + " total_out=" + me._itoa(after_n))
      }
      before_n = out.size()
      if me._rule_enabled(rules_only, rules_skip, "duplicate_method") == 1 {
        me._log_stderr("[rule/exec] HC013 (duplicate_method) " + p)
        RuleDuplicateMethodBox.apply_ir(ir, p, out)
      }
      if debug == 1 {
        local after_n = out.size()
        local added = after_n - before_n
        print("[hako_check/HC013] file=" + p + " added=" + me._itoa(added) + " total_out=" + me._itoa(after_n))
      }
      before_n = out.size()
      if me._rule_enabled(rules_only, rules_skip, "missing_entrypoint") == 1 {
        me._log_stderr("[rule/exec] HC014 (missing_entrypoint) " + p)
        RuleMissingEntrypointBox.apply_ir(ir, p, out)
      }
      if debug == 1 {
        local after_n = out.size()
        local added = after_n - before_n
        print("[hako_check/HC014] file=" + p + " added=" + me._itoa(added) + " total_out=" + me._itoa(after_n))
      }
      before_n = out.size()
      if me._rule_enabled(rules_only, rules_skip, "arity_mismatch") == 1 {
        me._log_stderr("[rule/exec] HC015 (arity_mismatch) " + p)
        RuleArityMismatchBox.apply_ir(ir, p, out)
      }
      if debug == 1 {
        local after_n = out.size()
        local added = after_n - before_n
        print("[hako_check/HC015] file=" + p + " added=" + me._itoa(added) + " total_out=" + me._itoa(after_n))
      }
      // Phase 153: HC019 Dead Code Analyzer (comprehensive)
      before_n = out.size()
      if dead_code_mode == 1 || me._rule_enabled(rules_only, rules_skip, "dead_code") == 1 {
        me._log_stderr("[rule/exec] HC019 (dead_code) " + p)
        DeadCodeAnalyzerBox.apply_ir(ir, p, out)
      }
      if debug == 1 {
        local after_n = out.size()
        local added = after_n - before_n
        print("[hako_check/HC019] file=" + p + " added=" + me._itoa(added) + " total_out=" + me._itoa(after_n))
      }
      // Phase 154: HC020 Dead Block Analyzer (block-level unreachable detection)
      before_n = out.size()
      if dead_blocks_mode == 1 || me._rule_enabled(rules_only, rules_skip, "dead_blocks") == 1 {
        me._log_stderr("[rule/exec] HC020 (dead_blocks) " + p)
        DeadBlockAnalyzerBox.apply_ir(ir, p, out)
      }
      if debug == 1 {
        local after_n = out.size()
        local added = after_n - before_n
        print("[hako_check/HC020] file=" + p + " added=" + me._itoa(added) + " total_out=" + me._itoa(after_n))
      }
      // suppression: HC012(dead box) > HC011(unreachable method)
      local filtered = me._suppress_overlap(out)
      // flush (text only)
      local n = filtered.size(); if n > 0 && fmt == "text" {
        local j = 0; while j < n { print(filtered.get(j)); j = j + 1 }
      }
      // collect diagnostics for json-lsp
      local j2 = 0; while j2 < n { local msg = filtered.get(j2); local d = me._parse_msg_to_diag(msg, p); if d != null { diags.push(d) }; j2 = j2 + 1 }
      fail = fail + n
    }
    // optional DOT/JSON output
    if fmt == "dot" { me._render_dot_multi(irs) }
    if fmt == "json-lsp" { me._render_json_lsp(diags) }
    // return number of findings as RC
    return fail
  }
  // no-op
  _sanitize(text) {
    if text == null { return text }
    // Normalize CRLF -> LF
    local out = ""
    local n = text.length()
    local i2 = 0
    while i2 < n {
      local ch = text.substring(i2, i2+1)
      // drop CR
      if ch == "\r" { i2 = i2 + 1; continue }
      // NOTE: Fancy quote conversion removed - StringBox lacks byte-level access
      out = out.concat(ch)
      i2 = i2 + 1
    }
    return out
  }
  _render_json_lsp(diags) {
    // Emit diagnostics pretty-printed to match expected fixtures
    diags = me._sort_diags(diags)
    print("{\"diagnostics\":[")
    if diags != null {
      local i = 0
      while i < diags.size() {
        local d = diags.get(i)
        local file = me._json_quote(d.get("file"))
        local line = me._itoa(d.get("line"))
        local rule = me._json_quote(d.get("rule"))
        local msg = me._json_quote(d.get("message"))
        local qf = d.get("quickFix"); if qf == null { qf = "" }
        local sev = d.get("severity"); if sev == null { sev = "warning" }
        local qfj = me._json_quote(qf)
        local entry = "  {\"file\":" + file + ",\"line\":" + line + ",\"rule\":" + rule + ",\"message\":" + msg + ",\"quickFix\":" + qfj + ",\"severity\":\"" + sev + "\"}"
        if i != diags.size()-1 { print(entry + ",") } else { print(entry) }
        i = i + 1
      }
    }
    print("]}")
    return 0
  }
  // ---- QuickFix (text scope, MVP: HC003 using quoted) ----
  _fix_text_quick(text) {
    if text == null { return null }
    // QuickFix (text scope):
    //  - HC002 include → using
    //  - HC003 using 非引用 → 引用化
    //  - HC016 未使用 alias の削除（using ... as Alias）
    //  - HC014 missing entrypoint の空スタブ提案
    local lines = me._split_lines(text)
    local changed = 0
    local out = new ArrayBox()
    // 事前: alias 宣言の収集（行番号と別名）
    local alias_lines = new ArrayBox() // of Map {idx, alias}
    local i = 0
    while i < lines.size() {
      local ln = lines.get(i)
      // HC002: include → using（相対→modulesの推測はしない；文字列そのまま移行）
      {
        local ltrim = me._ltrim(ln)
        if ltrim.indexOf("include \"") == 0 {
          // preserve indentation and trailing content (;等)
          local head_len = ln.indexOf("include \"")
          if head_len < 0 { head_len = 0 }
          local head = ln.substring(0, head_len)
          local rest = ln.substring(head_len)
          // rest begins with include "
          local after = rest.substring(8) // length("include ") = 8
          local newline = head + "using " + after
          if newline != ln { changed = 1; out.push(newline) } else { out.push(ln) }
          i = i + 1
          continue
        }
      }

      // HC016 用: using ... as Alias の抽出
      {
        local ltrim2 = me._ltrim(ln)
        if ltrim2.indexOf("using ") == 0 && ltrim2.indexOf(" as ") > 0 {
          // パターン: using "X" as Alias もしくは using X as Alias
          local aspos = ltrim2.indexOf(" as ")
          local tail = ltrim2.substring(aspos + 4)
          // Alias の終端は空白/セミコロン/行末
          local j = 0; local alias = ""
          while j < tail.length() {
            local ch = tail.substring(j,j+1)
            if ch == " " || ch == "\t" || ch == ";" { break }
            alias = alias + ch
            j = j + 1
          }
          if alias != "" {
            local ent = new MapBox(); ent.set("idx", i); ent.set("alias", alias); alias_lines.push(ent)
          }
        }
      }

      // HC003: using 非引用 → 引用化
      local ltrim = me._ltrim(ln)
      if ltrim.indexOf("using ") == 0 && ltrim.indexOf('using "') != 0 {
        // rewrite: using X ... -> using "X" ...（X は最初の空白/セミコロン/"as" まで）
        local prefix_len = ln.indexOf("using ")
        if prefix_len < 0 { prefix_len = 0 }
        local head = ln.substring(0, prefix_len)
        local rest = ln.substring(prefix_len)
        // rest starts with 'using '
        local after = rest.substring(6)
        // Find boundary
        local b = 0; local n = after.length()
        while b < n {
          local ch = after.substring(b,b+1)
          // stop at space, semicolon or before ' as '
          if ch == " " || ch == ";" { break }
          b = b + 1
        }
        local name = after.substring(0,b)
        local tail = after.substring(b)
        // If tail starts with ' as ' but name had trailing spaces, we already stopped at space
        // Wrap name with quotes if not already
        if name.length() > 0 {
          local newline = head + "using \"" + name + "\"" + tail
          if newline != ln { changed = 1; out.push(newline) } else { out.push(ln) }
        } else { out.push(ln) }
      } else {
        out.push(ln)
      }
      i = i + 1
    }
    // HC016: 未使用 alias の削除（安全に、完全一致のみ削除）
    if alias_lines.size() > 0 {
      // 再結合した out を基にテキストを作り、使用有無チェック
      local cur_text = ""; local k=0; while k<out.size() { cur_text = cur_text + out.get(k); if k<out.size()-1 { cur_text = cur_text + "\n" } k=k+1 }
      // 逆順に処理して行番号のズレを回避
      local ai = alias_lines.size() - 1
      while ai >= 0 {
        local ent = alias_lines.get(ai)
        local idx = ent.get("idx")
        local alias = ent.get("alias")
        if me._alias_unused(cur_text, alias) == 1 {
          // remove that line from out if matches using ... as alias
          // ただし out の該当行が using でない場合はスキップ
          if idx >= 0 && idx < out.size() {
            local l = me._ltrim(out.get(idx))
            if l.indexOf("using ") == 0 && l.indexOf(" as ") > 0 && l.indexOf(" " + alias) > 0 {
              // delete
              out.set(idx, null)
              changed = 1
            }
          }
        }
        ai = ai - 1
      }
      // null を除去
      local out2 = new ArrayBox(); local z=0; while z<out.size() { if out.get(z)!=null { out2.push(out.get(z)) } z=z+1 }
      out = out2
    }

    // HC014: missing entrypoint → 空スタブ提案
    local joined = ""; local jj=0; while jj<out.size() { joined = joined + out.get(jj); if jj<out.size()-1 { joined = joined + "\n" } jj=jj+1 }
    if me._has_entrypoint(joined) == 0 {
      // append stub
      local stub = "\nstatic box Main { method main(args) { return 0 } }"
      joined = joined + stub
      changed = 1
      // split again for diff
      out = me._split_lines(joined)
    }

    if changed == 0 { return text }
    // Join with \n
    local j = 0; local buf = ""; while j < out.size() { buf = buf + out.get(j); if j < out.size()-1 { buf = buf + "\n" } j = j + 1 }
    return buf
  }
  _alias_unused(text, alias) {
    if text == null || alias == null { return 0 }
    // 使用判定は保守的に: 'Alias.' / 'Alias(' / 'Alias ' / 'Alias::' のいずれかが存在すれば使用中とみなす
    local pats = new ArrayBox(); pats.push(alias+"."); pats.push(alias+"("); pats.push(alias+" "); pats.push(alias+"::")
    local i = 0; while i < pats.size() {
      if text.indexOf(pats.get(i)) >= 0 { return 0 }
      i = i + 1
    }
    return 1
  }
  _has_entrypoint(text) {
    if text == null { return 1 }
    // Rough check: presence of 'method main(' is considered as entrypoint
    if text.indexOf("method main(") >= 0 { return 1 }
    return 0
  }
  _print_unified_diff(path, before, after) {
    if before == null || after == null || before == after { return 0 }
    // Minimal unified diff for whole file replacement（MVP）
    print("--- " + path)
    print("+++ " + path)
    print("@@")
    // Print lines with +/-; this MVP prints only changed lines as + and original as - when they differ line-wise
    local bl = me._split_lines(before); local al = me._split_lines(after)
    local max = bl.size(); if al.size() > max { max = al.size() }
    local i = 0
    while i < max {
      local b = i < bl.size() ? bl.get(i) : null
      local a = i < al.size() ? al.get(i) : null
      if b == a {
        print(" " + (a==null?"":a))
      } else {
        if b != null { print("-" + b) }
        if a != null { print("+" + a) }
      }
      i = i + 1
    }
    return 0
  }

  // ---- AST rename (Box/Method) within a single file ----
  _fix_ast_rename(text, path, ren_boxes, ren_methods) {
    if text == null { return null }
    local out = text
    // Box rename first (affects definitions and calls)
    if ren_boxes != null {
      local i = 0
      while i < ren_boxes.size() {
        local rb = ren_boxes.get(i)
        out = me._rename_box_text(out, rb.get("old"), rb.get("new"))
        i = i + 1
      }
    }
    // Method renames scoped by box, use AST spans for definitions
    if ren_methods != null && ren_methods.size() > 0 {
      // parse AST once to get method span lines
      local ast = HakoParserCoreBox.parse(out)
      local i2 = 0
      while i2 < ren_methods.size() {
        local rm = ren_methods.get(i2)
        out = me._rename_method_text(out, ast, rm.get("box"), rm.get("old"), rm.get("new"))
        i2 = i2 + 1
      }
    }
    return out
  }
  _rename_box_text(text, old, neu) {
    if text == null || old == null || neu == null { return text }
    local lines = me._split_lines(text)
    local changed = 0
    // Def line: static box Old { → replace Old only after the prefix
    local i = 0
    while i < lines.size() {
      local ln = lines.get(i)
      local pref = "static box " + old + " "
      if me._ltrim(ln).indexOf(pref) == 0 {
        lines.set(i, ln.replace(pref, "static box " + neu + " "))
        changed = 1
      }
      // Calls: occurrences of "Old." → "New." when "Old." is found
      if ln.indexOf(old + ".") >= 0 {
        // simplistic boundary: avoid replacing inside quoted strings is out of scope (MVP)
        lines.set(i, ln.replace(old + ".", neu + "."))
        if lines.get(i) != ln { changed = 1 }
      }
      i = i + 1
    }
    if changed == 0 { return text }
    // join
    local j=0; local buf=""; while j<lines.size() { buf = buf + lines.get(j); if j<lines.size()-1 { buf = buf + "\n" } j=j+1 }
    return buf
  }
  _rename_method_text(text, ast, box, old, neu) {
    // NOTE(Phase 73): AST-based method rename QuickFix is temporarily disabled
    // for Stage-3 parser compatibility. For now, return original text unchanged.
    if text == null { return text }
    return text
  }
  _print_refactor_plan_json(path, ren_boxes, ren_methods) {
    // Print a small JSON describing intended refactors and a shell apply skeleton
    print("{")
    print("  \"file\": " + me._json_quote(path) + ",")
    // boxes
    print("  \"rename_boxes\": [")
    if ren_boxes != null {
      local i=0; while i<ren_boxes.size() {
        local rb = ren_boxes.get(i)
        print("    {\"old\": " + me._json_quote(rb.get("old")) + ", \"new\": " + me._json_quote(rb.get("new")) + "}" + (i<ren_boxes.size()-1?",":""))
        i = i + 1
      }
    }
    print("  ],")
    // methods
    print("  \"rename_methods\": [")
    if ren_methods != null {
      local j=0; while j<ren_methods.size() {
        local rm = ren_methods.get(j)
        print("    {\"box\": " + me._json_quote(rm.get("box")) + ", \"old\": " + me._json_quote(rm.get("old")) + ", \"new\": " + me._json_quote(rm.get("new")) + "}" + (j<ren_methods.size()-1?",":""))
        j = j + 1
      }
    }
    print("  ],")
    // apply script (sed-based skeleton; manual review required)
    print("  \"apply_script\": " + me._json_quote("#!/bin/sh\n# Review before apply\nFILE=\"" + path + "\"\n# Box renames\n"))
    if ren_boxes != null {
      local i2=0; while i2<ren_boxes.size() {
        local rb2=ren_boxes.get(i2)
        print(me._json_quote("sed -i -E 's/\\b" + rb2.get("old") + "\\./" + rb2.get("new") + "./g' \"$FILE\"\n") + "+")
        i2 = i2 + 1
      }
    }
    print(me._json_quote("# Method renames\n"))
    if ren_methods != null {
      local j2=0; while j2<ren_methods.size() {
        local rm2=ren_methods.get(j2)
        print(me._json_quote("sed -i -E 's/" + rm2.get("box") + "\\." + rm2.get("old") + "\\(/" + rm2.get("box") + "." + rm2.get("new") + "(/g' \"$FILE\"\n") + "+")
        j2 = j2 + 1
      }
    }
    print(me._json_quote("# Definition lines may require manual adjustment if formatting differs\n"))
    print("}")
    return 0
  }
  _needs_ast(only, skip) {
    // Parse AST when duplicate_method is explicitly requested (needs method spans/definitions precision).
    if only != null {
      local i = 0; while i < only.size() { local k = only.get(i); if k == "duplicate_method" { return 1 } if k == "force_ast" { return 1 } i = i + 1 }
      return 0
    }
    // Default (all rules): avoid AST to reduce VM/PHI risks; rely on IR/text fallbacks.
    return 0
  }
  _needs_ir(only, skip) {
    // IR is needed for rules that analyze methods, calls, or boxes
    // Text-only rules (brace_heuristics, non_ascii_quotes, etc.) don't need IR
    if only != null {
      local i = 0
      while i < only.size() {
        local k = only.get(i)
        // Rules that need IR
        if k == "dead_methods" { return 1 }
        if k == "dead_static_box" { return 1 }
        if k == "dead_code" { return 1 }
        if k == "duplicate_method" { return 1 }
        if k == "missing_entrypoint" { return 1 }
        if k == "arity_mismatch" { return 1 }
        if k == "include_forbidden" { return 1 }
        if k == "force_ast" { return 1 }
        i = i + 1
      }
      // If we get here, only text-based rules are active (e.g., brace_heuristics)
      return 0
    }
    // Default (all rules): need IR
    return 1
  }
  _parse_csv(s) {
    if s == null { return null }
    local arr = new ArrayBox();
    local cur = ""; local i = 0; while i < s.length() {
      local ch = s.substring(i,i+1)
      if ch == "," { if cur != "" { arr.push(cur) }; cur = ""; i = i + 1; continue }
      cur = cur + ch; i = i + 1
    }
    if cur != "" { arr.push(cur) }
    return arr
  }
  _rule_enabled(only, skip, key) {
    if key == null { return 1 }
    if only != null {
      // enabled only if key is present
      local i = 0; while i < only.size() { if only.get(i) == key { return 1 } i = i + 1 }
      return 0
    }
    if skip != null {
      local j = 0; while j < skip.size() { if skip.get(j) == key { return 0 } j = j + 1 }
    }
    return 1
  }
  // Build dead-box set and drop HC011 for methods inside dead boxes
  _suppress_overlap(out) {
    if out == null { return new ArrayBox() }
    // collect dead boxes from HC012 messages
    local dead = new MapBox()
    local i = 0
    while i < out.size() {
      local s = out.get(i)
      if me._is_hc012(s) == 1 {
        local bx = me._extract_box_from_hc012(s)
        if bx != null { dead.set(bx, 1) }
      }
      i = i + 1
    }
    // filter
    local res = new ArrayBox()
    i = 0
    while i < out.size() {
      local s = out.get(i)
      if me._is_hc011(s) == 1 {
        local qual = me._extract_method_from_hc011(s)
        if qual != null {
          // method qual: Box.method/arity -> Box
          local dot = qual.lastIndexOf(".")
          if dot > 0 {
          local box_name = qual.substring(0, dot)
            if dead.has(box_name) == 1 { i = i + 1; continue }
          }
        }
      }
      res.push(s)
      i = i + 1
    }
    return res
  }
  _is_hc011(s) {
    if s == null { return 0 }
    if s.indexOf("[HC011]") == 0 { return 1 }
    return 0
  }
  _is_hc012(s) {
    if s == null { return 0 }
    if s.indexOf("[HC012]") == 0 { return 1 }
    return 0
  }
  _extract_box_from_hc012(s) {
    // format: [HC012] dead static box (never referenced): Name
    if s == null { return null }
    local p = s.lastIndexOf(":")
    if p < 0 { return null }
    local name = s.substring(p+1)
    // trim spaces
    local t = 0; while t < name.length() { local c=name.substring(t,t+1); if c==" "||c=="\t" { t=t+1; continue } break }
    if t > 0 { name = name.substring(t) }
    return name
  }
  _extract_method_from_hc011(s) {
    // format: [HC011] ... :: Box.method/arity
    if s == null { return null }
    local p = s.lastIndexOf("::")
    if p < 0 { return null }
    local qual = s.substring(p+2)
    // trim leading space
    local t = 0; while t < qual.length() { local c=qual.substring(t,t+1); if c==" "||c=="\t" { t=t+1; continue } break }
    if t > 0 { qual = qual.substring(t) }
    return qual
  }
  _parse_msg_to_diag(msg, path) {
    if msg == null { return null }
    // Expect prefixes like: [HC002] ... path:LINE or [HC011] ... :: Method
    local rule = "HC000"; local i0 = msg.indexOf("["); local i1 = msg.indexOf("]")
    if i0 == 0 && i1 > 1 { rule = msg.substring(1, i1) }
    // find last ':' as line separator
    local line = 1
    local p = msg.lastIndexOf(":")
      if p > 0 {
        // try parse after ':' as int (consume consecutive trailing digits)
        local tail = msg.substring(p+1)
        // remove leading spaces
      local q = 0; while q < tail.length() { local c=tail.substring(q,q+1); if c==" "||c=="\t" { q = q + 1  continue } break }
      local digits = ""; while q < tail.length() { local c=tail.substring(q,q+1); if c>="0" && c<="9" { digits = digits + c; q = q + 1; continue } break }
      if digits != "" { line = me._atoi(digits) }
      }
    // message: drop path and line suffix
    local message = msg
    // naive quickFix suggestions
    local qf = ""
    if rule == "HC002" { qf = "Replace include with using (alias)" }
    if rule == "HC003" { qf = "Quote module name: using \"mod\"" }
    if rule == "HC010" { qf = "Move assignment into a method (lazy init)" }
    if rule == "HC011" { qf = "Remove or reference the dead method from an entrypoint" }
    local sev = "warning"
    if rule == "HC001" || rule == "HC002" || rule == "HC010" || rule == "HC011" { sev = "error" }
    if rule == "HC003" || rule == "HC020" { sev = "warning" }
    local d = new MapBox(); d.set("file", path); d.set("line", line); d.set("rule", rule); d.set("message", message); d.set("quickFix", qf); d.set("severity", sev)
    return d
  }
  _render_dot_multi(irs) {
    // Minimal, safe DOT renderer (inline) to avoid type issues
    // Build nodes/edges from IR we just created in this CLI
    print("digraph Hako {")
    // node/edge sets
    local nodes = new MapBox()
    local edges = new MapBox()
    // collect
    if irs != null {
      local i = 0
      while i < irs.size() {
        local ir = irs.get(i)
        if ir != null {
          // methods
          local ms = ir.get("methods")
          if ms != null {
            local mi = 0
            while mi < ms.size() {
              local name = ms.get(mi)
              if name != null { nodes.set(name, 1) }
              mi = mi + 1
            }
          }
          // calls
          local cs = ir.get("calls")
          if cs != null {
            local ci = 0
            while ci < cs.size() {
              local c = cs.get(ci)
              if c != null {
                local src = c.get("from")
                local dst = c.get("to")
                if src != null && dst != null {
                  local key = src + "\t" + dst
                  edges.set(key, 1)
                }
              }
              ci = ci + 1
            }
          }
        }
        i = i + 1
      }
    }
    // print edges
    // Also print nodes to ensure isolated nodes are present (minimal)
    // edges
    // NOTE: MapBox has no key iterator; we use a simple heuristic by re‑scanning possible sources
    // For stability in this MVP, emit edges for calls we still have in IRs
    if irs != null {
      local i2 = 0
      while i2 < irs.size() {
        local ir2 = irs.get(i2)
        if ir2 != null {
          local cs2 = ir2.get("calls")
          if cs2 != null {
            local cj = 0
            while cj < cs2.size() {
              local c2 = cs2.get(cj)
              if c2 != null {
                local s = c2.get("from")
                local d = c2.get("to")
                if s != null && d != null { print("  \"" + s + "\" -> \"" + d + "\";") }
              }
              cj = cj + 1
            }
          }
        }
        i2 = i2 + 1
      }
    }
    print("}")
    return 0
  }
  _sort_diags(diags) {
    if diags == null { return new ArrayBox() }
    local out = new ArrayBox(); local i=0; while i<diags.size() { out.push(diags.get(i)); i=i+1 }
    local n = out.size(); local a=0; while a<n { local b=a+1; while b<n {
      local da = out.get(a); local db = out.get(b)
      local ka = da.get("file") + ":" + me._itoa(da.get("line"))
      local kb = db.get("file") + ":" + me._itoa(db.get("line"))
      if kb < ka { local tmp=out.get(a); out.set(a,out.get(b)); out.set(b,tmp) }
      b=b+1 } a=a+1 }
    return out
  }
  _itoa(n) {
    // assume non-negative small ints for diagnostics
    local v = 0 + n
    if v == 0 { return "0" }
    local out = ""; local digits = "0123456789"
    local tmp = ""
    while v > 0 { local d = v % 10; tmp = digits.substring(d,d+1) + tmp; v = v / 10 }
    out = tmp
    return out
  }
  _json_quote(s) {
    if s == null { return "\"\"" }
    local out = ""; local i = 0; local n = s.length()
    while i < n {
      local ch = s.substring(i,i+1)
      if ch == "\\" { out = out + "\\\\" }
      else { if ch == "\"" { out = out + "\\\"" } else { if ch == "\n" { out = out + "\\n" } else { if ch == "\r" { out = out + "\\r" } else { if ch == "\t" { out = out + "\\t" } else { out = out + ch } } } } }
      i = i + 1
    }
    return "\"" + out + "\""
  }
  _atoi(s) {
    if s == null { return 0 }
    local n = s.length(); if n == 0 { return 0 }
    local i = 0; local v = 0
    local digits = "0123456789"
    while i < n {
      local ch = s.substring(i,i+1)
      // stop at first non-digit
      if ch < "0" || ch > "9" { break }
      // map to int via indexOf
      local pos = digits.indexOf(ch)
      if pos < 0 { break }
      v = v * 10 + pos
      i = i + 1
    }
    return v
  }
  _log_stderr(msg) {
    // Log rule execution context to help diagnose VM errors
    // Note: This logs to stdout, but test framework filters it out during JSON extraction
    // In the future, this should use error() extern function for true stderr output
    print(msg)
    return 0
  }
}

// Default entry: Main.main so runner resolves without explicit --entry
static box Main { method main(args) { return HakoAnalyzerBox.run(args) } }
