entrylib-rubric 0.4.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.
- entrylib_rubric-0.4.0.dist-info/METADATA +30 -0
- entrylib_rubric-0.4.0.dist-info/RECORD +10 -0
- entrylib_rubric-0.4.0.dist-info/WHEEL +5 -0
- entrylib_rubric-0.4.0.dist-info/licenses/LICENSE +201 -0
- entrylib_rubric-0.4.0.dist-info/top_level.txt +1 -0
- rubric/__init__.py +21 -0
- rubric/novelty.py +97 -0
- rubric/report.py +84 -0
- rubric/rubric.json +101 -0
- rubric/score.py +375 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: entrylib-rubric
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: EntryLib's scoring rubric: reliability scores of claimsets, computed by public rules.
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Project-URL: Homepage, https://github.com/entrylib/rubric
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Dynamic: license-file
|
|
11
|
+
|
|
12
|
+
# EntryLib scoring rubric
|
|
13
|
+
|
|
14
|
+
How [EntryLib](https://entrylib.org) scores the claimsets it publishes: a **reliability** score for every claim and for the paper, and a **novelty** label with a close-match check. The rules are public so that anyone can see why a claimset scored as it did, and what would raise it.
|
|
15
|
+
|
|
16
|
+
- [RUBRIC.md](https://github.com/entrylib/rubric/blob/main/RUBRIC.md): the rubric, normative and versioned.
|
|
17
|
+
- `src/rubric/`: the scorer that implements it (Python 3.10+, standard library only); the costs are in `src/rubric/rubric.json`.
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
import json, claimset.validate, rubric
|
|
21
|
+
doc = json.load(open("claimset.json")) # a published claimset file
|
|
22
|
+
result = rubric.score(doc, warnings=claimset.validate.validate(doc, published=True).warnings) # warnings optional
|
|
23
|
+
result["paper"]["score"], result["claims"]["C1"]["dots"]
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`rubric.report(result)` gives the scores as plain text: every issue that lowers a score and what would fix it. Before submitting, `claimset score paper.claimset.json` ([claimset](https://github.com/entrylib/claimset)) scores a claimset this way on your own machine, as it would be scored once published.
|
|
27
|
+
|
|
28
|
+
Install: `pip install git+https://github.com/entrylib/rubric`. The portal installs it the same way and records the rubric version with every score.
|
|
29
|
+
|
|
30
|
+
Planned: an AI reviewer ([#1](https://github.com/entrylib/rubric/issues/1)) and reproduction from artefacts ([#2](https://github.com/entrylib/rubric/issues/2)).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
entrylib_rubric-0.4.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
2
|
+
rubric/__init__.py,sha256=zzhQy_saVoRptJqv--h04Q7CU0pCfgKYTGx3qur_3Gs,1251
|
|
3
|
+
rubric/novelty.py,sha256=eCC9L8793VUPZLjTfLKnjv1_WRot1Wr_2HmuYWs_PvQ,4794
|
|
4
|
+
rubric/report.py,sha256=v_45iZZjKy5V-chsCmwNQKf1pg7idNMxRgYh8dhtUjs,4554
|
|
5
|
+
rubric/rubric.json,sha256=8ETVWMfJOniY5EcP7ZlToL12a7nIE2Oe_cEoUVOF0q4,5249
|
|
6
|
+
rubric/score.py,sha256=Or6SW2hA56bitgF9rPrcL57BxLZjlD988pkpc6EzFc4,19405
|
|
7
|
+
entrylib_rubric-0.4.0.dist-info/METADATA,sha256=e50xvBloOqGGU77aM9Kk30vWtbBcLPXQhHcFGZW_A_M,1817
|
|
8
|
+
entrylib_rubric-0.4.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
9
|
+
entrylib_rubric-0.4.0.dist-info/top_level.txt,sha256=PB2Y0f8oFtUr8uA8PkbDIgEHmZsqJvCBG1zeQtm_4EI,7
|
|
10
|
+
entrylib_rubric-0.4.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright [yyyy] [name of copyright owner]
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
rubric
|
rubric/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""EntryLib's scoring rubric (RUBRIC.md): reliability scores of a claimset's claims and of the paper, computed by rules
|
|
2
|
+
from the published claimset file, and the novelty close-match check.
|
|
3
|
+
|
|
4
|
+
score(doc, warnings=None) -> the scores of a claimset: claims, paper, problems (see score.py); warnings
|
|
5
|
+
are the claimset validator's for the file (its validation report)
|
|
6
|
+
assess(doc, find=None, warnings=None)
|
|
7
|
+
-> score() with novelty, comparing principal claims with what find() returns
|
|
8
|
+
novelty(claim, matches, ...) -> a claim's novelty label and close-match result (see novelty.py)
|
|
9
|
+
report(scores, biggest_n=0) -> the scores as a plain-text report (see report.py)
|
|
10
|
+
similarity(a, b) -> how closely two claim statements match, 0 to 1
|
|
11
|
+
stems(statement) -> the stems similarity compares
|
|
12
|
+
VERSION -> the rubric version every score records
|
|
13
|
+
|
|
14
|
+
Standard library only, Python 3.10+.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from .novelty import assess, novelty, similarity, stems
|
|
18
|
+
from .report import report
|
|
19
|
+
from .score import RUBRIC, VERSION, band, score
|
|
20
|
+
|
|
21
|
+
__all__ = ["RUBRIC", "assess", "VERSION", "band", "novelty", "report", "score", "similarity", "stems"]
|
rubric/novelty.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Novelty in this version: the authors' label and the close-match check (RUBRIC.md §6).
|
|
2
|
+
|
|
3
|
+
similarity(a, b) -> 0..1, the overlap of the stems of two claim statements
|
|
4
|
+
novelty(claim, matches, concept=None) -> {"label", "check", "matches"}
|
|
5
|
+
|
|
6
|
+
`matches` are the claims of claimsets published on EntryLib before this one that the site found as candidates, each
|
|
7
|
+
{"claim": "entrylib:2026.000014#C3", "statement": "..."}; the site finds candidates (by full-text search), this module
|
|
8
|
+
decides which match closely and whether the claim acknowledges them.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
CLOSE = 0.6
|
|
14
|
+
|
|
15
|
+
STOP = set("""
|
|
16
|
+
a about above after again against all also an and any are as at be because been before being below between both but by
|
|
17
|
+
can could did do does doing down during each either few for from further had has have having here how if in into is it
|
|
18
|
+
its itself just less more most much must no nor not of off on once only or other our out over own same shall should so
|
|
19
|
+
some such than that the their them then there these they this those through to too under until up upon very was we were
|
|
20
|
+
what when where whether which while who whom why will with within without would yet
|
|
21
|
+
""".split())
|
|
22
|
+
|
|
23
|
+
# Endings removed, longest first, when at least three letters remain (RUBRIC.md §6).
|
|
24
|
+
ENDINGS = ("ational", "ations", "ation", "ments", "ment", "ings", "ing", "ies", "ied", "ed", "es", "ly", "s")
|
|
25
|
+
|
|
26
|
+
TERM = re.compile(r"\[\[[^\]:]+:([^\]]*)\]\]")
|
|
27
|
+
MATH = re.compile(r"\$\$.+?\$\$|\$[^$]+\$|\\\(.+?\\\)", re.S)
|
|
28
|
+
WORD = re.compile(r"[^\W_]+")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _stem(w):
|
|
32
|
+
for e in ENDINGS:
|
|
33
|
+
if w.endswith(e) and len(w) - len(e) >= 3:
|
|
34
|
+
return w[: -len(e)]
|
|
35
|
+
return w
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def stems(statement):
|
|
39
|
+
"""The set of stems of a statement: terms as their text, mathematics removed, stop words dropped."""
|
|
40
|
+
text = MATH.sub(" ", TERM.sub(lambda m: m.group(1), statement if isinstance(statement, str) else ""))
|
|
41
|
+
return {_stem(w) for w in WORD.findall(text.lower()) if w not in STOP}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def similarity(a, b):
|
|
45
|
+
"""Overlap coefficient of the stems of two statements: shared stems over those of the shorter one."""
|
|
46
|
+
sa, sb = stems(a), stems(b)
|
|
47
|
+
if not sa or not sb:
|
|
48
|
+
return 0.0
|
|
49
|
+
return len(sa & sb) / min(len(sa), len(sb))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _concept(global_id):
|
|
53
|
+
"""entrylib:2026.000014@v2#C3 -> entrylib:2026.000014"""
|
|
54
|
+
return re.split(r"[@#]", global_id, maxsplit=1)[0] if isinstance(global_id, str) else ""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def novelty(claim, matches, concept=None):
|
|
58
|
+
"""The claim's novelty label and the close-match check: check is "none" (no close match on EntryLib),
|
|
59
|
+
"acknowledged" (close matches, all related to by the claim) or "unacknowledged"; matches lists the close ones,
|
|
60
|
+
each with its similarity and whether it is acknowledged. Matches from the claim's own claimset (`concept`) are
|
|
61
|
+
ignored."""
|
|
62
|
+
label = claim.get("novelty")
|
|
63
|
+
targets = {r.get("target") for r in claim.get("relations") or [] if isinstance(r, dict)}
|
|
64
|
+
targets |= {_concept(t) for t in targets}
|
|
65
|
+
close = []
|
|
66
|
+
for m in matches or []:
|
|
67
|
+
gid = m.get("claim") if isinstance(m, dict) else None
|
|
68
|
+
if not isinstance(gid, str) or (concept and _concept(gid) == concept):
|
|
69
|
+
continue
|
|
70
|
+
s = similarity(claim.get("statement"), m.get("statement"))
|
|
71
|
+
if s >= CLOSE:
|
|
72
|
+
close.append({"claim": gid, "similarity": round(s, 2),
|
|
73
|
+
"acknowledged": gid in targets or _concept(gid) in targets})
|
|
74
|
+
close.sort(key=lambda m: -m["similarity"])
|
|
75
|
+
check = ("none" if not close else "acknowledged" if all(m["acknowledged"] for m in close)
|
|
76
|
+
else "unacknowledged")
|
|
77
|
+
return {"label": label if label != "not_applicable" else None, "check": check, "matches": close}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def assess(doc, find=None, warnings=None):
|
|
81
|
+
"""score(doc) with `novelty`: {claim id: novelty() + `checked`} for every scored claim that has a novelty label.
|
|
82
|
+
Each principal claim is compared with the published claims that find(statement) returns ([{"claim", "statement"}],
|
|
83
|
+
supplied by the caller: the portal's search, or EntryLib's public search); `checked` says whether it was. Without
|
|
84
|
+
`find` nothing is compared and every `checked` is false. `warnings` are the claimset validator's, as for score()."""
|
|
85
|
+
from .score import score
|
|
86
|
+
result = score(doc, warnings)
|
|
87
|
+
concept = (doc.get("paper") or {}).get("id") if isinstance(doc.get("paper"), dict) else None
|
|
88
|
+
novelty_of = {}
|
|
89
|
+
for c in doc.get("claims") or []:
|
|
90
|
+
if not isinstance(c, dict) or c.get("id") not in result["claims"] or not c.get("novelty"):
|
|
91
|
+
continue
|
|
92
|
+
compared = find is not None and c.get("role") == "principal"
|
|
93
|
+
n = novelty(c, find(c.get("statement")) if compared else [], concept)
|
|
94
|
+
n["checked"] = compared
|
|
95
|
+
novelty_of[c["id"]] = n
|
|
96
|
+
result["novelty"] = novelty_of
|
|
97
|
+
return result
|
rubric/report.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""The scores as a plain-text report (RUBRIC.md §4.3), for the authors and their agents: of a published claimset (the
|
|
2
|
+
portal's issues.txt) or of one not yet submitted (`claimset score`).
|
|
3
|
+
|
|
4
|
+
report(scores, biggest_n=0) -> the report; `biggest_n` > 0 adds the problems that cost one claim the most
|
|
5
|
+
open_problems(scores) -> [(id, problem, {claim: cost})] of the problems that lower a score, in rubric order
|
|
6
|
+
biggest(scores, n) -> the n problems that cost one claim the most, most costly first
|
|
7
|
+
|
|
8
|
+
`scores` is what score() or assess() returns; assess() adds `novelty`, and only claims it compared (`checked`) can be
|
|
9
|
+
in question.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _sentence(words):
|
|
14
|
+
words = str(words)
|
|
15
|
+
return words if words.rstrip().endswith((".", "!", "?")) else words + "."
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _lowered(scores, pid, cid, cost):
|
|
19
|
+
""""C5 (-10)", or "C5 (-10, from C3)" for a problem the claim inherits from what it rests on (§2)."""
|
|
20
|
+
source = ((scores.get("claims") or {}).get(cid) or {}).get("inherited", {}).get(pid)
|
|
21
|
+
return f"{cid} (-{cost}, from {', '.join(source)})" if source else f"{cid} (-{cost})"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def open_problems(scores):
|
|
25
|
+
"""[(id, problem, {claim id: cost})] of every problem that lowers some claim's score, in the order of score()
|
|
26
|
+
(those of the principal claims first, most costly first)."""
|
|
27
|
+
out = []
|
|
28
|
+
for pid, p in (scores.get("problems") or {}).items():
|
|
29
|
+
lowers = {cid: c["costs"][pid] for cid, c in (scores.get("claims") or {}).items() if pid in c["problems"]}
|
|
30
|
+
if lowers:
|
|
31
|
+
out.append((pid, p, lowers))
|
|
32
|
+
return out
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def biggest(scores, n=3):
|
|
36
|
+
"""The n problems that cost one claim the most, most costly first (ties keep the rubric's order)."""
|
|
37
|
+
return sorted(open_problems(scores), key=lambda x: -max(x[2].values()))[:n]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def report(scores, biggest_n=0, name="reliability"):
|
|
41
|
+
"""The report of RUBRIC.md §4.3, as text ending in a newline. `name`: what the score is called, for a site that
|
|
42
|
+
shows it under another name (§4.3)."""
|
|
43
|
+
out = []
|
|
44
|
+
paper = scores.get("paper")
|
|
45
|
+
if paper and "lowest_score" in paper: # rubric 0.3: the mean, with the lowest shown
|
|
46
|
+
n = len(paper.get("principal") or [])
|
|
47
|
+
out.append(f"PAPER: {name} {paper['score']} ({paper['band']}), the mean of its {n} principal claim"
|
|
48
|
+
f"{'s' if n != 1 else ''}; the lowest is {paper['lowest']} at {paper['lowest_score']}.")
|
|
49
|
+
elif paper:
|
|
50
|
+
out.append(f"PAPER: {name} {paper['score']} ({paper['band']}), the score of its lowest principal claim, "
|
|
51
|
+
f"{paper['lowest']}. Raising the lowest principal claim raises the paper.")
|
|
52
|
+
else:
|
|
53
|
+
out.append("PAPER: no score (no principal claim is scored).")
|
|
54
|
+
out.append("")
|
|
55
|
+
top = biggest(scores, biggest_n) if biggest_n else []
|
|
56
|
+
if top:
|
|
57
|
+
out.append("BIGGEST ISSUES, most costly first:")
|
|
58
|
+
out += [f" {pid} ({', '.join(f'{c} -{cost}' for c, cost in lowers.items())}): {_sentence(p['text'])} "
|
|
59
|
+
f"To fix: {p['fix']}" for pid, p, lowers in top]
|
|
60
|
+
out.append("")
|
|
61
|
+
flagged = {cid: n for cid, n in (scores.get("novelty") or {}).items()
|
|
62
|
+
if isinstance(n, dict) and n.get("check") == "unacknowledged" and n.get("checked", True)}
|
|
63
|
+
if flagged:
|
|
64
|
+
out.append("NOVELTY IN QUESTION: close matches on EntryLib that the claim does not relate to:")
|
|
65
|
+
out += [f" {cid}: " + ", ".join(f"{m['claim']} (similarity {m['similarity']})" for m in n["matches"]
|
|
66
|
+
if not m["acknowledged"]) for cid, n in flagged.items()]
|
|
67
|
+
out.append("")
|
|
68
|
+
problems = open_problems(scores)
|
|
69
|
+
if not problems:
|
|
70
|
+
out.append("No issues: every scored claim keeps all its points.")
|
|
71
|
+
return "\n".join(out) + "\n"
|
|
72
|
+
out.append("ISSUES, those of the principal claims first, most costly first:")
|
|
73
|
+
for pid, p, lowers in problems:
|
|
74
|
+
out += ["", f"{pid}. {_sentence(p['text'])}",
|
|
75
|
+
f" Check: {p['check']} (group: {p['group']}); costs up to {p['cost']} points.",
|
|
76
|
+
f" Lowers: {', '.join(_lowered(scores, pid, c, cost) for c, cost in lowers.items())}."]
|
|
77
|
+
if p.get("objects"):
|
|
78
|
+
out.append(f" Concerns: {', '.join(p['objects'])}.")
|
|
79
|
+
out.append(f" To fix: {p['fix']}")
|
|
80
|
+
out += ["", "CLAIMS: score (band): issues"]
|
|
81
|
+
for cid, c in scores["claims"].items():
|
|
82
|
+
issues = ", ".join(f"{p} (-{c['costs'][p]})" for p in c["problems"]) or "none"
|
|
83
|
+
out.append(f"{cid}: {c['score']} ({c['band']}): {issues}")
|
|
84
|
+
return "\n".join(out) + "\n"
|
rubric/rubric.json
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.4",
|
|
3
|
+
"bands": [
|
|
4
|
+
{"name": "high", "label": "High", "min": 80},
|
|
5
|
+
{"name": "medium", "label": "Medium", "min": 60},
|
|
6
|
+
{"name": "low", "label": "Low", "min": 0}
|
|
7
|
+
],
|
|
8
|
+
"groups": [
|
|
9
|
+
{"name": "support", "label": "Support", "about": "what the claim rests on: evidence, proof, citation, argument or judgement"},
|
|
10
|
+
{"name": "plan", "label": "Plan", "about": "whether the test was planned before it was run, and how the plan was anchored"},
|
|
11
|
+
{"name": "checkable", "label": "Checkable", "about": "whether a reader can obtain what is needed to check the claim"},
|
|
12
|
+
{"name": "context", "label": "Context", "about": "where the claim holds, what earlier work it relates to, and which recorded results its numbers come from"},
|
|
13
|
+
{"name": "open", "label": "Open problems", "about": "limitations and assumptions that are still open"}
|
|
14
|
+
],
|
|
15
|
+
"dots": {"green_max": 2, "amber_max": 10},
|
|
16
|
+
"checks": {
|
|
17
|
+
"support_kind": {
|
|
18
|
+
"group": "support", "count": "kind",
|
|
19
|
+
"costs": {"evidence": 0, "evidence_unspecified": 10, "formal_checked": 0, "formal_unchecked": 25,
|
|
20
|
+
"formal_disputed": 45, "cited": 20, "consensus": 25, "argument_from_claims": 10, "argument": 30,
|
|
21
|
+
"expert_judgement": 45, "unstated": 70},
|
|
22
|
+
"text": "Rests on {what}",
|
|
23
|
+
"fix": "Support the claim with evidence the claimset records, citing the result entries or table rows it reports, or have its proof checked."
|
|
24
|
+
},
|
|
25
|
+
"exploratory_inference": {
|
|
26
|
+
"group": "plan", "count": "kind",
|
|
27
|
+
"costs": {"with_public_record": 5, "without_public_record": 15},
|
|
28
|
+
"text": "{type} claim tested without a plan recorded beforehand{record}",
|
|
29
|
+
"fix": "Test it again under a lab-record plan or a registration, or publish the lab record that shows every run."
|
|
30
|
+
},
|
|
31
|
+
"exploratory_principal": {
|
|
32
|
+
"group": "plan", "count": "kind",
|
|
33
|
+
"costs": {"with_public_record": 3, "without_public_record": 5},
|
|
34
|
+
"text": "A principal claim reported without a plan recorded beforehand{record}",
|
|
35
|
+
"fix": "Plan the measurement in the lab record or a registration before running it, or publish the lab record that shows every run."
|
|
36
|
+
},
|
|
37
|
+
"plan_anchor": {
|
|
38
|
+
"group": "plan", "count": "kind",
|
|
39
|
+
"costs": {"git_remote": 2, "none": 10},
|
|
40
|
+
"text": "The plan's time is anchored {how}",
|
|
41
|
+
"fix": "Anchor plans with the platform, a timestamp authority (RFC 3161) or a registry."
|
|
42
|
+
},
|
|
43
|
+
"plan_deviations": {
|
|
44
|
+
"group": "plan", "count": "kind", "costs": {"deviations": 5},
|
|
45
|
+
"text": "The test deviated from its plan",
|
|
46
|
+
"fix": "Each deviation is a limitation: say how it bears on the result."
|
|
47
|
+
},
|
|
48
|
+
"plan_flags": {
|
|
49
|
+
"group": "plan", "count": "kind", "cost_each": 5,
|
|
50
|
+
"text": "The lab record flags the test: {flags}",
|
|
51
|
+
"fix": "Rerun the test as planned, or explain the flag in a limitation."
|
|
52
|
+
},
|
|
53
|
+
"verifiability": {
|
|
54
|
+
"group": "checkable", "count": "kind",
|
|
55
|
+
"costs": {"audit_only": 10, "unverifiable": 45},
|
|
56
|
+
"text": "Verifiability: {level}",
|
|
57
|
+
"fix": "Make the data, code and models the claim rests on available, or state why they cannot be."
|
|
58
|
+
},
|
|
59
|
+
"verifiability_flags": {
|
|
60
|
+
"group": "checkable", "count": "kind", "cost_each": 5,
|
|
61
|
+
"text": "Flags: {flags}",
|
|
62
|
+
"fix": "Publish what each flag says is missing."
|
|
63
|
+
},
|
|
64
|
+
"no_scope": {
|
|
65
|
+
"group": "context", "count": "kind", "costs": {"missing": 10},
|
|
66
|
+
"text": "No scope: where the claim holds is not stated",
|
|
67
|
+
"fix": "Give the claim a scope: population, setting, period, data, conditions, exclusions."
|
|
68
|
+
},
|
|
69
|
+
"no_prior_work": {
|
|
70
|
+
"group": "context", "count": "kind", "costs": {"missing": 5},
|
|
71
|
+
"text": "A principal claim related to no earlier work",
|
|
72
|
+
"fix": "Relate the claim to the earlier work it extends, addresses or departs from."
|
|
73
|
+
},
|
|
74
|
+
"numbers_without_results": {
|
|
75
|
+
"group": "context", "count": "kind", "costs": {"missing": 5},
|
|
76
|
+
"text": "Numbers in the statement match no recorded result",
|
|
77
|
+
"fix": "Record each number as a result or table cell of the evidence the claim cites, and list it in the support's results; or correct the number."
|
|
78
|
+
},
|
|
79
|
+
"limitation": {
|
|
80
|
+
"group": "open", "count": "each",
|
|
81
|
+
"costs": {
|
|
82
|
+
"could_reverse": {"none": 10, "partial": 5, "addressed": 0},
|
|
83
|
+
"could_bias_magnitude": {"none": 6, "partial": 3, "addressed": 0},
|
|
84
|
+
"reduces_precision": {"none": 3, "partial": 1, "addressed": 0},
|
|
85
|
+
"reduces_transferability": {"none": 1, "partial": 0, "addressed": 0}
|
|
86
|
+
},
|
|
87
|
+
"text": "Limitation {id} ({impact}, {mitigation}): {statement}",
|
|
88
|
+
"fix": "Mitigate it with evidence, or narrow the claim's scope so that it no longer applies."
|
|
89
|
+
},
|
|
90
|
+
"no_limitation": {
|
|
91
|
+
"group": "open", "count": "kind", "costs": {"missing": 10},
|
|
92
|
+
"text": "A principal claim that declares no limitation",
|
|
93
|
+
"fix": "Declare what could bias, reverse or limit the claim within its scope, each as a limitation that affects it."
|
|
94
|
+
},
|
|
95
|
+
"unchecked_assumption": {
|
|
96
|
+
"group": "open", "count": "each", "cost_each": 3,
|
|
97
|
+
"text": "Assumption {id} is not checked: {statement}",
|
|
98
|
+
"fix": "Test the assumption, or analyse how sensitive the claim is to it."
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
rubric/score.py
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
"""Reliability scores (RUBRIC.md §2–5).
|
|
2
|
+
|
|
3
|
+
score(doc, warnings=None) -> {
|
|
4
|
+
"rubric": "0.4",
|
|
5
|
+
"claims": {"C1": {"score": 94, "band": "high", "dots": {"support": "green", ...}, "problems": ["P3", ...],
|
|
6
|
+
"costs": {"P3": 6, ...}, "inherited": {"P5": ["C2"], ...}}, ...},
|
|
7
|
+
"assumptions": ["C24", ...], # not scored
|
|
8
|
+
"paper": {"score": 74, "band": "medium", "dots": {...}, "problems": [...], "principal": [...], "lowest": "C6",
|
|
9
|
+
"lowest_score": 60, "lowest_problems": [...]} or None when no principal claim is scored,
|
|
10
|
+
"problems": {"P1": {"check", "group", "cost", "claims", "arises_in", "objects", "text", "fix"}, ...},
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
`warnings` are the claimset validator's warnings for the file ([{rule, where, message}], its published validation
|
|
14
|
+
report), when given: warning 17 decides `numbers_without_results`, and a limitation with warning 46 (it restates the
|
|
15
|
+
declared competing interest) costs nothing.
|
|
16
|
+
|
|
17
|
+
A problem in "problems" is one aggregated problem (§3.6) over the principal claims and what they rest on (then over the
|
|
18
|
+
other claims); its "cost" is the most it costs any one claim, "claims" every claim it lowers and "arises_in" the claims
|
|
19
|
+
it is found on. A claim lists the problems that reach it, with what each costs that claim under "costs", and under
|
|
20
|
+
"inherited" the ones it has from what it rests on, with the claims they arise in. The paper's score is the mean of its
|
|
21
|
+
principal claims' (§5); "problems" of the paper are every problem open in its principal claims, for the explanation.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import re
|
|
26
|
+
from importlib import resources
|
|
27
|
+
|
|
28
|
+
RUBRIC = json.loads(resources.files(__package__).joinpath("rubric.json").read_text(encoding="utf-8"))
|
|
29
|
+
VERSION = RUBRIC["version"]
|
|
30
|
+
CHECKS = RUBRIC["checks"]
|
|
31
|
+
GROUPS = [g["name"] for g in RUBRIC["groups"]]
|
|
32
|
+
|
|
33
|
+
EVIDENCE_KINDS = {"empirical_quant", "empirical_qual", "empirical_obs", "simulation", "demonstration"}
|
|
34
|
+
OBSERVATION_KINDS = {"empirical_quant", "empirical_obs", "simulation"} # evidence whose observations are cited
|
|
35
|
+
SPECIFIC = re.compile(r"\.R\d+$|\.T\d+\.r\d+$|#row=") # a result entry, a table row, data rows
|
|
36
|
+
STUDIED_ROLES = {"subject", "baseline"}
|
|
37
|
+
INFERENTIAL = {"associational", "causal", "mechanistic", "predictive"}
|
|
38
|
+
SUPPORT_WORDS = {"evidence": "evidence",
|
|
39
|
+
"evidence_unspecified": "evidence named without the observations the claim reports",
|
|
40
|
+
"argument_from_claims": "an argument from other claims",
|
|
41
|
+
"formal_checked": "a checked proof", "formal_unchecked": "an unchecked proof",
|
|
42
|
+
"formal_disputed": "a disputed proof", "cited": "cited work only", "consensus": "consensus only",
|
|
43
|
+
"argument": "argument only", "expert_judgement": "expert judgement only",
|
|
44
|
+
"unstated": "no stated support"}
|
|
45
|
+
TERM = re.compile(r"\[\[[^\]:]+:([^\]]*)\]\]")
|
|
46
|
+
MATH = re.compile(r"\$\$.+?\$\$|\$[^$]+\$|\\\(.+?\\\)", re.S)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def band(value):
|
|
50
|
+
"""The band of a score: high, medium or low (§4.1)."""
|
|
51
|
+
if value is None:
|
|
52
|
+
return None
|
|
53
|
+
for b in RUBRIC["bands"]:
|
|
54
|
+
if value >= b["min"]:
|
|
55
|
+
return b["name"]
|
|
56
|
+
return RUBRIC["bands"][-1]["name"]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _dot(points):
|
|
60
|
+
d = RUBRIC["dots"]
|
|
61
|
+
return "green" if points <= d["green_max"] else "amber" if points <= d["amber_max"] else "red"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _list(x):
|
|
65
|
+
return x if isinstance(x, list) else []
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _dict(x):
|
|
69
|
+
return x if isinstance(x, dict) else {}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def is_assumption(c):
|
|
73
|
+
kinds = [s.get("kind") for s in _list(c.get("support")) if isinstance(s, dict)]
|
|
74
|
+
return bool(kinds) and all(k == "assumption" for k in kinds)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _public_record(doc):
|
|
78
|
+
rid = _dict(_dict(doc.get("paper")).get("exploration")).get("lab_record")
|
|
79
|
+
for a in _list(doc.get("artefacts")):
|
|
80
|
+
if isinstance(a, dict) and a.get("id") == rid:
|
|
81
|
+
return _dict(a.get("availability")).get("level") == "public"
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _cites_observations(s, evidence):
|
|
86
|
+
"""Whether a support entry names the observations its claim reports (§3.1): a result entry, a table row or data
|
|
87
|
+
rows, or evidence that records no results or tables."""
|
|
88
|
+
if any(isinstance(r, str) and SPECIFIC.search(r) for r in _list(s.get("results"))):
|
|
89
|
+
return True
|
|
90
|
+
e = _dict(evidence.get(s.get("evidence")))
|
|
91
|
+
return bool(e) and not _list(e.get("results")) and not _list(e.get("tables"))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _support_cost(c, by_id, evidence):
|
|
95
|
+
costs = CHECKS["support_kind"]["costs"]
|
|
96
|
+
best = None
|
|
97
|
+
for s in _list(c.get("support")):
|
|
98
|
+
kind = s.get("kind") if isinstance(s, dict) else None
|
|
99
|
+
if kind in OBSERVATION_KINDS:
|
|
100
|
+
key = "evidence" if _cites_observations(s, evidence) else "evidence_unspecified"
|
|
101
|
+
elif kind in EVIDENCE_KINDS:
|
|
102
|
+
key = "evidence"
|
|
103
|
+
elif kind == "formal_proof":
|
|
104
|
+
v = c.get("verification")
|
|
105
|
+
key = ("formal_checked" if v in ("machine_checked", "agent_checked", "peer_checked")
|
|
106
|
+
else "formal_disputed" if v == "disputed" else "formal_unchecked")
|
|
107
|
+
elif kind == "argument":
|
|
108
|
+
premises = [by_id.get(d) for d in _list(c.get("depends_on")) if isinstance(d, str)]
|
|
109
|
+
key = "argument_from_claims" if any(p and not is_assumption(p) for p in premises) else "argument"
|
|
110
|
+
elif kind in costs:
|
|
111
|
+
key = kind
|
|
112
|
+
else:
|
|
113
|
+
continue
|
|
114
|
+
if best is None or costs[key] < costs[best]:
|
|
115
|
+
best = key
|
|
116
|
+
return best
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _api_model_studied(c, doc, evidence):
|
|
120
|
+
"""Whether every API-only model in the evidence the claim's support names is what is studied there (§3.3)."""
|
|
121
|
+
weights = {m.get("id"): m.get("weights") for m in _list(doc.get("models")) if isinstance(m, dict)}
|
|
122
|
+
roles = [_dict(dm).get("role") for s in _list(c.get("support")) if isinstance(s, dict)
|
|
123
|
+
for dm in _list(_dict(evidence.get(s.get("evidence"))).get("models"))
|
|
124
|
+
if weights.get(_dict(dm).get("model")) == "api_only"]
|
|
125
|
+
return bool(roles) and all(r in STUDIED_ROLES for r in roles)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
NAME_WITH_DIGITS = re.compile(r"(?<![\w.])[A-Za-z0-9_]+(?:-[A-Za-z0-9_]+)*")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _has_numbers(statement):
|
|
132
|
+
text = MATH.sub(" ", TERM.sub(" ", statement if isinstance(statement, str) else ""))
|
|
133
|
+
text = re.sub(r"\b[A-Z]{1,2}\d+(?:\.[A-Za-z]*\d+)*\b", " ", text) # object IDs: C4, E1.T1.r2
|
|
134
|
+
text = NAME_WITH_DIGITS.sub(lambda m: " " if re.search(r"[A-Za-z]", m.group(0)) else m.group(0), text) # GPT-4o
|
|
135
|
+
return re.search(r"\d", text) is not None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
#: Planning values of claims planned before they were run (§3.2): tests, and estimates (claimset v0.42).
|
|
139
|
+
PLANNED = ("confirmatory", "confirmatory_with_deviations", "planned_estimate")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _unplanned_cost(c, public_record):
|
|
143
|
+
"""What the claim would cost for being exploratory (§3.2): the most its plan's problems may cost."""
|
|
144
|
+
record = "with_public_record" if public_record else "without_public_record"
|
|
145
|
+
if c.get("type") in INFERENTIAL:
|
|
146
|
+
return CHECKS["exploratory_inference"]["costs"][record]
|
|
147
|
+
if c.get("role") == "principal" and c.get("type") != "formal":
|
|
148
|
+
return CHECKS["exploratory_principal"]["costs"][record]
|
|
149
|
+
return 0
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _capped(plan, limit):
|
|
153
|
+
"""A plan's problems cut so that together they cost at most `limit`, the costliest kept first (§3.2)."""
|
|
154
|
+
out = []
|
|
155
|
+
for check, key, cost, claims, args in sorted(plan, key=lambda i: -i[2]):
|
|
156
|
+
cost = min(cost, limit)
|
|
157
|
+
limit -= cost
|
|
158
|
+
if cost > 0:
|
|
159
|
+
out.append((check, key, cost, claims, args))
|
|
160
|
+
return out
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _instances(doc, c, by_id, public_record, evidence, warned):
|
|
164
|
+
"""The problems of one claim itself: [(check, key, cost, objects, args)]. `warned` is the set of (rule, where) of
|
|
165
|
+
the validator's warnings, or None when they were not given."""
|
|
166
|
+
out = []
|
|
167
|
+
cid = c.get("id")
|
|
168
|
+
formal = c.get("type") == "formal"
|
|
169
|
+
|
|
170
|
+
key = _support_cost(c, by_id, evidence)
|
|
171
|
+
cost = CHECKS["support_kind"]["costs"].get(key, 0) if key else CHECKS["support_kind"]["costs"]["unstated"]
|
|
172
|
+
if cost:
|
|
173
|
+
out.append(("support_kind", cid, cost, [cid], {"what": SUPPORT_WORDS.get(key or "unstated")}))
|
|
174
|
+
|
|
175
|
+
planning = c.get("planning")
|
|
176
|
+
record = "; the lab record shows every run" if public_record else ""
|
|
177
|
+
if planning in (None, "exploratory"):
|
|
178
|
+
t = c.get("type")
|
|
179
|
+
cost = _unplanned_cost(c, public_record)
|
|
180
|
+
if t in INFERENTIAL:
|
|
181
|
+
out.append(("exploratory_inference", cid, cost, [cid],
|
|
182
|
+
{"type": ("An " if t[0] in "aeiou" else "A ") + t, "record": record}))
|
|
183
|
+
elif cost:
|
|
184
|
+
out.append(("exploratory_principal", cid, cost, [cid], {"type": t or "", "record": record}))
|
|
185
|
+
plan = []
|
|
186
|
+
if planning in PLANNED:
|
|
187
|
+
anchor = c.get("planned_at_anchor")
|
|
188
|
+
costs = CHECKS["plan_anchor"]["costs"]
|
|
189
|
+
if anchor == "git_remote" or anchor in (None, "none"):
|
|
190
|
+
how = "by a git push only" if anchor == "git_remote" else "by nothing"
|
|
191
|
+
plan.append(("plan_anchor", cid, costs["git_remote" if anchor == "git_remote" else "none"], [cid],
|
|
192
|
+
{"how": how}))
|
|
193
|
+
if planning == "confirmatory_with_deviations":
|
|
194
|
+
plan.append(("plan_deviations", cid, CHECKS["plan_deviations"]["costs"]["deviations"], [cid], {}))
|
|
195
|
+
flags = [f for f in _list(_dict(c.get("test")).get("flags")) + _list(_dict(c.get("estimate")).get("flags"))
|
|
196
|
+
if isinstance(f, str)]
|
|
197
|
+
if flags:
|
|
198
|
+
ch = CHECKS["plan_flags"]
|
|
199
|
+
plan.append(("plan_flags", cid, sum(repeated([ch["cost_each"]] * len(flags))), [cid],
|
|
200
|
+
{"flags": ", ".join(f.replace("_", " ") for f in flags)}))
|
|
201
|
+
out += _capped(plan, _unplanned_cost(c, public_record)) if planning in PLANNED else plan
|
|
202
|
+
|
|
203
|
+
level = c.get("verifiability")
|
|
204
|
+
if level in CHECKS["verifiability"]["costs"]:
|
|
205
|
+
out.append(("verifiability", cid, CHECKS["verifiability"]["costs"][level], [cid],
|
|
206
|
+
{"level": level.replace("_", " ")}))
|
|
207
|
+
vflags = [f for f in _list(c.get("verifiability_flags"))
|
|
208
|
+
if isinstance(f, str) and f not in ("unstated_support", "unanchored_plan")
|
|
209
|
+
and not (f == "api_model_dependency" and _api_model_studied(c, doc, evidence))]
|
|
210
|
+
if vflags:
|
|
211
|
+
ch = CHECKS["verifiability_flags"]
|
|
212
|
+
out.append(("verifiability_flags", cid, sum(repeated([ch["cost_each"]] * len(vflags))), [cid],
|
|
213
|
+
{"flags": ", ".join(f.replace("_", " ") for f in vflags)}))
|
|
214
|
+
|
|
215
|
+
if not formal and not _dict(c.get("scope")):
|
|
216
|
+
out.append(("no_scope", cid, CHECKS["no_scope"]["costs"]["missing"], [cid], {}))
|
|
217
|
+
if c.get("role") == "principal" and c.get("novelty") != "not_applicable":
|
|
218
|
+
outside = any(isinstance(r, dict) and r.get("id_type") != "local" for r in _list(c.get("relations")))
|
|
219
|
+
cited = any(isinstance(s, dict) and s.get("kind") == "cited" for s in _list(c.get("support")))
|
|
220
|
+
if not (outside or cited):
|
|
221
|
+
out.append(("no_prior_work", cid, CHECKS["no_prior_work"]["costs"]["missing"], [cid], {}))
|
|
222
|
+
if warned is not None:
|
|
223
|
+
unsourced = not formal and ("W17", f"claims:{cid}.statement") in warned
|
|
224
|
+
else:
|
|
225
|
+
unsourced = not formal and _has_numbers(c.get("statement")) and not any(
|
|
226
|
+
isinstance(s, dict) and _list(s.get("results")) for s in _list(c.get("support")))
|
|
227
|
+
if unsourced:
|
|
228
|
+
out.append(("numbers_without_results", cid, CHECKS["numbers_without_results"]["costs"]["missing"], [cid], {}))
|
|
229
|
+
|
|
230
|
+
lim_costs = CHECKS["limitation"]["costs"]
|
|
231
|
+
for lim in _list(doc.get("limitations")):
|
|
232
|
+
if isinstance(lim, dict) and cid in _list(lim.get("affects")) and not _restated(lim, warned):
|
|
233
|
+
cost = _dict(lim_costs.get(lim.get("impact"))).get(lim.get("mitigation") or "none", 0)
|
|
234
|
+
if cost:
|
|
235
|
+
out.append(("limitation", lim.get("id"), cost, [cid],
|
|
236
|
+
{"id": lim.get("id"), "impact": str(lim.get("impact", "")).replace("_", " "),
|
|
237
|
+
"mitigation": "not mitigated" if (lim.get("mitigation") or "none") == "none"
|
|
238
|
+
else f"mitigation {lim.get('mitigation')}", "statement": lim.get("statement", "")}))
|
|
239
|
+
for aid in _list(c.get("assumptions")):
|
|
240
|
+
a = by_id.get(aid)
|
|
241
|
+
if isinstance(a, dict) and a.get("check") == "unchecked":
|
|
242
|
+
out.append(("unchecked_assumption", aid, CHECKS["unchecked_assumption"]["cost_each"], [cid],
|
|
243
|
+
{"id": aid, "statement": a.get("statement", "")}))
|
|
244
|
+
return out
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _restated(lim, warned):
|
|
248
|
+
"""A limitation that only restates the declared competing interest (warning 46; §3.5)."""
|
|
249
|
+
return warned is not None and ("W46", f"limitations:{lim.get('id')}.statement") in warned
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _closure(cid, by_id):
|
|
253
|
+
"""The claim and everything it rests on (depends_on and assumptions), followed to the end."""
|
|
254
|
+
seen, stack = [], [cid]
|
|
255
|
+
while stack:
|
|
256
|
+
x = stack.pop()
|
|
257
|
+
if x in seen or x not in by_id:
|
|
258
|
+
continue
|
|
259
|
+
seen.append(x)
|
|
260
|
+
c = by_id[x]
|
|
261
|
+
stack.extend(i for i in _list(c.get("depends_on")) + _list(c.get("assumptions")) if isinstance(i, str))
|
|
262
|
+
return seen
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def repeated(costs):
|
|
266
|
+
"""The costs of repeated problems of one check on one claim (RUBRIC.md §3.6): sorted from the largest, the first
|
|
267
|
+
in full and each next one half the one before (rounded down, at least 1), so that every one removed or mitigated
|
|
268
|
+
lowers the total and the total stays below twice the largest."""
|
|
269
|
+
return [0 if c <= 0 else max(1, c // 2 ** i) for i, c in enumerate(sorted(costs, reverse=True))]
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _aggregate(instances):
|
|
273
|
+
"""Instances counted as RUBRIC.md §3.6: {(check, key): {cost, claims, objects, args}}; key is None for `kind`."""
|
|
274
|
+
out = {}
|
|
275
|
+
for check, key, cost, claims, args in instances:
|
|
276
|
+
k = (check, None) if CHECKS[check]["count"] == "kind" else (check, key)
|
|
277
|
+
p = out.setdefault(k, {"check": check, "cost": 0, "claims": [], "args": args})
|
|
278
|
+
if cost > p["cost"]:
|
|
279
|
+
p["cost"], p["args"] = cost, args
|
|
280
|
+
for c in claims:
|
|
281
|
+
if c not in p["claims"]:
|
|
282
|
+
p["claims"].append(c)
|
|
283
|
+
for check in {k[0] for k in out if CHECKS[k[0]]["count"] == "each"}:
|
|
284
|
+
items = sorted((p for k, p in out.items() if k[0] == check), key=lambda p: -p["cost"])
|
|
285
|
+
for p, cost in zip(items, repeated([p["cost"] for p in items])):
|
|
286
|
+
p["cost"] = cost
|
|
287
|
+
return out
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _sort_claims(ids):
|
|
291
|
+
return sorted(ids, key=lambda i: (len(i), i))
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _result(agg):
|
|
295
|
+
total = sum(p["cost"] for p in agg.values())
|
|
296
|
+
value = max(0, 100 - total)
|
|
297
|
+
lost = {g: 0 for g in GROUPS}
|
|
298
|
+
for p in agg.values():
|
|
299
|
+
lost[CHECKS[p["check"]]["group"]] += p["cost"]
|
|
300
|
+
return value, {g: _dot(v) for g, v in lost.items()}
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def score(doc, warnings=None):
|
|
304
|
+
"""The reliability scores of a published claimset (RUBRIC.md §2–5), given the validator's warnings for it when
|
|
305
|
+
they are available (its validation report)."""
|
|
306
|
+
claims = [c for c in _list(doc.get("claims")) if isinstance(c, dict) and isinstance(c.get("id"), str)]
|
|
307
|
+
by_id = {c["id"]: c for c in claims}
|
|
308
|
+
evidence = {e.get("id"): e for e in _list(doc.get("evidence")) if isinstance(e, dict)}
|
|
309
|
+
warned = None if warnings is None else {(_dict(w).get("rule"), _dict(w).get("where")) for w in _list(warnings)}
|
|
310
|
+
public_record = _public_record(doc)
|
|
311
|
+
own = {c["id"]: _instances(doc, c, by_id, public_record, evidence, warned) for c in claims if not is_assumption(c)}
|
|
312
|
+
limited = {cid for lim in _list(doc.get("limitations")) if isinstance(lim, dict) and not _restated(lim, warned)
|
|
313
|
+
for cid in _list(lim.get("affects")) if isinstance(cid, str)}
|
|
314
|
+
for c in claims: # no_limitation (§3.5): nothing it rests on declares one
|
|
315
|
+
if (c["id"] in own and c.get("role") == "principal" and c.get("type") != "formal"
|
|
316
|
+
and not limited & set(_closure(c["id"], by_id))):
|
|
317
|
+
own[c["id"]].append(("no_limitation", c["id"], CHECKS["no_limitation"]["costs"]["missing"], [c["id"]], {}))
|
|
318
|
+
|
|
319
|
+
def gathered(ids):
|
|
320
|
+
return [i for x in ids for cid in _closure(x, by_id) for i in own.get(cid, [])]
|
|
321
|
+
|
|
322
|
+
principal = [c["id"] for c in claims if c.get("role") == "principal" and c["id"] in own]
|
|
323
|
+
paper_agg = _aggregate(gathered(principal)) if principal else {}
|
|
324
|
+
everything = _aggregate(gathered(list(own)))
|
|
325
|
+
# Problem IDs: those of the paper first, by cost, then the rest; stable for a given file.
|
|
326
|
+
order = sorted(paper_agg, key=lambda k: (-paper_agg[k]["cost"], k[0], str(k[1])))
|
|
327
|
+
order += sorted((k for k in everything if k not in paper_agg), key=lambda k: (k[0], str(k[1])))
|
|
328
|
+
pid = {k: f"P{i + 1}" for i, k in enumerate(order)}
|
|
329
|
+
|
|
330
|
+
def describe(k, p):
|
|
331
|
+
ch = CHECKS[k[0]]
|
|
332
|
+
try:
|
|
333
|
+
text = ch["text"].format(**p["args"])
|
|
334
|
+
except (KeyError, IndexError):
|
|
335
|
+
text = ch["text"]
|
|
336
|
+
return {"check": k[0], "group": ch["group"], "cost": p["cost"], "claims": _sort_claims(p["claims"]),
|
|
337
|
+
"arises_in": _sort_claims(p["claims"]),
|
|
338
|
+
"objects": [k[1]] if k[1] is not None else [], "text": text, "fix": ch["fix"]}
|
|
339
|
+
|
|
340
|
+
problems = {pid[k]: describe(k, paper_agg.get(k) or everything[k]) for k in order}
|
|
341
|
+
|
|
342
|
+
out_claims = {}
|
|
343
|
+
for cid in own:
|
|
344
|
+
agg = _aggregate(gathered([cid]))
|
|
345
|
+
mine = set(_aggregate(own[cid]))
|
|
346
|
+
value, dots = _result(agg)
|
|
347
|
+
listed = sorted(agg, key=lambda k: (-agg[k]["cost"], pid[k]))
|
|
348
|
+
out_claims[cid] = {"score": value, "band": band(value), "dots": dots,
|
|
349
|
+
"problems": [pid[k] for k in listed if agg[k]["cost"]],
|
|
350
|
+
"costs": {pid[k]: agg[k]["cost"] for k in listed if agg[k]["cost"]},
|
|
351
|
+
"inherited": {pid[k]: problems[pid[k]]["arises_in"] for k in listed
|
|
352
|
+
if agg[k]["cost"] and k not in mine}}
|
|
353
|
+
|
|
354
|
+
# A problem's cost is the most it costs any one claim (a repeated problem may cost one claim less than another);
|
|
355
|
+
# its claims are every claim it lowers, as the claims' own lists say (§2: inherited problems).
|
|
356
|
+
for k in order:
|
|
357
|
+
lowered = [cid for cid, c in out_claims.items() if pid[k] in c["costs"]]
|
|
358
|
+
if lowered:
|
|
359
|
+
problems[pid[k]]["cost"] = max(out_claims[c]["costs"][pid[k]] for c in lowered)
|
|
360
|
+
problems[pid[k]]["claims"] = _sort_claims(lowered)
|
|
361
|
+
|
|
362
|
+
paper = None
|
|
363
|
+
if principal:
|
|
364
|
+
# The paper's score is the mean of its principal claims' (§5), shown with the lowest; its dots, the worst of
|
|
365
|
+
# each group among them.
|
|
366
|
+
lowest = min(principal, key=lambda c: (out_claims[c]["score"], len(c), c))
|
|
367
|
+
value = int(sum(out_claims[c]["score"] for c in principal) / len(principal) + 0.5)
|
|
368
|
+
rank = {"green": 0, "amber": 1, "red": 2}
|
|
369
|
+
dots = {g: max((out_claims[c]["dots"][g] for c in principal), key=rank.get) for g in GROUPS}
|
|
370
|
+
listed = [k for k in order if k in paper_agg and paper_agg[k]["cost"]]
|
|
371
|
+
paper = {"score": value, "band": band(value), "dots": dots, "problems": [pid[k] for k in listed],
|
|
372
|
+
"principal": principal, "lowest": lowest, "lowest_score": out_claims[lowest]["score"],
|
|
373
|
+
"lowest_problems": out_claims[lowest]["problems"]}
|
|
374
|
+
return {"rubric": VERSION, "claims": out_claims,
|
|
375
|
+
"assumptions": [c["id"] for c in claims if is_assumption(c)], "paper": paper, "problems": problems}
|