codex-workflow-v2 2.0.0-beta.10 → 2.0.0-beta.11
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/README.md +43 -9
- package/dist/src/alpha6/handoff.d.ts +4 -3
- package/dist/src/alpha6/handoff.js +58 -3
- package/dist/src/alpha6/handoff.js.map +1 -1
- package/dist/src/alpha6/mechanical-feasibility.d.ts +7 -0
- package/dist/src/alpha6/mechanical-feasibility.js +303 -0
- package/dist/src/alpha6/mechanical-feasibility.js.map +1 -0
- package/dist/src/alpha6/milestone.d.ts +25 -2
- package/dist/src/alpha6/milestone.js +359 -32
- package/dist/src/alpha6/milestone.js.map +1 -1
- package/dist/src/alpha6/plan-integrity.d.ts +14 -0
- package/dist/src/alpha6/plan-integrity.js +127 -0
- package/dist/src/alpha6/plan-integrity.js.map +1 -0
- package/dist/src/alpha6/remediation.d.ts +2 -1
- package/dist/src/alpha6/remediation.js +168 -3
- package/dist/src/alpha6/remediation.js.map +1 -1
- package/dist/src/cli.js +13 -1
- package/dist/src/cli.js.map +1 -1
- package/dist/src/contracts.d.ts +69 -1
- package/dist/src/git.d.ts +1 -1
- package/dist/src/git.js +8 -1
- package/dist/src/git.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/lifecycle/corrective-replan.js +8 -3
- package/dist/src/lifecycle/corrective-replan.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/workflow.d.ts +5 -0
- package/dist/src/workflow.js +399 -90
- package/dist/src/workflow.js.map +1 -1
- package/docs/autonomy-guardrails.md +21 -0
- package/docs/beta11-plan-integrity-recovery-brief.md +38 -0
- package/docs/development-flow.md +31 -6
- package/docs/lifecycle/state-machine-stabilization.md +1 -1
- package/docs/pdf/README.md +24 -0
- package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
- package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
- package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
- package/docs/pdf/requirements.txt +1 -0
- package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +234 -0
- package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +334 -0
- package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +413 -0
- package/docs/problem-briefs/01-pre-implementation-integrity.md +478 -0
- package/docs/problem-briefs/02-minimal-step-integrity.md +411 -0
- package/docs/problem-briefs/03-minimal-agent-context-integrity.md +358 -0
- package/docs/problem-briefs/04-task-dependency-and-structural-replacement-integrity.md +566 -0
- package/docs/problem-briefs/BRIEF-TEMPLATE.md +56 -0
- package/docs/problem-briefs/README.md +120 -0
- package/docs/problem-briefs/evidence/p01-mechanical-feasibility-corpus.md +90 -0
- package/docs/problem-briefs/evidence/signal-v4-pre-m3-replay.md +246 -0
- package/docs/release.md +15 -6
- package/docs/split-required-recovery.md +19 -26
- package/docs/validation-report.md +116 -77
- package/package.json +5 -1
- package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +1 -1
- package/plugins/codex-workflow-gateway/references/protocol.md +19 -2
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +17 -3
- package/references/state-machine.md +9 -5
- package/schemas/authorization-event.schema.json +64 -1
- package/schemas/corrective-decision-event.schema.json +48 -3
- package/schemas/milestone-scope-change-event.schema.json +6 -1
- package/schemas/milestone.schema.json +6 -1
- package/schemas/task-handoff-event.schema.json +18 -2
- package/scripts/generate-pdf-docs.py +512 -0
- package/scripts/run-pdf-docs.mjs +62 -0
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate the tracked Russian Workflow V2 PDF documentation.
|
|
3
|
+
|
|
4
|
+
The Markdown sources under docs/pdf/sources are the reviewable authority for the
|
|
5
|
+
three human-facing PDF artifacts. The generator is deterministic so release
|
|
6
|
+
validation can compare regenerated bytes with the committed files.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import hashlib
|
|
13
|
+
import html
|
|
14
|
+
import json
|
|
15
|
+
import re
|
|
16
|
+
import sys
|
|
17
|
+
import tempfile
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from reportlab.lib import colors
|
|
21
|
+
from reportlab.lib.enums import TA_CENTER, TA_LEFT
|
|
22
|
+
from reportlab.lib.pagesizes import A4
|
|
23
|
+
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
|
|
24
|
+
from reportlab.lib.units import mm
|
|
25
|
+
from reportlab.pdfbase import pdfmetrics
|
|
26
|
+
from reportlab.pdfbase.ttfonts import TTFont
|
|
27
|
+
from reportlab.pdfgen import canvas
|
|
28
|
+
from reportlab.platypus import (
|
|
29
|
+
KeepTogether,
|
|
30
|
+
ListFlowable,
|
|
31
|
+
ListItem,
|
|
32
|
+
LongTable,
|
|
33
|
+
PageBreak,
|
|
34
|
+
Paragraph,
|
|
35
|
+
SimpleDocTemplate,
|
|
36
|
+
Spacer,
|
|
37
|
+
Table,
|
|
38
|
+
TableStyle,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
43
|
+
SOURCE_DIR = ROOT / "docs" / "pdf" / "sources"
|
|
44
|
+
OUTPUT_DIR = ROOT / "docs" / "pdf"
|
|
45
|
+
PACKAGE = json.loads((ROOT / "package.json").read_text(encoding="utf-8"))
|
|
46
|
+
PACKAGE_VERSION = PACKAGE["version"]
|
|
47
|
+
|
|
48
|
+
DOCUMENTS = (
|
|
49
|
+
("codex-workflow-v2-architecture-ru.md", "codex-workflow-v2-architecture-ru.pdf"),
|
|
50
|
+
("codex-workflow-v2-chat-only-guide-ru.md", "codex-workflow-v2-chat-only-guide-ru.pdf"),
|
|
51
|
+
("codex-workflow-v2-technical-reference-ru.md", "codex-workflow-v2-technical-reference-ru.pdf"),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
PAGE_WIDTH, PAGE_HEIGHT = A4
|
|
55
|
+
LEFT = 22 * mm
|
|
56
|
+
RIGHT = 20 * mm
|
|
57
|
+
TOP = 22 * mm
|
|
58
|
+
BOTTOM = 18 * mm
|
|
59
|
+
CONTENT_WIDTH = PAGE_WIDTH - LEFT - RIGHT
|
|
60
|
+
|
|
61
|
+
NAVY = colors.HexColor("#14213D")
|
|
62
|
+
BLUE = colors.HexColor("#2563EB")
|
|
63
|
+
PALE_BLUE = colors.HexColor("#EFF6FF")
|
|
64
|
+
PALE_GREEN = colors.HexColor("#ECFDF5")
|
|
65
|
+
PALE_ORANGE = colors.HexColor("#FFF7ED")
|
|
66
|
+
PALE_GRAY = colors.HexColor("#F8FAFC")
|
|
67
|
+
MID_GRAY = colors.HexColor("#CBD5E1")
|
|
68
|
+
TEXT = colors.HexColor("#172033")
|
|
69
|
+
MUTED = colors.HexColor("#64748B")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def first_existing(candidates: tuple[str, ...]) -> str:
|
|
73
|
+
for candidate in candidates:
|
|
74
|
+
if Path(candidate).is_file():
|
|
75
|
+
return candidate
|
|
76
|
+
raise RuntimeError(f"No usable font found in: {', '.join(candidates)}")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def register_fonts() -> None:
|
|
80
|
+
regular = first_existing((
|
|
81
|
+
"/System/Library/Fonts/Supplemental/Arial.ttf",
|
|
82
|
+
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
|
83
|
+
"/usr/share/fonts/dejavu/DejaVuSans.ttf",
|
|
84
|
+
))
|
|
85
|
+
bold = first_existing((
|
|
86
|
+
"/System/Library/Fonts/Supplemental/Arial Bold.ttf",
|
|
87
|
+
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
|
|
88
|
+
"/usr/share/fonts/dejavu/DejaVuSans-Bold.ttf",
|
|
89
|
+
))
|
|
90
|
+
italic = first_existing((
|
|
91
|
+
"/System/Library/Fonts/Supplemental/Arial Italic.ttf",
|
|
92
|
+
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Oblique.ttf",
|
|
93
|
+
"/usr/share/fonts/dejavu/DejaVuSans-Oblique.ttf",
|
|
94
|
+
))
|
|
95
|
+
bold_italic = first_existing((
|
|
96
|
+
"/System/Library/Fonts/Supplemental/Arial Bold Italic.ttf",
|
|
97
|
+
"/usr/share/fonts/truetype/dejavu/DejaVuSans-BoldOblique.ttf",
|
|
98
|
+
"/usr/share/fonts/dejavu/DejaVuSans-BoldOblique.ttf",
|
|
99
|
+
))
|
|
100
|
+
pdfmetrics.registerFont(TTFont("WorkflowBody", regular))
|
|
101
|
+
pdfmetrics.registerFont(TTFont("WorkflowBold", bold))
|
|
102
|
+
pdfmetrics.registerFont(TTFont("WorkflowItalic", italic))
|
|
103
|
+
pdfmetrics.registerFont(TTFont("WorkflowBoldItalic", bold_italic))
|
|
104
|
+
pdfmetrics.registerFontFamily(
|
|
105
|
+
"WorkflowBody",
|
|
106
|
+
normal="WorkflowBody",
|
|
107
|
+
bold="WorkflowBold",
|
|
108
|
+
italic="WorkflowItalic",
|
|
109
|
+
boldItalic="WorkflowBoldItalic",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class InvariantCanvas(canvas.Canvas):
|
|
114
|
+
def __init__(self, *args, **kwargs):
|
|
115
|
+
kwargs["invariant"] = 1
|
|
116
|
+
super().__init__(*args, **kwargs)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def styles() -> dict[str, ParagraphStyle]:
|
|
120
|
+
base = getSampleStyleSheet()
|
|
121
|
+
return {
|
|
122
|
+
"title": ParagraphStyle(
|
|
123
|
+
"Title",
|
|
124
|
+
parent=base["Title"],
|
|
125
|
+
fontName="WorkflowBold",
|
|
126
|
+
fontSize=25,
|
|
127
|
+
leading=30,
|
|
128
|
+
textColor=NAVY,
|
|
129
|
+
alignment=TA_LEFT,
|
|
130
|
+
spaceAfter=10,
|
|
131
|
+
),
|
|
132
|
+
"subtitle": ParagraphStyle(
|
|
133
|
+
"Subtitle",
|
|
134
|
+
parent=base["Normal"],
|
|
135
|
+
fontName="WorkflowBody",
|
|
136
|
+
fontSize=12,
|
|
137
|
+
leading=17,
|
|
138
|
+
textColor=MUTED,
|
|
139
|
+
spaceAfter=18,
|
|
140
|
+
),
|
|
141
|
+
"part": ParagraphStyle(
|
|
142
|
+
"Part",
|
|
143
|
+
parent=base["Normal"],
|
|
144
|
+
fontName="WorkflowBold",
|
|
145
|
+
fontSize=8,
|
|
146
|
+
leading=10,
|
|
147
|
+
textColor=BLUE,
|
|
148
|
+
spaceAfter=10,
|
|
149
|
+
),
|
|
150
|
+
"h1": ParagraphStyle(
|
|
151
|
+
"H1",
|
|
152
|
+
parent=base["Heading1"],
|
|
153
|
+
fontName="WorkflowBold",
|
|
154
|
+
fontSize=18,
|
|
155
|
+
leading=22,
|
|
156
|
+
textColor=NAVY,
|
|
157
|
+
spaceBefore=10,
|
|
158
|
+
spaceAfter=8,
|
|
159
|
+
keepWithNext=True,
|
|
160
|
+
),
|
|
161
|
+
"h2": ParagraphStyle(
|
|
162
|
+
"H2",
|
|
163
|
+
parent=base["Heading2"],
|
|
164
|
+
fontName="WorkflowBold",
|
|
165
|
+
fontSize=14,
|
|
166
|
+
leading=18,
|
|
167
|
+
textColor=BLUE,
|
|
168
|
+
spaceBefore=8,
|
|
169
|
+
spaceAfter=6,
|
|
170
|
+
keepWithNext=True,
|
|
171
|
+
),
|
|
172
|
+
"h3": ParagraphStyle(
|
|
173
|
+
"H3",
|
|
174
|
+
parent=base["Heading3"],
|
|
175
|
+
fontName="WorkflowBold",
|
|
176
|
+
fontSize=11,
|
|
177
|
+
leading=14,
|
|
178
|
+
textColor=NAVY,
|
|
179
|
+
spaceBefore=6,
|
|
180
|
+
spaceAfter=4,
|
|
181
|
+
keepWithNext=True,
|
|
182
|
+
),
|
|
183
|
+
"body": ParagraphStyle(
|
|
184
|
+
"Body",
|
|
185
|
+
parent=base["BodyText"],
|
|
186
|
+
fontName="WorkflowBody",
|
|
187
|
+
fontSize=9.1,
|
|
188
|
+
leading=13.2,
|
|
189
|
+
textColor=TEXT,
|
|
190
|
+
spaceAfter=5,
|
|
191
|
+
allowWidows=0,
|
|
192
|
+
allowOrphans=0,
|
|
193
|
+
),
|
|
194
|
+
"small": ParagraphStyle(
|
|
195
|
+
"Small",
|
|
196
|
+
parent=base["BodyText"],
|
|
197
|
+
fontName="WorkflowBody",
|
|
198
|
+
fontSize=7.7,
|
|
199
|
+
leading=10.3,
|
|
200
|
+
textColor=TEXT,
|
|
201
|
+
),
|
|
202
|
+
"code": ParagraphStyle(
|
|
203
|
+
"Code",
|
|
204
|
+
parent=base["BodyText"],
|
|
205
|
+
fontName="WorkflowBody",
|
|
206
|
+
fontSize=7.6,
|
|
207
|
+
leading=10.1,
|
|
208
|
+
textColor=colors.HexColor("#E2E8F0"),
|
|
209
|
+
backColor=NAVY,
|
|
210
|
+
borderPadding=0,
|
|
211
|
+
wordWrap="CJK",
|
|
212
|
+
),
|
|
213
|
+
"callout": ParagraphStyle(
|
|
214
|
+
"Callout",
|
|
215
|
+
parent=base["BodyText"],
|
|
216
|
+
fontName="WorkflowBody",
|
|
217
|
+
fontSize=8.5,
|
|
218
|
+
leading=12,
|
|
219
|
+
textColor=TEXT,
|
|
220
|
+
),
|
|
221
|
+
"footer": ParagraphStyle(
|
|
222
|
+
"Footer",
|
|
223
|
+
parent=base["Normal"],
|
|
224
|
+
fontName="WorkflowBody",
|
|
225
|
+
fontSize=7,
|
|
226
|
+
textColor=MUTED,
|
|
227
|
+
alignment=TA_CENTER,
|
|
228
|
+
),
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def inline_markup(value: str) -> str:
|
|
233
|
+
escaped = html.escape(value.strip())
|
|
234
|
+
escaped = re.sub(r"`([^`]+)`", r'<font name="WorkflowBold">\1</font>', escaped)
|
|
235
|
+
escaped = re.sub(r"\*\*([^*]+)\*\*", r"<b>\1</b>", escaped)
|
|
236
|
+
escaped = re.sub(r"\*([^*]+)\*", r"<i>\1</i>", escaped)
|
|
237
|
+
return escaped
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def parse_source(path: Path) -> tuple[dict[str, str], list[str]]:
|
|
241
|
+
lines = path.read_text(encoding="utf-8").splitlines()
|
|
242
|
+
if not lines or lines[0].strip() != "---":
|
|
243
|
+
raise RuntimeError(f"{path} must start with frontmatter")
|
|
244
|
+
metadata: dict[str, str] = {}
|
|
245
|
+
index = 1
|
|
246
|
+
while index < len(lines) and lines[index].strip() != "---":
|
|
247
|
+
line = lines[index]
|
|
248
|
+
if ":" not in line:
|
|
249
|
+
raise RuntimeError(f"Invalid frontmatter line in {path}: {line}")
|
|
250
|
+
key, value = line.split(":", 1)
|
|
251
|
+
metadata[key.strip()] = value.strip().strip('"')
|
|
252
|
+
index += 1
|
|
253
|
+
if index >= len(lines):
|
|
254
|
+
raise RuntimeError(f"Unclosed frontmatter in {path}")
|
|
255
|
+
return metadata, lines[index + 1 :]
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def table_widths(column_count: int) -> list[float]:
|
|
259
|
+
if column_count == 2:
|
|
260
|
+
return [CONTENT_WIDTH * 0.34, CONTENT_WIDTH * 0.66]
|
|
261
|
+
if column_count == 3:
|
|
262
|
+
return [CONTENT_WIDTH * 0.26, CONTENT_WIDTH * 0.34, CONTENT_WIDTH * 0.40]
|
|
263
|
+
return [CONTENT_WIDTH / column_count] * column_count
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def markdown_story(lines: list[str], style: dict[str, ParagraphStyle]) -> list:
|
|
267
|
+
story: list = []
|
|
268
|
+
index = 0
|
|
269
|
+
while index < len(lines):
|
|
270
|
+
raw = lines[index]
|
|
271
|
+
stripped = raw.strip()
|
|
272
|
+
if not stripped:
|
|
273
|
+
story.append(Spacer(1, 2.5))
|
|
274
|
+
index += 1
|
|
275
|
+
continue
|
|
276
|
+
if stripped == "<!-- pagebreak -->":
|
|
277
|
+
story.append(PageBreak())
|
|
278
|
+
index += 1
|
|
279
|
+
continue
|
|
280
|
+
if stripped.startswith("```"):
|
|
281
|
+
code_lines: list[str] = []
|
|
282
|
+
index += 1
|
|
283
|
+
while index < len(lines) and not lines[index].strip().startswith("```"):
|
|
284
|
+
code_lines.append(lines[index].rstrip())
|
|
285
|
+
index += 1
|
|
286
|
+
if index >= len(lines):
|
|
287
|
+
raise RuntimeError("Unclosed code block")
|
|
288
|
+
code_html = "<br/>".join(html.escape(line or " ") for line in code_lines)
|
|
289
|
+
block = Table([[Paragraph(code_html, style["code"])]], colWidths=[CONTENT_WIDTH])
|
|
290
|
+
block.setStyle(TableStyle([
|
|
291
|
+
("BACKGROUND", (0, 0), (-1, -1), NAVY),
|
|
292
|
+
("BOX", (0, 0), (-1, -1), 0.8, BLUE),
|
|
293
|
+
("LEFTPADDING", (0, 0), (-1, -1), 10),
|
|
294
|
+
("RIGHTPADDING", (0, 0), (-1, -1), 10),
|
|
295
|
+
("TOPPADDING", (0, 0), (-1, -1), 8),
|
|
296
|
+
("BOTTOMPADDING", (0, 0), (-1, -1), 8),
|
|
297
|
+
]))
|
|
298
|
+
story.extend([block, Spacer(1, 6)])
|
|
299
|
+
index += 1
|
|
300
|
+
continue
|
|
301
|
+
heading = re.match(r"^(#{1,3})\s+(.+)$", stripped)
|
|
302
|
+
if heading:
|
|
303
|
+
level = len(heading.group(1))
|
|
304
|
+
story.append(Paragraph(inline_markup(heading.group(2)), style[f"h{level}"]))
|
|
305
|
+
index += 1
|
|
306
|
+
continue
|
|
307
|
+
if stripped.startswith("|"):
|
|
308
|
+
table_lines: list[str] = []
|
|
309
|
+
while index < len(lines) and lines[index].strip().startswith("|"):
|
|
310
|
+
table_lines.append(lines[index].strip())
|
|
311
|
+
index += 1
|
|
312
|
+
rows = [[cell.strip() for cell in line.strip("|").split("|")] for line in table_lines]
|
|
313
|
+
if len(rows) >= 2 and all(re.fullmatch(r":?-{3,}:?", cell) for cell in rows[1]):
|
|
314
|
+
rows.pop(1)
|
|
315
|
+
column_count = max(len(row) for row in rows)
|
|
316
|
+
normalized = [row + [""] * (column_count - len(row)) for row in rows]
|
|
317
|
+
rendered = [
|
|
318
|
+
[Paragraph(inline_markup(cell), style["small"]) for cell in row]
|
|
319
|
+
for row in normalized
|
|
320
|
+
]
|
|
321
|
+
table = LongTable(rendered, colWidths=table_widths(column_count), repeatRows=1)
|
|
322
|
+
table.setStyle(TableStyle([
|
|
323
|
+
("BACKGROUND", (0, 0), (-1, 0), NAVY),
|
|
324
|
+
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
|
|
325
|
+
("FONTNAME", (0, 0), (-1, 0), "WorkflowBold"),
|
|
326
|
+
("BACKGROUND", (0, 1), (-1, -1), PALE_GRAY),
|
|
327
|
+
("GRID", (0, 0), (-1, -1), 0.45, MID_GRAY),
|
|
328
|
+
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
|
329
|
+
("LEFTPADDING", (0, 0), (-1, -1), 6),
|
|
330
|
+
("RIGHTPADDING", (0, 0), (-1, -1), 6),
|
|
331
|
+
("TOPPADDING", (0, 0), (-1, -1), 5),
|
|
332
|
+
("BOTTOMPADDING", (0, 0), (-1, -1), 5),
|
|
333
|
+
]))
|
|
334
|
+
story.extend([table, Spacer(1, 6)])
|
|
335
|
+
continue
|
|
336
|
+
if stripped.startswith(">"):
|
|
337
|
+
quote_lines: list[str] = []
|
|
338
|
+
while index < len(lines) and lines[index].strip().startswith(">"):
|
|
339
|
+
quote_lines.append(lines[index].strip()[1:].strip())
|
|
340
|
+
index += 1
|
|
341
|
+
value = " ".join(quote_lines)
|
|
342
|
+
color = PALE_ORANGE if "Важно" in value or "Стоп" in value else PALE_BLUE
|
|
343
|
+
callout = Table([[Paragraph(inline_markup(value), style["callout"])]], colWidths=[CONTENT_WIDTH])
|
|
344
|
+
callout.setStyle(TableStyle([
|
|
345
|
+
("BACKGROUND", (0, 0), (-1, -1), color),
|
|
346
|
+
("BOX", (0, 0), (-1, -1), 0.8, BLUE),
|
|
347
|
+
("LINEBEFORE", (0, 0), (0, -1), 3, BLUE),
|
|
348
|
+
("LEFTPADDING", (0, 0), (-1, -1), 10),
|
|
349
|
+
("RIGHTPADDING", (0, 0), (-1, -1), 10),
|
|
350
|
+
("TOPPADDING", (0, 0), (-1, -1), 7),
|
|
351
|
+
("BOTTOMPADDING", (0, 0), (-1, -1), 7),
|
|
352
|
+
]))
|
|
353
|
+
story.extend([callout, Spacer(1, 6)])
|
|
354
|
+
continue
|
|
355
|
+
if re.match(r"^-\s+", stripped):
|
|
356
|
+
items: list[ListItem] = []
|
|
357
|
+
while index < len(lines) and re.match(r"^-\s+", lines[index].strip()):
|
|
358
|
+
value = re.sub(r"^-\s+", "", lines[index].strip())
|
|
359
|
+
items.append(ListItem(Paragraph(inline_markup(value), style["body"]), leftIndent=10))
|
|
360
|
+
index += 1
|
|
361
|
+
story.append(ListFlowable(items, bulletType="bullet", leftIndent=16, bulletColor=BLUE))
|
|
362
|
+
story.append(Spacer(1, 3))
|
|
363
|
+
continue
|
|
364
|
+
if re.match(r"^\d+\.\s+", stripped):
|
|
365
|
+
items = []
|
|
366
|
+
while index < len(lines) and re.match(r"^\d+\.\s+", lines[index].strip()):
|
|
367
|
+
value = re.sub(r"^\d+\.\s+", "", lines[index].strip())
|
|
368
|
+
items.append(ListItem(Paragraph(inline_markup(value), style["body"]), leftIndent=12))
|
|
369
|
+
index += 1
|
|
370
|
+
story.append(ListFlowable(items, bulletType="1", leftIndent=18, bulletColor=BLUE))
|
|
371
|
+
story.append(Spacer(1, 3))
|
|
372
|
+
continue
|
|
373
|
+
|
|
374
|
+
paragraph_lines = [stripped]
|
|
375
|
+
index += 1
|
|
376
|
+
while index < len(lines):
|
|
377
|
+
candidate = lines[index].strip()
|
|
378
|
+
if (
|
|
379
|
+
not candidate
|
|
380
|
+
or candidate == "<!-- pagebreak -->"
|
|
381
|
+
or candidate.startswith(("#", "|", ">", "```", "- "))
|
|
382
|
+
or re.match(r"^\d+\.\s+", candidate)
|
|
383
|
+
):
|
|
384
|
+
break
|
|
385
|
+
paragraph_lines.append(candidate)
|
|
386
|
+
index += 1
|
|
387
|
+
story.append(Paragraph(inline_markup(" ".join(paragraph_lines)), style["body"]))
|
|
388
|
+
return story
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def draw_page(canvas_obj: canvas.Canvas, doc: SimpleDocTemplate, title: str) -> None:
|
|
392
|
+
canvas_obj.saveState()
|
|
393
|
+
canvas_obj.setStrokeColor(MID_GRAY)
|
|
394
|
+
canvas_obj.setLineWidth(0.35)
|
|
395
|
+
canvas_obj.line(LEFT, PAGE_HEIGHT - 13 * mm, PAGE_WIDTH - RIGHT, PAGE_HEIGHT - 13 * mm)
|
|
396
|
+
canvas_obj.setFont("WorkflowBody", 7)
|
|
397
|
+
canvas_obj.setFillColor(MUTED)
|
|
398
|
+
canvas_obj.drawString(LEFT, PAGE_HEIGHT - 10 * mm, title[:74])
|
|
399
|
+
canvas_obj.drawRightString(PAGE_WIDTH - RIGHT, PAGE_HEIGHT - 10 * mm, f"beta.11 | {PACKAGE_VERSION}")
|
|
400
|
+
canvas_obj.line(LEFT, 11 * mm, PAGE_WIDTH - RIGHT, 11 * mm)
|
|
401
|
+
canvas_obj.drawString(LEFT, 7 * mm, "Codex Workflow V2")
|
|
402
|
+
canvas_obj.drawRightString(PAGE_WIDTH - RIGHT, 7 * mm, f"Страница {doc.page}")
|
|
403
|
+
canvas_obj.restoreState()
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def build_pdf(source_path: Path, output_path: Path) -> None:
|
|
407
|
+
metadata, lines = parse_source(source_path)
|
|
408
|
+
required = ("title", "subtitle", "part", "document_version", "date", "subject")
|
|
409
|
+
missing = [key for key in required if key not in metadata]
|
|
410
|
+
if missing:
|
|
411
|
+
raise RuntimeError(f"Missing metadata in {source_path}: {', '.join(missing)}")
|
|
412
|
+
style = styles()
|
|
413
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
414
|
+
doc = SimpleDocTemplate(
|
|
415
|
+
str(output_path),
|
|
416
|
+
pagesize=A4,
|
|
417
|
+
leftMargin=LEFT,
|
|
418
|
+
rightMargin=RIGHT,
|
|
419
|
+
topMargin=TOP,
|
|
420
|
+
bottomMargin=BOTTOM,
|
|
421
|
+
title=metadata["title"],
|
|
422
|
+
author="Codex Workflow Maintainers",
|
|
423
|
+
subject=metadata["subject"],
|
|
424
|
+
creator="codex-workflow-v2 deterministic PDF generator",
|
|
425
|
+
pageCompression=1,
|
|
426
|
+
)
|
|
427
|
+
title_block = [
|
|
428
|
+
Spacer(1, 28 * mm),
|
|
429
|
+
Paragraph(inline_markup(metadata["part"].upper()), style["part"]),
|
|
430
|
+
Paragraph(inline_markup(metadata["title"]), style["title"]),
|
|
431
|
+
Paragraph(inline_markup(metadata["subtitle"]), style["subtitle"]),
|
|
432
|
+
Spacer(1, 8 * mm),
|
|
433
|
+
Table(
|
|
434
|
+
[[
|
|
435
|
+
Paragraph(
|
|
436
|
+
inline_markup(
|
|
437
|
+
f"Документ {metadata['document_version']} | Пакет {PACKAGE_VERSION} | {metadata['date']}"
|
|
438
|
+
),
|
|
439
|
+
style["callout"],
|
|
440
|
+
)
|
|
441
|
+
]],
|
|
442
|
+
colWidths=[CONTENT_WIDTH],
|
|
443
|
+
style=TableStyle([
|
|
444
|
+
("BACKGROUND", (0, 0), (-1, -1), PALE_BLUE),
|
|
445
|
+
("BOX", (0, 0), (-1, -1), 0.8, BLUE),
|
|
446
|
+
("LEFTPADDING", (0, 0), (-1, -1), 10),
|
|
447
|
+
("RIGHTPADDING", (0, 0), (-1, -1), 10),
|
|
448
|
+
("TOPPADDING", (0, 0), (-1, -1), 8),
|
|
449
|
+
("BOTTOMPADDING", (0, 0), (-1, -1), 8),
|
|
450
|
+
]),
|
|
451
|
+
),
|
|
452
|
+
Spacer(1, 55 * mm),
|
|
453
|
+
Paragraph(
|
|
454
|
+
inline_markup("Целевая среда: Codex App, один пользователь, одна машина, exact project-local package."),
|
|
455
|
+
style["small"],
|
|
456
|
+
),
|
|
457
|
+
PageBreak(),
|
|
458
|
+
]
|
|
459
|
+
story = title_block + markdown_story(lines, style)
|
|
460
|
+
doc.build(
|
|
461
|
+
story,
|
|
462
|
+
onFirstPage=lambda canv, current_doc: draw_page(canv, current_doc, metadata["title"]),
|
|
463
|
+
onLaterPages=lambda canv, current_doc: draw_page(canv, current_doc, metadata["title"]),
|
|
464
|
+
canvasmaker=InvariantCanvas,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def sha256(path: Path) -> str:
|
|
469
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def generate(output_dir: Path) -> None:
|
|
473
|
+
for source_name, output_name in DOCUMENTS:
|
|
474
|
+
build_pdf(SOURCE_DIR / source_name, output_dir / output_name)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def check() -> int:
|
|
478
|
+
with tempfile.TemporaryDirectory(prefix="codex-workflow-pdf-") as directory:
|
|
479
|
+
candidate_dir = Path(directory)
|
|
480
|
+
generate(candidate_dir)
|
|
481
|
+
stale = []
|
|
482
|
+
for _, output_name in DOCUMENTS:
|
|
483
|
+
tracked = OUTPUT_DIR / output_name
|
|
484
|
+
candidate = candidate_dir / output_name
|
|
485
|
+
if not tracked.is_file() or tracked.read_bytes() != candidate.read_bytes():
|
|
486
|
+
stale.append({
|
|
487
|
+
"file": str(tracked.relative_to(ROOT)),
|
|
488
|
+
"trackedSha256": sha256(tracked) if tracked.is_file() else None,
|
|
489
|
+
"generatedSha256": sha256(candidate),
|
|
490
|
+
})
|
|
491
|
+
print(json.dumps({"ok": not stale, "packageVersion": PACKAGE_VERSION, "stale": stale}, ensure_ascii=False))
|
|
492
|
+
return 0 if not stale else 1
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def main() -> int:
|
|
496
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
497
|
+
parser.add_argument("--check", action="store_true", help="fail when tracked PDFs differ from deterministic output")
|
|
498
|
+
args = parser.parse_args()
|
|
499
|
+
register_fonts()
|
|
500
|
+
if args.check:
|
|
501
|
+
return check()
|
|
502
|
+
generate(OUTPUT_DIR)
|
|
503
|
+
print(json.dumps({
|
|
504
|
+
"ok": True,
|
|
505
|
+
"packageVersion": PACKAGE_VERSION,
|
|
506
|
+
"outputs": [str((OUTPUT_DIR / output).relative_to(ROOT)) for _, output in DOCUMENTS],
|
|
507
|
+
}, ensure_ascii=False))
|
|
508
|
+
return 0
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
if __name__ == "__main__":
|
|
512
|
+
sys.exit(main())
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
|
|
6
|
+
const repositoryRoot = path.resolve(import.meta.dirname, '..');
|
|
7
|
+
const generator = path.join(repositoryRoot, 'scripts', 'generate-pdf-docs.py');
|
|
8
|
+
const configuredPython = process.env.CODEX_WORKFLOW_PDF_PYTHON?.trim() || null;
|
|
9
|
+
const bundledPython = path.join(
|
|
10
|
+
os.homedir(),
|
|
11
|
+
'.cache',
|
|
12
|
+
'codex-runtimes',
|
|
13
|
+
'codex-primary-runtime',
|
|
14
|
+
'dependencies',
|
|
15
|
+
'python',
|
|
16
|
+
'bin',
|
|
17
|
+
'python3',
|
|
18
|
+
);
|
|
19
|
+
const candidates = [configuredPython, bundledPython, 'python3'].filter((value, index, values) =>
|
|
20
|
+
value && values.indexOf(value) === index,
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
let selected = null;
|
|
24
|
+
const diagnostics = [];
|
|
25
|
+
for (const candidate of candidates) {
|
|
26
|
+
if (path.isAbsolute(candidate) && !existsSync(candidate)) {
|
|
27
|
+
diagnostics.push({ candidate, reason: 'not-found' });
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const probe = spawnSync(candidate, ['-c', 'import reportlab'], { encoding: 'utf8' });
|
|
31
|
+
if (probe.status === 0) {
|
|
32
|
+
selected = candidate;
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
diagnostics.push({
|
|
36
|
+
candidate,
|
|
37
|
+
reason: probe.error?.code ?? 'reportlab-unavailable',
|
|
38
|
+
stderr: probe.stderr?.trim() || null,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!selected) {
|
|
43
|
+
process.stderr.write(`${JSON.stringify({
|
|
44
|
+
ok: false,
|
|
45
|
+
error: 'PDF_PYTHON_UNAVAILABLE',
|
|
46
|
+
required: 'Python 3 with reportlab 4.4.x',
|
|
47
|
+
diagnostics,
|
|
48
|
+
})}\n`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const result = spawnSync(selected, [generator, ...process.argv.slice(2)], {
|
|
53
|
+
cwd: repositoryRoot,
|
|
54
|
+
encoding: 'utf8',
|
|
55
|
+
});
|
|
56
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
57
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
58
|
+
if (result.error) {
|
|
59
|
+
process.stderr.write(`${JSON.stringify({ ok: false, error: result.error.message })}\n`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
process.exit(result.status ?? 1);
|