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
@@ -4,6 +4,12 @@ import {
4
4
  normalizeOfficeContentModel,
5
5
  summarizeOfficeContentModel,
6
6
  } from './content-model.mjs';
7
+ import {
8
+ planOfficeComposition,
9
+ resolveOfficeCompositionContext,
10
+ reviewOfficeCompositionSequence,
11
+ summarizeOfficeCompositions,
12
+ } from './composition-system.mjs';
7
13
 
8
14
  const FORMATS = new Set(['docx', 'xlsx', 'pptx', 'pdf', 'csv', 'tsv']);
9
15
  const PPTX_BACKGROUND_MODES = new Set(['sandwich', 'light', 'dark', 'custom']);
@@ -286,6 +292,8 @@ function compactDesign(design) {
286
292
  audience: design.audience,
287
293
  tone: design.tone,
288
294
  density: design.density,
295
+ purpose: design.purpose,
296
+ expressionMode: design.expressionMode,
289
297
  signature: design.signature,
290
298
  source: design.source,
291
299
  library: design.library,
@@ -322,6 +330,7 @@ function compactLibrary(library) {
322
330
  ...(library.coverage ? { coverage: clone(library.coverage) } : {}),
323
331
  } : null,
324
332
  templateIndexRevision: String(library.templateIndexRevision || ''),
333
+ recentCompositionCount: Array.isArray(library.recentCompositions) ? library.recentCompositions.length : 0,
325
334
  pinned: library.pinned === true,
326
335
  warning: String(library.warning || ''),
327
336
  };
@@ -378,6 +387,9 @@ export function resolveOfficeDesign(format, request = {}, { library = null } = {
378
387
  ])),
379
388
  typography,
380
389
  });
390
+ const composition = resolveOfficeCompositionContext(normalizedFormat, input, {
391
+ recentCompositions: library?.recentCompositions,
392
+ });
381
393
  const deck = normalizedFormat === 'pptx' ? resolvePptxDeckPlan(input, tokens) : null;
382
394
  return {
383
395
  profile,
@@ -389,6 +401,9 @@ export function resolveOfficeDesign(format, request = {}, { library = null } = {
389
401
  density: ['light', 'balanced', 'dense'].includes(String(input.density || '').toLowerCase())
390
402
  ? String(input.density).toLowerCase()
391
403
  : 'balanced',
404
+ purpose: composition.purpose,
405
+ expressionMode: composition.expressionMode,
406
+ recentCompositions: composition.recentCompositions,
392
407
  signature: String(input.signature || ''),
393
408
  source: String(input.source || library?.source || 'mixdog-starter'),
394
409
  content: normalizeOfficeContentModel(input.content),
@@ -400,6 +415,7 @@ export function resolveOfficeDesign(format, request = {}, { library = null } = {
400
415
  ...(normalizedFormat === 'pptx' ? {
401
416
  slidePlans: Array.isArray(input.slidePlans) ? clone(input.slidePlans) : [],
402
417
  } : {}),
418
+ compositions: Array.isArray(input.compositions) ? clone(input.compositions) : [],
403
419
  review: {
404
420
  required: normalizedFormat === 'pptx'
405
421
  ? true
@@ -487,11 +503,17 @@ function pptxLayoutFit(layout, operation, design, usage) {
487
503
  const densityPenalty = densityDistance(layout.density || design.density, demand.density);
488
504
  const textSlots = Math.max(Number(capacity.textSlots) || 0, slots.filter((slot) => slot.type === 'text').length);
489
505
  const textSlotShortfall = nativeSample ? Math.max(0, demand.textItems - textSlots) : 0;
506
+ const plannedVariant = String(operation.__composition?.variant || '').toLowerCase();
507
+ const purposeFit = !layout.purposes?.length || layout.purposes.includes(design.purpose);
508
+ const expressionFit = !layout.expressionModes?.length || layout.expressionModes.includes(design.expressionMode);
490
509
  const score = (
491
510
  (layout.profile === design.profile ? 4 : 0)
492
511
  + (layout.density === demand.density ? 7 : 0)
493
512
  - densityPenalty * 2
494
513
  + (String(operation.variant || '').toLowerCase() && layout.variant === String(operation.variant).toLowerCase() ? 2 : 0)
514
+ + (plannedVariant && layout.variant === plannedVariant ? 3 : 0)
515
+ + (purposeFit ? 4 : -6)
516
+ + (expressionFit ? 2 : -3)
495
517
  + demand.capabilities.filter((capability) => layout.capabilities?.includes(capability)).length * 4
496
518
  + Math.min(demand.metrics, available.metrics) * 3
497
519
  + Math.min(demand.columns, available.columns) * 3
@@ -512,6 +534,8 @@ function pptxLayoutFit(layout, operation, design, usage) {
512
534
  textSlots,
513
535
  textSlotShortfall,
514
536
  nativeSample,
537
+ purpose: purposeFit,
538
+ expressionMode: expressionFit,
515
539
  },
516
540
  };
517
541
  }
@@ -572,7 +596,7 @@ function templateSlotText(operation, role) {
572
596
  return undefined;
573
597
  }
574
598
 
575
- function expandTemplatePptxSlide(operation, layout, slide) {
599
+ function expandTemplatePptxSlide(operation, layout, slide, backend = '') {
576
600
  if (
577
601
  operation.create === false
578
602
  || !layout?.templatePath
@@ -611,6 +635,9 @@ function expandTemplatePptxSlide(operation, layout, slide) {
611
635
  path: imagePath,
612
636
  });
613
637
  } else if (slot.type === 'chart' && plainObject(operation.chart)) {
638
+ if (backend === 'mixdog-ooxml') {
639
+ throw new Error('compose_slide chart requires Microsoft PowerPoint; omit chart or create the deck with PowerPoint installed');
640
+ }
614
641
  output.push({
615
642
  op: 'set_chart_data',
616
643
  slide,
@@ -743,10 +770,12 @@ function provenanceText(source) {
743
770
  return target ? `${document}#${target}` : document;
744
771
  }
745
772
 
746
- function expandPptxSlide(operation, design, slide) {
773
+ function expandPptxSlide(operation, design, slide, backend = '') {
774
+ const portable = backend === 'mixdog-ooxml';
747
775
  const colors = design.tokens.colors;
748
776
  const type = design.tokens.typography;
749
777
  const kind = String(operation.kind || 'content').toLowerCase();
778
+ const compositionVariant = String(operation.__composition?.variant || '').toLowerCase();
750
779
  if (!PPTX_COMPONENTS.includes(kind)) {
751
780
  throw new Error(`Unknown compose_slide kind "${kind}". Use one of: ${PPTX_COMPONENTS.join(', ')}`);
752
781
  }
@@ -770,16 +799,17 @@ function expandPptxSlide(operation, design, slide) {
770
799
  const contentTitleSize = pptTitleSize(title, Number(operation.titleSize) || 34);
771
800
 
772
801
  if (kind === 'cover') {
802
+ const minimalFocus = compositionVariant === 'minimal-focus';
773
803
  if (eyebrow) output.push(pptText(slide, eyebrow.toUpperCase(), {
774
804
  left: 58, top: 58, width: 420, height: 24,
775
805
  fontName: type.body, fontSize: 12, bold: true, color: colors.accent,
776
806
  }));
777
807
  output.push(pptText(slide, title, {
778
- left: 58, top: 126, width: 720, height: 170,
779
- fontName: type.display, fontSize: pptTitleSize(title, Number(operation.titleSize) || 44), bold: true, color: colors.onInverse,
808
+ left: 58, top: minimalFocus ? 150 : 126, width: minimalFocus ? 842 : 720, height: 180,
809
+ fontName: type.display, fontSize: pptTitleSize(title, Number(operation.titleSize) || (minimalFocus ? 48 : 44)), bold: true, color: colors.onInverse,
780
810
  }));
781
811
  if (subtitle) output.push(pptText(slide, subtitle, {
782
- left: 58, top: 326, width: 650, height: 72,
812
+ left: 58, top: minimalFocus ? 350 : 326, width: minimalFocus ? 760 : 650, height: 72,
783
813
  fontName: type.body, fontSize: 18, color: mutedColor,
784
814
  }));
785
815
  const meta = strings(operation.meta);
@@ -787,39 +817,30 @@ function expandPptxSlide(operation, design, slide) {
787
817
  left: 58, top: 474, width: 720, height: 22,
788
818
  fontName: type.body, fontSize: 12, color: mutedColor,
789
819
  }));
790
- output.push(pptShape(slide, 'oval', {
791
- left: 734, top: 92, width: 62, height: 62,
792
- fillColor: colors.accent, lineColor: colors.accent,
793
- }));
794
- output.push(pptShape(slide, 'right_arrow', {
795
- left: 802, top: 110, width: 46, height: 26,
796
- fillColor: colors.accent2, lineColor: colors.accent2,
797
- }));
798
- output.push(pptShape(slide, 'oval', {
799
- left: 854, top: 92, width: 62, height: 62,
800
- fillColor: colors.surface2, lineColor: colors.surface2,
801
- }));
802
820
  } else if (kind === 'statement') {
803
- output.push(pptShape(slide, 'oval', {
804
- left: 714, top: 154, width: 164, height: 164,
805
- fillColor: colors.surface, lineColor: colors.surface,
806
- }));
807
821
  if (eyebrow) output.push(pptText(slide, eyebrow.toUpperCase(), {
808
822
  left: 58, top: 52, width: 420, height: 22,
809
823
  fontName: type.body, fontSize: 12, bold: true, color: colors.accent,
810
824
  }));
811
825
  const metric = operation.metric || operation.metrics?.[0] || {};
812
- if (metric.value != null) output.push(pptText(slide, String(metric.value), {
813
- left: 674, top: 188, width: 244, height: 92,
814
- fontName: type.display, fontSize: 64, bold: true, color: colors.accent, alignment: 'center',
815
- }));
816
- if (metric.label) output.push(pptText(slide, String(metric.label), {
817
- left: 734, top: 286, width: 124, height: 24,
818
- fontName: type.body, fontSize: 12, bold: true, color: colors.muted, alignment: 'center',
819
- }));
826
+ const showMetric = metric.value != null && compositionVariant !== 'typographic-statement';
827
+ if (showMetric) {
828
+ output.push(pptShape(slide, 'oval', {
829
+ left: 714, top: 154, width: 164, height: 164,
830
+ fillColor: colors.surface, lineColor: colors.surface,
831
+ }));
832
+ output.push(pptText(slide, metricDisplayValue(metric), {
833
+ left: 674, top: 188, width: 244, height: 92,
834
+ fontName: type.display, fontSize: 64, bold: true, color: colors.accent, alignment: 'center',
835
+ }));
836
+ if (metric.label) output.push(pptText(slide, String(metric.label), {
837
+ left: 734, top: 286, width: 124, height: 24,
838
+ fontName: type.body, fontSize: 12, bold: true, color: colors.muted, alignment: 'center',
839
+ }));
840
+ }
820
841
  output.push(pptText(slide, title, {
821
- left: 58, top: 144, width: metric.value != null ? 560 : 790, height: 150,
822
- fontName: type.display, fontSize: pptTitleSize(title, Number(operation.titleSize) || 38), bold: true, color: colors.ink,
842
+ left: 58, top: showMetric ? 144 : 132, width: showMetric ? 560 : 810, height: showMetric ? 150 : 188,
843
+ fontName: type.display, fontSize: pptTitleSize(title, Number(operation.titleSize) || (showMetric ? 38 : 44)), bold: true, color: colors.ink,
823
844
  }));
824
845
  if (subtitle) output.push(pptText(slide, subtitle, {
825
846
  left: 58, top: 356, width: 650, height: 76,
@@ -835,51 +856,74 @@ function expandPptxSlide(operation, design, slide) {
835
856
  fontName: type.body, fontSize: 15, color: colors.muted,
836
857
  }));
837
858
  const metrics = Array.isArray(operation.metrics) ? operation.metrics.slice(0, 4) : [];
838
- metrics.forEach((metric, index) => {
839
- const primary = index === 0;
840
- const left = primary ? 58 : 520;
841
- const top = primary ? 208 : 192 + ((index - 1) * 118);
842
- output.push(pptText(slide, metricDisplayValue(metric), {
843
- left, top, width: primary ? 392 : 320, height: primary ? 96 : 62,
844
- fontName: type.display, fontSize: primary ? 58 : 40, bold: true,
845
- color: primary ? colors.accent : colors.ink,
846
- }));
847
- output.push(pptText(slide, String(metric.label || ''), {
848
- left, top: top + (primary ? 104 : 58), width: primary ? 360 : 320, height: 32,
849
- fontName: type.body, fontSize: 13, bold: true, color: colors.muted,
850
- }));
851
- if (metric.detail) output.push(pptText(slide, String(metric.detail), {
852
- left, top: top + (primary ? 148 : 86), width: primary ? 390 : 340, height: 38,
853
- fontName: type.body, fontSize: 12, color: colors.muted,
854
- }));
855
- if (!primary) output.push(pptShape(slide, 'oval', {
856
- left: 486, top: top + 16, width: 14, height: 14,
857
- fillColor: index === 1 ? colors.accent2 : colors.surface2,
858
- lineColor: index === 1 ? colors.accent2 : colors.surface2,
859
- }));
860
- });
859
+ if (compositionVariant === 'metric-band') {
860
+ const metricWidth = 824 / Math.max(1, metrics.length);
861
+ metrics.forEach((metric, index) => {
862
+ const left = 58 + (metricWidth * index);
863
+ output.push(pptText(slide, metricDisplayValue(metric), {
864
+ left, top: 214, width: metricWidth - 18, height: 88,
865
+ fontName: type.display, fontSize: metrics.length >= 4 ? 42 : 50, bold: true,
866
+ color: index === 0 ? colors.accent : colors.ink,
867
+ }));
868
+ output.push(pptText(slide, String(metric.label || ''), {
869
+ left, top: 316, width: metricWidth - 18, height: 34,
870
+ fontName: type.body, fontSize: 13, bold: true, color: colors.muted,
871
+ }));
872
+ if (metric.detail) output.push(pptText(slide, String(metric.detail), {
873
+ left, top: 360, width: metricWidth - 22, height: 54,
874
+ fontName: type.body, fontSize: 12, color: colors.muted,
875
+ }));
876
+ });
877
+ } else {
878
+ metrics.forEach((metric, index) => {
879
+ const primary = index === 0;
880
+ const left = primary ? 58 : 520;
881
+ const top = primary ? 208 : 192 + ((index - 1) * 118);
882
+ output.push(pptText(slide, metricDisplayValue(metric), {
883
+ left, top, width: primary ? 392 : 320, height: primary ? 96 : 62,
884
+ fontName: type.display, fontSize: primary ? 58 : 40, bold: true,
885
+ color: primary ? colors.accent : colors.ink,
886
+ }));
887
+ output.push(pptText(slide, String(metric.label || ''), {
888
+ left, top: top + (primary ? 104 : 58), width: primary ? 360 : 320, height: 32,
889
+ fontName: type.body, fontSize: 13, bold: true, color: colors.muted,
890
+ }));
891
+ if (metric.detail) output.push(pptText(slide, String(metric.detail), {
892
+ left, top: top + (primary ? 148 : 86), width: primary ? 390 : 340, height: 38,
893
+ fontName: type.body, fontSize: 12, color: colors.muted,
894
+ }));
895
+ if (!primary) output.push(pptShape(slide, 'oval', {
896
+ left: 486, top: top + 16, width: 14, height: 14,
897
+ fillColor: index === 1 ? colors.accent2 : colors.surface2,
898
+ lineColor: index === 1 ? colors.accent2 : colors.surface2,
899
+ }));
900
+ });
901
+ }
861
902
  } else if (kind === 'comparison') {
862
903
  output.push(pptText(slide, title, {
863
904
  left: 58, top: 42, width: 842, height: 76,
864
905
  fontName: type.display, fontSize: contentTitleSize, bold: true, color: colors.ink,
865
906
  }));
866
907
  const columns = Array.isArray(operation.columns) ? operation.columns.slice(0, 2) : [];
867
- const fills = [colors.surface, colors.inverse];
908
+ const alignedEvidence = compositionVariant === 'aligned-evidence';
909
+ const fills = alignedEvidence ? [colors.canvas, colors.canvas] : [colors.surface, colors.inverse];
868
910
  columns.forEach((column, index) => {
869
911
  const left = index === 0 ? 58 : 492;
870
912
  const fill = fills[index];
871
- const foreground = index === 0 ? colors.ink : colors.onInverse;
872
- output.push(pptShape(slide, 'rectangle', {
873
- left, top: 142, width: 410, height: 328,
874
- fillColor: fill, lineColor: fill,
875
- }));
913
+ const foreground = alignedEvidence ? colors.ink : index === 0 ? colors.ink : colors.onInverse;
914
+ if (!alignedEvidence) {
915
+ output.push(pptShape(slide, 'rectangle', {
916
+ left, top: 142, width: 410, height: 328,
917
+ fillColor: fill, lineColor: fill,
918
+ }));
919
+ }
876
920
  output.push(pptText(slide, String(column.title || ''), {
877
921
  left: left + 28, top: 174, width: 340, height: 48,
878
- fontName: type.display, fontSize: 24, bold: true, color: foreground,
922
+ fontName: type.display, fontSize: 24, bold: true, color: alignedEvidence ? colors.accent : foreground,
879
923
  }));
880
924
  const paragraphs = pptBulletParagraphs(column.items || column.body, design, {
881
925
  size: 15,
882
- color: index === 0 ? colors.muted : colors.surface2,
926
+ color: alignedEvidence || index === 0 ? colors.muted : colors.surface2,
883
927
  });
884
928
  output.push(pptText(slide, '', {
885
929
  left: left + 28, top: 244, width: 340, height: 176,
@@ -892,33 +936,38 @@ function expandPptxSlide(operation, design, slide) {
892
936
  }));
893
937
  const steps = Array.isArray(operation.steps) ? operation.steps.slice(0, 5) : [];
894
938
  const width = 820 / Math.max(1, steps.length);
939
+ const staggered = compositionVariant === 'staggered-flow';
895
940
  steps.forEach((step, index) => {
896
941
  const left = 58 + (width * index);
942
+ const top = staggered ? (index % 2 === 0 ? 160 : 236) : 172;
897
943
  output.push(pptShape(slide, 'oval', {
898
- left, top: 172, width: 58, height: 58,
944
+ left, top, width: 58, height: 58,
899
945
  fillColor: index === 0 ? colors.accent : colors.surface2,
900
946
  lineColor: index === 0 ? colors.accent : colors.surface2,
901
947
  }));
902
948
  output.push(pptText(slide, String(index + 1).padStart(2, '0'), {
903
- left, top: 187, width: 58, height: 30,
949
+ left, top: top + 15, width: 58, height: 30,
904
950
  fontName: type.data, fontSize: 22, bold: true,
905
951
  color: index === 0 ? colors.onAccent : colors.muted,
906
952
  alignment: 'center',
907
953
  }));
908
954
  output.push(pptText(slide, String(step.title || step.label || ''), {
909
- left: left - 24, top: 260, width: 106, height: 50,
955
+ left: left - 24, top: top + 88, width: 106, height: 50,
910
956
  fontName: type.body, fontSize: 15, bold: true, color: colors.ink, alignment: 'center',
911
957
  }));
912
958
  if (step.detail || step.body) output.push(pptText(slide, String(step.detail || step.body), {
913
- left: left - 44, top: 324, width: 146, height: 94,
959
+ left: Math.max(20, left - 40), top: top + 146, width: 150, height: 80,
914
960
  fontName: type.body, fontSize: 12, color: colors.muted, alignment: 'center',
915
961
  }));
916
962
  if (index < steps.length - 1) output.push(pptShape(slide, 'right_arrow', {
917
- left: left + width - 78, top: 190, width: 52, height: 20,
963
+ left: left + width - 78, top: staggered ? 220 : 190, width: 52, height: 20,
918
964
  fillColor: colors.accent2, lineColor: colors.accent2,
919
965
  }));
920
966
  });
921
967
  } else if (kind === 'chart' || (kind === 'content' && plainObject(operation.chart))) {
968
+ if (portable) {
969
+ throw new Error('compose_slide chart requires Microsoft PowerPoint; use kind "metrics" or "table" for a portable deck');
970
+ }
922
971
  output.push(pptText(slide, title, {
923
972
  left: 58, top: 42, width: 842, height: 76,
924
973
  fontName: type.display, fontSize: contentTitleSize, bold: true, color: colors.ink,
@@ -930,7 +979,10 @@ function expandPptxSlide(operation, design, slide) {
930
979
  const paragraphs = operation.bullets
931
980
  ? pptBulletParagraphs(operation.bullets, design, { size: 14 })
932
981
  : pptBodyParagraphs(operation.body, design, { size: 14 });
933
- if (paragraphs.length) output.push(pptText(slide, '', {
982
+ const chartLed = compositionVariant === 'chart-led';
983
+ if (paragraphs.length) output.push(pptText(slide, '', chartLed ? {
984
+ left: 58, top: 440, width: 842, height: 54,
985
+ } : {
934
986
  left: 58, top: 184, width: 274, height: 238,
935
987
  }, paragraphs));
936
988
  const chart = operation.chart || {};
@@ -944,12 +996,13 @@ function expandPptxSlide(operation, design, slide) {
944
996
  ...entry,
945
997
  color: entry?.color || (index === 0 ? colors.accent : index === 1 ? colors.accent2 : colors.muted),
946
998
  })) : [],
947
- left: paragraphs.length ? 360 : 58,
999
+ left: chartLed || !paragraphs.length ? 58 : 360,
948
1000
  top: 176,
949
- width: paragraphs.length ? 540 : 842,
950
- height: 286,
1001
+ width: chartLed || !paragraphs.length ? 842 : 540,
1002
+ height: chartLed && paragraphs.length ? 244 : 286,
951
1003
  });
952
1004
  } else if (kind === 'table' || (kind === 'content' && (Array.isArray(operation.table) || plainObject(operation.table)))) {
1005
+ const tableCallout = compositionVariant === 'table-callout';
953
1006
  output.push(pptText(slide, title, {
954
1007
  left: 58, top: 42, width: 842, height: 76,
955
1008
  fontName: type.display, fontSize: contentTitleSize, bold: true, color: colors.ink,
@@ -968,17 +1021,17 @@ function expandPptxSlide(operation, design, slide) {
968
1021
  op: 'add_table',
969
1022
  slide,
970
1023
  values: Array.isArray(values) ? values : [],
971
- left: 58,
972
- top: 174,
973
- width: 842,
1024
+ left: tableCallout ? 116 : 58,
1025
+ top: tableCallout ? 184 : 174,
1026
+ width: tableCallout ? 728 : 842,
974
1027
  height: tableHeight,
975
1028
  properties: {
976
1029
  fontName: type.body,
977
1030
  fontSize: 13,
978
1031
  color: colors.ink,
979
- headerFillColor: colors.inverse,
980
- headerColor: colors.onInverse,
981
- bodyFillColor: colors.canvas,
1032
+ headerFillColor: tableCallout ? colors.accent : colors.inverse,
1033
+ headerColor: tableCallout ? colors.onAccent : colors.onInverse,
1034
+ bodyFillColor: tableCallout ? colors.canvas : colors.surface,
982
1035
  headerRowHeight: 42,
983
1036
  bodyRowHeight,
984
1037
  },
@@ -991,8 +1044,9 @@ function expandPptxSlide(operation, design, slide) {
991
1044
  const paragraphs = operation.bullets
992
1045
  ? pptBulletParagraphs(operation.bullets, design, { size: 16 })
993
1046
  : pptBodyParagraphs(operation.body, design, { size: 16 });
1047
+ const visualLeft = compositionVariant === 'visual-left';
994
1048
  output.push(pptText(slide, '', {
995
- left: 58, top: 156, width: 374, height: 280,
1049
+ left: visualLeft ? 526 : 58, top: 156, width: 374, height: 280,
996
1050
  }, paragraphs));
997
1051
  const imagePath = operation.image?.path || operation.imagePath;
998
1052
  if (imagePath) {
@@ -1000,45 +1054,49 @@ function expandPptxSlide(operation, design, slide) {
1000
1054
  op: 'add_image',
1001
1055
  slide,
1002
1056
  path: imagePath,
1003
- left: 492,
1057
+ left: visualLeft ? 58 : 492,
1004
1058
  top: 142,
1005
1059
  width: 408,
1006
1060
  height: 306,
1007
1061
  });
1008
1062
  } else {
1009
1063
  output.push(pptShape(slide, 'rectangle', {
1010
- left: 492, top: 142, width: 408, height: 306,
1064
+ left: visualLeft ? 58 : 492, top: 142, width: 408, height: 306,
1011
1065
  fillColor: colors.inverse, lineColor: colors.inverse,
1012
1066
  }));
1013
1067
  output.push(pptText(slide, String(operation.visualText || subtitle || ''), {
1014
- left: 530, top: 214, width: 330, height: 156,
1068
+ left: visualLeft ? 96 : 530, top: 214, width: 330, height: 156,
1015
1069
  fontName: type.display, fontSize: 30, bold: true, color: colors.onInverse,
1016
1070
  }));
1017
1071
  }
1018
1072
  } else if (kind === 'closing') {
1073
+ const decisionFocus = compositionVariant === 'decision-focus';
1019
1074
  if (eyebrow) output.push(pptText(slide, eyebrow.toUpperCase(), {
1020
- left: 58, top: 68, width: 420, height: 22,
1075
+ left: decisionFocus ? 220 : 58, top: 68, width: decisionFocus ? 520 : 420, height: 22,
1021
1076
  fontName: type.body, fontSize: 12, bold: true, color: colors.accent,
1077
+ ...(decisionFocus ? { alignment: 'center' } : {}),
1022
1078
  }));
1023
1079
  output.push(pptText(slide, title, {
1024
- left: 58, top: 156, width: operation.visualText ? 650 : 760, height: 142,
1080
+ left: decisionFocus ? 120 : 58, top: 156, width: decisionFocus ? 720 : operation.visualText ? 410 : 760, height: 142,
1025
1081
  fontName: type.display, fontSize: pptTitleSize(title, 42), bold: true, color: colors.onInverse,
1082
+ ...(decisionFocus ? { alignment: 'center' } : {}),
1026
1083
  }));
1027
1084
  if (subtitle) output.push(pptText(slide, subtitle, {
1028
- left: 58, top: 330, width: 690, height: 72,
1085
+ left: decisionFocus ? 150 : 58, top: 330, width: decisionFocus ? 660 : 690, height: 72,
1029
1086
  fontName: type.body, fontSize: 17, color: mutedColor,
1087
+ ...(decisionFocus ? { alignment: 'center' } : {}),
1030
1088
  }));
1031
1089
  if (operation.visualText) {
1032
1090
  output.push(pptShape(slide, 'oval', {
1033
- left: 770, top: 160, width: 126, height: 126,
1091
+ left: decisionFocus ? 417 : 770, top: decisionFocus ? 398 : 160, width: 126, height: 126,
1034
1092
  fillColor: colors.accent, lineColor: colors.accent,
1035
1093
  }));
1036
1094
  output.push(pptText(slide, String(operation.visualText), {
1037
- left: 770, top: 184, width: 126, height: 54,
1095
+ left: decisionFocus ? 417 : 770, top: decisionFocus ? 422 : 184, width: 126, height: 54,
1038
1096
  fontName: type.display, fontSize: 34, bold: true, color: colors.onAccent, alignment: 'center',
1039
1097
  }));
1040
1098
  if (operation.visualLabel) output.push(pptText(slide, String(operation.visualLabel), {
1041
- left: 785, top: 246, width: 96, height: 24,
1099
+ left: decisionFocus ? 432 : 785, top: decisionFocus ? 484 : 246, width: 96, height: 24,
1042
1100
  fontName: type.body, fontSize: 12, bold: true, color: colors.onAccent, alignment: 'center',
1043
1101
  }));
1044
1102
  }
@@ -1051,14 +1109,30 @@ function expandPptxSlide(operation, design, slide) {
1051
1109
  left: 58, top: 116, width: 760, height: 38,
1052
1110
  fontName: type.body, fontSize: 14, color: mutedColor,
1053
1111
  }));
1054
- const paragraphs = operation.bullets
1055
- ? pptBulletParagraphs(operation.bullets, design)
1056
- : pptBodyParagraphs(operation.body, design);
1057
- output.push(pptText(slide, '', {
1058
- left: 58, top: 178, width: 590, height: 250,
1059
- }, paragraphs));
1060
1112
  const visualText = String(operation.visualText || '');
1061
- if (visualText) {
1113
+ const editorialWide = compositionVariant === 'editorial-wide';
1114
+ const body = strings(operation.bullets || operation.body);
1115
+ if (editorialWide) {
1116
+ const lead = visualText || body.shift() || subtitle;
1117
+ if (lead) output.push(pptText(slide, lead, {
1118
+ left: 58, top: 176, width: 804, height: 132,
1119
+ fontName: type.display, fontSize: pptTitleSize(lead, 42), bold: true, color: colors.accent,
1120
+ }));
1121
+ const support = operation.bullets
1122
+ ? pptBulletParagraphs(body, design, { size: 16 })
1123
+ : pptBodyParagraphs(body, design, { size: 16 });
1124
+ if (support.length) output.push(pptText(slide, '', {
1125
+ left: 58, top: 332, width: 760, height: 118,
1126
+ }, support));
1127
+ } else {
1128
+ const paragraphs = operation.bullets
1129
+ ? pptBulletParagraphs(operation.bullets, design)
1130
+ : pptBodyParagraphs(operation.body, design);
1131
+ output.push(pptText(slide, '', {
1132
+ left: 58, top: 178, width: 590, height: 250,
1133
+ }, paragraphs));
1134
+ }
1135
+ if (visualText && !editorialWide) {
1062
1136
  output.push(pptShape(slide, 'rectangle', {
1063
1137
  left: 680, top: 178, width: 220, height: 220,
1064
1138
  fillColor: colors.surface, lineColor: colors.surface,
@@ -1075,20 +1149,31 @@ function expandPptxSlide(operation, design, slide) {
1075
1149
  return output;
1076
1150
  }
1077
1151
 
1078
- function expandDocxDocument(operation, design, state, backend) {
1152
+ function expandDocxDocument(operation, design, state, backend, composition) {
1079
1153
  const output = [];
1080
1154
  const colors = design.tokens.colors;
1081
1155
  const type = design.tokens.typography;
1082
1156
  const format = design.format;
1157
+ const compositionId = String(composition?.id || 'decision-brief');
1158
+ const compactMemo = compositionId === 'compact-memo';
1159
+ const editorialReport = compositionId === 'editorial-report';
1160
+ const evidenceBrief = compositionId === 'evidence-brief';
1161
+ const decisionBrief = compositionId === 'decision-brief';
1162
+ const pageMargin = compactMemo
1163
+ ? Math.max(46.8, format.margin * 0.84)
1164
+ : editorialReport
1165
+ ? format.margin * 1.08
1166
+ : format.margin;
1167
+ const bodySize = compactMemo ? Math.max(9.5, format.body - 0.5) : format.body;
1083
1168
  if (backend === 'microsoft-office-com' && operation.page !== false) {
1084
1169
  output.push({
1085
1170
  op: 'set_page',
1086
1171
  properties: {
1087
1172
  orientation: operation.orientation || 'portrait',
1088
- topMargin: format.margin,
1089
- bottomMargin: format.margin,
1090
- leftMargin: format.margin,
1091
- rightMargin: format.margin,
1173
+ topMargin: pageMargin,
1174
+ bottomMargin: pageMargin,
1175
+ leftMargin: pageMargin,
1176
+ rightMargin: pageMargin,
1092
1177
  },
1093
1178
  });
1094
1179
  }
@@ -1105,73 +1190,72 @@ function expandDocxDocument(operation, design, state, backend) {
1105
1190
  };
1106
1191
  append(operation.title, 'Title', {
1107
1192
  name: type.display,
1108
- size: Number(operation.titleSize) || format.title,
1193
+ size: Number(operation.titleSize) || format.title + (editorialReport ? 3 : compactMemo ? -1 : 0),
1109
1194
  bold: true,
1110
1195
  color: colors.ink,
1111
1196
  spacingBefore: 0,
1112
- spacingAfter: 8,
1197
+ spacingAfter: editorialReport ? 12 : compactMemo ? 5 : 8,
1113
1198
  keepWithNext: true,
1114
1199
  });
1115
1200
  append(operation.subtitle, 'Normal', {
1116
1201
  name: type.body,
1117
- size: format.body,
1202
+ size: bodySize,
1118
1203
  color: colors.muted,
1119
1204
  spacingBefore: 0,
1120
- spacingAfter: 12,
1121
- lineSpacing: format.body * 1.35,
1205
+ spacingAfter: compactMemo ? 7 : 12,
1206
+ lineSpacing: bodySize * 1.35,
1122
1207
  });
1123
1208
  const meta = strings(operation.meta);
1124
1209
  if (meta.length) {
1125
1210
  append(meta.join(' · '), 'Normal', {
1126
1211
  name: type.body,
1127
- size: Math.max(8.5, format.body - 1),
1212
+ size: Math.max(8.5, bodySize - 1),
1128
1213
  color: colors.muted,
1129
1214
  spacingBefore: 0,
1130
- spacingAfter: 14,
1215
+ spacingAfter: compactMemo ? 8 : 14,
1131
1216
  });
1132
1217
  }
1133
1218
  if (operation.summary) {
1134
1219
  append(strings(operation.summary).join(' '), 'Normal', {
1135
1220
  name: type.display,
1136
- size: format.body + 1,
1221
+ size: bodySize + (decisionBrief ? 1.5 : 1),
1137
1222
  bold: true,
1138
- color: colors.accent,
1139
- spacingBefore: 4,
1140
- spacingAfter: 14,
1141
- lineSpacing: (format.body + 1) * 1.35,
1223
+ color: decisionBrief ? colors.accent : colors.ink,
1224
+ spacingBefore: compactMemo ? 2 : 4,
1225
+ spacingAfter: editorialReport ? 18 : compactMemo ? 9 : 14,
1226
+ lineSpacing: (bodySize + 1) * 1.35,
1142
1227
  keepWithNext: true,
1143
- border: { side: 'left', color: colors.accent },
1144
1228
  });
1145
1229
  }
1146
1230
  for (const section of Array.isArray(operation.sections) ? operation.sections : []) {
1147
1231
  append(section.heading || section.title, Number(section.level) === 2 ? 'Heading 2' : 'Heading 1', {
1148
1232
  name: type.display,
1149
- size: Number(section.level) === 2 ? format.heading2 : format.heading1,
1233
+ size: (Number(section.level) === 2 ? format.heading2 : format.heading1) + (editorialReport ? 1 : 0),
1150
1234
  bold: true,
1151
- color: section.accent === false ? colors.ink : colors.accent,
1152
- spacingBefore: Number(section.level) === 2 ? 9 : 14,
1153
- spacingAfter: 5,
1235
+ color: section.accent === false || evidenceBrief ? colors.ink : colors.accent,
1236
+ spacingBefore: compactMemo ? (Number(section.level) === 2 ? 6 : 10) : Number(section.level) === 2 ? 9 : 14,
1237
+ spacingAfter: editorialReport ? 7 : 5,
1154
1238
  keepWithNext: true,
1155
1239
  pageBreakBefore: section.pageBreak === true,
1156
1240
  });
1157
1241
  for (const paragraph of strings(section.paragraphs || section.body)) {
1158
1242
  append(paragraph, 'Normal', {
1159
1243
  name: type.body,
1160
- size: format.body,
1244
+ size: bodySize,
1161
1245
  color: colors.ink,
1162
1246
  spacingBefore: 0,
1163
- spacingAfter: 6,
1164
- lineSpacing: format.body * 1.4,
1247
+ spacingAfter: compactMemo ? 4 : editorialReport ? 8 : 6,
1248
+ lineSpacing: bodySize * (compactMemo ? 1.32 : 1.4),
1165
1249
  });
1166
1250
  }
1167
1251
  for (const bullet of strings(section.bullets)) {
1168
1252
  append(bullet, 'Normal', {
1169
1253
  name: type.body,
1170
- size: format.body,
1254
+ size: bodySize,
1171
1255
  color: colors.ink,
1172
1256
  spacingBefore: 0,
1173
1257
  spacingAfter: 3,
1174
- lineSpacing: format.body * 1.35,
1258
+ lineSpacing: bodySize * 1.35,
1175
1259
  listKind: 'bullet',
1176
1260
  listLevel: 0,
1177
1261
  });
@@ -1179,12 +1263,12 @@ function expandDocxDocument(operation, design, state, backend) {
1179
1263
  if (section.quote) {
1180
1264
  append(section.quote, 'Quote', {
1181
1265
  name: type.display,
1182
- size: format.body + 1,
1266
+ size: bodySize + (editorialReport ? 2 : 1),
1183
1267
  italic: true,
1184
1268
  color: colors.accent,
1185
1269
  spacingBefore: 5,
1186
1270
  spacingAfter: 9,
1187
- lineSpacing: (format.body + 1) * 1.35,
1271
+ lineSpacing: (bodySize + 1) * 1.35,
1188
1272
  });
1189
1273
  }
1190
1274
  if (Array.isArray(section.table) && section.table.length) {
@@ -1196,7 +1280,7 @@ function expandDocxDocument(operation, design, state, backend) {
1196
1280
  style: 'Table Grid',
1197
1281
  textStyle: 'Normal',
1198
1282
  fontName: type.body,
1199
- fontSize: Math.max(9, format.body - 1),
1283
+ fontSize: Math.max(9, bodySize - 1),
1200
1284
  color: colors.ink,
1201
1285
  spacingAfter: 0,
1202
1286
  borders: true,
@@ -1212,8 +1296,8 @@ function expandDocxDocument(operation, design, state, backend) {
1212
1296
  row: 1,
1213
1297
  col: column,
1214
1298
  properties: {
1215
- fillColor: colors.inverse,
1216
- color: colors.onInverse,
1299
+ fillColor: evidenceBrief ? colors.accent : colors.inverse,
1300
+ color: evidenceBrief ? colors.onAccent : colors.onInverse,
1217
1301
  bold: true,
1218
1302
  verticalAlignment: 'center',
1219
1303
  },
@@ -1222,11 +1306,15 @@ function expandDocxDocument(operation, design, state, backend) {
1222
1306
  }
1223
1307
  }
1224
1308
  }
1225
- if (backend === 'microsoft-office-com' && operation.footer) {
1226
- output.push({ op: 'set_header_footer', header: false, text: String(operation.footer) });
1227
- }
1228
1309
  if (backend === 'microsoft-office-com' && operation.pageNumbers === true) {
1229
- output.push({ op: 'add_page_numbers', includeTotal: true });
1310
+ output.push({
1311
+ op: 'add_page_numbers',
1312
+ includeTotal: true,
1313
+ alignment: 'center',
1314
+ ...(operation.footer ? { prefix: `${String(operation.footer)} · Page ` } : {}),
1315
+ });
1316
+ } else if (backend === 'microsoft-office-com' && operation.footer) {
1317
+ output.push({ op: 'set_header_footer', header: false, text: String(operation.footer) });
1230
1318
  }
1231
1319
  return output;
1232
1320
  }
@@ -1252,11 +1340,17 @@ function isExcelTotalRow(row) {
1252
1340
  return /^(?:(?:grand\s+total|sub\s*total|total)\b|(?:합계|총계|소계)(?:\s|$))/i.test(String(row?.[0] || '').trim());
1253
1341
  }
1254
1342
 
1255
- function expandXlsxSheet(operation, design) {
1343
+ function expandXlsxSheet(operation, design, composition, backend = '') {
1344
+ const portable = backend === 'mixdog-ooxml';
1256
1345
  const output = [];
1257
1346
  const colors = design.tokens.colors;
1258
1347
  const type = design.tokens.typography;
1259
1348
  const format = design.format;
1349
+ const compositionId = String(composition?.id || 'monitor-dashboard');
1350
+ const trendDashboard = compositionId === 'trend-dashboard';
1351
+ const comparisonBoard = compositionId === 'comparison-board';
1352
+ const analysisSheet = compositionId === 'analysis-sheet';
1353
+ const narrativeScorecard = compositionId === 'narrative-scorecard';
1260
1354
  const sheet = String(operation.sheet || 'Sheet1');
1261
1355
  const headers = strings(operation.headers);
1262
1356
  const rows = Array.isArray(operation.rows) ? operation.rows : [];
@@ -1285,8 +1379,8 @@ function expandXlsxSheet(operation, design) {
1285
1379
  fontName: type.display,
1286
1380
  fontSize: Number(operation.titleSize) || format.title,
1287
1381
  bold: true,
1288
- color: colors.onInverse,
1289
- fillColor: colors.inverse,
1382
+ color: analysisSheet ? colors.ink : narrativeScorecard ? colors.onAccent : colors.onInverse,
1383
+ fillColor: analysisSheet ? colors.surface2 : narrativeScorecard ? colors.accent : colors.inverse,
1290
1384
  verticalAlignment: 'center',
1291
1385
  wrapText: true,
1292
1386
  },
@@ -1336,10 +1430,10 @@ function expandXlsxSheet(operation, design) {
1336
1430
  range: `${start}${valueRow}:${end}${valueRow}`,
1337
1431
  properties: {
1338
1432
  fontName: type.data,
1339
- fontSize: 22,
1433
+ fontSize: narrativeScorecard ? 25 : comparisonBoard ? 20 : 22,
1340
1434
  bold: true,
1341
- color: index === 0 ? colors.onAccent : colors.ink,
1342
- fillColor: index === 0 ? colors.accent : colors.surface,
1435
+ color: index === 0 && !analysisSheet ? colors.onAccent : colors.ink,
1436
+ fillColor: index === 0 && !analysisSheet ? colors.accent : trendDashboard ? colors.canvas : colors.surface,
1343
1437
  numberFormat: metric?.numberFormat || 'General',
1344
1438
  horizontalAlignment: 'center',
1345
1439
  verticalAlignment: 'center',
@@ -1375,8 +1469,8 @@ function expandXlsxSheet(operation, design) {
1375
1469
  fontName: type.body,
1376
1470
  fontSize: format.body,
1377
1471
  bold: true,
1378
- color: colors.ink,
1379
- fillColor: colors.surface2,
1472
+ color: narrativeScorecard ? colors.onInverse : colors.ink,
1473
+ fillColor: narrativeScorecard ? colors.inverse : trendDashboard ? colors.surface : colors.surface2,
1380
1474
  wrapText: true,
1381
1475
  },
1382
1476
  });
@@ -1408,13 +1502,15 @@ function expandXlsxSheet(operation, design) {
1408
1502
  wrapText: true,
1409
1503
  },
1410
1504
  });
1411
- output.push({
1412
- op: 'add_table',
1413
- sheet,
1414
- range: `A${startRow}:${dataLastColumn}${endRow}`,
1415
- name: safeTableName(operation.tableName || `${sheet}Data`),
1416
- style: operation.tableStyle || format.tableStyle,
1417
- });
1505
+ if (!portable) {
1506
+ output.push({
1507
+ op: 'add_table',
1508
+ sheet,
1509
+ range: `A${startRow}:${dataLastColumn}${endRow}`,
1510
+ name: safeTableName(operation.tableName || `${sheet}Data`),
1511
+ style: operation.tableStyle || format.tableStyle,
1512
+ });
1513
+ }
1418
1514
  output.push({ op: 'freeze_panes', sheet, row: startRow + 1, column: 1 });
1419
1515
  }
1420
1516
  output.push({
@@ -1441,31 +1537,68 @@ function expandXlsxSheet(operation, design) {
1441
1537
  });
1442
1538
  }
1443
1539
  if (plainObject(operation.chart)) {
1540
+ if (portable) {
1541
+ throw new Error('compose_sheet chart requires Microsoft Excel; omit chart or create the workbook with Excel installed');
1542
+ }
1444
1543
  let chartRows = rows.length;
1445
1544
  while (chartRows > 0 && isExcelTotalRow(rows[chartRows - 1])) chartRows -= 1;
1446
1545
  const chartEndRow = startRow + (headers.length ? 1 : 0) + chartRows - 1;
1447
1546
  const chartRange = operation.chart.range
1448
1547
  || `A${startRow}:${dataLastColumn}${Math.max(startRow, chartEndRow)}`;
1548
+ const chartType = operation.chart.type || 'column';
1549
+ const showValues = operation.chart.showValues ?? (dashboard && chartRows <= 6);
1550
+ const dataLabelPosition = operation.chart.dataLabelPosition
1551
+ || (showValues && ['column', 'bar'].includes(String(chartType).toLowerCase()) ? 'inside_end' : '');
1552
+ const chartDefaults = trendDashboard
1553
+ ? { left: 360, top: 172, width: 510, height: 286 }
1554
+ : comparisonBoard
1555
+ ? { left: 390, top: 184, width: 480, height: 278 }
1556
+ : analysisSheet
1557
+ ? { left: 520, top: 40, width: 480, height: 280 }
1558
+ : narrativeScorecard
1559
+ ? { left: 430, top: 206, width: 450, height: 258 }
1560
+ : { left: dashboard ? 410 : 520, top: dashboard ? 190 : 40, width: dashboard ? 420 : 480, height: 280 };
1449
1561
  output.push({
1450
1562
  op: 'add_chart',
1451
1563
  sheet,
1452
1564
  range: chartRange,
1453
- chartType: operation.chart.type || 'column',
1565
+ chartType,
1454
1566
  title: operation.chart.title || '',
1455
- left: Number(operation.chart.left) || (dashboard ? 390 : 520),
1456
- top: Number(operation.chart.top) || (dashboard ? 220 : 40),
1457
- width: Number(operation.chart.width) || (dashboard ? 400 : 480),
1458
- height: Number(operation.chart.height) || (dashboard ? 260 : 280),
1567
+ left: Number(operation.chart.left) || chartDefaults.left,
1568
+ top: Number(operation.chart.top) || chartDefaults.top,
1569
+ width: Number(operation.chart.width) || chartDefaults.width,
1570
+ height: Number(operation.chart.height) || chartDefaults.height,
1571
+ seriesColors: operation.chart.seriesColors || [colors.accent, colors.accent2, colors.muted],
1572
+ showValues,
1573
+ ...(dataLabelPosition ? { dataLabelPosition } : {}),
1574
+ ...(dataLabelPosition === 'inside_end' ? {
1575
+ dataLabelColor: operation.chart.dataLabelColor || colors.onAccent,
1576
+ } : {}),
1577
+ zeroBaseline: operation.chart.zeroBaseline ?? ['column', 'bar'].includes(String(chartType).toLowerCase()),
1578
+ ...(operation.chart.showLegend == null ? {} : { showLegend: operation.chart.showLegend }),
1579
+ ...(operation.chart.valueNumberFormat ? { valueNumberFormat: operation.chart.valueNumberFormat } : {}),
1459
1580
  });
1460
1581
  }
1461
1582
  }
1462
1583
  if (operation.source) {
1463
- output.push({
1464
- op: 'add_note',
1465
- sheet,
1466
- cell: 'A1',
1467
- text: `Source: ${provenanceText(operation.source) || String(operation.source)}`,
1468
- });
1584
+ const attribution = `Source: ${provenanceText(operation.source) || String(operation.source)}`;
1585
+ if (portable) {
1586
+ const captionRow = Math.max(row, startRow + values.length) + 1;
1587
+ output.push({ op: 'set_cell', sheet, cell: `A${captionRow}`, value: attribution });
1588
+ output.push({
1589
+ op: 'set_style',
1590
+ sheet,
1591
+ range: `A${captionRow}:${dataLastColumn}${captionRow}`,
1592
+ properties: {
1593
+ fontName: type.body,
1594
+ fontSize: 9,
1595
+ italic: true,
1596
+ color: colors.muted,
1597
+ },
1598
+ });
1599
+ } else {
1600
+ output.push({ op: 'add_note', sheet, cell: 'A1', text: attribution });
1601
+ }
1469
1602
  }
1470
1603
  output.push({ op: 'autofit_range', sheet, range: `A:${dataLastColumn}` });
1471
1604
  const printColumns = plainObject(operation.chart) ? Math.max(columns, 14) : columns;
@@ -1477,8 +1610,14 @@ function expandXlsxSheet(operation, design) {
1477
1610
  fitToPagesWide: 1,
1478
1611
  fitToPagesTall: dashboard ? 1 : 0,
1479
1612
  centerHorizontally: true,
1613
+ centerVertically: dashboard && !analysisSheet,
1614
+ });
1615
+ output.push({
1616
+ op: 'set_sheet_view',
1617
+ sheet,
1618
+ showGridlines: false,
1619
+ zoom: analysisSheet ? 100 : trendDashboard ? 85 : dashboard ? 90 : 100,
1480
1620
  });
1481
- output.push({ op: 'set_sheet_view', sheet, showGridlines: false, zoom: dashboard ? 90 : 100 });
1482
1621
  return output;
1483
1622
  }
1484
1623
 
@@ -1498,19 +1637,28 @@ export function expandOfficeDesignOperations({
1498
1637
  let nextSlide = created && Number(snapshotVersion || 0) === 0 ? 1 : null;
1499
1638
  const docxState = { paragraph: 0, table: 0 };
1500
1639
  const layoutUsage = new Map();
1640
+ const compositionUsage = new Map();
1501
1641
  for (const operation of operations || []) {
1502
1642
  const bound = bindOfficeContent(operation, design.content);
1503
1643
  const contentOperation = bound.operation;
1504
1644
  const name = String(contentOperation?.op || '');
1505
1645
  if (normalizedFormat === 'pptx' && name === 'compose_slide') {
1506
- const slide = Number(contentOperation.slide) || nextSlide;
1646
+ const composition = planOfficeComposition(normalizedFormat, contentOperation, design, {
1647
+ usage: compositionUsage,
1648
+ });
1649
+ const plannedOperation = {
1650
+ ...contentOperation,
1651
+ kind: composition.kind,
1652
+ __composition: composition,
1653
+ };
1654
+ const slide = Number(plannedOperation.slide) || nextSlide;
1507
1655
  if (!slide) throw new Error('compose_slide requires slide for an existing presentation');
1508
- const selectedLayout = selectPptxLayout(contentOperation, design, layoutUsage);
1656
+ const selectedLayout = selectPptxLayout(plannedOperation, design, layoutUsage);
1509
1657
  const layout = selectedLayout?.layout || null;
1510
- const composed = layout ? merge(layout.defaults || {}, contentOperation) : contentOperation;
1511
- const backgroundSpec = pptxBackgroundSpec(composed, design, String(contentOperation.kind || 'content').toLowerCase(), slide);
1512
- const plan = pptxSlidePlan(composed, String(contentOperation.kind || 'content').toLowerCase(), slide);
1513
- const templateOperations = expandTemplatePptxSlide(composed, layout, slide);
1658
+ const composed = layout ? merge(layout.defaults || {}, plannedOperation) : plannedOperation;
1659
+ const backgroundSpec = pptxBackgroundSpec(composed, design, composition.kind, slide);
1660
+ const plan = pptxSlidePlan(composed, composition.kind, slide);
1661
+ const templateOperations = expandTemplatePptxSlide(composed, layout, slide, backend);
1514
1662
  if (templateOperations) {
1515
1663
  output.push(...templateOperations);
1516
1664
  layoutUsage.set(layout.id, (layoutUsage.get(layout.id) || 0) + 1);
@@ -1518,15 +1666,23 @@ export function expandOfficeDesignOperations({
1518
1666
  if (design.deck.templateMode === 'strict') {
1519
1667
  throw new Error(`compose_slide requires an approved native template layout for kind "${operation.kind}" in strict mode`);
1520
1668
  }
1521
- output.push(...expandPptxSlide(composed, design, slide));
1669
+ output.push(...expandPptxSlide(composed, design, slide, backend));
1522
1670
  }
1523
1671
  semantic.push({
1524
1672
  op: name,
1525
- kind: String(contentOperation.kind || 'content'),
1673
+ kind: composition.kind,
1674
+ requestedKind: String(contentOperation.kind || 'content'),
1526
1675
  slide,
1527
1676
  slideRole: backgroundSpec.slideRole,
1528
1677
  backgroundRole: backgroundSpec.backgroundRole,
1529
1678
  plan,
1679
+ composition: layout ? {
1680
+ ...composition,
1681
+ id: `${composition.kind}:template:${layout.id}`,
1682
+ family: 'native-template',
1683
+ variant: layout.variant || composition.variant,
1684
+ source: 'native-template',
1685
+ } : composition,
1530
1686
  renderMode: templateOperations ? 'native-template' : 'scratch',
1531
1687
  ...(bound.binding ? { contentBinding: bound.binding } : {}),
1532
1688
  ...(layout ? {
@@ -1545,20 +1701,27 @@ export function expandOfficeDesignOperations({
1545
1701
  continue;
1546
1702
  }
1547
1703
  if (normalizedFormat === 'docx' && name === 'compose_document') {
1548
- output.push(...expandDocxDocument(contentOperation, design, docxState, backend));
1704
+ const composition = planOfficeComposition(normalizedFormat, contentOperation, design, {
1705
+ usage: compositionUsage,
1706
+ });
1707
+ output.push(...expandDocxDocument(contentOperation, design, docxState, backend, composition));
1549
1708
  semantic.push({
1550
1709
  op: name,
1551
1710
  sections: Array.isArray(contentOperation.sections) ? contentOperation.sections.length : 0,
1711
+ composition,
1552
1712
  ...(bound.binding ? { contentBinding: bound.binding } : {}),
1553
1713
  });
1554
1714
  continue;
1555
1715
  }
1556
1716
  if (normalizedFormat === 'xlsx' && name === 'compose_sheet') {
1557
- if (backend !== 'microsoft-office-com') throw new Error('compose_sheet requires Microsoft Excel');
1558
- output.push(...expandXlsxSheet(contentOperation, design));
1717
+ const composition = planOfficeComposition(normalizedFormat, contentOperation, design, {
1718
+ usage: compositionUsage,
1719
+ });
1720
+ output.push(...expandXlsxSheet(contentOperation, design, composition, backend));
1559
1721
  semantic.push({
1560
1722
  op: name,
1561
1723
  sheet: String(contentOperation.sheet || 'Sheet1'),
1724
+ composition,
1562
1725
  ...(bound.binding ? { contentBinding: bound.binding } : {}),
1563
1726
  });
1564
1727
  continue;
@@ -1573,6 +1736,7 @@ export function expandOfficeDesignOperations({
1573
1736
  semantic,
1574
1737
  design: compactDesign(design),
1575
1738
  content: summarizeOfficeContentModel(design.content),
1739
+ composition: summarizeOfficeCompositions(normalizedFormat, semantic),
1576
1740
  };
1577
1741
  }
1578
1742
 
@@ -1931,12 +2095,19 @@ export function reviewOfficeDesign({
1931
2095
  } = {}) {
1932
2096
  const normalizedFormat = String(format || '').toLowerCase();
1933
2097
  const design = resolveOfficeDesign(normalizedFormat, request, { library });
2098
+ const compositionReview = reviewOfficeCompositionSequence({
2099
+ format: normalizedFormat,
2100
+ compositions: design.compositions,
2101
+ recentCompositions: design.recentCompositions,
2102
+ allowRepetition: design.review.allowRepetition,
2103
+ });
1934
2104
  if (!design.review.required) {
1935
2105
  return {
1936
2106
  ok: true,
1937
2107
  status: 'skipped',
1938
2108
  profile: design.profile,
1939
2109
  issues: [],
2110
+ composition: compositionReview.summary,
1940
2111
  requiresVisualInspection: false,
1941
2112
  };
1942
2113
  }
@@ -1948,11 +2119,29 @@ export function reviewOfficeDesign({
1948
2119
  const issues = normalizedFormat === 'pptx'
1949
2120
  ? [...reviewPptx(document, design), ...structureIssues]
1950
2121
  : structureIssues;
2122
+ if (
2123
+ compositionReview.repeated
2124
+ && !issues.some((entry) => entry.code === 'repetitive_composition')
2125
+ ) {
2126
+ issues.push(designIssue(
2127
+ 'repetitive_composition',
2128
+ '/',
2129
+ `${compositionReview.repeated.count} of ${compositionReview.repeated.total} semantic compositions reuse ${compositionReview.repeated.id}.`,
2130
+ ));
2131
+ }
2132
+ if (compositionReview.recentMatch) {
2133
+ issues.push(designIssue(
2134
+ 'recent_composition_repeat',
2135
+ '/',
2136
+ 'The complete composition sequence matches a recent deliverable; recompose the structure while preserving brand constraints.',
2137
+ ));
2138
+ }
1951
2139
  return {
1952
2140
  ok: issues.length === 0,
1953
2141
  status: issues.length ? 'needs-polish' : 'pass',
1954
2142
  profile: design.profile,
1955
2143
  issues,
2144
+ composition: compositionReview.summary,
1956
2145
  requiresVisualInspection: !['csv', 'tsv'].includes(normalizedFormat),
1957
2146
  modelReview: [
1958
2147
  'Inspect every rendered page, not only lint counts.',