Firemedic15's picture
download
raw
29.3 kB
#!/usr/bin/env python3
# ---------------------------------------------------------------------------
# Vendored from ARIS (Auto-claude-code-research-in-sleep), skill paper-poster-html.
# Origin : https://github.com/wanshuiyin/Auto-claude-code-research-in-sleep
# Upstream base: MIT (c) 2026 wanshuiyin -- full text in ../LICENSES/aris-MIT.txt
# Vendored into posterly and ADAPTED (asset gate opt-in / no silent false-pass).
# As an adapted derivative this file ships as part of posterly under AGPL-3.0; see
# ../NOTICE.md. The other vendored gate scripts keep ARIS bodies unmodified (MIT).
# ---------------------------------------------------------------------------
"""run_gates - canonical gate orchestrator for HTML academic posters.
Runs the five poster gates in their fixed canonical order and writes a
single ``GATE_REPORT.json`` that the SKILL workflow (and any reviewer)
reads to decide pass/fail and find the full fix surface in one shot::
preflight -> style -> asset -> measure -> polish
WHY an orchestrator instead of calling each gate by hand:
- The order is load-bearing. ``preflight`` is a cheap static lint that
catches the bugs (LaTeX residue, missing images, broken roles) that
would otherwise burn an expensive render cycle in ``measure`` /
``polish``; ``style`` and ``asset`` source-gate the design before we
pay for rendering at all. Putting that order in one script keeps it
from drifting across callers.
- Default ACCUMULATE: a poster author wants the WHOLE fix surface at
once, not a stop-at-first-error trickle. ``--fail-fast`` is opt-in
for CI that only cares whether the build is green.
- The vendored ``poster_check.py`` keeps its own subcommand CLI
(preflight / measure / polish) and the new sibling gates
(style_check.py / asset_check.py) keep theirs, so the vendor diff
stays clean. This file is pure orchestration: it shells out to each,
parses what it can, and aggregates into the §7 schema.
Integration contract (DESIGN_FINAL.md §7, IMPLEMENTATION_CONVENTIONS.md
§C): child processes run with ``sys.executable``, ``cwd`` = the poster
HTML's directory (so relative ``assets/...`` paths resolve), and the
gate script paths are resolved relative to THIS file's location -- never
the cwd -- so the orchestrator is position-independent.
Exit code: 0 = overall PASS (zero hard failures), 1 = at least one hard
failure, 2 = usage / environment error.
posterly adaptation (vendored + modified -- see ../NOTICE.md): the asset
gate is OPT-IN. Without ``--manifest`` it is reported ``NOT_RUN`` and
excluded from ``overall`` (real-figure provenance simply not checked),
rather than being run, crashing on its required ``--manifest`` (child
exit 2), and being silently counted as a pass. Conversely, a HARD gate
that ends in SKIPPED from a real environment error (missing playwright /
script, child exit 2) now counts as a failure instead of a silent pass.
"""
from __future__ import annotations
import argparse
import datetime
import json
import os
import re
import subprocess
import sys
from pathlib import Path
from typing import Any
# Make ``_posterly`` importable when this file is run directly, exactly
# like the vendored poster_check.py / render_preview.py do.
_THIS_DIR = os.path.dirname(os.path.abspath(__file__))
if _THIS_DIR not in sys.path:
sys.path.insert(0, _THIS_DIR)
from _posterly import canvas as _canvas # noqa: E402
from _posterly.textutil import ascii_safe # noqa: E402
SCHEMA_VERSION = 1
SKILL_NAME = "posterly"
# Canonical gate order. The orchestration depends on this list order, so
# it is the single source of truth for "what runs and in what sequence".
CANONICAL_ORDER = ["preflight", "style", "asset", "measure", "polish"]
# Severity of each gate as a whole. ``preflight``, ``style``, ``asset``,
# and ``measure`` are HARD gates: a FAIL blocks ``overall``. ``polish``
# is SOFT by default (its findings are WARN) and only becomes hard under
# ``--strict-polish`` (which maps to ``poster_check.py polish --strict``).
GATE_SEVERITY = {
"preflight": "hard",
"style": "hard",
"asset": "hard",
"measure": "hard",
"polish": "soft",
}
# How many trailing lines of child stdout/stderr to keep in the report
# summary for the vendored gates (which have no machine-readable output).
# Enough to capture the "[gate] PASS/FAIL" verdict line and the couple of
# FAIL: lines above it without bloating the JSON.
TAIL_LINES = 8
def _eprint(*args: Any, **kw: Any) -> None:
print(*args, file=sys.stderr, **kw)
def _tail(text: str, n: int = TAIL_LINES) -> str:
"""Return the last ``n`` non-empty lines of ``text``, ascii-safe.
Used to summarise a vendored gate's human output when it offers no
``--json``. Non-empty filtering drops the trailing newline noise so
the verdict line lands at the end of the tail.
"""
lines = [ln for ln in text.splitlines() if ln.strip()]
return ascii_safe("\n".join(lines[-n:]))
# Lines that open measure's structured fix-surface blocks. When present,
# the summary keeps everything from the FIRST marker to the end (capped)
# instead of the last-8-lines tail -- an 8-line tail would truncate the
# per-column adjustments and edit targets that make a measure FAIL
# actionable without re-running.
_MEASURE_MARKERS = (
"[measure] suggested adjustments",
"[measure] edit targets",
"CIRCUIT BREAKER",
)
def _measure_tail(text: str, cap: int = 120) -> str:
"""Measure-specific summary: from the first marker line to the end,
capped at ``cap`` lines (middle elided, verdict tail kept). Falls
back to the generic ``_tail`` when no marker is present."""
lines = [ln for ln in text.splitlines() if ln.strip()]
idxs = [
i for i, ln in enumerate(lines)
if any(m in ln for m in _MEASURE_MARKERS)
]
if not idxs:
return ascii_safe("\n".join(lines[-TAIL_LINES:]))
block = lines[min(idxs):]
if len(block) > cap:
block = block[:cap - 4] + ["... (truncated) ..."] + block[-3:]
return ascii_safe("\n".join(block))
def _now_iso() -> str:
"""UTC timestamp, second precision, ``Z`` suffix -- stable for diffs."""
return (
datetime.datetime.now(datetime.timezone.utc)
.replace(microsecond=0)
.isoformat()
.replace("+00:00", "Z")
)
# --------------------------------------------------------------------------
# Canvas resolution (GATE_REPORT.json `canvas` block).
# --------------------------------------------------------------------------
def _orientation(width: float, height: float) -> str:
"""Landscape when wider than tall, else portrait (square -> portrait)."""
return "landscape" if width > height else "portrait"
def resolve_canvas(html_path: Path) -> dict[str, Any]:
"""Build the ``canvas`` block for the report.
Source priority (DESIGN_FINAL §7):
1. ``POSTER_STATE.json`` sitting next to the poster HTML, if it
carries canvas dimensions -- the workflow records the resolved
venue canvas there, so it is authoritative once present.
2. The ``@page { size }`` rule parsed from the HTML via
``_posterly.canvas`` -- source tagged ``"page-rule"``.
3. ``None`` everywhere with source ``"unknown"`` -- we never invent
a default; a downstream consumer must notice the canvas is not
resolved rather than trust a fabricated size.
"""
state_path = html_path.parent / "POSTER_STATE.json"
if state_path.exists():
block = _canvas_from_state(state_path)
if block is not None:
return block
# Fall back to the @page rule. canvas.py returns inches; the report
# schema is in centimetres.
parsed = _canvas.read_canvas_from_html(html_path)
if parsed is not None:
w_cm = round(parsed[0] * 2.54, 2)
h_cm = round(parsed[1] * 2.54, 2)
return {
"source": "page-rule",
"width_cm": w_cm,
"height_cm": h_cm,
"orientation": _orientation(w_cm, h_cm),
"source_url": None,
}
# Neither source resolved -- be explicit, do not fabricate a size.
return {
"source": "unknown",
"width_cm": None,
"height_cm": None,
"orientation": None,
"source_url": None,
}
def _canvas_from_state(state_path: Path) -> dict[str, Any] | None:
"""Pull a canvas block out of POSTER_STATE.json, defensively.
The workflow owns POSTER_STATE.json's full shape; we only need the
canvas. Accept either a nested ``{"canvas": {...}}`` object or the
canvas fields at top level, and tolerate missing/garbage by returning
``None`` so the caller can fall through to the @page rule. A partial
state (e.g. dimensions but no orientation) is completed where we can.
"""
try:
data = json.loads(state_path.read_text(encoding="utf-8"))
except (OSError, ValueError):
return None
if not isinstance(data, dict):
return None
canvas = data.get("canvas") if isinstance(data.get("canvas"), dict) else data
width = canvas.get("width_cm")
height = canvas.get("height_cm")
if not isinstance(width, (int, float)) or not isinstance(height, (int, float)):
# No usable dimensions -> let the @page rule speak instead.
return None
orientation = canvas.get("orientation")
if orientation not in ("landscape", "portrait"):
orientation = _orientation(float(width), float(height))
return {
"source": canvas.get("source") or "poster-state",
"width_cm": round(float(width), 2),
"height_cm": round(float(height), 2),
"orientation": orientation,
"source_url": canvas.get("source_url"),
}
# --------------------------------------------------------------------------
# Child-process gate invocation.
# --------------------------------------------------------------------------
def _run_child(argv: list[str], cwd: Path) -> tuple[int, str, str]:
"""Run a gate as a subprocess, return ``(returncode, stdout, stderr)``.
A child that cannot even start (e.g. its script file is missing, or
the interpreter rejects it) is surfaced as return code 2 with the
OSError text on stderr -- the same "environment error" code the
children themselves use -- so the gate is marked SKIPPED rather than
crashing the orchestrator.
"""
try:
proc = subprocess.run(
argv,
cwd=str(cwd),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
except OSError as exc: # script missing, not executable, etc.
return 2, "", f"failed to launch {ascii_safe(argv)}: {ascii_safe(exc)}"
return proc.returncode, proc.stdout, proc.stderr
def _parse_json_gate(stdout: str) -> dict[str, Any] | None:
"""Parse a JSON-emitting gate's stdout (style / asset).
Those gates write their structured verdict to a ``--json`` file, but
we also accept JSON on stdout as a fallback. We scan from the LAST
``{`` so any leading human log lines don't poison the parse. Returns
``None`` when no JSON object is found.
"""
start = stdout.rfind("{")
if start == -1:
return None
candidate = stdout[start:]
try:
obj = json.loads(candidate)
except ValueError:
return None
return obj if isinstance(obj, dict) else None
def _status_from_returncode(returncode: int, severity: str) -> str:
"""Map a child exit code to a gate status string.
0 -> PASS, 1 -> FAIL (hard) / WARN (soft polish), 3 -> FAIL
(measure's circuit breaker: the loop budget is exhausted -- a hard
stop, NOT an environment skip), 2 -> SKIPPED (environment error:
missing playwright, missing script, etc.). The soft/hard
distinction only matters at exit 1: a soft gate that exits 1
*without* ``--strict`` would not have happened (it returns 0 on
warnings), so an exit-1 polish under ``--strict-polish`` is a real
FAIL; we still resolve via severity for safety.
"""
if returncode == 0:
return "PASS"
if returncode in (1, 3):
return "FAIL" if severity == "hard" else "WARN"
return "SKIPPED"
def _build_argv(
gate: str,
scripts_dir: Path,
html_path: Path,
opts: argparse.Namespace,
report_json_dir: Path,
) -> list[str]:
"""Construct the exact argv for one gate.
Script paths are resolved against ``scripts_dir`` (THIS file's
directory) so the orchestrator is position-independent. The HTML is
passed as an ABSOLUTE path because each child runs with cwd set to
the HTML's directory and we don't want to assume the child re-derives
it. Per-gate ``--json`` sidecar files (style / asset) are written
next to the report so the full machine-readable trail lives together.
"""
py = sys.executable
html = str(html_path)
if gate == "preflight":
return [py, str(scripts_dir / "poster_check.py"), "preflight", html]
if gate == "style":
argv = [py, str(scripts_dir / "style_check.py"), html]
if opts.tokens:
argv += ["--tokens", str(Path(opts.tokens).resolve())]
if opts.no_render:
argv += ["--no-render"]
if opts.style_disable:
argv += ["--disable", opts.style_disable]
argv += ["--json", str(report_json_dir / "style_check.json")]
return argv
if gate == "asset":
argv = [py, str(scripts_dir / "asset_check.py"), html]
# asset_check requires --manifest; run_all only reaches this branch
# when --manifest is present (the asset gate is opt-in), so forward it.
if opts.manifest:
argv += ["--manifest", str(Path(opts.manifest).resolve())]
if opts.hero:
argv += ["--hero"]
if opts.waive_total_area:
argv += ["--waive-total-area"]
if opts.no_render:
argv += ["--no-render"]
argv += ["--json", str(report_json_dir / "asset_check.json")]
return argv
if gate == "measure":
argv = [py, str(scripts_dir / "poster_check.py"), "measure", html]
if getattr(opts, "measure_budget", None) is not None:
argv += ["--measure-budget", str(opts.measure_budget)]
if getattr(opts, "reset_measure_budget", False):
argv += ["--reset-budget"]
return argv
if gate == "polish":
argv = [py, str(scripts_dir / "poster_check.py"), "polish", html]
if opts.strict_polish:
argv += ["--strict"]
return argv
raise ValueError(f"unknown gate: {gate}") # pragma: no cover
def _summarize_gate(
gate: str,
returncode: int,
stdout: str,
stderr: str,
report_json_dir: Path,
) -> tuple[str | dict[str, Any], list[str]]:
"""Build ``(summary, artifacts)`` for one finished gate.
style / asset speak JSON, so we prefer their structured verdict
(sidecar file first, then stdout). The vendored gates (preflight /
measure / polish) have no machine format, so we keep a tail of their
combined stdout+stderr -- that captures the ``[gate] PASS/FAIL``
verdict line and any ``FAIL:`` lines, which is all a human needs to
locate the failure without re-running.
"""
artifacts: list[str] = []
if gate in ("style", "asset"):
sidecar = report_json_dir / f"{gate}_check.json"
obj: dict[str, Any] | None = None
if sidecar.exists():
try:
obj = json.loads(sidecar.read_text(encoding="utf-8"))
artifacts.append(str(sidecar))
except (OSError, ValueError):
obj = None
if obj is None:
obj = _parse_json_gate(stdout)
if obj is not None:
# Keep resolver diagnostics (malformed --tokens fields, font/hue
# fallbacks) visible: they go to the gate's stderr, which would
# otherwise be dropped once a JSON report exists.
warns = [ln for ln in stderr.splitlines() if "WARNING" in ln]
if warns:
obj["gate_stderr_warnings"] = warns[-8:]
return obj, artifacts
# No JSON at all -> the gate likely failed to start; fall back to
# the stderr/stdout tail so the report still says something useful.
return _tail(stdout + "\n" + stderr), artifacts
# Vendored gate: heuristic stdout/stderr tail + exit code. measure
# gets a marker-aware tail so its adjustments / edit-targets blocks
# survive into the report.
combined = (stdout + "\n" + stderr).strip()
return {
"exit_code": returncode,
"tail": (_measure_tail(combined) if gate == "measure"
else _tail(combined)),
}, artifacts
def run_gate(
gate: str,
scripts_dir: Path,
html_path: Path,
opts: argparse.Namespace,
report_json_dir: Path,
) -> dict[str, Any]:
"""Run one gate and return its report entry (DESIGN_FINAL §7 shape)."""
severity = GATE_SEVERITY[gate]
argv = _build_argv(gate, scripts_dir, html_path, opts, report_json_dir)
returncode, stdout, stderr = _run_child(argv, cwd=html_path.parent)
status = _status_from_returncode(returncode, severity)
summary, artifacts = _summarize_gate(
gate, returncode, stdout, stderr, report_json_dir
)
entry = {
"name": gate,
"severity": severity,
"status": status,
"command": argv,
"summary": summary,
"artifacts": artifacts,
}
# posterly: a SOFT gate (polish) reports advisory findings -- WIDOW /
# ORPHAN / GLUE-CHAIN / TRACK etc. -- on stdout but exits 0, so its status
# is PASS and the top-level `warnings` counter (which only tallies
# WARN-*status* gates) never sees them; they get buried in the truncated
# tail. That is exactly how two real WIDOWs shipped behind a
# `PASS / warnings: 0` summary. Surface them: pull the gate's own advisory
# count and the individual `WARN:` lines onto the entry so both the human
# digest and the report show what a soft PASS is otherwise hiding. This is
# display-only -- it does NOT change pass/fail (`overall`/`hard_failures`).
if severity == "soft":
adv_lines = [ln.strip() for ln in stdout.splitlines()
if ln.strip().startswith("WARN:")]
count = None
for ln in stdout.splitlines():
m = re.match(r"\s*warnings\s*:\s*(\d+)\s*$", ln)
if m:
count = int(m.group(1)) # last match = the summary tally
entry["advisories"] = count if count is not None else len(adv_lines)
if adv_lines:
entry["advisory_lines"] = adv_lines
return entry
# --------------------------------------------------------------------------
# Orchestration.
# --------------------------------------------------------------------------
def run_all(html_path: Path, opts: argparse.Namespace) -> dict[str, Any]:
"""Run the canonical gate sequence and assemble the GATE_REPORT dict."""
scripts_dir = Path(__file__).resolve().parent
report_path = (
Path(opts.report).resolve()
if opts.report
else html_path.parent / "GATE_REPORT.json"
)
# Sidecar JSON (style/asset --json) lands next to the report.
report_json_dir = report_path.parent
gates: list[dict[str, Any]] = []
hard_failures = 0
warnings = 0
soft_advisories = 0
for gate in CANONICAL_ORDER:
# posterly adaptation: the asset gate is OPT-IN. Without --manifest,
# report it NOT_RUN and exclude it from `overall`, instead of running
# asset_check, crashing on its required --manifest (child exit 2), and
# silently counting that as a pass. See ../NOTICE.md.
if gate == "asset" and not opts.manifest:
gates.append({
"name": "asset",
"severity": GATE_SEVERITY["asset"],
"status": "NOT_RUN",
"command": [],
"summary": {"not_run": "no --manifest: real-figure provenance "
"gate opted out (NOT verified)"},
"artifacts": [],
})
continue
entry = run_gate(gate, scripts_dir, html_path, opts, report_json_dir)
gates.append(entry)
soft_advisories += entry.get("advisories", 0)
is_hard = entry["severity"] == "hard"
# measure's circuit breaker (exit 3) means "stop iterating NOW"
# -- honouring it only under --fail-fast would immediately spend
# another Chromium render on polish right after a banner that
# said to stop. Halt the remainder unconditionally.
breaker = (
gate == "measure"
and isinstance(entry["summary"], dict)
and entry["summary"].get("exit_code") == 3
)
if entry["status"] == "FAIL" and is_hard:
hard_failures += 1
if opts.fail_fast or breaker:
# Stop scheduling further gates; mark the rest SKIPPED so
# the report still enumerates the full canonical sequence.
gates.extend(_skipped_remainder(
gate,
reason=("measure circuit breaker (exit 3): loop "
"budget exhausted" if breaker else None),
))
break
elif entry["status"] == "WARN":
warnings += 1
# posterly adaptation: a HARD gate that ends in SKIPPED here is a real
# environment error (child exit 2: missing playwright / script), not an
# opt-out -- count it as a failure rather than a silent pass.
elif entry["status"] == "SKIPPED" and is_hard:
hard_failures += 1
if opts.fail_fast:
gates.extend(_skipped_remainder(gate))
break
# A soft polish FAIL only happens under --strict-polish, where it
# counts as a hard failure for `overall`.
if entry["status"] == "FAIL" and not is_hard:
hard_failures += 1
if opts.fail_fast:
gates.extend(_skipped_remainder(gate))
break
overall = "PASS" if hard_failures == 0 else "FAIL"
return {
"schema_version": SCHEMA_VERSION,
"skill": SKILL_NAME,
"timestamp": _now_iso(),
"poster_html": str(html_path),
"canvas": resolve_canvas(html_path),
"overall": overall,
"hard_failures": hard_failures,
"warnings": warnings,
"soft_advisories": soft_advisories,
"gates": gates,
}
def _skipped_remainder(
stopped_at: str, reason: str | None = None
) -> list[dict[str, Any]]:
"""Build SKIPPED entries for every gate after ``stopped_at``.
Under ``--fail-fast`` (or measure's circuit breaker) we abort the
run on the hard failure but still want the report to list the full
canonical sequence, with the not-run gates explicitly marked
SKIPPED (with the reason) rather than silently absent.
"""
idx = CANONICAL_ORDER.index(stopped_at)
out: list[dict[str, Any]] = []
for gate in CANONICAL_ORDER[idx + 1:]:
out.append({
"name": gate,
"severity": GATE_SEVERITY[gate],
"status": "SKIPPED",
"command": [],
"summary": {"skipped": reason or
"fail-fast: a prior hard gate failed"},
"artifacts": [],
})
return out
def _print_human_summary(report: dict[str, Any]) -> None:
"""Print a compact human-readable digest to stdout alongside the JSON
file write, so a terminal run isn't silent."""
print(f"[run_gates] {ascii_safe(report['poster_html'])}")
canvas = report["canvas"]
if canvas["width_cm"] is not None:
print(
f" canvas: {canvas['width_cm']} x {canvas['height_cm']} cm "
f"{canvas['orientation']} (source: {canvas['source']})"
)
else:
print(f" canvas: UNRESOLVED (source: {canvas['source']})")
for g in report["gates"]:
adv = g.get("advisories") or 0
tag = (f" ({adv} advisor{'y' if adv == 1 else 'ies'})"
if adv else "")
print(f" {g['name']:9s} [{g['severity']:4s}] -> {g['status']}{tag}")
s = g.get("summary")
if isinstance(s, dict) and s.get("gate_stderr_warnings"):
for ln in s["gate_stderr_warnings"]:
print(f" ! {ascii_safe(ln)}")
# A soft gate's advisories are PASS-status findings the top-line
# counters don't reflect -- print each (compact) so they're visible at
# a glance, capped so a widow-heavy poster can't flood the terminal.
adv_lines = g.get("advisory_lines") or []
_CAP = 12
for ln in adv_lines[:_CAP]:
snippet = ln if len(ln) <= 150 else ln[:147] + "..."
print(f" ! {ascii_safe(snippet)}")
if len(adv_lines) > _CAP:
print(f" ! ... {len(adv_lines) - _CAP} more "
f"(run poster_check.py polish for the full list)")
print(
f" overall: {report['overall']} "
f"hard_failures: {report['hard_failures']} "
f"warnings: {report['warnings']}"
+ (f" soft-advisories: {report['soft_advisories']}"
if report.get("soft_advisories") else "")
)
def build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(
prog="run_gates",
description=(
"Run the canonical poster gate sequence "
"(preflight -> style -> asset -> measure -> polish) and write "
"GATE_REPORT.json. Default accumulates all results; "
"--fail-fast stops at the first hard failure."
),
)
p.add_argument("html", help="path to poster.html")
p.add_argument(
"--report", default=None,
help="output GATE_REPORT.json path "
"(default: GATE_REPORT.json next to the HTML)",
)
p.add_argument(
"--fail-fast", action="store_true",
help="stop at the first HARD failure instead of accumulating "
"the full fix surface",
)
p.add_argument(
"--strict-polish", action="store_true",
help="treat polish warnings as failures "
"(maps to poster_check.py polish --strict)",
)
p.add_argument(
"--tokens", default=None,
help="design tokens JSON, passed through to style_check.py",
)
p.add_argument(
"--manifest", default=None,
help="FIGURE_MANIFEST.json for the real-figure asset gate. If "
"omitted, the asset gate is opted out and reported NOT_RUN "
"(real-figure provenance is not verified).",
)
p.add_argument(
"--hero", action="store_true",
help="hero-template poster: forwarded to asset_check.py so a large "
"legitimate hero figure isn't flagged oversized",
)
p.add_argument(
"--waive-total-area", action="store_true",
help="theory-paper waiver, passed through to asset_check.py",
)
p.add_argument(
"--no-render", action="store_true",
help="skip render-dependent checks; passed through to "
"style_check.py / asset_check.py (their render rules degrade "
"to SKIPPED / natural-size estimates)",
)
p.add_argument(
"--style-disable", default="4,5",
help="comma-separated style rule IDs to drop, forwarded to "
"style_check.py --disable. posterly default '4,5' drops the "
"<=2-hue-cluster (R4) and no-gradient (R5) design-opinion rules "
"while keeping the operational discipline; pass '' to enforce "
"all 14.",
)
p.add_argument(
"--measure-budget", type=int, default=None,
help="forwarded to poster_check.py measure --measure-budget "
"(circuit breaker cap; measure's own default applies when "
"omitted; 0 disables)",
)
p.add_argument(
"--reset-measure-budget", action="store_true",
help="forwarded to poster_check.py measure --reset-budget "
"(zero the on-disk consecutive-failure counter first)",
)
return p
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
html_path = Path(args.html).resolve()
if not html_path.exists():
_eprint(f"ERROR: HTML not found: {ascii_safe(html_path)}")
return 2
report = run_all(html_path, args)
report_path = (
Path(args.report).resolve()
if args.report
else html_path.parent / "GATE_REPORT.json"
)
try:
report_path.write_text(
json.dumps(report, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
except OSError as exc:
_eprint(f"ERROR: cannot write report {ascii_safe(report_path)}: "
f"{ascii_safe(exc)}")
return 2
_print_human_summary(report)
print(f"[run_gates] report -> {ascii_safe(report_path)}")
return 0 if report["overall"] == "PASS" else 1
if __name__ == "__main__":
sys.exit(main())

Xet Storage Details

Size:
29.3 kB
·
Xet hash:
0aa2ed3a00c0950ff9c0cd6f48590ee28316f594bbcb262023e2a7e3ee23efbf

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.