Merge pull request 'docs(vault): documentation exists for point of use — destroy the ledgers (−13.7k lines)' (#31) from docs/vault-destruction-20261019 into main
This commit was merged in pull request #31.
This commit is contained in:
Executable
+164
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Destroy measurement ledgers in the surviving pages.
|
||||
|
||||
Point of use (AGENTS.md → Documentation Policy): a rate/throughput figure for
|
||||
anything not running and not being compared is destroyed, not filed. This pass
|
||||
deletes
|
||||
|
||||
1. any section whose heading is a bench/throughput/rate/ladder/verdict ledger,
|
||||
2. any table row or standalone bullet that REPORTS a rate (`123.4 t/s`,
|
||||
`tok/s`, `ms/token`, `TTFT`, acceptance %, `llama-bench`, a bench arm),
|
||||
|
||||
and it never touches a shape number (ctx, slots, MiB budgets, ports) or a bullet
|
||||
under a *Laws* / *Wire contract* / *Rollback* heading, where the sentence is the
|
||||
payload and the number is decoration. Dry run by default; `--apply` writes.
|
||||
|
||||
Usage: axe-measurements.py [--apply] [--verbose]
|
||||
"""
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
VAULT = pathlib.Path("serverAppConfigs-docs")
|
||||
RATE = re.compile(
|
||||
r"\d[\d,.]*\s*(t/s|tok/s|tokens?/s|tok s|tps|ms/token|µs/token|GB/s|GiB/s)\b|"
|
||||
r"\b(llama-bench|bench (arm|ladder|window)|decode ladder|prefill ladder)\b|"
|
||||
r"\bacceptance (rate|of)\b|\bdraft_n\b|\bttft\b|\bServer-Timing\b",
|
||||
re.I,
|
||||
)
|
||||
# A heading is a ledger only when the noun LEDS it and nothing instructional
|
||||
# follows: `## Throughput` yes, `## 6. The context ladder (find the max bootable
|
||||
# window by measurement)` no — that one is the method, which is point-of-use.
|
||||
LEDGER_HEAD = re.compile(
|
||||
r"^#{2,4}\s+(?:\d+[.) ]\s*)?(bench(es|mark|marks)?|throughput|results?|timings?|"
|
||||
r"measured (figures|numbers|results|rates|rates)|rates|verdict|acceptance|ladder)"
|
||||
r"\b[^|]{0,40}$",
|
||||
re.I,
|
||||
)
|
||||
INSTRUCTIONAL = re.compile(r"how|why|law|policy|method|procedure|find|by measurement|execution|flip|checklist", re.I)
|
||||
PROTECTED = re.compile(
|
||||
r"^#{2,4}\s+.*(law|wire|rollback|contract|shape|eras|files|health|see also|related|"
|
||||
r"boot|preflight|environment)",
|
||||
re.I,
|
||||
)
|
||||
def blank_rate_cells(text):
|
||||
"""Kill throughput COLUMNS wherever they appear: a cell that reports a rate
|
||||
becomes an em-dash, so the row keeps whatever else it records (the payload
|
||||
name, the reason, the rollback) and loses only the dead number."""
|
||||
out = []
|
||||
for line in text.split("\n"):
|
||||
st = line.strip()
|
||||
if st.startswith("|") and st.endswith("|") and RATE.search(line):
|
||||
cells = line.split("|")
|
||||
for i, c in enumerate(cells):
|
||||
if RATE.search(c):
|
||||
cells[i] = " — "
|
||||
line = "|".join(cells)
|
||||
out.append(line)
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def collapse_eras(text):
|
||||
"""One Eras row per era is fleet law; the NUMBERS in the old rows are not.
|
||||
Keep the current era verbatim, fold everything older into one row that says
|
||||
the numbers were destroyed and where the rollback actually lives (git)."""
|
||||
lines = text.split("\n")
|
||||
out = []
|
||||
in_eras = False
|
||||
ncol = 0
|
||||
folded = 0
|
||||
for line in lines:
|
||||
if line.startswith("#"):
|
||||
in_eras = bool(re.match(r"^#{2,4}\s+.*eras\b", line, re.I))
|
||||
if folded:
|
||||
out.append("| " + " | ".join(
|
||||
[f"≤ 2026-09 — {folded} earlier eras: payloads, shapes and every measured "
|
||||
"number destroyed 2026-10-19 as obsolete"] + ["—"] * max(0, ncol - 1)))
|
||||
folded = 0
|
||||
out.append(line)
|
||||
continue
|
||||
st = line.strip()
|
||||
if in_eras and st.startswith("|"):
|
||||
cells = [c.strip() for c in st.strip("|").split("|")]
|
||||
if set("".join(cells)) <= set(":- "):
|
||||
ncol = len(cells)
|
||||
out.append(line)
|
||||
continue
|
||||
if ncol == 0:
|
||||
ncol = len(cells)
|
||||
if re.search(r"2026-1[01]|current|active|running", st, re.I):
|
||||
out.append(line)
|
||||
else:
|
||||
folded += 1
|
||||
continue
|
||||
if folded and not st.startswith("|"):
|
||||
out.append("| " + " | ".join(
|
||||
[f"≤ 2026-09 — {folded} earlier eras: payloads, shapes and every measured number "
|
||||
"destroyed 2026-10-19 as obsolete"] + ["—"] * max(0, ncol - 1)))
|
||||
folded = 0
|
||||
ncol = 0
|
||||
out.append(line)
|
||||
if folded:
|
||||
out.append("| " + " | ".join(
|
||||
[f"≤ 2026-09 — {folded} earlier eras destroyed 2026-10-19"] + ["—"] * max(0, ncol - 1)))
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def main(argv):
|
||||
apply = "--apply" in argv
|
||||
verbose = "--verbose" in argv
|
||||
total_lines = 0
|
||||
pages = 0
|
||||
for page in sorted(VAULT.rglob("*.md")):
|
||||
s = str(page)
|
||||
if ".AGENTTRASH" in s or ".obsidian" in s:
|
||||
continue
|
||||
text = page.read_text(errors="replace")
|
||||
before = len(text.split("\n"))
|
||||
if s.startswith("serverAppConfigs-docs/LLM/"):
|
||||
text = collapse_eras(blank_rate_cells(text))
|
||||
lines = text.split("\n")
|
||||
out = []
|
||||
skip = False
|
||||
under_protected = False
|
||||
cut = 0
|
||||
for line in lines:
|
||||
if line.startswith("#"):
|
||||
under_protected = bool(PROTECTED.search(line))
|
||||
skip = (bool(LEDGER_HEAD.match(line)) and not under_protected
|
||||
and not INSTRUCTIONAL.search(line))
|
||||
if skip:
|
||||
cut += 1
|
||||
continue
|
||||
if skip:
|
||||
cut += 1
|
||||
continue
|
||||
stripped = line.strip()
|
||||
out.append(line)
|
||||
if under_protected:
|
||||
# Eras rows are the rollback record, Law bullets are the payload:
|
||||
# the number is decoration inside them, and the row/bullet is not
|
||||
# a ledger entry. Leave the whole section alone.
|
||||
continue
|
||||
if stripped.startswith(("|", "-", "*")) and RATE.search(line):
|
||||
# Destroy only when the line EXISTS to report a number: a table
|
||||
# row, or a bullet whose verb is measurement.
|
||||
if stripped.startswith("|") or re.search(
|
||||
r"\b(measured|measures|bench|benched|ran at|runs at|clocked|hit|"
|
||||
r"landed at|came in at|peak|peaks at|sustained|wall.clock)\b", line, re.I):
|
||||
out.pop()
|
||||
cut += 1
|
||||
final = re.sub(r"\n{3,}", "\n\n", "\n".join(out))
|
||||
saved = before - len(final.split("\n"))
|
||||
if saved or cut:
|
||||
pages += 1
|
||||
total_lines += max(saved, cut)
|
||||
print(f" {s.replace('serverAppConfigs-docs/', '')}: -{max(saved, cut)}")
|
||||
if apply:
|
||||
page.write_text(final.rstrip("\n") + "\n")
|
||||
print(f"{'APPLIED' if apply else 'dry run'}: {pages} pages, {total_lines} lines of ledger")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
Executable
+293
@@ -0,0 +1,293 @@
|
||||
#!/usr/bin/env python3
|
||||
"""OKF v0.1 conformance linter for a docs bundle (default serverAppConfigs-docs).
|
||||
|
||||
Checks the hard rules of the Open Knowledge Format spec plus the maintenance
|
||||
guidance, so "conforms to OKF" is a test and not a claim:
|
||||
|
||||
FRONTMATTER every non-reserved .md has parseable YAML frontmatter with a
|
||||
non-empty `type` from the spec's value set
|
||||
TYPE the value is one of reference / howto / tutorial / explanation
|
||||
DESC `description` is present, non-blank, not the title, <= 120 chars,
|
||||
and does not restate a fleet quantity
|
||||
TIMESTAMP `timestamp` present, ISO 8601, not in the future
|
||||
LENGTH lines between 1 and 500 inclusive
|
||||
INDEX-FM no index.md may carry frontmatter (index.md is reserved)
|
||||
LOG-DATE log.md date headings are `## YYYY-MM-DD` or `## YYYY-MM-DD — Title`,
|
||||
one entry per date, newest first
|
||||
INDEX-COVER every index.md links each of its sibling pages; the root index
|
||||
links every page in the bundle
|
||||
INDEX-NUM an index table row restates a fleet quantity — a digit run of
|
||||
three or more, a digits-plus-unit figure, a port, or a
|
||||
multiplier. Names (Qwen3.6, RDNA4, RTX 3090, PICO-8, P4) are
|
||||
not quantities and are allowed
|
||||
CITATIONS informational: pages with factual claims and no Citations
|
||||
section are counted, never failed
|
||||
|
||||
LENGTH lines between 1 and 500 inclusive. A page may opt out of the
|
||||
ceiling with `okf-exempt: length` in its frontmatter: AGENTS.md
|
||||
makes one page per endpoint permanent, and an endpoint page with
|
||||
a full Eras table cannot split without creating a second owner
|
||||
of the same shape (tests/docs_completeness.bats enforces that)
|
||||
|
||||
Exit 0 when there are no violations.
|
||||
"""
|
||||
import datetime
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
TYPES = {"reference", "howto", "tutorial", "explanation"}
|
||||
RESERVED = {"index.md", "log.md", "AGENTS.md"}
|
||||
|
||||
# A quantity, as opposed to a name: a comma-grouped figure, a run of 3+ digits,
|
||||
# a unit-bearing figure, a port (":7770"), or a multiplier ("2x", "3 *").
|
||||
QUANTITY = re.compile(
|
||||
r"\d{1,3}(?:,\d{3})+" # 262,144
|
||||
r"|\d{3,}" # 200000, 7770, 3090
|
||||
r"|\d+(?:\.\d+)?\s*(?:GiB|MiB|GB|MB|TB|tok/s|t/s|x\b|%)"
|
||||
r"|[:×]\s*\d"
|
||||
r"|\d+\s*[x×]\s*\d"
|
||||
)
|
||||
# Product / model / standard names that legitimately contain digits.
|
||||
NAMES = re.compile(
|
||||
r"Qwen[\d.]*[-\w]*|Gemma\s?\d[\w.]*|MiMo[-\w.\d.]*|Ornith[-\w.\d]*"
|
||||
r"|Bonsai[-\w.\d]*|Ling[-\w.\d.]*|DeepSeek[-\w.\d]*|GLM[-\w.\d]*"
|
||||
r"|PICO-\d+|RDNA\s?\d+|R\d{4}|RTX\s?\d+|XDNA\d|gfx\d+|CUDA\s?[\d.]+"
|
||||
r"|\d+\s?GbE|Wi-?Fi\s?\d|v?\d+\.\d+(?:\.\d+)?|b\d+|P\d\b|A\d+\b"
|
||||
r"|frank-\w*2026-\d\d-\d\d|2026-\d\d-\d\d",
|
||||
re.I,
|
||||
)
|
||||
DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}")
|
||||
FM_KEY = re.compile(r"^([A-Za-z][\w-]*):\s*(.*)$")
|
||||
|
||||
|
||||
def split_frontmatter(text):
|
||||
if not text.startswith("---"):
|
||||
return None, text
|
||||
end = text.find("\n---", 3)
|
||||
if end < 0:
|
||||
return None, text
|
||||
body_start = text.find("\n", end + 1)
|
||||
return text[3:end], text[body_start + 1 :] if body_start >= 0 else ""
|
||||
|
||||
|
||||
def fields(raw):
|
||||
out = {}
|
||||
if not raw:
|
||||
return out
|
||||
key = None
|
||||
for line in raw.splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
m = FM_KEY.match(line)
|
||||
if m:
|
||||
key, val = m.group(1), m.group(2).strip().strip("\"'")
|
||||
out[key] = val
|
||||
elif line.startswith(" ") and key:
|
||||
out[key] = (out[key] + " " + line.strip()).strip()
|
||||
return out
|
||||
|
||||
|
||||
def strip_names(value):
|
||||
return NAMES.sub(" ", value)
|
||||
|
||||
|
||||
def check_file(path: Path, root: Path, length_exempt=frozenset()):
|
||||
v = []
|
||||
rel = str(path.relative_to(root))
|
||||
text = path.read_text(errors="replace")
|
||||
lines = text.splitlines()
|
||||
name = path.name
|
||||
raw, body = split_frontmatter(text)
|
||||
fm = fields(raw)
|
||||
|
||||
if name not in RESERVED:
|
||||
if raw is None:
|
||||
v.append(("FRONTMATTER", rel, "no parseable frontmatter"))
|
||||
else:
|
||||
t = fm.get("type", "")
|
||||
if not t:
|
||||
v.append(("TYPE", rel, "missing type"))
|
||||
elif t not in TYPES:
|
||||
v.append(("TYPE", rel, f"type '{t}' not in {sorted(TYPES)}"))
|
||||
d = fm.get("description", "")
|
||||
if not d:
|
||||
v.append(("DESC", rel, "missing description"))
|
||||
else:
|
||||
if len(d) > 120:
|
||||
v.append(("DESC", rel, f"description is {len(d)} chars (max 120)"))
|
||||
if d.strip("# ").lower() == fm.get("title", "\0").strip("# ").lower():
|
||||
v.append(("DESC", rel, "description equals title"))
|
||||
if QUANTITY.search(strip_names(d)):
|
||||
v.append(("DESC", rel, "description restates a quantity"))
|
||||
ts = fm.get("timestamp", "")
|
||||
if not ts:
|
||||
v.append(("TIMESTAMP", rel, "missing timestamp"))
|
||||
else:
|
||||
try:
|
||||
stamp = datetime.date.fromisoformat(ts[:10])
|
||||
if stamp > datetime.date.today():
|
||||
v.append(("TIMESTAMP", rel, f"timestamp {ts} is in the future"))
|
||||
except ValueError:
|
||||
v.append(("TIMESTAMP", rel, f"unparseable timestamp {ts!r}"))
|
||||
|
||||
if name == "index.md" and raw is not None:
|
||||
v.append(("INDEX-FM", rel, "index.md must not carry frontmatter"))
|
||||
|
||||
if name == "log.md":
|
||||
dates = []
|
||||
for line in lines:
|
||||
m = re.match(r"^##\s+(\S.*?)\s*$", line)
|
||||
if not m:
|
||||
continue
|
||||
head = m.group(1)
|
||||
if not DATE_RE.match(head):
|
||||
continue
|
||||
dates.append(head[:10])
|
||||
# OKF requires the heading to USE the date format; a title after an
|
||||
# em-dash is allowed and costs zero lines, so `## 2026-08-17 — Why the
|
||||
# pair was rebuilt` is conformant and `## 2026-08-17Rebuilt` is not.
|
||||
if head != head[:10] and not re.match(r"^\d{4}-\d{2}-\d{2}\s+—\s+\S", head):
|
||||
v.append(("LOG-DATE", rel,
|
||||
f"date heading must be the date alone or `date — Title`: '## {head}'"))
|
||||
if len(dates) != len(set(dates)):
|
||||
v.append(("LOG-DATE", rel, "duplicate date heading"))
|
||||
if dates != sorted(dates, reverse=True):
|
||||
v.append(("LOG-DATE", rel, "date headings not newest first"))
|
||||
|
||||
if raw is not None and name not in RESERVED:
|
||||
n = len(lines)
|
||||
body_lines = len([ln for ln in body.splitlines() if ln.strip()])
|
||||
opted_out = "length" in fm.get("okf-exempt", "").lower()
|
||||
if n > 500:
|
||||
if rel in length_exempt:
|
||||
v.append(("LENGTH-OK", rel,
|
||||
f"{n} lines — over the hint, pinned by the "
|
||||
"one-page-per-endpoint law"))
|
||||
elif opted_out:
|
||||
v.append(("LENGTH-OK", rel,
|
||||
f"{n} lines — over the hint, opted out by the page"))
|
||||
else:
|
||||
v.append(("LENGTH", rel, f"{n} lines (max 500)"))
|
||||
elif body_lines < 15:
|
||||
v.append(("LENGTH", rel, "stub: fewer than 15 body lines"))
|
||||
|
||||
# A second `type:` key inside the body means the page carries two copies of
|
||||
# itself with a frontmatter block in the middle — the shape a naive
|
||||
# frontmatter rewrite produces. It doubles the length, splits the wikilinks
|
||||
# and makes the page's facts ambiguous, so it is a hard failure.
|
||||
if raw is not None and name not in RESERVED:
|
||||
dupes = [i + len((raw or "").splitlines()) + 2
|
||||
for i, ln in enumerate(body.splitlines())
|
||||
if re.match(r"^(type|title|description):", ln)]
|
||||
if dupes:
|
||||
v.append(("DOUBLED", rel,
|
||||
f"frontmatter keys inside the body at line(s) "
|
||||
f"{', '.join(str(d) for d in dupes[:5])} — the page "
|
||||
"contains a duplicated copy of itself"))
|
||||
|
||||
return v
|
||||
|
||||
|
||||
def index_rows(text):
|
||||
rows = []
|
||||
for line in text.splitlines():
|
||||
if line.startswith("|") and "[[" in line:
|
||||
rows.append(line)
|
||||
return rows
|
||||
|
||||
|
||||
def check_links(root: Path):
|
||||
v = []
|
||||
pages = [
|
||||
p for p in sorted(root.rglob("*.md"))
|
||||
if ".AGENTTRASH" not in str(p) and ".obsidian" not in str(p)
|
||||
]
|
||||
for index in [p for p in pages if p.name == "index.md"]:
|
||||
text = index.read_text(errors="replace")
|
||||
links = set(re.findall(r"\[\[([^\]|#]+)", text))
|
||||
if index.parent == root:
|
||||
targets = [p for p in pages if p.name not in RESERVED]
|
||||
else:
|
||||
targets = [p for p in index.parent.glob("*.md") if p.name != "index.md"]
|
||||
for target in targets:
|
||||
if target.stem not in links and target.name not in links:
|
||||
v.append(("INDEX-COVER", str(index.relative_to(root)),
|
||||
f"does not link sibling {target.name}"))
|
||||
for index in [p for p in pages if p.name == "index.md"]:
|
||||
for row in index_rows(index.read_text(errors="replace")):
|
||||
if QUANTITY.search(strip_names(row)):
|
||||
v.append(("INDEX-NUM", str(index.relative_to(root)),
|
||||
"row restates a quantity: " + row[:80]))
|
||||
return v
|
||||
|
||||
|
||||
def live_endpoints(root: Path):
|
||||
"""Pages pinned by AGENTS.md's one-page-per-endpoint law.
|
||||
|
||||
Derived, never hardcoded — the same derivation as
|
||||
tests/docs_completeness.bats: an endpoint is live when scripts/ holds both
|
||||
its env and its unit. Those pages carry the Eras table for their whole
|
||||
history, so the OKF 500-line split hint cannot apply to them without
|
||||
creating a second owner of the same shape.
|
||||
"""
|
||||
scripts = root.parent / "scripts"
|
||||
out = set()
|
||||
if scripts.is_dir():
|
||||
for envf in scripts.glob("frank-*.env"):
|
||||
if (scripts / f"{envf.stem}.service").exists():
|
||||
out.add(f"LLM/{envf.stem}.md")
|
||||
return out
|
||||
|
||||
|
||||
def retired_era_pages(root: Path):
|
||||
"""Era pages of a retired endpoint: campaign ledgers, one owner each.
|
||||
|
||||
AGENTS.md lets a campaign ledger that exists nowhere else live on the page
|
||||
that measured it, and a retired endpoint keeps exactly one page per era.
|
||||
Splitting such a ledger to satisfy the 500-line hint would fragment one
|
||||
campaign across two owners, so the tag opts the page out.
|
||||
"""
|
||||
out = set()
|
||||
for path in root.rglob("*.md"):
|
||||
s = str(path)
|
||||
if ".AGENTTRASH" in s or ".obsidian" in s:
|
||||
continue
|
||||
raw, _ = split_frontmatter(path.read_text(errors="replace"))
|
||||
if raw is not None and "retired-endpoint" in fields(raw).get("tags", ""):
|
||||
out.add(str(path.relative_to(root)))
|
||||
return out
|
||||
|
||||
|
||||
def main(argv):
|
||||
root = Path(argv[1] if len(argv) > 1 else "serverAppConfigs-docs")
|
||||
exempt = frozenset(live_endpoints(root)) | retired_era_pages(root)
|
||||
violations = []
|
||||
notes = []
|
||||
citation_gaps = 0
|
||||
pages = 0
|
||||
for path in sorted(root.rglob("*.md")):
|
||||
s = str(path)
|
||||
if ".AGENTTRASH" in s or ".obsidian" in s:
|
||||
continue
|
||||
pages += 1
|
||||
for code, rel, detail in check_file(path, root, exempt):
|
||||
(notes if code == "LENGTH-OK" else violations).append((code, rel, detail))
|
||||
text = path.read_text(errors="replace")
|
||||
if len(text.splitlines()) > 40 and not re.search(
|
||||
r"^##\s+(Citations|Sources)\b", text, re.M):
|
||||
citation_gaps += 1
|
||||
violations += check_links(root)
|
||||
|
||||
for code, rel, detail in violations:
|
||||
print(f" {code:12s} {rel}: {detail}")
|
||||
for code, rel, detail in notes:
|
||||
print(f" {code:12s} {rel}: {detail}")
|
||||
print(f"{pages} concept pages, {len(violations)} OKF violations"
|
||||
f" ({citation_gaps} pages without a Citations section — soft;"
|
||||
f" {len(notes)} length exemptions honoured)")
|
||||
return 1 if violations else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
@@ -31,7 +31,12 @@ def strip_md(path: str) -> str:
|
||||
|
||||
|
||||
def build_index(vault: pathlib.Path):
|
||||
pages = sorted(vault.rglob("*.md"))
|
||||
# `.AGENTTRASH` is deleted-but-held state, not vault content: it must not be
|
||||
# scanned (its relative links resolve against the trash dir, not the vault)
|
||||
# and must not answer as a link target — a live page pointing into the trash
|
||||
# is an unresolved link, not a resolved one.
|
||||
pages = sorted(p for p in vault.rglob("*.md")
|
||||
if ".AGENTTRASH" not in str(p))
|
||||
stems: dict[str, list[pathlib.Path]] = {}
|
||||
for page in pages:
|
||||
stems.setdefault(page.stem.casefold(), []).append(page)
|
||||
|
||||
Executable
+109
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Destroy the vault's dead weight: era ledgers, retired runtimes, dead experiments.
|
||||
|
||||
`AGENTS.md` was treating a measurement as a record worth keeping forever. It is not: a
|
||||
throughput figure for a payload that no longer runs, on a runtime that no longer exists,
|
||||
is a liability — the next agent spends context on a number that AI progress has already
|
||||
obsolete. This pass moves those pages to `.AGENTTRASH/` (repo law: never `rm`) and then
|
||||
deletes every reference to them from the surviving pages, including the sentence that
|
||||
carried the link when the link was the sentence's only content.
|
||||
|
||||
Usage: destroy-vault.py <list-file>
|
||||
List file: one vault-relative path per line; a directory path destroys the whole tree.
|
||||
"""
|
||||
import pathlib
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
VAULT = pathlib.Path("serverAppConfigs-docs")
|
||||
TRASH = VAULT / ".AGENTTRASH" / "20261019-destruction"
|
||||
|
||||
|
||||
def targets(list_file):
|
||||
out = []
|
||||
for line in pathlib.Path(list_file).read_text().splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
p = VAULT / line
|
||||
if p.is_dir():
|
||||
out += [f for f in sorted(p.rglob("*.md"))]
|
||||
elif p.is_file():
|
||||
out.append(p)
|
||||
else:
|
||||
print(f" SKIP (missing): {line}")
|
||||
return out
|
||||
|
||||
|
||||
def main(argv):
|
||||
if len(argv) < 2:
|
||||
print(__doc__)
|
||||
return 2
|
||||
doomed = targets(argv[1])
|
||||
# Stems come from the trash too, so re-running the pass (or running it after a
|
||||
# partial destruction) still knows which names are dead and keeps scrubbing.
|
||||
stems = {p.stem for p in doomed}
|
||||
if TRASH.is_dir():
|
||||
stems |= {p.stem for p in TRASH.rglob("*.md")}
|
||||
moved = 0
|
||||
for p in doomed:
|
||||
dest = TRASH / p.relative_to(VAULT)
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.move(str(p), str(dest))
|
||||
moved += 1
|
||||
print(f"destroyed {moved} pages ({sum(len(p.read_text(errors='replace').splitlines()) for p in TRASH.rglob('*.md'))} lines staged in trash)")
|
||||
|
||||
# Repoint-or-delete: a link to a destroyed page is itself dead information.
|
||||
link_wl = re.compile(r"\[\[([^\]|#]+)(?:#[^\]|]*)?(?:\|([^\]]*))?\]\]")
|
||||
link_md = re.compile(r"\[[^\]]*\]\(([^)#]*\.md)(?:#[^)]*)?\)")
|
||||
pages_cleaned = links_dropped = lines_dropped = 0
|
||||
for page in sorted(VAULT.rglob("*.md")):
|
||||
s = str(page)
|
||||
if ".AGENTTRASH" in s or ".obsidian" in s:
|
||||
continue
|
||||
text = page.read_text(errors="replace")
|
||||
lines = text.split("\n")
|
||||
out = []
|
||||
hits = 0
|
||||
|
||||
def dead(target):
|
||||
# NOT pathlib.Path(...).stem: that strips a real suffix off names like
|
||||
# `Lemonade and sd.cpp`, so a link to a destroyed page survives and the
|
||||
# vault then fails its own link check.
|
||||
t = target.strip()
|
||||
if t.endswith(".md"):
|
||||
t = t[:-3]
|
||||
return t in stems or pathlib.PurePosixPath(t).name in stems
|
||||
|
||||
for line in lines:
|
||||
found = [t for t in link_wl.findall(line) if dead(t[0])]
|
||||
for m in link_md.finditer(line):
|
||||
if dead(m.group(1)):
|
||||
found.append(m.group(1))
|
||||
if not found:
|
||||
out.append(line)
|
||||
continue
|
||||
hits += len(found)
|
||||
stripped = link_wl.sub(
|
||||
lambda m: m.group(2) if (m.group(2) and not dead(m.group(1))) else "",
|
||||
line)
|
||||
stripped = link_md.sub(
|
||||
lambda m: "" if dead(m.group(1)) else m.group(0), stripped)
|
||||
# What is left of the line after the dead links are gone?
|
||||
residue = re.sub(r"[\s\-*•·|:;,.\u2014\u2013]+", "", stripped)
|
||||
if residue:
|
||||
out.append(re.sub(r"\s+$", "", re.sub(r"^(\s*[-*]\s*)[\s:;,.\u2014-]+", r"\1", stripped)))
|
||||
else:
|
||||
lines_dropped += 1
|
||||
if hits:
|
||||
page.write_text("\n".join(out))
|
||||
pages_cleaned += 1
|
||||
links_dropped += hits
|
||||
print(f"cleaned {pages_cleaned} surviving pages: {links_dropped} dead links dropped, "
|
||||
f"{lines_dropped} lines left empty and deleted")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
Executable
+211
@@ -0,0 +1,211 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regenerate serverAppConfigs-docs/index.md from page frontmatter.
|
||||
|
||||
The root index is a locator (AGENTS.md -> *Documentation Policy*): one row per
|
||||
page, one clause, no numbers. Hand-maintaining it is how rows go missing, and
|
||||
the docs seam fails when a page is not linked from it, so it is generated:
|
||||
|
||||
* preamble everything above the first `## ` in the existing file is kept
|
||||
verbatim (that is the curated statement of the vault's law)
|
||||
* section from the page's directory, overridden by its frontmatter tags
|
||||
(`endpoint` / `retired-endpoint` split the LLM pages)
|
||||
* row text the page's frontmatter `description`, verbatim
|
||||
|
||||
A page with no description is a hard error rather than an empty cell, so the
|
||||
generator cannot silently produce a silent index. Sections with no pages are
|
||||
skipped. Deterministic: the same vault always yields the same bytes.
|
||||
|
||||
Sub-directory indices are hand-grouped (they carry curated prose), so they are
|
||||
synced rather than regenerated: dead rows dropped, row text refreshed from the
|
||||
page's description, uncovered siblings appended.
|
||||
"""
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
RESERVED = {"index.md", "log.md"}
|
||||
SECTIONS = [
|
||||
("Inference endpoints", "endpoint"),
|
||||
("Retired endpoints", "retired-endpoint"),
|
||||
]
|
||||
DIRECTORY_SECTIONS = [
|
||||
("Dankclaw", "Dankclaw services"),
|
||||
("Dankserver", "Dankserver services"),
|
||||
("FrankBook-Pro", "FrankBook-Pro"),
|
||||
("Desktop", "Workstations"),
|
||||
("Infrastructure", "Cross-host infrastructure"),
|
||||
("Architecture", "Repo architecture and policy"),
|
||||
("Operations", "Operations and incidents"),
|
||||
("Agent", "Agent / harness"),
|
||||
("Research", "Research and methodology"),
|
||||
]
|
||||
PIPELINE_SECTION = "Pipeline Parallelism (series)"
|
||||
LLM_FALLBACK = "LLM technique and hardware references"
|
||||
CHANGE_SECTION = "Change history"
|
||||
ORDER = [t for t, _ in SECTIONS] + [t for _, t in DIRECTORY_SECTIONS] + \
|
||||
[PIPELINE_SECTION, LLM_FALLBACK, CHANGE_SECTION]
|
||||
|
||||
|
||||
def frontmatter_fields(text):
|
||||
if not text.startswith("---"):
|
||||
return {}
|
||||
end = text.find("\n---", 3)
|
||||
if end == -1:
|
||||
return {}
|
||||
raw = text[3:end]
|
||||
fields = {}
|
||||
for key in ("description", "type"):
|
||||
m = re.search(rf"^{key}:\s*(.*)$", raw, re.M)
|
||||
if m:
|
||||
fields[key] = m.group(1).strip().strip("\"'")
|
||||
tags = []
|
||||
m = re.search(r"^tags:\s*\[(.*?)\]$", raw, re.M)
|
||||
if m:
|
||||
tags = [t.strip().strip("\"'") for t in m.group(1).split(",") if t.strip()]
|
||||
elif re.search(r"^tags:\s*$", raw, re.M):
|
||||
block = re.search(r"^tags:\s*\n((?:\s+-\s+.*\n?)+)", raw, re.M)
|
||||
if block:
|
||||
tags = [l.strip().lstrip("-").strip().strip("\"'")
|
||||
for l in block.group(1).splitlines() if l.strip()]
|
||||
fields["tags"] = tags
|
||||
return fields
|
||||
|
||||
|
||||
def section_for(page, vault, fields):
|
||||
rel = page.relative_to(vault)
|
||||
if len(rel.parts) == 1:
|
||||
return CHANGE_SECTION if page.name == "log.md" else "Uncategorised"
|
||||
top = rel.parts[0]
|
||||
if top == "LLM":
|
||||
if PIPELINE_SECTION_DIR in rel.parts:
|
||||
return PIPELINE_SECTION
|
||||
for title, tag in SECTIONS:
|
||||
if tag in fields.get("tags", []):
|
||||
return title
|
||||
return LLM_FALLBACK
|
||||
for prefix, title in DIRECTORY_SECTIONS:
|
||||
if top == prefix:
|
||||
return title
|
||||
return "Uncategorised"
|
||||
|
||||
|
||||
ROW = re.compile(r"^(\| )\[\[([^\]|]+)\]\]( \|)( .*?)( \|$)")
|
||||
BULLET = re.compile(r"^(- )\[\[([^\]]+)\]\]( — )(.*?)$")
|
||||
|
||||
|
||||
def sync_subindex(index: pathlib.Path, vault: pathlib.Path):
|
||||
"""Keep a hand-grouped sub-index honest without erasing its grouping.
|
||||
|
||||
Rows are locators, so their text is the linked page's own description; a
|
||||
row whose page is gone is a dead promise and is dropped; a direct sibling
|
||||
with no row is appended under `## Pages` (that is what the link-integrity
|
||||
seam checks). Curated headings and prose are untouched.
|
||||
"""
|
||||
here = index.parent
|
||||
known = {p.stem for p in here.rglob("*.md")
|
||||
if ".AGENTTRASH" not in str(p) and p.name != "index.md"}
|
||||
lines = index.read_text().split("\n")
|
||||
out, dropped, refreshed = [], 0, 0
|
||||
for line in lines:
|
||||
# A row whose first cell is empty is a description with no page: the link
|
||||
# was deleted when the page was destroyed, so the row is a dead promise.
|
||||
if re.match(r"^\|\s*\|", line.strip()):
|
||||
dropped += 1
|
||||
continue
|
||||
for pat, wrap in ((ROW, "| [[{}]] |{} {} |"), (BULLET, "- [[{}]] — {}")):
|
||||
m = pat.match(line)
|
||||
if not m:
|
||||
continue
|
||||
stem = m.group(2).strip()
|
||||
if stem not in known:
|
||||
dropped += 1
|
||||
line = None
|
||||
break
|
||||
cand = next((p for p in here.rglob(f"{stem}.md")
|
||||
if ".AGENTTRASH" not in str(p)), None)
|
||||
desc = frontmatter_fields(cand.read_text(errors="replace")).get("description") \
|
||||
if cand else None
|
||||
if desc:
|
||||
current = m.group(4).strip()
|
||||
if current != desc:
|
||||
line = wrap.format(stem, " " if pat is ROW else "", desc) \
|
||||
if pat is ROW else wrap.format(stem, desc)
|
||||
refreshed += 1
|
||||
break
|
||||
if line is not None:
|
||||
out.append(line)
|
||||
text = "\n".join(out)
|
||||
# Presence must be the wikilink itself: a bare stem is a substring of other
|
||||
# page names (`frank-medium` lives inside `frank-medium-rocmfpx`), and a
|
||||
# sibling matched that way silently never gets a row.
|
||||
missing = sorted(
|
||||
(p.stem, frontmatter_fields(p.read_text(errors="replace")).get("description", ""))
|
||||
for p in here.glob("*.md")
|
||||
if p.name != "index.md" and f"[[{p.stem}]]" not in text
|
||||
)
|
||||
if missing:
|
||||
block = ["", "## Pages", "", "| Page | Owns |", "|---|---|"]
|
||||
block += [f"| [[{stem}]] | {desc} |" for stem, desc in missing]
|
||||
text = text.rstrip("\n") + "\n" + "\n".join(block) + "\n"
|
||||
if text != index.read_text():
|
||||
index.write_text(text)
|
||||
return dropped, refreshed, len(missing)
|
||||
|
||||
|
||||
PIPELINE_SECTION_DIR = "Pipeline Parallelism"
|
||||
|
||||
|
||||
def main(argv):
|
||||
vault = pathlib.Path(argv[1] if len(argv) > 1 else "serverAppConfigs-docs")
|
||||
index = vault / "index.md"
|
||||
if not index.is_file():
|
||||
print(f"missing {index}")
|
||||
return 2
|
||||
existing = index.read_text()
|
||||
preamble = existing.split("\n## ", 1)[0].rstrip("\n")
|
||||
|
||||
pages = [p for p in vault.rglob("*.md")
|
||||
if ".AGENTTRASH" not in str(p) and ".obsidian" not in str(p)
|
||||
and p.name != "index.md" and p != index]
|
||||
buckets, errors = {}, []
|
||||
for page in pages:
|
||||
fields = frontmatter_fields(page.read_text(errors="replace"))
|
||||
if page.name != "log.md":
|
||||
desc = fields.get("description")
|
||||
if not desc:
|
||||
errors.append(str(page.relative_to(vault)))
|
||||
continue
|
||||
else:
|
||||
desc = "Chronological incident and law log for the whole bundle."
|
||||
buckets.setdefault(section_for(page, vault, fields), []).append((page.stem, desc))
|
||||
if errors:
|
||||
print("pages with no description (fix the frontmatter, not the index):")
|
||||
for e in sorted(errors):
|
||||
print(" " + e)
|
||||
return 1
|
||||
|
||||
out = [preamble, ""]
|
||||
for title in ORDER + sorted(set(buckets) - set(ORDER)):
|
||||
rows = sorted(buckets.get(title, []), key=lambda r: r[0].lower())
|
||||
if not rows:
|
||||
continue
|
||||
out += [f"## {title}", "", "| Page | Owns |", "|---|---|"]
|
||||
out += [f"| [[{stem}]] | {desc} |" for stem, desc in rows]
|
||||
out.append("")
|
||||
out.append("<!-- generated by scripts/gen-vault-index.py; edit page frontmatter instead -->")
|
||||
index.write_text("\n".join(out) + "\n")
|
||||
touched = 0
|
||||
for sub in sorted(vault.rglob("index.md")):
|
||||
if sub == index or ".AGENTTRASH" in str(sub):
|
||||
continue
|
||||
dropped, refreshed, added = sync_subindex(sub, vault)
|
||||
if dropped or refreshed or added:
|
||||
touched += 1
|
||||
print(f" {sub}: -{dropped} dead rows, {refreshed} refreshed, +{added} added")
|
||||
print(f"{index}: {sum(len(v) for v in buckets.values())} rows in "
|
||||
f"{len([t for t in ORDER if buckets.get(t)])} sections, {touched} sub-indices synced")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
Executable
+100
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Port census: prove a merge/collapse lost no fact.
|
||||
|
||||
For every line that existed in the OLD set and not in the NEW set, check that
|
||||
each of its significant tokens (numbers, long identifiers, paths) still occurs
|
||||
somewhere in the NEW corpus. A line whose every token survives is a reworded
|
||||
line; a line with an unheld token is a candidate fact loss and gets printed.
|
||||
|
||||
port-census.py <new-glob-dir> <old-file> [<old-file> ...]
|
||||
|
||||
Old files may be `git-rev:path` (read from git) or a path (e.g. a trash copy).
|
||||
"""
|
||||
import pathlib
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def toks(line):
|
||||
out = []
|
||||
for n in re.findall(r"\d[\d,]*(?:\.\d+)?", line):
|
||||
if len(n.replace(",", "").split(".")[0]) >= 3 or (
|
||||
"." in n and len(n.split(".")[-1]) >= 2 and len(n.replace(",", "")) >= 4
|
||||
):
|
||||
out.append(n)
|
||||
# Identifiers, flags, paths and backticked spans are the facts worth
|
||||
# tracking; ordinary prose words are not — they reword on every rewrite.
|
||||
# A hyphen alone is not evidence of an identifier (`long-standing`), so a
|
||||
# dashed word counts only with two hyphens (`frank-tts-engine`) or with a
|
||||
# digit, dot, slash, colon or `--` inside it.
|
||||
for w in re.findall(r"[A-Za-z][\w./:-]{7,}", line):
|
||||
core = w.strip(".,;:!?\"')]")
|
||||
if (core.startswith("--") or core.isupper()
|
||||
or re.search(r"[\d_/:.]", core) or core.count("-") >= 2):
|
||||
out.append(core)
|
||||
out += [s.strip() for s in re.findall(r"`([^`]+)`", line) if len(s) > 3]
|
||||
return out
|
||||
|
||||
|
||||
def held(tok, corpus):
|
||||
t = tok.replace(",", "")
|
||||
alt = rf"\b{re.escape(t)}\b"
|
||||
for c in corpus:
|
||||
if tok in c or t in c:
|
||||
return True
|
||||
if re.search(alt, c.replace(",", "")):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def main(argv):
|
||||
new_dir = pathlib.Path(argv[1])
|
||||
|
||||
def corpus_excluding(exclude):
|
||||
"""Every vault page except the one under test.
|
||||
|
||||
The corpus is what must still hold a fact AFTER the file is gone, so a
|
||||
file that lives inside the scanned directory has to be removed from it —
|
||||
otherwise every token is "held" by the file itself and the census proves
|
||||
nothing (it reports 0 losses for every page, which is the bug this
|
||||
guards).
|
||||
"""
|
||||
out = []
|
||||
for p in new_dir.rglob("*.md"):
|
||||
s = str(p)
|
||||
if ".AGENTTRASH" in s or ".obsidian" in s:
|
||||
continue
|
||||
if exclude is not None and p.resolve() == exclude:
|
||||
continue
|
||||
out.append(p.read_text(errors="replace"))
|
||||
return out
|
||||
|
||||
lost = checked = 0
|
||||
for spec in argv[2:]:
|
||||
if spec.startswith("git-"):
|
||||
ref, path = spec[4:].split(":", 1)
|
||||
old = subprocess.run(["git", "show", f"{ref}:{path}"],
|
||||
capture_output=True, text=True).stdout
|
||||
label = f"{ref}:{path}"
|
||||
exclude = None
|
||||
else:
|
||||
old = pathlib.Path(spec).read_text(errors="replace")
|
||||
label = spec
|
||||
exclude = pathlib.Path(spec).resolve()
|
||||
blob = "\n".join(corpus_excluding(exclude))
|
||||
for line in old.splitlines():
|
||||
s = line.strip().strip("->*|# ").strip()
|
||||
if len(s) < 30 or s.startswith("```"):
|
||||
continue
|
||||
checked += 1
|
||||
missing = [t for t in toks(s) if not held(t, [blob])]
|
||||
if missing:
|
||||
lost += 1
|
||||
print(f" {label}\n L: {s[:150]}\n unheld: {', '.join(missing[:6])}")
|
||||
print(f"{checked} old lines checked, {lost} with unheld tokens")
|
||||
return 1 if lost else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
Executable
+56
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Repair the two artifacts the destruction passes introduced.
|
||||
|
||||
1. The link stripper could eat the space after a `- ` marker (`-`X``) and leave a
|
||||
dangling `see .` where a wikilink used to be.
|
||||
2. The Eras collapse could swallow a table's HEADER row (it looked like just
|
||||
another non-current row), leaving a separator with no header above it.
|
||||
|
||||
Usage: repair-damage.py [--apply]
|
||||
"""
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
VAULT = pathlib.Path("serverAppConfigs-docs")
|
||||
ERAS_HEADER = "| Window | Payload | Runtime | Shape | Replaced because | Rollback |"
|
||||
|
||||
|
||||
def main(argv):
|
||||
apply = "--apply" in argv
|
||||
marker = dangling = header = 0
|
||||
for p in sorted(VAULT.rglob("*.md")):
|
||||
s = str(p)
|
||||
if ".AGENTTRASH" in s or ".obsidian" in s:
|
||||
continue
|
||||
lines = p.read_text(errors="replace").split("\n")
|
||||
out = []
|
||||
# Only a pointer whose target was deleted reads as `… see .` — never touch
|
||||
# a line merely because it wraps on a comma. (An earlier version of this
|
||||
# rule stripped the trailing comma off 660 wrapped prose lines, which
|
||||
# `scripts/restore-punctuation.py` then had to undo from HEAD.)
|
||||
for line in lines:
|
||||
new = line
|
||||
if re.match(r"^(\s*)[-*](?![-* ])[$`[(*_]", new):
|
||||
new = re.sub(r"^(\s*)[-*]", r"\1- ", new)
|
||||
marker += 1
|
||||
if re.search(r"\s(?:see|also|details in|via|per)\s*\.?\s*$", new):
|
||||
new = re.sub(r"\s(?:see|also|details in|via|per)\s*\.?\s*$", "", new)
|
||||
dangling += 1
|
||||
out.append(new)
|
||||
# A separator row whose line above is not a header gets its header back.
|
||||
fixed = []
|
||||
for i, line in enumerate(out):
|
||||
if re.match(r"^\|-+", line.strip()) and (not fixed or not fixed[-1].strip().startswith("|")):
|
||||
fixed.append(ERAS_HEADER)
|
||||
header += 1
|
||||
fixed.append(line)
|
||||
if apply and "".join(fixed) != "".join(lines):
|
||||
p.write_text("\n".join(fixed))
|
||||
print(f"{'APPLIED' if apply else 'dry run'}: marker spacing {marker}, "
|
||||
f"dangling pointers {dangling}, restored table headers {header}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
Executable
+66
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Undo trailing-punctuation damage by restoring lines that match HEAD exactly
|
||||
except for a stripped trailing `,` / `;` / removed trailing word.
|
||||
|
||||
The repair pass over-reached: its "dangling pointer" cleanup also matched every
|
||||
wrapped prose line ending in a comma, and deleted the comma. A line whose text
|
||||
is otherwise byte-identical to its HEAD version, differing only by trailing
|
||||
punctuation, is not an edit — it is damage, so HEAD's line goes back.
|
||||
|
||||
Usage: restore-punctuation.py [--apply]
|
||||
"""
|
||||
import pathlib
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
REPO = pathlib.Path(".")
|
||||
VAULT = REPO / "serverAppConfigs-docs"
|
||||
|
||||
|
||||
def head_text(rel):
|
||||
r = subprocess.run(["git", "show", f"HEAD:{rel}"], capture_output=True, text=True)
|
||||
return r.stdout if r.returncode == 0 else None
|
||||
|
||||
|
||||
def main(argv):
|
||||
apply = "--apply" in argv
|
||||
restored = 0
|
||||
pages = 0
|
||||
for p in sorted(VAULT.rglob("*.md")):
|
||||
s = str(p)
|
||||
if ".AGENTTRASH" in s or ".obsidian" in s:
|
||||
continue
|
||||
rel = s
|
||||
head = head_text(rel)
|
||||
if head is None:
|
||||
continue
|
||||
cur = p.read_text(errors="replace").split("\n")
|
||||
# Key on (length, prefix) of the HEAD line minus trailing punctuation so a
|
||||
# damaged line can find its original.
|
||||
index = {}
|
||||
for hl in head.split("\n"):
|
||||
h = hl.rstrip()
|
||||
if h.endswith((",", ";")) or True:
|
||||
key = (len(h.rstrip(",;. ")), h.rstrip(",;. ")[:60])
|
||||
index.setdefault(key, h)
|
||||
touched = 0
|
||||
for i, cl in enumerate(cur):
|
||||
c = cl.rstrip()
|
||||
if not c or c.endswith((".", ":", "|", "`", ")", "," , ";")):
|
||||
continue
|
||||
key = (len(c), c[:60])
|
||||
hl = index.get(key)
|
||||
if hl and hl != c and hl.startswith(c):
|
||||
cur[i] = hl
|
||||
touched += 1
|
||||
if touched:
|
||||
pages += 1
|
||||
restored += touched
|
||||
if apply:
|
||||
p.write_text("\n".join(cur))
|
||||
print(f"{'APPLIED' if apply else 'dry run'}: {restored} lines restored across {pages} pages")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
Reference in New Issue
Block a user