mirror of
https://github.com/AgriciDaniel/claude-blog.git
synced 2026-09-19 03:32:21 +08:00
87872da7d3
Verify every open pull request and issue against current code and primary sources. Refresh Google guidance and promote the bundled Brain only after deterministic and independent audits pass. - Add live and offline Google currentness enforcement. - Add complete Brain source-review records and public projection gates. - Fix analyzer, installer, dependency, and security regressions. - Record the evidence-led PR and issue disposition in release notes. This does not merge or close external pull requests or issues. Antivirus vendor reclassification remains external. Verified: - Root tests: 343 passed, 1 skipped. - Brain tests: 26 passed. - Brain audit: market-ready, score 100, no warnings. - Brainstein strict audit: SSS+, score 100, no warnings. - Google live and offline currentness checks passed. - Secret scan passed with 217 adjudicated findings and none unexpected. - Public release, consistency, plugin, lock, prose, and actionlint gates passed. Co-Authored-By: GPT-5 <noreply@openai.com>
432 lines
16 KiB
Python
Vendored
432 lines
16 KiB
Python
Vendored
#!/usr/bin/env python3
|
|
"""Verify source-ledger URLs and apply an explicit claim-review decision file."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import concurrent.futures
|
|
import hashlib
|
|
import ipaddress
|
|
import json
|
|
import re
|
|
import socket
|
|
import subprocess
|
|
import tempfile
|
|
from datetime import date, timedelta
|
|
from pathlib import Path
|
|
from typing import Any
|
|
from urllib.parse import urljoin, urlparse
|
|
|
|
REPO = Path(__file__).resolve().parent.parent
|
|
LEDGER_PATH = REPO / "references" / "source-ledger.json"
|
|
STOPWORDS = {
|
|
"about", "after", "also", "and", "are", "because", "been", "being",
|
|
"between", "both", "but", "can", "could", "does", "each", "for",
|
|
"from", "had", "has", "have", "how", "into", "its", "may", "more",
|
|
"must", "not", "only", "other", "our", "out", "over", "per", "same",
|
|
"should", "than", "that", "the", "their", "them", "then", "there",
|
|
"these", "they", "this", "those", "through", "under", "use", "used",
|
|
"uses", "using", "very", "was", "were", "what", "when", "where",
|
|
"which", "while", "who", "will", "with", "without", "would", "your",
|
|
}
|
|
DECISION_GROUPS = {
|
|
"confirmed_by_content",
|
|
"confirmed_by_manual_review",
|
|
"corrected",
|
|
"retired",
|
|
}
|
|
MAX_BYTES = 20 * 1024 * 1024
|
|
MIN_CONTENT_COVERAGE = 0.60
|
|
|
|
|
|
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument(
|
|
"--review-file",
|
|
default="references/source-review-2026-08-25.json",
|
|
help="Brain-relative explicit review decisions.",
|
|
)
|
|
parser.add_argument("--as-of", default=date.today().isoformat())
|
|
parser.add_argument("--workers", type=int, default=8)
|
|
parser.add_argument("--apply", action="store_true")
|
|
parser.add_argument(
|
|
"--offline-check",
|
|
action="store_true",
|
|
help="Validate current ledger verification records without network access.",
|
|
)
|
|
return parser.parse_args(argv)
|
|
|
|
|
|
def load_object(path: Path) -> dict[str, Any]:
|
|
try:
|
|
value = json.loads(path.read_text(encoding="utf-8"))
|
|
except (OSError, ValueError) as exc:
|
|
raise SystemExit(f"ERROR: cannot read {path}: {exc}") from exc
|
|
if not isinstance(value, dict):
|
|
raise SystemExit(f"ERROR: {path} must contain a JSON object")
|
|
return value
|
|
|
|
|
|
def parse_day(value: str, label: str) -> date:
|
|
try:
|
|
return date.fromisoformat(value)
|
|
except ValueError as exc:
|
|
raise SystemExit(f"ERROR: {label} must be YYYY-MM-DD") from exc
|
|
|
|
|
|
def validate_public_https(url: str) -> None:
|
|
parsed = urlparse(url)
|
|
if parsed.scheme != "https" or not parsed.hostname or parsed.username or parsed.password:
|
|
raise ValueError("URL must be public HTTPS without credentials")
|
|
try:
|
|
addresses = socket.getaddrinfo(
|
|
parsed.hostname,
|
|
parsed.port or 443,
|
|
type=socket.SOCK_STREAM,
|
|
)
|
|
except socket.gaierror as exc:
|
|
raise ValueError(f"DNS resolution failed: {exc}") from exc
|
|
for address in addresses:
|
|
resolved = ipaddress.ip_address(address[4][0])
|
|
if not resolved.is_global:
|
|
raise ValueError(f"URL resolved to a non-public address: {resolved}")
|
|
|
|
|
|
def fetch_source(url: str) -> dict[str, Any]:
|
|
import requests
|
|
|
|
session = requests.Session()
|
|
current = url
|
|
for _ in range(7):
|
|
validate_public_https(current)
|
|
response = session.get(
|
|
current,
|
|
headers={
|
|
"User-Agent": "Mozilla/5.0 ClaudeBlogSourceAudit/1.0",
|
|
"Accept": "text/html,application/pdf,application/json,text/plain,*/*;q=0.5",
|
|
},
|
|
timeout=30,
|
|
allow_redirects=False,
|
|
stream=True,
|
|
)
|
|
if response.is_redirect or response.is_permanent_redirect:
|
|
location = response.headers.get("location")
|
|
if not location:
|
|
raise ValueError("redirect response is missing Location")
|
|
current = urljoin(current, location)
|
|
continue
|
|
response.raise_for_status()
|
|
final_url = response.url
|
|
validate_public_https(final_url)
|
|
body = bytearray()
|
|
for chunk in response.iter_content(64 * 1024):
|
|
body.extend(chunk)
|
|
if len(body) > MAX_BYTES:
|
|
raise ValueError(f"source exceeds {MAX_BYTES} bytes")
|
|
content_type = response.headers.get("content-type", "").split(";", 1)[0].lower()
|
|
text = extract_text(bytes(body), content_type, final_url)
|
|
normalized = normalize_text(text)
|
|
return {
|
|
"http_status": response.status_code,
|
|
"final_url": final_url,
|
|
"content_type": content_type,
|
|
"bytes": len(body),
|
|
"normalized_content_sha256": hashlib.sha256(
|
|
normalized.encode("utf-8")
|
|
).hexdigest(),
|
|
"text": normalized,
|
|
"reviewable_text_bytes": len(normalized.encode("utf-8")),
|
|
}
|
|
raise ValueError("too many redirects")
|
|
|
|
|
|
def extract_text(body: bytes, content_type: str, final_url: str) -> str:
|
|
if content_type == "application/pdf" or urlparse(final_url).path.lower().endswith(".pdf"):
|
|
with tempfile.NamedTemporaryFile(suffix=".pdf") as pdf_file:
|
|
with tempfile.NamedTemporaryFile(suffix=".txt") as text_file:
|
|
pdf_file.write(body)
|
|
pdf_file.flush()
|
|
result = subprocess.run(
|
|
["pdftotext", "-layout", pdf_file.name, text_file.name],
|
|
text=True,
|
|
capture_output=True,
|
|
check=False,
|
|
)
|
|
if result.returncode:
|
|
raise ValueError("pdftotext could not extract the review source")
|
|
return Path(text_file.name).read_text(
|
|
encoding="utf-8", errors="replace"
|
|
)
|
|
if "html" in content_type:
|
|
from bs4 import BeautifulSoup
|
|
|
|
soup = BeautifulSoup(body, "html.parser")
|
|
for element in soup(["script", "style", "svg", "noscript", "template"]):
|
|
element.decompose()
|
|
review_root = soup.find("main") or soup.find("article") or soup
|
|
return review_root.get_text(" ", strip=True)
|
|
return body.decode("utf-8", errors="replace")
|
|
|
|
|
|
def normalize_text(value: str) -> str:
|
|
return re.sub(r"\s+", " ", value).strip().lower()
|
|
|
|
|
|
def claim_text(source: dict[str, Any]) -> str:
|
|
claims = source.get("claims")
|
|
if not isinstance(claims, list):
|
|
return ""
|
|
return " ".join(str(item) for item in claims if isinstance(item, str))
|
|
|
|
|
|
def claim_evidence(source: dict[str, Any], text: str) -> dict[str, Any]:
|
|
claim = claim_text(source).lower()
|
|
tokens = list(
|
|
dict.fromkeys(
|
|
token
|
|
for token in re.findall(r"[a-z0-9]+", claim)
|
|
if len(token) >= 4 and token not in STOPWORDS and not token.isdigit()
|
|
)
|
|
)
|
|
matched = sum(token in text for token in tokens)
|
|
coverage = matched / len(tokens) if tokens else 1.0
|
|
numbers = list(
|
|
dict.fromkeys(re.findall(r"(?<![a-z])\d+(?:\.\d+)?%?", claim))
|
|
)
|
|
missing_numbers = [
|
|
number
|
|
for number in numbers
|
|
if number not in text
|
|
and not (number.startswith("0") and number.lstrip("0") in text)
|
|
]
|
|
return {
|
|
"claim_token_coverage": round(coverage, 3),
|
|
"missing_numeric_literals": missing_numbers,
|
|
}
|
|
|
|
|
|
def review_decisions(review: dict[str, Any]) -> tuple[dict[str, str], dict[str, dict[str, Any]]]:
|
|
decisions: dict[str, str] = {}
|
|
corrections: dict[str, dict[str, Any]] = {}
|
|
for group in DECISION_GROUPS:
|
|
value = review.get(group, [] if group != "corrected" else {})
|
|
if group == "corrected":
|
|
if not isinstance(value, dict):
|
|
raise SystemExit("ERROR: corrected review decisions must be an object")
|
|
for source_id, correction in value.items():
|
|
if not isinstance(correction, dict):
|
|
raise SystemExit(f"ERROR: corrected decision for {source_id} must be an object")
|
|
if source_id in decisions:
|
|
raise SystemExit(f"ERROR: duplicate review decision for {source_id}")
|
|
decisions[source_id] = group
|
|
corrections[source_id] = correction
|
|
continue
|
|
if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
|
|
raise SystemExit(f"ERROR: {group} review decisions must be a string list")
|
|
for source_id in value:
|
|
if source_id in decisions:
|
|
raise SystemExit(f"ERROR: duplicate review decision for {source_id}")
|
|
decisions[source_id] = group
|
|
return decisions, corrections
|
|
|
|
|
|
def next_refresh(source: dict[str, Any], reviewed_on: date) -> date:
|
|
if source.get("living_doc") is True:
|
|
days = 31
|
|
elif str(source.get("source_type", "")) in {"primary", "practitioner", "market"}:
|
|
days = 90
|
|
else:
|
|
days = 180
|
|
return reviewed_on + timedelta(days=days)
|
|
|
|
|
|
def apply_correction(source: dict[str, Any], correction: dict[str, Any]) -> None:
|
|
allowed = {
|
|
"title", "url", "source_type", "claims", "supports_claims",
|
|
"confidence", "evidence_tier", "limitations", "last_updated",
|
|
"published", "date_precision", "living_doc", "review_note",
|
|
}
|
|
unknown = sorted(set(correction) - allowed)
|
|
if unknown:
|
|
raise SystemExit(
|
|
f"ERROR: unsupported correction fields for {source.get('id')}: {unknown}"
|
|
)
|
|
for key, value in correction.items():
|
|
if key != "review_note":
|
|
source[key] = value
|
|
|
|
|
|
def offline_check(ledger: dict[str, Any], as_of: date) -> dict[str, Any]:
|
|
failures: list[str] = []
|
|
verified = 0
|
|
for source in ledger.get("sources", []):
|
|
if not isinstance(source, dict):
|
|
failures.append("non-object source entry")
|
|
continue
|
|
verification = source.get("verification")
|
|
if not isinstance(verification, dict):
|
|
failures.append(f"{source.get('id', '<missing>')} missing verification record")
|
|
continue
|
|
reviewed_on = verification.get("reviewed_on")
|
|
try:
|
|
reviewed_day = date.fromisoformat(str(reviewed_on))
|
|
except ValueError:
|
|
failures.append(f"{source.get('id', '<missing>')} has invalid verification date")
|
|
continue
|
|
if reviewed_day > as_of:
|
|
failures.append(f"{source.get('id', '<missing>')} has future verification date")
|
|
continue
|
|
verified += 1
|
|
return {"status": "pass" if not failures else "fail", "verified": verified, "failures": failures}
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
args = parse_args(argv)
|
|
as_of = parse_day(args.as_of, "--as-of")
|
|
ledger = load_object(LEDGER_PATH)
|
|
if args.offline_check:
|
|
result = offline_check(ledger, as_of)
|
|
print(json.dumps(result, indent=2, sort_keys=True))
|
|
return 0 if result["status"] == "pass" else 1
|
|
|
|
review_path = (REPO / args.review_file).resolve()
|
|
if not review_path.is_relative_to(REPO):
|
|
raise SystemExit("ERROR: --review-file must stay inside the Brain")
|
|
review = load_object(review_path)
|
|
if review.get("reviewed_on") != as_of.isoformat():
|
|
raise SystemExit("ERROR: review-file date does not match --as-of")
|
|
decisions, corrections = review_decisions(review)
|
|
|
|
sources = ledger.get("sources")
|
|
if not isinstance(sources, list):
|
|
raise SystemExit("ERROR: source-ledger sources must be a list")
|
|
review_candidates = [
|
|
source
|
|
for source in sources
|
|
if isinstance(source, dict)
|
|
and (
|
|
str(source.get("refresh_due", "")) < as_of.isoformat()
|
|
or not isinstance(source.get("verification"), dict)
|
|
)
|
|
]
|
|
candidate_ids = {str(source.get("id", "")) for source in review_candidates}
|
|
source_by_id = {
|
|
str(source.get("id", "")): source
|
|
for source in sources
|
|
if isinstance(source, dict)
|
|
}
|
|
permitted_prior = {
|
|
source_id
|
|
for source_id, source in source_by_id.items()
|
|
if isinstance(source.get("verification"), dict)
|
|
and source["verification"].get("reviewed_on") == as_of.isoformat()
|
|
}
|
|
missing = sorted(candidate_ids - set(decisions))
|
|
extra = sorted(set(decisions) - candidate_ids - permitted_prior)
|
|
if missing or extra:
|
|
raise SystemExit(
|
|
f"ERROR: review coverage mismatch; missing={missing[:8]} extra={extra[:8]}"
|
|
)
|
|
|
|
# Apply reviewed metadata corrections before fetching. This makes a corrected
|
|
# canonical URL the actual source that is retrieved and hashed, rather than
|
|
# recording evidence from the superseded URL.
|
|
for source in review_candidates:
|
|
source_id = str(source["id"])
|
|
if decisions[source_id] == "corrected":
|
|
apply_correction(source, corrections[source_id])
|
|
|
|
urls = sorted({str(source["url"]) for source in review_candidates})
|
|
fetched: dict[str, dict[str, Any]] = {}
|
|
failures: list[dict[str, str]] = []
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=max(1, args.workers)) as executor:
|
|
future_map = {executor.submit(fetch_source, url): url for url in urls}
|
|
for future in concurrent.futures.as_completed(future_map):
|
|
url = future_map[future]
|
|
try:
|
|
fetched[url] = future.result()
|
|
except Exception as exc:
|
|
failures.append({"url": url, "error": f"{type(exc).__name__}: {exc}"})
|
|
if failures:
|
|
print(json.dumps({"status": "fail", "network_failures": failures}, indent=2))
|
|
return 1
|
|
|
|
results: list[dict[str, Any]] = []
|
|
for source in review_candidates:
|
|
source_id = str(source["id"])
|
|
decision = decisions[source_id]
|
|
correction = corrections.get(source_id, {})
|
|
evidence = fetched[str(source["url"])]
|
|
claim_check = claim_evidence(source, evidence["text"])
|
|
if decision == "confirmed_by_content" and (
|
|
claim_check["claim_token_coverage"] < MIN_CONTENT_COVERAGE
|
|
or claim_check["missing_numeric_literals"]
|
|
):
|
|
failures.append(
|
|
{
|
|
"url": str(source["url"]),
|
|
"error": f"{source_id} no longer meets content-confirmation thresholds",
|
|
}
|
|
)
|
|
continue
|
|
if decision == "retired":
|
|
source["status"] = "retired"
|
|
source["retrieved"] = as_of.isoformat()
|
|
source["last_verified"] = as_of.isoformat()
|
|
source["refresh_due"] = next_refresh(source, as_of).isoformat()
|
|
source["verification"] = {
|
|
"reviewed_on": as_of.isoformat(),
|
|
"decision": decision,
|
|
"method": "public-source content check plus explicit claim review",
|
|
"http_status": evidence["http_status"],
|
|
"final_url": evidence["final_url"],
|
|
"content_type": evidence["content_type"],
|
|
"reviewable_text_bytes": evidence["reviewable_text_bytes"],
|
|
"normalized_content_sha256": evidence["normalized_content_sha256"],
|
|
**claim_check,
|
|
"review_note": correction.get(
|
|
"review_note",
|
|
"Claim retained after source-content review.",
|
|
),
|
|
}
|
|
results.append(
|
|
{
|
|
"id": source_id,
|
|
"decision": decision,
|
|
**claim_check,
|
|
}
|
|
)
|
|
|
|
if failures:
|
|
print(json.dumps({"status": "fail", "review_failures": failures}, indent=2))
|
|
return 1
|
|
ledger["last_verified"] = as_of.isoformat()
|
|
ledger["status"] = "market-ready-research" if len(results) == len(review_candidates) else ledger.get("status")
|
|
if args.apply:
|
|
LEDGER_PATH.write_text(
|
|
json.dumps(ledger, indent=2, ensure_ascii=False) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"status": "pass",
|
|
"applied": args.apply,
|
|
"reviewed": len(results),
|
|
"unique_urls": len(urls),
|
|
"decisions": {
|
|
group: sum(item["decision"] == group for item in results)
|
|
for group in sorted(DECISION_GROUPS)
|
|
},
|
|
"results": results,
|
|
},
|
|
indent=2,
|
|
sort_keys=True,
|
|
)
|
|
)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|