takt-marp 0.1.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 (69) hide show
  1. package/README.ja.md +108 -0
  2. package/README.md +108 -0
  3. package/bin/takt-marp.mjs +24 -0
  4. package/fixtures/marp-slide-workflow/_workflow-smoke/README.md +23 -0
  5. package/fixtures/marp-slide-workflow/_workflow-smoke/brief.md +44 -0
  6. package/marp.config.mjs +3 -0
  7. package/package.json +56 -0
  8. package/scripts/lib/takt-marp-cli.mjs +199 -0
  9. package/scripts/lib/takt-marp-project-init.mjs +81 -0
  10. package/scripts/lib/takt-marp-project-templates.mjs +93 -0
  11. package/scripts/lib/takt-marp-runtime-context.mjs +24 -0
  12. package/scripts/lib/takt-marp-slide-workflow.mjs +453 -0
  13. package/scripts/takt-marp-approve-slide-workflow-state.mjs +37 -0
  14. package/scripts/takt-marp-build-slide-artifact.mjs +151 -0
  15. package/scripts/takt-marp-check-slide-workflow-state.mjs +41 -0
  16. package/scripts/takt-marp-render-slide-workflow-evidence.mjs +70 -0
  17. package/scripts/takt-marp-run-slide-workflow.mjs +435 -0
  18. package/scripts/takt-marp-sync-project-templates.mjs +125 -0
  19. package/scripts/takt-marp-validate-global-install.mjs +391 -0
  20. package/scripts/takt-marp-validate-package-boundary.mjs +276 -0
  21. package/scripts/takt-marp-validate-slide-workflow-foundation.mjs +571 -0
  22. package/scripts/takt-marp-validate-slide-workflow-smoke.mjs +1935 -0
  23. package/scripts/takt-marp-verify-delivery-artifacts.mjs +181 -0
  24. package/scripts/takt-marp-verify-render-evidence-metadata.mjs +133 -0
  25. package/templates/project/facets/instructions/takt-marp-ai-antipattern-fix.md +47 -0
  26. package/templates/project/facets/instructions/takt-marp-ai-antipattern-review.md +37 -0
  27. package/templates/project/facets/instructions/takt-marp-compose-fix.md +25 -0
  28. package/templates/project/facets/instructions/takt-marp-compose-review.md +30 -0
  29. package/templates/project/facets/instructions/takt-marp-compose-slides.md +35 -0
  30. package/templates/project/facets/instructions/takt-marp-compose-work-summary.md +23 -0
  31. package/templates/project/facets/instructions/takt-marp-deliver-build.md +30 -0
  32. package/templates/project/facets/instructions/takt-marp-deliver-fix.md +25 -0
  33. package/templates/project/facets/instructions/takt-marp-deliver-verify.md +25 -0
  34. package/templates/project/facets/instructions/takt-marp-design-system.md +37 -0
  35. package/templates/project/facets/instructions/takt-marp-intake.md +15 -0
  36. package/templates/project/facets/instructions/takt-marp-normalize-brief.md +24 -0
  37. package/templates/project/facets/instructions/takt-marp-plan-fix.md +26 -0
  38. package/templates/project/facets/instructions/takt-marp-plan-review.md +24 -0
  39. package/templates/project/facets/instructions/takt-marp-plan-work-summary.md +24 -0
  40. package/templates/project/facets/instructions/takt-marp-plan.md +26 -0
  41. package/templates/project/facets/instructions/takt-marp-polish-fix.md +25 -0
  42. package/templates/project/facets/instructions/takt-marp-polish-inspect.md +25 -0
  43. package/templates/project/facets/instructions/takt-marp-render-evidence.md +35 -0
  44. package/templates/project/facets/instructions/takt-marp-supervise-command.md +58 -0
  45. package/templates/project/facets/instructions/takt-marp-visual-generate.md +26 -0
  46. package/templates/project/facets/knowledge/takt-marp-repo-conventions.md +119 -0
  47. package/templates/project/facets/output-contracts/takt-marp-ai-antipattern-fix.md +48 -0
  48. package/templates/project/facets/output-contracts/takt-marp-ai-antipattern-review.md +43 -0
  49. package/templates/project/facets/output-contracts/takt-marp-command-fix.md +32 -0
  50. package/templates/project/facets/output-contracts/takt-marp-command-review.md +32 -0
  51. package/templates/project/facets/output-contracts/takt-marp-command-work.md +42 -0
  52. package/templates/project/facets/output-contracts/takt-marp-normalized-brief.md +31 -0
  53. package/templates/project/facets/output-contracts/takt-marp-slide-plan.md +30 -0
  54. package/templates/project/facets/output-contracts/takt-marp-supervision.md +45 -0
  55. package/templates/project/facets/personas/takt-marp-slide-planner.md +24 -0
  56. package/templates/project/facets/personas/takt-marp-slide-qa.md +23 -0
  57. package/templates/project/facets/personas/takt-marp-slide-reviewer.md +22 -0
  58. package/templates/project/facets/personas/takt-marp-slide-reviser.md +22 -0
  59. package/templates/project/facets/personas/takt-marp-slide-supervisor.md +24 -0
  60. package/templates/project/facets/personas/takt-marp-slide-writer.md +22 -0
  61. package/templates/project/facets/policies/takt-marp-general-slide-quality.md +91 -0
  62. package/templates/project/facets/policies/takt-marp-slide-quality.md +73 -0
  63. package/templates/project/facets/policies/takt-marp-svg-first-visual.md +66 -0
  64. package/templates/project/facets/policies/takt-marp-worker-boundary.md +32 -0
  65. package/templates/project/workflows/takt-marp-slide-ai-quality-gate.yaml +125 -0
  66. package/templates/project/workflows/takt-marp-slide-compose.yaml +209 -0
  67. package/templates/project/workflows/takt-marp-slide-deliver.yaml +164 -0
  68. package/templates/project/workflows/takt-marp-slide-plan.yaml +213 -0
  69. package/templates/project/workflows/takt-marp-slide-polish.yaml +158 -0
@@ -0,0 +1,1935 @@
1
+ #!/usr/bin/env node
2
+ import { cp, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { spawnSync } from "node:child_process";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import {
8
+ SlideWorkflowError,
9
+ approvalPath,
10
+ archiveCommandArtifacts,
11
+ cleanGeneratedOutputs,
12
+ downstreamCommands,
13
+ formatError,
14
+ parseFrontMatter,
15
+ readApproval,
16
+ readFrontMatter,
17
+ readSupervision,
18
+ resolveDeckTarget,
19
+ supervisionPath,
20
+ } from "./lib/takt-marp-slide-workflow.mjs";
21
+ import { runtimeExecutablePath } from "./lib/takt-marp-runtime-context.mjs";
22
+
23
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
24
+ const PACKAGE_ROOT = path.resolve(SCRIPT_DIR, "..");
25
+ const ROOT = process.cwd();
26
+ const FIXTURE_PATH = path.join(PACKAGE_ROOT, "fixtures", "marp-slide-workflow", "_workflow-smoke");
27
+ const RUNNER_SCRIPT = path.join(SCRIPT_DIR, "takt-marp-run-slide-workflow.mjs");
28
+ const DEFAULT_TARGET = "slides/_workflow-smoke";
29
+ const DEFAULT_SMOKE_PROVIDER = "mock";
30
+ const STATE_VALIDATION_TARGET = "slides/_workflow-smoke-state-validation";
31
+ const RENDER_VALIDATION_TARGET = "slides/_workflow-smoke-render-validation";
32
+ const WORKFLOW_COMMANDS = ["plan", "compose", "polish", "deliver"];
33
+ const SOURCE_FIXTURE_EXCLUDES = new Set(["README.md"]);
34
+ const WORKFLOW_COMMAND_TIMEOUT_MS = 45 * 60 * 1000;
35
+ const NODE_CHECK_TIMEOUT_MS = 2 * 60 * 1000;
36
+ const CAPTURE_MAX_BUFFER = 64 * 1024 * 1024;
37
+ const MOCK_GENERATED_AT = "2026-06-06T00:00:00.000Z";
38
+
39
+ async function main() {
40
+ const options = parseSmokeArgs(process.argv.slice(2));
41
+ if (options.help) {
42
+ printHelp();
43
+ return;
44
+ }
45
+
46
+ const commandLine = `node scripts/takt-marp-validate-slide-workflow-smoke.mjs${process.argv.slice(2).length > 0 ? ` ${process.argv.slice(2).join(" ")}` : ""}`;
47
+ const checks = [];
48
+ const commands = [commandLine];
49
+ const observedPaths = [];
50
+ const failures = [];
51
+ let currentCheckName = "setup:fixture-to-target";
52
+
53
+ let targetInfo;
54
+ try {
55
+ currentCheckName = "setup:fixture-to-target";
56
+ const setup = await setupSmokeDeck(options.target);
57
+ targetInfo = setup.targetInfo;
58
+ observedPaths.push(...setup.observedPaths);
59
+ checks.push(pass("setup:fixture-to-target", "Fixture source files copied into a clean smoke target."));
60
+ checks.push(pass("setup:generated-output-cleanup", "Generated output roots were cleaned before validation."));
61
+ currentCheckName = "failure-path:invalid-target";
62
+ const invalidTargetChecks = runInvalidTargetChecks(targetInfo);
63
+ checks.push(...invalidTargetChecks.checks);
64
+ commands.push(...invalidTargetChecks.commands);
65
+ currentCheckName = "failure-path:approval-preflight";
66
+ const approvalPreflightChecks = await runApprovalPreflightChecks(targetInfo);
67
+ checks.push(...approvalPreflightChecks.checks);
68
+ commands.push(...approvalPreflightChecks.commands);
69
+ currentCheckName = "approval-command:negative-checks";
70
+ const approvalCommandChecks = await runApprovalCommandNegativeChecks(targetInfo);
71
+ checks.push(...approvalCommandChecks.checks);
72
+ commands.push(...approvalCommandChecks.commands);
73
+ currentCheckName = "failure-path:compose-state-validation";
74
+ const composeStateValidationChecks = await runComposeStateValidationNegativeChecks();
75
+ checks.push(...composeStateValidationChecks.checks);
76
+ currentCheckName = "failure-path:convergence-routing";
77
+ const convergenceChecks = await runConvergenceRouteChecks();
78
+ checks.push(...convergenceChecks.checks);
79
+ observedPaths.push(...convergenceChecks.observedPaths);
80
+ currentCheckName = "failure-path:render-evidence-boundary";
81
+ const renderEvidenceBoundaryChecks = await runRenderEvidenceBoundaryChecks();
82
+ checks.push(...renderEvidenceBoundaryChecks.checks);
83
+ commands.push(...renderEvidenceBoundaryChecks.commands);
84
+ observedPaths.push(...renderEvidenceBoundaryChecks.observedPaths);
85
+ currentCheckName = "sequence:workflow";
86
+ const planSequenceChecks = await runPlanSequenceChecks(targetInfo, { provider: options.provider });
87
+ checks.push(...planSequenceChecks.checks);
88
+ commands.push(...planSequenceChecks.commands);
89
+ observedPaths.push(...planSequenceChecks.observedPaths);
90
+ currentCheckName = "failure-path:force-invalidation";
91
+ const forceChecks = await runForceInvalidationChecks(targetInfo, { provider: options.provider });
92
+ checks.push(...forceChecks.checks);
93
+ commands.push(...forceChecks.commands);
94
+ observedPaths.push(...forceChecks.observedPaths);
95
+ currentCheckName = "failure-path:successful-rerun-rejection";
96
+ const rerunChecks = await runSuccessfulRerunRejectionChecks(targetInfo);
97
+ checks.push(...rerunChecks.checks);
98
+ commands.push(...rerunChecks.commands);
99
+ observedPaths.push(...rerunChecks.observedPaths);
100
+ currentCheckName = "failure-path:rejected-rerun-archive";
101
+ const rejectedRerunChecks = await runRejectedRerunArchiveChecks(targetInfo, { provider: options.provider });
102
+ checks.push(...rejectedRerunChecks.checks);
103
+ commands.push(...rejectedRerunChecks.commands);
104
+ observedPaths.push(...rejectedRerunChecks.observedPaths);
105
+ } catch (error) {
106
+ const reason = formatError(error);
107
+ failures.push(reason);
108
+ checks.push(fail(currentCheckName, reason));
109
+ }
110
+
111
+ if (targetInfo) {
112
+ const summaryPath = path.join(targetInfo.reviewPath, smokeSummaryFileName(options));
113
+ observedPaths.push(relativePath(summaryPath));
114
+ const summaryChecks = [
115
+ ...checks,
116
+ pass("summary:write-smoke-summary", "Smoke summary written."),
117
+ ];
118
+ await writeSummary(summaryPath, {
119
+ target: targetInfo.target,
120
+ provider: options.provider,
121
+ smokeMode: smokeMode(options),
122
+ result: failures.length === 0 ? "passed" : "failed",
123
+ commands,
124
+ checks: summaryChecks,
125
+ observedPaths,
126
+ failures,
127
+ keep: options.keep,
128
+ });
129
+ console.log(`Smoke summary: ${relativePath(summaryPath)}`);
130
+ }
131
+
132
+ if (failures.length > 0) {
133
+ for (const failure of failures) {
134
+ console.error(failure);
135
+ }
136
+ process.exit(1);
137
+ }
138
+ }
139
+
140
+ function runInvalidTargetChecks(targetInfo) {
141
+ const cases = [
142
+ {
143
+ name: "failure-path:invalid-target:brief-file",
144
+ target: `${targetInfo.target}/brief.md`,
145
+ reason: "brief.md command target was rejected before TAKT startup.",
146
+ },
147
+ {
148
+ name: "failure-path:invalid-target:markdown-file",
149
+ target: "README.md",
150
+ reason: "Markdown file command target was rejected before TAKT startup.",
151
+ },
152
+ {
153
+ name: "failure-path:invalid-target:outside-slides",
154
+ target: "fixtures",
155
+ reason: "Path outside slides/ was rejected before TAKT startup.",
156
+ },
157
+ ];
158
+
159
+ const checks = [];
160
+ const commands = [];
161
+ for (const item of cases) {
162
+ const command = `node scripts/takt-marp-run-slide-workflow.mjs plan ${JSON.stringify(item.target)}`;
163
+ commands.push(command);
164
+ const result = spawnSync(process.execPath, [RUNNER_SCRIPT, "plan", item.target], {
165
+ cwd: ROOT,
166
+ encoding: "utf8",
167
+ timeout: NODE_CHECK_TIMEOUT_MS,
168
+ maxBuffer: CAPTURE_MAX_BUFFER,
169
+ });
170
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
171
+ assert(result.status !== 0, `${item.name} unexpectedly succeeded`);
172
+ assert(output.includes("INVALID_TARGET:"), `${item.name} did not report INVALID_TARGET: ${output}`);
173
+ assert(output.includes("slides/<deck>"), `${item.name} did not explain expected target slides/<deck>: ${output}`);
174
+ assertNoWorkflowExecution(output, item.name);
175
+ checks.push(pass(item.name, `${item.reason} Observed ${firstLine(output)}`));
176
+ }
177
+
178
+ return Object.freeze({ checks: Object.freeze(checks), commands: Object.freeze(commands) });
179
+ }
180
+
181
+ async function runApprovalPreflightChecks(targetInfo) {
182
+ const checks = [];
183
+ const commands = [];
184
+
185
+ try {
186
+ await cleanApprovalPreflightState(targetInfo);
187
+
188
+ await writeSyntheticSupervision(targetInfo, "plan", {
189
+ state: "planned",
190
+ result: "passed",
191
+ workflowRunId: "smoke-plan-approved-state",
192
+ });
193
+ commands.push(assertPreflightFailure("failure-path:missing-plan-approval", "compose", targetInfo.target, "FILE_MISSING"));
194
+ checks.push(pass("failure-path:missing-plan-approval", "compose rejected missing plan approval before TAKT startup."));
195
+
196
+ await cleanApprovalPreflightState(targetInfo);
197
+ await writeSyntheticSupervision(targetInfo, "compose", {
198
+ state: "composed",
199
+ result: "passed",
200
+ workflowRunId: "smoke-compose-approved-state",
201
+ });
202
+ commands.push(assertPreflightFailure("failure-path:missing-compose-approval", "polish", targetInfo.target, "FILE_MISSING"));
203
+ checks.push(pass("failure-path:missing-compose-approval", "polish rejected missing compose approval before TAKT startup."));
204
+
205
+ await cleanApprovalPreflightState(targetInfo);
206
+ await writeSyntheticSupervision(targetInfo, "plan", {
207
+ state: "planned",
208
+ result: "rejected",
209
+ workflowRunId: "smoke-stale-plan-report",
210
+ });
211
+ commands.push(assertPreflightFailure("failure-path:stale-report-not-accepted", "compose", targetInfo.target, "STATE_NOT_PASSED"));
212
+ checks.push(pass("failure-path:stale-report-not-accepted", "stale plan report alone was not accepted as approved state."));
213
+
214
+ await cleanApprovalPreflightState(targetInfo);
215
+ await writeSyntheticSupervision(targetInfo, "plan", {
216
+ state: "planned",
217
+ result: "passed",
218
+ workflowRunId: "smoke-canonical-plan-run",
219
+ });
220
+ await writeSyntheticApproval(targetInfo, "plan", {
221
+ supervisionWorkflowRunId: "smoke-stale-plan-run",
222
+ });
223
+ commands.push(assertPreflightFailure("failure-path:stale-approval-mismatch", "compose", targetInfo.target, "FIELD_MISMATCH"));
224
+ checks.push(pass("failure-path:stale-approval-mismatch", "stale plan approval mismatching canonical supervision was rejected before TAKT startup."));
225
+ } finally {
226
+ await cleanApprovalPreflightState(targetInfo);
227
+ }
228
+
229
+ return Object.freeze({ checks: Object.freeze(checks), commands: Object.freeze(commands) });
230
+ }
231
+
232
+ async function runApprovalCommandNegativeChecks(targetInfo) {
233
+ const checks = [];
234
+ const commands = [];
235
+
236
+ try {
237
+ await cleanApprovalCommandState(targetInfo);
238
+
239
+ await writeSyntheticSupervision(targetInfo, "plan", {
240
+ state: "planned",
241
+ result: "passed",
242
+ workflowRunId: "smoke-approval-command-plan-run",
243
+ });
244
+ commands.push(assertApprovalCommandFailure("approval-command:missing-by-rejected", targetInfo.target, "plan", [], "Usage:"));
245
+ assertApprovalFileAbsent(targetInfo, "plan", "approval-command:missing-by-rejected");
246
+ checks.push(pass("approval-command:missing-by-rejected", "plan approval without --by failed without writing an approval file."));
247
+
248
+ await cleanApprovalCommandState(targetInfo);
249
+ await writeSyntheticSupervision(targetInfo, "polish", {
250
+ state: "polished",
251
+ result: "passed",
252
+ workflowRunId: "smoke-approval-command-polish-run",
253
+ });
254
+ commands.push(
255
+ assertApprovalCommandFailure("approval-command:polish-rejected", targetInfo.target, "polish", ["--by", "smoke-validation"], "APPROVAL_UNSUPPORTED:"),
256
+ );
257
+ assertApprovalFileAbsent(targetInfo, "polish", "approval-command:polish-rejected");
258
+ checks.push(pass("approval-command:polish-rejected", "polish approval was rejected without writing an approval file."));
259
+
260
+ await cleanApprovalCommandState(targetInfo);
261
+ await writeSyntheticSupervision(targetInfo, "deliver", {
262
+ state: "delivered",
263
+ result: "passed",
264
+ workflowRunId: "smoke-approval-command-deliver-run",
265
+ });
266
+ commands.push(
267
+ assertApprovalCommandFailure("approval-command:deliver-rejected", targetInfo.target, "deliver", ["--by", "smoke-validation"], "APPROVAL_UNSUPPORTED:"),
268
+ );
269
+ assertApprovalFileAbsent(targetInfo, "deliver", "approval-command:deliver-rejected");
270
+ checks.push(pass("approval-command:deliver-rejected", "deliver approval was rejected without writing an approval file."));
271
+
272
+ } finally {
273
+ await cleanApprovalCommandState(targetInfo);
274
+ }
275
+
276
+ assertApprovalCommandStateAbsent(targetInfo, "approval-command:no-approval-pollution");
277
+ checks.push(pass("approval-command:no-approval-pollution", "approval negative checks left no approval or synthetic supervision state for later sequence checks."));
278
+
279
+ return Object.freeze({ checks: Object.freeze(checks), commands: Object.freeze(commands) });
280
+ }
281
+
282
+ async function runComposeStateValidationNegativeChecks() {
283
+ const checks = [];
284
+ const targetPath = path.join(ROOT, STATE_VALIDATION_TARGET);
285
+ const targetInfo = resolveDeckTarget(await setupSyntheticValidationDeck(targetPath), { root: ROOT });
286
+
287
+ try {
288
+ await writeFile(
289
+ supervisionPath(targetInfo, "compose"),
290
+ [
291
+ "command: compose",
292
+ `target: ${targetInfo.target}`,
293
+ "state: composed",
294
+ "result: passed",
295
+ "",
296
+ ].join("\n"),
297
+ "utf8",
298
+ );
299
+ await assertSlideWorkflowFailure(
300
+ "failure-path:compose-invalid-front-matter",
301
+ () => readSupervision(targetInfo, "compose"),
302
+ "FRONT_MATTER_MISSING",
303
+ );
304
+ checks.push(pass("failure-path:compose-invalid-front-matter", "compose supervision without front matter was rejected by state validation."));
305
+
306
+ await writeSyntheticSupervision(targetInfo, "compose", {
307
+ state: "planned",
308
+ result: "passed",
309
+ workflowRunId: "smoke-stale-compose-report",
310
+ });
311
+ await assertSlideWorkflowFailure(
312
+ "failure-path:stale-compose-supervision-mismatch",
313
+ () => readSupervision(targetInfo, "compose"),
314
+ "STATE_MISMATCH",
315
+ );
316
+ checks.push(pass("failure-path:stale-compose-supervision-mismatch", "stale compose supervision with mismatched state was rejected by state validation."));
317
+
318
+ await writeSyntheticSupervision(targetInfo, "compose", {
319
+ state: "composed",
320
+ result: "passed",
321
+ workflowRunId: "smoke-canonical-compose-run",
322
+ });
323
+ const supervision = await readSupervision(targetInfo, "compose");
324
+ await writeSyntheticApproval(targetInfo, "compose", {
325
+ supervisionWorkflowRunId: "smoke-stale-compose-run",
326
+ });
327
+ await assertSlideWorkflowFailure(
328
+ "failure-path:stale-compose-approval-mismatch",
329
+ () => readApproval(targetInfo, "compose", supervision.data),
330
+ "FIELD_MISMATCH",
331
+ );
332
+ checks.push(pass("failure-path:stale-compose-approval-mismatch", "stale compose approval mismatching canonical supervision was rejected by state validation."));
333
+ } finally {
334
+ await rm(targetPath, { recursive: true, force: true });
335
+ }
336
+
337
+ return Object.freeze({ checks: Object.freeze(checks) });
338
+ }
339
+
340
+ async function runConvergenceRouteChecks() {
341
+ const checks = [];
342
+ const observedPaths = [];
343
+
344
+ const expectations = {
345
+ plan: {
346
+ cycle: ["review_plan", "fix_plan", "summarize_plan_work"],
347
+ healthyNext: "review_plan",
348
+ approvedNext: "supervise_plan",
349
+ removedMonitorStep: "monitor_plan_loop",
350
+ workStep: "summarize_plan_work",
351
+ gateStep: "ai_quality_gate_plan",
352
+ normalReviewStep: "review_plan",
353
+ replanStep: "summarize_plan_work",
354
+ },
355
+ compose: {
356
+ cycle: ["review_compose", "fix_compose", "summarize_compose_work"],
357
+ healthyNext: "review_compose",
358
+ approvedNext: "supervise_compose",
359
+ removedMonitorStep: "monitor_compose_loop",
360
+ workStep: "summarize_compose_work",
361
+ gateStep: "ai_quality_gate_compose",
362
+ normalReviewStep: "review_compose",
363
+ replanStep: "summarize_compose_work",
364
+ },
365
+ polish: {
366
+ cycle: ["inspect_render", "fix_polish", "render_evidence"],
367
+ healthyNext: "inspect_render",
368
+ approvedNext: "supervise_polish",
369
+ removedMonitorStep: "monitor_polish_loop",
370
+ workStep: "render_evidence",
371
+ gateStep: "ai_quality_gate_polish",
372
+ normalReviewStep: "inspect_render",
373
+ replanStep: "render_evidence",
374
+ },
375
+ deliver: {
376
+ cycle: ["verify_delivery", "fix_delivery", "build_delivery"],
377
+ healthyNext: "verify_delivery",
378
+ approvedNext: "supervise_delivery",
379
+ removedMonitorStep: "monitor_delivery_loop",
380
+ workStep: "build_delivery",
381
+ gateStep: "ai_quality_gate_deliver",
382
+ normalReviewStep: "verify_delivery",
383
+ replanStep: "build_delivery",
384
+ },
385
+ };
386
+
387
+ for (const command of WORKFLOW_COMMANDS) {
388
+ assertWorkflowLoopMonitor(command, expectations[command]);
389
+ assertAiGateWorkflowRoute(command, expectations[command]);
390
+ }
391
+ assertAiGateCallableWorkflowRules();
392
+ assertWorkflowDoctorPasses();
393
+ assertNoDeckLocalLoopMonitorFacets();
394
+ assertNoUnsupportedWorkflowCommandGateObjects();
395
+ observedPaths.push(
396
+ ...WORKFLOW_COMMANDS.map((command) => relativePath(path.join(ROOT, ".takt", "workflows", `takt-marp-slide-${command}.yaml`))),
397
+ relativePath(path.join(PACKAGE_ROOT, "scripts", "takt-marp-verify-render-evidence-metadata.mjs")),
398
+ relativePath(path.join(PACKAGE_ROOT, "scripts", "takt-marp-verify-delivery-artifacts.mjs")),
399
+ relativePath(path.join(PACKAGE_ROOT, "scripts", "takt-marp-render-slide-workflow-evidence.mjs")),
400
+ );
401
+ checks.push(pass("failure-path:convergence-workflow-loop-monitors", "TAKT loop_monitors guard review/fix cycles and route nonproductive loops to ABORT."));
402
+ checks.push(pass("sequence:ai-gate-workflow-routes", "AI antipattern gates sit between command work and normal review/inspection/verification with command-local replan routes."));
403
+ checks.push(pass("sequence:workflow-schema-compatible", "workflow YAML passes TAKT workflow doctor and avoids command quality gate objects rejected by string-only quality_gates schemas."));
404
+
405
+ return Object.freeze({ checks: Object.freeze(checks), observedPaths: Object.freeze(observedPaths) });
406
+ }
407
+
408
+ async function runRenderEvidenceBoundaryChecks() {
409
+ const checks = [];
410
+ const commands = [];
411
+ const observedPaths = [
412
+ relativePath(path.join(PACKAGE_ROOT, "scripts", "takt-marp-verify-render-evidence-metadata.mjs")),
413
+ ];
414
+ const targetPath = path.join(ROOT, RENDER_VALIDATION_TARGET);
415
+ const renderRoot = path.join(ROOT, ".takt", "render", path.basename(RENDER_VALIDATION_TARGET));
416
+
417
+ try {
418
+ await setupSyntheticRenderEvidenceDeck(targetPath);
419
+ await writeSyntheticRenderEvidenceMetadata({
420
+ target: RENDER_VALIDATION_TARGET,
421
+ htmlPng: { status: "passed", files: ["slide-1.png"] },
422
+ pdf: { status: "passed", file: "SLIDES.pdf" },
423
+ pdfRaster: { status: "degraded", reason: "pdftoppm not found", files: [] },
424
+ });
425
+ const degradedCommand = runNodeScript("failure-path:render-evidence-pdf-raster-degraded", "takt-marp-verify-render-evidence-metadata.mjs", [RENDER_VALIDATION_TARGET, "--cycle", "1"]);
426
+ commands.push(degradedCommand);
427
+ checks.push(pass("failure-path:render-evidence-pdf-raster-degraded", "pdf_raster degraded evidence with a reason is accepted as optional evidence."));
428
+
429
+ await writeSyntheticRenderEvidenceMetadata({
430
+ target: RENDER_VALIDATION_TARGET,
431
+ htmlPng: { status: "failed", files: [] },
432
+ pdf: { status: "passed", file: "SLIDES.pdf" },
433
+ pdfRaster: { status: "degraded", reason: "pdftoppm not found", files: [] },
434
+ });
435
+ const failedCommand = assertNodeScriptFailure(
436
+ "failure-path:render-evidence-html-png-failed",
437
+ "takt-marp-verify-render-evidence-metadata.mjs",
438
+ [RENDER_VALIDATION_TARGET, "--cycle", "1"],
439
+ "RENDER_EVIDENCE_INVALID",
440
+ );
441
+ commands.push(failedCommand);
442
+ checks.push(pass("failure-path:render-evidence-html-png-failed", "html_png failed evidence is rejected as smoke failure evidence."));
443
+ } finally {
444
+ await Promise.all([
445
+ rm(targetPath, { recursive: true, force: true }),
446
+ rm(renderRoot, { recursive: true, force: true }),
447
+ ]);
448
+ }
449
+
450
+ return Object.freeze({
451
+ checks: Object.freeze(checks),
452
+ commands: Object.freeze(commands),
453
+ observedPaths: Object.freeze(observedPaths),
454
+ });
455
+ }
456
+
457
+ async function runPlanSequenceChecks(targetInfo, options) {
458
+ const checks = [];
459
+ const commands = [];
460
+ const observedPaths = [];
461
+
462
+ const planCommand = await runWorkflowCommand("sequence:plan-command", "plan", targetInfo, options);
463
+ commands.push(planCommand);
464
+ const supervision = await readSupervision(targetInfo, "plan");
465
+ assert(supervision.data.state === "planned", `sequence:plan-supervision-state expected planned, got ${supervision.data.state}`);
466
+ assert(supervision.data.result === "passed", `sequence:plan-supervision-result expected passed, got ${supervision.data.result}`);
467
+ observedPaths.push(relativePath(supervision.filePath));
468
+ checks.push(pass("sequence:plan-command", "slide:plan completed for the smoke deck."));
469
+ checks.push(pass("sequence:plan-supervision", "plan-supervision.md exists with state planned and result passed."));
470
+
471
+ assertApprovalFileAbsent(targetInfo, "plan", "approval-command:workflow-only-non-generation");
472
+ checks.push(pass("approval-command:workflow-only-non-generation", "slide:plan did not generate plan-approval.md before explicit approval."));
473
+
474
+ const approveCommand = runWorkflowNodeScript("approval-command:plan-approved", "takt-marp-approve-slide-workflow-state.mjs", [targetInfo.target, "plan", "--by", "smoke-validation"]);
475
+ commands.push(approveCommand);
476
+ const approval = await readApproval(targetInfo, "plan", supervision.data);
477
+ assert(approval.data.approved_by === "smoke-validation", `approval-command:plan-approved expected approved_by smoke-validation, got ${approval.data.approved_by}`);
478
+ observedPaths.push(relativePath(approval.filePath));
479
+ checks.push(pass("approval-command:plan-approved", "slide:approve plan --by smoke-validation generated a matching plan approval file."));
480
+
481
+ const composeCommand = await runWorkflowCommand("sequence:compose-command", "compose", targetInfo, options);
482
+ commands.push(composeCommand);
483
+ const composeArtifactPaths = await assertComposeSourceArtifacts(targetInfo);
484
+ observedPaths.push(...composeArtifactPaths.map(relativePath));
485
+ checks.push(pass("sequence:compose-command", "slide:compose completed for the smoke deck after plan approval."));
486
+ checks.push(pass("sequence:compose-source-artifacts", "compose source artifacts exist: design-system.md, SLIDES.md, and images/*.svg."));
487
+
488
+ const composeSupervision = await readSupervision(targetInfo, "compose");
489
+ assert(composeSupervision.data.state === "composed", `sequence:compose-supervision-state expected composed, got ${composeSupervision.data.state}`);
490
+ assert(composeSupervision.data.result === "passed", `sequence:compose-supervision-result expected passed, got ${composeSupervision.data.result}`);
491
+ observedPaths.push(relativePath(composeSupervision.filePath));
492
+ checks.push(pass("sequence:compose-supervision", "compose-supervision.md exists with state composed and result passed."));
493
+
494
+ assertApprovalFileAbsent(targetInfo, "compose", "approval-command:compose-workflow-only-non-generation");
495
+ checks.push(pass("approval-command:compose-workflow-only-non-generation", "slide:compose did not generate compose-approval.md before explicit approval."));
496
+
497
+ const approveComposeCommand = runWorkflowNodeScript("approval-command:compose-approved", "takt-marp-approve-slide-workflow-state.mjs", [targetInfo.target, "compose", "--by", "smoke-validation"]);
498
+ commands.push(approveComposeCommand);
499
+ const composeApproval = await readApproval(targetInfo, "compose", composeSupervision.data);
500
+ assert(composeApproval.data.approved_by === "smoke-validation", `approval-command:compose-approved expected approved_by smoke-validation, got ${composeApproval.data.approved_by}`);
501
+ observedPaths.push(relativePath(composeApproval.filePath));
502
+ checks.push(pass("approval-command:compose-approved", "slide:approve compose --by smoke-validation generated a matching compose approval file."));
503
+
504
+ const polishCommand = await runWorkflowCommand("sequence:polish-command", "polish", targetInfo, options);
505
+ commands.push(polishCommand);
506
+ const renderEvidence = await assertRenderEvidenceArtifacts(targetInfo, 1);
507
+ const verifyCommand = runNodeScript("sequence:polish-render-evidence-verify", "takt-marp-verify-render-evidence-metadata.mjs", [targetInfo.target, "--cycle", "1"]);
508
+ commands.push(verifyCommand);
509
+ const polishSupervision = await readSupervision(targetInfo, "polish");
510
+ assert(polishSupervision.data.state === "polished", `sequence:polish-supervision-state expected polished, got ${polishSupervision.data.state}`);
511
+ assert(polishSupervision.data.result === "passed", `sequence:polish-supervision-result expected passed, got ${polishSupervision.data.result}`);
512
+ observedPaths.push(...renderEvidence.observedPaths.map(relativePath));
513
+ observedPaths.push(relativePath(polishSupervision.filePath));
514
+ checks.push(pass("sequence:polish-command", "slide:polish completed for the smoke deck after compose approval."));
515
+ checks.push(pass("sequence:polish-render-evidence-marker", "polish render evidence marker points at the smoke deck cycle 1."));
516
+ checks.push(pass("sequence:polish-render-evidence-root", "polish render evidence root exists under .takt/render/_workflow-smoke/cycle-1."));
517
+ checks.push(pass("sequence:polish-render-evidence-metadata", "polish render evidence metadata records target, cycle, and completed evidence statuses."));
518
+ checks.push(pass("sequence:polish-render-evidence-html-png", "HTML PNG render evidence is usable and backed by non-empty files."));
519
+ checks.push(pass("sequence:polish-render-evidence-verify", "render evidence metadata verifier accepts the polish output for the smoke deck."));
520
+ checks.push(pass("sequence:polish-supervision", "polish-supervision.md exists with state polished and result passed."));
521
+
522
+ const staleDeliveryArtifactPaths = await seedStaleDeliveryArtifacts(targetInfo);
523
+ observedPaths.push(...staleDeliveryArtifactPaths.map(relativePath));
524
+ const deliverCommand = await runWorkflowCommand("sequence:deliver-command", "deliver", targetInfo, options);
525
+ commands.push(deliverCommand);
526
+ const deliverSupervision = await readSupervision(targetInfo, "deliver");
527
+ assert(deliverSupervision.data.state === "delivered", `sequence:deliver-supervision-state expected delivered, got ${deliverSupervision.data.state}`);
528
+ assert(deliverSupervision.data.result === "passed", `sequence:deliver-supervision-result expected passed, got ${deliverSupervision.data.result}`);
529
+ const deliveryArtifacts = await assertDeliveryArtifacts(targetInfo, {
530
+ staleArtifactPaths: staleDeliveryArtifactPaths,
531
+ renderEvidenceRoot: renderEvidence.evidenceRoot,
532
+ });
533
+ const verifyDeliveryCommand = runNodeScript("sequence:deliver-artifact-verify", "takt-marp-verify-delivery-artifacts.mjs", ["verify", targetInfo.target]);
534
+ commands.push(verifyDeliveryCommand);
535
+ const reportFrontMatterPaths = await assertCommandReportFrontMatter(targetInfo);
536
+ observedPaths.push(relativePath(deliverSupervision.filePath));
537
+ observedPaths.push(...deliveryArtifacts.observedPaths.map(relativePath));
538
+ observedPaths.push(...reportFrontMatterPaths.map(relativePath));
539
+ checks.push(pass("sequence:deliver-command", "slide:deliver completed for the smoke deck after polish."));
540
+ checks.push(pass("sequence:deliver-supervision", "deliver-supervision.md exists with state delivered and result passed."));
541
+ checks.push(pass("sequence:deliver-stale-cleanup", "slide:deliver cleans stale official artifacts before generating requested outputs."));
542
+ checks.push(pass("sequence:deliver-artifacts", "dist/_workflow-smoke contains readable official artifacts requested by plan.md."));
543
+ checks.push(pass("sequence:deliver-render-evidence-boundary", "render evidence under .takt/render is not counted as an official delivery artifact."));
544
+ checks.push(pass("sequence:command-report-front-matter", "canonical command reports have closed YAML front matter readable by the foundation parser."));
545
+ checks.push(pass("sequence:final-state", "final delivered state is confirmed by deliver supervision and delivery artifacts."));
546
+
547
+ return Object.freeze({
548
+ checks: Object.freeze(checks),
549
+ commands: Object.freeze(commands),
550
+ observedPaths: Object.freeze(observedPaths),
551
+ });
552
+ }
553
+
554
+ async function assertCommandReportFrontMatter(targetInfo) {
555
+ const requiredReportNamesByCommand = {
556
+ plan: ["plan-work.md", "plan-review.md", "plan-supervision.md"],
557
+ compose: ["compose-work.md", "compose-review.md", "compose-supervision.md"],
558
+ polish: ["polish-work.md", "polish-inspect.md", "polish-supervision.md"],
559
+ deliver: ["deliver-work.md", "deliver-verify.md", "deliver-supervision.md"],
560
+ };
561
+ const optionalReportNamesByCommand = {
562
+ plan: ["plan-fix.md"],
563
+ compose: ["compose-fix.md"],
564
+ polish: ["polish-fix.md"],
565
+ deliver: ["deliver-fix.md"],
566
+ };
567
+ const reportPaths = [];
568
+ for (const [command, reportNames] of Object.entries(requiredReportNamesByCommand)) {
569
+ const supervision = await readSupervision(targetInfo, command);
570
+ const workflowRunId = supervision.data.workflow_run_id;
571
+ assert(
572
+ typeof workflowRunId === "string" && workflowRunId.length > 0,
573
+ `sequence:command-report-front-matter ${command} supervision missing workflow_run_id`,
574
+ );
575
+ reportPaths.push(...reportNames.map((fileName) => path.join(targetInfo.reviewPath, fileName)));
576
+ reportPaths.push(...(await matchingRunReportPaths(command, targetInfo, workflowRunId, reportNames)));
577
+ for (const fileName of optionalReportNamesByCommand[command]) {
578
+ const deckReportPath = path.join(targetInfo.reviewPath, fileName);
579
+ if (existsSync(deckReportPath)) {
580
+ reportPaths.push(deckReportPath);
581
+ }
582
+ reportPaths.push(...(await matchingRunReportPaths(command, targetInfo, workflowRunId, [fileName], { optional: true })));
583
+ }
584
+ reportPaths.push(...(await assertAiGateReportFrontMatter(targetInfo, command, workflowRunId)));
585
+ }
586
+
587
+ for (const reportPath of reportPaths) {
588
+ try {
589
+ await readFrontMatter(reportPath);
590
+ } catch (error) {
591
+ throw new SlideWorkflowError(
592
+ `sequence:command-report-front-matter invalid report front matter in ${relativePath(reportPath)}: ${formatError(error)}`,
593
+ "SMOKE_ASSERTION_FAILED",
594
+ );
595
+ }
596
+ }
597
+ return Object.freeze(reportPaths);
598
+ }
599
+
600
+ async function assertAiGateReportFrontMatter(targetInfo, command, workflowRunId) {
601
+ const reviewPath = path.join(targetInfo.reviewPath, `${command}-ai-antipattern-review.md`);
602
+ const fixPath = path.join(targetInfo.reviewPath, `${command}-ai-antipattern-fix.md`);
603
+ const reportPaths = [reviewPath];
604
+ const review = await readReportWithBody(reviewPath);
605
+ assertAiGateReviewData(targetInfo, command, workflowRunId, review.frontMatter, reviewPath);
606
+
607
+ if (existsSync(fixPath)) {
608
+ const fix = await readReportWithBody(fixPath);
609
+ assertAiGateFixData(targetInfo, command, workflowRunId, fix.frontMatter, fix.body, fixPath);
610
+ reportPaths.push(fixPath);
611
+ } else {
612
+ assert(
613
+ review.frontMatter.blocking_finding_count === 0,
614
+ `sequence:ai-gate-report-front-matter ${command} missing fix report despite blocking AI findings`,
615
+ );
616
+ }
617
+
618
+ return Object.freeze(reportPaths);
619
+ }
620
+
621
+ async function readReportWithBody(filePath) {
622
+ try {
623
+ return parseFrontMatter(await readFile(filePath, "utf8"));
624
+ } catch (error) {
625
+ throw new SlideWorkflowError(
626
+ `sequence:ai-gate-report-front-matter invalid report front matter in ${relativePath(filePath)}: ${formatError(error)}`,
627
+ "SMOKE_ASSERTION_FAILED",
628
+ );
629
+ }
630
+ }
631
+
632
+ function assertAiGateReviewData(targetInfo, command, workflowRunId, data, filePath) {
633
+ assert(data.command === command, `sequence:ai-gate-report-front-matter ${relativePath(filePath)} command mismatch: ${data.command}`);
634
+ assert(data.target === targetInfo.target, `sequence:ai-gate-report-front-matter ${relativePath(filePath)} target mismatch: ${data.target}`);
635
+ assert(data.workflow_run_id === workflowRunId, `sequence:ai-gate-report-front-matter ${relativePath(filePath)} workflow_run_id mismatch: ${data.workflow_run_id}`);
636
+ assert(data.step === "ai_antipattern_review", `sequence:ai-gate-report-front-matter ${relativePath(filePath)} step mismatch: ${data.step}`);
637
+ assert(typeof data.reviewed_scope === "string" && data.reviewed_scope, `sequence:ai-gate-report-front-matter ${relativePath(filePath)} reviewed_scope missing`);
638
+ assert(["approved", "needs_fix", "blocked"].includes(data.result), `sequence:ai-gate-report-front-matter ${relativePath(filePath)} invalid result: ${data.result}`);
639
+ assert(Number.isInteger(data.finding_count), `sequence:ai-gate-report-front-matter ${relativePath(filePath)} finding_count must be numeric`);
640
+ assert(Number.isInteger(data.blocking_finding_count), `sequence:ai-gate-report-front-matter ${relativePath(filePath)} blocking_finding_count must be numeric`);
641
+ }
642
+
643
+ function assertAiGateFixData(targetInfo, command, workflowRunId, data, body, filePath) {
644
+ assert(data.command === command, `sequence:ai-gate-report-front-matter ${relativePath(filePath)} command mismatch: ${data.command}`);
645
+ assert(data.target === targetInfo.target, `sequence:ai-gate-report-front-matter ${relativePath(filePath)} target mismatch: ${data.target}`);
646
+ assert(data.workflow_run_id === workflowRunId, `sequence:ai-gate-report-front-matter ${relativePath(filePath)} workflow_run_id mismatch: ${data.workflow_run_id}`);
647
+ assert(data.step === "ai_antipattern_fix", `sequence:ai-gate-report-front-matter ${relativePath(filePath)} step mismatch: ${data.step}`);
648
+ assert(["FIXED", "NO_FIX_NEEDED", "NEED_REPLAN", "BLOCKED"].includes(data.status), `sequence:ai-gate-report-front-matter ${relativePath(filePath)} invalid status: ${data.status}`);
649
+ assert(Number.isInteger(data.handled_finding_count), `sequence:ai-gate-report-front-matter ${relativePath(filePath)} handled_finding_count must be numeric`);
650
+ assert(Number.isInteger(data.changed_file_count), `sequence:ai-gate-report-front-matter ${relativePath(filePath)} changed_file_count must be numeric`);
651
+ assert(Number.isInteger(data.remaining_context_count), `sequence:ai-gate-report-front-matter ${relativePath(filePath)} remaining_context_count must be numeric`);
652
+ if (data.status === "NO_FIX_NEEDED" && data.handled_finding_count > 0) {
653
+ assertFindingDecisionEvidence(body, filePath);
654
+ }
655
+ }
656
+
657
+ function assertFindingDecisionEvidence(body, filePath) {
658
+ const section = tableSection(body, "## Finding Decisions");
659
+ const rows = section
660
+ .split("\n")
661
+ .filter((line) => line.trim().startsWith("|"))
662
+ .slice(2);
663
+ assert(rows.length > 0, `sequence:ai-gate-report-front-matter ${relativePath(filePath)} missing finding decision rows`);
664
+ for (const row of rows) {
665
+ const cells = row.split("|").map((cell) => cell.trim());
666
+ const evidence = cells[cells.length - 2] ?? "";
667
+ assert(evidence && evidence.toLowerCase() !== "none", `sequence:ai-gate-report-front-matter ${relativePath(filePath)} NO_FIX_NEEDED row lacks finding-level evidence: ${row}`);
668
+ }
669
+ }
670
+
671
+ function tableSection(body, heading) {
672
+ const start = body.indexOf(heading);
673
+ assert(start !== -1, `missing section ${heading}`);
674
+ const next = body.indexOf("\n## ", start + heading.length);
675
+ return next === -1 ? body.slice(start) : body.slice(start, next);
676
+ }
677
+
678
+ async function matchingRunReportPaths(command, targetInfo, workflowRunId, reportNames, options = {}) {
679
+ const runsRoot = path.join(ROOT, ".takt", "runs");
680
+ if (!existsSync(runsRoot)) {
681
+ assert(options.optional, `sequence:command-report-front-matter missing .takt/runs for ${command}`);
682
+ return [];
683
+ }
684
+
685
+ const entries = await readdir(runsRoot, { withFileTypes: true });
686
+ const reportPaths = [];
687
+ for (const reportName of reportNames) {
688
+ const reportPath = await findMatchingRunReportPath(entries, runsRoot, command, targetInfo, workflowRunId, reportName);
689
+ if (reportPath) {
690
+ reportPaths.push(reportPath);
691
+ } else {
692
+ assert(
693
+ options.optional,
694
+ `sequence:command-report-front-matter missing matching TAKT run report for ${command} ${reportName} workflow_run_id ${workflowRunId}`,
695
+ );
696
+ }
697
+ }
698
+ return Object.freeze(reportPaths);
699
+ }
700
+
701
+ async function findMatchingRunReportPath(entries, runsRoot, command, targetInfo, workflowRunId, reportName) {
702
+ const step = reportStepFromName(command, reportName);
703
+ for (const entry of entries.filter((candidate) => candidate.isDirectory())) {
704
+ const reportPath = path.join(runsRoot, entry.name, "reports", reportName);
705
+ if (!existsSync(reportPath)) {
706
+ continue;
707
+ }
708
+ try {
709
+ const data = await readFrontMatter(reportPath);
710
+ if (
711
+ data.command === command &&
712
+ data.target === targetInfo.target &&
713
+ data.workflow_run_id === workflowRunId &&
714
+ data.step === step
715
+ ) {
716
+ return reportPath;
717
+ }
718
+ } catch {
719
+ // A same-named report from another run is not the target unless its front matter matches.
720
+ }
721
+ }
722
+ return null;
723
+ }
724
+
725
+ function reportStepFromName(command, reportName) {
726
+ const prefix = `${command}-`;
727
+ assert(reportName.startsWith(prefix) && reportName.endsWith(".md"), `unexpected report name for ${command}: ${reportName}`);
728
+ return reportName.slice(prefix.length, -".md".length);
729
+ }
730
+
731
+ async function runSuccessfulRerunRejectionChecks(targetInfo) {
732
+ const checks = [];
733
+ const commands = [];
734
+ const protectedPaths = [supervisionPath(targetInfo, "plan"), approvalPath(targetInfo, "plan")];
735
+ const snapshots = await snapshotFiles(protectedPaths);
736
+ const historyBefore = await listHistoryFiles(targetInfo);
737
+ const commandLine = `node scripts/takt-marp-run-slide-workflow.mjs plan ${JSON.stringify(targetInfo.target)}`;
738
+ const result = spawnSync(process.execPath, [RUNNER_SCRIPT, "plan", targetInfo.target], {
739
+ cwd: ROOT,
740
+ encoding: "utf8",
741
+ timeout: NODE_CHECK_TIMEOUT_MS,
742
+ maxBuffer: CAPTURE_MAX_BUFFER,
743
+ });
744
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
745
+ commands.push(commandLine);
746
+
747
+ assert(result.status !== 0, "failure-path:successful-rerun-rejected unexpectedly succeeded");
748
+ assert(output.includes("RERUN_BLOCKED:"), `failure-path:successful-rerun-rejected did not report RERUN_BLOCKED: ${output}`);
749
+ assert(!output.includes("Workflow completed"), `failure-path:successful-rerun-rejected reached workflow completion: ${output}`);
750
+ assert(!output.includes("takt-marp-slide-plan"), `failure-path:successful-rerun-rejected reached TAKT workflow output: ${output}`);
751
+ await assertSnapshotsUnchanged("failure-path:successful-rerun-state-preserved", snapshots);
752
+ const historyAfter = await listHistoryFiles(targetInfo);
753
+ assert(
754
+ JSON.stringify(historyAfter) === JSON.stringify(historyBefore),
755
+ `failure-path:successful-rerun-state-preserved changed history files: before=${historyBefore.join(",")} after=${historyAfter.join(",")}`,
756
+ );
757
+
758
+ checks.push(pass("failure-path:successful-rerun-rejected", "successful plan rerun was rejected without TAKT startup when --force was omitted."));
759
+ checks.push(pass("failure-path:successful-rerun-state-preserved", "rejected successful rerun left canonical supervision, approval, and history unchanged."));
760
+
761
+ return Object.freeze({
762
+ checks: Object.freeze(checks),
763
+ commands: Object.freeze(commands),
764
+ observedPaths: Object.freeze(protectedPaths.map(relativePath)),
765
+ });
766
+ }
767
+
768
+ async function runForceInvalidationChecks(targetInfo, options) {
769
+ const checks = [];
770
+ const commands = [];
771
+ const historyBefore = await listHistoryFiles(targetInfo);
772
+ const sourceArtifactPaths = [
773
+ path.join(targetInfo.deckPath, "brief.md"),
774
+ path.join(targetInfo.deckPath, "brief.normalized.md"),
775
+ path.join(targetInfo.deckPath, "plan.md"),
776
+ path.join(targetInfo.deckPath, "design-system.md"),
777
+ path.join(targetInfo.deckPath, "SLIDES.md"),
778
+ path.join(targetInfo.deckPath, "images", "workflow-overview.svg"),
779
+ ];
780
+ await assertSourceArtifactsPresent("failure-path:force-source-retention-before", sourceArtifactPaths);
781
+ const expectedArchivedBasenames = [
782
+ "force-plan-supervision.md",
783
+ "force-plan-approval.md",
784
+ "force-compose-supervision.md",
785
+ "force-compose-approval.md",
786
+ "force-polish-supervision.md",
787
+ "force-deliver-supervision.md",
788
+ ];
789
+ for (const filePath of [
790
+ supervisionPath(targetInfo, "plan"),
791
+ approvalPath(targetInfo, "plan"),
792
+ supervisionPath(targetInfo, "compose"),
793
+ approvalPath(targetInfo, "compose"),
794
+ supervisionPath(targetInfo, "polish"),
795
+ supervisionPath(targetInfo, "deliver"),
796
+ ]) {
797
+ assert(existsSync(filePath), `failure-path:force-archive missing pre-force artifact: ${relativePath(filePath)}`);
798
+ }
799
+
800
+ const forceCommand = await runWorkflowCommand("failure-path:force-command", "plan", targetInfo, options, ["--force"]);
801
+ commands.push(forceCommand);
802
+
803
+ const historyAfter = await listHistoryFiles(targetInfo);
804
+ const newHistoryFiles = historyAfter.filter((fileName) => !historyBefore.includes(fileName));
805
+ for (const basename of expectedArchivedBasenames) {
806
+ assert(
807
+ newHistoryFiles.some((fileName) => fileName.endsWith(basename)),
808
+ `failure-path:force-archive missing archived ${basename}; new history files: ${newHistoryFiles.join(",")}`,
809
+ );
810
+ }
811
+
812
+ const generatedOutputPaths = [
813
+ path.join(ROOT, "dist", targetInfo.deckName),
814
+ path.join(ROOT, ".takt", "render", targetInfo.deckName),
815
+ ];
816
+ for (const generatedPath of generatedOutputPaths) {
817
+ assert(!existsSync(generatedPath), `failure-path:force-generated-cleanup found stale generated output: ${relativePath(generatedPath)}`);
818
+ }
819
+ await assertSourceArtifactsPresent("failure-path:force-source-retention", sourceArtifactPaths);
820
+
821
+ const planSupervision = await readSupervision(targetInfo, "plan");
822
+ assert(planSupervision.data.state === "planned", `failure-path:force-new-plan-supervision expected planned, got ${planSupervision.data.state}`);
823
+ assert(planSupervision.data.result === "passed", `failure-path:force-new-plan-supervision expected passed, got ${planSupervision.data.result}`);
824
+ for (const invalidatedPath of [
825
+ approvalPath(targetInfo, "plan"),
826
+ supervisionPath(targetInfo, "compose"),
827
+ approvalPath(targetInfo, "compose"),
828
+ supervisionPath(targetInfo, "polish"),
829
+ supervisionPath(targetInfo, "deliver"),
830
+ ]) {
831
+ assert(!existsSync(invalidatedPath), `failure-path:force-invalidation left downstream state: ${relativePath(invalidatedPath)}`);
832
+ }
833
+
834
+ checks.push(pass("failure-path:force-command", "slide:plan --force completed after archiving command state."));
835
+ checks.push(pass("failure-path:force-archive", "force archived plan and downstream supervision/approval files to review/history."));
836
+ checks.push(pass("failure-path:force-generated-cleanup", "force cleaned stale dist and render generated output roots."));
837
+ checks.push(pass("failure-path:force-source-retention", "force retained deck source artifacts such as brief, plan, design-system, SLIDES.md, and SVG."));
838
+ checks.push(pass("failure-path:force-new-plan-supervision", "force rerun generated a new passed canonical plan supervision report."));
839
+
840
+ return Object.freeze({
841
+ checks: Object.freeze(checks),
842
+ commands: Object.freeze(commands),
843
+ observedPaths: Object.freeze([
844
+ planSupervision.filePath,
845
+ path.join(targetInfo.reviewPath, "history"),
846
+ ...newHistoryFiles.map((fileName) => path.join(targetInfo.reviewPath, "history", fileName)),
847
+ ...sourceArtifactPaths,
848
+ ...generatedOutputPaths,
849
+ ].map(relativePath)),
850
+ });
851
+ }
852
+
853
+ async function runRejectedRerunArchiveChecks(targetInfo, options) {
854
+ const checks = [];
855
+ const commands = [];
856
+ const rejectedWorkflowRunId = "smoke-rejected-plan-rerun";
857
+ const historyBefore = await listHistoryFiles(targetInfo);
858
+
859
+ await writeSyntheticSupervision(targetInfo, "plan", {
860
+ state: "none",
861
+ result: "rejected",
862
+ workflowRunId: rejectedWorkflowRunId,
863
+ });
864
+
865
+ const rejectedSnapshot = await readFile(supervisionPath(targetInfo, "plan"), "utf8");
866
+ const providerArgs = providerFlagArgs(options);
867
+ const commandLine = `node scripts/takt-marp-run-slide-workflow.mjs plan ${[JSON.stringify(targetInfo.target), ...providerArgs.map((arg) => JSON.stringify(arg))].join(" ")}`;
868
+ commands.push(commandLine);
869
+
870
+ if (isMockProvider(options)) {
871
+ await archiveCommandArtifacts(targetInfo, ["plan"], "rejected-rerun");
872
+ await writeMockCommandResult(targetInfo, "plan");
873
+ } else {
874
+ const result = spawnSync(process.execPath, [RUNNER_SCRIPT, "plan", targetInfo.target, ...providerArgs], {
875
+ cwd: ROOT,
876
+ encoding: "utf8",
877
+ timeout: WORKFLOW_COMMAND_TIMEOUT_MS,
878
+ maxBuffer: CAPTURE_MAX_BUFFER,
879
+ });
880
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
881
+ assert(result.status === 0, `failure-path:rejected-rerun-allowed failed with exit code ${result.status ?? "unknown"}: ${output}`);
882
+ assert(output.includes("Workflow completed"), `failure-path:rejected-rerun-allowed did not run workflow to completion: ${output}`);
883
+ assert(!output.includes("RERUN_BLOCKED:"), `failure-path:rejected-rerun-allowed was blocked as successful rerun: ${output}`);
884
+ }
885
+
886
+ const historyAfter = await listHistoryFiles(targetInfo);
887
+ const archivedFiles = historyAfter.filter((fileName) => !historyBefore.includes(fileName) && fileName.includes("rejected-rerun-plan-supervision.md"));
888
+ assert(archivedFiles.length === 1, `failure-path:rejected-rerun-archive expected one rejected archive, got ${archivedFiles.join(",")}`);
889
+ const archivedPath = path.join(targetInfo.reviewPath, "history", archivedFiles[0]);
890
+ const archivedContent = await readFile(archivedPath, "utf8");
891
+ assert(archivedContent === rejectedSnapshot, `failure-path:rejected-rerun-archive archived content did not match rejected supervision: ${relativePath(archivedPath)}`);
892
+ assert(archivedContent.includes(`workflow_run_id: ${rejectedWorkflowRunId}`), `failure-path:rejected-rerun-archive archived wrong report: ${relativePath(archivedPath)}`);
893
+ assert(archivedContent.includes("result: rejected"), `failure-path:rejected-rerun-archive archived report was not rejected: ${relativePath(archivedPath)}`);
894
+
895
+ const supervision = await readSupervision(targetInfo, "plan");
896
+ assert(supervision.data.state === "planned", `failure-path:rejected-rerun-new-report expected planned, got ${supervision.data.state}`);
897
+ assert(supervision.data.result === "passed", `failure-path:rejected-rerun-new-report expected passed, got ${supervision.data.result}`);
898
+ assert(
899
+ supervision.data.workflow_run_id !== rejectedWorkflowRunId,
900
+ "failure-path:rejected-rerun-new-report did not replace the rejected workflow_run_id",
901
+ );
902
+
903
+ checks.push(pass("failure-path:rejected-rerun-allowed", "rejected canonical plan supervision allowed rerun without --force."));
904
+ checks.push(pass("failure-path:rejected-rerun-archive", "rejected plan supervision was archived to review/history before rerun."));
905
+ checks.push(pass("failure-path:rejected-rerun-new-report", "rerun generated a new passed canonical plan supervision report."));
906
+
907
+ return Object.freeze({
908
+ checks: Object.freeze(checks),
909
+ commands: Object.freeze(commands),
910
+ observedPaths: Object.freeze([
911
+ supervision.filePath,
912
+ archivedPath,
913
+ path.join(targetInfo.reviewPath, "history"),
914
+ ].map(relativePath)),
915
+ });
916
+ }
917
+
918
+ async function snapshotFiles(filePaths) {
919
+ const snapshots = [];
920
+ for (const filePath of filePaths) {
921
+ snapshots.push(Object.freeze({
922
+ filePath,
923
+ exists: existsSync(filePath),
924
+ content: existsSync(filePath) ? await readFile(filePath, "utf8") : null,
925
+ }));
926
+ }
927
+ return Object.freeze(snapshots);
928
+ }
929
+
930
+ async function assertSnapshotsUnchanged(name, snapshots) {
931
+ for (const snapshot of snapshots) {
932
+ assert(existsSync(snapshot.filePath) === snapshot.exists, `${name} changed protected file existence: ${relativePath(snapshot.filePath)}`);
933
+ if (snapshot.exists) {
934
+ const content = await readFile(snapshot.filePath, "utf8");
935
+ assert(content === snapshot.content, `${name} changed protected file: ${relativePath(snapshot.filePath)}`);
936
+ }
937
+ }
938
+ }
939
+
940
+ async function assertSourceArtifactsPresent(name, filePaths) {
941
+ for (const filePath of filePaths) {
942
+ await assertReadableFile(filePath, name);
943
+ }
944
+ }
945
+
946
+ async function listHistoryFiles(targetInfo) {
947
+ const historyPath = path.join(targetInfo.reviewPath, "history");
948
+ if (!existsSync(historyPath)) {
949
+ return Object.freeze([]);
950
+ }
951
+ const entries = await readdir(historyPath, { withFileTypes: true });
952
+ return Object.freeze(entries.filter((entry) => entry.isFile()).map((entry) => entry.name).sort());
953
+ }
954
+
955
+ async function setupSyntheticValidationDeck(targetPath) {
956
+ await rm(targetPath, { recursive: true, force: true });
957
+ await mkdir(path.join(targetPath, "review"), { recursive: true });
958
+ await writeFile(path.join(targetPath, "brief.md"), "# Synthetic State Validation\n", "utf8");
959
+ return STATE_VALIDATION_TARGET;
960
+ }
961
+
962
+ async function assertSlideWorkflowFailure(name, run, expectedCode) {
963
+ try {
964
+ await run();
965
+ } catch (error) {
966
+ assert(error instanceof SlideWorkflowError, `${name} failed with unexpected error type: ${formatError(error)}`);
967
+ assert(error.code === expectedCode, `${name} expected ${expectedCode}, got ${error.code}: ${formatError(error)}`);
968
+ return;
969
+ }
970
+ throw new SlideWorkflowError(`${name} unexpectedly succeeded`, "SMOKE_ASSERTION_FAILED");
971
+ }
972
+
973
+ function assertPreflightFailure(name, command, target, expectedCode) {
974
+ const commandLine = `node scripts/takt-marp-run-slide-workflow.mjs ${command} ${JSON.stringify(target)}`;
975
+ const result = spawnSync(process.execPath, [RUNNER_SCRIPT, command, target], {
976
+ cwd: ROOT,
977
+ encoding: "utf8",
978
+ timeout: NODE_CHECK_TIMEOUT_MS,
979
+ maxBuffer: CAPTURE_MAX_BUFFER,
980
+ });
981
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
982
+ assert(result.status !== 0, `${name} unexpectedly succeeded`);
983
+ assert(output.includes(`${expectedCode}:`), `${name} did not report ${expectedCode}: ${output}`);
984
+ assertNoWorkflowExecution(output, name);
985
+ return commandLine;
986
+ }
987
+
988
+ function assertNoWorkflowExecution(output, name) {
989
+ assert(!output.includes("TAKT_EXECUTABLE_MISSING"), `${name} reached TAKT executable preflight: ${output}`);
990
+ assert(!output.includes("Workflow completed"), `${name} reached workflow completion: ${output}`);
991
+ assert(!output.includes("TAKT_REPORT_SYNC_"), `${name} reached workflow report sync: ${output}`);
992
+ }
993
+
994
+ function assertApprovalCommandFailure(name, target, command, args, expectedOutput) {
995
+ const commandArgs = [target, command, ...args];
996
+ const commandLine = `node scripts/takt-marp-approve-slide-workflow-state.mjs ${commandArgs.map((arg) => JSON.stringify(arg)).join(" ")}`;
997
+ const result = spawnSync(process.execPath, [path.join(SCRIPT_DIR, "takt-marp-approve-slide-workflow-state.mjs"), ...commandArgs], {
998
+ cwd: ROOT,
999
+ encoding: "utf8",
1000
+ timeout: NODE_CHECK_TIMEOUT_MS,
1001
+ maxBuffer: CAPTURE_MAX_BUFFER,
1002
+ });
1003
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
1004
+ assert(result.status !== 0, `${name} unexpectedly succeeded`);
1005
+ assert(output.includes(expectedOutput), `${name} did not report ${expectedOutput}: ${output}`);
1006
+ return commandLine;
1007
+ }
1008
+
1009
+ async function runWorkflowCommand(name, command, targetInfo, options, extraArgs = []) {
1010
+ const args = workflowCommandArgs(targetInfo.target, options, extraArgs);
1011
+ const runnerArgs = [command, ...args];
1012
+ const commandLine = `node scripts/takt-marp-run-slide-workflow.mjs ${runnerArgs.map((arg) => JSON.stringify(arg)).join(" ")}`;
1013
+ if (!isMockProvider(options)) {
1014
+ return runWorkflowNodeScript(name, "takt-marp-run-slide-workflow.mjs", runnerArgs);
1015
+ }
1016
+
1017
+ if (extraArgs.includes("--force")) {
1018
+ await archiveCommandArtifacts(targetInfo, downstreamCommands(command), "force", { includeApprovals: true });
1019
+ await cleanGeneratedOutputs(targetInfo, { root: ROOT });
1020
+ }
1021
+ await writeMockCommandResult(targetInfo, command);
1022
+ return commandLine;
1023
+ }
1024
+
1025
+ function isMockProvider(options) {
1026
+ return options?.provider === "mock";
1027
+ }
1028
+
1029
+ async function writeMockCommandResult(targetInfo, command) {
1030
+ await mkdir(targetInfo.reviewPath, { recursive: true });
1031
+ const workflowRunId = `mock-smoke-${command}`;
1032
+ const reportsPath = path.join(ROOT, ".takt", "runs", `mock-${targetInfo.deckName}-${command}`, "reports");
1033
+ await rm(path.dirname(reportsPath), { recursive: true, force: true });
1034
+ await mkdir(reportsPath, { recursive: true });
1035
+
1036
+ if (command === "plan") {
1037
+ await writeMockPlanArtifacts(targetInfo);
1038
+ } else if (command === "compose") {
1039
+ await writeMockComposeArtifacts(targetInfo);
1040
+ } else if (command === "polish") {
1041
+ await writeMockRenderEvidence(targetInfo, 1);
1042
+ } else if (command === "deliver") {
1043
+ await writeMockDeliveryArtifacts(targetInfo);
1044
+ }
1045
+
1046
+ for (const report of mockReports(targetInfo, command, workflowRunId)) {
1047
+ await writeReportCopies(targetInfo.reviewPath, reportsPath, report.name, report.content);
1048
+ }
1049
+ }
1050
+
1051
+ async function writeReportCopies(reviewPath, reportsPath, reportName, content) {
1052
+ await writeFile(path.join(reviewPath, reportName), content, "utf8");
1053
+ await writeFile(path.join(reportsPath, reportName), content, "utf8");
1054
+ }
1055
+
1056
+ async function writeMockPlanArtifacts(targetInfo) {
1057
+ await writeFile(path.join(targetInfo.deckPath, "brief.normalized.md"), "# Normalized Brief\n\nMock smoke normalized brief.\n", "utf8");
1058
+ await writeFile(
1059
+ path.join(targetInfo.deckPath, "plan.md"),
1060
+ [
1061
+ "# Slide Plan",
1062
+ "",
1063
+ "deliverables: [html, pdf]",
1064
+ "",
1065
+ "## Slides",
1066
+ "- Title",
1067
+ "- Workflow overview",
1068
+ "- Input discipline",
1069
+ "- Review discipline",
1070
+ "- Delivery QA",
1071
+ "",
1072
+ ].join("\n"),
1073
+ "utf8",
1074
+ );
1075
+ }
1076
+
1077
+ async function writeMockComposeArtifacts(targetInfo) {
1078
+ await mkdir(path.join(targetInfo.deckPath, "images"), { recursive: true });
1079
+ await writeFile(path.join(targetInfo.deckPath, "design-system.md"), "# Design System\n\nMock smoke design system.\n", "utf8");
1080
+ await writeFile(
1081
+ path.join(targetInfo.deckPath, "SLIDES.md"),
1082
+ [
1083
+ "---",
1084
+ "marp: true",
1085
+ "title: Workflow smoke test",
1086
+ "---",
1087
+ "",
1088
+ "# Workflow smoke test",
1089
+ "",
1090
+ "---",
1091
+ "",
1092
+ "![workflow overview](images/workflow-overview.svg)",
1093
+ "",
1094
+ "---",
1095
+ "",
1096
+ "Input discipline",
1097
+ "",
1098
+ "---",
1099
+ "",
1100
+ "Review discipline",
1101
+ "",
1102
+ "---",
1103
+ "",
1104
+ "Delivery QA",
1105
+ "",
1106
+ ].join("\n"),
1107
+ "utf8",
1108
+ );
1109
+ await writeFile(
1110
+ path.join(targetInfo.deckPath, "images", "workflow-overview.svg"),
1111
+ [
1112
+ '<svg xmlns="http://www.w3.org/2000/svg" width="640" height="360" viewBox="0 0 640 360">',
1113
+ '<rect width="640" height="360" fill="#f7f7f7"/>',
1114
+ '<text x="40" y="180" font-family="sans-serif" font-size="32" fill="#222">Mock workflow overview</text>',
1115
+ "</svg>",
1116
+ "",
1117
+ ].join("\n"),
1118
+ "utf8",
1119
+ );
1120
+ }
1121
+
1122
+ async function writeMockRenderEvidence(targetInfo, cycle) {
1123
+ const evidenceRoot = path.join(ROOT, ".takt", "render", targetInfo.deckName, `cycle-${cycle}`);
1124
+ await rm(evidenceRoot, { recursive: true, force: true });
1125
+ await mkdir(evidenceRoot, { recursive: true });
1126
+ await writeFile(path.join(evidenceRoot, "slide-1.png"), "mock png evidence\n", "utf8");
1127
+ await writeFile(path.join(evidenceRoot, "SLIDES.pdf"), "mock pdf evidence\n", "utf8");
1128
+ await writeFile(
1129
+ path.join(evidenceRoot, "metadata.json"),
1130
+ `${JSON.stringify(
1131
+ {
1132
+ deck: targetInfo.deckName,
1133
+ target: targetInfo.target,
1134
+ cycle,
1135
+ html_png: { status: "passed", files: ["slide-1.png"] },
1136
+ pdf: { status: "passed", file: "SLIDES.pdf" },
1137
+ pdf_raster: { status: "skipped", reason: "mock smoke uses deterministic synthetic render evidence", files: [] },
1138
+ },
1139
+ null,
1140
+ 2,
1141
+ )}\n`,
1142
+ "utf8",
1143
+ );
1144
+ await writeRenderEvidenceMarker(targetInfo, cycle);
1145
+ }
1146
+
1147
+ async function writeMockDeliveryArtifacts(targetInfo) {
1148
+ const distPath = path.join(ROOT, "dist", targetInfo.deckName);
1149
+ await rm(distPath, { recursive: true, force: true });
1150
+ await mkdir(distPath, { recursive: true });
1151
+ await writeFile(path.join(distPath, "SLIDES.html"), "<!doctype html><title>Mock smoke</title>\n", "utf8");
1152
+ await writeFile(path.join(distPath, "SLIDES.pdf"), "mock pdf artifact\n", "utf8");
1153
+ }
1154
+
1155
+ function mockReports(targetInfo, command, workflowRunId) {
1156
+ const reports = [];
1157
+ const workReport = command === "deliver"
1158
+ ? [
1159
+ "- Result: passed",
1160
+ `- Cleaned directory: ${relativePath(path.join(ROOT, "dist", targetInfo.deckName))}`,
1161
+ "- Artifacts: SLIDES.html, SLIDES.pdf",
1162
+ ].join("\n")
1163
+ : "- Result: passed\n";
1164
+ reports.push(mockReport(`${command}-work.md`, targetInfo, command, workflowRunId, reportStepFromName(command, `${command}-work.md`), "passed", workReport));
1165
+
1166
+ const normalReportName = {
1167
+ plan: "plan-review.md",
1168
+ compose: "compose-review.md",
1169
+ polish: "polish-inspect.md",
1170
+ deliver: "deliver-verify.md",
1171
+ }[command];
1172
+ const normalBody = command === "deliver"
1173
+ ? "- Result: approved\n- Verified artifacts: SLIDES.html, SLIDES.pdf\n"
1174
+ : "- Result: approved\n";
1175
+ reports.push(mockReport(normalReportName, targetInfo, command, workflowRunId, reportStepFromName(command, normalReportName), "approved", normalBody));
1176
+ reports.push(mockAiGateReviewReport(targetInfo, command, workflowRunId));
1177
+ reports.push(mockSupervisionReport(targetInfo, command, workflowRunId));
1178
+ return reports;
1179
+ }
1180
+
1181
+ function mockReport(name, targetInfo, command, workflowRunId, step, result, body) {
1182
+ return Object.freeze({
1183
+ name,
1184
+ content: [
1185
+ "---",
1186
+ `command: ${command}`,
1187
+ `target: ${targetInfo.target}`,
1188
+ `generated_at: ${MOCK_GENERATED_AT}`,
1189
+ `workflow_run_id: ${workflowRunId}`,
1190
+ `step: ${step}`,
1191
+ "cycle: 1",
1192
+ `result: ${result}`,
1193
+ "---",
1194
+ "",
1195
+ `# Mock ${step}`,
1196
+ "",
1197
+ body,
1198
+ "",
1199
+ ].join("\n"),
1200
+ });
1201
+ }
1202
+
1203
+ function mockAiGateReviewReport(targetInfo, command, workflowRunId) {
1204
+ return Object.freeze({
1205
+ name: `${command}-ai-antipattern-review.md`,
1206
+ content: [
1207
+ "---",
1208
+ `command: ${command}`,
1209
+ `target: ${targetInfo.target}`,
1210
+ `generated_at: ${MOCK_GENERATED_AT}`,
1211
+ `workflow_run_id: ${workflowRunId}`,
1212
+ "step: ai_antipattern_review",
1213
+ "cycle: 1",
1214
+ `reviewed_scope: ${command} mock smoke output`,
1215
+ "result: approved",
1216
+ "finding_count: 0",
1217
+ "blocking_finding_count: 0",
1218
+ "---",
1219
+ "",
1220
+ "# AI Antipattern Review Report",
1221
+ "",
1222
+ "Mock smoke found no AI-specific findings.",
1223
+ "",
1224
+ "## AI Findings",
1225
+ "",
1226
+ "| ID | Severity | Evidence |",
1227
+ "| --- | --- | --- |",
1228
+ "",
1229
+ ].join("\n"),
1230
+ });
1231
+ }
1232
+
1233
+ function mockSupervisionReport(targetInfo, command, workflowRunId) {
1234
+ return Object.freeze({
1235
+ name: `${command}-supervision.md`,
1236
+ content: [
1237
+ "---",
1238
+ `command: ${command}`,
1239
+ `target: ${targetInfo.target}`,
1240
+ `generated_at: ${MOCK_GENERATED_AT}`,
1241
+ `workflow_run_id: ${workflowRunId}`,
1242
+ "step: supervision",
1243
+ "cycle: 1",
1244
+ `state: ${mockCommandState(command)}`,
1245
+ "result: passed",
1246
+ "blocking_findings: 0",
1247
+ "major_findings: 0",
1248
+ "minor_findings: 0",
1249
+ "info_findings: 0",
1250
+ "---",
1251
+ "",
1252
+ "# Mock Supervision",
1253
+ "",
1254
+ "Result: passed",
1255
+ "",
1256
+ ].join("\n"),
1257
+ });
1258
+ }
1259
+
1260
+ function mockCommandState(command) {
1261
+ return {
1262
+ plan: "planned",
1263
+ compose: "composed",
1264
+ polish: "polished",
1265
+ deliver: "delivered",
1266
+ }[command];
1267
+ }
1268
+
1269
+ function workflowCommandArgs(target, options, extraArgs = []) {
1270
+ return [target, ...providerFlagArgs(options), ...extraArgs];
1271
+ }
1272
+
1273
+ function providerFlagArgs(options) {
1274
+ return options?.provider ? ["--provider", options.provider] : [];
1275
+ }
1276
+
1277
+ function runWorkflowNodeScript(name, script, args) {
1278
+ const commandLine = `node scripts/${script} ${args.map((arg) => JSON.stringify(arg)).join(" ")}`;
1279
+ const result = spawnSync(process.execPath, [path.join(SCRIPT_DIR, script), ...args], {
1280
+ cwd: ROOT,
1281
+ stdio: "inherit",
1282
+ timeout: WORKFLOW_COMMAND_TIMEOUT_MS,
1283
+ });
1284
+ assertSpawnSucceeded(result, name);
1285
+ return commandLine;
1286
+ }
1287
+
1288
+ function runNodeScript(name, script, args) {
1289
+ const scriptPath = path.join(SCRIPT_DIR, script);
1290
+ const commandLine = `node scripts/${script} ${args.map((arg) => JSON.stringify(arg)).join(" ")}`;
1291
+ const result = spawnSync(process.execPath, [scriptPath, ...args], {
1292
+ cwd: ROOT,
1293
+ stdio: "inherit",
1294
+ timeout: NODE_CHECK_TIMEOUT_MS,
1295
+ });
1296
+ assertSpawnSucceeded(result, name);
1297
+ return commandLine;
1298
+ }
1299
+
1300
+ function assertSpawnSucceeded(result, name) {
1301
+ if (result.error) {
1302
+ assert(false, `${name} failed while executing: ${result.error.message}`);
1303
+ }
1304
+ assert(result.status === 0, `${name} failed with exit code ${result.status ?? "unknown"}`);
1305
+ }
1306
+
1307
+ function assertNodeScriptFailure(name, script, args, expectedCode) {
1308
+ const scriptPath = path.join(SCRIPT_DIR, script);
1309
+ const commandLine = `node scripts/${script} ${args.map((arg) => JSON.stringify(arg)).join(" ")}`;
1310
+ const result = spawnSync(process.execPath, [scriptPath, ...args], {
1311
+ cwd: ROOT,
1312
+ encoding: "utf8",
1313
+ timeout: NODE_CHECK_TIMEOUT_MS,
1314
+ maxBuffer: CAPTURE_MAX_BUFFER,
1315
+ });
1316
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
1317
+ assert(result.status !== 0, `${name} unexpectedly succeeded`);
1318
+ assert(output.includes(`${expectedCode}:`), `${name} did not report ${expectedCode}: ${output}`);
1319
+ return commandLine;
1320
+ }
1321
+
1322
+ async function cleanApprovalPreflightState(targetInfo) {
1323
+ await Promise.all([
1324
+ rm(supervisionPath(targetInfo, "plan"), { force: true }),
1325
+ rm(supervisionPath(targetInfo, "compose"), { force: true }),
1326
+ rm(approvalPath(targetInfo, "plan"), { force: true }),
1327
+ rm(approvalPath(targetInfo, "compose"), { force: true }),
1328
+ ]);
1329
+ }
1330
+
1331
+ async function cleanApprovalCommandState(targetInfo) {
1332
+ await Promise.all(["plan", "compose", "polish", "deliver"].flatMap((command) => [
1333
+ rm(supervisionPath(targetInfo, command), { force: true }),
1334
+ rm(approvalPath(targetInfo, command), { force: true }),
1335
+ ]));
1336
+ }
1337
+
1338
+ function assertApprovalCommandStateAbsent(targetInfo, name) {
1339
+ for (const command of ["plan", "compose", "polish", "deliver"]) {
1340
+ assertApprovalFileAbsent(targetInfo, command, name);
1341
+ const filePath = supervisionPath(targetInfo, command);
1342
+ assert(!existsSync(filePath), `${name} left unexpected synthetic supervision file: ${relativePath(filePath)}`);
1343
+ }
1344
+ }
1345
+
1346
+ function assertApprovalFileAbsent(targetInfo, command, name) {
1347
+ const filePath = approvalPath(targetInfo, command);
1348
+ assert(!existsSync(filePath), `${name} left unexpected approval file: ${relativePath(filePath)}`);
1349
+ }
1350
+
1351
+ async function assertComposeSourceArtifacts(targetInfo) {
1352
+ const designSystemPath = path.join(targetInfo.deckPath, "design-system.md");
1353
+ const slidesPath = path.join(targetInfo.deckPath, "SLIDES.md");
1354
+ const imagesPath = path.join(targetInfo.deckPath, "images");
1355
+ await assertReadableFile(designSystemPath, "sequence:compose-source-artifacts");
1356
+ await assertReadableFile(slidesPath, "sequence:compose-source-artifacts");
1357
+ const entries = await readdir(imagesPath, { withFileTypes: true });
1358
+ const svgPaths = entries
1359
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".svg"))
1360
+ .map((entry) => path.join(imagesPath, entry.name));
1361
+ assert(svgPaths.length > 0, `sequence:compose-source-artifacts expected at least one SVG in ${relativePath(imagesPath)}`);
1362
+ for (const svgPath of svgPaths) {
1363
+ await assertReadableFile(svgPath, "sequence:compose-source-artifacts");
1364
+ }
1365
+ return Object.freeze([designSystemPath, slidesPath, ...svgPaths]);
1366
+ }
1367
+
1368
+ async function setupSyntheticRenderEvidenceDeck(targetPath) {
1369
+ await rm(targetPath, { recursive: true, force: true });
1370
+ await mkdir(path.join(targetPath, "review"), { recursive: true });
1371
+ await writeFile(path.join(targetPath, "brief.md"), "# Synthetic Render Evidence Validation\n", "utf8");
1372
+ }
1373
+
1374
+ async function writeSyntheticRenderEvidenceMetadata({ target, htmlPng, pdf, pdfRaster }) {
1375
+ const targetInfo = resolveDeckTarget(target, { root: ROOT });
1376
+ const evidenceRoot = path.join(ROOT, ".takt", "render", targetInfo.deckName, "cycle-1");
1377
+ await rm(evidenceRoot, { recursive: true, force: true });
1378
+ await mkdir(evidenceRoot, { recursive: true });
1379
+ await writeFile(path.join(evidenceRoot, "slide-1.png"), "synthetic png evidence\n", "utf8");
1380
+ await writeFile(path.join(evidenceRoot, "SLIDES.pdf"), "synthetic pdf evidence\n", "utf8");
1381
+ await writeFile(
1382
+ path.join(evidenceRoot, "metadata.json"),
1383
+ `${JSON.stringify(
1384
+ {
1385
+ deck: targetInfo.deckName,
1386
+ target: targetInfo.target,
1387
+ cycle: 1,
1388
+ html_png: htmlPng,
1389
+ pdf,
1390
+ pdf_raster: pdfRaster,
1391
+ },
1392
+ null,
1393
+ 2,
1394
+ )}\n`,
1395
+ "utf8",
1396
+ );
1397
+ await writeRenderEvidenceMarker(targetInfo, 1);
1398
+ }
1399
+
1400
+ async function writeRenderEvidenceMarker(targetInfo, cycle) {
1401
+ const metadataPath = path.join(ROOT, ".takt", "render", targetInfo.deckName, `cycle-${cycle}`, "metadata.json");
1402
+ const markerPath = path.join(ROOT, ".takt", "render", "latest-render-evidence.json");
1403
+ await mkdir(path.dirname(markerPath), { recursive: true });
1404
+ await writeFile(
1405
+ markerPath,
1406
+ `${JSON.stringify(
1407
+ {
1408
+ target: targetInfo.target,
1409
+ deck: targetInfo.deckName,
1410
+ cycle,
1411
+ metadata_path: relativePath(metadataPath),
1412
+ },
1413
+ null,
1414
+ 2,
1415
+ )}\n`,
1416
+ "utf8",
1417
+ );
1418
+ }
1419
+
1420
+ async function assertRenderEvidenceArtifacts(targetInfo, cycle) {
1421
+ const markerPath = path.join(ROOT, ".takt", "render", "latest-render-evidence.json");
1422
+ const marker = JSON.parse(await readFile(markerPath, "utf8"));
1423
+ assert(marker.target === targetInfo.target, `sequence:polish-render-evidence-marker expected ${targetInfo.target}, got ${marker.target}`);
1424
+ assert(marker.cycle === cycle, `sequence:polish-render-evidence-marker expected cycle ${cycle}, got ${marker.cycle}`);
1425
+
1426
+ const evidenceRoot = path.join(ROOT, ".takt", "render", targetInfo.deckName, `cycle-${cycle}`);
1427
+ const metadataPath = path.join(evidenceRoot, "metadata.json");
1428
+ assert(existsSync(evidenceRoot), `sequence:polish-render-evidence-root missing ${relativePath(evidenceRoot)}`);
1429
+ assert(marker.metadata_path === relativePath(metadataPath), `sequence:polish-render-evidence-marker metadata_path mismatch: ${marker.metadata_path}`);
1430
+
1431
+ const metadata = JSON.parse(await readFile(metadataPath, "utf8"));
1432
+ assert(metadata.target === targetInfo.target, `sequence:polish-render-evidence-metadata target mismatch: ${metadata.target}`);
1433
+ assert(metadata.cycle === cycle, `sequence:polish-render-evidence-metadata cycle mismatch: ${metadata.cycle}`);
1434
+ assertUsableRenderStatus("html_png", metadata.html_png);
1435
+ const htmlPngPaths = await assertRenderEvidenceFiles(evidenceRoot, "html_png", metadata.html_png.files);
1436
+ assertUsableRenderStatus("pdf", metadata.pdf);
1437
+ const pdfPath = await assertRenderEvidenceFile(evidenceRoot, "pdf", metadata.pdf.file);
1438
+ const pdfRasterPaths = await assertOptionalPdfRasterEvidence(evidenceRoot, metadata.pdf_raster);
1439
+
1440
+ return Object.freeze({
1441
+ evidenceRoot,
1442
+ observedPaths: Object.freeze([
1443
+ markerPath,
1444
+ evidenceRoot,
1445
+ metadataPath,
1446
+ ...htmlPngPaths,
1447
+ pdfPath,
1448
+ ...pdfRasterPaths,
1449
+ ]),
1450
+ });
1451
+ }
1452
+
1453
+ function assertUsableRenderStatus(key, item) {
1454
+ assert(item && typeof item.status === "string", `sequence:polish-render-evidence-metadata ${key} status missing`);
1455
+ assert(!["pending", "failed", "degraded", "skipped"].includes(item.status), `sequence:polish-render-evidence-metadata ${key} status not usable: ${item.status}`);
1456
+ }
1457
+
1458
+ async function assertOptionalPdfRasterEvidence(evidenceRoot, item) {
1459
+ assert(item && typeof item.status === "string", "sequence:polish-render-evidence-metadata pdf_raster status missing");
1460
+ assert(!["pending", "failed"].includes(item.status), `sequence:polish-render-evidence-metadata pdf_raster status not usable: ${item.status}`);
1461
+ if (["degraded", "skipped"].includes(item.status)) {
1462
+ assert(typeof item.reason === "string" && item.reason.trim(), `sequence:polish-render-evidence-metadata pdf_raster reason missing for ${item.status}`);
1463
+ return Object.freeze([]);
1464
+ }
1465
+ return assertRenderEvidenceFiles(evidenceRoot, "pdf_raster", item.files);
1466
+ }
1467
+
1468
+ async function assertRenderEvidenceFiles(evidenceRoot, key, files) {
1469
+ assert(Array.isArray(files) && files.length > 0, `sequence:polish-render-evidence-metadata ${key} files missing`);
1470
+ const paths = [];
1471
+ for (const [index, file] of files.entries()) {
1472
+ paths.push(await assertRenderEvidenceFile(evidenceRoot, `${key}[${index}]`, file));
1473
+ }
1474
+ return Object.freeze(paths);
1475
+ }
1476
+
1477
+ async function assertRenderEvidenceFile(evidenceRoot, label, value) {
1478
+ assert(typeof value === "string" && value.trim(), `sequence:polish-render-evidence-metadata ${label} path missing`);
1479
+ const filePath = resolveRenderEvidenceFile(evidenceRoot, value);
1480
+ await assertReadableFile(filePath, `sequence:polish-render-evidence-metadata ${label}`);
1481
+ return filePath;
1482
+ }
1483
+
1484
+ function resolveRenderEvidenceFile(evidenceRoot, value) {
1485
+ if (path.isAbsolute(value)) {
1486
+ return value;
1487
+ }
1488
+ const evidenceRelativePath = path.join(evidenceRoot, value);
1489
+ if (existsSync(evidenceRelativePath)) {
1490
+ return evidenceRelativePath;
1491
+ }
1492
+ return path.join(ROOT, value);
1493
+ }
1494
+
1495
+ async function seedStaleDeliveryArtifacts(targetInfo) {
1496
+ const distPath = path.join(ROOT, "dist", targetInfo.deckName);
1497
+ const staleArtifactPaths = [
1498
+ path.join(distPath, "SLIDES.pptx"),
1499
+ path.join(distPath, "stale.html"),
1500
+ ];
1501
+ await mkdir(distPath, { recursive: true });
1502
+ await Promise.all(staleArtifactPaths.map((filePath) => writeFile(filePath, `stale artifact for ${targetInfo.target}\n`, "utf8")));
1503
+ return Object.freeze(staleArtifactPaths);
1504
+ }
1505
+
1506
+ async function assertDeliveryArtifacts(targetInfo, options = {}) {
1507
+ const requested = await readRequestedDeliverables(targetInfo);
1508
+ const distPath = path.join(ROOT, "dist", targetInfo.deckName);
1509
+ const entries = existsSync(distPath) ? await readdir(distPath, { withFileTypes: true }) : [];
1510
+ const officialFiles = entries
1511
+ .filter((entry) => entry.isFile() && /\.(html|pdf|pptx)$/.test(entry.name))
1512
+ .map((entry) => entry.name);
1513
+ const expected = Object.freeze({ html: "SLIDES.html", pdf: "SLIDES.pdf", pptx: "SLIDES.pptx" });
1514
+ const artifactPaths = [];
1515
+
1516
+ for (const staleArtifactPath of options.staleArtifactPaths ?? []) {
1517
+ assert(!existsSync(staleArtifactPath), `sequence:deliver-stale-cleanup found stale artifact after deliver: ${relativePath(staleArtifactPath)}`);
1518
+ }
1519
+
1520
+ for (const item of requested) {
1521
+ const fileName = expected[item];
1522
+ assert(fileName, `sequence:deliver-artifacts unsupported deliverable ${item}`);
1523
+ assert(officialFiles.includes(fileName), `sequence:deliver-artifacts missing ${fileName}`);
1524
+ const filePath = path.join(distPath, fileName);
1525
+ await assertReadableFile(filePath, "sequence:deliver-artifacts");
1526
+ artifactPaths.push(filePath);
1527
+ }
1528
+
1529
+ for (const fileName of officialFiles) {
1530
+ const kind = Object.keys(expected).find((key) => expected[key] === fileName);
1531
+ assert(kind && requested.includes(kind), `sequence:deliver-artifacts found stale or unrequested artifact ${fileName}`);
1532
+ }
1533
+
1534
+ assertRenderEvidenceOutsideDeliveryArtifacts(options.renderEvidenceRoot, distPath, artifactPaths);
1535
+
1536
+ return Object.freeze({
1537
+ observedPaths: Object.freeze([distPath, ...artifactPaths]),
1538
+ });
1539
+ }
1540
+
1541
+ function assertRenderEvidenceOutsideDeliveryArtifacts(renderEvidenceRoot, distPath, artifactPaths) {
1542
+ assert(renderEvidenceRoot && existsSync(renderEvidenceRoot), `sequence:deliver-render-evidence-boundary missing render evidence root: ${renderEvidenceRoot}`);
1543
+ const relativeRenderRoot = relativePath(renderEvidenceRoot);
1544
+ assert(relativeRenderRoot.startsWith(".takt/render/"), `sequence:deliver-render-evidence-boundary expected render evidence under .takt/render, got ${relativeRenderRoot}`);
1545
+ for (const artifactPath of artifactPaths) {
1546
+ assert(artifactPath.startsWith(`${distPath}${path.sep}`), `sequence:deliver-render-evidence-boundary official artifact outside dist: ${relativePath(artifactPath)}`);
1547
+ assert(!artifactPath.startsWith(`${renderEvidenceRoot}${path.sep}`), `sequence:deliver-render-evidence-boundary counted render evidence as delivery artifact: ${relativePath(artifactPath)}`);
1548
+ }
1549
+ }
1550
+
1551
+ async function readRequestedDeliverables(targetInfo) {
1552
+ const plan = await readFile(path.join(targetInfo.deckPath, "plan.md"), "utf8");
1553
+ const match = plan.match(/deliverables\s*:\s*\[([^\]]*)\]/i);
1554
+ assert(match, "sequence:deliver-artifacts expected plan.md deliverables field");
1555
+ return Object.freeze(
1556
+ match[1]
1557
+ .split(",")
1558
+ .map((item) => item.trim().replace(/^['"]|['"]$/g, "").toLowerCase())
1559
+ .filter(Boolean),
1560
+ );
1561
+ }
1562
+
1563
+ async function assertReadableFile(filePath, name) {
1564
+ const content = await readFile(filePath, "utf8");
1565
+ assert(content.trim().length > 0, `${name} expected non-empty file: ${relativePath(filePath)}`);
1566
+ }
1567
+
1568
+ function assertWorkflowLoopMonitor(command, expected) {
1569
+ const workflowFile = path.join(ROOT, ".takt", "workflows", `takt-marp-slide-${command}.yaml`);
1570
+ const source = readFileSyncUtf8(workflowFile);
1571
+ const loopMonitorsIndex = source.indexOf("loop_monitors:");
1572
+ const stepsIndex = source.indexOf("\nsteps:");
1573
+ assert(loopMonitorsIndex !== -1, `${command} workflow missing TAKT loop_monitors`);
1574
+ assert(stepsIndex > loopMonitorsIndex, `${command} workflow loop_monitors must be defined before steps`);
1575
+
1576
+ const monitorBlock = source.slice(loopMonitorsIndex, stepsIndex);
1577
+ assertTextSequence(monitorBlock, expected.cycle.map((step) => `- ${step}`), `${command} loop monitor cycle`);
1578
+ assert(monitorBlock.includes("threshold: 3"), `${command} loop monitor threshold must be 3`);
1579
+ assert(monitorBlock.includes("persona: takt-marp-slide-supervisor"), `${command} loop monitor judge must use takt-marp-slide-supervisor`);
1580
+ assert(monitorBlock.includes("instruction: loop-monitor-reviewers-fix"), `${command} loop monitor must use TAKT built-in instruction`);
1581
+ assert(monitorBlock.includes(`next: ${expected.healthyNext}`), `${command} healthy loop monitor route must continue to ${expected.healthyNext}`);
1582
+ assert(monitorBlock.includes("next: ABORT"), `${command} nonproductive loop monitor route must abort`);
1583
+ assert(!source.includes("takt-marp-loop-monitor"), `${command} workflow must not use the deck-local loop monitor instruction`);
1584
+ assert(!source.includes("takt-marp-slide-loop-monitor"), `${command} workflow must not use the deck-local loop monitor persona`);
1585
+ assert(!source.includes(expected.removedMonitorStep), `${command} workflow must not use a deck-local monitor step`);
1586
+ assertRouteNext(source, "approved", expected.approvedNext);
1587
+ }
1588
+
1589
+ function assertAiGateWorkflowRoute(command, expected) {
1590
+ const workflowFile = path.join(ROOT, ".takt", "workflows", `takt-marp-slide-${command}.yaml`);
1591
+ const source = readFileSyncUtf8(workflowFile);
1592
+ const workStepBlock = workflowStepBlock(source, expected.workStep);
1593
+ const gateStepBlock = workflowStepBlock(source, expected.gateStep);
1594
+
1595
+ assertRouteNext(workStepBlock, "resultがpassed", expected.gateStep);
1596
+ assert(gateStepBlock.includes("kind: workflow_call"), `${command} AI gate step must use workflow_call`);
1597
+ assert(gateStepBlock.includes("call: ./takt-marp-slide-ai-quality-gate.yaml"), `${command} AI gate step must call takt-marp-slide-ai-quality-gate.yaml`);
1598
+ assertRouteNext(gateStepBlock, "COMPLETE", expected.normalReviewStep);
1599
+ assertRouteNext(gateStepBlock, "need_replan", expected.replanStep);
1600
+ assertRouteNext(gateStepBlock, "ABORT", "ABORT");
1601
+ assert(
1602
+ source.indexOf(` - name: ${expected.gateStep}`) < source.indexOf(` - name: ${expected.normalReviewStep}`),
1603
+ `${command} AI gate must appear before normal review step`,
1604
+ );
1605
+ }
1606
+
1607
+ function assertAiGateCallableWorkflowRules() {
1608
+ const workflowFile = path.join(ROOT, ".takt", "workflows", "takt-marp-slide-ai-quality-gate.yaml");
1609
+ const source = readFileSyncUtf8(workflowFile);
1610
+ const reviewStepBlock = workflowStepBlock(source, "ai-antipattern-review-1st");
1611
+ assertRouteNext(reviewStepBlock, "result approved and blocking_finding_count is 0", "COMPLETE");
1612
+ assert(
1613
+ !reviewStepBlock.includes("result approved and finding_count is 0"),
1614
+ "AI gate review completion must allow resolved finding rows after a fix cycle",
1615
+ );
1616
+ }
1617
+
1618
+ function workflowStepBlock(source, stepName) {
1619
+ const stepStart = source.indexOf(`\n - name: ${stepName}\n`);
1620
+ assert(stepStart !== -1, `workflow step not found: ${stepName}`);
1621
+ const nextStepStart = source.indexOf("\n - name: ", stepStart + 1);
1622
+ return nextStepStart === -1 ? source.slice(stepStart) : source.slice(stepStart, nextStepStart);
1623
+ }
1624
+
1625
+ function assertNoDeckLocalLoopMonitorFacets() {
1626
+ for (const filePath of [
1627
+ path.join(ROOT, ".takt", "facets", "instructions", "takt-marp-loop-monitor.md"),
1628
+ path.join(ROOT, ".takt", "facets", "personas", "takt-marp-slide-loop-monitor.md"),
1629
+ path.join(ROOT, ".takt", "facets", "output-contracts", "takt-marp-loop-monitor.md"),
1630
+ ]) {
1631
+ assert(!existsSync(filePath), `deck-local loop monitor facet still exists: ${relativePath(filePath)}`);
1632
+ }
1633
+ }
1634
+
1635
+ function assertWorkflowDoctorPasses() {
1636
+ const workflowPaths = [
1637
+ ...WORKFLOW_COMMANDS.map((command) => path.join(".takt", "workflows", `takt-marp-slide-${command}.yaml`)),
1638
+ path.join(".takt", "workflows", "takt-marp-slide-ai-quality-gate.yaml"),
1639
+ ];
1640
+ const result = spawnSync(runtimeExecutablePath("takt"), ["workflow", "doctor", ...workflowPaths], {
1641
+ cwd: ROOT,
1642
+ encoding: "utf8",
1643
+ timeout: NODE_CHECK_TIMEOUT_MS,
1644
+ maxBuffer: CAPTURE_MAX_BUFFER,
1645
+ });
1646
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
1647
+ assert(result.status === 0, `workflow doctor failed for AI gate workflow set: ${output}`);
1648
+ }
1649
+
1650
+ function assertTextSequence(source, snippets, name) {
1651
+ let cursor = -1;
1652
+ for (const snippet of snippets) {
1653
+ const nextIndex = source.indexOf(snippet, cursor + 1);
1654
+ assert(nextIndex !== -1, `${name} missing ordered snippet: ${snippet}`);
1655
+ cursor = nextIndex;
1656
+ }
1657
+ }
1658
+
1659
+ function assertNoUnsupportedWorkflowCommandGateObjects() {
1660
+ const workflowFiles = [
1661
+ ...WORKFLOW_COMMANDS.map((command) => path.join(ROOT, ".takt", "workflows", `takt-marp-slide-${command}.yaml`)),
1662
+ path.join(ROOT, ".takt", "workflows", "takt-marp-slide-ai-quality-gate.yaml"),
1663
+ ];
1664
+ for (const workflowFile of workflowFiles) {
1665
+ const source = readFileSyncUtf8(workflowFile);
1666
+ assert(!source.includes("type: command"), `${relativePath(workflowFile)} must not use command quality gate objects`);
1667
+ assert(!source.includes("{task}"), `${relativePath(workflowFile)} must not use unsupported {task} interpolation`);
1668
+ }
1669
+ }
1670
+
1671
+ function assertRouteNext(source, condition, next) {
1672
+ const conditionIndex = source.indexOf(`condition: ${condition}`);
1673
+ assert(conditionIndex !== -1, `workflow route condition not found: ${condition}`);
1674
+ const afterCondition = source.slice(conditionIndex, conditionIndex + 300);
1675
+ const nextLine = new RegExp(`(?:^|\\n)\\s*next:\\s*${escapeRegExp(next)}\\s*(?:\\n|$)`);
1676
+ assert(nextLine.test(afterCondition), `workflow route '${condition}' did not point to ${next}`);
1677
+ }
1678
+
1679
+ function escapeRegExp(value) {
1680
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1681
+ }
1682
+
1683
+ function readFileSyncUtf8(filePath) {
1684
+ return existsSync(filePath) ? readFileSync(filePath, "utf8") : "";
1685
+ }
1686
+
1687
+ async function writeSyntheticSupervision(targetInfo, command, { state, result, workflowRunId }) {
1688
+ await mkdir(targetInfo.reviewPath, { recursive: true });
1689
+ await writeFile(
1690
+ supervisionPath(targetInfo, command),
1691
+ [
1692
+ "---",
1693
+ `command: ${command}`,
1694
+ `target: ${targetInfo.target}`,
1695
+ "generated_at: 2026-06-06T00:00:00.000Z",
1696
+ `workflow_run_id: ${workflowRunId}`,
1697
+ "step: supervision",
1698
+ "cycle: 1",
1699
+ `state: ${state}`,
1700
+ `result: ${result}`,
1701
+ "blocking_findings: 0",
1702
+ "major_findings: 0",
1703
+ "minor_findings: 0",
1704
+ "info_findings: 0",
1705
+ "---",
1706
+ "",
1707
+ "# Synthetic Supervision",
1708
+ "",
1709
+ ].join("\n"),
1710
+ "utf8",
1711
+ );
1712
+ }
1713
+
1714
+ async function writeSyntheticApproval(targetInfo, command, { supervisionWorkflowRunId }) {
1715
+ await mkdir(targetInfo.reviewPath, { recursive: true });
1716
+ await writeFile(
1717
+ approvalPath(targetInfo, command),
1718
+ [
1719
+ "---",
1720
+ "status: approved",
1721
+ `command: ${command}`,
1722
+ `target: ${targetInfo.target}`,
1723
+ `approved_state: ${command === "plan" ? "planned" : "composed"}`,
1724
+ `supervision_workflow_run_id: ${supervisionWorkflowRunId}`,
1725
+ "approved_by: smoke-validation",
1726
+ "approved_at: 2026-06-06T00:01:00.000Z",
1727
+ "waivers: []",
1728
+ "decisions: []",
1729
+ "---",
1730
+ "",
1731
+ "# Synthetic Approval",
1732
+ "",
1733
+ ].join("\n"),
1734
+ "utf8",
1735
+ );
1736
+ }
1737
+
1738
+ function parseSmokeArgs(argv) {
1739
+ const options = { target: DEFAULT_TARGET, provider: DEFAULT_SMOKE_PROVIDER, keep: false, help: false };
1740
+
1741
+ for (let index = 0; index < argv.length; index += 1) {
1742
+ const arg = argv[index];
1743
+ if (arg === "--help" || arg === "-h") {
1744
+ return Object.freeze({ ...options, help: true });
1745
+ }
1746
+ if (arg === "--keep") {
1747
+ options.keep = true;
1748
+ continue;
1749
+ }
1750
+ if (arg === "--deck") {
1751
+ const value = argv[index + 1];
1752
+ if (!value || value.startsWith("--")) {
1753
+ throw new SlideWorkflowError("Missing value for --deck", "INVALID_ARGS");
1754
+ }
1755
+ options.target = value;
1756
+ index += 1;
1757
+ continue;
1758
+ }
1759
+ if (arg === "--provider") {
1760
+ const value = argv[index + 1];
1761
+ if (!value || value.startsWith("--")) {
1762
+ throw new SlideWorkflowError("Missing value for --provider", "INVALID_ARGS");
1763
+ }
1764
+ options.provider = value;
1765
+ index += 1;
1766
+ continue;
1767
+ }
1768
+ throw new SlideWorkflowError(`Unsupported argument: ${arg}`, "INVALID_ARGS");
1769
+ }
1770
+
1771
+ return Object.freeze(options);
1772
+ }
1773
+
1774
+ function printHelp() {
1775
+ console.log(
1776
+ [
1777
+ "Usage: node scripts/takt-marp-validate-slide-workflow-smoke.mjs [--deck slides/_workflow-smoke] [--provider mock] [--keep]",
1778
+ "",
1779
+ "Sets up the workflow smoke deck from fixtures and writes provider-specific smoke summary evidence.",
1780
+ "",
1781
+ "Options:",
1782
+ " --deck <target> Smoke deck target directory. Task 1.2 accepts only slides/_workflow-smoke",
1783
+ " --provider <name> TAKT provider for workflow execution. Defaults to mock for deterministic CI; pass claude, codex, etc. for real provider smoke.",
1784
+ " --keep Keep the generated smoke target and summary for inspection",
1785
+ " -h, --help Show this help",
1786
+ "",
1787
+ ].join("\n"),
1788
+ );
1789
+ }
1790
+
1791
+ async function setupSmokeDeck(target) {
1792
+ assertSmokeTarget(target);
1793
+ if (!existsSync(FIXTURE_PATH)) {
1794
+ throw new SlideWorkflowError(`Smoke fixture not found: ${relativePath(FIXTURE_PATH)}`, "FIXTURE_MISSING");
1795
+ }
1796
+
1797
+ const targetPath = path.join(ROOT, target);
1798
+ await rm(targetPath, { recursive: true, force: true });
1799
+ await mkdir(targetPath, { recursive: true });
1800
+
1801
+ const sourcePaths = await copyFixtureSources(FIXTURE_PATH, targetPath);
1802
+ const targetInfo = resolveDeckTarget(target, { root: ROOT });
1803
+ await cleanGeneratedOutputs(targetInfo, { root: ROOT });
1804
+ await mkdir(targetInfo.reviewPath, { recursive: true });
1805
+
1806
+ return Object.freeze({
1807
+ targetInfo,
1808
+ observedPaths: Object.freeze([
1809
+ ...sourcePaths.map(relativePath),
1810
+ relativePath(path.join(ROOT, "dist", targetInfo.deckName)),
1811
+ relativePath(path.join(ROOT, ".takt", "render", targetInfo.deckName)),
1812
+ ]),
1813
+ });
1814
+ }
1815
+
1816
+ function assertSmokeTarget(target) {
1817
+ if (target !== DEFAULT_TARGET) {
1818
+ throw new SlideWorkflowError(`Task 1.2 smoke setup is limited to ${DEFAULT_TARGET}`, "INVALID_TARGET");
1819
+ }
1820
+ }
1821
+
1822
+ async function copyFixtureSources(sourceRoot, destinationRoot) {
1823
+ const copied = [];
1824
+ const entries = await readdir(sourceRoot, { withFileTypes: true });
1825
+ for (const entry of entries) {
1826
+ if (SOURCE_FIXTURE_EXCLUDES.has(entry.name)) continue;
1827
+ const source = path.join(sourceRoot, entry.name);
1828
+ const destination = path.join(destinationRoot, entry.name);
1829
+ await cp(source, destination, { recursive: true });
1830
+ copied.push(destination);
1831
+ }
1832
+
1833
+ const briefPath = path.join(destinationRoot, "brief.md");
1834
+ if (!existsSync(briefPath)) {
1835
+ throw new SlideWorkflowError(`Smoke fixture must provide brief.md: ${relativePath(briefPath)}`, "FIXTURE_INVALID");
1836
+ }
1837
+ return Object.freeze(copied);
1838
+ }
1839
+
1840
+ async function writeSummary(summaryPath, data) {
1841
+ const rerunRisk = data.checks.some((check) => check.status === "PASS" && check.name === "failure-path:force-command")
1842
+ ? "- Rerun and force behavior are covered for successful rerun rejection, rejected rerun archive, and force invalidation."
1843
+ : data.checks.some((check) => check.status === "PASS" && check.name === "failure-path:successful-rerun-rejected")
1844
+ ? "- Successful rerun rejection is covered; rejected rerun archive and force invalidation remain later-task coverage."
1845
+ : "- Rerun/force behavior is not executed yet.";
1846
+ const content = [
1847
+ "---",
1848
+ `target: ${data.target}`,
1849
+ `provider: ${data.provider}`,
1850
+ `smoke_mode: ${data.smokeMode}`,
1851
+ `generated_at: ${new Date().toISOString()}`,
1852
+ `result: ${data.result}`,
1853
+ `commands_run: [${data.commands.map((command) => JSON.stringify(command)).join(", ")}]`,
1854
+ `failed_checks: [${data.checks.filter((check) => check.status === "FAIL").map((check) => JSON.stringify(check.name)).join(", ")}]`,
1855
+ "upstream_feedback_count: 0",
1856
+ "---",
1857
+ "",
1858
+ "# Smoke Summary",
1859
+ "",
1860
+ "## Executed Commands",
1861
+ "",
1862
+ ...listLines(data.commands),
1863
+ "",
1864
+ "## Checks",
1865
+ "",
1866
+ ...data.checks.map((check) => `- ${check.status}: ${check.name} - ${check.reason}`),
1867
+ "",
1868
+ "## Observed Paths",
1869
+ "",
1870
+ ...listLines(data.observedPaths),
1871
+ "",
1872
+ "## Failure Reasons",
1873
+ "",
1874
+ ...(data.failures.length > 0 ? listLines(data.failures) : ["- None"]),
1875
+ "",
1876
+ "## Residual Risks",
1877
+ "",
1878
+ rerunRisk,
1879
+ data.keep ? "- The smoke target is kept for inspection because --keep was provided." : "- The smoke summary is written under the target review directory for validation evidence.",
1880
+ "",
1881
+ ].join("\n");
1882
+
1883
+ await mkdir(path.dirname(summaryPath), { recursive: true });
1884
+ await writeFile(summaryPath, content, "utf8");
1885
+
1886
+ const written = await readFile(summaryPath, "utf8");
1887
+ if (!written.includes("# Smoke Summary")) {
1888
+ throw new SlideWorkflowError(`Smoke summary was not written correctly: ${relativePath(summaryPath)}`, "SUMMARY_INVALID");
1889
+ }
1890
+ }
1891
+
1892
+ function smokeSummaryFileName(options) {
1893
+ return isMockProvider(options)
1894
+ ? "smoke-summary-mock.md"
1895
+ : `smoke-summary-real-${safeFileSegment(options.provider)}.md`;
1896
+ }
1897
+
1898
+ function smokeMode(options) {
1899
+ return isMockProvider(options) ? "mock" : "real";
1900
+ }
1901
+
1902
+ function safeFileSegment(value) {
1903
+ return String(value).replace(/[^A-Za-z0-9._-]+/g, "-");
1904
+ }
1905
+
1906
+ function pass(name, reason) {
1907
+ return Object.freeze({ name, status: "PASS", reason });
1908
+ }
1909
+
1910
+ function fail(name, reason) {
1911
+ return Object.freeze({ name, status: "FAIL", reason });
1912
+ }
1913
+
1914
+ function listLines(items) {
1915
+ return items.length > 0 ? items.map((item) => `- ${item}`) : ["- None"];
1916
+ }
1917
+
1918
+ function firstLine(value) {
1919
+ return value.trim().split(/\r?\n/)[0] ?? "";
1920
+ }
1921
+
1922
+ function assert(condition, message) {
1923
+ if (!condition) {
1924
+ throw new SlideWorkflowError(message, "SMOKE_ASSERTION_FAILED");
1925
+ }
1926
+ }
1927
+
1928
+ function relativePath(filePath) {
1929
+ return path.relative(ROOT, filePath).replaceAll(path.sep, "/");
1930
+ }
1931
+
1932
+ main().catch((error) => {
1933
+ console.error(formatError(error));
1934
+ process.exit(1);
1935
+ });