@seanyao/roll 3.610.1 → 3.611.2
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/CHANGELOG.md +69 -0
- package/README.md +30 -19
- package/dist/roll.mjs +10485 -11206
- package/package.json +3 -1
- package/skills/README.md +14 -1
- package/skills/docs/skill-authoring.md +66 -0
- package/skills/reports/skill-audit-summary.md +53 -0
- package/skills/roll-.changelog/SKILL.md +25 -443
- package/skills/roll-.changelog/references/full-contract.md +462 -0
- package/skills/roll-.clarify/SKILL.md +6 -4
- package/skills/roll-.dream/SKILL.md +26 -353
- package/skills/roll-.dream/references/full-contract.md +365 -0
- package/skills/roll-.echo/SKILL.md +6 -4
- package/skills/roll-.qa/SKILL.md +25 -236
- package/skills/roll-.qa/references/full-contract.md +256 -0
- package/skills/roll-.review/SKILL.md +6 -2
- package/skills/roll-brief/SKILL.md +6 -8
- package/skills/roll-build/SKILL.md +28 -864
- package/skills/roll-build/references/full-contract.md +883 -0
- package/skills/roll-debug/SKILL.md +26 -585
- package/skills/roll-debug/references/full-contract.md +607 -0
- package/skills/roll-design/SKILL.md +28 -903
- package/skills/roll-design/references/full-contract.md +923 -0
- package/skills/roll-doc/SKILL.md +25 -574
- package/skills/roll-doc/references/full-contract.md +594 -0
- package/skills/roll-doctor/SKILL.md +21 -2
- package/skills/roll-fix/SKILL.md +28 -621
- package/skills/roll-fix/references/full-contract.md +640 -0
- package/skills/roll-idea/SKILL.md +6 -2
- package/skills/roll-loop/SKILL.md +29 -542
- package/skills/roll-loop/references/full-contract.md +555 -0
- package/skills/roll-notes/SKILL.md +6 -2
- package/skills/roll-onboard/SKILL.md +6 -2
- package/skills/roll-peer/SKILL.md +27 -316
- package/skills/roll-peer/references/full-contract.md +329 -0
- package/skills/roll-propose/SKILL.md +6 -8
- package/skills/roll-review-pr/SKILL.md +6 -2
- package/skills/roll-sentinel/SKILL.md +26 -344
- package/skills/roll-sentinel/references/full-contract.md +363 -0
- package/skills/roll-spar/SKILL.md +27 -269
- package/skills/roll-spar/references/full-contract.md +288 -0
- package/skills/route-cases/skills.json +235 -0
- package/skills/scripts/audit-skills.mjs +272 -0
- package/skills/scripts/test-audit-skills.mjs +39 -0
- package/skills/tests/fixtures/skill-audit/block-skill/SKILL.md +12 -0
- package/skills/tests/fixtures/skill-audit/minimal-skill/SKILL.md +8 -0
- package/skills/tests/fixtures/skill-audit/quoted-skill/SKILL.md +10 -0
- package/skills/tests/fixtures/skill-audit/route-cases.json +21 -0
- package/skills/tests/fixtures/skill-audit/spoke-skill/SKILL.md +12 -0
- package/skills/tests/fixtures/skill-audit/spoke-skill/references/runbook.md +3 -0
- package/lib/i18n/slides.sh +0 -3
- package/lib/i18n/slides_build.sh +0 -38
- package/lib/i18n/slides_delete.sh +0 -19
- package/lib/i18n/slides_list.sh +0 -14
- package/lib/i18n/slides_logs.sh +0 -12
- package/lib/i18n/slides_new.sh +0 -15
- package/lib/i18n/slides_preview.sh +0 -14
- package/lib/i18n/slides_templates.sh +0 -7
- package/lib/slides/components/README.md +0 -123
- package/lib/slides/components/cards-2.html +0 -9
- package/lib/slides/components/cards-3.html +0 -9
- package/lib/slides/components/cards-4.html +0 -9
- package/lib/slides/components/compare.html +0 -22
- package/lib/slides/components/highlight.html +0 -9
- package/lib/slides/components/pipeline.html +0 -12
- package/lib/slides/components/plain.html +0 -7
- package/lib/slides/components/quote.html +0 -4
- package/lib/slides/components/timeline.html +0 -9
- package/lib/slides/templates/introduction-v3.html +0 -571
- package/lib/slides/templates/pitch.html +0 -0
- package/skills/roll-deck/SKILL.md +0 -296
- /package/skills/roll-debug/{injectable-bb.js → assets/injectable-bb.js} +0 -0
- /package/skills/roll-design/{ENGINEERING_CHECKLIST.md → references/engineering-checklist.md} +0 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
function readText(file) {
|
|
8
|
+
return fs.readFileSync(file, "utf8");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function stripYamlQuotes(value) {
|
|
12
|
+
const trimmed = value.trim();
|
|
13
|
+
if (
|
|
14
|
+
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
15
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
16
|
+
) {
|
|
17
|
+
return trimmed.slice(1, -1);
|
|
18
|
+
}
|
|
19
|
+
return trimmed;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function parseFrontmatter(text) {
|
|
23
|
+
if (!text.startsWith("---\n")) {
|
|
24
|
+
return { fields: {}, body: text, ok: false };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const end = text.indexOf("\n---", 4);
|
|
28
|
+
if (end === -1) {
|
|
29
|
+
return { fields: {}, body: text, ok: false };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const raw = text.slice(4, end);
|
|
33
|
+
const bodyStart = text.indexOf("\n", end + 4);
|
|
34
|
+
const body = bodyStart === -1 ? "" : text.slice(bodyStart + 1);
|
|
35
|
+
const fields = {};
|
|
36
|
+
const lines = raw.split(/\r?\n/);
|
|
37
|
+
|
|
38
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
39
|
+
const line = lines[index];
|
|
40
|
+
const blockMatch = line.match(/^([A-Za-z0-9_.-]+):\s*\|\s*$/);
|
|
41
|
+
if (blockMatch) {
|
|
42
|
+
const key = blockMatch[1];
|
|
43
|
+
const blockLines = [];
|
|
44
|
+
index += 1;
|
|
45
|
+
while (index < lines.length) {
|
|
46
|
+
const next = lines[index];
|
|
47
|
+
if (/^\S[^:]*:/.test(next)) {
|
|
48
|
+
index -= 1;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
blockLines.push(next.replace(/^ ?/, ""));
|
|
52
|
+
index += 1;
|
|
53
|
+
}
|
|
54
|
+
fields[key] = blockLines.join("\n").trim();
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const scalarMatch = line.match(/^([A-Za-z0-9_.-]+):\s*(.*)$/);
|
|
59
|
+
if (scalarMatch) {
|
|
60
|
+
fields[scalarMatch[1]] = stripYamlQuotes(scalarMatch[2]);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { fields, body, ok: true };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function wordCount(text) {
|
|
68
|
+
const words = text.trim().split(/\s+/).filter(Boolean);
|
|
69
|
+
return words.length;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function walkFiles(dir) {
|
|
73
|
+
if (!fs.existsSync(dir)) return [];
|
|
74
|
+
return fs
|
|
75
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
76
|
+
.flatMap((entry) => {
|
|
77
|
+
const full = path.join(dir, entry.name);
|
|
78
|
+
if (entry.isDirectory()) return walkFiles(full);
|
|
79
|
+
if (entry.isFile()) return [full];
|
|
80
|
+
return [];
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function toPosix(relativePath) {
|
|
85
|
+
return relativePath.split(path.sep).join("/");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function collectSpokeFiles(skillDir) {
|
|
89
|
+
return ["references", "assets", "scripts"].flatMap((dirName) => {
|
|
90
|
+
const base = path.join(skillDir, dirName);
|
|
91
|
+
return walkFiles(base).map((file) => toPosix(path.relative(skillDir, file)));
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function collectReferencedSpokes(body) {
|
|
96
|
+
const refs = new Set();
|
|
97
|
+
const patterns = [
|
|
98
|
+
/\]\(((?:references|assets|scripts)\/[^)#\s]+)(?:#[^)]+)?\)/g,
|
|
99
|
+
/`((?:references|assets|scripts)\/[^`#]+)(?:#[^`]*)?`/g,
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
for (const pattern of patterns) {
|
|
103
|
+
for (const match of body.matchAll(pattern)) {
|
|
104
|
+
refs.add(match[1].replace(/^\.\//, ""));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return [...refs].sort();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function parseSkillFile(file) {
|
|
112
|
+
const text = readText(file);
|
|
113
|
+
const { fields, body, ok } = parseFrontmatter(text);
|
|
114
|
+
const skillDir = path.dirname(file);
|
|
115
|
+
const description = fields.description ?? "";
|
|
116
|
+
const spokeFiles = collectSpokeFiles(skillDir);
|
|
117
|
+
const referencedSpokes = collectReferencedSpokes(body);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
name: fields.name ?? path.basename(skillDir),
|
|
121
|
+
file,
|
|
122
|
+
frontmatterOk: ok,
|
|
123
|
+
lines: text.trimEnd().split(/\r?\n/).length,
|
|
124
|
+
description,
|
|
125
|
+
descriptionWordCount: wordCount(description),
|
|
126
|
+
descriptionLoadTrigger: /^Load when\b/i.test(description),
|
|
127
|
+
hasWhenNotToUse: /^##\s+When Not to Use\b/im.test(body),
|
|
128
|
+
hasGotchas: /^##\s+(Gotchas|Known Failure Modes)\b/im.test(body),
|
|
129
|
+
hasReviewedWaiver: /Reviewed Waiver:/i.test(body),
|
|
130
|
+
auxiliaryDirs: ["scripts", "references", "assets"].filter((dirName) =>
|
|
131
|
+
fs.existsSync(path.join(skillDir, dirName)),
|
|
132
|
+
),
|
|
133
|
+
spokeFiles,
|
|
134
|
+
referencedSpokes,
|
|
135
|
+
missingSpokeRefs: referencedSpokes.filter((ref) => !spokeFiles.includes(ref)),
|
|
136
|
+
unreferencedSpokes: spokeFiles.filter((filePath) => !referencedSpokes.includes(filePath)),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function findSkillFiles(skillsDir) {
|
|
141
|
+
return fs
|
|
142
|
+
.readdirSync(skillsDir, { withFileTypes: true })
|
|
143
|
+
.filter((entry) => entry.isDirectory())
|
|
144
|
+
.map((entry) => path.join(skillsDir, entry.name, "SKILL.md"))
|
|
145
|
+
.filter((file) => fs.existsSync(file))
|
|
146
|
+
.sort();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function loadRouteCases(routeFile) {
|
|
150
|
+
if (!fs.existsSync(routeFile)) {
|
|
151
|
+
return { skills: {} };
|
|
152
|
+
}
|
|
153
|
+
return JSON.parse(readText(routeFile));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function routeCoverageFor(skillName, routes) {
|
|
157
|
+
const entry = routes.skills?.[skillName] ?? {};
|
|
158
|
+
const positive = Array.isArray(entry.positive) ? entry.positive : [];
|
|
159
|
+
const negative = Array.isArray(entry.negative) ? entry.negative : [];
|
|
160
|
+
return {
|
|
161
|
+
positive,
|
|
162
|
+
negative,
|
|
163
|
+
hasMinimumCoverage: positive.length >= 2 && negative.length >= 2,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function violationsFor(skill, routeCoverage) {
|
|
168
|
+
const violations = [];
|
|
169
|
+
|
|
170
|
+
if (!skill.frontmatterOk) violations.push("frontmatter-missing-or-invalid");
|
|
171
|
+
if (!skill.descriptionLoadTrigger) violations.push("description-not-load-trigger");
|
|
172
|
+
if (skill.descriptionWordCount > 50) violations.push("description-over-50-words");
|
|
173
|
+
if (!routeCoverage.hasMinimumCoverage) violations.push("route-fixture-coverage-missing");
|
|
174
|
+
if (!skill.hasGotchas) violations.push("gotchas-missing");
|
|
175
|
+
if (skill.lines > 250 && !skill.hasReviewedWaiver) violations.push("hub-over-250-lines");
|
|
176
|
+
for (const missing of skill.missingSpokeRefs) violations.push(`missing-spoke-ref:${missing}`);
|
|
177
|
+
for (const extra of skill.unreferencedSpokes) violations.push(`unreferenced-spoke:${extra}`);
|
|
178
|
+
|
|
179
|
+
return violations;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function auditSkills({ skillsDir, routeFile }) {
|
|
183
|
+
const routes = loadRouteCases(routeFile);
|
|
184
|
+
const skills = findSkillFiles(skillsDir).map((file) => {
|
|
185
|
+
const skill = parseSkillFile(file);
|
|
186
|
+
const routeCoverage = routeCoverageFor(skill.name, routes);
|
|
187
|
+
return {
|
|
188
|
+
...skill,
|
|
189
|
+
routeCoverage: {
|
|
190
|
+
positiveCount: routeCoverage.positive.length,
|
|
191
|
+
negativeCount: routeCoverage.negative.length,
|
|
192
|
+
hasMinimumCoverage: routeCoverage.hasMinimumCoverage,
|
|
193
|
+
},
|
|
194
|
+
violations: violationsFor(skill, routeCoverage),
|
|
195
|
+
};
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const summary = {
|
|
199
|
+
skills: skills.length,
|
|
200
|
+
violations: skills.reduce((count, skill) => count + skill.violations.length, 0),
|
|
201
|
+
over250: skills.filter((skill) => skill.lines > 250).length,
|
|
202
|
+
withGotchas: skills.filter((skill) => skill.hasGotchas).length,
|
|
203
|
+
loadTriggerDescriptions: skills.filter((skill) => skill.descriptionLoadTrigger).length,
|
|
204
|
+
withAuxiliaryFiles: skills.filter((skill) => skill.spokeFiles.length > 0).length,
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
return { summary, skills };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function printHuman(report) {
|
|
211
|
+
console.log(`Skill audit: ${report.summary.skills} skills`);
|
|
212
|
+
console.log(`Load-trigger descriptions: ${report.summary.loadTriggerDescriptions}/${report.summary.skills}`);
|
|
213
|
+
console.log(`Gotchas coverage: ${report.summary.withGotchas}/${report.summary.skills}`);
|
|
214
|
+
console.log(`Skills over 250 lines: ${report.summary.over250}`);
|
|
215
|
+
console.log(`Skills with auxiliary files: ${report.summary.withAuxiliaryFiles}`);
|
|
216
|
+
console.log(`Violations: ${report.summary.violations}`);
|
|
217
|
+
|
|
218
|
+
for (const skill of report.skills) {
|
|
219
|
+
const markers = [];
|
|
220
|
+
markers.push(`${skill.lines} lines`);
|
|
221
|
+
markers.push(`${skill.descriptionWordCount} desc words`);
|
|
222
|
+
markers.push(`${skill.routeCoverage.positiveCount}+/${skill.routeCoverage.negativeCount}- route cases`);
|
|
223
|
+
if (skill.spokeFiles.length > 0) markers.push(`${skill.spokeFiles.length} spokes`);
|
|
224
|
+
const status = skill.violations.length === 0 ? "ok" : skill.violations.join(", ");
|
|
225
|
+
console.log(`- ${skill.name}: ${status} (${markers.join("; ")})`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function parseArgs(argv) {
|
|
230
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
231
|
+
const options = {
|
|
232
|
+
skillsDir: root,
|
|
233
|
+
routeFile: path.join(root, "route-cases", "skills.json"),
|
|
234
|
+
json: false,
|
|
235
|
+
strict: false,
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
239
|
+
const arg = argv[index];
|
|
240
|
+
if (arg === "--json") options.json = true;
|
|
241
|
+
else if (arg === "--strict") options.strict = true;
|
|
242
|
+
else if (arg === "--skills-dir") {
|
|
243
|
+
index += 1;
|
|
244
|
+
options.skillsDir = path.resolve(argv[index]);
|
|
245
|
+
} else if (arg === "--routes") {
|
|
246
|
+
index += 1;
|
|
247
|
+
options.routeFile = path.resolve(argv[index]);
|
|
248
|
+
} else {
|
|
249
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return options;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function main() {
|
|
257
|
+
const options = parseArgs(process.argv.slice(2));
|
|
258
|
+
const report = auditSkills(options);
|
|
259
|
+
if (options.json) console.log(JSON.stringify(report, null, 2));
|
|
260
|
+
else printHuman(report);
|
|
261
|
+
|
|
262
|
+
if (options.strict && report.summary.violations > 0) {
|
|
263
|
+
process.exitCode = 1;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
268
|
+
main().catch((error) => {
|
|
269
|
+
console.error(error.message);
|
|
270
|
+
process.exitCode = 1;
|
|
271
|
+
});
|
|
272
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { auditSkills, parseSkillFile } from "./audit-skills.mjs";
|
|
7
|
+
|
|
8
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
|
+
const fixtureRoot = path.join(root, "tests", "fixtures", "skill-audit");
|
|
10
|
+
|
|
11
|
+
const quoted = parseSkillFile(path.join(fixtureRoot, "quoted-skill", "SKILL.md"));
|
|
12
|
+
assert.equal(quoted.name, "quoted-skill");
|
|
13
|
+
assert.equal(quoted.description, "Load when quoted scalar YAML should parse cleanly.");
|
|
14
|
+
assert.equal(quoted.descriptionLoadTrigger, true);
|
|
15
|
+
|
|
16
|
+
const block = parseSkillFile(path.join(fixtureRoot, "block-skill", "SKILL.md"));
|
|
17
|
+
assert.equal(block.name, "block-skill");
|
|
18
|
+
assert.match(block.description, /Load when block descriptions/);
|
|
19
|
+
assert.equal(block.descriptionLoadTrigger, true);
|
|
20
|
+
|
|
21
|
+
const minimal = parseSkillFile(path.join(fixtureRoot, "minimal-skill", "SKILL.md"));
|
|
22
|
+
assert.equal(minimal.hasGotchas, false);
|
|
23
|
+
assert.equal(minimal.hasWhenNotToUse, false);
|
|
24
|
+
|
|
25
|
+
const spoke = parseSkillFile(path.join(fixtureRoot, "spoke-skill", "SKILL.md"));
|
|
26
|
+
assert.deepEqual(spoke.spokeFiles, ["references/runbook.md"]);
|
|
27
|
+
assert.deepEqual(spoke.referencedSpokes, ["references/runbook.md"]);
|
|
28
|
+
assert.deepEqual(spoke.missingSpokeRefs, []);
|
|
29
|
+
assert.deepEqual(spoke.unreferencedSpokes, []);
|
|
30
|
+
|
|
31
|
+
const report = auditSkills({
|
|
32
|
+
skillsDir: fixtureRoot,
|
|
33
|
+
routeFile: path.join(fixtureRoot, "route-cases.json"),
|
|
34
|
+
});
|
|
35
|
+
assert.equal(report.summary.skills, 4);
|
|
36
|
+
assert.equal(report.skills.find((skill) => skill.name === "minimal-skill").violations.includes("gotchas-missing"), true);
|
|
37
|
+
assert.equal(report.skills.find((skill) => skill.name === "spoke-skill").violations.length, 0);
|
|
38
|
+
|
|
39
|
+
console.log("audit-skills tests passed");
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"skills": {
|
|
4
|
+
"quoted-skill": {
|
|
5
|
+
"positive": ["parse a quoted description", "audit scalar YAML frontmatter"],
|
|
6
|
+
"negative": ["parse block YAML only", "ignore all skill files"]
|
|
7
|
+
},
|
|
8
|
+
"block-skill": {
|
|
9
|
+
"positive": ["parse a block description", "audit multiline YAML frontmatter"],
|
|
10
|
+
"negative": ["parse quoted YAML only", "skip description checks"]
|
|
11
|
+
},
|
|
12
|
+
"minimal-skill": {
|
|
13
|
+
"positive": ["confirm missing optional sections do not crash", "audit a tiny skill"],
|
|
14
|
+
"negative": ["validate spoke references", "require gotchas to parse frontmatter"]
|
|
15
|
+
},
|
|
16
|
+
"spoke-skill": {
|
|
17
|
+
"positive": ["validate spoke references", "find auxiliary runbooks"],
|
|
18
|
+
"negative": ["audit only frontmatter", "ignore references directories"]
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: spoke-skill
|
|
3
|
+
description: "Load when auxiliary files and references need integrity checks."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Spoke Skill
|
|
7
|
+
|
|
8
|
+
Read [the runbook](references/runbook.md) when executing the full workflow.
|
|
9
|
+
|
|
10
|
+
## Gotchas
|
|
11
|
+
|
|
12
|
+
- Every spoke file must be referenced by the hub.
|
package/lib/i18n/slides.sh
DELETED
package/lib/i18n/slides_build.sh
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
_i18n_set en slides_build.unknown_option_1 "Unknown option: %s"
|
|
3
|
-
_i18n_set zh slides_build.unknown_option_1 "未知选项: %s"
|
|
4
|
-
_i18n_set en slides_build.unexpected_argument_1 "Unexpected argument: %s"
|
|
5
|
-
_i18n_set zh slides_build.unexpected_argument_1 "多余参数: %s"
|
|
6
|
-
_i18n_set en slides_build.slides_toolchain_missing_re_run_roll "Slides toolchain missing — re-run 'roll setup'"
|
|
7
|
-
_i18n_set zh slides_build.slides_toolchain_missing_re_run_roll "渲染工具缺失,请运行 roll setup"
|
|
8
|
-
_i18n_set en slides_build.deck_md " deck.md"
|
|
9
|
-
_i18n_set zh slides_build.deck_md "校验失败,请先修复上方提示再重试。"
|
|
10
|
-
_i18n_set en slides_build.template_not_found "Template not found: %s"
|
|
11
|
-
_i18n_set zh slides_build.template_not_found "未找到模板:%s"
|
|
12
|
-
_i18n_set en slides_build.render_failed_for "Render failed for %s"
|
|
13
|
-
_i18n_set zh slides_build.render_failed_for "渲染失败:%s"
|
|
14
|
-
_i18n_set en slides_build.rendered "Rendered → %s"
|
|
15
|
-
_i18n_set zh slides_build.rendered "渲染完成 → %s"
|
|
16
|
-
|
|
17
|
-
# US-DECK-012: build failure recovery paths
|
|
18
|
-
_i18n_set en slides_build.validation_failed_for "Validation failed for %s"
|
|
19
|
-
_i18n_set zh slides_build.validation_failed_for "校验失败:%s"
|
|
20
|
-
_i18n_set en slides_build.hint_fix_and_rerun "Hint: fix the issues above in %s and re-run 'roll slides build %s'"
|
|
21
|
-
_i18n_set zh slides_build.hint_fix_and_rerun "提示:请按上方提示修复 %s,然后重新运行 'roll slides build %s'"
|
|
22
|
-
_i18n_set en slides_build.available_templates "Available templates:"
|
|
23
|
-
_i18n_set zh slides_build.available_templates "可用模板:"
|
|
24
|
-
_i18n_set en slides_build.templates_list_hint "Hint: 'roll slides templates' lists all installed templates"
|
|
25
|
-
_i18n_set zh slides_build.templates_list_hint "提示:'roll slides templates' 列出所有已安装模板"
|
|
26
|
-
_i18n_set en slides_build.renderer_crashed_for "Renderer crashed for %s"
|
|
27
|
-
_i18n_set zh slides_build.renderer_crashed_for "渲染器崩溃:%s"
|
|
28
|
-
_i18n_set en slides_build.see_full_error_logs "See full error: roll slides logs %s"
|
|
29
|
-
_i18n_set zh slides_build.see_full_error_logs "查看完整错误:roll slides logs %s"
|
|
30
|
-
_i18n_set en slides_build.last_5_lines_of_renderer_output "Last 5 lines of renderer output:"
|
|
31
|
-
_i18n_set zh slides_build.last_5_lines_of_renderer_output "渲染器最后 5 行:"
|
|
32
|
-
|
|
33
|
-
_i18n_set en slides_build.usage_roll_slides_build_slug_no "Usage: roll slides build <slug> [--no-open]"
|
|
34
|
-
_i18n_set zh slides_build.usage_roll_slides_build_slug_no "用法: roll slides build <slug> [--no-open]"
|
|
35
|
-
_i18n_set en slides_build.en_deck "[EN: 未找到 deck 文件:%s...]"
|
|
36
|
-
_i18n_set zh slides_build.en_deck " 未找到 deck 文件:%s"
|
|
37
|
-
_i18n_set en slides_build.en_roll_slides_new "[EN: 提示:先运行 'roll slides new \"<主题>\"' 生成新的幻灯片。...]"
|
|
38
|
-
_i18n_set zh slides_build.en_roll_slides_new " 提示:先运行 'roll slides new \"<主题>\"' 生成新的幻灯片。"
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
_i18n_set en slides_delete.unknown_option_1 "Unknown option: %s"
|
|
3
|
-
_i18n_set zh slides_delete.unknown_option_1 "未知选项: %s"
|
|
4
|
-
_i18n_set en slides_delete.unexpected_argument_1 "Unexpected argument: %s"
|
|
5
|
-
_i18n_set zh slides_delete.unexpected_argument_1 "多余参数: %s"
|
|
6
|
-
_i18n_set en slides_delete.deck_not_found "Deck not found: %s"
|
|
7
|
-
_i18n_set zh slides_delete.deck_not_found "未找到幻灯片:%s"
|
|
8
|
-
_i18n_set en slides_delete.non_interactive_terminal_must_use_force "Non-interactive terminal: must use --force to delete"
|
|
9
|
-
_i18n_set zh slides_delete.non_interactive_terminal_must_use_force "非交互终端:需使用 --force 参数"
|
|
10
|
-
_i18n_set en slides_delete.cancelled "Cancelled"
|
|
11
|
-
_i18n_set zh slides_delete.cancelled "已取消"
|
|
12
|
-
_i18n_set en slides_delete.deleted "Deleted %s"
|
|
13
|
-
_i18n_set zh slides_delete.deleted "已删除 %s"
|
|
14
|
-
|
|
15
|
-
_i18n_set en slides_delete.usage_roll_slides_delete_slug_force "Usage: roll slides delete <slug> [--force]"
|
|
16
|
-
_i18n_set zh slides_delete.usage_roll_slides_delete_slug_force "用法: roll slides delete <slug> [--force]"
|
|
17
|
-
|
|
18
|
-
_i18n_set en slides_delete.prompt 'Delete deck "%s"? (y/N)'
|
|
19
|
-
_i18n_set zh slides_delete.prompt '删除幻灯片 "%s"?(y/N)'
|
package/lib/i18n/slides_list.sh
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
_i18n_set en slides_list.unknown_option_1 "Unknown option: %s"
|
|
3
|
-
_i18n_set zh slides_list.unknown_option_1 "未知选项: %s"
|
|
4
|
-
_i18n_set en slides_list.unexpected_argument_1 "Unexpected argument: %s"
|
|
5
|
-
_i18n_set zh slides_list.unexpected_argument_1 "多余参数: %s"
|
|
6
|
-
_i18n_set en slides_list.no_decks_found_under_roll_slides "No decks found under .roll/slides/"
|
|
7
|
-
_i18n_set zh slides_list.no_decks_found_under_roll_slides "无幻灯片"
|
|
8
|
-
_i18n_set en slides_list.no_decks_found_under_roll_slides_2 "No decks found under .roll/slides/"
|
|
9
|
-
_i18n_set zh slides_list.no_decks_found_under_roll_slides_2 "无幻灯片"
|
|
10
|
-
|
|
11
|
-
_i18n_set en slides_list.en_roll_slides_new "[EN: 提示:运行 'roll slides new \"<主题>\"' 创建第一个幻灯片。...]"
|
|
12
|
-
_i18n_set zh slides_list.en_roll_slides_new " 提示:运行 'roll slides new \"<主题>\"' 创建第一个幻灯片。"
|
|
13
|
-
_i18n_set en slides_list.en_roll_slides_new_2 "[EN: 提示:运行 'roll slides new \"<主题>\"' 创建第一个幻灯片。...]"
|
|
14
|
-
_i18n_set zh slides_list.en_roll_slides_new_2 " 提示:运行 'roll slides new \"<主题>\"' 创建第一个幻灯片。"
|
package/lib/i18n/slides_logs.sh
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
_i18n_set en slides_logs.unknown_option_1 "Unknown option: %s"
|
|
3
|
-
_i18n_set zh slides_logs.unknown_option_1 "未知选项: %s"
|
|
4
|
-
_i18n_set en slides_logs.unexpected_argument_1 "Unexpected argument: %s"
|
|
5
|
-
_i18n_set zh slides_logs.unexpected_argument_1 "多余参数: %s"
|
|
6
|
-
_i18n_set en slides_logs.deck_not_found "Deck not found: %s"
|
|
7
|
-
_i18n_set zh slides_logs.deck_not_found "未找到幻灯片:%s"
|
|
8
|
-
_i18n_set en slides_logs.no_failure_records_for "No failure records for %s"
|
|
9
|
-
_i18n_set zh slides_logs.no_failure_records_for "该幻灯片没有失败记录"
|
|
10
|
-
|
|
11
|
-
_i18n_set en slides_logs.usage_roll_slides_logs_slug "Usage: roll slides logs <slug>"
|
|
12
|
-
_i18n_set zh slides_logs.usage_roll_slides_logs_slug "用法: roll slides logs <slug>"
|
package/lib/i18n/slides_new.sh
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
_i18n_set en slides_new.unknown_option_1 "Unknown option: %s"
|
|
3
|
-
_i18n_set zh slides_new.unknown_option_1 "未知选项: %s"
|
|
4
|
-
_i18n_set en slides_new.unexpected_argument_1 "Unexpected argument: %s"
|
|
5
|
-
_i18n_set zh slides_new.unexpected_argument_1 "多余参数: %s"
|
|
6
|
-
|
|
7
|
-
_i18n_set en slides_new.en_roll_slides_new_template "[EN: 用法:roll slides new \"<主题>\" [--template <模板名>] [...]"
|
|
8
|
-
_i18n_set zh slides_new.en_roll_slides_new_template " 用法:roll slides new \"<主题>\" [--template <模板名>] [--quiet] [--no-build]"
|
|
9
|
-
_i18n_set en slides_new.en_slug "[EN: 无法从主题派生 slug:%s...]"
|
|
10
|
-
_i18n_set zh slides_new.en_slug " 无法从主题派生 slug:%s"
|
|
11
|
-
_i18n_set en slides_new.en_roll_slides_build "[EN:下一步:roll slides build %s...]"
|
|
12
|
-
_i18n_set zh slides_new.en_roll_slides_build "下一步:roll slides build %s"
|
|
13
|
-
|
|
14
|
-
_i18n_set en slides_new.template_requires_value "--template requires a value"
|
|
15
|
-
_i18n_set zh slides_new.template_requires_value "--template 需要一个值"
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
_i18n_set en slides_preview.unknown_option_1 "Unknown option: %s"
|
|
3
|
-
_i18n_set zh slides_preview.unknown_option_1 "未知选项: %s"
|
|
4
|
-
_i18n_set en slides_preview.unexpected_argument_1 "Unexpected argument: %s"
|
|
5
|
-
_i18n_set zh slides_preview.unexpected_argument_1 "多余参数: %s"
|
|
6
|
-
_i18n_set en slides_preview.preview "Preview → %s"
|
|
7
|
-
_i18n_set zh slides_preview.preview "打开预览 → %s"
|
|
8
|
-
|
|
9
|
-
_i18n_set en slides_preview.usage_roll_slides_preview_slug_no "Usage: roll slides preview <slug> [--no-open]"
|
|
10
|
-
_i18n_set zh slides_preview.usage_roll_slides_preview_slug_no "用法: roll slides preview <slug> [--no-open]"
|
|
11
|
-
_i18n_set en slides_preview.en_html "[EN: 未找到已渲染的 HTML:%s...]"
|
|
12
|
-
_i18n_set zh slides_preview.en_html " 未找到已渲染的 HTML:%s"
|
|
13
|
-
_i18n_set en slides_preview.en_roll_slides_build "[EN: 提示:先运行 'roll slides build %s' 渲染幻灯片。...]"
|
|
14
|
-
_i18n_set zh slides_preview.en_roll_slides_build " 提示:先运行 'roll slides build %s' 渲染幻灯片。"
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
_i18n_set en slides_templates.unknown_option_1 "Unknown option: %s"
|
|
3
|
-
_i18n_set zh slides_templates.unknown_option_1 "未知选项: %s"
|
|
4
|
-
_i18n_set en slides_templates.unexpected_argument_1 "Unexpected argument: %s"
|
|
5
|
-
_i18n_set zh slides_templates.unexpected_argument_1 "多余参数: %s"
|
|
6
|
-
_i18n_set en slides_templates.no_templates_found "No templates found"
|
|
7
|
-
_i18n_set zh slides_templates.no_templates_found "无可用模板"
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
# Slide Components Library
|
|
2
|
-
|
|
3
|
-
Reusable Mustache partials for slide layouts. Each partial is a self-contained
|
|
4
|
-
HTML fragment consumed by `lib/slides-render.py`. Partials use CSS classes from
|
|
5
|
-
the introduction-v3 template and require no additional stylesheets.
|
|
6
|
-
|
|
7
|
-
## Layout Reference
|
|
8
|
-
|
|
9
|
-
| Layout | Partial File | Use When | Avoid When |
|
|
10
|
-
|-------------|--------------------|--------------------------------------------|-------------------------------------|
|
|
11
|
-
| `plain` | `plain.html` | Free-form text, no structure needed | Data has compare / flow / timeline |
|
|
12
|
-
| `cards-2` | `cards-2.html` | 2 parallel concepts, side-by-side feature | 3+ items (use cards-3/cards-4) |
|
|
13
|
-
| `cards-3` | `cards-3.html` | 3 pillars, triple option, 3-step summary | 2 items (use cards-2) |
|
|
14
|
-
| `cards-4` | `cards-4.html` | 4 quadrants, pricing tiers, team roles | <4 items (too sparse) |
|
|
15
|
-
| `compare` | `compare.html` | Before/after, problem/solution, old/new | Unrelated items (use cards) |
|
|
16
|
-
| `pipeline` | `pipeline.html` | Sequential flow, CI/CD, process steps | Unordered items (use cards) |
|
|
17
|
-
| `timeline` | `timeline.html` | Chronological events, history, roadmap | Single event (use highlight) |
|
|
18
|
-
| `quote` | `quote.html` | Testimonial, key takeaway, memorable line | Multi-paragraph prose (use plain) |
|
|
19
|
-
| `highlight` | `highlight.html` | Callout, warning, important note | Normal body text (use plain) |
|
|
20
|
-
|
|
21
|
-
## Field Tables
|
|
22
|
-
|
|
23
|
-
### cards-2 / cards-3 / cards-4
|
|
24
|
-
|
|
25
|
-
| Variable | Required | Type | Description |
|
|
26
|
-
|-----------------|----------|--------|---------------------------------|
|
|
27
|
-
| `cards` | yes | array | Array of card objects |
|
|
28
|
-
| `cards[].title_en` | yes | string | Card title (English) |
|
|
29
|
-
| `cards[].title_zh` | yes | string | Card title (Chinese) |
|
|
30
|
-
| `cards[].body_en` | yes | string | Card body HTML (English, raw) |
|
|
31
|
-
| `cards[].body_zh` | yes | string | Card body HTML (Chinese, raw) |
|
|
32
|
-
| `accent_color` | no | string | Unused — reserved for future |
|
|
33
|
-
|
|
34
|
-
### compare
|
|
35
|
-
|
|
36
|
-
| Variable | Required | Type | Description |
|
|
37
|
-
|----------------------|----------|--------|---------------------------------|
|
|
38
|
-
| `left_title_en` | yes | string | Left column heading (EN) |
|
|
39
|
-
| `left_title_zh` | yes | string | Left column heading (ZH) |
|
|
40
|
-
| `right_title_en` | yes | string | Right column heading (EN) |
|
|
41
|
-
| `right_title_zh` | yes | string | Right column heading (ZH) |
|
|
42
|
-
| `left_items` | yes | array | Left column items |
|
|
43
|
-
| `left_items[].text_en` | yes | string | Item text (EN) |
|
|
44
|
-
| `left_items[].text_zh` | yes | string | Item text (ZH) |
|
|
45
|
-
| `right_items` | yes | array | Right column items |
|
|
46
|
-
| `right_items[].text_en` | yes | string | Item text (EN) |
|
|
47
|
-
| `right_items[].text_zh` | yes | string | Item text (ZH) |
|
|
48
|
-
|
|
49
|
-
### pipeline
|
|
50
|
-
|
|
51
|
-
| Variable | Required | Type | Description |
|
|
52
|
-
|---------------------|----------|--------|---------------------------------|
|
|
53
|
-
| `stages` | yes | array | Pipeline stages in order |
|
|
54
|
-
| `stages[].title_en` | yes | string | Stage title (EN) |
|
|
55
|
-
| `stages[].title_zh` | yes | string | Stage title (ZH) |
|
|
56
|
-
| `stages[].desc_en` | yes | string | Stage description (EN) |
|
|
57
|
-
| `stages[].desc_zh` | yes | string | Stage description (ZH) |
|
|
58
|
-
| `stages[].css_class` | yes | string | CSS class: `pipe-idea`, `pipe-backlog`, `pipe-build`, `pipe-verify`, or `pipe-release` |
|
|
59
|
-
|
|
60
|
-
### timeline
|
|
61
|
-
|
|
62
|
-
| Variable | Required | Type | Description |
|
|
63
|
-
|---------------------|----------|--------|---------------------------------|
|
|
64
|
-
| `items` | yes | array | Timeline entries (chronological)|
|
|
65
|
-
| `items[].title_en` | yes | string | Entry title (EN) |
|
|
66
|
-
| `items[].title_zh` | yes | string | Entry title (ZH) |
|
|
67
|
-
| `items[].body_en` | yes | string | Entry body HTML (EN, raw) |
|
|
68
|
-
| `items[].body_zh` | yes | string | Entry body HTML (ZH, raw) |
|
|
69
|
-
|
|
70
|
-
### quote
|
|
71
|
-
|
|
72
|
-
| Variable | Required | Type | Description |
|
|
73
|
-
|-----------|----------|--------|---------------------------------|
|
|
74
|
-
| `text_en` | yes | string | Quote text (EN) |
|
|
75
|
-
| `text_zh` | yes | string | Quote text (ZH) |
|
|
76
|
-
|
|
77
|
-
### highlight
|
|
78
|
-
|
|
79
|
-
| Variable | Required | Type | Description |
|
|
80
|
-
|-----------|----------|--------|---------------------------------|
|
|
81
|
-
| `body_en` | yes | string | Body HTML (EN, raw) |
|
|
82
|
-
| `body_zh` | yes | string | Body HTML (ZH, raw) |
|
|
83
|
-
|
|
84
|
-
### plain
|
|
85
|
-
|
|
86
|
-
| Variable | Required | Type | Description |
|
|
87
|
-
|-----------|----------|--------|---------------------------------|
|
|
88
|
-
| `body_en` | yes | string | Body HTML (EN, raw) |
|
|
89
|
-
| `body_zh` | yes | string | Body HTML (ZH, raw) |
|
|
90
|
-
|
|
91
|
-
## CSS Classes
|
|
92
|
-
|
|
93
|
-
Every class name in these partials is copied verbatim from the introduction-v3
|
|
94
|
-
template (`lib/slides/templates/introduction-v3.html`). Do **not** introduce
|
|
95
|
-
new class names — the template's CSS is the single source of truth.
|
|
96
|
-
|
|
97
|
-
## Usage
|
|
98
|
-
|
|
99
|
-
Partials are consumed by `lib/slides-render.py` when a `deck.md` slide declares
|
|
100
|
-
a `layout` field. A slide that omits `layout` renders as `plain`.
|
|
101
|
-
|
|
102
|
-
For the user-facing walkthrough — per-layout `deck.md` examples, rendered
|
|
103
|
-
screenshots, and how `$roll-deck` picks a layout — see the Layouts section of
|
|
104
|
-
the slides guide: [`guide/en/slides.md`](../../../guide/en/slides.md#layouts)
|
|
105
|
-
([中文](../../../guide/zh/slides.md#layouts布局)). Field names in this file,
|
|
106
|
-
that guide, and `skills/roll-deck/SKILL.md` are kept in sync — change one,
|
|
107
|
-
change all three.
|
|
108
|
-
|
|
109
|
-
```markdown
|
|
110
|
-
### Slide 3: Architecture Overview
|
|
111
|
-
|
|
112
|
-
layout: cards-3
|
|
113
|
-
title_en: Three Layers
|
|
114
|
-
title_zh: 三层架构
|
|
115
|
-
|
|
116
|
-
body_en: |
|
|
117
|
-
1. {{#cards}}...{{/cards}}
|
|
118
|
-
body_zh: |
|
|
119
|
-
1. {{#cards}}...{{/cards}}
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
The renderer inlines the partial's HTML into the template and resolves
|
|
123
|
-
Mustache variables and sections from the slide context.
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
<!-- cards-2: requires cards[].{title_en,title_zh,body_en,body_zh}, optional accent_color -->
|
|
2
|
-
<div class="cards cards-2">
|
|
3
|
-
{{#cards}}
|
|
4
|
-
<div class="card">
|
|
5
|
-
<h3><span class="lang-en">{{title_en}}</span><span class="lang-zh">{{title_zh}}</span></h3>
|
|
6
|
-
<p><span class="lang-en">{{{body_en}}}</span><span class="lang-zh">{{{body_zh}}}</span></p>
|
|
7
|
-
</div>
|
|
8
|
-
{{/cards}}
|
|
9
|
-
</div>
|