0.0.2-alpha.4
This commit is contained in:
96
scripts/audit_markdown_headers.py
Normal file
96
scripts/audit_markdown_headers.py
Normal file
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
# file: scripts/audit_markdown_headers.py
|
||||
# version: 1
|
||||
|
||||
"""Validate path and positive revision in selected Markdown file headers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
_IGNORED_DIRECTORIES = frozenset({".git", ".idea", ".venv", "__pycache__", "dist", "node_modules", "target", "builds"})
|
||||
_VERSION = re.compile(r"<!-- version: [1-9][0-9]* -->")
|
||||
|
||||
|
||||
def _selected_files(raw_paths: list[str], root: pathlib.Path) -> tuple[list[pathlib.Path], list[str]]:
|
||||
files: set[pathlib.Path] = set()
|
||||
errors: list[str] = []
|
||||
|
||||
for raw_path in raw_paths:
|
||||
path = pathlib.Path(raw_path)
|
||||
if path.is_dir() or path.is_file():
|
||||
if not path.resolve().is_relative_to(root):
|
||||
errors.append(f"Markdown header audit: path outside project root: {path}")
|
||||
continue
|
||||
if path.is_dir():
|
||||
candidates = (
|
||||
candidate
|
||||
for candidate in path.rglob("*.md")
|
||||
if candidate.is_file() and not any(part in _IGNORED_DIRECTORIES for part in candidate.parts)
|
||||
)
|
||||
elif path.is_file() and path.suffix.lower() == ".md":
|
||||
candidates = iter((path,))
|
||||
else:
|
||||
errors.append(f"Markdown header audit: unsupported or missing path: {path}")
|
||||
continue
|
||||
|
||||
for candidate in candidates:
|
||||
canonical = candidate.resolve()
|
||||
if not canonical.is_relative_to(root):
|
||||
errors.append(f"Markdown header audit: path outside project root: {candidate}")
|
||||
else:
|
||||
files.add(canonical)
|
||||
|
||||
return sorted(files), errors
|
||||
|
||||
|
||||
def _audit_file(path: pathlib.Path, root: pathlib.Path) -> list[str]:
|
||||
relative = path.relative_to(root).as_posix()
|
||||
lines = path.read_text(encoding="utf-8").splitlines()
|
||||
errors: list[str] = []
|
||||
expected_file = f"<!-- file: {relative} -->"
|
||||
|
||||
if not lines or lines[0] != expected_file:
|
||||
errors.append(f"{relative}:1: expected `{expected_file}`")
|
||||
if len(lines) < 2 or _VERSION.fullmatch(lines[1]) is None:
|
||||
errors.append(f"{relative}:2: expected positive `<!-- version: N -->`")
|
||||
return errors
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Run the read-only Markdown header audit from the project root."""
|
||||
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("paths", nargs="+", help="Markdown files or directories to audit")
|
||||
args = parser.parse_args()
|
||||
|
||||
root = pathlib.Path.cwd().resolve()
|
||||
files, selection_errors = _selected_files(args.paths, root)
|
||||
if selection_errors or not files:
|
||||
for error in selection_errors or ["Markdown header audit: no Markdown files selected"]:
|
||||
print(error, file=sys.stderr)
|
||||
return 2
|
||||
|
||||
errors: list[str] = []
|
||||
for path in files:
|
||||
try:
|
||||
errors.extend(_audit_file(path, root))
|
||||
except (OSError, UnicodeError) as error:
|
||||
print(f"Markdown header audit: cannot read {path}: {error}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
for error in errors:
|
||||
print(error, file=sys.stderr)
|
||||
if errors:
|
||||
print(f"Markdown header audit: {len(errors)} error(s) across {len(files)} file(s)", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print(f"Markdown header audit: clean ({len(files)} file(s))")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
131
scripts/audit_markdown_links.py
Normal file
131
scripts/audit_markdown_links.py
Normal file
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env python3
|
||||
# file: scripts/audit_markdown_links.py
|
||||
# version: 1
|
||||
|
||||
"""Validate that local inline Markdown link targets exist."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
import urllib.parse
|
||||
|
||||
_IGNORED_DIRECTORIES = frozenset({".git", ".idea", ".venv", "__pycache__", "dist", "node_modules", "target", "builds"})
|
||||
_INLINE_LINK = re.compile(r"(?<!!)\[[^\]]+\]\(([^)]+)\)")
|
||||
|
||||
|
||||
def _selected_files(raw_paths: list[str]) -> tuple[list[pathlib.Path], list[str]]:
|
||||
files: list[pathlib.Path] = []
|
||||
errors: list[str] = []
|
||||
|
||||
for raw_path in raw_paths:
|
||||
path = pathlib.Path(raw_path)
|
||||
if path.is_dir():
|
||||
files.extend(
|
||||
candidate
|
||||
for candidate in path.rglob("*.md")
|
||||
if candidate.is_file() and not any(part in _IGNORED_DIRECTORIES for part in candidate.parts)
|
||||
)
|
||||
elif path.is_file() and path.suffix.lower() == ".md":
|
||||
files.append(path)
|
||||
else:
|
||||
errors.append(f"Markdown link audit: unsupported or missing path: {path}")
|
||||
|
||||
return sorted(set(files)), errors
|
||||
|
||||
|
||||
def _destination(raw_destination: str) -> str:
|
||||
destination = raw_destination.strip()
|
||||
|
||||
if destination.startswith("<"):
|
||||
closing = destination.find(">")
|
||||
return destination[1:closing] if closing > 0 else destination
|
||||
|
||||
return destination.split(maxsplit=1)[0] if destination else ""
|
||||
|
||||
|
||||
def _local_target(destination: str) -> str | None:
|
||||
if not destination or destination.startswith("#") or destination.startswith("//"):
|
||||
return None
|
||||
|
||||
parsed = urllib.parse.urlsplit(destination)
|
||||
if parsed.scheme or parsed.netloc:
|
||||
return None
|
||||
|
||||
target = urllib.parse.unquote(parsed.path)
|
||||
return target or None
|
||||
|
||||
|
||||
def _audit_file(path: pathlib.Path) -> tuple[int, list[str]]:
|
||||
errors: list[str] = []
|
||||
checked = 0
|
||||
fence: str | None = None
|
||||
|
||||
for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
|
||||
stripped = line.lstrip()
|
||||
if stripped.startswith("```") or stripped.startswith("~~~"):
|
||||
marker = stripped[:3]
|
||||
if fence is None:
|
||||
fence = marker
|
||||
elif fence == marker:
|
||||
fence = None
|
||||
continue
|
||||
|
||||
if fence is not None:
|
||||
continue
|
||||
|
||||
for match in _INLINE_LINK.finditer(line):
|
||||
destination = _destination(match.group(1))
|
||||
target = _local_target(destination)
|
||||
if target is None:
|
||||
continue
|
||||
|
||||
checked += 1
|
||||
if target.startswith("/"):
|
||||
candidate = pathlib.Path.cwd() / target.lstrip("/")
|
||||
else:
|
||||
candidate = path.parent / target
|
||||
|
||||
if not candidate.exists():
|
||||
errors.append(f"{path}:{line_number}: missing local Markdown target: {destination}")
|
||||
|
||||
return checked, errors
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Run the read-only local Markdown link audit."""
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("paths", nargs="+", help="Markdown files or directories to audit")
|
||||
arguments = parser.parse_args()
|
||||
|
||||
files, selection_errors = _selected_files(arguments.paths)
|
||||
if selection_errors:
|
||||
for error in selection_errors:
|
||||
print(error, file=sys.stderr)
|
||||
return 2
|
||||
if not files:
|
||||
print("Markdown link audit: no Markdown files selected", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
link_count = 0
|
||||
errors: list[str] = []
|
||||
for path in files:
|
||||
current_count, current_errors = _audit_file(path)
|
||||
link_count += current_count
|
||||
errors.extend(current_errors)
|
||||
|
||||
if errors:
|
||||
for error in errors:
|
||||
print(error, file=sys.stderr)
|
||||
print(f"Markdown link audit: {len(errors)} error(s) across {len(files)} file(s)", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print(f"Markdown link audit: clean ({link_count} local link(s), {len(files)} file(s))")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
216
scripts/audit_markdown_tables.py
Normal file
216
scripts/audit_markdown_tables.py
Normal file
@@ -0,0 +1,216 @@
|
||||
#!/usr/bin/env python3
|
||||
# file: scripts/audit_markdown_tables.py
|
||||
# version: 2
|
||||
|
||||
"""Validate source formatting of Markdown tables in explicitly selected paths."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
_SEPARATOR = re.compile(r"^:?-{3,}:?$")
|
||||
_IGNORED_DIRECTORIES = frozenset({".git", ".idea", ".venv", "__pycache__", "dist", "node_modules", "target", "builds"})
|
||||
_MARKDOWN_FENCE_LANGUAGES = frozenset({"markdown", "md"})
|
||||
|
||||
|
||||
def _selected_files(raw_paths: list[str]) -> tuple[list[pathlib.Path], list[str]]:
|
||||
files: list[pathlib.Path] = []
|
||||
errors: list[str] = []
|
||||
|
||||
for raw_path in raw_paths:
|
||||
path = pathlib.Path(raw_path)
|
||||
if path.is_dir():
|
||||
files.extend(
|
||||
candidate
|
||||
for candidate in path.rglob("*.md")
|
||||
if candidate.is_file() and not any(part in _IGNORED_DIRECTORIES for part in candidate.parts)
|
||||
)
|
||||
elif path.is_file() and path.suffix.lower() == ".md":
|
||||
files.append(path)
|
||||
else:
|
||||
errors.append(f"Markdown table audit: unsupported or missing path: {path}")
|
||||
|
||||
return sorted(set(files)), errors
|
||||
|
||||
|
||||
def _is_row(line: str) -> bool:
|
||||
return line.startswith("|") and line.endswith("|")
|
||||
|
||||
|
||||
def _cells(line: str) -> list[str]:
|
||||
return line.split("|")[1:-1]
|
||||
|
||||
|
||||
def _separator_marker(cell: str) -> str | None:
|
||||
marker = cell.strip()
|
||||
return marker if _SEPARATOR.fullmatch(marker) is not None else None
|
||||
|
||||
|
||||
def _is_separator_row(line: str) -> bool:
|
||||
if not _is_row(line):
|
||||
return False
|
||||
cells = _cells(line)
|
||||
return bool(cells) and all(_separator_marker(cell) is not None for cell in cells)
|
||||
|
||||
|
||||
def _alignment(marker: str) -> str:
|
||||
if marker.startswith(":") and marker.endswith(":"):
|
||||
return "center"
|
||||
if marker.endswith(":"):
|
||||
return "right"
|
||||
return "left"
|
||||
|
||||
|
||||
def _padding(cell: str) -> tuple[int, int]:
|
||||
return len(cell) - len(cell.lstrip(" ")), len(cell) - len(cell.rstrip(" "))
|
||||
|
||||
|
||||
def _validate_table(path: pathlib.Path, start_line: int, rows: list[str]) -> list[str]:
|
||||
errors: list[str] = []
|
||||
|
||||
if any("\\|" in row for row in rows):
|
||||
errors.append(f"{path}:{start_line}: escaped pipe is forbidden inside a table cell")
|
||||
|
||||
parsed = [_cells(row) for row in rows]
|
||||
column_count = len(parsed[0])
|
||||
|
||||
for offset, row in enumerate(parsed):
|
||||
if len(row) != column_count:
|
||||
errors.append(f"{path}:{start_line + offset}: table row has {len(row)} columns; expected {column_count}")
|
||||
return errors
|
||||
|
||||
for column in range(column_count):
|
||||
raw_cells = [row[column] for row in parsed]
|
||||
widths = [len(cell) for cell in raw_cells]
|
||||
expected_width = widths[0]
|
||||
|
||||
if any(width != expected_width for width in widths):
|
||||
errors.append(f"{path}:{start_line}: column {column + 1} is not vertically aligned; raw widths are {widths}")
|
||||
continue
|
||||
|
||||
marker = _separator_marker(raw_cells[1])
|
||||
if marker is None:
|
||||
errors.append(f"{path}:{start_line + 1}: separator for column {column + 1} is malformed")
|
||||
continue
|
||||
|
||||
content_cells = [cell for index, cell in enumerate(raw_cells) if index != 1]
|
||||
longest_content = max(len(cell.strip()) for cell in content_cells)
|
||||
required_width = max(longest_content + 2, 3 + marker.count(":"))
|
||||
|
||||
if expected_width != required_width:
|
||||
errors.append(
|
||||
f"{path}:{start_line}: column {column + 1} width is {expected_width}; expected {required_width}"
|
||||
)
|
||||
continue
|
||||
|
||||
alignment = _alignment(marker)
|
||||
for cell in content_cells:
|
||||
content_width = len(cell.strip())
|
||||
left, right = _padding(cell)
|
||||
expected_padding = expected_width - content_width
|
||||
|
||||
if content_width + left + right != len(cell) or left < 1 or right < 1:
|
||||
errors.append(f"{path}:{start_line}: column {column + 1} must use spaces only for outer padding")
|
||||
break
|
||||
if alignment == "left" and (left != 1 or right != expected_padding - 1):
|
||||
errors.append(f"{path}:{start_line}: column {column + 1} must be source-aligned to the left")
|
||||
break
|
||||
if alignment == "right" and (right != 1 or left != expected_padding - 1):
|
||||
errors.append(f"{path}:{start_line}: column {column + 1} must be source-aligned to the right")
|
||||
break
|
||||
if alignment == "center" and abs(left - right) > 1:
|
||||
errors.append(f"{path}:{start_line}: column {column + 1} must be source-aligned to the center")
|
||||
break
|
||||
|
||||
if len(raw_cells[1]) != expected_width or _SEPARATOR.fullmatch(raw_cells[1]) is None:
|
||||
errors.append(f"{path}:{start_line + 1}: separator for column {column + 1} must fill the column width")
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def _fence_language(stripped: str) -> str | None:
|
||||
info = stripped[3:].strip()
|
||||
if not info:
|
||||
return None
|
||||
return info.split(None, 1)[0].lower()
|
||||
|
||||
|
||||
def _audit_file(path: pathlib.Path) -> tuple[int, list[str]]:
|
||||
lines = path.read_text(encoding="utf-8").splitlines()
|
||||
errors: list[str] = []
|
||||
tables = 0
|
||||
fence: str | None = None
|
||||
audit_fenced_markdown = False
|
||||
index = 0
|
||||
|
||||
while index < len(lines):
|
||||
stripped = lines[index].lstrip()
|
||||
if stripped.startswith("```") or stripped.startswith("~~~"):
|
||||
marker = stripped[:3]
|
||||
if fence is None:
|
||||
fence = marker
|
||||
audit_fenced_markdown = _fence_language(stripped) in _MARKDOWN_FENCE_LANGUAGES
|
||||
elif fence == marker:
|
||||
fence = None
|
||||
audit_fenced_markdown = False
|
||||
index += 1
|
||||
continue
|
||||
|
||||
if fence is not None and not audit_fenced_markdown:
|
||||
index += 1
|
||||
continue
|
||||
|
||||
if index + 1 < len(lines) and _is_row(lines[index]) and _is_separator_row(lines[index + 1]):
|
||||
start = index
|
||||
rows = [lines[index], lines[index + 1]]
|
||||
index += 2
|
||||
while index < len(lines) and _is_row(lines[index]):
|
||||
rows.append(lines[index])
|
||||
index += 1
|
||||
tables += 1
|
||||
errors.extend(_validate_table(path, start + 1, rows))
|
||||
continue
|
||||
|
||||
index += 1
|
||||
|
||||
return tables, errors
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Run the read-only Markdown table audit."""
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("paths", nargs="+", help="Markdown files or directories to audit")
|
||||
arguments = parser.parse_args()
|
||||
|
||||
files, selection_errors = _selected_files(arguments.paths)
|
||||
if selection_errors:
|
||||
for error in selection_errors:
|
||||
print(error, file=sys.stderr)
|
||||
return 2
|
||||
if not files:
|
||||
print("Markdown table audit: no Markdown files selected", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
table_count = 0
|
||||
errors: list[str] = []
|
||||
for path in files:
|
||||
current_count, current_errors = _audit_file(path)
|
||||
table_count += current_count
|
||||
errors.extend(current_errors)
|
||||
|
||||
if errors:
|
||||
for error in errors:
|
||||
print(error, file=sys.stderr)
|
||||
print(f"Markdown table audit: {len(errors)} error(s) across {len(files)} file(s)", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print(f"Markdown table audit: clean ({table_count} table(s), {len(files)} file(s))")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
78
scripts/audit_rust_crate_root_facades.py
Normal file
78
scripts/audit_rust_crate_root_facades.py
Normal file
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
# file: scripts/audit_rust_crate_root_facades.py
|
||||
# version: 1
|
||||
|
||||
"""Audit Rust library crate roots so `src/lib.rs` remains a facade."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
from _rust_audit_common import line_depths, mask_source
|
||||
|
||||
_ITEM = re.compile(
|
||||
r"^\s*(?:(?:pub(?:\([^)]*\))?)\s+)?"
|
||||
r"(?:(?:async|const|unsafe)\s+)*(?:extern\s+\"[^\"]+\"\s+)?"
|
||||
r"(?:fn|struct|enum|union|trait|impl|type|const|static|macro_rules!)\b"
|
||||
)
|
||||
|
||||
|
||||
def _selected_lib_roots(raw_paths: list[str]) -> tuple[list[pathlib.Path], list[str]]:
|
||||
roots: set[pathlib.Path] = set()
|
||||
errors: list[str] = []
|
||||
for raw_path in raw_paths:
|
||||
path = pathlib.Path(raw_path)
|
||||
if path.is_dir():
|
||||
roots.update(candidate for candidate in path.rglob("src/lib.rs") if candidate.is_file())
|
||||
elif path.is_file() and path.as_posix().endswith("src/lib.rs"):
|
||||
roots.add(path)
|
||||
else:
|
||||
errors.append(f"unsupported or missing library-root path: {path}")
|
||||
return sorted(roots), errors
|
||||
|
||||
|
||||
def _audit_file(path: pathlib.Path) -> list[str]:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
masked, _ = mask_source(text)
|
||||
lines = masked.splitlines()
|
||||
depths = line_depths(masked)
|
||||
errors: list[str] = []
|
||||
for index, line in enumerate(lines):
|
||||
if depths[index] != 0:
|
||||
continue
|
||||
if _ITEM.match(line) is not None:
|
||||
errors.append(f"{path}:{index + 1}: crate root must remain a facade; move this item to a responsibility module")
|
||||
return errors
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Run the read-only Knowledge library crate-root facade audit."""
|
||||
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("paths", nargs="+", help="Crate roots or directories containing Rust library crates")
|
||||
arguments = parser.parse_args()
|
||||
files, errors = _selected_lib_roots(arguments.paths)
|
||||
if errors or not files:
|
||||
for error in errors or ["no Rust library crate roots selected"]:
|
||||
print(f"Rust crate-root facade audit: {error}", file=sys.stderr)
|
||||
return 2
|
||||
for path in files:
|
||||
try:
|
||||
errors.extend(_audit_file(path))
|
||||
except (OSError, UnicodeError) as error:
|
||||
print(f"Rust crate-root facade audit: cannot read {path}: {error}", file=sys.stderr)
|
||||
return 2
|
||||
for error in errors:
|
||||
print(error, file=sys.stderr)
|
||||
if errors:
|
||||
print(f"Rust crate-root facade audit: {len(errors)} error(s) across {len(files)} crate root(s)", file=sys.stderr)
|
||||
return 1
|
||||
print(f"Rust crate-root facade audit: clean ({len(files)} crate root(s))")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user