mixdog 1.0.3 → 1.0.5

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 (160) hide show
  1. package/README.md +10 -5
  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/goal-management/SKILL.md +2 -2
  15. package/src/defaults/skills/pdf/SKILL.md +10 -2
  16. package/src/defaults/skills/pdf/references/html.md +64 -0
  17. package/src/defaults/skills/pptx/references/html.md +1 -1
  18. package/src/defaults/skills/xlsx/SKILL.md +7 -3
  19. package/src/defaults/skills/xlsx/references/html.md +40 -0
  20. package/src/defaults/skills/xlsx/references/report-design.md +21 -4
  21. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +1 -1
  22. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -3
  23. package/src/runtime/agent/orchestrator/internal-agents.mjs +1 -1
  24. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +6 -5
  25. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-defs.mjs +1 -1
  26. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +6 -3
  27. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +6 -2
  28. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +50 -9
  29. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +28 -1
  30. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +4 -1
  31. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +34 -8
  32. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +10 -2
  33. package/src/runtime/agent/orchestrator/session/transcript-restore/agent-envelope.mjs +1 -1
  34. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +1 -0
  35. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +3 -1
  36. package/src/runtime/attachments/pdf-extract.mjs +7 -3
  37. package/src/runtime/computer-bridge/client.mjs +1 -1
  38. package/src/runtime/memory/index.mjs +4 -15
  39. package/src/runtime/memory/lib/core-memory-store.mjs +1 -1
  40. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +0 -2
  41. package/src/runtime/memory/lib/cycle-scheduler/backlog-probe.mjs +3 -11
  42. package/src/runtime/memory/lib/cycle-scheduler/health-ledger.mjs +1 -3
  43. package/src/runtime/memory/lib/cycle-scheduler/scheduled-enqueue.mjs +1 -11
  44. package/src/runtime/memory/lib/cycle-scheduler/tick-loop.mjs +0 -2
  45. package/src/runtime/memory/lib/cycle-scheduler.mjs +4 -13
  46. package/src/runtime/memory/lib/cycle-signatures.mjs +0 -6
  47. package/src/runtime/memory/lib/cycle1/cycle1-rows.mjs +1 -1
  48. package/src/runtime/memory/lib/cycle1/cycle1-window.mjs +2 -2
  49. package/src/runtime/memory/lib/embedding-reindex.mjs +1 -1
  50. package/src/runtime/memory/lib/http-router/lifecycle-routes.mjs +0 -1
  51. package/src/runtime/memory/lib/memory-action-handlers/cycle-actions.mjs +7 -52
  52. package/src/runtime/memory/lib/memory-action-handlers/cycle-backfill-action.mjs +6 -22
  53. package/src/runtime/memory/lib/memory-action-handlers/cycle-rebuild-action.mjs +10 -17
  54. package/src/runtime/memory/lib/memory-action-handlers/maintenance-actions.mjs +1 -3
  55. package/src/runtime/memory/lib/memory-action-handlers/manage-actions.mjs +2 -4
  56. package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -4
  57. package/src/runtime/memory/lib/memory-cycle-requests.mjs +1 -1
  58. package/src/runtime/memory/lib/{memory-cycle2-shared.mjs → memory-cycle-shared.mjs} +14 -16
  59. package/src/runtime/memory/lib/memory-cycle.mjs +3 -2
  60. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
  61. package/src/runtime/memory/lib/memory-embed.mjs +14 -1
  62. package/src/runtime/memory/lib/memory-ops-policy.mjs +8 -11
  63. package/src/runtime/memory/lib/memory-schema/entries.mjs +0 -1
  64. package/src/runtime/memory/lib/memory.mjs +2 -6
  65. package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
  66. package/src/runtime/memory/lib/query-handlers.mjs +1 -1
  67. package/src/runtime/memory/lib/query-hybrid-search.mjs +1 -1
  68. package/src/runtime/memory/lib/query-maintenance-handlers.mjs +0 -6
  69. package/src/runtime/memory/lib/recall-format.mjs +1 -1
  70. package/src/runtime/office/authoring/docx-html-build.mjs +375 -0
  71. package/src/runtime/office/authoring/docx-html-measure.mjs +561 -0
  72. package/src/runtime/office/authoring/docx-html-runner.mjs +102 -0
  73. package/src/runtime/office/authoring/html-browser.mjs +61 -0
  74. package/src/runtime/office/authoring/html-document-author.mjs +181 -0
  75. package/src/runtime/office/authoring/html-source-drift.mjs +36 -0
  76. package/src/runtime/office/authoring/pdf-author-action.mjs +266 -0
  77. package/src/runtime/office/authoring/pdf-html-charts.mjs +379 -0
  78. package/src/runtime/office/authoring/pdf-html-runner.mjs +317 -0
  79. package/src/runtime/office/authoring/pptx-author-action.mjs +44 -22
  80. package/src/runtime/office/authoring/pptx-author-session.mjs +12 -6
  81. package/src/runtime/office/authoring/pptx-html-build.mjs +23 -4
  82. package/src/runtime/office/authoring/pptx-html-measure.mjs +38 -44
  83. package/src/runtime/office/authoring/pptx-html-runner.mjs +18 -3
  84. package/src/runtime/office/authoring/pptx-receipt.mjs +39 -31
  85. package/src/runtime/office/authoring/pptx-review-artifacts.mjs +9 -1
  86. package/src/runtime/office/authoring/pptx-script-runner.mjs +6 -2
  87. package/src/runtime/office/authoring/xlsx-html-build.mjs +365 -0
  88. package/src/runtime/office/authoring/xlsx-html-measure.mjs +257 -0
  89. package/src/runtime/office/authoring/xlsx-html-runner.mjs +139 -0
  90. package/src/runtime/office/capabilities-catalog.mjs +8 -0
  91. package/src/runtime/office/capabilities-signatures.mjs +18 -6
  92. package/src/runtime/office/capabilities.mjs +26 -1
  93. package/src/runtime/office/com/office-com-host.ps1 +133 -17
  94. package/src/runtime/office/core/office-actions-batch.mjs +16 -1
  95. package/src/runtime/office/core/office-actions-inspect.mjs +3 -2
  96. package/src/runtime/office/core/office-actions-lifecycle.mjs +6 -2
  97. package/src/runtime/office/core/office-actions-render.mjs +4 -1
  98. package/src/runtime/office/core/office-finalize/review-stage.mjs +15 -2
  99. package/src/runtime/office/core/office-qa/design-review-stage.mjs +9 -0
  100. package/src/runtime/office/core/office-session-dispatch.mjs +3 -0
  101. package/src/runtime/office/core/office-sessionless-actions.mjs +7 -2
  102. package/src/runtime/office/core/office-sessions.mjs +19 -2
  103. package/src/runtime/office/pdf/pdf-writer.mjs +1 -1
  104. package/src/runtime/office/portable/docx-formatting.mjs +21 -0
  105. package/src/runtime/office/portable/ooxml-validator.mjs +22 -1
  106. package/src/runtime/office/portable/portable-chart.mjs +10 -2
  107. package/src/runtime/office/portable/portable-docx-edits.mjs +12 -6
  108. package/src/runtime/office/portable/portable-docx-operations.mjs +75 -1
  109. package/src/runtime/office/portable/portable-docx-xml.mjs +49 -6
  110. package/src/runtime/office/portable/portable-docx.mjs +2 -0
  111. package/src/runtime/office/portable/portable-snapshot-docx.mjs +2 -0
  112. package/src/runtime/office/portable/portable-validation.mjs +6 -3
  113. package/src/runtime/office/portable/portable-xlsx-cell-values.mjs +87 -0
  114. package/src/runtime/office/portable/portable-xlsx-charts.mjs +55 -13
  115. package/src/runtime/office/portable/portable-xlsx-sheet-edits.mjs +1 -1
  116. package/src/runtime/office/portable/xlsx-contract.mjs +82 -3
  117. package/src/runtime/office/quality/assurance-checklist.mjs +2 -2
  118. package/src/runtime/office/quality/assurance-rendered.mjs +27 -1
  119. package/src/runtime/office/quality/assurance-structure-docx.mjs +44 -0
  120. package/src/runtime/office/quality/assurance-structure-pptx.mjs +24 -6
  121. package/src/runtime/office/quality/assurance-structure-xlsx.mjs +50 -1
  122. package/src/runtime/office/quality/design-review-critique.mjs +3 -2
  123. package/src/runtime/office/quality/document-brief.mjs +44 -0
  124. package/src/runtime/office/quality/inline-audit.mjs +7 -2
  125. package/src/runtime/office/quality/quality-pipeline.mjs +9 -1
  126. package/src/runtime/office/tool-defs.mjs +2 -2
  127. package/src/runtime/shared/llm/quota-value-estimate.mjs +317 -0
  128. package/src/runtime/shared/llm/usage-ledger-quota.mjs +266 -63
  129. package/src/runtime/shared/llm/usage-ledger-rollup.mjs +35 -0
  130. package/src/runtime/shared/llm/usage-ledger-worker.mjs +2 -1
  131. package/src/runtime/shared/llm/usage-ledger.mjs +10 -1
  132. package/src/runtime/shared/provider-accounts.mjs +11 -1
  133. package/src/runtime/shared/statusline/statusline-segments.mjs +4 -4
  134. package/src/runtime/shared/tool-execution-contract.mjs +1 -1
  135. package/src/runtime/shared/wait-until.test-support.mjs +33 -0
  136. package/src/session-runtime/boot/apis.mjs +1 -1
  137. package/src/session-runtime/services/channel-admin.mjs +4 -3
  138. package/src/session-runtime/services/usage-stats-model.mjs +22 -0
  139. package/src/session-runtime/setup-tool/settings-contract.mjs +6 -1
  140. package/src/session-runtime/turn/session-ops.mjs +15 -9
  141. package/src/session-runtime/usage-stats-api.mjs +34 -2
  142. package/src/standalone/agent-dispatch-broker.mjs +1 -1
  143. package/src/standalone/local-session-runtime.mjs +488 -0
  144. package/src/standalone/session-protocol.mjs +1 -0
  145. package/src/standalone/session-runtime-inline-host.mjs +1 -1
  146. package/src/tui/hooks/useSession.mjs +1 -1
  147. package/src/tui/session/session-api/commands.mjs +1 -1
  148. package/src/tui/session/session-api/integrations.mjs +3 -0
  149. package/src/tui/session/session-flow/auto-clear.mjs +6 -7
  150. package/src/tui/session/tool-card-results.mjs +1 -1
  151. package/src/tui/session/turn.mjs +5 -0
  152. package/src/tui/session-local.mjs +3 -488
  153. package/src/tui/session.mjs +1 -1
  154. package/src/ui/statusline-agents.mjs +0 -1
  155. package/src/rules/agent/41-cycle2-agent.md +0 -24
  156. package/src/runtime/memory/lib/cycle-scheduler/cycle2-runs.mjs +0 -120
  157. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +0 -110
  158. package/src/runtime/memory/lib/memory-cycle2-quarantine.mjs +0 -99
  159. package/src/runtime/memory/lib/memory-cycle2-review.mjs +0 -240
  160. package/src/runtime/memory/lib/memory-cycle2.mjs +0 -191
@@ -0,0 +1,317 @@
1
+ // The `author` action's PDF route: a local Chrome/Edge lays an HTML document
2
+ // out under its own @page rules and prints it. Before printing, the document
3
+ // is laid out at the printable width and read for what print would silently
4
+ // cut or move — a block wider than the page, a picture that never loaded —
5
+ // so the file lands only when it holds. Designed pages (section.slide on the
6
+ // sheet) are measured and gated as a deck first (pdf-author-action.mjs) and
7
+ // printed here one page per sheet.
8
+ import { readFile, rm, stat, writeFile } from 'node:fs/promises';
9
+ import { pathToFileURL } from 'node:url';
10
+ import { PDFDict, PDFDocument, PDFHexString, PDFName } from 'pdf-lib';
11
+
12
+ import { htmlFailure, launchHtmlBrowser, runInBrowser } from './html-browser.mjs';
13
+ import { applyDocumentProperties } from '../pdf/pdf-writer.mjs';
14
+ import { iconTable, injectIcons } from './pptx-html-measure.mjs';
15
+ import { drawDataCharts } from './pdf-html-charts.mjs';
16
+
17
+ // Runs inside the page: every h1-h3 as the browser names it in the outline (its text with the line
18
+ // breaks dropped) and as a reader sees it (a <br> read as a space).
19
+ function headingTitles() {
20
+ return [...document.querySelectorAll('h1, h2, h3')].map((heading) => ({
21
+ raw: heading.textContent || '',
22
+ shown: (heading.innerText || '').replace(/\s+/g, ' ').trim(),
23
+ }));
24
+ }
25
+
26
+ // The browser names a bookmark after its heading's text with the line breaks dropped, so a title set
27
+ // on two lines ("…틀로<br>설계하고") reads "…틀로설계하고". Each such outline entry takes the heading
28
+ // as shown. Returns how many titles changed.
29
+ function repairOutlineTitles(document, headings) {
30
+ const key = (text) => String(text || '').replace(/\s+/g, '');
31
+ const shown = new Map();
32
+ for (const heading of headings) if (heading.shown) shown.set(key(heading.raw), heading.shown);
33
+ const outlines = document.catalog.lookupMaybe(PDFName.of('Outlines'), PDFDict);
34
+ let changed = 0;
35
+ const seen = new Set();
36
+ const walk = (node) => {
37
+ while (node && !seen.has(node)) {
38
+ seen.add(node);
39
+ const title = node.get(PDFName.of('Title'));
40
+ const text = typeof title?.decodeText === 'function' ? title.decodeText() : '';
41
+ const fixed = shown.get(key(text));
42
+ if (fixed && fixed !== text) {
43
+ node.set(PDFName.of('Title'), PDFHexString.fromText(fixed));
44
+ changed += 1;
45
+ }
46
+ walk(node.lookupMaybe(PDFName.of('First'), PDFDict));
47
+ node = node.lookupMaybe(PDFName.of('Next'), PDFDict);
48
+ }
49
+ };
50
+ walk(outlines?.lookupMaybe(PDFName.of('First'), PDFDict));
51
+ return changed;
52
+ }
53
+
54
+ /** The HTML as laid out, kept beside the PDF so relative <img> paths resolve and a re-author starts from it. */
55
+ export function pdfHtmlSourcePath(target) {
56
+ return `${target}.mixdog-source.html`;
57
+ }
58
+
59
+ // CSS px per unit (96 px to the inch) and the named sheets in millimetres, portrait.
60
+ const PX_PER_UNIT = { px: 1, in: 96, cm: 96 / 2.54, mm: 96 / 25.4, q: 96 / 101.6, pt: 96 / 72, pc: 16 };
61
+ const NAMED_SHEETS_MM = {
62
+ a3: [297, 420],
63
+ a4: [210, 297],
64
+ a5: [148, 210],
65
+ b4: [250, 353],
66
+ b5: [176, 250],
67
+ letter: [215.9, 279.4],
68
+ legal: [215.9, 355.6],
69
+ ledger: [279.4, 431.8],
70
+ };
71
+
72
+ function lengthPx(token, what) {
73
+ const match = /^(-?\d*\.?\d+)([a-z]+)?$/i.exec(String(token).trim());
74
+ const unit = (match?.[2] || (Number(match?.[1]) === 0 ? 'px' : '')).toLowerCase();
75
+ if (!match || !PX_PER_UNIT[unit]) {
76
+ throw new Error(`@page ${what} "${token}" is not a length; write it in mm, cm, in, pt, or px.`);
77
+ }
78
+ return Number(match[1]) * PX_PER_UNIT[unit];
79
+ }
80
+
81
+ /** The printable area in CSS px from the @page size and margins the document declared. */
82
+ export function printableArea(rule) {
83
+ const tokens = String(rule?.size || '')
84
+ .trim()
85
+ .toLowerCase()
86
+ .split(/\s+/)
87
+ .filter(Boolean);
88
+ if (!tokens.length || tokens[0] === 'auto') {
89
+ throw new Error(
90
+ 'The document declares no @page size; open the stylesheet with `@page { size: A4; margin: 18mm }` (or letter, A5 landscape, 148mm 210mm).'
91
+ );
92
+ }
93
+ let landscape = false;
94
+ const lengths = [];
95
+ let named = null;
96
+ for (const token of tokens) {
97
+ if (token === 'landscape') landscape = true;
98
+ else if (token === 'portrait') landscape = false;
99
+ else if (NAMED_SHEETS_MM[token]) named = NAMED_SHEETS_MM[token].map((mm) => mm * PX_PER_UNIT.mm);
100
+ else lengths.push(lengthPx(token, 'size'));
101
+ }
102
+ let [width, height] = named || [lengths[0], lengths[1] ?? lengths[0]];
103
+ if (!(width > 0 && height > 0)) throw new Error(`@page size "${rule.size}" names no sheet.`);
104
+ if (named && landscape) [width, height] = [height, width];
105
+ const margin = Object.fromEntries(
106
+ ['top', 'right', 'bottom', 'left'].map((side) => [
107
+ side,
108
+ rule.margin?.[side] ? lengthPx(rule.margin[side], `margin-${side}`) : 0,
109
+ ])
110
+ );
111
+ return {
112
+ sheet: { width, height },
113
+ margin,
114
+ width: width - margin.left - margin.right,
115
+ height: height - margin.top - margin.bottom,
116
+ };
117
+ }
118
+
119
+ // Runs inside the page: the unnamed @page rules that apply to print, later declarations winning.
120
+ export function readPageRule() {
121
+ const out = { size: '', margin: {} };
122
+ const visit = (list) => {
123
+ for (const rule of list) {
124
+ if (rule instanceof CSSPageRule && !rule.selectorText) {
125
+ const size = rule.style.getPropertyValue('size');
126
+ if (size) out.size = size;
127
+ for (const side of ['top', 'right', 'bottom', 'left']) {
128
+ const value = rule.style.getPropertyValue(`margin-${side}`);
129
+ if (value) out.margin[side] = value;
130
+ }
131
+ } else if (rule instanceof CSSMediaRule && window.matchMedia(rule.conditionText).matches) {
132
+ visit(rule.cssRules);
133
+ }
134
+ }
135
+ };
136
+ for (const sheet of document.styleSheets) {
137
+ try {
138
+ visit(sheet.cssRules);
139
+ } catch {}
140
+ }
141
+ return out;
142
+ }
143
+
144
+ /**
145
+ * The canvas of a PDF's designed pages (the deck contract, pptx-html-measure.mjs): the sheet the document
146
+ * declares, printed full-bleed, in CSS px at 96 to the inch. Runs on the page the measure has loaded.
147
+ */
148
+ export async function frameCanvas(page) {
149
+ await page.emulateMediaType('print');
150
+ const area = printableArea(await page.evaluate(readPageRule));
151
+ if (Object.values(area.margin).some((margin) => margin > 0.5)) {
152
+ throw new Error(
153
+ 'Designed pages print full-bleed: declare `@page { size: A4; margin: 0 }` and give each section.slide the whole sheet (width: 210mm; height: 297mm), with the margins inside it.'
154
+ );
155
+ }
156
+ // The viewport takes whole px; the working deck is sized from those same px so 1 px stays exactly
157
+ // 0.75 pt (A4's 793.7 px rounded to 794 is 0.2 pt wider, and a 22 pt title stays 22 pt).
158
+ const width = Math.round(area.sheet.width);
159
+ const height = Math.round(area.sheet.height);
160
+ return { width, height, inchWidth: width / 96, inchHeight: height / 96 };
161
+ }
162
+
163
+ // Designed pages print one section.slide per sheet whatever the screen CSS does; speaker notes stay off the page.
164
+ const FRAME_PRINT_CSS = `section.slide { break-after: page; break-inside: avoid; }
165
+ section.slide:last-of-type { break-after: auto; }
166
+ aside.notes { display: none !important; }`;
167
+
168
+ // Runs inside the page, laid out at the printable width under print media.
169
+ // Errors refuse the document; warnings ride on the result. Designed pages
170
+ // are measured on their canvas before printing, so only their pictures are read here.
171
+ function measurePrintLayout(area, frame) {
172
+ const describe = (element) => {
173
+ const id = element.id ? `#${element.id}` : '';
174
+ const className = typeof element.className === 'string' ? element.className.trim() : '';
175
+ const classes = className ? `.${className.split(/\s+/).join('.')}` : '';
176
+ const text = (element.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 40);
177
+ return `<${element.localName}${id}${classes}>${text ? ` "${text}"` : ''}`;
178
+ };
179
+ const px = (value) => Math.round(value);
180
+ const findings = [];
181
+ for (const image of document.images) {
182
+ const src = image.getAttribute('src') || '';
183
+ if (/^(?:https?:)?\/\//i.test(src)) {
184
+ findings.push({
185
+ code: 'remote_image',
186
+ severity: 'error',
187
+ element: describe(image),
188
+ message: `${src} is a web address; save the picture beside the document or inline it as a data: URI.`,
189
+ });
190
+ } else if (!image.complete || image.naturalWidth === 0) {
191
+ findings.push({
192
+ code: 'image_missing',
193
+ severity: 'error',
194
+ element: describe(image),
195
+ message: `${src || '(no src)'} did not load; the path resolves from the PDF's folder.`,
196
+ });
197
+ }
198
+ }
199
+ if (frame) return { findings };
200
+ const clipped = (element) => {
201
+ for (let parent = element.parentElement; parent && parent !== document.body; parent = parent.parentElement) {
202
+ if (getComputedStyle(parent).overflowX !== 'visible') return true;
203
+ }
204
+ return false;
205
+ };
206
+ const outside = (box) => box.right > area.width + 1 || box.left < -1;
207
+ const tall = new Set();
208
+ for (const element of document.body.querySelectorAll('*')) {
209
+ const style = getComputedStyle(element);
210
+ if (style.display === 'none' || style.position === 'fixed') continue;
211
+ const box = element.getBoundingClientRect();
212
+ if (!box.width && !box.height) continue;
213
+ const parent = element.parentElement;
214
+ if (
215
+ outside(box) &&
216
+ !clipped(element) &&
217
+ !(parent && parent !== document.body && outside(parent.getBoundingClientRect()))
218
+ ) {
219
+ findings.push({
220
+ code: 'overflow_x',
221
+ severity: 'error',
222
+ element: describe(element),
223
+ message: `spans ${px(box.left)}–${px(box.right)} px of a ${px(area.width)} px printable width; print cuts what lies past the margin.`,
224
+ });
225
+ }
226
+ const unbreakable =
227
+ ['img', 'svg', 'canvas', 'video'].includes(element.localName) ||
228
+ ['avoid', 'avoid-page'].includes(style.breakInside);
229
+ if (unbreakable && box.height > area.height + 1) {
230
+ let ancestor = element.parentElement;
231
+ while (ancestor && !tall.has(ancestor)) ancestor = ancestor.parentElement;
232
+ tall.add(element);
233
+ if (!ancestor) {
234
+ findings.push({
235
+ code: 'unbreakable_too_tall',
236
+ severity: 'warning',
237
+ element: describe(element),
238
+ message: `is ${px(box.height)} px tall and may not break, but a page holds ${px(area.height)} px; print splits it anyway. Shorten it or let it break.`,
239
+ });
240
+ }
241
+ }
242
+ }
243
+ return { findings };
244
+ }
245
+
246
+ /**
247
+ * Lays out and prints an HTML document to `output`.
248
+ * @param {string} html the document
249
+ * @param {string} output where the PDF is written (a staging file beside the target)
250
+ * @param {{ sourcePath: string, properties?: object, frame?: boolean, timeoutMs?: number, signal?: AbortSignal }} options
251
+ * frame: designed pages (section.slide on the sheet, already measured): one page per sheet, icons drawn
252
+ * @returns {Promise<object>} `{ ok:true, output, bytes, pageCount, area, warnings }`, or
253
+ * `{ ok:false, error }` / `{ ok:false, layout }` when the browser failed or the layout check refused it
254
+ */
255
+ export async function printHtmlPdf(
256
+ html,
257
+ output,
258
+ { sourcePath, properties = {}, frame = false, timeoutMs = 90_000, signal = null }
259
+ ) {
260
+ const startedAt = performance.now();
261
+ await writeFile(sourcePath, html, 'utf8');
262
+ await rm(output, { force: true });
263
+ let browser;
264
+ try {
265
+ browser = await launchHtmlBrowser('pdf-html-browser');
266
+ } catch (error) {
267
+ return htmlFailure(error, startedAt);
268
+ }
269
+ const work = async () => {
270
+ const page = await browser.newPage();
271
+ await page.emulateMediaType('print');
272
+ await page.goto(pathToFileURL(sourcePath).href, { waitUntil: 'load' });
273
+ const area = printableArea(await page.evaluate(readPageRule));
274
+ if (frame) {
275
+ await page.evaluate(injectIcons, iconTable().table, '0 0 24 24');
276
+ await page.addStyleTag({ content: FRAME_PRINT_CSS });
277
+ }
278
+ await page.setViewport({ width: Math.round(area.width), height: Math.round(area.height), deviceScaleFactor: 1 });
279
+ await page.evaluate(() => document.fonts.ready);
280
+ // Charts are drawn at the size print lays them out, before the layout is read for what print would cut.
281
+ const charts = await page.evaluate(drawDataCharts);
282
+ const measured = await page.evaluate(measurePrintLayout, { width: area.width, height: area.height }, frame);
283
+ const errors = measured.findings.filter((finding) => finding.severity === 'error');
284
+ if (errors.length) return { refused: errors, area };
285
+ const headings = await page.evaluate(headingTitles);
286
+ await page.pdf({ path: output, preferCSSPageSize: true, printBackground: true, tagged: true, outline: true });
287
+ return {
288
+ area,
289
+ headings,
290
+ charts: charts.drawn,
291
+ warnings: [...charts.warnings, ...measured.findings.filter((finding) => finding.severity !== 'error')],
292
+ };
293
+ };
294
+ let printed;
295
+ try {
296
+ printed = await runInBrowser(browser, { signal, timeoutMs }, work);
297
+ } catch (error) {
298
+ if (signal?.aborted) throw error;
299
+ return htmlFailure(error, startedAt);
300
+ }
301
+ if (printed.refused)
302
+ return htmlFailure(new Error('layout check refused the document'), startedAt, { layout: printed.refused });
303
+ const document = await PDFDocument.load(await readFile(output));
304
+ const described = ['title', 'author', 'subject', 'keywords'].some((key) => properties?.[key] != null);
305
+ if (described) applyDocumentProperties(document, properties);
306
+ if (repairOutlineTitles(document, printed.headings) || described) await writeFile(output, await document.save());
307
+ return {
308
+ ok: true,
309
+ output,
310
+ bytes: (await stat(output)).size,
311
+ pageCount: document.getPageCount(),
312
+ area: { width: Math.round(printed.area.width), height: Math.round(printed.area.height) },
313
+ ...(printed.charts ? { charts: printed.charts } : {}),
314
+ warnings: printed.warnings,
315
+ elapsedMs: Math.round(performance.now() - startedAt),
316
+ };
317
+ }
@@ -1,4 +1,5 @@
1
- import { rm } from 'node:fs/promises';
1
+ import { readFile } from 'node:fs/promises';
2
+ import { dirname } from 'node:path';
2
3
  import { render } from '../core/office-actions.mjs';
3
4
  import { documentFormat, officeSessionForDocument } from '../core/office-core.mjs';
4
5
  import { createAuthoredSession, fullPath, validatePptxAuthorMode } from '../core/office-sessions.mjs';
@@ -7,14 +8,22 @@ import {
7
8
  exists,
8
9
  landStagedDeck,
9
10
  releaseExistingSession,
11
+ releaseStagingTarget,
10
12
  reusableAuthoredSession,
11
13
  stagingTarget,
12
14
  swapAuthoredDocument,
13
15
  throwIfAuthoringCancelled,
14
16
  } from './pptx-author-session.mjs';
15
17
  import { runPptxAuthoringScript } from './pptx-script-runner.mjs';
16
- import { htmlBriefScript, measureHtmlDrift, runPptxHtmlAuthoring, writeHtmlComparisons } from './pptx-html-runner.mjs';
18
+ import {
19
+ htmlArtifacts,
20
+ htmlBriefScript,
21
+ measureHtmlDrift,
22
+ runPptxHtmlAuthoring,
23
+ writeHtmlComparisons,
24
+ } from './pptx-html-runner.mjs';
17
25
  import { factsGate, parseAuthoringBrief, planGate } from './pptx-brief.mjs';
26
+ import { editsReplacedByAuthor, markAuthoredFromHtml } from './html-source-drift.mjs';
18
27
  import { readCompositionReceipt } from './pptx-review-artifacts.mjs';
19
28
  import { receiptForDelivery } from './pptx-receipt.mjs';
20
29
  import { snapshotPortableOoxml } from '../portable/portable-ooxml.mjs';
@@ -22,10 +31,10 @@ import { snapshotPortableOoxml } from '../portable/portable-ooxml.mjs';
22
31
  /** The design guide lives in the built-in `pptx` skill; the tool never
23
32
  * serves it so one copy stays authoritative and user-overridable. */
24
33
  const PPTX_AUTHOR_NEEDS_SOURCE =
25
- 'author requires script: HTML slides or a pptxgenjs script. Load the `pptx` Skill first (Skill name:"pptx"): it carries the authoring workflow, the HTML contract, the composition grammar, and the device kit, then call author again with path and script.';
34
+ 'author requires script: HTML slides or a pptxgenjs script (without one it re-authors from <deck>.pptx.mixdog-source.html, and none is there). Load the `pptx` Skill first (Skill name:"pptx"): it carries the authoring workflow, the HTML contract, the composition grammar, and the device kit, then call author again with path and script.';
26
35
 
27
36
  // HTML opens with markup (a doctype, a comment, an element); a pptxgenjs script never does.
28
- function isHtmlSource(source) {
37
+ export function isHtmlSource(source) {
29
38
  return /^\s*</.test(String(source || ''));
30
39
  }
31
40
 
@@ -33,7 +42,7 @@ function isHtmlSource(source) {
33
42
  // will hold it; a package the reader cannot open is left to qa, never turned
34
43
  // into a refusal. One snapshot answers both: the figures against the fact
35
44
  // sheet, then the pages against the plan that was written before them.
36
- async function gateStagedDeck(path, brief) {
45
+ export async function gateStagedDeck(path, brief) {
37
46
  if (!brief.present) return { blocked: false };
38
47
  let document;
39
48
  try {
@@ -44,7 +53,8 @@ async function gateStagedDeck(path, brief) {
44
53
  const facts = factsGate(document, brief);
45
54
  if (facts.blocked) return { ...facts, gate: 'facts' };
46
55
  const plan = planGate(document, brief);
47
- return plan.blocked ? { ...plan, gate: 'plan' } : { blocked: false };
56
+ // The snapshot rides on a pass so a caller can read the same deck further (a PDF's design read).
57
+ return plan.blocked ? { ...plan, gate: 'plan' } : { blocked: false, document };
48
58
  }
49
59
 
50
60
  function describePlanSlide(entry) {
@@ -53,7 +63,7 @@ function describePlanSlide(entry) {
53
63
  return `slide ${entry.slide}`;
54
64
  }
55
65
 
56
- function planGateResult(target, gate, run) {
66
+ export function planGateResult(target, gate, run) {
57
67
  const listed = gate.slides.map(describePlanSlide).join('; ');
58
68
  const nextAction = {
59
69
  plan_missing:
@@ -73,7 +83,7 @@ function planGateResult(target, gate, run) {
73
83
  };
74
84
  }
75
85
 
76
- function factsGateResult(target, brief, gate, run) {
86
+ export function factsGateResult(target, brief, gate, run) {
77
87
  const listed = gate.slides.map((entry) => `slide ${entry.slide}: ${entry.figures.join(', ')}`).join('; ');
78
88
  return {
79
89
  ok: false,
@@ -91,7 +101,7 @@ function factsGateResult(target, brief, gate, run) {
91
101
 
92
102
  // The HTML's own geometry, read in the browser before anything lands: a declared relation that does not
93
103
  // hold, or a near miss no declaration covers (pptx-html-geometry.mjs).
94
- function geometryGateResult(target, run) {
104
+ export function geometryGateResult(target, run) {
95
105
  const listed = run.geometry
96
106
  .map((page) => `slide ${page.slide}: ${page.findings.map((finding) => finding.message).join('; ')}`)
97
107
  .join(' | ');
@@ -107,11 +117,12 @@ function geometryGateResult(target, run) {
107
117
  }
108
118
 
109
119
  function resolveAuthorTarget(args, cwd) {
110
- if (!String(args.script || '').trim()) throw new Error(PPTX_AUTHOR_NEEDS_SOURCE);
111
120
  const requestedPath = String(args.path || args.output || '').trim();
112
- if (!requestedPath) throw new Error('author requires path');
121
+ // A bare author (no path, no script) is a call made before reading the skill: it is pointed there.
122
+ if (!requestedPath)
123
+ throw new Error(String(args.script || '').trim() ? 'author requires path' : PPTX_AUTHOR_NEEDS_SOURCE);
113
124
  const target = fullPath(requestedPath, cwd);
114
- if (documentFormat(target) !== 'pptx') throw new Error('author writes .pptx targets only');
125
+ if (documentFormat(target) !== 'pptx') throw new Error('author writes .pptx and .pdf targets only');
115
126
  return { target, mode: validatePptxAuthorMode(args.mode) };
116
127
  }
117
128
 
@@ -182,10 +193,10 @@ async function finishAuthoredDeck(session, { args, cwd, target, run, signal, rep
182
193
  // swaps its deck; an existing session hands its audit rounds on. A re-author
183
194
  // replaces the session, but the audit fix rounds belong to the deck: the loop
184
195
  // keeps counting across passes on the same path.
185
- async function authoringSessionState(target, mode, args) {
196
+ async function authoringSessionState(target, mode, args, fromSource) {
186
197
  const reusable = reusableAuthoredSession(target, mode);
187
198
  const existing = officeSessionForDocument(target);
188
- if (!reusable && !existing && (await exists(target)) && args.overwrite !== true) {
199
+ if (!reusable && !existing && !fromSource && (await exists(target)) && args.overwrite !== true) {
189
200
  throw new Error(`author target already exists: ${target}; pass overwrite:true to replace it`);
190
201
  }
191
202
  return { reusable, priorAudit: reusable ? null : existing?.inlineAudit || null };
@@ -193,14 +204,21 @@ async function authoringSessionState(target, mode, args) {
193
204
 
194
205
  export async function authorPptx(args, { cwd, dataDir, signal = null }) {
195
206
  const { target, mode } = resolveAuthorTarget(args, cwd);
207
+ // Without a script the deck is authored again from the HTML kept beside it, as a PDF or Word document is: a fix
208
+ // is an edit to that file, not the whole deck sent again.
209
+ const sourcePath = htmlArtifacts(target).source;
210
+ const fromSource = !String(args.script || '').trim();
211
+ if (fromSource && !(await exists(sourcePath))) throw new Error(PPTX_AUTHOR_NEEDS_SOURCE);
212
+ const script = fromSource ? await readFile(sourcePath, 'utf8') : String(args.script);
196
213
  throwIfAuthoringCancelled(signal);
197
- const { reusable, priorAudit } = await authoringSessionState(target, mode, args);
214
+ const replacedEdits = editsReplacedByAuthor(officeSessionForDocument(target));
215
+ const { reusable, priorAudit } = await authoringSessionState(target, mode, args, fromSource);
198
216
  // The script always writes beside the target: a failed script or a refused
199
217
  // deck leaves the file on disk and the session holding the previous deck
200
218
  // untouched.
201
- const staging = stagingTarget(target);
202
- const html = isHtmlSource(args.script);
203
- const brief = parseAuthoringBrief(html ? htmlBriefScript(args.script) : args.script);
219
+ const staging = await stagingTarget(target);
220
+ const html = isHtmlSource(script);
221
+ const brief = parseAuthoringBrief(html ? htmlBriefScript(script) : script);
204
222
  let run;
205
223
  let session = null;
206
224
  let reusedSession = false;
@@ -209,8 +227,8 @@ export async function authorPptx(args, { cwd, dataDir, signal = null }) {
209
227
  try {
210
228
  throwIfAuthoringCancelled(signal);
211
229
  run = html
212
- ? await runPptxHtmlAuthoring(args.script, staging, { target, signal })
213
- : await runPptxAuthoringScript(args.script, staging);
230
+ ? await runPptxHtmlAuthoring(script, staging, { target, signal })
231
+ : await runPptxAuthoringScript(script, staging, { cwd: dirname(target) });
214
232
  throwIfAuthoringCancelled(signal);
215
233
  if (!run.ok) return scriptFailedResult(target, run, html);
216
234
  if (run.geometry?.length) return geometryGateResult(target, run);
@@ -236,11 +254,15 @@ export async function authorPptx(args, { cwd, dataDir, signal = null }) {
236
254
  if (signal?.aborted || error?.name === 'AbortError') discardStaging = true;
237
255
  throw error;
238
256
  } finally {
239
- if (discardStaging) await rm(staging, { force: true }).catch(() => {});
257
+ if (discardStaging) await releaseStagingTarget(staging).catch(() => {});
240
258
  }
241
259
  session.authoredBrief = brief;
260
+ // A deck from HTML counts the batch edits that land past its kept source; a pptxgenjs deck keeps no source.
261
+ markAuthoredFromHtml(session, run.htmlSource || null);
242
262
  if (priorAudit && !session.inlineAudit) session.inlineAudit = { ...priorAudit };
243
- return finishAuthoredDeck(session, { args, cwd, target, run, signal, replacedSession, reusedSession });
263
+ const result = await finishAuthoredDeck(session, { args, cwd, target, run, signal, replacedSession, reusedSession });
264
+ if (replacedEdits) result.replacedEdits = replacedEdits;
265
+ return result;
244
266
  }
245
267
 
246
268
  const UNRENDERED_NEXT_ACTION =
@@ -1,6 +1,6 @@
1
- import { access, copyFile, rm } from 'node:fs/promises';
1
+ import { access, copyFile, mkdir, mkdtemp, rm } from 'node:fs/promises';
2
2
  import { constants as fsConstants } from 'node:fs';
3
- import { basename, dirname, extname, join } from 'node:path';
3
+ import { basename, dirname, join } from 'node:path';
4
4
  import { callMicrosoftOffice } from '../com/com-adapter.mjs';
5
5
  import { closeSession } from '../core/office-actions.mjs';
6
6
  import { emptyOfficeDesignState, officeSessionForDocument, releaseOfficeSession } from '../core/office-core.mjs';
@@ -54,10 +54,16 @@ export function reusableAuthoredSession(target, mode = 'auto') {
54
54
  return reusable ? existing : null;
55
55
  }
56
56
 
57
- // The script writes beside the target, not over it, so a failed script leaves both the file on disk
58
- // and the open session holding the previous deck untouched.
59
- export function stagingTarget(target) {
60
- return join(dirname(target), `.${basename(target, extname(target))}.authoring${extname(target)}`);
57
+ // Each call owns a directory on the target's filesystem. A failed author must
58
+ // never consume an existing neighbor or another call's staged document.
59
+ export async function stagingTarget(target) {
60
+ await mkdir(dirname(target), { recursive: true });
61
+ const directory = await mkdtemp(join(dirname(target), '.mixdog-authoring-'));
62
+ return join(directory, basename(target));
63
+ }
64
+
65
+ export async function releaseStagingTarget(staging) {
66
+ await rm(dirname(staging), { recursive: true, force: true });
61
67
  }
62
68
 
63
69
  export async function swapAuthoredDocument(session, source, signal, { callOffice = callMicrosoftOffice } = {}) {
@@ -1,7 +1,7 @@
1
1
  // The HTML authoring path, second half: the measurement pptx-html-measure.mjs
2
2
  // read from the browser becomes a deck of native PowerPoint objects — text
3
3
  // boxes that keep the browser's line breaks, shapes, lines, tables, charts,
4
- // and pictures — on the 13.333 × 7.5 in wide layout.
4
+ // and pictures — on the 13.333 × 7.5 in wide layout, or the sheet the measure names.
5
5
  import { readFile } from 'node:fs/promises';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { createRequire } from 'node:module';
@@ -24,9 +24,23 @@ const MIDDLE_LIFT = 0.09; // em a middle-anchored single line sits above the bro
24
24
  const middleBaseline = (spacing) => 1.02 * spacing - 0.09; // the same, for a middle-anchored block
25
25
  const EDGE = 24; // px: text slack never carries a box past this margin
26
26
 
27
+ // The deck canvas lands on the 13.333 in wide layout; a measured sheet (a PDF's designed pages) carries its own inches.
28
+ const WIDE_INCHES = 13.333;
29
+ const pageInches = (measure) => ({
30
+ width: measure.inchWidth || WIDE_INCHES,
31
+ height: measure.inchHeight || (WIDE_INCHES * (measure.height || 1080)) / measure.width,
32
+ });
33
+
27
34
  function units(measure) {
28
- const K = 13.333 / measure.width; // inches per CSS px
29
- return { K, PT: 0.5 * (1920 / measure.width), inch: (v) => v * K, W: measure.width, H: measure.height || 1080 };
35
+ const inches = pageInches(measure).width;
36
+ const K = inches / measure.width; // inches per CSS px
37
+ return {
38
+ K,
39
+ PT: 0.5 * (1920 / measure.width) * (inches / WIDE_INCHES),
40
+ inch: (v) => v * K,
41
+ W: measure.width,
42
+ H: measure.height || 1080,
43
+ };
30
44
  }
31
45
 
32
46
  // The page clips what runs past it (section.slide is overflow: hidden); PowerPoint would keep the part past
@@ -586,7 +600,12 @@ export async function buildPptxFromMeasure(measure, output) {
586
600
  const PptxGenJS = require('pptxgenjs');
587
601
  const sharp = require('sharp');
588
602
  const pres = new PptxGenJS();
589
- pres.layout = 'LAYOUT_WIDE';
603
+ const page = pageInches(measure);
604
+ if (page.width === WIDE_INCHES && Math.abs(page.height - 7.5) < 0.01) pres.layout = 'LAYOUT_WIDE';
605
+ else {
606
+ pres.defineLayout({ name: 'MIXDOG_SHEET', width: page.width, height: page.height });
607
+ pres.layout = 'MIXDOG_SHEET';
608
+ }
590
609
  const u = units(measure);
591
610
  for (const measured of measure.slides) {
592
611
  const slide = pres.addSlide();