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.
- package/README.ja.md +108 -0
- package/README.md +108 -0
- package/bin/takt-marp.mjs +24 -0
- package/fixtures/marp-slide-workflow/_workflow-smoke/README.md +23 -0
- package/fixtures/marp-slide-workflow/_workflow-smoke/brief.md +44 -0
- package/marp.config.mjs +3 -0
- package/package.json +56 -0
- package/scripts/lib/takt-marp-cli.mjs +199 -0
- package/scripts/lib/takt-marp-project-init.mjs +81 -0
- package/scripts/lib/takt-marp-project-templates.mjs +93 -0
- package/scripts/lib/takt-marp-runtime-context.mjs +24 -0
- package/scripts/lib/takt-marp-slide-workflow.mjs +453 -0
- package/scripts/takt-marp-approve-slide-workflow-state.mjs +37 -0
- package/scripts/takt-marp-build-slide-artifact.mjs +151 -0
- package/scripts/takt-marp-check-slide-workflow-state.mjs +41 -0
- package/scripts/takt-marp-render-slide-workflow-evidence.mjs +70 -0
- package/scripts/takt-marp-run-slide-workflow.mjs +435 -0
- package/scripts/takt-marp-sync-project-templates.mjs +125 -0
- package/scripts/takt-marp-validate-global-install.mjs +391 -0
- package/scripts/takt-marp-validate-package-boundary.mjs +276 -0
- package/scripts/takt-marp-validate-slide-workflow-foundation.mjs +571 -0
- package/scripts/takt-marp-validate-slide-workflow-smoke.mjs +1935 -0
- package/scripts/takt-marp-verify-delivery-artifacts.mjs +181 -0
- package/scripts/takt-marp-verify-render-evidence-metadata.mjs +133 -0
- package/templates/project/facets/instructions/takt-marp-ai-antipattern-fix.md +47 -0
- package/templates/project/facets/instructions/takt-marp-ai-antipattern-review.md +37 -0
- package/templates/project/facets/instructions/takt-marp-compose-fix.md +25 -0
- package/templates/project/facets/instructions/takt-marp-compose-review.md +30 -0
- package/templates/project/facets/instructions/takt-marp-compose-slides.md +35 -0
- package/templates/project/facets/instructions/takt-marp-compose-work-summary.md +23 -0
- package/templates/project/facets/instructions/takt-marp-deliver-build.md +30 -0
- package/templates/project/facets/instructions/takt-marp-deliver-fix.md +25 -0
- package/templates/project/facets/instructions/takt-marp-deliver-verify.md +25 -0
- package/templates/project/facets/instructions/takt-marp-design-system.md +37 -0
- package/templates/project/facets/instructions/takt-marp-intake.md +15 -0
- package/templates/project/facets/instructions/takt-marp-normalize-brief.md +24 -0
- package/templates/project/facets/instructions/takt-marp-plan-fix.md +26 -0
- package/templates/project/facets/instructions/takt-marp-plan-review.md +24 -0
- package/templates/project/facets/instructions/takt-marp-plan-work-summary.md +24 -0
- package/templates/project/facets/instructions/takt-marp-plan.md +26 -0
- package/templates/project/facets/instructions/takt-marp-polish-fix.md +25 -0
- package/templates/project/facets/instructions/takt-marp-polish-inspect.md +25 -0
- package/templates/project/facets/instructions/takt-marp-render-evidence.md +35 -0
- package/templates/project/facets/instructions/takt-marp-supervise-command.md +58 -0
- package/templates/project/facets/instructions/takt-marp-visual-generate.md +26 -0
- package/templates/project/facets/knowledge/takt-marp-repo-conventions.md +119 -0
- package/templates/project/facets/output-contracts/takt-marp-ai-antipattern-fix.md +48 -0
- package/templates/project/facets/output-contracts/takt-marp-ai-antipattern-review.md +43 -0
- package/templates/project/facets/output-contracts/takt-marp-command-fix.md +32 -0
- package/templates/project/facets/output-contracts/takt-marp-command-review.md +32 -0
- package/templates/project/facets/output-contracts/takt-marp-command-work.md +42 -0
- package/templates/project/facets/output-contracts/takt-marp-normalized-brief.md +31 -0
- package/templates/project/facets/output-contracts/takt-marp-slide-plan.md +30 -0
- package/templates/project/facets/output-contracts/takt-marp-supervision.md +45 -0
- package/templates/project/facets/personas/takt-marp-slide-planner.md +24 -0
- package/templates/project/facets/personas/takt-marp-slide-qa.md +23 -0
- package/templates/project/facets/personas/takt-marp-slide-reviewer.md +22 -0
- package/templates/project/facets/personas/takt-marp-slide-reviser.md +22 -0
- package/templates/project/facets/personas/takt-marp-slide-supervisor.md +24 -0
- package/templates/project/facets/personas/takt-marp-slide-writer.md +22 -0
- package/templates/project/facets/policies/takt-marp-general-slide-quality.md +91 -0
- package/templates/project/facets/policies/takt-marp-slide-quality.md +73 -0
- package/templates/project/facets/policies/takt-marp-svg-first-visual.md +66 -0
- package/templates/project/facets/policies/takt-marp-worker-boundary.md +32 -0
- package/templates/project/workflows/takt-marp-slide-ai-quality-gate.yaml +125 -0
- package/templates/project/workflows/takt-marp-slide-compose.yaml +209 -0
- package/templates/project/workflows/takt-marp-slide-deliver.yaml +164 -0
- package/templates/project/workflows/takt-marp-slide-plan.yaml +213 -0
- package/templates/project/workflows/takt-marp-slide-polish.yaml +158 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolveRuntimeContext } from "./takt-marp-runtime-context.mjs";
|
|
4
|
+
import { SlideWorkflowError } from "./takt-marp-slide-workflow.mjs";
|
|
5
|
+
|
|
6
|
+
export const TEMPLATE_DOMAINS = Object.freeze(["workflows", "facets"]);
|
|
7
|
+
|
|
8
|
+
export const PROHIBITED_TEMPLATE_PATTERNS = Object.freeze([
|
|
9
|
+
/(^|\/)config\.yaml$/i,
|
|
10
|
+
/(^|\/)runs(\/|$)/i,
|
|
11
|
+
/(^|\/)render(\/|$)/i,
|
|
12
|
+
/(^|\/)persona_sessions\.json$/i,
|
|
13
|
+
/(^|\/)session-state\.json$/i,
|
|
14
|
+
/(^|\/)workflow-current-target\.json$/i,
|
|
15
|
+
/(^|\/)\.env(\.|\/|$)/i,
|
|
16
|
+
/credential/i,
|
|
17
|
+
/api[^/]*key/i,
|
|
18
|
+
/secret/i,
|
|
19
|
+
/token/i,
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
export function templateRootPath() {
|
|
23
|
+
return path.join(resolveRuntimeContext().packageRoot, "templates", "project");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function listDomainFiles(templateRoot, domain) {
|
|
27
|
+
const domainDir = path.join(templateRoot, domain);
|
|
28
|
+
let dirents;
|
|
29
|
+
try {
|
|
30
|
+
dirents = await readdir(domainDir, { recursive: true, withFileTypes: true });
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error.code === "ENOENT" || error.code === "ENOTDIR") {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
return dirents
|
|
38
|
+
.filter((dirent) => dirent.isFile())
|
|
39
|
+
.map((dirent) => {
|
|
40
|
+
const absolutePath = path.join(dirent.parentPath, dirent.name);
|
|
41
|
+
return path.relative(templateRoot, absolutePath).split(path.sep).join("/");
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function listTemplateEntries(options = {}) {
|
|
46
|
+
const templateRoot = options.templateRoot ?? templateRootPath();
|
|
47
|
+
const entries = [];
|
|
48
|
+
for (const domain of TEMPLATE_DOMAINS) {
|
|
49
|
+
for (const relativePath of await listDomainFiles(templateRoot, domain)) {
|
|
50
|
+
entries.push({ domain, relativePath, sourcePath: path.join(templateRoot, relativePath) });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
entries.sort((a, b) => (a.relativePath < b.relativePath ? -1 : a.relativePath > b.relativePath ? 1 : 0));
|
|
54
|
+
return entries;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function assertNoProhibitedEntries(entries) {
|
|
58
|
+
const offending = entries
|
|
59
|
+
.map((entry) => entry.relativePath)
|
|
60
|
+
.filter((relativePath) => PROHIBITED_TEMPLATE_PATTERNS.some((pattern) => pattern.test(relativePath)))
|
|
61
|
+
.sort();
|
|
62
|
+
if (offending.length > 0) {
|
|
63
|
+
throw new SlideWorkflowError(
|
|
64
|
+
`Template tree contains prohibited entries: ${offending.join(", ")}`,
|
|
65
|
+
"PACKAGE_BOUNDARY_VIOLATION",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function diffTemplateTrees(templateRoot, devTaktRoot) {
|
|
71
|
+
const templateEntries = await listTemplateEntries({ templateRoot });
|
|
72
|
+
const devEntries = await listTemplateEntries({ templateRoot: devTaktRoot });
|
|
73
|
+
const templatePaths = new Map(templateEntries.map((entry) => [entry.relativePath, entry.sourcePath]));
|
|
74
|
+
const devPaths = new Map(devEntries.map((entry) => [entry.relativePath, entry.sourcePath]));
|
|
75
|
+
|
|
76
|
+
const missingInTemplate = [...devPaths.keys()].filter((relativePath) => !templatePaths.has(relativePath)).sort();
|
|
77
|
+
const missingInDev = [...templatePaths.keys()].filter((relativePath) => !devPaths.has(relativePath)).sort();
|
|
78
|
+
|
|
79
|
+
const contentMismatch = [];
|
|
80
|
+
for (const [relativePath, sourcePath] of templatePaths) {
|
|
81
|
+
const devPath = devPaths.get(relativePath);
|
|
82
|
+
if (!devPath) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const [templateContent, devContent] = await Promise.all([readFile(sourcePath), readFile(devPath)]);
|
|
86
|
+
if (!templateContent.equals(devContent)) {
|
|
87
|
+
contentMismatch.push(relativePath);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
contentMismatch.sort();
|
|
91
|
+
|
|
92
|
+
return { missingInTemplate, missingInDev, contentMismatch };
|
|
93
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
|
|
4
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
5
|
+
const RUNTIME_TOOLS = ["takt", "marp"];
|
|
6
|
+
|
|
7
|
+
export function resolveRuntimeContext() {
|
|
8
|
+
return {
|
|
9
|
+
packageRoot: PACKAGE_ROOT,
|
|
10
|
+
runtimeBinDir: path.join(PACKAGE_ROOT, "node_modules", ".bin"),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function runtimeExecutablePath(tool, options = {}) {
|
|
15
|
+
if (!RUNTIME_TOOLS.includes(tool)) {
|
|
16
|
+
throw new Error(`Unknown runtime tool: ${tool}. Expected one of: ${RUNTIME_TOOLS.join(", ")}.`);
|
|
17
|
+
}
|
|
18
|
+
const root = options.root ?? PACKAGE_ROOT;
|
|
19
|
+
return path.join(root, "node_modules", ".bin", process.platform === "win32" ? `${tool}.cmd` : tool);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function packageScriptPath(relative) {
|
|
23
|
+
return path.join(PACKAGE_ROOT, relative);
|
|
24
|
+
}
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import { accessSync, constants, existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { runtimeExecutablePath } from "./takt-marp-runtime-context.mjs";
|
|
6
|
+
|
|
7
|
+
export const COMMANDS = ["plan", "compose", "polish", "deliver"];
|
|
8
|
+
export const COMMAND_STATES = {
|
|
9
|
+
plan: "planned",
|
|
10
|
+
compose: "composed",
|
|
11
|
+
polish: "polished",
|
|
12
|
+
deliver: "delivered",
|
|
13
|
+
};
|
|
14
|
+
export const APPROVAL_COMMANDS = ["plan", "compose"];
|
|
15
|
+
|
|
16
|
+
export class SlideWorkflowError extends Error {
|
|
17
|
+
constructor(message, code = "SLIDE_WORKFLOW_ERROR") {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = "SlideWorkflowError";
|
|
20
|
+
this.code = code;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function parseArgs(argv) {
|
|
25
|
+
const positional = [];
|
|
26
|
+
const flags = {};
|
|
27
|
+
|
|
28
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
29
|
+
const arg = argv[index];
|
|
30
|
+
if (arg === "--force") {
|
|
31
|
+
flags.force = true;
|
|
32
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
33
|
+
flags.help = true;
|
|
34
|
+
} else if (arg.startsWith("--")) {
|
|
35
|
+
const key = arg.slice(2);
|
|
36
|
+
const value = argv[index + 1];
|
|
37
|
+
if (!value || value.startsWith("--")) {
|
|
38
|
+
throw new SlideWorkflowError(`Missing value for --${key}`, "INVALID_ARGS");
|
|
39
|
+
}
|
|
40
|
+
flags[key] = value;
|
|
41
|
+
index += 1;
|
|
42
|
+
} else {
|
|
43
|
+
positional.push(arg);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { positional, flags };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function formatError(error) {
|
|
51
|
+
return error instanceof SlideWorkflowError ? `${error.code}: ${error.message}` : String(error?.stack ?? error);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function requireCommand(command) {
|
|
55
|
+
if (!COMMANDS.includes(command)) {
|
|
56
|
+
throw new SlideWorkflowError(
|
|
57
|
+
`Unsupported command '${command}'. Expected one of: ${COMMANDS.join(", ")}`,
|
|
58
|
+
"INVALID_COMMAND",
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return command;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function resolveDeckTarget(target, options = {}) {
|
|
65
|
+
const root = options.root ?? process.cwd();
|
|
66
|
+
if (!target) {
|
|
67
|
+
throw new SlideWorkflowError("Missing target. Expected target: slides/<deck>", "INVALID_TARGET");
|
|
68
|
+
}
|
|
69
|
+
if (target.endsWith(".md")) {
|
|
70
|
+
throw new SlideWorkflowError(
|
|
71
|
+
`Invalid target '${target}'. Expected target: slides/<deck>; pass the deck directory instead of a Markdown file.`,
|
|
72
|
+
"INVALID_TARGET",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const normalized = path.posix.normalize(target.replaceAll(path.sep, "/"));
|
|
77
|
+
if (normalized.startsWith("../") || normalized === ".." || path.isAbsolute(target)) {
|
|
78
|
+
throw new SlideWorkflowError(`Invalid target '${target}'. Target must be under slides/<deck>`, "INVALID_TARGET");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const parts = normalized.split("/");
|
|
82
|
+
if (parts.length !== 2 || parts[0] !== "slides" || !parts[1] || parts[1] === "." || parts[1] === "..") {
|
|
83
|
+
throw new SlideWorkflowError(`Invalid target '${target}'. Expected target: slides/<deck>`, "INVALID_TARGET");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const deckPath = path.join(root, parts[0], parts[1]);
|
|
87
|
+
if (!existsSync(deckPath)) {
|
|
88
|
+
throw new SlideWorkflowError(`Deck directory not found: ${normalized}`, "INVALID_TARGET");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return Object.freeze({
|
|
92
|
+
deckName: parts[1],
|
|
93
|
+
target: normalized,
|
|
94
|
+
deckPath,
|
|
95
|
+
reviewPath: path.join(deckPath, "review"),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function workflowPath(command, options = {}) {
|
|
100
|
+
requireCommand(command);
|
|
101
|
+
const root = options.root ?? process.cwd();
|
|
102
|
+
return path.join(root, ".takt", "workflows", `takt-marp-slide-${command}.yaml`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function assertWorkflowAvailable(command, options = {}) {
|
|
106
|
+
const expectedPath = workflowPath(command, options);
|
|
107
|
+
if (!existsSync(expectedPath)) {
|
|
108
|
+
throw new SlideWorkflowError(
|
|
109
|
+
`Workflow YAML is not implemented: ${path.relative(options.root ?? process.cwd(), expectedPath)}. ` +
|
|
110
|
+
"Implement it in the slide-workflow-orchestration spec before running this command.",
|
|
111
|
+
"WORKFLOW_NOT_IMPLEMENTED",
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
return expectedPath;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function taktExecutablePath(options = {}) {
|
|
118
|
+
return runtimeExecutablePath("takt", options);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function assertTaktExecutableAvailable(options = {}) {
|
|
122
|
+
const executablePath = taktExecutablePath(options);
|
|
123
|
+
try {
|
|
124
|
+
accessSync(executablePath, constants.X_OK);
|
|
125
|
+
} catch {
|
|
126
|
+
throw new SlideWorkflowError(
|
|
127
|
+
`TAKT executable is not available: ${executablePath}. Reinstall takt-marp (npm install -g takt-marp) and verify its dependencies.`,
|
|
128
|
+
"TAKT_EXECUTABLE_MISSING",
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
return executablePath;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function parseFrontMatter(markdown) {
|
|
135
|
+
if (!markdown.startsWith("---\n")) {
|
|
136
|
+
throw new SlideWorkflowError("Missing YAML front matter", "FRONT_MATTER_MISSING");
|
|
137
|
+
}
|
|
138
|
+
const end = markdown.indexOf("\n---", 4);
|
|
139
|
+
if (end === -1) {
|
|
140
|
+
throw new SlideWorkflowError("Unclosed YAML front matter", "FRONT_MATTER_INVALID");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const body = markdown.slice(end + 4).replace(/^\n/, "");
|
|
144
|
+
const frontMatter = {};
|
|
145
|
+
const lines = markdown.slice(4, end).split("\n");
|
|
146
|
+
|
|
147
|
+
for (const line of lines) {
|
|
148
|
+
if (!line.trim()) continue;
|
|
149
|
+
if (/^\s/.test(line) || line.trim().startsWith("- ")) {
|
|
150
|
+
throw new SlideWorkflowError(`Unsupported front matter syntax: ${line}`, "FRONT_MATTER_UNSUPPORTED");
|
|
151
|
+
}
|
|
152
|
+
const match = line.match(/^([A-Za-z0-9_-]+):(?:\s*(.*))?$/);
|
|
153
|
+
if (!match) {
|
|
154
|
+
throw new SlideWorkflowError(`Unsupported front matter syntax: ${line}`, "FRONT_MATTER_UNSUPPORTED");
|
|
155
|
+
}
|
|
156
|
+
frontMatter[match[1]] = parseScalar(match[2] ?? "");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return Object.freeze({ frontMatter: Object.freeze(frontMatter), body });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function parseScalar(value) {
|
|
163
|
+
const trimmed = value.trim();
|
|
164
|
+
if (trimmed === "[]") return [];
|
|
165
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) return parseInlineArray(trimmed);
|
|
166
|
+
if (trimmed === "true") return true;
|
|
167
|
+
if (trimmed === "false") return false;
|
|
168
|
+
if (/^-?\d+$/.test(trimmed)) return Number(trimmed);
|
|
169
|
+
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
170
|
+
return trimmed.slice(1, -1);
|
|
171
|
+
}
|
|
172
|
+
if (trimmed.includes("[") || trimmed.includes("]") || trimmed.includes("{") || trimmed.includes("}")) {
|
|
173
|
+
throw new SlideWorkflowError(`Unsupported scalar value: ${value}`, "FRONT_MATTER_UNSUPPORTED");
|
|
174
|
+
}
|
|
175
|
+
return trimmed;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function parseInlineArray(value) {
|
|
179
|
+
const inner = value.slice(1, -1).trim();
|
|
180
|
+
if (!inner) return [];
|
|
181
|
+
if (inner.includes("[") || inner.includes("]") || inner.includes("{") || inner.includes("}")) {
|
|
182
|
+
throw new SlideWorkflowError(`Unsupported scalar value: ${value}`, "FRONT_MATTER_UNSUPPORTED");
|
|
183
|
+
}
|
|
184
|
+
return inner.split(",").map((item) => parseArrayItem(item.trim()));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function parseArrayItem(value) {
|
|
188
|
+
if (!value) {
|
|
189
|
+
throw new SlideWorkflowError("Empty inline array item is unsupported", "FRONT_MATTER_UNSUPPORTED");
|
|
190
|
+
}
|
|
191
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
192
|
+
return value.slice(1, -1);
|
|
193
|
+
}
|
|
194
|
+
if (value.includes(":")) {
|
|
195
|
+
throw new SlideWorkflowError(`Unsupported inline array item: ${value}`, "FRONT_MATTER_UNSUPPORTED");
|
|
196
|
+
}
|
|
197
|
+
return value;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function readFrontMatter(filePath) {
|
|
201
|
+
if (!existsSync(filePath)) {
|
|
202
|
+
throw new SlideWorkflowError(`Missing file: ${filePath}`, "FILE_MISSING");
|
|
203
|
+
}
|
|
204
|
+
return parseFrontMatter(await readFile(filePath, "utf8")).frontMatter;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function supervisionPath(targetInfo, command) {
|
|
208
|
+
return path.join(targetInfo.reviewPath, `${command}-supervision.md`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function approvalPath(targetInfo, command) {
|
|
212
|
+
return path.join(targetInfo.reviewPath, `${command}-approval.md`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function readSupervision(targetInfo, command) {
|
|
216
|
+
requireCommand(command);
|
|
217
|
+
const filePath = supervisionPath(targetInfo, command);
|
|
218
|
+
const data = await readFrontMatter(filePath);
|
|
219
|
+
validateSupervision(data, targetInfo, command);
|
|
220
|
+
return Object.freeze({ filePath, data });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function validateSupervision(data, targetInfo, command) {
|
|
224
|
+
const expectedState = COMMAND_STATES[command];
|
|
225
|
+
requireField(data, "command", command);
|
|
226
|
+
requireField(data, "target", targetInfo.target);
|
|
227
|
+
requireField(data, "step", "supervision");
|
|
228
|
+
requireField(data, "workflow_run_id");
|
|
229
|
+
requireNumberField(data, "cycle");
|
|
230
|
+
requireField(data, "state");
|
|
231
|
+
requireField(data, "result");
|
|
232
|
+
requireNumberField(data, "blocking_findings");
|
|
233
|
+
requireNumberField(data, "major_findings");
|
|
234
|
+
requireNumberField(data, "minor_findings");
|
|
235
|
+
requireNumberField(data, "info_findings");
|
|
236
|
+
requireDate(data, "generated_at");
|
|
237
|
+
if (data.result === "passed" && data.state !== expectedState) {
|
|
238
|
+
throw new SlideWorkflowError(
|
|
239
|
+
`Invalid supervision state for ${command}. Expected '${expectedState}', got '${data.state}'`,
|
|
240
|
+
"STATE_MISMATCH",
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export async function readApproval(targetInfo, command, supervision) {
|
|
246
|
+
if (!APPROVAL_COMMANDS.includes(command)) {
|
|
247
|
+
throw new SlideWorkflowError(`Approval is supported only for: ${APPROVAL_COMMANDS.join(", ")}`, "APPROVAL_UNSUPPORTED");
|
|
248
|
+
}
|
|
249
|
+
const filePath = approvalPath(targetInfo, command);
|
|
250
|
+
const data = await readFrontMatter(filePath);
|
|
251
|
+
validateApproval(data, targetInfo, command, supervision);
|
|
252
|
+
return Object.freeze({ filePath, data });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function validateApproval(data, targetInfo, command, supervision) {
|
|
256
|
+
requireField(data, "status", "approved");
|
|
257
|
+
requireField(data, "command", command);
|
|
258
|
+
requireField(data, "target", targetInfo.target);
|
|
259
|
+
requireField(data, "approved_state", COMMAND_STATES[command]);
|
|
260
|
+
requireField(data, "approved_by");
|
|
261
|
+
requireField(data, "supervision_workflow_run_id", supervision.workflow_run_id);
|
|
262
|
+
requireDate(data, "approved_at");
|
|
263
|
+
if (!Array.isArray(data.waivers)) {
|
|
264
|
+
throw new SlideWorkflowError("Approval field 'waivers' must be an array", "APPROVAL_INVALID");
|
|
265
|
+
}
|
|
266
|
+
if (!Array.isArray(data.decisions)) {
|
|
267
|
+
throw new SlideWorkflowError("Approval field 'decisions' must be an array", "APPROVAL_INVALID");
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function requireField(data, key, expected) {
|
|
272
|
+
if (data[key] === undefined || data[key] === "") {
|
|
273
|
+
throw new SlideWorkflowError(`Missing required field '${key}'`, "FIELD_MISSING");
|
|
274
|
+
}
|
|
275
|
+
if (expected !== undefined && data[key] !== expected) {
|
|
276
|
+
throw new SlideWorkflowError(`Field '${key}' expected '${expected}', got '${data[key]}'`, "FIELD_MISMATCH");
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function requireNumberField(data, key) {
|
|
281
|
+
requireField(data, key);
|
|
282
|
+
if (typeof data[key] !== "number" || !Number.isFinite(data[key])) {
|
|
283
|
+
throw new SlideWorkflowError(`Field '${key}' must be a finite number`, "FIELD_INVALID");
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function requireDate(data, key) {
|
|
288
|
+
requireField(data, key);
|
|
289
|
+
const time = Date.parse(data[key]);
|
|
290
|
+
if (Number.isNaN(time)) {
|
|
291
|
+
throw new SlideWorkflowError(`Field '${key}' must be parseable date/time`, "FIELD_INVALID");
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function parseRequiredState(value) {
|
|
296
|
+
const [command, state, approval] = (value ?? "").split(":");
|
|
297
|
+
requireCommand(command);
|
|
298
|
+
if (!state) {
|
|
299
|
+
throw new SlideWorkflowError(`Invalid --require '${value}'. Expected command:state[:approved]`, "INVALID_REQUIRE");
|
|
300
|
+
}
|
|
301
|
+
if (approval && approval !== "approved") {
|
|
302
|
+
throw new SlideWorkflowError(`Invalid approval requirement '${approval}'`, "INVALID_REQUIRE");
|
|
303
|
+
}
|
|
304
|
+
return Object.freeze({ command, state, approvalRequired: approval === "approved" });
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export async function checkRequiredState(targetInfo, requirement) {
|
|
308
|
+
const supervision = await readSupervision(targetInfo, requirement.command);
|
|
309
|
+
if (supervision.data.result !== "passed") {
|
|
310
|
+
throw new SlideWorkflowError(
|
|
311
|
+
`Required supervision is not passed: ${supervision.filePath} result=${supervision.data.result}`,
|
|
312
|
+
"STATE_NOT_PASSED",
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
if (supervision.data.state !== requirement.state) {
|
|
316
|
+
throw new SlideWorkflowError(
|
|
317
|
+
`Required state mismatch. Expected ${requirement.command}:${requirement.state}, got ${supervision.data.state}`,
|
|
318
|
+
"STATE_MISMATCH",
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
if (requirement.approvalRequired) {
|
|
322
|
+
await readApproval(targetInfo, requirement.command, supervision.data);
|
|
323
|
+
}
|
|
324
|
+
return supervision;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export async function assertCommandPrerequisites(targetInfo, command) {
|
|
328
|
+
if (command === "plan") {
|
|
329
|
+
const briefPath = path.join(targetInfo.deckPath, "brief.md");
|
|
330
|
+
if (!existsSync(briefPath)) {
|
|
331
|
+
throw new SlideWorkflowError(`Missing brief.md: ${path.relative(process.cwd(), briefPath)}`, "PREREQUISITE_MISSING");
|
|
332
|
+
}
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (command === "compose") {
|
|
336
|
+
await checkRequiredState(targetInfo, parseRequiredState("plan:planned:approved"));
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (command === "polish") {
|
|
340
|
+
await checkRequiredState(targetInfo, parseRequiredState("compose:composed:approved"));
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (command === "deliver") {
|
|
344
|
+
await checkRequiredState(targetInfo, parseRequiredState("polish:polished"));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export async function writeApproval(targetInfo, command, approvedBy, options = {}) {
|
|
349
|
+
if (!APPROVAL_COMMANDS.includes(command)) {
|
|
350
|
+
throw new SlideWorkflowError(`Approval is supported only for: ${APPROVAL_COMMANDS.join(", ")}`, "APPROVAL_UNSUPPORTED");
|
|
351
|
+
}
|
|
352
|
+
if (!approvedBy) {
|
|
353
|
+
throw new SlideWorkflowError("Missing --by. Approval records must include the human approver.", "APPROVAL_MISSING_BY");
|
|
354
|
+
}
|
|
355
|
+
const supervision = await readSupervision(targetInfo, command);
|
|
356
|
+
if (supervision.data.result !== "passed") {
|
|
357
|
+
throw new SlideWorkflowError(`Cannot approve non-passed supervision: ${supervision.filePath}`, "APPROVAL_REJECTED");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const filePath = approvalPath(targetInfo, command);
|
|
361
|
+
if (existsSync(filePath) && !options.force) {
|
|
362
|
+
throw new SlideWorkflowError(`Approval already exists: ${filePath}. Use --force to overwrite.`, "APPROVAL_EXISTS");
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
await mkdir(targetInfo.reviewPath, { recursive: true });
|
|
366
|
+
const content = [
|
|
367
|
+
"---",
|
|
368
|
+
"status: approved",
|
|
369
|
+
`command: ${command}`,
|
|
370
|
+
`target: ${targetInfo.target}`,
|
|
371
|
+
`approved_state: ${COMMAND_STATES[command]}`,
|
|
372
|
+
`supervision_workflow_run_id: ${supervision.data.workflow_run_id}`,
|
|
373
|
+
`approved_by: ${approvedBy}`,
|
|
374
|
+
`approved_at: ${new Date().toISOString()}`,
|
|
375
|
+
"waivers: []",
|
|
376
|
+
"decisions: []",
|
|
377
|
+
"---",
|
|
378
|
+
"",
|
|
379
|
+
`# ${command} Approval`,
|
|
380
|
+
"",
|
|
381
|
+
`Approved by ${approvedBy}.`,
|
|
382
|
+
"",
|
|
383
|
+
].join("\n");
|
|
384
|
+
await writeFile(filePath, content, "utf8");
|
|
385
|
+
return filePath;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export function isSuccessfulCommandState(targetInfo, command) {
|
|
389
|
+
const filePath = supervisionPath(targetInfo, command);
|
|
390
|
+
if (!existsSync(filePath)) return false;
|
|
391
|
+
try {
|
|
392
|
+
const parsed = parseFrontMatter(readFileSync(filePath, "utf8")).frontMatter;
|
|
393
|
+
validateSupervision(parsed, targetInfo, command);
|
|
394
|
+
return parsed.result === "passed" && parsed.state === COMMAND_STATES[command];
|
|
395
|
+
} catch {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export async function commandSupervisionResult(targetInfo, command) {
|
|
401
|
+
const filePath = supervisionPath(targetInfo, command);
|
|
402
|
+
if (!existsSync(filePath)) return null;
|
|
403
|
+
const { data } = await readSupervision(targetInfo, command);
|
|
404
|
+
return data.result ?? null;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export async function archiveCommandArtifacts(targetInfo, commands, reason, options = {}) {
|
|
408
|
+
const historyPath = path.join(targetInfo.reviewPath, "history");
|
|
409
|
+
const timestamp = timestampForFile();
|
|
410
|
+
const includeApprovals = options.includeApprovals ?? false;
|
|
411
|
+
const moved = [];
|
|
412
|
+
|
|
413
|
+
for (const command of commands) {
|
|
414
|
+
const candidates = [supervisionPath(targetInfo, command)];
|
|
415
|
+
if (includeApprovals) candidates.push(approvalPath(targetInfo, command));
|
|
416
|
+
|
|
417
|
+
for (const source of candidates) {
|
|
418
|
+
if (!existsSync(source)) continue;
|
|
419
|
+
await mkdir(historyPath, { recursive: true });
|
|
420
|
+
const destination = path.join(historyPath, `${timestamp}-${reason}-${path.basename(source)}`);
|
|
421
|
+
await rename(source, destination);
|
|
422
|
+
moved.push(destination);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return moved;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export async function cleanGeneratedOutputs(targetInfo, options = {}) {
|
|
430
|
+
const root = options.root ?? process.cwd();
|
|
431
|
+
const paths = [
|
|
432
|
+
path.join(root, "dist", targetInfo.deckName),
|
|
433
|
+
path.join(root, ".takt", "render", targetInfo.deckName),
|
|
434
|
+
];
|
|
435
|
+
for (const generatedPath of paths) {
|
|
436
|
+
await rm(generatedPath, { recursive: true, force: true });
|
|
437
|
+
}
|
|
438
|
+
return paths;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export function downstreamCommands(command) {
|
|
442
|
+
const index = COMMANDS.indexOf(command);
|
|
443
|
+
return COMMANDS.slice(index);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export function timestampForFile() {
|
|
447
|
+
return new Date().toISOString().replace(/[:.]/g, "-");
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export function hasExecutable(name) {
|
|
451
|
+
const result = spawnSync("which", [name], { stdio: "ignore" });
|
|
452
|
+
return result.status === 0;
|
|
453
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
formatError,
|
|
4
|
+
parseArgs,
|
|
5
|
+
resolveDeckTarget,
|
|
6
|
+
writeApproval,
|
|
7
|
+
} from "./lib/takt-marp-slide-workflow.mjs";
|
|
8
|
+
|
|
9
|
+
function usage() {
|
|
10
|
+
return [
|
|
11
|
+
"Usage: node scripts/takt-marp-approve-slide-workflow-state.mjs <target> <command> --by <name> [--force]",
|
|
12
|
+
"",
|
|
13
|
+
"Examples:",
|
|
14
|
+
" npm run slide:approve -- \"slides/my-talk\" plan --by j5ik2o",
|
|
15
|
+
].join("\n");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function main() {
|
|
19
|
+
const { positional, flags } = parseArgs(process.argv.slice(2));
|
|
20
|
+
if (flags.help) {
|
|
21
|
+
console.log(usage());
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const [target, command] = positional;
|
|
25
|
+
if (!target || !command || !flags.by) {
|
|
26
|
+
throw new Error(usage());
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const targetInfo = resolveDeckTarget(target);
|
|
30
|
+
const filePath = await writeApproval(targetInfo, command, flags.by, { force: flags.force });
|
|
31
|
+
console.log(`approval written: ${filePath}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
main().catch((error) => {
|
|
35
|
+
console.error(formatError(error));
|
|
36
|
+
process.exit(1);
|
|
37
|
+
});
|