package distillation

// Audit-FULL pipeline — Go port of scripts/distillation/audit_full.ts
// (Rust legacy). Runs the metric-collection passes that produce
// audit_baselines.jsonl entries. Pure observability: never modifies
// pipeline data, only reads and tallies.
//
// Phase coverage in this port:
//   - Phase 0 (file presence)            ✓ ported
//   - Phase 1 (schema validators)        ✓ ported (invokes `go test`
//                                          on internal/distillation)
//   - Phase 2 (evidence materialization) ✓ ported as observer — reads
//                                          existing data/evidence/
//                                          and tallies rows. Doesn't
//                                          re-run the materializer
//                                          (which is Rust-side); the
//                                          audit-FULL discipline is
//                                          OBSERVATION, not re-execution.
//   - Phase 3 (scored-runs distribution) ✓ ported
//   - Phase 4 (contamination firewall)   ✓ ported
//   - Phase 5 (receipts validation)      ✓ ported as observer — reads
//                                          reports/distillation/{run_id}/
//                                          summary.json + 5 stage
//                                          receipts (any-runtime artifacts).
//   - Phase 6 (acceptance gate)          ✗ skipped — TS-only fixture
//                                          harness at scripts/distillation/
//                                          acceptance.ts with bun-
//                                          specific deps. Porting the
//                                          fixtures + invariant runner
//                                          to Go is its own ADR-worth
//                                          of work; out of scope.
//   - Phase 7 (replay log shape)         ✓ ported as observer — reads
//                                          data/_kb/replay_runs.jsonl
//                                          and checks shape, doesn't
//                                          re-run replay (Rust-side
//                                          replay.ts is the producer).
//
// Output: a structured PhaseCheckReport plus a Markdown summary.
// Operators run this from cmd/audit_full to validate a Go-side
// distillation pipeline run produced sane outputs.

import (
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"sort"
	"strings"
)

// PhaseCheck is one observable check within a phase. Mirrors the
// Rust shape exactly — Markdown rendering uses the same column
// layout so cross-runtime diff'ing is meaningful.
type PhaseCheck struct {
	Phase    int      `json:"phase"`
	Name     string   `json:"name"`
	Expected string   `json:"expected"`
	Actual   string   `json:"actual"`
	Passed   bool     `json:"passed"`
	Required bool     `json:"required"` // false → informational only, doesn't fail audit
	Notes    []string `json:"notes,omitempty"`
}

// PhaseCheckReport is the aggregate result of one audit-full run.
// Metrics is the AuditBaseline-shape metric snapshot that the
// caller can pass to AppendBaseline to grow the longitudinal log.
type PhaseCheckReport struct {
	Checks  []PhaseCheck     `json:"checks"`
	Metrics map[string]int64 `json:"metrics"`
	Failed  int              `json:"failed"`           // count of REQUIRED checks that failed
	Skipped int              `json:"deferred_phases"`  // phases not yet ported
	GitHEAD string           `json:"git_head,omitempty"`
}

// AuditFullOptions controls a single audit-full run. Root is the
// data dir (defaults to LH_DISTILL_ROOT or /home/profit/lakehouse
// to keep operators running both runtimes hitting the same paths).
type AuditFullOptions struct {
	Root    string
	GitHEAD string // optional — caller resolves and passes through
	// GoTestModule is the package-pattern Phase 1 invokes via
	// `go test`. Defaults to "./internal/distillation/..." when
	// empty. Tests pass an empty path to disable the live
	// `go test` invocation (which would recurse).
	GoTestModule string
}

// RunAuditFull orchestrates the ported phases (0, 3, 4) and
// returns the aggregated report. Each phase is independent; a
// phase that errors is recorded as a failed check rather than
// aborting the run, matching Rust's "always emit a report" stance.
func RunAuditFull(opts AuditFullOptions) PhaseCheckReport {
	if opts.Root == "" {
		if env := os.Getenv("LH_DISTILL_ROOT"); env != "" {
			opts.Root = env
		} else {
			opts.Root = "/home/profit/lakehouse"
		}
	}
	report := PhaseCheckReport{
		Metrics: make(map[string]int64),
		GitHEAD: opts.GitHEAD,
		Skipped: 1, // only phase 6 (TS-only acceptance harness) deferred
	}
	auditPhase0(opts.Root, &report)
	auditPhase1(opts.Root, &report, opts.GoTestModule)
	auditPhase2(opts.Root, &report)
	auditPhase3(opts.Root, &report)
	auditPhase4(opts.Root, &report)
	auditPhase5(opts.Root, &report)
	// phase 6 intentionally skipped — see header comment
	auditPhase7(opts.Root, &report)
	for _, c := range report.Checks {
		if c.Required && !c.Passed {
			report.Failed++
		}
	}
	return report
}

// ── Phase 0: file presence ─────────────────────────────────────────

func auditPhase0(root string, report *PhaseCheckReport) {
	// The recon doc is Rust-specific (docs/recon/local-distillation-
	// recon.md); a Go-side equivalent would live in the
	// golangLAKEHOUSE repo. For audit-full's purposes, we treat its
	// presence as informational rather than required when running
	// against a non-Rust root.
	reconPath := filepath.Join(root, "docs", "recon", "local-distillation-recon.md")
	exists := fileExists(reconPath)
	report.Checks = append(report.Checks, PhaseCheck{
		Phase: 0, Name: "recon doc exists",
		Expected: "docs/recon/local-distillation-recon.md present",
		Actual:   fmt.Sprintf("%v", exists),
		Passed:   exists, Required: false, // informational on Go-side runs
	})

	tier1 := []string{
		"data/_kb/distilled_facts.jsonl",
		"data/_kb/scrum_reviews.jsonl",
		"data/_kb/audit_facts.jsonl",
		"data/_kb/mode_experiments.jsonl",
	}
	missing := []string{}
	for _, p := range tier1 {
		if !fileExists(filepath.Join(root, p)) {
			missing = append(missing, p)
		}
	}
	notes := []string{}
	if len(missing) > 0 {
		notes = append(notes, "fresh-clone or post-rotation environment — Phase 2 will tally as rows_present=false; not a hard fail")
	}
	report.Checks = append(report.Checks, PhaseCheck{
		Phase: 0, Name: "tier-1 source streams present",
		Expected: "all 4 tier-1 jsonls on disk",
		Actual: func() string {
			if len(missing) == 0 {
				return "all present"
			}
			return "missing: " + strings.Join(missing, ", ")
		}(),
		Passed: len(missing) == 0, Required: false,
		Notes: notes,
	})
}

// ── Phase 1: schema validators ─────────────────────────────────────

// auditPhase1 invokes `go test` on the distillation package — the Go
// equivalent of Rust's `bun test auditor/schemas/distillation/`. The
// audit-FULL semantic: "do the schema validators still pass on
// fixtures?" When module == "" (test mode) the phase records a
// skipped-with-rationale check rather than recursing into itself.
func auditPhase1(root string, report *PhaseCheckReport, module string) {
	if module == "" {
		// Test-disabled mode: record but don't invoke (would recurse
		// when called from a `go test` already in progress).
		report.Checks = append(report.Checks, PhaseCheck{
			Phase: 1, Name: "schema validators (skipped — test invocation disabled)",
			Expected: "go test ./internal/distillation/...",
			Actual:   "skipped",
			Passed:   true, Required: false,
			Notes: []string{"caller passed empty GoTestModule — typically because we're already inside a test run"},
		})
		return
	}
	cmd := exec.Command("go", "test", "-count=1", module)
	cmd.Dir = root // run from go module root if caller supplied it; otherwise cwd
	out, err := cmd.CombinedOutput()
	passed := err == nil
	actual := "PASS"
	if !passed {
		actual = "FAIL — " + abbrevOutput(string(out), 200)
	}
	report.Checks = append(report.Checks, PhaseCheck{
		Phase: 1, Name: "schema validators pass on fixtures",
		Expected: "go test ./internal/distillation/... → exit 0",
		Actual:   actual,
		Passed:   passed, Required: true,
	})
}

// abbrevOutput truncates noisy command-output to a stable preview.
// Long stack traces would blow out the report Markdown without this.
func abbrevOutput(s string, max int) string {
	s = strings.TrimSpace(s)
	if len(s) <= max {
		return s
	}
	return s[:max] + "...(truncated)"
}

// ── Phase 2: evidence materialization (observer) ───────────────────

// auditPhase2 reads data/evidence/ and tallies rows + skipped
// markers. Mirrors the Rust phase 2's "materializer dry-run
// completes / tier-1 sources each materialize ≥1 row" checks but
// in OBSERVER mode — doesn't re-run the materializer (which is
// Rust-side); instead reads what the Rust side already produced.
//
// Records p2_evidence_rows + p2_evidence_skips metrics that match
// the Rust shape, so a Go-side audit-full producing baselines is
// drop-in-comparable to a Rust-side run.
func auditPhase2(root string, report *PhaseCheckReport) {
	evidenceDir := filepath.Join(root, "data", "evidence")
	if !fileExists(evidenceDir) {
		report.Checks = append(report.Checks, PhaseCheck{
			Phase: 2, Name: "evidence materialization output present",
			Expected: "data/evidence/ populated",
			Actual:   "missing",
			Passed:   false, Required: true,
			Notes: []string{"run materializer (Rust: ./scripts/distill collect; Go-side materializer not yet ported) before audit-full"},
		})
		return
	}
	rows := int64(0)
	skips := int64(0)
	bySource := map[string]int64{}
	tier1Hits := map[string]bool{
		"distilled_facts":  false,
		"scrum_reviews":    false,
		"audit_facts":      false,
		"mode_experiments": false,
	}

	walkErr := filepath.Walk(evidenceDir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return nil
		}
		if info.IsDir() || !strings.HasSuffix(path, ".jsonl") {
			return nil
		}
		data, err := os.ReadFile(path)
		if err != nil {
			return nil
		}
		// Tally per-source via the ev.provenance.source_file field on
		// each evidence row. Match Rust's "by_source" map shape.
		for _, line := range strings.Split(string(data), "\n") {
			line = strings.TrimSpace(line)
			if line == "" {
				continue
			}
			rows++
			var rec struct {
				Provenance struct {
					SourceFile string `json:"source_file"`
				} `json:"provenance"`
				SuccessMarkers []string `json:"success_markers,omitempty"`
				FailureMarkers []string `json:"failure_markers,omitempty"`
			}
			if err := json.Unmarshal([]byte(line), &rec); err != nil {
				skips++
				continue
			}
			stem := stemFromSourceFile(rec.Provenance.SourceFile)
			bySource[stem]++
			if _, ok := tier1Hits[stem]; ok {
				tier1Hits[stem] = true
			}
		}
		return nil
	})
	if walkErr != nil {
		report.Checks = append(report.Checks, PhaseCheck{
			Phase: 2, Name: "evidence walk",
			Expected: "no error", Actual: walkErr.Error(),
			Passed: false, Required: true,
		})
		return
	}

	report.Metrics["p2_evidence_rows"] = rows
	report.Metrics["p2_evidence_skips"] = skips

	report.Checks = append(report.Checks, PhaseCheck{
		Phase: 2, Name: "evidence materialization output non-empty",
		Expected: ">=1 row across all sources",
		Actual:   fmt.Sprintf("%d rows · %d skipped", rows, skips),
		Passed:   rows >= 1, Required: true,
	})

	tier1Found := []string{}
	for src, hit := range tier1Hits {
		if hit {
			tier1Found = append(tier1Found, src)
		}
	}
	sort.Strings(tier1Found)
	notes := []string{}
	if len(tier1Found) < 4 {
		notes = append(notes, "fresh-environment OK; expect lower count when source streams are absent")
	}
	report.Checks = append(report.Checks, PhaseCheck{
		Phase: 2, Name: "tier-1 sources each materialize ≥1 row",
		Expected: "4/4: distilled_facts, scrum_reviews, audit_facts, mode_experiments",
		Actual:   fmt.Sprintf("%d/4 hit (%s)", len(tier1Found), strings.Join(tier1Found, ", ")),
		Passed:   len(tier1Found) >= 1, Required: false,
		Notes: notes,
	})
}

// ── Phase 3: scored-runs distribution ──────────────────────────────

func auditPhase3(root string, report *PhaseCheckReport) {
	scoredDir := filepath.Join(root, "data", "scored-runs")
	if !fileExists(scoredDir) {
		report.Checks = append(report.Checks, PhaseCheck{
			Phase: 3, Name: "scored-runs on disk",
			Expected: "data/scored-runs/ populated",
			Actual:   "missing",
			Passed:   false, Required: true,
			Notes: []string{"run scoring before audit-full (Go: scripts/distillation/score; Rust: ./scripts/distill score)"},
		})
		return
	}

	counts := map[string]int64{
		"accepted":           0,
		"partially_accepted": 0,
		"rejected":           0,
		"needs_human_review": 0,
	}
	files, err := ListScoredRunFiles(root)
	if err != nil {
		report.Checks = append(report.Checks, PhaseCheck{
			Phase: 3, Name: "scored-runs walk",
			Expected: "no error", Actual: err.Error(),
			Passed: false, Required: true,
		})
		return
	}
	for _, f := range files {
		runs, _, err := LoadScoredRunsFromFile(f)
		if err != nil {
			continue
		}
		for _, r := range runs {
			if _, ok := counts[string(r.Category)]; ok {
				counts[string(r.Category)]++
			}
		}
	}
	total := counts["accepted"] + counts["partially_accepted"] + counts["rejected"] + counts["needs_human_review"]

	report.Metrics["p3_accepted"] = counts["accepted"]
	report.Metrics["p3_partial"] = counts["partially_accepted"]
	report.Metrics["p3_rejected"] = counts["rejected"]
	report.Metrics["p3_human"] = counts["needs_human_review"]

	report.Checks = append(report.Checks, PhaseCheck{
		Phase: 3, Name: "on-disk scored-runs distribution non-empty",
		Expected: ">=1 accepted",
		Actual:   fmt.Sprintf("acc=%d part=%d rej=%d hum=%d", counts["accepted"], counts["partially_accepted"], counts["rejected"], counts["needs_human_review"]),
		Passed:   counts["accepted"] >= 1, Required: true,
	})
	report.Checks = append(report.Checks, PhaseCheck{
		Phase: 3, Name: "scored-runs distribution sums positive",
		Expected: ">0 total", Actual: fmt.Sprintf("%d total", total),
		Passed: total > 0, Required: false,
	})
}

// ── Phase 4: contamination firewall + provenance ───────────────────

// sigHashRe pre-compiled match for the canonical sig_hash shape:
// 64 lowercase hex characters (sha256 hex). Used per-row in the
// provenance check.
var sigHashRe = regexp.MustCompile(`^[0-9a-f]{64}$`)

func auditPhase4(root string, report *PhaseCheckReport) {
	sftPath := filepath.Join(root, "exports", "sft", "instruction_response.jsonl")
	ragPath := filepath.Join(root, "exports", "rag", "playbooks.jsonl")
	prefPath := filepath.Join(root, "exports", "preference", "chosen_rejected.jsonl")

	sftRows := readJSONLLines(sftPath)
	ragRows := readJSONLLines(ragPath)
	prefRows := readJSONLLines(prefPath)

	report.Metrics["p4_sft_rows"] = int64(len(sftRows))
	report.Metrics["p4_rag_rows"] = int64(len(ragRows))
	report.Metrics["p4_pref_pairs"] = int64(len(prefRows))

	// SFT contamination firewall: 0 forbidden quality_scores. The
	// only legal SFT quality scores are accepted + partially_accepted.
	sftForbidden := 0
	for _, line := range sftRows {
		var r struct {
			QualityScore string `json:"quality_score"`
		}
		if err := json.Unmarshal([]byte(line), &r); err != nil {
			continue // tolerate malformed (matches Rust)
		}
		if r.QualityScore != "accepted" && r.QualityScore != "partially_accepted" {
			sftForbidden++
		}
	}
	report.Checks = append(report.Checks, PhaseCheck{
		Phase: 4, Name: "SFT contamination firewall: 0 forbidden quality_scores",
		Expected: "0", Actual: fmt.Sprintf("%d", sftForbidden),
		Passed: sftForbidden == 0, Required: true,
		Notes: []string{"this is the spec non-negotiable — rejected/needs_human_review must NEVER appear in SFT"},
	})

	// RAG firewall: 0 rejected leaks
	ragRejected := 0
	for _, line := range ragRows {
		var r struct {
			SuccessScore string `json:"success_score"`
		}
		if err := json.Unmarshal([]byte(line), &r); err != nil {
			continue
		}
		if r.SuccessScore == "rejected" {
			ragRejected++
		}
	}
	report.Checks = append(report.Checks, PhaseCheck{
		Phase: 4, Name: "RAG firewall: 0 rejected leaks",
		Expected: "0", Actual: fmt.Sprintf("%d", ragRejected),
		Passed: ragRejected == 0, Required: true,
	})

	// Preference: 0 self-pairs + 0 identical-text pairs.
	prefSelfPairs, prefIdenticalText := 0, 0
	for _, line := range prefRows {
		var r struct {
			ChosenRunID   string `json:"chosen_run_id"`
			RejectedRunID string `json:"rejected_run_id"`
			Chosen        string `json:"chosen"`
			Rejected      string `json:"rejected"`
		}
		if err := json.Unmarshal([]byte(line), &r); err != nil {
			continue
		}
		if r.ChosenRunID == r.RejectedRunID {
			prefSelfPairs++
		}
		if r.Chosen == r.Rejected {
			prefIdenticalText++
		}
	}
	report.Checks = append(report.Checks, PhaseCheck{
		Phase: 4, Name: "Preference: 0 self-pairs (chosen_run_id != rejected_run_id)",
		Expected: "0", Actual: fmt.Sprintf("%d", prefSelfPairs),
		Passed: prefSelfPairs == 0, Required: true,
	})
	report.Checks = append(report.Checks, PhaseCheck{
		Phase: 4, Name: "Preference: 0 identical-text pairs",
		Expected: "0", Actual: fmt.Sprintf("%d", prefIdenticalText),
		Passed: prefIdenticalText == 0, Required: true,
	})

	// Provenance check: every export row must carry a 64-char hex
	// sig_hash. Walks sft + rag + pref together since the contract
	// is uniform across all three.
	noProv := 0
	checkProv := func(line string) {
		var r struct {
			Provenance struct {
				SigHash string `json:"sig_hash"`
			} `json:"provenance"`
		}
		if err := json.Unmarshal([]byte(line), &r); err != nil {
			return
		}
		if r.Provenance.SigHash == "" || !sigHashRe.MatchString(r.Provenance.SigHash) {
			noProv++
		}
	}
	for _, line := range sftRows {
		checkProv(line)
	}
	for _, line := range ragRows {
		checkProv(line)
	}
	for _, line := range prefRows {
		checkProv(line)
	}
	report.Checks = append(report.Checks, PhaseCheck{
		Phase: 4, Name: "every export row carries valid sha256 provenance.sig_hash",
		Expected: "0 missing", Actual: fmt.Sprintf("%d missing", noProv),
		Passed: noProv == 0, Required: true,
	})

	// Quarantine totals (informational — feeds the p4_total_quarantined
	// metric used by the longitudinal drift signal).
	totalQuar := int64(0)
	for _, qp := range []string{
		"exports/quarantine/sft.jsonl",
		"exports/quarantine/rag.jsonl",
		"exports/quarantine/preference.jsonl",
	} {
		totalQuar += int64(len(readJSONLLines(filepath.Join(root, qp))))
	}
	report.Metrics["p4_total_quarantined"] = totalQuar
}

// ── Phase 5: receipts validation (observer) ────────────────────────

// runSummaryShape mirrors the Rust RunSummary just enough to
// validate the file's shape — schema_version, run_hash sha256,
// git_commit hex, and the 5 stage names. Full schema validation
// is intentionally NOT ported (it would require porting the
// schemas/distillation/ TS validators); we check the load-bearing
// invariants and call it good.
type runSummaryShape struct {
	SchemaVersion int    `json:"schema_version"`
	RunID         string `json:"run_id"`
	GitCommit     string `json:"git_commit"`
	RunHash       string `json:"run_hash"`
	Stages        []struct {
		Stage string `json:"stage"`
	} `json:"stages"`
}

func auditPhase5(root string, report *PhaseCheckReport) {
	reportsDir := filepath.Join(root, "reports", "distillation")
	if !fileExists(reportsDir) {
		report.Checks = append(report.Checks, PhaseCheck{
			Phase: 5, Name: "receipts directory exists",
			Expected: "reports/distillation/", Actual: "MISSING",
			Passed: false, Required: true,
		})
		return
	}
	// Find the most recent run_id directory with a summary.json.
	// Mirrors the Rust mtime-sort behavior — ordering matters when
	// both Rust + Go runs land in the same directory.
	type cand struct {
		id    string
		mtime int64
	}
	var cands []cand
	entries, err := os.ReadDir(reportsDir)
	if err != nil {
		report.Checks = append(report.Checks, PhaseCheck{
			Phase: 5, Name: "scan reports/distillation",
			Expected: "no error", Actual: err.Error(),
			Passed: false, Required: true,
		})
		return
	}
	for _, e := range entries {
		if !e.IsDir() {
			continue
		}
		sumPath := filepath.Join(reportsDir, e.Name(), "summary.json")
		st, err := os.Stat(sumPath)
		if err != nil {
			continue
		}
		cands = append(cands, cand{id: e.Name(), mtime: st.ModTime().UnixMilli()})
	}
	if len(cands) == 0 {
		report.Checks = append(report.Checks, PhaseCheck{
			Phase: 5, Name: "≥1 run with summary.json",
			Expected: "≥1", Actual: "0",
			Passed: false, Required: false,
			Notes: []string{"no Phase 5 run-all has executed yet — Rust: ./scripts/distill run-all"},
		})
		return
	}
	sort.Slice(cands, func(i, j int) bool { return cands[i].mtime > cands[j].mtime })
	latest := cands[0]
	runDir := filepath.Join(reportsDir, latest.id)

	// All 5 stage receipts present.
	expected := []string{"collect", "score", "export-rag", "export-sft", "export-preference"}
	missing := []string{}
	for _, s := range expected {
		if !fileExists(filepath.Join(runDir, s+".json")) {
			missing = append(missing, s)
		}
	}
	report.Checks = append(report.Checks, PhaseCheck{
		Phase: 5, Name: fmt.Sprintf("latest run (%s) has all 5 stage receipts", latest.id),
		Expected: strings.Join(expected, ","),
		Actual: func() string {
			if len(missing) == 0 {
				return "all present"
			}
			return "missing: " + strings.Join(missing, ",")
		}(),
		Passed: len(missing) == 0, Required: true,
	})

	// Each receipt parses as JSON. Full schema validation (StageReceipt
	// schema) is Rust-side only; we check basic decodability here.
	invalid := 0
	for _, s := range expected {
		path := filepath.Join(runDir, s+".json")
		data, err := os.ReadFile(path)
		if err != nil {
			continue
		}
		var anyShape any
		if err := json.Unmarshal(data, &anyShape); err != nil {
			invalid++
		}
	}
	report.Checks = append(report.Checks, PhaseCheck{
		Phase: 5, Name: "every stage receipt parses as JSON",
		Expected: "0 invalid", Actual: fmt.Sprintf("%d invalid", invalid),
		Passed: invalid == 0, Required: true,
	})

	// RunSummary shape: schema_version=1, run_hash sha256, git_commit
	// 40-char hex.
	summaryPath := filepath.Join(runDir, "summary.json")
	data, err := os.ReadFile(summaryPath)
	if err != nil {
		report.Checks = append(report.Checks, PhaseCheck{
			Phase: 5, Name: "summary.json readable",
			Expected: "ok", Actual: err.Error(),
			Passed: false, Required: true,
		})
		return
	}
	var sum runSummaryShape
	if err := json.Unmarshal(data, &sum); err != nil {
		report.Checks = append(report.Checks, PhaseCheck{
			Phase: 5, Name: "summary.json decodable",
			Expected: "ok", Actual: err.Error(),
			Passed: false, Required: true,
		})
		return
	}
	report.Checks = append(report.Checks, PhaseCheck{
		Phase: 5, Name: "summary.schema_version == 1",
		Expected: "1", Actual: fmt.Sprintf("%d", sum.SchemaVersion),
		Passed: sum.SchemaVersion == 1, Required: true,
	})
	gitHEADRe := regexp.MustCompile(`^[0-9a-f]{40}$`)
	report.Checks = append(report.Checks, PhaseCheck{
		Phase: 5, Name: "summary.git_commit is 40-char hex",
		Expected: "/^[0-9a-f]{40}$/", Actual: shortHash(sum.GitCommit),
		Passed: gitHEADRe.MatchString(sum.GitCommit), Required: false,
	})
	report.Checks = append(report.Checks, PhaseCheck{
		Phase: 5, Name: "run_hash is sha256",
		Expected: "/^[0-9a-f]{64}$/", Actual: shortHash(sum.RunHash),
		Passed: sigHashRe.MatchString(sum.RunHash), Required: true,
	})
}

func shortHash(h string) string {
	if len(h) <= 16 {
		return h
	}
	return h[:16] + "..."
}

// ── Phase 7: replay log shape (observer) ───────────────────────────

// auditPhase7 checks data/_kb/replay_runs.jsonl exists and contains
// well-shaped records. Mirrors Rust phase 7's "persisted log shape"
// check but skips the live-replay invocation (which would require
// porting Rust replay.ts, a substantial effort). The full Rust
// phase 7 also runs 3 dry-run replays — operators wanting that
// signal continue to invoke the Rust audit-full.
func auditPhase7(root string, report *PhaseCheckReport) {
	logPath := filepath.Join(root, "data", "_kb", "replay_runs.jsonl")
	lines := readJSONLLines(logPath)
	report.Checks = append(report.Checks, PhaseCheck{
		Phase: 7, Name: "replay_runs.jsonl exists",
		Expected: "exists with ≥1 row",
		Actual: func() string {
			if !fileExists(logPath) {
				return "missing"
			}
			return fmt.Sprintf("%d rows total", len(lines))
		}(),
		Passed: fileExists(logPath), Required: false,
	})
	if !fileExists(logPath) {
		return
	}
	// Validate shape on a sample of rows — full validation across
	// thousands of lines isn't worth the cost, and a structural
	// problem will show up in any sample.
	sample := lines
	if len(sample) > 50 {
		sample = sample[len(sample)-50:]
	}
	malformed := 0
	for _, line := range sample {
		var anyShape any
		if err := json.Unmarshal([]byte(line), &anyShape); err != nil {
			malformed++
		}
	}
	report.Checks = append(report.Checks, PhaseCheck{
		Phase: 7, Name: "replay_runs.jsonl tail rows parse as JSON",
		Expected: "0 malformed in last 50", Actual: fmt.Sprintf("%d malformed", malformed),
		Passed: malformed == 0, Required: true,
	})
}

// ── helpers ────────────────────────────────────────────────────────

func fileExists(p string) bool {
	_, err := os.Stat(p)
	return err == nil
}

// readJSONLLines reads a JSONL file and returns non-empty lines.
// Returns nil on missing file (matches Rust's existsSync ? read : []).
func readJSONLLines(path string) []string {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil
	}
	out := make([]string, 0)
	for _, line := range strings.Split(string(data), "\n") {
		if strings.TrimSpace(line) != "" {
			out = append(out, line)
		}
	}
	return out
}

// FormatAuditFullReport renders a Markdown report mirroring the
// Rust phase8-full-audit-report.md shape so operators reading
// across runtimes don't have to re-learn the layout.
func FormatAuditFullReport(report PhaseCheckReport) string {
	var b strings.Builder
	fmt.Fprintln(&b, "# Audit-FULL report (Go)")
	fmt.Fprintln(&b)
	if report.GitHEAD != "" {
		fmt.Fprintf(&b, "**git HEAD:** `%s`\n\n", report.GitHEAD)
	}
	failed := report.Failed
	total := 0
	for _, c := range report.Checks {
		if c.Required {
			total++
		}
	}
	verdict := "PASS"
	if failed > 0 {
		verdict = "FAIL"
	}
	fmt.Fprintf(&b, "**Verdict:** %s — %d/%d required checks passed; %d phase(s) deferred.\n\n",
		verdict, total-failed, total, report.Skipped)

	fmt.Fprintln(&b, "## Checks")
	fmt.Fprintln(&b)
	fmt.Fprintln(&b, "| phase | name | expected | actual | required | passed |")
	fmt.Fprintln(&b, "|---|---|---|---|---|---|")
	for _, c := range report.Checks {
		req := "no"
		if c.Required {
			req = "**yes**"
		}
		passed := "✗"
		if c.Passed {
			passed = "✓"
		}
		fmt.Fprintf(&b, "| %d | %s | %s | %s | %s | %s |\n",
			c.Phase, c.Name, c.Expected, c.Actual, req, passed)
		for _, n := range c.Notes {
			fmt.Fprintf(&b, "| | _note_ | %s | | | |\n", n)
		}
	}

	if len(report.Metrics) > 0 {
		fmt.Fprintln(&b)
		fmt.Fprintln(&b, "## Metrics")
		fmt.Fprintln(&b)
		fmt.Fprintln(&b, "| metric | value |")
		fmt.Fprintln(&b, "|---|---:|")
		// Stable order for diffs.
		names := make([]string, 0, len(report.Metrics))
		for k := range report.Metrics {
			names = append(names, k)
		}
		sort.Strings(names)
		for _, k := range names {
			fmt.Fprintf(&b, "| %s | %d |\n", k, report.Metrics[k])
		}
	}
	return b.String()
}
