Watch
1
0
Fork
You've already forked SouveraineOS
0
SouveraineOS/tools/project-state/synthesize.py
Fimeg a8caf27c14 project-state: weekly evidence, carried state, and an agent's door
Five weeks of forge evidence per week rather than a rolling window, so a
finding survives under one key and "closed after three weeks" is computable.
Rules run deterministically; agent.py hands the packet to whoever writes the
prose and never calls a model itself.

Model choice fails closed — no default, no substitute, and the exact named
model must be served or the pass refuses to run.
2026-08-18 19:53:19 -04:00

821 lines
38 KiB
Python

#!/usr/bin/env python3
"""Turn a week of evidence plus last week's state into this week's state.
Deterministic rules run first and produce every finding that can be computed.
A model, when one is configured, only reads those findings and the rollup and
adds cross-cutting observations; it is never asked to discover a fact. Model
findings that cite an evidence id the week does not contain are dropped.
tools/project-state/synthesize.py --week 2026-W33
tools/project-state/synthesize.py --week 2026-W33 --interpret
tools/project-state/synthesize.py --explain f-0007
Env: ANALYST_URL, ANALYST_KEY, ANALYST_MODEL — any OpenAI-compatible endpoint.
See docs/tasks/79-project-state.md.
"""
import argparse
import datetime as dt
import ipaddress
import re
import json
import os
import sys
import urllib.parse
import urllib.request
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import common as c
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
STORE = os.path.join(ROOT, "project-state")
VERSION = 1
STALE_DAYS = 14
SAMPLE_PER_DIMENSION = 10
MODEL_MAX = 6
MOVEMENTS = ["milestone_reached", "regressed", "blocked", "decision_required",
"advanced", "unchanged"]
# Rules whose findings survive a week that says nothing about them. The rest
# describe one week and lapse with it.
CARRIES = {"task-standing", "task-listed", "task-moved", "task-restated",
"task-delisted", "pull-aging", "issue-aging", "issue-tracker-unused"}
CHARTER = """You read one week of evidence from the Souveraine project. Deterministic code has
already counted everything countable and its findings are included so you do not repeat
them. Your job is the three things code cannot do:
1. Work in one subsystem that changes what is true in another.
2. A contradiction between what the repository declares about itself and what its
commits, CI runs or pull requests show.
3. An assumption this week invalidated.
Rules, and a finding that breaks one is discarded unread:
- Write about the project, never about the findings list. "The deterministic finding
says X" is not a finding. Never count anything.
- Cite at least two evidence ids of different kinds — a commit id (c:) with a declared
change (d:), a CI run (ci:) with a pull request (p:), and so on. One kind alone is a
restatement, not a connection.
- Never say that two commits share a dimension, or that one id appears in two samples.
That is an artefact of how the evidence was sampled, not a fact about the project.
- At most six findings. Fewer is better. An empty list is a legal answer.
- One or two plain sentences. No hype, no praise, no summary of the week.
- Do not invent releases, decisions, people, or outcomes.
Answer with JSON only: {"findings": [{"dimension": ..., "movement": ..., "title": ...,
"statement": ..., "evidence": ["id", ...]}]}
Movements: milestone_reached, regressed, blocked, decision_required, advanced, unchanged."""
def load(path, default=None):
try:
return json.load(open(path))
except (FileNotFoundError, json.JSONDecodeError):
return default if default is not None else {}
def finding(key, dimension, title, movement, statement, evidence, rule):
return {
"key": key,
"dimension": dimension,
"title": title,
"movement": movement,
"statement": statement,
"evidence": evidence,
"provenance": {"by": "deterministic", "rule": rule},
}
# ---- deterministic rules ---------------------------------------------------
def ci_findings(ev, prior_ev):
out = []
for repo, stat in sorted(ev["rollup"]["ci"].items()):
runs, failed = stat["runs"], stat["failure"]
was = (prior_ev.get("rollup", {}).get("ci", {}) or {}).get(repo)
rate = failed / runs if runs else 0
was_rate = (was["failure"] / was["runs"]) if was and was["runs"] else None
newest = stat.get("newest") or {}
cite = [newest["id"]] if newest.get("id") else []
cite += [row["id"] for row in ev["ci"]
if row["repo"] == repo and row["conclusion"] == "failure"
and row["id"] not in cite][:5]
tally = f"{failed} of {runs} runs failed"
if stat["other"]:
tally += f" and {stat['other']} never concluded"
if newest.get("conclusion") == "failure":
movement, title = "regressed", f"{repo} ends the week red"
statement = f"The last run of the week on {repo} failed; {tally}."
elif was_rate is not None and rate > was_rate + 0.15:
movement, title = "regressed", f"{repo} CI failure rate rose"
statement = f"{tally} on {repo}{rate:.0%}, up from {was_rate:.0%}."
elif failed == 0 and runs:
movement, title = "advanced", f"{repo} CI clean"
statement = f"{runs} runs on {repo}, none failed."
else:
movement, title = "unchanged", f"{repo} CI mixed"
statement = f"On {repo}, {tally}."
out.append(finding(f"ci/{repo}", "reliability", title, movement,
statement, cite, "ci-week"))
return out
def dimension_findings(ev, prior_ev):
out = []
now = ev["rollup"]["by_dimension"]
was = prior_ev.get("rollup", {}).get("by_dimension", {})
for dim in c.DIMENSIONS:
here = now.get(dim, {})
commits = here.get("commits", 0)
declared = [row for row in ev["declared"] if row["dimension"] == dim]
before = was.get(dim, {}).get("commits", 0) if was else None
cite = [row["id"] for row in ev["commits"] if row["dimension"] == dim][:8]
cite += [row["id"] for row in declared][:4]
if not commits and not declared:
if before:
statement = f"No commits, after {before} the week before."
elif before == 0:
statement = "A second quiet week."
else:
statement = "No commits, and no week before this one to compare against."
out.append(finding(f"activity/{dim}", dim, f"{dim} did not move",
"unchanged", statement, cite or [f"w:{ev['week']}"],
"dimension-activity"))
continue
parts = [f"{commits} commit(s)"]
if before is not None:
parts.append(f"{before} the week before")
statement = ", ".join(parts) + "."
if declared:
named = "; ".join(f"{row['change']} {row.get('title') or row['file']}"
for row in declared[:4])
statement += f" Declared: {named}."
out.append(finding(f"activity/{dim}", dim, f"{dim} moved",
"advanced" if declared else "unchanged", statement,
cite, "dimension-activity"))
return out
BACKWARD = {"Live": 3, "In progress": 2, "Open": 1, "Blocked or gated": 0}
# Casey writes the open question into the row itself. These are his phrasings,
# read off the index on 2026-08-18 — not a general vocabulary of hesitation.
DECISION = re.compile(
r"verdict owed|decisions? (?:are|first|before)|needs a decision|open decision"
r"|unanswered|undecided|scope and deliverable|answer it before|product call"
r"|Casey'?s(?: call| own)?\b|owed\b", re.I)
def movement_for(row):
text = row.get("now") or row.get("was") or ""
if row["section"] == "Blocked or gated":
return "blocked"
if DECISION.search(text):
return "decision_required"
return "unchanged" if row["section"] == "Open" else "advanced"
def task_findings(ev):
"""One finding per task standing in the index, moved by this week's diff.
Tracking only the diff made every restatement a permanent finding and the
task itself invisible; the index is the state, the diff is the movement.
"""
changes = {row["task"]: row for row in ev["declared"] if row["kind"] == "task-index"}
out = []
for number, row in sorted(ev.get("index", {}).items(), key=lambda kv: int(kv[0])):
change = changes.get(number)
dim = c.classify_text(f"{row['title']} {row['file']} {row['left']}")[0]
key, section, left = f"task/{number}", row["section"], row["left"][:240]
cite = [change["id"]] if change else [f"d:task-{number}"]
state = {"section": section, "left": row["left"], "file": row["file"]}
if not change:
f = finding(key, dim, f"task {number}: {row['title']}",
"blocked" if section == "Blocked or gated" else "unchanged",
f"Standing under “{section}”. {left}", cite, "task-standing")
elif change["change"] == "listed":
f = finding(key, dim, f"task {number} entered the index: {row['title']}",
movement_for(change), f"Filed under “{section}”. {left}",
cite, "task-listed")
elif change["change"] == "moved":
old, new = change["detail"].split("")
back = BACKWARD.get(new, 9) < BACKWARD.get(old, 9)
f = finding(key, dim, f"task {number} moved: {row['title']}",
"regressed" if back else movement_for(change),
f"{old}{new}. {left}", cite, "task-moved")
else:
f = finding(key, dim, f"task {number} restated: {row['title']}",
movement_for(change),
f"What's left was rewritten under “{section}”. {left}",
cite, "task-restated")
f["task_state"] = state
out.append(f)
for number, change in sorted(changes.items(), key=lambda kv: int(kv[0])):
if change["change"] != "delisted":
continue
out.append(finding(f"task/{number}", change["dimension"],
f"task {number} left the index: {change['title']}",
"milestone_reached",
f"Dropped from “{change['detail']}” — closed work moves to archive/.",
[change["id"]], "task-delisted"))
return out
def aging_findings(ev):
out = []
for issue in ev["issues"]:
if issue["state"] != "open" or (issue["age_days"] or 0) < STALE_DAYS:
continue
out.append(finding(f"issue/{issue['repo']}#{issue['number']}",
c.classify_text(issue["title"])[0],
f"{issue['repo']}#{issue['number']} open {issue['age_days']}d: {issue['title'][:70]}",
"unchanged",
f"Filed {issue['created'][:10]}, still open.",
[issue["id"]], "issue-aging"))
for pull in ev["pulls"]:
if pull["state"] != "open" or (pull["age_days"] or 0) < STALE_DAYS:
continue
out.append(finding(f"pull/{pull['repo']}#{pull['number']}",
c.classify_text(pull["title"])[0],
f"{pull['repo']}#{pull['number']} unmerged {pull['age_days']}d: {pull['title'][:70]}",
"decision_required",
f"Opened {pull['created'][:10]}, never merged or closed.",
[pull["id"]], "pull-aging"))
for rel in ev["releases"]:
if rel["cut_this_week"]:
out.append(finding(f"release/{rel['repo']}", "release",
f"{rel['repo']} released {rel['tag']}", "milestone_reached",
f"{rel['assets']} assets published {rel['published'][:10]}.",
[rel["id"]], "release-cut"))
issues = ev["rollup"]["issues"]
if issues["open"] and not issues["ever_closed"] and issues["oldest_open_days"] >= STALE_DAYS:
oldest = next((i for i in ev["issues"] if i["id"] == issues["oldest_open"]), None)
out.append(finding("issues/never-closed", "adoption",
"No issue has ever been closed", "unchanged",
f"{issues['open']} open, none closed since the tracker was first used"
+ (f"; the oldest is {oldest['repo']}#{oldest['number']}, "
f"{oldest['age_days']}d." if oldest else "."),
[i["id"] for i in ev["issues"] if i["state"] == "open"][:8],
"issue-tracker-unused"))
return out
def deterministic(ev, prior_ev):
return (task_findings(ev) + ci_findings(ev, prior_ev)
+ aging_findings(ev) + dimension_findings(ev, prior_ev))
# ---- model pass ------------------------------------------------------------
def locality(url):
host = urllib.parse.urlsplit(url).hostname or ""
try:
return "local" if ipaddress.ip_address(host).is_private else "remote"
except ValueError:
return "local" if host in ("localhost", "127.0.0.1") else "remote"
def parse_findings(text):
"""The findings, and whether they were salvaged from a truncated answer.
A reasoning model's answer arrives after its thinking, so a budget that runs
out cuts the array mid-object. Every complete object before the cut is still
a whole finding, and throwing them away because the closing bracket is
missing loses real work.
"""
start, end = text.find("{"), text.rfind("}")
if start >= 0 and end > start:
try:
return json.loads(text[start:end + 1]).get("findings", []), False
except json.JSONDecodeError:
pass
out, depth, begin = [], 0, None
for i, ch in enumerate(text[text.find("[") + 1:] if "[" in text else ""):
if ch == "{":
if depth == 0:
begin = i
depth += 1
elif ch == "}" and depth:
depth -= 1
if depth == 0:
fragment = text[text.find("[") + 1:][begin:i + 1]
try:
out.append(json.loads(fragment))
except json.JSONDecodeError:
pass
return (out, True) if out else (None, False)
class AnalystUnavailable(Exception):
"""The named model cannot be run. Never a reason to run a different one."""
def require_model(url, model, note):
"""Confirm the endpoint serves the exact model named, or refuse to proceed.
There is no default model and no fallback. A convenient default made the
choice feel fungible, and on 2026-08-18 that ended in three other models
being tried after one had been named.
"""
req = urllib.request.Request(f"{url.rstrip('/')}/v1/models")
key = os.environ.get("ANALYST_KEY")
if key:
req.add_header("Authorization", f"Bearer {key}")
try:
with urllib.request.urlopen(req, timeout=30) as r:
served = [m.get("id") for m in (json.load(r).get("data") or [])]
except Exception as e:
raise AnalystUnavailable(f"{url} did not answer /v1/models: {e}")
if model not in served:
near = [m for m in served if model.split("-")[0].lower() in (m or "").lower()][:4]
raise AnalystUnavailable(
f"{url} does not serve {model!r}"
+ (f"; it does serve {near} — name one of those explicitly if you want it"
if near else f"; {len(served)} models served, none of them that one"))
note(f"analyst model {model} confirmed served by {url}")
def interpret(ev, findings, prior_state, note):
url = os.environ.get("ANALYST_URL")
model = os.environ.get("ANALYST_MODEL")
if not url or not model:
raise AnalystUnavailable(
"--interpret needs both ANALYST_URL and ANALYST_MODEL; there is no default model")
require_model(url, model, note)
ids = {row["id"] for group in ("commits", "ci", "issues", "pulls", "releases", "declared")
for row in ev[group]}
# The whole week is 39k characters of commit subjects. A local 9B spends its
# entire budget reading that and never answers (measured 2026-08-18, 323s,
# nothing returned). Sampling per dimension is what makes the pass affordable
# — and reading every subject is a job for the deterministic half anyway.
sample = {}
for row in ev["commits"]:
seen = sample.setdefault(row["dimension"], [])
if len(seen) < SAMPLE_PER_DIMENSION:
seen.append({"id": row["id"], "subject": row["subject"]})
brief = {
"week": ev["week"],
"counts": {"by_dimension": ev["rollup"]["by_dimension"], "ci": ev["rollup"]["ci"],
"issues": ev["rollup"]["issues"], "pulls": ev["rollup"]["pulls"]},
"deterministic_findings": [
{"dimension": f["dimension"], "movement": f["movement"], "title": f["title"]}
for f in findings],
"commit_sample": sample,
"declared": [{"id": row["id"], "change": row["change"], "detail": row["detail"],
"title": row.get("title") or row.get("file")} for row in ev["declared"]],
"open_from_last_week": [
{"key": f["key"], "title": f["title"], "weeks_open": f.get("weeks_open")}
for f in prior_state.get("findings", []) if f.get("status") == "open"][:30],
}
payload = {
"model": model,
"messages": [{"role": "system", "content": CHARTER},
{"role": "user", "content": json.dumps(brief, ensure_ascii=False)}],
# Fable is a reasoning model: it fills `reasoning_content` first and
# leaves `content` empty if the budget runs out before it concludes.
# Measured 2026-08-18 — 3000 tokens went entirely to reasoning and
# returned nothing, and a 250-token cap (what the ledger's narrator
# uses) cannot ever answer.
"max_tokens": int(os.environ.get("ANALYST_MAX_TOKENS", 6000)),
"temperature": 0.3,
}
headers = {"Content-Type": "application/json"}
key = os.environ.get("ANALYST_KEY")
if key:
headers["Authorization"] = f"Bearer {key}"
started = dt.datetime.now(dt.timezone.utc)
try:
req = urllib.request.Request(f"{url.rstrip('/')}/v1/chat/completions",
data=json.dumps(payload).encode(), headers=headers)
with urllib.request.urlopen(req, timeout=900) as r:
body = json.load(r)
except Exception as e:
raise AnalystUnavailable(
f"{model} failed after {(dt.datetime.now(dt.timezone.utc) - started).seconds}s: {e}")
choice = (body.get("choices") or [{}])[0]
message = choice.get("message", {})
text = (message.get("content") or "").strip()
took = (dt.datetime.now(dt.timezone.utc) - started).seconds
meta = {"model": model, "endpoint": locality(url), "ok": True, "seconds": took,
"usage": body.get("usage"), "finish": choice.get("finish_reason"),
"reasoning_chars": len(message.get("reasoning_content") or "")}
# An empty findings list is a legal answer. No answer at all is a failure of
# the model that was named, and stays that.
if not text:
raise AnalystUnavailable(
f"{model} returned no answer in {took}s — {meta['reasoning_chars']} chars of "
f"reasoning, finish={meta['finish']}; the budget went entirely to thinking")
parsed, meta["salvaged"] = parse_findings(text)
if parsed is None:
raise AnalystUnavailable(
f"{model} produced {len(text)} chars that are not findings JSON "
f"(finish={meta['finish']})")
out, dropped, seen = [], 0, set()
for i, row in enumerate(parsed):
cited = [e for e in (row.get("evidence") or []) if e in ids]
kinds = {e.split(":", 1)[0] for e in cited}
signature = frozenset(cited)
# A connection spans two kinds of evidence. One kind, or a citation set
# already used, is the model restating itself.
if (len(cited) < 2 or len(kinds) < 2 or signature in seen
or row.get("movement") not in MOVEMENTS or len(out) >= MODEL_MAX):
dropped += 1
continue
seen.add(signature)
title = str(row.get("title") or "")[:120]
out.append({
"key": f"model/{ev['week']}/{i}",
"dimension": row.get("dimension") if row.get("dimension") in c.DIMENSIONS else c.UNASSIGNED,
"title": title,
"movement": row["movement"],
"statement": str(row.get("statement") or "")[:600],
"evidence": cited,
"provenance": {"by": "model", "rule": "cross-cutting", **meta},
})
meta["accepted"], meta["dropped"] = len(out), dropped
note(f"analyst: {len(out)} findings kept, {dropped} dropped for uncited or bad movement, {took}s")
return out, meta
# ---- carry forward ---------------------------------------------------------
def carry(prior, produced, week, analyst, closes):
"""This week's findings over last week's, with the resolution rule explicit.
A finding the week says nothing about is still open. Only positive evidence
closes one — a task leaving the index, a pull merging. Absence of evidence
resolved four CI findings on the first two-week run, which is the difference
between a state document and a diff.
"""
by_key = {f["key"]: f for f in prior.get("findings", [])}
held = verdicts()
seq = max((int(f["id"][2:]) for f in prior.get("findings", []) if f.get("id", "").startswith("f-")),
default=0)
stamp = c.iso_z(dt.datetime.now(dt.timezone.utc))
weeks = prior.get("weeks", [])
if week not in weeks:
weeks = sorted(weeks + [week])
out, seen = [], set()
for new in produced:
old = by_key.get(new["key"])
seen.add(new["key"])
if old:
new["id"] = old["id"]
new["first_seen"] = old.get("first_seen", week)
new["was"] = {"movement": old.get("movement"), "week": old.get("last_seen")}
else:
seq += 1
new["id"] = f"f-{seq:04d}"
new["first_seen"] = week
new["human"] = held.get(new["key"], {"verdict": None, "note": None, "edited": False})
new["last_seen"] = week
# A model reading belongs to the week that produced it and is never
# carried as project state; it has no human verdict behind it.
new["status"] = "reading" if new["provenance"]["by"] == "model" else "open"
new["weeks_open"] = sum(1 for w in weeks if w >= new["first_seen"])
if "task_state" in new:
same_section = old and (old.get("task_state") or {}).get("section") == new["task_state"]["section"]
new["section_since"] = old.get("section_since", week) if same_section else week
new["weeks_in_section"] = sum(1 for w in weeks if w >= new["section_since"])
new["provenance"].setdefault("at", stamp)
new["provenance"].setdefault("evidence_week", week)
out.append(new)
for key, old in by_key.items():
if key in seen:
continue
if old.get("status") == "open":
if key in closes:
old["status"] = "resolved"
old["resolved_in"] = week
elif old["provenance"]["rule"] not in CARRIES:
# A CI week, a dimension's activity, a release cut: each states
# something about one week and says nothing about the next.
old["status"] = "lapsed"
old["lapsed_in"] = week
else:
old["weeks_open"] = sum(1 for w in weeks if w >= old.get("first_seen", week))
old["carried"] = True
out.append(old)
out.sort(key=lambda f: (f["status"] != "open", MOVEMENTS.index(f["movement"])
if f["movement"] in MOVEMENTS else 9, f["dimension"], f["id"]))
return {
"version": VERSION,
"updated": stamp,
"latest_week": week,
"weeks": weeks,
"analysis": {"week": week, "analyst": analyst,
"deterministic_rules": sorted({f["provenance"]["rule"] for f in produced
if f["provenance"]["by"] == "deterministic"})},
"findings": out,
}
# ---- report ----------------------------------------------------------------
BADGE = {"milestone_reached": "◆ milestone", "regressed": "▼ regressed",
"blocked": "■ blocked", "decision_required": "? decision",
"advanced": "▲ advanced", "unchanged": "· unchanged"}
def report(state, ev, week):
lines = [f"# Souveraine project state — {week}",
"",
f"{ev['range']['start']} to {ev['range']['end']} ({ev['range']['tz']}). "
f"Internal. Generated from `project-state/evidence/{week}.json`; "
f"the prose is disposable, the evidence and `state.json` are not.",
""]
r = ev["rollup"]
lines += ["## The week, counted", "",
f"- {len(ev['commits'])} commits across "
f"{len({row['repo'] for row in ev['commits']})} repos, "
f"{len(ev['ci'])} CI runs, {len(ev['declared'])} declared state changes.",
f"- Issues: {r['issues']['open']} open, {r['issues']['opened_this_week']} filed, "
f"{r['issues']['closed_this_week']} closed. "
f"Pulls: {r['pulls']['open']} open, {r['pulls']['merged_this_week']} merged.",
"- Dimension basis: "
+ ", ".join(f"{n} by {b}" for b, n in sorted(r["dimension_basis"].items(),
key=lambda kv: -kv[1]))
+ " (`paths` read the diff, `subject` read the commit subject, "
"`repo` fell back to the repository, `none` placed nothing).",
""]
open_now = [f for f in state["findings"] if f["status"] == "open"]
current = [f for f in open_now if f["last_seen"] == week]
standing = [f for f in open_now if f["last_seen"] != week]
resolved = [f for f in state["findings"] if f.get("resolved_in") == week]
read = [f for f in state["findings"]
if f["status"] == "reading" and f["last_seen"] == week]
# A task that simply stands under its section is state, not news; it belongs
# in Standing, not in a movement group that would be 60 rows long.
quiet = [f for f in current if f["provenance"]["rule"] == "task-standing"]
current = [f for f in current if f["provenance"]["rule"] != "task-standing"
or f["movement"] == "blocked"]
for movement in MOVEMENTS:
group = [f for f in current if f["movement"] == movement]
if not group:
continue
lines += [f"## {BADGE[movement]}", ""]
for f in group:
age = f" · open {f['weeks_open']} weeks since {f['first_seen']}" if f["weeks_open"] > 1 else ""
was = f["was"]["movement"] if f.get("was") else None
turn = f" · was {was.replace('_', ' ')}" if was and was != movement else ""
mark = "model" if f["provenance"]["by"] == "model" else f["provenance"]["rule"]
lines += [f"**{f['id']} · {f['dimension']} · {f['title']}** ",
f"{f['statement']} ",
f"<sub>{mark}{age}{turn} · evidence: "
f"{', '.join('`' + e + '`' for e in f['evidence'][:8])}"
f"{'' if len(f['evidence']) > 8 else ''}</sub>", ""]
if read:
analyst = state["analysis"]["analyst"] or {}
lines += ["## Read by the analyst — unreviewed", "",
f"Written by {analyst.get('model', 'a model')}, not computed. Every id below "
f"exists in the week's evidence and every finding spans two kinds of it; "
f"neither check makes the reading correct. Nothing here has a human verdict.",
""]
for f in read:
lines += [f"**{f['id']} · {f['dimension']} · {BADGE[f['movement']]} · {f['title']}** ",
f"{f['statement']} ",
f"<sub>evidence: {', '.join('`' + e + '`' for e in f['evidence'])}</sub>", ""]
if resolved:
lines += ["## Closed this week", ""]
for f in resolved:
lines += [f"- **{f['id']} · {f['title']}** — held from {f['first_seen']} "
f"to {f['last_seen']}, {f['weeks_open']} week(s)."]
lines.append("")
held = sorted((f for f in quiet if f["movement"] != "blocked"),
key=lambda f: (-f.get("weeks_in_section", 1), int(f["key"].split("/")[1])))
if held:
lines += ["## Standing longest without moving", "",
f"{len(quiet)} task(s) sat in their section untouched this week. "
f"The ten that have sat longest:", ""]
for f in held[:10]:
lines.append(f"- **{f['id']} · {f['dimension']} · {f['title']}** — "
f"{f['task_state']['section']}” for "
f"{f.get('weeks_in_section', 1)} week(s), since {f.get('section_since')}.")
lines.append("")
if standing:
lines += ["## Still open, unmentioned by this week's evidence", "",
"Nothing closed these; the week simply said nothing about them.", ""]
for f in sorted(standing, key=lambda f: -f["weeks_open"]):
lines.append(f"- **{f['id']} · {f['dimension']} · {f['title']}** — "
f"last evidence {f['last_seen']}, open {f['weeks_open']} week(s) "
f"since {f['first_seen']}.")
lines.append("")
analyst = state["analysis"]["analyst"]
lines += ["## Provenance", "",
f"- Evidence: `project-state/evidence/{week}.json`, collected "
f"{ev['collected_at']} by `{ev['collector']['tool']}` v{ev['collector']['version']} "
f"against {ev['collector']['forge']} in {ev['collector']['api_calls']} API calls.",
f"- Deterministic rules: {', '.join(state['analysis']['deterministic_rules'])}.",
(f"- Analyst: {analyst['model']} ({analyst['endpoint']}), "
f"{analyst.get('accepted', 0)} findings kept, {analyst.get('dropped', 0)} dropped."
if analyst else "- Analyst: none run; every finding here is deterministic."),
f"- Trace one: `tools/project-state/synthesize.py --explain <id>`.", ""]
if ev["collector"]["notes"]:
lines += ["Collector notes: " + "; ".join(ev["collector"]["notes"]), ""]
return "\n".join(lines)
def explain(state, finding_id):
f = next((x for x in state.get("findings", []) if x["id"] == finding_id or x["key"] == finding_id), None)
if not f:
print(f"no finding {finding_id}", file=sys.stderr)
return 1
print(json.dumps({k: v for k, v in f.items() if k != "evidence"}, indent=2, ensure_ascii=False))
week = f["provenance"].get("evidence_week", f["last_seen"])
ev = load(os.path.join(STORE, "evidence", f"{week}.json"))
index = {row["id"]: (group, row)
for group in ("commits", "ci", "issues", "pulls", "releases", "declared")
for row in ev.get(group, [])}
index[f"w:{week}"] = ("rollup", ev.get("rollup", {}))
print(f"\nevidence ({len(f['evidence'])} rows from {week}):")
for eid in f["evidence"]:
group, row = index.get(eid, (None, None))
if not row:
print(f" {eid}: MISSING from {week} evidence")
continue
print(f"\n [{group}] {eid}")
print(" " + json.dumps(row, indent=2, ensure_ascii=False).replace("\n", "\n "))
return 0
VERDICTS = ("confirm", "promote", "reject", "edit")
VERDICT_FILE = "verdicts.json"
def verdicts():
"""A person's word on a finding, keyed by finding key.
Kept outside state.json because state is derived and gets regenerated — a
verdict written into it vanished the next time the week was passed over.
This file is the one thing here a machine does not write.
"""
return load(os.path.join(STORE, VERDICT_FILE), {})
def write_verdict(spec, note, text=None, by=None):
finding_id, _, verdict = spec.partition("=")
if verdict not in VERDICTS:
note(f"verdict must be one of {', '.join(VERDICTS)}, not {verdict!r}")
return 2
state = load(os.path.join(STORE, "state.json"))
target = next((f for f in state.get("findings", [])
if f["id"] == finding_id or f["key"] == finding_id), None)
if not target:
note(f"no finding {finding_id}")
return 1
held = verdicts()
held[target["key"]] = {
"verdict": verdict,
"note": text,
"by": by or os.environ.get("USER") or "unknown",
"at": c.iso_z(dt.datetime.now(dt.timezone.utc)),
"edited": verdict == "edit",
"week": state.get("latest_week"),
"finding_id": target["id"],
"title": target["title"],
}
path = os.path.join(STORE, VERDICT_FILE)
open(path, "w").write(json.dumps(held, indent=2, ensure_ascii=False) + "\n")
target["human"] = held[target["key"]]
open(os.path.join(STORE, "state.json"), "w").write(
json.dumps(state, indent=2, ensure_ascii=False) + "\n")
note(f"{target['id']} {verdict} by {held[target['key']]['by']}{target['title'][:60]}")
return 0
def evidence_weeks():
path = os.path.join(STORE, "evidence")
return sorted(name[:-5] for name in os.listdir(path)) if os.path.isdir(path) else []
def pending(state):
"""Evidence weeks the state has not yet passed over, oldest first."""
done = set(state.get("weeks", []))
return [week for week in evidence_weeks() if week not in done]
def run_week(label, prior, args, note):
ev = load(os.path.join(STORE, "evidence", f"{label}.json"))
if not ev:
note(f"no evidence for {label} — run collect.py --week {label}")
return None, None
prior_ev = load(os.path.join(STORE, "evidence", f"{c.previous_week(label)}.json"))
produced = deterministic(ev, prior_ev)
model_rows, analyst = interpret(ev, produced, prior, note) if getattr(args, "interpret", False) else ([], None)
# A task closes when it is gone from the index, not when it merely goes
# unmentioned. The delisting week still shows its milestone; the week after
# is when it stops being open state.
listed = set(ev.get("index", {}))
closes = {f["key"] for f in prior.get("findings", [])
if f["key"].startswith("task/") and f["key"].split("/", 1)[1] not in listed}
return carry(prior, produced + model_rows, label, analyst, closes), ev
def persist(state, ev, label):
text = report(state, ev, label)
for folder in ("reports", "history"):
os.makedirs(os.path.join(STORE, folder), exist_ok=True)
open(os.path.join(STORE, "reports", f"{label}.md"), "w").write(text)
blob = json.dumps(state, indent=2, ensure_ascii=False) + "\n"
open(os.path.join(STORE, "history", f"{label}.json"), "w").write(blob)
open(os.path.join(STORE, "state.json"), "w").write(blob)
return text
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--week", default="last")
ap.add_argument("--catch-up", action="store_true",
help="pass over every collected week the state has not seen, in order")
ap.add_argument("--pending", action="store_true", help="name those weeks and stop")
ap.add_argument("--interpret", action="store_true",
help="run the model pass; needs ANALYST_URL and ANALYST_MODEL, "
"fails closed if that exact model is not served")
ap.add_argument("--verdict", help="a person's word on a finding, as f-0069=promote")
ap.add_argument("--note", help="text to store with --verdict")
ap.add_argument("--by", help="who is giving the verdict (default $USER)")
ap.add_argument("--explain", help="print a finding and every evidence row it cites")
ap.add_argument("--dry-run", action="store_true", help="print the report, write nothing")
ap.add_argument("--report-only", action="store_true",
help="rewrite the report from the stored state, running nothing")
args = ap.parse_args()
state_path = os.path.join(STORE, "state.json")
prior = load(state_path, {"findings": []})
if args.explain:
return explain(prior, args.explain)
notes = []
def note(message):
notes.append(message)
print(f"[project-state] {message}", file=sys.stderr)
if args.verdict:
return write_verdict(args.verdict, note, args.note, args.by)
if args.pending:
weeks = pending(prior)
print("\n".join(weeks) if weeks else "", end="\n" if weeks else "")
note(f"{len(weeks)} week(s) collected but not yet passed over" if weeks
else f"state is current through {prior.get('latest_week', 'nothing')}")
return 0
if args.catch_up:
weeks = pending(prior)
if not weeks:
note(f"nothing pending; state is current through {prior.get('latest_week')}")
return 0
for label in weeks:
state, ev = run_week(label, prior, args, note)
if not state:
return 2
persist(state, ev, label)
note(f"{label}: {sum(1 for f in state['findings'] if f['status'] == 'open')} open")
prior = state
return 0
label, _, _ = c.parse_week(args.week)
ev = load(os.path.join(STORE, "evidence", f"{label}.json"))
if not ev:
note(f"no evidence for {label} — run collect.py --week {label}")
return 2
if args.report_only:
state = load(os.path.join(STORE, "history", f"{label}.json")) or prior
text = report(state, ev, label)
if args.dry_run:
sys.stdout.write(text)
return 0
open(os.path.join(STORE, "reports", f"{label}.md"), "w").write(text)
note(f"{label}: report rewritten from stored state")
return 0
# Re-running a week already in the state starts from the week before it, so
# a second pass does not compound onto its own output.
if label in prior.get("weeks", []):
prior = load(os.path.join(STORE, "history", f"{c.previous_week(label)}.json"),
{"findings": []})
elif prior.get("latest_week") and c.previous_week(label) != prior["latest_week"]:
note(f"state stops at {prior['latest_week']} and this is {label}"
f"{len(pending(prior))} week(s) unseen; --catch-up runs them in order")
state, ev = run_week(label, prior, args, note)
if not state:
return 2
if args.dry_run:
sys.stdout.write(report(state, ev, label))
return 0
persist(state, ev, label)
produced = [f for f in state["findings"] if f["last_seen"] == label]
note(f"{label}: {len(produced)} findings this week, "
f"{sum(1 for f in state['findings'] if f['status'] == 'open')} open "
f"-> project-state/reports/{label}.md")
return 0
if __name__ == "__main__":
try:
sys.exit(main())
except AnalystUnavailable as failure:
# Nothing was written. The named model did not run, and no other one
# stands in for it.
print(f"[project-state] analyst unavailable: {failure}", file=sys.stderr)
sys.exit(3)