claude-dev-env 8.62.0 → 8.63.0
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.
- package/.agents/skills/context-audit/SKILL.md +48 -0
- package/.agents/skills/context-audit/references/depth-modes.md +67 -0
- package/.agents/skills/context-audit/scripts/context_audit.py +109 -0
- package/.agents/skills/context-audit/scripts/context_audit_constants/__init__.py +1 -0
- package/.agents/skills/context-audit/scripts/context_audit_constants/config/__init__.py +1 -0
- package/.agents/skills/context-audit/scripts/context_audit_constants/config/constants.py +179 -0
- package/.agents/skills/context-audit/scripts/context_inventory.py +362 -0
- package/.agents/skills/context-audit/scripts/context_report.py +176 -0
- package/.agents/skills/context-audit/scripts/context_sources.py +252 -0
- package/.agents/skills/context-audit/scripts/test_context_audit.py +217 -0
- package/bin/ever-shipped-skills.mjs +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""Walk one checkout and record a row for each file an agent loads.
|
|
2
|
+
|
|
3
|
+
::
|
|
4
|
+
|
|
5
|
+
audit_checkout(Path("repo"), None, None, ())
|
|
6
|
+
-> [ContextRow(path="CLAUDE.md", kind=instructions, trigger=session-start, ...),
|
|
7
|
+
ContextRow(path=".claude/rules/api.md", kind=rule, trigger=path-match, ...)]
|
|
8
|
+
|
|
9
|
+
Instruction files, rules, skills, saved hook text, linked docs, and module
|
|
10
|
+
docstrings each get rows. Links out of each text file are followed in turn.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import ast
|
|
16
|
+
from collections import deque
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from context_audit_constants.config import constants
|
|
21
|
+
from context_audit_constants.config.constants import CapKey, DepthMode, RowKind, Trigger
|
|
22
|
+
from context_sources import (
|
|
23
|
+
ContextRow,
|
|
24
|
+
RowSource,
|
|
25
|
+
find_import_targets,
|
|
26
|
+
find_relative_links,
|
|
27
|
+
list_tracked_files,
|
|
28
|
+
measure_row,
|
|
29
|
+
parse_frontmatter,
|
|
30
|
+
read_text,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class SkillHome:
|
|
36
|
+
"""Where a skill folder sits and when its description loads."""
|
|
37
|
+
|
|
38
|
+
location: str
|
|
39
|
+
trigger: Trigger
|
|
40
|
+
note: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ContextInventory:
|
|
44
|
+
"""Collect one row per loaded file through ``collect`` and return them."""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self, root: Path, rules_directory: Path | None, skills_directory: Path | None
|
|
48
|
+
) -> None:
|
|
49
|
+
self.root = root.resolve()
|
|
50
|
+
self.rules_directory = rules_directory
|
|
51
|
+
self.skills_directory = skills_directory
|
|
52
|
+
self.all_rows: list[ContextRow] = []
|
|
53
|
+
self.all_seen_keys: set[tuple[str, RowKind]] = set()
|
|
54
|
+
self.link_queue: deque[tuple[Path, str, int]] = deque()
|
|
55
|
+
|
|
56
|
+
def collect(self, all_hook_text_files: tuple[Path, ...]) -> list[ContextRow]:
|
|
57
|
+
"""Return the rows for every loaded file in discovery order.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
all_hook_text_files: Saved text a SessionStart hook prints.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
The inventory rows.
|
|
64
|
+
"""
|
|
65
|
+
all_paths = list_tracked_files(self.root)
|
|
66
|
+
self._add_instructions(all_paths)
|
|
67
|
+
self._add_rules(all_paths)
|
|
68
|
+
self._add_skills(all_paths)
|
|
69
|
+
for each_hook_text_file in all_hook_text_files:
|
|
70
|
+
self._add_hook_text(each_hook_text_file)
|
|
71
|
+
self._follow_links()
|
|
72
|
+
for each_path in all_paths:
|
|
73
|
+
if each_path.suffix == constants.PYTHON_SUFFIX:
|
|
74
|
+
self._add_docstring(each_path, constants.LOADER_DOCSTRING)
|
|
75
|
+
return self.all_rows
|
|
76
|
+
|
|
77
|
+
def _relative(self, path: Path) -> str:
|
|
78
|
+
return path.resolve().relative_to(self.root).as_posix()
|
|
79
|
+
|
|
80
|
+
def _add(
|
|
81
|
+
self,
|
|
82
|
+
path: Path,
|
|
83
|
+
source: RowSource,
|
|
84
|
+
cap_key: CapKey | None,
|
|
85
|
+
text: str,
|
|
86
|
+
link_depth: int = constants.FIRST_LINK_DEPTH,
|
|
87
|
+
) -> ContextRow | None:
|
|
88
|
+
key = (source.path, source.kind)
|
|
89
|
+
if key in self.all_seen_keys:
|
|
90
|
+
return None
|
|
91
|
+
self.all_seen_keys.add(key)
|
|
92
|
+
is_markdown = path.suffix in constants.ALL_TEXT_SUFFIXES
|
|
93
|
+
all_links = find_relative_links(path, text, self.root) if is_markdown else []
|
|
94
|
+
cap = constants.ALL_LINE_CAP_BY_KEY[cap_key] if cap_key else None
|
|
95
|
+
row = measure_row(source, text, cap, len(all_links))
|
|
96
|
+
self.all_rows.append(row)
|
|
97
|
+
for each_link in all_links:
|
|
98
|
+
self.link_queue.append((each_link, row.path, link_depth))
|
|
99
|
+
return row
|
|
100
|
+
|
|
101
|
+
def _source(
|
|
102
|
+
self, path: Path, kind: RowKind, loader: str, trigger: Trigger, note: str = ""
|
|
103
|
+
) -> RowSource:
|
|
104
|
+
return RowSource(self._relative(path), kind, loader, trigger, note)
|
|
105
|
+
|
|
106
|
+
def _add_instructions(self, all_paths: list[Path]) -> None:
|
|
107
|
+
for each_path in sorted(all_paths):
|
|
108
|
+
if each_path.name not in constants.ALL_INSTRUCTION_FILE_NAMES:
|
|
109
|
+
continue
|
|
110
|
+
folder = each_path.parent
|
|
111
|
+
is_root = folder == self.root or (
|
|
112
|
+
folder.name == constants.CLAUDE_FOLDER_NAME
|
|
113
|
+
and folder.parent == self.root
|
|
114
|
+
)
|
|
115
|
+
trigger = Trigger.SESSION_START if is_root else Trigger.FOLDER_ENTER
|
|
116
|
+
cap_key = (
|
|
117
|
+
CapKey.ROOT_INSTRUCTIONS if is_root else CapKey.NESTED_INSTRUCTIONS
|
|
118
|
+
)
|
|
119
|
+
is_agents_file = each_path.name == constants.AGENTS_FILE_NAME
|
|
120
|
+
note = constants.NOTE_CODEX_AGENTS if is_agents_file else ""
|
|
121
|
+
loader = self._instruction_loader(each_path)
|
|
122
|
+
source = self._source(
|
|
123
|
+
each_path, RowKind.INSTRUCTIONS, loader, trigger, note
|
|
124
|
+
)
|
|
125
|
+
text = read_text(each_path)
|
|
126
|
+
self._add(each_path, source, cap_key, text)
|
|
127
|
+
self._add_imports(each_path, text, trigger, cap_key)
|
|
128
|
+
|
|
129
|
+
def _instruction_loader(self, path: Path) -> str:
|
|
130
|
+
if path.name != constants.AGENTS_FILE_NAME:
|
|
131
|
+
return constants.LOADER_HARNESS
|
|
132
|
+
sibling_claude = path.parent / constants.CLAUDE_FILE_NAME
|
|
133
|
+
if not sibling_claude.exists():
|
|
134
|
+
return constants.LOADER_AGENTS_NATIVE
|
|
135
|
+
if constants.AGENTS_IMPORT_LINE in read_text(sibling_claude):
|
|
136
|
+
return constants.LOADER_AGENTS_IMPORTED
|
|
137
|
+
return constants.LOADER_AGENTS_SHADOWED
|
|
138
|
+
|
|
139
|
+
def _add_imports(
|
|
140
|
+
self, importer: Path, text: str, trigger: Trigger, cap_key: CapKey
|
|
141
|
+
) -> None:
|
|
142
|
+
loader = constants.LOADER_IMPORT_TEMPLATE.format(
|
|
143
|
+
source=self._relative(importer)
|
|
144
|
+
)
|
|
145
|
+
for each_import in find_import_targets(importer, text, self.root):
|
|
146
|
+
if each_import.name in constants.ALL_INSTRUCTION_FILE_NAMES:
|
|
147
|
+
continue
|
|
148
|
+
source = self._source(
|
|
149
|
+
each_import, RowKind.IMPORT, loader, trigger, constants.NOTE_IMPORT
|
|
150
|
+
)
|
|
151
|
+
imported_text = read_text(each_import)
|
|
152
|
+
if self._add(each_import, source, cap_key, imported_text) is not None:
|
|
153
|
+
self._add_imports(each_import, imported_text, trigger, cap_key)
|
|
154
|
+
|
|
155
|
+
def _add_rules(self, all_paths: list[Path]) -> None:
|
|
156
|
+
for each_path in all_paths:
|
|
157
|
+
is_installed_rule = (
|
|
158
|
+
self.rules_directory is not None
|
|
159
|
+
and self.rules_directory in each_path.parents
|
|
160
|
+
)
|
|
161
|
+
if each_path.suffix != constants.MARKDOWN_SUFFIX or not (
|
|
162
|
+
is_installed_rule or self._is_project_rule(each_path)
|
|
163
|
+
):
|
|
164
|
+
continue
|
|
165
|
+
text = read_text(each_path)
|
|
166
|
+
is_path_scoped = constants.PATHS_FIELD in parse_frontmatter(text)
|
|
167
|
+
loader = (
|
|
168
|
+
constants.LOADER_RULE_INSTALLED
|
|
169
|
+
if is_installed_rule
|
|
170
|
+
else constants.LOADER_RULE_PROJECT
|
|
171
|
+
)
|
|
172
|
+
is_global = is_installed_rule and not is_path_scoped
|
|
173
|
+
note = constants.NOTE_INSTALLED_RULE if is_global else ""
|
|
174
|
+
trigger = Trigger.PATH_MATCH if is_path_scoped else Trigger.SESSION_START
|
|
175
|
+
source = self._source(each_path, RowKind.RULE, loader, trigger, note)
|
|
176
|
+
self._add(each_path, source, CapKey.RULE, text)
|
|
177
|
+
|
|
178
|
+
def _is_project_rule(self, path: Path) -> bool:
|
|
179
|
+
all_prefix_parts = constants.ALL_PROJECT_RULES_PATH_PARTS
|
|
180
|
+
return (
|
|
181
|
+
path.relative_to(self.root).parts[: len(all_prefix_parts)]
|
|
182
|
+
== all_prefix_parts
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def _skill_home(self, skill_path: Path) -> SkillHome | None:
|
|
186
|
+
parts = skill_path.relative_to(self.root).parts
|
|
187
|
+
if len(parts) < constants.MINIMUM_SKILL_PATH_PARTS or any(
|
|
188
|
+
constants.ARCHIVE_FOLDER_MARKER in each_part
|
|
189
|
+
or each_part in constants.ALL_FIXTURE_FOLDER_NAMES
|
|
190
|
+
for each_part in parts
|
|
191
|
+
):
|
|
192
|
+
return None
|
|
193
|
+
if (
|
|
194
|
+
self.skills_directory is not None
|
|
195
|
+
and self.skills_directory in skill_path.parents
|
|
196
|
+
):
|
|
197
|
+
return SkillHome(
|
|
198
|
+
constants.LOCATION_INSTALLED_SKILLS, Trigger.SESSION_START, ""
|
|
199
|
+
)
|
|
200
|
+
return _folder_skill_home(parts)
|
|
201
|
+
|
|
202
|
+
def _add_skills(self, all_paths: list[Path]) -> None:
|
|
203
|
+
all_resolved_skill_paths: set[Path] = set()
|
|
204
|
+
for each_path in sorted(all_paths):
|
|
205
|
+
if each_path.name != constants.SKILL_FILE_NAME:
|
|
206
|
+
continue
|
|
207
|
+
resolved_path = each_path.resolve()
|
|
208
|
+
home = self._skill_home(each_path)
|
|
209
|
+
if home is None or resolved_path in all_resolved_skill_paths:
|
|
210
|
+
continue
|
|
211
|
+
all_resolved_skill_paths.add(resolved_path)
|
|
212
|
+
text = read_text(each_path)
|
|
213
|
+
self._add_skill_description(each_path, parse_frontmatter(text), home)
|
|
214
|
+
source = self._source(
|
|
215
|
+
each_path,
|
|
216
|
+
RowKind.SKILL_BODY,
|
|
217
|
+
constants.LOADER_SKILL_INVOCATION,
|
|
218
|
+
Trigger.SKILL_INVOKE,
|
|
219
|
+
)
|
|
220
|
+
self._add(each_path, source, CapKey.SKILL_BODY, text)
|
|
221
|
+
self._add_skill_references(each_path)
|
|
222
|
+
|
|
223
|
+
def _add_skill_description(
|
|
224
|
+
self, skill_path: Path, field_by_name: dict[str, str], home: SkillHome
|
|
225
|
+
) -> None:
|
|
226
|
+
disable_text = field_by_name.get(constants.DISABLE_MODEL_INVOCATION_FIELD, "")
|
|
227
|
+
if disable_text.lower() == constants.TRUE_TEXT:
|
|
228
|
+
return
|
|
229
|
+
full_size = _description_size(field_by_name)
|
|
230
|
+
listed_size = min(full_size, constants.DESCRIPTION_LISTING_CHARACTER_LIMIT)
|
|
231
|
+
is_truncated = full_size > constants.DESCRIPTION_LISTING_CHARACTER_LIMIT
|
|
232
|
+
truncation_note = constants.NOTE_DESCRIPTION_TRUNCATED if is_truncated else ""
|
|
233
|
+
is_over_cap = full_size > constants.DESCRIPTION_CHARACTER_CAP
|
|
234
|
+
relative_path = self._relative(skill_path)
|
|
235
|
+
self.all_seen_keys.add((relative_path, RowKind.SKILL_DESCRIPTION))
|
|
236
|
+
self.all_rows.append(
|
|
237
|
+
ContextRow(
|
|
238
|
+
path=relative_path,
|
|
239
|
+
kind=RowKind.SKILL_DESCRIPTION,
|
|
240
|
+
loader=constants.LOADER_SKILL_LISTING_TEMPLATE.format(location=home.location),
|
|
241
|
+
trigger=home.trigger,
|
|
242
|
+
bytes=listed_size,
|
|
243
|
+
lines=constants.DESCRIPTION_LINE_COUNT,
|
|
244
|
+
est_tokens=round(listed_size / constants.BYTES_PER_ESTIMATED_TOKEN),
|
|
245
|
+
links_out=0,
|
|
246
|
+
depth_mode=DepthMode.CARRIES if is_over_cap else DepthMode.POINTS,
|
|
247
|
+
cap=constants.DESCRIPTION_CHARACTER_CAP,
|
|
248
|
+
note=constants.NOTE_SEPARATOR.join(
|
|
249
|
+
each_note for each_note in (home.note, truncation_note) if each_note
|
|
250
|
+
),
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
def _add_skill_references(self, skill_path: Path) -> None:
|
|
255
|
+
loader = constants.LOADER_SKILL_REFERENCE_TEMPLATE.format(
|
|
256
|
+
source=self._relative(skill_path)
|
|
257
|
+
)
|
|
258
|
+
for each_other in sorted(skill_path.parent.rglob("*")):
|
|
259
|
+
if (
|
|
260
|
+
each_other.is_file()
|
|
261
|
+
and each_other.name != constants.SKILL_FILE_NAME
|
|
262
|
+
and each_other.suffix in constants.ALL_TEXT_SUFFIXES
|
|
263
|
+
and not constants.ALL_SKIPPED_PATH_PARTS.intersection(each_other.parts)
|
|
264
|
+
):
|
|
265
|
+
source = self._source(
|
|
266
|
+
each_other, RowKind.SKILL_REFERENCE, loader, Trigger.ON_LINK
|
|
267
|
+
)
|
|
268
|
+
self._add(each_other, source, None, read_text(each_other))
|
|
269
|
+
|
|
270
|
+
def _add_hook_text(self, hook_text_file: Path) -> None:
|
|
271
|
+
source = RowSource(
|
|
272
|
+
hook_text_file.name,
|
|
273
|
+
RowKind.HOOK_TEXT,
|
|
274
|
+
constants.LOADER_HOOK_TEXT,
|
|
275
|
+
Trigger.SESSION_START,
|
|
276
|
+
constants.NOTE_HOOK_TEXT,
|
|
277
|
+
)
|
|
278
|
+
cap = constants.ALL_LINE_CAP_BY_KEY[CapKey.ROOT_INSTRUCTIONS]
|
|
279
|
+
self.all_rows.append(measure_row(source, read_text(hook_text_file), cap, 0))
|
|
280
|
+
|
|
281
|
+
def _follow_links(self) -> None:
|
|
282
|
+
while self.link_queue:
|
|
283
|
+
each_path, linked_from, depth = self.link_queue.popleft()
|
|
284
|
+
if each_path.suffix == constants.PYTHON_SUFFIX:
|
|
285
|
+
loader = constants.LOADER_LINKED_DOCSTRING_TEMPLATE.format(
|
|
286
|
+
source=linked_from
|
|
287
|
+
)
|
|
288
|
+
self._add_docstring(each_path, loader)
|
|
289
|
+
continue
|
|
290
|
+
if each_path.suffix not in constants.ALL_TEXT_SUFFIXES or self._is_recorded(
|
|
291
|
+
each_path
|
|
292
|
+
):
|
|
293
|
+
continue
|
|
294
|
+
source = self._source(
|
|
295
|
+
each_path,
|
|
296
|
+
RowKind.LINKED_DOC,
|
|
297
|
+
constants.LOADER_LINK_TEMPLATE.format(source=linked_from),
|
|
298
|
+
Trigger.ON_LINK,
|
|
299
|
+
constants.NOTE_LINK_DEPTH_TEMPLATE.format(depth=depth),
|
|
300
|
+
)
|
|
301
|
+
self._add(each_path, source, None, read_text(each_path), depth + 1)
|
|
302
|
+
|
|
303
|
+
def _is_recorded(self, path: Path) -> bool:
|
|
304
|
+
relative_path = self._relative(path)
|
|
305
|
+
return any(each_key[0] == relative_path for each_key in self.all_seen_keys)
|
|
306
|
+
|
|
307
|
+
def _add_docstring(self, path: Path, loader: str) -> None:
|
|
308
|
+
try:
|
|
309
|
+
tree = ast.parse(read_text(path))
|
|
310
|
+
except (SyntaxError, ValueError):
|
|
311
|
+
return
|
|
312
|
+
docstring = ast.get_docstring(tree, clean=False)
|
|
313
|
+
if docstring:
|
|
314
|
+
source = self._source(path, RowKind.DOCSTRING, loader, Trigger.FILE_OPEN)
|
|
315
|
+
self._add(path, source, CapKey.DOCSTRING, docstring)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _folder_skill_home(all_parts: tuple[str, ...]) -> SkillHome | None:
|
|
319
|
+
has_skills_folder = constants.SKILLS_FOLDER_NAME in all_parts
|
|
320
|
+
if (
|
|
321
|
+
all_parts[0] in constants.ALL_SKILL_HOME_FOLDER_NAMES
|
|
322
|
+
and all_parts[1] == constants.SKILLS_FOLDER_NAME
|
|
323
|
+
):
|
|
324
|
+
return SkillHome(constants.LOCATION_PROJECT_SKILLS, Trigger.SESSION_START, "")
|
|
325
|
+
if constants.CLAUDE_FOLDER_NAME in all_parts and has_skills_folder:
|
|
326
|
+
return SkillHome(constants.LOCATION_NESTED_SKILLS, Trigger.FOLDER_ENTER, "")
|
|
327
|
+
if all_parts[0] == constants.PLUGINS_FOLDER_NAME and has_skills_folder:
|
|
328
|
+
return SkillHome(
|
|
329
|
+
constants.LOCATION_PLUGIN_SKILLS,
|
|
330
|
+
Trigger.SESSION_START,
|
|
331
|
+
constants.NOTE_PLUGIN_SKILL,
|
|
332
|
+
)
|
|
333
|
+
return None
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _description_size(field_by_name: dict[str, str]) -> int:
|
|
337
|
+
description = constants.DESCRIPTION_JOIN_TEMPLATE.format(
|
|
338
|
+
description=field_by_name.get(constants.DESCRIPTION_FIELD, ""),
|
|
339
|
+
when_to_use=field_by_name.get(constants.WHEN_TO_USE_FIELD, ""),
|
|
340
|
+
)
|
|
341
|
+
return len(description.strip().encode(constants.TEXT_ENCODING))
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def audit_checkout(
|
|
345
|
+
root: Path,
|
|
346
|
+
rules_directory: Path | None,
|
|
347
|
+
skills_directory: Path | None,
|
|
348
|
+
all_hook_text_files: tuple[Path, ...],
|
|
349
|
+
) -> list[ContextRow]:
|
|
350
|
+
"""Return one row for each file an agent loads from the checkout at *root*.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
root: A git checkout.
|
|
354
|
+
rules_directory: A tracked folder installed as the user rules folder.
|
|
355
|
+
skills_directory: A tracked folder installed as the user skills folder.
|
|
356
|
+
all_hook_text_files: Saved text a SessionStart hook prints.
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
The inventory rows in discovery order.
|
|
360
|
+
"""
|
|
361
|
+
inventory = ContextInventory(root, rules_directory, skills_directory)
|
|
362
|
+
return inventory.collect(all_hook_text_files)
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Format inventory rows as a markdown report with a cleanup list.
|
|
2
|
+
|
|
3
|
+
::
|
|
4
|
+
|
|
5
|
+
# Context audit: my-repo
|
|
6
|
+
412 loaded files. Startup loads 96 lines, about 1830 tokens, from 9 files.
|
|
7
|
+
## Load by trigger
|
|
8
|
+
## Carries depth over cap
|
|
9
|
+
## Empty instruction stubs
|
|
10
|
+
## Long skills to split
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from context_audit_constants.config import constants
|
|
16
|
+
from context_audit_constants.config.constants import DepthMode, RowKind, Trigger
|
|
17
|
+
from context_sources import ContextRow
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def format_trigger_table(all_rows: list[ContextRow]) -> list[str]:
|
|
21
|
+
"""Return the load-by-trigger table lines.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
all_rows: Inventory rows.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Markdown table lines, one row per trigger present.
|
|
28
|
+
"""
|
|
29
|
+
all_lines = [
|
|
30
|
+
constants.REPORT_TRIGGER_HEADING,
|
|
31
|
+
"",
|
|
32
|
+
constants.REPORT_TRIGGER_HEADER,
|
|
33
|
+
constants.REPORT_FOUR_COLUMN_RULE,
|
|
34
|
+
]
|
|
35
|
+
for each_trigger in Trigger:
|
|
36
|
+
all_matching = [
|
|
37
|
+
each_row for each_row in all_rows if each_row.trigger == each_trigger
|
|
38
|
+
]
|
|
39
|
+
if all_matching:
|
|
40
|
+
all_lines.append(
|
|
41
|
+
constants.REPORT_TRIGGER_ROW_TEMPLATE.format(
|
|
42
|
+
trigger=each_trigger.value,
|
|
43
|
+
files=len(all_matching),
|
|
44
|
+
lines=sum(each_row.lines for each_row in all_matching),
|
|
45
|
+
tokens=sum(each_row.est_tokens for each_row in all_matching),
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
return all_lines
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def format_carries_section(all_carries: list[ContextRow]) -> list[str]:
|
|
52
|
+
"""Return the section that lists files over their cap.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
all_carries: Rows in carries depth mode, skill bodies excluded.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Markdown lines for the section.
|
|
59
|
+
"""
|
|
60
|
+
all_lines = ["", constants.REPORT_CARRIES_HEADING, ""]
|
|
61
|
+
if not all_carries:
|
|
62
|
+
return [*all_lines, constants.REPORT_NONE_LINE]
|
|
63
|
+
all_lines += [constants.REPORT_CARRIES_HEADER, constants.REPORT_SIX_COLUMN_RULE]
|
|
64
|
+
return all_lines + [
|
|
65
|
+
constants.REPORT_CARRIES_ROW_TEMPLATE.format(
|
|
66
|
+
path=each_row.path,
|
|
67
|
+
kind=each_row.kind.value,
|
|
68
|
+
trigger=each_row.trigger.value,
|
|
69
|
+
lines=each_row.lines,
|
|
70
|
+
cap=each_row.cap,
|
|
71
|
+
tokens=each_row.est_tokens,
|
|
72
|
+
)
|
|
73
|
+
for each_row in all_carries
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def format_stub_section(all_stubs: list[ContextRow]) -> list[str]:
|
|
78
|
+
"""Return the section that lists empty instruction files.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
all_stubs: Empty instruction, import, and rule rows.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Markdown lines for the section.
|
|
85
|
+
"""
|
|
86
|
+
all_lines = ["", constants.REPORT_EMPTY_HEADING, ""]
|
|
87
|
+
all_stub_lines = [
|
|
88
|
+
constants.REPORT_EMPTY_ROW_TEMPLATE.format(
|
|
89
|
+
path=each_row.path, kind=each_row.kind.value
|
|
90
|
+
)
|
|
91
|
+
for each_row in all_stubs
|
|
92
|
+
]
|
|
93
|
+
return all_lines + (all_stub_lines or [constants.REPORT_NONE_LINE])
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def format_split_section(all_long_skills: list[ContextRow]) -> list[str]:
|
|
97
|
+
"""Return the section that lists skill bodies over their cap.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
all_long_skills: Skill body rows in carries depth mode.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Markdown lines for the section.
|
|
104
|
+
"""
|
|
105
|
+
all_lines = ["", constants.REPORT_SPLIT_HEADING, ""]
|
|
106
|
+
if not all_long_skills:
|
|
107
|
+
return [*all_lines, constants.REPORT_NONE_LINE]
|
|
108
|
+
all_lines += [constants.REPORT_SPLIT_HEADER, constants.REPORT_FOUR_COLUMN_RULE]
|
|
109
|
+
return all_lines + [
|
|
110
|
+
constants.REPORT_SPLIT_ROW_TEMPLATE.format(
|
|
111
|
+
path=each_row.path,
|
|
112
|
+
lines=each_row.lines,
|
|
113
|
+
cap=each_row.cap,
|
|
114
|
+
tokens=each_row.est_tokens,
|
|
115
|
+
)
|
|
116
|
+
for each_row in all_long_skills
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def format_cleanup_list(all_rows: list[ContextRow]) -> list[str]:
|
|
121
|
+
"""Return the over-cap, empty-stub, and long-skill sections, largest first.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
all_rows: Inventory rows.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Markdown lines for the three sections.
|
|
128
|
+
"""
|
|
129
|
+
all_over_cap = sorted(
|
|
130
|
+
(each_row for each_row in all_rows if each_row.depth_mode == DepthMode.CARRIES),
|
|
131
|
+
key=lambda each_row: each_row.est_tokens,
|
|
132
|
+
reverse=True,
|
|
133
|
+
)
|
|
134
|
+
all_long_skills = [
|
|
135
|
+
each_row for each_row in all_over_cap if each_row.kind == RowKind.SKILL_BODY
|
|
136
|
+
]
|
|
137
|
+
all_carries = [
|
|
138
|
+
each_row for each_row in all_over_cap if each_row.kind != RowKind.SKILL_BODY
|
|
139
|
+
]
|
|
140
|
+
all_stubs = [
|
|
141
|
+
each_row
|
|
142
|
+
for each_row in all_rows
|
|
143
|
+
if each_row.depth_mode == DepthMode.EMPTY and each_row.kind in constants.ALL_STUB_KINDS
|
|
144
|
+
]
|
|
145
|
+
all_carries_lines = format_carries_section(all_carries)
|
|
146
|
+
return all_carries_lines + format_stub_section(all_stubs) + format_split_section(all_long_skills)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def format_report(checkout_name: str, all_rows: list[ContextRow]) -> str:
|
|
150
|
+
"""Return the markdown report: summary, load by trigger, and cleanup list.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
checkout_name: The name shown in the title.
|
|
154
|
+
all_rows: Inventory rows.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
The report text.
|
|
158
|
+
"""
|
|
159
|
+
all_session_start = [
|
|
160
|
+
each_row for each_row in all_rows if each_row.trigger == Trigger.SESSION_START
|
|
161
|
+
]
|
|
162
|
+
summary = constants.REPORT_SUMMARY_TEMPLATE.format(
|
|
163
|
+
row_count=len(all_rows),
|
|
164
|
+
lines=sum(each_row.lines for each_row in all_session_start),
|
|
165
|
+
tokens=sum(each_row.est_tokens for each_row in all_session_start),
|
|
166
|
+
files=len(all_session_start),
|
|
167
|
+
)
|
|
168
|
+
all_lines = [
|
|
169
|
+
constants.REPORT_TITLE_TEMPLATE.format(name=checkout_name),
|
|
170
|
+
"",
|
|
171
|
+
summary,
|
|
172
|
+
"",
|
|
173
|
+
]
|
|
174
|
+
all_lines += format_trigger_table(all_rows)
|
|
175
|
+
all_lines += format_cleanup_list(all_rows)
|
|
176
|
+
return constants.LINE_BREAK.join(all_lines) + constants.LINE_BREAK
|