mixdog 1.0.4 → 1.0.6

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.
Files changed (158) hide show
  1. package/README.md +117 -434
  2. package/package.json +18 -17
  3. package/scripts/gate-local.mjs +93 -0
  4. package/scripts/lib/gate-scheduler.mjs +24 -0
  5. package/scripts/lib/run-node-tests.mjs +5 -1
  6. package/scripts/release-paths.mjs +22 -1
  7. package/scripts/test-direct.mjs +2 -1
  8. package/scripts/test.mjs +3 -1
  9. package/src/defaults/agents.json +0 -12
  10. package/src/defaults/mixdog-config.template.json +1 -2
  11. package/src/defaults/skills/docx/SKILL.md +7 -4
  12. package/src/defaults/skills/docx/references/html.md +46 -0
  13. package/src/defaults/skills/docx/references/native-authoring.md +18 -4
  14. package/src/defaults/skills/pdf/SKILL.md +10 -2
  15. package/src/defaults/skills/pdf/references/html.md +64 -0
  16. package/src/defaults/skills/pptx/references/html.md +1 -1
  17. package/src/defaults/skills/xlsx/SKILL.md +7 -3
  18. package/src/defaults/skills/xlsx/references/html.md +40 -0
  19. package/src/defaults/skills/xlsx/references/report-design.md +21 -4
  20. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +1 -1
  21. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -3
  22. package/src/runtime/agent/orchestrator/internal-agents.mjs +1 -1
  23. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +6 -5
  24. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +6 -3
  25. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +6 -2
  26. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +50 -9
  27. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +28 -1
  28. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +4 -1
  29. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +34 -8
  30. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +10 -2
  31. package/src/runtime/agent/orchestrator/session/transcript-restore/agent-envelope.mjs +1 -1
  32. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +1 -0
  33. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +3 -1
  34. package/src/runtime/attachments/pdf-extract.mjs +7 -3
  35. package/src/runtime/computer-bridge/client.mjs +1 -1
  36. package/src/runtime/memory/index.mjs +4 -15
  37. package/src/runtime/memory/lib/core-memory-store.mjs +1 -1
  38. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +0 -2
  39. package/src/runtime/memory/lib/cycle-scheduler/backlog-probe.mjs +3 -11
  40. package/src/runtime/memory/lib/cycle-scheduler/health-ledger.mjs +1 -3
  41. package/src/runtime/memory/lib/cycle-scheduler/scheduled-enqueue.mjs +1 -11
  42. package/src/runtime/memory/lib/cycle-scheduler/tick-loop.mjs +0 -2
  43. package/src/runtime/memory/lib/cycle-scheduler.mjs +4 -13
  44. package/src/runtime/memory/lib/cycle-signatures.mjs +0 -6
  45. package/src/runtime/memory/lib/cycle1/cycle1-rows.mjs +1 -1
  46. package/src/runtime/memory/lib/cycle1/cycle1-window.mjs +2 -2
  47. package/src/runtime/memory/lib/embedding-reindex.mjs +1 -1
  48. package/src/runtime/memory/lib/http-router/lifecycle-routes.mjs +0 -1
  49. package/src/runtime/memory/lib/memory-action-handlers/cycle-actions.mjs +7 -52
  50. package/src/runtime/memory/lib/memory-action-handlers/cycle-backfill-action.mjs +6 -22
  51. package/src/runtime/memory/lib/memory-action-handlers/cycle-rebuild-action.mjs +10 -17
  52. package/src/runtime/memory/lib/memory-action-handlers/maintenance-actions.mjs +1 -3
  53. package/src/runtime/memory/lib/memory-action-handlers/manage-actions.mjs +2 -4
  54. package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -4
  55. package/src/runtime/memory/lib/memory-cycle-requests.mjs +1 -1
  56. package/src/runtime/memory/lib/{memory-cycle2-shared.mjs → memory-cycle-shared.mjs} +14 -16
  57. package/src/runtime/memory/lib/memory-cycle.mjs +3 -2
  58. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
  59. package/src/runtime/memory/lib/memory-embed.mjs +14 -1
  60. package/src/runtime/memory/lib/memory-ops-policy.mjs +8 -11
  61. package/src/runtime/memory/lib/memory-schema/entries.mjs +0 -1
  62. package/src/runtime/memory/lib/memory.mjs +2 -6
  63. package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
  64. package/src/runtime/memory/lib/query-handlers.mjs +1 -1
  65. package/src/runtime/memory/lib/query-hybrid-search.mjs +1 -1
  66. package/src/runtime/memory/lib/query-maintenance-handlers.mjs +0 -6
  67. package/src/runtime/memory/lib/recall-format.mjs +1 -1
  68. package/src/runtime/office/authoring/docx-html-build.mjs +375 -0
  69. package/src/runtime/office/authoring/docx-html-measure.mjs +561 -0
  70. package/src/runtime/office/authoring/docx-html-runner.mjs +102 -0
  71. package/src/runtime/office/authoring/html-browser.mjs +61 -0
  72. package/src/runtime/office/authoring/html-document-author.mjs +181 -0
  73. package/src/runtime/office/authoring/html-source-drift.mjs +36 -0
  74. package/src/runtime/office/authoring/pdf-author-action.mjs +266 -0
  75. package/src/runtime/office/authoring/pdf-html-charts.mjs +379 -0
  76. package/src/runtime/office/authoring/pdf-html-runner.mjs +317 -0
  77. package/src/runtime/office/authoring/pptx-author-action.mjs +44 -22
  78. package/src/runtime/office/authoring/pptx-author-session.mjs +12 -6
  79. package/src/runtime/office/authoring/pptx-html-build.mjs +23 -4
  80. package/src/runtime/office/authoring/pptx-html-measure.mjs +38 -44
  81. package/src/runtime/office/authoring/pptx-html-runner.mjs +18 -3
  82. package/src/runtime/office/authoring/pptx-receipt.mjs +39 -31
  83. package/src/runtime/office/authoring/pptx-review-artifacts.mjs +9 -1
  84. package/src/runtime/office/authoring/pptx-script-runner.mjs +6 -2
  85. package/src/runtime/office/authoring/xlsx-html-build.mjs +365 -0
  86. package/src/runtime/office/authoring/xlsx-html-measure.mjs +257 -0
  87. package/src/runtime/office/authoring/xlsx-html-runner.mjs +139 -0
  88. package/src/runtime/office/capabilities-catalog.mjs +8 -0
  89. package/src/runtime/office/capabilities-signatures.mjs +18 -6
  90. package/src/runtime/office/capabilities.mjs +26 -1
  91. package/src/runtime/office/com/office-com-host.ps1 +133 -17
  92. package/src/runtime/office/core/office-actions-batch.mjs +16 -1
  93. package/src/runtime/office/core/office-actions-inspect.mjs +3 -2
  94. package/src/runtime/office/core/office-actions-lifecycle.mjs +6 -2
  95. package/src/runtime/office/core/office-actions-render.mjs +4 -1
  96. package/src/runtime/office/core/office-finalize/review-stage.mjs +15 -2
  97. package/src/runtime/office/core/office-qa/design-review-stage.mjs +9 -0
  98. package/src/runtime/office/core/office-session-dispatch.mjs +3 -0
  99. package/src/runtime/office/core/office-sessionless-actions.mjs +7 -2
  100. package/src/runtime/office/core/office-sessions.mjs +19 -2
  101. package/src/runtime/office/pdf/pdf-writer.mjs +1 -1
  102. package/src/runtime/office/portable/docx-formatting.mjs +21 -0
  103. package/src/runtime/office/portable/ooxml-validator.mjs +22 -1
  104. package/src/runtime/office/portable/portable-chart.mjs +10 -2
  105. package/src/runtime/office/portable/portable-docx-edits.mjs +12 -6
  106. package/src/runtime/office/portable/portable-docx-operations.mjs +75 -1
  107. package/src/runtime/office/portable/portable-docx-xml.mjs +49 -6
  108. package/src/runtime/office/portable/portable-docx.mjs +2 -0
  109. package/src/runtime/office/portable/portable-snapshot-docx.mjs +2 -0
  110. package/src/runtime/office/portable/portable-validation.mjs +6 -3
  111. package/src/runtime/office/portable/portable-xlsx-cell-values.mjs +87 -0
  112. package/src/runtime/office/portable/portable-xlsx-charts.mjs +55 -13
  113. package/src/runtime/office/portable/portable-xlsx-sheet-edits.mjs +1 -1
  114. package/src/runtime/office/portable/xlsx-contract.mjs +82 -3
  115. package/src/runtime/office/quality/assurance-checklist.mjs +2 -2
  116. package/src/runtime/office/quality/assurance-rendered.mjs +27 -1
  117. package/src/runtime/office/quality/assurance-structure-docx.mjs +44 -0
  118. package/src/runtime/office/quality/assurance-structure-pptx.mjs +24 -6
  119. package/src/runtime/office/quality/assurance-structure-xlsx.mjs +50 -1
  120. package/src/runtime/office/quality/design-review-critique.mjs +3 -2
  121. package/src/runtime/office/quality/document-brief.mjs +44 -0
  122. package/src/runtime/office/quality/inline-audit.mjs +7 -2
  123. package/src/runtime/office/quality/quality-pipeline.mjs +9 -1
  124. package/src/runtime/office/tool-defs.mjs +2 -2
  125. package/src/runtime/shared/llm/quota-value-estimate.mjs +317 -0
  126. package/src/runtime/shared/llm/usage-ledger-quota.mjs +266 -63
  127. package/src/runtime/shared/llm/usage-ledger-rollup.mjs +35 -0
  128. package/src/runtime/shared/llm/usage-ledger-worker.mjs +2 -1
  129. package/src/runtime/shared/llm/usage-ledger.mjs +10 -1
  130. package/src/runtime/shared/provider-accounts.mjs +11 -1
  131. package/src/runtime/shared/statusline/statusline-segments.mjs +4 -4
  132. package/src/runtime/shared/tool-execution-contract.mjs +1 -1
  133. package/src/runtime/shared/wait-until.test-support.mjs +33 -0
  134. package/src/session-runtime/boot/apis.mjs +1 -1
  135. package/src/session-runtime/services/channel-admin.mjs +4 -3
  136. package/src/session-runtime/services/usage-stats-model.mjs +22 -0
  137. package/src/session-runtime/setup-tool/settings-contract.mjs +6 -1
  138. package/src/session-runtime/turn/session-ops.mjs +15 -9
  139. package/src/session-runtime/usage-stats-api.mjs +34 -2
  140. package/src/standalone/agent-dispatch-broker.mjs +1 -1
  141. package/src/standalone/local-session-runtime.mjs +488 -0
  142. package/src/standalone/session-protocol.mjs +1 -0
  143. package/src/standalone/session-runtime-inline-host.mjs +1 -1
  144. package/src/tui/hooks/useSession.mjs +1 -1
  145. package/src/tui/session/session-api/commands.mjs +1 -1
  146. package/src/tui/session/session-api/integrations.mjs +3 -0
  147. package/src/tui/session/session-flow/auto-clear.mjs +6 -7
  148. package/src/tui/session/tool-card-results.mjs +1 -1
  149. package/src/tui/session/turn.mjs +5 -0
  150. package/src/tui/session-local.mjs +3 -488
  151. package/src/tui/session.mjs +1 -1
  152. package/src/ui/statusline-agents.mjs +0 -1
  153. package/src/rules/agent/41-cycle2-agent.md +0 -24
  154. package/src/runtime/memory/lib/cycle-scheduler/cycle2-runs.mjs +0 -120
  155. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +0 -110
  156. package/src/runtime/memory/lib/memory-cycle2-quarantine.mjs +0 -99
  157. package/src/runtime/memory/lib/memory-cycle2-review.mjs +0 -240
  158. package/src/runtime/memory/lib/memory-cycle2.mjs +0 -191
@@ -8,14 +8,16 @@ import { writeFile } from 'node:fs/promises';
8
8
  import { createRequire } from 'node:module';
9
9
  import { pathToFileURL } from 'node:url';
10
10
 
11
- import { buildPuppeteerLaunchArgs, resolveBrowserLaunchOptions } from '../../shared/browser-launch.mjs';
12
- import { startChildGuardian } from '../../shared/child-guardian.mjs';
11
+ import { launchHtmlBrowser, runInBrowser } from './html-browser.mjs';
13
12
  import { iconGlobal } from './pptx-icons.mjs';
14
13
  import { GEOMETRY_TOLERANCE_PX, NEAR_MISS_PX, readGeometry } from './pptx-html-geometry.mjs';
15
14
 
16
15
  const require = createRequire(import.meta.url);
17
16
 
18
- export const HTML_CANVAS = { width: 1920, height: 1080 };
17
+ /** The deck canvas in CSS px and the page it lands on in inches; a PDF's designed pages pass their sheet instead. */
18
+ export const HTML_CANVAS = { width: 1920, height: 1080, inchWidth: 13.333, inchHeight: 7.5 };
19
+
20
+ const viewport = (canvas, deviceScaleFactor) => ({ width: canvas.width, height: canvas.height, deviceScaleFactor });
19
21
 
20
22
  // Runs inside the page: everything it needs is defined here. Returns one
21
23
  // slide's items in paint order (a parent's box before its children).
@@ -464,7 +466,7 @@ function showSlide(index) {
464
466
 
465
467
  // <i data-icon="name" data-sw="1.75"> becomes the offline Lucide SVG, sized by the <i>'s CSS box
466
468
  // and stroked in its CSS color.
467
- function injectIcons(table, viewBox) {
469
+ export function injectIcons(table, viewBox) {
468
470
  const missing = [];
469
471
  for (const i of document.querySelectorAll('i[data-icon]')) {
470
472
  const markup = table[String(i.dataset.icon || '').toLowerCase()];
@@ -488,18 +490,18 @@ const CAPTURE_SCALE = 2;
488
490
 
489
491
  // Each marked element photographed alone at twice the canvas density on a transparent page, trimmed to
490
492
  // what it painted (shadows, pseudo-elements and turned corners included): id → { png, box } in CSS px.
491
- async function captureRichPaint(page, slideBox, ids) {
493
+ async function captureRichPaint(page, slideBox, ids, canvas) {
492
494
  const out = new Map();
493
495
  if (!ids.length) return out;
494
496
  const sharp = require('sharp');
495
- await page.setViewport({ ...HTML_CANVAS, deviceScaleFactor: CAPTURE_SCALE });
497
+ await page.setViewport(viewport(canvas, CAPTURE_SCALE));
496
498
  try {
497
499
  for (const id of ids) {
498
500
  await page.evaluate(isolateCapture, id);
499
501
  // Runtimes with Uint8Array.fromBase64 get a plain Uint8Array back, whose toString is not base64.
500
502
  const shot = Buffer.from(
501
503
  await page.screenshot({
502
- clip: { x: slideBox.x, y: slideBox.y, width: HTML_CANVAS.width, height: HTML_CANVAS.height },
504
+ clip: { x: slideBox.x, y: slideBox.y, width: canvas.width, height: canvas.height },
503
505
  omitBackground: true,
504
506
  type: 'png',
505
507
  })
@@ -511,7 +513,7 @@ async function captureRichPaint(page, slideBox, ids) {
511
513
  if (!alpha || alpha.min === 255) {
512
514
  out.set(id, {
513
515
  png: shot.toString('base64'),
514
- draw: { x: 0, y: 0, w: HTML_CANVAS.width, h: HTML_CANVAS.height },
516
+ draw: { x: 0, y: 0, w: canvas.width, h: canvas.height },
515
517
  });
516
518
  continue;
517
519
  }
@@ -530,39 +532,40 @@ async function captureRichPaint(page, slideBox, ids) {
530
532
  }
531
533
  } finally {
532
534
  await page.evaluate(isolateCapture, null);
533
- await page.setViewport({ ...HTML_CANVAS, deviceScaleFactor: 1 });
535
+ await page.setViewport(viewport(canvas, 1));
534
536
  }
535
537
  return out;
536
538
  }
537
539
 
540
+ /** The offline Lucide table `injectIcons` takes, keyed by icon name. */
541
+ export function iconTable() {
542
+ const icons = iconGlobal();
543
+ return { icons, table: Object.fromEntries(icons.names.map((name) => [name, icons(name)])) };
544
+ }
545
+
538
546
  /**
539
547
  * Lays out an HTML deck in a local browser and measures it.
540
548
  * @param {string} html the deck
541
- * @param {{ sourcePath: string, shotPath: (page: number) => string, timeoutMs?: number, signal?: AbortSignal }} options
549
+ * @param {{ sourcePath: string, shotPath: (page: number) => string, timeoutMs?: number, signal?: AbortSignal,
550
+ * prepare?: (page: object) => Promise<object> }} options
542
551
  * sourcePath: where the HTML is written before loading (beside the deck, so relative <img> paths resolve)
543
- * @returns {Promise<{ width: number, height: number, slides: object[], shots: string[], notes: string[], geometry: object[] }>}
552
+ * prepare: run on the loaded page; returns the canvas (`HTML_CANVAS` shape) the slides are measured on
553
+ * @returns {Promise<{ width: number, height: number, inchWidth: number, inchHeight: number, slides: object[], shots: string[], notes: string[], geometry: object[] }>}
544
554
  * geometry: per slide with findings, `{ slide, findings }` from pptx-html-geometry.mjs
545
555
  */
546
- export async function measureHtmlDeck(html, { sourcePath, shotPath, timeoutMs = 90_000, signal = null }) {
556
+ export async function measureHtmlDeck(
557
+ html,
558
+ { sourcePath, shotPath, timeoutMs = 90_000, signal = null, prepare = null }
559
+ ) {
547
560
  await writeFile(sourcePath, html, 'utf8');
548
- const puppeteer = (await import('puppeteer-core')).default;
549
- const browser = await puppeteer.launch({
550
- headless: true,
551
- ...resolveBrowserLaunchOptions(),
552
- args: buildPuppeteerLaunchArgs(['--font-render-hinting=none']),
553
- });
554
- try {
555
- startChildGuardian({ childPid: browser.process?.()?.pid, label: 'pptx-html-browser' });
556
- } catch {}
557
- const abort = () => browser.close().catch(() => {});
558
- signal?.addEventListener('abort', abort, { once: true });
559
- let timer = null;
560
- const work = (async () => {
561
+ const browser = await launchHtmlBrowser('pptx-html-browser');
562
+ const work = async () => {
561
563
  const page = await browser.newPage();
562
- await page.setViewport({ ...HTML_CANVAS, deviceScaleFactor: 1 });
564
+ await page.setViewport(viewport(HTML_CANVAS, 1));
563
565
  await page.goto(pathToFileURL(sourcePath).href, { waitUntil: 'load' });
564
- const icons = iconGlobal();
565
- const table = Object.fromEntries(icons.names.map((name) => [name, icons(name)]));
566
+ const canvas = prepare ? await prepare(page) : HTML_CANVAS;
567
+ if (canvas !== HTML_CANVAS) await page.setViewport(viewport(canvas, 1));
568
+ const { icons, table } = iconTable();
566
569
  const missing = await page.evaluate(injectIcons, table, '0 0 24 24');
567
570
  if (missing.length) {
568
571
  const hint = missing.map((name) => {
@@ -578,7 +581,7 @@ export async function measureHtmlDeck(html, { sourcePath, shotPath, timeoutMs =
578
581
  const count = await page.$$eval('section.slide', (sections) => sections.length);
579
582
  if (!count)
580
583
  throw new Error(
581
- 'The HTML holds no <section class="slide">; every page is one section.slide on a 1920×1080 canvas.'
584
+ `The HTML holds no <section class="slide">; every page is one section.slide on a ${canvas.width}×${canvas.height} canvas.`
582
585
  );
583
586
  const slides = [];
584
587
  const shots = [];
@@ -589,9 +592,9 @@ export async function measureHtmlDeck(html, { sourcePath, shotPath, timeoutMs =
589
592
  await page.evaluate(() => document.fonts.ready);
590
593
  const element = (await page.$$('section.slide'))[index];
591
594
  const box = await element.boundingBox();
592
- if (Math.round(box.width) !== HTML_CANVAS.width || Math.round(box.height) !== HTML_CANVAS.height) {
595
+ if (Math.round(box.width) !== canvas.width || Math.round(box.height) !== canvas.height) {
593
596
  throw new Error(
594
- `slide ${index + 1} measures ${Math.round(box.width)}×${Math.round(box.height)} px; every section.slide is exactly 1920×1080.`
597
+ `slide ${index + 1} measures ${Math.round(box.width)}×${Math.round(box.height)} px; every section.slide is exactly ${canvas.width}×${canvas.height}.`
595
598
  );
596
599
  }
597
600
  const shot = shotPath(index + 1);
@@ -601,7 +604,7 @@ export async function measureHtmlDeck(html, { sourcePath, shotPath, timeoutMs =
601
604
  for (const note of measured.notes) notes.push(`slide ${index + 1}: ${note}`);
602
605
  const findings = await page.evaluate(readGeometry, index, GEOMETRY_TOLERANCE_PX, NEAR_MISS_PX);
603
606
  if (findings.length) geometry.push({ slide: index + 1, findings });
604
- const drawn = await captureRichPaint(page, box, measured.captures);
607
+ const drawn = await captureRichPaint(page, box, measured.captures, canvas);
605
608
  const items = measured.items
606
609
  .map((item) => {
607
610
  if (item.kind !== 'capture') return item;
@@ -611,16 +614,7 @@ export async function measureHtmlDeck(html, { sourcePath, shotPath, timeoutMs =
611
614
  .filter(Boolean);
612
615
  slides.push({ bg: measured.bg, items, notes: measured.speakerNotes });
613
616
  }
614
- return { ...HTML_CANVAS, slides, shots, notes, geometry };
615
- })();
616
- const timeout = new Promise((_, reject) => {
617
- timer = setTimeout(() => reject(new Error(`HTML layout exceeded ${timeoutMs} ms`)), timeoutMs);
618
- });
619
- try {
620
- return await Promise.race([work, timeout]);
621
- } finally {
622
- clearTimeout(timer);
623
- signal?.removeEventListener('abort', abort);
624
- await browser.close().catch(() => {});
625
- }
617
+ return { ...canvas, slides, shots, notes, geometry };
618
+ };
619
+ return runInBrowser(browser, { signal, timeoutMs }, work);
626
620
  }
@@ -41,9 +41,18 @@ function failure(error, startedAt) {
41
41
  };
42
42
  }
43
43
 
44
- export async function runPptxHtmlAuthoring(html, output, { target, signal = null }) {
44
+ /** Whether the HTML is written as pages under the deck contract: one <section class="slide"> per page. */
45
+ export function hasSlideSections(html) {
46
+ return /<section[^>]*class=["'][^"']*\bslide\b/i.test(String(html || ''));
47
+ }
48
+
49
+ /**
50
+ * @param {{ target: string, signal?: AbortSignal, prepare?: (page: object) => Promise<object> }} options
51
+ * prepare: names the canvas on the loaded page (pptx-html-measure.mjs); the deck's 1920 × 1080 without it
52
+ */
53
+ export async function runPptxHtmlAuthoring(html, output, { target, signal = null, prepare = null }) {
45
54
  const source = String(html || '');
46
- if (!/<section[^>]*class=["'][^"']*\bslide\b/i.test(source)) {
55
+ if (!hasSlideSections(source)) {
47
56
  throw new Error('author html requires <section class="slide"> pages; see the pptx skill html reference.');
48
57
  }
49
58
  const startedAt = performance.now();
@@ -52,7 +61,12 @@ export async function runPptxHtmlAuthoring(html, output, { target, signal = null
52
61
  await rm(output, { force: true });
53
62
  let measure;
54
63
  try {
55
- measure = await measureHtmlDeck(source, { sourcePath: artifacts.source, shotPath: artifacts.shot, signal });
64
+ measure = await measureHtmlDeck(source, {
65
+ sourcePath: artifacts.source,
66
+ shotPath: artifacts.shot,
67
+ signal,
68
+ prepare,
69
+ });
56
70
  } catch (error) {
57
71
  if (signal?.aborted) throw error;
58
72
  return failure(error, startedAt);
@@ -70,6 +84,7 @@ export async function runPptxHtmlAuthoring(html, output, { target, signal = null
70
84
  bytes: info.size,
71
85
  logs: measure.notes.map((text) => ({ level: 'warn', text })),
72
86
  kit: 'html',
87
+ slideCount: measure.slides.length,
73
88
  normalizedParagraphs: normalized.removed,
74
89
  htmlShots: measure.shots,
75
90
  htmlText: measure.slides.map((slide) => {
@@ -8,7 +8,13 @@ import { plannedCarrierGaps } from './pptx-brief.mjs';
8
8
  import { isPictureShape, relativeLuminance } from '../design/design-discipline.mjs';
9
9
  import { rectangleGap } from '../portable/pptx-relations.mjs';
10
10
 
11
- const CANVAS_AREA = 960 * 540; // 13.33 × 7.5 in, in points
11
+ // The page the shapes sit on, in points: the deck's 13.33 × 7.5 in unless the document names its slide size
12
+ // (a PDF's designed pages are a sheet).
13
+ const WIDE_CANVAS = Object.freeze({ width: 960, height: 540 });
14
+ const canvasOf = (document) =>
15
+ Number(document?.slideWidth) > 0 && Number(document?.slideHeight) > 0
16
+ ? { width: Number(document.slideWidth), height: Number(document.slideHeight) }
17
+ : WIDE_CANVAS;
12
18
 
13
19
  function backgroundRole(slide) {
14
20
  const l = relativeLuminance(slide?.background?.color);
@@ -27,8 +33,8 @@ function saturation(hex) {
27
33
 
28
34
  // A field that owns the page: a dark or saturated surface over three fifths of the canvas is what a reader
29
35
  // sees as a beat (a cover, a section mark, a claim on a field), whatever the slide's own background says.
30
- function isBeatField(fill, area) {
31
- if (!fill || area < CANVAS_AREA * 0.6) return false;
36
+ function isBeatField(fill, area, canvas) {
37
+ if (!fill || area < canvas.width * canvas.height * 0.6) return false;
32
38
  const l = relativeLuminance(fill);
33
39
  return (l !== null && l < 0.35) || saturation(fill) > 0.35;
34
40
  }
@@ -48,8 +54,6 @@ function surfaceColor(shape) {
48
54
  // Air, quadrant air, the largest object's share, the visual footprint, text
49
55
  // left edges, fill areas, and where the title sits. Read from shape footprints
50
56
  // on a 5 pt raster; no threshold turns any of them into a verdict.
51
- const CANVAS_W = 960;
52
- const CANVAS_H = 540;
53
57
  const CELL = 5;
54
58
 
55
59
  function footprint(shape) {
@@ -72,9 +76,9 @@ function extent(shape) {
72
76
  return { left, top, width, height };
73
77
  }
74
78
 
75
- function raster(shapes) {
76
- const cols = Math.ceil(CANVAS_W / CELL),
77
- rows = Math.ceil(CANVAS_H / CELL);
79
+ function raster(shapes, canvas) {
80
+ const cols = Math.ceil(canvas.width / CELL),
81
+ rows = Math.ceil(canvas.height / CELL);
78
82
  const cells = new Uint8Array(cols * rows);
79
83
  for (const box of shapes) {
80
84
  const x0 = Math.max(0, Math.floor(box.left / CELL)),
@@ -211,20 +215,20 @@ function noteSpec(specs, shape) {
211
215
  // in canvas units [0, 1]; offset is its ellipse-normalized distance from the canvas center with
212
216
  // a horizontal tolerance of 0.05 and a vertical one of 0.15 (a sideways drift reads first, as in
213
217
  // AeSlides' imbalance metric). Both are numbers the author weighs; a breathing slide may sit off center on purpose.
214
- function centroidOf(boxes) {
218
+ function centroidOf(boxes, canvas) {
215
219
  let area = 0,
216
220
  sx = 0,
217
221
  sy = 0;
218
222
  for (const box of boxes) {
219
223
  const a = box.width * box.height;
220
- if (a >= CANVAS_W * CANVAS_H * 0.9) continue;
224
+ if (a >= canvas.width * canvas.height * 0.9) continue;
221
225
  area += a;
222
226
  sx += a * (box.left + box.width / 2);
223
227
  sy += a * (box.top + box.height / 2);
224
228
  }
225
229
  if (!area) return null;
226
- const x = sx / area / CANVAS_W,
227
- y = sy / area / CANVAS_H;
230
+ const x = sx / area / canvas.width,
231
+ y = sy / area / canvas.height;
228
232
  const offset = Math.sqrt(((x - 0.5) / 0.05) ** 2 + ((y - 0.5) / 0.15) ** 2);
229
233
  return { centroid: [Number(x.toFixed(2)), Number(y.toFixed(2))], centroidOffset: Number(offset.toFixed(1)) };
230
234
  }
@@ -233,16 +237,17 @@ function centroidOf(boxes) {
233
237
  // content zone begins. Read across the deck it shows whether content slides share one body top.
234
238
  // Body fill: how much of the zone under the title (down to the lower safe margin) the content spans,
235
239
  // as a share — a dense slide whose content stops halfway reads as a hollow; a breathing slide is meant to.
236
- function bodyTopOf(boxes, titleBox) {
240
+ function bodyTopOf(boxes, titleBox, canvas) {
237
241
  if (!titleBox) return { bodyTop: null, bodyFill: null };
238
242
  const titleBottom = titleBox.top + titleBox.height;
239
243
  const below = boxes.filter(
240
- (box) => box !== titleBox && box.top >= titleBottom - 2 && box.width * box.height < CANVAS_W * CANVAS_H * 0.9
244
+ (box) =>
245
+ box !== titleBox && box.top >= titleBottom - 2 && box.width * box.height < canvas.width * canvas.height * 0.9
241
246
  );
242
247
  if (!below.length) return { bodyTop: null, bodyFill: null };
243
248
  const top = Math.min(...below.map((box) => box.top)),
244
249
  bottom = Math.max(...below.map((box) => box.top + box.height));
245
- const zone = CANVAS_H - 0.5 * 72 - top;
250
+ const zone = canvas.height - 0.5 * 72 - top;
246
251
  return {
247
252
  bodyTop: Number((top / 72).toFixed(2)),
248
253
  bodyFill: zone > 0 ? Number(Math.min(1, (bottom - top) / zone).toFixed(2)) : null,
@@ -253,8 +258,8 @@ function bodyTopOf(boxes, titleBox) {
253
258
  // fieldFill is the share of each field of 6% of the canvas or more (a full-page background is not one)
254
259
  // that content actually covers, lowest first. Neither `air` nor `quadrantAir` can say this — the field's
255
260
  // own footprint fills the very quadrant it leaves empty (user: 이 사각은 밸런스가 망가진 것 같은데).
256
- function fieldFillOf(surfaces, grid) {
257
- const canvas = CANVAS_W * CANVAS_H;
261
+ function fieldFillOf(surfaces, grid, page) {
262
+ const canvas = page.width * page.height;
258
263
  return surfaces
259
264
  .filter((box) => box.width * box.height >= canvas * 0.06 && box.width * box.height < canvas * 0.9)
260
265
  .map(
@@ -275,27 +280,28 @@ function fieldFillOf(surfaces, grid) {
275
280
 
276
281
  function observe(
277
282
  shapes,
278
- { textBoxes, visuals, content, blocks, surfaces = [], constructs = [], labels = [], fills, titleBox }
283
+ { textBoxes, visuals, content, blocks, surfaces = [], constructs = [], labels = [], fills, titleBox, page }
279
284
  ) {
280
285
  const boxes = shapes.map(footprint).filter(Boolean);
281
286
  if (!boxes.length) return null;
282
287
  const authored = shapes.filter((shape) => !isChrome(shape));
283
288
  const typeSet = [...new Set(authored.flatMap(typeSizesOf))].sort((a, b) => a - b);
284
289
  const textColors = [...new Set(authored.filter((shape) => String(shape.text || '').trim()).flatMap(textColorsOf))];
285
- const all = raster(boxes);
286
- const inner = raster(content); // the same canvas read from content alone: where a surface is carrying nothing
287
- const centroid = centroidOf(boxes);
290
+ const all = raster(boxes, page);
291
+ const inner = raster(content, page); // the same canvas read from content alone: where a surface is carrying nothing
292
+ const centroid = centroidOf(boxes, page);
288
293
  // Body top and gaps read the content (text, charts, tables, pictures, contours), never a surface field or a rule.
289
294
  const { bodyTop, bodyFill } = bodyTopOf(
290
295
  content,
291
296
  titleBox
292
297
  ? content.find((box) => box.left === titleBox.left && box.top === titleBox.top && box.width === titleBox.width) ||
293
298
  titleBox
294
- : null
299
+ : null,
300
+ page
295
301
  );
296
302
  const midX = Math.floor(all.cols / 2),
297
303
  midY = Math.floor(all.rows / 2);
298
- const canvas = CANVAS_W * CANVAS_H;
304
+ const canvas = page.width * page.height;
299
305
  const fillShares = [...fills.entries()]
300
306
  .map(([color, area]) => ({ color, share: Number((area / canvas).toFixed(2)) }))
301
307
  .filter((entry) => entry.share > 0)
@@ -327,9 +333,9 @@ function observe(
327
333
  airOf(inner, 0, midY, midX, inner.rows),
328
334
  airOf(inner, midX, midY, inner.cols, inner.rows),
329
335
  ],
330
- fieldFill: fieldFillOf(surfaces, inner),
336
+ fieldFill: fieldFillOf(surfaces, inner, page),
331
337
  largestShare: Number((Math.max(...boxes.map((box) => box.width * box.height)) / canvas).toFixed(2)),
332
- visualShare: visuals.length ? Number((1 - airOf(raster(visuals))).toFixed(2)) : 0,
338
+ visualShare: visuals.length ? Number((1 - airOf(raster(visuals, page))).toFixed(2)) : 0,
333
339
  presence,
334
340
  textColumns: { ...leftEdges(textBoxes.filter((box) => !labels.includes(box))), labels: labels.length },
335
341
  fills: fillShares,
@@ -343,7 +349,7 @@ function observe(
343
349
  };
344
350
  }
345
351
 
346
- export function slideReceipt(slide) {
352
+ export function slideReceipt(slide, page = WIDE_CANVAS) {
347
353
  const shapes = Array.isArray(slide?.shapes) ? slide.shapes : [];
348
354
  const receipt = {
349
355
  slide: Number(slide?.index) || 0,
@@ -362,10 +368,10 @@ export function slideReceipt(slide) {
362
368
  coverage: 0,
363
369
  grammar: 'text',
364
370
  };
365
- const read = readShapes(shapes, receipt);
371
+ const read = readShapes(shapes, receipt, page);
366
372
  receipt.presets = [...read.presets];
367
373
  if (Object.keys(read.specs).length) receipt.specs = read.specs;
368
- receipt.coverage = Math.min(1, Number((read.covered / CANVAS_AREA).toFixed(2)));
374
+ receipt.coverage = Math.min(1, Number((read.covered / (page.width * page.height)).toFixed(2)));
369
375
  receipt.grammar = pageGrammar(receipt, read);
370
376
  const observed = observe(read.seen, {
371
377
  textBoxes: read.textBoxes,
@@ -377,6 +383,7 @@ export function slideReceipt(slide) {
377
383
  labels: diagramLabels(read.textBoxes, read.constructs),
378
384
  fills: read.fills,
379
385
  titleBox: read.titleBox,
386
+ page,
380
387
  });
381
388
  if (observed) receipt.observe = observed;
382
389
  return receipt;
@@ -388,7 +395,7 @@ export function slideReceipt(slide) {
388
395
  * content, spacing blocks, surfaces, drawn constructs — plus the covered
389
396
  * area, the fill weights and the largest text box.
390
397
  */
391
- function readShapes(shapes, receipt) {
398
+ function readShapes(shapes, receipt, page) {
392
399
  let covered = 0;
393
400
  let beatField = false;
394
401
  const presets = new Set();
@@ -481,7 +488,7 @@ function readShapes(shapes, receipt) {
481
488
  receipt.fields += 1;
482
489
  covered += area;
483
490
  if (box && fill) surfaces.push(box);
484
- if (isBeatField(fill, area)) beatField = true;
491
+ if (isBeatField(fill, area, page)) beatField = true;
485
492
  } else if (geometry) {
486
493
  presets.add(geometry);
487
494
  covered += area;
@@ -540,7 +547,8 @@ function diagramLabels(textBoxes, constructs) {
540
547
  // The whole deck: per-slide receipts, totals, the families that never appear,
541
548
  // and the plan lines whose named carriers the snapshot cannot see.
542
549
  export function compositionReceipt(document, brief = null) {
543
- const slides = (Array.isArray(document?.slides) ? document.slides : []).map(slideReceipt);
550
+ const page = canvasOf(document);
551
+ const slides = (Array.isArray(document?.slides) ? document.slides : []).map((slide) => slideReceipt(slide, page));
544
552
  const deck = {
545
553
  slides: slides.length,
546
554
  charts: 0,
@@ -21,7 +21,15 @@ export async function pptxReviewArtifacts(session, preview) {
21
21
  // The receipt reads an authored deck against the brief it was written to. A deck opened for editing has no
22
22
  // brief, and its pages are reviewed as edits, so its render carries the pages alone — a template's receipt
23
23
  // ran to fifteen thousand characters beside three page images.
24
- const receipt = session.authored === true ? await readCompositionReceipt(session) : null;
24
+ // A PDF of designed pages keeps the receipt its working deck was read into at author time (the PDF
25
+ // itself holds no shapes); each render reads its own copy against the printed pixels.
26
+ const receipt = session.authoredFrame
27
+ ? session.frameReceipt
28
+ ? structuredClone(session.frameReceipt)
29
+ : null
30
+ : session.authored === true
31
+ ? await readCompositionReceipt(session)
32
+ : null;
25
33
  if (receipt) {
26
34
  const air = await renderedAirByPage(result._images).catch(() => null);
27
35
  if (air) attachRenderedAir(receipt, air);
@@ -98,7 +98,11 @@ async function pathExists(path) {
98
98
  }
99
99
  }
100
100
 
101
- export async function runPptxAuthoringScript(script, output, { timeoutMs = PPTX_SCRIPT_CONTRACT.timeoutMs } = {}) {
101
+ export async function runPptxAuthoringScript(
102
+ script,
103
+ output,
104
+ { timeoutMs = PPTX_SCRIPT_CONTRACT.timeoutMs, cwd = dirname(output) } = {}
105
+ ) {
102
106
  const source = String(script || '');
103
107
  if (!source.trim()) throw new Error('author requires a non-empty pptxgenjs script');
104
108
  await mkdir(dirname(output), { recursive: true });
@@ -126,7 +130,7 @@ export async function runPptxAuthoringScript(script, output, { timeoutMs = PPTX_
126
130
  } catch (error) {
127
131
  return { ok: false, error: scriptError(error, source, preludeLines), logs, elapsedMs: 0 };
128
132
  }
129
- const scopedProcess = { env: { ...process.env }, cwd: () => dirname(output), platform: process.platform };
133
+ const scopedProcess = { env: { ...process.env }, cwd: () => cwd, platform: process.platform };
130
134
  let timer = null;
131
135
  const timeout = new Promise((_, reject) => {
132
136
  timer = setTimeout(() => reject(new Error(`Authoring script exceeded ${timeoutMs} ms`)), timeoutMs);