concept-atlas-dense-explain 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -26,9 +26,9 @@ npx concept-atlas-dense-explain validate topic.mdx --mode atlas
26
26
  npx concept-atlas-dense-explain topic.mdx --mode atlas
27
27
  ```
28
28
 
29
- `guide` and `create` refuse to overwrite an existing file unless `--force` is
30
- passed. Use `-o` to choose the output path (a directory when passing several
31
- inputs). Validation errors abort the build; `--no-validate` forces a knowingly
29
+ Existing outputs are overwritten; `--force` is accepted for compatibility and is
30
+ a no-op. `--mode` defaults to `scroll`. Use `-o` to choose the output path (a
31
+ directory when passing several inputs). Validation errors abort the build; `--no-validate` forces a knowingly
32
32
  broken build.
33
33
 
34
34
  Mermaid loads from a CDN at runtime by default (fast builds, needs network);
package/bin/cli.mjs CHANGED
@@ -1,11 +1,31 @@
1
1
  #!/usr/bin/env node
2
- import { access, constants, copyFile, cp, mkdir, readFile, rm, rename, writeFile } from 'node:fs/promises';
2
+ import {
3
+ access,
4
+ constants,
5
+ copyFile,
6
+ cp,
7
+ mkdir,
8
+ readFile,
9
+ rm,
10
+ rename,
11
+ writeFile
12
+ } from 'node:fs/promises';
3
13
  import { existsSync, statSync } from 'node:fs';
4
14
  import path from 'node:path';
5
15
  import { build } from 'vite';
6
16
  import { fileURLToPath } from 'node:url';
7
- import { validateMdxSource, countBySeverity, detectFeatures, extractPageTitle } from '../template/src/model/validate-content.js';
8
- import { SKINS, normalizeSkin, COMPONENT_STYLES, normalizeStyle } from '../template/src/model/skins.js';
17
+ import {
18
+ validateMdxSource,
19
+ countBySeverity,
20
+ detectFeatures,
21
+ extractPageTitle
22
+ } from '../template/src/model/validate-content.js';
23
+ import {
24
+ SKINS,
25
+ normalizeSkin,
26
+ COMPONENT_STYLES,
27
+ normalizeStyle
28
+ } from '../template/src/model/skins.js';
9
29
 
10
30
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
11
31
  const templateRoot = path.join(packageRoot, 'template');
@@ -14,26 +34,50 @@ let buildCounter = 0;
14
34
 
15
35
  function usage() {
16
36
  console.log('Usage:');
17
- console.log(' npx concept-atlas-dense-explain <input.mdx>... [--mode atlas|scroll] [--skin <id>] [--default-mode dark|light|system] [--style <id>] [-o output.html|dir] [--force] [--concurrency N] [--inline-assets] [--inline-mermaid] [--mermaid-cdn <url>] [--json] [--no-validate]');
37
+ console.log(
38
+ ' npx concept-atlas-dense-explain <input.mdx>... [--mode atlas|scroll] [--skin <id>] [--default-mode dark|light|system] [--style <id>] [-o output.html|dir] [--concurrency N] [--inline-assets] [--inline-mermaid] [--mermaid-cdn <url>] [--json] [--no-validate]'
39
+ );
18
40
  console.log(' npx concept-atlas-dense-explain render <input.mdx>... [-o output.html|dir]');
19
- console.log(' npx concept-atlas-dense-explain validate <input.mdx> [--mode atlas|scroll] [--strict] [--json]');
20
- console.log(' npx concept-atlas-dense-explain create <output.mdx> [--mode atlas|scroll] [--force]');
21
- console.log(' npx concept-atlas-dense-explain guide [--mode atlas|scroll] [-o output.mdx] [--force]');
41
+ console.log(
42
+ ' npx concept-atlas-dense-explain validate <input.mdx> [--mode atlas|scroll] [--strict] [--json]'
43
+ );
44
+ console.log(' npx concept-atlas-dense-explain create <output.mdx> [--mode atlas|scroll]');
45
+ console.log(' npx concept-atlas-dense-explain guide [--mode atlas|scroll] [-o output.mdx]');
22
46
  console.log('');
47
+ console.log(
48
+ ' --mode defaults to scroll. Existing outputs are overwritten; --force is accepted for compatibility and is a no-op.'
49
+ );
23
50
  console.log(' --help shows this text; --version prints the package version.');
24
- console.log(' Multiple inputs build in parallel (default 2 at a time, cap 4); -o is then a directory.');
25
- console.log(' Figures are kept as relative links by default (small HTML; ship the assets/ dir beside it). --inline-assets bakes every local image into the HTML as base64 instead; a per-tag inline={true|false} on <Figure> overrides that for one image. --link-assets is kept as an explicit alias for the default.');
26
- console.log(' Mermaid diagrams load from a CDN at runtime by default (fast builds, needs network); --inline-mermaid bakes Mermaid into the HTML for a fully offline single file; --mermaid-cdn overrides the CDN URL.');
27
- console.log(` --skin bakes a default palette (${SKINS.map(skin => skin.id).join(', ')}); --default-mode bakes a default dark/light mode; --style bakes a default component style (${COMPONENT_STYLES.map(style => style.id).join(', ')}). Readers can still switch in the UI.`);
51
+ console.log(
52
+ ' Multiple inputs build in parallel (default 2 at a time, cap 4); -o is then a directory.'
53
+ );
54
+ console.log(
55
+ ' Figures are kept as relative links by default (small HTML; ship the assets/ dir beside it). --inline-assets bakes every local image into the HTML as base64 instead; a per-tag inline={true|false} on <Figure> overrides that for one image. --link-assets is kept as an explicit alias for the default.'
56
+ );
57
+ console.log(
58
+ ' Mermaid diagrams load from a CDN at runtime by default (fast builds, needs network); --inline-mermaid bakes Mermaid into the HTML for a fully offline single file; --mermaid-cdn overrides the CDN URL.'
59
+ );
60
+ console.log(
61
+ ` --skin bakes a default palette (${SKINS.map(skin => skin.id).join(', ')}); --default-mode bakes a default dark/light mode; --style bakes a default component style (${COMPONENT_STYLES.map(style => style.id).join(', ')}). Readers can still switch in the UI.`
62
+ );
28
63
  }
29
64
 
30
65
  async function exists(filePath) {
31
- try { await access(filePath, constants.F_OK); return true; } catch { return false; }
66
+ try {
67
+ await access(filePath, constants.F_OK);
68
+ return true;
69
+ } catch {
70
+ return false;
71
+ }
32
72
  }
33
73
 
34
74
  /** Byte size of an asset, or null when it does not exist. */
35
75
  function assetByteSize(filePath) {
36
- try { return statSync(filePath).size; } catch { return null; }
76
+ try {
77
+ return statSync(filePath).size;
78
+ } catch {
79
+ return null;
80
+ }
37
81
  }
38
82
 
39
83
  function flagValue(flags, names) {
@@ -50,8 +94,29 @@ function fail(message) {
50
94
  process.exit(1);
51
95
  }
52
96
 
53
- const VALUE_FLAGS = new Set(['--mode', '-o', '--output', '--concurrency', '--skin', '--default-mode', '--style', '--mermaid-cdn']);
54
- const BOOLEAN_FLAGS = new Set(['--force', '--json', '--strict', '--no-validate', '--link-assets', '--inline-assets', '--inline-mermaid', '--help', '-h', '--version', '-v']);
97
+ const VALUE_FLAGS = new Set([
98
+ '--mode',
99
+ '-o',
100
+ '--output',
101
+ '--concurrency',
102
+ '--skin',
103
+ '--default-mode',
104
+ '--style',
105
+ '--mermaid-cdn'
106
+ ]);
107
+ const BOOLEAN_FLAGS = new Set([
108
+ '--force',
109
+ '--json',
110
+ '--strict',
111
+ '--no-validate',
112
+ '--link-assets',
113
+ '--inline-assets',
114
+ '--inline-mermaid',
115
+ '--help',
116
+ '-h',
117
+ '--version',
118
+ '-v'
119
+ ]);
55
120
  const COMMAND_NAMES = ['help', 'create', 'new', 'render', 'validate', 'guide'];
56
121
 
57
122
  /** Splits argv into flags, flag values and positional arguments. */
@@ -82,7 +147,10 @@ function extractCommand(argv) {
82
147
  const rest = [...argv];
83
148
  for (let i = 0; i < rest.length; i += 1) {
84
149
  const arg = rest[i];
85
- if (VALUE_FLAGS.has(arg)) { i += 1; continue; }
150
+ if (VALUE_FLAGS.has(arg)) {
151
+ i += 1;
152
+ continue;
153
+ }
86
154
  if (arg.startsWith('-') && arg.length > 1) continue;
87
155
  if (COMMAND_NAMES.includes(arg)) {
88
156
  rest.splice(i, 1);
@@ -105,7 +173,9 @@ function resolveOutputs(inputs, explicit) {
105
173
  if (path.extname(target).toLowerCase() === '.html') {
106
174
  fail('`-o` must be a directory when building more than one input.');
107
175
  }
108
- return inputs.map(input => path.join(target, `${path.basename(input, path.extname(input))}.html`));
176
+ return inputs.map(input =>
177
+ path.join(target, `${path.basename(input, path.extname(input))}.html`)
178
+ );
109
179
  }
110
180
 
111
181
  function printDiagnostics(source, options, { json, label = null, quiet = false }) {
@@ -123,7 +193,9 @@ function printDiagnostics(source, options, { json, label = null, quiet = false }
123
193
  console.error(`${severity} ${where} ${item.code} ${item.message}`);
124
194
  }
125
195
  const { error, warning } = countBySeverity(diagnostics);
126
- const scope = carrier ? `${carrier} · ${stats.nodes} 节点 / ${stats.relations} 关系` : '未识别载体';
196
+ const scope = carrier
197
+ ? `${carrier} · ${stats.nodes} 节点 / ${stats.relations} 关系`
198
+ : '未识别载体';
127
199
  if (error) console.error(`校验失败:${error} 个错误,${warning} 个警告(${scope})`);
128
200
  else if (warning) console.error(`校验通过:${warning} 个警告(${scope})`);
129
201
  else console.error(`校验通过:无问题(${scope})`);
@@ -133,7 +205,10 @@ function printDiagnostics(source, options, { json, label = null, quiet = false }
133
205
  const { command, rest } = extractCommand(args);
134
206
  args = rest;
135
207
 
136
- if (args.includes('--help') || args.includes('-h')) { usage(); process.exit(0); }
208
+ if (args.includes('--help') || args.includes('-h')) {
209
+ usage();
210
+ process.exit(0);
211
+ }
137
212
  if (args.includes('--version') || args.includes('-v')) {
138
213
  const pkg = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
139
214
  console.log(pkg.version);
@@ -155,16 +230,17 @@ for (let i = 0; i < args.length; i += 1) {
155
230
  }
156
231
  }
157
232
 
158
- if (command === 'help') { usage(); process.exit(0); }
233
+ if (command === 'help') {
234
+ usage();
235
+ process.exit(0);
236
+ }
159
237
 
160
238
  if (command === 'guide') {
161
- const mode = flagValue(args, ['--mode']) || 'atlas';
239
+ const mode = flagValue(args, ['--mode']) || 'scroll';
162
240
  if (!['atlas', 'scroll'].includes(mode)) fail(`Unknown mode: ${mode}`);
163
- const output = path.resolve(flagValue(args, ['-o', '--output']) || `concept-atlas-${mode}-guide.mdx`);
164
- if (await exists(output) && !args.includes('--force')) {
165
- console.error(`Refusing to overwrite ${output}; pass --force to replace it.`);
166
- process.exit(1);
167
- }
241
+ const output = path.resolve(
242
+ flagValue(args, ['-o', '--output']) || `concept-atlas-${mode}-guide.mdx`
243
+ );
168
244
  const source = path.join(templateRoot, 'references', `${mode}-guide.mdx`);
169
245
  if (!(await exists(source))) {
170
246
  console.error(`Guide for mode "${mode}" is missing from the package.`);
@@ -174,7 +250,7 @@ if (command === 'guide') {
174
250
  await copyFile(source, output);
175
251
  const assetsSource = path.join(templateRoot, 'references', 'assets');
176
252
  const assetsTarget = path.join(path.dirname(output), 'assets');
177
- if (await exists(assetsSource) && path.resolve(assetsSource) !== path.resolve(assetsTarget) && (args.includes('--force') || !(await exists(assetsTarget)))) {
253
+ if ((await exists(assetsSource)) && path.resolve(assetsSource) !== path.resolve(assetsTarget)) {
178
254
  await cp(assetsSource, assetsTarget, { recursive: true, force: true });
179
255
  console.log(`Copied guide assets to ${assetsTarget}`);
180
256
  }
@@ -185,17 +261,19 @@ if (command === 'guide') {
185
261
 
186
262
  if (command === 'create' || command === 'new') {
187
263
  const output = args[0] ? path.resolve(args[0]) : null;
188
- const mode = flagValue(args, ['--mode']) || 'atlas';
189
- if (!output || path.extname(output).toLowerCase() !== '.mdx' || !['atlas', 'scroll'].includes(mode)) {
264
+ const mode = flagValue(args, ['--mode']) || 'scroll';
265
+ if (
266
+ !output ||
267
+ path.extname(output).toLowerCase() !== '.mdx' ||
268
+ !['atlas', 'scroll'].includes(mode)
269
+ ) {
190
270
  usage();
191
271
  process.exit(1);
192
272
  }
193
- if (await exists(output) && !args.includes('--force')) {
194
- console.error(`Refusing to overwrite ${output}; pass --force to replace it.`);
195
- process.exit(1);
196
- }
197
273
  await mkdir(path.dirname(output), { recursive: true });
198
- const template = mode === 'atlas' ? `\
274
+ const template =
275
+ mode === 'atlas'
276
+ ? `\
199
277
  {/* shell 的 title 会成为浏览器标签页标题;页面图标固定为 📃。请把“主题名称”改成真实标题。 */}
200
278
  <ExplainPage id="topic-id" title="主题名称" summary="用一句话说明这个主题解决什么问题。">
201
279
  <ConceptGraph root="root-node">
@@ -221,7 +299,8 @@ if (command === 'create' || command === 'new') {
221
299
  <Relation from="first-branch" to="second-branch" type="depends-on" label="依赖" />
222
300
  </ConceptGraph>
223
301
  </ExplainPage>
224
- ` : `\
302
+ `
303
+ : `\
225
304
  {/* shell 的 title 会成为浏览器标签页标题;页面图标固定为 📃。请把“主题名称”改成真实标题。 */}
226
305
  <ScrollDocument>
227
306
  <ScrollHeader title="主题名称">用一两句话说明主题、背景和读者应该带走的判断。</ScrollHeader>
@@ -247,7 +326,9 @@ if (command === 'create' || command === 'new') {
247
326
  `;
248
327
  await writeFile(output, template, 'utf8');
249
328
  console.log(`Created ${mode} MDX template: ${output}`);
250
- console.log(`Tip: run "npx concept-atlas-dense-explain guide --mode ${mode}" for a full component reference.`);
329
+ console.log(
330
+ `Tip: run "npx concept-atlas-dense-explain guide --mode ${mode}" for a full component reference.`
331
+ );
251
332
  process.exit(0);
252
333
  }
253
334
 
@@ -255,7 +336,6 @@ const parsed = parseFlags(args);
255
336
  const json = parsed.flags.has('--json');
256
337
  const strict = parsed.flags.has('--strict');
257
338
  const skipValidate = parsed.flags.has('--no-validate');
258
- const force = parsed.flags.has('--force');
259
339
  const linkAssets = parsed.flags.has('--link-assets');
260
340
  const inlineAssets = parsed.flags.has('--inline-assets');
261
341
  if (linkAssets && inlineAssets) {
@@ -270,18 +350,25 @@ const modeFlag = parsed.values.get('--mode') || null;
270
350
  let skinFlag = null;
271
351
  if (parsed.values.has('--skin')) {
272
352
  skinFlag = normalizeSkin(parsed.values.get('--skin'));
273
- if (!skinFlag) fail(`Unknown skin: ${parsed.values.get('--skin')} (available: ${SKINS.map(skin => skin.id).join(', ')})`);
353
+ if (!skinFlag)
354
+ fail(
355
+ `Unknown skin: ${parsed.values.get('--skin')} (available: ${SKINS.map(skin => skin.id).join(', ')})`
356
+ );
274
357
  }
275
358
  let defaultModeFlag = null;
276
359
  if (parsed.values.has('--default-mode')) {
277
360
  const raw = parsed.values.get('--default-mode');
278
- if (!['dark', 'light', 'system'].includes(raw)) fail(`Invalid --default-mode: ${raw} (use dark, light or system)`);
361
+ if (!['dark', 'light', 'system'].includes(raw))
362
+ fail(`Invalid --default-mode: ${raw} (use dark, light or system)`);
279
363
  defaultModeFlag = raw;
280
364
  }
281
365
  let styleFlag = null;
282
366
  if (parsed.values.has('--style')) {
283
367
  styleFlag = normalizeStyle(parsed.values.get('--style'));
284
- if (!styleFlag) fail(`Unknown component style: ${parsed.values.get('--style')} (available: ${COMPONENT_STYLES.map(style => style.id).join(', ')})`);
368
+ if (!styleFlag)
369
+ fail(
370
+ `Unknown component style: ${parsed.values.get('--style')} (available: ${COMPONENT_STYLES.map(style => style.id).join(', ')})`
371
+ );
285
372
  }
286
373
 
287
374
  if (command === 'validate') {
@@ -290,21 +377,26 @@ if (command === 'validate') {
290
377
  fail('Provide an existing .mdx file to validate.');
291
378
  }
292
379
  const source = await readFile(target, 'utf8');
293
- const result = printDiagnostics(source, {
294
- filePath: target,
295
- mode: modeFlag,
296
- strict,
297
- inlineAssets,
298
- assetExists: spec => existsSync(path.resolve(path.dirname(target), spec)),
299
- assetSize: spec => assetByteSize(path.resolve(path.dirname(target), spec)),
300
- }, { json });
380
+ const result = printDiagnostics(
381
+ source,
382
+ {
383
+ filePath: target,
384
+ mode: modeFlag,
385
+ strict,
386
+ inlineAssets,
387
+ assetExists: spec => existsSync(path.resolve(path.dirname(target), spec)),
388
+ assetSize: spec => assetByteSize(path.resolve(path.dirname(target), spec))
389
+ },
390
+ { json }
391
+ );
301
392
  process.exit(countBySeverity(result.diagnostics).error ? 1 : 0);
302
393
  }
303
394
 
304
395
  // `render` is the default command, so it may still appear as a leading token.
305
- const positional = parsed.positional[0] && parsed.positional[0].toLowerCase() === 'render'
306
- ? parsed.positional.slice(1)
307
- : parsed.positional;
396
+ const positional =
397
+ parsed.positional[0] && parsed.positional[0].toLowerCase() === 'render'
398
+ ? parsed.positional.slice(1)
399
+ : parsed.positional;
308
400
  const inputs = positional.map(entry => path.resolve(entry));
309
401
 
310
402
  if (!inputs.length) {
@@ -321,28 +413,25 @@ for (const input of inputs) {
321
413
 
322
414
  const outputs = resolveOutputs(inputs, parsed.values.get('-o') || parsed.values.get('--output'));
323
415
 
324
- for (const output of outputs) {
325
- if (await exists(output) && !force) {
326
- console.error(`Refusing to overwrite ${output}; pass --force to replace it.`);
327
- process.exit(1);
328
- }
329
- }
330
-
331
416
  const multi = inputs.length > 1;
332
417
  const sources = await Promise.all(inputs.map(input => readFile(input, 'utf8')));
333
418
 
334
419
  // Validate every document before building any of them: a batch should fail as a
335
420
  // batch rather than leaving half the targets rendered.
336
- const validations = sources.map((source, index) => printDiagnostics(source, {
337
- filePath: inputs[index],
338
- mode: modeFlag,
339
- strict,
340
- inlineAssets,
341
- assetExists: spec => existsSync(path.resolve(path.dirname(inputs[index]), spec)),
342
- assetSize: spec => assetByteSize(path.resolve(path.dirname(inputs[index]), spec)),
343
- }, json
344
- ? { json: false, quiet: true }
345
- : { json: false, label: multi ? inputs[index] : null }));
421
+ const validations = sources.map((source, index) =>
422
+ printDiagnostics(
423
+ source,
424
+ {
425
+ filePath: inputs[index],
426
+ mode: modeFlag,
427
+ strict,
428
+ inlineAssets,
429
+ assetExists: spec => existsSync(path.resolve(path.dirname(inputs[index]), spec)),
430
+ assetSize: spec => assetByteSize(path.resolve(path.dirname(inputs[index]), spec))
431
+ },
432
+ json ? { json: false, quiet: true } : { json: false, label: multi ? inputs[index] : null }
433
+ )
434
+ );
346
435
 
347
436
  if (json) {
348
437
  const payload = multi
@@ -351,7 +440,10 @@ if (json) {
351
440
  process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
352
441
  }
353
442
 
354
- const errorCount = validations.reduce((sum, result) => sum + countBySeverity(result.diagnostics).error, 0);
443
+ const errorCount = validations.reduce(
444
+ (sum, result) => sum + countBySeverity(result.diagnostics).error,
445
+ 0
446
+ );
355
447
  if (errorCount && !skipValidate) {
356
448
  console.error('内容校验未通过,已停止构建。修复后重试,或用 --no-validate 强制构建。');
357
449
  process.exit(1);
@@ -360,27 +452,47 @@ if (errorCount && !skipValidate) {
360
452
  const jobs = inputs.map((input, index) => {
361
453
  const mode = modeFlag || validations[index].carrier;
362
454
  if (!mode || !['atlas', 'scroll'].includes(mode)) {
363
- console.error(`Could not detect the MDX carrier for ${input}; choose --mode atlas or --mode scroll.`);
455
+ console.error(
456
+ `Could not detect the MDX carrier for ${input}; choose --mode atlas or --mode scroll.`
457
+ );
364
458
  process.exit(1);
365
459
  }
366
460
  // Figures link by default. If the HTML is written outside the MDX's directory
367
461
  // its relative image paths no longer resolve, so warn unless --inline-assets
368
462
  // bakes them in.
369
- if (!inlineAssets && path.resolve(path.dirname(outputs[index])) !== path.resolve(path.dirname(input))) {
370
- console.error(`警告:默认外链图片,但 ${outputs[index]} 不在 ${path.dirname(input)} 内,相对图片路径会失效;请用 --inline-assets,或把 assets/ 一并放到输出目录。`);
463
+ if (
464
+ !inlineAssets &&
465
+ path.resolve(path.dirname(outputs[index])) !== path.resolve(path.dirname(input))
466
+ ) {
467
+ console.error(
468
+ `警告:默认外链图片,但 ${outputs[index]} 不在 ${path.dirname(input)} 内,相对图片路径会失效;请用 --inline-assets,或把 assets/ 一并放到输出目录。`
469
+ );
371
470
  }
372
- return { input, output: outputs[index], mode, title: extractPageTitle(sources[index]), features: detectFeatures(sources[index]), inlineAssets };
471
+ return {
472
+ input,
473
+ output: outputs[index],
474
+ mode,
475
+ title: extractPageTitle(sources[index]),
476
+ features: detectFeatures(sources[index]),
477
+ inlineAssets
478
+ };
373
479
  });
374
480
 
375
481
  const limit = clampConcurrency(parsed.values.get('--concurrency'), jobs.length);
376
482
  const started = Date.now();
377
- const results = await runPool(jobs.map(job => () => buildOne(job)), limit);
483
+ const results = await runPool(
484
+ jobs.map(job => () => buildOne(job)),
485
+ limit
486
+ );
378
487
 
379
488
  const failures = results.filter(result => !result.ok);
380
489
  const saved = summarizeFeatures(jobs, results);
381
- console.log(`Built ${results.length - failures.length}/${results.length} page(s) with concurrency ${limit} in ${((Date.now() - started) / 1000).toFixed(1)}s${saved ? ` (${saved})` : ''}.`);
490
+ console.log(
491
+ `Built ${results.length - failures.length}/${results.length} page(s) with concurrency ${limit} in ${((Date.now() - started) / 1000).toFixed(1)}s${saved ? ` (${saved})` : ''}.`
492
+ );
382
493
  if (failures.length) {
383
- for (const failure of failures) console.error(`Build failed for ${failure.input}:`, failure.error);
494
+ for (const failure of failures)
495
+ console.error(`Build failed for ${failure.input}:`, failure.error);
384
496
  process.exit(1);
385
497
  }
386
498
 
@@ -390,7 +502,10 @@ async function buildOne(job) {
390
502
  // Each build gets its own scratch outDir: the template always writes
391
503
  // `index.html`/`scroll.html`, so concurrent builds sharing a directory would
392
504
  // overwrite each other before the rename.
393
- const scratch = path.join(path.dirname(output), `.concept-atlas-${process.pid}-${(buildCounter += 1)}`);
505
+ const scratch = path.join(
506
+ path.dirname(output),
507
+ `.concept-atlas-${process.pid}-${(buildCounter += 1)}`
508
+ );
394
509
  await mkdir(path.dirname(output), { recursive: true });
395
510
  await mkdir(scratch, { recursive: true });
396
511
  const define = { __ATLAS_FEATURES__: JSON.stringify(features) };
@@ -413,8 +528,8 @@ async function buildOne(job) {
413
528
  build: {
414
529
  outDir: scratch,
415
530
  emptyOutDir: false,
416
- rollupOptions: { input: path.join(templateRoot, templateEntry) },
417
- },
531
+ rollupOptions: { input: path.join(templateRoot, templateEntry) }
532
+ }
418
533
  });
419
534
  // Swap the new build in without a window where no output exists: on
420
535
  // POSIX rename replaces atomically; the fallback path (Windows can refuse
@@ -441,7 +556,9 @@ async function buildOne(job) {
441
556
  }
442
557
  if (hasBackup) await rm(backup, { force: true });
443
558
  }
444
- console.log(`Built ${mode} HTML: ${output}${title ? ` [tab: ${title}]` : ''}${describeFeatures(features)}${inline ? ' [figures inlined]' : ' [figures linked]'}`);
559
+ console.log(
560
+ `Built ${mode} HTML: ${output}${title ? ` [tab: ${title}]` : ''}${describeFeatures(features)}${inline ? ' [figures inlined]' : ' [figures linked]'}`
561
+ );
445
562
  return { ok: true, input, output };
446
563
  } catch (error) {
447
564
  return { ok: false, input, output, error };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "concept-atlas-dense-explain",
3
- "version": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "description": "Portable dense-explanation skill and MDX concept atlas template",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,7 +24,7 @@ This is the only runner — there is nothing to look for. Do not search for a lo
24
24
  - `references/atlas-guide.mdx` (atlas) or `references/scroll-guide.mdx` (scroll)
25
25
  It is real, compilable MDX showing that shell's components and their exact props; search it for a component name instead of guessing, and do not look for component documentation anywhere else. Do not run `guide` for this.
26
26
  Run `npx concept-atlas-dense-explain guide --mode <mode> -o <file>` only when you specifically need a project-local copy to compile beside the page, and delete that copy when done.
27
- 3. Start from a skeleton when useful: `npx concept-atlas-dense-explain create <file>.mdx --mode atlas|scroll`. `create` and `guide` refuse to overwrite an existing file unless `--force` is passed.
27
+ 3. Start from a skeleton when useful: `npx concept-atlas-dense-explain create <file>.mdx --mode atlas|scroll` (`--mode` defaults to `scroll`). `create` and `guide` overwrite their output, so never point them at a file you want to keep.
28
28
  4. Write the semantic MDX into the user's `.mdx` file (see Authoring rules).
29
29
  5. Validate before rendering:
30
30
  ```bash
@@ -34,7 +34,7 @@ This is the only runner — there is nothing to look for. Do not search for a lo
34
34
  Diagnostics are `CODE line:column message`. Fix all `error`s and re-run; address warnings when cheap.
35
35
  6. Compile: `npx concept-atlas-dense-explain <file>.mdx --mode atlas|scroll [-o out.html] [--skin <id>] [--default-mode dark|light|system] [--style <id>] [--mermaid-cdn <url>]`. Output is a standalone HTML beside the MDX unless `-o` is given. Validation errors abort the build (`--no-validate` forces a knowingly broken build). **Mermaid stays on its runtime CDN by default — do not pass `--inline-mermaid` on your own.** Only add `--inline-mermaid` when the user explicitly asks for a fully offline single file, since it bloats the HTML with the whole Mermaid bundle.
36
36
  7. **Appearance (optional)**: pages ship a reader-facing appearance menu — palette (`aurora` indigo, `ember` gold, `verdant` forest, `sakura` pink-plum, `noir` ink), a dark/light toggle, and a component style pack (`manuscript` editorial marginalia, `classic` boxed cards, `shadcn` hairline-bordered minimal UI, `elastic` bordered observability panels). The shipped default is aurora × manuscript × light; choices persist in localStorage across both carriers. Bake different compile-time defaults with `--skin ember --default-mode dark --style classic` (or `CONCEPT_ATLAS_SKIN` / `CONCEPT_ATLAS_DEFAULT_MODE` / `CONCEPT_ATLAS_STYLE` on the repo build); `--default-mode` honors `dark`/`light` and resolves `system` to the carrier default (`light`). Bake a default only when the user asks for one — content MDX never sets appearance.
37
- 8. For several documents, pass them all in one call: `npx concept-atlas-dense-explain a.mdx b.mdx c.mdx -o dist --force [--concurrency 3]` (`-o` is then a directory; everything validates first, then builds in parallel). Builds bundle only the heavy renderers the content uses: no `<Math>` skips KaTeX's ~1.4MB inlined fonts, and Mermaid stays on a CDN. Never add dummy `<Math>`/`<Mermaid>` nodes to "enable" them.
37
+ 8. For several documents, pass them all in one call: `npx concept-atlas-dense-explain a.mdx b.mdx c.mdx -o dist [--concurrency 3]` (`-o` is then a directory; everything validates first, then builds in parallel). Builds bundle only the heavy renderers the content uses: no `<Math>` skips KaTeX's ~1.4MB inlined fonts, and Mermaid stays on a CDN. Never add dummy `<Math>`/`<Mermaid>` nodes to "enable" them.
38
38
  9. Report the shell, output path, validation result (errors/warnings), and limitations. Do not claim interactions you did not verify.
39
39
 
40
40
  ## Carriers
@@ -47,12 +47,12 @@ This is the only runner — there is nothing to look for. Do not search for a lo
47
47
  ## Component families
48
48
 
49
49
  - Node semantics: `Overview`, `Definition`, `Mechanism`, `Implementation`, `CodeBlock`, `Boundary`, `Example`, `Counterexample`, `Prerequisite`, `Input`, `Output`, `Glossary`
50
- - Argument and evidence: `Evidence`, `Invariant`, `FailureMode`, `Tradeoff`, `LearningObjectives`, `KeyQuestion`
51
- - Learning loop and provenance: `WorkedExample` + `Step`, `Quiz`, `KeyTakeaways`, `Source`, `Confidence`, `Term`
50
+ - Argument and evidence: `Evidence`, `Invariant`, `FailureMode`, `Tradeoff`, `Checklist`, `LearningObjectives`, `KeyQuestion`
51
+ - Learning loop and provenance: `WorkedExample` + `Step`, `Quiz`, `KeyTakeaways`, `Source`, `Confidence`, `LastReviewed`, `Term`
52
52
  - Information models: `Flow`, `Timeline`, `Compare`, `DecisionMatrix`, `FrameworkModel`, `MatrixModel`, `FormulaModel`, `PyramidModel`, `FunnelModel`
53
- - Data and behaviour: `DataTable`, `Metric`, `StateMachine`, `DecisionTree`, `FeedbackLoop`, `CodeDiff`
54
- - Reading and layout: `Insight`, `Callout`, `Details`, `NoteGrid`, `Tabs`, `Columns`, `Stack`, `Grid`, `Split`, `ScrollGrid`, `ScrollPair`, `ScrollToc`
55
- - Graphics and extensions: `Mermaid`, `RelationMap`, `RelationPath`, `Math`, `MathBlock`, `Chart`, `Figure` (alias `Image`), `FigureRef`, `Cite`, `References`
53
+ - Data and behaviour: `DataTable`, `Metric`, `PropertyList`, `TreeView`, `StateMachine`, `DecisionTree`, `FeedbackLoop`, `CodeDiff`
54
+ - Reading and layout: `Insight`, `Callout`, `Details`, `Quote`, `NoteGrid`, `Tabs`, `Columns`, `Stack`, `Grid`, `Split`, `ScrollGrid`, `ScrollPair`, `ScrollToc`
55
+ - Graphics and extensions: `Mermaid`, `RelationMap`, `RelationPath`, `Math`, `MathBlock`, `Chart`, `CodeTabs`, `AnnotatedCode`, `Figure` (alias `Image`), `FigureRef`, `Cite`, `References`
56
56
 
57
57
  ## Authoring rules
58
58
 
@@ -67,6 +67,7 @@ This is the only runner — there is nothing to look for. Do not search for a lo
67
67
  - **Learning loop**: `WorkedExample` holds `Step` children; give each step a `reason` (the justification) so it teaches reasoning, not just the result. `Quiz question answer` reveals the answer on click with children as the explanation. `KeyTakeaways items={[...]}` closes a section.
68
68
  - **Provenance**: `Source kind="spec|rfc|implementation|experiment|experience" label href` marks one claim's origin inline; `Confidence level="high|medium|low" basis` wraps a claim with how strongly it is established; `Term name` (or children as the definition) gives an inline hover definition.
69
69
  - **Data and behaviour**: `DataTable headers={[...]} rows={[[...]]}` is the neutral table (no comparative stance). `Metric label value unit delta trend note` is one headline number — compose several with `Grid`/`ScrollGrid`. `StateMachine states={[{id,label,terminal}]} transitions={[{from,to,event,guard}]}` expresses cycles and guards. `DecisionTree branches={[{condition,outcome,tone,branches}]}` nests branches. `FeedbackLoop type="reinforcing|balancing" nodes={[{label,description}]}` closes a loop. `CodeDiff before after language` shows a change.
70
+ - **Structure and code extensions**: `Quote author source href` attributes a quotation. `Checklist title items={[{text,status:'pass'|'fail'|'unknown'|'todo',note}]}` is a verification list. `PropertyList title items={[{name,value,note}]}` is a name/value spec ledger. `TreeView title items={[{label,description,children:[…]}]}` nests a hierarchy. `CodeTabs items={[{label,language,code}]} title caption lineNumbers` gives one collapsible listing per variant. `AnnotatedCode code language title notes={[{line,text}]}` numbers a listing and calls out specific lines. `LastReviewed date by note` is a review stamp.
70
71
  - **Figure**: a relative `src` (`./assets/diagram.png`) stays a relative link by default; compile with `--inline-assets` to bake every local image in as base64, or set `inline={true}` / `inline={false}` on one figure to override (precedence: per-figure prop > global flag > link). `http(s)` URLs stay links and warn (`ASSET_REMOTE`). Always set `alt`; a missing relative file warns (`ASSET_MISSING`) and shows a placeholder. Readers can click a figure to open it full-screen (zoom, drag, `Esc`) — mention it for diagram-heavy pages.
71
72
  - **Figure numbering**: give a figure an `id` and reference it with `<FigureRef id="..." />` to render "图 N". Numbering follows document order (per node in `atlas`, whole document in `scroll`) and reflows automatically, so never hand-write "图 1"; an explicit `label` still wins. A ref with no matching `id` warns (`FIGURE_REF_UNRESOLVED`).
72
73
  - **Figure size & cost**: linking keeps the HTML small but requires the output to sit beside the MDX's `assets/` (the CLI warns when `-o` points elsewhere). Inlining is what makes a screenshot-heavy page large (a page with no heavy renderers otherwise lands near 250KB), and an inlined asset over 512KB warns (`ASSET_LARGE`). Decide the trade-off with the user instead of choosing silently.
@@ -175,6 +175,20 @@ npm run dev`}</CodeBlock>
175
175
  </ScrollGrid>
176
176
  </ScrollSection>
177
177
 
178
+ <ScrollSection title="扩展组件:引用、清单、规格与结构">
179
+ <ScrollProse>以下组件补齐了引用、验证清单、规格表、显式层级与代码注解的表达缺口。</ScrollProse>
180
+ <Quote author="Donald Knuth" source="Literate Programming">程序首先是写给人读的,只是顺便让机器执行。</Quote>
181
+ <LastReviewed date="2026-09-29" by="架构组" note="随 ABI 变更重审" />
182
+ <Checklist title="上线前验证" items={[{text:'依赖已锁定',status:'pass'},{text:'压测通过',status:'fail',note:'P99 仍超标 12%'},{text:'回滚脚本演练',status:'todo'},{text:'灰度范围确认',status:'unknown'}]} />
183
+ <ScrollGrid columns="2">
184
+ <TreeView title="模块结构" items={[{label:'core',description:'内核',children:[{label:'scheduler'},{label:'memory',children:[{label:'allocator'}]}]}]} />
185
+ <PropertyList title="运行时参数" items={[{name:'heap',value:'4Gi',note:'按峰值负载预留'},{name:'gc',value:'zgc'},{name:'timeout',value:'30s'}]} />
186
+ </ScrollGrid>
187
+ <CodeTabs title="同一逻辑的两种写法" items={[{label:'JavaScript',language:'javascript',code:'const sum = ns.reduce((a, n) => a + n, 0);'},{label:'Rust',language:'rust',code:'let sum: i64 = ns.iter().sum();'}]} />
188
+ <AnnotatedCode language="bash" title="定位动态链接问题" code={`ldd app
189
+ readelf -d app | head`} notes={[{line:1,text:'列出动态依赖,缺失项会显示 not found'},{line:2,text:'确认 .dynamic 段与 SONAME'}]} />
190
+ </ScrollSection>
191
+
178
192
  <ScrollSection title="参考文献">
179
193
  <References title="本页引用" items={[{id:'tufte1983',authors:'Tufte, E. R.',year:'1983',title:'The Visual Display of Quantitative Information',source:'Graphics Press',note:'关于以图形压缩与呈现证据的经典论述。'}]} />
180
194
  </ScrollSection>
@@ -175,6 +175,20 @@ npm run dev`}</CodeBlock>
175
175
  </ScrollGrid>
176
176
  </ScrollSection>
177
177
 
178
+ <ScrollSection title="扩展组件:引用、清单、规格与结构">
179
+ <ScrollProse>以下组件补齐了引用、验证清单、规格表、显式层级与代码注解的表达缺口。</ScrollProse>
180
+ <Quote author="Donald Knuth" source="Literate Programming">程序首先是写给人读的,只是顺便让机器执行。</Quote>
181
+ <LastReviewed date="2026-09-29" by="架构组" note="随 ABI 变更重审" />
182
+ <Checklist title="上线前验证" items={[{text:'依赖已锁定',status:'pass'},{text:'压测通过',status:'fail',note:'P99 仍超标 12%'},{text:'回滚脚本演练',status:'todo'},{text:'灰度范围确认',status:'unknown'}]} />
183
+ <ScrollGrid columns="2">
184
+ <TreeView title="模块结构" items={[{label:'core',description:'内核',children:[{label:'scheduler'},{label:'memory',children:[{label:'allocator'}]}]}]} />
185
+ <PropertyList title="运行时参数" items={[{name:'heap',value:'4Gi',note:'按峰值负载预留'},{name:'gc',value:'zgc'},{name:'timeout',value:'30s'}]} />
186
+ </ScrollGrid>
187
+ <CodeTabs title="同一逻辑的两种写法" items={[{label:'JavaScript',language:'javascript',code:'const sum = ns.reduce((a, n) => a + n, 0);'},{label:'Rust',language:'rust',code:'let sum: i64 = ns.iter().sum();'}]} />
188
+ <AnnotatedCode language="bash" title="定位动态链接问题" code={`ldd app
189
+ readelf -d app | head`} notes={[{line:1,text:'列出动态依赖,缺失项会显示 not found'},{line:2,text:'确认 .dynamic 段与 SONAME'}]} />
190
+ </ScrollSection>
191
+
178
192
  <ScrollSection title="参考文献">
179
193
  <References title="本页引用" items={[{id:'tufte1983',authors:'Tufte, E. R.',year:'1983',title:'The Visual Display of Quantitative Information',source:'Graphics Press',note:'关于以图形压缩与呈现证据的经典论述。'}]} />
180
194
  </ScrollSection>