Obtain CR-000's first independent criticism via a freshly spawned, context-isolated agent instance with no prior authorship context (CR-000-X003-X007 in criticism.md). It found two internal-consistency violations self-criticism had missed: CR-000-C006's cited evidence was circular (X003) and conflated manuscript assertion with a real Evidence object (X004), so C006 is now contested and its evidence withdrawn. It also flagged that self-criticism had concentrated on infrastructure while leaving substantive claims unchallenged (X005), an unlabeled overreach in the Abstract (X006, now fixed), and a provenance-field ambiguity (X007). Add evidence.md's first two entries (CR-000-E001, CR-000-E002), both explicitly assessed as weak and left conjectural rather than inflated to supported. Add scripts/query_paper_graph.py, a minimal read-only script answering claim/evidence/criticism relation queries without manual grepping, as a partial response to CR-000-X001 (which remains open: it is a query layer over flat files, not a real graph). Revise manuscript.md (Abstract, SS11-14) and questions.md to reflect all of the above; Q2 gets a provisional "mixed" answer. Close CR-WP-0004 and hand off CR-WP-0005 (resolving remaining open criticism, and obtaining organizationally- rather than only procedurally-independent review) rather than starting M3, which this workplan judges premature while CR-000 still has open internal-consistency findings. 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
179 lines
6.9 KiB
Python
179 lines
6.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Query claim/evidence/criticism relations for a Coulomb-native paper.
|
|
|
|
Directly addresses CR-000-X001 in
|
|
research/programs/RP-000-researching-research/papers/paper-000/criticism.md:
|
|
CR-000's own claims/evidence/criticism cross-references are strings in flat
|
|
Markdown files, not edges in anything queryable. This script parses those
|
|
files' explicit relation fields ("Bears on:", "Challenges:") and answers
|
|
relation queries without manual grepping.
|
|
|
|
This is intentionally the smallest useful prototype, not a database or
|
|
service, per the institutional-minimality principle
|
|
(seed/CoulombResearchKernel.md SS10) and this workplan's own instruction
|
|
(workplans/CR-WP-0004-claim-evidence-discipline.md T03) to build only as
|
|
much as one paper's scale justifies. It has no dependencies beyond the
|
|
standard library. It is read-only: it does not modify the source
|
|
Markdown files, which remain the authoritative record.
|
|
|
|
Recognized relation fields, one per object block ("## CR-000-<T><NNN>"):
|
|
- claims.md entries: "Evidence for/against:" -> evidence ids (supports/bears-on)
|
|
"Criticism:" -> criticism ids (challenged-by)
|
|
- evidence.md entries: "Bears on:" -> claim ids (bears-on)
|
|
- criticism.md entries: "Challenges:" -> claim ids (challenges)
|
|
|
|
Usage:
|
|
python3 scripts/query_paper_graph.py <paper-dir> --id CR-000-C002
|
|
python3 scripts/query_paper_graph.py <paper-dir> --challenges CR-000-C002
|
|
python3 scripts/query_paper_graph.py <paper-dir> --bears-on CR-000-C003
|
|
python3 scripts/query_paper_graph.py <paper-dir> --list
|
|
|
|
Default paper-dir if omitted:
|
|
research/programs/RP-000-researching-research/papers/paper-000
|
|
"""
|
|
|
|
import argparse
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
DEFAULT_PAPER_DIR = (
|
|
REPO_ROOT / "research" / "programs" / "RP-000-researching-research" / "papers" / "paper-000"
|
|
)
|
|
|
|
BLOCK_RE = re.compile(r"^## (CR-\d{3}-[CEX]\d{3})\s*$", re.MULTILINE)
|
|
FIELD_RE = re.compile(
|
|
r"\*\*(Bears on|Challenges|Evidence for/against|Criticism):\*\*\s*(.*)", re.IGNORECASE
|
|
)
|
|
|
|
# Each relation field only ever points at one object type. Filtering by type
|
|
# here (rather than grabbing every CR-*-[CEX]\d{3} the line happens to
|
|
# mention) matters: a field's prose commonly *mentions* an id of a
|
|
# different type in passing (e.g. a claim's "Criticism:" field explaining
|
|
# itself by referring back to another claim id), which is not the same as
|
|
# that field *asserting a relation* to it. Confusing the two produced a
|
|
# false "C002 --challenges--> C003" edge in earlier testing.
|
|
FIELD_ID_TYPE = {
|
|
"bears on": "C",
|
|
"challenges": "C",
|
|
"evidence for/against": "E",
|
|
"criticism": "X",
|
|
}
|
|
|
|
|
|
def parse_file(path):
|
|
"""Return {object_id: {field_name: [related_id, ...]}} for one file."""
|
|
if not path.exists():
|
|
return {}
|
|
text = path.read_text()
|
|
starts = [(m.group(1), m.start()) for m in BLOCK_RE.finditer(text)]
|
|
objects = {}
|
|
for i, (obj_id, start) in enumerate(starts):
|
|
end = starts[i + 1][1] if i + 1 < len(starts) else len(text)
|
|
lines = text[start:end].splitlines()
|
|
fields = {}
|
|
current_field = None
|
|
current_text = []
|
|
for line in lines:
|
|
m = FIELD_RE.search(line)
|
|
if m:
|
|
if current_field:
|
|
fields.setdefault(current_field, []).append(" ".join(current_text))
|
|
current_field, first = m.group(1).lower(), m.group(2)
|
|
current_text = [first]
|
|
continue
|
|
# A field's value wraps onto following lines until a blank
|
|
# line, a metadata list item ("- **Critic:** ..."), or the
|
|
# next field/heading ends it.
|
|
if current_field:
|
|
if not line.strip() or line.lstrip().startswith("- **") or line.startswith("#"):
|
|
fields.setdefault(current_field, []).append(" ".join(current_text))
|
|
current_field, current_text = None, []
|
|
else:
|
|
current_text.append(line.strip())
|
|
if current_field:
|
|
fields.setdefault(current_field, []).append(" ".join(current_text))
|
|
|
|
resolved = {}
|
|
for field, chunks in fields.items():
|
|
type_letter = FIELD_ID_TYPE[field]
|
|
id_re = re.compile(rf"CR-\d{{3}}-{type_letter}\d{{3}}")
|
|
found = []
|
|
for chunk in chunks:
|
|
found.extend(id_re.findall(chunk))
|
|
resolved[field] = found
|
|
objects[obj_id] = resolved
|
|
return objects
|
|
|
|
|
|
def build_graph(paper_dir):
|
|
"""Return list of (subject, relation, object) edges across all files."""
|
|
edges = []
|
|
|
|
for obj_id, fields in parse_file(paper_dir / "claims.md").items():
|
|
for eid in fields.get("evidence for/against", []):
|
|
edges.append((eid, "bears-on", obj_id))
|
|
for xid in fields.get("criticism", []):
|
|
edges.append((xid, "challenges", obj_id))
|
|
|
|
for obj_id, fields in parse_file(paper_dir / "evidence.md").items():
|
|
for cid in fields.get("bears on", []):
|
|
edges.append((obj_id, "bears-on", cid))
|
|
|
|
for obj_id, fields in parse_file(paper_dir / "criticism.md").items():
|
|
for cid in fields.get("challenges", []):
|
|
edges.append((obj_id, "challenges", cid))
|
|
|
|
# de-duplicate while preserving order (same edge often stated in both
|
|
# directions, once in each file)
|
|
seen = set()
|
|
unique = []
|
|
for e in edges:
|
|
if e not in seen:
|
|
seen.add(e)
|
|
unique.append(e)
|
|
return unique
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
ap.add_argument("paper_dir", nargs="?", default=str(DEFAULT_PAPER_DIR))
|
|
ap.add_argument("--id", help="show all edges touching this id")
|
|
ap.add_argument("--challenges", metavar="ID", help="what challenges ID")
|
|
ap.add_argument("--bears-on", metavar="ID", help="what evidence bears on ID")
|
|
ap.add_argument("--list", action="store_true", help="list every edge")
|
|
args = ap.parse_args()
|
|
|
|
paper_dir = Path(args.paper_dir)
|
|
edges = build_graph(paper_dir)
|
|
|
|
if args.list:
|
|
for s, r, o in edges:
|
|
print(f"{s} --{r}--> {o}")
|
|
return 0
|
|
|
|
if args.challenges:
|
|
hits = [s for s, r, o in edges if r == "challenges" and o == args.challenges]
|
|
print("\n".join(hits) if hits else f"(nothing found challenging {args.challenges})")
|
|
return 0
|
|
|
|
if args.bears_on:
|
|
hits = [s for s, r, o in edges if r == "bears-on" and o == args.bears_on]
|
|
print("\n".join(hits) if hits else f"(nothing found bearing on {args.bears_on})")
|
|
return 0
|
|
|
|
if args.id:
|
|
hits = [(s, r, o) for s, r, o in edges if s == args.id or o == args.id]
|
|
if not hits:
|
|
print(f"(no edges touch {args.id})")
|
|
for s, r, o in hits:
|
|
print(f"{s} --{r}--> {o}")
|
|
return 0
|
|
|
|
ap.print_help()
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|