@zzclub/pipeline 0.2.0 → 0.4.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 (194) hide show
  1. package/README.md +366 -170
  2. package/package.json +4 -7
  3. package/src/adapter-loader.ts +138 -0
  4. package/src/adapter-types.ts +172 -0
  5. package/src/adapters/builtin-image-renderer.ts +353 -0
  6. package/src/adapters/builtin-markdown-renderer.ts +63 -0
  7. package/src/adapters/index.ts +2 -0
  8. package/src/args.test.ts +118 -0
  9. package/src/args.ts +90 -0
  10. package/src/cli.ts +51 -0
  11. package/src/commands/abandon.test.ts +104 -0
  12. package/src/commands/abandon.ts +202 -0
  13. package/src/commands/attach-body-images.ts +88 -0
  14. package/src/commands/attach-body.ts +86 -0
  15. package/src/commands/attach-newspic-spec.ts +41 -0
  16. package/src/commands/checkpoint.test.ts +91 -0
  17. package/src/commands/checkpoint.ts +66 -0
  18. package/src/commands/config.ts +106 -0
  19. package/src/commands/cos-upload.ts +62 -0
  20. package/src/commands/doctor.ts +66 -0
  21. package/src/commands/find-run.ts +55 -0
  22. package/src/commands/hermes-metrics.ts +226 -0
  23. package/src/commands/imgx.ts +60 -0
  24. package/src/commands/ingest-handoff.ts +509 -0
  25. package/src/commands/init.ts +126 -0
  26. package/src/commands/prepare-finalize.ts +348 -0
  27. package/src/commands/prepare.ts +310 -0
  28. package/src/commands/publish.ts +147 -0
  29. package/src/commands/reconcile.ts +36 -0
  30. package/src/commands/render.ts +244 -0
  31. package/src/commands/reset.test.ts +151 -0
  32. package/src/commands/reset.ts +159 -0
  33. package/src/commands/review.ts +77 -0
  34. package/src/commands/status.ts +68 -0
  35. package/src/commands/sync-blog.ts +39 -0
  36. package/src/commands/tasks.ts +50 -0
  37. package/src/commands/wechat-export.ts +68 -0
  38. package/src/config.ts +0 -0
  39. package/src/imgx/ascii-portrait.ts +77 -0
  40. package/src/imgx/assets/fonts/AlimamaShuHeiTi-Bold.ttf +0 -0
  41. package/src/imgx/assets/fonts/LXGWNeoZhiSongPlus.ttf +0 -0
  42. package/src/imgx/assets/fonts/LXGWWenKai-Regular.ttf +0 -0
  43. package/src/imgx/assets/icons/logo.png +0 -0
  44. package/src/imgx/assets/icons/logo.svg +8 -0
  45. package/{dist/assets → src}/imgx/assets/templates/longform-3-4.html +1 -1
  46. package/{dist/assets → src}/imgx/assets/templates/poster-3-4.html +69 -30
  47. package/{dist/assets → src}/imgx/assets/templates/wechat-cover-split.html +58 -27
  48. package/src/imgx/cli.ts +57 -0
  49. package/src/imgx/geometry.ts +68 -0
  50. package/src/imgx/index.ts +5 -0
  51. package/src/imgx/longform-theme.ts +243 -0
  52. package/src/imgx/poster-recipe.ts +81 -0
  53. package/src/imgx/pretext-adapter.ts +14 -0
  54. package/src/imgx/pretext-runtime.ts +61 -0
  55. package/src/imgx/references/ascii-portrait-3-4.md +59 -0
  56. package/src/imgx/references/longform-3-4.md +173 -0
  57. package/src/imgx/references/poster-3-4.md +87 -0
  58. package/src/imgx/references/tips-3-4.md +69 -0
  59. package/src/imgx/references/wechat-cover-split.md +89 -0
  60. package/src/imgx/references/x-like-posts.md +139 -0
  61. package/src/imgx/render-article.test.ts +53 -0
  62. package/src/imgx/render-article.ts +1364 -0
  63. package/src/imgx/render-ascii-portrait.ts +53 -0
  64. package/src/imgx/render-card.ts +232 -0
  65. package/src/imgx/render-x-like-posts.ts +235 -0
  66. package/src/imgx/runtime.ts +403 -0
  67. package/src/imgx/typographic-scale.test.ts +56 -0
  68. package/src/imgx/typographic-scale.ts +44 -0
  69. package/src/output.ts +632 -0
  70. package/src/plugins.ts +101 -0
  71. package/src/profiles.ts +149 -0
  72. package/src/providers/blog.test.ts +58 -0
  73. package/src/providers/blog.ts +154 -0
  74. package/src/providers/cos.ts +183 -0
  75. package/src/providers/index.test.ts +26 -0
  76. package/src/providers/index.ts +185 -0
  77. package/src/providers/wechat.test.ts +215 -0
  78. package/src/providers/wechat.ts +548 -0
  79. package/src/routes.ts +287 -0
  80. package/src/runtime-paths.ts +179 -0
  81. package/src/schema/config.ts +133 -0
  82. package/src/schema/state.ts +376 -0
  83. package/src/spawn.ts +103 -0
  84. package/src/state.ts +265 -0
  85. package/src/task-manager.ts +657 -0
  86. package/src/task-views.ts +222 -0
  87. package/src/text.test.ts +689 -0
  88. package/src/text.ts +683 -0
  89. package/src/wechat-preview/assets/fonts/SweiCurveLegCJKsc-Regular.woff +0 -0
  90. package/src/wechat-preview/browser/editor-export.css +241 -0
  91. package/src/wechat-preview/browser/editor-export.ts +103 -0
  92. package/src/wechat-preview/frontmatter-handler.ts +73 -0
  93. package/src/wechat-preview/index.test.ts +154 -0
  94. package/src/wechat-preview/index.ts +294 -0
  95. package/src/wechat-preview/themes.ts +141 -0
  96. package/src/wechat-preview/wechat-formatter.test.ts +288 -0
  97. package/src/wechat-preview/wechat-formatter.ts +1506 -0
  98. package/src/wechat-preview.test.ts +73 -0
  99. package/src/workflow-materials.test.ts +225 -0
  100. package/src/workflow-materials.ts +217 -0
  101. package/src/workflow.test.ts +2087 -0
  102. package/dist/assets/imgx/assets/icons/avatar_jinx_cartoon.jpg +0 -0
  103. package/dist/assets/imgx/assets/icons/fishbone-logo-square.png +0 -0
  104. package/dist/assets/imgx/assets/icons/fishbone-logo.jpg +0 -0
  105. package/dist/assets/imgx/assets/icons/fishbone-logo.png +0 -0
  106. package/dist/assets/imgx/assets/icons/openclaw-logo.svg +0 -22
  107. package/dist/assets/imgx/assets/icons/zzclub-logo-black.jpg +0 -0
  108. package/dist/assets/imgx/assets/icons/zzclub-logo-gray.svg +0 -25
  109. package/dist/cli.js +0 -432
  110. package/dist/node_modules/@chenglou/pretext/dist/analysis.d.ts +0 -33
  111. package/dist/node_modules/@chenglou/pretext/dist/analysis.js +0 -1063
  112. package/dist/node_modules/@chenglou/pretext/dist/bidi.d.ts +0 -1
  113. package/dist/node_modules/@chenglou/pretext/dist/bidi.js +0 -175
  114. package/dist/node_modules/@chenglou/pretext/dist/generated/bidi-data.d.ts +0 -4
  115. package/dist/node_modules/@chenglou/pretext/dist/generated/bidi-data.js +0 -979
  116. package/dist/node_modules/@chenglou/pretext/dist/layout.d.ts +0 -70
  117. package/dist/node_modules/@chenglou/pretext/dist/layout.js +0 -496
  118. package/dist/node_modules/@chenglou/pretext/dist/line-break.d.ts +0 -36
  119. package/dist/node_modules/@chenglou/pretext/dist/line-break.js +0 -820
  120. package/dist/node_modules/@chenglou/pretext/dist/measurement.d.ts +0 -28
  121. package/dist/node_modules/@chenglou/pretext/dist/measurement.js +0 -219
  122. package/dist/node_modules/@chenglou/pretext/dist/rich-inline.d.ts +0 -51
  123. package/dist/node_modules/@chenglou/pretext/dist/rich-inline.js +0 -401
  124. /package/{dist/assets → src}/imgx/assets/browser/obstacle-flow.d.ts +0 -0
  125. /package/{dist/assets → src}/imgx/assets/browser/obstacle-flow.js +0 -0
  126. /package/{dist/assets → src}/imgx/assets/icons/clover.svg +0 -0
  127. /package/{dist/assets → src}/imgx/assets/templates/ascii-portrait-3-4.html +0 -0
  128. /package/{dist/assets → src}/imgx/assets/templates/ascii-portrait-tile.html +0 -0
  129. /package/{dist/assets → src}/imgx/assets/templates/tips-3-4.html +0 -0
  130. /package/{dist/assets → src}/imgx/assets/templates/x-like-posts.html +0 -0
  131. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/.vite/manifest.json +0 -0
  132. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  133. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  134. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  135. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  136. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  137. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  138. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  139. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  140. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  141. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  142. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  143. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  144. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  145. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  146. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  147. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  148. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  149. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  150. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  151. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  152. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  153. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  154. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  155. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  156. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  157. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  158. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  159. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  160. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  161. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  162. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  163. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  164. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  165. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  166. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  167. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  168. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  169. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  170. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  171. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  172. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  173. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  174. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  175. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  176. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  177. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  178. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  179. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  180. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  181. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  182. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  183. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  184. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  185. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  186. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  187. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  188. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  189. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  190. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  191. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/SweiCurveLegCJKsc-Regular-B3Ut5bwH.woff +0 -0
  192. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/style-BYNXnUvc.css +0 -0
  193. /package/{dist/assets → src}/wechat-preview/assets/browser-dist/editor-export.js +0 -0
  194. /package/{dist/assets → src}/wechat-preview/assets/templates/export-shell.html +0 -0
package/src/state.ts ADDED
@@ -0,0 +1,265 @@
1
+ /**
2
+ * state.ts — TypeScript types and CRUD for workflow-state.json
3
+ *
4
+ * Authoritative source: state-contract.md
5
+ * Design: body text never enters state; only recovery-essential facts are persisted.
6
+ *
7
+ * Types and validation are powered by Zod schemas in schema/state.ts.
8
+ */
9
+
10
+ import { readFile, writeFile, mkdir } from "fs/promises";
11
+ import { dirname } from "path";
12
+ import {
13
+ WorkflowStateSchema,
14
+ NewspicRenderSpecSchema,
15
+ } from "./schema/state";
16
+
17
+ import type {
18
+ WorkflowState,
19
+ NewspicRenderSpec,
20
+ PhaseName,
21
+ } from "./schema/state";
22
+
23
+ // ── Re-export types from schema ───────────────────────────────────
24
+
25
+ export type {
26
+ Mode,
27
+ PhaseName,
28
+ PhaseStatus,
29
+ PhaseEntry,
30
+ Phase,
31
+ TaskKind,
32
+ ContentForm,
33
+ ContentOrigin,
34
+ Target,
35
+ IntentRequires,
36
+ Intent,
37
+ NewspicPaginationMode,
38
+ NewspicPageSpec,
39
+ NewspicRenderSpec,
40
+ RoutePrimary,
41
+ AccountVisualParams,
42
+ Route,
43
+ StyleMode,
44
+ Authoring,
45
+ Metadata,
46
+ Artifacts,
47
+ ImagePlanStatus,
48
+ ImagePlan,
49
+ BodyInputScope,
50
+ BodyInputStatus,
51
+ BodyInputReceived,
52
+ BodyInputs,
53
+ RenderAssetKind,
54
+ RenderAsset,
55
+ Images,
56
+ PublishResultStatus,
57
+ PublishResult,
58
+ PublishResults,
59
+ ContentReviewStatus,
60
+ ContentReview,
61
+ HandoffResearchPolicy,
62
+ HandoffAuthoringPolicy,
63
+ HandoffReviewPolicy,
64
+ Handoff,
65
+ WorkflowState,
66
+ } from "./schema/state";
67
+
68
+ export { WorkflowStateSchema, NewspicRenderSpecSchema } from "./schema/state";
69
+
70
+ // ── Defaults ──────────────────────────────────────────────────────
71
+
72
+ export function defaultState(): WorkflowState {
73
+ return WorkflowStateSchema.parse({});
74
+ }
75
+
76
+ // Backward-compatible sub-defaults used by commands
77
+ const _defaults = defaultState();
78
+
79
+ export const defaultPhase = () => structuredClone(_defaults.phase);
80
+ export const defaultIntent = () => structuredClone(_defaults.intent);
81
+ export const defaultRoute = () => structuredClone(_defaults.route);
82
+ export const defaultAuthoring = () => structuredClone(_defaults.authoring);
83
+ export const defaultMetadata = () => structuredClone(_defaults.metadata);
84
+ export const defaultArtifacts = () => structuredClone(_defaults.artifacts);
85
+ export const defaultImagePlan = () => structuredClone(_defaults.images.plan);
86
+ export const defaultBodyInputs = () => structuredClone(_defaults.images.body_inputs);
87
+ export const defaultImages = () => structuredClone(_defaults.images);
88
+ export const defaultPublish = () => structuredClone(_defaults.publish);
89
+ export const defaultContentReview = () => structuredClone(_defaults.content_review);
90
+ export const defaultHandoff = () => structuredClone(_defaults.handoff);
91
+ export const defaultPhaseEntry = () => structuredClone(_defaults.phase.prepare);
92
+
93
+ export function defaultNewspicRenderSpec(): NewspicRenderSpec {
94
+ return NewspicRenderSpecSchema.parse({});
95
+ }
96
+
97
+ // ── Normalize ─────────────────────────────────────────────────────
98
+
99
+ export function normalizeNewspicRenderSpec(raw: unknown): NewspicRenderSpec {
100
+ return NewspicRenderSpecSchema.parse(raw ?? {});
101
+ }
102
+
103
+ // ── CRUD ──────────────────────────────────────────────────────────
104
+
105
+ /**
106
+ * Read and parse a workflow state JSON file.
107
+ * Throws if file doesn't exist or JSON is malformed.
108
+ */
109
+ export async function readState(path: string): Promise<WorkflowState> {
110
+ const raw = await readFile(path, "utf-8");
111
+ const parsed = JSON.parse(raw);
112
+ return WorkflowStateSchema.parse(parsed);
113
+ }
114
+
115
+ /**
116
+ * Write workflow state to JSON file (pretty-printed).
117
+ * Creates parent directories if needed.
118
+ */
119
+ export async function writeState(
120
+ path: string,
121
+ state: WorkflowState,
122
+ ): Promise<void> {
123
+ const now = new Date().toISOString();
124
+ if (!state.created_at) {
125
+ state.created_at = now;
126
+ }
127
+ state.updated_at = now;
128
+ await mkdir(dirname(path), { recursive: true });
129
+ const json = JSON.stringify(state, null, 2) + "\n";
130
+ await writeFile(path, json, "utf-8");
131
+ }
132
+
133
+ /**
134
+ * Read state, apply a mutation function, write back.
135
+ * Returns the updated state.
136
+ */
137
+ export async function updateState(
138
+ path: string,
139
+ mutate: (state: WorkflowState) => void,
140
+ ): Promise<WorkflowState> {
141
+ const state = await readState(path);
142
+ mutate(state);
143
+ await writeState(path, state);
144
+ return state;
145
+ }
146
+
147
+ // ── Run ID ────────────────────────────────────────────────────────
148
+
149
+ /**
150
+ * Generate a run ID in the format YYYYMMDD-HHmmss.
151
+ */
152
+ export function generateRunId(): string {
153
+ const now = new Date();
154
+ const pad2 = (n: number) => String(n).padStart(2, "0");
155
+ const y = now.getFullYear();
156
+ const m = pad2(now.getMonth() + 1);
157
+ const d = pad2(now.getDate());
158
+ const h = pad2(now.getHours());
159
+ const mi = pad2(now.getMinutes());
160
+ const s = pad2(now.getSeconds());
161
+ const random = crypto.randomUUID().replace(/-/g, "").slice(0, 6);
162
+ return `${y}${m}${d}-${h}${mi}${s}-${random}`;
163
+ }
164
+
165
+ // ── Validation helpers ────────────────────────────────────────────
166
+
167
+ export interface ValidationError {
168
+ field: string;
169
+ message: string;
170
+ }
171
+
172
+ /**
173
+ * Validate essential state fields for a given phase.
174
+ * Returns an array of validation errors (empty = valid).
175
+ */
176
+ export function validateForPhase(
177
+ state: WorkflowState,
178
+ phase: PhaseName,
179
+ ): ValidationError[] {
180
+ const errors: ValidationError[] = [];
181
+ const publishRoutes = [state.route.primary, ...state.route.extras];
182
+ const needsWechatArticle = publishRoutes.includes("wechat-article");
183
+
184
+ if (!state.run_id) {
185
+ errors.push({ field: "run_id", message: "Missing run_id" });
186
+ }
187
+ if (!state.workspace_root) {
188
+ errors.push({
189
+ field: "workspace_root",
190
+ message: "Missing workspace_root",
191
+ });
192
+ }
193
+
194
+ if (phase === "render" || phase === "publish") {
195
+ if (!state.asset_path) {
196
+ errors.push({ field: "asset_path", message: "Missing asset_path" });
197
+ }
198
+ if (!state.state_path) {
199
+ errors.push({ field: "state_path", message: "Missing state_path" });
200
+ }
201
+ if (!state.metadata.title) {
202
+ errors.push({ field: "metadata.title", message: "Missing title" });
203
+ }
204
+ if (!state.metadata.slug) {
205
+ errors.push({ field: "metadata.slug", message: "Missing slug" });
206
+ }
207
+ if (!state.metadata.date) {
208
+ errors.push({ field: "metadata.date", message: "Missing date" });
209
+ }
210
+ if (!state.route.primary) {
211
+ errors.push({
212
+ field: "route.primary",
213
+ message: "Missing primary route",
214
+ });
215
+ }
216
+ if (state.content_review.status !== "passed") {
217
+ errors.push({
218
+ field: "content_review.status",
219
+ message: `Content review must pass before ${phase}; current status is ${state.content_review.status}`,
220
+ });
221
+ }
222
+ }
223
+
224
+ if (phase === "publish") {
225
+ if (
226
+ state.route.primary !== "blog" &&
227
+ state.images.plan.needed &&
228
+ state.images.plan.status !== "rendered" &&
229
+ state.images.plan.status !== "skipped"
230
+ ) {
231
+ errors.push({
232
+ field: "images.plan.status",
233
+ message: `Images needed but status is ${state.images.plan.status}`,
234
+ });
235
+ }
236
+ if (
237
+ needsWechatArticle &&
238
+ state.images.body_inputs.scope === "article" &&
239
+ state.images.body_inputs.status === "pending"
240
+ ) {
241
+ errors.push({
242
+ field: "images.body_inputs.status",
243
+ message: "Wechat article body images are still pending user input",
244
+ });
245
+ }
246
+ }
247
+
248
+ return errors;
249
+ }
250
+
251
+ // ── State path helpers ────────────────────────────────────────────
252
+
253
+ /**
254
+ * Get temporary run state path.
255
+ */
256
+ export function getRunStatePath(workspaceRoot: string, runId: string): string {
257
+ return `${workspaceRoot}/.zzhub-media/runs/${runId}.json`;
258
+ }
259
+
260
+ /**
261
+ * Get canonical state path from asset path.
262
+ */
263
+ export function getCanonicalStatePath(assetPath: string): string {
264
+ return `${assetPath}/workflow-state.json`;
265
+ }