@laitszkin/apollo-toolkit 2.9.0 → 2.11.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 (36) hide show
  1. package/AGENTS.md +3 -1
  2. package/CHANGELOG.md +22 -0
  3. package/README.md +4 -1
  4. package/archive-specs/SKILL.md +4 -0
  5. package/commit-and-push/SKILL.md +3 -0
  6. package/develop-new-features/README.md +1 -0
  7. package/develop-new-features/SKILL.md +13 -1
  8. package/develop-new-features/agents/openai.yaml +1 -1
  9. package/document-vision-reader/LICENSE +21 -0
  10. package/document-vision-reader/README.md +66 -0
  11. package/document-vision-reader/SKILL.md +151 -0
  12. package/document-vision-reader/agents/openai.yaml +4 -0
  13. package/document-vision-reader/references/legibility-checklist.md +13 -0
  14. package/document-vision-reader/references/rendering-guide.md +37 -0
  15. package/enhance-existing-features/README.md +1 -0
  16. package/enhance-existing-features/SKILL.md +15 -2
  17. package/enhance-existing-features/agents/openai.yaml +1 -1
  18. package/exam-pdf-workflow/LICENSE +21 -0
  19. package/exam-pdf-workflow/README.md +38 -0
  20. package/exam-pdf-workflow/SKILL.md +106 -0
  21. package/exam-pdf-workflow/agents/openai.yaml +4 -0
  22. package/generate-spec/SKILL.md +5 -0
  23. package/katex/SKILL.md +92 -0
  24. package/katex/agents/openai.yaml +4 -0
  25. package/katex/references/insertion-patterns.md +54 -0
  26. package/katex/references/official-docs.md +35 -0
  27. package/katex/scripts/render_katex.py +247 -0
  28. package/katex/scripts/render_katex.sh +11 -0
  29. package/learning-error-book/SKILL.md +46 -31
  30. package/learning-error-book/agents/openai.yaml +2 -2
  31. package/learning-error-book/assets/long_question_reference_template.json +57 -0
  32. package/learning-error-book/assets/mc_question_reference_template.json +49 -0
  33. package/learning-error-book/scripts/render_error_book_json_to_pdf.py +590 -0
  34. package/package.json +1 -1
  35. package/learning-error-book/assets/error_book_template.md +0 -66
  36. package/learning-error-book/scripts/render_markdown_to_pdf.py +0 -367
@@ -0,0 +1,590 @@
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())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@laitszkin/apollo-toolkit",
3
- "version": "2.9.0",
3
+ "version": "2.11.0",
4
4
  "description": "Apollo Toolkit npm installer for managed skill copying across Codex, OpenClaw, and Trae.",
5
5
  "license": "MIT",
6
6
  "author": "LaiTszKin",
@@ -1,66 +0,0 @@
1
- # Error Book
2
-
3
- Last updated: {{DATE}}
4
-
5
- ## 1. Coverage Scope
6
-
7
- This error book includes the following question sources (with traceable locators):
8
-
9
- - (Question file/source):
10
- - Path: `...`
11
- - Included questions: Q... / Page ... (optional)
12
-
13
- ## 2. Common Mistake Types Overview
14
-
15
- > Goal: classify mistake patterns first, then use questions as evidence later.
16
-
17
- - Concept misunderstanding:
18
- - Representative questions: ...
19
- - Misreading / missing conditions:
20
- - Representative questions: ...
21
- - Incomplete reasoning steps:
22
- - Representative questions: ...
23
- - Calculation / sign / algebra errors (if any):
24
- - Representative questions: ...
25
- - Option traps / intuitive misjudgment (common in MC):
26
- - Representative questions: ...
27
-
28
- ## 3. Conceptual Mistake Highlights
29
-
30
- ### Concept: {{CONCEPT_NAME}}
31
-
32
- - Definition (precise and actionable):
33
- - ...
34
- - User's common misjudgment (mapped to this mistake):
35
- - ...
36
- - Cautions / checklists:
37
- - ...
38
- - Minimal example (optional):
39
- - ...
40
-
41
- ## 4. Mistake-by-Mistake Analysis & Solutions
42
-
43
- ### Question: {{QUESTION_ID}}
44
-
45
- - Source locator:
46
- - File: `...`
47
- - Page / Question id: ...
48
- - Stem:
49
- - ...
50
- - (If MC) Options:
51
- - A. ...
52
- - B. ...
53
- - C. ...
54
- - D. ...
55
- - User answer: ...
56
- - Correct answer: ...
57
- - Why it was wrong (link to mistake type + concept):
58
- - ...
59
- - Correct solution (step-by-step):
60
- 1. ...
61
- 2. ...
62
- - (If MC) Option-by-option reasoning:
63
- - Why A is wrong/right: ...
64
- - Why B is wrong/right: ...
65
- - Why C is wrong/right: ...
66
- - Why D is wrong/right: ...