feat: land superpaper v1 notes scaffold
Add the ledger schema, class router, lint codes, ingest/build pipeline, and three work-tree examples: align derivation, superfig delegation, and supertensor delegation.
This commit is contained in:
Executable
+61
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
# Lint, project the ledger, compile notes. Overfull is logged, not fatal.
|
||||
# ./scripts/build.sh --work <dir>
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
WORK=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--work|--out) WORK="$2"; shift 2 ;;
|
||||
*) echo "usage: build.sh --work DIR" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
[[ -n "$WORK" ]] || { echo "usage: build.sh --work DIR" >&2; exit 2; }
|
||||
WORK="$(cd "$WORK" && pwd)"
|
||||
NOTES="$WORK/notes"
|
||||
[[ -f "$WORK/ledger.yaml" ]] || { echo "no ledger.yaml in $WORK" >&2; exit 2; }
|
||||
[[ -f "$NOTES/notes.tex" ]] || { echo "no notes/notes.tex in $WORK" >&2; exit 2; }
|
||||
|
||||
echo "==> lint"
|
||||
"$ROOT/scripts/python" "$ROOT/scripts/lint.py" --work "$WORK"
|
||||
|
||||
echo "==> render ledger"
|
||||
"$ROOT/scripts/python" "$ROOT/scripts/render_ledger.py" --work "$WORK"
|
||||
|
||||
mkdir -p "$WORK/out"
|
||||
echo "==> xelatex"
|
||||
(
|
||||
cd "$NOTES"
|
||||
export TEXINPUTS="$ROOT/assets:$NOTES:"
|
||||
xelatex -halt-on-error -interaction=nonstopmode notes.tex >/dev/null
|
||||
xelatex -halt-on-error -interaction=nonstopmode notes.tex >/dev/null
|
||||
)
|
||||
|
||||
LOG="$NOTES/notes.log"
|
||||
status=0
|
||||
if grep -q "Missing character" "$LOG"; then
|
||||
echo "!! missing glyphs:" >&2
|
||||
grep -m5 "Missing character" "$LOG" >&2
|
||||
status=1
|
||||
fi
|
||||
if grep -qE "LaTeX Warning: (Reference|Citation|There were undefined references|Label\(s\) may have changed)" "$LOG"; then
|
||||
if grep -q "undefined" "$LOG"; then
|
||||
echo "!! undefined references:" >&2
|
||||
grep -m5 "undefined" "$LOG" >&2 || true
|
||||
status=1
|
||||
fi
|
||||
fi
|
||||
if grep -q "multiply-defined" "$LOG"; then
|
||||
echo "!! multiply-defined labels:" >&2
|
||||
grep -m5 "multiply-defined" "$LOG" >&2
|
||||
status=1
|
||||
fi
|
||||
if grep -qE "^(Overfull|Underfull) \\\\[hv]box" "$LOG"; then
|
||||
echo "==> overfull/underfull (recorded, not fatal)"
|
||||
grep -m5 -E "^(Overfull|Underfull) \\\\[hv]box" "$LOG" || true
|
||||
fi
|
||||
|
||||
cp -f "$NOTES/notes.pdf" "$WORK/out/notes.pdf"
|
||||
echo "==> $WORK/out/notes.pdf"
|
||||
exit $status
|
||||
Executable
+108
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env bash
|
||||
# Ingest a paper source into a --work tree. Never compiles e-print TeX.
|
||||
set -euo pipefail
|
||||
|
||||
WORK=""
|
||||
KIND=""
|
||||
SRC=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--work|--out) WORK="$2"; shift 2 ;;
|
||||
--arxiv) KIND=arxiv; SRC="$2"; shift 2 ;;
|
||||
--pdf) KIND=pdf; SRC="$2"; shift 2 ;;
|
||||
--tex) KIND=tex; SRC="$2"; shift 2 ;;
|
||||
--excerpt|--markdown) KIND=excerpt; SRC="$2"; shift 2 ;;
|
||||
*) echo "usage: ingest.sh --work DIR (--arxiv ID|--pdf FILE|--tex FILE|--excerpt FILE)" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
[[ -n "$WORK" && -n "$KIND" ]] || { echo "need --work and a source" >&2; exit 2; }
|
||||
|
||||
ARXIV_RE='^(ar[Xx]iv:)?([0-9]{4}\.[0-9]{4,5}(v[0-9]+)?|[a-z-]+/[0-9]{7})$'
|
||||
|
||||
mkdir -p "$WORK/source/pages" "$WORK/notes/sections" "$WORK/out"
|
||||
meta="$WORK/source/meta.yaml"
|
||||
|
||||
sha_of() { sha256sum "$1" | awk '{print $1}'; }
|
||||
|
||||
write_meta() {
|
||||
local extra="$1"
|
||||
cat > "$meta" <<EOF
|
||||
kind: $KIND
|
||||
$extra
|
||||
EOF
|
||||
}
|
||||
|
||||
case "$KIND" in
|
||||
excerpt)
|
||||
cp -- "$SRC" "$WORK/source/excerpt.md"
|
||||
write_meta "excerpt: source/excerpt.md"
|
||||
;;
|
||||
tex)
|
||||
mkdir -p "$WORK/source/tex"
|
||||
if [[ -d "$SRC" ]]; then
|
||||
cp -R -- "$SRC/." "$WORK/source/tex/"
|
||||
else
|
||||
cp -- "$SRC" "$WORK/source/tex/"
|
||||
fi
|
||||
write_meta "tex: source/tex"
|
||||
;;
|
||||
pdf)
|
||||
cp -- "$SRC" "$WORK/source/paper.pdf"
|
||||
pdftotext -layout "$WORK/source/paper.pdf" "$WORK/source/paper.txt" || true
|
||||
pdftoppm -png -r 120 "$WORK/source/paper.pdf" "$WORK/source/pages/pg"
|
||||
python3 - <<'PY' "$WORK/source/pages"
|
||||
from pathlib import Path
|
||||
import sys, re
|
||||
d = Path(sys.argv[1])
|
||||
files = sorted(d.glob("pg*.png"))
|
||||
for i, p in enumerate(files, 1):
|
||||
dest = d / f"pg-{i:03d}.png"
|
||||
if p.resolve() != dest.resolve():
|
||||
p.rename(dest)
|
||||
PY
|
||||
pages=$(find "$WORK/source/pages" -name 'pg-*.png' | wc -l)
|
||||
write_meta "local_pdf: source/paper.pdf
|
||||
sha256: $(sha_of "$WORK/source/paper.pdf")
|
||||
pages: $pages"
|
||||
;;
|
||||
arxiv)
|
||||
id="$SRC"
|
||||
[[ "$id" =~ $ARXIV_RE ]] || { echo "bad arxiv id: $id" >&2; exit 2; }
|
||||
id="${id#arxiv:}"; id="${id#arXiv:}"
|
||||
mkdir -p "$WORK/source"
|
||||
pdf="$WORK/source/paper.pdf"
|
||||
if [[ ! -f "$pdf" ]]; then
|
||||
curl -fsSL "https://arxiv.org/pdf/${id}.pdf" -o "$pdf" \
|
||||
|| curl -fsSL "https://export.arxiv.org/pdf/${id}.pdf" -o "$pdf"
|
||||
fi
|
||||
pdftotext -layout "$pdf" "$WORK/source/paper.txt" || true
|
||||
pdftoppm -png -r 120 "$pdf" "$WORK/source/pages/pg"
|
||||
python3 - <<'PY' "$WORK/source/pages"
|
||||
from pathlib import Path
|
||||
import sys
|
||||
d = Path(sys.argv[1])
|
||||
files = sorted(d.glob("pg*.png"))
|
||||
for i, p in enumerate(files, 1):
|
||||
dest = d / f"pg-{i:03d}.png"
|
||||
if p.resolve() != dest.resolve():
|
||||
p.rename(dest)
|
||||
PY
|
||||
# best-effort e-print; never compile
|
||||
if [[ ! -d "$WORK/source/eprint" ]]; then
|
||||
tmp=$(mktemp)
|
||||
if curl -fsSL "https://arxiv.org/e-print/${id}" -o "$tmp"; then
|
||||
mkdir -p "$WORK/source/eprint"
|
||||
tar -xf "$tmp" -C "$WORK/source/eprint" --max-size=50M 2>/dev/null \
|
||||
|| tar -xf "$tmp" -C "$WORK/source/eprint" || true
|
||||
fi
|
||||
rm -f "$tmp"
|
||||
fi
|
||||
pages=$(find "$WORK/source/pages" -name 'pg-*.png' | wc -l)
|
||||
write_meta "arxiv: $id
|
||||
local_pdf: source/paper.pdf
|
||||
sha256: $(sha_of "$pdf")
|
||||
pages: $pages"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "ingested $KIND -> $WORK"
|
||||
Executable
+243
@@ -0,0 +1,243 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Ledger and notes lint for superpaper. Codes live in DESIGN.md §10."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
from jsonschema import Draft7Validator
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
from router import SIBLING, check_row, phase_from_env # noqa: E402
|
||||
|
||||
SCHEMA_PATH = ROOT / "assets" / "ledger.schema.yaml"
|
||||
KNOWN_TOP = {
|
||||
"schema", "retired_ids", "paper", "coverage", "questions", "claims",
|
||||
"definitions", "assumptions", "lemmas", "symbols", "derivations",
|
||||
"figures", "evidence", "terms", "source_assets",
|
||||
}
|
||||
ID_GROUPS = (
|
||||
"questions", "claims", "definitions", "assumptions", "lemmas",
|
||||
"derivations", "figures", "evidence", "source_assets",
|
||||
)
|
||||
KIND_ALIASES = {
|
||||
"activation": "value",
|
||||
"value / activation": "value",
|
||||
"logit": "score",
|
||||
"score / logit": "score",
|
||||
"coordinate": "index",
|
||||
"index / coordinate": "index",
|
||||
"order": "rank",
|
||||
"rank / order": "rank",
|
||||
"support": "mask",
|
||||
"mask / support": "mask",
|
||||
"shape-parameter": "shape parameter",
|
||||
"shape_parameter": "shape parameter",
|
||||
}
|
||||
STY_RE = re.compile(r"\\usepackage(?:\[[^\]]*\])?\{(superfig|supertensor|superderive)\}")
|
||||
LABEL_RE = re.compile(r"\\splabel\{(C[1-9][0-9]*)\}")
|
||||
|
||||
|
||||
def emit(path: Path, messages: list[str]) -> None:
|
||||
print(f"!! {path}", file=sys.stderr)
|
||||
for msg in messages:
|
||||
print(f" {msg}", file=sys.stderr)
|
||||
|
||||
|
||||
def normalize_kinds(data: dict) -> dict:
|
||||
out = deepcopy(data)
|
||||
for sym in out.get("symbols") or []:
|
||||
if isinstance(sym, dict) and "kind" in sym:
|
||||
kind = sym["kind"]
|
||||
if kind in KIND_ALIASES:
|
||||
sym["kind"] = KIND_ALIASES[kind]
|
||||
return out
|
||||
|
||||
|
||||
def collect_ids(data: dict) -> list[str]:
|
||||
ids: list[str] = []
|
||||
for key in ID_GROUPS:
|
||||
for row in data.get(key) or []:
|
||||
if isinstance(row, dict) and "id" in row:
|
||||
ids.append(str(row["id"]))
|
||||
return ids
|
||||
|
||||
|
||||
def notes_text(notes_paths: list[Path]) -> str:
|
||||
chunks: list[str] = []
|
||||
for path in notes_paths:
|
||||
if path.is_file():
|
||||
chunks.append(path.read_text(encoding="utf-8"))
|
||||
return "\n".join(chunks)
|
||||
|
||||
|
||||
def find_notes(work: Path | None, notes: Path | None) -> list[Path]:
|
||||
found: list[Path] = []
|
||||
if notes is not None:
|
||||
found.append(notes)
|
||||
if work is not None:
|
||||
nd = work / "notes"
|
||||
if (nd / "notes.tex").is_file():
|
||||
found.append(nd / "notes.tex")
|
||||
found.extend(sorted(nd.glob("sections/*.tex")))
|
||||
# unique, keep order
|
||||
seen: set[Path] = set()
|
||||
uniq: list[Path] = []
|
||||
for p in found:
|
||||
rp = p.resolve()
|
||||
if rp not in seen:
|
||||
seen.add(rp)
|
||||
uniq.append(p)
|
||||
return uniq
|
||||
|
||||
|
||||
def lint(
|
||||
ledger_path: Path,
|
||||
*,
|
||||
work: Path | None,
|
||||
extra_notes: Path | None,
|
||||
disk: bool,
|
||||
notes_scan: bool,
|
||||
) -> tuple[list[str], list[str]]:
|
||||
errors: list[str] = []
|
||||
warnings: list[str] = []
|
||||
raw = yaml.safe_load(ledger_path.read_text(encoding="utf-8"))
|
||||
if not isinstance(raw, dict):
|
||||
return ["SP001 ledger is not a mapping"], warnings
|
||||
|
||||
data = normalize_kinds(raw)
|
||||
schema = yaml.safe_load(SCHEMA_PATH.read_text(encoding="utf-8"))
|
||||
validator = Draft7Validator(schema)
|
||||
for err in validator.iter_errors(data):
|
||||
loc = ".".join(str(p) for p in err.absolute_path) or "$"
|
||||
errors.append(f"SP001 {loc}: {err.message}")
|
||||
if errors:
|
||||
return errors, warnings
|
||||
|
||||
for key in data:
|
||||
if key not in KNOWN_TOP:
|
||||
warnings.append(f"unknown top-level key {key!r}")
|
||||
|
||||
coverage = data.get("coverage") or {}
|
||||
if coverage.get("mode") == "full" and not coverage.get("sections_in"):
|
||||
warnings.append("coverage.mode=full and sections_in is empty")
|
||||
|
||||
ids = collect_ids(data)
|
||||
seen: set[str] = set()
|
||||
for i in ids:
|
||||
if i in seen:
|
||||
errors.append(f"SP002 duplicate id {i}")
|
||||
seen.add(i)
|
||||
|
||||
retired = set(data.get("retired_ids") or [])
|
||||
for row_key in ID_GROUPS:
|
||||
for row in data.get(row_key) or []:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
rid = row.get("id")
|
||||
if rid in retired and row.get("status") != "dropped":
|
||||
errors.append(f"SP023 {rid} is in retired_ids")
|
||||
|
||||
claim_ids = {c["id"] for c in (data.get("claims") or []) if "id" in c}
|
||||
phase = phase_from_env()
|
||||
notes_paths = find_notes(work, extra_notes)
|
||||
body = notes_text(notes_paths) if notes_scan else ""
|
||||
|
||||
if notes_scan:
|
||||
for path in notes_paths:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
if STY_RE.search(text):
|
||||
errors.append(f"SP003 {path.name} loads a figure package")
|
||||
|
||||
labels = set(LABEL_RE.findall(body)) if notes_scan else set()
|
||||
|
||||
for fig in data.get("figures") or []:
|
||||
fid = fig.get("id", "F?")
|
||||
signals = set(fig.get("signals") or [])
|
||||
toolkit = fig.get("toolkit")
|
||||
status = fig.get("status", "planned")
|
||||
include = fig.get("include", None)
|
||||
code = check_row(
|
||||
signals, toolkit, status=status, include=include,
|
||||
fig_id=fid, phase=phase,
|
||||
)
|
||||
if code:
|
||||
errors.append(f"{code} {fid}")
|
||||
if toolkit == "superderive" and phase < 2:
|
||||
errors.append(f"SP022 {fid} toolkit superderive is v2; use align")
|
||||
if toolkit in SIBLING and status != "dropped" and disk:
|
||||
req = fig.get("request")
|
||||
req_path = (work / "notes" / req) if (work and req) else None
|
||||
if not req or req_path is None or not req_path.is_file():
|
||||
errors.append(f"SP012 {fid} missing request file")
|
||||
if include and disk and work is not None:
|
||||
on_disk = work / "notes" / include
|
||||
if not on_disk.is_file():
|
||||
errors.append(f"SP021 {fid} include not on disk: {include}")
|
||||
claim = fig.get("claim")
|
||||
if claim and claim not in claim_ids:
|
||||
errors.append(f"SP024 {fid} claim {claim} is not a known C*")
|
||||
|
||||
for der in data.get("derivations") or []:
|
||||
claim = der.get("claim")
|
||||
if claim and claim not in claim_ids:
|
||||
errors.append(f"SP024 {der.get('id')} claim {claim} is not a known C*")
|
||||
|
||||
if notes_scan:
|
||||
for claim in data.get("claims") or []:
|
||||
if claim.get("status") == "core" and claim.get("id") not in labels:
|
||||
errors.append(f"SP020 core {claim['id']} has no \\splabel")
|
||||
for sym in data.get("symbols") or []:
|
||||
name = str(sym.get("name", ""))
|
||||
latex = str(sym.get("latex", ""))
|
||||
if name and name not in body and latex and latex not in body:
|
||||
warnings.append(f"symbol {name} never appears in notes")
|
||||
|
||||
return errors, warnings
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="lint a superpaper ledger / notes tree")
|
||||
parser.add_argument("--work", help="work tree root (ledger.yaml + notes/)")
|
||||
parser.add_argument("--out", dest="work_alias", help="alias of --work")
|
||||
parser.add_argument("--ledger", help="ledger.yaml (skips disk/notes codes unless --notes)")
|
||||
parser.add_argument("--notes", help="extra notes.tex for SP003/SP020")
|
||||
args = parser.parse_args()
|
||||
|
||||
work = Path(args.work or args.work_alias).resolve() if (args.work or args.work_alias) else None
|
||||
if work is not None:
|
||||
ledger = work / "ledger.yaml"
|
||||
disk = True
|
||||
notes_scan = True
|
||||
elif args.ledger:
|
||||
ledger = Path(args.ledger).resolve()
|
||||
disk = False
|
||||
notes_scan = args.notes is not None
|
||||
else:
|
||||
parser.error("need --work or --ledger")
|
||||
return 2
|
||||
|
||||
extra = Path(args.notes).resolve() if args.notes else None
|
||||
if not ledger.is_file():
|
||||
print(f"!! {ledger}", file=sys.stderr)
|
||||
print(" SP000 no such ledger", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
errors, warnings = lint(ledger, work=work, extra_notes=extra, disk=disk, notes_scan=notes_scan)
|
||||
if errors:
|
||||
emit(ledger, errors)
|
||||
return 1
|
||||
for warn in warnings:
|
||||
print(f"warning: {warn}", file=sys.stderr)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
# Notes toolchain preflight. 0 = full, 1 = degraded, 2 = no LaTeX engine.
|
||||
set -uo pipefail
|
||||
|
||||
QUIET=0
|
||||
[[ "${1:-}" == "--quiet" ]] && QUIET=1
|
||||
say() { [[ $QUIET -eq 1 ]] || echo -e "$*"; }
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
ok=0; warn=0; fail=0
|
||||
check() {
|
||||
local name="$1"; shift
|
||||
if "$@" >/dev/null 2>&1; then say " ok $name"; ok=$((ok+1)); return 0
|
||||
else say " MISS $name"; return 1; fi
|
||||
}
|
||||
|
||||
say "superpaper preflight"
|
||||
say "--- engine ---"
|
||||
check "xelatex" command -v xelatex || fail=$((fail+1))
|
||||
check "article.cls" kpsewhich article.cls || fail=$((fail+1))
|
||||
|
||||
say "--- template / CJK ---"
|
||||
cjk=0
|
||||
for pkg in ctex.sty FandolSong-Regular.otf amsmath.sty amssymb.sty \
|
||||
tcolorbox.sty graphicx.sty hyperref.sty geometry.sty \
|
||||
listings.sty booktabs.sty subcaption.sty float.sty tikz.sty etoolbox.sty; do
|
||||
check "$pkg" kpsewhich "$pkg" || { warn=$((warn+1)); cjk=1; }
|
||||
done
|
||||
|
||||
say "--- python ---"
|
||||
PY="$ROOT/scripts/python"
|
||||
check "python3" command -v python3 || warn=$((warn+1))
|
||||
check "PyYAML" "$PY" -c "import yaml" || warn=$((warn+1))
|
||||
check "jsonschema" "$PY" -c "import jsonschema" || warn=$((warn+1))
|
||||
|
||||
say "--- ingest ---"
|
||||
check "pdftotext" command -v pdftotext || warn=$((warn+1))
|
||||
check "pdftoppm" command -v pdftoppm || warn=$((warn+1))
|
||||
|
||||
say "--- optional ---"
|
||||
check "magick" command -v magick || true
|
||||
check "pdftocairo" command -v pdftocairo || true
|
||||
|
||||
if [[ $fail -gt 0 ]]; then
|
||||
say ""
|
||||
say "RESULT: no LaTeX. Deliver ledger + Markdown; no PDF."
|
||||
exit 2
|
||||
fi
|
||||
if [[ $warn -gt 0 ]]; then
|
||||
say ""
|
||||
say "RESULT: degraded."
|
||||
[[ $cjk -eq 1 ]] && say " - missing CJK/template package -> English notes or later Missing character."
|
||||
exit 1
|
||||
fi
|
||||
say ""
|
||||
say "RESULT: full notes path (XeLaTeX + CJK + lint)."
|
||||
exit 0
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# Prefer the local venv so jsonschema is available without a system install.
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
if [[ -x "$ROOT/.venv/bin/python" ]]; then
|
||||
exec "$ROOT/.venv/bin/python" "$@"
|
||||
fi
|
||||
exec python3 "$@"
|
||||
Executable
+67
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Project ledger.yaml into notes/sections/symbols.tex and optional claims.md."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
def tex_escape(text: str) -> str:
|
||||
return (
|
||||
text.replace("\\", "\\textbackslash{}")
|
||||
.replace("&", "\\&")
|
||||
.replace("%", "\\%")
|
||||
.replace("#", "\\#")
|
||||
.replace("_", "\\_")
|
||||
)
|
||||
|
||||
|
||||
def render(work: Path) -> None:
|
||||
ledger = yaml.safe_load((work / "ledger.yaml").read_text(encoding="utf-8"))
|
||||
symbols = ledger.get("symbols") or []
|
||||
claims = ledger.get("claims") or []
|
||||
sec = work / "notes" / "sections"
|
||||
sec.mkdir(parents=True, exist_ok=True)
|
||||
lines = [
|
||||
"% generated by render_ledger.py — do not edit",
|
||||
"\\begin{itemize}",
|
||||
]
|
||||
if not symbols:
|
||||
lines.append("\\item (无符号)")
|
||||
for sym in symbols:
|
||||
name = tex_escape(str(sym.get("name", "")))
|
||||
meaning = tex_escape(str(sym.get("meaning", "")))
|
||||
kind = tex_escape(str(sym.get("kind", "")))
|
||||
latex = sym.get("latex") or name
|
||||
lines.append(f"\\item ${latex}$ — {meaning} ({kind};{name})")
|
||||
lines.append("\\end{itemize}")
|
||||
(sec / "symbols.tex").write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
md = ["# Claims", ""]
|
||||
for c in claims:
|
||||
md.append(f"- `{c.get('id')}` ({c.get('status')}): {c.get('text')}")
|
||||
(work / "claims.md").write_text("\n".join(md) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--work", required=False)
|
||||
parser.add_argument("--out", dest="work")
|
||||
args = parser.parse_args()
|
||||
if not args.work:
|
||||
print("usage: render_ledger.py --work <dir>", file=sys.stderr)
|
||||
return 2
|
||||
work = Path(args.work).resolve()
|
||||
if not (work / "ledger.yaml").is_file():
|
||||
print(f"no ledger at {work / 'ledger.yaml'}", file=sys.stderr)
|
||||
return 2
|
||||
render(work)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Executable
+100
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Figure-class router. suggest() returns a class, never a toolkit."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
NUMERIC = frozenset({"loss-curve", "bar", "scatter", "histogram", "numeric-plot"})
|
||||
RASTER = frozenset({"table", "photo", "apparatus", "ui"})
|
||||
TENSOR = frozenset({
|
||||
"axis", "shape", "transpose", "broadcast", "gather",
|
||||
"shard", "contraction", "face",
|
||||
})
|
||||
DERIVE = frozenset({"rewrite-figure", "cancel-visual", "subst-visual"})
|
||||
FIG = frozenset({
|
||||
"architecture", "pipeline", "data-flow", "state", "time",
|
||||
"dependency", "what-eats-what", "argument-map",
|
||||
})
|
||||
|
||||
CLASS_SIGNALS = {
|
||||
"numeric": NUMERIC,
|
||||
"raster": RASTER,
|
||||
"tensor": TENSOR,
|
||||
"derive": DERIVE,
|
||||
"fig": FIG,
|
||||
}
|
||||
|
||||
VECTOR = frozenset({"superfig", "supertensor", "superderive", "matplotlib"})
|
||||
SIBLING = frozenset({"superfig", "supertensor", "superderive"})
|
||||
|
||||
|
||||
def classify(signals: set[str]) -> list[str]:
|
||||
s = set(signals)
|
||||
hit = [c for c, vocab in CLASS_SIGNALS.items() if s & vocab]
|
||||
return hit or ["none"]
|
||||
|
||||
|
||||
def suggest(signals: set[str], *, phase: int = 1) -> str:
|
||||
"""Return a class name, or 'SPLIT:a+b+...' in CLASS_SIGNALS order."""
|
||||
classes = classify(signals)
|
||||
if len(classes) > 1:
|
||||
return "SPLIT:" + "+".join(classes)
|
||||
return classes[0]
|
||||
|
||||
|
||||
def accepted(cls: str, *, phase: int = 1) -> frozenset[str]:
|
||||
if cls == "numeric":
|
||||
return frozenset({"screenshot", "matplotlib"})
|
||||
if cls == "raster":
|
||||
return frozenset({"screenshot"})
|
||||
if cls == "tensor":
|
||||
return frozenset({"supertensor"})
|
||||
if cls == "fig":
|
||||
return frozenset({"superfig"})
|
||||
if cls == "derive":
|
||||
return frozenset({"align", "superderive"} if phase >= 2 else {"align"})
|
||||
if cls == "none":
|
||||
return frozenset({"none"})
|
||||
raise KeyError(cls)
|
||||
|
||||
|
||||
def include_pdf(fig_id: str) -> str:
|
||||
return f"figures/{fig_id}/build/{fig_id}.pdf"
|
||||
|
||||
|
||||
def include_png(fig_id: str) -> str:
|
||||
return f"figures/{fig_id}/orig.png"
|
||||
|
||||
|
||||
def check_row(
|
||||
signals: set[str],
|
||||
toolkit: str,
|
||||
*,
|
||||
status: str = "planned",
|
||||
include: str | None = None,
|
||||
fig_id: str = "F1",
|
||||
phase: int = 1,
|
||||
) -> str | None:
|
||||
"""First matching code, or None. SP010 is not an include/status rule."""
|
||||
classes = classify(signals)
|
||||
if len(classes) > 1:
|
||||
return "SP011"
|
||||
if toolkit not in accepted(classes[0], phase=phase):
|
||||
return "SP010"
|
||||
if toolkit == "none" and status != "dropped":
|
||||
return "SP013"
|
||||
if toolkit in {"align", "none"} and include is not None:
|
||||
return "SP014"
|
||||
if status == "included" and toolkit in VECTOR and include != include_pdf(fig_id):
|
||||
return "SP015"
|
||||
if status == "included" and toolkit == "screenshot" and include != include_png(fig_id):
|
||||
return "SP016"
|
||||
return None
|
||||
|
||||
|
||||
def phase_from_env() -> int:
|
||||
import os
|
||||
raw = os.environ.get("SUPERPAPER_PHASE", "1")
|
||||
try:
|
||||
return int(raw)
|
||||
except ValueError:
|
||||
return 1
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
# Raster one ledger figure from paper.pdf via pdftoppm. crop_bbox is 200 dpi page pixels.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
WORK=""
|
||||
FID=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--work|--out) WORK="$2"; shift 2 ;;
|
||||
--id) FID="$2"; shift 2 ;;
|
||||
*) echo "usage: screenshot.sh --work DIR --id F2" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
[[ -n "$WORK" && -n "$FID" ]] || { echo "need --work and --id" >&2; exit 2; }
|
||||
|
||||
"$ROOT/scripts/python" - "$WORK" "$FID" <<'PY'
|
||||
import sys, tempfile, subprocess, shutil
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
|
||||
work = Path(sys.argv[1])
|
||||
fid = sys.argv[2]
|
||||
ledger = yaml.safe_load((work / "ledger.yaml").read_text())
|
||||
fig = next((f for f in (ledger.get("figures") or []) if f.get("id") == fid), None)
|
||||
if fig is None:
|
||||
sys.exit(f"no figure {fid}")
|
||||
pages = fig.get("source_pages") or []
|
||||
if not pages:
|
||||
sys.exit("source_pages required")
|
||||
n = int(pages[0])
|
||||
pdf = work / "source" / "paper.pdf"
|
||||
if not pdf.is_file():
|
||||
sys.exit(f"missing {pdf}")
|
||||
dest_dir = work / "notes" / "figures" / fid
|
||||
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||
dest = dest_dir / "orig.png"
|
||||
bbox = fig.get("crop_bbox")
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
prefix = Path(td) / "pg"
|
||||
subprocess.run(
|
||||
["pdftoppm", "-png", "-r", "200", "-f", str(n), "-l", str(n), str(pdf), str(prefix)],
|
||||
check=True,
|
||||
)
|
||||
produced = sorted(Path(td).glob("pg*.png"))
|
||||
if not produced:
|
||||
sys.exit("pdftoppm produced no page")
|
||||
page = produced[0]
|
||||
if bbox is None:
|
||||
shutil.copy2(page, dest)
|
||||
else:
|
||||
if shutil.which("magick") is None:
|
||||
sys.exit("crop_bbox set but magick is missing")
|
||||
x0, y0, x1, y1 = bbox
|
||||
w, h = int(x1 - x0), int(y1 - y0)
|
||||
subprocess.run(
|
||||
["magick", str(page), "-crop", f"{w}x{h}+{int(x0)}+{int(y0)}", "+repage", str(dest)],
|
||||
check=True,
|
||||
)
|
||||
print(dest)
|
||||
PY
|
||||
Executable
+91
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env bash
|
||||
# Superpaper v1 checks: router unit tests, ledger codes, work-tree builds.
|
||||
set -uo pipefail
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
PY="$ROOT/scripts/python"
|
||||
LINT=("$PY" "$ROOT/scripts/lint.py")
|
||||
fail=0
|
||||
|
||||
ok() { echo " ok $*"; }
|
||||
bad() { echo " FAIL $*"; fail=$((fail+1)); }
|
||||
|
||||
echo "==> router_test"
|
||||
if "$PY" "$ROOT/tests/router_test.py"; then ok router_test.py; else bad router_test.py; fi
|
||||
|
||||
echo "==> ledger-valid"
|
||||
if "${LINT[@]}" --ledger "$ROOT/tests/ledger-valid.yaml" >/dev/null 2>&1; then
|
||||
ok ledger-valid.yaml
|
||||
else
|
||||
bad ledger-valid.yaml
|
||||
fi
|
||||
|
||||
expect_ledger() {
|
||||
local file="$1" code="$2"
|
||||
local out
|
||||
out=$("${LINT[@]}" --ledger "$file" 2>&1) || true
|
||||
if echo "$out" | grep -q "$code"; then
|
||||
ok "$(basename "$file") ($code)"
|
||||
else
|
||||
bad "$(basename "$file") (wanted $code)"
|
||||
echo "$out" | sed 's/^/ /'
|
||||
fi
|
||||
}
|
||||
|
||||
echo "==> ledger-invalid"
|
||||
for spec in \
|
||||
"missing-paper-id.yaml:SP001" \
|
||||
"bad-toolkit.yaml:SP001" \
|
||||
"duplicate-id.yaml:SP002" \
|
||||
"mixed-signals.yaml:SP011" \
|
||||
"toolkit-mismatch.yaml:SP010" \
|
||||
"none-not-dropped.yaml:SP013" \
|
||||
"align-has-include.yaml:SP014" \
|
||||
"included-null-include.yaml:SP015" \
|
||||
"screenshot-bad-include.yaml:SP016" \
|
||||
"superderive-v1.yaml:SP022" \
|
||||
"retired-reuse.yaml:SP023" \
|
||||
"dangling-claim.yaml:SP024"
|
||||
do
|
||||
f="${spec%%:*}"; c="${spec##*:}"
|
||||
expect_ledger "$ROOT/tests/ledger-invalid/$f" "$c"
|
||||
done
|
||||
|
||||
expect_work() {
|
||||
local dir="$1" code="$2"
|
||||
local out
|
||||
out=$("${LINT[@]}" --work "$dir" 2>&1) || true
|
||||
if echo "$out" | grep -q "$code"; then
|
||||
ok "$(basename "$dir") ($code)"
|
||||
else
|
||||
bad "$(basename "$dir") (wanted $code)"
|
||||
echo "$out" | sed 's/^/ /'
|
||||
fi
|
||||
}
|
||||
|
||||
echo "==> work-tree lint"
|
||||
expect_work "$ROOT/tests/loads-sty" SP003
|
||||
expect_work "$ROOT/tests/missing-request" SP012
|
||||
expect_work "$ROOT/tests/unlabeled-core" SP020
|
||||
expect_work "$ROOT/tests/missing-include" SP021
|
||||
|
||||
echo "==> compile"
|
||||
for dir in \
|
||||
"$ROOT/tests/notes-smoke" \
|
||||
"$ROOT/examples/excerpt-toy" \
|
||||
"$ROOT/examples/pipeline-delegate" \
|
||||
"$ROOT/examples/tensor-delegate"
|
||||
do
|
||||
name="$(basename "$dir")"
|
||||
if "$ROOT/scripts/build.sh" --work "$dir" >/dev/null 2>&1; then
|
||||
ok "$name (build)"
|
||||
else
|
||||
bad "$name (build)"
|
||||
"$ROOT/scripts/build.sh" --work "$dir" || true
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $fail -gt 0 ]]; then
|
||||
echo "$fail failing check(s)" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "all v1 checks passed"
|
||||
Reference in New Issue
Block a user