@techierathore/techieflow 1.0.2 → 1.0.4
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.
- package/.claude/commands/TechieFlow/agents/analyst.md +5 -13
- package/.claude/commands/TechieFlow/agents/architect.md +1 -16
- package/.claude/commands/TechieFlow/agents/flow-master.md +42 -215
- package/.claude/commands/TechieFlow/agents/verifier.md +24 -64
- package/.claude/commands/TechieFlow/tasks/_metrics-emit-gate.md +24 -117
- package/.claude/commands/TechieFlow/tasks/_owner-language.md +26 -0
- package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +20 -84
- package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +10 -74
- package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +20 -58
- package/.claude/commands/TechieFlow/tasks/amend-docs.md +21 -143
- package/.claude/commands/TechieFlow/tasks/build-phase.md +14 -224
- package/.claude/commands/TechieFlow/tasks/create-deep-research-prompt.md +4 -0
- package/.claude/commands/TechieFlow/tasks/create-doc.md +4 -0
- package/.claude/commands/TechieFlow/tasks/create-project-brief.md +22 -0
- package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +31 -505
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +28 -213
- package/.claude/commands/TechieFlow/tasks/deploy-checklist.md +19 -0
- package/.claude/commands/TechieFlow/tasks/devguide.md +10 -219
- package/.claude/commands/TechieFlow/tasks/facilitate-brainstorming-session.md +4 -0
- package/.claude/commands/TechieFlow/tasks/fix-issues.md +10 -129
- package/.claude/commands/TechieFlow/tasks/generate-html.md +5 -1
- package/.claude/commands/TechieFlow/tasks/handoff-phase.md +18 -16
- package/.claude/commands/TechieFlow/tasks/log-miss.md +19 -158
- package/.claude/commands/TechieFlow/tasks/metrics-report.md +3 -1
- package/.claude/commands/TechieFlow/tasks/mockups.md +12 -117
- package/.claude/commands/TechieFlow/tasks/refresh-status.md +9 -130
- package/.claude/commands/TechieFlow/tasks/split-brd.md +10 -126
- package/.claude/commands/TechieFlow/tasks/triage-and-fix.md +14 -0
- package/.claude/commands/TechieFlow/tasks/triage-issues.md +11 -152
- package/.claude/commands/TechieFlow/tasks/verify-phase.md +36 -572
- package/.opencode/plugin/techieflow.js +40 -13
- package/.tfcore/agents/analyst.md +5 -13
- package/.tfcore/agents/architect.md +1 -16
- package/.tfcore/agents/flow-master.md +42 -215
- package/.tfcore/agents/verifier.md +24 -64
- package/.tfcore/core-config.yaml +10 -0
- package/.tfcore/hooks/block-git.sh +4 -4
- package/.tfcore/hooks/guard-artifacts.sh +2 -2
- package/.tfcore/hooks/guard-build.sh +76 -0
- package/.tfcore/hooks/guard-db.sh +90 -0
- package/.tfcore/hooks/guard-metrics.sh +68 -0
- package/.tfcore/hooks/guard-status-html.sh +116 -71
- package/.tfcore/hooks/guard-status.sh +43 -16
- package/.tfcore/hooks/guard-verify-deps.sh +124 -0
- package/.tfcore/hooks/guard-verify.sh +70 -48
- package/.tfcore/hooks/sweep-artifacts.sh +2 -2
- package/.tfcore/install-manifest.yaml +0 -18
- package/.tfcore/routing.yaml +86 -14
- package/.tfcore/standards/coding-standards-core.md +61 -0
- package/.tfcore/standards/coding-standards-dotnet.md +93 -0
- package/.tfcore/tasks/_metrics-emit-gate.md +24 -117
- package/.tfcore/tasks/_owner-language.md +26 -0
- package/.tfcore/tasks/_smoke-test-policy.md +20 -84
- package/.tfcore/tasks/_status-update-gate.md +10 -74
- package/.tfcore/tasks/_yolo-mode.md +20 -58
- package/.tfcore/tasks/amend-docs.md +21 -143
- package/.tfcore/tasks/build-phase.md +14 -224
- package/.tfcore/tasks/create-deep-research-prompt.md +4 -0
- package/.tfcore/tasks/create-doc.md +4 -0
- package/.tfcore/tasks/create-project-brief.md +22 -0
- package/.tfcore/tasks/day1-brownfield.md +31 -505
- package/.tfcore/tasks/day1-greenfield.md +28 -213
- package/.tfcore/tasks/deploy-checklist.md +19 -0
- package/.tfcore/tasks/devguide.md +10 -219
- package/.tfcore/tasks/facilitate-brainstorming-session.md +4 -0
- package/.tfcore/tasks/fix-issues.md +10 -129
- package/.tfcore/tasks/generate-html.md +5 -1
- package/.tfcore/tasks/handoff-phase.md +18 -16
- package/.tfcore/tasks/log-miss.md +19 -158
- package/.tfcore/tasks/metrics-report.md +3 -1
- package/.tfcore/tasks/mockups.md +12 -117
- package/.tfcore/tasks/refresh-status.md +9 -130
- package/.tfcore/tasks/split-brd.md +10 -126
- package/.tfcore/tasks/triage-and-fix.md +14 -0
- package/.tfcore/tasks/triage-issues.md +11 -152
- package/.tfcore/tasks/verify-phase.md +36 -572
- package/.tfcore/telemetry/SCHEMA.md +100 -8
- package/.tfcore/telemetry/tf-metrics.sh +564 -32
- package/.tfcore/templates/stack-defaults/dotnet.md +35 -0
- package/.tfcore/templates/stack-questions.md +77 -0
- package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +3 -4
- package/.tfcore/templates/v4custom/app-architecture-tmpl.md +107 -115
- package/.tfcore/templates/v4custom/app-brd-tmpl.md +108 -184
- package/.tfcore/templates/v4custom/app-brief-tmpl.md +65 -0
- package/.tfcore/templates/v4custom/app-checklist-tmpl.md +48 -73
- package/.tfcore/templates/v4custom/app-claude-md-tmpl.md +1 -1
- package/.tfcore/templates/v4custom/app-coding-standards-tmpl.md +52 -0
- package/.tfcore/templates/v4custom/app-decision-request-tmpl.md +85 -0
- package/.tfcore/templates/v4custom/app-deployment-checklist-tmpl.md +86 -0
- package/.tfcore/templates/v4custom/app-devguide-tmpl.md +69 -146
- package/.tfcore/templates/v4custom/app-library-feedback-tmpl.md +86 -31
- package/.tfcore/templates/v4custom/app-phases-tmpl.md +35 -0
- package/.tfcore/templates/v4custom/app-productguide-tmpl.md +38 -35
- package/.tfcore/templates/v4custom/app-project-status-tmpl.md +63 -73
- package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +62 -37
- package/.tfcore/templates/v4custom/app-usageguide-tmpl.md +69 -54
- package/.tfcore/templates/v4custom/build-invocation-ladder.md +17 -180
- package/.tfcore/templates/v4custom/build-subagent-prompt.md +17 -0
- package/.tfcore/templates/v4custom/html-render-shell.md +1 -1
- package/.tfcore/templates/v4custom/metrics-report-template.md +2 -2
- package/.tfcore/user-guide.md +0 -23
- package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-checklist-edit.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-codex-bind.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-codex-telemetry.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-perf-grade.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
- package/.tfcore/utils/tf-brd-status.py +176 -0
- package/.tfcore/utils/tf-brd-status.sh +19 -0
- package/.tfcore/utils/tf-build-list.py +211 -0
- package/.tfcore/utils/tf-build-list.sh +21 -0
- package/.tfcore/utils/tf-build.sh +143 -0
- package/.tfcore/utils/tf-cdp-relay.ps1 +64 -0
- package/.tfcore/utils/tf-checklist-edit.py +133 -0
- package/.tfcore/utils/tf-day1-files.py +208 -0
- package/.tfcore/utils/tf-day1-files.sh +20 -0
- package/.tfcore/utils/tf-devguide-list.py +249 -0
- package/.tfcore/utils/tf-devguide-list.sh +18 -0
- package/.tfcore/utils/tf-doc-check.py +1369 -0
- package/.tfcore/utils/tf-doc-check.sh +29 -0
- package/.tfcore/utils/tf-emit.sh +545 -18
- package/.tfcore/utils/tf-fix-close.sh +115 -0
- package/.tfcore/utils/tf-gitignore-audit.sh +50 -3
- package/.tfcore/utils/tf-goal.sh +284 -66
- package/.tfcore/utils/tf-harness.sh +4 -9
- package/.tfcore/utils/tf-log-miss.py +197 -0
- package/.tfcore/utils/tf-log-miss.sh +14 -0
- package/.tfcore/utils/tf-misses-md.py +208 -0
- package/.tfcore/utils/tf-misses-md.sh +14 -0
- package/.tfcore/utils/tf-mockups-locate.py +66 -0
- package/.tfcore/utils/tf-mockups-locate.sh +18 -0
- package/.tfcore/utils/tf-model-pick.sh +475 -0
- package/.tfcore/utils/tf-opencode-audit.py +23 -7
- package/.tfcore/utils/tf-perf-grade.py +97 -0
- package/.tfcore/utils/tf-perf-grade.sh +12 -0
- package/.tfcore/utils/tf-phase.sh +92 -0
- package/.tfcore/utils/tf-render-html.py +20 -0
- package/.tfcore/utils/tf-routing-bind.sh +97 -6
- package/.tfcore/utils/tf-routing.sh +13 -7
- package/.tfcore/utils/tf-selfcheck.sh +323 -0
- package/.tfcore/utils/tf-split-brd.py +309 -0
- package/.tfcore/utils/tf-split-brd.sh +22 -0
- package/.tfcore/utils/tf-status-evidence.sh +68 -0
- package/.tfcore/utils/tf-status-facts.py +358 -0
- package/.tfcore/utils/tf-status-facts.sh +20 -0
- package/.tfcore/utils/tf-triage.py +243 -0
- package/.tfcore/utils/tf-triage.sh +16 -0
- package/.tfcore/utils/tf-verify-boot.sh +250 -0
- package/.tfcore/utils/tf-verify-emit.sh +110 -0
- package/.tfcore/utils/tf-verify-env.sh +88 -0
- package/.tfcore/utils/tf-verify-list.py +270 -0
- package/.tfcore/utils/tf-verify-list.sh +13 -0
- package/.tfcore/utils/tf-verify-screens.mjs +355 -0
- package/.tfcore/utils/tf-verify-screens.sh +31 -0
- package/.tfcore/utils/tf-verify-tests.sh +166 -0
- package/.tfcore/utils/tf-verify-verdict.py +292 -0
- package/.tfcore/utils/tf-verify-verdict.sh +15 -0
- package/.tfcore/utils/tf-yolo.sh +59 -0
- package/README.md +120 -1145
- package/docs/TechieFlow-Installation.md +1 -5
- package/opencode.jsonc +37 -33
- package/package.json +1 -5
- package/scripts/install.mjs +56 -46
- package/.claude/commands/TechieFlow/tasks/advanced-elicitation.md +0 -117
- package/.claude/commands/TechieFlow/tasks/author-brd.md +0 -79
- package/.claude/commands/TechieFlow/tasks/document-project.md +0 -343
- package/.claude/commands/TechieFlow/tasks/execute-checklist.md +0 -86
- package/.claude/commands/TechieFlow/tasks/index-docs.md +0 -173
- package/.claude/commands/TechieFlow/tasks/kb-mode-interaction.md +0 -75
- package/.claude/commands/TechieFlow/tasks/shard-doc.md +0 -185
- package/.codex/config.toml +0 -46
- package/.codex/hooks.json +0 -63
- package/.codex/rules/techieflow.rules +0 -8
- package/.tfcore/hooks/codex-adapter.py +0 -248
- package/.tfcore/tasks/advanced-elicitation.md +0 -117
- package/.tfcore/tasks/author-brd.md +0 -79
- package/.tfcore/tasks/document-project.md +0 -343
- package/.tfcore/tasks/execute-checklist.md +0 -86
- package/.tfcore/tasks/index-docs.md +0 -173
- package/.tfcore/tasks/kb-mode-interaction.md +0 -75
- package/.tfcore/tasks/shard-doc.md +0 -185
- package/.tfcore/templates/architecture-tmpl.yaml +0 -650
- package/.tfcore/templates/brownfield-architecture-tmpl.yaml +0 -476
- package/.tfcore/templates/competitor-analysis-tmpl.yaml +0 -306
- package/.tfcore/templates/front-end-architecture-tmpl.yaml +0 -218
- package/.tfcore/templates/fullstack-architecture-tmpl.yaml +0 -823
- package/.tfcore/templates/market-research-tmpl.yaml +0 -252
- package/.tfcore/templates/project-brief-tmpl.yaml +0 -221
- package/.tfcore/utils/techieflow-doc-template.md +0 -325
- package/.tfcore/utils/tf-codex-bind.py +0 -196
- package/.tfcore/utils/tf-codex-telemetry.py +0 -60
- package/WORKFLOW.html +0 -1986
|
@@ -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
|
|
@@ -34,6 +34,7 @@ import re
|
|
|
34
34
|
import subprocess
|
|
35
35
|
import sys
|
|
36
36
|
from collections import Counter, defaultdict, OrderedDict
|
|
37
|
+
from datetime import datetime
|
|
37
38
|
|
|
38
39
|
STREAMS = ("runs", "gates", "sessions", "commits", "misses")
|
|
39
40
|
VERDICTS = ("Verified", "Needs re-verify", "FAIL", "Blocked", "Implemented", "Done (pre-existing)")
|
|
@@ -52,20 +53,155 @@ LATE_GATES = {"perf": "2026-08-10", "assets": "2026-08-31", "mockup-parity": "20
|
|
|
52
53
|
# fill, so counting it as "not assessed" understates the practice being measured.
|
|
53
54
|
# It leaves that field's denominator and is reported separately. SCHEMA.md §5.5.6 /
|
|
54
55
|
# §3.5. Keep this table in sync when an optional field is added to any stream.
|
|
55
|
-
FIELD_SINCE = {"why_missed": "2026-08-28"}
|
|
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
|
|
56
117
|
|
|
57
118
|
# Fields a `miss-amend` record may complete, and their closed vocabularies.
|
|
58
119
|
# Kept identical to tf-emit.sh's `_AMENDABLE`; the emitter enforces it on write and
|
|
59
120
|
# this enforces it again on read, because a merged stream can carry records this
|
|
60
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
|
+
|
|
61
127
|
AMENDABLE_FIELDS = {
|
|
62
128
|
"why_missed": ("missing-checklist-item", "insufficient-verify-method",
|
|
63
129
|
"code-audit-limitation", "ambiguous-acceptance",
|
|
64
130
|
"dependency-not-declared", "instruction-ignored", "other"),
|
|
131
|
+
"sort": ("spec", "unsaid", "weak-check", "ignored"),
|
|
65
132
|
}
|
|
133
|
+
# The four-question sort in words, for the report (SCHEMA.md §5.5.10).
|
|
134
|
+
SORT_WORDS = {"spec": "the app's spec did not say it", "unsaid": "the framework never said it",
|
|
135
|
+
"weak-check": "the check was too weak", "ignored": "said and ignored"}
|
|
66
136
|
MIN_N = 3 # fewer supporting records than this -> "insufficient data", never a number
|
|
67
137
|
|
|
68
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
|
+
|
|
69
205
|
# ---------------------------------------------------------------- utilities
|
|
70
206
|
def die(msg):
|
|
71
207
|
sys.stderr.write("tf-metrics: %s\n" % msg)
|
|
@@ -94,6 +230,42 @@ def read_stream(repo, stream):
|
|
|
94
230
|
return out
|
|
95
231
|
|
|
96
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
|
+
|
|
97
269
|
def emit(repo, stream, records, dry_run, quiet=False):
|
|
98
270
|
"""Append via tf-emit.sh — the single append primitive. Never write JSONL directly.
|
|
99
271
|
|
|
@@ -206,6 +378,7 @@ def analyse_misses(misses):
|
|
|
206
378
|
opened = [m for m in misses if m.get("kind") == "miss" and not m.get("backfilled")]
|
|
207
379
|
fixes = [m for m in misses if m.get("kind") == "miss-fix" and not m.get("backfilled")]
|
|
208
380
|
amends = [m for m in misses if m.get("kind") == "miss-amend"]
|
|
381
|
+
reviews = [m for m in misses if m.get("kind") == "review"]
|
|
209
382
|
|
|
210
383
|
# ---- fold amendments into their parents BEFORE anything is counted (§5.5.7).
|
|
211
384
|
# An amend completes a field the miss left null; it can never overwrite one, so
|
|
@@ -221,11 +394,14 @@ def analyse_misses(misses):
|
|
|
221
394
|
for a in sorted(amends, key=lambda r: r.get("ts") or ""):
|
|
222
395
|
parent = opened_by_id.get(a.get("miss_id"))
|
|
223
396
|
fld = a.get("field")
|
|
224
|
-
if parent is None or fld not in AMENDABLE_FIELDS:
|
|
397
|
+
if parent is None or (fld not in AMENDABLE_FIELDS and fld not in AMENDABLE_TEXT_FIELDS):
|
|
225
398
|
orphan_amends += 1
|
|
226
399
|
continue
|
|
227
|
-
|
|
228
|
-
|
|
400
|
+
val = a.get("value")
|
|
401
|
+
ok = (isinstance(val, str) and val.strip()) if fld in AMENDABLE_TEXT_FIELDS \
|
|
402
|
+
else (val in AMENDABLE_FIELDS.get(fld, ()))
|
|
403
|
+
if parent.get(fld) is None and ok:
|
|
404
|
+
parent[fld] = val
|
|
229
405
|
amended += 1
|
|
230
406
|
|
|
231
407
|
# ---- eligibility floor for optional fields added mid-stream (SCHEMA.md §3.5,
|
|
@@ -235,8 +411,10 @@ def analyse_misses(misses):
|
|
|
235
411
|
# reported separately — never silently, and never backfilled with a value
|
|
236
412
|
# nobody assessed at the time.
|
|
237
413
|
def _eligible(rec, field):
|
|
414
|
+
# A record written after the field existed, or one that carries it anyway (an older
|
|
415
|
+
# record completed by an amend is sorted, whatever its date; Session 5, 2026-09-07).
|
|
238
416
|
since = FIELD_SINCE.get(field)
|
|
239
|
-
return not since or (rec.get("ts") or "") >= since
|
|
417
|
+
return not since or (rec.get("ts") or "") >= since or bool(rec.get(field))
|
|
240
418
|
|
|
241
419
|
# Latest fix per miss decides open/closed; a fix naming no known miss is an orphan.
|
|
242
420
|
known = {m.get("miss_id") for m in opened}
|
|
@@ -349,13 +527,36 @@ def analyse_misses(misses):
|
|
|
349
527
|
sole_tokens, sole_priced_n = tok(sole)
|
|
350
528
|
shared_priced = [f for f in shared if f.get("tokens_out") is not None]
|
|
351
529
|
|
|
352
|
-
# Dollars exist ONLY where a harness measured them. Claude Code
|
|
530
|
+
# Dollars exist ONLY where a harness measured them. Claude Code carries
|
|
353
531
|
# cost_usd:null permanently (SCHEMA.md §4) and are never priced from a rate card
|
|
354
532
|
# here — a pooled sum over mixed harnesses would silently under-report.
|
|
355
|
-
|
|
533
|
+
#
|
|
534
|
+
# A null was never the only way to have no money figure, though. An OpenCode run on a
|
|
535
|
+
# subscription records cost_usd 0.0 — a real, measured zero, because the plan was already
|
|
536
|
+
# paid for — and averaging that in as a FREE repair is the TF-005 defect again, in the
|
|
537
|
+
# column right beside the one TF-005 was found in. Only a `metered` record is money
|
|
538
|
+
# (SCHEMA.md §2.5b). A record written before billing_mode existed is admitted on its old
|
|
539
|
+
# terms, and counted, so the change does not silently rewrite the past.
|
|
540
|
+
paid = [f for f in sole if f.get("cost_usd") is not None
|
|
541
|
+
and (f.get("billing_mode") in (None, "metered"))]
|
|
542
|
+
unpriced_subscription_n = len([f for f in sole if f.get("cost_usd") is not None
|
|
543
|
+
and f.get("billing_mode") not in (None, "metered")])
|
|
544
|
+
# And the figure that works on every harness: what the repair's tokens cost at list price,
|
|
545
|
+
# worked out here from the tokens the record carries. It is the only money-shaped number a
|
|
546
|
+
# Claude Code repair can have, because a Max subscription bills nothing per token.
|
|
547
|
+
listed = [p for p in (list_price(REPO_OF.get(id(f), ""), f) for f in sole) if p is not None]
|
|
356
548
|
escaped = [m for m in opened if m.get("found_by") in ("owner", "production")]
|
|
357
549
|
design = [m for m in opened if m.get("miss_class") == "unspecified-gap"]
|
|
358
550
|
|
|
551
|
+
# Owner reviews (§5.5.9): how many corrections each phase's output needed, and what
|
|
552
|
+
# producing it and correcting it cost. Token means divide by the records that carry the
|
|
553
|
+
# figure (§5.5.8), never by all of them.
|
|
554
|
+
def _mean(key):
|
|
555
|
+
xs = [r[key] for r in reviews if isinstance(r.get(key), (int, float))]
|
|
556
|
+
return (round(float(sum(xs)) / len(xs), 1) if len(xs) >= MIN_N else None), len(xs)
|
|
557
|
+
tp, tp_n = _mean("tokens_produce")
|
|
558
|
+
tc, tc_n = _mean("tokens_correct")
|
|
559
|
+
|
|
359
560
|
return {
|
|
360
561
|
"misses_total": len(opened),
|
|
361
562
|
"miss_fixes_total": len(fixes),
|
|
@@ -381,6 +582,22 @@ def analyse_misses(misses):
|
|
|
381
582
|
if m.get("found_by") in ("owner", "production")
|
|
382
583
|
and not m.get("why_missed")
|
|
383
584
|
and _eligible(m, "why_missed")),
|
|
585
|
+
# Whose gap (§5.5.10, FR-32): the four-question sort. Same denominator rule as
|
|
586
|
+
# why_missed — records that carry it, out of records that could have.
|
|
587
|
+
"sort_n": sum(1 for m in opened if m.get("sort")),
|
|
588
|
+
"sort_eligible": sum(1 for m in opened if _eligible(m, "sort")),
|
|
589
|
+
"sort_predates_field": sum(1 for m in opened if not _eligible(m, "sort")),
|
|
590
|
+
"sort": OrderedDict(
|
|
591
|
+
sorted(Counter(m["sort"] for m in opened if m.get("sort")).items(),
|
|
592
|
+
key=lambda kv: (-kv[1], kv[0]))),
|
|
593
|
+
# Owner reviews (§5.5.9)
|
|
594
|
+
"reviews_n": len(reviews),
|
|
595
|
+
"review_corrections": sum(r["corrections"] for r in reviews if isinstance(r.get("corrections"), int)),
|
|
596
|
+
"reviews_by_phase": OrderedDict(
|
|
597
|
+
sorted(Counter(r.get("phase") or "?" for r in reviews).items(),
|
|
598
|
+
key=lambda kv: (-kv[1], kv[0]))),
|
|
599
|
+
"review_tokens_produce_mean": tp, "review_tokens_produce_n": tp_n,
|
|
600
|
+
"review_tokens_correct_mean": tc, "review_tokens_correct_n": tc_n,
|
|
384
601
|
"class_distribution": OrderedDict(
|
|
385
602
|
sorted(Counter(m.get("miss_class") or "?" for m in opened).items(),
|
|
386
603
|
key=lambda kv: (-kv[1], kv[0]))),
|
|
@@ -429,6 +646,12 @@ def analyse_misses(misses):
|
|
|
429
646
|
"cost_usd_per_miss_measured": round(
|
|
430
647
|
sum(f["cost_usd"] for f in paid) / len(paid), 4) if len(paid) >= MIN_N else None,
|
|
431
648
|
"cost_usd_records": len(paid),
|
|
649
|
+
# Repairs whose dollars were measured and are NOT money: a subscription's zero, a local
|
|
650
|
+
# model's absent bill. Printed beside the figure, never inside it.
|
|
651
|
+
"cost_usd_excluded_not_money_n": unpriced_subscription_n,
|
|
652
|
+
"cost_list_usd_per_miss": round(
|
|
653
|
+
sum(listed) / len(listed), 4) if len(listed) >= MIN_N else None,
|
|
654
|
+
"cost_list_usd_records": len(listed),
|
|
432
655
|
}
|
|
433
656
|
|
|
434
657
|
|
|
@@ -455,7 +678,7 @@ def analyse_phases(runs):
|
|
|
455
678
|
confident fan-out figures largely composed of runs that could not have
|
|
456
679
|
seen a subagent. Tree-scope only, with the exclusion printed.
|
|
457
680
|
|
|
458
|
-
3. DOLLARS. Never pooled across harness (SCHEMA.md §4). Claude
|
|
681
|
+
3. DOLLARS. Never pooled across harness (SCHEMA.md §4). Claude
|
|
459
682
|
carry cost_usd:null permanently; a sum over mixed records under-reports
|
|
460
683
|
silently. Reported per harness or not at all.
|
|
461
684
|
|
|
@@ -464,6 +687,41 @@ def analyse_phases(runs):
|
|
|
464
687
|
when someone asks how it was measured."""
|
|
465
688
|
live = [r for r in runs if r.get("kind", "run") == "run" and not r.get("backfilled")]
|
|
466
689
|
|
|
690
|
+
# A run that carries both ends but no `duration_s` was silently worth ZERO TIME here,
|
|
691
|
+
# while its tokens still counted — so a phase's total time was a sum over some of its
|
|
692
|
+
# runs and its tokens a sum over others (found 2026-09-07: the reset's own 13 runs
|
|
693
|
+
# reported 16h49m, because 6 of them predate the field and the true figure is ~55h).
|
|
694
|
+
# Deriving it is arithmetic on two recorded facts, not a guess, so it is done — and
|
|
695
|
+
# counted, so the report can say how many of its minutes were derived rather than read.
|
|
696
|
+
# `ended` itself may be absent on an older record; SCHEMA.md's own rule is that `ended`
|
|
697
|
+
# IS the moment the record was written, which is exactly what `ts` holds.
|
|
698
|
+
def _secs(a, b):
|
|
699
|
+
try:
|
|
700
|
+
fmt = "%Y-%m-%dT%H:%M:%SZ"
|
|
701
|
+
return int((datetime.strptime(b, fmt) - datetime.strptime(a, fmt)).total_seconds())
|
|
702
|
+
except Exception:
|
|
703
|
+
return None
|
|
704
|
+
|
|
705
|
+
# A stored duration of zero or less is not a measurement: it is an impossible record
|
|
706
|
+
# (`ended` before `started`) that reached the stream before the emitter refused those.
|
|
707
|
+
# Treat it as absent so it is re-derived here, and -- when the timestamps are just as
|
|
708
|
+
# impossible -- excluded from every figure below rather than admitted by a truthiness
|
|
709
|
+
# test. tests/regression/run.sh tf_015.
|
|
710
|
+
derived_n = 0
|
|
711
|
+
negative_durations = sum(1 for r in live
|
|
712
|
+
if isinstance(r.get("duration_s"), (int, float)) and r["duration_s"] < 0)
|
|
713
|
+
for r in live:
|
|
714
|
+
if _dur(r) is not None or not r.get("started"):
|
|
715
|
+
continue
|
|
716
|
+
end = r.get("ended") or r.get("ts")
|
|
717
|
+
if not end:
|
|
718
|
+
continue
|
|
719
|
+
d = _secs(r["started"], end)
|
|
720
|
+
if d is not None and d >= 0:
|
|
721
|
+
r["duration_s"] = d
|
|
722
|
+
r["duration_derived"] = "ended" if r.get("ended") else "ts"
|
|
723
|
+
derived_n += 1
|
|
724
|
+
|
|
467
725
|
def scope_of(r):
|
|
468
726
|
return r.get("tokens_scope") or "absent"
|
|
469
727
|
|
|
@@ -481,16 +739,28 @@ def analyse_phases(runs):
|
|
|
481
739
|
return scope_of(r) == "tree" and r.get("subagent_runs") is not None
|
|
482
740
|
|
|
483
741
|
grand_out = sum(r.get("tokens_out") or 0 for r in live if has_tokens(r))
|
|
484
|
-
grand_dur = sum(r
|
|
742
|
+
grand_dur = sum(_dur(r) or 0 for r in live if _dur(r) is not None)
|
|
485
743
|
|
|
486
744
|
phases = OrderedDict()
|
|
487
745
|
for cmd in sorted({r.get("cmd") or "?" for r in live}):
|
|
488
746
|
rs = [r for r in live if (r.get("cmd") or "?") == cmd]
|
|
489
747
|
priced = [r for r in rs if has_tokens(r)]
|
|
490
|
-
durs = [r
|
|
748
|
+
durs = [_dur(r) for r in rs if _dur(r) is not None]
|
|
491
749
|
fan = [r for r in rs if sees_subagents(r)]
|
|
492
750
|
|
|
493
751
|
models = defaultdict(lambda: {"runs": 0, "tokens_out": 0})
|
|
752
|
+
# List price, COMPUTED HERE from the tokens the record carries (see list_price above —
|
|
753
|
+
# pricing is a reporting job, and nothing priced is ever stored). Per model, attributed
|
|
754
|
+
# only where the window ran ONE model, the same rule the measured dollars follow below.
|
|
755
|
+
list_of = {}
|
|
756
|
+
for r in priced:
|
|
757
|
+
lp = list_price(REPO_OF.get(id(r), ""), r)
|
|
758
|
+
if lp is None:
|
|
759
|
+
continue
|
|
760
|
+
list_of[id(r)] = lp
|
|
761
|
+
ms = r.get("models") or ([r["model"]] if r.get("model") else [])
|
|
762
|
+
if len(ms) == 1:
|
|
763
|
+
models[ms[0]]["list_usd"] = round(models[ms[0]].get("list_usd", 0.0) + lp, 6)
|
|
494
764
|
for r in priced:
|
|
495
765
|
split = r.get("model_tokens_out")
|
|
496
766
|
if isinstance(split, dict) and split:
|
|
@@ -517,12 +787,42 @@ def analyse_phases(runs):
|
|
|
517
787
|
sub_out = sum(r.get("tokens_out_subagents") or 0 for r in fan)
|
|
518
788
|
fan_out_total = sum(r.get("tokens_out") or 0 for r in fan if has_tokens(r))
|
|
519
789
|
|
|
790
|
+
# DOLLARS, and what each one means. A harness split was never enough: one OpenCode
|
|
791
|
+
# machine mixes a flat-fee subscription (whose marginal cost is a true zero), a metered
|
|
792
|
+
# API key (whose dollars are real money) and a local model (which has no bill at all),
|
|
793
|
+
# and pooling those three produces a money figure that is neither the money spent nor
|
|
794
|
+
# the effort used. `billing_mode` is the separation; only `metered` is money.
|
|
520
795
|
cost_by_harness = defaultdict(float)
|
|
521
796
|
cost_n = Counter()
|
|
797
|
+
cost_by_mode = defaultdict(float)
|
|
798
|
+
mode_n = Counter()
|
|
799
|
+
model_usd = defaultdict(float)
|
|
800
|
+
model_usd_n = Counter()
|
|
801
|
+
plan_by_model = defaultdict(float)
|
|
802
|
+
usd_unattributed, usd_unattributed_n = 0.0, 0
|
|
522
803
|
for r in priced:
|
|
804
|
+
mode = r.get("billing_mode") or "unrecorded"
|
|
805
|
+
mode_n[mode] += 1
|
|
523
806
|
if r.get("cost_usd") is not None:
|
|
524
807
|
cost_by_harness[r.get("harness") or "?"] += r["cost_usd"]
|
|
525
808
|
cost_n[r.get("harness") or "?"] += 1
|
|
809
|
+
cost_by_mode[mode] += r["cost_usd"]
|
|
810
|
+
if mode == "plan":
|
|
811
|
+
ms = r.get("models") or ([r["model"]] if r.get("model") else [])
|
|
812
|
+
if len(ms) == 1:
|
|
813
|
+
plan_by_model[ms[0]] += r["cost_usd"]
|
|
814
|
+
if mode == "metered":
|
|
815
|
+
# Per-model spend is attributed ONLY where the window ran one model.
|
|
816
|
+
# Splitting one window's dollars across several models by their token
|
|
817
|
+
# share is arithmetic, not measurement — the same line §5.5.3 draws
|
|
818
|
+
# between `sole` and `shared`, one stream over.
|
|
819
|
+
ms = r.get("models") or ([r["model"]] if r.get("model") else [])
|
|
820
|
+
if len(ms) == 1:
|
|
821
|
+
model_usd[ms[0]] += r["cost_usd"]
|
|
822
|
+
model_usd_n[ms[0]] += 1
|
|
823
|
+
else:
|
|
824
|
+
usd_unattributed += r["cost_usd"]
|
|
825
|
+
usd_unattributed_n += 1
|
|
526
826
|
|
|
527
827
|
tok = lambda k: sum(r.get(k) or 0 for r in priced)
|
|
528
828
|
phases[cmd] = {
|
|
@@ -532,6 +832,10 @@ def analyse_phases(runs):
|
|
|
532
832
|
"median": median(durs),
|
|
533
833
|
"max": max(durs) if durs else None,
|
|
534
834
|
"n": len(durs),
|
|
835
|
+
# how many of those minutes were computed from the record's own two
|
|
836
|
+
# timestamps because the record predates `duration_s` — arithmetic on
|
|
837
|
+
# recorded facts, but the reader is told rather than left to assume.
|
|
838
|
+
"derived_n": sum(1 for r in rs if r.get("duration_derived")),
|
|
535
839
|
},
|
|
536
840
|
"share_of_duration": pct(sum(durs), grand_dur),
|
|
537
841
|
"tokens_measured_n": len(priced),
|
|
@@ -549,6 +853,27 @@ def analyse_phases(runs):
|
|
|
549
853
|
key=lambda kv: (-kv[1]["tokens_out"], kv[0]))),
|
|
550
854
|
"harnesses": OrderedDict(sorted(Counter(r.get("harness") or "?" for r in rs).items())),
|
|
551
855
|
"modes": OrderedDict(sorted(Counter(r.get("mode") or "—" for r in rs).items())),
|
|
856
|
+
# A phase run in named modes (a reset's sessions, a build's fresh/fix passes)
|
|
857
|
+
# is several different jobs under one `cmd`. The per-mode split says which,
|
|
858
|
+
# in the order they happened. Same denominators as the phase above: a run
|
|
859
|
+
# with no computable window contributes no tokens, never a zero.
|
|
860
|
+
"by_mode": OrderedDict(
|
|
861
|
+
(mode, {
|
|
862
|
+
"runs": len([r for r in rs if (r.get("mode") or "—") == mode]),
|
|
863
|
+
"duration_s": sum(_dur(r) or 0
|
|
864
|
+
for r in rs if (r.get("mode") or "—") == mode),
|
|
865
|
+
"tokens_out": sum(r.get("tokens_out") or 0
|
|
866
|
+
for r in rs if (r.get("mode") or "—") == mode and has_tokens(r)),
|
|
867
|
+
"tokens_unmeasured_n": len([r for r in rs
|
|
868
|
+
if (r.get("mode") or "—") == mode and not has_tokens(r)]),
|
|
869
|
+
"files_written": sum(r.get("files_written") or 0
|
|
870
|
+
for r in rs if (r.get("mode") or "—") == mode),
|
|
871
|
+
"first_started": min((r.get("started") or "" for r in rs
|
|
872
|
+
if (r.get("mode") or "—") == mode), default=""),
|
|
873
|
+
})
|
|
874
|
+
for mode in sorted({(r.get("mode") or "—") for r in rs},
|
|
875
|
+
key=lambda m: min((r.get("started") or "" for r in rs
|
|
876
|
+
if (r.get("mode") or "—") == m), default=""))),
|
|
552
877
|
"build_result": OrderedDict(sorted(Counter(r.get("build_result") or "—" for r in rs).items())),
|
|
553
878
|
"reqs_touched_total": sum(r.get("reqs_count") or 0 for r in rs),
|
|
554
879
|
"files_written_total": sum(r.get("files_written") or 0 for r in rs),
|
|
@@ -580,6 +905,36 @@ def analyse_phases(runs):
|
|
|
580
905
|
"cost_usd_by_harness": OrderedDict(
|
|
581
906
|
(h, {"usd": round(v, 6), "records": cost_n[h]})
|
|
582
907
|
for h, v in sorted(cost_by_harness.items())),
|
|
908
|
+
# …and per billing mode, which is the separation that decides whether a number
|
|
909
|
+
# is money at all (SCHEMA.md §2.5b).
|
|
910
|
+
"billing_modes": OrderedDict(sorted(mode_n.items())),
|
|
911
|
+
"cost_usd_by_billing_mode": OrderedDict(
|
|
912
|
+
(mode, round(v, 6)) for mode, v in sorted(cost_by_mode.items())),
|
|
913
|
+
"money_usd": round(cost_by_mode.get("metered", 0.0), 6),
|
|
914
|
+
"money_records": mode_n.get("metered", 0),
|
|
915
|
+
# A window that mixed a metered model with a subscription one really was billed, but
|
|
916
|
+
# the bill cannot be separated from the part that was already paid for. Real money,
|
|
917
|
+
# reported on its own line rather than folded into the headline or dropped.
|
|
918
|
+
"money_mixed_usd": round(cost_by_mode.get("mixed", 0.0), 6),
|
|
919
|
+
"money_mixed_records": mode_n.get("mixed", 0),
|
|
920
|
+
"spend_by_model": OrderedDict(
|
|
921
|
+
(m, {"usd": round(v, 6), "records": model_usd_n[m]})
|
|
922
|
+
for m, v in sorted(model_usd.items(), key=lambda kv: (-kv[1], kv[0]))),
|
|
923
|
+
"spend_unattributed": {"usd": round(usd_unattributed, 6), "records": usd_unattributed_n},
|
|
924
|
+
# LIST PRICE — what these tokens would cost bought at the published rate. It is the
|
|
925
|
+
# only figure that compares a subscription phase with a metered one, and it is a
|
|
926
|
+
# price list applied to a measurement, never a bill. Reported apart from `money_usd`
|
|
927
|
+
# for exactly that reason, with the unpriced count beside it.
|
|
928
|
+
"list_usd": round(sum(list_of.values()), 6),
|
|
929
|
+
"list_usd_records": len(list_of),
|
|
930
|
+
"list_usd_unpriced_n": len(priced) - len(list_of),
|
|
931
|
+
# A monthly plan reports dollars too, but they are ALLOWANCE CONSUMED against a
|
|
932
|
+
# per-model limit, not an invoice — and they are the numbers that produce the limit
|
|
933
|
+
# the fallback chain exists for, so they are worth their own line.
|
|
934
|
+
"plan_allowance_usd": round(cost_by_mode.get("plan", 0.0), 6),
|
|
935
|
+
"plan_allowance_records": mode_n.get("plan", 0),
|
|
936
|
+
"plan_allowance_by_model": OrderedDict(
|
|
937
|
+
(m, round(v, 6)) for m, v in sorted(plan_by_model.items(), key=lambda kv: -kv[1])),
|
|
583
938
|
}
|
|
584
939
|
|
|
585
940
|
return {
|
|
@@ -587,6 +942,12 @@ def analyse_phases(runs):
|
|
|
587
942
|
"scope_coverage": OrderedDict(sorted(scope_cov.items())),
|
|
588
943
|
"tokens_out_total": grand_out,
|
|
589
944
|
"duration_s_total": grand_dur,
|
|
945
|
+
# What the duration figures are actually built on. A total is only honest when the
|
|
946
|
+
# reader is told what it left out: TechieBlog's 46 run records yield 32 usable ones.
|
|
947
|
+
"duration_measured_n": len([r for r in live if _dur(r) is not None]),
|
|
948
|
+
"duration_impossible_n": _dur_quality(live)[0],
|
|
949
|
+
"duration_absent_n": _dur_quality(live)[1],
|
|
950
|
+
"duration_recomputed_n": _dur_quality(live)[2],
|
|
590
951
|
"phases": phases,
|
|
591
952
|
"note": ("Per-phase effort over LIVE run records, aggregated by `cmd`. Token "
|
|
592
953
|
"figures exclude runs whose window could not be computed; fan-out "
|
|
@@ -879,10 +1240,15 @@ def analyse(repos):
|
|
|
879
1240
|
per_repo = []
|
|
880
1241
|
commit_dupes = 0
|
|
881
1242
|
session_dupes = 0
|
|
1243
|
+
voided_runs = 0
|
|
1244
|
+
void_reasons = []
|
|
1245
|
+
void_orphans = 0
|
|
882
1246
|
for repo in repos:
|
|
883
1247
|
g = read_stream(repo, "gates")
|
|
884
|
-
r = read_stream(repo, "runs")
|
|
885
|
-
|
|
1248
|
+
r, vn, vr, vo = apply_voids(read_stream(repo, "runs"), app_name(repo))
|
|
1249
|
+
voided_runs += vn; void_reasons += vr; void_orphans += vo
|
|
1250
|
+
repo_misses = read_stream(repo, "misses")
|
|
1251
|
+
misses += repo_misses
|
|
886
1252
|
# Per repo, not across them (SCHEMA.md §4): the OpenCode plugin
|
|
887
1253
|
# appends a cumulative snapshot at every root-session idle.
|
|
888
1254
|
s, sd = dedupe_sessions(read_stream(repo, "sessions"))
|
|
@@ -890,6 +1256,11 @@ def analyse(repos):
|
|
|
890
1256
|
# Per repo, not across them: two repos may legitimately share a short sha.
|
|
891
1257
|
c, d = dedupe_commits(read_stream(repo, "commits"))
|
|
892
1258
|
commit_dupes += d
|
|
1259
|
+
# Which repo each record came from, so the report can price it with THAT project's rate
|
|
1260
|
+
# card and routing. Kept beside the records rather than written into them: nothing here
|
|
1261
|
+
# may add a field a record did not have.
|
|
1262
|
+
for _x in r + repo_misses:
|
|
1263
|
+
REPO_OF[id(_x)] = repo
|
|
893
1264
|
gates += g; runs += r; sessions += s; commits += c
|
|
894
1265
|
per_repo.append({"repo": repo, "app": app_name(repo),
|
|
895
1266
|
"project_type": project_type(repo)[0],
|
|
@@ -910,39 +1281,73 @@ def analyse(repos):
|
|
|
910
1281
|
"commit_hook": has_commit_hook(repo)})
|
|
911
1282
|
|
|
912
1283
|
def seg(records):
|
|
1284
|
+
# An `FR` verdict grades a framework requirement line, not an application's
|
|
1285
|
+
# screen: it is written by a runner over a document the framework controls, so
|
|
1286
|
+
# it passes far more often than a REQ verified against a running app. Pooled
|
|
1287
|
+
# into an application segment it raises that segment's first-pass rate and
|
|
1288
|
+
# dilutes its escape rate, with nothing on the output to show it happened.
|
|
1289
|
+
# SCHEMA.md §3 states the rule -- "it never pools with the others" -- and this
|
|
1290
|
+
# is where it is applied. Tested by tests/regression/run.sh tf_020.
|
|
913
1291
|
d = defaultdict(list)
|
|
914
1292
|
for r in records:
|
|
915
|
-
|
|
1293
|
+
if r.get("req_class") == "FR":
|
|
1294
|
+
key = "framework-requirement"
|
|
1295
|
+
else:
|
|
1296
|
+
key = "unclassified" if r.get("project_type_inferred") else r.get("project_type", "app")
|
|
916
1297
|
d[key].append(r)
|
|
917
1298
|
return d
|
|
918
1299
|
|
|
919
1300
|
live = [g for g in gates if not g.get("backfilled")]
|
|
920
1301
|
back = [g for g in gates if g.get("backfilled")]
|
|
921
1302
|
|
|
1303
|
+
# `attempt` is DEFINED by §3.1 as 1 + the number of prior live gate records for the same
|
|
1304
|
+
# requirement in the same app — a count over this very stream, not a judgement. A record
|
|
1305
|
+
# that omits it was therefore being read as "no attempt", which drops it out of the
|
|
1306
|
+
# first-pass rate and reports 0% for a set of records that all passed on their first and
|
|
1307
|
+
# only verdict (found 2026-09-07 on the framework's own requirement lines). Derived here,
|
|
1308
|
+
# in stream order, for records that lack it. A record that carries one is never touched.
|
|
1309
|
+
_seen = {}
|
|
1310
|
+
for g in sorted(live, key=lambda r: (r.get("ts") or "", r.get("run_id") or "")):
|
|
1311
|
+
key = (g.get("app"), g.get("req_id"))
|
|
1312
|
+
_seen[key] = _seen.get(key, 0) + 1
|
|
1313
|
+
if g.get("attempt") is None:
|
|
1314
|
+
g["attempt"] = _seen[key]
|
|
1315
|
+
g["attempt_derived"] = True
|
|
1316
|
+
|
|
922
1317
|
# REQs with ANY backfilled record are excluded from the live first-pass rate:
|
|
923
1318
|
# their live `attempt` numbering restarts at 1 (SCHEMA.md §3.1).
|
|
924
|
-
|
|
1319
|
+
# A requirement is keyed by (app, req_id): REQ-UI-001 exists in every project, so a rollup that
|
|
1320
|
+
# keyed on req_id alone counted TfLens's and TechieBlog's as one requirement and its first-pass
|
|
1321
|
+
# rate was wrong (MISS-TechieFlow-20260907-03, Session 5).
|
|
1322
|
+
def rk(r):
|
|
1323
|
+
return (r.get("app"), r.get("req_id"))
|
|
1324
|
+
|
|
1325
|
+
tainted = {rk(g) for g in back}
|
|
925
1326
|
|
|
926
|
-
out = {"per_repo": per_repo, "tainted_reqs": sorted(x for x in tainted if x),
|
|
1327
|
+
out = {"per_repo": per_repo, "tainted_reqs": sorted("%s:%s" % x for x in tainted if x[1]),
|
|
927
1328
|
"live": {}, "backfilled": {}, "pooled": {},
|
|
1329
|
+
# what a `run-void` took out of every figure above, and why (SCHEMA.md §2.7)
|
|
1330
|
+
"runs_voided_n": voided_runs,
|
|
1331
|
+
"runs_voided": void_reasons,
|
|
1332
|
+
"run_voids_orphaned_n": void_orphans,
|
|
928
1333
|
"misses": analyse_misses(misses),
|
|
929
1334
|
"phases": analyse_phases(runs)}
|
|
930
1335
|
|
|
931
1336
|
for label, bucket in (("live", live), ("backfilled", back)):
|
|
932
1337
|
for ptype, recs in sorted(seg(bucket).items()):
|
|
933
|
-
eligible = [r for r in recs if label == "backfilled" or r
|
|
934
|
-
reqs = {r
|
|
935
|
-
first_pass = {r
|
|
1338
|
+
eligible = [r for r in recs if label == "backfilled" or rk(r) not in tainted]
|
|
1339
|
+
reqs = {rk(r) for r in eligible}
|
|
1340
|
+
first_pass = {rk(r) for r in eligible
|
|
936
1341
|
if r.get("attempt") == 1 and r.get("verdict") == "Verified"}
|
|
937
1342
|
failures = [r for r in recs if r.get("verdict") not in ("Verified", "Done (pre-existing)")]
|
|
938
1343
|
dist = Counter(r.get("gate") or "unattributed" for r in failures)
|
|
939
|
-
escaped_reqs = {r
|
|
940
|
-
failed_reqs = {r
|
|
1344
|
+
escaped_reqs = {rk(r) for r in recs if r.get("gate") == "escaped"}
|
|
1345
|
+
failed_reqs = {rk(r) for r in failures}
|
|
941
1346
|
out[label][ptype] = {
|
|
942
1347
|
"records": len(recs),
|
|
943
1348
|
"reqs_scored": len(reqs),
|
|
944
1349
|
"reqs_excluded_backfill_taint":
|
|
945
|
-
len({r
|
|
1350
|
+
len({rk(r) for r in recs if rk(r) in tainted})
|
|
946
1351
|
if label == "live" else 0,
|
|
947
1352
|
"first_pass_n": len(first_pass),
|
|
948
1353
|
"first_pass_rate": pct(len(first_pass), len(reqs)) if len(reqs) >= MIN_N else "insufficient data (n=%d)" % len(reqs),
|
|
@@ -963,10 +1368,12 @@ def analyse(repos):
|
|
|
963
1368
|
# ---- poolable metrics: runs, cadence, tokens. Exempt from both separations.
|
|
964
1369
|
build_runs = [r for r in runs if r.get("cmd") == "build-phase"]
|
|
965
1370
|
fix_runs = [r for r in runs if r.get("mode") == "fix"]
|
|
966
|
-
throughput = [float(r["reqs_count"]) / r
|
|
967
|
-
for r in runs if r
|
|
1371
|
+
throughput = [float(r["reqs_count"]) / _dur(r)
|
|
1372
|
+
for r in runs if _dur(r) is not None and r.get("reqs_count")]
|
|
968
1373
|
batch = [r["reqs_count"] for r in build_runs if r.get("reqs_count") is not None]
|
|
969
1374
|
verified_transitions = sum(1 for g in gates if g.get("verdict") == "Verified")
|
|
1375
|
+
pooled_list = [p for p in (list_price(REPO_OF.get(id(r), ""), r) for r in runs)
|
|
1376
|
+
if p is not None]
|
|
970
1377
|
tok = sum((s.get("input_tokens") or 0) + (s.get("output_tokens") or 0) for s in sessions)
|
|
971
1378
|
days = {c.get("ts", "")[:10] for c in commits if c.get("ts")}
|
|
972
1379
|
out["pooled"] = {
|
|
@@ -980,7 +1387,16 @@ def analyse(repos):
|
|
|
980
1387
|
"tokens_total": tok,
|
|
981
1388
|
"tokens_per_verified_req": round(float(tok) / verified_transitions, 1)
|
|
982
1389
|
if verified_transitions >= MIN_N and tok else None,
|
|
983
|
-
"cost_usd": None, # never estimated — see SCHEMA.md §4
|
|
1390
|
+
"cost_usd": None, # what was BILLED is never estimated — see SCHEMA.md §4
|
|
1391
|
+
# What the same work costs at list price. A price, not a bill (SCHEMA.md §2.5b), and the
|
|
1392
|
+
# only money-shaped headline that works on a subscription — which is most of this
|
|
1393
|
+
# framework's runs. Summed over the runs that carry it, with that count published, so a
|
|
1394
|
+
# figure built on half the runs cannot be read as if it covered all of them.
|
|
1395
|
+
"list_usd_total": round(sum(pooled_list), 4),
|
|
1396
|
+
"list_usd_records": len(pooled_list),
|
|
1397
|
+
"list_usd_per_verified_req": (
|
|
1398
|
+
round(sum(pooled_list) / verified_transitions, 4)
|
|
1399
|
+
if verified_transitions >= MIN_N and len(pooled_list) >= MIN_N else None),
|
|
984
1400
|
"commits": len(commits),
|
|
985
1401
|
"commit_duplicates_collapsed": commit_dupes,
|
|
986
1402
|
"session_duplicates_collapsed": session_dupes,
|
|
@@ -1067,6 +1483,20 @@ def print_report(a, repos):
|
|
|
1067
1483
|
print(" across project_type or across live/backfilled would produce a")
|
|
1068
1484
|
print(" number that cannot be defended. See SCHEMA.md §6.")
|
|
1069
1485
|
print("")
|
|
1486
|
+
if a.get("runs_voided_n"):
|
|
1487
|
+
print(" %d run record(s) VOIDED — named by a run-void record as wrong, so they are"
|
|
1488
|
+
% a["runs_voided_n"])
|
|
1489
|
+
print(" in no figure above. Both records stay on the stream; nothing was deleted:")
|
|
1490
|
+
for line in a.get("runs_voided", [])[:6]:
|
|
1491
|
+
print(" " + line)
|
|
1492
|
+
if len(a.get("runs_voided", [])) > 6:
|
|
1493
|
+
print(" ... and %d more" % (len(a["runs_voided"]) - 6))
|
|
1494
|
+
print("")
|
|
1495
|
+
if a.get("run_voids_orphaned_n"):
|
|
1496
|
+
print(" %d run-void record(s) name a run that is not on this stream — the void was"
|
|
1497
|
+
% a["run_voids_orphaned_n"])
|
|
1498
|
+
print(" written against a record that never arrived. Nothing was excluded for them.")
|
|
1499
|
+
print("")
|
|
1070
1500
|
if a["tainted_reqs"]:
|
|
1071
1501
|
print(" %d REQ(s) excluded from the LIVE first-pass rate because they carry"
|
|
1072
1502
|
% len(a["tainted_reqs"]))
|
|
@@ -1087,8 +1517,14 @@ def print_report(a, repos):
|
|
|
1087
1517
|
print(" REQ throughput : %s REQs/hour (median across runs)" %
|
|
1088
1518
|
(p["throughput_median_reqs_per_hour"] if p["throughput_median_reqs_per_hour"] is not None else "insufficient data"))
|
|
1089
1519
|
print(" sessions / tokens : %d sessions, %s total tokens" % (p["sessions"], "{:,}".format(p["tokens_total"])))
|
|
1090
|
-
print(" tokens per Verified : %s (
|
|
1520
|
+
print(" tokens per Verified : %s (what you were BILLED is never estimated — SCHEMA.md §4)" %
|
|
1091
1521
|
(p["tokens_per_verified_req"] if p["tokens_per_verified_req"] is not None else "insufficient data"))
|
|
1522
|
+
if p.get("list_usd_records"):
|
|
1523
|
+
print(" list price : $%s over %d priced run(s) — the published rate applied to"
|
|
1524
|
+
% (p["list_usd_total"], p["list_usd_records"]))
|
|
1525
|
+
print(" the tokens actually measured. A price, not a bill.")
|
|
1526
|
+
if p.get("list_usd_per_verified_req") is not None:
|
|
1527
|
+
print(" list $ per Verified : $%s" % p["list_usd_per_verified_req"])
|
|
1092
1528
|
print(" commit cadence : %s commits/active day (%d commits over %d days)" %
|
|
1093
1529
|
(p["commits_per_active_day"] if p["commits_per_active_day"] is not None else "—",
|
|
1094
1530
|
p["commits"], p["active_days"]))
|
|
@@ -1137,6 +1573,15 @@ def print_phases(p, W):
|
|
|
1137
1573
|
print("-" * W)
|
|
1138
1574
|
print(" %d live run record(s). Token-window coverage: %s"
|
|
1139
1575
|
% (p["runs_live"], " ".join("%s=%d" % kv for kv in p["scope_coverage"].items())))
|
|
1576
|
+
if any(p.get(k) for k in ("duration_impossible_n", "duration_absent_n", "duration_recomputed_n")):
|
|
1577
|
+
print(" duration: %d of %d record(s) usable"
|
|
1578
|
+
% (p.get("duration_measured_n", 0), p["runs_live"])
|
|
1579
|
+
+ (", %d impossible (start after end, discarded)" % p["duration_impossible_n"]
|
|
1580
|
+
if p.get("duration_impossible_n") else "")
|
|
1581
|
+
+ (", %d record no elapsed time" % p["duration_absent_n"]
|
|
1582
|
+
if p.get("duration_absent_n") else "")
|
|
1583
|
+
+ (", %d taken from the timestamps over a stored figure that disagreed"
|
|
1584
|
+
% p["duration_recomputed_n"] if p.get("duration_recomputed_n") else ""))
|
|
1140
1585
|
print(" A window is only as good as its scope: 'tree' saw the subagents, 'main'")
|
|
1141
1586
|
print(" did not look, 'none'/absent measured nothing at all and is excluded from")
|
|
1142
1587
|
print(" every token figure below rather than averaged in as a zero.")
|
|
@@ -1154,8 +1599,9 @@ def print_phases(p, W):
|
|
|
1154
1599
|
for cmd, m in p["phases"].items():
|
|
1155
1600
|
print(" [%s] %d run(s)" % (cmd, m["runs"]))
|
|
1156
1601
|
d = m["duration_s"]
|
|
1157
|
-
print(" wall clock : total %s · median %s · max %s (n=%d timed)"
|
|
1158
|
-
% (_hms(d["total"]), _hms(d["median"]), _hms(d["max"]), d["n"]
|
|
1602
|
+
print(" wall clock : total %s · median %s · max %s (n=%d timed%s)"
|
|
1603
|
+
% (_hms(d["total"]), _hms(d["median"]), _hms(d["max"]), d["n"],
|
|
1604
|
+
", %d derived from the record's own timestamps" % d["derived_n"] if d.get("derived_n") else ""))
|
|
1159
1605
|
t = m["tokens"]
|
|
1160
1606
|
print(" tokens : out %s · in %s · cache-read %s · cache-write %s"
|
|
1161
1607
|
% (_k(t["out"]), _k(t["in"]), _k(t["cache_read"]), _k(t["cache_write"])))
|
|
@@ -1216,8 +1662,65 @@ def print_phases(p, W):
|
|
|
1216
1662
|
for h, c in m["cost_usd_by_harness"].items():
|
|
1217
1663
|
print(" cost (%s) : $%s over %d record(s) — MEASURED, never pooled "
|
|
1218
1664
|
"across harness" % (h, c["usd"], c["records"]))
|
|
1219
|
-
|
|
1220
|
-
|
|
1665
|
+
bmodes = m.get("billing_modes") or {}
|
|
1666
|
+
if bmodes:
|
|
1667
|
+
print(" paid for by : " +
|
|
1668
|
+
" · ".join("%s %d" % (k, v) for k, v in bmodes.items()))
|
|
1669
|
+
if m.get("list_usd_records"):
|
|
1670
|
+
print(" list price : $%s over %d run(s) — the published rate applied to"
|
|
1671
|
+
% (m["list_usd"], m["list_usd_records"]))
|
|
1672
|
+
print(" the tokens on the record, worked out HERE and never")
|
|
1673
|
+
print(" stored. A price, not a bill: it is what makes a")
|
|
1674
|
+
print(" subscription run comparable with a metered one.")
|
|
1675
|
+
if m.get("list_usd_unpriced_n"):
|
|
1676
|
+
print(" %d run(s) had no rate for their model and are"
|
|
1677
|
+
% m["list_usd_unpriced_n"])
|
|
1678
|
+
print(" outside it — never priced at zero.")
|
|
1679
|
+
if m.get("money_records"):
|
|
1680
|
+
print(" money billed : $%s over %d metered run(s). Only a metered run is"
|
|
1681
|
+
% (m["money_usd"], m["money_records"]))
|
|
1682
|
+
print(" money billed; a subscription's zero is a flat fee")
|
|
1683
|
+
print(" already paid, a local model has no bill (§2.5b).")
|
|
1684
|
+
if m.get("plan_allowance_records"):
|
|
1685
|
+
print(" plan allowance : $%s used over %d run(s) on a monthly plan. This is"
|
|
1686
|
+
% (m["plan_allowance_usd"], m["plan_allowance_records"]))
|
|
1687
|
+
print(" the provider's own meter against a PER-MODEL")
|
|
1688
|
+
print(" allowance, not an invoice — and it is what runs out")
|
|
1689
|
+
print(" and sends a tier to its fallback.")
|
|
1690
|
+
for mod, v in (m.get("plan_allowance_by_model") or {}).items():
|
|
1691
|
+
print(" %-30s $%s of allowance" % (mod, v))
|
|
1692
|
+
if m.get("money_mixed_usd"):
|
|
1693
|
+
print(" also billed : $%s on %d run(s) whose window mixed billing modes —"
|
|
1694
|
+
% (m["money_mixed_usd"], m["money_mixed_records"]))
|
|
1695
|
+
print(" real money, but not separable from the part a")
|
|
1696
|
+
print(" subscription had already paid for.")
|
|
1697
|
+
for mod, c in (m.get("models") or {}).items():
|
|
1698
|
+
bits = []
|
|
1699
|
+
if c.get("list_usd"):
|
|
1700
|
+
bits.append("list $%s" % c["list_usd"])
|
|
1701
|
+
sp = (m.get("spend_by_model") or {}).get(mod)
|
|
1702
|
+
if sp:
|
|
1703
|
+
bits.append("billed $%s" % sp["usd"])
|
|
1704
|
+
if bits:
|
|
1705
|
+
print(" %-30s %s" % (mod, " · ".join(bits)))
|
|
1706
|
+
un = m.get("spend_unattributed") or {}
|
|
1707
|
+
if un.get("records"):
|
|
1708
|
+
print(" %-30s billed $%-8s over %d run(s) — the window ran several"
|
|
1709
|
+
% ("(not attributed)", un["usd"], un["records"]))
|
|
1710
|
+
print(" models, and one window's dollars are not split by token share.")
|
|
1711
|
+
# `bm` used to be assigned inside `if m["reqs_touched_total"] or m["files_written_total"]:`
|
|
1712
|
+
# and read outside it, so --phases crashed with UnboundLocalError on any phase that
|
|
1713
|
+
# touched no REQ and wrote no file — which is every metrics-report and every render.
|
|
1714
|
+
# MISS-TechieFlow-20260910-03.
|
|
1715
|
+
bm = m.get("by_mode") or {}
|
|
1716
|
+
if len(bm) > 1:
|
|
1717
|
+
print(" by mode : (in the order they ran)")
|
|
1718
|
+
for mode, v in bm.items():
|
|
1719
|
+
unm = (" [%d unmeasured]" % v["tokens_unmeasured_n"]) if v["tokens_unmeasured_n"] else ""
|
|
1720
|
+
print(" %-22s %2d run(s) %8s %8s out %3d file(s)%s"
|
|
1721
|
+
% (mode, v["runs"], _hms(v["duration_s"]), _k(v["tokens_out"]),
|
|
1722
|
+
v["files_written"], unm))
|
|
1723
|
+
print(" work : %d REQ touch(es) · %d file(s) written"
|
|
1221
1724
|
% (m["reqs_touched_total"], m["files_written_total"]))
|
|
1222
1725
|
print("")
|
|
1223
1726
|
|
|
@@ -1225,7 +1728,7 @@ def print_phases(p, W):
|
|
|
1225
1728
|
def print_misses(m, W):
|
|
1226
1729
|
"""The §5.5 block. Two exclusions are printed with the figures they bound —
|
|
1227
1730
|
an exclusion the reader cannot see is indistinguishable from a bug."""
|
|
1228
|
-
if not m["misses_total"] and not m["miss_fixes_total"]:
|
|
1731
|
+
if not m["misses_total"] and not m["miss_fixes_total"] and not m.get("reviews_n"):
|
|
1229
1732
|
return
|
|
1230
1733
|
print("")
|
|
1231
1734
|
print("-" * W)
|
|
@@ -1269,9 +1772,29 @@ def print_misses(m, W):
|
|
|
1269
1772
|
% m["escapes_missing_why"])
|
|
1270
1773
|
print(" nothing recorded why. That is the most valuable record in the stream.")
|
|
1271
1774
|
print(" Complete it: tf-emit.sh --amend <miss_id> why_missed <value> (§5.5.7)")
|
|
1775
|
+
# whose gap — the four-question sort (§5.5.10). Its own denominator, like why_missed.
|
|
1776
|
+
if m.get("sort"):
|
|
1777
|
+
print(" whose gap : (%d of %d misses sorted)" % (m["sort_n"], m["sort_eligible"]))
|
|
1778
|
+
for k, n in m["sort"].items():
|
|
1779
|
+
print(" %-12s %4d %-5s %s" % (k, n, pct(n, m["sort_n"]), SORT_WORDS.get(k, "")))
|
|
1780
|
+
if m.get("sort_predates_field"):
|
|
1781
|
+
print(" %d miss(es) predate the sort field (added %s); sort them with"
|
|
1782
|
+
% (m["sort_predates_field"], FIELD_SINCE["sort"]))
|
|
1783
|
+
print(" tf-emit.sh --amend <miss_id> sort <spec|unsaid|weak-check|ignored>")
|
|
1272
1784
|
if m["amendments_applied"]:
|
|
1273
1785
|
print(" amendments folded : %d field(s) completed by miss-amend records (§5.5.7)"
|
|
1274
1786
|
% m["amendments_applied"])
|
|
1787
|
+
# owner reviews (§5.5.9): corrections per reviewed phase, and the two costs
|
|
1788
|
+
if m.get("reviews_n"):
|
|
1789
|
+
print(" owner reviews : %d review(s), %d correction(s) given — "
|
|
1790
|
+
% (m["reviews_n"], m["review_corrections"])
|
|
1791
|
+
+ " ".join("%s=%d" % kv for kv in m["reviews_by_phase"].items()))
|
|
1792
|
+
print(" tokens to produce the reviewed output : %s (n=%d)"
|
|
1793
|
+
% (m["review_tokens_produce_mean"] if m["review_tokens_produce_mean"] is not None
|
|
1794
|
+
else "insufficient data", m["review_tokens_produce_n"]))
|
|
1795
|
+
print(" tokens to apply the corrections : %s (n=%d)"
|
|
1796
|
+
% (m["review_tokens_correct_mean"] if m["review_tokens_correct_mean"] is not None
|
|
1797
|
+
else "insufficient data", m["review_tokens_correct_n"]))
|
|
1275
1798
|
if m["orphan_amends"]:
|
|
1276
1799
|
print(" ⚠ %d miss-amend record(s) name no known miss, or a field outside the"
|
|
1277
1800
|
% m["orphan_amends"])
|
|
@@ -1315,9 +1838,18 @@ def print_misses(m, W):
|
|
|
1315
1838
|
else:
|
|
1316
1839
|
print(" USD per miss : no measured dollars (%d priced records)"
|
|
1317
1840
|
% m["cost_usd_records"])
|
|
1318
|
-
print(" Claude Code
|
|
1841
|
+
print(" Claude Code carries cost_usd:null permanently and is NEVER")
|
|
1319
1842
|
print(" priced from a rate card here (SCHEMA.md §4). Real dollars come from")
|
|
1320
1843
|
print(" OpenCode runs; token counts are the honest figure everywhere else.")
|
|
1844
|
+
if m.get("cost_list_usd_per_miss") is not None:
|
|
1845
|
+
print(" list price per miss : $%s (%d record(s)) — the repair's tokens at the"
|
|
1846
|
+
% (m["cost_list_usd_per_miss"], m["cost_list_usd_records"]))
|
|
1847
|
+
print(" published rate. Works on every harness, and is a price, not a bill.")
|
|
1848
|
+
if m.get("cost_usd_excluded_not_money_n"):
|
|
1849
|
+
print(" %d fix record(s) measured a cost that is NOT money — a subscription's"
|
|
1850
|
+
% m["cost_usd_excluded_not_money_n"])
|
|
1851
|
+
print(" zero or a local model's absent bill — and are outside that figure")
|
|
1852
|
+
print(" rather than averaged in as free repairs (SCHEMA.md §2.5b).")
|
|
1321
1853
|
print(" shared (apportioned): %d fix records — equal division, NOT a measurement"
|
|
1322
1854
|
% m["cost_shared_n"])
|
|
1323
1855
|
print(" tokens out per miss : %s (n=%d priced)" %
|