mixdog 1.0.4 → 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 (158) 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/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
@@ -0,0 +1,61 @@
1
+ // The browser session every HTML authoring route (pptx, pdf, docx, xlsx) lays its document out in: launch, the
2
+ // deadline and abort wiring around the layout work, the failure result, and the photographs of captured elements.
3
+ import { join } from 'node:path';
4
+
5
+ import { buildPuppeteerLaunchArgs, resolveBrowserLaunchOptions } from '../../shared/browser-launch.mjs';
6
+ import { startChildGuardian } from '../../shared/child-guardian.mjs';
7
+
8
+ /** Launches the local browser the HTML is laid out in, under a guardian that reaps it with this process. */
9
+ export async function launchHtmlBrowser(label) {
10
+ const puppeteer = (await import('puppeteer-core')).default;
11
+ const browser = await puppeteer.launch({
12
+ headless: true,
13
+ ...resolveBrowserLaunchOptions(),
14
+ args: buildPuppeteerLaunchArgs(['--font-render-hinting=none']),
15
+ });
16
+ try {
17
+ startChildGuardian({ childPid: browser.process?.()?.pid, label });
18
+ } catch {}
19
+ return browser;
20
+ }
21
+
22
+ /** Runs `work` against the browser under a deadline and an abort signal, and closes the browser afterwards. */
23
+ export async function runInBrowser(browser, { signal, timeoutMs }, work) {
24
+ const abort = () => browser.close().catch(() => {});
25
+ signal?.addEventListener('abort', abort, { once: true });
26
+ let timer = null;
27
+ const running = work();
28
+ const timeout = new Promise((_, reject) => {
29
+ timer = setTimeout(() => reject(new Error(`HTML layout exceeded ${timeoutMs} ms`)), timeoutMs);
30
+ });
31
+ try {
32
+ return await Promise.race([running, timeout]);
33
+ } finally {
34
+ clearTimeout(timer);
35
+ signal?.removeEventListener('abort', abort);
36
+ await browser.close().catch(() => {});
37
+ }
38
+ }
39
+
40
+ /** The result of a route whose browser or layout failed. */
41
+ export function htmlFailure(error, startedAt, extra = {}) {
42
+ return {
43
+ ok: false,
44
+ error: { message: error?.message || String(error) },
45
+ elapsedMs: Math.round(performance.now() - startedAt),
46
+ ...extra,
47
+ };
48
+ }
49
+
50
+ /** Photographs each element the measure tagged `[attribute="1".."count"]` into `scratch`; index → png path. */
51
+ export async function photographCaptures(page, attribute, count, scratch) {
52
+ const captures = new Map();
53
+ for (let index = 1; index <= count; index += 1) {
54
+ const element = await page.$(`[${attribute}="${index}"]`);
55
+ if (!element) continue;
56
+ const path = join(scratch, `capture-${index}.png`);
57
+ await element.screenshot({ path, omitBackground: true });
58
+ captures.set(index, path);
59
+ }
60
+ return captures;
61
+ }
@@ -0,0 +1,181 @@
1
+ // The `author` action for a Word or Excel target: an HTML document laid out by a local browser and written as native
2
+ // Word (docx-html-runner.mjs) or Excel (xlsx-html-runner.mjs), landed over the target once it was written, then opened
3
+ // as an authored document — measured by the inline audit, rendered, and reviewed beside the browser's own picture of
4
+ // the same HTML: its print for a document, each sheet's section for a workbook.
5
+ import { readFile } from 'node:fs/promises';
6
+ import { createRequire } from 'node:module';
7
+ import { render } from '../core/office-actions.mjs';
8
+ import { documentFormat, officeSessionForDocument } from '../core/office-core.mjs';
9
+ import { createAuthoredSession, exists, fullPath, validatePptxAuthorMode } from '../core/office-sessions.mjs';
10
+ import { inlineOfficeAudit } from '../quality/inline-audit.mjs';
11
+ import { parseAuthoringBrief } from './pptx-brief.mjs';
12
+ import { htmlBriefScript } from './pptx-html-runner.mjs';
13
+ import {
14
+ landStagedDeck,
15
+ releaseExistingSession,
16
+ releaseStagingTarget,
17
+ stagingTarget,
18
+ throwIfAuthoringCancelled,
19
+ } from './pptx-author-session.mjs';
20
+ import { runDocxHtmlAuthoring } from './docx-html-runner.mjs';
21
+ import { runXlsxHtmlAuthoring } from './xlsx-html-runner.mjs';
22
+ import { editsReplacedByAuthor, markAuthoredFromHtml } from './html-source-drift.mjs';
23
+
24
+ const require = createRequire(import.meta.url);
25
+
26
+ const KINDS = {
27
+ docx: {
28
+ run: runDocxHtmlAuthoring,
29
+ noun: 'a Word file',
30
+ skill: 'docx',
31
+ contract: '@page sheet, what becomes a paragraph, a table, a box',
32
+ left: 'the HTML as the browser printed it',
33
+ checks:
34
+ 'reading order, page breaks, a box or table that lost its fill or width, type that changed size, a picture that moved',
35
+ },
36
+ xlsx: {
37
+ run: runXlsxHtmlAuthoring,
38
+ noun: 'an Excel workbook',
39
+ skill: 'xlsx',
40
+ contract: 'one <section data-sheet> per worksheet, data-formula, data-chart',
41
+ left: 'the sheet as the browser drew it',
42
+ checks:
43
+ 'a card or box that lost its fill, a text cut by its column, a figure that should be a number or a formula, a chart off its place, the printed page',
44
+ },
45
+ };
46
+
47
+ // One image per page: the browser's picture of the HTML on the left, the Office render on the right, at one height.
48
+ async function writeComparisons(target, htmlPages, officePages) {
49
+ const sharp = require('sharp');
50
+ const height = 1100;
51
+ const gap = 16;
52
+ const pairs = [];
53
+ const count = Math.max(htmlPages.length, officePages.length);
54
+ const scaled = async (image) => {
55
+ if (!image?.path) return null;
56
+ const data = await sharp(image.path).resize({ height }).png().toBuffer();
57
+ return { data, width: (await sharp(data).metadata()).width };
58
+ };
59
+ const blank = async (width) => ({
60
+ data: await sharp({ create: { width, height, channels: 3, background: '#FFFFFF' } })
61
+ .png()
62
+ .toBuffer(),
63
+ width,
64
+ });
65
+ for (let page = 1; page <= count; page += 1) {
66
+ const left = (await scaled(htmlPages.find((image) => image.page === page))) || (await blank(778));
67
+ const right =
68
+ (await scaled(officePages.find((image) => image.page === page && (image.pages?.length || 1) === 1))) ||
69
+ (await blank(left.width));
70
+ const path = `${target}.mixdog-compare-page-${page}.png`;
71
+ await sharp({ create: { width: left.width + right.width + gap, height, channels: 3, background: '#808080' } })
72
+ .composite([
73
+ { input: left.data, left: 0, top: 0 },
74
+ { input: right.data, left: left.width + gap, top: 0 },
75
+ ])
76
+ .png()
77
+ .toFile(path);
78
+ pairs.push({ page, path });
79
+ }
80
+ return pairs;
81
+ }
82
+
83
+ export async function authorHtmlDocument(args, { cwd, dataDir, signal = null }) {
84
+ const target = fullPath(String(args.path || args.output || '').trim(), cwd);
85
+ const format = documentFormat(target);
86
+ const kind = KINDS[format];
87
+ if (!kind) throw new Error('author writes .pptx, .pdf, .docx, and .xlsx targets only');
88
+ const sourcePath = `${target}.mixdog-source.html`;
89
+ // Without a script the document is authored again from the HTML kept beside it.
90
+ const fromSource = !String(args.script || '').trim();
91
+ if (fromSource && !(await exists(sourcePath))) {
92
+ throw new Error(
93
+ `author requires script: an HTML document for ${kind.noun} (without one it re-authors from ${sourcePath}, and none is there). Load the \`${kind.skill}\` Skill first (Skill name:"${kind.skill}"): it carries the HTML route (${kind.contract}), then call author again with path and script.`
94
+ );
95
+ }
96
+ const html = fromSource ? await readFile(sourcePath, 'utf8') : String(args.script);
97
+ if (!/^\s*</.test(html))
98
+ throw new Error(`author writes ${kind.noun} from an HTML document only (a source that opens with <).`);
99
+ const mode = validatePptxAuthorMode(args.mode);
100
+ const held = officeSessionForDocument(target);
101
+ const replacedEdits = editsReplacedByAuthor(held);
102
+ if ((await exists(target)) && !held && !fromSource && args.overwrite !== true) {
103
+ throw new Error(`author target already exists: ${target}; pass overwrite:true to replace it`);
104
+ }
105
+ const staging = await stagingTarget(target);
106
+ let run;
107
+ let session;
108
+ try {
109
+ throwIfAuthoringCancelled(signal);
110
+ run = await kind.run(html, staging, { target, signal });
111
+ throwIfAuthoringCancelled(signal);
112
+ if (!run.ok) {
113
+ return {
114
+ ok: false,
115
+ reason: 'html_failed',
116
+ output: target,
117
+ htmlSource: sourcePath,
118
+ error: run.error,
119
+ elapsedMs: run.elapsedMs,
120
+ nextAction: 'Fix the HTML the error names and call author again.',
121
+ };
122
+ }
123
+ await releaseExistingSession(target, signal);
124
+ await landStagedDeck(staging, target, signal);
125
+ session = await createAuthoredSession(
126
+ signal ? { ...args, mode, __signal: signal } : { ...args, mode },
127
+ cwd,
128
+ dataDir,
129
+ target
130
+ );
131
+ } finally {
132
+ await releaseStagingTarget(staging).catch(() => {});
133
+ }
134
+ markAuthoredFromHtml(session, sourcePath);
135
+ const brief = parseAuthoringBrief(htmlBriefScript(html));
136
+ if (brief.present) session.authoredBrief = brief;
137
+ const audit = args.audit === false ? null : await inlineOfficeAudit(session);
138
+ const result = {
139
+ ok: true,
140
+ session: session.id,
141
+ mode: session.mode,
142
+ backend: session.backend,
143
+ output: target,
144
+ bytes: run.bytes,
145
+ htmlSource: sourcePath,
146
+ logs: run.logs,
147
+ ...(replacedEdits ? { replacedEdits } : {}),
148
+ ...(audit ? { audit } : {}),
149
+ elapsedMs: run.elapsedMs,
150
+ };
151
+ if (args.render === false) {
152
+ result.nextAction =
153
+ audit?.status === 'fail'
154
+ ? audit.nextAction
155
+ : 'Written and measured, not visually reviewed: call action:render on this session for the page images and reviewToken, then inspect every page before finalizing.';
156
+ return result;
157
+ }
158
+ session.activeSignal = signal;
159
+ try {
160
+ const rendered = await render(session, { pages: args.pages, maxWidth: args.maxWidth }, cwd);
161
+ result.render = {
162
+ output: rendered.output,
163
+ pageCount: rendered.pageCount,
164
+ visualCoverage: rendered.visualCoverage,
165
+ images: rendered.images,
166
+ reviewToken: rendered.reviewToken,
167
+ };
168
+ result._images = Array.isArray(rendered._images) ? rendered._images : [];
169
+ if (run.htmlPages?.length) {
170
+ result.render.compare = await writeComparisons(target, run.htmlPages, rendered.images || []);
171
+ result.render.htmlPageCount = run.htmlPages.length;
172
+ }
173
+ } finally {
174
+ delete session.activeSignal;
175
+ }
176
+ result.nextAction =
177
+ audit?.status === 'fail'
178
+ ? `${audit.nextAction} The rendered pages are attached; the visual read starts once the audit passes.`
179
+ : `Inspect every rendered page beside its compare pair (${kind.left} on the left, the Office render on the right): ${kind.checks}. Fix the HTML (edit ${sourcePath} and call author with no script) for a material difference, or finalize with design: { reviewed: true, reviewToken, critique }.`;
180
+ return result;
181
+ }
@@ -0,0 +1,36 @@
1
+ // A document authored from HTML keeps that HTML beside it (`<file>.mixdog-source.html`), and `author` with no script
2
+ // rebuilds the file from it. An edit made to the file itself — a `batch` on the session — is not in that HTML, so
3
+ // the next rebuild drops it without a word: a deck's spacing fixed by moving shapes came back the moment the deck
4
+ // was authored again from its source. The session remembers which HTML it was built from and how many edits have
5
+ // landed since; a batch says so when it changes the file, and the next author says what it replaced.
6
+
7
+ /** Records the HTML a session's file was just authored from; edits counted from here. */
8
+ export function markAuthoredFromHtml(session, sourcePath) {
9
+ if (!session) return;
10
+ session.htmlSource = sourcePath || null;
11
+ session.htmlSourceEdits = 0;
12
+ }
13
+
14
+ /** After a batch: counts the edits that landed and returns the notice for the batch result, or null. */
15
+ export function noteBatchAgainstHtml(session, batch) {
16
+ if (!session?.htmlSource) return null;
17
+ const changed = Number(batch?.changeSummary?.changed) || 0;
18
+ if (!changed) return null;
19
+ session.htmlSourceEdits = (Number(session.htmlSourceEdits) || 0) + changed;
20
+ return {
21
+ path: session.htmlSource,
22
+ current: false,
23
+ editsSinceAuthor: session.htmlSourceEdits,
24
+ note: `These edits changed the file, not ${session.htmlSource}. author with no script rebuilds the file from that HTML and drops them: make the same change in the HTML too, or keep finishing this file with batch.`,
25
+ };
26
+ }
27
+
28
+ /** Before a re-author replaces the file: what the previous session had edited past its HTML, or null. */
29
+ export function editsReplacedByAuthor(previous) {
30
+ const count = Number(previous?.htmlSourceEdits) || 0;
31
+ if (!count) return null;
32
+ return {
33
+ count,
34
+ note: `${count} edit${count === 1 ? '' : 's'} made with batch since the last author ${count === 1 ? 'was' : 'were'} in the replaced file, not in the HTML; the new file does not carry ${count === 1 ? 'it' : 'them'}. Check that the HTML now holds the same fixes.`,
35
+ };
36
+ }
@@ -0,0 +1,266 @@
1
+ // The `author` action for a .pdf target: an HTML document printed by a local
2
+ // browser (pdf-html-runner.mjs), landed over the target only once it printed
3
+ // and passed its checks, then opened as a created PDF for review.
4
+ //
5
+ // Two kinds of document. A flowing one (running text the browser paginates)
6
+ // is checked for what print would cut. Designed pages — one <section
7
+ // class="slide"> per sheet — go through the deck's own frame first: the same
8
+ // browser measure, geometry gate, facts and plan gates, measured audit, and
9
+ // composition receipt the pptx route runs, on a working deck built at the
10
+ // sheet's size and discarded once read; then the browser prints the HTML
11
+ // itself, and finalize asks for the deck's scored critique.
12
+ import { mkdtemp, readFile, rm } from 'node:fs/promises';
13
+ import { dirname, join } from 'node:path';
14
+ import { render } from '../core/office-actions.mjs';
15
+ import { officeSessionForDocument } from '../core/office-core.mjs';
16
+ import {
17
+ createAuthoredSession,
18
+ exists,
19
+ fullPath,
20
+ registerHtmlPdfSession,
21
+ releaseOverwrittenSession,
22
+ } from '../core/office-sessions.mjs';
23
+ import { snapshotPdf } from '../pdf/pdf-adapter.mjs';
24
+ import { inlineOfficeAudit } from '../quality/inline-audit.mjs';
25
+ import { factsGate, parseAuthoringBrief, reviewBriefPromises, reviewSourceGrounding } from './pptx-brief.mjs';
26
+ import { editsReplacedByAuthor, markAuthoredFromHtml } from './html-source-drift.mjs';
27
+ import { hasSlideSections, htmlBriefScript, runPptxHtmlAuthoring } from './pptx-html-runner.mjs';
28
+ import {
29
+ landStagedDeck,
30
+ releaseExistingSession,
31
+ releaseStagingTarget,
32
+ stagingTarget,
33
+ throwIfAuthoringCancelled,
34
+ } from './pptx-author-session.mjs';
35
+ import { factsGateResult, gateStagedDeck, geometryGateResult, planGateResult } from './pptx-author-action.mjs';
36
+ import { readCompositionReceipt } from './pptx-review-artifacts.mjs';
37
+ import { receiptForDelivery } from './pptx-receipt.mjs';
38
+ import { frameCanvas, pdfHtmlSourcePath, printHtmlPdf } from './pdf-html-runner.mjs';
39
+
40
+ const PDF_AUTHOR_NEEDS_SOURCE =
41
+ 'author requires script: an HTML document for a PDF (without one it re-authors from <file>.pdf.mixdog-source.html, and none is there). Load the `pdf` Skill first (Skill name:"pdf"): it carries the page contract (@page size and margins, page breaks, fonts), then call author again with path and script.';
42
+
43
+ const RENDERED_NEXT_ACTION =
44
+ 'Inspect every rendered page for reading order, page breaks (a heading left at a page foot, a table or figure split), clipping, and legibility; fix the HTML and author again for any defect, or finalize with design: { reviewed: true, reviewToken, critique: [one entry per page] }.';
45
+
46
+ const UNRENDERED_NEXT_ACTION =
47
+ 'Printed and laid out clean, not visually reviewed: call action:render on this session for the page images and reviewToken, then inspect every page before finalizing.';
48
+
49
+ const FRAME_RENDERED_NEXT_ACTION =
50
+ 'Inspect every rendered page for message visibility, relevant evidence, legibility, and grouping, then read the sequence for coherence; use the receipt to investigate possible defects. Change the HTML only for an observed problem, or finalize with design: { reviewed: true, reviewToken, critique: [one entry per page with page, verdict, hierarchy, balance, legibility, cohesion, evidence, note, fixes, checks] }.';
51
+
52
+ const FRAME_UNRENDERED_NEXT_ACTION =
53
+ 'Printed, gated, and measured clean, not visually reviewed: call action:render on this session for the page images and reviewToken, then inspect every page before finalizing with the scored critique.';
54
+
55
+ // The browser that prints set every line of the PDF; the working deck's PowerPoint text metrics
56
+ // (fit, clipping, box width) describe a renderer the reader never sees.
57
+ const BROWSER_SET_TEXT = ['text_overflow', 'text_clipped', 'text_box_too_narrow'];
58
+
59
+ // Designed pages through the deck's frame: measured on the sheet, gated, audited, and read into a
60
+ // receipt. `{ refused }` carries the result to return when a gate holds; otherwise the audit and
61
+ // receipt ride on to the printed PDF.
62
+ async function frameDesignedPages(html, target, brief, { args, cwd, dataDir, signal, priorAudit }) {
63
+ const scratch = await mkdtemp(join(dirname(target), '.mixdog-pdf-frame-'));
64
+ const deck = join(scratch, 'frame.pptx');
65
+ try {
66
+ const run = await runPptxHtmlAuthoring(html, deck, { target, signal, prepare: frameCanvas });
67
+ throwIfAuthoringCancelled(signal);
68
+ if (!run.ok) {
69
+ return {
70
+ refused: {
71
+ ok: false,
72
+ reason: 'html_failed',
73
+ output: target,
74
+ error: run.error,
75
+ elapsedMs: run.elapsedMs,
76
+ nextAction: 'Fix the HTML the error names and call author again.',
77
+ },
78
+ };
79
+ }
80
+ if (run.geometry?.length) return { refused: geometryGateResult(target, run) };
81
+ const gate = await gateStagedDeck(deck, brief);
82
+ if (gate.blocked) {
83
+ return {
84
+ refused: gate.gate === 'plan' ? planGateResult(target, gate, run) : factsGateResult(target, brief, gate, run),
85
+ };
86
+ }
87
+ const shadow = await createAuthoredSession({ mode: 'portable' }, cwd, dataDir, deck);
88
+ shadow.authoredBrief = brief;
89
+ // The working deck's pages are paper: its audit holds them to the print type floors.
90
+ shadow.printSheet = true;
91
+ if (priorAudit) shadow.inlineAudit = { ...priorAudit };
92
+ const audit = args.audit === false ? null : await inlineOfficeAudit(shadow, { exclude: BROWSER_SET_TEXT });
93
+ const receipt = await readCompositionReceipt(shadow);
94
+ // The deck's design read against the brief, kept for the PDF's qa: the plan's page count and the
95
+ // facts' locators (the refusing readings already ran as gates).
96
+ const designIssues = gate.document
97
+ ? [...reviewBriefPromises(gate.document, brief), ...reviewSourceGrounding(brief)]
98
+ : [];
99
+ return { run, audit, receipt, designIssues, rounds: shadow.inlineAudit || null };
100
+ } finally {
101
+ await releaseExistingSession(deck, null).catch(() => {});
102
+ await rm(scratch, { recursive: true, force: true }).catch(() => {});
103
+ }
104
+ }
105
+
106
+ // A figure the pages show that the brief's facts line does not carry. The
107
+ // gate that refuses a deck only reports here: the document still lands.
108
+ // Page counters ("3 / 12") are chrome, not figures.
109
+ async function factsWarning(target, brief) {
110
+ if (!brief.present || brief.factsMode === 'sample') return null;
111
+ const { pages } = await snapshotPdf(target, { maxChars: 100_000, outline: false });
112
+ const document = {
113
+ slides: pages.map((page) => ({
114
+ index: page.index,
115
+ shapes: [{ text: String(page.text || '').replace(/(^|\n)\s*\d+\s*\/\s*\d+\s*(?=\n|$)/g, '$1') }],
116
+ })),
117
+ };
118
+ const gate = factsGate(document, brief);
119
+ if (!gate.blocked) return null;
120
+ return {
121
+ code: gate.code,
122
+ pages: gate.slides.map((entry) => ({ page: entry.slide, figures: entry.figures })),
123
+ message:
124
+ gate.code === 'facts_missing'
125
+ ? 'The document shows figures but its BRIEF comment has no facts line; list each figure with its source (F1 <value> — <source>).'
126
+ : 'These figures appear on the pages but in no fact of the BRIEF comment; add each with its source, or correct the figure.',
127
+ };
128
+ }
129
+
130
+ export async function authorPdf(args, { cwd, dataDir, signal = null }) {
131
+ const target = fullPath(String(args.path || args.output || '').trim(), cwd);
132
+ // Without a script the PDF is authored again from the HTML kept beside it, so a fix is an edit to
133
+ // that file rather than the whole document sent again.
134
+ const fromSource = !String(args.script || '').trim();
135
+ if (fromSource && !(await exists(pdfHtmlSourcePath(target)))) throw new Error(PDF_AUTHOR_NEEDS_SOURCE);
136
+ const html = fromSource ? await readFile(pdfHtmlSourcePath(target), 'utf8') : String(args.script);
137
+ if (!/^\s*</.test(html))
138
+ throw new Error('author writes a PDF from an HTML document only (a source that opens with <).');
139
+ const targetExisted = await exists(target);
140
+ const held = officeSessionForDocument(target);
141
+ const replacedEdits = editsReplacedByAuthor(held);
142
+ // Its own source names its own output: re-authoring from it replaces the file without overwrite:true.
143
+ if (targetExisted && !held && !fromSource && args.overwrite !== true) {
144
+ throw new Error(`author target already exists: ${target}; pass overwrite:true to replace it`);
145
+ }
146
+ // The browser prints beside the target: a failed print or a refused layout
147
+ // leaves the file on disk and any session holding it untouched.
148
+ const staging = await stagingTarget(target);
149
+ const htmlSource = pdfHtmlSourcePath(target);
150
+ const brief = parseAuthoringBrief(htmlBriefScript(html));
151
+ const frame = hasSlideSections(html);
152
+ let framed = null;
153
+ let run;
154
+ let session;
155
+ try {
156
+ throwIfAuthoringCancelled(signal);
157
+ if (frame) {
158
+ framed = await frameDesignedPages(html, target, brief, {
159
+ args,
160
+ cwd,
161
+ dataDir,
162
+ signal,
163
+ priorAudit: held?.inlineAudit || null,
164
+ });
165
+ if (framed.refused) return framed.refused;
166
+ }
167
+ run = await printHtmlPdf(html, staging, { sourcePath: htmlSource, properties: args.properties, frame, signal });
168
+ throwIfAuthoringCancelled(signal);
169
+ if (run.ok && frame && run.pageCount !== framed.run.slideCount) {
170
+ return {
171
+ ok: false,
172
+ reason: 'html_failed',
173
+ output: target,
174
+ htmlSource,
175
+ error: {
176
+ message: `${framed.run.slideCount} section.slide pages printed onto ${run.pageCount} sheets: a page is taller than the sheet, or the CSS hides or stacks pages in print. Keep every section.slide exactly the sheet's size and displayed.`,
177
+ },
178
+ elapsedMs: run.elapsedMs,
179
+ nextAction: 'Fix the HTML the error names and call author again.',
180
+ };
181
+ }
182
+ if (!run.ok) {
183
+ return {
184
+ ok: false,
185
+ reason: run.layout ? 'layout_gate' : 'html_failed',
186
+ output: target,
187
+ htmlSource,
188
+ ...(run.layout ? { findings: run.layout } : { error: run.error }),
189
+ elapsedMs: run.elapsedMs,
190
+ nextAction: run.layout
191
+ ? 'Nothing landed: fix each finding in the HTML (its element and measure are named) and call author again.'
192
+ : 'Fix the HTML the error names and call author again.',
193
+ };
194
+ }
195
+ await releaseOverwrittenSession(target, true);
196
+ await landStagedDeck(staging, target, signal);
197
+ session = await registerHtmlPdfSession(args, dataDir, target, {
198
+ targetExisted,
199
+ receipt: { pageCount: run.pageCount, printableArea: run.area },
200
+ });
201
+ } finally {
202
+ await releaseStagingTarget(staging).catch(() => {});
203
+ }
204
+ if (brief.present) session.authoredBrief = brief;
205
+ markAuthoredFromHtml(session, htmlSource);
206
+ if (frame) {
207
+ // Designed pages are the author's deck on paper: reviewed and finalized as one.
208
+ session.authored = true;
209
+ session.authoredFrame = true;
210
+ session.frameAudit = framed.audit;
211
+ session.frameReceipt = framed.receipt;
212
+ session.frameDesignIssues = framed.designIssues;
213
+ if (framed.rounds) session.inlineAudit = framed.rounds;
214
+ }
215
+ // The facts gate already refused a designed page's unlisted figure; a flowing document only reports it.
216
+ const facts = frame ? null : await factsWarning(target, brief);
217
+ const result = {
218
+ ok: true,
219
+ session: session.id,
220
+ mode: session.mode,
221
+ backend: session.backend,
222
+ output: target,
223
+ bytes: run.bytes,
224
+ pageCount: run.pageCount,
225
+ printableArea: run.area,
226
+ ...(frame ? { designedPages: true } : {}),
227
+ htmlSource,
228
+ ...(replacedEdits ? { replacedEdits } : {}),
229
+ warnings: run.warnings,
230
+ ...(frame && framed.run.logs?.length ? { logs: framed.run.logs } : {}),
231
+ ...(facts ? { facts } : {}),
232
+ ...(framed?.audit ? { audit: framed.audit } : {}),
233
+ elapsedMs: run.elapsedMs + (framed?.run.elapsedMs || 0),
234
+ };
235
+ const auditFailed = framed?.audit?.status === 'fail';
236
+ if (args.render === false) {
237
+ if (framed?.receipt) result.receipt = receiptForDelivery(framed.receipt, session);
238
+ result.nextAction = auditFailed
239
+ ? framed.audit.nextAction
240
+ : frame
241
+ ? FRAME_UNRENDERED_NEXT_ACTION
242
+ : UNRENDERED_NEXT_ACTION;
243
+ return result;
244
+ }
245
+ session.activeSignal = signal;
246
+ try {
247
+ const rendered = await render(session, { pages: args.pages, maxWidth: args.maxWidth }, cwd);
248
+ result.render = {
249
+ output: rendered.output,
250
+ pageCount: rendered.pageCount,
251
+ visualCoverage: rendered.visualCoverage,
252
+ images: rendered.images,
253
+ reviewToken: rendered.reviewToken,
254
+ };
255
+ // Designed pages: the render read the printed pixels into the receipt and wrote the contact sheet.
256
+ if (rendered.contactSheet) result.render.contactSheet = rendered.contactSheet;
257
+ if (rendered.receipt) result.receipt = rendered.receipt;
258
+ result._images = Array.isArray(rendered._images) ? rendered._images : [];
259
+ } finally {
260
+ delete session.activeSignal;
261
+ }
262
+ if (auditFailed) {
263
+ result.nextAction = `${framed.audit.nextAction} The rendered pages are attached; the visual read starts once the audit passes.`;
264
+ } else result.nextAction = frame ? FRAME_RENDERED_NEXT_ACTION : RENDERED_NEXT_ACTION;
265
+ return result;
266
+ }