Repository navigation
Expand file tree
/
Copy pathcoverage.py
More file actions
287 lines (251 loc) · 12.1 KB
/
Copy pathcoverage.py
File metadata and controls
287 lines (251 loc) · 12.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
#!/usr/bin/env python3
"""Coop-readiness register generator.
Every column comes from ONE machine source with a NAMED limit. Nothing here
judges whether a class "should" count -- the 2026-07-22 pass measured that a
syntactic filter cannot express that semantic property (36% false negatives on
a known-positive set, and a false-positive side that is uncalibratable in
principle). So this tool ENUMERATES and ATTRIBUTES; it never classifies.
Columns and their sources:
denominator the dump. 2291 BP `_C` classes by the `class A<name>_C` form.
floor classes with ZERO own functions -- they cannot diverge between
peers by construction. SUBTRACTED FROM THE DENOMINATOR, never
added to the numerator: 838/2291 rendered as a percentage would
read as "36% done" where it means "this work does not exist".
coverage literal | base-walk | none. `literal` = our source names the
class in a string literal (parameter-free: a literal either
equals the class name or it does not). `base-walk` = the class
is not named but a non-engine ANCESTOR is -- our lanes match by
WalksToBase, so these ARE covered and a name-only rule reports
them as holes. Measured 2026-07-22: 3 of the 44 doc-named
classes (7%) are base-walk-only (kerfusFace_C, prop_deadRoach_C,
wisp_C). This column exists so that 7% is not silently read as
absence -- it is the characterized false-negative of the literal
source, and a characterized false negative hidden in prose reads
as a boundary.
rung sub-partition INSIDE the covered set, parsed from the free-text
tail our docs already write (smoke / awaiting hands-on / ...).
NEVER the boundary: measured 2026-07-22, tail-parse and the
literal source diverge in BOTH directions (17 vs 27, 14 common).
verified NOT derived. Read from tools/verified_takes.tsv, where every row
must cite a real artifact. Tail-parse scored 6 against a
measured 3 on the same set -- it is not admissible on this rung.
Ancestor walking STOPS at the engine roots. Walking to AActor once returned
"100% covered", because every class reaches it and our source mentions it
somewhere; that wrong number was caught only by absurdity.
"""
import argparse
import collections
import csv
import os
import re
import sys
DUMP = ("Game_0.9.0n_HOST/WindowsNoEditor/VotV/Binaries/Win64/"
"CXXHeaderDump")
SRC = "src/votv-coop" # NOT src/ alone: headers live in include/, and the
# first version scanned only src/ and undercounted by 43
# classes (161 -> 204). Scope the scan at the module root.
TAKES = "tools/verified_takes.tsv"
TRACKERS = ["docs/COOP_SYNC_MAP.md", "docs/signals/TRACKER.md",
"docs/COOP_ENTITY_EXPRESSION_MAP.md", "docs/COOP_RNG_AUTHORITY.md"]
RUNG_RE = re.compile(r"\b(VERIFIED|AS-BUILT|DESIGN|OPEN|PARTIAL)\b", re.I)
CLASS_TOK_RE = re.compile(r"\b([A-Za-z0-9_]+_C)\b")
# The walk stops here. Every BP class reaches these, so continuing past them
# makes "is an ancestor covered?" trivially true for everything.
ENGINE_ROOTS = {
"AActor", "APawn", "ACharacter", "AController", "APlayerController",
"UObject", "UActorComponent", "USceneComponent", "UUserWidget",
"USaveGame", "AInfo", "AHUD", "AGameModeBase", "AGameMode",
# Slipped past the first version and "covered" 57 classes on its own.
"UAnimInstance", "UBlueprintFunctionLibrary", "UInterface",
}
# A class HEADER, not a member declaration. `class Aprop_fireExt_C* fireExt;`
# is a member whose TYPE is a class, and it matches a bare `class X_C` pattern
# exactly as a header does. Splitting on the bare form cut class bodies at
# their own member declarations and re-attributed the functions that followed
# to the member's type: measured 2026-07-22, AfireExtHolder_C reported 0 own
# functions while its four (ReceiveBeginPlay, two BndEvt__, ExecuteUbergraph_)
# were credited to Aprop_fireExt_C. That single bug is what put the floor at
# 1170. Requiring the inheritance colon or the opening brace separates them.
CLASS_RE = re.compile(r"\bclass\s+([AU][A-Za-z0-9_]+_C)\s*(?=:|\{|\n\{)")
# `class Aprop_drive_C : public Aprop_C` -- the dump's single-inheritance form.
INHERIT_RE = re.compile(
r"\bclass\s+([AU][A-Za-z0-9_]+_C)\s*:\s*public\s+([AU][A-Za-z0-9_]+)")
# Own functions = declarations inside the class body. The dump lists them one
# per line with a return type; counting them is what makes the floor a fact
# rather than a guess.
FUNC_RE = re.compile(r"^\s+(?:static\s+)?[A-Za-z_][\w:<>*&,\s]*\s+"
r"([A-Za-z_]\w*)\s*\(", re.M)
LITERAL_RE = re.compile(r'"([A-Za-z0-9_]{3,})"')
WLITERAL_RE = re.compile(r'L"([A-Za-z0-9_]{3,})"')
def read_dump(root):
"""Return (own_fn_count_by_class, parent_by_class)."""
own = {}
parent = {}
for dirpath, _, names in os.walk(root):
for name in names:
if not name.endswith(".hpp"):
continue
path = os.path.join(dirpath, name)
with open(path, encoding="utf-8", errors="replace") as fh:
text = fh.read()
for m in INHERIT_RE.finditer(text):
parent[m.group(1)] = m.group(2)
# Split on class headers so each body is attributed to its class.
parts = CLASS_RE.split(text)
for i in range(1, len(parts), 2):
cls, body = parts[i], parts[i + 1]
own[cls] = own.get(cls, 0) + len(FUNC_RE.findall(body))
return own, parent
def read_source_literals(root):
"""Distinct string literals our own source carries. No free parameter."""
lits = set()
for dirpath, _, names in os.walk(root):
for name in names:
if not name.endswith((".cpp", ".h")):
continue
with open(os.path.join(dirpath, name),
encoding="utf-8", errors="replace") as fh:
text = fh.read()
lits.update(LITERAL_RE.findall(text))
lits.update(WLITERAL_RE.findall(text))
return lits
def names_class(lits, cls):
"""A literal names the class, or it does not. Nothing in between."""
stem = cls[1:]
core = stem[:-2] if stem.endswith("_C") else stem
return any(lit in (cls, stem, core, core + "_C") for lit in lits)
def ancestors(cls, parent):
"""Ancestors up to (not through) the engine roots."""
out = []
seen = {cls}
cur = parent.get(cls)
while cur and cur not in ENGINE_ROOTS and cur not in seen:
out.append(cur)
seen.add(cur)
cur = parent.get(cur)
return out
def coverage_of(cls, lits, parent):
"""Returns (kind, covering_ancestor_or_None).
The ancestor is returned, not discarded, because the AGGREGATE of
base-walk coverage is misleading and must never be printed as one number.
Measured 2026-07-22: `Aprop_C` alone accounts for 346 of them and
`Aactor_save_C` for 108 -- these are second-tier ROOTS, so "681 covered"
reads as reach the generic lane does not have. "346 classes descend from
the prop_C lane" is checkable; "681 covered" is the same shape of false
number as the ancestor walk that once returned "100% covered".
"""
if names_class(lits, cls):
return "literal", None
for anc in ancestors(cls, parent):
if names_class(lits, anc):
return "base-walk", anc
return "none", None
def read_doc_status(classes):
"""Classes carrying a rung token on a tracker line that names them.
This is the DOCUMENTATION's coverage, not the code's -- the gap between it
and the literal-named set is the register's most useful product: our code
handles the class, nobody wrote down where it stands.
"""
bare = {c[1:]: c for c in classes}
graded = set()
for path in TRACKERS:
if not os.path.exists(path):
continue
with open(path, encoding="utf-8", errors="replace") as fh:
for line in fh:
if not RUNG_RE.search(line):
continue
for tok in set(CLASS_TOK_RE.findall(line)):
c = tok if tok in classes else bare.get(tok)
if c:
graded.add(c)
return graded
def read_verified(path):
"""Hand-curated, but every row must CITE an artifact. A row without a
citation is refused, not silently counted -- the whole point of this rung
is that it cannot be derived from a syntactic proxy."""
rows = {}
if not os.path.exists(path):
return rows, ["%s absent -- VERIFIED rung reports 0" % path]
problems = []
with open(path, encoding="utf-8") as fh:
for line in fh:
line = line.strip()
if not line or line.startswith("#"):
continue
parts = [p.strip() for p in line.split("\t") if p.strip()]
if len(parts) < 4:
problems.append("row lacks class/date/artifact/verdict: %r"
% line)
continue
cls, date, artifact, verdict = parts[:4]
if not os.path.exists(artifact):
problems.append("%s cites a missing artifact: %s"
% (cls, artifact))
continue
rows[cls] = (date, artifact, verdict)
return rows, problems
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--dump", default=DUMP)
ap.add_argument("--src", default=SRC)
ap.add_argument("--takes", default=TAKES)
ap.add_argument("--csv", help="write the per-class register here")
args = ap.parse_args()
if not os.path.isdir(args.dump):
sys.exit("dump not found: %s" % args.dump)
own, parent = read_dump(args.dump)
lits = read_source_literals(args.src)
verified, problems = read_verified(args.takes)
total = len(own)
floor = {c for c, n in own.items() if n == 0}
live = [c for c in own if c not in floor]
lit_named = {c for c in live if names_class(lits, c)}
graded = len(read_doc_status(set(own)) & lit_named)
cov = {}
via = {}
for c in live:
cov[c], via[c] = coverage_of(c, lits, parent)
tally = collections.Counter(cov.values())
by_anc = collections.Counter(a for a in via.values() if a)
print("REGISTER (every column one machine source, each with a named limit)")
print()
print(" dump BP classes : %d" % total)
print(" floor, zero own functions : %d "
"(cannot diverge by construction)" % len(floor))
print(" denominator = dump - floor : %d "
"<- the floor REDUCES this, it is not progress" % len(live))
print()
print(" named by a literal : %d" % tally["literal"])
print(" not named, no covered ancestor : %d" % tally["none"])
print()
print(" reached only through an ancestor's lane -- NOT summed, because")
print(" the aggregate reads as coverage the generic lane does not have:")
for anc, n in by_anc.most_common(10):
print(" via %-26s %4d" % (anc, n))
if len(by_anc) > 10:
print(" (+%d more ancestors)" % (len(by_anc) - 10))
print()
print(" ...of those, a written status : %d "
"<- a rung token in a tracker doc" % graded)
print()
print(" VERIFIED hands-on (cited takes) : %d "
"<- re-counted from %s, never parsed from a doc tail"
% (len(verified), args.takes))
print()
if problems:
print("PROBLEMS (a register that hides these is the defect it replaces)")
for p in problems:
print(" ! %s" % p)
print()
if args.csv:
with open(args.csv, "w", newline="", encoding="utf-8") as fh:
w = csv.writer(fh)
w.writerow(["class", "own_fns", "coverage", "via_ancestor", "verified_date",
"verified_artifact"])
for c in sorted(live):
v = verified.get(c, ("", "", ""))
w.writerow([c, own[c], cov[c], via[c] or "", v[0], v[1]])
print("per-class register -> %s" % args.csv)
if __name__ == "__main__":
main()