Updated project files and configuration
- Added/updated .gitignore file - Fixed remote URL configuration - Updated project structure and files 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
141
scripts/speclint_check.py
Normal file
141
scripts/speclint_check.py
Normal file
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Iterable, List, Optional
|
||||
import re
|
||||
|
||||
ALLOWED_PROJ = {
|
||||
"CHORUS",
|
||||
"COOEE",
|
||||
"DHT",
|
||||
"SHHH",
|
||||
"KACHING",
|
||||
"HMMM",
|
||||
"UCXL",
|
||||
"SLURP",
|
||||
"RUSTLE",
|
||||
"WHOOSH",
|
||||
"BUBBLE",
|
||||
}
|
||||
ALLOWED_CAT = {"REQ", "INT", "SEC", "OBS", "PER", "COMP"}
|
||||
|
||||
REQ_PATTERN = re.compile(r"REQ:\s*([A-Z]+)-([A-Z]+)-(\d{3})")
|
||||
UCXL_PATTERN = re.compile(r"UCXL:\s*ucxl://")
|
||||
|
||||
SKIP_DIRS = {
|
||||
".git",
|
||||
"node_modules",
|
||||
"dist",
|
||||
"build",
|
||||
"venv",
|
||||
"__pycache__",
|
||||
".venv",
|
||||
"target",
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Finding:
|
||||
path: Path
|
||||
line_no: int
|
||||
severity: str
|
||||
message: str
|
||||
line: str
|
||||
|
||||
|
||||
def iter_files(paths: Iterable[Path]) -> Iterable[Path]:
|
||||
for p in paths:
|
||||
if p.is_dir():
|
||||
for sub in p.rglob("*"):
|
||||
if sub.is_dir():
|
||||
if sub.name in SKIP_DIRS:
|
||||
continue
|
||||
continue
|
||||
yield sub
|
||||
elif p.is_file():
|
||||
yield p
|
||||
|
||||
|
||||
def validate_file(path: Path, require_ucxl: bool, max_distance: int) -> List[Finding]:
|
||||
findings: List[Finding] = []
|
||||
try:
|
||||
text = path.read_text(errors="ignore")
|
||||
except Exception as e:
|
||||
findings.append(Finding(path, 0, "warn", f"unable to read file: {e}", line=""))
|
||||
return findings
|
||||
|
||||
lines = text.splitlines()
|
||||
for idx, line in enumerate(lines, start=1):
|
||||
m = REQ_PATTERN.search(line)
|
||||
if not m:
|
||||
continue
|
||||
proj, cat, _ = m.group(1), m.group(2), m.group(3)
|
||||
if proj not in ALLOWED_PROJ:
|
||||
findings.append(Finding(path, idx, "error", f"unknown PROJ '{proj}' in ID", line=line))
|
||||
if cat not in ALLOWED_CAT:
|
||||
findings.append(Finding(path, idx, "error", f"unknown CAT '{cat}' in ID", line=line))
|
||||
if require_ucxl:
|
||||
start = max(1, idx - max_distance)
|
||||
end = min(len(lines), idx + max_distance)
|
||||
window = lines[start - 1 : end]
|
||||
if not any(UCXL_PATTERN.search(l) for l in window):
|
||||
findings.append(
|
||||
Finding(
|
||||
path,
|
||||
idx,
|
||||
"error",
|
||||
f"missing nearby UCXL backlink (±{max_distance} lines)",
|
||||
line=line,
|
||||
)
|
||||
)
|
||||
return findings
|
||||
|
||||
|
||||
def cmd_check(args: argparse.Namespace) -> int:
|
||||
paths = [Path(p) for p in args.paths]
|
||||
all_findings: List[Finding] = []
|
||||
for f in iter_files(paths):
|
||||
try:
|
||||
if f.stat().st_size > 3_000_000:
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
all_findings.extend(
|
||||
validate_file(f, require_ucxl=args.require_ucxl, max_distance=args.max_distance)
|
||||
)
|
||||
if all_findings:
|
||||
for fd in all_findings:
|
||||
print(f"{fd.path}:{fd.line_no}: {fd.severity}: {fd.message}")
|
||||
if fd.line:
|
||||
print(f" {fd.line.strip()}")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def build_argparser() -> argparse.ArgumentParser:
|
||||
p = argparse.ArgumentParser(prog="speclint-check", description="Suite 2.0.0 traceability linter (WHOOSH local)")
|
||||
sub = p.add_subparsers(dest="cmd", required=True)
|
||||
c = sub.add_parser("check", help="validate requirement IDs and UCXL backlinks")
|
||||
c.add_argument("paths", nargs="+", help="files or directories to scan")
|
||||
c.add_argument("--require-ucxl", action="store_true", help="require nearby UCXL backlink")
|
||||
c.add_argument("--max-distance", type=int, default=5, help="line distance for UCXL proximity check")
|
||||
c.set_defaults(func=cmd_check)
|
||||
return p
|
||||
|
||||
|
||||
def main(argv: Optional[list[str]] = None) -> int:
|
||||
try:
|
||||
args = build_argparser().parse_args(argv)
|
||||
return args.func(args)
|
||||
except Exception as e:
|
||||
print(f"speclint-check: internal error: {e}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
||||
Reference in New Issue
Block a user