Add docs/ResearchArtifactConvention.md defining program/paper/paper-local identifier namespaces, file layout, publication metadata, and provenance conventions. Register RP-000 (Researching Research) with CR-000 (Paper Zero) as an honest draft skeleton: outline manuscript with explicit [MISSING]/[CONJECTURE] markers, five conjectural claims transcribed from the M2 example list, and empty evidence/criticism files. Add the facility-level paper registry and a small validation script covering registry parsing, id uniqueness, publication-metadata agreement, and Markdown link integrity. Close CR-WP-0002 and hand off M1 (Paper Zero v0.1 manuscript) as CR-WP-0003, per the workplan's residual-handoff requirement. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: sonnet Assistant-Process: 228422@bnt-lap001 Assistant-Session: 49089735-86e1-4d29-8a6e-74c26947f8c4
173 lines
5.6 KiB
Python
173 lines
5.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Validate the Coulomb Research paper registry and its referenced artifacts.
|
|
|
|
Checks, per docs/ResearchArtifactConvention.md:
|
|
1. research/registry/papers.yaml parses as YAML and each entry has the
|
|
required fields (id, title, program, stage, version, status, path).
|
|
2. Paper ids are unique within the registry.
|
|
3. Each entry's `path` exists and contains a publication.yaml.
|
|
4. Each entry's stage/version/status agree with the paper's own
|
|
publication.yaml (paper_id, publication_stage, version,
|
|
publication_state).
|
|
5. Markdown links of the form [text](relative/path) inside the paper's
|
|
own files (README.md, manuscript.md, claims.md, evidence.md,
|
|
criticism.md, revision-log.md) that point at a relative file resolve
|
|
to an existing file.
|
|
|
|
No external dependencies beyond PyYAML (`import yaml`), which is already
|
|
relied on by this facility's tooling. This is intentionally a single script,
|
|
not a test framework or package, because the artifact set it validates is a
|
|
handful of small, hand-authored files -- see the "smallest repeatable
|
|
validation tool" note in workplans/CR-WP-0002-research-facility-skeleton.md
|
|
task T03.
|
|
|
|
Usage: python3 scripts/validate_research_registry.py
|
|
Exit status is non-zero if any check fails.
|
|
"""
|
|
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
REGISTRY_PATH = REPO_ROOT / "research" / "registry" / "papers.yaml"
|
|
|
|
REQUIRED_REGISTRY_FIELDS = ["id", "title", "program", "stage", "version", "status", "path"]
|
|
|
|
PAPER_STAGE_STATE_MAP = {
|
|
"P1": "internal-circulation",
|
|
}
|
|
|
|
MD_LINK_RE = re.compile(r"\[[^\]]*\]\(([^)]+)\)")
|
|
|
|
PAPER_MD_FILES = [
|
|
"README.md",
|
|
"manuscript.md",
|
|
"claims.md",
|
|
"evidence.md",
|
|
"criticism.md",
|
|
"revision-log.md",
|
|
]
|
|
|
|
|
|
def fail(errors, message):
|
|
errors.append(message)
|
|
|
|
|
|
def check_registry(errors):
|
|
if not REGISTRY_PATH.exists():
|
|
fail(errors, f"missing registry file: {REGISTRY_PATH}")
|
|
return []
|
|
|
|
with REGISTRY_PATH.open() as f:
|
|
try:
|
|
data = yaml.safe_load(f)
|
|
except yaml.YAMLError as exc:
|
|
fail(errors, f"registry YAML did not parse: {exc}")
|
|
return []
|
|
|
|
papers = (data or {}).get("papers")
|
|
if not isinstance(papers, list) or not papers:
|
|
fail(errors, "registry has no non-empty 'papers' list")
|
|
return []
|
|
|
|
seen_ids = set()
|
|
for entry in papers:
|
|
missing = [f for f in REQUIRED_REGISTRY_FIELDS if f not in entry]
|
|
if missing:
|
|
fail(errors, f"registry entry {entry.get('id', '<no id>')} missing fields: {missing}")
|
|
continue
|
|
|
|
pid = entry["id"]
|
|
if pid in seen_ids:
|
|
fail(errors, f"duplicate paper id in registry: {pid}")
|
|
seen_ids.add(pid)
|
|
|
|
return papers
|
|
|
|
|
|
def check_paper(errors, entry):
|
|
pid = entry["id"]
|
|
paper_dir = REPO_ROOT / entry["path"]
|
|
if not paper_dir.is_dir():
|
|
fail(errors, f"{pid}: registry path does not exist or is not a directory: {entry['path']}")
|
|
return
|
|
|
|
pub_path = paper_dir / "publication.yaml"
|
|
if not pub_path.exists():
|
|
fail(errors, f"{pid}: missing publication.yaml at {pub_path}")
|
|
return
|
|
|
|
with pub_path.open() as f:
|
|
try:
|
|
pub = yaml.safe_load(f)
|
|
except yaml.YAMLError as exc:
|
|
fail(errors, f"{pid}: publication.yaml did not parse: {exc}")
|
|
return
|
|
|
|
if pub.get("paper_id") != pid:
|
|
fail(errors, f"{pid}: publication.yaml paper_id {pub.get('paper_id')!r} != registry id {pid!r}")
|
|
|
|
if pub.get("publication_stage") != entry["stage"]:
|
|
fail(
|
|
errors,
|
|
f"{pid}: publication_stage {pub.get('publication_stage')!r} != registry stage {entry['stage']!r}",
|
|
)
|
|
|
|
if str(pub.get("version")) != str(entry["version"]):
|
|
fail(errors, f"{pid}: version {pub.get('version')!r} != registry version {entry['version']!r}")
|
|
|
|
if pub.get("publication_state") != entry["status"]:
|
|
fail(
|
|
errors,
|
|
f"{pid}: publication_state {pub.get('publication_state')!r} != registry status {entry['status']!r}",
|
|
)
|
|
|
|
expected_state = PAPER_STAGE_STATE_MAP.get(pub.get("publication_stage"))
|
|
if expected_state and pub.get("publication_state") != expected_state:
|
|
fail(
|
|
errors,
|
|
f"{pid}: publication_state {pub.get('publication_state')!r} does not match "
|
|
f"expected state {expected_state!r} for stage {pub.get('publication_stage')!r}",
|
|
)
|
|
|
|
for name in PAPER_MD_FILES:
|
|
md_path = paper_dir / name
|
|
if not md_path.exists():
|
|
fail(errors, f"{pid}: missing required artifact {name}")
|
|
continue
|
|
check_markdown_links(errors, pid, md_path)
|
|
|
|
|
|
def check_markdown_links(errors, pid, md_path):
|
|
text = md_path.read_text()
|
|
for match in MD_LINK_RE.finditer(text):
|
|
target = match.group(1).strip()
|
|
if target.startswith(("http://", "https://", "#", "mailto:")):
|
|
continue
|
|
target_path = (md_path.parent / target).resolve()
|
|
if not target_path.exists():
|
|
fail(errors, f"{pid}: {md_path.relative_to(REPO_ROOT)} has broken link: {target}")
|
|
|
|
|
|
def main():
|
|
errors = []
|
|
papers = check_registry(errors)
|
|
for entry in papers:
|
|
if all(f in entry for f in REQUIRED_REGISTRY_FIELDS):
|
|
check_paper(errors, entry)
|
|
|
|
if errors:
|
|
print(f"FAIL: {len(errors)} issue(s):")
|
|
for e in errors:
|
|
print(f" - {e}")
|
|
return 1
|
|
|
|
print(f"OK: registry valid, {len(papers)} paper(s) checked.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|