Add persistence scaffolding
This commit is contained in:
parent
03231c7285
commit
e66aceb372
8 changed files with 849 additions and 0 deletions
114
AGENTS.md
Normal file
114
AGENTS.md
Normal 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.
|
||||
111
notes/fresh-workspace-handoff.md
Normal file
111
notes/fresh-workspace-handoff.md
Normal 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
17
state/branches.json
Normal 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
3
state/evidence.jsonl
Normal 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
90
state/map.yaml
Normal 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
7
state/scratch.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Scratch
|
||||
|
||||
This file is disposable working memory for the current bounded subgoal.
|
||||
|
||||
## Current Subgoal
|
||||
|
||||
No active scratch subgoal.
|
||||
328
tools/repixelizer_prepare_compaction.py
Normal file
328
tools/repixelizer_prepare_compaction.py
Normal 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
179
tools/repixelizer_state.py
Normal 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())
|
||||
Loading…
Reference in a new issue