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,125 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync } from "node:fs";
3
+ import { cp, mkdir, rm } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import {
6
+ TEMPLATE_DOMAINS,
7
+ assertNoProhibitedEntries,
8
+ diffTemplateTrees,
9
+ listTemplateEntries,
10
+ templateRootPath,
11
+ } from "./lib/takt-marp-project-templates.mjs";
12
+ import { resolveRuntimeContext } from "./lib/takt-marp-runtime-context.mjs";
13
+ import { SlideWorkflowError, formatError } from "./lib/takt-marp-slide-workflow.mjs";
14
+
15
+ const DRIFT_KINDS = [
16
+ { key: "missingInTemplate", label: "missing in template (exists only in dev .takt)" },
17
+ { key: "missingInDev", label: "missing in dev .takt (exists only in template)" },
18
+ { key: "contentMismatch", label: "content mismatch" },
19
+ ];
20
+
21
+ function usage() {
22
+ return [
23
+ "Usage: node scripts/takt-marp-sync-project-templates.mjs [--write]",
24
+ "",
25
+ "Default mode checks that templates/project matches the dev .takt",
26
+ `{${TEMPLATE_DOMAINS.join(",")}} trees and fails with TEMPLATE_DRIFT on any drift.`,
27
+ "",
28
+ "Options:",
29
+ " --write Sync dev .takt -> templates/project before checking.",
30
+ " -h, --help Show this help.",
31
+ "",
32
+ "Examples:",
33
+ " npm run installer:check-templates",
34
+ " npm run installer:sync-templates",
35
+ ].join("\n");
36
+ }
37
+
38
+ function parseSyncArgs(argv) {
39
+ const options = { write: false, help: false };
40
+ for (const arg of argv) {
41
+ if (arg === "--write") {
42
+ options.write = true;
43
+ } else if (arg === "--help" || arg === "-h") {
44
+ options.help = true;
45
+ } else {
46
+ throw new SlideWorkflowError(`Unknown argument '${arg}'.\n${usage()}`, "INVALID_ARGS");
47
+ }
48
+ }
49
+ return options;
50
+ }
51
+
52
+ async function main() {
53
+ const options = parseSyncArgs(process.argv.slice(2));
54
+ if (options.help) {
55
+ console.log(usage());
56
+ return;
57
+ }
58
+
59
+ const templateRoot = templateRootPath();
60
+ const devTaktRoot = path.join(resolveRuntimeContext().packageRoot, ".takt");
61
+
62
+ if (options.write) {
63
+ await syncTemplates(templateRoot, devTaktRoot);
64
+ }
65
+
66
+ const drift = await diffTemplateTrees(templateRoot, devTaktRoot);
67
+ const driftCount = DRIFT_KINDS.reduce((total, kind) => total + drift[kind.key].length, 0);
68
+ if (driftCount > 0) {
69
+ for (const kind of DRIFT_KINDS) {
70
+ const relativePaths = drift[kind.key];
71
+ if (relativePaths.length === 0) {
72
+ continue;
73
+ }
74
+ console.error(`${kind.label} (${relativePaths.length}):`);
75
+ for (const relativePath of relativePaths) {
76
+ console.error(` - ${relativePath}`);
77
+ }
78
+ }
79
+ throw new SlideWorkflowError(
80
+ `templates/project drifted from dev .takt in ${driftCount} path(s). Run \`npm run installer:sync-templates\` to sync.`,
81
+ "TEMPLATE_DRIFT",
82
+ );
83
+ }
84
+
85
+ const entryCount = (await listTemplateEntries({ templateRoot })).length;
86
+ console.log(`template check ok: templates/project matches dev .takt/{${TEMPLATE_DOMAINS.join(",")}} (${entryCount} files).`);
87
+ }
88
+
89
+ async function syncTemplates(templateRoot, devTaktRoot) {
90
+ const devEntries = await listTemplateEntries({ templateRoot: devTaktRoot });
91
+ assertNoProhibitedEntries(devEntries);
92
+
93
+ const devPaths = new Set(devEntries.map((entry) => entry.relativePath));
94
+ const removed = (await listTemplateEntries({ templateRoot }))
95
+ .map((entry) => entry.relativePath)
96
+ .filter((relativePath) => !devPaths.has(relativePath));
97
+
98
+ await mkdir(templateRoot, { recursive: true });
99
+ for (const domain of TEMPLATE_DOMAINS) {
100
+ const templateDomainDir = path.join(templateRoot, domain);
101
+ const devDomainDir = path.join(devTaktRoot, domain);
102
+ await rm(templateDomainDir, { recursive: true, force: true });
103
+ if (existsSync(devDomainDir)) {
104
+ await cp(devDomainDir, templateDomainDir, { recursive: true });
105
+ }
106
+ }
107
+
108
+ assertNoProhibitedEntries(await listTemplateEntries({ templateRoot }));
109
+
110
+ console.log(`synced templates/project from dev .takt (${devEntries.length} files):`);
111
+ for (const entry of devEntries) {
112
+ console.log(` - ${entry.relativePath}`);
113
+ }
114
+ if (removed.length > 0) {
115
+ console.log(`removed stale template files (${removed.length}):`);
116
+ for (const relativePath of removed) {
117
+ console.log(` - ${relativePath}`);
118
+ }
119
+ }
120
+ }
121
+
122
+ main().catch((error) => {
123
+ console.error(formatError(error));
124
+ process.exit(1);
125
+ });
@@ -0,0 +1,391 @@
1
+ #!/usr/bin/env node
2
+ // GlobalInstallValidator: E2E validation of the real `npm pack` tarball through a
3
+ // temporary global install prefix (requirements 8.1-8.5, plus the E2E confirmation
4
+ // of 1.1-1.3 and 4.2-4.3).
5
+ //
6
+ // Phase policy: phases run sequentially. If pack-install fails, every later phase
7
+ // is skipped (no installed CLI to exercise). Otherwise failures are collected and
8
+ // later phases keep running, except phases whose declared dependency failed
9
+ // (conflict-force and workflow-command-modes need the project initialized by
10
+ // init-boundary); those are skipped with the blocking phase named. Any failure
11
+ // ends in GLOBAL_INSTALL_VALIDATION_FAILED with the failed phase name(s).
12
+ //
13
+ // Everything runs inside os.tmpdir(); the repo working tree is never modified.
14
+ // Temp directories are removed on success and retained (with paths printed) on
15
+ // failure for diagnosis.
16
+ import { spawn } from "node:child_process";
17
+ import { createHash } from "node:crypto";
18
+ import { existsSync } from "node:fs";
19
+ import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
20
+ import os from "node:os";
21
+ import path from "node:path";
22
+ import { listTemplateEntries } from "./lib/takt-marp-project-templates.mjs";
23
+ import { resolveRuntimeContext } from "./lib/takt-marp-runtime-context.mjs";
24
+ import { SlideWorkflowError, formatError } from "./lib/takt-marp-slide-workflow.mjs";
25
+
26
+ const CLI_COMMANDS = ["init", "plan", "compose", "polish", "deliver", "smoke"];
27
+ // Runtime state / provider configuration that init must never generate (8.2).
28
+ const RUNTIME_STATE_NAMES = [
29
+ "config.yaml",
30
+ "runs",
31
+ "render",
32
+ "persona_sessions.json",
33
+ "session-state.json",
34
+ "workflow-current-target.json",
35
+ ];
36
+
37
+ const NPM_TIMEOUT_MS = 10 * 60 * 1000;
38
+ const CLI_TIMEOUT_MS = 5 * 60 * 1000;
39
+ // The mandatory mock smoke runs the full workflow suite; give it a long leash.
40
+ const SMOKE_TIMEOUT_MS = 20 * 60 * 1000;
41
+
42
+ const PREPLACED_NOTES_CONTENT = "unrelated project file: takt-marp init must leave this untouched\n";
43
+ const PREPLACED_LOCAL_CONTENT = "user-owned .takt note: not template-managed, must survive init and --force\n";
44
+
45
+ function check(condition, detail) {
46
+ if (!condition) {
47
+ throw new SlideWorkflowError(detail, "PHASE_ASSERTION_FAILED");
48
+ }
49
+ }
50
+
51
+ function outputTail(text, limit = 2000) {
52
+ return text.length > limit ? `...${text.slice(-limit)}` : text;
53
+ }
54
+
55
+ function commandSummary(result) {
56
+ const signalNote = result.signal ? ` (terminated by ${result.signal})` : "";
57
+ return `exit=${result.code}${signalNote}\nstdout: ${outputTail(result.stdout)}\nstderr: ${outputTail(result.stderr)}`;
58
+ }
59
+
60
+ function runCommand(command, args, options = {}) {
61
+ return new Promise((resolve, reject) => {
62
+ const child = spawn(command, args, {
63
+ cwd: options.cwd,
64
+ env: options.env ?? process.env,
65
+ shell: process.platform === "win32",
66
+ stdio: ["ignore", "pipe", "pipe"],
67
+ timeout: options.timeoutMs ?? CLI_TIMEOUT_MS,
68
+ killSignal: "SIGKILL",
69
+ });
70
+ let stdout = "";
71
+ let stderr = "";
72
+ child.stdout.setEncoding("utf8");
73
+ child.stderr.setEncoding("utf8");
74
+ child.stdout.on("data", (chunk) => {
75
+ stdout += chunk;
76
+ });
77
+ child.stderr.on("data", (chunk) => {
78
+ stderr += chunk;
79
+ });
80
+ child.on("error", reject);
81
+ child.on("close", (code, signal) => {
82
+ resolve({ code: code ?? 1, signal, stdout, stderr, output: `${stdout}\n${stderr}` });
83
+ });
84
+ });
85
+ }
86
+
87
+ function runNpm(args, options = {}) {
88
+ const npmCli = process.platform === "win32" ? "npm.cmd" : "npm";
89
+ return runCommand(npmCli, args, { ...options, timeoutMs: options.timeoutMs ?? NPM_TIMEOUT_MS });
90
+ }
91
+
92
+ // All takt-marp invocations resolve the binary via PATH with the temp prefix bin
93
+ // prepended (8.1: PATH-based execution, not a direct path to the install target).
94
+ function runTaktMarp(ctx, args, options = {}) {
95
+ return runCommand("takt-marp", args, {
96
+ cwd: options.cwd,
97
+ env: { ...process.env, PATH: `${ctx.prefixBin}${path.delimiter}${process.env.PATH ?? ""}` },
98
+ timeoutMs: options.timeoutMs ?? CLI_TIMEOUT_MS,
99
+ });
100
+ }
101
+
102
+ async function listFilesRecursive(rootDir) {
103
+ let dirents;
104
+ try {
105
+ dirents = await readdir(rootDir, { recursive: true, withFileTypes: true });
106
+ } catch (error) {
107
+ if (error.code === "ENOENT" || error.code === "ENOTDIR") {
108
+ return [];
109
+ }
110
+ throw error;
111
+ }
112
+ return dirents
113
+ .filter((dirent) => dirent.isFile())
114
+ .map((dirent) => path.relative(rootDir, path.join(dirent.parentPath, dirent.name)).split(path.sep).join("/"))
115
+ .sort();
116
+ }
117
+
118
+ async function snapshotDir(rootDir) {
119
+ const snapshot = new Map();
120
+ for (const relativePath of await listFilesRecursive(rootDir)) {
121
+ const content = await readFile(path.join(rootDir, ...relativePath.split("/")));
122
+ snapshot.set(relativePath, createHash("sha256").update(content).digest("hex"));
123
+ }
124
+ return snapshot;
125
+ }
126
+
127
+ function assertSnapshotsEqual(before, after, label) {
128
+ const changes = [];
129
+ for (const [relativePath, hash] of before) {
130
+ const afterHash = after.get(relativePath);
131
+ if (afterHash === undefined) {
132
+ changes.push(`removed: ${relativePath}`);
133
+ } else if (afterHash !== hash) {
134
+ changes.push(`modified: ${relativePath}`);
135
+ }
136
+ }
137
+ for (const relativePath of after.keys()) {
138
+ if (!before.has(relativePath)) {
139
+ changes.push(`added: ${relativePath}`);
140
+ }
141
+ }
142
+ check(changes.length === 0, `${label}: filesystem changed: ${changes.join(", ")}`);
143
+ }
144
+
145
+ async function assertFileContent(filePath, expectedContent, label) {
146
+ check(existsSync(filePath), `${label}: file is missing: ${filePath}`);
147
+ const actual = await readFile(filePath, "utf8");
148
+ check(actual === expectedContent, `${label}: file content changed: ${filePath}`);
149
+ }
150
+
151
+ // Phase 1 (8.1): real tarball -> temp global prefix; later phases run the CLI
152
+ // from that prefix via PATH.
153
+ async function phasePackInstall(ctx) {
154
+ const packDir = path.join(ctx.workDir, "pack");
155
+ await mkdir(packDir, { recursive: true });
156
+ const pack = await runNpm(["pack", "--pack-destination", packDir], { cwd: ctx.packageRoot });
157
+ check(pack.code === 0, `npm pack failed.\n${commandSummary(pack)}`);
158
+ const tarballs = (await readdir(packDir)).filter((name) => name.endsWith(".tgz"));
159
+ check(tarballs.length === 1, `expected exactly one tarball under ${packDir}, found: ${tarballs.join(", ") || "(none)"}`);
160
+ const tarballPath = path.join(packDir, tarballs[0]);
161
+
162
+ const prefixDir = path.join(ctx.workDir, "prefix");
163
+ await mkdir(prefixDir, { recursive: true });
164
+ const install = await runNpm(["install", "-g", "--prefix", prefixDir, "--no-audit", "--no-fund", tarballPath], {
165
+ cwd: ctx.workDir,
166
+ });
167
+ check(install.code === 0, `npm install -g --prefix failed.\n${commandSummary(install)}`);
168
+
169
+ ctx.prefixBin = process.platform === "win32" ? prefixDir : path.join(prefixDir, "bin");
170
+ const binPath = path.join(ctx.prefixBin, process.platform === "win32" ? "takt-marp.cmd" : "takt-marp");
171
+ check(existsSync(binPath), `installed takt-marp binary not found at ${binPath}`);
172
+ return `tarball ${tarballs[0]} installed into temp prefix`;
173
+ }
174
+
175
+ // Phase 2 (1.1, 1.2, 1.3): help lists all six commands; slide:* is rejected.
176
+ async function phaseSurface(ctx) {
177
+ const help = await runTaktMarp(ctx, ["--help"], { cwd: ctx.workDir });
178
+ check(help.code === 0, `takt-marp --help must exit 0.\n${commandSummary(help)}`);
179
+ for (const command of CLI_COMMANDS) {
180
+ check(
181
+ new RegExp(`^ ${command}\\b`, "m").test(help.stdout),
182
+ `takt-marp --help must list command '${command}'.\n${commandSummary(help)}`,
183
+ );
184
+ }
185
+
186
+ const unknown = await runTaktMarp(ctx, ["slide:plan"], { cwd: ctx.workDir });
187
+ check(unknown.code === 1, `takt-marp slide:plan must exit 1.\n${commandSummary(unknown)}`);
188
+ check(
189
+ unknown.output.includes("UNKNOWN_COMMAND"),
190
+ `takt-marp slide:plan must be rejected with UNKNOWN_COMMAND.\n${commandSummary(unknown)}`,
191
+ );
192
+ return "help lists 6 commands; slide:* rejected with UNKNOWN_COMMAND";
193
+ }
194
+
195
+ // Phase 3 (8.2): init generates exactly workflows/** + facets/** under .takt,
196
+ // no runtime state / provider configuration, and pre-placed files stay intact.
197
+ async function phaseInitBoundary(ctx) {
198
+ const projectDir = path.join(ctx.workDir, "project");
199
+ await mkdir(path.join(projectDir, ".takt"), { recursive: true });
200
+ await writeFile(path.join(projectDir, "notes.md"), PREPLACED_NOTES_CONTENT, "utf8");
201
+ await writeFile(path.join(projectDir, ".takt", "my-local.md"), PREPLACED_LOCAL_CONTENT, "utf8");
202
+
203
+ // The tarball was packed from this repo, so the repo template canon is the
204
+ // authoritative expected file set for what init must generate.
205
+ ctx.templateEntries = await listTemplateEntries();
206
+ check(ctx.templateEntries.length > 0, "template canon is empty; cannot validate init output");
207
+
208
+ const init = await runTaktMarp(ctx, ["init", "."], { cwd: projectDir });
209
+ check(init.code === 0, `takt-marp init . must exit 0.\n${commandSummary(init)}`);
210
+
211
+ const taktDir = path.join(projectDir, ".takt");
212
+ const observed = await listFilesRecursive(taktDir);
213
+ const expected = ["my-local.md", ...ctx.templateEntries.map((entry) => entry.relativePath)].sort();
214
+ const expectedSet = new Set(expected);
215
+ const observedSet = new Set(observed);
216
+ const unexpected = observed.filter((relativePath) => !expectedSet.has(relativePath));
217
+ const missing = expected.filter((relativePath) => !observedSet.has(relativePath));
218
+ check(
219
+ unexpected.length === 0 && missing.length === 0,
220
+ `init output mismatch under .takt.${unexpected.length > 0 ? ` unexpected: ${unexpected.join(", ")}.` : ""}${missing.length > 0 ? ` missing: ${missing.join(", ")}.` : ""}`,
221
+ );
222
+
223
+ for (const name of RUNTIME_STATE_NAMES) {
224
+ check(!existsSync(path.join(taktDir, name)), `init must not generate runtime state / provider config: .takt/${name}`);
225
+ }
226
+ const statePattern = /(^|\/)(config\.yaml|persona_sessions\.json|session-state\.json|workflow-current-target\.json)$|(^|\/)(runs|render)(\/|$)/;
227
+ const stateLeaks = observed.filter((relativePath) => statePattern.test(relativePath));
228
+ check(stateLeaks.length === 0, `runtime state / provider config found under .takt: ${stateLeaks.join(", ")}`);
229
+
230
+ await assertFileContent(path.join(projectDir, "notes.md"), PREPLACED_NOTES_CONTENT, "pre-placed project file after init");
231
+ await assertFileContent(path.join(taktDir, "my-local.md"), PREPLACED_LOCAL_CONTENT, "pre-placed .takt file after init");
232
+
233
+ ctx.projectDir = projectDir;
234
+ ctx.snapshotAfterInit = await snapshotDir(projectDir);
235
+ return `init generated exactly ${ctx.templateEntries.length} template files; pre-placed files intact`;
236
+ }
237
+
238
+ // Phase 4 (8.3): re-init fails with INIT_CONFLICT and zero writes; --force
239
+ // restores template-owned files to canon and leaves everything else alone.
240
+ async function phaseConflictForce(ctx) {
241
+ const reinit = await runTaktMarp(ctx, ["init", "."], { cwd: ctx.projectDir });
242
+ check(reinit.code === 1, `re-running takt-marp init . must exit 1.\n${commandSummary(reinit)}`);
243
+ check(reinit.output.includes("INIT_CONFLICT"), `re-init must fail with INIT_CONFLICT.\n${commandSummary(reinit)}`);
244
+ assertSnapshotsEqual(ctx.snapshotAfterInit, await snapshotDir(ctx.projectDir), "after rejected re-init (must be zero writes)");
245
+
246
+ const mutatedEntry = ctx.templateEntries[0];
247
+ const mutatedPath = path.join(ctx.projectDir, ".takt", ...mutatedEntry.relativePath.split("/"));
248
+ await writeFile(mutatedPath, "MUTATED-BY-GLOBAL-INSTALL-VALIDATOR\n", "utf8");
249
+
250
+ const force = await runTaktMarp(ctx, ["init", ".", "--force"], { cwd: ctx.projectDir });
251
+ check(force.code === 0, `takt-marp init . --force must exit 0.\n${commandSummary(force)}`);
252
+
253
+ const restored = await readFile(mutatedPath);
254
+ const canon = await readFile(mutatedEntry.sourcePath);
255
+ check(restored.equals(canon), `--force must restore template file to canon content: .takt/${mutatedEntry.relativePath}`);
256
+ await assertFileContent(path.join(ctx.projectDir, "notes.md"), PREPLACED_NOTES_CONTENT, "pre-placed project file after --force");
257
+ await assertFileContent(path.join(ctx.projectDir, ".takt", "my-local.md"), PREPLACED_LOCAL_CONTENT, "pre-placed .takt file after --force");
258
+ // Force only rewrites template-owned paths with canon bytes, so the whole
259
+ // project must be back to its exact post-init state.
260
+ assertSnapshotsEqual(ctx.snapshotAfterInit, await snapshotDir(ctx.projectDir), "after --force (only template-owned paths rewritten)");
261
+ return "INIT_CONFLICT with zero writes; --force restored canon without touching other files";
262
+ }
263
+
264
+ // Phase 5 (4.2, 4.3): uninitialized dirs get the init guidance; an initialized
265
+ // project without package.json / node_modules / .takt/config.yaml fails on the
266
+ // workflow target contract, never on npm-project absence. Per tasks.md
267
+ // Implementation Notes 2.6, TAKT itself needs no config.yaml here, so the fixed
268
+ // assertion for the config-absent failure mode is the target error (INVALID_TARGET).
269
+ async function phaseWorkflowCommandModes(ctx) {
270
+ const uninitializedDir = path.join(ctx.workDir, "uninitialized");
271
+ await mkdir(uninitializedDir, { recursive: true });
272
+ const uninitialized = await runTaktMarp(ctx, ["plan", "slides/x"], { cwd: uninitializedDir });
273
+ check(uninitialized.code === 1, `workflow command in an uninitialized dir must exit 1.\n${commandSummary(uninitialized)}`);
274
+ check(
275
+ uninitialized.output.includes("PROJECT_NOT_INITIALIZED"),
276
+ `uninitialized dir must fail with PROJECT_NOT_INITIALIZED.\n${commandSummary(uninitialized)}`,
277
+ );
278
+ check(
279
+ uninitialized.output.includes("takt-marp init ."),
280
+ `uninitialized failure must mention 'takt-marp init .'.\n${commandSummary(uninitialized)}`,
281
+ );
282
+
283
+ check(!existsSync(path.join(ctx.projectDir, "package.json")), "precondition: target project must not have package.json");
284
+ check(!existsSync(path.join(ctx.projectDir, "node_modules")), "precondition: target project must not have node_modules");
285
+ check(!existsSync(path.join(ctx.projectDir, ".takt", "config.yaml")), "precondition: target project must not have .takt/config.yaml");
286
+
287
+ const invalidTarget = await runTaktMarp(ctx, ["plan", "slides/missing-deck"], { cwd: ctx.projectDir });
288
+ check(invalidTarget.code === 1, `workflow command with a missing deck must exit 1.\n${commandSummary(invalidTarget)}`);
289
+ check(
290
+ invalidTarget.output.includes("INVALID_TARGET"),
291
+ `failure in the initialized non-npm project must be the workflow target error (INVALID_TARGET).\n${commandSummary(invalidTarget)}`,
292
+ );
293
+ for (const forbiddenReason of ["package.json", "node_modules", "PROJECT_NOT_INITIALIZED"]) {
294
+ check(
295
+ !invalidTarget.output.includes(forbiddenReason),
296
+ `failure must not blame npm-project absence or initialization (found '${forbiddenReason}').\n${commandSummary(invalidTarget)}`,
297
+ );
298
+ }
299
+ return "PROJECT_NOT_INITIALIZED guidance; non-npm project fails only on INVALID_TARGET";
300
+ }
301
+
302
+ // Phase 6 (8.4, 8.5): mock smoke (provider unspecified) is mandatory and must
303
+ // pass. Real provider smoke is intentionally never executed here and nothing in
304
+ // this validator reads or requires real-provider environment configuration.
305
+ async function phaseMockSmoke(ctx) {
306
+ const smokeCwd = path.join(ctx.workDir, "smoke-cwd");
307
+ await mkdir(smokeCwd, { recursive: true });
308
+ const smoke = await runTaktMarp(ctx, ["smoke"], { cwd: smokeCwd, timeoutMs: SMOKE_TIMEOUT_MS });
309
+ check(smoke.code === 0, `takt-marp smoke (mock default) must exit 0.\n${commandSummary(smoke)}`);
310
+ check(
311
+ smoke.stdout.includes("smoke-summary-mock.md"),
312
+ `smoke output must reference the mock summary (smoke-summary-mock.md).\n${commandSummary(smoke)}`,
313
+ );
314
+
315
+ const projectMatch = smoke.stdout.match(/^Temporary smoke project(?: \(retained for inspection\))?: (.+)$/m);
316
+ check(projectMatch !== null, `smoke output must print the temporary smoke project path.\n${commandSummary(smoke)}`);
317
+ ctx.smokeProjectDir = projectMatch[1].trim();
318
+
319
+ const summaryPath = path.join(ctx.smokeProjectDir, "slides", "_workflow-smoke", "review", "smoke-summary-mock.md");
320
+ check(existsSync(summaryPath), `mock smoke summary was not written: ${summaryPath}`);
321
+ const summary = await readFile(summaryPath, "utf8");
322
+ check(summary.includes("provider: mock"), `mock smoke summary must record 'provider: mock': ${summaryPath}`);
323
+ check(summary.includes("result: passed"), `mock smoke summary must record 'result: passed': ${summaryPath}`);
324
+ return "mock smoke passed; real provider smoke not executed and not required";
325
+ }
326
+
327
+ const PHASES = [
328
+ { name: "pack-install", run: phasePackInstall, deps: [] },
329
+ { name: "surface", run: phaseSurface, deps: [] },
330
+ { name: "init-boundary", run: phaseInitBoundary, deps: [] },
331
+ { name: "conflict-force", run: phaseConflictForce, deps: ["init-boundary"] },
332
+ { name: "workflow-command-modes", run: phaseWorkflowCommandModes, deps: ["init-boundary"] },
333
+ { name: "mock-smoke", run: phaseMockSmoke, deps: [] },
334
+ ];
335
+
336
+ function retainedPaths(ctx) {
337
+ return [ctx.workDir, ctx.smokeProjectDir].filter(Boolean);
338
+ }
339
+
340
+ async function cleanup(ctx) {
341
+ for (const dir of retainedPaths(ctx)) {
342
+ await rm(dir, { recursive: true, force: true });
343
+ }
344
+ }
345
+
346
+ async function main() {
347
+ const ctx = {
348
+ workDir: await mkdtemp(path.join(os.tmpdir(), "takt-marp-global-install-")),
349
+ packageRoot: resolveRuntimeContext().packageRoot,
350
+ };
351
+ const failures = [];
352
+ const failedNames = new Set();
353
+
354
+ for (const phase of PHASES) {
355
+ if (failedNames.has("pack-install")) {
356
+ console.error(`phase ${phase.name}: skipped (pack-install failed; no installed CLI to exercise)`);
357
+ continue;
358
+ }
359
+ const blockedBy = phase.deps.find((dep) => failedNames.has(dep));
360
+ if (blockedBy) {
361
+ console.error(`phase ${phase.name}: skipped (depends on failed phase '${blockedBy}')`);
362
+ continue;
363
+ }
364
+ try {
365
+ const note = await phase.run(ctx);
366
+ console.log(`phase ${phase.name}: ok${note ? ` (${note})` : ""}`);
367
+ } catch (error) {
368
+ failedNames.add(phase.name);
369
+ const detail = error instanceof SlideWorkflowError ? formatError(error) : String(error?.stack ?? error);
370
+ failures.push({ name: phase.name, detail });
371
+ console.error(`phase ${phase.name}: FAIL`);
372
+ console.error(detail.replace(/^/gm, " "));
373
+ }
374
+ }
375
+
376
+ if (failures.length > 0) {
377
+ console.error(`Retained for diagnosis: ${retainedPaths(ctx).join(", ")}`);
378
+ throw new SlideWorkflowError(
379
+ `${failures.length} phase(s) failed: ${failures.map((failure) => failure.name).join(", ")}. See phase details above.`,
380
+ "GLOBAL_INSTALL_VALIDATION_FAILED",
381
+ );
382
+ }
383
+
384
+ await cleanup(ctx);
385
+ console.log(`global install validation ok: ${PHASES.length} phases passed.`);
386
+ }
387
+
388
+ main().catch((error) => {
389
+ console.error(formatError(error));
390
+ process.exit(1);
391
+ });