@techierathore/techieflow 1.0.3 → 1.1.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 (150) hide show
  1. package/.claude/commands/TechieFlow/agents/analyst.md +5 -8
  2. package/.claude/commands/TechieFlow/agents/architect.md +1 -10
  3. package/.claude/commands/TechieFlow/agents/flow-master.md +1 -0
  4. package/.claude/commands/TechieFlow/agents/verifier.md +1 -0
  5. package/.claude/commands/TechieFlow/tasks/_owner-language.md +27 -0
  6. package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +1 -1
  7. package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +4 -3
  8. package/.claude/commands/TechieFlow/tasks/amend-docs.md +6 -5
  9. package/.claude/commands/TechieFlow/tasks/build-phase.md +1 -1
  10. package/.claude/commands/TechieFlow/tasks/create-deep-research-prompt.md +4 -0
  11. package/.claude/commands/TechieFlow/tasks/create-doc.md +4 -0
  12. package/.claude/commands/TechieFlow/tasks/create-project-brief.md +22 -0
  13. package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +3 -3
  14. package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +3 -3
  15. package/.claude/commands/TechieFlow/tasks/facilitate-brainstorming-session.md +4 -0
  16. package/.claude/commands/TechieFlow/tasks/fix-issues.md +2 -2
  17. package/.claude/commands/TechieFlow/tasks/generate-html.md +7 -3
  18. package/.claude/commands/TechieFlow/tasks/handoff-phase.md +20 -18
  19. package/.claude/commands/TechieFlow/tasks/metrics-report.md +2 -0
  20. package/.claude/commands/TechieFlow/tasks/mockups.md +1 -1
  21. package/.claude/commands/TechieFlow/tasks/productguide.md +2 -2
  22. package/.claude/commands/TechieFlow/tasks/render-workflow-docs.md +1 -1
  23. package/.claude/commands/TechieFlow/tasks/triage-issues.md +2 -2
  24. package/.claude/commands/TechieFlow/tasks/verify-phase.md +1 -1
  25. package/.opencode/plugin/techieflow.js +181 -9
  26. package/.tfcore/agents/analyst.md +5 -8
  27. package/.tfcore/agents/architect.md +1 -10
  28. package/.tfcore/agents/flow-master.md +1 -0
  29. package/.tfcore/agents/verifier.md +1 -0
  30. package/.tfcore/hooks/block-git.sh +4 -2
  31. package/.tfcore/hooks/guard-build.sh +5 -1
  32. package/.tfcore/hooks/guard-db.sh +6 -4
  33. package/.tfcore/hooks/guard-metrics.sh +13 -1
  34. package/.tfcore/hooks/guard-status-html.sh +86 -2
  35. package/.tfcore/hooks/guard-verify-deps.sh +124 -0
  36. package/.tfcore/hooks/guard-verify.sh +8 -3
  37. package/.tfcore/hooks/sweep-artifacts.sh +46 -0
  38. package/.tfcore/routing.yaml +61 -0
  39. package/.tfcore/standards/owner-words.txt +23 -0
  40. package/.tfcore/tasks/_owner-language.md +27 -0
  41. package/.tfcore/tasks/_smoke-test-policy.md +1 -1
  42. package/.tfcore/tasks/_status-update-gate.md +4 -3
  43. package/.tfcore/tasks/amend-docs.md +6 -5
  44. package/.tfcore/tasks/build-phase.md +1 -1
  45. package/.tfcore/tasks/create-deep-research-prompt.md +4 -0
  46. package/.tfcore/tasks/create-doc.md +4 -0
  47. package/.tfcore/tasks/create-project-brief.md +22 -0
  48. package/.tfcore/tasks/day1-brownfield.md +3 -3
  49. package/.tfcore/tasks/day1-greenfield.md +3 -3
  50. package/.tfcore/tasks/facilitate-brainstorming-session.md +4 -0
  51. package/.tfcore/tasks/fix-issues.md +2 -2
  52. package/.tfcore/tasks/generate-html.md +7 -3
  53. package/.tfcore/tasks/handoff-phase.md +20 -18
  54. package/.tfcore/tasks/metrics-report.md +2 -0
  55. package/.tfcore/tasks/mockups.md +1 -1
  56. package/.tfcore/tasks/productguide.md +2 -2
  57. package/.tfcore/tasks/render-workflow-docs.md +1 -1
  58. package/.tfcore/tasks/triage-issues.md +2 -2
  59. package/.tfcore/tasks/verify-phase.md +1 -1
  60. package/.tfcore/telemetry/SCHEMA.md +61 -1
  61. package/.tfcore/telemetry/install-metrics.sh +3 -2
  62. package/.tfcore/telemetry/tf-metrics.sh +435 -21
  63. package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +6 -0
  64. package/.tfcore/templates/v4custom/app-brd-tmpl.md +5 -3
  65. package/.tfcore/templates/v4custom/app-brief-tmpl.md +65 -0
  66. package/.tfcore/templates/v4custom/app-decision-request-tmpl.md +85 -0
  67. package/.tfcore/templates/v4custom/app-library-feedback-tmpl.md +86 -31
  68. package/.tfcore/templates/v4custom/app-phase-brd-tmpl.md +94 -0
  69. package/.tfcore/templates/v4custom/app-phase-uidesign-tmpl.md +60 -0
  70. package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +4 -2
  71. package/.tfcore/templates/v4custom/build-subagent-prompt.md +2 -2
  72. package/.tfcore/templates/v4custom/html-render-shell.md +4 -2
  73. package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
  74. package/.tfcore/utils/__pycache__/tf-checklist-edit.cpython-312.pyc +0 -0
  75. package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
  76. package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
  77. package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
  78. package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
  79. package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
  80. package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
  81. package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
  82. package/.tfcore/utils/__pycache__/tf-perf-grade.cpython-312.pyc +0 -0
  83. package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
  84. package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
  85. package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
  86. package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
  87. package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
  88. package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
  89. package/.tfcore/utils/tf-assets-browser.mjs +96 -0
  90. package/.tfcore/utils/tf-assets.sh +97 -12
  91. package/.tfcore/utils/tf-brd-status.py +14 -5
  92. package/.tfcore/utils/tf-build-list.py +99 -11
  93. package/.tfcore/utils/tf-build-list.sh +5 -3
  94. package/.tfcore/utils/tf-build.sh +88 -15
  95. package/.tfcore/utils/tf-ci-repro.py +685 -0
  96. package/.tfcore/utils/tf-ci-repro.sh +24 -0
  97. package/.tfcore/utils/tf-devguide-list.py +47 -22
  98. package/.tfcore/utils/tf-doc-check.py +242 -10
  99. package/.tfcore/utils/tf-doc-tests.sh +79 -0
  100. package/.tfcore/utils/tf-emit.sh +413 -19
  101. package/.tfcore/utils/tf-feedback.sh +13 -0
  102. package/.tfcore/utils/tf-fix-close.sh +113 -31
  103. package/.tfcore/utils/tf-gitignore-audit.sh +50 -3
  104. package/.tfcore/utils/tf-goal.sh +105 -18
  105. package/.tfcore/utils/tf-harness.sh +9 -4
  106. package/.tfcore/utils/tf-lock.sh +29 -0
  107. package/.tfcore/utils/tf-log-miss.py +34 -8
  108. package/.tfcore/utils/tf-login.mjs +167 -0
  109. package/.tfcore/utils/tf-misses-md.py +24 -21
  110. package/.tfcore/utils/tf-misses-md.sh +5 -4
  111. package/.tfcore/utils/tf-mockup-parity.mjs +427 -63
  112. package/.tfcore/utils/tf-mockup-parity.sh +14 -2
  113. package/.tfcore/utils/tf-model-pick.sh +475 -0
  114. package/.tfcore/utils/tf-owner-text.py +233 -0
  115. package/.tfcore/utils/tf-owner-text.sh +14 -0
  116. package/.tfcore/utils/tf-phase.sh +33 -1
  117. package/.tfcore/utils/tf-portable.sh +230 -0
  118. package/.tfcore/utils/tf-render-html.py +80 -2
  119. package/.tfcore/utils/tf-routing-bind.sh +87 -1
  120. package/.tfcore/utils/tf-routing.sh +7 -0
  121. package/.tfcore/utils/tf-selfcheck.sh +356 -0
  122. package/.tfcore/utils/tf-split-brd.py +17 -2
  123. package/.tfcore/utils/tf-status-evidence.sh +11 -9
  124. package/.tfcore/utils/tf-status-facts.py +133 -44
  125. package/.tfcore/utils/tf-triage.py +62 -16
  126. package/.tfcore/utils/tf-triage.sh +2 -0
  127. package/.tfcore/utils/tf-verify-boot.sh +263 -43
  128. package/.tfcore/utils/tf-verify-emit.sh +20 -0
  129. package/.tfcore/utils/tf-verify-env.sh +33 -4
  130. package/.tfcore/utils/tf-verify-list.py +75 -8
  131. package/.tfcore/utils/tf-verify-screens.mjs +254 -43
  132. package/.tfcore/utils/tf-verify-screens.sh +18 -4
  133. package/.tfcore/utils/tf-verify-tests.sh +272 -28
  134. package/.tfcore/utils/tf-verify-verdict.py +59 -11
  135. package/.tfcore/utils/tf_defect.py +27 -0
  136. package/.tfcore/utils/tf_feedback.py +209 -0
  137. package/.tfcore/utils/tf_roadmap.py +35 -0
  138. package/README.md +19 -5
  139. package/docs/TechieFlow-Installation.md +5 -3
  140. package/opencode.jsonc +24 -0
  141. package/package.json +1 -1
  142. package/scripts/install.mjs +4 -0
  143. package/.tfcore/templates/architecture-tmpl.yaml +0 -650
  144. package/.tfcore/templates/brownfield-architecture-tmpl.yaml +0 -476
  145. package/.tfcore/templates/competitor-analysis-tmpl.yaml +0 -306
  146. package/.tfcore/templates/front-end-architecture-tmpl.yaml +0 -218
  147. package/.tfcore/templates/fullstack-architecture-tmpl.yaml +0 -823
  148. package/.tfcore/templates/market-research-tmpl.yaml +0 -252
  149. package/.tfcore/templates/project-brief-tmpl.yaml +0 -221
  150. package/.tfcore/utils/techieflow-doc-template.md +0 -325
@@ -26,7 +26,7 @@ set -uo pipefail
26
26
 
27
27
  command -v python3 >/dev/null 2>&1 || { echo "tf-metrics: python3 is required." >&2; exit 1; }
28
28
 
29
- exec python3 - "$@" <<'PYEOF'
29
+ TF_METRICS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" exec python3 - "$@" <<'PYEOF'
30
30
  import glob
31
31
  import json
32
32
  import os
@@ -53,12 +53,77 @@ LATE_GATES = {"perf": "2026-08-10", "assets": "2026-08-31", "mockup-parity": "20
53
53
  # fill, so counting it as "not assessed" understates the practice being measured.
54
54
  # It leaves that field's denominator and is reported separately. SCHEMA.md §5.5.6 /
55
55
  # §3.5. Keep this table in sync when an optional field is added to any stream.
56
- FIELD_SINCE = {"why_missed": "2026-08-28", "sort": "2026-09-07"}
56
+ FIELD_SINCE = {"why_missed": "2026-08-28", "sort": "2026-09-07", "what": "2026-09-07"}
57
+
58
+
59
+ def _span(r):
60
+ """Seconds between a run's own `started` and `ended`, or None if they cannot be read."""
61
+ a, b = r.get("started"), r.get("ended")
62
+ if not isinstance(a, str) or not isinstance(b, str):
63
+ return None
64
+ try:
65
+ fmt = "%Y-%m-%dT%H:%M:%SZ"
66
+ return int((datetime.strptime(b, fmt) - datetime.strptime(a, fmt)).total_seconds())
67
+ except Exception:
68
+ return None
69
+
70
+
71
+ def _dur(r):
72
+ """A run's duration in seconds, or None when the record does not carry a real one.
73
+
74
+ THE TIMESTAMPS WIN. A record stores a start, an end and a duration, and on a stream
75
+ written before the emitter checked them the third can contradict the first two:
76
+ TechieBlog holds 14 such records, one storing -166 and thirteen storing a plausible
77
+ round number (3600, 2700, 1800, 900, 600) that bears no relation to their own clocks
78
+ -- `refresh-status` started 20:05:00, ended 17:59:39, stored 600. Only the one that
79
+ stored the negative was ever detectable by reading the number alone.
80
+
81
+ So the duration is taken from the timestamps whenever they can be read, and a record
82
+ whose timestamps are themselves impossible carries NO duration rather than a
83
+ plausible one. The count of both is published (`duration_excluded_n`,
84
+ `duration_recomputed_n`) and printed, because "32 records, correct" is only honest
85
+ when the reader is also told that 14 were dropped. Every consumer goes through this
86
+ one function, so the two readers of a stream cannot disagree about the same record.
87
+ tests/regression/run.sh tf_015."""
88
+ span = _span(r)
89
+ if span is not None:
90
+ return span if span > 0 else None
91
+ d = r.get("duration_s")
92
+ return d if isinstance(d, (int, float)) and d > 0 else None
93
+
94
+
95
+ def _dur_quality(records):
96
+ """(impossible, absent, recomputed) -- what _dur changed, split so the report can be
97
+ accurate rather than merely alarming.
98
+
99
+ "Its start is after its end" and "it never carried a duration" are different facts and
100
+ were briefly reported as one, which made this repo look like it held 30 corrupt records
101
+ when it holds 30 records that simply record no elapsed time (`started` == `ended`, no
102
+ `duration_s`). Neither reached a figure before or after -- but a reader told the wrong
103
+ reason chases the wrong thing."""
104
+ impossible = absent = recomputed = 0
105
+ for r in records:
106
+ span, stored = _span(r), r.get("duration_s")
107
+ has_stored = isinstance(stored, (int, float)) and stored > 0
108
+ if span is not None and span < 0:
109
+ impossible += 1
110
+ elif span is not None and span == 0:
111
+ absent += 1
112
+ elif span is None and not has_stored:
113
+ absent += 1
114
+ elif span is not None and has_stored and abs(span - stored) > 1:
115
+ recomputed += 1
116
+ return impossible, absent, recomputed
57
117
 
58
118
  # Fields a `miss-amend` record may complete, and their closed vocabularies.
59
119
  # Kept identical to tf-emit.sh's `_AMENDABLE`; the emitter enforces it on write and
60
120
  # this enforces it again on read, because a merged stream can carry records this
61
121
  # machine's emitter never saw. SCHEMA.md §5.5.7 states the rule for extending it.
122
+ # `what` is the miss's own sentence: free text, not a vocabulary. Kept amendable
123
+ # because the never-overwrite rule -- not the vocabulary -- is what stops an amend
124
+ # altering a fact. MISS-TechieFlow-20260909-02; tests/regression/run.sh tf_018.
125
+ AMENDABLE_TEXT_FIELDS = ("what",)
126
+
62
127
  AMENDABLE_FIELDS = {
63
128
  "why_missed": ("missing-checklist-item", "insufficient-verify-method",
64
129
  "code-audit-limitation", "ambiguous-acceptance",
@@ -71,6 +136,72 @@ SORT_WORDS = {"spec": "the app's spec did not say it", "unsaid": "the framework
71
136
  MIN_N = 3 # fewer supporting records than this -> "insufficient data", never a number
72
137
 
73
138
 
139
+ # --------------------------------------------------- list price (report time)
140
+ # WHERE PRICING BELONGS (owner, 2026-09-10). The framework's job is to record what it MEASURED:
141
+ # tokens, per model, and whatever cost the provider itself reported. Turning tokens into money is
142
+ # a reporting job — this file's, and TfLens's — so it happens HERE, at report time, from the
143
+ # fields already on the stream. Nothing priced is ever written to a record. That is SCHEMA.md §8's
144
+ # existing rule ("derived metrics are computed, never stored") applied to money, and it is why a
145
+ # rate card can change, or a reader disagree with this one, without a single record being wrong.
146
+ #
147
+ # The rate itself comes from `tf-model-pick.sh rate`, so the rate card is defined once and the
148
+ # project's own `.tfcore/rate-card.json` wins where it exists.
149
+ _RATE_CACHE = {}
150
+ REPO_OF = {} # id(record) -> the repo it was read from; never written into a record
151
+
152
+
153
+ def rate_for(repo, model):
154
+ key = (repo, model)
155
+ if key not in _RATE_CACHE:
156
+ got = None
157
+ for base in (os.path.join(repo, ".tfcore", "utils"),
158
+ os.path.join(os.environ.get("TF_METRICS_DIR", ""), "..", "utils")):
159
+ script = os.path.join(base, "tf-model-pick.sh")
160
+ if not os.path.isfile(script):
161
+ continue
162
+ try:
163
+ out = subprocess.run(["bash", script, "rate", model],
164
+ capture_output=True, text=True, timeout=20,
165
+ env=dict(os.environ, TF_PROJECT_DIR=repo))
166
+ if out.returncode == 0:
167
+ parts = (out.stdout or "").strip().split("\t")
168
+ if len(parts) >= 4:
169
+ got = tuple(float(x) for x in parts[:4])
170
+ break
171
+ except Exception:
172
+ pass
173
+ _RATE_CACHE[key] = got
174
+ return _RATE_CACHE[key]
175
+
176
+
177
+ def list_price(repo, rec):
178
+ """What one run's measured tokens cost at list price, or None when nothing prices its model.
179
+
180
+ Output tokens are exact per model (`model_tokens_out`). Input and cache tokens are counted for
181
+ the window as a whole, so where a window ran several models they are shared out by each
182
+ model's output share — arithmetic, and the caller is told."""
183
+ split = rec.get("model_tokens_out")
184
+ if not isinstance(split, dict) or not split:
185
+ m = rec.get("model")
186
+ if not m or rec.get("tokens_out") is None:
187
+ return None
188
+ split = {m: rec.get("tokens_out") or 0}
189
+ total_out = sum(v or 0 for v in split.values())
190
+ ti = rec.get("tokens_in") or 0
191
+ tcr = rec.get("tokens_cache_read") or 0
192
+ tcw = rec.get("tokens_cache_write") or 0
193
+ usd, priced = 0.0, 0
194
+ for m, out in split.items():
195
+ r = rate_for(repo, m)
196
+ if not r:
197
+ continue
198
+ share = (float(out or 0) / total_out) if total_out else 0.0
199
+ usd += (ti * share * r[0] + (out or 0) * r[1]
200
+ + tcr * share * r[2] + tcw * share * r[3]) / 1_000_000.0
201
+ priced += 1
202
+ return round(usd, 6) if priced else None
203
+
204
+
74
205
  # ---------------------------------------------------------------- utilities
75
206
  def die(msg):
76
207
  sys.stderr.write("tf-metrics: %s\n" % msg)
@@ -99,6 +230,42 @@ def read_stream(repo, stream):
99
230
  return out
100
231
 
101
232
 
233
+ def apply_voids(runs, app=None):
234
+ """Drop the run records a `run-void` names, and hand back what was dropped.
235
+
236
+ A stream is append-only, so a run record written with a wrong figure can be
237
+ neither edited nor deleted — and nothing inside the record says it is wrong, so
238
+ every figure over it is quietly polluted. The correction is therefore another
239
+ record: `run-void` names a run by the pair every run carries (`cmd` + `started`)
240
+ and says why (SCHEMA.md §2.7, written by `tf-emit.sh --void-run`).
241
+
242
+ Two rules, both the same one this file applies everywhere else. The voided record
243
+ leaves every figure — it is not clamped, guessed at or half-counted. And the count
244
+ of what left travels with the figures, because a total offered without its
245
+ exclusions is just a different wrong number. A void naming no run on this stream
246
+ is an orphan: it is counted and reported, never silently dropped, exactly as an
247
+ orphaned miss-amend is. tests/regression/run.sh tf_void."""
248
+ key = lambda r: (r.get("cmd"), r.get("started"))
249
+ voids = {}
250
+ for v in runs:
251
+ if v.get("kind") == "run-void" and v.get("started"):
252
+ voids[key(v)] = v
253
+ kept, dropped, reasons = [], 0, []
254
+ matched = set()
255
+ for r in runs:
256
+ if r.get("kind") == "run-void":
257
+ continue
258
+ if key(r) in voids:
259
+ matched.add(key(r))
260
+ dropped += 1
261
+ v = voids[key(r)]
262
+ reasons.append("%s %s %s — %s" % (app or r.get("app") or "?", r.get("cmd"),
263
+ r.get("started"), v.get("reason") or "no reason recorded"))
264
+ continue
265
+ kept.append(r)
266
+ return kept, dropped, reasons, len(set(voids) - matched)
267
+
268
+
102
269
  def emit(repo, stream, records, dry_run, quiet=False):
103
270
  """Append via tf-emit.sh — the single append primitive. Never write JSONL directly.
104
271
 
@@ -208,6 +375,11 @@ def analyse_misses(misses):
208
375
 
209
376
  Raw counts and the miss-class distribution ARE poolable: a miss counts as a miss
210
377
  whoever missed it; only its attribution is confidence-bounded."""
378
+ # a miss withdrawn by a miss-void leaves every figure, with its fixes and amends (§5.5.11)
379
+ voids = {m.get("miss_id"): m for m in misses if m.get("kind") == "miss-void"}
380
+ ids = {m.get("miss_id") for m in misses if m.get("kind") == "miss"}
381
+ voided = ["%s — %s" % (k, v.get("reason") or "no reason recorded") for k, v in voids.items() if k in ids]
382
+ misses = [m for m in misses if m.get("miss_id") not in voids or m.get("kind") == "review"]
211
383
  opened = [m for m in misses if m.get("kind") == "miss" and not m.get("backfilled")]
212
384
  fixes = [m for m in misses if m.get("kind") == "miss-fix" and not m.get("backfilled")]
213
385
  amends = [m for m in misses if m.get("kind") == "miss-amend"]
@@ -227,11 +399,14 @@ def analyse_misses(misses):
227
399
  for a in sorted(amends, key=lambda r: r.get("ts") or ""):
228
400
  parent = opened_by_id.get(a.get("miss_id"))
229
401
  fld = a.get("field")
230
- if parent is None or fld not in AMENDABLE_FIELDS:
402
+ if parent is None or (fld not in AMENDABLE_FIELDS and fld not in AMENDABLE_TEXT_FIELDS):
231
403
  orphan_amends += 1
232
404
  continue
233
- if parent.get(fld) is None and a.get("value") in AMENDABLE_FIELDS[fld]:
234
- parent[fld] = a["value"]
405
+ val = a.get("value")
406
+ ok = (isinstance(val, str) and val.strip()) if fld in AMENDABLE_TEXT_FIELDS \
407
+ else (val in AMENDABLE_FIELDS.get(fld, ()))
408
+ if parent.get(fld) is None and ok:
409
+ parent[fld] = val
235
410
  amended += 1
236
411
 
237
412
  # ---- eligibility floor for optional fields added mid-stream (SCHEMA.md §3.5,
@@ -360,7 +535,21 @@ def analyse_misses(misses):
360
535
  # Dollars exist ONLY where a harness measured them. Claude Code carries
361
536
  # cost_usd:null permanently (SCHEMA.md §4) and are never priced from a rate card
362
537
  # here — a pooled sum over mixed harnesses would silently under-report.
363
- paid = [f for f in sole if f.get("cost_usd") is not None]
538
+ #
539
+ # A null was never the only way to have no money figure, though. An OpenCode run on a
540
+ # subscription records cost_usd 0.0 — a real, measured zero, because the plan was already
541
+ # paid for — and averaging that in as a FREE repair is the TF-005 defect again, in the
542
+ # column right beside the one TF-005 was found in. Only a `metered` record is money
543
+ # (SCHEMA.md §2.5b). A record written before billing_mode existed is admitted on its old
544
+ # terms, and counted, so the change does not silently rewrite the past.
545
+ paid = [f for f in sole if f.get("cost_usd") is not None
546
+ and (f.get("billing_mode") in (None, "metered"))]
547
+ unpriced_subscription_n = len([f for f in sole if f.get("cost_usd") is not None
548
+ and f.get("billing_mode") not in (None, "metered")])
549
+ # And the figure that works on every harness: what the repair's tokens cost at list price,
550
+ # worked out here from the tokens the record carries. It is the only money-shaped number a
551
+ # Claude Code repair can have, because a Max subscription bills nothing per token.
552
+ listed = [p for p in (list_price(REPO_OF.get(id(f), ""), f) for f in sole) if p is not None]
364
553
  escaped = [m for m in opened if m.get("found_by") in ("owner", "production")]
365
554
  design = [m for m in opened if m.get("miss_class") == "unspecified-gap"]
366
555
 
@@ -382,6 +571,9 @@ def analyse_misses(misses):
382
571
  "resolved_misses": len(resolved),
383
572
  "amendments_applied": amended,
384
573
  "orphan_amends": orphan_amends,
574
+ "misses_voided_n": len(voided),
575
+ "misses_voided": voided,
576
+ "miss_voids_orphaned_n": len(voids) - len(voided),
385
577
  # Denominator is records that CARRY the field — why_missed is optional (§5.5.6)
386
578
  # and a missing value means "not assessed", never a zero for some category —
387
579
  # AND records that COULD have carried it: one written before 2026-08-28 had no
@@ -462,6 +654,12 @@ def analyse_misses(misses):
462
654
  "cost_usd_per_miss_measured": round(
463
655
  sum(f["cost_usd"] for f in paid) / len(paid), 4) if len(paid) >= MIN_N else None,
464
656
  "cost_usd_records": len(paid),
657
+ # Repairs whose dollars were measured and are NOT money: a subscription's zero, a local
658
+ # model's absent bill. Printed beside the figure, never inside it.
659
+ "cost_usd_excluded_not_money_n": unpriced_subscription_n,
660
+ "cost_list_usd_per_miss": round(
661
+ sum(listed) / len(listed), 4) if len(listed) >= MIN_N else None,
662
+ "cost_list_usd_records": len(listed),
465
663
  }
466
664
 
467
665
 
@@ -512,9 +710,16 @@ def analyse_phases(runs):
512
710
  except Exception:
513
711
  return None
514
712
 
713
+ # A stored duration of zero or less is not a measurement: it is an impossible record
714
+ # (`ended` before `started`) that reached the stream before the emitter refused those.
715
+ # Treat it as absent so it is re-derived here, and -- when the timestamps are just as
716
+ # impossible -- excluded from every figure below rather than admitted by a truthiness
717
+ # test. tests/regression/run.sh tf_015.
515
718
  derived_n = 0
719
+ negative_durations = sum(1 for r in live
720
+ if isinstance(r.get("duration_s"), (int, float)) and r["duration_s"] < 0)
516
721
  for r in live:
517
- if r.get("duration_s") or not r.get("started"):
722
+ if _dur(r) is not None or not r.get("started"):
518
723
  continue
519
724
  end = r.get("ended") or r.get("ts")
520
725
  if not end:
@@ -542,16 +747,28 @@ def analyse_phases(runs):
542
747
  return scope_of(r) == "tree" and r.get("subagent_runs") is not None
543
748
 
544
749
  grand_out = sum(r.get("tokens_out") or 0 for r in live if has_tokens(r))
545
- grand_dur = sum(r.get("duration_s") or 0 for r in live if r.get("duration_s"))
750
+ grand_dur = sum(_dur(r) or 0 for r in live if _dur(r) is not None)
546
751
 
547
752
  phases = OrderedDict()
548
753
  for cmd in sorted({r.get("cmd") or "?" for r in live}):
549
754
  rs = [r for r in live if (r.get("cmd") or "?") == cmd]
550
755
  priced = [r for r in rs if has_tokens(r)]
551
- durs = [r["duration_s"] for r in rs if r.get("duration_s")]
756
+ durs = [_dur(r) for r in rs if _dur(r) is not None]
552
757
  fan = [r for r in rs if sees_subagents(r)]
553
758
 
554
759
  models = defaultdict(lambda: {"runs": 0, "tokens_out": 0})
760
+ # List price, COMPUTED HERE from the tokens the record carries (see list_price above —
761
+ # pricing is a reporting job, and nothing priced is ever stored). Per model, attributed
762
+ # only where the window ran ONE model, the same rule the measured dollars follow below.
763
+ list_of = {}
764
+ for r in priced:
765
+ lp = list_price(REPO_OF.get(id(r), ""), r)
766
+ if lp is None:
767
+ continue
768
+ list_of[id(r)] = lp
769
+ ms = r.get("models") or ([r["model"]] if r.get("model") else [])
770
+ if len(ms) == 1:
771
+ models[ms[0]]["list_usd"] = round(models[ms[0]].get("list_usd", 0.0) + lp, 6)
555
772
  for r in priced:
556
773
  split = r.get("model_tokens_out")
557
774
  if isinstance(split, dict) and split:
@@ -578,12 +795,42 @@ def analyse_phases(runs):
578
795
  sub_out = sum(r.get("tokens_out_subagents") or 0 for r in fan)
579
796
  fan_out_total = sum(r.get("tokens_out") or 0 for r in fan if has_tokens(r))
580
797
 
798
+ # DOLLARS, and what each one means. A harness split was never enough: one OpenCode
799
+ # machine mixes a flat-fee subscription (whose marginal cost is a true zero), a metered
800
+ # API key (whose dollars are real money) and a local model (which has no bill at all),
801
+ # and pooling those three produces a money figure that is neither the money spent nor
802
+ # the effort used. `billing_mode` is the separation; only `metered` is money.
581
803
  cost_by_harness = defaultdict(float)
582
804
  cost_n = Counter()
805
+ cost_by_mode = defaultdict(float)
806
+ mode_n = Counter()
807
+ model_usd = defaultdict(float)
808
+ model_usd_n = Counter()
809
+ plan_by_model = defaultdict(float)
810
+ usd_unattributed, usd_unattributed_n = 0.0, 0
583
811
  for r in priced:
812
+ mode = r.get("billing_mode") or "unrecorded"
813
+ mode_n[mode] += 1
584
814
  if r.get("cost_usd") is not None:
585
815
  cost_by_harness[r.get("harness") or "?"] += r["cost_usd"]
586
816
  cost_n[r.get("harness") or "?"] += 1
817
+ cost_by_mode[mode] += r["cost_usd"]
818
+ if mode == "plan":
819
+ ms = r.get("models") or ([r["model"]] if r.get("model") else [])
820
+ if len(ms) == 1:
821
+ plan_by_model[ms[0]] += r["cost_usd"]
822
+ if mode == "metered":
823
+ # Per-model spend is attributed ONLY where the window ran one model.
824
+ # Splitting one window's dollars across several models by their token
825
+ # share is arithmetic, not measurement — the same line §5.5.3 draws
826
+ # between `sole` and `shared`, one stream over.
827
+ ms = r.get("models") or ([r["model"]] if r.get("model") else [])
828
+ if len(ms) == 1:
829
+ model_usd[ms[0]] += r["cost_usd"]
830
+ model_usd_n[ms[0]] += 1
831
+ else:
832
+ usd_unattributed += r["cost_usd"]
833
+ usd_unattributed_n += 1
587
834
 
588
835
  tok = lambda k: sum(r.get(k) or 0 for r in priced)
589
836
  phases[cmd] = {
@@ -621,7 +868,7 @@ def analyse_phases(runs):
621
868
  "by_mode": OrderedDict(
622
869
  (mode, {
623
870
  "runs": len([r for r in rs if (r.get("mode") or "—") == mode]),
624
- "duration_s": sum(r.get("duration_s") or 0
871
+ "duration_s": sum(_dur(r) or 0
625
872
  for r in rs if (r.get("mode") or "—") == mode),
626
873
  "tokens_out": sum(r.get("tokens_out") or 0
627
874
  for r in rs if (r.get("mode") or "—") == mode and has_tokens(r)),
@@ -666,6 +913,36 @@ def analyse_phases(runs):
666
913
  "cost_usd_by_harness": OrderedDict(
667
914
  (h, {"usd": round(v, 6), "records": cost_n[h]})
668
915
  for h, v in sorted(cost_by_harness.items())),
916
+ # …and per billing mode, which is the separation that decides whether a number
917
+ # is money at all (SCHEMA.md §2.5b).
918
+ "billing_modes": OrderedDict(sorted(mode_n.items())),
919
+ "cost_usd_by_billing_mode": OrderedDict(
920
+ (mode, round(v, 6)) for mode, v in sorted(cost_by_mode.items())),
921
+ "money_usd": round(cost_by_mode.get("metered", 0.0), 6),
922
+ "money_records": mode_n.get("metered", 0),
923
+ # A window that mixed a metered model with a subscription one really was billed, but
924
+ # the bill cannot be separated from the part that was already paid for. Real money,
925
+ # reported on its own line rather than folded into the headline or dropped.
926
+ "money_mixed_usd": round(cost_by_mode.get("mixed", 0.0), 6),
927
+ "money_mixed_records": mode_n.get("mixed", 0),
928
+ "spend_by_model": OrderedDict(
929
+ (m, {"usd": round(v, 6), "records": model_usd_n[m]})
930
+ for m, v in sorted(model_usd.items(), key=lambda kv: (-kv[1], kv[0]))),
931
+ "spend_unattributed": {"usd": round(usd_unattributed, 6), "records": usd_unattributed_n},
932
+ # LIST PRICE — what these tokens would cost bought at the published rate. It is the
933
+ # only figure that compares a subscription phase with a metered one, and it is a
934
+ # price list applied to a measurement, never a bill. Reported apart from `money_usd`
935
+ # for exactly that reason, with the unpriced count beside it.
936
+ "list_usd": round(sum(list_of.values()), 6),
937
+ "list_usd_records": len(list_of),
938
+ "list_usd_unpriced_n": len(priced) - len(list_of),
939
+ # A monthly plan reports dollars too, but they are ALLOWANCE CONSUMED against a
940
+ # per-model limit, not an invoice — and they are the numbers that produce the limit
941
+ # the fallback chain exists for, so they are worth their own line.
942
+ "plan_allowance_usd": round(cost_by_mode.get("plan", 0.0), 6),
943
+ "plan_allowance_records": mode_n.get("plan", 0),
944
+ "plan_allowance_by_model": OrderedDict(
945
+ (m, round(v, 6)) for m, v in sorted(plan_by_model.items(), key=lambda kv: -kv[1])),
669
946
  }
670
947
 
671
948
  return {
@@ -673,6 +950,12 @@ def analyse_phases(runs):
673
950
  "scope_coverage": OrderedDict(sorted(scope_cov.items())),
674
951
  "tokens_out_total": grand_out,
675
952
  "duration_s_total": grand_dur,
953
+ # What the duration figures are actually built on. A total is only honest when the
954
+ # reader is told what it left out: TechieBlog's 46 run records yield 32 usable ones.
955
+ "duration_measured_n": len([r for r in live if _dur(r) is not None]),
956
+ "duration_impossible_n": _dur_quality(live)[0],
957
+ "duration_absent_n": _dur_quality(live)[1],
958
+ "duration_recomputed_n": _dur_quality(live)[2],
676
959
  "phases": phases,
677
960
  "note": ("Per-phase effort over LIVE run records, aggregated by `cmd`. Token "
678
961
  "figures exclude runs whose window could not be computed; fan-out "
@@ -965,10 +1248,15 @@ def analyse(repos):
965
1248
  per_repo = []
966
1249
  commit_dupes = 0
967
1250
  session_dupes = 0
1251
+ voided_runs = 0
1252
+ void_reasons = []
1253
+ void_orphans = 0
968
1254
  for repo in repos:
969
1255
  g = read_stream(repo, "gates")
970
- r = read_stream(repo, "runs")
971
- misses += read_stream(repo, "misses")
1256
+ r, vn, vr, vo = apply_voids(read_stream(repo, "runs"), app_name(repo))
1257
+ voided_runs += vn; void_reasons += vr; void_orphans += vo
1258
+ repo_misses = read_stream(repo, "misses")
1259
+ misses += repo_misses
972
1260
  # Per repo, not across them (SCHEMA.md §4): the OpenCode plugin
973
1261
  # appends a cumulative snapshot at every root-session idle.
974
1262
  s, sd = dedupe_sessions(read_stream(repo, "sessions"))
@@ -976,13 +1264,18 @@ def analyse(repos):
976
1264
  # Per repo, not across them: two repos may legitimately share a short sha.
977
1265
  c, d = dedupe_commits(read_stream(repo, "commits"))
978
1266
  commit_dupes += d
1267
+ # Which repo each record came from, so the report can price it with THAT project's rate
1268
+ # card and routing. Kept beside the records rather than written into them: nothing here
1269
+ # may add a field a record did not have.
1270
+ for _x in r + repo_misses:
1271
+ REPO_OF[id(_x)] = repo
979
1272
  gates += g; runs += r; sessions += s; commits += c
980
1273
  per_repo.append({"repo": repo, "app": app_name(repo),
981
1274
  "project_type": project_type(repo)[0],
982
1275
  "gates": len(g), "gates_backfilled": sum(1 for x in g if x.get("backfilled")),
983
1276
  "runs": len(r), "sessions": len(s), "commits": len(c),
984
- "misses": sum(1 for x in read_stream(repo, "misses")
985
- if x.get("kind") == "miss"),
1277
+ "misses": sum(1 for x in repo_misses if x.get("kind") == "miss" and x.get("miss_id") not in
1278
+ {v.get("miss_id") for v in repo_misses if v.get("kind") == "miss-void"}),
986
1279
  # A repo reclassified after records were written (most often a
987
1280
  # greenfield project born `docs` and upgraded once it grew a head)
988
1281
  # keeps the OLD value on every existing record — append-only means
@@ -996,9 +1289,19 @@ def analyse(repos):
996
1289
  "commit_hook": has_commit_hook(repo)})
997
1290
 
998
1291
  def seg(records):
1292
+ # An `FR` verdict grades a framework requirement line, not an application's
1293
+ # screen: it is written by a runner over a document the framework controls, so
1294
+ # it passes far more often than a REQ verified against a running app. Pooled
1295
+ # into an application segment it raises that segment's first-pass rate and
1296
+ # dilutes its escape rate, with nothing on the output to show it happened.
1297
+ # SCHEMA.md §3 states the rule -- "it never pools with the others" -- and this
1298
+ # is where it is applied. Tested by tests/regression/run.sh tf_020.
999
1299
  d = defaultdict(list)
1000
1300
  for r in records:
1001
- key = "unclassified" if r.get("project_type_inferred") else r.get("project_type", "app")
1301
+ if r.get("req_class") == "FR":
1302
+ key = "framework-requirement"
1303
+ else:
1304
+ key = "unclassified" if r.get("project_type_inferred") else r.get("project_type", "app")
1002
1305
  d[key].append(r)
1003
1306
  return d
1004
1307
 
@@ -1031,6 +1334,10 @@ def analyse(repos):
1031
1334
 
1032
1335
  out = {"per_repo": per_repo, "tainted_reqs": sorted("%s:%s" % x for x in tainted if x[1]),
1033
1336
  "live": {}, "backfilled": {}, "pooled": {},
1337
+ # what a `run-void` took out of every figure above, and why (SCHEMA.md §2.7)
1338
+ "runs_voided_n": voided_runs,
1339
+ "runs_voided": void_reasons,
1340
+ "run_voids_orphaned_n": void_orphans,
1034
1341
  "misses": analyse_misses(misses),
1035
1342
  "phases": analyse_phases(runs)}
1036
1343
 
@@ -1069,10 +1376,12 @@ def analyse(repos):
1069
1376
  # ---- poolable metrics: runs, cadence, tokens. Exempt from both separations.
1070
1377
  build_runs = [r for r in runs if r.get("cmd") == "build-phase"]
1071
1378
  fix_runs = [r for r in runs if r.get("mode") == "fix"]
1072
- throughput = [float(r["reqs_count"]) / r["duration_s"]
1073
- for r in runs if r.get("duration_s") and r.get("reqs_count")]
1379
+ throughput = [float(r["reqs_count"]) / _dur(r)
1380
+ for r in runs if _dur(r) is not None and r.get("reqs_count")]
1074
1381
  batch = [r["reqs_count"] for r in build_runs if r.get("reqs_count") is not None]
1075
1382
  verified_transitions = sum(1 for g in gates if g.get("verdict") == "Verified")
1383
+ pooled_list = [p for p in (list_price(REPO_OF.get(id(r), ""), r) for r in runs)
1384
+ if p is not None]
1076
1385
  tok = sum((s.get("input_tokens") or 0) + (s.get("output_tokens") or 0) for s in sessions)
1077
1386
  days = {c.get("ts", "")[:10] for c in commits if c.get("ts")}
1078
1387
  out["pooled"] = {
@@ -1086,7 +1395,16 @@ def analyse(repos):
1086
1395
  "tokens_total": tok,
1087
1396
  "tokens_per_verified_req": round(float(tok) / verified_transitions, 1)
1088
1397
  if verified_transitions >= MIN_N and tok else None,
1089
- "cost_usd": None, # never estimated — see SCHEMA.md §4
1398
+ "cost_usd": None, # what was BILLED is never estimated — see SCHEMA.md §4
1399
+ # What the same work costs at list price. A price, not a bill (SCHEMA.md §2.5b), and the
1400
+ # only money-shaped headline that works on a subscription — which is most of this
1401
+ # framework's runs. Summed over the runs that carry it, with that count published, so a
1402
+ # figure built on half the runs cannot be read as if it covered all of them.
1403
+ "list_usd_total": round(sum(pooled_list), 4),
1404
+ "list_usd_records": len(pooled_list),
1405
+ "list_usd_per_verified_req": (
1406
+ round(sum(pooled_list) / verified_transitions, 4)
1407
+ if verified_transitions >= MIN_N and len(pooled_list) >= MIN_N else None),
1090
1408
  "commits": len(commits),
1091
1409
  "commit_duplicates_collapsed": commit_dupes,
1092
1410
  "session_duplicates_collapsed": session_dupes,
@@ -1173,6 +1491,20 @@ def print_report(a, repos):
1173
1491
  print(" across project_type or across live/backfilled would produce a")
1174
1492
  print(" number that cannot be defended. See SCHEMA.md §6.")
1175
1493
  print("")
1494
+ if a.get("runs_voided_n"):
1495
+ print(" %d run record(s) VOIDED — named by a run-void record as wrong, so they are"
1496
+ % a["runs_voided_n"])
1497
+ print(" in no figure above. Both records stay on the stream; nothing was deleted:")
1498
+ for line in a.get("runs_voided", [])[:6]:
1499
+ print(" " + line)
1500
+ if len(a.get("runs_voided", [])) > 6:
1501
+ print(" ... and %d more" % (len(a["runs_voided"]) - 6))
1502
+ print("")
1503
+ if a.get("run_voids_orphaned_n"):
1504
+ print(" %d run-void record(s) name a run that is not on this stream — the void was"
1505
+ % a["run_voids_orphaned_n"])
1506
+ print(" written against a record that never arrived. Nothing was excluded for them.")
1507
+ print("")
1176
1508
  if a["tainted_reqs"]:
1177
1509
  print(" %d REQ(s) excluded from the LIVE first-pass rate because they carry"
1178
1510
  % len(a["tainted_reqs"]))
@@ -1193,8 +1525,14 @@ def print_report(a, repos):
1193
1525
  print(" REQ throughput : %s REQs/hour (median across runs)" %
1194
1526
  (p["throughput_median_reqs_per_hour"] if p["throughput_median_reqs_per_hour"] is not None else "insufficient data"))
1195
1527
  print(" sessions / tokens : %d sessions, %s total tokens" % (p["sessions"], "{:,}".format(p["tokens_total"])))
1196
- print(" tokens per Verified : %s (cost in USD is NEVER estimated — SCHEMA.md §4)" %
1528
+ print(" tokens per Verified : %s (what you were BILLED is never estimated — SCHEMA.md §4)" %
1197
1529
  (p["tokens_per_verified_req"] if p["tokens_per_verified_req"] is not None else "insufficient data"))
1530
+ if p.get("list_usd_records"):
1531
+ print(" list price : $%s over %d priced run(s) — the published rate applied to"
1532
+ % (p["list_usd_total"], p["list_usd_records"]))
1533
+ print(" the tokens actually measured. A price, not a bill.")
1534
+ if p.get("list_usd_per_verified_req") is not None:
1535
+ print(" list $ per Verified : $%s" % p["list_usd_per_verified_req"])
1198
1536
  print(" commit cadence : %s commits/active day (%d commits over %d days)" %
1199
1537
  (p["commits_per_active_day"] if p["commits_per_active_day"] is not None else "—",
1200
1538
  p["commits"], p["active_days"]))
@@ -1243,6 +1581,15 @@ def print_phases(p, W):
1243
1581
  print("-" * W)
1244
1582
  print(" %d live run record(s). Token-window coverage: %s"
1245
1583
  % (p["runs_live"], " ".join("%s=%d" % kv for kv in p["scope_coverage"].items())))
1584
+ if any(p.get(k) for k in ("duration_impossible_n", "duration_absent_n", "duration_recomputed_n")):
1585
+ print(" duration: %d of %d record(s) usable"
1586
+ % (p.get("duration_measured_n", 0), p["runs_live"])
1587
+ + (", %d impossible (start after end, discarded)" % p["duration_impossible_n"]
1588
+ if p.get("duration_impossible_n") else "")
1589
+ + (", %d record no elapsed time" % p["duration_absent_n"]
1590
+ if p.get("duration_absent_n") else "")
1591
+ + (", %d taken from the timestamps over a stored figure that disagreed"
1592
+ % p["duration_recomputed_n"] if p.get("duration_recomputed_n") else ""))
1246
1593
  print(" A window is only as good as its scope: 'tree' saw the subagents, 'main'")
1247
1594
  print(" did not look, 'none'/absent measured nothing at all and is excluded from")
1248
1595
  print(" every token figure below rather than averaged in as a zero.")
@@ -1323,8 +1670,57 @@ def print_phases(p, W):
1323
1670
  for h, c in m["cost_usd_by_harness"].items():
1324
1671
  print(" cost (%s) : $%s over %d record(s) — MEASURED, never pooled "
1325
1672
  "across harness" % (h, c["usd"], c["records"]))
1326
- if m["reqs_touched_total"] or m["files_written_total"]:
1327
- bm = m.get("by_mode") or {}
1673
+ bmodes = m.get("billing_modes") or {}
1674
+ if bmodes:
1675
+ print(" paid for by : " +
1676
+ " · ".join("%s %d" % (k, v) for k, v in bmodes.items()))
1677
+ if m.get("list_usd_records"):
1678
+ print(" list price : $%s over %d run(s) — the published rate applied to"
1679
+ % (m["list_usd"], m["list_usd_records"]))
1680
+ print(" the tokens on the record, worked out HERE and never")
1681
+ print(" stored. A price, not a bill: it is what makes a")
1682
+ print(" subscription run comparable with a metered one.")
1683
+ if m.get("list_usd_unpriced_n"):
1684
+ print(" %d run(s) had no rate for their model and are"
1685
+ % m["list_usd_unpriced_n"])
1686
+ print(" outside it — never priced at zero.")
1687
+ if m.get("money_records"):
1688
+ print(" money billed : $%s over %d metered run(s). Only a metered run is"
1689
+ % (m["money_usd"], m["money_records"]))
1690
+ print(" money billed; a subscription's zero is a flat fee")
1691
+ print(" already paid, a local model has no bill (§2.5b).")
1692
+ if m.get("plan_allowance_records"):
1693
+ print(" plan allowance : $%s used over %d run(s) on a monthly plan. This is"
1694
+ % (m["plan_allowance_usd"], m["plan_allowance_records"]))
1695
+ print(" the provider's own meter against a PER-MODEL")
1696
+ print(" allowance, not an invoice — and it is what runs out")
1697
+ print(" and sends a tier to its fallback.")
1698
+ for mod, v in (m.get("plan_allowance_by_model") or {}).items():
1699
+ print(" %-30s $%s of allowance" % (mod, v))
1700
+ if m.get("money_mixed_usd"):
1701
+ print(" also billed : $%s on %d run(s) whose window mixed billing modes —"
1702
+ % (m["money_mixed_usd"], m["money_mixed_records"]))
1703
+ print(" real money, but not separable from the part a")
1704
+ print(" subscription had already paid for.")
1705
+ for mod, c in (m.get("models") or {}).items():
1706
+ bits = []
1707
+ if c.get("list_usd"):
1708
+ bits.append("list $%s" % c["list_usd"])
1709
+ sp = (m.get("spend_by_model") or {}).get(mod)
1710
+ if sp:
1711
+ bits.append("billed $%s" % sp["usd"])
1712
+ if bits:
1713
+ print(" %-30s %s" % (mod, " · ".join(bits)))
1714
+ un = m.get("spend_unattributed") or {}
1715
+ if un.get("records"):
1716
+ print(" %-30s billed $%-8s over %d run(s) — the window ran several"
1717
+ % ("(not attributed)", un["usd"], un["records"]))
1718
+ print(" models, and one window's dollars are not split by token share.")
1719
+ # `bm` used to be assigned inside `if m["reqs_touched_total"] or m["files_written_total"]:`
1720
+ # and read outside it, so --phases crashed with UnboundLocalError on any phase that
1721
+ # touched no REQ and wrote no file — which is every metrics-report and every render.
1722
+ # MISS-TechieFlow-20260910-03.
1723
+ bm = m.get("by_mode") or {}
1328
1724
  if len(bm) > 1:
1329
1725
  print(" by mode : (in the order they ran)")
1330
1726
  for mode, v in bm.items():
@@ -1407,6 +1803,15 @@ def print_misses(m, W):
1407
1803
  print(" tokens to apply the corrections : %s (n=%d)"
1408
1804
  % (m["review_tokens_correct_mean"] if m["review_tokens_correct_mean"] is not None
1409
1805
  else "insufficient data", m["review_tokens_correct_n"]))
1806
+ if m.get("misses_voided_n"):
1807
+ print(" withdrawn : %d miss(es) named by a miss-void, in no figure above:" % m["misses_voided_n"])
1808
+ for line in m["misses_voided"][:6]:
1809
+ print(" " + line)
1810
+ if m["misses_voided_n"] > 6:
1811
+ print(" ... and %d more" % (m["misses_voided_n"] - 6))
1812
+ if m.get("miss_voids_orphaned_n"):
1813
+ print(" ⚠ %d miss-void record(s) name no miss on this stream — nothing was excluded for them"
1814
+ % m["miss_voids_orphaned_n"])
1410
1815
  if m["orphan_amends"]:
1411
1816
  print(" ⚠ %d miss-amend record(s) name no known miss, or a field outside the"
1412
1817
  % m["orphan_amends"])
@@ -1453,6 +1858,15 @@ def print_misses(m, W):
1453
1858
  print(" Claude Code carries cost_usd:null permanently and is NEVER")
1454
1859
  print(" priced from a rate card here (SCHEMA.md §4). Real dollars come from")
1455
1860
  print(" OpenCode runs; token counts are the honest figure everywhere else.")
1861
+ if m.get("cost_list_usd_per_miss") is not None:
1862
+ print(" list price per miss : $%s (%d record(s)) — the repair's tokens at the"
1863
+ % (m["cost_list_usd_per_miss"], m["cost_list_usd_records"]))
1864
+ print(" published rate. Works on every harness, and is a price, not a bill.")
1865
+ if m.get("cost_usd_excluded_not_money_n"):
1866
+ print(" %d fix record(s) measured a cost that is NOT money — a subscription's"
1867
+ % m["cost_usd_excluded_not_money_n"])
1868
+ print(" zero or a local model's absent bill — and are outside that figure")
1869
+ print(" rather than averaged in as free repairs (SCHEMA.md §2.5b).")
1456
1870
  print(" shared (apportioned): %d fix records — equal division, NOT a measurement"
1457
1871
  % m["cost_shared_n"])
1458
1872
  print(" tokens out per miss : %s (n=%d priced)" %