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
@@ -54,19 +54,19 @@ const CATALOG = {
54
54
  fields: ['toc', 'page', 'numPages', 'pageBreak', 'sectionBreak'],
55
55
  links: ['address', 'subAddress', 'display', 'bookmark'],
56
56
  provenance: ['source.document', 'source.target', 'source.label'],
57
- design: ['profile', 'intent', 'audience', 'tone', 'density', 'palette', 'typography', 'signature', 'content.packageId', 'content.audience', 'content.objective', 'content.decision', 'content.period', 'content.facts', 'content.claims', 'review'],
57
+ design: ['profile', 'purpose', 'expressionMode', 'intent', 'audience', 'tone', 'density', 'palette', 'typography', 'signature', 'content.packageId', 'content.audience', 'content.objective', 'content.decision', 'content.period', 'content.facts', 'content.claims', 'review'],
58
58
  },
59
59
  },
60
60
  xlsx: {
61
61
  paths: ['/sheet[NAME]', '/sheet[NAME]/cell[A1]', '/sheet[NAME]/range[A1:C10]'],
62
62
  operations: {
63
- common: ['replace_text', 'set_cell', 'set_formula', 'set_range', 'append_row', 'clear_cell'],
64
- office: ['compose_sheet', 'add_sheet', 'copy_sheet', 'delete_sheet', 'rename_sheet', 'set_style', 'add_note', 'delete_note', 'add_image', 'add_table', 'add_chart', 'add_conditional_format', 'delete_conditional_formats', 'add_validation', 'freeze_panes', 'add_pivot_table', 'autofit_range', 'set_page_setup', 'set_sheet_view', 'set_sheet_visibility', 'insert_rows', 'delete_rows', 'insert_columns', 'delete_columns', 'merge_cells', 'unmerge_cells', 'set_autofilter', 'set_hyperlink', 'define_name', 'delete_name', 'protect_sheet', 'unprotect_sheet', 'add_provenance'],
63
+ common: ['replace_text', 'set_cell', 'set_formula', 'set_range', 'append_row', 'clear_cell', 'compose_sheet', 'add_sheet', 'delete_sheet', 'rename_sheet', 'set_style', 'merge_cells', 'unmerge_cells', 'freeze_panes', 'autofit_range', 'set_page_setup', 'set_sheet_view'],
64
+ office: ['copy_sheet', 'add_note', 'delete_note', 'add_image', 'add_table', 'add_chart', 'add_conditional_format', 'delete_conditional_formats', 'add_validation', 'add_pivot_table', 'set_sheet_visibility', 'insert_rows', 'delete_rows', 'insert_columns', 'delete_columns', 'set_autofilter', 'set_hyperlink', 'define_name', 'delete_name', 'protect_sheet', 'unprotect_sheet', 'add_provenance'],
65
65
  portable: [],
66
66
  },
67
67
  properties: {
68
68
  cellStyle: ['fontName', 'fontSize', 'bold', 'italic', 'color', 'fillColor', 'numberFormat', 'horizontalAlignment', 'verticalAlignment', 'wrapText'],
69
- chart: ['chartType', 'left', 'top', 'width', 'height', 'title'],
69
+ chart: ['chartType', 'left', 'top', 'width', 'height', 'title', 'seriesColors', 'showValues', 'showLegend', 'zeroBaseline', 'valueNumberFormat', 'dataLabelPosition', 'dataLabelColor'],
70
70
  table: ['name', 'style'],
71
71
  pivot: ['source', 'destination', 'name', 'rows', 'columns', 'values'],
72
72
  note: ['text', 'author'],
@@ -77,14 +77,14 @@ const CATALOG = {
77
77
  pageSetup: ['printArea', 'orientation', 'fitToPagesWide', 'fitToPagesTall', 'centerHorizontally', 'topMargin', 'bottomMargin', 'leftMargin', 'rightMargin'],
78
78
  sheetView: ['showGridlines', 'zoom'],
79
79
  provenance: ['source.document', 'source.target', 'source.label'],
80
- design: ['profile', 'intent', 'audience', 'tone', 'density', 'palette', 'typography', 'signature', 'content.packageId', 'content.audience', 'content.objective', 'content.decision', 'content.period', 'content.facts', 'content.claims', 'review'],
80
+ design: ['profile', 'purpose', 'expressionMode', 'intent', 'audience', 'tone', 'density', 'palette', 'typography', 'signature', 'content.packageId', 'content.audience', 'content.objective', 'content.decision', 'content.period', 'content.facts', 'content.claims', 'review'],
81
81
  },
82
82
  },
83
83
  pptx: {
84
84
  paths: ['/slide[N]', '/slide[N]/shape[N]'],
85
85
  operations: {
86
- common: ['replace_text', 'fill_template', 'set_text', 'add_textbox', 'delete_shape'],
87
- office: ['compose_slide', 'add_slide', 'delete_slide', 'move_slide', 'duplicate_slide', 'import_slides', 'keep_slides', 'set_notes', 'set_footer', 'set_slide_number', 'add_comment', 'delete_comment', 'add_image', 'replace_image', 'crop_image', 'add_media', 'set_shape', 'group_shapes', 'ungroup_shape', 'set_slide_background', 'set_layout', 'apply_theme', 'set_transition', 'add_animation', 'add_chart', 'fit_text', 'add_shape', 'add_table', 'set_table_data', 'set_chart_data', 'set_chart_series', 'set_chart_axis', 'set_chart_data_labels', 'set_chart_trendline', 'set_chart_error_bars', 'set_hyperlink', 'z_order', 'align_shapes', 'distribute_shapes', 'add_provenance'],
86
+ common: ['replace_text', 'fill_template', 'set_text', 'add_textbox', 'delete_shape', 'compose_slide', 'add_slide', 'delete_slide', 'move_slide', 'set_notes', 'add_image', 'add_shape', 'add_table', 'set_shape', 'set_slide_background', 'import_slides', 'replace_image', 'set_table_data'],
87
+ office: ['duplicate_slide', 'keep_slides', 'set_footer', 'set_slide_number', 'add_comment', 'delete_comment', 'crop_image', 'add_media', 'group_shapes', 'ungroup_shape', 'set_layout', 'apply_theme', 'set_transition', 'add_animation', 'add_chart', 'fit_text', 'set_chart_data', 'set_chart_series', 'set_chart_axis', 'set_chart_data_labels', 'set_chart_trendline', 'set_chart_error_bars', 'set_hyperlink', 'z_order', 'align_shapes', 'distribute_shapes', 'add_provenance'],
88
88
  portable: [],
89
89
  },
90
90
  properties: {
@@ -100,7 +100,7 @@ const CATALOG = {
100
100
  media: ['kind', 'link', 'embed', 'poster'],
101
101
  comment: ['author', 'initials', 'left', 'top'],
102
102
  provenance: ['source.document', 'source.target', 'source.label'],
103
- design: ['profile', 'intent', 'audience', 'tone', 'density', 'palette', 'typography', 'signature', 'content.packageId', 'content.audience', 'content.objective', 'content.decision', 'content.period', 'content.facts', 'content.claims', 'template', 'deck.backgroundMode', 'deck.dominantColorRole', 'deck.motif', 'deck.spacingScale', 'deck.sectionSlides', 'deck.roles', 'deck.requireSlidePlan', 'deck.templateMode', 'review', 'reviewed', 'reviewToken', 'critique'],
103
+ design: ['profile', 'purpose', 'expressionMode', 'intent', 'audience', 'tone', 'density', 'palette', 'typography', 'signature', 'content.packageId', 'content.audience', 'content.objective', 'content.decision', 'content.period', 'content.facts', 'content.claims', 'template', 'deck.backgroundMode', 'deck.dominantColorRole', 'deck.motif', 'deck.spacingScale', 'deck.sectionSlides', 'deck.roles', 'deck.requireSlidePlan', 'deck.templateMode', 'review', 'reviewed', 'reviewToken', 'critique'],
104
104
  },
105
105
  },
106
106
  pdf: {
@@ -143,9 +143,9 @@ const COMMON_SIGNATURES = {
143
143
 
144
144
  const FORMAT_SIGNATURES = {
145
145
  docx: {
146
- compose_document: signature(['title'], ['claimId', 'subtitle', 'summary', 'meta', 'sections', 'footer', 'orientation', 'pageNumbers'], {
146
+ compose_document: signature(['title'], ['claimId', 'purpose', 'expressionMode', 'variant', 'subtitle', 'summary', 'meta', 'sections', 'footer', 'orientation', 'pageNumbers'], {
147
147
  propertySets: ['design'],
148
- notes: 'Semantic create-time composition. Content is mapped to native Word title, heading, paragraph, list, table, section, and footer operations.',
148
+ notes: 'Purpose-aware native Word composition; variant is optional because content topology selects the default.',
149
149
  }),
150
150
  append_text: signature(['text'], ['style', 'properties'], {
151
151
  propertySets: ['paragraph', 'font', 'paragraphFormat'],
@@ -179,7 +179,7 @@ const FORMAT_SIGNATURES = {
179
179
  set_page: signature(['properties'], ['section'], { propertySets: ['page'] }),
180
180
  fit_table: signature(['table']),
181
181
  insert_toc: signature([], ['paragraph', 'lowerHeadingLevel', 'upperHeadingLevel']),
182
- add_page_numbers: signature([], ['section', 'kind', 'prefix', 'separator', 'includeTotal']),
182
+ add_page_numbers: signature([], ['section', 'kind', 'prefix', 'separator', 'includeTotal', 'alignment']),
183
183
  insert_break: signature([], ['paragraph', 'kind']),
184
184
  set_list: signature(['paragraph', 'kind'], ['level']),
185
185
  add_hyperlink: signature([], ['find', 'paragraph', 'address', 'subAddress', 'display'], {
@@ -190,9 +190,9 @@ const FORMAT_SIGNATURES = {
190
190
  add_provenance: signature(['paragraph', 'source'], [], { propertySets: ['provenance'] }),
191
191
  },
192
192
  xlsx: {
193
- compose_sheet: signature(['rows'], ['claimId', 'sheet', 'kind', 'title', 'subtitle', 'source', 'headers', 'metrics', 'insights', 'columnFormats', 'tableName', 'tableStyle', 'chart'], {
193
+ compose_sheet: signature(['rows'], ['claimId', 'purpose', 'expressionMode', 'variant', 'sheet', 'kind', 'title', 'subtitle', 'source', 'headers', 'metrics', 'insights', 'columnFormats', 'tableName', 'tableStyle', 'chart'], {
194
194
  propertySets: ['design'],
195
- notes: 'Semantic Excel composition with title hierarchy, native table styling, frozen headers, chart placement, and autofit.',
195
+ notes: 'Purpose-aware native Excel composition with content-selected dashboard, trend, comparison, scorecard, or analysis layout.',
196
196
  }),
197
197
  set_cell: signature(['cell', 'value'], ['sheet']),
198
198
  set_formula: signature(['cell', 'formula'], ['sheet']),
@@ -211,7 +211,7 @@ const FORMAT_SIGNATURES = {
211
211
  delete_note: signature(['cell'], ['sheet']),
212
212
  add_image: signature(['path'], ['sheet', 'left', 'top', 'width', 'height']),
213
213
  add_table: signature(['range'], ['sheet', 'name', 'style'], { propertySets: ['table'] }),
214
- add_chart: signature(['range'], ['sheet', 'chartType', 'title', 'left', 'top', 'width', 'height'], {
214
+ add_chart: signature(['range'], ['sheet', 'chartType', 'title', 'left', 'top', 'width', 'height', 'seriesColors', 'showValues', 'showLegend', 'zeroBaseline', 'valueNumberFormat', 'dataLabelPosition', 'dataLabelColor'], {
215
215
  propertySets: ['chart'],
216
216
  notes: 'The first source column supplies categories; remaining columns become series.',
217
217
  }),
@@ -223,7 +223,7 @@ const FORMAT_SIGNATURES = {
223
223
  autofit_range: signature(['range'], ['sheet', 'rows'], {
224
224
  notes: 'Accepts cell, whole-column, or whole-row ranges such as A1:D5, A:D, or 2:8.',
225
225
  }),
226
- set_page_setup: signature([], ['sheet', 'printArea', 'orientation', 'fitToPagesWide', 'fitToPagesTall', 'centerHorizontally', 'topMargin', 'bottomMargin', 'leftMargin', 'rightMargin'], {
226
+ set_page_setup: signature([], ['sheet', 'printArea', 'orientation', 'fitToPagesWide', 'fitToPagesTall', 'centerHorizontally', 'centerVertically', 'topMargin', 'bottomMargin', 'leftMargin', 'rightMargin'], {
227
227
  propertySets: ['pageSetup'],
228
228
  }),
229
229
  set_sheet_view: signature([], ['sheet', 'showGridlines', 'zoom'], {
@@ -247,9 +247,9 @@ const FORMAT_SIGNATURES = {
247
247
  add_provenance: signature(['cell', 'source'], ['sheet'], { propertySets: ['provenance'] }),
248
248
  },
249
249
  pptx: {
250
- compose_slide: signature(['kind'], ['claimId', 'title', 'subtitle', 'takeaway', 'eyebrow', 'body', 'bullets', 'metrics', 'columns', 'steps', 'chart', 'table', 'image', 'imagePath', 'visualText', 'visualLabel', 'meta', 'notes', 'source', 'background', 'backgroundRole', 'slideRole', 'plan', 'create', 'slide', 'layoutId', 'variant', 'titleSize'], {
250
+ compose_slide: signature(['kind'], ['claimId', 'purpose', 'expressionMode', 'title', 'subtitle', 'takeaway', 'eyebrow', 'body', 'bullets', 'metrics', 'columns', 'steps', 'chart', 'table', 'image', 'imagePath', 'visualText', 'visualLabel', 'meta', 'notes', 'source', 'background', 'backgroundRole', 'slideRole', 'plan', 'create', 'slide', 'layoutId', 'variant', 'titleSize'], {
251
251
  propertySets: ['design'],
252
- notes: 'Template-first semantic slide. Use a declarative title as the takeaway, subject-specific evidence, and source notes for material numbers. Approved sample slides preserve native masters/layouts; deck.templateMode:"strict" rejects scratch fallback. Kinds: cover, statement, content, split, comparison, metrics, process, chart, table, closing.',
252
+ notes: 'Template-first semantic slide; generic content is reclassified from its evidence, while explicit layoutId or variant stays pinned.',
253
253
  }),
254
254
  set_text: signature(['slide', 'shape', 'text']),
255
255
  add_textbox: signature(['slide', 'text'], ['paragraphs', 'left', 'top', 'width', 'height', 'fontName', 'fontSize', 'color', 'properties'], { propertySets: ['shape', 'authoring'] }),
@@ -0,0 +1,451 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ const PURPOSES = new Set(['monitor', 'decide', 'compare', 'explain', 'inspect']);
4
+ const EXPRESSION_MODES = new Set(['conservative', 'strong-fit', 'divergent']);
5
+
6
+ const FORMAT_CANDIDATES = Object.freeze({
7
+ docx: Object.freeze([
8
+ Object.freeze({
9
+ id: 'decision-brief',
10
+ family: 'decision',
11
+ purposes: ['decide'],
12
+ modes: ['conservative', 'strong-fit'],
13
+ evidence: ['claims', 'facts'],
14
+ densities: ['balanced', 'dense'],
15
+ novelty: 0,
16
+ }),
17
+ Object.freeze({
18
+ id: 'evidence-brief',
19
+ family: 'evidence',
20
+ purposes: ['decide', 'compare', 'inspect'],
21
+ modes: ['strong-fit', 'divergent'],
22
+ evidence: ['table', 'facts'],
23
+ densities: ['balanced', 'dense'],
24
+ novelty: 1,
25
+ }),
26
+ Object.freeze({
27
+ id: 'editorial-report',
28
+ family: 'narrative',
29
+ purposes: ['explain'],
30
+ modes: ['strong-fit', 'divergent'],
31
+ evidence: ['narrative', 'claims'],
32
+ densities: ['light', 'balanced'],
33
+ novelty: 1,
34
+ }),
35
+ Object.freeze({
36
+ id: 'compact-memo',
37
+ family: 'memo',
38
+ purposes: ['decide', 'inspect'],
39
+ modes: ['conservative'],
40
+ evidence: ['narrative'],
41
+ densities: ['balanced', 'dense'],
42
+ novelty: 0,
43
+ }),
44
+ ]),
45
+ xlsx: Object.freeze([
46
+ Object.freeze({
47
+ id: 'monitor-dashboard',
48
+ family: 'monitor',
49
+ purposes: ['monitor'],
50
+ modes: ['conservative', 'strong-fit'],
51
+ evidence: ['metrics', 'table'],
52
+ densities: ['balanced', 'dense'],
53
+ novelty: 0,
54
+ }),
55
+ Object.freeze({
56
+ id: 'trend-dashboard',
57
+ family: 'trend',
58
+ purposes: ['monitor', 'explain'],
59
+ modes: ['strong-fit', 'divergent'],
60
+ evidence: ['chart', 'table'],
61
+ densities: ['balanced'],
62
+ novelty: 1,
63
+ }),
64
+ Object.freeze({
65
+ id: 'comparison-board',
66
+ family: 'comparison',
67
+ purposes: ['compare', 'decide'],
68
+ modes: ['strong-fit', 'divergent'],
69
+ evidence: ['chart', 'metrics'],
70
+ densities: ['balanced', 'dense'],
71
+ novelty: 1,
72
+ }),
73
+ Object.freeze({
74
+ id: 'analysis-sheet',
75
+ family: 'analysis',
76
+ purposes: ['inspect', 'explain'],
77
+ modes: ['conservative', 'strong-fit'],
78
+ evidence: ['table', 'facts'],
79
+ densities: ['dense'],
80
+ novelty: 0,
81
+ }),
82
+ Object.freeze({
83
+ id: 'narrative-scorecard',
84
+ family: 'scorecard',
85
+ purposes: ['monitor', 'decide'],
86
+ modes: ['divergent'],
87
+ evidence: ['metrics', 'claims'],
88
+ densities: ['light', 'balanced'],
89
+ novelty: 2,
90
+ }),
91
+ ]),
92
+ });
93
+
94
+ const PPTX_VARIANTS = Object.freeze({
95
+ cover: Object.freeze([
96
+ Object.freeze({ variant: 'editorial-left', family: 'editorial', modes: ['conservative', 'strong-fit'], novelty: 0 }),
97
+ Object.freeze({ variant: 'minimal-focus', family: 'minimal', modes: ['strong-fit', 'divergent'], novelty: 1 }),
98
+ ]),
99
+ statement: Object.freeze([
100
+ Object.freeze({ variant: 'metric-statement', family: 'evidence', purposes: ['decide', 'monitor'], evidence: ['metrics'], novelty: 0 }),
101
+ Object.freeze({ variant: 'typographic-statement', family: 'typography', purposes: ['decide', 'explain'], evidence: ['narrative'], novelty: 1 }),
102
+ ]),
103
+ metrics: Object.freeze([
104
+ Object.freeze({ variant: 'asymmetric-metrics', family: 'evidence', purposes: ['decide', 'monitor'], modes: ['conservative', 'strong-fit'], novelty: 0 }),
105
+ Object.freeze({ variant: 'metric-band', family: 'rhythm', purposes: ['monitor', 'explain'], modes: ['strong-fit', 'divergent'], novelty: 1 }),
106
+ ]),
107
+ comparison: Object.freeze([
108
+ Object.freeze({ variant: 'contrast-panels', family: 'comparison', modes: ['conservative', 'strong-fit'], novelty: 0 }),
109
+ Object.freeze({ variant: 'aligned-evidence', family: 'evidence', modes: ['strong-fit', 'divergent'], novelty: 1 }),
110
+ ]),
111
+ process: Object.freeze([
112
+ Object.freeze({ variant: 'horizontal-flow', family: 'process', modes: ['conservative', 'strong-fit'], novelty: 0 }),
113
+ Object.freeze({ variant: 'staggered-flow', family: 'rhythm', modes: ['strong-fit', 'divergent'], novelty: 1 }),
114
+ ]),
115
+ chart: Object.freeze([
116
+ Object.freeze({ variant: 'commentary-chart', family: 'evidence', purposes: ['decide', 'explain'], evidence: ['narrative'], novelty: 0 }),
117
+ Object.freeze({ variant: 'chart-led', family: 'data', purposes: ['monitor', 'compare'], evidence: ['chart'], modes: ['strong-fit', 'divergent'], novelty: 1 }),
118
+ ]),
119
+ table: Object.freeze([
120
+ Object.freeze({ variant: 'table-led', family: 'evidence', modes: ['conservative', 'strong-fit'], novelty: 0 }),
121
+ Object.freeze({ variant: 'table-callout', family: 'comparison', modes: ['strong-fit', 'divergent'], novelty: 1 }),
122
+ ]),
123
+ split: Object.freeze([
124
+ Object.freeze({ variant: 'visual-right', family: 'split', modes: ['conservative', 'strong-fit'], novelty: 0 }),
125
+ Object.freeze({ variant: 'visual-left', family: 'editorial', modes: ['strong-fit', 'divergent'], novelty: 1 }),
126
+ ]),
127
+ content: Object.freeze([
128
+ Object.freeze({ variant: 'evidence-right', family: 'evidence', purposes: ['decide', 'inspect'], evidence: ['visual'], novelty: 0 }),
129
+ Object.freeze({ variant: 'editorial-wide', family: 'typography', purposes: ['explain'], evidence: ['narrative'], modes: ['strong-fit', 'divergent'], novelty: 1 }),
130
+ ]),
131
+ closing: Object.freeze([
132
+ Object.freeze({ variant: 'decision-left', family: 'decision', modes: ['conservative', 'strong-fit'], novelty: 0 }),
133
+ Object.freeze({ variant: 'decision-focus', family: 'minimal', modes: ['strong-fit', 'divergent'], novelty: 1 }),
134
+ ]),
135
+ });
136
+
137
+ function plainObject(value) {
138
+ return value && typeof value === 'object' && !Array.isArray(value);
139
+ }
140
+
141
+ function stableValue(value) {
142
+ if (Array.isArray(value)) return value.map(stableValue);
143
+ if (!plainObject(value)) return value;
144
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stableValue(value[key])]));
145
+ }
146
+
147
+ function sha256(value) {
148
+ return createHash('sha256').update(JSON.stringify(stableValue(value))).digest('hex');
149
+ }
150
+
151
+ function bucket(value, boundaries = [0, 1, 3, 8]) {
152
+ const count = Math.max(0, Number(value) || 0);
153
+ if (count <= boundaries[0]) return '0';
154
+ if (count <= boundaries[1]) return '1';
155
+ if (count <= boundaries[2]) return 'few';
156
+ if (count <= boundaries[3]) return 'several';
157
+ return 'many';
158
+ }
159
+
160
+ function rowsOf(value) {
161
+ if (Array.isArray(value)) return value;
162
+ return Array.isArray(value?.values) ? value.values : [];
163
+ }
164
+
165
+ function textCount(value) {
166
+ if (Array.isArray(value)) return value.filter((entry) => String(entry ?? '').trim()).length;
167
+ return value == null || value === '' ? 0 : 1;
168
+ }
169
+
170
+ function normalizePurpose(value, fallback) {
171
+ const normalized = String(value || '').trim().toLowerCase();
172
+ return PURPOSES.has(normalized) ? normalized : fallback;
173
+ }
174
+
175
+ function normalizeExpressionMode(value) {
176
+ const normalized = String(value || '').trim().toLowerCase();
177
+ return EXPRESSION_MODES.has(normalized) ? normalized : 'strong-fit';
178
+ }
179
+
180
+ function defaultPurpose(format, input) {
181
+ if (format === 'xlsx') return 'monitor';
182
+ if (format === 'csv' || format === 'tsv') return 'inspect';
183
+ if (input?.content?.decision) return 'decide';
184
+ return 'explain';
185
+ }
186
+
187
+ function normalizedRecentCompositions(value) {
188
+ if (!Array.isArray(value)) return [];
189
+ return value
190
+ .filter(plainObject)
191
+ .slice(0, 24)
192
+ .map((entry) => ({
193
+ fingerprint: String(entry.fingerprint || ''),
194
+ format: String(entry.format || ''),
195
+ profile: String(entry.profile || ''),
196
+ purpose: String(entry.purpose || ''),
197
+ expressionMode: String(entry.expressionMode || ''),
198
+ compositionIds: Array.isArray(entry.compositionIds)
199
+ ? entry.compositionIds.map((id) => String(id || '')).filter(Boolean).slice(0, 64)
200
+ : [],
201
+ createdAt: String(entry.createdAt || ''),
202
+ }));
203
+ }
204
+
205
+ export function resolveOfficeCompositionContext(format, input = {}, {
206
+ recentCompositions = [],
207
+ } = {}) {
208
+ const normalizedFormat = String(format || '').toLowerCase();
209
+ const composition = plainObject(input.composition) ? input.composition : {};
210
+ const fallbackPurpose = defaultPurpose(normalizedFormat, input);
211
+ return {
212
+ purpose: normalizePurpose(input.purpose || composition.purpose, fallbackPurpose),
213
+ expressionMode: normalizeExpressionMode(input.expressionMode || composition.expressionMode),
214
+ recentCompositions: normalizedRecentCompositions(recentCompositions),
215
+ };
216
+ }
217
+
218
+ export function officeContentTopology(format, operation = {}, design = {}) {
219
+ const sections = Array.isArray(operation.sections) ? operation.sections : [];
220
+ const tableRows = Math.max(
221
+ rowsOf(operation.table).length,
222
+ ...sections.map((section) => rowsOf(section?.table).length),
223
+ );
224
+ const metrics = Array.isArray(operation.metrics) ? operation.metrics.length : 0;
225
+ const columns = Array.isArray(operation.columns) ? operation.columns.length : 0;
226
+ const steps = Array.isArray(operation.steps) ? operation.steps.length : 0;
227
+ const rows = Array.isArray(operation.rows) ? operation.rows.length : 0;
228
+ const chartSeries = Array.isArray(operation.chart?.series) ? operation.chart.series.length : 0;
229
+ const paragraphs = textCount(operation.body)
230
+ + textCount(operation.bullets)
231
+ + sections.reduce((total, section) => (
232
+ total + textCount(section?.paragraphs || section?.body) + textCount(section?.bullets)
233
+ ), 0);
234
+ const facts = Array.isArray(design.content?.facts) ? design.content.facts.length : 0;
235
+ const claims = Array.isArray(design.content?.claims) ? design.content.claims.length : 0;
236
+ const evidence = [];
237
+ if (metrics) evidence.push('metrics');
238
+ if (tableRows || rows) evidence.push('table');
239
+ if (operation.chart || chartSeries) evidence.push('chart');
240
+ if (operation.image || operation.imagePath) evidence.push('image');
241
+ if (operation.visualText) evidence.push('visual');
242
+ if (columns) evidence.push('comparison');
243
+ if (steps) evidence.push('process');
244
+ if (paragraphs) evidence.push('narrative');
245
+ if (facts) evidence.push('facts');
246
+ if (claims) evidence.push('claims');
247
+ const weight = metrics * 2 + columns * 2 + steps + Math.ceil((rows + tableRows) / 4) + paragraphs + claims;
248
+ const density = weight >= 14 ? 'dense' : weight <= 4 ? 'light' : 'balanced';
249
+ const signature = [
250
+ String(format || '').toLowerCase(),
251
+ `m:${bucket(metrics)}`,
252
+ `c:${bucket(columns)}`,
253
+ `s:${bucket(steps)}`,
254
+ `r:${bucket(rows + tableRows, [0, 3, 10, 24])}`,
255
+ `p:${bucket(paragraphs)}`,
256
+ `e:${evidence.sort().join(',')}`,
257
+ ].join('|');
258
+ return {
259
+ metrics,
260
+ columns,
261
+ steps,
262
+ rows,
263
+ tableRows,
264
+ chartSeries,
265
+ paragraphs,
266
+ sections: sections.length,
267
+ facts,
268
+ claims,
269
+ evidence,
270
+ density,
271
+ signature,
272
+ };
273
+ }
274
+
275
+ function recentPenalty(context, candidateId) {
276
+ return context.recentCompositions.slice(0, 8).reduce((total, entry, index) => (
277
+ entry.compositionIds.includes(candidateId)
278
+ ? total + Math.max(1, 4 - Math.floor(index / 2))
279
+ : total
280
+ ), 0);
281
+ }
282
+
283
+ function stableTie(seed, id) {
284
+ return Number.parseInt(sha256(`${seed}\0${id}`).slice(0, 8), 16) / 0xFFFFFFFF;
285
+ }
286
+
287
+ function selectCandidate(candidates, {
288
+ context,
289
+ topology,
290
+ density,
291
+ seed,
292
+ usage,
293
+ explicitVariant = '',
294
+ }) {
295
+ const evidence = new Set(topology.evidence);
296
+ const ranked = candidates.map((candidate) => {
297
+ const candidateId = String(candidate.id || candidate.variant || '');
298
+ let score = 0;
299
+ if (candidate.purposes?.includes(context.purpose)) score += 18;
300
+ else if (candidate.purposes?.length) score -= 6;
301
+ if (candidate.modes?.includes(context.expressionMode)) score += 5;
302
+ if (candidate.densities?.includes(density || topology.density)) score += 3;
303
+ score += (candidate.evidence || []).filter((entry) => evidence.has(entry)).length * 3;
304
+ if (context.expressionMode === 'divergent') score += Number(candidate.novelty || 0) * 3;
305
+ if (context.expressionMode === 'conservative') score -= Number(candidate.novelty || 0) * 2;
306
+ if (explicitVariant && (candidate.variant === explicitVariant || candidate.id === explicitVariant)) score += 100;
307
+ const historyPenalty = recentPenalty(context, candidateId);
308
+ score -= historyPenalty;
309
+ score -= (usage.get(candidateId) || 0) * 7;
310
+ score += stableTie(seed, candidateId);
311
+ return { candidate, candidateId, score, historyPenalty };
312
+ });
313
+ ranked.sort((left, right) => right.score - left.score || left.candidateId.localeCompare(right.candidateId));
314
+ return ranked[0];
315
+ }
316
+
317
+ function resolvedPptxKind(operation, topology) {
318
+ const requested = String(operation.kind || 'content').toLowerCase();
319
+ if (requested !== 'content' || operation.layoutId) return requested;
320
+ if (operation.chart) return 'chart';
321
+ if (operation.table) return 'table';
322
+ if (topology.metrics >= 2) return 'metrics';
323
+ if (topology.columns >= 2) return 'comparison';
324
+ if (topology.steps >= 2) return 'process';
325
+ if (operation.image || operation.imagePath) return 'split';
326
+ return 'content';
327
+ }
328
+
329
+ export function planOfficeComposition(format, operation = {}, design = {}, {
330
+ usage = new Map(),
331
+ } = {}) {
332
+ const normalizedFormat = String(format || '').toLowerCase();
333
+ const topology = officeContentTopology(normalizedFormat, operation, design);
334
+ const context = {
335
+ purpose: normalizePurpose(operation.purpose, design.purpose || 'explain'),
336
+ expressionMode: normalizeExpressionMode(operation.expressionMode || design.expressionMode),
337
+ recentCompositions: normalizedRecentCompositions(design.recentCompositions),
338
+ };
339
+ const seed = design.content?.fingerprint
340
+ || [design.intent, design.audience, design.signature, operation.title, topology.signature].join('|');
341
+ if (normalizedFormat === 'pptx') {
342
+ const kind = resolvedPptxKind(operation, topology);
343
+ const explicitVariant = String(operation.variant || '').trim().toLowerCase();
344
+ const variants = PPTX_VARIANTS[kind] || PPTX_VARIANTS.content;
345
+ const candidates = explicitVariant && !variants.some((entry) => entry.variant === explicitVariant)
346
+ ? [...variants, { variant: explicitVariant, family: kind, novelty: 0 }]
347
+ : variants;
348
+ const selected = selectCandidate(candidates.map((entry) => ({
349
+ ...entry,
350
+ id: `${kind}:${entry.variant}`,
351
+ purposes: entry.purposes || [context.purpose],
352
+ })), {
353
+ context,
354
+ topology,
355
+ density: design.density,
356
+ seed,
357
+ usage,
358
+ explicitVariant,
359
+ });
360
+ const result = {
361
+ id: selected.candidateId,
362
+ family: selected.candidate.family,
363
+ kind,
364
+ variant: selected.candidate.variant,
365
+ purpose: context.purpose,
366
+ expressionMode: context.expressionMode,
367
+ topology,
368
+ historyPenalty: selected.historyPenalty,
369
+ source: explicitVariant ? 'explicit' : 'content-planner',
370
+ };
371
+ usage.set(result.id, (usage.get(result.id) || 0) + 1);
372
+ return result;
373
+ }
374
+ const candidates = FORMAT_CANDIDATES[normalizedFormat] || [{
375
+ id: `${normalizedFormat || 'office'}-default`,
376
+ family: 'default',
377
+ purposes: [context.purpose],
378
+ modes: [context.expressionMode],
379
+ novelty: 0,
380
+ }];
381
+ const selected = selectCandidate(candidates, {
382
+ context,
383
+ topology,
384
+ density: design.density,
385
+ seed,
386
+ usage,
387
+ explicitVariant: String(operation.variant || '').trim().toLowerCase(),
388
+ });
389
+ const result = {
390
+ id: selected.candidateId,
391
+ family: selected.candidate.family,
392
+ purpose: context.purpose,
393
+ expressionMode: context.expressionMode,
394
+ topology,
395
+ historyPenalty: selected.historyPenalty,
396
+ source: operation.variant ? 'explicit' : 'content-planner',
397
+ };
398
+ usage.set(result.id, (usage.get(result.id) || 0) + 1);
399
+ return result;
400
+ }
401
+
402
+ export function summarizeOfficeCompositions(format, compositions = []) {
403
+ const normalized = (Array.isArray(compositions) ? compositions : [])
404
+ .map((entry) => entry?.composition || entry)
405
+ .filter((entry) => plainObject(entry) && entry.id)
406
+ .map((entry) => ({
407
+ id: String(entry.id),
408
+ family: String(entry.family || ''),
409
+ kind: String(entry.kind || ''),
410
+ purpose: String(entry.purpose || ''),
411
+ topology: String(entry.topology?.signature || ''),
412
+ }));
413
+ if (!normalized.length) {
414
+ return {
415
+ format: String(format || '').toLowerCase(),
416
+ fingerprint: '',
417
+ compositionIds: [],
418
+ count: 0,
419
+ };
420
+ }
421
+ return {
422
+ format: String(format || '').toLowerCase(),
423
+ fingerprint: sha256({ format: String(format || '').toLowerCase(), sequence: normalized }),
424
+ compositionIds: normalized.map((entry) => entry.id),
425
+ count: normalized.length,
426
+ };
427
+ }
428
+
429
+ export function reviewOfficeCompositionSequence({
430
+ format,
431
+ compositions = [],
432
+ recentCompositions = [],
433
+ allowRepetition = false,
434
+ } = {}) {
435
+ const plans = (Array.isArray(compositions) ? compositions : [])
436
+ .map((entry) => entry?.composition || entry)
437
+ .filter((entry) => plainObject(entry) && entry.id);
438
+ const summary = summarizeOfficeCompositions(format, plans);
439
+ if (allowRepetition || !plans.length) return { summary, repeated: null, recentMatch: null };
440
+ const eligible = plans.filter((entry) => !['cover', 'closing'].includes(String(entry.kind || '')));
441
+ const counts = new Map();
442
+ for (const entry of eligible) counts.set(entry.id, (counts.get(entry.id) || 0) + 1);
443
+ const [repeatedId = '', repeatedCount = 0] = [...counts.entries()]
444
+ .sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))[0] || [];
445
+ const repeated = eligible.length >= 4 && repeatedCount / eligible.length >= 0.75
446
+ ? { id: repeatedId, count: repeatedCount, total: eligible.length }
447
+ : null;
448
+ const recentMatch = normalizedRecentCompositions(recentCompositions)
449
+ .find((entry) => entry.fingerprint && entry.fingerprint === summary.fingerprint) || null;
450
+ return { summary, repeated, recentMatch };
451
+ }