Add persistence scaffolding

This commit is contained in:
Meta Vens 2026-04-26 04:53:29 +01:00
commit e66aceb372
8 changed files with 849 additions and 0 deletions

114
AGENTS.md Normal file
View file

@ -0,0 +1,114 @@
# Repixelizer Instructions
## Project Purpose
Repixelizer is a standalone Python CLI for forcing fake pixel art onto a real
pixel lattice without turning the solver into a landfill of compensating
heuristics.
The live machine is intentionally lean: infer a lattice, analyze structure,
optimize one displacement field, sample real source pixels, then write
diagnostics good enough to catch when we are lying to ourselves.
## Canonical State
- Treat `state/map.yaml` as the canonical current project map.
- Treat `state/scratch.md` as disposable working memory for one bounded subgoal.
- Treat `state/evidence.jsonl` as the durable distilled ledger of decisions,
regressions, verified findings, and rejected paths.
- Treat `state/branches.json` as hypothesis tracking, not phase/status prose.
- Treat `notes/fresh-workspace-handoff.md` as the compact re-entry packet.
- Treat `docs/implementation-plan.md` as the forward plan.
- Treat `docs/lean-optimizer-algorithm-map.md` as the source-grounded live
algorithm map.
- Update `state/map.yaml` when project understanding changes.
- Add evidence after meaningful research, implementation, verification, or
rejected paths, but keep it distilled. Routine "I just did this" proof belongs
in git history, diagnostics, benchmark artifacts, or commit messages unless it
changes what the next agent should believe.
## Important Paths
- Project root: `E:\Projects\repixelizer`
- Implementation plan: `E:\Projects\repixelizer\docs\implementation-plan.md`
- Algorithm map: `E:\Projects\repixelizer\docs\lean-optimizer-algorithm-map.md`
- Handoff summary: `E:\Projects\repixelizer\notes\fresh-workspace-handoff.md`
- State CLI: `E:\Projects\repixelizer\tools\repixelizer_state.py`
- Pre-compaction helper: `E:\Projects\repixelizer\tools\repixelizer_prepare_compaction.py`
- Live pipeline: `E:\Projects\repixelizer\src\repixelizer\pipeline.py`
- Live solver: `E:\Projects\repixelizer\src\repixelizer\phase_field.py`
- Metrics: `E:\Projects\repixelizer\src\repixelizer\metrics.py`
## Useful Commands
Use the repo virtualenv when available:
```powershell
.\.venv\Scripts\python .\tools\repixelizer_state.py status
.\.venv\Scripts\python .\tools\repixelizer_state.py add-evidence --type research --status ok --note "..."
.\.venv\Scripts\python .\tools\repixelizer_prepare_compaction.py
.\.venv\Scripts\python -m pytest -q
```
Fallback if the virtualenv is unavailable:
```powershell
python .\tools\repixelizer_state.py status
python .\tools\repixelizer_prepare_compaction.py
```
## Session Bootstrap And Re-entry Protocol
On fresh session load, do this before editing:
1. read:
- `state/map.yaml`
- `notes/fresh-workspace-handoff.md`
- `docs/lean-optimizer-algorithm-map.md`
- `docs/implementation-plan.md`
2. run:
- `.\.venv\Scripts\python .\tools\repixelizer_state.py status`
- `git status --short --branch`
- `git log --oneline -5`
3. restate the current next action from persisted state before touching code
After compaction, resume, or suspicious continuity loss:
1. rerun `repixelizer_state.py status`
2. reread `state/map.yaml` and `notes/fresh-workspace-handoff.md`
3. treat the persisted next action as authoritative unless fresh repo evidence
contradicts it
When context pressure is rising:
1. stop broad exploration
2. narrow the active move to one bounded landing zone
3. persist map/handoff updates plus distilled evidence before continuity gets
atomized
Do not wait for the blackout and then act surprised.
When the user says to prepare for imminent compaction:
1. run `tools/repixelizer_prepare_compaction.py` before editing persistence surfaces
2. use its warnings as the checklist for map, handoff, scratch, evidence, and git hygiene
3. update only the state that actually changed
4. run `tools/repixelizer_prepare_compaction.py` again after edits
5. fix errors, address warnings, and commit the completed persistence pass unless the work is deliberately mid-surgery
## Operating Discipline
- Before substantial edits, restate the current mechanism and intended change.
- Prefer one clear hypothesis per pass.
- Validate against the real objective: pinned badge runs, focused crops, visual
diffs, and `source_structure`, not just a convenient proxy metric.
- Revert failed regression or benchmark fixes immediately before trying the next
idea.
- If the diff grows while understanding shrinks, stop implementation and switch
to diagnosis, mapping, or simplification.
- Keep prose and maps grounded in concrete source paths.
- Do not reintroduce trays, portrait layers, candidate sets, or multi-stage
solver religions unless the design has actually changed and the map says so.
- Before handoff or compaction, sync `state/map.yaml`, refresh
`notes/fresh-workspace-handoff.md`, add distilled evidence when future belief
changed, and make the next action explicit.

View file

@ -0,0 +1,111 @@
# Fresh Workspace Handoff
This is the re-entry packet for `E:\Projects\repixelizer`.
It is intentionally short. Historical proof belongs in git history,
diagnostics, benchmark artifacts, and the distilled `state/evidence.jsonl`
ledger; exact control flow belongs in `docs/lean-optimizer-algorithm-map.md`;
forward planning belongs in `docs/implementation-plan.md`.
## Rehydrate
From the repo root:
```powershell
.\.venv\Scripts\python .\tools\repixelizer_state.py status
Get-Content '.\state\map.yaml'
Get-Content '.\notes\fresh-workspace-handoff.md'
Get-Content '.\docs\lean-optimizer-algorithm-map.md'
Get-Content '.\docs\implementation-plan.md'
git status --short --branch
git log --oneline -5
Get-Content '.\state\evidence.jsonl' -Tail 8
```
Do not trust this file for the exact live HEAD. Always check git.
## Current Orientation
- `phase-field` is the only live reconstruction engine.
- The live pipeline is `source -> lattice inference -> edge analysis -> phase-field reconstruction -> cleanup -> optional palette fit -> diagnostics`.
- The low-confidence phase rerank path is a short preview solve, not a second full optimizer.
- Cleanup is secondary and usually a no-op; the core result is supposed to come from the solver, not cleanup cosplay.
- The current tracked weakness is the widened dark contour near the badge sword tip on tapered shapes.
- `source_structure` exists because lattice-only `source_fidelity` could call visibly better outputs worse.
- The old tray optimizer is dead and should stay dead unless the entire machine map changes for a real reason.
## Critical Doctrine
- Persistent state is the agent's mind.
- Cut persistent memory as ruthlessly as code; stale context is bad thought, not harmless clutter.
- Remember Jenga: growing diffs, growing notes, and growing confidence are not proof that the system still makes sense.
- If compaction hits while source gathering or slice planning is still unpersisted, that work is gone. Re-gather it instead of pretending continuity happened.
- Keep the machine explainable in plain language and anchored to code. If a change cannot be explained against the algorithm map, it is not ready to land.
## Landed Machine
The current spine:
- lattice inference with fixed-size and searched-size paths
- low-confidence phase rerank through short preview reconstruction
- edge analysis feeding one projected displacement-field optimizer
- nearest-source final sampling from `uv0_px + disp_t`
- cleanup, optional palette fit, diagnostics, compare mode, tuning, and GUI observer events on the live path
- `source_structure` plus `source_fidelity` in run summaries and comparisons
- focused sword-tip fixture in `tests/fixtures/real/ai-badge-tip-focus.json`
The exact current control flow is documented in
`docs/lean-optimizer-algorithm-map.md`.
## Boundaries
- Do not reintroduce tray optimizers, candidate sets, portrait layers, or multi-stage solver religions casually.
- Do not trust a metric win that makes the image look worse.
- Do not let cleanup become the real solver.
- Do not let `state/evidence.jsonl` turn into an activity feed.
- Do not restart broad exploratory surgery when the current weak spot is still one bounded tapered-contour blemish.
## Verification Guardrails
- For scaffolding or note changes, run the repo-local state helper and compaction helper.
- For Python changes, run:
```powershell
.\.venv\Scripts\python -m pytest -q
```
- For solver behavior changes, reproduce the pinned badge case from
`docs/implementation-plan.md`, inspect the sword-tip focus fixture, and
compare both the image and `source_structure`.
## Persistent State Hygiene
Rules now in force:
- `state/map.yaml` is canonical current truth.
- `state/scratch.md` is disposable scratch.
- `state/evidence.jsonl` is a distilled durable belief ledger.
- `tools/repixelizer_prepare_compaction.py` is the pre-compaction persistence check; run it before and after imminent-compaction persistence passes.
- this handoff is a compact re-entry packet.
- `docs/implementation-plan.md` is the forward plan.
- `docs/lean-optimizer-algorithm-map.md` is the source-grounded control-flow map.
Do not let any one note become all of those things. That is how the tower grows
sideways and starts calling itself architecture.
## Next Real Move
Do not continue implementation automatically from a rehydrate-only request.
If the user asks to continue, the current next move is to take one bounded
hypothesis for tapered-contour behavior in `src/repixelizer/phase_field.py`,
check it against the pinned badge case and
`tests/fixtures/real/ai-badge-tip-focus.json`, and revert it if the visual
result or `source_structure` does not clearly improve.
## Immediate Re-entry Instruction
After compaction, first rehydrate and reorient from the listed files and git
state. Do not continue implementation merely because the state names a next
move. Wait for the user's next instruction unless they explicitly say to
continue.

17
state/branches.json Normal file
View file

@ -0,0 +1,17 @@
{
"branches": [
{
"id": "main",
"hypothesis": "A lean phase-field solver plus structure-aware evaluation can repixelize fake pixel art better than resize baselines without reviving optimizer sprawl.",
"status": "active",
"artifacts": [
"state/map.yaml",
"state/scratch.md",
"state/evidence.jsonl",
"docs/implementation-plan.md",
"docs/lean-optimizer-algorithm-map.md"
],
"notes": "Active mainline. Current solver work should stay anchored to the pinned badge case and the sword-tip focus fixture."
}
]
}

3
state/evidence.jsonl Normal file
View file

@ -0,0 +1,3 @@
{"ts":"2026-04-26T00:00:00Z","type":"design","status":"accepted","note":"Repixelizer now has one live reconstruction engine: phase-field in src/repixelizer/phase_field.py. The old tray-based optimizer is gone."}
{"ts":"2026-04-26T00:00:00Z","type":"metric","status":"accepted","note":"source_structure is tracked alongside source_fidelity because lattice-only scoring could rank visibly better outputs as worse."}
{"ts":"2026-04-26T00:00:00Z","type":"regression","status":"rejected","note":"The d9fa411 phase-field tuning pass was a real regression on the pinned badge case; reverting it restores the known-good badge result exactly."}

90
state/map.yaml Normal file
View file

@ -0,0 +1,90 @@
project:
name: Repixelizer
status: active
phase: phase_field_stabilization
premise: >
Recover fake pixel art by inferring a real lattice and moving one
displacement field under one coherent objective instead of piling up
decorative solver machinery.
objective:
primary: >
Produce true grid-aligned output that preserves silhouette and internal
structure better than resize baselines on icons, emblems, logos, and simple
sprites.
current: >
Keep the phase-field pipeline lean and coherent while improving the tracked
sword-tip contour failure near tapered shapes without reviving optimizer
sprawl.
constraints:
- Prefer one field, one compact objective, and nearest-source final sampling.
- Use visible structure and focused fixtures, not just lattice proxy metrics.
- Keep compare mode, diagnostics, tuning, and GUI observer surfaces working.
- Avoid reviving trays, portrait layers, or multi-stage solver religions.
invariants:
- The map is canonical and slow-changing.
- Scratch is temporary and disposable.
- Evidence is durable and distilled; routine activity logs belong in git history, diagnostics, benchmark artifacts, or commit messages.
- The live pipeline is `source -> lattice inference -> edge analysis -> phase-field reconstruction -> cleanup -> optional palette fit -> diagnostics`.
- `phase-field` is the only live reconstruction engine.
- The final image is emitted by nearest-source sampling from `uv0_px + disp_t`, not by bilinear output sampling.
- If a change cannot be explained cleanly against `docs/lean-optimizer-algorithm-map.md`, it should not land.
- Failed benchmark or regression fixes should be reverted before the next hypothesis.
- Persistent state cruft is the same failure class as code cruft.
data_flow:
observe:
- task request
- repository state
- focused fixtures and diagnostics
- algorithm map and implementation plan
process:
- rehydrate from canonical state
- choose one bounded hypothesis
- modify only the surface that deserves to exist
- verify visually and numerically
- distill belief-changing evidence
- commit or backtrack
cut:
- revert failed code hypotheses immediately
- reject proxy-only wins when the image got worse
- trim duplicate or stale persistent notes instead of preserving them as fake continuity
active_subgoals:
- Fix the tracked sword-tip contour widening without sacrificing the current internal linework win.
- Improve along-stroke versus across-stroke behavior near tapered contours in a way that still fits the one-field optimizer model.
- Keep inference, compare mode, diagnostics, tuning, and GUI observer events working after solver changes.
- Preserve `source_structure` as a real objective signal instead of over-trusting lattice-only fidelity.
accepted_design:
- `phase-field` in `src/repixelizer/phase_field.py` is the only live reconstruction engine.
- The working pipeline is `source image -> lattice inference -> edge analysis -> phase-field reconstruction -> cleanup -> optional palette fit -> diagnostics`.
- `src/repixelizer/pipeline.py` owns orchestration and phase rerank selection.
- `src/repixelizer/analysis.py` provides the normalized edge scout used for loss and cleanup guidance.
- `src/repixelizer/phase_field.py` owns the one honest optimization variable: `disp_t`.
- The optimizer starts from fixed lattice centers, optimizes displacement with projected Adam, then rounds to real source pixels for final output.
- Cleanup is local and secondary; it does not define the core reconstruction model.
- `source_fidelity` is useful but incomplete; `source_structure` exists because visible structure can improve while lattice-only scoring complains.
- The tracked badge fixture and sword-tip focus are the current reality check for solver changes.
- The algorithm map and forward plan must stay separate from scratch and evidence so the notes do not congeal into one giant lying blob.
landed_baseline:
- Lattice inference with fixed or searched target dimensions and optional low-confidence phase rerank.
- Edge analysis plus one displacement-field optimizer with projected constraints.
- Compare mode, diagnostics writing, tuning, and GUI observer plumbing all working on the phase-field path.
- `source_structure` reporting alongside `source_fidelity`.
- Focus fixture `tests/fixtures/real/ai-badge-tip-focus.json` for the tracked tapered-contour blemish.
rejected_paths:
- name: tray_optimizer_sprawl
reason: the old tray-based optimizer added machinery without preserving the current lean model
- name: metric_worship
reason: lattice-only fidelity can call visibly better outputs worse
- name: multi_stage_solver_religion
reason: stacked stages and compensators hide confusion instead of fixing the one-field model
current_status:
summary: The repo has one live reconstruction engine, `phase-field`, wired through the main pipeline with lattice inference, edge analysis, projected displacement optimization, cleanup, optional palette fitting, diagnostics, compare mode, tuning, and GUI observer events. `source_structure` now rides alongside `source_fidelity` because lattice-only scoring was lying about visible quality. The tracked weakness is still the widened dark contour near the badge sword tip on tapered shapes.
next_action: After rehydrate, if the user wants continued solver work, take one bounded hypothesis aimed at along-stroke versus across-stroke behavior near tapered contours in `src/repixelizer/phase_field.py`, validate it against the pinned badge case and `tests/fixtures/real/ai-badge-tip-focus.json`, and revert immediately if the visual result or `source_structure` does not clearly improve.

7
state/scratch.md Normal file
View file

@ -0,0 +1,7 @@
# Scratch
This file is disposable working memory for the current bounded subgoal.
## Current Subgoal
No active scratch subgoal.

View file

@ -0,0 +1,328 @@
from __future__ import annotations
import argparse
import json
import re
import subprocess
from dataclasses import dataclass
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
STATE_DIR = ROOT / "state"
NOTES_DIR = ROOT / "notes"
DOCS_DIR = ROOT / "docs"
MAP_PATH = STATE_DIR / "map.yaml"
SCRATCH_PATH = STATE_DIR / "scratch.md"
BRANCHES_PATH = STATE_DIR / "branches.json"
EVIDENCE_PATH = STATE_DIR / "evidence.jsonl"
HANDOFF_PATH = NOTES_DIR / "fresh-workspace-handoff.md"
PLAN_PATH = DOCS_DIR / "implementation-plan.md"
ALGO_MAP_PATH = DOCS_DIR / "lean-optimizer-algorithm-map.md"
AGENTS_PATH = ROOT / "AGENTS.md"
@dataclass
class Finding:
level: str
message: str
def read_text(path: Path) -> str:
return path.read_text(encoding="utf-8")
def run_git(args: list[str]) -> str:
completed = subprocess.run(
["git", *args],
cwd=ROOT,
check=True,
capture_output=True,
text=True,
)
return completed.stdout.strip()
def extract_map_field(name: str) -> str | None:
prefix = f" {name}:"
for line in read_text(MAP_PATH).splitlines():
if line.startswith(prefix):
return line.split(":", 1)[1].strip()
return None
def extract_active_subgoals() -> list[str]:
lines = read_text(MAP_PATH).splitlines()
results: list[str] = []
in_section = False
for line in lines:
if not in_section:
if line.strip() == "active_subgoals:":
in_section = True
continue
if line.startswith(" - "):
results.append(line[4:].strip())
continue
if line and not line.startswith(" "):
break
return results
def current_scratch_subgoal() -> str | None:
lines = read_text(SCRATCH_PATH).splitlines()
for index, line in enumerate(lines):
if line.strip() != "## Current Subgoal":
continue
for candidate in lines[index + 1 :]:
stripped = candidate.strip()
if stripped:
return stripped
return None
def load_evidence() -> list[dict[str, Any]]:
records: list[dict[str, Any]] = []
for line_number, line in enumerate(read_text(EVIDENCE_PATH).splitlines(), start=1):
if not line.strip():
continue
try:
record = json.loads(line)
except json.JSONDecodeError as exc:
raise ValueError(f"evidence line {line_number} is invalid JSON: {exc}") from exc
if not isinstance(record, dict):
raise ValueError(f"evidence line {line_number} is not a JSON object")
records.append(record)
return records
def load_branches() -> dict[str, Any]:
data = json.loads(read_text(BRANCHES_PATH))
if not isinstance(data, dict):
raise ValueError("branches.json must be a JSON object")
branches = data.get("branches")
if not isinstance(branches, list):
raise ValueError("branches.json must contain a branches list")
return data
def add_required_file_checks(findings: list[Finding]) -> None:
for path in [
MAP_PATH,
SCRATCH_PATH,
BRANCHES_PATH,
EVIDENCE_PATH,
HANDOFF_PATH,
PLAN_PATH,
ALGO_MAP_PATH,
AGENTS_PATH,
]:
if path.exists():
findings.append(Finding("ok", f"found {path.relative_to(ROOT)}"))
else:
findings.append(Finding("error", f"missing {path.relative_to(ROOT)}"))
def add_content_checks(findings: list[Finding]) -> tuple[list[dict[str, Any]], dict[str, Any]]:
evidence: list[dict[str, Any]] = []
branches: dict[str, Any] = {}
summary = extract_map_field("summary")
next_action = extract_map_field("next_action")
if summary:
findings.append(Finding("ok", "state/map.yaml has current_status.summary"))
else:
findings.append(Finding("error", "state/map.yaml is missing current_status.summary"))
if next_action:
findings.append(Finding("ok", "state/map.yaml has current_status.next_action"))
else:
findings.append(Finding("error", "state/map.yaml is missing current_status.next_action"))
subgoals = extract_active_subgoals()
if subgoals:
findings.append(Finding("ok", f"state/map.yaml has {len(subgoals)} active subgoal(s)"))
else:
findings.append(Finding("warn", "state/map.yaml has no active_subgoals entries"))
scratch_subgoal = current_scratch_subgoal()
if scratch_subgoal == "No active scratch subgoal.":
findings.append(Finding("ok", "state/scratch.md has no stale active scratch subgoal"))
elif scratch_subgoal:
findings.append(Finding("warn", f"state/scratch.md has active scratch subgoal: {scratch_subgoal}"))
else:
findings.append(Finding("warn", "state/scratch.md has no Current Subgoal value"))
try:
evidence = load_evidence()
findings.append(Finding("ok", f"state/evidence.jsonl parses ({len(evidence)} record(s))"))
except ValueError as exc:
findings.append(Finding("error", str(exc)))
if EVIDENCE_PATH.exists() and EVIDENCE_PATH.stat().st_size > 25_000:
findings.append(Finding("warn", "state/evidence.jsonl is larger than 25 KB; consider distillation"))
try:
branches = load_branches()
active = [
branch
for branch in branches.get("branches", [])
if isinstance(branch, dict) and branch.get("status") == "active"
]
findings.append(Finding("ok", f"state/branches.json parses ({len(active)} active branch(es))"))
except (json.JSONDecodeError, ValueError) as exc:
findings.append(Finding("error", f"branches.json parse failed: {exc}"))
return evidence, branches
def add_handoff_checks(findings: list[Finding]) -> None:
text = read_text(HANDOFF_PATH)
stale_branch = re.search(r"Current branch before .*ahead \d+", text)
stale_head = re.search(r"Current HEAD before .*\b[0-9a-f]{7,40}\b", text)
if stale_branch or stale_head:
findings.append(Finding("error", "handoff embeds an exact branch or HEAD snapshot; use git commands instead"))
else:
findings.append(Finding("ok", "handoff avoids exact branch/HEAD snapshots"))
required_phrases = [
"Do not continue implementation automatically from a rehydrate-only request.",
"Do not trust this file for the exact live HEAD.",
"Immediate Re-entry Instruction",
]
for phrase in required_phrases:
if phrase in text:
findings.append(Finding("ok", f"handoff contains: {phrase}"))
else:
findings.append(Finding("warn", f"handoff missing: {phrase}"))
def add_agents_checks(findings: list[Finding]) -> None:
text = read_text(AGENTS_PATH)
if "repixelizer_prepare_compaction.py" in text:
findings.append(Finding("ok", "AGENTS.md tells agents to use the compaction helper"))
else:
findings.append(Finding("error", "AGENTS.md does not mention tools/repixelizer_prepare_compaction.py"))
if "prepare for imminent compaction" in text.lower():
findings.append(Finding("ok", "AGENTS.md names the imminent-compaction trigger"))
else:
findings.append(Finding("warn", "AGENTS.md does not name the imminent-compaction trigger phrase"))
def add_git_checks(findings: list[Finding]) -> tuple[str, str]:
status = run_git(["status", "--short", "--branch"])
log = run_git(["log", "--oneline", "-5"])
dirty_lines = [line for line in status.splitlines()[1:] if line.strip()]
if dirty_lines:
findings.append(Finding("warn", "git worktree has uncommitted changes; commit or explain before compaction"))
else:
findings.append(Finding("ok", "git worktree is clean"))
return status, log
def render_report(findings: list[Finding], status: str, log: str, evidence: list[dict[str, Any]]) -> str:
summary = extract_map_field("summary") or "(missing)"
next_action = extract_map_field("next_action") or "(missing)"
subgoals = extract_active_subgoals()
counts = {
"ok": sum(1 for finding in findings if finding.level == "ok"),
"warn": sum(1 for finding in findings if finding.level == "warn"),
"error": sum(1 for finding in findings if finding.level == "error"),
}
lines = [
"Repixelizer pre-compaction persistence check",
f"Workspace: {ROOT}",
f"Findings: {counts['ok']} ok, {counts['warn']} warn, {counts['error']} error",
"",
"Git status:",
status,
"",
"Recent commits:",
log,
"",
f"Summary: {summary}",
f"Next action: {next_action}",
]
if subgoals:
lines.append("Active subgoals:")
lines.extend(f"- {item}" for item in subgoals)
if evidence:
latest = evidence[-1]
lines.extend(
[
"",
"Latest distilled evidence:",
f"- {latest.get('ts', '(missing ts)')} {latest.get('type', '(missing type)')}/{latest.get('status', '(missing status)')}: {latest.get('note', '(missing note)')}",
]
)
lines.append("")
lines.append("Findings:")
for finding in findings:
lines.append(f"[{finding.level.upper()}] {finding.message}")
lines.extend(
[
"",
"Pre-compaction checklist:",
"- Update state/map.yaml only if current understanding changed.",
"- Refresh notes/fresh-workspace-handoff.md if re-entry instructions changed.",
"- Add distilled evidence only for a belief-changing lesson, verification, rejected path, or scar.",
"- Keep exact branch/HEAD out of handoff prose; git commands own volatile truth.",
"- Commit completed persistence changes, or state why the worktree must stay dirty.",
"- Re-run this helper after edits before yielding to compaction.",
]
)
return "\n".join(lines)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Audit Repixelizer persistent state before imminent compaction."
)
parser.add_argument(
"--strict",
action="store_true",
help="Exit nonzero on warnings as well as errors.",
)
return parser
def main() -> int:
args = build_parser().parse_args()
findings: list[Finding] = []
evidence: list[dict[str, Any]] = []
add_required_file_checks(findings)
try:
evidence, _branches = add_content_checks(findings)
except OSError as exc:
findings.append(Finding("error", f"state content check failed: {exc}"))
try:
add_handoff_checks(findings)
except OSError as exc:
findings.append(Finding("error", f"handoff check failed: {exc}"))
try:
add_agents_checks(findings)
except OSError as exc:
findings.append(Finding("error", f"AGENTS check failed: {exc}"))
try:
status, log = add_git_checks(findings)
except (OSError, subprocess.CalledProcessError) as exc:
findings.append(Finding("error", f"git check failed: {exc}"))
status = "(git status unavailable)"
log = "(git log unavailable)"
print(render_report(findings, status, log, evidence))
has_error = any(finding.level == "error" for finding in findings)
has_warning = any(finding.level == "warn" for finding in findings)
if has_error or (args.strict and has_warning):
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())

179
tools/repixelizer_state.py Normal file
View file

@ -0,0 +1,179 @@
from __future__ import annotations
import argparse
import json
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
STATE_DIR = ROOT / "state"
MAP_PATH = STATE_DIR / "map.yaml"
BRANCHES_PATH = STATE_DIR / "branches.json"
EVIDENCE_PATH = STATE_DIR / "evidence.jsonl"
def read_text(path: Path) -> str:
return path.read_text(encoding="utf-8")
def write_text(path: Path, content: str) -> None:
path.write_text(content, encoding="utf-8")
def load_branches() -> dict[str, Any]:
return json.loads(read_text(BRANCHES_PATH))
def save_branches(data: dict[str, Any]) -> None:
write_text(BRANCHES_PATH, json.dumps(data, indent=2) + "\n")
def append_evidence(record: dict[str, Any]) -> None:
with EVIDENCE_PATH.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(record, ensure_ascii=True) + "\n")
def utc_stamp() -> str:
return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
def extract_map_field(name: str) -> str | None:
prefix = f" {name}:"
for line in read_text(MAP_PATH).splitlines():
if line.startswith(prefix):
return line.split(":", 1)[1].strip()
return None
def extract_active_subgoals() -> list[str]:
lines = read_text(MAP_PATH).splitlines()
results: list[str] = []
in_section = False
for line in lines:
if not in_section:
if line.strip() == "active_subgoals:":
in_section = True
continue
if line.startswith(" - "):
results.append(line[4:].strip())
continue
if line and not line.startswith(" "):
break
return results
def cmd_status(_: argparse.Namespace) -> int:
branches = load_branches().get("branches", [])
active = [branch for branch in branches if branch.get("status") == "active"]
summary = extract_map_field("summary") or "(missing)"
next_action = extract_map_field("next_action") or "(missing)"
subgoals = extract_active_subgoals()
print(f"Workspace: {ROOT}")
print(f"Summary: {summary}")
print(f"Next action: {next_action}")
print(f"Active branches: {len(active)} / {len(branches)}")
if subgoals:
print("Active subgoals:")
for item in subgoals:
print(f"- {item}")
return 0
def cmd_add_evidence(args: argparse.Namespace) -> int:
record: dict[str, Any] = {
"ts": utc_stamp(),
"type": args.type,
"status": args.status,
"note": args.note,
}
if args.branch:
record["branch"] = args.branch
append_evidence(record)
print("Appended evidence record.")
return 0
def cmd_add_branch(args: argparse.Namespace) -> int:
data = load_branches()
branches = data.setdefault("branches", [])
if any(branch.get("id") == args.id for branch in branches):
raise SystemExit(f"Branch '{args.id}' already exists.")
branches.append(
{
"id": args.id,
"hypothesis": args.hypothesis,
"status": "active",
"artifacts": args.artifact or [],
"notes": args.note or "",
}
)
save_branches(data)
print(f"Added branch '{args.id}'.")
return 0
def cmd_close_branch(args: argparse.Namespace) -> int:
data = load_branches()
for branch in data.get("branches", []):
if branch.get("id") != args.id:
continue
branch["status"] = args.status
if args.note:
branch["notes"] = args.note
save_branches(data)
print(f"Updated branch '{args.id}' to status '{args.status}'.")
return 0
raise SystemExit(f"Branch '{args.id}' was not found.")
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Inspect and update Repixelizer persistent state files."
)
subparsers = parser.add_subparsers(dest="command", required=True)
status_parser = subparsers.add_parser("status", help="Show a compact state summary.")
status_parser.set_defaults(func=cmd_status)
evidence_parser = subparsers.add_parser(
"add-evidence", help="Append one distilled JSONL evidence record."
)
evidence_parser.add_argument("--type", required=True)
evidence_parser.add_argument("--status", required=True)
evidence_parser.add_argument("--note", required=True)
evidence_parser.add_argument("--branch")
evidence_parser.set_defaults(func=cmd_add_evidence)
add_branch_parser = subparsers.add_parser(
"add-branch", help="Create a new active branch entry."
)
add_branch_parser.add_argument("--id", required=True)
add_branch_parser.add_argument("--hypothesis", required=True)
add_branch_parser.add_argument("--artifact", action="append")
add_branch_parser.add_argument("--note")
add_branch_parser.set_defaults(func=cmd_add_branch)
close_branch_parser = subparsers.add_parser(
"close-branch", help="Close an existing branch."
)
close_branch_parser.add_argument("--id", required=True)
close_branch_parser.add_argument(
"--status", required=True, choices=["accepted", "rejected", "archived"]
)
close_branch_parser.add_argument("--note")
close_branch_parser.set_defaults(func=cmd_close_branch)
return parser
def main() -> int:
parser = build_parser()
args = parser.parse_args()
return args.func(args)
if __name__ == "__main__":
raise SystemExit(main())