@r1z4x/tezgah 1.5.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +126 -1
  2. package/MANIFEST +11 -0
  3. package/README.md +6 -3
  4. package/bin/consult +13 -10
  5. package/bin/tezgah-docs +18 -16
  6. package/bin/tezgah-doctor +152 -0
  7. package/bin/tezgah-research +286 -12
  8. package/bin/tezgah-route +48 -1
  9. package/bin/tezgah-setup +27 -14
  10. package/bin/tezgah-task +771 -46
  11. package/bin/tezgah-taste +48 -10
  12. package/docs/architecture.md +7 -4
  13. package/docs/citations-baseline.json +1 -1
  14. package/docs/clarity-baseline.json +5 -5
  15. package/docs/contract.md +112 -25
  16. package/docs/evidence.md +201 -28
  17. package/docs/feature-audit.md +25 -4
  18. package/docs/gate.md +70 -33
  19. package/docs/glossary.md +4 -4
  20. package/docs/hosts.md +8 -8
  21. package/docs/index.json +30 -10
  22. package/docs/judge.md +9 -3
  23. package/docs/models.md +35 -2
  24. package/docs/operations.md +103 -3
  25. package/docs/research.md +392 -13
  26. package/docs/skills.md +24 -6
  27. package/docs/status-line.md +5 -5
  28. package/docs/testing.md +2 -2
  29. package/docs/vision.md +12 -4
  30. package/hooks/hooks.json +2 -2
  31. package/hooks/lang/en.json +323 -0
  32. package/hooks/lang/tr.json +344 -0
  33. package/hooks/projects-posttooluse.py +24 -5
  34. package/hooks/projects-pretooluse.py +9 -4
  35. package/hooks/projects-stop.py +28 -16
  36. package/hooks/tezgah_agents.py +29 -9
  37. package/hooks/tezgah_clarity.py +7 -3
  38. package/hooks/tezgah_components.py +2 -1
  39. package/hooks/tezgah_context.py +460 -390
  40. package/hooks/tezgah_gate.py +640 -104
  41. package/hooks/tezgah_integrity.py +1480 -197
  42. package/hooks/tezgah_models.py +337 -15
  43. package/hooks/tezgah_paths.py +103 -10
  44. package/hooks/tezgah_policy.py +286 -130
  45. package/hooks/tezgah_product.py +107 -0
  46. package/hooks/tezgah_rank.py +29 -27
  47. package/hooks/tezgah_replay.py +200 -30
  48. package/hooks/tezgah_research.py +1297 -12
  49. package/hooks/tezgah_skill_pick.py +58 -17
  50. package/hooks/tezgah_stopspec.py +7 -0
  51. package/hooks/tezgah_store.py +74 -27
  52. package/hooks/tezgah_task.py +917 -8
  53. package/hosts/codex/hook.py +9 -10
  54. package/hosts/cursor/hook.py +21 -14
  55. package/hosts/dsh/hooks.json +2 -2
  56. package/hosts/omp/hook.py +44 -23
  57. package/hosts/omp/tezgah-hook.ts.in +93 -7
  58. package/hosts/opencode/plugins/tezgah.js +119 -39
  59. package/output-styles/tezgah.md +76 -42
  60. package/package.json +1 -1
  61. package/skills/analyze-app/SKILL.md +57 -0
  62. package/skills/design-contract/EVALS.md +24 -1
  63. package/skills/design-contract/SKILL.md +17 -1
  64. package/skills/design-contract/data-views.md +86 -0
  65. package/skills/feature-audit/SKILL.md +32 -3
  66. package/skills/i-have-adhd/EVALS.md +22 -2
  67. package/skills/i-have-adhd/SKILL.md +12 -5
  68. package/skills/plan-add/EVALS.md +8 -8
  69. package/skills/plan-add/SKILL.md +22 -21
  70. package/skills/plan-sync/EVALS.md +2 -2
  71. package/skills/plan-sync/SKILL.md +14 -8
  72. package/skills/pm-frameworks/SKILL.md +6 -0
  73. package/skills/ponytail/EVALS.md +21 -2
  74. package/skills/ponytail/SKILL.md +4 -4
  75. package/skills/product-analysis/EVALS.md +23 -1
  76. package/skills/product-analysis/SKILL.md +50 -11
  77. package/skills/product-flow/EVALS.md +81 -0
  78. package/skills/product-flow/SKILL.md +91 -0
  79. package/skills/research/SKILL.md +99 -2
  80. package/skills/tezgah-contract/EVALS.md +52 -7
  81. package/skills/tezgah-contract/SKILL.md +204 -85
package/CHANGELOG.md CHANGED
@@ -6,6 +6,129 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.7.0] - 2026-10-12
10
+
11
+ ### Added
12
+
13
+ - **Exploratory research lines (rules 7).** An ask for new ideas, methods or approaches now gets a campaign: `tezgah-research init --method exploratory`. Its arena holds the problem and a budget, and the line's scope says what it covers. A measurable arena adds an evaluator, its sha256 and its metric. The session commits the arena before the first idea. `idea`, `stage` and `match` append `ideas.jsonl`, `stages.jsonl` and `tournament.jsonl` under the lock. One function judges each row type for the writer and for `check`. An idea moves forward only, and each stage needs the one before it: prior art, then screening, a pilot, a full run and a confirmation. A kill with a reason ends it at any stage, and a duplicate ends in a kill. Prior art needs three indexed nearest works and a verdict, and `known` kills the idea in the same append. `check` refuses ideas or results committed before the arena and an arena changed after it. It refuses every stage row while the evaluator hash differs. It also refuses a confirmation on the promoting run. The same goes for a set another idea looked at first, or one this idea looked at before its promotion. It warns about thin judging and about a report that hides how many ideas it tested. `board`, `dedup` and `yield` are reports. `board` gives a Bradley-Terry prior and each idea's next action, and it is the resume point after a crash. `dedup` names its backend, embed-mrl or BM25. Its thresholds come from 40 fixture pairs and 400 human-labelled MRPC pairs. `yield` counts per operator and model family. Lines opened under rules 1-6 read none of it. The research paragraph armed per turn grows by 149 characters, and the on-demand research rule by 593.
14
+ - **An ask policy.** One class table in the contract now says what is asked at all. Work and process steps, a local merge of the session's branch included, are taken, never asked. A push or a release runs on a grant the user wrote in chat ("bana sormadan pushla") and is otherwise asked once. A force push, a delete, production, a credential or a spend over the new `spend_cap` setting waits for an answer. Every other question goes in one "Onay için sorular" batch with a recommended default, and the work goes on with the defaults. A new Stop class, `needless ask`, refuses once per turn a reply that ends on a question the agent can decide. A KARAR item with no options or default is no blocker any more. An omp `ask` or Claude AskUserQuestion question with no recommended option gets a warning, never a deny, and leaves an `ask` row. Each prompt's grants and lifts leave `grant` rows. `tezgah-gate replay` prints four new counts: process questions, cheap answers, ask-tool questions without a default and outward turns without a grant.
15
+ - **Language packs.** The detection words now live in data files, one per language: `hooks/lang/en.json` and `hooks/lang/tr.json`. They cover claims, admissions, openers, closers, residual labels, blocker tokens, prompt hints, constraint shapes and ranking stopwords. Every reader compiles the union of the packs, so a third language is one file. On a corpus of 45,810 test, doc and transcript strings, the en and tr packs answer as the inlined regexes did.
16
+ - **`language` and `--language`.** The reply-language setting is `language` in `config.json`. An older `reply_lang` is still read and never rewritten, and `--reply-lang` still works. A first install takes the locale (`LANGUAGE`, `LC_ALL`, `LC_MESSAGES`, `LANG`), and a locale with no pack gives English. An install over a config with neither key writes `tr`.
17
+ - **A router context nudge.** The prompt hook reads the router's newest input-side token count from the transcript. This works on omp, Claude and Codex, and omp now hands its session file to the prompt hook for it. At 700 k or more it adds one line, once per session until a compaction. The line says to push heavy reads and check output through a slice, or to run `/compact`. The threshold sits just under the measured p90 of 724 k. `orchestrate-off` silences it. The contract skill now says heavy check output goes through a reader slice. The disarmed-gate check now reads omp's transcript too.
18
+ - **An ownership lease on every delegated slice.** `Owns:` lines in a brief
19
+ name the globs only that slice writes. They sit on the line or as `- glob`
20
+ items under it. The journal stores them as `owns`. The gate refuses a
21
+ sibling's write into them while the owner runs. A brief may declare its
22
+ worktree on a `Worktree:`, `Checkout` or `Cwd:` line. The gate then refuses
23
+ that slice's write into the main checkout, the shape two wave-1 slices hit
24
+ with relative paths. A shell target resolves against the call's own `cwd`
25
+ and its `cd` or `pushd`. A path the brief only names declares nothing, so an
26
+ integrator that lists worktrees to remove still works in the main checkout.
27
+ The journal keeps that named worktree for reconcile, resume and the board.
28
+ The refusal names the owner, its globs, how to reach it - omp `write
29
+ agent://<name>`, Claude `SendMessage`, else the parent - and the
30
+ `tezgah-task slice done <id>` that frees it. `.tezgah/` and scratch stay
31
+ free. A closed owner frees its files. A reader agent holds no lease. omp
32
+ maps a writing session to its
33
+ record through its ledger's `spawned` row. A Claude background child maps
34
+ through the `agentId` its launch result names, and its SubagentStop closes
35
+ the record. Every other host fails open. `tezgah-task slice board` lists one
36
+ parent's open slices with their lease, worktree, branch and last commit.
37
+ `slice merge-plan` puts a branch touching MANIFEST or the citation baseline
38
+ last. It orders the rest by their forecast `git merge-tree` conflicts and
39
+ leaves CHANGELOG.md to its union driver. It names a branch it cannot
40
+ forecast and exits 1. A spawn whose writing slices name one worktree gets a
41
+ warning. The orchestration text now tells every writing brief to name
42
+ `Owns:` globs and declare its worktree. It gives the shared append points to
43
+ the integrator. Siblings message each other only on an interface or
44
+ ownership change. `orchestrate-off` and `slices-off` disarm it.
45
+
46
+ - **`CHANGELOG.md` merges with the union driver.** A new `.gitattributes` keeps both branches' lines, so parallel slices no longer conflict on the `[Unreleased]` head.
47
+ - **The todo list is the orchestrator's step queue.** The orchestration text, the fan-out rule and the orchestrator agent say to dispatch every ready step at once, mark each dispatched step blocked on its slice and never `wait` while a step is ready. Once every open step waits on a running slice, `wait` and ending the turn are both fine. The per-turn checks still run in the turn, and only the heavy tier joins at merge. Every host's todo call now leaves a `todo` ledger row with its open, ready and blocked counts and the ready heads: omp `todo` and `wait` are watched, Claude and dsh `TodoWrite` joined the PostToolUse matcher, opencode records `todowrite`, and Cursor `todo_write` and Codex `update_plan` arrive with no matcher. No pre-tool rule reads a todo call, so only omp's `wait` reaches the gate. A `ready steps` Stop class refuses, once per turn, a turn that did work and ends while the session's newest todo list holds a ready step the reply neither did nor named beside KARAR, YETKİ, GERİ ALINAMAZ or ORTAM. A step an open slice of the session works on is not ready, and a subagent's own list never counts. An omp `wait` right after a spawn, with ready steps open, gets a warning with its result, never a block.
48
+ - **`tezgah-task tick` and `tezgah-task note`.** `tick NNN <n>` ticks Acceptance item n, and `--undo` unticks it. `note NNN state|next "<text>"` replaces the newest State body or the Next body. Both work on any open plan and commit nothing. The gate refuses a direct write to the active plan, so an agent used to report a box it could not tick as a blocker. The refusal now names both commands.
49
+ - **The subagent model chain follows provider health.** A family whose provider answered a limit leaves omp's chain until its reset. The reset is read from a `429 ... reset at` or a `retry-after-ms`. An OpenRouter key with no limit left is down too. `tezgah-route --health` probes the other families, records the downs and re-applies the chain. Session start and each prompt start it in the background when it is due. `tezgah-route --fallback vendor|any|none` sets which families a chain may fall back to. Under `any`, zai comes before OpenRouter for every agent. Before, any credential counted as funded, so subagents fell back onto a key past its limit.
50
+ - **A `residual work` Stop class.** A turn that did work can no longer end on a list of what is left, such as "Açık kalanlar", "Doğrulanmadı:", "Sıradaki adım", "TODO" or "Next steps". The Stop rule refuses it once with "Bu maddeleri şimdi yap veya engel türü ve kanıtla yaz." An item stays when it names KARAR or GERİ ALINAMAZ in capitals. It also stays when it names YETKİ or ORTAM in capitals and quotes a command that failed in the turn; the type or the quote alone does not keep it. A label that closes itself ("Düzeltilen eksik", "Doğrulanmayan kalmadı"), a risk, and "kalan" as an adjective ("Kalan riskler", "Kalan süre") open no list. A turn with no work, a subagent's end and a nested session stay unjudged. Such a refusal counts in `blocked_claims` but not as a false completion. Before the class, 503 of 1876 final replies carried such a label, 467 of them in turns with work.
51
+ - **Trap cases for doable work listed as open.** The EVALS of tezgah-contract, i-have-adhd and ponytail each gained one.
52
+ - **Held-out, repeat and noise-band warnings (rules 6).** A measured line may name `evaluation.held_out` and log each look at it in `held_out_log.jsonl`. It may also declare `evaluation.noise_band`. `check` warns when a decision picks and reports a variant on the same cells. It warns when one candidate looks at the held-out set twice. It warns when an armed experiment runs once per arm. It warns when `capability_tolerance` has no measured band, or a wider one. None of these refuses, `--strict` included.
53
+ - **`tezgah-gate replay --split search|holdout`.** A stable hash of the session id picks the side. Tune a gate threshold on one side and report it on the other. The report names its side.
54
+ - **A `lesson_changed` ledger row.** Rewriting or removing a `.tezgah/lessons.md` line now leaves one row per line. The row holds the old line's key and its redacted text. Taint does not matter, and it never refuses.
55
+ - **`tezgah-doctor --repair-evidence`.** It finds sessions whose legacy ledger file went into the store more than once and counts the extra rows. Each import offset row of a file is one import, so a file imported three times keeps one copy, not two. A row written to the database directly beside the copies stays. `--apply` backs the database up first and deletes the copies in one transaction. It counts the rows against the plan before the commit and rolls back, exiting 1, if one is missing or left. It leaves a session whole for manual review when the session holds a line fewer times than its imports put it in. It names a file that is gone and skips it.
56
+ - **A slice journal for delegated work.** Each spawned subagent slice gets one row in `.tezgah/slices/slices.db`, a SQLite file opened like the taste store. The first open imports an older one-file-per-record journal once and deletes it. A crash, an omp `/clear`, a quota or 429 stop, or a model or host switch no longer loses it. The hooks open a record on every spawn call of omp, Claude, dsh, Cursor, Codex and opencode, and close it with a failure class. Session start lists the pending slices. `tezgah-task slice reconcile --prune` lists them with a resume brief and removes landed worktrees and branches. `slice resume` prints the native resume path or a re-dispatch brief that starts from the last commit. `slice stats` counts failures per class and host and names the lever for a repeating one. `slices-off` turns the journal off.
57
+ - **A SWE-Bench Pro split for arm-bench.** `benchmarks/arm-bench/swe_pro.py` samples public Pro instances and grades them with the official test script. Its `validate` checks that the gold patch passes and the base fails. The committed 3-instance sample validates 3 of 3.
58
+ - **A cross-project write notice.** A write into another repository than the session's cwd gets one line, once per session and target repository. The line says that repository's plans, lessons and slice journal do not apply to this session. It never refuses. A worktree counts as its main checkout. opencode does not show it yet.
59
+
60
+ ### Fixed
61
+
62
+ - **A subagent's limit stop moves the routing at once.** A slice that stops on a quota or rate limit now holds its provider family down in the health record and re-applies omp's chain. Before, the next session start's probe moved it. A quota is held until its reset or six hours, a rate limit until its retry-after or ten minutes. The mode's own family still leads every chain. The record is re-read under a lock before each write. A YETKİ or ORTAM blocker that quotes a model provider's quota or rate limit is refused while another family the fallback setting allows is up. The refusal names that family. A GitHub or npm rate limit stays a blocker. The task gate's scope refusal prints the ready `tezgah-task allow` line with the current globs and the new path, and none for a path outside the repository. `RELEASING.md` ends with a local `tezgah-setup --refresh` and version read.
63
+ - **An English install no longer asks for Turkish tokens.** The contract and the Stop refusals name the blocker heading, the admission word and the four blocker tokens in the configured language: `Blockers`, `unverified` and DECISION/ACCESS/IRREVERSIBLE/ENV under `en`. Under `tr` they stay "Engeller", `doğrulanmadı` and KARAR/YETKİ/GERİ ALINAMAZ/ORTAM. The Turkish first lines of the residual, ready-step and stopped-slice refusals now come only with `tr`. The reviewer and orchestrator agents and `tezgah-task`'s review lines take the token from the same pack. The contract, plan-sync and i-have-adhd skills name both spellings. opencode's todo reader takes its tokens from the packs. The Stop rule accepts both spellings.
64
+ - **Spanish, French and Dutch prose no longer passes as Turkish.** The reply-language markers dropped de, da, en, ne, ya, her and mi. Those replies now score 0. No Turkish reply in the 4,003 judged corpus texts changed sides.
65
+ - **Another session no longer takes over a stopped slice.** The journal keyed a slice record by the agent name alone. A second session that spawned an unrelated `Fixer` re-opened the first session's stopped `Fixer`, which then left that session's resume line and its Stop rule. A same-named spawn under another brief now gets its own record, `Fixer-<sha1[:6]>`. The same brief from another session still re-opens the record. `slice done|stop|resume` takes an exact id from any session, so a crashed session's slice stays reachable. A bare spawned name answers only for this session under `TEZGAH_SESSION`. The review guard counts this session's records by id, agent or name, and its refusal prints the exact id. A slice whose brief works in another repository names it as `repo`, and its line says `(in <name>)`.
66
+ - **The race rule knows a session's own subagent and its fork.** A subagent stopped on a quota error. The rule then refused its parent's takeover for ten minutes. A subagent whose slice record closed after its last write of the file is now no race. A live one still is. The refusal now names how each writer relates to this session, such as your subagent or an omp fork. It used to print a bare ledger id. The omp hook records a fork's parent at its start.
67
+ - **The transcript miners read only the current repository.** `tezgah-taste mine` and `tests/replay_context.py` read every project under `~/Projects` by default. Another project's prompts and paths then landed in this repository's research data. Both now read the current repository. `--all-projects` reads every project. `mine` writes a pseudonym (`project-<sha1[:8]>`) for each mined repository, in its `repo` and `cwd` and in every prompt, in any case.
68
+ - **The resume block names this repository's last check.** A session that last ran a check in another project showed that check as this repository's.
69
+ - **The residual-list rule asks every turn it should.** On omp the reply after a Stop refusal sat in a turn of its own. A rewrite with no tool call skipped `residual work`, and a failure quoted from the refused turn did not count. That reply is now judged on the rows of every turn in the re-ask chain. A subagent's end is still never refused, but its `subagent_end` row counts the items no type accounts for. The parent's Stop refuses a reply that types none of its own. Claude and Cursor write that row from their subagent-stop events. omp's bridge now writes it from a task session's `agent_end`, into the ledger of the session that spawned the child, once per child. Codex, opencode and dsh still write none. A turn of known reads, such as `git status`, `tail`, `find`, `jq`, `sed -n`, `gh pr view`, `curl -s`, a read-only `sqlite3` or `ssh host ls`, no longer owes a check. A reader with a writing option or script, such as `find -exec`, `sort -o` or `curl -o`, still does, and so does any other command, a commit or an unknown program included. The labels gained first-person negatives ("Yapamadım", "Test etmedim"), "Yarım kalanlar", "Henüz kontrol edilmeyen", "Unresolved", "Still open" and a residual word alone on a line over a list. "Engeller: yok", an empty "Engeller" heading and a blocker item that reports itself gone ("… yoktu. Artık var.") hold no item. A YETKİ or ORTAM item now needs an error of its type, such as a missing tool or a 403. It also needs a quoted command that failed, or, for an item about a subagent or a provider, a spawn call of the turn that failed at the provider. An assertion, a grep alone that matched nothing, a bare "yok" or a failed edit no longer passes as one. A spent quota now classifies as a `user` failure.
70
+ - **A session lists only its own slices.** Every prompt listed each pending slice of the repository, so another session's stopped slices rode this session's context. The prompt now lists only the slices this session spawned and those opened by hand. The resume block at session start adds one line that counts the other sessions' slices. `tezgah-task slice reconcile` still lists them all. The `.tezgah` repository also ignores the journal now.
71
+ - **A device-id change no longer imports a ledger twice.** The store found a legacy file's offset by name, device and inode. After the volume's device id changed, every file went in again from the start, and most ledgers read every row twice. The store now resumes from the longest import under any device id, so an old id coming back imports nothing again either. It keeps one offset row per earlier import for the repair to count.
72
+ - **The task rule no longer refuses targets that are no project file.** It refused `write xd://<tool>`, `agent://X` and `conflict://2` as paths outside the allowlist. It refused `git diff > $B/x.patch` under the literal name `$B/x.patch`. It refused files in tezgah's own `.tezgah/` workspace. Now a URI, a shell target with an unknown variable and the `.tezgah/` workspace pass. A `file://` URI and a variable set to the checkout stay held. So do the slice journal and a plan file that could make itself active. The gate first expands a variable set earlier on the line. The record rule still refuses the active plan record, and `tezgah-task start`, `review`, `tick` and `note` take only an open plan.
73
+ - **The task allowlist reads linked worktrees.** The gate used to place a file in a linked worktree outside the repo root. It then refused every in-scope write there. The gate now matches such a file against the worktree's own top. A worktree of another repository stays outside.
74
+ - **The task allowlist expands `~`.** It read a `~/...` path as relative to the working directory. It then refused an in-scope file spelled that way. With a `**` allowlist it also let a write to the home directory through as a repo path. It now expands `~` first.
75
+ - **The plan and slow role fallbacks reach omp.** `apply_omp` wrote them under `retry.fallbackChains.fallbackChains.plan`, a key omp never reads. They now go under `plan` and `slow`, and `apply_omp` deletes the misplaced keys.
76
+
77
+ ### Changed
78
+
79
+ - **The Stop rule judges the reply after a block.** The rule used to only record that reply, so a new fault in it went unjudged. Now the rule refuses it once more for a new class, one it has not refused in that turn. An example is a blocker list with no evidence after a `no verify_ok` refusal. A turn takes at most two refusals and never two of one class. The core counts them from the turn's ledger rows for Claude, Codex, Cursor and omp. On omp the block comes back as a new `turn` row, and a refusal row's `reask` key ties that row to its turn. Before, omp left that reply unjudged and unrecorded. A host's own flag decides only when the session has no turn row.
80
+ - **"doğrulanmadı" no longer ends a turn by itself.** The admission used to clear every Stop refusal, so writing it cost less than verifying. It now clears only a class whose repair is a check: check failed, partial failure, stale evidence and no verify_ok. It needs a check that ran and failed after the turn's newest change. It clears no ui_ok only when that failed check is the screen or design check itself, it never clears no external read or evidence tampered, and the tamper check now runs first.
81
+ - **Residuals are work, not a report section.** The Integrity paragraph carries this rule. Its label reads "Integrity: evidence, or a named blocker." It asks for every open item to be done now. An item that cannot be done stays as one of the four blockers, with its evidence, under one "Engeller" heading of at most three lines. The per-turn reminder carries the short form. Ponytail notes only a requested part the session cut. A reply ends on the one step only the reader can take, or on nothing. The decision-quality line no longer asks to name a skipped check: it asks to run every check the change reaches. The session fixes a minor review finding, and fix-later needs a user decision (KARAR).
82
+ - **The session budgets move with the core.** The residual rule added 1473 B to the core and 151 B to the reminder. `session_start` and `post_compact` go from 13000 to 14500 B, and `user_prompt` from 6000 to 6500 B.
83
+ - **Heavy checks run once, at merge.** In a turn the session runs compileall, ruff and the impacted tests of what changed. The full suite, neuter, both docs audits, `render_table.py --acceptance --strict` and the e2e scripts run once at the merge step and in CI. A replay of 522 turns keeps about 0.6 of the in-turn checks with no lost failure. The Stop refusals for stale evidence and a missing check now name `python3 tests/impacted.py --run <paths>`. `tezgah-task` lists the deferred checks as the merge step's one run, and CI runs `tezgah-docs --clarity` beside every citations step.
84
+ - **The session resumes a stopped slice instead of absorbing it.** The orchestration text and the orchestrator agent say so. A slice commits `wip:` after each step. The session resumes or re-dispatches a stopped slice, and `tezgah-task slice reconcile --prune` runs before the turn that merged slices ends.
85
+ - **Judge figures name their provider.** Each judge figure in `docs/judge.md` and `docs/models.md` now names its provider and model. Where the record does not say, the page says so. A figure does not carry over to another default.
86
+
87
+ ## [1.6.0] - 2026-10-10
88
+
89
+ ### Added
90
+
91
+ - **A data, metric or dashboard ask reaches the product rule.** "Bu veriye uygun bir metric dashboard tasarla" armed no rule before. A dashboard, chart or metric-choice ask now arms `product`, judged per clause. A code marker in the same clause, such as a metrics endpoint, keeps it out.
92
+ - **product-analysis chooses metrics.** It names a North Star and its input metrics. Each metric gets one record: goal, signal, definition, window, baseline and more.
93
+ - **Dashboard rules in design-contract.** The new `data-views.md` picks the chart for the question. It also sets the context, the window and the widget states.
94
+ - **feature-audit forward mode.** A feature not built yet gets its five matrices in `spec.md`. The audit after the build compares against it.
95
+ - **analyze-app writes the design measurement.** Nothing produced `measurement.json` before. The skill now gives its shape and the snippet that reads it.
96
+ - **The local skill search reaches design-library's entries.** The search now reads the entries a skill's `INDEX.md` lists, one row each.
97
+ - **The product-flow skill and `tezgah-research next`.** A new product capability now runs in a fixed order, from research to re-measure. Each step leaves a file under `.tezgah/analysis/<topic>/`: a `## <step>` note in `flow.md`, or `spec.md` for the forward spec. `tezgah-research next --topic <t>` prints each step's status from those files, and a re-measure is due once the decision's `observe after:` date has passed. It then names the first missing or due step's skill and command. The skill lists the steps, and `FLOWS` in `hooks/tezgah_product.py` holds their done rules.
98
+
99
+ ### Removed
100
+
101
+ - **The product ledger is cut before release (ADR 025).** Plan 075's measured run did not show the ledger beating the playbook alone. Gone: writing and reading `.tezgah/product/ledger.jsonl`, `tezgah-research record`, `due` and `commit --product`, the ledger part of `check`, `product:` citations and the session-start note on due decisions. An existing `ledger.jsonl` is left in place, and nothing reads it.
102
+
103
+ ### Changed
104
+
105
+ - **The session runs the plan lifecycle itself.** The gate refused a session's
106
+ own `tezgah-task start|phase|allow|stop`. It told the session to ask the user,
107
+ so every plan ended with a command handed back. That refusal is gone. A phase
108
+ or scope refusal now names the move (`tezgah-task phase implementation`,
109
+ `tezgah-task allow`). The CLI still checks Acceptance, the spike, the
110
+ checkpoint and the review on each move. The gate still refuses a write to the
111
+ plan file itself. `tezgah-task allow` now needs `--why`, and `status` prints
112
+ the reason next to the widened scope.
113
+ - **`close` cleans up.** `tezgah-task close` on the active task clears its
114
+ phase instead of refusing. It then removes each linked worktree whose branch
115
+ has landed and which holds nothing else, through Orca when Orca tracks it. A
116
+ squash merge counts as landed. A branch whose commits cancel out does not. It
117
+ prints every worktree it keeps, with the reason. Uncommitted files, unknown
118
+ ignored files and `.tezgah` data keep a worktree. So do an unmerged branch, a
119
+ branch nobody committed on, an agent at work and an Orca that does not answer.
120
+ - **No process questions.** The contract says the orchestrator picks a process
121
+ parameter the user did not set, such as a spend ceiling, and states it.
122
+ `consult` on exit 4/5 records the first member of another model family
123
+ itself instead of asking the user to choose. After two review rounds the
124
+ session fixes what is left and closes with `close NNN done --resolved`. A
125
+ missing credential sends it to a route that needs none first. A silent gate
126
+ has it run `tezgah-setup --report` itself.
127
+
128
+ ### Fixed
129
+
130
+ - **product-analysis no longer says `check` demands three compared solutions.** An analysis answers once. A choice between solutions goes under `decisions/`.
131
+
9
132
  ## [1.5.1] - 2026-10-09
10
133
 
11
134
  ### Fixed
@@ -981,7 +1104,9 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
981
1104
  the copy could not find `hooks/` and died on import. It now links into the
982
1105
  installed tree, and also installs the command as `tezgah`.
983
1106
 
984
- [Unreleased]: https://github.com/r1z4x/tezgah/compare/v1.5.1...HEAD
1107
+ [Unreleased]: https://github.com/r1z4x/tezgah/compare/v1.7.0...HEAD
1108
+ [1.7.0]: https://github.com/r1z4x/tezgah/releases/tag/v1.7.0
1109
+ [1.6.0]: https://github.com/r1z4x/tezgah/releases/tag/v1.6.0
985
1110
  [1.5.1]: https://github.com/r1z4x/tezgah/releases/tag/v1.5.1
986
1111
  [1.5.0]: https://github.com/r1z4x/tezgah/releases/tag/v1.5.0
987
1112
  [1.4.0]: https://github.com/r1z4x/tezgah/releases/tag/v1.4.0
package/MANIFEST CHANGED
@@ -1,3 +1,4 @@
1
+ .gitattributes
1
2
  .github/ISSUE_TEMPLATE/bug_report.yml
2
3
  .github/ISSUE_TEMPLATE/config.yml
3
4
  .github/ISSUE_TEMPLATE/feature_request.yml
@@ -111,6 +112,8 @@ docs/status-line.md
111
112
  docs/testing.md
112
113
  docs/vision.md
113
114
  hooks/hooks.json
115
+ hooks/lang/en.json
116
+ hooks/lang/tr.json
114
117
  hooks/projects-auto-init.py
115
118
  hooks/projects-posttooluse.py
116
119
  hooks/projects-pretooluse.py
@@ -133,6 +136,7 @@ hooks/tezgah_models.py
133
136
  hooks/tezgah_orca.py
134
137
  hooks/tezgah_paths.py
135
138
  hooks/tezgah_policy.py
139
+ hooks/tezgah_product.py
136
140
  hooks/tezgah_rank.py
137
141
  hooks/tezgah_replay.py
138
142
  hooks/tezgah_research.py
@@ -545,6 +549,7 @@ skills/analyze-app/EVALS.md
545
549
  skills/analyze-app/SKILL.md
546
550
  skills/design-contract/EVALS.md
547
551
  skills/design-contract/SKILL.md
552
+ skills/design-contract/data-views.md
548
553
  skills/design-library/EVALS.md
549
554
  skills/design-library/INDEX.md
550
555
  skills/design-library/SKILL.md
@@ -612,6 +617,8 @@ skills/ponytail/EVALS.md
612
617
  skills/ponytail/SKILL.md
613
618
  skills/product-analysis/EVALS.md
614
619
  skills/product-analysis/SKILL.md
620
+ skills/product-flow/EVALS.md
621
+ skills/product-flow/SKILL.md
615
622
  skills/research/EVALS.md
616
623
  skills/research/SKILL.md
617
624
  skills/rl-env/SKILL.md
@@ -715,6 +722,7 @@ tests/test_integrity.py
715
722
  tests/test_interpreter.py
716
723
  tests/test_judge.py
717
724
  tests/test_lang.py
725
+ tests/test_lang_packs.py
718
726
  tests/test_lesson_lineage.py
719
727
  tests/test_lessons.py
720
728
  tests/test_lessons_compile.py
@@ -733,6 +741,8 @@ tests/test_packaging_scripts.py
733
741
  tests/test_paths.py
734
742
  tests/test_paths_worktree.py
735
743
  tests/test_platform.py
744
+ tests/test_product_flow_e2e.py
745
+ tests/test_product_next.py
736
746
  tests/test_providers.py
737
747
  tests/test_rank.py
738
748
  tests/test_replay.py
@@ -746,6 +756,7 @@ tests/test_shapes.py
746
756
  tests/test_skill_pick.py
747
757
  tests/test_skill_search.py
748
758
  tests/test_skills.py
759
+ tests/test_slices.py
749
760
  tests/test_snapshot.py
750
761
  tests/test_stack.py
751
762
  tests/test_statusline.py
package/README.md CHANGED
@@ -120,9 +120,12 @@ it set out to arm is not armed.
120
120
  turns it off.
121
121
  - **orx.** The install fetches the OpenResearch CLI that research tasks route
122
122
  through; `TEZGAH_NO_DEPS=1` skips it.
123
- - **Reply language.** Replies are Turkish by default and the Stop rule holds
124
- them to it. `--reply-lang en` asks for English, and `any` for your own
125
- language; neither is checked: `curl -fsSL … | sh -s -- --reply-lang en`.
123
+ - **Reply language.** A first install asks for replies in your locale's
124
+ language (`LANG` and friends) when tezgah ships a pack for it (`tr`, `en`),
125
+ else English; an existing install keeps its language. `--language en` asks
126
+ for English and `any` for your own language: `curl -fsSL … | sh -s --
127
+ --reply-lang en` (the installer's older spelling of `--language`). Only
128
+ Turkish replies are checked by the Stop rule.
126
129
 
127
130
  Rather let your coding assistant do it? Paste this into omp, Claude Code,
128
131
  Codex, Cursor or opencode:
package/bin/consult CHANGED
@@ -56,7 +56,8 @@ suffix); 2 no key for the chosen provider, or no member available at all; 3 the
56
56
  --provider/--models run got no answer; 4 no recorded choice - one "offer:" line
57
57
  per available member, nothing was asked; 5 every recorded member (and the
58
58
  fallback) failed - one "reoffer:" line per member still available that did not
59
- fail. On 4 or 5, ask the user which to use and record it with --use.
59
+ fail. On 4 or 5, the agent records the first listed member of another model
60
+ family with --use itself and asks again; the user is not asked to choose.
60
61
  """
61
62
  import concurrent.futures
62
63
  import json
@@ -504,11 +505,11 @@ def retry_hint(classes):
504
505
  return "the referee answered outside its five headings; re-run, or judge the panel yourself"
505
506
  if c == "credit":
506
507
  return ("the provider account has no credit (HTTP 402) - the key is fine; "
507
- "top it up, or ask the user for another member and record it with --use")
508
+ "top it up, or record another member with --use")
508
509
  if c == "key":
509
- return "set the provider's key, or ask the user for another member and record it with --use"
510
+ return "set the provider's key, or record another member with --use"
510
511
  if c == "missing":
511
- return "the agent CLI is not installed; ask the user for another member and record it with --use"
512
+ return "the agent CLI is not installed; record another member with --use"
512
513
  if c.startswith("cli-exit"):
513
514
  return ("the agent CLI failed (not logged in, or a model name it does not "
514
515
  "know); run it once by hand, or record another member with --use")
@@ -645,8 +646,9 @@ def main():
645
646
  print("consult: no recorded choice, so nothing was asked.")
646
647
  for opt in found:
647
648
  print(offer_line("offer", opt))
648
- print("next: ask the user which of these to use (two or more make a "
649
- "panel), then run: consult --use <member>[,<member>] and ask again")
649
+ print("next: record the first of these from a model family other than "
650
+ "yours (two make a panel) with: consult --use <member>[,<member>], "
651
+ "then ask again - do not ask the user to choose")
650
652
  sys.stdout.flush()
651
653
  os._exit(4)
652
654
 
@@ -775,14 +777,15 @@ def main():
775
777
  print("retry: %s" % hint)
776
778
  code = 0 if answers else 3
777
779
  if not answers and not explicit:
778
- # The recorded choice is the user's, so consult never swaps it on its
779
- # own: it names what is left and the agent asks again.
780
+ # consult never swaps the recorded choice on its own: it names what is
781
+ # left, and the agent records the next member and asks again.
780
782
  lost = {c[1] for c, _cls in failures} | skip_opts
781
783
  left = [o for o in options() if o not in lost]
782
784
  for opt in left:
783
785
  print(offer_line("reoffer", opt))
784
- print("next: ask the user which of these to use instead, then run: "
785
- "consult --use <member>[,<member>] and ask again" if left else
786
+ print("next: record the first of these from another model family with: "
787
+ "consult --use <member>[,<member>], then ask again - do not ask the "
788
+ "user to choose" if left else
786
789
  "next: nothing else can answer here; tell the user the second "
787
790
  "opinion was skipped (install an agent CLI or add a provider key)")
788
791
  code = 5
package/bin/tezgah-docs CHANGED
@@ -106,7 +106,7 @@ RUN = re.compile(r"((?:`[A-Za-z_][A-Za-z0-9_]*(?:\(\))?`\s*[/,]?\s*){1,5})$")
106
106
  CITE = re.compile(r"`([\w.-]+(?:/[\w.-]+)+|[\w-]+\.\w+):(\d+)(?:-(\d+))?`"
107
107
  r"|`(:(\d+)(?:-(\d+))?)`")
108
108
  # The symbol written after the citation instead of before it, the way a table
109
- # cell reads: `hooks/tezgah_policy.py:680-948` (`CORE`).
109
+ # cell reads: `hooks/tezgah_policy.py:784-1100` (`CORE`).
110
110
  AFTER = re.compile(r"\s*\(`([A-Za-z_][A-Za-z0-9_]*)(?:\(\))?`\)")
111
111
  # Any `path:line`, backticked or not: what CITE does not match of these (a
112
112
  # bare token, a comma list `x.py:12,40`) is counted as unjudged, so a citation
@@ -405,12 +405,14 @@ def ratchet_drift(unjudged, baseline):
405
405
  # audit reads a symbol: the gate's rules are the `_deny(session_id, "<rule>",
406
406
  # ...)` calls in hooks/tezgah_gate.py, and the Stop rule's triggers are the
407
407
  # classes `_stop_block` returns - including the four `_shape_block` and the six
408
- # `_evidence_block` hand back, which is why those two functions are read too.
408
+ # `_evidence_block` hand back, and the one `_residual_block` adds at every exit
409
+ # that would end the turn, which is why those three functions are read too.
409
410
 
410
411
  GATE = os.path.join(HERE, "hooks", "tezgah_gate.py")
411
412
  STOP = os.path.join(HERE, "hooks", "tezgah_integrity.py")
412
413
  LEDGER = os.path.join(HERE, "docs", "gate.md")
413
414
  LEDGER_COLUMNS = ("rule", "incident", "evidence", "pin")
415
+ STOP_FUNCS = ("_stop_block", "_shape_block", "_evidence_block", "_residual_block")
414
416
 
415
417
 
416
418
  def source(path):
@@ -450,13 +452,12 @@ def gate_rules(text=None):
450
452
 
451
453
 
452
454
  def stop_triggers():
453
- """The classes `_stop_block` can return, the shape and evidence halves
454
- included."""
455
+ """The classes `_stop_block` can return, the shape, evidence and residual
456
+ halves included."""
455
457
  tree = ast.parse(source(STOP))
456
458
  out = set()
457
459
  for node in ast.walk(tree):
458
- if isinstance(node, ast.FunctionDef) and node.name in (
459
- "_stop_block", "_shape_block", "_evidence_block"):
460
+ if isinstance(node, ast.FunctionDef) and node.name in STOP_FUNCS:
460
461
  out |= returned_classes(node)
461
462
  return out
462
463
 
@@ -535,12 +536,13 @@ CONTRACT_SKILL = os.path.join(HERE, "skills", "tezgah-contract", "SKILL.md")
535
536
  SETUP = os.path.join(HERE, "bin", "tezgah-setup")
536
537
  NUMBERS = ("zero", "one", "two", "three", "four", "five", "six", "seven",
537
538
  "eight", "nine", "ten")
538
- # ADR 007 (2026-10-05) leaves these unclassified: two `off()` names the CORE
539
- # kill-switch paragraph does not list and two opt-in markers read with
540
- # `armed()`. Until the owner decides whether they are rule switches the check
541
- # pins CORE against the code and carries these as the one explicit exception.
539
+ # ADR 007 (2026-10-05) leaves these unclassified: three `off()` names the CORE
540
+ # kill-switch paragraph does not list (`slices-off` joined on 2026-10-11 with
541
+ # the slice journal) and two opt-in markers read with `armed()`. Until the owner
542
+ # decides whether they are rule switches the check pins CORE against the code
543
+ # and carries these as the one explicit exception.
542
544
  UNCLASSIFIED_SWITCHES = frozenset((
543
- "agents-off", "update-check-off", "taste-on", "skill-suggest-on"))
545
+ "agents-off", "update-check-off", "slices-off", "taste-on", "skill-suggest-on"))
544
546
 
545
547
 
546
548
  def rule_sites(text=None):
@@ -835,8 +837,8 @@ def _guarded_returns(func, funcs, pick):
835
837
  names, with what its enclosing `if` tests guard it by. A conjunct of a test
836
838
  that reads a switch (`_switch_words`, or a call to a function in `funcs`
837
839
  that reads one: `_adhd_armed`) is a switch guard; a conjunct that names no
838
- argument or local of `func` is a condition on the environment (`reply_lang()
839
- == "tr"`); a conjunct reading the call's own data is the trigger itself and
840
+ argument or local of `func` is a condition on the environment
841
+ (`_markers(language())`); a conjunct reading the call's own data is the trigger itself and
840
842
  is not a guard. The switch a whole hook checks first (`pretooluse-off`, the
841
843
  Stop hook's `verify-off`) is outside these functions and is not counted."""
842
844
  own = {a.arg for a in func.args.args + func.args.kwonlyargs} | {
@@ -874,11 +876,11 @@ def _guarded_returns(func, funcs, pick):
874
876
 
875
877
 
876
878
  def stop_sites():
877
- """[(class, switches, conditions)] for every class `_shape_block`,
878
- `_stop_block` and `_evidence_block` return (`_guarded_returns`)."""
879
+ """[(class, switches, conditions)] for every class the `STOP_FUNCS`
880
+ return (`_guarded_returns`)."""
879
881
  tree = ast.parse(source(STOP), filename=STOP)
880
882
  funcs = {n.name: n for n in tree.body if isinstance(n, ast.FunctionDef)}
881
- return [site for name in ("_shape_block", "_stop_block", "_evidence_block")
883
+ return [site for name in STOP_FUNCS
882
884
  for site in _guarded_returns(funcs[name], funcs,
883
885
  lambda r: sorted(returned_classes(r)))]
884
886
 
package/bin/tezgah-doctor CHANGED
@@ -42,8 +42,13 @@ this tree.
42
42
  tezgah-doctor --prune-sessions 30 delete sessions idle > 30 days, then vacuum
43
43
  tezgah-doctor --import-evidence import the legacy JSONL evidence ledgers
44
44
  into the evidence database now
45
+ tezgah-doctor --repair-evidence [--apply]
46
+ the rows a legacy ledger imported again
47
+ under another device id holds; --apply
48
+ backs the database up and deletes them
45
49
  """
46
50
  import argparse
51
+ import collections
47
52
  import importlib.machinery
48
53
  import importlib.util
49
54
  import json
@@ -343,6 +348,143 @@ def import_evidence():
343
348
  for base in dict.fromkeys((CACHE, fallback_cache()))}
344
349
 
345
350
 
351
+ def evidence_plan(conn, cache):
352
+ """The copies every legacy ledger the remount bug imported more than once
353
+ put in the cache database `conn`: ({session: [row n to delete]},
354
+ {(name, ino): (current dev, longest import's bytes)} to collapse to,
355
+ {(session, ino): why that file is left alone}, {session: {line text: rows it
356
+ holds after the repair}}).
357
+
358
+ Every `imported` row of one (name, ino) is one import stream: a store from
359
+ before the fix started over at offset 0 under a renumbered device id, and
360
+ the store now swaps only the row it resumed from and keeps a file cut
361
+ below what went in as one stream (`tezgah_store._record`).
362
+ A stream put the file's first `bytes` in, so a line text went in once per
363
+ occurrence in each stream's prefix. The longest stream's count stays; every
364
+ other stream's count is a copy, and that many of the newest rows go. A row
365
+ written to the database directly beside them stays. A session holding a
366
+ line fewer times than its streams put it in is not what they explain - a
367
+ lost row, or a row that only continued another import, as the unreleased
368
+ plan-078 store wrote - so it is left whole for review; only a direct row
369
+ equal to every line such a row covers would hide it.
370
+
371
+ Only the file still at its name with that inode, and at least as long as
372
+ its longest stream, is read: another file there is listed and skipped,
373
+ because counting its lines would delete the wrong rows."""
374
+ import tezgah_store
375
+ streams = collections.defaultdict(list)
376
+ for name, ino, size in conn.execute(
377
+ "SELECT name, ino, bytes FROM imported WHERE (name, ino) IN "
378
+ "(SELECT name, ino FROM imported GROUP BY name, ino HAVING count(*) > 1) "
379
+ "ORDER BY name"):
380
+ streams[(name, ino)].append(size)
381
+ doomed, collapse, review, bounds = {}, {}, {}, {}
382
+ for (name, ino), sizes in streams.items():
383
+ path = os.path.join(cache, "evidence", name)
384
+ session = tezgah_store._ledger(path)[1]
385
+ try:
386
+ st = os.stat(path)
387
+ with open(path, "rb") as fh:
388
+ raw = fh.read(max(sizes))
389
+ except OSError:
390
+ st = None
391
+ if st is None or st.st_ino != ino or st.st_size < max(sizes):
392
+ review[(session, ino)] = ("skipped: its file is gone, replaced or shorter "
393
+ "than its imports")
394
+ continue
395
+ counts = []
396
+ for size in sizes:
397
+ prefix = raw[:size]
398
+ lines = prefix[:prefix.rfind(b"\n") + 1].split(b"\n")
399
+ counts.append(collections.Counter(filter(None, map(tezgah_store._line_text, lines))))
400
+ longest = counts[sizes.index(max(sizes))]
401
+ put = sum(counts, collections.Counter())
402
+ rows = conn.execute("SELECT n, row FROM evidence WHERE session = ? ORDER BY n DESC",
403
+ (session,)).fetchall()
404
+ copies = collections.defaultdict(list)
405
+ for n, text in rows:
406
+ copies[text].append(n)
407
+ short = sum(1 for text, many in put.items() if len(copies[text]) < many)
408
+ if short:
409
+ review[(session, ino)] = ("manual review: %d line(s) held fewer times than its "
410
+ "imports put them in" % short)
411
+ continue
412
+ extra = put - longest
413
+ doomed[session] = [n for text, many in extra.items() for n in copies[text][:many]]
414
+ bounds[session] = {text: len(copies[text]) - extra[text] for text in longest}
415
+ collapse[(name, ino)] = (st.st_dev, max(sizes))
416
+ return doomed, collapse, review, bounds
417
+
418
+
419
+ def print_plan(plan):
420
+ doomed, _collapse, review, _bounds = plan
421
+ for session, ns in doomed.items():
422
+ if ns:
423
+ print(" %s %d row(s)" % (session, len(ns)))
424
+ for (session, _ino), why in review.items():
425
+ print(" %s %s" % (session, why))
426
+ total = sum(map(len, doomed.values()))
427
+ print(" total %d row(s) in %d session(s)" % (total, sum(1 for ns in doomed.values() if ns)))
428
+ return total
429
+
430
+
431
+ def repair_evidence(apply):
432
+ """`--repair-evidence` over the cache database and its sandbox fallback's:
433
+ the dry run prints `evidence_plan`; with `apply` the database is backed up
434
+ through the SQLite backup API first, the rows go and `imported` collapses to
435
+ one row per file in one transaction, whose rows are counted against the
436
+ plan before it commits: a count off the plan rolls it back. The exit code."""
437
+ import tezgah_store
438
+ code = 0
439
+ for cache in dict.fromkeys((CACHE, fallback_cache())):
440
+ db = os.path.join(cache, tezgah_store.CACHE_DB)
441
+ if not os.path.exists(db):
442
+ continue
443
+ print("%s: evidence repair, %s" % (db, "apply" if apply else "dry run"))
444
+ conn = sqlite3.connect(db, isolation_level=None)
445
+ try:
446
+ conn.execute("PRAGMA busy_timeout = %d" % tezgah_store.BUSY_MS)
447
+ if not apply:
448
+ print_plan(evidence_plan(conn, cache))
449
+ continue
450
+ backup = "%s.bak-%s" % (db, time.strftime("%Y%m%d-%H%M%S"))
451
+ os.close(os.open(backup, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600))
452
+ dst = sqlite3.connect(backup)
453
+ try:
454
+ conn.backup(dst)
455
+ finally:
456
+ dst.close()
457
+ print(" backup %s" % backup)
458
+ with tezgah_store.transaction(conn):
459
+ doomed, collapse, _review, bounds = plan = evidence_plan(conn, cache)
460
+ print_plan(plan)
461
+ conn.executemany("DELETE FROM evidence WHERE n = ?",
462
+ [(n,) for ns in doomed.values() for n in ns])
463
+ for (name, ino), (dev, size) in collapse.items():
464
+ conn.execute("DELETE FROM imported WHERE name = ? AND ino = ?", (name, ino))
465
+ conn.execute("INSERT INTO imported (name, dev, ino, bytes) "
466
+ "VALUES (?, ?, ?, ?)", (name, dev, ino, size))
467
+ # the rows against the plan before the commit: a row the plan
468
+ # kept that is gone (missing), or a copy it deleted still there
469
+ # (left), means the deletion went wrong, and it rolls back
470
+ missing = left = 0
471
+ for session, want in bounds.items():
472
+ held = collections.Counter(text for (text,) in conn.execute(
473
+ "SELECT row FROM evidence WHERE session = ?", (session,)))
474
+ for text, many in want.items():
475
+ missing += max(0, many - held[text])
476
+ left += max(0, held[text] - many)
477
+ print(" re-check: %d row(s) missing, %d row(s) left" % (missing, left))
478
+ if missing or left:
479
+ raise sqlite3.DatabaseError("the re-check is off the plan; rolled back")
480
+ except (OSError, sqlite3.Error) as exc:
481
+ print(" failed: %s" % exc, file=sys.stderr)
482
+ code = 1
483
+ finally:
484
+ conn.close()
485
+ return code
486
+
487
+
346
488
 
347
489
  def load_probe():
348
490
  """The bench spike, imported by path so its two coverage rules stay the only
@@ -465,14 +607,24 @@ def main():
465
607
  help="import the legacy JSONL evidence ledgers into the "
466
608
  "evidence database now, instead of at the next "
467
609
  "session start's daily run")
610
+ ap.add_argument("--repair-evidence", action="store_true",
611
+ help="count the evidence rows a legacy ledger imported "
612
+ "again under another device id (dry run)")
613
+ ap.add_argument("--apply", action="store_true",
614
+ help="with --repair-evidence: back the database up, then "
615
+ "delete those rows")
468
616
  args = ap.parse_args()
469
617
  if args.retention_days < 1:
470
618
  ap.error("--retention-days wants a positive day count")
619
+ if args.apply and not args.repair_evidence:
620
+ ap.error("--apply goes with --repair-evidence")
471
621
 
472
622
  if args.coverage:
473
623
  repo = os.path.realpath(os.path.expanduser(args.repo[0]) if args.repo
474
624
  else os.getcwd())
475
625
  return coverage_report(repo, args.dump)
626
+ if args.repair_evidence:
627
+ return repair_evidence(args.apply)
476
628
  if args.import_evidence:
477
629
  import tezgah_store
478
630
  try: