73from pydantic
import BaseModel, ConfigDict, Field, ValidationError
122 """A single claim receipt — the full amended spec §0.3 field set."""
124 model_config = ConfigDict(extra=
"forbid", strict=
True)
126 claim_id: str = Field(pattern=
r"^VC-\d{4}$")
128 spec_line: int = Field(ge=1)
129 claim_text: str = Field(min_length=1)
131 "behavior",
"schema",
"cli",
"error-code",
"test-surface",
"ci-surface"
133 expected_status: Literal[
"VERIFIED_CURRENT",
"TARGET",
"DORMANT",
"DISPUTED"]
134 resolved_code_sites: list[CodeSite] = Field(min_length=1)
135 claim_fingerprint: str
136 evidence_command_or_test: str
137 last_verified_commit: str
139 reviewer: str |
None =
None
171def relocate_site(lines: list[str], site: CodeSite) -> tuple[str, list[tuple[int, int]]]:
172 """Return (status, candidates) for a site fingerprint.
174 status ∈ {"located", "relocated", "drift", "ambiguous"}. "located" means
175 the hinted range still carries the fingerprint (the hint disambiguates even
176 if the same bytes recur elsewhere). Otherwise a same-line-count window slides
177 over the file: 0 = drift, exactly 1 = relocated, >=2 = ambiguous.
180 span = site.end - site.start + 1
181 if 1 <= site.start <= len(lines)
and site.end <= len(lines):
183 return (
"located", [(site.start, site.end)])
184 candidates: list[tuple[int, int]] = []
185 for start
in range(1, len(lines) - span + 2):
186 end = start + span - 1
188 candidates.append((start, end))
191 if len(candidates) == 1:
192 return (
"relocated", candidates)
193 return (
"ambiguous", candidates)
209 """Parse `tools/valid_audit_lexicon.txt` (`token :: regex`; `#` comments)."""
211 path = root / LEXICON_FILE
212 entries: list[LexiconEntry] = []
213 for raw
in path.read_text(encoding=
"utf-8").splitlines():
215 if not line
or line.startswith(
"#")
or "::" not in line:
217 token, pattern = line.split(
"::", 1)
218 entries.append(
LexiconEntry(token=token.strip(), pattern=pattern.strip()))
257 """Map each `@claim: <ID>` to its 1-indexed line (last wins on a dup — flagged elsewhere)."""
259 tags: dict[str, int] = {}
260 for idx, line
in enumerate(lines, start=1):
261 for match
in _TAG_RE.finditer(line):
262 tags.setdefault(match.group(1), idx)
297 lines: list[str], lexicon: list[LexiconEntry], rel: str
298) -> list[dict[str, object]]:
299 """Fence/comment-aware scan for current-state trigger lines lacking a tag or waiver."""
301 compiled = [(entry, re.compile(entry.pattern))
for entry
in lexicon]
302 violations: list[dict[str, object]] = []
304 in_html_comment =
False
305 for idx, raw
in enumerate(lines, start=1):
306 stripped = raw.strip()
307 if stripped.startswith(
"```")
or stripped.startswith(
"~~~"):
308 in_fence =
not in_fence
313 for entry, pattern
in compiled:
314 if not pattern.search(scannable):
322 "kind":
"untagged_trigger",
325 "token": entry.token,
423 """Structured audit outcome (also the `--json` payload shape)."""
425 model_config = ConfigDict(extra=
"forbid")
428 violations: list[dict[str, object]]
429 pending_relocations: list[dict[str, object]]
430 stale_spec_lines: list[dict[str, object]]
431 disputed: list[dict[str, object]]
435def run_audit(root: Path, allow_disputed: bool) -> AuditResult:
436 """Run the full read-only audit and return a structured result."""
438 violations: list[dict[str, object]] = []
439 pending: list[dict[str, object]] = []
440 stale_lines: list[dict[str, object]] = []
441 disputed: list[dict[str, object]] = []
444 receipts = {r.claim_id: r
for r
in receipt_file.claims}
447 seen: set[str] = set()
448 for receipt
in receipt_file.claims:
449 if receipt.claim_id
in seen:
450 violations.append({
"kind":
"duplicate_receipt_id",
"claim_id": receipt.claim_id})
451 seen.add(receipt.claim_id)
454 md_path = root / RECEIPTS_MD
455 if not md_path.is_file():
456 raise ConfigError(f
"receipts markdown view not found: {RECEIPTS_MD}")
457 if md_path.read_text(encoding=
"utf-8") !=
render_markdown(receipt_file):
458 violations.append({
"kind":
"json_md_disagreement",
"detail": RECEIPTS_MD})
461 for receipt
in receipt_file.claims:
464 {
"kind":
"claim_fingerprint_mismatch",
"claim_id": receipt.claim_id}
468 doc_lines: dict[str, list[str]] = {rel:
_read_lines(root, rel)
for rel
in GOVERNED_DOCS}
469 all_tag_ids: list[str] = []
470 tag_line: dict[tuple[str, str], int] = {}
471 for rel, lines
in doc_lines.items():
473 all_tag_ids.extend(ids_in_doc)
475 tag_line[(rel, cid)] = ln
477 tag_id_set = set(all_tag_ids)
478 if len(all_tag_ids) != len(tag_id_set):
479 dupes = sorted({cid
for cid
in all_tag_ids
if all_tag_ids.count(cid) > 1})
480 violations.append({
"kind":
"duplicate_tag",
"claim_ids": dupes})
482 for cid
in tag_id_set:
483 if cid
not in receipts:
484 violations.append({
"kind":
"tag_without_receipt",
"claim_id": cid})
486 if cid
not in tag_id_set:
487 violations.append({
"kind":
"receipt_without_tag",
"claim_id": cid})
490 for receipt
in sorted(receipt_file.claims, key=
lambda r: r.claim_id):
491 cid = receipt.claim_id
492 if receipt.expected_status ==
"DISPUTED":
493 disputed.append({
"claim_id": cid,
"claim_text": receipt.claim_text})
494 if not allow_disputed:
495 violations.append({
"kind":
"disputed_receipt",
"claim_id": cid})
498 key = (receipt.spec_file, cid)
500 true_line = tag_line[key]
501 lines = doc_lines[receipt.spec_file]
502 on_line = receipt.claim_text
in lines[true_line - 1]
503 on_prev = true_line >= 2
and receipt.claim_text
in lines[true_line - 2]
504 if not (on_line
or on_prev):
505 violations.append({
"kind":
"claim_text_drift",
"claim_id": cid})
506 if receipt.spec_line != true_line:
508 {
"claim_id": cid,
"stored": receipt.spec_line,
"actual": true_line}
514 for site
in receipt.resolved_code_sites:
515 code_path = root / site.path
516 if not code_path.is_file():
518 {
"kind":
"code_site_missing_file",
"claim_id": cid,
"path": site.path}
521 code_lines = code_path.read_text(encoding=
"utf-8").splitlines()
523 if status ==
"drift":
525 {
"kind":
"code_drift",
"claim_id": cid,
"path": site.path,
526 "hint": [site.start, site.end]}
528 elif status ==
"ambiguous":
530 {
"kind":
"ambiguous_relocation",
"claim_id": cid,
"path": site.path,
531 "candidates": [list(c)
for c
in candidates]}
533 elif status ==
"relocated":
534 new_start, new_end = candidates[0]
536 {
"claim_id": cid,
"path": site.path,
537 "stored": [site.start, site.end],
"actual": [new_start, new_end]}
543 violations=violations,
544 pending_relocations=pending,
545 stale_spec_lines=stale_lines,
547 allow_disputed=allow_disputed,
554def refresh(root: Path, only_relocations: bool, claim_id: str |
None) -> int:
555 """Rewrite mechanical relocations / stale spec_lines + regenerate the markdown view."""
559 doc_lines: dict[str, list[str]] = {rel:
_read_lines(root, rel)
for rel
in GOVERNED_DOCS}
560 tag_line: dict[tuple[str, str], int] = {}
561 for rel, lines
in doc_lines.items():
563 tag_line[(rel, cid)] = ln
566 for receipt
in receipt_file.claims:
567 if claim_id
is not None and receipt.claim_id != claim_id:
569 key = (receipt.spec_file, receipt.claim_id)
570 if key
in tag_line
and receipt.spec_line != tag_line[key]:
571 receipt.spec_line = tag_line[key]
573 for site
in receipt.resolved_code_sites:
574 code_path = root / site.path
575 if not code_path.is_file():
577 code_lines = code_path.read_text(encoding=
"utf-8").splitlines()
579 if status ==
"relocated":
580 site.start, site.end = candidates[0]
583 (root / RECEIPTS_JSON).write_text(
canonical_json(receipt_file), encoding=
"utf-8", newline=
"\n")
584 (root / RECEIPTS_MD).write_text(
render_markdown(receipt_file), encoding=
"utf-8", newline=
"\n")
585 _dbg(
"OC_CHAT.MAIN", func, f
"{changed} field(s) rewritten", 2)
586 print(f
"refresh: {changed} mechanical field(s) rewritten; markdown view regenerated")
635def main(argv: list[str] |
None =
None) -> int:
636 parser = argparse.ArgumentParser(
638 description=
"V-A-L-I-D claim-vs-code audit for the registry V0.24 spec.",
640 parser.add_argument(
"--check", action=
"store_true", help=
"Read-only audit (CI/ACs/docs invocation).")
645 help=
"Relax ONLY the DISPUTED gate (pre-flip-only; forbidden after the gate-blocking flip).",
647 parser.add_argument(
"--refresh", metavar=
"CLAIM_ID", help=
"Refresh one receipt (after re-verifying real drift).")
649 "--refresh-relocations",
651 help=
"Rewrite all mechanical relocations / stale spec_lines + regenerate the markdown view.",
653 parser.add_argument(
"--root", default=
None, help=
"Repo root to audit (default: this tool's repo).")
654 parser.add_argument(
"--json", action=
"store_true", help=
"Emit a JSON report (check mode).")
655 args = parser.parse_args(argv)
657 root = Path(args.root).resolve()
if args.root
else Path(__file__).resolve().parent.parent
659 modes = [bool(args.check), bool(args.refresh), bool(args.refresh_relocations)]
661 print(
"usage: exactly one of --check / --refresh <id> / --refresh-relocations", file=sys.stderr)
665 return check(root, args.allow_disputed, args.json)
666 if args.refresh_relocations:
667 return refresh(root, only_relocations=
True, claim_id=
None)
668 return refresh(root, only_relocations=
False, claim_id=args.refresh)
669 except ConfigError
as exc:
670 print(f
"config error: {exc}", file=sys.stderr)