paperdelta 0.3.0__py3-none-any.whl
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.
- paperdelta/__init__.py +3 -0
- paperdelta/__main__.py +3 -0
- paperdelta/actions.py +56 -0
- paperdelta/agent.py +230 -0
- paperdelta/analysis.py +461 -0
- paperdelta/arguments.py +75 -0
- paperdelta/assets/report.css +15 -0
- paperdelta/assets/report.js +100 -0
- paperdelta/batch.py +344 -0
- paperdelta/batch_agent.py +126 -0
- paperdelta/batch_cli.py +295 -0
- paperdelta/builder.py +373 -0
- paperdelta/cli.py +559 -0
- paperdelta/config.py +78 -0
- paperdelta/coverage.py +300 -0
- paperdelta/demo.py +100 -0
- paperdelta/demo_project/paper/abstract.tex +4 -0
- paperdelta/demo_project/paper/appendix.tex +8 -0
- paperdelta/demo_project/paper/figure.tex +5 -0
- paperdelta/demo_project/paper/figures/accuracy.pdf +0 -0
- paperdelta/demo_project/paper/figures/accuracy.record.json +14 -0
- paperdelta/demo_project/paper/main.tex +11 -0
- paperdelta/demo_project/paper/results.tex +18 -0
- paperdelta/demo_project/paperdelta.yaml +65 -0
- paperdelta/demo_project/results/metrics.csv +9 -0
- paperdelta/demo_project/scripts/plot_accuracy.py +78 -0
- paperdelta/doctor.py +155 -0
- paperdelta/errors.py +47 -0
- paperdelta/guided.py +288 -0
- paperdelta/html_report.py +476 -0
- paperdelta/i18n.py +184 -0
- paperdelta/interactive.py +184 -0
- paperdelta/latex.py +407 -0
- paperdelta/locales/en.json +752 -0
- paperdelta/locales/zh-CN.json +752 -0
- paperdelta/mcp_server.py +233 -0
- paperdelta/metrics.py +99 -0
- paperdelta/models.py +269 -0
- paperdelta/onboarding.py +377 -0
- paperdelta/patches.py +370 -0
- paperdelta/preferences.py +31 -0
- paperdelta/records.py +419 -0
- paperdelta/repairs.py +358 -0
- paperdelta/reports.py +117 -0
- paperdelta/reviews.py +76 -0
- paperdelta/snapshots.py +35 -0
- paperdelta/sources.py +269 -0
- paperdelta/storage.py +201 -0
- paperdelta/tables.py +138 -0
- paperdelta/watch.py +268 -0
- paperdelta-0.3.0.dist-info/METADATA +98 -0
- paperdelta-0.3.0.dist-info/RECORD +56 -0
- paperdelta-0.3.0.dist-info/WHEEL +5 -0
- paperdelta-0.3.0.dist-info/entry_points.txt +2 -0
- paperdelta-0.3.0.dist-info/licenses/LICENSE +22 -0
- paperdelta-0.3.0.dist-info/top_level.txt +1 -0
paperdelta/__init__.py
ADDED
paperdelta/__main__.py
ADDED
paperdelta/actions.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""A deterministic review queue; actions describe work without performing it."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def review_actions(report):
|
|
5
|
+
groups = {}
|
|
6
|
+
|
|
7
|
+
def add(kind, subject, finding=None):
|
|
8
|
+
item = groups.setdefault(kind, {"kind": kind, "subjects": [], "findings": []})
|
|
9
|
+
if subject not in item["subjects"]:
|
|
10
|
+
item["subjects"].append(subject)
|
|
11
|
+
if finding and finding not in item["findings"]:
|
|
12
|
+
item["findings"].append(finding)
|
|
13
|
+
|
|
14
|
+
for finding in report["diagnostics"]:
|
|
15
|
+
rule, subject = finding["rule"], finding["subject"]
|
|
16
|
+
group, _, name = subject.partition(":")
|
|
17
|
+
if rule == "WATCH_PENDING":
|
|
18
|
+
kind = "wait_for_check"
|
|
19
|
+
elif rule == "VALUE_MISMATCH":
|
|
20
|
+
state = report["occurrences"][name]
|
|
21
|
+
kind = (
|
|
22
|
+
"review_claims"
|
|
23
|
+
if state.get("suggestion", {}).get("blocked_by")
|
|
24
|
+
else "update_numbers"
|
|
25
|
+
)
|
|
26
|
+
elif group == "claim" and rule == "CLAIM_FALSE":
|
|
27
|
+
kind = "review_claims"
|
|
28
|
+
elif group == "figure":
|
|
29
|
+
kind = "update_figures"
|
|
30
|
+
elif rule.startswith("ANCHOR_") or rule in {
|
|
31
|
+
"UNREACHABLE_TEX",
|
|
32
|
+
"NUMERIC_ANCHOR",
|
|
33
|
+
"UNSUPPORTED_SPAN",
|
|
34
|
+
}:
|
|
35
|
+
kind = "repair_bindings"
|
|
36
|
+
elif rule.startswith("EXCLUSION_"):
|
|
37
|
+
kind = "review_exclusions"
|
|
38
|
+
elif rule in {"NO_BINDINGS", "INCOMPLETE_COVERAGE"}:
|
|
39
|
+
kind = "complete_coverage"
|
|
40
|
+
else:
|
|
41
|
+
kind = "resolve_evidence"
|
|
42
|
+
add(kind, subject, finding["id"])
|
|
43
|
+
coverage = report["coverage"]
|
|
44
|
+
if coverage["unbound_numbers"] or coverage["unregistered_figures"]:
|
|
45
|
+
add("complete_coverage", "project")
|
|
46
|
+
order = (
|
|
47
|
+
"wait_for_check",
|
|
48
|
+
"resolve_evidence",
|
|
49
|
+
"repair_bindings",
|
|
50
|
+
"review_exclusions",
|
|
51
|
+
"review_claims",
|
|
52
|
+
"update_numbers",
|
|
53
|
+
"update_figures",
|
|
54
|
+
"complete_coverage",
|
|
55
|
+
)
|
|
56
|
+
return [groups[kind] for kind in order if kind in groups]
|
paperdelta/agent.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""Small read-only views for existing agents, using the same deterministic core."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from copy import deepcopy
|
|
6
|
+
from decimal import Decimal
|
|
7
|
+
|
|
8
|
+
from paperdelta import builder
|
|
9
|
+
from paperdelta.analysis import check_project
|
|
10
|
+
from paperdelta.errors import PaperDeltaError
|
|
11
|
+
from paperdelta.i18n import current_language, msg, tr, translated
|
|
12
|
+
from paperdelta.onboarding import propose_bindings, scan_project
|
|
13
|
+
from paperdelta.patches import create_patch
|
|
14
|
+
from paperdelta.snapshots import read_snapshot
|
|
15
|
+
from paperdelta.storage import Project, json_text, parse_json
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _wire(value):
|
|
19
|
+
if isinstance(value, Decimal):
|
|
20
|
+
return str(value)
|
|
21
|
+
if isinstance(value, dict):
|
|
22
|
+
return {key: _wire(item) for key, item in value.items()}
|
|
23
|
+
if isinstance(value, list):
|
|
24
|
+
return [_wire(item) for item in value]
|
|
25
|
+
return value
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def compact_report(report: dict) -> dict:
|
|
29
|
+
value = deepcopy(report)
|
|
30
|
+
for metric in value["metrics"].values():
|
|
31
|
+
for evidence in metric.get("evidence", []):
|
|
32
|
+
for key in ("records", "locations"):
|
|
33
|
+
evidence[key + "_total"] = len(evidence[key])
|
|
34
|
+
evidence[key] = evidence[key][:10]
|
|
35
|
+
value["agent_view"] = {
|
|
36
|
+
"evidence_row_limit": 10,
|
|
37
|
+
"decimal_encoding": msg("agent.decimal_encoding"),
|
|
38
|
+
"language": current_language(),
|
|
39
|
+
}
|
|
40
|
+
return _wire(translated(value))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class AgentSession:
|
|
44
|
+
def __init__(self, project: Project, config_path="paperdelta.yaml"):
|
|
45
|
+
from paperdelta.batch_agent import BatchSessions
|
|
46
|
+
|
|
47
|
+
self.project = project
|
|
48
|
+
self.config_path = config_path
|
|
49
|
+
self.batch = BatchSessions(project, config_path)
|
|
50
|
+
|
|
51
|
+
def batch_call(self, method, *args):
|
|
52
|
+
return _wire(translated(getattr(self.batch, method)(*args)))
|
|
53
|
+
|
|
54
|
+
def scan_project(self) -> dict:
|
|
55
|
+
return _wire(translated(scan_project(self.project, self.config_path)))
|
|
56
|
+
|
|
57
|
+
def check_project(self, baseline: str | None = None) -> dict:
|
|
58
|
+
previous = read_snapshot(self.project, baseline) if baseline else None
|
|
59
|
+
return compact_report(check_project(self.project.root, self.config_path, previous))
|
|
60
|
+
|
|
61
|
+
def propose_bindings(self, additions_json: str, rationale: dict[str, str]) -> dict:
|
|
62
|
+
value = propose_bindings(
|
|
63
|
+
self.project, parse_json(additions_json), rationale, config_path=self.config_path
|
|
64
|
+
)
|
|
65
|
+
return {
|
|
66
|
+
"status": "proposed",
|
|
67
|
+
"proposal_id": value["proposal_id"],
|
|
68
|
+
"proposal_json": json_text(value),
|
|
69
|
+
"next": tr("agent.next_binding"),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
def _draft_view(self, value):
|
|
73
|
+
preview = builder.inspect_draft(self.project, value)
|
|
74
|
+
for state in preview["metrics"].values():
|
|
75
|
+
for source in state["evidence"]:
|
|
76
|
+
source["records"] = source["records"][:10]
|
|
77
|
+
source["locations"] = source["locations"][:10]
|
|
78
|
+
return {
|
|
79
|
+
"status": "draft",
|
|
80
|
+
"draft_json": json_text(value),
|
|
81
|
+
"preview": _wire(preview),
|
|
82
|
+
"next": tr("agent.next_draft"),
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
def start_binding_draft(self):
|
|
86
|
+
value = builder.start_draft(self.project, self.config_path)
|
|
87
|
+
return {**self._draft_view(value), "discovery": self.scan_project()}
|
|
88
|
+
|
|
89
|
+
def add_draft_source(self, draft_json, name, path, format, columns, primary_key):
|
|
90
|
+
return self._draft_view(
|
|
91
|
+
builder.add_source(
|
|
92
|
+
self.project,
|
|
93
|
+
parse_json(draft_json),
|
|
94
|
+
name=name,
|
|
95
|
+
path=path,
|
|
96
|
+
format=format,
|
|
97
|
+
columns=columns,
|
|
98
|
+
primary_key=primary_key,
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def add_draft_metric(
|
|
103
|
+
self,
|
|
104
|
+
draft_json,
|
|
105
|
+
name,
|
|
106
|
+
source,
|
|
107
|
+
field,
|
|
108
|
+
unit,
|
|
109
|
+
reduce,
|
|
110
|
+
where,
|
|
111
|
+
expected_count,
|
|
112
|
+
seed_column,
|
|
113
|
+
expected_seeds,
|
|
114
|
+
):
|
|
115
|
+
return self._draft_view(
|
|
116
|
+
builder.add_metric(
|
|
117
|
+
self.project,
|
|
118
|
+
parse_json(draft_json),
|
|
119
|
+
name=name,
|
|
120
|
+
source=source,
|
|
121
|
+
field=field,
|
|
122
|
+
unit=unit,
|
|
123
|
+
reduce=reduce,
|
|
124
|
+
where=where,
|
|
125
|
+
expected_count=expected_count,
|
|
126
|
+
seed_column=seed_column,
|
|
127
|
+
expected_seeds=expected_seeds,
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def add_draft_derived(self, draft_json, name, operation, left, right):
|
|
132
|
+
return self._draft_view(
|
|
133
|
+
builder.add_derived(
|
|
134
|
+
self.project,
|
|
135
|
+
parse_json(draft_json),
|
|
136
|
+
name=name,
|
|
137
|
+
operation=operation,
|
|
138
|
+
left=left,
|
|
139
|
+
right=right,
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
def add_draft_locations(
|
|
144
|
+
self,
|
|
145
|
+
draft_json,
|
|
146
|
+
metric,
|
|
147
|
+
candidate_ids,
|
|
148
|
+
names,
|
|
149
|
+
display_kind,
|
|
150
|
+
places,
|
|
151
|
+
percent_symbol,
|
|
152
|
+
rationale,
|
|
153
|
+
):
|
|
154
|
+
return self._draft_view(
|
|
155
|
+
builder.add_occurrences(
|
|
156
|
+
self.project,
|
|
157
|
+
parse_json(draft_json),
|
|
158
|
+
metric=metric,
|
|
159
|
+
candidate_ids=candidate_ids,
|
|
160
|
+
names=names,
|
|
161
|
+
display_kind=display_kind,
|
|
162
|
+
places=places,
|
|
163
|
+
percent_symbol=percent_symbol,
|
|
164
|
+
rationale=rationale,
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def finish_binding_draft(self, draft_json):
|
|
169
|
+
value = builder.finalize_draft(self.project, parse_json(draft_json))
|
|
170
|
+
return {
|
|
171
|
+
"status": "proposed",
|
|
172
|
+
"proposal_id": value["proposal_id"],
|
|
173
|
+
"proposal_json": json_text(value),
|
|
174
|
+
"next": tr("agent.next_binding"),
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
def scan_binding_repairs(self, baseline=None):
|
|
178
|
+
from paperdelta.repairs import scan_repairs
|
|
179
|
+
|
|
180
|
+
return _wire(translated(scan_repairs(self.project, self.config_path, baseline)))
|
|
181
|
+
|
|
182
|
+
def propose_binding_repair(self, binding, rationale, candidate_id, file, exact, baseline):
|
|
183
|
+
from paperdelta.repairs import inspect_repair, propose_repairs
|
|
184
|
+
|
|
185
|
+
choice = {
|
|
186
|
+
"binding": binding,
|
|
187
|
+
"rationale": rationale,
|
|
188
|
+
"candidate_id": candidate_id,
|
|
189
|
+
"file": file,
|
|
190
|
+
"anchor": {"exact": exact} if exact is not None else None,
|
|
191
|
+
}
|
|
192
|
+
proposal = propose_repairs(
|
|
193
|
+
self.project, [choice], config_path=self.config_path, baseline=baseline
|
|
194
|
+
)
|
|
195
|
+
_, _, preview = inspect_repair(self.project, proposal)
|
|
196
|
+
return {
|
|
197
|
+
"status": "proposed",
|
|
198
|
+
"repair_json": json_text(proposal),
|
|
199
|
+
"repair_id": proposal["repair_id"],
|
|
200
|
+
"changes": _wire(preview["changes"]),
|
|
201
|
+
"preview": compact_report(preview["preview"]),
|
|
202
|
+
"next": tr("agent.next_repair"),
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
def explain_finding(self, finding_id: str) -> dict:
|
|
206
|
+
report = self.check_project()
|
|
207
|
+
finding = next((item for item in report["diagnostics"] if item["id"] == finding_id), None)
|
|
208
|
+
if finding is None:
|
|
209
|
+
raise PaperDeltaError("FINDING_MISSING", msg("error.FINDING_MISSING"))
|
|
210
|
+
group, _, name = finding["subject"].partition(":")
|
|
211
|
+
states = {"occurrence": "occurrences", "claim": "claims", "figure": "figures"}
|
|
212
|
+
state = report[states[group]].get(name, {}) if group in states else {}
|
|
213
|
+
refs = state.get("metrics", [state["metric"]] if "metric" in state else [])
|
|
214
|
+
return {
|
|
215
|
+
"finding": finding,
|
|
216
|
+
"state": state,
|
|
217
|
+
"metrics": {ref: report["metrics"][ref] for ref in refs},
|
|
218
|
+
"input_hashes": report["input_hashes"],
|
|
219
|
+
"agent_view": report["agent_view"],
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
def propose_patch(self, occurrences: list[str] | None = None) -> dict:
|
|
223
|
+
report = check_project(self.project.root, self.config_path)
|
|
224
|
+
patch = create_patch(self.project, report, occurrences)
|
|
225
|
+
return {
|
|
226
|
+
"status": "proposed",
|
|
227
|
+
"patch_json": json_text(patch),
|
|
228
|
+
"patch_id": patch["patch_id"],
|
|
229
|
+
"next": tr("agent.next_patch"),
|
|
230
|
+
}
|