@laitszkin/apollo-toolkit 3.13.2 → 3.14.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.
Files changed (154) hide show
  1. package/AGENTS.md +7 -7
  2. package/CHANGELOG.md +27 -0
  3. package/CLAUDE.md +8 -8
  4. package/analyse-app-logs/SKILL.md +3 -3
  5. package/bin/apollo-toolkit.ts +7 -0
  6. package/codex/codex-memory-manager/SKILL.md +2 -2
  7. package/codex/learn-skill-from-conversations/SKILL.md +3 -3
  8. package/dist/bin/apollo-toolkit.d.ts +2 -0
  9. package/dist/bin/apollo-toolkit.js +7 -0
  10. package/dist/lib/cli.d.ts +41 -0
  11. package/dist/lib/cli.js +655 -0
  12. package/dist/lib/installer.d.ts +59 -0
  13. package/dist/lib/installer.js +404 -0
  14. package/dist/lib/tool-runner.d.ts +19 -0
  15. package/dist/lib/tool-runner.js +536 -0
  16. package/dist/lib/tools/architecture.d.ts +2 -0
  17. package/dist/lib/tools/architecture.js +34 -0
  18. package/dist/lib/tools/create-specs.d.ts +2 -0
  19. package/dist/lib/tools/create-specs.js +175 -0
  20. package/dist/lib/tools/docs-to-voice.d.ts +2 -0
  21. package/dist/lib/tools/docs-to-voice.js +705 -0
  22. package/dist/lib/tools/enforce-video-aspect-ratio.d.ts +2 -0
  23. package/dist/lib/tools/enforce-video-aspect-ratio.js +312 -0
  24. package/dist/lib/tools/extract-conversations.d.ts +2 -0
  25. package/dist/lib/tools/extract-conversations.js +105 -0
  26. package/dist/lib/tools/extract-pdf-text.d.ts +2 -0
  27. package/dist/lib/tools/extract-pdf-text.js +92 -0
  28. package/dist/lib/tools/filter-logs.d.ts +2 -0
  29. package/dist/lib/tools/filter-logs.js +94 -0
  30. package/dist/lib/tools/find-github-issues.d.ts +2 -0
  31. package/dist/lib/tools/find-github-issues.js +176 -0
  32. package/dist/lib/tools/generate-storyboard-images.d.ts +2 -0
  33. package/dist/lib/tools/generate-storyboard-images.js +419 -0
  34. package/dist/lib/tools/log-cli-utils.d.ts +35 -0
  35. package/dist/lib/tools/log-cli-utils.js +233 -0
  36. package/dist/lib/tools/open-github-issue.d.ts +2 -0
  37. package/dist/lib/tools/open-github-issue.js +750 -0
  38. package/dist/lib/tools/read-github-issue.d.ts +2 -0
  39. package/dist/lib/tools/read-github-issue.js +134 -0
  40. package/dist/lib/tools/render-error-book.d.ts +2 -0
  41. package/dist/lib/tools/render-error-book.js +265 -0
  42. package/dist/lib/tools/render-katex.d.ts +2 -0
  43. package/dist/lib/tools/render-katex.js +294 -0
  44. package/dist/lib/tools/review-threads.d.ts +2 -0
  45. package/dist/lib/tools/review-threads.js +491 -0
  46. package/dist/lib/tools/search-logs.d.ts +2 -0
  47. package/dist/lib/tools/search-logs.js +164 -0
  48. package/dist/lib/tools/sync-memory-index.d.ts +2 -0
  49. package/dist/lib/tools/sync-memory-index.js +113 -0
  50. package/dist/lib/tools/validate-openai-agent-config.d.ts +2 -0
  51. package/dist/lib/tools/validate-openai-agent-config.js +184 -0
  52. package/dist/lib/tools/validate-skill-frontmatter.d.ts +2 -0
  53. package/dist/lib/tools/validate-skill-frontmatter.js +118 -0
  54. package/dist/lib/types.d.ts +82 -0
  55. package/dist/lib/types.js +2 -0
  56. package/dist/lib/updater.d.ts +34 -0
  57. package/dist/lib/updater.js +112 -0
  58. package/dist/lib/utils/format.d.ts +2 -0
  59. package/dist/lib/utils/format.js +6 -0
  60. package/dist/lib/utils/terminal.d.ts +12 -0
  61. package/dist/lib/utils/terminal.js +26 -0
  62. package/docs-to-voice/SKILL.md +0 -1
  63. package/generate-spec/SKILL.md +1 -1
  64. package/katex/SKILL.md +1 -2
  65. package/lib/cli.ts +780 -0
  66. package/lib/installer.ts +466 -0
  67. package/lib/tool-runner.ts +561 -0
  68. package/lib/tools/architecture.ts +34 -0
  69. package/lib/tools/create-specs.ts +204 -0
  70. package/lib/tools/docs-to-voice.ts +799 -0
  71. package/lib/tools/enforce-video-aspect-ratio.ts +368 -0
  72. package/lib/tools/extract-conversations.ts +114 -0
  73. package/lib/tools/extract-pdf-text.ts +99 -0
  74. package/lib/tools/filter-logs.ts +118 -0
  75. package/lib/tools/find-github-issues.ts +211 -0
  76. package/lib/tools/generate-storyboard-images.ts +455 -0
  77. package/lib/tools/log-cli-utils.ts +262 -0
  78. package/lib/tools/open-github-issue.ts +930 -0
  79. package/lib/tools/read-github-issue.ts +179 -0
  80. package/lib/tools/render-error-book.ts +300 -0
  81. package/lib/tools/render-katex.ts +325 -0
  82. package/lib/tools/review-threads.ts +590 -0
  83. package/lib/tools/search-logs.ts +200 -0
  84. package/lib/tools/sync-memory-index.ts +114 -0
  85. package/lib/tools/validate-openai-agent-config.ts +209 -0
  86. package/lib/tools/validate-skill-frontmatter.ts +124 -0
  87. package/lib/types.ts +90 -0
  88. package/lib/updater.ts +165 -0
  89. package/lib/utils/format.ts +7 -0
  90. package/lib/utils/terminal.ts +22 -0
  91. package/open-github-issue/SKILL.md +2 -2
  92. package/optimise-skill/SKILL.md +1 -1
  93. package/package.json +13 -4
  94. package/resources/project-architecture/assets/architecture.css +764 -0
  95. package/resources/project-architecture/assets/viewer.client.js +144 -0
  96. package/resources/project-architecture/index.html +42 -0
  97. package/review-spec-related-changes/SKILL.md +1 -1
  98. package/solve-issues-found-during-review/SKILL.md +2 -1
  99. package/tsconfig.json +28 -0
  100. package/analyse-app-logs/scripts/__pycache__/filter_logs_by_time.cpython-312.pyc +0 -0
  101. package/analyse-app-logs/scripts/__pycache__/log_cli_utils.cpython-312.pyc +0 -0
  102. package/analyse-app-logs/scripts/__pycache__/search_logs.cpython-312.pyc +0 -0
  103. package/analyse-app-logs/scripts/filter_logs_by_time.py +0 -64
  104. package/analyse-app-logs/scripts/log_cli_utils.py +0 -112
  105. package/analyse-app-logs/scripts/search_logs.py +0 -137
  106. package/analyse-app-logs/tests/test_filter_logs_by_time.py +0 -95
  107. package/analyse-app-logs/tests/test_search_logs.py +0 -100
  108. package/codex/codex-memory-manager/scripts/extract_recent_conversations.py +0 -369
  109. package/codex/codex-memory-manager/scripts/sync_memory_index.py +0 -130
  110. package/codex/codex-memory-manager/tests/test_extract_recent_conversations.py +0 -177
  111. package/codex/codex-memory-manager/tests/test_memory_template.py +0 -37
  112. package/codex/codex-memory-manager/tests/test_sync_memory_index.py +0 -84
  113. package/codex/learn-skill-from-conversations/scripts/extract_recent_conversations.py +0 -369
  114. package/codex/learn-skill-from-conversations/tests/test_extract_recent_conversations.py +0 -177
  115. package/docs-to-voice/scripts/__pycache__/docs_to_voice.cpython-312.pyc +0 -0
  116. package/docs-to-voice/scripts/docs_to_voice.py +0 -1385
  117. package/docs-to-voice/scripts/docs_to_voice.sh +0 -11
  118. package/docs-to-voice/tests/test_docs_to_voice_api_max_chars.py +0 -210
  119. package/docs-to-voice/tests/test_docs_to_voice_sentence_timeline.py +0 -115
  120. package/docs-to-voice/tests/test_docs_to_voice_settings.py +0 -43
  121. package/docs-to-voice/tests/test_docs_to_voice_shell_wrapper.py +0 -51
  122. package/docs-to-voice/tests/test_docs_to_voice_speech_rate.py +0 -57
  123. package/generate-spec/scripts/__pycache__/create-specscpython-312.pyc +0 -0
  124. package/generate-spec/scripts/create-specs +0 -215
  125. package/generate-spec/tests/test_create_specs.py +0 -200
  126. package/init-project-html/scripts/architecture-bootstrap-render.js +0 -16
  127. package/init-project-html/scripts/architecture.js +0 -296
  128. package/katex/scripts/__pycache__/render_katex.cpython-312.pyc +0 -0
  129. package/katex/scripts/render_katex.py +0 -247
  130. package/katex/scripts/render_katex.sh +0 -11
  131. package/katex/tests/test_render_katex.py +0 -174
  132. package/learning-error-book/scripts/render_error_book_json_to_pdf.py +0 -590
  133. package/learning-error-book/tests/test_render_error_book_json_to_pdf.py +0 -134
  134. package/open-github-issue/scripts/__pycache__/open_github_issue.cpython-312.pyc +0 -0
  135. package/open-github-issue/scripts/open_github_issue.py +0 -705
  136. package/open-github-issue/tests/test_open_github_issue.py +0 -381
  137. package/openai-text-to-image-storyboard/scripts/generate_storyboard_images.py +0 -763
  138. package/openai-text-to-image-storyboard/tests/test_generate_storyboard_images.py +0 -177
  139. package/read-github-issue/scripts/__pycache__/find_issues.cpython-312.pyc +0 -0
  140. package/read-github-issue/scripts/__pycache__/read_issue.cpython-312.pyc +0 -0
  141. package/read-github-issue/scripts/find_issues.py +0 -148
  142. package/read-github-issue/scripts/read_issue.py +0 -108
  143. package/read-github-issue/tests/test_find_issues.py +0 -127
  144. package/read-github-issue/tests/test_read_issue.py +0 -109
  145. package/resolve-review-comments/scripts/__pycache__/review_threads.cpython-312.pyc +0 -0
  146. package/resolve-review-comments/scripts/review_threads.py +0 -425
  147. package/resolve-review-comments/tests/test_review_threads.py +0 -74
  148. package/scripts/validate_openai_agent_config.py +0 -209
  149. package/scripts/validate_skill_frontmatter.py +0 -131
  150. package/text-to-short-video/scripts/__pycache__/enforce_video_aspect_ratio.cpython-312.pyc +0 -0
  151. package/text-to-short-video/scripts/enforce_video_aspect_ratio.py +0 -350
  152. package/text-to-short-video/tests/test_enforce_video_aspect_ratio.py +0 -194
  153. package/weekly-financial-event-report/scripts/extract_pdf_text_pdfkit.swift +0 -99
  154. package/weekly-financial-event-report/tests/test_extract_pdf_text_pdfkit.py +0 -64
@@ -1,590 +0,0 @@
1
- #!/usr/bin/env python3
2
- from __future__ import annotations
3
-
4
- import argparse
5
- import json
6
- import os
7
- from typing import Any, List, Optional, Sequence, Tuple
8
- from xml.sax.saxutils import escape
9
-
10
- from reportlab.lib import colors
11
- from reportlab.lib.pagesizes import A4, letter
12
- from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
13
- from reportlab.lib.units import mm
14
- from reportlab.pdfbase import pdfmetrics
15
- from reportlab.pdfbase.ttfonts import TTFont
16
- from reportlab.platypus import (
17
- HRFlowable,
18
- PageBreak,
19
- Paragraph,
20
- SimpleDocTemplate,
21
- Spacer,
22
- Table,
23
- TableStyle,
24
- )
25
-
26
-
27
- THEME = {
28
- "ink": colors.HexColor("#1F2937"),
29
- "muted": colors.HexColor("#6B7280"),
30
- "line": colors.HexColor("#D1D5DB"),
31
- "panel": colors.HexColor("#F8FAFC"),
32
- "panel_alt": colors.HexColor("#EEF2FF"),
33
- "accent": colors.HexColor("#0F766E"),
34
- "warning": colors.HexColor("#9A3412"),
35
- "warning_soft": colors.HexColor("#FFEDD5"),
36
- }
37
-
38
-
39
- def _read_json(path: str) -> Any:
40
- with open(path, "r", encoding="utf-8") as f:
41
- return json.load(f)
42
-
43
-
44
- def _ensure_parent_dir(path: str) -> None:
45
- parent = os.path.dirname(os.path.abspath(path))
46
- if parent:
47
- os.makedirs(parent, exist_ok=True)
48
-
49
-
50
- def _detect_cjk_font_path(user_font_path: Optional[str]) -> Tuple[str, int]:
51
- if user_font_path:
52
- return user_font_path, 0
53
-
54
- candidates = [
55
- "/System/Library/Fonts/PingFang.ttc",
56
- "/System/Library/Fonts/STHeiti Light.ttc",
57
- "/System/Library/Fonts/STHeiti Medium.ttc",
58
- "/System/Library/Fonts/ヒラギノ角ゴシック W3.ttc",
59
- "/System/Library/Fonts/ヒラギノ角ゴシック W4.ttc",
60
- "/System/Library/Fonts/ヒラギノ角ゴシック W5.ttc",
61
- "/System/Library/Fonts/ヒラギノ明朝 ProN.ttc",
62
- "/Library/Fonts/Arial Unicode.ttf",
63
- ]
64
- for path in candidates:
65
- if os.path.exists(path):
66
- return path, 0
67
- raise SystemExit("No CJK font found. Re-run with --font-path pointing to a .ttf/.otf/.ttc font file.")
68
-
69
-
70
- def _register_font(font_path: str, subfont_index: int) -> str:
71
- font_name = "ErrorBookBodyFont"
72
- if font_name not in pdfmetrics.getRegisteredFontNames():
73
- pdfmetrics.registerFont(TTFont(font_name, font_path, subfontIndex=subfont_index))
74
- return font_name
75
-
76
-
77
- def _safe_text(value: Any, default: str = "-") -> str:
78
- if value is None:
79
- return default
80
- if isinstance(value, str):
81
- stripped = value.strip()
82
- return stripped if stripped else default
83
- if isinstance(value, (int, float)):
84
- return str(value)
85
- if isinstance(value, list):
86
- parts = [_safe_text(item, default="") for item in value]
87
- joined = ", ".join(part for part in parts if part)
88
- return joined if joined else default
89
- return str(value)
90
-
91
-
92
- def _markup(value: Any, default: str = "-") -> str:
93
- return escape(_safe_text(value, default=default)).replace("\n", "<br/>")
94
-
95
-
96
- def _paragraph(text: Any, style: ParagraphStyle, default: str = "-") -> Paragraph:
97
- return Paragraph(_markup(text, default=default), style)
98
-
99
-
100
- def _bullet_lines(items: Sequence[Any], styles: dict, empty_text: str = "-") -> List[Any]:
101
- values = list(items or [])
102
- if not values:
103
- return [_paragraph(empty_text, styles["body"])]
104
- return [Paragraph(f"- {_markup(item)}", styles["bullet"]) for item in values]
105
-
106
-
107
- def _bullet_paragraph(items: Sequence[Any], style: ParagraphStyle, empty_text: str = "-") -> Paragraph:
108
- values = list(items or [])
109
- if not values:
110
- return Paragraph(_markup(empty_text), style)
111
- lines = "<br/>".join(f"- {_markup(item)}" for item in values)
112
- return Paragraph(lines, style)
113
-
114
-
115
- def _table(data: List[List[Any]], col_widths: Sequence[float], table_style: TableStyle) -> Table:
116
- table = Table(data, colWidths=list(col_widths), repeatRows=1)
117
- table.setStyle(table_style)
118
- return table
119
-
120
-
121
- def _build_styles(font_name: str, font_size: int) -> dict:
122
- styles = getSampleStyleSheet()
123
- return {
124
- "title": ParagraphStyle(
125
- "ErrorBookTitle",
126
- parent=styles["Title"],
127
- fontName=font_name,
128
- fontSize=24,
129
- leading=30,
130
- textColor=THEME["ink"],
131
- spaceAfter=10,
132
- ),
133
- "subtitle": ParagraphStyle(
134
- "ErrorBookSubtitle",
135
- parent=styles["BodyText"],
136
- fontName=font_name,
137
- fontSize=11,
138
- leading=15,
139
- textColor=THEME["muted"],
140
- spaceAfter=8,
141
- ),
142
- "section": ParagraphStyle(
143
- "ErrorBookSection",
144
- parent=styles["Heading1"],
145
- fontName=font_name,
146
- fontSize=16,
147
- leading=20,
148
- textColor=THEME["accent"],
149
- spaceBefore=8,
150
- spaceAfter=6,
151
- ),
152
- "question": ParagraphStyle(
153
- "ErrorBookQuestion",
154
- parent=styles["Heading2"],
155
- fontName=font_name,
156
- fontSize=14,
157
- leading=18,
158
- textColor=THEME["ink"],
159
- spaceAfter=4,
160
- ),
161
- "subhead": ParagraphStyle(
162
- "ErrorBookSubhead",
163
- parent=styles["Heading3"],
164
- fontName=font_name,
165
- fontSize=11,
166
- leading=14,
167
- textColor=THEME["ink"],
168
- spaceBefore=4,
169
- spaceAfter=2,
170
- ),
171
- "body": ParagraphStyle(
172
- "ErrorBookBody",
173
- parent=styles["BodyText"],
174
- fontName=font_name,
175
- fontSize=font_size,
176
- leading=int(font_size * 1.55),
177
- textColor=THEME["ink"],
178
- spaceAfter=4,
179
- ),
180
- "bullet": ParagraphStyle(
181
- "ErrorBookBullet",
182
- parent=styles["BodyText"],
183
- fontName=font_name,
184
- fontSize=font_size,
185
- leading=int(font_size * 1.5),
186
- textColor=THEME["ink"],
187
- leftIndent=10,
188
- spaceAfter=3,
189
- ),
190
- "meta": ParagraphStyle(
191
- "ErrorBookMeta",
192
- parent=styles["BodyText"],
193
- fontName=font_name,
194
- fontSize=10,
195
- leading=13,
196
- textColor=THEME["muted"],
197
- ),
198
- "label": ParagraphStyle(
199
- "ErrorBookLabel",
200
- parent=styles["BodyText"],
201
- fontName=font_name,
202
- fontSize=10,
203
- leading=13,
204
- textColor=THEME["accent"],
205
- ),
206
- "table": ParagraphStyle(
207
- "ErrorBookTable",
208
- parent=styles["BodyText"],
209
- fontName=font_name,
210
- fontSize=9,
211
- leading=12,
212
- textColor=THEME["ink"],
213
- ),
214
- "table_head": ParagraphStyle(
215
- "ErrorBookTableHead",
216
- parent=styles["BodyText"],
217
- fontName=font_name,
218
- fontSize=9,
219
- leading=12,
220
- textColor=colors.white,
221
- ),
222
- "callout": ParagraphStyle(
223
- "ErrorBookCallout",
224
- parent=styles["BodyText"],
225
- fontName=font_name,
226
- fontSize=10,
227
- leading=14,
228
- textColor=THEME["warning"],
229
- ),
230
- }
231
-
232
-
233
- def _header_footer(canvas, doc) -> None:
234
- canvas.saveState()
235
- canvas.setFont("Helvetica", 9)
236
- canvas.setFillColor(THEME["muted"])
237
- canvas.drawString(doc.leftMargin, doc.height + doc.topMargin + 8, doc.title)
238
- canvas.drawRightString(doc.pagesize[0] - doc.rightMargin, 12, f"Page {canvas.getPageNumber()}")
239
- canvas.restoreState()
240
-
241
-
242
- def _section_heading(text: str, styles: dict) -> List[Any]:
243
- return [Spacer(1, 6), Paragraph(_markup(text), styles["section"]), HRFlowable(color=THEME["line"], thickness=1, width="100%"), Spacer(1, 6)]
244
-
245
-
246
- def _coverage_story(data: dict, styles: dict, width: float) -> List[Any]:
247
- rows: List[List[Any]] = [[
248
- Paragraph("Source", styles["table_head"]),
249
- Paragraph("Questions", styles["table_head"]),
250
- Paragraph("Notes", styles["table_head"]),
251
- ]]
252
- for item in data.get("coverage_scope", []):
253
- rows.append([
254
- _paragraph(item.get("source_path"), styles["table"]),
255
- _paragraph(item.get("included_questions", []), styles["table"]),
256
- _paragraph(item.get("notes"), styles["table"]),
257
- ])
258
- if len(rows) == 1:
259
- rows.append([_paragraph("-", styles["table"]), _paragraph("-", styles["table"]), _paragraph("-", styles["table"])])
260
-
261
- style = TableStyle([
262
- ("BACKGROUND", (0, 0), (-1, 0), THEME["accent"]),
263
- ("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
264
- ("FONTNAME", (0, 0), (-1, -1), styles["table"].fontName),
265
- ("VALIGN", (0, 0), (-1, -1), "TOP"),
266
- ("GRID", (0, 0), (-1, -1), 0.6, THEME["line"]),
267
- ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, THEME["panel"]]),
268
- ("LEFTPADDING", (0, 0), (-1, -1), 8),
269
- ("RIGHTPADDING", (0, 0), (-1, -1), 8),
270
- ("TOPPADDING", (0, 0), (-1, -1), 6),
271
- ("BOTTOMPADDING", (0, 0), (-1, -1), 6),
272
- ])
273
- return _section_heading("Coverage Scope", styles) + [_table(rows, [width * 0.36, width * 0.24, width * 0.40], style)]
274
-
275
-
276
- def _overview_story(data: dict, styles: dict) -> List[Any]:
277
- story = _section_heading("Common Mistake Types Overview", styles)
278
- entries = data.get("mistake_overview", []) or []
279
- if not entries:
280
- return story + [_paragraph("No mistake overview provided.", styles["body"])]
281
-
282
- for entry in entries:
283
- box = [
284
- Paragraph(_markup(entry.get("type")), styles["subhead"]),
285
- _paragraph(entry.get("summary"), styles["body"]),
286
- _paragraph(f"Representative questions: {_safe_text(entry.get('representative_questions', []))}", styles["meta"]),
287
- ]
288
- story.append(
289
- Table(
290
- [[box]],
291
- colWidths=[None],
292
- style=TableStyle([
293
- ("BACKGROUND", (0, 0), (-1, -1), THEME["panel"]),
294
- ("BOX", (0, 0), (-1, -1), 0.8, THEME["line"]),
295
- ("LEFTPADDING", (0, 0), (-1, -1), 10),
296
- ("RIGHTPADDING", (0, 0), (-1, -1), 10),
297
- ("TOPPADDING", (0, 0), (-1, -1), 8),
298
- ("BOTTOMPADDING", (0, 0), (-1, -1), 8),
299
- ]),
300
- )
301
- )
302
- story.append(Spacer(1, 6))
303
- return story
304
-
305
-
306
- def _concept_story(data: dict, styles: dict, width: float) -> List[Any]:
307
- story = _section_heading("Conceptual Mistake Highlights", styles)
308
- concepts = data.get("concept_highlights", []) or []
309
- if not concepts:
310
- return story + [_paragraph("No concept highlights provided.", styles["body"])]
311
-
312
- for concept in concepts:
313
- rows = [
314
- [_paragraph("Definition", styles["label"]), _paragraph(concept.get("definition"), styles["body"])],
315
- [_paragraph("Common misjudgment", styles["label"]), _paragraph(concept.get("common_misjudgment"), styles["body"])],
316
- [_paragraph("Checklist", styles["label"]), _bullet_paragraph(concept.get("checklist", []), styles["body"])],
317
- ]
318
- story.append(Paragraph(_markup(concept.get("name"), default="Unnamed concept"), styles["question"]))
319
- story.append(
320
- Table(
321
- rows,
322
- colWidths=[width * 0.22, width * 0.78],
323
- style=TableStyle([
324
- ("BACKGROUND", (0, 0), (0, -1), THEME["panel_alt"]),
325
- ("BACKGROUND", (1, 0), (1, -1), colors.white),
326
- ("BOX", (0, 0), (-1, -1), 0.8, THEME["line"]),
327
- ("INNERGRID", (0, 0), (-1, -1), 0.6, THEME["line"]),
328
- ("VALIGN", (0, 0), (-1, -1), "TOP"),
329
- ("LEFTPADDING", (0, 0), (-1, -1), 8),
330
- ("RIGHTPADDING", (0, 0), (-1, -1), 8),
331
- ("TOPPADDING", (0, 0), (-1, -1), 6),
332
- ("BOTTOMPADDING", (0, 0), (-1, -1), 6),
333
- ]),
334
- )
335
- )
336
- story.append(Spacer(1, 8))
337
- return story
338
-
339
-
340
- def _question_meta(question: dict, styles: dict, width: float) -> Table:
341
- rows = [
342
- [_paragraph("Source", styles["label"]), _paragraph(question.get("source_path"), styles["body"])],
343
- [_paragraph("Locator", styles["label"]), _paragraph(question.get("page_or_locator"), styles["body"])],
344
- [_paragraph("User answer", styles["label"]), _paragraph(question.get("user_answer"), styles["body"])],
345
- [_paragraph("Correct answer", styles["label"]), _paragraph(question.get("correct_answer"), styles["body"])],
346
- [_paragraph("Mistake type", styles["label"]), _paragraph(question.get("mistake_type"), styles["body"])],
347
- [_paragraph("Concepts", styles["label"]), _paragraph(question.get("concepts", []), styles["body"])],
348
- ]
349
- return Table(
350
- rows,
351
- colWidths=[width * 0.18, width * 0.82],
352
- style=TableStyle([
353
- ("BACKGROUND", (0, 0), (0, -1), THEME["panel"]),
354
- ("BACKGROUND", (1, 0), (1, -1), colors.white),
355
- ("BOX", (0, 0), (-1, -1), 0.8, THEME["line"]),
356
- ("INNERGRID", (0, 0), (-1, -1), 0.6, THEME["line"]),
357
- ("VALIGN", (0, 0), (-1, -1), "TOP"),
358
- ("LEFTPADDING", (0, 0), (-1, -1), 8),
359
- ("RIGHTPADDING", (0, 0), (-1, -1), 8),
360
- ("TOPPADDING", (0, 0), (-1, -1), 6),
361
- ("BOTTOMPADDING", (0, 0), (-1, -1), 6),
362
- ]),
363
- )
364
-
365
-
366
- def _steps_block(title: str, steps: Sequence[Any], styles: dict) -> List[Any]:
367
- return [Paragraph(_markup(title), styles["subhead"])] + _bullet_lines(steps or [], styles, empty_text="No steps provided.")
368
-
369
-
370
- def _mc_options_table(question: dict, styles: dict, width: float) -> Table:
371
- rows: List[List[Any]] = [[
372
- Paragraph("Option", styles["table_head"]),
373
- Paragraph("Text", styles["table_head"]),
374
- Paragraph("Verdict", styles["table_head"]),
375
- Paragraph("Reason", styles["table_head"]),
376
- ]]
377
- for option in question.get("options", []) or []:
378
- rows.append([
379
- _paragraph(option.get("label"), styles["table"]),
380
- _paragraph(option.get("text"), styles["table"]),
381
- _paragraph(option.get("verdict"), styles["table"]),
382
- _paragraph(option.get("reason"), styles["table"]),
383
- ])
384
- if len(rows) == 1:
385
- rows.append([_paragraph("-", styles["table"]), _paragraph("-", styles["table"]), _paragraph("-", styles["table"]), _paragraph("-", styles["table"])])
386
- return _table(
387
- rows,
388
- [width * 0.10, width * 0.28, width * 0.12, width * 0.50],
389
- TableStyle([
390
- ("BACKGROUND", (0, 0), (-1, 0), THEME["accent"]),
391
- ("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
392
- ("FONTNAME", (0, 0), (-1, -1), styles["table"].fontName),
393
- ("GRID", (0, 0), (-1, -1), 0.6, THEME["line"]),
394
- ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, THEME["panel"]]),
395
- ("VALIGN", (0, 0), (-1, -1), "TOP"),
396
- ("LEFTPADDING", (0, 0), (-1, -1), 6),
397
- ("RIGHTPADDING", (0, 0), (-1, -1), 6),
398
- ("TOPPADDING", (0, 0), (-1, -1), 5),
399
- ("BOTTOMPADDING", (0, 0), (-1, -1), 5),
400
- ]),
401
- )
402
-
403
-
404
- def _long_key_concepts_table(question: dict, styles: dict, width: float) -> Table:
405
- rows: List[List[Any]] = [[
406
- Paragraph("Concept", styles["table_head"]),
407
- Paragraph("Why it matters", styles["table_head"]),
408
- ]]
409
- for item in question.get("key_concepts", []) or []:
410
- rows.append([
411
- _paragraph(item.get("name"), styles["table"]),
412
- _paragraph(item.get("why_it_matters"), styles["table"]),
413
- ])
414
- if len(rows) == 1:
415
- rows.append([_paragraph("-", styles["table"]), _paragraph("-", styles["table"])])
416
- return _table(
417
- rows,
418
- [width * 0.24, width * 0.76],
419
- TableStyle([
420
- ("BACKGROUND", (0, 0), (-1, 0), THEME["accent"]),
421
- ("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
422
- ("FONTNAME", (0, 0), (-1, -1), styles["table"].fontName),
423
- ("GRID", (0, 0), (-1, -1), 0.6, THEME["line"]),
424
- ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, THEME["panel"]]),
425
- ("VALIGN", (0, 0), (-1, -1), "TOP"),
426
- ("LEFTPADDING", (0, 0), (-1, -1), 6),
427
- ("RIGHTPADDING", (0, 0), (-1, -1), 6),
428
- ("TOPPADDING", (0, 0), (-1, -1), 5),
429
- ("BOTTOMPADDING", (0, 0), (-1, -1), 5),
430
- ]),
431
- )
432
-
433
-
434
- def _step_comparison_table(question: dict, styles: dict, width: float) -> Table:
435
- rows: List[List[Any]] = [[
436
- Paragraph("Step", styles["table_head"]),
437
- Paragraph("Expected", styles["table_head"]),
438
- Paragraph("User", styles["table_head"]),
439
- Paragraph("Gap", styles["table_head"]),
440
- Paragraph("Fix", styles["table_head"]),
441
- ]]
442
- for item in question.get("step_comparison", []) or []:
443
- rows.append([
444
- _paragraph(item.get("step_no"), styles["table"]),
445
- _paragraph(item.get("expected_step"), styles["table"]),
446
- _paragraph(item.get("user_step"), styles["table"]),
447
- _paragraph(item.get("gap"), styles["table"]),
448
- _paragraph(item.get("fix"), styles["table"]),
449
- ])
450
- if len(rows) == 1:
451
- rows.append([
452
- _paragraph("-", styles["table"]),
453
- _paragraph("-", styles["table"]),
454
- _paragraph("-", styles["table"]),
455
- _paragraph("-", styles["table"]),
456
- _paragraph("-", styles["table"]),
457
- ])
458
- return _table(
459
- rows,
460
- [width * 0.08, width * 0.24, width * 0.22, width * 0.20, width * 0.26],
461
- TableStyle([
462
- ("BACKGROUND", (0, 0), (-1, 0), THEME["warning"]),
463
- ("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
464
- ("FONTNAME", (0, 0), (-1, -1), styles["table"].fontName),
465
- ("GRID", (0, 0), (-1, -1), 0.6, THEME["line"]),
466
- ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, THEME["warning_soft"]]),
467
- ("VALIGN", (0, 0), (-1, -1), "TOP"),
468
- ("LEFTPADDING", (0, 0), (-1, -1), 6),
469
- ("RIGHTPADDING", (0, 0), (-1, -1), 6),
470
- ("TOPPADDING", (0, 0), (-1, -1), 5),
471
- ("BOTTOMPADDING", (0, 0), (-1, -1), 5),
472
- ]),
473
- )
474
-
475
-
476
- def _mc_question_story(question: dict, styles: dict, width: float) -> List[Any]:
477
- story: List[Any] = [
478
- Paragraph(_markup(question.get("question_id"), default="Unnamed question"), styles["question"]),
479
- _question_meta(question, styles, width),
480
- Spacer(1, 6),
481
- Paragraph("Stem", styles["subhead"]),
482
- _paragraph(question.get("stem"), styles["body"]),
483
- Paragraph("Why it was wrong", styles["subhead"]),
484
- _paragraph(question.get("why_wrong"), styles["body"]),
485
- ]
486
- story.extend(_steps_block("Correct solution", question.get("correct_solution_steps", []), styles))
487
- story.extend([Spacer(1, 4), Paragraph("Option-by-option reasoning", styles["subhead"]), _mc_options_table(question, styles, width), Spacer(1, 10)])
488
- return story
489
-
490
-
491
- def _long_question_story(question: dict, styles: dict, width: float) -> List[Any]:
492
- first_wrong = _safe_text(question.get("first_incorrect_step"))
493
- story: List[Any] = [
494
- Paragraph(_markup(question.get("question_id"), default="Unnamed question"), styles["question"]),
495
- _question_meta(question, styles, width),
496
- Spacer(1, 6),
497
- Paragraph("Stem", styles["subhead"]),
498
- _paragraph(question.get("stem"), styles["body"]),
499
- Paragraph("Why it was wrong", styles["subhead"]),
500
- _paragraph(question.get("why_wrong"), styles["body"]),
501
- Spacer(1, 4),
502
- Table(
503
- [[Paragraph(f"First incorrect step: {_markup(first_wrong)}", styles["callout"])]],
504
- colWidths=[width],
505
- style=TableStyle([
506
- ("BACKGROUND", (0, 0), (-1, -1), THEME["warning_soft"]),
507
- ("BOX", (0, 0), (-1, -1), 0.8, THEME["warning"]),
508
- ("LEFTPADDING", (0, 0), (-1, -1), 10),
509
- ("RIGHTPADDING", (0, 0), (-1, -1), 10),
510
- ("TOPPADDING", (0, 0), (-1, -1), 8),
511
- ("BOTTOMPADDING", (0, 0), (-1, -1), 8),
512
- ]),
513
- ),
514
- Spacer(1, 6),
515
- Paragraph("Key concepts involved", styles["subhead"]),
516
- _long_key_concepts_table(question, styles, width),
517
- Spacer(1, 6),
518
- Paragraph("Step-by-step comparison", styles["subhead"]),
519
- _step_comparison_table(question, styles, width),
520
- ]
521
- story.extend([Spacer(1, 6)] + _steps_block("Correct solution", question.get("correct_solution_steps", []), styles) + [Spacer(1, 10)])
522
- return story
523
-
524
-
525
- def _questions_story(data: dict, styles: dict, width: float) -> List[Any]:
526
- story = _section_heading("Mistake-by-Mistake Analysis & Solutions", styles)
527
- questions = data.get("questions", []) or []
528
- if not questions:
529
- return story + [_paragraph("No question analysis provided.", styles["body"])]
530
-
531
- builder = _mc_question_story if data.get("book_type") == "mc-question" else _long_question_story
532
- for question in questions:
533
- story.extend(builder(question, styles, width))
534
- return story
535
-
536
-
537
- def _build_story(data: dict, styles: dict, doc: SimpleDocTemplate) -> List[Any]:
538
- title = _safe_text(data.get("title"), default="Error Book")
539
- subtitle = f"Type: {_safe_text(data.get('book_type'), default='general')} | Last updated: {_safe_text(data.get('last_updated'))}"
540
- story: List[Any] = [
541
- Spacer(1, 8),
542
- Paragraph(_markup(title), styles["title"]),
543
- Paragraph(_markup(subtitle), styles["subtitle"]),
544
- Paragraph(_markup("A structured review of mistakes, concepts, and corrections."), styles["subtitle"]),
545
- ]
546
- story.extend(_coverage_story(data, styles, doc.width))
547
- story.extend(_overview_story(data, styles))
548
- story.extend(_concept_story(data, styles, doc.width))
549
- story.append(PageBreak())
550
- story.extend(_questions_story(data, styles, doc.width))
551
- return story
552
-
553
-
554
- def main() -> int:
555
- parser = argparse.ArgumentParser(description="Render structured error-book JSON to a polished PDF.")
556
- parser.add_argument("input_json", help="Input JSON file path")
557
- parser.add_argument("output_pdf", help="Output PDF file path")
558
- parser.add_argument("--font-path", default=None, help="Path to a CJK-capable font file (.ttf/.otf/.ttc)")
559
- parser.add_argument("--font-size", type=int, default=11, help="Base font size (default: 11)")
560
- parser.add_argument("--pagesize", choices=["a4", "letter"], default="a4", help="Page size (default: a4)")
561
- parser.add_argument("--margin-mm", type=float, default=16.0, help="Page margin in mm (default: 16)")
562
- args = parser.parse_args()
563
-
564
- data = _read_json(args.input_json)
565
- font_path, subfont_index = _detect_cjk_font_path(args.font_path)
566
- font_name = _register_font(font_path, subfont_index)
567
- styles = _build_styles(font_name, args.font_size)
568
-
569
- page_size = A4 if args.pagesize == "a4" else letter
570
- margin = args.margin_mm * mm
571
-
572
- _ensure_parent_dir(args.output_pdf)
573
- doc = SimpleDocTemplate(
574
- args.output_pdf,
575
- pagesize=page_size,
576
- leftMargin=margin,
577
- rightMargin=margin,
578
- topMargin=margin,
579
- bottomMargin=margin,
580
- title=_safe_text(data.get("title"), default=os.path.basename(args.output_pdf)),
581
- author="Apollo Toolkit Learning Error Book Skill",
582
- )
583
-
584
- story = _build_story(data, styles, doc)
585
- doc.build(story, onFirstPage=_header_footer, onLaterPages=_header_footer)
586
- return 0
587
-
588
-
589
- if __name__ == "__main__":
590
- raise SystemExit(main())