Implement M2: claim/evidence discipline for CR-000 (CR-WP-0004)
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
This commit is contained in:
parent
b9c843041f
commit
dbf610b7b6
11 changed files with 853 additions and 115 deletions
179
scripts/query_paper_graph.py
Normal file
179
scripts/query_paper_graph.py
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
#!/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())
|
||||
Loading…
Add table
Add a link
Reference in a new issue