@hajimism/dek 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  Release notes for `@hajimism/dek`. From 0.1.0 on, release-please writes this file from the Conventional Commits on `main`.
4
4
 
5
+ ## [0.6.1](https://github.com/hajimism/dek/compare/v0.6.0...v0.6.1) (2026-11-09)
6
+
7
+
8
+ ### Features
9
+
10
+ * run a project's plugins in dekc check, for the slide it checks ([8f091ca](https://github.com/hajimism/dek/commit/8f091ca1ee65f0c2994f59ad1f40bcb30ac35141))
11
+ * run a project's plugins in dekc check, for the slide it checks ([07cd3cc](https://github.com/hajimism/dek/commit/07cd3ccc14ec5ce0ff299880f2895780438a1510))
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * tell the agent to deal with lint's warnings, and which plugin data is whose ([8550412](https://github.com/hajimism/dek/commit/85504129dbe29b3a4b3b92b7033a903ef7c321e2))
17
+ * tell the agent to deal with lint's warnings, and which plugin data is whose ([7e69b3b](https://github.com/hajimism/dek/commit/7e69b3b525421f08de65fe806b525d70504e7923))
18
+
19
+
20
+ ### Documentation
21
+
22
+ * **adr:** record what AGENTS.md says of warnings and of whose plugin data is ([d42d28f](https://github.com/hajimism/dek/commit/d42d28f4d70572ecdd0e5ea9c15678f69b46af17))
23
+ * say in the lint reference that plugins run in dekc check too ([c7f0cbb](https://github.com/hajimism/dek/commit/c7f0cbbbf49cf3bccab9cc7996f49d8e067d8f73))
24
+
5
25
  ## [0.6.0](https://github.com/hajimism/dek/compare/v0.5.1...v0.6.0) (2026-11-07)
6
26
 
7
27
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hajimism/dek",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "A build system for talks.",
5
5
  "license": "MIT",
6
6
  "author": "Hajime Nakagawa (https://github.com/hajimism)",
package/src/cli/check.ts CHANGED
@@ -4,6 +4,7 @@ import { shellQuote } from "../core/escape.ts";
4
4
  import type { Fill } from "../core/fill.ts";
5
5
  import { lintDeck } from "../core/lint.ts";
6
6
  import { type PlaywrightRunner, playwrightMissingError } from "../core/playwright.ts";
7
+ import { checkSlide } from "../core/plugin-host.ts";
7
8
  import { runVisualDeck } from "../core/visual.ts";
8
9
  import { hasVoice, loadCachedTimeline, VOICE_SETUP_HINT } from "../core/voice.ts";
9
10
  import { type SkippedCheck, skippedChecks, visualSkipped } from "./result.ts";
@@ -35,26 +36,34 @@ export async function checkCommand(
35
36
  requireSection(deck, slug);
36
37
 
37
38
  const diagnostics = lintDeck({ project, deck }, { slug });
38
- const visual = await runVisualDeck(
39
- { project, deck },
40
- {
41
- slug,
42
- screenshot: options.shot === true,
43
- ...(options.runner ? { runner: options.runner } : {}),
44
- },
45
- );
39
+ // The plugins run in processes of their own while the browser measures the slide.
40
+ const [visual, plugins] = await Promise.all([
41
+ runVisualDeck(
42
+ { project, deck },
43
+ {
44
+ slug,
45
+ screenshot: options.shot === true,
46
+ ...(options.runner ? { runner: options.runner } : {}),
47
+ },
48
+ ),
49
+ checkSlide(project, deck, slug),
50
+ ]);
46
51
  if (visual) {
47
52
  diagnostics.push(...visual.diagnostics);
48
53
  } else if (options.shot) {
49
54
  throw playwrightMissingError();
50
55
  }
56
+ diagnostics.push(...plugins.diagnostics);
51
57
 
52
58
  const shot = visual?.screenshotPath;
53
59
  const { slug: _slide, ...fill } = visual?.fills.find((found) => found.slug === slug) ?? {};
54
60
 
55
- const skipped: SkippedCheck[] = visual
56
- ? []
57
- : [visualSkipped(`dekc check ${shellQuote(slug)}`, { reason: "Playwright is not installed" })];
61
+ const skipped: SkippedCheck[] = [...plugins.skipped];
62
+ if (!visual) {
63
+ skipped.push(
64
+ visualSkipped(`dekc check ${shellQuote(slug)}`, { reason: "Playwright is not installed" }),
65
+ );
66
+ }
58
67
 
59
68
  // A live-only deck is done without voice, so asking for it skips the check rather than failing,
60
69
  // the way a missing Playwright skips visual.
@@ -1,7 +1,7 @@
1
1
  import { type Diagnostic, diag } from "../diagnostic.ts";
2
2
  import type { DekError } from "../error.ts";
3
3
  import { ScriptError } from "../parse.ts";
4
- import { scriptLines } from "../script-lines.ts";
4
+ import { scriptLines, slugAtLine } from "../script-lines.ts";
5
5
  import type { LintContext } from "./context.ts";
6
6
 
7
7
  /** DEK004: a section id or a beat id used twice. */
@@ -56,7 +56,6 @@ export function strayHeadingDiagnostics(ctx: LintContext): Diagnostic[] {
56
56
  if (!script) {
57
57
  return [];
58
58
  }
59
- const starts = deck.deck.sections.map((section) => ({ line: section.line, slug: section.slug }));
60
59
  const diagnostics: Diagnostic[] = [];
61
60
  // What the parser reads as text, a code block or a comment, is not a heading here either.
62
61
  for (const [offset, { text, literal }] of scriptLines(script.body).entries()) {
@@ -65,8 +64,7 @@ export function strayHeadingDiagnostics(ctx: LintContext): Diagnostic[] {
65
64
  continue;
66
65
  }
67
66
  const line = script.bodyStartLine + offset;
68
- // Under a slide heading it belongs to that slide; above the first, to the deck.
69
- const slug = starts.filter((start) => start.line < line).at(-1)?.slug;
67
+ const slug = slugAtLine(deck.deck.sections, line);
70
68
  diagnostics.push(
71
69
  diag("DEK044", {
72
70
  message: `${"#".repeat(level)} heading is not a slide or a beat; it is read as spoken text`,
@@ -20,6 +20,7 @@ import {
20
20
  } from "./plugin.ts";
21
21
  import type { Project, ProjectDeck } from "./resolve.ts";
22
22
  import { readSourceIfExists, writeInside } from "./safe-fs.ts";
23
+ import { slugAtLine } from "./script-lines.ts";
23
24
  import { OverrunError, runPiped, workerCommand } from "./spawn.ts";
24
25
 
25
26
  /**
@@ -53,14 +54,21 @@ function binName(name: string): string {
53
54
  * any plugin does, and never from PATH: its code is the dek the project installed, which the
54
55
  * project's lockfile pins.
55
56
  */
56
- const BUNDLED: Record<string, { main: string; page?: string }> = {
57
+ const BUNDLED: Record<string, { main: string; page?: string; guide: string }> = {
57
58
  study: bundled("study", { page: true }),
58
59
  words: bundled("words", { page: false }),
59
60
  };
60
61
 
61
- function bundled(name: string, { page }: { page: boolean }): { main: string; page?: string } {
62
+ function bundled(
63
+ name: string,
64
+ { page }: { page: boolean },
65
+ ): { main: string; page?: string; guide: string } {
62
66
  const dir = join(import.meta.dir, "..", "plugins", name);
63
- return { main: join(dir, "main.ts"), ...(page ? { page: join(dir, "page.ts") } : {}) };
67
+ return {
68
+ main: join(dir, "main.ts"),
69
+ ...(page ? { page: join(dir, "page.ts") } : {}),
70
+ guide: `https://hajimism.github.io/dek/reference/plugins.html#dek-${name}`,
71
+ };
64
72
  }
65
73
 
66
74
  /** The page of each plugin dek ships with one, as a file to compile for the browser (ADR 34). */
@@ -304,7 +312,7 @@ export async function checkDeck(
304
312
  }
305
313
  return { diagnostics: [failed("could not start")] };
306
314
  }
307
- const read = readSarif(stdout, plugin, [deck.dir, pluginDir(project.root, plugin.name)]);
315
+ const read = readSarif(stdout, plugin, deck, pluginDir(project.root, plugin.name));
308
316
  return typeof read === "string" ? { diagnostics: [failed(read)] } : { diagnostics: read };
309
317
  }
310
318
 
@@ -340,13 +348,14 @@ type SarifResult = {
340
348
  };
341
349
 
342
350
  /**
343
- * The findings in a check's SARIF, or what is wrong with it, as the end of a DEK046 message.
344
- * `dirs` are the deck's folder, which a relative location is read against, then the plugin's own.
351
+ * The findings in a check's SARIF, or what is wrong with it, as the end of a DEK046 message. A
352
+ * relative location is read against the deck's folder; `ownDir` is the plugin's own.
345
353
  */
346
354
  function readSarif(
347
355
  stdout: string,
348
356
  plugin: ReadyPlugin,
349
- dirs: [deck: string, plugin: string],
357
+ deck: ProjectDeck,
358
+ ownDir: string,
350
359
  ): Diagnostic[] | string {
351
360
  let log: unknown;
352
361
  try {
@@ -381,7 +390,7 @@ function readSarif(
381
390
  id,
382
391
  severity: rule.severity,
383
392
  message: foreignLine(text, PLUGIN_TEXT_MAX),
384
- ...location(result, dirs),
393
+ ...location(result, deck, ownDir),
385
394
  ...(typeof hint === "string" && hint.trim() !== ""
386
395
  ? { hint: foreignLine(hint, PLUGIN_TEXT_MAX) }
387
396
  : {}),
@@ -394,11 +403,13 @@ function readSarif(
394
403
  /**
395
404
  * Where a finding is, from its first location: a path relative to the deck, or a file URL, inside
396
405
  * the deck or the plugin's own `plugins/<name>/` (ADR 35). A location anywhere else is dropped,
397
- * never shown as a file to change.
406
+ * never shown as a file to change. A finding in a slide's files, or in its section of script.md,
407
+ * is on that slide, as dek's own are.
398
408
  */
399
409
  function location(
400
410
  result: SarifResult,
401
- [deckDir, ownDir]: [string, string],
411
+ deck: ProjectDeck,
412
+ ownDir: string,
402
413
  ): Pick<Diagnostic, "path" | "line" | "column" | "slug"> {
403
414
  const physical = result.locations?.[0]?.physicalLocation;
404
415
  const uri = physical?.artifactLocation?.uri;
@@ -407,20 +418,20 @@ function location(
407
418
  }
408
419
  let path: string;
409
420
  try {
410
- path = uri.startsWith("file:") ? fileURLToPath(uri) : resolve(deckDir, decodeURI(uri));
421
+ path = uri.startsWith("file:") ? fileURLToPath(uri) : resolve(deck.dir, decodeURI(uri));
411
422
  } catch {
412
423
  return {};
413
424
  }
414
- const inDeck = inside(deckDir, path);
425
+ const inDeck = inside(deck.dir, path)?.split(sep).join("/");
415
426
  if (inDeck === undefined && inside(ownDir, path) === undefined) {
416
427
  return {};
417
428
  }
418
429
  const line = positive(physical?.region?.startLine);
419
430
  const column = line === undefined ? undefined : positive(physical?.region?.startColumn);
420
- const slug = inDeck
421
- ?.split(sep)
422
- .join("/")
423
- .match(/^slides\/([^/]+)\.(?:html|css|ts)$/)?.[1];
431
+ const slug =
432
+ inDeck === "script.md" && line !== undefined
433
+ ? slugAtLine(deck.deck.sections, line)
434
+ : inDeck?.match(/^slides\/([^/]+)\.(?:html|css|ts)$/)?.[1];
424
435
  return {
425
436
  path,
426
437
  ...(line === undefined ? {} : { line }),
@@ -530,6 +541,30 @@ export async function pluginChecks(
530
541
  return { ready, skipped, diagnostics };
531
542
  }
532
543
 
544
+ /**
545
+ * The plugins' findings for `dekc check` on one slide, and the plugins that did not run. Each
546
+ * pinned plugin checks the deck, as lint has it, and the slide keeps what is on it. It also keeps
547
+ * an error on no slide, such as data that does not read or a plugin that failed: the slide's
548
+ * check may rest on it, and leaving it out would pass a slide no plugin checked.
549
+ */
550
+ export async function checkSlide(
551
+ project: Project,
552
+ deck: ProjectDeck,
553
+ slug: string,
554
+ ): Promise<{ diagnostics: Diagnostic[]; skipped: SkippedCheck[] }> {
555
+ const plugins = await pluginChecks(project);
556
+ const outcomes = await Promise.all(
557
+ plugins.ready.map((plugin) => checkDeck(plugin, project, deck)),
558
+ );
559
+ const diagnostics = [
560
+ ...plugins.diagnostics,
561
+ ...outcomes.flatMap((outcome) => outcome.diagnostics),
562
+ ].filter((found) =>
563
+ found.slug === undefined ? found.severity === "error" : found.slug === slug,
564
+ );
565
+ return { diagnostics, skipped: plugins.skipped };
566
+ }
567
+
533
568
  /** What `dekc sync` did with one plugin, for its `--json` and its notes. */
534
569
  export type PluginSync = {
535
570
  name: string;
@@ -628,7 +663,12 @@ export function pinnedPlugins(root: string): PinnedPlugin[] {
628
663
  } catch {
629
664
  reading = undefined;
630
665
  }
631
- return { name, manifest: reading?.ok ? reading.manifest : undefined };
666
+ const guide = BUNDLED[name]?.guide;
667
+ return {
668
+ name,
669
+ manifest: reading?.ok ? reading.manifest : undefined,
670
+ ...(guide === undefined ? {} : { guide }),
671
+ };
632
672
  });
633
673
  }
634
674
 
@@ -359,30 +359,35 @@ function checkChanges(
359
359
  return changes;
360
360
  }
361
361
 
362
- /** A plugin dek.toml names, with the manifest its lock pins when it has one. */
363
- export type PinnedPlugin = { name: string; manifest: PluginManifest | undefined };
362
+ /**
363
+ * A plugin dek.toml names, with the manifest its lock pins when it has one, and dek's own page on
364
+ * how to write its data when dek ships it.
365
+ */
366
+ export type PinnedPlugin = { name: string; manifest: PluginManifest | undefined; guide?: string };
364
367
 
365
368
  /**
366
369
  * The plugins in AGENTS.md: one line each, made from the lock and never from the executable, so
367
370
  * every sync writes the same block, the dev server's included. Each line is dek's words about
368
- * checked fields; the plugin's own summaries stay out. Empty when dek.toml names no plugin.
371
+ * checked fields; the plugin's own summaries stay out. A data file says whether it is the
372
+ * project's or each deck's, since the two can share a name. Empty when dek.toml names no plugin.
369
373
  */
370
374
  export function pluginsSection(plugins: PinnedPlugin[]): string {
371
375
  if (plugins.length === 0) {
372
376
  return "";
373
377
  }
374
- const lines = plugins.map(({ name, manifest }) => {
378
+ const lines = plugins.map(({ name, manifest, guide }) => {
375
379
  if (!manifest) {
376
380
  return `- \`${name}\`: not pinned yet; \`dekc sync\` pins it.`;
377
381
  }
378
382
  const facts = [
379
- ...(manifest.check
380
- ? [`\`dekc lint\` checks ${ruleList(Object.keys(manifest.check.rules))}.`]
381
- : []),
383
+ ...(manifest.check ? [`It checks ${ruleList(Object.keys(manifest.check.rules))}.`] : []),
382
384
  ...(manifest.data && manifest.data.length > 0
383
385
  ? [
384
386
  `Its data: ${projectFirst(manifest.data)
385
- .map((entry) => `\`${dataPath(name, entry)}\``)
387
+ .map(
388
+ (entry) =>
389
+ `\`${dataPath(name, entry)}\` ${entry.in === "project" ? "in the project" : "in each deck"}`,
390
+ )
386
391
  .join(", ")}.`,
387
392
  ]
388
393
  : []),
@@ -391,13 +396,14 @@ export function pluginsSection(plugins: PinnedPlugin[]): string {
391
396
  `Its page takes the ${manifest.page.keys.length === 1 ? "key" : "keys"} ${manifest.page.keys.map((key) => `\`${key}\``).join(", ")}.`,
392
397
  ]
393
398
  : []),
399
+ ...(guide && manifest.data && manifest.data.length > 0 ? [`How to write it: ${guide}`] : []),
394
400
  ];
395
401
  return `- \`${name}\` ${manifest.version}${facts.length > 0 ? `: ${facts.join(" ")}` : ""}`;
396
402
  });
397
403
  return `
398
404
  ## Plugins
399
405
 
400
- Checks installed beside dek, named in \`dek.toml\` \`[plugins]\` and pinned in \`plugins/<name>/plugin.json\`. \`dekc lint\` runs them with dek's own rules; their findings carry the plugin's rule ids.
406
+ Checks installed beside dek, named in \`dek.toml\` \`[plugins]\` and pinned in \`plugins/<name>/plugin.json\`. \`dekc lint\` runs them with dek's own rules, and \`dekc check <slug>\` reports what they find on that slide; their findings carry the plugin's rule ids.
401
407
 
402
408
  ${lines.join("\n")}
403
409
  `;
@@ -51,3 +51,14 @@ function commentOpenAfter(text: string, open: boolean): boolean {
51
51
  }
52
52
  return inside;
53
53
  }
54
+
55
+ /**
56
+ * The slide a line of script.md belongs to: the `##` section it is in, heading included. Above the
57
+ * first heading it belongs to the deck, and to no slide.
58
+ */
59
+ export function slugAtLine(
60
+ sections: ReadonlyArray<{ line: number; slug: string }>,
61
+ line: number,
62
+ ): string | undefined {
63
+ return sections.filter((section) => section.line <= line).at(-1)?.slug;
64
+ }
package/src/core/sync.ts CHANGED
@@ -288,7 +288,7 @@ A build system for talks. Write what you will say; dek builds, measures, and shi
288
288
 
289
289
  ## Before you report a deck as done
290
290
 
291
- - \`dekc lint --visual\` passes.
291
+ - \`dekc lint --visual\` passes. Warnings do not fail it: fix each one, or tell the author which you kept and why.
292
292
  - You have read \`dekc shot --sheet\` and judged the deck's balance.
293
293
  - Say what you could not judge, such as the argument and the timing, and leave it to the author.
294
294
  ${referencesSection(root)}${pluginsSection(pinnedPlugins(root))}