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.
This commit is contained in:
parent
1c565ce0e2
commit
a8caf27c14
26 changed files with 56303 additions and 0 deletions
212
docs/tasks/79-project-state.md
Normal file
212
docs/tasks/79-project-state.md
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
# TASK-79 — project state: what became true, and what stopped being true
|
||||||
|
|
||||||
|
**Status:** first slice built and exercised 2026-08-18 over five weeks,
|
||||||
|
2026-W30 to 2026-W34. Internal; nothing publishes.
|
||||||
|
**Repos:** `SouveraineOS` (the tools and the store). Reads the forge and the
|
||||||
|
local clones; writes nothing outside this repo.
|
||||||
|
**Connects to:** `saf/state.md` (the hand-written spine this reads and never
|
||||||
|
writes), `docs/tasks/README.md` (the declared state it diffs),
|
||||||
|
`~/Projects/Gitea Pipeline/TASK-02-the-ledger-pipeline.md` D4/D5 (the narrator
|
||||||
|
and the quality pass — same seam, different question),
|
||||||
|
`~/Projects/Web/CaseyTunturi.com/scripts/ledger/collect.py` (the ancestor).
|
||||||
|
|
||||||
|
## The question this answers, and the one it does not
|
||||||
|
|
||||||
|
The ledger on caseytunturi.com answers **what happened** — day-bucketed, 30-day
|
||||||
|
rolling, overwritten in place. It is a record of a man at work and it is good at
|
||||||
|
that. It cannot answer what *became true*, because nothing carries across the
|
||||||
|
window: every week is an island, and a 31-day-old finding is simply gone.
|
||||||
|
|
||||||
|
This answers **what is true now, what stopped being true, and how long it has
|
||||||
|
been unresolved.** Same forge, different shape: evidence is kept per week and
|
||||||
|
never overwritten, and a finding survives from one week to the next under a
|
||||||
|
stable key. Statements the five-week run can already make:
|
||||||
|
|
||||||
|
- *task 8 left the index, held W30 to W32, closed after two weeks.*
|
||||||
|
- *task 15, Idle coordinator, has been Blocked or gated for four weeks.*
|
||||||
|
- *souveraine#1 has been unmerged for 38 days and is the oldest decision open.*
|
||||||
|
- *souveraine CI was regressed in W33 and mixed in W34* — the finding turned.
|
||||||
|
|
||||||
|
## Why here and not in the ledger's repo
|
||||||
|
|
||||||
|
The ledger's output repo is the public site. Its CI builds and deploys on every
|
||||||
|
push, so an analytical artifact living there is one careless import away from
|
||||||
|
being published. SouveraineOS already owns project state — `saf/state.md` is the
|
||||||
|
resume anchor and `docs/tasks/README.md` is the work index — and it deploys
|
||||||
|
nowhere. The forge answers 403 to anonymous requests, so the store is behind a
|
||||||
|
login even though the repo is not marked private.
|
||||||
|
|
||||||
|
Promotion to either public surface stays a separate, deliberate act:
|
||||||
|
`caseytunturi.com` for engineering history, `souveraineai.com` for current
|
||||||
|
capability. One evidence base, two editorial surfaces, neither automatic.
|
||||||
|
|
||||||
|
## Shape
|
||||||
|
|
||||||
|
```
|
||||||
|
forge API + local clones
|
||||||
|
↓ collect.py deterministic, no model
|
||||||
|
project-state/evidence/<week>.json
|
||||||
|
↓ synthesize.py rules first, model second
|
||||||
|
project-state/state.json durable, carried forward
|
||||||
|
project-state/history/<week>.json
|
||||||
|
project-state/reports/<week>.md prose, disposable
|
||||||
|
```
|
||||||
|
|
||||||
|
Nothing schedules it. The tools name their own gaps instead:
|
||||||
|
|
||||||
|
```
|
||||||
|
collect.py --gaps [--since 2026-W30] weeks holding no evidence
|
||||||
|
collect.py --backfill --since 2026-W30 collect every one of them, in order
|
||||||
|
synthesize.py --pending weeks collected but not passed over
|
||||||
|
synthesize.py --catch-up pass over them oldest first
|
||||||
|
```
|
||||||
|
|
||||||
|
Order matters — carry-forward is sequential, so `--catch-up` is the entry point
|
||||||
|
and `--week` warns when it would skip ahead of the state.
|
||||||
|
|
||||||
|
The deterministic/model split is the point. A model is never asked to discover a
|
||||||
|
fact code can read; it gets the computed findings and looks only for what
|
||||||
|
crosses between them. Its findings are marked `by: model`, live in their own
|
||||||
|
section of the report, and are dropped unless every evidence id they cite exists
|
||||||
|
in that week's evidence and spans two kinds of it.
|
||||||
|
|
||||||
|
The prose is regenerable — `--report-only` rewrites any week's report from the
|
||||||
|
stored state and evidence, so a future model can rewrite history without the
|
||||||
|
original report ever having been authoritative.
|
||||||
|
|
||||||
|
## Evidence, measured 2026-08-18
|
||||||
|
|
||||||
|
| | W30 | W31 | W32 | W33 | W34 (partial) |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| Commits | 226 | 287 | 201 | 260 | 43 |
|
||||||
|
| CI runs | 98 | 133 | 143 | 185 | 25 |
|
||||||
|
| Declared changes | 255 | 56 | 18 | 37 | 116 |
|
||||||
|
|
||||||
|
16 repos listed, ~95 API calls a week, five weeks collected in 20 s, zero disk.
|
||||||
|
|
||||||
|
Three things the collector inherits from the ledger because they were paid for
|
||||||
|
once already: the `limit=50` page clamp (Gitea ignores a larger limit and hid 24
|
||||||
|
of 74 repos for months), the branch-tip prefilter (a branch whose tip predates
|
||||||
|
the window cannot hold work inside it), and per-repo SHA dedup.
|
||||||
|
|
||||||
|
Two it does not:
|
||||||
|
|
||||||
|
- **The Actions API.** `/repos/{repo}/actions/runs` ignores `limit` and `page`
|
||||||
|
and returns the whole history, so it is fetched once and sliced. Paging it
|
||||||
|
loops on the same rows. `02-the-ledger.md` lists CI state as an unbuilt later
|
||||||
|
increment; this is that increment, joined to commits by `head_sha`.
|
||||||
|
- **Redaction.** Evidence is stored raw because it never leaves the forge.
|
||||||
|
`--redact` applies the ledger's patterns for anything promoted outward.
|
||||||
|
|
||||||
|
## Dimensions
|
||||||
|
|
||||||
|
The SAF's own sections, plus the two it has no page for:
|
||||||
|
|
||||||
|
`memory` `runtime` `reflection` `identity` `surface` `sensorium` `release`
|
||||||
|
`reliability` `federation` `adoption`
|
||||||
|
|
||||||
|
A commit is placed by scoring its changed paths and its subject against a
|
||||||
|
keyword table, weighted alike — paths alone file `compaction: target the active
|
||||||
|
conversation` under the server module it edits, and the subject is the author
|
||||||
|
saying which subsystem he thinks he moved. On W33: 198 by paths, 29 by subject,
|
||||||
|
22 by repository default, 11 placed nothing and are reported as `unassigned`
|
||||||
|
rather than guessed at. Every row keeps its `basis` and its paths, so any
|
||||||
|
placement can be argued with.
|
||||||
|
|
||||||
|
One dimension per commit is a simplification. A crash fix in the shell is both
|
||||||
|
`surface` and `reliability` and counts once.
|
||||||
|
|
||||||
|
## State vocabulary, and the resolution rule
|
||||||
|
|
||||||
|
`milestone_reached` `regressed` `blocked` `decision_required` `advanced`
|
||||||
|
`unchanged` — assigned by rule:
|
||||||
|
|
||||||
|
- a task's section in the index (Live / In progress / Blocked or gated / Open)
|
||||||
|
gives its standing state; a move backwards is `regressed`, and leaving the
|
||||||
|
index entirely is `milestone_reached`;
|
||||||
|
- `decision_required` comes from Casey's own phrasing in the row — *verdict
|
||||||
|
owed*, *decisions are Casey's*, *scope and deliverable unanswered*;
|
||||||
|
- a repository whose last run of the week failed is `regressed`;
|
||||||
|
- a pull unmerged past fourteen days is `decision_required`.
|
||||||
|
|
||||||
|
**A finding the week says nothing about stays open.** Only positive evidence
|
||||||
|
closes one — a task gone from the index, a pull merged. The first two-week run
|
||||||
|
resolved four CI findings because the second week had no runs for those repos,
|
||||||
|
which is the difference between a state document and a diff.
|
||||||
|
|
||||||
|
Findings that describe *one week* — a CI week, a dimension's activity, a release
|
||||||
|
cut — do not carry at all; they lapse with their week. Findings that describe a
|
||||||
|
*thing* — a task, a pull, an issue — carry until something closes them.
|
||||||
|
|
||||||
|
Task findings track the index snapshot, not only its diff. Tracking the diff
|
||||||
|
alone made every restatement a permanent finding and the task itself invisible;
|
||||||
|
the index is the state, the diff is the movement, and `weeks_in_section` is how
|
||||||
|
"this remains the oldest blocker" gets computed.
|
||||||
|
|
||||||
|
## Provenance
|
||||||
|
|
||||||
|
Each finding carries `provenance` — deterministic rule name or model, endpoint
|
||||||
|
locality resolved from the URL rather than asserted, timestamp, and the week its
|
||||||
|
evidence came from — plus a `human` block (`verdict`, `note`, `edited`) that is
|
||||||
|
empty and exists so a promote/reject pass has somewhere to write.
|
||||||
|
|
||||||
|
`synthesize.py --explain <id>` prints a finding and every evidence row it cites,
|
||||||
|
in full. That is the trace path, and it is one command.
|
||||||
|
|
||||||
|
## The analyst, measured
|
||||||
|
|
||||||
|
`ANALYST_URL` / `ANALYST_KEY` / `ANALYST_MODEL`, any OpenAI-compatible endpoint.
|
||||||
|
No URL means no model pass and a complete deterministic report — the same "never
|
||||||
|
a failed run" contract the ledger's narrator has.
|
||||||
|
|
||||||
|
Against Fable on llama-swap, 2026-08-18:
|
||||||
|
|
||||||
|
- **Warm, it answers in 6 s.** The 14 s empty first call recorded in TASK-02 D4
|
||||||
|
was **not** llama-swap loading the model. Fable is a reasoning model: it fills
|
||||||
|
`reasoning_content` first and leaves `content` empty when the budget runs out
|
||||||
|
before it concludes. At `max_tokens: 250` — what `collect.py`'s `narrate()`
|
||||||
|
sets — it can never answer. **The ledger's narrator has that bug now.**
|
||||||
|
- The full week as a brief is 39k characters; Fable spent 3000 tokens reasoning
|
||||||
|
over it, returned nothing, and took 323 s. Sampling ten commit subjects per
|
||||||
|
dimension cuts the brief to 20k and the run to ~200 s.
|
||||||
|
- Reasoning length varies run to run and can still eat the budget, so a
|
||||||
|
truncated answer is salvaged object by object — every complete finding before
|
||||||
|
the cut is kept.
|
||||||
|
- **Quality is the open problem.** On W33 it returned 21 findings, of which ~15
|
||||||
|
were commentary on the deterministic findings rather than on the project. A
|
||||||
|
tightened charter and a gate requiring two kinds of evidence cut that to four.
|
||||||
|
Of those four: one good (CI runs that neither pass nor fail nearly equal the
|
||||||
|
passes, which the counts support), one arguable, two wrong — one cites a
|
||||||
|
success run as evidence of failure, another cites an unrelated doc commit.
|
||||||
|
A 9B is not a good analyst for this. The seam works; the model is Casey's call.
|
||||||
|
|
||||||
|
## Acceptance
|
||||||
|
|
||||||
|
- [x] Weeks collected deterministically, every row carrying a stable id.
|
||||||
|
- [x] A durable state file carrying a finding across weeks under one key, with
|
||||||
|
`first_seen`, `weeks_open`, `weeks_in_section`, and resolution only on
|
||||||
|
positive evidence.
|
||||||
|
- [x] Carry-forward exercised over five real weeks, not designed.
|
||||||
|
- [x] A human-readable report where each finding names its evidence.
|
||||||
|
- [x] One command traces a finding back to every row it rests on.
|
||||||
|
- [x] The tools name their own unfilled weeks rather than assuming a schedule.
|
||||||
|
- [ ] An analyst whose findings survive review, or the pass explicitly parked.
|
||||||
|
- [ ] A cadence decision: on demand, or a Monday job on the same runner.
|
||||||
|
- [ ] A human verdict written into a finding — the seam exists, nothing uses it.
|
||||||
|
|
||||||
|
## Open, and Casey's to answer
|
||||||
|
|
||||||
|
- **Which model holds the analyst.** Fable is honest and slow and wrong about
|
||||||
|
half the time on this. The substrate's own config already reaches bifrost,
|
||||||
|
deepseek, zai and claude; the deterministic half needs none of them.
|
||||||
|
- **Where a promoted finding goes.** The seam exists; the editorial call does
|
||||||
|
not.
|
||||||
|
- **Whether the store is committed here.** It is project state, it belongs with
|
||||||
|
`saf/state.md`, and it is behind the forge login — but it is a machine-written
|
||||||
|
tree in a repository whose other files are hand-written.
|
||||||
|
|
||||||
|
## Noticed while building this, not part of it
|
||||||
|
|
||||||
|
`docs/tasks/README.md` publishes `10.10.20.123`, `172.16.42.1` and an ssh key
|
||||||
|
path into this repo. Behind the forge login, so not the leak TASK-02 D8 closed —
|
||||||
|
same class, and it sits in the file this task diffs every week.
|
||||||
|
|
@ -38,6 +38,7 @@ Four that are cheap relative to what they unblock:
|
||||||
|
|
||||||
| # | Task | What's left |
|
| # | Task | What's left |
|
||||||
|---|------|-------------|
|
|---|------|-------------|
|
||||||
|
| 79 | [Project state: what became true, and what stopped being true](79-project-state.md) | First slice runs 2026-08-18 — `project-state/` holds W33 and W34 evidence, a carry-forward `state.json`, and a report where every finding traces to its rows. Left: which model holds the analyst (Fable reasons past any budget on this brief), a cadence decision, and whether the machine-written store belongs in this repo. |
|
||||||
| 77 | [The lens: a human-first garden over a git vault](77-lens-garden.md) | Built + packaged 2026-08-15 — folder/subcategory support and inbox capture landed (`61cf534`), publisher chmod fix (`c71c7e4`). Left: lens CI green + `edge` asset published, `pacman -U` on the laptop, then edge-repo DB wiring. |
|
| 77 | [The lens: a human-first garden over a git vault](77-lens-garden.md) | Built + packaged 2026-08-15 — folder/subcategory support and inbox capture landed (`61cf534`), publisher chmod fix (`c71c7e4`). Left: lens CI green + `edge` asset published, `pacman -U` on the laptop, then edge-repo DB wiring. |
|
||||||
| 65 | [A stalled subconscious wedges the surface](65-subconscious-stall-wedges-the-surface.md) | Reproduced and measured, not diagnosed. The reply commits and `PrimaryComplete` fires, then the N+1 pass stalls at round 3 of 10 with a fully idle runtime — and because it runs inside `run_turn`, the SSE stream never closes and Surfaces spins forever. Lead is the `todo` tool. Needs a backtrace; the stream-lifetime coupling is a fix in its own right. |
|
| 65 | [A stalled subconscious wedges the surface](65-subconscious-stall-wedges-the-surface.md) | Reproduced and measured, not diagnosed. The reply commits and `PrimaryComplete` fires, then the N+1 pass stalls at round 3 of 10 with a fully idle runtime — and because it runs inside `run_turn`, the SSE stream never closes and Surfaces spins forever. Lead is the `todo` tool. Needs a backtrace; the stream-lifetime coupling is a fix in its own right. |
|
||||||
| 63 | [The second body: SouveraineOS on the iPhone 7](63-d10-second-body.md) | **The shell is on the glass 2026-08-09** — viewtop, sessiond and `qs -c souveraine` all run on a T8010 with no GPU, drawn by llvmpipe, over one USB cable. Hoolock is the bootloader and nothing else. The build half of this is TASK-64. |
|
| 63 | [The second body: SouveraineOS on the iPhone 7](63-d10-second-body.md) | **The shell is on the glass 2026-08-09** — viewtop, sessiond and `qs -c souveraine` all run on a T8010 with no GPU, drawn by llvmpipe, over one USB cable. Hoolock is the bootloader and nothing else. The build half of this is TASK-64. |
|
||||||
|
|
|
||||||
25
project-state/README.md
Normal file
25
project-state/README.md
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# project-state — what became true, week by week
|
||||||
|
|
||||||
|
Machine-written. Internal. Nothing here publishes anywhere.
|
||||||
|
|
||||||
|
`saf/state.md` says what is true right now, in Casey's hand. This says how it
|
||||||
|
got there and what has been unresolved for how long.
|
||||||
|
|
||||||
|
```
|
||||||
|
evidence/<week>.json one ISO week of forge evidence; never overwritten
|
||||||
|
state.json the carry-forward — findings under stable keys
|
||||||
|
history/<week>.json state.json as it stood after that week's pass
|
||||||
|
reports/<week>.md the human read; disposable, regenerable from the two above
|
||||||
|
```
|
||||||
|
|
||||||
|
Written by `tools/project-state/collect.py` and `synthesize.py`. The reasoning
|
||||||
|
and the open decisions are in `docs/tasks/79-project-state.md`.
|
||||||
|
|
||||||
|
```
|
||||||
|
tools/project-state/collect.py --week last | current | 2026-W33
|
||||||
|
tools/project-state/synthesize.py --week 2026-W33 [--interpret]
|
||||||
|
tools/project-state/synthesize.py --explain f-0022
|
||||||
|
```
|
||||||
|
|
||||||
|
A finding cites evidence ids and nothing else. `--explain` prints the finding
|
||||||
|
and every row it rests on.
|
||||||
11
project-state/drafts/2026-W33--annie.json
Normal file
11
project-state/drafts/2026-W33--annie.json
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"week": "2026-W33",
|
||||||
|
"author": "Annie",
|
||||||
|
"model": "claude-opus-5",
|
||||||
|
"recorded_at": "2026-08-18T23:24:37Z",
|
||||||
|
"state_updated": "2026-08-18T22:48:32Z",
|
||||||
|
"findings_open": 81,
|
||||||
|
"evidence": "project-state/evidence/2026-W33.json",
|
||||||
|
"published": false,
|
||||||
|
"source": "/tmp/claude-1000/-home-casey/0c2cbaa2-dc6e-402b-9cb7-87cf56b4d4ea/scratchpad/draft.md"
|
||||||
|
}
|
||||||
4
project-state/drafts/2026-W33--annie.md
Normal file
4
project-state/drafts/2026-W33--annie.md
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
# Souveraine — 2026-W33 (draft)
|
||||||
|
|
||||||
|
Placeholder written to exercise the handoff, not a real reading.
|
||||||
|
souveraine ended the week red (f-0037, ci:souveraine:1658).
|
||||||
9863
project-state/evidence/2026-W30.json
Normal file
9863
project-state/evidence/2026-W30.json
Normal file
File diff suppressed because it is too large
Load diff
8127
project-state/evidence/2026-W31.json
Normal file
8127
project-state/evidence/2026-W31.json
Normal file
File diff suppressed because it is too large
Load diff
6393
project-state/evidence/2026-W32.json
Normal file
6393
project-state/evidence/2026-W32.json
Normal file
File diff suppressed because it is too large
Load diff
8325
project-state/evidence/2026-W33.json
Normal file
8325
project-state/evidence/2026-W33.json
Normal file
File diff suppressed because it is too large
Load diff
3603
project-state/evidence/2026-W34.json
Normal file
3603
project-state/evidence/2026-W34.json
Normal file
File diff suppressed because it is too large
Load diff
1576
project-state/history/2026-W30.json
Normal file
1576
project-state/history/2026-W30.json
Normal file
File diff suppressed because it is too large
Load diff
2386
project-state/history/2026-W31.json
Normal file
2386
project-state/history/2026-W31.json
Normal file
File diff suppressed because it is too large
Load diff
2721
project-state/history/2026-W32.json
Normal file
2721
project-state/history/2026-W32.json
Normal file
File diff suppressed because it is too large
Load diff
3323
project-state/history/2026-W33.json
Normal file
3323
project-state/history/2026-W33.json
Normal file
File diff suppressed because it is too large
Load diff
3377
project-state/history/2026-W34.json
Normal file
3377
project-state/history/2026-W34.json
Normal file
File diff suppressed because it is too large
Load diff
221
project-state/reports/2026-W30.md
Normal file
221
project-state/reports/2026-W30.md
Normal file
|
|
@ -0,0 +1,221 @@
|
||||||
|
# Souveraine project state — 2026-W30
|
||||||
|
|
||||||
|
2026-07-20 to 2026-07-26 (America/Toronto). Internal. Generated from `project-state/evidence/2026-W30.json`; the prose is disposable, the evidence and `state.json` are not.
|
||||||
|
|
||||||
|
## The week, counted
|
||||||
|
|
||||||
|
- 226 commits across 8 repos, 98 CI runs, 255 declared state changes.
|
||||||
|
- Issues: 6 open, 0 filed, 0 closed. Pulls: 2 open, 0 merged.
|
||||||
|
- Dimension basis: 154 by paths, 41 by subject, 19 by none, 12 by repo (`paths` read the diff, `subject` read the commit subject, `repo` fell back to the repository, `none` placed nothing).
|
||||||
|
|
||||||
|
## ◆ milestone
|
||||||
|
|
||||||
|
**f-0033 · identity · task 11 left the index: souveraine-secrets on the phone**
|
||||||
|
Dropped from “Index” — closed work moves to archive/.
|
||||||
|
<sub>task-delisted · evidence: `d:task-11`</sub>
|
||||||
|
|
||||||
|
**f-0039 · release · souveraine released edge**
|
||||||
|
93 assets published 2026-07-25.
|
||||||
|
<sub>release-cut · evidence: `r:souveraine:edge`</sub>
|
||||||
|
|
||||||
|
**f-0030 · reliability · task 5 left the index: Crash reporter surfacing**
|
||||||
|
Dropped from “Index” — closed work moves to archive/.
|
||||||
|
<sub>task-delisted · evidence: `d:task-5`</sub>
|
||||||
|
|
||||||
|
**f-0029 · runtime · task 4 left the index: Notification server (org.freedesktop.Notifications)**
|
||||||
|
Dropped from “Index” — closed work moves to archive/.
|
||||||
|
<sub>task-delisted · evidence: `d:task-4`</sub>
|
||||||
|
|
||||||
|
**f-0028 · unassigned · task 1 left the index: Favorites — wife's name on her number**
|
||||||
|
Dropped from “Index” — closed work moves to archive/.
|
||||||
|
<sub>task-delisted · evidence: `d:task-1`</sub>
|
||||||
|
|
||||||
|
**f-0031 · unassigned · task 6 left the index: qtpim aarch64 build → contacts store**
|
||||||
|
Dropped from “Index” — closed work moves to archive/.
|
||||||
|
<sub>task-delisted · evidence: `d:task-6`</sub>
|
||||||
|
|
||||||
|
**f-0032 · unassigned · task 10 left the index: Boot chain: pmOS init → mkinitcpio**
|
||||||
|
Dropped from “Index” — closed work moves to archive/.
|
||||||
|
<sub>task-delisted · evidence: `d:task-10`</sub>
|
||||||
|
|
||||||
|
## ▼ regressed
|
||||||
|
|
||||||
|
**f-0036 · reliability · pocketboot ends the week red**
|
||||||
|
The last run of the week on pocketboot failed; 2 of 2 runs failed.
|
||||||
|
<sub>ci-week · evidence: `ci:pocketboot:834`, `ci:pocketboot:833`</sub>
|
||||||
|
|
||||||
|
## ? decision
|
||||||
|
|
||||||
|
**f-0004 · identity · task 8 restated: Device state manager / power profiles**
|
||||||
|
What's left was rewritten under “Index”. actuating 07-25 | clock + actuators; lock-before-blank is an invariant and hypridle's listeners are deleted; (b) charge real, (c)(d) open, **(e) lease still missing**, **(f) `sensors_degraded` reaches no surface**, **(g) the confidence gate
|
||||||
|
<sub>task-restated · evidence: `d:task-8`</sub>
|
||||||
|
|
||||||
|
## ▲ advanced
|
||||||
|
|
||||||
|
**f-0016 · federation · task 23 entered the index: culver locked-notification nudge**
|
||||||
|
Filed under “Index”. open | designed 07-22; needs `SyncBurst()` + IdleCoordinator schedule
|
||||||
|
<sub>task-listed · evidence: `d:task-23`</sub>
|
||||||
|
|
||||||
|
**f-0048 · federation · federation moved**
|
||||||
|
2 commit(s). Declared: listed culver locked-notification nudge; added docs/substrate/FEDERATION_SKETCH.md; added docs/substrate/tasks/archive/federation-seed-id.md; added docs/substrate/tasks/federation-summon.md.
|
||||||
|
<sub>dimension-activity · evidence: `c:culver:bb6385d1`, `c:Pixel3Arch:e760a39f`, `d:task-23`, `d:docs/substrate/FEDERATION_SKETCH.md`, `d:docs/substrate/tasks/archive/federation-seed-id.md`, `d:docs/substrate/tasks/federation-summon.md`</sub>
|
||||||
|
|
||||||
|
**f-0009 · identity · task 15 entered the index: Idle coordinator: freeze, doze, scheduled wakeups**
|
||||||
|
Filed under “Index”. blocked | freezing `app.slice` would freeze sessiond+shell+secrets; needs slice separation first (measured 07-25)
|
||||||
|
<sub>task-listed · evidence: `d:task-15`</sub>
|
||||||
|
|
||||||
|
**f-0020 · identity · task 28 entered the index: Handover on upgrade**
|
||||||
|
Filed under “Index”. open | half-upgrades move authority silently; blocked-by TASK-27 packaging
|
||||||
|
<sub>task-listed · evidence: `d:task-28`</sub>
|
||||||
|
|
||||||
|
**f-0027 · identity · task 35 entered the index: sessiond answers nothing: handler threads park forever**
|
||||||
|
Filed under “Index”. **open, live** | 347 threads in futex_wait, 691/1024 fds; accept loop fine, no request ever answered
|
||||||
|
<sub>task-listed · evidence: `d:task-35`</sub>
|
||||||
|
|
||||||
|
**f-0043 · identity · identity moved**
|
||||||
|
35 commit(s). Declared: restated Device state manager / power profiles; delisted souveraine-secrets on the phone; listed Idle coordinator: freeze, doze, scheduled wakeups; listed Handover on upgrade.
|
||||||
|
<sub>dimension-activity · evidence: `c:culver:d6201130`, `c:souveraine:d824ca7f`, `c:SouveraineOS:ae6e2be5`, `c:SouveraineOS:19475878`, `c:souveraine:b4b30b12`, `c:souveraine:261d7467`, `c:souveraine:07d45c10`, `c:souveraine:91c0cf80` …</sub>
|
||||||
|
|
||||||
|
**f-0040 · memory · memory moved**
|
||||||
|
1 commit(s). Declared: added docs/substrate/COMPACTION_STRATEGIES.md; added docs/substrate/tasks/archive/compaction-rebuild.md; added docs/substrate/tasks/archive/in-session-compaction-implementation.md; added docs/substrate/tasks/archive/n100-archivist-compression.md.
|
||||||
|
<sub>dimension-activity · evidence: `c:souveraine:687cfeb5`, `d:docs/substrate/COMPACTION_STRATEGIES.md`, `d:docs/substrate/tasks/archive/compaction-rebuild.md`, `d:docs/substrate/tasks/archive/in-session-compaction-implementation.md`, `d:docs/substrate/tasks/archive/n100-archivist-compression.md`</sub>
|
||||||
|
|
||||||
|
**f-0042 · reflection · reflection moved**
|
||||||
|
1 commit(s). Declared: added docs/substrate/subconscious-evolution/2026-05-19_subconscious_mandate_enrichment.md; added docs/substrate/tasks/archive/n25-reflection-four-elements.md; added docs/substrate/tasks/archive/rename-aster-to-subconscious.md; added docs/substrate/tasks/archive/scope-4-n25-reflection.md.
|
||||||
|
<sub>dimension-activity · evidence: `c:Pixel3Arch:d39c1fdc`, `d:docs/substrate/subconscious-evolution/2026-05-19_subconscious_mandate_enrichment.md`, `d:docs/substrate/tasks/archive/n25-reflection-four-elements.md`, `d:docs/substrate/tasks/archive/rename-aster-to-subconscious.md`, `d:docs/substrate/tasks/archive/scope-4-n25-reflection.md`</sub>
|
||||||
|
|
||||||
|
**f-0014 · release · task 20 entered the index: Souveraine Player video + agent control**
|
||||||
|
Filed under “Index”. open | video/snapshot/manifest; preserve music
|
||||||
|
<sub>task-listed · evidence: `d:task-20`</sub>
|
||||||
|
|
||||||
|
**f-0018 · release · task 25 entered the index: One repo, all packages**
|
||||||
|
Filed under “Index”. parked 07-25 | four blockers before `repo-add --sign`; see also TASK-27
|
||||||
|
<sub>task-listed · evidence: `d:task-25`</sub>
|
||||||
|
|
||||||
|
**f-0019 · release · task 27 entered the index: Gitea pipeline audit**
|
||||||
|
Filed under “Index”. open | overlay is not a package; no `primary` branch; poisoned build sysroot
|
||||||
|
<sub>task-listed · evidence: `d:task-27`</sub>
|
||||||
|
|
||||||
|
**f-0046 · release · release moved**
|
||||||
|
32 commit(s). Declared: listed Souveraine Player video + agent control; listed One repo, all packages; listed Gitea pipeline audit; added docs/archive/DUMP-pacman-pipeline-2026-07-24.md.
|
||||||
|
<sub>dimension-activity · evidence: `c:culver:2963a794`, `c:souveraine-player:3ad5a504`, `c:Pixel3Arch:a970742a`, `c:Pixel3Arch:693cde44`, `c:Pixel3Arch:9135d3fc`, `c:Pixel3Arch:80bb0c86`, `c:Pixel3Arch:42aedb2e`, `c:Pixel3Arch:36268f61` …</sub>
|
||||||
|
|
||||||
|
**f-0026 · reliability · task 34 entered the index: SLPI bring-up sequencing regression**
|
||||||
|
Filed under “Index”. open | old sequencing error; doorbell dies on SSR and stays dead; blocks TASK-13 step 2
|
||||||
|
<sub>task-listed · evidence: `d:task-34`</sub>
|
||||||
|
|
||||||
|
**f-0038 · reliability · souveraine-updater CI clean**
|
||||||
|
6 runs on souveraine-updater, none failed.
|
||||||
|
<sub>ci-week · evidence: `ci:souveraine-updater:885`</sub>
|
||||||
|
|
||||||
|
**f-0047 · reliability · reliability moved**
|
||||||
|
12 commit(s). Declared: delisted Crash reporter surfacing; listed SLPI bring-up sequencing regression.
|
||||||
|
<sub>dimension-activity · evidence: `c:souveraine-player:2ed0389f`, `c:SouveraineOS:651f24d2`, `c:souveraine:a799bf1c`, `c:souveraine:f620a5d5`, `c:souveraine:155288d9`, `c:culver:b28b15f8`, `c:souveraine:044c373c`, `c:souveraine-updater:ec1f42e8` …</sub>
|
||||||
|
|
||||||
|
**f-0012 · runtime · task 18 entered the index: Selection/highlight action menu**
|
||||||
|
Filed under “Index”. open | design captured 07-21; component-first, Read Aloud gated on TTS server
|
||||||
|
<sub>task-listed · evidence: `d:task-18`</sub>
|
||||||
|
|
||||||
|
**f-0041 · runtime · runtime moved**
|
||||||
|
15 commit(s). Declared: delisted Notification server (org.freedesktop.Notifications); listed Selection/highlight action menu; added docs/substrate/ANI_PRESSURE_PHENOMENOLOGY.md; added docs/substrate/ARCHITECTURE_v3.md.
|
||||||
|
<sub>dimension-activity · evidence: `c:Pixel3Arch:2b3351d6`, `c:souveraine-player:9ec06edc`, `c:souveraine-player:15be14ae`, `c:souveraine-player:313481db`, `c:souveraine-player:d3bfbcd4`, `c:SouveraineOS:95808d9a`, `c:Pixel3Arch:67aeb254`, `c:SouveraineOS:2d95f374` …</sub>
|
||||||
|
|
||||||
|
**f-0007 · sensorium · task 13 entered the index: Active Edge (squeeze) as a sensor input**
|
||||||
|
Filed under “Index”. open, wanted | rail is UP and held from boot (07-26); step 2 now blocked on TASK-34 (SLPI doorbell)
|
||||||
|
<sub>task-listed · evidence: `d:task-13`</sub>
|
||||||
|
|
||||||
|
**f-0025 · sensorium · task 33 entered the index: Battery and charging belong to the device state machine**
|
||||||
|
Filed under “Index”. open | last raw feed going straight to the glass; the charge ceiling is writable today
|
||||||
|
<sub>task-listed · evidence: `d:task-33`</sub>
|
||||||
|
|
||||||
|
**f-0045 · sensorium · sensorium moved**
|
||||||
|
47 commit(s). Declared: listed Active Edge (squeeze) as a sensor input; listed Battery and charging belong to the device state machine; added docs/AUDIO-PRIVACY.md.
|
||||||
|
<sub>dimension-activity · evidence: `c:SouveraineOS:7ec2479f`, `c:linux-blueline:4580513b`, `c:linux-blueline:22f9f080`, `c:Pixel3Arch:b564b2ab`, `c:Pixel3Arch:c8557cfa`, `c:Pixel3Arch:46855d89`, `c:Pixel3Arch:2a3aeaa5`, `c:Pixel3Arch:4760853c` …</sub>
|
||||||
|
|
||||||
|
**f-0008 · surface · task 14 entered the index: Overview/app grid + Auxo multitasking**
|
||||||
|
Filed under “Index”. in progress | shell-side; Home path + app cards
|
||||||
|
<sub>task-listed · evidence: `d:task-14`</sub>
|
||||||
|
|
||||||
|
**f-0010 · surface · task 16 entered the index: culver: usable messenger + contacts as Personal-class store**
|
||||||
|
Filed under “Index”. in progress | A/B/D landed; C contact card next; F is the folded-in TASK-01
|
||||||
|
<sub>task-listed · evidence: `d:task-16`</sub>
|
||||||
|
|
||||||
|
**f-0011 · surface · task 17 entered the index: Keyboard (stevia) + dictation polish**
|
||||||
|
Filed under “Index”. shipped-partial | OSK + mic key + STT live; **no word completion** — stevia asks hunspell for `en-us`, the dict is `en_US` (07-25)
|
||||||
|
<sub>task-listed · evidence: `d:task-17`</sub>
|
||||||
|
|
||||||
|
**f-0017 · surface · task 24 entered the index: Keyboard layout rethink**
|
||||||
|
Filed under “Index”. open | start from squeekboard `us+apple`; Casey decides where the mic key goes
|
||||||
|
<sub>task-listed · evidence: `d:task-24`</sub>
|
||||||
|
|
||||||
|
**f-0022 · surface · task 30 entered the index: Agent reach: verb tables across the surfaces**
|
||||||
|
Filed under “Index”. half built 07-26 | sessiond has describe + refusal codes + intent; dock/apps/shell/settings do not
|
||||||
|
<sub>task-listed · evidence: `d:task-30`</sub>
|
||||||
|
|
||||||
|
**f-0023 · surface · task 31 entered the index: The radial dial**
|
||||||
|
Filed under “Index”. open | component before contents; entries come from the verb tables, not a list in QML
|
||||||
|
<sub>task-listed · evidence: `d:task-31`</sub>
|
||||||
|
|
||||||
|
**f-0024 · surface · task 32 entered the index: The pill and the keyboard, as owned apps**
|
||||||
|
Filed under “Index”. open | both erroneous daily; neither has an owner or a shipping story
|
||||||
|
<sub>task-listed · evidence: `d:task-32`</sub>
|
||||||
|
|
||||||
|
**f-0044 · surface · surface moved**
|
||||||
|
62 commit(s). Declared: listed Overview/app grid + Auxo multitasking; listed culver: usable messenger + contacts as Personal-class store; listed Keyboard (stevia) + dictation polish; listed Keyboard layout rethink.
|
||||||
|
<sub>dimension-activity · evidence: `c:souveraine:a2320641`, `c:Pixel3Arch:9c64e9ee`, `c:souveraine-player:6dc8ee6c`, `c:Pixel3Arch:2e762c49`, `c:souveraine:e3cf50bf`, `c:souveraine:d0272510`, `c:culver:c1088d2b`, `c:souveraine-player:0d693b9d` …</sub>
|
||||||
|
|
||||||
|
**f-0002 · unassigned · task 3 restated: Boot timing: fade splash → lock, no extra fixes**
|
||||||
|
What's left was rewritten under “Index”. diagnosed | EBUSY handoff race is the last timing bug; splash text killed, holder added
|
||||||
|
<sub>task-restated · evidence: `d:task-3`</sub>
|
||||||
|
|
||||||
|
**f-0003 · unassigned · task 7 restated: WCD9340 mic: functional-pinned; unpin + fidelity**
|
||||||
|
What's left was rewritten under “Index”. functional 07-20 | defect: power pin; kernel `mic-race-fix`
|
||||||
|
<sub>task-restated · evidence: `d:task-7`</sub>
|
||||||
|
|
||||||
|
**f-0006 · unassigned · task 12 entered the index: Face auth as a capability factor (Gaze reference)**
|
||||||
|
Filed under “Index”. open | per-target: phone needs front-cam bring-up
|
||||||
|
<sub>task-listed · evidence: `d:task-12`</sub>
|
||||||
|
|
||||||
|
**f-0013 · unassigned · task 19 entered the index: First-party Settings control center**
|
||||||
|
Filed under “Index”. in progress | guarded standalone launch + owned pages; absorbed TASK-22's polish sweep
|
||||||
|
<sub>task-listed · evidence: `d:task-19`</sub>
|
||||||
|
|
||||||
|
**f-0015 · unassigned · task 21 entered the index: Reboot-to-Android confirmation**
|
||||||
|
Filed under “Index”. open | scoped 07-22; one-tap irreversible slot flip still bare in app search
|
||||||
|
<sub>task-listed · evidence: `d:task-21`</sub>
|
||||||
|
|
||||||
|
**f-0021 · unassigned · task 29 entered the index: SouveraineOS Updater**
|
||||||
|
Filed under “Index”. started 07-25 | rust rewrite of pachub; doesn't build yet; first repo to build *into* the pipeline
|
||||||
|
<sub>task-listed · evidence: `d:task-29`</sub>
|
||||||
|
|
||||||
|
## · unchanged
|
||||||
|
|
||||||
|
**f-0049 · adoption · adoption did not move**
|
||||||
|
No commits, and no week before this one to compare against.
|
||||||
|
<sub>dimension-activity · evidence: `w:2026-W30`</sub>
|
||||||
|
|
||||||
|
**f-0034 · reliability · Pixel3Arch CI mixed**
|
||||||
|
On Pixel3Arch, 3 of 5 runs failed.
|
||||||
|
<sub>ci-week · evidence: `ci:Pixel3Arch:930`, `ci:Pixel3Arch:909`, `ci:Pixel3Arch:911`, `ci:Pixel3Arch:929`</sub>
|
||||||
|
|
||||||
|
**f-0035 · reliability · culver CI mixed**
|
||||||
|
On culver, 13 of 14 runs failed.
|
||||||
|
<sub>ci-week · evidence: `ci:culver:862`, `ci:culver:813`, `ci:culver:815`, `ci:culver:816`, `ci:culver:817`, `ci:culver:818`</sub>
|
||||||
|
|
||||||
|
**f-0037 · reliability · souveraine CI mixed**
|
||||||
|
On souveraine, 10 of 71 runs failed and 21 never concluded.
|
||||||
|
<sub>ci-week · evidence: `ci:souveraine:931`, `ci:souveraine:839`, `ci:souveraine:840`, `ci:souveraine:842`, `ci:souveraine:843`, `ci:souveraine:844`</sub>
|
||||||
|
|
||||||
|
## Standing longest without moving
|
||||||
|
|
||||||
|
2 task(s) sat in their section untouched this week. The ten that have sat longest:
|
||||||
|
|
||||||
|
- **f-0001 · surface · task 2: Lockscreen-as-Rust-system: glance + swipe** — “Index” for 1 week(s), since 2026-W30.
|
||||||
|
- **f-0005 · unassigned · task 9: Suspend-resume FTS calibration race** — “Index” for 1 week(s), since 2026-W30.
|
||||||
|
|
||||||
|
## Provenance
|
||||||
|
|
||||||
|
- Evidence: `project-state/evidence/2026-W30.json`, collected 2026-08-18T22:46:26Z by `tools/project-state/collect.py` v1 against gitea 1.25.5 in 101 API calls.
|
||||||
|
- Deterministic rules: ci-week, dimension-activity, release-cut, task-delisted, task-listed, task-restated, task-standing.
|
||||||
|
- Analyst: none run; every finding here is deterministic.
|
||||||
|
- Trace one: `tools/project-state/synthesize.py --explain <id>`.
|
||||||
281
project-state/reports/2026-W31.md
Normal file
281
project-state/reports/2026-W31.md
Normal file
|
|
@ -0,0 +1,281 @@
|
||||||
|
# Souveraine project state — 2026-W31
|
||||||
|
|
||||||
|
2026-07-27 to 2026-08-02 (America/Toronto). Internal. Generated from `project-state/evidence/2026-W31.json`; the prose is disposable, the evidence and `state.json` are not.
|
||||||
|
|
||||||
|
## The week, counted
|
||||||
|
|
||||||
|
- 287 commits across 9 repos, 133 CI runs, 51 declared state changes.
|
||||||
|
- Issues: 6 open, 0 filed, 0 closed. Pulls: 2 open, 0 merged.
|
||||||
|
- Dimension basis: 204 by paths, 45 by repo, 30 by subject, 8 by none (`paths` read the diff, `subject` read the commit subject, `repo` fell back to the repository, `none` placed nothing).
|
||||||
|
|
||||||
|
## ◆ milestone
|
||||||
|
|
||||||
|
**f-0027 · identity · task 35 left the index: sessiond answers nothing: handler threads park forever**
|
||||||
|
Dropped from “Index” — closed work moves to archive/.
|
||||||
|
<sub>task-delisted · open 2 weeks since 2026-W30 · was advanced · evidence: `d:task-35`</sub>
|
||||||
|
|
||||||
|
**f-0070 · release · Pixel3Arch released kernel-latest**
|
||||||
|
2 assets published 2026-07-28.
|
||||||
|
<sub>release-cut · evidence: `r:Pixel3Arch:kernel-latest`</sub>
|
||||||
|
|
||||||
|
**f-0007 · sensorium · task 13 left the index: Active Edge (squeeze) as a sensor input**
|
||||||
|
Dropped from “Index” — closed work moves to archive/.
|
||||||
|
<sub>task-delisted · open 2 weeks since 2026-W30 · was advanced · evidence: `d:task-13`</sub>
|
||||||
|
|
||||||
|
## ▼ regressed
|
||||||
|
|
||||||
|
**f-0021 · adoption · task 29 moved: SouveraineOS Updater**
|
||||||
|
Index → Live. Ships and installs by `pacman -Syu`. **Read-only on device** until something runs a polkit agent — that is the shell's job. Also owns TASK-42's layer 2.
|
||||||
|
<sub>task-moved · open 2 weeks since 2026-W30 · was advanced · evidence: `d:task-29`</sub>
|
||||||
|
|
||||||
|
**f-0016 · federation · task 23 moved: culver locked-notification nudge**
|
||||||
|
Index → Open. Needs `SyncBurst()` + an IdleCoordinator schedule (TASK-15).
|
||||||
|
<sub>task-moved · open 2 weeks since 2026-W30 · was advanced · evidence: `d:task-23`</sub>
|
||||||
|
|
||||||
|
**f-0004 · identity · task 8 moved: Device state manager / power profiles**
|
||||||
|
Index → In progress. Largest task; sub-divide before starting. (e) lease missing, (f) **surfaced 2026-07-31** — health, evidence and the decision trail render on the Device page; last-seen and grip still absent from `device_state`. (g) confidence gates need a d
|
||||||
|
<sub>task-moved · open 2 weeks since 2026-W30 · was decision required · evidence: `d:task-8`</sub>
|
||||||
|
|
||||||
|
**f-0009 · identity · task 15 moved: Idle coordinator**
|
||||||
|
Index → Blocked or gated. Freezing `app.slice` would freeze sessiond, shell and secrets. Needs slice separation first (measured 07-25).
|
||||||
|
<sub>task-moved · open 2 weeks since 2026-W30 · was advanced · evidence: `d:task-15`</sub>
|
||||||
|
|
||||||
|
**f-0014 · release · task 20 moved: Player: video + agent control**
|
||||||
|
Index → Open. Video, snapshot, manifest. Preserve the music path.
|
||||||
|
<sub>task-moved · open 2 weeks since 2026-W30 · was advanced · evidence: `d:task-20`</sub>
|
||||||
|
|
||||||
|
**f-0018 · release · task 25 moved: One repo, all packages**
|
||||||
|
Index → Blocked or gated. Parked 07-25 — four blockers before `repo-add --sign`. See TASK-27.
|
||||||
|
<sub>task-moved · open 2 weeks since 2026-W30 · was advanced · evidence: `d:task-25`</sub>
|
||||||
|
|
||||||
|
**f-0019 · release · task 27 moved: Gitea pipeline audit**
|
||||||
|
Index → Open. The fixes. Overlay is not a package; no `primary` branch; poisoned build sysroot.
|
||||||
|
<sub>task-moved · open 2 weeks since 2026-W30 · was advanced · evidence: `d:task-27`</sub>
|
||||||
|
|
||||||
|
**f-0006 · sensorium · task 12 moved: Face auth as a capability factor**
|
||||||
|
Index → Open. Per-target. Phone needs front-camera bring-up — **that is TASK-46's IMX355 half.**
|
||||||
|
<sub>task-moved · open 2 weeks since 2026-W30 · was advanced · evidence: `d:task-12`</sub>
|
||||||
|
|
||||||
|
**f-0025 · sensorium · task 33 moved: Battery belongs to the state machine**
|
||||||
|
Index → Open. Last raw feed going straight to the glass. The charge ceiling is writable today.
|
||||||
|
<sub>task-moved · open 2 weeks since 2026-W30 · was advanced · evidence: `d:task-33`</sub>
|
||||||
|
|
||||||
|
**f-0026 · sensorium · task 34 moved: SLPI FastRPC session wedge**
|
||||||
|
Index → Open. Re-diagnose. The real symptom: a DSP session survives its host daemon's death and only a cold boot clears it. **No longer blocks grip.**
|
||||||
|
<sub>task-moved · open 2 weeks since 2026-W30 · was advanced · evidence: `d:task-34`</sub>
|
||||||
|
|
||||||
|
**f-0001 · surface · task 2 moved: Lockscreen-as-Rust-system**
|
||||||
|
Index → In progress. sessiond's fallback should render the glance, not a second PIN pad. Stopgap shipped 07-17.
|
||||||
|
<sub>task-moved · open 2 weeks since 2026-W30 · was unchanged · evidence: `d:task-2`</sub>
|
||||||
|
|
||||||
|
**f-0008 · surface · task 14 moved: Overview → phone app grid**
|
||||||
|
Index → In progress. Paged app grid replacing the desktop workspace pane; Auxo-style running cards; unconditional Home path.
|
||||||
|
<sub>task-moved · open 2 weeks since 2026-W30 · was advanced · evidence: `d:task-14`</sub>
|
||||||
|
|
||||||
|
**f-0010 · surface · task 16 moved: culver: messenger + contacts**
|
||||||
|
Index → In progress. A/B/D landed. C (contact card) builds and awaits phone acceptance. F is the folded-in TASK-01.
|
||||||
|
<sub>task-moved · open 2 weeks since 2026-W30 · was advanced · evidence: `d:task-16`</sub>
|
||||||
|
|
||||||
|
**f-0011 · surface · task 17 moved: Keyboard + dictation polish**
|
||||||
|
Index → Open. No word completion (stevia asks hunspell for `en-us`, the dict is `en_US`); no recording indicator on the mic key. The layout half moved to TASK-24. **TTS now exists** (`10.10.20.123:7863`) — the settings field can be filled.
|
||||||
|
<sub>task-moved · open 2 weeks since 2026-W30 · was advanced · evidence: `d:task-17`</sub>
|
||||||
|
|
||||||
|
**f-0013 · surface · task 19 moved: First-party Settings control center**
|
||||||
|
Index → In progress. State-machine + source-health readout **built 2026-07-31**, unverified on glass. 34 and 42's layer 3 still surface here.
|
||||||
|
<sub>task-moved · open 2 weeks since 2026-W30 · was advanced · evidence: `d:task-19`</sub>
|
||||||
|
|
||||||
|
**f-0017 · surface · task 24 moved: Keyboard layout rethink**
|
||||||
|
Index → Open. Redesign from scratch, not more patches. Start from squeekboard `us+apple`. Casey decides where the mic key goes.
|
||||||
|
<sub>task-moved · open 2 weeks since 2026-W30 · was advanced · evidence: `d:task-24`</sub>
|
||||||
|
|
||||||
|
**f-0022 · surface · task 30 moved: Agent reach: verb tables**
|
||||||
|
Index → In progress. sessiond has describe + refusal codes + intent. Dock, apps, shell and settings do not. One session per surface.
|
||||||
|
<sub>task-moved · open 2 weeks since 2026-W30 · was advanced · evidence: `d:task-30`</sub>
|
||||||
|
|
||||||
|
**f-0023 · surface · task 31 moved: The radial dial**
|
||||||
|
Index → In progress. Component, gestures and squeeze invocation are live. **Entries are still a list in `DialHost.qml`** — they must come from the verb tables.
|
||||||
|
<sub>task-moved · open 2 weeks since 2026-W30 · was advanced · evidence: `d:task-31`</sub>
|
||||||
|
|
||||||
|
**f-0024 · surface · task 32 moved: Pill and keyboard as owned apps**
|
||||||
|
Index → Open. Both erroneous daily; neither has an owner or a shipping story.
|
||||||
|
<sub>task-moved · open 2 weeks since 2026-W30 · was advanced · evidence: `d:task-32`</sub>
|
||||||
|
|
||||||
|
**f-0002 · unassigned · task 3 moved: Boot timing: splash → lock**
|
||||||
|
Index → Open. The EBUSY handoff race is the last timing bug. Splash text killed, holder added.
|
||||||
|
<sub>task-moved · open 2 weeks since 2026-W30 · was advanced · evidence: `d:task-3`</sub>
|
||||||
|
|
||||||
|
**f-0003 · unassigned · task 7 moved: WCD9340 mic**
|
||||||
|
Index → Open. Functional since 07-20. One defect: the power pin that makes it work. Kernel branch `mic-race-fix`.
|
||||||
|
<sub>task-moved · open 2 weeks since 2026-W30 · was advanced · evidence: `d:task-7`</sub>
|
||||||
|
|
||||||
|
**f-0005 · unassigned · task 9 moved: Suspend-resume FTS calibration race**
|
||||||
|
Index → Open. Kernel-side real fix; interim fix shipped. Cold-boot only.
|
||||||
|
<sub>task-moved · open 2 weeks since 2026-W30 · was unchanged · evidence: `d:task-9`</sub>
|
||||||
|
|
||||||
|
**f-0012 · unassigned · task 18 moved: Selection/highlight action menu**
|
||||||
|
Index → Open. Component first, fluid before functional. **Read Aloud is unblocked** — TTS is live.
|
||||||
|
<sub>task-moved · open 2 weeks since 2026-W30 · was advanced · evidence: `d:task-18`</sub>
|
||||||
|
|
||||||
|
**f-0015 · unassigned · task 21 moved: Reboot-to-Android confirmation**
|
||||||
|
Index → Open. Scoped 07-22. A one-tap irreversible slot flip still sits bare in app search.
|
||||||
|
<sub>task-moved · open 2 weeks since 2026-W30 · was advanced · evidence: `d:task-21`</sub>
|
||||||
|
|
||||||
|
**f-0020 · unassigned · task 28 moved: Handover on upgrade**
|
||||||
|
Index → Blocked or gated. TASK-27's packaging. Also owns TASK-13's outstanding cold-boot handshake check.
|
||||||
|
<sub>task-moved · open 2 weeks since 2026-W30 · was advanced · evidence: `d:task-28`</sub>
|
||||||
|
|
||||||
|
## ■ blocked
|
||||||
|
|
||||||
|
**f-0054 · runtime · task 40 entered the index: Inference tiering for the sensors**
|
||||||
|
Filed under “Blocked or gated”. Nothing — **it is the gate.** P1 applied to sensord/grip/dial; gates the grip raw stream and TASK-31's IPC. Decisions first, then small code.
|
||||||
|
<sub>task-listed · evidence: `d:task-40`</sub>
|
||||||
|
|
||||||
|
## ? decision
|
||||||
|
|
||||||
|
**f-0069 · identity · souveraine#1 unmerged 17d: machined: system-tier machine identity daemon**
|
||||||
|
Opened 2026-07-16, never merged or closed.
|
||||||
|
<sub>pull-aging · evidence: `p:souveraine:1`</sub>
|
||||||
|
|
||||||
|
**f-0058 · sensorium · task 44 entered the index: Fingerprint (FPC1020)**
|
||||||
|
Filed under “Open”. Piece 1 (wake source / input) is a session — DT + shim already extracted. Piece 2 (biometrics) is a TrustZone question, answer it before scheduling it.
|
||||||
|
<sub>task-listed · evidence: `d:task-44`</sub>
|
||||||
|
|
||||||
|
## ▲ advanced
|
||||||
|
|
||||||
|
**f-0049 · adoption · adoption moved**
|
||||||
|
1 commit(s), 0 the week before. Declared: moved SouveraineOS Updater.
|
||||||
|
<sub>dimension-activity · open 2 weeks since 2026-W30 · was unchanged · evidence: `c:Pixel3Arch:3090440a`, `d:task-29`</sub>
|
||||||
|
|
||||||
|
**f-0048 · federation · federation moved**
|
||||||
|
0 commit(s), 2 the week before. Declared: moved culver locked-notification nudge.
|
||||||
|
<sub>dimension-activity · open 2 weeks since 2026-W30 · evidence: `d:task-23`</sub>
|
||||||
|
|
||||||
|
**f-0043 · identity · identity moved**
|
||||||
|
25 commit(s), 35 the week before. Declared: moved Device state manager / power profiles; moved Idle coordinator; delisted sessiond answers nothing: handler threads park forever.
|
||||||
|
<sub>dimension-activity · open 2 weeks since 2026-W30 · evidence: `c:souveraine:3d934005`, `c:souveraine:4ed8f3dd`, `c:souveraine-viewtop:c6f74d0f`, `c:souveraine-viewtop:d1b7a16f`, `c:souveraine:c4873762`, `c:souveraine-viewtop:3ef1bc43`, `c:souveraine:c8507e92`, `c:Pixel3Arch:230d7dd7` …</sub>
|
||||||
|
|
||||||
|
**f-0042 · reflection · reflection moved**
|
||||||
|
1 commit(s), 1 the week before. Declared: added docs/SHELL-SURFACES.md.
|
||||||
|
<sub>dimension-activity · open 2 weeks since 2026-W30 · evidence: `c:souveraine:15af38ae`, `d:docs/SHELL-SURFACES.md`</sub>
|
||||||
|
|
||||||
|
**f-0046 · release · release moved**
|
||||||
|
30 commit(s), 32 the week before. Declared: moved Player: video + agent control; moved One repo, all packages; moved Gitea pipeline audit; added docs/DUMP-packages-publish-2026-07-30.md.
|
||||||
|
<sub>dimension-activity · open 2 weeks since 2026-W30 · evidence: `c:Pixel3Arch:84adb458`, `c:Pixel3Arch:c8b0287b`, `c:Pixel3Arch:a07801b5`, `c:Pixel3Arch:ab09c5ea`, `c:Pixel3Arch:1561d216`, `c:Pixel3Arch:2dbe9c4f`, `c:Pixel3Arch:b4a45ff9`, `c:Pixel3Arch:c18be31f` …</sub>
|
||||||
|
|
||||||
|
**f-0038 · reliability · souveraine-updater CI clean**
|
||||||
|
1 runs on souveraine-updater, none failed.
|
||||||
|
<sub>ci-week · open 2 weeks since 2026-W30 · evidence: `ci:souveraine-updater:996`</sub>
|
||||||
|
|
||||||
|
**f-0066 · reliability · hexagonrpc CI clean**
|
||||||
|
6 runs on hexagonrpc, none failed.
|
||||||
|
<sub>ci-week · evidence: `ci:hexagonrpc:962`</sub>
|
||||||
|
|
||||||
|
**f-0067 · reliability · smithay CI clean**
|
||||||
|
2 runs on smithay, none failed.
|
||||||
|
<sub>ci-week · evidence: `ci:smithay:1066`</sub>
|
||||||
|
|
||||||
|
**f-0041 · runtime · runtime moved**
|
||||||
|
12 commit(s), 15 the week before. Declared: listed Inference tiering for the sensors; listed Camera: **the front camera works**; added docs/substrate/VPN-WIUF.md.
|
||||||
|
<sub>dimension-activity · open 2 weeks since 2026-W30 · evidence: `c:hexagonrpc:37437453`, `c:SouveraineOS:04831db5`, `c:SouveraineOS:4287d876`, `c:souveraine-speech:48b5caa5`, `c:souveraine:e9bf33e6`, `c:SouveraineOS:051da9e0`, `c:souveraine-speech:f10e34b9`, `c:souveraine:426dcf63` …</sub>
|
||||||
|
|
||||||
|
**f-0045 · sensorium · sensorium moved**
|
||||||
|
89 commit(s), 47 the week before. Declared: moved Face auth as a capability factor; delisted Active Edge (squeeze) as a sensor input; moved Battery belongs to the state machine; moved SLPI FastRPC session wedge.
|
||||||
|
<sub>dimension-activity · open 2 weeks since 2026-W30 · evidence: `c:hexagonrpc:6c0fa9cb`, `c:hexagonrpc:55c0d186`, `c:hexagonrpc:c314f76f`, `c:hexagonrpc:8d76b8f5`, `c:hexagonrpc:0aa66cf8`, `c:hexagonrpc:c875a342`, `c:SouveraineOS:3abf45a5`, `c:SouveraineOS:ccf02e3b` …</sub>
|
||||||
|
|
||||||
|
**f-0044 · surface · surface moved**
|
||||||
|
108 commit(s), 62 the week before. Declared: moved Lockscreen-as-Rust-system; moved Overview → phone app grid; moved culver: messenger + contacts; moved Keyboard + dictation polish.
|
||||||
|
<sub>dimension-activity · open 2 weeks since 2026-W30 · evidence: `c:souveraine:53dcb40c`, `c:SouveraineOS:78c62a60`, `c:souveraine:2170ba68`, `c:souveraine:deda4a7b`, `c:souveraine:68b60252`, `c:souveraine-viewtop:36b6b2e7`, `c:souveraine-viewtop:e1c93a5e`, `c:souveraine-viewtop:d1a9d73e` …</sub>
|
||||||
|
|
||||||
|
**f-0057 · surface · task 43 entered the index: viewtop: the Souveraine compositor**
|
||||||
|
Filed under “Live”. Runs on the phone. Next lane is the ext-session-lock admission gate. The two-client lock handoff is the thing not to get wrong.
|
||||||
|
<sub>task-listed · evidence: `d:task-43`</sub>
|
||||||
|
|
||||||
|
**f-0056 · unassigned · task 42 entered the index: Build outcomes are invisible**
|
||||||
|
Filed under “In progress”. Layer 1 built (`tools/ci-status.sh` + push hook). Layer 2 is TASK-29's, layer 3 is TASK-19's. Pin-rot prevention unbuilt.
|
||||||
|
<sub>task-listed · evidence: `d:task-42`</sub>
|
||||||
|
|
||||||
|
## · unchanged
|
||||||
|
|
||||||
|
**f-0040 · memory · memory moved**
|
||||||
|
2 commit(s), 1 the week before.
|
||||||
|
<sub>dimension-activity · open 2 weeks since 2026-W30 · was advanced · evidence: `c:linux-blueline:4d52867f`, `c:linux-blueline:a8bac15b`</sub>
|
||||||
|
|
||||||
|
**f-0034 · reliability · Pixel3Arch CI mixed**
|
||||||
|
On Pixel3Arch, 2 of 14 runs failed and 1 never concluded.
|
||||||
|
<sub>ci-week · open 2 weeks since 2026-W30 · evidence: `ci:Pixel3Arch:1085`, `ci:Pixel3Arch:992`, `ci:Pixel3Arch:1035`</sub>
|
||||||
|
|
||||||
|
**f-0037 · reliability · souveraine CI mixed**
|
||||||
|
On souveraine, 17 of 65 runs failed and 11 never concluded.
|
||||||
|
<sub>ci-week · open 2 weeks since 2026-W30 · evidence: `ci:souveraine:1089`, `ci:souveraine:934`, `ci:souveraine:937`, `ci:souveraine:942`, `ci:souveraine:950`, `ci:souveraine:952`</sub>
|
||||||
|
|
||||||
|
**f-0047 · reliability · reliability moved**
|
||||||
|
11 commit(s), 12 the week before.
|
||||||
|
<sub>dimension-activity · open 2 weeks since 2026-W30 · was advanced · evidence: `c:souveraine:7eb21359`, `c:Pixel3Arch:84673db7`, `c:souveraine:25d1e192`, `c:souveraine:684d1f9f`, `c:souveraine:21b602a7`, `c:souveraine-speech:19c0e307`, `c:souveraine:3bc363a9`, `c:souveraine-updater:1540559a`</sub>
|
||||||
|
|
||||||
|
**f-0068 · reliability · souveraine-viewtop CI mixed**
|
||||||
|
On souveraine-viewtop, 10 of 45 runs failed.
|
||||||
|
<sub>ci-week · evidence: `ci:souveraine-viewtop:1090`, `ci:souveraine-viewtop:954`, `ci:souveraine-viewtop:955`, `ci:souveraine-viewtop:958`, `ci:souveraine-viewtop:991`, `ci:souveraine-viewtop:1005`</sub>
|
||||||
|
|
||||||
|
**f-0060 · runtime · task 46 entered the index: Camera: **the front camera works****
|
||||||
|
Filed under “Open”. **First frame captured 2026-07-29** on the running kernel — no reboot, no DT change. Both front IMX355 were already bound; nobody had tried streaming. Recipe + the stride/packing gotchas are in the task. Next: exposure controls, then libcam
|
||||||
|
<sub>task-listed · evidence: `d:task-46`</sub>
|
||||||
|
|
||||||
|
**f-0050 · sensorium · task 36 entered the index: CHRE nanoapp host for the SLPI**
|
||||||
|
Filed under “Open”. Three methods to add; method table recovered 07-27. No longer on grip's critical path — wanted for DozeDeep detection and `elmyra_haptics_control`.
|
||||||
|
<sub>task-listed · evidence: `d:task-36`</sub>
|
||||||
|
|
||||||
|
**f-0053 · sensorium · task 39 entered the index: Audio levels as tracked state**
|
||||||
|
Filed under “Open”. Defaults + roles, **not** a save bug — WirePlumber persistence measured working 07-28. Do not add a second writer.
|
||||||
|
<sub>task-listed · evidence: `d:task-39`</sub>
|
||||||
|
|
||||||
|
**f-0059 · sensorium · task 45 entered the index: Haptics (CS40L20)**
|
||||||
|
Filed under “Open”. **First: which chip actually drives the motor** — `pmi8998_haptics` is what the dial's detents use today. Then port `cs40l2x` from Kirisakura; firmware blobs are committed.
|
||||||
|
<sub>task-listed · evidence: `d:task-45`</sub>
|
||||||
|
|
||||||
|
**f-0061 · sensorium · task 47 entered the index: Auto-brightness: anchors, none of it built**
|
||||||
|
Filed under “Open”. §12 reads as settled and has **zero** code behind its actuation half — no brightness `Action`, no anchors, `grep anchor src/` is empty. The evidence half (lux in, health tracked) is live.
|
||||||
|
<sub>task-listed · evidence: `d:task-47`</sub>
|
||||||
|
|
||||||
|
**f-0051 · surface · task 37 entered the index: "Back" as a first-class verb**
|
||||||
|
Filed under “Open”. Our `Gestures.qml` is already the compositor half; `can_go_back` is the dial's enabled/reason.
|
||||||
|
<sub>task-listed · evidence: `d:task-37`</sub>
|
||||||
|
|
||||||
|
**f-0052 · surface · task 38 entered the index: Pill swipe-to-max swaps the keyboard**
|
||||||
|
Filed under “Open”. Micro. The only route to a terminal keyboard since stevia dropped its layout.
|
||||||
|
<sub>task-listed · evidence: `d:task-38`</sub>
|
||||||
|
|
||||||
|
**f-0055 · surface · task 41 entered the index: Producers must be attested**
|
||||||
|
Filed under “Open”. Design first. Demonstrated live 07-28 — a squeeze and a dial-open both accepted from an SSH shell.
|
||||||
|
<sub>task-listed · evidence: `d:task-41`</sub>
|
||||||
|
|
||||||
|
**f-0062 · surface · task 48 entered the index: Shell dies on any reload that owes a lock**
|
||||||
|
Filed under “Open”. Fully diagnosed 07-29, 11 crashes since 07-17. A reload re-requests ext-session-lock while the outgoing instance still holds it → `FATAL: Tried to show lockscreen surfaces without active lock`. **Sync with TASK-43 before touching the lock p
|
||||||
|
<sub>task-listed · evidence: `d:task-48`</sub>
|
||||||
|
|
||||||
|
**f-0063 · surface · task 50 entered the index: Her hand on the glass**
|
||||||
|
Filed under “Open”. Every scene intent reaches the compositor now **except touch** — so she can move, scale, dim and close a window and cannot press anything inside one. The receiving end is built (`Origin::Agent`, the disjoint slot namespace, the evidence pre
|
||||||
|
<sub>task-listed · evidence: `d:task-50`</sub>
|
||||||
|
|
||||||
|
**f-0064 · surface · task 51 entered the index: viewtop to daily driver**
|
||||||
|
Filed under “Open”. The punch list between *runs* and *stable*, all of it found by using the phone: windows stack with nothing to tell them apart, screen capture is written and unmerged (so no UI change can be verified), volume buttons are recognised and dropp
|
||||||
|
<sub>task-listed · evidence: `d:task-51`</sub>
|
||||||
|
|
||||||
|
**f-0065 · surface · task 52 entered the index: Atmosphere**
|
||||||
|
Filed under “Open”. Attention as a compositor capability — the fifteen-technique vocabulary. Two are **already landed** (`expose` is negative space, `pose` is gravity wells), and `compile_custom_pixel_shader` + `PixelShaderElement` are already in our smithay,
|
||||||
|
<sub>task-listed · evidence: `d:task-52`</sub>
|
||||||
|
|
||||||
|
## Closed this week
|
||||||
|
|
||||||
|
- **f-0033 · task 11 left the index: souveraine-secrets on the phone** — held from 2026-W30 to 2026-W30, 1 week(s).
|
||||||
|
- **f-0030 · task 5 left the index: Crash reporter surfacing** — held from 2026-W30 to 2026-W30, 1 week(s).
|
||||||
|
- **f-0029 · task 4 left the index: Notification server (org.freedesktop.Notifications)** — held from 2026-W30 to 2026-W30, 1 week(s).
|
||||||
|
- **f-0028 · task 1 left the index: Favorites — wife's name on her number** — held from 2026-W30 to 2026-W30, 1 week(s).
|
||||||
|
- **f-0031 · task 6 left the index: qtpim aarch64 build → contacts store** — held from 2026-W30 to 2026-W30, 1 week(s).
|
||||||
|
- **f-0032 · task 10 left the index: Boot chain: pmOS init → mkinitcpio** — held from 2026-W30 to 2026-W30, 1 week(s).
|
||||||
|
|
||||||
|
## Provenance
|
||||||
|
|
||||||
|
- Evidence: `project-state/evidence/2026-W31.json`, collected 2026-08-18T22:46:32Z by `tools/project-state/collect.py` v1 against gitea 1.25.5 in 99 API calls.
|
||||||
|
- Deterministic rules: ci-week, dimension-activity, pull-aging, release-cut, task-delisted, task-listed, task-moved.
|
||||||
|
- Analyst: none run; every finding here is deterministic.
|
||||||
|
- Trace one: `tools/project-state/synthesize.py --explain <id>`.
|
||||||
166
project-state/reports/2026-W32.md
Normal file
166
project-state/reports/2026-W32.md
Normal file
|
|
@ -0,0 +1,166 @@
|
||||||
|
# Souveraine project state — 2026-W32
|
||||||
|
|
||||||
|
2026-08-03 to 2026-08-09 (America/Toronto). Internal. Generated from `project-state/evidence/2026-W32.json`; the prose is disposable, the evidence and `state.json` are not.
|
||||||
|
|
||||||
|
## The week, counted
|
||||||
|
|
||||||
|
- 201 commits across 5 repos, 143 CI runs, 18 declared state changes.
|
||||||
|
- Issues: 6 open, 0 filed, 0 closed. Pulls: 2 open, 0 merged.
|
||||||
|
- Dimension basis: 158 by paths, 19 by subject, 14 by none, 10 by repo (`paths` read the diff, `subject` read the commit subject, `repo` fell back to the repository, `none` placed nothing).
|
||||||
|
|
||||||
|
## ▼ regressed
|
||||||
|
|
||||||
|
**f-0034 · reliability · Pixel3Arch ends the week red**
|
||||||
|
The last run of the week on Pixel3Arch failed; 12 of 36 runs failed.
|
||||||
|
<sub>ci-week · open 3 weeks since 2026-W30 · was unchanged · evidence: `ci:Pixel3Arch:1256`, `ci:Pixel3Arch:1137`, `ci:Pixel3Arch:1138`, `ci:Pixel3Arch:1139`, `ci:Pixel3Arch:1140`, `ci:Pixel3Arch:1141`</sub>
|
||||||
|
|
||||||
|
## ■ blocked
|
||||||
|
|
||||||
|
**f-0009 · identity · task 15: Idle coordinator**
|
||||||
|
Standing under “Blocked or gated”. Freezing `app.slice` would freeze sessiond, shell and secrets. Needs slice separation first (measured 07-25).
|
||||||
|
<sub>task-standing · open 3 weeks since 2026-W30 · was regressed · evidence: `d:task-15`</sub>
|
||||||
|
|
||||||
|
**f-0018 · release · task 25: One repo, all packages**
|
||||||
|
Standing under “Blocked or gated”. Parked 07-25 — four blockers before `repo-add --sign`. See TASK-27.
|
||||||
|
<sub>task-standing · open 3 weeks since 2026-W30 · was regressed · evidence: `d:task-25`</sub>
|
||||||
|
|
||||||
|
**f-0054 · runtime · task 40: Inference tiering for the sensors**
|
||||||
|
Standing under “Blocked or gated”. Nothing — **it is the gate.** P1 applied to sensord/grip/dial; gates the grip raw stream and TASK-31's IPC. Decisions first, then small code.
|
||||||
|
<sub>task-standing · open 2 weeks since 2026-W31 · evidence: `d:task-40`</sub>
|
||||||
|
|
||||||
|
**f-0020 · unassigned · task 28: Handover on upgrade**
|
||||||
|
Standing under “Blocked or gated”. TASK-27's packaging. Also owns TASK-13's outstanding cold-boot handshake check.
|
||||||
|
<sub>task-standing · open 3 weeks since 2026-W30 · was regressed · evidence: `d:task-28`</sub>
|
||||||
|
|
||||||
|
## ? decision
|
||||||
|
|
||||||
|
**f-0069 · identity · souveraine#1 unmerged 24d: machined: system-tier machine identity daemon**
|
||||||
|
Opened 2026-07-16, never merged or closed.
|
||||||
|
<sub>pull-aging · open 2 weeks since 2026-W31 · evidence: `p:souveraine:1`</sub>
|
||||||
|
|
||||||
|
**f-0077 · surface · task 60 entered the index: Multitasking is a place, not a transition**
|
||||||
|
Filed under “Live”. One-owner compositor and shell halves are built; strand repro and measured landing pass on glass. `16c6ee1` is pushed and CI 1236 is green. What remains is Casey's thumb on the settle/fade and a real split-zone card check.
|
||||||
|
<sub>task-listed · evidence: `d:task-60`</sub>
|
||||||
|
|
||||||
|
## ▲ advanced
|
||||||
|
|
||||||
|
**f-0043 · identity · identity moved**
|
||||||
|
14 commit(s), 25 the week before. Declared: listed viewtop audit: what it left open; listed Full USB KVM; added docs/DUMP-bugs-2026-08-06.md.
|
||||||
|
<sub>dimension-activity · open 3 weeks since 2026-W30 · evidence: `c:souveraine:6cb24549`, `c:souveraine:1f028c12`, `c:souveraine:91ea54eb`, `c:souveraine:133daa94`, `c:SouveraineOS:271e22cf`, `c:souveraine:bc094803`, `c:souveraine:425693a2`, `c:souveraine:620c7d31` …</sub>
|
||||||
|
|
||||||
|
**f-0071 · identity · task 53 entered the index: viewtop audit: what it left open**
|
||||||
|
Filed under “Live”. All ten fixed and **packaged onto the phone** (`r74.g6c9614c20471`). The cold boot then exposed a *third*: a lost sessiond race had left the power button dead — discovery waits now. Hardware found two more the gates did not — `delivered` re
|
||||||
|
<sub>task-listed · evidence: `d:task-53`</sub>
|
||||||
|
|
||||||
|
**f-0078 · identity · task 62 entered the index: Full USB KVM**
|
||||||
|
Filed under “In progress”. GUD + HID + NCM + smoo and the signed package path are green. Still open: install and cable/glass acceptance; real Session Authority gates (peer identity, probe lease, step-up, D-Bus enforcement); cold-boot SSH-over-NCM proof. Type-C/TCPM h
|
||||||
|
<sub>task-listed · evidence: `d:task-62`</sub>
|
||||||
|
|
||||||
|
**f-0067 · reliability · smithay CI clean**
|
||||||
|
2 runs on smithay, none failed.
|
||||||
|
<sub>ci-week · open 2 weeks since 2026-W31 · evidence: `ci:smithay:1207`</sub>
|
||||||
|
|
||||||
|
**f-0041 · runtime · runtime moved**
|
||||||
|
14 commit(s), 12 the week before. Declared: listed Her face on the glass: a Live2D presence; added docs/davids-tasks/slimbus-def-act-chan.md.
|
||||||
|
<sub>dimension-activity · open 3 weeks since 2026-W30 · evidence: `c:souveraine:1a3c24fa`, `c:SouveraineOS:ec552a82`, `c:souveraine:d4f9213a`, `c:souveraine:7f294a5f`, `c:souveraine:14c3c80b`, `c:Pixel3Arch:6a757981`, `c:Pixel3Arch:e8bb2bff`, `c:Pixel3Arch:b2a1d2c4` …</sub>
|
||||||
|
|
||||||
|
**f-0045 · sensorium · sensorium moved**
|
||||||
|
20 commit(s), 89 the week before. Declared: restated Audio levels as tracked state; listed Host power: the SMB2 OTG boost; added docs/DUMP-fingerprint-2026-08-09.md; revised docs/README.md.
|
||||||
|
<sub>dimension-activity · open 3 weeks since 2026-W30 · evidence: `c:souveraine:6b67512c`, `c:souveraine:fe48bb76`, `c:Pixel3Arch:9b1afd1b`, `c:SouveraineOS:4aa61d73`, `c:SouveraineOS:721b0c7a`, `c:Pixel3Arch:68dca16c`, `c:Pixel3Arch:f30063b5`, `c:Pixel3Arch:13888ed3` …</sub>
|
||||||
|
|
||||||
|
**f-0044 · surface · surface moved**
|
||||||
|
119 commit(s), 108 the week before. Declared: restated Keyboard + dictation polish; listed The hand's verbs: a window action sheet; listed The island, and the bar's honest face; listed The Pi dock head.
|
||||||
|
<sub>dimension-activity · open 3 weeks since 2026-W30 · evidence: `c:SouveraineOS:5c9a652d`, `c:souveraine-viewtop:ded525a7`, `c:souveraine-viewtop:243a8373`, `c:souveraine-viewtop:9bcc0138`, `c:souveraine:ca44b0be`, `c:souveraine-viewtop:24d743bf`, `c:souveraine:c21bd1d7`, `c:SouveraineOS:384cd945` …</sub>
|
||||||
|
|
||||||
|
## · unchanged
|
||||||
|
|
||||||
|
**f-0049 · adoption · adoption moved**
|
||||||
|
2 commit(s), 1 the week before.
|
||||||
|
<sub>dimension-activity · open 3 weeks since 2026-W30 · was advanced · evidence: `c:SouveraineOS:67923009`, `c:Pixel3Arch:39242795`</sub>
|
||||||
|
|
||||||
|
**f-0048 · federation · federation did not move**
|
||||||
|
A second quiet week.
|
||||||
|
<sub>dimension-activity · open 3 weeks since 2026-W30 · was advanced · evidence: `w:2026-W32`</sub>
|
||||||
|
|
||||||
|
**f-0040 · memory · memory moved**
|
||||||
|
3 commit(s), 2 the week before.
|
||||||
|
<sub>dimension-activity · open 3 weeks since 2026-W30 · evidence: `c:souveraine:0c9db098`, `c:souveraine:9ceaf61f`, `c:SouveraineOS:5cc4020c`</sub>
|
||||||
|
|
||||||
|
**f-0042 · reflection · reflection did not move**
|
||||||
|
No commits, after 1 the week before.
|
||||||
|
<sub>dimension-activity · open 3 weeks since 2026-W30 · was advanced · evidence: `w:2026-W32`</sub>
|
||||||
|
|
||||||
|
**f-0046 · release · release moved**
|
||||||
|
14 commit(s), 30 the week before.
|
||||||
|
<sub>dimension-activity · open 3 weeks since 2026-W30 · was advanced · evidence: `c:Pixel3Arch:f234928f`, `c:Pixel3Arch:dfb2763d`, `c:Pixel3Arch:aee40a07`, `c:Pixel3Arch:af17845b`, `c:Pixel3Arch:64495406`, `c:Pixel3Arch:a3d5df9a`, `c:SouveraineOS:71f6ce59`, `c:Pixel3Arch:258c995b`</sub>
|
||||||
|
|
||||||
|
**f-0037 · reliability · souveraine CI mixed**
|
||||||
|
On souveraine, 10 of 68 runs failed and 25 never concluded.
|
||||||
|
<sub>ci-week · open 3 weeks since 2026-W30 · evidence: `ci:souveraine:1259`, `ci:souveraine:1132`, `ci:souveraine:1158`, `ci:souveraine:1183`, `ci:souveraine:1184`, `ci:souveraine:1185`</sub>
|
||||||
|
|
||||||
|
**f-0047 · reliability · reliability moved**
|
||||||
|
1 commit(s), 11 the week before.
|
||||||
|
<sub>dimension-activity · open 3 weeks since 2026-W30 · evidence: `c:souveraine-viewtop:875e97ed`</sub>
|
||||||
|
|
||||||
|
**f-0068 · reliability · souveraine-viewtop CI mixed**
|
||||||
|
On souveraine-viewtop, 4 of 37 runs failed and 1 never concluded.
|
||||||
|
<sub>ci-week · open 2 weeks since 2026-W31 · evidence: `ci:souveraine-viewtop:1234`, `ci:souveraine-viewtop:1124`, `ci:souveraine-viewtop:1131`, `ci:souveraine-viewtop:1133`, `ci:souveraine-viewtop:1165`</sub>
|
||||||
|
|
||||||
|
**f-0076 · runtime · task 59 entered the index: Her face on the glass: a Live2D presence**
|
||||||
|
Filed under “Open”. **Phase 1 passed on hardware 2026-08-05: ~58 fps, rig verified drawing (`painted_px` 23.6k/24k), 283 MB WebKit RSS.** Scoped from a Live2D board-girl found in a cloned blog. The reference streams its chat over **SSE terminated by `[DONE]`**
|
||||||
|
<sub>task-listed · evidence: `d:task-59`</sub>
|
||||||
|
|
||||||
|
**f-0053 · sensorium · task 39 restated: Audio levels as tracked state**
|
||||||
|
What's left was rewritten under “Open”. Open policy: roles, defaults, lock tiers, and state-driven profile intent. WirePlumber persistence works; do not add a second writer.
|
||||||
|
<sub>task-restated · open 2 weeks since 2026-W31 · evidence: `d:task-39`</sub>
|
||||||
|
|
||||||
|
**f-0075 · sensorium · task 58 entered the index: Host power: the SMB2 OTG boost**
|
||||||
|
Filed under “Open”. Host data works; the boost isn't. The SMB2 5 V boost exists in silicon and mainline's `qcom-smbx-charger` never registers it. Investigation first (which tree builds the running kernel, does smb2 touch the boost, does `qcom_pmic_typec` bind
|
||||||
|
<sub>task-listed · evidence: `d:task-58`</sub>
|
||||||
|
|
||||||
|
**f-0011 · surface · task 17 restated: Keyboard + dictation polish**
|
||||||
|
What's left was rewritten under “Open”. Dictation works in daily use. Open: no word completion (`en-us` vs `en_US`) and the stateful mic-key indicator is written but not built. Layout moved to TASK-24. **TTS now exists** (`10.10.20.123:7863`).
|
||||||
|
<sub>task-restated · open 3 weeks since 2026-W30 · was regressed · evidence: `d:task-17`</sub>
|
||||||
|
|
||||||
|
**f-0072 · surface · task 55 entered the index: The hand's verbs: a window action sheet**
|
||||||
|
Filed under “Open”. **Sheet built and deployed 2026-08-05.** The tap carries its window now (the centroid was being dropped, which is why it could only reach the overview); `Kill` added because `Close` is a request a hung client never reads; `ViewtopControl.qm
|
||||||
|
<sub>task-listed · evidence: `d:task-55`</sub>
|
||||||
|
|
||||||
|
**f-0073 · surface · task 56 entered the index: The island, and the bar's honest face**
|
||||||
|
Filed under “Open”. Bar clears a region for sprawling notification banners, agent-controlled (Part A: design questions). Part B is buildable now: drop the LTE suffix, tap-to-cycle the carrier name (Fido → Casey → Fimeg), and a network-type indicator that shows
|
||||||
|
<sub>task-listed · evidence: `d:task-56`</sub>
|
||||||
|
|
||||||
|
**f-0074 · surface · task 57 entered the index: The Pi dock head**
|
||||||
|
Filed under “Open”. Phone stays a gadget; the Pi 4 drives the monitor + dock. NCM link is default, Venus encoder is present, compositor capture landed — the unproven seams are `zwlr_screencopy` vs our own capture IPC, and the Pi's own bring-up ("major malfunct
|
||||||
|
<sub>task-listed · evidence: `d:task-57`</sub>
|
||||||
|
|
||||||
|
**f-0003 · unassigned · task 7 restated: WCD9340 mic**
|
||||||
|
What's left was rewritten under “Open”. Functional and in daily STT use. One defect: the permanent sysclk + MICB2 power pin. Kernel branch `mic-race-fix`.
|
||||||
|
<sub>task-restated · open 3 weeks since 2026-W30 · was regressed · evidence: `d:task-7`</sub>
|
||||||
|
|
||||||
|
## Closed this week
|
||||||
|
|
||||||
|
- **f-0027 · task 35 left the index: sessiond answers nothing: handler threads park forever** — held from 2026-W30 to 2026-W31, 2 week(s).
|
||||||
|
- **f-0007 · task 13 left the index: Active Edge (squeeze) as a sensor input** — held from 2026-W30 to 2026-W31, 2 week(s).
|
||||||
|
|
||||||
|
## Standing longest without moving
|
||||||
|
|
||||||
|
38 task(s) sat in their section untouched this week. The ten that have sat longest:
|
||||||
|
|
||||||
|
- **f-0001 · surface · task 2: Lockscreen-as-Rust-system** — “In progress” for 2 week(s), since 2026-W31.
|
||||||
|
- **f-0002 · unassigned · task 3: Boot timing: splash → lock** — “Open” for 2 week(s), since 2026-W31.
|
||||||
|
- **f-0004 · identity · task 8: Device state manager / power profiles** — “In progress” for 2 week(s), since 2026-W31.
|
||||||
|
- **f-0005 · unassigned · task 9: Suspend-resume FTS calibration race** — “Open” for 2 week(s), since 2026-W31.
|
||||||
|
- **f-0006 · sensorium · task 12: Face auth as a capability factor** — “Open” for 2 week(s), since 2026-W31.
|
||||||
|
- **f-0008 · surface · task 14: Overview → phone app grid** — “In progress” for 2 week(s), since 2026-W31.
|
||||||
|
- **f-0010 · surface · task 16: culver: messenger + contacts** — “In progress” for 2 week(s), since 2026-W31.
|
||||||
|
- **f-0012 · unassigned · task 18: Selection/highlight action menu** — “Open” for 2 week(s), since 2026-W31.
|
||||||
|
- **f-0013 · surface · task 19: First-party Settings control center** — “In progress” for 2 week(s), since 2026-W31.
|
||||||
|
- **f-0014 · release · task 20: Player: video + agent control** — “Open” for 2 week(s), since 2026-W31.
|
||||||
|
|
||||||
|
## Provenance
|
||||||
|
|
||||||
|
- Evidence: `project-state/evidence/2026-W32.json`, collected 2026-08-18T22:46:38Z by `tools/project-state/collect.py` v1 against gitea 1.25.5 in 91 API calls.
|
||||||
|
- Deterministic rules: ci-week, dimension-activity, pull-aging, task-listed, task-restated, task-standing.
|
||||||
|
- Analyst: none run; every finding here is deterministic.
|
||||||
|
- Trace one: `tools/project-state/synthesize.py --explain <id>`.
|
||||||
201
project-state/reports/2026-W33.md
Normal file
201
project-state/reports/2026-W33.md
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
# Souveraine project state — 2026-W33
|
||||||
|
|
||||||
|
2026-08-10 to 2026-08-16 (America/Toronto). Internal. Generated from `project-state/evidence/2026-W33.json`; the prose is disposable, the evidence and `state.json` are not.
|
||||||
|
|
||||||
|
## The week, counted
|
||||||
|
|
||||||
|
- 260 commits across 7 repos, 185 CI runs, 37 declared state changes.
|
||||||
|
- Issues: 6 open, 6 filed, 0 closed. Pulls: 2 open, 0 merged.
|
||||||
|
- Dimension basis: 198 by paths, 29 by subject, 22 by repo, 11 by none (`paths` read the diff, `subject` read the commit subject, `repo` fell back to the repository, `none` placed nothing).
|
||||||
|
|
||||||
|
## ▼ regressed
|
||||||
|
|
||||||
|
**f-0037 · reliability · souveraine ends the week red**
|
||||||
|
The last run of the week on souveraine failed; 16 of 111 runs failed and 50 never concluded.
|
||||||
|
<sub>ci-week · open 4 weeks since 2026-W30 · was unchanged · evidence: `ci:souveraine:1658`, `ci:souveraine:1268`, `ci:souveraine:1307`, `ci:souveraine:1328`, `ci:souveraine:1400`, `ci:souveraine:1403`</sub>
|
||||||
|
|
||||||
|
## ■ blocked
|
||||||
|
|
||||||
|
**f-0009 · identity · task 15: Idle coordinator**
|
||||||
|
Standing under “Blocked or gated”. Freezing `app.slice` would freeze sessiond, shell and secrets. Needs slice separation first (measured 07-25).
|
||||||
|
<sub>task-standing · open 4 weeks since 2026-W30 · evidence: `d:task-15`</sub>
|
||||||
|
|
||||||
|
**f-0018 · release · task 25: One repo, all packages**
|
||||||
|
Standing under “Blocked or gated”. Parked 07-25 — four blockers before `repo-add --sign`. See TASK-27.
|
||||||
|
<sub>task-standing · open 4 weeks since 2026-W30 · evidence: `d:task-25`</sub>
|
||||||
|
|
||||||
|
**f-0054 · runtime · task 40: Inference tiering for the sensors**
|
||||||
|
Standing under “Blocked or gated”. Nothing — **it is the gate.** P1 applied to sensord/grip/dial; gates the grip raw stream and TASK-31's IPC. Decisions first, then small code.
|
||||||
|
<sub>task-standing · open 3 weeks since 2026-W31 · evidence: `d:task-40`</sub>
|
||||||
|
|
||||||
|
**f-0020 · unassigned · task 28: Handover on upgrade**
|
||||||
|
Standing under “Blocked or gated”. TASK-27's packaging. Also owns TASK-13's outstanding cold-boot handshake check.
|
||||||
|
<sub>task-standing · open 4 weeks since 2026-W30 · evidence: `d:task-28`</sub>
|
||||||
|
|
||||||
|
## ? decision
|
||||||
|
|
||||||
|
**f-0069 · identity · souveraine#1 unmerged 31d: machined: system-tier machine identity daemon**
|
||||||
|
Opened 2026-07-16, never merged or closed.
|
||||||
|
<sub>pull-aging · open 3 weeks since 2026-W31 · evidence: `p:souveraine:1`</sub>
|
||||||
|
|
||||||
|
**f-0095 · identity · souveraine#2 unmerged 19d: sessiond: squeeze is input, and a pocket can veto it**
|
||||||
|
Opened 2026-07-28, never merged or closed.
|
||||||
|
<sub>pull-aging · evidence: `p:souveraine:2`</sub>
|
||||||
|
|
||||||
|
**f-0083 · memory · task 67 entered the index: The caching landed blind, and compaction drifted**
|
||||||
|
Filed under “Open”. Prompt caching works — one turn shows `cache_write=22585` then `cache_read=22585` — and **nothing reads the counters**: `assistant_with_usage` has zero callers, so the only proof is a log line. Separately, the live config hands the subconsc
|
||||||
|
<sub>task-listed · evidence: `d:task-67`</sub>
|
||||||
|
|
||||||
|
**f-0088 · release · task 73 entered the index: One spine, two skins: the self-dashboard**
|
||||||
|
Filed under “Open”. Designed 2026-08-11, nothing built. RedFlag Desktop (Tauri, public) + house Gatehouse (wry + QML glance, lens skeleton), spine is agent-side pacman closure resolution that retires souveraine-updater's `pkexec --noconfirm`. Name and home-rep
|
||||||
|
<sub>task-listed · evidence: `d:task-73`</sub>
|
||||||
|
|
||||||
|
**f-0087 · sensorium · task 71 entered the index: QmlMobileScanner as a components app candidate**
|
||||||
|
Filed under “Open”. Recon 2026-08-11, verdict owed. GPL-3.0 Qt Quick scanner+writer (zxing-cpp/QZXing/zint, SQLite history). Compatible inside AGPL; camera gate is TASK-46; ffmpeg backend needs a freedreno check; fontawesome-qml likely **not** needed (no FontL
|
||||||
|
<sub>task-listed · evidence: `d:task-71`</sub>
|
||||||
|
|
||||||
|
## ▲ advanced
|
||||||
|
|
||||||
|
**f-0049 · adoption · adoption moved**
|
||||||
|
5 commit(s), 2 the week before. Declared: listed Provisioning belongs to a package.
|
||||||
|
<sub>dimension-activity · open 4 weeks since 2026-W30 · was unchanged · evidence: `c:souveraine-lens:c2830abc`, `c:SouveraineOS:6e2f3f80`, `c:souveraine-lens:0f34f557`, `c:souveraine-lens:1bbda8df`, `c:souveraine-lens:cbe8f672`, `d:task-76`</sub>
|
||||||
|
|
||||||
|
**f-0043 · identity · identity moved**
|
||||||
|
14 commit(s), 14 the week before. Declared: listed Shared household state: tasks first; added docs/SHARED-STATE-ARCHITECTURE.md; revised docs/session-authority-boot-order.md.
|
||||||
|
<sub>dimension-activity · open 4 weeks since 2026-W30 · evidence: `c:souveraine:c367a4db`, `c:souveraine:f4837c52`, `c:souveraine:715080bb`, `c:souveraine:c31fa10e`, `c:souveraine:af801b88`, `c:souveraine:0dd8aaab`, `c:souveraine:24086b35`, `c:SouveraineOS:4cd894a5` …</sub>
|
||||||
|
|
||||||
|
**f-0040 · memory · memory moved**
|
||||||
|
11 commit(s), 3 the week before. Declared: listed The caching landed blind, and compaction drifted.
|
||||||
|
<sub>dimension-activity · open 4 weeks since 2026-W30 · was unchanged · evidence: `c:SouveraineOS:f4683a6f`, `c:souveraine-lens:531e7e75`, `c:SouveraineOS:22adbe11`, `c:souveraine-lens:d0f2fabd`, `c:souveraine:ce6741bf`, `c:souveraine:e498e72b`, `c:SouveraineOS:3bf40c80`, `c:souveraine:aae9344f` …</sub>
|
||||||
|
|
||||||
|
**f-0042 · reflection · reflection moved**
|
||||||
|
7 commit(s), 0 the week before. Declared: listed One SouveraineOS build for the iPhone.
|
||||||
|
<sub>dimension-activity · open 4 weeks since 2026-W30 · was unchanged · evidence: `c:SouveraineOS:287acdff`, `c:souveraine:3175700e`, `c:souveraine:9149c409`, `c:souveraine:dfc72d72`, `c:souveraine:837e0242`, `c:souveraine:63ef8e5b`, `c:souveraine:4662fa1c`, `d:task-64`</sub>
|
||||||
|
|
||||||
|
**f-0046 · release · release moved**
|
||||||
|
38 commit(s), 14 the week before. Declared: listed One spine, two skins: the self-dashboard; listed The lens: a human-first garden over a git vault; revised docs/README.md; revised docs/archive/DUMP-pacman-pipeline-2026-07-24.md.
|
||||||
|
<sub>dimension-activity · open 4 weeks since 2026-W30 · was unchanged · evidence: `c:SouveraineOS:d1312cbb`, `c:SouveraineOS:6cc89ef2`, `c:SouveraineOS:a1931fd6`, `c:SouveraineOS:38409222`, `c:SouveraineOS:5cf7bbc5`, `c:SouveraineOS:fff06182`, `c:souveraine-viewtop:1bcbfa51`, `c:Pixel3Arch:b272d1b3` …</sub>
|
||||||
|
|
||||||
|
**f-0092 · release · task 77 entered the index: The lens: a human-first garden over a git vault**
|
||||||
|
Filed under “In progress”. Built + packaged 2026-08-15 — folder/subcategory support and inbox capture landed (`61cf534`), publisher chmod fix (`c71c7e4`). Left: lens CI green + `edge` asset published, `pacman -U` on the laptop, then edge-repo DB wiring.
|
||||||
|
<sub>task-listed · evidence: `d:task-77`</sub>
|
||||||
|
|
||||||
|
**f-0067 · reliability · smithay CI clean**
|
||||||
|
3 runs on smithay, none failed.
|
||||||
|
<sub>ci-week · open 3 weeks since 2026-W31 · evidence: `ci:smithay:1645`</sub>
|
||||||
|
|
||||||
|
**f-0093 · reliability · SouveraineOS CI clean**
|
||||||
|
7 runs on SouveraineOS, none failed.
|
||||||
|
<sub>ci-week · evidence: `ci:SouveraineOS:1555`</sub>
|
||||||
|
|
||||||
|
**f-0041 · runtime · runtime moved**
|
||||||
|
47 commit(s), 14 the week before. Declared: listed A stalled subconscious wedges the surface; listed souveraineai.com overhaul; listed Agent usage, one spine: the session daemon; added docs/FACE-VEIL-ASSETS.md.
|
||||||
|
<sub>dimension-activity · open 4 weeks since 2026-W30 · evidence: `c:souveraine:a10ef5e3`, `c:souveraine:dfc89d09`, `c:souveraine:eee26fcf`, `c:souveraine:30e1c34a`, `c:souveraine:6eda4914`, `c:Pixel3Arch:e06aa70b`, `c:souveraine-speech:f89f7f9d`, `c:souveraine-speech:f6ec2a28` …</sub>
|
||||||
|
|
||||||
|
**f-0081 · runtime · task 65 entered the index: A stalled subconscious wedges the surface**
|
||||||
|
Filed under “In progress”. Reproduced and measured, not diagnosed. The reply commits and `PrimaryComplete` fires, then the N+1 pass stalls at round 3 of 10 with a fully idle runtime — and because it runs inside `run_turn`, the SSE stream never closes and Surfaces spi
|
||||||
|
<sub>task-listed · evidence: `d:task-65`</sub>
|
||||||
|
|
||||||
|
**f-0082 · runtime · task 66 entered the index: souveraineai.com overhaul**
|
||||||
|
Filed under “Live”. Rework landed 2026-08-10, waiting on push. Nested Astro docs repo folded into `souveraine-website` with history; the static `public/*.html` shell archived, not deleted. Left: `astro build` green in the merged repo and the server rebuilding
|
||||||
|
<sub>task-listed · evidence: `d:task-66`</sub>
|
||||||
|
|
||||||
|
**f-0045 · sensorium · sensorium moved**
|
||||||
|
15 commit(s), 20 the week before. Declared: listed QmlMobileScanner as a components app candidate; listed LibrePods: an admitted accessory; revised docs/AUDIO-PRIVACY.md; revised docs/DEVICE-STATE-MACHINE.md.
|
||||||
|
<sub>dimension-activity · open 4 weeks since 2026-W30 · evidence: `c:SouveraineOS:9b54eb41`, `c:Pixel3Arch:5927aa7f`, `c:SouveraineOS:a1c184d3`, `c:Pixel3Arch:f52a57b9`, `c:souveraine:29ec9fe2`, `c:souveraine-speech:ea43970b`, `c:souveraine-speech:be9219dc`, `c:Pixel3Arch:b2281f02` …</sub>
|
||||||
|
|
||||||
|
**f-0044 · surface · surface moved**
|
||||||
|
105 commit(s), 119 the week before. Declared: restated Squeekboard + dictation polish; restated Keyboard layout rethink; restated Pill and keyboard as owned apps; restated Pill swipe-to-max swaps the keyboard.
|
||||||
|
<sub>dimension-activity · open 4 weeks since 2026-W30 · evidence: `c:souveraine-viewtop:72608e91`, `c:smithay:e57116a0`, `c:souveraine-viewtop:f49af4af`, `c:souveraine:902a7248`, `c:souveraine:658217e7`, `c:souveraine:cb7dd73d`, `c:souveraine:28ed6f21`, `c:souveraine:ec87e6a9` …</sub>
|
||||||
|
|
||||||
|
**f-0079 · surface · task 63 entered the index: The second body: SouveraineOS on the iPhone 7**
|
||||||
|
Filed under “In progress”. **The shell is on the glass 2026-08-09** — viewtop, sessiond and `qs -c souveraine` all run on a T8010 with no GPU, drawn by llvmpipe, over one USB cable. Hoolock is the bootloader and nothing else. The build half of this is TASK-64.
|
||||||
|
<sub>task-listed · evidence: `d:task-63`</sub>
|
||||||
|
|
||||||
|
## · unchanged
|
||||||
|
|
||||||
|
**f-0091 · adoption · task 76 entered the index: Provisioning belongs to a package**
|
||||||
|
Filed under “Open”. Five hand-placed files own no package on the two live devices, and `/etc/systemd/system/souveraine-machined.service` shadows the packaged signer unit on **both**. Activation is two unversioned lua lines. `blueline-edge-sense` already ships
|
||||||
|
<sub>task-listed · evidence: `d:task-76`</sub>
|
||||||
|
|
||||||
|
**f-0048 · federation · federation did not move**
|
||||||
|
A second quiet week.
|
||||||
|
<sub>dimension-activity · open 4 weeks since 2026-W30 · evidence: `w:2026-W33`</sub>
|
||||||
|
|
||||||
|
**f-0084 · identity · task 68 entered the index: Shared household state: tasks first**
|
||||||
|
Filed under “Open”. Canonical plan landed 2026-08-10. First slice is an admitted local task trail + materialized QML app/sidebar view; Culver is the Personal-class client, not the authority.
|
||||||
|
<sub>task-listed · evidence: `d:task-68`</sub>
|
||||||
|
|
||||||
|
**f-0080 · reflection · task 64 entered the index: One SouveraineOS build for the iPhone**
|
||||||
|
Filed under “Open”. Scoped 2026-08-10, not started. `linux-pauli` is not a repository, reaches the phone by `scp`, and CI has never built it. The display work that day is worth more than its code: the raster is **derived, not guessed** — 888×1845 @ 60 off `adp
|
||||||
|
<sub>task-listed · evidence: `d:task-64`</sub>
|
||||||
|
|
||||||
|
**f-0034 · reliability · Pixel3Arch CI mixed**
|
||||||
|
On Pixel3Arch, 14 of 30 runs failed and 5 never concluded.
|
||||||
|
<sub>ci-week · open 4 weeks since 2026-W30 · was regressed · evidence: `ci:Pixel3Arch:1705`, `ci:Pixel3Arch:1264`, `ci:Pixel3Arch:1265`, `ci:Pixel3Arch:1284`, `ci:Pixel3Arch:1289`, `ci:Pixel3Arch:1294`</sub>
|
||||||
|
|
||||||
|
**f-0047 · reliability · reliability moved**
|
||||||
|
7 commit(s), 1 the week before.
|
||||||
|
<sub>dimension-activity · open 4 weeks since 2026-W30 · evidence: `c:souveraine-viewtop:f45f459f`, `c:souveraine-viewtop:991f7664`, `c:souveraine-viewtop:0f82700d`, `c:souveraine:8bc9c9a7`, `c:souveraine:2fa92210`, `c:souveraine-lens:cd646e09`, `c:souveraine-lens:3f0f4de4`</sub>
|
||||||
|
|
||||||
|
**f-0068 · reliability · souveraine-viewtop CI mixed**
|
||||||
|
On souveraine-viewtop, 1 of 20 runs failed.
|
||||||
|
<sub>ci-week · open 3 weeks since 2026-W31 · evidence: `ci:souveraine-viewtop:1663`, `ci:souveraine-viewtop:1271`</sub>
|
||||||
|
|
||||||
|
**f-0094 · reliability · souveraine-lens CI mixed**
|
||||||
|
On souveraine-lens, 3 of 14 runs failed.
|
||||||
|
<sub>ci-week · evidence: `ci:souveraine-lens:1692`, `ci:souveraine-lens:1632`, `ci:souveraine-lens:1634`, `ci:souveraine-lens:1679`</sub>
|
||||||
|
|
||||||
|
**f-0085 · runtime · task 69 entered the index: Agent usage, one spine: the session daemon**
|
||||||
|
Filed under “Open”. Recon 2026-08-11, nothing built. One collector tracks Souveraine + Claude Code + Codex from their local truth (`~/.codex/sessions` JSONL, `~/.claude/projects` JSONL, substrate `core/session`). Two gaps are one shape: the substrate records u
|
||||||
|
<sub>task-listed · evidence: `d:task-69`</sub>
|
||||||
|
|
||||||
|
**f-0089 · sensorium · task 74 entered the index: LibrePods: an admitted accessory**
|
||||||
|
Filed under “Open”. **Scoped 2026-08-13.** AirPods protocol stays in LibrePods; sessiond must admit/audit presentation events and Quickshell projects the card. First gate: capture a redacted case-open fixture — connection is not proof of opening.
|
||||||
|
<sub>task-listed · evidence: `d:task-74`</sub>
|
||||||
|
|
||||||
|
**f-0011 · surface · task 17 restated: Squeekboard + dictation polish**
|
||||||
|
What's left was rewritten under “Open”. Sole OSK installed at pkgrel 4. Pkgrel 5 adds Daily's visible mode key and chooser for the separate five-row Terminal board; CI, install, repeated reopen, mic-state, and side-panel focus proof remain. Native completion follows.
|
||||||
|
<sub>task-restated · open 4 weeks since 2026-W30 · evidence: `d:task-17`</sub>
|
||||||
|
|
||||||
|
**f-0017 · surface · task 24 restated: Keyboard layout rethink**
|
||||||
|
What's left was rewritten under “Open”. Superseded by TASK-17's reference-led Daily/Terminal Squeekboard layouts. Retained as design evidence; do not resume its Stevia patch plan.
|
||||||
|
<sub>task-restated · open 4 weeks since 2026-W30 · evidence: `d:task-24`</sub>
|
||||||
|
|
||||||
|
**f-0024 · surface · task 32 restated: Pill and keyboard as owned apps**
|
||||||
|
What's left was rewritten under “Open”. Keyboard now has one runtime/package owner and no dual-OSK provisioning; glass proof remains. Pill ownership and package-owned shell delivery remain open.
|
||||||
|
<sub>task-restated · open 4 weeks since 2026-W30 · evidence: `d:task-32`</sub>
|
||||||
|
|
||||||
|
**f-0052 · surface · task 38 restated: Pill swipe-to-max swaps the keyboard**
|
||||||
|
What's left was rewritten under “Open”. Superseded: Squeekboard's own visible mode chooser selects Daily/Terminal inside one process. Archive after TASK-17 glass acceptance.
|
||||||
|
<sub>task-restated · open 3 weeks since 2026-W31 · evidence: `d:task-38`</sub>
|
||||||
|
|
||||||
|
**f-0086 · surface · task 70 entered the index: The island: agent surfacing for the shell**
|
||||||
|
Filed under “Open”. Recon 2026-08-11. "Open Agent Island" — end-4 fork (same ancestor as ii-base) with a morphing notch, Claude Code status, and permission Allow/Deny from the notch. Take the mechanisms (AgentService + bridge, 13/13 safety) onto ii-base, not t
|
||||||
|
<sub>task-listed · evidence: `d:task-70`</sub>
|
||||||
|
|
||||||
|
**f-0090 · surface · task 75 entered the index: The shell must have a memory budget**
|
||||||
|
Filed under “Open”. **Scoped 2026-08-13.** QuickShell's resident core must stay small; the AI panel is the first separately supervised, genuinely unloadable surface. Measure before choosing a full native-Rust shell rewrite.
|
||||||
|
<sub>task-listed · evidence: `d:task-75`</sub>
|
||||||
|
|
||||||
|
## Standing longest without moving
|
||||||
|
|
||||||
|
45 task(s) sat in their section untouched this week. The ten that have sat longest:
|
||||||
|
|
||||||
|
- **f-0001 · surface · task 2: Lockscreen-as-Rust-system** — “In progress” for 3 week(s), since 2026-W31.
|
||||||
|
- **f-0002 · unassigned · task 3: Boot timing: splash → lock** — “Open” for 3 week(s), since 2026-W31.
|
||||||
|
- **f-0003 · unassigned · task 7: WCD9340 mic** — “Open” for 3 week(s), since 2026-W31.
|
||||||
|
- **f-0004 · identity · task 8: Device state manager / power profiles** — “In progress” for 3 week(s), since 2026-W31.
|
||||||
|
- **f-0005 · unassigned · task 9: Suspend-resume FTS calibration race** — “Open” for 3 week(s), since 2026-W31.
|
||||||
|
- **f-0006 · sensorium · task 12: Face auth as a capability factor** — “Open” for 3 week(s), since 2026-W31.
|
||||||
|
- **f-0008 · surface · task 14: Overview → phone app grid** — “In progress” for 3 week(s), since 2026-W31.
|
||||||
|
- **f-0010 · surface · task 16: culver: messenger + contacts** — “In progress” for 3 week(s), since 2026-W31.
|
||||||
|
- **f-0012 · unassigned · task 18: Selection/highlight action menu** — “Open” for 3 week(s), since 2026-W31.
|
||||||
|
- **f-0013 · surface · task 19: First-party Settings control center** — “In progress” for 3 week(s), since 2026-W31.
|
||||||
|
|
||||||
|
## Provenance
|
||||||
|
|
||||||
|
- Evidence: `project-state/evidence/2026-W33.json`, collected 2026-08-18T22:46:44Z by `tools/project-state/collect.py` v1 against gitea 1.25.5 in 92 API calls.
|
||||||
|
- Deterministic rules: ci-week, dimension-activity, pull-aging, task-listed, task-restated, task-standing.
|
||||||
|
- Analyst: none run; every finding here is deterministic.
|
||||||
|
- Trace one: `tools/project-state/synthesize.py --explain <id>`.
|
||||||
173
project-state/reports/2026-W34.md
Normal file
173
project-state/reports/2026-W34.md
Normal file
|
|
@ -0,0 +1,173 @@
|
||||||
|
# Souveraine project state — 2026-W34
|
||||||
|
|
||||||
|
2026-08-17 to 2026-08-23 (America/Toronto). Internal. Generated from `project-state/evidence/2026-W34.json`; the prose is disposable, the evidence and `state.json` are not.
|
||||||
|
|
||||||
|
## The week, counted
|
||||||
|
|
||||||
|
- 43 commits across 4 repos, 25 CI runs, 116 declared state changes.
|
||||||
|
- Issues: 6 open, 0 filed, 0 closed. Pulls: 2 open, 0 merged.
|
||||||
|
- Dimension basis: 33 by paths, 7 by subject, 3 by repo (`paths` read the diff, `subject` read the commit subject, `repo` fell back to the repository, `none` placed nothing).
|
||||||
|
|
||||||
|
## ◆ milestone
|
||||||
|
|
||||||
|
**f-0082 · runtime · task 66 left the index: souveraineai.com overhaul**
|
||||||
|
Dropped from “Live” — closed work moves to archive/.
|
||||||
|
<sub>task-delisted · open 2 weeks since 2026-W33 · was advanced · evidence: `d:task-66`</sub>
|
||||||
|
|
||||||
|
**f-0085 · runtime · task 69 left the index: Agent usage, one spine: the session daemon**
|
||||||
|
Dropped from “Open” — closed work moves to archive/.
|
||||||
|
<sub>task-delisted · open 2 weeks since 2026-W33 · was unchanged · evidence: `d:task-69`</sub>
|
||||||
|
|
||||||
|
**f-0017 · surface · task 24 left the index: Keyboard layout rethink**
|
||||||
|
Dropped from “Open” — closed work moves to archive/.
|
||||||
|
<sub>task-delisted · open 5 weeks since 2026-W30 · was unchanged · evidence: `d:task-24`</sub>
|
||||||
|
|
||||||
|
**f-0052 · surface · task 38 left the index: Pill swipe-to-max swaps the keyboard**
|
||||||
|
Dropped from “Open” — closed work moves to archive/.
|
||||||
|
<sub>task-delisted · open 4 weeks since 2026-W31 · was unchanged · evidence: `d:task-38`</sub>
|
||||||
|
|
||||||
|
**f-0072 · surface · task 55 left the index: The hand's verbs: a window action sheet**
|
||||||
|
Dropped from “Open” — closed work moves to archive/.
|
||||||
|
<sub>task-delisted · open 3 weeks since 2026-W32 · was unchanged · evidence: `d:task-55`</sub>
|
||||||
|
|
||||||
|
**f-0086 · surface · task 70 left the index: The island: agent surfacing for the shell**
|
||||||
|
Dropped from “Open” — closed work moves to archive/.
|
||||||
|
<sub>task-delisted · open 2 weeks since 2026-W33 · was unchanged · evidence: `d:task-70`</sub>
|
||||||
|
|
||||||
|
**f-0090 · surface · task 75 left the index: The shell must have a memory budget**
|
||||||
|
Dropped from “Open” — closed work moves to archive/.
|
||||||
|
<sub>task-delisted · open 2 weeks since 2026-W33 · was unchanged · evidence: `d:task-75`</sub>
|
||||||
|
|
||||||
|
## ■ blocked
|
||||||
|
|
||||||
|
**f-0009 · identity · task 15: Idle coordinator**
|
||||||
|
Standing under “Blocked or gated”. Freezing `app.slice` would freeze sessiond, shell and secrets. Needs slice separation first (measured 07-25).
|
||||||
|
<sub>task-standing · open 5 weeks since 2026-W30 · evidence: `d:task-15`</sub>
|
||||||
|
|
||||||
|
**f-0018 · release · task 25: One repo, all packages**
|
||||||
|
Standing under “Blocked or gated”. Parked 07-25 — four blockers before `repo-add --sign`. See TASK-27.
|
||||||
|
<sub>task-standing · open 5 weeks since 2026-W30 · evidence: `d:task-25`</sub>
|
||||||
|
|
||||||
|
**f-0054 · runtime · task 40: Inference tiering for the sensors**
|
||||||
|
Standing under “Blocked or gated”. Nothing — **it is the gate.** P1 applied to sensord/grip/dial; gates the grip raw stream and TASK-31's IPC. Decisions first, then small code.
|
||||||
|
<sub>task-standing · open 4 weeks since 2026-W31 · evidence: `d:task-40`</sub>
|
||||||
|
|
||||||
|
**f-0020 · unassigned · task 28: Handover on upgrade**
|
||||||
|
Standing under “Blocked or gated”. TASK-27's packaging. Also owns TASK-13's outstanding cold-boot handshake check.
|
||||||
|
<sub>task-standing · open 5 weeks since 2026-W30 · evidence: `d:task-28`</sub>
|
||||||
|
|
||||||
|
## ? decision
|
||||||
|
|
||||||
|
**f-0069 · identity · souveraine#1 unmerged 38d: machined: system-tier machine identity daemon**
|
||||||
|
Opened 2026-07-16, never merged or closed.
|
||||||
|
<sub>pull-aging · open 4 weeks since 2026-W31 · evidence: `p:souveraine:1`</sub>
|
||||||
|
|
||||||
|
**f-0095 · identity · souveraine#2 unmerged 26d: sessiond: squeeze is input, and a pocket can veto it**
|
||||||
|
Opened 2026-07-28, never merged or closed.
|
||||||
|
<sub>pull-aging · open 2 weeks since 2026-W33 · evidence: `p:souveraine:2`</sub>
|
||||||
|
|
||||||
|
## ▲ advanced
|
||||||
|
|
||||||
|
**f-0049 · adoption · adoption moved**
|
||||||
|
0 commit(s), 5 the week before. Declared: restated Provisioning belongs to a package; added saf/device/evidence/first_provision_capture.md; added saf/device/evidence/qcril_full_provisioning.md.
|
||||||
|
<sub>dimension-activity · open 5 weeks since 2026-W30 · evidence: `d:task-76`, `d:saf/device/evidence/first_provision_capture.md`, `d:saf/device/evidence/qcril_full_provisioning.md`</sub>
|
||||||
|
|
||||||
|
**f-0048 · federation · federation moved**
|
||||||
|
0 commit(s), 0 the week before. Declared: moved docs/substrate/tasks/archive/federation-summon.md; added saf/archive/atproto-federation.md; added saf/federation/01-node-enrollment.md.
|
||||||
|
<sub>dimension-activity · open 5 weeks since 2026-W30 · was unchanged · evidence: `d:docs/substrate/tasks/archive/federation-summon.md`, `d:saf/archive/atproto-federation.md`, `d:saf/federation/01-node-enrollment.md`</sub>
|
||||||
|
|
||||||
|
**f-0043 · identity · identity moved**
|
||||||
|
13 commit(s), 14 the week before. Declared: listed The claude login belongs to souveraine, not to `claude`; revised docs/SESSION-AUTHORITY-DOCTRINE.md; revised docs/souveraine-components/INVENTORY.md; revised docs/souveraine-components/audit-status.md.
|
||||||
|
<sub>dimension-activity · open 5 weeks since 2026-W30 · evidence: `c:souveraine:21ef10e8`, `c:souveraine:91ab8053`, `c:SouveraineOS:9815185c`, `c:souveraine:af60d9e4`, `c:souveraine:ee496a7c`, `c:SouveraineOS:341a2fe0`, `c:souveraine:14298e73`, `c:souveraine:0f8fe22a` …</sub>
|
||||||
|
|
||||||
|
**f-0040 · memory · memory moved**
|
||||||
|
5 commit(s), 11 the week before. Declared: added saf/archive/per-agent-compaction-provider.md; added saf/memory/01-compaction.md; added saf/memory/02-replay.md.
|
||||||
|
<sub>dimension-activity · open 5 weeks since 2026-W30 · evidence: `c:souveraine:06a1a035`, `c:souveraine:707a2772`, `c:souveraine:49c6a6a8`, `c:souveraine:04ceac79`, `c:souveraine:5f7f23f3`, `d:saf/archive/per-agent-compaction-provider.md`, `d:saf/memory/01-compaction.md`, `d:saf/memory/02-replay.md`</sub>
|
||||||
|
|
||||||
|
**f-0042 · reflection · reflection moved**
|
||||||
|
3 commit(s), 7 the week before. Declared: revised docs/substrate/tasks/archive/rename-aster-to-subconscious.md; deleted docs/substrate/tasks/rename-aster-to-subconscious.md.
|
||||||
|
<sub>dimension-activity · open 5 weeks since 2026-W30 · evidence: `c:souveraine:5a5b647b`, `c:souveraine:9bee0e8a`, `c:souveraine:727c4969`, `d:docs/substrate/tasks/archive/rename-aster-to-subconscious.md`, `d:docs/substrate/tasks/rename-aster-to-subconscious.md`</sub>
|
||||||
|
|
||||||
|
**f-0068 · reliability · souveraine-viewtop CI clean**
|
||||||
|
2 runs on souveraine-viewtop, none failed.
|
||||||
|
<sub>ci-week · open 4 weeks since 2026-W31 · was unchanged · evidence: `ci:souveraine-viewtop:1746`</sub>
|
||||||
|
|
||||||
|
**f-0041 · runtime · runtime moved**
|
||||||
|
7 commit(s), 47 the week before. Declared: delisted souveraineai.com overhaul; restated The caching landed blind, and compaction drifted; delisted Agent usage, one spine: the session daemon; revised docs/substrate/FELT_STATE_ARCHITECTURE.md.
|
||||||
|
<sub>dimension-activity · open 5 weeks since 2026-W30 · evidence: `c:souveraine:b3bbfeea`, `c:souveraine:dcd44a0c`, `c:souveraine:7b45054f`, `c:souveraine:7089dfb8`, `c:souveraine:9e147093`, `c:souveraine:f32f00c6`, `c:souveraine:76e12045`, `d:task-66` …</sub>
|
||||||
|
|
||||||
|
**f-0045 · sensorium · sensorium moved**
|
||||||
|
1 commit(s), 15 the week before. Declared: restated Battery belongs to the state machine; revised docs/AUDIO-PRIVACY.md; added saf/device/audio.md; added saf/device/evidence/android_modem_bringup_capture.md.
|
||||||
|
<sub>dimension-activity · open 5 weeks since 2026-W30 · evidence: `c:SouveraineOS:bde961c6`, `d:task-33`, `d:docs/AUDIO-PRIVACY.md`, `d:saf/device/audio.md`, `d:saf/device/evidence/android_modem_bringup_capture.md`</sub>
|
||||||
|
|
||||||
|
**f-0044 · surface · surface moved**
|
||||||
|
12 commit(s), 105 the week before. Declared: delisted Keyboard layout rethink; delisted Pill swipe-to-max swaps the keyboard; restated Shell dies on any reload that owes a lock; listed Bearer selection and DNS.
|
||||||
|
<sub>dimension-activity · open 5 weeks since 2026-W30 · evidence: `c:souveraine:3b119033`, `c:souveraine:da8d69af`, `c:souveraine:5237039c`, `c:souveraine:799f0650`, `c:souveraine:59b52a81`, `c:souveraine:2bd61ec4`, `c:souveraine:184a4497`, `c:SouveraineOS:7fc623c5` …</sub>
|
||||||
|
|
||||||
|
**f-0071 · surface · task 53 restated: viewtop audit: what it left open**
|
||||||
|
What's left was rewritten under “Live”. The original ten and cold-boot follow-ups are fixed. Firefox's 504/540 mismatch and the false touch-frame storm are source-fixed and pushed (`3431223`, `41f07b1`), awaiting package/glass proof. Open: overshoot clamp, persistent control conn
|
||||||
|
<sub>task-restated · open 3 weeks since 2026-W32 · was unchanged · evidence: `d:task-53`</sub>
|
||||||
|
|
||||||
|
**f-0077 · surface · task 60 restated: Multitasking is a place, not a transition**
|
||||||
|
What's left was rewritten under “Live”. One-owner carry, measured card geometry, velocity-seeded spring, tray, Home, and the strand repro are on glass. What remains is a real split-zone card check; the inert 1.22 overshoot clamp is a TASK-53 compositor defect.
|
||||||
|
<sub>task-restated · open 3 weeks since 2026-W32 · was unchanged · evidence: `d:task-60`</sub>
|
||||||
|
|
||||||
|
**f-0096 · surface · task 49 entered the index: Bearer selection and DNS**
|
||||||
|
Filed under “In progress”. Machine evidence, settle window, `PreferLink`, tunnel-underlay pinning, readout, and the first two hardware checks landed. Open: split DNS through resolved, wifi/LTE transition, and honest deaf-tunnel proof.
|
||||||
|
<sub>task-listed · evidence: `d:task-49`</sub>
|
||||||
|
|
||||||
|
## · unchanged
|
||||||
|
|
||||||
|
**f-0091 · adoption · task 76 restated: Provisioning belongs to a package**
|
||||||
|
What's left was rewritten under “Open”. The viewtop-owned session edge and legal wrapper targets landed. Still open: package-owned QuickShell content, commission-time activation on each device, removal of `/etc` and user-unit shadows, and fresh-install proof.
|
||||||
|
<sub>task-restated · open 2 weeks since 2026-W33 · evidence: `d:task-76`</sub>
|
||||||
|
|
||||||
|
**f-0097 · identity · task 78 entered the index: The claude login belongs to souveraine, not to `claude`**
|
||||||
|
Filed under “Open”. Scoped 2026-08-18, nothing built. Headless PKCE `souveraine login` (flow verified), a souveraine-owned credential file outside the phone/laptop sync, and a Panel notice when no login can speak. Full Providers subsection in the Panel is a la
|
||||||
|
<sub>task-listed · evidence: `d:task-78`</sub>
|
||||||
|
|
||||||
|
**f-0046 · release · release moved**
|
||||||
|
2 commit(s), 38 the week before.
|
||||||
|
<sub>dimension-activity · open 5 weeks since 2026-W30 · was advanced · evidence: `c:souveraine-viewtop:e8e3f9b1`, `c:SouveraineOS:b1c9494f`</sub>
|
||||||
|
|
||||||
|
**f-0037 · reliability · souveraine CI mixed**
|
||||||
|
On souveraine, 6 of 23 runs failed and 9 never concluded.
|
||||||
|
<sub>ci-week · open 5 weeks since 2026-W30 · was regressed · evidence: `ci:souveraine:1829`, `ci:souveraine:1729`, `ci:souveraine:1753`, `ci:souveraine:1790`, `ci:souveraine:1795`, `ci:souveraine:1799`</sub>
|
||||||
|
|
||||||
|
**f-0047 · reliability · reliability did not move**
|
||||||
|
No commits, after 7 the week before.
|
||||||
|
<sub>dimension-activity · open 5 weeks since 2026-W30 · evidence: `w:2026-W34`</sub>
|
||||||
|
|
||||||
|
**f-0083 · runtime · task 67 restated: The caching landed blind, and compaction drifted**
|
||||||
|
What's left was rewritten under “Open”. Prompt caching is measured and per-turn usage now persists through `assistant_with_usage`; context occupancy reaches both TUI and Panel. Still open: render the provider cache split, regression-test a stable second-round read, settle the wid
|
||||||
|
<sub>task-restated · open 2 weeks since 2026-W33 · was decision required · evidence: `d:task-67`</sub>
|
||||||
|
|
||||||
|
**f-0025 · sensorium · task 33 restated: Battery belongs to the state machine**
|
||||||
|
What's left was rewritten under “Open”. Charge now enters as `SensorSource::Charge` through sensord and sessiond interprets it. Open: UPower supplier-walk threshold write support and the step-up-gated charge policy.
|
||||||
|
<sub>task-restated · open 5 weeks since 2026-W30 · evidence: `d:task-33`</sub>
|
||||||
|
|
||||||
|
**f-0062 · surface · task 48 restated: Shell dies on any reload that owes a lock**
|
||||||
|
What's left was rewritten under “Open”. **Unblocked.** Viewtop's lock admission/restore and two-client handoff now exist. The QML root still lacks the reload identity its children carry; the locked reload acceptance has not been rerun.
|
||||||
|
<sub>task-restated · open 4 weeks since 2026-W31 · evidence: `d:task-48`</sub>
|
||||||
|
|
||||||
|
## Standing longest without moving
|
||||||
|
|
||||||
|
50 task(s) sat in their section untouched this week. The ten that have sat longest:
|
||||||
|
|
||||||
|
- **f-0001 · surface · task 2: Lockscreen-as-Rust-system** — “In progress” for 4 week(s), since 2026-W31.
|
||||||
|
- **f-0002 · unassigned · task 3: Boot timing: splash → lock** — “Open” for 4 week(s), since 2026-W31.
|
||||||
|
- **f-0003 · unassigned · task 7: WCD9340 mic** — “Open” for 4 week(s), since 2026-W31.
|
||||||
|
- **f-0004 · identity · task 8: Device state manager / power profiles** — “In progress” for 4 week(s), since 2026-W31.
|
||||||
|
- **f-0005 · unassigned · task 9: Suspend-resume FTS calibration race** — “Open” for 4 week(s), since 2026-W31.
|
||||||
|
- **f-0006 · sensorium · task 12: Face auth as a capability factor** — “Open” for 4 week(s), since 2026-W31.
|
||||||
|
- **f-0008 · surface · task 14: Overview → phone app grid** — “In progress” for 4 week(s), since 2026-W31.
|
||||||
|
- **f-0010 · surface · task 16: culver: messenger + contacts** — “In progress” for 4 week(s), since 2026-W31.
|
||||||
|
- **f-0011 · surface · task 17: Squeekboard + dictation polish** — “Open” for 4 week(s), since 2026-W31.
|
||||||
|
- **f-0012 · unassigned · task 18: Selection/highlight action menu** — “Open” for 4 week(s), since 2026-W31.
|
||||||
|
|
||||||
|
## Provenance
|
||||||
|
|
||||||
|
- Evidence: `project-state/evidence/2026-W34.json`, collected 2026-08-18T22:46:47Z by `tools/project-state/collect.py` v1 against gitea 1.25.5 in 85 API calls.
|
||||||
|
- Deterministic rules: ci-week, dimension-activity, pull-aging, task-delisted, task-listed, task-restated, task-standing.
|
||||||
|
- Analyst: none run; every finding here is deterministic.
|
||||||
|
- Trace one: `tools/project-state/synthesize.py --explain <id>`.
|
||||||
3377
project-state/state.json
Normal file
3377
project-state/state.json
Normal file
File diff suppressed because it is too large
Load diff
12
project-state/verdicts.json
Normal file
12
project-state/verdicts.json
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"pull/souveraine#1": {
|
||||||
|
"verdict": "promote",
|
||||||
|
"note": "machined has sat a month; decide or close it",
|
||||||
|
"by": "casey",
|
||||||
|
"at": "2026-08-18T23:27:09Z",
|
||||||
|
"edited": false,
|
||||||
|
"week": "2026-W34",
|
||||||
|
"finding_id": "f-0069",
|
||||||
|
"title": "souveraine#1 unmerged 38d: machined: system-tier machine identity daemon"
|
||||||
|
}
|
||||||
|
}
|
||||||
353
tools/project-state/agent.py
Normal file
353
tools/project-state/agent.py
Normal file
|
|
@ -0,0 +1,353 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""The agent's door into project state. No inference happens here, ever.
|
||||||
|
|
||||||
|
The deterministic layer establishes what is true; the agent reading this writes
|
||||||
|
the prose. Nothing in this file selects, calls, or configures a model — the
|
||||||
|
agent invoking it is the model, and it already has the identity, the context and
|
||||||
|
the tools.
|
||||||
|
|
||||||
|
agent.py status what is collected, what is missing
|
||||||
|
agent.py packet --week 2026-W33 everything needed to write the draft
|
||||||
|
agent.py explain f-0069 one finding and every row it rests on
|
||||||
|
agent.py draft --week 2026-W33 --file draft.md --author Annie
|
||||||
|
agent.py verdict f-0069 promote --note "worth a task"
|
||||||
|
|
||||||
|
`packet` is the contract. It carries the week's state, what turned, what has
|
||||||
|
stood longest, and an `investigate` list naming where the deterministic layer
|
||||||
|
knows it is thin — go read those repositories rather than trusting the packet.
|
||||||
|
|
||||||
|
See docs/tasks/79-project-state.md.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import datetime as dt
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
import common as c
|
||||||
|
import collect
|
||||||
|
import synthesize as s
|
||||||
|
|
||||||
|
STORE = s.STORE
|
||||||
|
CONTRACT = {
|
||||||
|
"you_are": "the agent that writes Souveraine's weekly project-state draft",
|
||||||
|
"the_split": (
|
||||||
|
"Everything under `counted`, `movement`, `standing`, `carried` and `closed` was "
|
||||||
|
"computed from the forge by deterministic code and is authoritative. Your draft is "
|
||||||
|
"downstream of it and is disposable — it can be regenerated from this packet and "
|
||||||
|
"the stored state at any time."
|
||||||
|
),
|
||||||
|
"produce": (
|
||||||
|
"A markdown draft answering: what became true about Souveraine this week, what "
|
||||||
|
"stopped being true, what advanced, what regressed, what remains unresolved, what "
|
||||||
|
"decisions are waiting, and which milestones were crossed or are approaching."
|
||||||
|
),
|
||||||
|
"rules": [
|
||||||
|
"Cite finding ids (f-0069) and evidence ids (c:, ci:, d:, i:, p:, r:) for every claim.",
|
||||||
|
"Do not invent a shipment, release, decision, or outcome that is not in the evidence.",
|
||||||
|
"'Nothing moved here this week' is a legal sentence.",
|
||||||
|
"Where the packet's `investigate` list says the evidence is thin, go read the "
|
||||||
|
"repository before asserting; say so inline if you did not.",
|
||||||
|
"The report is internal. It publishes nowhere until a human promotes it.",
|
||||||
|
],
|
||||||
|
"when_the_packet_is_not_enough": (
|
||||||
|
"Read the repositories directly. `evidence.file` holds every row including full "
|
||||||
|
"commit bodies and changed paths; the working trees named in `repos` are checked "
|
||||||
|
"out locally. `agent.py explain <id>` prints any finding with its rows in full."
|
||||||
|
),
|
||||||
|
"return_with": "agent.py draft --week <week> --file <path> --author <you> [--model <id>]",
|
||||||
|
"no_inference_here": (
|
||||||
|
"This tool never calls a model. Model choice belongs to whoever invokes it and is "
|
||||||
|
"recorded, not decided, by --model."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
EVIDENCE_KINDS = {
|
||||||
|
"c:": "commit — repo, sha, subject, body, changed paths, dimension and the basis that chose it",
|
||||||
|
"ci:": "Gitea Actions run — workflow, branch, head sha, conclusion; joins to a commit id",
|
||||||
|
"d:": "declared change — a task-index row or a SAF/docs file that moved",
|
||||||
|
"i:": "issue", "p:": "pull request", "r:": "release", "w:": "the week's rollup",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def load_state():
|
||||||
|
return s.load(os.path.join(STORE, "state.json"), {"findings": []})
|
||||||
|
|
||||||
|
|
||||||
|
def load_evidence(week):
|
||||||
|
return s.load(os.path.join(STORE, "evidence", f"{week}.json"))
|
||||||
|
|
||||||
|
|
||||||
|
def status():
|
||||||
|
state = load_state()
|
||||||
|
held = collect.held()
|
||||||
|
return {
|
||||||
|
"weeks_held": held,
|
||||||
|
"latest_synthesized": state.get("latest_week"),
|
||||||
|
"gaps": collect.gaps(),
|
||||||
|
"pending": s.pending(state),
|
||||||
|
"open_findings": sum(1 for f in state.get("findings", []) if f["status"] == "open"),
|
||||||
|
"drafts": sorted(os.listdir(os.path.join(STORE, "drafts")))
|
||||||
|
if os.path.isdir(os.path.join(STORE, "drafts")) else [],
|
||||||
|
"next": next_action(state, held),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def next_action(state, held):
|
||||||
|
if collect.gaps():
|
||||||
|
return "collect.py --backfill — weeks are missing evidence"
|
||||||
|
if s.pending(state):
|
||||||
|
return "synthesize.py --catch-up — weeks are collected but not passed over"
|
||||||
|
if not held:
|
||||||
|
return "collect.py --backfill --since <week> — nothing collected yet"
|
||||||
|
return f"agent.py packet --week {state.get('latest_week')} — state is current; write the draft"
|
||||||
|
|
||||||
|
|
||||||
|
def thin_spots(ev, findings):
|
||||||
|
"""Where the deterministic layer knows it did not see enough.
|
||||||
|
|
||||||
|
This is the half of the packet that asks for work: each entry says what is
|
||||||
|
weak, and what reading it would take to settle.
|
||||||
|
"""
|
||||||
|
out = []
|
||||||
|
unplaced = [r for r in ev["commits"] if r["dimension"] == c.UNASSIGNED]
|
||||||
|
if unplaced:
|
||||||
|
out.append({
|
||||||
|
"why": f"{len(unplaced)} commit(s) matched no dimension keyword and were not guessed at",
|
||||||
|
"how": "read these commits in their repositories and place them yourself",
|
||||||
|
"rows": [{"id": r["id"], "repo": r["repo"], "subject": r["subject"],
|
||||||
|
"paths": r["paths"][:6]} for r in unplaced],
|
||||||
|
})
|
||||||
|
blind = [name for name, meta in ev["collector"]["repos"].items()
|
||||||
|
if not meta["checkout"] and meta["commits"]]
|
||||||
|
if blind:
|
||||||
|
out.append({
|
||||||
|
"why": f"no local checkout for {', '.join(blind)}, so changed paths were unavailable "
|
||||||
|
f"and those commits were placed by subject or repository alone",
|
||||||
|
"how": "clone or fetch those repos if a dimension call there matters this week",
|
||||||
|
"rows": [],
|
||||||
|
})
|
||||||
|
single = [f for f in findings if len(f["evidence"]) == 1
|
||||||
|
and f["provenance"]["rule"] not in ("task-standing", "pull-aging", "issue-aging")]
|
||||||
|
if single:
|
||||||
|
out.append({
|
||||||
|
"why": f"{len(single)} finding(s) rest on exactly one evidence row",
|
||||||
|
"how": "agent.py explain <id>, then read the surrounding work before leaning on it",
|
||||||
|
"rows": [{"id": f["id"], "title": f["title"], "evidence": f["evidence"]}
|
||||||
|
for f in single[:12]],
|
||||||
|
})
|
||||||
|
declared_only = [dim for dim, r in ev["rollup"]["by_dimension"].items()
|
||||||
|
if r.get("declared") and not r.get("commits")]
|
||||||
|
if declared_only:
|
||||||
|
out.append({
|
||||||
|
"why": f"{', '.join(declared_only)} changed in the documentation with no commits behind it",
|
||||||
|
"how": "a doc can lead the code or lag it — check which, before calling it advanced",
|
||||||
|
"rows": [{"id": r["id"], "file": r.get("file"), "title": r.get("title"),
|
||||||
|
"change": r["change"]}
|
||||||
|
for r in ev["declared"] if r["dimension"] in declared_only][:12],
|
||||||
|
})
|
||||||
|
notes = ev["collector"].get("notes") or []
|
||||||
|
if notes:
|
||||||
|
out.append({"why": "the collector reported problems", "how": "treat these areas as incomplete",
|
||||||
|
"rows": [{"note": n} for n in notes]})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def packet(week, brief=False):
|
||||||
|
label, monday, sunday = c.parse_week(week)
|
||||||
|
ev = load_evidence(label)
|
||||||
|
if not ev:
|
||||||
|
return None, f"no evidence for {label} — collect.py --week {label}"
|
||||||
|
state = s.load(os.path.join(STORE, "history", f"{label}.json")) or load_state()
|
||||||
|
if label not in state.get("weeks", []):
|
||||||
|
return None, f"{label} is collected but not synthesized — synthesize.py --catch-up"
|
||||||
|
|
||||||
|
findings = state["findings"]
|
||||||
|
this_week = [f for f in findings if f["last_seen"] == label and f["status"] == "open"]
|
||||||
|
quiet = [f for f in this_week if f["provenance"]["rule"] == "task-standing"
|
||||||
|
and f["movement"] != "blocked"]
|
||||||
|
moved = [f for f in this_week if f not in quiet]
|
||||||
|
carried = [f for f in findings if f["status"] == "open" and f["last_seen"] != label]
|
||||||
|
closed = [f for f in findings if f.get("resolved_in") == label]
|
||||||
|
turned = [f for f in moved if f.get("was") and f["was"]["movement"] != f["movement"]]
|
||||||
|
|
||||||
|
def slim(f):
|
||||||
|
row = {"id": f["id"], "key": f["key"], "dimension": f["dimension"],
|
||||||
|
"movement": f["movement"], "title": f["title"], "statement": f["statement"],
|
||||||
|
"evidence": f["evidence"], "rule": f["provenance"]["rule"],
|
||||||
|
"first_seen": f["first_seen"], "weeks_open": f.get("weeks_open")}
|
||||||
|
for extra in ("was", "weeks_in_section", "section_since", "task_state", "resolved_in"):
|
||||||
|
if f.get(extra):
|
||||||
|
row[extra] = f[extra]
|
||||||
|
if (f.get("human") or {}).get("verdict"):
|
||||||
|
row["human"] = f["human"]
|
||||||
|
return row
|
||||||
|
|
||||||
|
commits = [{"id": r["id"], "repo": r["repo"], "at": r["at"], "dimension": r["dimension"],
|
||||||
|
"basis": r["basis"], "subject": r["subject"],
|
||||||
|
**({} if brief else {"detail": r["detail"], "paths": r["paths"][:12]})}
|
||||||
|
for r in ev["commits"]]
|
||||||
|
# Only a finding that carries can be the oldest one; a CI week or a
|
||||||
|
# dimension's activity describes one week and lapses with it.
|
||||||
|
oldest = max((f for f in findings if f["status"] == "open"
|
||||||
|
and f["provenance"]["rule"] in s.CARRIES),
|
||||||
|
key=lambda f: (f.get("weeks_open", 1), f["id"]), default=None)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"contract": CONTRACT,
|
||||||
|
"week": label,
|
||||||
|
"range": ev["range"],
|
||||||
|
"completeness": {
|
||||||
|
"weeks_held": collect.held(),
|
||||||
|
"gaps": collect.gaps(),
|
||||||
|
"pending": s.pending(load_state()),
|
||||||
|
"collected_at": ev["collected_at"],
|
||||||
|
"collector": {k: ev["collector"][k] for k in ("tool", "version", "forge", "api_calls",
|
||||||
|
"redacted")},
|
||||||
|
},
|
||||||
|
"counted": ev["rollup"],
|
||||||
|
"movement": [slim(f) for f in moved],
|
||||||
|
"standing": [slim(f) for f in sorted(quiet, key=lambda f: -f.get("weeks_in_section", 1))],
|
||||||
|
"carried": [slim(f) for f in sorted(carried, key=lambda f: -f.get("weeks_open", 1))],
|
||||||
|
"closed": [slim(f) for f in closed],
|
||||||
|
"continuity": {
|
||||||
|
"weeks_available": state.get("weeks", []),
|
||||||
|
"turned_this_week": [{"id": f["id"], "title": f["title"],
|
||||||
|
"from": f["was"]["movement"], "to": f["movement"],
|
||||||
|
"since": f["was"]["week"]} for f in turned],
|
||||||
|
"oldest_open": slim(oldest) if oldest else None,
|
||||||
|
},
|
||||||
|
"investigate": thin_spots(ev, this_week),
|
||||||
|
# --brief keeps every row the counts cannot replace and drops the rest:
|
||||||
|
# green CI runs are already a tally, and the task index is `standing`.
|
||||||
|
"evidence": {
|
||||||
|
"file": f"project-state/evidence/{label}.json",
|
||||||
|
"explain": "tools/project-state/agent.py explain <finding-id>",
|
||||||
|
"kinds": EVIDENCE_KINDS,
|
||||||
|
"commits": commits,
|
||||||
|
"declared": ev["declared"],
|
||||||
|
"ci": [r for r in ev["ci"] if r["conclusion"] != "success"] if brief else ev["ci"],
|
||||||
|
"ci_note": "green runs omitted; counts are under `counted.ci`" if brief else None,
|
||||||
|
"issues": ev["issues"],
|
||||||
|
"pulls": ev["pulls"],
|
||||||
|
"releases": ev["releases"],
|
||||||
|
**({} if brief else {"task_index": ev.get("index", {})}),
|
||||||
|
},
|
||||||
|
"repos": {name: {"commits": meta["commits"],
|
||||||
|
"checkout": os.path.join(c.PROJECTS, name) if meta["checkout"] else None}
|
||||||
|
for name, meta in ev["collector"]["repos"].items() if meta["commits"]},
|
||||||
|
}, None
|
||||||
|
|
||||||
|
|
||||||
|
def record_draft(week, path, author, model, note):
|
||||||
|
label, _, _ = c.parse_week(week)
|
||||||
|
try:
|
||||||
|
body = open(path).read()
|
||||||
|
except OSError as e:
|
||||||
|
note(f"cannot read {path}: {e}")
|
||||||
|
return 2
|
||||||
|
state = s.load(os.path.join(STORE, "history", f"{label}.json")) or load_state()
|
||||||
|
folder = os.path.join(STORE, "drafts")
|
||||||
|
os.makedirs(folder, exist_ok=True)
|
||||||
|
slug = "".join(ch if ch.isalnum() or ch in "-_" else "-" for ch in author).strip("-").lower()
|
||||||
|
stem = os.path.join(folder, f"{label}--{slug}")
|
||||||
|
open(stem + ".md", "w").write(body)
|
||||||
|
# The draft names the state it was written against, so a later reader can
|
||||||
|
# tell whether it still describes the findings that exist.
|
||||||
|
open(stem + ".json", "w").write(json.dumps({
|
||||||
|
"week": label,
|
||||||
|
"author": author,
|
||||||
|
"model": model,
|
||||||
|
"recorded_at": c.iso_z(dt.datetime.now(dt.timezone.utc)),
|
||||||
|
"state_updated": state.get("updated"),
|
||||||
|
"findings_open": sum(1 for f in state.get("findings", []) if f["status"] == "open"),
|
||||||
|
"evidence": f"project-state/evidence/{label}.json",
|
||||||
|
"published": False,
|
||||||
|
"source": os.path.abspath(path),
|
||||||
|
}, indent=2, ensure_ascii=False) + "\n")
|
||||||
|
note(f"{label}: draft by {author}"
|
||||||
|
+ (f" ({model})" if model else "")
|
||||||
|
+ f" -> project-state/drafts/{label}--{slug}.md; nothing published")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
|
||||||
|
sub = ap.add_subparsers(dest="command", required=True)
|
||||||
|
|
||||||
|
sub.add_parser("status", help="what is collected, synthesized, and missing")
|
||||||
|
|
||||||
|
p = sub.add_parser("packet", help="everything needed to write the week's draft")
|
||||||
|
p.add_argument("--week", default="last")
|
||||||
|
p.add_argument("--brief", action="store_true", help="drop commit bodies and paths")
|
||||||
|
p.add_argument("--out", help="write here instead of stdout")
|
||||||
|
|
||||||
|
e = sub.add_parser("explain", help="one finding and every evidence row it cites")
|
||||||
|
e.add_argument("finding")
|
||||||
|
e.add_argument("--json", action="store_true")
|
||||||
|
|
||||||
|
d = sub.add_parser("draft", help="record a draft written by an agent or a person")
|
||||||
|
d.add_argument("--week", default="last")
|
||||||
|
d.add_argument("--file", required=True)
|
||||||
|
d.add_argument("--author", required=True)
|
||||||
|
d.add_argument("--model", help="the model that wrote it, recorded not chosen")
|
||||||
|
|
||||||
|
v = sub.add_parser("verdict", help="a person's word on a finding")
|
||||||
|
v.add_argument("finding")
|
||||||
|
v.add_argument("verdict", choices=s.VERDICTS)
|
||||||
|
v.add_argument("--note")
|
||||||
|
v.add_argument("--by")
|
||||||
|
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
def note(message):
|
||||||
|
print(f"[project-state] {message}", file=sys.stderr)
|
||||||
|
|
||||||
|
if args.command == "status":
|
||||||
|
print(json.dumps(status(), indent=2, ensure_ascii=False))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if args.command == "packet":
|
||||||
|
data, problem = packet(args.week, args.brief)
|
||||||
|
if problem:
|
||||||
|
note(problem)
|
||||||
|
return 2
|
||||||
|
text = json.dumps(data, indent=2, ensure_ascii=False) + "\n"
|
||||||
|
if args.out:
|
||||||
|
open(args.out, "w").write(text)
|
||||||
|
note(f"{data['week']}: packet -> {args.out} ({len(text):,} chars)")
|
||||||
|
else:
|
||||||
|
sys.stdout.write(text)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if args.command == "explain":
|
||||||
|
if not args.json:
|
||||||
|
return s.explain(load_state(), args.finding)
|
||||||
|
state = load_state()
|
||||||
|
f = next((x for x in state["findings"]
|
||||||
|
if x["id"] == args.finding or x["key"] == args.finding), None)
|
||||||
|
if not f:
|
||||||
|
note(f"no finding {args.finding}")
|
||||||
|
return 1
|
||||||
|
week = f["provenance"].get("evidence_week", f["last_seen"])
|
||||||
|
ev = load_evidence(week)
|
||||||
|
index = {row["id"]: row for group in ("commits", "ci", "issues", "pulls",
|
||||||
|
"releases", "declared")
|
||||||
|
for row in ev.get(group, [])}
|
||||||
|
index[f"w:{week}"] = ev.get("rollup", {})
|
||||||
|
print(json.dumps({"finding": f, "week": week,
|
||||||
|
"evidence": {eid: index.get(eid, "MISSING") for eid in f["evidence"]}},
|
||||||
|
indent=2, ensure_ascii=False))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if args.command == "draft":
|
||||||
|
return record_draft(args.week, args.file, args.author, args.model, note)
|
||||||
|
|
||||||
|
if args.command == "verdict":
|
||||||
|
return s.write_verdict(f"{args.finding}={args.verdict}", note, args.note, args.by)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
475
tools/project-state/collect.py
Normal file
475
tools/project-state/collect.py
Normal file
|
|
@ -0,0 +1,475 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Weekly evidence for the project-state pass. Deterministic; no model runs here.
|
||||||
|
|
||||||
|
Reads the forge for one ISO week — commits, Actions runs, issues, pulls,
|
||||||
|
releases — and the local SouveraineOS clone for what the task index and the SAF
|
||||||
|
declared, then writes project-state/evidence/<week>.json. Every row carries a
|
||||||
|
stable id; a finding cites those ids and nothing else.
|
||||||
|
|
||||||
|
tools/project-state/collect.py --week last
|
||||||
|
tools/project-state/collect.py --week 2026-W33 --out -
|
||||||
|
|
||||||
|
Env: GITEA_URL, GITEA_TOKEN (falls back to ~/.git-credentials).
|
||||||
|
See docs/tasks/79-project-state.md.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import datetime as dt
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import urllib.error
|
||||||
|
|
||||||
|
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__), "..", ".."))
|
||||||
|
VERSION = 1
|
||||||
|
|
||||||
|
|
||||||
|
def commits(forge, repo, start, end, note):
|
||||||
|
"""Commits in the window, every branch, deduped by SHA.
|
||||||
|
|
||||||
|
A branch whose tip predates the window cannot hold work inside it, so it is
|
||||||
|
never walked — the same prefilter the ledger's collector uses.
|
||||||
|
"""
|
||||||
|
full = f"{c.OWNER}/{repo}"
|
||||||
|
edge = c.iso_z(start)
|
||||||
|
rows, seen = [], set()
|
||||||
|
try:
|
||||||
|
branches = forge.pages(f"repos/{full}/branches")
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
note(f"{repo}: branches unavailable ({e.code})")
|
||||||
|
return []
|
||||||
|
live = [b["name"] for b in branches
|
||||||
|
if ((b.get("commit") or {}).get("timestamp") or "") >= edge]
|
||||||
|
for branch in live:
|
||||||
|
path = (f"repos/{full}/commits?sha={branch}&since={edge}&until={c.iso_z(end)}"
|
||||||
|
f"&stat=false&verification=false&files=false")
|
||||||
|
try:
|
||||||
|
page = forge.pages(path)
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
note(f"{repo}@{branch}: commits unavailable ({e.code})")
|
||||||
|
continue
|
||||||
|
for row in page:
|
||||||
|
sha = row.get("sha", "")
|
||||||
|
if sha in seen or len((row.get("parents") or [])) > 1:
|
||||||
|
continue
|
||||||
|
seen.add(sha)
|
||||||
|
commit = row.get("commit") or {}
|
||||||
|
author = commit.get("author") or {}
|
||||||
|
subject, _, body = (commit.get("message") or "").partition("\n")
|
||||||
|
paths = c.commit_paths(repo, sha)
|
||||||
|
dimension, basis = c.classify(repo, subject.strip(), paths)
|
||||||
|
rows.append({
|
||||||
|
"id": f"c:{repo}:{sha[:8]}",
|
||||||
|
"repo": repo,
|
||||||
|
"sha": sha,
|
||||||
|
"branch": branch,
|
||||||
|
"at": author.get("date", ""),
|
||||||
|
"author": author.get("email", ""),
|
||||||
|
"subject": subject.strip(),
|
||||||
|
"detail": re.split(r"\n\s*\n", body.strip(), maxsplit=1)[0].strip(),
|
||||||
|
"dimension": dimension,
|
||||||
|
"basis": basis,
|
||||||
|
"paths": paths[:40],
|
||||||
|
"url": row.get("html_url", ""),
|
||||||
|
})
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def runs(forge, repo, start, end, note):
|
||||||
|
"""Actions runs started inside the window.
|
||||||
|
|
||||||
|
This endpoint ignores limit and page and hands back the whole history, so it
|
||||||
|
is fetched once and sliced here — paging it would loop on the same rows.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
payload = forge.get(f"repos/{c.OWNER}/{repo}/actions/runs")
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
if e.code != 404:
|
||||||
|
note(f"{repo}: actions unavailable ({e.code})")
|
||||||
|
return []
|
||||||
|
rows = []
|
||||||
|
for run in payload.get("workflow_runs", []) if isinstance(payload, dict) else payload:
|
||||||
|
started = c.parse_iso(run.get("started_at") or run.get("run_started_at"))
|
||||||
|
if not started or not (start <= started < end):
|
||||||
|
continue
|
||||||
|
rows.append({
|
||||||
|
"id": f"ci:{repo}:{run.get('id')}",
|
||||||
|
"repo": repo,
|
||||||
|
"run": run.get("id"),
|
||||||
|
"workflow": os.path.basename(run.get("path") or ""),
|
||||||
|
"branch": run.get("head_branch", ""),
|
||||||
|
"sha": run.get("head_sha", ""),
|
||||||
|
"commit": f"c:{repo}:{(run.get('head_sha') or '')[:8]}",
|
||||||
|
"conclusion": run.get("conclusion") or run.get("status") or "",
|
||||||
|
"started": run.get("started_at", ""),
|
||||||
|
"completed": run.get("completed_at", ""),
|
||||||
|
"title": run.get("display_title", ""),
|
||||||
|
})
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def issues(forge, repo, start, end, note):
|
||||||
|
"""Every issue, open or closed — aging needs the ones that predate the week."""
|
||||||
|
try:
|
||||||
|
rows = forge.pages(f"repos/{c.OWNER}/{repo}/issues?state=all&type=issues")
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
if e.code != 404:
|
||||||
|
note(f"{repo}: issues unavailable ({e.code})")
|
||||||
|
return []
|
||||||
|
out = []
|
||||||
|
for i in rows:
|
||||||
|
created, closed = c.parse_iso(i.get("created_at")), c.parse_iso(i.get("closed_at"))
|
||||||
|
out.append({
|
||||||
|
"id": f"i:{repo}:{i.get('number')}",
|
||||||
|
"repo": repo,
|
||||||
|
"number": i.get("number"),
|
||||||
|
"title": i.get("title", ""),
|
||||||
|
"state": i.get("state", ""),
|
||||||
|
"labels": [l.get("name") for l in (i.get("labels") or [])],
|
||||||
|
"created": i.get("created_at", ""),
|
||||||
|
"closed": i.get("closed_at"),
|
||||||
|
"age_days": (end - created).days if created else None,
|
||||||
|
"opened_this_week": bool(created and start <= created < end),
|
||||||
|
"closed_this_week": bool(closed and start <= closed < end),
|
||||||
|
"url": i.get("html_url", ""),
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def pulls(forge, repo, start, end, note):
|
||||||
|
try:
|
||||||
|
rows = forge.pages(f"repos/{c.OWNER}/{repo}/pulls?state=all")
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
if e.code != 404:
|
||||||
|
note(f"{repo}: pulls unavailable ({e.code})")
|
||||||
|
return []
|
||||||
|
out = []
|
||||||
|
for p in rows:
|
||||||
|
created, merged = c.parse_iso(p.get("created_at")), c.parse_iso(p.get("merged_at"))
|
||||||
|
out.append({
|
||||||
|
"id": f"p:{repo}:{p.get('number')}",
|
||||||
|
"repo": repo,
|
||||||
|
"number": p.get("number"),
|
||||||
|
"title": p.get("title", ""),
|
||||||
|
"state": p.get("state", ""),
|
||||||
|
"created": p.get("created_at", ""),
|
||||||
|
"merged": p.get("merged_at"),
|
||||||
|
"age_days": (end - created).days if created else None,
|
||||||
|
"opened_this_week": bool(created and start <= created < end),
|
||||||
|
"merged_this_week": bool(merged and start <= merged < end),
|
||||||
|
"url": p.get("html_url", ""),
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def releases(forge, repo, start, end, note):
|
||||||
|
try:
|
||||||
|
rows = forge.pages(f"repos/{c.OWNER}/{repo}/releases")
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
if e.code != 404:
|
||||||
|
note(f"{repo}: releases unavailable ({e.code})")
|
||||||
|
return []
|
||||||
|
out = []
|
||||||
|
for r in rows:
|
||||||
|
published = c.parse_iso(r.get("published_at") or r.get("created_at"))
|
||||||
|
out.append({
|
||||||
|
"id": f"r:{repo}:{r.get('tag_name')}",
|
||||||
|
"repo": repo,
|
||||||
|
"tag": r.get("tag_name", ""),
|
||||||
|
"name": r.get("name", ""),
|
||||||
|
"published": r.get("published_at") or r.get("created_at", ""),
|
||||||
|
"assets": len(r.get("assets") or []),
|
||||||
|
"draft": bool(r.get("draft")),
|
||||||
|
"cut_this_week": bool(published and start <= published < end),
|
||||||
|
"age_days": (end - published).days if published else None,
|
||||||
|
"url": r.get("html_url", ""),
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# ---- declared state --------------------------------------------------------
|
||||||
|
|
||||||
|
TASK_ROW = re.compile(r"^\|\s*(\d+)\s*\|\s*\[([^\]]+)\]\(([^)]+)\)\s*\|\s*(.*?)\s*\|\s*$")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_index(text):
|
||||||
|
"""The task index as {number: {section, title, left}} — the hand-written state."""
|
||||||
|
section, out = "", {}
|
||||||
|
for line in text.splitlines():
|
||||||
|
if line.startswith("## "):
|
||||||
|
section = line[3:].split("—")[0].strip()
|
||||||
|
continue
|
||||||
|
m = TASK_ROW.match(line)
|
||||||
|
if m:
|
||||||
|
# The index writes both `| 08 |` and `| 8 |` for task 8; unnormalised,
|
||||||
|
# one task carried two keys and closed itself while still standing.
|
||||||
|
out[str(int(m.group(1)))] = {
|
||||||
|
"section": section,
|
||||||
|
"title": m.group(2).strip(),
|
||||||
|
"file": m.group(3).strip(),
|
||||||
|
"left": m.group(4).strip(),
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def blob_at(path, sha, filename):
|
||||||
|
return c.git(path, "show", f"{sha}:{filename}")
|
||||||
|
|
||||||
|
|
||||||
|
def boundary(path, when, branch="main"):
|
||||||
|
"""Newest commit at or before an instant — the index as it stood then."""
|
||||||
|
out = c.git(path, "rev-list", "-1", f"--before={c.iso_z(when)}", branch)
|
||||||
|
return out.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def declared(start, end, note, snapshot):
|
||||||
|
"""What the task index and the SAF said before and after the week."""
|
||||||
|
path = c.checkout("SouveraineOS")
|
||||||
|
if not path:
|
||||||
|
note("SouveraineOS is not checked out — declared state skipped")
|
||||||
|
return []
|
||||||
|
before, after = boundary(path, start), boundary(path, end)
|
||||||
|
if not before or not after:
|
||||||
|
note("no SouveraineOS commit on one side of the week — declared state skipped")
|
||||||
|
return []
|
||||||
|
index_file = "docs/tasks/README.md"
|
||||||
|
was = parse_index(blob_at(path, before, index_file))
|
||||||
|
now = parse_index(blob_at(path, after, index_file))
|
||||||
|
snapshot.update(now)
|
||||||
|
rows = []
|
||||||
|
for number in sorted(set(was) | set(now), key=int):
|
||||||
|
old, new = was.get(number), now.get(number)
|
||||||
|
if old and new and old == new:
|
||||||
|
continue
|
||||||
|
if not old:
|
||||||
|
change, detail = "listed", new["section"]
|
||||||
|
elif not new:
|
||||||
|
change, detail = "delisted", old["section"]
|
||||||
|
elif old["section"] != new["section"]:
|
||||||
|
change, detail = "moved", f"{old['section']} → {new['section']}"
|
||||||
|
else:
|
||||||
|
change, detail = "restated", "what's left rewritten"
|
||||||
|
rows.append({
|
||||||
|
"id": f"d:task-{number}",
|
||||||
|
"kind": "task-index",
|
||||||
|
"task": number,
|
||||||
|
"title": (new or old)["title"],
|
||||||
|
"file": (new or old)["file"],
|
||||||
|
"change": change,
|
||||||
|
"detail": detail,
|
||||||
|
"section": (new or old)["section"],
|
||||||
|
"was": old["left"] if old else None,
|
||||||
|
"now": new["left"] if new else None,
|
||||||
|
"dimension": c.classify_text(
|
||||||
|
f"{(new or old)['title']} {(new or old)['file']} {(new or old)['left']}")[0],
|
||||||
|
})
|
||||||
|
|
||||||
|
status = {"A": "added", "M": "revised", "D": "deleted", "R": "moved", "C": "copied"}
|
||||||
|
changed = c.git(path, "diff", "--name-status", before, after, "--", "saf/", "docs/")
|
||||||
|
for line in changed.splitlines():
|
||||||
|
fields = line.split("\t")
|
||||||
|
if len(fields) < 2:
|
||||||
|
continue
|
||||||
|
filename = fields[-1]
|
||||||
|
if filename.startswith("docs/tasks/"):
|
||||||
|
continue
|
||||||
|
log = c.git(path, "log", "--format=%h|%aI|%s", f"{before}..{after}", "--", filename)
|
||||||
|
touches = [entry.split("|", 2) for entry in log.splitlines() if entry]
|
||||||
|
rows.append({
|
||||||
|
"id": f"d:{filename}",
|
||||||
|
"kind": "document",
|
||||||
|
"file": filename,
|
||||||
|
"change": status.get(fields[0][0], fields[0]),
|
||||||
|
"detail": f"{len(touches)} commit(s)",
|
||||||
|
"commits": [t[0] for t in touches],
|
||||||
|
"subjects": [t[2] for t in touches],
|
||||||
|
"dimension": c.classify_text(f"{filename} {' '.join(t[2] for t in touches)}")[0],
|
||||||
|
})
|
||||||
|
if not rows:
|
||||||
|
note("declared state did not move this week")
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
# ---- rollup ----------------------------------------------------------------
|
||||||
|
|
||||||
|
def rollup(evidence, start, end):
|
||||||
|
by_dim = {}
|
||||||
|
for row in evidence["commits"]:
|
||||||
|
d = by_dim.setdefault(row["dimension"], {"commits": 0, "repos": [], "authors": []})
|
||||||
|
d["commits"] += 1
|
||||||
|
for key, value in (("repos", row["repo"]), ("authors", row["author"])):
|
||||||
|
if value and value not in d[key]:
|
||||||
|
d[key].append(value)
|
||||||
|
for row in evidence["declared"]:
|
||||||
|
d = by_dim.setdefault(row["dimension"], {"commits": 0, "repos": [], "authors": []})
|
||||||
|
d["declared"] = d.get("declared", 0) + 1
|
||||||
|
|
||||||
|
ci = {}
|
||||||
|
for row in evidence["ci"]:
|
||||||
|
r = ci.setdefault(row["repo"], {"runs": 0, "success": 0, "failure": 0, "other": 0})
|
||||||
|
r["runs"] += 1
|
||||||
|
key = row["conclusion"] if row["conclusion"] in ("success", "failure") else "other"
|
||||||
|
r[key] += 1
|
||||||
|
for repo, r in ci.items():
|
||||||
|
newest = max((row for row in evidence["ci"] if row["repo"] == repo),
|
||||||
|
key=lambda row: row["started"], default=None)
|
||||||
|
r["newest"] = {"id": newest["id"], "conclusion": newest["conclusion"]} if newest else None
|
||||||
|
|
||||||
|
open_issues = [i for i in evidence["issues"] if i["state"] == "open"]
|
||||||
|
open_pulls = [p for p in evidence["pulls"] if p["state"] == "open"]
|
||||||
|
basis = {}
|
||||||
|
for row in evidence["commits"]:
|
||||||
|
basis[row["basis"]] = basis.get(row["basis"], 0) + 1
|
||||||
|
return {
|
||||||
|
"by_dimension": by_dim,
|
||||||
|
"ci": ci,
|
||||||
|
"issues": {
|
||||||
|
"open": len(open_issues),
|
||||||
|
"opened_this_week": sum(i["opened_this_week"] for i in evidence["issues"]),
|
||||||
|
"closed_this_week": sum(i["closed_this_week"] for i in evidence["issues"]),
|
||||||
|
"ever_closed": sum(1 for i in evidence["issues"] if i["closed"]),
|
||||||
|
"oldest_open": max(open_issues, key=lambda i: i["age_days"] or 0)["id"] if open_issues else None,
|
||||||
|
"oldest_open_days": max((i["age_days"] or 0) for i in open_issues) if open_issues else 0,
|
||||||
|
},
|
||||||
|
"pulls": {
|
||||||
|
"open": len(open_pulls),
|
||||||
|
"merged_this_week": sum(p["merged_this_week"] for p in evidence["pulls"]),
|
||||||
|
"oldest_open_days": max((p["age_days"] or 0) for p in open_pulls) if open_pulls else 0,
|
||||||
|
},
|
||||||
|
"releases": {
|
||||||
|
"cut_this_week": [r["id"] for r in evidence["releases"] if r["cut_this_week"]],
|
||||||
|
"newest_age_days": min((r["age_days"] or 0) for r in evidence["releases"]) if evidence["releases"] else None,
|
||||||
|
},
|
||||||
|
"dimension_basis": basis,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def held():
|
||||||
|
"""Weeks that already have evidence on disk."""
|
||||||
|
path = os.path.join(ROOT, "project-state", "evidence")
|
||||||
|
return sorted(name[:-5] for name in os.listdir(path)) if os.path.isdir(path) else []
|
||||||
|
|
||||||
|
|
||||||
|
def gaps(since=None):
|
||||||
|
"""Weeks with no evidence, from the first week held (or --since) to now.
|
||||||
|
|
||||||
|
Nothing runs this on a schedule. The tool has to be able to say which weeks
|
||||||
|
are unfilled, or a missed Monday quietly becomes a hole in the record.
|
||||||
|
"""
|
||||||
|
have = held()
|
||||||
|
current, _, _ = c.parse_week("current")
|
||||||
|
first = since or (have[0] if have else current)
|
||||||
|
return [week for week in c.week_span(first, current) if week not in have]
|
||||||
|
|
||||||
|
|
||||||
|
def collect_week(week, args, forge=None):
|
||||||
|
label, monday, sunday = c.parse_week(week)
|
||||||
|
start, end = c.week_bounds_utc(monday, sunday)
|
||||||
|
repos = [r.strip() for r in args.repos.split(",")] if args.repos else c.REPOS
|
||||||
|
forge = forge or c.Forge()
|
||||||
|
|
||||||
|
notes = []
|
||||||
|
def note(message):
|
||||||
|
notes.append(message)
|
||||||
|
print(f"[project-state] {message}", file=sys.stderr)
|
||||||
|
|
||||||
|
evidence = {"commits": [], "ci": [], "issues": [], "pulls": [], "releases": []}
|
||||||
|
repo_summary = {}
|
||||||
|
before = forge.calls
|
||||||
|
for repo in repos:
|
||||||
|
rows = commits(forge, repo, start, end, note)
|
||||||
|
evidence["commits"] += rows
|
||||||
|
evidence["ci"] += runs(forge, repo, start, end, note)
|
||||||
|
evidence["issues"] += issues(forge, repo, start, end, note)
|
||||||
|
evidence["pulls"] += pulls(forge, repo, start, end, note)
|
||||||
|
evidence["releases"] += releases(forge, repo, start, end, note)
|
||||||
|
repo_summary[repo] = {"commits": len(rows), "checkout": bool(c.checkout(repo))}
|
||||||
|
index = {}
|
||||||
|
evidence["declared"] = declared(start, end, note, index)
|
||||||
|
evidence["index"] = index
|
||||||
|
|
||||||
|
for key in ("commits", "ci"):
|
||||||
|
evidence[key].sort(key=lambda row: row.get("at") or row.get("started") or "")
|
||||||
|
|
||||||
|
if args.redact:
|
||||||
|
for row in evidence["commits"]:
|
||||||
|
row["subject"] = c.redact(row["subject"])
|
||||||
|
row["detail"] = c.redact(row["detail"])
|
||||||
|
|
||||||
|
out = {
|
||||||
|
"week": label,
|
||||||
|
"range": {
|
||||||
|
"start": monday.isoformat(), "end": sunday.isoformat(), "tz": str(c.LOCAL),
|
||||||
|
"start_utc": c.iso_z(start), "end_utc": c.iso_z(end),
|
||||||
|
},
|
||||||
|
"collected_at": c.iso_z(dt.datetime.now(dt.timezone.utc)),
|
||||||
|
"collector": {
|
||||||
|
"tool": "tools/project-state/collect.py",
|
||||||
|
"version": VERSION,
|
||||||
|
"forge": f"gitea {forge.version()}",
|
||||||
|
"api_calls": forge.calls - before,
|
||||||
|
"redacted": bool(args.redact),
|
||||||
|
"repos": repo_summary,
|
||||||
|
"notes": notes,
|
||||||
|
},
|
||||||
|
**evidence,
|
||||||
|
}
|
||||||
|
out["rollup"] = rollup(out, start, end)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def write(out, path):
|
||||||
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||||
|
open(path, "w").write(json.dumps(out, indent=2, ensure_ascii=False) + "\n")
|
||||||
|
print(f"[project-state] {out['week']}: {len(out['commits'])} commits, {len(out['ci'])} runs, "
|
||||||
|
f"{len(out['declared'])} declared changes, {out['collector']['api_calls']} api calls "
|
||||||
|
f"-> {path}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--week", default="last", help="2026-W33 | current | last (default)")
|
||||||
|
ap.add_argument("--repos", help="comma-separated override of the repo list")
|
||||||
|
ap.add_argument("--out", help="output path, '-' for stdout")
|
||||||
|
ap.add_argument("--redact", action="store_true",
|
||||||
|
help="mask hosts and home paths — for evidence leaving the forge")
|
||||||
|
ap.add_argument("--gaps", action="store_true", help="name the weeks holding no evidence")
|
||||||
|
ap.add_argument("--backfill", action="store_true", help="collect every week --gaps names")
|
||||||
|
ap.add_argument("--since", help="earliest week --gaps and --backfill consider")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
if args.gaps or args.backfill:
|
||||||
|
missing = gaps(args.since)
|
||||||
|
if not missing:
|
||||||
|
print(f"[project-state] no gaps; {len(held())} week(s) held", file=sys.stderr)
|
||||||
|
return 0
|
||||||
|
if args.gaps:
|
||||||
|
print("\n".join(missing))
|
||||||
|
return 0
|
||||||
|
forge = c.Forge()
|
||||||
|
if not forge.token:
|
||||||
|
print("project-state: no forge token (set GITEA_TOKEN)", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
for week in missing:
|
||||||
|
write(collect_week(week, args, forge),
|
||||||
|
os.path.join(ROOT, "project-state", "evidence", f"{week}.json"))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
forge = c.Forge()
|
||||||
|
if not forge.token:
|
||||||
|
print("project-state: no forge token (set GITEA_TOKEN)", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
out = collect_week(args.week, args, forge)
|
||||||
|
if args.out == "-":
|
||||||
|
sys.stdout.write(json.dumps(out, indent=2, ensure_ascii=False) + "\n")
|
||||||
|
return 0
|
||||||
|
write(out, args.out or os.path.join(ROOT, "project-state", "evidence", f"{out['week']}.json"))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
276
tools/project-state/common.py
Normal file
276
tools/project-state/common.py
Normal file
|
|
@ -0,0 +1,276 @@
|
||||||
|
"""Shared pieces for the project-state pass: forge client, weeks, dimensions.
|
||||||
|
|
||||||
|
See docs/tasks/79-project-state.md for what this is for and why it is shaped
|
||||||
|
this way.
|
||||||
|
"""
|
||||||
|
import datetime as dt
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
|
LOCAL = ZoneInfo("America/Toronto")
|
||||||
|
OWNER = "Fimeg"
|
||||||
|
PROJECTS = os.path.expanduser("~/Projects")
|
||||||
|
|
||||||
|
# Listed, not discovered. A repo joining Souveraine is a deliberate addition
|
||||||
|
# here, and one that goes quiet should be visible as a quiet row rather than
|
||||||
|
# silently stop being read. Same reasoning as tools/ci-status.sh.
|
||||||
|
REPOS = [
|
||||||
|
"souveraine", "SouveraineOS", "Pixel3Arch", "souveraine-viewtop",
|
||||||
|
"souveraine-updater", "souveraine-lens", "souveraine-speech",
|
||||||
|
"souveraine-player", "souveraine-usb", "culver", "linux-blueline",
|
||||||
|
"hexagonrpc", "smithay", "pocketboot", "libcmatrix", "upower",
|
||||||
|
]
|
||||||
|
|
||||||
|
# The SAF sections, plus the two the pipeline owns and SAF has no page for.
|
||||||
|
DIMENSIONS = [
|
||||||
|
"memory", "runtime", "reflection", "identity", "surface",
|
||||||
|
"sensorium", "release", "reliability", "federation", "adoption",
|
||||||
|
]
|
||||||
|
|
||||||
|
UNASSIGNED = "unassigned"
|
||||||
|
|
||||||
|
# Longer keywords score higher, so "device state" beats a bare "state" and a
|
||||||
|
# path full of a subsystem's name beats one incidental word in a subject.
|
||||||
|
KEYWORDS = {
|
||||||
|
"memory": ["memory", "memfs", "compaction", "archivist", "recall", "vault", "memoir"],
|
||||||
|
"runtime": ["server", "handler", "runtime", "substrate", "provider", "tool call",
|
||||||
|
"stream", "sse", "prompt", "context", "turn", "inference", "token"],
|
||||||
|
"reflection": ["reflection", "subconscious", "cadence", "n+1", "aster", "introspect"],
|
||||||
|
"identity": ["identity", "authority", "sessiond", "principal", "secrets", "machined",
|
||||||
|
"trust", "login", "credential", "permission", "polkit", "device state",
|
||||||
|
"device_state", "admission"],
|
||||||
|
"surface": ["shell", "panel", "tui", "surface", "qml", "dock", "dial", "osk",
|
||||||
|
"keyboard", "island", "overview", "compositor", "viewtop", "window",
|
||||||
|
"gesture", "lockscreen", "card", "render"],
|
||||||
|
"sensorium": ["sensor", "grip", "battery", "charge", "audio", "microphone", "camera",
|
||||||
|
"haptic", "bluetooth", "airpods", "librepods", "modem", "bearer",
|
||||||
|
"brightness", "fingerprint", "touch", "squeeze"],
|
||||||
|
"release": ["release", "package", "pkgbuild", "pacman", "repo-add", "distribution",
|
||||||
|
"artifact", "signing", "manifest"],
|
||||||
|
"reliability": ["crash", "stall", "deadlock", "leak", "regression", "revert",
|
||||||
|
"hotfix", "clippy", "flake", "panic", "timeout", "retry", "workflow"],
|
||||||
|
"federation": ["federation", "enrol", "enroll", "peer", "mirror", "distributed",
|
||||||
|
"multi-device", "household", "sync"],
|
||||||
|
"adoption": ["license", "licence", "install", "provision", "onboard",
|
||||||
|
"quickstart", "getting started", "contributor"],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Two path shapes that no keyword scan should have to guess at.
|
||||||
|
PATH_RULES = [
|
||||||
|
("reliability", r"(^|/)\.gitea/"),
|
||||||
|
("release", r"(^|/)(packaging|pkgs?)/|PKGBUILD"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Only for a commit that neither its paths nor its subject place. A repo whose
|
||||||
|
# whole purpose is one dimension answers for it.
|
||||||
|
REPO_DEFAULT = {
|
||||||
|
"souveraine": "runtime",
|
||||||
|
"Pixel3Arch": "sensorium",
|
||||||
|
"linux-blueline": "sensorium",
|
||||||
|
"souveraine-viewtop": "surface",
|
||||||
|
"souveraine-lens": "adoption",
|
||||||
|
"souveraine-updater": "release",
|
||||||
|
"souveraine-player": "surface",
|
||||||
|
"souveraine-speech": "sensorium",
|
||||||
|
"souveraine-usb": "federation",
|
||||||
|
"culver": "surface",
|
||||||
|
"hexagonrpc": "sensorium",
|
||||||
|
"smithay": "surface",
|
||||||
|
"pocketboot": "release",
|
||||||
|
"libcmatrix": "surface",
|
||||||
|
"upower": "sensorium",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Commit subjects are written for the man who owns the machines. Evidence stays
|
||||||
|
# raw because it never leaves the forge; this is here for whatever gets promoted
|
||||||
|
# to a public surface later. Same patterns as the ledger's collector.
|
||||||
|
REDACTIONS = [
|
||||||
|
(re.compile(r'\b(?:10|127)(?:\.\d{1,3}){2,3}(?::\d+)?\b'), "[host]"),
|
||||||
|
(re.compile(r'\b192\.168(?:\.\d{1,3}){1,2}(?::\d+)?\b'), "[host]"),
|
||||||
|
(re.compile(r'\b172\.(?:1[6-9]|2\d|3[01])(?:\.\d{1,3}){1,2}(?::\d+)?\b'), "[host]"),
|
||||||
|
(re.compile(r'\b(?:WIUF[-\w]*|wiufph|archdev)\b', re.I), "[host]"),
|
||||||
|
(re.compile(r'/home/casey|/root\b'), "~"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def redact(text):
|
||||||
|
for pattern, mask in REDACTIONS:
|
||||||
|
text = pattern.sub(mask, text)
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def score(text):
|
||||||
|
"""Dimension scores for any text; ties go to the earlier dimension."""
|
||||||
|
low = (text or "").lower()
|
||||||
|
hits = {d: sum(len(k) for k in words if k in low) for d, words in KEYWORDS.items()}
|
||||||
|
hits = {d: s for d, s in hits.items() if s}
|
||||||
|
if not hits:
|
||||||
|
return None, {}
|
||||||
|
best = max(hits, key=lambda d: (hits[d], -DIMENSIONS.index(d)))
|
||||||
|
return best, hits
|
||||||
|
|
||||||
|
|
||||||
|
def classify_text(text):
|
||||||
|
"""For a task row or a document: what is this about, or nothing."""
|
||||||
|
best, _ = score(text)
|
||||||
|
return (best, "text") if best else (UNASSIGNED, "none")
|
||||||
|
|
||||||
|
|
||||||
|
def classify(repo, subject, paths=None):
|
||||||
|
"""One dimension for a commit, and the basis that chose it.
|
||||||
|
|
||||||
|
Paths and subject are both read and weighted alike. Paths alone put
|
||||||
|
`compaction: target the active conversation` under the server module it
|
||||||
|
edits; the subject is the author saying which subsystem he thinks he moved.
|
||||||
|
"""
|
||||||
|
joined = " ".join(paths or [])
|
||||||
|
_, from_paths = score(joined)
|
||||||
|
_, from_subject = score(subject)
|
||||||
|
total = {d: from_paths.get(d, 0) + from_subject.get(d, 0)
|
||||||
|
for d in set(from_paths) | set(from_subject)}
|
||||||
|
if total:
|
||||||
|
best = max(total, key=lambda d: (total[d], -DIMENSIONS.index(d)))
|
||||||
|
return best, "paths" if from_paths.get(best, 0) else "subject"
|
||||||
|
for dim, pattern in PATH_RULES:
|
||||||
|
if joined and re.search(pattern, joined, re.I):
|
||||||
|
return dim, "paths"
|
||||||
|
if repo in REPO_DEFAULT:
|
||||||
|
return REPO_DEFAULT[repo], "repo"
|
||||||
|
return UNASSIGNED, "none"
|
||||||
|
|
||||||
|
|
||||||
|
# ---- weeks -----------------------------------------------------------------
|
||||||
|
|
||||||
|
def parse_week(spec, today=None):
|
||||||
|
"""'2026-W33', 'current', 'last' -> (label, monday, sunday) as local dates."""
|
||||||
|
today = today or dt.datetime.now(LOCAL).date()
|
||||||
|
if spec in ("current", "last", None):
|
||||||
|
monday = today - dt.timedelta(days=today.isoweekday() - 1)
|
||||||
|
if spec == "last":
|
||||||
|
monday -= dt.timedelta(days=7)
|
||||||
|
else:
|
||||||
|
m = re.fullmatch(r"(\d{4})-?W(\d{1,2})", spec.upper())
|
||||||
|
if not m:
|
||||||
|
raise ValueError(f"week must look like 2026-W33, got {spec!r}")
|
||||||
|
monday = dt.date.fromisocalendar(int(m.group(1)), int(m.group(2)), 1)
|
||||||
|
year, week, _ = monday.isocalendar()
|
||||||
|
return f"{year}-W{week:02d}", monday, monday + dt.timedelta(days=6)
|
||||||
|
|
||||||
|
|
||||||
|
def week_bounds_utc(monday, sunday):
|
||||||
|
"""Local week -> UTC instants, so a Sunday-evening commit stays in its week."""
|
||||||
|
start = dt.datetime.combine(monday, dt.time.min, LOCAL).astimezone(dt.timezone.utc)
|
||||||
|
end = dt.datetime.combine(sunday + dt.timedelta(days=1), dt.time.min, LOCAL).astimezone(dt.timezone.utc)
|
||||||
|
return start, end
|
||||||
|
|
||||||
|
|
||||||
|
def week_monday(label):
|
||||||
|
year, week = label.split("-W")
|
||||||
|
return dt.date.fromisocalendar(int(year), int(week), 1)
|
||||||
|
|
||||||
|
|
||||||
|
def label_for(monday):
|
||||||
|
y, w, _ = monday.isocalendar()
|
||||||
|
return f"{y}-W{w:02d}"
|
||||||
|
|
||||||
|
|
||||||
|
def previous_week(label):
|
||||||
|
return label_for(week_monday(label) - dt.timedelta(days=7))
|
||||||
|
|
||||||
|
|
||||||
|
def week_span(first, last):
|
||||||
|
"""Every ISO week label from first to last inclusive, in order."""
|
||||||
|
monday, end, out = week_monday(first), week_monday(last), []
|
||||||
|
while monday <= end:
|
||||||
|
out.append(label_for(monday))
|
||||||
|
monday += dt.timedelta(days=7)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def iso_z(when):
|
||||||
|
return when.astimezone(dt.timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_iso(value):
|
||||||
|
if not value:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return dt.datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# ---- forge -----------------------------------------------------------------
|
||||||
|
|
||||||
|
class Forge:
|
||||||
|
# Gitea clamps a page to MAX_RESPONSE_ITEMS (50) and ignores a larger limit
|
||||||
|
# without saying so. The ledger lost 24 of 74 repos to this for months.
|
||||||
|
PAGE = 50
|
||||||
|
|
||||||
|
def __init__(self, base=None, token=None):
|
||||||
|
self.base = (base or os.environ.get("GITEA_URL") or "http://10.10.20.120:4455").rstrip("/")
|
||||||
|
self.token = token or os.environ.get("GITEA_TOKEN") or self._stored_token()
|
||||||
|
self.calls = 0
|
||||||
|
|
||||||
|
def _stored_token(self):
|
||||||
|
path = os.path.expanduser("~/.git-credentials")
|
||||||
|
host = urllib.parse.urlsplit(self.base).hostname or ""
|
||||||
|
try:
|
||||||
|
for line in open(path):
|
||||||
|
parsed = urllib.parse.urlsplit(line.strip())
|
||||||
|
if parsed.hostname and parsed.hostname.split("%")[0] == host:
|
||||||
|
return parsed.password or ""
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def get(self, path):
|
||||||
|
req = urllib.request.Request(f"{self.base}/api/v1/{path}")
|
||||||
|
if self.token:
|
||||||
|
req.add_header("Authorization", f"token {self.token}")
|
||||||
|
self.calls += 1
|
||||||
|
with urllib.request.urlopen(req, timeout=30) as r:
|
||||||
|
return json.load(r)
|
||||||
|
|
||||||
|
def pages(self, path, max_pages=40):
|
||||||
|
sep = "&" if "?" in path else "?"
|
||||||
|
out = []
|
||||||
|
for page in range(1, max_pages + 1):
|
||||||
|
chunk = self.get(f"{path}{sep}limit={self.PAGE}&page={page}")
|
||||||
|
if isinstance(chunk, dict):
|
||||||
|
chunk = chunk.get("workflow_runs") or chunk.get("data") or []
|
||||||
|
if not chunk:
|
||||||
|
return out
|
||||||
|
out += chunk
|
||||||
|
if len(chunk) < self.PAGE:
|
||||||
|
return out
|
||||||
|
raise RuntimeError(f"{path}: {max_pages} pages and still going")
|
||||||
|
|
||||||
|
def version(self):
|
||||||
|
return self.get("version").get("version", "?")
|
||||||
|
|
||||||
|
|
||||||
|
# ---- local checkouts -------------------------------------------------------
|
||||||
|
|
||||||
|
def checkout(repo):
|
||||||
|
path = os.path.join(PROJECTS, repo)
|
||||||
|
return path if os.path.isdir(os.path.join(path, ".git")) else None
|
||||||
|
|
||||||
|
|
||||||
|
def git(path, *args):
|
||||||
|
proc = subprocess.run(["git", "-C", path, *args], capture_output=True, text=True)
|
||||||
|
return proc.stdout if proc.returncode == 0 else ""
|
||||||
|
|
||||||
|
|
||||||
|
def commit_paths(repo, sha):
|
||||||
|
"""Changed paths from the local clone, empty when it doesn't have the SHA."""
|
||||||
|
path = checkout(repo)
|
||||||
|
if not path:
|
||||||
|
return []
|
||||||
|
out = git(path, "show", "--name-only", "--format=", "--no-renames", sha)
|
||||||
|
return [line for line in out.splitlines() if line.strip()]
|
||||||
821
tools/project-state/synthesize.py
Normal file
821
tools/project-state/synthesize.py
Normal file
|
|
@ -0,0 +1,821 @@
|
||||||
|
#!/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)
|
||||||
Loading…
Reference in a new issue