commonground-engine 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. commonground_engine-0.2.0/PKG-INFO +59 -0
  2. commonground_engine-0.2.0/README.md +45 -0
  3. commonground_engine-0.2.0/commonground/__init__.py +0 -0
  4. commonground_engine-0.2.0/commonground/__main__.py +499 -0
  5. commonground_engine-0.2.0/commonground/abduce.py +1332 -0
  6. commonground_engine-0.2.0/commonground/argmap.py +207 -0
  7. commonground_engine-0.2.0/commonground/bench.py +547 -0
  8. commonground_engine-0.2.0/commonground/crosscamp.py +408 -0
  9. commonground_engine-0.2.0/commonground/extract.py +239 -0
  10. commonground_engine-0.2.0/commonground/fol.py +496 -0
  11. commonground_engine-0.2.0/commonground/folio.py +139 -0
  12. commonground_engine-0.2.0/commonground/formalize.py +249 -0
  13. commonground_engine-0.2.0/commonground/ground.py +245 -0
  14. commonground_engine-0.2.0/commonground/lean.py +246 -0
  15. commonground_engine-0.2.0/commonground/load.py +52 -0
  16. commonground_engine-0.2.0/commonground/model.py +94 -0
  17. commonground_engine-0.2.0/commonground/smt.py +137 -0
  18. commonground_engine-0.2.0/commonground/verdict.py +118 -0
  19. commonground_engine-0.2.0/commonground_engine.egg-info/PKG-INFO +59 -0
  20. commonground_engine-0.2.0/commonground_engine.egg-info/SOURCES.txt +37 -0
  21. commonground_engine-0.2.0/commonground_engine.egg-info/dependency_links.txt +1 -0
  22. commonground_engine-0.2.0/commonground_engine.egg-info/requires.txt +6 -0
  23. commonground_engine-0.2.0/commonground_engine.egg-info/top_level.txt +1 -0
  24. commonground_engine-0.2.0/pyproject.toml +26 -0
  25. commonground_engine-0.2.0/setup.cfg +4 -0
  26. commonground_engine-0.2.0/tests/test_abduce.py +689 -0
  27. commonground_engine-0.2.0/tests/test_argmap.py +88 -0
  28. commonground_engine-0.2.0/tests/test_bench.py +260 -0
  29. commonground_engine-0.2.0/tests/test_crosscamp.py +401 -0
  30. commonground_engine-0.2.0/tests/test_extract.py +367 -0
  31. commonground_engine-0.2.0/tests/test_fol.py +86 -0
  32. commonground_engine-0.2.0/tests/test_folio.py +153 -0
  33. commonground_engine-0.2.0/tests/test_formalize.py +195 -0
  34. commonground_engine-0.2.0/tests/test_generated.py +16 -0
  35. commonground_engine-0.2.0/tests/test_golden.py +198 -0
  36. commonground_engine-0.2.0/tests/test_lean.py +99 -0
  37. commonground_engine-0.2.0/tests/test_load.py +44 -0
  38. commonground_engine-0.2.0/tests/test_report.py +458 -0
  39. commonground_engine-0.2.0/tests/test_semantics.py +125 -0
@@ -0,0 +1,59 @@
1
+ Metadata-Version: 2.4
2
+ Name: commonground-engine
3
+ Version: 0.2.0
4
+ Summary: First-order verdicts with checked evidence, Lean certificates and conflict analysis for arguments
5
+ License-Expression: MIT
6
+ Classifier: Programming Language :: Python :: 3
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: z3-solver>=4.13
10
+ Requires-Dist: python-sat>=1.8.dev13
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=8; extra == "dev"
13
+ Requires-Dist: hypothesis>=6.100; extra == "dev"
14
+
15
+ # commonground-engine
16
+
17
+ First-order verdicts with checked evidence: a parser, two decision engines, Lean
18
+ kernel certificates, conflict analysis, and a benchmark. Given reviewed statements
19
+ of one or more camps, it reports sets of formulas that are **verified sufficient**
20
+ to generate a camp's claims, ranks them by a declared cost, and shows where two
21
+ camps' systems agree and clash. It certifies deductions, not truth and not belief.
22
+
23
+ Every verdict carries a badge: `SUFFICIENT` (kernel-checked, needs a Lean 4
24
+ executable via `LEAN_BIN` and `--lean`), `SOLVER-CHECKED` (Z3 refutation / finite
25
+ model), or `PENDING` (no verdict — never silently picked).
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pip install commonground-engine
31
+ ```
32
+
33
+ Requires Python >= 3.10. Dependencies (`z3-solver`, `python-sat`) install automatically.
34
+
35
+ ## Quickstart
36
+
37
+ ```bash
38
+ python -m commonground check --premise "∀x (Human(x) → Mortal(x))" --premise "Human(socrates)" --conclusion "Mortal(socrates)"
39
+ python -m commonground --help
40
+ ```
41
+
42
+ Add `--lean` to `check` for a kernel-checked certificate (set `LEAN_BIN` to a Lean 4.35.0
43
+ `lean` executable); without it the same argument is answered `SOLVER-CHECKED`.
44
+
45
+ Commands: `check` (one argument), `analyze` (conflicts, concessions, justifications
46
+ for a debate file), `camp` / `crosscamp` (per-camp axiom systems and cross-camp
47
+ common ground / fault lines from reviewed records), `co2` (the golden CO2 example,
48
+ end to end; from a pip install pass `--dir <repo>/examples/co2-2021`, the default corpus path only resolves in a source checkout), `extract` (statements from text, with verbatim spans, via a
49
+ configured LLM endpoint), `folio` / `compare` / `report` (benchmarks and run reports).
50
+
51
+ ## Documentation
52
+
53
+ Full documentation, the worked CO2 example with a screenshot, the research behind
54
+ the definitions, and the recorded benchmark runs:
55
+ https://github.com/ysmouhib/common-ground
56
+
57
+ ## License
58
+
59
+ MIT.
@@ -0,0 +1,45 @@
1
+ # commonground-engine
2
+
3
+ First-order verdicts with checked evidence: a parser, two decision engines, Lean
4
+ kernel certificates, conflict analysis, and a benchmark. Given reviewed statements
5
+ of one or more camps, it reports sets of formulas that are **verified sufficient**
6
+ to generate a camp's claims, ranks them by a declared cost, and shows where two
7
+ camps' systems agree and clash. It certifies deductions, not truth and not belief.
8
+
9
+ Every verdict carries a badge: `SUFFICIENT` (kernel-checked, needs a Lean 4
10
+ executable via `LEAN_BIN` and `--lean`), `SOLVER-CHECKED` (Z3 refutation / finite
11
+ model), or `PENDING` (no verdict — never silently picked).
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ pip install commonground-engine
17
+ ```
18
+
19
+ Requires Python >= 3.10. Dependencies (`z3-solver`, `python-sat`) install automatically.
20
+
21
+ ## Quickstart
22
+
23
+ ```bash
24
+ python -m commonground check --premise "∀x (Human(x) → Mortal(x))" --premise "Human(socrates)" --conclusion "Mortal(socrates)"
25
+ python -m commonground --help
26
+ ```
27
+
28
+ Add `--lean` to `check` for a kernel-checked certificate (set `LEAN_BIN` to a Lean 4.35.0
29
+ `lean` executable); without it the same argument is answered `SOLVER-CHECKED`.
30
+
31
+ Commands: `check` (one argument), `analyze` (conflicts, concessions, justifications
32
+ for a debate file), `camp` / `crosscamp` (per-camp axiom systems and cross-camp
33
+ common ground / fault lines from reviewed records), `co2` (the golden CO2 example,
34
+ end to end; from a pip install pass `--dir <repo>/examples/co2-2021`, the default corpus path only resolves in a source checkout), `extract` (statements from text, with verbatim spans, via a
35
+ configured LLM endpoint), `folio` / `compare` / `report` (benchmarks and run reports).
36
+
37
+ ## Documentation
38
+
39
+ Full documentation, the worked CO2 example with a screenshot, the research behind
40
+ the definitions, and the recorded benchmark runs:
41
+ https://github.com/ysmouhib/common-ground
42
+
43
+ ## License
44
+
45
+ MIT.
File without changes
@@ -0,0 +1,499 @@
1
+ """Command line: ``python -m commonground <command>``.
2
+
3
+ check decide one argument given as formulas
4
+ analyze conflicts, concessions and justifications for a debate file
5
+ extract extract the statements of text files, with verbatim spans
6
+ camp per-camp report: statements -> axioms -> load -> certificates
7
+ crosscamp common ground + fault lines for two or more camp records
8
+ co2 the bundled CO2 golden example, end to end (headless)
9
+ folio run the FOLIO benchmark (reference formalizations, or a language model)
10
+ compare paired comparison of two language-model runs, as JSON
11
+ report the same comparison as a Markdown summary
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import json
17
+ import os
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ from . import argmap, bench, folio, lean
22
+ from .fol import Not, ParseError, parse
23
+ from .verdict import decide
24
+
25
+
26
+ def _check(args) -> int:
27
+ try:
28
+ premises = [parse(p) for p in args.premise]
29
+ conclusion = parse(args.conclusion)
30
+ except ParseError as exc:
31
+ print(f"not a formula: {exc}", file=sys.stderr)
32
+ return 2
33
+ verdict = decide(premises, conclusion, args.timeout_ms)
34
+ out = verdict.to_json()
35
+ if args.lean and verdict.label in ("True", "False"):
36
+ lean_bin = lean.find_lean()
37
+ if lean_bin is None:
38
+ out["lean"] = f"no Lean {lean.TOOLCHAIN} found (set LEAN_BIN)"
39
+ else:
40
+ goal = conclusion if verdict.label == "True" else Not(conclusion)
41
+ certificate = lean.certify([(premises, goal)], lean_bin)[0]
42
+ out["lean"] = {"certified": certificate.accepted, "axioms": certificate.axioms,
43
+ "reason": certificate.reason}
44
+ print(json.dumps(out, indent=2, ensure_ascii=False))
45
+ return 0
46
+
47
+
48
+ TS_HEADER = """// Generated by `python -m commonground analyze {source} --out {target}`.
49
+ // Do not edit: engine/tests/test_generated.py fails when this file is out of date.
50
+
51
+ export interface StatementGroup {{
52
+ statements: string[];
53
+ positions: string[];
54
+ }}
55
+
56
+ export interface DebateAnalysis {{
57
+ /** SHA-256 of the statements the analysis was computed from. */
58
+ input_hash: string;
59
+ premise_ids: string[];
60
+ positions: string[];
61
+ /** All premises of all positions have a common model. */
62
+ positions_compatible: boolean;
63
+ compatibility_witness: {{ domain_size: number; engine: string; rechecked_by_independent_evaluator: boolean }} | null;
64
+ /** Minimal sets of premises that cannot all hold. Empty when the positions are compatible. */
65
+ conflicts: StatementGroup[];
66
+ /** Smallest sets of premises whose withdrawal restores consistency. */
67
+ minimal_concessions: StatementGroup[];
68
+ /** Premises that belong to every maximal consistent set. */
69
+ uncontested: string[];
70
+ position_consistent: Record<string, boolean>;
71
+ conclusions: Record<string, {{
72
+ owner: string;
73
+ from_all_premises: string;
74
+ from_each_position: Record<string, string>;
75
+ /** Every minimal set of premises that entails the conclusion. */
76
+ justifications: string[][];
77
+ }}>;
78
+ statements_without_formula: string[];
79
+ }}
80
+
81
+ export const analysis: DebateAnalysis = {body};
82
+ """
83
+
84
+
85
+ def render_analysis(debate: str, out: str | None, timeout_ms: int = 5000) -> str:
86
+ _, statements = argmap.load_debate(debate)
87
+ body = json.dumps(argmap.analyze(statements, timeout_ms), indent=2, ensure_ascii=False)
88
+ if out and out.endswith(".ts"):
89
+ return TS_HEADER.format(source=Path(debate).as_posix(), target=Path(out).as_posix(), body=body)
90
+ return body + "\n"
91
+
92
+
93
+ def _analyze(args) -> int:
94
+ text = render_analysis(args.debate, args.out, args.timeout_ms)
95
+ if args.out:
96
+ Path(args.out).write_text(text, encoding="utf-8")
97
+ else:
98
+ print(text, end="")
99
+ return 0
100
+
101
+
102
+ def _folio(args) -> int:
103
+ out_dir = Path(args.out)
104
+ if args.mode == "reference":
105
+ lean_bin = lean.find_lean() if args.lean else None
106
+ if args.lean and lean_bin is None:
107
+ print(f"no Lean {lean.TOOLCHAIN} found (set LEAN_BIN)", file=sys.stderr)
108
+ return 2
109
+ summary = bench.reference(folio.fetch(Path(args.data)), out_dir, lean_bin, args.timeout_ms)
110
+ elif args.mode == "abduction":
111
+ lean_bin = lean.find_lean() if args.lean else None
112
+ if args.lean and lean_bin is None:
113
+ print(f"no Lean {lean.TOOLCHAIN} found (set LEAN_BIN)", file=sys.stderr)
114
+ return 2
115
+ data_file = folio.fetch(Path(args.data))
116
+ try:
117
+ summary = bench.abduction(data_file, out_dir, args.limit, args.timeout_ms, lean_bin,
118
+ args.lam, args.max_checks)
119
+ except bench.ConfigurationMismatch as exc:
120
+ print(exc, file=sys.stderr)
121
+ return 2
122
+ else:
123
+ from .formalize import OpenAICompatible, ProviderError
124
+ key = os.environ.get(args.api_key_env, "")
125
+ if not (args.base_url and args.model and key):
126
+ print(f"--base-url, --model and the {args.api_key_env} environment variable are required", file=sys.stderr)
127
+ return 2
128
+ try:
129
+ client = OpenAICompatible(args.base_url, args.model, key, json_mode=not args.no_json_mode,
130
+ temperature=None if args.no_temperature else args.temperature,
131
+ max_tokens=args.max_tokens)
132
+ except ValueError as exc:
133
+ print(exc, file=sys.stderr)
134
+ return 2
135
+ data_file = folio.fetch(Path(args.data))
136
+ try:
137
+ summary = bench.with_model(data_file, out_dir, client, args.mode, args.limit, args.timeout_ms)
138
+ except bench.ConfigurationMismatch as exc:
139
+ print(exc, file=sys.stderr)
140
+ return 2
141
+ except ProviderError as exc:
142
+ # The run stops instead of scoring an outage as wrong answers; the same command resumes it.
143
+ print(f"stopped: {exc}\nfinished examples are kept in {out_dir / 'runs.jsonl'}; "
144
+ "run the same command again to resume", file=sys.stderr)
145
+ if exc.status in (400, 422):
146
+ print("if the provider's message names response_format or temperature, add --no-json-mode "
147
+ "or --no-temperature (in a new --out directory: the settings are part of the run)",
148
+ file=sys.stderr)
149
+ return 3
150
+ print(json.dumps(summary, indent=2, ensure_ascii=False))
151
+ return 0
152
+
153
+
154
+ def _load_plausibilities(path: str | None) -> dict:
155
+ if not path:
156
+ return {}
157
+ data = json.loads(Path(path).read_text(encoding="utf-8"))
158
+ if not isinstance(data, dict):
159
+ raise ValueError("the plausibilities file must be a JSON object mapping FOL text to a number in [0, 1] "
160
+ "or a two-element range [min, max]")
161
+ for key, value in data.items():
162
+ ok = (isinstance(value, (int, float)) and not isinstance(value, bool)) or \
163
+ (isinstance(value, list) and len(value) == 2 and all(
164
+ isinstance(v, (int, float)) and not isinstance(v, bool) for v in value))
165
+ if not ok:
166
+ raise ValueError(f"the plausibility of {key!r} must be a number in [0, 1] or a [min, max] range, "
167
+ f"got {value!r}")
168
+ return data
169
+
170
+
171
+ def _lean_for(args) -> tuple[str | None, bool]:
172
+ """(lean executable or None, ok). Asking for kernel certificates without a Lean of the
173
+ pinned version is an error, not a silent fall-back to solver-only badges."""
174
+ if not args.lean:
175
+ return None, True
176
+ lean_bin = lean.find_lean()
177
+ if lean_bin is None:
178
+ print(f"no Lean {lean.TOOLCHAIN} found (set LEAN_BIN), or drop --lean for solver-checked verdicts",
179
+ file=sys.stderr)
180
+ return lean_bin, lean_bin is not None
181
+
182
+
183
+ def _camp_dir(out: Path, camp: str) -> Path:
184
+ """<out>/<camp>; a camp name that is not a plain directory name is refused, so a
185
+ record cannot make the report land outside the output directory."""
186
+ if (camp != camp.strip() or camp in ("", ".", "..") or camp.endswith(".")
187
+ or any(ch in '/\\:*?"<>|' or ord(ch) < 32 for ch in camp)):
188
+ raise ValueError(f"camp name {camp!r} cannot be used as a directory name")
189
+ return out / camp
190
+
191
+
192
+ def _write_camp_report(directory: Path, report: dict) -> None:
193
+ from . import abduce
194
+ directory.mkdir(parents=True, exist_ok=True)
195
+ (directory / "report.json").write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")
196
+ (directory / "report.md").write_text(abduce.render_markdown(report), encoding="utf-8")
197
+
198
+
199
+ def _camp(args) -> int:
200
+ from . import abduce
201
+ from .argmap import TooMany
202
+ try:
203
+ record = abduce.load_record(args.record)
204
+ candidates = abduce.load_candidates(args.candidates)
205
+ plausibilities = _load_plausibilities(args.plausibilities)
206
+ except (OSError, ValueError) as exc:
207
+ print(f"cannot read the inputs: {exc}", file=sys.stderr)
208
+ return 2
209
+ lean_bin, ok = _lean_for(args)
210
+ if not ok:
211
+ return 2
212
+ try:
213
+ report = abduce.report_camp(record, abduce.ScriptedSource(candidates), plausibilities,
214
+ args.lam, args.timeout_ms, lean_bin, args.max_checks)
215
+ except TooMany as exc:
216
+ print(f"input too large for the documented scope: {exc}", file=sys.stderr)
217
+ return 2
218
+ except ValueError as exc:
219
+ print(f"cannot run the analysis: {exc}", file=sys.stderr)
220
+ return 2
221
+ _write_camp_report(Path(args.out), report)
222
+ print(json.dumps({"out": str(Path(args.out)), "camp": report["camp"],
223
+ "selected_systems": len(report["systems"]),
224
+ "search_complete": report["search_complete"],
225
+ "abstention": report["abstention"]}, ensure_ascii=False))
226
+ return 0
227
+
228
+
229
+ def _crosscamp_summary(out: Path, result: dict, reports: list[dict]) -> str:
230
+ n_mus = sum(len(p.get("mus") or []) for p in result["fault_lines"]["per_system_pair"])
231
+ return json.dumps({"out": str(out), "camps": result["camps"],
232
+ "stated_agreed": len(result["common_ground"]["stated"]["agreed"]),
233
+ "fault_lines_found": n_mus,
234
+ "search_complete": {r["camp"]: r["search_complete"] for r in reports}},
235
+ ensure_ascii=False)
236
+
237
+
238
+ def _crosscamp(args) -> int:
239
+ from . import abduce, crosscamp
240
+ from .argmap import TooMany
241
+ if len(args.records) < 2:
242
+ print("crosscamp needs at least two camp records", file=sys.stderr)
243
+ return 2
244
+ if len(args.candidates) != len(args.records):
245
+ print("--candidates once per record, in the same order", file=sys.stderr)
246
+ return 2
247
+ out = Path(args.out)
248
+ try:
249
+ plausibilities = _load_plausibilities(args.plausibilities)
250
+ camps = []
251
+ for rec_path, cand_path in zip(args.records, args.candidates):
252
+ record = abduce.load_record(rec_path)
253
+ camps.append((record, abduce.load_candidates(cand_path), _camp_dir(out, record.camp)))
254
+ names = [record.camp for record, _, _ in camps]
255
+ if len(set(names)) != len(names):
256
+ raise ValueError(f"camp names must be unique, got {names}")
257
+ except (OSError, ValueError) as exc:
258
+ print(f"cannot read the inputs: {exc}", file=sys.stderr)
259
+ return 2
260
+ lean_bin, ok = _lean_for(args)
261
+ if not ok:
262
+ return 2
263
+ out.mkdir(parents=True, exist_ok=True)
264
+ camp_objs = []
265
+ try:
266
+ for record, candidates, camp_dir in camps:
267
+ report = abduce.report_camp(record, abduce.ScriptedSource(candidates), plausibilities,
268
+ args.lam, args.timeout_ms, lean_bin, args.max_checks)
269
+ _write_camp_report(camp_dir, report)
270
+ camp_objs.append(crosscamp.Camp(record, report))
271
+ result = crosscamp.analyze(camp_objs, args.timeout_ms, lean_bin)
272
+ except TooMany as exc:
273
+ print(f"input too large for the documented scope: {exc}; per-camp reports were written",
274
+ file=sys.stderr)
275
+ return 2
276
+ except ValueError as exc:
277
+ print(f"cannot run the analysis: {exc}", file=sys.stderr)
278
+ return 2
279
+ (out / "crosscamp.json").write_text(json.dumps(result, indent=2, ensure_ascii=False), encoding="utf-8")
280
+ (out / "crosscamp.md").write_text(crosscamp.render_markdown(result), encoding="utf-8")
281
+ print(_crosscamp_summary(out, result, [c.report for c in camp_objs]))
282
+ return 0
283
+
284
+
285
+ def _camp_from_corpus(camp_id, statements, golden_camp):
286
+ """Build a Record plus scripted candidates from the CO2 corpus files. The corpus ships
287
+ statement texts, not whole booklet pages: the document is the camp's statements joined
288
+ by newline, spans located in it, so the verbatim-span discipline still holds."""
289
+ from . import abduce, fol
290
+ texts = {s["id"]: s["text"] for s in statements if s["camp"] == camp_id}
291
+ fol_by_id = {s["id"]: s["fol"] for s in golden_camp["statements"]}
292
+ doc_id = f"doc-{camp_id}"
293
+ parts, spans = [], {}
294
+ offset = 0
295
+ for s in golden_camp["statements"]:
296
+ t = texts[s["id"]]
297
+ parts.append(t)
298
+ spans[s["id"]] = (offset, offset + len(t))
299
+ offset += len(t) + 1
300
+ record = abduce.Record(camp_id, [abduce.Document(doc_id, "\n".join(parts))],
301
+ [abduce.RecordStatement(s["id"], "endorsed", fol_by_id[s["id"]], doc_id,
302
+ texts[s["id"]], spans[s["id"]])
303
+ for s in golden_camp["statements"]])
304
+ cands = [abduce.Candidate(c["fol"], [(doc_id, texts[ev], spans[ev]) for ev in c["evidence"]])
305
+ for c in golden_camp["candidate_axioms"]]
306
+ plaus = {fol.to_text(fol.parse(c["fol"])): c["plausibility"] for c in golden_camp["candidate_axioms"]}
307
+ return record, cands, plaus
308
+
309
+
310
+ def _co2(args) -> int:
311
+ from . import abduce, crosscamp
312
+ from .argmap import TooMany
313
+ ex = Path(args.dir)
314
+ try:
315
+ statements = json.loads((ex / "statements.json").read_text(encoding="utf-8"))["statements"]
316
+ golden = json.loads((ex / "golden.json").read_text(encoding="utf-8"))
317
+ except (OSError, ValueError, KeyError) as exc:
318
+ print(f"cannot read the corpus in {ex}: {exc}", file=sys.stderr)
319
+ return 2
320
+ lean_bin, ok = _lean_for(args)
321
+ if not ok:
322
+ return 2
323
+ out = Path(args.out)
324
+ out.mkdir(parents=True, exist_ok=True)
325
+ camps = []
326
+ try:
327
+ for camp_id, g in golden["camps"].items():
328
+ record, cands, plaus = _camp_from_corpus(camp_id, statements, g)
329
+ report = abduce.report_camp(record, abduce.ScriptedSource(cands), plaus, args.lam,
330
+ args.timeout_ms, lean_bin, args.max_checks)
331
+ _write_camp_report(_camp_dir(out, camp_id), report)
332
+ camps.append(crosscamp.Camp(record, report))
333
+ result = crosscamp.analyze(camps, args.timeout_ms, lean_bin)
334
+ except TooMany as exc:
335
+ print(f"input too large for the documented scope: {exc}; per-camp reports were written",
336
+ file=sys.stderr)
337
+ return 2
338
+ except (ValueError, KeyError) as exc:
339
+ print(f"cannot run the analysis on the corpus in {ex}: {exc}", file=sys.stderr)
340
+ return 2
341
+ (out / "crosscamp.json").write_text(json.dumps(result, indent=2, ensure_ascii=False), encoding="utf-8")
342
+ (out / "crosscamp.md").write_text(crosscamp.render_markdown(result), encoding="utf-8")
343
+ print(_crosscamp_summary(out, result, [c.report for c in camps]))
344
+ return 0
345
+
346
+
347
+ def _extract(args) -> int:
348
+ from . import extract as extract_mod
349
+ from .formalize import OpenAICompatible, ProviderError
350
+ key = os.environ.get(args.api_key_env, "")
351
+ if not (args.base_url and args.model and key):
352
+ print(f"--base-url, --model and the {args.api_key_env} environment variable are required", file=sys.stderr)
353
+ return 2
354
+ try:
355
+ client = OpenAICompatible(args.base_url, args.model, key, json_mode=not args.no_json_mode,
356
+ temperature=None if args.no_temperature else args.temperature,
357
+ max_tokens=args.max_tokens)
358
+ except ValueError as exc:
359
+ print(exc, file=sys.stderr)
360
+ return 2
361
+ try:
362
+ documents = [(path, Path(path).read_text(encoding="utf-8")) for path in args.files]
363
+ except OSError as exc:
364
+ print(f"cannot read a document: {exc}", file=sys.stderr)
365
+ return 2
366
+ out_dir = Path(args.out)
367
+ try:
368
+ summary = extract_mod.run(client, documents, out_dir)
369
+ except ValueError as exc: # a configuration mismatch, or a document that fails validation
370
+ print(exc, file=sys.stderr)
371
+ return 2
372
+ except ProviderError as exc:
373
+ # The run stops instead of scoring an outage; the same command resumes it.
374
+ print(f"stopped: {exc}\nfinished documents are kept in {out_dir / 'runs.jsonl'}; "
375
+ "run the same command again to resume", file=sys.stderr)
376
+ return 3
377
+ print(json.dumps(summary, indent=2, ensure_ascii=False))
378
+ return 0
379
+
380
+
381
+ def _compare(args) -> int:
382
+ try:
383
+ if args.command == "report":
384
+ print(bench.report(Path(args.first), Path(args.second)), end="")
385
+ else:
386
+ print(json.dumps(bench.compare(Path(args.first), Path(args.second)), indent=2, ensure_ascii=False))
387
+ except (OSError, ValueError, KeyError) as exc:
388
+ print(f"cannot compare: {exc}", file=sys.stderr)
389
+ return 2
390
+ return 0
391
+
392
+
393
+ def main(argv: list[str] | None = None) -> int:
394
+ parser = argparse.ArgumentParser(prog="commonground", description=__doc__,
395
+ formatter_class=argparse.RawDescriptionHelpFormatter)
396
+ parser.add_argument("--timeout-ms", type=int, default=5000, help="solver limit per satisfiability question")
397
+ commands = parser.add_subparsers(dest="command", required=True)
398
+
399
+ check = commands.add_parser("check", help="decide one argument")
400
+ check.add_argument("--premise", action="append", default=[], help="a formula; repeat for each premise")
401
+ check.add_argument("--conclusion", required=True)
402
+ check.add_argument("--lean", action="store_true", help="also ask the Lean kernel to certify the verdict")
403
+ check.set_defaults(run=_check)
404
+
405
+ analyze = commands.add_parser("analyze", help="analyse a debate file")
406
+ analyze.add_argument("debate")
407
+ analyze.add_argument("--out")
408
+ analyze.set_defaults(run=_analyze)
409
+
410
+ camp = commands.add_parser("camp", help="per-camp report: statements -> axioms -> load -> certificates")
411
+ camp.add_argument("record", help="camp record JSON (camp, documents, statements with stances/spans)")
412
+ camp.add_argument("--candidates", required=True,
413
+ help="JSON file of scripted unstated candidates: {\"candidates\": [{\"fol\", \"quotes\": [...]}]}")
414
+ camp.add_argument("--plausibilities", help="optional JSON object mapping FOL text to a plausibility estimate: a number in [0, 1] or a [min, max] range")
415
+ camp.add_argument("--lam", type=float, default=0.3, help="cost weight of one axiom (default 0.3)")
416
+ camp.add_argument("--lean", action="store_true", help="also ask the Lean kernel to certify sufficiency")
417
+ camp.add_argument("--max-checks", type=int, default=12000,
418
+ help="budget of certification checks (default 12000, enough to finish the CO2 "
419
+ "`yes` search, AUDIT-4); a search it stops is reported as incomplete, never "
420
+ "as cost-minimal")
421
+ camp.add_argument("--out", required=True, help="output directory: report.json and report.md")
422
+ camp.set_defaults(run=_camp)
423
+
424
+ xc = commands.add_parser("crosscamp", help="cross-camp report: common ground + fault lines for 2+ camps")
425
+ xc.add_argument("records", nargs="+", help="camp record JSONs (two or more)")
426
+ xc.add_argument("--candidates", nargs="+", required=True,
427
+ help="one scripted-candidates JSON per record, in the same order")
428
+ xc.add_argument("--plausibilities", help="optional JSON object mapping FOL text to a plausibility estimate: a number in [0, 1] or a [min, max] range")
429
+ xc.add_argument("--lam", type=float, default=0.3, help="cost weight of one axiom (default 0.3)")
430
+ xc.add_argument("--lean", action="store_true", help="also ask the Lean kernel to certify sufficiency")
431
+ xc.add_argument("--timeout-ms", type=int, default=5000, help="solver limit per satisfiability question")
432
+ xc.add_argument("--max-checks", type=int, default=12000,
433
+ help="budget of certification checks per camp (default 12000); a search it stops is "
434
+ "reported as incomplete, never as cost-minimal")
435
+ xc.add_argument("--out", required=True,
436
+ help="output directory: <camp>/report.{json,md} per camp, plus crosscamp.{json,md}")
437
+ xc.set_defaults(run=_crosscamp)
438
+
439
+ co2 = commands.add_parser("co2", help="run the bundled CO2 golden example end to end (headless)")
440
+ co2.add_argument("--dir",
441
+ default=str(Path(__file__).resolve().parents[2] / "examples" / "co2-2021"),
442
+ help="corpus directory holding statements.json and golden.json")
443
+ co2.add_argument("--lam", type=float, default=0.3, help="cost weight of one axiom (default 0.3)")
444
+ co2.add_argument("--lean", action="store_true", help="also ask the Lean kernel to certify sufficiency")
445
+ co2.add_argument("--timeout-ms", type=int, default=5000, help="solver limit per satisfiability question")
446
+ co2.add_argument("--max-checks", type=int, default=12000,
447
+ help="cap on certification checks per camp (default 12000, enough for the CO2 `yes` "
448
+ "search; see AUDIT.md AUDIT-4)")
449
+ co2.add_argument("--out", required=True,
450
+ help="output directory: <camp>/report.{json,md} per camp, plus crosscamp.{json,md}")
451
+ co2.set_defaults(run=_co2)
452
+
453
+ ex = commands.add_parser("extract", help="extract the statements of text files, with verbatim spans")
454
+ ex.add_argument("files", nargs="+", help="text files; each file is one document, id = the path as given")
455
+ ex.add_argument("--out", required=True, help="output directory: runs.jsonl, config.json, summary.json")
456
+ ex.add_argument("--base-url")
457
+ ex.add_argument("--model")
458
+ ex.add_argument("--api-key-env", default="LLM_API_KEY", help="name of the variable holding the key")
459
+ ex.add_argument("--no-json-mode", action="store_true",
460
+ help="do not send response_format=json_object (for providers that reject it)")
461
+ ex.add_argument("--temperature", type=float, default=0.0, help="sampling temperature (default 0)")
462
+ ex.add_argument("--no-temperature", action="store_true",
463
+ help="send no temperature (for models that accept only their default)")
464
+ ex.add_argument("--max-tokens", type=int, default=8000, help="reply length limit (default 8000)")
465
+ ex.set_defaults(run=_extract)
466
+
467
+ fol = commands.add_parser("folio", help="run the FOLIO benchmark")
468
+ fol.add_argument("--data", default="data/FOLIO", help="where the dataset repository is (cloned if absent)")
469
+ fol.add_argument("--out", required=True)
470
+ fol.add_argument("--mode", choices=("reference", "llm", "direct", "abduction"), default="reference")
471
+ fol.add_argument("--lam", type=float, default=0.3, help="abduction mode: cost weight of one axiom (default 0.3)")
472
+ fol.add_argument("--max-checks", type=int, default=12000,
473
+ help="abduction mode: cap on certification checks per example (default 12000)")
474
+ fol.add_argument("--lean", action="store_true", help="reference mode: certify verdicts with Lean")
475
+ fol.add_argument("--base-url")
476
+ fol.add_argument("--model")
477
+ fol.add_argument("--api-key-env", default="LLM_API_KEY", help="name of the variable holding the key")
478
+ fol.add_argument("--limit", type=int, help="model and abduction modes: only the first N examples")
479
+ fol.add_argument("--no-json-mode", action="store_true",
480
+ help="do not send response_format=json_object (for providers that reject it)")
481
+ fol.add_argument("--temperature", type=float, default=0.0, help="model modes: sampling temperature (default 0)")
482
+ fol.add_argument("--no-temperature", action="store_true",
483
+ help="send no temperature (for models that accept only their default)")
484
+ fol.add_argument("--max-tokens", type=int, default=4000, help="model modes: reply length limit (default 4000)")
485
+ fol.set_defaults(run=_folio)
486
+
487
+ for name, text in (("compare", "paired comparison of two model runs (McNemar), as JSON"),
488
+ ("report", "the same comparison as a Markdown summary")):
489
+ compare = commands.add_parser(name, help=text)
490
+ compare.add_argument("first", help="output directory of one run, e.g. results/direct")
491
+ compare.add_argument("second", help="output directory of the other, e.g. results/llm")
492
+ compare.set_defaults(run=_compare)
493
+
494
+ args = parser.parse_args(argv)
495
+ return args.run(args)
496
+
497
+
498
+ if __name__ == "__main__":
499
+ sys.exit(main())