codex-workflow-v2 2.0.0-beta.10 → 2.0.0-beta.12

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