Until now this repo was an export of a private tree, pushed as squashed snapshots. From here on it is the working tree itself, and the private repo is archived. The root holds AGENTS.md and the Rust and GitHub files only. Everything an agent needs to orient is in docs/: METHOD.md (the rules and how the project is built, 123 lines, project method only), MAP.md (generated from the code at every land) and CONTRIBUTING.md (what gets in and how a change is judged). One skill, `probe`, holds the procedure for running the client unattended against the local server. Brought over because the project needs it: reference/ (the 1.12 name catalogues 34 source files read; the export failed its own tests without them), the gate chain and the pool (scripts/), the hooks that enforce the install rule and the worktree rule, the full .gitignore, the `cargo play` alias, and the original wording of eleven source files the export had reworded. Left out: the decision records (archived with the private repo), the export tooling, the operator's Discord and triage loop, the lab-laptop scripts and the instruments nothing references, which live outside the repo now. What read the records is gone from the tooling: no corpus lint, no records index, no `reserve` verb, no number-collision check. Three hooks are gone: fmt-at-commit and the file-size nudge were redundant with the gates, and the assets/ui guard is now a test (`assets_ui_does_not_grow`) that fails for everyone, not only for a Claude Code session. The remaining pool hooks stand down on a machine without a pool. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
77 lines
3.3 KiB
Python
Executable File
77 lines
3.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""pass-span-lint — one `pass_span` per render pass at a time (decision 2258, bug B390).
|
|
|
|
bevy's `DiagnosticsRecorder::pass_span` is a timestamp pair PLUS a pipeline-statistics query, and
|
|
wgpu allows exactly one statistics query active at a time: a second `pass_span` opened on a pass
|
|
whose own span is still open is a validation error — on the backends that expose
|
|
`PIPELINE_STATISTICS_QUERY`, which in wgpu 27 is Vulkan alone (Metal never had it; wgpu-hal's
|
|
DX12 arm has the line commented out). So the gates on this machine (Metal) and the lab laptop
|
|
(pinned to DX12) run the nesting as a silent no-op, and the 09-15 sync shipped one: every Linux
|
|
player, and every Windows player wgpu had put on Vulkan, aborted on the first world frame (B390).
|
|
No compile, no test and no macOS run can see that shape. This check can, in a tenth of a second.
|
|
|
|
The shape it refuses, per function: `let g = <recorder>.pass_span(&mut P, …)` followed by another
|
|
`.pass_span(&mut P, …)` before `g.end(&mut P)`. Timestamp-only spans (`time_span`) nest fine and
|
|
are not looked at. Lexical, per file, reset at every `fn` — a span never outlives its function.
|
|
|
|
Usage: scripts/pass-span-lint.py [path …] (default: every crates/**/*.rs)
|
|
Exit: 0 clean, 1 findings.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pathlib
|
|
import re
|
|
import sys
|
|
|
|
OPEN = re.compile(r"\blet\s+(?:mut\s+)?(\w+)\s*=\s*[\w.]+\.pass_span\(\s*&mut\s+(\w+)\s*,")
|
|
ANY_OPEN = re.compile(r"\.pass_span\(\s*&mut\s+(\w+)\s*,")
|
|
END = re.compile(r"\b(\w+)\.end\(\s*&mut\s+(\w+)\s*\)")
|
|
FN = re.compile(r"^\s*(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?(?:unsafe\s+)?fn\s+\w+")
|
|
|
|
|
|
def lint(path: pathlib.Path) -> list[str]:
|
|
findings: list[str] = []
|
|
open_spans: list[tuple[str, str, int]] = [] # (guard, pass, line)
|
|
for lineno, raw in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
|
|
line = raw.split("//", 1)[0]
|
|
if FN.match(line):
|
|
open_spans.clear()
|
|
m = OPEN.search(line)
|
|
if m:
|
|
guard, pass_name = m.group(1), m.group(2)
|
|
else:
|
|
m = ANY_OPEN.search(line)
|
|
guard, pass_name = (None, m.group(1)) if m else (None, None)
|
|
if pass_name is not None:
|
|
for outer_guard, outer_pass, outer_line in open_spans:
|
|
if outer_pass == pass_name:
|
|
findings.append(
|
|
f"{path}:{lineno}: a second pass_span on `{pass_name}` while "
|
|
f"`{outer_guard}` (line {outer_line}) is still open — one "
|
|
f"pipeline-statistics query at a time (2258, B390)"
|
|
)
|
|
if guard is not None:
|
|
open_spans.append((guard, pass_name, lineno))
|
|
e = END.search(line)
|
|
if e:
|
|
open_spans = [s for s in open_spans if s[0] != e.group(1)]
|
|
return findings
|
|
|
|
|
|
def main() -> int:
|
|
args = [pathlib.Path(a) for a in sys.argv[1:]] or [pathlib.Path("crates")]
|
|
files: list[pathlib.Path] = []
|
|
for a in args:
|
|
files.extend(sorted(a.rglob("*.rs")) if a.is_dir() else [a])
|
|
findings = [f for p in files for f in lint(p)]
|
|
for f in findings:
|
|
print(f)
|
|
print(f"pass-span-lint: {'clean' if not findings else f'{len(findings)} finding(s)'} — "
|
|
f"{len(files)} file(s)")
|
|
return 1 if findings else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|