372 lines
14 KiB
Python
372 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
"""gen_tui_validator_truth.py — freeze the tui_validator oracle's full
|
|
input→output surface (TUI design capture; Bubble Tea reconstruction parity).
|
|
|
|
Shared-harness freeze pattern (scripts/freeze_truth.py): exercise every
|
|
public function — and every branch-bearing private helper — of
|
|
src/rune/tui_validator.py and record {"fn", "args", "kwargs", "expected"}
|
|
rows into docs/tui-design-capture/frozen_tui_validator_truth.json. The
|
|
Go/Bubble Tea rewrite must prove parity against this file.
|
|
|
|
Why FIXTURES, not the live tree: validate_all_modules / validate_imports
|
|
read the LIVE src/rune/*.py — freezing that would rot on every repo edit.
|
|
The truth instead pins self-contained fixture trees under
|
|
scripts/tui_validator_fixtures/ (rewritten byte-identically on every run),
|
|
so the DECISION LOGIC — not today's CSS — is what's frozen. Live-tree mode
|
|
stays exercised by the CLI (`tui_validator.py check|css`) and
|
|
tests/test_tui_validator_imports.py.
|
|
|
|
Determinism pins (byte-identical regeneration):
|
|
* PYTHONHASHSEED=0 — _find_closest_property iterates the
|
|
VALID_CSS_PROPERTIES SET; an equidistant suggestion tie (e.g. 'layerz'
|
|
→ layer|layers, both edit-distance 1) otherwise depends on per-process
|
|
hash order. This script re-execs itself with the pin set.
|
|
* RUNE_SCRIPTS_DIR=scripts/tui_validator_fixtures/scripts_home — the
|
|
'scripts' import classification resolves via memory_env.scripts_dir().
|
|
* cwd pinned to the repo root — fixture paths are repo-relative, so
|
|
frozen args (and 'Could not read …' messages) are checkout-location
|
|
independent.
|
|
|
|
Usage:
|
|
python3 scripts/gen_tui_validator_truth.py # freeze
|
|
python3 scripts/gen_tui_validator_truth.py --verify # re-freeze + compare
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
REPO = Path(__file__).resolve().parents[1]
|
|
TRUTH_PATH = REPO / "docs" / "tui-design-capture" / "frozen_tui_validator_truth.json"
|
|
FIXTURE_ROOT = "scripts/tui_validator_fixtures" # repo-relative (cwd is pinned)
|
|
ORACLE = "rune.tui_validator"
|
|
FROZEN_NOW = "2026-10-14T00:00:00"
|
|
|
|
# ── deterministic pin: hash order decides suggestion ties ──────────────────
|
|
if os.environ.get("PYTHONHASHSEED") != "0":
|
|
os.environ["PYTHONHASHSEED"] = "0"
|
|
os.execv(sys.executable, [sys.executable, *sys.argv])
|
|
|
|
os.chdir(REPO)
|
|
os.environ["RUNE_SCRIPTS_DIR"] = f"{FIXTURE_ROOT}/scripts_home"
|
|
|
|
sys.path.insert(0, str(REPO / "scripts"))
|
|
sys.path.insert(0, str(REPO / "src"))
|
|
|
|
import freeze_truth # noqa: E402
|
|
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
# Fixture trees (rewritten identically on every run — freeze AND verify)
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
|
|
CLEAN_TUI = '''from __future__ import annotations
|
|
import os
|
|
import sys
|
|
from textual.app import App
|
|
from textual.widgets import Input
|
|
from textual_autocomplete import AutoComplete
|
|
import rich
|
|
import rune
|
|
from rune.tui_transport import get_transport
|
|
from .tui_backlog import list_backlog
|
|
from . import tui_transport
|
|
import memory_env
|
|
import bare_helper
|
|
|
|
DEFAULT_CSS = """
|
|
Screen {
|
|
background: $surface;
|
|
layer: base;
|
|
align: center middle;
|
|
}
|
|
"""
|
|
'''
|
|
|
|
CLEAN_BUBBLE = '''CSS = """
|
|
.bubble {
|
|
background: $panel;
|
|
border: round $primary;
|
|
margin: 0 1;
|
|
}
|
|
"""
|
|
'''
|
|
|
|
CLEAN_SPINNER = '''DEFAULT_CSS = """
|
|
.spinner {
|
|
text-style: bold;
|
|
color: $accent;
|
|
}
|
|
"""
|
|
'''
|
|
|
|
# Comma-import ('os,') + unknown import are FLAGGED; the indented import
|
|
# inside the function is INVISIBLE to the ^-anchored regex (quirk frozen).
|
|
DIRTY_TUI = '''from __future__ import annotations
|
|
import os, sys
|
|
from textual.app import App
|
|
import mystery_mod
|
|
from .tui_backlog import list_backlog
|
|
|
|
|
|
def _helper():
|
|
import indented_invisible
|
|
|
|
|
|
DEFAULT_CSS = """
|
|
#chat {
|
|
dock: bottom;
|
|
height: auto;
|
|
}
|
|
"""
|
|
'''
|
|
|
|
# backgroud (suggestion expected) + selection-background (Rich-ism, no
|
|
# suggestion possible) + Width (uppercase — silently IGNORED by the regex).
|
|
DIRTY_BUBBLE = '''CSS = """
|
|
.bubble {
|
|
backgroud: $panel;
|
|
selection-background: rgb(60, 60, 80);
|
|
Width: 100%;
|
|
}
|
|
"""
|
|
'''
|
|
|
|
# NOT in validate_all_modules' hardcoded file list — must be ignored (quirk).
|
|
OTHER_WIDGET = '''DEFAULT_CSS = """
|
|
.other {
|
|
colr: red;
|
|
}
|
|
"""
|
|
'''
|
|
|
|
GOOD_MODULE = '''"""Fixture module — docstring triple-quotes are not CSS literals."""
|
|
from __future__ import annotations
|
|
|
|
DEFAULT_CSS = """
|
|
.foo {
|
|
width: 100%;
|
|
height: 1fr;
|
|
background: $surface; /* inline comment */
|
|
}
|
|
.foo:focus {
|
|
border: solid $primary;
|
|
padding: 1 2;
|
|
margin: 0 1;
|
|
}
|
|
"""
|
|
'''
|
|
|
|
# Two REAL literals (DEFAULT_CSS, CSS) are validated; the _ALT name, the
|
|
# f-prefixed literal, and the single-quoted triple are all IGNORED (quirks).
|
|
BAD_MODULE = '''DEFAULT_CSS = """
|
|
.a {
|
|
backgroud: red;
|
|
}
|
|
"""
|
|
CSS = """
|
|
.b {
|
|
colr: blue;
|
|
}
|
|
"""
|
|
DEFAULT_CSS_ALT = """
|
|
.c {
|
|
zzz: 1;
|
|
}
|
|
"""
|
|
CSS = f"""f-prefix trap: zzzz: 2;"""
|
|
SINGLE = \'\'\'
|
|
.e {
|
|
zzzzz: 3;
|
|
}
|
|
\'\'\'
|
|
'''
|
|
|
|
FIXTURES: dict[str, str] = {
|
|
f"{FIXTURE_ROOT}/scripts_home/bare_helper.py": "# stub — a legacy scripts-dir module\n",
|
|
f"{FIXTURE_ROOT}/tree_clean/src/rune/__init__.py": "",
|
|
f"{FIXTURE_ROOT}/tree_clean/src/rune/memory_env.py": "# stub — first-party via directory listing\n",
|
|
f"{FIXTURE_ROOT}/tree_clean/src/rune/rune_tui.py": CLEAN_TUI,
|
|
f"{FIXTURE_ROOT}/tree_clean/src/rune/chat_bubble.py": CLEAN_BUBBLE,
|
|
f"{FIXTURE_ROOT}/tree_clean/src/rune/rune_spinner.py": CLEAN_SPINNER,
|
|
f"{FIXTURE_ROOT}/tree_dirty/src/rune/__init__.py": "",
|
|
f"{FIXTURE_ROOT}/tree_dirty/src/rune/memory_env.py": "# stub — first-party via directory listing\n",
|
|
f"{FIXTURE_ROOT}/tree_dirty/src/rune/rune_tui.py": DIRTY_TUI,
|
|
f"{FIXTURE_ROOT}/tree_dirty/src/rune/chat_bubble.py": DIRTY_BUBBLE,
|
|
f"{FIXTURE_ROOT}/tree_dirty/src/rune/rune_spinner.py": CLEAN_SPINNER,
|
|
f"{FIXTURE_ROOT}/tree_dirty/src/rune/other_widget.py": OTHER_WIDGET,
|
|
f"{FIXTURE_ROOT}/tree_no_tui/src/rune/chat_bubble.py": CLEAN_BUBBLE,
|
|
f"{FIXTURE_ROOT}/mod_css_good.py": GOOD_MODULE,
|
|
f"{FIXTURE_ROOT}/mod_css_bad.py": BAD_MODULE,
|
|
}
|
|
|
|
CLEAN_TREE = f"{FIXTURE_ROOT}/tree_clean"
|
|
DIRTY_TREE = f"{FIXTURE_ROOT}/tree_dirty"
|
|
NO_TUI_TREE = f"{FIXTURE_ROOT}/tree_no_tui"
|
|
CLEAN_SRC = f"{CLEAN_TREE}/src/rune"
|
|
|
|
|
|
def write_fixtures() -> None:
|
|
for rel, content in sorted(FIXTURES.items()):
|
|
path = REPO / rel
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(content)
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
# Case battery — every branch of the decision surface
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
def cases() -> list[tuple]:
|
|
c: list[tuple] = []
|
|
|
|
# ── extract_css_properties: line-level decision table ──────────────────
|
|
extract = [
|
|
("",),
|
|
(".foo {\n width: 100%;\n}\n",),
|
|
("/* header\n spanning */\nwidth: 1\n",), # line-shift after comment strip
|
|
("width: 1 /* trailing */\n",),
|
|
("#id {\n* star\n&:hover {\n@media screen\n{\n}\n",), # all selector/comment forms skipped
|
|
("Width: 10\n",), # uppercase first char: dropped
|
|
("wIdth: 10\n",), # mixed case: dropped
|
|
("width:1\n",),
|
|
("width : 1\n",),
|
|
("grid-size: 3 3\n",),
|
|
(".a {\n width: 1;\n height: 2;\n hatch: /;\n}\n",),
|
|
("color: red; border: solid\n",), # only the FIRST prop per line
|
|
("width: 1\r\nheight: 2\r\n",), # CRLF tolerated
|
|
("3d-depth: 1\n",), # digit-led: dropped
|
|
("background: url(data:image/png;base64,AAA)\n",), # colons inside the value
|
|
("-webkit-transform: none\n",), # leading hyphen: dropped
|
|
]
|
|
for css in extract:
|
|
c.append(("extract_css_properties", css, {}))
|
|
|
|
# ── validate_css: property verdict table ───────────────────────────────
|
|
validate = [
|
|
(""".foo {
|
|
width: 100%;
|
|
height: 1fr;
|
|
background: $surface;
|
|
border: solid $primary;
|
|
padding: 1 2;
|
|
margin: 0 1;
|
|
}
|
|
""", "selftest1-valid"),
|
|
(""".foo {
|
|
width: 100%;
|
|
selection-background: rgb(60, 60, 80);
|
|
selection-foreground: $text;
|
|
}
|
|
""", "selftest2-richisms"),
|
|
(".foo {\n backgroud: red;\n}", "suggestion-dist1"),
|
|
(".a {\n layerz: 1;\n}", {}), # TIE: layer|layers both dist 1 (hash pin)
|
|
(".a {\n zzzzzz: 1;\n}", "no-suggestion-far"),
|
|
(".a {\n bor: 1;\n}", "no-suggestion-dist3"), # dist 3 is NOT < 3
|
|
(".a {\n Width: 100%;\n}", "uppercase-escape"), # silently valid (quirk)
|
|
(".a {\n zzzz: 1;\n}", "mymod:L5"), # source label pass-through
|
|
(".a {\n colr: 1;\n colr: 2;\n}\n", "dup-lines"), # same bad prop on two lines
|
|
(".a {\n width: nonsense-value;\n}", "values-unchecked"),
|
|
(".foo:focus {\n border: solid;\n}", "pseudo-selector"),
|
|
("/*\ncolr: red\n*/\nwidth: 1\n", "comment-swallow"), # prop inside comment: stripped, no error
|
|
(".a {\n hatch: -;\n keyline: heavy;\n scrollbar-corner-color: red;\n text-opacity: 0.5;\n}\n", "exotic-valid"),
|
|
("", "empty"),
|
|
]
|
|
for css, source in validate:
|
|
if isinstance(source, dict):
|
|
c.append(("validate_css", (css,), source))
|
|
else:
|
|
c.append(("validate_css", (css, source), {}))
|
|
|
|
# ── _find_closest_property / _edit_distance: suggestion engine ────────
|
|
for prop in ("backgroud", "layerz", "zzzzzz", "border", "", "colr"):
|
|
c.append(("_find_closest_property", (prop,), {}))
|
|
for a, b in [
|
|
("", ""), ("a", ""), ("", "abc"), ("same", "same"),
|
|
("kitten", "sitting"), ("backgroud", "background"),
|
|
("layerz", "layer"), ("layerz", "layers"),
|
|
("abc", "yabd"), ("flaw", "lawn"), ("intention", "execution"),
|
|
]:
|
|
c.append(("_edit_distance", (a, b), {}))
|
|
|
|
# ── _classify: import verdict table ────────────────────────────────────
|
|
for module_name in [
|
|
".", ".sibling", # relative
|
|
"os", "__future__", "sys.path", # stdlib (incl. dotted top)
|
|
"textual", "textual.widgets", "textual_autocomplete", "rich", # external allowlist
|
|
"rune", "rune.tui_transport", # first-party: package basename
|
|
"memory_env", # first-party: directory listing
|
|
"bare_helper", # scripts dir (RUNE_SCRIPTS_DIR fixture)
|
|
"mystery_mod", # unknown
|
|
"os,", # comma-capture quirk: trailing comma
|
|
]:
|
|
c.append(("_classify", (module_name, CLEAN_SRC), {}))
|
|
|
|
# ── validate_module_css: literal-harvest decision table ────────────────
|
|
for path in [
|
|
f"{FIXTURE_ROOT}/mod_css_good.py",
|
|
f"{FIXTURE_ROOT}/mod_css_bad.py",
|
|
f"{CLEAN_TREE}/src/rune/rune_tui.py",
|
|
f"{DIRTY_TREE}/src/rune/chat_bubble.py",
|
|
f"{FIXTURE_ROOT}/missing.py", # unreadable → single degrade message
|
|
]:
|
|
c.append(("validate_module_css", (path,), {}))
|
|
|
|
# ── tree-level scanners ────────────────────────────────────────────────
|
|
for fn in ("validate_imports", "validate_all_modules", "validate_composition", "run_full_validation"):
|
|
for tree in (CLEAN_TREE, DIRTY_TREE, NO_TUI_TREE):
|
|
if fn in ("validate_composition",) and tree == NO_TUI_TREE:
|
|
continue # stub — two trees prove the constant []
|
|
c.append((fn, (tree,), {}))
|
|
|
|
return c
|
|
|
|
|
|
def main() -> int:
|
|
write_fixtures()
|
|
|
|
if "--verify" in sys.argv:
|
|
return freeze_truth.verify(TRUTH_PATH, ORACLE)
|
|
|
|
battery = cases()
|
|
truth = freeze_truth.freeze(ORACLE, battery, oracle_module=ORACLE)
|
|
|
|
doc = {
|
|
"frozen_now": FROZEN_NOW,
|
|
"generator": "scripts/gen_tui_validator_truth.py",
|
|
"oracle": "src/rune/tui_validator.py",
|
|
"python": ".".join(map(str, sys.version_info[:3])),
|
|
"pins": {
|
|
"PYTHONHASHSEED": "0 (suggestion ties iterate the VALID_CSS_PROPERTIES set)",
|
|
"RUNE_SCRIPTS_DIR": f"{FIXTURE_ROOT}/scripts_home",
|
|
"cwd": "repo root (all fixture paths repo-relative)",
|
|
},
|
|
"note": (
|
|
"Frozen via the shared harness (scripts/freeze_truth.py) against fixture "
|
|
"trees under scripts/tui_validator_fixtures/ — the decision logic is the "
|
|
"parity surface, not the live tree's current CSS. Verify: "
|
|
"python3 scripts/gen_tui_validator_truth.py --verify"
|
|
),
|
|
"cases": truth,
|
|
}
|
|
TRUTH_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
TRUTH_PATH.write_text(json.dumps(doc, indent=2, ensure_ascii=False, sort_keys=False) + "\n")
|
|
|
|
by_fn: dict[str, int] = {}
|
|
for row in truth:
|
|
by_fn[row["fn"]] = by_fn.get(row["fn"], 0) + 1
|
|
for fn, n in by_fn.items():
|
|
print(f" {fn}: {n}")
|
|
digest = hashlib.sha256(TRUTH_PATH.read_bytes()).hexdigest()
|
|
print(f"froze {len(truth)} rows -> {TRUTH_PATH.relative_to(REPO)}")
|
|
print(f"sha256 {digest}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|