@qvac/harness 0.8.0 → 0.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +43 -1
  2. package/dist-lib/lib/docx/index.d.ts +1 -1
  3. package/dist-lib/lib/docx/index.js +16 -24
  4. package/dist-lib/lib/office/extract.d.ts +9 -0
  5. package/dist-lib/lib/office/extract.js +19 -0
  6. package/dist-lib/lib/office/index.d.ts +1 -1
  7. package/dist-lib/lib/office/index.js +74 -19
  8. package/dist-lib/lib/tools/exec/adapters/python/module-distributions.js +0 -3
  9. package/dist-lib/lib/tools/exec/adapters/python/python-adapter.d.ts +7 -50
  10. package/dist-lib/lib/tools/exec/adapters/python/python-adapter.js +38 -695
  11. package/dist-lib/lib/tools/exec/adapters/python/run-session.js +1 -85
  12. package/dist-lib/lib/tools/exec.js +9 -20
  13. package/dist-lib/lib/tools/office/ops.d.ts +13 -0
  14. package/dist-lib/lib/tools/office/ops.js +100 -0
  15. package/dist-lib/lib/tools/office.d.ts +12 -0
  16. package/dist-lib/lib/tools/office.js +486 -0
  17. package/package.json +10 -6
  18. package/wheels/manifest.json +1 -160
  19. package/worker/harness-sidecar.mjs +3 -1
  20. package/dist-lib/lib/office/pptx.d.ts +0 -1
  21. package/dist-lib/lib/office/pptx.js +0 -26
  22. package/dist-lib/lib/office/python.d.ts +0 -8
  23. package/dist-lib/lib/office/python.js +0 -32
  24. package/dist-lib/lib/office/xlsx.d.ts +0 -1
  25. package/dist-lib/lib/office/xlsx.js +0 -26
  26. package/dist-lib/lib/tools/exec/adapters/python/create-effect.d.ts +0 -7
  27. package/dist-lib/lib/tools/exec/adapters/python/create-effect.js +0 -318
  28. package/wheels/cffi-2.0.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl +0 -0
  29. package/wheels/cryptography-47.0.0-cp314-abi3-pyemscripten_2026_0_wasm32.whl +0 -0
  30. package/wheels/defusedxml-0.7.1-py2.py3-none-any.whl +0 -0
  31. package/wheels/et_xmlfile-2.0.0-py3-none-any.whl +0 -0
  32. package/wheels/fonttools-4.62.1-py3-none-any.whl +0 -0
  33. package/wheels/fpdf2-2.8.8-py3-none-any.whl +0 -0
  34. package/wheels/lxml-6.0.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl +0 -0
  35. package/wheels/openpyxl-3.1.5-py2.py3-none-any.whl +0 -0
  36. package/wheels/pillow-12.2.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl +0 -0
  37. package/wheels/pycparser-3.0-py3-none-any.whl +0 -0
  38. package/wheels/pypdf-6.15.0-py3-none-any.whl +0 -0
  39. package/wheels/python_docx-1.2.0-py3-none-any.whl +0 -0
  40. package/wheels/python_pptx-1.0.2-py3-none-any.whl +0 -0
  41. package/wheels/six-1.17.0-py2.py3-none-any.whl +0 -0
  42. package/wheels/typing_extensions-4.15.0-py3-none-any.whl +0 -0
  43. package/wheels/xlsxwriter-3.2.9-py3-none-any.whl +0 -0
@@ -105,72 +105,6 @@ async function runOnce(py, code, opts) {
105
105
  }
106
106
  return retry;
107
107
  }
108
- const FIT_DECKS = `
109
- def _qvac_fit_decks(names):
110
- try:
111
- from pptx import Presentation
112
- from pptx.oxml.ns import qn
113
- except Exception:
114
- return
115
- for name in names:
116
- if not name.lower().endswith('.pptx'):
117
- continue
118
- try:
119
- prs = Presentation(name)
120
- except Exception:
121
- continue
122
- slide_height = prs.slide_height
123
- changed = False
124
- for slide in prs.slides:
125
- for shape in slide.shapes:
126
- if not shape.has_text_frame:
127
- continue
128
- tf = shape.text_frame
129
- text = tf.text or ''
130
- if not text.strip():
131
- continue
132
- tf.word_wrap = True
133
- # Estimated wrapped lines against the box: a character at N pt is
134
- # about 0.5 N wide and a line about 1.2 N tall. Rough on purpose —
135
- # it only has to pick a scale, and the renderer does the real layout.
136
- width_pt = (shape.width or 0) / 12700.0
137
- height_pt = (shape.height or slide_height) / 12700.0
138
- lines = 0
139
- tallest = 0.0
140
- for p in tf.paragraphs:
141
- # An unsized paragraph inherits from the layout, and the bundled
142
- # template's body is 28pt — assuming something smaller here is
143
- # what let a seven-bullet slide measure as fitting.
144
- size = p.font.size.pt if p.font.size is not None else 28.0
145
- tallest = max(tallest, size)
146
- per_line = max(1.0, width_pt / (size * 0.5))
147
- lines += max(1, int(len(p.text or '') / per_line) + 1)
148
- needed = lines * max(tallest, 12.0) * 1.2
149
- scale = 1.0 if needed <= height_pt or height_pt <= 0 else height_pt / needed
150
- # 55% is the floor: below that the deck is unreadable from a room and
151
- # the honest answer is fewer bullets, not smaller type.
152
- scale = max(0.55, min(1.0, scale))
153
- bodyPr = tf._txBody.bodyPr
154
- for tag in ('a:normAutofit', 'a:spAutoFit', 'a:noAutofit'):
155
- found = bodyPr.find(qn(tag))
156
- if found is not None:
157
- bodyPr.remove(found)
158
- bodyPr.append(
159
- bodyPr.makeelement(
160
- qn('a:normAutofit'),
161
- {
162
- 'fontScale': str(int(scale * 100000)),
163
- 'lnSpcReduction': '0' if scale >= 1.0 else '10000'
164
- }
165
- )
166
- )
167
- changed = True
168
- if changed:
169
- try:
170
- prs.save(name)
171
- except Exception:
172
- pass
173
- `;
174
108
  class InstallTimeoutError extends Error {
175
109
  }
176
110
  function installSource(py, packages) {
@@ -213,21 +147,6 @@ function listFiles(py, dir) {
213
147
  function newDeliverables(before, after) {
214
148
  return [...after].filter((name) => !before.has(name) && DELIVERABLE.test(name));
215
149
  }
216
- function fitDecks(py, outputs) {
217
- if (!outputs.some((name) => name.toLowerCase().endsWith('.pptx'))) {
218
- return false;
219
- }
220
- try {
221
- py.runPython(`${FIT_DECKS}\n_qvac_fit_decks(${JSON.stringify(outputs)})`);
222
- return false;
223
- }
224
- catch (error) {
225
- hlog('python: deck fit skipped', {
226
- err: errorMessage(error)
227
- });
228
- return isPyodideFatal(error);
229
- }
230
- }
231
150
  export async function performRun(py, spec, opts = {}) {
232
151
  const isCancelled = opts.isCancelled ?? (() => false);
233
152
  const ask = opts.askConnect ?? (async () => false);
@@ -297,10 +216,7 @@ export async function performRun(py, spec, opts = {}) {
297
216
  maxOutputChars: spec.maxOutputChars,
298
217
  access
299
218
  });
300
- let evict = fatal;
301
- if (result.exitCode === 0) {
302
- evict = fitDecks(py, spec.outputs ?? []);
303
- }
219
+ const evict = fatal;
304
220
  if (evict) {
305
221
  return { result: { ...result, missing: spec.outputs ?? [] }, evict };
306
222
  }
@@ -1,8 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { AbortedError } from '../helpers/race-abort.js';
4
- import { checkPdfBuildSource, checkBareLengthArgs, checkPptxBuildSource, checkPythonSource, checkUnstagedRead, createPythonAdapter, isPythonLanguage, parseInputs, parseOutputs, parsePackages, parseWorkdir, pinned, runsThroughPyodide, sandboxInstallError, unwrapPythonSource } from './exec/adapters/python/python-adapter.js';
5
- import { pythonCreateEffect } from './exec/adapters/python/create-effect.js';
4
+ import { checkPythonSource, createPythonAdapter, isPythonLanguage, officeImportError, parseInputs, parseOutputs, parsePackages, parseWorkdir, pinned, runsThroughPyodide, sandboxInstallError, unwrapPythonSource } from './exec/adapters/python/python-adapter.js';
6
5
  import { namedHosts } from './exec/adapters/python/named-hosts.js';
7
6
  import { pythonReachesNetwork } from './exec/adapters/python/net-shim.js';
8
7
  import { createShellAdapter } from './exec/adapters/shell/shell-adapter.js';
@@ -422,7 +421,7 @@ export function createExecTool({ spawn, killProcess, loadPyodide, disablePyodide
422
421
  return 'read';
423
422
  }
424
423
  if (runsThroughPyodide(args.language, disablePyodide)) {
425
- return pythonCreateEffect(args);
424
+ return 'write';
426
425
  }
427
426
  return shellEffect(String(args.command ?? '').trim(), ctx?.toolOps);
428
427
  },
@@ -459,6 +458,13 @@ export function createExecTool({ spawn, killProcess, loadPyodide, disablePyodide
459
458
  if (packages && 'error' in packages) {
460
459
  return packages.error;
461
460
  }
461
+ const office = officeImportError(unwrapPythonSource(command) ?? command, {
462
+ packages,
463
+ toolNames: ctx?.toolNames
464
+ });
465
+ if (office) {
466
+ return office;
467
+ }
462
468
  const refusal = ctx?.sandboxMode === true ? sandboxInstallError(command, packages) : null;
463
469
  if (refusal) {
464
470
  return refusal;
@@ -468,23 +474,6 @@ export function createExecTool({ spawn, killProcess, loadPyodide, disablePyodide
468
474
  if (sourceError) {
469
475
  return sourceError;
470
476
  }
471
- const pptxError = checkPptxBuildSource(unwrapPythonSource(command) ?? command, outputs, packages);
472
- if (pptxError) {
473
- return pptxError;
474
- }
475
- const pdfError = checkPdfBuildSource(unwrapPythonSource(command) ?? command, outputs, packages);
476
- if (pdfError) {
477
- return pdfError;
478
- }
479
- const bareLength = checkBareLengthArgs(unwrapPythonSource(command) ?? command, packages);
480
- if (bareLength) {
481
- return bareLength;
482
- }
483
- const inputs = parseInputs(rawArgs.inputs);
484
- const unstagedRead = checkUnstagedRead(unwrapPythonSource(command) ?? command, Array.isArray(inputs) ? inputs : undefined);
485
- if (unstagedRead) {
486
- return unstagedRead;
487
- }
488
477
  }
489
478
  }
490
479
  const parsed = parsePackages(rawArgs.packages);
@@ -0,0 +1,13 @@
1
+ export type Slot = string | readonly string[];
2
+ export interface OpSpec {
3
+ readonly slots: readonly Slot[];
4
+ readonly inches?: boolean;
5
+ readonly query?: boolean;
6
+ readonly aliases?: Readonly<Record<string, string>>;
7
+ }
8
+ export declare const PPTX_OPS: Readonly<Record<string, OpSpec>>;
9
+ export declare const DOCX_OPS: Readonly<Record<string, OpSpec>>;
10
+ export declare const XLSX_OPS: Readonly<Record<string, OpSpec>>;
11
+ export declare function opKeys(spec: OpSpec): string[];
12
+ export declare function withAliases(spec: OpSpec, op: Readonly<Record<string, unknown>>): Record<string, unknown>;
13
+ export declare function opArguments(spec: OpSpec, op: Readonly<Record<string, unknown>>, toEmu: (inches: number) => number): unknown[];
@@ -0,0 +1,100 @@
1
+ const TABLE_INDEX = { index: 'table' };
2
+ const BOX = ['x', 'y', 'width', 'height'];
3
+ const CHART = ['type', 'categories', 'series', 'title', 'stacked', 'legend', 'name'];
4
+ export const PPTX_OPS = {
5
+ read: { slots: [], query: true },
6
+ layouts: { slots: [], query: true },
7
+ setText: { slots: [['slide', 'shape'], 'text'] },
8
+ setCellText: { slots: [['slide', 'shape', 'row', 'column'], 'text'] },
9
+ setStyle: { slots: [['slide', 'shape'], 'style', ['paragraph']] },
10
+ addSlide: { slots: [['like', 'layout', 'at', 'title', 'body']] },
11
+ addPicture: { slots: ['slide', 'image', [...BOX, 'name', 'description']], inches: true },
12
+ addTextBox: { slots: ['slide', [...BOX, 'text', 'style', 'fill', 'name']], inches: true },
13
+ addTable: { slots: ['slide', [...BOX, 'rows', 'header', 'name']], inches: true },
14
+ addChart: { slots: ['slide', [...BOX, ...CHART]], inches: true },
15
+ setChartData: {
16
+ slots: [
17
+ ['slide', 'shape'],
18
+ ['categories', 'series']
19
+ ]
20
+ },
21
+ setChartTitle: { slots: [['slide', 'shape'], 'title'] },
22
+ setBackground: { slots: ['slide', 'color'] },
23
+ deleteSlide: { slots: ['slide'] },
24
+ moveSlide: { slots: ['from', ['to']] }
25
+ };
26
+ export const DOCX_OPS = {
27
+ read: { slots: [], query: true },
28
+ sections: { slots: [], query: true },
29
+ paragraphStyles: { slots: [], query: true },
30
+ setText: { slots: ['index', 'text'] },
31
+ insertParagraphs: { slots: ['at', 'paragraphs'] },
32
+ appendParagraphs: { slots: ['paragraphs'] },
33
+ replaceSection: { slots: ['index', ['title', 'body']] },
34
+ deleteSection: { slots: ['index'] },
35
+ deleteBlocks: { slots: ['index', ['count']] },
36
+ formatParagraph: { slots: ['index', 'format'] },
37
+ formatStyle: { slots: ['name', 'format'] },
38
+ addTable: { slots: ['at', ['rows', 'header', 'style']] },
39
+ setCell: { slots: [['table', 'row', 'column'], 'text'], aliases: TABLE_INDEX },
40
+ addRow: { slots: ['table', 'values', ['at']], aliases: TABLE_INDEX },
41
+ deleteRow: { slots: [['table', 'row']], aliases: TABLE_INDEX },
42
+ addImage: {
43
+ slots: ['at', 'image', ['width', 'height', 'align', 'name', 'description']],
44
+ inches: true
45
+ },
46
+ addChart: { slots: ['at', ['width', 'height', ...CHART]], inches: true },
47
+ setChartData: { slots: ['index', ['categories', 'series'], ['chart']] },
48
+ setChartTitle: { slots: ['index', 'title', ['chart']] }
49
+ };
50
+ export const XLSX_OPS = {
51
+ sheets: { slots: [], query: true },
52
+ read: { slots: ['sheet', ['range', 'limit']], query: true },
53
+ get: { slots: ['sheet', 'ref'], query: true },
54
+ setCells: { slots: ['sheet', 'cells'] },
55
+ appendRows: { slots: ['sheet', 'rows'] },
56
+ insertRows: { slots: ['sheet', ['at', 'rows', 'count']] },
57
+ deleteRows: { slots: ['sheet', ['at', 'count']] },
58
+ deleteColumns: { slots: ['sheet', ['column', 'count']] },
59
+ setColumnWidth: { slots: ['sheet', ['column', 'width']] },
60
+ addSheet: { slots: ['name', ['at']] },
61
+ renameSheet: { slots: ['sheet', 'name'] },
62
+ deleteSheet: { slots: ['sheet'] },
63
+ addImage: {
64
+ slots: ['sheet', 'image', ['cell', 'width', 'height', 'name', 'description']],
65
+ inches: true
66
+ },
67
+ removeMacros: { slots: [] }
68
+ };
69
+ const LENGTHS = new Set(BOX);
70
+ export function opKeys(spec) {
71
+ const keys = spec.slots.flatMap((slot) => (typeof slot === 'string' ? [slot] : [...slot]));
72
+ return [...keys, ...Object.keys(spec.aliases ?? {})];
73
+ }
74
+ export function withAliases(spec, op) {
75
+ const named = { ...op };
76
+ for (const [alias, key] of Object.entries(spec.aliases ?? {})) {
77
+ if (alias in named && named[key] === undefined) {
78
+ named[key] = named[alias];
79
+ }
80
+ delete named[alias];
81
+ }
82
+ return named;
83
+ }
84
+ export function opArguments(spec, op, toEmu) {
85
+ const value = (key) => {
86
+ const raw = op[key];
87
+ return spec.inches && LENGTHS.has(key) && typeof raw === 'number' ? toEmu(raw) : raw;
88
+ };
89
+ const args = spec.slots.map((slot) => {
90
+ if (typeof slot === 'string') {
91
+ return value(slot);
92
+ }
93
+ const present = slot.filter((key) => op[key] !== undefined);
94
+ return present.length ? Object.fromEntries(present.map((key) => [key, value(key)])) : undefined;
95
+ });
96
+ while (args.length && args.at(-1) === undefined) {
97
+ args.pop();
98
+ }
99
+ return args;
100
+ }
@@ -0,0 +1,12 @@
1
+ import type { ToolDefinition } from '../capabilities/completion.ts';
2
+ import { pptx, xlsx } from '../office/engine/index.ts';
3
+ type Deck = ReturnType<typeof pptx.open>;
4
+ type Workbook = ReturnType<typeof xlsx.open>;
5
+ type FormatName = 'pptx' | 'docx' | 'xlsx';
6
+ type Args = Record<string, unknown>;
7
+ export declare function shapeFor(deck: Deck, op: Args): Args;
8
+ export declare function sheetFor(book: Workbook, op: Args, created: boolean): Args;
9
+ export declare function normalizeArgs(args: Args): Args;
10
+ export declare function revisedName(format: FormatName, fileName: string): string;
11
+ export declare function createOfficeTools(): ToolDefinition[];
12
+ export {};