@techierathore/techieflow 1.0.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.
- package/.claude/commands/TechieFlow/agents/analyst.md +97 -0
- package/.claude/commands/TechieFlow/agents/architect.md +84 -0
- package/.claude/commands/TechieFlow/agents/flow-master.md +217 -0
- package/.claude/commands/TechieFlow/agents/verifier.md +72 -0
- package/.claude/commands/TechieFlow/tasks/_metrics-emit-gate.md +138 -0
- package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +98 -0
- package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +78 -0
- package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +77 -0
- package/.claude/commands/TechieFlow/tasks/advanced-elicitation.md +117 -0
- package/.claude/commands/TechieFlow/tasks/amend-docs.md +149 -0
- package/.claude/commands/TechieFlow/tasks/author-brd.md +79 -0
- package/.claude/commands/TechieFlow/tasks/build-phase.md +231 -0
- package/.claude/commands/TechieFlow/tasks/create-deep-research-prompt.md +278 -0
- package/.claude/commands/TechieFlow/tasks/create-doc.md +101 -0
- package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +507 -0
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +224 -0
- package/.claude/commands/TechieFlow/tasks/devguide.md +224 -0
- package/.claude/commands/TechieFlow/tasks/document-project.md +343 -0
- package/.claude/commands/TechieFlow/tasks/execute-checklist.md +86 -0
- package/.claude/commands/TechieFlow/tasks/facilitate-brainstorming-session.md +136 -0
- package/.claude/commands/TechieFlow/tasks/fix-issues.md +134 -0
- package/.claude/commands/TechieFlow/tasks/generate-html.md +115 -0
- package/.claude/commands/TechieFlow/tasks/handoff-phase.md +126 -0
- package/.claude/commands/TechieFlow/tasks/index-docs.md +173 -0
- package/.claude/commands/TechieFlow/tasks/kb-mode-interaction.md +75 -0
- package/.claude/commands/TechieFlow/tasks/log-miss.md +160 -0
- package/.claude/commands/TechieFlow/tasks/metrics-report.md +143 -0
- package/.claude/commands/TechieFlow/tasks/mockups.md +122 -0
- package/.claude/commands/TechieFlow/tasks/productguide.md +105 -0
- package/.claude/commands/TechieFlow/tasks/refresh-status.md +135 -0
- package/.claude/commands/TechieFlow/tasks/render-workflow-docs.md +117 -0
- package/.claude/commands/TechieFlow/tasks/shard-doc.md +185 -0
- package/.claude/commands/TechieFlow/tasks/split-brd.md +131 -0
- package/.claude/commands/TechieFlow/tasks/triage-issues.md +157 -0
- package/.claude/commands/TechieFlow/tasks/verify-phase.md +579 -0
- package/.claude/commands/generate-html.md +124 -0
- package/.codex/config.toml +46 -0
- package/.codex/hooks.json +63 -0
- package/.codex/rules/techieflow.rules +8 -0
- package/.opencode/command/generate-html.md +129 -0
- package/.opencode/plugin/techieflow.js +350 -0
- package/.tfcore/TOKEN-GUIDE.md +59 -0
- package/.tfcore/agents/analyst.md +97 -0
- package/.tfcore/agents/architect.md +84 -0
- package/.tfcore/agents/flow-master.md +217 -0
- package/.tfcore/agents/verifier.md +72 -0
- package/.tfcore/checklists/architect-checklist.md +438 -0
- package/.tfcore/core-config.yaml +47 -0
- package/.tfcore/data/brainstorming-techniques.md +36 -0
- package/.tfcore/data/elicitation-methods.md +154 -0
- package/.tfcore/data/techieflow-kb.md +807 -0
- package/.tfcore/data/technical-preferences.md +3 -0
- package/.tfcore/enhanced-ide-development-workflow.md +248 -0
- package/.tfcore/hooks/block-git.sh +301 -0
- package/.tfcore/hooks/codex-adapter.py +248 -0
- package/.tfcore/hooks/guard-artifacts.sh +101 -0
- package/.tfcore/hooks/guard-status-html.sh +102 -0
- package/.tfcore/hooks/guard-status.sh +124 -0
- package/.tfcore/hooks/guard-verify.sh +108 -0
- package/.tfcore/hooks/metrics-session.sh +129 -0
- package/.tfcore/hooks/session-pointer.sh +52 -0
- package/.tfcore/hooks/sweep-artifacts.sh +198 -0
- package/.tfcore/install-manifest.yaml +231 -0
- package/.tfcore/routing.yaml +105 -0
- package/.tfcore/tasks/_metrics-emit-gate.md +138 -0
- package/.tfcore/tasks/_smoke-test-policy.md +98 -0
- package/.tfcore/tasks/_status-update-gate.md +78 -0
- package/.tfcore/tasks/_yolo-mode.md +77 -0
- package/.tfcore/tasks/advanced-elicitation.md +117 -0
- package/.tfcore/tasks/amend-docs.md +149 -0
- package/.tfcore/tasks/author-brd.md +79 -0
- package/.tfcore/tasks/build-phase.md +231 -0
- package/.tfcore/tasks/create-deep-research-prompt.md +278 -0
- package/.tfcore/tasks/create-doc.md +101 -0
- package/.tfcore/tasks/day1-brownfield.md +507 -0
- package/.tfcore/tasks/day1-greenfield.md +224 -0
- package/.tfcore/tasks/devguide.md +224 -0
- package/.tfcore/tasks/document-project.md +343 -0
- package/.tfcore/tasks/execute-checklist.md +86 -0
- package/.tfcore/tasks/facilitate-brainstorming-session.md +136 -0
- package/.tfcore/tasks/fix-issues.md +134 -0
- package/.tfcore/tasks/generate-html.md +115 -0
- package/.tfcore/tasks/handoff-phase.md +126 -0
- package/.tfcore/tasks/index-docs.md +173 -0
- package/.tfcore/tasks/kb-mode-interaction.md +75 -0
- package/.tfcore/tasks/log-miss.md +160 -0
- package/.tfcore/tasks/metrics-report.md +143 -0
- package/.tfcore/tasks/mockups.md +122 -0
- package/.tfcore/tasks/productguide.md +105 -0
- package/.tfcore/tasks/refresh-status.md +135 -0
- package/.tfcore/tasks/render-workflow-docs.md +117 -0
- package/.tfcore/tasks/shard-doc.md +185 -0
- package/.tfcore/tasks/split-brd.md +131 -0
- package/.tfcore/tasks/triage-issues.md +157 -0
- package/.tfcore/tasks/verify-phase.md +579 -0
- package/.tfcore/telemetry/SCHEMA.md +678 -0
- package/.tfcore/telemetry/install-metrics.sh +390 -0
- package/.tfcore/telemetry/pre-commit +94 -0
- package/.tfcore/telemetry/tf-metrics.sh +1415 -0
- package/.tfcore/templates/architecture-tmpl.yaml +650 -0
- package/.tfcore/templates/brainstorming-output-tmpl.yaml +156 -0
- package/.tfcore/templates/brownfield-architecture-tmpl.yaml +476 -0
- package/.tfcore/templates/competitor-analysis-tmpl.yaml +306 -0
- package/.tfcore/templates/front-end-architecture-tmpl.yaml +218 -0
- package/.tfcore/templates/fullstack-architecture-tmpl.yaml +823 -0
- package/.tfcore/templates/market-research-tmpl.yaml +252 -0
- package/.tfcore/templates/project-brief-tmpl.yaml +221 -0
- package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +61 -0
- package/.tfcore/templates/v4custom/app-architecture-tmpl.md +127 -0
- package/.tfcore/templates/v4custom/app-brd-tmpl.md +204 -0
- package/.tfcore/templates/v4custom/app-checklist-tmpl.md +90 -0
- package/.tfcore/templates/v4custom/app-claude-md-tmpl.md +33 -0
- package/.tfcore/templates/v4custom/app-devguide-tmpl.md +167 -0
- package/.tfcore/templates/v4custom/app-editorconfig-tmpl.editorconfig +44 -0
- package/.tfcore/templates/v4custom/app-library-feedback-tmpl.md +31 -0
- package/.tfcore/templates/v4custom/app-productguide-tmpl.md +54 -0
- package/.tfcore/templates/v4custom/app-project-status-tmpl.md +98 -0
- package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +48 -0
- package/.tfcore/templates/v4custom/app-usageguide-tmpl.md +67 -0
- package/.tfcore/templates/v4custom/build-invocation-ladder.md +190 -0
- package/.tfcore/templates/v4custom/html-render-shell.md +502 -0
- package/.tfcore/templates/v4custom/metrics-report-template.md +273 -0
- package/.tfcore/templates/v4custom/opencode-operating-contract.md +56 -0
- package/.tfcore/user-guide.md +633 -0
- package/.tfcore/utils/techieflow-doc-template.md +325 -0
- package/.tfcore/utils/tf-assets.sh +310 -0
- package/.tfcore/utils/tf-codex-bind.py +196 -0
- package/.tfcore/utils/tf-codex-telemetry.py +60 -0
- package/.tfcore/utils/tf-emit.sh +1075 -0
- package/.tfcore/utils/tf-gitignore-audit.sh +281 -0
- package/.tfcore/utils/tf-goal.sh +421 -0
- package/.tfcore/utils/tf-harness.sh +160 -0
- package/.tfcore/utils/tf-mockup-parity.mjs +555 -0
- package/.tfcore/utils/tf-mockup-parity.sh +65 -0
- package/.tfcore/utils/tf-opencode-audit.py +126 -0
- package/.tfcore/utils/tf-perf.sh +241 -0
- package/.tfcore/utils/tf-render-html.py +715 -0
- package/.tfcore/utils/tf-render-html.sh +28 -0
- package/.tfcore/utils/tf-routing-bind.sh +216 -0
- package/.tfcore/utils/tf-routing.sh +247 -0
- package/.tfcore/utils/tf-yolo.sh +119 -0
- package/.tfcore/working-in-the-brownfield.md +606 -0
- package/LICENSE +201 -0
- package/README.md +1212 -0
- package/WORKFLOW.html +1986 -0
- package/docs/TechieFlow-Installation.md +299 -0
- package/opencode.jsonc +929 -0
- package/package.json +60 -0
- package/scripts/install.mjs +992 -0
- package/scripts/test-install.mjs +505 -0
- package/scripts/validate.mjs +155 -0
|
@@ -0,0 +1,1415 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# TechieFlow telemetry — owner-run reporting & backfill tool.
|
|
3
|
+
#
|
|
4
|
+
# ⚠ THIS IS THE ONLY FILE IN THE FRAMEWORK ALLOWED TO CONTAIN GIT COMMANDS.
|
|
5
|
+
#
|
|
6
|
+
# --backfill-commits OWNER-RUN ONLY. Invokes git. An agent must NEVER run it.
|
|
7
|
+
# --backfill-gates OWNER-RUN ONLY. Writes reconstructed history; only the
|
|
8
|
+
# owner decides that a repo's past gets synthesised.
|
|
9
|
+
# --report / --rollup READ-ONLY. No git, no writes. The *metrics task
|
|
10
|
+
# (.tfcore/tasks/metrics-report.md) invokes these, and
|
|
11
|
+
# should — the provenance separations of SCHEMA.md §6 are
|
|
12
|
+
# enforced HERE, in code, so no agent has to be trusted
|
|
13
|
+
# to resist producing the combined figure by hand.
|
|
14
|
+
#
|
|
15
|
+
# USAGE
|
|
16
|
+
# tf-metrics.sh --report [<repo>] [--json]
|
|
17
|
+
# tf-metrics.sh --backfill-commits [<repo>] [--dry-run]
|
|
18
|
+
# tf-metrics.sh --backfill-gates [<repo>] [--dry-run]
|
|
19
|
+
# tf-metrics.sh --rollup <repo> [<repo> ...] [--json]
|
|
20
|
+
#
|
|
21
|
+
# PROVENANCE RULE (SCHEMA.md §6) — enforced here, not just documented:
|
|
22
|
+
# live and backfilled records NEVER pool, and app/library/docs NEVER pool, for
|
|
23
|
+
# first-pass rate, gate catch distribution, or escape rate. There is no flag to
|
|
24
|
+
# turn that off. Commit-derived metrics are exempt (git log is a real log).
|
|
25
|
+
set -uo pipefail
|
|
26
|
+
|
|
27
|
+
command -v python3 >/dev/null 2>&1 || { echo "tf-metrics: python3 is required." >&2; exit 1; }
|
|
28
|
+
|
|
29
|
+
exec python3 - "$@" <<'PYEOF'
|
|
30
|
+
import glob
|
|
31
|
+
import json
|
|
32
|
+
import os
|
|
33
|
+
import re
|
|
34
|
+
import subprocess
|
|
35
|
+
import sys
|
|
36
|
+
from collections import Counter, defaultdict, OrderedDict
|
|
37
|
+
|
|
38
|
+
STREAMS = ("runs", "gates", "sessions", "commits", "misses")
|
|
39
|
+
VERDICTS = ("Verified", "Needs re-verify", "FAIL", "Blocked", "Implemented", "Done (pre-existing)")
|
|
40
|
+
GATE_ORDER = ("build", "acceptance", "render", "assets", "visual", "mockup-parity",
|
|
41
|
+
"perf", "standards", "escaped")
|
|
42
|
+
|
|
43
|
+
# Gates that entered the enum after gates.jsonl started collecting. Their share of a raw
|
|
44
|
+
# distribution is structurally understated — records written before the date never had the
|
|
45
|
+
# chance to record them, and (for `perf`) most records after it still do not run the gate.
|
|
46
|
+
# The honest denominator is "records that actually ran it", i.e. gates_run membership.
|
|
47
|
+
# SCHEMA.md §3.5. Keep this table in sync when a gate is added.
|
|
48
|
+
LATE_GATES = {"perf": "2026-08-10", "assets": "2026-08-31", "mockup-parity": "2026-08-31"}
|
|
49
|
+
|
|
50
|
+
# The same hazard as LATE_GATES, one stream over: an OPTIONAL FIELD added after a
|
|
51
|
+
# stream started collecting. A record written before the date had no such field to
|
|
52
|
+
# fill, so counting it as "not assessed" understates the practice being measured.
|
|
53
|
+
# It leaves that field's denominator and is reported separately. SCHEMA.md §5.5.6 /
|
|
54
|
+
# §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
|
+
|
|
57
|
+
# Fields a `miss-amend` record may complete, and their closed vocabularies.
|
|
58
|
+
# Kept identical to tf-emit.sh's `_AMENDABLE`; the emitter enforces it on write and
|
|
59
|
+
# this enforces it again on read, because a merged stream can carry records this
|
|
60
|
+
# machine's emitter never saw. SCHEMA.md §5.5.7 states the rule for extending it.
|
|
61
|
+
AMENDABLE_FIELDS = {
|
|
62
|
+
"why_missed": ("missing-checklist-item", "insufficient-verify-method",
|
|
63
|
+
"code-audit-limitation", "ambiguous-acceptance",
|
|
64
|
+
"dependency-not-declared", "instruction-ignored", "other"),
|
|
65
|
+
}
|
|
66
|
+
MIN_N = 3 # fewer supporting records than this -> "insufficient data", never a number
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------- utilities
|
|
70
|
+
def die(msg):
|
|
71
|
+
sys.stderr.write("tf-metrics: %s\n" % msg)
|
|
72
|
+
raise SystemExit(1)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def metrics_dir(repo):
|
|
76
|
+
return os.path.join(repo, "docs", "metrics")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def read_stream(repo, stream):
|
|
80
|
+
path = os.path.join(metrics_dir(repo), stream + ".jsonl")
|
|
81
|
+
out = []
|
|
82
|
+
try:
|
|
83
|
+
with open(path, encoding="utf-8") as fh:
|
|
84
|
+
for n, line in enumerate(fh, 1):
|
|
85
|
+
line = line.strip()
|
|
86
|
+
if not line:
|
|
87
|
+
continue
|
|
88
|
+
try:
|
|
89
|
+
out.append(json.loads(line))
|
|
90
|
+
except Exception:
|
|
91
|
+
sys.stderr.write(" ! %s:%d is not valid JSON — skipped\n" % (path, n))
|
|
92
|
+
except FileNotFoundError:
|
|
93
|
+
pass
|
|
94
|
+
return out
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def emit(repo, stream, records, dry_run, quiet=False):
|
|
98
|
+
"""Append via tf-emit.sh — the single append primitive. Never write JSONL directly.
|
|
99
|
+
|
|
100
|
+
The whole batch goes down ONE pipe as JSONL. The pre-commit hook reconciles
|
|
101
|
+
on every commit, so the per-record process spawn this used to do turned a
|
|
102
|
+
first-run catch-up of a few hundred commits into a visible stall inside the
|
|
103
|
+
owner's `git commit`."""
|
|
104
|
+
if not records:
|
|
105
|
+
return
|
|
106
|
+
emitter = os.path.join(repo, ".tfcore", "utils", "tf-emit.sh")
|
|
107
|
+
if dry_run:
|
|
108
|
+
for r in records[:3]:
|
|
109
|
+
print(" WOULD append: " + json.dumps(r, separators=(",", ":")))
|
|
110
|
+
if len(records) > 3:
|
|
111
|
+
print(" WOULD append: ... and %d more" % (len(records) - 3))
|
|
112
|
+
return
|
|
113
|
+
if not os.path.isfile(emitter):
|
|
114
|
+
if quiet:
|
|
115
|
+
return
|
|
116
|
+
die("no %s — run update-framework.sh on this repo first" % emitter)
|
|
117
|
+
env = dict(os.environ, TF_METRICS_ROOT=os.path.abspath(repo))
|
|
118
|
+
payload = "".join(json.dumps(r, separators=(",", ":")) + "\n" for r in records)
|
|
119
|
+
p = subprocess.Popen(["bash", emitter, stream], stdin=subprocess.PIPE, env=env)
|
|
120
|
+
p.communicate(payload.encode("utf-8"))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def dedupe_commits(records):
|
|
124
|
+
"""Collapse commit records that share a sha, keeping the first.
|
|
125
|
+
|
|
126
|
+
Duplicates are EXPECTED, not corruption. A commit made on machine A is
|
|
127
|
+
recorded there by the hook; if that record has not been pushed when machine B
|
|
128
|
+
pulls the commit itself, B's reconcile reconstructs the same record from the
|
|
129
|
+
log. Both are then real lines in two branches of the same file, and
|
|
130
|
+
`merge=union` — which exists so that no record is ever lost to a hand-resolved
|
|
131
|
+
conflict — keeps both. De-duplicating on read is the other half of that trade:
|
|
132
|
+
union merge guarantees nothing is dropped, this guarantees nothing is counted
|
|
133
|
+
twice. `sha` is the natural key; a commit happens once.
|
|
134
|
+
|
|
135
|
+
Scope note (corrected 2026-08-27, TfLens TF-001): this reasoning is about
|
|
136
|
+
UNION-MERGE duplicates specifically. runs/gates record events that happen on
|
|
137
|
+
ONE machine and are never independently reconstructible, so union merge
|
|
138
|
+
cannot manufacture a second copy of them. Sessions are NOT in that set —
|
|
139
|
+
they have their own, unrelated duplication source (the OpenCode plugin's
|
|
140
|
+
cumulative snapshots, SCHEMA.md §4) and are handled by dedupe_sessions
|
|
141
|
+
below. The original wording reasoned about one source of duplication and
|
|
142
|
+
concluded there were none at all, which silently overstated every session
|
|
143
|
+
and token figure."""
|
|
144
|
+
seen, out, dupes = set(), [], 0
|
|
145
|
+
for r in records:
|
|
146
|
+
sha = r.get("sha")
|
|
147
|
+
if sha is None:
|
|
148
|
+
out.append(r)
|
|
149
|
+
continue
|
|
150
|
+
if sha in seen:
|
|
151
|
+
dupes += 1
|
|
152
|
+
continue
|
|
153
|
+
seen.add(sha)
|
|
154
|
+
out.append(r)
|
|
155
|
+
return out, dupes
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def dedupe_sessions(records):
|
|
159
|
+
"""Collapse session records that share a session_id, keeping the completest.
|
|
160
|
+
|
|
161
|
+
Duplicates are EXPECTED here too, but for a different reason than commits.
|
|
162
|
+
The OpenCode plugin appends a CUMULATIVE snapshot at every root-session idle
|
|
163
|
+
(SCHEMA.md §4 — a TUI session idles after each turn), so one session
|
|
164
|
+
legitimately produces several records and only the largest is complete. The
|
|
165
|
+
documented consumer rule is to take the record with the highest
|
|
166
|
+
output_tokens per session_id, ties broken on the latest ts.
|
|
167
|
+
|
|
168
|
+
Claude Code records stay one-per-session via SessionEnd, so they are
|
|
169
|
+
unaffected: a session_id seen once keeps its single record untouched.
|
|
170
|
+
|
|
171
|
+
Call this PER REPO, exactly as commits are — two repos may legitimately
|
|
172
|
+
carry the same session_id and collapsing across them would under-count."""
|
|
173
|
+
rank = lambda r: ((r.get("output_tokens") or 0), r.get("ts") or "")
|
|
174
|
+
best, order, dupes = {}, [], 0
|
|
175
|
+
for r in records:
|
|
176
|
+
sid = r.get("session_id")
|
|
177
|
+
if sid is None: # no natural key: pass through, as commits does
|
|
178
|
+
order.append(r)
|
|
179
|
+
continue
|
|
180
|
+
if sid in best:
|
|
181
|
+
dupes += 1
|
|
182
|
+
if rank(r) > rank(best[sid]):
|
|
183
|
+
best[sid] = r
|
|
184
|
+
else:
|
|
185
|
+
best[sid] = r
|
|
186
|
+
order.append(sid) # placeholder, resolved on the way out
|
|
187
|
+
return [x if isinstance(x, dict) else best[x] for x in order], dupes
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def analyse_misses(misses):
|
|
191
|
+
"""The §5.5 stream, with the THIRD provenance separation enforced here in code.
|
|
192
|
+
|
|
193
|
+
Two exclusions, both APPLIED AND DISPLAYED (SCHEMA.md §6):
|
|
194
|
+
|
|
195
|
+
origin_confidence != "linked" -> excluded from every per-phase / per-model /
|
|
196
|
+
per-agent miss rate. A routing decision made on guessed attributions is a
|
|
197
|
+
decision made on invented evidence, and nothing in the output would show it.
|
|
198
|
+
|
|
199
|
+
cost_attribution != "sole" -> excluded from the headline cost figures. A run
|
|
200
|
+
that fixed three misses has ONE token window; dividing it three ways is
|
|
201
|
+
arithmetic, not measurement. The apportioned total is carried separately so
|
|
202
|
+
it can be shown in a labelled column, never summed into the headline.
|
|
203
|
+
|
|
204
|
+
Raw counts and the miss-class distribution ARE poolable: a miss counts as a miss
|
|
205
|
+
whoever missed it; only its attribution is confidence-bounded."""
|
|
206
|
+
opened = [m for m in misses if m.get("kind") == "miss" and not m.get("backfilled")]
|
|
207
|
+
fixes = [m for m in misses if m.get("kind") == "miss-fix" and not m.get("backfilled")]
|
|
208
|
+
amends = [m for m in misses if m.get("kind") == "miss-amend"]
|
|
209
|
+
|
|
210
|
+
# ---- fold amendments into their parents BEFORE anything is counted (§5.5.7).
|
|
211
|
+
# An amend completes a field the miss left null; it can never overwrite one, so
|
|
212
|
+
# folding cannot change a value that was already recorded. The null-check is
|
|
213
|
+
# repeated here rather than trusted from the emitter: this reader also sees
|
|
214
|
+
# streams merged from another machine, where an amend and a later-written value
|
|
215
|
+
# could arrive in either order.
|
|
216
|
+
opened_by_id = {}
|
|
217
|
+
for m in opened:
|
|
218
|
+
if m.get("miss_id"):
|
|
219
|
+
opened_by_id.setdefault(m["miss_id"], m)
|
|
220
|
+
amended, orphan_amends = 0, 0
|
|
221
|
+
for a in sorted(amends, key=lambda r: r.get("ts") or ""):
|
|
222
|
+
parent = opened_by_id.get(a.get("miss_id"))
|
|
223
|
+
fld = a.get("field")
|
|
224
|
+
if parent is None or fld not in AMENDABLE_FIELDS:
|
|
225
|
+
orphan_amends += 1
|
|
226
|
+
continue
|
|
227
|
+
if parent.get(fld) is None and a.get("value") in AMENDABLE_FIELDS[fld]:
|
|
228
|
+
parent[fld] = a["value"]
|
|
229
|
+
amended += 1
|
|
230
|
+
|
|
231
|
+
# ---- eligibility floor for optional fields added mid-stream (SCHEMA.md §3.5,
|
|
232
|
+
# the same rule the `perf` gate established, arriving on a different stream).
|
|
233
|
+
# A record written before a field existed had ZERO chance of carrying it, so it
|
|
234
|
+
# is not evidence that the field was skipped. It leaves the denominator and is
|
|
235
|
+
# reported separately — never silently, and never backfilled with a value
|
|
236
|
+
# nobody assessed at the time.
|
|
237
|
+
def _eligible(rec, field):
|
|
238
|
+
since = FIELD_SINCE.get(field)
|
|
239
|
+
return not since or (rec.get("ts") or "") >= since
|
|
240
|
+
|
|
241
|
+
# Latest fix per miss decides open/closed; a fix naming no known miss is an orphan.
|
|
242
|
+
known = {m.get("miss_id") for m in opened}
|
|
243
|
+
latest = {}
|
|
244
|
+
orphans = 0
|
|
245
|
+
for f in fixes:
|
|
246
|
+
mid = f.get("miss_id")
|
|
247
|
+
if mid not in known:
|
|
248
|
+
orphans += 1
|
|
249
|
+
continue
|
|
250
|
+
prev = latest.get(mid)
|
|
251
|
+
if prev is None or (f.get("ts") or "") >= (prev.get("ts") or ""):
|
|
252
|
+
latest[mid] = f
|
|
253
|
+
# TWO DIFFERENT QUESTIONS, DELIBERATELY NOT THE SAME PREDICATE.
|
|
254
|
+
#
|
|
255
|
+
# "is this defect still live?" -> the COLLAPSE check in tf-emit.sh --open-miss.
|
|
256
|
+
# A wont-fix defect is still live: the next verify
|
|
257
|
+
# failure on that REQ is the SAME defect and must not
|
|
258
|
+
# open a second record. So --open-miss keeps its
|
|
259
|
+
# `verdict_after != "Verified"` predicate. Do not
|
|
260
|
+
# "fix" the two to agree — they are asking different
|
|
261
|
+
# things and agreeing would break one of them.
|
|
262
|
+
#
|
|
263
|
+
# "how much work is outstanding?" -> THIS count, which the owner reads as a backlog.
|
|
264
|
+
# `wont-fix` is a decision, not a backlog item, so it
|
|
265
|
+
# gets its own bucket. `deferred` IS outstanding —
|
|
266
|
+
# postponed work is still work — so it stays open.
|
|
267
|
+
def _verdict(m):
|
|
268
|
+
return (latest.get(m.get("miss_id")) or {}).get("verdict_after")
|
|
269
|
+
|
|
270
|
+
open_misses = [m for m in opened if _verdict(m) not in ("Verified", "wont-fix")]
|
|
271
|
+
wont_fix = [m for m in opened if _verdict(m) == "wont-fix"]
|
|
272
|
+
resolved = [m for m in opened if _verdict(m) == "Verified"]
|
|
273
|
+
|
|
274
|
+
linked = [m for m in opened if m.get("origin_confidence") == "linked"]
|
|
275
|
+
excluded = len(opened) - len(linked)
|
|
276
|
+
|
|
277
|
+
# RECOMPUTE the share per fix run before bucketing (SCHEMA §8: derived metrics
|
|
278
|
+
# are computed at report time, never trusted from storage). Two reasons this is
|
|
279
|
+
# not merely defensive. (1) The stored value is written one record at a time, so
|
|
280
|
+
# a run that closed four misses stamped shared:1, shared:2, shared:3, shared:4 —
|
|
281
|
+
# only the last is right, and the stream is append-only so none can be edited.
|
|
282
|
+
# (2) Records written before 2026-08-28 carry cost_attribution:"none" from the
|
|
283
|
+
# empty-reqs_touched bug: a `framework` or `docs` repo has no REQs, so the old
|
|
284
|
+
# divisor collapsed and every measured window in those repos was discarded as
|
|
285
|
+
# unattributable. Counting the miss_ids actually closed against each fix_run_id
|
|
286
|
+
# recovers both cases from data already on the stream.
|
|
287
|
+
closed_per_run = {}
|
|
288
|
+
for f in fixes:
|
|
289
|
+
if f.get("fix_run_id") and f.get("miss_id"):
|
|
290
|
+
closed_per_run.setdefault(f["fix_run_id"], set()).add(f["miss_id"])
|
|
291
|
+
|
|
292
|
+
def _attribution(f):
|
|
293
|
+
# `none` is only honest where there is genuinely nothing to divide: no run
|
|
294
|
+
# matched, or the window itself could not be computed. Anything else with a
|
|
295
|
+
# real window is a share, and how many ways it splits is countable.
|
|
296
|
+
if not f.get("tokens_scope") or f.get("tokens_scope") == "none":
|
|
297
|
+
return "none"
|
|
298
|
+
if not f.get("fix_run_id"):
|
|
299
|
+
return "none"
|
|
300
|
+
# THE RECOUNT WINS OVER THE STORED VALUE — INCLUDING OVER A STORED `sole`.
|
|
301
|
+
#
|
|
302
|
+
# This was the bug (found 2026-08-31, on this repo's own live data). The
|
|
303
|
+
# emitter stamps attribution one record at a time, so a run that closed nine
|
|
304
|
+
# misses wrote sole, shared:2, shared:3 … shared:9 — the FIRST record is
|
|
305
|
+
# stamped `sole` because at that instant it was the only miss the run had
|
|
306
|
+
# closed. Short-circuiting on a stored `sole` therefore preserved exactly the
|
|
307
|
+
# value the recompute exists to correct, and it did so in the worst possible
|
|
308
|
+
# column: `sole` is the HEADLINE cost figure, the one §5.5.3 reserves for
|
|
309
|
+
# actual measurements. One whole multi-miss window was being reported as the
|
|
310
|
+
# measured cost of a single repair, once per multi-miss run, silently and
|
|
311
|
+
# upward.
|
|
312
|
+
#
|
|
313
|
+
# `sole` now means what §5.5.3 says it means — the run closed exactly one
|
|
314
|
+
# miss — and that is a fact about the finished stream, not about the order
|
|
315
|
+
# records happened to be written in.
|
|
316
|
+
n = len(closed_per_run.get(f["fix_run_id"]) or {f.get("miss_id")})
|
|
317
|
+
return "sole" if n <= 1 else "shared:%d" % n
|
|
318
|
+
|
|
319
|
+
for f in fixes:
|
|
320
|
+
f["cost_attribution_computed"] = _attribution(f)
|
|
321
|
+
|
|
322
|
+
sole = [f for f in fixes if f["cost_attribution_computed"] == "sole"]
|
|
323
|
+
shared = [f for f in fixes if f["cost_attribution_computed"].startswith("shared:")]
|
|
324
|
+
unattributable = sum(1 for f in fixes if f["cost_attribution_computed"] == "none")
|
|
325
|
+
recovered = sum(1 for f in fixes
|
|
326
|
+
if f.get("cost_attribution") == "none"
|
|
327
|
+
and f["cost_attribution_computed"] != "none")
|
|
328
|
+
|
|
329
|
+
def tok(fs):
|
|
330
|
+
"""Sum tokens_out over the records that ACTUALLY CARRY IT, and say how many.
|
|
331
|
+
|
|
332
|
+
`or 0` cannot distinguish an absent field from a recorded zero, so the
|
|
333
|
+
obvious one-liner averages an unmeasured repair in as a FREE repair and
|
|
334
|
+
understates rework — in the direction that flatters the framework. That is
|
|
335
|
+
the same defect class the miss stream exists to expose, appearing in the
|
|
336
|
+
tool that measures it: SCHEMA.md §2.5 says an absent optional stays null and
|
|
337
|
+
is never coerced to zero, and cost_usd already honours that (it is not pooled
|
|
338
|
+
across harnesses for exactly this reason). The token mean had the identical
|
|
339
|
+
hazard and no guard.
|
|
340
|
+
|
|
341
|
+
Fixed 2026-08-31 from TfLens TF-005. cost_sole_n still carries the record
|
|
342
|
+
count separately, so excluding unpriced records from the divisor loses no
|
|
343
|
+
information — and returning the denominator alongside the sum is what lets a
|
|
344
|
+
consumer agree with this reference AND be correct, which is the position the
|
|
345
|
+
old behaviour made impossible."""
|
|
346
|
+
priced = [f for f in fs if f.get("tokens_out") is not None]
|
|
347
|
+
return sum(f["tokens_out"] for f in priced), len(priced)
|
|
348
|
+
|
|
349
|
+
sole_tokens, sole_priced_n = tok(sole)
|
|
350
|
+
shared_priced = [f for f in shared if f.get("tokens_out") is not None]
|
|
351
|
+
|
|
352
|
+
# Dollars exist ONLY where a harness measured them. Claude Code and Codex carry
|
|
353
|
+
# cost_usd:null permanently (SCHEMA.md §4) and are never priced from a rate card
|
|
354
|
+
# here — a pooled sum over mixed harnesses would silently under-report.
|
|
355
|
+
paid = [f for f in sole if f.get("cost_usd") is not None]
|
|
356
|
+
escaped = [m for m in opened if m.get("found_by") in ("owner", "production")]
|
|
357
|
+
design = [m for m in opened if m.get("miss_class") == "unspecified-gap"]
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
"misses_total": len(opened),
|
|
361
|
+
"miss_fixes_total": len(fixes),
|
|
362
|
+
"orphan_fixes": orphans,
|
|
363
|
+
"open_misses": len(open_misses),
|
|
364
|
+
"wont_fix": len(wont_fix),
|
|
365
|
+
"resolved_misses": len(resolved),
|
|
366
|
+
"amendments_applied": amended,
|
|
367
|
+
"orphan_amends": orphan_amends,
|
|
368
|
+
# Denominator is records that CARRY the field — why_missed is optional (§5.5.6)
|
|
369
|
+
# and a missing value means "not assessed", never a zero for some category —
|
|
370
|
+
# AND records that COULD have carried it: one written before 2026-08-28 had no
|
|
371
|
+
# such field to fill, so counting it as unassessed understates the practice.
|
|
372
|
+
"why_missed_n": sum(1 for m in opened if m.get("why_missed")),
|
|
373
|
+
"why_missed_eligible": sum(1 for m in opened if _eligible(m, "why_missed")),
|
|
374
|
+
"why_missed_predates_field": sum(1 for m in opened if not _eligible(m, "why_missed")),
|
|
375
|
+
"why_missed": OrderedDict(
|
|
376
|
+
sorted(Counter(m["why_missed"] for m in opened if m.get("why_missed")).items(),
|
|
377
|
+
key=lambda kv: (-kv[1], kv[0]))),
|
|
378
|
+
# An escape with no why_missed wastes the most valuable record in the stream:
|
|
379
|
+
# something got past every gate and nobody recorded why nothing caught it.
|
|
380
|
+
"escapes_missing_why": sum(1 for m in opened
|
|
381
|
+
if m.get("found_by") in ("owner", "production")
|
|
382
|
+
and not m.get("why_missed")
|
|
383
|
+
and _eligible(m, "why_missed")),
|
|
384
|
+
"class_distribution": OrderedDict(
|
|
385
|
+
sorted(Counter(m.get("miss_class") or "?" for m in opened).items(),
|
|
386
|
+
key=lambda kv: (-kv[1], kv[0]))),
|
|
387
|
+
"found_by": OrderedDict(
|
|
388
|
+
sorted(Counter(m.get("found_by") or "?" for m in opened).items(),
|
|
389
|
+
key=lambda kv: (-kv[1], kv[0]))),
|
|
390
|
+
"design_miss_share": pct(len(design), len(opened)) if len(opened) >= MIN_N
|
|
391
|
+
else "insufficient data (n=%d)" % len(opened),
|
|
392
|
+
"escape_share": pct(len(escaped), len(opened)) if len(opened) >= MIN_N
|
|
393
|
+
else "insufficient data (n=%d)" % len(opened),
|
|
394
|
+
# Attribution-bounded. The denominator is `linked` records ONLY.
|
|
395
|
+
"attributed_n": len(linked),
|
|
396
|
+
"attribution_excluded": excluded,
|
|
397
|
+
"by_origin_phase": OrderedDict(
|
|
398
|
+
sorted(Counter(m.get("origin_phase") or "?" for m in linked).items(),
|
|
399
|
+
key=lambda kv: (-kv[1], kv[0]))),
|
|
400
|
+
"by_origin_model": OrderedDict(
|
|
401
|
+
sorted(Counter(m.get("origin_model") or "?" for m in linked).items(),
|
|
402
|
+
key=lambda kv: (-kv[1], kv[0]))),
|
|
403
|
+
"by_origin_agent": OrderedDict(
|
|
404
|
+
sorted(Counter(m.get("origin_agent") or "?" for m in linked).items(),
|
|
405
|
+
key=lambda kv: (-kv[1], kv[0]))),
|
|
406
|
+
# Cost-attribution-bounded. Measured and apportioned NEVER combine.
|
|
407
|
+
"cost_sole_n": len(sole),
|
|
408
|
+
"cost_shared_n": len(shared),
|
|
409
|
+
"cost_unattributable_n": unattributable,
|
|
410
|
+
# Windows the stream had written off as unattributable that the recomputed
|
|
411
|
+
# divisor recovers. Reported so a jump in the cost figures is explained by
|
|
412
|
+
# a fixed derivation rather than looking like the work got more expensive.
|
|
413
|
+
"cost_recovered_n": recovered,
|
|
414
|
+
# The divisor is the records that CARRY a token count, never all of them —
|
|
415
|
+
# and it is published beside the figure (`_n`) so a consumer can reproduce
|
|
416
|
+
# it exactly rather than choosing between agreeing and being right.
|
|
417
|
+
# TfLens TF-005, fixed 2026-08-31.
|
|
418
|
+
"tokens_per_miss_measured": round(float(sole_tokens) / sole_priced_n, 1)
|
|
419
|
+
if sole_priced_n >= MIN_N else None,
|
|
420
|
+
"tokens_per_miss_measured_n": sole_priced_n,
|
|
421
|
+
"tokens_unrecorded_sole_n": len(sole) - sole_priced_n,
|
|
422
|
+
"tokens_per_miss_apportioned": round(
|
|
423
|
+
sum(float(f["tokens_out"])
|
|
424
|
+
/ int(f["cost_attribution_computed"].split(":")[1])
|
|
425
|
+
for f in shared_priced) / len(shared_priced), 1)
|
|
426
|
+
if len(shared_priced) >= MIN_N else None,
|
|
427
|
+
"tokens_per_miss_apportioned_n": len(shared_priced),
|
|
428
|
+
"tokens_unrecorded_shared_n": len(shared) - len(shared_priced),
|
|
429
|
+
"cost_usd_per_miss_measured": round(
|
|
430
|
+
sum(f["cost_usd"] for f in paid) / len(paid), 4) if len(paid) >= MIN_N else None,
|
|
431
|
+
"cost_usd_records": len(paid),
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def analyse_phases(runs):
|
|
436
|
+
"""PER-PHASE EFFORT — what each `cmd` actually cost: time, tokens, which model,
|
|
437
|
+
how much fan-out. SCHEMA.md §2 / §2.5 / §2.6.
|
|
438
|
+
|
|
439
|
+
The unit is the RUN, aggregated by `cmd`. It is NOT per-feature cycle time —
|
|
440
|
+
§0's non-goal is unchanged, and nothing here is keyed on a REQ or a ticket.
|
|
441
|
+
|
|
442
|
+
THREE SEPARATIONS ARE ENFORCED HERE, not merely documented, because each one is
|
|
443
|
+
a way this block could quietly lie:
|
|
444
|
+
|
|
445
|
+
1. TOKENS. A run whose window could not be computed carries
|
|
446
|
+
tokens_scope:"none" (or no scope at all) and has NO token numbers. It is
|
|
447
|
+
excluded from every token figure and counted in `tokens_unmeasured_n`.
|
|
448
|
+
Averaging it in as zero is exactly the TF-005 defect one stream over —
|
|
449
|
+
`or 0` cannot tell an absent field from a measured zero, and the error
|
|
450
|
+
always flatters the framework.
|
|
451
|
+
|
|
452
|
+
2. FAN-OUT. `subagent_runs` is only meaningful on a tokens_scope:"tree"
|
|
453
|
+
record. A "main"-scope window never LOOKED at the subagent transcripts, so
|
|
454
|
+
its 0 means "not observed", not "none ran". Pooling the two would report
|
|
455
|
+
confident fan-out figures largely composed of runs that could not have
|
|
456
|
+
seen a subagent. Tree-scope only, with the exclusion printed.
|
|
457
|
+
|
|
458
|
+
3. DOLLARS. Never pooled across harness (SCHEMA.md §4). Claude and Codex
|
|
459
|
+
carry cost_usd:null permanently; a sum over mixed records under-reports
|
|
460
|
+
silently. Reported per harness or not at all.
|
|
461
|
+
|
|
462
|
+
Backfilled runs are excluded outright: a reconstructed duration is a guess, and
|
|
463
|
+
an effort report built on guesses is the kind of number that cannot be defended
|
|
464
|
+
when someone asks how it was measured."""
|
|
465
|
+
live = [r for r in runs if r.get("kind", "run") == "run" and not r.get("backfilled")]
|
|
466
|
+
|
|
467
|
+
def scope_of(r):
|
|
468
|
+
return r.get("tokens_scope") or "absent"
|
|
469
|
+
|
|
470
|
+
scope_cov = Counter(scope_of(r) for r in live)
|
|
471
|
+
|
|
472
|
+
def has_tokens(r):
|
|
473
|
+
return scope_of(r) not in ("none", "absent") and r.get("tokens_out") is not None
|
|
474
|
+
|
|
475
|
+
def sees_subagents(r):
|
|
476
|
+
# ONLY a tree-scope window observed the subagent transcripts at all — AND
|
|
477
|
+
# only a record written after the field existed could carry the count. Both
|
|
478
|
+
# exclusions are real and they are DIFFERENT, so they are reported apart:
|
|
479
|
+
# "we did not look" and "we could not have looked" are separate facts, and
|
|
480
|
+
# the §3.5 rule for a late-added field says say which.
|
|
481
|
+
return scope_of(r) == "tree" and r.get("subagent_runs") is not None
|
|
482
|
+
|
|
483
|
+
grand_out = sum(r.get("tokens_out") or 0 for r in live if has_tokens(r))
|
|
484
|
+
grand_dur = sum(r.get("duration_s") or 0 for r in live if r.get("duration_s"))
|
|
485
|
+
|
|
486
|
+
phases = OrderedDict()
|
|
487
|
+
for cmd in sorted({r.get("cmd") or "?" for r in live}):
|
|
488
|
+
rs = [r for r in live if (r.get("cmd") or "?") == cmd]
|
|
489
|
+
priced = [r for r in rs if has_tokens(r)]
|
|
490
|
+
durs = [r["duration_s"] for r in rs if r.get("duration_s")]
|
|
491
|
+
fan = [r for r in rs if sees_subagents(r)]
|
|
492
|
+
|
|
493
|
+
models = defaultdict(lambda: {"runs": 0, "tokens_out": 0})
|
|
494
|
+
for r in priced:
|
|
495
|
+
split = r.get("model_tokens_out")
|
|
496
|
+
if isinstance(split, dict) and split:
|
|
497
|
+
# The per-model SPLIT when the record carries it: a run that spent
|
|
498
|
+
# 90% of its output on one model and 10% on another is a different
|
|
499
|
+
# fact from an even split, and attributing the whole window to the
|
|
500
|
+
# dominant model is how a per-model cost figure goes wrong.
|
|
501
|
+
for m, t in split.items():
|
|
502
|
+
models[m]["tokens_out"] += t or 0
|
|
503
|
+
for m in split:
|
|
504
|
+
models[m]["runs"] += 1
|
|
505
|
+
elif r.get("model"):
|
|
506
|
+
models[r["model"]]["runs"] += 1
|
|
507
|
+
models[r["model"]]["tokens_out"] += r.get("tokens_out") or 0
|
|
508
|
+
|
|
509
|
+
# Declared fan-out (what an agent typed) is kept BESIDE measured fan-out,
|
|
510
|
+
# never merged with it — SCHEMA.md §2.6.
|
|
511
|
+
declared = Counter()
|
|
512
|
+
for r in rs:
|
|
513
|
+
for a in (r.get("subagents") or []):
|
|
514
|
+
declared[a] += 1
|
|
515
|
+
|
|
516
|
+
spawns = [r.get("subagent_runs") or 0 for r in fan]
|
|
517
|
+
sub_out = sum(r.get("tokens_out_subagents") or 0 for r in fan)
|
|
518
|
+
fan_out_total = sum(r.get("tokens_out") or 0 for r in fan if has_tokens(r))
|
|
519
|
+
|
|
520
|
+
cost_by_harness = defaultdict(float)
|
|
521
|
+
cost_n = Counter()
|
|
522
|
+
for r in priced:
|
|
523
|
+
if r.get("cost_usd") is not None:
|
|
524
|
+
cost_by_harness[r.get("harness") or "?"] += r["cost_usd"]
|
|
525
|
+
cost_n[r.get("harness") or "?"] += 1
|
|
526
|
+
|
|
527
|
+
tok = lambda k: sum(r.get(k) or 0 for r in priced)
|
|
528
|
+
phases[cmd] = {
|
|
529
|
+
"runs": len(rs),
|
|
530
|
+
"duration_s": {
|
|
531
|
+
"total": sum(durs),
|
|
532
|
+
"median": median(durs),
|
|
533
|
+
"max": max(durs) if durs else None,
|
|
534
|
+
"n": len(durs),
|
|
535
|
+
},
|
|
536
|
+
"share_of_duration": pct(sum(durs), grand_dur),
|
|
537
|
+
"tokens_measured_n": len(priced),
|
|
538
|
+
"tokens_unmeasured_n": len(rs) - len(priced),
|
|
539
|
+
"tokens": {
|
|
540
|
+
"in": tok("tokens_in"), "out": tok("tokens_out"),
|
|
541
|
+
"cache_read": tok("tokens_cache_read"), "cache_write": tok("tokens_cache_write"),
|
|
542
|
+
},
|
|
543
|
+
"tokens_out_median": median([r.get("tokens_out") or 0 for r in priced]),
|
|
544
|
+
"share_of_tokens_out": pct(tok("tokens_out"), grand_out),
|
|
545
|
+
"tokens_out_per_run": (round(float(tok("tokens_out")) / len(priced), 1)
|
|
546
|
+
if len(priced) >= MIN_N else None),
|
|
547
|
+
"models": OrderedDict(sorted(
|
|
548
|
+
((m, dict(v)) for m, v in models.items()),
|
|
549
|
+
key=lambda kv: (-kv[1]["tokens_out"], kv[0]))),
|
|
550
|
+
"harnesses": OrderedDict(sorted(Counter(r.get("harness") or "?" for r in rs).items())),
|
|
551
|
+
"modes": OrderedDict(sorted(Counter(r.get("mode") or "—" for r in rs).items())),
|
|
552
|
+
"build_result": OrderedDict(sorted(Counter(r.get("build_result") or "—" for r in rs).items())),
|
|
553
|
+
"reqs_touched_total": sum(r.get("reqs_count") or 0 for r in rs),
|
|
554
|
+
"files_written_total": sum(r.get("files_written") or 0 for r in rs),
|
|
555
|
+
"subagents_declared": OrderedDict(sorted(declared.items(), key=lambda kv: (-kv[1], kv[0]))),
|
|
556
|
+
# Tree-scope ONLY. `observed_n` is the denominator for every figure in
|
|
557
|
+
# this block and is printed with it — an exclusion the reader cannot see
|
|
558
|
+
# is indistinguishable from a bug (SCHEMA.md §6).
|
|
559
|
+
"fanout": {
|
|
560
|
+
"observed_n": len(fan),
|
|
561
|
+
"unobserved_n": len(rs) - len(fan),
|
|
562
|
+
# Two different reasons, never merged into one number.
|
|
563
|
+
"unobserved_not_tree": sum(1 for r in rs if scope_of(r) != "tree"),
|
|
564
|
+
"unobserved_predates_field": sum(
|
|
565
|
+
1 for r in rs
|
|
566
|
+
if scope_of(r) == "tree" and r.get("subagent_runs") is None),
|
|
567
|
+
"spawns_total": sum(spawns),
|
|
568
|
+
"spawns_median": median(spawns),
|
|
569
|
+
"spawns_max": max(spawns) if spawns else None,
|
|
570
|
+
"runs_with_fanout": sum(1 for s in spawns if s > 0),
|
|
571
|
+
"tokens_out_subagents": sub_out,
|
|
572
|
+
"subagent_share_of_tokens_out": pct(sub_out, fan_out_total),
|
|
573
|
+
},
|
|
574
|
+
"routing": {
|
|
575
|
+
"routed": sum(1 for r in rs if r.get("routed") is True),
|
|
576
|
+
"drifted": sum(1 for r in rs if r.get("routed") is False),
|
|
577
|
+
"unknown": sum(1 for r in rs if r.get("routed") is None),
|
|
578
|
+
},
|
|
579
|
+
# Dollars per harness, never pooled (SCHEMA.md §4).
|
|
580
|
+
"cost_usd_by_harness": OrderedDict(
|
|
581
|
+
(h, {"usd": round(v, 6), "records": cost_n[h]})
|
|
582
|
+
for h, v in sorted(cost_by_harness.items())),
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return {
|
|
586
|
+
"runs_live": len(live),
|
|
587
|
+
"scope_coverage": OrderedDict(sorted(scope_cov.items())),
|
|
588
|
+
"tokens_out_total": grand_out,
|
|
589
|
+
"duration_s_total": grand_dur,
|
|
590
|
+
"phases": phases,
|
|
591
|
+
"note": ("Per-phase effort over LIVE run records, aggregated by `cmd`. Token "
|
|
592
|
+
"figures exclude runs whose window could not be computed; fan-out "
|
|
593
|
+
"figures use tokens_scope='tree' records ONLY, because a 'main' "
|
|
594
|
+
"window never looked at the subagent transcripts and its 0 means "
|
|
595
|
+
"'not observed', not 'none ran'. The unit is the RUN, never the "
|
|
596
|
+
"feature — SCHEMA.md §0 non-goal, §2.6."),
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def app_name(repo):
|
|
601
|
+
hits = glob.glob(os.path.join(repo, "docs", "*-Checklist.md"))
|
|
602
|
+
if len(hits) == 1:
|
|
603
|
+
return os.path.basename(hits[0])[: -len("-Checklist.md")]
|
|
604
|
+
return os.path.basename(os.path.abspath(repo))
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def project_type(repo):
|
|
608
|
+
# Structural signal first and authoritative — see tf-emit.sh _project_type()
|
|
609
|
+
# for why the framework repo is detected from its tree shape rather than from
|
|
610
|
+
# core-config.yaml (that file is rsynced into every new app, so a value written
|
|
611
|
+
# there would misclassify all of them). Not inferred: this is a fact about the
|
|
612
|
+
# tree, so it never carries project_type_inferred.
|
|
613
|
+
if (os.path.isfile(os.path.join(repo, "scaffold-brownfield.sh"))
|
|
614
|
+
and os.path.isdir(os.path.join(repo, ".tfcore", "tasks"))):
|
|
615
|
+
return "framework", False
|
|
616
|
+
cfg = os.path.join(repo, ".tfcore", "core-config.yaml")
|
|
617
|
+
try:
|
|
618
|
+
text = open(cfg, encoding="utf-8").read()
|
|
619
|
+
except Exception:
|
|
620
|
+
return "app", True
|
|
621
|
+
m = re.search(
|
|
622
|
+
r"^metrics:[ \t]*$(?:\n(?:[ \t]+.*|[ \t]*))*?\n[ \t]+project_type:[ \t]*"
|
|
623
|
+
r"[\"']?(app|library|docs|framework)[\"']?[ \t]*$",
|
|
624
|
+
text, re.M,
|
|
625
|
+
)
|
|
626
|
+
return (m.group(1), False) if m else ("app", True)
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def has_commit_hook(repo):
|
|
630
|
+
"""Is the commit-telemetry hook installed in THIS clone? A filesystem read,
|
|
631
|
+
never a git call, so --report stays agent-safe. The hook lives in .git/, which
|
|
632
|
+
is not part of the repository, so every clone needs its own — a machine that
|
|
633
|
+
never ran update-framework.sh records nothing until it does."""
|
|
634
|
+
git = os.path.join(repo, ".git")
|
|
635
|
+
hooks = None
|
|
636
|
+
if os.path.isdir(git):
|
|
637
|
+
hooks = os.path.join(git, "hooks")
|
|
638
|
+
elif os.path.isfile(git):
|
|
639
|
+
try:
|
|
640
|
+
for line in open(git, encoding="utf-8"):
|
|
641
|
+
if line.startswith("gitdir:"):
|
|
642
|
+
gd = line.split(":", 1)[1].strip()
|
|
643
|
+
if not os.path.isabs(gd):
|
|
644
|
+
gd = os.path.join(repo, gd)
|
|
645
|
+
hooks = os.path.join(gd, "hooks")
|
|
646
|
+
break
|
|
647
|
+
except Exception:
|
|
648
|
+
return None
|
|
649
|
+
if not hooks:
|
|
650
|
+
return None # not a work tree — say nothing rather than warn
|
|
651
|
+
# pre-commit since 2026-08-11; post-commit is the retired form, still counted
|
|
652
|
+
# so a clone that has not been refreshed does not read as "no telemetry".
|
|
653
|
+
return any(os.path.isfile(os.path.join(hooks, h))
|
|
654
|
+
for h in ("pre-commit", "post-commit"))
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def run_git(repo, args, soft=False):
|
|
658
|
+
try:
|
|
659
|
+
out = subprocess.run(["git"] + args, cwd=repo, stdout=subprocess.PIPE,
|
|
660
|
+
stderr=subprocess.PIPE, check=True)
|
|
661
|
+
return out.stdout.decode("utf-8", "replace")
|
|
662
|
+
except Exception as e:
|
|
663
|
+
if soft:
|
|
664
|
+
return None # the hook path: a commit must never fail over telemetry
|
|
665
|
+
die("git failed in %s: %s" % (repo, e))
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
# ------------------------------------------------------- backfill: commits
|
|
669
|
+
PREFIX_RE = re.compile(r"^(feat|fix|docs|chore|refactor|test|build)([(!: ]|$)")
|
|
670
|
+
SHORTSTAT_RE = re.compile(r"(\d+) files? changed(?:, (\d+) insertions?\(\+\))?(?:, (\d+) deletions?\(-\))?")
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def backfill_commits(repo, dry_run, limit=None, quiet=False):
|
|
674
|
+
"""Reconcile commits.jsonl against `git log` — the ONE source that is already
|
|
675
|
+
replicated to every machine by push/pull.
|
|
676
|
+
|
|
677
|
+
git log is itself an append-only log, so these records are as trustworthy as
|
|
678
|
+
live ones. They are NOT flagged backfilled and need no separation in reporting.
|
|
679
|
+
|
|
680
|
+
Idempotent and gap-filling: every sha already in the stream is skipped, so
|
|
681
|
+
running this repeatedly is free, and a commit made on another machine (or on
|
|
682
|
+
this one before the hook existed) is picked up the next time it runs. That is
|
|
683
|
+
what makes the post-commit hook a reconciler rather than a lossy appender."""
|
|
684
|
+
app = app_name(repo)
|
|
685
|
+
existing = {r.get("sha") for r in read_stream(repo, "commits")}
|
|
686
|
+
branch = run_git(repo, ["rev-parse", "--abbrev-ref", "HEAD"], soft=quiet)
|
|
687
|
+
if branch is None:
|
|
688
|
+
return 0
|
|
689
|
+
branch = branch.strip() or "detached"
|
|
690
|
+
log_args = ["log", "--reverse", "--shortstat", "--date=format-local:%Y-%m-%dT%H:%M:%SZ",
|
|
691
|
+
"--format=\x01%h\x02%cd\x02%s"]
|
|
692
|
+
if limit:
|
|
693
|
+
log_args.insert(1, "-n%d" % limit)
|
|
694
|
+
raw = run_git(repo, log_args, soft=quiet)
|
|
695
|
+
if raw is None:
|
|
696
|
+
return 0
|
|
697
|
+
records, skipped = [], 0
|
|
698
|
+
for chunk in raw.split("\x01"):
|
|
699
|
+
if not chunk.strip():
|
|
700
|
+
continue
|
|
701
|
+
head, _, rest = chunk.partition("\n")
|
|
702
|
+
parts = head.split("\x02")
|
|
703
|
+
if len(parts) < 3:
|
|
704
|
+
continue
|
|
705
|
+
sha, when, subject = parts[0], parts[1], parts[2]
|
|
706
|
+
if sha in existing:
|
|
707
|
+
skipped += 1
|
|
708
|
+
continue
|
|
709
|
+
m = SHORTSTAT_RE.search(rest)
|
|
710
|
+
files = int(m.group(1)) if m else 0
|
|
711
|
+
ins = int(m.group(2) or 0) if m else 0
|
|
712
|
+
dele = int(m.group(3) or 0) if m else 0
|
|
713
|
+
pm = PREFIX_RE.match(subject.strip())
|
|
714
|
+
records.append(OrderedDict([
|
|
715
|
+
("v", 1), ("ts", when), ("kind", "commit"), ("app", app),
|
|
716
|
+
("sha", sha), ("files", files), ("insertions", ins), ("deletions", dele),
|
|
717
|
+
("subject_prefix", pm.group(1) if pm else None), # subject itself is DISCARDED
|
|
718
|
+
("branch", branch),
|
|
719
|
+
]))
|
|
720
|
+
if not quiet:
|
|
721
|
+
print("→ backfill-commits %s: %d new, %d already recorded" % (repo, len(records), skipped))
|
|
722
|
+
emit(repo, "commits", records, dry_run, quiet=quiet)
|
|
723
|
+
return len(records)
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
# --------------------------------------------------------- backfill: gates
|
|
727
|
+
ROW_RE = re.compile(r"^\|\s*(REQ-(UI|FN|RAG|NFR)-\d+)\s*\|(.*)$")
|
|
728
|
+
DATE_RE = re.compile(r"(20\d\d-\d\d-\d\d)")
|
|
729
|
+
|
|
730
|
+
# Remark markers -> (gate, failure_class). Matched against the Remarks cell ONLY to
|
|
731
|
+
# CLASSIFY it. The remark text itself is never stored — it is requirement prose.
|
|
732
|
+
REMARK_MARKERS = [
|
|
733
|
+
(re.compile(r"⚠\s*visual", re.I), "visual", None),
|
|
734
|
+
(re.compile(r"visual[- ]fail|overlap|off-?viewport|clipped|mockup[- ]drift", re.I), "visual", None),
|
|
735
|
+
(re.compile(r"⚠\s*render gate|render-?empty|render-?error|renders empty", re.I), "render", "blank-data"),
|
|
736
|
+
(re.compile(r"⚠\s*UAT bug", re.I), "escaped", None),
|
|
737
|
+
(re.compile(r"build (error|failed)|NETSDK|compile error", re.I), "build", "build-error"),
|
|
738
|
+
(re.compile(r"naming|prefix violation|coding[- ]standard", re.I), "standards", "naming"),
|
|
739
|
+
(re.compile(r"assert|expected .* got|spec failed|test failed", re.I), "acceptance", "assert-fail"),
|
|
740
|
+
]
|
|
741
|
+
CLASS_MARKERS = [
|
|
742
|
+
(re.compile(r"overlap", re.I), "overlap"),
|
|
743
|
+
(re.compile(r"clip", re.I), "clipped"),
|
|
744
|
+
(re.compile(r"off-?viewport|off-?screen", re.I), "offscreen"),
|
|
745
|
+
(re.compile(r"zero rows|no rows|0 rows", re.I), "zero-rows"),
|
|
746
|
+
(re.compile(r"blank|empty", re.I), "blank-data"),
|
|
747
|
+
(re.compile(r"exception|unhandled", re.I), "exception"),
|
|
748
|
+
]
|
|
749
|
+
|
|
750
|
+
STATUS_TO_VERDICT = {
|
|
751
|
+
"verified": "Verified",
|
|
752
|
+
"done (pre-existing)": "Done (pre-existing)",
|
|
753
|
+
"needs re-verify": "Needs re-verify",
|
|
754
|
+
"fail": "FAIL",
|
|
755
|
+
"blocked": "Blocked",
|
|
756
|
+
"implemented": "Implemented",
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
def classify(remark):
|
|
761
|
+
gate, fclass = None, None
|
|
762
|
+
for rx, g, fc in REMARK_MARKERS:
|
|
763
|
+
if rx.search(remark):
|
|
764
|
+
gate, fclass = g, fc
|
|
765
|
+
break
|
|
766
|
+
if gate and not fclass:
|
|
767
|
+
for rx, fc in CLASS_MARKERS:
|
|
768
|
+
if rx.search(remark):
|
|
769
|
+
fclass = fc
|
|
770
|
+
break
|
|
771
|
+
fclass = fclass or "other"
|
|
772
|
+
return gate, fclass
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
def backfill_gates(repo, dry_run):
|
|
776
|
+
"""The Requirements Status table is a mutated-in-place SNAPSHOT, not a log.
|
|
777
|
+
A REQ that failed three times then passed is indistinguishable from one that
|
|
778
|
+
passed first try. `attempt` is NOT recoverable — it is assumed, always, and
|
|
779
|
+
always listed in `inferred`. This data is context and volume, never evidence
|
|
780
|
+
for a published rate."""
|
|
781
|
+
app = app_name(repo)
|
|
782
|
+
ptype, ptype_inf = project_type(repo)
|
|
783
|
+
checklists = glob.glob(os.path.join(repo, "docs", "*-Checklist.md"))
|
|
784
|
+
if not checklists:
|
|
785
|
+
die("no docs/*-Checklist.md in %s" % repo)
|
|
786
|
+
|
|
787
|
+
already = {r.get("req_id") for r in read_stream(repo, "gates") if r.get("backfilled")}
|
|
788
|
+
records, skipped_status, skipped_dup = [], Counter(), 0
|
|
789
|
+
|
|
790
|
+
for path in checklists:
|
|
791
|
+
for line in open(path, encoding="utf-8"):
|
|
792
|
+
m = ROW_RE.match(line.rstrip("\n"))
|
|
793
|
+
if not m:
|
|
794
|
+
continue
|
|
795
|
+
req_id, req_class = m.group(1), m.group(2)
|
|
796
|
+
cells = [c.strip() for c in m.group(3).split("|")]
|
|
797
|
+
if len(cells) < 3:
|
|
798
|
+
continue
|
|
799
|
+
status = re.sub(r"[*`_]", "", cells[1]).strip()
|
|
800
|
+
remark = cells[3] if len(cells) > 3 else ""
|
|
801
|
+
verdict = STATUS_TO_VERDICT.get(status.lower())
|
|
802
|
+
if not verdict:
|
|
803
|
+
skipped_status[status or "(blank)"] += 1
|
|
804
|
+
continue
|
|
805
|
+
if req_id in already:
|
|
806
|
+
skipped_dup += 1
|
|
807
|
+
continue
|
|
808
|
+
|
|
809
|
+
date = DATE_RE.search(remark)
|
|
810
|
+
ts = (date.group(1) if date else "1970-01-01") + "T00:00:00Z"
|
|
811
|
+
inferred = ["attempt"]
|
|
812
|
+
|
|
813
|
+
# A remark that names a failure is evidence of a PRIOR attempt that the
|
|
814
|
+
# snapshot no longer shows. It is the only extra history recoverable.
|
|
815
|
+
gate, fclass = classify(remark)
|
|
816
|
+
attempt = 1
|
|
817
|
+
if gate and verdict in ("Verified", "Done (pre-existing)"):
|
|
818
|
+
records.append(OrderedDict([
|
|
819
|
+
("v", 1), ("ts", ts), ("kind", "gate"), ("app", app),
|
|
820
|
+
("project_type", ptype), ("run_id", "backfill"),
|
|
821
|
+
("req_id", req_id), ("req_class", req_class), ("attempt", 1),
|
|
822
|
+
("verdict", "Needs re-verify"), ("gate", gate),
|
|
823
|
+
("gates_run", []), ("failure_class", fclass), ("prior_verdict", None),
|
|
824
|
+
("backfilled", True),
|
|
825
|
+
("inferred", ["attempt", "gate", "failure_class", "gates_run", "verdict"]),
|
|
826
|
+
]))
|
|
827
|
+
attempt = 2
|
|
828
|
+
|
|
829
|
+
rec = OrderedDict([
|
|
830
|
+
("v", 1), ("ts", ts), ("kind", "gate"), ("app", app),
|
|
831
|
+
("project_type", ptype), ("run_id", "backfill"),
|
|
832
|
+
("req_id", req_id), ("req_class", req_class), ("attempt", attempt),
|
|
833
|
+
("verdict", verdict),
|
|
834
|
+
("gate", gate if verdict not in ("Verified", "Done (pre-existing)") else None),
|
|
835
|
+
("gates_run", []), # unknown — the snapshot never recorded which gates ran
|
|
836
|
+
("failure_class", fclass if verdict not in ("Verified", "Done (pre-existing)") else None),
|
|
837
|
+
("prior_verdict", None),
|
|
838
|
+
("backfilled", True),
|
|
839
|
+
])
|
|
840
|
+
if rec["gate"]:
|
|
841
|
+
inferred += ["gate", "failure_class"]
|
|
842
|
+
inferred += ["gates_run", "prior_verdict"]
|
|
843
|
+
if not date:
|
|
844
|
+
inferred.append("ts")
|
|
845
|
+
rec["inferred"] = inferred
|
|
846
|
+
if ptype_inf:
|
|
847
|
+
rec["project_type_inferred"] = True
|
|
848
|
+
records.append(rec)
|
|
849
|
+
|
|
850
|
+
print("→ backfill-gates %s (project_type=%s%s)" % (repo, ptype, ", INFERRED" if ptype_inf else ""))
|
|
851
|
+
print(" %d records to write, %d REQs already backfilled (skipped)" % (len(records), skipped_dup))
|
|
852
|
+
if skipped_status:
|
|
853
|
+
print(" rows skipped — status is not a verify verdict: " +
|
|
854
|
+
", ".join("%s×%d" % (k, v) for k, v in sorted(skipped_status.items())))
|
|
855
|
+
print(" EVERY record is flagged backfilled:true. `attempt` is INFERRED on all of them.")
|
|
856
|
+
print(" This set CANNOT support a published first-pass rate. See SCHEMA.md §7.")
|
|
857
|
+
emit(repo, "gates", records, dry_run)
|
|
858
|
+
return len(records)
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
# ------------------------------------------------------------------ report
|
|
862
|
+
def pct(num, den):
|
|
863
|
+
return "—" if not den else "%.0f%%" % (100.0 * num / den)
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
def median(xs):
|
|
867
|
+
xs = sorted(xs)
|
|
868
|
+
if not xs:
|
|
869
|
+
return None
|
|
870
|
+
n = len(xs)
|
|
871
|
+
return xs[n // 2] if n % 2 else (xs[n // 2 - 1] + xs[n // 2]) / 2.0
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
def analyse(repos):
|
|
875
|
+
"""Returns a dict of segmented figures. Segmentation is structural: the
|
|
876
|
+
live-only metrics are computed PER (project_type) over LIVE records only,
|
|
877
|
+
and there is no code path that produces a combined figure."""
|
|
878
|
+
gates, runs, sessions, commits, misses = [], [], [], [], []
|
|
879
|
+
per_repo = []
|
|
880
|
+
commit_dupes = 0
|
|
881
|
+
session_dupes = 0
|
|
882
|
+
for repo in repos:
|
|
883
|
+
g = read_stream(repo, "gates")
|
|
884
|
+
r = read_stream(repo, "runs")
|
|
885
|
+
misses += read_stream(repo, "misses")
|
|
886
|
+
# Per repo, not across them (SCHEMA.md §4): the OpenCode plugin
|
|
887
|
+
# appends a cumulative snapshot at every root-session idle.
|
|
888
|
+
s, sd = dedupe_sessions(read_stream(repo, "sessions"))
|
|
889
|
+
session_dupes += sd
|
|
890
|
+
# Per repo, not across them: two repos may legitimately share a short sha.
|
|
891
|
+
c, d = dedupe_commits(read_stream(repo, "commits"))
|
|
892
|
+
commit_dupes += d
|
|
893
|
+
gates += g; runs += r; sessions += s; commits += c
|
|
894
|
+
per_repo.append({"repo": repo, "app": app_name(repo),
|
|
895
|
+
"project_type": project_type(repo)[0],
|
|
896
|
+
"gates": len(g), "gates_backfilled": sum(1 for x in g if x.get("backfilled")),
|
|
897
|
+
"runs": len(r), "sessions": len(s), "commits": len(c),
|
|
898
|
+
"misses": sum(1 for x in read_stream(repo, "misses")
|
|
899
|
+
if x.get("kind") == "miss"),
|
|
900
|
+
# A repo reclassified after records were written (most often a
|
|
901
|
+
# greenfield project born `docs` and upgraded once it grew a head)
|
|
902
|
+
# keeps the OLD value on every existing record — append-only means
|
|
903
|
+
# corrections happen at read time, never by rewriting history. Say
|
|
904
|
+
# so: otherwise one project silently occupies two segments that,
|
|
905
|
+
# by §6, are never allowed to pool, and neither is the whole story.
|
|
906
|
+
"stale_types": sorted(
|
|
907
|
+
{x.get("project_type") for x in (g + r)
|
|
908
|
+
if x.get("project_type")
|
|
909
|
+
and x.get("project_type") != project_type(repo)[0]}),
|
|
910
|
+
"commit_hook": has_commit_hook(repo)})
|
|
911
|
+
|
|
912
|
+
def seg(records):
|
|
913
|
+
d = defaultdict(list)
|
|
914
|
+
for r in records:
|
|
915
|
+
key = "unclassified" if r.get("project_type_inferred") else r.get("project_type", "app")
|
|
916
|
+
d[key].append(r)
|
|
917
|
+
return d
|
|
918
|
+
|
|
919
|
+
live = [g for g in gates if not g.get("backfilled")]
|
|
920
|
+
back = [g for g in gates if g.get("backfilled")]
|
|
921
|
+
|
|
922
|
+
# REQs with ANY backfilled record are excluded from the live first-pass rate:
|
|
923
|
+
# their live `attempt` numbering restarts at 1 (SCHEMA.md §3.1).
|
|
924
|
+
tainted = {g.get("req_id") for g in back}
|
|
925
|
+
|
|
926
|
+
out = {"per_repo": per_repo, "tainted_reqs": sorted(x for x in tainted if x),
|
|
927
|
+
"live": {}, "backfilled": {}, "pooled": {},
|
|
928
|
+
"misses": analyse_misses(misses),
|
|
929
|
+
"phases": analyse_phases(runs)}
|
|
930
|
+
|
|
931
|
+
for label, bucket in (("live", live), ("backfilled", back)):
|
|
932
|
+
for ptype, recs in sorted(seg(bucket).items()):
|
|
933
|
+
eligible = [r for r in recs if label == "backfilled" or r.get("req_id") not in tainted]
|
|
934
|
+
reqs = {r.get("req_id") for r in eligible}
|
|
935
|
+
first_pass = {r.get("req_id") for r in eligible
|
|
936
|
+
if r.get("attempt") == 1 and r.get("verdict") == "Verified"}
|
|
937
|
+
failures = [r for r in recs if r.get("verdict") not in ("Verified", "Done (pre-existing)")]
|
|
938
|
+
dist = Counter(r.get("gate") or "unattributed" for r in failures)
|
|
939
|
+
escaped_reqs = {r.get("req_id") for r in recs if r.get("gate") == "escaped"}
|
|
940
|
+
failed_reqs = {r.get("req_id") for r in failures}
|
|
941
|
+
out[label][ptype] = {
|
|
942
|
+
"records": len(recs),
|
|
943
|
+
"reqs_scored": len(reqs),
|
|
944
|
+
"reqs_excluded_backfill_taint":
|
|
945
|
+
len({r.get("req_id") for r in recs if r.get("req_id") in tainted})
|
|
946
|
+
if label == "live" else 0,
|
|
947
|
+
"first_pass_n": len(first_pass),
|
|
948
|
+
"first_pass_rate": pct(len(first_pass), len(reqs)) if len(reqs) >= MIN_N else "insufficient data (n=%d)" % len(reqs),
|
|
949
|
+
"gate_distribution": OrderedDict(
|
|
950
|
+
(g, dist.get(g, 0)) for g in GATE_ORDER + ("unattributed",) if dist.get(g)),
|
|
951
|
+
"gate_distribution_n": len(failures),
|
|
952
|
+
"gate_distribution_note": None if len(failures) >= MIN_N else "insufficient data (n=%d)" % len(failures),
|
|
953
|
+
# Coverage for late-added gates: how many records actually RAN each one.
|
|
954
|
+
# Reported beside the count so nobody reads a share off the wrong denominator.
|
|
955
|
+
"late_gate_coverage": OrderedDict(
|
|
956
|
+
(g, {"ran": sum(1 for r in recs if g in (r.get("gates_run") or [])),
|
|
957
|
+
"caught": dist.get(g, 0), "since": since})
|
|
958
|
+
for g, since in LATE_GATES.items()),
|
|
959
|
+
"escape_rate": pct(len(escaped_reqs), len(failed_reqs)) if len(failed_reqs) >= MIN_N
|
|
960
|
+
else "insufficient data (n=%d)" % len(failed_reqs),
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
# ---- poolable metrics: runs, cadence, tokens. Exempt from both separations.
|
|
964
|
+
build_runs = [r for r in runs if r.get("cmd") == "build-phase"]
|
|
965
|
+
fix_runs = [r for r in runs if r.get("mode") == "fix"]
|
|
966
|
+
throughput = [float(r["reqs_count"]) / r["duration_s"]
|
|
967
|
+
for r in runs if r.get("duration_s") and r.get("reqs_count")]
|
|
968
|
+
batch = [r["reqs_count"] for r in build_runs if r.get("reqs_count") is not None]
|
|
969
|
+
verified_transitions = sum(1 for g in gates if g.get("verdict") == "Verified")
|
|
970
|
+
tok = sum((s.get("input_tokens") or 0) + (s.get("output_tokens") or 0) for s in sessions)
|
|
971
|
+
days = {c.get("ts", "")[:10] for c in commits if c.get("ts")}
|
|
972
|
+
out["pooled"] = {
|
|
973
|
+
"runs_total": len(runs),
|
|
974
|
+
"runs_by_cmd": OrderedDict(sorted(Counter(r.get("cmd") or "?" for r in runs).items())),
|
|
975
|
+
"rework_ratio": pct(len(fix_runs), len(build_runs)) if len(build_runs) >= MIN_N
|
|
976
|
+
else "insufficient data (n=%d build-phase runs)" % len(build_runs),
|
|
977
|
+
"throughput_median_reqs_per_hour": round(median(throughput) * 3600, 2) if len(throughput) >= MIN_N else None,
|
|
978
|
+
"batch_size_median": median(batch) if len(batch) >= MIN_N else None,
|
|
979
|
+
"sessions": len(sessions),
|
|
980
|
+
"tokens_total": tok,
|
|
981
|
+
"tokens_per_verified_req": round(float(tok) / verified_transitions, 1)
|
|
982
|
+
if verified_transitions >= MIN_N and tok else None,
|
|
983
|
+
"cost_usd": None, # never estimated — see SCHEMA.md §4
|
|
984
|
+
"commits": len(commits),
|
|
985
|
+
"commit_duplicates_collapsed": commit_dupes,
|
|
986
|
+
"session_duplicates_collapsed": session_dupes,
|
|
987
|
+
"active_days": len(days),
|
|
988
|
+
"commits_per_active_day": round(float(len(commits)) / len(days), 2) if days else None,
|
|
989
|
+
}
|
|
990
|
+
return out
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
def print_report(a, repos):
|
|
994
|
+
W = 78
|
|
995
|
+
print("=" * W)
|
|
996
|
+
print("TechieFlow development telemetry")
|
|
997
|
+
print("=" * W)
|
|
998
|
+
for r in a["per_repo"]:
|
|
999
|
+
print(" %-16s %-10s gates %4d (%d backfilled) runs %3d sessions %3d commits %4d misses %3d"
|
|
1000
|
+
% (r["app"], r["project_type"], r["gates"], r["gates_backfilled"],
|
|
1001
|
+
r["runs"], r["sessions"], r["commits"], r.get("misses", 0)))
|
|
1002
|
+
# The hook is per-CLONE (.git/ is not part of the repository), so a machine
|
|
1003
|
+
# that has never been refreshed silently records no commits at all. Say so —
|
|
1004
|
+
# this is the one telemetry gap the owner cannot see by reading the files.
|
|
1005
|
+
stale = [r for r in a["per_repo"] if r.get("stale_types")]
|
|
1006
|
+
if stale:
|
|
1007
|
+
print("")
|
|
1008
|
+
for r in stale:
|
|
1009
|
+
print(" ⚠ %s is classified '%s' now, but %s record(s) already written carry %s."
|
|
1010
|
+
% (r["app"], r["project_type"], "some",
|
|
1011
|
+
" / ".join("'%s'" % t for t in r["stale_types"])))
|
|
1012
|
+
print(" Those records keep the old value — the streams are append-only and a")
|
|
1013
|
+
print(" correction is applied at READ time, never by rewriting history. So this")
|
|
1014
|
+
print(" project appears under BOTH project_type segments below, and §6 forbids")
|
|
1015
|
+
print(" pooling them: read each as a period of the project, not as the whole of it.")
|
|
1016
|
+
print(" New records carry the corrected value from here on.")
|
|
1017
|
+
|
|
1018
|
+
missing = [r["app"] for r in a["per_repo"] if r["commit_hook"] is False]
|
|
1019
|
+
if missing:
|
|
1020
|
+
print("")
|
|
1021
|
+
print(" ⚠ no commit-telemetry hook in THIS clone of: %s" % ", ".join(missing))
|
|
1022
|
+
print(" Commit telemetry is not being written here. Fix it for good with")
|
|
1023
|
+
print(" update-framework.sh <repo>, or reconcile what is already in the log:")
|
|
1024
|
+
print(" .tfcore/telemetry/tf-metrics.sh --backfill-commits <repo>")
|
|
1025
|
+
print("")
|
|
1026
|
+
|
|
1027
|
+
for label in ("live", "backfilled"):
|
|
1028
|
+
if not a[label]:
|
|
1029
|
+
continue
|
|
1030
|
+
print("-" * W)
|
|
1031
|
+
if label == "live":
|
|
1032
|
+
print("LIVE records — written at the moment of the event")
|
|
1033
|
+
else:
|
|
1034
|
+
print("BACKFILLED records — RECONSTRUCTED. Context and volume only.")
|
|
1035
|
+
print(" `attempt` is inferred on every one of these. They CANNOT support a")
|
|
1036
|
+
print(" published first-pass rate. They are never summed with live figures.")
|
|
1037
|
+
print("-" * W)
|
|
1038
|
+
for ptype, m in a[label].items():
|
|
1039
|
+
print(" [%s] %d records, %d REQs scored" % (ptype, m["records"], m["reqs_scored"]))
|
|
1040
|
+
print(" first-pass rate : %s" % m["first_pass_rate"])
|
|
1041
|
+
print(" escape rate : %s" % m["escape_rate"])
|
|
1042
|
+
if m["gate_distribution_note"]:
|
|
1043
|
+
print(" gate catch dist. : %s" % m["gate_distribution_note"])
|
|
1044
|
+
else:
|
|
1045
|
+
total = m["gate_distribution_n"]
|
|
1046
|
+
print(" gate catch dist. : (%d failures)" % total)
|
|
1047
|
+
for g, n in m["gate_distribution"].items():
|
|
1048
|
+
tag = " <- escaped: NO gate caught it" if g == "escaped" else ""
|
|
1049
|
+
if g in LATE_GATES:
|
|
1050
|
+
tag = " <- see coverage below (added %s)" % LATE_GATES[g]
|
|
1051
|
+
print(" %-14s %4d %s%s" % (g, n, pct(n, total), tag))
|
|
1052
|
+
# A late-added gate's share of the total above is NOT its catch rate. Print the
|
|
1053
|
+
# real denominator, or say plainly that the gate has not run yet. SCHEMA.md §3.5.
|
|
1054
|
+
for g, cov in m.get("late_gate_coverage", {}).items():
|
|
1055
|
+
if cov["ran"] == 0 and cov["caught"] == 0:
|
|
1056
|
+
print(" %-19s: not yet run on this data (gate added %s)" % (g + " gate", cov["since"]))
|
|
1057
|
+
else:
|
|
1058
|
+
rate = (pct(cov["caught"], cov["ran"]) if cov["ran"] >= MIN_N
|
|
1059
|
+
else "insufficient data (n=%d)" % cov["ran"])
|
|
1060
|
+
print(" %-19s: ran on %d records, caught %d -> %s"
|
|
1061
|
+
% (g + " gate", cov["ran"], cov["caught"], rate))
|
|
1062
|
+
print(" (share of the distribution above is NOT this rate)")
|
|
1063
|
+
print("")
|
|
1064
|
+
|
|
1065
|
+
if len(a["live"]) > 1 or (a["live"] and a["backfilled"]):
|
|
1066
|
+
print(" NOTE: the figures above are deliberately NOT combined. Merging them")
|
|
1067
|
+
print(" across project_type or across live/backfilled would produce a")
|
|
1068
|
+
print(" number that cannot be defended. See SCHEMA.md §6.")
|
|
1069
|
+
print("")
|
|
1070
|
+
if a["tainted_reqs"]:
|
|
1071
|
+
print(" %d REQ(s) excluded from the LIVE first-pass rate because they carry"
|
|
1072
|
+
% len(a["tainted_reqs"]))
|
|
1073
|
+
print(" backfilled history, so their live attempt numbering restarts at 1:")
|
|
1074
|
+
print(" " + ", ".join(a["tainted_reqs"][:12]) +
|
|
1075
|
+
(" ..." if len(a["tainted_reqs"]) > 12 else ""))
|
|
1076
|
+
print("")
|
|
1077
|
+
|
|
1078
|
+
p = a["pooled"]
|
|
1079
|
+
print("-" * W)
|
|
1080
|
+
print("POOLABLE — comparable across project_type and provenance")
|
|
1081
|
+
print("-" * W)
|
|
1082
|
+
print(" runs total : %d %s" % (p["runs_total"],
|
|
1083
|
+
" ".join("%s=%d" % kv for kv in p["runs_by_cmd"].items())))
|
|
1084
|
+
print(" rework ratio : %s (fix-mode runs / build-phase runs)" % p["rework_ratio"])
|
|
1085
|
+
print(" batch size (median) : %s REQs per build-phase run" %
|
|
1086
|
+
(p["batch_size_median"] if p["batch_size_median"] is not None else "insufficient data"))
|
|
1087
|
+
print(" REQ throughput : %s REQs/hour (median across runs)" %
|
|
1088
|
+
(p["throughput_median_reqs_per_hour"] if p["throughput_median_reqs_per_hour"] is not None else "insufficient data"))
|
|
1089
|
+
print(" sessions / tokens : %d sessions, %s total tokens" % (p["sessions"], "{:,}".format(p["tokens_total"])))
|
|
1090
|
+
print(" tokens per Verified : %s (cost in USD is NEVER estimated — SCHEMA.md §4)" %
|
|
1091
|
+
(p["tokens_per_verified_req"] if p["tokens_per_verified_req"] is not None else "insufficient data"))
|
|
1092
|
+
print(" commit cadence : %s commits/active day (%d commits over %d days)" %
|
|
1093
|
+
(p["commits_per_active_day"] if p["commits_per_active_day"] is not None else "—",
|
|
1094
|
+
p["commits"], p["active_days"]))
|
|
1095
|
+
if p.get("session_duplicates_collapsed"):
|
|
1096
|
+
print(" (%d duplicate session_id(s) collapsed — normal for"
|
|
1097
|
+
% p["session_duplicates_collapsed"])
|
|
1098
|
+
print(" OpenCode, which snapshots cumulatively; SCHEMA.md §4)")
|
|
1099
|
+
if p["commit_duplicates_collapsed"]:
|
|
1100
|
+
print(" (%d duplicate sha(s) collapsed — normal after a union"
|
|
1101
|
+
% p["commit_duplicates_collapsed"])
|
|
1102
|
+
print(" merge; the same commit was recorded on two machines)")
|
|
1103
|
+
|
|
1104
|
+
print_phases(a["phases"], W)
|
|
1105
|
+
print_misses(a["misses"], W)
|
|
1106
|
+
print("=" * W)
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
def _hms(s):
|
|
1110
|
+
if not s:
|
|
1111
|
+
return "—"
|
|
1112
|
+
s = int(s)
|
|
1113
|
+
h, rem = divmod(s, 3600)
|
|
1114
|
+
m, sec = divmod(rem, 60)
|
|
1115
|
+
return ("%dh%02dm" % (h, m)) if h else ("%dm%02ds" % (m, sec))
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
def _k(n):
|
|
1119
|
+
if n is None:
|
|
1120
|
+
return "—"
|
|
1121
|
+
n = float(n)
|
|
1122
|
+
for unit, div in (("M", 1e6), ("k", 1e3)):
|
|
1123
|
+
if abs(n) >= div:
|
|
1124
|
+
return "%.1f%s" % (n / div, unit)
|
|
1125
|
+
return "%d" % n
|
|
1126
|
+
|
|
1127
|
+
|
|
1128
|
+
def print_phases(p, W):
|
|
1129
|
+
"""PER-PHASE EFFORT (SCHEMA.md §2.6). Every denominator this block uses is
|
|
1130
|
+
printed with the figure it bounds — the same discipline the misses block
|
|
1131
|
+
applies, for the same reason."""
|
|
1132
|
+
if not p or not p["phases"]:
|
|
1133
|
+
return
|
|
1134
|
+
print("")
|
|
1135
|
+
print("-" * W)
|
|
1136
|
+
print("EFFORT PER PHASE — time, tokens, model, fan-out (SCHEMA.md §2 / §2.5 / §2.6)")
|
|
1137
|
+
print("-" * W)
|
|
1138
|
+
print(" %d live run record(s). Token-window coverage: %s"
|
|
1139
|
+
% (p["runs_live"], " ".join("%s=%d" % kv for kv in p["scope_coverage"].items())))
|
|
1140
|
+
print(" A window is only as good as its scope: 'tree' saw the subagents, 'main'")
|
|
1141
|
+
print(" did not look, 'none'/absent measured nothing at all and is excluded from")
|
|
1142
|
+
print(" every token figure below rather than averaged in as a zero.")
|
|
1143
|
+
print("")
|
|
1144
|
+
hdr = " %-16s %5s %8s %8s %9s %7s %7s" % (
|
|
1145
|
+
"phase", "runs", "time", "tok-out", "tok-in", "%out", "%time")
|
|
1146
|
+
print(hdr)
|
|
1147
|
+
print(" " + "-" * (len(hdr) - 2))
|
|
1148
|
+
for cmd, m in p["phases"].items():
|
|
1149
|
+
print(" %-16s %5d %8s %8s %9s %7s %7s" % (
|
|
1150
|
+
cmd[:16], m["runs"], _hms(m["duration_s"]["total"]),
|
|
1151
|
+
_k(m["tokens"]["out"]), _k(m["tokens"]["in"]),
|
|
1152
|
+
m["share_of_tokens_out"], m["share_of_duration"]))
|
|
1153
|
+
print("")
|
|
1154
|
+
for cmd, m in p["phases"].items():
|
|
1155
|
+
print(" [%s] %d run(s)" % (cmd, m["runs"]))
|
|
1156
|
+
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"]))
|
|
1159
|
+
t = m["tokens"]
|
|
1160
|
+
print(" tokens : out %s · in %s · cache-read %s · cache-write %s"
|
|
1161
|
+
% (_k(t["out"]), _k(t["in"]), _k(t["cache_read"]), _k(t["cache_write"])))
|
|
1162
|
+
print(" measured on %d of %d run(s)%s"
|
|
1163
|
+
% (m["tokens_measured_n"], m["runs"],
|
|
1164
|
+
("; %d unmeasured and EXCLUDED, never counted as zero"
|
|
1165
|
+
% m["tokens_unmeasured_n"]) if m["tokens_unmeasured_n"] else ""))
|
|
1166
|
+
if m["tokens_out_per_run"] is not None:
|
|
1167
|
+
print(" tokens out / run : %s (median %s)"
|
|
1168
|
+
% (_k(m["tokens_out_per_run"]), _k(m["tokens_out_median"])))
|
|
1169
|
+
else:
|
|
1170
|
+
print(" tokens out / run : insufficient data (n=%d)" % m["tokens_measured_n"])
|
|
1171
|
+
if m["models"]:
|
|
1172
|
+
print(" by model :")
|
|
1173
|
+
for mod, v in m["models"].items():
|
|
1174
|
+
print(" %-28s %8s out over %d run(s) %s"
|
|
1175
|
+
% (mod[:28], _k(v["tokens_out"]), v["runs"],
|
|
1176
|
+
pct(v["tokens_out"], t["out"])))
|
|
1177
|
+
if m["harnesses"]:
|
|
1178
|
+
print(" harness : " + " ".join("%s=%d" % kv for kv in m["harnesses"].items()))
|
|
1179
|
+
# --- fan-out, on its own denominator and nobody else's.
|
|
1180
|
+
f = m["fanout"]
|
|
1181
|
+
if f["observed_n"]:
|
|
1182
|
+
print(" subagents : %d spawn(s) over %d observed run(s) "
|
|
1183
|
+
"(median %s, max %s; %d run(s) fanned out)"
|
|
1184
|
+
% (f["spawns_total"], f["observed_n"],
|
|
1185
|
+
f["spawns_median"] if f["spawns_median"] is not None else "—",
|
|
1186
|
+
f["spawns_max"] if f["spawns_max"] is not None else "—",
|
|
1187
|
+
f["runs_with_fanout"]))
|
|
1188
|
+
print(" %s output tokens in subagents = %s of this "
|
|
1189
|
+
"phase's observed output"
|
|
1190
|
+
% (_k(f["tokens_out_subagents"]), f["subagent_share_of_tokens_out"]))
|
|
1191
|
+
if f["unobserved_n"]:
|
|
1192
|
+
print(" ⚠ fan-out NOT observed on %d of %d run(s), and they are outside"
|
|
1193
|
+
% (f["unobserved_n"], m["runs"]))
|
|
1194
|
+
print(" every fan-out figure above — 0 there means NOT LOOKED, not none:")
|
|
1195
|
+
if f["unobserved_not_tree"]:
|
|
1196
|
+
print(" %d — window was not 'tree' scope, so no subagent transcript"
|
|
1197
|
+
% f["unobserved_not_tree"])
|
|
1198
|
+
print(" was ever read (SCHEMA.md §2.5)")
|
|
1199
|
+
if f["unobserved_predates_field"]:
|
|
1200
|
+
print(" %d — written before subagent_runs existed (2026-08-31); the"
|
|
1201
|
+
% f["unobserved_predates_field"])
|
|
1202
|
+
print(" field had no chance to be filled, which is not the same")
|
|
1203
|
+
print(" as being left empty (SCHEMA.md §3.5)")
|
|
1204
|
+
if m["subagents_declared"]:
|
|
1205
|
+
print(" declared by agent: " +
|
|
1206
|
+
" ".join("%s=%d" % kv for kv in m["subagents_declared"].items()))
|
|
1207
|
+
if f["observed_n"] and sum(m["subagents_declared"].values()) != f["spawns_total"]:
|
|
1208
|
+
print(" (declared %d vs measured %d — `subagents` is typed by the agent,"
|
|
1209
|
+
% (sum(m["subagents_declared"].values()), f["spawns_total"]))
|
|
1210
|
+
print(" `subagent_runs` is counted from the harness store; SCHEMA §2.6.")
|
|
1211
|
+
print(" Where they disagree the MEASURED one is right.)")
|
|
1212
|
+
r = m["routing"]
|
|
1213
|
+
if r["routed"] or r["drifted"]:
|
|
1214
|
+
print(" routing : on-tier %d · drifted %d · unknown %d "
|
|
1215
|
+
"(observed, never enforced)" % (r["routed"], r["drifted"], r["unknown"]))
|
|
1216
|
+
for h, c in m["cost_usd_by_harness"].items():
|
|
1217
|
+
print(" cost (%s) : $%s over %d record(s) — MEASURED, never pooled "
|
|
1218
|
+
"across harness" % (h, c["usd"], c["records"]))
|
|
1219
|
+
if m["reqs_touched_total"] or m["files_written_total"]:
|
|
1220
|
+
print(" work : %d REQ touch(es) · %d file(s) written"
|
|
1221
|
+
% (m["reqs_touched_total"], m["files_written_total"]))
|
|
1222
|
+
print("")
|
|
1223
|
+
|
|
1224
|
+
|
|
1225
|
+
def print_misses(m, W):
|
|
1226
|
+
"""The §5.5 block. Two exclusions are printed with the figures they bound —
|
|
1227
|
+
an exclusion the reader cannot see is indistinguishable from a bug."""
|
|
1228
|
+
if not m["misses_total"] and not m["miss_fixes_total"]:
|
|
1229
|
+
return
|
|
1230
|
+
print("")
|
|
1231
|
+
print("-" * W)
|
|
1232
|
+
print("MISSES — what was missed, who missed it, what the fix cost (SCHEMA.md §5.5)")
|
|
1233
|
+
print("-" * W)
|
|
1234
|
+
print(" misses logged : %d (%d open, %d resolved, %d wont-fix; %d fix records)"
|
|
1235
|
+
% (m["misses_total"], m["open_misses"], m["resolved_misses"],
|
|
1236
|
+
m["wont_fix"], m["miss_fixes_total"]))
|
|
1237
|
+
if m["wont_fix"]:
|
|
1238
|
+
print(" (wont-fix is a DECISION, not a backlog item, so it is not")
|
|
1239
|
+
print(" counted as open. The collapse check still treats it as a")
|
|
1240
|
+
print(" live defect, so a repeat failure will not open a duplicate.)")
|
|
1241
|
+
if m["orphan_fixes"]:
|
|
1242
|
+
print(" ⚠ %d miss-fix record(s) name a miss_id with no opening record" % m["orphan_fixes"])
|
|
1243
|
+
print(" design-miss share : %s (miss_class=unspecified-gap — the spec, not the build)"
|
|
1244
|
+
% m["design_miss_share"])
|
|
1245
|
+
print(" found by a human : %s (found_by owner/production — reported BESIDE the"
|
|
1246
|
+
% m["escape_share"])
|
|
1247
|
+
print(" gates.jsonl escape rate above, never merged with it)")
|
|
1248
|
+
if m["class_distribution"]:
|
|
1249
|
+
print(" miss classes :")
|
|
1250
|
+
for k, n in m["class_distribution"].items():
|
|
1251
|
+
print(" %-24s %4d %s" % (k, n, pct(n, m["misses_total"])))
|
|
1252
|
+
if m["found_by"]:
|
|
1253
|
+
print(" found by : " +
|
|
1254
|
+
" ".join("%s=%d" % kv for kv in m["found_by"].items()))
|
|
1255
|
+
# which PRACTICE failed — the field that says whether specification or verification
|
|
1256
|
+
# is the weak one. Optional, so it is reported against its own denominator.
|
|
1257
|
+
if m["why_missed"]:
|
|
1258
|
+
print(" why it was missed : (%d of %d misses assessed)"
|
|
1259
|
+
% (m["why_missed_n"], m["why_missed_eligible"]))
|
|
1260
|
+
for k, n in m["why_missed"].items():
|
|
1261
|
+
print(" %-26s %4d %s" % (k, n, pct(n, m["why_missed_n"])))
|
|
1262
|
+
if m["why_missed_predates_field"]:
|
|
1263
|
+
print(" %d miss(es) predate the field (added %s) and are outside that"
|
|
1264
|
+
% (m["why_missed_predates_field"], FIELD_SINCE["why_missed"]))
|
|
1265
|
+
print(" denominator — they had no field to fill, which is not the same as")
|
|
1266
|
+
print(" leaving one empty (SCHEMA.md §3.5, §5.5.6).")
|
|
1267
|
+
if m["escapes_missing_why"]:
|
|
1268
|
+
print(" ⚠ %d escape(s) carry no why_missed — something got past every gate and"
|
|
1269
|
+
% m["escapes_missing_why"])
|
|
1270
|
+
print(" nothing recorded why. That is the most valuable record in the stream.")
|
|
1271
|
+
print(" Complete it: tf-emit.sh --amend <miss_id> why_missed <value> (§5.5.7)")
|
|
1272
|
+
if m["amendments_applied"]:
|
|
1273
|
+
print(" amendments folded : %d field(s) completed by miss-amend records (§5.5.7)"
|
|
1274
|
+
% m["amendments_applied"])
|
|
1275
|
+
if m["orphan_amends"]:
|
|
1276
|
+
print(" ⚠ %d miss-amend record(s) name no known miss, or a field outside the"
|
|
1277
|
+
% m["orphan_amends"])
|
|
1278
|
+
print(" allowlist — counted, never applied")
|
|
1279
|
+
|
|
1280
|
+
# ---- attribution-bounded figures
|
|
1281
|
+
print("")
|
|
1282
|
+
print(" ATTRIBUTION — linked records only (%d of %d; %d excluded as inferred/unknown)"
|
|
1283
|
+
% (m["attributed_n"], m["misses_total"], m["attribution_excluded"]))
|
|
1284
|
+
if m["attribution_excluded"]:
|
|
1285
|
+
print(" Excluded records named a phase no runs.jsonl record backs, so their model")
|
|
1286
|
+
print(" is unknown. A per-model rate computed from guesses is a routing decision")
|
|
1287
|
+
print(" made on invented evidence — SCHEMA.md §6, third separation.")
|
|
1288
|
+
if m["attributed_n"] < MIN_N:
|
|
1289
|
+
print(" insufficient data (n=%d)" % m["attributed_n"])
|
|
1290
|
+
else:
|
|
1291
|
+
for label, key in (("by origin phase", "by_origin_phase"),
|
|
1292
|
+
("by origin agent", "by_origin_agent"),
|
|
1293
|
+
("by origin model", "by_origin_model")):
|
|
1294
|
+
if m[key]:
|
|
1295
|
+
print(" %-16s: " % label +
|
|
1296
|
+
" ".join("%s=%d" % kv for kv in m[key].items()))
|
|
1297
|
+
|
|
1298
|
+
# ---- cost, measured and apportioned, NEVER combined
|
|
1299
|
+
print("")
|
|
1300
|
+
print(" REWORK COST — measured and apportioned are separate columns, never one number")
|
|
1301
|
+
print(" sole (measured) : %d fix records" % m["cost_sole_n"])
|
|
1302
|
+
print(" tokens out per miss : %s (n=%d priced)" %
|
|
1303
|
+
((m["tokens_per_miss_measured"] if m["tokens_per_miss_measured"] is not None
|
|
1304
|
+
else "insufficient data (n=%d)" % m["tokens_per_miss_measured_n"]),
|
|
1305
|
+
m["tokens_per_miss_measured_n"]))
|
|
1306
|
+
# An unrecorded token count is NOT a zero. Excluding it from the divisor is the
|
|
1307
|
+
# whole fix; saying how many were excluded is what makes the figure defensible.
|
|
1308
|
+
if m.get("tokens_unrecorded_sole_n"):
|
|
1309
|
+
print(" %d sole fix record(s) carry NO tokens_out and are outside that"
|
|
1310
|
+
% m["tokens_unrecorded_sole_n"])
|
|
1311
|
+
print(" divisor — an unmeasured repair is unmeasured, never a free one.")
|
|
1312
|
+
if m["cost_usd_per_miss_measured"] is not None:
|
|
1313
|
+
print(" USD per miss : $%s (MEASURED — %d OpenCode records)"
|
|
1314
|
+
% (m["cost_usd_per_miss_measured"], m["cost_usd_records"]))
|
|
1315
|
+
else:
|
|
1316
|
+
print(" USD per miss : no measured dollars (%d priced records)"
|
|
1317
|
+
% m["cost_usd_records"])
|
|
1318
|
+
print(" Claude Code and Codex carry cost_usd:null permanently and are NEVER")
|
|
1319
|
+
print(" priced from a rate card here (SCHEMA.md §4). Real dollars come from")
|
|
1320
|
+
print(" OpenCode runs; token counts are the honest figure everywhere else.")
|
|
1321
|
+
print(" shared (apportioned): %d fix records — equal division, NOT a measurement"
|
|
1322
|
+
% m["cost_shared_n"])
|
|
1323
|
+
print(" tokens out per miss : %s (n=%d priced)" %
|
|
1324
|
+
((m["tokens_per_miss_apportioned"] if m["tokens_per_miss_apportioned"] is not None
|
|
1325
|
+
else "insufficient data (n=%d)" % m["tokens_per_miss_apportioned_n"]),
|
|
1326
|
+
m["tokens_per_miss_apportioned_n"]))
|
|
1327
|
+
if m.get("tokens_unrecorded_shared_n"):
|
|
1328
|
+
print(" %d apportioned fix record(s) carry no tokens_out — same exclusion"
|
|
1329
|
+
% m["tokens_unrecorded_shared_n"])
|
|
1330
|
+
print(" unattributable : %d fix records with no usable token window"
|
|
1331
|
+
% m["cost_unattributable_n"])
|
|
1332
|
+
if m["cost_unattributable_n"]:
|
|
1333
|
+
print(" A miss fixed inline, with no distinct run record, cannot be costed.")
|
|
1334
|
+
print(" It counts toward the miss count and contributes nothing to the money.")
|
|
1335
|
+
if m.get("cost_recovered_n"):
|
|
1336
|
+
print(" recovered : %d fix record(s) stored as 'none' that DO have a"
|
|
1337
|
+
% m["cost_recovered_n"])
|
|
1338
|
+
print(" measured window — the divisor is recomputed here from the misses")
|
|
1339
|
+
print(" each run actually closed (SCHEMA.md §5.5.3). Records written before")
|
|
1340
|
+
print(" 2026-08-28 in a repo with no REQs were all written off this way.")
|
|
1341
|
+
|
|
1342
|
+
|
|
1343
|
+
# -------------------------------------------------------------------- main
|
|
1344
|
+
def main(argv):
|
|
1345
|
+
if not argv or argv[0] in ("-h", "--help"):
|
|
1346
|
+
print("tf-metrics.sh — OWNER-RUN telemetry reporting & backfill (the only file")
|
|
1347
|
+
print(" in the framework allowed to contain git commands).")
|
|
1348
|
+
print("")
|
|
1349
|
+
print(" --report [<repo>] [--json] roll up one repo to stdout")
|
|
1350
|
+
print(" --rollup <repo>... [--json] cross-project view, still segmented")
|
|
1351
|
+
print(" --phases [<repo>...] [--json] effort per phase: time, tokens,")
|
|
1352
|
+
print(" model split, subagent fan-out (SCHEMA.md §2.6).")
|
|
1353
|
+
print(" Read-only and agent-safe, exactly like --report.")
|
|
1354
|
+
print(" --backfill-commits [<repo>] [--limit N] [--quiet] [--dry-run]")
|
|
1355
|
+
print(" reconcile commits.jsonl against git log — idempotent,")
|
|
1356
|
+
print(" gap-filling, and safe to run on any machine at any time.")
|
|
1357
|
+
print(" This is how commits made on ANOTHER machine get recorded.")
|
|
1358
|
+
print(" --backfill-gates [<repo>] [--dry-run] parse the checklist -> gates.jsonl")
|
|
1359
|
+
print("")
|
|
1360
|
+
print("Live and backfilled figures are NEVER combined, and app/library/docs are")
|
|
1361
|
+
print("never pooled, for first-pass rate / gate distribution / escape rate.")
|
|
1362
|
+
print("There is no flag to turn that off. See .tfcore/telemetry/SCHEMA.md §6.")
|
|
1363
|
+
return 0
|
|
1364
|
+
|
|
1365
|
+
mode = argv[0]
|
|
1366
|
+
rest = argv[1:]
|
|
1367
|
+
dry_run = "--dry-run" in rest
|
|
1368
|
+
as_json = "--json" in rest
|
|
1369
|
+
quiet = "--quiet" in rest
|
|
1370
|
+
limit = None
|
|
1371
|
+
for i, a in enumerate(rest):
|
|
1372
|
+
if a == "--limit" and i + 1 < len(rest):
|
|
1373
|
+
try:
|
|
1374
|
+
limit = int(rest[i + 1])
|
|
1375
|
+
except ValueError:
|
|
1376
|
+
die("--limit takes an integer")
|
|
1377
|
+
skip = set()
|
|
1378
|
+
for i, a in enumerate(rest):
|
|
1379
|
+
if a == "--limit":
|
|
1380
|
+
skip.add(i + 1)
|
|
1381
|
+
repos = [a for i, a in enumerate(rest) if not a.startswith("--") and i not in skip] or ["."]
|
|
1382
|
+
for r in repos:
|
|
1383
|
+
if not os.path.isdir(r):
|
|
1384
|
+
die("not a directory: %s" % r)
|
|
1385
|
+
|
|
1386
|
+
if mode == "--backfill-commits":
|
|
1387
|
+
backfill_commits(repos[0], dry_run, limit=limit, quiet=quiet)
|
|
1388
|
+
elif mode == "--backfill-gates":
|
|
1389
|
+
backfill_gates(repos[0], dry_run)
|
|
1390
|
+
elif mode in ("--report", "--rollup"):
|
|
1391
|
+
a = analyse(repos)
|
|
1392
|
+
if as_json:
|
|
1393
|
+
print(json.dumps(a, indent=2))
|
|
1394
|
+
else:
|
|
1395
|
+
print_report(a, repos)
|
|
1396
|
+
elif mode == "--phases":
|
|
1397
|
+
# The §2.6 effort view on its own. Read-only, no git, agent-safe — the same
|
|
1398
|
+
# standing as --report, which is what the parity gate and the *metrics task
|
|
1399
|
+
# already invoke.
|
|
1400
|
+
a = analyse(repos)
|
|
1401
|
+
if as_json:
|
|
1402
|
+
print(json.dumps(a["phases"], indent=2))
|
|
1403
|
+
else:
|
|
1404
|
+
print("=" * 78)
|
|
1405
|
+
print("TechieFlow effort per phase")
|
|
1406
|
+
print("=" * 78)
|
|
1407
|
+
print_phases(a["phases"], 78)
|
|
1408
|
+
print("=" * 78)
|
|
1409
|
+
else:
|
|
1410
|
+
die("unknown mode '%s' — see --help" % mode)
|
|
1411
|
+
return 0
|
|
1412
|
+
|
|
1413
|
+
|
|
1414
|
+
raise SystemExit(main(sys.argv[1:]))
|
|
1415
|
+
PYEOF
|