calkit-python 0.47.5__py3-none-any.whl → 0.47.6__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.
- calkit/agent_skills/build-paper-pipeline/SKILL.md +5 -2
- calkit/agent_skills/check-questions/SKILL.md +44 -4
- calkit/cli/list.py +6 -4
- calkit/models/core.py +9 -1
- calkit/questions.py +230 -18
- calkit/tests/cli/test_check.py +34 -17
- calkit/tests/test_questions.py +155 -1
- {calkit_python-0.47.5.dist-info → calkit_python-0.47.6.dist-info}/METADATA +1 -1
- {calkit_python-0.47.5.dist-info → calkit_python-0.47.6.dist-info}/RECORD +25 -25
- {calkit_python-0.47.5.data → calkit_python-0.47.6.data}/data/etc/jupyter/jupyter_server_config.d/calkit.json +0 -0
- {calkit_python-0.47.5.data → calkit_python-0.47.6.data}/data/share/jupyter/labextensions/calkit/package.json +0 -0
- {calkit_python-0.47.5.data → calkit_python-0.47.6.data}/data/share/jupyter/labextensions/calkit/schemas/calkit/package.json.orig +0 -0
- {calkit_python-0.47.5.data → calkit_python-0.47.6.data}/data/share/jupyter/labextensions/calkit/schemas/calkit/plugin.json +0 -0
- {calkit_python-0.47.5.data → calkit_python-0.47.6.data}/data/share/jupyter/labextensions/calkit/static/506.c34b070098184e33.js +0 -0
- {calkit_python-0.47.5.data → calkit_python-0.47.6.data}/data/share/jupyter/labextensions/calkit/static/57d13a7399cec3c5.png +0 -0
- {calkit_python-0.47.5.data → calkit_python-0.47.6.data}/data/share/jupyter/labextensions/calkit/static/616.528427eb54a4a0c0.js +0 -0
- {calkit_python-0.47.5.data → calkit_python-0.47.6.data}/data/share/jupyter/labextensions/calkit/static/740.6bf87276e9e5788a.js +0 -0
- {calkit_python-0.47.5.data → calkit_python-0.47.6.data}/data/share/jupyter/labextensions/calkit/static/899.f9b9f9bf705a6493.js +0 -0
- {calkit_python-0.47.5.data → calkit_python-0.47.6.data}/data/share/jupyter/labextensions/calkit/static/935.46ecc6bf99aa593a.js +0 -0
- {calkit_python-0.47.5.data → calkit_python-0.47.6.data}/data/share/jupyter/labextensions/calkit/static/remoteEntry.109a3a8379365e59.js +0 -0
- {calkit_python-0.47.5.data → calkit_python-0.47.6.data}/data/share/jupyter/labextensions/calkit/static/style.js +0 -0
- {calkit_python-0.47.5.data → calkit_python-0.47.6.data}/data/share/jupyter/labextensions/calkit/static/third-party-licenses.json +0 -0
- {calkit_python-0.47.5.dist-info → calkit_python-0.47.6.dist-info}/WHEEL +0 -0
- {calkit_python-0.47.5.dist-info → calkit_python-0.47.6.dist-info}/entry_points.txt +0 -0
- {calkit_python-0.47.5.dist-info → calkit_python-0.47.6.dist-info}/licenses/LICENSE +0 -0
|
@@ -84,8 +84,11 @@ building a pipeline from scratch.
|
|
|
84
84
|
`calkit.yaml`: the `question` as the paper poses it, the `answer` as the
|
|
85
85
|
paper states it with `{name}` placeholders where numbers go, and
|
|
86
86
|
`evidence` naming the results file and key, the figure, and the
|
|
87
|
-
publication section that carries the argument.
|
|
88
|
-
|
|
87
|
+
publication section that carries the argument. The results file must be
|
|
88
|
+
one a stage from step 3 writes, never one written by hand. Where the
|
|
89
|
+
claim hinges on a threshold, write a conditional answer; the
|
|
90
|
+
`check-questions` skill covers both. Run `calkit check questions` and
|
|
91
|
+
fix what it reports.
|
|
89
92
|
|
|
90
93
|
6. **Run the pipeline, then check your own work.** Run `calkit run`, then
|
|
91
94
|
`calkit check repro`, which reads the manuscript back and reports any
|
|
@@ -21,12 +21,17 @@ Deterministic, done by `calkit check questions` — never re-derive by hand:
|
|
|
21
21
|
- every evidence path exists;
|
|
22
22
|
- every `value` key resolves in its file, and every `{name}` placeholder in
|
|
23
23
|
the prose resolves and formats;
|
|
24
|
+
- every clause of a conditional answer parses, names only evidence, and
|
|
25
|
+
renders, including the clauses the current values don't select;
|
|
24
26
|
- every publication `label` still exists in the LaTeX source;
|
|
25
27
|
- no evidence has changed (Git history for Git-tracked outputs, `dvc.lock`
|
|
26
28
|
for DVC-tracked ones) since the commit that last edited the question;
|
|
27
|
-
- each
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
- each `value` entry reads a file a pipeline stage produces, or one
|
|
30
|
+
declared with `imported_from`; anything else is an error, since a number
|
|
31
|
+
nothing computes is a magic number;
|
|
32
|
+
- each other evidence path is produced by a pipeline stage, or declared
|
|
33
|
+
with `imported_from` or `created_by`. This one is advisory: it is
|
|
34
|
+
reported as `unattributed` and does not fail the check.
|
|
30
35
|
|
|
31
36
|
Judgment, done here — the check reads paths and hashes, and cannot read a
|
|
32
37
|
sentence:
|
|
@@ -35,6 +40,8 @@ sentence:
|
|
|
35
40
|
this skill exists;
|
|
36
41
|
- does the answer still follow from the evidence, given what changed;
|
|
37
42
|
- are numbers retyped into the prose that should be `{name}` placeholders;
|
|
43
|
+
- does a claim resting on a threshold use a conditional answer, with a
|
|
44
|
+
threshold chosen before the value was known;
|
|
38
45
|
- is the answer concise, and does it point at the publication section that
|
|
39
46
|
carries the argument rather than repeating it.
|
|
40
47
|
|
|
@@ -104,7 +111,8 @@ evidence without showing them both.
|
|
|
104
111
|
from. If a stage should produce it, that is a pipeline gap worth
|
|
105
112
|
reporting. If it was imported or made by hand, declare it under
|
|
106
113
|
`figures`, `datasets`, or `publications` with `imported_from` or
|
|
107
|
-
`created_by` so the project says so.
|
|
114
|
+
`created_by` so the project says so. A `value` entry with no stage is
|
|
115
|
+
reported as an error rather than `unattributed`: give it a stage.
|
|
108
116
|
4. For each **error**, fix the reference: a missing path means the pipeline
|
|
109
117
|
has not been run or pulled; a bad key or placeholder means a results
|
|
110
118
|
file was restructured; a missing label means the publication was
|
|
@@ -128,6 +136,38 @@ project, not a tidy-up.
|
|
|
128
136
|
what it means; leave the reasoning to the publication.
|
|
129
137
|
- Numbers come from `value` evidence via placeholders, formatted to the
|
|
130
138
|
precision the claim needs: `{ratio:.1f}x`, `{error:.0%}`.
|
|
139
|
+
- A `value` entry must read a file a pipeline stage writes. A placeholder
|
|
140
|
+
over a results file written by hand, including one you write, is a
|
|
141
|
+
retyped number with extra steps, and the check fails it. If no stage
|
|
142
|
+
produces the number, add one (`/calkit:add-pipeline-stage`) instead of
|
|
143
|
+
writing the file. Never declare a file `imported_from` or `created_by`
|
|
144
|
+
to clear that error unless it really came from there, and never edit a
|
|
145
|
+
results file to change what an answer says.
|
|
146
|
+
- When the claim itself depends on a value, not just the number in it,
|
|
147
|
+
e.g., significant or not, which method wins, write a conditional answer
|
|
148
|
+
so the wording follows the evidence on a rerun:
|
|
149
|
+
|
|
150
|
+
```yaml
|
|
151
|
+
answer:
|
|
152
|
+
if p < 0.05: "The closure cuts error by {improvement:.1f}x."
|
|
153
|
+
else: "The closure does not measurably reduce error."
|
|
154
|
+
evidence:
|
|
155
|
+
- kind: value
|
|
156
|
+
path: results/closure.json
|
|
157
|
+
key: p-value
|
|
158
|
+
name: p
|
|
159
|
+
- kind: value
|
|
160
|
+
path: results/closure.json
|
|
161
|
+
key: improvement
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Conditions name `value` evidence, so those names must be valid Python
|
|
165
|
+
identifiers; give the entry a `name` otherwise. Every branch must be a
|
|
166
|
+
claim the evidence would support if it held. The threshold is part of
|
|
167
|
+
the claim: take it from the field's convention or the user, never pick
|
|
168
|
+
it to make the current branch hold, and don't use branches to hedge a
|
|
169
|
+
claim that should simply be weakened.
|
|
170
|
+
|
|
131
171
|
- Point at the publication with a `publication` evidence entry carrying
|
|
132
172
|
`section` (for the reader) and `label` (for the check), instead of an
|
|
133
173
|
`explanation` that restates the argument.
|
calkit/cli/list.py
CHANGED
|
@@ -243,7 +243,7 @@ def list_questions(
|
|
|
243
243
|
render_question,
|
|
244
244
|
)
|
|
245
245
|
|
|
246
|
-
def _texts(question: dict) -> list[str]:
|
|
246
|
+
def _texts(question: dict) -> list[str | dict]:
|
|
247
247
|
evidence = question.get("evidence") or []
|
|
248
248
|
return [question.get(f) or "" for f in TEMPLATED_FIELDS] + [
|
|
249
249
|
ev.get("explanation") or ""
|
|
@@ -260,16 +260,18 @@ def list_questions(
|
|
|
260
260
|
# let a fresh clone read as a project that types its braces. Any
|
|
261
261
|
# placeholder left standing counts, whether it names evidence that
|
|
262
262
|
# could not be read or names nothing at all: a brace meant to stay
|
|
263
|
-
# in the text is written '{{' and never reaches here.
|
|
263
|
+
# in the text is written '{{' and never reaches here. A conditional
|
|
264
|
+
# answer still in clauses is one whose conditions could not be read.
|
|
264
265
|
unfilled = any(
|
|
265
|
-
placeholders(text)
|
|
266
|
+
isinstance(text, dict) or placeholders(text)
|
|
266
267
|
for q in rendered
|
|
267
268
|
if isinstance(q, dict)
|
|
268
269
|
for text in _texts(q)
|
|
269
270
|
)
|
|
270
271
|
if unfilled:
|
|
271
272
|
warn(
|
|
272
|
-
"Some placeholders could not be filled from
|
|
273
|
+
"Some placeholders or conditions could not be filled from "
|
|
274
|
+
"the evidence. "
|
|
273
275
|
"Run 'calkit check questions' to see why; 'calkit pull' if "
|
|
274
276
|
"the results files are not here yet.",
|
|
275
277
|
err=json_output,
|
calkit/models/core.py
CHANGED
|
@@ -1841,7 +1841,15 @@ class Question(BaseModel):
|
|
|
1841
1841
|
|
|
1842
1842
|
question: str
|
|
1843
1843
|
hypothesis: str | None = None
|
|
1844
|
-
answer: str | None =
|
|
1844
|
+
answer: str | dict[str, str] | None = Field(
|
|
1845
|
+
default=None,
|
|
1846
|
+
description=(
|
|
1847
|
+
"The claim the evidence supports. A mapping keyed by 'if "
|
|
1848
|
+
"<condition>', 'elif <condition>' and 'else' picks its wording "
|
|
1849
|
+
"from the evidence, so an answer resting on a threshold states "
|
|
1850
|
+
"the other outcome instead of going stale when a value moves."
|
|
1851
|
+
),
|
|
1852
|
+
)
|
|
1845
1853
|
notes: str | None = Field(
|
|
1846
1854
|
default=None,
|
|
1847
1855
|
description=(
|
calkit/questions.py
CHANGED
|
@@ -32,13 +32,15 @@ updates itself.
|
|
|
32
32
|
|
|
33
33
|
from __future__ import annotations
|
|
34
34
|
|
|
35
|
+
import ast
|
|
35
36
|
import glob
|
|
36
37
|
import io
|
|
37
38
|
import json
|
|
39
|
+
import operator
|
|
38
40
|
import os
|
|
39
41
|
import re
|
|
40
42
|
import string
|
|
41
|
-
from typing import Any, Literal
|
|
43
|
+
from typing import Any, Literal, TypeGuard
|
|
42
44
|
|
|
43
45
|
from pydantic import BaseModel, Field
|
|
44
46
|
|
|
@@ -245,14 +247,208 @@ def placeholders(text: str) -> list[str]:
|
|
|
245
247
|
return [m.group(1) for m in _PLACEHOLDER.finditer(text or "")]
|
|
246
248
|
|
|
247
249
|
|
|
248
|
-
def
|
|
250
|
+
def _placeholder_problems(text: str, values: dict[str, Any]) -> list[str]:
|
|
251
|
+
if "{" not in text:
|
|
252
|
+
return []
|
|
253
|
+
try:
|
|
254
|
+
_FORMATTER.vformat(text, (), values)
|
|
255
|
+
except KeyError as e:
|
|
256
|
+
return [
|
|
257
|
+
f"placeholder {{{e.args[0]}}} names no evidence; write "
|
|
258
|
+
"'{{' and '}}' for braces meant to stay in the text"
|
|
259
|
+
]
|
|
260
|
+
except (ValueError, IndexError) as e:
|
|
261
|
+
return [f"cannot render {text[:40]!r}...: {e}"]
|
|
262
|
+
return []
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
_IF_KEY = re.compile(r"^\s*(if|elif)\s+(.+?)\s*$")
|
|
266
|
+
_ELSE_KEY = re.compile(r"^\s*else\s*$")
|
|
267
|
+
_COMPARISONS = {
|
|
268
|
+
ast.Lt: operator.lt,
|
|
269
|
+
ast.LtE: operator.le,
|
|
270
|
+
ast.Gt: operator.gt,
|
|
271
|
+
ast.GtE: operator.ge,
|
|
272
|
+
ast.Eq: operator.eq,
|
|
273
|
+
ast.NotEq: operator.ne,
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def is_conditional(value: Any) -> TypeGuard[dict]:
|
|
278
|
+
"""Whether a value picks its wording with ``if``/``elif``/``else``."""
|
|
279
|
+
return isinstance(value, dict)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def parse_conditional(clauses: dict) -> list[tuple[str | None, str]]:
|
|
283
|
+
"""Read a conditional's keys into ordered ``(condition, wording)``.
|
|
284
|
+
|
|
285
|
+
The condition is ``None`` for ``else``. Keys are read in the order
|
|
286
|
+
they appear in the file, so the clauses are tried in the order they
|
|
287
|
+
were written.
|
|
288
|
+
"""
|
|
289
|
+
parsed: list[tuple[str | None, str]] = []
|
|
290
|
+
for position, (key, wording) in enumerate(clauses.items()):
|
|
291
|
+
opened = _IF_KEY.match(str(key))
|
|
292
|
+
if opened:
|
|
293
|
+
keyword, condition = opened.groups()
|
|
294
|
+
if keyword == "if" and position:
|
|
295
|
+
raise ValueError("only the first clause may be 'if'")
|
|
296
|
+
if keyword == "elif" and not parsed:
|
|
297
|
+
raise ValueError("'elif' with no 'if' before it")
|
|
298
|
+
parsed.append((condition, str(wording)))
|
|
299
|
+
continue
|
|
300
|
+
if _ELSE_KEY.match(str(key)):
|
|
301
|
+
if not parsed:
|
|
302
|
+
raise ValueError("'else' with no 'if' before it")
|
|
303
|
+
parsed.append((None, str(wording)))
|
|
304
|
+
continue
|
|
305
|
+
raise ValueError(f"expected 'if', 'elif' or 'else', got {key!r}")
|
|
306
|
+
if not parsed:
|
|
307
|
+
raise ValueError("a conditional needs at least an 'if' clause")
|
|
308
|
+
for condition, _ in parsed[:-1]:
|
|
309
|
+
if condition is None:
|
|
310
|
+
raise ValueError("'else' must be the last clause")
|
|
311
|
+
return parsed
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _operand(node: ast.AST, values: dict[str, Any]) -> Any:
|
|
315
|
+
"""One side of a comparison, resolved against the evidence values.
|
|
316
|
+
|
|
317
|
+
Names and literals are read directly; anything else is arithmetic and
|
|
318
|
+
goes to the same evaluator the calculations use, so there is one
|
|
319
|
+
audited path for arithmetic rather than two.
|
|
320
|
+
"""
|
|
321
|
+
if isinstance(node, ast.Constant):
|
|
322
|
+
return node.value
|
|
323
|
+
if isinstance(node, ast.Name):
|
|
324
|
+
if node.id not in values:
|
|
325
|
+
raise KeyError(node.id)
|
|
326
|
+
return values[node.id]
|
|
327
|
+
for inner in ast.walk(node):
|
|
328
|
+
if isinstance(inner, ast.Name) and inner.id not in values:
|
|
329
|
+
raise KeyError(inner.id)
|
|
330
|
+
import arithmetic_eval # type: ignore[import-untyped]
|
|
331
|
+
|
|
332
|
+
return arithmetic_eval.evaluate(ast.unparse(node), values)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _truth(node: ast.AST, values: dict[str, Any]) -> bool:
|
|
336
|
+
if isinstance(node, ast.BoolOp):
|
|
337
|
+
# Not short-circuited, so a misspelled name is an error whatever
|
|
338
|
+
# the current values are
|
|
339
|
+
outcomes = [_truth(v, values) for v in node.values]
|
|
340
|
+
if isinstance(node.op, ast.And):
|
|
341
|
+
return all(outcomes)
|
|
342
|
+
return any(outcomes)
|
|
343
|
+
if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not):
|
|
344
|
+
return not _truth(node.operand, values)
|
|
345
|
+
if isinstance(node, ast.Compare):
|
|
346
|
+
left = _operand(node.left, values)
|
|
347
|
+
for op, comparator in zip(node.ops, node.comparators):
|
|
348
|
+
right = _operand(comparator, values)
|
|
349
|
+
compare = _COMPARISONS.get(type(op))
|
|
350
|
+
if compare is None:
|
|
351
|
+
raise ValueError(
|
|
352
|
+
f"{type(op).__name__} is not a supported comparison"
|
|
353
|
+
)
|
|
354
|
+
if not compare(left, right):
|
|
355
|
+
return False
|
|
356
|
+
left = right
|
|
357
|
+
return True
|
|
358
|
+
raise ValueError("a condition must compare values, e.g., 'p < 0.05'")
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def evaluate_condition(expression: str, values: dict[str, Any]) -> bool:
|
|
362
|
+
"""Evaluate one ``if``/``elif`` condition against the evidence values.
|
|
363
|
+
|
|
364
|
+
Only comparisons, ``and``/``or``/``not`` and arithmetic are allowed, so
|
|
365
|
+
a condition read from ``calkit.yaml`` cannot call anything.
|
|
366
|
+
"""
|
|
367
|
+
try:
|
|
368
|
+
tree = ast.parse(expression, mode="eval")
|
|
369
|
+
except SyntaxError as e:
|
|
370
|
+
raise ValueError(f"cannot parse condition {expression!r}: {e}") from e
|
|
371
|
+
try:
|
|
372
|
+
return bool(_truth(tree.body, values))
|
|
373
|
+
except KeyError:
|
|
374
|
+
# A name like 'paired-gain.vawt-8' reads as arithmetic and
|
|
375
|
+
# attribute access, so the failure would otherwise name a
|
|
376
|
+
# fragment of it and look like missing evidence.
|
|
377
|
+
unusable = [
|
|
378
|
+
name
|
|
379
|
+
for name in values
|
|
380
|
+
if not name.isidentifier() and name in expression
|
|
381
|
+
]
|
|
382
|
+
if unusable:
|
|
383
|
+
raise ValueError(
|
|
384
|
+
f"condition {expression!r} refers to {unusable[0]!r}, which "
|
|
385
|
+
"cannot be read as a variable; give that evidence a 'name' "
|
|
386
|
+
"that is a valid Python identifier"
|
|
387
|
+
) from None
|
|
388
|
+
raise
|
|
389
|
+
except ValueError:
|
|
390
|
+
raise
|
|
391
|
+
except Exception as e:
|
|
392
|
+
# E.g., comparing a string to a number, or syntax arithmetic_eval
|
|
393
|
+
# refuses, so callers only have to handle one kind of bad condition
|
|
394
|
+
raise ValueError(
|
|
395
|
+
f"cannot evaluate condition {expression!r}: {e}"
|
|
396
|
+
) from e
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def check_conditional(clauses: dict, values: dict[str, Any]) -> list[str]:
|
|
400
|
+
"""Problems with every clause of a conditional, not just the one chosen.
|
|
401
|
+
|
|
402
|
+
The clauses the current values don't select are the ones a rerun will
|
|
403
|
+
reach, so a typo in them is an error now rather than after the rerun.
|
|
404
|
+
"""
|
|
405
|
+
try:
|
|
406
|
+
parsed = parse_conditional(clauses)
|
|
407
|
+
except ValueError as e:
|
|
408
|
+
return [f"conditional answer: {e}"]
|
|
409
|
+
messages: list[str] = []
|
|
410
|
+
held = False
|
|
411
|
+
for condition, wording in parsed:
|
|
412
|
+
if condition is None:
|
|
413
|
+
held = True
|
|
414
|
+
else:
|
|
415
|
+
try:
|
|
416
|
+
held = evaluate_condition(condition, values) or held
|
|
417
|
+
except KeyError as e:
|
|
418
|
+
messages.append(
|
|
419
|
+
f"condition {condition!r} names no evidence {e.args[0]!r}"
|
|
420
|
+
)
|
|
421
|
+
except ValueError as e:
|
|
422
|
+
messages.append(str(e))
|
|
423
|
+
messages += _placeholder_problems(wording, values)
|
|
424
|
+
if not messages and not held:
|
|
425
|
+
messages.append(
|
|
426
|
+
"no condition of the conditional answer holds and there is no "
|
|
427
|
+
"'else' clause"
|
|
428
|
+
)
|
|
429
|
+
return messages
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def select_branch(clauses: dict, values: dict[str, Any]) -> str:
|
|
433
|
+
"""The wording whose condition holds, for a conditional answer."""
|
|
434
|
+
for condition, wording in parse_conditional(clauses):
|
|
435
|
+
if condition is None or evaluate_condition(condition, values):
|
|
436
|
+
return wording
|
|
437
|
+
raise ValueError("no condition held and there is no 'else' clause")
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def render(text: str | dict | None, values: dict[str, Any]) -> str | None:
|
|
249
441
|
"""Fill a question text's placeholders from its evidence values.
|
|
250
442
|
|
|
251
443
|
Raises ``KeyError`` for a name with no evidence and ``ValueError`` for
|
|
252
444
|
a format spec the value cannot satisfy, so a template that cannot be
|
|
253
445
|
rendered is an error rather than a silently unfilled sentence.
|
|
254
446
|
"""
|
|
255
|
-
if text is None
|
|
447
|
+
if text is None:
|
|
448
|
+
return text
|
|
449
|
+
if isinstance(text, dict):
|
|
450
|
+
text = select_branch(text, values)
|
|
451
|
+
if "{" not in text:
|
|
256
452
|
return text
|
|
257
453
|
return _FORMATTER.vformat(text, (), values)
|
|
258
454
|
|
|
@@ -596,7 +792,11 @@ def _check_publication_label(
|
|
|
596
792
|
|
|
597
793
|
|
|
598
794
|
def _is_attributed(
|
|
599
|
-
path: str,
|
|
795
|
+
path: str,
|
|
796
|
+
stage: str | None,
|
|
797
|
+
ck_info: dict,
|
|
798
|
+
wdir: str,
|
|
799
|
+
computed: bool = False,
|
|
600
800
|
) -> bool:
|
|
601
801
|
"""Whether the project says where an evidence path came from.
|
|
602
802
|
|
|
@@ -605,7 +805,9 @@ def _is_attributed(
|
|
|
605
805
|
may be declared as an artifact that records an import or a person,
|
|
606
806
|
which is what :func:`calkit.provenance.has_provenance` reads---an
|
|
607
807
|
imported dataset or a hand-drawn schematic is accounted for even
|
|
608
|
-
though there is nothing upstream to point at.
|
|
808
|
+
though there is nothing upstream to point at. With ``computed``, a
|
|
809
|
+
person doesn't count, since a number someone typed into a file is
|
|
810
|
+
still a magic number.
|
|
609
811
|
"""
|
|
610
812
|
from calkit.provenance import has_provenance
|
|
611
813
|
|
|
@@ -624,7 +826,12 @@ def _is_attributed(
|
|
|
624
826
|
if (
|
|
625
827
|
isinstance(artifact, dict)
|
|
626
828
|
and artifact.get("path") == path
|
|
627
|
-
and
|
|
829
|
+
and (
|
|
830
|
+
artifact.get("stage") is not None
|
|
831
|
+
or artifact.get("imported_from") is not None
|
|
832
|
+
if computed
|
|
833
|
+
else has_provenance(artifact)
|
|
834
|
+
)
|
|
628
835
|
):
|
|
629
836
|
return True
|
|
630
837
|
return False
|
|
@@ -808,7 +1015,19 @@ def check_evidence(
|
|
|
808
1015
|
],
|
|
809
1016
|
)
|
|
810
1017
|
)
|
|
811
|
-
if
|
|
1018
|
+
if (
|
|
1019
|
+
out.status in ("ok", "changed")
|
|
1020
|
+
and is_value_evidence(ev)
|
|
1021
|
+
and not _is_attributed(path, out.stage, ck_info, wdir, computed=True)
|
|
1022
|
+
):
|
|
1023
|
+
out.status = "error"
|
|
1024
|
+
out.message = (
|
|
1025
|
+
"no pipeline stage computes this value, so nothing can show "
|
|
1026
|
+
"where the number came from or keep it current; produce the file "
|
|
1027
|
+
"with a stage, or declare it with 'imported_from' if another "
|
|
1028
|
+
"project computed it"
|
|
1029
|
+
)
|
|
1030
|
+
elif out.status == "ok" and not _is_attributed(
|
|
812
1031
|
path, out.stage, ck_info, wdir
|
|
813
1032
|
):
|
|
814
1033
|
out.status = "unattributed"
|
|
@@ -874,17 +1093,10 @@ def check_question(
|
|
|
874
1093
|
ev.get("explanation") for ev in evidence
|
|
875
1094
|
]
|
|
876
1095
|
for t in texts:
|
|
877
|
-
if
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
except KeyError as e:
|
|
882
|
-
messages.append(
|
|
883
|
-
f"placeholder {{{e.args[0]}}} names no evidence; write "
|
|
884
|
-
"'{{' and '}}' for braces meant to stay in the text"
|
|
885
|
-
)
|
|
886
|
-
except (ValueError, IndexError) as e:
|
|
887
|
-
messages.append(f"cannot render {t[:40]!r}...: {e}")
|
|
1096
|
+
if is_conditional(t):
|
|
1097
|
+
messages += check_conditional(t, values)
|
|
1098
|
+
elif t:
|
|
1099
|
+
messages += _placeholder_problems(t, values)
|
|
888
1100
|
# Worst first, matching what the hub shows against each question: an
|
|
889
1101
|
# answer resting on nothing anyone can find is worse off than one
|
|
890
1102
|
# resting on something merely out of date.
|
calkit/tests/cli/test_check.py
CHANGED
|
@@ -1182,11 +1182,24 @@ def test_check_questions(tmp_dir):
|
|
|
1182
1182
|
calkit.ryaml.dump(ck_info, f)
|
|
1183
1183
|
subprocess.check_call(["git", "add", "-A"])
|
|
1184
1184
|
subprocess.check_call(["git", "commit", "-q", "-m", "Answer"])
|
|
1185
|
+
# No stage computes the value, so it is a magic number and fails
|
|
1186
|
+
proc = subprocess.run(
|
|
1187
|
+
["calkit", "check", "questions"], capture_output=True, text=True
|
|
1188
|
+
)
|
|
1189
|
+
assert proc.returncode != 0
|
|
1190
|
+
assert "no pipeline stage computes this value" in proc.stdout
|
|
1191
|
+
# Another project computed it, which is traceable
|
|
1192
|
+
ck_info["datasets"] = [
|
|
1193
|
+
{
|
|
1194
|
+
"path": "results/findings.json",
|
|
1195
|
+
"imported_from": {"project": "someone/else"},
|
|
1196
|
+
}
|
|
1197
|
+
]
|
|
1198
|
+
with open("calkit.yaml", "w") as f:
|
|
1199
|
+
calkit.ryaml.dump(ck_info, f)
|
|
1200
|
+
subprocess.check_call(["git", "commit", "-q", "-am", "Import it"])
|
|
1185
1201
|
out = subprocess.check_output(["calkit", "check", "questions"], text=True)
|
|
1186
1202
|
assert "Answers backed by current evidence: 1/1" in out
|
|
1187
|
-
# Nothing in the project says where the results file came from, which
|
|
1188
|
-
# is advice rather than a failure: the answer may be perfectly good
|
|
1189
|
-
assert "Evidence with nothing recorded behind it: 1" in out
|
|
1190
1203
|
# Listing renders the placeholder from the results file
|
|
1191
1204
|
out = subprocess.check_output(["calkit", "list", "questions"], text=True)
|
|
1192
1205
|
assert "answer: 8 of eight do." in out
|
|
@@ -1252,19 +1265,6 @@ def test_check_questions(tmp_dir):
|
|
|
1252
1265
|
subprocess.check_call(["git", "commit", "-q", "-am", "Review"])
|
|
1253
1266
|
out = subprocess.check_output(["calkit", "check", "questions"], text=True)
|
|
1254
1267
|
assert "changed after the answer was written" not in out
|
|
1255
|
-
# Declaring where the results file came from clears the advice
|
|
1256
|
-
ck_info = calkit.load_calkit_info()
|
|
1257
|
-
ck_info["datasets"] = [
|
|
1258
|
-
{
|
|
1259
|
-
"path": "results/findings.json",
|
|
1260
|
-
"created_by": {"name": "A person"},
|
|
1261
|
-
}
|
|
1262
|
-
]
|
|
1263
|
-
with open("calkit.yaml", "w") as f:
|
|
1264
|
-
calkit.ryaml.dump(ck_info, f)
|
|
1265
|
-
subprocess.check_call(["git", "commit", "-q", "-am", "Declare"])
|
|
1266
|
-
out = subprocess.check_output(["calkit", "check", "questions"], text=True)
|
|
1267
|
-
assert "nothing recorded behind it" not in out
|
|
1268
1268
|
# A console that cannot encode a check mark gets a '?' rather than a
|
|
1269
1269
|
# UnicodeEncodeError, which on Windows would kill the command with no
|
|
1270
1270
|
# output at all
|
|
@@ -1289,8 +1289,25 @@ def test_check_questions(tmp_dir):
|
|
|
1289
1289
|
with open("calkit.yaml", "w") as f:
|
|
1290
1290
|
calkit.ryaml.dump(ck_info, f)
|
|
1291
1291
|
out = subprocess.check_output(["calkit", "list", "questions"], text=True)
|
|
1292
|
-
assert "
|
|
1292
|
+
assert "could not be filled" in out
|
|
1293
1293
|
assert "It is {missing} of them." in out
|
|
1294
|
+
# A conditional answer is listed as the branch that holds, and one with
|
|
1295
|
+
# no branch holding is listed as written, with the same warning
|
|
1296
|
+
ck_info["questions"][0]["answer"] = {
|
|
1297
|
+
"if n_top > 4": "Most do.",
|
|
1298
|
+
"else": "{n_top} do.",
|
|
1299
|
+
}
|
|
1300
|
+
with open("calkit.yaml", "w") as f:
|
|
1301
|
+
calkit.ryaml.dump(ck_info, f)
|
|
1302
|
+
out = subprocess.check_output(["calkit", "list", "questions"], text=True)
|
|
1303
|
+
assert "answer: 0 do." in out
|
|
1304
|
+
assert "could not be filled" not in out
|
|
1305
|
+
del ck_info["questions"][0]["answer"]["else"]
|
|
1306
|
+
with open("calkit.yaml", "w") as f:
|
|
1307
|
+
calkit.ryaml.dump(ck_info, f)
|
|
1308
|
+
out = subprocess.check_output(["calkit", "list", "questions"], text=True)
|
|
1309
|
+
assert "could not be filled" in out
|
|
1310
|
+
assert "if n_top > 4: Most do." in out
|
|
1294
1311
|
# A project with no questions says so rather than printing nothing
|
|
1295
1312
|
os.remove("calkit.yaml")
|
|
1296
1313
|
with open("calkit.yaml", "w") as f:
|
calkit/tests/test_questions.py
CHANGED
|
@@ -14,11 +14,14 @@ from calkit.questions import (
|
|
|
14
14
|
QuestionsStatus,
|
|
15
15
|
check_question,
|
|
16
16
|
check_questions,
|
|
17
|
+
evaluate_condition,
|
|
17
18
|
format_status,
|
|
19
|
+
parse_conditional,
|
|
18
20
|
placeholders,
|
|
19
21
|
render,
|
|
20
22
|
render_question,
|
|
21
23
|
resolve_key,
|
|
24
|
+
select_branch,
|
|
22
25
|
)
|
|
23
26
|
|
|
24
27
|
|
|
@@ -92,6 +95,16 @@ def test_history_is_read_once_for_all_questions(tmp_dir):
|
|
|
92
95
|
with open("results/findings.json", "w") as f:
|
|
93
96
|
json.dump({f"k{i}": i for i in range(4)}, f)
|
|
94
97
|
ck_info = {
|
|
98
|
+
"pipeline": {
|
|
99
|
+
"stages": {
|
|
100
|
+
"summarize": {
|
|
101
|
+
"kind": "python-script",
|
|
102
|
+
"environment": "py",
|
|
103
|
+
"script_path": "s.py",
|
|
104
|
+
"outputs": ["results/findings.json"],
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
},
|
|
95
108
|
"questions": [
|
|
96
109
|
{
|
|
97
110
|
"question": f"Q{i}?",
|
|
@@ -106,7 +119,7 @@ def test_history_is_read_once_for_all_questions(tmp_dir):
|
|
|
106
119
|
],
|
|
107
120
|
}
|
|
108
121
|
for i in range(4)
|
|
109
|
-
]
|
|
122
|
+
],
|
|
110
123
|
}
|
|
111
124
|
with open("calkit.yaml", "w") as f:
|
|
112
125
|
calkit.ryaml.dump(ck_info, f)
|
|
@@ -235,6 +248,32 @@ def test_check_questions(tmp_dir):
|
|
|
235
248
|
# makes it and it is not declared with an import or a person
|
|
236
249
|
assert q4.evidence[4].status == "unattributed"
|
|
237
250
|
assert [ev.path for ev in status.unattributed] == ["figures/plot.png"]
|
|
251
|
+
# A value no stage computes is a magic number, so it fails rather than
|
|
252
|
+
# being advice; an import is traceable, a person typing it in is not
|
|
253
|
+
with open("results/typed.json", "w") as f:
|
|
254
|
+
json.dump({"n": 3}, f)
|
|
255
|
+
typed = {
|
|
256
|
+
"question": "Typed?",
|
|
257
|
+
"answer": "{n}",
|
|
258
|
+
"evidence": [
|
|
259
|
+
{"kind": "value", "path": "results/typed.json", "key": "n"}
|
|
260
|
+
],
|
|
261
|
+
}
|
|
262
|
+
for declared, expected in [
|
|
263
|
+
(None, "error"),
|
|
264
|
+
({"created_by": "someone"}, "error"),
|
|
265
|
+
({"imported_from": {"project": "a/b"}}, "ok"),
|
|
266
|
+
]:
|
|
267
|
+
info = dict(ck_info)
|
|
268
|
+
if declared:
|
|
269
|
+
info["datasets"] = [{"path": "results/typed.json"} | declared]
|
|
270
|
+
checked = check_question(5, typed, info, ".")
|
|
271
|
+
assert checked.status == expected, declared
|
|
272
|
+
if expected == "error":
|
|
273
|
+
assert "no pipeline stage computes" in (
|
|
274
|
+
checked.evidence[0].message or ""
|
|
275
|
+
)
|
|
276
|
+
os.remove("results/typed.json")
|
|
238
277
|
rendered = render_question(ck_info["questions"][3], ck_info, ".")
|
|
239
278
|
assert rendered["answer"] == "8 of eight do, a 5.1x gain."
|
|
240
279
|
assert rendered["evidence"][2]["explanation"] == "The best is a."
|
|
@@ -530,3 +569,118 @@ def test_check_questions_pipeline_and_pins(tmp_dir):
|
|
|
530
569
|
assert checked.status == "ok"
|
|
531
570
|
assert checked.evidence[0].current == 2
|
|
532
571
|
assert checked.evidence[0].git_ref == sha
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def test_conditional_answers(tmp_dir):
|
|
575
|
+
values = {"p": 0.007, "rho": 0.8373, "n": 17, "leader": "turns-max"}
|
|
576
|
+
# Comparisons, chaining, boolean operators, strings, and arithmetic
|
|
577
|
+
assert evaluate_condition("p < 0.05", values)
|
|
578
|
+
assert not evaluate_condition("p >= 0.05", values)
|
|
579
|
+
assert evaluate_condition("0.0 <= p < 0.05", values)
|
|
580
|
+
assert evaluate_condition("p < 0.05 and rho > 0.8", values)
|
|
581
|
+
assert evaluate_condition("p > 0.5 or rho > 0.8", values)
|
|
582
|
+
assert evaluate_condition("not p > 0.5", values)
|
|
583
|
+
assert evaluate_condition("leader == 'turns-max'", values)
|
|
584
|
+
assert evaluate_condition("n / 2 > 8", values)
|
|
585
|
+
# A name with no evidence is an error, not a false condition, and
|
|
586
|
+
# nothing may be called or used as a bare value
|
|
587
|
+
with pytest.raises(KeyError):
|
|
588
|
+
evaluate_condition("missing < 1", values)
|
|
589
|
+
with pytest.raises(Exception):
|
|
590
|
+
evaluate_condition("len(leader) > 1", values)
|
|
591
|
+
with pytest.raises(ValueError):
|
|
592
|
+
evaluate_condition("p", values)
|
|
593
|
+
# A comparison the values can't make is a ValueError like the rest
|
|
594
|
+
with pytest.raises(ValueError, match="cannot evaluate"):
|
|
595
|
+
evaluate_condition("leader < 0.5", values)
|
|
596
|
+
# A name that is not a valid identifier cannot be read as a variable,
|
|
597
|
+
# and says so rather than reporting a fragment of itself as missing
|
|
598
|
+
with pytest.raises(ValueError, match="valid Python identifier"):
|
|
599
|
+
evaluate_condition("paired-gain > 0.1", {"paired-gain": 0.25})
|
|
600
|
+
# Clauses are tried in the order written, with None marking the else
|
|
601
|
+
clauses = {
|
|
602
|
+
"if p < 0.05": "strong, rho {rho:.2f}",
|
|
603
|
+
"elif p < 0.1": "weak",
|
|
604
|
+
"else": "none",
|
|
605
|
+
}
|
|
606
|
+
assert [c for c, _ in parse_conditional(clauses)] == [
|
|
607
|
+
"p < 0.05",
|
|
608
|
+
"p < 0.1",
|
|
609
|
+
None,
|
|
610
|
+
]
|
|
611
|
+
assert select_branch(clauses, {"p": 0.007}) == "strong, rho {rho:.2f}"
|
|
612
|
+
assert select_branch(clauses, {"p": 0.08}) == "weak"
|
|
613
|
+
assert select_branch(clauses, {"p": 0.9}) == "none"
|
|
614
|
+
# Malformed clause sets are errors rather than silent misreadings
|
|
615
|
+
with pytest.raises(ValueError):
|
|
616
|
+
parse_conditional({"elif p < 1": "x"})
|
|
617
|
+
with pytest.raises(ValueError):
|
|
618
|
+
parse_conditional({"else": "x"})
|
|
619
|
+
with pytest.raises(ValueError):
|
|
620
|
+
parse_conditional({"when p < 1": "x"})
|
|
621
|
+
with pytest.raises(ValueError):
|
|
622
|
+
parse_conditional({"if p < 1": "x", "else": "y", "elif p < 2": "z"})
|
|
623
|
+
with pytest.raises(ValueError):
|
|
624
|
+
parse_conditional({"if p < 1": "x", "if p < 2": "y"})
|
|
625
|
+
# Nothing holding with no else is an error rather than a blank answer
|
|
626
|
+
with pytest.raises(ValueError):
|
|
627
|
+
select_branch({"if p < 0.05": "strong"}, {"p": 0.9})
|
|
628
|
+
# Rendering picks the branch, then fills its placeholders
|
|
629
|
+
answer = {
|
|
630
|
+
"if p < 0.05": "{leader} predicts it (rho {rho:+.2f})",
|
|
631
|
+
"else": "no feature predicts it",
|
|
632
|
+
}
|
|
633
|
+
assert render(answer, values) == "turns-max predicts it (rho +0.84)"
|
|
634
|
+
assert render(answer, values | {"p": 0.2}) == "no feature predicts it"
|
|
635
|
+
# Plain strings are untouched by the conditional path
|
|
636
|
+
assert render("if and only if", values) == "if and only if"
|
|
637
|
+
# Checking reports every clause's problems, including the clauses the
|
|
638
|
+
# current values don't select, rather than crashing or passing
|
|
639
|
+
with open("r.json", "w") as f:
|
|
640
|
+
json.dump({"p": 0.2, "leader": "x"}, f)
|
|
641
|
+
evidence = [
|
|
642
|
+
{"kind": "value", "path": "r.json", "key": "p"},
|
|
643
|
+
{"kind": "value", "path": "r.json", "key": "leader"},
|
|
644
|
+
]
|
|
645
|
+
cases = [
|
|
646
|
+
({"if p < 0.5": "{leader}", "else": "no"}, []),
|
|
647
|
+
(
|
|
648
|
+
{"if p < 0.05": "x"},
|
|
649
|
+
["no condition of the conditional answer holds"],
|
|
650
|
+
),
|
|
651
|
+
({"when p < 1": "x"}, ["conditional answer: expected 'if'"]),
|
|
652
|
+
(
|
|
653
|
+
{"if leader < 0.05": "x", "else": "y"},
|
|
654
|
+
["cannot evaluate condition 'leader < 0.05'"],
|
|
655
|
+
),
|
|
656
|
+
(
|
|
657
|
+
{"if p > 0.1": "yes", "elif typo < 1": "x", "else": "{rhoo:.2f}"},
|
|
658
|
+
[
|
|
659
|
+
"condition 'typo < 1' names no evidence 'typo'",
|
|
660
|
+
"placeholder {rhoo} names no evidence",
|
|
661
|
+
],
|
|
662
|
+
),
|
|
663
|
+
]
|
|
664
|
+
ck_info = {
|
|
665
|
+
"pipeline": {
|
|
666
|
+
"stages": {
|
|
667
|
+
"scan": {
|
|
668
|
+
"kind": "python-script",
|
|
669
|
+
"environment": "py",
|
|
670
|
+
"script_path": "s.py",
|
|
671
|
+
"outputs": ["r.json"],
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
for answer, expected in cases:
|
|
677
|
+
checked = check_question(
|
|
678
|
+
1,
|
|
679
|
+
{"question": "q", "answer": answer, "evidence": evidence},
|
|
680
|
+
ck_info,
|
|
681
|
+
".",
|
|
682
|
+
)
|
|
683
|
+
messages = checked.message or ""
|
|
684
|
+
assert (checked.status == "error") == bool(expected), answer
|
|
685
|
+
for fragment in expected:
|
|
686
|
+
assert fragment in messages, (answer, messages)
|
|
@@ -31,7 +31,7 @@ calkit/overleaf.py,sha256=ycCX6G28CTd8IprCAh4o9ihtgECi4dR1hwwgfUnBZmw,53210
|
|
|
31
31
|
calkit/pipeline.py,sha256=2y6NsgncyGXBFVcyCQxXjVajA28fAdGUOMlfFTjzBfw,105006
|
|
32
32
|
calkit/procedures.py,sha256=ZZtoWWmq_H0dyuUb6M-g_y77GRsJzxLMovT_X0RtYrM,2860
|
|
33
33
|
calkit/provenance.py,sha256=tnj_2yWkVk929GEKr76lacO2jCzGCzpz8_Ygr6Mds_Q,33863
|
|
34
|
-
calkit/questions.py,sha256=
|
|
34
|
+
calkit/questions.py,sha256=7VZ2KTe9vvpKRQAJADYbXxkhjl1y46C5aPJzYTX3CMQ,45142
|
|
35
35
|
calkit/releases.py,sha256=RDURVbJJ1M0UNmOvlEs1-eNof6H6fPplxeKT9BcfcLw,32277
|
|
36
36
|
calkit/reproducibility.py,sha256=sNsstw1tWyLPQoZMKldRD8NCOpvU988zSMhhQBXIvGE,34354
|
|
37
37
|
calkit/schema.py,sha256=grCxoUc7JEPhntow__ibS45hdSCqpdAHBwd2dmtiwEM,2704
|
|
@@ -47,7 +47,7 @@ calkit/cli/dev.py,sha256=cu4QsfDiA_KurGx4WJcgA2qgfghMoZutE8kl4IZbUcM,1070
|
|
|
47
47
|
calkit/cli/hub.py,sha256=ZBOlL7-mV57q_ynr4hPuhV7NlkKEkINSRksXD-jjiOg,2972
|
|
48
48
|
calkit/cli/import_.py,sha256=QOZEfKUmqMntle47PRZhN8cs7gfWCBte_smPZqksl54,25563
|
|
49
49
|
calkit/cli/latex.py,sha256=XtahLrNaYdqtqt_mMoP_wfHXIEpdecGlSW6jPR_yulA,51907
|
|
50
|
-
calkit/cli/list.py,sha256=
|
|
50
|
+
calkit/cli/list.py,sha256=DCWIX9LpTU6KpE5KKcT_DDjoRz5xPxQgzeixMsp36tM,20620
|
|
51
51
|
calkit/cli/new.py,sha256=VUgPE5iOpflnuJxUNcy-GzAR-IIs9KRDwwkQtblWfoQ,158690
|
|
52
52
|
calkit/cli/notebooks.py,sha256=aR4I0qatOtkrAxEG5PVLej20SBLKo8qr-lxDSzgljUQ,38836
|
|
53
53
|
calkit/cli/office.py,sha256=jVSTvbl91TCzlglST5O2b8hdApZA_QHw_UiEQFJqxdc,1754
|
|
@@ -64,7 +64,7 @@ calkit/dvc/zip.py,sha256=nf4EzOyc5nI_OA3M6S7wwZpL7N4EQz9yCf8FbnPjlxM,21857
|
|
|
64
64
|
calkit/jupyterlab/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
65
65
|
calkit/jupyterlab/routes.py,sha256=sARrFjWnHVV-LunrgqO4DCt_x7XLhVPAqA1eaUFf7HQ,51443
|
|
66
66
|
calkit/models/__init__.py,sha256=vGYF4MZBGzDk1BPBzpp1bRb59RCJ2Bj2kzHuVVRacug,120
|
|
67
|
-
calkit/models/core.py,sha256=
|
|
67
|
+
calkit/models/core.py,sha256=3Z0o6_vLOQ-CKix6iFFmz2FsAr6w1nSv93Xmi1INcjw,77660
|
|
68
68
|
calkit/models/docx.py,sha256=rgR1Y5s_yEomOeC6YlhHvsMDC9TmZp3rPN1wEt_93ds,2539
|
|
69
69
|
calkit/models/io.py,sha256=0nup-gDz3FbiD8AT5pTpxji6XN_rW5AsijErsIFdpbg,2330
|
|
70
70
|
calkit/models/iteration.py,sha256=_QajYhWVXV9ge9naX8ZkT1PBNzfc75cpl2BR2f8VPOo,2945
|
|
@@ -124,7 +124,7 @@ calkit/tests/test_overleaf.py,sha256=Itc4K3mYInHi36ZWhqH7drCHLCDbH1SXk3z_fB0-W8o
|
|
|
124
124
|
calkit/tests/test_pipeline.py,sha256=27BfaE9oCwj9BfDdLQWr-Yco_mpJ1mvULTgajbfLOEI,135627
|
|
125
125
|
calkit/tests/test_procedures.py,sha256=NJUIDy-gkou1BBABeWjazm__-8ZxHwvzzjkLm6kbviI,11359
|
|
126
126
|
calkit/tests/test_provenance.py,sha256=bkL6a7E2Q-JINS8oLS5hs5x-2IzRQkzKJ8HWxSeiemo,23818
|
|
127
|
-
calkit/tests/test_questions.py,sha256=
|
|
127
|
+
calkit/tests/test_questions.py,sha256=XSKwPfJpOY8HmhpY0uwf33rZU2gbJnCZz57f35m6GL8,26790
|
|
128
128
|
calkit/tests/test_releases.py,sha256=tiQvrAYMxhg0mHzhcRGi0avAu_rJGhPdhK_lCg2Sb9k,19224
|
|
129
129
|
calkit/tests/test_reproducibility.py,sha256=QIkUugP_1zqUDlgYZsEnkO144FqIQpamt6v7aDPabnE,10273
|
|
130
130
|
calkit/tests/test_resources.py,sha256=Wm2hr_Vy-Dk8rSElYbIzlZnCbomJInpTvUMOxT-mg04,6746
|
|
@@ -132,7 +132,7 @@ calkit/tests/test_schema.py,sha256=3h7R23bjqQntfuygrKzGwQfWPBZxX9fdBhnu6AmBqp8,8
|
|
|
132
132
|
calkit/tests/test_templates.py,sha256=yWln6iro64_vqCgJElhSFOthA7xrHXE1GXDJJ9JEBMQ,2613
|
|
133
133
|
calkit/tests/test_workspace.py,sha256=l1txfs2Oa71vbC9dEHrlcSCXinUeV19SwMZlWqdSpro,60163
|
|
134
134
|
calkit/tests/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
135
|
-
calkit/tests/cli/test_check.py,sha256=
|
|
135
|
+
calkit/tests/cli/test_check.py,sha256=gcix6EcPCQdPNx_VcdnYNU3So5PE4oIqGDguz80fJnk,52303
|
|
136
136
|
calkit/tests/cli/test_config.py,sha256=AAz-cwCgYM_XTLZdv58XAax1GmvAWgDfksEpn7iiezs,5393
|
|
137
137
|
calkit/tests/cli/test_core.py,sha256=eMEzdL7YKp6Ss5ey4HVtzvjWzqk9KtVJegFjCfhGDu8,1640
|
|
138
138
|
calkit/tests/cli/test_delete.py,sha256=qIbgwKc8nBjmkynzyVuVAjrQB96XSmGRet_jfxs19_0,750
|
|
@@ -163,26 +163,26 @@ calkit/tests/models/test_core.py,sha256=iDi0mvgZDBj2n7bo-cyKd8dvRZHl5ywWbcab4BPs
|
|
|
163
163
|
calkit/tests/models/test_iteration.py,sha256=vjuDKwyYQAV4w1qF9VJWQEHksq3KRd6gtT39_a-RhnI,644
|
|
164
164
|
calkit/tests/models/test_pipeline.py,sha256=6oNoJV7ookVKpeQcgs0sOSVaO99PvX_xbb-cToTc3ZQ,33446
|
|
165
165
|
calkit/agent_skills/add-pipeline-stage/SKILL.md,sha256=kpYYb-rJsS4B9p1Ub3gL21irCG5ZZ4DvWZpdJeEwPZk,3772
|
|
166
|
-
calkit/agent_skills/build-paper-pipeline/SKILL.md,sha256=
|
|
167
|
-
calkit/agent_skills/check-questions/SKILL.md,sha256=
|
|
166
|
+
calkit/agent_skills/build-paper-pipeline/SKILL.md,sha256=aV9t86FxQNkeXyswM03fb5LW3ns4pcsWQyYNZ33niLI,6829
|
|
167
|
+
calkit/agent_skills/check-questions/SKILL.md,sha256=6-NVWX2O_soAL0vg38NsBgA_h3roUT3pHwKQ5cwDxQw,9576
|
|
168
168
|
calkit/agent_skills/check-reproducibility/SKILL.md,sha256=5SGmtnbq8bSJW0Zwav_aVzfuhilHwBkkdxGuZvPHxGU,4699
|
|
169
169
|
calkit/agent_skills/conventions/SKILL.md,sha256=kK0Sa54uIORAslE3U53sXJUG-TMdM-Ie72hCMIzzbxQ,8909
|
|
170
170
|
calkit/agent_skills/create-pipeline/SKILL.md,sha256=2FGw1iDQVRk5FUq79GP3lPdgvgof2Wi1O9O-abGGtgs,5422
|
|
171
|
-
calkit_python-0.47.
|
|
172
|
-
calkit_python-0.47.
|
|
173
|
-
calkit_python-0.47.
|
|
174
|
-
calkit_python-0.47.
|
|
175
|
-
calkit_python-0.47.
|
|
176
|
-
calkit_python-0.47.
|
|
177
|
-
calkit_python-0.47.
|
|
178
|
-
calkit_python-0.47.
|
|
179
|
-
calkit_python-0.47.
|
|
180
|
-
calkit_python-0.47.
|
|
181
|
-
calkit_python-0.47.
|
|
182
|
-
calkit_python-0.47.
|
|
183
|
-
calkit_python-0.47.
|
|
184
|
-
calkit_python-0.47.
|
|
185
|
-
calkit_python-0.47.
|
|
186
|
-
calkit_python-0.47.
|
|
187
|
-
calkit_python-0.47.
|
|
188
|
-
calkit_python-0.47.
|
|
171
|
+
calkit_python-0.47.6.data/data/etc/jupyter/jupyter_server_config.d/calkit.json,sha256=CWrZP--JDGz8fsvbAlKr_duTL1vDriGT48SL1YCtkY0,81
|
|
172
|
+
calkit_python-0.47.6.data/data/share/jupyter/labextensions/calkit/package.json,sha256=0hnHqhCly59Wg7urFGahc2mSupCWwiPfwKWCYfnbB2E,6202
|
|
173
|
+
calkit_python-0.47.6.data/data/share/jupyter/labextensions/calkit/schemas/calkit/package.json.orig,sha256=egkbkHmJYzDARH3_4qpoF75_Svpj_dfPaIwRU-PNjW4,6064
|
|
174
|
+
calkit_python-0.47.6.data/data/share/jupyter/labextensions/calkit/schemas/calkit/plugin.json,sha256=YSpIrwpwB-lQBk9Mwv-npedWTOOZreO6nlWZhqlWyGI,840
|
|
175
|
+
calkit_python-0.47.6.data/data/share/jupyter/labextensions/calkit/static/506.c34b070098184e33.js,sha256=0PxdmgdZvLeh6RBcfgN1pcriNO8oCeTK5WJvUIEA3MY,112384
|
|
176
|
+
calkit_python-0.47.6.data/data/share/jupyter/labextensions/calkit/static/57d13a7399cec3c5.png,sha256=svHD7-cMtTnRITFwugwsVaB9nZ-h8A61ose8z32HiRE,24850
|
|
177
|
+
calkit_python-0.47.6.data/data/share/jupyter/labextensions/calkit/static/616.528427eb54a4a0c0.js,sha256=d0rHq7cqAjj1GNAMLOpl8dyY8yyKUQcLPCOUr-wJGdQ,8129
|
|
178
|
+
calkit_python-0.47.6.data/data/share/jupyter/labextensions/calkit/static/740.6bf87276e9e5788a.js,sha256=4E0sK8x-ldVkL-1sM07wgJGS1vtVD9Uly7VtoFTi23c,221
|
|
179
|
+
calkit_python-0.47.6.data/data/share/jupyter/labextensions/calkit/static/899.f9b9f9bf705a6493.js,sha256=RNTIItG9k-A_Ip54fRR3QMCjiffkY0bo8zQ2Xh7TyOY,57096
|
|
180
|
+
calkit_python-0.47.6.data/data/share/jupyter/labextensions/calkit/static/935.46ecc6bf99aa593a.js,sha256=Cmp0KWM5YDZ-zcsJWWgqCMIF6adeHQH80rxz1sas45E,51183
|
|
181
|
+
calkit_python-0.47.6.data/data/share/jupyter/labextensions/calkit/static/remoteEntry.109a3a8379365e59.js,sha256=4JavPLcn703qVSC2E1_MD1thrJNL4N-eITjnfNhSDSM,15066
|
|
182
|
+
calkit_python-0.47.6.data/data/share/jupyter/labextensions/calkit/static/style.js,sha256=LTlA3M58hwjqmhomdcrNRO9GM58zZcf0V76WX9HI6EQ,146
|
|
183
|
+
calkit_python-0.47.6.data/data/share/jupyter/labextensions/calkit/static/third-party-licenses.json,sha256=2BGdItLJwO3fAopLl4eJvp26TEwuscYC0-yFaF-LaLU,13683
|
|
184
|
+
calkit_python-0.47.6.dist-info/METADATA,sha256=OeP79x--jBv_sZbbCf6uUoALyK6Dr6821tDz2vgR9sA,16997
|
|
185
|
+
calkit_python-0.47.6.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
186
|
+
calkit_python-0.47.6.dist-info/entry_points.txt,sha256=TtpHpcv2vFVAVYxFVp0XrwCV_z7FWIOguEiypXRDoOw,131
|
|
187
|
+
calkit_python-0.47.6.dist-info/licenses/LICENSE,sha256=9ZamCaSUTZk9rcrnf-sWFKLOHr3ws-S_dgKMegW4nw8,1056
|
|
188
|
+
calkit_python-0.47.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|