Format Specification revision 0.8 closes the thirty repair packages (V1-V30) and three additional defects (A1-A3) of reviews/01-triage.md before slice 4d writes any renderer code. Every Tier-1/2/3 design decision adopts the triage's own recommended fix; reviews/05-visual-fix-log.md records each choice, its reason, and the sections it touched. Contradictory surfaces first. `visuals.automation` is added to the 17.3 allowed-field table that previously rejected it. Trace 17.16.14 is rewritten around the four target families that now exist rather than asserting a table that changed. `fill` and `stroke` accept ValueSpec<color>, which is what makes 18.1's own component example legal, with three limits stated so a ValueSpec can never return a paint object. The repeater automation registry loses its nonexistent `step` block. An infinite loop is legal in every scope, bounded by whatever owns the track. Spawned-instance lifecycle moves into a `spawn` container. A top-level `lifetime` is now unambiguously per item and `spawn.lifetime` per instance, so a spawned emitter expresses both and a spawned repeater takes an instance lifetime without contradicting 18.5. Component inputs get one canonical location, the `component` object's own `inputs`, and the system-level `inputs` on emitters and repeaters is removed. The composition chain is written out in named spaces. The fit matrix carries the contain and cover centering offsets it was missing, the camera center is mapped into CSS coordinates before the translation is formed, and the chain B x P x V x F x M applies the device-pixel multiplier exactly once. Local extents and anchors are fixed for all fourteen primitives, arc sweep is directed with its bound checked before normalization, catmull-rom is a uniform cardinal spline with its equation given, and a closed bezier spline closes with a line. Compositing gets a normative eight-stage order. Layer opacity applies once, at the layer, not a second time per object, and a separate release factor initialized to 1 means release never rewrites an authored opacity. The sixteen offscreen buffers are one pool shared by layers and objects, with allocation order, reuse, and farthest-first shedding stated, and buffer refusal named as a diagnosed exception to 17.1. Sorting units are defined for emitters and repeaters, not only particles: a procedural system is atomic, with a representative depth and internal ordinal ordering. Depth fog fixes its blend space and fogs gradient stops before the paint is built. Coherent noise becomes an algorithm - ordered gradients, a 255-sample Fisher-Yates shuffle, the full lattice hash, octave normalization, and curl as the explicit perpendicular of the potential's gradient - with published tolerances. Distributions get their equations and an explicit per-distribution draw list, replacing "field order". All seventeen behaviors get complete field contracts, waveform equations, an accumulating-versus-fresh rule, and channel write sets. Live automation totals separate from authoring bounds: a spawn that would cross the live budget is refused atomically, consuming no ordinal, rather than being admitted with tracks dropped. One diagnostic cadence covers every ceiling and every shed. The centralized table gains the ceilings it was missing and two new authoring bounds - 64 declared systems and 16384 expanded static objects - that close the unbounded static draw load. `focalLength` gains a real lower bound of 1 so the clamp has a value to clamp to. The backing store cap wins over the multiplier floor on displays wider than 4096. Parallax 0 is pinned against camera translation, not exempt from zoom and rotation. tools/verify-spec-contract.py commits the contract harness that was previously run ad hoc. Its post-edit run reports 46 diagnostic codes declared, 107 distinct cross-references all resolving, zero unresolved references, 170 balanced fence markers, and 112 well-formed tables. Documentation only: no runtime, schema, fixture, or test file was touched, and npm test still passes 102 tests with zero failures. Every 19.5 value, including the two new static bounds, remains provisional pending the slice 4h GC6 measurement. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_0162Jb1J36judZNT8fHabGVt
144 lines
5.6 KiB
Python
144 lines
5.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Contract verification harness for XZBT_0-1_Format_Specification.md.
|
|
|
|
Checks, over the whole document:
|
|
1. every ERR_*/WARN_*/INFO_* code used resolves to a row of the section 7 table;
|
|
2. every section 7 row is used at least once outside the table;
|
|
3. every numeric cross-reference (e.g. "section 19.5", "19.5", "17.16.14") resolves
|
|
to a real heading or a numbered trace item;
|
|
4. code fences balance;
|
|
5. markdown table rows are well formed (consistent column count per table,
|
|
discounting escaped pipes).
|
|
|
|
Exit status is non-zero when any check fails.
|
|
"""
|
|
import io, os, re, sys, collections
|
|
|
|
SPEC = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
|
"..", "docs", "XZBT_0-1_Format_Specification.md")
|
|
|
|
def main():
|
|
text = io.open(SPEC, encoding="utf-8").read()
|
|
lines = text.split("\n")
|
|
problems = []
|
|
|
|
# ---- fences -------------------------------------------------------
|
|
fence_lines = [i for i, l in enumerate(lines, 1) if l.strip().startswith("```")]
|
|
if len(fence_lines) % 2:
|
|
problems.append("unbalanced code fences: %d fence markers" % len(fence_lines))
|
|
in_fence = set()
|
|
for a, b in zip(fence_lines[0::2], fence_lines[1::2]):
|
|
in_fence.update(range(a, b + 1))
|
|
|
|
# ---- headings -----------------------------------------------------
|
|
headings = {}
|
|
for i, l in enumerate(lines, 1):
|
|
m = re.match(r"^#{2,4}\s+(\d+(?:\.\d+)*)\.?\s", l)
|
|
if m:
|
|
headings[m.group(1)] = i
|
|
# numbered trace items inside a "Required traces" subsection
|
|
trace_items = collections.defaultdict(set)
|
|
current = None
|
|
for i, l in enumerate(lines, 1):
|
|
m = re.match(r"^#{2,4}\s+(\d+(?:\.\d+)*)\.?\s", l)
|
|
if m:
|
|
current = m.group(1)
|
|
continue
|
|
if current and re.match(r"^(\d+)\.\s", l) and i not in in_fence:
|
|
trace_items[current].add(re.match(r"^(\d+)\.\s", l).group(1))
|
|
|
|
# ---- section 7 diagnostic table ------------------------------------
|
|
declared = set()
|
|
sec7 = headings.get("7")
|
|
if not sec7:
|
|
problems.append("section 7 heading not found")
|
|
else:
|
|
i = sec7
|
|
while i < len(lines) and not re.match(r"^## 8\.", lines[i]):
|
|
m = re.match(r"^\|\s*`((?:ERR|WARN|INFO)_[A-Z0-9_]+)`\s*\|", lines[i])
|
|
if m:
|
|
declared.add(m.group(1))
|
|
i += 1
|
|
|
|
used = collections.Counter()
|
|
for i, l in enumerate(lines, 1):
|
|
if sec7 and sec7 <= i < sec7 + 60 and re.match(r"^\|\s*`(?:ERR|WARN|INFO)_", l):
|
|
continue
|
|
for code in re.findall(r"`((?:ERR|WARN|INFO)_[A-Z0-9_]+)`", l):
|
|
used[code] += 1
|
|
|
|
for code in sorted(used):
|
|
if code not in declared:
|
|
problems.append("undeclared diagnostic code used: %s" % code)
|
|
declared_only = sorted(c for c in declared if c not in used)
|
|
|
|
# ---- cross references ---------------------------------------------
|
|
xrefs = collections.Counter()
|
|
for i, l in enumerate(lines, 1):
|
|
if i in in_fence:
|
|
continue
|
|
if re.match(r"^\|?\s*`?(?:ERR|WARN|INFO)_", l):
|
|
pass
|
|
scan = re.sub(r"`[^`]*`", " ", l) # inline code spans hold data, not references
|
|
for m in re.finditer(r"(?<![\w.\-/])(\d{1,2}\.\d{1,2}(?:\.\d{1,2})?)(?![\w.\-])", scan):
|
|
ref = m.group(1)
|
|
head = ref.split(".")[0]
|
|
if head not in ("1","2","3","4","5","6","7","8","9","10","11","12","13",
|
|
"14","15","16","17","18","19"):
|
|
continue
|
|
xrefs[ref] += 1
|
|
|
|
unresolved = []
|
|
for ref in sorted(xrefs):
|
|
if ref in headings:
|
|
continue
|
|
parts = ref.split(".")
|
|
if len(parts) == 3:
|
|
parent = parts[0] + "." + parts[1]
|
|
if parent in headings and parts[2] in trace_items.get(parent, set()):
|
|
continue
|
|
unresolved.append(ref)
|
|
|
|
# ---- tables --------------------------------------------------------
|
|
tbl_start = None
|
|
tbl_cols = None
|
|
tables = 0
|
|
for i, l in enumerate(lines, 1):
|
|
if i in in_fence:
|
|
continue
|
|
is_row = l.lstrip().startswith("|") and l.rstrip().endswith("|")
|
|
if is_row:
|
|
cols = len(re.findall(r"(?<!\\)\|", l)) - 1
|
|
if tbl_start is None:
|
|
tbl_start, tbl_cols = i, cols
|
|
tables += 1
|
|
elif cols != tbl_cols:
|
|
problems.append("line %d: table starting at line %d has %d columns here, %d at its head"
|
|
% (i, tbl_start, cols, tbl_cols))
|
|
else:
|
|
tbl_start, tbl_cols = None, None
|
|
|
|
print("headings: %d" % len(headings))
|
|
print("diagnostic codes: %d declared, %d distinct used" % (len(declared), len(used)))
|
|
print("cross-references: %d distinct, %d unresolved" % (len(xrefs), len(unresolved)))
|
|
print("code fences: %d markers (%s)" % (len(fence_lines),
|
|
"balanced" if len(fence_lines) % 2 == 0 else "UNBALANCED"))
|
|
print("tables: %d" % tables)
|
|
if declared_only:
|
|
print("declared-only codes: %s" % ", ".join(declared_only))
|
|
if unresolved:
|
|
print("\nunresolved cross-references:")
|
|
for r in unresolved:
|
|
print(" %s (%d use%s)" % (r, xrefs[r], "" if xrefs[r] == 1 else "s"))
|
|
if problems:
|
|
print("\nproblems:")
|
|
for p in problems:
|
|
print(" " + p)
|
|
if problems or unresolved:
|
|
return 1
|
|
print("\nOK — every code resolves, every cross-reference resolves, fences balanced, tables well formed.")
|
|
return 0
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|