quirework 0.1.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 (138) hide show
  1. quirework/__init__.py +3 -0
  2. quirework/__main__.py +3 -0
  3. quirework/cli.py +431 -0
  4. quirework/core/__init__.py +1 -0
  5. quirework/core/artifact_paths.py +86 -0
  6. quirework/core/checks.py +235 -0
  7. quirework/core/frontmatter.py +83 -0
  8. quirework/core/globs.py +45 -0
  9. quirework/core/pinned.py +169 -0
  10. quirework/core/usage.py +604 -0
  11. quirework/delivery/__init__.py +1 -0
  12. quirework/delivery/artifacts.py +210 -0
  13. quirework/delivery/text.py +166 -0
  14. quirework/manuscript/__init__.py +1 -0
  15. quirework/manuscript/backfill.py +1972 -0
  16. quirework/manuscript/editions.py +111 -0
  17. quirework/manuscript/footprint.py +95 -0
  18. quirework/manuscript/formats.py +208 -0
  19. quirework/manuscript/lifecycle.py +191 -0
  20. quirework/manuscript/notes.py +2301 -0
  21. quirework/manuscript/pages.py +284 -0
  22. quirework/manuscript/quarto.py +416 -0
  23. quirework/manuscript/quotes.py +512 -0
  24. quirework/manuscript/rename.py +309 -0
  25. quirework/manuscript/sources.py +286 -0
  26. quirework/manuscript/trail.py +1875 -0
  27. quirework/manuscript/units.py +416 -0
  28. quirework/project.py +645 -0
  29. quirework/provenance/__init__.py +1 -0
  30. quirework/provenance/agyfilter.py +436 -0
  31. quirework/provenance/backup/__init__.py +469 -0
  32. quirework/provenance/backup/base.py +290 -0
  33. quirework/provenance/backup/command.py +107 -0
  34. quirework/provenance/backup/directory.py +142 -0
  35. quirework/provenance/backup/git.py +150 -0
  36. quirework/provenance/backup/rclone.py +153 -0
  37. quirework/provenance/capture/__init__.py +1 -0
  38. quirework/provenance/capture/commands.py +28 -0
  39. quirework/provenance/capture/extract_artifacts.py +110 -0
  40. quirework/provenance/capture/extract_files.py +118 -0
  41. quirework/provenance/capture/fetch_conversation.py +127 -0
  42. quirework/provenance/capture/fetch_gemini.py +149 -0
  43. quirework/provenance/capture/render_transcript.py +126 -0
  44. quirework/provenance/capture/transform.py +22 -0
  45. quirework/provenance/catalog.py +733 -0
  46. quirework/provenance/ccfilter.py +1126 -0
  47. quirework/provenance/codexfilter.py +883 -0
  48. quirework/provenance/coldread.py +1731 -0
  49. quirework/provenance/curate.py +67 -0
  50. quirework/provenance/humaninput.py +811 -0
  51. quirework/provenance/ingest.py +31 -0
  52. quirework/provenance/ingestion/__init__.py +1 -0
  53. quirework/provenance/ingestion/agy.py +192 -0
  54. quirework/provenance/ingestion/claude.py +493 -0
  55. quirework/provenance/ingestion/codex.py +296 -0
  56. quirework/provenance/lineage.py +148 -0
  57. quirework/provenance/metrics.py +1247 -0
  58. quirework/provenance/rulings.py +478 -0
  59. quirework/provenance/scope.py +73 -0
  60. quirework/provenance/search.py +455 -0
  61. quirework/provenance/semantic.py +475 -0
  62. quirework/provenance/show.py +1213 -0
  63. quirework/provenance/store.py +1271 -0
  64. quirework/provenance/trees.py +133 -0
  65. quirework/provenance/worktrees.py +199 -0
  66. quirework/providers/__init__.py +1 -0
  67. quirework/providers/blind.py +2192 -0
  68. quirework/providers/canary.py +685 -0
  69. quirework/providers/catalog.py +44 -0
  70. quirework/providers/claude_login.py +92 -0
  71. quirework/providers/exits.py +196 -0
  72. quirework/providers/harness.py +830 -0
  73. quirework/providers/images.py +92 -0
  74. quirework/providers/models.py +1284 -0
  75. quirework/providers/quota.py +588 -0
  76. quirework/providers/reads.py +474 -0
  77. quirework/providers/replies.py +175 -0
  78. quirework/publish/__init__.py +1 -0
  79. quirework/publish/backends.py +112 -0
  80. quirework/publish/bookstage.py +1086 -0
  81. quirework/publish/build.py +60 -0
  82. quirework/publish/editions.py +48 -0
  83. quirework/publish/epubcheck.py +287 -0
  84. quirework/publish/experiment.py +1097 -0
  85. quirework/publish/figfonts.py +234 -0
  86. quirework/publish/fignumbers.py +293 -0
  87. quirework/publish/figures.py +729 -0
  88. quirework/publish/folios.py +62 -0
  89. quirework/publish/fonts.py +307 -0
  90. quirework/publish/graybuild.py +73 -0
  91. quirework/publish/hugo.py +117 -0
  92. quirework/publish/kindle.js +411 -0
  93. quirework/publish/kindle.py +1393 -0
  94. quirework/publish/panelkit.py +918 -0
  95. quirework/publish/plans.py +651 -0
  96. quirework/publish/proof.py +792 -0
  97. quirework/publish/redpen.py +1654 -0
  98. quirework/publish/render.py +942 -0
  99. quirework/publish/targets.py +216 -0
  100. quirework/publish/variant.py +918 -0
  101. quirework/runs/__init__.py +1 -0
  102. quirework/runs/__main__.py +15 -0
  103. quirework/runs/commands.py +278 -0
  104. quirework/runs/coverage.py +86 -0
  105. quirework/runs/engine.py +2143 -0
  106. quirework/runs/feed.py +853 -0
  107. quirework/runs/observe.py +379 -0
  108. quirework/runs/storage.py +155 -0
  109. quirework/settings.py +492 -0
  110. quirework/sops/__init__.py +1 -0
  111. quirework/sops/catalog.py +386 -0
  112. quirework/sops/commands.py +567 -0
  113. quirework/sops/composition.py +382 -0
  114. quirework/sops/instruments.py +184 -0
  115. quirework/sops/personas.py +296 -0
  116. quirework/sops/review.py +2568 -0
  117. quirework/sops/room.py +2791 -0
  118. quirework/sops/roundtable.py +2578 -0
  119. quirework/sops/sections.py +169 -0
  120. quirework/sops/workshop.py +1629 -0
  121. quirework/templates/backup.toml +39 -0
  122. quirework/templates/book.toml +88 -0
  123. quirework/templates/run.toml +32 -0
  124. quirework/templates/site.toml +90 -0
  125. quirework/templates/tells.toml +64 -0
  126. quirework/templates/terminology.toml +20 -0
  127. quirework/templates/tmpfiles.conf +42 -0
  128. quirework/testing.py +345 -0
  129. quirework/text/__init__.py +1 -0
  130. quirework/text/jargon.py +545 -0
  131. quirework/text/mdwrap.py +1001 -0
  132. quirework/text/tells.py +547 -0
  133. quirework-0.1.0.dist-info/METADATA +444 -0
  134. quirework-0.1.0.dist-info/RECORD +138 -0
  135. quirework-0.1.0.dist-info/WHEEL +5 -0
  136. quirework-0.1.0.dist-info/entry_points.txt +2 -0
  137. quirework-0.1.0.dist-info/licenses/LICENSE +21 -0
  138. quirework-0.1.0.dist-info/top_level.txt +1 -0
quirework/settings.py ADDED
@@ -0,0 +1,492 @@
1
+ """Every setting Quirework reads, and every key of the policy files it reads.
2
+
3
+ A setting a project leaves undeclared takes its default and says nothing,
4
+ which is how an optional setting should behave and why a new one is easy to
5
+ miss: a project that does not know the key exists never learns of it. So
6
+ the keys are listed here, and `quirework config keys` sets them against
7
+ what a project declares: each known key declared or not, and each declared
8
+ key Quirework does not know, which is usually a misspelling. A key the
9
+ project's own code reads is listed by the project under `[extensions]`, and
10
+ then counts as its own rather than unknown.
11
+
12
+ A key is written dotted, as `quirework.toml` nests it. A `*` stands for one
13
+ name the project chooses, such as an edition's kind or a reader preset's
14
+ name. A setting that names a policy file has the keys that file may hold in
15
+ `POLICIES`, written the same way from the top of the file.
16
+
17
+ This module names keys; `docs/configuration.md` says what each one means.
18
+ Quirework's own suite records every key the code reads and fails on one
19
+ that is not listed here (`recording`), so the list cannot fall behind.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from pathlib import Path
25
+
26
+ # The keys of [publish] a publish target's own table may also declare: a
27
+ # target is read as its [publish], each key it names replacing the shared one.
28
+ _PUBLISH = (
29
+ "backend",
30
+ "build",
31
+ "figure_checks",
32
+ "figure_fonts",
33
+ "figure_type_floor_pt",
34
+ "figures",
35
+ "grayscale_transform",
36
+ "layout_policy",
37
+ "render_policy",
38
+ "site",
39
+ "typst_template",
40
+ "variant_policy",
41
+ "variants",
42
+ )
43
+
44
+ SETTINGS: tuple[str, ...] = (
45
+ "schema",
46
+ "checks.command",
47
+ # The keys a project's own code reads, which Quirework's never does.
48
+ "extensions.policy_keys.*",
49
+ "extensions.settings",
50
+ *(
51
+ f"manuscript.{key}"
52
+ for key in (
53
+ "chapter_policy",
54
+ "chapters",
55
+ "collections.*.order",
56
+ "collections.*.pages",
57
+ "companion_anchors",
58
+ "companion_dirs",
59
+ "format",
60
+ "ideas",
61
+ "marks",
62
+ "noncanonical_roots",
63
+ "pages",
64
+ "quarto",
65
+ "references",
66
+ "references_public",
67
+ "review_notes",
68
+ "revision_field",
69
+ "source",
70
+ "source_copies",
71
+ "sources",
72
+ "spec",
73
+ "spec_public",
74
+ "spec_trail",
75
+ "spec_trail_leads",
76
+ "unit_name",
77
+ "unit_spec",
78
+ )
79
+ ),
80
+ "project.cache",
81
+ "project.name",
82
+ # Documents the project's private space; nothing reads it.
83
+ "project.private",
84
+ "project.scratchpad",
85
+ *(
86
+ f"provenance.{key}"
87
+ for key in (
88
+ "coldread",
89
+ "coldread_policy",
90
+ "enabled",
91
+ "human_input",
92
+ "metrics_policy",
93
+ "redaction_markers",
94
+ "redaction_policy",
95
+ "redpen",
96
+ "rendered",
97
+ "rulings",
98
+ "reports",
99
+ "root",
100
+ "store",
101
+ "store_dir",
102
+ )
103
+ ),
104
+ # Per runner: claude, codex or agy.
105
+ "providers.*.account",
106
+ "providers.*.state",
107
+ "providers.agy.sessions",
108
+ "providers.canary_models.*",
109
+ "providers.claude.gateway_env",
110
+ "providers.claude.models",
111
+ "providers.claude.sessions",
112
+ "providers.codex.catalog",
113
+ "providers.isolation_policy",
114
+ *(f"publish.{key}" for key in _PUBLISH),
115
+ # Where a reader's Kindle notes are kept; one place for the whole
116
+ # project, so no publish target overlays it.
117
+ "publish.reader_notes",
118
+ "publish.targets.*.copy",
119
+ "publish.targets.*.needs",
120
+ *(f"publish.targets.*.{key}" for key in _PUBLISH),
121
+ "runs.evidence_roots",
122
+ "runs.reviews",
123
+ "runs.store",
124
+ "sops.catalog",
125
+ "sops.default_reader",
126
+ "sops.instruments",
127
+ "sops.operator_guide",
128
+ "sops.panels",
129
+ "sops.persona_policy",
130
+ "sops.personas",
131
+ "sops.reader_driver",
132
+ *(
133
+ f"sops.reader_presets.*.{key}"
134
+ for key in ("ask", "delivery", "include_matter", "instrument", "wrap_ask")
135
+ ),
136
+ "sops.review_policy",
137
+ "sops.root",
138
+ "sops.roster",
139
+ "sops.skill_prefix",
140
+ "text.classifier",
141
+ "text.modes",
142
+ "text.terminology",
143
+ "text.tells_policy",
144
+ "usage.enabled",
145
+ "usage.store",
146
+ )
147
+
148
+ POLICIES: dict[str, tuple[str, ...]] = {
149
+ "manuscript.chapter_policy": ("metadata_layers",),
150
+ "provenance.coldread_policy": ("cli_model", "doc_dirs", "doc_prefixes", "model"),
151
+ "provenance.metrics_policy": (
152
+ "canonical_dir",
153
+ "essay_paths",
154
+ "output_counts.*",
155
+ "source_of_truth",
156
+ ),
157
+ "provenance.redaction_policy": ("markers",),
158
+ "providers.isolation_policy": ("mount_markers", "project_markers"),
159
+ "publish.layout_policy": ("typst_epilogues.*", "typst_prologues.*"),
160
+ "publish.render_policy": (
161
+ "cover_source",
162
+ "epub_style_headers",
163
+ "folio_sides",
164
+ "noparts_drops",
165
+ "print_drops",
166
+ "proof_name",
167
+ "proof_switch",
168
+ "proof_title",
169
+ "reader_metadata.*",
170
+ "revision_metadata",
171
+ ),
172
+ "publish.variant_policy": (
173
+ "blind_tells",
174
+ "default_effort",
175
+ "default_harness",
176
+ "default_model",
177
+ "document_intro",
178
+ "prompt_intro",
179
+ *(f"kinds.*.{key}" for key in ("effort", "harness", "model", "omit", "shape")),
180
+ ),
181
+ "sops.persona_policy": ("default",),
182
+ "sops.review_policy": (
183
+ "frame",
184
+ "hidden",
185
+ "own_output",
186
+ "procedure_frame",
187
+ "reviewable",
188
+ ),
189
+ "text.terminology": ("allow", "exclude_names", "retired", "scope_glob", "terms.*"),
190
+ "text.tells_policy": ("contrast", "exclude_names", "phrases.*", "scope_glob"),
191
+ }
192
+
193
+
194
+ def _parts(key: str) -> tuple[str, ...]:
195
+ return tuple(key.split(".")) if key else ()
196
+
197
+
198
+ def _matches(pattern: tuple[str, ...], path: tuple[str, ...]) -> bool:
199
+ return len(pattern) == len(path) and all(
200
+ p == "*" or p == q for p, q in zip(pattern, path)
201
+ )
202
+
203
+
204
+ def _leads_to(pattern: tuple[str, ...], path: tuple[str, ...]) -> bool:
205
+ """Whether ``path`` is ``pattern`` or a table on the way to it."""
206
+ return len(path) <= len(pattern) and _matches(pattern[: len(path)], path)
207
+
208
+
209
+ def known(path, patterns) -> bool:
210
+ """Whether ``patterns`` name this key, a table on the way to one, or a
211
+ name below a pattern that ends in ``*``, whose contents are the project's."""
212
+ path = _parts(path) if isinstance(path, str) else tuple(path)
213
+ for pattern in map(_parts, patterns):
214
+ if _leads_to(pattern, path):
215
+ return True
216
+ if pattern[-1:] == ("*",) and _matches(pattern, path[: len(pattern)]):
217
+ return True
218
+ return False
219
+
220
+
221
+ # --------------------------------------------------------------------------- #
222
+ # Recording what the code reads, for Quirework's own suite
223
+ # --------------------------------------------------------------------------- #
224
+
225
+ _seen: set[tuple[str, str]] | None = None
226
+
227
+
228
+ def recording() -> None:
229
+ """Start noting every key read through a project, for `unregistered`."""
230
+ global _seen
231
+ _seen = set()
232
+
233
+
234
+ def unregistered() -> list[str]:
235
+ """The keys read since `recording` that this module does not list."""
236
+ found = []
237
+ for where, key in sorted(_seen or ()):
238
+ patterns = SETTINGS if where == "" else POLICIES.get(where, ())
239
+ if not known(key, patterns):
240
+ found.append(f"{where}: {key}" if where else key)
241
+ return found
242
+
243
+
244
+ def note(where: str, key: str) -> None:
245
+ if _seen is not None:
246
+ _seen.add((where, key))
247
+
248
+
249
+ def watched(value, where: str, prefix: str):
250
+ """``value``, made to note the keys read out of it, while recording."""
251
+ if _seen is None or not isinstance(value, dict):
252
+ return value
253
+ return Watched(value, where, prefix)
254
+
255
+
256
+ class Watched(dict):
257
+ """A table that notes each key the code names when reading it.
258
+
259
+ Only a named read counts: iterating a table reads names the project
260
+ chose, not keys Quirework knows. A table reached inside it, by name or
261
+ by iterating, is watched too, under the longer prefix, and replaces the
262
+ plain one in place, so writing through it is seen by the next read.
263
+ """
264
+
265
+ def __init__(self, value, where, prefix):
266
+ super().__init__(value)
267
+ self._where, self._prefix = where, prefix
268
+
269
+ def _named(self, key):
270
+ note(self._where, f"{self._prefix}.{key}" if self._prefix else str(key))
271
+
272
+ def _inner(self, key):
273
+ value = super().__getitem__(key)
274
+ if isinstance(value, dict) and not isinstance(value, Watched):
275
+ prefix = f"{self._prefix}.{key}" if self._prefix else str(key)
276
+ value = Watched(value, self._where, prefix)
277
+ super().__setitem__(key, value)
278
+ return value
279
+
280
+ def __getitem__(self, key):
281
+ self._named(key)
282
+ return self._inner(key)
283
+
284
+ def get(self, key, default=None):
285
+ self._named(key)
286
+ if super().__contains__(key):
287
+ return self._inner(key)
288
+ # A table the project left out still has its keys read, from the
289
+ # caller's default, and those reads count as much as any.
290
+ prefix = f"{self._prefix}.{key}" if self._prefix else str(key)
291
+ return watched(default, self._where, prefix)
292
+
293
+ def __contains__(self, key):
294
+ self._named(key)
295
+ return super().__contains__(key)
296
+
297
+ def setdefault(self, key, default=None):
298
+ self._named(key)
299
+ if not super().__contains__(key):
300
+ super().__setitem__(key, default)
301
+ return self._inner(key)
302
+
303
+ def pop(self, key, *default):
304
+ self._named(key)
305
+ return super().pop(key, *default)
306
+
307
+ def values(self):
308
+ return [self._inner(key) for key in self]
309
+
310
+ def items(self):
311
+ return [(key, self._inner(key)) for key in self]
312
+
313
+
314
+ # --------------------------------------------------------------------------- #
315
+ # What a project declares, against what Quirework knows
316
+ # --------------------------------------------------------------------------- #
317
+
318
+
319
+ def _declared(table, patterns, prefix=()):
320
+ """Every declared key in ``table``, as a tuple of names.
321
+
322
+ A table is walked into until a key a pattern names exactly, since that
323
+ setting's value may itself be a table, or a name below a pattern ending
324
+ in ``*``, whose value is the project's own."""
325
+ for name, value in table.items():
326
+ path = (*prefix, name)
327
+ whole = any(_matches(_parts(p), path) for p in patterns)
328
+ if isinstance(value, dict) and value and not whole:
329
+ yield from _declared(value, patterns, path)
330
+ else:
331
+ yield path
332
+
333
+
334
+ def _patterns(value, name):
335
+ from .project import ConfigurationError
336
+
337
+ if not isinstance(value, list) or not all(
338
+ isinstance(v, str) and v.strip() for v in value
339
+ ):
340
+ raise ConfigurationError(f"{name} must be a list of dotted keys")
341
+ return tuple(value)
342
+
343
+
344
+ def own(project) -> tuple[tuple[str, ...], dict[str, tuple[str, ...]]]:
345
+ """The keys ``project``'s own code reads, which Quirework's does not.
346
+
347
+ An extension a project installs may read a setting of its own, or a key
348
+ of its own in a policy file it shares with Quirework, and nothing here
349
+ can know either. So the project lists them: `extensions.settings` for
350
+ `quirework.toml`, dotted as Quirework's are, and `extensions.policy_keys`,
351
+ one list per policy setting, written from the top of that file. Only a
352
+ policy file Quirework reads takes a list, since a file nothing here reads
353
+ is never set against Quirework's keys in the first place."""
354
+ from .project import ConfigurationError
355
+
356
+ settings = _patterns(project.get("extensions.settings", []), "extensions.settings")
357
+ table = project.get("extensions.policy_keys", {})
358
+ if not isinstance(table, dict):
359
+ raise ConfigurationError(
360
+ "extensions.policy_keys must be a table: one list per policy setting"
361
+ )
362
+ policies = {}
363
+ for setting, value in table.items():
364
+ if setting not in POLICIES:
365
+ raise ConfigurationError(
366
+ f"extensions.policy_keys names {setting}, which is not a policy "
367
+ "file Quirework reads, so no key of it needs listing"
368
+ )
369
+ policies[setting] = _patterns(value, f"extensions.policy_keys.{setting}")
370
+ return settings, policies
371
+
372
+
373
+ def _compare(table, patterns, mine=()):
374
+ keys = list(_declared(table, (*patterns, *mine)))
375
+
376
+ def present(candidates):
377
+ return sorted(
378
+ p
379
+ for p in candidates
380
+ if any(_matches(_parts(p), k[: len(_parts(p))]) for k in keys)
381
+ )
382
+
383
+ declared = present(patterns)
384
+ return {
385
+ "declared": declared,
386
+ "undeclared": sorted(set(patterns) - set(declared)),
387
+ "own": present(mine),
388
+ "unknown": sorted(
389
+ ".".join(k) for k in keys if not known(k, patterns) and not known(k, mine)
390
+ ),
391
+ }
392
+
393
+
394
+ def report(project) -> dict:
395
+ """Each known setting and policy key, declared by ``project`` or not, the
396
+ project's own keys it declares, and each it declares that nobody knows."""
397
+ own_settings, own_policies = own(project)
398
+ found = {
399
+ "settings": _compare(project.describe()["settings"], SETTINGS, own_settings)
400
+ }
401
+ policies = {}
402
+ for setting, patterns in sorted(POLICIES.items()):
403
+ if project.get(setting) is None:
404
+ policies[setting] = {
405
+ "file": None,
406
+ "declared": [],
407
+ "undeclared": sorted(patterns),
408
+ "own": [],
409
+ "unknown": [],
410
+ }
411
+ continue
412
+ data = project.data(setting, {})
413
+ policies[setting] = {
414
+ "file": str(project.path(setting)),
415
+ **_compare(
416
+ data if isinstance(data, dict) else {},
417
+ patterns,
418
+ own_policies.get(setting, ()),
419
+ ),
420
+ }
421
+ found["policies"] = policies
422
+ return found
423
+
424
+
425
+ def _where(project, path) -> str:
426
+ """A file as the project would name it: relative to its root when inside."""
427
+ path = Path(path)
428
+ root = project.root.resolve()
429
+ resolved = path.resolve()
430
+ return (
431
+ str(resolved.relative_to(root)) if resolved.is_relative_to(root) else str(path)
432
+ )
433
+
434
+
435
+ def unknown(project) -> list[str]:
436
+ """Each declared key nothing reads, as a line naming its file.
437
+
438
+ A misspelled key is read as absent, so it takes its default and says
439
+ nothing, and so does a key a release retired. `config check` refuses a
440
+ project that declares one, in `quirework.toml`, in the local overlay or
441
+ in a policy file the project names, so the misspelling surfaces where
442
+ somebody asked whether the configuration is right. A key the project's
443
+ own code reads is not one of them once `[extensions]` lists it (`own`),
444
+ and a key under `[extensions]` other than its two lists is."""
445
+ found = report(project)
446
+ described = project.describe()
447
+ origins, fallback = described["origins"], described["configuration"]
448
+ lines = [
449
+ f"{_where(project, origins.get(key) or fallback)}: unknown setting {key}"
450
+ for key in found["settings"]["unknown"]
451
+ ]
452
+ for setting, rows in found["policies"].items():
453
+ lines.extend(
454
+ f"{_where(project, rows['file'])}: unknown key {key} in {setting}"
455
+ for key in rows["unknown"]
456
+ )
457
+ return lines
458
+
459
+
460
+ def show(project, *, undeclared_only=False) -> str:
461
+ """`report` for a person: the settings first, then each policy file."""
462
+ found = report(project)
463
+ lines = []
464
+
465
+ def section(rows, indent):
466
+ kinds = (
467
+ ("undeclared", "unknown")
468
+ if undeclared_only
469
+ else (
470
+ "declared",
471
+ "own",
472
+ "undeclared",
473
+ "unknown",
474
+ )
475
+ )
476
+ for kind in kinds:
477
+ for key in rows[kind]:
478
+ label = "unknown" if kind == "unknown" else kind
479
+ lines.append(f"{indent}{label:11} {key}")
480
+
481
+ lines.append("Settings (quirework.toml):")
482
+ section(found["settings"], " ")
483
+ for setting, rows in found["policies"].items():
484
+ where = rows["file"] or "not declared, so every key is undeclared"
485
+ lines.append(f"{setting} ({where}):")
486
+ section(rows, " ")
487
+ lines.append(
488
+ "An own key is one the project's own code reads, as [extensions] lists "
489
+ "it. An unknown key is one nothing reads, usually a misspelling. "
490
+ "docs/configuration.md explains every known one."
491
+ )
492
+ return "\n".join(lines)
@@ -0,0 +1 @@
1
+ """Sops services."""