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
@@ -0,0 +1,169 @@
1
+ """Read part of a procedure: its outline, named sections, or a declared mode.
2
+
3
+ A long procedure is usually read for one of the things it covers, and the
4
+ procedure itself says which sections that needs. Reading it whole on every
5
+ run costs the reader all of it; slicing it by hand (a heading grep, then a
6
+ line range) works but leaves no record that the read happened. These
7
+ helpers do the slicing, so `sop show` can print exactly that part, through
8
+ the command that counts the read.
9
+
10
+ A section is a Markdown heading through the line before the next heading at
11
+ its own level or above, so it carries its subsections. A procedure declares
12
+ what a kind of request needs in a marker, one per mode:
13
+
14
+ <!-- @mode per-edit: Choosing a mode; Reviewing one edit -->
15
+
16
+ The mode's reading is the procedure's opening (everything before its second
17
+ heading, where it states its goal and scope) followed by those sections.
18
+
19
+ Stdlib only, like the rest of the SOP layer, so repository hooks can run it
20
+ without an installed environment.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import re
26
+
27
+ _HEADING = re.compile(r"^(#{1,6})[ \t]+(.*?)(?:[ \t]+#+)?[ \t]*$")
28
+ _FENCE = re.compile(r"^[ \t]{0,3}(`{3,}|~{3,})")
29
+ _ATTRIBUTES = re.compile(r"\s*\{[^{}]*\}\s*$")
30
+ _MODE = re.compile(r"<!--\s*@mode\b(.*?)-->", re.S)
31
+ _MODE_NAME = re.compile(r"[a-z][a-z0-9-]*")
32
+ PATH_SEPARATOR = " > "
33
+
34
+
35
+ def _key(title: str) -> str:
36
+ """How a title is matched: case, spacing, code ticks and emphasis ignored."""
37
+ return " ".join(re.sub(r"[`*]", "", title).split()).casefold()
38
+
39
+
40
+ def _comment_open(line: str, open_: bool) -> bool:
41
+ """Whether an HTML comment is still open after this line."""
42
+ for token in re.findall(r"<!--|-->", line):
43
+ open_ = token == "<!--"
44
+ return open_
45
+
46
+
47
+ def headings(text: str) -> list[dict]:
48
+ """Every heading, in order, with its level, title, path and line range.
49
+
50
+ A `#` line inside a fenced code block or a multi-line HTML comment is not
51
+ a heading; both are where a procedure quotes Markdown it is describing.
52
+ """
53
+ lines = text.splitlines(keepends=True)
54
+ found, fence, comment = [], None, False
55
+ for number, line in enumerate(lines):
56
+ if fence:
57
+ if line.lstrip().startswith(fence):
58
+ fence = None
59
+ continue
60
+ if comment:
61
+ comment = _comment_open(line, comment)
62
+ continue
63
+ opening = _FENCE.match(line)
64
+ if opening:
65
+ fence = opening.group(1)
66
+ continue
67
+ match = _HEADING.match(line)
68
+ if match:
69
+ title = _ATTRIBUTES.sub("", match.group(2)).strip()
70
+ found.append(
71
+ {"level": len(match.group(1)), "title": title, "start": number}
72
+ )
73
+ comment = _comment_open(line, comment)
74
+ stack = []
75
+ for index, item in enumerate(found):
76
+ while stack and stack[-1]["level"] >= item["level"]:
77
+ stack.pop()
78
+ item["path"] = [s["title"] for s in stack] + [item["title"]]
79
+ stack.append(item)
80
+ item["end"] = next(
81
+ (h["start"] for h in found[index + 1 :] if h["level"] <= item["level"]),
82
+ len(lines),
83
+ )
84
+ return found
85
+
86
+
87
+ def outline(text: str) -> str:
88
+ """The heading tree, indented by level, then any declared modes."""
89
+ tree = headings(text)
90
+ top = min((h["level"] for h in tree), default=1)
91
+ rows = [f"{' ' * (h['level'] - top)}{h['title']}" for h in tree]
92
+ declared = modes(text)
93
+ if declared:
94
+ rows += ["", "Modes (sop show NAME --mode MODE):"]
95
+ rows += [f" {name}: {'; '.join(titles)}" for name, titles in declared.items()]
96
+ return "\n".join(rows) + "\n"
97
+
98
+
99
+ def find(text: str, title: str) -> dict:
100
+ """The one heading a title names; `Parent > Child` settles a repeated one."""
101
+ wanted = [_key(part) for part in title.split(PATH_SEPARATOR.strip())]
102
+ tree = headings(text)
103
+ matches = [
104
+ h for h in tree if [_key(t) for t in h["path"][-len(wanted) :]] == wanted
105
+ ]
106
+ if len(matches) == 1:
107
+ return matches[0]
108
+ if matches:
109
+ paths = "; ".join(PATH_SEPARATOR.join(h["path"]) for h in matches)
110
+ raise ValueError(
111
+ f"section {title!r} is ambiguous: {paths}. Name it by its path, "
112
+ f"parents first, joined with {PATH_SEPARATOR.strip()!r}"
113
+ )
114
+ available = ", ".join(repr(h["title"]) for h in tree) or "none"
115
+ raise ValueError(f"no section {title!r}; the headings are {available}")
116
+
117
+
118
+ def _join(text: str, spans) -> str:
119
+ """The lines the spans cover, once each, in document order."""
120
+ lines = text.splitlines(keepends=True)
121
+ covered = sorted({n for start, end in spans for n in range(start, end)})
122
+ return "".join(lines[n] for n in covered)
123
+
124
+
125
+ def select(text: str, titles) -> str:
126
+ """The named sections, verbatim, in document order and never twice."""
127
+ return _join(
128
+ text, [(h["start"], h["end"]) for h in (find(text, t) for t in titles)]
129
+ )
130
+
131
+
132
+ def modes(text: str) -> dict[str, list[str]]:
133
+ """The modes a procedure declares, each with the section titles it needs."""
134
+ declared = {}
135
+ for body in _MODE.findall(text):
136
+ name, colon, titles = body.strip().partition(":")
137
+ name = name.strip()
138
+ titles = [t.strip() for t in " ".join(titles.split()).split(";") if t.strip()]
139
+ if not colon or not _MODE_NAME.fullmatch(name) or not titles:
140
+ raise ValueError(
141
+ f"@mode marker needs `NAME: Section; Section`, got {body.strip()!r}"
142
+ )
143
+ if name in declared:
144
+ raise ValueError(f"@mode {name} is declared twice")
145
+ declared[name] = titles
146
+ return declared
147
+
148
+
149
+ def check_modes(text: str) -> None:
150
+ """Every declared section resolves, so a renamed heading fails a gate."""
151
+ for name, titles in modes(text).items():
152
+ for title in titles:
153
+ try:
154
+ find(text, title)
155
+ except ValueError as exc:
156
+ raise ValueError(f"@mode {name}: {exc}") from None
157
+
158
+
159
+ def mode(text: str, name: str) -> str:
160
+ """The opening and the declared sections of one mode."""
161
+ declared = modes(text)
162
+ if name not in declared:
163
+ offered = ", ".join(declared) or "none"
164
+ raise ValueError(f"no mode {name!r}; this procedure declares {offered}")
165
+ tree = headings(text)
166
+ opening = tree[1]["start"] if len(tree) > 1 else len(text.splitlines())
167
+ spans = [(0, opening)]
168
+ spans += [(h["start"], h["end"]) for h in (find(text, t) for t in declared[name])]
169
+ return _join(text, spans)