claims-cli 0.6.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.
Files changed (51) hide show
  1. claims/__init__.py +132 -0
  2. claims/__main__.py +5 -0
  3. claims/adapters/__init__.py +6 -0
  4. claims/adapters/array.py +212 -0
  5. claims/adapters/base.py +75 -0
  6. claims/adapters/cells.py +94 -0
  7. claims/adapters/columnar.py +188 -0
  8. claims/adapters/prose.py +261 -0
  9. claims/adapters/reads.py +47 -0
  10. claims/adapters/rows.py +116 -0
  11. claims/adapters/sheet.py +149 -0
  12. claims/adapters/sqlite.py +73 -0
  13. claims/adapters/table.py +135 -0
  14. claims/adapters/tree.py +163 -0
  15. claims/checks.py +511 -0
  16. claims/cli.py +1103 -0
  17. claims/core/__init__.py +9 -0
  18. claims/core/artifacts.py +121 -0
  19. claims/core/checks.py +132 -0
  20. claims/core/claims.py +100 -0
  21. claims/core/evidence.py +708 -0
  22. claims/core/manifest.py +349 -0
  23. claims/core/numbers.py +146 -0
  24. claims/core/outcomes.py +410 -0
  25. claims/core/report.py +503 -0
  26. claims/corpus.py +344 -0
  27. claims/exceptions.py +106 -0
  28. claims/export/__init__.py +23 -0
  29. claims/export/rocrate.py +787 -0
  30. claims/export/sarif.py +401 -0
  31. claims/manifest.py +59 -0
  32. claims/models.py +222 -0
  33. claims/numbers.py +453 -0
  34. claims/origins.py +303 -0
  35. claims/policy.py +369 -0
  36. claims/provenance.py +42 -0
  37. claims/record.py +1099 -0
  38. claims/regression.py +171 -0
  39. claims/renderers/__init__.py +10 -0
  40. claims/renderers/sarif.py +294 -0
  41. claims/reruns.py +111 -0
  42. claims/resolve.py +116 -0
  43. claims/stale.py +333 -0
  44. claims/starter.py +432 -0
  45. claims/toolchain.py +120 -0
  46. claims/verify.py +1371 -0
  47. claims/why.py +398 -0
  48. claims_cli-0.6.0.dist-info/METADATA +139 -0
  49. claims_cli-0.6.0.dist-info/RECORD +51 -0
  50. claims_cli-0.6.0.dist-info/WHEEL +4 -0
  51. claims_cli-0.6.0.dist-info/entry_points.txt +2 -0
claims/checks.py ADDED
@@ -0,0 +1,511 @@
1
+ """A record of what each assertion said when it was checked.
2
+
3
+ `repro verify` reports on a manifest as it is now. After a check does not verify, the
4
+ `reported` value, the address or the claim's text can be edited until it does, and the report
5
+ of the last run shows none of that. Every `repro verify` therefore appends, for each assertion
6
+ whose declaration, artifact digest or outcome differs from the last entry recorded for it, what
7
+ it found.
8
+
9
+ **Two records, one format.** The local record is `repro/checks.jsonl` in the repository's git
10
+ directory, the one `git rev-parse --absolute-git-dir` names, so a linked worktree keeps its
11
+ own. It is written by every check, belongs to this clone and is never committed, as git's
12
+ reflog is, and the working tree is not touched. The tracked record is `.repro/checks.jsonl` in
13
+ the working tree, written where `repro verify --record` asks, for a project whose history
14
+ should travel. The tracked record is a copy of the local one, line for line, up to the last
15
+ time it was asked for; a clone that finds a tracked record longer than its local one takes
16
+ the entries it lacks. Outside a git repository there is no local record, and `--record`
17
+ appends to the tracked one directly.
18
+
19
+ **A chain.** Each entry carries its position, `seq`, and the SHA-256 of the entry before it as
20
+ stored, `prev_hash`, and a head file beside the record, `ledger.head`, holds the number of
21
+ entries and the hash of the last. These are the `results` ledger's fields and its writer:
22
+ `provenance_core.chain.append_event` appends under a lock held across reading the last entry and
23
+ writing the next, so two checks running at once write two entries in a row and not two
24
+ claiming one position, and it refuses to append to a chain that does not verify.
25
+
26
+ Reading is done here and not by `provenance_core.chain.verify`, which takes a lock by creating a file
27
+ beside the record: `repro verify` and `repro why` must leave the working tree as it is.
28
+
29
+ **What the chain shows, and what it does not.** An entry edited, removed from the middle or
30
+ moved is found, with its number, and the entries from there on are shown as the record holds
31
+ them and marked unverifiable. Entries cut from the end are found while the head file is as it
32
+ was. A record and its head file cut back together, or deleted together and grown again, look
33
+ like a shorter or a new record: nothing inside the two files can show it. The local record has
34
+ no witness outside its directory. The tracked record's head file is committed with it, so the
35
+ repository's history holds every earlier head, and that is what makes a truncation visible to
36
+ someone who has the history. Nothing here is signed or timestamped by anyone else, and whoever
37
+ can write both files and rewrite the history can rewrite the record.
38
+ """
39
+
40
+ from __future__ import annotations
41
+
42
+ import collections
43
+ import datetime
44
+ import json
45
+ import os
46
+ import pathlib
47
+ from typing import Literal
48
+
49
+ from provenance_core import ZERO, exclusive_lock, sha256_of_text, try_run
50
+ from provenance_core.chain import ANCHOR, ChainStatus, append_event, read_anchor, write_anchor
51
+ from pydantic import ValidationError
52
+
53
+ from claims.exceptions import ReproError
54
+ from claims.models import (
55
+ AggregateEvidence,
56
+ CheckedAssertion,
57
+ CheckRecord,
58
+ Claim,
59
+ CorrespondenceEvidence,
60
+ Decision,
61
+ Evidence,
62
+ Locator,
63
+ Manifest,
64
+ QuoteEvidence,
65
+ RecordSetAside,
66
+ RecordState,
67
+ TableCellEvidence,
68
+ TreeLocator,
69
+ VerificationReport,
70
+ )
71
+
72
+ #: The tracked record, beside the manifest.
73
+ RECORD = pathlib.Path(".repro") / "checks.jsonl"
74
+
75
+ #: The local record, inside the git directory.
76
+ LOCAL = pathlib.Path("repro") / "checks.jsonl"
77
+
78
+ #: Why no local record is kept, where none is.
79
+ NOT_A_REPOSITORY = "not in a git repository"
80
+
81
+ #: The flag that sets a damaged record aside. Named in the line that reports one.
82
+ SET_ASIDE = "--set-aside-record"
83
+
84
+ #: Which assertion an entry is about: the claim's id, the kind, the name and the position.
85
+ Identity = tuple[str, str, str, int]
86
+
87
+ #: Which of the two records.
88
+ Kind = Literal["local", "tracked"]
89
+
90
+ #: The states of a chain whose every entry follows from the one before it.
91
+ _SOUND = (ChainStatus.INTACT, ChainStatus.EXTENDED, ChainStatus.NO_ANCHOR)
92
+
93
+
94
+ class RecordDivergedError(ReproError):
95
+ """The tracked record is not the beginning of the local one, so it cannot be brought up
96
+ to it by appending."""
97
+
98
+ def __init__(self, path: pathlib.Path, entry: int) -> None:
99
+ self.path = path
100
+ self.entry = entry
101
+ super().__init__(f"{path}: diverges from the local record at entry {entry}")
102
+
103
+
104
+ class RecordBrokenError(ReproError):
105
+ """A record's chain does not verify, so nothing is appended to it."""
106
+
107
+ def __init__(self, state: RecordState, program: str = "repro") -> None:
108
+ self.state = state
109
+ super().__init__(
110
+ f"{state.path}: refusing to append to a chain reported as {state.status}: "
111
+ f"{'; '.join(state.problems)}; `{program} verify {SET_ASIDE}` keeps it and starts anew"
112
+ )
113
+
114
+
115
+ class RecordIntactError(ReproError):
116
+ """Nothing was set aside: every record there is verifies, and the two agree."""
117
+
118
+ def __init__(self, root: pathlib.Path) -> None:
119
+ self.root = root
120
+ super().__init__(f"{root}: no record here is damaged, so none is set aside")
121
+
122
+
123
+ def address(locator: Locator) -> str:
124
+ """A locator as a reader would write it: a JSON Pointer bare, any other by its fields."""
125
+ if isinstance(locator, TreeLocator):
126
+ return locator.pointer
127
+ fields = locator.model_dump(mode="json", exclude={"contract", "kind"}, exclude_defaults=True)
128
+ return ", ".join(
129
+ f"{key} {json.dumps(value, ensure_ascii=False)}" for key, value in fields.items()
130
+ )
131
+
132
+
133
+ def _address(evidence: Evidence) -> str:
134
+ if isinstance(evidence, QuoteEvidence):
135
+ return ""
136
+ if isinstance(evidence, CorrespondenceEvidence):
137
+ return "; ".join(
138
+ f"{side.name}: {side.artifact} at {address(side.locator)}" for side in evidence.sides
139
+ )
140
+ if isinstance(evidence, AggregateEvidence):
141
+ return "; ".join(
142
+ f"{member.artifact} at {address(member.locator)}" for member in evidence.members
143
+ )
144
+ if isinstance(evidence, TableCellEvidence) and not evidence.addresses_one_row:
145
+ # Neither `row` nor `where`, or both: there is no one address to record.
146
+ return ""
147
+ return address(evidence.locator)
148
+
149
+
150
+ def _conversion(evidence: Evidence) -> str:
151
+ if isinstance(evidence, CorrespondenceEvidence):
152
+ return "; ".join(
153
+ f"{side.name}: {side.conversion.value}" for side in evidence.sides if side.conversion
154
+ )
155
+ declared = getattr(evidence, "conversion", None)
156
+ return declared.value if declared else ""
157
+
158
+
159
+ def checked(
160
+ claim: Claim, decisions: tuple[Decision, ...], at: str = ""
161
+ ) -> tuple[CheckedAssertion, ...]:
162
+ """Each assertion of one claim as a check found it, in the order declared."""
163
+ seen: collections.Counter[tuple[str, str]] = collections.Counter()
164
+ found = []
165
+ for evidence, decision in zip(claim.evidence, decisions, strict=True):
166
+ name = getattr(evidence, "name", "")
167
+ found.append(
168
+ CheckedAssertion(
169
+ claim_id=claim.id,
170
+ kind=evidence.kind,
171
+ name=name,
172
+ position=seen[evidence.kind, name],
173
+ claim_digest=claim.digest.value,
174
+ printed=evidence.text
175
+ if isinstance(evidence, QuoteEvidence)
176
+ else getattr(evidence, "reported", None),
177
+ address=_address(evidence),
178
+ conversion=_conversion(evidence),
179
+ digests={side.artifact_id: side.artifact_digest for side in decision.sides}
180
+ or {member.artifact_id: member.artifact_digest for member in decision.members}
181
+ or dict.fromkeys(evidence.artifacts, decision.artifact_digest),
182
+ outcome=decision.outcome,
183
+ reason=decision.reason,
184
+ checked_at=at,
185
+ )
186
+ )
187
+ seen[evidence.kind, name] += 1
188
+ return tuple(found)
189
+
190
+
191
+ def git_dir(root: pathlib.Path) -> pathlib.Path | None:
192
+ """The git directory of the repository `root` is in, as git resolves it: a linked
193
+ worktree's own, and a submodule's. None outside a repository."""
194
+ found = try_run("rev-parse", "--absolute-git-dir", cwd=root)
195
+ return pathlib.Path(found) if found else None
196
+
197
+
198
+ def _raw(path: pathlib.Path) -> list[str]:
199
+ """A record's entries as stored, one per line."""
200
+ if not path.is_file():
201
+ return []
202
+ return [line.strip() for line in path.read_text(encoding="utf-8").splitlines() if line.strip()]
203
+
204
+
205
+ def _parsed(raw: str) -> CheckRecord | None:
206
+ try:
207
+ return CheckRecord.model_validate_json(raw)
208
+ except ValidationError:
209
+ return None
210
+
211
+
212
+ def _chain(path: pathlib.Path, kind: Kind, lines: list[str]) -> RecordState:
213
+ """Whether each entry follows from the one before it, and whether the head file agrees.
214
+
215
+ The status is the first fault's, as in the `results` ledger: a chain edited and then cut
216
+ is edited.
217
+ """
218
+ status, problems, first = ChainStatus.INTACT, [], None
219
+
220
+ def fault(found: ChainStatus, entry: int, said: str) -> None:
221
+ nonlocal status, first
222
+ problems.append(said)
223
+ if status is ChainStatus.INTACT:
224
+ status = found
225
+ first = entry if first is None else min(first, entry)
226
+
227
+ prev = ZERO
228
+ for number, raw in enumerate(lines, start=1):
229
+ entry = _parsed(raw)
230
+ if entry is None:
231
+ fault(ChainStatus.CORRUPT, number, f"entry {number} is not one `repro verify` wrote")
232
+ else:
233
+ if entry.prev_hash != prev:
234
+ fault(
235
+ ChainStatus.EDITED,
236
+ max(number - 1, 1),
237
+ f"entry {number - 1} does not hash to what entry {number} recorded for the "
238
+ "entry before it"
239
+ if number > 1
240
+ else "entry 1 records an entry before it",
241
+ )
242
+ if entry.seq != number - 1:
243
+ fault(
244
+ ChainStatus.REORDERED,
245
+ number,
246
+ f"entry {number} is numbered {entry.seq + 1} of the chain",
247
+ )
248
+ prev = sha256_of_text(raw)
249
+
250
+ anchor = read_anchor(path)
251
+ if anchor is None:
252
+ if status is ChainStatus.INTACT:
253
+ status = ChainStatus.NO_ANCHOR
254
+ problems.append("no head file, so entries cut from the end would not be found")
255
+ elif anchor.get("count") != len(lines):
256
+ longer = len(lines) > anchor.get("count", 0)
257
+ if status is ChainStatus.INTACT:
258
+ status = ChainStatus.EXTENDED if longer else ChainStatus.TRUNCATED
259
+ problems.append(
260
+ f"the head file records {anchor.get('count')} entries, the record holds {len(lines)}"
261
+ )
262
+ elif anchor.get("head") != prev:
263
+ fault(
264
+ ChainStatus.EDITED,
265
+ max(len(lines), 1),
266
+ f"entry {len(lines)} does not hash to what the head file records",
267
+ )
268
+ return RecordState(
269
+ kind=kind,
270
+ path=str(path),
271
+ entries=len(lines),
272
+ status=status.value,
273
+ problems=tuple(problems),
274
+ unverifiable_from=first,
275
+ )
276
+
277
+
278
+ def _prefix(one: list[str], other: list[str]) -> int:
279
+ """How many entries two records share from the start."""
280
+ shared = 0
281
+ while shared < min(len(one), len(other)) and one[shared] == other[shared]:
282
+ shared += 1
283
+ return shared
284
+
285
+
286
+ class Records:
287
+ """The project's records of checks, read once: each one's state, and per assertion each
288
+ way an entry holds it, oldest first."""
289
+
290
+ def __init__(self, root: pathlib.Path, local: bool = True) -> None:
291
+ at = git_dir(root) if local else None
292
+ self.no_local_record = NOT_A_REPOSITORY if local and at is None else ""
293
+ self.local = at / LOCAL if at is not None else None
294
+ self.tracked = root / RECORD
295
+ here = _raw(self.local) if self.local is not None else []
296
+ there = _raw(self.tracked)
297
+ shared = _prefix(here, there)
298
+
299
+ states = []
300
+ vouched: dict[str, bool] = {}
301
+ for kind, path, lines in (("local", self.local, here), ("tracked", self.tracked, there)):
302
+ if path is None or not (lines or read_anchor(path)):
303
+ continue
304
+ state = _chain(path, kind, lines)
305
+ if kind == "tracked" and here and shared < min(len(here), len(there)):
306
+ state = state.model_copy(update={"diverges_at": shared + 1})
307
+ states.append(state)
308
+ for number, raw in enumerate(lines, start=1):
309
+ sound = state.unverifiable_from is None or number < state.unverifiable_from
310
+ vouched[raw] = vouched.get(raw, False) or sound
311
+ self.states = tuple(states)
312
+ self.lines = {"local": here, "tracked": there}
313
+
314
+ # What the two share, then what each holds beyond it, in the order appended.
315
+ # An entry that does not parse is counted as a fault above and holds nothing to show.
316
+ read = {raw: entry for raw in (*here, *there) if (entry := _parsed(raw)) is not None}
317
+ beyond = sorted(
318
+ (raw for raw in dict.fromkeys(here[shared:] + there[shared:]) if raw in read),
319
+ key=lambda raw: read[raw].timestamp,
320
+ )
321
+ found: dict[Identity, list[CheckedAssertion]] = collections.defaultdict(list)
322
+ for raw in (*(raw for raw in here[:shared] if raw in read), *beyond):
323
+ entry = read[raw]
324
+ for assertion in entry.assertions:
325
+ found[assertion.identity].append(
326
+ assertion.model_copy(
327
+ update={"checked_at": entry.timestamp, "verifiable": vouched[raw]}
328
+ )
329
+ )
330
+ self.history = {identity: tuple(states) for identity, states in found.items()}
331
+ self.set_aside = tuple(
332
+ note.model_copy(update={"at": read[raw].timestamp})
333
+ for raw in (*(raw for raw in here[:shared] if raw in read), *beyond)
334
+ if (note := read[raw].set_aside) is not None
335
+ )
336
+
337
+ def state(self, kind: Kind) -> RecordState | None:
338
+ return next((state for state in self.states if state.kind == kind), None)
339
+
340
+
341
+ def _take(behind: pathlib.Path, lines: list[str], held: int) -> None:
342
+ """Append to one record the entries another holds beyond it, as stored, and move its head
343
+ file."""
344
+ behind.parent.mkdir(parents=True, exist_ok=True)
345
+ with behind.open("a", encoding="utf-8") as handle:
346
+ handle.write("".join(f"{line}\n" for line in lines[held:]))
347
+ handle.flush()
348
+ os.fsync(handle.fileno())
349
+ write_anchor(behind, len(lines), sha256_of_text(lines[-1]))
350
+
351
+
352
+ def record(
353
+ manifest: Manifest,
354
+ report: VerificationReport,
355
+ local: bool = True,
356
+ tracked: bool = False,
357
+ program: str = "repro",
358
+ ) -> None:
359
+ """Append what one `repro verify` found to the project's records of checks.
360
+
361
+ To the local record where `local` and the project is in a git repository, and to the
362
+ tracked one as well where `tracked`. Nothing is written where every assertion is as it
363
+ was last recorded and the tracked record, if asked for, already holds every local entry.
364
+
365
+ Raises `RecordBrokenError` where the chain to append to does not verify,
366
+ `RecordDivergedError` where the tracked record is not the beginning of the local one, and
367
+ `OSError` where a record cannot be written.
368
+ """
369
+ root = manifest.path.parent if manifest.path else pathlib.Path.cwd()
370
+ at = git_dir(root) if local else None
371
+ kept = at / LOCAL if at is not None else None
372
+ carried = root / RECORD
373
+ written = kept or (carried if tracked else None)
374
+ if written is None:
375
+ return
376
+
377
+ # One lock across reading the last entries and writing the next, so two checks at once
378
+ # append one after the other and the second finds what the first wrote.
379
+ with exclusive_lock(written):
380
+ found = Records(root, local)
381
+ here, there = found.lines["local"], found.lines["tracked"]
382
+ carried_state = found.state("tracked")
383
+ if (
384
+ kept is not None
385
+ and _prefix(here, there) == len(here) < len(there)
386
+ and carried_state is not None
387
+ and carried_state.unverifiable_from is None
388
+ ):
389
+ # A clone, or a pull: the tracked record holds entries this clone has not seen.
390
+ _take(kept, there, len(here))
391
+ found = Records(root, local)
392
+ state = found.state("local" if kept is not None else "tracked")
393
+ if state is not None and ChainStatus(state.status) not in _SOUND:
394
+ raise RecordBrokenError(state, program)
395
+ changed = [
396
+ assertion
397
+ for claim, assessed in zip(manifest.claims, report.claims, strict=True)
398
+ for assertion in checked(claim, assessed.decisions)
399
+ if assertion.identity not in found.history
400
+ or not assertion.same_as(found.history[assertion.identity][-1])
401
+ ]
402
+ if changed:
403
+ append_event(
404
+ written,
405
+ {
406
+ "schema_version": CheckRecord.model_fields["schema_version"].default,
407
+ "assertions": [
408
+ assertion.model_dump(mode="json", exclude={"checked_at", "verifiable"})
409
+ for assertion in changed
410
+ ],
411
+ },
412
+ )
413
+
414
+ if tracked and kept is not None:
415
+ with exclusive_lock(carried):
416
+ here, there = _raw(kept), _raw(carried)
417
+ shared = _prefix(here, there)
418
+ if shared < len(there):
419
+ raise RecordDivergedError(carried, shared + 1)
420
+ state = _chain(carried, "tracked", there)
421
+ if there and ChainStatus(state.status) not in _SOUND:
422
+ raise RecordBrokenError(state, program)
423
+ if len(here) > len(there):
424
+ _take(carried, here, len(there))
425
+
426
+
427
+ def _move_aside(path: pathlib.Path, state: RecordState, lines: list[str]) -> RecordSetAside:
428
+ """Rename a record and its head file to names that say when they were set aside, and say
429
+ what was found in it. Nothing is deleted, and a name already taken is not written over."""
430
+ stamp = datetime.datetime.now(datetime.UTC).strftime("%Y%m%dT%H%M%SZ")
431
+ vouched = len(lines) if state.unverifiable_from is None else state.unverifiable_from - 1
432
+ for name in (path.name, ANCHOR):
433
+ source, target = path.with_name(name), path.with_name(f"{name}.set-aside-{stamp}")
434
+ if target.exists():
435
+ raise FileExistsError(17, "File exists", str(target))
436
+ if source.exists():
437
+ os.replace(source, target)
438
+ problems = state.problems + (
439
+ (f"diverges from the local record at entry {state.diverges_at}",)
440
+ if state.diverges_at is not None
441
+ else ()
442
+ )
443
+ return RecordSetAside(
444
+ kind=state.kind,
445
+ kept_as=f"{path.name}.set-aside-{stamp}",
446
+ entries=len(lines),
447
+ head=sha256_of_text(lines[vouched - 1]) if vouched else ZERO,
448
+ status=state.status,
449
+ problems=problems,
450
+ )
451
+
452
+
453
+ def set_aside(
454
+ manifest: Manifest, local: bool = True, tracked: bool = False
455
+ ) -> tuple[RecordSetAside, ...]:
456
+ """Move a damaged record of checks aside and say so in the chain that goes on.
457
+
458
+ The record `repro verify` appends to, where its chain does not verify: the next entry is
459
+ the first of a new chain and names the old one, the hash of its last entry that verified
460
+ and what was found. With `tracked`, the tracked record as well, where it does not verify or
461
+ is not the beginning of the local one; it is then copied afresh by the check that follows.
462
+ The files are renamed and kept.
463
+
464
+ Raises `RecordIntactError` where no record qualifies. Never called except by name.
465
+ """
466
+ root = manifest.path.parent if manifest.path else pathlib.Path.cwd()
467
+ at = git_dir(root) if local else None
468
+ kept = at / LOCAL if at is not None else None
469
+ carried = root / RECORD
470
+ written = kept or carried
471
+ with exclusive_lock(written):
472
+ found = Records(root, local)
473
+ moved = []
474
+ for kind, path in (("local", kept), ("tracked", carried)):
475
+ state = found.state(kind)
476
+ if path is None or state is None or (path != written and not tracked):
477
+ continue
478
+ broken = ChainStatus(state.status) not in _SOUND
479
+ # A tracked record cannot follow a local one that starts again.
480
+ orphaned = path != written and (state.diverges_at is not None or bool(moved))
481
+ if broken or orphaned:
482
+ moved.append(_move_aside(path, state, found.lines[kind]))
483
+ if not moved:
484
+ raise RecordIntactError(root)
485
+ for note in moved:
486
+ append_event(
487
+ written,
488
+ {
489
+ "schema_version": CheckRecord.model_fields["schema_version"].default,
490
+ "assertions": [],
491
+ "set_aside": note.model_dump(mode="json", exclude={"at"}),
492
+ },
493
+ )
494
+ return tuple(moved)
495
+
496
+
497
+ __all__ = [
498
+ "LOCAL",
499
+ "NOT_A_REPOSITORY",
500
+ "RECORD",
501
+ "Identity",
502
+ "RecordBrokenError",
503
+ "RecordDivergedError",
504
+ "RecordIntactError",
505
+ "Records",
506
+ "address",
507
+ "checked",
508
+ "git_dir",
509
+ "record",
510
+ "set_aside",
511
+ ]