slides-grab 1.3.0 → 1.3.1
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-ko.md +8 -6
- package/README.md +8 -6
- package/bin/ppt-agent.js +86 -1
- package/package.json +6 -2
- package/runtimes/claude-code/agents/design-critic-agent.md +23 -0
- package/runtimes/codex/agents/slides-grab-design-critic.md +22 -0
- package/scripts/design-gate.js +241 -0
- package/scripts/html2png.js +246 -0
- package/scripts/install-runtime.js +216 -0
- package/skills/slides-grab/SKILL.md +14 -12
- package/skills/slides-grab/references/presentation-workflow-reference.md +1 -1
- package/skills/slides-grab-card-news/SKILL.md +1 -1
- package/skills/slides-grab-design/SKILL.md +10 -6
- package/skills/slides-grab-design/references/design-gate.md +349 -0
- package/skills/slides-grab-design/references/design-rules.md +3 -3
- package/skills/slides-grab-design/references/design-system-full.md +4 -4
- package/skills/slides-grab-export/SKILL.md +5 -4
- package/skills/slides-grab-export/references/pptx-skill-reference.md +7 -42
- package/skills/slides-grab-plan/SKILL.md +3 -3
- package/skills/slides-grab-plan/references/plan-workflow-reference.md +14 -14
- package/src/design-diversity-data.js +6932 -0
- package/src/design-gate-report.js +244 -0
- package/src/design-gate-state.js +294 -0
- package/src/design-styles.js +19 -2
- package/src/editor/codex-edit.js +26 -1
- package/src/editor/editor.html +1 -1
- package/src/editor/js/model-registry.js +1 -1
- package/templates/design-styles/README.md +2 -1
- package/templates/design-styles/preview.html +1088 -6
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
const PASS_A_SPEC = {
|
|
2
|
+
label: 'Pass A',
|
|
3
|
+
title: 'Pass A System Contract / Constraint Integrity',
|
|
4
|
+
titlePattern: /\bPass\s*A:?\s*System Contract\s*\/\s*Constraint Integrity\b/i,
|
|
5
|
+
checks: [
|
|
6
|
+
'System consistency',
|
|
7
|
+
'Color discipline',
|
|
8
|
+
'AI slop tropes',
|
|
9
|
+
'Content discipline',
|
|
10
|
+
],
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const PASS_B_SPEC = {
|
|
14
|
+
label: 'Pass B',
|
|
15
|
+
title: 'Pass B Audience Impact / Expressive Readability',
|
|
16
|
+
titlePattern: /\bPass\s*B:?\s*Audience Impact\s*\/\s*Expressive Readability\b/i,
|
|
17
|
+
checks: [
|
|
18
|
+
'Composition & hierarchy',
|
|
19
|
+
'Typography & legibility',
|
|
20
|
+
'Korean/CJK word-break integrity',
|
|
21
|
+
'Review Litmus',
|
|
22
|
+
],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const CONFIDENCE_PATTERN = /^Confidence:\s*(High|Medium|Low)\s*$/im;
|
|
26
|
+
const EVIDENCE_PATTERN = /^Evidence:\s*.+\.png\b.*$/im;
|
|
27
|
+
const VERDICT_PATTERN = /^\s*VERDICT:\s*(\S+)\s*$/gim;
|
|
28
|
+
const BLOCKING_FINDINGS_PATTERN = /^\s*Blocking findings:\s*(.+)$/gim;
|
|
29
|
+
const UNRESOLVED_CRITICAL_PATTERN = /^\s*Unresolved Critical:\s*(\d+)\s*$/gim;
|
|
30
|
+
const SUPPORTED_FINDING_SEVERITIES = new Set(['Major', 'Minor', 'Note']);
|
|
31
|
+
|
|
32
|
+
export function assertProceedReportsComplete({ passAReport, passBReport, evidenceFiles = [], slideFingerprints = [] }) {
|
|
33
|
+
const validation = validateProceedReportsComplete({
|
|
34
|
+
passAReport,
|
|
35
|
+
passBReport,
|
|
36
|
+
evidenceFiles,
|
|
37
|
+
slideFingerprints,
|
|
38
|
+
});
|
|
39
|
+
if (validation.failures.length > 0) {
|
|
40
|
+
throw new Error(`Design gate cannot proceed: ${validation.failures.join(' ')}`);
|
|
41
|
+
}
|
|
42
|
+
return validation;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function validateProceedReportsComplete({
|
|
46
|
+
passAReport,
|
|
47
|
+
passBReport,
|
|
48
|
+
evidenceFiles = [],
|
|
49
|
+
slideFingerprints = [],
|
|
50
|
+
}) {
|
|
51
|
+
const passA = validateProceedReport(passAReport, PASS_A_SPEC, evidenceFiles, slideFingerprints);
|
|
52
|
+
const passB = validateProceedReport(passBReport, PASS_B_SPEC, evidenceFiles, slideFingerprints);
|
|
53
|
+
const failures = [
|
|
54
|
+
...passA.failures,
|
|
55
|
+
...passB.failures,
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
status: failures.length === 0 ? 'passed' : 'failed',
|
|
60
|
+
failures,
|
|
61
|
+
passA: passA.summary,
|
|
62
|
+
passB: passB.summary,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function validateProceedReport(report, spec, evidenceFiles, slideFingerprints) {
|
|
67
|
+
const failures = [];
|
|
68
|
+
const confidenceMatch = report.match(CONFIDENCE_PATTERN);
|
|
69
|
+
const criticalMatches = [...report.matchAll(UNRESOLVED_CRITICAL_PATTERN)];
|
|
70
|
+
const blockingMatches = [...report.matchAll(BLOCKING_FINDINGS_PATTERN)];
|
|
71
|
+
const verdict = parseVerdict(report);
|
|
72
|
+
const missingChecks = spec.checks.filter((check) => !hasPassedCheck(report, check));
|
|
73
|
+
|
|
74
|
+
if (!spec.titlePattern.test(report)) {
|
|
75
|
+
failures.push(`${spec.label} is missing required role title "${spec.title}".`);
|
|
76
|
+
}
|
|
77
|
+
failures.push(...validatePassVerdict(verdict, spec.label));
|
|
78
|
+
if (!CONFIDENCE_PATTERN.test(report)) {
|
|
79
|
+
failures.push(`${spec.label} is missing required Confidence: High|Medium|Low.`);
|
|
80
|
+
}
|
|
81
|
+
if (!EVIDENCE_PATTERN.test(report)) {
|
|
82
|
+
failures.push(`${spec.label} is missing required rendered PNG evidence reference.`);
|
|
83
|
+
}
|
|
84
|
+
failures.push(...validateEvidenceFiles(report, spec.label, evidenceFiles));
|
|
85
|
+
failures.push(...validateSlideFingerprints(report, spec.label, slideFingerprints));
|
|
86
|
+
failures.push(...validateFindingsTable(report, spec.label));
|
|
87
|
+
|
|
88
|
+
failures.push(...validateCriticalSummary(criticalMatches, blockingMatches, spec.label));
|
|
89
|
+
failures.push(...validateCriticalFindingRows(report, spec.label));
|
|
90
|
+
failures.push(...missingChecks.map((check) => `${spec.label} missing required passed check: ${check}.`));
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
failures,
|
|
94
|
+
summary: {
|
|
95
|
+
label: spec.label,
|
|
96
|
+
verdict: verdict.values.length === 1 ? verdict.values[0] : '',
|
|
97
|
+
confidence: confidenceMatch?.[1] || '',
|
|
98
|
+
unresolvedCritical: summarizeUnresolvedCritical(criticalMatches),
|
|
99
|
+
blockingFindings: parseBlockingFindings(blockingMatches),
|
|
100
|
+
checks: spec.checks.map((check) => ({
|
|
101
|
+
name: check,
|
|
102
|
+
status: missingChecks.includes(check) ? 'missing' : 'pass',
|
|
103
|
+
})),
|
|
104
|
+
evidenceFiles: evidenceFiles.filter((fileName) => report.includes(fileName)),
|
|
105
|
+
slideFingerprints: slideFingerprints.filter((entry) => report.includes(entry.file) && report.includes(entry.sha256)),
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function validateEvidenceFiles(report, label, evidenceFiles) {
|
|
111
|
+
return evidenceFiles
|
|
112
|
+
.filter((fileName) => !report.includes(fileName))
|
|
113
|
+
.map((fileName) => `${label} is missing rendered evidence file: ${fileName}.`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function validateSlideFingerprints(report, label, slideFingerprints) {
|
|
117
|
+
return slideFingerprints
|
|
118
|
+
.filter((entry) => !report.includes(entry.file) || !report.includes(entry.sha256))
|
|
119
|
+
.map((entry) => `${label} is missing current slide fingerprint: ${entry.file}: ${entry.sha256}.`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function validateFindingsTable(report, label) {
|
|
123
|
+
const headerPattern = /^\|\s*Slide\s*\|\s*Finding\s*\|\s*Severity\s*\|\s*Fix\s*\|\s*Status\s*\|/im;
|
|
124
|
+
const separatorPattern = /^\|\s*-+\s*\|\s*-+\s*\|\s*-+\s*\|\s*-+\s*\|\s*-+\s*\|/im;
|
|
125
|
+
const dataRows = parseFindingRows(report);
|
|
126
|
+
const failures = [];
|
|
127
|
+
|
|
128
|
+
if (!headerPattern.test(report)) {
|
|
129
|
+
failures.push(`${label} is missing required findings table columns: Slide | Finding | Severity | Fix | Status.`);
|
|
130
|
+
}
|
|
131
|
+
if (!separatorPattern.test(report)) {
|
|
132
|
+
failures.push(`${label} is missing required findings table separator row.`);
|
|
133
|
+
}
|
|
134
|
+
if (dataRows.length === 0) {
|
|
135
|
+
failures.push(`${label} is missing at least one findings table data row with a supported severity.`);
|
|
136
|
+
}
|
|
137
|
+
failures.push(...validateFindingRowSeverities(dataRows, label));
|
|
138
|
+
|
|
139
|
+
return failures;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function validateCriticalFindingRows(report, label) {
|
|
143
|
+
return parseFindingRows(report)
|
|
144
|
+
.filter((row) => row.severity.toLowerCase() === 'critical')
|
|
145
|
+
.map(() => `${label} lists a Critical severity finding and cannot proceed.`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function validateFindingRowSeverities(rows, label) {
|
|
149
|
+
return rows
|
|
150
|
+
.filter((row) => !SUPPORTED_FINDING_SEVERITIES.has(row.severity))
|
|
151
|
+
.map((row) => `${label} lists unsupported finding severity "${row.severity}".`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function validatePassVerdict(verdict, label) {
|
|
155
|
+
if (verdict.values.length === 0) {
|
|
156
|
+
return [`${label} is missing required "VERDICT: PASS".`];
|
|
157
|
+
}
|
|
158
|
+
if (verdict.values.length > 1) {
|
|
159
|
+
return [`${label} must include exactly one VERDICT line.`];
|
|
160
|
+
}
|
|
161
|
+
if (verdict.values[0] !== 'PASS') {
|
|
162
|
+
return [`${label} must use "VERDICT: PASS".`];
|
|
163
|
+
}
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function parseVerdict(report) {
|
|
168
|
+
return {
|
|
169
|
+
values: [...report.matchAll(VERDICT_PATTERN)].map((match) => match[1].trim().toUpperCase()),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function parseFindingRows(report) {
|
|
174
|
+
const lines = report.split(/\r?\n/);
|
|
175
|
+
const headerIndex = lines.findIndex((line) => (
|
|
176
|
+
/^\|\s*Slide\s*\|\s*Finding\s*\|\s*Severity\s*\|\s*Fix\s*\|\s*Status\s*\|/i.test(line.trim())
|
|
177
|
+
));
|
|
178
|
+
if (headerIndex === -1) return [];
|
|
179
|
+
|
|
180
|
+
const rows = [];
|
|
181
|
+
for (const rawLine of lines.slice(headerIndex + 1)) {
|
|
182
|
+
const line = rawLine.trim();
|
|
183
|
+
if (!line.startsWith('|')) {
|
|
184
|
+
if (line) break;
|
|
185
|
+
if (rows.length > 0) break;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const cells = line.split('|').slice(1, -1).map((cell) => cell.trim());
|
|
189
|
+
if (cells.length < 5) continue;
|
|
190
|
+
if (cells.every((cell) => /^-+$/.test(cell))) continue;
|
|
191
|
+
if (/^slide$/i.test(cells[0]) && /^finding$/i.test(cells[1])) continue;
|
|
192
|
+
rows.push({
|
|
193
|
+
slide: cells[0],
|
|
194
|
+
finding: cells[1],
|
|
195
|
+
severity: cells[2],
|
|
196
|
+
fix: cells[3],
|
|
197
|
+
status: cells[4],
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
return rows;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function validateCriticalSummary(criticalMatches, blockingMatches, label) {
|
|
204
|
+
const failures = [];
|
|
205
|
+
|
|
206
|
+
if (criticalMatches.length === 0) {
|
|
207
|
+
failures.push(`${label} is missing required Unresolved Critical: 0.`);
|
|
208
|
+
} else if (criticalMatches.length > 1) {
|
|
209
|
+
failures.push(`${label} must include exactly one Unresolved Critical line.`);
|
|
210
|
+
} else if (criticalMatches[0][1] !== '0') {
|
|
211
|
+
failures.push(`${label} has unresolved Critical findings and blocks proceed.`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (blockingMatches.length === 0) {
|
|
215
|
+
failures.push(`${label} is missing required Blocking findings: None.`);
|
|
216
|
+
} else if (blockingMatches.length > 1) {
|
|
217
|
+
failures.push(`${label} must include exactly one Blocking findings line.`);
|
|
218
|
+
} else if (blockingMatches[0][1].trim().toLowerCase() !== 'none') {
|
|
219
|
+
failures.push(`${label} has blocking findings and cannot proceed.`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return failures;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function hasPassedCheck(report, check) {
|
|
226
|
+
const checkPattern = new RegExp(`^\\s*-\\s*\\[[xX]\\]\\s*${escapeRegex(check)}\\s*:\\s*PASS\\b`, 'im');
|
|
227
|
+
return checkPattern.test(report);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function summarizeUnresolvedCritical(matches) {
|
|
231
|
+
if (matches.length !== 1) return null;
|
|
232
|
+
return Number.parseInt(matches[0][1], 10);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function parseBlockingFindings(matches) {
|
|
236
|
+
if (matches.length !== 1) return null;
|
|
237
|
+
const value = matches[0][1].trim();
|
|
238
|
+
if (value.toLowerCase() === 'none') return [];
|
|
239
|
+
return [value];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function escapeRegex(value) {
|
|
243
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
244
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { basename, join, relative, resolve } from 'node:path';
|
|
5
|
+
import { classifyImageSource, extractCssUrls } from './image-contract.js';
|
|
6
|
+
|
|
7
|
+
const SLIDE_FILE_PATTERN = /^slide-.*\.html$/i;
|
|
8
|
+
const GATE_DIR_NAME = '.slides-grab';
|
|
9
|
+
const GATE_STATE_FILE = 'design-gate.json';
|
|
10
|
+
const GATE_REPORT_FILE = 'design-gate-report.md';
|
|
11
|
+
const GATE_PREVIEW_DIR = 'gate-preview';
|
|
12
|
+
const PROCEED = 'proceed';
|
|
13
|
+
const VALID_VERDICTS = new Set([PROCEED, 'revise', 'rethink']);
|
|
14
|
+
|
|
15
|
+
export function normalizeGateVerdict(value) {
|
|
16
|
+
const verdict = String(value || '').trim().toLowerCase();
|
|
17
|
+
if (!VALID_VERDICTS.has(verdict)) {
|
|
18
|
+
throw new Error(`Unknown design gate verdict "${value}". Expected: proceed, revise, or rethink.`);
|
|
19
|
+
}
|
|
20
|
+
return verdict;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function buildDesignGatePaths(slidesDir) {
|
|
24
|
+
const resolvedSlidesDir = resolve(process.cwd(), slidesDir);
|
|
25
|
+
const gateDir = join(resolvedSlidesDir, GATE_DIR_NAME);
|
|
26
|
+
return {
|
|
27
|
+
slidesDir: resolvedSlidesDir,
|
|
28
|
+
gateDir,
|
|
29
|
+
previewDir: join(gateDir, GATE_PREVIEW_DIR),
|
|
30
|
+
reportPath: join(gateDir, GATE_REPORT_FILE),
|
|
31
|
+
statePath: join(gateDir, GATE_STATE_FILE),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function findGateSlideFiles(slidesDir) {
|
|
36
|
+
const entries = await readdir(slidesDir, { withFileTypes: true });
|
|
37
|
+
const files = entries
|
|
38
|
+
.filter((entry) => entry.isFile() && SLIDE_FILE_PATTERN.test(entry.name))
|
|
39
|
+
.map((entry) => entry.name)
|
|
40
|
+
.sort(sortSlideFiles);
|
|
41
|
+
|
|
42
|
+
if (files.length === 0) {
|
|
43
|
+
throw new Error(`No slide-*.html files found in ${slidesDir}.`);
|
|
44
|
+
}
|
|
45
|
+
return files;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function collectSlideFingerprints(slidesDir) {
|
|
49
|
+
const resolvedSlidesDir = resolve(process.cwd(), slidesDir);
|
|
50
|
+
const files = await findGateSlideFiles(resolvedSlidesDir);
|
|
51
|
+
return Promise.all(files.map(async (fileName) => {
|
|
52
|
+
return {
|
|
53
|
+
file: fileName,
|
|
54
|
+
sha256: await hashFile(join(resolvedSlidesDir, fileName)),
|
|
55
|
+
};
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function collectLocalAssetFingerprints(slidesDir) {
|
|
60
|
+
const resolvedSlidesDir = resolve(process.cwd(), slidesDir);
|
|
61
|
+
const slideFiles = await findGateSlideFiles(resolvedSlidesDir);
|
|
62
|
+
const localAssets = new Set();
|
|
63
|
+
|
|
64
|
+
for (const fileName of slideFiles) {
|
|
65
|
+
const html = await readFile(join(resolvedSlidesDir, fileName), 'utf-8');
|
|
66
|
+
for (const source of extractLocalAssetSources(html)) {
|
|
67
|
+
localAssets.add(source);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return collectFileFingerprints(resolvedSlidesDir, Array.from(localAssets).sort(sortSlideFiles));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function collectFileFingerprints(baseDir, files) {
|
|
75
|
+
const resolvedBaseDir = resolve(process.cwd(), baseDir);
|
|
76
|
+
return Promise.all(files.map(async (fileName) => ({
|
|
77
|
+
file: fileName,
|
|
78
|
+
sha256: await hashFile(join(resolvedBaseDir, fileName)),
|
|
79
|
+
})));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function readDesignGateState(slidesDir) {
|
|
83
|
+
const { statePath } = buildDesignGatePaths(slidesDir);
|
|
84
|
+
if (!existsSync(statePath)) return null;
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
return JSON.parse(await readFile(statePath, 'utf-8'));
|
|
88
|
+
} catch (error) {
|
|
89
|
+
throw new Error(`Design gate state is unreadable: ${statePath}. ${error.message}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function writeDesignGateState(slidesDir, state, reportMarkdown = '') {
|
|
94
|
+
const paths = buildDesignGatePaths(slidesDir);
|
|
95
|
+
await mkdir(paths.gateDir, { recursive: true });
|
|
96
|
+
await writeFile(paths.statePath, `${JSON.stringify(state, null, 2)}\n`, 'utf-8');
|
|
97
|
+
if (reportMarkdown) {
|
|
98
|
+
await writeFile(paths.reportPath, reportMarkdown, 'utf-8');
|
|
99
|
+
}
|
|
100
|
+
return paths;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function createDesignGateState(options) {
|
|
104
|
+
const slidesDir = resolve(process.cwd(), options.slidesDir);
|
|
105
|
+
const verdict = normalizeGateVerdict(options.verdict);
|
|
106
|
+
const slideFingerprints = await collectSlideFingerprints(slidesDir);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
schemaVersion: 2,
|
|
110
|
+
verdict,
|
|
111
|
+
generatedAt: new Date().toISOString(),
|
|
112
|
+
slideMode: options.slideMode,
|
|
113
|
+
resolution: options.resolution,
|
|
114
|
+
previewDir: options.previewDir ? relative(slidesDir, options.previewDir) : join(GATE_DIR_NAME, GATE_PREVIEW_DIR),
|
|
115
|
+
reportPath: options.reportPath ? relative(slidesDir, options.reportPath) : join(GATE_DIR_NAME, GATE_REPORT_FILE),
|
|
116
|
+
passA: normalizeEvidence(options.passA),
|
|
117
|
+
passB: normalizeEvidence(options.passB),
|
|
118
|
+
gateValidation: options.gateValidation || { status: verdict === PROCEED ? 'passed' : 'not-run' },
|
|
119
|
+
slideFingerprints,
|
|
120
|
+
localAssetFingerprints: options.localAssetFingerprints || await collectLocalAssetFingerprints(slidesDir),
|
|
121
|
+
passReportFingerprints: options.passReportFingerprints || [],
|
|
122
|
+
previewFingerprints: options.previewFingerprints || [],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function assertDesignGateReady(slidesDir, options = {}) {
|
|
127
|
+
const label = options.label || 'export';
|
|
128
|
+
const paths = buildDesignGatePaths(slidesDir);
|
|
129
|
+
const state = await readDesignGateState(paths.slidesDir);
|
|
130
|
+
if (!state) {
|
|
131
|
+
throw new Error(`${label} blocked: run slides-grab design-gate and record a Proceed verdict before exporting.`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (normalizeGateVerdict(state.verdict) !== PROCEED) {
|
|
135
|
+
throw new Error(`${label} blocked: latest design gate verdict is "${state.verdict}", not "proceed".`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (state.schemaVersion !== 2) {
|
|
139
|
+
throw new Error(`${label} blocked: design gate receipt is from a legacy unenforced schema; rerun slides-grab design-gate.`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (state.gateValidation?.status !== 'passed') {
|
|
143
|
+
throw new Error(`${label} blocked: design gate report validation did not pass.`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!state.passA?.reportPath || !state.passB?.reportPath) {
|
|
147
|
+
throw new Error(`${label} blocked: design gate state is missing dual-oracle Pass A/Pass B evidence.`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
assertPassReady(state.passA, 'Pass A', label);
|
|
151
|
+
assertPassReady(state.passB, 'Pass B', label);
|
|
152
|
+
|
|
153
|
+
const currentFingerprints = await collectSlideFingerprints(paths.slidesDir);
|
|
154
|
+
const previousFingerprints = Array.isArray(state.slideFingerprints) ? state.slideFingerprints : [];
|
|
155
|
+
const staleFiles = diffFingerprints(previousFingerprints, currentFingerprints);
|
|
156
|
+
if (staleFiles.length > 0) {
|
|
157
|
+
throw new Error(`${label} blocked: design gate is stale because slides changed: ${staleFiles.join(', ')}.`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const localAssetFingerprints = Array.isArray(state.localAssetFingerprints) ? state.localAssetFingerprints : null;
|
|
161
|
+
if (!localAssetFingerprints) {
|
|
162
|
+
throw new Error(`${label} blocked: design gate receipt is missing local asset fingerprints; rerun slides-grab design-gate.`);
|
|
163
|
+
}
|
|
164
|
+
let currentAssetFingerprints = [];
|
|
165
|
+
try {
|
|
166
|
+
currentAssetFingerprints = await collectLocalAssetFingerprints(paths.slidesDir);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
throw new Error(`${label} blocked: design gate is stale because local assets changed. ${error.message}`);
|
|
169
|
+
}
|
|
170
|
+
const staleAssets = diffFingerprints(localAssetFingerprints, currentAssetFingerprints);
|
|
171
|
+
if (staleAssets.length > 0) {
|
|
172
|
+
throw new Error(`${label} blocked: design gate is stale because local assets changed: ${staleAssets.join(', ')}.`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const passReportFingerprints = Array.isArray(state.passReportFingerprints) ? state.passReportFingerprints : [];
|
|
176
|
+
if (passReportFingerprints.length < 2) {
|
|
177
|
+
throw new Error(`${label} blocked: design gate receipt is missing Pass A/Pass B report fingerprints.`);
|
|
178
|
+
}
|
|
179
|
+
const currentReportFingerprints = await collectFileFingerprints(paths.slidesDir, passReportFingerprints.map((entry) => entry.file));
|
|
180
|
+
const staleReports = diffFingerprints(passReportFingerprints, currentReportFingerprints);
|
|
181
|
+
if (staleReports.length > 0) {
|
|
182
|
+
throw new Error(`${label} blocked: design gate is stale because Pass A/Pass B reports changed: ${staleReports.join(', ')}.`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const previewFingerprints = Array.isArray(state.previewFingerprints) ? state.previewFingerprints : [];
|
|
186
|
+
if (previewFingerprints.length === 0) {
|
|
187
|
+
throw new Error(`${label} blocked: design gate receipt is missing rendered evidence fingerprints.`);
|
|
188
|
+
}
|
|
189
|
+
const currentPreviewFingerprints = await collectFileFingerprints(paths.slidesDir, previewFingerprints.map((entry) => entry.file));
|
|
190
|
+
const stalePreviewFiles = diffFingerprints(previewFingerprints, currentPreviewFingerprints);
|
|
191
|
+
if (stalePreviewFiles.length > 0) {
|
|
192
|
+
throw new Error(`${label} blocked: design gate is stale because rendered evidence changed: ${stalePreviewFiles.join(', ')}.`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return state;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function buildDesignGateReport(state, passAReport, passBReport) {
|
|
199
|
+
return [
|
|
200
|
+
'# slides-grab Design Gate Report',
|
|
201
|
+
'',
|
|
202
|
+
`Verdict: ${state.verdict}`,
|
|
203
|
+
`Generated: ${state.generatedAt}`,
|
|
204
|
+
`Slide mode: ${state.slideMode}`,
|
|
205
|
+
`Resolution: ${state.resolution}`,
|
|
206
|
+
'',
|
|
207
|
+
'## Pass A: System Contract / Constraint Integrity',
|
|
208
|
+
'',
|
|
209
|
+
passAReport.trim(),
|
|
210
|
+
'',
|
|
211
|
+
'## Pass B: Audience Impact / Expressive Readability',
|
|
212
|
+
'',
|
|
213
|
+
passBReport.trim(),
|
|
214
|
+
'',
|
|
215
|
+
'## Slide Fingerprints',
|
|
216
|
+
'',
|
|
217
|
+
...state.slideFingerprints.map((entry) => `- ${entry.file}: ${entry.sha256}`),
|
|
218
|
+
'',
|
|
219
|
+
].join('\n');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function normalizeEvidence(value = {}) {
|
|
223
|
+
return {
|
|
224
|
+
reportPath: value.reportPath || '',
|
|
225
|
+
summary: value.summary || '',
|
|
226
|
+
verdict: value.verdict || '',
|
|
227
|
+
unresolvedCritical: value.unresolvedCritical ?? null,
|
|
228
|
+
blockingFindings: Array.isArray(value.blockingFindings) ? value.blockingFindings : [],
|
|
229
|
+
checks: Array.isArray(value.checks) ? value.checks : [],
|
|
230
|
+
evidenceFiles: Array.isArray(value.evidenceFiles) ? value.evidenceFiles : [],
|
|
231
|
+
slideFingerprints: Array.isArray(value.slideFingerprints) ? value.slideFingerprints : [],
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function extractLocalAssetSources(html) {
|
|
236
|
+
const sources = new Set();
|
|
237
|
+
const attributePattern = /\b(?:src|poster)\s*=\s*(['"])(.*?)\1/gi;
|
|
238
|
+
let match;
|
|
239
|
+
|
|
240
|
+
while ((match = attributePattern.exec(html)) !== null) {
|
|
241
|
+
addLocalAssetSource(sources, match[2]);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
for (const source of extractCssUrls(html)) {
|
|
245
|
+
addLocalAssetSource(sources, source);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return Array.from(sources);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function addLocalAssetSource(sources, source) {
|
|
252
|
+
const value = String(source || '').trim();
|
|
253
|
+
if (classifyImageSource(value).kind === 'local-asset-path') {
|
|
254
|
+
sources.add(value);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function hashFile(filePath) {
|
|
259
|
+
const bytes = await readFile(filePath);
|
|
260
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function assertPassReady(pass, passLabel, exportLabel) {
|
|
264
|
+
if (pass.verdict !== 'PASS') {
|
|
265
|
+
throw new Error(`${exportLabel} blocked: ${passLabel} did not record VERDICT: PASS.`);
|
|
266
|
+
}
|
|
267
|
+
if (pass.unresolvedCritical !== 0) {
|
|
268
|
+
throw new Error(`${exportLabel} blocked: ${passLabel} has unresolved Critical findings.`);
|
|
269
|
+
}
|
|
270
|
+
if (pass.blockingFindings.length > 0) {
|
|
271
|
+
throw new Error(`${exportLabel} blocked: ${passLabel} has blocking findings.`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function toSlideOrder(fileName) {
|
|
276
|
+
const match = basename(fileName).match(/\d+/);
|
|
277
|
+
return match ? Number.parseInt(match[0], 10) : Number.POSITIVE_INFINITY;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function sortSlideFiles(left, right) {
|
|
281
|
+
const leftOrder = toSlideOrder(left);
|
|
282
|
+
const rightOrder = toSlideOrder(right);
|
|
283
|
+
if (leftOrder !== rightOrder) return leftOrder - rightOrder;
|
|
284
|
+
return left.localeCompare(right);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function diffFingerprints(previous, current) {
|
|
288
|
+
const previousByFile = new Map(previous.map((entry) => [entry.file, entry.sha256]));
|
|
289
|
+
const currentByFile = new Map(current.map((entry) => [entry.file, entry.sha256]));
|
|
290
|
+
const files = new Set([...previousByFile.keys(), ...currentByFile.keys()]);
|
|
291
|
+
return Array.from(files)
|
|
292
|
+
.filter((file) => previousByFile.get(file) !== currentByFile.get(file))
|
|
293
|
+
.sort(sortSlideFiles);
|
|
294
|
+
}
|
package/src/design-styles.js
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
import { basename, dirname, isAbsolute, resolve } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
|
|
5
|
+
import { DESIGN_DIVERSITY_SOURCE, RAW_DESIGN_DIVERSITY_STYLES } from './design-diversity-data.js';
|
|
5
6
|
import { RAW_DESIGN_STYLES } from './design-styles-data.js';
|
|
6
7
|
import { parseDesignMarkdownFile } from './design-md-parser.js';
|
|
7
8
|
|
|
@@ -21,9 +22,25 @@ export const DESIGN_STYLES_SOURCE = Object.freeze({
|
|
|
21
22
|
citation: 'Design collections derived from corazzon/pptx-design-styles. Styles 31–35 are slides-grab originals.',
|
|
22
23
|
});
|
|
23
24
|
|
|
24
|
-
const
|
|
25
|
+
export const SLIDES_GRAB_ORIGINAL_STYLES_SOURCE = Object.freeze({
|
|
26
|
+
name: 'slides-grab original styles',
|
|
27
|
+
repo: 'NomaDamas/slides-grab',
|
|
28
|
+
url: 'https://github.com/NomaDamas/slides-grab',
|
|
29
|
+
license: 'MIT',
|
|
30
|
+
citation: 'slides-grab original bundled styles 31–35.',
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
function getStyleSource(style) {
|
|
34
|
+
if (style.source) return style.source;
|
|
35
|
+
if (style.collection === 'design-diversity') return DESIGN_DIVERSITY_SOURCE;
|
|
36
|
+
const styleNumber = Number(style.number);
|
|
37
|
+
if (styleNumber >= 31 && styleNumber <= 35) return SLIDES_GRAB_ORIGINAL_STYLES_SOURCE;
|
|
38
|
+
return DESIGN_STYLES_SOURCE;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const DESIGN_STYLES = [...RAW_DESIGN_STYLES, ...RAW_DESIGN_DIVERSITY_STYLES].map((style) => Object.freeze({
|
|
25
42
|
...style,
|
|
26
|
-
source:
|
|
43
|
+
source: getStyleSource(style),
|
|
27
44
|
}));
|
|
28
45
|
|
|
29
46
|
const DESIGN_STYLES_BY_ID = new Map(DESIGN_STYLES.map((style) => [style.id, style]));
|
package/src/editor/codex-edit.js
CHANGED
|
@@ -486,10 +486,35 @@ export function buildCodexExecArgs({ prompt, imagePath, model }) {
|
|
|
486
486
|
|
|
487
487
|
export { CLAUDE_MODELS, isClaudeModel } from './js/model-registry.js';
|
|
488
488
|
|
|
489
|
+
const CLAUDE_PERMISSION_MODES = new Set([
|
|
490
|
+
'acceptEdits',
|
|
491
|
+
'auto',
|
|
492
|
+
'bypassPermissions',
|
|
493
|
+
'default',
|
|
494
|
+
'dontAsk',
|
|
495
|
+
'plan',
|
|
496
|
+
]);
|
|
497
|
+
|
|
498
|
+
function buildClaudePermissionArgs() {
|
|
499
|
+
if (process.env.PPT_AGENT_CLAUDE_SKIP_PERMISSIONS === '1') {
|
|
500
|
+
return ['--dangerously-skip-permissions'];
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const permissionMode = process.env.PPT_AGENT_CLAUDE_PERMISSION_MODE?.trim() || 'acceptEdits';
|
|
504
|
+
if (!CLAUDE_PERMISSION_MODES.has(permissionMode)) {
|
|
505
|
+
throw new Error(
|
|
506
|
+
`Invalid PPT_AGENT_CLAUDE_PERMISSION_MODE: ${permissionMode}. ` +
|
|
507
|
+
`Allowed values: ${Array.from(CLAUDE_PERMISSION_MODES).join(', ')}`,
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return ['--permission-mode', permissionMode];
|
|
512
|
+
}
|
|
513
|
+
|
|
489
514
|
export function buildClaudeExecArgs({ prompt, imagePath, model }) {
|
|
490
515
|
const args = [
|
|
491
516
|
'-p',
|
|
492
|
-
|
|
517
|
+
...buildClaudePermissionArgs(),
|
|
493
518
|
'--model', model.trim(),
|
|
494
519
|
'--max-turns', '30',
|
|
495
520
|
'--verbose',
|
package/src/editor/editor.html
CHANGED
|
@@ -1634,7 +1634,7 @@
|
|
|
1634
1634
|
<option value="gpt-5.4">gpt-5.4</option>
|
|
1635
1635
|
<option value="gpt-5.3-codex">gpt-5.3-codex</option>
|
|
1636
1636
|
<option value="gpt-5.3-codex-spark">gpt-5.3-codex-spark</option>
|
|
1637
|
-
<option value="claude-opus-4-
|
|
1637
|
+
<option value="claude-opus-4-8">claude-opus-4-8</option>
|
|
1638
1638
|
<option value="claude-sonnet-4-6">claude-sonnet-4-6</option>
|
|
1639
1639
|
</select>
|
|
1640
1640
|
</div>
|
|
@@ -18,7 +18,7 @@ export const CODEX_MODELS = [
|
|
|
18
18
|
'gpt-5.3-codex-spark',
|
|
19
19
|
];
|
|
20
20
|
|
|
21
|
-
export const CLAUDE_MODELS = ['claude-opus-4-
|
|
21
|
+
export const CLAUDE_MODELS = ['claude-opus-4-8', 'claude-sonnet-4-6'];
|
|
22
22
|
|
|
23
23
|
export const ALL_MODELS = [...CODEX_MODELS, ...CLAUDE_MODELS];
|
|
24
24
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Design Style Collections
|
|
2
2
|
|
|
3
|
-
slides-grab bundles
|
|
3
|
+
slides-grab bundles 95 design styles: 30 derived from [corazzon/pptx-design-styles](https://github.com/corazzon/pptx-design-styles) (MIT), 5 slides-grab originals, and 60 PPT packs derived from [epoko77-ai/design-diversity](https://github.com/epoko77-ai/design-diversity) (MIT).
|
|
4
4
|
|
|
5
5
|
These styles are reference directions for slide generation, not drop-in HTML slide templates. Agents may also design fully custom visuals beyond the bundled collection.
|
|
6
6
|
|
|
@@ -15,5 +15,6 @@ The preview/select flow is intentionally simple: it keeps design approval inside
|
|
|
15
15
|
## Citation
|
|
16
16
|
|
|
17
17
|
- Upstream collection: `corazzon/pptx-design-styles`
|
|
18
|
+
- Upstream design catalog: `epoko77-ai/design-diversity`
|
|
18
19
|
- URL: <https://github.com/corazzon/pptx-design-styles>
|
|
19
20
|
- Reference used in this repo: `references/styles.md`
|