mixdog 0.9.154 → 0.9.155

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 (38) hide show
  1. package/package.json +2 -2
  2. package/scripts/build-voice-runtime-unix.sh +24 -0
  3. package/scripts/voice-runtime-config.json +1 -1
  4. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +2 -2
  5. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +5 -2
  6. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +5 -2
  7. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +47 -4
  8. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +12 -0
  9. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +7 -0
  10. package/src/runtime/agent/orchestrator/session/store/live-state.mjs +4 -1
  11. package/src/runtime/channels/data/voice-runtime-manifest.json +17 -17
  12. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +59 -0
  13. package/src/runtime/office/assurance-benchmark.mjs +67 -1
  14. package/src/runtime/office/assurance.mjs +24 -17
  15. package/src/runtime/office/capabilities.mjs +17 -17
  16. package/src/runtime/office/composition-system.mjs +451 -0
  17. package/src/runtime/office/design-library.mjs +102 -0
  18. package/src/runtime/office/design-system.mjs +357 -168
  19. package/src/runtime/office/index.mjs +149 -20
  20. package/src/runtime/office/office-com-host.ps1 +132 -46
  21. package/src/runtime/office/pdf-render.mjs +203 -35
  22. package/src/runtime/office/portable-ooxml.mjs +1090 -47
  23. package/src/runtime/office/portable-package.mjs +390 -0
  24. package/src/runtime/office/portable-sheet-styles.mjs +241 -0
  25. package/src/runtime/office/portable-slide-shapes.mjs +283 -0
  26. package/src/runtime/office/quality-live-benchmark.mjs +82 -36
  27. package/src/runtime/office/quality-pipeline.mjs +3 -0
  28. package/src/runtime/office/tool-defs.mjs +2 -2
  29. package/src/session-runtime/goal-reminder.mjs +79 -0
  30. package/src/session-runtime/goal-runtime.mjs +105 -45
  31. package/src/session-runtime/goal-text.mjs +58 -0
  32. package/src/session-runtime/runtime-core.mjs +14 -1
  33. package/src/session-runtime/session-lifecycle.mjs +1 -1
  34. package/src/session-runtime/session-turn-api.mjs +5 -0
  35. package/src/standalone/session-service.mjs +30 -3
  36. package/src/tui/dist/index.mjs +12 -3
  37. package/src/tui/lib/voice-setup.mjs +19 -3
  38. package/src/tui/session/goal-continuation.mjs +7 -0
@@ -35,6 +35,7 @@ import {
35
35
  validateLibreOfficeReopen,
36
36
  validatePortableOoxml,
37
37
  } from './portable-ooxml.mjs';
38
+ import { createPortableOoxmlDocument, portableCreateSupported } from './portable-package.mjs';
38
39
  import {
39
40
  applyPdfBatch,
40
41
  createPdf,
@@ -83,10 +84,12 @@ import {
83
84
  reviewOfficeDesign,
84
85
  reviewPptxVisualCritique,
85
86
  } from './design-system.mjs';
87
+ import { summarizeOfficeCompositions } from './composition-system.mjs';
86
88
  import {
87
89
  createPptxSlideSelection,
88
90
  inspectOfficeDesignLibrary,
89
91
  persistOfficeDesignBinding,
92
+ recordOfficeCompositionHistory,
90
93
  resolveOfficeDesignLibrary,
91
94
  } from './design-library.mjs';
92
95
  import {
@@ -901,6 +904,28 @@ async function selectMode(requested, format, source) {
901
904
  return { mode: 'portable', backend: 'mixdog-ooxml' };
902
905
  }
903
906
 
907
+ async function seedPortableDeck(source, slides, target) {
908
+ const selected = Array.isArray(slides) ? slides.map(Number).filter(Number.isInteger) : [];
909
+ if (selected.length) {
910
+ const selection = await createPptxSlideSelection(source, selected, target);
911
+ return { count: selection.count };
912
+ }
913
+ await copyFile(source, target);
914
+ const zip = await JSZip.loadAsync(await readFile(target));
915
+ return {
916
+ count: Object.keys(zip.files).filter((name) => /^ppt\/slides\/slide\d+\.xml$/.test(name)).length,
917
+ };
918
+ }
919
+
920
+ async function selectCreateMode(requested, format, target) {
921
+ if (requested === 'portable') return { mode: 'portable', backend: 'mixdog-ooxml' };
922
+ if (requested !== 'auto') return await selectMode(requested, format, target);
923
+ const detected = await selectMode('auto', format, target);
924
+ return detected.backend === 'microsoft-office-com'
925
+ ? { mode: 'visible', backend: 'microsoft-office-com' }
926
+ : detected;
927
+ }
928
+
904
929
  async function openSession(args, cwd, dataDir) {
905
930
  const source = fullPath(args.path, cwd);
906
931
  if (!await exists(source)) throw new Error(`Office document not found: ${source}`);
@@ -947,6 +972,7 @@ async function openSession(args, cwd, dataDir) {
947
972
  semanticCount: 0,
948
973
  requiresVisualReview: format === 'pptx' && designContext.design.review.required,
949
974
  slidePlans: [],
975
+ compositions: [],
950
976
  },
951
977
  };
952
978
  if (selected.backend === 'microsoft-office-com') {
@@ -1021,7 +1047,7 @@ async function createSession(args, cwd, dataDir) {
1021
1047
  designRequest,
1022
1048
  designLibrary,
1023
1049
  design,
1024
- designState: { renderedVersion: null, semanticCount: 0, requiresVisualReview: false },
1050
+ designState: { renderedVersion: null, semanticCount: 0, requiresVisualReview: false, compositions: [] },
1025
1051
  };
1026
1052
  await registerOfficeSession(session);
1027
1053
  return session;
@@ -1056,18 +1082,52 @@ async function createSession(args, cwd, dataDir) {
1056
1082
  designRequest,
1057
1083
  designLibrary,
1058
1084
  design,
1059
- designState: { renderedVersion: null, semanticCount: 0, requiresVisualReview: false },
1085
+ designState: { renderedVersion: null, semanticCount: 0, requiresVisualReview: false, compositions: [] },
1060
1086
  };
1061
1087
  await registerOfficeSession(session);
1062
1088
  return session;
1063
1089
  }
1064
- const requestedMode = String(args.mode || 'visible').toLowerCase();
1065
- if (['attach', 'live', 'portable'].includes(requestedMode)) {
1066
- throw new Error('Office create requires visible or background mode');
1090
+ const requestedMode = String(args.mode || 'auto').toLowerCase();
1091
+ if (['attach', 'live'].includes(requestedMode)) {
1092
+ throw new Error('Office create requires visible, background, or portable mode');
1067
1093
  }
1068
- const selected = await selectMode(requestedMode === 'auto' ? 'visible' : requestedMode, format, target);
1069
- if (selected.backend !== 'microsoft-office-com') {
1070
- throw new Error(`Creating .${format} requires Microsoft Office on Windows`);
1094
+ const selected = await selectCreateMode(requestedMode, format, target);
1095
+ if (selected.backend === 'mixdog-ooxml') {
1096
+ if (!portableCreateSupported(fileKind)) {
1097
+ throw new Error(`Creating .${fileKind} without Microsoft Office is unsupported; open Microsoft Office or choose a docx, xlsx, or pptx target`);
1098
+ }
1099
+ await mkdir(dirname(target), { recursive: true });
1100
+ await createPortableOoxmlDocument(target, {
1101
+ fileKind,
1102
+ title: basename(target, extname(target)),
1103
+ });
1104
+ const session = {
1105
+ id: `office_${randomUUID().replaceAll('-', '').slice(0, 16)}`,
1106
+ source: target,
1107
+ target,
1108
+ fileKind,
1109
+ format,
1110
+ mode: 'portable',
1111
+ backend: 'mixdog-ooxml',
1112
+ openedAt: new Date().toISOString(),
1113
+ dataDir,
1114
+ created: true,
1115
+ ownership: 'owned',
1116
+ visible: false,
1117
+ snapshotVersion: 0,
1118
+ designRequest,
1119
+ designLibrary,
1120
+ design,
1121
+ designState: {
1122
+ renderedVersion: null,
1123
+ semanticCount: 0,
1124
+ requiresVisualReview: format === 'pptx' && design.review.required,
1125
+ slidePlans: [],
1126
+ compositions: [],
1127
+ },
1128
+ };
1129
+ await registerOfficeSession(session);
1130
+ return session;
1071
1131
  }
1072
1132
  const id = `office_${randomUUID().replaceAll('-', '').slice(0, 16)}`;
1073
1133
  const opened = await openMicrosoftOfficeSession({
@@ -1106,6 +1166,7 @@ async function createSession(args, cwd, dataDir) {
1106
1166
  semanticCount: 0,
1107
1167
  requiresVisualReview: format === 'pptx' && design.review.required,
1108
1168
  slidePlans: [],
1169
+ compositions: [],
1109
1170
  },
1110
1171
  };
1111
1172
  await registerOfficeSession(session);
@@ -1270,16 +1331,13 @@ async function applyBatch(session, args) {
1270
1331
  });
1271
1332
  session.designRequest = designRequest;
1272
1333
  session.design = prepared.design;
1273
- session.designState ||= { renderedVersion: null, semanticCount: 0, requiresVisualReview: false };
1274
- session.designState.semanticCount += prepared.semantic.length;
1275
- if (session.format === 'pptx' && session.design.review.required) {
1276
- session.designState.requiresVisualReview = true;
1277
- const existingPlans = new Map((session.designState.slidePlans || []).map((plan) => [Number(plan.slide), plan]));
1278
- for (const semantic of prepared.semantic) {
1279
- if (semantic?.plan && Number(semantic.slide) > 0) existingPlans.set(Number(semantic.slide), semantic.plan);
1280
- }
1281
- session.designState.slidePlans = [...existingPlans.values()].sort((left, right) => left.slide - right.slide);
1282
- }
1334
+ session.designState ||= {
1335
+ renderedVersion: null,
1336
+ semanticCount: 0,
1337
+ requiresVisualReview: false,
1338
+ slidePlans: [],
1339
+ compositions: [],
1340
+ };
1283
1341
  const pathOperations = new Set(['add_image', 'replace_image', 'stamp_image', 'add_attachment', 'merge_pdf', 'apply_theme', 'import_slides', 'add_media']);
1284
1342
  const operations = Array.isArray(prepared.operations)
1285
1343
  ? prepared.operations.map((operation) => {
@@ -1327,6 +1385,13 @@ async function applyBatch(session, args) {
1327
1385
  && operations[0].op === 'import_slides'
1328
1386
  && Number(operations[0].after || 0) === 0
1329
1387
  && extname(operations[0].path).toLowerCase() === extname(target).toLowerCase();
1388
+ const portableDeckSeed = session.backend === 'mixdog-ooxml'
1389
+ && session.format === 'pptx'
1390
+ && session.created === true
1391
+ && Number(session.snapshotVersion || 0) === 0
1392
+ && operations[0].op === 'import_slides'
1393
+ && Number(operations[0].after || 0) === 0
1394
+ && extname(operations[0].path).toLowerCase() === extname(target).toLowerCase();
1330
1395
  const needsComCheckpoint = isMicrosoftOfficeSession(session)
1331
1396
  && session.mode === 'background'
1332
1397
  && operations.some((operation) => operation.op === 'import_slides');
@@ -1429,6 +1494,20 @@ async function applyBatch(session, args) {
1429
1494
  });
1430
1495
  } else if (TABULAR_FORMATS.has(session.format)) {
1431
1496
  results = await applyTabularBatch(target, session.format, operations);
1497
+ } else if (portableDeckSeed) {
1498
+ const operation = operations[0];
1499
+ const seeded = await seedPortableDeck(operation.path, operation.slides, target);
1500
+ const remaining = operations.slice(1);
1501
+ results = [
1502
+ {
1503
+ op: 'import_slides',
1504
+ changed: true,
1505
+ count: seeded.count,
1506
+ source: operation.path,
1507
+ seeded: true,
1508
+ },
1509
+ ...(remaining.length ? await applyPortableOoxmlBatch(target, session.format, remaining) : []),
1510
+ ];
1432
1511
  } else {
1433
1512
  results = await applyPortableOoxmlBatch(target, session.format, operations);
1434
1513
  }
@@ -1456,6 +1535,19 @@ async function applyBatch(session, args) {
1456
1535
  transactionResult = transactionView(transaction);
1457
1536
  if (journalWarning) transactionResult.journalWarning = journalWarning;
1458
1537
  }
1538
+ session.designState.semanticCount += prepared.semantic.length;
1539
+ session.designState.compositions = [
1540
+ ...(session.designState.compositions || []),
1541
+ ...prepared.semantic.map((entry) => entry?.composition).filter(Boolean),
1542
+ ];
1543
+ if (session.format === 'pptx' && session.design.review.required) {
1544
+ session.designState.requiresVisualReview = true;
1545
+ const existingPlans = new Map((session.designState.slidePlans || []).map((plan) => [Number(plan.slide), plan]));
1546
+ for (const semantic of prepared.semantic) {
1547
+ if (semantic?.plan && Number(semantic.slide) > 0) existingPlans.set(Number(semantic.slide), semantic.plan);
1548
+ }
1549
+ session.designState.slidePlans = [...existingPlans.values()].sort((left, right) => left.slide - right.slide);
1550
+ }
1459
1551
  session.snapshotVersion = Number(session.snapshotVersion || 0) + 1;
1460
1552
  session.designState.renderedVersion = null;
1461
1553
  return {
@@ -1757,6 +1849,7 @@ async function qa(session, args, cwd) {
1757
1849
  ...(session.format === 'pptx' ? {
1758
1850
  slidePlans: session.designState?.slidePlans || [],
1759
1851
  } : {}),
1852
+ compositions: session.designState?.compositions || [],
1760
1853
  },
1761
1854
  library: session.designLibrary,
1762
1855
  auditProfile: args.auditProfile,
@@ -2078,7 +2171,28 @@ async function finalize(session, args, cwd, signal) {
2078
2171
  _images: reviewImages,
2079
2172
  };
2080
2173
  }
2174
+ const composition = summarizeOfficeCompositions(
2175
+ session.format,
2176
+ session.designState?.compositions || [],
2177
+ );
2081
2178
  const closed = await timedStep('close', async () => await closeSession(session, { save: false, signal }));
2179
+ let compositionHistory = null;
2180
+ let compositionHistoryWarning = '';
2181
+ if (session.created && composition.fingerprint) {
2182
+ try {
2183
+ compositionHistory = await recordOfficeCompositionHistory(session.dataDir, {
2184
+ documentPath: session.target,
2185
+ format: session.format,
2186
+ profile: session.design?.profile,
2187
+ purpose: session.design?.purpose,
2188
+ expressionMode: session.design?.expressionMode,
2189
+ fingerprint: composition.fingerprint,
2190
+ compositionIds: composition.compositionIds,
2191
+ });
2192
+ } catch (error) {
2193
+ compositionHistoryWarning = error?.message || String(error);
2194
+ }
2195
+ }
2082
2196
  return {
2083
2197
  ok: true,
2084
2198
  finalized: true,
@@ -2092,6 +2206,9 @@ async function finalize(session, args, cwd, signal) {
2092
2206
  review,
2093
2207
  validation,
2094
2208
  design: session.design,
2209
+ composition,
2210
+ compositionHistory,
2211
+ ...(compositionHistoryWarning ? { compositionHistoryWarning } : {}),
2095
2212
  stepMetrics,
2096
2213
  _images: reviewImages,
2097
2214
  };
@@ -2178,7 +2295,13 @@ export async function executeOfficeTool(args = {}, {
2178
2295
  created: session.created === true,
2179
2296
  });
2180
2297
  Object.assign(session, designContext);
2181
- session.designState = { renderedVersion: null, semanticCount: 0, requiresVisualReview: false };
2298
+ session.designState = {
2299
+ renderedVersion: null,
2300
+ semanticCount: 0,
2301
+ requiresVisualReview: false,
2302
+ slidePlans: [],
2303
+ compositions: [],
2304
+ };
2182
2305
  } else if (args.design) {
2183
2306
  session.designRequest = mergeOfficeDesignRequest(session.designRequest, args.design);
2184
2307
  session.design = resolveOfficeDesign(session.format, session.designRequest, { library: session.designLibrary });
@@ -2265,7 +2388,13 @@ export async function executeOfficeTool(args = {}, {
2265
2388
  created: session.created === true,
2266
2389
  });
2267
2390
  Object.assign(session, designContext);
2268
- session.designState = { renderedVersion: null, semanticCount: 0, requiresVisualReview: false };
2391
+ session.designState = {
2392
+ renderedVersion: null,
2393
+ semanticCount: 0,
2394
+ requiresVisualReview: false,
2395
+ slidePlans: [],
2396
+ compositions: [],
2397
+ };
2269
2398
  } else if (args.design) {
2270
2399
  if (args.design.upgradeLibrary === true) {
2271
2400
  const upgraded = await resolveOfficeDesignContext({
@@ -101,6 +101,7 @@ function Open-BackgroundDocument($app, [string]$format, [string]$path) {
101
101
  }
102
102
 
103
103
  function Snapshot-Word($doc, $payload) {
104
+ try { $null = $doc.Repaginate() } catch {}
104
105
  $paragraphs = @()
105
106
  $paragraphOffset = if ($payload.paged) { [Math]::Max(0, [int]$payload.offset) } else { 0 }
106
107
  $paragraphLimit = if ($payload.paged) { [Math]::Max(1, [int]$payload.limit) } else { [int]$doc.Paragraphs.Count }
@@ -135,10 +136,12 @@ function Snapshot-Word($doc, $payload) {
135
136
  style = $styleName
136
137
  start = [int]$p.Range.Start
137
138
  end = [int]$p.Range.End
139
+ inTable = $(try { [bool]$p.Range.Information(12) } catch { $false })
138
140
  pageStart = $(try { [int]$p.Range.Information(3) } catch { 0 })
139
141
  pageEnd = $(try {
140
142
  $endRange = $p.Range.Duplicate
141
- $endRange.Collapse(0)
143
+ $end = [Math]::Max([int]$endRange.Start, [int]$endRange.End - 1)
144
+ $endRange.SetRange($end, $end)
142
145
  [int]$endRange.Information(3)
143
146
  } catch { 0 })
144
147
  format = [ordered]@{
@@ -201,19 +204,20 @@ function Snapshot-Word($doc, $payload) {
201
204
  } catch { 0 })
202
205
  pageEnd = $(try {
203
206
  $endRange = $table.Range.Duplicate
204
- $endRange.Collapse(0)
207
+ $end = [Math]::Max([int]$endRange.Start, [int]$endRange.End - 1)
208
+ $endRange.SetRange($end, $end)
205
209
  [int]$endRange.Information(3)
206
210
  } catch { 0 })
207
211
  }
208
212
  }
209
213
  $blockOrder = @(
210
- @($paragraphs | ForEach-Object {
214
+ @($paragraphs | Where-Object { -not [bool]$_.inTable } | ForEach-Object {
211
215
  [ordered]@{ type = 'paragraph'; index = [int]$_.index; path = [string]$_.path; start = [int]$_.start }
212
216
  })
213
217
  @($tables | ForEach-Object {
214
218
  [ordered]@{ type = 'table'; index = [int]$_.index; path = [string]$_.path; start = [int]$_.start }
215
219
  })
216
- ) | Sort-Object start
220
+ ) | Sort-Object { [int]$_['start'] }
217
221
  $sections = @()
218
222
  for ($sectionIndex = 1; $sectionIndex -le $doc.Sections.Count; $sectionIndex++) {
219
223
  $section = $doc.Sections.Item($sectionIndex)
@@ -227,6 +231,7 @@ function Snapshot-Word($doc, $payload) {
227
231
  try {
228
232
  $collection = if ($location -eq 'header') { $section.Headers } else { $section.Footers }
229
233
  $item = $collection.Item([int]$kind.value)
234
+ if (-not [bool]$item.Exists) { continue }
230
235
  $stories += [ordered]@{
231
236
  path = "/section[$sectionIndex]/${location}[$($kind.name)]"
232
237
  kind = [string]$kind.name
@@ -1452,9 +1457,24 @@ function Apply-WordOperation($doc, $op) {
1452
1457
  $paragraph = $doc.Paragraphs.Add($range)
1453
1458
  $paragraph.Range.Text = "$text`r"
1454
1459
  }
1460
+ $paragraphIndex = if ($reusedInitialParagraph) { 1 } else { [Math]::Max(1, [int]$doc.Paragraphs.Count - 1) }
1461
+ $paragraph = $doc.Paragraphs.Item($paragraphIndex)
1462
+ $props = $op.properties
1463
+ if ($props.listKind) {
1464
+ $kind = ([string]$props.listKind).ToLowerInvariant()
1465
+ if ($kind -eq 'number') {
1466
+ $paragraph.Range.ListFormat.ApplyNumberDefault()
1467
+ } elseif ($kind -ne 'none') {
1468
+ $paragraph.Range.ListFormat.ApplyBulletDefault()
1469
+ } else {
1470
+ $paragraph.Range.ListFormat.RemoveNumbers()
1471
+ }
1472
+ for ($level = 0; $level -lt [int]$props.listLevel; $level++) { $paragraph.Range.ListFormat.ListIndent() }
1473
+ } else {
1474
+ $paragraph.Range.ListFormat.RemoveNumbers()
1475
+ }
1455
1476
  $style = if ($op.style) { [string]$op.style } elseif ($op.properties.style) { [string]$op.properties.style } else { '' }
1456
1477
  if ($style) { $paragraph.Range.Style = Word-StyleValue $style }
1457
- $props = $op.properties
1458
1478
  if ($props.name) { $paragraph.Range.Font.Name = [string]$props.name }
1459
1479
  if ($props.size) { $paragraph.Range.Font.Size = [single]$props.size }
1460
1480
  if ($null -ne $props.bold) { $paragraph.Range.Font.Bold = if ($props.bold) { -1 } else { 0 } }
@@ -1483,20 +1503,6 @@ function Apply-WordOperation($doc, $op) {
1483
1503
  $border.LineStyle = 1
1484
1504
  if ($props.border.color) { $border.Color = Color-Value ([string]$props.border.color) }
1485
1505
  }
1486
- if ($props.listKind) {
1487
- $kind = ([string]$props.listKind).ToLowerInvariant()
1488
- if ($kind -eq 'number') {
1489
- $paragraph.Range.ListFormat.ApplyNumberDefault()
1490
- } elseif ($kind -ne 'none') {
1491
- $paragraph.Range.ListFormat.ApplyBulletDefault()
1492
- } else {
1493
- $paragraph.Range.ListFormat.RemoveNumbers()
1494
- }
1495
- for ($level = 0; $level -lt [int]$props.listLevel; $level++) { $paragraph.Range.ListFormat.ListIndent() }
1496
- } else {
1497
- $paragraph.Range.ListFormat.RemoveNumbers()
1498
- }
1499
- $paragraphIndex = if ($reusedInitialParagraph) { 1 } else { [Math]::Max(1, [int]$doc.Paragraphs.Count - 1) }
1500
1506
  return [ordered]@{ op = 'append_text'; changed = $true; paragraph = $paragraphIndex; path = "/body/p[$paragraphIndex]"; style = $style }
1501
1507
  }
1502
1508
  'set_table_cell' {
@@ -1741,7 +1747,9 @@ function Apply-WordOperation($doc, $op) {
1741
1747
  default { 1 }
1742
1748
  }
1743
1749
  $collection = if ($null -ne $op.header -and -not [bool]$op.header) { $section.Footers } else { $section.Headers }
1744
- $collection.Item($kind).Range.Text = [string]$op.text
1750
+ $item = $collection.Item($kind)
1751
+ $item.Exists = $true
1752
+ $item.Range.Text = [string]$op.text
1745
1753
  return [ordered]@{ op = 'set_header_footer'; changed = $true; section = $section.Index }
1746
1754
  }
1747
1755
  'track_changes' {
@@ -1780,24 +1788,43 @@ function Apply-WordOperation($doc, $op) {
1780
1788
  }
1781
1789
  'add_page_numbers' {
1782
1790
  $section = $doc.Sections.Item($(if ($op.section) { [int]$op.section } else { 1 }))
1791
+ if (-not $op.kind) {
1792
+ $section.PageSetup.DifferentFirstPageHeaderFooter = 0
1793
+ $section.PageSetup.OddAndEvenPagesHeaderFooter = 0
1794
+ }
1783
1795
  $kind = switch ([string]$op.kind) {
1784
1796
  'first' { 2 }
1785
1797
  'even' { 3 }
1786
1798
  default { 1 }
1787
1799
  }
1788
- $footer = $section.Footers.Item($kind)
1789
- $range = $footer.Range
1790
- $range.Text = $(if ($null -ne $op.prefix) { [string]$op.prefix } else { 'Page ' })
1791
- $range = $footer.Range
1792
- $range.Collapse(0)
1793
- $null = $footer.Range.Fields.Add($range, -1, 'PAGE', $true)
1794
- if ($null -eq $op.includeTotal -or [bool]$op.includeTotal) {
1800
+ $footerKinds = if ($op.kind) { @($kind) } else { @(1, 2, 3) }
1801
+ foreach ($footerKind in $footerKinds) {
1802
+ $footer = $section.Footers.Item($footerKind)
1803
+ $footer.Exists = $true
1795
1804
  $range = $footer.Range
1796
- $range.Collapse(0)
1797
- $range.InsertAfter($(if ($null -ne $op.separator) { [string]$op.separator } else { ' of ' }))
1805
+ $range.Text = $(if ($null -ne $op.prefix) { [string]$op.prefix } else { 'Page ' })
1798
1806
  $range = $footer.Range
1799
1807
  $range.Collapse(0)
1800
- $null = $footer.Range.Fields.Add($range, -1, 'NUMPAGES', $true)
1808
+ $null = $footer.Range.Fields.Add($range, -1, 'PAGE', $true)
1809
+ if ($null -eq $op.includeTotal -or [bool]$op.includeTotal) {
1810
+ $range = $footer.Range
1811
+ $range.Collapse(0)
1812
+ $range.InsertAfter($(if ($null -ne $op.separator) { [string]$op.separator } else { ' of ' }))
1813
+ $range = $footer.Range
1814
+ $range.Collapse(0)
1815
+ $null = $footer.Range.Fields.Add($range, -1, 'NUMPAGES', $true)
1816
+ }
1817
+ $footer.Range.ParagraphFormat.Alignment = switch (([string]$op.alignment).ToLowerInvariant()) {
1818
+ 'left' { 0 }
1819
+ 'right' { 2 }
1820
+ default { 1 }
1821
+ }
1822
+ try {
1823
+ $normalFont = $doc.Styles.Item(-1).Font
1824
+ if ($normalFont.Name) { $footer.Range.Font.Name = [string]$normalFont.Name }
1825
+ if ($normalFont.NameFarEast) { $footer.Range.Font.NameFarEast = [string]$normalFont.NameFarEast }
1826
+ $footer.Range.Font.Size = 9
1827
+ } catch {}
1801
1828
  }
1802
1829
  return [ordered]@{ op = 'add_page_numbers'; changed = $true; section = [int]$section.Index }
1803
1830
  }
@@ -2134,10 +2161,62 @@ function Apply-ExcelOperation($book, $op) {
2134
2161
  $width = if ($op.width) { [single]$op.width } else { [single]480 }
2135
2162
  $height = if ($op.height) { [single]$op.height } else { [single]280 }
2136
2163
  $shape = $sheet.Shapes.AddChart2(-1, $chartType, $left, $top, $width, $height)
2137
- if ($op.range) { $shape.Chart.SetSourceData($sheet.Range([string]$op.range), 2) }
2138
- if ($op.title) { $shape.Chart.HasTitle = $true; $shape.Chart.ChartTitle.Text = [string]$op.title }
2139
- $seriesCount = [int]$shape.Chart.SeriesCollection().Count
2140
- $pointCount = if ($seriesCount -gt 0) { [int]$shape.Chart.SeriesCollection().Item(1).Points().Count } else { 0 }
2164
+ $chart = $shape.Chart
2165
+ if ($op.range) { $chart.SetSourceData($sheet.Range([string]$op.range), 2) }
2166
+ if ($op.title) { $chart.HasTitle = $true; $chart.ChartTitle.Text = [string]$op.title }
2167
+ $seriesCount = [int]$chart.SeriesCollection().Count
2168
+ $palette = @($op.seriesColors)
2169
+ for ($seriesIndex = 1; $seriesIndex -le $seriesCount; $seriesIndex++) {
2170
+ $series = $chart.SeriesCollection().Item($seriesIndex)
2171
+ if ($palette.Count -gt 0) {
2172
+ $color = Color-Value ([string]$palette[($seriesIndex - 1) % $palette.Count])
2173
+ try { $series.Format.Fill.Solid(); $series.Format.Fill.ForeColor.RGB = $color } catch {}
2174
+ try { $series.Format.Line.ForeColor.RGB = $color } catch {}
2175
+ }
2176
+ if ([bool]$op.showValues) {
2177
+ try {
2178
+ $series.ApplyDataLabels()
2179
+ $labels = $series.DataLabels()
2180
+ $labels.ShowValue = $true
2181
+ $labels.ShowCategoryName = $false
2182
+ if ($op.valueNumberFormat) { $labels.NumberFormat = [string]$op.valueNumberFormat }
2183
+ if ($op.dataLabelPosition) {
2184
+ $labels.Position = switch (([string]$op.dataLabelPosition).ToLowerInvariant()) {
2185
+ 'center' { -4108 }
2186
+ 'inside_base' { 4 }
2187
+ 'inside_end' { 3 }
2188
+ 'outside_end' { 2 }
2189
+ default { 5 }
2190
+ }
2191
+ }
2192
+ if ($op.dataLabelColor) { $labels.Font.Color = Color-Value ([string]$op.dataLabelColor) }
2193
+ } catch {}
2194
+ }
2195
+ }
2196
+ $chart.HasLegend = $(if ($null -ne $op.showLegend) { [bool]$op.showLegend } else { $seriesCount -gt 1 })
2197
+ try { $chart.ChartArea.Format.Line.Visible = 0 } catch {}
2198
+ try { $chart.PlotArea.Format.Fill.Visible = 0 } catch {}
2199
+ try {
2200
+ $categoryAxis = $chart.Axes(1, 1)
2201
+ $categoryAxis.CategoryType = 2
2202
+ $categoryAxis.TickLabelPosition = 4
2203
+ $categoryAxis.TickLabels.Orientation = 0
2204
+ } catch {}
2205
+ try {
2206
+ $valueAxis = $chart.Axes(2, 1)
2207
+ if ([bool]$op.zeroBaseline) { $valueAxis.MinimumScale = 0 }
2208
+ if ($op.valueNumberFormat) { $valueAxis.TickLabels.NumberFormat = [string]$op.valueNumberFormat }
2209
+ } catch {}
2210
+ try { $chart.ChartGroups(1).GapWidth = 90 } catch {}
2211
+ if ([bool]$op.showValues -and ([string]$op.dataLabelPosition).ToLowerInvariant() -eq 'inside_end') {
2212
+ try {
2213
+ $currentInsideWidth = [single]$chart.PlotArea.InsideWidth
2214
+ if ($currentInsideWidth -gt 100) {
2215
+ $chart.PlotArea.InsideWidth = [single]($currentInsideWidth - 18)
2216
+ }
2217
+ } catch {}
2218
+ }
2219
+ $pointCount = if ($seriesCount -gt 0) { [int]$chart.SeriesCollection().Item(1).Points().Count } else { 0 }
2141
2220
  return [ordered]@{ op = 'add_chart'; changed = $true; name = [string]$shape.Name; series = $seriesCount; categories = $pointCount }
2142
2221
  }
2143
2222
  'add_conditional_format' {
@@ -2283,6 +2362,7 @@ function Apply-ExcelOperation($book, $op) {
2283
2362
  if ($null -ne $op.fitToPagesWide) { $setup.FitToPagesWide = [int]$op.fitToPagesWide }
2284
2363
  if ($null -ne $op.fitToPagesTall) { $setup.FitToPagesTall = [int]$op.fitToPagesTall }
2285
2364
  if ($null -ne $op.centerHorizontally) { $setup.CenterHorizontally = [bool]$op.centerHorizontally }
2365
+ if ($null -ne $op.centerVertically) { $setup.CenterVertically = [bool]$op.centerVertically }
2286
2366
  if ($null -ne $op.topMargin) { $setup.TopMargin = [double]$op.topMargin }
2287
2367
  if ($null -ne $op.bottomMargin) { $setup.BottomMargin = [double]$op.bottomMargin }
2288
2368
  if ($null -ne $op.leftMargin) { $setup.LeftMargin = [double]$op.leftMargin }
@@ -2360,23 +2440,23 @@ function Set-PowerPointChartData($chart, $categoryValues, $seriesValues) {
2360
2440
  $null = ($source.Value2 = $matrix)
2361
2441
  $collection = $chart.SeriesCollection()
2362
2442
  $placeholderSeriesCount = [int]$collection.Count
2363
- $categoryArray = New-Object 'object[]' $pointCount
2364
- for ($pointIndex = 0; $pointIndex -lt $pointCount; $pointIndex++) {
2365
- $categoryArray[$pointIndex] = if ($pointIndex -lt $categories.Count) { $categories[$pointIndex] } else { "Item $($pointIndex + 1)" }
2366
- }
2443
+ $sheetName = ([string]$worksheet.Name).Replace("'", "''")
2444
+ $lastRow = $pointCount + 1
2367
2445
  for ($seriesIndex = 1; $seriesIndex -le $specs.Count; $seriesIndex++) {
2368
2446
  $series = $collection.NewSeries()
2369
- $values = @($specs[$seriesIndex - 1].values)
2370
- $valueArray = New-Object 'object[]' $pointCount
2371
- for ($pointIndex = 0; $pointIndex -lt $pointCount; $pointIndex++) {
2372
- if ($pointIndex -lt $values.Count) { $valueArray[$pointIndex] = $values[$pointIndex] }
2373
- }
2447
+ $column = Excel-ColumnLabel ($seriesIndex + 1)
2374
2448
  $null = ($series.Name = [string]$specs[$seriesIndex - 1].name)
2375
- $null = ($series.XValues = $categoryArray)
2376
- $null = ($series.Values = $valueArray)
2449
+ $null = ($series.XValues = "='$sheetName'!`$A`$2:`$A`$$lastRow")
2450
+ $null = ($series.Values = "='$sheetName'!`$$column`$2:`$$column`$$lastRow")
2377
2451
  }
2378
2452
  for ($seriesIndex = 0; $seriesIndex -lt $placeholderSeriesCount; $seriesIndex++) { $null = $collection.Item(1).Delete() }
2379
2453
  $null = $chart.Refresh()
2454
+ try {
2455
+ $categoryAxis = $chart.Axes(1, 1)
2456
+ $categoryAxis.CategoryType = 2
2457
+ $categoryAxis.TickLabelPosition = 4
2458
+ $categoryAxis.TickLabels.Orientation = 0
2459
+ } catch {}
2380
2460
  } finally {
2381
2461
  if ($null -ne $workbook) { try { $null = $workbook.Close($true) } catch {} }
2382
2462
  if ($null -ne $worksheet) { try { [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($worksheet) } catch {} }
@@ -3400,6 +3480,8 @@ function Issues-PowerPoint($presentation, $payload) {
3400
3480
  $shapeIndex = 0
3401
3481
  $textShapeCount = 0
3402
3482
  $visualShapeCount = 0
3483
+ $largeTextShapeCount = 0
3484
+ $dominantTextShapeCount = 0
3403
3485
  foreach ($shape in @($slide.Shapes)) {
3404
3486
  $shapeIndex++
3405
3487
  $path = "/slide[$($slide.SlideIndex)]/shape[$shapeIndex]"
@@ -3422,6 +3504,8 @@ function Issues-PowerPoint($presentation, $payload) {
3422
3504
  $fontIssue = Missing-FontIssue $fonts ([string]$shape.TextFrame.TextRange.Font.Name) $path
3423
3505
  if ($fontIssue) { $issues += $fontIssue }
3424
3506
  $fontSize = [single]$shape.TextFrame.TextRange.Font.Size
3507
+ if ($fontSize -ge 34) { $largeTextShapeCount++ }
3508
+ if ($fontSize -ge 42) { $dominantTextShapeCount++ }
3425
3509
  if ($fontSize -gt 0 -and $fontSize -lt 12) {
3426
3510
  $issues += Office-Issue 'warning' 'small_font' $path "Text uses $fontSize pt; presentation body text should normally be at least 12 pt."
3427
3511
  }
@@ -3481,7 +3565,9 @@ function Issues-PowerPoint($presentation, $payload) {
3481
3565
  } catch {}
3482
3566
  }
3483
3567
  }
3484
- if ($textShapeCount -gt 0 -and $visualShapeCount -eq 0) {
3568
+ $typographicVisual = $dominantTextShapeCount -gt 0 -or $largeTextShapeCount -ge 2
3569
+ $boundarySlide = [int]$slide.SlideIndex -eq 1 -or [int]$slide.SlideIndex -eq [int]$presentation.Slides.Count
3570
+ if ($textShapeCount -gt 0 -and $visualShapeCount -eq 0 -and -not $typographicVisual -and -not $boundarySlide) {
3485
3571
  $issues += Office-Issue 'warning' 'text_only_slide' "/slide[$($slide.SlideIndex)]" 'Slide contains text but no visual shape, chart, table, diagram, or picture.'
3486
3572
  }
3487
3573
  if ($payload.auditProfile -eq 'model-backed-deck') {