@laitszkin/apollo-toolkit 2.8.0 → 2.10.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.md +2 -1
- package/CHANGELOG.md +17 -0
- package/README.md +2 -0
- package/document-vision-reader/LICENSE +21 -0
- package/document-vision-reader/README.md +66 -0
- package/document-vision-reader/SKILL.md +151 -0
- package/document-vision-reader/agents/openai.yaml +4 -0
- package/document-vision-reader/references/legibility-checklist.md +13 -0
- package/document-vision-reader/references/rendering-guide.md +37 -0
- package/katex/SKILL.md +92 -0
- package/katex/agents/openai.yaml +4 -0
- package/katex/references/insertion-patterns.md +54 -0
- package/katex/references/official-docs.md +35 -0
- package/katex/scripts/render_katex.py +247 -0
- package/katex/scripts/render_katex.sh +11 -0
- package/learning-error-book/SKILL.md +46 -31
- package/learning-error-book/agents/openai.yaml +2 -2
- package/learning-error-book/assets/long_question_reference_template.json +57 -0
- package/learning-error-book/assets/mc_question_reference_template.json +49 -0
- package/learning-error-book/scripts/render_error_book_json_to_pdf.py +590 -0
- package/open-github-issue/README.md +7 -1
- package/open-github-issue/SKILL.md +10 -3
- package/open-github-issue/scripts/open_github_issue.py +25 -0
- package/open-github-issue/tests/test_open_github_issue.py +49 -1
- package/package.json +1 -1
- package/scheduled-runtime-health-check/README.md +26 -15
- package/scheduled-runtime-health-check/SKILL.md +70 -53
- package/scheduled-runtime-health-check/agents/openai.yaml +2 -2
- package/learning-error-book/assets/error_book_template.md +0 -66
- 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())
|
|
@@ -42,7 +42,7 @@ The bundled script can also be called directly:
|
|
|
42
42
|
python scripts/open_github_issue.py \
|
|
43
43
|
--issue-type problem \
|
|
44
44
|
--title "[Log] Payment timeout spike" \
|
|
45
|
-
--problem-description
|
|
45
|
+
--problem-description $'Expected Behavior (BDD)\nGiven the payment service sees transient upstream latency\nWhen the retry path runs\nThen requests should recover without user-visible failures\n\nCurrent Behavior (BDD)\nGiven the payment service sees transient upstream latency\nWhen the retry path runs\nThen repeated timeout warnings still escalate into request failures\n\nBehavior Gap\n- Expected: retries absorb transient upstream slowness.\n- Actual: retries still end in request failures.\n- Difference/Impact: customers receive failed payment attempts during the incident window.\n\nEvidence\n- symptom: repeated timeout warnings escalated into request failures.\n- impact: payment attempts failed for end users.\n- key evidence: logs from the incident window show retries without successful recovery.' \
|
|
46
46
|
--suspected-cause "payment-api/handler.py:84 retries immediately against a slow upstream with no jitter; confidence high." \
|
|
47
47
|
--reproduction "Not yet reliably reproducible; more runtime evidence is required." \
|
|
48
48
|
--repo owner/repo
|
|
@@ -72,6 +72,12 @@ Problem issues always include exactly three sections:
|
|
|
72
72
|
- `Suspected Cause`
|
|
73
73
|
- `Reproduction Conditions (if available)`
|
|
74
74
|
|
|
75
|
+
Within `Problem Description`, include:
|
|
76
|
+
|
|
77
|
+
- `Expected Behavior (BDD)`
|
|
78
|
+
- `Current Behavior (BDD)`
|
|
79
|
+
- `Behavior Gap`
|
|
80
|
+
|
|
75
81
|
For Chinese-language repositories, use translated section titles with the same meaning.
|
|
76
82
|
|
|
77
83
|
Feature proposal issues always include:
|
|
@@ -14,9 +14,9 @@ description: Publish structured GitHub issues and feature proposals with determi
|
|
|
14
14
|
|
|
15
15
|
## Standards
|
|
16
16
|
|
|
17
|
-
- Evidence: Require structured issue inputs
|
|
17
|
+
- Evidence: Require structured issue inputs, detect repository language from the target README instead of guessing, and for `problem` issues capture BDD-style expected vs current behavior with an explicit delta.
|
|
18
18
|
- Execution: Resolve the repo, normalize the issue body, publish with strict auth order, then return the publication result.
|
|
19
|
-
- Quality: Preserve upstream evidence, localize only the structural parts,
|
|
19
|
+
- Quality: Preserve upstream evidence, localize only the structural parts, keep publication deterministic and reproducible, and make behavioral mismatches easy for maintainers to verify.
|
|
20
20
|
- Output: Return publication mode, issue URL when created, rendered body, and any publish error in the standardized JSON contract.
|
|
21
21
|
|
|
22
22
|
## Overview
|
|
@@ -37,6 +37,7 @@ It is designed to be reusable by other skills that already know the issue title
|
|
|
37
37
|
- Detect repository issue language from the target remote README instead of guessing.
|
|
38
38
|
- Preserve upstream evidence content; only localize section headers and default fallback text.
|
|
39
39
|
- Make the issue type explicit: `problem` for defects/incidents, `feature` for proposals.
|
|
40
|
+
- For `problem` issues, describe the expected behavior and current behavior with BDD-style `Given / When / Then`, then state the behavioral difference explicitly.
|
|
40
41
|
|
|
41
42
|
## Workflow
|
|
42
43
|
|
|
@@ -49,6 +50,11 @@ It is designed to be reusable by other skills that already know the issue title
|
|
|
49
50
|
- `problem-description`
|
|
50
51
|
- `suspected-cause`
|
|
51
52
|
- `reproduction` (optional)
|
|
53
|
+
- Within `problem-description`, require a precise behavior diff:
|
|
54
|
+
- `Expected Behavior (BDD)`: `Given / When / Then` for what the program should do.
|
|
55
|
+
- `Current Behavior (BDD)`: `Given / When / Then` for what the program does now.
|
|
56
|
+
- `Behavior Gap`: a short explicit comparison of the observable difference and impact.
|
|
57
|
+
- Include the symptom, impact, and key evidence alongside the behavior diff; do not leave the mismatch implicit.
|
|
52
58
|
- For `feature` issues, require these structured sections:
|
|
53
59
|
- `proposal` (optional; defaults to title when omitted)
|
|
54
60
|
- `reason`
|
|
@@ -74,7 +80,7 @@ Problem issue:
|
|
|
74
80
|
python scripts/open_github_issue.py \
|
|
75
81
|
--issue-type problem \
|
|
76
82
|
--title "[Log] <short symptom>" \
|
|
77
|
-
--problem-description
|
|
83
|
+
--problem-description $'Expected Behavior (BDD)\nGiven ...\nWhen ...\nThen ...\n\nCurrent Behavior (BDD)\nGiven ...\nWhen ...\nThen ...\n\nBehavior Gap\n- Expected: ...\n- Actual: ...\n- Difference/Impact: ...\n\nEvidence\n- symptom: ...\n- impact: ...\n- key evidence: ...' \
|
|
78
84
|
--suspected-cause "<path:line + causal chain + confidence>" \
|
|
79
85
|
--reproduction "<steps/conditions or leave empty>" \
|
|
80
86
|
--repo <owner/repo>
|
|
@@ -111,6 +117,7 @@ When another skill depends on `open-github-issue`:
|
|
|
111
117
|
|
|
112
118
|
- Pass exactly one confirmed problem or one accepted feature proposal per invocation.
|
|
113
119
|
- Prepare evidence or proposal details before calling this skill; do not ask this skill to infer root cause or architecture.
|
|
120
|
+
- For `problem` issues, pass a `problem-description` that contains `Expected Behavior (BDD)`, `Current Behavior (BDD)`, and `Behavior Gap`; the difference must be explicit, not implied.
|
|
114
121
|
- Reuse the returned `mode`, `issue_url`, and `publish_error` in the parent skill response.
|
|
115
122
|
- For accepted feature proposals, pass `--issue-type feature` plus `--proposal`, `--reason`, and `--suggested-architecture`.
|
|
116
123
|
|