dsh-nerd-mode 0.5.0 → 0.5.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/README.md +1 -1
- package/README.zh.md +1 -1
- package/assets/calibration.svg +69 -69
- package/assets/nerd.svg +22 -22
- package/assets/section.md +15 -15
- package/client.js +2170 -2225
- package/cordis.patch.yml +24 -24
- package/lib/index.js +838 -286
- package/lib/study-sessions.js +283 -283
- package/package.json +70 -70
- package/skills/convert-documents-to-markdown/SKILL.md +26 -26
- package/skills/diagram/SKILL.md +43 -19
- package/skills/diagram/scripts/check-svg.mjs +184 -0
- package/skills/humanizer/.claude-plugin/marketplace.json +18 -18
- package/skills/humanizer/.claude-plugin/plugin.json +15 -15
- package/skills/humanizer/.cursor-plugin/plugin.json +12 -12
- package/skills/humanizer/.github/ISSUE_TEMPLATE/config.yml +8 -8
- package/skills/humanizer/.github/ISSUE_TEMPLATE/new-pattern.yml +24 -24
- package/skills/humanizer/.github/ISSUE_TEMPLATE/rewrite-problem.yml +44 -44
- package/skills/humanizer/.github/workflows/validate.yml +29 -29
- package/skills/humanizer/AGENTS.md +54 -54
- package/skills/humanizer/CHANGELOG.md +116 -116
- package/skills/humanizer/LICENSE +21 -21
- package/skills/humanizer/README.md +206 -206
- package/skills/humanizer/SKILL.md +397 -397
- package/skills/humanizer/agents/openai.yaml +4 -4
- package/skills/humanizer/scripts/validate-package.py +146 -146
- package/skills/study/SKILL.md +81 -78
- package/skills/study-plus/SKILL.md +246 -246
- package/skills/study-plus/scripts/book-client.js +284 -284
- package/skills/study-plus/scripts/book-data.mjs +289 -289
- package/skills/study-plus/scripts/build-book.mjs +155 -155
- package/skills/writing-beats/SKILL.md +66 -66
- package/skills/writing-beats/agents/openai.yaml +5 -5
|
@@ -1,155 +1,155 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Build study/<course>/book.html from the graph and its existing lecture files.
|
|
3
|
-
|
|
4
|
-
import { readFileSync, readdirSync, writeFileSync, existsSync, unlinkSync, realpathSync, statSync, lstatSync, copyFileSync, renameSync } from 'node:fs';
|
|
5
|
-
import { join, resolve, dirname, relative, isAbsolute, sep } from 'node:path';
|
|
6
|
-
import { spawnSync } from 'node:child_process';
|
|
7
|
-
import { createHash } from 'node:crypto';
|
|
8
|
-
import { fileURLToPath } from 'node:url';
|
|
9
|
-
import { parseBookData } from './book-data.mjs';
|
|
10
|
-
|
|
11
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
12
|
-
const AM = resolve(here, '..', '..', 'answer-me-with-html', 'scripts', 'am.mjs');
|
|
13
|
-
const CLIENT = join(here, 'book-client.js');
|
|
14
|
-
|
|
15
|
-
function die(message) {
|
|
16
|
-
console.error(message);
|
|
17
|
-
process.exit(1);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function findTopic(arg) {
|
|
21
|
-
const studyPath = resolve('study');
|
|
22
|
-
let studyStat;
|
|
23
|
-
try {
|
|
24
|
-
studyStat = lstatSync(studyPath);
|
|
25
|
-
} catch (error) {
|
|
26
|
-
if (error.code === 'ENOENT') die('No study/ folder here. Pass a Course folder under workspace study/.');
|
|
27
|
-
die(`Cannot inspect workspace study/ folder: ${error.message}`);
|
|
28
|
-
}
|
|
29
|
-
if (studyStat.isSymbolicLink()) die(`Refusing to use symlink as workspace study/ folder: ${studyPath}`);
|
|
30
|
-
let studyRoot;
|
|
31
|
-
try {
|
|
32
|
-
studyRoot = realpathSync(studyPath);
|
|
33
|
-
} catch (error) {
|
|
34
|
-
die(`Cannot resolve workspace study/ folder: ${error.message}`);
|
|
35
|
-
}
|
|
36
|
-
if (!statSync(studyRoot).isDirectory()) die(`Workspace study/ path is not a directory: ${studyRoot}`);
|
|
37
|
-
|
|
38
|
-
let topicPath = arg ? resolve(arg) : '';
|
|
39
|
-
if (!arg) {
|
|
40
|
-
const hits = readdirSync(studyRoot).filter((name) => existsSync(join(studyRoot, name, 'graph.md')));
|
|
41
|
-
if (hits.length !== 1) die(`Pass a Course folder under workspace study/. Found ${hits.length} Courses under study/: ${hits.join(', ') || 'none'}.`);
|
|
42
|
-
topicPath = join(studyRoot, hits[0]);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
let topicDir;
|
|
46
|
-
try {
|
|
47
|
-
topicDir = realpathSync(topicPath);
|
|
48
|
-
} catch (error) {
|
|
49
|
-
die(`Cannot resolve Course folder ${topicPath}: ${error.message}`);
|
|
50
|
-
}
|
|
51
|
-
const topicRelative = relative(studyRoot, topicDir);
|
|
52
|
-
if (!statSync(topicDir).isDirectory() || !topicRelative || topicRelative === '..' || topicRelative.startsWith(`..${sep}`) || isAbsolute(topicRelative)) {
|
|
53
|
-
die(`Course folder must be a real directory at least one level below workspace study/: ${topicPath}`);
|
|
54
|
-
}
|
|
55
|
-
return topicDir;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function build() {
|
|
59
|
-
const dir = findTopic(process.argv[2]);
|
|
60
|
-
const graphPath = join(dir, 'graph.md');
|
|
61
|
-
const lectureDir = join(dir, 'lectures');
|
|
62
|
-
if (!existsSync(graphPath)) die(`graph.md not found at ${graphPath}`);
|
|
63
|
-
if (!existsSync(lectureDir)) die(`No lectures folder found at ${lectureDir}`);
|
|
64
|
-
if (!existsSync(AM)) die(`am CLI not found at ${AM}. Install answer-me-with-html next to study-plus.`);
|
|
65
|
-
if (!existsSync(CLIENT)) die(`book-client.js not found at ${CLIENT}. Install the study-plus runtime scripts first.`);
|
|
66
|
-
|
|
67
|
-
const files = readdirSync(lectureDir)
|
|
68
|
-
.filter((name) => name.endsWith('.md') && !name.startsWith('.'))
|
|
69
|
-
.sort()
|
|
70
|
-
.map((name) => ({ name, text: readFileSync(join(lectureDir, name), 'utf8') }));
|
|
71
|
-
let book;
|
|
72
|
-
try {
|
|
73
|
-
book = parseBookData(readFileSync(graphPath, 'utf8'), files, { topicFallback: dir.split(/[\\/]/).pop() });
|
|
74
|
-
} catch (error) {
|
|
75
|
-
die(error.message);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const draft = [
|
|
79
|
-
'---',
|
|
80
|
-
'template: doc',
|
|
81
|
-
`title: ${JSON.stringify(book.title)}`,
|
|
82
|
-
...(book.goal ? [`subtitle: ${JSON.stringify(book.goal)}`] : []),
|
|
83
|
-
'---',
|
|
84
|
-
'',
|
|
85
|
-
book.units.map((unit) => `## ${unit.chapter}.${unit.section} ${unit.title}\n\n${unit.body}`).join('\n\n'),
|
|
86
|
-
'',
|
|
87
|
-
].join('\n')
|
|
88
|
-
// am reads $ and $$ math only: a LaTeX display delimiter on its own line
|
|
89
|
-
// would reach the page as a literal bracket, so turn it into a $$ block.
|
|
90
|
-
.replace(/^ {0,3}\\\[\s*$/gm, () => '$$')
|
|
91
|
-
.replace(/^ {0,3}\\\]\s*$/gm, () => '$$');
|
|
92
|
-
const outPath = join(dir, 'book.html');
|
|
93
|
-
const backupPath = join(dir, 'book.html.bak');
|
|
94
|
-
const guardPath = (path, what) => {
|
|
95
|
-
try {
|
|
96
|
-
const stat = lstatSync(path);
|
|
97
|
-
if (stat.isSymbolicLink()) die(`Refusing to overwrite symlink book ${what}: ${path}`);
|
|
98
|
-
if (!stat.isFile()) die(`Refusing to overwrite non-regular book ${what}: ${path}`);
|
|
99
|
-
} catch (error) {
|
|
100
|
-
if (error.code !== 'ENOENT') die(`Cannot inspect book ${what} ${path}: ${error.message}`);
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
guardPath(outPath, 'output');
|
|
104
|
-
guardPath(backupPath, 'backup');
|
|
105
|
-
// Render beside book.html: a failed build must never damage the current book.
|
|
106
|
-
const tmpPath = join(dir, `.book.new-${process.pid}.html`);
|
|
107
|
-
const draftPath = join(dir, `.book.draft-${process.pid}.md`);
|
|
108
|
-
writeFileSync(draftPath, draft, { encoding: 'utf8', flag: 'wx' });
|
|
109
|
-
|
|
110
|
-
let run;
|
|
111
|
-
try {
|
|
112
|
-
// Book builds stay offline: without this the renderer may start a weekly update check.
|
|
113
|
-
run = spawnSync(process.execPath, [AM, 'render', draftPath, '-o', tmpPath, '--no-open', '--template', 'doc', '--theme', 'shadcn', '--mode', 'light', '--style', 'off'], { stdio: 'inherit', env: { ...process.env, AM_NO_UPDATE_CHECK: '1' } });
|
|
114
|
-
} finally {
|
|
115
|
-
if (existsSync(draftPath)) unlinkSync(draftPath);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
try {
|
|
119
|
-
if (run.error) throw new Error(`Could not run am render: ${run.error.message}`);
|
|
120
|
-
if (run.status !== 0) throw new Error(`am render failed with exit ${run.status ?? 'unknown'}`);
|
|
121
|
-
|
|
122
|
-
const payload = {
|
|
123
|
-
title: book.title,
|
|
124
|
-
goal: book.goal,
|
|
125
|
-
lang: book.lang,
|
|
126
|
-
chapters: book.chapters.map(({ n, name, description, count }) => ({ n, name, description, count })),
|
|
127
|
-
units: book.units.map(({ concept, title, chapter, section, index, status }) => ({ concept, title, chapter, section, index, status })),
|
|
128
|
-
};
|
|
129
|
-
const json = JSON.stringify(payload).replace(/</g, '\\u003c');
|
|
130
|
-
// HTML parsing turns CRLF into LF before the browser checks the CSP hash,
|
|
131
|
-
// so a Windows checkout would otherwise be blocked as the wrong hash.
|
|
132
|
-
const client = readFileSync(CLIENT, 'utf8').replace(/\r\n?/g, '\n');
|
|
133
|
-
const html = readFileSync(tmpPath, 'utf8');
|
|
134
|
-
const scriptText = `\n${client}\n`;
|
|
135
|
-
const hash = createHash('sha256').update(scriptText, 'utf8').digest('base64');
|
|
136
|
-
const csp = /(<meta http-equiv="Content-Security-Policy" content="[^"]*?script-src )/;
|
|
137
|
-
if (!csp.test(html)) throw new Error('No Content-Security-Policy script-src entry found in the rendered page.');
|
|
138
|
-
const out = html
|
|
139
|
-
.replace(csp, `$1'sha256-${hash}' `)
|
|
140
|
-
.replace('</head>', `<script>${scriptText}</script>\n</head>`)
|
|
141
|
-
.replace('</body>', `<script type="application/json" id="bk-book">${json}</script>\n</body>`);
|
|
142
|
-
writeFileSync(tmpPath, out, 'utf8');
|
|
143
|
-
|
|
144
|
-
if (existsSync(outPath)) copyFileSync(outPath, backupPath);
|
|
145
|
-
renameSync(tmpPath, outPath);
|
|
146
|
-
} catch (error) {
|
|
147
|
-
if (existsSync(tmpPath)) unlinkSync(tmpPath);
|
|
148
|
-
die(error.message);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (book.orphans.length) console.warn(`Orphan lecture files excluded (concept number is not in graph.md): ${book.orphans.join(', ')}`);
|
|
152
|
-
console.log(`book: ${outPath} (${book.units.length} sections, ${book.chapters.length} chapters)`);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
build();
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Build study/<course>/book.html from the graph and its existing lecture files.
|
|
3
|
+
|
|
4
|
+
import { readFileSync, readdirSync, writeFileSync, existsSync, unlinkSync, realpathSync, statSync, lstatSync, copyFileSync, renameSync } from 'node:fs';
|
|
5
|
+
import { join, resolve, dirname, relative, isAbsolute, sep } from 'node:path';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { parseBookData } from './book-data.mjs';
|
|
10
|
+
|
|
11
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const AM = resolve(here, '..', '..', 'answer-me-with-html', 'scripts', 'am.mjs');
|
|
13
|
+
const CLIENT = join(here, 'book-client.js');
|
|
14
|
+
|
|
15
|
+
function die(message) {
|
|
16
|
+
console.error(message);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function findTopic(arg) {
|
|
21
|
+
const studyPath = resolve('study');
|
|
22
|
+
let studyStat;
|
|
23
|
+
try {
|
|
24
|
+
studyStat = lstatSync(studyPath);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (error.code === 'ENOENT') die('No study/ folder here. Pass a Course folder under workspace study/.');
|
|
27
|
+
die(`Cannot inspect workspace study/ folder: ${error.message}`);
|
|
28
|
+
}
|
|
29
|
+
if (studyStat.isSymbolicLink()) die(`Refusing to use symlink as workspace study/ folder: ${studyPath}`);
|
|
30
|
+
let studyRoot;
|
|
31
|
+
try {
|
|
32
|
+
studyRoot = realpathSync(studyPath);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
die(`Cannot resolve workspace study/ folder: ${error.message}`);
|
|
35
|
+
}
|
|
36
|
+
if (!statSync(studyRoot).isDirectory()) die(`Workspace study/ path is not a directory: ${studyRoot}`);
|
|
37
|
+
|
|
38
|
+
let topicPath = arg ? resolve(arg) : '';
|
|
39
|
+
if (!arg) {
|
|
40
|
+
const hits = readdirSync(studyRoot).filter((name) => existsSync(join(studyRoot, name, 'graph.md')));
|
|
41
|
+
if (hits.length !== 1) die(`Pass a Course folder under workspace study/. Found ${hits.length} Courses under study/: ${hits.join(', ') || 'none'}.`);
|
|
42
|
+
topicPath = join(studyRoot, hits[0]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let topicDir;
|
|
46
|
+
try {
|
|
47
|
+
topicDir = realpathSync(topicPath);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
die(`Cannot resolve Course folder ${topicPath}: ${error.message}`);
|
|
50
|
+
}
|
|
51
|
+
const topicRelative = relative(studyRoot, topicDir);
|
|
52
|
+
if (!statSync(topicDir).isDirectory() || !topicRelative || topicRelative === '..' || topicRelative.startsWith(`..${sep}`) || isAbsolute(topicRelative)) {
|
|
53
|
+
die(`Course folder must be a real directory at least one level below workspace study/: ${topicPath}`);
|
|
54
|
+
}
|
|
55
|
+
return topicDir;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function build() {
|
|
59
|
+
const dir = findTopic(process.argv[2]);
|
|
60
|
+
const graphPath = join(dir, 'graph.md');
|
|
61
|
+
const lectureDir = join(dir, 'lectures');
|
|
62
|
+
if (!existsSync(graphPath)) die(`graph.md not found at ${graphPath}`);
|
|
63
|
+
if (!existsSync(lectureDir)) die(`No lectures folder found at ${lectureDir}`);
|
|
64
|
+
if (!existsSync(AM)) die(`am CLI not found at ${AM}. Install answer-me-with-html next to study-plus.`);
|
|
65
|
+
if (!existsSync(CLIENT)) die(`book-client.js not found at ${CLIENT}. Install the study-plus runtime scripts first.`);
|
|
66
|
+
|
|
67
|
+
const files = readdirSync(lectureDir)
|
|
68
|
+
.filter((name) => name.endsWith('.md') && !name.startsWith('.'))
|
|
69
|
+
.sort()
|
|
70
|
+
.map((name) => ({ name, text: readFileSync(join(lectureDir, name), 'utf8') }));
|
|
71
|
+
let book;
|
|
72
|
+
try {
|
|
73
|
+
book = parseBookData(readFileSync(graphPath, 'utf8'), files, { topicFallback: dir.split(/[\\/]/).pop() });
|
|
74
|
+
} catch (error) {
|
|
75
|
+
die(error.message);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const draft = [
|
|
79
|
+
'---',
|
|
80
|
+
'template: doc',
|
|
81
|
+
`title: ${JSON.stringify(book.title)}`,
|
|
82
|
+
...(book.goal ? [`subtitle: ${JSON.stringify(book.goal)}`] : []),
|
|
83
|
+
'---',
|
|
84
|
+
'',
|
|
85
|
+
book.units.map((unit) => `## ${unit.chapter}.${unit.section} ${unit.title}\n\n${unit.body}`).join('\n\n'),
|
|
86
|
+
'',
|
|
87
|
+
].join('\n')
|
|
88
|
+
// am reads $ and $$ math only: a LaTeX display delimiter on its own line
|
|
89
|
+
// would reach the page as a literal bracket, so turn it into a $$ block.
|
|
90
|
+
.replace(/^ {0,3}\\\[\s*$/gm, () => '$$')
|
|
91
|
+
.replace(/^ {0,3}\\\]\s*$/gm, () => '$$');
|
|
92
|
+
const outPath = join(dir, 'book.html');
|
|
93
|
+
const backupPath = join(dir, 'book.html.bak');
|
|
94
|
+
const guardPath = (path, what) => {
|
|
95
|
+
try {
|
|
96
|
+
const stat = lstatSync(path);
|
|
97
|
+
if (stat.isSymbolicLink()) die(`Refusing to overwrite symlink book ${what}: ${path}`);
|
|
98
|
+
if (!stat.isFile()) die(`Refusing to overwrite non-regular book ${what}: ${path}`);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (error.code !== 'ENOENT') die(`Cannot inspect book ${what} ${path}: ${error.message}`);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
guardPath(outPath, 'output');
|
|
104
|
+
guardPath(backupPath, 'backup');
|
|
105
|
+
// Render beside book.html: a failed build must never damage the current book.
|
|
106
|
+
const tmpPath = join(dir, `.book.new-${process.pid}.html`);
|
|
107
|
+
const draftPath = join(dir, `.book.draft-${process.pid}.md`);
|
|
108
|
+
writeFileSync(draftPath, draft, { encoding: 'utf8', flag: 'wx' });
|
|
109
|
+
|
|
110
|
+
let run;
|
|
111
|
+
try {
|
|
112
|
+
// Book builds stay offline: without this the renderer may start a weekly update check.
|
|
113
|
+
run = spawnSync(process.execPath, [AM, 'render', draftPath, '-o', tmpPath, '--no-open', '--template', 'doc', '--theme', 'shadcn', '--mode', 'light', '--style', 'off'], { stdio: 'inherit', env: { ...process.env, AM_NO_UPDATE_CHECK: '1' } });
|
|
114
|
+
} finally {
|
|
115
|
+
if (existsSync(draftPath)) unlinkSync(draftPath);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
if (run.error) throw new Error(`Could not run am render: ${run.error.message}`);
|
|
120
|
+
if (run.status !== 0) throw new Error(`am render failed with exit ${run.status ?? 'unknown'}`);
|
|
121
|
+
|
|
122
|
+
const payload = {
|
|
123
|
+
title: book.title,
|
|
124
|
+
goal: book.goal,
|
|
125
|
+
lang: book.lang,
|
|
126
|
+
chapters: book.chapters.map(({ n, name, description, count }) => ({ n, name, description, count })),
|
|
127
|
+
units: book.units.map(({ concept, title, chapter, section, index, status }) => ({ concept, title, chapter, section, index, status })),
|
|
128
|
+
};
|
|
129
|
+
const json = JSON.stringify(payload).replace(/</g, '\\u003c');
|
|
130
|
+
// HTML parsing turns CRLF into LF before the browser checks the CSP hash,
|
|
131
|
+
// so a Windows checkout would otherwise be blocked as the wrong hash.
|
|
132
|
+
const client = readFileSync(CLIENT, 'utf8').replace(/\r\n?/g, '\n');
|
|
133
|
+
const html = readFileSync(tmpPath, 'utf8');
|
|
134
|
+
const scriptText = `\n${client}\n`;
|
|
135
|
+
const hash = createHash('sha256').update(scriptText, 'utf8').digest('base64');
|
|
136
|
+
const csp = /(<meta http-equiv="Content-Security-Policy" content="[^"]*?script-src )/;
|
|
137
|
+
if (!csp.test(html)) throw new Error('No Content-Security-Policy script-src entry found in the rendered page.');
|
|
138
|
+
const out = html
|
|
139
|
+
.replace(csp, `$1'sha256-${hash}' `)
|
|
140
|
+
.replace('</head>', `<script>${scriptText}</script>\n</head>`)
|
|
141
|
+
.replace('</body>', `<script type="application/json" id="bk-book">${json}</script>\n</body>`);
|
|
142
|
+
writeFileSync(tmpPath, out, 'utf8');
|
|
143
|
+
|
|
144
|
+
if (existsSync(outPath)) copyFileSync(outPath, backupPath);
|
|
145
|
+
renameSync(tmpPath, outPath);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (existsSync(tmpPath)) unlinkSync(tmpPath);
|
|
148
|
+
die(error.message);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (book.orphans.length) console.warn(`Orphan lecture files excluded (concept number is not in graph.md): ${book.orphans.join(', ')}`);
|
|
152
|
+
console.log(`book: ${outPath} (${book.units.length} sections, ${book.chapters.length} chapters)`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
build();
|
|
@@ -1,66 +1,66 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: writing-beats
|
|
3
|
-
description: Writing, exploit; assemble raw material into a journey of beats, grounding each term before a beat leans on it.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
<what-to-do>
|
|
7
|
-
|
|
8
|
-
The user has passed (or will pass) a markdown file of raw material. This is **exploit**: the exploring is done, the pile is fixed. Commit to a path through it and mine the pile to fill each beat.
|
|
9
|
-
|
|
10
|
-
If the user did not say where to save the article, ask once and remember the path.
|
|
11
|
-
|
|
12
|
-
Then run a beat-by-beat journey, choose-your-own-adventure style:
|
|
13
|
-
|
|
14
|
-
1. **Establish the prerequisites.** Before any beats, settle with the user what the audience already knows walking in: the concepts that are **grounded** from the start. Everything else must be grounded by a beat before a later beat can use it. See [Grounding](#grounding).
|
|
15
|
-
2. Write 2–3 candidate **starting beats**, drawn from the raw material. Each is a different entry point into the article. Each may only lean on grounded concepts; note what new concepts each one grounds. Show the user the beats before writing to the article file. The user picks one. Preview what beats that pick unlocks, as if the user is seeing a little way down the path.
|
|
16
|
-
3. Once the user picks a starting beat, write **only that beat** to the article file. A beat may be one sentence or several paragraphs, whatever that beat naturally is. Stop there.
|
|
17
|
-
4. Re-read the article file from disk. Then offer 2–3 candidate **next beats**: different directions the journey could pivot to from where the article now stands. Each must be reachable from the current grounded set; note what each one grounds.
|
|
18
|
-
5. Loop steps 3–5 until the article reaches a natural end.
|
|
19
|
-
|
|
20
|
-
</what-to-do>
|
|
21
|
-
|
|
22
|
-
<supporting-info>
|
|
23
|
-
|
|
24
|
-
## Grounding
|
|
25
|
-
|
|
26
|
-
Every **concept** has to be **grounded** before a beat can lean on it: the audience either walked in knowing it or met it in an earlier beat. A beat that reaches for an ungrounded concept loses the reader; that is the one move the journey can't make. The unit is the concept, not the word for it: a beat can lean on an idea the reader lacks even with no jargon in sight. Where a concept has a name (a **term**), grounding it means landing the idea and the term together.
|
|
27
|
-
|
|
28
|
-
A concept gets grounded one of two ways:
|
|
29
|
-
|
|
30
|
-
- **Prerequisite**: grounded before the first beat. The audience brings it. Fixed at the start.
|
|
31
|
-
- **Introduced**: a beat establishes it, and from then on it's grounded for every later beat.
|
|
32
|
-
|
|
33
|
-
So each beat does two jobs: it **requires** concepts that are already grounded, and it **grounds** new ones. Keep a running list of what's grounded so far, and update it each time a beat lands.
|
|
34
|
-
|
|
35
|
-
This is what shapes the choose-your-own-adventure. A candidate beat is only reachable if everything it requires is already grounded; picking a beat that grounds concept X unlocks every beat that was waiting on X. When you offer next beats, they must all be reachable from the current grounded set, and say what each one grounds, so the user can see which paths it opens.
|
|
36
|
-
|
|
37
|
-
The big lever is what you make a prerequisite versus what you ground inside the piece. Demand too much up front and you shut out readers who don't have it; ground too much inside and the early beats drown in definitions. Settle this with the user when you establish prerequisites, and revisit it whenever a tempting beat turns out to require a concept nothing has grounded yet: the fix is either a grounding beat before it, or promoting the concept to a prerequisite.
|
|
38
|
-
|
|
39
|
-
## What is a beat
|
|
40
|
-
|
|
41
|
-
A beat is one move in the journey. It does one thing: sets a scene, lands a point, asks a question, drops an aside, twists the angle. Then it stops, leaving the reader at a place where the next beat can pivot.
|
|
42
|
-
|
|
43
|
-
A beat is sized by what it needs:
|
|
44
|
-
|
|
45
|
-
- A single sentence if that's all the move is ("And then nothing happened for three weeks.").
|
|
46
|
-
- A short paragraph if the move needs setup.
|
|
47
|
-
- Multiple paragraphs if the beat is a self-contained vignette, argument, or example.
|
|
48
|
-
|
|
49
|
-
If a "beat" needs five paragraphs and three subheadings, it's not a beat; it's two beats glued together. Split it.
|
|
50
|
-
|
|
51
|
-
## Pulling from the pile
|
|
52
|
-
|
|
53
|
-
Pull material from the raw pile to populate each beat. You can paraphrase, split, recombine, or quote. The pile is a quarry.
|
|
54
|
-
|
|
55
|
-
## Ending the journey
|
|
56
|
-
|
|
57
|
-
The article ends when the journey is complete, not when the pile is empty. Most piles will have leftover fragments that don't make it in. That is fine; that is the point of having more raw material than you need.
|
|
58
|
-
|
|
59
|
-
## Writing rhythm
|
|
60
|
-
|
|
61
|
-
- Append one beat at a time. Never write ahead.
|
|
62
|
-
- Re-read the article file from disk before every write. Preserve user edits absolutely.
|
|
63
|
-
- If the user edits a previous beat substantially, let it change what comes next.
|
|
64
|
-
- If the user says "rewrite that beat" or "go back and try a different beat 3", do it: edit in place, leave the rest alone.
|
|
65
|
-
|
|
66
|
-
</supporting-info>
|
|
1
|
+
---
|
|
2
|
+
name: writing-beats
|
|
3
|
+
description: Writing, exploit; assemble raw material into a journey of beats, grounding each term before a beat leans on it.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<what-to-do>
|
|
7
|
+
|
|
8
|
+
The user has passed (or will pass) a markdown file of raw material. This is **exploit**: the exploring is done, the pile is fixed. Commit to a path through it and mine the pile to fill each beat.
|
|
9
|
+
|
|
10
|
+
If the user did not say where to save the article, ask once and remember the path.
|
|
11
|
+
|
|
12
|
+
Then run a beat-by-beat journey, choose-your-own-adventure style:
|
|
13
|
+
|
|
14
|
+
1. **Establish the prerequisites.** Before any beats, settle with the user what the audience already knows walking in: the concepts that are **grounded** from the start. Everything else must be grounded by a beat before a later beat can use it. See [Grounding](#grounding).
|
|
15
|
+
2. Write 2–3 candidate **starting beats**, drawn from the raw material. Each is a different entry point into the article. Each may only lean on grounded concepts; note what new concepts each one grounds. Show the user the beats before writing to the article file. The user picks one. Preview what beats that pick unlocks, as if the user is seeing a little way down the path.
|
|
16
|
+
3. Once the user picks a starting beat, write **only that beat** to the article file. A beat may be one sentence or several paragraphs, whatever that beat naturally is. Stop there.
|
|
17
|
+
4. Re-read the article file from disk. Then offer 2–3 candidate **next beats**: different directions the journey could pivot to from where the article now stands. Each must be reachable from the current grounded set; note what each one grounds.
|
|
18
|
+
5. Loop steps 3–5 until the article reaches a natural end.
|
|
19
|
+
|
|
20
|
+
</what-to-do>
|
|
21
|
+
|
|
22
|
+
<supporting-info>
|
|
23
|
+
|
|
24
|
+
## Grounding
|
|
25
|
+
|
|
26
|
+
Every **concept** has to be **grounded** before a beat can lean on it: the audience either walked in knowing it or met it in an earlier beat. A beat that reaches for an ungrounded concept loses the reader; that is the one move the journey can't make. The unit is the concept, not the word for it: a beat can lean on an idea the reader lacks even with no jargon in sight. Where a concept has a name (a **term**), grounding it means landing the idea and the term together.
|
|
27
|
+
|
|
28
|
+
A concept gets grounded one of two ways:
|
|
29
|
+
|
|
30
|
+
- **Prerequisite**: grounded before the first beat. The audience brings it. Fixed at the start.
|
|
31
|
+
- **Introduced**: a beat establishes it, and from then on it's grounded for every later beat.
|
|
32
|
+
|
|
33
|
+
So each beat does two jobs: it **requires** concepts that are already grounded, and it **grounds** new ones. Keep a running list of what's grounded so far, and update it each time a beat lands.
|
|
34
|
+
|
|
35
|
+
This is what shapes the choose-your-own-adventure. A candidate beat is only reachable if everything it requires is already grounded; picking a beat that grounds concept X unlocks every beat that was waiting on X. When you offer next beats, they must all be reachable from the current grounded set, and say what each one grounds, so the user can see which paths it opens.
|
|
36
|
+
|
|
37
|
+
The big lever is what you make a prerequisite versus what you ground inside the piece. Demand too much up front and you shut out readers who don't have it; ground too much inside and the early beats drown in definitions. Settle this with the user when you establish prerequisites, and revisit it whenever a tempting beat turns out to require a concept nothing has grounded yet: the fix is either a grounding beat before it, or promoting the concept to a prerequisite.
|
|
38
|
+
|
|
39
|
+
## What is a beat
|
|
40
|
+
|
|
41
|
+
A beat is one move in the journey. It does one thing: sets a scene, lands a point, asks a question, drops an aside, twists the angle. Then it stops, leaving the reader at a place where the next beat can pivot.
|
|
42
|
+
|
|
43
|
+
A beat is sized by what it needs:
|
|
44
|
+
|
|
45
|
+
- A single sentence if that's all the move is ("And then nothing happened for three weeks.").
|
|
46
|
+
- A short paragraph if the move needs setup.
|
|
47
|
+
- Multiple paragraphs if the beat is a self-contained vignette, argument, or example.
|
|
48
|
+
|
|
49
|
+
If a "beat" needs five paragraphs and three subheadings, it's not a beat; it's two beats glued together. Split it.
|
|
50
|
+
|
|
51
|
+
## Pulling from the pile
|
|
52
|
+
|
|
53
|
+
Pull material from the raw pile to populate each beat. You can paraphrase, split, recombine, or quote. The pile is a quarry.
|
|
54
|
+
|
|
55
|
+
## Ending the journey
|
|
56
|
+
|
|
57
|
+
The article ends when the journey is complete, not when the pile is empty. Most piles will have leftover fragments that don't make it in. That is fine; that is the point of having more raw material than you need.
|
|
58
|
+
|
|
59
|
+
## Writing rhythm
|
|
60
|
+
|
|
61
|
+
- Append one beat at a time. Never write ahead.
|
|
62
|
+
- Re-read the article file from disk before every write. Preserve user edits absolutely.
|
|
63
|
+
- If the user edits a previous beat substantially, let it change what comes next.
|
|
64
|
+
- If the user says "rewrite that beat" or "go back and try a different beat 3", do it: edit in place, leave the rest alone.
|
|
65
|
+
|
|
66
|
+
</supporting-info>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
interface:
|
|
2
|
-
display_name: "Writing Beats"
|
|
3
|
-
short_description: "Assemble raw material into beats"
|
|
4
|
-
policy:
|
|
5
|
-
allow_implicit_invocation: true
|
|
1
|
+
interface:
|
|
2
|
+
display_name: "Writing Beats"
|
|
3
|
+
short_description: "Assemble raw material into beats"
|
|
4
|
+
policy:
|
|
5
|
+
allow_implicit_invocation: true
|