@chengzhao-dev/dsh-quarto-book 0.4.0 → 0.4.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.
@@ -1,249 +0,0 @@
1
- // 技能文件的自检:路径、旧布局残留、已收口的冲突句、标点与树注释的落点。
2
- // 用法:node scripts/checking-skill-links.mjs
3
- import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs';
4
- import { join, dirname, resolve, relative, sep } from 'node:path';
5
- import { fileURLToPath } from 'node:url';
6
-
7
- const repo = resolve(dirname(fileURLToPath(import.meta.url)), '..');
8
- const skill = join(repo, 'skills', 'writing-quarto');
9
- const problems = [];
10
- const fail = (file, message) => problems.push(`${relative(repo, file)}: ${message}`);
11
-
12
- const expected = [
13
- 'SKILL.md',
14
- ...['01-task', '02-structure', '03-blocks', '04-paragraphs', '05-sentences', '06-wording', '07-layout', '08-reread', '09-check'].map((n) => `steps/${n}.md`),
15
- ...['01-language-scope', '02-structure-shape', '03-block-after', '03-excerpt-lead', '03-layout-list', '03-qmd-elements', '04-paragraph-cases', '04-paragraph-opening', '05-sentence-brevity', '05-sentence-frame', '05-sentence-reference', '06-wording-cases', '08-appendix'].map((n) => `examples/${n}.md`),
16
- ...['authoring', 'basics', 'coords', 'mermaid', 'refactor', 'render', 'terminal'].map((n) => `quarto/${n}.md`),
17
- ...['answering-disclosure.lua', 'checking-callouts.py', 'checking-dom.py', 'checking-links.py', 'checking-names.py', 'checking-output.py', 'testing-check-ascii-names.py', 'testing-content.py', 'verifying-content.py'].map((n) => `scripts/${n}`),
18
- ];
19
-
20
- const theme = join(repo, 'skills', 'designing-theme');
21
- const themeExpected = ['SKILL.md', 'scripts/checking-layout.py', 'scripts/measuring-pages.mjs', 'references/theme-system.md', 'knowledge/reading-surface.md'];
22
-
23
- const gone = ['references', 'conventions', 'knowledge', 'writer-spine.md', 'writing-spine.md', 'task-routes.md', 'INDEX.md'];
24
-
25
- for (const file of expected) {
26
- if (!existsSync(join(skill, file))) problems.push(`缺少 skills/writing-quarto/${file}`);
27
- }
28
- for (const file of themeExpected) {
29
- if (!existsSync(join(theme, file))) problems.push(`缺少 skills/designing-theme/${file}`);
30
- }
31
- for (const name of gone) {
32
- if (existsSync(join(skill, name))) problems.push(`旧路径仍在 skills/writing-quarto/${name}`);
33
- }
34
-
35
- function walk(dir, out = []) {
36
- for (const entry of readdirSync(dir)) {
37
- const path = join(dir, entry);
38
- if (statSync(path).isDirectory()) walk(path, out);
39
- else if (/\.(md|py|lua|ps1)$/.test(path)) out.push(path);
40
- }
41
- return out;
42
- }
43
-
44
- // 装机脚本也读技能目录:它引用旧布局时同样会断,必须一起扫。
45
- const watched = [...walk(skill), ...walk(join(repo, 'templates')), ...walk(join(repo, 'scripts')).filter((p) => p.endsWith('.ps1'))].filter((p) => !p.includes(`${join('scripts', 'diagrams')}`) || p.endsWith('.md'));
46
-
47
- const conflicts = ['不另立第二套', '标记方式统一见', '全角冒号判失败', '注释是名词', '不用全角冒号挂解释'];
48
- const oldTrees = [/\bconventions\//, /\breferences\/zh\//, /\bknowledge\/cases\//, /\bkb_id\b/, /\bwriting-quarto[/\\](?:conventions|knowledge|references)\b/];
49
- // 检出的工作树可能是 CRLF(Windows + autocrlf),先归一化再解析,
50
- // 否则行尾的 \r 会让注释匹配和标题匹配静默失配。
51
- const read = (file) => readFileSync(file, 'utf8').split('\r\n').join('\n');
52
-
53
-
54
- for (const file of watched) {
55
- const text = read(file);
56
- const rel = relative(skill, file).split(sep).join('/');
57
-
58
- for (const phrase of conflicts) {
59
- if (text.includes(phrase)) fail(file, `已收口的冲突句仍在:${phrase}`);
60
- }
61
- for (const pattern of oldTrees) {
62
- const hit = pattern.exec(text);
63
- if (!hit) continue;
64
- const line = text.slice(0, hit.index).split('\n').length;
65
- const lineText = text.split('\n')[line - 1] ?? '';
66
- if (lineText.includes('designing-theme/')) continue;
67
- fail(file, `旧布局路径仍在第 ${line} 行:${hit[0]}`);
68
- }
69
- // 条文只留一处:其他地方可以画树,但不许再写一套标记规则。
70
- if (rel !== 'steps/03-blocks.md' && /tree -a|目录注释以|文件注释以|标记方式统一见|目录用 `#\/`/.test(text)) {
71
- fail(file, '目录树的标记规则只允许写在 steps/03-blocks.md');
72
- }
73
- if (text.includes('引导词**:完整句') && rel !== 'steps/07-layout.md') {
74
- fail(file, '列表项格式只允许写在 steps/07-layout.md');
75
- }
76
- }
77
-
78
- // 反引号里指向技能或仓库的路径必须存在;书仓库自己的路径不在这里核对。
79
- const own = /^(steps|examples|quarto|scripts|templates|docs|lib|locale)\//;
80
- for (const file of watched) {
81
- if (!file.endsWith('.md')) continue;
82
- const text = read(file);
83
- for (const match of text.matchAll(/`([^`\n]+)`/g)) {
84
- const token = match[1].trim();
85
- if (!own.test(token) || /[\s*{}<>]/.test(token)) continue;
86
- if (!existsSync(resolve(skill, token)) && !existsSync(resolve(repo, token))) {
87
- fail(file, `路径不存在:${token}`);
88
- }
89
- }
90
- }
91
-
92
- // 「`steps/03-blocks.md`「情况:目录树」」这类引用,节名必须真在目标文件里。
93
- // 路径能解析而节名不存在,读者打开文件找不到那句话,等于断链。
94
- function headings(file) {
95
- const titles = new Set();
96
- let fence = false;
97
- for (const line of read(file).split('\n')) {
98
- if (/^\s*(```|~~~)/.test(line)) { fence = !fence; continue; }
99
- if (fence) continue;
100
- const match = /^#{1,4}\s+(.+?)\s*#*\s*$/.exec(line);
101
- if (match) titles.add(match[1].trim());
102
- }
103
- return titles;
104
- }
105
-
106
- const headingCache = new Map();
107
- for (const file of watched) {
108
- if (!file.endsWith('.md')) continue;
109
- const text = read(file);
110
- // 节名本身可能带引号(如「结构助词「的」」),所以内层要允许一对嵌套引号。
111
- for (const match of text.matchAll(/`((?:steps|examples|quarto)\/[^`\s]+\.md|SKILL\.md)`「([^「」]*(?:「[^「」]*」[^「」]*)*)」/g)) {
112
- const [, target, section] = match;
113
- const targetFile = resolve(skill, target);
114
- if (!existsSync(targetFile)) continue;
115
- if (!headingCache.has(targetFile)) headingCache.set(targetFile, headings(targetFile));
116
- if (headingCache.get(targetFile).has(section)) continue;
117
- const line = text.slice(0, match.index).split('\n').length;
118
- fail(file, `第 ${line} 行引用的节名不存在:${target}「${section}」`);
119
- }
120
- }
121
-
122
- // 每一步的文件名编号、H1 编号与 SKILL.md 的九步清单必须对齐:重排或复制步骤时最容易漏掉这里。
123
- const stepSlugs = ['01-task', '02-structure', '03-blocks', '04-paragraphs', '05-sentences', '06-wording', '07-layout', '08-reread', '09-check'];
124
- for (const [index, slug] of stepSlugs.entries()) {
125
- const file = join(skill, 'steps', `${slug}.md`);
126
- const heading = read(file).split(/\r?\n/).find((line) => line.startsWith('# ')) ?? '';
127
- const number = index + 1;
128
- if (!heading.startsWith(`# 第 ${number} 步`)) fail(file, `H1 应是「# 第 ${number} 步…」,实际是「${heading}」`);
129
- }
130
- const skillText = read(join(skill, 'SKILL.md'));
131
- for (const slug of stepSlugs) {
132
- if (!skillText.includes(`steps/${slug}.md`)) fail(join(skill, 'SKILL.md'), `九步清单没点到 steps/${slug}.md`);
133
- }
134
-
135
- // 目录树注释只有「名词」与「名词:一句」两档,同一棵树不混用。
136
- function treeBlocks(text) {
137
- const blocks = [];
138
- let current = null;
139
- let fence = false;
140
- for (const line of text.split('\n')) {
141
- if (/^\s*(```|~~~)/.test(line)) {
142
- if (current) { blocks.push(current); current = null; }
143
- fence = !fence;
144
- continue;
145
- }
146
- if (!fence) continue;
147
- if (/[├└]──/.test(line)) {
148
- current ??= [];
149
- const comment = /(?:#\/|\s#)\s?(.*)$/.exec(line);
150
- if (comment) current.push(comment[1].trim());
151
- } else if (current) {
152
- blocks.push(current);
153
- current = null;
154
- }
155
- }
156
- if (current) blocks.push(current);
157
- return blocks;
158
- }
159
-
160
- for (const file of [join(repo, 'README.md'), join(repo, 'README.en.md'), ...walk(join(repo, 'docs')).filter((p) => p.endsWith('.md'))]) {
161
- const text = read(file);
162
- for (const block of treeBlocks(text)) {
163
- const styles = new Set();
164
- for (const comment of block) {
165
- const colons = comment.split(':').length - 1;
166
- if (colons > 1) fail(file, `树注释有第二个冒号:${comment}`);
167
- if (/[。;]/.test(comment)) fail(file, `树注释不止一句:${comment}`);
168
- if (colons === 1 && comment.split(':')[1].trim() === '') fail(file, `树注释冒号后为空:${comment}`);
169
- styles.add(colons === 0 ? 'noun' : 'clause');
170
- }
171
- if (styles.size > 1) fail(file, `同一棵树混用了两档注释:${block.join(' / ')}`);
172
- }
173
- }
174
-
175
- // 目录 `#/`、文件 `#` 且右移 4 列:列错一位读者就看不出哪个是目录。
176
- // 只查整棵树(README、docs)与案例里标成「改写」的围栏;「不佳」保留同一作对照。
177
- function columnFences(file, text) {
178
- const fences = [];
179
- let current = null;
180
- let fence = false;
181
- let first = '';
182
- for (const line of text.split('\n')) {
183
- if (/^\s*(```|~~~)/.test(line)) {
184
- if (fence) { fences.push({ first, lines: current ?? [] }); current = null; }
185
- fence = !fence;
186
- first = '';
187
- continue;
188
- }
189
- if (!fence) continue;
190
- if (!first && line.trim()) first = line.trim();
191
- current ??= [];
192
- current.push(line);
193
- }
194
- return fences
195
- .filter(({ lines }) => lines.some((line) => /[├└]──/.test(line)))
196
- .filter(({ first }) => {
197
- if (file.startsWith(join(repo, 'skills'))) return first.startsWith('改写(');
198
- return true;
199
- });
200
- }
201
-
202
- for (const file of [
203
- join(repo, 'README.md'),
204
- join(repo, 'README.en.md'),
205
- ...walk(join(repo, 'docs')).filter((p) => p.endsWith('.md')),
206
- ...walk(join(skill, 'examples')),
207
- ]) {
208
- for (const { lines } of columnFences(file, read(file))) {
209
- const tree = [];
210
- for (const [index, line] of lines.entries()) {
211
- if (!/[├└]──/.test(line)) continue;
212
- const depth = (/^(?:│ | )*/.exec(line)?.[0].length ?? 0) / 4;
213
- const marker = /( +)(#\/?) /.exec(line);
214
- if (!marker) { fail(file, `树行没有行尾注释:${line.trim()}`); continue; }
215
- const column = marker.index + marker[1].length;
216
- // 目录不带结尾斜杠,只能看后面还有没有更深的节点。
217
- let hasChild = false;
218
- for (const next of lines.slice(index + 1)) {
219
- if (!/[├└]──/.test(next)) continue;
220
- const nextDepth = (/^(?:│ | )*/.exec(next)?.[0].length ?? 0) / 4;
221
- if (nextDepth <= depth) break;
222
- hasChild = true;
223
- break;
224
- }
225
- tree.push({ column, line, marker: marker[2], isDir: hasChild || marker[2] === '#/' });
226
- }
227
- const dirs = tree.filter((entry) => entry.isDir);
228
- const files = tree.filter((entry) => !entry.isDir);
229
- for (const entry of tree) {
230
- if (entry.isDir && entry.marker !== '#/') fail(file, `目录行注释缺 #/:${entry.line.trim()}`);
231
- if (!entry.isDir && entry.marker === '#/') fail(file, `文件行不应写 #/:${entry.line.trim()}`);
232
- }
233
- if (!dirs.length || !files.length) continue;
234
- const dirColumn = dirs[0].column;
235
- for (const entry of dirs.slice(1)) {
236
- if (entry.column !== dirColumn) fail(file, `目录行 #/ 没对齐:${entry.line.trim()}`);
237
- }
238
- for (const entry of files) {
239
- if (entry.column !== dirColumn + 4) fail(file, `文件行 # 未右移 4 列(目录在 ${dirColumn},文件在 ${entry.column}):${entry.line.trim()}`);
240
- }
241
- }
242
- }
243
-
244
- if (problems.length) {
245
- console.error(problems.join('\n'));
246
- console.error(`\n${problems.length} 处待修`);
247
- process.exit(1);
248
- }
249
- console.log(`checked ${watched.length} files; layout, paths and wording are clean`);
@@ -1,113 +0,0 @@
1
- #Requires -Version 7.0
2
- <#
3
- .SYNOPSIS
4
- Copy this bundle's skill trees into another repository's .agents layout.
5
-
6
- .DESCRIPTION
7
- The plugin package and an embedded .agents copy have different jobs. This
8
- script copies only the skill payload, never the bundle manifests or runtime
9
- directories, so the target repository stays a plain Quarto book.
10
-
11
- Layout written:
12
- <target>/.agents/skills/writing-quarto/
13
- <target>/.agents/skills/designing-theme/
14
-
15
- .PARAMETER Target
16
- Repository root that receives the .agents tree.
17
-
18
- .PARAMETER Source
19
- Bundle root. Defaults to the parent of this script.
20
-
21
- .PARAMETER Force
22
- Replace existing destination directories.
23
-
24
- .EXAMPLE
25
- pwsh -File scripts/embedding-agents.ps1 -Target D:\Github\my-book
26
- #>
27
- [CmdletBinding()]
28
- param(
29
- [Parameter(Mandatory = $true)]
30
- [string]$Target,
31
-
32
- [string]$Source = (Split-Path -Parent $PSScriptRoot),
33
-
34
- [switch]$Force
35
- )
36
-
37
- Set-StrictMode -Version Latest
38
- $ErrorActionPreference = 'Stop'
39
-
40
- $forbiddenNames = @('package.json', 'cordis.patch.yml', 'icon.svg')
41
- $forbiddenDirs = @('lib', 'locale')
42
- $excludedDirs = @('__pycache__', '.pytest_cache')
43
-
44
- function Resolve-FullPath {
45
- param([string]$Path, [string]$Label)
46
- if (-not (Test-Path -LiteralPath $Path -PathType Container)) {
47
- throw "$Label does not exist or is not a directory: $Path"
48
- }
49
- return (Resolve-Path -LiteralPath $Path).Path
50
- }
51
-
52
- function Assert-NoBundleFiles {
53
- param([string]$Path, [string]$Label)
54
- foreach ($name in $forbiddenNames) {
55
- if (Test-Path -LiteralPath (Join-Path $Path $name)) {
56
- throw "$Label contains bundle file '$name'; refusing to embed: $Path"
57
- }
58
- }
59
- foreach ($name in $forbiddenDirs) {
60
- if (Test-Path -LiteralPath (Join-Path $Path $name) -PathType Container) {
61
- throw "$Label contains bundle directory '$name/'; refusing to embed: $Path"
62
- }
63
- }
64
- }
65
-
66
- function Assert-SkillSource {
67
- param([string]$Path, [string]$Label)
68
- $skillFile = Join-Path $Path 'SKILL.md'
69
- if (-not (Test-Path -LiteralPath $skillFile -PathType Leaf)) {
70
- throw "$Label has no SKILL.md: $Path"
71
- }
72
- Assert-NoBundleFiles -Path $Path -Label $Label
73
- }
74
-
75
- function Copy-SkillTree {
76
- param([string]$From, [string]$To)
77
-
78
- Assert-SkillSource -Path $From -Label 'source skill'
79
- if (Test-Path -LiteralPath $To) {
80
- if (-not $Force) {
81
- throw "destination exists; pass -Force to replace: $To"
82
- }
83
- Remove-Item -LiteralPath $To -Recurse -Force
84
- }
85
- New-Item -ItemType Directory -Path $To -Force | Out-Null
86
- Copy-Item -Path (Join-Path $From '*') -Destination $To -Recurse -Force
87
-
88
- foreach ($name in $excludedDirs) {
89
- Get-ChildItem -LiteralPath $To -Directory -Recurse -Filter $name -ErrorAction SilentlyContinue |
90
- Remove-Item -Recurse -Force
91
- }
92
- Get-ChildItem -LiteralPath $To -File -Recurse -Include '*.pyc', '*.pyo' -ErrorAction SilentlyContinue |
93
- Remove-Item -Force
94
-
95
- Assert-SkillSource -Path $To -Label 'copied skill'
96
- }
97
-
98
- $sourceRoot = Resolve-FullPath -Path $Source -Label 'source'
99
- $targetRoot = Resolve-FullPath -Path $Target -Label 'target'
100
-
101
- $writing = Join-Path $sourceRoot 'skills/writing-quarto'
102
- $theme = Join-Path $sourceRoot 'skills/designing-theme'
103
-
104
- $writingDestination = Join-Path $targetRoot '.agents/skills/writing-quarto'
105
- $themeDestination = Join-Path $targetRoot '.agents/skills/designing-theme'
106
-
107
- Copy-SkillTree -From $writing -To $writingDestination
108
-
109
- Copy-SkillTree -From $theme -To $themeDestination
110
-
111
- Write-Host "Embedded skills into $targetRoot"
112
- Write-Host " writing-quarto -> $writingDestination"
113
- Write-Host " designing-theme -> $themeDestination"