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.
821 lines
38 KiB
Python
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)
|